From 54409b63121fe6d6bca3c71dee9980da5944fab7 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Thu, 20 Mar 2025 14:55:22 +0100 Subject: [PATCH 001/118] fix #133 --- .../jrpie/android/launcher/ui/HomeActivity.kt | 22 +++++++------------ .../launcher/ui/TouchGestureDetector.kt | 14 ++++++++++-- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt index a658cd1..53a0876 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt @@ -2,10 +2,10 @@ package de.jrpie.android.launcher.ui import android.annotation.SuppressLint import android.content.SharedPreferences +import android.content.res.Configuration import android.content.res.Resources import android.os.Build import android.os.Bundle -import android.util.DisplayMetrics import android.view.KeyEvent import android.view.MotionEvent import android.view.View @@ -56,22 +56,11 @@ class HomeActivity : UIObject, AppCompatActivity() { super.onCreate(savedInstanceState) super.onCreate() - val displayMetrics = DisplayMetrics() - - @Suppress("deprecation") // required to support API < 30 - windowManager.defaultDisplay.getMetrics(displayMetrics) - - val width = displayMetrics.widthPixels - val height = displayMetrics.heightPixels - touchGestureDetector = TouchGestureDetector( - this, - width, - height, + this, 0, 0, LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f ) - - + touchGestureDetector.updateScreenSize(windowManager) // Initialise layout binding = HomeBinding.inflate(layoutInflater) @@ -103,6 +92,11 @@ class HomeActivity : UIObject, AppCompatActivity() { } } + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + touchGestureDetector.updateScreenSize(windowManager) + } + override fun onStart() { super.onStart() diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt b/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt index 9000fa8..8e8ed4e 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt @@ -5,8 +5,10 @@ import android.graphics.Insets import android.os.Build import android.os.Handler import android.os.Looper +import android.util.DisplayMetrics import android.view.MotionEvent import android.view.ViewConfiguration +import android.view.WindowManager import androidx.annotation.RequiresApi import de.jrpie.android.launcher.actions.Gesture import de.jrpie.android.launcher.preferences.LauncherPreferences @@ -17,8 +19,8 @@ import kotlin.math.tan class TouchGestureDetector( private val context: Context, - val width: Int, - val height: Int, + var width: Int, + var height: Int, var edgeWidth: Float ) { private val ANGULAR_THRESHOLD = tan(Math.PI / 6) @@ -319,6 +321,14 @@ class TouchGestureDetector( } } + fun updateScreenSize(windowManager: WindowManager) { + val displayMetrics = DisplayMetrics() + @Suppress("deprecation") // required to support API < 30 + windowManager.defaultDisplay.getMetrics(displayMetrics) + width = displayMetrics.widthPixels + height = displayMetrics.heightPixels + } + @RequiresApi(Build.VERSION_CODES.Q) fun setSystemGestureInsets(insets: Insets) { systemGestureInsetTop = insets.top From 7fc58fe38482a692b297a746358e3a04daf01a87 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Thu, 20 Mar 2025 15:52:12 +0100 Subject: [PATCH 002/118] 0.1.3 --- app/build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/43.txt | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/43.txt diff --git a/app/build.gradle b/app/build.gradle index 349f75c..c280794 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,8 +23,8 @@ android { minSdkVersion 21 targetSdkVersion 35 compileSdk 35 - versionCode 42 - versionName "0.1.2" + versionCode 43 + versionName "0.1.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/fastlane/metadata/android/en-US/changelogs/43.txt b/fastlane/metadata/android/en-US/changelogs/43.txt new file mode 100644 index 0000000..2bca600 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/43.txt @@ -0,0 +1 @@ +* Fixed gesture detection in landscape orientation From 8e140e2e69d88ab3190642cc271ffb0f793e7c82 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Thu, 20 Mar 2025 16:23:01 +0100 Subject: [PATCH 003/118] rename tab "Apps" to "Actions" and "Volume Up/Down" to "Volume Up/Down Key" --- .../jrpie/android/launcher/ui/settings/SettingsActivity.kt | 2 +- app/src/main/res/values-de/strings.xml | 6 +++--- app/src/main/res/values/strings.xml | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/settings/SettingsActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/settings/SettingsActivity.kt index 4c464e5..cd59726 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/settings/SettingsActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/settings/SettingsActivity.kt @@ -109,7 +109,7 @@ class SettingsActivity : AppCompatActivity(), UIObject { } private val TAB_TITLES = arrayOf( - R.string.settings_tab_app, + R.string.settings_tab_actions, R.string.settings_tab_launcher, R.string.settings_tab_meta ) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 735aa9d..d07ff7d 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -14,7 +14,7 @@ - --> Einstellungen - Apps + Aktionen Launcher Meta Settings - Apps + Actions Launcher Meta @@ -85,9 +85,9 @@ Λ (Reverse) Bottom right -> top mid -> bottom left - Volume Up + Volume Up Key Press the volume up button - Volume Down + Volume Down Key Press the volume down button Double Click Double click an empty area From b4608ef15338891d9715ec7ca133c05bbde3ec28 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Mon, 24 Mar 2025 13:21:58 +0100 Subject: [PATCH 004/118] add new action: show recent apps --- app/src/main/AndroidManifest.xml | 4 +-- .../launcher/actions/LauncherAction.kt | 12 +++++++- .../lock/LauncherAccessibilityService.kt | 30 +++++++++++++++---- .../launcher/actions/lock/LockMethod.kt | 1 + .../main/res/drawable/baseline_apps_24.xml | 11 +++++++ app/src/main/res/values/strings.xml | 21 ++++++++++--- 6 files changed, 67 insertions(+), 12 deletions(-) create mode 100644 app/src/main/res/drawable/baseline_apps_24.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 93f6ce8..a5f8831 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -85,7 +85,7 @@ @@ -97,4 +97,4 @@ - \ No newline at end of file + diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt b/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt index 5d2be94..ee9502c 100644 --- a/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt +++ b/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt @@ -11,7 +11,9 @@ import android.view.KeyEvent import android.widget.Toast import androidx.appcompat.content.res.AppCompatResources import de.jrpie.android.launcher.Application +import de.jrpie.android.launcher.BuildConfig import de.jrpie.android.launcher.R +import de.jrpie.android.launcher.actions.lock.LauncherAccessibilityService import de.jrpie.android.launcher.apps.AppFilter import de.jrpie.android.launcher.apps.hidePrivateSpaceWhenLocked import de.jrpie.android.launcher.apps.isPrivateSpaceSupported @@ -132,6 +134,14 @@ enum class LauncherAction( R.drawable.baseline_settings_applications_24, ::expandSettingsPanel ), + RECENT_APPS( + "recent_apps", + R.string.list_other_recent_apps, + R.drawable.baseline_apps_24, + LauncherAccessibilityService::openRecentApps, + false, + { _ -> BuildConfig.USE_ACCESSIBILITY_SERVICE } + ), LOCK_SCREEN( "lock_screen", R.string.list_other_lock_screen, @@ -142,7 +152,7 @@ enum class LauncherAction( "toggle_torch", R.string.list_other_torch, R.drawable.baseline_flashlight_on_24, - ::toggleTorch + ::toggleTorch, ), NOP("nop", R.string.list_other_nop, R.drawable.baseline_not_interested_24, {}); diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/lock/LauncherAccessibilityService.kt b/app/src/main/java/de/jrpie/android/launcher/actions/lock/LauncherAccessibilityService.kt index a8ef6f2..7cb32d9 100644 --- a/app/src/main/java/de/jrpie/android/launcher/actions/lock/LauncherAccessibilityService.kt +++ b/app/src/main/java/de/jrpie/android/launcher/actions/lock/LauncherAccessibilityService.kt @@ -22,26 +22,44 @@ class LauncherAccessibilityService : AccessibilityService() { companion object { private const val TAG = "Launcher Accessibility" + private const val ACTION_REQUEST_ENABLE = "ACTION_REQUEST_ENABLE" const val ACTION_LOCK_SCREEN = "ACTION_LOCK_SCREEN" + const val ACTION_RECENT_APPS = "ACTION_RECENT_APPS" - fun lockScreen(context: Context) { + private fun invoke(context: Context, action: String, failureMessageRes: Int) { try { context.startService( Intent( context, LauncherAccessibilityService::class.java ).apply { - action = ACTION_LOCK_SCREEN + this.action = action }) - } catch (e: Exception) { + } catch (_: Exception) { Toast.makeText( context, - context.getString(R.string.alert_lock_screen_failed), + context.getString(failureMessageRes), Toast.LENGTH_LONG ).show() } } + fun lockScreen(context: Context) { + if (!isEnabled(context)) { + showEnableDialog(context) + } else { + invoke(context, ACTION_LOCK_SCREEN, R.string.alert_lock_screen_failed) + } + } + + fun openRecentApps(context: Context) { + if (!isEnabled(context)) { + showEnableDialog(context) + } else { + invoke(context, ACTION_RECENT_APPS, R.string.alert_recent_apps_failed) + } + } + fun isEnabled(context: Context): Boolean { val enabledServices = Settings.Secure.getString( context.contentResolver, @@ -58,7 +76,7 @@ class LauncherAccessibilityService : AccessibilityService() { setView(R.layout.dialog_consent_accessibility) setTitle(R.string.dialog_consent_accessibility_title) setPositiveButton(R.string.dialog_consent_accessibility_ok) { _, _ -> - lockScreen(context) + invoke(context, ACTION_REQUEST_ENABLE, R.string.alert_enable_accessibility_failed) } setNegativeButton(R.string.dialog_cancel) { _, _ -> } }.create().also { it.show() }.apply { @@ -94,7 +112,9 @@ class LauncherAccessibilityService : AccessibilityService() { } when (action) { + ACTION_REQUEST_ENABLE -> {} // do nothing ACTION_LOCK_SCREEN -> handleLockScreen() + ACTION_RECENT_APPS -> performGlobalAction(GLOBAL_ACTION_RECENTS) } } return super.onStartCommand(intent, flags, startId) diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/lock/LockMethod.kt b/app/src/main/java/de/jrpie/android/launcher/actions/lock/LockMethod.kt index 541510a..93b4cbf 100644 --- a/app/src/main/java/de/jrpie/android/launcher/actions/lock/LockMethod.kt +++ b/app/src/main/java/de/jrpie/android/launcher/actions/lock/LockMethod.kt @@ -6,6 +6,7 @@ import android.widget.Button import androidx.appcompat.app.AlertDialog import de.jrpie.android.launcher.BuildConfig import de.jrpie.android.launcher.R +import de.jrpie.android.launcher.actions.lock.LauncherAccessibilityService import de.jrpie.android.launcher.preferences.LauncherPreferences diff --git a/app/src/main/res/drawable/baseline_apps_24.xml b/app/src/main/res/drawable/baseline_apps_24.xml new file mode 100644 index 0000000..c5a49a0 --- /dev/null +++ b/app/src/main/res/drawable/baseline_apps_24.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c426a5c..83b963b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -252,6 +252,7 @@ Music: Previous Music: Play / Pause Expand notifications panel + Recent Apps Do nothing Lock Screen Toggle Torch @@ -307,6 +308,8 @@ No camera with torch detected. Error: Can\'t access torch. Error: Failed to lock screen. (If you just upgraded the app, try to disable and re-enable the accessibility service in phone settings) + Error: Failed to show recent apps. (If you just upgraded the app, try to disable and re-enable the accessibility service in phone settings) + Error: Failed to enable the accessibility service. μLauncher\'s accessibility service is not enabled. Please enable it in settings Private space locked Private space unlocked @@ -315,12 +318,17 @@ Lock private space Unlock private space Error: Locking the screen using accessibility is not supported on this device. Please use device admin instead. - μLauncher - lock screen + μLauncher - Setting μLauncher as an accessibility service allows it to lock the screen. + Setting μLauncher as an accessibility service allows it to lock the screen and open the recent apps menu. Note that excessive permissions are required. You should never grant such permissions lightly to any app. - μLauncher will use the accessibility service only for locking the screen. You can check the source code to make sure. + μLauncher will use the accessibility service only for performing the following actions when requested by the user: + + * lock screen + * open recent apps + + μLauncher will never use the accessibility service to collect data. You can check the source code to make sure. Note that locking the screen can also be accomplished by granting μLauncher device administrator permissions. However that method doesn\'t work with fingerprint and face unlock. @@ -365,7 +373,12 @@ I am aware that other options exist (using device administrator privileges or the power button). I consent to μLauncher using the accessibility service to provide functionality unrelated to accessibility. I consent to μLauncher not collecting any data. - far-reaching privileges to μLauncher.
μLauncher will use these privileges only to lock the screen. μLauncher will never collect any data. In particular, μLauncher does not use the accessibility service to collect any data.]]>
+ far-reaching privileges to μLauncher.
μLauncher will use these privileges only to perform the following actions: +
    +
  • Lock Screen
  • +
  • Recent Apps
  • +
+ μLauncher will never collect any data. In particular, μLauncher does not use the accessibility service to collect any data.]]>
Activating the Accessibility Service Activate Accessibility Service Cancel From 5d695ec0ea49177cf4627196e8ebcf2e7a1ce886 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sat, 29 Mar 2025 18:45:53 +0100 Subject: [PATCH 005/118] fix #135 --- app/src/main/java/de/jrpie/android/launcher/Functions.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/de/jrpie/android/launcher/Functions.kt b/app/src/main/java/de/jrpie/android/launcher/Functions.kt index 7bbbdb5..df57008 100644 --- a/app/src/main/java/de/jrpie/android/launcher/Functions.kt +++ b/app/src/main/java/de/jrpie/android/launcher/Functions.kt @@ -38,6 +38,8 @@ import androidx.core.net.toUri const val LOG_TAG = "Launcher" +const val REQUEST_SET_DEFAULT_HOME = 42 + fun isDefaultHomeScreen(context: Context): Boolean { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val roleManager = context.getSystemService(RoleManager::class.java) @@ -62,8 +64,9 @@ fun setDefaultHomeScreen(context: Context, checkDefault: Boolean = false) { && !isDefault // using role manager only works when µLauncher is not already the default. ) { val roleManager = context.getSystemService(RoleManager::class.java) - context.startActivity( - roleManager.createRequestRoleIntent(RoleManager.ROLE_HOME) + context.startActivityForResult( + roleManager.createRequestRoleIntent(RoleManager.ROLE_HOME), + REQUEST_SET_DEFAULT_HOME ) return } From 653d16b269f609d9de0c523d9f3bd56930e29302 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sat, 29 Mar 2025 21:09:15 +0100 Subject: [PATCH 006/118] new action: launch other launchers --- .../java/de/jrpie/android/launcher/Functions.kt | 2 +- .../android/launcher/actions/LauncherAction.kt | 15 +++++++++++++++ app/src/main/res/drawable/baseline_home_24.xml | 11 +++++++++++ app/src/main/res/values/strings.xml | 1 + 4 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/drawable/baseline_home_24.xml diff --git a/app/src/main/java/de/jrpie/android/launcher/Functions.kt b/app/src/main/java/de/jrpie/android/launcher/Functions.kt index df57008..afc2c31 100644 --- a/app/src/main/java/de/jrpie/android/launcher/Functions.kt +++ b/app/src/main/java/de/jrpie/android/launcher/Functions.kt @@ -61,7 +61,7 @@ fun setDefaultHomeScreen(context: Context, checkDefault: Boolean = false) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && context is Activity - && !isDefault // using role manager only works when µLauncher is not already the default. + && checkDefault // using role manager only works when µLauncher is not already the default. ) { val roleManager = context.getSystemService(RoleManager::class.java) context.startActivityForResult( diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt b/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt index ee9502c..6ba467e 100644 --- a/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt +++ b/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt @@ -154,6 +154,12 @@ enum class LauncherAction( R.drawable.baseline_flashlight_on_24, ::toggleTorch, ), + LAUNCH_OTHER_LAUNCHER( + "launcher_other_launcher", + R.string.list_other_launch_other_launcher, + R.drawable.baseline_home_24, + ::launchOtherLauncher + ), NOP("nop", R.string.list_other_nop, R.drawable.baseline_not_interested_24, {}); override fun invoke(context: Context, rect: Rect?): Boolean { @@ -258,6 +264,15 @@ private fun expandSettingsPanel(context: Context) { } } +private fun launchOtherLauncher(context: Context) { + context.startActivity( + Intent.createChooser( + Intent(Intent.ACTION_MAIN).also { it.addCategory(Intent.CATEGORY_HOME) }, + context.getString(R.string.list_other_launch_other_launcher) + ) + ) +} + private fun openSettings(context: Context) { context.startActivity(Intent(context, SettingsActivity::class.java)) } diff --git a/app/src/main/res/drawable/baseline_home_24.xml b/app/src/main/res/drawable/baseline_home_24.xml new file mode 100644 index 0000000..935d1b6 --- /dev/null +++ b/app/src/main/res/drawable/baseline_home_24.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 83b963b..a8bc9b3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -256,6 +256,7 @@ Do nothing Lock Screen Toggle Torch + Launch other Home Screen Add Shortcut From e7c1d285766bb0f8a3625653158d70a15b72abf1 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sun, 13 Apr 2025 14:40:57 +0200 Subject: [PATCH 007/118] upgrade AGP --- .scripts/release.sh | 2 +- build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.scripts/release.sh b/.scripts/release.sh index 0c71f4a..f207c87 100755 --- a/.scripts/release.sh +++ b/.scripts/release.sh @@ -1,5 +1,5 @@ #!/bin/bash -export JAVA_HOME="/usr/lib/jvm/java-23-openjdk/" +export JAVA_HOME="/usr/lib/jvm/java-21-openjdk/" OUTPUT_DIR="$HOME/launcher-release" BUILD_TOOLS_DIR="$HOME/Android/Sdk/build-tools/35.0.0" KEYSTORE="$HOME/data/keys/launcher_jrpie.jks" diff --git a/build.gradle b/build.gradle index 2bb501f..57dd74a 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext.kotlin_version = '2.0.0' - ext.android_plugin_version = '8.9.0' + ext.android_plugin_version = '8.9.1' repositories { google() mavenCentral() @@ -10,7 +10,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:8.9.0' + classpath 'com.android.tools.build:gradle:8.9.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "com.android.tools.build:gradle:$android_plugin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" From 0441b3fd3d76cde098c496327b42af20a2f9b929 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sun, 13 Apr 2025 14:54:11 +0200 Subject: [PATCH 008/118] set max width for choose app button; change label from scegliere l'applicazione to scegliere in Italian --- app/src/main/res/layout/settings_actions_row.xml | 6 ++++-- app/src/main/res/values-it/strings.xml | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/settings_actions_row.xml b/app/src/main/res/layout/settings_actions_row.xml index df449c4..a60a5a4 100644 --- a/app/src/main/res/layout/settings_actions_row.xml +++ b/app/src/main/res/layout/settings_actions_row.xml @@ -43,13 +43,15 @@ android:id="@+id/settings_actions_row_button_choose" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:ellipsize="end" + android:maxLines="2" + android:maxWidth="100dp" android:text="@string/settings_apps_choose" android:textAllCaps="false" android:visibility="invisible" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="parent" - /> + app:layout_constraintTop_toTopOf="parent" /> Scorri verso destra sul bordo inferiore dello schermo Aspetto - Scegliere l\'applicazione + Scegliere Tema Questo launcher è progettato per essere minimale, efficiente e privo di distrazioni. Non contiene pagamenti, pubblicità o servizi di tracciamento. Predefinito From e6dd2634ae434bdde7a47fb24e8f5b251f7e3c3a Mon Sep 17 00:00:00 2001 From: class0068 Date: Tue, 18 Mar 2025 19:41:41 +0000 Subject: [PATCH 009/118] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (254 of 254 strings) Translation: jrpie-Launcher/Launcher Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/zh_Hans/ --- app/src/main/res/values-zh-rCN/strings.xml | 32 +++++++++++----------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 55376c3..12e6a68 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -104,7 +104,7 @@ 不要在应用抽屉中显示被绑定到手势的应用 此功能需要 Android 6 或更高版本。 应用程序已隐藏。您可在设置中让它再次显示。 - µLauncher 需要获得设备管理员权限才能够锁定屏幕。 + µLauncher 需要激活“设备管理应用”权限才能够锁定屏幕。 这是执行锁屏操作所必需的。 µLauncher - 锁屏 收藏的应用 @@ -158,16 +158,16 @@ 边缘宽度 重命名 启用锁屏动作 - μLauncher 的无障碍服务未启用,请在设置中启用它。 - 错误:此设备不支持使用无障碍功能锁定屏幕。请改用设备管理员模式。 - 使用无障碍服务 - 使用设备管理员模式 + μLauncher 的“无障碍”服务未启用,请在设置中启用它。 + 错误:此设备不支持使用“无障碍”服务锁定屏幕。请改用激活“设备管理应用”权限。 + 使用“无障碍”服务 + 激活“设备管理应用”权限 取消 亮色 快速设置 - 错误:锁定屏幕失败。(如果您刚刚升级了本启动器,请尝试在手机设置中手动禁用并重新启用无障碍服务) + 错误:锁定屏幕失败。(如果您刚刚升级了本启动器,请尝试在手机设置中手动禁用并重新启用“无障碍”服务) 在屏幕边缘滑动 - 将 µLauncher 设为无障碍服务允许其锁定屏幕。请注意,这需要过多的权限。你永远不应该轻易地授予任何应用程序这样的权限。µLauncher 将仅使用无障碍服务功能锁屏。您可以审核源代码。请注意,锁屏也可以通过授予 µLauncher 设备管理员权限来实现,然而,这种方法不适用于以指纹和面部解锁。 + 将 µLauncher 设置为“无障碍”服务以允许其锁定屏幕。请注意,这会使 µLauncher 获得额外的权限。你永远不应该轻易地授予任何应用程序这样的权限。获得授权后“无障碍”服务将仅被用于锁定屏幕。您可以审核我们的源代码。请注意,锁定屏幕也可以通过激活 µLauncher 的“设备管理应用”权限来实现,然而,这种方法无法与于指纹解锁和面部解锁兼容。 返回 红色 蓝色 @@ -193,8 +193,8 @@ 复制到剪贴板 此功能需要 Android 15 或更高版本。 切换私人空间锁 - 激活无障碍服务 - 正在激活无障碍服务 + 激活“无障碍”服务 + 正在激活“无障碍”服务 开源许可证 开源许可证 在应用列表中显示 @@ -219,8 +219,8 @@ 有2种方式可以用来锁定屏幕。 遗憾的是,两者都有缺点:

-

通过设置“设备管理应用”

- 无法和指纹解锁和脸部解锁共同使用。 +

通过激活“设备管理应用”权限

+ 该方法无法和指纹解锁和脸部解锁共同使用。

@@ -229,16 +229,16 @@ 需要更多的权限。 μLauncher 将这些权限仅用于锁定屏幕。
- (对于任何一个从网上下载的应用所做的类似声明,你都不应该抱持“默认为可信”的态度,你可以并应该检查一下它的源代码.) + (对于任何一个从网上下载的应用所做的类似声明,你都不应该抱持“默认为可信”的态度,你可以并应该检查一下它的源代码.)
- 在某些设备上,激活辅助功能服务后,启动PIN码将不再用于加密数据。 - 如果遇到该问题,可以通过该方法重新激活启动PIN码用于数据加密。 + 在某些设备上,激活“无障碍”服务后,启动 PIN 码将不再用于加密数据。 + 如果遇到该问题,可以通过该方法重新激活启动 PIN 码用于数据加密。



你可以在设置中随时更改这个选项。 ]]> 搜索(不触发自动启动匹配项) - 广泛且重要的权限。
μLauncher 将这些权限仅用于锁定屏幕。µLauncher 绝不会收集任何数据。尤其是,μLauncher 不会使用“无障碍”服务来收集任何数据。]]>
+ 广泛且重要的权限。
但 μLauncher 仅会在需要锁定屏幕时使用这些权限。µLauncher 绝不会收集任何数据。尤其是,μLauncher 不会使用“无障碍”服务来收集任何数据。]]>
(从)左上 (滑向)中右(滑向)左下 单击 + 上滑 单击 + 下滑 @@ -263,7 +263,7 @@ 报告安全漏洞 请不要在 Github 上以公开的方式报告安全漏洞,请使用以下方式进行报告: 感谢您帮助改进 µLauncher!\n请考虑在您的应用程序错误反馈中添加以下信息: - 我已知晓,还有其他替代方法(使用设备管理员模式或电源按键)。 + 我已知晓,还有其他替代方法(激活“设备管理应用”权限或通过电源按键)。 我同意 μLauncher 不收集任何数据。 捐赠 调整音量 From 8a487eb4c78a07b1f646bc64227625ff3694fce8 Mon Sep 17 00:00:00 2001 From: class0068 Date: Thu, 20 Mar 2025 23:06:32 +0000 Subject: [PATCH 010/118] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (254 of 254 strings) Translation: jrpie-Launcher/Launcher Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/zh_Hans/ --- app/src/main/res/values-zh-rCN/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 12e6a68..e5ca8ac 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -13,7 +13,7 @@ 无法打开应用 要更改其设置吗? 打开设置,为该手势绑定一个应用程序 - 应用程序 + 快捷操作 启动器 杂项 From 4f801427a41d890788e1e3dd54eb1cc8d1513592 Mon Sep 17 00:00:00 2001 From: toolatebot Date: Fri, 21 Mar 2025 00:07:18 +0000 Subject: [PATCH 011/118] Update translation files Updated by "Cleanup translation files" add-on in Weblate. Translation: jrpie-Launcher/Launcher Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/ --- app/src/main/res/values-es/strings.xml | 1 - app/src/main/res/values-fr/strings.xml | 1 - app/src/main/res/values-it/strings.xml | 1 - app/src/main/res/values-ja/strings.xml | 1 - app/src/main/res/values-pt-rBR/strings.xml | 1 - app/src/main/res/values-tr/strings.xml | 1 - app/src/main/res/values-zh-rCN/strings.xml | 1 - 7 files changed, 7 deletions(-) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index ccbeb3f..4a363b0 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -23,7 +23,6 @@ - --> Configuración - Aplicaciones Launcher Meta Réglages - Applications Launcher Meta Configurações - Apps Launcher Meta No se pudo abrir la aplicación ¿Desea cambiar la configuración? - Abra la configuración para elegir una aplicación para esta acción + Abra la configuración para elegir una acción para este gesto - Deslizar Arriba + Arriba Doble Arriba - Deslizar Abajo + Abajo Doble Abajo - Deslizar Izquierda + Izquierda Doble Izquierda - Deslizar Derecha + Derecha Doble Derecha - Subir Volumen - Bajar Volumen + Tecla para subir el volumen + Tecla para bajar el volumen Doble Click Click Largo - Toca la fecha - Toca el reloj + Fecha + reloj Elegir Aplicación Instalar aplicaciones No se encontró la Store @@ -72,7 +72,7 @@ --> Seleccionar Launcher Información de la aplicación - Ver tutorial de Launcher + Ver el tutorial de µLauncher Configuración por defecto Todas sus preferencias se eliminarán. Desea continuar? Reportar un error @@ -91,11 +91,11 @@ Otros Desinstalar Información - Buscar Aplicaciones - Configuración de Launcher + Buscar + Configuración de μLauncher Aplicaciones - Música: Subir - Música: Bajar + Subir el volumen + Bajar volumen Música: Siguiente Música: Anterior Nada @@ -105,19 +105,214 @@ - --> Tutorial - Tómate unos segundos para aprender a usar este launcher + 👋\n\n¡Tómate unos segundos para aprender a utilizar este Launcher! Concepto - Launcher está diseñado para ser minimalista, eficiente y libre de distracciones.\n\nEs gratis y libre de anuncios y servicios de rastreo. - La aplicación es de código abierto (licencia MIT) y está disponible en GitHub!\n\nNo olvides echarle un vistazo al repositorio! + μLauncher está diseñado para ser mínimo, eficiente y libre de distracciones.\n\nNo contiene anuncios ni recopila datos. + ¡Es software libre (licencia MIT)!\n¡No olvides visitar el repositorio! Uso Tu pantalla de inicio contiene la fecha y hora local. Sin distracciones. - Puedes iniciar tus aplicaciones con solo presionar o deslizar una vez. Elige algunas en la siguiente pantalla. + Puede iniciar sus aplicaciones más importantes con gestos táctiles o presionando botones. Puesta a punto - Elegimos algunas aplicaciones por defecto para ti, si lo deseas puedes cambiarlas ahora. + Elegimos algunas aplicaciones predeterminadas para ti. Puedes cambiarlos ahora si lo deseas: También puedes cambiar tu selección más tarde. Vamos! - Estás listo para iniciar!\n\n Esperamos que esto te sea de gran ayuda!\n\n- Finn M Glas\n(el desarrollador) + ¡Estás listo para comenzar!\n\n¡Espero que esto sea de gran valor para ti!\n\n- Finn (quien creó Launcher) y Josia (quien hizo algunas mejoras y mantiene la bifurcación μLauncher) Iniciar Configuración Más opciones + atrás + Toca + Derecha Toca y desliza hacia la derecha + Desliza el dedo hacia la izquierda en la parte inferior de la pantalla + Desliza hacia abajo en el borde derecho de la pantalla + Botón de retroceso / gesto de retroceso + Desliza el dedo hacia arriba en el borde izquierdo de la pantalla + Desliza el dedo hacia arriba en el borde derecho de la pantalla + Desliza el dedo hacia abajo en el borde izquierdo de la pantalla + Dinámico + Color + Toca y desliza hacia arriba + Toque + Izquierda + Desliza el dedo hacia la derecha en la parte inferior de la pantalla + Desliza el dedo hacia la izquierda en la parte superior de la pantalla + Abajo (borde izquierdo) + Presione el botón para subir el volumen + Presione la barra espaciadora para desactivar esta función temporalmente. + Aplicaciones + Arriba a la izquierda -> Abajo en el medio -> Arriba a la derecha + Presione regresar mientras busca en la lista de aplicaciones para iniciar una búsqueda web. + Error: No se puede acceder a la antorcha. + Fondo (lista de aplicaciones y configuración) + Tema de color + Fuente + Iconos de aplicaciones monocromáticos + Derecha (arriba) + Derecha (Abajo) + Izquierda (arriba) + nunca recopilará ningún dato. En particular, μLauncher no utiliza el servicio de accesibilidad para recopilar ningún dato.]]> + Configurar μLauncher como un servicio de accesibilidad le permite bloquear la pantalla. Tenga en cuenta que se requieren permisos excesivos. Nunca debe otorgar dichos permisos a la ligera a ninguna aplicación. μLauncher utilizará el servicio de accesibilidad solo para bloquear la pantalla. Puedes consultar el código fuente para asegurarte. Tenga en cuenta que también se puede bloquear la pantalla otorgando permisos de administrador del dispositivo μLauncher. Sin embargo, ese método no funciona con el desbloqueo mediante huellas dactilares y rostro. + Ocultar el espacio privado de la lista de aplicaciones + Diseño de la lista de aplicaciones + Espacio privado + No se detectó ninguna cámara con linterna. + Activar o desactivar el bloqueo del espacio privado + Activar o desactivar la antorcha + Ocultar aplicaciones pausadas + Aplicaciones ocultas + Cambiar el nombre de %1$s + Cancelar + V + Informar un error + mostrar + μLauncher debe ser la pantalla de inicio predeterminada para acceder al espacio privado. + No informe vulnerabilidades de seguridad públicamente en GitHub, sino utilice lo siguiente: + Informar sobre una vulnerabilidad de seguridad + Crear informe + Únete al chat de μLauncher + Donar + Ajustar el volumen + Expandir el panel de notificaciones + El espacio privado no está disponible + Desliza hacia arriba + Haga doble clic en un área vacía + Presione el botón para bajar el volumen + Haga clic largo en un área vacía + Haga clic en la fecha + Haga clic en el reloj + Mostrar la hora + Sombra de texto + Ocultar la barra de estado + Ocultar la barra de navegación + Deslizar con dos dedos + predeterminado + Aplicaciones favoritas + Aplicaciones ocultas + Deshacer + Doy mi consentimiento para que μLauncher utilice el servicio de accesibilidad para proporcionar una funcionalidad no relacionada con la accesibilidad. + Buscar (sin inicio automático) + Soy consciente de que esto otorgará privilegios de gran alcance a μLauncher. + Soy consciente de que existen otras opciones (utilizando privilegios de administrador del dispositivo o el botón de encendido). + Licencias de código abierto + Licencias de código abierto + versión + Todas las aplicaciones + Elija el método para bloquear +
+ + Administrador del dispositivo + No funciona con desbloqueo por huella dactilar ni reconocimiento facial. + +
+
+ + Servicio de Accesibilidad + Requiere privilegios excesivos. + μLauncher utilizará esos privilegios solo para bloquear la pantalla. +
+ (Realmente no deberías confiar en una aplicación aleatoria que acabas de descargar con tal afirmación, pero puedes consultar el código fuente). +
+ En algunos dispositivos, el PIN de inicio ya no se utilizará para cifrar datos después de activar un servicio de accesibilidad. + Esto se puede reactivar posteriormente. + +



+ Puede cambiar su selección más tarde en la configuración. + ]]>
+ Utilice el servicio de accesibilidad + Usar el administrador del dispositivo + Elija el método para bloquear la pantalla + Rojo + alfa + Azul + Verde + Sans serif + Serif + Monoespaciado + Serif monoespaciado + Mostrar la fecha + Utilice el formato de fecha localizado + Mostrar segundos + Cambiar fecha y hora + Girar la pantalla + Acciones de deslizamiento de borde + Desliza el dedo por el borde de la pantalla + Ancho del borde + No mostrar aplicaciones que estén vinculadas a un gesto en la lista de aplicaciones + Ver el código fuente + ¡Gracias por ayudarnos a mejorar μLauncher!\nConsidere agregar la siguiente información a su informe de error: + Copiar al portapapeles + Añadir a favoritos + Eliminar de favoritos + Esconder + Renombrar + Aplicaciones favoritas + Espacio privado + Puede buscar rápidamente entre todas las aplicaciones en la lista de aplicaciones.\n\nDesliza hacia arriba para abrirlo o asigne un gesto diferente. + Una vez que sólo coincide una aplicación, se inicia automáticamente.\nEsto se puede desactivar anteponiendo un espacio a la consulta. + Error: No se puede expandir la barra de estado. Esta acción utiliza una funcionalidad que no forma parte de la API de Android publicada. Lamentablemente, no parece funcionar en su dispositivo. + Esta funcionalidad requiere Android 6 o posterior. + Esta funcionalidad requiere Android 15 o posterior. + Aplicación oculta. Puedes hacerlo visible nuevamente en la configuración. + Configuración rápida + μLauncher debe ser administrador del dispositivo para poder bloquear la pantalla. + Esto es necesario para la acción de la pantalla de bloqueo. + Habilitar la acción de bloqueo de pantalla + Error: Error al bloquear la pantalla. (Si acaba de actualizar la aplicación, intente deshabilitar y volver a habilitar el servicio de accesibilidad en la configuración del teléfono) + El servicio de accesibilidad de μLauncher no está habilitado. Por favor, habilítelo en la configuración + Espacio privado bloqueado + Espacio privado desbloqueado + Bloquear el espacio privado + Desbloquear espacio privado + Error: El bloqueo de la pantalla mediante accesibilidad no es compatible con este dispositivo. En su lugar, utilice el administrador del dispositivo. + μLauncher - pantalla de bloqueo + Color + Elige el color + Doy mi consentimiento para que μLauncher no recopile ningún dato. + Activación del servicio de accesibilidad + Activar el servicio de accesibilidad + No se encontró ninguna aplicación para gestionar la búsqueda. + No se puede abrir la URL: no se encontró ningún navegador. + Acciones + Toca + Arriba + Desliza hacia arriba con dos dedos + Desliza hacia abajo + Toque + Abajo + Toca y desliza hacia abajo + Desliza hacia abajo con dos dedos + Desliza hacia la izquierda + Toca y desliza hacia la izquierda + Desliza dos dedos hacia la izquierda + Desliza hacia la derecha + Toque + Derecha + Desliza con dos dedos hacia la derecha + Desliza el dedo hacia la derecha en la parte superior de la pantalla + Izquierda (Abajo) + Arriba (borde izquierdo) + Arriba (borde derecho) + Abajo (borde derecho) + Arriba a la izquierda -> centro a la derecha -> abajo a la izquierda + Abajo a la izquierda -> centro a la derecha -> arriba a la izquierda + Arriba a la derecha -> centro a la izquierda -> abajo a la derecha + Abajo a la derecha -> centro a la izquierda -> arriba a la derecha + V (Reversa) + Arriba a la derecha -> Abajo en el medio -> Arriba a la izquierda + Λ + Abajo a la izquierda -> arriba en el medio -> abajo a la derecha + Λ (Inverso) + Abajo a la derecha -> arriba en el medio -> abajo a la izquierda + Atenuación + Sólido + Valor predeterminado del sistema + Transparente + Difuminar + Buscar en la web + texto + Red + Música: Reproducir / Pausa + Pantalla de bloqueo + Agregar acceso directo + Vincular al gesto + Mostrar en la lista de aplicaciones + Invertir la lista de aplicaciones From bfc84b57ca573a8699dd6a9c29dc2482ca4c61c7 Mon Sep 17 00:00:00 2001 From: Vossa Excelencia Date: Sat, 22 Mar 2025 16:48:40 +0000 Subject: [PATCH 013/118] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (254 of 254 strings) Translation: jrpie-Launcher/Launcher Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/pt_BR/ --- app/src/main/res/values-pt-rBR/strings.xml | 57 +++++++++++++--------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index c39f0cb..4b2bcd2 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -37,8 +37,8 @@ Para cima (borda direita) Para baixo (borda esquerda) Para baixo (borda direita) - Aumento de volume - Diminuição de volume + Botão de aumentar volume + Botão de diminuição de volume Toque duplo Toque longo Data @@ -76,7 +76,7 @@ --> Definir o μLauncher como tela inicial Informações do aplicativo - Ver tutorial do launcher + Ver tutorial do µLauncher Redefinir configuraçãos Você vai descartar todas as suas preferências. Continuar? Reportar um bug @@ -96,10 +96,10 @@ Desinstalar Informações do aplicativo Busque - Configurações do µLauncher + Configurações do μLauncher Todos os apps - Música: Mais alto - Música: Mais silencioso + Aumento de volume + Diminuição de volume Música: Próximo Música: Anterior Não faça nada @@ -109,18 +109,18 @@ - --> Tutorial - Tire alguns segundos para aprender a usar este Launcher! + 👋\n\nTire alguns segundos para aprender a usar este Launcher! Conceito - O Launcher foi criado para ser minimalista, eficiente e livre de distrações. Ele é livre de pagamentos, anúncios e serviços de rastreamento. - O app é de código aberto (licença MIT) e está disponível no GitHub! Não deixe de conferir o repositório! + O μLauncher foi criado para ser minimalista, eficiente e livre de distrações. \n\n\nNão contém anúncios e não coleta dados. + É um software livre (sob licença MIT)!\nNão deixe de conferir o repositório! Uso Sua tela inicial contém a data e hora local. Sem distrações. - Você pode iniciar seus aplicativos com um gesto único ou apertando um botão. Escolha algumas ações no próximo slide. + Você pode iniciar seus aplicativos com gestos de toque ou apertando um botão. Configurar Selecionamos alguns aplicativos padrão para você. Se quiser, você pode alterá-los agora: Você pode alterar suas escolhas mais tarde. Vamos lá! - Tá todo pronto para começar! Espero que isso seja de grande valor para você! - Finn (que criou o Launcher) \te Josia (que fez algumas melhorias e tb mantém o fork do μLauncher) + Tá todo pronto para começar!\n\nEspero que isso seja de grande valor para você!\n\n- Finn (que criou o Launcher) e Josia (que fez algumas melhorias e tb mantém o fork do μLauncher) Começar Configurações Mais opções @@ -147,11 +147,11 @@ Erro: Falha ao bloquear a tela. (Se você acabou de atualizar o app, tente desativar e reativar o Serviço de acessibilidade em configurações do aparelho) O Serviço de acessibilidade do μLauncher não está ativado. Entre em configurações para ativar Não mostrar apps com um gesto atribuído na lista de aplicativos - O µLauncher precisa tornar-se o Administrador do dispositivo para poder bloquear a tela. + O μLauncher precisa virar um Administrador do dispositivo para poder bloquear a tela. Isto é necessário para realizar a ação de bloqueio da tela. Permitir a ação de bloqueio da tela Erro: Não é possível acessar a lanterna. - µLauncher - bloqueio da tela + μLauncher - bloqueio da tela Usar o Serviço de acessibilidade Usar o Administrador do dispositivo Escolha um método de bloqueio @@ -160,7 +160,7 @@ Essa funcionalidade requer o Android 6 ou mais recente. Nenhuma câmera com lanterna detectada. Erro: O bloqueio da tela via Serviço de acessibilidade não é compatível com este aparelho. Tente usar Administrador do dispositivo como método alternativo. - Definindo µLauncher como Serviço de acessibilidade permite a ele bloquear a tela. Considere que é necessário conceder as permissões elevadas. Você nunca deveria autorizar essas permissões a qualquer aplicativo sem avaliação. O µLauncher usará o Serviço de acessibilidade somente para bloquear a tela. Você pode verificar o código-fonte para ter certeza. O bloqueio da tela também pode ser realizado dando ao µLauncher permissões de Administrador do dispositivo. Apesar de que esse método não funciona com impressão digital e desbloqueio facial. + Definindo o μLauncher como Serviço de acessibilidade permite bloquear a tela. Considere que é necessário conceder as permissões elevadas. Você nunca deveria autorizar essas permissões a qualquer aplicativo sem avaliação. O μLauncher usará o Serviço de acessibilidade somente para bloquear a tela. Você pode verificar o código-fonte para ter certeza. O bloqueio da tela também pode ser realizado dando ao μLauncher permissões de Administrador do dispositivo. Apesar de que esse método não funciona com impressão digital e desbloqueio facial. Escolha um método de bloqueio Há dois métodos para bloquear a tela. @@ -174,9 +174,9 @@

Serviço de acessibilidade

Exige permissões elevadas. - O µLauncher usará essas permissões apenas para bloquear a tela. + O μLauncher usará essas permissões apenas para bloquear a tela.
- (Você realmente não deveria confiar num app aleatório que você baixou que tá pedindo estas permissões, mas pode verificar o código-fonte.) + (Você realmente não deveria confiar num app aleatório que você baixou e tá pedindo estas permissões, mas pode verificar o código-fonte.)
Em alguns aparelhos após ativação do Serviço de acessibilidade não será mais exigido o PIN para acessar dados criptografados, na inicialização do celular. Isto pode ser reativado depois. @@ -210,7 +210,7 @@ Deslize na borda da tela Largura da borda Ver código-fonte - Entre no chat do µLauncher + Entre no chat do μLauncher Bloquear a tela Sombra no texto Transparente @@ -236,13 +236,13 @@ Verde Cor Escola a cor - Autorizo a utilização do Serviço de acessibilidade para disponibilizar funcionalidades não relacionadas com a acessibilidade. - Não autorizo ao µLauncher a coleta de quaisquer dados. + Autorizo o μLauncher a usar Serviço de acessibilidade para acessar funcionalidades não relacionadas com a acessibilidade. + Não autorizo ao μLauncher coletar quaisquer dados. Ativação do Serviço de acessibilidade Ativar o Serviço de acessibilidade Cancelar - permissões elevadas ao µLauncher.
µLauncher usará estas permissões apenas para bloquear a tela. µLauncher nunca coletará nenhum dado. Sobretudo, o µLauncher não implementa o Serviço de acessibilidade para coletar dados.]]>
- Estou ciente de que isto concederá permissões elevadas ao µLauncher. + permissões elevadas ao μLauncher.
μLauncher usará estas permissões apenas para bloquear a tela. μLauncher nunca coletará nenhum dado. Sobretudo, o μLauncher não implementa o Serviço de acessibilidade para coletar dados.]]>
+ Estou ciente de que isto concederá permissões elevadas ao μLauncher. Estou ciente de que existem outras opções (permissões de Administrador do aparelho ou o botão de ligar). Pesquise na internet Ao buscar na lista de apps toque no Enter para iniciar uma pesquisa na internet. @@ -255,13 +255,13 @@ Espaço privado trancado Espaço privado liberado Espaço privado indisponível - O µLauncher precisa ser definido como a tela inicial padrão para poder usar Espaço privado. + O μLauncher precisa ser definido como a tela inicial padrão para poder usar Espaço privado. Copiar para memória Não relate vulnerabilidades de segurança publicamente no GitHub, use o seguinte: Relatar vulnerabilidade de segurança Criar relatório Relatar um bug - Obrigado por ajudar a melhorar o µLauncher!\nConsidere adicionar as seguintes informações ao relatório de bug: + Obrigado por ajudar a melhorar o μLauncher!\nConsidere adicionar as seguintes informações ao relatório dos bugs: Toque no espaço para temporariamente desativar esta funcionalidade. Não foi possível abrir a URL: nenhum navegador encontrado. Nenhum app encontrado para efetuar a pesquisa. @@ -300,5 +300,14 @@ Música: Reproduzir / Pausar Canto inferior direito -> centro esquerdo -> canto superior direito Inferior direito -> superior médio -> inferior esquerdo - Lista de apps inversa + Inverter a lista de apps + Doar + Ajuste de volume + Ocultar barra de status + Ocultar barra de navegação + Versão + Todos apps + Você pode encontrar rápido todos os apps na lista de aplicativos.\n\nDeslize para cima para abrir ou definir um gesto específico. + Quando apenas um aplicativo corresponde, vai ser iniciado automaticamente.\nIsso pode ser desativado acrescentando um espaço durante a busca. + Ações From 14ffbd1f6cefe6e8b74c8d027729c16ce43e4a53 Mon Sep 17 00:00:00 2001 From: class0068 Date: Mon, 24 Mar 2025 13:07:24 +0000 Subject: [PATCH 014/118] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (254 of 254 strings) Translation: jrpie-Launcher/Launcher Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/zh_Hans/ --- app/src/main/res/values-zh-rCN/strings.xml | 135 +++++++++++---------- 1 file changed, 68 insertions(+), 67 deletions(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 3eeaf50..4a7cd64 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -5,36 +5,36 @@ 更多选项 设置 外观 - 主题风格 + 色调风格 显示 其他 - - + 上滑 + 下滑 无法打开应用 要更改其设置吗? 打开设置,为该手势绑定一个应用程序 启动器 杂项 - 左滑两次 - 上滑两次 - 下滑两次 + 双指左滑 + 双指上滑 + 双指下滑 - 右滑两次 - 右(顶部) - 右(底部) - 左(底部) - 左(顶部) - 上(左边缘) - 上(右边缘) - 下(左边缘) - 下(右边缘) - 音量加 - 音量减 + 双指右滑 + 右滑(顶部) + 右滑(底部) + 左滑(底部) + 左滑(顶部) + 上滑(左边缘) + 上滑(右边缘) + 下滑(左边缘) + 下滑(右边缘) + 音量增加键 + 音量降低键 双击 长按 - 日期 - 时间 + 桌面日期 + 桌面时钟 选择应用 安装应用 没有找到应用市场 @@ -42,9 +42,9 @@ 选择壁纸 保持屏幕常亮 功能 - 边缘滑动动作 - 启动搜索匹配项 - 搜索时呼出键盘 + 边缘滑动手势 + 自动启动搜索匹配项 + 自动激活搜索 灵敏度 应用信息 查看 µLauncher 的使用教程 @@ -61,10 +61,10 @@ 概念 这是一款自由软件(遵循 MIT 许可)!\n欢迎查看项目仓库! 使用方法 - 您的主屏幕仅包含本地日期和时间,没有多余项目。 + 您的主屏幕仅包含本地日期和时间,没有多余的项目。 设置 - 我们为您选择了一些默认应用。如果您希望进行更改,现在就可以: - 您也可以稍后对您的选择进行更改。 + 我们为您预设了一些快捷操作。如果您不满意,现在就试试点击右侧图标: + 您也可以稍后更改您的选择。 开始! 应用 卸载 @@ -79,15 +79,15 @@ 啥也不干 教程 μLauncher 的设计理念是简约、高效,无干扰。\n\n不含广告、且不收集任何数据。 - 您可以通过手势或按键来启动最重要的应用程序。 - 将 μLauncher 设为默认桌面 - 您已经准备好开始使用本启动器了!\n\n希望这对你有帮助!\n\n- Finn(Launcher 的作者)和 Josia(对 μLauncher 进行了改进和维护) - 双滑动作 + 您可以通过手势或按键来启动对您来说最重要的应用程序。 + 将 μLauncher 设为默认启动器 + 您已经准备好开始使用本启动器了!\n\n希望本快捷教程能对您有所帮助!\n\n- Finn(Launcher 的作者)和 Josia(对 μLauncher 进行了改进和维护) + 双指滑动手势 使用本地日期格式 显示时间 显示日期 - 翻转日期和时间 - 背景(应用列表和设置) + 交换日期和时间位置 + 背景(应用程序列表和设置页面) 字体 黑白应用图标 显示秒 @@ -100,7 +100,7 @@ 错误:无法访问闪光灯。 选择锁屏方法 选择锁屏的方法 - 不要在应用抽屉中显示被绑定到手势的应用 + 不要在应用程序列表中显示已被绑定到手势操作的应用 此功能需要 Android 6 或更高版本。 应用程序已隐藏。您可在设置中让它再次显示。 µLauncher 需要激活“设备管理应用”权限才能够锁定屏幕。 @@ -114,34 +114,34 @@ 撤销 隐藏的应用 隐藏的应用 - 上滑 - 用双指向上滑动 - 下滑 + 向上滑动 + 双指向上滑动 + 向下滑动 双指向下滑动 - 左滑 + 向左滑动 双指向左滑动 - 右滑 + 向右滑动 双指向右滑动 - 在屏幕顶部向右滑动 - 在屏幕底部向右滑动 - 在屏幕底部向左滑动 - 在屏幕顶部向左滑动 - 在屏幕左边缘向上滑动 - 在屏幕右边缘向上滑动 - 在屏幕左边缘向下滑动 - 在屏幕右边缘向下滑动 - 按下音量增大按钮 - 按下音量降低按钮 + 在桌面顶部向右滑动 + 在桌面底部向右滑动 + 在桌面底部向左滑动 + 在桌面顶部向左滑动 + 在桌面左边缘向上滑动 + 在桌面右边缘向上滑动 + 在桌面左边缘向下滑动 + 在桌面右边缘向下滑动 + 按下音量增加键 + 按下音量降低键 双击空白区域 长按空白区域 - 点击日期 - 点击时间 + 点击桌面日期 + 点击桌面时钟 查看源代码 加入 μLauncher 的聊天群 收藏的应用 锁屏 文本阴影 - 双指滑动 + 使用双指进行滑动手势操作 重命名 %1$s 默认 暗色 @@ -165,9 +165,9 @@ 亮色 快速设置 错误:锁定屏幕失败。(如果您刚刚升级了本启动器,请尝试在手机设置中手动禁用并重新启用“无障碍”服务) - 在屏幕边缘滑动 + 在桌面边缘进行滑动手势操作 将 µLauncher 设置为“无障碍”服务以允许其锁定屏幕。请注意,这会使 µLauncher 获得额外的权限。你永远不应该轻易地授予任何应用程序这样的权限。获得授权后“无障碍”服务将仅被用于锁定屏幕。您可以审核我们的源代码。请注意,锁定屏幕也可以通过激活 µLauncher 的“设备管理应用”权限来实现,然而,这种方法无法与于指纹解锁和面部解锁兼容。 - 返回 + 返回操作 红色 蓝色 透明度 @@ -175,19 +175,19 @@ 动态 私人空间 私人空间 - 选择颜色 + 设置颜色 颜色 错误反馈 ]]> 锁定私人空间 V Λ - 文本 + 纯文本 网格 创建报告 解锁私人空间 默认 - 颜色 + 文本颜色 复制到剪贴板 此功能需要 Android 15 或更高版本。 @@ -210,9 +210,9 @@ 返回按键 / 返回手势 先单击然后再下滑 在网络上搜索 - (从)右上 (滑向)中左(滑向)右下 - 通过按回车键在应用列表搜索界面激活网络搜索。 - (从)左下 (滑向)中上(滑向)右下 + (从)右上(滑向)中左(滑向)右下 + 输入搜索内容后,按回车键直接在应用列表界面启动网络搜索。 + (从)左下(滑向)中上(滑向)右下 选择锁定设备的方式 有2种方式可以用来锁定屏幕。 @@ -238,7 +238,7 @@ ]]> 搜索(不触发自动启动匹配项) 广泛且重要的权限。
但 μLauncher 仅会在需要锁定屏幕时使用这些权限。µLauncher 绝不会收集任何数据。尤其是,μLauncher 不会使用“无障碍”服务来收集任何数据。]]>
- (从)左上 (滑向)中右(滑向)左下 + (从)左上(滑向)中右(滑向)左下 单击 + 上滑 单击 + 下滑 单击 + 左滑 @@ -246,16 +246,16 @@ 先单击然后再上滑 单击 + 右滑 先单击然后再右滑 - (从)左下 (滑向)中右(滑向)左上 - (从)右下 (滑向)中左(滑向)右上 - (从)左上 (滑向)中下(滑向)右上 - (从)右上 (滑向)中下(滑向)左上 - (从)右下 (滑向)中上(滑向)左下 + (从)左下(滑向)中右(滑向)左上 + (从)右下(滑向)中左(滑向)右上 + (从)左上(滑向)中下(滑向)右上 + (从)右上(滑向)中下(滑向)左上 + (从)右下(滑向)中上(滑向)左下 Λ (反向) V(反向) (反向)]]> - 开启后将直接启动匹配搜索内容的应用,可以通过按空格键临时暂停该功能。 + 开启后将直接启动搜索所匹配到的应用,可以通过在搜索内容前添加空格来临时停用该功能。 应用程序列表样式 绑定到手势 音乐:播放 / 暂停 @@ -268,10 +268,11 @@ 调整音量 版本 所有应用 - 您可以在应用程序列表中快速所搜所有应用。\n\n您可以通过上滑打开应用程序列表,也可以通过绑定其他手势操作来打开应用程序列表。 - 当匹配到唯一的应用程序后,该应用将自动启动。\n如果你不想触发自动启动,在查询内容前加上空格即可禁用。 + 您可以在应用程序列表中快速找到已安装的应用程序。\n\n您可以通过上滑打开应用程序列表,也可以通过绑定其他手势操作来打开应用程序列表。 + 您还可以搜索,当匹配到唯一的应用程序后,该应用将自动启动。\n如果你不想触发自动启动,可以在搜索内容前加上空格以禁用。 隐藏状态栏 隐藏导航栏 倒序排列应用程序 我同意 μLauncher 使用无障碍服务来提供与无障碍服务无关的其他功能。 + 快捷操作 From 940e5785dc4d716c633bd603f115b6c60335411d Mon Sep 17 00:00:00 2001 From: Vossa Excelencia Date: Mon, 24 Mar 2025 20:00:26 +0000 Subject: [PATCH 015/118] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (254 of 254 strings) Translation: jrpie-Launcher/Launcher Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/pt_BR/ --- app/src/main/res/values-pt-rBR/strings.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 4b2bcd2..5ee6a46 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -38,7 +38,7 @@ Para baixo (borda esquerda) Para baixo (borda direita) Botão de aumentar volume - Botão de diminuição de volume + Botão de diminuir volume Toque duplo Toque longo Data @@ -98,8 +98,8 @@ Busque Configurações do μLauncher Todos os apps - Aumento de volume - Diminuição de volume + Aumentar volume + Diminuir volume Música: Próximo Música: Anterior Não faça nada @@ -302,7 +302,7 @@ Inferior direito -> superior médio -> inferior esquerdo Inverter a lista de apps Doar - Ajuste de volume + Ajustar volume Ocultar barra de status Ocultar barra de navegação Versão From cbd23159da4a64ba5eaf7327fcdce97ba636478f Mon Sep 17 00:00:00 2001 From: class0068 Date: Tue, 25 Mar 2025 12:40:13 +0000 Subject: [PATCH 016/118] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 98.8% (254 of 257 strings) Translation: jrpie-Launcher/Launcher Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/zh_Hans/ --- app/src/main/res/values-zh-rCN/strings.xml | 29 ++++++++++++---------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 4a7cd64..a0d7381 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -61,7 +61,7 @@ 概念 这是一款自由软件(遵循 MIT 许可)!\n欢迎查看项目仓库! 使用方法 - 您的主屏幕仅包含本地日期和时间,没有多余的项目。 + 您的桌面仅包含本地日期和时间,没有多余的项目。 设置 我们为您预设了一些快捷操作。如果您不满意,现在就试试点击右侧图标: 您也可以稍后更改您的选择。 @@ -76,7 +76,7 @@ 降低音量 音乐:上一首 音乐:下一首 - 啥也不干 + 不做任何设置 教程 μLauncher 的设计理念是简约、高效,无干扰。\n\n不含广告、且不收集任何数据。 您可以通过手势或按键来启动对您来说最重要的应用程序。 @@ -115,13 +115,13 @@ 隐藏的应用 隐藏的应用 向上滑动 - 双指向上滑动 + 使用双指向上滑动 向下滑动 - 双指向下滑动 + 使用双指向下滑动 向左滑动 - 双指向左滑动 + 使用双指向左滑动 向右滑动 - 双指向右滑动 + 使用双指向右滑动 在桌面顶部向右滑动 在桌面底部向右滑动 在桌面底部向左滑动 @@ -163,8 +163,8 @@ 激活“设备管理应用”权限 取消 亮色 - 快速设置 - 错误:锁定屏幕失败。(如果您刚刚升级了本启动器,请尝试在手机设置中手动禁用并重新启用“无障碍”服务) + 启动器设置 + 错误:锁定屏幕失败。(如果您刚刚升级了本启动器,请尝试在手机设置中手动禁用再重新启用“无障碍”服务。) 在桌面边缘进行滑动手势操作 将 µLauncher 设置为“无障碍”服务以允许其锁定屏幕。请注意,这会使 µLauncher 获得额外的权限。你永远不应该轻易地授予任何应用程序这样的权限。获得授权后“无障碍”服务将仅被用于锁定屏幕。您可以审核我们的源代码。请注意,锁定屏幕也可以通过激活 µLauncher 的“设备管理应用”权限来实现,然而,这种方法无法与于指纹解锁和面部解锁兼容。 返回操作 @@ -201,14 +201,14 @@ 私人空间已锁定 私人空间已解锁 私人空间不可用 - µLauncher 需要作为默认主屏幕才能访问私人空间。 + µLauncher 需要作为默认启动器才能访问私人空间。 没有找到处理搜索的应用。 无法打开 URL:找不到浏览器。 我已知晓,这将赋予 μLauncher 广泛且重要的权限。 在应用程序列表中隐藏私人空间 隐藏已被暂停的应用 返回按键 / 返回手势 - 先单击然后再下滑 + 先单击然后再向下滑动 在网络上搜索 (从)右上(滑向)中左(滑向)右下 输入搜索内容后,按回车键直接在应用列表界面启动网络搜索。 @@ -242,10 +242,10 @@ 单击 + 上滑 单击 + 下滑 单击 + 左滑 - 先单击然后再左滑 - 先单击然后再上滑 + 先单击然后再向左滑动 + 先单击然后再向上滑动 单击 + 右滑 - 先单击然后再右滑 + 先单击然后再向右滑动 (从)左下(滑向)中右(滑向)左上 (从)右下(滑向)中左(滑向)右上 (从)左上(滑向)中下(滑向)右上 @@ -275,4 +275,7 @@ 倒序排列应用程序 我同意 μLauncher 使用无障碍服务来提供与无障碍服务无关的其他功能。 快捷操作 + 最近使用的应用 + 错误:启用“无障碍”服务失败。 + 错误:无法显示最近使用的应用。(如果您刚刚升级了本启动器,请尝试在手机设置中手动禁用再重新启用“无障碍”服务。) From c085087e1ef2b7b27d2e2b0a3e588c7e66cb1a56 Mon Sep 17 00:00:00 2001 From: class0068 Date: Tue, 25 Mar 2025 20:55:21 +0000 Subject: [PATCH 017/118] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (257 of 257 strings) Translation: jrpie-Launcher/Launcher Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/zh_Hans/ --- app/src/main/res/values-zh-rCN/strings.xml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index a0d7381..de7c02b 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -105,7 +105,7 @@ 应用程序已隐藏。您可在设置中让它再次显示。 µLauncher 需要激活“设备管理应用”权限才能够锁定屏幕。 这是执行锁屏操作所必需的。 - µLauncher - 锁屏 + µLauncher 收藏的应用 添加到收藏夹 从收藏夹中移除 @@ -166,7 +166,7 @@ 启动器设置 错误:锁定屏幕失败。(如果您刚刚升级了本启动器,请尝试在手机设置中手动禁用再重新启用“无障碍”服务。) 在桌面边缘进行滑动手势操作 - 将 µLauncher 设置为“无障碍”服务以允许其锁定屏幕。请注意,这会使 µLauncher 获得额外的权限。你永远不应该轻易地授予任何应用程序这样的权限。获得授权后“无障碍”服务将仅被用于锁定屏幕。您可以审核我们的源代码。请注意,锁定屏幕也可以通过激活 µLauncher 的“设备管理应用”权限来实现,然而,这种方法无法与于指纹解锁和面部解锁兼容。 + 将 µLauncher 设置为“无障碍”服务以允许其锁定屏幕和展示最近应用屏幕。请注意,这会使 µLauncher 获得额外的权限。你永远不应该轻易地授予任何应用程序这样的权限。μLauncher 仅在被用户要求时才会使用“无障碍”服务权限以实现: * 锁定屏幕 * 展示最近应用屏幕。μLauncher 不会使用“无障碍”服务来收集任何数据。您可以审核我们的源代码。请注意,锁定屏幕也可以通过激活 µLauncher 的“设备管理应用”权限来实现,然而,这种方法无法与于指纹解锁和面部解锁兼容。 返回操作 红色 蓝色 @@ -237,7 +237,12 @@ 你可以在设置中随时更改这个选项。 ]]>
搜索(不触发自动启动匹配项) - 广泛且重要的权限。
但 μLauncher 仅会在需要锁定屏幕时使用这些权限。µLauncher 绝不会收集任何数据。尤其是,μLauncher 不会使用“无障碍”服务来收集任何数据。]]>
+ 广泛且重要的权限。
但 μLauncher 将这些权限用于: +
    +
  • 锁定屏幕
  • +
  • 展示最近应用屏幕
  • +
+µLauncher 绝不会收集任何数据。尤其是,μLauncher 不会使用“无障碍”服务来收集任何数据。]]>
(从)左上(滑向)中右(滑向)左下 单击 + 上滑 单击 + 下滑 @@ -275,7 +280,7 @@ 倒序排列应用程序 我同意 μLauncher 使用无障碍服务来提供与无障碍服务无关的其他功能。 快捷操作 - 最近使用的应用 + 最近应用屏幕 错误:启用“无障碍”服务失败。 - 错误:无法显示最近使用的应用。(如果您刚刚升级了本启动器,请尝试在手机设置中手动禁用再重新启用“无障碍”服务。) + 错误:无法展示最近应用屏幕。(如果您刚刚升级了本启动器,请尝试在手机设置中手动禁用再重新启用“无障碍”服务。) From ce65741717e314057d59b21d43b5dabf83b6a5e4 Mon Sep 17 00:00:00 2001 From: Vossa Excelencia Date: Sun, 30 Mar 2025 16:53:37 +0000 Subject: [PATCH 018/118] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (258 of 258 strings) Translation: jrpie-Launcher/Launcher Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/pt_BR/ --- app/src/main/res/values-pt-rBR/strings.xml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 5ee6a46..fcdd9e8 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -151,7 +151,7 @@ Isto é necessário para realizar a ação de bloqueio da tela. Permitir a ação de bloqueio da tela Erro: Não é possível acessar a lanterna. - μLauncher - bloqueio da tela + μLauncher Usar o Serviço de acessibilidade Usar o Administrador do dispositivo Escolha um método de bloqueio @@ -160,7 +160,7 @@ Essa funcionalidade requer o Android 6 ou mais recente. Nenhuma câmera com lanterna detectada. Erro: O bloqueio da tela via Serviço de acessibilidade não é compatível com este aparelho. Tente usar Administrador do dispositivo como método alternativo. - Definindo o μLauncher como Serviço de acessibilidade permite bloquear a tela. Considere que é necessário conceder as permissões elevadas. Você nunca deveria autorizar essas permissões a qualquer aplicativo sem avaliação. O μLauncher usará o Serviço de acessibilidade somente para bloquear a tela. Você pode verificar o código-fonte para ter certeza. O bloqueio da tela também pode ser realizado dando ao μLauncher permissões de Administrador do dispositivo. Apesar de que esse método não funciona com impressão digital e desbloqueio facial. + Definindo o μLauncher como Serviço de acessibilidade permite bloquear a tela e abrir o menu de apps recentes. Considere que é necessário conceder as permissões elevadas. Você nunca deveria autorizar essas permissões a qualquer aplicativo sem avaliação. O μLauncher usará o Serviço de acessibilidade somente para executar as seguintes ações quando solicitado pelo usuário: * bloquear a tela * abrir aplicativos recentes μLauncher nunca usará o Serviço de acessibilidade para coletar os dados. Você pode verificar o código-fonte para ter certeza. O bloqueio da tela também pode ser realizado dando ao μLauncher permissões de Administrador do dispositivo. Apesar de que esse método não funciona com impressão digital e desbloqueio facial. Escolha um método de bloqueio Há dois métodos para bloquear a tela. @@ -241,7 +241,12 @@ Ativação do Serviço de acessibilidade Ativar o Serviço de acessibilidade Cancelar - permissões elevadas ao μLauncher.
μLauncher usará estas permissões apenas para bloquear a tela. μLauncher nunca coletará nenhum dado. Sobretudo, o μLauncher não implementa o Serviço de acessibilidade para coletar dados.]]>
+ permissões elevadas ao μLauncher.
μLauncher usará estas permissões apenas para executar as seguintes ações: +
    +
  • Bloquear a tela
  • +
  • Apps recentes
  • +
+ μLaunchernunca coletará nenhum dado. Sobretudo, o μLauncher não implementa o Serviço de acessibilidade para coletar os dados.]]>
Estou ciente de que isto concederá permissões elevadas ao μLauncher. Estou ciente de que existem outras opções (permissões de Administrador do aparelho ou o botão de ligar). Pesquise na internet @@ -310,4 +315,8 @@ Você pode encontrar rápido todos os apps na lista de aplicativos.\n\nDeslize para cima para abrir ou definir um gesto específico. Quando apenas um aplicativo corresponde, vai ser iniciado automaticamente.\nIsso pode ser desativado acrescentando um espaço durante a busca. Ações + Apps recentes + Erro: Falha ao ativar o Serviço de acessibilidade. + Usar outro iniciador + Erro: Falha ao mostrar apps recentes. (Se você acabou de atualizar o app, tente desativar e reativar o Serviço de acessibilidade em configurações do Android) From 03a9833b5154335b31bd5a753e4ecb1517c3f792 Mon Sep 17 00:00:00 2001 From: class0068 Date: Wed, 2 Apr 2025 19:01:34 +0000 Subject: [PATCH 019/118] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (258 of 258 strings) Translation: jrpie-Launcher/Launcher Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/zh_Hans/ --- app/src/main/res/values-zh-rCN/strings.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index de7c02b..608e57b 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -196,7 +196,7 @@ 正在激活“无障碍”服务 开源许可证 开源许可证 - 在应用列表中显示 + 在应用程序列表中显示 添加快捷方式 私人空间已锁定 私人空间已解锁 @@ -211,7 +211,7 @@ 先单击然后再向下滑动 在网络上搜索 (从)右上(滑向)中左(滑向)右下 - 输入搜索内容后,按回车键直接在应用列表界面启动网络搜索。 + 输入搜索内容后,按回车键直接在应用程序列表界面启动网络搜索。 (从)左下(滑向)中上(滑向)右下 选择锁定设备的方式 @@ -283,4 +283,5 @@ 最近应用屏幕 错误:启用“无障碍”服务失败。 错误:无法展示最近应用屏幕。(如果您刚刚升级了本启动器,请尝试在手机设置中手动禁用再重新启用“无障碍”服务。) + 启动其他启动器 From 0877ca677260a52377c21f5ca26b323746b52887 Mon Sep 17 00:00:00 2001 From: class0068 Date: Wed, 2 Apr 2025 18:56:56 +0000 Subject: [PATCH 020/118] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 14.2% (3 of 21 strings) Translation: jrpie-Launcher/metadata Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/metadata/zh_Hans/ --- .../android/zh-CN/full_description.txt | 32 ++++++++++--------- .../android/zh-CN/short_description.txt | 2 +- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/fastlane/metadata/android/zh-CN/full_description.txt b/fastlane/metadata/android/zh-CN/full_description.txt index b4defca..89025a5 100644 --- a/fastlane/metadata/android/zh-CN/full_description.txt +++ b/fastlane/metadata/android/zh-CN/full_description.txt @@ -1,19 +1,21 @@ -µLauncher 是主屏幕启动程序,允许您使用滑动手势和按下按钮来启动其他应用。 -它是最小、高效且无干扰。 +µLauncher 是桌面启动器程序,允许您使用各种滑动手势和按下按钮来启动其他应用。 +它是简约、高效且无干扰的。 -您的主屏幕仅显示日期、时间和壁纸。 -按返回或向上滑动(可以配置)打开 -所有已安装应用的列表,可以高效地搜索。 +您的桌面仅显示日期、时间和壁纸。 +按返回按键或向上滑动(可自定义其他手势)即可打开 +应用程序列表,且支持高效地搜索。 -这是 Finn M Glas 的应用 Launcher 的一个 fork。 +本启动器是基于 Finn M Glas 开发的 Launcher 启动器 的一个派生应用程序。 -显著变化: -* 边缘手势:可分为屏幕边缘滑动和中心滑动的设置。 -* 与工作配置文件兼容,因此可以使用 Shelter 等应用。 -* 此应用使用系统壁纸而不是自定义解决方案。 -* 字体已更改为 Hack。 -* Material 图标所取代了 Font Awesome 图标。 -* 移除了主屏幕上的齿轮按钮。按返回按钮会打开应用列表,可以从那里访问应用设置。 -* 搜索算法已修改为优先匹配应用名称开头的内容,即当搜索“te”时,“termux”会排在“notes”之前。 -* 搜索栏已移动到屏幕底部 +功能: +* 您可以设定 35 个不同的手势操作。如: + - 启动一个应用程序 + - 打开应用程序列表 + - 打开收藏的应用程序列表 + - 调整音量 + - 快速切换 上一首/下一首 音乐 + - 锁定屏幕 + - 开启/关闭 手机闪光灯 + - 展开通知栏 / 快捷设定栏 +* 兼容工作空间配置,因此支持使用 Shelter 等应用。 diff --git a/fastlane/metadata/android/zh-CN/short_description.txt b/fastlane/metadata/android/zh-CN/short_description.txt index d49c27c..66d4684 100644 --- a/fastlane/metadata/android/zh-CN/short_description.txt +++ b/fastlane/metadata/android/zh-CN/short_description.txt @@ -1 +1 @@ -无干扰的最小主屏幕应用启动器。 +无干扰的简约风格启动器。 From 7cce425339a84449dc9c2c121867cb730a2f3781 Mon Sep 17 00:00:00 2001 From: Lukas Hamm Date: Fri, 11 Apr 2025 20:54:28 +0000 Subject: [PATCH 021/118] Added translation using Weblate (Lithuanian) --- app/src/main/res/values-lt/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 app/src/main/res/values-lt/strings.xml diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml new file mode 100644 index 0000000..a6b3dae --- /dev/null +++ b/app/src/main/res/values-lt/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From 24250ad345a95f440bac1e48e3bd032cce0d59ac Mon Sep 17 00:00:00 2001 From: Lukas Hamm Date: Fri, 11 Apr 2025 20:57:34 +0000 Subject: [PATCH 022/118] Translated using Weblate (Lithuanian) Currently translated at 5.4% (14 of 258 strings) Translation: jrpie-Launcher/Launcher Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/lt/ --- app/src/main/res/values-lt/strings.xml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index a6b3dae..0a45d57 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -1,2 +1,16 @@ - \ No newline at end of file + + Norite pakeisti nustatymus? + Nustatymai + Veiksmai + Paleidimo programėlė + Apie + Atgal + Grįžimo mygtukas / grįžimo gestas + Perbraukimas aukštyn + Bakstelėkite + aukštyn + Nepavyksta paleisti programėlės + Atidarykite nustatymus norėdami pasirinkti šio gesto veiksmą + Aukštyn + Bakstelėjimas ir perbraukimas aukštyn + From 20a01e9f0329b222e1637e59ef0c1cd4d7d51caf Mon Sep 17 00:00:00 2001 From: letterhaven Date: Sat, 12 Apr 2025 14:03:03 +0000 Subject: [PATCH 023/118] Added translation using Weblate (Arabic) --- app/src/main/res/values-ar/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 app/src/main/res/values-ar/strings.xml diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml new file mode 100644 index 0000000..a6b3dae --- /dev/null +++ b/app/src/main/res/values-ar/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From a0b2417363850772db574f82654fbadb5e228b07 Mon Sep 17 00:00:00 2001 From: letterhaven Date: Sat, 12 Apr 2025 14:04:14 +0000 Subject: [PATCH 024/118] Translated using Weblate (Arabic) Currently translated at 99.6% (257 of 258 strings) Translation: jrpie-Launcher/Launcher Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/ar/ --- app/src/main/res/values-ar/strings.xml | 264 ++++++++++++++++++++++++- 1 file changed, 263 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index a6b3dae..1b7d9a2 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -1,2 +1,264 @@ - \ No newline at end of file + + لا يمكن فتح التطبيق + هل تريد تعديل اعداداته؟ + افتح الاعدادات لاختيار أمر لهذه الإيماءة + الاعدادات + الأوامر + المشغل + بيانات + رجوع + أعلى + لأعلى بأصبعين + اسحب لأعلى بأصبعين + أنقر ثم اسحب لأعلى + اضغط على زر الرجوع + اسحب لأعلى + نقرة + أعلى + أسفل + اسحب لأسفل + نقرة + لأسفل + لأسفل بأصبعين + يسار + اسحب من اليسار + نقرة + يسار + أنقر ثم اسحب إلى اليسار + يسار بأصبعين + يمين + اسحب إلى اليمين + نقرة + يمين + أنقر ثم اسحب إلى اليمين + يمين بأصبعين + اسحب إلى اليمين بأصبعين + يمين (في الأعلى) + يمين (في الأسفل) + اسحب إلى اليمين من أسفل الشاشة + يسار (في الأعلى) + اسحب إلى اليسار من أعلى الشاشة + أعلى (الحافة اليسرى) + اسحب إلى الأعلى من حافة الشاشة اليسرى + أعلى (الحافة اليمنى) + أسفل (الحافة اليسرى) + اسحب إلى الأسفل من حافة الشاشة اليسرى + أسفل (الحافة اليمنى) + أعلى اليسار -> وسط اليمين -> أسفل اليسار + أسفل اليسار -> وسط اليمين -> أعلى اليسار + أعلى اليمين -> وسط اليسار -> أسفل اليمين + أعلى اليسار -> الأسفل الوسط -> أعلى اليمين + V (معكوس) + أعلى اليمين -> الأسفل الوسط -> أعلى اليسار + أسفل اليسار -> أعلى الوسط -> أسفل اليمين + Λ (معكوس) + زر رفع الصوت + اضغط على زر رفع الصوت + زر خفض الصوت + اضغط على زر خفض الصوت + نقرة مزدوجة + نقرة مطولة + أنقر مطولًا في مكان فارغ على الشاشة + التاريخ + أنقر على التاريخ + الوقت + أنقر على الوقت + اختر تطبيق + ثبت تطبيقات + المظهر + السمة + ]]> + (معكوس)]]> + + + V + Λ + داكن + فاتح + متغير + ظل النص + شفاف + مظلل + مُموه + لون ثابت + الخط + خط النظام + بدون تذييل + مذيل + أحادي المسافة + أيقونات تطبيقات أحادية اللون + اللون + أظهر الوقت + أظهر التاريخ + استخدم تنسيق التاريخ المحلي + إقلب مكان التاريخ مع الوقت + اختر خلفية + العرض + حافظ على بقاء الشاشة قيد التشغيل + إخفِ شريط الحالة + تدوير الشاشة + الوظائف + أوامر السحب بأصبعين + اسحب باستخدام أصبعين + أوامر السحب من الحواف + عرض الحواف + إظهار نتائج البحث + اضغط مسافة لتعطيل هذه الميزة مؤقتًا + ابحث في الويب + أظهر لوحة المفاتيح عند البحث + الحساسية + التطبيقات + التطبيقات المخفية + + إخفِ المساحة الخاصة من قائمة التطبيقات + تخطيط قائمة التطبيقات + الافتراضي + نص + شبكة + تعيين μauncher كشاشة المنزل + معلومات التطبيق + إعادة تعيين الإعدادات + أنت على وشك تجاهل كل تفضيلاتك، هل تريد الإكمال؟ + عرض شيفرة المصدر + الإبلاغ عن خطأ + النسخ إلى الحافظة + يرجى عدم الإبلاغ عن الثغرات الأمنية علنًا على Github ، استخدم ما يلي بدلاً من ذلك: + الإبلاغ عن ثغرة أمنية + إنشاء تقرير + اتصل بمطور النسخة + انضم إلى دردشة μauncher + تبرع + سياسة الخصوصية + كل التطبيقات + التطبيقات المفضلة + التطبيقات المخفية + المساحة الخاصة + اختر تطبيق + التطبيقات + أخرى + إلغاء التثبيت + إزالة من المفضلة + إخفِ + أظهر + إعادة التسمية + بحث + بحث (بدون تشغيل تلقائي) + اعدادات التطبيق + درج التطبيقات + المساحة الخاصة + تبديل قفل المساحة الخاصة + ارفع الصوت + إنضم إلى مجموعة discord! + ضبط مستوى الصوت + الموسيقى: السابق + الموسيقى: تشغيل / ايقاف مؤقت + التطبيقات الحديثة + لا تفعل شيئًا + قفل الشاشة + تبديل الفلاش + شغل شاشة منزل أخرى + أضف اختصارًا + اربط بإيماءة + درس تعليمي + 👋\n\nخذ بضع ثوان لمعرفة كيفية استخدام هذا المشغل! + المفهوم + إنه برنامج مجاني (ترخيص MIT)!\nتأكد من مراجعة المستودع! + النسخة + الاستخدام + تحتوي شاشتك الرئيسية على التاريخ والوقت المحليين. لا الهاء. + كل التطبيقات + بمجرد تطابق تطبيق واحد فقط، يتم تشغيله تلقائيًا.\nيمكن تعطيل ذلك عن طريق اضافة مساحة في بداية استعلام. + الإعداد + اخترنا بعض التطبيقات الافتراضية لك. يمكنك تغييرها الآن إذا كنت تريد: + يمكنك أيضًا تغيير اختيارك لاحقًا. + لنبدأ! + ابدأ + الإعدادات + المزيد من الخيارات + هذه الوظيفة تتطلب أندرويد 6 أو أحدث. + هذه الوظيفة تتطلب أندرويد 15 أو أحدث. + تراجع + تم إخفاء التطبيق. يمكنك جعله مرئيًا مرة أخرى في الإعدادات. + الاعدادات السريعة + يجب أن يكون μlauncher مسؤولًا من أجل قفل الشاشة. + هذا مطلوب لإجراء شاشة القفل. + تمكين إجراء شاشة القفل + لم يتم اكتشاف كاميرا مع فلاش. + خطأ: لا يمكن الوصول إلى الفلاش. + خطأ: فشل في إظهار التطبيقات الحديثة. (إذا قمت للتو بترقية التطبيق ، فحاول تعطيل خدمة الوصول وإعادة تمكينها في إعدادات الهاتف) + خطأ: فشل في تمكين خدمة الوصول. + لم يتم تمكين خدمة الوصول إلى μlauncher. يرجى تمكينه في الإعدادات + المساحة الخاصة مقفلة + المساحة الخاصة مفتوحة + المساحة الخاصة غير متوافرة + قفل المساحة الخاصة + فتح المساحة الخاصة + μLauncher + اختر طريقة القفل + استخدام خدمة إمكانية الوصول + استخدم مسؤول الجهاز + اختر طريقة لقفل الشاشة + اعادة تسمية %1$s + أحمر + شفافية + أزرق + أخضر + اللون + اختر لونًا + أدرك أن هذا سيمنح أذونات كثيرة لـμauncher. + أدرك وجود خيارات أخرى (باستخدام أذونات مسؤول الجهاز أو زر الطاقة). + أوافق لـμlauncher باستخدام خدمة إمكانية الوصول لتوفير الوظائف ليس لها صلة بإمكانية الوصول. + أوافق على أن μlauncher لا يجمع أي بيانات. + يتم تفعيل خدمة إمكانية الوصول + تفعيل خدمة إمكانية الوصول + إلغاء + تراخيص المصادر المفتوحة + تراخيص المصادر المفتوحة + لم يتم العثور على تطبيق للتعامل مع البحث. + لا يمكن فتح عنوان URL: لم يتم العثور على متصفح. + أذونات كثيرة إلى التطبيق: +
    +
  • قفل الشاشة
  • +
  • التطبيقات الحديثة
  • +
+ لن يقومμlauncher أبدًا بجمع أي بيانات . على وجه الخصوص ، لا يستخدم μlauncher خدمة إمكانية الوصول لجمع أي بيانات.]]>
+ أنقر ثم اسحب لأسفل + الافتراضي + أظهر في قائمة التطبيقات + اسحب لأسفل بأصبعين + أظهر الثواني + الإبلاغ عن خطأ + اسحب إلى اليسار بأصبعين + أنقر مرتين في مكان فارغ على الشاشة + اضغط على زر الرجوع أثناء البحث في قائمة التطبيقات لإظهار البحث على الويب + اسحب إلى اليمين من أعلى الشاشة + يسار (في الأسفل) + اسحب إلى الأعلى من حافة الشاشة اليمنى + أسفل اليمين -> أعلى الوسط -> أسفل اليسار + لا تظهر التطبيقات المرتبطة بإيماءة في قائمة التطبيقات + درج التطبيقات المفضلة + الموسيقى: التالي + تم تصميم μlauncher ليكون فعال، وخالي من الهاء.\n\nلا يحتوي على أي إعلانات ولا يجمع أي بيانات. + أنت مستعد للبدء!\n\nآمل أن يكون هذا ذا قيمة كبيرة بالنسبة لك!\n\n- المطورين + يجب أن يكون μlauncher الشاشة الرئيسية الافتراضية للوصول إلى مساحة خاصة. + اسحب إلى اليسار من أسفل الشاشة + أسفل اليمين -> وسط اليسار -> أعلى اليمين + لم يتم العثور على تطبيق المتجر + إخفِ شريط التنقل + معلومات التطبيق + أضف إلى المفضلة + توسيع لوحة الاشعارات + يمكنك البحث بسرعة من خلال جميع التطبيقات في قائمة التطبيقات.\n\nاسحب لأعلى لفتح القائمة، أو ربطها بإيماءة ما. + خطأ: فشل في قفل الشاشة. (إذا قمت للتو بترقية التطبيق ، فحاول تعطيل خدمة الوصول وإعادة تمكينها في إعدادات الهاتف) + اسحب إلى الأسفل من حافة الشاشة اليمنى + أحادي المسافة مذيل + عكس قائمة التطبيقات + خطأ: لا يمكن توسيع شريط الحالة. يستخدم هذا الإجراء وظيفة ليست جزءًا من نظام أندرويد. لسوء الحظ ، لا يبدو أنه يعمل على جهازك. + خلفية القوائم + إخفِ التطبيقات المتوقفة + أظهر العرض التعليمي للتطبيق + شكرا لك على المساعدة في تحسين μLauncher!\nيرجى إضافة المعلومات التالية إلى تقرير الأخطاء الخاص بك: + اتصل بالمطور الأصلي + اخفض الصوت + يمكنك تشغيل أهم تطبيقاتك بوسطة إيماءات اللمس أو الضغط على الأزرار. + اسحب من حواف الشاشة + خطأ: قفل الشاشة باستخدام إمكانية الوصول غير مدعوم على هذا الجهاز. الرجاء استخدام طريقة مسؤول الجهاز بدلاً من ذلك. + يتيح تعيين μlauncher كخدمة إمكانية الوصول قفل الشاشة وفتح قائمة التطبيقات الحديثة. يرجى ملاحظة أنه يتطلب كمية كبيرة من الأذونات. يجب ألا تمنح مثل هذه الأذونات باستخفاف لأي تطبيق. سوف يستخدم μlauncher خدمة إمكانية الوصول فقط لأداء الإجراءات التالية عند طلب المستخدم: * قفل شاشة * فتح التطبيقات الحديثة μlauncher لن يستخدم أبدًا خدمة إمكانية الوصول لجمع البيانات. يمكنك التحقق من شيفرة المصدر للتأكد. يرجى ملاحظة أنه يمكنك قفل الشاشة من خلال منح أذونات مسؤول الجهاز، لكنها لا تعمل مع بصمات الأصابع وفتح الوجه. +
From 2774b74d9d47539dcb08fb29bdce6b1ffd409965 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Tue, 15 Apr 2025 18:38:00 +0200 Subject: [PATCH 025/118] 0.1.4 --- app/build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/44.txt | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/44.txt diff --git a/app/build.gradle b/app/build.gradle index c280794..1a0a6fb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,8 +23,8 @@ android { minSdkVersion 21 targetSdkVersion 35 compileSdk 35 - versionCode 43 - versionName "0.1.3" + versionCode 44 + versionName "0.1.4" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/fastlane/metadata/android/en-US/changelogs/44.txt b/fastlane/metadata/android/en-US/changelogs/44.txt new file mode 100644 index 0000000..6c6c4f7 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/44.txt @@ -0,0 +1,11 @@ +* New action: Launch other launchers +* New action: Show recent apps (workaround for an Android bug) +* Fixed "Set µLauncher as home screen" button +* Size of "choose app" button was limited + +* Added Arabic translation (thank you, letterhaven!) +* Started Lithuanian translation (thank you, IdeallyGrey!) +* Improved Chinese translation (thank you, monkeyotg!) +* Improved Portuguese translation (thank you, "Vossa Excelencia"!) +* Improved Spanish translation (thank you, T!) + From 4f795289d5176fb6c9130cd8aa136061eb6d0793 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Tue, 15 Apr 2025 18:55:13 +0200 Subject: [PATCH 026/118] improve English translation --- app/src/main/res/values/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a8bc9b3..21f25f5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -251,12 +251,12 @@ Music: Next Music: Previous Music: Play / Pause - Expand notifications panel + Expand Notifications Panel Recent Apps - Do nothing + Do Nothing Lock Screen Toggle Torch - Launch other Home Screen + Launch Other Home Screen Add Shortcut From 22633bdac3d057b552258d8bd84868c9cc7473b8 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Tue, 15 Apr 2025 19:24:23 +0200 Subject: [PATCH 027/118] try to fix #138 --- .../jrpie/android/launcher/ui/HomeActivity.kt | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt index 53a0876..2ab5d9f 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt @@ -37,7 +37,7 @@ import java.util.Locale class HomeActivity : UIObject, AppCompatActivity() { private lateinit var binding: HomeBinding - private lateinit var touchGestureDetector: TouchGestureDetector + private var touchGestureDetector: TouchGestureDetector? = null private var sharedPreferencesListener = SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey -> @@ -56,29 +56,12 @@ class HomeActivity : UIObject, AppCompatActivity() { super.onCreate(savedInstanceState) super.onCreate() - touchGestureDetector = TouchGestureDetector( - this, 0, 0, - LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f - ) - touchGestureDetector.updateScreenSize(windowManager) // Initialise layout binding = HomeBinding.inflate(layoutInflater) setContentView(binding.root) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - binding.root.setOnApplyWindowInsetsListener { _, windowInsets -> - @Suppress("deprecation") // required to support API 29 - val insets = windowInsets.systemGestureInsets - touchGestureDetector.setSystemGestureInsets(insets) - - windowInsets - } - } - - - // Handle back key / gesture on Android 13+, cf. onKeyDown() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { onBackInvokedDispatcher.registerOnBackInvokedCallback( @@ -94,7 +77,7 @@ class HomeActivity : UIObject, AppCompatActivity() { override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) - touchGestureDetector.updateScreenSize(windowManager) + touchGestureDetector?.updateScreenSize(windowManager) } override fun onStart() { @@ -188,8 +171,28 @@ class HomeActivity : UIObject, AppCompatActivity() { override fun onResume() { super.onResume() - touchGestureDetector.edgeWidth = + /* This should be initialized in onCreate() + However on some devices there seems to be a bug where the touchGestureDetector + is not working properly after resuming the app. + Reinitializing the touchGestureDetector every time the app is resumed might help to fix that. + (see issue #138) + */ + touchGestureDetector = TouchGestureDetector( + this, 0, 0, LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f + ).also { + it.updateScreenSize(windowManager) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + binding.root.setOnApplyWindowInsetsListener { _, windowInsets -> + @Suppress("deprecation") // required to support API 29 + val insets = windowInsets.systemGestureInsets + touchGestureDetector?.setSystemGestureInsets(insets) + + windowInsets + } + } initClock() updateSettingsFallbackButtonVisibility() @@ -230,7 +233,7 @@ class HomeActivity : UIObject, AppCompatActivity() { } override fun onTouchEvent(event: MotionEvent): Boolean { - touchGestureDetector.onTouchEvent(event) + touchGestureDetector?.onTouchEvent(event) return true } From 077bd1ce448808f969254e704681924143873da7 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Wed, 23 Apr 2025 01:51:01 +0200 Subject: [PATCH 028/118] add option to hide keyboard when scrolling (cf. #142) --- .../preferences/LauncherPreferences$Config.java | 1 + .../java/de/jrpie/android/launcher/ui/Helper.kt | 16 ++++++++++++---- .../launcher/ui/list/apps/ListFragmentApps.kt | 17 +++++++++++++++++ app/src/main/res/values/donottranslate.xml | 1 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/preferences.xml | 4 ++++ 6 files changed, 36 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java b/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java index 85979fe..4653910 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java @@ -72,6 +72,7 @@ import eu.jonahbauer.android.preference.annotations.Preferences; @Preference(name = "search_auto_launch", type = boolean.class, defaultValue = "true"), @Preference(name = "search_web", type = boolean.class, description = "false"), @Preference(name = "search_auto_open_keyboard", type = boolean.class, defaultValue = "true"), + @Preference(name = "search_auto_close_keyboard", type = boolean.class, defaultValue = "false"), }), @PreferenceGroup(name = "enabled_gestures", prefix = "settings_enabled_gestures_", suffix = "_key", value = { @Preference(name = "double_swipe", type = boolean.class, defaultValue = "true"), diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/Helper.kt b/app/src/main/java/de/jrpie/android/launcher/ui/Helper.kt index 1ca4d2b..a863c67 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/Helper.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/Helper.kt @@ -1,5 +1,6 @@ package de.jrpie.android.launcher.ui +import android.app.Activity import android.content.Context import android.graphics.ColorMatrix import android.graphics.ColorMatrixColorFilter @@ -38,10 +39,17 @@ fun ImageView.transformGrayscale(grayscale: Boolean) { } -// Taken from https://stackoverflow.com/a/50743764/12787264 +// Taken from https://stackoverflow.com/a/50743764 fun View.openSoftKeyboard(context: Context) { this.requestFocus() - // open the soft keyboard - val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) + (context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager) + .showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) } + +// https://stackoverflow.com/a/17789187 +fun closeSoftKeyboard(activity: Activity) { + activity.currentFocus?.let { focus -> + (activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager) + .hideSoftInputFromWindow( focus.windowToken, 0 ) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/ListFragmentApps.kt b/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/ListFragmentApps.kt index 1a55bbb..a8e59ba 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/ListFragmentApps.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/ListFragmentApps.kt @@ -11,13 +11,16 @@ import android.widget.Toast import androidx.fragment.app.Fragment import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import de.jrpie.android.launcher.R import de.jrpie.android.launcher.apps.AppFilter import de.jrpie.android.launcher.databinding.ListAppsBinding import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.ui.UIObject +import de.jrpie.android.launcher.ui.closeSoftKeyboard import de.jrpie.android.launcher.ui.list.ListActivity import de.jrpie.android.launcher.ui.openSoftKeyboard +import kotlin.math.absoluteValue /** @@ -90,6 +93,20 @@ class ListFragmentApps : Fragment(), UIObject { } } adapter = appsRecyclerAdapter + if (LauncherPreferences.functionality().searchAutoCloseKeyboard()) { + addOnScrollListener(object : RecyclerView.OnScrollListener() { + var totalDy: Int = 0 + var threshold = (resources.displayMetrics.density * 100).toInt() + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + totalDy += dy + + if (totalDy.absoluteValue > 100) { + totalDy = 0 + closeSoftKeyboard(requireActivity()) + } + } + }) + } } binding.listAppsSearchview.setOnQueryTextListener(object : diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 89ec086..69c7f6a 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -147,6 +147,7 @@ functionality.search_auto_launch functionality.search_web functionality.search_auto_keyboard + functionality.search_auto_close_keyboard settings_action_lock_method diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 21f25f5..ed2bb66 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -167,6 +167,7 @@ Search the web Press return while searching the app list to launch a web search. Start keyboard for search + Close keyboard when scrolling Sensitivity diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 6ef5d07..7d906ff 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -104,6 +104,10 @@ android:key="@string/settings_functionality_search_auto_open_keyboard_key" android:defaultValue="true" android:title="@string/settings_functionality_auto_keyboard" /> + Date: Thu, 24 Apr 2025 14:37:05 +0200 Subject: [PATCH 029/118] add support for app widgets (see #44) --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 10 +- .../de/jrpie/android/launcher/Application.kt | 26 ++- .../de/jrpie/android/launcher/Functions.kt | 5 +- .../LauncherPreferences$Config.java | 4 + .../launcher/preferences/Preferences.kt | 10 + .../serialization/PreferenceSerializers.kt | 19 ++ .../jrpie/android/launcher/ui/HomeActivity.kt | 95 +++------- .../launcher/ui/PinShortcutActivity.kt | 16 +- .../launcher/SettingsFragmentLauncher.kt | 9 + .../android/launcher/ui/widgets/ClockView.kt | 80 ++++++++ .../ui/widgets/WidgetContainerView.kt | 137 ++++++++++++++ .../widgets/manage/ManageWidgetsActivity.kt | 173 ++++++++++++++++++ .../ui/widgets/manage/SelectWidgetActivity.kt | 168 +++++++++++++++++ .../ui/widgets/manage/WidgetManagerView.kt | 172 +++++++++++++++++ .../ui/widgets/manage/WidgetOverlayView.kt | 131 +++++++++++++ .../android/launcher/widgets/AppWidget.kt | 120 ++++++++++++ .../android/launcher/widgets/ClockWidget.kt | 41 +++++ .../widgets/LauncherWidgetProvider.kt | 58 ++++++ .../jrpie/android/launcher/widgets/Widget.kt | 60 ++++++ .../launcher/widgets/WidgetPosition.kt | 58 ++++++ .../jrpie/android/launcher/widgets/Widgets.kt | 87 +++++++++ app/src/main/res/drawable/baseline_add_24.xml | 11 ++ .../main/res/drawable/baseline_clock_24.xml | 15 ++ .../res/layout/activity_manage_widgets.xml | 25 +++ .../res/layout/activity_select_widget.xml | 72 ++++++++ app/src/main/res/layout/clock.xml | 35 ++++ app/src/main/res/layout/home.xml | 27 +-- .../main/res/layout/list_widgets_header.xml | 35 ++++ app/src/main/res/layout/list_widgets_row.xml | 60 ++++++ app/src/main/res/values/colors.xml | 4 + app/src/main/res/values/donottranslate.xml | 1 + app/src/main/res/values/strings.xml | 12 ++ app/src/main/res/values/styles.xml | 10 +- app/src/main/res/xml/preferences.xml | 4 + 35 files changed, 1691 insertions(+), 100 deletions(-) create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/widgets/ClockView.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/LauncherWidgetProvider.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/WidgetPosition.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt create mode 100644 app/src/main/res/drawable/baseline_add_24.xml create mode 100644 app/src/main/res/drawable/baseline_clock_24.xml create mode 100644 app/src/main/res/layout/activity_manage_widgets.xml create mode 100644 app/src/main/res/layout/activity_select_widget.xml create mode 100644 app/src/main/res/layout/clock.xml create mode 100644 app/src/main/res/layout/list_widgets_header.xml create mode 100644 app/src/main/res/layout/list_widgets_row.xml diff --git a/app/build.gradle b/app/build.gradle index 1a0a6fb..eaf97f7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -106,6 +106,7 @@ dependencies { implementation 'com.google.android.material:material:1.12.0' implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") implementation "eu.jonahbauer:android-preference-annotations:1.1.2" + implementation 'androidx.activity:activity:1.10.1' annotationProcessor "eu.jonahbauer:android-preference-annotations:1.1.2" annotationProcessor "com.android.databinding:compiler:$android_plugin_version" testImplementation 'junit:junit:4.13.2' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a5f8831..841c9bd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ tools:ignore="QueryAllPackagesPermission" /> + + + - + \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/Application.kt b/app/src/main/java/de/jrpie/android/launcher/Application.kt index e6cce23..775621c 100644 --- a/app/src/main/java/de/jrpie/android/launcher/Application.kt +++ b/app/src/main/java/de/jrpie/android/launcher/Application.kt @@ -12,6 +12,8 @@ import android.os.Build.VERSION_CODES import android.os.UserHandle import androidx.core.content.ContextCompat import androidx.lifecycle.MutableLiveData +import android.appwidget.AppWidgetHost +import android.appwidget.AppWidgetManager import androidx.preference.PreferenceManager import de.jrpie.android.launcher.actions.TorchManager import de.jrpie.android.launcher.apps.AbstractAppInfo @@ -20,13 +22,22 @@ import de.jrpie.android.launcher.apps.isPrivateSpaceLocked import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.migratePreferencesToNewVersion import de.jrpie.android.launcher.preferences.resetPreferences +import de.jrpie.android.launcher.widgets.LauncherWidgetProvider +import de.jrpie.android.launcher.widgets.Widget import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch + +const val APP_WIDGET_HOST_ID = 42; + + class Application : android.app.Application() { val apps = MutableLiveData>() + val widgets = MutableLiveData>() val privateSpaceLocked = MutableLiveData() + lateinit var appWidgetHost: AppWidgetHost + lateinit var appWidgetManager: AppWidgetManager private val profileAvailabilityBroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { @@ -90,6 +101,8 @@ class Application : android.app.Application() { customAppNames = LauncherPreferences.apps().customNames() } else if (pref == LauncherPreferences.apps().keys().pinnedShortcuts()) { loadApps() + } else if (pref == LauncherPreferences.widgets().keys().widgets()) { + widgets.postValue(LauncherPreferences.widgets().widgets() ?: setOf()) } } @@ -103,10 +116,15 @@ class Application : android.app.Application() { torchManager = TorchManager(this) } + appWidgetHost = AppWidgetHost(this.applicationContext, APP_WIDGET_HOST_ID) + appWidgetManager = AppWidgetManager.getInstance(this.applicationContext) + + appWidgetHost.startListening() + + val preferences = PreferenceManager.getDefaultSharedPreferences(this) LauncherPreferences.init(preferences, this.resources) - // Try to restore old preferences migratePreferencesToNewVersion(this) @@ -157,4 +175,10 @@ class Application : android.app.Application() { apps.postValue(getApps(packageManager, applicationContext)) } } + + override fun onTerminate() { + appWidgetHost.stopListening() + super.onTerminate() + + } } diff --git a/app/src/main/java/de/jrpie/android/launcher/Functions.kt b/app/src/main/java/de/jrpie/android/launcher/Functions.kt index afc2c31..9679ae5 100644 --- a/app/src/main/java/de/jrpie/android/launcher/Functions.kt +++ b/app/src/main/java/de/jrpie/android/launcher/Functions.kt @@ -6,6 +6,9 @@ import android.app.role.RoleManager import android.content.ActivityNotFoundException import android.content.ClipData import android.content.ClipboardManager +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.appwidget.AppWidgetProviderInfo import android.content.Context import android.content.Intent import android.content.pm.LauncherApps @@ -223,4 +226,4 @@ fun copyToClipboard(context: Context, text: String) { val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clipData = ClipData.newPlainText("Debug Info", text) clipboardManager.setPrimaryClip(clipData) -} \ No newline at end of file +} diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java b/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java index 4653910..575346a 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java @@ -8,6 +8,7 @@ import de.jrpie.android.launcher.actions.lock.LockMethod; import de.jrpie.android.launcher.preferences.serialization.MapAbstractAppInfoStringPreferenceSerializer; import de.jrpie.android.launcher.preferences.serialization.SetAbstractAppInfoPreferenceSerializer; import de.jrpie.android.launcher.preferences.serialization.SetPinnedShortcutInfoPreferenceSerializer; +import de.jrpie.android.launcher.preferences.serialization.SetWidgetSerializer; import de.jrpie.android.launcher.preferences.theme.Background; import de.jrpie.android.launcher.preferences.theme.ColorTheme; import de.jrpie.android.launcher.preferences.theme.Font; @@ -82,5 +83,8 @@ import eu.jonahbauer.android.preference.annotations.Preferences; @PreferenceGroup(name = "actions", prefix = "settings_actions_", suffix = "_key", value = { @Preference(name = "lock_method", type = LockMethod.class, defaultValue = "DEVICE_ADMIN"), }), + @PreferenceGroup(name = "widgets", prefix = "settings_widgets_", suffix= "_key", value = { + @Preference(name = "widgets", type = Set.class, serializer = SetWidgetSerializer.class) + }), }) public final class LauncherPreferences$Config {} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt index 59ecc7a..332f4df 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt @@ -13,6 +13,9 @@ import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersio import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion3 import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersionUnknown import de.jrpie.android.launcher.ui.HomeActivity +import de.jrpie.android.launcher.widgets.ClockWidget +import de.jrpie.android.launcher.widgets.WidgetPosition +import de.jrpie.android.launcher.widgets.deleteAllWidgets /* Current version of the structure of preferences. * Increase when breaking changes are introduced and write an appropriate case in @@ -71,6 +74,13 @@ fun resetPreferences(context: Context) { Log.i(TAG, "Resetting preferences") LauncherPreferences.clear() LauncherPreferences.internal().versionCode(PREFERENCE_VERSION) + deleteAllWidgets(context) + + LauncherPreferences.widgets().widgets( + setOf( + ClockWidget(-500, WidgetPosition(1,4,10,3)) + ) + ) val hidden: MutableSet = mutableSetOf() diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt index 3e19daf..a2749ae 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt @@ -4,6 +4,7 @@ package de.jrpie.android.launcher.preferences.serialization import de.jrpie.android.launcher.apps.AbstractAppInfo import de.jrpie.android.launcher.apps.PinnedShortcutInfo +import de.jrpie.android.launcher.widgets.Widget import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializationException import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializer import kotlinx.serialization.Serializable @@ -28,6 +29,24 @@ class SetAbstractAppInfoPreferenceSerializer : } } + +@Suppress("UNCHECKED_CAST") +class SetWidgetSerializer : + PreferenceSerializer?, java.util.Set?> { + @Throws(PreferenceSerializationException::class) + override fun serialize(value: java.util.Set?): java.util.Set? { + return value?.map(Widget::serialize) + ?.toHashSet() as? java.util.Set + } + + @Throws(PreferenceSerializationException::class) + override fun deserialize(value: java.util.Set?): java.util.Set? { + return value?.map(java.lang.String::toString)?.map(Widget::deserialize) + ?.toHashSet() as? java.util.Set + } +} + + @Suppress("UNCHECKED_CAST") class SetPinnedShortcutInfoPreferenceSerializer : PreferenceSerializer?, java.util.Set?> { diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt index 2ab5d9f..3f1e497 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt @@ -1,6 +1,7 @@ package de.jrpie.android.launcher.ui import android.annotation.SuppressLint +import android.app.Activity import android.content.SharedPreferences import android.content.res.Configuration import android.content.res.Resources @@ -10,8 +11,7 @@ import android.view.KeyEvent import android.view.MotionEvent import android.view.View import android.window.OnBackInvokedDispatcher -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.isVisible +import de.jrpie.android.launcher.Application import de.jrpie.android.launcher.R import de.jrpie.android.launcher.actions.Action import de.jrpie.android.launcher.actions.Gesture @@ -20,7 +20,6 @@ import de.jrpie.android.launcher.databinding.HomeBinding import de.jrpie.android.launcher.openTutorial import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.ui.tutorial.TutorialActivity -import java.util.Locale /** * [HomeActivity] is the actual application Launcher, @@ -34,7 +33,7 @@ import java.util.Locale * - Setting global variables (preferences etc.) * - Opening the [TutorialActivity] on new installations */ -class HomeActivity : UIObject, AppCompatActivity() { +class HomeActivity : UIObject, Activity() { private lateinit var binding: HomeBinding private var touchGestureDetector: TouchGestureDetector? = null @@ -45,15 +44,18 @@ class HomeActivity : UIObject, AppCompatActivity() { prefKey?.startsWith("display.") == true ) { recreate() + } else if (prefKey?.startsWith("action.") == true) { + updateSettingsFallbackButtonVisibility() + } else if (prefKey == LauncherPreferences.widgets().keys().widgets()) { + binding.homeWidgetContainer.updateWidgets(this@HomeActivity, + LauncherPreferences.widgets().widgets() + ) } - if (prefKey?.startsWith("action.") == true) { - updateSettingsFallbackButtonVisibility() - } } override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) + super.onCreate(savedInstanceState) super.onCreate() @@ -81,8 +83,7 @@ class HomeActivity : UIObject, AppCompatActivity() { } override fun onStart() { - super.onStart() - + super.onStart() super.onStart() // If the tutorial was not finished, start it @@ -93,6 +94,15 @@ class HomeActivity : UIObject, AppCompatActivity() { LauncherPreferences.getSharedPreferences() .registerOnSharedPreferenceChangeListener(sharedPreferencesListener) + (application as Application).appWidgetHost.startListening() + + } + + + + override fun onStop() { + (application as Application).appWidgetHost.stopListening() + super.onStop() } override fun onWindowFocusChanged(hasFocus: Boolean) { @@ -118,44 +128,6 @@ class HomeActivity : UIObject, AppCompatActivity() { } } - private fun initClock() { - val locale = Locale.getDefault() - val dateVisible = LauncherPreferences.clock().dateVisible() - val timeVisible = LauncherPreferences.clock().timeVisible() - - var dateFMT = "yyyy-MM-dd" - var timeFMT = "HH:mm" - if (LauncherPreferences.clock().showSeconds()) { - timeFMT += ":ss" - } - - if (LauncherPreferences.clock().localized()) { - dateFMT = android.text.format.DateFormat.getBestDateTimePattern(locale, dateFMT) - timeFMT = android.text.format.DateFormat.getBestDateTimePattern(locale, timeFMT) - } - - var upperFormat = dateFMT - var lowerFormat = timeFMT - var upperVisible = dateVisible - var lowerVisible = timeVisible - - if (LauncherPreferences.clock().flipDateTime()) { - upperFormat = lowerFormat.also { lowerFormat = upperFormat } - upperVisible = lowerVisible.also { lowerVisible = upperVisible } - } - - binding.homeUpperView.isVisible = upperVisible - binding.homeLowerView.isVisible = lowerVisible - - binding.homeUpperView.setTextColor(LauncherPreferences.clock().color()) - binding.homeLowerView.setTextColor(LauncherPreferences.clock().color()) - - binding.homeLowerView.format24Hour = lowerFormat - binding.homeUpperView.format24Hour = upperFormat - binding.homeLowerView.format12Hour = lowerFormat - binding.homeUpperView.format12Hour = upperFormat - } - override fun getTheme(): Resources.Theme { val mTheme = modifyTheme(super.getTheme()) mTheme.applyStyle(R.style.backgroundWallpaper, true) @@ -193,9 +165,11 @@ class HomeActivity : UIObject, AppCompatActivity() { windowInsets } } - - initClock() updateSettingsFallbackButtonVisibility() + + binding.homeWidgetContainer.updateWidgets(this@HomeActivity, + LauncherPreferences.widgets().widgets() + ) } override fun onDestroy() { @@ -233,30 +207,11 @@ class HomeActivity : UIObject, AppCompatActivity() { } override fun onTouchEvent(event: MotionEvent): Boolean { + android.util.Log.e("Launcher", "on touch") touchGestureDetector?.onTouchEvent(event) return true } - override fun setOnClicks() { - - binding.homeUpperView.setOnClickListener { - if (LauncherPreferences.clock().flipDateTime()) { - Gesture.TIME(this) - } else { - Gesture.DATE(this) - } - } - - binding.homeLowerView.setOnClickListener { - if (LauncherPreferences.clock().flipDateTime()) { - Gesture.DATE(this) - } else { - Gesture.TIME(this) - } - } - } - - private fun handleBack() { Gesture.BACK(this) } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/PinShortcutActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/PinShortcutActivity.kt index 71908ba..3dbdda8 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/PinShortcutActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/PinShortcutActivity.kt @@ -49,7 +49,21 @@ class PinShortcutActivity : AppCompatActivity(), UIObject { val request = launcherApps.getPinItemRequest(intent) this.request = request - if (request == null || request.requestType != PinItemRequest.REQUEST_TYPE_SHORTCUT) { + if (request == null) { + finish() + return + } + + if (request.requestType == PinItemRequest.REQUEST_TYPE_APPWIDGET) { + + // TODO + request.getAppWidgetProviderInfo(this) + // startActivity() + finish() + return + } + + if (request.requestType != PinItemRequest.REQUEST_TYPE_SHORTCUT) { finish() return } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/settings/launcher/SettingsFragmentLauncher.kt b/app/src/main/java/de/jrpie/android/launcher/ui/settings/launcher/SettingsFragmentLauncher.kt index a8efb43..8907f04 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/settings/launcher/SettingsFragmentLauncher.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/settings/launcher/SettingsFragmentLauncher.kt @@ -11,6 +11,7 @@ import de.jrpie.android.launcher.actions.openAppsList import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.theme.ColorTheme import de.jrpie.android.launcher.setDefaultHomeScreen +import de.jrpie.android.launcher.ui.widgets.manage.ManageWidgetsActivity /** @@ -81,6 +82,14 @@ class SettingsFragmentLauncher : PreferenceFragmentCompat() { true } + val manageWidgets = findPreference( + LauncherPreferences.widgets().keys().widgets() + ) + manageWidgets?.setOnPreferenceClickListener { + startActivity(Intent(requireActivity(), ManageWidgetsActivity::class.java)) + true + } + val hiddenApps = findPreference( LauncherPreferences.apps().keys().hidden() ) diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/ClockView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/ClockView.kt new file mode 100644 index 0000000..33c4888 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/ClockView.kt @@ -0,0 +1,80 @@ +package de.jrpie.android.launcher.ui.widgets + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible +import de.jrpie.android.launcher.actions.Gesture +import de.jrpie.android.launcher.databinding.ClockBinding +import de.jrpie.android.launcher.preferences.LauncherPreferences +import java.util.Locale + +class ClockView(context: Context, attrs: AttributeSet? = null, val appWidgetId: Int): ConstraintLayout(context, attrs) { + + val binding: ClockBinding = ClockBinding.inflate(LayoutInflater.from(context), this, true) + init { + initClock() + setOnClicks() + } + + + private fun initClock() { + val locale = Locale.getDefault() + val dateVisible = LauncherPreferences.clock().dateVisible() + val timeVisible = LauncherPreferences.clock().timeVisible() + + var dateFMT = "yyyy-MM-dd" + var timeFMT = "HH:mm" + if (LauncherPreferences.clock().showSeconds()) { + timeFMT += ":ss" + } + + if (LauncherPreferences.clock().localized()) { + dateFMT = android.text.format.DateFormat.getBestDateTimePattern(locale, dateFMT) + timeFMT = android.text.format.DateFormat.getBestDateTimePattern(locale, timeFMT) + } + + var upperFormat = dateFMT + var lowerFormat = timeFMT + var upperVisible = dateVisible + var lowerVisible = timeVisible + + if (LauncherPreferences.clock().flipDateTime()) { + upperFormat = lowerFormat.also { lowerFormat = upperFormat } + upperVisible = lowerVisible.also { lowerVisible = upperVisible } + } + + binding.clockUpperView.isVisible = upperVisible + binding.clockLowerView.isVisible = lowerVisible + + binding.clockUpperView.setTextColor(LauncherPreferences.clock().color()) + binding.clockLowerView.setTextColor(LauncherPreferences.clock().color()) + + binding.clockLowerView.format24Hour = lowerFormat + binding.clockUpperView.format24Hour = upperFormat + binding.clockLowerView.format12Hour = lowerFormat + binding.clockUpperView.format12Hour = upperFormat + } + + fun setOnClicks() { + binding.clockUpperView.setOnClickListener { + if (LauncherPreferences.clock().flipDateTime()) { + Gesture.TIME(context) + } else { + Gesture.DATE(context) + } + } + + binding.clockLowerView.setOnClickListener { + if (LauncherPreferences.clock().flipDateTime()) { + Gesture.DATE(context) + } else { + Gesture.TIME(context) + } + } + } + + + +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt new file mode 100644 index 0000000..04668ca --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt @@ -0,0 +1,137 @@ +package de.jrpie.android.launcher.ui.widgets + +import android.app.Activity +import android.content.Context +import android.graphics.PointF +import android.graphics.RectF +import android.util.AttributeSet +import android.util.Log +import android.view.MotionEvent +import android.view.View +import android.view.View.MeasureSpec.makeMeasureSpec +import android.view.ViewGroup +import androidx.core.graphics.contains +import androidx.core.view.size +import de.jrpie.android.launcher.widgets.Widget +import de.jrpie.android.launcher.widgets.WidgetPosition +import kotlin.math.max + + +/** + * This only works in an Activity, not AppCompatActivity + */ +open class WidgetContainerView(context: Context, attrs: AttributeSet? = null) : ViewGroup(context, attrs) { + + var widgetViewById = HashMap() + + open fun updateWidgets(activity: Activity, widgets: Set?) { + if (widgets == null) { + return + } + Log.i("WidgetContainer", "updating ${activity.localClassName}") + widgetViewById.clear() + (0.. + widget.createView(activity)?.let { + addView(it, WidgetContainerView.Companion.LayoutParams(widget.position)) + widgetViewById.put(widget.id, it) + } + } + } + + override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { + if (ev == null) { + return false + } + val position = PointF(ev.x, ev.y) + + return widgetViewById.filter { + RectF( + it.value.x, + it.value.y, + it.value.x + it.value.width, + it.value.y + it.value.height + ).contains(position) == true + }.any { + Widget.byId(context, it.key)?.allowInteraction == false + } + } + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + + var maxHeight = suggestedMinimumHeight + var maxWidth = suggestedMinimumWidth + + val mWidth = MeasureSpec.getSize(widthMeasureSpec) + val mHeight = MeasureSpec.getSize(heightMeasureSpec) + + (0.. + if (prefKey == LauncherPreferences.widgets().keys().widgets()) { + // We can't observe the livedata because this is not an AppCompatActivity + findViewById(R.id.manage_widgets_container).updateWidgets(this, + LauncherPreferences.widgets().widgets() + ) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + super.onCreate() + setContentView(R.layout.activity_manage_widgets) + findViewById(R.id.manage_widgets_button_add).setOnClickListener { + selectWidget() + } + + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + + findViewById(R.id.manage_widgets_container).updateWidgets(this, + (application as Application).widgets.value + ) + } + + override fun onStart() { + super.onStart() + super.onStart() + + LauncherPreferences.getSharedPreferences() + .registerOnSharedPreferenceChangeListener(sharedPreferencesListener) + + } + + override fun onResume() { + super.onResume() + findViewById(R.id.manage_widgets_container).updateWidgets(this, + LauncherPreferences.widgets().widgets() + ) + + } + override fun getTheme(): Resources.Theme { + val mTheme = modifyTheme(super.getTheme()) + mTheme.applyStyle(R.style.backgroundWallpaper, true) + LauncherPreferences.clock().font().applyToTheme(mTheme) + LauncherPreferences.theme().colorTheme().applyToTheme( + mTheme, + LauncherPreferences.theme().textShadow() + ) + return mTheme + } + + override fun onDestroy() { + LauncherPreferences.getSharedPreferences() + .unregisterOnSharedPreferenceChangeListener(sharedPreferencesListener) + super.onDestroy() + } + + + fun selectWidget() { + val appWidgetHost = (application as Application).appWidgetHost + startActivityForResult( + Intent(this, SelectWidgetActivity::class.java).also { + it.putExtra( + AppWidgetManager.EXTRA_APPWIDGET_ID, + appWidgetHost.allocateAppWidgetId() + ) + }, REQUEST_PICK_APPWIDGET + ) + } + + + fun createWidget(data: Intent) { + Log.i("Launcher", "creating widget") + val appWidgetManager = (application as Application).appWidgetManager + val appWidgetId = data.extras?.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID) ?: return + + val provider = appWidgetManager.getAppWidgetInfo(appWidgetId) + + val display = windowManager.defaultDisplay + + val position = WidgetPosition.fromAbsoluteRect( + Rect(0,0, + min(400, appWidgetManager.getAppWidgetInfo(appWidgetId).minWidth), + min(400, appWidgetManager.getAppWidgetInfo(appWidgetId).minHeight) + ), + display.width, + display.height + ) + + val widget = AppWidget(appWidgetId, provider, position) + LauncherPreferences.widgets().widgets( + (LauncherPreferences.widgets().widgets() ?: HashSet()).also { + it.add(widget) + } + ) + } + + private fun configureWidget(data: Intent) { + val extras = data.extras + val appWidgetId = extras!!.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) + val widget = AppWidget(appWidgetId) + if (widget.isConfigurable(this)) { + widget.configure(this, REQUEST_CREATE_APPWIDGET) + } else { + createWidget(data) + } + } + + override fun onActivityResult( + requestCode: Int, resultCode: Int, + data: Intent? + ) { + super.onActivityResult(requestCode, resultCode, data) + if (resultCode == RESULT_OK) { + if (requestCode == REQUEST_PICK_APPWIDGET) { + configureWidget(data!!) + } else if (requestCode == REQUEST_CREATE_APPWIDGET) { + createWidget(data!!) + } + } else if (resultCode == RESULT_CANCELED && data != null) { + val appWidgetId = + data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) + if (appWidgetId != -1) { + AppWidget(appWidgetId).delete(this) + } + } + } + + + /** + * For a better preview, [ManageWidgetsActivity] should behave exactly like [HomeActivity] + */ + override fun isHomeScreen(): Boolean { + return true + } +} diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt new file mode 100644 index 0000000..c414db6 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt @@ -0,0 +1,168 @@ +package de.jrpie.android.launcher.ui.widgets.manage + +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProviderInfo +import android.content.Intent +import android.content.res.Resources +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import de.jrpie.android.launcher.R +import de.jrpie.android.launcher.databinding.ActivitySelectWidgetBinding +import de.jrpie.android.launcher.ui.UIObject +import de.jrpie.android.launcher.widgets.ClockWidget +import de.jrpie.android.launcher.widgets.LauncherAppWidgetProvider +import de.jrpie.android.launcher.widgets.LauncherClockWidgetProvider +import de.jrpie.android.launcher.widgets.LauncherWidgetProvider +import de.jrpie.android.launcher.widgets.WidgetPosition +import de.jrpie.android.launcher.widgets.bindAppWidgetOrRequestPermission +import de.jrpie.android.launcher.widgets.getAppWidgetHost +import de.jrpie.android.launcher.widgets.getAppWidgetProviders +import de.jrpie.android.launcher.widgets.updateWidget + + +private const val REQUEST_WIDGET_PERMISSION = 29 + +/** + * This activity lets the user pick an app widget to add. + * It provides an interface similar to [android.appwidget.AppWidgetManager.ACTION_APPWIDGET_PICK], + * but shows more information and also shows widgets from other user profiles. + */ +class SelectWidgetActivity : AppCompatActivity(), UIObject { + lateinit var binding: ActivitySelectWidgetBinding + var widgetId: Int = -1 + + private fun tryBindWidget(info: LauncherWidgetProvider) { + when (info) { + is LauncherAppWidgetProvider -> { + if (bindAppWidgetOrRequestPermission( + this, + info.info, + widgetId, + REQUEST_WIDGET_PERMISSION + ) + ) { + setResult( + RESULT_OK, + Intent().also { + it.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId) + } + ) + finish() + } + } + is LauncherClockWidgetProvider -> { + updateWidget(ClockWidget(widgetId, WidgetPosition(0,4,12,3))) + finish() + } + } + } + + override fun onStart() { + super.onStart() + super.onStart() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + super.onCreate() + + binding = ActivitySelectWidgetBinding.inflate(layoutInflater) + setContentView(binding.root) + + + widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) + if (widgetId == -1) { + widgetId = getAppWidgetHost().allocateAppWidgetId() + } + + val viewManager = LinearLayoutManager(this) + val viewAdapter = SelectWidgetRecyclerAdapter() + + binding.selectWidgetRecycler.apply { + setHasFixedSize(false) + layoutManager = viewManager + adapter = viewAdapter + } + } + + override fun getTheme(): Resources.Theme { + return modifyTheme(super.getTheme()) + } + + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + if (requestCode == REQUEST_WIDGET_PERMISSION && resultCode == RESULT_OK) { + data ?: return + val provider = (data.getSerializableExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER) as? AppWidgetProviderInfo) ?: return + tryBindWidget(LauncherAppWidgetProvider(provider)) + } + } + + inner class SelectWidgetRecyclerAdapter() : + RecyclerView.Adapter() { + + private val widgets = getAppWidgetProviders(this@SelectWidgetActivity).toTypedArray() + + inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), + View.OnClickListener { + var textView: TextView = itemView.findViewById(R.id.list_widgets_row_name) + var descriptionView: TextView = itemView.findViewById(R.id.list_widgets_row_description) + var iconView: ImageView = itemView.findViewById(R.id.list_widgets_row_icon) + var previewView: ImageView = itemView.findViewById(R.id.list_widgets_row_preview) + + + override fun onClick(v: View) { + tryBindWidget(widgets[bindingAdapterPosition]) + } + + init { + itemView.setOnClickListener(this) + } + } + + override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) { + val label = widgets[i].loadLabel(this@SelectWidgetActivity) + val description = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + widgets[i].loadDescription(this@SelectWidgetActivity) + } else { + "" + } + val preview = + widgets[i].loadPreviewImage(this@SelectWidgetActivity) + val icon = + widgets[i].loadIcon(this@SelectWidgetActivity) + + viewHolder.textView.text = label + viewHolder.descriptionView.text = description + viewHolder.descriptionView.visibility = + if (description?.isEmpty() == false) { View.VISIBLE } else { View.GONE } + viewHolder.iconView.setImageDrawable(icon) + + viewHolder.previewView.setImageDrawable(preview) + viewHolder.previewView.visibility = + if (preview != null) { View.VISIBLE } else { View.GONE } + + viewHolder.previewView.requestLayout() + } + + override fun getItemCount(): Int { + return widgets.size + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val inflater = LayoutInflater.from(parent.context) + val view: View = inflater.inflate(R.layout.list_widgets_row, parent, false) + return ViewHolder(view) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt new file mode 100644 index 0000000..2d41e13 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt @@ -0,0 +1,172 @@ +package de.jrpie.android.launcher.ui.widgets.manage + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.graphics.Point +import android.graphics.Rect +import android.graphics.RectF +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.util.AttributeSet +import android.view.HapticFeedbackConstants +import android.view.MotionEvent +import android.view.View +import android.view.ViewConfiguration +import androidx.core.graphics.contains +import androidx.core.graphics.minus +import androidx.core.graphics.toRect +import androidx.core.view.children +import de.jrpie.android.launcher.ui.widgets.WidgetContainerView +import de.jrpie.android.launcher.widgets.Widget +import de.jrpie.android.launcher.widgets.WidgetPosition +import de.jrpie.android.launcher.widgets.updateWidget +import kotlin.math.max +import kotlin.math.min + +/** + * A variant of the [WidgetContainerView] which allows to manage widgets. + */ +class WidgetManagerView(context: Context, attrs: AttributeSet? = null) : + WidgetContainerView(context, attrs) { + + val TOUCH_SLOP: Int + val TOUCH_SLOP_SQUARE: Int + val LONG_PRESS_TIMEOUT: Long + + init { + val configuration = ViewConfiguration.get(context) + TOUCH_SLOP = configuration.scaledTouchSlop + TOUCH_SLOP_SQUARE = TOUCH_SLOP * TOUCH_SLOP + + LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout().toLong() + } + + + + enum class EditMode(val resize: (dx: Int, dy: Int, rect: Rect) -> Rect) { + MOVE({ dx, dy, rect -> + Rect(rect.left + dx, rect.top + dy, rect.right + dx, rect.bottom + dy) + }), + TOP({ dx, dy, rect -> + Rect(rect.left, min(rect.top + dy, rect.bottom - 200), rect.right, rect.bottom) + }), + BOTTOM({ dx, dy, rect -> + Rect(rect.left, rect.top, rect.right, max(rect.top + 200, rect.bottom + dy)) + }), + LEFT({ dx, dy, rect -> + Rect(min(rect.left + dx, rect.right - 200), rect.top, rect.right, rect.bottom) + }), + RIGHT({ dx, dy, rect -> + Rect(rect.left, rect.top, max(rect.left + 200, rect.right + dx), rect.bottom) + }), + } + + var selectedWidgetOverlayView: WidgetOverlayView? = null + var selectedWidgetView: View? = null + var currentGestureStart: Point? = null + var startWidgetPosition: Rect? = null + var lastPosition = Rect() + + private val longPressHandler = Handler(Looper.getMainLooper()) + + + override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { + return true + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent?): Boolean { + if (event == null) { + return false + } + synchronized(this) { + if (event.actionMasked == MotionEvent.ACTION_DOWN) { + val start = Point(event.x.toInt(), event.y.toInt()) + currentGestureStart = start + val view = children.mapNotNull { it as? WidgetOverlayView }.firstOrNull { + RectF(it.x, it.y, it.x + it.width, it.y + it.height).toRect().contains(start) == true + } ?: return false + + val position = (view.layoutParams as Companion.LayoutParams).position.getAbsoluteRect(width, height) + selectedWidgetOverlayView = view + selectedWidgetView = widgetViewById.get(view.widgetId) ?: return true + startWidgetPosition = position + + val positionInView = start.minus(Point(position.left, position.top)) + view.mode = view.getHandles().firstOrNull { it.position.contains(positionInView) }?.mode ?: EditMode.MOVE + + longPressHandler.postDelayed({ + synchronized(this@WidgetManagerView) { + view.showPopupMenu() + view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + endInteraction() + } + }, LONG_PRESS_TIMEOUT) + } + if (event.actionMasked == MotionEvent.ACTION_MOVE || + event.actionMasked == MotionEvent.ACTION_UP + ) { + val distanceX = event.x - (currentGestureStart?.x ?: return true) + val distanceY = event.y - (currentGestureStart?.y ?: return true) + if (distanceX * distanceX + distanceY * distanceY > TOUCH_SLOP_SQUARE) { + longPressHandler.removeCallbacksAndMessages(null) + } + val view = selectedWidgetOverlayView ?: return true + val start = startWidgetPosition ?: return true + val absoluteNewPosition = view.mode?.resize( + distanceX.toInt(), + distanceY.toInt(), + start + ) ?: return true + val newPosition = WidgetPosition.fromAbsoluteRect( + absoluteNewPosition, width, height + ) + if (newPosition != lastPosition) { + lastPosition = absoluteNewPosition + (view.layoutParams as Companion.LayoutParams).position = newPosition + (selectedWidgetView?.layoutParams as? Companion.LayoutParams)?.position = newPosition + requestLayout() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_PRESS) + } + } + + if (event.actionMasked == MotionEvent.ACTION_UP) { + longPressHandler.removeCallbacksAndMessages(null) + val id = selectedWidgetOverlayView?.widgetId ?: return true + val widget = Widget.byId(context, id) ?: return true + widget.position = newPosition + endInteraction() + updateWidget(widget) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + view.performHapticFeedback(HapticFeedbackConstants.GESTURE_END) + } + } + } + } + + + return true + } + private fun endInteraction() { + startWidgetPosition = null + selectedWidgetOverlayView?.mode = null + } + + override fun updateWidgets(activity: Activity, widgets: Set?) { + super.updateWidgets(activity, widgets) + if (widgets == null) { + return + } + + widgets.forEach { widget -> + WidgetOverlayView(activity).let { + addView(it) + it.widgetId = widget.id + (it.layoutParams as Companion.LayoutParams).position = widget.position + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt new file mode 100644 index 0000000..0ce789f --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt @@ -0,0 +1,131 @@ +package de.jrpie.android.launcher.ui.widgets.manage + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.View +import android.widget.PopupMenu +import androidx.core.graphics.toRectF +import de.jrpie.android.launcher.R +import de.jrpie.android.launcher.widgets.Widget +import de.jrpie.android.launcher.widgets.updateWidget + +/** + * An overlay to show configuration options for a widget. + */ + +private const val HANDLE_SIZE = 100 +private const val HANDLE_EDGE_SIZE = (1.2 * HANDLE_SIZE).toInt() +class WidgetOverlayView : View { + + + val paint = Paint() + val handlePaint = Paint() + val selectedHandlePaint = Paint() + var mode: WidgetManagerView.EditMode? = null + class Handle(val mode: WidgetManagerView.EditMode, val position: Rect) + init { + handlePaint.style = Paint.Style.STROKE + handlePaint.setARGB(255, 255, 255, 255) + + selectedHandlePaint.style = Paint.Style.FILL_AND_STROKE + selectedHandlePaint.setARGB(100, 255, 255, 255) + + paint.style = Paint.Style.STROKE + paint.setARGB(255, 255, 255, 255) + } + + private var preview: Drawable? = null + var widgetId: Int = -1 + set(newId) { + field = newId + preview = Widget.byId(context, widgetId)?.getPreview(context) + } + + constructor(context: Context) : super(context) { + init(null, 0) + } + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + init(attrs, 0) + } + + constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super( + context, + attrs, + defStyle + ) { + init(attrs, defStyle) + } + + private fun init(attrs: AttributeSet?, defStyle: Int) { } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + getHandles().forEach { + if (it.mode == mode) { + canvas.drawRoundRect(it.position.toRectF(), 5f, 5f, selectedHandlePaint) + } else { + canvas.drawRoundRect(it.position.toRectF(), 5f, 5f, handlePaint) + } + } + val bounds = getBounds() + canvas.drawRoundRect(bounds.toRectF(), 5f, 5f, paint) + + if (mode == null) { + return + } + + //preview?.bounds = bounds + //preview?.draw(canvas) + + + } + + fun showPopupMenu() { + val widget = Widget.byId(context, widgetId)?: return + val menu = PopupMenu(context, this) + menu.menu.let { + it.add( + context.getString(R.string.widget_menu_remove) + ).setOnMenuItemClickListener { _ -> + Widget.byId(context, widgetId)?.delete(context) + return@setOnMenuItemClickListener true + } + it.add( + if (widget.allowInteraction) { + context.getString(R.string.widget_menu_disable_interaction) + } else { + context.getString(R.string.widget_menu_enable_interaction) + } + ).setOnMenuItemClickListener { _ -> + widget.allowInteraction = !widget.allowInteraction + updateWidget(widget) + return@setOnMenuItemClickListener true + } + } + menu.show() + } + + fun getHandles(): List { + return listOf( + Handle(WidgetManagerView.EditMode.TOP, + Rect(HANDLE_EDGE_SIZE, 0, width - HANDLE_EDGE_SIZE, HANDLE_SIZE)), + Handle(WidgetManagerView.EditMode.BOTTOM, + Rect(HANDLE_EDGE_SIZE, height - HANDLE_SIZE, width - HANDLE_EDGE_SIZE, height)), + Handle(WidgetManagerView.EditMode.LEFT, + Rect(0, HANDLE_EDGE_SIZE, HANDLE_SIZE, height - HANDLE_EDGE_SIZE)), + Handle(WidgetManagerView.EditMode.RIGHT, + Rect(width - HANDLE_SIZE, HANDLE_EDGE_SIZE, width, height - HANDLE_EDGE_SIZE)) + ) + + } + + private fun getBounds(): Rect { + return Rect(0,0, width, height) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt new file mode 100644 index 0000000..3e9a2eb --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt @@ -0,0 +1,120 @@ +package de.jrpie.android.launcher.widgets; + +import android.app.Activity +import android.appwidget.AppWidgetHostView +import android.appwidget.AppWidgetProviderInfo +import android.content.Context +import android.graphics.drawable.Drawable +import android.os.Build +import android.os.Bundle +import android.util.DisplayMetrics +import android.util.SizeF +import android.view.View +import de.jrpie.android.launcher.Application +import de.jrpie.android.launcher.ui.HomeActivity +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@SerialName("widget:app") +class AppWidget( + override val id: Int, + override var position: WidgetPosition = WidgetPosition(0,0,1,1), + override var allowInteraction: Boolean = false, + + // We keep track of packageName, className and user to make it possible to restore the widget + // on a new device when restoring settings (currently not implemented) + // In normal operation only id and position are used. + val packageName: String? = null, + val className: String? = null, + val user: Int? = null +): Widget() { + + + constructor(id: Int, widgetProviderInfo: AppWidgetProviderInfo, position: WidgetPosition) : + this( + id, + position, + false, + widgetProviderInfo.provider.packageName, + widgetProviderInfo.provider.className, + widgetProviderInfo.profile.hashCode() + ) + + /** + * Get the [AppWidgetProviderInfo] by [id]. + * If the widget is not installed, use [restoreAppWidgetProviderInfo] instead. + */ + fun getAppWidgetProviderInfo(context: Context): AppWidgetProviderInfo? { + if (id < 0) { + return null + } + return (context.applicationContext as Application).appWidgetManager + .getAppWidgetInfo(id) + } + + /** + * Restore the AppWidgetProviderInfo from [user], [packageName] and [className]. + * Only use this when the widget is not installed, + * in normal operation use [getAppWidgetProviderInfo] instead. + */ + /*fun restoreAppWidgetProviderInfo(context: Context): AppWidgetProviderInfo? { + return getAppWidgetProviders(context).firstOrNull { + it.profile.hashCode() == user + && it.provider.packageName == packageName + && it.provider.className == className + } + }*/ + + override fun toString(): String { + return "WidgetInfo(id=$id, position=$position, packageName=$packageName, className=$className, user=$user)" + } + + override fun createView(activity: Activity): AppWidgetHostView? { + val providerInfo = activity.getAppWidgetManager().getAppWidgetInfo(id) ?: return null + val view = activity.getAppWidgetHost() + .createView(activity, this.id, providerInfo) + + val dp = activity.resources.displayMetrics.density + val screenWidth = activity.resources.displayMetrics.widthPixels + val screenHeight = activity.resources.displayMetrics.heightPixels + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val absolutePosition = position.getAbsoluteRect(screenWidth, screenHeight) + view.updateAppWidgetSize(Bundle.EMPTY, + listOf(SizeF( + absolutePosition.width() / dp, + absolutePosition.height() / dp + ))) + } + view.setPadding(0,0,0,0) + return view + } + + override fun findView(views: Sequence): AppWidgetHostView? { + return views.mapNotNull { it as? AppWidgetHostView }.firstOrNull { it.appWidgetId == id } + } + + override fun getIcon(context: Context): Drawable? { + return context.getAppWidgetManager().getAppWidgetInfo(id)?.loadIcon(context, DisplayMetrics.DENSITY_HIGH) + } + + override fun getPreview(context: Context): Drawable? { + return context.getAppWidgetManager().getAppWidgetInfo(id)?.loadPreviewImage(context, DisplayMetrics.DENSITY_HIGH) + } + + override fun isConfigurable(context: Context): Boolean { + return context.getAppWidgetManager().getAppWidgetInfo(id)?.configure != null + } + override fun configure(activity: Activity, requestCode: Int) { + if (!isConfigurable(activity)) { + return + } + activity.getAppWidgetHost().startAppWidgetConfigureActivityForResult( + activity, + id, + 0, + requestCode, + null + ) + } +} diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt new file mode 100644 index 0000000..d819538 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt @@ -0,0 +1,41 @@ +package de.jrpie.android.launcher.widgets + +import android.app.Activity +import android.content.Context +import android.graphics.drawable.Drawable +import android.view.View +import de.jrpie.android.launcher.ui.widgets.ClockView +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + + +@Serializable +@SerialName("widget:clock") +class ClockWidget( + override val id: Int, + override var position: WidgetPosition, + override var allowInteraction: Boolean = true +) : Widget() { + + override fun createView(activity: Activity): View? { + return ClockView(activity, null, id) + } + + override fun findView(views: Sequence): ClockView? { + return views.mapNotNull { it as? ClockView }.firstOrNull { it.appWidgetId == id } + } + + override fun getPreview(context: Context): Drawable? { + return null + } + + override fun getIcon(context: Context): Drawable? { + return null + } + + override fun isConfigurable(context: Context): Boolean { + return false + } + + override fun configure(activity: Activity, requestCode: Int) { } +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/LauncherWidgetProvider.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/LauncherWidgetProvider.kt new file mode 100644 index 0000000..018b29b --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/LauncherWidgetProvider.kt @@ -0,0 +1,58 @@ +package de.jrpie.android.launcher.widgets + +import android.appwidget.AppWidgetProviderInfo +import android.content.Context +import android.graphics.drawable.Drawable +import android.os.Build +import android.util.DisplayMetrics +import androidx.appcompat.content.res.AppCompatResources +import de.jrpie.android.launcher.R + +sealed class LauncherWidgetProvider { + abstract fun loadLabel(context: Context): CharSequence? + abstract fun loadPreviewImage(context: Context): Drawable? + abstract fun loadIcon(context: Context): Drawable? + abstract fun loadDescription(context: Context): CharSequence? +} + +class LauncherAppWidgetProvider(val info: AppWidgetProviderInfo) : LauncherWidgetProvider() { + + override fun loadLabel(context: Context): CharSequence? { + return info.loadLabel(context.packageManager) + } + override fun loadPreviewImage(context: Context): Drawable? { + return info.loadPreviewImage(context, DisplayMetrics.DENSITY_DEFAULT) + } + + override fun loadIcon(context: Context): Drawable? { + return info.loadIcon(context, DisplayMetrics.DENSITY_DEFAULT) + } + + override fun loadDescription(context: Context): CharSequence? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + info.loadDescription(context) + } else { + null + } + } + +} +class LauncherClockWidgetProvider : LauncherWidgetProvider() { + + override fun loadLabel(context: Context): CharSequence? { + return context.getString(R.string.widget_clock_label) + } + + override fun loadDescription(context: Context): CharSequence? { + return context.getString(R.string.widget_clock_description) + } + + override fun loadPreviewImage(context: Context): Drawable? { + return null + } + + override fun loadIcon(context: Context): Drawable? { + return AppCompatResources.getDrawable(context, R.drawable.baseline_clock_24) + } +} + diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt new file mode 100644 index 0000000..d3610dd --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt @@ -0,0 +1,60 @@ +package de.jrpie.android.launcher.widgets + +import android.app.Activity +import android.content.Context +import android.graphics.drawable.Drawable +import android.view.View +import de.jrpie.android.launcher.Application +import de.jrpie.android.launcher.preferences.LauncherPreferences +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + + +@Serializable +sealed class Widget { + abstract val id: Int + abstract var position: WidgetPosition + abstract var allowInteraction: Boolean + + /** + * @param activity The activity where the view will be used. Must not be an AppCompatActivity. + */ + abstract fun createView(activity: Activity): View? + abstract fun findView(views: Sequence): View? + abstract fun getPreview(context: Context): Drawable? + abstract fun getIcon(context: Context): Drawable? + abstract fun isConfigurable(context: Context): Boolean + abstract fun configure(activity: Activity, requestCode: Int) + + fun delete(context: Context) { + context.getAppWidgetHost().deleteAppWidgetId(id) + + LauncherPreferences.widgets().widgets( + LauncherPreferences.widgets().widgets()?.also { + it.remove(this) + } + ) + } + + override fun hashCode(): Int { + return id + } + + override fun equals(other: Any?): Boolean { + return (other as? Widget)?.id == id + } + + fun serialize(): String { + return Json.encodeToString(serializer(), this) + } + companion object { + fun deserialize(serialized: String): Widget { + return Json.decodeFromString(serialized) + } + fun byId(context: Context, id: Int): Widget? { + return (context.applicationContext as Application).widgets.value?.firstOrNull { + it.id == id + } + } + } +} diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetPosition.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetPosition.kt new file mode 100644 index 0000000..b575665 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetPosition.kt @@ -0,0 +1,58 @@ +package de.jrpie.android.launcher.widgets + +import android.graphics.Rect +import kotlinx.serialization.Serializable +import kotlin.math.ceil +import kotlin.math.roundToInt +import kotlin.math.max + +const val GRID_SIZE: Short = 12 + +@Serializable +data class WidgetPosition(var x: Short, var y: Short, var width: Short, var height: Short) { + + fun getAbsoluteRect(screenWidth: Int, screenHeight: Int): Rect { + val gridWidth = screenWidth / GRID_SIZE.toFloat() + val gridHeight= screenHeight / GRID_SIZE.toFloat() + + return Rect( + (x * gridWidth).toInt(), + (y * gridHeight).toInt(), + ((x + width) * gridWidth).toInt(), + ((y + height) * gridHeight).toInt() + ) + } + + companion object { + fun fromAbsoluteRect(absolute: Rect, screenWidth: Int, screenHeight: Int): WidgetPosition { + val gridWidth = screenWidth / GRID_SIZE.toFloat() + val gridHeight= screenHeight / GRID_SIZE.toFloat() + + val x = (absolute.left / gridWidth).roundToInt().toShort().coerceIn(0, (GRID_SIZE-1).toShort()) + val y = (absolute.top / gridHeight).roundToInt().toShort().coerceIn(0, (GRID_SIZE-1).toShort()) + + + val w = max(2, ((absolute.right - absolute.left) / gridWidth).roundToInt()).toShort() + val h = max(2, ((absolute.bottom - absolute.top) / gridHeight).roundToInt()).toShort() + + return WidgetPosition(x,y,w,h) + + } + + fun center(minWidth: Int, minHeight: Int, screenWidth: Int, screenHeight: Int): WidgetPosition { + val gridWidth = screenWidth / GRID_SIZE.toFloat() + val gridHeight= screenHeight / GRID_SIZE.toFloat() + + val cellsWidth = ceil(minWidth / gridWidth).toInt().toShort() + val cellsHeight = ceil(minHeight / gridHeight).toInt().toShort() + + return WidgetPosition( + ((GRID_SIZE - cellsWidth) / 2).toShort(), + ((GRID_SIZE - cellsHeight) / 2).toShort(), + cellsWidth, + cellsHeight + ) + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt new file mode 100644 index 0000000..cd4ef29 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt @@ -0,0 +1,87 @@ +package de.jrpie.android.launcher.widgets + +import android.app.Activity +import android.app.Service +import android.appwidget.AppWidgetHost +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProviderInfo +import android.content.Context +import android.content.Intent +import android.content.pm.LauncherApps +import android.os.Build +import android.os.UserManager +import android.util.Log +import de.jrpie.android.launcher.Application +import de.jrpie.android.launcher.preferences.LauncherPreferences + +fun deleteAllWidgets(context: Context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.getAppWidgetHost().appWidgetIds.forEach { AppWidget(it).delete(context) } + } +} + +/** + * Tries to bind [providerInfo] to the id [id]. + * @param providerInfo The widget to be bound. + * @param id The id to bind the widget to. If -1 is provided, a new id is allocated. + * @param + * @param requestCode Used to start an activity to request permission to bind the widget. + * + * @return true iff the app widget was bound successfully. + */ +fun bindAppWidgetOrRequestPermission(activity: Activity, providerInfo: AppWidgetProviderInfo, id: Int, requestCode: Int? = null): Boolean { + val appWidgetId = if(id == -1) { + activity.getAppWidgetHost().allocateAppWidgetId() + } else { id } + + Log.i("Launcher", "Binding new widget ${appWidgetId}") + if (!activity.getAppWidgetManager().bindAppWidgetIdIfAllowed( + appWidgetId, + providerInfo.provider + ) + ) { + Log.i("Widgets", "requesting permission for widget") + val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_BIND).apply { + putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,appWidgetId) + putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, providerInfo.provider) + } + activity.startActivityForResult(intent, requestCode ?: 0) + return false + } + return true +} + + +fun getAppWidgetProviders( context: Context ): List { + val list = mutableListOf(LauncherClockWidgetProvider()) + val appWidgetManager = context.getAppWidgetManager() + val profiles = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + (context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps).profiles + } else { + (context.getSystemService(Service.USER_SERVICE) as UserManager).userProfiles + } + list.addAll( + profiles.map { + appWidgetManager.getInstalledProvidersForProfile(it) + .map { LauncherAppWidgetProvider(it) } + }.flatten() + ) + + + return list +} + + +fun updateWidget(widget: Widget) { + var widgets = LauncherPreferences.widgets().widgets() ?: setOf() + widgets = widgets.minus(widget).plus(widget) + LauncherPreferences.widgets().widgets(widgets) +} + +fun Context.getAppWidgetHost(): AppWidgetHost { + return (this.applicationContext as Application).appWidgetHost +} +fun Context.getAppWidgetManager(): AppWidgetManager { + return (this.applicationContext as Application).appWidgetManager +} diff --git a/app/src/main/res/drawable/baseline_add_24.xml b/app/src/main/res/drawable/baseline_add_24.xml new file mode 100644 index 0000000..13267ce --- /dev/null +++ b/app/src/main/res/drawable/baseline_add_24.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_clock_24.xml b/app/src/main/res/drawable/baseline_clock_24.xml new file mode 100644 index 0000000..7968998 --- /dev/null +++ b/app/src/main/res/drawable/baseline_clock_24.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/app/src/main/res/layout/activity_manage_widgets.xml b/app/src/main/res/layout/activity_manage_widgets.xml new file mode 100644 index 0000000..c77f0e3 --- /dev/null +++ b/app/src/main/res/layout/activity_manage_widgets.xml @@ -0,0 +1,25 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_select_widget.xml b/app/src/main/res/layout/activity_select_widget.xml new file mode 100644 index 0000000..82db94d --- /dev/null +++ b/app/src/main/res/layout/activity_select_widget.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/clock.xml b/app/src/main/res/layout/clock.xml new file mode 100644 index 0000000..d81fc5f --- /dev/null +++ b/app/src/main/res/layout/clock.xml @@ -0,0 +1,35 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/home.xml b/app/src/main/res/layout/home.xml index ecefdea..717151f 100644 --- a/app/src/main/res/layout/home.xml +++ b/app/src/main/res/layout/home.xml @@ -10,29 +10,10 @@ android:fitsSystemWindows="true" tools:context=".ui.HomeActivity"> - - - + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_widgets_row.xml b/app/src/main/res/layout/list_widgets_row.xml new file mode 100644 index 0000000..878aaad --- /dev/null +++ b/app/src/main/res/layout/list_widgets_row.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 1e4d12b..2712036 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -11,5 +11,9 @@ #fff #9999ff #000 + #FF29B6F6 + #FF039BE5 + #FFBDBDBD + #FF757575 diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 69c7f6a..30e4cda 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -9,6 +9,7 @@ internal.started_before internal.first_startup internal.version_code + widgets.widgets apps.favorites apps.hidden apps.pinned_shortcuts diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ed2bb66..a8813f8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -98,6 +98,8 @@ Time Click on time + Manage widgets + Choose App @@ -388,5 +390,15 @@ Open Source Licenses No app found to handle search. Can\'t open URL: no browser found. + Choose Widget + + Remove + Configure + Enable Interaction + Disable Interaction + + + Clock + The default clock of μLauncher diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 20ccb67..c5b7252 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -66,12 +66,12 @@ 0 2 + - - + + + + + + diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 7d906ff..b4bc5f0 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -6,6 +6,10 @@ + + Date: Fri, 25 Apr 2025 11:25:00 +0200 Subject: [PATCH 030/118] fix #145 --- app/src/main/res/layout/list_apps_row_variant_grid.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/layout/list_apps_row_variant_grid.xml b/app/src/main/res/layout/list_apps_row_variant_grid.xml index ee57c45..f63d724 100644 --- a/app/src/main/res/layout/list_apps_row_variant_grid.xml +++ b/app/src/main/res/layout/list_apps_row_variant_grid.xml @@ -27,6 +27,8 @@ android:text="" android:textSize="11sp" tools:text="@string/app_name" + android:ellipsize="end" + android:lines="1" app:layout_constraintTop_toBottomOf="@id/list_apps_row_icon" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" From a4fcdf60c7d3be3d37be15fb3fc8c9b286a7f953 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sat, 26 Apr 2025 21:52:21 +0200 Subject: [PATCH 031/118] add widget panels (see #44) --- app/src/main/AndroidManifest.xml | 10 +- .../jrpie/android/launcher/actions/Action.kt | 10 +- .../launcher/actions/WidgetPanelAction.kt | 83 ++++++++++++++ .../LauncherPreferences$Config.java | 4 +- .../launcher/preferences/Preferences.kt | 24 +++- .../launcher/preferences/legacy/Version1.kt | 9 +- .../launcher/preferences/legacy/Version2.kt | 5 +- .../launcher/preferences/legacy/Version3.kt | 10 +- .../launcher/preferences/legacy/Version4.kt | 27 +++++ .../preferences/legacy/VersionUnknown.kt | 4 +- .../serialization/PreferenceSerializers.kt | 17 +++ .../jrpie/android/launcher/ui/HomeActivity.kt | 1 - .../ui/list/other/OtherRecyclerAdapter.kt | 19 ++-- .../launcher/SettingsFragmentLauncher.kt | 9 ++ .../ui/widgets/WidgetContainerView.kt | 31 ++++-- .../ui/widgets/WidgetPanelActivity.kt | 50 +++++++++ .../manage/ManageWidgetPanelsActivity.kt | 104 ++++++++++++++++++ .../widgets/manage/ManageWidgetsActivity.kt | 24 +++- .../ui/widgets/manage/SelectWidgetActivity.kt | 6 +- .../ui/widgets/manage/WidgetManagerView.kt | 11 +- .../ui/widgets/manage/WidgetOverlayView.kt | 7 +- .../manage/WidgetPanelsRecyclerAdapter.kt | 98 +++++++++++++++++ .../android/launcher/widgets/AppWidget.kt | 12 +- .../android/launcher/widgets/ClockWidget.kt | 1 + .../jrpie/android/launcher/widgets/Widget.kt | 5 + .../android/launcher/widgets/WidgetPanel.kt | 58 ++++++++++ .../jrpie/android/launcher/widgets/Widgets.kt | 16 ++- .../main/res/drawable/baseline_widgets_24.xml | 11 ++ .../layout/activity_manage_widget_panels.xml | 73 ++++++++++++ .../main/res/layout/activity_widget_panel.xml | 15 +++ .../res/layout/dialog_create_widget_panel.xml | 18 +++ .../res/layout/dialog_rename_widget_panel.xml | 18 +++ .../res/layout/dialog_select_widget_panel.xml | 26 +++++ .../res/layout/list_widget_panels_row.xml | 23 ++++ app/src/main/res/values/donottranslate.xml | 1 + app/src/main/res/values/strings.xml | 19 ++++ app/src/main/res/xml/preferences.xml | 16 ++- 37 files changed, 807 insertions(+), 68 deletions(-) create mode 100644 app/src/main/java/de/jrpie/android/launcher/actions/WidgetPanelAction.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version4.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetPanelActivity.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetPanelsActivity.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetPanelsRecyclerAdapter.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/WidgetPanel.kt create mode 100644 app/src/main/res/drawable/baseline_widgets_24.xml create mode 100644 app/src/main/res/layout/activity_manage_widget_panels.xml create mode 100644 app/src/main/res/layout/activity_widget_panel.xml create mode 100644 app/src/main/res/layout/dialog_create_widget_panel.xml create mode 100644 app/src/main/res/layout/dialog_rename_widget_panel.xml create mode 100644 app/src/main/res/layout/dialog_select_widget_panel.xml create mode 100644 app/src/main/res/layout/list_widget_panels_row.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 841c9bd..5a1d5a0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,9 +21,15 @@ android:theme="@style/launcherBaseTheme" tools:ignore="UnusedAttribute"> + + diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/Action.kt b/app/src/main/java/de/jrpie/android/launcher/actions/Action.kt index 9a2dc62..a883922 100644 --- a/app/src/main/java/de/jrpie/android/launcher/actions/Action.kt +++ b/app/src/main/java/de/jrpie/android/launcher/actions/Action.kt @@ -6,14 +6,18 @@ import android.content.SharedPreferences.Editor import android.graphics.Rect import android.graphics.drawable.Drawable import android.widget.Toast +import androidx.core.content.edit import de.jrpie.android.launcher.R import de.jrpie.android.launcher.preferences.LauncherPreferences import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import androidx.core.content.edit +/** + * Represents an action that can be bound to a [Gesture]. + * There are four types of actions: [AppAction], [ShortcutAction], [LauncherAction] and [WidgetPanelAction] + */ @Serializable sealed interface Action { fun invoke(context: Context, rect: Rect? = null): Boolean @@ -21,6 +25,10 @@ sealed interface Action { fun getIcon(context: Context): Drawable? fun isAvailable(context: Context): Boolean + fun showConfigurationDialog(context: Context, onSuccess: (Action) -> Unit) { + onSuccess(this) + } + // Can the action be used to reach µLauncher settings? fun canReachSettings(): Boolean diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/WidgetPanelAction.kt b/app/src/main/java/de/jrpie/android/launcher/actions/WidgetPanelAction.kt new file mode 100644 index 0000000..d7829a6 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/actions/WidgetPanelAction.kt @@ -0,0 +1,83 @@ +package de.jrpie.android.launcher.actions + +import android.content.Context +import android.content.Intent +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.view.View +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.core.content.res.ResourcesCompat +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import de.jrpie.android.launcher.R +import de.jrpie.android.launcher.ui.widgets.WidgetPanelActivity +import de.jrpie.android.launcher.ui.widgets.manage.EXTRA_PANEL_ID +import de.jrpie.android.launcher.ui.widgets.manage.WidgetPanelsRecyclerAdapter +import de.jrpie.android.launcher.widgets.WidgetPanel +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@SerialName("action:panel") +class WidgetPanelAction(val widgetPanelId: Int) : Action { + + override fun invoke(context: Context, rect: Rect?): Boolean { + + if (WidgetPanel.byId(widgetPanelId) == null) { + Toast.makeText(context, R.string.alert_widget_panel_not_found, Toast.LENGTH_LONG).show() + } else { + context.startActivity(Intent(context, WidgetPanelActivity::class.java).also { + it.putExtra(EXTRA_PANEL_ID, widgetPanelId) + }) + } + return true + } + + override fun label(context: Context): String { + return WidgetPanel.byId(widgetPanelId)?.label + ?: context.getString(R.string.list_other_open_widget_panel) + } + + override fun isAvailable(context: Context): Boolean { + return true + } + + override fun canReachSettings(): Boolean { + return false + } + + override fun getIcon(context: Context): Drawable? { + return ResourcesCompat.getDrawable( + context.resources, + R.drawable.baseline_widgets_24, + context.theme + ) + } + + override fun showConfigurationDialog(context: Context, onSuccess: (Action) -> Unit) { + AlertDialog.Builder(context, R.style.AlertDialogCustom).apply { + setTitle(R.string.dialog_select_widget_panel_title) + setNegativeButton(R.string.dialog_cancel) { _, _ -> } + setView(R.layout.dialog_select_widget_panel) + }.create().also { it.show() }.also { alertDialog -> + val infoTextView = + alertDialog.findViewById(R.id.dialog_select_widget_panel_info) + alertDialog.findViewById(R.id.dialog_select_widget_panel_recycler) + ?.apply { + setHasFixedSize(true) + layoutManager = LinearLayoutManager(alertDialog.context) + adapter = + WidgetPanelsRecyclerAdapter(alertDialog.context, false) { widgetPanel -> + onSuccess(WidgetPanelAction(widgetPanel.id)) + alertDialog.dismiss() + } + if (adapter?.itemCount == 0) { + infoTextView?.visibility = View.VISIBLE + } + } + } + true + } +} diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java b/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java index 575346a..d509ef2 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java @@ -8,6 +8,7 @@ import de.jrpie.android.launcher.actions.lock.LockMethod; import de.jrpie.android.launcher.preferences.serialization.MapAbstractAppInfoStringPreferenceSerializer; import de.jrpie.android.launcher.preferences.serialization.SetAbstractAppInfoPreferenceSerializer; import de.jrpie.android.launcher.preferences.serialization.SetPinnedShortcutInfoPreferenceSerializer; +import de.jrpie.android.launcher.preferences.serialization.SetWidgetPanelSerializer; import de.jrpie.android.launcher.preferences.serialization.SetWidgetSerializer; import de.jrpie.android.launcher.preferences.theme.Background; import de.jrpie.android.launcher.preferences.theme.ColorTheme; @@ -84,7 +85,8 @@ import eu.jonahbauer.android.preference.annotations.Preferences; @Preference(name = "lock_method", type = LockMethod.class, defaultValue = "DEVICE_ADMIN"), }), @PreferenceGroup(name = "widgets", prefix = "settings_widgets_", suffix= "_key", value = { - @Preference(name = "widgets", type = Set.class, serializer = SetWidgetSerializer.class) + @Preference(name = "widgets", type = Set.class, serializer = SetWidgetSerializer.class), + @Preference(name = "custom_panels", type = Set.class, serializer = SetWidgetPanelSerializer.class) }), }) public final class LauncherPreferences$Config {} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt index 332f4df..e8e717e 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt @@ -2,18 +2,21 @@ package de.jrpie.android.launcher.preferences import android.content.Context import android.util.Log +import de.jrpie.android.launcher.Application import de.jrpie.android.launcher.BuildConfig import de.jrpie.android.launcher.actions.Action -import de.jrpie.android.launcher.apps.AppInfo import de.jrpie.android.launcher.apps.AbstractAppInfo import de.jrpie.android.launcher.apps.AbstractAppInfo.Companion.INVALID_USER +import de.jrpie.android.launcher.apps.AppInfo import de.jrpie.android.launcher.apps.DetailedAppInfo import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion1 import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion2 import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion3 +import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion4 import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersionUnknown import de.jrpie.android.launcher.ui.HomeActivity import de.jrpie.android.launcher.widgets.ClockWidget +import de.jrpie.android.launcher.widgets.WidgetPanel import de.jrpie.android.launcher.widgets.WidgetPosition import de.jrpie.android.launcher.widgets.deleteAllWidgets @@ -21,7 +24,7 @@ import de.jrpie.android.launcher.widgets.deleteAllWidgets * Increase when breaking changes are introduced and write an appropriate case in * `migratePreferencesToNewVersion` */ -const val PREFERENCE_VERSION = 4 +const val PREFERENCE_VERSION = 5 const val UNKNOWN_PREFERENCE_VERSION = -1 private const val TAG = "Launcher - Preferences" @@ -43,18 +46,23 @@ fun migratePreferencesToNewVersion(context: Context) { } 1 -> { - migratePreferencesFromVersion1() + migratePreferencesFromVersion1(context) Log.i(TAG, "migration of preferences complete (1 -> ${PREFERENCE_VERSION}).") } 2 -> { - migratePreferencesFromVersion2() + migratePreferencesFromVersion2(context) Log.i(TAG, "migration of preferences complete (2 -> ${PREFERENCE_VERSION}).") } 3 -> { - migratePreferencesFromVersion3() + migratePreferencesFromVersion3(context) Log.i(TAG, "migration of preferences complete (3 -> ${PREFERENCE_VERSION}).") } + 4 -> { + migratePreferencesFromVersion4(context) + Log.i(TAG, "migration of preferences complete (4 -> ${PREFERENCE_VERSION}).") + } + else -> { Log.w( TAG, @@ -78,7 +86,11 @@ fun resetPreferences(context: Context) { LauncherPreferences.widgets().widgets( setOf( - ClockWidget(-500, WidgetPosition(1,4,10,3)) + ClockWidget( + (context.applicationContext as Application).appWidgetHost.allocateAppWidgetId(), + WidgetPosition(1, 3, 10, 4), + WidgetPanel.HOME.id + ) ) ) diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version1.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version1.kt index a1cb022..6cd9819 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version1.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version1.kt @@ -1,11 +1,13 @@ package de.jrpie.android.launcher.preferences.legacy +import android.content.Context +import androidx.core.content.edit import de.jrpie.android.launcher.actions.Action import de.jrpie.android.launcher.actions.AppAction import de.jrpie.android.launcher.actions.Gesture import de.jrpie.android.launcher.actions.LauncherAction -import de.jrpie.android.launcher.apps.AppInfo import de.jrpie.android.launcher.apps.AbstractAppInfo.Companion.INVALID_USER +import de.jrpie.android.launcher.apps.AppInfo import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION import kotlinx.serialization.Serializable @@ -13,7 +15,6 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import org.json.JSONException import org.json.JSONObject -import androidx.core.content.edit @Serializable @@ -129,7 +130,7 @@ private fun migrateAction(key: String) { * Migrate preferences from version 1 (used until version j-0.0.18) to the current format * (see [PREFERENCE_VERSION]) */ -fun migratePreferencesFromVersion1() { +fun migratePreferencesFromVersion1(context: Context) { assert(LauncherPreferences.internal().versionCode() == 1) Gesture.entries.forEach { g -> migrateAction(g.id) } migrateAppInfoSet(LauncherPreferences.apps().keys().hidden()) @@ -137,5 +138,5 @@ fun migratePreferencesFromVersion1() { migrateAppInfoStringMap(LauncherPreferences.apps().keys().customNames()) LauncherPreferences.internal().versionCode(2) - migratePreferencesFromVersion2() + migratePreferencesFromVersion2(context) } \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version2.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version2.kt index 4e6eae1..9714359 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version2.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version2.kt @@ -1,5 +1,6 @@ package de.jrpie.android.launcher.preferences.legacy +import android.content.Context import de.jrpie.android.launcher.actions.Action import de.jrpie.android.launcher.actions.Gesture import de.jrpie.android.launcher.actions.LauncherAction @@ -11,10 +12,10 @@ import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION * Migrate preferences from version 2 (used until version 0.0.21) to the current format * (see [PREFERENCE_VERSION]) */ -fun migratePreferencesFromVersion2() { +fun migratePreferencesFromVersion2(context: Context) { assert(LauncherPreferences.internal().versionCode() == 2) // previously there was no setting for this Action.setActionForGesture(Gesture.BACK, LauncherAction.CHOOSE) LauncherPreferences.internal().versionCode(3) - migratePreferencesFromVersion3() + migratePreferencesFromVersion3(context) } \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version3.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version3.kt index 4a9241f..e0a8447 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version3.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version3.kt @@ -1,17 +1,17 @@ package de.jrpie.android.launcher.preferences.legacy +import android.content.Context import android.content.SharedPreferences import android.content.SharedPreferences.Editor -import de.jrpie.android.launcher.apps.AppInfo +import androidx.core.content.edit import de.jrpie.android.launcher.apps.AbstractAppInfo +import de.jrpie.android.launcher.apps.AppInfo import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION import de.jrpie.android.launcher.preferences.serialization.MapAbstractAppInfoStringPreferenceSerializer import de.jrpie.android.launcher.preferences.serialization.SetAbstractAppInfoPreferenceSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json -import java.util.HashSet -import androidx.core.content.edit /** * Migrate preferences from version 3 (used until version 0.0.23) to the current format @@ -70,8 +70,7 @@ private fun migrateMapAppInfoString(key: String, preferences: SharedPreferences, } } -fun migratePreferencesFromVersion3() { - assert(PREFERENCE_VERSION == 4) +fun migratePreferencesFromVersion3(context: Context) { assert(LauncherPreferences.internal().versionCode() == 3) val preferences = LauncherPreferences.getSharedPreferences() @@ -82,4 +81,5 @@ fun migratePreferencesFromVersion3() { } LauncherPreferences.internal().versionCode(4) + migratePreferencesFromVersion4(context) } \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version4.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version4.kt new file mode 100644 index 0000000..d4c7441 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version4.kt @@ -0,0 +1,27 @@ +package de.jrpie.android.launcher.preferences.legacy + +import android.content.Context +import de.jrpie.android.launcher.Application +import de.jrpie.android.launcher.preferences.LauncherPreferences +import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION +import de.jrpie.android.launcher.widgets.ClockWidget +import de.jrpie.android.launcher.widgets.WidgetPanel +import de.jrpie.android.launcher.widgets.WidgetPosition + +fun migratePreferencesFromVersion4(context: Context) { + assert(PREFERENCE_VERSION == 5) + assert(LauncherPreferences.internal().versionCode() == 4) + + LauncherPreferences.widgets().widgets( + setOf( + ClockWidget( + (context.applicationContext as Application).appWidgetHost.allocateAppWidgetId(), + WidgetPosition(1, 3, 10, 4), + WidgetPanel.HOME.id + ) + ) + ) + + + LauncherPreferences.internal().versionCode(5) +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/VersionUnknown.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/VersionUnknown.kt index 2d1152d..f954b31 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/VersionUnknown.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/VersionUnknown.kt @@ -3,10 +3,10 @@ package de.jrpie.android.launcher.preferences.legacy import android.content.Context import android.content.SharedPreferences import android.util.Log +import androidx.core.content.edit import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.theme.Background import de.jrpie.android.launcher.preferences.theme.ColorTheme -import androidx.core.content.edit private fun migrateStringPreference( @@ -392,5 +392,5 @@ fun migratePreferencesFromVersionUnknown(context: Context) { LauncherPreferences.internal().versionCode(1) Log.i(TAG, "migrated preferences to version 1.") - migratePreferencesFromVersion1() + migratePreferencesFromVersion1(context) } \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt index a2749ae..7b5d794 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt @@ -5,6 +5,7 @@ package de.jrpie.android.launcher.preferences.serialization import de.jrpie.android.launcher.apps.AbstractAppInfo import de.jrpie.android.launcher.apps.PinnedShortcutInfo import de.jrpie.android.launcher.widgets.Widget +import de.jrpie.android.launcher.widgets.WidgetPanel import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializationException import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializer import kotlinx.serialization.Serializable @@ -46,6 +47,22 @@ class SetWidgetSerializer : } } +@Suppress("UNCHECKED_CAST") +class SetWidgetPanelSerializer : + PreferenceSerializer?, java.util.Set?> { + @Throws(PreferenceSerializationException::class) + override fun serialize(value: java.util.Set?): java.util.Set? { + return value?.map(WidgetPanel::serialize) + ?.toHashSet() as? java.util.Set + } + + @Throws(PreferenceSerializationException::class) + override fun deserialize(value: java.util.Set?): java.util.Set? { + return value?.map(java.lang.String::toString)?.map(WidgetPanel::deserialize) + ?.toHashSet() as? java.util.Set + } +} + @Suppress("UNCHECKED_CAST") class SetPinnedShortcutInfoPreferenceSerializer : diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt index 3f1e497..192a8e9 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt @@ -207,7 +207,6 @@ class HomeActivity : UIObject, Activity() { } override fun onTouchEvent(event: MotionEvent): Boolean { - android.util.Log.e("Launcher", "on touch") touchGestureDetector?.onTouchEvent(event) return true } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/list/other/OtherRecyclerAdapter.kt b/app/src/main/java/de/jrpie/android/launcher/ui/list/other/OtherRecyclerAdapter.kt index f176469..06be78a 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/list/other/OtherRecyclerAdapter.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/list/other/OtherRecyclerAdapter.kt @@ -11,6 +11,7 @@ import de.jrpie.android.launcher.R import de.jrpie.android.launcher.actions.Action import de.jrpie.android.launcher.actions.Gesture import de.jrpie.android.launcher.actions.LauncherAction +import de.jrpie.android.launcher.actions.WidgetPanelAction import de.jrpie.android.launcher.ui.list.ListActivity /** @@ -23,8 +24,10 @@ import de.jrpie.android.launcher.ui.list.ListActivity class OtherRecyclerAdapter(val activity: Activity) : RecyclerView.Adapter() { - private val othersList: Array = - LauncherAction.entries.filter { it.isAvailable(activity) }.toTypedArray() + private val othersList: Array = + LauncherAction.entries.filter { it.isAvailable(activity) } + .plus(WidgetPanelAction(-1)) + .toTypedArray() inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener { @@ -36,10 +39,12 @@ class OtherRecyclerAdapter(val activity: Activity) : val pos = bindingAdapterPosition val content = othersList[pos] - activity.finish() val gestureId = (activity as? ListActivity)?.forGesture ?: return val gesture = Gesture.byId(gestureId) ?: return - Action.setActionForGesture(gesture, content) + content.showConfigurationDialog(activity) { configuredAction -> + Action.setActionForGesture(gesture, configuredAction) + activity.finish() + } } init { @@ -48,11 +53,11 @@ class OtherRecyclerAdapter(val activity: Activity) : } override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) { - val otherLabel = activity.getString(othersList[i].label) - val icon = othersList[i].icon + val otherLabel = othersList[i].label(activity) + val icon = othersList[i].getIcon(activity) viewHolder.textView.text = otherLabel - viewHolder.iconView.setImageResource(icon) + viewHolder.iconView.setImageDrawable(icon) } override fun getItemCount(): Int { diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/settings/launcher/SettingsFragmentLauncher.kt b/app/src/main/java/de/jrpie/android/launcher/ui/settings/launcher/SettingsFragmentLauncher.kt index 8907f04..bb9df74 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/settings/launcher/SettingsFragmentLauncher.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/settings/launcher/SettingsFragmentLauncher.kt @@ -11,6 +11,7 @@ import de.jrpie.android.launcher.actions.openAppsList import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.theme.ColorTheme import de.jrpie.android.launcher.setDefaultHomeScreen +import de.jrpie.android.launcher.ui.widgets.manage.ManageWidgetPanelsActivity import de.jrpie.android.launcher.ui.widgets.manage.ManageWidgetsActivity @@ -90,6 +91,14 @@ class SettingsFragmentLauncher : PreferenceFragmentCompat() { true } + val manageWidgetPanels = findPreference( + LauncherPreferences.widgets().keys().customPanels() + ) + manageWidgetPanels?.setOnPreferenceClickListener { + startActivity(Intent(requireActivity(), ManageWidgetPanelsActivity::class.java)) + true + } + val hiddenApps = findPreference( LauncherPreferences.apps().keys().hidden() ) diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt index 04668ca..d071771 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt @@ -13,6 +13,7 @@ import android.view.ViewGroup import androidx.core.graphics.contains import androidx.core.view.size import de.jrpie.android.launcher.widgets.Widget +import de.jrpie.android.launcher.widgets.WidgetPanel import de.jrpie.android.launcher.widgets.WidgetPosition import kotlin.math.max @@ -20,22 +21,29 @@ import kotlin.math.max /** * This only works in an Activity, not AppCompatActivity */ -open class WidgetContainerView(context: Context, attrs: AttributeSet? = null) : ViewGroup(context, attrs) { +open class WidgetContainerView( + var widgetPanelId: Int, + context: Context, + attrs: AttributeSet? = null +) : ViewGroup(context, attrs) { + constructor(context: Context, attrs: AttributeSet) : this(WidgetPanel.HOME.id, context, attrs) var widgetViewById = HashMap() - open fun updateWidgets(activity: Activity, widgets: Set?) { - if (widgets == null) { - return - } - Log.i("WidgetContainer", "updating ${activity.localClassName}") - widgetViewById.clear() - (0.. + open fun updateWidgets(activity: Activity, widgets: Collection?) { + synchronized(widgetViewById) { + if (widgets == null) { + return + } + Log.i("WidgetContainer", "updating ${activity.localClassName}") + widgetViewById.forEach { removeView(it.value) } + widgetViewById.clear() + widgets.filter { it.panelId == widgetPanelId }.forEach { widget -> widget.createView(activity)?.let { - addView(it, WidgetContainerView.Companion.LayoutParams(widget.position)) + addView(it, LayoutParams(widget.position)) widgetViewById.put(widget.id, it) } + } } } @@ -67,7 +75,6 @@ open class WidgetContainerView(context: Context, attrs: AttributeSet? = null) : (0...onCreate(savedInstanceState) + super.onCreate() + widgetPanelId = intent.getIntExtra(EXTRA_PANEL_ID, WidgetPanel.Companion.HOME.id) + val binding = ActivityWidgetPanelBinding.inflate(layoutInflater) + setContentView(binding.root) + binding.widgetPanelWidgetContainer.widgetPanelId = widgetPanelId + binding.widgetPanelWidgetContainer.updateWidgets( + this, + LauncherPreferences.widgets().widgets() + ) + } + + override fun getTheme(): Resources.Theme { + val mTheme = modifyTheme(super.getTheme()) + mTheme.applyStyle(R.style.backgroundWallpaper, true) + LauncherPreferences.clock().font().applyToTheme(mTheme) + LauncherPreferences.theme().colorTheme().applyToTheme( + mTheme, + LauncherPreferences.theme().textShadow() + ) + + return mTheme + } + + + override fun onStart() { + super.onStart() + super.onStart() + } + + override fun isHomeScreen(): Boolean { + return true + } +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetPanelsActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetPanelsActivity.kt new file mode 100644 index 0000000..b18852f --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetPanelsActivity.kt @@ -0,0 +1,104 @@ +package de.jrpie.android.launcher.ui.widgets.manage + +import android.annotation.SuppressLint +import android.content.Intent +import android.content.SharedPreferences +import android.content.res.Resources +import android.os.Bundle +import android.widget.EditText +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.LinearLayoutManager +import de.jrpie.android.launcher.R +import de.jrpie.android.launcher.databinding.ActivityManageWidgetPanelsBinding +import de.jrpie.android.launcher.preferences.LauncherPreferences +import de.jrpie.android.launcher.ui.UIObject +import de.jrpie.android.launcher.widgets.WidgetPanel +import de.jrpie.android.launcher.widgets.updateWidgetPanel + +class ManageWidgetPanelsActivity : AppCompatActivity(), UIObject { + + private val sharedPreferencesListener = + SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey -> + if (prefKey == LauncherPreferences.widgets().keys().customPanels()) { + viewAdapter.widgetPanels = + (LauncherPreferences.widgets().customPanels() ?: setOf()).toTypedArray() + + @SuppressLint("NotifyDataSetChanged") + viewAdapter.notifyDataSetChanged() + } + } + private lateinit var binding: ActivityManageWidgetPanelsBinding + private lateinit var viewAdapter: WidgetPanelsRecyclerAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + super.onCreate() + + binding = ActivityManageWidgetPanelsBinding.inflate(layoutInflater) + setContentView(binding.main) + + val viewManager = LinearLayoutManager(this) + viewAdapter = WidgetPanelsRecyclerAdapter(this, true) { widgetPanel -> + startActivity( + Intent( + this@ManageWidgetPanelsActivity, + ManageWidgetsActivity::class.java + ).also { + it.putExtra(EXTRA_PANEL_ID, widgetPanel.id) + }) + } + binding.manageWidgetPanelsRecycler.apply { + // improve performance (since content changes don't change the layout size) + setHasFixedSize(true) + layoutManager = viewManager + adapter = viewAdapter + } + binding.manageWidgetPanelsClose.setOnClickListener { finish() } + binding.manageWidgetPanelsAddPanel.setOnClickListener { + AlertDialog.Builder(this@ManageWidgetPanelsActivity, R.style.AlertDialogCustom).apply { + setTitle(R.string.dialog_create_widget_panel_title) + setNegativeButton(R.string.dialog_cancel) { _, _ -> } + setPositiveButton(R.string.dialog_ok) { dialogInterface, _ -> + val panelId = WidgetPanel.allocateId() + val label = (dialogInterface as? AlertDialog) + ?.findViewById(R.id.dialog_create_widget_panel_edit_text)?.text?.toString() + ?: (getString(R.string.widget_panel_default_name, panelId)) + + updateWidgetPanel(WidgetPanel(panelId, label)) + } + setView(R.layout.dialog_create_widget_panel) + }.create().also { it.show() }.apply { + findViewById(R.id.dialog_create_widget_panel_edit_text) + ?.setText( + getString( + R.string.widget_panel_default_name, + WidgetPanel.allocateId() + ) + ) + } + true + } + } + + override fun onStart() { + super.onStart() + super.onStart() + LauncherPreferences.getSharedPreferences() + .registerOnSharedPreferenceChangeListener(sharedPreferencesListener) + } + + override fun onPause() { + LauncherPreferences.getSharedPreferences() + .unregisterOnSharedPreferenceChangeListener(sharedPreferencesListener) + super.onPause() + } + + override fun getTheme(): Resources.Theme { + return modifyTheme(super.getTheme()) + } + + override fun setOnClicks() { + binding.manageWidgetPanelsClose.setOnClickListener { finish() } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt index 3172401..d191b70 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt @@ -2,7 +2,6 @@ package de.jrpie.android.launcher.ui.widgets.manage import android.app.Activity import android.appwidget.AppWidgetManager -import android.appwidget.AppWidgetProviderInfo import android.content.Intent import android.content.SharedPreferences import android.content.res.Resources @@ -18,6 +17,7 @@ import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.ui.UIObject import de.jrpie.android.launcher.ui.widgets.WidgetContainerView import de.jrpie.android.launcher.widgets.AppWidget +import de.jrpie.android.launcher.widgets.WidgetPanel import de.jrpie.android.launcher.widgets.WidgetPosition import kotlin.math.min @@ -27,9 +27,13 @@ import kotlin.math.min const val REQUEST_CREATE_APPWIDGET = 1 const val REQUEST_PICK_APPWIDGET = 2 +const val EXTRA_PANEL_ID = "widgetPanelId" + // We can't use AppCompatActivity, since some AppWidgets don't work there. class ManageWidgetsActivity : Activity(), UIObject { + var panelId: Int = WidgetPanel.HOME.id + private var sharedPreferencesListener = SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey -> if (prefKey == LauncherPreferences.widgets().keys().widgets()) { @@ -44,6 +48,9 @@ class ManageWidgetsActivity : Activity(), UIObject { super.onCreate(savedInstanceState) super.onCreate() setContentView(R.layout.activity_manage_widgets) + + panelId = intent.extras?.getInt(EXTRA_PANEL_ID, WidgetPanel.HOME.id) ?: WidgetPanel.HOME.id + findViewById(R.id.manage_widgets_button_add).setOnClickListener { selectWidget() } @@ -54,9 +61,10 @@ class ManageWidgetsActivity : Activity(), UIObject { insets } - findViewById(R.id.manage_widgets_container).updateWidgets(this, - (application as Application).widgets.value - ) + findViewById(R.id.manage_widgets_container).let { + it.widgetPanelId = panelId + it.updateWidgets(this, (application as Application).widgets.value) + } } override fun onStart() { @@ -101,6 +109,10 @@ class ManageWidgetsActivity : Activity(), UIObject { AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetHost.allocateAppWidgetId() ) + it.putExtra( + EXTRA_PANEL_ID, + panelId + ) }, REQUEST_PICK_APPWIDGET ) } @@ -124,7 +136,7 @@ class ManageWidgetsActivity : Activity(), UIObject { display.height ) - val widget = AppWidget(appWidgetId, provider, position) + val widget = AppWidget(appWidgetId, position, panelId, provider) LauncherPreferences.widgets().widgets( (LauncherPreferences.widgets().widgets() ?: HashSet()).also { it.add(widget) @@ -135,7 +147,7 @@ class ManageWidgetsActivity : Activity(), UIObject { private fun configureWidget(data: Intent) { val extras = data.extras val appWidgetId = extras!!.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) - val widget = AppWidget(appWidgetId) + val widget = AppWidget(appWidgetId, panelId = panelId) if (widget.isConfigurable(this)) { widget.configure(this, REQUEST_CREATE_APPWIDGET) } else { diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt index c414db6..a1bd3b5 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt @@ -21,6 +21,7 @@ import de.jrpie.android.launcher.widgets.ClockWidget import de.jrpie.android.launcher.widgets.LauncherAppWidgetProvider import de.jrpie.android.launcher.widgets.LauncherClockWidgetProvider import de.jrpie.android.launcher.widgets.LauncherWidgetProvider +import de.jrpie.android.launcher.widgets.WidgetPanel import de.jrpie.android.launcher.widgets.WidgetPosition import de.jrpie.android.launcher.widgets.bindAppWidgetOrRequestPermission import de.jrpie.android.launcher.widgets.getAppWidgetHost @@ -38,6 +39,7 @@ private const val REQUEST_WIDGET_PERMISSION = 29 class SelectWidgetActivity : AppCompatActivity(), UIObject { lateinit var binding: ActivitySelectWidgetBinding var widgetId: Int = -1 + var widgetPanelId: Int = WidgetPanel.HOME.id private fun tryBindWidget(info: LauncherWidgetProvider) { when (info) { @@ -53,13 +55,14 @@ class SelectWidgetActivity : AppCompatActivity(), UIObject { RESULT_OK, Intent().also { it.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId) + it.putExtra(EXTRA_PANEL_ID, widgetPanelId) } ) finish() } } is LauncherClockWidgetProvider -> { - updateWidget(ClockWidget(widgetId, WidgetPosition(0,4,12,3))) + updateWidget(ClockWidget(widgetId, WidgetPosition(0, 4, 12, 3), widgetPanelId)) finish() } } @@ -79,6 +82,7 @@ class SelectWidgetActivity : AppCompatActivity(), UIObject { widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) + widgetPanelId = intent.getIntExtra(EXTRA_PANEL_ID, WidgetPanel.HOME.id) if (widgetId == -1) { widgetId = getAppWidgetHost().allocateAppWidgetId() } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt index 2d41e13..7a355f7 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt @@ -20,6 +20,7 @@ import androidx.core.graphics.toRect import androidx.core.view.children import de.jrpie.android.launcher.ui.widgets.WidgetContainerView import de.jrpie.android.launcher.widgets.Widget +import de.jrpie.android.launcher.widgets.WidgetPanel import de.jrpie.android.launcher.widgets.WidgetPosition import de.jrpie.android.launcher.widgets.updateWidget import kotlin.math.max @@ -28,8 +29,9 @@ import kotlin.math.min /** * A variant of the [WidgetContainerView] which allows to manage widgets. */ -class WidgetManagerView(context: Context, attrs: AttributeSet? = null) : - WidgetContainerView(context, attrs) { +class WidgetManagerView(widgetPanelId: Int, context: Context, attrs: AttributeSet? = null) : + WidgetContainerView(widgetPanelId, context, attrs) { + constructor(context: Context, attrs: AttributeSet?) : this(WidgetPanel.HOME.id, context, attrs) val TOUCH_SLOP: Int val TOUCH_SLOP_SQUARE: Int @@ -155,13 +157,14 @@ class WidgetManagerView(context: Context, attrs: AttributeSet? = null) : selectedWidgetOverlayView?.mode = null } - override fun updateWidgets(activity: Activity, widgets: Set?) { + override fun updateWidgets(activity: Activity, widgets: Collection?) { super.updateWidgets(activity, widgets) if (widgets == null) { return } + children.mapNotNull { it as? WidgetOverlayView }.forEach { removeView(it) } - widgets.forEach { widget -> + widgets.filter { it.panelId == widgetPanelId }.forEach { widget -> WidgetOverlayView(activity).let { addView(it) it.widgetId = widget.id diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt index 0ce789f..1b8a2d2 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt @@ -13,12 +13,13 @@ import de.jrpie.android.launcher.R import de.jrpie.android.launcher.widgets.Widget import de.jrpie.android.launcher.widgets.updateWidget -/** - * An overlay to show configuration options for a widget. - */ private const val HANDLE_SIZE = 100 private const val HANDLE_EDGE_SIZE = (1.2 * HANDLE_SIZE).toInt() + +/** + * An overlay to show configuration options for a widget in [WidgetManagerView] + */ class WidgetOverlayView : View { diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetPanelsRecyclerAdapter.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetPanelsRecyclerAdapter.kt new file mode 100644 index 0000000..40c2c2f --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetPanelsRecyclerAdapter.kt @@ -0,0 +1,98 @@ +package de.jrpie.android.launcher.ui.widgets.manage + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.EditText +import android.widget.PopupMenu +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.recyclerview.widget.RecyclerView +import de.jrpie.android.launcher.R +import de.jrpie.android.launcher.preferences.LauncherPreferences +import de.jrpie.android.launcher.widgets.WidgetPanel +import de.jrpie.android.launcher.widgets.updateWidgetPanel + + +class WidgetPanelsRecyclerAdapter( + val context: Context, + val showMenu: Boolean = false, + val onSelectWidgetPanel: (WidgetPanel) -> Unit +) : + RecyclerView.Adapter() { + + var widgetPanels = (LauncherPreferences.widgets().customPanels() ?: setOf()).toTypedArray() + + class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + var labelView: TextView = itemView.findViewById(R.id.list_widget_panels_label) + } + + override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) { + viewHolder.labelView.text = widgetPanels[i].label + + viewHolder.itemView.setOnClickListener { + onSelectWidgetPanel(widgetPanels[i]) + } + + if (showMenu) { + viewHolder.itemView.setOnLongClickListener { + showOptionsPopup( + viewHolder, + widgetPanels[i] + ) + } + } + } + + @Suppress("SameReturnValue") + private fun showOptionsPopup( + viewHolder: ViewHolder, + widgetPanel: WidgetPanel + ): Boolean { + //create the popup menu + + val popup = PopupMenu(context, viewHolder.labelView) + popup.menu.add(R.string.manage_widget_panels_delete).setOnMenuItemClickListener { _ -> + widgetPanel.delete(context) + true + } + popup.menu.add(R.string.manage_widget_panels_rename).setOnMenuItemClickListener { _ -> + AlertDialog.Builder(context, R.style.AlertDialogCustom).apply { + setNegativeButton(R.string.dialog_cancel) { _, _ -> } + setPositiveButton(R.string.dialog_ok) { dialogInterface, _ -> + var newLabel = (dialogInterface as? AlertDialog) + ?.findViewById(R.id.dialog_rename_widget_panel_edit_text) + ?.text?.toString() + if (newLabel == null || newLabel.isEmpty()) { + newLabel = + (context.getString(R.string.widget_panel_default_name, widgetPanel.id)) + } + widgetPanel.label = newLabel + updateWidgetPanel(widgetPanel) + } + setView(R.layout.dialog_rename_widget_panel) + }.create().also { it.show() }.apply { + findViewById(R.id.dialog_rename_widget_panel_edit_text)?.let { + it.setText(widgetPanel.label) + it.hint = context.getString(R.string.widget_panel_default_name, widgetPanel.id) + } + } + true + } + + popup.show() + return true + } + + override fun getItemCount(): Int { + return widgetPanels.size + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view: View = + LayoutInflater.from(context).inflate(R.layout.list_widget_panels_row, parent, false) + val viewHolder = ViewHolder(view) + return viewHolder + } +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt index 3e9a2eb..22a63eb 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt @@ -1,4 +1,4 @@ -package de.jrpie.android.launcher.widgets; +package de.jrpie.android.launcher.widgets import android.app.Activity import android.appwidget.AppWidgetHostView @@ -11,7 +11,6 @@ import android.util.DisplayMetrics import android.util.SizeF import android.view.View import de.jrpie.android.launcher.Application -import de.jrpie.android.launcher.ui.HomeActivity import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -20,6 +19,7 @@ import kotlinx.serialization.Serializable class AppWidget( override val id: Int, override var position: WidgetPosition = WidgetPosition(0,0,1,1), + override var panelId: Int = WidgetPanel.HOME.id, override var allowInteraction: Boolean = false, // We keep track of packageName, className and user to make it possible to restore the widget @@ -31,10 +31,16 @@ class AppWidget( ): Widget() { - constructor(id: Int, widgetProviderInfo: AppWidgetProviderInfo, position: WidgetPosition) : + constructor( + id: Int, + position: WidgetPosition, + panelId: Int, + widgetProviderInfo: AppWidgetProviderInfo + ) : this( id, position, + panelId, false, widgetProviderInfo.provider.packageName, widgetProviderInfo.provider.className, diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt index d819538..d0d1c0e 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt @@ -14,6 +14,7 @@ import kotlinx.serialization.Serializable class ClockWidget( override val id: Int, override var position: WidgetPosition, + override val panelId: Int, override var allowInteraction: Boolean = true ) : Widget() { diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt index d3610dd..dbe667b 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt @@ -14,6 +14,7 @@ import kotlinx.serialization.json.Json sealed class Widget { abstract val id: Int abstract var position: WidgetPosition + abstract val panelId: Int abstract var allowInteraction: Boolean /** @@ -36,6 +37,10 @@ sealed class Widget { ) } + fun getPanel(): WidgetPanel? { + return WidgetPanel.byId(panelId) + } + override fun hashCode(): Int { return id } diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetPanel.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetPanel.kt new file mode 100644 index 0000000..93e588d --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetPanel.kt @@ -0,0 +1,58 @@ +package de.jrpie.android.launcher.widgets + +import android.content.Context +import de.jrpie.android.launcher.preferences.LauncherPreferences +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + + +@Serializable +@SerialName("panel") +class WidgetPanel(val id: Int, var label: String) { + + override fun equals(other: Any?): Boolean { + return (other as? WidgetPanel)?.id == id + } + + override fun hashCode(): Int { + return id + } + + fun serialize(): String { + return Json.encodeToString(this) + } + + fun delete(context: Context) { + LauncherPreferences.widgets().customPanels( + (LauncherPreferences.widgets().customPanels() ?: setOf()).minus(this) + ) + (LauncherPreferences.widgets().widgets() ?: return) + .filter { it.panelId == this.id }.forEach { it.delete(context) } + } + + + companion object { + val HOME = WidgetPanel(0, "home") + fun byId(id: Int): WidgetPanel? { + if (id == 0) { + return HOME + } + return LauncherPreferences.widgets().customPanels()?.firstOrNull { it.id == id } + } + + fun allocateId(): Int { + return ( + (LauncherPreferences.widgets().customPanels() ?: setOf()) + .plus(HOME) + .maxOfOrNull { it.id } ?: 0 + ) + 1 + } + + fun deserialize(serialized: String): WidgetPanel { + return Json.decodeFromString(serialized) + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt index cd4ef29..b7f140b 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt @@ -74,9 +74,19 @@ fun getAppWidgetProviders( context: Context ): List { fun updateWidget(widget: Widget) { - var widgets = LauncherPreferences.widgets().widgets() ?: setOf() - widgets = widgets.minus(widget).plus(widget) - LauncherPreferences.widgets().widgets(widgets) + LauncherPreferences.widgets().widgets( + (LauncherPreferences.widgets().widgets() ?: setOf()) + .minus(widget) + .plus(widget) + ) +} + +fun updateWidgetPanel(widgetPanel: WidgetPanel) { + LauncherPreferences.widgets().customPanels( + (LauncherPreferences.widgets().customPanels() ?: setOf()) + .minus(widgetPanel) + .plus(widgetPanel) + ) } fun Context.getAppWidgetHost(): AppWidgetHost { diff --git a/app/src/main/res/drawable/baseline_widgets_24.xml b/app/src/main/res/drawable/baseline_widgets_24.xml new file mode 100644 index 0000000..fd0f571 --- /dev/null +++ b/app/src/main/res/drawable/baseline_widgets_24.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/layout/activity_manage_widget_panels.xml b/app/src/main/res/layout/activity_manage_widget_panels.xml new file mode 100644 index 0000000..60413a3 --- /dev/null +++ b/app/src/main/res/layout/activity_manage_widget_panels.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_widget_panel.xml b/app/src/main/res/layout/activity_widget_panel.xml new file mode 100644 index 0000000..6ef6b20 --- /dev/null +++ b/app/src/main/res/layout/activity_widget_panel.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_create_widget_panel.xml b/app/src/main/res/layout/dialog_create_widget_panel.xml new file mode 100644 index 0000000..900823d --- /dev/null +++ b/app/src/main/res/layout/dialog_create_widget_panel.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_rename_widget_panel.xml b/app/src/main/res/layout/dialog_rename_widget_panel.xml new file mode 100644 index 0000000..effb783 --- /dev/null +++ b/app/src/main/res/layout/dialog_rename_widget_panel.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_select_widget_panel.xml b/app/src/main/res/layout/dialog_select_widget_panel.xml new file mode 100644 index 0000000..5f83d51 --- /dev/null +++ b/app/src/main/res/layout/dialog_select_widget_panel.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_widget_panels_row.xml b/app/src/main/res/layout/list_widget_panels_row.xml new file mode 100644 index 0000000..53f7449 --- /dev/null +++ b/app/src/main/res/layout/list_widget_panels_row.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 30e4cda..f783d2a 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -10,6 +10,7 @@ internal.first_startup internal.version_code widgets.widgets + widgets.custom_panels apps.favorites apps.hidden apps.pinned_shortcuts diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a8813f8..7d29128 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -99,6 +99,7 @@ Click on time Manage widgets + Manage widget panels Choose App @@ -400,5 +401,23 @@ Clock The default clock of μLauncher + Delete + Rename + + Widget Panel #%1$d + + Contains %d widget. + Contains %d widgets. + + + + Ok + Widget Panels + Select a Widget Panel + Create new widget panel + Launcher > Manage Widget Panels.]]> + Open Widget Panel + This widget panel no longer exists. + Widgets diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index b4bc5f0..0ee7c17 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -3,14 +3,10 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> + - - - + + + + Date: Tue, 29 Apr 2025 22:19:08 -0500 Subject: [PATCH 032/118] add wiki --- .gitmodules | 3 +++ launcher.wiki | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 launcher.wiki diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f1aceb4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "launcher.wiki"] + path = launcher.wiki + url = https://github.com/wassupluke/Launcher.wiki.git diff --git a/launcher.wiki b/launcher.wiki new file mode 160000 index 0000000..d40df78 --- /dev/null +++ b/launcher.wiki @@ -0,0 +1 @@ +Subproject commit d40df78a56336aa0ae26ad1308e4aac48456dff8 From f6155102560d140f72e9e5c77937166a0f157aba Mon Sep 17 00:00:00 2001 From: Luke Wass Date: Wed, 30 Apr 2025 20:26:05 -0500 Subject: [PATCH 033/118] migrate wiki to docs directory --- docs/{build.md => Building-from-Source.md} | 21 +- ...cher.md => Changes-from-Finns-Launcher.md} | 0 docs/Contributing.md | 10 + docs/Gestures-and-Bindings.md | 47 ++++ docs/Home.md | 37 +++ docs/Launcher-Settings.md | 222 ++++++++++++++++++ docs/Work-Profile.md | 3 + launcher.wiki | 1 - 8 files changed, 331 insertions(+), 10 deletions(-) rename docs/{build.md => Building-from-Source.md} (76%) rename docs/{launcher.md => Changes-from-Finns-Launcher.md} (100%) create mode 100644 docs/Contributing.md create mode 100644 docs/Gestures-and-Bindings.md create mode 100644 docs/Home.md create mode 100644 docs/Launcher-Settings.md create mode 100644 docs/Work-Profile.md delete mode 160000 launcher.wiki diff --git a/docs/build.md b/docs/Building-from-Source.md similarity index 76% rename from docs/build.md rename to docs/Building-from-Source.md index 75921f9..3c2ef1f 100644 --- a/docs/build.md +++ b/docs/Building-from-Source.md @@ -1,11 +1,9 @@ -# Building µLauncher +# Using the command line -## Using the command line - -Install JDK 17 and the Android Sdk. +Install JDK 17 and the Android SDK. Make sure that `JAVA_HOME` and `ANDROID_HOME` are set correctly. -``` +```bash git clone https://github.com/jrpie/Launcher cd Launcher @@ -15,7 +13,7 @@ cd Launcher This will create an apk file at `app/build/outputs/apk/default/release/app-default-release-unsigned.apk`. Note that you need to sign it: -``` +```bash apksigner sign --ks "$YOUR_KEYSTORE" \ --ks-key-alias "$YOUR_ALIAS" \ --ks-pass="pass:$YOUR_PASSWORD" \ @@ -28,13 +26,18 @@ apksigner sign --ks "$YOUR_KEYSTORE" \ app-default-release-unsigned.apk ``` - See [this guide](https://developer.android.com/build/building-cmdline) for further instructions. -## Using Android Studio +# Using Android Studio + Install [Android Studio](https://developer.android.com/studio), import this project and build it. See [this guide](https://developer.android.com/studio/run) -for further instructions. +for further instructions. How to + +# CI Pipeline + +The [CI pipeline](https://github.com/jrpie/Launcher/actions) automatically creates debug builds. +> Note: These builds are *not* signed. diff --git a/docs/launcher.md b/docs/Changes-from-Finns-Launcher.md similarity index 100% rename from docs/launcher.md rename to docs/Changes-from-Finns-Launcher.md diff --git a/docs/Contributing.md b/docs/Contributing.md new file mode 100644 index 0000000..bd56a7a --- /dev/null +++ b/docs/Contributing.md @@ -0,0 +1,10 @@ +# Ways to contribute + +- Found a **bug** or have an idea for a **new feature**? [Join the chat](https://s.jrpie.de/launcher-chat) or open an [issue](https://github.com/jrpie/Launcher/issues/). + > Please note that I work on this project in my free time. Thus I might not respond immediately and not all ideas will be implemented. +- Implement a new feature yourself: + 1. [Fork](https://github.com/jrpie/launcher/fork) this repository. + 2. Create a **new branch** named *feature/\* or *fix/\* and commit your changes. + 3. Open a new pull request. +- Add or improve [translations](https://toolate.othing.xyz/projects/jrpie-launcher/). +translation status \ No newline at end of file diff --git a/docs/Gestures-and-Bindings.md b/docs/Gestures-and-Bindings.md new file mode 100644 index 0000000..d87a406 --- /dev/null +++ b/docs/Gestures-and-Bindings.md @@ -0,0 +1,47 @@ +# Available Gestures +## Swipes +- Up, down, left, or right. +- Up, down, left, or right with two fingers. +- Up or down on the left or right edge. +- Left or right on the top or bottom edge. + +## Taps +- Tap on date or time. +- Double tap. +- Long tap. + +## Tap-then-Swipes +tap then swipe up, down, left, or right + +## Complex Gestures +- Draw <, >, V, or Λ. +- Draw <, >, V, or Λ in reverse direction. + +## Hardware Buttons as Gestures +- Back button. +- Volume up or down button. + + +*** + + +# Available Bindings +Any of the above gestures can be bound to any of the following bindings. +## Launcher Bindings +- Open µLauncher settings. +- Open a list of all*, favorite, or private* apps. + +## App Bindings +- Launch an app. +- Launch another Home Screen. + +## Android / Device Bindings +- Toggle private space lock. +- Lock the screen. +- Toggle the torch (flashlight) +- Raise or lower volume. +- Skip to previous or next audio track. +- Open Notifications shade. +- Open Quick Settings shade. + +> \* excludes apps hidden by the user \ No newline at end of file diff --git a/docs/Home.md b/docs/Home.md new file mode 100644 index 0000000..8ce432c --- /dev/null +++ b/docs/Home.md @@ -0,0 +1,37 @@ +# Welcome to the μLauncher wiki! +We're thrilled you're here and confident you'll love your new Android launcher! Check out this wiki to get familiar with your new app. + +## What is μLauncher? +µLauncher is an *minimal* and *distraction-free* Android home screen that lets you launch apps using *efficient* swipe gestures and button presses. This project is a fork of [finnmglas's app Launcher](https://github.com/finnmglas/Launcher). An incomplete list of changes can be found [here](https://github.com/wassupluke/Launcher/blob/master/docs/launcher.md). + +## Where can I get μLauncher? +Get it on F-Droid +Get it on Accrescent +Get it on Obtainium +Get it on GitHub + +> You can also [get it on Google Play](https://play.google.com/store/apps/details?id=de.jrpie.android.launcher), but this is not recommend. + +## Screenshots +μLauncher Home Screen screenshot +μLauncher Settings screenshot +μLauncher All Apps list view with icons screenshot +μLauncher Favorite Apps list view with icons screenshot +μLauncher Choose App to bind to gesture screenshot +μLauncher App options card from list view with icons screenshot +μLauncher All Apps list view without icons screenshot + \ No newline at end of file diff --git a/docs/Launcher-Settings.md b/docs/Launcher-Settings.md new file mode 100644 index 0000000..ac665fa --- /dev/null +++ b/docs/Launcher-Settings.md @@ -0,0 +1,222 @@ +Tweaks and customizations can be made from within the Launcher Settings page. + +These settings let you change wallpapers, change colors and fonts, enable monochrome app icons, change the app drawer layout, and much more. + +In the following documentation, 'app drawer' will be used to refer to the 'All Apps' and 'Favorite Apps' views. + + +# Appearance + +### Choose a wallpaper +Lets you change the wallpaper using a photos app, file explorer, or native wallpaper setting app. + + +> ### Font + +Set the font used within the app settings. This setting does not affect the date/time [home screen font](https://github.com/wassupluke/Launcher/wiki/Tweaks-and-Customizations/_edit#font-1). + +**type:** `dropdown` + +**options:** `Hack`,`System default`,`Sans serif`,`Serif`,`Monospace`,`Serif monospace` + +> ### Text Shadow + +**type:** `toggle` + +> ### Background (app list and setting) + +**type:** `dropdown` + +**type:** `Transparent`,`Dim`,`Blur`,`Solid` + +> ### Monochrome app icons + +Remove coloring from all app icons. Can help decrease visual stimulus when enabled. + +**type:** `toggle` + + +# Date & Time + +> ### Font + +Set the home screen font for date and time. This setting does not affect the [app settings font](https://github.com/wassupluke/Launcher/wiki/Tweaks-and-Customizations/_edit#font). + +**type:** `dropdown` + +**options:** `Hack`,`System default`,`Sans serif`,`Serif`,`Monospace`,`Serif monospace` + +> ### Color [`[bug]`](https://github.com/jrpie/launcher/issues/151) + +Set the color for the home screen date and time. + +Accepts a HEX color code (consisting of a '#' followed by three sets of two alphanumeric (letters and numbers) characters. A fourth set of two alphanumeric characters may be added to set the transparency of the color. + +[Color wheel picker](https://rgbacolorpicker.com/color-wheel-picker) + +**type:** `HEX`,`RGBA` + +> ### Use localized date format + +Adapt the display of dates and times to the specific conventions of a particular locale or region. Different locales use different date orders (e.g., MM/DD/YYYY in the US, DD/MM/YYYY in Europe). + +**type:** `toggle` + +> ### Show time + +Show the current time on the home screen. + +**type:** `toggle` + +> ### Show seconds + +Show the current time down to the second on the home screen. + +**type:** `toggle` + +> ### Show date + +Show the current date on the home screen. + +**type:** `toggle` + +> ### Flip date and time + +Place the current time above the current date on the home screen. + +**type:** `toggle` + + +# Functionality + +> ### Launch search results + +Launches any app that matches user keyboard input when no other apps match. + +As you type inside the app drawer, the app narrows down the list of apps shown based on the app title matching your text input. For example, if you type, `a`, the app list narrows to any apps with a title containing the letter `a`. Continuing the example, if you then follow your `a` with the letter `m`, the list now shows only apps containing the letter combination `am` in that order. If the only app matching this combination was, for example, `Amazon`, simply typing `am` in the app drawer would immediately launch the `Amazon` app for you. + +This feature becomes more powerful when combined with [renaming](https://github.com/wassupluke/Launcher/wiki/Launcher-Settings/_edit#additional-settings) apps, effectively letting you define custom app names that could be considered 'aliases' or shortcuts. For instance, if you wanted to "bind" the keyboard input `gh` to open your `GitHub` app, you could rename `GitHub` to `GitHub gh`, `gh GitHub`, or simply `gh`. Assuming no other installed apps have the `gh` combination of letters in them, opening the app drawer and typing `gh` would immediately launch your `GitHub` app. + +Press space to temporarily disable this feature and allow text entry without prematurely launching an app. Useful when combined with the [Search the web](https://github.com/wassupluke/Launcher/wiki/Launcher-Settings/_edit#search-the-web) feature. + +**type:** `toggle` + +> ### Search the web + +Press return/enter while searching the app list to launch a web search. + +**type:** `toggle` + +> ### Start keyboard for search + +Automatically open the keyboard when the app drawer is opened. + +**type:** `toggle` + +> ### Double swipe actions + +Enable double swipe (two finger) actions as bindable gestures in launcher settings. Does not erase gesture bindings if accidentally turned off. + +**type:** `toggle` + +> ### Edge swipe actions + +Enable edge swipe (near edges of screen) actions as bindable gestures in launcher settings. Does not erase gesture bindings if accidentally turned off. + +**type:** `toggle` + +> ### Edge width + +Change how large a margin is used for detecting edge gestures. Shows the edge margin preview when using the slider. + +**type:** `slider` + +> ### Choose method for locking the screen + +There are two methods to lock the screen and unfortunately both have downsides. +1. **`Device Admin`** + - Doesn't work with unlocking by fingerprint or face recognition. +2. **`Accessibility Service`** + - Requires excessive privileges. + - μLauncher will use those privileges *only* for locking the screen. + - As a rule of thumb, it is [not recommended](https://android.stackexchange.com/questions/248171/is-it-safe-to-give-accessibility-permission-to-an-app) to grant access to accessibility services on a random app. Always review the [source code](https://github.com/jrpie/Launcher) before granting accessibility permissions so you familiarize yourself with what the code might do. + - On some devices, the start-up PIN will no longer be used for encrypting data after activating an accessibility service. + - This can be [reactivated](https://issuetracker.google.com/issues/37010136#comment36) afterwards. + +**type:** `text buttons` + +**options:** `USE DEVICE ADMIN`,`USE ACCESSIBILITY SERVICE` + +# Apps + +> ### Hidden apps + +Open an app drawer containing only hidden apps. + +> ### Don't show apps that are bound to a gesture in the app list + +Remove certain apps from the app drawer if they are already accessible via a gesture. + +Reduces redundancy and tidies up app drawer. + +**type:** `toggle` + +> ### Hide paused apps + +Remove paused apps from the app drawer. + +**type:** `toggle` + +> ### Hide private space from app list + +Remove private space from app drawer. + +**type:** `toggle` + +> ### Layout of app list + +Change how the apps are displayed when accessing the app drawer. By `Default`, all apps in the drawer will show in a vertically-scrolled list with their app icon and title. `Text` removes the app icons, shows only app titles in the drawer as a vertically-scrolled list. `Grid` shows apps with their app icon and title in a grid layout. + +**type:** `dropdown` + +**options:** `Default`,`Text`,`Grid` + +> ### Reverse the app list + +Enable Z-A sorting of apps in the app drawer. Useful for keeping apps within easier reach from the keyboard. + +**type:** `toggle` + + +# Display + +> ### Rotate screen + +**type:** `toggle` + +> ### Keep screen on + +**type:** `toggle` + +> ### Hide status bar + +Remove the top status bar from the home screen. + +**type:** `toggle` + +> ### Hide navigation bar + +Remove the navigation bar from the home screen. Enabling this setting may make it difficult to use the device if gestures are not setup properly. + +**type:** `toggle` + + +# Additional Settings + +> ### App Drawer Long Press on App + +Access additional per-app details and settings. To use, open the app drawer and long press on any app. + +**type:** `dropdown` + +**options:** `App Info`,`Add to favorites`,`Hide`,`Rename`,`Uninstall` \ No newline at end of file diff --git a/docs/Work-Profile.md b/docs/Work-Profile.md new file mode 100644 index 0000000..38c6598 --- /dev/null +++ b/docs/Work-Profile.md @@ -0,0 +1,3 @@ +# Android Enterprise Work Profile + +µLauncher is compatible with [work profile](https://www.android.com/enterprise/work-profile/), so apps like [Shelter](https://gitea.angry.im/PeterCxy/Shelter) can be used. \ No newline at end of file diff --git a/launcher.wiki b/launcher.wiki deleted file mode 160000 index d40df78..0000000 --- a/launcher.wiki +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d40df78a56336aa0ae26ad1308e4aac48456dff8 From b2b823446f03c6d66c7e45c8088f2e9e4644d18e Mon Sep 17 00:00:00 2001 From: Luke Wass Date: Wed, 30 Apr 2025 20:34:06 -0500 Subject: [PATCH 034/118] remove stray .gitmodules after wiki deletion --- .gitmodules | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .gitmodules diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index f1aceb4..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "launcher.wiki"] - path = launcher.wiki - url = https://github.com/wassupluke/Launcher.wiki.git From 8f9f8ac928697939ec09be78bdaac0f8c2e143d4 Mon Sep 17 00:00:00 2001 From: Luke Wass Date: Wed, 30 Apr 2025 22:34:28 -0500 Subject: [PATCH 035/118] improve codestyle --- docs/Building-from-Source.md | 10 +++-- docs/Changes-from-Finns-Launcher.md | 27 +++++++------ docs/Contributing.md | 11 +++++- docs/Gestures-and-Bindings.md | 37 +++++++++++------- docs/Home.md | 59 +++++++++++++++-------------- docs/Launcher-Settings.md | 49 ++++++++++++------------ docs/Work-Profile.md | 3 +- 7 files changed, 111 insertions(+), 85 deletions(-) diff --git a/docs/Building-from-Source.md b/docs/Building-from-Source.md index 3c2ef1f..fc8de3d 100644 --- a/docs/Building-from-Source.md +++ b/docs/Building-from-Source.md @@ -1,4 +1,6 @@ -# Using the command line +# Building from Source + +## Using the command line Install JDK 17 and the Android SDK. Make sure that `JAVA_HOME` and `ANDROID_HOME` are set correctly. @@ -13,6 +15,7 @@ cd Launcher This will create an apk file at `app/build/outputs/apk/default/release/app-default-release-unsigned.apk`. Note that you need to sign it: + ```bash apksigner sign --ks "$YOUR_KEYSTORE" \ --ks-key-alias "$YOUR_ALIAS" \ @@ -29,15 +32,14 @@ apksigner sign --ks "$YOUR_KEYSTORE" \ See [this guide](https://developer.android.com/build/building-cmdline) for further instructions. - -# Using Android Studio +## Using Android Studio Install [Android Studio](https://developer.android.com/studio), import this project and build it. See [this guide](https://developer.android.com/studio/run) for further instructions. How to -# CI Pipeline +## CI Pipeline The [CI pipeline](https://github.com/jrpie/Launcher/actions) automatically creates debug builds. > Note: These builds are *not* signed. diff --git a/docs/Changes-from-Finns-Launcher.md b/docs/Changes-from-Finns-Launcher.md index cb290a0..f08678d 100644 --- a/docs/Changes-from-Finns-Launcher.md +++ b/docs/Changes-from-Finns-Launcher.md @@ -1,17 +1,17 @@ -# Notable changes compared to [Finn's Launcher][original-repo]: +# Notable changes compared to Finn's Launcher -µLauncher is a fork of [finnmglas's app Launcher][original-repo]. +µLauncher is a fork of [finnmglas's app Launcher](https://github.com/finnmglas/Launcher). Here is an incomplete list of changes: - - Additional gestures: - - Back - - V,Λ,<,> - - Edge gestures: There is a setting to allow distinguishing swiping at the edges of the screen from swiping in the center. + - Back + - V,Λ,<,> + - Edge gestures: There is a setting to allow distinguishing swiping at the edges of the screen from swiping in the center. + - Compatible with [work profile](https://www.android.com/enterprise/work-profile/), so apps like [Shelter](https://gitea.angry.im/PeterCxy/Shelter) can be used. - Compatible with [private space](https://source.android.com/docs/security/features/private-space) - Option to rename apps @@ -23,18 +23,20 @@ The decision to create a hard fork was made two years later.--> - The home button now works as expected. - Improved gesture detection. -### Visual +## Visual + - This app uses the system wallpaper instead of a custom solution. - The font has been changed to [Hack][hack-font], other fonts can be selected. - Font Awesome Icons were replaced by Material icons. - The gear button on the home screen was removed. A smaller button is show at the top right when necessary. +## Search -### Search - The search algorithm was modified to prefer matches at the beginning of the app name, i.e. when searching for `"te"`, `"termux"` is sorted before `"notes"`. - The search bar was moved to the bottom of the screen. -### Technical +## Technical + - Improved gesture detection. - Different apps set as default. - Package name was changed to `de.jrpie.android.launcher` to avoid clashing with the original app. @@ -42,9 +44,10 @@ The decision to create a hard fork was made two years later.--> - Fixed some bugs - Some refactoring - The complete list of changes can be viewed [here](https://github.com/jrpie/launcher/compare/340ee731...master). --- - [original-repo]: https://github.com/finnmglas/Launcher - [hack-font]: https://sourcefoundry.org/hack/ + +[original-repo]: https://github.com/finnmglas/Launcher + +[hack-font]: https://sourcefoundry.org/hack/ diff --git a/docs/Contributing.md b/docs/Contributing.md index bd56a7a..cc79e87 100644 --- a/docs/Contributing.md +++ b/docs/Contributing.md @@ -1,10 +1,17 @@ -# Ways to contribute +# Contributing + +## Ways to contribute - Found a **bug** or have an idea for a **new feature**? [Join the chat](https://s.jrpie.de/launcher-chat) or open an [issue](https://github.com/jrpie/Launcher/issues/). + > Please note that I work on this project in my free time. Thus I might not respond immediately and not all ideas will be implemented. + - Implement a new feature yourself: + 1. [Fork](https://github.com/jrpie/launcher/fork) this repository. 2. Create a **new branch** named *feature/\* or *fix/\* and commit your changes. 3. Open a new pull request. + - Add or improve [translations](https://toolate.othing.xyz/projects/jrpie-launcher/). -translation status \ No newline at end of file + +![translation status](https://toolate.othing.xyz/widget/jrpie-launcher/launcher/horizontal-auto.svg) diff --git a/docs/Gestures-and-Bindings.md b/docs/Gestures-and-Bindings.md index d87a406..09ff172 100644 --- a/docs/Gestures-and-Bindings.md +++ b/docs/Gestures-and-Bindings.md @@ -1,41 +1,52 @@ -# Available Gestures -## Swipes +# Gestures and Bindings + +## Available Gestures + +### Swipes + - Up, down, left, or right. - Up, down, left, or right with two fingers. - Up or down on the left or right edge. - Left or right on the top or bottom edge. -## Taps +### Taps + - Tap on date or time. - Double tap. - Long tap. -## Tap-then-Swipes -tap then swipe up, down, left, or right +### Tap-then-Swipes + +Tap then swipe up, down, left, or right + +### Complex Gestures -## Complex Gestures - Draw <, >, V, or Λ. - Draw <, >, V, or Λ in reverse direction. -## Hardware Buttons as Gestures +### Hardware Buttons as Gestures + - Back button. - Volume up or down button. - *** +## Available Bindings -# Available Bindings Any of the above gestures can be bound to any of the following bindings. -## Launcher Bindings + +### Launcher Bindings + - Open µLauncher settings. - Open a list of all*, favorite, or private* apps. -## App Bindings +### App Bindings + - Launch an app. - Launch another Home Screen. -## Android / Device Bindings +### Android / Device Bindings + - Toggle private space lock. - Lock the screen. - Toggle the torch (flashlight) @@ -44,4 +55,4 @@ Any of the above gestures can be bound to any of the following bindings. - Open Notifications shade. - Open Quick Settings shade. -> \* excludes apps hidden by the user \ No newline at end of file +> \* excludes apps hidden by the user diff --git a/docs/Home.md b/docs/Home.md index 8ce432c..2cb6093 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -1,37 +1,38 @@ -# Welcome to the μLauncher wiki! +# Welcome to the μLauncher wiki + We're thrilled you're here and confident you'll love your new Android launcher! Check out this wiki to get familiar with your new app. -## What is μLauncher? +## What is μLauncher + µLauncher is an *minimal* and *distraction-free* Android home screen that lets you launch apps using *efficient* swipe gestures and button presses. This project is a fork of [finnmglas's app Launcher](https://github.com/finnmglas/Launcher). An incomplete list of changes can be found [here](https://github.com/wassupluke/Launcher/blob/master/docs/launcher.md). -## Where can I get μLauncher? -Get it on F-Droid -Get it on Accrescent -Get it on Obtainium -Get it on GitHub +## Where can I get μLauncher + +[![Get it on F-Droid](https://fdroid.gitlab.io/artwork/badge/get-it-on.png)](https://f-droid.org/packages/de.jrpie.android.launcher/) + +[![Get it on Accrescent](https://accrescent.app/badges/get-it-on.png)](https://accrescent.app/app/de.jrpie.android.launcher.accrescent) + +[![Get it on Obtainium](https://raw.githubusercontent.com/ImranR98/Obtainium/b1c8ac6f2ab08497189721a788a5763e28ff64cd/assets/graphics/badge_obtainium.png)](https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/{%22id%22:%22de.jrpie.android.launcher%22,%22url%22:%22https://github.com/jrpie/Launcher%22,%22author%22:%22jrpie%22,%22name%22:%22%c2%b5Launcher%22,%22additionalSettings%22:%22{\%22apkFilterRegEx\%22:\%22release\%22,\%22invertAPKFilter\%22:false,\%22about\%22:\%22%c2%b5Launcher%20is%20a%20minimal%20home%20screen.\%22}%22}) + +[![Get it on GitHub](https://raw.githubusercontent.com/NeoApplications/Neo-Backup/034b226cea5c1b30eb4f6a6f313e4dadcbb0ece4/badge_github.png)](https://github.com/jrpie/launcher/releases/latest) > You can also [get it on Google Play](https://play.google.com/store/apps/details?id=de.jrpie.android.launcher), but this is not recommend. ## Screenshots -μLauncher Home Screen screenshot -μLauncher Settings screenshot -μLauncher All Apps list view with icons screenshot -μLauncher Favorite Apps list view with icons screenshot -μLauncher Choose App to bind to gesture screenshot -μLauncher App options card from list view with icons screenshot -μLauncher All Apps list view without icons screenshot - \ No newline at end of file + +![μLauncher Home Screen screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg) + +![μLauncher Settings screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg) + +![μLauncher All Apps list view with icons screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg) + +![μLauncher Favorite Apps list view with icons screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/4.jpg) + +![μLauncher Choose App to bind to gesture screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/5.jpg) + +![μLauncher App options card from list view with icons screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/7.jpg +) + +![μLauncher All Apps list view without icons screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/8.jpg) + + diff --git a/docs/Launcher-Settings.md b/docs/Launcher-Settings.md index ac665fa..1444c73 100644 --- a/docs/Launcher-Settings.md +++ b/docs/Launcher-Settings.md @@ -1,19 +1,20 @@ +# Launcher Settings + Tweaks and customizations can be made from within the Launcher Settings page. These settings let you change wallpapers, change colors and fonts, enable monochrome app icons, change the app drawer layout, and much more. In the following documentation, 'app drawer' will be used to refer to the 'All Apps' and 'Favorite Apps' views. +## Appearance -# Appearance +> ### Choose a wallpaper -### Choose a wallpaper Lets you change the wallpaper using a photos app, file explorer, or native wallpaper setting app. - -> ### Font +> ### Font (in-app font) -Set the font used within the app settings. This setting does not affect the date/time [home screen font](https://github.com/wassupluke/Launcher/wiki/Tweaks-and-Customizations/_edit#font-1). +Set the font used within the app settings. This setting does not affect the date/time home screen font. **type:** `dropdown` @@ -35,12 +36,11 @@ Remove coloring from all app icons. Can help decrease visual stimulus when enabl **type:** `toggle` +## Date & Time -# Date & Time +> ### Font (home screen) -> ### Font - -Set the home screen font for date and time. This setting does not affect the [app settings font](https://github.com/wassupluke/Launcher/wiki/Tweaks-and-Customizations/_edit#font). +Set the home screen font for date and time. This setting does not affect the in-app font. **type:** `dropdown` @@ -86,8 +86,7 @@ Place the current time above the current date on the home screen. **type:** `toggle` - -# Functionality +## Functionality > ### Launch search results @@ -134,20 +133,24 @@ Change how large a margin is used for detecting edge gestures. Shows the edge ma > ### Choose method for locking the screen There are two methods to lock the screen and unfortunately both have downsides. + 1. **`Device Admin`** - - Doesn't work with unlocking by fingerprint or face recognition. + + - Doesn't work with unlocking by fingerprint or face recognition. + 2. **`Accessibility Service`** - - Requires excessive privileges. - - μLauncher will use those privileges *only* for locking the screen. - - As a rule of thumb, it is [not recommended](https://android.stackexchange.com/questions/248171/is-it-safe-to-give-accessibility-permission-to-an-app) to grant access to accessibility services on a random app. Always review the [source code](https://github.com/jrpie/Launcher) before granting accessibility permissions so you familiarize yourself with what the code might do. - - On some devices, the start-up PIN will no longer be used for encrypting data after activating an accessibility service. + + - Requires excessive privileges. + - μLauncher will use those privileges *only* for locking the screen. + - As a rule of thumb, it is [not recommended](https://android.stackexchange.com/questions/248171/is-it-safe-to-give-accessibility-permission-to-an-app) to grant access to accessibility services on a random app. Always review the [source code](https://github.com/jrpie/Launcher) before granting accessibility permissions so you familiarize yourself with what the code might do. + - On some devices, the start-up PIN will no longer be used for encrypting data after activating an accessibility service. - This can be [reactivated](https://issuetracker.google.com/issues/37010136#comment36) afterwards. -**type:** `text buttons` + **type:** `text buttons` -**options:** `USE DEVICE ADMIN`,`USE ACCESSIBILITY SERVICE` + **options:** `USE DEVICE ADMIN`,`USE ACCESSIBILITY SERVICE` -# Apps +## Apps > ### Hidden apps @@ -187,8 +190,7 @@ Enable Z-A sorting of apps in the app drawer. Useful for keeping apps within eas **type:** `toggle` - -# Display +## Display > ### Rotate screen @@ -210,8 +212,7 @@ Remove the navigation bar from the home screen. Enabling this setting may make i **type:** `toggle` - -# Additional Settings +## Additional Settings > ### App Drawer Long Press on App @@ -219,4 +220,4 @@ Access additional per-app details and settings. To use, open the app drawer and **type:** `dropdown` -**options:** `App Info`,`Add to favorites`,`Hide`,`Rename`,`Uninstall` \ No newline at end of file +**options:** `App Info`,`Add to favorites`,`Hide`,`Rename`,`Uninstall` diff --git a/docs/Work-Profile.md b/docs/Work-Profile.md index 38c6598..718fe28 100644 --- a/docs/Work-Profile.md +++ b/docs/Work-Profile.md @@ -1,3 +1,4 @@ # Android Enterprise Work Profile -µLauncher is compatible with [work profile](https://www.android.com/enterprise/work-profile/), so apps like [Shelter](https://gitea.angry.im/PeterCxy/Shelter) can be used. \ No newline at end of file +µLauncher is compatible with [work profile](https://www.android.com/enterprise/work-profile/), so apps like [Shelter](https://gitea.angry.im/PeterCxy/Shelter) can be used. + From 6d271970fe082a3aec726dfe4f6f418966d376ba Mon Sep 17 00:00:00 2001 From: Luke Wass Date: Wed, 30 Apr 2025 22:39:03 -0500 Subject: [PATCH 036/118] fix links --- docs/Changes-from-Finns-Launcher.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Changes-from-Finns-Launcher.md b/docs/Changes-from-Finns-Launcher.md index f08678d..30ae32b 100644 --- a/docs/Changes-from-Finns-Launcher.md +++ b/docs/Changes-from-Finns-Launcher.md @@ -48,6 +48,6 @@ The complete list of changes can be viewed [here](https://github.com/jrpie/launc --- -[original-repo]: https://github.com/finnmglas/Launcher +\[original-repo\]: [https://github.com/finnmglas/Launcher](https://github.com/finnmglas/Launcher) -[hack-font]: https://sourcefoundry.org/hack/ +\[hack-font\]: [https://sourcefoundry.org/hack/](https://sourcefoundry.org/hack/) From 106254664df7a9d90328f25c077070a3a951c532 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Fri, 9 May 2025 21:31:58 +0200 Subject: [PATCH 037/118] fix #151 - move alpha slider to the top to clarify that the format is ARGB not RGBA --- .../main/res/layout/dialog_choose_color.xml | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/app/src/main/res/layout/dialog_choose_color.xml b/app/src/main/res/layout/dialog_choose_color.xml index 90d13c3..dc7ddca 100644 --- a/app/src/main/res/layout/dialog_choose_color.xml +++ b/app/src/main/res/layout/dialog_choose_color.xml @@ -24,6 +24,21 @@ android:layout_width="match_parent" android:layout_height="10dp" /> + + + + + + - - - - - \ No newline at end of file From 49785e66f2e11fb7761ad4293ced78d140287bd1 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Fri, 9 May 2025 23:54:23 +0200 Subject: [PATCH 038/118] improved documentation --- .scripts/release.sh | 19 ++++- app/src/main/res/values/strings.xml | 4 +- docs/Contributing.md | 17 ----- docs/Gestures-and-Bindings.md | 58 --------------- docs/Work-Profile.md | 4 -- docs/actions-and-gestures.md | 71 +++++++++++++++++++ docs/{Building-from-Source.md => build.md} | 2 +- ...from-Finns-Launcher.md => changes-fork.md} | 5 +- docs/contributing.md | 24 +++++++ docs/{Home.md => home.md} | 15 ++-- docs/profiles.md | 21 ++++++ docs/{Launcher-Settings.md => settings.md} | 34 ++++++--- 12 files changed, 173 insertions(+), 101 deletions(-) delete mode 100644 docs/Contributing.md delete mode 100644 docs/Gestures-and-Bindings.md delete mode 100644 docs/Work-Profile.md create mode 100644 docs/actions-and-gestures.md rename docs/{Building-from-Source.md => build.md} (92%) rename docs/{Changes-from-Finns-Launcher.md => changes-fork.md} (89%) create mode 100644 docs/contributing.md rename docs/{Home.md => home.md} (82%) create mode 100644 docs/profiles.md rename docs/{Launcher-Settings.md => settings.md} (72%) diff --git a/.scripts/release.sh b/.scripts/release.sh index f207c87..dc6959d 100755 --- a/.scripts/release.sh +++ b/.scripts/release.sh @@ -1,9 +1,25 @@ #!/bin/bash + +# This script builds all variants of µLauncher to create a release, namely: +# - app-release.apk (GitHub release; used by F-Droid for reproducible builds) +# - launcher-accrescent.apks (Accrescent) +# - app-release.aab (Play Store) + +# This is only intended to work on my (@jrpie) computer. +# To use this script for building a fork you need to: +# - install bundletool.jar and +# - create a keystore and modify the variables below accordingly + export JAVA_HOME="/usr/lib/jvm/java-21-openjdk/" OUTPUT_DIR="$HOME/launcher-release" BUILD_TOOLS_DIR="$HOME/Android/Sdk/build-tools/35.0.0" + +# keystore for the default release KEYSTORE="$HOME/data/keys/launcher_jrpie.jks" +# keystore for the default accrescent release KEYSTORE_ACCRESCENT="$HOME/data/keys/launcher_jrpie_accrescent.jks" + +# keepassxc-password is a custom script to fetch passwords from my password manager KEYSTORE_PASS=$(keepassxc-password "android_keys/launcher") KEYSTORE_ACCRESCENT_PASS=$(keepassxc-password "android_keys/launcher-accrescent") @@ -11,12 +27,11 @@ if [[ $(git status --porcelain) ]]; then echo "There are uncommitted changes." read -p "Continue anyway? (y/n) " -n 1 -r - echo # (optional) move to a new line + echo if ! [[ $REPLY =~ ^[Yy]$ ]] then exit 1 fi - fi rm -rf "$OUTPUT_DIR" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7d29128..46fce3d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -159,9 +159,9 @@ Functionality - Double swipe actions + Double swipe gestures Swipe with two fingers - Edge swipe actions + Edge swipe gestures Swipe at the edge of the screen Edge width Launch search results diff --git a/docs/Contributing.md b/docs/Contributing.md deleted file mode 100644 index cc79e87..0000000 --- a/docs/Contributing.md +++ /dev/null @@ -1,17 +0,0 @@ -# Contributing - -## Ways to contribute - -- Found a **bug** or have an idea for a **new feature**? [Join the chat](https://s.jrpie.de/launcher-chat) or open an [issue](https://github.com/jrpie/Launcher/issues/). - - > Please note that I work on this project in my free time. Thus I might not respond immediately and not all ideas will be implemented. - -- Implement a new feature yourself: - - 1. [Fork](https://github.com/jrpie/launcher/fork) this repository. - 2. Create a **new branch** named *feature/\* or *fix/\* and commit your changes. - 3. Open a new pull request. - -- Add or improve [translations](https://toolate.othing.xyz/projects/jrpie-launcher/). - -![translation status](https://toolate.othing.xyz/widget/jrpie-launcher/launcher/horizontal-auto.svg) diff --git a/docs/Gestures-and-Bindings.md b/docs/Gestures-and-Bindings.md deleted file mode 100644 index 09ff172..0000000 --- a/docs/Gestures-and-Bindings.md +++ /dev/null @@ -1,58 +0,0 @@ -# Gestures and Bindings - -## Available Gestures - -### Swipes - -- Up, down, left, or right. -- Up, down, left, or right with two fingers. -- Up or down on the left or right edge. -- Left or right on the top or bottom edge. - -### Taps - -- Tap on date or time. -- Double tap. -- Long tap. - -### Tap-then-Swipes - -Tap then swipe up, down, left, or right - -### Complex Gestures - -- Draw <, >, V, or Λ. -- Draw <, >, V, or Λ in reverse direction. - -### Hardware Buttons as Gestures - -- Back button. -- Volume up or down button. - -*** - -## Available Bindings - -Any of the above gestures can be bound to any of the following bindings. - -### Launcher Bindings - -- Open µLauncher settings. -- Open a list of all*, favorite, or private* apps. - -### App Bindings - -- Launch an app. -- Launch another Home Screen. - -### Android / Device Bindings - -- Toggle private space lock. -- Lock the screen. -- Toggle the torch (flashlight) -- Raise or lower volume. -- Skip to previous or next audio track. -- Open Notifications shade. -- Open Quick Settings shade. - -> \* excludes apps hidden by the user diff --git a/docs/Work-Profile.md b/docs/Work-Profile.md deleted file mode 100644 index 718fe28..0000000 --- a/docs/Work-Profile.md +++ /dev/null @@ -1,4 +0,0 @@ -# Android Enterprise Work Profile - -µLauncher is compatible with [work profile](https://www.android.com/enterprise/work-profile/), so apps like [Shelter](https://gitea.angry.im/PeterCxy/Shelter) can be used. - diff --git a/docs/actions-and-gestures.md b/docs/actions-and-gestures.md new file mode 100644 index 0000000..f5d831f --- /dev/null +++ b/docs/actions-and-gestures.md @@ -0,0 +1,71 @@ +# Gestures and Actions + +µLauncher's central mechanism for accessing important functionality quickly +is to bind actions (e.g. launching an app) to gestures (e.g. swiping up). +These bindings can be configured in µLauncher Settings > ACTIONS. + + +## Available Gestures + +### Swipes + +- Basic swipes: Swipe up, down, left, or right +- Double swipes: Swipe up, down, left, or right with two fingers +- Edge swipes: + - Swipe up or down on the left or right edge + - Swipe left or right on the top or bottom edge + + The size of the edges is configurable in settings. + For a swipe to be detected as an edge swipe, the finger must not leave the respective edge region while swiping. + +### Taps + +- Tap on date or time +- Double tap +- Long click + +### Tap-then-Swipes + +- Tap then swipe up, down, left, or right + + To execute these gesture consistently, it is helpful to think of them as double taps, + where the finger stays on the screen after the second tap and then does a swipe. + The swipe must start very shortly after the tap ended. + +### Complex Gestures + +- Draw <, >, V, or Λ +- Draw <, >, V, or Λ in reverse direction + +### Hardware Buttons as Gestures + +- Back button (or back gesture if gesture navigation is enabled) +- Volume buttons + +*** + +## Available Actions + +To any of the available gestures, one of the following actions can be bound: + +- Launch an app (or a pinned shortcut) +- Open a widget panel. + Widget panels can hold widgets that are not needed on the home screen itself. + They can be created and managed in µLauncher Settings > Manage Widget Panels +- Open a list of all, favorite, or private apps (hidden apps are excluded). + Actions related to private space are only shown if private space is set up on the device. + µLauncher's settings can be accessed from those lists. + If private space is set up, an icon to (un)lock it is shown on the top right. +- Open µLauncher's settings +- Toggle private space lock +- Lock the screen: This allows to lock the screen. + There are two mechanisms by which the screen can be locked, accessibility service and device admin. +- Toggle the flashlight +- Raise, lower or adjust volume +- Play or pause media playback +- Skip to previous or next audio track +- Open notifications panel: Might be useful if the top of your screen is broken. +- Open quick settings panel: Why swipe down twice? +- Open [recent apps](https://developer.android.com/guide/components/activities/recents): Requires accessibility service. Can be used as a workaround for a Android bug. +- Launch another home screen: Allows using another installed home screen temporarily. +- Do nothing: Just prevents showing the message saying that no action is bound to this gesture. diff --git a/docs/Building-from-Source.md b/docs/build.md similarity index 92% rename from docs/Building-from-Source.md rename to docs/build.md index fc8de3d..1ffc338 100644 --- a/docs/Building-from-Source.md +++ b/docs/build.md @@ -42,4 +42,4 @@ for further instructions. How to ## CI Pipeline The [CI pipeline](https://github.com/jrpie/Launcher/actions) automatically creates debug builds. -> Note: These builds are *not* signed. +> Note: These builds are *not* signed. They are in built in debug mode and only suitable for testing. diff --git a/docs/Changes-from-Finns-Launcher.md b/docs/changes-fork.md similarity index 89% rename from docs/Changes-from-Finns-Launcher.md rename to docs/changes-fork.md index 30ae32b..8efc965 100644 --- a/docs/Changes-from-Finns-Launcher.md +++ b/docs/changes-fork.md @@ -14,12 +14,15 @@ The decision to create a hard fork was made two years later.--> - Compatible with [work profile](https://www.android.com/enterprise/work-profile/), so apps like [Shelter](https://gitea.angry.im/PeterCxy/Shelter) can be used. - Compatible with [private space](https://source.android.com/docs/security/features/private-space) +- Support for [app widgets](https://developer.android.com/develop/ui/views/appwidgets/overview) +- Support for [pinned shortcuts](https://developer.android.com/develop/ui/views/launch/shortcuts/creating-shortcuts) - Option to rename apps - Option to hide apps - Favorite apps - New actions: - Toggle Torch - Lock screen + - Open a widget panel - The home button now works as expected. - Improved gesture detection. @@ -48,6 +51,6 @@ The complete list of changes can be viewed [here](https://github.com/jrpie/launc --- -\[original-repo\]: [https://github.com/finnmglas/Launcher](https://github.com/finnmglas/Launcher) +\[original-repo\]: [https://github.com/finnmglas/Launcher](https://github.com/finnmglas/Launcher) \[hack-font\]: [https://sourcefoundry.org/hack/](https://sourcefoundry.org/hack/) diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..8e9de53 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,24 @@ +# Contributing + +There are several ways to contribute to this app: +* You can add or improve [translations][toolate]. +
translation status +* If you found a bug or have an idea for a new feature you can [join the chat][chat] or open an [issue][issues]. + + > Please note that I work on this project in my free time. Thus I might not respond immediately and not all ideas will be implemented. + +* You can implement a new feature yourself: + - Create a [fork][fork] of this repository. + - Create a new branch named `feature/` or `fix/` and commit your changes. + - Open a new pull request. + + +See [build.md](build.md) for instructions how to build this project. +The [CI pipeline](https://github.com/jrpie/Launcher/actions) automatically creates debug builds. + + +--- + [fork]: https://github.com/jrpie/Launcher/fork/ + [issues]: https://github.com/jrpie/Launcher/issues/ + [chat]: https://s.jrpie.de/launcher-chat + [toolate]: https://toolate.othing.xyz/projects/jrpie-launcher/ diff --git a/docs/Home.md b/docs/home.md similarity index 82% rename from docs/Home.md rename to docs/home.md index 2cb6093..6ddf2b5 100644 --- a/docs/Home.md +++ b/docs/home.md @@ -1,12 +1,12 @@ -# Welcome to the μLauncher wiki +# Welcome to the μLauncher Documentation -We're thrilled you're here and confident you'll love your new Android launcher! Check out this wiki to get familiar with your new app. +## What is μLauncher? -## What is μLauncher +µLauncher is an *minimal* and *distraction-free* Android home screen that lets you launch apps using [swipe gestures and button presses](docs/actions-and-gestured.md). -µLauncher is an *minimal* and *distraction-free* Android home screen that lets you launch apps using *efficient* swipe gestures and button presses. This project is a fork of [finnmglas's app Launcher](https://github.com/finnmglas/Launcher). An incomplete list of changes can be found [here](https://github.com/wassupluke/Launcher/blob/master/docs/launcher.md). +This project is a fork of [finnmglas's app Launcher](https://github.com/finnmglas/Launcher). An incomplete list of changes can be found [here](https://github.com/wassupluke/Launcher/blob/master/docs/launcher.md). -## Where can I get μLauncher +## Where can I get μLauncher? [![Get it on F-Droid](https://fdroid.gitlab.io/artwork/badge/get-it-on.png)](https://f-droid.org/packages/de.jrpie.android.launcher/) @@ -18,6 +18,11 @@ We're thrilled you're here and confident you'll love your new Android launcher! > You can also [get it on Google Play](https://play.google.com/store/apps/details?id=de.jrpie.android.launcher), but this is not recommend. + +## How can I contribute? + +See [docs/contribute](docs/contribute.md) + ## Screenshots ![μLauncher Home Screen screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg) diff --git a/docs/profiles.md b/docs/profiles.md new file mode 100644 index 0000000..d9eaf52 --- /dev/null +++ b/docs/profiles.md @@ -0,0 +1,21 @@ +# Work Profile + +µLauncher is compatible with [work profile](https://www.android.com/enterprise/work-profile/), so apps like [Shelter](https://gitea.angry.im/PeterCxy/Shelter) can be used. +Apps from the work profile are shown in the usual app list. + + +# Private Space + +µLauncher is compatible with [private space](https://source.android.com/docs/security/features/private-space). + + +The private space can be (un)locked by a dedicated action. + +It is configurable whether apps from private space are + +1. shown in the usual app list + (in this case, (un)locking is accessible through a lock icon on the top right of the app drawer) + or +2. only shown in a separate list. + + diff --git a/docs/Launcher-Settings.md b/docs/settings.md similarity index 72% rename from docs/Launcher-Settings.md rename to docs/settings.md index 1444c73..f033f62 100644 --- a/docs/Launcher-Settings.md +++ b/docs/settings.md @@ -4,13 +4,14 @@ Tweaks and customizations can be made from within the Launcher Settings page. These settings let you change wallpapers, change colors and fonts, enable monochrome app icons, change the app drawer layout, and much more. -In the following documentation, 'app drawer' will be used to refer to the 'All Apps' and 'Favorite Apps' views. +In the following documentation, 'app drawer' will be used to refer to the 'All Apps', 'Favorite Apps' and 'Private Space' views. ## Appearance > ### Choose a wallpaper -Lets you change the wallpaper using a photos app, file explorer, or native wallpaper setting app. +This triggers Android's mechanism to change the wallpaper using a photos app, file explorer, or native wallpaper setting app. +µLauncher uses the system-wide wallpaper, i.e. this change also affects other launchers. > ### Font (in-app font) @@ -92,11 +93,15 @@ Place the current time above the current date on the home screen. Launches any app that matches user keyboard input when no other apps match. -As you type inside the app drawer, the app narrows down the list of apps shown based on the app title matching your text input. For example, if you type, `a`, the app list narrows to any apps with a title containing the letter `a`. Continuing the example, if you then follow your `a` with the letter `m`, the list now shows only apps containing the letter combination `am` in that order. If the only app matching this combination was, for example, `Amazon`, simply typing `am` in the app drawer would immediately launch the `Amazon` app for you. +As you type inside the app drawer, the app narrows down the list of apps shown based on the app title matching your text input. +With the 'launch search results' setting, once only one matching app remains, it is launched immediately. +Usually it suffices to type two or three characters the single out the desired app. -This feature becomes more powerful when combined with [renaming](https://github.com/wassupluke/Launcher/wiki/Launcher-Settings/_edit#additional-settings) apps, effectively letting you define custom app names that could be considered 'aliases' or shortcuts. For instance, if you wanted to "bind" the keyboard input `gh` to open your `GitHub` app, you could rename `GitHub` to `GitHub gh`, `gh GitHub`, or simply `gh`. Assuming no other installed apps have the `gh` combination of letters in them, opening the app drawer and typing `gh` would immediately launch your `GitHub` app. +This feature becomes more powerful when combined with [renaming](#additional-settings) apps, effectively letting you define custom app names that could be considered 'aliases' or shortcuts. +For instance, if you want the keyboard input `gh` to open your `GitHub` app, you could rename `GitHub` to `GitHub gh`, `gh GitHub`, or simply `gh`. +Assuming no other installed apps have the `gh` combination of letters in them, opening the app drawer and typing `gh` would immediately launch your `GitHub` app. -Press space to temporarily disable this feature and allow text entry without prematurely launching an app. Useful when combined with the [Search the web](https://github.com/wassupluke/Launcher/wiki/Launcher-Settings/_edit#search-the-web) feature. +Press space to temporarily disable this feature and allow text entry without prematurely launching an app. Useful when combined with the [Search the web](#search-the-web) feature. **type:** `toggle` @@ -112,15 +117,15 @@ Automatically open the keyboard when the app drawer is opened. **type:** `toggle` -> ### Double swipe actions +> ### Double swipe gestures -Enable double swipe (two finger) actions as bindable gestures in launcher settings. Does not erase gesture bindings if accidentally turned off. +Enable double swipe (two finger) gestures in launcher settings. Does not erase gesture bindings if accidentally turned off. **type:** `toggle` -> ### Edge swipe actions +> ### Edge swipe gestures -Enable edge swipe (near edges of screen) actions as bindable gestures in launcher settings. Does not erase gesture bindings if accidentally turned off. +Enable edge swipe (near edges of screen) gestures in launcher settings. Does not erase gesture bindings if accidentally turned off. **type:** `toggle` @@ -167,6 +172,7 @@ Reduces redundancy and tidies up app drawer. > ### Hide paused apps Remove paused apps from the app drawer. +For example an app belonging to the work profile is paused when the work profile is inactive. **type:** `toggle` @@ -178,7 +184,12 @@ Remove private space from app drawer. > ### Layout of app list -Change how the apps are displayed when accessing the app drawer. By `Default`, all apps in the drawer will show in a vertically-scrolled list with their app icon and title. `Text` removes the app icons, shows only app titles in the drawer as a vertically-scrolled list. `Grid` shows apps with their app icon and title in a grid layout. +Changes how the apps are displayed when accessing the app drawer. + +- `Default`: All apps in the drawer will show in a vertically-scrolled list with their app icon and title. +- `Text`: Removes the app icons, shows only app titles in the drawer as a vertically-scrolled list. + Work profile and private space apps are distinguished by a different label instead of a badge. +- `Grid`: Shows apps with their app icon and title in a grid layout. **type:** `dropdown` @@ -186,7 +197,8 @@ Change how the apps are displayed when accessing the app drawer. By `Default`, a > ### Reverse the app list -Enable Z-A sorting of apps in the app drawer. Useful for keeping apps within easier reach from the keyboard. +Enable reverse alphabetical sorting of apps in the app drawer. +Useful for keeping apps within easier reach from the keyboard. **type:** `toggle` From 24e90deb627f76c2d528fbc12510ea4825dc9a98 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sat, 10 May 2025 00:52:56 +0200 Subject: [PATCH 039/118] improve widget management --- .../de/jrpie/android/launcher/Application.kt | 5 --- .../manage/ManageWidgetPanelsActivity.kt | 1 - .../widgets/manage/ManageWidgetsActivity.kt | 5 +-- .../ui/widgets/manage/WidgetManagerView.kt | 32 ++++++++++++------- .../jrpie/android/launcher/widgets/Widget.kt | 3 +- 5 files changed, 25 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/de/jrpie/android/launcher/Application.kt b/app/src/main/java/de/jrpie/android/launcher/Application.kt index 775621c..3c2e3bc 100644 --- a/app/src/main/java/de/jrpie/android/launcher/Application.kt +++ b/app/src/main/java/de/jrpie/android/launcher/Application.kt @@ -22,8 +22,6 @@ import de.jrpie.android.launcher.apps.isPrivateSpaceLocked import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.migratePreferencesToNewVersion import de.jrpie.android.launcher.preferences.resetPreferences -import de.jrpie.android.launcher.widgets.LauncherWidgetProvider -import de.jrpie.android.launcher.widgets.Widget import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -34,7 +32,6 @@ const val APP_WIDGET_HOST_ID = 42; class Application : android.app.Application() { val apps = MutableLiveData>() - val widgets = MutableLiveData>() val privateSpaceLocked = MutableLiveData() lateinit var appWidgetHost: AppWidgetHost lateinit var appWidgetManager: AppWidgetManager @@ -101,8 +98,6 @@ class Application : android.app.Application() { customAppNames = LauncherPreferences.apps().customNames() } else if (pref == LauncherPreferences.apps().keys().pinnedShortcuts()) { loadApps() - } else if (pref == LauncherPreferences.widgets().keys().widgets()) { - widgets.postValue(LauncherPreferences.widgets().widgets() ?: setOf()) } } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetPanelsActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetPanelsActivity.kt index b18852f..cb57fda 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetPanelsActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetPanelsActivity.kt @@ -49,7 +49,6 @@ class ManageWidgetPanelsActivity : AppCompatActivity(), UIObject { }) } binding.manageWidgetPanelsRecycler.apply { - // improve performance (since content changes don't change the layout size) setHasFixedSize(true) layoutManager = viewManager adapter = viewAdapter diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt index d191b70..665a851 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt @@ -34,10 +34,11 @@ class ManageWidgetsActivity : Activity(), UIObject { var panelId: Int = WidgetPanel.HOME.id + + // We can't observe the livedata because this is not an AppCompatActivity private var sharedPreferencesListener = SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey -> if (prefKey == LauncherPreferences.widgets().keys().widgets()) { - // We can't observe the livedata because this is not an AppCompatActivity findViewById(R.id.manage_widgets_container).updateWidgets(this, LauncherPreferences.widgets().widgets() ) @@ -63,7 +64,7 @@ class ManageWidgetsActivity : Activity(), UIObject { findViewById(R.id.manage_widgets_container).let { it.widgetPanelId = panelId - it.updateWidgets(this, (application as Application).widgets.value) + it.updateWidgets(this, LauncherPreferences.widgets().widgets()) } } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt index 7a355f7..f6c6c0d 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt @@ -19,6 +19,7 @@ import androidx.core.graphics.minus import androidx.core.graphics.toRect import androidx.core.view.children import de.jrpie.android.launcher.ui.widgets.WidgetContainerView +import de.jrpie.android.launcher.widgets.GRID_SIZE import de.jrpie.android.launcher.widgets.Widget import de.jrpie.android.launcher.widgets.WidgetPanel import de.jrpie.android.launcher.widgets.WidgetPosition @@ -47,21 +48,27 @@ class WidgetManagerView(widgetPanelId: Int, context: Context, attrs: AttributeSe - enum class EditMode(val resize: (dx: Int, dy: Int, rect: Rect) -> Rect) { - MOVE({ dx, dy, rect -> - Rect(rect.left + dx, rect.top + dy, rect.right + dx, rect.bottom + dy) + enum class EditMode(val resize: (dx: Int, dy: Int, screenWidth: Int, screenHeight: Int, rect: Rect) -> Rect) { + MOVE({ dx, dy, sw, sh, rect -> + val cdx = dx.coerceIn(-rect.left, sw - rect.right) + val cdy = dy.coerceIn(-rect.top, sh - rect.bottom) + Rect(rect.left + cdx, rect.top + cdy, rect.right + cdx, rect.bottom + cdy) }), - TOP({ dx, dy, rect -> - Rect(rect.left, min(rect.top + dy, rect.bottom - 200), rect.right, rect.bottom) + TOP({ dx, dy, sw, sh, rect -> + val cdy = dy.coerceIn(-rect.top, rect.bottom - rect.top - (2 * sh / GRID_SIZE) + 5) + Rect(rect.left, rect.top + cdy, rect.right, rect.bottom) }), - BOTTOM({ dx, dy, rect -> - Rect(rect.left, rect.top, rect.right, max(rect.top + 200, rect.bottom + dy)) + BOTTOM({ dx, dy, sw, sh, rect -> + val cdy = dy.coerceIn((2 * sh / GRID_SIZE) + 5 + rect.top - rect.bottom, sh - rect.bottom) + Rect(rect.left, rect.top, rect.right, rect.bottom + cdy) }), - LEFT({ dx, dy, rect -> - Rect(min(rect.left + dx, rect.right - 200), rect.top, rect.right, rect.bottom) + LEFT({ dx, dy, sw, sh, rect -> + val cdx = dx.coerceIn(-rect.left, rect.right - rect.left - (2 * sw / GRID_SIZE) + 5) + Rect(rect.left + cdx, rect.top, rect.right, rect.bottom) }), - RIGHT({ dx, dy, rect -> - Rect(rect.left, rect.top, max(rect.left + 200, rect.right + dx), rect.bottom) + RIGHT({ dx, dy, sw, sh, rect -> + val cdx = dx.coerceIn((2 * sw / GRID_SIZE) + 5 + rect.left - rect.right, sw - rect.right) + Rect(rect.left, rect.top, rect.right + cdx, rect.bottom) }), } @@ -120,6 +127,7 @@ class WidgetManagerView(widgetPanelId: Int, context: Context, attrs: AttributeSe val absoluteNewPosition = view.mode?.resize( distanceX.toInt(), distanceY.toInt(), + width, height, start ) ?: return true val newPosition = WidgetPosition.fromAbsoluteRect( @@ -162,7 +170,7 @@ class WidgetManagerView(widgetPanelId: Int, context: Context, attrs: AttributeSe if (widgets == null) { return } - children.mapNotNull { it as? WidgetOverlayView }.forEach { removeView(it) } + children.filter { it is WidgetOverlayView }.forEach { removeView(it) } widgets.filter { it.panelId == widgetPanelId }.forEach { widget -> WidgetOverlayView(activity).let { diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt index dbe667b..e31250b 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt @@ -57,7 +57,8 @@ sealed class Widget { return Json.decodeFromString(serialized) } fun byId(context: Context, id: Int): Widget? { - return (context.applicationContext as Application).widgets.value?.firstOrNull { + // TODO: do some caching + return LauncherPreferences.widgets().widgets().firstOrNull() { it.id == id } } From f5b8953601016816fad92042d5d95c55709db756 Mon Sep 17 00:00:00 2001 From: Sven van de Lagemaat Date: Mon, 14 Apr 2025 08:14:03 +0000 Subject: [PATCH 040/118] Added translation using Weblate (Dutch) --- app/src/main/res/values-nl/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 app/src/main/res/values-nl/strings.xml diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml new file mode 100644 index 0000000..a6b3dae --- /dev/null +++ b/app/src/main/res/values-nl/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From ba2c117eb5bbd88a186d0b745011c21679cdc58a Mon Sep 17 00:00:00 2001 From: Sven van de Lagemaat Date: Mon, 14 Apr 2025 08:16:32 +0000 Subject: [PATCH 041/118] Translated using Weblate (Dutch) Currently translated at 14.2% (3 of 21 strings) Translation: jrpie-Launcher/metadata Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/metadata/nl/ --- fastlane/metadata/android/nl-NL/full_description.txt | 4 ++++ fastlane/metadata/android/nl-NL/short_description.txt | 1 + fastlane/metadata/android/nl-NL/title.txt | 1 + 3 files changed, 6 insertions(+) create mode 100644 fastlane/metadata/android/nl-NL/full_description.txt create mode 100644 fastlane/metadata/android/nl-NL/short_description.txt create mode 100644 fastlane/metadata/android/nl-NL/title.txt diff --git a/fastlane/metadata/android/nl-NL/full_description.txt b/fastlane/metadata/android/nl-NL/full_description.txt new file mode 100644 index 0000000..3040dfd --- /dev/null +++ b/fastlane/metadata/android/nl-NL/full_description.txt @@ -0,0 +1,4 @@ +µLauncher is een thuisscherm die je andere apps laat starten met gebruik van veeg gebaren en knoppen indrukken. +Het is minimalistisch, efficiënt en vrij van afleiding. + +Je thuisscherm laat alleen de datum, tijd en achtergrond zien. diff --git a/fastlane/metadata/android/nl-NL/short_description.txt b/fastlane/metadata/android/nl-NL/short_description.txt new file mode 100644 index 0000000..43e1dbf --- /dev/null +++ b/fastlane/metadata/android/nl-NL/short_description.txt @@ -0,0 +1 @@ +Een afleidingsvrije, minimalistisch thuisscherm voor Android. diff --git a/fastlane/metadata/android/nl-NL/title.txt b/fastlane/metadata/android/nl-NL/title.txt new file mode 100644 index 0000000..4305604 --- /dev/null +++ b/fastlane/metadata/android/nl-NL/title.txt @@ -0,0 +1 @@ +µLauncher From b9fa079048b5e24a4efd73c7b9b5d0338fbcd46b Mon Sep 17 00:00:00 2001 From: Vladi69 Date: Sat, 19 Apr 2025 09:45:17 +0000 Subject: [PATCH 042/118] Translated using Weblate (Italian) Currently translated at 87.9% (227 of 258 strings) Translation: jrpie-Launcher/Launcher Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/it/ --- app/src/main/res/values-it/strings.xml | 201 +++++++++++++++---------- 1 file changed, 121 insertions(+), 80 deletions(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 318ad43..33ab7dd 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -2,15 +2,15 @@ Scorri verso destra sul bordo inferiore dello schermo Aspetto - Scegliere + Imposta app Tema - Questo launcher è progettato per essere minimale, efficiente e privo di distrazioni. Non contiene pagamenti, pubblicità o servizi di tracciamento. + μLauncher is designed to be minimal, efficient and free of distraction. \n\nIt contains no ads and collects no data. Predefinito - Non mostrare applicazioni collegate a gesti nella lista delle app + Non mostrare applicazioni abbinate a gesti nella lista delle app Testo Impostare µLauncher come servizio accessibilità permette l\'azione di blocco dello schermo. Si precisa che sono richiesti permessi eccessivi. Non si dovrebbe mai concedere con leggerezza permessi ad alcuna app. µLauncher utilizza i servizi di accessibilità esclusivamente per il blocco dello schermo. Puoi verificare il codice sorgente. Si segnala che il blocco schermo si può attivare anche concedendo a µLauncher i permessi di amministratore, tuttavia tale metodo non funziona con lo sblocco tramite impronta o riconoscimento facciale. - Sans serif (senza grazie) - Trascina due dita dal basso verso l\'alto + Sans serif + Scorri con due dita verso l\'alto Negozio non trovato Griglia


Puoi cambiare le tue scelte in seguito nelle impostazioni. ]]>
- Impossibile aprire l\'applicazione - Desideri modificare le impostazioni? - Apri le impostazioni per abbinare un\'azione a questo gesto + Impossibile avviare l\'app + Modificare le impostazioni? + Apri impostazioni per associare un\'azione a questo gesto Impostazioni Launcher Meta Scorri verso il basso con due dita Sinistra - Scorrere verso sinistra - Due dita verso sinistra - Scorrere verso sinistra con due dita + Scorri verso sinistra + Due dita a sx + Scorri verso sinistra con due dita Destra Scorri verso destra - Due dita verso destra - Scorri a destra con due dita + Due dita verso dx + Scorri verso destra con due dita Destra (in alto) - Scorri verso destra sul bordo superiore dello schermo + Scorri verso destra sul bordo alto dello schermo Scorri verso sinistra sul bordo inferiore dello schermo Scorri verso sinistra sul bordo superiore dello schermo - Verso l\'alto (lato sinistro) - Verso l\'alto - Striscia il dito dal basso verso l\'alto - Verso il basso - Trascina due dita dall\'alto verso il basso - Trascina un dito dall\'alto verso il basso - Destra (bordo inferiore) - Sinistra (bordo inferiore) - Sinistra (bordo superiore) + Su (bordo sinistro) + Su + Scorri su + Giù + Scorri con due dita giù + Scorri verso il basso + Destra (in basso) + Sinistra (in basso) + Sinistra (in alto) Scorri verso l\'alto sul bordo sinistro dello schermo - Alto (lato destro) + Su (bordo destro) Scorri verso l\'alto sul bordo destro dello schermo - Basso (bordo sinistro) + Giù (bordo sinistro) Scorri verso il basso sul bordo sinistro dello schermo - Basso (Lato destro) - Scorri verso il basso sul lato destro dello schermo - Aumenta il volume - Doppio click - Doppio click in un\'area vuota - Tocco prolungato - Tocco prolungato su un\'area vuota + Giù (bordo destro) + Scorri verso il basso sul bordo destro dello schermo + Volume + + Doppio tap + Doppio tap in spazio vuoto + Tap lungo + Tap lungo in spazio vuoto Data - Premi sulla data + Tap sulla data Ora - Premi sull\'ora - Premi il pulsante di aumento del volume - Riduci il volume - Premi il pulsante per ridurre il volume - Installa le applicazioni + Tap sull\'ora + Premi il pulsante Volume + + Volume - + Premi il pulsante Volume - + Installa apps Icone monocromatiche - Mostra l\'ora - Mostra la data - Usa il formato ora locale + Mostra orario + Mostra data + Usa formato data locale Mostra i secondi Scorri con due dita - Azioni a scorrimento con due dita + Azioni di scorrimento a due dita Apri il risultato della ricerca - Azioni a scorrimento ai lati dello schermo - Scorri sul lato dello schermo - Larghezza margine laterale - Vedi il codice sorgente + Azioni di scorrimento sui bordi dello schermo + Scorri sui bordi dello schermo + Larghezza bordo + Vai al codice sorgente Tutte le applicazioni Applicazioni preferite - Musica: passa alla traccia successiva - Musica: riduci il volume - Musica: torna alla traccia precedente - Musica: Aumenta il volume + Musica: traccia successiva + Volume - + Musica: traccia precedente + Volume + Annulla Impostazioni rapide Azione necessaria per abilitare il blocco dello schermo. - Ombreggiatura del testo - Sfondo (lista applicazioni e impostazioni) + Ombreggiatura testo + Sfondo (lista apps e impostazioni) Font Inverti data e ora - Scegli uno sfondo + Imposta sfondo Schermo - Mantieni lo schermo acceso - Ruota lo schermo - Funzionalità - Apri automaticamente la tastiera per cercare + Mantieni schermo acceso + Ruota schermo + Funzioni + Apri automaticamente la tastiera Sensibilità - Scegli un\'applicazione + Scegli applicazione Configurazione Abbiamo impostato alcune app predefinite per te. Puoi modificarle ora se lo desideri: Puoi anche cambiare la tua selezione in seguito. @@ -145,22 +145,22 @@ Sfocato Solido Predefinito di sistema - Serif (con grazie) - Monospace (a larghezza fissa) - Serif monospace (a larghezza fissa con grazie) - Applicazioni + Serif + Monospace + Serif monospace + Apps Applicazioni nascoste - Configurazione della lista applicazioni + Configura la lista applicazioni Predefinito Offuscato - Imposta μLauncher come predefinito per la schermata principale - Informazioni sulle applicazioni - Apri il tutorial del launcher - Ripristina le impostazioni predefinite - Stai per eliminare tutte le preferenze impostate. Vuoi continuare? + Imposta μLauncher come launcher predefinito + Informazioni app + Apri il tutorial di µLauncher + Ripristina le impostazioni + Stai per eliminare le preferenze impostate. Continuare? Segnala un bug Contatta lo sviluppatore del fork - Partecipa alla chat di μLauncher + Unisciti alla chat di μLauncher Informativa sulla privacy Contatta lo sviluppatore originale Unisciti a noi su Discord! @@ -168,7 +168,7 @@ Applicazioni preferite Applicazioni nascoste Applicazioni - Altri + Altro Disinstalla Informazioni sull\'app Aggiungi ai preferiti @@ -178,21 +178,21 @@ Rinomina Cerca Impostazioni μLauncher - Espandi il pannello notifiche + Espandi pannello notifiche Non fare niente - Blocca lo schermo - Accendi/spegni la torcia + Blocca schermo + Torcia ON/OFF Tutorial - Prenditi qualche secondo per imparare ad usare questo launcher! + 👋\n\nPrenditi qualche secondo per imparare ad usare questo launcher! Concetto - L\'app è open source (sotto licenza MIT) e disponibile su GitHub! Visita il nostro archivio! + E\' software libero (licenza MIT)!\nAssicurati di controllare il repository! Utilizzo La schermata principale contiene solo data e ora. Nessuna distrazione. Questa funzione richiede Android 6 o successivi. Rinomina %1$s Dinamico Colore - Due dita verso l\'alto + Scorri con due dita verso l\'alto Sono consapevole che questo concederà privilegi estesi a µLauncher. Accetto che µLauncher utilizzi il servizio di accessibilità per fornire funzionalità non correlate all\'accessibilità. Accetto che µLauncher non raccolga alcun dato. @@ -208,17 +208,17 @@ Attiva Servizi di Accessibilità Sono consapevole che esistono altre opzioni (utilizzando i privilegi di amministratore del dispositivo o il pulsante di accensione). Attivazione dei Servizi di Accessibilità - Cerca su internet - Premi invio durante la ricerca nell\'elenco delle app per avviare una ricerca su internet. + Cerca sul web + Premi invio durante la ricerca delle app per avviare una ricerca web. Cerca (senza avvio automatico) Licenze Open Source Licenze Open Source Segnala un bug - Grazie per aver contribuito a migliorare µLauncher!\nSi prega di aggiungere le seguenti informazioni alla segnalazione del bug: + Grazie per il tuo contributo al miglioramento di µLauncher!\nAggiungi le seguenti informazioni alla segnalazione del bug: Copia negli appunti Non segnalare pubblicamente le vulnerabilità di sicurezza su GitHub, ma utilizza invece: Annulla - Premi spazio per disabilitare temporaneamente questa funzionalità. + Premi spazio per disabilitare temporaneamente questa funzione Segnala una vulnerabilità di sicurezza Crea una segnalazione Spazio privato bloccato @@ -228,4 +228,45 @@ Impossibile aprire l\'URL: nessun browser trovato. Non è stata trovata un\'applicazione per gestire la ricerca. privilegi più ampi a µLauncher.
µLauncher utilizzerà questi privilegi solo per bloccare lo schermo. µLauncher non raccoglierà mai alcun dato. In particolare, µLauncher non usa il servizio di accessibilità per raccogliere nessun dato.]]>
+ Spazio privato + Spazio privato + Regola volume + Associa al gesto + Nascondi barra di stato + Nascondi barra di navigazione + Dona + App recenti + Tap + Su + Tap e scorri su + Tap + Giù + Tap e scorri verso il basso + Tap + Sinistra + Tap + Destra + Alto sx -> centro dx -> basso sx + Tap e scorri vesro sinistra + Tap e scorri verso destra + (Inverso)]]> + Basso sx -> centro dx -> alto sx + Alto dx -> centro sx -> basso dx + V + Alto sx -> centro basso -> alto dx + V (Reverse) + Alto dx -> centro basso -> alto sx + Λ + Basso sx -> centro alto -> basso dx + Basso dx -> centro alto -> basso sx + + + Λ (Inverso) + "Musica: Riproduci / Pausa" + Inverti la lista applicazioni + Azioni + Pulsante / gesto indietro + Indietro + Basso dx -> centro sx -> alto dx + Nascondi lo spazio privato dalla lista app + Lancia un altro launcher + Aggiungi scorciatoia + ]]> + Mostra in lista app
From 3d4bddbb4c5015373c8b8b8f140d218bf5898e13 Mon Sep 17 00:00:00 2001 From: Vladi69 Date: Sat, 19 Apr 2025 21:23:55 +0000 Subject: [PATCH 043/118] Translated using Weblate (Italian) Currently translated at 98.0% (253 of 258 strings) Translation: jrpie-Launcher/Launcher Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/it/ --- app/src/main/res/values-it/strings.xml | 138 +++++++++++-------------- 1 file changed, 63 insertions(+), 75 deletions(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 33ab7dd..de71a7b 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,39 +1,19 @@ - Scorri verso destra sul bordo inferiore dello schermo + Scorri a destra sul bordo inferiore dello schermo Aspetto - Imposta app + Associa app Tema - μLauncher is designed to be minimal, efficient and free of distraction. \n\nIt contains no ads and collects no data. + μLauncher è progettato per essere minimale, efficiente e privo di distrazione.\n\nNon contiene annunci e non raccoglie dati. Predefinito - Non mostrare applicazioni abbinate a gesti nella lista delle app + Non mostrare nella lista le applicazioni associate a gesti Testo - Impostare µLauncher come servizio accessibilità permette l\'azione di blocco dello schermo. Si precisa che sono richiesti permessi eccessivi. Non si dovrebbe mai concedere con leggerezza permessi ad alcuna app. µLauncher utilizza i servizi di accessibilità esclusivamente per il blocco dello schermo. Puoi verificare il codice sorgente. Si segnala che il blocco schermo si può attivare anche concedendo a µLauncher i permessi di amministratore, tuttavia tale metodo non funziona con lo sblocco tramite impronta o riconoscimento facciale. + Impostare µLauncher come servizio accessibilità permette l\'azione di blocco dello schermo. Nota che sono richiesti permessi eccessivi. Non si dovrebbero mai concedere con leggerezza permessi ad alcuna app. µLauncher utilizza i servizi di accessibilità esclusivamente per il blocco dello schermo. Puoi verificare il codice sorgente. Nota che il blocco schermo si può attivare anche concedendo a µLauncher i permessi di amministratore, tuttavia tale metodo non funziona con lo sblocco tramite impronta o riconoscimento facciale. Sans serif - Scorri con due dita verso l\'alto - Negozio non trovato + Scorri verso l\'alto con due dita + Store non trovato Griglia - Scegli un sistema di blocco - Esistono due modi di bloccare lo schermo. - Sfortunatamente entrambi presentano svantaggi:

- -

Amministratore Dispositivo

- Non funziona con impronta e riconoscimento facciale. - -
-
- -

Servizio Accessibilità

- Richiede privilegi eccessivi. - µLauncher userà quei privilegi esclusivamente per bloccare lo schermo. -
- (Non ci si dovrebbe mai fidare di un\'applicazione qualsiasi appena scaricata su queste cose, ma puoi verificare il codice sorgente.) - - -



- Puoi cambiare le tue scelte in seguito nelle impostazioni. - ]]>
+ "<a href=\"https://issuetracker.google.com/issues/37010136#comment36\"><![CDATA[\n <h1>Scegli un sistema di blocco</h1>\n Esistono due modi di bloccare lo schermo.\n Sfortunatamente entrambi presentano svantaggi:<br/><br/>\n\n <h3>Amministratore Dispositivo</h3>\n Non funziona con impronta e riconoscimento facciale.\n\n <br/>\n <br/>\n\n <h3>Servizio Accessibilità</h3>\n Richiede privilegi eccessivi.\n µLauncher userà quei privilegi esclusivamente per bloccare lo schermo.\n <br/>\n (Non ci si dovrebbe mai fidare di un\\\'applicazione qualsiasi appena scaricata su queste cose, ma puoi verificare il <a href=\\\"https://github.com/jrpie/Launcher\\\">codice sorgente</a>.)\n\n In alcuni dispositivi, il PIN iniziale non verrà più utilizzato per la crittografia dei dati dopo aver attivato un servizio di accessibilità.\n\n Può essere <a href=\"https://issuetracker.google.com/issues/37010136#comment36\">riattivato</a> dopo.\n\n\n <br/><br/><br/><br/>\n Puoi cambiare le tue scelte in seguito nelle impostazioni.\n ]]>" Impossibile avviare l\'app Modificare le impostazioni? Apri impostazioni per associare un\'azione a questo gesto @@ -43,32 +23,32 @@ Scorri verso il basso con due dita Sinistra Scorri verso sinistra - Due dita a sx + Due dita a sinistra Scorri verso sinistra con due dita Destra Scorri verso destra - Due dita verso dx + Due dita a destra Scorri verso destra con due dita Destra (in alto) - Scorri verso destra sul bordo alto dello schermo - Scorri verso sinistra sul bordo inferiore dello schermo - Scorri verso sinistra sul bordo superiore dello schermo + Scorri a destra sul bordo superiore dello schermo + Scorri a sinistra sul bordo inferiore dello schermo + Scorri a sinistra sul bordo superiore dello schermo Su (bordo sinistro) Su - Scorri su + Scorri verso l\'alto Giù - Scorri con due dita giù + Scorri giù con due dita Scorri verso il basso Destra (in basso) Sinistra (in basso) Sinistra (in alto) - Scorri verso l\'alto sul bordo sinistro dello schermo + Scorri in alto sul bordo sinistro dello schermo Su (bordo destro) - Scorri verso l\'alto sul bordo destro dello schermo + Scorri in alto sul bordo destro dello schermo Giù (bordo sinistro) - Scorri verso il basso sul bordo sinistro dello schermo + Scorri in basso sul bordo sinistro dello schermo Giù (bordo destro) - Scorri verso il basso sul bordo destro dello schermo + Scorri in basso sul bordo destro dello schermo Volume + Doppio tap Doppio tap in spazio vuoto @@ -86,55 +66,55 @@ Mostra orario Mostra data Usa formato data locale - Mostra i secondi + Mostra secondi Scorri con due dita - Azioni di scorrimento a due dita + Azioni a due dita Apri il risultato della ricerca - Azioni di scorrimento sui bordi dello schermo + Azioni sui bordi dello schermo Scorri sui bordi dello schermo Larghezza bordo - Vai al codice sorgente + Codice sorgente Tutte le applicazioni Applicazioni preferite Musica: traccia successiva - Volume - + Abbassa volume Musica: traccia precedente - Volume + + Alza volume Annulla Impostazioni rapide - Azione necessaria per abilitare il blocco dello schermo. + Azione necessaria per abilitare il blocco schermo. Ombreggiatura testo Sfondo (lista apps e impostazioni) Font Inverti data e ora Imposta sfondo Schermo - Mantieni schermo acceso + Mantieni lo schermo acceso Ruota schermo Funzioni Apri automaticamente la tastiera Sensibilità Scegli applicazione Configurazione - Abbiamo impostato alcune app predefinite per te. Puoi modificarle ora se lo desideri: - Puoi anche cambiare la tua selezione in seguito. + Abbiamo impostato alcune app predefinite. Puoi modificarle ora se vuoi: + Puoi anche modificare la selezione in seguito. Iniziamo! - Sei pronto per iniziare! Spero questa applicazione ti risulti preziosa! - Finn (che ha ideato il launcher)\n \te Josia (che ha aggiunto qualche miglioramento e mantiene il fork μLauncher) + Sei pronto per iniziare!\n\nSpero che lo apprezzi!\n\n- Finn (che ha fatto Launcher) e Josia (che ha apportato alcuni miglioramenti e mantiene il fork μLauncher) Inizia Impostazioni Altre opzioni - Puoi aprire le tue app facendo scorrere il dito sullo schermo o premendo un pulsante. Configura i gesti nella prossima slide. - Errore: impossibile espandere la barra di stato. Questa azione utilizza funzionalità non incluse nelle API Android pubbliche. Sfortunatamente, non sembra funzionare sul tuo dispositivo. - Applicazione nascosta. Puoi renderla nuovamente visibile nelle impostazioni. - µLauncher deve essere autorizzato come amministratore del dispositivo per bloccare lo schermo. - Abilita il blocco dello schermo - Nessuna camera con torcia rilevata. - Errore: impossibile accedere alla torcia. - Il servizio accessibilità per µLauncher non è attivo. Per favore attivalo nelle impostazioni + Puoi avviare le tue app principali scorrendo il dito sullo schermo o premendo un pulsante. + Errore: impossibile espandere la barra di stato. Questa azione utilizza funzionalità non incluse nelle API Android. Sfortunatamente, non sembra funzionare sul tuo dispositivo. + Applicazione nascosta. Puoi renderla nuovamente visibile dalle impostazioni. + µLauncher deve amministrare il dispositivo per poter bloccare lo schermo. + Abilita il blocco schermo + Nessuna camera con torcia rilevata + Errore: impossibile accedere alla torcia + Il servizio accessibilità per µLauncher non è attivo. Attivalo nelle impostazioni Errore: impossibile bloccare lo schermo. (Se hai appena aggiornato l\'app, prova a disabilitare e riattivare il servizio accessibilità nelle impostazioni del telefono) - Errore: Il blocco schermo tramite accessibilità non è supportato su questo dispositivo. Per favore usa invece il servizio amministratore dispositivo. - µLauncher - blocco schermo + Errore: Il blocco schermo tramite accessibilità non è supportato su questo dispositivo. In alternativa usa il servizio amministratore dispositivo. + μLauncher Scegli come bloccare lo schermo Usa il servizio accessibilità Usa l\'amministratore dispositivo @@ -148,16 +128,16 @@ Serif Monospace Serif monospace - Apps + Applicazioni Applicazioni nascoste Configura la lista applicazioni Predefinito Offuscato - Imposta μLauncher come launcher predefinito + Imposta μLauncher come predefinito Informazioni app - Apri il tutorial di µLauncher + Tutorial di µLauncher Ripristina le impostazioni - Stai per eliminare le preferenze impostate. Continuare? + Stai per ripristinare tutte le impostazioni. Continuare? Segnala un bug Contatta lo sviluppatore del fork Unisciti alla chat di μLauncher @@ -170,7 +150,7 @@ Applicazioni Altro Disinstalla - Informazioni sull\'app + Informazioni app Aggiungi ai preferiti Rimuovi dai preferiti Nascondi @@ -192,13 +172,13 @@ Rinomina %1$s Dinamico Colore - Scorri con due dita verso l\'alto + Scorri su con due dita Sono consapevole che questo concederà privilegi estesi a µLauncher. Accetto che µLauncher utilizzi il servizio di accessibilità per fornire funzionalità non correlate all\'accessibilità. Accetto che µLauncher non raccolga alcun dato. Nascondi le app in pausa - Attiva/Disattiva Blocco Spazio Privato - Questa funzionalità richiede Android 15 o successivi. + Blocca/Sblocca Spazio Privato + Questa funzione richiede Android 15 o successivi. Rosso Trasparente Blu @@ -209,24 +189,24 @@ Sono consapevole che esistono altre opzioni (utilizzando i privilegi di amministratore del dispositivo o il pulsante di accensione). Attivazione dei Servizi di Accessibilità Cerca sul web - Premi invio durante la ricerca delle app per avviare una ricerca web. + Invio in ricerca app per avviare una ricerca web Cerca (senza avvio automatico) Licenze Open Source Licenze Open Source Segnala un bug Grazie per il tuo contributo al miglioramento di µLauncher!\nAggiungi le seguenti informazioni alla segnalazione del bug: Copia negli appunti - Non segnalare pubblicamente le vulnerabilità di sicurezza su GitHub, ma utilizza invece: + Non segnalare le vulnerabilità di sicurezza pubblicamente su GitHub, ma usa invece: Annulla - Premi spazio per disabilitare temporaneamente questa funzione + Spazio per disabilitare temporaneamente Segnala una vulnerabilità di sicurezza Crea una segnalazione Spazio privato bloccato Spazio privato sbloccato Spazio privato non disponibile - µLauncher deve essere la schermata iniziale predefinita per accedere allo spazio privato. + µLauncher deve essere il launcher predefinito per poter accedere allo spazio privato. Impossibile aprire l\'URL: nessun browser trovato. - Non è stata trovata un\'applicazione per gestire la ricerca. + Nessuna applicazione trovata per gestire la ricerca. privilegi più ampi a µLauncher.
µLauncher utilizzerà questi privilegi solo per bloccare lo schermo. µLauncher non raccoglierà mai alcun dato. In particolare, µLauncher non usa il servizio di accessibilità per raccogliere nessun dato.]]>
Spazio privato Spazio privato @@ -250,7 +230,7 @@ Alto dx -> centro sx -> basso dx V Alto sx -> centro basso -> alto dx - V (Reverse) + V (Inverso) Alto dx -> centro basso -> alto sx Λ Basso sx -> centro alto -> basso dx @@ -265,8 +245,16 @@ Indietro Basso dx -> centro sx -> alto dx Nascondi lo spazio privato dalla lista app - Lancia un altro launcher + Avvia un altro launcher Aggiungi scorciatoia ]]> Mostra in lista app + Blocca spazio privato + Sblocca spazio privato + Versione + Errore: impossibile abilitare il servizio di accessibilità + Quando corrisponde una sola app, viene avviata automaticamente.\nPuoi disabilitare l\'avvio con uno spazio prima della query. + Tutte le app + Puoi cercare rapidamente tra tutte le app nella lista app.\n\nScorri su per la lista o associa ad un gesto diverso. + Errore: impossibile mostrare le app recenti. (Se hai appena aggiornato l\'app, prova a disabilitare e riabilitare il servizio di accessibilità dalle impostazioni del telefono)
From 68eb59a9348bf71a2d9a19d3d228ec748002f635 Mon Sep 17 00:00:00 2001 From: Vladi69 Date: Sat, 19 Apr 2025 17:39:37 +0000 Subject: [PATCH 044/118] Translated using Weblate (Italian) Currently translated at 4.5% (1 of 22 strings) Translation: jrpie-Launcher/metadata Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/metadata/it/ --- fastlane/metadata/android/it-IT/changelogs/26.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 fastlane/metadata/android/it-IT/changelogs/26.txt diff --git a/fastlane/metadata/android/it-IT/changelogs/26.txt b/fastlane/metadata/android/it-IT/changelogs/26.txt new file mode 100644 index 0000000..d43e091 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/26.txt @@ -0,0 +1 @@ +bugfix From aabc607ed63c377753a5c226e17067ddf63dffe2 Mon Sep 17 00:00:00 2001 From: Vladi69 Date: Tue, 22 Apr 2025 07:10:03 +0000 Subject: [PATCH 045/118] Translated using Weblate (Italian) Currently translated at 99.6% (257 of 258 strings) Translation: jrpie-Launcher/Launcher Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/it/ --- app/src/main/res/values-it/strings.xml | 51 ++++++++++++++++++++------ 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index de71a7b..c7e8fcb 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -13,7 +13,31 @@ Scorri verso l\'alto con due dita Store non trovato Griglia - "<a href=\"https://issuetracker.google.com/issues/37010136#comment36\"><![CDATA[\n <h1>Scegli un sistema di blocco</h1>\n Esistono due modi di bloccare lo schermo.\n Sfortunatamente entrambi presentano svantaggi:<br/><br/>\n\n <h3>Amministratore Dispositivo</h3>\n Non funziona con impronta e riconoscimento facciale.\n\n <br/>\n <br/>\n\n <h3>Servizio Accessibilità</h3>\n Richiede privilegi eccessivi.\n µLauncher userà quei privilegi esclusivamente per bloccare lo schermo.\n <br/>\n (Non ci si dovrebbe mai fidare di un\\\'applicazione qualsiasi appena scaricata su queste cose, ma puoi verificare il <a href=\\\"https://github.com/jrpie/Launcher\\\">codice sorgente</a>.)\n\n In alcuni dispositivi, il PIN iniziale non verrà più utilizzato per la crittografia dei dati dopo aver attivato un servizio di accessibilità.\n\n Può essere <a href=\"https://issuetracker.google.com/issues/37010136#comment36\">riattivato</a> dopo.\n\n\n <br/><br/><br/><br/>\n Puoi cambiare le tue scelte in seguito nelle impostazioni.\n ]]>" + Scegli un sistema di blocco + Esistono due modi di bloccare lo schermo. + Sfortunatamente entrambi presentano svantaggi:

+ +

Amministratore Dispositivo

+ Non funziona con impronta e riconoscimento facciale. + +
+
+ +

Servizio Accessibilità

+ Richiede privilegi eccessivi. + µLauncher userà quei privilegi esclusivamente per bloccare lo schermo. +
+ (Non ci si dovrebbe mai fidare di un\'applicazione qualsiasi appena scaricata su queste cose, ma puoi verificare il codice sorgente.) + + In alcuni dispositivi, il PIN iniziale non verrà più utilizzato per la crittografia dei dati dopo aver attivato un servizio di accessibilità. + + Può essere riattivato dopo. + + +



+ Puoi cambiare le tue scelte in seguito nelle impostazioni. + ]]>
Impossibile avviare l\'app Modificare le impostazioni? Apri impostazioni per associare un\'azione a questo gesto @@ -109,15 +133,15 @@ Applicazione nascosta. Puoi renderla nuovamente visibile dalle impostazioni. µLauncher deve amministrare il dispositivo per poter bloccare lo schermo. Abilita il blocco schermo - Nessuna camera con torcia rilevata - Errore: impossibile accedere alla torcia + Nessuna camera con torcia rilevata. + Errore: impossibile accedere alla torcia. Il servizio accessibilità per µLauncher non è attivo. Attivalo nelle impostazioni Errore: impossibile bloccare lo schermo. (Se hai appena aggiornato l\'app, prova a disabilitare e riattivare il servizio accessibilità nelle impostazioni del telefono) Errore: Il blocco schermo tramite accessibilità non è supportato su questo dispositivo. In alternativa usa il servizio amministratore dispositivo. μLauncher Scegli come bloccare lo schermo Usa il servizio accessibilità - Usa l\'amministratore dispositivo + Usa Amministratore dispositivo Scegli un sistema di blocco dello schermo Scuro Chiaro @@ -132,7 +156,7 @@ Applicazioni nascoste Configura la lista applicazioni Predefinito - Offuscato + Soffuso Imposta μLauncher come predefinito Informazioni app Tutorial di µLauncher @@ -180,7 +204,7 @@ Blocca/Sblocca Spazio Privato Questa funzione richiede Android 15 o successivi. Rosso - Trasparente + Alpha Blu Verde Colore @@ -189,7 +213,7 @@ Sono consapevole che esistono altre opzioni (utilizzando i privilegi di amministratore del dispositivo o il pulsante di accensione). Attivazione dei Servizi di Accessibilità Cerca sul web - Invio in ricerca app per avviare una ricerca web + Invio in ricerca app per avviare una ricerca web. Cerca (senza avvio automatico) Licenze Open Source Licenze Open Source @@ -198,7 +222,7 @@ Copia negli appunti Non segnalare le vulnerabilità di sicurezza pubblicamente su GitHub, ma usa invece: Annulla - Spazio per disabilitare temporaneamente + Spazio per disabilitare temporaneamente. Segnala una vulnerabilità di sicurezza Crea una segnalazione Spazio privato bloccato @@ -207,7 +231,12 @@ µLauncher deve essere il launcher predefinito per poter accedere allo spazio privato. Impossibile aprire l\'URL: nessun browser trovato. Nessuna applicazione trovata per gestire la ricerca. - privilegi più ampi a µLauncher.
µLauncher utilizzerà questi privilegi solo per bloccare lo schermo. µLauncher non raccoglierà mai alcun dato. In particolare, µLauncher non usa il servizio di accessibilità per raccogliere nessun dato.]]>
+ privilegi estesi a μLauncher.
μLauncher userà questi privilegi solo per eseguire le seguenti azioni: +
    +
  • Blocco shermo
  • +
  • App recenti
  • +
+ μLauncher non raccoglierà mai alcun dato. In particolare, μLauncher non utilizza il servizio di accessibilità per raccogliere dati.]]>
Spazio privato Spazio privato Regola volume @@ -238,7 +267,7 @@ Λ (Inverso) - "Musica: Riproduci / Pausa" + Musica: Riproduci / Pausa Inverti la lista applicazioni Azioni Pulsante / gesto indietro @@ -252,7 +281,7 @@ Blocca spazio privato Sblocca spazio privato Versione - Errore: impossibile abilitare il servizio di accessibilità + Errore: impossibile abilitare il servizio di accessibilità. Quando corrisponde una sola app, viene avviata automaticamente.\nPuoi disabilitare l\'avvio con uno spazio prima della query. Tutte le app Puoi cercare rapidamente tra tutte le app nella lista app.\n\nScorri su per la lista o associa ad un gesto diverso. From bcf722ae34019ae9caa0682285f996611ad8adb2 Mon Sep 17 00:00:00 2001 From: Nicola Bortoletto Date: Wed, 23 Apr 2025 19:41:28 +0000 Subject: [PATCH 046/118] Translated using Weblate (Italian) Currently translated at 100.0% (259 of 259 strings) Translation: jrpie-Launcher/Launcher Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/it/ --- app/src/main/res/values-it/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index c7e8fcb..71bc28e 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -286,4 +286,5 @@ Tutte le app Puoi cercare rapidamente tra tutte le app nella lista app.\n\nScorri su per la lista o associa ad un gesto diverso. Errore: impossibile mostrare le app recenti. (Se hai appena aggiornato l\'app, prova a disabilitare e riabilitare il servizio di accessibilità dalle impostazioni del telefono) + Chiudi la tastiera durante lo scorrimento From 0f92e6cd988d7d0e6440907fcce50a06268d2d58 Mon Sep 17 00:00:00 2001 From: class0068 Date: Thu, 24 Apr 2025 20:57:36 +0000 Subject: [PATCH 047/118] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (259 of 259 strings) Translation: jrpie-Launcher/Launcher Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/zh_Hans/ --- app/src/main/res/values-zh-rCN/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 608e57b..32502fa 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -284,4 +284,5 @@ 错误:启用“无障碍”服务失败。 错误:无法展示最近应用屏幕。(如果您刚刚升级了本启动器,请尝试在手机设置中手动禁用再重新启用“无障碍”服务。) 启动其他启动器 + 滚动应用程序列表时自动隐藏键盘 From 3ac1794e14f3f2ad62a0ee0caa447d541790be9e Mon Sep 17 00:00:00 2001 From: Vossa Excelencia Date: Sun, 27 Apr 2025 13:42:12 +0000 Subject: [PATCH 048/118] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (280 of 280 strings) Translation: jrpie-Launcher/Launcher Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/pt_BR/ --- app/src/main/res/values-pt-rBR/strings.xml | 28 +++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index fcdd9e8..3efb611 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -102,7 +102,7 @@ Diminuir volume Música: Próximo Música: Anterior - Não faça nada + Não fazer nada https://github.com/jrpie/Launcher https://github.com/jrpie/Launcher/issues/new?template=bug_report.yaml + android-launcher-crash@jrpie.de https://github.com/jrpie/Launcher/security/policy https://s.jrpie.de/contact https://s.jrpie.de/android-legal diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d399d12..998fced 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -419,5 +419,28 @@ Open Widget Panel This widget panel no longer exists. Widgets - + μLauncher crashed + Sorry! Click for more information. +
+ For privacy reasons, crash logs are not collected automatically.
+ However logs are very useful for debugging, so I would be very grateful if you could send me the attached log by mail + or create a bug report on github.

+ Note that crash logs might contain sensitive information, e.g. the name of an app you tried to launch. + Please redact such information before sending the report. +

What can I do now?

+ If this bug appears again and again, you can try several things: +
    +
  • Force stop μLauncher
  • +
  • Clear μLauncher\'s storage (Your settings will be lost!)
  • +
  • Install an older version (GitHub, F-Droid)
  • +
+ ]]> +
+ Copy crash report to clipboard + Send report by mail + Create bug report on GitHub + μLauncher crashed + Send Email + Crashes and Debug Information diff --git a/build.gradle b/build.gradle index 2ef0f7e..e128ac7 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext.kotlin_version = '2.0.0' - ext.android_plugin_version = '8.9.2' + ext.android_plugin_version = '8.10.0' repositories { google() mavenCentral() @@ -10,7 +10,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:8.9.2' + classpath 'com.android.tools.build:gradle:8.10.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "com.android.tools.build:gradle:$android_plugin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" From eaece8e334b11ca43b3321cc64f9c105283303c1 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Thu, 15 May 2025 17:48:27 +0200 Subject: [PATCH 079/118] fix #168 --- .../de/jrpie/android/launcher/Application.kt | 8 -------- .../jrpie/android/launcher/ui/HomeActivity.kt | 18 ++++++++---------- .../launcher/ui/widgets/WidgetPanelActivity.kt | 12 ++++++++++++ .../manage/ManageWidgetPanelsActivity.kt | 1 + .../ui/widgets/manage/ManageWidgetsActivity.kt | 8 +++++++- .../ui/widgets/manage/WidgetManagerView.kt | 2 +- .../android/launcher/widgets/AppWidget.kt | 4 ++++ 7 files changed, 33 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/de/jrpie/android/launcher/Application.kt b/app/src/main/java/de/jrpie/android/launcher/Application.kt index cf9e697..e0d1d00 100644 --- a/app/src/main/java/de/jrpie/android/launcher/Application.kt +++ b/app/src/main/java/de/jrpie/android/launcher/Application.kt @@ -120,8 +120,6 @@ class Application : android.app.Application() { appWidgetHost = AppWidgetHost(this.applicationContext, APP_WIDGET_HOST_ID) appWidgetManager = AppWidgetManager.getInstance(this.applicationContext) - appWidgetHost.startListening() - val preferences = PreferenceManager.getDefaultSharedPreferences(this) LauncherPreferences.init(preferences, this.resources) @@ -178,10 +176,4 @@ class Application : android.app.Application() { apps.postValue(getApps(packageManager, applicationContext)) } } - - override fun onTerminate() { - appWidgetHost.stopListening() - super.onTerminate() - - } } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt index f501107..03c55fc 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt @@ -93,15 +93,6 @@ class HomeActivity : UIObject, Activity() { LauncherPreferences.getSharedPreferences() .registerOnSharedPreferenceChangeListener(sharedPreferencesListener) - (application as Application).appWidgetHost.startListening() - - } - - - - override fun onStop() { - (application as Application).appWidgetHost.stopListening() - super.onStop() } override fun onWindowFocusChanged(hasFocus: Boolean) { @@ -112,7 +103,6 @@ class HomeActivity : UIObject, Activity() { } } - private fun updateSettingsFallbackButtonVisibility() { // If µLauncher settings can not be reached from any action bound to an enabled gesture, // show the fallback button. @@ -131,6 +121,11 @@ class HomeActivity : UIObject, Activity() { return modifyTheme(super.getTheme()) } + override fun onPause() { + (application as Application).appWidgetHost.stopListening() + super.onPause() + } + override fun onResume() { super.onResume() @@ -161,6 +156,9 @@ class HomeActivity : UIObject, Activity() { binding.homeWidgetContainer.updateWidgets(this@HomeActivity, LauncherPreferences.widgets().widgets() ) + + + (application as Application).appWidgetHost.startListening() } override fun onDestroy() { diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetPanelActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetPanelActivity.kt index 4392451..ef7bf25 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetPanelActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetPanelActivity.kt @@ -4,6 +4,7 @@ import android.app.Activity import android.content.res.Resources import android.os.Bundle import androidx.core.view.ViewCompat +import de.jrpie.android.launcher.Application import de.jrpie.android.launcher.R import de.jrpie.android.launcher.databinding.ActivityWidgetPanelBinding import de.jrpie.android.launcher.preferences.LauncherPreferences @@ -57,6 +58,17 @@ class WidgetPanelActivity : Activity(), UIObject { override fun onStart() { super.onStart() super.onStart() + + } + + override fun onPause() { + (application as Application).appWidgetHost.stopListening() + super.onPause() + } + + override fun onResume() { + super.onResume() + (application as Application).appWidgetHost.startListening() } override fun isHomeScreen(): Boolean { diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetPanelsActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetPanelsActivity.kt index 163777f..89e1057 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetPanelsActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetPanelsActivity.kt @@ -9,6 +9,7 @@ import android.widget.EditText import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.LinearLayoutManager +import de.jrpie.android.launcher.Application import de.jrpie.android.launcher.R import de.jrpie.android.launcher.databinding.ActivityManageWidgetPanelsBinding import de.jrpie.android.launcher.preferences.LauncherPreferences diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt index 4b5c0c2..58b47be 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt @@ -92,13 +92,19 @@ class ManageWidgetsActivity : UIObject, Activity() { } + override fun onPause() { + (application as Application).appWidgetHost.stopListening() + super.onPause() + } + override fun onResume() { super.onResume() + (application as Application).appWidgetHost.startListening() + binding.manageWidgetsContainer.updateWidgets( this, LauncherPreferences.widgets().widgets() ) - } override fun onWindowFocusChanged(hasFocus: Boolean) { diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt index 76a2572..d1c84d0 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt @@ -113,7 +113,7 @@ class WidgetManagerView(widgetPanelId: Int, context: Context, attrs: AttributeSe height ) selectedWidgetOverlayView = view - selectedWidgetView = widgetViewById[view.widgetId] ?: return true + selectedWidgetView = widgetViewById[view.widgetId] startWidgetPosition = position val positionInView = start.minus(Point(position.left, position.top)) diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt index 22a63eb..788f709 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt @@ -78,6 +78,10 @@ class AppWidget( override fun createView(activity: Activity): AppWidgetHostView? { val providerInfo = activity.getAppWidgetManager().getAppWidgetInfo(id) ?: return null + /* TODO: if providerInfo is null, the corresponding app was probably uninstalled. + There does not seem to be a way to recover the widget when the app is installed again, + hence it should be deleted. */ + val view = activity.getAppWidgetHost() .createView(activity, this.id, providerInfo) From 5f847a8d401d36f36318909fc4a83f2858849611 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Thu, 15 May 2025 20:55:15 +0200 Subject: [PATCH 080/118] enable widget interaction by default on widget panels --- .../main/java/de/jrpie/android/launcher/widgets/AppWidget.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt index 788f709..a968962 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt @@ -41,7 +41,7 @@ class AppWidget( id, position, panelId, - false, + panelId != WidgetPanel.HOME.id, widgetProviderInfo.provider.packageName, widgetProviderInfo.provider.className, widgetProviderInfo.profile.hashCode() From 271850f75a4ab2375d49282627b2e094082d276b Mon Sep 17 00:00:00 2001 From: renar Date: Tue, 13 May 2025 07:22:28 +0000 Subject: [PATCH 081/118] Translated using Weblate (Italian) Currently translated at 100.0% (280 of 280 strings) Translation: jrpie-Launcher/Launcher Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/it/ --- app/src/main/res/values-it/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 10362a0..bca9108 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -302,9 +302,9 @@ Ok Pannelli widget - Contiene %d widget. - Contiene %d widget. - Contiene %d widget. + Contiene %1$d widget. + Contiene %1$d widget. + Contiene %1$d widget. Crea nuovo pannello widget Apri pannello widget From 013b835ed8f4abb5c453cb8dfecec9a2caea2215 Mon Sep 17 00:00:00 2001 From: Luke Wass Date: Fri, 16 May 2025 02:53:31 -0500 Subject: [PATCH 082/118] more translations --- app/src/main/res/values-lt/strings.xml | 126 ++++++++++++++++++++++++- 1 file changed, 123 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 0a45d57..d5c9ecf 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -1,16 +1,136 @@ + + Nepavyksta paleisti programėlės Norite pakeisti nustatymus? + + Atidarykite nustatymus norėdami pasirinkti šio gesto veiksmą + + Nustatymai + Veiksmai Paleidimo programėlė Apie + + Atgal Grįžimo mygtukas / grįžimo gestas + Aukštyn Perbraukimas aukštyn Bakstelėkite + aukštyn - Nepavyksta paleisti programėlės - Atidarykite nustatymus norėdami pasirinkti šio gesto veiksmą - Aukštyn Bakstelėjimas ir perbraukimas aukštyn + Dvigubai aukštyn + Perbraukite aukštyn dviem pirštais + Žemyn + Perbraukite žemyn + Bakstelėkite + žemyn + Bakstelėkite ir perbraukite žemyn + Dvigubai žemyn + Perbraukite dviem pirštais + Kairėje + Perbraukite į kairę + Bakstelėkite + kairę + Bakstelėkite ir perbraukite į kairę + Dvigubai kairėje + Du pirštais perbraukite kairėn + Dešinė + Perbraukite į dešinę + Bakstelėkite + dešinė + Bakstelėkite ir perbraukite į dešinę + Dviguba dešinė + Perbraukite į dešinę dviem pirštais + Dešinė (viršuje) + Perbraukite tiesiai ekrano viršuje + Dešinė (apačia) + Perbraukite tiesiai ekrano apačioje + Kairė (apačia) + Perbraukite į kairę ekrano apačioje + Kairė (viršuje) + Perbraukite kairėn ekrano viršuje + Aukštyn (kairysis kraštas) + Perbraukite aukštyn kairiajame ekrano krašte + Aukštyn (dešinysis kraštas) + Perbraukite aukštyn dešiniajame ekrano krašte + Žemyn (kairysis kraštas) + Perbraukite žemyn kairiajame ekrano krašte + Žemyn (dešinysis kraštas) + Žemyn (dešinysis kraštas) + + Garsumo didinimo klavišas + Paspauskite mygtuką „Volume Up“ + Volume žemyn klavišas + Paspauskite mygtuką „Volume Down“ + Dukart spustelėkite + Dukart spustelėkite tuščią sritį + Ilgas spustelėjimas + Ilgai spustelėkite tuščią sritį + Data + Spustelėkite datą + Laikas + Spustelėkite laiką + + Tvarkykite valdiklius + Tvarkykite valdiklio skydelius + + + Pasirinkite programą + + Įdiekite programas + Parduotuvėje nerasta + + + Išvaizda + + + Spalvos tema + Numatytasis + Tamsu + Šviesa + Dinaminis + + Teksto šešėlis + Fonas (programų sąrašas ir nustatymas) + Skaidrus + Dim + Blur + Solidus + + + Šriftas + + + Sistemos numatytasis + Be serifo + Serifas + Monoerdvė + Serifo monospace + Vienspalvių programų piktogramos + + + Spalva + Rodyti laiką + Rodyti datą + Naudoti lokalizuotą datos formatą + Rodyti sekundes + Apversti datą ir laiką + + Pasirinkite ekrano foną + Ekranas From 31a9049861b3b08e8e8c953818544e5d7013bef5 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sat, 17 May 2025 11:42:21 +0200 Subject: [PATCH 083/118] try to mitigate #172 --- .../main/java/de/jrpie/android/launcher/ui/HomeActivity.kt | 7 ++++++- .../android/launcher/ui/widgets/WidgetPanelActivity.kt | 7 ++++++- .../launcher/ui/widgets/manage/ManageWidgetsActivity.kt | 7 ++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt index 03c55fc..f3cde9a 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt @@ -122,7 +122,12 @@ class HomeActivity : UIObject, Activity() { } override fun onPause() { - (application as Application).appWidgetHost.stopListening() + try { + (application as Application).appWidgetHost.stopListening() + } catch (e: Exception) { + // Throws a NullPointerException on Android 12 an earlier, see #172 + e.printStackTrace() + } super.onPause() } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetPanelActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetPanelActivity.kt index ef7bf25..3c884db 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetPanelActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetPanelActivity.kt @@ -62,7 +62,12 @@ class WidgetPanelActivity : Activity(), UIObject { } override fun onPause() { - (application as Application).appWidgetHost.stopListening() + try { + (application as Application).appWidgetHost.stopListening() + } catch (e: Exception) { + // Throws a NullPointerException on Android 12 an earlier, see #172 + e.printStackTrace() + } super.onPause() } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt index 58b47be..984df85 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt @@ -93,7 +93,12 @@ class ManageWidgetsActivity : UIObject, Activity() { } override fun onPause() { - (application as Application).appWidgetHost.stopListening() + try { + (application as Application).appWidgetHost.stopListening() + } catch (e: Exception) { + // Throws a NullPointerException on Android 12 an earlier, see #172 + e.printStackTrace() + } super.onPause() } From bd7df4f6a04d92240a4e40c3c807cc6c9c10d70d Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sat, 17 May 2025 12:12:03 +0200 Subject: [PATCH 084/118] (try to) fix #172 --- .../launcher/preferences/Preferences.kt | 12 ++++-- .../launcher/preferences/legacy/Version100.kt | 39 +++++++++++++++++++ .../launcher/preferences/legacy/Version4.kt | 5 ++- .../widgets/manage/ManageWidgetsActivity.kt | 4 -- .../ui/widgets/manage/SelectWidgetActivity.kt | 12 +++--- .../android/launcher/widgets/ClockWidget.kt | 2 +- .../launcher/widgets/DebugInfoWidget.kt | 2 +- .../jrpie/android/launcher/widgets/Widget.kt | 4 +- .../jrpie/android/launcher/widgets/Widgets.kt | 17 +++++--- 9 files changed, 72 insertions(+), 25 deletions(-) create mode 100644 app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version100.kt diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt index 8936675..e5877f5 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt @@ -10,6 +10,7 @@ import de.jrpie.android.launcher.apps.AbstractAppInfo.Companion.INVALID_USER import de.jrpie.android.launcher.apps.AppInfo import de.jrpie.android.launcher.apps.DetailedAppInfo import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion1 +import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion100 import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion2 import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion3 import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion4 @@ -21,12 +22,13 @@ import de.jrpie.android.launcher.widgets.DebugInfoWidget import de.jrpie.android.launcher.widgets.WidgetPanel import de.jrpie.android.launcher.widgets.WidgetPosition import de.jrpie.android.launcher.widgets.deleteAllWidgets +import de.jrpie.android.launcher.widgets.generateInternalId /* Current version of the structure of preferences. * Increase when breaking changes are introduced and write an appropriate case in * `migratePreferencesToNewVersion` */ -const val PREFERENCE_VERSION = 100 +const val PREFERENCE_VERSION = 101 const val UNKNOWN_PREFERENCE_VERSION = -1 private const val TAG = "Launcher - Preferences" @@ -65,6 +67,10 @@ fun migratePreferencesToNewVersion(context: Context) { migratePreferencesFromVersion4(context) Log.i(TAG, "migration of preferences complete (4 -> ${PREFERENCE_VERSION}).") } + 100 -> { + migratePreferencesFromVersion100(context) + Log.i(TAG, "migration of preferences complete (100 -> ${PREFERENCE_VERSION}).") + } else -> { Log.w( @@ -91,7 +97,7 @@ fun resetPreferences(context: Context) { LauncherPreferences.widgets().widgets( setOf( ClockWidget( - (context.applicationContext as Application).appWidgetHost.allocateAppWidgetId(), + generateInternalId(), WidgetPosition(1, 3, 10, 4), WidgetPanel.HOME.id ) @@ -103,7 +109,7 @@ fun resetPreferences(context: Context) { LauncherPreferences.widgets().widgets().also { it.add( DebugInfoWidget( - (context.applicationContext as Application).appWidgetHost.allocateAppWidgetId(), + generateInternalId(), WidgetPosition(1, 1, 10, 4), WidgetPanel.HOME.id ) diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version100.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version100.kt new file mode 100644 index 0000000..43e4bc7 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version100.kt @@ -0,0 +1,39 @@ +package de.jrpie.android.launcher.preferences.legacy + +import android.content.Context +import de.jrpie.android.launcher.Application +import de.jrpie.android.launcher.preferences.LauncherPreferences +import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION +import de.jrpie.android.launcher.widgets.ClockWidget +import de.jrpie.android.launcher.widgets.DebugInfoWidget +import de.jrpie.android.launcher.widgets.generateInternalId +import de.jrpie.android.launcher.widgets.updateWidget + +fun migratePreferencesFromVersion100(context: Context) { + assert(PREFERENCE_VERSION == 101) + assert(LauncherPreferences.internal().versionCode() == 100) + + val widgets = LauncherPreferences.widgets().widgets() ?: setOf() + widgets.forEach { widget -> + when (widget) { + is ClockWidget -> { + val id = widget.id + val newId = generateInternalId() + (context.applicationContext as Application).appWidgetHost.deleteAppWidgetId(id) + widget.delete(context) + widget.id = newId + updateWidget(widget) + } + is DebugInfoWidget -> { + val id = widget.id + val newId = generateInternalId() + (context.applicationContext as Application).appWidgetHost.deleteAppWidgetId(id) + widget.delete(context) + widget.id = newId + updateWidget(widget) + } + else -> {} + } + } + LauncherPreferences.internal().versionCode(101) +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version4.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version4.kt index a9ab3a1..b13978b 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version4.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version4.kt @@ -7,19 +7,20 @@ import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION import de.jrpie.android.launcher.widgets.ClockWidget import de.jrpie.android.launcher.widgets.WidgetPanel import de.jrpie.android.launcher.widgets.WidgetPosition +import de.jrpie.android.launcher.widgets.generateInternalId fun migratePreferencesFromVersion4(context: Context) { - assert(PREFERENCE_VERSION == 100) assert(LauncherPreferences.internal().versionCode() < 100) LauncherPreferences.widgets().widgets( setOf( ClockWidget( - (context.applicationContext as Application).appWidgetHost.allocateAppWidgetId(), + generateInternalId(), WidgetPosition(1, 3, 10, 4), WidgetPanel.HOME.id ) ) ) LauncherPreferences.internal().versionCode(100) + migratePreferencesFromVersion100(context) } \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt index 984df85..953fc28 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt @@ -135,10 +135,6 @@ class ManageWidgetsActivity : UIObject, Activity() { val appWidgetHost = (application as Application).appWidgetHost startActivityForResult( Intent(this, SelectWidgetActivity::class.java).also { - it.putExtra( - AppWidgetManager.EXTRA_APPWIDGET_ID, - appWidgetHost.allocateAppWidgetId() - ) it.putExtra( EXTRA_PANEL_ID, panelId diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt index 0efdb43..eeb98df 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt @@ -14,6 +14,7 @@ import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import de.jrpie.android.launcher.Application import de.jrpie.android.launcher.R import de.jrpie.android.launcher.databinding.ActivitySelectWidgetBinding import de.jrpie.android.launcher.ui.UIObject @@ -24,7 +25,7 @@ import de.jrpie.android.launcher.widgets.LauncherWidgetProvider import de.jrpie.android.launcher.widgets.WidgetPanel import de.jrpie.android.launcher.widgets.WidgetPosition import de.jrpie.android.launcher.widgets.bindAppWidgetOrRequestPermission -import de.jrpie.android.launcher.widgets.getAppWidgetHost +import de.jrpie.android.launcher.widgets.generateInternalId import de.jrpie.android.launcher.widgets.getAppWidgetProviders import de.jrpie.android.launcher.widgets.updateWidget @@ -38,12 +39,13 @@ private const val REQUEST_WIDGET_PERMISSION = 29 */ class SelectWidgetActivity : AppCompatActivity(), UIObject { lateinit var binding: ActivitySelectWidgetBinding - var widgetId: Int = -1 var widgetPanelId: Int = WidgetPanel.HOME.id private fun tryBindWidget(info: LauncherWidgetProvider) { when (info) { is LauncherAppWidgetProvider -> { + val widgetId = + (applicationContext as Application).appWidgetHost.allocateAppWidgetId() if (bindAppWidgetOrRequestPermission( this, info.info, @@ -62,7 +64,7 @@ class SelectWidgetActivity : AppCompatActivity(), UIObject { } } is LauncherClockWidgetProvider -> { - updateWidget(ClockWidget(widgetId, WidgetPosition(0, 4, 12, 3), widgetPanelId)) + updateWidget(ClockWidget(generateInternalId(), WidgetPosition(0, 4, 12, 3), widgetPanelId)) finish() } } @@ -81,11 +83,7 @@ class SelectWidgetActivity : AppCompatActivity(), UIObject { setContentView(binding.root) - widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) widgetPanelId = intent.getIntExtra(EXTRA_PANEL_ID, WidgetPanel.HOME.id) - if (widgetId == -1) { - widgetId = getAppWidgetHost().allocateAppWidgetId() - } val viewManager = LinearLayoutManager(this) val viewAdapter = SelectWidgetRecyclerAdapter() diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt index d0d1c0e..f864ee8 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt @@ -12,7 +12,7 @@ import kotlinx.serialization.Serializable @Serializable @SerialName("widget:clock") class ClockWidget( - override val id: Int, + override var id: Int, override var position: WidgetPosition, override val panelId: Int, override var allowInteraction: Boolean = true diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/DebugInfoWidget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/DebugInfoWidget.kt index 01ecddc..75ae6d0 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/DebugInfoWidget.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/DebugInfoWidget.kt @@ -12,7 +12,7 @@ import kotlinx.serialization.Serializable @Serializable @SerialName("widget:debuginfo") class DebugInfoWidget( - override val id: Int, + override var id: Int, override var position: WidgetPosition, override val panelId: Int, override var allowInteraction: Boolean = true diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt index 28539a2..fd96f14 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt @@ -27,7 +27,9 @@ sealed class Widget { abstract fun configure(activity: Activity, requestCode: Int) fun delete(context: Context) { - context.getAppWidgetHost().deleteAppWidgetId(id) + if (id >= 0) { + context.getAppWidgetHost().deleteAppWidgetId(id) + } LauncherPreferences.widgets().widgets( LauncherPreferences.widgets().widgets()?.also { diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt index cded50c..593f3b3 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt @@ -13,6 +13,7 @@ import android.os.UserManager import android.util.Log import de.jrpie.android.launcher.Application import de.jrpie.android.launcher.preferences.LauncherPreferences +import kotlin.math.min fun deleteAllWidgets(context: Context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -29,12 +30,9 @@ fun deleteAllWidgets(context: Context) { * * @return true iff the app widget was bound successfully. */ -fun bindAppWidgetOrRequestPermission(activity: Activity, providerInfo: AppWidgetProviderInfo, id: Int, requestCode: Int? = null): Boolean { - val appWidgetId = if(id == -1) { - activity.getAppWidgetHost().allocateAppWidgetId() - } else { id } +fun bindAppWidgetOrRequestPermission(activity: Activity, providerInfo: AppWidgetProviderInfo, appWidgetId: Int, requestCode: Int? = null): Boolean { - Log.i("Launcher", "Binding new widget ${appWidgetId}") + Log.i("Launcher", "Binding new widget $appWidgetId") if (!activity.getAppWidgetManager().bindAppWidgetIdIfAllowed( appWidgetId, providerInfo.provider @@ -79,6 +77,13 @@ fun updateWidget(widget: Widget) { ) } + +// TODO: this needs to be improved +fun generateInternalId(): Int { + val minId = min(-5,(LauncherPreferences.widgets().widgets() ?: setOf()).minOfOrNull { it.id } ?: 0) + return minId -1 +} + fun updateWidgetPanel(widgetPanel: WidgetPanel) { LauncherPreferences.widgets().customPanels( (LauncherPreferences.widgets().customPanels() ?: setOf()) @@ -92,4 +97,4 @@ fun Context.getAppWidgetHost(): AppWidgetHost { } fun Context.getAppWidgetManager(): AppWidgetManager { return (this.applicationContext as Application).appWidgetManager -} +} \ No newline at end of file From dd3a2e91bd7baaa510ea0b12f1ce7f2db3f5d327 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sat, 17 May 2025 13:00:16 +0200 Subject: [PATCH 085/118] 0.2.2 --- app/build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/47.txt | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/47.txt diff --git a/app/build.gradle b/app/build.gradle index 42f0a2b..54e7b2b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,8 +23,8 @@ android { minSdkVersion 21 targetSdkVersion 35 compileSdk 35 - versionCode 46 - versionName "0.2.1" + versionCode 47 + versionName "0.2.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/fastlane/metadata/android/en-US/changelogs/47.txt b/fastlane/metadata/android/en-US/changelogs/47.txt new file mode 100644 index 0000000..33899dd --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/47.txt @@ -0,0 +1,10 @@ + * Fixed a bug related to widget causing crashes on Android 12 and earlier making the app unusable + * Fixed some additional bugs related to widgets + + * Improved Lithuanian translation (thank you, wassupluke!) + * Improved Arabic translation (thank you, anonymous contributor!) + * Improved Chinese translation (thank you, class0068!) + * Improved Dutch translation (thank you, renar!) + * Improved German translation (thank you, renar!) + * Improved Italian translation (thank you, renar!) + * Improved Portuguese translation (thank you, anonymous contributor!) From ba3255d9ec6218c274f3be575e610012cced48cc Mon Sep 17 00:00:00 2001 From: Luke Wass Date: Mon, 19 May 2025 16:34:56 -0500 Subject: [PATCH 086/118] Improve widget list layout --- app/src/main/res/layout/list_widgets_row.xml | 131 ++++++++++++------- 1 file changed, 85 insertions(+), 46 deletions(-) diff --git a/app/src/main/res/layout/list_widgets_row.xml b/app/src/main/res/layout/list_widgets_row.xml index 878aaad..147cc36 100644 --- a/app/src/main/res/layout/list_widgets_row.xml +++ b/app/src/main/res/layout/list_widgets_row.xml @@ -6,55 +6,94 @@ android:id="@+id/list_apps_row_container" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="15sp"> + android:layout_marginTop="15sp" + android:layout_marginHorizontal="30sp"> - - - - - - - - + app:layout_constraintTop_toTopOf="parent"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From a56cc772f71ea38560dc46c57ad5932277038a2b Mon Sep 17 00:00:00 2001 From: Luke Wass Date: Tue, 20 May 2025 11:58:12 -0500 Subject: [PATCH 087/118] Add visual cues and interactive styling to the settings fragment --- app/src/main/res/layout/tutorial_4_setup.xml | 26 ++++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/app/src/main/res/layout/tutorial_4_setup.xml b/app/src/main/res/layout/tutorial_4_setup.xml index 9650c57..c55bdc8 100644 --- a/app/src/main/res/layout/tutorial_4_setup.xml +++ b/app/src/main/res/layout/tutorial_4_setup.xml @@ -33,17 +33,27 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tutorial_setup_title" /> - + app:layout_constraintEnd_toEndOf="parent"> + + + + Date: Sat, 24 May 2025 13:31:00 +0200 Subject: [PATCH 088/118] prevent crash when unable to access widgetproviderinfo --- .../ui/widgets/manage/ManageWidgetsActivity.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt index 953fc28..38bbb84 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt @@ -147,13 +147,17 @@ class ManageWidgetsActivity : UIObject, Activity() { private fun createWidget(data: Intent) { Log.i("Launcher", "creating widget") val appWidgetManager = (application as Application).appWidgetManager + val appWidgetHost = (application as Application).appWidgetHost val appWidgetId = data.extras?.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID) ?: return - val provider = appWidgetManager.getAppWidgetInfo(appWidgetId) - val display = windowManager.defaultDisplay val widgetInfo = appWidgetManager.getAppWidgetInfo(appWidgetId) + if (widgetInfo == null) { + Log.w("Launcher", "can't access widget") + appWidgetHost.deleteAppWidgetId(appWidgetId) + return + } val position = WidgetPosition.findFreeSpace( WidgetPanel.byId(panelId), @@ -161,7 +165,7 @@ class ManageWidgetsActivity : UIObject, Activity() { max(3, (GRID_SIZE * (widgetInfo.minHeight) / display.height.toFloat()).roundToInt()) ) - val widget = AppWidget(appWidgetId, position, panelId, provider) + val widget = AppWidget(appWidgetId, position, panelId, widgetInfo) LauncherPreferences.widgets().widgets( (LauncherPreferences.widgets().widgets() ?: HashSet()).also { it.add(widget) From e4c7ad099420606631011f8bcdafbe894f2ba718 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sat, 24 May 2025 17:40:32 +0200 Subject: [PATCH 089/118] prepare docs/ for hugo ssg (see #176) --- docs/_index.md | 0 docs/actions-and-gestures.md | 2 +- docs/alternatives.md | 3 ++ docs/changes-fork.md | 9 ++-- docs/contributing.md | 2 +- docs/home.md | 43 ----------------- docs/settings.md | 89 +++++++++++++++++++----------------- docs/widgets.md | 27 +++++++++++ 8 files changed, 86 insertions(+), 89 deletions(-) create mode 100644 docs/_index.md create mode 100644 docs/alternatives.md delete mode 100644 docs/home.md create mode 100644 docs/widgets.md diff --git a/docs/_index.md b/docs/_index.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/actions-and-gestures.md b/docs/actions-and-gestures.md index f5d831f..a5348a3 100644 --- a/docs/actions-and-gestures.md +++ b/docs/actions-and-gestures.md @@ -1,4 +1,4 @@ -# Gestures and Actions +# Actions and Gestures µLauncher's central mechanism for accessing important functionality quickly is to bind actions (e.g. launching an app) to gestures (e.g. swiping up). diff --git a/docs/alternatives.md b/docs/alternatives.md new file mode 100644 index 0000000..4c875f4 --- /dev/null +++ b/docs/alternatives.md @@ -0,0 +1,3 @@ + +TODO: move the [hedgedoc](https://pad.abstractnonsen.se/foss-launchers) document here. + diff --git a/docs/changes-fork.md b/docs/changes-fork.md index 8efc965..3abe96a 100644 --- a/docs/changes-fork.md +++ b/docs/changes-fork.md @@ -1,3 +1,7 @@ ++++ +title = 'Differences to the original Launcher' ++++ + # Notable changes compared to Finn's Launcher µLauncher is a fork of [finnmglas's app Launcher](https://github.com/finnmglas/Launcher). @@ -51,6 +55,5 @@ The complete list of changes can be viewed [here](https://github.com/jrpie/launc --- -\[original-repo\]: [https://github.com/finnmglas/Launcher](https://github.com/finnmglas/Launcher) - -\[hack-font\]: [https://sourcefoundry.org/hack/](https://sourcefoundry.org/hack/) +[original-repo]: https://github.com/finnmglas/Launcher +[hack-font]: https://sourcefoundry.org/hack/ diff --git a/docs/contributing.md b/docs/contributing.md index 8e9de53..350ba01 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -13,7 +13,7 @@ There are several ways to contribute to this app: - Open a new pull request. -See [build.md](build.md) for instructions how to build this project. +See [here](/docs/build) for instructions how to build this project. The [CI pipeline](https://github.com/jrpie/Launcher/actions) automatically creates debug builds. diff --git a/docs/home.md b/docs/home.md deleted file mode 100644 index 9812e93..0000000 --- a/docs/home.md +++ /dev/null @@ -1,43 +0,0 @@ -# Welcome to the μLauncher Documentation - -## What is μLauncher? - -µLauncher is an *minimal* and *distraction-free* Android home screen that lets you launch apps using [swipe gestures and button presses](/actions-and-gestured.md). - -This project is a fork of [finnmglas's app Launcher](https://github.com/finnmglas/Launcher). An incomplete list of changes can be found [here](https://github.com/wassupluke/Launcher/blob/master/docs/launcher.md). - -## Where can I get μLauncher? - -[![Get it on F-Droid](https://fdroid.gitlab.io/artwork/badge/get-it-on.png)](https://f-droid.org/packages/de.jrpie.android.launcher/) - -[![Get it on Accrescent](https://accrescent.app/badges/get-it-on.png)](https://accrescent.app/app/de.jrpie.android.launcher.accrescent) - -[![Get it on Obtainium](https://raw.githubusercontent.com/ImranR98/Obtainium/b1c8ac6f2ab08497189721a788a5763e28ff64cd/assets/graphics/badge_obtainium.png)](https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/{%22id%22:%22de.jrpie.android.launcher%22,%22url%22:%22https://github.com/jrpie/Launcher%22,%22author%22:%22jrpie%22,%22name%22:%22%c2%b5Launcher%22,%22additionalSettings%22:%22{\%22apkFilterRegEx\%22:\%22release\%22,\%22invertAPKFilter\%22:false,\%22about\%22:\%22%c2%b5Launcher%20is%20a%20minimal%20home%20screen.\%22}%22}) - -[![Get it on GitHub](https://raw.githubusercontent.com/NeoApplications/Neo-Backup/034b226cea5c1b30eb4f6a6f313e4dadcbb0ece4/badge_github.png)](https://github.com/jrpie/launcher/releases/latest) - -> You can also [get it on Google Play](https://play.google.com/store/apps/details?id=de.jrpie.android.launcher), but this is not recommend. - - -## How can I contribute? - -See [docs/contribute](/contribute.md) - -## Screenshots - -![μLauncher Home Screen screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg) - -![μLauncher Settings screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg) - -![μLauncher All Apps list view with icons screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg) - -![μLauncher Favorite Apps list view with icons screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/4.jpg) - -![μLauncher Choose App to bind to gesture screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/5.jpg) - -![μLauncher App options card from list view with icons screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/7.jpg -) - -![μLauncher All Apps list view without icons screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/8.jpg) - - diff --git a/docs/settings.md b/docs/settings.md index f033f62..3cbfbec 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -1,19 +1,18 @@ -# Launcher Settings +# Settings -Tweaks and customizations can be made from within the Launcher Settings page. +Tweaks and customizations can be made from within the settings page. +The settings can be opened by binding the Settings action to a gesture (this is especially useful when configuring μLauncher for the first time) or from the settings icon in the app drawer.[^1] -These settings let you change wallpapers, change colors and fonts, enable monochrome app icons, change the app drawer layout, and much more. - -In the following documentation, 'app drawer' will be used to refer to the 'All Apps', 'Favorite Apps' and 'Private Space' views. +[^1]: i.e. the 'All Apps', 'Favorite Apps' and 'Private Space' views. ## Appearance -> ### Choose a wallpaper +### Choose a wallpaper This triggers Android's mechanism to change the wallpaper using a photos app, file explorer, or native wallpaper setting app. µLauncher uses the system-wide wallpaper, i.e. this change also affects other launchers. -> ### Font (in-app font) +### Font (in-app font) Set the font used within the app settings. This setting does not affect the date/time home screen font. @@ -21,17 +20,17 @@ Set the font used within the app settings. This setting does not affect the date **options:** `Hack`,`System default`,`Sans serif`,`Serif`,`Monospace`,`Serif monospace` -> ### Text Shadow +### Text Shadow **type:** `toggle` -> ### Background (app list and setting) +### Background (app list and setting) **type:** `dropdown` **type:** `Transparent`,`Dim`,`Blur`,`Solid` -> ### Monochrome app icons +### Monochrome app icons Remove coloring from all app icons. Can help decrease visual stimulus when enabled. @@ -39,49 +38,56 @@ Remove coloring from all app icons. Can help decrease visual stimulus when enabl ## Date & Time -> ### Font (home screen) +These settings effect the clock shown on the home screen (or on widget panels). +If the clock is removed, the settings are not used. -Set the home screen font for date and time. This setting does not affect the in-app font. +### Font (home screen) + +Set the home screen font for date and time. This setting does not affect the font of other components. **type:** `dropdown` **options:** `Hack`,`System default`,`Sans serif`,`Serif`,`Monospace`,`Serif monospace` -> ### Color [`[bug]`](https://github.com/jrpie/launcher/issues/151) +### Color Set the color for the home screen date and time. -Accepts a HEX color code (consisting of a '#' followed by three sets of two alphanumeric (letters and numbers) characters. A fourth set of two alphanumeric characters may be added to set the transparency of the color. +Accepts an 6 digit RGB or or 8 digit ARGB color code characters.[^2] +Note that on Android the ARGB color format is used, i.e. the alpha component is specified first. +This differs from the more common RGBA, which is used in web development. -[Color wheel picker](https://rgbacolorpicker.com/color-wheel-picker) -**type:** `HEX`,`RGBA` +[^2]: More precisely, everything that is vaild input for [parseColor](https://developer.android.com/reference/android/graphics/Color#parseColor(java.lang.String)) can be used. -> ### Use localized date format -Adapt the display of dates and times to the specific conventions of a particular locale or region. Different locales use different date orders (e.g., MM/DD/YYYY in the US, DD/MM/YYYY in Europe). +**type:** `ARGB` + +### Use localized date format + +Adapt the display of dates and times to the specific conventions of a particular locale or region as set by the system. Different locales use different date orders (e.g., MM/DD/YYYY in the US, DD/MM/YYYY in Europe). **type:** `toggle` -> ### Show time +### Show time Show the current time on the home screen. **type:** `toggle` -> ### Show seconds +### Show seconds Show the current time down to the second on the home screen. **type:** `toggle` -> ### Show date +### Show date Show the current date on the home screen. **type:** `toggle` -> ### Flip date and time +### Flip date and time Place the current time above the current date on the home screen. @@ -89,7 +95,7 @@ Place the current time above the current date on the home screen. ## Functionality -> ### Launch search results +### Launch search results Launches any app that matches user keyboard input when no other apps match. @@ -105,37 +111,37 @@ Press space to temporarily disable this feature and allow text entry without pre **type:** `toggle` -> ### Search the web +### Search the web -Press return/enter while searching the app list to launch a web search. +Press return while searching the app list to launch a web search. **type:** `toggle` -> ### Start keyboard for search +### Start keyboard for search Automatically open the keyboard when the app drawer is opened. **type:** `toggle` -> ### Double swipe gestures +### Double swipe gestures Enable double swipe (two finger) gestures in launcher settings. Does not erase gesture bindings if accidentally turned off. **type:** `toggle` -> ### Edge swipe gestures +### Edge swipe gestures Enable edge swipe (near edges of screen) gestures in launcher settings. Does not erase gesture bindings if accidentally turned off. **type:** `toggle` -> ### Edge width +### Edge width Change how large a margin is used for detecting edge gestures. Shows the edge margin preview when using the slider. **type:** `slider` -> ### Choose method for locking the screen +### Choose method for locking the screen There are two methods to lock the screen and unfortunately both have downsides. @@ -157,11 +163,11 @@ There are two methods to lock the screen and unfortunately both have downsides. ## Apps -> ### Hidden apps +### Hidden apps Open an app drawer containing only hidden apps. -> ### Don't show apps that are bound to a gesture in the app list +### Don't show apps that are bound to a gesture in the app list Remove certain apps from the app drawer if they are already accessible via a gesture. @@ -169,20 +175,21 @@ Reduces redundancy and tidies up app drawer. **type:** `toggle` -> ### Hide paused apps +### Hide paused apps Remove paused apps from the app drawer. For example an app belonging to the work profile is paused when the work profile is inactive. **type:** `toggle` -> ### Hide private space from app list +### Hide private space from app list Remove private space from app drawer. +Private space apps can be accessed using a separate app drawer which can be opened with the Private Space action. **type:** `toggle` -> ### Layout of app list +### Layout of app list Changes how the apps are displayed when accessing the app drawer. @@ -195,7 +202,7 @@ Changes how the apps are displayed when accessing the app drawer. **options:** `Default`,`Text`,`Grid` -> ### Reverse the app list +### Reverse the app list Enable reverse alphabetical sorting of apps in the app drawer. Useful for keeping apps within easier reach from the keyboard. @@ -204,21 +211,21 @@ Useful for keeping apps within easier reach from the keyboard. ## Display -> ### Rotate screen +### Rotate screen **type:** `toggle` -> ### Keep screen on +### Keep screen on **type:** `toggle` -> ### Hide status bar +### Hide status bar Remove the top status bar from the home screen. **type:** `toggle` -> ### Hide navigation bar +### Hide navigation bar Remove the navigation bar from the home screen. Enabling this setting may make it difficult to use the device if gestures are not setup properly. @@ -226,7 +233,7 @@ Remove the navigation bar from the home screen. Enabling this setting may make i ## Additional Settings -> ### App Drawer Long Press on App +### App Drawer Long Press on App Access additional per-app details and settings. To use, open the app drawer and long press on any app. diff --git a/docs/widgets.md b/docs/widgets.md new file mode 100644 index 0000000..ab5e21f --- /dev/null +++ b/docs/widgets.md @@ -0,0 +1,27 @@ ++++ + title = 'Widgets' ++++ + +# Widgets + +μLauncher allows to add [app widgets](https://developer.android.com/develop/ui/views/appwidgets/overview) to the home screen and to widget panels. + +Widgets can be added, moved, removed and configured in `Settings > Manage Widgets`. + +It is configurable whether or not interaction with a widget should be enabled. + +* If interaction is enabled, touch events are forwarded to the widget as usual. +However, μLauncher [gestures](/docs/actions-and-gestures/) can not be executed in areas where such a widget is present. + +* If interaction is disabled, the widget does not respond to any touch events. + This is recommended when using a widget only to display information. + +μLauncher's clock behaves similar as an app widget and can be managed in the same way.[^1] + +[^1]: However, it is technically not an app widget and cannot be used with other launchers. + +# Widget Panels + +Widget panels can contain widgets that are not needed on the home screen. +They can be managed in `Settings > Manage Widget Panels`. +Widget panels can be opened by using the [Open Widget Panel](/docs/actions-and-gestures/#available-actions) action. From ac1639b77ed1c940f69d055c50e2a1583c4270ef Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sat, 24 May 2025 19:01:15 +0200 Subject: [PATCH 090/118] add termux example to docs (see #147 and #149) --- docs/examples/_index.md | 2 ++ docs/examples/termux/index.md | 23 +++++++++++++++++++++++ docs/examples/termux/screenshot1.png | Bin 0 -> 47999 bytes docs/examples/termux/screenshot2.png | Bin 0 -> 33737 bytes 4 files changed, 25 insertions(+) create mode 100644 docs/examples/_index.md create mode 100644 docs/examples/termux/index.md create mode 100644 docs/examples/termux/screenshot1.png create mode 100644 docs/examples/termux/screenshot2.png diff --git a/docs/examples/_index.md b/docs/examples/_index.md new file mode 100644 index 0000000..0847315 --- /dev/null +++ b/docs/examples/_index.md @@ -0,0 +1,2 @@ +# Examples +This section contains some examples how μLauncher can be tweaked. diff --git a/docs/examples/termux/index.md b/docs/examples/termux/index.md new file mode 100644 index 0000000..7d1f59c --- /dev/null +++ b/docs/examples/termux/index.md @@ -0,0 +1,23 @@ ++++ + title = 'Integration with Termux' ++++ + +# Termux + +μLauncher has no special support for [Termux](https://termux.dev/). +However it is possible to run Termux commands from μLauncher by using [Termux:Widget](https://wiki.termux.com/wiki/Termux:Widget) to create a pinned shortcut and bind that to a gesture. + +* Install Termux:Widget. +* Make sure that μLauncher is set as the default home screen.[^1] +* Put the script you want to run into `~/.shortcuts/`. +* Run `am start com.termux.widget/com.termux.widget.TermuxCreateShortcutActivity`. This will create a pinned shortcut which is treated like an app by μLauncher, i.e. open μLauncher's activity to create a shortcut. + +screenshot +screenshot + + +[^1]: Only the default home screen can access shortcuts. diff --git a/docs/examples/termux/screenshot1.png b/docs/examples/termux/screenshot1.png new file mode 100644 index 0000000000000000000000000000000000000000..684c8bfebd7c30da1655870bd5c52e4ecf8dfe53 GIT binary patch literal 47999 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 vUYP>$iMm$J-Uc#9=@uY#f8 Date: Sat, 24 May 2025 19:22:08 +0200 Subject: [PATCH 091/118] add example to docs --- docs/examples/_index.md | 5 +++++ docs/examples/apps-on-home-screen.md | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 docs/examples/apps-on-home-screen.md diff --git a/docs/examples/_index.md b/docs/examples/_index.md index 0847315..b50540f 100644 --- a/docs/examples/_index.md +++ b/docs/examples/_index.md @@ -1,2 +1,7 @@ ++++ + bookCollapseSection = true + weight = 20 ++++ + # Examples This section contains some examples how μLauncher can be tweaked. diff --git a/docs/examples/apps-on-home-screen.md b/docs/examples/apps-on-home-screen.md new file mode 100644 index 0000000..f63c500 --- /dev/null +++ b/docs/examples/apps-on-home-screen.md @@ -0,0 +1,18 @@ ++++ + title = 'Showing Apps on the Home Screen' ++++ + +# Showing Apps on the Home Screen + +Even though this is somewhat contrary to the general idea of μLauncher, +it is possible to show apps on the home screen using widgets. + +Users suggested: +* [Launchy](https://launchywidget.com/) (proprietary!) +* KWGT Kustom Widget Maker (proprietary!) + +{{% hint danger %}} +Both of these apps are not open source and KWGT even has ads. + +Please contact me if you know FOSS alternatives! +{{% /hint %}} From ea5a4ad7bfd4b03a132c719ce94b1512620d73a4 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sat, 24 May 2025 19:22:08 +0200 Subject: [PATCH 092/118] add example to docs --- docs/actions-and-gestures.md | 4 ++++ docs/alternatives.md | 5 ++++- docs/build.md | 5 +++++ docs/contributing.md | 5 +++++ docs/examples/_index.md | 5 +++++ docs/examples/apps-on-home-screen.md | 18 ++++++++++++++++++ docs/profiles.md | 6 ++++++ docs/settings.md | 4 ++++ docs/widgets.md | 1 + 9 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 docs/examples/apps-on-home-screen.md diff --git a/docs/actions-and-gestures.md b/docs/actions-and-gestures.md index a5348a3..dc00970 100644 --- a/docs/actions-and-gestures.md +++ b/docs/actions-and-gestures.md @@ -1,3 +1,7 @@ ++++ + weight = 10 ++++ + # Actions and Gestures µLauncher's central mechanism for accessing important functionality quickly diff --git a/docs/alternatives.md b/docs/alternatives.md index 4c875f4..174ee8a 100644 --- a/docs/alternatives.md +++ b/docs/alternatives.md @@ -1,3 +1,6 @@ ++++ + weight = 100 ++++ -TODO: move the [hedgedoc](https://pad.abstractnonsen.se/foss-launchers) document here. +ODO: move the [hedgedoc](https://pad.abstractnonsen.se/foss-launchers) document here. diff --git a/docs/build.md b/docs/build.md index 1ffc338..a45ab4c 100644 --- a/docs/build.md +++ b/docs/build.md @@ -1,3 +1,8 @@ ++++ + weight = 50 ++++ + + # Building from Source ## Using the command line diff --git a/docs/contributing.md b/docs/contributing.md index 350ba01..f74bcca 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1,3 +1,8 @@ ++++ + weight = 40 ++++ + + # Contributing There are several ways to contribute to this app: diff --git a/docs/examples/_index.md b/docs/examples/_index.md index 0847315..b50540f 100644 --- a/docs/examples/_index.md +++ b/docs/examples/_index.md @@ -1,2 +1,7 @@ ++++ + bookCollapseSection = true + weight = 20 ++++ + # Examples This section contains some examples how μLauncher can be tweaked. diff --git a/docs/examples/apps-on-home-screen.md b/docs/examples/apps-on-home-screen.md new file mode 100644 index 0000000..f63c500 --- /dev/null +++ b/docs/examples/apps-on-home-screen.md @@ -0,0 +1,18 @@ ++++ + title = 'Showing Apps on the Home Screen' ++++ + +# Showing Apps on the Home Screen + +Even though this is somewhat contrary to the general idea of μLauncher, +it is possible to show apps on the home screen using widgets. + +Users suggested: +* [Launchy](https://launchywidget.com/) (proprietary!) +* KWGT Kustom Widget Maker (proprietary!) + +{{% hint danger %}} +Both of these apps are not open source and KWGT even has ads. + +Please contact me if you know FOSS alternatives! +{{% /hint %}} diff --git a/docs/profiles.md b/docs/profiles.md index d9eaf52..2087d06 100644 --- a/docs/profiles.md +++ b/docs/profiles.md @@ -1,3 +1,9 @@ ++++ + title = 'User Profiles' + weight = 12 ++++ + + # Work Profile µLauncher is compatible with [work profile](https://www.android.com/enterprise/work-profile/), so apps like [Shelter](https://gitea.angry.im/PeterCxy/Shelter) can be used. diff --git a/docs/settings.md b/docs/settings.md index 3cbfbec..eff0417 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -1,3 +1,7 @@ ++++ + weight = 10 ++++ + # Settings Tweaks and customizations can be made from within the settings page. diff --git a/docs/widgets.md b/docs/widgets.md index ab5e21f..acbdf05 100644 --- a/docs/widgets.md +++ b/docs/widgets.md @@ -1,5 +1,6 @@ +++ title = 'Widgets' + weight = 11 +++ # Widgets From 580644f9d4a756499c31277a3d4170afca838977 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sat, 24 May 2025 20:29:01 +0200 Subject: [PATCH 093/118] add documentation of app drawer --- docs/app-drawer.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++ docs/settings.md | 10 --------- 2 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 docs/app-drawer.md diff --git a/docs/app-drawer.md b/docs/app-drawer.md new file mode 100644 index 0000000..d977c28 --- /dev/null +++ b/docs/app-drawer.md @@ -0,0 +1,54 @@ ++++ + weight = 10 ++++ + +# App Drawer + +Apps that are not needed all the time are shown in the app drawer. +There are several such drawers, but the basic concept is the same. +Besides regular apps, app drawers also show [pinned shortcuts](https://developer.android.com/develop/ui/views/launch/shortcuts/creating-shortcuts)[^1]. +μLauncher treats apps and shortcuts in the same way. + +The idea of the app drawer is to search for apps using the keyboard. +By default[^2] an app is launched automatically once it is the only app matching the query. +This can be prevented by typing space. +Usually only two or three characters are needed which is much faster than scrolling to find an app. + +[^1]: A pinned shortcut is created for example when pinning a website to the home screen. +[^2]: There are [several settings](/docs/settings/#functionality) available to modify the behavior. + +When long clicking an app, additional options are shown: +* Rename the app +* Add to / remove from Favorites: Adds the app to the [Favorite Apps](#favorite-apps) list or removes it from there. +* Hide / Show: This hides the app from all drawers (except from [Hidden Apps](#hidden-apps)) or makes it visible again if it was hidden. +* App Info: Opens the system settings page for the app. +* Uninstall: Tries to uninstall the app or remove the shortcut. + +## All Apps + +This lists all apps except hidden apps. +By default it is bound to `Swipe up`. + +## Favorite Apps + +Only shows favorite apps. +Pressing the star icon on the bottom right of any app drawer toggles whether +only favorite apps should be shown. +Additionally the `Favorite Apps` action can be used to launch this drawer directly. +By default it is bound to `Swipe up (left edge)`. + +## Private Space + +When [private space](/docs/profiles/#private-space) is available, this drawer +shows only apps from the private space. +It can be opened using the `Private Space` action. +If the private space is locked, instead of showing the list the unlock dialog is shown. + +By default, apps from the private space are shown in All Apps as well, +however this is [configurable](/docs/settings/#hide-private-space-from-app-list). + +## Hidden Apps + +This list shows hidden apps. +It is only accessible through the settings. +The feature is intended to be used only for apps which are not needed at all but [can not be uninstalled](https://en.wikipedia.org/wiki/Software_bloat#Bloatware). diff --git a/docs/settings.md b/docs/settings.md index eff0417..eb780fc 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -234,13 +234,3 @@ Remove the top status bar from the home screen. Remove the navigation bar from the home screen. Enabling this setting may make it difficult to use the device if gestures are not setup properly. **type:** `toggle` - -## Additional Settings - -### App Drawer Long Press on App - -Access additional per-app details and settings. To use, open the app drawer and long press on any app. - -**type:** `dropdown` - -**options:** `App Info`,`Add to favorites`,`Hide`,`Rename`,`Uninstall` From 71193a2e50b03bf46f16a37cdea357740563fb74 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sat, 24 May 2025 20:34:15 +0200 Subject: [PATCH 094/118] fixed typo --- docs/build.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/build.md b/docs/build.md index a45ab4c..3d1605c 100644 --- a/docs/build.md +++ b/docs/build.md @@ -42,7 +42,7 @@ for further instructions. Install [Android Studio](https://developer.android.com/studio), import this project and build it. See [this guide](https://developer.android.com/studio/run) -for further instructions. How to +for further instructions. ## CI Pipeline From 1d10d65adbc35c4f18945fe4edc41b10a6177365 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sat, 24 May 2025 20:41:37 +0200 Subject: [PATCH 095/118] update documentation --- docs/build.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/build.md b/docs/build.md index 3d1605c..28e889a 100644 --- a/docs/build.md +++ b/docs/build.md @@ -47,4 +47,8 @@ for further instructions. ## CI Pipeline The [CI pipeline](https://github.com/jrpie/Launcher/actions) automatically creates debug builds. -> Note: These builds are *not* signed. They are in built in debug mode and only suitable for testing. + +{{% hint warning %}} +Note: These builds are not signed. +They are in built in debug mode and only suitable for testing. +{{% /hint %}} From 85a7ed24ab2e6816f97e6cbe3feb3add3eda5465 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sat, 24 May 2025 21:08:45 +0200 Subject: [PATCH 096/118] add documentation button --- .../launcher/ui/settings/meta/SettingsFragmentMeta.kt | 3 +++ app/src/main/res/layout/settings_meta.xml | 7 +++++++ app/src/main/res/values/donottranslate.xml | 1 + app/src/main/res/values/strings.xml | 1 + 4 files changed, 12 insertions(+) diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/settings/meta/SettingsFragmentMeta.kt b/app/src/main/java/de/jrpie/android/launcher/ui/settings/meta/SettingsFragmentMeta.kt index 759d8cd..1a0e802 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/settings/meta/SettingsFragmentMeta.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/settings/meta/SettingsFragmentMeta.kt @@ -79,6 +79,9 @@ class SettingsFragmentMeta : Fragment(), UIObject { // view code bindURL(binding.settingsMetaButtonViewCode, R.string.settings_meta_link_github) + // view documentation + bindURL(binding.settingsMetaButtonViewDocs, R.string.settings_meta_link_docs) + // report a bug binding.settingsMetaButtonReportBug.setOnClickListener { val deviceInfo = getDeviceInfo() diff --git a/app/src/main/res/layout/settings_meta.xml b/app/src/main/res/layout/settings_meta.xml index 6f21baa..1adee37 100644 --- a/app/src/main/res/layout/settings_meta.xml +++ b/app/src/main/res/layout/settings_meta.xml @@ -41,6 +41,13 @@ android:text="@string/settings_meta_view_code" android:textAllCaps="false" /> +