add toast and app transition animations

Toast:
- Bigger size with bold text (18dp padding, 16dp font)
- Pop-up animation from bottom with bounce effect
- Fade-out animation when dismissing
- Cancels previous toast when showing new one

App transitions:
- Opening: fade in + scale from 0.8 to 1.0 with back-out easing
- Closing: fade out + scale from 1.0 to 0.8
- Skip animation on initial shell load
- Async close animation before loading previous app
This commit is contained in:
2026-01-20 12:27:44 +01:00
parent 11c59b890e
commit 469535f79a
2 changed files with 140 additions and 45 deletions

View File

@@ -30,8 +30,8 @@ function initShell(doc)
-- Update time display
updateTime()
-- Load home screen by default
loadAppContent_internal("home", "apps/home/home_content.rml")
-- Load home screen by default (skip animation on initial load)
loadAppContent_internal("home", "apps/home/home_content.rml", true)
print("[Shell] Shell initialized")
end
@@ -46,8 +46,8 @@ end
-- ===== APP LOADING =====
-- Internal function to load app content
function loadAppContent_internal(app_id, app_path)
-- Internal function to load app content with optional animation
function loadAppContent_internal(app_id, app_path, skip_animation)
if not app_container then
print("[Shell] ERROR: No app container")
return false
@@ -68,6 +68,18 @@ function loadAppContent_internal(app_id, app_path)
current_app_path = app_path
print("[Shell] App loaded: " .. app_id)
-- Play opening animation (unless skipped for initial load)
if not skip_animation then
app_container:SetClass("app-opening", true)
app_container:SetClass("app-closing", false)
-- Remove animation class after it completes
if setTimeout then
setTimeout(function()
app_container:SetClass("app-opening", false)
end, 300)
end
end
-- If home was loaded, populate apps dynamically
if app_id == "home" then
populateHomeApps()
@@ -126,6 +138,21 @@ end
-- ===== NAVIGATION =====
-- Play closing animation then execute callback
local function playCloseAnimation(callback)
if app_container and setTimeout then
app_container:SetClass("app-closing", true)
app_container:SetClass("app-opening", false)
setTimeout(function()
app_container:SetClass("app-closing", false)
if callback then callback() end
end, 200)
else
-- No animation support, execute immediately
if callback then callback() end
end
end
-- Go back to previous app
function shellGoBack()
print("[Shell] goBack called (history depth: " .. #nav_history .. ")")
@@ -139,17 +166,20 @@ function shellGoBack()
local previous = table.remove(nav_history)
print("[Shell] Going back to: " .. previous.app_id)
-- Don't push to history when going back
local temp_app = current_app
current_app = nil
current_app_path = nil
-- Play closing animation, then load previous app
playCloseAnimation(function()
-- Don't push to history when going back
local temp_app = current_app
current_app = nil
current_app_path = nil
local success = loadAppContent_internal(previous.app_id, previous.app_path)
if not success then
-- Restore state on failure
current_app = temp_app
end
return success
local success = loadAppContent_internal(previous.app_id, previous.app_path)
if not success then
-- Restore state on failure
current_app = temp_app
end
end)
return true
else
print("[Shell] No history - already at root")
if current_app ~= "home" then
@@ -171,10 +201,12 @@ function shellGoHome()
-- Clear history
nav_history = {}
-- Load home
current_app = nil
current_app_path = nil
loadAppContent_internal("home", "apps/home/home_content.rml")
-- Play closing animation, then load home
playCloseAnimation(function()
current_app = nil
current_app_path = nil
loadAppContent_internal("home", "apps/home/home_content.rml")
end)
end
-- Show recents (placeholder)
@@ -209,6 +241,8 @@ end
-- ===== TOASTS =====
local active_toast_id = nil
function showToast(message, type)
type = type or "default"
@@ -218,24 +252,39 @@ function showToast(message, type)
return
end
local class = "toast"
if type == "success" then
class = "toast toast-success"
elseif type == "error" then
class = "toast toast-error"
elseif type == "warning" then
class = "toast toast-warning"
-- Cancel any pending hide timer
if active_toast_id and clearTimeout then
clearTimeout(active_toast_id)
active_toast_id = nil
end
-- Clear existing toasts and add new one
local toast_html = '<div class="' .. class .. '"><span>' .. message .. '</span></div>'
local type_class = ""
if type == "success" then
type_class = " toast-success"
elseif type == "error" then
type_class = " toast-error"
elseif type == "warning" then
type_class = " toast-warning"
end
-- Create toast with visible animation class
local toast_html = '<div id="active-toast" class="toast toast-visible' .. type_class .. '"><span>' .. message .. '</span></div>'
container.inner_rml = toast_html
-- Auto-remove after 3 seconds
-- Auto-hide after 2.5 seconds (start hide animation, then remove)
if setTimeout then
setTimeout(function()
container.inner_rml = ""
end, 3000)
active_toast_id = setTimeout(function()
local toast = shell_document:GetElementById("active-toast")
if toast then
-- Switch to hiding animation
toast:SetAttribute("class", "toast toast-hiding" .. type_class)
-- Remove after animation completes
setTimeout(function()
container.inner_rml = ""
active_toast_id = nil
end, 300)
end
end, 2500)
end
print("[Shell] Toast: " .. message .. " (" .. type .. ")")

View File

@@ -51,6 +51,37 @@
background-color: #121212;
}
/* App launch/close animations */
.app-opening {
animation: 0.25s back-out app-open;
}
.app-closing {
animation: 0.2s quadratic-in app-close;
}
@keyframes app-open {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1.0);
}
}
@keyframes app-close {
from {
opacity: 1;
transform: scale(1.0);
}
to {
opacity: 0;
transform: scale(0.8);
}
}
/* System navigation bar at bottom - always visible */
.shell-nav-bar {
height: 56px;
@@ -97,28 +128,37 @@
/* ===== OVERLAY LAYERS ===== */
/* Toast container - bottom of screen, above nav bar */
/* Toast container - top of screen, below status bar */
#toast-container {
position: absolute;
bottom: 70px;
left: 16px;
right: 16px;
bottom: 70dp;
left: 0;
width: 100%;
z-index: 500;
display: flex;
flex-direction: column;
gap: 8px;
display: block;
text-align: center;
pointer-events: none;
}
.toast {
display: inline-block;
background-color: #323232;
color: #FFFFFF;
padding: 14dp 24dp;
border-radius: 8dp;
font-size: 14dp;
padding: 18dp 28dp;
margin: 0 16dp;
border-radius: 12dp;
font-size: 16dp;
font-weight: bold;
text-align: center;
pointer-events: auto;
width: auto;
}
.toast-visible {
animation: 0.4s back-out toast-pop-in;
}
.toast-hiding {
animation: 0.2s quadratic-in toast-pop-out;
}
.toast-success {
@@ -136,9 +176,15 @@
color: #000000;
}
@keyframes toast-in {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
@keyframes toast-pop-in {
0% { opacity: 0; transform: translateY(50dp) scale(0.8); }
70% { transform: translateY(-5dp) scale(1.02); }
100% { opacity: 1; transform: translateY(0) scale(1.0); }
}
@keyframes toast-pop-out {
from { opacity: 1; transform: translateY(0) scale(1.0); }
to { opacity: 0; transform: translateY(30dp) scale(0.9); }
}
/* Dialog/Modal overlay */