Startup-Splash (petrol-gruen) waehrend Plugin-Init
Verdeckt visuell die 3+ Sekunden Wartezeit beim Cold-Start (Panel- Registration + WindowLayout-Apply). Stilistisch identisch zum Launcher-Splash: petrol-gruener Verlauf mit "DOSSIER."-Logo, pulsierendem Dot, animierter Progress-Bar. Architektur: - _startup_splash.py: zentrale show() / hide() Helpers - Borderless Eto.Forms.Form (420x160), Topmost, kein Taskbar-Eintrag - WebView mit Inline-HTML (gleicher Stil wie launcher/public/splash.html) - Sticky-Key _dossier_startup_splash haelt die Form-Referenz - Safety-Timeout 8s falls hide() vergessen wird - startup.py _load_all: show() ganz am Anfang (bevor Imports laufen) - oberleiste._on_ready: hide() via 200ms-Timer NACH window-layout-apply (bzw. nach skip) — Layout-Animation ist auf Panels in Finalposition kurz sichtbar bevor Splash verschwindet Effekt: User sieht sofort einen schoenen Branded-Loading-Screen statt 3s grauer Rhino-UI mit halb-geladenen Panels.
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
_startup_splash.py
|
||||
Petrol-grüner Splash-Screen waehrend des DOSSIER-Plugin-Startups.
|
||||
Borderless Eto-Form mit WebView + Inline-HTML im selben Stil wie der
|
||||
Launcher-Splash. Bedeckt visuell die 3+ Sekunden waehrend Rhino die
|
||||
Panels registriert + WindowLayout neu anwendet.
|
||||
|
||||
Wird von startup.py beim ersten Idle gezeigt und nach Layout-Apply
|
||||
(oder Timeout) wieder versteckt.
|
||||
"""
|
||||
import scriptcontext as sc
|
||||
|
||||
_SPLASH_KEY = "_dossier_startup_splash"
|
||||
_SAFETY_TIMEOUT_SEC = 8.0 # spaetestens nach 8s wegmachen, falls Hide-Hook nicht feuert
|
||||
|
||||
|
||||
_SPLASH_HTML = '''<!DOCTYPE html>
|
||||
<html lang="de"><head><meta charset="utf-8"/><title>Dossier laedt</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=DM+Mono:ital,wght@0,300;0,400;0,500&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
:root {
|
||||
--accent: #5fa896; --accent-soft: #6fb5a3; --accent-deep: #2f5d54;
|
||||
--paper: #fff; --paper-mute: rgba(255,255,255,0.72); --paper-faint: rgba(255,255,255,0.45);
|
||||
--font-display: Krungthep, 'Archivo Black', sans-serif;
|
||||
--font-mono: 'DM Mono', 'Menlo', monospace;
|
||||
}
|
||||
html, body { margin:0; padding:0; width:100%; height:100%; background:transparent !important;
|
||||
color:var(--paper); overflow:hidden; font-family:var(--font-mono); user-select:none;
|
||||
-webkit-user-select:none; cursor:default; }
|
||||
.frame { box-sizing:border-box; width:100%; height:100%; padding:22px 26px;
|
||||
display:grid; grid-template-rows:auto 1fr auto; gap:0;
|
||||
background: radial-gradient(120% 140% at 0% 0%, var(--accent-soft) 0%, var(--accent) 55%, var(--accent-deep) 130%);
|
||||
border-radius:16px; box-shadow: inset 0 1px 0 rgba(255,255,255,0.18); }
|
||||
.brand-row { display:flex; align-items:baseline; justify-content:space-between; gap:12px; }
|
||||
.brand { font-family:var(--font-display); font-size:28px; letter-spacing:-0.01em;
|
||||
line-height:1; color:var(--paper); }
|
||||
.brand-dot { color:var(--accent-deep); }
|
||||
.version { font-family:var(--font-mono); font-size:10px; letter-spacing:0.10em;
|
||||
color:var(--paper-mute); text-transform:uppercase; }
|
||||
.status-row { align-self:end; display:flex; align-items:center; gap:10px;
|
||||
margin-top:18px; font-size:11px; letter-spacing:0.10em; color:var(--paper);
|
||||
text-transform:uppercase; }
|
||||
.dot-pulse { width:7px; height:7px; border-radius:50%; background:var(--paper);
|
||||
animation:pulse 1.6s ease-out infinite; }
|
||||
@keyframes pulse {
|
||||
0% { box-shadow:0 0 0 0 rgba(255,255,255,0.55); transform:scale(1); }
|
||||
70% { box-shadow:0 0 0 9px rgba(255,255,255,0); transform:scale(1.05); }
|
||||
100% { box-shadow:0 0 0 0 rgba(255,255,255,0); transform:scale(1); }
|
||||
}
|
||||
.bar { position:relative; height:2px; width:100%; background:rgba(255,255,255,0.18);
|
||||
border-radius:2px; overflow:hidden; margin-top:12px; }
|
||||
.bar::after { content:""; position:absolute; top:0; left:-35%; width:35%; height:100%;
|
||||
background: linear-gradient(90deg, transparent, var(--paper), transparent);
|
||||
animation: slide 1.6s linear infinite; }
|
||||
@keyframes slide { to { left:100%; } }
|
||||
.meta-row { display:flex; align-items:baseline; justify-content:space-between; gap:12px;
|
||||
margin-top:10px; font-size:9px; letter-spacing:0.14em; color:var(--paper-faint);
|
||||
text-transform:uppercase; }
|
||||
</style></head><body>
|
||||
<div class="frame">
|
||||
<div class="brand-row">
|
||||
<div class="brand">DOSSIER<span class="brand-dot">.</span></div>
|
||||
<div class="version">Rhino 8 Plugin</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="status-row">
|
||||
<span class="dot-pulse"></span>
|
||||
<span>Plugin laedt — Panels werden platziert</span>
|
||||
</div>
|
||||
<div class="bar"></div>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span>AGPL-3.0 · Karim Gabriele Varano</span>
|
||||
<span>CPython 3.9</span>
|
||||
</div>
|
||||
</div></body></html>
|
||||
'''
|
||||
|
||||
|
||||
def show():
|
||||
"""Zeigt den Splash. Idempotent — zweiter Aufruf bringt das bestehende
|
||||
Fenster nur in den Vordergrund. Auto-Hide nach _SAFETY_TIMEOUT_SEC
|
||||
als Fallback falls hide() vergessen wird."""
|
||||
if sc.sticky.get(_SPLASH_KEY) is not None:
|
||||
return
|
||||
try:
|
||||
import Eto.Forms as ef
|
||||
import Eto.Drawing as ed
|
||||
form = ef.Form()
|
||||
form.Title = "Dossier"
|
||||
# WindowStyle.None — "None" ist Python-keyword, daher via getattr
|
||||
try: form.WindowStyle = getattr(ef.WindowStyle, "None")
|
||||
except Exception: pass
|
||||
try: form.Resizable = False
|
||||
except Exception: pass
|
||||
try: form.Topmost = True
|
||||
except Exception: pass
|
||||
try: form.ShowInTaskbar = False
|
||||
except Exception: pass
|
||||
try: form.Size = ed.Size(420, 160)
|
||||
except Exception: pass
|
||||
try:
|
||||
# Hintergrund weiss, das WebView-content hat seine eigene
|
||||
# gerundete Petrol-Box — Form muss nur kein graues Border zeigen
|
||||
form.BackgroundColor = ed.Color(0.18, 0.50, 0.45)
|
||||
except Exception: pass
|
||||
wv = ef.WebView()
|
||||
try:
|
||||
wv.LoadHtml(_SPLASH_HTML)
|
||||
except Exception as ex:
|
||||
print("[SPLASH] LoadHtml:", ex)
|
||||
form.Content = wv
|
||||
try:
|
||||
# Center on screen
|
||||
screen = ef.Screen.PrimaryScreen
|
||||
sb = screen.Bounds
|
||||
x = int(sb.X + (sb.Width - form.Size.Width) / 2)
|
||||
y = int(sb.Y + (sb.Height - form.Size.Height) / 2 - 100)
|
||||
form.Location = ed.Point(x, y)
|
||||
except Exception as ex:
|
||||
print("[SPLASH] center:", ex)
|
||||
try: form.Show()
|
||||
except Exception as ex:
|
||||
print("[SPLASH] Show:", ex); return
|
||||
sc.sticky[_SPLASH_KEY] = form
|
||||
# Safety-Timeout — wenn nach 8s niemand hide() ruft, automatisch weg
|
||||
try:
|
||||
import threading
|
||||
def _auto_hide():
|
||||
try: hide()
|
||||
except Exception: pass
|
||||
threading.Timer(_SAFETY_TIMEOUT_SEC, _auto_hide).start()
|
||||
except Exception: pass
|
||||
except Exception as ex:
|
||||
print("[SPLASH] show:", ex)
|
||||
|
||||
|
||||
def hide():
|
||||
"""Versteckt + entsorgt den Splash. Idempotent."""
|
||||
form = sc.sticky.get(_SPLASH_KEY)
|
||||
if form is None:
|
||||
return
|
||||
try:
|
||||
sc.sticky[_SPLASH_KEY] = None
|
||||
try: form.Close()
|
||||
except Exception:
|
||||
try: form.Visible = False
|
||||
except Exception: pass
|
||||
except Exception as ex:
|
||||
print("[SPLASH] hide:", ex)
|
||||
Reference in New Issue
Block a user