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

View File

@@ -51,6 +51,37 @@
background-color: #121212; 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 */ /* System navigation bar at bottom - always visible */
.shell-nav-bar { .shell-nav-bar {
height: 56px; height: 56px;
@@ -97,28 +128,37 @@
/* ===== OVERLAY LAYERS ===== */ /* ===== OVERLAY LAYERS ===== */
/* Toast container - bottom of screen, above nav bar */ /* Toast container - top of screen, below status bar */
#toast-container { #toast-container {
position: absolute; position: absolute;
bottom: 70px; bottom: 70dp;
left: 16px; left: 0;
right: 16px; width: 100%;
z-index: 500; z-index: 500;
display: flex; display: block;
flex-direction: column; text-align: center;
gap: 8px;
pointer-events: none; pointer-events: none;
} }
.toast { .toast {
display: inline-block;
background-color: #323232; background-color: #323232;
color: #FFFFFF; color: #FFFFFF;
padding: 14dp 24dp; padding: 18dp 28dp;
border-radius: 8dp; margin: 0 16dp;
font-size: 14dp; border-radius: 12dp;
font-size: 16dp;
font-weight: bold;
text-align: center; text-align: center;
pointer-events: auto; 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 { .toast-success {
@@ -136,9 +176,15 @@
color: #000000; color: #000000;
} }
@keyframes toast-in { @keyframes toast-pop-in {
from { opacity: 0; transform: translateY(20px); } 0% { opacity: 0; transform: translateY(50dp) scale(0.8); }
to { opacity: 1; transform: translateY(0); } 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 */ /* Dialog/Modal overlay */