+
@@ -33,9 +25,11 @@
diff --git a/neode-ui/src/components/AppLauncherOverlay.vue b/neode-ui/src/components/AppLauncherOverlay.vue
new file mode 100644
index 00000000..1d66b9a2
--- /dev/null
+++ b/neode-ui/src/components/AppLauncherOverlay.vue
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ store.title || 'App' }}
+
+
Esc
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/neode-ui/src/components/Screensaver.vue b/neode-ui/src/components/Screensaver.vue
new file mode 100644
index 00000000..d01e7569
--- /dev/null
+++ b/neode-ui/src/components/Screensaver.vue
@@ -0,0 +1,166 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/neode-ui/src/composables/useControllerNav.ts b/neode-ui/src/composables/useControllerNav.ts
index 9689cb0f..187c0b00 100644
--- a/neode-ui/src/composables/useControllerNav.ts
+++ b/neode-ui/src/composables/useControllerNav.ts
@@ -10,6 +10,7 @@ import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useControllerStore } from '@/stores/controller'
import { useSpotlightStore } from '@/stores/spotlight'
+import { useAppLauncherStore } from '@/stores/appLauncher'
import { playNavSound } from '@/composables/useNavSounds'
const FOCUSABLE_SELECTOR = [
@@ -153,6 +154,12 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
// --- ESCAPE ---
if (e.key === 'Escape') {
+ if (useAppLauncherStore().isOpen) {
+ useAppLauncherStore().close()
+ e.preventDefault()
+ e.stopPropagation()
+ return
+ }
if (useSpotlightStore().isOpen) {
useSpotlightStore().close()
e.preventDefault()
diff --git a/neode-ui/src/stores/appLauncher.ts b/neode-ui/src/stores/appLauncher.ts
new file mode 100644
index 00000000..a7ee6e63
--- /dev/null
+++ b/neode-ui/src/stores/appLauncher.ts
@@ -0,0 +1,28 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+
+export const useAppLauncherStore = defineStore('appLauncher', () => {
+ const isOpen = ref(false)
+ const url = ref('')
+ const title = ref('')
+
+ function open(payload: { url: string; title: string }) {
+ url.value = payload.url
+ title.value = payload.title
+ isOpen.value = true
+ }
+
+ function close() {
+ isOpen.value = false
+ url.value = ''
+ title.value = ''
+ }
+
+ return {
+ isOpen,
+ url,
+ title,
+ open,
+ close,
+ }
+})
diff --git a/neode-ui/src/stores/screensaver.ts b/neode-ui/src/stores/screensaver.ts
new file mode 100644
index 00000000..1393ad0d
--- /dev/null
+++ b/neode-ui/src/stores/screensaver.ts
@@ -0,0 +1,42 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+
+const INACTIVITY_MS = 3 * 60 * 1000 // 3 minutes
+
+export const useScreensaverStore = defineStore('screensaver', () => {
+ const isActive = ref(false)
+ let inactivityTimer: ReturnType
| null = null
+
+ function activate() {
+ isActive.value = true
+ clearInactivityTimer()
+ }
+
+ function deactivate() {
+ isActive.value = false
+ resetInactivityTimer()
+ }
+
+ function resetInactivityTimer() {
+ clearInactivityTimer()
+ inactivityTimer = setTimeout(() => {
+ inactivityTimer = null
+ isActive.value = true
+ }, INACTIVITY_MS)
+ }
+
+ function clearInactivityTimer() {
+ if (inactivityTimer) {
+ clearTimeout(inactivityTimer)
+ inactivityTimer = null
+ }
+ }
+
+ return {
+ isActive,
+ activate,
+ deactivate,
+ resetInactivityTimer,
+ clearInactivityTimer,
+ }
+})
diff --git a/neode-ui/src/views/AppDetails.vue b/neode-ui/src/views/AppDetails.vue
index 0406002b..ef4edb7d 100644
--- a/neode-ui/src/views/AppDetails.vue
+++ b/neode-ui/src/views/AppDetails.vue
@@ -429,6 +429,7 @@
import { ref, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useAppStore } from '../stores/app'
+import { useAppLauncherStore } from '../stores/appLauncher'
import { PackageState } from '../types/api'
import { useMobileBackButton } from '../composables/useMobileBackButton'
import { dummyApps } from '../utils/dummyApps'
@@ -663,7 +664,7 @@ function launchApp() {
if (appUrls[id]) {
const url = isDev ? appUrls[id].dev : appUrls[id].prod
- window.open(url, '_blank', 'noopener,noreferrer')
+ useAppLauncherStore().open({ url, title: pkg.value.manifest.title })
return
}
diff --git a/neode-ui/src/views/Apps.vue b/neode-ui/src/views/Apps.vue
index dde1edaf..0a8ac9ae 100644
--- a/neode-ui/src/views/Apps.vue
+++ b/neode-ui/src/views/Apps.vue
@@ -171,6 +171,7 @@
import { computed, ref } from 'vue'
import { useRouter, RouterLink } from 'vue-router'
import { useAppStore } from '../stores/app'
+import { useAppLauncherStore } from '../stores/appLauncher'
import { PackageState } from '../types/api'
const router = useRouter()
@@ -223,7 +224,7 @@ function launchApp(id: string) {
}
if (lanAddress) {
- window.open(lanAddress, '_blank', 'noopener,noreferrer')
+ useAppLauncherStore().open({ url: lanAddress, title: pkg?.manifest?.title || id })
return
}
@@ -246,7 +247,7 @@ function launchApp(id: string) {
const currentHost = window.location.hostname
url = url.replace('localhost', currentHost)
}
- window.open(url, '_blank', 'noopener,noreferrer')
+ useAppLauncherStore().open({ url, title: pkg?.manifest?.title || id })
return
}
diff --git a/neode-ui/src/views/Cloud.vue b/neode-ui/src/views/Cloud.vue
index 9502607f..80994cd7 100644
--- a/neode-ui/src/views/Cloud.vue
+++ b/neode-ui/src/views/Cloud.vue
@@ -58,6 +58,7 @@
diff --git a/neode-ui/src/views/ContainerApps.vue b/neode-ui/src/views/ContainerApps.vue
index b735e5fc..bc55605e 100644
--- a/neode-ui/src/views/ContainerApps.vue
+++ b/neode-ui/src/views/ContainerApps.vue
@@ -132,16 +132,16 @@
{{ store.isAppLoading(app.id) ? 'Stopping...' : 'Stop' }}
-
Launch
-
+