From 469535f79ae3da542b52b6f0c3b79c116ae4dbd5 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Tue, 20 Jan 2026 12:27:44 +0100 Subject: [PATCH] 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 --- src/main/assets/apps/shell/shell.lua | 111 +++++++++++++++++++-------- src/main/assets/apps/shell/shell.rml | 74 ++++++++++++++---- 2 files changed, 140 insertions(+), 45 deletions(-) diff --git a/src/main/assets/apps/shell/shell.lua b/src/main/assets/apps/shell/shell.lua index 95b97f2..1e80610 100644 --- a/src/main/assets/apps/shell/shell.lua +++ b/src/main/assets/apps/shell/shell.lua @@ -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 = '
' .. message .. '
' + 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 = '
' .. message .. '
' 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 .. ")") diff --git a/src/main/assets/apps/shell/shell.rml b/src/main/assets/apps/shell/shell.rml index 9cfb1f6..5541719 100644 --- a/src/main/assets/apps/shell/shell.rml +++ b/src/main/assets/apps/shell/shell.rml @@ -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 */