├── .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 |
--------------------------------------------------------------------------------