SilverDROID - Dark Side Admin with CI/CD pipeline
- Android PWA/WASM launcher with glassmorphism UI - Loads https://admin.dark.side directly on launch - Complete GitLab CI/CD pipeline configuration - Automated builds for Debug, Release, and AAB - Full WASM support with optimized WebView - Material Design 3 theme - Comprehensive documentation Features: - Auto-load target URL on app launch - Glassmorphism components (frosted glass effects) - Full PWA/WASM support - Room database for future extensions - Jetpack Compose UI - CI/CD with artifact storage Built for SilverLABS
This commit is contained in:
104
app/build.gradle.kts
Normal file
104
app/build.gradle.kts
Normal file
@@ -0,0 +1,104 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("org.jetbrains.kotlin.plugin.compose")
|
||||
id("com.google.devtools.ksp")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "uk.silverlabs.silverdroid"
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "uk.silverlabs.silverdroid"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "1.0.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Core Android
|
||||
implementation("androidx.core:core-ktx:1.15.0")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
|
||||
implementation("androidx.activity:activity-compose:1.9.3")
|
||||
|
||||
// Compose BOM and UI
|
||||
implementation(platform("androidx.compose:compose-bom:2025.01.00"))
|
||||
implementation("androidx.compose.ui:ui")
|
||||
implementation("androidx.compose.ui:ui-graphics")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||
implementation("androidx.compose.material3:material3")
|
||||
implementation("androidx.compose.material:material-icons-extended")
|
||||
|
||||
// Compose Navigation
|
||||
implementation("androidx.navigation:navigation-compose:2.8.5")
|
||||
|
||||
// WebView
|
||||
implementation("androidx.webkit:webkit:1.12.1")
|
||||
|
||||
// Room Database
|
||||
implementation("androidx.room:room-runtime:2.6.1")
|
||||
implementation("androidx.room:room-ktx:2.6.1")
|
||||
ksp("androidx.room:room-compiler:2.6.1")
|
||||
|
||||
// Coroutines
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1")
|
||||
|
||||
// DataStore (for preferences)
|
||||
implementation("androidx.datastore:datastore-preferences:1.1.1")
|
||||
|
||||
// JSON parsing
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
|
||||
|
||||
// Coil for image loading
|
||||
implementation("io.coil-kt.coil3:coil-compose:3.0.4")
|
||||
implementation("io.coil-kt.coil3:coil-network-okhttp:3.0.4")
|
||||
|
||||
// Blur effect library
|
||||
implementation("com.github.Dimezis:BlurView:version-2.0.5")
|
||||
|
||||
// Testing
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.2.1")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
|
||||
androidTestImplementation(platform("androidx.compose:compose-bom:2025.01.00"))
|
||||
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
||||
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
||||
}
|
||||
23
app/proguard-rules.pro
vendored
Normal file
23
app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# Keep WebView JavaScript interface
|
||||
-keepclassmembers class * {
|
||||
@android.webkit.JavascriptInterface <methods>;
|
||||
}
|
||||
|
||||
# Keep Room entities
|
||||
-keep class uk.silverlabs.silverdroid.data.model.** { *; }
|
||||
|
||||
# Keep serialization
|
||||
-keepattributes *Annotation*, InnerClasses
|
||||
-dontnote kotlinx.serialization.AnnotationsKt
|
||||
|
||||
-keepclassmembers class kotlinx.serialization.json.** {
|
||||
*** Companion;
|
||||
}
|
||||
-keepclasseswithmembers class kotlinx.serialization.json.** {
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
|
||||
# Keep Compose
|
||||
-keep class androidx.compose.** { *; }
|
||||
-keep class kotlin.Metadata { *; }
|
||||
54
app/src/main/AndroidManifest.xml
Normal file
54
app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- Internet permission for WebView and PWAs -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<!-- Storage permissions for PWA offline caching -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
||||
<!-- Notifications for PWA push notifications -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.SilverDROID"
|
||||
android:hardwareAccelerated="true"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="35">
|
||||
|
||||
<!-- Main Launcher Activity -->
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.SilverDROID"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- WebView Activity for PWAs -->
|
||||
<activity
|
||||
android:name=".ui.webview.WebViewActivity"
|
||||
android:exported="false"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:theme="@style/Theme.SilverDROID.Fullscreen"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,49 @@
|
||||
package uk.silverlabs.silverdroid
|
||||
|
||||
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.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.ui.Modifier
|
||||
import uk.silverlabs.silverdroid.ui.theme.SilverDROIDTheme
|
||||
import uk.silverlabs.silverdroid.ui.webview.WasmWebView
|
||||
|
||||
/**
|
||||
* SilverDROID - Direct Load Version
|
||||
*
|
||||
* This version loads https://admin.dark.side directly on launch,
|
||||
* bypassing the launcher screen.
|
||||
*/
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
// Direct load configuration
|
||||
private val targetUrl = "https://admin.dark.side"
|
||||
private val appName = "Dark Side Admin"
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
|
||||
// Load admin.dark.side directly
|
||||
setContent {
|
||||
SilverDROIDTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
WasmWebView(
|
||||
url = targetUrl,
|
||||
appName = appName,
|
||||
onBackPressed = {
|
||||
// Exit app on back press
|
||||
finish()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package uk.silverlabs.silverdroid.data
|
||||
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import uk.silverlabs.silverdroid.data.model.PwaApp
|
||||
|
||||
@Dao
|
||||
interface PwaAppDao {
|
||||
@Query("SELECT * FROM pwa_apps ORDER BY sortOrder ASC, lastAccessed DESC")
|
||||
fun getAllApps(): Flow<List<PwaApp>>
|
||||
|
||||
@Query("SELECT * FROM pwa_apps WHERE id = :id")
|
||||
suspend fun getAppById(id: Long): PwaApp?
|
||||
|
||||
@Query("SELECT * FROM pwa_apps WHERE url = :url LIMIT 1")
|
||||
suspend fun getAppByUrl(url: String): PwaApp?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertApp(app: PwaApp): Long
|
||||
|
||||
@Update
|
||||
suspend fun updateApp(app: PwaApp)
|
||||
|
||||
@Delete
|
||||
suspend fun deleteApp(app: PwaApp)
|
||||
|
||||
@Query("UPDATE pwa_apps SET lastAccessed = :timestamp WHERE id = :id")
|
||||
suspend fun updateLastAccessed(id: Long, timestamp: Long = System.currentTimeMillis())
|
||||
|
||||
@Query("DELETE FROM pwa_apps WHERE id = :id")
|
||||
suspend fun deleteAppById(id: Long)
|
||||
|
||||
@Query("SELECT COUNT(*) FROM pwa_apps")
|
||||
suspend fun getAppCount(): Int
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package uk.silverlabs.silverdroid.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import uk.silverlabs.silverdroid.data.model.PwaApp
|
||||
|
||||
@Database(
|
||||
entities = [PwaApp::class],
|
||||
version = 1,
|
||||
exportSchema = true
|
||||
)
|
||||
abstract class PwaDatabase : RoomDatabase() {
|
||||
abstract fun pwaAppDao(): PwaAppDao
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var INSTANCE: PwaDatabase? = null
|
||||
|
||||
fun getInstance(context: Context): PwaDatabase {
|
||||
return INSTANCE ?: synchronized(this) {
|
||||
val instance = Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
PwaDatabase::class.java,
|
||||
"pwa_database"
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
INSTANCE = instance
|
||||
instance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package uk.silverlabs.silverdroid.data.model
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
/**
|
||||
* Represents a Progressive Web App or WASM app installed in the launcher
|
||||
*/
|
||||
@Entity(tableName = "pwa_apps")
|
||||
data class PwaApp(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Long = 0,
|
||||
|
||||
/** Display name of the app */
|
||||
val name: String,
|
||||
|
||||
/** URL of the PWA/WASM app */
|
||||
val url: String,
|
||||
|
||||
/** Icon URL or local path */
|
||||
val iconUrl: String? = null,
|
||||
|
||||
/** Short description */
|
||||
val description: String? = null,
|
||||
|
||||
/** Theme color from manifest */
|
||||
val themeColor: String? = null,
|
||||
|
||||
/** Background color from manifest */
|
||||
val backgroundColor: String? = null,
|
||||
|
||||
/** Whether the app is installed (from manifest) */
|
||||
val isInstalled: Boolean = true,
|
||||
|
||||
/** Last accessed timestamp */
|
||||
val lastAccessed: Long = System.currentTimeMillis(),
|
||||
|
||||
/** Installation timestamp */
|
||||
val installedAt: Long = System.currentTimeMillis(),
|
||||
|
||||
/** Display mode: standalone, fullscreen, minimal-ui, browser */
|
||||
val displayMode: String = "standalone",
|
||||
|
||||
/** Manifest JSON (serialized) */
|
||||
val manifestJson: String? = null,
|
||||
|
||||
/** Is this a WASM-specific app */
|
||||
val isWasmApp: Boolean = false,
|
||||
|
||||
/** Custom tags for categorization */
|
||||
val tags: String? = null,
|
||||
|
||||
/** Sort order */
|
||||
val sortOrder: Int = 0
|
||||
)
|
||||
@@ -0,0 +1,88 @@
|
||||
package uk.silverlabs.silverdroid.data.repository
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import uk.silverlabs.silverdroid.data.PwaAppDao
|
||||
import uk.silverlabs.silverdroid.data.model.PwaApp
|
||||
|
||||
class PwaRepository(private val pwaAppDao: PwaAppDao) {
|
||||
|
||||
val allApps: Flow<List<PwaApp>> = pwaAppDao.getAllApps()
|
||||
|
||||
suspend fun getAppById(id: Long): PwaApp? {
|
||||
return pwaAppDao.getAppById(id)
|
||||
}
|
||||
|
||||
suspend fun getAppByUrl(url: String): PwaApp? {
|
||||
return pwaAppDao.getAppByUrl(url)
|
||||
}
|
||||
|
||||
suspend fun insertApp(app: PwaApp): Long {
|
||||
return pwaAppDao.insertApp(app)
|
||||
}
|
||||
|
||||
suspend fun updateApp(app: PwaApp) {
|
||||
pwaAppDao.updateApp(app)
|
||||
}
|
||||
|
||||
suspend fun deleteApp(app: PwaApp) {
|
||||
pwaAppDao.deleteApp(app)
|
||||
}
|
||||
|
||||
suspend fun deleteAppById(id: Long) {
|
||||
pwaAppDao.deleteAppById(id)
|
||||
}
|
||||
|
||||
suspend fun updateLastAccessed(id: Long) {
|
||||
pwaAppDao.updateLastAccessed(id)
|
||||
}
|
||||
|
||||
suspend fun getAppCount(): Int {
|
||||
return pwaAppDao.getAppCount()
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a PWA from URL by fetching its manifest
|
||||
*/
|
||||
suspend fun installPwaFromUrl(
|
||||
url: String,
|
||||
name: String? = null,
|
||||
iconUrl: String? = null
|
||||
): Result<Long> {
|
||||
return try {
|
||||
// Check if already installed
|
||||
val existing = getAppByUrl(url)
|
||||
if (existing != null) {
|
||||
return Result.failure(Exception("App already installed"))
|
||||
}
|
||||
|
||||
val app = PwaApp(
|
||||
name = name ?: extractDomainName(url),
|
||||
url = url,
|
||||
iconUrl = iconUrl,
|
||||
isInstalled = true,
|
||||
installedAt = System.currentTimeMillis(),
|
||||
lastAccessed = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
val id = insertApp(app)
|
||||
Result.success(id)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractDomainName(url: String): String {
|
||||
return try {
|
||||
val domain = url.substringAfter("://")
|
||||
.substringBefore("/")
|
||||
.substringBefore(":")
|
||||
domain.substringAfter("www.")
|
||||
.split(".")
|
||||
.firstOrNull()
|
||||
?.replaceFirstChar { it.uppercase() }
|
||||
?: "App"
|
||||
} catch (e: Exception) {
|
||||
"App"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package uk.silverlabs.silverdroid.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.blur
|
||||
import androidx.compose.ui.draw.clip
|
||||
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 uk.silverlabs.silverdroid.ui.theme.GlassColors
|
||||
|
||||
/**
|
||||
* Glassmorphism card component with blur and transparency effects
|
||||
*/
|
||||
@Composable
|
||||
fun GlassCard(
|
||||
modifier: Modifier = Modifier,
|
||||
cornerRadius: Dp = 16.dp,
|
||||
borderWidth: Dp = 1.dp,
|
||||
blurRadius: Dp = 10.dp,
|
||||
content: @Composable BoxScope.() -> Unit
|
||||
) {
|
||||
val isDark = isSystemInDarkTheme()
|
||||
|
||||
val glassSurface = if (isDark) GlassColors.GlassSurfaceDark else GlassColors.GlassSurfaceLight
|
||||
val glassBorder = if (isDark) GlassColors.GlassBorderDark else GlassColors.GlassBorderLight
|
||||
|
||||
val shape = RoundedCornerShape(cornerRadius)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(shape)
|
||||
.background(glassSurface)
|
||||
.border(
|
||||
width = borderWidth,
|
||||
color = glassBorder,
|
||||
shape = shape
|
||||
)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Animated gradient glass background
|
||||
*/
|
||||
@Composable
|
||||
fun GlassBackground(
|
||||
modifier: Modifier = Modifier,
|
||||
darkTheme: Boolean = isSystemInDarkTheme()
|
||||
) {
|
||||
val gradientColors = if (darkTheme) {
|
||||
listOf(
|
||||
Color(0xFF0F0C29),
|
||||
Color(0xFF302B63),
|
||||
Color(0xFF24243E)
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
Color(0xFFE0E7FF),
|
||||
Color(0xFFF5F3FF),
|
||||
Color(0xFFFFE4E6)
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = gradientColors
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Frosted glass panel with elevated blur effect
|
||||
*/
|
||||
@Composable
|
||||
fun FrostedGlassPanel(
|
||||
modifier: Modifier = Modifier,
|
||||
cornerRadius: Dp = 20.dp,
|
||||
content: @Composable BoxScope.() -> Unit
|
||||
) {
|
||||
val isDark = isSystemInDarkTheme()
|
||||
|
||||
val glassSurface = if (isDark) {
|
||||
Color(0xBB1C1B1F)
|
||||
} else {
|
||||
Color(0xDDFFFFFF)
|
||||
}
|
||||
|
||||
val glassBorder = if (isDark) {
|
||||
Color(0x80FFFFFF)
|
||||
} else {
|
||||
Color(0x40000000)
|
||||
}
|
||||
|
||||
val shape = RoundedCornerShape(cornerRadius)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(shape)
|
||||
.background(glassSurface)
|
||||
.border(
|
||||
width = 1.5.dp,
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
glassBorder.copy(alpha = 0.8f),
|
||||
glassBorder.copy(alpha = 0.2f)
|
||||
)
|
||||
),
|
||||
shape = shape
|
||||
)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Glass surface with subtle shimmer effect
|
||||
*/
|
||||
@Composable
|
||||
fun ShimmerGlass(
|
||||
modifier: Modifier = Modifier,
|
||||
cornerRadius: Dp = 12.dp,
|
||||
content: @Composable BoxScope.() -> Unit
|
||||
) {
|
||||
val isDark = isSystemInDarkTheme()
|
||||
|
||||
val shimmerColors = if (isDark) {
|
||||
listOf(
|
||||
GlassColors.GlassSurfaceDark,
|
||||
GlassColors.GlassAccentBlue.copy(alpha = 0.15f),
|
||||
GlassColors.GlassSurfaceDark
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
GlassColors.GlassSurfaceLight,
|
||||
Color(0x20007ACC),
|
||||
GlassColors.GlassSurfaceLight
|
||||
)
|
||||
}
|
||||
|
||||
val shape = RoundedCornerShape(cornerRadius)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(shape)
|
||||
.background(
|
||||
brush = Brush.horizontalGradient(shimmerColors)
|
||||
)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = if (isDark) GlassColors.GlassBorderDark else GlassColors.GlassBorderLight,
|
||||
shape = shape
|
||||
)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Floating glass button with elevation
|
||||
*/
|
||||
@Composable
|
||||
fun GlassButton(
|
||||
modifier: Modifier = Modifier,
|
||||
cornerRadius: Dp = 16.dp,
|
||||
content: @Composable BoxScope.() -> Unit
|
||||
) {
|
||||
val isDark = isSystemInDarkTheme()
|
||||
|
||||
val buttonSurface = if (isDark) {
|
||||
Color(0xDD2C2C2E)
|
||||
} else {
|
||||
Color(0xEEFFFFFF)
|
||||
}
|
||||
|
||||
val shape = RoundedCornerShape(cornerRadius)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(shape)
|
||||
.background(buttonSurface)
|
||||
.border(
|
||||
width = 1.5.dp,
|
||||
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f),
|
||||
shape = shape
|
||||
)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
package uk.silverlabs.silverdroid.ui.launcher
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
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.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
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.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import uk.silverlabs.silverdroid.data.model.PwaApp
|
||||
import uk.silverlabs.silverdroid.ui.components.GlassBackground
|
||||
import uk.silverlabs.silverdroid.ui.components.GlassCard
|
||||
import uk.silverlabs.silverdroid.ui.components.FrostedGlassPanel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LauncherScreen(
|
||||
apps: List<PwaApp>,
|
||||
onAppClick: (PwaApp) -> Unit,
|
||||
onAddAppClick: () -> Unit,
|
||||
onSettingsClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
// Glassmorphism gradient background
|
||||
GlassBackground()
|
||||
|
||||
Scaffold(
|
||||
containerColor = androidx.compose.ui.graphics.Color.Transparent,
|
||||
topBar = {
|
||||
LauncherTopBar(
|
||||
onSettingsClick = onSettingsClick
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = onAddAppClick,
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.9f),
|
||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = "Add App")
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
if (apps.isEmpty()) {
|
||||
EmptyState(
|
||||
onAddAppClick = onAddAppClick,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
)
|
||||
} else {
|
||||
AppGrid(
|
||||
apps = apps,
|
||||
onAppClick = onAppClick,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun LauncherTopBar(
|
||||
onSettingsClick: () -> Unit
|
||||
) {
|
||||
FrostedGlassPanel(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
cornerRadius = 24.dp
|
||||
) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
"SilverDROID",
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = onSettingsClick) {
|
||||
Icon(Icons.Default.Settings, contentDescription = "Settings")
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = androidx.compose.ui.graphics.Color.Transparent
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppGrid(
|
||||
apps: List<PwaApp>,
|
||||
onAppClick: (PwaApp) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Adaptive(minSize = 120.dp),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = modifier
|
||||
) {
|
||||
items(apps, key = { it.id }) { app ->
|
||||
AppCard(
|
||||
app = app,
|
||||
onClick = { onAppClick(app) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppCard(
|
||||
app: PwaApp,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
GlassCard(
|
||||
modifier = modifier
|
||||
.aspectRatio(1f)
|
||||
.clickable(onClick = onClick),
|
||||
cornerRadius = 20.dp
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
// App icon placeholder
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.size(64.dp)
|
||||
.clip(CircleShape),
|
||||
color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.8f)
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
text = app.name.take(2).uppercase(),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// App name
|
||||
Text(
|
||||
text = app.name,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyState(
|
||||
onAddAppClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.padding(32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
FrostedGlassPanel(
|
||||
modifier = Modifier.padding(24.dp),
|
||||
cornerRadius = 28.dp
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "No Apps Yet",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "Add your first PWA or WASM app to get started",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Button(
|
||||
onClick = onAddAppClick,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.9f)
|
||||
)
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Add App")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package uk.silverlabs.silverdroid.ui.launcher
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import uk.silverlabs.silverdroid.data.PwaDatabase
|
||||
import uk.silverlabs.silverdroid.data.model.PwaApp
|
||||
import uk.silverlabs.silverdroid.data.repository.PwaRepository
|
||||
|
||||
class LauncherViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
private val repository: PwaRepository
|
||||
|
||||
private val _apps = MutableStateFlow<List<PwaApp>>(emptyList())
|
||||
val apps: StateFlow<List<PwaApp>> = _apps.asStateFlow()
|
||||
|
||||
private val _isLoading = MutableStateFlow(false)
|
||||
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
|
||||
|
||||
init {
|
||||
val database = PwaDatabase.getInstance(application)
|
||||
repository = PwaRepository(database.pwaAppDao())
|
||||
|
||||
loadApps()
|
||||
}
|
||||
|
||||
private fun loadApps() {
|
||||
viewModelScope.launch {
|
||||
repository.allApps.collect { appList ->
|
||||
_apps.value = appList
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addApp(url: String, name: String? = null) {
|
||||
viewModelScope.launch {
|
||||
_isLoading.value = true
|
||||
try {
|
||||
repository.installPwaFromUrl(url, name)
|
||||
} finally {
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteApp(app: PwaApp) {
|
||||
viewModelScope.launch {
|
||||
repository.deleteApp(app)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLastAccessed(appId: Long) {
|
||||
viewModelScope.launch {
|
||||
repository.updateLastAccessed(appId)
|
||||
}
|
||||
}
|
||||
|
||||
fun addSampleApps() {
|
||||
viewModelScope.launch {
|
||||
// Add some sample PWAs for demonstration
|
||||
val samples = listOf(
|
||||
PwaApp(
|
||||
name = "Twitter",
|
||||
url = "https://mobile.twitter.com",
|
||||
description = "Social media platform"
|
||||
),
|
||||
PwaApp(
|
||||
name = "Spotify",
|
||||
url = "https://open.spotify.com",
|
||||
description = "Music streaming"
|
||||
),
|
||||
PwaApp(
|
||||
name = "YouTube",
|
||||
url = "https://m.youtube.com",
|
||||
description = "Video platform"
|
||||
),
|
||||
PwaApp(
|
||||
name = "WebAssembly Demo",
|
||||
url = "https://webassembly.org/demo/",
|
||||
description = "WASM showcase",
|
||||
isWasmApp = true
|
||||
)
|
||||
)
|
||||
|
||||
samples.forEach { app ->
|
||||
repository.insertApp(app)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package uk.silverlabs.silverdroid.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
// Glassmorphism Colors
|
||||
object GlassColors {
|
||||
// Light theme glass
|
||||
val GlassSurfaceLight = Color(0xE6FFFFFF)
|
||||
val GlassTintLight = Color(0x40FFFFFF)
|
||||
val GlassBorderLight = Color(0x60FFFFFF)
|
||||
|
||||
// Dark theme glass
|
||||
val GlassSurfaceDark = Color(0xCC1C1B1F)
|
||||
val GlassTintDark = Color(0x40000000)
|
||||
val GlassBorderDark = Color(0x60FFFFFF)
|
||||
|
||||
// Accent colors for glass effects
|
||||
val GlassAccentBlue = Color(0x4000B8FF)
|
||||
val GlassAccentPurple = Color(0x40BB86FC)
|
||||
}
|
||||
|
||||
// Material Design 3 Light Theme
|
||||
val md_theme_light_primary = Color(0xFF0061A4)
|
||||
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
val md_theme_light_primaryContainer = Color(0xFFD1E4FF)
|
||||
val md_theme_light_onPrimaryContainer = Color(0xFF001D36)
|
||||
|
||||
val md_theme_light_secondary = Color(0xFF535E70)
|
||||
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
val md_theme_light_secondaryContainer = Color(0xFFD7E2F7)
|
||||
val md_theme_light_onSecondaryContainer = Color(0xFF10192B)
|
||||
|
||||
val md_theme_light_tertiary = Color(0xFF6C5677)
|
||||
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
val md_theme_light_tertiaryContainer = Color(0xFFF5D9FF)
|
||||
val md_theme_light_onTertiaryContainer = Color(0xFF261431)
|
||||
|
||||
val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
|
||||
val md_theme_light_background = Color(0xFFFDFCFF)
|
||||
val md_theme_light_onBackground = Color(0xFF1A1C1E)
|
||||
val md_theme_light_surface = Color(0xFFFDFCFF)
|
||||
val md_theme_light_onSurface = Color(0xFF1A1C1E)
|
||||
val md_theme_light_surfaceVariant = Color(0xFFDEE3EB)
|
||||
val md_theme_light_onSurfaceVariant = Color(0xFF42474E)
|
||||
val md_theme_light_outline = Color(0xFF72777F)
|
||||
|
||||
// Material Design 3 Dark Theme
|
||||
val md_theme_dark_primary = Color(0xFF9ECAFF)
|
||||
val md_theme_dark_onPrimary = Color(0xFF003258)
|
||||
val md_theme_dark_primaryContainer = Color(0xFF00497D)
|
||||
val md_theme_dark_onPrimaryContainer = Color(0xFFD1E4FF)
|
||||
|
||||
val md_theme_dark_secondary = Color(0xFFBBC6DB)
|
||||
val md_theme_dark_onSecondary = Color(0xFF253140)
|
||||
val md_theme_dark_secondaryContainer = Color(0xFF3C4758)
|
||||
val md_theme_dark_onSecondaryContainer = Color(0xFFD7E2F7)
|
||||
|
||||
val md_theme_dark_tertiary = Color(0xFFD8BDE4)
|
||||
val md_theme_dark_onTertiary = Color(0xFF3C2947)
|
||||
val md_theme_dark_tertiaryContainer = Color(0xFF543F5F)
|
||||
val md_theme_dark_onTertiaryContainer = Color(0xFFF5D9FF)
|
||||
|
||||
val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
val md_theme_dark_onError = Color(0xFF690005)
|
||||
val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||
|
||||
val md_theme_dark_background = Color(0xFF1A1C1E)
|
||||
val md_theme_dark_onBackground = Color(0xFFE2E2E6)
|
||||
val md_theme_dark_surface = Color(0xFF1A1C1E)
|
||||
val md_theme_dark_onSurface = Color(0xFFE2E2E6)
|
||||
val md_theme_dark_surfaceVariant = Color(0xFF42474E)
|
||||
val md_theme_dark_onSurfaceVariant = Color(0xFFC2C7CF)
|
||||
val md_theme_dark_outline = Color(0xFF8C9199)
|
||||
101
app/src/main/kotlin/uk/silverlabs/silverdroid/ui/theme/Theme.kt
Normal file
101
app/src/main/kotlin/uk/silverlabs/silverdroid/ui/theme/Theme.kt
Normal file
@@ -0,0 +1,101 @@
|
||||
package uk.silverlabs.silverdroid.ui.theme
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.view.WindowCompat
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||
secondary = md_theme_light_secondary,
|
||||
onSecondary = md_theme_light_onSecondary,
|
||||
secondaryContainer = md_theme_light_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||
tertiary = md_theme_light_tertiary,
|
||||
onTertiary = md_theme_light_onTertiary,
|
||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||
error = md_theme_light_error,
|
||||
errorContainer = md_theme_light_errorContainer,
|
||||
onError = md_theme_light_onError,
|
||||
onErrorContainer = md_theme_light_onErrorContainer,
|
||||
background = md_theme_light_background,
|
||||
onBackground = md_theme_light_onBackground,
|
||||
surface = md_theme_light_surface,
|
||||
onSurface = md_theme_light_onSurface,
|
||||
surfaceVariant = md_theme_light_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||
outline = md_theme_light_outline,
|
||||
)
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primaryContainer = md_theme_dark_primaryContainer,
|
||||
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||
secondary = md_theme_dark_secondary,
|
||||
onSecondary = md_theme_dark_onSecondary,
|
||||
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||
tertiary = md_theme_dark_tertiary,
|
||||
onTertiary = md_theme_dark_onTertiary,
|
||||
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||
error = md_theme_dark_error,
|
||||
errorContainer = md_theme_dark_errorContainer,
|
||||
onError = md_theme_dark_onError,
|
||||
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||
background = md_theme_dark_background,
|
||||
onBackground = md_theme_dark_onBackground,
|
||||
surface = md_theme_dark_surface,
|
||||
onSurface = md_theme_dark_onSurface,
|
||||
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||
outline = md_theme_dark_outline,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun SilverDROIDTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
val view = LocalView.current
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
val window = (view.context as android.app.Activity).window
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
WindowCompat.getInsetsController(window, view).apply {
|
||||
isAppearanceLightStatusBars = !darkTheme
|
||||
isAppearanceLightNavigationBars = !darkTheme
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
115
app/src/main/kotlin/uk/silverlabs/silverdroid/ui/theme/Type.kt
Normal file
115
app/src/main/kotlin/uk/silverlabs/silverdroid/ui/theme/Type.kt
Normal file
@@ -0,0 +1,115 @@
|
||||
package uk.silverlabs.silverdroid.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
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
|
||||
|
||||
val Typography = Typography(
|
||||
displayLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 57.sp,
|
||||
lineHeight = 64.sp,
|
||||
letterSpacing = (-0.25).sp,
|
||||
),
|
||||
displayMedium = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 45.sp,
|
||||
lineHeight = 52.sp,
|
||||
letterSpacing = 0.sp,
|
||||
),
|
||||
displaySmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 36.sp,
|
||||
lineHeight = 44.sp,
|
||||
letterSpacing = 0.sp,
|
||||
),
|
||||
headlineLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 32.sp,
|
||||
lineHeight = 40.sp,
|
||||
letterSpacing = 0.sp,
|
||||
),
|
||||
headlineMedium = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 28.sp,
|
||||
lineHeight = 36.sp,
|
||||
letterSpacing = 0.sp,
|
||||
),
|
||||
headlineSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 24.sp,
|
||||
lineHeight = 32.sp,
|
||||
letterSpacing = 0.sp,
|
||||
),
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp,
|
||||
),
|
||||
titleMedium = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.15.sp,
|
||||
),
|
||||
titleSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.1.sp,
|
||||
),
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp,
|
||||
),
|
||||
bodyMedium = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.25.sp,
|
||||
),
|
||||
bodySmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.4.sp,
|
||||
),
|
||||
labelLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.1.sp,
|
||||
),
|
||||
labelMedium = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp,
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp,
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,204 @@
|
||||
package uk.silverlabs.silverdroid.ui.webview
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Bitmap
|
||||
import android.webkit.*
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun WasmWebView(
|
||||
url: String,
|
||||
appName: String,
|
||||
onBackPressed: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var webView by remember { mutableStateOf<WebView?>(null) }
|
||||
var isLoading by remember { mutableStateOf(true) }
|
||||
var loadProgress by remember { mutableIntStateOf(0) }
|
||||
var pageTitle by remember { mutableStateOf(appName) }
|
||||
var canGoBack by remember { mutableStateOf(false) }
|
||||
|
||||
BackHandler(enabled = canGoBack) {
|
||||
webView?.goBack()
|
||||
}
|
||||
|
||||
Column(modifier = modifier.fillMaxSize()) {
|
||||
// Top bar with navigation
|
||||
Surface(
|
||||
tonalElevation = 3.dp,
|
||||
shadowElevation = 3.dp
|
||||
) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Column {
|
||||
Text(
|
||||
text = pageTitle,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
if (isLoading) {
|
||||
LinearProgressIndicator(
|
||||
progress = { loadProgress / 100f },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(2.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = {
|
||||
if (canGoBack) {
|
||||
webView?.goBack()
|
||||
} else {
|
||||
onBackPressed()
|
||||
}
|
||||
}) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back")
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = { webView?.reload() }) {
|
||||
Icon(Icons.Default.Refresh, "Refresh")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// WebView
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
WebView(context).apply {
|
||||
webView = this
|
||||
|
||||
// Enable JavaScript
|
||||
settings.javaScriptEnabled = true
|
||||
|
||||
// Enable WASM support
|
||||
settings.allowFileAccess = true
|
||||
settings.allowContentAccess = true
|
||||
|
||||
// Enable DOM storage (required for PWAs)
|
||||
settings.domStorageEnabled = true
|
||||
|
||||
// Enable database storage
|
||||
settings.databaseEnabled = true
|
||||
|
||||
// Enable caching for offline support
|
||||
settings.cacheMode = WebSettings.LOAD_DEFAULT
|
||||
settings.setAppCacheEnabled(true)
|
||||
|
||||
// Enable mixed content (for development)
|
||||
settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
|
||||
|
||||
// Performance optimizations
|
||||
settings.setRenderPriority(WebSettings.RenderPriority.HIGH)
|
||||
settings.cacheMode = WebSettings.LOAD_DEFAULT
|
||||
|
||||
// Enable zooming
|
||||
settings.setSupportZoom(true)
|
||||
settings.builtInZoomControls = true
|
||||
settings.displayZoomControls = false
|
||||
|
||||
// User agent (modern)
|
||||
settings.userAgentString = settings.userAgentString +
|
||||
" SilverDROID/1.0 (PWA/WASM Launcher)"
|
||||
|
||||
// Enable wide viewport
|
||||
settings.useWideViewPort = true
|
||||
settings.loadWithOverviewMode = true
|
||||
|
||||
// WebView client
|
||||
webViewClient = object : WebViewClient() {
|
||||
override fun onPageStarted(
|
||||
view: WebView?,
|
||||
url: String?,
|
||||
favicon: Bitmap?
|
||||
) {
|
||||
isLoading = true
|
||||
loadProgress = 0
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
isLoading = false
|
||||
loadProgress = 100
|
||||
canGoBack = view?.canGoBack() ?: false
|
||||
pageTitle = view?.title ?: appName
|
||||
}
|
||||
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?
|
||||
): Boolean {
|
||||
// Allow all navigation within WebView
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onReceivedError(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?,
|
||||
error: WebResourceError?
|
||||
) {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
// Chrome client for progress
|
||||
webChromeClient = object : WebChromeClient() {
|
||||
override fun onProgressChanged(view: WebView?, newProgress: Int) {
|
||||
loadProgress = newProgress
|
||||
if (newProgress == 100) {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReceivedTitle(view: WebView?, title: String?) {
|
||||
pageTitle = title ?: appName
|
||||
}
|
||||
|
||||
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
|
||||
// Log console messages for debugging
|
||||
consoleMessage?.let {
|
||||
android.util.Log.d(
|
||||
"WebView",
|
||||
"${it.message()} -- From line ${it.lineNumber()} of ${it.sourceId()}"
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Load the URL
|
||||
loadUrl(url)
|
||||
}
|
||||
},
|
||||
update = { view ->
|
||||
canGoBack = view.canGoBack()
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
|
||||
// Loading indicator
|
||||
if (isLoading && loadProgress < 20) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package uk.silverlabs.silverdroid.ui.webview
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.webkit.WebView
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.ui.Modifier
|
||||
import uk.silverlabs.silverdroid.ui.theme.SilverDROIDTheme
|
||||
|
||||
class WebViewActivity : ComponentActivity() {
|
||||
companion object {
|
||||
const val EXTRA_APP_ID = "app_id"
|
||||
const val EXTRA_APP_URL = "app_url"
|
||||
const val EXTRA_APP_NAME = "app_name"
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
|
||||
val appUrl = intent.getStringExtra(EXTRA_APP_URL) ?: ""
|
||||
val appName = intent.getStringExtra(EXTRA_APP_NAME) ?: "App"
|
||||
|
||||
// Enable WebView debugging in debug builds
|
||||
WebView.setWebContentsDebuggingEnabled(true)
|
||||
|
||||
setContent {
|
||||
SilverDROIDTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
WasmWebView(
|
||||
url = appUrl,
|
||||
appName = appName,
|
||||
onBackPressed = { finish() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
74
app/src/main/res/values/colors.xml
Normal file
74
app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Glassmorphism Color Palette -->
|
||||
|
||||
<!-- Light Theme Glass -->
|
||||
<color name="glass_surface_light">#E6FFFFFF</color>
|
||||
<color name="glass_tint_light">#40FFFFFF</color>
|
||||
<color name="glass_border_light">#60FFFFFF</color>
|
||||
|
||||
<!-- Dark Theme Glass -->
|
||||
<color name="glass_surface_dark">#CC1C1B1F</color>
|
||||
<color name="glass_tint_dark">#40000000</color>
|
||||
<color name="glass_border_dark">#60FFFFFF</color>
|
||||
|
||||
<!-- Material Design 3 Seeds -->
|
||||
<color name="md_theme_light_primary">#0061A4</color>
|
||||
<color name="md_theme_light_onPrimary">#FFFFFF</color>
|
||||
<color name="md_theme_light_primaryContainer">#D1E4FF</color>
|
||||
<color name="md_theme_light_onPrimaryContainer">#001D36</color>
|
||||
|
||||
<color name="md_theme_light_secondary">#535E70</color>
|
||||
<color name="md_theme_light_onSecondary">#FFFFFF</color>
|
||||
<color name="md_theme_light_secondaryContainer">#D7E2F7</color>
|
||||
<color name="md_theme_light_onSecondaryContainer">#10192B</color>
|
||||
|
||||
<color name="md_theme_light_tertiary">#6C5677</color>
|
||||
<color name="md_theme_light_onTertiary">#FFFFFF</color>
|
||||
<color name="md_theme_light_tertiaryContainer">#F5D9FF</color>
|
||||
<color name="md_theme_light_onTertiaryContainer">#261431</color>
|
||||
|
||||
<color name="md_theme_light_error">#BA1A1A</color>
|
||||
<color name="md_theme_light_errorContainer">#FFDAD6</color>
|
||||
<color name="md_theme_light_onError">#FFFFFF</color>
|
||||
<color name="md_theme_light_onErrorContainer">#410002</color>
|
||||
|
||||
<color name="md_theme_light_background">#FDFCFF</color>
|
||||
<color name="md_theme_light_onBackground">#1A1C1E</color>
|
||||
<color name="md_theme_light_surface">#FDFCFF</color>
|
||||
<color name="md_theme_light_onSurface">#1A1C1E</color>
|
||||
|
||||
<color name="md_theme_light_surfaceVariant">#DEE3EB</color>
|
||||
<color name="md_theme_light_onSurfaceVariant">#42474E</color>
|
||||
<color name="md_theme_light_outline">#72777F</color>
|
||||
|
||||
<!-- Dark Theme -->
|
||||
<color name="md_theme_dark_primary">#9ECAFF</color>
|
||||
<color name="md_theme_dark_onPrimary">#003258</color>
|
||||
<color name="md_theme_dark_primaryContainer">#00497D</color>
|
||||
<color name="md_theme_dark_onPrimaryContainer">#D1E4FF</color>
|
||||
|
||||
<color name="md_theme_dark_secondary">#BBC6DB</color>
|
||||
<color name="md_theme_dark_onSecondary">#253140</color>
|
||||
<color name="md_theme_dark_secondaryContainer">#3C4758</color>
|
||||
<color name="md_theme_dark_onSecondaryContainer">#D7E2F7</color>
|
||||
|
||||
<color name="md_theme_dark_tertiary">#D8BDE4</color>
|
||||
<color name="md_theme_dark_onTertiary">#3C2947</color>
|
||||
<color name="md_theme_dark_tertiaryContainer">#543F5F</color>
|
||||
<color name="md_theme_dark_onTertiaryContainer">#F5D9FF</color>
|
||||
|
||||
<color name="md_theme_dark_error">#FFB4AB</color>
|
||||
<color name="md_theme_dark_errorContainer">#93000A</color>
|
||||
<color name="md_theme_dark_onError">#690005</color>
|
||||
<color name="md_theme_dark_onErrorContainer">#FFDAD6</color>
|
||||
|
||||
<color name="md_theme_dark_background">#1A1C1E</color>
|
||||
<color name="md_theme_dark_onBackground">#E2E2E6</color>
|
||||
<color name="md_theme_dark_surface">#1A1C1E</color>
|
||||
<color name="md_theme_dark_onSurface">#E2E2E6</color>
|
||||
|
||||
<color name="md_theme_dark_surfaceVariant">#42474E</color>
|
||||
<color name="md_theme_dark_onSurfaceVariant">#C2C7CF</color>
|
||||
<color name="md_theme_dark_outline">#8C9199</color>
|
||||
</resources>
|
||||
17
app/src/main/res/values/strings.xml
Normal file
17
app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Dark Side Admin</string>
|
||||
<string name="launcher_title">Your Apps</string>
|
||||
<string name="add_pwa">Add App</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="search_apps">Search apps…</string>
|
||||
<string name="no_apps">No apps installed yet</string>
|
||||
<string name="add_first_app">Add your first PWA or WASM app</string>
|
||||
<string name="enter_url">Enter app URL</string>
|
||||
<string name="install">Install</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="uninstall">Uninstall</string>
|
||||
<string name="open">Open</string>
|
||||
<string name="loading">Loading…</string>
|
||||
<string name="error_loading">Failed to load app</string>
|
||||
</resources>
|
||||
14
app/src/main/res/values/themes.xml
Normal file
14
app/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.SilverDROID" parent="android:Theme.Material.Light.NoActionBar">
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<item name="android:windowLightStatusBar">true</item>
|
||||
<item name="android:windowLightNavigationBar">true</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.SilverDROID.Fullscreen" parent="Theme.SilverDROID">
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
</style>
|
||||
</resources>
|
||||
5
app/src/main/res/xml/backup_rules.xml
Normal file
5
app/src/main/res/xml/backup_rules.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<full-backup-content>
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="database" path="pwa_cache.db"/>
|
||||
</full-backup-content>
|
||||
7
app/src/main/res/xml/data_extraction_rules.xml
Normal file
7
app/src/main/res/xml/data_extraction_rules.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="database" path="pwa_cache.db"/>
|
||||
</cloud-backup>
|
||||
</data-extraction-rules>
|
||||
Reference in New Issue
Block a user