├── .gitignore ├── LICENSE ├── PRIVACY ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── lint-baseline.xml ├── proguard-rules.pro └── src │ ├── androidMain │ ├── AndroidManifest.xml │ ├── kotlin │ │ ├── MainAccessibilityService.kt │ │ ├── MainActivity.kt │ │ ├── MainApplication.kt │ │ ├── MainService.kt │ │ ├── components │ │ │ ├── page │ │ │ │ └── permissions │ │ │ │ │ ├── PermissionsPage.android.kt │ │ │ │ │ └── items.kt │ │ │ └── wrapper │ │ │ │ ├── AndroidUniLocaleProvider.kt │ │ │ │ └── AndroidUniWindowCompat.kt │ │ ├── createAndroidLocal.kt │ │ ├── impl │ │ │ ├── ActionFeatureImpl.kt │ │ │ ├── ControlFeatureImpl.kt │ │ │ ├── CustomDimmerFacade.kt │ │ │ ├── CustomToastFacade.kt │ │ │ ├── EdgeTouchListener.kt │ │ │ ├── ImplLocal.kt │ │ │ └── launchEdgeViewJob.kt │ │ ├── receiver │ │ │ ├── BootCompleteBroadcastReceiver.kt │ │ │ └── ScreenOffBroadCastReceiver.kt │ │ └── scripts │ │ │ ├── init_log_facade.kt │ │ │ └── register_shutdown_hook.kt │ └── res │ │ ├── drawable │ │ ├── ic_launcher.png │ │ └── ic_sync.png │ │ ├── values │ │ └── strings.xml │ │ └── xml │ │ ├── accessibility_service.xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── commonMain │ └── kotlin │ ├── Declarations.kt │ ├── Local.kt │ ├── components │ ├── common │ │ └── preferences.kt │ ├── lib │ │ ├── MobileModel.kt │ │ ├── list.kt │ │ └── preferences.kt │ ├── page │ │ ├── about │ │ │ └── AboutPage.kt │ │ ├── edge_edit │ │ │ ├── EdgeEditPage.kt │ │ │ └── items.kt │ │ ├── edge_list │ │ │ └── EdgeListPage.kt │ │ ├── home │ │ │ ├── HomePage.kt │ │ │ └── items.kt │ │ ├── log │ │ │ └── LogPage.kt │ │ ├── permissions │ │ │ └── PermissionsPage.kt │ │ └── presets │ │ │ └── PresetsPage.kt │ ├── window │ │ └── main │ │ │ └── MainWindow.kt │ ├── wizard │ │ └── introduction │ │ │ └── IntroductionWizard.kt │ └── wrapper │ │ └── UniTheme.kt │ ├── data │ └── settings │ │ └── edge_data.kt │ ├── l10n │ ├── Declarations.kt │ ├── Strings.kt │ └── strings │ │ ├── ar.kt │ │ └── en.kt │ ├── module.kt │ ├── presets.kt │ ├── scripts │ └── createUniL10nState.kt │ ├── theme.kt │ └── util │ ├── flow.kt │ ├── kermit.kt │ ├── lang.kt │ └── lifecycle.kt ├── build.gradle.kts ├── fastlane └── metadata │ └── android │ └── en-US │ ├── full_description.txt │ ├── images │ ├── icon.png │ └── phoneScreenshots │ │ ├── Screenshot_20250403-084816_edgeseek.png │ │ ├── Screenshot_20250403-084826_edgeseek.png │ │ ├── Screenshot_20250403-084840_edgeseek.png │ │ ├── Screenshot_20250403-084856_edgeseek.png │ │ └── Screenshot_20250403-084922_edgeseek.png │ ├── short_description.txt │ └── title.txt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | .idea 4 | /local.properties 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | .cxx 16 | local.properties 17 | -------------------------------------------------------------------------------- /PRIVACY: -------------------------------------------------------------------------------- 1 | WE DON'T GET ANY KIND OF INFORMATION FROM THE USER OF OUR APPLICATION 2 | OUR APPLICATION IS DESIGNED TO HELP USER ACCESSIBILITY AND IT CAN BE 3 | ACHIEVED OFFLINE 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Edge Seek 2 | 3 | Edge Seek Project 4 | [Latest Version](https://github.com/LSafer/edgeseek/releases/latest) 5 | 6 | ### Screenshots 7 | 8 |

9 | 10 | 11 | 12 | 13 | 14 |

15 | 16 | ### Submit an Issue 17 | 18 | You can submit an issue to one of the following: 19 | 20 | - GitHub Issues https://github.com/LSafer/edgeseek/issues/ 21 | - Dev Email lsafer@cufy.org 22 | - Telegram https://t.me/edgeseek 23 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release 3 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | import org.jetbrains.kotlin.gradle.plugin.extraProperties 3 | 4 | plugins { 5 | alias(libs.plugins.kotlin.multiplatform) 6 | alias(libs.plugins.kotlin.serialization) 7 | alias(libs.plugins.kotlin.compose) 8 | alias(libs.plugins.jetbrains.compose) 9 | alias(libs.plugins.android.application) 10 | alias(libs.plugins.gmazzo.buildConfig) 11 | } 12 | 13 | buildConfig { 14 | className = "BuildConfig" 15 | packageName = "net.lsafer.edgeseek.app" 16 | useKotlinOutput { 17 | internalVisibility = false 18 | } 19 | buildConfigField( 20 | type = "kotlin.String", 21 | name = "VERSION", 22 | value = "\"$version\"" 23 | ) 24 | buildConfigField( 25 | type = "kotlin.Int", 26 | name = "VERSION_CODE", 27 | value = rootProject.extraProperties["version_code"].toString().toInt() 28 | ) 29 | } 30 | 31 | kotlin { 32 | jvmToolchain(17) 33 | 34 | androidTarget { 35 | compilerOptions { 36 | jvmTarget.set(JvmTarget.JVM_17) 37 | } 38 | } 39 | 40 | sourceSets { 41 | commonMain.dependencies { 42 | // ##### Official Dependencies ##### 43 | implementation(libs.kotlinx.serialization.json) 44 | implementation(libs.kotlinx.coroutines.core) 45 | implementation(libs.kotlinx.datetime) 46 | 47 | implementation(libs.okio) 48 | 49 | // ##### Builtin Dependencies ##### 50 | implementation(compose.runtime) 51 | implementation(compose.foundation) 52 | implementation(compose.material3) 53 | implementation(compose.material3AdaptiveNavigationSuite) 54 | implementation(compose.materialIconsExtended) 55 | implementation(compose.ui) 56 | implementation(compose.components.resources) 57 | implementation(compose.components.uiToolingPreview) 58 | 59 | // ##### Internal Dependencies ##### 60 | 61 | implementation(libs.extkt.json) 62 | 63 | implementation(libs.lsafer.sundry.compose) 64 | implementation(libs.lsafer.sundry.compose.adaptive) 65 | implementation(libs.lsafer.sundry.storage) 66 | 67 | // ##### Community Dependencies ##### 68 | 69 | implementation(libs.touchlab.kermit) 70 | 71 | // ##### ANDROID Dependencies ##### 72 | 73 | implementation(libs.androidx.lifecycle.runtime.compose) 74 | 75 | implementation(libs.androidx.lifecycle.viewmodel) 76 | implementation(libs.androidx.lifecycle.viewmodel.compose) 77 | 78 | implementation(libs.material3.adaptive) 79 | implementation(libs.material3.adaptive.layout) 80 | implementation(libs.material3.adaptive.navigation) 81 | 82 | } 83 | androidMain.dependencies { 84 | // ##### ANDROID Dependencies ##### 85 | 86 | implementation(libs.androidx.core.ktx) 87 | implementation(libs.androidx.activity.compose) 88 | implementation(libs.androidx.appcompat) 89 | implementation(libs.androidx.cardview) 90 | 91 | // ##### Community Dependencies ##### 92 | 93 | implementation(libs.godaddy.colorpickerCompose) 94 | } 95 | } 96 | } 97 | 98 | android { 99 | namespace = "net.lsafer.edgeseek.app" 100 | compileSdk = libs.versions.android.compileSdk.get().toInt() 101 | 102 | dependenciesInfo { 103 | // Disables dependency metadata when building APKs. 104 | includeInApk = false 105 | // Disables dependency metadata when building Android App Bundles. 106 | includeInBundle = false 107 | } 108 | lint { 109 | baseline = file("lint-baseline.xml") 110 | } 111 | defaultConfig { 112 | applicationId = rootProject.extraProperties["application_id"].toString() 113 | minSdk = libs.versions.android.minSdk.get().toInt() 114 | targetSdk = libs.versions.android.targetSdk.get().toInt() 115 | versionCode = rootProject.extraProperties["version_code"].toString().toInt() 116 | versionName = version.toString() 117 | } 118 | packaging { 119 | resources { 120 | excludes += "/META-INF/LICENSE.md" 121 | excludes += "/META-INF/LICENSE-notice.md" 122 | excludes += "/META-INF/INDEX.LIST" 123 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 124 | excludes += "/META-INF/groovy-release-info.properties" 125 | excludes += "/META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule" 126 | } 127 | } 128 | buildTypes { 129 | release { 130 | isMinifyEnabled = false 131 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 132 | } 133 | debug { 134 | applicationIdSuffix = ".debug" 135 | versionNameSuffix = "-debug" 136 | } 137 | } 138 | compileOptions { 139 | isCoreLibraryDesugaringEnabled = true 140 | 141 | sourceCompatibility = JavaVersion.VERSION_17 142 | targetCompatibility = JavaVersion.VERSION_17 143 | } 144 | buildFeatures { 145 | compose = true 146 | } 147 | } 148 | 149 | dependencies { 150 | coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5") 151 | } 152 | -------------------------------------------------------------------------------- /app/lint-baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 24 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | 42 | 43 | 44 | 45 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/MainAccessibilityService.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app 2 | 3 | import android.accessibilityservice.AccessibilityService 4 | import android.view.accessibility.AccessibilityEvent 5 | import kotlinx.coroutines.flow.MutableStateFlow 6 | 7 | class MainAccessibilityService : AccessibilityService() { 8 | companion object { 9 | val aliveState = MutableStateFlow(false) 10 | } 11 | 12 | override fun onServiceConnected() { 13 | super.onServiceConnected() 14 | aliveState.value = true 15 | } 16 | 17 | override fun onDestroy() { 18 | super.onDestroy() 19 | aliveState.value = false 20 | } 21 | 22 | override fun onAccessibilityEvent(event: AccessibilityEvent?) { 23 | // See https://github.com/LSafer/edgeseek/tree/ebeb6df678a4a1ff02e9ea24dccef12d2e6d4086 24 | } 25 | 26 | override fun onInterrupt() { 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/MainActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2022 LSafer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package net.lsafer.edgeseek.app 17 | 18 | import android.os.Build 19 | import android.os.Bundle 20 | import android.view.WindowManager 21 | import androidx.activity.ComponentActivity 22 | import androidx.activity.compose.setContent 23 | import androidx.compose.material.Surface 24 | import androidx.compose.material3.MaterialTheme 25 | import kotlinx.coroutines.launch 26 | import net.lsafer.edgeseek.app.MainApplication.Companion.globalLocal 27 | import net.lsafer.edgeseek.app.components.window.main.MainWindow 28 | import net.lsafer.edgeseek.app.components.wrapper.AndroidUniLocaleProvider 29 | import net.lsafer.edgeseek.app.components.wrapper.AndroidUniWindowCompat 30 | import net.lsafer.edgeseek.app.components.wrapper.UniTheme 31 | 32 | class MainActivity : ComponentActivity() { 33 | override fun onCreate(savedInstanceState: Bundle?) { 34 | super.onCreate(savedInstanceState) 35 | 36 | setContent { 37 | val local = globalLocal 38 | val activity = this@MainActivity 39 | 40 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) 41 | window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) 42 | 43 | AndroidUniLocaleProvider(local) { 44 | AndroidUniWindowCompat(local, activity) { 45 | UniTheme(local) { 46 | Surface(color = MaterialTheme.colorScheme.background) { 47 | MainWindow(local) 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | override fun onResume() { 56 | super.onResume() 57 | 58 | globalLocal.ioScope.launch { 59 | globalLocal.eventbus.emit(UniEvent.StartService) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/MainApplication.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2022 LSafer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package net.lsafer.edgeseek.app 17 | 18 | import android.app.Application 19 | import android.content.Intent 20 | import android.os.Build 21 | import kotlinx.coroutines.flow.filterIsInstance 22 | import kotlinx.coroutines.flow.launchIn 23 | import kotlinx.coroutines.flow.onEach 24 | import kotlinx.coroutines.runBlocking 25 | 26 | class MainApplication : Application() { 27 | companion object { 28 | lateinit var globalLocal: Local 29 | } 30 | 31 | override fun onCreate() { 32 | super.onCreate() 33 | 34 | globalLocal = runBlocking { 35 | createAndroidLocal(this@MainApplication) 36 | } 37 | 38 | globalLocal.eventbus 39 | .filterIsInstance() 40 | .onEach { 41 | val context = this@MainApplication 42 | 43 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) 44 | startForegroundService(Intent(context, MainService::class.java)) 45 | else 46 | startService(Intent(context, MainService::class.java)) 47 | } 48 | .launchIn(globalLocal.ioScope) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/MainService.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2022 LSafer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package net.lsafer.edgeseek.app 17 | 18 | import android.app.NotificationChannel 19 | import android.app.NotificationManager 20 | import android.app.Service 21 | import android.content.Intent 22 | import android.content.IntentFilter 23 | import android.content.res.Configuration 24 | import android.graphics.Point 25 | import android.os.Build 26 | import android.os.IBinder 27 | import android.view.WindowManager 28 | import androidx.core.app.NotificationCompat 29 | import androidx.core.content.getSystemService 30 | import kotlinx.coroutines.* 31 | import kotlinx.coroutines.flow.* 32 | import net.lsafer.edgeseek.app.MainApplication.Companion.globalLocal 33 | import net.lsafer.edgeseek.app.data.settings.EdgePos 34 | import net.lsafer.edgeseek.app.data.settings.EdgePosData 35 | import net.lsafer.edgeseek.app.data.settings.EdgeSide 36 | import net.lsafer.edgeseek.app.data.settings.EdgeSideData 37 | import net.lsafer.edgeseek.app.impl.CustomDimmerFacade 38 | import net.lsafer.edgeseek.app.impl.CustomToastFacade 39 | import net.lsafer.edgeseek.app.impl.ImplLocal 40 | import net.lsafer.edgeseek.app.impl.launchEdgeViewJob 41 | import net.lsafer.edgeseek.app.receiver.ScreenOffBroadCastReceiver 42 | import net.lsafer.sundry.storage.select 43 | 44 | class MainService : Service() { 45 | private val implLocal = ImplLocal() 46 | private var launchedEdgeViewJobsSubJobFlow = MutableSharedFlow(1) 47 | 48 | override fun onBind(intent: Intent?): IBinder? = null 49 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int = START_STICKY 50 | 51 | override fun onCreate() { 52 | super.onCreate() 53 | implLocal.local = globalLocal 54 | implLocal.context = this 55 | implLocal.defaultScope = CoroutineScope( 56 | Dispatchers.Default + SupervisorJob() + 57 | CoroutineExceptionHandler { _, e -> 58 | moduleLogger.e("Unhandled coroutine exception", e) 59 | } 60 | ) 61 | implLocal.toast = CustomToastFacade(this) 62 | implLocal.dimmer = CustomDimmerFacade(this) 63 | startForeground() 64 | 65 | implLocal.defaultScope.launch { 66 | val activated = implLocal.local.dataStore 67 | .select(PK_FLAG_ACTIVATED) 68 | .firstOrNull() 69 | ?: false 70 | 71 | if (!activated) { 72 | stopSelf() 73 | return@launch 74 | } 75 | 76 | launchReceiverJob() 77 | launchEdgeViewJobsSubJobsCleanupSubJob() 78 | launchEdgeViewJobsSubJob() 79 | launchSelfStopSubJob() 80 | } 81 | } 82 | 83 | override fun onDestroy() { 84 | super.onDestroy() 85 | implLocal.defaultScope.cancel() 86 | } 87 | 88 | override fun onConfigurationChanged(newConfig: Configuration) { 89 | super.onConfigurationChanged(newConfig) 90 | launchEdgeViewJobsSubJob() 91 | } 92 | 93 | @Suppress("DEPRECATION") 94 | private fun launchEdgeViewJobsSubJob() { 95 | val windowManager = getSystemService()!! 96 | val display = windowManager.defaultDisplay 97 | val displayRotation = display.rotation 98 | val displayDensityDpi = resources.displayMetrics.densityDpi 99 | 100 | var displayHeight: Int 101 | var displayWidth: Int 102 | 103 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 104 | displayWidth = windowManager.currentWindowMetrics.bounds.width() 105 | displayWidth -= windowManager.currentWindowMetrics.windowInsets.systemWindowInsetLeft 106 | displayWidth -= windowManager.currentWindowMetrics.windowInsets.systemWindowInsetRight 107 | displayHeight = windowManager.currentWindowMetrics.bounds.height() 108 | displayHeight -= windowManager.currentWindowMetrics.windowInsets.systemWindowInsetTop 109 | displayHeight -= windowManager.currentWindowMetrics.windowInsets.systemWindowInsetBottom 110 | } else { 111 | val displayRealSize = Point() 112 | @Suppress("DEPRECATION") 113 | display.getRealSize(displayRealSize) 114 | displayWidth = displayRealSize.x 115 | displayHeight = displayRealSize.y 116 | } 117 | 118 | val subJob = Job(implLocal.defaultScope.coroutineContext.job) 119 | 120 | implLocal.defaultScope.launch { 121 | launchedEdgeViewJobsSubJobFlow.emit(subJob) 122 | 123 | launch(subJob) { 124 | for (side in EdgeSide.entries) { 125 | val sideDataFlow = implLocal.local.dataStore 126 | .select(side.key) 127 | .map { it ?: EdgeSideData(side) } 128 | .distinctUntilChanged() 129 | 130 | for (pos in EdgePos.entries.filter { it.side == side }) { 131 | val posDataFlow = implLocal.local.dataStore 132 | .select(pos.key) 133 | .map { it ?: EdgePosData(pos) } 134 | .distinctUntilChanged() 135 | 136 | launchEdgeViewJob( 137 | implLocal = implLocal, 138 | windowManager = windowManager, 139 | displayRotation = displayRotation, 140 | displayHeight = displayHeight, 141 | displayWidth = displayWidth, 142 | displayDensityDpi = displayDensityDpi, 143 | sideDataFlow = sideDataFlow, 144 | posDataFlow = posDataFlow, 145 | ) 146 | } 147 | } 148 | } 149 | } 150 | } 151 | 152 | private fun launchEdgeViewJobsSubJobsCleanupSubJob() { 153 | launchedEdgeViewJobsSubJobFlow 154 | .runningReduce { oldJob, newJob -> 155 | oldJob.cancel() 156 | newJob 157 | } 158 | .launchIn(implLocal.defaultScope) 159 | } 160 | 161 | private fun launchSelfStopSubJob() { 162 | implLocal.local.dataStore 163 | .select(PK_FLAG_ACTIVATED) 164 | .onEach { if (it == null || !it) stopSelf() } 165 | .launchIn(implLocal.defaultScope) 166 | } 167 | 168 | private fun launchReceiverJob() { 169 | val screenOffReceiver = ScreenOffBroadCastReceiver(implLocal) 170 | 171 | registerReceiver(screenOffReceiver, IntentFilter(Intent.ACTION_SCREEN_OFF)) 172 | 173 | implLocal.defaultScope.coroutineContext.job.invokeOnCompletion { 174 | unregisterReceiver(screenOffReceiver) 175 | } 176 | } 177 | 178 | private fun startForeground() { 179 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 180 | val strings = implLocal.local.l10nState.value.strings 181 | val title = strings.stmt.foreground_noti_title 182 | val description = strings.stmt.foreground_noti_text 183 | 184 | val channel = NotificationChannel("main", title, NotificationManager.IMPORTANCE_MIN) 185 | channel.description = description 186 | this.getSystemService(NotificationManager::class.java) 187 | .createNotificationChannel(channel) 188 | val notification = NotificationCompat.Builder(this, channel.id) 189 | .setContentTitle(strings.stmt.foreground_noti_title) 190 | .setContentText(strings.stmt.foreground_noti_text) 191 | .setSmallIcon(R.drawable.ic_sync) 192 | .build() 193 | this.startForeground(1, notification) 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/components/page/permissions/PermissionsPage.android.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app.components.page.permissions 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Spacer 5 | import androidx.compose.foundation.layout.height 6 | import androidx.compose.foundation.rememberScrollState 7 | import androidx.compose.foundation.verticalScroll 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.unit.dp 11 | import net.lsafer.edgeseek.app.Local 12 | import net.lsafer.edgeseek.app.components.lib.ListDivider 13 | import net.lsafer.edgeseek.app.components.lib.ListHeader 14 | import net.lsafer.edgeseek.app.components.lib.ListSectionTitle 15 | import net.lsafer.edgeseek.app.l10n.strings 16 | 17 | @Composable 18 | actual fun PermissionsPageContent( 19 | local: Local, 20 | modifier: Modifier, 21 | ) { 22 | Column( 23 | Modifier 24 | .verticalScroll(rememberScrollState()) 25 | .then(modifier) 26 | ) { 27 | ListHeader(title = strings.stmt.page_permissions_heading) 28 | ListSectionTitle(title = strings.label.mandatory) 29 | 30 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { 31 | PermissionsPage_ListItem_allow_restricted_permissions() 32 | } 33 | 34 | PermissionsPage_ListItem_display_over_other_apps() 35 | PermissionsPage_ListItem_write_system_settings() 36 | 37 | ListDivider() 38 | ListSectionTitle(title = strings.label.additional) 39 | PermissionsPage_ListItem_ignore_battery_optimizations() 40 | PermissionsPage_ListItem_accessibility_service() 41 | 42 | Spacer(Modifier.height(50.dp)) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/components/page/permissions/items.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app.components.page.permissions 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.os.PowerManager 7 | import android.provider.Settings 8 | import androidx.compose.foundation.clickable 9 | import androidx.compose.foundation.layout.Column 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.material.icons.Icons 12 | import androidx.compose.material.icons.filled.Settings 13 | import androidx.compose.material3.* 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.collectAsState 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.runtime.remember 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.platform.LocalContext 20 | import androidx.compose.ui.unit.dp 21 | import androidx.lifecycle.compose.LocalLifecycleOwner 22 | import net.lsafer.edgeseek.app.MainAccessibilityService 23 | import net.lsafer.edgeseek.app.components.lib.SwitchPreferenceListItem 24 | import net.lsafer.edgeseek.app.l10n.strings 25 | import net.lsafer.edgeseek.app.util.observeAsState 26 | 27 | @Composable 28 | fun PermissionsPage_ListItem_allow_restricted_permissions(modifier: Modifier = Modifier) { 29 | val context = LocalContext.current 30 | 31 | val handleOnClick: () -> Unit = { 32 | val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) 33 | intent.data = Uri.parse("package:${context.packageName}") 34 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 35 | context.startActivity(intent) 36 | } 37 | val handleOnOpenTutorial: () -> Unit = { 38 | val uri = Uri.parse("https://www.youtube.com/watch?v=28TomZ9tztw") 39 | val intent = Intent(Intent.ACTION_VIEW, uri) 40 | context.startActivity(intent) 41 | } 42 | 43 | ListItem( 44 | modifier = Modifier 45 | .clickable(onClick = handleOnClick) 46 | .then(modifier), 47 | headlineContent = { Text(strings.stmt.restricted_permissions_headline) }, 48 | trailingContent = { 49 | IconButton(handleOnClick) { 50 | Icon(Icons.Default.Settings, strings.stmt.open_settings) 51 | } 52 | }, 53 | supportingContent = { 54 | Column { 55 | Text(strings.stmt.restricted_permissions_supporting) 56 | 57 | OutlinedButton(handleOnOpenTutorial, Modifier.padding(8.dp)) { 58 | Text(strings.stmt.watch_tutorial) 59 | } 60 | } 61 | } 62 | ) 63 | } 64 | 65 | @Composable 66 | fun PermissionsPage_ListItem_display_over_other_apps(modifier: Modifier = Modifier) { 67 | val context = LocalContext.current 68 | val lifecycleOwner = LocalLifecycleOwner.current 69 | val lifecycleState = lifecycleOwner.lifecycle.observeAsState() 70 | 71 | val isChecked = remember(lifecycleState) { 72 | Settings.canDrawOverlays(context) 73 | } 74 | 75 | val handleOnChange = { _: Boolean -> 76 | val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION) 77 | intent.data = Uri.parse("package:${context.packageName}") 78 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 79 | context.startActivity(intent) 80 | } 81 | 82 | SwitchPreferenceListItem( 83 | value = isChecked, 84 | onChange = handleOnChange, 85 | headline = strings.stmt.display_over_other_apps_headline, 86 | supporting = strings.stmt.display_over_other_apps_supporting, 87 | modifier = modifier, 88 | ) 89 | } 90 | 91 | @Composable 92 | fun PermissionsPage_ListItem_write_system_settings(modifier: Modifier = Modifier) { 93 | val context = LocalContext.current 94 | val lifecycleOwner = LocalLifecycleOwner.current 95 | val lifecycleState = lifecycleOwner.lifecycle.observeAsState() 96 | 97 | val isChecked = remember(lifecycleState) { 98 | Settings.System.canWrite(context) 99 | } 100 | 101 | val handleOnChange = { _: Boolean -> 102 | val intent = Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS) 103 | intent.data = Uri.parse("package:${context.packageName}") 104 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 105 | context.startActivity(intent) 106 | } 107 | 108 | SwitchPreferenceListItem( 109 | value = isChecked, 110 | onChange = handleOnChange, 111 | headline = strings.stmt.write_system_settings_headline, 112 | supporting = strings.stmt.write_system_settings_supporting, 113 | modifier = modifier, 114 | ) 115 | } 116 | 117 | @Composable 118 | fun PermissionsPage_ListItem_ignore_battery_optimizations(modifier: Modifier = Modifier) { 119 | val context = LocalContext.current 120 | val lifecycleOwner = LocalLifecycleOwner.current 121 | val lifecycleState = lifecycleOwner.lifecycle.observeAsState() 122 | 123 | val isChecked = remember(lifecycleState) { 124 | context.getSystemService(PowerManager::class.java) 125 | .isIgnoringBatteryOptimizations(context.packageName) 126 | } 127 | 128 | val handleOnChange = { _: Boolean -> 129 | @SuppressLint("BatteryLife") 130 | val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) 131 | intent.data = Uri.parse("package:${context.packageName}") 132 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 133 | context.startActivity(intent) 134 | } 135 | 136 | SwitchPreferenceListItem( 137 | value = isChecked, 138 | onChange = handleOnChange, 139 | headline = strings.stmt.ignore_battery_optimizations_headline, 140 | supporting = strings.stmt.ignore_battery_optimizations_supporting, 141 | modifier = modifier, 142 | ) 143 | } 144 | 145 | @Composable 146 | fun PermissionsPage_ListItem_accessibility_service(modifier: Modifier = Modifier) { 147 | val context = LocalContext.current 148 | 149 | val isChecked by MainAccessibilityService.aliveState.collectAsState(false) 150 | 151 | val handleOnChange = { _: Boolean -> 152 | val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) 153 | // intent.data = Uri.parse("package:${context.packageName}") 154 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 155 | context.startActivity(intent) 156 | } 157 | 158 | SwitchPreferenceListItem( 159 | value = isChecked, 160 | onChange = handleOnChange, 161 | headline = strings.stmt.accessibility_service_headline, 162 | supporting = strings.stmt.accessibility_service_supporting, 163 | modifier = modifier, 164 | ) 165 | } 166 | -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/components/wrapper/AndroidUniLocaleProvider.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app.components.wrapper 2 | 3 | import androidx.compose.runtime.* 4 | import androidx.compose.ui.platform.LocalLayoutDirection 5 | import net.lsafer.edgeseek.app.Local 6 | import net.lsafer.edgeseek.app.l10n.LocalStrings 7 | 8 | @Composable 9 | fun AndroidUniLocaleProvider( 10 | local: Local, 11 | content: @Composable () -> Unit, 12 | ) { 13 | val l10n by local.l10nState.collectAsState() 14 | 15 | LaunchedEffect(l10n.lang) { 16 | val juLocale = java.util.Locale.forLanguageTag(l10n.lang) 17 | java.util.Locale.setDefault(juLocale) 18 | } 19 | 20 | CompositionLocalProvider( 21 | LocalLayoutDirection provides l10n.dir, 22 | LocalStrings provides l10n.strings, 23 | ) { 24 | key(l10n.lang) { 25 | content() 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/components/wrapper/AndroidUniWindowCompat.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app.components.wrapper 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import androidx.activity.ComponentActivity 6 | import androidx.activity.SystemBarStyle 7 | import androidx.activity.compose.BackHandler 8 | import androidx.activity.enableEdgeToEdge 9 | import androidx.compose.foundation.gestures.detectTapGestures 10 | import androidx.compose.foundation.isSystemInDarkTheme 11 | import androidx.compose.foundation.layout.Box 12 | import androidx.compose.foundation.layout.fillMaxSize 13 | import androidx.compose.material3.SnackbarDuration 14 | import androidx.compose.material3.SnackbarResult 15 | import androidx.compose.runtime.* 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.input.pointer.pointerInput 18 | import androidx.compose.ui.platform.LocalFocusManager 19 | import kotlinx.coroutines.flow.filterNotNull 20 | import kotlinx.coroutines.launch 21 | import net.lsafer.edgeseek.app.* 22 | import net.lsafer.edgeseek.app.l10n.LocalStrings 23 | import net.lsafer.sundry.compose.simplenav.InMemorySimpleNavController 24 | import net.lsafer.sundry.compose.util.SubscribeEffect 25 | import net.lsafer.sundry.storage.select 26 | 27 | @Composable 28 | fun AndroidUniWindowCompat( 29 | local: Local, 30 | activity: ComponentActivity, 31 | content: @Composable () -> Unit, 32 | ) { 33 | val focusManager = LocalFocusManager.current 34 | 35 | val coroutineScope = rememberCoroutineScope() 36 | val strings = LocalStrings.current 37 | val isSystemDarkTheme = isSystemInDarkTheme() 38 | val uiColors by produceState(UI_COLORS_DEFAULT) { 39 | local.dataStore 40 | .select(PK_UI_COLORS) 41 | .filterNotNull() 42 | .collect { value = it } 43 | } 44 | 45 | fun onLeaveRequest() = coroutineScope.launch { 46 | val result = local.snackbar.showSnackbar( 47 | message = strings.stmt.exit_application_qm, 48 | actionLabel = strings.label.yes, 49 | withDismissAction = true, 50 | duration = SnackbarDuration.Short, 51 | ) 52 | 53 | if (result == SnackbarResult.ActionPerformed) 54 | activity.finish() 55 | } 56 | 57 | BackHandler { 58 | val nc = local.navController as InMemorySimpleNavController 59 | 60 | if (nc.state.value.position == 0) 61 | onLeaveRequest() 62 | else 63 | local.navController.back() 64 | } 65 | 66 | SubscribeEffect(local.eventbus) { event -> 67 | when (event) { 68 | is UniEvent.OpenUrlRequest -> { 69 | val uri = Uri.parse(event.url) 70 | val intent = Intent(Intent.ACTION_VIEW, uri) 71 | activity.startActivity(intent) 72 | } 73 | 74 | is UniEvent.FocusRequest -> { 75 | val intent = Intent(activity, activity.javaClass) 76 | intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) 77 | activity.startActivity(intent) 78 | } 79 | 80 | else -> {} 81 | } 82 | } 83 | 84 | LaunchedEffect(uiColors, isSystemDarkTheme) { 85 | val isDark = when (uiColors) { 86 | UI_COLORS_BLACK, UI_COLORS_DARK -> true 87 | UI_COLORS_LIGHT, UI_COLORS_WHITE -> false 88 | else -> isSystemDarkTheme 89 | } 90 | 91 | activity.enableEdgeToEdge( 92 | statusBarStyle = SystemBarStyle.auto( 93 | android.graphics.Color.TRANSPARENT, 94 | android.graphics.Color.TRANSPARENT, 95 | ) { isDark }, 96 | navigationBarStyle = SystemBarStyle.auto( 97 | android.graphics.Color.TRANSPARENT, 98 | android.graphics.Color.TRANSPARENT, 99 | ) { isDark }, 100 | ) 101 | } 102 | 103 | Box( 104 | modifier = Modifier 105 | .fillMaxSize() 106 | .pointerInput(Unit) { 107 | detectTapGestures { 108 | focusManager.clearFocus() 109 | } 110 | } 111 | ) { 112 | content() 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/createAndroidLocal.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app 2 | 3 | import android.app.Application 4 | import androidx.compose.material3.SnackbarHostState 5 | import kotlinx.coroutines.CoroutineExceptionHandler 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.SupervisorJob 8 | import kotlinx.coroutines.flow.MutableSharedFlow 9 | import kotlinx.coroutines.flow.first 10 | import kotlinx.datetime.Clock 11 | import kotlinx.datetime.TimeZone 12 | import net.lsafer.edgeseek.app.scripts.createUniL10nState 13 | import net.lsafer.edgeseek.app.scripts.init_log_facade 14 | import net.lsafer.edgeseek.app.scripts.register_shutdown_hook 15 | import net.lsafer.sundry.compose.simplenav.InMemorySimpleNavController 16 | import net.lsafer.sundry.compose.util.platformIODispatcher 17 | import net.lsafer.sundry.storage.createFileJsonObjectDataStore 18 | import net.lsafer.sundry.storage.select 19 | import okio.Path.Companion.toOkioPath 20 | import java.util.* 21 | import kotlin.random.Random 22 | 23 | suspend fun createAndroidLocal(application: Application): Local { 24 | val local = Local() 25 | local.dataDir = application.filesDir.toOkioPath() 26 | local.cacheDir = application.cacheDir.toOkioPath() 27 | 28 | local.clock = Clock.System 29 | local.timeZone = TimeZone.currentSystemDefault() 30 | local.random = Random.Default 31 | local.ioScope = CoroutineScope( 32 | platformIODispatcher + SupervisorJob() + 33 | CoroutineExceptionHandler { _, e -> 34 | moduleLogger.e("Unhandled coroutine exception", e) 35 | } 36 | ) 37 | 38 | local.eventbus = MutableSharedFlow() 39 | local.dataStore = createFileJsonObjectDataStore( 40 | file = local.dataDir.resolve("datastore.json").toFile(), 41 | coroutineScope = local.ioScope, 42 | ) 43 | local.l10nState = createUniL10nState( 44 | language = local.dataStore.select(PK_UI_LANG), 45 | defaultLanguage = Locale.getDefault().language, 46 | coroutineScope = local.ioScope, 47 | ) 48 | local.navController = InMemorySimpleNavController( 49 | InMemorySimpleNavController.State( 50 | entries = when { 51 | local.dataStore.select(PK_WIZ_INTRO).first() != true -> 52 | listOf(UniRoute.IntroductionWizard()) 53 | 54 | else -> 55 | listOf(UniRoute.HomePage) 56 | }, 57 | ), 58 | ) 59 | local.snackbar = SnackbarHostState() 60 | local.init_log_facade() 61 | local.register_shutdown_hook() 62 | 63 | return local 64 | } 65 | -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/impl/ActionFeatureImpl.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app.impl 2 | 3 | import co.touchlab.kermit.Logger 4 | import net.lsafer.edgeseek.app.data.settings.ActionFeature 5 | 6 | sealed class ActionFeatureImpl { 7 | companion object { 8 | private val logger = Logger.withTag(ActionFeatureImpl::class.qualifiedName!!) 9 | 10 | fun from(feature: ActionFeature): ActionFeatureImpl? { 11 | return when (feature) { 12 | ActionFeature.Nothing -> null 13 | ActionFeature.ExpandStatusBar -> ExpandStatusBar 14 | } 15 | } 16 | } 17 | 18 | abstract fun execute(implLocal: ImplLocal) 19 | 20 | data object ExpandStatusBar : ActionFeatureImpl() { 21 | override fun execute(implLocal: ImplLocal) { 22 | try { 23 | //noinspection JavaReflectionMemberAccess, WrongConstant 24 | Class.forName("android.app.StatusBarManager") 25 | .getMethod("expandNotificationsPanel") 26 | .invoke(implLocal.context.getSystemService("statusbar")) 27 | } catch (e: ReflectiveOperationException) { 28 | logger.e("Couldn't expand status bar", e) 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/impl/ControlFeatureImpl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2022 LSafer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package net.lsafer.edgeseek.app.impl 17 | 18 | import android.media.AudioManager 19 | import android.provider.Settings 20 | import androidx.core.content.getSystemService 21 | import co.touchlab.kermit.Logger 22 | import net.lsafer.edgeseek.app.data.settings.ControlFeature 23 | 24 | sealed class ControlFeatureImpl { 25 | companion object { 26 | private val logger = Logger.withTag(ControlFeatureImpl::class.qualifiedName!!) 27 | 28 | fun from(feature: ControlFeature): ControlFeatureImpl? { 29 | return when (feature) { 30 | ControlFeature.Nothing -> null 31 | ControlFeature.Brightness -> Brightness 32 | ControlFeature.BrightnessWithDimmer -> BrightnessWithDimmer 33 | ControlFeature.Music -> Audio.Music 34 | ControlFeature.Alarm -> Audio.Alarm 35 | ControlFeature.System -> Audio.System 36 | ControlFeature.Ring -> Audio.Ring 37 | } 38 | } 39 | } 40 | 41 | abstract fun fetchRange(implLocal: ImplLocal): IntRange 42 | open fun fetchStepRange(implLocal: ImplLocal, sign: Int) = fetchRange(implLocal) 43 | abstract fun fetchValue(implLocal: ImplLocal): Int 44 | abstract fun updateValue(implLocal: ImplLocal, newValue: Int, showSystemPanel: Boolean): Int 45 | 46 | data object Brightness : ControlFeatureImpl() { 47 | override fun fetchRange(implLocal: ImplLocal) = 0..255 48 | 49 | override fun fetchValue(implLocal: ImplLocal): Int { 50 | return Settings.System.getInt( 51 | implLocal.context.contentResolver, 52 | Settings.System.SCREEN_BRIGHTNESS, 53 | ) 54 | } 55 | 56 | override fun updateValue(implLocal: ImplLocal, newValue: Int, showSystemPanel: Boolean): Int { 57 | val newSystemValue = newValue.coerceIn(0..255) 58 | 59 | try { 60 | Settings.System.putInt( 61 | implLocal.context.contentResolver, 62 | Settings.System.SCREEN_BRIGHTNESS_MODE, 63 | Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL, 64 | ) 65 | Settings.System.putInt( 66 | implLocal.context.contentResolver, 67 | Settings.System.SCREEN_BRIGHTNESS, 68 | newSystemValue, 69 | ) 70 | implLocal.dimmer.update(0) 71 | return newSystemValue 72 | } catch (e: Exception) { 73 | logger.e("Couldn't update brightness level", e) 74 | return fetchValue(implLocal) 75 | } 76 | } 77 | } 78 | 79 | data object BrightnessWithDimmer : ControlFeatureImpl() { 80 | override fun fetchRange(implLocal: ImplLocal) = -255..255 81 | 82 | override fun fetchStepRange(implLocal: ImplLocal, sign: Int): IntRange { 83 | val value = fetchValue(implLocal) 84 | return when { 85 | value in 1..255 -> 0..255 86 | value in -255..-1 -> -255..0 87 | sign > 0 -> 0..255 88 | sign < 0 -> -255..0 89 | else -> -255..255 90 | } 91 | } 92 | 93 | override fun fetchValue(implLocal: ImplLocal): Int { 94 | val currentSystemValue = Settings.System.getInt( 95 | implLocal.context.contentResolver, 96 | Settings.System.SCREEN_BRIGHTNESS, 97 | ) 98 | val currentDimmerValue = implLocal.dimmer.currentValue 99 | return currentSystemValue - currentDimmerValue 100 | } 101 | 102 | override fun updateValue(implLocal: ImplLocal, newValue: Int, showSystemPanel: Boolean): Int { 103 | val newSystemValue = newValue.coerceIn(0..255) 104 | val newDimmerValue = -newValue.coerceIn(-255..0) 105 | 106 | try { 107 | Settings.System.putInt( 108 | implLocal.context.contentResolver, 109 | Settings.System.SCREEN_BRIGHTNESS_MODE, 110 | Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL, 111 | ) 112 | Settings.System.putInt( 113 | implLocal.context.contentResolver, 114 | Settings.System.SCREEN_BRIGHTNESS, 115 | newSystemValue, 116 | ) 117 | implLocal.dimmer.update(newDimmerValue) 118 | return newSystemValue - newDimmerValue 119 | } catch (e: Exception) { 120 | logger.e("Couldn't update brightness (with dimmer) level", e) 121 | return fetchValue(implLocal) 122 | } 123 | } 124 | } 125 | 126 | sealed class Audio(private val streamType: Int) : ControlFeatureImpl() { 127 | data object Alarm : Audio(AudioManager.STREAM_ALARM) 128 | data object Music : Audio(AudioManager.STREAM_MUSIC) 129 | data object Ring : Audio(AudioManager.STREAM_RING) 130 | data object System : Audio(AudioManager.STREAM_SYSTEM) 131 | 132 | override fun fetchRange(implLocal: ImplLocal): IntRange { 133 | val manager = implLocal.context.getSystemService()!! 134 | val maximumValue = manager.getStreamMaxVolume(streamType) 135 | return 0..maximumValue 136 | } 137 | 138 | override fun fetchValue(implLocal: ImplLocal): Int { 139 | val manager = implLocal.context.getSystemService()!! 140 | return manager.getStreamVolume(streamType) 141 | } 142 | 143 | override fun updateValue(implLocal: ImplLocal, newValue: Int, showSystemPanel: Boolean): Int { 144 | val manager = implLocal.context.getSystemService(AudioManager::class.java) 145 | val maximumValue = manager.getStreamMaxVolume(streamType) 146 | val newSystemValue = newValue.coerceIn(0..maximumValue) 147 | 148 | try { 149 | manager.setStreamVolume( 150 | streamType, 151 | newSystemValue, 152 | if (showSystemPanel) 153 | AudioManager.FLAG_SHOW_UI 154 | else 155 | AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE, 156 | ) 157 | return newSystemValue 158 | } catch (e: Exception) { 159 | logger.e("Couldn't update stream volume of type: $streamType", e) 160 | return fetchValue(implLocal) 161 | } 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/impl/CustomDimmerFacade.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app.impl 2 | 3 | import android.content.Context 4 | import android.graphics.Point 5 | import android.os.Build 6 | import android.view.View 7 | import android.view.WindowManager 8 | import androidx.core.content.getSystemService 9 | import co.touchlab.kermit.Logger 10 | 11 | class CustomDimmerFacade(context: Context) { 12 | companion object { 13 | private val logger = Logger.withTag(CustomDimmerFacade::class.qualifiedName!!) 14 | } 15 | 16 | private val windowManager = context.getSystemService()!! 17 | private val windowParams = WindowManager.LayoutParams() 18 | 19 | private var view = View(context) 20 | 21 | private var attached = false 22 | 23 | var currentValue: Int = 0 24 | private set 25 | 26 | init { 27 | @Suppress("DEPRECATION") 28 | windowParams.type = when { 29 | Build.VERSION.SDK_INT >= 26 -> 30 | WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY 31 | 32 | else -> 33 | WindowManager.LayoutParams.TYPE_PHONE 34 | } 35 | @Suppress("DEPRECATION") 36 | windowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or 37 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or 38 | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or 39 | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or 40 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or 41 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or 42 | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR or 43 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_OVERSCAN or 44 | WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS 45 | 46 | val windowSize = Point() 47 | @Suppress("DEPRECATION") 48 | windowManager.defaultDisplay.getSize(windowSize) 49 | windowParams.height = windowSize.y * 3 50 | windowParams.width = windowSize.x * 3 51 | } 52 | 53 | fun update(value: Int) { 54 | require(value in 0..255) { "Bad alpha value: $value" } 55 | 56 | currentValue = value 57 | 58 | if (value == 0) { 59 | if (attached) { 60 | runCatching { windowManager.removeView(view) } 61 | .onSuccess { attached = false } 62 | } 63 | } else { 64 | view.setBackgroundColor(android.graphics.Color.argb(255, 0, 0, 0)) 65 | view.alpha = value / 255f 66 | windowParams.alpha = value / 255f 67 | 68 | if (!attached) { 69 | runCatching { windowManager.addView(view, windowParams) } 70 | .onFailure { e -> logger.e("failed to attach dimmer to window", e) } 71 | .onSuccess { attached = true } 72 | } else { 73 | runCatching { windowManager.updateViewLayout(view, windowParams) } 74 | .onFailure { e -> logger.e("failed to update dimmer window params", e) } 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/impl/CustomToastFacade.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2022 LSafer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:SuppressLint("StaticFieldLeak") 18 | 19 | package net.lsafer.edgeseek.app.impl 20 | 21 | import android.annotation.SuppressLint 22 | import android.content.Context 23 | import android.os.Build 24 | import android.view.Gravity 25 | import android.view.WindowManager 26 | import android.widget.TextView 27 | import androidx.cardview.widget.CardView 28 | import androidx.core.content.getSystemService 29 | import co.touchlab.kermit.Logger 30 | import kotlinx.coroutines.CoroutineScope 31 | import kotlinx.coroutines.Dispatchers 32 | import kotlinx.coroutines.delay 33 | import kotlinx.coroutines.launch 34 | 35 | class CustomToastFacade(context: Context) { 36 | companion object { 37 | private val logger = Logger.withTag(CustomToastFacade::class.qualifiedName!!) 38 | } 39 | 40 | private val windowManager = context.getSystemService()!! 41 | private val windowParams = WindowManager.LayoutParams() 42 | 43 | private var containerView = CardView(context) 44 | private var textView = TextView(context) 45 | 46 | private var showId = 0 47 | private var attached = false 48 | 49 | init { 50 | @Suppress("DEPRECATION") 51 | windowParams.type = when { 52 | Build.VERSION.SDK_INT >= 26 -> 53 | WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY 54 | 55 | else -> 56 | WindowManager.LayoutParams.TYPE_PHONE 57 | } 58 | @Suppress("DEPRECATION") 59 | windowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or 60 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or 61 | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or 62 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or 63 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED 64 | windowParams.gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL 65 | windowParams.height = 100 66 | windowParams.width = 200 67 | windowParams.y = 50 68 | 69 | textView.gravity = Gravity.CENTER 70 | containerView.radius = 25f 71 | containerView.addView(textView) 72 | } 73 | 74 | fun update(text: String) { 75 | CoroutineScope(Dispatchers.Main).launch { 76 | if (!attached) { 77 | runCatching { windowManager.addView(containerView, windowParams) } 78 | .onFailure { e -> logger.e("failed to attach toast to window", e) } 79 | .onFailure { return@launch } 80 | .onSuccess { attached = true } 81 | } 82 | 83 | val expectedId = ++showId 84 | 85 | textView.text = text 86 | 87 | delay(2_000) 88 | 89 | if (expectedId == showId && attached) { 90 | runCatching { windowManager.removeView(containerView) } 91 | .onSuccess { attached = false } 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/impl/EdgeTouchListener.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app.impl 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Build 5 | import android.os.SystemClock 6 | import android.os.VibrationEffect 7 | import android.os.Vibrator 8 | import android.view.GestureDetector 9 | import android.view.MotionEvent 10 | import android.view.View 11 | import net.lsafer.edgeseek.app.data.settings.EdgePosData 12 | import net.lsafer.edgeseek.app.data.settings.EdgeSide 13 | import kotlin.math.abs 14 | 15 | class EdgeTouchListener( 16 | private val implLocal: ImplLocal, 17 | private val edgePosData: EdgePosData, 18 | private val edgeSide: EdgeSide, 19 | private val dpi: Int, 20 | 21 | private val onLongClick: ActionFeatureImpl?, 22 | private val onDoubleClick: ActionFeatureImpl?, 23 | 24 | private val onSeekImpl: ControlFeatureImpl?, 25 | private val onSwipeUp: ActionFeatureImpl?, 26 | private val onSwipeDown: ActionFeatureImpl?, 27 | private val onSwipeLeft: ActionFeatureImpl?, 28 | private val onSwipeRight: ActionFeatureImpl?, 29 | ) : View.OnTouchListener, GestureDetector.SimpleOnGestureListener() { 30 | private val detector = GestureDetector(implLocal.context, this) 31 | 32 | private val xSeekSensitivityFactor = 155f * dpi 33 | private val xSwipeThresholdDistant = 10f * dpi 34 | // private val xSeekSensitivityFactor = 80_000f 35 | // private val xSwipeThresholdDistant = 5_000f 36 | 37 | private val xSwipeEnabled = 38 | onSwipeUp != null || 39 | onSwipeDown != null || 40 | onSwipeLeft != null || 41 | onSwipeRight != null 42 | 43 | private var mCurrentOriginXOrY: Float? = null 44 | private var mCurrentOriginYOrX: Float? = null 45 | 46 | private var mCurrentSeekRange: IntRange? = null 47 | private var mCurrentSeekOrigin: Int? = null 48 | 49 | private var mIsScrolling: Boolean = false 50 | private var mIsDone: Boolean = false 51 | 52 | init { 53 | detector.setIsLongpressEnabled(onLongClick != null) 54 | } 55 | 56 | @SuppressLint("ClickableViewAccessibility") 57 | override fun onTouch(v: View, event: MotionEvent): Boolean { 58 | return detector.onTouchEvent(event).also { 59 | if (event.action == MotionEvent.ACTION_UP) { 60 | if (!mIsDone) doFeedbackVibration() 61 | } 62 | } 63 | } 64 | 65 | override fun onDown(e: MotionEvent): Boolean { 66 | mIsScrolling = false 67 | mIsDone = false 68 | mCurrentSeekRange = null 69 | mCurrentSeekOrigin = null 70 | mCurrentOriginXOrY = when (edgeSide) { 71 | EdgeSide.Left, EdgeSide.Right -> e.y 72 | EdgeSide.Top, EdgeSide.Bottom -> e.x 73 | } 74 | mCurrentOriginYOrX = when (edgeSide) { 75 | EdgeSide.Left, EdgeSide.Right -> e.x 76 | EdgeSide.Top, EdgeSide.Bottom -> e.y 77 | } 78 | 79 | if (onSeekImpl != null && !xSwipeEnabled) { 80 | mIsScrolling = true 81 | doFeedbackVibration() 82 | doFeedbackToast() 83 | } 84 | 85 | return true 86 | } 87 | 88 | override fun onDoubleTap(e: MotionEvent): Boolean { 89 | if (onDoubleClick != null) { 90 | mIsDone = true 91 | doFeedbackVibration() 92 | onDoubleClick.execute(implLocal) 93 | return true 94 | } 95 | 96 | return false 97 | } 98 | 99 | override fun onLongPress(e: MotionEvent) { 100 | if (onLongClick != null) { 101 | mIsDone = true 102 | doFeedbackVibration() 103 | onLongClick.execute(implLocal) 104 | } 105 | } 106 | 107 | override fun onShowPress(e: MotionEvent) { 108 | if (onSeekImpl != null && xSwipeEnabled) { 109 | mIsScrolling = true 110 | doFeedbackVibration() 111 | doFeedbackToast() 112 | } 113 | } 114 | 115 | override fun onScroll( 116 | e1: MotionEvent?, 117 | e2: MotionEvent, 118 | distanceX: Float, 119 | distanceY: Float, 120 | ): Boolean { 121 | e1 ?: return false 122 | onSeekImpl ?: return false 123 | if (mIsDone) return false 124 | 125 | if (!mIsScrolling) { 126 | val now = SystemClock.uptimeMillis() 127 | 128 | if (now - e2.downTime > 300L) { 129 | mIsScrolling = true 130 | doFeedbackVibration() 131 | doFeedbackToast() 132 | } else { 133 | return false 134 | } 135 | } 136 | 137 | val deltaXOrY = when (edgeSide) { 138 | EdgeSide.Left, EdgeSide.Right -> e1.y - e2.y 139 | EdgeSide.Top, EdgeSide.Bottom -> e1.x - e2.x 140 | } 141 | 142 | if (mCurrentSeekOrigin == null) { 143 | mCurrentSeekOrigin = onSeekImpl.fetchValue(implLocal) 144 | } 145 | 146 | if (mCurrentSeekRange == null) { 147 | mCurrentSeekRange = if (edgePosData.seekSteps) 148 | onSeekImpl.fetchStepRange(implLocal, deltaXOrY.toInt()) 149 | else 150 | onSeekImpl.fetchRange(implLocal) 151 | } 152 | 153 | val factor = xSeekSensitivityFactor / (mCurrentSeekRange!!.last - mCurrentSeekRange!!.first) 154 | val accBoost = if (edgePosData.seekAcceleration) abs(deltaXOrY / factor) else 1f 155 | val newValue = mCurrentSeekOrigin!! + ((deltaXOrY * accBoost) / factor * edgePosData.sensitivity).toInt() 156 | val newValueCoerced = newValue.coerceIn(mCurrentSeekRange!!) 157 | 158 | val value = onSeekImpl.updateValue(implLocal, newValueCoerced, edgePosData.feedbackSystemPanel) 159 | 160 | if (edgePosData.feedbackToast) { 161 | implLocal.toast.update("$value") 162 | } 163 | 164 | return false 165 | } 166 | 167 | override fun onFling( 168 | e1: MotionEvent?, 169 | e2: MotionEvent, 170 | velocityX: Float, 171 | velocityY: Float, 172 | ): Boolean { 173 | val isVLeaning = abs(velocityX) < abs(velocityY) 174 | val isVFling = abs(velocityY) > xSwipeThresholdDistant 175 | val isHFling = abs(velocityX) > xSwipeThresholdDistant 176 | 177 | val isSkipUp = !isVFling || onSwipeUp == null || velocityY > 0 178 | val isSkipDown = !isVFling || onSwipeDown == null || velocityY < 0 179 | val isSkipLeft = !isHFling || onSwipeLeft == null || velocityX > 0 180 | val isSkipRight = !isHFling || onSwipeRight == null || velocityX < 0 181 | 182 | if (!isSkipUp && (isVLeaning || isSkipLeft && isSkipRight)) { 183 | mIsDone = true 184 | doFeedbackVibration() 185 | onSwipeUp!!.execute(implLocal) 186 | return true 187 | } 188 | if (!isSkipDown && (isVLeaning || isSkipLeft && isSkipRight)) { 189 | mIsDone = true 190 | doFeedbackVibration() 191 | onSwipeDown!!.execute(implLocal) 192 | return true 193 | } 194 | if (!isSkipLeft /* && (!isVerticalLeaning || isSkipUp && isSkipDown) */) { 195 | mIsDone = true 196 | doFeedbackVibration() 197 | onSwipeLeft!!.execute(implLocal) 198 | return true 199 | } 200 | if (!isSkipRight /* && (!isVerticalLeaning || isSkipUp && isSkipDown) */) { 201 | mIsDone = true 202 | doFeedbackVibration() 203 | onSwipeRight!!.execute(implLocal) 204 | return true 205 | } 206 | 207 | return false 208 | } 209 | 210 | private fun doFeedbackVibration() { 211 | if (edgePosData.feedbackVibration > 0) { 212 | val vibrator = implLocal.context.getSystemService(Vibrator::class.java) 213 | 214 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) 215 | vibrator.vibrate( 216 | VibrationEffect.createOneShot( 217 | edgePosData.feedbackVibration.toLong(), 218 | VibrationEffect.DEFAULT_AMPLITUDE 219 | ) 220 | ) 221 | else 222 | @Suppress("DEPRECATION") 223 | vibrator.vibrate(edgePosData.feedbackVibration.toLong()) 224 | } 225 | } 226 | 227 | private fun doFeedbackToast() { 228 | if (onSeekImpl != null && edgePosData.feedbackToast) { 229 | val value = onSeekImpl.fetchValue(implLocal) 230 | implLocal.toast.update("$value") 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/impl/ImplLocal.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app.impl 2 | 3 | import android.content.Context 4 | import kotlinx.coroutines.CoroutineScope 5 | import net.lsafer.edgeseek.app.Local 6 | 7 | class ImplLocal { 8 | lateinit var local: Local 9 | lateinit var context: Context 10 | 11 | lateinit var defaultScope: CoroutineScope 12 | 13 | lateinit var toast: CustomToastFacade 14 | lateinit var dimmer: CustomDimmerFacade 15 | } 16 | -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/impl/launchEdgeViewJob.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app.impl 2 | 3 | import android.annotation.SuppressLint 4 | import android.graphics.Color 5 | import android.os.Build 6 | import android.view.Gravity 7 | import android.view.WindowManager 8 | import android.view.WindowManager.LayoutParams 9 | import androidx.cardview.widget.CardView 10 | import co.touchlab.kermit.Logger 11 | import kotlinx.coroutines.* 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.flow.combine 14 | import kotlinx.coroutines.flow.launchIn 15 | import kotlinx.coroutines.flow.onEach 16 | import net.lsafer.edgeseek.app.data.settings.EdgeCorner 17 | import net.lsafer.edgeseek.app.data.settings.EdgePosData 18 | import net.lsafer.edgeseek.app.data.settings.EdgeSide 19 | import net.lsafer.edgeseek.app.data.settings.EdgeSideData 20 | import kotlin.math.roundToInt 21 | 22 | private val logger = Logger.withTag("net.lsafer.edgeseek.app.impl.launchEdgeViewJob") 23 | 24 | @SuppressLint("RtlHardcoded", "ClickableViewAccessibility") 25 | fun CoroutineScope.launchEdgeViewJob( 26 | implLocal: ImplLocal, 27 | windowManager: WindowManager, 28 | displayRotation: Int, 29 | displayHeight: Int, 30 | displayWidth: Int, 31 | displayDensityDpi: Int, 32 | sideDataFlow: Flow, 33 | posDataFlow: Flow, 34 | ): Job { 35 | val view = CardView(implLocal.context) 36 | view.radius = 25f 37 | view.elevation = 1f 38 | 39 | val windowParams = LayoutParams() 40 | @Suppress("DEPRECATION") 41 | windowParams.type = when { 42 | Build.VERSION.SDK_INT >= 26 -> 43 | LayoutParams.TYPE_APPLICATION_OVERLAY 44 | 45 | else -> 46 | LayoutParams.TYPE_PHONE 47 | } 48 | @Suppress("DEPRECATION") 49 | windowParams.flags = LayoutParams.FLAG_NOT_FOCUSABLE or 50 | LayoutParams.FLAG_NOT_TOUCH_MODAL or 51 | LayoutParams.FLAG_SHOW_WHEN_LOCKED // <-- this doesn't work for some reason 52 | 53 | val job = combine(sideDataFlow, posDataFlow) { a, b -> a to b } 54 | .onEach { (sideData, posData) -> 55 | if ( 56 | !posData.activated || 57 | !posData.pos.isIncludedWhenSegmented(sideData.nSegments) || 58 | !posData.orientationFilter.test(displayRotation) 59 | ) { 60 | runCatching { windowManager.removeView(view) } 61 | return@onEach 62 | } 63 | 64 | val sideRotated = posData.pos.side.rotate(displayRotation) 65 | val cornerRotated = posData.pos.corner.rotate(displayRotation) 66 | 67 | val lengthPct = 1f / sideData.nSegments 68 | val windowLength = when (sideRotated) { 69 | EdgeSide.Left, EdgeSide.Right -> displayHeight 70 | EdgeSide.Top, EdgeSide.Bottom -> displayWidth 71 | } 72 | 73 | val length = (lengthPct * windowLength).roundToInt() 74 | 75 | windowParams.height = when (sideRotated) { 76 | EdgeSide.Left, EdgeSide.Right -> length 77 | EdgeSide.Top, EdgeSide.Bottom -> posData.thickness 78 | } 79 | windowParams.width = when (sideRotated) { 80 | EdgeSide.Left, EdgeSide.Right -> posData.thickness 81 | EdgeSide.Top, EdgeSide.Bottom -> length 82 | } 83 | windowParams.gravity = when (cornerRotated) { 84 | EdgeCorner.BottomRight -> Gravity.BOTTOM or Gravity.RIGHT 85 | EdgeCorner.Bottom -> Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL 86 | EdgeCorner.BottomLeft -> Gravity.BOTTOM or Gravity.LEFT 87 | EdgeCorner.Left -> Gravity.LEFT or Gravity.CENTER_VERTICAL 88 | EdgeCorner.TopLeft -> Gravity.TOP or Gravity.LEFT 89 | EdgeCorner.Top -> Gravity.TOP or Gravity.CENTER_HORIZONTAL 90 | EdgeCorner.TopRight -> Gravity.TOP or Gravity.RIGHT 91 | EdgeCorner.Right -> Gravity.RIGHT or Gravity.CENTER_VERTICAL 92 | } 93 | windowParams.alpha = Color.alpha(posData.color) / 255f 94 | 95 | view.setCardBackgroundColor(posData.color) 96 | view.alpha = Color.alpha(posData.color) / 255f 97 | 98 | view.setOnTouchListener( 99 | EdgeTouchListener( 100 | implLocal = implLocal, 101 | edgePosData = posData, 102 | edgeSide = sideRotated, 103 | dpi = displayDensityDpi, 104 | onSeekImpl = ControlFeatureImpl.from(posData.onSeek), 105 | onLongClick = ActionFeatureImpl.from(posData.onLongClick), 106 | onDoubleClick = ActionFeatureImpl.from(posData.onDoubleClick), 107 | onSwipeUp = ActionFeatureImpl.from(posData.onSwipeUp), 108 | onSwipeDown = ActionFeatureImpl.from(posData.onSwipeDown), 109 | onSwipeLeft = ActionFeatureImpl.from(posData.onSwipeLeft), 110 | onSwipeRight = ActionFeatureImpl.from(posData.onSwipeRight), 111 | ) 112 | ) 113 | 114 | runCatching { windowManager.removeView(view) } 115 | runCatching { windowManager.addView(view, windowParams) } 116 | .onFailure { e -> logger.e("failed adding view to window", e) } 117 | } 118 | .launchIn(scope = this + Dispatchers.Main) 119 | 120 | job.invokeOnCompletion { e -> 121 | if (e !is CancellationException) 122 | logger.e("failure while executing job", e) 123 | 124 | launch(Dispatchers.Main + NonCancellable) { 125 | runCatching { windowManager.removeView(view) } 126 | } 127 | } 128 | 129 | return job 130 | } 131 | -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/receiver/BootCompleteBroadcastReceiver.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2022 LSafer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package net.lsafer.edgeseek.app.receiver 17 | 18 | import android.content.BroadcastReceiver 19 | import android.content.Context 20 | import android.content.Intent 21 | import kotlinx.coroutines.flow.firstOrNull 22 | import kotlinx.coroutines.launch 23 | import net.lsafer.edgeseek.app.MainApplication.Companion.globalLocal 24 | import net.lsafer.edgeseek.app.PK_FLAG_AUTO_BOOT 25 | import net.lsafer.edgeseek.app.UniEvent 26 | import net.lsafer.sundry.storage.select 27 | 28 | open class BootCompleteBroadcastReceiver : BroadcastReceiver() { 29 | override fun onReceive(context: Context, intent: Intent) { 30 | if (intent.action == Intent.ACTION_BOOT_COMPLETED) { 31 | globalLocal.ioScope.launch { 32 | val autoBoot = globalLocal.dataStore 33 | .select(PK_FLAG_AUTO_BOOT) 34 | .firstOrNull() 35 | 36 | if (autoBoot == false) 37 | return@launch 38 | 39 | globalLocal.eventbus.emit(UniEvent.StartService) 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/receiver/ScreenOffBroadCastReceiver.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2022 LSafer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package net.lsafer.edgeseek.app.receiver 17 | 18 | import android.content.BroadcastReceiver 19 | import android.content.Context 20 | import android.content.Intent 21 | import android.provider.Settings 22 | import co.touchlab.kermit.Logger 23 | import kotlinx.coroutines.flow.firstOrNull 24 | import kotlinx.coroutines.launch 25 | import net.lsafer.edgeseek.app.MainApplication.Companion.globalLocal 26 | import net.lsafer.edgeseek.app.PK_FLAG_BRIGHTNESS_RESET 27 | import net.lsafer.edgeseek.app.impl.ImplLocal 28 | import net.lsafer.sundry.storage.select 29 | 30 | // Used @JvmOverloads afraid android **might** try instantiating it 31 | open class ScreenOffBroadCastReceiver @JvmOverloads constructor( 32 | private val implLocal: ImplLocal? = null, 33 | ) : BroadcastReceiver() { 34 | companion object { 35 | private val logger = Logger.withTag(ScreenOffBroadCastReceiver::class.qualifiedName!!) 36 | } 37 | 38 | override fun onReceive(context: Context, intent: Intent) { 39 | if (intent.action == Intent.ACTION_SCREEN_OFF) { 40 | globalLocal.ioScope.launch { 41 | val brightnessReset = globalLocal.dataStore 42 | .select(PK_FLAG_BRIGHTNESS_RESET) 43 | .firstOrNull() 44 | 45 | if (brightnessReset == false) 46 | return@launch 47 | 48 | try { 49 | Settings.System.putInt( 50 | context.contentResolver, 51 | Settings.System.SCREEN_BRIGHTNESS_MODE, 52 | Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC 53 | ) 54 | implLocal?.dimmer?.update(0) 55 | } catch (e: Exception) { 56 | logger.e("Couldn't toggle auto brightness", e) 57 | } 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/scripts/init_log_facade.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app.scripts 2 | 3 | import android.os.Build 4 | import co.touchlab.kermit.LogcatWriter 5 | import co.touchlab.kermit.Logger 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.flow.flow 8 | import kotlinx.coroutines.flow.flowOn 9 | import net.lsafer.edgeseek.app.Local 10 | import net.lsafer.edgeseek.app.util.SimpleLogFormatter 11 | 12 | @Suppress("FunctionName") 13 | fun Local.init_log_facade() { 14 | val formatter = SimpleLogFormatter(timeZone, clock) 15 | 16 | // Initialize kermit writers with format 17 | Logger.setLogWriters(LogcatWriter(formatter)) 18 | 19 | // Cold flow for everyone wanting to read the full log. 20 | fullLog = flow { 21 | val p = Runtime.getRuntime().exec("logcat") 22 | 23 | try { 24 | for (line in p.inputStream.bufferedReader().lineSequence()) { 25 | emit(line) 26 | } 27 | } catch (e: Exception) { 28 | e.printStackTrace() 29 | } finally { 30 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 31 | p.destroyForcibly() 32 | } else { 33 | p.destroy() 34 | } 35 | } 36 | }.flowOn(Dispatchers.IO) 37 | } 38 | -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/scripts/register_shutdown_hook.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app.scripts 2 | 3 | import kotlinx.coroutines.cancel 4 | import kotlinx.coroutines.runBlocking 5 | import net.lsafer.edgeseek.app.Local 6 | 7 | @Suppress("FunctionName") 8 | fun Local.register_shutdown_hook() { 9 | Runtime.getRuntime().addShutdownHook(Thread { 10 | runBlocking { 11 | ioScope.cancel() 12 | } 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /app/src/androidMain/res/drawable/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LSafer/edgeseek/9a1e96750034b23ac242e3bd3229f20e786f0904/app/src/androidMain/res/drawable/ic_launcher.png -------------------------------------------------------------------------------- /app/src/androidMain/res/drawable/ic_sync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LSafer/edgeseek/9a1e96750034b23ac242e3bd3229f20e786f0904/app/src/androidMain/res/drawable/ic_sync.png -------------------------------------------------------------------------------- /app/src/androidMain/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Edge Seek 3 | 4 | 5 | The application uses the accessibility service for black-listing applications. 6 | 7 | Additionally, some Oppo/OnePlus devices require this permission for Controlling audio in the background. 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/androidMain/res/xml/accessibility_service.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /app/src/androidMain/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/androidMain/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 13 | 14 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/Declarations.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app 2 | 3 | import net.lsafer.edgeseek.app.data.settings.EdgePos 4 | 5 | /* ============= ------------------ ============= */ 6 | 7 | sealed interface UniEvent { 8 | data object StartService : UniEvent 9 | data object RefreshRequest : UniEvent 10 | data object FocusRequest : UniEvent 11 | 12 | data class OpenUrlRequest( 13 | val url: String, 14 | val force: Boolean = false, 15 | ) : UniEvent 16 | } 17 | 18 | /* ============= ------------------ ============= */ 19 | 20 | sealed interface UniRoute { 21 | data object HomePage : UniRoute 22 | data object EdgeListPage : UniRoute 23 | data class EdgeEditPage(val pos: EdgePos) : UniRoute 24 | data object PermissionsPage : UniRoute 25 | data object PresetsPage : UniRoute 26 | data object AboutPage : UniRoute 27 | data object LogPage : UniRoute 28 | 29 | data class IntroductionWizard(val step: Step = Step.Welcome) : UniRoute { 30 | enum class Step { 31 | Welcome, 32 | Permissions, 33 | Presets, 34 | Done, 35 | } 36 | } 37 | } 38 | 39 | /* ============= ------------------ ============= */ 40 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/Local.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app 2 | 3 | import androidx.compose.material3.SnackbarHostState 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.flow.MutableSharedFlow 7 | import kotlinx.coroutines.flow.StateFlow 8 | import kotlinx.datetime.Clock 9 | import kotlinx.datetime.TimeZone 10 | import kotlinx.serialization.json.JsonObject 11 | import net.lsafer.edgeseek.app.l10n.UniL10n 12 | import net.lsafer.sundry.compose.simplenav.SimpleNavController 13 | import net.lsafer.sundry.storage.SimpleDataStore 14 | import kotlin.random.Random 15 | 16 | class Local { 17 | // lateinit var etc: EtcOptions 18 | // lateinit var misc: MiscOptions 19 | lateinit var dataDir: okio.Path 20 | lateinit var cacheDir: okio.Path 21 | 22 | // etc 23 | 24 | lateinit var clock: Clock 25 | lateinit var random: Random 26 | lateinit var timeZone: TimeZone 27 | lateinit var ioScope: CoroutineScope 28 | 29 | lateinit var eventbus: MutableSharedFlow 30 | lateinit var l10nState: StateFlow 31 | lateinit var dataStore: SimpleDataStore 32 | lateinit var navController: SimpleNavController 33 | lateinit var snackbar: SnackbarHostState 34 | 35 | lateinit var fullLog: Flow 36 | } 37 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/components/common/preferences.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app.components.common 2 | 3 | import net.lsafer.edgeseek.app.Local 4 | import net.lsafer.edgeseek.app.data.settings.EdgePos 5 | import net.lsafer.edgeseek.app.data.settings.EdgePosData 6 | import net.lsafer.edgeseek.app.data.settings.EdgeSide 7 | import net.lsafer.edgeseek.app.data.settings.EdgeSideData 8 | import net.lsafer.sundry.storage.edit 9 | import org.cufy.json.deserializeOrNull 10 | import org.cufy.json.serializeToJsonElement 11 | 12 | fun Local.editEdgeData(pos: EdgePos, update: (EdgePosData) -> EdgePosData) { 13 | dataStore.edit { 14 | val oldValue = it[pos.key]?.deserializeOrNull() 15 | val newValue = update(oldValue ?: EdgePosData(pos)) 16 | it[pos.key] = newValue.serializeToJsonElement() 17 | } 18 | } 19 | 20 | fun Local.editEdgeSideData(side: EdgeSide, update: (EdgeSideData) -> EdgeSideData) { 21 | dataStore.edit { 22 | val oldValue = it[side.key]?.deserializeOrNull() 23 | val newValue = update(oldValue ?: EdgeSideData(side)) 24 | it[side.key] = newValue.serializeToJsonElement() 25 | } 26 | } 27 | 28 | fun Local.editEachEdgeData(update: (EdgePosData) -> EdgePosData) { 29 | dataStore.edit { 30 | EdgePos.entries.forEach { pos -> 31 | val oldValue = it[pos.key]?.deserializeOrNull() 32 | val newValue = update(oldValue ?: EdgePosData(pos)) 33 | it[pos.key] = newValue.serializeToJsonElement() 34 | } 35 | } 36 | } 37 | 38 | fun Local.clearAndSetEdgeDataList(edges: List, sides: List) { 39 | dataStore.edit { 40 | EdgePos.entries.forEach { pos -> it -= pos.key } 41 | edges.forEach { data -> it[data.pos.key] = data.serializeToJsonElement() } 42 | 43 | EdgeSide.entries.forEach { side -> it -= side.key } 44 | sides.forEach { data -> it[data.side.key] = data.serializeToJsonElement() } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/components/lib/MobileModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2022 LSafer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package net.lsafer.edgeseek.app.components.lib 17 | 18 | import androidx.compose.foundation.background 19 | import androidx.compose.foundation.layout.Box 20 | import androidx.compose.foundation.shape.RoundedCornerShape 21 | import androidx.compose.material3.MaterialTheme 22 | import androidx.compose.runtime.Composable 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.geometry.Offset 25 | import androidx.compose.ui.graphics.Brush 26 | import androidx.compose.ui.graphics.Color 27 | import androidx.compose.ui.unit.dp 28 | 29 | @Composable 30 | fun MobileModel(modifier: Modifier) { 31 | val startColor = when (MaterialTheme.colorScheme.background) { 32 | Color.Black -> Color(0xFF785858) 33 | Color.White -> Color(0xFF8989F4) 34 | else -> Color(0xFF81A0A1) 35 | } 36 | val centerColor = when (MaterialTheme.colorScheme.background) { 37 | Color.Black -> Color(0xFF816060) 38 | Color.White -> Color(0xFF9E7CEF) 39 | else -> Color(0xFF81A0A1) 40 | } 41 | val endColor = when (MaterialTheme.colorScheme.background) { 42 | Color.Black -> Color(0xFF804949) 43 | Color.White -> Color(0xFFB070EB) 44 | else -> Color(0xFF63ABAE) 45 | } 46 | 47 | val shapeModifier = Modifier 48 | .background(Brush.linearGradient( 49 | 0f to startColor, 50 | .5f to centerColor, 51 | 1f to endColor, 52 | start = Offset(Float.POSITIVE_INFINITY, 0f), 53 | end = Offset(0f, Float.POSITIVE_INFINITY) 54 | ), RoundedCornerShape(22.dp)) 55 | 56 | Box(shapeModifier then modifier) 57 | } 58 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/components/lib/list.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app.components.lib 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.material3.HorizontalDivider 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.unit.dp 11 | import androidx.compose.ui.unit.sp 12 | 13 | @Composable 14 | fun ListHeader( 15 | modifier: Modifier = Modifier, 16 | title: String, 17 | summary: String = "", 18 | ) { 19 | Column( 20 | Modifier 21 | .fillMaxWidth() 22 | .padding(50.dp) 23 | .then(modifier), 24 | horizontalAlignment = Alignment.CenterHorizontally 25 | ) { 26 | Text( 27 | text = title, 28 | color = MaterialTheme.colorScheme.secondary, 29 | fontSize = 30.sp 30 | ) 31 | Text( 32 | text = summary, 33 | fontSize = 15.sp, 34 | color = MaterialTheme.colorScheme.onBackground.copy(.5f) 35 | ) 36 | } 37 | } 38 | 39 | @Composable 40 | fun ListSectionTitle( 41 | modifier: Modifier = Modifier, 42 | title: String, 43 | ) { 44 | Text( 45 | text = title, 46 | color = MaterialTheme.colorScheme.secondary, 47 | fontSize = 14.sp, 48 | modifier = Modifier 49 | .padding(start = 65.dp) 50 | .then(modifier) 51 | ) 52 | } 53 | 54 | @Composable 55 | fun ListDivider() { 56 | HorizontalDivider() 57 | Spacer(Modifier.height(15.dp)) 58 | } 59 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/components/lib/preferences.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app.components.lib 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.foundation.shape.RoundedCornerShape 7 | import androidx.compose.foundation.text.BasicTextField 8 | import androidx.compose.material.icons.Icons 9 | import androidx.compose.material.icons.filled.VisibilityOff 10 | import androidx.compose.material3.* 11 | import androidx.compose.runtime.* 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.graphics.toArgb 16 | import androidx.compose.ui.text.style.TextAlign 17 | import androidx.compose.ui.unit.dp 18 | import androidx.compose.ui.window.Dialog 19 | import androidx.compose.ui.window.DialogProperties 20 | import com.godaddy.android.colorpicker.ClassicColorPicker 21 | import com.godaddy.android.colorpicker.toColorInt 22 | import kotlin.math.roundToInt 23 | 24 | @OptIn(ExperimentalStdlibApi::class) 25 | @Composable 26 | fun ColorPreferenceListItem( 27 | value: Int, 28 | onChange: (Int) -> Unit, 29 | headline: String, 30 | supporting: String? = null, 31 | modifier: Modifier = Modifier, 32 | ) { 33 | var localValueString by remember(value) { mutableStateOf(value.toHexString()) } 34 | val localValueInt by derivedStateOf { runCatching { localValueString.hexToInt() }.getOrNull() } 35 | 36 | var isMenuOpen by remember { mutableStateOf(false) } 37 | 38 | if (isMenuOpen) { 39 | Dialog( 40 | onDismissRequest = { 41 | onChange(localValueInt ?: value) 42 | isMenuOpen = false 43 | }, 44 | properties = DialogProperties( 45 | usePlatformDefaultWidth = true, 46 | ) 47 | ) { 48 | OutlinedCard { 49 | Column(Modifier.padding(8.dp)) { 50 | ClassicColorPicker( 51 | modifier = Modifier 52 | .fillMaxWidth() 53 | .aspectRatio(.9f), 54 | color = Color(localValueInt ?: value), 55 | onColorChanged = { 56 | localValueString = it.toColorInt().toHexString() 57 | } 58 | ) 59 | 60 | Spacer(Modifier.height(8.dp)) 61 | 62 | Row(Modifier.height(IntrinsicSize.Max)) { 63 | IconButton({ 64 | localValueString = Color(localValueInt ?: value) 65 | .copy(alpha = 0.01f) 66 | .toArgb() 67 | .toHexString() 68 | }) { 69 | Icon(Icons.Default.VisibilityOff, "Fix transparency") 70 | } 71 | 72 | OutlinedCard( 73 | Modifier.fillMaxHeight() 74 | ) { 75 | Row( 76 | Modifier 77 | .padding(8.dp) 78 | .fillMaxHeight(), 79 | Arrangement.spacedBy(8.dp), 80 | Alignment.CenterVertically, 81 | ) { 82 | Text("#") 83 | BasicTextField( 84 | value = localValueString, 85 | onValueChange = { localValueString = it }, 86 | modifier = Modifier.fillMaxWidth(), 87 | textStyle = LocalTextStyle.current.copy( 88 | color = MaterialTheme.colorScheme.onSurface, 89 | ) 90 | ) 91 | } 92 | } 93 | } 94 | } 95 | } 96 | } 97 | } 98 | 99 | ListItem( 100 | modifier = Modifier 101 | .clickable { isMenuOpen = true } 102 | .then(modifier), 103 | headlineContent = { Text(headline) }, 104 | supportingContent = supporting?.let { { Text(it) } }, 105 | trailingContent = { 106 | Box( 107 | Modifier 108 | .size(30.dp) 109 | .padding(2.dp) 110 | .background( 111 | color = Color(value).copy(alpha = 1f), 112 | shape = RoundedCornerShape(40), 113 | ) 114 | ) 115 | } 116 | ) 117 | } 118 | 119 | @Composable 120 | fun SwitchPreferenceListItem( 121 | value: Boolean, 122 | onChange: (Boolean) -> Unit, 123 | headline: String, 124 | supporting: String? = null, 125 | modifier: Modifier = Modifier, 126 | ) { 127 | ListItem( 128 | modifier = Modifier 129 | .clickable { onChange(!value) } 130 | .then(modifier), 131 | headlineContent = { Text(headline) }, 132 | supportingContent = supporting?.let { { Text(it) } }, 133 | trailingContent = { Switch(value, onChange) }, 134 | ) 135 | } 136 | 137 | @Composable 138 | fun SliderPreferenceListItem( 139 | value: Int, 140 | onChange: (Int) -> Unit, 141 | valueRange: IntRange, 142 | headline: String, 143 | supporting: String? = null, 144 | modifier: Modifier = Modifier, 145 | ) { 146 | var localValue by remember(value) { 147 | mutableStateOf(value.toFloat()) 148 | } 149 | 150 | ListItem( 151 | modifier = modifier, 152 | headlineContent = { Text(headline) }, 153 | supportingContent = { 154 | Column { 155 | if (supporting != null) 156 | Text(supporting) 157 | 158 | Slider( 159 | value = localValue, 160 | onValueChange = { localValue = it }, 161 | onValueChangeFinished = { onChange(localValue.roundToInt()) }, 162 | valueRange = valueRange.first.toFloat()..valueRange.last.toFloat(), 163 | colors = SliderDefaults.colors( 164 | thumbColor = MaterialTheme.colorScheme.secondary, 165 | activeTrackColor = MaterialTheme.colorScheme.secondary 166 | ), 167 | modifier = Modifier.fillMaxWidth() 168 | ) 169 | } 170 | }, 171 | trailingContent = { 172 | Text( 173 | text = "${localValue.toInt()}", 174 | textAlign = TextAlign.End, 175 | modifier = Modifier.width(50.dp) 176 | ) 177 | }, 178 | ) 179 | } 180 | 181 | @Composable 182 | fun SingleSelectPreferenceListItem( 183 | value: T, 184 | onChange: (T) -> Unit, 185 | items: Map, 186 | headline: String, 187 | supporting: String? = items[value], 188 | modifier: Modifier = Modifier, 189 | ) { 190 | var isMenuOpen by remember { mutableStateOf(false) } 191 | 192 | if (isMenuOpen) { 193 | Dialog( 194 | onDismissRequest = { isMenuOpen = false }, 195 | properties = DialogProperties( 196 | usePlatformDefaultWidth = true, 197 | ) 198 | ) { 199 | OutlinedCard { 200 | for ((itemValue, itemTitle) in items) { 201 | ListItem( 202 | modifier = Modifier 203 | .clickable { 204 | onChange(itemValue) 205 | isMenuOpen = false 206 | }, 207 | headlineContent = { Text(itemTitle) }, 208 | colors = if (itemValue == value) 209 | ListItemDefaults.colors( 210 | containerColor = MaterialTheme.colorScheme.surfaceTint, 211 | ) 212 | else 213 | ListItemDefaults.colors( 214 | containerColor = MaterialTheme.colorScheme.surfaceVariant, 215 | ) 216 | ) 217 | } 218 | } 219 | } 220 | } 221 | 222 | ListItem( 223 | modifier = Modifier 224 | .clickable { isMenuOpen = true } 225 | .then(modifier), 226 | headlineContent = { Text(headline) }, 227 | supportingContent = supporting?.let { { Text(it) } }, 228 | ) 229 | } 230 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/components/page/about/AboutPage.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2022 LSafer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package net.lsafer.edgeseek.app.components.page.about 17 | 18 | import androidx.compose.foundation.clickable 19 | import androidx.compose.foundation.layout.* 20 | import androidx.compose.foundation.rememberScrollState 21 | import androidx.compose.foundation.verticalScroll 22 | import androidx.compose.material3.ListItem 23 | import androidx.compose.material3.Scaffold 24 | import androidx.compose.material3.SnackbarHost 25 | import androidx.compose.material3.Text 26 | import androidx.compose.runtime.Composable 27 | import androidx.compose.runtime.rememberCoroutineScope 28 | import androidx.compose.ui.Modifier 29 | import androidx.compose.ui.unit.dp 30 | import kotlinx.coroutines.launch 31 | import net.lsafer.edgeseek.app.BuildConfig 32 | import net.lsafer.edgeseek.app.Local 33 | import net.lsafer.edgeseek.app.UniEvent 34 | import net.lsafer.edgeseek.app.UniRoute 35 | import net.lsafer.edgeseek.app.components.lib.ListDivider 36 | import net.lsafer.edgeseek.app.components.lib.ListHeader 37 | import net.lsafer.edgeseek.app.components.lib.ListSectionTitle 38 | import net.lsafer.edgeseek.app.l10n.strings 39 | 40 | @Composable 41 | fun AboutPage( 42 | local: Local, 43 | route: UniRoute.AboutPage, 44 | modifier: Modifier = Modifier, 45 | ) { 46 | Scaffold( 47 | Modifier 48 | .statusBarsPadding() 49 | .navigationBarsPadding() 50 | .then(modifier), 51 | snackbarHost = { 52 | SnackbarHost(local.snackbar) 53 | }, 54 | ) { innerPadding -> 55 | AboutPageContent(local, Modifier.padding(innerPadding)) 56 | } 57 | } 58 | 59 | @Composable 60 | fun AboutPageContent( 61 | local: Local, 62 | modifier: Modifier = Modifier, 63 | ) { 64 | val coroutineScope = rememberCoroutineScope() 65 | 66 | fun openIntroductionWizard() { 67 | @Suppress("ControlFlowWithEmptyBody") 68 | while (local.navController.back()); 69 | local.navController.push(UniRoute.IntroductionWizard()) 70 | } 71 | 72 | fun openUrl(url: String) = coroutineScope.launch { 73 | val event = UniEvent.OpenUrlRequest(url) 74 | local.eventbus.emit(event) 75 | } 76 | 77 | Column( 78 | Modifier 79 | .verticalScroll(rememberScrollState()) 80 | .then(modifier) 81 | ) { 82 | ListHeader(title = strings.stmt.page_about_heading) 83 | ListSectionTitle(title = strings.label.credits) 84 | ListItem( 85 | headlineContent = { Text(strings.label.author) }, 86 | supportingContent = { Text("LSafer") } 87 | ) 88 | 89 | ListDivider() 90 | ListSectionTitle(title = strings.label.version) 91 | ListItem( 92 | headlineContent = { Text(strings.label.version_name) }, 93 | supportingContent = { Text(BuildConfig.VERSION) }, 94 | ) 95 | ListItem( 96 | headlineContent = { Text(strings.label.version_code) }, 97 | supportingContent = { Text(BuildConfig.VERSION_CODE.toString()) }, 98 | ) 99 | 100 | ListDivider() 101 | ListSectionTitle(title = strings.label.links) 102 | ListItem( 103 | modifier = Modifier 104 | .clickable { openUrl("https://lsafer.net/edgeseek") }, 105 | headlineContent = { Text(strings.stmt.about_website_headline) }, 106 | supportingContent = { Text(strings.stmt.about_website_supporting) }, 107 | ) 108 | ListItem( 109 | modifier = Modifier 110 | .clickable { openUrl("https://github.com/lsafer/edgeseek") }, 111 | headlineContent = { Text(strings.stmt.about_source_code_headline) }, 112 | supportingContent = { Text(strings.stmt.about_source_code_supporting) } 113 | ) 114 | 115 | ListDivider() 116 | ListSectionTitle(title = strings.label.misc) 117 | ListItem( 118 | modifier = Modifier 119 | .clickable { openIntroductionWizard() }, 120 | headlineContent = { Text(strings.stmt.about_reintroduce_headline) }, 121 | supportingContent = { Text(strings.stmt.about_reintroduce_supporting) }, 122 | ) 123 | ListItem( 124 | modifier = Modifier 125 | .clickable { local.navController.push(UniRoute.LogPage) }, 126 | headlineContent = { Text(strings.stmt.page_log_headline) }, 127 | supportingContent = { Text(strings.stmt.page_log_supporting) }, 128 | ) 129 | 130 | Spacer(Modifier.height(50.dp)) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/components/page/edge_edit/EdgeEditPage.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2022 LSafer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package net.lsafer.edgeseek.app.components.page.edge_edit 17 | 18 | import androidx.compose.foundation.layout.* 19 | import androidx.compose.foundation.rememberScrollState 20 | import androidx.compose.foundation.verticalScroll 21 | import androidx.compose.material3.Scaffold 22 | import androidx.compose.material3.SnackbarHost 23 | import androidx.compose.runtime.Composable 24 | import androidx.compose.runtime.getValue 25 | import androidx.compose.runtime.produceState 26 | import androidx.compose.ui.Modifier 27 | import androidx.compose.ui.unit.dp 28 | import kotlinx.coroutines.flow.distinctUntilChanged 29 | import kotlinx.coroutines.flow.filterNotNull 30 | import net.lsafer.edgeseek.app.Local 31 | import net.lsafer.edgeseek.app.UniRoute 32 | import net.lsafer.edgeseek.app.components.common.editEdgeData 33 | import net.lsafer.edgeseek.app.components.lib.* 34 | import net.lsafer.edgeseek.app.data.settings.EdgePos 35 | import net.lsafer.edgeseek.app.data.settings.EdgePosData 36 | import net.lsafer.edgeseek.app.data.settings.EdgeSide 37 | import net.lsafer.edgeseek.app.data.settings.OrientationFilter 38 | import net.lsafer.edgeseek.app.l10n.strings 39 | import net.lsafer.sundry.storage.select 40 | 41 | @Composable 42 | fun EdgeEditPage( 43 | local: Local, 44 | route: UniRoute.EdgeEditPage, 45 | modifier: Modifier = Modifier, 46 | ) { 47 | Scaffold( 48 | Modifier 49 | .statusBarsPadding() 50 | .navigationBarsPadding() 51 | .then(modifier), 52 | snackbarHost = { 53 | SnackbarHost(local.snackbar) 54 | }, 55 | ) { innerPadding -> 56 | EdgeEditPageContent(local, route.pos, Modifier.padding(innerPadding)) 57 | } 58 | } 59 | 60 | @Composable 61 | fun EdgeEditPageContent( 62 | local: Local, 63 | pos: EdgePos, 64 | modifier: Modifier = Modifier, 65 | ) { 66 | val data by produceState(EdgePosData(pos), pos, local) { 67 | local.dataStore 68 | .select(pos.key) 69 | .filterNotNull() 70 | .distinctUntilChanged() 71 | .collect { value = it } 72 | } 73 | 74 | fun edit(block: (EdgePosData) -> EdgePosData) { 75 | local.editEdgeData(pos, block) 76 | } 77 | 78 | Column( 79 | Modifier 80 | .verticalScroll(rememberScrollState()) 81 | .then(modifier) 82 | ) { 83 | ListHeader( 84 | title = strings.stmt.page_edge_edit_heading, 85 | summary = pos.key, 86 | ) 87 | 88 | ListSectionTitle(title = strings.label.job) 89 | SwitchPreferenceListItem( 90 | value = data.activated, 91 | onChange = { newValue -> edit { it.copy(activated = newValue) } }, 92 | headline = strings.stmt.edge_activation_headline, 93 | supporting = strings.stmt.edge_activation_supporting, 94 | ) 95 | ControlFeaturePreferenceListItem( 96 | value = data.onSeek, 97 | onChange = { newValue -> edit { data.copy(onSeek = newValue) } }, 98 | headline = strings.stmt.edge_seek_task_headline, 99 | ) 100 | ActionFeaturePreferenceListItem( 101 | value = data.onLongClick, 102 | onChange = { newValue -> edit { data.copy(onLongClick = newValue) } }, 103 | headline = strings.stmt.edge_long_click_task_headline, 104 | ) 105 | ActionFeaturePreferenceListItem( 106 | value = data.onDoubleClick, 107 | onChange = { newValue -> edit { data.copy(onDoubleClick = newValue) } }, 108 | headline = strings.stmt.edge_double_click_task_headline, 109 | ) 110 | if (pos.side != EdgeSide.Top) ActionFeaturePreferenceListItem( 111 | value = data.onSwipeUp, 112 | onChange = { newValue -> edit { data.copy(onSwipeUp = newValue) } }, 113 | headline = strings.stmt.edge_swipe_up_task_headline, 114 | ) 115 | if (pos.side != EdgeSide.Bottom) ActionFeaturePreferenceListItem( 116 | value = data.onSwipeDown, 117 | onChange = { newValue -> edit { data.copy(onSwipeDown = newValue) } }, 118 | headline = strings.stmt.edge_swipe_down_task_headline, 119 | ) 120 | if (pos.side != EdgeSide.Left) ActionFeaturePreferenceListItem( 121 | value = data.onSwipeLeft, 122 | onChange = { newValue -> edit { data.copy(onSwipeLeft = newValue) } }, 123 | headline = strings.stmt.edge_swipe_left_task_headline, 124 | ) 125 | if (pos.side != EdgeSide.Right) ActionFeaturePreferenceListItem( 126 | value = data.onSwipeRight, 127 | onChange = { newValue -> edit { data.copy(onSwipeRight = newValue) } }, 128 | headline = strings.stmt.edge_swipe_right_task_headline, 129 | ) 130 | 131 | ListDivider() 132 | ListSectionTitle(title = strings.label.input) 133 | SliderPreferenceListItem( 134 | value = data.sensitivity, 135 | onChange = { newValue -> edit { data.copy(sensitivity = newValue) } }, 136 | headline = strings.stmt.edge_sensitivity_headline, 137 | supporting = strings.stmt.edge_sensitivity_supporting, 138 | valueRange = 5..100, 139 | ) 140 | 141 | ListDivider() 142 | ListSectionTitle(title = strings.label.dimensions) 143 | SliderPreferenceListItem( 144 | value = data.thickness, 145 | onChange = { newValue -> edit { data.copy(thickness = newValue) } }, 146 | headline = strings.stmt.edge_thickness_headline, 147 | supporting = strings.stmt.edge_thickness_supporting, 148 | valueRange = 0..100, 149 | ) 150 | 151 | ListDivider() 152 | ListSectionTitle(title = strings.label.appearance) 153 | ColorPreferenceListItem( 154 | value = data.color, 155 | onChange = { newValue -> edit { data.copy(color = newValue) } }, 156 | headline = strings.stmt.edge_color_headline, 157 | supporting = strings.stmt.edge_color_supporting, 158 | ) 159 | 160 | ListDivider() 161 | ListSectionTitle(title = strings.label.misc) 162 | SwitchPreferenceListItem( 163 | value = data.seekSteps, 164 | onChange = { newValue -> edit { data.copy(seekSteps = newValue) } }, 165 | headline = strings.stmt.edge_seek_steps_headline, 166 | supporting = strings.stmt.edge_seek_steps_supporting, 167 | ) 168 | SwitchPreferenceListItem( 169 | value = data.seekAcceleration, 170 | onChange = { newValue -> edit { data.copy(seekAcceleration = newValue) } }, 171 | headline = strings.stmt.edge_seek_acceleration_headline, 172 | supporting = strings.stmt.edge_seek_acceleration_supporting, 173 | ) 174 | SwitchPreferenceListItem( 175 | value = data.feedbackToast, 176 | onChange = { newValue -> edit { data.copy(feedbackToast = newValue) } }, 177 | headline = strings.stmt.edge_feedback_toast_headline, 178 | supporting = strings.stmt.edge_feedback_toast_supporting, 179 | ) 180 | SwitchPreferenceListItem( 181 | value = data.feedbackSystemPanel, 182 | onChange = { newValue -> edit { data.copy(feedbackSystemPanel = newValue) } }, 183 | headline = strings.stmt.edge_feedback_system_panel_headline, 184 | supporting = strings.stmt.edge_feedback_system_panel_supporting, 185 | ) 186 | SingleSelectPreferenceListItem( 187 | value = data.orientationFilter, 188 | onChange = { newValue -> edit { it.copy(orientationFilter = newValue) } }, 189 | headline = strings.stmt.edge_orientation_filter_headline, 190 | items = mapOf( 191 | OrientationFilter.All to strings.stmt.edge_orientation_filter_value_all, 192 | OrientationFilter.PortraitOnly to strings.stmt.edge_orientation_filter_value_portrait_only, 193 | OrientationFilter.LandscapeOnly to strings.stmt.edge_orientation_filter_value_landscape_only, 194 | ) 195 | ) 196 | SliderPreferenceListItem( 197 | value = data.feedbackVibration, 198 | onChange = { newValue -> edit { data.copy(feedbackVibration = newValue) } }, 199 | headline = strings.stmt.edge_feedback_vibration_headline, 200 | supporting = strings.stmt.edge_feedback_vibration_supporting, 201 | valueRange = 0..100, 202 | ) 203 | 204 | Spacer(Modifier.height(50.dp)) 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/components/page/edge_edit/items.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app.components.page.edge_edit 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import net.lsafer.edgeseek.app.components.lib.SingleSelectPreferenceListItem 6 | import net.lsafer.edgeseek.app.data.settings.ActionFeature 7 | import net.lsafer.edgeseek.app.data.settings.ControlFeature 8 | import net.lsafer.edgeseek.app.l10n.strings 9 | 10 | @Composable 11 | fun ControlFeaturePreferenceListItem( 12 | value: ControlFeature, 13 | onChange: (ControlFeature) -> Unit, 14 | headline: String, 15 | modifier: Modifier = Modifier, 16 | ) { 17 | SingleSelectPreferenceListItem( 18 | value = value, 19 | onChange = onChange, 20 | headline = headline, 21 | items = mapOf( 22 | ControlFeature.Nothing to strings.stmt.control_feature_nothing, 23 | ControlFeature.Brightness to strings.stmt.control_feature_brightness, 24 | ControlFeature.BrightnessWithDimmer to strings.stmt.control_feature_brightness_dimmer, 25 | ControlFeature.Alarm to strings.stmt.control_feature_alarm, 26 | ControlFeature.Music to strings.stmt.control_feature_music, 27 | ControlFeature.Ring to strings.stmt.control_feature_ring, 28 | ControlFeature.System to strings.stmt.control_feature_system, 29 | ), 30 | modifier = modifier, 31 | ) 32 | } 33 | 34 | @Composable 35 | fun ActionFeaturePreferenceListItem( 36 | value: ActionFeature, 37 | onChange: (ActionFeature) -> Unit, 38 | headline: String, 39 | modifier: Modifier = Modifier, 40 | ) { 41 | SingleSelectPreferenceListItem( 42 | value = value, 43 | onChange = onChange, 44 | headline = headline, 45 | items = mapOf( 46 | ActionFeature.Nothing to strings.stmt.action_feature_nothing, 47 | ActionFeature.ExpandStatusBar to strings.stmt.action_feature_expand_status_bar, 48 | ), 49 | modifier = modifier, 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/components/page/edge_list/EdgeListPage.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2022 LSafer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:OptIn(ExperimentalFoundationApi::class) 18 | 19 | package net.lsafer.edgeseek.app.components.page.edge_list 20 | 21 | import androidx.compose.foundation.ExperimentalFoundationApi 22 | import androidx.compose.foundation.background 23 | import androidx.compose.foundation.combinedClickable 24 | import androidx.compose.foundation.layout.* 25 | import androidx.compose.foundation.shape.RoundedCornerShape 26 | import androidx.compose.material3.* 27 | import androidx.compose.runtime.* 28 | import androidx.compose.ui.Alignment 29 | import androidx.compose.ui.Modifier 30 | import androidx.compose.ui.graphics.Color 31 | import androidx.compose.ui.platform.LocalLayoutDirection 32 | import androidx.compose.ui.unit.LayoutDirection 33 | import androidx.compose.ui.unit.dp 34 | import androidx.compose.ui.unit.sp 35 | import kotlinx.coroutines.flow.distinctUntilChanged 36 | import kotlinx.coroutines.flow.filterNotNull 37 | import kotlinx.coroutines.launch 38 | import net.lsafer.edgeseek.app.Local 39 | import net.lsafer.edgeseek.app.UniRoute 40 | import net.lsafer.edgeseek.app.components.common.editEdgeSideData 41 | import net.lsafer.edgeseek.app.components.lib.MobileModel 42 | import net.lsafer.edgeseek.app.data.settings.EdgePos 43 | import net.lsafer.edgeseek.app.data.settings.EdgePosData 44 | import net.lsafer.edgeseek.app.data.settings.EdgeSide 45 | import net.lsafer.edgeseek.app.data.settings.EdgeSideData 46 | import net.lsafer.edgeseek.app.l10n.strings 47 | import net.lsafer.sundry.storage.select 48 | 49 | @Composable 50 | fun EdgeListPage( 51 | local: Local, 52 | route: UniRoute.EdgeListPage, 53 | modifier: Modifier = Modifier, 54 | ) { 55 | Scaffold( 56 | Modifier 57 | .statusBarsPadding() 58 | .navigationBarsPadding() 59 | .then(modifier), 60 | snackbarHost = { 61 | SnackbarHost(local.snackbar) 62 | }, 63 | ) { innerPadding -> 64 | EdgeListPageContent(local, Modifier.padding(innerPadding)) 65 | } 66 | } 67 | 68 | @Composable 69 | fun EdgeListPageContent( 70 | local: Local, 71 | modifier: Modifier = Modifier, 72 | ) { 73 | Column(modifier) { 74 | Spacer(Modifier.height(50.dp)) 75 | 76 | Text( 77 | text = strings.stmt.page_edge_list_heading, 78 | color = MaterialTheme.colorScheme.secondary, 79 | fontSize = 30.sp, 80 | modifier = Modifier.align(Alignment.CenterHorizontally), 81 | ) 82 | Text( 83 | text = strings.stmt.page_edge_list_summary, 84 | fontSize = 15.sp, 85 | color = MaterialTheme.colorScheme.onBackground.copy(.5f), 86 | modifier = Modifier.align(Alignment.CenterHorizontally), 87 | ) 88 | 89 | CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { 90 | Box( 91 | Modifier 92 | .fillMaxSize() 93 | .padding(40.dp) 94 | ) { 95 | MobileModel(Modifier.fillMaxSize()) 96 | 97 | for (side in EdgeSide.entries) { 98 | val sideData by produceState(EdgeSideData(side)) { 99 | local.dataStore 100 | .select(side.key) 101 | .filterNotNull() 102 | .distinctUntilChanged() 103 | .collect { value = it } 104 | } 105 | 106 | EdgeSideItem(local, sideData) 107 | 108 | for (pos in EdgePos.entries.filter { it.side == side }) 109 | EdgeItem(local, pos, sideData) 110 | } 111 | } 112 | } 113 | } 114 | } 115 | 116 | @Composable 117 | private fun BoxScope.EdgeSideItem( 118 | local: Local, 119 | sideData: EdgeSideData, 120 | modifier: Modifier = Modifier, 121 | ) { 122 | fun edit(block: (EdgeSideData) -> EdgeSideData) { 123 | local.editEdgeSideData(sideData.side, block) 124 | } 125 | 126 | val alignModifier = when (sideData.side) { 127 | EdgeSide.Bottom -> Modifier.align(Alignment.BottomCenter) 128 | EdgeSide.Top -> Modifier.align(Alignment.TopCenter) 129 | EdgeSide.Left -> Modifier.align(Alignment.CenterStart) 130 | EdgeSide.Right -> Modifier.align(Alignment.CenterEnd) 131 | } 132 | 133 | IconButton( 134 | modifier = Modifier 135 | .padding(24.dp) 136 | .then(alignModifier) 137 | .then(modifier), 138 | onClick = { 139 | edit { it.copy(nSegments = it.nSegments % 3 + 1) } 140 | } 141 | ) { 142 | Text("${sideData.nSegments}") 143 | } 144 | } 145 | 146 | @OptIn(ExperimentalFoundationApi::class) 147 | @Composable 148 | private fun BoxScope.EdgeItem( 149 | local: Local, 150 | pos: EdgePos, 151 | sideData: EdgeSideData, 152 | modifier: Modifier = Modifier, 153 | ) { 154 | if (!pos.isIncludedWhenSegmented(sideData.nSegments)) 155 | return 156 | 157 | val coroutineScope = rememberCoroutineScope() 158 | 159 | val handleOnClick: () -> Unit = { 160 | coroutineScope.launch { 161 | local.navController.push(UniRoute.EdgeEditPage(pos)) 162 | } 163 | } 164 | 165 | val data by produceState(EdgePosData(pos)) { 166 | local.dataStore 167 | .select(pos.key) 168 | .filterNotNull() 169 | .distinctUntilChanged() 170 | .collect { value = it } 171 | } 172 | 173 | val thickness = 24.dp 174 | val lengthPct = when (sideData.nSegments) { 175 | 1 -> .9f // <-- idk why, when single, it ignores padding! 176 | else -> 1f / sideData.nSegments 177 | } 178 | 179 | val alignModifier = when (pos) { 180 | EdgePos.BottomLeft -> Modifier.align(Alignment.BottomStart) 181 | EdgePos.BottomCenter -> Modifier.align(Alignment.BottomCenter) 182 | EdgePos.BottomRight -> Modifier.align(Alignment.BottomEnd) 183 | EdgePos.TopLeft -> Modifier.align(Alignment.TopStart) 184 | EdgePos.TopCenter -> Modifier.align(Alignment.TopCenter) 185 | EdgePos.TopRight -> Modifier.align(Alignment.TopEnd) 186 | EdgePos.LeftBottom -> Modifier.align(Alignment.BottomStart) 187 | EdgePos.LeftCenter -> Modifier.align(Alignment.CenterStart) 188 | EdgePos.LeftTop -> Modifier.align(Alignment.TopStart) 189 | EdgePos.RightBottom -> Modifier.align(Alignment.BottomEnd) 190 | EdgePos.RightCenter -> Modifier.align(Alignment.CenterEnd) 191 | EdgePos.RightTop -> Modifier.align(Alignment.TopEnd) 192 | } 193 | val widthModifier = when (pos.side) { 194 | EdgeSide.Top, EdgeSide.Bottom -> Modifier.fillMaxWidth(lengthPct) 195 | EdgeSide.Left, EdgeSide.Right -> Modifier.width(thickness) 196 | } 197 | val heightModifier = when (pos.side) { 198 | EdgeSide.Top, EdgeSide.Bottom -> Modifier.height(thickness) 199 | EdgeSide.Left, EdgeSide.Right -> Modifier.fillMaxHeight(lengthPct) 200 | } 201 | val innerPaddingModifier = when (pos) { 202 | EdgePos.LeftBottom, EdgePos.RightBottom -> Modifier.padding(bottom = thickness) 203 | EdgePos.LeftTop, EdgePos.RightTop -> Modifier.padding(top = thickness) 204 | EdgePos.BottomLeft, EdgePos.TopLeft -> Modifier.padding(start = thickness) 205 | EdgePos.BottomRight, EdgePos.TopRight -> Modifier.padding(end = thickness) 206 | else -> Modifier 207 | } 208 | val innerColor = when { 209 | data.activated -> Color(data.color).copy(alpha = .5f) 210 | else -> Color.Gray 211 | } 212 | 213 | Box( 214 | Modifier 215 | .then(alignModifier) 216 | .then(widthModifier) 217 | .then(heightModifier) 218 | .then(innerPaddingModifier) 219 | .then(modifier) 220 | ) { 221 | Box( 222 | Modifier 223 | .fillMaxSize() 224 | .padding(2.5.dp) 225 | .background(innerColor, RoundedCornerShape(30.dp)) 226 | .combinedClickable(onClick = handleOnClick) 227 | ) 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/components/page/home/HomePage.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2022 LSafer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package net.lsafer.edgeseek.app.components.page.home 17 | 18 | import androidx.compose.foundation.clickable 19 | import androidx.compose.foundation.layout.* 20 | import androidx.compose.foundation.rememberScrollState 21 | import androidx.compose.foundation.verticalScroll 22 | import androidx.compose.material3.ListItem 23 | import androidx.compose.material3.Scaffold 24 | import androidx.compose.material3.SnackbarHost 25 | import androidx.compose.material3.Text 26 | import androidx.compose.runtime.Composable 27 | import androidx.compose.ui.Modifier 28 | import androidx.compose.ui.unit.dp 29 | import net.lsafer.edgeseek.app.Local 30 | import net.lsafer.edgeseek.app.UniRoute 31 | import net.lsafer.edgeseek.app.components.lib.ListDivider 32 | import net.lsafer.edgeseek.app.components.lib.ListHeader 33 | import net.lsafer.edgeseek.app.components.lib.ListSectionTitle 34 | import net.lsafer.edgeseek.app.l10n.strings 35 | 36 | @Composable 37 | fun HomePage( 38 | local: Local, 39 | route: UniRoute.HomePage, 40 | modifier: Modifier = Modifier, 41 | ) { 42 | Scaffold( 43 | Modifier 44 | .statusBarsPadding() 45 | .navigationBarsPadding() 46 | .then(modifier), 47 | snackbarHost = { 48 | SnackbarHost(local.snackbar) 49 | }, 50 | ) { innerPadding -> 51 | MainPageContent(local, Modifier.padding(innerPadding)) 52 | } 53 | } 54 | 55 | @Composable 56 | fun MainPageContent( 57 | local: Local, 58 | modifier: Modifier = Modifier, 59 | ) { 60 | Column( 61 | Modifier 62 | .verticalScroll(rememberScrollState()) 63 | .then(modifier), 64 | ) { 65 | ListHeader(title = strings.branding.app_name) 66 | 67 | ListSectionTitle(title = strings.label.application) 68 | HomePage_ListItem_activation(local) 69 | HomePage_ListItem_ui_colors(local) 70 | 71 | ListDivider() 72 | ListSectionTitle(title = strings.label.job) 73 | ListItem( 74 | modifier = Modifier 75 | .clickable { local.navController.push(UniRoute.EdgeListPage) }, 76 | headlineContent = { Text(strings.stmt.page_edge_list_headline) }, 77 | supportingContent = { Text(strings.stmt.page_edge_list_supporting) }, 78 | ) 79 | HomePage_ListItem_auto_boot(local) 80 | HomePage_ListItem_brightness_reset(local) 81 | 82 | ListDivider() 83 | ListSectionTitle(title = strings.label.misc) 84 | ListItem( 85 | modifier = Modifier 86 | .clickable { local.navController.push(UniRoute.PermissionsPage) }, 87 | headlineContent = { Text(strings.stmt.page_permissions_headline) }, 88 | supportingContent = { Text(strings.stmt.page_permissions_supporting) }, 89 | ) 90 | ListItem( 91 | modifier = Modifier 92 | .clickable { local.navController.push(UniRoute.PresetsPage) }, 93 | headlineContent = { Text(strings.stmt.page_presets_headline) }, 94 | supportingContent = { Text(strings.stmt.page_presets_supporting) }, 95 | ) 96 | ListItem( 97 | modifier = Modifier 98 | .clickable { local.navController.push(UniRoute.AboutPage) }, 99 | headlineContent = { Text(strings.stmt.page_about_headline) }, 100 | supportingContent = { Text(strings.stmt.page_about_supporting) }, 101 | ) 102 | 103 | Spacer(Modifier.height(50.dp)) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/components/page/home/items.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app.components.page.home 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.produceState 6 | import androidx.compose.runtime.rememberCoroutineScope 7 | import androidx.compose.ui.Modifier 8 | import kotlinx.coroutines.flow.filterNotNull 9 | import kotlinx.coroutines.launch 10 | import net.lsafer.edgeseek.app.* 11 | import net.lsafer.edgeseek.app.components.lib.SingleSelectPreferenceListItem 12 | import net.lsafer.edgeseek.app.components.lib.SwitchPreferenceListItem 13 | import net.lsafer.edgeseek.app.l10n.strings 14 | import net.lsafer.sundry.storage.edit 15 | import net.lsafer.sundry.storage.select 16 | import org.cufy.json.set 17 | 18 | @Composable 19 | fun HomePage_ListItem_activation( 20 | local: Local, 21 | modifier: Modifier = Modifier, 22 | ) { 23 | val coroutineScope = rememberCoroutineScope() 24 | val value by produceState(false) { 25 | local.dataStore 26 | .select(PK_FLAG_ACTIVATED) 27 | .filterNotNull() 28 | .collect { value = it } 29 | } 30 | 31 | val handleOnChange = { newValue: Boolean -> 32 | local.dataStore.edit { it[PK_FLAG_ACTIVATED] = newValue } 33 | 34 | if (newValue) coroutineScope.launch { 35 | local.eventbus.emit(UniEvent.StartService) 36 | } 37 | } 38 | 39 | SwitchPreferenceListItem( 40 | value = value, 41 | onChange = handleOnChange, 42 | headline = strings.stmt.app_activation_headline, 43 | supporting = strings.stmt.app_activation_supporting, 44 | modifier = modifier, 45 | ) 46 | } 47 | 48 | @Composable 49 | fun HomePage_ListItem_ui_colors( 50 | local: Local, 51 | modifier: Modifier = Modifier, 52 | ) { 53 | val value by produceState(UI_COLORS_DEFAULT) { 54 | local.dataStore 55 | .select(PK_UI_COLORS) 56 | .filterNotNull() 57 | .collect { value = it } 58 | } 59 | 60 | val handleOnChange = { newValue: String -> 61 | local.dataStore.edit { it[PK_UI_COLORS] = newValue } 62 | } 63 | 64 | SingleSelectPreferenceListItem( 65 | value = value, 66 | onChange = handleOnChange, 67 | headline = strings.stmt.app_colors_headline, 68 | items = mapOf( 69 | UI_COLORS_SYSTEM to strings.stmt.app_colors_value_system, 70 | UI_COLORS_BLACK to strings.stmt.app_colors_value_black, 71 | UI_COLORS_DARK to strings.stmt.app_colors_value_dark, 72 | UI_COLORS_LIGHT to strings.stmt.app_colors_value_light, 73 | UI_COLORS_WHITE to strings.stmt.app_colors_value_white, 74 | ), 75 | modifier = modifier, 76 | ) 77 | } 78 | 79 | @Composable 80 | fun HomePage_ListItem_auto_boot( 81 | local: Local, 82 | modifier: Modifier = Modifier, 83 | ) { 84 | val value by produceState(true) { 85 | local.dataStore 86 | .select(PK_FLAG_AUTO_BOOT) 87 | .filterNotNull() 88 | .collect { value = it } 89 | } 90 | 91 | val handleOnChange = { newValue: Boolean -> 92 | local.dataStore.edit { it[PK_FLAG_AUTO_BOOT] = newValue } 93 | } 94 | 95 | SwitchPreferenceListItem( 96 | value = value, 97 | onChange = handleOnChange, 98 | headline = strings.stmt.app_auto_boot_headline, 99 | supporting = strings.stmt.app_auto_boot_supporting, 100 | modifier = modifier, 101 | ) 102 | } 103 | 104 | @Composable 105 | fun HomePage_ListItem_brightness_reset( 106 | local: Local, 107 | modifier: Modifier = Modifier, 108 | ) { 109 | val value by produceState(true) { 110 | local.dataStore 111 | .select(PK_FLAG_BRIGHTNESS_RESET) 112 | .filterNotNull() 113 | .collect { value = it } 114 | } 115 | 116 | val handleOnChange = { newValue: Boolean -> 117 | local.dataStore.edit { it[PK_FLAG_BRIGHTNESS_RESET] = newValue } 118 | } 119 | 120 | SwitchPreferenceListItem( 121 | value = value, 122 | onChange = handleOnChange, 123 | headline = strings.stmt.app_brightness_reset_headline, 124 | supporting = strings.stmt.app_brightness_reset_supporting, 125 | modifier = modifier, 126 | ) 127 | } 128 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/components/page/log/LogPage.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app.components.page.log 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.navigationBarsPadding 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.layout.statusBarsPadding 8 | import androidx.compose.foundation.rememberScrollState 9 | import androidx.compose.foundation.verticalScroll 10 | import androidx.compose.material3.* 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.mutableStateListOf 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.text.font.FontFamily 17 | import androidx.compose.ui.unit.dp 18 | import androidx.compose.ui.unit.sp 19 | import net.lsafer.edgeseek.app.Local 20 | import net.lsafer.edgeseek.app.UniRoute 21 | import net.lsafer.edgeseek.app.l10n.strings 22 | import net.lsafer.sundry.compose.util.SubscribeEffect 23 | 24 | @OptIn(ExperimentalMaterial3Api::class) 25 | @Composable 26 | fun LogPage( 27 | local: Local, 28 | route: UniRoute.LogPage, 29 | modifier: Modifier = Modifier 30 | ) { 31 | val logs = remember { mutableStateListOf() } 32 | 33 | SubscribeEffect(local.fullLog) { 34 | logs += it 35 | } 36 | 37 | Scaffold( 38 | Modifier 39 | .statusBarsPadding() 40 | .navigationBarsPadding() 41 | .then(modifier), 42 | snackbarHost = { 43 | SnackbarHost(local.snackbar) 44 | }, 45 | topBar = { 46 | TopAppBar( 47 | title = { Text(strings.stmt.page_log_heading) } 48 | ) 49 | } 50 | ) { innerPadding -> 51 | Column( 52 | Modifier 53 | .verticalScroll(rememberScrollState(), reverseScrolling = true) 54 | .padding(innerPadding) 55 | .padding(8.dp) 56 | ) { 57 | for ((i, log) in logs.withIndex()) { 58 | Text( 59 | text = log.trim(), 60 | fontSize = 12.sp, 61 | modifier = Modifier.background( 62 | color = if (i % 2 == 0) Color.Gray else Color.DarkGray, 63 | ), 64 | fontFamily = FontFamily.Monospace, 65 | ) 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/components/page/permissions/PermissionsPage.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app.components.page.permissions 2 | 3 | import androidx.compose.foundation.layout.navigationBarsPadding 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.layout.statusBarsPadding 6 | import androidx.compose.material3.Scaffold 7 | import androidx.compose.material3.SnackbarHost 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import net.lsafer.edgeseek.app.Local 11 | import net.lsafer.edgeseek.app.UniRoute 12 | 13 | @Composable 14 | fun PermissionsPage( 15 | local: Local, 16 | route: UniRoute.PermissionsPage, 17 | modifier: Modifier = Modifier, 18 | ) { 19 | Scaffold( 20 | Modifier 21 | .statusBarsPadding() 22 | .navigationBarsPadding() 23 | .then(modifier), 24 | snackbarHost = { 25 | SnackbarHost(local.snackbar) 26 | }, 27 | ) { innerPadding -> 28 | PermissionsPageContent(local, Modifier.padding(innerPadding)) 29 | } 30 | } 31 | 32 | @Suppress("EXPECT_AND_ACTUAL_IN_THE_SAME_MODULE") 33 | @Composable 34 | expect fun PermissionsPageContent( 35 | local: Local, 36 | modifier: Modifier = Modifier, 37 | ) 38 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/components/page/presets/PresetsPage.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2022 LSafer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package net.lsafer.edgeseek.app.components.page.presets 17 | 18 | import androidx.compose.foundation.clickable 19 | import androidx.compose.foundation.layout.* 20 | import androidx.compose.foundation.rememberScrollState 21 | import androidx.compose.foundation.verticalScroll 22 | import androidx.compose.material3.ListItem 23 | import androidx.compose.material3.Scaffold 24 | import androidx.compose.material3.SnackbarHost 25 | import androidx.compose.material3.Text 26 | import androidx.compose.runtime.Composable 27 | import androidx.compose.ui.Modifier 28 | import androidx.compose.ui.graphics.Color 29 | import androidx.compose.ui.graphics.toArgb 30 | import androidx.compose.ui.unit.dp 31 | import net.lsafer.edgeseek.app.* 32 | import net.lsafer.edgeseek.app.components.common.clearAndSetEdgeDataList 33 | import net.lsafer.edgeseek.app.components.common.editEachEdgeData 34 | import net.lsafer.edgeseek.app.components.lib.ListHeader 35 | import net.lsafer.edgeseek.app.components.lib.ListSectionTitle 36 | import net.lsafer.edgeseek.app.l10n.strings 37 | 38 | @Composable 39 | fun PresetsPage( 40 | local: Local, 41 | route: UniRoute.PresetsPage, 42 | modifier: Modifier = Modifier, 43 | ) { 44 | Scaffold( 45 | Modifier 46 | .statusBarsPadding() 47 | .navigationBarsPadding() 48 | .then(modifier), 49 | snackbarHost = { 50 | SnackbarHost(local.snackbar) 51 | }, 52 | ) { innerPadding -> 53 | PresetsPageContent(local, Modifier.padding(innerPadding)) 54 | } 55 | } 56 | 57 | @Composable 58 | fun PresetsPageContent( 59 | local: Local, 60 | modifier: Modifier = Modifier, 61 | ) { 62 | Column( 63 | Modifier 64 | .verticalScroll(rememberScrollState()) 65 | .then(modifier), 66 | ) { 67 | ListHeader( 68 | title = strings.stmt.page_presets_heading, 69 | summary = strings.stmt.page_presets_summary, 70 | ) 71 | ListSectionTitle(title = strings.label.presets) 72 | ListItem( 73 | modifier = Modifier 74 | .clickable { local.clearAndSetEdgeDataList(PRESET_POS_STANDARD, PRESET_SIDE_STANDARD) }, 75 | headlineContent = { Text(strings.stmt.preset_standard_headline) }, 76 | supportingContent = { Text(strings.stmt.preset_standard_supporting) } 77 | ) 78 | ListItem( 79 | modifier = Modifier 80 | .clickable { local.clearAndSetEdgeDataList(PRESET_POS_STANDARD, PRESET_SIDE_CENTERED) }, 81 | headlineContent = { Text(strings.stmt.preset_standard_c_headline) }, 82 | supportingContent = { Text(strings.stmt.preset_standard_c_supporting) } 83 | ) 84 | ListItem( 85 | modifier = Modifier 86 | .clickable { local.clearAndSetEdgeDataList(PRESET_POS_BRIGHTNESS_ONLY, PRESET_SIDE_STANDARD) }, 87 | headlineContent = { Text(strings.stmt.preset_brightness_headline) }, 88 | supportingContent = { Text(strings.stmt.preset_brightness_supporting) } 89 | ) 90 | ListItem( 91 | modifier = Modifier 92 | .clickable { local.clearAndSetEdgeDataList(PRESET_POS_BRIGHTNESS_ONLY, PRESET_SIDE_CENTERED) }, 93 | headlineContent = { Text(strings.stmt.preset_brightness_c_headline) }, 94 | supportingContent = { Text(strings.stmt.preset_brightness_c_supporting) } 95 | ) 96 | ListItem( 97 | modifier = Modifier 98 | .clickable { local.clearAndSetEdgeDataList(PRESET_POS_DOUBLE_BRIGHTNESS, PRESET_SIDE_STANDARD) }, 99 | headlineContent = { Text(strings.stmt.preset_brightness_d_headline) }, 100 | supportingContent = { Text(strings.stmt.preset_brightness_d_supporting) } 101 | ) 102 | ListItem( 103 | modifier = Modifier 104 | .clickable { local.clearAndSetEdgeDataList(PRESET_POS_DOUBLE_BRIGHTNESS, PRESET_SIDE_CENTERED) }, 105 | headlineContent = { Text(strings.stmt.preset_brightness_dc_headline) }, 106 | supportingContent = { Text(strings.stmt.preset_brightness_dc_supporting) } 107 | ) 108 | 109 | ListSectionTitle(title = strings.label.utility) 110 | ListItem( 111 | modifier = Modifier 112 | .clickable { 113 | local.editEachEdgeData { 114 | it.copy(color = Color(it.color).copy(alpha = 1f).toArgb()) 115 | } 116 | }, 117 | headlineContent = { Text(strings.stmt.show_all_headline) }, 118 | supportingContent = { Text(strings.stmt.show_all_supporting) } 119 | ) 120 | ListItem( 121 | modifier = Modifier 122 | .clickable { 123 | local.editEachEdgeData { 124 | it.copy(color = Color(it.color).copy(alpha = .01f).toArgb()) 125 | } 126 | }, 127 | headlineContent = { Text(strings.stmt.hide_all_headline) }, 128 | supportingContent = { Text(strings.stmt.hide_all_supporting) } 129 | ) 130 | 131 | Spacer(Modifier.height(50.dp)) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/components/window/main/MainWindow.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2022 LSafer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package net.lsafer.edgeseek.app.components.window.main 17 | 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.ui.Modifier 20 | import net.lsafer.edgeseek.app.Local 21 | import net.lsafer.edgeseek.app.UniRoute 22 | import net.lsafer.edgeseek.app.components.page.about.AboutPage 23 | import net.lsafer.edgeseek.app.components.page.edge_edit.EdgeEditPage 24 | import net.lsafer.edgeseek.app.components.page.edge_list.EdgeListPage 25 | import net.lsafer.edgeseek.app.components.page.home.HomePage 26 | import net.lsafer.edgeseek.app.components.page.log.LogPage 27 | import net.lsafer.edgeseek.app.components.page.permissions.PermissionsPage 28 | import net.lsafer.edgeseek.app.components.page.presets.PresetsPage 29 | import net.lsafer.edgeseek.app.components.wizard.introduction.IntroductionWizard 30 | import net.lsafer.sundry.compose.simplenav.current 31 | 32 | @Composable 33 | fun MainWindow(local: Local, modifier: Modifier = Modifier) { 34 | when (val route = local.navController.current) { 35 | is UniRoute.HomePage -> 36 | HomePage(local, route, modifier) 37 | 38 | is UniRoute.EdgeListPage -> 39 | EdgeListPage(local, route, modifier) 40 | 41 | is UniRoute.EdgeEditPage -> 42 | EdgeEditPage(local, route, modifier) 43 | 44 | is UniRoute.PermissionsPage -> 45 | PermissionsPage(local, route, modifier) 46 | 47 | is UniRoute.PresetsPage -> 48 | PresetsPage(local, route, modifier) 49 | 50 | is UniRoute.AboutPage -> 51 | AboutPage(local, route, modifier) 52 | 53 | is UniRoute.LogPage -> 54 | LogPage(local, route, modifier) 55 | 56 | is UniRoute.IntroductionWizard -> 57 | IntroductionWizard(local, route, modifier) 58 | 59 | else -> {} 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/components/wizard/introduction/IntroductionWizard.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2022 LSafer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package net.lsafer.edgeseek.app.components.wizard.introduction 17 | 18 | import android.provider.Settings 19 | import androidx.compose.foundation.layout.* 20 | import androidx.compose.material3.* 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.runtime.rememberCoroutineScope 23 | import androidx.compose.ui.Alignment 24 | import androidx.compose.ui.Modifier 25 | import androidx.compose.ui.platform.LocalContext 26 | import androidx.compose.ui.unit.dp 27 | import kotlinx.coroutines.launch 28 | import net.lsafer.edgeseek.app.* 29 | import net.lsafer.edgeseek.app.components.page.permissions.PermissionsPageContent 30 | import net.lsafer.edgeseek.app.components.page.presets.PresetsPageContent 31 | import net.lsafer.edgeseek.app.l10n.LocalStrings 32 | import net.lsafer.edgeseek.app.l10n.strings 33 | import net.lsafer.sundry.storage.edit 34 | import org.cufy.json.set 35 | 36 | @Composable 37 | fun IntroductionWizard( 38 | local: Local, 39 | route: UniRoute.IntroductionWizard, 40 | modifier: Modifier = Modifier, 41 | ) { 42 | val context = LocalContext.current 43 | val strings = LocalStrings.current 44 | val coroutineScope = rememberCoroutineScope() 45 | val steps = UniRoute.IntroductionWizard.Step.entries 46 | 47 | val onStepCancel: () -> Unit = { 48 | local.navController.back() 49 | } 50 | 51 | val onStepConfirm: () -> Unit = { 52 | val i = route.step.ordinal + 1 53 | 54 | if (i <= steps.size) 55 | local.navController.push(route.copy(step = steps[i])) 56 | } 57 | 58 | val onPermissionsStepConfirm: () -> Unit = { 59 | if ( 60 | !Settings.canDrawOverlays(context) || 61 | !Settings.System.canWrite(context) 62 | ) { 63 | coroutineScope.launch { 64 | local.snackbar.showSnackbar( 65 | strings.stmt.mandatory_permissions_not_met, 66 | ) 67 | } 68 | } else { 69 | local.dataStore.edit { it[PK_FLAG_ACTIVATED] = true } 70 | coroutineScope.launch { local.eventbus.emit(UniEvent.StartService) } 71 | onStepConfirm() 72 | } 73 | } 74 | 75 | val onComplete: () -> Unit = { 76 | local.dataStore.edit { it[PK_WIZ_INTRO] = true } 77 | while (local.navController.back()); 78 | local.navController.push(UniRoute.HomePage) 79 | } 80 | 81 | when (route.step) { 82 | UniRoute.IntroductionWizard.Step.Welcome -> { 83 | IntroductionWizardWrapper( 84 | local = local, 85 | onConfirm = onStepConfirm, 86 | onCancel = onStepCancel, 87 | modifier = modifier, 88 | ) { 89 | Box(Modifier.fillMaxSize(), Alignment.Center) { 90 | Text(strings.stmt.welcome_phrase) 91 | } 92 | } 93 | } 94 | 95 | UniRoute.IntroductionWizard.Step.Permissions -> 96 | IntroductionWizardWrapper( 97 | local = local, 98 | onConfirm = onPermissionsStepConfirm, 99 | onCancel = onStepCancel, 100 | modifier = modifier, 101 | content = { PermissionsPageContent(local) } 102 | ) 103 | 104 | UniRoute.IntroductionWizard.Step.Presets -> 105 | IntroductionWizardWrapper( 106 | local = local, 107 | onConfirm = onStepConfirm, 108 | onCancel = onStepCancel, 109 | modifier = modifier, 110 | content = { PresetsPageContent(local) } 111 | ) 112 | 113 | UniRoute.IntroductionWizard.Step.Done -> { 114 | IntroductionWizardWrapper( 115 | local = local, 116 | onConfirm = onComplete, 117 | onCancel = onStepCancel, 118 | modifier = modifier, 119 | ) { 120 | Box(Modifier.fillMaxSize(), Alignment.Center) { 121 | Text(strings.stmt.all_setup_phrase) 122 | } 123 | } 124 | } 125 | } 126 | } 127 | 128 | @Composable 129 | fun IntroductionWizardWrapper( 130 | local: Local, 131 | onConfirm: () -> Unit, 132 | onCancel: () -> Unit, 133 | modifier: Modifier = Modifier, 134 | content: @Composable () -> Unit, 135 | ) { 136 | Scaffold( 137 | Modifier 138 | .statusBarsPadding() 139 | .navigationBarsPadding() 140 | .then(modifier), 141 | snackbarHost = { 142 | SnackbarHost(local.snackbar) 143 | }, 144 | ) { 145 | Column(Modifier.fillMaxSize()) { 146 | Box( 147 | Modifier 148 | .fillMaxSize() 149 | .weight(1f) 150 | ) { 151 | content() 152 | } 153 | 154 | Surface(Modifier.fillMaxWidth()) { 155 | Row(Modifier.padding(vertical = 5.dp, horizontal = 75.dp)) { 156 | TextButton(onClick = { onCancel() }) { 157 | Text(strings.label.back) 158 | } 159 | Spacer( 160 | Modifier 161 | .fillMaxWidth() 162 | .weight(1f) 163 | ) 164 | TextButton({ onConfirm() }) { 165 | Text(strings.label.next, color = MaterialTheme.colorScheme.secondary) 166 | } 167 | } 168 | } 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/components/wrapper/UniTheme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2022 LSafer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package net.lsafer.edgeseek.app.components.wrapper 17 | 18 | import androidx.compose.foundation.isSystemInDarkTheme 19 | import androidx.compose.foundation.shape.RoundedCornerShape 20 | import androidx.compose.material3.MaterialTheme 21 | import androidx.compose.material3.Shapes 22 | import androidx.compose.material3.Typography 23 | import androidx.compose.runtime.Composable 24 | import androidx.compose.runtime.getValue 25 | import androidx.compose.runtime.produceState 26 | import androidx.compose.ui.text.TextStyle 27 | import androidx.compose.ui.text.font.FontFamily 28 | import androidx.compose.ui.text.font.FontWeight 29 | import androidx.compose.ui.unit.dp 30 | import androidx.compose.ui.unit.sp 31 | import kotlinx.coroutines.flow.distinctUntilChanged 32 | import kotlinx.coroutines.flow.filterNotNull 33 | import net.lsafer.edgeseek.app.* 34 | import net.lsafer.sundry.storage.select 35 | 36 | @Composable 37 | fun UniTheme(local: Local, content: @Composable () -> Unit) { 38 | val uiColors by produceState(UI_COLORS_DEFAULT, local) { 39 | local.dataStore 40 | .select(PK_UI_COLORS) 41 | .filterNotNull() 42 | .distinctUntilChanged() 43 | .collect { value = it } 44 | } 45 | 46 | MaterialTheme( 47 | colorScheme = when (uiColors) { 48 | UI_COLORS_BLACK -> BlackColorPalette 49 | UI_COLORS_DARK -> DarkColorPalette 50 | UI_COLORS_LIGHT -> LightColorPalette 51 | UI_COLORS_WHITE -> WhiteColorPalette 52 | 53 | else -> when { 54 | isSystemInDarkTheme() -> DarkColorPalette 55 | else -> LightColorPalette 56 | } 57 | }, 58 | typography = Typography( 59 | bodyLarge = TextStyle( 60 | fontFamily = FontFamily.Default, 61 | fontWeight = FontWeight.Normal, 62 | fontSize = 16.sp 63 | ) 64 | ), 65 | shapes = Shapes( 66 | small = RoundedCornerShape(10.dp), 67 | medium = RoundedCornerShape(10.dp), 68 | large = RoundedCornerShape(0.dp) 69 | ), 70 | content = content 71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/data/settings/edge_data.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2022 LSafer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package net.lsafer.edgeseek.app.data.settings 17 | 18 | import android.graphics.Color 19 | import kotlinx.serialization.SerialName 20 | import kotlinx.serialization.Serializable 21 | 22 | @Serializable 23 | enum class EdgeSide( 24 | val key: String, 25 | ) { 26 | Bottom("side_bottom"), 27 | Left("side_left"), 28 | Top("side_top"), 29 | Right("side_right"); 30 | 31 | fun rotate(rotation: Int) = 32 | entries[(8 + ordinal - (rotation % 4) * 1) % 4] 33 | } 34 | 35 | @Serializable 36 | enum class EdgeCorner { 37 | BottomRight, Bottom, BottomLeft, Left, 38 | TopLeft, Top, TopRight, Right; 39 | 40 | fun rotate(rotation: Int) = 41 | entries[(16 + ordinal - (rotation % 8) * 2) % 8] 42 | } 43 | 44 | @Serializable 45 | enum class EdgePos( 46 | val key: String, 47 | val side: EdgeSide, 48 | val corner: EdgeCorner, 49 | ) { 50 | BottomRight("pos_bottom_right", EdgeSide.Bottom, EdgeCorner.BottomRight), 51 | BottomCenter("pos_bottom_center", EdgeSide.Bottom, EdgeCorner.Bottom), 52 | BottomLeft("pos_bottom_left", EdgeSide.Bottom, EdgeCorner.BottomLeft), 53 | LeftBottom("pos_left_bottom", EdgeSide.Left, EdgeCorner.BottomLeft), 54 | LeftCenter("pos_left_center", EdgeSide.Left, EdgeCorner.Left), 55 | LeftTop("pos_left_top", EdgeSide.Left, EdgeCorner.TopLeft), 56 | TopLeft("pos_top_left", EdgeSide.Top, EdgeCorner.TopLeft), 57 | TopCenter("pos_top_center", EdgeSide.Top, EdgeCorner.Top), 58 | TopRight("pos_top_right", EdgeSide.Top, EdgeCorner.TopRight), 59 | RightTop("pos_right_top", EdgeSide.Right, EdgeCorner.TopRight), 60 | RightCenter("pos_right_center", EdgeSide.Right, EdgeCorner.Right), 61 | RightBottom("pos_right_bottom", EdgeSide.Right, EdgeCorner.BottomRight); 62 | 63 | fun isIncludedWhenSegmented(nSegments: Int): Boolean { 64 | return when (this) { 65 | BottomCenter, 66 | TopCenter, 67 | RightCenter, 68 | LeftCenter, 69 | -> nSegments == 1 || nSegments == 3 70 | 71 | BottomLeft, BottomRight, 72 | TopLeft, TopRight, 73 | LeftTop, LeftBottom, 74 | RightTop, RightBottom, 75 | -> nSegments == 2 || nSegments == 3 76 | } 77 | } 78 | } 79 | 80 | @Serializable 81 | sealed interface ControlFeature { 82 | @Serializable 83 | @SerialName("nothing") 84 | data object Nothing : ControlFeature 85 | @Serializable 86 | @SerialName("brightness") 87 | data object Brightness : ControlFeature 88 | @Serializable 89 | @SerialName("brightness_dimmer") 90 | data object BrightnessWithDimmer : ControlFeature 91 | @Serializable 92 | @SerialName("alarm") 93 | data object Alarm : ControlFeature 94 | @Serializable 95 | @SerialName("music") 96 | data object Music : ControlFeature 97 | @Serializable 98 | @SerialName("ring") 99 | data object Ring : ControlFeature 100 | @Serializable 101 | @SerialName("system") 102 | data object System : ControlFeature 103 | } 104 | 105 | @Serializable 106 | sealed interface ActionFeature { 107 | @Serializable 108 | @SerialName("nothing") 109 | data object Nothing : ActionFeature 110 | @Serializable 111 | @SerialName("expand_status_bar") 112 | data object ExpandStatusBar : ActionFeature 113 | } 114 | 115 | @Serializable 116 | enum class OrientationFilter { 117 | @SerialName("all") 118 | All, 119 | @SerialName("portrait_only") 120 | PortraitOnly, 121 | @SerialName("landscape_only") 122 | LandscapeOnly, ; 123 | 124 | fun test(displayRotation: Int): Boolean { 125 | return when (this) { 126 | All -> true 127 | PortraitOnly -> displayRotation % 2 == 0 128 | LandscapeOnly -> displayRotation % 2 == 1 129 | } 130 | } 131 | } 132 | 133 | @Serializable 134 | data class EdgeSideData( 135 | val side: EdgeSide, 136 | val nSegments: Int = when (side) { 137 | EdgeSide.Top, EdgeSide.Bottom -> 2 138 | EdgeSide.Left, EdgeSide.Right -> 3 139 | }, 140 | ) 141 | 142 | @Serializable 143 | data class EdgePosData( 144 | /** 145 | * The position of this edge. 146 | */ 147 | val pos: EdgePos, 148 | /** 149 | * True, if this edge is activated. 150 | */ 151 | val activated: Boolean = false, 152 | /** 153 | * The width of the edge. 154 | */ 155 | val thickness: Int = 35, 156 | /** 157 | * The color of the edge. argb 158 | */ 159 | val color: Int = Color.argb(1, 255, 0, 0), 160 | /** 161 | * The sensitivity of this edge. 162 | */ 163 | val sensitivity: Int = 45, 164 | val onSeek: ControlFeature = ControlFeature.Nothing, 165 | val onLongClick: ActionFeature = ActionFeature.Nothing, 166 | val onDoubleClick: ActionFeature = ActionFeature.Nothing, 167 | val onSwipeUp: ActionFeature = ActionFeature.Nothing, 168 | val onSwipeDown: ActionFeature = ActionFeature.Nothing, 169 | val onSwipeLeft: ActionFeature = ActionFeature.Nothing, 170 | val onSwipeRight: ActionFeature = ActionFeature.Nothing, 171 | /** 172 | * The strength of vibrations. 173 | */ 174 | val feedbackVibration: Int = 1, 175 | /** 176 | * Display a toast with the current value when seeking. 177 | */ 178 | val feedbackToast: Boolean = true, 179 | /** 180 | * Show the system panel for the currently-being-controlled volume. 181 | */ 182 | val feedbackSystemPanel: Boolean = false, 183 | /** 184 | * Stop seek at pivot points requiring user to reengage gesture for going further. 185 | */ 186 | val seekSteps: Boolean = true, 187 | val seekAcceleration: Boolean = false, 188 | val orientationFilter: OrientationFilter = OrientationFilter.All, 189 | ) 190 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/l10n/Declarations.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app.l10n 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.compositionLocalOf 5 | import androidx.compose.ui.unit.LayoutDirection 6 | import net.lsafer.edgeseek.app.l10n.strings.Strings_ar 7 | import net.lsafer.edgeseek.app.l10n.strings.Strings_en 8 | 9 | val LocalStrings = compositionLocalOf { 10 | error("CompositionLocal LocalStrings not present") 11 | } 12 | 13 | val strings @Composable get() = LocalStrings.current 14 | 15 | val Strings_default = Strings_en 16 | val Strings_all = mapOf( 17 | "en" to Strings_en, 18 | "ar" to Strings_ar, 19 | ) 20 | 21 | data class UniL10n( 22 | val lang: String, 23 | val dir: LayoutDirection, 24 | val strings: Strings, 25 | ) 26 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/l10n/Strings.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app.l10n 2 | 3 | @Suppress("PropertyName") 4 | data class Strings( 5 | val branding: Branding = Branding(), 6 | val label: Label = Label(), 7 | val stmt: Stmt = Stmt(), 8 | ) { 9 | data class Branding( 10 | val app_name: String = "Edge Seek", 11 | 12 | val lang_AR: String = "عربي", 13 | val lang_EN: String = "English", 14 | ) 15 | 16 | data class Label( 17 | val additional: String = "Additional", 18 | val appearance: String = "Appearance", 19 | val application: String = "Application", 20 | val author: String = "Author", 21 | val back: String = "Back", 22 | val cancel: String = "Cancel", 23 | val clear_log: String = "Clear Log", 24 | val confirm: String = "Confirm", 25 | val credits: String = "Credits", 26 | val dimensions: String = "Dimensions", 27 | val input: String = "Input", 28 | val job: String = "Job", 29 | val links: String = "Links", 30 | val mandatory: String = "Mandatory", 31 | val misc: String = "Misc", 32 | val next: String = "Next", 33 | val presets: String = "Presets", 34 | val utility: String = "Utility", 35 | val version: String = "Version", 36 | val version_code: String = "Version Code", 37 | val version_name: String = "Version Name", 38 | val yes: String = "Yes", 39 | ) 40 | 41 | data class Stmt( 42 | val exit_application_qm: String = "Exit the application?", 43 | val welcome_phrase: String = "Welcome", 44 | val all_setup_phrase: String = "All Set Up!", 45 | val mandatory_permissions_not_met: String = "Mandatory Permissions are not met", 46 | val watch_tutorial: String = "Watch Tutorial", 47 | val open_settings: String = "Open Settings", 48 | 49 | // Pages 50 | val page_edge_list_headline: String = "Edges", 51 | val page_edge_list_supporting: String = "Customize the edges", 52 | val page_edge_list_heading: String = "Choose Edge", 53 | val page_edge_list_summary: String = "Touch an edge to customize it", 54 | 55 | val page_edge_edit_heading: String = "Edge Configuration", 56 | 57 | val page_permissions_headline: String = "Permissions", 58 | val page_permissions_supporting: String = "Manage application's permissions", 59 | val page_permissions_heading: String = "Permissions", 60 | 61 | val page_presets_headline: String = "Presets", 62 | val page_presets_supporting: String = "Choose a set of configurations", 63 | val page_presets_heading: String = "Presets", 64 | val page_presets_summary: String = "Preset configurations", 65 | 66 | val page_about_headline: String = "About", 67 | val page_about_supporting: String = "Information about this application", 68 | val page_about_heading: String = "About", 69 | 70 | val page_log_headline: String = "Logs", 71 | val page_log_supporting: String = "Open log file", 72 | val page_log_heading: String = "Logs", 73 | 74 | // Permissions 75 | val restricted_permissions_headline: String = 76 | "Allow Restricted Permissions", 77 | val restricted_permissions_supporting: String = 78 | "In Android 13 and later, applications installed outside Google Play need to be allowed 'Restricted Permissions' explicitly.", 79 | 80 | val display_over_other_apps_headline: String = 81 | "Display Over Other Apps", 82 | val display_over_other_apps_supporting: String = 83 | "Allow this application to draw views floating on your screen", 84 | 85 | val write_system_settings_headline: String = 86 | "Write System Settings", 87 | val write_system_settings_supporting: String = 88 | "Allow this application to edit settings such as brightness level and music volume", 89 | 90 | val ignore_battery_optimizations_headline: String = 91 | "Ignore Battery Optimizations", 92 | val ignore_battery_optimizations_supporting: String = 93 | "Make this application free from the system battery optimizations.", 94 | 95 | val accessibility_service_headline: String = 96 | "Accessibility Service", 97 | val accessibility_service_supporting: String = 98 | "Allows the app to enforce a per-application whitelist/blacklist. " + 99 | "Additionally, some Oppo/OnePlus devices require this permission for Controlling audio in the background.", 100 | 101 | // App 102 | val app_activation_headline: String = "Activation", 103 | val app_activation_supporting: String = "Toggle to activate or deactivate the application", 104 | 105 | val app_colors_headline: String = "Change Theme", 106 | val app_colors_value_system: String = "System", 107 | val app_colors_value_black: String = "Black", 108 | val app_colors_value_dark: String = "Dark", 109 | val app_colors_value_light: String = "Light", 110 | val app_colors_value_white: String = "White", 111 | 112 | val app_auto_boot_headline: String = "Auto Boot", 113 | val app_auto_boot_supporting: String = "Auto boot the service every time the device booted-up", 114 | 115 | val app_brightness_reset_headline: String = "Brightness Reset", 116 | val app_brightness_reset_supporting: String = "Turn on auto brightness each time the device turn on to sleep", 117 | 118 | // Edge 119 | val edge_activation_headline: String = "Activation", 120 | val edge_activation_supporting: String = "Toggle to activate or deactivate this edge", 121 | 122 | val edge_seek_task_headline: String = "Seek Task", 123 | val edge_long_click_task_headline: String = "Long Click Task", 124 | val edge_double_click_task_headline: String = "Double Click Task", 125 | val edge_swipe_up_task_headline: String = "Swipe Up Task", 126 | val edge_swipe_down_task_headline: String = "Swipe Down Task", 127 | val edge_swipe_left_task_headline: String = "Swipe Left Task", 128 | val edge_swipe_right_task_headline: String = "Swipe Right Task", 129 | 130 | val control_feature_nothing: String = "Nothing", 131 | val control_feature_brightness: String = "Control Brightness", 132 | val control_feature_brightness_dimmer: String = "Control Brightness with Dimmer", 133 | val control_feature_alarm: String = "Control Alarm", 134 | val control_feature_music: String = "Control Music", 135 | val control_feature_ring: String = "Control Ring", 136 | val control_feature_system: String = "Control System", 137 | 138 | val action_feature_nothing: String = "Nothing", 139 | val action_feature_expand_status_bar: String = "Expand Status Bar", 140 | 141 | val edge_sensitivity_headline: String = "Sensitivity", 142 | val edge_sensitivity_supporting: String = "How much you want the edge to be sensitive", 143 | 144 | val edge_thickness_headline: String = "Thickness", 145 | val edge_thickness_supporting: String = "The thickness of the edge", 146 | 147 | val edge_color_headline: String = "Color", 148 | val edge_color_supporting: String = "The color of the edge.", 149 | 150 | val edge_feedback_toast_headline: String = "Toast", 151 | val edge_feedback_toast_supporting: String = "Display a message with the current volume when seeking", 152 | 153 | val edge_feedback_system_panel_headline: String = "System Panel", 154 | val edge_feedback_system_panel_supporting: String = "Open system panel when controling volume.", 155 | 156 | val edge_seek_steps_headline: String = "Step on pivot", 157 | val edge_seek_steps_supporting: String = "Limit seeking range around pivot points", 158 | 159 | val edge_seek_acceleration_headline: String = "Seek Acceleration", 160 | val edge_seek_acceleration_supporting: String = "Increase change rate when seeking more", 161 | 162 | val edge_feedback_vibration_headline: String = "Vibrate", 163 | val edge_feedback_vibration_supporting: String = "The strength of vibration when the edge is touched", 164 | 165 | val edge_orientation_filter_headline: String = "Orientation filter", 166 | val edge_orientation_filter_value_all: String = "Portrait and Landscape", 167 | val edge_orientation_filter_value_portrait_only: String = "Portrait Only", 168 | val edge_orientation_filter_value_landscape_only: String = "Landscape Only", 169 | 170 | // About 171 | val about_website_headline: String = "Website", 172 | val about_website_supporting: String = "The official EdgeSeek website", 173 | 174 | val about_source_code_headline: String = "Source Code", 175 | val about_source_code_supporting: String = "The application source code", 176 | 177 | val about_reintroduce_headline: String = "Re-Introduce", 178 | val about_reintroduce_supporting: String = "Run the introduction wizard", 179 | 180 | // Presets 181 | val preset_standard_headline: String = "Standard", 182 | val preset_standard_supporting: String = "Control music audio left and brightness right", 183 | 184 | val preset_standard_c_headline: String = "Standard (Centered)", 185 | val preset_standard_c_supporting: String = "Same as Standard but with centered bars", 186 | 187 | val preset_brightness_headline: String = "Brightness Only", 188 | val preset_brightness_supporting: String = "Only control brightness from the right", 189 | 190 | val preset_brightness_c_headline: String = "Brightness Only (Centered)", 191 | val preset_brightness_c_supporting: String = "Same as Brightness Only but with centered bars", 192 | 193 | val preset_brightness_d_headline: String = "Double Brightness", 194 | val preset_brightness_d_supporting: String = "Control brightness from both sides", 195 | 196 | val preset_brightness_dc_headline: String = "Double Brightness (Centered)", 197 | val preset_brightness_dc_supporting: String = "Same as Double Brightness but with centered bars (LSafer's choice)", 198 | 199 | // Preset Util 200 | 201 | val show_all_headline: String = "Show All", 202 | val show_all_supporting: String = "Increase the opacity of all edges", 203 | 204 | val hide_all_headline: String = "Hide All", 205 | val hide_all_supporting: String = "Decrease the opacity of all edges", 206 | 207 | // Foreground Service 208 | val foreground_noti_title: String = "Running In Background", 209 | val foreground_noti_text: String = """ 210 | Allows the application to work in background. 211 | Recommended disabling this notification since it has no real value. 212 | """.trimIndent(), 213 | ) 214 | } 215 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/l10n/strings/ar.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app.l10n.strings 2 | 3 | import net.lsafer.edgeseek.app.l10n.Strings 4 | 5 | val Strings_ar = Strings( 6 | branding = Strings.Branding( 7 | app_name = "السحب على الحافة", 8 | ), 9 | label = Strings.Label( 10 | additional = "إضافي", 11 | appearance = "المظهر", 12 | application = "التطبيق", 13 | author = "المبرمج", 14 | back = "رجوع", 15 | cancel = "إلغاء", 16 | clear_log = "مسح السجل", 17 | confirm = "تأكيد", 18 | credits = "الحقوق", 19 | dimensions = "الأبعاد", 20 | input = "المدخلات", 21 | job = "الوظيفة", 22 | links = "روابط", 23 | mandatory = "ضروري", 24 | misc = "أخرى", 25 | next = "التالي", 26 | presets = "النماذج", 27 | utility = "أدوات", 28 | version = "النسخة", 29 | version_code = "رقم النسخة", 30 | version_name = "إسم النسخة", 31 | yes = "نعم" 32 | ), 33 | stmt = Strings.Stmt( 34 | exit_application_qm = "إغلاق التطبيق؟", 35 | welcome_phrase = "أهلا", 36 | all_setup_phrase = "التطبيق جاهز!", 37 | mandatory_permissions_not_met = "لم يتم إعطاء الصلاحيات الإلزامية", 38 | watch_tutorial = "مشاهدة شرح", 39 | open_settings = "فتح الإعدادات", 40 | 41 | page_edge_list_headline = "الحواف", 42 | page_edge_list_supporting = "تعديل الحواف", 43 | page_edge_list_heading = "إختر حافة", 44 | page_edge_list_summary = "إلمس حافة لتعديلها", 45 | 46 | page_edge_edit_heading = "إعدادات الحافة", 47 | 48 | page_permissions_headline = "الصلاحيات", 49 | page_permissions_supporting = "إدارة صلاحيات التطبيق", 50 | page_permissions_heading = "الصلاحيات", 51 | 52 | page_presets_headline = "النماذج", 53 | page_presets_supporting = "إختيار نموذج معد مسبقا", 54 | page_presets_heading = "النماذج", 55 | page_presets_summary = "النماذج المعدة مسبقا", 56 | 57 | page_about_headline = "حول", 58 | page_about_supporting = "بيانات عن هذا التطبيق", 59 | page_about_heading = "حول", 60 | 61 | page_log_headline = "السجل", 62 | page_log_supporting = "فتح ملف السجلات", 63 | page_log_heading = "السجل", 64 | 65 | restricted_permissions_headline = 66 | "السماح بالإعدادات المحظورة", 67 | restricted_permissions_supporting = 68 | "في نسخ أندرويد 13 فما فوق, التطبيقات المثبتة خارج جوجل بلاي تحتاج أن يسمح لها باستخدام 'الإعدادات المحظورة' يدويا", 69 | 70 | display_over_other_apps_headline = "إظهار على التطبيقات الأخرى", 71 | display_over_other_apps_supporting = "السماح لهذا التطبيق برسم مناظر عائمة على شاشتك", 72 | 73 | write_system_settings_headline = "كتابة إعدادات النظام", 74 | write_system_settings_supporting = "السماح لهذا التطبيق بتعديل الإعدادات مثل مستوى السطوع ومستوى الصوت", 75 | 76 | ignore_battery_optimizations_headline = "تجاهل تحسينات البطارية", 77 | ignore_battery_optimizations_supporting = "اجعل هذا التطبيق خاليًا من تحسينات بطارية النظام.", 78 | 79 | accessibility_service_headline = "خدمات تسهيل الوصول", 80 | accessibility_service_supporting = 81 | "التطبيق يستخدم صلاحيات تسهيل الوصول ليستطيع التوقف عن العمل في التطبيقات المحددة " + 82 | "إضافة إلى ذلك, بعض أجهزة OnePlus/Oppo تحتاج صلاحية تسهيل الوصول لتعديل مستوى الصوت في الخلفية", 83 | 84 | app_activation_headline = "التفعيل", 85 | app_activation_supporting = "إختر لتفعيل أو تعطيل التطبيق", 86 | 87 | app_colors_headline = "تغير المظهر", 88 | app_colors_value_system = "إتبع مظهر النظام", 89 | app_colors_value_black = "أسود", 90 | app_colors_value_dark = "داكن", 91 | app_colors_value_light = "فاتح", 92 | app_colors_value_white = "أبيض", 93 | 94 | app_auto_boot_headline = "تشغيل تلقالئي", 95 | app_auto_boot_supporting = "فعل التطبيق تلقائيا عند تشغيل الجهاز", 96 | 97 | app_brightness_reset_headline = "ضبط السطوع", 98 | app_brightness_reset_supporting = "تفعيل السطوع التلقائي عند قفل الجهاز", 99 | 100 | edge_activation_headline = "التفعيل", 101 | edge_activation_supporting = "إختر لفتعيل أو تعطيل الحافة", 102 | 103 | edge_seek_task_headline = "وظيفة السحب", 104 | edge_long_click_task_headline = "وظيفة الضغط مطولا", 105 | edge_double_click_task_headline = "وظيفة الضغط المزدوج", 106 | edge_swipe_up_task_headline = "وظيفة السحب للأعلى", 107 | edge_swipe_down_task_headline = "وظيفة السحب للأسفل", 108 | edge_swipe_left_task_headline = "وظيفة السحب لليسار", 109 | edge_swipe_right_task_headline = "وظيفة السحب لليمين", 110 | 111 | control_feature_nothing = "لا شيء", 112 | control_feature_brightness = "التحكم بالسطوع", 113 | control_feature_brightness_dimmer = "التحكم بالسطوع مع إمكانية خفض السطوع أكثر", 114 | control_feature_alarm = "التحكم بصوت المنبه", 115 | control_feature_music = "التحكم بصوت الوسائط", 116 | control_feature_ring = "التحكم بصوت الرنين", 117 | control_feature_system = "التحكم بأصوات النظام", 118 | 119 | action_feature_nothing = "لا شيء", 120 | action_feature_expand_status_bar = "فتح شريط الحالة", 121 | 122 | edge_sensitivity_headline = "الحساسية", 123 | edge_sensitivity_supporting = "تغيير حساسية الحافة", 124 | 125 | edge_thickness_headline = "السماكة", 126 | edge_thickness_supporting = "تغيير سماكة الحافة", 127 | 128 | edge_color_headline = "اللون", 129 | edge_color_supporting = "تغيير لون الحافة", 130 | 131 | edge_feedback_toast_headline = "الرقم الحالي", 132 | edge_feedback_toast_supporting = "إظهار الرقم الحالي كرسالة صغيرة عند السحب", 133 | 134 | edge_feedback_system_panel_headline = "الرقم الحالي (النظام)", 135 | edge_feedback_system_panel_supporting = "إظهار شريط النظام عند تغيير الرقم الحالي", 136 | 137 | edge_seek_steps_headline = "الوقوف عند الصفر", 138 | edge_seek_steps_supporting = "توقف عن السحب عند الوصول إلى الصفر", 139 | 140 | edge_seek_acceleration_headline = "التغير المتسارع", 141 | edge_seek_acceleration_supporting = "زيادة تسارع التغيير عند الإستمرار بالسحب", 142 | 143 | edge_feedback_vibration_headline = "الإهتزاز", 144 | edge_feedback_vibration_supporting = "قوة الإهتزاز عند لمس الحافة", 145 | 146 | edge_orientation_filter_headline = "وضع دوران الجهاز", 147 | edge_orientation_filter_value_all = "عمودي وأفقي", 148 | edge_orientation_filter_value_portrait_only = "عمودي فقط", 149 | edge_orientation_filter_value_landscape_only = "أفقي فقط", 150 | 151 | about_website_headline = "الموقع الإلكتروني", 152 | about_website_supporting = "الموقع الرسمي لهذا التطبيق", 153 | 154 | about_source_code_headline = "الكود المصدر", 155 | about_source_code_supporting = "رابط الكود المصدر لهذا التطبيق", 156 | 157 | about_reintroduce_headline = "إعادة تقديم", 158 | about_reintroduce_supporting = "إعادة تقديم التطبيق", 159 | 160 | preset_standard_headline = "عادي", 161 | preset_standard_supporting = "تحكم بالسطوع من اليمين وبأصوات الوسائط من اليسار", 162 | 163 | preset_standard_c_headline = "عادي (وسط)", 164 | preset_standard_c_supporting = "مثل 'عادي' لكن فقط في وسط الحافة", 165 | 166 | preset_brightness_headline = "سطوع فقط", 167 | preset_brightness_supporting = "تحكم بالسطوع من اليمين", 168 | 169 | preset_brightness_c_headline = "سطوع فقط (وسط)", 170 | preset_brightness_c_supporting = "مثل 'سطوع فقط' لكن فقط في وسط الحافة", 171 | 172 | preset_brightness_d_headline = "سطوع مزدوج", 173 | preset_brightness_d_supporting = "تحكم بالسطوع من اليمين واليسار", 174 | 175 | preset_brightness_dc_headline = "سطوع مزدوج (وسط)", 176 | preset_brightness_dc_supporting = "مثل 'سطوع مزدوج' لكن في وسط الحافة (مفضل من قبل المطور)", 177 | 178 | show_all_headline = "إظهار الكل", 179 | show_all_supporting = "إظهار كل الحواف", 180 | 181 | hide_all_headline = "إخفاء الكل", 182 | hide_all_supporting = "إخفاء كل الحواف", 183 | 184 | foreground_noti_title = "يعمل بالخلفية", 185 | foreground_noti_text = """ 186 | تسمح هذه الرسالة للتطبيق بالعمل بالخلفية. 187 | ننصح بتعطيل هذه الرسالة 188 | """.trimIndent(), 189 | ) 190 | ) 191 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/l10n/strings/en.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app.l10n.strings 2 | 3 | import net.lsafer.edgeseek.app.l10n.Strings 4 | 5 | val Strings_en = Strings() 6 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/module.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app 2 | 3 | import co.touchlab.kermit.Logger 4 | 5 | /* ============= ------------------ ============= */ 6 | 7 | internal val moduleLogger = Logger.withTag("net.lsafer.edgeseek.app") 8 | 9 | /* ============= ------------------ ============= */ 10 | 11 | const val PK_FLAG_ACTIVATED = "f.activated" 12 | const val PK_FLAG_AUTO_BOOT = "f.auto_boot" 13 | const val PK_FLAG_BRIGHTNESS_RESET = "f.brightness_reset" 14 | 15 | const val PK_WIZ_INTRO = "wiz.intro" 16 | 17 | const val PK_UI_LANG = "ui.lang" 18 | const val PK_UI_COLORS = "ui.colors" 19 | 20 | const val UI_LANG_DEFAULT = "en-US" 21 | const val UI_LANG_AR_SA = "ar-SA" 22 | const val UI_LANG_EN_US = "en-US" 23 | 24 | const val UI_COLORS_DEFAULT = "system" 25 | const val UI_COLORS_SYSTEM = "system" 26 | const val UI_COLORS_BLACK = "black" 27 | const val UI_COLORS_DARK = "dark" 28 | const val UI_COLORS_LIGHT = "light" 29 | const val UI_COLORS_WHITE = "white" 30 | 31 | /* ============= ------------------ ============= */ 32 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/presets.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app 2 | 3 | import net.lsafer.edgeseek.app.data.settings.* 4 | 5 | val PRESET_SIDE_STANDARD = listOf( 6 | EdgeSideData( 7 | side = EdgeSide.Top, 8 | nSegments = 1, 9 | ), 10 | EdgeSideData( 11 | side = EdgeSide.Bottom, 12 | nSegments = 1, 13 | ), 14 | EdgeSideData( 15 | side = EdgeSide.Left, 16 | nSegments = 1, 17 | ), 18 | EdgeSideData( 19 | side = EdgeSide.Right, 20 | nSegments = 1, 21 | ), 22 | ) 23 | 24 | val PRESET_SIDE_CENTERED = listOf( 25 | EdgeSideData( 26 | side = EdgeSide.Top, 27 | nSegments = 1, 28 | ), 29 | EdgeSideData( 30 | side = EdgeSide.Bottom, 31 | nSegments = 1, 32 | ), 33 | EdgeSideData( 34 | side = EdgeSide.Left, 35 | nSegments = 3, 36 | ), 37 | EdgeSideData( 38 | side = EdgeSide.Right, 39 | nSegments = 3, 40 | ), 41 | ) 42 | 43 | val PRESET_POS_STANDARD = listOf( 44 | // Left 45 | EdgePosData( 46 | pos = EdgePos.LeftCenter, 47 | activated = true, 48 | onSeek = ControlFeature.Music 49 | ), 50 | // Right 51 | EdgePosData( 52 | pos = EdgePos.RightCenter, 53 | activated = true, 54 | onSeek = ControlFeature.Brightness 55 | ), 56 | ) 57 | 58 | val PRESET_POS_BRIGHTNESS_ONLY = listOf( 59 | // Right 60 | EdgePosData( 61 | pos = EdgePos.RightCenter, 62 | activated = true, 63 | onSeek = ControlFeature.Brightness 64 | ), 65 | ) 66 | 67 | val PRESET_POS_DOUBLE_BRIGHTNESS = listOf( 68 | // Left 69 | EdgePosData( 70 | pos = EdgePos.LeftCenter, 71 | activated = true, 72 | onSeek = ControlFeature.Brightness 73 | ), 74 | // Right 75 | EdgePosData( 76 | pos = EdgePos.RightCenter, 77 | activated = true, 78 | onSeek = ControlFeature.Brightness 79 | ), 80 | ) 81 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/scripts/createUniL10nState.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app.scripts 2 | 3 | import androidx.compose.ui.unit.LayoutDirection 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.flow.StateFlow 7 | import kotlinx.coroutines.flow.distinctUntilChanged 8 | import kotlinx.coroutines.flow.map 9 | import net.lsafer.edgeseek.app.l10n.Strings_all 10 | import net.lsafer.edgeseek.app.l10n.Strings_default 11 | import net.lsafer.edgeseek.app.l10n.UniL10n 12 | import net.lsafer.edgeseek.app.util.firstShareStateIn 13 | import net.lsafer.edgeseek.app.util.langIsRTL 14 | import net.lsafer.edgeseek.app.util.langSelect 15 | 16 | suspend fun createUniL10nState( 17 | language: Flow, 18 | defaultLanguage: String, 19 | coroutineScope: CoroutineScope 20 | ): StateFlow { 21 | return language 22 | .map { it ?: defaultLanguage } 23 | .distinctUntilChanged() 24 | .map { 25 | UniL10n( 26 | lang = it, 27 | dir = when { 28 | langIsRTL(it) -> LayoutDirection.Rtl 29 | else -> LayoutDirection.Ltr 30 | }, 31 | strings = Strings_all[langSelect( 32 | languages = Strings_all.keys, 33 | ranges = listOf(it), 34 | )] ?: Strings_default, 35 | ) 36 | } 37 | .firstShareStateIn(coroutineScope) 38 | } 39 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/theme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2022 LSafer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:SuppressLint("ConflictingOnColor") 18 | 19 | package net.lsafer.edgeseek.app 20 | 21 | import android.annotation.SuppressLint 22 | import androidx.compose.material3.darkColorScheme 23 | import androidx.compose.material3.lightColorScheme 24 | import androidx.compose.ui.graphics.Color 25 | 26 | val Purple500 = Color(0xff905AD3) 27 | 28 | val Gray200 = Color(0xff858585) 29 | val Gray300 = Color(0xff808080) 30 | val Gray400 = Color(0xFF6E6E6E) 31 | val Gray500 = Color(0xFF5C5C5C) 32 | val Gray600 = Color(0xFF5A5A5A) 33 | val Gray700 = Color(0xff494949) 34 | val Gray800 = Color(0xFF424242) 35 | val Gray900 = Color(0xff303030) 36 | 37 | val Cyan100 = Color(0xFFB5BDBD) 38 | val Cyan500 = Color(0xFF00B1B8) 39 | val Cyan900 = Color(0xff01579B) 40 | 41 | val BlackColorPalette = darkColorScheme( 42 | primary = Gray800, 43 | // primaryVariant = Gray500, 44 | onPrimary = Color.White, 45 | secondary = Color.Red, 46 | background = Color.Black, 47 | onBackground = Color.White, 48 | surface = Color.Black, 49 | onSurface = Color.White 50 | ) 51 | 52 | val DarkColorPalette = darkColorScheme( 53 | primary = Gray600, 54 | // primaryVariant = Gray300, 55 | onPrimary = Color.White, 56 | secondary = Cyan500, 57 | background = Gray900, 58 | onBackground = Color.White, 59 | surface = Gray900, 60 | onSurface = Color.White 61 | ) 62 | 63 | val LightColorPalette = lightColorScheme( 64 | primary = Gray400, 65 | // primaryVariant = Gray700, 66 | onPrimary = Color.White, 67 | secondary = Cyan900, 68 | background = Cyan100, 69 | onBackground = Color.Black, 70 | surface = Cyan100, 71 | onSurface = Color.Black 72 | ) 73 | 74 | val WhiteColorPalette = lightColorScheme( 75 | primary = Gray200, 76 | // primaryVariant = Gray600, 77 | onPrimary = Color.Black, 78 | secondary = Purple500, 79 | background = Color.White, 80 | onBackground = Color.Black, 81 | surface = Color.White, 82 | onSurface = Color.Black 83 | ) 84 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/util/flow.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app.util 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.flow.* 5 | 6 | fun StateFlow.mapShareStateIn( 7 | scope: CoroutineScope, 8 | block: (T) -> R, 9 | ): StateFlow { 10 | val started: SharingStarted = SharingStarted.Eagerly 11 | return drop(1).map { block(it) } 12 | .shareIn(scope, started, replay = 1) 13 | .stateIn(scope, started, block(value)) 14 | } 15 | 16 | suspend fun Flow.firstShareStateIn(scope: CoroutineScope): StateFlow { 17 | val started: SharingStarted = SharingStarted.Eagerly 18 | return shareIn(scope, started, replay = 1).let { 19 | it.stateIn(scope, started, it.first()) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/util/kermit.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app.util 2 | 3 | import co.touchlab.kermit.* 4 | import kotlinx.datetime.Clock 5 | import kotlinx.datetime.TimeZone 6 | import kotlinx.datetime.toLocalDateTime 7 | 8 | class SimpleLogWriter( 9 | private val formatter: MessageStringFormatter, 10 | private val onLog: (message: String, throwable: Throwable?) -> Unit, 11 | ) : LogWriter() { 12 | override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) { 13 | onLog(formatter.formatMessage(severity, Tag(tag), Message(message)), throwable) 14 | } 15 | } 16 | 17 | class SimpleLogFormatter( 18 | private val timeZone: TimeZone, 19 | private val clock: Clock, 20 | ) : MessageStringFormatter { 21 | override fun formatMessage(severity: Severity?, tag: Tag?, message: Message) = buildString { 22 | val datetime = clock.now().toLocalDateTime(timeZone) 23 | 24 | append(datetime.year) 25 | append('-') 26 | append(datetime.monthNumber) 27 | append('-') 28 | append(datetime.dayOfMonth) 29 | append(' ') 30 | append(datetime.hour) 31 | append(':') 32 | append(datetime.minute) 33 | append(':') 34 | append(datetime.second) 35 | append(' ') 36 | if (tag != null) append("${tag.tag}: ") 37 | if (severity != null) append("[${severity.name.uppercase()}] - ") 38 | append(message.message) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/util/lang.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app.util 2 | 3 | fun langIsRTL(languageTag: String): Boolean { 4 | val language = languageTag 5 | .splitToSequence('_', '-') 6 | .firstOrNull() 7 | 8 | return when (language) { 9 | "ae", /* Avestan */ 10 | "ar", /* 'العربية', Arabic */ 11 | "arc", /* Aramaic */ 12 | "bcc", /* 'بلوچی مکرانی', Southern Balochi */ 13 | "bqi", /* 'بختياري', Bakthiari */ 14 | "ckb", /* 'Soranî / کوردی', Sorani */ 15 | "dv", /* Dhivehi */ 16 | "fa", /* 'فارسی', Persian */ 17 | "glk", /* 'گیلکی', Gilaki */ 18 | "ku", /* 'Kurdî / كوردی', Kurdish */ 19 | "mzn", /* 'مازِرونی', Mazanderani */ 20 | "nqo", /* N'Ko */ 21 | "pnb", /* 'پنجابی', Western Punjabi */ 22 | "prs", /* 'دری', Darī */ 23 | "ps", /* 'پښتو', Pashto, */ 24 | "sd", /* 'سنڌي', Sindhi */ 25 | "ug", /* 'Uyghurche / ئۇيغۇرچە', Uyghur */ 26 | "ur", /* 'اردو', Urdu */ 27 | -> true 28 | 29 | else -> false 30 | } 31 | } 32 | 33 | @Deprecated("default is too ambiguous") 34 | fun langSelect(languages: Collection, ranges: List, default: String): String { 35 | return langSelect(languages, ranges) ?: default 36 | } 37 | 38 | /** 39 | * @param languages the available languages 40 | * @param ranges the requested languages 41 | */ 42 | fun langSelect(languages: Collection, ranges: List): String? { 43 | if (ranges.isEmpty() || languages.isEmpty()) 44 | return null 45 | 46 | for (range in ranges) { 47 | // Special language range ("*") is ignored in lookup. 48 | if (range == "*") 49 | continue 50 | 51 | var normalRange = range.lowercase() 52 | 53 | while (normalRange.isNotEmpty()) { 54 | val regex = normalRange 55 | .replace("*", "[a-z0-9]*") 56 | .toRegex() 57 | 58 | for (language in languages) { 59 | val normalLanguage = language.lowercase() 60 | 61 | if (normalLanguage.matches(regex)) 62 | return language 63 | } 64 | 65 | // Truncate from the end.... 66 | normalRange = truncateRange(normalRange) 67 | } 68 | } 69 | 70 | return null 71 | } 72 | 73 | /* Truncate the range from end during the lookup match; copy-paste from java LocaleMatcher.truncateRange */ 74 | private fun truncateRange(rangeForRegex: String): String { 75 | var rangeForRegexVar = rangeForRegex 76 | var index = rangeForRegexVar.lastIndexOf('-') 77 | if (index >= 0) { 78 | rangeForRegexVar = rangeForRegexVar.substring(0, index) 79 | 80 | // if range ends with an extension key, truncate it. 81 | index = rangeForRegexVar.lastIndexOf('-') 82 | if (index >= 0 && index == rangeForRegexVar.length - 2) { 83 | rangeForRegexVar = rangeForRegexVar.substring(0, rangeForRegexVar.length - 2) 84 | } 85 | } else { 86 | rangeForRegexVar = "" 87 | } 88 | return rangeForRegexVar 89 | } 90 | -------------------------------------------------------------------------------- /app/src/commonMain/kotlin/util/lifecycle.kt: -------------------------------------------------------------------------------- 1 | package net.lsafer.edgeseek.app.util 2 | 3 | import androidx.compose.runtime.* 4 | import androidx.lifecycle.Lifecycle 5 | import androidx.lifecycle.LifecycleEventObserver 6 | 7 | @Composable 8 | fun Lifecycle.observeAsState(): Lifecycle.State { 9 | var state by remember { mutableStateOf(this.currentState) } 10 | 11 | DisposableEffect(this) { 12 | val observer = LifecycleEventObserver { _, event -> 13 | state = event.targetState 14 | } 15 | 16 | this@observeAsState.addObserver(observer) 17 | 18 | onDispose { 19 | this@observeAsState.removeObserver(observer) 20 | } 21 | } 22 | 23 | return state 24 | } 25 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.plugin.extraProperties 2 | 3 | plugins { 4 | // this is necessary to avoid the plugins to be loaded multiple times 5 | // in each subproject's classloader 6 | alias(libs.plugins.kotlin.multiplatform) apply false 7 | alias(libs.plugins.kotlin.compose) apply false 8 | alias(libs.plugins.jetbrains.compose) apply false 9 | alias(libs.plugins.android.application) apply false 10 | } 11 | 12 | group = "net.lsafer.edgeseek" 13 | version = "0.3-pre.6" 14 | project.extraProperties.set("version_code", 20) 15 | project.extraProperties.set("application_id", "lsafer.edgeseek") 16 | 17 | tasks.wrapper { 18 | gradleVersion = "8.9" 19 | } 20 | 21 | subprojects { 22 | version = rootProject.version 23 | group = buildString { 24 | append(rootProject.group) 25 | generateSequence(project.parent) { it.parent } 26 | .forEach { 27 | append(".") 28 | append(it.name) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | Control the audio and/or brightness by sliding your fingers alongside 2 | the edges of your screen. Each edge can be configured separately. 3 | Use the 'Control Brightness with Dimmer' feature to go below zero when 4 | controlling the brightness. 5 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LSafer/edgeseek/9a1e96750034b23ac242e3bd3229f20e786f0904/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_20250403-084816_edgeseek.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LSafer/edgeseek/9a1e96750034b23ac242e3bd3229f20e786f0904/fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_20250403-084816_edgeseek.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_20250403-084826_edgeseek.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LSafer/edgeseek/9a1e96750034b23ac242e3bd3229f20e786f0904/fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_20250403-084826_edgeseek.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_20250403-084840_edgeseek.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LSafer/edgeseek/9a1e96750034b23ac242e3bd3229f20e786f0904/fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_20250403-084840_edgeseek.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_20250403-084856_edgeseek.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LSafer/edgeseek/9a1e96750034b23ac242e3bd3229f20e786f0904/fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_20250403-084856_edgeseek.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_20250403-084922_edgeseek.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LSafer/edgeseek/9a1e96750034b23ac242e3bd3229f20e786f0904/fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_20250403-084922_edgeseek.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Control audio and brightness by sliding on edges 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/title.txt: -------------------------------------------------------------------------------- 1 | Edge Seek 2 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin = "2.1.0" 3 | kotlinx-serialization = "1.8.1" 4 | kotlinx-coroutines = "1.10.1" 5 | kotlinx-datetime = "0.6.1" 6 | 7 | compose = "1.7.0" 8 | 9 | extkt = "0.1.3" 10 | 11 | lsafer-sundry = "285d879ab5" 12 | 13 | # ANDROID 14 | agp = "8.7.3" 15 | android-compileSdk = "35" 16 | android-minSdk = "24" 17 | android-targetSdk = "35" 18 | 19 | androidx-activity = "1.10.1" 20 | androidx-appcompat = "1.7.0" 21 | androidx-core = "1.15.0" 22 | androidx-lifecycle = "2.8.4" 23 | androidx-cardview = "1.0.0" 24 | 25 | material3-adaptive = "1.0.0" 26 | 27 | [plugins] 28 | kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 29 | kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 30 | kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 31 | 32 | jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "compose" } 33 | 34 | android-application = { id = "com.android.application", version.ref = "agp" } 35 | 36 | gmazzo-buildConfig = { id = "com.github.gmazzo.buildconfig", version = "5.6.2" } 37 | 38 | [libraries] 39 | 40 | ##### Official Dependencies ##### 41 | 42 | kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } 43 | kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } 44 | kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } 45 | 46 | okio = { module = "com.squareup.okio:okio", version = "3.10.2" } 47 | 48 | ##### Internal Dependencies ##### 49 | 50 | extkt-json = { module = "org.cufy.extkt:extkt-json", version.ref = "extkt" } 51 | 52 | lsafer-sundry-compose = { module = "net.lsafer.sundry:sundry-compose", version.ref = "lsafer-sundry" } 53 | lsafer-sundry-compose-adaptive = { module = "net.lsafer.sundry:sundry-compose-adaptive", version.ref = "lsafer-sundry" } 54 | lsafer-sundry-storage = { module = "net.lsafer.sundry:sundry-storage", version.ref = "lsafer-sundry" } 55 | 56 | ##### Community Dependencies ##### 57 | 58 | touchlab-kermit = { module = "co.touchlab:kermit", version = "2.0.3" } 59 | 60 | godaddy-colorpickerCompose = { module = "com.godaddy.android.colorpicker:compose-color-picker", version = "0.4.2" } 61 | skydoves-colorpickerCompose = { module = "com.github.skydoves:colorpicker-compose", version = "1.1.2" } 62 | 63 | ##### ANDROID Dependencies ##### 64 | 65 | androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } 66 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } 67 | androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } 68 | androidx-cardview = { module = "androidx.cardview:cardview", version.ref = "androidx-cardview" } 69 | 70 | ##### ANDROID (JB) Dependencies ##### 71 | 72 | androidx-lifecycle-runtime-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } 73 | 74 | androidx-lifecycle-viewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel", version.ref = "androidx-lifecycle" } 75 | androidx-lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } 76 | 77 | material3-adaptive = { module = "org.jetbrains.compose.material3.adaptive:adaptive", version.ref = "material3-adaptive" } 78 | material3-adaptive-layout = { module = "org.jetbrains.compose.material3.adaptive:adaptive-layout", version.ref = "material3-adaptive" } 79 | material3-adaptive-navigation = { module = "org.jetbrains.compose.material3.adaptive:adaptive-navigation", version.ref = "material3-adaptive" } 80 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LSafer/edgeseek/9a1e96750034b23ac242e3bd3229f20e786f0904/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Use the maximum available, or set MAX_FD != -1 to use that value. 89 | MAX_FD=maximum 90 | 91 | warn () { 92 | echo "$*" 93 | } >&2 94 | 95 | die () { 96 | echo 97 | echo "$*" 98 | echo 99 | exit 1 100 | } >&2 101 | 102 | # OS specific support (must be 'true' or 'false'). 103 | cygwin=false 104 | msys=false 105 | darwin=false 106 | nonstop=false 107 | case "$( uname )" in #( 108 | CYGWIN* ) cygwin=true ;; #( 109 | Darwin* ) darwin=true ;; #( 110 | MSYS* | MINGW* ) msys=true ;; #( 111 | NONSTOP* ) nonstop=true ;; 112 | esac 113 | 114 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 115 | 116 | 117 | # Determine the Java command to use to start the JVM. 118 | if [ -n "$JAVA_HOME" ] ; then 119 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 120 | # IBM's JDK on AIX uses strange locations for the executables 121 | JAVACMD=$JAVA_HOME/jre/sh/java 122 | else 123 | JAVACMD=$JAVA_HOME/bin/java 124 | fi 125 | if [ ! -x "$JAVACMD" ] ; then 126 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 127 | 128 | Please set the JAVA_HOME variable in your environment to match the 129 | location of your Java installation." 130 | fi 131 | else 132 | JAVACMD=java 133 | if ! command -v java >/dev/null 2>&1 134 | then 135 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 136 | 137 | Please set the JAVA_HOME variable in your environment to match the 138 | location of your Java installation." 139 | fi 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 147 | # shellcheck disable=SC3045 148 | MAX_FD=$( ulimit -H -n ) || 149 | warn "Could not query maximum file descriptor limit" 150 | esac 151 | case $MAX_FD in #( 152 | '' | soft) :;; #( 153 | *) 154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 155 | # shellcheck disable=SC3045 156 | ulimit -n "$MAX_FD" || 157 | warn "Could not set maximum file descriptor limit to $MAX_FD" 158 | esac 159 | fi 160 | 161 | # Collect all arguments for the java command, stacking in reverse order: 162 | # * args from the command line 163 | # * the main class name 164 | # * -classpath 165 | # * -D...appname settings 166 | # * --module-path (only if needed) 167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 168 | 169 | # For Cygwin or MSYS, switch paths to Windows format before running java 170 | if "$cygwin" || "$msys" ; then 171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 173 | 174 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 175 | 176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 177 | for arg do 178 | if 179 | case $arg in #( 180 | -*) false ;; # don't mess with options #( 181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 182 | [ -e "$t" ] ;; #( 183 | *) false ;; 184 | esac 185 | then 186 | arg=$( cygpath --path --ignore --mixed "$arg" ) 187 | fi 188 | # Roll the args list around exactly as many times as the number of 189 | # args, so each arg winds up back in the position where it started, but 190 | # possibly modified. 191 | # 192 | # NB: a `for` loop captures its iteration list before it begins, so 193 | # changing the positional parameters here affects neither the number of 194 | # iterations, nor the values presented in `arg`. 195 | shift # remove old arg 196 | set -- "$@" "$arg" # push replacement arg 197 | done 198 | fi 199 | 200 | 201 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 202 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 203 | 204 | # Collect all arguments for the java command; 205 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 206 | # shell script including quotes and variable substitutions, so put them in 207 | # double quotes to make sure that they get re-expanded; and 208 | # * put everything else in single quotes, so that it's not re-expanded. 209 | 210 | set -- \ 211 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 212 | -classpath "$CLASSPATH" \ 213 | org.gradle.wrapper.GradleWrapperMain \ 214 | "$@" 215 | 216 | # Stop when "xargs" is not available. 217 | if ! command -v xargs >/dev/null 2>&1 218 | then 219 | die "xargs is not available" 220 | fi 221 | 222 | # Use "xargs" to parse quoted args. 223 | # 224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 225 | # 226 | # In Bash we could simply go: 227 | # 228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 229 | # set -- "${ARGS[@]}" "$@" 230 | # 231 | # but POSIX shell has neither arrays nor command substitution, so instead we 232 | # post-process each arg (as a line of input to sed) to backslash-escape any 233 | # character that might be a shell metacharacter, then use eval to reverse 234 | # that process (while maintaining the separation between arguments), and wrap 235 | # the whole thing up as a single "set" statement. 236 | # 237 | # This will of course break if any of these variables contains a newline or 238 | # an unmatched quote. 239 | # 240 | 241 | eval "set -- $( 242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 243 | xargs -n1 | 244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 245 | tr '\n' ' ' 246 | )" '"$@"' 247 | 248 | exec "$JAVACMD" "$@" 249 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "edgeseek" 2 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 3 | 4 | pluginManagement { 5 | repositories { 6 | google { 7 | mavenContent { 8 | includeGroupAndSubgroups("androidx") 9 | includeGroupAndSubgroups("com.android") 10 | includeGroupAndSubgroups("com.google") 11 | } 12 | } 13 | mavenCentral() 14 | gradlePluginPortal() 15 | maven("https://jitpack.io") 16 | } 17 | } 18 | 19 | plugins { 20 | id("org.gradle.toolchains.foojay-resolver-convention") version ("0.9.0") 21 | } 22 | 23 | dependencyResolutionManagement { 24 | repositories { 25 | mavenCentral() 26 | google { 27 | mavenContent { 28 | includeGroupAndSubgroups("androidx") 29 | includeGroupAndSubgroups("com.android") 30 | includeGroupAndSubgroups("com.google") 31 | } 32 | } 33 | maven("https://jitpack.io") 34 | } 35 | } 36 | 37 | include(":app") 38 | --------------------------------------------------------------------------------