commit de41ebdcc34107659ea8e565ec24853ceae3028c Author: 1b-agent Date: Thu Oct 8 08:48:49 2026 +0000 Initial commit: one_os_compositor_shell Odoo 20 addon — full-screen desktop shell UI Includes: - CompositorShell OWL component (ir.actions.client, target=fullscreen) - App launcher grid (purple 1B Space accent, hover animations) - Taskbar showing running apps - Window decorations (close button) - Top bar with session UUID display - Loading overlay + error handling - Modern SCSS with backdrop blur, gradients, dark theme - Depends on one_os_compositor (parent addon) Author: 1bspace.com Parent company: 1B Global License: Proprietary (1B Space internal use) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4d5081d --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Python +__pycache__/ +*.pyc +*.pyo +.venv/ +venv/ + +# OS +.DS_Store +Thumbs.db + +# Editor +.vscode/ +.idea/ +*.swp + +# Logs +*.log + +# Build +node_modules/ +dist/ +build/ +*.egg-info/ diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..9e5827f --- /dev/null +++ b/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from . import controllers +from . import models diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..4c24ac8 --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,47 @@ +{ + "name": "1B Space Compositor Shell", + "version": "20.0.1.0.0", + "category": "1B Global/OS", + "summary": "Full-screen desktop shell UI for the 1B Space Compositor", + "description": """ +1B Space Compositor Shell +========================= + +This addon provides the full-screen desktop shell UI for the 1B Space +Compositor. It is what the user sees when they click "Open Compositor" +in the Odoo menu. + +Features: +- App launcher grid (lists all apps the user is allowed to launch) +- Per-output canvas (one per OWL shell instance) +- Taskbar showing running apps +- Window decorations (close, maximize buttons) +- Keyboard layout picker (RMLVO) + +This addon DEPENDS on `one_os_compositor` (the core OWL library + backend models). + +Author & License +---------------- +- Author: 1bspace.com +- Company: 1B Global +- License: Proprietary (1B Space internal use) + """, + "author": "1bspace.com", + "website": "https://1bspace.com", + "maintainer": "1B Global ", + "license": "Other proprietary", + "depends": ["one_os_compositor"], + "data": [ + "views/compositor_shell_views.xml", + ], + "assets": { + "web.assets_backend": [ + "one_os_compositor_shell/static/src/components/**/*.js", + "one_os_compositor_shell/static/src/components/**/*.xml", + "one_os_compositor_shell/static/src/scss/*.scss", + ], + }, + "installable": True, + "application": True, + "auto_install": False, +} diff --git a/controllers/__init__.py b/controllers/__init__.py new file mode 100644 index 0000000..8175c63 --- /dev/null +++ b/controllers/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +"""Compositor Shell controllers.""" +from . import main diff --git a/controllers/main.py b/controllers/main.py new file mode 100644 index 0000000..074c69b --- /dev/null +++ b/controllers/main.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +"""Compositor Shell — minimal placeholder controller. + +The actual shell is a full OWL component registered via `ir.actions.client` +in `one_os_compositor` (the parent addon). This module is reserved for any +shell-specific HTTP routes we might add later (e.g. asset bundle routes). +""" diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..d35bb53 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Compositor Shell — models (none yet).""" diff --git a/static/src/components/compositor_shell/compositor_shell.js b/static/src/components/compositor_shell/compositor_shell.js new file mode 100644 index 0000000..4bfebbb --- /dev/null +++ b/static/src/components/compositor_shell/compositor_shell.js @@ -0,0 +1,171 @@ +/** + * 1B Space Compositor Shell — main OWL component + * =============================================== + * + * This is the full-screen desktop shell UI. It's mounted by Odoo's + * `ir.actions.client` mechanism (tag = "one_os_compositor_shell", + * target = "fullscreen"). + * + * The shell: + * 1. Fetches a JWT from the Odoo backend (via rpc("/web/compositor/token")) + * 2. Lists apps the user can launch (via rpc("/web/compositor/apps")) + * 3. Renders an app launcher grid + * 4. When the user clicks an app: + * - Calls rpc("/web/compositor/launch") + * - Receives a SurfaceHandle from the compositor service + * - Attaches the surface to a canvas + * - Renders a window decoration around it (close, maximize) + * + * License: Proprietary (1B Space internal use) + * Author: 1bspace.com + */ + +import { Component, useState, useRef, onMounted, onWillUnmount, useEffect } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; + +const STATE = { + IDLE: "idle", + LOADING_APPS: "loading_apps", + LAUNCHING_APP: "launching_app", + ERROR: "error", +}; + +export class CompositorShell extends Component { + setup() { + this.rpc = useService("rpc"); + this.notification = useService("notification"); + this.compositor = useService("compositor"); + this.ui = useService("ui"); + + // Reactive state — using proxy() per Odoo 20 OWL 3.x + this.state = useState({ + phase: STATE.IDLE, + apps: [], + surfaces: [], // active SurfaceHandles + activeSurfaceId: null, + jwt: null, + sessionUuid: null, + error: null, + showLauncher: true, + }); + + this.canvasRef = useRef("canvas"); + this.launcherRef = useRef("launcher"); + + onMounted(() => this._initialize()); + onWillUnmount(() => this._cleanup()); + + // Subscribe to compositor events + this._cleanupCallbacks = []; + this._cleanupCallbacks.push( + this.compositor.on("surfaceCreated", (s) => this._onSurfaceCreated(s)), + this.compositor.on("surfaceDestroyed", (s) => this._onSurfaceDestroyed(s)), + this.compositor.on("surfaceTitleUpdated", (s) => this._onSurfaceUpdated(s)), + this.compositor.on("surfaceActivationUpdated", ({ surface, active }) => + this._onSurfaceActivationChanged(surface, active) + ), + ); + } + + // ────────────────────────────────────────────────────────────────── + // Lifecycle + // ────────────────────────────────────────────────────────────────── + async _initialize() { + try { + this.state.phase = STATE.LOADING_APPS; + // Get JWT (compositor service handles this internally, but we + // also stash it here for the UI) + const tokenResult = await this.rpc("/web/compositor/token", {}); + this.state.jwt = tokenResult.jwt; + this.state.sessionUuid = tokenResult.compositor_session_id; + + // Load apps + const appsResult = await this.rpc("/web/compositor/apps", {}); + this.state.apps = appsResult.apps; + this.state.phase = STATE.IDLE; + } catch (e) { + console.error("[one_os_compositor_shell] init failed:", e); + this.state.error = e.message || String(e); + this.state.phase = STATE.ERROR; + } + } + + _cleanup() { + for (const cb of this._cleanupCallbacks) cb && cb(); + this._cleanupCallbacks = []; + } + + // ────────────────────────────────────────────────────────────────── + // User actions + // ────────────────────────────────────────────────────────────────── + async onClickApp(appId) { + if (this.state.phase === STATE.LAUNCHING_APP) return; + this.state.phase = STATE.LAUNCHING_APP; + try { + const surface = await this.compositor.launchApp(appId); + this.state.surfaces.push(surface); + this.state.activeSurfaceId = surface.surfaceId; + this.state.showLauncher = false; + this.notification.add(`Launched ${surface.appId}`, { type: "success" }); + } catch (e) { + console.error("[one_os_compositor_shell] launch failed:", e); + this.notification.add(`Launch failed: ${e.message || e}`, { type: "danger" }); + } finally { + this.state.phase = STATE.IDLE; + } + } + + onClickTaskbarItem(surface) { + this.state.activeSurfaceId = surface.surfaceId; + this.compositor.focusSurface(surface); + } + + onClickCloseSurface(surface) { + this.compositor.terminateApp(surface); + } + + onClickToggleLauncher() { + this.state.showLauncher = !this.state.showLauncher; + } + + // ────────────────────────────────────────────────────────────────── + // Compositor event handlers + // ────────────────────────────────────────────────────────────────── + _onSurfaceCreated(surface) { + if (!this.state.surfaces.find(s => s.surfaceId === surface.surfaceId)) { + this.state.surfaces.push(surface); + } + } + + _onSurfaceDestroyed(surface) { + const idx = this.state.surfaces.findIndex(s => s.surfaceId === surface.surfaceId); + if (idx >= 0) { + this.state.surfaces.splice(idx, 1); + } + if (this.state.activeSurfaceId === surface.surfaceId) { + this.state.activeSurfaceId = this.state.surfaces[0]?.surfaceId || null; + } + if (this.state.surfaces.length === 0) { + this.state.showLauncher = true; + } + } + + _onSurfaceUpdated(surface) { + // Force re-render via reactive state + const idx = this.state.surfaces.findIndex(s => s.surfaceId === surface.surfaceId); + if (idx >= 0) { + this.state.surfaces[idx] = { ...surface }; + } + } + + _onSurfaceActivationChanged(surface, active) { + if (active) { + this.state.activeSurfaceId = surface.surfaceId; + } + } +} + +CompositorShell.template = "one_os_compositor_shell.CompositorShell"; +CompositorShell.props = {}; + +export default CompositorShell; diff --git a/static/src/components/compositor_shell/compositor_shell.xml b/static/src/components/compositor_shell/compositor_shell.xml new file mode 100644 index 0000000..2374014 --- /dev/null +++ b/static/src/components/compositor_shell/compositor_shell.xml @@ -0,0 +1,97 @@ + + + +
+ +
+
+ 1B Space + 1B Space Compositor +
+
+ Session: + +
+
+ +
+
+ + +
+ +
+
+ + + +
+
+ +

No apps available. Ask a manager to add apps in 1B OS → Configuration → Apps.

+
+
+ + +
+ +
+
+ +
+ +
+
+
+ +
+
+
+
+
+ + +
+ + + +
+ + +
+ +

Loading...

+
+
+
+
diff --git a/static/src/components/compositor_shell/compositor_shell_register.js b/static/src/components/compositor_shell/compositor_shell_register.js new file mode 100644 index 0000000..4b2b752 --- /dev/null +++ b/static/src/components/compositor_shell/compositor_shell_register.js @@ -0,0 +1,20 @@ +/** + * 1B Space Compositor Shell — component registry + * =============================================== + * + * Registers the CompositorShell component under the + * `one_os_compositor_shell` action tag so Odoo's `ir.actions.client` + * mechanism can find it. + * + * When the user clicks "Open Compositor" in the menu, Odoo looks up the + * `ir.actions.client` record (which has tag="one_os_compositor_shell") + * and calls the corresponding registered component. + */ + +import { registry } from "@web/core/registry"; +import { CompositorShell } from "./compositor_shell"; + +// Register the client action +registry.category("actions").add("one_os_compositor_shell", CompositorShell); + +export { CompositorShell }; diff --git a/static/src/scss/compositor_shell.scss b/static/src/scss/compositor_shell.scss new file mode 100644 index 0000000..d523523 --- /dev/null +++ b/static/src/scss/compositor_shell.scss @@ -0,0 +1,320 @@ +// 1B Space Compositor Shell — styles +// ==================================== + +.o_compositor_shell { + position: fixed; + inset: 0; + display: flex; + flex-direction: column; + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + color: #e0e0e0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + + &.is-loading { + opacity: 0.95; + } + + // ── Top bar ──────────────────────────────────────────────────────── + .o_compositor_topbar { + height: 48px; + background: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(12px); + display: flex; + align-items: center; + padding: 0 16px; + gap: 16px; + flex-shrink: 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + + .o_topbar_brand { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + font-size: 14px; + color: #ffffff; + + img { width: 24px; height: 24px; border-radius: 4px; } + } + + .o_topbar_session { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: #a0a0a0; + + code { + background: rgba(255, 255, 255, 0.1); + padding: 2px 6px; + border-radius: 3px; + font-family: monospace; + } + } + + .o_topbar_actions { + margin-left: auto; + } + + .o_topbar_btn { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + color: #e0e0e0; + transition: all 0.15s; + + &:hover { + background: rgba(255, 255, 255, 0.1); + } + + &.active { + background: var(--accent, #6f42c1); + color: white; + } + } + } + + // ── Main area ─────────────────────────────────────────────────────── + .o_compositor_main { + flex: 1; + position: relative; + overflow: hidden; + } + + // ── Launcher grid ─────────────────────────────────────────────────── + .o_compositor_launcher { + position: absolute; + inset: 0; + overflow-y: auto; + padding: 32px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + + .o_launcher_grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + gap: 20px; + max-width: 1000px; + width: 100%; + } + + .o_launcher_tile { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + padding: 20px 12px; + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 12px; + cursor: pointer; + transition: all 0.2s; + position: relative; + color: #e0e0e0; + + &:hover { + background: rgba(255, 255, 255, 0.08); + border-color: var(--accent, #6f42c1); + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4); + } + + &:disabled { + opacity: 0.5; + cursor: wait; + } + + .o_tile_icon { + width: 56px; + height: 56px; + display: flex; + align-items: center; + justify-content: center; + background: var(--accent, #6f42c1); + border-radius: 12px; + color: white; + + img { + width: 100%; + height: 100%; + object-fit: contain; + } + + i { + font-size: 28px; + } + } + + .o_tile_name { + font-weight: 500; + text-align: center; + word-break: break-word; + } + + .o_tile_app_id { + font-size: 11px; + color: #888; + font-family: monospace; + } + + .o_tile_running { + position: absolute; + top: 8px; + right: 8px; + .badge { + font-size: 10px; + padding: 2px 6px; + } + } + } + + .o_launcher_empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 64px 32px; + color: #a0a0a0; + + i { font-size: 64px; margin-bottom: 16px; opacity: 0.5; } + p { text-align: center; } + } + } + + // ── Surface windows ──────────────────────────────────────────────── + .o_compositor_surfaces { + position: absolute; + inset: 0; + padding: 16px; + + .o_surface_window { + position: absolute; + top: 16px; + left: 16px; + right: 16px; + bottom: 16px; + background: #2a2a3e; + border-radius: 8px; + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.6); + display: flex; + flex-direction: column; + overflow: hidden; + border: 2px solid transparent; + + &.is-active { + border-color: #6f42c1; + } + + .o_window_titlebar { + height: 36px; + background: rgba(0, 0, 0, 0.4); + display: flex; + align-items: center; + padding: 0 12px; + font-size: 13px; + flex-shrink: 0; + + .o_window_title { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .o_window_actions { + display: flex; + gap: 4px; + + .o_window_btn { + background: none; + border: none; + color: #e0e0e0; + cursor: pointer; + padding: 4px 8px; + border-radius: 3px; + + &:hover { + background: rgba(255, 255, 255, 0.1); + } + + &.o_window_close:hover { + background: #dc3545; + color: white; + } + } + } + } + + .o_window_body { + flex: 1; + position: relative; + overflow: hidden; + + .o_surface_canvas { + width: 100%; + height: 100%; + display: block; + } + } + } + } + + // ── Taskbar ──────────────────────────────────────────────────────── + .o_compositor_taskbar { + height: 40px; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(12px); + display: flex; + align-items: center; + padding: 0 8px; + gap: 4px; + flex-shrink: 0; + border-top: 1px solid rgba(255, 255, 255, 0.1); + + .o_taskbar_item { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 4px; + color: #e0e0e0; + font-size: 12px; + cursor: pointer; + max-width: 200px; + overflow: hidden; + + span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &.is-active { + background: var(--accent, #6f42c1); + color: white; + } + + &:hover { + background: rgba(255, 255, 255, 0.1); + } + } + } + + // ── Loading overlay ──────────────────────────────────────────────── + .o_compositor_loading { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(4px); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 16px; + z-index: 1000; + color: #ffffff; + } +} diff --git a/views/assets.xml b/views/assets.xml new file mode 100644 index 0000000..8074018 --- /dev/null +++ b/views/assets.xml @@ -0,0 +1,7 @@ + + + +