Redesign overlay to Windows Start Menu style - compact bottom bar with expand

- Windows-style search bar with rounded corners
- Positioned at bottom center of screen
- Logo button to expand/collapse (like Windows Start)
- Search input with type-to-search
- Expanded view shows Quick Access grid (6 items)
- Categorized list (Tools, Plugins, System)
- Keyboard shortcuts displayed
- Active plugin indicators
- Click-through when not focused
- Session timer in header
- Smooth expand/collapse animations
This commit is contained in:
Aether 2026-02-23 21:32:13 +00:00
parent b60b67f508
commit 5ec15e12f3
No known key found for this signature in database
GPG Key ID: 95AFEE837E39AFD2
2 changed files with 309 additions and 210 deletions

View File

@ -1,4 +1,4 @@
use tauri::{AppHandle, Manager, Window, WindowBuilder, WindowUrl};
use tauri::{AppHandle, Manager, Window, WindowBuilder, WindowUrl, Position, PhysicalPosition};
use tracing::info;
pub fn create_main_window(app: &AppHandle) -> Window {
@ -16,43 +16,39 @@ pub fn create_main_window(app: &AppHandle) -> Window {
}
pub fn setup_main_window(window: &Window) {
// Additional window setup if needed
info!("Main window setup complete");
}
pub fn create_overlay_window(app: &AppHandle) {
// Get primary monitor size
let monitor = app.primary_monitor().unwrap();
let monitor_size = monitor.as_ref().map(|m| m.size()).unwrap_or_else(|| tauri::PhysicalSize::new(1920, 1080));
// Position overlay at bottom center like Windows Start Menu
let window_width = 720.0;
let window_height = 600.0; // Expanded height
let x = (monitor_size.width as f64 - window_width) / 2.0;
let y = monitor_size.height as f64 - window_height - 20.0; // 20px from bottom
let window = WindowBuilder::new(
app,
"overlay",
WindowUrl::App("/#/overlay".into())
)
.title("EU-Utility Overlay")
.inner_size(400.0, 600.0)
.position(100.0, 100.0)
.inner_size(window_width, window_height)
.position(x, y)
.transparent(true)
.decorations(false)
.always_on_top(true)
.skip_taskbar(true)
.visible(false)
// Enable click-through when not focused
.focus()
.build()
.expect("Failed to create overlay window");
// Make window click-through when not focused
// This allows clicking through to the game when overlay is not active
#[cfg(target_os = "windows")]
{
use windows::Win32::Foundation::HWND;
use windows::Win32::UI::WindowsAndMessaging::{SetWindowLongW, GetWindowLongW, GWL_EXSTYLE};
use windows::Win32::UI::WindowsAndMessaging::{WS_EX_LAYERED, WS_EX_TRANSPARENT};
unsafe {
let hwnd = HWND(window.hwnd().unwrap().0 as *mut _);
let ex_style = GetWindowLongW(hwnd, GWL_EXSTYLE);
SetWindowLongW(hwnd, GWL_EXSTYLE, ex_style | WS_EX_LAYERED.0 as i32 | WS_EX_TRANSPARENT.0 as i32);
}
}
info!("Overlay window created with transparency and click-through");
info!("Overlay window created at bottom center (Windows-style)");
}
pub fn toggle_overlay_window(app: &AppHandle) {
@ -61,15 +57,42 @@ pub fn toggle_overlay_window(app: &AppHandle) {
if is_visible {
window.hide().ok();
// Re-enable click-through
enable_click_through(&window, true);
} else {
window.show().ok();
window.set_always_on_top(true).ok();
window.set_focus().ok();
// Disable click-through when active
enable_click_through(&window, false);
}
} else {
create_overlay_window(app);
if let Some(window) = app.get_window("overlay") {
window.show().ok();
enable_click_through(&window, false);
}
}
}
fn enable_click_through(window: &Window, enable: bool) {
#[cfg(target_os = "windows")]
unsafe {
use windows::Win32::Foundation::HWND;
use windows::Win32::UI::WindowsAndMessaging::{SetWindowLongW, GetWindowLongW, GWL_EXSTYLE};
use windows::Win32::UI::WindowsAndMessaging::{WS_EX_LAYERED, WS_EX_TRANSPARENT, WS_EX_NOACTIVATE};
let hwnd = HWND(window.hwnd().unwrap().0 as *mut _);
let ex_style = GetWindowLongW(hwnd, GWL_EXSTYLE);
if enable {
// Enable click-through
SetWindowLongW(hwnd, GWL_EXSTYLE,
ex_style | WS_EX_LAYERED.0 as i32 | WS_EX_TRANSPARENT.0 as i32);
} else {
// Disable click-through
SetWindowLongW(hwnd, GWL_EXSTYLE,
ex_style & !(WS_EX_TRANSPARENT.0 as i32));
}
}
}
@ -79,34 +102,13 @@ pub fn show_overlay_window(app: &AppHandle) {
window.show().ok();
window.set_always_on_top(true).ok();
window.set_focus().ok();
enable_click_through(&window, false);
}
}
pub fn hide_overlay_window(app: &AppHandle) {
if let Some(window) = app.get_window("overlay") {
window.hide().ok();
enable_click_through(&window, true);
}
}
pub fn setup_game_window_detection(app: &AppHandle) {
// Set up a timer to check if game window is focused
// Only show overlay when game is focused (or on hotkey)
let app_handle = app.clone();
std::thread::spawn(move || {
loop {
std::thread::sleep(std::time::Duration::from_secs(1));
// Check if overlay should auto-hide when game loses focus
// This is optional behavior - overlay can stay visible
if let Some(overlay) = app_handle.get_window("overlay") {
if let Ok(is_visible) = overlay.is_visible() {
if is_visible {
// Optional: Auto-hide when game not focused
// Check game focus here and hide if needed
}
}
}
}
});
}

View File

@ -1,50 +1,54 @@
import { useState, useEffect } from 'react'
import { useState, useEffect, useRef } from 'react'
import { invoke } from '@tauri-apps/api/tauri'
import { listen } from '@tauri-apps/api/event'
import {
Search,
LayoutDashboard,
Calculator,
Target,
TrendingUp,
Calculator,
Settings,
X,
GripHorizontal,
ChevronUp,
GripVertical,
Zap,
Search,
Grid,
Crosshair,
Clock,
BarChart3,
Layers
Clock,
Layers,
Maximize2,
Minimize2,
Grid,
Command
} from 'lucide-react'
interface QuickAction {
interface MenuItem {
id: string
icon: React.ReactNode
label: string
description?: string
hotkey?: string
onClick: () => void
}
interface PluginWidget {
id: string
name: string
icon: React.ReactNode
active: boolean
onToggle: () => void
category: 'tools' | 'plugins' | 'system'
}
export default function Overlay() {
const [isVisible, setIsVisible] = useState(false)
const [activePlugins, setActivePlugins] = useState<string[]>([])
const [sessionTime, setSessionTime] = useState(0)
const [currentPED, setCurrentPED] = useState(0)
const [isExpanded, setIsExpanded] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [activePlugins, setActivePlugins] = useState<string[]>(['loot', 'skills'])
const [sessionTime, setSessionTime] = useState(0)
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
// Listen for overlay toggle
const unlisten = listen('overlay:toggle', () => {
setIsVisible(prev => !prev)
setIsVisible(prev => {
const newVisible = !prev
if (newVisible && inputRef.current) {
setTimeout(() => inputRef.current?.focus(), 100)
}
return newVisible
})
})
// Session timer
@ -58,205 +62,298 @@ export default function Overlay() {
}
}, [])
useEffect(() => {
if (isVisible && !isExpanded && inputRef.current) {
inputRef.current.focus()
}
}, [isVisible])
const formatTime = (seconds: number) => {
const h = Math.floor(seconds / 3600)
const m = Math.floor((seconds % 3600) / 60)
const s = seconds % 60
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`
}
const quickActions: QuickAction[] = [
const menuItems: MenuItem[] = [
{
id: 'calculator',
icon: <Calculator className="w-5 h-5" />,
label: 'Calc',
label: 'Calculator',
description: 'Quick calculations',
hotkey: 'Ctrl+Shift+C',
onClick: () => invoke('open_calculator')
onClick: () => invoke('open_calculator'),
category: 'tools'
},
{
id: 'loot-tracker',
icon: <Target className="w-5 h-5" />,
label: 'Loot',
label: 'Loot Tracker',
description: 'Track hunting sessions',
hotkey: 'Ctrl+Shift+L',
onClick: () => invoke('toggle_loot_tracker')
onClick: () => togglePlugin('loot'),
category: 'plugins'
},
{
id: 'skill-tracker',
icon: <TrendingUp className="w-5 h-5" />,
label: 'Skill Tracker',
description: 'Monitor skill gains',
onClick: () => togglePlugin('skills'),
category: 'plugins'
},
{
id: 'price-check',
icon: <BarChart3 className="w-5 h-5" />,
label: 'Prices',
onClick: () => invoke('open_price_checker')
label: 'Price Check',
description: 'Item market values',
onClick: () => invoke('open_price_checker'),
category: 'tools'
},
{
id: 'skills',
icon: <TrendingUp className="w-5 h-5" />,
label: 'Skills',
onClick: () => invoke('open_skill_tracker')
},
{
id: 'search',
icon: <Search className="w-5 h-5" />,
label: 'Search',
hotkey: 'Ctrl+Shift+F',
onClick: () => invoke('open_universal_search')
id: 'dashboard',
icon: <LayoutDashboard className="w-5 h-5" />,
label: 'Dashboard',
description: 'Main application window',
onClick: () => invoke('show_main_window'),
category: 'system'
},
{
id: 'settings',
icon: <Settings className="w-5 h-5" />,
label: 'Settings',
onClick: () => invoke('show_settings_window')
}
description: 'Configure application',
onClick: () => invoke('show_settings_window'),
category: 'system'
},
]
const plugins: PluginWidget[] = [
{ id: '1', name: 'Loot Tracker', icon: <Target className="w-4 h-4" />, active: true, onToggle: () => {} },
{ id: '2', name: 'HP Monitor', icon: <Crosshair className="w-4 h-4" />, active: false, onToggle: () => {} },
{ id: '3', name: 'Skill Watch', icon: <TrendingUp className="w-4 h-4" />, active: true, onToggle: () => {} },
{ id: '4', name: 'Coords', icon: <Grid className="w-4 h-4" />, active: false, onToggle: () => {} },
]
const togglePlugin = (pluginId: string) => {
setActivePlugins(prev =>
prev.includes(pluginId)
? prev.filter(id => id !== pluginId)
: [...prev, pluginId]
)
}
const filteredPlugins = plugins.filter(p =>
p.name.toLowerCase().includes(searchQuery.toLowerCase())
)
const filteredItems = searchQuery
? menuItems.filter(item =>
item.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.description?.toLowerCase().includes(searchQuery.toLowerCase())
)
: menuItems
const groupedItems = filteredItems.reduce((acc, item) => {
if (!acc[item.category]) acc[item.category] = []
acc[item.category].push(item)
return acc
}, {} as Record<string, MenuItem[]>)
if (!isVisible) return null
return (
<div className="fixed inset-0 pointer-events-none flex items-start justify-center pt-4">
{/* Overlay Container */}
<div className="fixed inset-0 pointer-events-none flex items-end justify-center pb-6">
{/* Windows-Style Overlay Container */}
<div
className="pointer-events-auto animate-in slide-in-from-top-4 duration-200"
className="pointer-events-auto transition-all duration-300 ease-out"
style={{
background: 'rgba(10, 10, 15, 0.92)',
backdropFilter: 'blur(20px) saturate(180%)',
WebkitBackdropFilter: 'blur(20px) saturate(180%)',
border: '1px solid rgba(99, 102, 241, 0.3)',
borderRadius: '16px',
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.8), 0 0 0 1px rgba(99, 102, 241, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.05)',
minWidth: '380px',
maxWidth: '420px',
width: isExpanded ? '720px' : '640px',
maxWidth: '90vw',
}}
>
{/* Header - Drag Handle */}
{/* Main Search Bar - Windows Style */}
<div
className="flex items-center justify-between px-4 py-3 border-b border-white/10 cursor-move"
data-tauri-drag-region
className="relative overflow-hidden"
style={{
background: 'rgba(32, 32, 32, 0.95)',
backdropFilter: 'blur(32px) saturate(180%)',
WebkitBackdropFilter: 'blur(32px) saturate(180%)',
borderRadius: isExpanded ? '12px' : '32px',
border: '1px solid rgba(255, 255, 255, 0.08)',
boxShadow: isExpanded
? '0 32px 64px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.05), inset 0 1px 0 rgba(255, 255, 255, 0.1)'
: '0 8px 32px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.05)',
}}
>
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center shadow-lg shadow-indigo-500/20">
<Layers className="w-4 h-4 text-white" />
</div>
<div>
<h3 className="text-sm font-semibold text-white">EU-Utility</h3>
<p className="text-xs text-white/50">App Drawer</p>
</div>
</div>
<div className="flex items-center gap-2">
{/* Session Timer */}
<div className="flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-white/5 border border-white/10">
<Clock className="w-3 h-3 text-indigo-400" />
<span className="text-xs font-mono text-white/80">{formatTime(sessionTime)}</span>
</div>
<button
onClick={() => setIsVisible(false)}
className="w-7 h-7 flex items-center justify-center rounded-lg hover:bg-white/10 transition-colors"
{/* Search Input Row */}
<div
className="flex items-center gap-3 px-5 h-14"
data-tauri-drag-region
>
{/* Logo Icon - Windows Style */}
<div
className="flex-shrink-0 w-10 h-10 rounded-xl flex items-center justify-center cursor-pointer transition-all hover:scale-105 active:scale-95"
style={{
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%)',
boxShadow: '0 4px 12px rgba(99, 102, 241, 0.4)',
}}
onClick={() => setIsExpanded(!isExpanded)}
>
<X className="w-4 h-4 text-white/60 hover:text-white" />
</button>
</div>
</div>
<Layers className="w-5 h-5 text-white" />
</div>
{/* Search Bar */}
<div className="px-4 py-3 border-b border-white/10">
<div className="relative">
<Search className="w-4 h-4 text-white/40 absolute left-3 top-1/2 -translate-y-1/2" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search plugins..."
className="w-full pl-9 pr-3 py-2 bg-black/30 border border-white/10 rounded-lg text-sm text-white placeholder:text-white/30 focus:outline-none focus:border-indigo-500/50 transition-colors"
/>
</div>
</div>
{/* Search Input */}
<div className="flex-1 relative">
<input
ref={inputRef}
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onFocus={() => setIsExpanded(true)}
placeholder="Type to search plugins, tools..."
className="w-full bg-transparent border-none text-white text-base placeholder:text-white/40 focus:outline-none"
style={{ fontSize: '15px' }}
/>
</div>
{/* Quick Actions Grid */}
<div className="px-4 py-3 border-b border-white/10">
<p className="text-xs font-medium text-white/40 uppercase tracking-wider mb-3">Quick Actions</p>
<div className="grid grid-cols-3 gap-2">
{quickActions.map(action => (
{/* Right Side Actions */}
<div className="flex items-center gap-2">
{/* Session Timer */}
<div
className="flex items-center gap-2 px-3 py-1.5 rounded-full"
style={{ background: 'rgba(255, 255, 255, 0.06)' }}
>
<Clock className="w-4 h-4 text-indigo-400" />
<span className="text-sm font-mono text-white/80">{formatTime(sessionTime)}</span>
</div>
{/* Expand/Collapse */}
<button
key={action.id}
onClick={action.onClick}
className="group flex flex-col items-center gap-1.5 p-2.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/5 hover:border-indigo-500/30 transition-all duration-150"
onClick={() => setIsExpanded(!isExpanded)}
className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-white/10 transition-colors"
>
<div className="w-9 h-9 rounded-lg bg-gradient-to-br from-indigo-500/20 to-purple-500/20 group-hover:from-indigo-500/30 group-hover:to-purple-500/30 flex items-center justify-center transition-colors">
<div className="text-indigo-400 group-hover:text-indigo-300 transition-colors">
{action.icon}
</div>
</div>
<span className="text-[10px] font-medium text-white/70 group-hover:text-white transition-colors">
{action.label}
</span>
{isExpanded ? (
<Minimize2 className="w-4 h-4 text-white/60" />
) : (
<Maximize2 className="w-4 h-4 text-white/60" />
)}
</button>
))}
</div>
</div>
{/* Active Plugins */}
<div className="px-4 py-3">
<div className="flex items-center justify-between mb-3">
<p className="text-xs font-medium text-white/40 uppercase tracking-wider">Plugins</p>
<span className="text-xs text-white/30">{filteredPlugins.filter(p => p.active).length} active</span>
</div>
<div className="space-y-1.5">
{filteredPlugins.map(plugin => (
<div
key={plugin.id}
className={`flex items-center justify-between p-2.5 rounded-lg border transition-all cursor-pointer ${
plugin.active
? 'bg-indigo-500/10 border-indigo-500/30'
: 'bg-white/5 border-white/5 hover:border-white/10'
}`}
onClick={plugin.onToggle}
{/* Close */}
<button
onClick={() => setIsVisible(false)}
className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-white/10 transition-colors"
>
<div className="flex items-center gap-2.5">
<div className={`w-7 h-7 rounded-md flex items-center justify-center ${
plugin.active ? 'bg-indigo-500/20 text-indigo-400' : 'bg-white/10 text-white/50'
}`}>
{plugin.icon}
<X className="w-4 h-4 text-white/60 hover:text-white" />
</button>
</div>
</div>
{/* Expanded Menu - Windows Start Menu Style */}
{isExpanded && (
<div
className="border-t border-white/10"
style={{
maxHeight: '480px',
overflow: 'hidden',
}}
>
{/* Quick Actions Grid */}
<div className="p-5">
<div className="flex items-center justify-between mb-4">
<span className="text-xs font-semibold uppercase tracking-wider text-white/40">Quick Access</span>
<div className="flex items-center gap-2">
<span className="text-xs text-white/30">{activePlugins.length} active</span>
</div>
<span className={`text-xs font-medium ${plugin.active ? 'text-white' : 'text-white/60'}`}>
{plugin.name}
</span>
</div>
<div className={`w-8 h-4 rounded-full relative transition-colors ${
plugin.active ? 'bg-indigo-500' : 'bg-white/20'
}`}>
<div className={`absolute top-0.5 w-3 h-3 rounded-full bg-white transition-all ${
plugin.active ? 'left-4.5' : 'left-0.5'
}`} />
<div className="grid grid-cols-6 gap-2">
{menuItems.slice(0, 6).map((item) => (
<button
key={item.id}
onClick={() => {
item.onClick()
if (!item.id.includes('tracker')) setIsVisible(false)
}}
className="group flex flex-col items-center gap-2 p-3 rounded-xl transition-all hover:bg-white/10"
>
<div
className={`w-12 h-12 rounded-xl flex items-center justify-center transition-all ${
activePlugins.includes(item.id) || item.id === 'loot' && activePlugins.includes('loot')
? 'bg-indigo-500/20 text-indigo-400 ring-1 ring-indigo-500/30'
: 'bg-white/5 text-white/60 group-hover:bg-white/10 group-hover:text-white'
}`}
>
{item.icon}
</div>
<span className="text-[11px] text-white/70 text-center leading-tight">{item.label}</span>
</button>
))}
</div>
</div>
))}
</div>
</div>
{/* Stats Footer */}
<div className="px-4 py-3 border-t border-white/10 bg-white/5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
<span className="text-xs text-white/60">System Online</span>
{/* Search Results or All Items */}
<div className="px-5 pb-5" style={{ maxHeight: '280px', overflowY: 'auto' }}>
{Object.entries(groupedItems).map(([category, items]) => (
<div key={category} className="mb-4 last:mb-0">
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-semibold uppercase tracking-wider text-white/40">
{category === 'tools' ? 'Tools' : category === 'plugins' ? 'Plugins' : 'System'}
</span>
</div>
<div className="space-y-1">
{items.map((item) => (
<button
key={item.id}
onClick={() => {
item.onClick()
if (!item.id.includes('tracker')) setIsVisible(false)
}}
className="w-full flex items-center gap-3 p-3 rounded-lg hover:bg-white/10 transition-all group"
>
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${
activePlugins.includes(item.id) || (item.id === 'loot-tracker' && activePlugins.includes('loot'))
? 'bg-indigo-500/20 text-indigo-400'
: 'bg-white/5 text-white/60 group-hover:text-white'
}`}>
{item.icon}
</div>
<div className="flex-1 text-left">
<div className="text-sm font-medium text-white/90 group-hover:text-white">{item.label}</div>
{item.description && (
<div className="text-xs text-white/40">{item.description}</div>
)}
</div>
{item.hotkey && (
<kbd className="px-2 py-1 text-xs rounded bg-white/5 text-white/40 font-mono">
{item.hotkey}
</kbd>
)}
{(activePlugins.includes(item.id) || (item.id === 'loot-tracker' && activePlugins.includes('loot'))) && (
<div className="w-2 h-2 rounded-full bg-emerald-400 shadow-[0_0_8px_rgba(52,211,153,0.5)]" />
)}
</button>
))}
</div>
</div>
))}
{filteredItems.length === 0 && (
<div className="text-center py-8 text-white/40">
<Search className="w-12 h-12 mx-auto mb-3 opacity-30" />
<p className="text-sm">No results found for "{searchQuery}"</p>
</div>
)}
</div>
{/* Footer */}
<div
className="flex items-center justify-between px-5 py-3 border-t border-white/10"
style={{ background: 'rgba(0, 0, 0, 0.3)' }}
>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
<span className="text-xs text-white/50">EU-Utility V3.0</span>
</div>
<div className="flex items-center gap-1 text-xs text-white/40">
<Command className="w-3 h-3" />
<span>Press</span>
<kbd className="px-1.5 py-0.5 rounded bg-white/10 text-white/60">Ctrl+Shift+U</kbd>
<span>to toggle</span>
</div>
</div>
</div>
<div className="flex items-center gap-1.5 text-xs text-white/50">
<span className="text-emerald-400 font-medium">{currentPED.toFixed(2)}</span>
<span>PED/h</span>
</div>
</div>
)}
</div>
</div>
</div>