diff --git a/blackglass-launcher/.gitignore b/blackglass-launcher/.gitignore
new file mode 100644
index 00000000..71a79ab1
--- /dev/null
+++ b/blackglass-launcher/.gitignore
@@ -0,0 +1,26 @@
+# Gradle
+.gradle/
+build/
+!gradle/wrapper/gradle-wrapper.jar
+
+# Android
+*.apk
+*.aab
+*.ap_
+*.dex
+local.properties
+
+# IDE
+.idea/
+*.iml
+.DS_Store
+
+# Kotlin
+*.kotlin_module
+
+# Build outputs
+app/build/
+app/release/
+
+# Logs
+*.log
diff --git a/blackglass-launcher/README.md b/blackglass-launcher/README.md
new file mode 100644
index 00000000..41738767
--- /dev/null
+++ b/blackglass-launcher/README.md
@@ -0,0 +1,151 @@
+# BlackGlass UI OS - Android Launcher
+
+
+
+
+
+> **Deep Glass Aesthetic** โ A tri-layer dark interface inspired by macOS and Cyberpunk with Neon Wine Red & Dark Violet accents.
+
+## โจ Features
+
+### ๐ฏ Top Tier Feature Set
+
+| Feature | Description | Status |
+|---------|-------------|--------|
+| **Elastic Grid** | Free placement of widgets/icons (Nova style) | โ
|
+| **Smart Sorting** | Auto-categorize apps: Social, Media, Games, Productivity | โ
|
+| **Universal Search** | Global search for Apps, Contacts, and Web | โ
|
+| **Glass Engine** | Real-time frosted glass blur using Haze library | โ
|
+| **Neon Dock** | macOS-style dock with touch glow effects | โ
|
+
+### ๐จ Visual Design
+
+- **Tri-Layer Glass System**: Background, middle, and foreground glass layers
+- **Neon Glow Effects**: Wine Red and Dark Violet gradient glows
+- **Smooth Animations**: Spring-based physics animations
+- **Material 3**: Full Material You theming support
+
+## ๐ฑ Screenshots
+
+*Coming soon*
+
+## ๐ Getting Started
+
+### Prerequisites
+
+- Android Studio Hedgehog (2023.1.1) or later
+- JDK 17
+- Android SDK 34 (Android 14)
+- Minimum SDK: 26 (Android 8.0)
+
+### Build Instructions
+
+1. Clone the repository:
+```bash
+git clone https://github.com/your-repo/blackglass-launcher.git
+cd blackglass-launcher
+```
+
+2. Open in Android Studio
+
+3. Sync Gradle and build:
+```bash
+./gradlew assembleDebug
+```
+
+4. Install on device:
+```bash
+./gradlew installDebug
+```
+
+### Setting as Default Launcher
+
+After installation, press the Home button and select "BlackGlass" from the launcher chooser. Select "Always" to make it your default launcher.
+
+## ๐๏ธ Architecture
+
+```
+com.blackglass.launcher/
+โโโ data/
+โ โโโ model/ # Data models (AppInfo, Categories, etc.)
+โ โโโ repository/ # Data repositories
+โโโ receiver/ # Broadcast receivers
+โโโ ui/
+โ โโโ components/ # Reusable UI components
+โ โ โโโ AppDrawer.kt
+โ โ โโโ AppIcon.kt
+โ โ โโโ ClockWidget.kt
+โ โ โโโ ElasticGrid.kt
+โ โ โโโ GlassComponents.kt
+โ โ โโโ NeonDock.kt
+โ โ โโโ UniversalSearch.kt
+โ โโโ screens/ # Screen composables
+โ โ โโโ HomeScreen.kt
+โ โโโ theme/ # Theme configuration
+โ โโโ Color.kt
+โ โโโ Theme.kt
+โ โโโ Typography.kt
+โโโ viewmodel/ # ViewModels
+โ โโโ LauncherViewModel.kt
+โโโ BlackGlassApplication.kt
+โโโ MainActivity.kt
+```
+
+## ๐ ๏ธ Tech Stack
+
+- **Language**: Kotlin 1.9.22
+- **UI Framework**: Jetpack Compose with Material 3
+- **Blur Engine**: [Haze](https://github.com/chrisbanes/haze) by Chris Banes
+- **System UI**: Accompanist System UI Controller
+- **Architecture**: MVVM with StateFlow
+- **Min SDK**: 26 (Android 8.0 Oreo)
+- **Target SDK**: 34 (Android 14)
+
+## ๐ฆ Dependencies
+
+```kotlin
+// Core
+implementation("androidx.core:core-ktx:1.12.0")
+implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
+implementation("androidx.activity:activity-compose:1.8.2")
+
+// Compose
+implementation(platform("androidx.compose:compose-bom:2024.02.00"))
+implementation("androidx.compose.material3:material3:1.2.0")
+
+// Blur Engine (Critical for Glass Effect)
+implementation("dev.chrisbanes.haze:haze:0.6.0")
+implementation("dev.chrisbanes.haze:haze-materials:0.6.0")
+
+// System UI
+implementation("com.google.accompanist:accompanist-systemuicontroller:0.32.0")
+implementation("com.google.accompanist:accompanist-drawablepainter:0.32.0")
+```
+
+## ๐จ Color Palette
+
+| Color | Hex | Usage |
+|-------|-----|-------|
+| Neon Wine | `#1A1A2E` | Primary background |
+| Neon Wine Bright | `#DC143C` | Primary accent |
+| Neon Wine Glow | `#FF4D6D` | Glow effects |
+| Dark Violet | `#16213E` | Secondary background |
+| Dark Violet Bright | `#9B59B6` | Secondary accent |
+| Accent Cyan | `#00D9FF` | Tertiary accent |
+
+## ๐ License
+
+This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
+
+## ๐ Acknowledgments
+
+- [Chris Banes](https://github.com/chrisbanes) for the amazing Haze blur library
+- Nova Launcher for elastic grid inspiration
+- Smart Launcher for smart sorting concept
+- Niagara Launcher for minimal design philosophy
+
+---
+
+
+ Made with โค๏ธ and โ
+
diff --git a/blackglass-launcher/app/build.gradle.kts b/blackglass-launcher/app/build.gradle.kts
new file mode 100644
index 00000000..dccacc9c
--- /dev/null
+++ b/blackglass-launcher/app/build.gradle.kts
@@ -0,0 +1,109 @@
+@Suppress("DSL_SCOPE_VIOLATION")
+plugins {
+ id("com.android.application")
+ id("org.jetbrains.kotlin.android")
+}
+
+android {
+ namespace = "com.blackglass.launcher"
+ compileSdk = 34
+
+ defaultConfig {
+ applicationId = "com.blackglass.launcher"
+ minSdk = 26
+ targetSdk = 34
+ versionCode = 1
+ versionName = "1.0.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables {
+ useSupportLibrary = true
+ }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = true
+ isShrinkResources = true
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ debug {
+ isDebuggable = true
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+
+ buildFeatures {
+ compose = true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.8"
+ }
+
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+}
+
+dependencies {
+ // Core Android
+ implementation("androidx.core:core-ktx:1.12.0")
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
+ implementation("androidx.activity:activity-compose:1.8.2")
+
+ // Compose BOM (Bill of Materials)
+ implementation(platform("androidx.compose:compose-bom:2024.02.00"))
+ implementation("androidx.compose.ui:ui")
+ implementation("androidx.compose.ui:ui-graphics")
+ implementation("androidx.compose.ui:ui-tooling-preview")
+ implementation("androidx.compose.material3:material3:1.2.0")
+ implementation("androidx.compose.material:material-icons-extended")
+ implementation("androidx.compose.animation:animation")
+ implementation("androidx.compose.foundation:foundation")
+
+ // Navigation
+ implementation("androidx.navigation:navigation-compose:2.7.7")
+
+ // CRITICAL: High-performance real-time glass blur engine
+ implementation("dev.chrisbanes.haze:haze:0.6.0")
+ implementation("dev.chrisbanes.haze:haze-materials:0.6.0")
+
+ // System UI Control (Transparent Status/Navigation Bars)
+ implementation("com.google.accompanist:accompanist-systemuicontroller:0.32.0")
+
+ // Permissions handling
+ implementation("com.google.accompanist:accompanist-permissions:0.32.0")
+
+ // Drawable painter for app icons
+ implementation("com.google.accompanist:accompanist-drawablepainter:0.32.0")
+
+ // Coil for image loading
+ implementation("io.coil-kt:coil-compose:2.5.0")
+
+ // Datastore for preferences
+ implementation("androidx.datastore:datastore-preferences:1.0.0")
+
+ // Testing
+ testImplementation("junit:junit:4.13.2")
+ androidTestImplementation("androidx.test.ext:junit:1.1.5")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
+ androidTestImplementation(platform("androidx.compose:compose-bom:2024.02.00"))
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4")
+ debugImplementation("androidx.compose.ui:ui-tooling")
+ debugImplementation("androidx.compose.ui:ui-test-manifest")
+}
diff --git a/blackglass-launcher/app/proguard-rules.pro b/blackglass-launcher/app/proguard-rules.pro
new file mode 100644
index 00000000..edd6659f
--- /dev/null
+++ b/blackglass-launcher/app/proguard-rules.pro
@@ -0,0 +1,10 @@
+# Add project specific ProGuard rules here.
+-keepclassmembers class * {
+ @androidx.compose.runtime.Composable ;
+}
+
+# Keep launcher activities
+-keep class com.blackglass.launcher.** { *; }
+
+# Haze blur library
+-keep class dev.chrisbanes.haze.** { *; }
diff --git a/blackglass-launcher/app/src/main/AndroidManifest.xml b/blackglass-launcher/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..e19f3919
--- /dev/null
+++ b/blackglass-launcher/app/src/main/AndroidManifest.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/BlackGlassApplication.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/BlackGlassApplication.kt
new file mode 100644
index 00000000..7c16c6e3
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/BlackGlassApplication.kt
@@ -0,0 +1,24 @@
+package com.blackglass.launcher
+
+import android.app.Application
+import android.content.Context
+
+/**
+ * BlackGlass Launcher Application
+ * Initializes core services and maintains app-level state
+ */
+class BlackGlassApplication : Application() {
+
+ companion object {
+ lateinit var instance: BlackGlassApplication
+ private set
+
+ val context: Context
+ get() = instance.applicationContext
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ instance = this
+ }
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/MainActivity.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/MainActivity.kt
new file mode 100644
index 00000000..bc2ffa82
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/MainActivity.kt
@@ -0,0 +1,62 @@
+package com.blackglass.launcher
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.ui.Modifier
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsControllerCompat
+import com.blackglass.launcher.ui.home.HomeScreen
+import com.blackglass.launcher.ui.theme.GlassTheme
+
+/**
+ * BlackGlass UI OS - Main Activity
+ * Entry point for the launcher application
+ */
+class MainActivity : ComponentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ // Enable edge-to-edge display
+ enableEdgeToEdge()
+
+ // Configure window for immersive launcher experience
+ configureWindow()
+
+ setContent {
+ GlassTheme {
+ HomeScreen(
+ modifier = Modifier.fillMaxSize()
+ )
+ }
+ }
+ }
+
+ private fun configureWindow() {
+ // Fit system windows for proper inset handling
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+
+ // Get the insets controller
+ val controller = WindowInsetsControllerCompat(window, window.decorView)
+
+ // Configure system bar appearance
+ controller.apply {
+ // Use dark icons/text on light backgrounds, light on dark
+ isAppearanceLightStatusBars = false
+ isAppearanceLightNavigationBars = false
+ }
+ }
+
+ override fun onBackPressed() {
+ // Launchers typically don't respond to back button
+ // Could be used to close app drawer if open
+ }
+
+ override fun onResume() {
+ super.onResume()
+ // Could refresh app list here if needed
+ }
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/data/AppRepository.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/data/AppRepository.kt
new file mode 100644
index 00000000..0e3834b2
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/data/AppRepository.kt
@@ -0,0 +1,210 @@
+package com.blackglass.launcher.data
+
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.graphics.drawable.Drawable
+import android.os.Build
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.withContext
+
+/**
+ * BlackGlass UI OS - App Repository
+ * Fetches and manages installed applications
+ */
+
+/**
+ * Represents an installed application
+ */
+data class AppInfo(
+ val packageName: String,
+ val label: String,
+ val icon: Drawable?,
+ val category: AppCategory = AppCategory.UTILITIES,
+ val isSystemApp: Boolean = false,
+ val lastUsed: Long = 0L,
+ val usageCount: Int = 0
+)
+
+/**
+ * App category for smart sorting
+ */
+enum class AppCategory(val displayName: String, val emoji: String) {
+ ALL("All", "๐ฑ"),
+ SOCIAL("Social", "๐ฌ"),
+ MEDIA("Media", "๐ต"),
+ GAMES("Games", "๐ฎ"),
+ PRODUCTIVITY("Productivity", "๐ผ"),
+ UTILITIES("Utilities", "๐ง")
+}
+
+/**
+ * Repository for fetching and managing installed apps
+ */
+class AppRepository(private val context: Context) {
+
+ private val packageManager: PackageManager = context.packageManager
+ private val iconPackManager = IconPackManager(context)
+
+ private val _apps = MutableStateFlow>(emptyList())
+ val apps: Flow> = _apps.asStateFlow()
+
+ private val _isLoading = MutableStateFlow(false)
+ val isLoading: Flow = _isLoading.asStateFlow()
+
+ /**
+ * Load all launchable apps from the device
+ */
+ suspend fun loadApps() = withContext(Dispatchers.IO) {
+ _isLoading.value = true
+
+ try {
+ val mainIntent = Intent(Intent.ACTION_MAIN, null).apply {
+ addCategory(Intent.CATEGORY_LAUNCHER)
+ }
+
+ val resolveInfoList: List = queryActivities(mainIntent)
+
+ val appList = resolveInfoList.mapNotNull { resolveInfo ->
+ createAppInfo(resolveInfo)
+ }.sortedBy { it.label.lowercase() }
+
+ _apps.value = appList
+ } finally {
+ _isLoading.value = false
+ }
+ }
+
+ private fun queryActivities(intent: Intent): List {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ packageManager.queryIntentActivities(
+ intent,
+ PackageManager.ResolveInfoFlags.of(0)
+ )
+ } else {
+ @Suppress("DEPRECATION")
+ packageManager.queryIntentActivities(intent, 0)
+ }
+ }
+
+ private fun createAppInfo(resolveInfo: ResolveInfo): AppInfo? {
+ return try {
+ val packageName = resolveInfo.activityInfo.packageName
+
+ // Skip our own launcher
+ if (packageName == context.packageName) return null
+
+ val label = resolveInfo.loadLabel(packageManager).toString()
+ val icon = iconPackManager.getIconForPackage(packageName)
+ ?: resolveInfo.loadIcon(packageManager)
+ val isSystemApp = (resolveInfo.activityInfo.applicationInfo.flags and
+ ApplicationInfo.FLAG_SYSTEM) != 0
+
+ AppInfo(
+ packageName = packageName,
+ label = label,
+ icon = icon,
+ category = categorizeApp(packageName),
+ isSystemApp = isSystemApp
+ )
+ } catch (e: Exception) {
+ null
+ }
+ }
+
+ /**
+ * Auto-categorize an app based on its package name
+ */
+ private fun categorizeApp(packageName: String): AppCategory {
+ val lowerPkg = packageName.lowercase()
+
+ return when {
+ SOCIAL_PATTERNS.any { lowerPkg.contains(it) } -> AppCategory.SOCIAL
+ MEDIA_PATTERNS.any { lowerPkg.contains(it) } -> AppCategory.MEDIA
+ GAME_PATTERNS.any { lowerPkg.contains(it) } -> AppCategory.GAMES
+ PRODUCTIVITY_PATTERNS.any { lowerPkg.contains(it) } -> AppCategory.PRODUCTIVITY
+ else -> AppCategory.UTILITIES
+ }
+ }
+
+ /**
+ * Get apps filtered by category
+ */
+ fun getAppsByCategory(category: AppCategory): List {
+ return if (category == AppCategory.ALL) {
+ _apps.value
+ } else {
+ _apps.value.filter { it.category == category }
+ }
+ }
+
+ /**
+ * Search apps by query
+ */
+ fun searchApps(query: String): List {
+ if (query.isBlank()) return _apps.value
+
+ val lowerQuery = query.lowercase()
+ return _apps.value.filter {
+ it.label.lowercase().contains(lowerQuery) ||
+ it.packageName.lowercase().contains(lowerQuery)
+ }
+ }
+
+ /**
+ * Launch an app by package name
+ */
+ fun launchApp(packageName: String) {
+ val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
+ launchIntent?.let {
+ it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ context.startActivity(it)
+ }
+ }
+
+ /**
+ * Get app info by package name
+ */
+ fun getApp(packageName: String): AppInfo? {
+ return _apps.value.find { it.packageName == packageName }
+ }
+
+ /**
+ * Refresh app list
+ */
+ suspend fun refresh() = loadApps()
+
+ companion object {
+ private val SOCIAL_PATTERNS = listOf(
+ "facebook", "twitter", "instagram", "whatsapp", "telegram",
+ "messenger", "snapchat", "tiktok", "discord", "slack",
+ "linkedin", "reddit", "pinterest", "wechat", "viber",
+ "signal", "skype", "zoom", "teams", "mastodon"
+ )
+
+ private val MEDIA_PATTERNS = listOf(
+ "youtube", "netflix", "spotify", "music", "video",
+ "player", "gallery", "photos", "camera", "podcast",
+ "twitch", "prime", "hulu", "disney", "hbo",
+ "soundcloud", "deezer", "radio", "vlc", "plex"
+ )
+
+ private val GAME_PATTERNS = listOf(
+ "game", "play", "puzzle", "racing", "adventure",
+ "arcade", "minecraft", "roblox", "fortnite", "pubg",
+ "clash", "candy", "angry", "temple", "subway"
+ )
+
+ private val PRODUCTIVITY_PATTERNS = listOf(
+ "docs", "sheets", "slides", "office", "word",
+ "excel", "powerpoint", "notion", "evernote", "notes",
+ "calendar", "mail", "gmail", "outlook", "drive",
+ "dropbox", "trello", "asana", "todoist", "keep"
+ )
+ }
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/data/IconPackManager.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/data/IconPackManager.kt
new file mode 100644
index 00000000..f4ff6a56
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/data/IconPackManager.kt
@@ -0,0 +1,267 @@
+package com.blackglass.launcher.data
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.os.Build
+import androidx.core.graphics.drawable.toBitmap
+import org.xmlpull.v1.XmlPullParser
+import org.xmlpull.v1.XmlPullParserFactory
+
+/**
+ * BlackGlass UI OS - Icon Pack Manager
+ * Handles icon rendering and icon pack support
+ */
+class IconPackManager(private val context: Context) {
+
+ private val packageManager: PackageManager = context.packageManager
+
+ // Cache for loaded icon packs
+ private val iconPackCache = mutableMapOf()
+
+ // Currently active icon pack
+ private var activeIconPack: IconPack? = null
+
+ /**
+ * Represents an icon pack
+ */
+ data class IconPack(
+ val packageName: String,
+ val name: String,
+ val iconMap: Map, // component -> drawable name
+ val resources: Resources?,
+ val scaleFactor: Float = 1.0f,
+ val backImages: List = emptyList(),
+ val maskImage: Drawable? = null,
+ val frontImage: Drawable? = null
+ )
+
+ /**
+ * Get all installed icon packs
+ */
+ fun getInstalledIconPacks(): List {
+ val iconPacks = mutableListOf()
+
+ // Common icon pack intent actions
+ val intents = listOf(
+ Intent("org.adw.launcher.THEMES"),
+ Intent("com.gau.go.launcherex.theme"),
+ Intent("com.novalauncher.THEME"),
+ Intent("com.teslacoilsw.launcher.THEME"),
+ Intent("com.fede.launcher.THEME_ICONPACK"),
+ Intent("android.intent.action.MAIN").apply {
+ addCategory("com.anddoes.launcher.THEME")
+ }
+ )
+
+ val packageNames = mutableSetOf()
+
+ for (intent in intents) {
+ val resolveInfoList = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ packageManager.queryIntentActivities(
+ intent,
+ PackageManager.ResolveInfoFlags.of(0)
+ )
+ } else {
+ @Suppress("DEPRECATION")
+ packageManager.queryIntentActivities(intent, 0)
+ }
+
+ for (info in resolveInfoList) {
+ val pkgName = info.activityInfo.packageName
+ if (pkgName !in packageNames) {
+ packageNames.add(pkgName)
+ loadIconPack(pkgName)?.let { iconPacks.add(it) }
+ }
+ }
+ }
+
+ return iconPacks
+ }
+
+ /**
+ * Load an icon pack by package name
+ */
+ private fun loadIconPack(packageName: String): IconPack? {
+ // Check cache first
+ iconPackCache[packageName]?.let { return it }
+
+ return try {
+ val resources = packageManager.getResourcesForApplication(packageName)
+ val appInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ packageManager.getApplicationInfo(
+ packageName,
+ PackageManager.ApplicationInfoFlags.of(0)
+ )
+ } else {
+ @Suppress("DEPRECATION")
+ packageManager.getApplicationInfo(packageName, 0)
+ }
+
+ val name = packageManager.getApplicationLabel(appInfo).toString()
+ val iconMap = parseAppFilterXml(resources, packageName)
+
+ val iconPack = IconPack(
+ packageName = packageName,
+ name = name,
+ iconMap = iconMap,
+ resources = resources
+ )
+
+ iconPackCache[packageName] = iconPack
+ iconPack
+ } catch (e: Exception) {
+ null
+ }
+ }
+
+ /**
+ * Parse appfilter.xml from icon pack
+ */
+ private fun parseAppFilterXml(resources: Resources, packageName: String): Map {
+ val iconMap = mutableMapOf()
+
+ try {
+ val resourceId = resources.getIdentifier("appfilter", "xml", packageName)
+ if (resourceId == 0) return iconMap
+
+ val parser = resources.getXml(resourceId)
+ var eventType = parser.eventType
+
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ if (eventType == XmlPullParser.START_TAG && parser.name == "item") {
+ val component = parser.getAttributeValue(null, "component")
+ val drawable = parser.getAttributeValue(null, "drawable")
+
+ if (component != null && drawable != null) {
+ iconMap[component] = drawable
+ }
+ }
+ eventType = parser.next()
+ }
+ } catch (e: Exception) {
+ // Silently fail - return empty map
+ }
+
+ return iconMap
+ }
+
+ /**
+ * Set the active icon pack
+ */
+ fun setActiveIconPack(packageName: String?) {
+ activeIconPack = packageName?.let { loadIconPack(it) }
+ }
+
+ /**
+ * Get icon for a package using active icon pack
+ */
+ fun getIconForPackage(packageName: String): Drawable? {
+ val iconPack = activeIconPack ?: return null
+
+ // Try to find in icon pack
+ val componentName = "ComponentInfo{$packageName}"
+ val drawableName = iconPack.iconMap.entries.find {
+ it.key.contains(packageName)
+ }?.value
+
+ return drawableName?.let {
+ getDrawableFromIconPack(iconPack, it)
+ }
+ }
+
+ /**
+ * Get drawable from icon pack resources
+ */
+ private fun getDrawableFromIconPack(iconPack: IconPack, drawableName: String): Drawable? {
+ return try {
+ val resources = iconPack.resources ?: return null
+ val resourceId = resources.getIdentifier(
+ drawableName,
+ "drawable",
+ iconPack.packageName
+ )
+ if (resourceId != 0) {
+ resources.getDrawable(resourceId, null)
+ } else null
+ } catch (e: Exception) {
+ null
+ }
+ }
+
+ /**
+ * Apply glass effect to an icon
+ */
+ fun applyGlassEffect(drawable: Drawable, glowColor: Int): Drawable {
+ val bitmap = drawable.toBitmap(96, 96)
+ val result = Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888)
+ val canvas = Canvas(result)
+
+ // Draw original icon
+ canvas.drawBitmap(bitmap, 0f, 0f, null)
+
+ // Add subtle glass overlay
+ val paint = Paint().apply {
+ colorFilter = PorterDuffColorFilter(
+ glowColor and 0x33FFFFFF, // 20% opacity
+ PorterDuff.Mode.OVERLAY
+ )
+ }
+ canvas.drawBitmap(bitmap, 0f, 0f, paint)
+
+ return BitmapDrawable(context.resources, result)
+ }
+
+ /**
+ * Create adaptive icon with mask
+ */
+ fun createMaskedIcon(
+ drawable: Drawable,
+ maskDrawable: Drawable?,
+ size: Int = 96
+ ): Drawable {
+ val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
+ val canvas = Canvas(bitmap)
+
+ // Draw icon
+ drawable.setBounds(0, 0, size, size)
+ drawable.draw(canvas)
+
+ // Apply mask if available
+ maskDrawable?.let { mask ->
+ val maskPaint = Paint().apply {
+ xfermode = android.graphics.PorterDuffXfermode(PorterDuff.Mode.DST_IN)
+ }
+ mask.setBounds(0, 0, size, size)
+
+ val maskBitmap = mask.toBitmap(size, size)
+ canvas.drawBitmap(maskBitmap, 0f, 0f, maskPaint)
+ }
+
+ return BitmapDrawable(context.resources, bitmap)
+ }
+
+ /**
+ * Get the icon size based on display density
+ */
+ fun getIconSize(): Int {
+ val density = context.resources.displayMetrics.density
+ return (48 * density).toInt()
+ }
+
+ /**
+ * Clear icon pack cache
+ */
+ fun clearCache() {
+ iconPackCache.clear()
+ }
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/data/model/Models.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/data/model/Models.kt
new file mode 100644
index 00000000..f1bae2d2
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/data/model/Models.kt
@@ -0,0 +1,115 @@
+package com.blackglass.launcher.data.model
+
+import android.graphics.drawable.Drawable
+
+/**
+ * App Category for Smart Sorting feature
+ */
+enum class AppCategory(val displayName: String) {
+ ALL("All"),
+ SOCIAL("Social"),
+ MEDIA("Media"),
+ GAMES("Games"),
+ PRODUCTIVITY("Productivity"),
+ UTILITIES("Utilities")
+}
+
+/**
+ * Represents an installed application
+ */
+data class AppInfo(
+ val packageName: String,
+ val label: String,
+ val icon: Drawable?,
+ val category: AppCategory = AppCategory.UTILITIES,
+ val isSystemApp: Boolean = false,
+ val lastUsed: Long = 0L,
+ val usageCount: Int = 0
+) {
+ companion object {
+ // Package name patterns for smart categorization
+ private val socialPatterns = listOf(
+ "facebook", "twitter", "instagram", "whatsapp", "telegram",
+ "messenger", "snapchat", "tiktok", "discord", "slack",
+ "linkedin", "reddit", "pinterest", "tumblr", "wechat",
+ "viber", "signal", "skype", "zoom", "teams"
+ )
+
+ private val mediaPatterns = listOf(
+ "youtube", "netflix", "spotify", "music", "video",
+ "player", "gallery", "photos", "camera", "podcast",
+ "twitch", "prime", "hulu", "disney", "hbo",
+ "soundcloud", "deezer", "radio", "vlc", "plex"
+ )
+
+ private val gamePatterns = listOf(
+ "game", "play", "puzzle", "racing", "adventure",
+ "arcade", "minecraft", "roblox", "fortnite", "pubg",
+ "clash", "candy", "angry", "temple", "subway"
+ )
+
+ private val productivityPatterns = listOf(
+ "docs", "sheets", "slides", "office", "word",
+ "excel", "powerpoint", "notion", "evernote", "notes",
+ "calendar", "mail", "gmail", "outlook", "drive",
+ "dropbox", "trello", "asana", "todoist", "keep"
+ )
+
+ /**
+ * Auto-categorize an app based on its package name
+ */
+ fun categorize(packageName: String): AppCategory {
+ val lowerPkg = packageName.lowercase()
+
+ return when {
+ socialPatterns.any { lowerPkg.contains(it) } -> AppCategory.SOCIAL
+ mediaPatterns.any { lowerPkg.contains(it) } -> AppCategory.MEDIA
+ gamePatterns.any { lowerPkg.contains(it) } -> AppCategory.GAMES
+ productivityPatterns.any { lowerPkg.contains(it) } -> AppCategory.PRODUCTIVITY
+ else -> AppCategory.UTILITIES
+ }
+ }
+ }
+}
+
+/**
+ * Represents a dock item
+ */
+data class DockItem(
+ val position: Int,
+ val appInfo: AppInfo? = null,
+ val isEmpty: Boolean = true
+)
+
+/**
+ * Widget placement info for elastic grid
+ */
+data class WidgetInfo(
+ val id: Int,
+ val appWidgetId: Int,
+ val packageName: String,
+ val className: String,
+ val x: Float,
+ val y: Float,
+ val width: Int,
+ val height: Int
+)
+
+/**
+ * Contact info for universal search
+ */
+data class ContactInfo(
+ val id: String,
+ val name: String,
+ val phoneNumber: String?,
+ val photoUri: String?
+)
+
+/**
+ * Search result wrapper
+ */
+sealed class SearchResult {
+ data class App(val appInfo: AppInfo) : SearchResult()
+ data class Contact(val contactInfo: ContactInfo) : SearchResult()
+ data class Web(val query: String) : SearchResult()
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/data/repository/AppRepository.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/data/repository/AppRepository.kt
new file mode 100644
index 00000000..b5af2220
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/data/repository/AppRepository.kt
@@ -0,0 +1,121 @@
+package com.blackglass.launcher.data.repository
+
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.os.Build
+import com.blackglass.launcher.data.model.AppCategory
+import com.blackglass.launcher.data.model.AppInfo
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.withContext
+
+/**
+ * Repository for managing installed applications
+ */
+class AppRepository(private val context: Context) {
+
+ private val packageManager: PackageManager = context.packageManager
+
+ private val _apps = MutableStateFlow>(emptyList())
+ val apps: Flow> = _apps.asStateFlow()
+
+ private val _isLoading = MutableStateFlow(false)
+ val isLoading: Flow = _isLoading.asStateFlow()
+
+ /**
+ * Load all launchable apps from the device
+ */
+ suspend fun loadApps() = withContext(Dispatchers.IO) {
+ _isLoading.value = true
+
+ try {
+ val mainIntent = Intent(Intent.ACTION_MAIN, null).apply {
+ addCategory(Intent.CATEGORY_LAUNCHER)
+ }
+
+ val resolveInfoList: List = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ packageManager.queryIntentActivities(
+ mainIntent,
+ PackageManager.ResolveInfoFlags.of(0)
+ )
+ } else {
+ @Suppress("DEPRECATION")
+ packageManager.queryIntentActivities(mainIntent, 0)
+ }
+
+ val appList = resolveInfoList.mapNotNull { resolveInfo ->
+ try {
+ val packageName = resolveInfo.activityInfo.packageName
+
+ // Skip our own launcher
+ if (packageName == context.packageName) return@mapNotNull null
+
+ val label = resolveInfo.loadLabel(packageManager).toString()
+ val icon = resolveInfo.loadIcon(packageManager)
+ val isSystemApp = (resolveInfo.activityInfo.applicationInfo.flags and
+ android.content.pm.ApplicationInfo.FLAG_SYSTEM) != 0
+
+ AppInfo(
+ packageName = packageName,
+ label = label,
+ icon = icon,
+ category = AppInfo.categorize(packageName),
+ isSystemApp = isSystemApp
+ )
+ } catch (e: Exception) {
+ null
+ }
+ }.sortedBy { it.label.lowercase() }
+
+ _apps.value = appList
+ } finally {
+ _isLoading.value = false
+ }
+ }
+
+ /**
+ * Get apps filtered by category
+ */
+ fun getAppsByCategory(category: AppCategory): List {
+ return if (category == AppCategory.ALL) {
+ _apps.value
+ } else {
+ _apps.value.filter { it.category == category }
+ }
+ }
+
+ /**
+ * Search apps by name
+ */
+ fun searchApps(query: String): List {
+ if (query.isBlank()) return _apps.value
+
+ val lowerQuery = query.lowercase()
+ return _apps.value.filter {
+ it.label.lowercase().contains(lowerQuery) ||
+ it.packageName.lowercase().contains(lowerQuery)
+ }
+ }
+
+ /**
+ * Launch an app
+ */
+ fun launchApp(packageName: String) {
+ val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
+ launchIntent?.let {
+ it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ context.startActivity(it)
+ }
+ }
+
+ /**
+ * Refresh the app list (call when packages change)
+ */
+ suspend fun refresh() {
+ loadApps()
+ }
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/domain/SearchEngine.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/domain/SearchEngine.kt
new file mode 100644
index 00000000..b8a3dbe2
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/domain/SearchEngine.kt
@@ -0,0 +1,307 @@
+package com.blackglass.launcher.domain
+
+import android.content.Context
+import android.database.Cursor
+import android.net.Uri
+import android.provider.ContactsContract
+import com.blackglass.launcher.data.AppInfo
+import com.blackglass.launcher.data.AppRepository
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+/**
+ * BlackGlass UI OS - Search Engine
+ * Unified search across apps, contacts, and web
+ */
+class SearchEngine(
+ private val context: Context,
+ private val appRepository: AppRepository
+) {
+
+ private val smartSorter = SmartSorter()
+
+ /**
+ * Search result types
+ */
+ sealed class SearchResult {
+ data class AppResult(
+ val app: AppInfo,
+ val relevanceScore: Float
+ ) : SearchResult()
+
+ data class ContactResult(
+ val id: String,
+ val name: String,
+ val phoneNumber: String?,
+ val photoUri: String?
+ ) : SearchResult()
+
+ data class WebSuggestion(
+ val query: String,
+ val suggestion: String
+ ) : SearchResult()
+
+ data class ActionResult(
+ val action: QuickAction,
+ val displayText: String
+ ) : SearchResult()
+ }
+
+ /**
+ * Quick actions that can be triggered from search
+ */
+ enum class QuickAction {
+ CALL,
+ MESSAGE,
+ EMAIL,
+ MAP,
+ CALCULATE,
+ WEB_SEARCH,
+ SETTINGS
+ }
+
+ /**
+ * Perform unified search
+ */
+ suspend fun search(query: String): List = withContext(Dispatchers.IO) {
+ if (query.isBlank()) return@withContext emptyList()
+
+ val results = mutableListOf()
+
+ // Search apps
+ val appResults = searchApps(query)
+ results.addAll(appResults)
+
+ // Search contacts (if permission granted)
+ try {
+ val contactResults = searchContacts(query)
+ results.addAll(contactResults)
+ } catch (e: SecurityException) {
+ // Contact permission not granted
+ }
+
+ // Parse quick actions
+ val actionResult = parseQuickAction(query)
+ actionResult?.let { results.add(0, it) }
+
+ // Add web search suggestion
+ if (results.size < 5) {
+ results.add(SearchResult.WebSuggestion(query, "Search web for \"$query\""))
+ }
+
+ results
+ }
+
+ /**
+ * Search installed apps
+ */
+ private fun searchApps(query: String): List {
+ val apps = appRepository.searchApps(query)
+
+ return smartSorter.smartSearch(apps, query)
+ .map { app ->
+ SearchResult.AppResult(
+ app = app,
+ relevanceScore = smartSorter.calculateRelevanceScore(app, query)
+ )
+ }
+ .sortedByDescending { it.relevanceScore }
+ .take(10)
+ }
+
+ /**
+ * Search contacts
+ */
+ private fun searchContacts(query: String): List {
+ val contacts = mutableListOf()
+
+ val projection = arrayOf(
+ ContactsContract.Contacts._ID,
+ ContactsContract.Contacts.DISPLAY_NAME_PRIMARY,
+ ContactsContract.Contacts.PHOTO_THUMBNAIL_URI,
+ ContactsContract.Contacts.HAS_PHONE_NUMBER
+ )
+
+ val selection = "${ContactsContract.Contacts.DISPLAY_NAME_PRIMARY} LIKE ?"
+ val selectionArgs = arrayOf("%$query%")
+
+ val cursor: Cursor? = context.contentResolver.query(
+ ContactsContract.Contacts.CONTENT_URI,
+ projection,
+ selection,
+ selectionArgs,
+ "${ContactsContract.Contacts.DISPLAY_NAME_PRIMARY} ASC"
+ )
+
+ cursor?.use {
+ val idIndex = it.getColumnIndex(ContactsContract.Contacts._ID)
+ val nameIndex = it.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY)
+ val photoIndex = it.getColumnIndex(ContactsContract.Contacts.PHOTO_THUMBNAIL_URI)
+ val hasPhoneIndex = it.getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER)
+
+ while (it.moveToNext() && contacts.size < 5) {
+ val id = it.getString(idIndex)
+ val name = it.getString(nameIndex) ?: continue
+ val photoUri = it.getString(photoIndex)
+ val hasPhone = it.getInt(hasPhoneIndex) > 0
+
+ var phoneNumber: String? = null
+ if (hasPhone) {
+ phoneNumber = getPhoneNumber(id)
+ }
+
+ contacts.add(
+ SearchResult.ContactResult(
+ id = id,
+ name = name,
+ phoneNumber = phoneNumber,
+ photoUri = photoUri
+ )
+ )
+ }
+ }
+
+ return contacts
+ }
+
+ /**
+ * Get phone number for a contact
+ */
+ private fun getPhoneNumber(contactId: String): String? {
+ val cursor = context.contentResolver.query(
+ ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
+ arrayOf(ContactsContract.CommonDataKinds.Phone.NUMBER),
+ "${ContactsContract.CommonDataKinds.Phone.CONTACT_ID} = ?",
+ arrayOf(contactId),
+ null
+ )
+
+ return cursor?.use {
+ if (it.moveToFirst()) {
+ it.getString(0)
+ } else null
+ }
+ }
+
+ /**
+ * Parse query for quick actions
+ */
+ private fun parseQuickAction(query: String): SearchResult.ActionResult? {
+ val lowerQuery = query.lowercase().trim()
+
+ return when {
+ // Call action
+ lowerQuery.startsWith("call ") -> {
+ val target = query.substring(5).trim()
+ SearchResult.ActionResult(
+ action = QuickAction.CALL,
+ displayText = "Call $target"
+ )
+ }
+
+ // Message action
+ lowerQuery.startsWith("text ") || lowerQuery.startsWith("message ") -> {
+ val target = query.substringAfter(" ").trim()
+ SearchResult.ActionResult(
+ action = QuickAction.MESSAGE,
+ displayText = "Message $target"
+ )
+ }
+
+ // Email action
+ lowerQuery.startsWith("email ") -> {
+ val target = query.substring(6).trim()
+ SearchResult.ActionResult(
+ action = QuickAction.EMAIL,
+ displayText = "Email $target"
+ )
+ }
+
+ // Calculator
+ lowerQuery.matches(Regex("[0-9+\\-*/().\\s]+")) -> {
+ try {
+ val result = evaluateExpression(lowerQuery)
+ SearchResult.ActionResult(
+ action = QuickAction.CALCULATE,
+ displayText = "$query = $result"
+ )
+ } catch (e: Exception) {
+ null
+ }
+ }
+
+ // Maps/Navigation
+ lowerQuery.startsWith("navigate to ") || lowerQuery.startsWith("directions to ") -> {
+ val destination = query.substringAfter(" to ").trim()
+ SearchResult.ActionResult(
+ action = QuickAction.MAP,
+ displayText = "Navigate to $destination"
+ )
+ }
+
+ // Settings
+ lowerQuery.contains("settings") || lowerQuery.contains("wifi") ||
+ lowerQuery.contains("bluetooth") -> {
+ SearchResult.ActionResult(
+ action = QuickAction.SETTINGS,
+ displayText = "Open Settings"
+ )
+ }
+
+ else -> null
+ }
+ }
+
+ /**
+ * Simple expression evaluator for calculator
+ */
+ private fun evaluateExpression(expression: String): Double {
+ // Basic implementation - in production use a proper expression parser
+ val cleanExpr = expression.replace(" ", "")
+ return try {
+ // Simple evaluation for basic operations
+ when {
+ cleanExpr.contains("+") -> {
+ val parts = cleanExpr.split("+")
+ parts.sumOf { it.toDouble() }
+ }
+ cleanExpr.contains("-") && !cleanExpr.startsWith("-") -> {
+ val parts = cleanExpr.split("-")
+ parts.first().toDouble() - parts.drop(1).sumOf { it.toDouble() }
+ }
+ cleanExpr.contains("*") -> {
+ val parts = cleanExpr.split("*")
+ parts.fold(1.0) { acc, s -> acc * s.toDouble() }
+ }
+ cleanExpr.contains("/") -> {
+ val parts = cleanExpr.split("/")
+ parts.first().toDouble() / parts[1].toDouble()
+ }
+ else -> cleanExpr.toDouble()
+ }
+ } catch (e: Exception) {
+ throw IllegalArgumentException("Invalid expression")
+ }
+ }
+
+ /**
+ * Get search suggestions based on history
+ */
+ fun getSearchSuggestions(partialQuery: String): List {
+ // In a full implementation, this would query search history
+ return emptyList()
+ }
+
+ /**
+ * Build web search URL
+ */
+ fun buildWebSearchUrl(query: String, searchEngine: String = "google"): String {
+ val encodedQuery = Uri.encode(query)
+ return when (searchEngine.lowercase()) {
+ "google" -> "https://www.google.com/search?q=$encodedQuery"
+ "duckduckgo" -> "https://duckduckgo.com/?q=$encodedQuery"
+ "bing" -> "https://www.bing.com/search?q=$encodedQuery"
+ else -> "https://www.google.com/search?q=$encodedQuery"
+ }
+ }
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/domain/SmartSorter.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/domain/SmartSorter.kt
new file mode 100644
index 00000000..7c29c064
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/domain/SmartSorter.kt
@@ -0,0 +1,204 @@
+package com.blackglass.launcher.domain
+
+import com.blackglass.launcher.data.AppCategory
+import com.blackglass.launcher.data.AppInfo
+
+/**
+ * BlackGlass UI OS - Smart Sorter
+ * Logic for intelligently categorizing and sorting apps
+ */
+class SmartSorter {
+
+ /**
+ * Sort mode options
+ */
+ enum class SortMode {
+ ALPHABETICAL,
+ MOST_USED,
+ RECENTLY_USED,
+ CATEGORY,
+ CUSTOM
+ }
+
+ /**
+ * Sort apps by the given mode
+ */
+ fun sortApps(apps: List, mode: SortMode): List {
+ return when (mode) {
+ SortMode.ALPHABETICAL -> apps.sortedBy { it.label.lowercase() }
+ SortMode.MOST_USED -> apps.sortedByDescending { it.usageCount }
+ SortMode.RECENTLY_USED -> apps.sortedByDescending { it.lastUsed }
+ SortMode.CATEGORY -> apps.sortedWith(
+ compareBy { it.category.ordinal }
+ .thenBy { it.label.lowercase() }
+ )
+ SortMode.CUSTOM -> apps // Preserve existing order
+ }
+ }
+
+ /**
+ * Group apps by category
+ */
+ fun groupByCategory(apps: List): Map> {
+ return apps.groupBy { it.category }
+ .toSortedMap(compareBy { it.ordinal })
+ }
+
+ /**
+ * Get suggested apps based on usage patterns
+ */
+ fun getSuggestedApps(
+ apps: List,
+ currentHour: Int,
+ limit: Int = 8
+ ): List {
+ // Time-based suggestions
+ val timeBasedApps = when {
+ currentHour in 6..9 -> {
+ // Morning: Productivity, News, Social
+ apps.filter {
+ it.category in listOf(
+ AppCategory.PRODUCTIVITY,
+ AppCategory.SOCIAL
+ )
+ }
+ }
+ currentHour in 12..14 -> {
+ // Lunch: Social, Media
+ apps.filter {
+ it.category in listOf(
+ AppCategory.SOCIAL,
+ AppCategory.MEDIA
+ )
+ }
+ }
+ currentHour in 18..22 -> {
+ // Evening: Games, Media
+ apps.filter {
+ it.category in listOf(
+ AppCategory.GAMES,
+ AppCategory.MEDIA
+ )
+ }
+ }
+ else -> apps
+ }
+
+ // Combine with most used apps
+ val mostUsed = apps.sortedByDescending { it.usageCount }.take(limit)
+
+ return (timeBasedApps + mostUsed)
+ .distinctBy { it.packageName }
+ .take(limit)
+ }
+
+ /**
+ * Get frequently used apps
+ */
+ fun getFrequentApps(apps: List, limit: Int = 8): List {
+ return apps
+ .filter { it.usageCount > 0 }
+ .sortedByDescending { it.usageCount }
+ .take(limit)
+ }
+
+ /**
+ * Get recently used apps
+ */
+ fun getRecentApps(apps: List, limit: Int = 8): List {
+ return apps
+ .filter { it.lastUsed > 0 }
+ .sortedByDescending { it.lastUsed }
+ .take(limit)
+ }
+
+ /**
+ * Smart search with fuzzy matching
+ */
+ fun smartSearch(apps: List, query: String): List {
+ if (query.isBlank()) return emptyList()
+
+ val lowerQuery = query.lowercase().trim()
+
+ // Exact match first
+ val exactMatches = apps.filter {
+ it.label.lowercase() == lowerQuery
+ }
+
+ // Starts with query
+ val startsWithMatches = apps.filter {
+ it.label.lowercase().startsWith(lowerQuery) &&
+ it !in exactMatches
+ }
+
+ // Contains query
+ val containsMatches = apps.filter {
+ it.label.lowercase().contains(lowerQuery) &&
+ it !in exactMatches &&
+ it !in startsWithMatches
+ }
+
+ // Fuzzy match (initials)
+ val initialMatches = apps.filter { app ->
+ val initials = app.label
+ .split(" ")
+ .mapNotNull { it.firstOrNull()?.lowercase() }
+ .joinToString("")
+
+ initials.contains(lowerQuery) &&
+ app !in exactMatches &&
+ app !in startsWithMatches &&
+ app !in containsMatches
+ }
+
+ return exactMatches + startsWithMatches + containsMatches + initialMatches
+ }
+
+ /**
+ * Calculate app relevance score for search ranking
+ */
+ fun calculateRelevanceScore(app: AppInfo, query: String): Float {
+ val lowerQuery = query.lowercase()
+ val lowerLabel = app.label.lowercase()
+
+ var score = 0f
+
+ // Exact match
+ if (lowerLabel == lowerQuery) score += 100f
+
+ // Starts with
+ if (lowerLabel.startsWith(lowerQuery)) score += 50f
+
+ // Contains
+ if (lowerLabel.contains(lowerQuery)) score += 25f
+
+ // Usage boost
+ score += (app.usageCount * 0.1f).coerceAtMost(20f)
+
+ // Recency boost
+ val daysSinceUse = (System.currentTimeMillis() - app.lastUsed) / (1000 * 60 * 60 * 24)
+ if (daysSinceUse < 7) score += (7 - daysSinceUse) * 2
+
+ return score
+ }
+
+ /**
+ * Get apps that match a category keyword
+ */
+ fun getAppsByCategoryKeyword(
+ apps: List,
+ keyword: String
+ ): List {
+ val category = when (keyword.lowercase()) {
+ "social", "chat", "message" -> AppCategory.SOCIAL
+ "media", "music", "video", "photo" -> AppCategory.MEDIA
+ "game", "games", "play" -> AppCategory.GAMES
+ "work", "office", "productivity" -> AppCategory.PRODUCTIVITY
+ else -> null
+ }
+
+ return category?.let { cat ->
+ apps.filter { it.category == cat }
+ } ?: emptyList()
+ }
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/receiver/BootReceiver.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/receiver/BootReceiver.kt
new file mode 100644
index 00000000..dda1ccb1
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/receiver/BootReceiver.kt
@@ -0,0 +1,21 @@
+package com.blackglass.launcher.receiver
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+
+/**
+ * Boot Receiver to ensure launcher is ready after device restart
+ */
+class BootReceiver : BroadcastReceiver() {
+
+ override fun onReceive(context: Context, intent: Intent) {
+ when (intent.action) {
+ Intent.ACTION_BOOT_COMPLETED,
+ "android.intent.action.QUICKBOOT_POWERON" -> {
+ // Launcher will be started by the system when user goes to home
+ // This receiver ensures our app is aware of boot completion
+ }
+ }
+ }
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/receiver/PackageChangeReceiver.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/receiver/PackageChangeReceiver.kt
new file mode 100644
index 00000000..8bd4c7fb
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/receiver/PackageChangeReceiver.kt
@@ -0,0 +1,39 @@
+package com.blackglass.launcher.receiver
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+
+/**
+ * Package Change Receiver to detect app installations/uninstallations
+ * Allows the launcher to refresh its app list when changes occur
+ */
+class PackageChangeReceiver : BroadcastReceiver() {
+
+ companion object {
+ private var onPackageChanged: (() -> Unit)? = null
+
+ fun setOnPackageChangedListener(listener: () -> Unit) {
+ onPackageChanged = listener
+ }
+
+ fun removeOnPackageChangedListener() {
+ onPackageChanged = null
+ }
+ }
+
+ override fun onReceive(context: Context, intent: Intent) {
+ when (intent.action) {
+ Intent.ACTION_PACKAGE_ADDED,
+ Intent.ACTION_PACKAGE_REMOVED,
+ Intent.ACTION_PACKAGE_CHANGED -> {
+ val packageName = intent.data?.schemeSpecificPart
+
+ // Ignore our own package changes
+ if (packageName != context.packageName) {
+ onPackageChanged?.invoke()
+ }
+ }
+ }
+ }
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/AppDrawer.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/AppDrawer.kt
new file mode 100644
index 00000000..0fce6e1d
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/AppDrawer.kt
@@ -0,0 +1,224 @@
+package com.blackglass.launcher.ui.components
+
+import androidx.compose.animation.*
+import androidx.compose.animation.core.*
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.detectVerticalDragGestures
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.dp
+import com.blackglass.launcher.data.model.AppCategory
+import com.blackglass.launcher.data.model.AppInfo
+import com.blackglass.launcher.ui.theme.BlackGlassColors
+import com.blackglass.launcher.ui.theme.BlackGlassTypography
+import dev.chrisbanes.haze.HazeState
+import dev.chrisbanes.haze.haze
+import dev.chrisbanes.haze.hazeChild
+import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
+import dev.chrisbanes.haze.materials.HazeMaterials
+
+/**
+ * BlackGlass UI OS - App Drawer Component
+ * Full-screen app drawer with smart sorting categories
+ */
+
+@OptIn(ExperimentalHazeMaterialsApi::class)
+@Composable
+fun AppDrawer(
+ apps: List,
+ selectedCategory: AppCategory,
+ onCategorySelected: (AppCategory) -> Unit,
+ onAppClick: (AppInfo) -> Unit,
+ onAppLongClick: (AppInfo) -> Unit,
+ onDismiss: () -> Unit,
+ isVisible: Boolean,
+ hazeState: HazeState,
+ modifier: Modifier = Modifier
+) {
+ var dragOffset by remember { mutableFloatStateOf(0f) }
+ val density = LocalDensity.current
+
+ AnimatedVisibility(
+ visible = isVisible,
+ enter = slideInVertically(
+ initialOffsetY = { it },
+ animationSpec = spring(
+ dampingRatio = Spring.DampingRatioLowBouncy,
+ stiffness = Spring.StiffnessLow
+ )
+ ) + fadeIn(),
+ exit = slideOutVertically(
+ targetOffsetY = { it },
+ animationSpec = tween(300)
+ ) + fadeOut()
+ ) {
+ Box(
+ modifier = modifier
+ .fillMaxSize()
+ .graphicsLayer {
+ translationY = dragOffset.coerceAtLeast(0f)
+ }
+ .pointerInput(Unit) {
+ detectVerticalDragGestures(
+ onDragEnd = {
+ if (dragOffset > 200) {
+ onDismiss()
+ }
+ dragOffset = 0f
+ },
+ onDragCancel = {
+ dragOffset = 0f
+ },
+ onVerticalDrag = { _, dragAmount ->
+ dragOffset += dragAmount
+ }
+ )
+ }
+ .hazeChild(
+ state = hazeState,
+ style = HazeMaterials.ultraThin()
+ )
+ .background(
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ BlackGlassColors.BackgroundDark.copy(alpha = 0.85f),
+ BlackGlassColors.BackgroundMedium.copy(alpha = 0.9f),
+ BlackGlassColors.BackgroundDark.copy(alpha = 0.95f)
+ )
+ )
+ )
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .statusBarsPadding()
+ ) {
+ // Drag handle
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 12.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Box(
+ modifier = Modifier
+ .width(40.dp)
+ .height(4.dp)
+ .clip(RoundedCornerShape(2.dp))
+ .background(BlackGlassColors.TextTertiary)
+ )
+ }
+
+ // Category tabs
+ CategoryTabs(
+ selectedCategory = selectedCategory,
+ onCategorySelected = onCategorySelected,
+ modifier = Modifier.padding(vertical = 8.dp)
+ )
+
+ // App grid
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(4),
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 16.dp),
+ contentPadding = PaddingValues(vertical = 16.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ items(
+ items = apps,
+ key = { it.packageName }
+ ) { app ->
+ AppIcon(
+ app = app,
+ onClick = { onAppClick(app) },
+ onLongClick = { onAppLongClick(app) },
+ modifier = Modifier.animateItem()
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun CategoryTabs(
+ selectedCategory: AppCategory,
+ onCategorySelected: (AppCategory) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val categories = AppCategory.entries
+
+ val categoryColors = mapOf(
+ AppCategory.ALL to BlackGlassColors.TextPrimary,
+ AppCategory.SOCIAL to BlackGlassColors.CategorySocial,
+ AppCategory.MEDIA to BlackGlassColors.CategoryMedia,
+ AppCategory.GAMES to BlackGlassColors.CategoryGames,
+ AppCategory.PRODUCTIVITY to BlackGlassColors.CategoryProductivity,
+ AppCategory.UTILITIES to BlackGlassColors.CategoryUtilities
+ )
+
+ LazyRow(
+ modifier = modifier.fillMaxWidth(),
+ contentPadding = PaddingValues(horizontal = 16.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ items(categories) { category ->
+ CategoryChip(
+ label = category.displayName,
+ isSelected = category == selectedCategory,
+ onClick = { onCategorySelected(category) },
+ categoryColor = categoryColors[category] ?: BlackGlassColors.TextPrimary
+ )
+ }
+ }
+}
+
+/**
+ * Empty state for app drawer
+ */
+@Composable
+fun EmptyAppState(
+ message: String,
+ modifier: Modifier = Modifier
+) {
+ Box(
+ modifier = modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = "๐ฑ",
+ style = BlackGlassTypography.DisplayLarge
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Text(
+ text = message,
+ style = BlackGlassTypography.BodyLarge,
+ color = BlackGlassColors.TextSecondary
+ )
+ }
+ }
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/AppIcon.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/AppIcon.kt
new file mode 100644
index 00000000..331a1800
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/AppIcon.kt
@@ -0,0 +1,263 @@
+package com.blackglass.launcher.ui.components
+
+import androidx.compose.animation.*
+import androidx.compose.animation.core.*
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.gestures.detectDragGestures
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsPressedAsState
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalHapticFeedback
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import com.blackglass.launcher.data.model.AppInfo
+import com.blackglass.launcher.ui.theme.BlackGlassColors
+import com.blackglass.launcher.ui.theme.BlackGlassTypography
+import com.google.accompanist.drawablepainter.rememberDrawablePainter
+import kotlin.math.roundToInt
+
+/**
+ * BlackGlass UI OS - App Icon Component
+ * Individual app icon with glass styling and animations
+ */
+
+@Composable
+fun AppIcon(
+ app: AppInfo,
+ onClick: () -> Unit,
+ onLongClick: () -> Unit = {},
+ modifier: Modifier = Modifier,
+ showLabel: Boolean = true,
+ iconSize: Int = 56,
+ isEditing: Boolean = false
+) {
+ val haptic = LocalHapticFeedback.current
+ val interactionSource = remember { MutableInteractionSource() }
+ val isPressed by interactionSource.collectIsPressedAsState()
+
+ // Animation states
+ val scale by animateFloatAsState(
+ targetValue = if (isPressed) 0.9f else 1f,
+ animationSpec = spring(
+ dampingRatio = Spring.DampingRatioMediumBouncy,
+ stiffness = Spring.StiffnessMedium
+ ),
+ label = "scale"
+ )
+
+ // Wiggle animation for edit mode
+ val infiniteTransition = rememberInfiniteTransition(label = "wiggle")
+ val wiggleRotation by infiniteTransition.animateFloat(
+ initialValue = -2f,
+ targetValue = 2f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(150, easing = LinearEasing),
+ repeatMode = RepeatMode.Reverse
+ ),
+ label = "rotation"
+ )
+
+ Column(
+ modifier = modifier
+ .width(80.dp)
+ .graphicsLayer {
+ scaleX = scale
+ scaleY = scale
+ rotationZ = if (isEditing) wiggleRotation else 0f
+ }
+ .pointerInput(Unit) {
+ detectTapGestures(
+ onTap = {
+ haptic.performHapticFeedback(HapticFeedbackType.LongPress)
+ onClick()
+ },
+ onLongPress = {
+ haptic.performHapticFeedback(HapticFeedbackType.LongPress)
+ onLongClick()
+ }
+ )
+ },
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ // Icon container with glass effect
+ Box(
+ modifier = Modifier
+ .size(iconSize.dp)
+ .clip(RoundedCornerShape(14.dp))
+ .background(
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ Color.White.copy(alpha = 0.12f),
+ Color.White.copy(alpha = 0.05f)
+ )
+ )
+ )
+ .border(
+ width = 0.5.dp,
+ color = Color.White.copy(alpha = 0.15f),
+ shape = RoundedCornerShape(14.dp)
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ // App icon
+ Image(
+ painter = rememberDrawablePainter(drawable = app.icon),
+ contentDescription = app.label,
+ modifier = Modifier
+ .size((iconSize - 8).dp)
+ .clip(RoundedCornerShape(12.dp))
+ )
+ }
+
+ // App label
+ if (showLabel) {
+ Spacer(modifier = Modifier.height(6.dp))
+ Text(
+ text = app.label,
+ style = BlackGlassTypography.AppIconLabel,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
+}
+
+/**
+ * Draggable app icon for elastic grid
+ */
+@Composable
+fun DraggableAppIcon(
+ app: AppInfo,
+ onClick: () -> Unit,
+ onLongClick: () -> Unit = {},
+ onDragEnd: (Offset) -> Unit = {},
+ modifier: Modifier = Modifier,
+ iconSize: Int = 56
+) {
+ var offset by remember { mutableStateOf(Offset.Zero) }
+ var isDragging by remember { mutableStateOf(false) }
+
+ val haptic = LocalHapticFeedback.current
+
+ Box(
+ modifier = modifier
+ .offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) }
+ .graphicsLayer {
+ scaleX = if (isDragging) 1.1f else 1f
+ scaleY = if (isDragging) 1.1f else 1f
+ alpha = if (isDragging) 0.8f else 1f
+ }
+ .pointerInput(Unit) {
+ detectDragGestures(
+ onDragStart = {
+ isDragging = true
+ haptic.performHapticFeedback(HapticFeedbackType.LongPress)
+ },
+ onDragEnd = {
+ isDragging = false
+ onDragEnd(offset)
+ offset = Offset.Zero
+ },
+ onDragCancel = {
+ isDragging = false
+ offset = Offset.Zero
+ },
+ onDrag = { change, dragAmount ->
+ change.consume()
+ offset += dragAmount
+ }
+ )
+ }
+ ) {
+ AppIcon(
+ app = app,
+ onClick = onClick,
+ onLongClick = onLongClick,
+ iconSize = iconSize
+ )
+ }
+}
+
+/**
+ * Category chip for smart sorting tabs
+ */
+@Composable
+fun CategoryChip(
+ label: String,
+ isSelected: Boolean,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ categoryColor: Color = BlackGlassColors.NeonWineGlow
+) {
+ val backgroundColor by animateColorAsState(
+ targetValue = if (isSelected) {
+ categoryColor.copy(alpha = 0.25f)
+ } else {
+ Color.White.copy(alpha = 0.08f)
+ },
+ animationSpec = tween(200),
+ label = "bg"
+ )
+
+ val borderColor by animateColorAsState(
+ targetValue = if (isSelected) {
+ categoryColor.copy(alpha = 0.6f)
+ } else {
+ Color.White.copy(alpha = 0.15f)
+ },
+ animationSpec = tween(200),
+ label = "border"
+ )
+
+ val textColor by animateColorAsState(
+ targetValue = if (isSelected) {
+ categoryColor
+ } else {
+ BlackGlassColors.TextSecondary
+ },
+ animationSpec = tween(200),
+ label = "text"
+ )
+
+ Box(
+ modifier = modifier
+ .clip(RoundedCornerShape(20.dp))
+ .background(backgroundColor)
+ .border(
+ width = 1.dp,
+ color = borderColor,
+ shape = RoundedCornerShape(20.dp)
+ )
+ .clickable { onClick() }
+ .padding(horizontal = 16.dp, vertical = 8.dp)
+ ) {
+ Text(
+ text = label,
+ style = BlackGlassTypography.CategoryTab,
+ color = textColor
+ )
+ }
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/BlurDock.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/BlurDock.kt
new file mode 100644
index 00000000..7b0909d2
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/BlurDock.kt
@@ -0,0 +1,338 @@
+package com.blackglass.launcher.ui.components
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.*
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Apps
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.blackglass.launcher.data.AppInfo
+import com.blackglass.launcher.ui.theme.GlassColors
+import com.blackglass.launcher.ui.theme.GlassConfig
+import dev.chrisbanes.haze.HazeState
+import dev.chrisbanes.haze.hazeChild
+import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
+import dev.chrisbanes.haze.materials.HazeMaterials
+
+/**
+ * BlackGlass UI OS - Blur Dock Component
+ * Bottom navigation dock with blur effect and neon glow
+ */
+
+/**
+ * Main Blur Dock composable
+ */
+@OptIn(ExperimentalHazeMaterialsApi::class)
+@Composable
+fun BlurDock(
+ dockItems: List,
+ onItemClick: (AppInfo) -> Unit,
+ onItemLongClick: (Int) -> Unit,
+ onEmptySlotClick: (Int) -> Unit,
+ onDrawerClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ hazeState: HazeState? = null,
+ maxItems: Int = 5
+) {
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 8.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ // Dock container with blur
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(80.dp)
+ .clip(RoundedCornerShape(32.dp))
+ .then(
+ hazeState?.let {
+ Modifier.hazeChild(it, style = HazeMaterials.ultraThin())
+ } ?: Modifier
+ )
+ .background(
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ Color.Black.copy(alpha = 0.3f),
+ Color.Black.copy(alpha = 0.2f)
+ )
+ )
+ )
+ .border(
+ width = 1.dp,
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ Color.White.copy(alpha = 0.2f),
+ Color.White.copy(alpha = 0.08f)
+ )
+ ),
+ shape = RoundedCornerShape(32.dp)
+ )
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 12.dp),
+ horizontalArrangement = Arrangement.SpaceEvenly,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // App drawer button
+ DockDrawerButton(
+ onClick = onDrawerClick
+ )
+
+ // Divider
+ DockDivider()
+
+ // Dock items
+ dockItems.take(maxItems).forEachIndexed { index, app ->
+ DockSlot(
+ app = app,
+ position = index,
+ onAppClick = { app?.let { onItemClick(it) } },
+ onLongClick = { onItemLongClick(index) },
+ onEmptyClick = { onEmptySlotClick(index) }
+ )
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Individual dock slot
+ */
+@Composable
+private fun DockSlot(
+ app: AppInfo?,
+ position: Int,
+ onAppClick: () -> Unit,
+ onLongClick: () -> Unit,
+ onEmptyClick: () -> Unit
+) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ if (app != null) {
+ CircularNeonIcon(
+ drawable = app.icon,
+ contentDescription = app.label,
+ size = 52.dp,
+ glowColor = GlassColors.NeonWineGlow,
+ onClick = onAppClick,
+ onLongClick = onLongClick
+ )
+
+ // Active indicator
+ Spacer(modifier = Modifier.height(4.dp))
+ NeonActiveIndicator(
+ isActive = false, // TODO: Check if app is running
+ color = GlassColors.NeonWineGlow
+ )
+ } else {
+ // Empty slot
+ EmptyDockSlot(onClick = onEmptyClick)
+ }
+ }
+}
+
+/**
+ * Empty dock slot with add button
+ */
+@Composable
+private fun EmptyDockSlot(
+ onClick: () -> Unit
+) {
+ CircularNeonIcon(
+ drawable = null,
+ contentDescription = "Add app",
+ size = 52.dp,
+ glowColor = GlassColors.TextTertiary,
+ onClick = onClick
+ )
+
+ // Overlay add icon on empty slot
+ Box(
+ modifier = Modifier.size(52.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.Default.Add,
+ contentDescription = "Add",
+ tint = GlassColors.TextTertiary,
+ modifier = Modifier.size(24.dp)
+ )
+ }
+}
+
+/**
+ * App drawer button in dock
+ */
+@Composable
+private fun DockDrawerButton(
+ onClick: () -> Unit
+) {
+ CircularNeonIcon(
+ drawable = null,
+ contentDescription = "App Drawer",
+ size = 48.dp,
+ glowColor = GlassColors.NeonVioletGlow,
+ onClick = onClick
+ )
+
+ // Overlay grid icon
+ Box(
+ modifier = Modifier.size(48.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.Default.Apps,
+ contentDescription = "Apps",
+ tint = GlassColors.TextSecondary,
+ modifier = Modifier.size(22.dp)
+ )
+ }
+}
+
+/**
+ * Vertical divider in dock
+ */
+@Composable
+private fun DockDivider() {
+ Box(
+ modifier = Modifier
+ .width(1.dp)
+ .height(40.dp)
+ .background(
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ Color.Transparent,
+ GlassColors.GlassBorder,
+ GlassColors.GlassBorder,
+ Color.Transparent
+ )
+ )
+ )
+ )
+}
+
+/**
+ * Compact Blur Dock variant (fewer items, smaller)
+ */
+@OptIn(ExperimentalHazeMaterialsApi::class)
+@Composable
+fun CompactBlurDock(
+ dockItems: List,
+ onItemClick: (AppInfo) -> Unit,
+ modifier: Modifier = Modifier,
+ hazeState: HazeState? = null,
+ itemSize: Dp = 44.dp
+) {
+ Box(
+ modifier = modifier
+ .clip(RoundedCornerShape(28.dp))
+ .then(
+ hazeState?.let {
+ Modifier.hazeChild(it, style = HazeMaterials.thin())
+ } ?: Modifier
+ )
+ .background(Color.Black.copy(alpha = 0.25f))
+ .border(
+ width = 0.5.dp,
+ color = Color.White.copy(alpha = 0.15f),
+ shape = RoundedCornerShape(28.dp)
+ )
+ .padding(horizontal = 16.dp, vertical = 8.dp)
+ ) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ dockItems.filterNotNull().take(4).forEach { app ->
+ CircularNeonIcon(
+ drawable = app.icon,
+ contentDescription = app.label,
+ size = itemSize,
+ onClick = { onItemClick(app) }
+ )
+ }
+ }
+ }
+}
+
+/**
+ * Floating Dock variant (centered, minimal)
+ */
+@OptIn(ExperimentalHazeMaterialsApi::class)
+@Composable
+fun FloatingDock(
+ dockItems: List,
+ onItemClick: (AppInfo) -> Unit,
+ onDrawerClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ hazeState: HazeState? = null
+) {
+ Row(
+ modifier = modifier
+ .clip(RoundedCornerShape(36.dp))
+ .then(
+ hazeState?.let {
+ Modifier.hazeChild(it, style = HazeMaterials.ultraThin())
+ } ?: Modifier
+ )
+ .background(
+ brush = Brush.horizontalGradient(
+ colors = listOf(
+ Color.Black.copy(alpha = 0.35f),
+ Color.Black.copy(alpha = 0.25f),
+ Color.Black.copy(alpha = 0.35f)
+ )
+ )
+ )
+ .border(
+ width = 1.dp,
+ color = Color.White.copy(alpha = 0.15f),
+ shape = RoundedCornerShape(36.dp)
+ )
+ .padding(horizontal = 20.dp, vertical = 12.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ dockItems.filterNotNull().take(5).forEach { app ->
+ CircularNeonIcon(
+ drawable = app.icon,
+ contentDescription = app.label,
+ size = 48.dp,
+ glowColor = GlassColors.NeonWineGlow,
+ onClick = { onItemClick(app) }
+ )
+ }
+
+ // Drawer button
+ DockDivider()
+
+ CircularNeonIcon(
+ drawable = null,
+ contentDescription = "App Drawer",
+ size = 48.dp,
+ glowColor = GlassColors.NeonVioletGlow,
+ onClick = onDrawerClick
+ )
+ }
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/ClockWidget.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/ClockWidget.kt
new file mode 100644
index 00000000..5e3b5e8c
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/ClockWidget.kt
@@ -0,0 +1,198 @@
+package com.blackglass.launcher.ui.components
+
+import androidx.compose.animation.core.*
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import com.blackglass.launcher.ui.theme.BlackGlassColors
+import com.blackglass.launcher.ui.theme.BlackGlassTypography
+import kotlinx.coroutines.delay
+import java.text.SimpleDateFormat
+import java.util.*
+
+/**
+ * BlackGlass UI OS - Clock Widget Component
+ * Large glass-styled clock display for home screen
+ */
+
+@Composable
+fun ClockWidget(
+ modifier: Modifier = Modifier
+) {
+ var currentTime by remember { mutableStateOf(Calendar.getInstance()) }
+
+ // Update time every second
+ LaunchedEffect(Unit) {
+ while (true) {
+ currentTime = Calendar.getInstance()
+ delay(1000)
+ }
+ }
+
+ val timeFormat = remember { SimpleDateFormat("HH:mm", Locale.getDefault()) }
+ val dateFormat = remember { SimpleDateFormat("EEEE, MMMM d", Locale.getDefault()) }
+
+ val timeString = timeFormat.format(currentTime.time)
+ val dateString = dateFormat.format(currentTime.time)
+
+ // Subtle glow animation
+ val infiniteTransition = rememberInfiniteTransition(label = "glow")
+ val glowAlpha by infiniteTransition.animateFloat(
+ initialValue = 0.3f,
+ targetValue = 0.5f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(2000, easing = EaseInOutSine),
+ repeatMode = RepeatMode.Reverse
+ ),
+ label = "alpha"
+ )
+
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .drawBehind {
+ // Subtle neon glow behind clock
+ drawCircle(
+ brush = Brush.radialGradient(
+ colors = listOf(
+ BlackGlassColors.DarkVioletGlow.copy(alpha = glowAlpha * 0.2f),
+ Color.Transparent
+ ),
+ center = Offset(size.width / 2, size.height / 2),
+ radius = size.minDimension
+ )
+ )
+ }
+ .padding(vertical = 32.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ // Time display
+ Text(
+ text = timeString,
+ style = BlackGlassTypography.ClockDisplay,
+ textAlign = TextAlign.Center
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ // Date display
+ Text(
+ text = dateString.uppercase(),
+ style = BlackGlassTypography.DateDisplay,
+ textAlign = TextAlign.Center,
+ letterSpacing = BlackGlassTypography.DateDisplay.letterSpacing
+ )
+ }
+}
+
+/**
+ * Weather widget placeholder (for future implementation)
+ */
+@Composable
+fun WeatherWidget(
+ modifier: Modifier = Modifier
+) {
+ GlassSurface(
+ modifier = modifier
+ .width(160.dp)
+ .height(80.dp),
+ cornerRadius = 20.dp,
+ glassAlpha = 0.15f
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // Weather icon placeholder
+ Text(
+ text = "โ๏ธ",
+ style = BlackGlassTypography.HeadlineLarge
+ )
+
+ Spacer(modifier = Modifier.width(12.dp))
+
+ Column {
+ Text(
+ text = "24ยฐ",
+ style = BlackGlassTypography.TitleLarge,
+ color = BlackGlassColors.TextPrimary
+ )
+ Text(
+ text = "Sunny",
+ style = BlackGlassTypography.BodySmall,
+ color = BlackGlassColors.TextSecondary
+ )
+ }
+ }
+ }
+}
+
+/**
+ * Quick actions widget row
+ */
+@Composable
+fun QuickActionsRow(
+ onWifiClick: () -> Unit,
+ onBluetoothClick: () -> Unit,
+ onFlashlightClick: () -> Unit,
+ onCameraClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = 32.dp),
+ horizontalArrangement = Arrangement.SpaceEvenly
+ ) {
+ QuickActionButton(
+ emoji = "๐ถ",
+ label = "WiFi",
+ onClick = onWifiClick
+ )
+
+ QuickActionButton(
+ emoji = "๐ต",
+ label = "Bluetooth",
+ onClick = onBluetoothClick
+ )
+
+ QuickActionButton(
+ emoji = "๐ฆ",
+ label = "Torch",
+ onClick = onFlashlightClick
+ )
+
+ QuickActionButton(
+ emoji = "๐ท",
+ label = "Camera",
+ onClick = onCameraClick
+ )
+ }
+}
+
+@Composable
+private fun QuickActionButton(
+ emoji: String,
+ label: String,
+ onClick: () -> Unit
+) {
+ CircularGlassButton(
+ onClick = onClick,
+ size = 52.dp
+ ) {
+ Text(
+ text = emoji,
+ style = BlackGlassTypography.TitleLarge
+ )
+ }
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/ElasticGrid.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/ElasticGrid.kt
new file mode 100644
index 00000000..1b904a9a
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/ElasticGrid.kt
@@ -0,0 +1,180 @@
+package com.blackglass.launcher.ui.components
+
+import androidx.compose.animation.core.*
+import androidx.compose.foundation.gestures.detectDragGestures
+import androidx.compose.foundation.layout.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import com.blackglass.launcher.data.model.AppInfo
+import kotlin.math.roundToInt
+
+/**
+ * BlackGlass UI OS - Elastic Grid Component
+ * Free-form placement of widgets and icons (Nova Launcher style)
+ */
+
+data class GridItem(
+ val id: String,
+ val x: Float,
+ val y: Float,
+ val widthCells: Int = 1,
+ val heightCells: Int = 1,
+ val content: @Composable () -> Unit
+)
+
+@Composable
+fun ElasticGrid(
+ items: List,
+ onItemMoved: (String, Float, Float) -> Unit,
+ modifier: Modifier = Modifier,
+ columns: Int = 4,
+ rows: Int = 6,
+ cellPadding: Int = 8
+) {
+ var draggedItemId by remember { mutableStateOf(null) }
+ var dragOffset by remember { mutableStateOf(Offset.Zero) }
+
+ Layout(
+ content = {
+ items.forEach { item ->
+ val isDragging = draggedItemId == item.id
+
+ Box(
+ modifier = Modifier
+ .graphicsLayer {
+ if (isDragging) {
+ translationX = dragOffset.x
+ translationY = dragOffset.y
+ scaleX = 1.1f
+ scaleY = 1.1f
+ alpha = 0.8f
+ }
+ }
+ .pointerInput(item.id) {
+ detectDragGestures(
+ onDragStart = {
+ draggedItemId = item.id
+ dragOffset = Offset.Zero
+ },
+ onDragEnd = {
+ draggedItemId?.let { id ->
+ onItemMoved(
+ id,
+ item.x + dragOffset.x,
+ item.y + dragOffset.y
+ )
+ }
+ draggedItemId = null
+ dragOffset = Offset.Zero
+ },
+ onDragCancel = {
+ draggedItemId = null
+ dragOffset = Offset.Zero
+ },
+ onDrag = { change, dragAmount ->
+ change.consume()
+ dragOffset += dragAmount
+ }
+ )
+ }
+ ) {
+ item.content()
+ }
+ }
+ },
+ modifier = modifier
+ ) { measurables, constraints ->
+ val cellWidth = constraints.maxWidth / columns
+ val cellHeight = constraints.maxHeight / rows
+
+ val placeables = measurables.mapIndexed { index, measurable ->
+ val item = items.getOrNull(index)
+ val itemWidth = (item?.widthCells ?: 1) * cellWidth - (cellPadding * 2)
+ val itemHeight = (item?.heightCells ?: 1) * cellHeight - (cellPadding * 2)
+
+ measurable.measure(
+ Constraints(
+ maxWidth = itemWidth.coerceAtLeast(0),
+ maxHeight = itemHeight.coerceAtLeast(0)
+ )
+ )
+ }
+
+ layout(constraints.maxWidth, constraints.maxHeight) {
+ placeables.forEachIndexed { index, placeable ->
+ val item = items.getOrNull(index) ?: return@forEachIndexed
+
+ // Use stored position or calculate grid position
+ val x = if (item.x > 0) item.x.roundToInt() else {
+ (index % columns) * cellWidth + cellPadding
+ }
+ val y = if (item.y > 0) item.y.roundToInt() else {
+ (index / columns) * cellHeight + cellPadding
+ }
+
+ placeable.place(x, y)
+ }
+ }
+ }
+}
+
+/**
+ * Widget container with resize handles
+ */
+@Composable
+fun ResizableWidget(
+ content: @Composable () -> Unit,
+ onResize: (Int, Int) -> Unit,
+ modifier: Modifier = Modifier,
+ minWidth: Int = 1,
+ minHeight: Int = 1,
+ maxWidth: Int = 4,
+ maxHeight: Int = 4
+) {
+ var currentWidth by remember { mutableIntStateOf(minWidth) }
+ var currentHeight by remember { mutableIntStateOf(minHeight) }
+ var isResizing by remember { mutableStateOf(false) }
+
+ Box(modifier = modifier) {
+ content()
+
+ // Resize handle would go here in edit mode
+ // This is a placeholder for the resize functionality
+ }
+}
+
+/**
+ * Snap-to-grid helper
+ */
+fun snapToGrid(
+ x: Float,
+ y: Float,
+ cellWidth: Int,
+ cellHeight: Int
+): Pair {
+ val snappedX = (x / cellWidth).roundToInt() * cellWidth.toFloat()
+ val snappedY = (y / cellHeight).roundToInt() * cellHeight.toFloat()
+ return snappedX to snappedY
+}
+
+/**
+ * Calculate grid cell from position
+ */
+fun positionToCell(
+ x: Float,
+ y: Float,
+ cellWidth: Int,
+ cellHeight: Int
+): Pair {
+ val col = (x / cellWidth).toInt()
+ val row = (y / cellHeight).toInt()
+ return col to row
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/GlassCard.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/GlassCard.kt
new file mode 100644
index 00000000..70121653
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/GlassCard.kt
@@ -0,0 +1,392 @@
+package com.blackglass.launcher.ui.components
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.*
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsPressedAsState
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.blackglass.launcher.ui.theme.GlassColors
+import com.blackglass.launcher.ui.theme.GlassConfig
+import dev.chrisbanes.haze.HazeState
+import dev.chrisbanes.haze.HazeStyle
+import dev.chrisbanes.haze.hazeChild
+import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
+import dev.chrisbanes.haze.materials.HazeMaterials
+
+/**
+ * BlackGlass UI OS - Glass Card Component
+ * The core UI building block implementing the TRI-LAYER Glass Engine
+ *
+ * TRI-LAYER SPEC:
+ * - Layer 1 (The Void): #050505 background
+ * - Layer 2 (The Frost): #1E1E24 @ 40% alpha + Blur(30dp)
+ * - Layer 3 (The Neon): Gradient #7B0323 โ #2B0033 for borders
+ *
+ * COMPONENT SPEC:
+ * - Clip: RoundedCornerShape(24.dp)
+ * - Border: Brush.linearGradient(WineRed, DeepViolet) width 1.dp alpha 0.3f
+ * - Background: Color.White.copy(alpha = 0.05f) (Glass tint)
+ * - Uses dev.chrisbanes.haze for blur effect
+ */
+
+/**
+ * Glass Card Style variants
+ */
+enum class GlassCardStyle {
+ STANDARD, // Default glass look with Frost layer
+ ELEVATED, // More prominent, brighter border
+ SUBTLE, // Very transparent, minimal
+ NEON, // With neon gradient glow effect
+ FROSTED // Heavy blur using The Frost layer
+}
+
+/**
+ * TRI-LAYER Glass Card - Primary composable
+ *
+ * Uses the Tri-Layer Glass Engine:
+ * - Background: The Void (#050505)
+ * - Surface: The Frost (#1E1E24 @ 40% + blur)
+ * - Border: The Neon gradient (#7B0323 โ #2B0033)
+ */
+@OptIn(ExperimentalHazeMaterialsApi::class)
+@Composable
+fun GlassCard(
+ modifier: Modifier = Modifier,
+ style: GlassCardStyle = GlassCardStyle.STANDARD,
+ cornerRadius: Dp = 24.dp, // Spec: RoundedCornerShape(24.dp)
+ glassAlpha: Float = GlassConfig.DEFAULT_GLASS_ALPHA,
+ borderAlpha: Float = 0.3f, // Spec: alpha 0.3f
+ neonColor: Color = GlassColors.NeonWineStart,
+ hazeState: HazeState? = null,
+ onClick: (() -> Unit)? = null,
+ content: @Composable BoxScope.() -> Unit
+) {
+ val shape = RoundedCornerShape(cornerRadius)
+ val interactionSource = remember { MutableInteractionSource() }
+ val isPressed by interactionSource.collectIsPressedAsState()
+
+ // Animation states
+ val scale by animateFloatAsState(
+ targetValue = if (isPressed && onClick != null) 0.98f else 1f,
+ animationSpec = spring(dampingRatio = 0.5f),
+ label = "scale"
+ )
+
+ val glowAlpha by animateFloatAsState(
+ targetValue = when {
+ isPressed -> 0.6f
+ style == GlassCardStyle.NEON -> 0.4f
+ else -> 0f
+ },
+ animationSpec = tween(GlassConfig.ANIMATION_FAST),
+ label = "glow"
+ )
+
+ // Calculate style-specific values based on Tri-Layer spec
+ val (useNeonBorder, borderWidth, applyHeavyBlur) = when (style) {
+ GlassCardStyle.STANDARD -> Triple(false, 1.dp, false)
+ GlassCardStyle.ELEVATED -> Triple(false, 1.5.dp, false)
+ GlassCardStyle.SUBTLE -> Triple(false, 0.5.dp, false)
+ GlassCardStyle.NEON -> Triple(true, 1.dp, false) // Uses Neon gradient border
+ GlassCardStyle.FROSTED -> Triple(false, 1.dp, true) // Uses Frost blur
+ }
+
+ Box(
+ modifier = modifier
+ .graphicsLayer {
+ scaleX = scale
+ scaleY = scale
+ }
+ // Layer 3 (The Neon) - Glow effect behind card for NEON style
+ .then(
+ if (useNeonBorder || glowAlpha > 0f) {
+ Modifier.drawBehind {
+ drawRect(
+ brush = Brush.radialGradient(
+ colors = listOf(
+ GlassColors.NeonWineStart.copy(alpha = glowAlpha * 0.5f),
+ GlassColors.NeonVioletEnd.copy(alpha = glowAlpha * 0.3f),
+ Color.Transparent
+ ),
+ center = Offset(size.width / 2, size.height / 2),
+ radius = size.maxDimension * 0.9f
+ )
+ )
+ }
+ } else Modifier
+ )
+ // Spec: Clip RoundedCornerShape(24.dp)
+ .clip(shape)
+ // Layer 2 (The Frost) - Apply blur using Haze library
+ .then(
+ hazeState?.let { state ->
+ if (applyHeavyBlur) {
+ // Frosted style: heavier blur
+ Modifier.hazeChild(
+ state = state,
+ style = HazeMaterials.thick()
+ )
+ } else {
+ // Standard blur with Frost settings
+ Modifier.hazeChild(
+ state = state,
+ style = HazeMaterials.thin()
+ )
+ }
+ } ?: Modifier
+ )
+ // Spec: Background Color.White.copy(alpha = 0.05f) (Glass tint)
+ .background(GlassColors.GlassTint)
+ // Layer 2 overlay: The Frost at 40% alpha
+ .background(
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ GlassColors.TheFrostAlpha,
+ GlassColors.TheFrostAlpha.copy(alpha = 0.3f)
+ )
+ )
+ )
+ // Top highlight for glass effect
+ .drawBehind {
+ drawRect(
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ Color.White.copy(alpha = 0.08f),
+ Color.Transparent
+ ),
+ endY = size.height * 0.3f
+ )
+ )
+ }
+ // Spec: Border Brush.linearGradient(WineRed, DeepViolet) width 1.dp alpha 0.3f
+ .border(
+ width = borderWidth,
+ brush = if (useNeonBorder) {
+ // Layer 3 (The Neon) - Gradient border for active states
+ Brush.linearGradient(
+ colors = listOf(
+ GlassColors.NeonWineStart.copy(alpha = 0.6f),
+ GlassColors.NeonVioletEnd.copy(alpha = 0.4f)
+ )
+ )
+ } else {
+ // Standard border with Neon gradient at borderAlpha
+ Brush.linearGradient(
+ colors = listOf(
+ GlassColors.NeonWineStart.copy(alpha = borderAlpha),
+ GlassColors.NeonVioletEnd.copy(alpha = borderAlpha)
+ )
+ )
+ },
+ shape = shape
+ )
+ .then(
+ if (onClick != null) {
+ Modifier.clickable(
+ interactionSource = interactionSource,
+ indication = null,
+ onClick = onClick
+ )
+ } else Modifier
+ ),
+ content = content
+ )
+}
+
+/**
+ * Glass Pill - Horizontal pill-shaped glass surface
+ */
+@Composable
+fun GlassPill(
+ modifier: Modifier = Modifier,
+ glassAlpha: Float = 0.2f,
+ hazeState: HazeState? = null,
+ content: @Composable BoxScope.() -> Unit
+) {
+ GlassCard(
+ modifier = modifier.height(56.dp),
+ cornerRadius = 28.dp,
+ glassAlpha = glassAlpha,
+ hazeState = hazeState,
+ content = content
+ )
+}
+
+/**
+ * Glass Button - Clickable glass surface with press animation
+ */
+@Composable
+fun GlassButton(
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ style: GlassCardStyle = GlassCardStyle.STANDARD,
+ enabled: Boolean = true,
+ content: @Composable BoxScope.() -> Unit
+) {
+ GlassCard(
+ modifier = modifier,
+ style = style,
+ glassAlpha = if (enabled) GlassConfig.DEFAULT_GLASS_ALPHA else 0.05f,
+ onClick = if (enabled) onClick else null,
+ content = content
+ )
+}
+
+/**
+ * Glass Divider - Horizontal gradient line
+ */
+@Composable
+fun GlassDivider(
+ modifier: Modifier = Modifier,
+ color: Color = GlassColors.GlassBorder
+) {
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .height(1.dp)
+ .background(
+ brush = Brush.horizontalGradient(
+ colors = listOf(
+ Color.Transparent,
+ color,
+ color,
+ Color.Transparent
+ )
+ )
+ )
+ )
+}
+
+/**
+ * Glass Container - Simple glass background wrapper
+ */
+@Composable
+fun GlassContainer(
+ modifier: Modifier = Modifier,
+ cornerRadius: Dp = 16.dp,
+ alpha: Float = 0.1f,
+ content: @Composable BoxScope.() -> Unit
+) {
+ Box(
+ modifier = modifier
+ .clip(RoundedCornerShape(cornerRadius))
+ .background(Color.Black.copy(alpha = alpha))
+ .border(
+ width = 0.5.dp,
+ color = Color.White.copy(alpha = 0.1f),
+ shape = RoundedCornerShape(cornerRadius)
+ ),
+ content = content
+ )
+}
+
+/**
+ * Animated Neon Border Card using Layer 3 (The Neon) gradient
+ * Border animates between Wine Red and Deep Violet
+ */
+@Composable
+fun NeonBorderCard(
+ modifier: Modifier = Modifier,
+ cornerRadius: Dp = 24.dp,
+ animateGradient: Boolean = true,
+ hazeState: HazeState? = null,
+ content: @Composable BoxScope.() -> Unit
+) {
+ val infiniteTransition = rememberInfiniteTransition(label = "neon")
+
+ val glowPulse by infiniteTransition.animateFloat(
+ initialValue = 0.3f,
+ targetValue = 0.6f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(1500, easing = EaseInOutSine),
+ repeatMode = RepeatMode.Reverse
+ ),
+ label = "glow_pulse"
+ )
+
+ GlassCard(
+ modifier = modifier,
+ style = GlassCardStyle.NEON,
+ cornerRadius = cornerRadius,
+ borderAlpha = if (animateGradient) glowPulse else 0.4f,
+ hazeState = hazeState,
+ content = content
+ )
+}
+
+/**
+ * Void Background - Layer 1 of Tri-Layer system
+ * Pure black (#050505) with subtle noise texture
+ */
+@Composable
+fun VoidBackground(
+ modifier: Modifier = Modifier,
+ content: @Composable BoxScope.() -> Unit
+) {
+ Box(
+ modifier = modifier
+ .background(GlassColors.TheVoid)
+ // Subtle noise pattern to prevent banding
+ .drawBehind {
+ // 2% noise texture simulation
+ for (i in 0 until 50) {
+ val x = (Math.random() * size.width).toFloat()
+ val y = (Math.random() * size.height).toFloat()
+ drawCircle(
+ color = GlassColors.VoidNoise,
+ radius = 1f,
+ center = Offset(x, y),
+ alpha = 0.02f
+ )
+ }
+ },
+ content = content
+ )
+}
+
+/**
+ * Frost Surface - Layer 2 of Tri-Layer system
+ * #1E1E24 @ 40% Alpha + Blur(30dp) for widget backgrounds
+ */
+@OptIn(ExperimentalHazeMaterialsApi::class)
+@Composable
+fun FrostSurface(
+ modifier: Modifier = Modifier,
+ hazeState: HazeState,
+ cornerRadius: Dp = 24.dp,
+ content: @Composable BoxScope.() -> Unit
+) {
+ val shape = RoundedCornerShape(cornerRadius)
+
+ Box(
+ modifier = modifier
+ .clip(shape)
+ .hazeChild(
+ state = hazeState,
+ style = HazeMaterials.ultraThin()
+ )
+ .background(GlassColors.TheFrostAlpha)
+ .border(
+ width = 1.dp,
+ brush = GlassColors.NeonBorderGradient,
+ shape = shape
+ ),
+ content = content
+ )
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/GlassComponents.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/GlassComponents.kt
new file mode 100644
index 00000000..60964679
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/GlassComponents.kt
@@ -0,0 +1,323 @@
+package com.blackglass.launcher.ui.components
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.*
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsPressedAsState
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.blur
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.blackglass.launcher.ui.theme.BlackGlassColors
+
+/**
+ * BlackGlass UI OS - Glass Surface Components
+ * Tri-layer deep glass rendering system
+ */
+
+/**
+ * Glass surface with blur effect and neon border
+ */
+@Composable
+fun GlassSurface(
+ modifier: Modifier = Modifier,
+ cornerRadius: Dp = 24.dp,
+ glassAlpha: Float = 0.15f,
+ borderAlpha: Float = 0.2f,
+ borderWidth: Dp = 1.dp,
+ hasNeonGlow: Boolean = false,
+ neonColor: Color = BlackGlassColors.NeonWineGlow,
+ content: @Composable BoxScope.() -> Unit
+) {
+ val glassColor = Color.Black.copy(alpha = glassAlpha)
+ val borderColor = Color.White.copy(alpha = borderAlpha)
+
+ Box(
+ modifier = modifier
+ .clip(RoundedCornerShape(cornerRadius))
+ .then(
+ if (hasNeonGlow) {
+ Modifier.drawBehind {
+ drawRect(
+ brush = Brush.radialGradient(
+ colors = listOf(
+ neonColor.copy(alpha = 0.3f),
+ Color.Transparent
+ ),
+ center = Offset(size.width / 2, size.height / 2),
+ radius = size.maxDimension
+ )
+ )
+ }
+ } else Modifier
+ )
+ .background(
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ glassColor,
+ glassColor.copy(alpha = glassAlpha * 0.7f)
+ )
+ )
+ )
+ .border(
+ width = borderWidth,
+ color = borderColor,
+ shape = RoundedCornerShape(cornerRadius)
+ ),
+ content = content
+ )
+}
+
+/**
+ * Pill-shaped glass surface (for search bar)
+ */
+@Composable
+fun GlassPill(
+ modifier: Modifier = Modifier,
+ glassAlpha: Float = 0.2f,
+ content: @Composable BoxScope.() -> Unit
+) {
+ GlassSurface(
+ modifier = modifier.height(56.dp),
+ cornerRadius = 28.dp,
+ glassAlpha = glassAlpha,
+ borderAlpha = 0.15f,
+ content = content
+ )
+}
+
+/**
+ * Neon-bordered button with glow effect
+ */
+@Composable
+fun NeonGlowButton(
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ color: Color = BlackGlassColors.NeonWineGlow,
+ cornerRadius: Dp = 16.dp,
+ content: @Composable BoxScope.() -> Unit
+) {
+ val interactionSource = remember { MutableInteractionSource() }
+ val isPressed by interactionSource.collectIsPressedAsState()
+
+ val glowAlpha by animateFloatAsState(
+ targetValue = if (isPressed) 0.8f else 0.4f,
+ animationSpec = tween(150),
+ label = "glow"
+ )
+
+ val scale by animateFloatAsState(
+ targetValue = if (isPressed) 0.95f else 1f,
+ animationSpec = spring(dampingRatio = 0.5f),
+ label = "scale"
+ )
+
+ Box(
+ modifier = modifier
+ .graphicsLayer {
+ scaleX = scale
+ scaleY = scale
+ }
+ .drawBehind {
+ // Neon glow effect
+ drawRect(
+ brush = Brush.radialGradient(
+ colors = listOf(
+ color.copy(alpha = glowAlpha),
+ Color.Transparent
+ ),
+ center = Offset(size.width / 2, size.height / 2),
+ radius = size.maxDimension * 0.8f
+ )
+ )
+ }
+ .clip(RoundedCornerShape(cornerRadius))
+ .background(
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ color.copy(alpha = 0.3f),
+ color.copy(alpha = 0.15f)
+ )
+ )
+ )
+ .border(
+ width = 1.5.dp,
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ color.copy(alpha = 0.8f),
+ color.copy(alpha = 0.4f)
+ )
+ ),
+ shape = RoundedCornerShape(cornerRadius)
+ )
+ .clickable(
+ interactionSource = interactionSource,
+ indication = null,
+ onClick = onClick
+ ),
+ content = content
+ )
+}
+
+/**
+ * Circular glass button (for dock items)
+ */
+@Composable
+fun CircularGlassButton(
+ onClick: () -> Unit,
+ onLongClick: (() -> Unit)? = null,
+ modifier: Modifier = Modifier,
+ size: Dp = 56.dp,
+ hasActiveGlow: Boolean = false,
+ glowColor: Color = BlackGlassColors.NeonWineGlow,
+ content: @Composable BoxScope.() -> Unit
+) {
+ val interactionSource = remember { MutableInteractionSource() }
+ val isPressed by interactionSource.collectIsPressedAsState()
+
+ val scale by animateFloatAsState(
+ targetValue = if (isPressed) 0.9f else 1f,
+ animationSpec = spring(dampingRatio = 0.5f, stiffness = Spring.StiffnessMedium),
+ label = "scale"
+ )
+
+ val glowAlpha by animateFloatAsState(
+ targetValue = when {
+ isPressed -> 1f
+ hasActiveGlow -> 0.6f
+ else -> 0f
+ },
+ animationSpec = tween(200),
+ label = "glow"
+ )
+
+ Box(
+ modifier = modifier
+ .size(size)
+ .graphicsLayer {
+ scaleX = scale
+ scaleY = scale
+ }
+ .then(
+ if (glowAlpha > 0f) {
+ Modifier.drawBehind {
+ drawCircle(
+ brush = Brush.radialGradient(
+ colors = listOf(
+ glowColor.copy(alpha = glowAlpha * 0.5f),
+ Color.Transparent
+ )
+ ),
+ radius = this.size.maxDimension * 0.7f
+ )
+ }
+ } else Modifier
+ )
+ .clip(CircleShape)
+ .background(
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ Color.White.copy(alpha = 0.15f),
+ Color.White.copy(alpha = 0.08f)
+ )
+ )
+ )
+ .border(
+ width = 1.dp,
+ color = Color.White.copy(alpha = 0.2f),
+ shape = CircleShape
+ )
+ .clickable(
+ interactionSource = interactionSource,
+ indication = null,
+ onClick = onClick
+ ),
+ contentAlignment = Alignment.Center,
+ content = content
+ )
+}
+
+/**
+ * Gradient divider line with glass effect
+ */
+@Composable
+fun GlassDivider(
+ modifier: Modifier = Modifier,
+ color: Color = BlackGlassColors.GlassBorder
+) {
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .height(1.dp)
+ .background(
+ brush = Brush.horizontalGradient(
+ colors = listOf(
+ Color.Transparent,
+ color,
+ color,
+ Color.Transparent
+ )
+ )
+ )
+ )
+}
+
+/**
+ * Animated neon gradient border
+ */
+@Composable
+fun NeonBorderBox(
+ modifier: Modifier = Modifier,
+ cornerRadius: Dp = 16.dp,
+ content: @Composable BoxScope.() -> Unit
+) {
+ val infiniteTransition = rememberInfiniteTransition(label = "neon")
+
+ val animatedOffset by infiniteTransition.animateFloat(
+ initialValue = 0f,
+ targetValue = 1f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(3000, easing = LinearEasing),
+ repeatMode = RepeatMode.Restart
+ ),
+ label = "offset"
+ )
+
+ val gradientColors = listOf(
+ BlackGlassColors.NeonWineGlow,
+ BlackGlassColors.DarkVioletGlow,
+ BlackGlassColors.AccentCyan,
+ BlackGlassColors.NeonWineGlow
+ )
+
+ Box(
+ modifier = modifier
+ .clip(RoundedCornerShape(cornerRadius))
+ .background(BlackGlassColors.GlassLayer2)
+ .border(
+ width = 2.dp,
+ brush = Brush.sweepGradient(
+ colors = gradientColors,
+ center = Offset(animatedOffset * 1000, animatedOffset * 1000)
+ ),
+ shape = RoundedCornerShape(cornerRadius)
+ ),
+ content = content
+ )
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/NeonDock.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/NeonDock.kt
new file mode 100644
index 00000000..5f9c4443
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/NeonDock.kt
@@ -0,0 +1,274 @@
+package com.blackglass.launcher.ui.components
+
+import android.content.Intent
+import android.provider.MediaStore
+import android.provider.Settings
+import androidx.compose.animation.*
+import androidx.compose.animation.core.*
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.*
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import com.blackglass.launcher.ui.theme.GlassColors
+import dev.chrisbanes.haze.HazeState
+import dev.chrisbanes.haze.hazeChild
+import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
+import dev.chrisbanes.haze.materials.HazeMaterials
+
+/**
+ * BlackGlass UI OS - Neon Dock Component
+ *
+ * PHASE 3 SPEC:
+ * - 5 fixed icons: Phone, Messages, Browser, Camera, Settings
+ * - On touch: Emit "Neon Glow" shadow with blur radius 15dp and color #7B0323
+ */
+
+/**
+ * SPEC: Neon glow color #7B0323 (Wine Red from The Neon layer)
+ */
+private val NeonGlowColor = Color(0xFF7B0323)
+
+/**
+ * The 5 fixed dock items as per spec
+ */
+private val FixedDockItems = listOf(
+ Icons.Default.Phone to "Phone",
+ Icons.Default.Message to "Messages",
+ Icons.Default.Language to "Browser",
+ Icons.Default.CameraAlt to "Camera",
+ Icons.Default.Settings to "Settings"
+)
+
+/**
+ * Neon Dock - Phase 3 Implementation
+ *
+ * SPEC:
+ * - Visual: 5 fixed icons (Phone, Messages, Browser, Camera, Settings)
+ * - Interaction: On touch down, emit "Neon Glow" shadow
+ * (BoxShadow with blur radius 15dp and color #7B0323)
+ */
+@OptIn(ExperimentalHazeMaterialsApi::class)
+@Composable
+fun NeonDock(
+ modifier: Modifier = Modifier,
+ hazeState: HazeState? = null
+) {
+ val context = LocalContext.current
+
+ // Track which icon is currently pressed (-1 = none)
+ var pressedIndex by remember { mutableIntStateOf(-1) }
+
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = 24.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ // Dock container with Frost layer styling
+ Row(
+ modifier = Modifier
+ .clip(RoundedCornerShape(36.dp))
+ .then(
+ hazeState?.let {
+ Modifier.hazeChild(it, style = HazeMaterials.thin())
+ } ?: Modifier
+ )
+ // Layer 2 (The Frost) background
+ .background(GlassColors.TheFrostAlpha)
+ // Layer 3 (The Neon) gradient border
+ .border(
+ width = 1.dp,
+ brush = GlassColors.NeonBorderGradient,
+ shape = RoundedCornerShape(36.dp)
+ )
+ .padding(horizontal = 20.dp, vertical = 14.dp),
+ horizontalArrangement = Arrangement.spacedBy(20.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ FixedDockItems.forEachIndexed { index, (icon, label) ->
+ NeonDockIcon(
+ icon = icon,
+ label = label,
+ isPressed = pressedIndex == index,
+ onTouchDown = { pressedIndex = index },
+ onTouchUp = { pressedIndex = -1 },
+ onClick = {
+ // Launch appropriate action for each dock item
+ when (index) {
+ 0 -> { // Phone
+ val intent = Intent(Intent.ACTION_DIAL).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ }
+ context.startActivity(intent)
+ }
+ 1 -> { // Messages
+ val intent = Intent(Intent.ACTION_MAIN).apply {
+ addCategory(Intent.CATEGORY_APP_MESSAGING)
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ }
+ try { context.startActivity(intent) } catch (_: Exception) {}
+ }
+ 2 -> { // Browser
+ val intent = Intent(Intent.ACTION_MAIN).apply {
+ addCategory(Intent.CATEGORY_APP_BROWSER)
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ }
+ try { context.startActivity(intent) } catch (_: Exception) {}
+ }
+ 3 -> { // Camera
+ val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ }
+ try { context.startActivity(intent) } catch (_: Exception) {}
+ }
+ 4 -> { // Settings
+ val intent = Intent(Settings.ACTION_SETTINGS).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ }
+ context.startActivity(intent)
+ }
+ }
+ }
+ )
+ }
+ }
+ }
+}
+
+/**
+ * Individual dock icon with Neon Glow effect
+ *
+ * SPEC: On touch, emit BoxShadow with blur radius 15dp and color #7B0323
+ */
+@Composable
+private fun NeonDockIcon(
+ icon: ImageVector,
+ label: String,
+ isPressed: Boolean,
+ onTouchDown: () -> Unit,
+ onTouchUp: () -> Unit,
+ onClick: () -> Unit
+) {
+ // Animated glow intensity (0 to 1)
+ val glowAlpha by animateFloatAsState(
+ targetValue = if (isPressed) 1f else 0f,
+ animationSpec = tween(100),
+ label = "neon_glow"
+ )
+
+ // Scale animation on press
+ val scale by animateFloatAsState(
+ targetValue = if (isPressed) 0.88f else 1f,
+ animationSpec = spring(
+ dampingRatio = Spring.DampingRatioMediumBouncy,
+ stiffness = Spring.StiffnessHigh
+ ),
+ label = "press_scale"
+ )
+
+ Box(
+ modifier = Modifier
+ .size(52.dp)
+ .graphicsLayer {
+ scaleX = scale
+ scaleY = scale
+ }
+ // SPEC: BoxShadow with blur radius 15dp and color #7B0323
+ .drawBehind {
+ if (glowAlpha > 0f) {
+ // Neon glow shadow - simulates 15dp blur radius
+ drawCircle(
+ brush = Brush.radialGradient(
+ colors = listOf(
+ NeonGlowColor.copy(alpha = glowAlpha * 0.9f),
+ NeonGlowColor.copy(alpha = glowAlpha * 0.5f),
+ NeonGlowColor.copy(alpha = glowAlpha * 0.2f),
+ Color.Transparent
+ ),
+ center = Offset(size.width / 2, size.height / 2),
+ radius = size.width * 1.3f // ~15dp blur effect
+ )
+ )
+ }
+ }
+ .clip(CircleShape)
+ .background(
+ color = if (isPressed)
+ NeonGlowColor.copy(alpha = 0.35f)
+ else
+ GlassColors.GlassTint
+ )
+ .border(
+ width = 1.dp,
+ color = if (isPressed)
+ NeonGlowColor.copy(alpha = 0.7f)
+ else
+ Color.White.copy(alpha = 0.15f),
+ shape = CircleShape
+ )
+ .pointerInput(Unit) {
+ detectTapGestures(
+ onPress = {
+ onTouchDown()
+ tryAwaitRelease()
+ onTouchUp()
+ },
+ onTap = { onClick() }
+ )
+ },
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = label,
+ tint = if (isPressed) NeonGlowColor else GlassColors.TextPrimary,
+ modifier = Modifier.size(26.dp)
+ )
+ }
+}
+
+/**
+ * Active app indicator dot
+ */
+@Composable
+fun DockActiveIndicator(
+ isActive: Boolean,
+ modifier: Modifier = Modifier
+) {
+ AnimatedVisibility(
+ visible = isActive,
+ enter = fadeIn() + scaleIn(),
+ exit = fadeOut() + scaleOut()
+ ) {
+ Box(
+ modifier = modifier
+ .size(4.dp)
+ .clip(CircleShape)
+ .background(NeonGlowColor)
+ .drawBehind {
+ drawCircle(
+ color = NeonGlowColor.copy(alpha = 0.5f),
+ radius = size.maxDimension
+ )
+ }
+ )
+ }
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/NeonIcon.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/NeonIcon.kt
new file mode 100644
index 00000000..61088545
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/NeonIcon.kt
@@ -0,0 +1,384 @@
+package com.blackglass.launcher.ui.components
+
+import android.graphics.drawable.Drawable
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.*
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsPressedAsState
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalHapticFeedback
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.blackglass.launcher.ui.theme.GlassColors
+import com.blackglass.launcher.ui.theme.Type
+import com.google.accompanist.drawablepainter.rememberDrawablePainter
+
+/**
+ * BlackGlass UI OS - Neon Icon Component
+ * App icons with glow effects and animations
+ */
+
+/**
+ * Glow intensity levels
+ */
+enum class GlowIntensity {
+ NONE,
+ SUBTLE,
+ MEDIUM,
+ BRIGHT,
+ PULSE // Animated pulsing glow
+}
+
+/**
+ * Neon Icon - Icon with glow effect
+ */
+@Composable
+fun NeonIcon(
+ drawable: Drawable?,
+ contentDescription: String?,
+ modifier: Modifier = Modifier,
+ size: Dp = 56.dp,
+ glowColor: Color = GlassColors.NeonWineGlow,
+ glowIntensity: GlowIntensity = GlowIntensity.NONE,
+ showBackground: Boolean = true,
+ cornerRadius: Dp = 14.dp,
+ onClick: (() -> Unit)? = null,
+ onLongClick: (() -> Unit)? = null
+) {
+ val haptic = LocalHapticFeedback.current
+ var isPressed by remember { mutableStateOf(false) }
+
+ // Animation for press state
+ val scale by animateFloatAsState(
+ targetValue = if (isPressed) 0.9f else 1f,
+ animationSpec = spring(
+ dampingRatio = Spring.DampingRatioMediumBouncy,
+ stiffness = Spring.StiffnessMedium
+ ),
+ label = "scale"
+ )
+
+ // Pulsing glow animation
+ val infiniteTransition = rememberInfiniteTransition(label = "pulse")
+ val pulseAlpha by infiniteTransition.animateFloat(
+ initialValue = 0.3f,
+ targetValue = 0.7f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(1000, easing = EaseInOutSine),
+ repeatMode = RepeatMode.Reverse
+ ),
+ label = "pulse"
+ )
+
+ // Calculate glow alpha based on intensity
+ val glowAlpha = when (glowIntensity) {
+ GlowIntensity.NONE -> 0f
+ GlowIntensity.SUBTLE -> 0.2f
+ GlowIntensity.MEDIUM -> 0.4f
+ GlowIntensity.BRIGHT -> 0.7f
+ GlowIntensity.PULSE -> pulseAlpha
+ }
+
+ // Press state glow
+ val pressGlowAlpha by animateFloatAsState(
+ targetValue = if (isPressed) 0.6f else glowAlpha,
+ animationSpec = tween(150),
+ label = "pressGlow"
+ )
+
+ Box(
+ modifier = modifier
+ .size(size)
+ .graphicsLayer {
+ scaleX = scale
+ scaleY = scale
+ }
+ // Glow effect
+ .then(
+ if (pressGlowAlpha > 0f) {
+ Modifier.drawBehind {
+ drawCircle(
+ brush = Brush.radialGradient(
+ colors = listOf(
+ glowColor.copy(alpha = pressGlowAlpha),
+ glowColor.copy(alpha = pressGlowAlpha * 0.5f),
+ Color.Transparent
+ )
+ ),
+ radius = this.size.maxDimension * 0.7f
+ )
+ }
+ } else Modifier
+ )
+ // Background
+ .then(
+ if (showBackground) {
+ Modifier
+ .clip(RoundedCornerShape(cornerRadius))
+ .background(
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ Color.White.copy(alpha = 0.12f),
+ Color.White.copy(alpha = 0.05f)
+ )
+ )
+ )
+ .border(
+ width = 0.5.dp,
+ color = Color.White.copy(alpha = 0.15f),
+ shape = RoundedCornerShape(cornerRadius)
+ )
+ } else Modifier
+ )
+ .pointerInput(onClick, onLongClick) {
+ detectTapGestures(
+ onPress = {
+ isPressed = true
+ tryAwaitRelease()
+ isPressed = false
+ },
+ onTap = {
+ haptic.performHapticFeedback(HapticFeedbackType.LongPress)
+ onClick?.invoke()
+ },
+ onLongPress = {
+ haptic.performHapticFeedback(HapticFeedbackType.LongPress)
+ onLongClick?.invoke()
+ }
+ )
+ },
+ contentAlignment = Alignment.Center
+ ) {
+ // Icon image
+ drawable?.let {
+ Image(
+ painter = rememberDrawablePainter(drawable = it),
+ contentDescription = contentDescription,
+ modifier = Modifier
+ .size(size - 8.dp)
+ .clip(RoundedCornerShape(cornerRadius - 2.dp))
+ )
+ }
+ }
+}
+
+/**
+ * Neon App Icon - Complete app icon with label
+ */
+@Composable
+fun NeonAppIcon(
+ icon: Drawable?,
+ label: String,
+ modifier: Modifier = Modifier,
+ iconSize: Dp = 56.dp,
+ glowColor: Color = GlassColors.NeonWineGlow,
+ showLabel: Boolean = true,
+ isEditing: Boolean = false,
+ onClick: () -> Unit,
+ onLongClick: () -> Unit = {}
+) {
+ // Wiggle animation for edit mode
+ val infiniteTransition = rememberInfiniteTransition(label = "wiggle")
+ val wiggleRotation by infiniteTransition.animateFloat(
+ initialValue = -2f,
+ targetValue = 2f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(150, easing = LinearEasing),
+ repeatMode = RepeatMode.Reverse
+ ),
+ label = "rotation"
+ )
+
+ Column(
+ modifier = modifier
+ .width(80.dp)
+ .graphicsLayer {
+ rotationZ = if (isEditing) wiggleRotation else 0f
+ },
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ NeonIcon(
+ drawable = icon,
+ contentDescription = label,
+ size = iconSize,
+ glowColor = glowColor,
+ onClick = onClick,
+ onLongClick = onLongClick
+ )
+
+ if (showLabel) {
+ Spacer(modifier = Modifier.height(6.dp))
+ Text(
+ text = label,
+ style = Type.appLabel,
+ color = GlassColors.TextPrimary,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
+}
+
+/**
+ * Circular Neon Icon - For dock items
+ */
+@Composable
+fun CircularNeonIcon(
+ drawable: Drawable?,
+ contentDescription: String?,
+ modifier: Modifier = Modifier,
+ size: Dp = 52.dp,
+ glowColor: Color = GlassColors.NeonWineGlow,
+ isActive: Boolean = false,
+ onClick: () -> Unit,
+ onLongClick: () -> Unit = {}
+) {
+ val haptic = LocalHapticFeedback.current
+ var isPressed by remember { mutableStateOf(false) }
+
+ val scale by animateFloatAsState(
+ targetValue = when {
+ isPressed -> 1.15f
+ isActive -> 1.05f
+ else -> 1f
+ },
+ animationSpec = spring(
+ dampingRatio = Spring.DampingRatioMediumBouncy,
+ stiffness = Spring.StiffnessLow
+ ),
+ label = "scale"
+ )
+
+ val glowAlpha by animateFloatAsState(
+ targetValue = when {
+ isPressed -> 0.8f
+ isActive -> 0.5f
+ else -> 0f
+ },
+ animationSpec = tween(150),
+ label = "glow"
+ )
+
+ Box(
+ modifier = modifier
+ .size(size)
+ .graphicsLayer {
+ scaleX = scale
+ scaleY = scale
+ translationY = if (isPressed) -8.dp.toPx() else 0f
+ }
+ .then(
+ if (glowAlpha > 0f) {
+ Modifier.drawBehind {
+ drawCircle(
+ brush = Brush.radialGradient(
+ colors = listOf(
+ glowColor.copy(alpha = glowAlpha),
+ glowColor.copy(alpha = glowAlpha * 0.5f),
+ Color.Transparent
+ )
+ ),
+ radius = this.size.maxDimension * 0.8f
+ )
+ }
+ } else Modifier
+ )
+ .clip(CircleShape)
+ .background(
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ Color.White.copy(alpha = 0.15f),
+ Color.White.copy(alpha = 0.05f)
+ )
+ )
+ )
+ .border(
+ width = 1.dp,
+ color = Color.White.copy(alpha = 0.2f),
+ shape = CircleShape
+ )
+ .pointerInput(Unit) {
+ detectTapGestures(
+ onPress = {
+ isPressed = true
+ tryAwaitRelease()
+ isPressed = false
+ },
+ onTap = {
+ haptic.performHapticFeedback(HapticFeedbackType.LongPress)
+ onClick()
+ },
+ onLongPress = {
+ haptic.performHapticFeedback(HapticFeedbackType.LongPress)
+ onLongClick()
+ }
+ )
+ },
+ contentAlignment = Alignment.Center
+ ) {
+ drawable?.let {
+ Image(
+ painter = rememberDrawablePainter(drawable = it),
+ contentDescription = contentDescription,
+ modifier = Modifier
+ .size(size - 12.dp)
+ .clip(CircleShape)
+ )
+ }
+ }
+}
+
+/**
+ * Active indicator dot for dock items
+ */
+@Composable
+fun NeonActiveIndicator(
+ isActive: Boolean,
+ color: Color = GlassColors.NeonWineGlow,
+ modifier: Modifier = Modifier
+) {
+ val alpha by animateFloatAsState(
+ targetValue = if (isActive) 1f else 0f,
+ animationSpec = tween(200),
+ label = "alpha"
+ )
+
+ if (alpha > 0f) {
+ Box(
+ modifier = modifier
+ .size(4.dp)
+ .graphicsLayer { this.alpha = alpha }
+ .clip(CircleShape)
+ .background(color)
+ .drawBehind {
+ drawCircle(
+ color = color.copy(alpha = 0.5f),
+ radius = size.maxDimension
+ )
+ }
+ )
+ }
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/UniversalSearch.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/UniversalSearch.kt
new file mode 100644
index 00000000..64c7f4a5
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/UniversalSearch.kt
@@ -0,0 +1,267 @@
+package com.blackglass.launcher.ui.components
+
+import androidx.compose.animation.*
+import androidx.compose.animation.core.*
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material.icons.outlined.Language
+import androidx.compose.material.icons.outlined.Person
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.unit.dp
+import com.blackglass.launcher.ui.theme.GlassColors
+import com.blackglass.launcher.ui.theme.Type
+
+/**
+ * BlackGlass UI OS - Universal Search Component
+ * Global search pill for Apps, Contacts, and Web
+ */
+
+@Composable
+fun UniversalSearchBar(
+ query: String,
+ onQueryChange: (String) -> Unit,
+ onSearch: () -> Unit,
+ onWebSearch: (String) -> Unit,
+ isExpanded: Boolean,
+ onExpandedChange: (Boolean) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val focusRequester = remember { FocusRequester() }
+ val focusManager = LocalFocusManager.current
+ val keyboardController = LocalSoftwareKeyboardController.current
+
+ LaunchedEffect(isExpanded) {
+ if (isExpanded) {
+ focusRequester.requestFocus()
+ }
+ }
+
+ AnimatedContent(
+ targetState = isExpanded,
+ transitionSpec = {
+ fadeIn(animationSpec = tween(200)) +
+ scaleIn(initialScale = 0.9f, animationSpec = tween(200)) togetherWith
+ fadeOut(animationSpec = tween(200)) +
+ scaleOut(targetScale = 0.9f, animationSpec = tween(200))
+ },
+ label = "search"
+ ) { expanded ->
+ if (expanded) {
+ // Expanded search bar
+ GlassCard(
+ modifier = modifier
+ .fillMaxWidth()
+ .height(56.dp),
+ style = GlassCardStyle.FROSTED,
+ cornerRadius = 28.dp
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = Icons.Default.Search,
+ contentDescription = "Search",
+ tint = GlassColors.TextSecondary,
+ modifier = Modifier.size(24.dp)
+ )
+
+ Spacer(modifier = Modifier.width(12.dp))
+
+ BasicTextField(
+ value = query,
+ onValueChange = onQueryChange,
+ modifier = Modifier
+ .weight(1f)
+ .focusRequester(focusRequester),
+ textStyle = Type.bodyLarge.copy(
+ color = GlassColors.TextPrimary
+ ),
+ cursorBrush = SolidColor(GlassColors.AccentCyan),
+ singleLine = true,
+ keyboardOptions = KeyboardOptions(
+ imeAction = ImeAction.Search
+ ),
+ keyboardActions = KeyboardActions(
+ onSearch = {
+ if (query.isNotBlank()) {
+ onWebSearch(query)
+ }
+ keyboardController?.hide()
+ }
+ ),
+ decorationBox = { innerTextField ->
+ Box {
+ if (query.isEmpty()) {
+ Text(
+ text = "Search apps, contacts, webโฆ",
+ style = Type.searchHint,
+ color = GlassColors.TextTertiary
+ )
+ }
+ innerTextField()
+ }
+ }
+ )
+
+ if (query.isNotEmpty()) {
+ Spacer(modifier = Modifier.width(8.dp))
+
+ // Web search button
+ Box(
+ modifier = Modifier
+ .size(32.dp)
+ .clip(CircleShape)
+ .background(GlassColors.AccentCyan.copy(alpha = 0.2f))
+ .clickable { onWebSearch(query) },
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.Language,
+ contentDescription = "Web search",
+ tint = GlassColors.AccentCyan,
+ modifier = Modifier.size(18.dp)
+ )
+ }
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ // Clear button
+ Box(
+ modifier = Modifier
+ .size(32.dp)
+ .clip(CircleShape)
+ .background(Color.White.copy(alpha = 0.1f))
+ .clickable {
+ onQueryChange("")
+ },
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.Default.Close,
+ contentDescription = "Clear",
+ tint = GlassColors.TextSecondary,
+ modifier = Modifier.size(18.dp)
+ )
+ }
+ }
+ }
+ }
+ } else {
+ // Collapsed search pill
+ GlassCard(
+ modifier = modifier
+ .width(200.dp)
+ .height(48.dp)
+ .clickable { onExpandedChange(true) },
+ style = GlassCardStyle.SUBTLE,
+ cornerRadius = 24.dp
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Icon(
+ imageVector = Icons.Default.Search,
+ contentDescription = "Search",
+ tint = GlassColors.TextTertiary,
+ modifier = Modifier.size(20.dp)
+ )
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ Text(
+ text = "Search",
+ style = Type.bodyMedium,
+ color = GlassColors.TextTertiary
+ )
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Search result item
+ */
+@Composable
+fun SearchResultItem(
+ icon: @Composable () -> Unit,
+ title: String,
+ subtitle: String? = null,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ trailingIcon: @Composable (() -> Unit)? = null
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(12.dp))
+ .clickable { onClick() }
+ .padding(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // Icon container
+ Box(
+ modifier = Modifier
+ .size(44.dp)
+ .clip(RoundedCornerShape(10.dp))
+ .background(Color.White.copy(alpha = 0.1f)),
+ contentAlignment = Alignment.Center
+ ) {
+ icon()
+ }
+
+ Spacer(modifier = Modifier.width(12.dp))
+
+ // Text content
+ Column(
+ modifier = Modifier.weight(1f)
+ ) {
+ Text(
+ text = title,
+ style = Type.titleMedium,
+ color = GlassColors.TextPrimary
+ )
+
+ if (subtitle != null) {
+ Text(
+ text = subtitle,
+ style = Type.bodySmall,
+ color = GlassColors.TextTertiary
+ )
+ }
+ }
+
+ // Trailing icon
+ trailingIcon?.invoke()
+ }
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/UniversalSearchPill.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/UniversalSearchPill.kt
new file mode 100644
index 00000000..5e05a02c
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/components/UniversalSearchPill.kt
@@ -0,0 +1,447 @@
+package com.blackglass.launcher.ui.components
+
+import android.content.Intent
+import android.net.Uri
+import androidx.compose.animation.*
+import androidx.compose.animation.core.*
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.*
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.unit.dp
+import com.blackglass.launcher.data.AppInfo
+import com.blackglass.launcher.ui.theme.GlassColors
+import com.blackglass.launcher.ui.theme.Type
+import dev.chrisbanes.haze.HazeState
+import dev.chrisbanes.haze.hazeChild
+import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
+import dev.chrisbanes.haze.materials.HazeMaterials
+
+/**
+ * BlackGlass UI OS - Universal Search Pill
+ *
+ * PHASE 3 SPEC:
+ * - Visual: Pill-shaped glass field floating 16dp above bottom edge
+ * - Function: Replaces standard Dock. When tapped, expands to show keyboard
+ * and search results (Apps first, then Contacts)
+ */
+
+@OptIn(ExperimentalHazeMaterialsApi::class)
+@Composable
+fun UniversalSearchPill(
+ query: String,
+ onQueryChange: (String) -> Unit,
+ searchResults: List,
+ onAppClick: (AppInfo) -> Unit,
+ onWebSearch: (String) -> Unit,
+ isExpanded: Boolean,
+ onExpandedChange: (Boolean) -> Unit,
+ modifier: Modifier = Modifier,
+ hazeState: HazeState? = null
+) {
+ val context = LocalContext.current
+ val focusRequester = remember { FocusRequester() }
+ val keyboardController = LocalSoftwareKeyboardController.current
+ val focusManager = LocalFocusManager.current
+
+ // Request focus when expanded
+ LaunchedEffect(isExpanded) {
+ if (isExpanded) {
+ focusRequester.requestFocus()
+ } else {
+ focusManager.clearFocus()
+ keyboardController?.hide()
+ }
+ }
+
+ // SPEC: Floating 16dp above bottom edge
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = 32.dp)
+ .padding(bottom = 16.dp), // 16dp above bottom
+ contentAlignment = Alignment.Center
+ ) {
+ AnimatedContent(
+ targetState = isExpanded,
+ transitionSpec = {
+ (fadeIn(animationSpec = tween(200)) +
+ scaleIn(initialScale = 0.9f, animationSpec = tween(200)))
+ .togetherWith(
+ fadeOut(animationSpec = tween(150)) +
+ scaleOut(targetScale = 0.95f, animationSpec = tween(150))
+ )
+ },
+ label = "search_expand"
+ ) { expanded ->
+ if (expanded) {
+ // Expanded search with results
+ ExpandedSearchPill(
+ query = query,
+ onQueryChange = onQueryChange,
+ searchResults = searchResults,
+ onAppClick = {
+ onAppClick(it)
+ onExpandedChange(false)
+ },
+ onWebSearch = {
+ onWebSearch(query)
+ onExpandedChange(false)
+ },
+ onClose = { onExpandedChange(false) },
+ focusRequester = focusRequester,
+ hazeState = hazeState
+ )
+ } else {
+ // Collapsed pill
+ CollapsedSearchPill(
+ onClick = { onExpandedChange(true) },
+ hazeState = hazeState
+ )
+ }
+ }
+ }
+}
+
+/**
+ * Collapsed search pill - tap to expand
+ */
+@OptIn(ExperimentalHazeMaterialsApi::class)
+@Composable
+private fun CollapsedSearchPill(
+ onClick: () -> Unit,
+ hazeState: HazeState?
+) {
+ Row(
+ modifier = Modifier
+ .clip(RoundedCornerShape(28.dp))
+ .then(
+ hazeState?.let {
+ Modifier.hazeChild(it, style = HazeMaterials.thin())
+ } ?: Modifier
+ )
+ // Layer 2 (The Frost)
+ .background(GlassColors.TheFrostAlpha)
+ // Layer 3 (The Neon) border
+ .border(
+ width = 1.dp,
+ brush = GlassColors.NeonBorderGradient,
+ shape = RoundedCornerShape(28.dp)
+ )
+ .clickable { onClick() }
+ .padding(horizontal = 24.dp, vertical = 14.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Icon(
+ imageVector = Icons.Default.Search,
+ contentDescription = "Search",
+ tint = GlassColors.TextTertiary,
+ modifier = Modifier.size(20.dp)
+ )
+
+ Spacer(modifier = Modifier.width(12.dp))
+
+ Text(
+ text = "Search apps, contacts, webโฆ",
+ style = Type.searchHint,
+ color = GlassColors.TextTertiary
+ )
+ }
+}
+
+/**
+ * Expanded search with input field and results
+ */
+@OptIn(ExperimentalHazeMaterialsApi::class)
+@Composable
+private fun ExpandedSearchPill(
+ query: String,
+ onQueryChange: (String) -> Unit,
+ searchResults: List,
+ onAppClick: (AppInfo) -> Unit,
+ onWebSearch: () -> Unit,
+ onClose: () -> Unit,
+ focusRequester: FocusRequester,
+ hazeState: HazeState?
+) {
+ val keyboardController = LocalSoftwareKeyboardController.current
+
+ Column(
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ // Search input field
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(28.dp))
+ .then(
+ hazeState?.let {
+ Modifier.hazeChild(it, style = HazeMaterials.thin())
+ } ?: Modifier
+ )
+ .background(GlassColors.TheFrostAlpha)
+ .border(
+ width = 1.5.dp,
+ brush = GlassColors.NeonGradient,
+ shape = RoundedCornerShape(28.dp)
+ )
+ .padding(horizontal = 20.dp, vertical = 14.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = Icons.Default.Search,
+ contentDescription = null,
+ tint = GlassColors.NeonWineStart,
+ modifier = Modifier.size(22.dp)
+ )
+
+ Spacer(modifier = Modifier.width(12.dp))
+
+ BasicTextField(
+ value = query,
+ onValueChange = onQueryChange,
+ modifier = Modifier
+ .weight(1f)
+ .focusRequester(focusRequester),
+ textStyle = Type.bodyLarge.copy(color = GlassColors.TextPrimary),
+ cursorBrush = SolidColor(GlassColors.NeonWineStart),
+ singleLine = true,
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
+ keyboardActions = KeyboardActions(
+ onSearch = {
+ if (query.isNotBlank()) {
+ onWebSearch()
+ }
+ keyboardController?.hide()
+ }
+ ),
+ decorationBox = { innerTextField ->
+ Box {
+ if (query.isEmpty()) {
+ Text(
+ text = "Search apps, contacts, webโฆ",
+ style = Type.searchHint,
+ color = GlassColors.TextTertiary
+ )
+ }
+ innerTextField()
+ }
+ }
+ )
+
+ if (query.isNotEmpty()) {
+ Spacer(modifier = Modifier.width(8.dp))
+
+ // Web search button
+ Box(
+ modifier = Modifier
+ .size(32.dp)
+ .clip(CircleShape)
+ .background(GlassColors.NeonWineStart.copy(alpha = 0.2f))
+ .clickable { onWebSearch() },
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.Default.Language,
+ contentDescription = "Web search",
+ tint = GlassColors.NeonWineStart,
+ modifier = Modifier.size(18.dp)
+ )
+ }
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ // Clear button
+ Box(
+ modifier = Modifier
+ .size(32.dp)
+ .clip(CircleShape)
+ .background(Color.White.copy(alpha = 0.1f))
+ .clickable { onQueryChange("") },
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.Default.Close,
+ contentDescription = "Clear",
+ tint = GlassColors.TextSecondary,
+ modifier = Modifier.size(18.dp)
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ // Close button
+ Box(
+ modifier = Modifier
+ .size(32.dp)
+ .clip(CircleShape)
+ .background(Color.White.copy(alpha = 0.08f))
+ .clickable { onClose() },
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.Default.KeyboardArrowDown,
+ contentDescription = "Close",
+ tint = GlassColors.TextSecondary,
+ modifier = Modifier.size(20.dp)
+ )
+ }
+ }
+
+ // Search results (Apps first, then Contacts as per spec)
+ AnimatedVisibility(
+ visible = query.isNotEmpty() && searchResults.isNotEmpty(),
+ enter = fadeIn() + expandVertically(),
+ exit = fadeOut() + shrinkVertically()
+ ) {
+ SearchResultsList(
+ results = searchResults,
+ onAppClick = onAppClick,
+ hazeState = hazeState,
+ modifier = Modifier.padding(top = 8.dp)
+ )
+ }
+ }
+}
+
+/**
+ * Search results list - Apps first, then Contacts
+ */
+@OptIn(ExperimentalHazeMaterialsApi::class)
+@Composable
+private fun SearchResultsList(
+ results: List,
+ onAppClick: (AppInfo) -> Unit,
+ hazeState: HazeState?,
+ modifier: Modifier = Modifier
+) {
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .heightIn(max = 280.dp)
+ .clip(RoundedCornerShape(20.dp))
+ .then(
+ hazeState?.let {
+ Modifier.hazeChild(it, style = HazeMaterials.thin())
+ } ?: Modifier
+ )
+ .background(GlassColors.TheFrostAlpha)
+ .border(
+ width = 1.dp,
+ brush = GlassColors.NeonBorderGradient,
+ shape = RoundedCornerShape(20.dp)
+ )
+ ) {
+ LazyColumn(
+ modifier = Modifier.padding(8.dp),
+ verticalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ // Apps section header
+ if (results.isNotEmpty()) {
+ item {
+ Text(
+ text = "Apps",
+ style = Type.labelSmall,
+ color = GlassColors.TextTertiary,
+ modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp)
+ )
+ }
+ }
+
+ // App results (show first 5)
+ items(results.take(5)) { app ->
+ SearchResultRow(
+ app = app,
+ onClick = { onAppClick(app) }
+ )
+ }
+
+ // Contacts section would go here
+ // (Placeholder for future contact search integration)
+ }
+ }
+}
+
+/**
+ * Individual search result row
+ */
+@Composable
+private fun SearchResultRow(
+ app: AppInfo,
+ onClick: () -> Unit
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(12.dp))
+ .clickable { onClick() }
+ .padding(horizontal = 12.dp, vertical = 10.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // App icon
+ Box(
+ modifier = Modifier
+ .size(40.dp)
+ .clip(RoundedCornerShape(10.dp))
+ .background(GlassColors.GlassTint),
+ contentAlignment = Alignment.Center
+ ) {
+ app.icon?.let { drawable ->
+ androidx.compose.foundation.Image(
+ painter = com.google.accompanist.drawablepainter.rememberDrawablePainter(drawable),
+ contentDescription = app.label,
+ modifier = Modifier.size(32.dp)
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.width(12.dp))
+
+ // App info
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = app.label,
+ style = Type.titleMedium,
+ color = GlassColors.TextPrimary
+ )
+ Text(
+ text = app.category.displayName,
+ style = Type.labelSmall,
+ color = GlassColors.TextTertiary
+ )
+ }
+
+ // Launch arrow
+ Icon(
+ imageVector = Icons.Default.ChevronRight,
+ contentDescription = null,
+ tint = GlassColors.TextTertiary,
+ modifier = Modifier.size(20.dp)
+ )
+ }
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/drawer/AppDrawer.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/drawer/AppDrawer.kt
new file mode 100644
index 00000000..d8acb53d
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/drawer/AppDrawer.kt
@@ -0,0 +1,332 @@
+package com.blackglass.launcher.ui.drawer
+
+import androidx.compose.animation.*
+import androidx.compose.animation.core.*
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.detectVerticalDragGestures
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.unit.dp
+import com.blackglass.launcher.data.AppCategory
+import com.blackglass.launcher.data.AppInfo
+import com.blackglass.launcher.ui.components.NeonAppIcon
+import com.blackglass.launcher.ui.theme.GlassColors
+import com.blackglass.launcher.ui.theme.Type
+import dev.chrisbanes.haze.HazeState
+import dev.chrisbanes.haze.hazeChild
+import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
+import dev.chrisbanes.haze.materials.HazeMaterials
+
+/**
+ * BlackGlass UI OS - Smart App Drawer
+ *
+ * PHASE 3 SPEC:
+ * - UI: ModalBottomSheet that drags up
+ * - Background: #000000 at 85% opacity with background blur
+ * - Logic: Sort alphabetically by default, group by Category tabs
+ */
+
+/**
+ * SPEC: Background color #000000 at 85% opacity
+ */
+private val DrawerBackgroundColor = Color.Black.copy(alpha = 0.85f)
+
+@OptIn(ExperimentalHazeMaterialsApi::class, ExperimentalMaterial3Api::class)
+@Composable
+fun AppDrawer(
+ apps: List,
+ selectedCategory: AppCategory,
+ onCategorySelected: (AppCategory) -> Unit,
+ onAppClick: (AppInfo) -> Unit,
+ onAppLongClick: (AppInfo) -> Unit,
+ onDismiss: () -> Unit,
+ isVisible: Boolean,
+ hazeState: HazeState,
+ modifier: Modifier = Modifier
+) {
+ // Filter and sort apps based on category
+ val displayedApps = remember(apps, selectedCategory) {
+ val filtered = when (selectedCategory) {
+ AppCategory.ALL -> apps
+ else -> apps.filter { it.category == selectedCategory }
+ }
+ // SPEC: Sort alphabetically by default
+ filtered.sortedBy { it.label.lowercase() }
+ }
+
+ // ModalBottomSheet state
+ val sheetState = rememberModalBottomSheetState(
+ skipPartiallyExpanded = true
+ )
+
+ // Show/hide based on isVisible
+ LaunchedEffect(isVisible) {
+ if (isVisible) {
+ sheetState.show()
+ } else {
+ sheetState.hide()
+ }
+ }
+
+ // Handle dismiss
+ LaunchedEffect(sheetState.isVisible) {
+ if (!sheetState.isVisible && isVisible) {
+ onDismiss()
+ }
+ }
+
+ if (isVisible) {
+ ModalBottomSheet(
+ onDismissRequest = onDismiss,
+ sheetState = sheetState,
+ containerColor = Color.Transparent,
+ dragHandle = null,
+ modifier = modifier
+ ) {
+ // SPEC: Background #000000 at 85% opacity + blur
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .hazeChild(state = hazeState, style = HazeMaterials.ultraThin())
+ .background(DrawerBackgroundColor)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .statusBarsPadding()
+ ) {
+ // Drag handle
+ DragHandle()
+
+ // Category tabs
+ CategoryTabs(
+ selectedCategory = selectedCategory,
+ onCategorySelected = onCategorySelected,
+ modifier = Modifier.padding(vertical = 8.dp)
+ )
+
+ // App count indicator
+ Text(
+ text = "${displayedApps.size} apps",
+ style = Type.labelSmall,
+ color = GlassColors.TextTertiary,
+ modifier = Modifier.padding(horizontal = 20.dp, vertical = 4.dp)
+ )
+
+ // App grid
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(4),
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 16.dp),
+ contentPadding = PaddingValues(vertical = 16.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ items(
+ items = displayedApps,
+ key = { it.packageName }
+ ) { app ->
+ NeonAppIcon(
+ icon = app.icon,
+ label = app.label,
+ glowColor = getCategoryGlowColor(app.category),
+ onClick = { onAppClick(app) },
+ onLongClick = { onAppLongClick(app) },
+ modifier = Modifier.animateItem()
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Alternative: Animated slide-up drawer (non-ModalBottomSheet version)
+ */
+@OptIn(ExperimentalHazeMaterialsApi::class)
+@Composable
+fun SlideUpAppDrawer(
+ apps: List,
+ selectedCategory: AppCategory,
+ onCategorySelected: (AppCategory) -> Unit,
+ onAppClick: (AppInfo) -> Unit,
+ onAppLongClick: (AppInfo) -> Unit,
+ onDismiss: () -> Unit,
+ isVisible: Boolean,
+ hazeState: HazeState,
+ modifier: Modifier = Modifier
+) {
+ var dragOffset by remember { mutableFloatStateOf(0f) }
+
+ // Filter and sort
+ val displayedApps = remember(apps, selectedCategory) {
+ val filtered = when (selectedCategory) {
+ AppCategory.ALL -> apps
+ else -> apps.filter { it.category == selectedCategory }
+ }
+ filtered.sortedBy { it.label.lowercase() }
+ }
+
+ AnimatedVisibility(
+ visible = isVisible,
+ enter = slideInVertically(
+ initialOffsetY = { it },
+ animationSpec = spring(
+ dampingRatio = Spring.DampingRatioLowBouncy,
+ stiffness = Spring.StiffnessLow
+ )
+ ) + fadeIn(),
+ exit = slideOutVertically(
+ targetOffsetY = { it },
+ animationSpec = tween(300)
+ ) + fadeOut()
+ ) {
+ Box(
+ modifier = modifier
+ .fillMaxSize()
+ .graphicsLayer {
+ translationY = dragOffset.coerceAtLeast(0f)
+ }
+ .pointerInput(Unit) {
+ detectVerticalDragGestures(
+ onDragEnd = {
+ if (dragOffset > 200) onDismiss()
+ dragOffset = 0f
+ },
+ onDragCancel = { dragOffset = 0f },
+ onVerticalDrag = { _, dragAmount ->
+ dragOffset += dragAmount
+ }
+ )
+ }
+ // SPEC: Background blur + #000000 at 85% opacity
+ .hazeChild(state = hazeState, style = HazeMaterials.ultraThin())
+ .background(DrawerBackgroundColor)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .statusBarsPadding()
+ ) {
+ // Drag handle
+ DragHandle()
+
+ // Category tabs
+ CategoryTabs(
+ selectedCategory = selectedCategory,
+ onCategorySelected = onCategorySelected,
+ modifier = Modifier.padding(vertical = 8.dp)
+ )
+
+ // App count indicator
+ Text(
+ text = "${apps.size} apps",
+ style = Type.labelSmall,
+ color = GlassColors.TextTertiary,
+ modifier = Modifier.padding(horizontal = 20.dp, vertical = 4.dp)
+ )
+
+ // App grid
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(4),
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 16.dp),
+ contentPadding = PaddingValues(vertical = 16.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ items(
+ items = apps,
+ key = { it.packageName }
+ ) { app ->
+ NeonAppIcon(
+ icon = app.icon,
+ label = app.label,
+ glowColor = getCategoryGlowColor(app.category),
+ onClick = { onAppClick(app) },
+ onLongClick = { onAppLongClick(app) },
+ modifier = Modifier.animateItem()
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun DragHandle() {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 12.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Box(
+ modifier = Modifier
+ .width(40.dp)
+ .height(4.dp)
+ .clip(RoundedCornerShape(2.dp))
+ .background(GlassColors.TextTertiary)
+ )
+ }
+}
+
+/**
+ * Get glow color based on category
+ */
+private fun getCategoryGlowColor(category: AppCategory): androidx.compose.ui.graphics.Color {
+ return when (category) {
+ AppCategory.SOCIAL -> GlassColors.CategorySocial
+ AppCategory.MEDIA -> GlassColors.CategoryMedia
+ AppCategory.GAMES -> GlassColors.CategoryGames
+ AppCategory.PRODUCTIVITY -> GlassColors.CategoryProductivity
+ else -> GlassColors.NeonWineGlow
+ }
+}
+
+/**
+ * Empty drawer state
+ */
+@Composable
+fun EmptyDrawerState(
+ message: String,
+ modifier: Modifier = Modifier
+) {
+ Box(
+ modifier = modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = "๐ฑ",
+ style = Type.displayLarge
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Text(
+ text = message,
+ style = Type.bodyLarge,
+ color = GlassColors.TextSecondary
+ )
+ }
+ }
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/drawer/CategoryTabs.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/drawer/CategoryTabs.kt
new file mode 100644
index 00000000..2248420f
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/drawer/CategoryTabs.kt
@@ -0,0 +1,220 @@
+package com.blackglass.launcher.ui.drawer
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import com.blackglass.launcher.data.AppCategory
+import com.blackglass.launcher.ui.theme.GlassColors
+import com.blackglass.launcher.ui.theme.Type
+
+/**
+ * BlackGlass UI OS - Category Tabs
+ * Horizontal scrolling category filter for app drawer
+ */
+
+@Composable
+fun CategoryTabs(
+ selectedCategory: AppCategory,
+ onCategorySelected: (AppCategory) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val categories = AppCategory.entries
+
+ LazyRow(
+ modifier = modifier.fillMaxWidth(),
+ contentPadding = PaddingValues(horizontal = 16.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ items(categories) { category ->
+ CategoryTab(
+ category = category,
+ isSelected = category == selectedCategory,
+ onClick = { onCategorySelected(category) }
+ )
+ }
+ }
+}
+
+@Composable
+private fun CategoryTab(
+ category: AppCategory,
+ isSelected: Boolean,
+ onClick: () -> Unit
+) {
+ val categoryColor = getCategoryColor(category)
+
+ val backgroundColor by animateColorAsState(
+ targetValue = if (isSelected) {
+ categoryColor.copy(alpha = 0.25f)
+ } else {
+ Color.White.copy(alpha = 0.08f)
+ },
+ animationSpec = tween(200),
+ label = "bg"
+ )
+
+ val borderColor by animateColorAsState(
+ targetValue = if (isSelected) {
+ categoryColor.copy(alpha = 0.6f)
+ } else {
+ Color.White.copy(alpha = 0.15f)
+ },
+ animationSpec = tween(200),
+ label = "border"
+ )
+
+ val textColor by animateColorAsState(
+ targetValue = if (isSelected) {
+ categoryColor
+ } else {
+ GlassColors.TextSecondary
+ },
+ animationSpec = tween(200),
+ label = "text"
+ )
+
+ Row(
+ modifier = Modifier
+ .clip(RoundedCornerShape(20.dp))
+ .background(backgroundColor)
+ .border(
+ width = 1.dp,
+ color = borderColor,
+ shape = RoundedCornerShape(20.dp)
+ )
+ .clickable { onClick() }
+ .padding(horizontal = 14.dp, vertical = 8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(6.dp)
+ ) {
+ // Category emoji
+ Text(
+ text = category.emoji,
+ style = Type.labelMedium
+ )
+
+ // Category name
+ Text(
+ text = category.displayName,
+ style = Type.categoryTab,
+ color = textColor
+ )
+ }
+}
+
+/**
+ * Get color for category
+ */
+private fun getCategoryColor(category: AppCategory): Color {
+ return when (category) {
+ AppCategory.ALL -> GlassColors.TextPrimary
+ AppCategory.SOCIAL -> GlassColors.CategorySocial
+ AppCategory.MEDIA -> GlassColors.CategoryMedia
+ AppCategory.GAMES -> GlassColors.CategoryGames
+ AppCategory.PRODUCTIVITY -> GlassColors.CategoryProductivity
+ AppCategory.UTILITIES -> GlassColors.CategoryUtilities
+ }
+}
+
+/**
+ * Compact category chips (alternative style)
+ */
+@Composable
+fun CompactCategoryTabs(
+ selectedCategory: AppCategory,
+ onCategorySelected: (AppCategory) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ horizontalArrangement = Arrangement.SpaceEvenly
+ ) {
+ AppCategory.entries.forEach { category ->
+ CompactCategoryChip(
+ category = category,
+ isSelected = category == selectedCategory,
+ onClick = { onCategorySelected(category) }
+ )
+ }
+ }
+}
+
+@Composable
+private fun CompactCategoryChip(
+ category: AppCategory,
+ isSelected: Boolean,
+ onClick: () -> Unit
+) {
+ val categoryColor = getCategoryColor(category)
+
+ val backgroundColor by animateColorAsState(
+ targetValue = if (isSelected) {
+ categoryColor.copy(alpha = 0.2f)
+ } else {
+ Color.Transparent
+ },
+ animationSpec = tween(200),
+ label = "bg"
+ )
+
+ Box(
+ modifier = Modifier
+ .clip(RoundedCornerShape(12.dp))
+ .background(backgroundColor)
+ .clickable { onClick() }
+ .padding(horizontal = 10.dp, vertical = 6.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = category.emoji,
+ style = Type.titleMedium
+ )
+ }
+}
+
+/**
+ * Category indicator dots (minimal style)
+ */
+@Composable
+fun CategoryIndicators(
+ selectedCategory: AppCategory,
+ onCategorySelected: (AppCategory) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Row(
+ modifier = modifier,
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ AppCategory.entries.forEach { category ->
+ val isSelected = category == selectedCategory
+ val color = getCategoryColor(category)
+
+ Box(
+ modifier = Modifier
+ .size(if (isSelected) 10.dp else 6.dp)
+ .clip(RoundedCornerShape(50))
+ .background(
+ if (isSelected) color else color.copy(alpha = 0.3f)
+ )
+ .clickable { onCategorySelected(category) }
+ )
+ }
+ }
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/home/HomeScreen.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/home/HomeScreen.kt
new file mode 100644
index 00000000..fd44b8b9
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/home/HomeScreen.kt
@@ -0,0 +1,256 @@
+package com.blackglass.launcher.ui.home
+
+import android.app.WallpaperManager
+import android.content.Intent
+import android.net.Uri
+import androidx.compose.animation.*
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.detectVerticalDragGestures
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.core.graphics.drawable.toBitmap
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.blackglass.launcher.data.model.AppInfo
+import com.blackglass.launcher.ui.components.*
+import com.blackglass.launcher.ui.drawer.AppDrawer
+import com.blackglass.launcher.ui.theme.GlassColors
+import com.blackglass.launcher.ui.widgets.ClockWidget
+import com.blackglass.launcher.viewmodel.LauncherViewModel
+import dev.chrisbanes.haze.HazeState
+import dev.chrisbanes.haze.haze
+
+/**
+ * BlackGlass UI OS - Home Screen
+ *
+ * PHASE 3 Layout:
+ * - Clock widget at top
+ * - Widget grid in middle
+ * - Universal Search Pill floating 16dp above bottom
+ * - Neon Dock with 5 fixed icons at bottom
+ * - Smart App Drawer (ModalBottomSheet)
+ */
+
+@Composable
+fun HomeScreen(
+ modifier: Modifier = Modifier,
+ viewModel: LauncherViewModel = viewModel()
+) {
+ val context = LocalContext.current
+
+ // Collect state
+ val apps by viewModel.apps.collectAsState()
+ val filteredApps by viewModel.filteredApps.collectAsState()
+ val isLoading by viewModel.isLoading.collectAsState()
+ val selectedCategory by viewModel.selectedCategory.collectAsState()
+ val searchQuery by viewModel.searchQuery.collectAsState()
+ val searchResults by viewModel.searchResults.collectAsState()
+ val isDrawerOpen by viewModel.isDrawerOpen.collectAsState()
+ val isSearchActive by viewModel.isSearchActive.collectAsState()
+
+ // Haze state for blur effects
+ val hazeState = remember { HazeState() }
+
+ // Get wallpaper
+ val wallpaperManager = remember { WallpaperManager.getInstance(context) }
+ val wallpaperDrawable = remember {
+ try { wallpaperManager.drawable } catch (e: Exception) { null }
+ }
+
+ // Drag gesture state for opening drawer
+ var dragOffset by remember { mutableFloatStateOf(0f) }
+
+ Box(
+ modifier = modifier
+ .fillMaxSize()
+ // Layer 1 (The Void) background
+ .background(GlassColors.TheVoid)
+ ) {
+ // Wallpaper background with blur capability
+ wallpaperDrawable?.let { drawable ->
+ Image(
+ bitmap = drawable.toBitmap().asImageBitmap(),
+ contentDescription = null,
+ modifier = Modifier
+ .fillMaxSize()
+ .haze(hazeState),
+ contentScale = ContentScale.Crop
+ )
+ }
+
+ // Dark gradient overlay for readability
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ GlassColors.TheVoid.copy(alpha = 0.3f),
+ GlassColors.TheVoid.copy(alpha = 0.1f),
+ GlassColors.TheVoid.copy(alpha = 0.3f),
+ GlassColors.TheVoid.copy(alpha = 0.6f)
+ )
+ )
+ )
+ )
+
+ // Main content
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .systemBarsPadding()
+ .pointerInput(Unit) {
+ detectVerticalDragGestures(
+ onDragEnd = {
+ // Swipe up to open drawer
+ if (dragOffset < -100) {
+ viewModel.openDrawer()
+ }
+ dragOffset = 0f
+ },
+ onDragCancel = { dragOffset = 0f },
+ onVerticalDrag = { _, dragAmount ->
+ dragOffset += dragAmount
+ }
+ )
+ }
+ ) {
+ // Clock widget at top
+ ClockWidget(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 48.dp)
+ )
+
+ // Widget grid area (flexible space)
+ WidgetGrid(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ .padding(horizontal = 16.dp)
+ )
+
+ // Search results overlay (animated)
+ AnimatedVisibility(
+ visible = searchQuery.isNotEmpty() && searchResults.isNotEmpty(),
+ enter = fadeIn() + slideInVertically { -it / 2 },
+ exit = fadeOut() + slideOutVertically { -it / 2 }
+ ) {
+ SearchResultsOverlay(
+ results = searchResults,
+ onAppClick = { app ->
+ viewModel.launchApp(app.packageName)
+ viewModel.toggleSearch()
+ }
+ )
+ }
+
+ // PHASE 3: Universal Search Pill - floating 16dp above Neon Dock
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 32.dp)
+ .padding(bottom = 16.dp), // 16dp above dock per spec
+ contentAlignment = Alignment.Center
+ ) {
+ UniversalSearchPill(
+ query = searchQuery,
+ onQueryChange = { viewModel.updateSearchQuery(it) },
+ onSearch = { query ->
+ // Search apps
+ },
+ onWebSearch = { query ->
+ val searchIntent = Intent(Intent.ACTION_VIEW).apply {
+ data = Uri.parse("https://www.google.com/search?q=$query")
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ }
+ context.startActivity(searchIntent)
+ },
+ isExpanded = isSearchActive,
+ onExpandedChange = { viewModel.toggleSearch() }
+ )
+ }
+
+ // PHASE 3: Neon Dock - 5 fixed icons with glow (self-contained handlers)
+ NeonDock(
+ hazeState = hazeState,
+ modifier = Modifier.padding(bottom = 8.dp)
+ )
+ }
+
+ // Loading indicator
+ if (isLoading) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ CircularProgressIndicator(
+ color = GlassColors.NeonWineGlow,
+ strokeWidth = 2.dp
+ )
+ }
+ }
+
+ // PHASE 3: Smart App Drawer (ModalBottomSheet style)
+ AppDrawer(
+ apps = if (searchQuery.isEmpty()) filteredApps else searchResults,
+ selectedCategory = selectedCategory,
+ onCategorySelected = { viewModel.selectCategory(it) },
+ onAppClick = { app ->
+ viewModel.launchApp(app.packageName)
+ viewModel.closeDrawer()
+ },
+ onAppLongClick = { },
+ onDismiss = { viewModel.closeDrawer() },
+ isVisible = isDrawerOpen,
+ hazeState = hazeState
+ )
+ }
+}
+
+@Composable
+private fun SearchResultsOverlay(
+ results: List,
+ onAppClick: (AppInfo) -> Unit
+) {
+ GlassCard(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp)
+ .heightIn(max = 300.dp),
+ cornerRadius = 20.dp,
+ glassAlpha = 0.4f // Frost @ 40%
+ ) {
+ Column(
+ modifier = Modifier.padding(8.dp)
+ ) {
+ results.take(5).forEach { app ->
+ SearchResultItem(
+ icon = {
+ app.icon?.let {
+ androidx.compose.foundation.Image(
+ painter = com.google.accompanist.drawablepainter.rememberDrawablePainter(
+ drawable = it
+ ),
+ contentDescription = app.label,
+ modifier = Modifier.size(32.dp)
+ )
+ }
+ },
+ title = app.label,
+ subtitle = app.category.displayName,
+ onClick = { onAppClick(app) }
+ )
+ }
+ }
+ }
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/home/WidgetGrid.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/home/WidgetGrid.kt
new file mode 100644
index 00000000..c50ffc11
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/home/WidgetGrid.kt
@@ -0,0 +1,127 @@
+package com.blackglass.launcher.ui.home
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.blackglass.launcher.ui.widgets.MusicGlassWidget
+import com.blackglass.launcher.ui.widgets.QuickControlWidget
+import com.blackglass.launcher.ui.widgets.WeatherGlassWidget
+
+/**
+ * BlackGlass UI OS - Widget Grid
+ * Displays widgets on the home screen in a flexible grid
+ */
+
+@Composable
+fun WidgetGrid(
+ modifier: Modifier = Modifier
+) {
+ var isEditMode by remember { mutableStateOf(false) }
+
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .padding(horizontal = 8.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // Weather widget
+ WeatherGlassWidget(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp)
+ )
+
+ // Music widget
+ MusicGlassWidget(
+ isPlaying = false,
+ songTitle = "Not Playing",
+ artistName = "No Artist",
+ onPlayPause = { },
+ onNext = { },
+ onPrevious = { },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp)
+ )
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ // Quick controls at bottom of widget area
+ QuickControlWidget(
+ onWifiClick = { },
+ onBluetoothClick = { },
+ onFlashlightClick = { },
+ onSettingsClick = { },
+ modifier = Modifier.padding(horizontal = 8.dp)
+ )
+ }
+}
+
+/**
+ * Widget placeholder for empty slots
+ */
+@Composable
+fun WidgetPlaceholder(
+ modifier: Modifier = Modifier,
+ onClick: () -> Unit = {}
+) {
+ com.blackglass.launcher.ui.components.GlassCard(
+ modifier = modifier
+ .height(100.dp),
+ style = com.blackglass.launcher.ui.components.GlassCardStyle.SUBTLE,
+ onClick = onClick
+ ) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ androidx.compose.material3.Text(
+ text = "Tap to add widget",
+ style = com.blackglass.launcher.ui.theme.Type.bodyMedium,
+ color = com.blackglass.launcher.ui.theme.GlassColors.TextTertiary
+ )
+ }
+ }
+}
+
+/**
+ * Editable widget wrapper with drag handles
+ */
+@Composable
+fun EditableWidget(
+ isEditing: Boolean,
+ onRemove: () -> Unit,
+ onResize: () -> Unit,
+ modifier: Modifier = Modifier,
+ content: @Composable () -> Unit
+) {
+ Box(modifier = modifier) {
+ content()
+
+ if (isEditing) {
+ // Edit mode overlay with remove button
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(4.dp)
+ ) {
+ // Remove button in top-right corner
+ androidx.compose.material3.IconButton(
+ onClick = onRemove,
+ modifier = Modifier.align(Alignment.TopEnd)
+ ) {
+ androidx.compose.material3.Icon(
+ imageVector = androidx.compose.material.icons.Icons.Default.Close,
+ contentDescription = "Remove",
+ tint = com.blackglass.launcher.ui.theme.GlassColors.NeonWineGlow
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/screens/HomeScreen.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/screens/HomeScreen.kt
new file mode 100644
index 00000000..c14cdd20
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/screens/HomeScreen.kt
@@ -0,0 +1,258 @@
+package com.blackglass.launcher.ui.screens
+
+import android.app.WallpaperManager
+import android.content.Intent
+import android.net.Uri
+import androidx.compose.animation.*
+import androidx.compose.animation.core.*
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.detectVerticalDragGestures
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.blur
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.core.graphics.drawable.toBitmap
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.blackglass.launcher.data.model.AppCategory
+import com.blackglass.launcher.data.model.AppInfo
+import com.blackglass.launcher.ui.components.*
+import com.blackglass.launcher.ui.theme.BlackGlassColors
+import com.blackglass.launcher.viewmodel.LauncherViewModel
+import dev.chrisbanes.haze.HazeState
+import dev.chrisbanes.haze.haze
+
+/**
+ * BlackGlass UI OS - Home Screen
+ * Main launcher home screen with glass blur background
+ */
+
+@Composable
+fun HomeScreen(
+ modifier: Modifier = Modifier,
+ viewModel: LauncherViewModel = viewModel()
+) {
+ val context = LocalContext.current
+
+ // State
+ val apps by viewModel.apps.collectAsState()
+ val filteredApps by viewModel.filteredApps.collectAsState()
+ val isLoading by viewModel.isLoading.collectAsState()
+ val selectedCategory by viewModel.selectedCategory.collectAsState()
+ val searchQuery by viewModel.searchQuery.collectAsState()
+ val searchResults by viewModel.searchResults.collectAsState()
+ val dockApps by viewModel.dockApps.collectAsState()
+ val isDrawerOpen by viewModel.isDrawerOpen.collectAsState()
+ val isSearchActive by viewModel.isSearchActive.collectAsState()
+
+ // Haze state for blur effect
+ val hazeState = remember { HazeState() }
+
+ // Wallpaper
+ val wallpaperManager = remember { WallpaperManager.getInstance(context) }
+ val wallpaperDrawable = remember {
+ try {
+ wallpaperManager.drawable
+ } catch (e: Exception) {
+ null
+ }
+ }
+
+ // Drag state for opening drawer
+ var dragOffset by remember { mutableFloatStateOf(0f) }
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(BlackGlassColors.BackgroundDark)
+ ) {
+ // Wallpaper background with blur capability
+ wallpaperDrawable?.let { drawable ->
+ Image(
+ bitmap = drawable.toBitmap().asImageBitmap(),
+ contentDescription = null,
+ modifier = Modifier
+ .fillMaxSize()
+ .haze(hazeState),
+ contentScale = ContentScale.Crop
+ )
+ }
+
+ // Dark gradient overlay for better readability
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ BlackGlassColors.BackgroundDark.copy(alpha = 0.3f),
+ BlackGlassColors.BackgroundDark.copy(alpha = 0.1f),
+ BlackGlassColors.BackgroundDark.copy(alpha = 0.3f),
+ BlackGlassColors.BackgroundDark.copy(alpha = 0.6f)
+ )
+ )
+ )
+ )
+
+ // Main content
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .systemBarsPadding()
+ .pointerInput(Unit) {
+ detectVerticalDragGestures(
+ onDragEnd = {
+ if (dragOffset < -100) {
+ viewModel.openDrawer()
+ }
+ dragOffset = 0f
+ },
+ onDragCancel = {
+ dragOffset = 0f
+ },
+ onVerticalDrag = { _, dragAmount ->
+ dragOffset += dragAmount
+ }
+ )
+ }
+ ) {
+ // Clock widget at top
+ ClockWidget(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 48.dp)
+ )
+
+ // Spacer to push dock to bottom
+ Spacer(modifier = Modifier.weight(1f))
+
+ // Universal Search pill
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 32.dp, vertical = 16.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ UniversalSearchBar(
+ query = searchQuery,
+ onQueryChange = { viewModel.updateSearchQuery(it) },
+ onSearch = { },
+ onWebSearch = { query ->
+ val searchIntent = Intent(Intent.ACTION_VIEW).apply {
+ data = Uri.parse("https://www.google.com/search?q=$query")
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ }
+ context.startActivity(searchIntent)
+ },
+ isExpanded = isSearchActive,
+ onExpandedChange = { viewModel.toggleSearch() }
+ )
+ }
+
+ // Search results overlay
+ AnimatedVisibility(
+ visible = searchQuery.isNotEmpty() && searchResults.isNotEmpty(),
+ enter = fadeIn() + slideInVertically { -it / 2 },
+ exit = fadeOut() + slideOutVertically { -it / 2 }
+ ) {
+ SearchResultsOverlay(
+ results = searchResults,
+ onAppClick = { app ->
+ viewModel.launchApp(app.packageName)
+ viewModel.toggleSearch()
+ }
+ )
+ }
+
+ // Neon Dock at bottom
+ NeonDock(
+ dockApps = dockApps,
+ onAppClick = { app ->
+ viewModel.launchApp(app.packageName)
+ },
+ onAppLongClick = { position ->
+ viewModel.removeFromDock(position)
+ },
+ onAddClick = { position ->
+ // Open app drawer to select app for dock
+ viewModel.openDrawer()
+ },
+ modifier = Modifier.padding(bottom = 8.dp)
+ )
+ }
+
+ // Loading indicator
+ if (isLoading) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ CircularProgressIndicator(
+ color = BlackGlassColors.NeonWineGlow,
+ strokeWidth = 2.dp
+ )
+ }
+ }
+
+ // App drawer overlay
+ AppDrawer(
+ apps = if (searchQuery.isEmpty()) filteredApps else searchResults,
+ selectedCategory = selectedCategory,
+ onCategorySelected = { viewModel.selectCategory(it) },
+ onAppClick = { app ->
+ viewModel.launchApp(app.packageName)
+ viewModel.closeDrawer()
+ },
+ onAppLongClick = { app ->
+ // Could show app options menu
+ },
+ onDismiss = { viewModel.closeDrawer() },
+ isVisible = isDrawerOpen,
+ hazeState = hazeState
+ )
+ }
+}
+
+@Composable
+private fun SearchResultsOverlay(
+ results: List,
+ onAppClick: (AppInfo) -> Unit
+) {
+ GlassSurface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp)
+ .heightIn(max = 300.dp),
+ cornerRadius = 20.dp,
+ glassAlpha = 0.4f
+ ) {
+ Column(
+ modifier = Modifier.padding(8.dp)
+ ) {
+ results.take(5).forEach { app ->
+ SearchResultItem(
+ icon = {
+ androidx.compose.foundation.Image(
+ painter = com.google.accompanist.drawablepainter.rememberDrawablePainter(
+ drawable = app.icon
+ ),
+ contentDescription = app.label,
+ modifier = Modifier.size(32.dp)
+ )
+ },
+ title = app.label,
+ subtitle = app.category.displayName,
+ onClick = { onAppClick(app) }
+ )
+ }
+ }
+ }
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/theme/Color.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/theme/Color.kt
new file mode 100644
index 00000000..afa385f3
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/theme/Color.kt
@@ -0,0 +1,192 @@
+package com.blackglass.launcher.ui.theme
+
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+
+/**
+ * BlackGlass UI OS - Color System
+ * The TRI-LAYER Glass Engine Color Palette
+ *
+ * โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ * โ THE TRI-LAYER GLASS ENGINE โ
+ * โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
+ * โ Layer 1 (The Void) โ #050505 Pure black + 2% noise texture โ
+ * โ Layer 2 (The Frost) โ #1E1E24 @ 40% Alpha + Blur(30dp) โ
+ * โ Layer 3 (The Neon) โ Gradient #7B0323 โ #2B0033 โ
+ * โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ */
+object GlassColors {
+
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // TRI-LAYER GLASS ENGINE
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+ /**
+ * LAYER 1 - THE VOID
+ * Pure black background with 2% noise texture to prevent banding.
+ * Used for: Main backgrounds, base layer
+ */
+ val TheVoid = Color(0xFF050505)
+ val VoidNoise = Color(0xFF0A0A0A) // Subtle noise variation
+
+ /**
+ * LAYER 2 - THE FROST
+ * #1E1E24 @ 40% Alpha + Blur(30dp)
+ * Used for: Widget backgrounds, cards, containers
+ */
+ val TheFrost = Color(0xFF1E1E24)
+ val TheFrostAlpha = Color(0x661E1E24) // 40% alpha
+ const val FrostBlurRadius = 30 // dp
+
+ /**
+ * LAYER 3 - THE NEON
+ * Linear Gradient: Wine Red (#7B0323) โ Deep Violet (#2B0033)
+ * Used for: Active states, borders, progress bars
+ */
+ val NeonWineStart = Color(0xFF7B0323) // Wine Red - gradient start
+ val NeonVioletEnd = Color(0xFF2B0033) // Deep Violet - gradient end
+
+ /** Pre-built Neon gradient brush for borders and active states */
+ val NeonGradient: Brush
+ get() = Brush.linearGradient(
+ colors = listOf(NeonWineStart, NeonVioletEnd)
+ )
+
+ /** Border gradient with 30% alpha */
+ val NeonBorderGradient: Brush
+ get() = Brush.linearGradient(
+ colors = listOf(
+ NeonWineStart.copy(alpha = 0.3f),
+ NeonVioletEnd.copy(alpha = 0.3f)
+ )
+ )
+
+ /** Glass tint overlay - 5% white */
+ val GlassTint = Color.White.copy(alpha = 0.05f)
+
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // LEGACY 3-SHADE ALIASES
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+ /** Shade 1 - Maps to The Void */
+ val Shade1 = TheVoid
+ val Shade1Variant = Color(0xFF0D0D14)
+ val Shade1Surface = Color(0xFF0F0F1A)
+
+ /** Shade 2 - Maps to The Frost */
+ val Shade2 = TheFrost
+ val Shade2Variant = Color(0xFF16213E)
+ val Shade2Surface = Color(0xFF1E1E35)
+
+ /** Shade 3 - Borders, highlights */
+ val Shade3 = Color(0xFF2D2D4A)
+ val Shade3Variant = Color(0xFF3A3A5C)
+ val Shade3Surface = Color(0xFF4A4A6A)
+
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // PRIMARY: Neon Wine Red
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ val NeonWineRed = Color(0xFFDC143C)
+ val NeonWine = Color(0xFFDC143C)
+ val NeonWineBright = Color(0xFFDC143C)
+ val NeonWineLight = Color(0xFFFF4D6D)
+ val NeonWineGlow = Color(0xFFFF4D6D)
+ val NeonWineDeep = Color(0xFF8B0000)
+ val NeonWineDark = Color(0xFF8B0000)
+
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // SECONDARY: Dark Violet
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ val DarkViolet = Color(0xFF16213E)
+ val DarkVioletBright = Color(0xFF9B59B6)
+ val DarkVioletGlow = Color(0xFFDA70D6)
+ val DarkVioletDeep = Color(0xFF4B0082)
+
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // GLASS SURFACES (Tri-Layer System)
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+ /** Layer 1 - The Void layer (background) */
+ val GlassLayer1 = TheVoid
+
+ /** Layer 2 - The Frost layer (widgets @ 40% alpha) */
+ val GlassLayer2 = TheFrostAlpha
+
+ /** Layer 3 - The Neon layer (use NeonGradient brush) */
+ val GlassLayer3 = NeonWineStart // Solid fallback
+
+ // Surface variants for different depths
+ val GlassSurfaceDark = TheVoid
+ val GlassSurfaceMedium = TheFrostAlpha
+ val GlassSurfaceLight = Color(0x4D1E1E24)
+
+ // Border system - prefer NeonBorderGradient for active states
+ val GlassBorder = Color(0x33FFFFFF)
+ val GlassBorderBright = Color(0x66FFFFFF)
+ val GlassHighlight = Color(0x1AFFFFFF)
+
+ // Neon glow colors for effects
+ val NeonVioletGlow = Color(0xFF9B59B6)
+ val NeonCyanGlow = Color(0xFF00D9FF)
+
+ // Theme compatibility aliases
+ val NeonViolet = DarkVioletBright
+ val NeonVioletLight = DarkVioletGlow
+ val NeonCyan = AccentCyan
+ val NeonCyanLight = Color(0xFF80ECFF)
+ val Error = Color(0xFFCF6679)
+ val GlassBorderLight = Color(0x1AFFFFFF)
+
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // ACCENT COLORS
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ val AccentCyan = Color(0xFF00D9FF)
+ val AccentGold = Color(0xFFFFD700)
+ val AccentEmerald = Color(0xFF00FF7F)
+ val AccentPink = Color(0xFFFF69B4)
+
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // NEON GLOW GRADIENTS
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ val NeonGlowStart = Color(0xFFDC143C)
+ val NeonGlowMiddle = Color(0xFF9B59B6)
+ val NeonGlowEnd = Color(0xFF00D9FF)
+
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // TEXT COLORS
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ val TextPrimary = Color(0xFFFFFFFF)
+ val TextSecondary = Color(0xB3FFFFFF) // 70% white
+ val TextTertiary = Color(0x66FFFFFF) // 40% white
+ val TextDisabled = Color(0x33FFFFFF) // 20% white
+
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // DOCK COLORS
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ val DockBackground = Color(0x40000000)
+ val DockBorder = Color(0x33FFFFFF)
+ val DockIconBackground = Color(0x26FFFFFF)
+ val DockGlowActive = NeonWineGlow
+
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // CATEGORY COLORS (Smart Sorting)
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ val CategorySocial = Color(0xFF1DA1F2) // Twitter Blue
+ val CategoryMedia = Color(0xFFE91E63) // Pink
+ val CategoryGames = Color(0xFF4CAF50) // Green
+ val CategoryProductivity = Color(0xFF2196F3) // Blue
+ val CategoryUtilities = Color(0xFF9E9E9E) // Gray
+
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // SYSTEM COLORS
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ val Transparent = Color(0x00000000)
+ val Scrim = Color(0x99000000)
+ val Black = Color(0xFF000000)
+ val White = Color(0xFFFFFFFF)
+
+ // Background gradient colors
+ val BackgroundDark = Color(0xFF0A0A0F)
+ val BackgroundMedium = Color(0xFF0F0F1A)
+ val BackgroundLight = Color(0xFF151520)
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/theme/GlassTheme.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/theme/GlassTheme.kt
new file mode 100644
index 00000000..cbcfc575
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/theme/GlassTheme.kt
@@ -0,0 +1,206 @@
+package com.blackglass.launcher.ui.theme
+
+import android.app.Activity
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.Typography
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.WindowCompat
+import com.google.accompanist.systemuicontroller.rememberSystemUiController
+
+/**
+ * BlackGlass UI OS - Glass Theme
+ * Deep Glass aesthetic with Material 3 integration
+ *
+ * Implements the TRI-LAYER GLASS ENGINE:
+ * - Layer 1 (The Void): #050505 - Background
+ * - Layer 2 (The Frost): #1E1E24 @ 40% + Blur(30dp) - Surfaces
+ * - Layer 3 (The Neon): #7B0323 โ #2B0033 - Accents/Borders
+ */
+
+/**
+ * BlackGlass Dark Color Scheme using Tri-Layer Glass Engine
+ */
+private val BlackGlassColorScheme = darkColorScheme(
+ // Primary (Neon Layer - Wine Red start)
+ primary = GlassColors.NeonWineStart,
+ onPrimary = GlassColors.White,
+ primaryContainer = GlassColors.TheFrost,
+ onPrimaryContainer = GlassColors.NeonWineLight,
+
+ // Secondary (Neon Layer - Violet end)
+ secondary = GlassColors.NeonVioletEnd,
+ onSecondary = GlassColors.White,
+ secondaryContainer = GlassColors.Shade2Variant,
+ onSecondaryContainer = GlassColors.NeonVioletLight,
+
+ // Tertiary (Accent Cyan)
+ tertiary = GlassColors.NeonCyan,
+ onTertiary = GlassColors.Black,
+ tertiaryContainer = GlassColors.TheFrost,
+ onTertiaryContainer = GlassColors.NeonCyanLight,
+
+ // Error
+ error = GlassColors.Error,
+ onError = GlassColors.White,
+ errorContainer = GlassColors.NeonWineDark,
+ onErrorContainer = GlassColors.NeonWineLight,
+
+ // Background (Layer 1 - The Void)
+ background = GlassColors.TheVoid,
+ onBackground = GlassColors.TextPrimary,
+
+ // Surface (Layer 2 - The Frost)
+ surface = GlassColors.TheFrost,
+ onSurface = GlassColors.TextPrimary,
+ surfaceVariant = GlassColors.Shade3,
+ onSurfaceVariant = GlassColors.TextSecondary,
+ surfaceTint = GlassColors.NeonWineStart,
+
+ // Outline (Neon gradient borders)
+ outline = GlassColors.GlassBorder,
+ outlineVariant = GlassColors.GlassBorderLight,
+
+ // Inverse
+ inverseSurface = GlassColors.White,
+ inverseOnSurface = GlassColors.Black,
+ inversePrimary = GlassColors.NeonWineDark,
+
+ // Scrim
+ scrim = GlassColors.Scrim
+)
+
+/**
+ * BlackGlass Material 3 Typography
+ */
+private val BlackGlassTypography = Typography(
+ displayLarge = Type.displayLarge,
+ displayMedium = Type.displayMedium,
+ displaySmall = Type.displaySmall,
+ headlineLarge = Type.headlineLarge,
+ headlineMedium = Type.headlineMedium,
+ headlineSmall = Type.headlineSmall,
+ titleLarge = Type.titleLarge,
+ titleMedium = Type.titleMedium,
+ titleSmall = Type.titleSmall,
+ bodyLarge = Type.bodyLarge,
+ bodyMedium = Type.bodyMedium,
+ bodySmall = Type.bodySmall,
+ labelLarge = Type.labelLarge,
+ labelMedium = Type.labelMedium,
+ labelSmall = Type.labelSmall
+)
+
+/**
+ * Main theme composable for BlackGlass Launcher
+ */
+@Composable
+fun GlassTheme(
+ darkTheme: Boolean = true, // Always dark for glass effect
+ content: @Composable () -> Unit
+) {
+ val colorScheme = BlackGlassColorScheme
+
+ // Configure system UI
+ val systemUiController = rememberSystemUiController()
+ val view = LocalView.current
+
+ if (!view.isInEditMode) {
+ SideEffect {
+ // Make system bars transparent
+ systemUiController.setSystemBarsColor(
+ color = Color.Transparent,
+ darkIcons = false
+ )
+ systemUiController.setNavigationBarColor(
+ color = Color.Transparent,
+ darkIcons = false,
+ navigationBarContrastEnforced = false
+ )
+
+ // Configure window for edge-to-edge
+ val window = (view.context as? Activity)?.window
+ window?.let {
+ WindowCompat.setDecorFitsSystemWindows(it, false)
+ }
+ }
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = BlackGlassTypography,
+ content = content
+ )
+}
+
+/**
+ * Glass effect configuration - TRI-LAYER GLASS ENGINE
+ *
+ * Layer 1 (The Void): #050505 background
+ * Layer 2 (The Frost): #1E1E24 @ 40% alpha + Blur(30dp)
+ * Layer 3 (The Neon): Gradient #7B0323 โ #2B0033
+ */
+object GlassConfig {
+ /**
+ * FROST BLUR RADIUS - Layer 2 spec
+ * The Frost layer uses 30dp blur
+ */
+ const val FROST_BLUR_RADIUS = 30f
+
+ /**
+ * Default blur radius for glass surfaces
+ */
+ const val DEFAULT_BLUR_RADIUS = 30f // Updated to match Frost spec
+
+ /**
+ * Light blur for subtle effects
+ */
+ const val LIGHT_BLUR_RADIUS = 15f
+
+ /**
+ * Heavy blur for strong glass effect
+ */
+ const val HEAVY_BLUR_RADIUS = 40f
+
+ /**
+ * Default glass surface opacity (Layer 2 Frost @ 40%)
+ */
+ const val DEFAULT_GLASS_ALPHA = 0.40f // Updated to match Frost spec
+
+ /**
+ * Default border opacity (Neon gradient @ 30%)
+ */
+ const val DEFAULT_BORDER_ALPHA = 0.30f // Updated to match spec
+
+ /**
+ * Default corner radius for glass cards
+ */
+ const val DEFAULT_CORNER_RADIUS = 24
+
+ /**
+ * Animation durations (ms)
+ */
+ const val ANIMATION_FAST = 150
+ const val ANIMATION_MEDIUM = 300
+ const val ANIMATION_SLOW = 500
+
+ /**
+ * Glow intensity levels
+ */
+ const val GLOW_SUBTLE = 0.3f
+ const val GLOW_MEDIUM = 0.5f
+ const val GLOW_BRIGHT = 0.8f
+}
+
+/**
+ * Preview helper - wraps content in GlassTheme
+ */
+@Composable
+fun GlassPreview(content: @Composable () -> Unit) {
+ GlassTheme(content = content)
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/theme/Theme.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/theme/Theme.kt
new file mode 100644
index 00000000..fb2b8538
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/theme/Theme.kt
@@ -0,0 +1,87 @@
+package com.blackglass.launcher.ui.theme
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.ui.graphics.Color
+import com.google.accompanist.systemuicontroller.rememberSystemUiController
+
+/**
+ * BlackGlass UI OS - Theme Configuration
+ * Deep Glass aesthetic with neon accents
+ */
+
+private val BlackGlassColorScheme = darkColorScheme(
+ // Primary colors
+ primary = BlackGlassColors.NeonWineBright,
+ onPrimary = BlackGlassColors.White,
+ primaryContainer = BlackGlassColors.NeonWine,
+ onPrimaryContainer = BlackGlassColors.NeonWineGlow,
+
+ // Secondary colors
+ secondary = BlackGlassColors.DarkVioletBright,
+ onSecondary = BlackGlassColors.White,
+ secondaryContainer = BlackGlassColors.DarkViolet,
+ onSecondaryContainer = BlackGlassColors.DarkVioletGlow,
+
+ // Tertiary colors
+ tertiary = BlackGlassColors.AccentCyan,
+ onTertiary = BlackGlassColors.Black,
+ tertiaryContainer = BlackGlassColors.DarkViolet,
+ onTertiaryContainer = BlackGlassColors.AccentCyan,
+
+ // Error colors
+ error = BlackGlassColors.NeonWineBright,
+ onError = BlackGlassColors.White,
+ errorContainer = BlackGlassColors.NeonWineDeep,
+ onErrorContainer = BlackGlassColors.NeonWineGlow,
+
+ // Background colors
+ background = BlackGlassColors.BackgroundDark,
+ onBackground = BlackGlassColors.TextPrimary,
+
+ // Surface colors
+ surface = BlackGlassColors.GlassLayer2,
+ onSurface = BlackGlassColors.TextPrimary,
+ surfaceVariant = BlackGlassColors.GlassLayer3,
+ onSurfaceVariant = BlackGlassColors.TextSecondary,
+
+ // Outline
+ outline = BlackGlassColors.GlassBorder,
+ outlineVariant = BlackGlassColors.GlassBorderBright,
+
+ // Inverse colors
+ inverseSurface = BlackGlassColors.White,
+ inverseOnSurface = BlackGlassColors.Black,
+ inversePrimary = BlackGlassColors.NeonWineDeep,
+
+ // Scrim
+ scrim = BlackGlassColors.Scrim
+)
+
+@Composable
+fun BlackGlassTheme(
+ content: @Composable () -> Unit
+) {
+ val systemUiController = rememberSystemUiController()
+
+ SideEffect {
+ // Make system bars fully transparent
+ systemUiController.setSystemBarsColor(
+ color = Color.Transparent,
+ darkIcons = false
+ )
+ systemUiController.setNavigationBarColor(
+ color = Color.Transparent,
+ darkIcons = false,
+ navigationBarContrastEnforced = false
+ )
+ }
+
+ MaterialTheme(
+ colorScheme = BlackGlassColorScheme,
+ content = content
+ )
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/theme/Type.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/theme/Type.kt
new file mode 100644
index 00000000..4bea768d
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/theme/Type.kt
@@ -0,0 +1,286 @@
+package com.blackglass.launcher.ui.theme
+
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+/**
+ * BlackGlass UI OS - Typography System
+ * Clean, modern typography optimized for glass surfaces
+ */
+
+/**
+ * Font families - using system fonts for now
+ * Can be replaced with custom fonts (e.g., Inter, SF Pro)
+ */
+val GlassFontFamily = FontFamily.SansSerif
+val GlassDisplayFont = FontFamily.SansSerif
+val GlassMonoFont = FontFamily.Monospace
+
+/**
+ * Typography object with all text styles
+ */
+object Type {
+
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // DISPLAY STYLES
+ // Large headers, hero text, splash screens
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+ val displayLarge = TextStyle(
+ fontFamily = GlassDisplayFont,
+ fontWeight = FontWeight.Light,
+ fontSize = 57.sp,
+ lineHeight = 64.sp,
+ letterSpacing = (-0.25).sp
+ )
+
+ val displayMedium = TextStyle(
+ fontFamily = GlassDisplayFont,
+ fontWeight = FontWeight.Light,
+ fontSize = 45.sp,
+ lineHeight = 52.sp,
+ letterSpacing = 0.sp
+ )
+
+ val displaySmall = TextStyle(
+ fontFamily = GlassDisplayFont,
+ fontWeight = FontWeight.Normal,
+ fontSize = 36.sp,
+ lineHeight = 44.sp,
+ letterSpacing = 0.sp
+ )
+
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // HEADLINE STYLES
+ // Section headers, page titles
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+ val headlineLarge = TextStyle(
+ fontFamily = GlassFontFamily,
+ fontWeight = FontWeight.Medium,
+ fontSize = 32.sp,
+ lineHeight = 40.sp,
+ letterSpacing = 0.sp
+ )
+
+ val headlineMedium = TextStyle(
+ fontFamily = GlassFontFamily,
+ fontWeight = FontWeight.Medium,
+ fontSize = 28.sp,
+ lineHeight = 36.sp,
+ letterSpacing = 0.sp
+ )
+
+ val headlineSmall = TextStyle(
+ fontFamily = GlassFontFamily,
+ fontWeight = FontWeight.Medium,
+ fontSize = 24.sp,
+ lineHeight = 32.sp,
+ letterSpacing = 0.sp
+ )
+
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // TITLE STYLES
+ // Cards, dialogs, list items
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+ val titleLarge = TextStyle(
+ fontFamily = GlassFontFamily,
+ fontWeight = FontWeight.Medium,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ )
+
+ val titleMedium = TextStyle(
+ fontFamily = GlassFontFamily,
+ fontWeight = FontWeight.Medium,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.15.sp
+ )
+
+ val titleSmall = TextStyle(
+ fontFamily = GlassFontFamily,
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.1.sp
+ )
+
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // BODY STYLES
+ // Content text, descriptions
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+ val bodyLarge = TextStyle(
+ fontFamily = GlassFontFamily,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ )
+
+ val bodyMedium = TextStyle(
+ fontFamily = GlassFontFamily,
+ fontWeight = FontWeight.Normal,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.25.sp
+ )
+
+ val bodySmall = TextStyle(
+ fontFamily = GlassFontFamily,
+ fontWeight = FontWeight.Normal,
+ fontSize = 12.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.4.sp
+ )
+
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // LABEL STYLES
+ // Buttons, chips, tags, captions
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+ val labelLarge = TextStyle(
+ fontFamily = GlassFontFamily,
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.1.sp
+ )
+
+ val labelMedium = TextStyle(
+ fontFamily = GlassFontFamily,
+ fontWeight = FontWeight.Medium,
+ fontSize = 12.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+
+ val labelSmall = TextStyle(
+ fontFamily = GlassFontFamily,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // SPECIAL STYLES
+ // App-specific typography
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+ /**
+ * Clock display - large time on home screen
+ */
+ val clock = TextStyle(
+ fontFamily = GlassDisplayFont,
+ fontWeight = FontWeight.Thin,
+ fontSize = 80.sp,
+ lineHeight = 88.sp,
+ letterSpacing = (-3).sp
+ )
+
+ /**
+ * Clock small - for widgets
+ */
+ val clockSmall = TextStyle(
+ fontFamily = GlassDisplayFont,
+ fontWeight = FontWeight.Light,
+ fontSize = 48.sp,
+ lineHeight = 52.sp,
+ letterSpacing = (-1).sp
+ )
+
+ /**
+ * Date display
+ */
+ val date = TextStyle(
+ fontFamily = GlassFontFamily,
+ fontWeight = FontWeight.Light,
+ fontSize = 18.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 2.sp
+ )
+
+ /**
+ * App icon label
+ */
+ val appLabel = TextStyle(
+ fontFamily = GlassFontFamily,
+ fontWeight = FontWeight.Normal,
+ fontSize = 11.sp,
+ lineHeight = 14.sp,
+ letterSpacing = 0.sp
+ )
+
+ /**
+ * Category tab
+ */
+ val categoryTab = TextStyle(
+ fontFamily = GlassFontFamily,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 13.sp,
+ lineHeight = 18.sp,
+ letterSpacing = 0.5.sp
+ )
+
+ /**
+ * Search hint
+ */
+ val searchHint = TextStyle(
+ fontFamily = GlassFontFamily,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.sp
+ )
+
+ /**
+ * Widget title
+ */
+ val widgetTitle = TextStyle(
+ fontFamily = GlassFontFamily,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 14.sp,
+ lineHeight = 18.sp,
+ letterSpacing = 0.25.sp
+ )
+
+ /**
+ * Widget subtitle
+ */
+ val widgetSubtitle = TextStyle(
+ fontFamily = GlassFontFamily,
+ fontWeight = FontWeight.Normal,
+ fontSize = 12.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.2.sp
+ )
+
+ /**
+ * Widget value (large number/stat)
+ */
+ val widgetValue = TextStyle(
+ fontFamily = GlassDisplayFont,
+ fontWeight = FontWeight.Medium,
+ fontSize = 32.sp,
+ lineHeight = 36.sp,
+ letterSpacing = (-0.5).sp
+ )
+
+ /**
+ * Monospace for code/numbers
+ */
+ val mono = TextStyle(
+ fontFamily = GlassMonoFont,
+ fontWeight = FontWeight.Normal,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.sp
+ )
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/theme/Typography.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/theme/Typography.kt
new file mode 100644
index 00000000..c3195d7b
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/theme/Typography.kt
@@ -0,0 +1,211 @@
+package com.blackglass.launcher.ui.theme
+
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+/**
+ * BlackGlass UI OS - Typography System
+ * Clean, modern typography optimized for glass surfaces
+ */
+object BlackGlassTypography {
+
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // DISPLAY STYLES (Large headers, splash screens)
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ val DisplayLarge = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Light,
+ fontSize = 57.sp,
+ lineHeight = 64.sp,
+ letterSpacing = (-0.25).sp,
+ color = BlackGlassColors.TextPrimary
+ )
+
+ val DisplayMedium = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Light,
+ fontSize = 45.sp,
+ lineHeight = 52.sp,
+ letterSpacing = 0.sp,
+ color = BlackGlassColors.TextPrimary
+ )
+
+ val DisplaySmall = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Normal,
+ fontSize = 36.sp,
+ lineHeight = 44.sp,
+ letterSpacing = 0.sp,
+ color = BlackGlassColors.TextPrimary
+ )
+
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // HEADLINE STYLES (Section headers)
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ val HeadlineLarge = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Medium,
+ fontSize = 32.sp,
+ lineHeight = 40.sp,
+ letterSpacing = 0.sp,
+ color = BlackGlassColors.TextPrimary
+ )
+
+ val HeadlineMedium = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Medium,
+ fontSize = 28.sp,
+ lineHeight = 36.sp,
+ letterSpacing = 0.sp,
+ color = BlackGlassColors.TextPrimary
+ )
+
+ val HeadlineSmall = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Medium,
+ fontSize = 24.sp,
+ lineHeight = 32.sp,
+ letterSpacing = 0.sp,
+ color = BlackGlassColors.TextPrimary
+ )
+
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // TITLE STYLES (Cards, dialogs)
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ val TitleLarge = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Medium,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp,
+ color = BlackGlassColors.TextPrimary
+ )
+
+ val TitleMedium = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Medium,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.15.sp,
+ color = BlackGlassColors.TextPrimary
+ )
+
+ val TitleSmall = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.1.sp,
+ color = BlackGlassColors.TextPrimary
+ )
+
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // BODY STYLES (Content text)
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ val BodyLarge = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp,
+ color = BlackGlassColors.TextSecondary
+ )
+
+ val BodyMedium = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Normal,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.25.sp,
+ color = BlackGlassColors.TextSecondary
+ )
+
+ val BodySmall = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Normal,
+ fontSize = 12.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.4.sp,
+ color = BlackGlassColors.TextSecondary
+ )
+
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // LABEL STYLES (Buttons, chips, tags)
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ val LabelLarge = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.1.sp,
+ color = BlackGlassColors.TextPrimary
+ )
+
+ val LabelMedium = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Medium,
+ fontSize = 12.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp,
+ color = BlackGlassColors.TextPrimary
+ )
+
+ val LabelSmall = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp,
+ color = BlackGlassColors.TextSecondary
+ )
+
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ // SPECIAL STYLES (App icons, dock, clock)
+ // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ val AppIconLabel = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Normal,
+ fontSize = 11.sp,
+ lineHeight = 14.sp,
+ letterSpacing = 0.sp,
+ color = BlackGlassColors.TextPrimary
+ )
+
+ val ClockDisplay = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Thin,
+ fontSize = 72.sp,
+ lineHeight = 80.sp,
+ letterSpacing = (-2).sp,
+ color = BlackGlassColors.TextPrimary
+ )
+
+ val DateDisplay = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Light,
+ fontSize = 18.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 1.sp,
+ color = BlackGlassColors.TextSecondary
+ )
+
+ val SearchHint = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.sp,
+ color = BlackGlassColors.TextTertiary
+ )
+
+ val CategoryTab = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 13.sp,
+ lineHeight = 18.sp,
+ letterSpacing = 0.5.sp,
+ color = BlackGlassColors.TextPrimary
+ )
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/widgets/ClockWidget.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/widgets/ClockWidget.kt
new file mode 100644
index 00000000..e27c943c
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/widgets/ClockWidget.kt
@@ -0,0 +1,253 @@
+package com.blackglass.launcher.ui.widgets
+
+import androidx.compose.animation.core.*
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.blackglass.launcher.ui.theme.GlassColors
+import com.blackglass.launcher.ui.theme.Type
+import kotlinx.coroutines.delay
+import java.text.SimpleDateFormat
+import java.util.*
+
+/**
+ * BlackGlass UI OS - Clock Widget
+ * Elegant time display with glass aesthetics
+ */
+
+@Composable
+fun ClockWidget(
+ onOpenClock: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ var currentTime by remember { mutableStateOf(System.currentTimeMillis()) }
+
+ LaunchedEffect(Unit) {
+ while (true) {
+ currentTime = System.currentTimeMillis()
+ delay(1000)
+ }
+ }
+
+ val timeFormat = remember { SimpleDateFormat("HH:mm", Locale.getDefault()) }
+ val dateFormat = remember { SimpleDateFormat("EEEE, MMMM d", Locale.getDefault()) }
+
+ val time = remember(currentTime) { timeFormat.format(Date(currentTime)) }
+ val date = remember(currentTime) { dateFormat.format(Date(currentTime)) }
+
+ // Subtle glow animation
+ val infiniteTransition = rememberInfiniteTransition(label = "clock_glow")
+ val glowAlpha by infiniteTransition.animateFloat(
+ initialValue = 0.3f,
+ targetValue = 0.5f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(2000, easing = EaseInOutSine),
+ repeatMode = RepeatMode.Reverse
+ ),
+ label = "glow"
+ )
+
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(28.dp))
+ .background(
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ GlassColors.Shade1.copy(alpha = 0.7f),
+ GlassColors.Shade2.copy(alpha = 0.5f)
+ )
+ )
+ )
+ .border(
+ width = 1.dp,
+ brush = Brush.linearGradient(
+ colors = listOf(
+ GlassColors.NeonWineGlow.copy(alpha = glowAlpha),
+ Color.White.copy(alpha = 0.1f)
+ )
+ ),
+ shape = RoundedCornerShape(28.dp)
+ )
+ .clickable { onOpenClock() }
+ .padding(24.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ // Time display
+ Text(
+ text = time,
+ style = Type.clock.copy(
+ fontSize = 72.sp,
+ fontWeight = FontWeight.Thin,
+ letterSpacing = 4.sp
+ ),
+ color = GlassColors.TextPrimary
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ // Date display
+ Text(
+ text = date,
+ style = Type.widgetSubtitle.copy(
+ letterSpacing = 1.sp
+ ),
+ color = GlassColors.TextSecondary
+ )
+ }
+ }
+}
+
+/**
+ * Compact clock for smaller spaces
+ */
+@Composable
+fun CompactClockWidget(
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ var currentTime by remember { mutableStateOf(System.currentTimeMillis()) }
+
+ LaunchedEffect(Unit) {
+ while (true) {
+ currentTime = System.currentTimeMillis()
+ delay(1000)
+ }
+ }
+
+ val timeFormat = remember { SimpleDateFormat("HH:mm", Locale.getDefault()) }
+ val time = remember(currentTime) { timeFormat.format(Date(currentTime)) }
+
+ Text(
+ text = time,
+ style = Type.clock.copy(fontSize = 32.sp),
+ color = GlassColors.TextPrimary,
+ modifier = modifier.clickable { onClick() }
+ )
+}
+
+/**
+ * Minimal clock inline display
+ */
+@Composable
+fun InlineClockWidget(
+ modifier: Modifier = Modifier
+) {
+ var currentTime by remember { mutableStateOf(System.currentTimeMillis()) }
+
+ LaunchedEffect(Unit) {
+ while (true) {
+ currentTime = System.currentTimeMillis()
+ delay(1000)
+ }
+ }
+
+ val timeFormat = remember { SimpleDateFormat("HH:mm", Locale.getDefault()) }
+ val shortDateFormat = remember { SimpleDateFormat("EEE, MMM d", Locale.getDefault()) }
+
+ val time = remember(currentTime) { timeFormat.format(Date(currentTime)) }
+ val date = remember(currentTime) { shortDateFormat.format(Date(currentTime)) }
+
+ Row(
+ modifier = modifier,
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Text(
+ text = time,
+ style = Type.titleLarge,
+ color = GlassColors.TextPrimary
+ )
+
+ Box(
+ modifier = Modifier
+ .width(1.dp)
+ .height(16.dp)
+ .background(GlassColors.TextTertiary)
+ )
+
+ Text(
+ text = date,
+ style = Type.labelMedium,
+ color = GlassColors.TextSecondary
+ )
+ }
+}
+
+/**
+ * Digital clock with seconds
+ */
+@Composable
+fun DigitalClockWithSeconds(
+ onOpenClock: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ var currentTime by remember { mutableStateOf(System.currentTimeMillis()) }
+
+ LaunchedEffect(Unit) {
+ while (true) {
+ currentTime = System.currentTimeMillis()
+ delay(1000)
+ }
+ }
+
+ val timeFormat = remember { SimpleDateFormat("HH:mm:ss", Locale.getDefault()) }
+ val time = remember(currentTime) { timeFormat.format(Date(currentTime)) }
+
+ Row(
+ modifier = modifier
+ .clip(RoundedCornerShape(16.dp))
+ .background(GlassColors.Shade2.copy(alpha = 0.6f))
+ .border(1.dp, Color.White.copy(alpha = 0.1f), RoundedCornerShape(16.dp))
+ .clickable { onOpenClock() }
+ .padding(horizontal = 20.dp, vertical = 12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ time.forEachIndexed { index, char ->
+ if (char == ':') {
+ // Blinking colon animation
+ val infiniteTransition = rememberInfiniteTransition(label = "blink_$index")
+ val alpha by infiniteTransition.animateFloat(
+ initialValue = 1f,
+ targetValue = 0.3f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(500),
+ repeatMode = RepeatMode.Reverse
+ ),
+ label = "colon_blink"
+ )
+
+ Text(
+ text = ":",
+ style = Type.clock.copy(fontSize = 28.sp),
+ color = GlassColors.NeonWineRed.copy(alpha = alpha),
+ modifier = Modifier.padding(horizontal = 2.dp)
+ )
+ } else {
+ Text(
+ text = char.toString(),
+ style = Type.clock.copy(
+ fontSize = 28.sp,
+ fontWeight = FontWeight.Medium
+ ),
+ color = GlassColors.TextPrimary
+ )
+ }
+ }
+ }
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/widgets/MusicGlassWidget.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/widgets/MusicGlassWidget.kt
new file mode 100644
index 00000000..71ea8b94
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/widgets/MusicGlassWidget.kt
@@ -0,0 +1,310 @@
+package com.blackglass.launcher.ui.widgets
+
+import android.graphics.RuntimeShader
+import android.os.Build
+import androidx.compose.animation.core.*
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.*
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawWithCache
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ShaderBrush
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import com.blackglass.launcher.ui.theme.GlassColors
+import com.blackglass.launcher.ui.theme.Type
+
+/**
+ * BlackGlass UI OS - Music Glass Widget
+ * Elegant music control with glass aesthetics
+ */
+
+data class MusicState(
+ val isPlaying: Boolean = false,
+ val trackTitle: String = "No Track Playing",
+ val artistName: String = "Unknown Artist",
+ val albumArt: Any? = null,
+ val progress: Float = 0f,
+ val duration: Long = 0L
+)
+
+@Composable
+fun MusicGlassWidget(
+ musicState: MusicState,
+ onPlayPause: () -> Unit,
+ onNext: () -> Unit,
+ onPrevious: () -> Unit,
+ onOpenPlayer: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val infiniteTransition = rememberInfiniteTransition(label = "music_wave")
+
+ // Animated glow when playing
+ val glowAlpha by infiniteTransition.animateFloat(
+ initialValue = 0.3f,
+ targetValue = 0.6f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(1500, easing = EaseInOutSine),
+ repeatMode = RepeatMode.Reverse
+ ),
+ label = "glow"
+ )
+
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(24.dp))
+ .background(
+ brush = Brush.linearGradient(
+ colors = listOf(
+ GlassColors.Shade2.copy(alpha = 0.85f),
+ GlassColors.Shade1.copy(alpha = 0.75f)
+ )
+ )
+ )
+ .border(
+ width = 1.dp,
+ brush = Brush.linearGradient(
+ colors = listOf(
+ if (musicState.isPlaying)
+ GlassColors.CategoryMedia.copy(alpha = glowAlpha)
+ else
+ Color.White.copy(alpha = 0.15f),
+ Color.White.copy(alpha = 0.05f)
+ )
+ ),
+ shape = RoundedCornerShape(24.dp)
+ )
+ .clickable { onOpenPlayer() }
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // Album art / placeholder
+ AlbumArtBox(
+ isPlaying = musicState.isPlaying,
+ modifier = Modifier.size(56.dp)
+ )
+
+ Spacer(modifier = Modifier.width(12.dp))
+
+ // Track info
+ Column(
+ modifier = Modifier.weight(1f)
+ ) {
+ Text(
+ text = musicState.trackTitle,
+ style = Type.widgetTitle,
+ color = GlassColors.TextPrimary,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+
+ Spacer(modifier = Modifier.height(2.dp))
+
+ Text(
+ text = musicState.artistName,
+ style = Type.widgetSubtitle,
+ color = GlassColors.TextSecondary,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ // Progress bar
+ MusicProgressBar(
+ progress = musicState.progress,
+ isPlaying = musicState.isPlaying
+ )
+ }
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ // Controls
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ MusicControlButton(
+ icon = Icons.Default.SkipPrevious,
+ onClick = onPrevious,
+ isAccent = false
+ )
+
+ MusicControlButton(
+ icon = if (musicState.isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
+ onClick = onPlayPause,
+ isAccent = true
+ )
+
+ MusicControlButton(
+ icon = Icons.Default.SkipNext,
+ onClick = onNext,
+ isAccent = false
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun AlbumArtBox(
+ isPlaying: Boolean,
+ modifier: Modifier = Modifier
+) {
+ val rotation by rememberInfiniteTransition(label = "disc").animateFloat(
+ initialValue = 0f,
+ targetValue = 360f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(8000, easing = LinearEasing)
+ ),
+ label = "disc_rotation"
+ )
+
+ Box(
+ modifier = modifier
+ .graphicsLayer {
+ if (isPlaying) rotationZ = rotation
+ }
+ .clip(CircleShape)
+ .background(
+ brush = Brush.radialGradient(
+ colors = listOf(
+ GlassColors.CategoryMedia.copy(alpha = 0.3f),
+ GlassColors.Shade3.copy(alpha = 0.8f),
+ GlassColors.CategoryMedia.copy(alpha = 0.2f)
+ )
+ )
+ )
+ .border(2.dp, GlassColors.CategoryMedia.copy(alpha = 0.4f), CircleShape),
+ contentAlignment = Alignment.Center
+ ) {
+ // Center hole of vinyl
+ Box(
+ modifier = Modifier
+ .size(12.dp)
+ .clip(CircleShape)
+ .background(GlassColors.Shade1)
+ .border(1.dp, GlassColors.CategoryMedia.copy(alpha = 0.5f), CircleShape)
+ )
+ }
+}
+
+@Composable
+private fun MusicProgressBar(
+ progress: Float,
+ isPlaying: Boolean
+) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(3.dp)
+ .clip(RoundedCornerShape(1.5.dp))
+ .background(Color.White.copy(alpha = 0.1f))
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxHeight()
+ .fillMaxWidth(progress.coerceIn(0f, 1f))
+ .clip(RoundedCornerShape(1.5.dp))
+ .background(
+ if (isPlaying) GlassColors.CategoryMedia
+ else GlassColors.TextSecondary
+ )
+ )
+ }
+}
+
+@Composable
+private fun MusicControlButton(
+ icon: ImageVector,
+ onClick: () -> Unit,
+ isAccent: Boolean
+) {
+ IconButton(
+ onClick = onClick,
+ modifier = Modifier
+ .size(if (isAccent) 44.dp else 36.dp)
+ .clip(CircleShape)
+ .background(
+ if (isAccent) GlassColors.CategoryMedia.copy(alpha = 0.2f)
+ else Color.Transparent
+ )
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = null,
+ tint = if (isAccent) GlassColors.CategoryMedia else GlassColors.TextSecondary,
+ modifier = Modifier.size(if (isAccent) 28.dp else 22.dp)
+ )
+ }
+}
+
+/**
+ * Compact music widget (for smaller spaces)
+ */
+@Composable
+fun CompactMusicWidget(
+ musicState: MusicState,
+ onPlayPause: () -> Unit,
+ onOpenPlayer: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Row(
+ modifier = modifier
+ .clip(RoundedCornerShape(16.dp))
+ .background(GlassColors.Shade2.copy(alpha = 0.8f))
+ .border(1.dp, Color.White.copy(alpha = 0.1f), RoundedCornerShape(16.dp))
+ .clickable { onOpenPlayer() }
+ .padding(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = Icons.Default.MusicNote,
+ contentDescription = null,
+ tint = GlassColors.CategoryMedia,
+ modifier = Modifier.size(20.dp)
+ )
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ Text(
+ text = musicState.trackTitle,
+ style = Type.labelMedium,
+ color = GlassColors.TextPrimary,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier.weight(1f)
+ )
+
+ IconButton(
+ onClick = onPlayPause,
+ modifier = Modifier.size(32.dp)
+ ) {
+ Icon(
+ imageVector = if (musicState.isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
+ contentDescription = null,
+ tint = GlassColors.TextPrimary
+ )
+ }
+ }
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/widgets/QuickControlWidget.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/widgets/QuickControlWidget.kt
new file mode 100644
index 00000000..37f4aaff
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/widgets/QuickControlWidget.kt
@@ -0,0 +1,412 @@
+package com.blackglass.launcher.ui.widgets
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.*
+import androidx.compose.material.icons.outlined.*
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.unit.dp
+import com.blackglass.launcher.ui.theme.GlassColors
+import com.blackglass.launcher.ui.theme.Type
+
+/**
+ * BlackGlass UI OS - Quick Control Widget
+ * Toggle buttons for WiFi, Bluetooth, Flashlight, etc.
+ */
+
+data class QuickControlState(
+ val wifiEnabled: Boolean = false,
+ val bluetoothEnabled: Boolean = false,
+ val flashlightEnabled: Boolean = false,
+ val silentMode: Boolean = false,
+ val rotationLock: Boolean = false,
+ val airplaneMode: Boolean = false,
+ val locationEnabled: Boolean = false,
+ val batteryLevel: Int = 100
+)
+
+@Composable
+fun QuickControlWidget(
+ state: QuickControlState,
+ onWifiToggle: () -> Unit,
+ onBluetoothToggle: () -> Unit,
+ onFlashlightToggle: () -> Unit,
+ onSilentToggle: () -> Unit,
+ onRotationToggle: () -> Unit,
+ onAirplaneToggle: () -> Unit,
+ onOpenSettings: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(24.dp))
+ .background(
+ brush = Brush.linearGradient(
+ colors = listOf(
+ GlassColors.Shade2.copy(alpha = 0.85f),
+ GlassColors.Shade1.copy(alpha = 0.75f)
+ )
+ )
+ )
+ .border(
+ width = 1.dp,
+ brush = Brush.linearGradient(
+ colors = listOf(
+ Color.White.copy(alpha = 0.15f),
+ Color.White.copy(alpha = 0.05f)
+ )
+ ),
+ shape = RoundedCornerShape(24.dp)
+ )
+ .padding(16.dp)
+ ) {
+ Column {
+ // Header with settings button
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = "Quick Controls",
+ style = Type.widgetTitle,
+ color = GlassColors.TextPrimary
+ )
+
+ // Battery indicator
+ BatteryIndicator(level = state.batteryLevel)
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // Control grid - 2 rows
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceEvenly
+ ) {
+ QuickControlButton(
+ icon = Icons.Default.Wifi,
+ iconOff = Icons.Outlined.WifiOff,
+ label = "WiFi",
+ isEnabled = state.wifiEnabled,
+ enabledColor = GlassColors.CategoryMedia,
+ onClick = onWifiToggle
+ )
+
+ QuickControlButton(
+ icon = Icons.Default.Bluetooth,
+ iconOff = Icons.Default.BluetoothDisabled,
+ label = "Bluetooth",
+ isEnabled = state.bluetoothEnabled,
+ enabledColor = GlassColors.DarkViolet,
+ onClick = onBluetoothToggle
+ )
+
+ QuickControlButton(
+ icon = Icons.Default.FlashlightOn,
+ iconOff = Icons.Default.FlashlightOff,
+ label = "Flashlight",
+ isEnabled = state.flashlightEnabled,
+ enabledColor = Color(0xFFFFD54F),
+ onClick = onFlashlightToggle
+ )
+
+ QuickControlButton(
+ icon = Icons.Default.VolumeOff,
+ iconOff = Icons.Default.VolumeUp,
+ label = if (state.silentMode) "Silent" else "Sound",
+ isEnabled = state.silentMode,
+ enabledColor = GlassColors.NeonWineRed,
+ onClick = onSilentToggle
+ )
+ }
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceEvenly
+ ) {
+ QuickControlButton(
+ icon = Icons.Default.ScreenRotation,
+ iconOff = Icons.Default.ScreenLockRotation,
+ label = "Rotation",
+ isEnabled = !state.rotationLock,
+ enabledColor = GlassColors.CategoryProductivity,
+ onClick = onRotationToggle
+ )
+
+ QuickControlButton(
+ icon = Icons.Default.AirplanemodeActive,
+ iconOff = Icons.Default.AirplanemodeInactive,
+ label = "Airplane",
+ isEnabled = state.airplaneMode,
+ enabledColor = Color(0xFF64B5F6),
+ onClick = onAirplaneToggle
+ )
+
+ QuickControlButton(
+ icon = Icons.Default.LocationOn,
+ iconOff = Icons.Default.LocationOff,
+ label = "Location",
+ isEnabled = state.locationEnabled,
+ enabledColor = GlassColors.CategoryGames,
+ onClick = { }
+ )
+
+ QuickControlButton(
+ icon = Icons.Default.Settings,
+ iconOff = Icons.Default.Settings,
+ label = "Settings",
+ isEnabled = false,
+ enabledColor = GlassColors.TextSecondary,
+ onClick = onOpenSettings,
+ alwaysShowLabel = true
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun QuickControlButton(
+ icon: ImageVector,
+ iconOff: ImageVector,
+ label: String,
+ isEnabled: Boolean,
+ enabledColor: Color,
+ onClick: () -> Unit,
+ alwaysShowLabel: Boolean = false,
+ modifier: Modifier = Modifier
+) {
+ val backgroundColor by animateColorAsState(
+ targetValue = if (isEnabled) enabledColor.copy(alpha = 0.2f) else Color.White.copy(alpha = 0.08f),
+ animationSpec = tween(200),
+ label = "bg"
+ )
+
+ val borderColor by animateColorAsState(
+ targetValue = if (isEnabled) enabledColor.copy(alpha = 0.5f) else Color.White.copy(alpha = 0.1f),
+ animationSpec = tween(200),
+ label = "border"
+ )
+
+ val iconColor by animateColorAsState(
+ targetValue = if (isEnabled) enabledColor else GlassColors.TextSecondary,
+ animationSpec = tween(200),
+ label = "icon"
+ )
+
+ Column(
+ modifier = modifier,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Box(
+ modifier = Modifier
+ .size(52.dp)
+ .clip(CircleShape)
+ .background(backgroundColor)
+ .border(1.dp, borderColor, CircleShape)
+ .clickable { onClick() },
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = if (isEnabled || alwaysShowLabel) icon else iconOff,
+ contentDescription = label,
+ tint = iconColor,
+ modifier = Modifier.size(24.dp)
+ )
+ }
+
+ Spacer(modifier = Modifier.height(4.dp))
+
+ Text(
+ text = label,
+ style = Type.labelSmall,
+ color = if (isEnabled) enabledColor else GlassColors.TextTertiary
+ )
+ }
+}
+
+@Composable
+private fun BatteryIndicator(level: Int) {
+ val batteryColor = when {
+ level > 50 -> Color(0xFF4CAF50)
+ level > 20 -> Color(0xFFFFB74D)
+ else -> GlassColors.NeonWineRed
+ }
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .clip(RoundedCornerShape(8.dp))
+ .background(Color.White.copy(alpha = 0.05f))
+ .padding(horizontal = 8.dp, vertical = 4.dp)
+ ) {
+ Icon(
+ imageVector = when {
+ level > 90 -> Icons.Default.BatteryFull
+ level > 50 -> Icons.Default.Battery5Bar
+ level > 20 -> Icons.Default.Battery3Bar
+ else -> Icons.Default.Battery1Bar
+ },
+ contentDescription = null,
+ tint = batteryColor,
+ modifier = Modifier.size(18.dp)
+ )
+
+ Spacer(modifier = Modifier.width(4.dp))
+
+ Text(
+ text = "$level%",
+ style = Type.labelSmall,
+ color = batteryColor
+ )
+ }
+}
+
+/**
+ * Compact quick controls (horizontal strip)
+ */
+@Composable
+fun CompactQuickControls(
+ state: QuickControlState,
+ onWifiToggle: () -> Unit,
+ onBluetoothToggle: () -> Unit,
+ onSilentToggle: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Row(
+ modifier = modifier
+ .clip(RoundedCornerShape(16.dp))
+ .background(GlassColors.Shade2.copy(alpha = 0.8f))
+ .border(1.dp, Color.White.copy(alpha = 0.1f), RoundedCornerShape(16.dp))
+ .padding(8.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ CompactToggle(
+ icon = Icons.Default.Wifi,
+ isEnabled = state.wifiEnabled,
+ enabledColor = GlassColors.CategoryMedia,
+ onClick = onWifiToggle
+ )
+
+ CompactToggle(
+ icon = Icons.Default.Bluetooth,
+ isEnabled = state.bluetoothEnabled,
+ enabledColor = GlassColors.DarkViolet,
+ onClick = onBluetoothToggle
+ )
+
+ CompactToggle(
+ icon = Icons.Default.VolumeOff,
+ isEnabled = state.silentMode,
+ enabledColor = GlassColors.NeonWineRed,
+ onClick = onSilentToggle
+ )
+ }
+}
+
+@Composable
+private fun CompactToggle(
+ icon: ImageVector,
+ isEnabled: Boolean,
+ enabledColor: Color,
+ onClick: () -> Unit
+) {
+ Box(
+ modifier = Modifier
+ .size(36.dp)
+ .clip(CircleShape)
+ .background(
+ if (isEnabled) enabledColor.copy(alpha = 0.2f)
+ else Color.White.copy(alpha = 0.08f)
+ )
+ .clickable { onClick() },
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = null,
+ tint = if (isEnabled) enabledColor else GlassColors.TextSecondary,
+ modifier = Modifier.size(18.dp)
+ )
+ }
+}
+
+/**
+ * Brightness slider for expanded settings
+ */
+@Composable
+fun BrightnessControl(
+ brightness: Float,
+ onBrightnessChange: (Float) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(12.dp))
+ .background(Color.White.copy(alpha = 0.05f))
+ .padding(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = Icons.Default.BrightnessLow,
+ contentDescription = null,
+ tint = GlassColors.TextSecondary,
+ modifier = Modifier.size(20.dp)
+ )
+
+ Spacer(modifier = Modifier.width(12.dp))
+
+ // Simple brightness bar
+ Box(
+ modifier = Modifier
+ .weight(1f)
+ .height(4.dp)
+ .clip(RoundedCornerShape(2.dp))
+ .background(Color.White.copy(alpha = 0.1f))
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxHeight()
+ .fillMaxWidth(brightness)
+ .clip(RoundedCornerShape(2.dp))
+ .background(
+ brush = Brush.horizontalGradient(
+ colors = listOf(
+ GlassColors.TextSecondary,
+ Color(0xFFFFD54F)
+ )
+ )
+ )
+ )
+ }
+
+ Spacer(modifier = Modifier.width(12.dp))
+
+ Icon(
+ imageVector = Icons.Default.BrightnessHigh,
+ contentDescription = null,
+ tint = Color(0xFFFFD54F),
+ modifier = Modifier.size(20.dp)
+ )
+ }
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/widgets/WeatherGlassWidget.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/widgets/WeatherGlassWidget.kt
new file mode 100644
index 00000000..1077129c
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/ui/widgets/WeatherGlassWidget.kt
@@ -0,0 +1,341 @@
+package com.blackglass.launcher.ui.widgets
+
+import androidx.compose.animation.core.*
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.*
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.rotate
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.blackglass.launcher.ui.theme.GlassColors
+import com.blackglass.launcher.ui.theme.Type
+
+/**
+ * BlackGlass UI OS - Weather Glass Widget
+ * Beautiful weather display with glass aesthetic
+ */
+
+data class WeatherData(
+ val temperature: Int = 0,
+ val unit: String = "ยฐC",
+ val condition: WeatherCondition = WeatherCondition.SUNNY,
+ val location: String = "Unknown",
+ val humidity: Int = 0,
+ val windSpeed: Int = 0,
+ val highTemp: Int = 0,
+ val lowTemp: Int = 0,
+ val feelsLike: Int = 0,
+ val isDay: Boolean = true
+)
+
+enum class WeatherCondition(val icon: ImageVector, val description: String) {
+ SUNNY(Icons.Default.WbSunny, "Sunny"),
+ CLOUDY(Icons.Default.Cloud, "Cloudy"),
+ PARTLY_CLOUDY(Icons.Default.FilterDrama, "Partly Cloudy"),
+ RAINY(Icons.Default.Grain, "Rainy"),
+ STORMY(Icons.Default.Thunderstorm, "Stormy"),
+ SNOWY(Icons.Default.AcUnit, "Snowy"),
+ FOGGY(Icons.Default.Deblur, "Foggy"),
+ WINDY(Icons.Default.Air, "Windy"),
+ NIGHT_CLEAR(Icons.Default.NightsStay, "Clear Night"),
+ NIGHT_CLOUDY(Icons.Default.Bedtime, "Cloudy Night")
+}
+
+@Composable
+fun WeatherGlassWidget(
+ weather: WeatherData,
+ onOpenWeather: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val conditionColor = getConditionColor(weather.condition)
+
+ // Subtle animation for weather icons
+ val infiniteTransition = rememberInfiniteTransition(label = "weather")
+ val iconRotation by infiniteTransition.animateFloat(
+ initialValue = -5f,
+ targetValue = 5f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(3000, easing = EaseInOutSine),
+ repeatMode = RepeatMode.Reverse
+ ),
+ label = "icon_sway"
+ )
+
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(24.dp))
+ .background(
+ brush = Brush.linearGradient(
+ colors = listOf(
+ GlassColors.Shade2.copy(alpha = 0.85f),
+ conditionColor.copy(alpha = 0.15f),
+ GlassColors.Shade1.copy(alpha = 0.8f)
+ )
+ )
+ )
+ .border(
+ width = 1.dp,
+ brush = Brush.linearGradient(
+ colors = listOf(
+ conditionColor.copy(alpha = 0.4f),
+ Color.White.copy(alpha = 0.1f)
+ )
+ ),
+ shape = RoundedCornerShape(24.dp)
+ )
+ .clickable { onOpenWeather() }
+ .padding(16.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // Weather icon with animation
+ Box(
+ modifier = Modifier
+ .size(64.dp)
+ .rotate(iconRotation),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = weather.condition.icon,
+ contentDescription = weather.condition.description,
+ tint = conditionColor,
+ modifier = Modifier.size(48.dp)
+ )
+ }
+
+ Spacer(modifier = Modifier.width(16.dp))
+
+ // Temperature and location
+ Column(
+ modifier = Modifier.weight(1f)
+ ) {
+ // Main temperature
+ Row(
+ verticalAlignment = Alignment.Top
+ ) {
+ Text(
+ text = "${weather.temperature}",
+ style = Type.displayLarge.copy(
+ fontSize = 42.sp,
+ fontWeight = FontWeight.Light
+ ),
+ color = GlassColors.TextPrimary
+ )
+ Text(
+ text = weather.unit,
+ style = Type.titleLarge,
+ color = GlassColors.TextSecondary,
+ modifier = Modifier.padding(top = 4.dp)
+ )
+ }
+
+ // Condition text
+ Text(
+ text = weather.condition.description,
+ style = Type.widgetSubtitle,
+ color = conditionColor
+ )
+
+ // Location
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = Icons.Default.LocationOn,
+ contentDescription = null,
+ tint = GlassColors.TextTertiary,
+ modifier = Modifier.size(12.dp)
+ )
+ Spacer(modifier = Modifier.width(2.dp))
+ Text(
+ text = weather.location,
+ style = Type.labelSmall,
+ color = GlassColors.TextTertiary
+ )
+ }
+ }
+
+ // Additional info column
+ Column(
+ horizontalAlignment = Alignment.End,
+ verticalArrangement = Arrangement.spacedBy(6.dp)
+ ) {
+ // High/Low
+ WeatherDetailChip(
+ icon = Icons.Default.KeyboardArrowUp,
+ value = "${weather.highTemp}ยฐ",
+ color = GlassColors.NeonWineRed.copy(alpha = 0.8f)
+ )
+
+ WeatherDetailChip(
+ icon = Icons.Default.KeyboardArrowDown,
+ value = "${weather.lowTemp}ยฐ",
+ color = GlassColors.DarkViolet.copy(alpha = 0.8f)
+ )
+
+ // Humidity
+ WeatherDetailChip(
+ icon = Icons.Default.WaterDrop,
+ value = "${weather.humidity}%",
+ color = GlassColors.TextSecondary
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun WeatherDetailChip(
+ icon: ImageVector,
+ value: String,
+ color: Color
+) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .clip(RoundedCornerShape(8.dp))
+ .background(Color.White.copy(alpha = 0.05f))
+ .padding(horizontal = 6.dp, vertical = 3.dp)
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = null,
+ tint = color,
+ modifier = Modifier.size(14.dp)
+ )
+ Spacer(modifier = Modifier.width(2.dp))
+ Text(
+ text = value,
+ style = Type.labelSmall,
+ color = color
+ )
+ }
+}
+
+/**
+ * Get color based on weather condition
+ */
+private fun getConditionColor(condition: WeatherCondition): Color {
+ return when (condition) {
+ WeatherCondition.SUNNY -> Color(0xFFFFB74D)
+ WeatherCondition.CLOUDY -> Color(0xFF90A4AE)
+ WeatherCondition.PARTLY_CLOUDY -> Color(0xFFB0BEC5)
+ WeatherCondition.RAINY -> Color(0xFF4FC3F7)
+ WeatherCondition.STORMY -> Color(0xFF7E57C2)
+ WeatherCondition.SNOWY -> Color(0xFFE1F5FE)
+ WeatherCondition.FOGGY -> Color(0xFFB0BEC5)
+ WeatherCondition.WINDY -> Color(0xFF81D4FA)
+ WeatherCondition.NIGHT_CLEAR -> Color(0xFF5C6BC0)
+ WeatherCondition.NIGHT_CLOUDY -> Color(0xFF7986CB)
+ }
+}
+
+/**
+ * Compact weather widget (smaller variant)
+ */
+@Composable
+fun CompactWeatherWidget(
+ weather: WeatherData,
+ onOpenWeather: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Row(
+ modifier = modifier
+ .clip(RoundedCornerShape(16.dp))
+ .background(GlassColors.Shade2.copy(alpha = 0.8f))
+ .border(1.dp, Color.White.copy(alpha = 0.1f), RoundedCornerShape(16.dp))
+ .clickable { onOpenWeather() }
+ .padding(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = weather.condition.icon,
+ contentDescription = null,
+ tint = getConditionColor(weather.condition),
+ modifier = Modifier.size(24.dp)
+ )
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ Text(
+ text = "${weather.temperature}${weather.unit}",
+ style = Type.titleMedium,
+ color = GlassColors.TextPrimary
+ )
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ Text(
+ text = weather.location,
+ style = Type.labelSmall,
+ color = GlassColors.TextSecondary,
+ modifier = Modifier.weight(1f)
+ )
+ }
+}
+
+/**
+ * Forecast day item for extended widget
+ */
+@Composable
+fun ForecastDayItem(
+ dayName: String,
+ condition: WeatherCondition,
+ highTemp: Int,
+ lowTemp: Int,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier
+ .clip(RoundedCornerShape(12.dp))
+ .background(Color.White.copy(alpha = 0.05f))
+ .padding(8.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = dayName,
+ style = Type.labelSmall,
+ color = GlassColors.TextSecondary
+ )
+
+ Spacer(modifier = Modifier.height(4.dp))
+
+ Icon(
+ imageVector = condition.icon,
+ contentDescription = null,
+ tint = getConditionColor(condition),
+ modifier = Modifier.size(20.dp)
+ )
+
+ Spacer(modifier = Modifier.height(4.dp))
+
+ Text(
+ text = "$highTempยฐ",
+ style = Type.labelMedium,
+ color = GlassColors.TextPrimary
+ )
+
+ Text(
+ text = "$lowTempยฐ",
+ style = Type.labelSmall,
+ color = GlassColors.TextTertiary
+ )
+ }
+}
diff --git a/blackglass-launcher/app/src/main/java/com/blackglass/launcher/viewmodel/LauncherViewModel.kt b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/viewmodel/LauncherViewModel.kt
new file mode 100644
index 00000000..ab684350
--- /dev/null
+++ b/blackglass-launcher/app/src/main/java/com/blackglass/launcher/viewmodel/LauncherViewModel.kt
@@ -0,0 +1,224 @@
+package com.blackglass.launcher.viewmodel
+
+import android.app.Application
+import android.content.Intent
+import android.net.Uri
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.viewModelScope
+import com.blackglass.launcher.data.model.AppCategory
+import com.blackglass.launcher.data.model.AppInfo
+import com.blackglass.launcher.data.repository.AppRepository
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+
+/**
+ * Main ViewModel for the BlackGlass Launcher
+ * Phase 3: Intelligent Features Support
+ */
+class LauncherViewModel(application: Application) : AndroidViewModel(application) {
+
+ private val appRepository = AppRepository(application)
+ private val context = application.applicationContext
+
+ // App list state
+ private val _apps = MutableStateFlow>(emptyList())
+ val apps: StateFlow> = _apps.asStateFlow()
+
+ // Loading state
+ private val _isLoading = MutableStateFlow(true)
+ val isLoading: StateFlow = _isLoading.asStateFlow()
+
+ // Selected category for smart sorting
+ private val _selectedCategory = MutableStateFlow(AppCategory.ALL)
+ val selectedCategory: StateFlow = _selectedCategory.asStateFlow()
+
+ // Filtered apps based on category
+ private val _filteredApps = MutableStateFlow>(emptyList())
+ val filteredApps: StateFlow> = _filteredApps.asStateFlow()
+
+ // Search query
+ private val _searchQuery = MutableStateFlow("")
+ val searchQuery: StateFlow = _searchQuery.asStateFlow()
+
+ // Search results
+ private val _searchResults = MutableStateFlow>(emptyList())
+ val searchResults: StateFlow> = _searchResults.asStateFlow()
+
+ // Dock apps (pinned to dock)
+ private val _dockApps = MutableStateFlow>(List(5) { null })
+ val dockApps: StateFlow> = _dockApps.asStateFlow()
+
+ // App drawer visibility
+ private val _isDrawerOpen = MutableStateFlow(false)
+ val isDrawerOpen: StateFlow = _isDrawerOpen.asStateFlow()
+
+ // Search bar visibility
+ private val _isSearchActive = MutableStateFlow(false)
+ val isSearchActive: StateFlow = _isSearchActive.asStateFlow()
+
+ init {
+ loadApps()
+ }
+
+ /**
+ * Load all installed apps
+ */
+ fun loadApps() {
+ viewModelScope.launch {
+ _isLoading.value = true
+ appRepository.loadApps()
+ appRepository.apps.collect { appList ->
+ _apps.value = appList
+ _filteredApps.value = appList
+ updateDockWithDefaults(appList)
+ _isLoading.value = false
+ }
+ }
+ }
+
+ /**
+ * Set default dock apps based on common patterns
+ */
+ private fun updateDockWithDefaults(apps: List) {
+ if (_dockApps.value.all { it == null }) {
+ val defaultDock = mutableListOf()
+
+ // Try to find common apps for dock
+ val phoneApp = apps.find { it.packageName.contains("dialer") || it.packageName.contains("phone") }
+ val messageApp = apps.find { it.packageName.contains("messaging") || it.packageName.contains("sms") || it.packageName.contains("mms") }
+ val browserApp = apps.find { it.packageName.contains("chrome") || it.packageName.contains("browser") }
+ val cameraApp = apps.find { it.packageName.contains("camera") }
+ val settingsApp = apps.find { it.packageName.contains("settings") }
+
+ defaultDock.add(phoneApp)
+ defaultDock.add(messageApp)
+ defaultDock.add(browserApp)
+ defaultDock.add(cameraApp)
+ defaultDock.add(settingsApp)
+
+ // Pad to 5 items
+ while (defaultDock.size < 5) {
+ defaultDock.add(null)
+ }
+
+ _dockApps.value = defaultDock.take(5)
+ }
+ }
+
+ /**
+ * Select a category for smart sorting
+ */
+ fun selectCategory(category: AppCategory) {
+ _selectedCategory.value = category
+ _filteredApps.value = if (category == AppCategory.ALL) {
+ _apps.value
+ } else {
+ _apps.value.filter { it.category == category }
+ }
+ }
+
+ /**
+ * Update search query
+ */
+ fun updateSearchQuery(query: String) {
+ _searchQuery.value = query
+ _searchResults.value = if (query.isBlank()) {
+ emptyList()
+ } else {
+ appRepository.searchApps(query)
+ }
+ }
+
+ /**
+ * Toggle app drawer
+ */
+ fun toggleDrawer() {
+ _isDrawerOpen.value = !_isDrawerOpen.value
+ }
+
+ /**
+ * Open app drawer
+ */
+ fun openDrawer() {
+ _isDrawerOpen.value = true
+ }
+
+ /**
+ * Close app drawer
+ */
+ fun closeDrawer() {
+ _isDrawerOpen.value = false
+ _searchQuery.value = ""
+ _searchResults.value = emptyList()
+ }
+
+ /**
+ * Toggle search mode
+ */
+ fun toggleSearch() {
+ _isSearchActive.value = !_isSearchActive.value
+ if (!_isSearchActive.value) {
+ _searchQuery.value = ""
+ _searchResults.value = emptyList()
+ }
+ }
+
+ /**
+ * Launch an app
+ */
+ fun launchApp(packageName: String) {
+ appRepository.launchApp(packageName)
+ }
+
+ /**
+ * Launch app by action intent (Phase 3 - Neon Dock support)
+ * Used for fixed dock icons: Messages, Camera, etc.
+ */
+ fun launchAppByAction(action: String, data: String? = null) {
+ try {
+ val intent = Intent(action).apply {
+ if (data != null) {
+ setData(Uri.parse(data))
+ }
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ }
+ context.startActivity(intent)
+ } catch (e: Exception) {
+ // Fallback: try to find an app that handles this action
+ e.printStackTrace()
+ }
+ }
+
+ /**
+ * Add app to dock at position
+ */
+ fun addToDock(app: AppInfo, position: Int) {
+ if (position in 0..4) {
+ val newDock = _dockApps.value.toMutableList()
+ newDock[position] = app
+ _dockApps.value = newDock
+ }
+ }
+
+ /**
+ * Remove app from dock at position
+ */
+ fun removeFromDock(position: Int) {
+ if (position in 0..4) {
+ val newDock = _dockApps.value.toMutableList()
+ newDock[position] = null
+ _dockApps.value = newDock
+ }
+ }
+
+ /**
+ * Refresh app list
+ */
+ fun refreshApps() {
+ viewModelScope.launch {
+ appRepository.refresh()
+ }
+ }
+}
diff --git a/blackglass-launcher/app/src/main/res/drawable/ic_launcher_background.xml b/blackglass-launcher/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..69a9e3b1
--- /dev/null
+++ b/blackglass-launcher/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/blackglass-launcher/app/src/main/res/drawable/ic_launcher_foreground.xml b/blackglass-launcher/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 00000000..754317f5
--- /dev/null
+++ b/blackglass-launcher/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
diff --git a/blackglass-launcher/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/blackglass-launcher/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..d378acd7
--- /dev/null
+++ b/blackglass-launcher/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/blackglass-launcher/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/blackglass-launcher/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..d378acd7
--- /dev/null
+++ b/blackglass-launcher/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/blackglass-launcher/app/src/main/res/values/colors.xml b/blackglass-launcher/app/src/main/res/values/colors.xml
new file mode 100644
index 00000000..ecd8d390
--- /dev/null
+++ b/blackglass-launcher/app/src/main/res/values/colors.xml
@@ -0,0 +1,30 @@
+
+
+
+ #FF1A1A2E
+ #FFDC143C
+ #FFFF4D6D
+
+
+ #FF16213E
+ #FF9B59B6
+ #FFDA70D6
+
+
+ #1A0D0D0D
+ #330D0D0D
+ #4D1A1A1A
+
+
+ #FF00D9FF
+ #FFFFD700
+
+
+ #FFFFFFFF
+ #B3FFFFFF
+ #66FFFFFF
+
+
+ #00000000
+ #99000000
+
diff --git a/blackglass-launcher/app/src/main/res/values/strings.xml b/blackglass-launcher/app/src/main/res/values/strings.xml
new file mode 100644
index 00000000..30b51b3b
--- /dev/null
+++ b/blackglass-launcher/app/src/main/res/values/strings.xml
@@ -0,0 +1,15 @@
+
+
+ BlackGlass
+ Search apps, contacts, webโฆ
+ All Apps
+ Social
+ Media
+ Games
+ Productivity
+ Utilities
+ Settings
+ No apps found
+ Wallpaper
+ Widgets
+
diff --git a/blackglass-launcher/app/src/main/res/values/themes.xml b/blackglass-launcher/app/src/main/res/values/themes.xml
new file mode 100644
index 00000000..7fac3ee2
--- /dev/null
+++ b/blackglass-launcher/app/src/main/res/values/themes.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/blackglass-launcher/build.gradle.kts b/blackglass-launcher/build.gradle.kts
new file mode 100644
index 00000000..975e48a8
--- /dev/null
+++ b/blackglass-launcher/build.gradle.kts
@@ -0,0 +1,5 @@
+// Top-level build file for BlackGlass UI OS Launcher
+plugins {
+ id("com.android.application") version "8.2.2" apply false
+ id("org.jetbrains.kotlin.android") version "1.9.22" apply false
+}
diff --git a/blackglass-launcher/build.sh b/blackglass-launcher/build.sh
new file mode 100644
index 00000000..deb51998
--- /dev/null
+++ b/blackglass-launcher/build.sh
@@ -0,0 +1,55 @@
+#!/bin/bash
+# BlackGlass Launcher Build Script
+
+echo "๐ค BlackGlass UI OS - Build Script"
+echo "=================================="
+
+# Check for required tools
+if ! command -v java &> /dev/null; then
+ echo "โ Java not found. Please install JDK 17+"
+ exit 1
+fi
+
+# Navigate to project directory
+cd "$(dirname "$0")"
+
+# Make gradlew executable
+chmod +x gradlew 2>/dev/null || true
+
+# Check if gradlew exists, if not use system gradle
+if [ -f "./gradlew" ]; then
+ GRADLE="./gradlew"
+else
+ GRADLE="gradle"
+fi
+
+# Parse command
+case "$1" in
+ "build")
+ echo "๐ฆ Building debug APK..."
+ $GRADLE assembleDebug
+ echo "โ
APK built at: app/build/outputs/apk/debug/app-debug.apk"
+ ;;
+ "release")
+ echo "๐ฆ Building release APK..."
+ $GRADLE assembleRelease
+ echo "โ
APK built at: app/build/outputs/apk/release/app-release.apk"
+ ;;
+ "install")
+ echo "๐ฑ Installing on connected device..."
+ $GRADLE installDebug
+ ;;
+ "clean")
+ echo "๐งน Cleaning build files..."
+ $GRADLE clean
+ ;;
+ *)
+ echo "Usage: ./build.sh [build|release|install|clean]"
+ echo ""
+ echo "Commands:"
+ echo " build - Build debug APK"
+ echo " release - Build release APK"
+ echo " install - Install debug APK on connected device"
+ echo " clean - Clean build directory"
+ ;;
+esac
diff --git a/blackglass-launcher/gradle.properties b/blackglass-launcher/gradle.properties
new file mode 100644
index 00000000..47562894
--- /dev/null
+++ b/blackglass-launcher/gradle.properties
@@ -0,0 +1,5 @@
+# Project-wide Gradle settings
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+android.useAndroidX=true
+kotlin.code.style=official
+android.nonTransitiveRClass=true
diff --git a/blackglass-launcher/gradle/wrapper/gradle-wrapper.jar b/blackglass-launcher/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..f8e1ee31
Binary files /dev/null and b/blackglass-launcher/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/blackglass-launcher/gradle/wrapper/gradle-wrapper.properties b/blackglass-launcher/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..1af9e093
--- /dev/null
+++ b/blackglass-launcher/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/blackglass-launcher/gradlew b/blackglass-launcher/gradlew
new file mode 100755
index 00000000..adff685a
--- /dev/null
+++ b/blackglass-launcher/gradlew
@@ -0,0 +1,248 @@
+#!/bin/sh
+
+#
+# Copyright ยฉ 2015 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions ยซ$varยป, ยซ${var}ยป, ยซ${var:-default}ยป, ยซ${var+SET}ยป,
+# ยซ${var#prefix}ยป, ยซ${var%suffix}ยป, and ยซ$( cmd )ยป;
+# * compound commands having a testable exit status, especially ยซcaseยป;
+# * various built-in commands including ยซcommandยป, ยซsetยป, and ยซulimitยป.
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/blackglass-launcher/gradlew.bat b/blackglass-launcher/gradlew.bat
new file mode 100644
index 00000000..e509b2dd
--- /dev/null
+++ b/blackglass-launcher/gradlew.bat
@@ -0,0 +1,93 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/blackglass-launcher/settings.gradle.kts b/blackglass-launcher/settings.gradle.kts
new file mode 100644
index 00000000..5bb5f2dd
--- /dev/null
+++ b/blackglass-launcher/settings.gradle.kts
@@ -0,0 +1,18 @@
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "BlackGlass Launcher"
+include(":app")