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 + +

+ BlackGlass Logo +

+ +> **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")