完成初步框架
This commit is contained in:
@@ -1,29 +1,16 @@
|
||||
export default defineAppConfig({
|
||||
app: {
|
||||
name: "Nuxtor",
|
||||
author: "Nicola Spadari",
|
||||
repo: "https://github.com/NicolaSpadari/nuxtor",
|
||||
tauriSite: "https://tauri.app",
|
||||
nuxtSite: "https://nuxt.com",
|
||||
nuxtUiSite: "https://ui.nuxt.dev"
|
||||
name: "Video Player",
|
||||
author: "Your Name",
|
||||
version: "1.0.0",
|
||||
description: "Desktop video player with remote control"
|
||||
},
|
||||
pageCategories: {
|
||||
system: {
|
||||
label: "System",
|
||||
icon: "lucide:square-terminal"
|
||||
},
|
||||
storage: {
|
||||
label: "Storage",
|
||||
icon: "lucide:archive"
|
||||
},
|
||||
interface: {
|
||||
label: "Interface",
|
||||
icon: "lucide:app-window-mac"
|
||||
},
|
||||
other: {
|
||||
label: "Other",
|
||||
icon: "lucide:folder"
|
||||
}
|
||||
player: {
|
||||
defaultVolume: 50,
|
||||
autoFullscreen: true,
|
||||
showControls: false,
|
||||
webSocketPort: 8080,
|
||||
reconnectInterval: 5000
|
||||
},
|
||||
ui: {
|
||||
colors: {
|
||||
|
@@ -1,10 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<SiteNavbar class="sticky bg-(--ui-bg)/75 backdrop-blur" />
|
||||
<SiteSidebar />
|
||||
|
||||
<UContainer>
|
||||
<slot />
|
||||
</UContainer>
|
||||
<div class="h-screen w-screen overflow-hidden bg-black">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
@@ -1,4 +1,6 @@
|
||||
import * as tauriApp from "@tauri-apps/api/app";
|
||||
import * as tauriCore from "@tauri-apps/api/core";
|
||||
import * as tauriEvent from "@tauri-apps/api/event";
|
||||
import * as tauriWebviewWindow from "@tauri-apps/api/webviewWindow";
|
||||
import * as tauriFs from "@tauri-apps/plugin-fs";
|
||||
import * as tauriNotification from "@tauri-apps/plugin-notification";
|
||||
@@ -13,6 +15,8 @@ const capitalize = (name: string) => {
|
||||
|
||||
const tauriModules = [
|
||||
{ module: tauriApp, prefix: "App", importPath: "@tauri-apps/api/app" },
|
||||
{ module: tauriCore, prefix: "Core", importPath: "@tauri-apps/api/core" },
|
||||
{ module: tauriEvent, prefix: "Event", importPath: "@tauri-apps/api/event" },
|
||||
{ module: tauriWebviewWindow, prefix: "WebviewWindow", importPath: "@tauri-apps/api/webviewWindow" },
|
||||
{ module: tauriShell, prefix: "Shell", importPath: "@tauri-apps/plugin-shell" },
|
||||
{ module: tauriOs, prefix: "Os", importPath: "@tauri-apps/plugin-os" },
|
||||
|
@@ -1,26 +0,0 @@
|
||||
<template>
|
||||
<div class="grid place-items-center py-12 md:py-24">
|
||||
<div class="flex flex-col items-center gap-y-4 md:gap-y-8">
|
||||
<p class="text-(--ui-success) font-semibold">
|
||||
404
|
||||
</p>
|
||||
<div class="text-center space-y-3">
|
||||
<h1 class="text-3xl font-bold tracking-tight" sm="text-5xl">
|
||||
Page not found
|
||||
</h1>
|
||||
<p class="text-base text-(--ui-muted) leading-7">
|
||||
Sorry, we couldn't find the page you're looking for.
|
||||
</p>
|
||||
</div>
|
||||
<UButton to="/" variant="outline" size="lg" :ui="{ base: 'px-5' }">
|
||||
Go home
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
definePageMeta({
|
||||
layout: "blank"
|
||||
});
|
||||
</script>
|
@@ -1,63 +0,0 @@
|
||||
<template>
|
||||
<LayoutTile
|
||||
title="Commands"
|
||||
description="Access the system shell. Allows you to spawn child processes and manage files and URLs using their default application."
|
||||
>
|
||||
<div class="space-y-6 md:space-y-8">
|
||||
<UForm :state="inputState" :schema="schema" class="flex flex-col gap-y-4 items-end" @submit="sendCommand">
|
||||
<UFormField label="Command input" name="input">
|
||||
<UInput v-model="inputState.input" variant="subtle" size="lg" />
|
||||
</UFormField>
|
||||
|
||||
<UButton type="submit" size="lg">
|
||||
Send command
|
||||
</UButton>
|
||||
</UForm>
|
||||
|
||||
<UForm :state="outputState" class="flex flex-col gap-y-4 items-end">
|
||||
<UFormField label="Command output" name="command-output">
|
||||
<UTextarea v-model="outputState.output" variant="subtle" size="lg" :rows="8" readonly />
|
||||
</UFormField>
|
||||
</UForm>
|
||||
</div>
|
||||
</LayoutTile>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
definePageMeta({
|
||||
name: "Shell commands",
|
||||
icon: "lucide:terminal",
|
||||
description: "Execute shell commands",
|
||||
category: "system"
|
||||
});
|
||||
|
||||
const schema = z.object({
|
||||
input: z.string({
|
||||
error: "Input is required"
|
||||
}).nonempty()
|
||||
});
|
||||
|
||||
type Schema = zInfer<typeof schema>;
|
||||
|
||||
const inputState = ref<Partial<Schema>>({
|
||||
input: undefined
|
||||
});
|
||||
const outputState = ref({
|
||||
output: ""
|
||||
});
|
||||
|
||||
const sendCommand = async () => {
|
||||
try {
|
||||
const response = await useTauriShellCommand.create("exec-sh", [
|
||||
"-c",
|
||||
inputState.value.input!
|
||||
]).execute();
|
||||
|
||||
outputState.value.output = JSON.stringify(response, null, 4);
|
||||
} catch (error) {
|
||||
outputState.value.output = JSON.stringify(error, null, 4);
|
||||
} finally {
|
||||
inputState.value.input = undefined;
|
||||
}
|
||||
};
|
||||
</script>
|
@@ -1,83 +0,0 @@
|
||||
<template>
|
||||
<LayoutTile
|
||||
title="File System"
|
||||
description="Access the file system. For this demo the only allowed permission is read/write to the Documents folder (no sub directories)."
|
||||
>
|
||||
<UForm :state="inputState" :schema="schema" class="flex flex-col gap-y-4 items-end" @submit="createFile">
|
||||
<UFormField label="Text file name (with extension)" name="fileName">
|
||||
<UInput v-model="inputState.fileName" variant="subtle" size="lg" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="File content" name="fileContent">
|
||||
<UTextarea v-model="inputState.fileContent" variant="subtle" size="lg" :rows="10" />
|
||||
</UFormField>
|
||||
|
||||
<UButton type="submit" size="lg">
|
||||
Create file
|
||||
</UButton>
|
||||
</UForm>
|
||||
</LayoutTile>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
definePageMeta({
|
||||
name: "Files",
|
||||
icon: "lucide:file-text",
|
||||
category: "storage",
|
||||
description: "Create and manage files"
|
||||
});
|
||||
|
||||
const schema = z.object({
|
||||
fileName: z.string({
|
||||
error: "File name is required"
|
||||
}).nonempty().regex(/^[\w,\s-]+\.[A-Z0-9]+$/i, {
|
||||
message: "Invalid filename format"
|
||||
}),
|
||||
fileContent: z.string({
|
||||
error: "File content is required"
|
||||
}).nonempty()
|
||||
});
|
||||
|
||||
type Schema = zInfer<typeof schema>;
|
||||
|
||||
const inputState = ref<Partial<Schema>>({
|
||||
fileName: undefined,
|
||||
fileContent: undefined
|
||||
});
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const createFile = async () => {
|
||||
try {
|
||||
const fileExists = await useTauriFsExists(inputState.value.fileName!, {
|
||||
baseDir: useTauriFsBaseDirectory.Document
|
||||
});
|
||||
|
||||
if (fileExists) {
|
||||
toast.add({
|
||||
title: "Error",
|
||||
description: "The file already exists",
|
||||
color: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await useTauriFsWriteTextFile(inputState.value.fileName!, inputState.value.fileContent!, {
|
||||
baseDir: useTauriFsBaseDirectory.Document
|
||||
});
|
||||
toast.add({
|
||||
title: "Success",
|
||||
description: "The file has been created",
|
||||
color: "success"
|
||||
});
|
||||
inputState.value.fileName = inputState.value.fileContent = undefined;
|
||||
} catch (err) {
|
||||
toast.add({
|
||||
title: "Error",
|
||||
description: String(err),
|
||||
color: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
@@ -1,73 +1,373 @@
|
||||
<template>
|
||||
<UContainer class="relative overflow-hidden h-screen">
|
||||
<div class="grid size-full place-content-center gap-y-8">
|
||||
<SvgoLogo :filled="true" :font-controlled="false" class="mx-auto size-40" />
|
||||
<div class="h-screen w-screen flex items-center justify-center bg-black text-white relative overflow-hidden">
|
||||
<!-- Connection Status -->
|
||||
<div class="absolute top-4 right-4 z-10">
|
||||
<div class="flex items-center gap-2 px-3 py-1 rounded-full text-sm" :class="connectionStatusClass">
|
||||
<div class="w-2 h-2 rounded-full" :class="connectionDotClass"></div>
|
||||
{{ connectionStatus }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center gap-y-3">
|
||||
<h1 class="animate-pulse text-3xl sm:text-4xl text-pretty font-bold font-heading md:mb-5">
|
||||
{{ app.name.toUpperCase() }}
|
||||
<!-- Video Player Container -->
|
||||
<div class="w-full h-full flex items-center justify-center relative">
|
||||
<!-- Video Element -->
|
||||
<video
|
||||
v-if="currentVideo"
|
||||
ref="videoElement"
|
||||
class="w-full h-full object-contain"
|
||||
:src="currentVideo.path"
|
||||
:volume="volume / 100"
|
||||
:loop="isLooping"
|
||||
autoplay
|
||||
@loadedmetadata="onVideoLoaded"
|
||||
@timeupdate="onTimeUpdate"
|
||||
@ended="onVideoEnded"
|
||||
@error="onVideoError"
|
||||
/>
|
||||
|
||||
<!-- Waiting Screen -->
|
||||
<div v-else class="flex flex-col items-center justify-center space-y-8">
|
||||
<!-- Logo or App Icon -->
|
||||
<div class="w-32 h-32 rounded-full bg-gray-800 flex items-center justify-center mb-8">
|
||||
<UIcon name="lucide:play-circle" class="w-16 h-16 text-gray-400" />
|
||||
</div>
|
||||
|
||||
<!-- App Name -->
|
||||
<h1 class="text-4xl font-bold text-gray-300 mb-4">
|
||||
{{ appConfig.app.name }}
|
||||
</h1>
|
||||
<p class="leading-7 text-pretty">
|
||||
Powered by
|
||||
|
||||
<!-- Status Message -->
|
||||
<p class="text-xl text-gray-400 text-center max-w-md">
|
||||
{{ statusMessage }}
|
||||
</p>
|
||||
|
||||
<div class="flex flex-wrap justify-center gap-1 md:gap-3">
|
||||
<UButton
|
||||
variant="ghost"
|
||||
size="xl"
|
||||
:to="app.nuxtSite"
|
||||
target="_blank"
|
||||
external
|
||||
>
|
||||
Nuxt 4
|
||||
</UButton>
|
||||
<UButton
|
||||
variant="ghost"
|
||||
size="xl"
|
||||
:to="app.tauriSite"
|
||||
target="_blank"
|
||||
external
|
||||
>
|
||||
Tauri 2
|
||||
</UButton>
|
||||
<UButton
|
||||
variant="ghost"
|
||||
size="xl"
|
||||
:to="app.nuxtUiSite"
|
||||
target="_blank"
|
||||
external
|
||||
>
|
||||
NuxtUI 3
|
||||
</UButton>
|
||||
<!-- Loading Animation -->
|
||||
<div v-if="isConnecting" class="flex items-center space-x-1">
|
||||
<div class="w-2 h-2 bg-gray-500 rounded-full animate-pulse"></div>
|
||||
<div class="w-2 h-2 bg-gray-500 rounded-full animate-pulse" style="animation-delay: 0.2s"></div>
|
||||
<div class="w-2 h-2 bg-gray-500 rounded-full animate-pulse" style="animation-delay: 0.4s"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<UButton
|
||||
:to="app.repo"
|
||||
>
|
||||
Star on GitHub
|
||||
</UButton>
|
||||
<!-- Overlay Controls (hidden by default, shown on hover if enabled) -->
|
||||
<div v-if="showControls && currentVideo"
|
||||
class="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity duration-300">
|
||||
<div class="flex items-center space-x-4">
|
||||
<button @click="togglePlay" class="p-3 rounded-full bg-white bg-opacity-20 hover:bg-opacity-30 transition-all">
|
||||
<UIcon :name="isPlaying ? 'lucide:pause' : 'lucide:play'" class="w-8 h-8" />
|
||||
</button>
|
||||
<button @click="stop" class="p-3 rounded-full bg-white bg-opacity-20 hover:bg-opacity-30 transition-all">
|
||||
<UIcon name="lucide:square" class="w-8 h-8" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fixed bottom-6 text-sm absolute-center-h">
|
||||
<div class="flex items-center gap-1 text-(--ui-text-muted)">
|
||||
<p class="text-sm">
|
||||
Made by
|
||||
</p>
|
||||
<ULink :to="app.repo" external target="_blank">
|
||||
{{ app.author }}
|
||||
</ULink>
|
||||
</div>
|
||||
<!-- Debug Info (only in dev mode) -->
|
||||
<div v-if="isDev" class="absolute bottom-4 left-4 text-xs text-gray-500 space-y-1">
|
||||
<div>Volume: {{ volume }}%</div>
|
||||
<div v-if="currentVideo">Position: {{ Math.floor(position) }}s / {{ Math.floor(duration) }}s</div>
|
||||
<div>Loop: {{ isLooping ? 'On' : 'Off' }}</div>
|
||||
<div>Fullscreen: {{ isFullscreen ? 'On' : 'Off' }}</div>
|
||||
</div>
|
||||
</UContainer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const { app } = useAppConfig();
|
||||
<script setup>
|
||||
const appConfig = useAppConfig()
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
|
||||
definePageMeta({
|
||||
layout: "home"
|
||||
});
|
||||
// Tauri functions will be loaded dynamically
|
||||
let invoke = null
|
||||
let listen = null
|
||||
|
||||
// Player state
|
||||
const connectionStatus = ref('Disconnected')
|
||||
const isConnecting = ref(false)
|
||||
const currentVideo = ref(null)
|
||||
const isPlaying = ref(false)
|
||||
const position = ref(0)
|
||||
const duration = ref(0)
|
||||
const volume = ref(appConfig.player.defaultVolume)
|
||||
const isLooping = ref(false)
|
||||
const isFullscreen = ref(false)
|
||||
const showControls = ref(false)
|
||||
|
||||
// Video element ref
|
||||
const videoElement = ref(null)
|
||||
|
||||
// Computed properties
|
||||
const connectionStatusClass = computed(() => {
|
||||
switch (connectionStatus.value) {
|
||||
case 'Connected':
|
||||
return 'bg-green-900 bg-opacity-50 text-green-300'
|
||||
case 'Connecting':
|
||||
return 'bg-yellow-900 bg-opacity-50 text-yellow-300'
|
||||
default:
|
||||
return 'bg-red-900 bg-opacity-50 text-red-300'
|
||||
}
|
||||
})
|
||||
|
||||
const connectionDotClass = computed(() => {
|
||||
switch (connectionStatus.value) {
|
||||
case 'Connected':
|
||||
return 'bg-green-400'
|
||||
case 'Connecting':
|
||||
return 'bg-yellow-400 animate-pulse'
|
||||
default:
|
||||
return 'bg-red-400'
|
||||
}
|
||||
})
|
||||
|
||||
const statusMessage = computed(() => {
|
||||
if (isConnecting.value) {
|
||||
return 'Connecting to controller...'
|
||||
}
|
||||
if (connectionStatus.value === 'Connected') {
|
||||
return 'Ready to play. Waiting for controller commands...'
|
||||
}
|
||||
return 'Waiting for controller connection...'
|
||||
})
|
||||
|
||||
// Video control functions
|
||||
function togglePlay() {
|
||||
if (!videoElement.value) return
|
||||
|
||||
if (isPlaying.value) {
|
||||
videoElement.value.pause()
|
||||
} else {
|
||||
videoElement.value.play()
|
||||
}
|
||||
isPlaying.value = !isPlaying.value
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (!videoElement.value) return
|
||||
|
||||
videoElement.value.pause()
|
||||
videoElement.value.currentTime = 0
|
||||
isPlaying.value = false
|
||||
position.value = 0
|
||||
}
|
||||
|
||||
function seek(time) {
|
||||
if (!videoElement.value) return
|
||||
|
||||
videoElement.value.currentTime = time
|
||||
position.value = time
|
||||
}
|
||||
|
||||
function setVolume(newVolume) {
|
||||
volume.value = Math.max(0, Math.min(100, newVolume))
|
||||
if (videoElement.value) {
|
||||
videoElement.value.volume = volume.value / 100
|
||||
}
|
||||
}
|
||||
|
||||
// Video event handlers
|
||||
function onVideoLoaded() {
|
||||
if (videoElement.value) {
|
||||
duration.value = videoElement.value.duration
|
||||
videoElement.value.volume = volume.value / 100
|
||||
}
|
||||
}
|
||||
|
||||
function onTimeUpdate() {
|
||||
if (videoElement.value) {
|
||||
position.value = videoElement.value.currentTime
|
||||
}
|
||||
}
|
||||
|
||||
function onVideoEnded() {
|
||||
isPlaying.value = false
|
||||
// Send playback finished event to controller
|
||||
// This will be implemented when WebSocket is ready
|
||||
}
|
||||
|
||||
function onVideoError(event) {
|
||||
console.error('Video playback error:', event)
|
||||
currentVideo.value = null
|
||||
isPlaying.value = false
|
||||
}
|
||||
|
||||
// Initialize fullscreen if configured
|
||||
onMounted(async () => {
|
||||
if (appConfig.player.autoFullscreen) {
|
||||
// Request fullscreen
|
||||
nextTick(() => {
|
||||
document.documentElement.requestFullscreen?.() ||
|
||||
document.documentElement.webkitRequestFullscreen?.()
|
||||
})
|
||||
}
|
||||
|
||||
// Setup Tauri event listeners
|
||||
await setupTauriListeners()
|
||||
|
||||
// Get initial player state
|
||||
await updatePlayerState()
|
||||
})
|
||||
|
||||
// Setup Tauri event listeners
|
||||
async function setupTauriListeners() {
|
||||
try {
|
||||
// Load Tauri APIs dynamically
|
||||
const { invoke: tauriInvoke } = await import('@tauri-apps/api/core')
|
||||
const { listen: tauriListen } = await import('@tauri-apps/api/event')
|
||||
|
||||
// Set global references
|
||||
invoke = tauriInvoke
|
||||
listen = tauriListen
|
||||
|
||||
// Listen for connection status changes
|
||||
await listen('connection-status', (event) => {
|
||||
connectionStatus.value = event.payload === 'connected' ? 'Connected' : 'Disconnected'
|
||||
isConnecting.value = false
|
||||
})
|
||||
|
||||
// Listen for player commands from backend
|
||||
await listen('player-command', async (event) => {
|
||||
const command = event.payload
|
||||
await handlePlayerCommand(command)
|
||||
})
|
||||
|
||||
console.log('✅ Tauri event listeners setup complete')
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to setup Tauri listeners:', error)
|
||||
// Fallback for development mode
|
||||
setTimeout(() => {
|
||||
connectionStatus.value = 'Waiting for WebSocket server...'
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
// Update player state from backend
|
||||
async function updatePlayerState() {
|
||||
if (!invoke) {
|
||||
console.warn('Tauri invoke function not available yet')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const state = await invoke('get_player_state')
|
||||
|
||||
currentVideo.value = state.current_video
|
||||
isPlaying.value = state.playback_status === 'playing'
|
||||
position.value = state.position
|
||||
duration.value = state.duration
|
||||
volume.value = state.volume
|
||||
isLooping.value = state.is_looping
|
||||
isFullscreen.value = state.is_fullscreen
|
||||
connectionStatus.value = state.connection_status === 'connected' ? 'Connected' : 'Disconnected'
|
||||
console.log('✅ Player state updated:', state)
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to get player state:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle player commands from WebSocket
|
||||
async function handlePlayerCommand(command) {
|
||||
switch (command.type) {
|
||||
case 'play':
|
||||
if (videoElement.value) {
|
||||
try {
|
||||
await videoElement.value.play()
|
||||
isPlaying.value = true
|
||||
} catch (error) {
|
||||
console.error('Play error:', error)
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'pause':
|
||||
if (videoElement.value) {
|
||||
videoElement.value.pause()
|
||||
isPlaying.value = false
|
||||
}
|
||||
break
|
||||
case 'stop':
|
||||
stop()
|
||||
break
|
||||
case 'seek':
|
||||
seek(command.position)
|
||||
break
|
||||
case 'setVolume':
|
||||
setVolume(command.volume)
|
||||
break
|
||||
case 'setLoop':
|
||||
isLooping.value = command.enabled
|
||||
if (videoElement.value) {
|
||||
videoElement.value.loop = command.enabled
|
||||
}
|
||||
break
|
||||
case 'toggleFullscreen':
|
||||
toggleFullscreenMode()
|
||||
break
|
||||
case 'loadVideo':
|
||||
await loadVideoFromPath(command.path)
|
||||
break
|
||||
}
|
||||
|
||||
// Update state after command
|
||||
await updatePlayerState()
|
||||
}
|
||||
|
||||
// Load video from file path
|
||||
async function loadVideoFromPath(path) {
|
||||
try {
|
||||
// Convert file path to URL for video element
|
||||
const videoUrl = `file://${path}`
|
||||
|
||||
// Create video info
|
||||
const title = path.split('/').pop() || path
|
||||
currentVideo.value = {
|
||||
path: videoUrl,
|
||||
title,
|
||||
duration: null,
|
||||
size: null,
|
||||
format: null
|
||||
}
|
||||
|
||||
// Update backend state if invoke is available
|
||||
if (invoke) {
|
||||
await invoke('load_video', { path })
|
||||
}
|
||||
console.log('📹 Video loaded:', title)
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to load video:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle fullscreen mode
|
||||
function toggleFullscreenMode() {
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen?.()
|
||||
isFullscreen.value = false
|
||||
} else {
|
||||
document.documentElement.requestFullscreen?.()
|
||||
isFullscreen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// Global keyboard shortcuts
|
||||
function handleKeyPress(event) {
|
||||
switch (event.key) {
|
||||
case ' ':
|
||||
event.preventDefault()
|
||||
togglePlay()
|
||||
break
|
||||
case 'f':
|
||||
case 'F':
|
||||
event.preventDefault()
|
||||
// Toggle fullscreen
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', handleKeyPress)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', handleKeyPress)
|
||||
})
|
||||
</script>
|
||||
|
@@ -1,70 +0,0 @@
|
||||
<template>
|
||||
<LayoutTile
|
||||
title="Notifications"
|
||||
description="Send native notifications to the client using the notification plugin."
|
||||
>
|
||||
<UForm :state="inputState" :schema="schema" class="flex flex-col gap-y-4 items-end" @submit="createNotification">
|
||||
<UFormField label="Notification title" name="notificationTitle">
|
||||
<UInput v-model="inputState.notificationTitle" variant="subtle" size="lg" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Notification body (optional)" name="notificationBody">
|
||||
<UInput v-model="inputState.notificationBody" variant="subtle" size="lg" />
|
||||
</UFormField>
|
||||
|
||||
<UButton type="submit" size="lg">
|
||||
Send notification
|
||||
</UButton>
|
||||
</UForm>
|
||||
</LayoutTile>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
definePageMeta({
|
||||
name: "Notifications",
|
||||
icon: "lucide:message-square-more",
|
||||
category: "interface",
|
||||
description: "Send native notifications"
|
||||
});
|
||||
|
||||
const schema = z.object({
|
||||
notificationTitle: z.string({
|
||||
error: "Title is required"
|
||||
}).nonempty(),
|
||||
notificationBody: z.string().optional()
|
||||
});
|
||||
|
||||
type Schema = zInfer<typeof schema>;
|
||||
|
||||
const inputState = ref<Partial<Schema>>({
|
||||
notificationTitle: undefined,
|
||||
notificationBody: undefined
|
||||
});
|
||||
|
||||
const toast = useToast();
|
||||
const permissionGranted = ref(false);
|
||||
|
||||
const createNotification = async () => {
|
||||
permissionGranted.value = await useTauriNotificationIsPermissionGranted();
|
||||
|
||||
if (!permissionGranted.value) {
|
||||
const permission = await useTauriNotificationRequestPermission();
|
||||
permissionGranted.value = permission === "granted";
|
||||
}
|
||||
|
||||
if (permissionGranted.value) {
|
||||
useTauriNotificationSendNotification({
|
||||
title: inputState.value.notificationTitle!,
|
||||
body: inputState.value.notificationBody
|
||||
});
|
||||
|
||||
inputState.value.notificationTitle = inputState.value.notificationBody = undefined;
|
||||
} else {
|
||||
toast.add({
|
||||
title: "Error",
|
||||
description: "Missing notifications permission",
|
||||
color: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
@@ -1,35 +0,0 @@
|
||||
<template>
|
||||
<LayoutTile
|
||||
title="OS Information"
|
||||
description="Read information about the operating system using the OS Information plugin."
|
||||
>
|
||||
<UAccordion :items="items" type="multiple" />
|
||||
</LayoutTile>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
definePageMeta({
|
||||
name: "OS Informations",
|
||||
icon: "lucide:info",
|
||||
category: "system",
|
||||
description: "Read operating system informations."
|
||||
});
|
||||
|
||||
const items = ref([
|
||||
{
|
||||
label: "System",
|
||||
icon: "lucide:monitor",
|
||||
content: `${useTauriOsPlatform()} ${useTauriOsVersion()}`
|
||||
},
|
||||
{
|
||||
label: "Arch",
|
||||
icon: "lucide:microchip",
|
||||
content: useTauriOsArch()
|
||||
},
|
||||
{
|
||||
label: "Locale",
|
||||
icon: "lucide:globe",
|
||||
content: await useTauriOsLocale() || "Not detectable"
|
||||
}
|
||||
]);
|
||||
</script>
|
@@ -1,91 +0,0 @@
|
||||
<template>
|
||||
<LayoutTile
|
||||
title="Store"
|
||||
description="Persistent key-value store. Allows you to handle state to a file which can be saved and loaded on demand including between app restarts."
|
||||
>
|
||||
<div class="space-y-6 md:space-y-8">
|
||||
<UForm :state="inputState" :schema="schema" class="flex flex-col gap-y-4 items-end" @submit="setStoreValue">
|
||||
<UFormField label="Store value" name="value">
|
||||
<UInput v-model="inputState.value" variant="subtle" size="lg" />
|
||||
</UFormField>
|
||||
|
||||
<UButton type="submit" size="lg">
|
||||
Set value
|
||||
</UButton>
|
||||
</UForm>
|
||||
|
||||
<UForm :state="outputState" class="flex flex-col gap-y-4 items-end">
|
||||
<UFormField label="Store content" name="content">
|
||||
<UTextarea v-model="outputState.content" variant="subtle" size="lg" :rows="8" readonly />
|
||||
</UFormField>
|
||||
</UForm>
|
||||
</div>
|
||||
</LayoutTile>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
definePageMeta({
|
||||
name: "Store",
|
||||
icon: "lucide:database",
|
||||
category: "storage",
|
||||
description: "Handle file creation in the file system"
|
||||
});
|
||||
|
||||
const schema = z.object({
|
||||
value: z.string({
|
||||
error: "Store key is required"
|
||||
}).nonempty()
|
||||
});
|
||||
|
||||
type Schema = zInfer<typeof schema>;
|
||||
|
||||
const inputState = ref<Partial<Schema>>({
|
||||
value: undefined
|
||||
});
|
||||
const outputState = ref({
|
||||
content: ""
|
||||
});
|
||||
|
||||
const toast = useToast();
|
||||
const autosave = ref(false);
|
||||
|
||||
const store = await useTauriStoreLoad("store.bin", {
|
||||
autoSave: autosave.value
|
||||
});
|
||||
|
||||
const getStoreValue = async () => {
|
||||
try {
|
||||
outputState.value.content = await store.get<string>("myData") || "";
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
title: "Error",
|
||||
description: String(error),
|
||||
color: "error"
|
||||
});
|
||||
outputState.value.content = JSON.stringify(error, null, 4);
|
||||
}
|
||||
};
|
||||
|
||||
await getStoreValue();
|
||||
|
||||
const setStoreValue = async () => {
|
||||
try {
|
||||
await store.set("myData", inputState.value!.value);
|
||||
await getStoreValue();
|
||||
toast.add({
|
||||
title: "Success",
|
||||
description: "Store value retieved",
|
||||
color: "success"
|
||||
});
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
title: "Error",
|
||||
description: String(error),
|
||||
color: "error"
|
||||
});
|
||||
outputState.value.content = JSON.stringify(error, null, 4);
|
||||
} finally {
|
||||
inputState.value.value = undefined;
|
||||
}
|
||||
};
|
||||
</script>
|
@@ -1,51 +0,0 @@
|
||||
<template>
|
||||
<LayoutTile
|
||||
title="Webview window"
|
||||
description="Create new webview in a detached window. This will create a new window flagged 'secondary' that has the same permissions as the main one. If you need more windows, update the permissions under capabilities > main or create a new capabilities file for the new window only."
|
||||
>
|
||||
<div class="flex flex-col items-center gap-6">
|
||||
<UButton variant="subtle" @click="openWindow((new Date).valueOf().toString(), app.repo)">
|
||||
Create external Webview
|
||||
</UButton>
|
||||
<UButton variant="subtle" @click="openWindow('secondary', '/os')">
|
||||
Create internal Webview
|
||||
</UButton>
|
||||
</div>
|
||||
</LayoutTile>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
definePageMeta({
|
||||
name: "Webview",
|
||||
icon: "lucide:app-window",
|
||||
category: "interface",
|
||||
description: "Create new webview in a detached window"
|
||||
});
|
||||
|
||||
const { app } = useAppConfig();
|
||||
const toast = useToast();
|
||||
|
||||
const openWindow = async (id: string, page: string) => {
|
||||
const webview = new useTauriWebviewWindowWebviewWindow(id, {
|
||||
title: "Nuxtor webview",
|
||||
url: page,
|
||||
width: 1280,
|
||||
height: 720
|
||||
});
|
||||
|
||||
webview.once("tauri://created", () => {
|
||||
toast.add({
|
||||
title: "Success",
|
||||
description: "Webview created",
|
||||
color: "success"
|
||||
});
|
||||
});
|
||||
webview.once("tauri://error", (error) => {
|
||||
toast.add({
|
||||
title: "Error",
|
||||
description: (error as any).payload,
|
||||
color: "error"
|
||||
});
|
||||
});
|
||||
};
|
||||
</script>
|
Reference in New Issue
Block a user