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)
This commit is contained in:
commit
de41ebdcc3
12 files changed
+713
No files matched your search
+24
@@ -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/
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import controllers
|
||||||
|
from . import models
|
||||||
@@ -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 <canvas> 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 <dev@1bspace.com>",
|
||||||
|
"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,
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Compositor Shell controllers."""
|
||||||
|
from . import main
|
||||||
@@ -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).
|
||||||
|
"""
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Compositor Shell — models (none yet)."""
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
<t t-name="one_os_compositor_shell.CompositorShell">
|
||||||
|
<div class="o_compositor_shell" t-att-class="{ 'is-loading': state.phase === 'loading' or state.phase === 'launching' }">
|
||||||
|
<!-- ─── Top bar (always visible) ─────────────────────────── -->
|
||||||
|
<header class="o_compositor_topbar">
|
||||||
|
<div class="o_topbar_brand">
|
||||||
|
<img src="/one_os_compositor/static/img/icon.png" alt="1B Space"/>
|
||||||
|
<span>1B Space Compositor</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_topbar_session" t-if="state.sessionUuid">
|
||||||
|
<span class="o_session_label">Session:</span>
|
||||||
|
<code t-esc="state.sessionUuid.slice(0, 8) + '...'"/>
|
||||||
|
</div>
|
||||||
|
<div class="o_topbar_actions">
|
||||||
|
<button t-on-click="onClickToggleLauncher"
|
||||||
|
class="btn btn-sm btn-light o_topbar_btn"
|
||||||
|
t-att-class="{ 'active': state.showLauncher }">
|
||||||
|
<i class="fa fa-th"/> Launcher
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- ─── Main content area ─────────────────────────────────── -->
|
||||||
|
<div class="o_compositor_main">
|
||||||
|
<!-- App launcher grid -->
|
||||||
|
<div t-ref="launcher" class="o_compositor_launcher"
|
||||||
|
t-if="state.showLauncher">
|
||||||
|
<div class="o_launcher_grid">
|
||||||
|
<t t-foreach="state.apps" t-as="app" t-key="app.app_id">
|
||||||
|
<button t-on-click="onClickApp(app.app_id)"
|
||||||
|
class="o_launcher_tile"
|
||||||
|
t-att-style="'--accent:' + (app.color or '#6f42c1')"
|
||||||
|
t-att-disabled="state.phase === 'launching'">
|
||||||
|
<div class="o_tile_icon">
|
||||||
|
<img t-if="app.icon" t-att-src="'data:image/png;base64,' + app.icon" alt="icon"/>
|
||||||
|
<i t-else="" class="fa fa-cube"/>
|
||||||
|
</div>
|
||||||
|
<div class="o_tile_name" t-esc="app.name"/>
|
||||||
|
<div class="o_tile_app_id" t-esc="app.app_id"/>
|
||||||
|
<div class="o_tile_running" t-if="app.running_session_count">
|
||||||
|
<span class="badge bg-success" t-esc="app.running_session_count + ' running'"/>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
<div class="o_launcher_empty" t-if="!state.apps.length">
|
||||||
|
<i class="fa fa-folder-open"/>
|
||||||
|
<p>No apps available. Ask a manager to add apps in <b>1B OS → Configuration → Apps</b>.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Active surface canvases -->
|
||||||
|
<div class="o_compositor_surfaces" t-else="">
|
||||||
|
<t t-foreach="state.surfaces" t-as="surface" t-key="surface.surfaceId">
|
||||||
|
<div class="o_surface_window"
|
||||||
|
t-att-class="{ 'is-active': surface.surfaceId === state.activeSurfaceId }"
|
||||||
|
t-on-click="() => onClickTaskbarItem(surface)">
|
||||||
|
<div class="o_window_titlebar">
|
||||||
|
<span class="o_window_title" t-esc="surface.title || surface.appId"/>
|
||||||
|
<div class="o_window_actions">
|
||||||
|
<button t-on-click="() => onClickCloseSurface(surface)"
|
||||||
|
class="o_window_btn o_window_close" title="Close">
|
||||||
|
<i class="fa fa-times"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="o_window_body">
|
||||||
|
<canvas t-ref="canvas"
|
||||||
|
t-att-id="'surface-canvas-' + surface.surfaceId"
|
||||||
|
class="o_surface_canvas"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ─── Taskbar (bottom) ──────────────────────────────────── -->
|
||||||
|
<footer class="o_compositor_taskbar" t-if="state.surfaces.length">
|
||||||
|
<t t-foreach="state.surfaces" t-as="surface" t-key="surface.surfaceId">
|
||||||
|
<button class="o_taskbar_item"
|
||||||
|
t-att-class="{ 'is-active': surface.surfaceId === state.activeSurfaceId }"
|
||||||
|
t-on-click="() => onClickTaskbarItem(surface)">
|
||||||
|
<i class="fa fa-window-restore"/>
|
||||||
|
<span t-esc="surface.title || surface.appId"/>
|
||||||
|
</button>
|
||||||
|
</t>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- ─── Loading overlay ───────────────────────────────────── -->
|
||||||
|
<div class="o_compositor_loading" t-if="state.phase === 'loading'">
|
||||||
|
<i class="fa fa-spinner fa-pulse fa-3x"/>
|
||||||
|
<p>Loading...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
@@ -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 };
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Old-style asset inheritance doesn't work in Odoo 20 — the SCSS
|
||||||
|
is already declared in the manifest's `assets` block. This file
|
||||||
|
is intentionally minimal so we don't double-register. -->
|
||||||
|
<template id="compositor_shell_assets_placeholder" name="1B Space Compositor Shell (placeholder)"/>
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Register the OWL CompositorShell component as a client action.
|
||||||
|
The SCSS is already declared in the manifest's `assets` block —
|
||||||
|
we don't use the old inherit_id="web.assets_backend" approach. -->
|
||||||
|
|
||||||
|
<record id="compositor_shell_client_action" model="ir.actions.client">
|
||||||
|
<field name="name">1B Space Compositor</field>
|
||||||
|
<field name="tag">one_os_compositor_shell</field>
|
||||||
|
<field name="target">fullscreen</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
Reference in new issue
Block a user