diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0f9a8e9c1..806edc12d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -30,6 +30,7 @@
+
@@ -157,6 +158,18 @@
+
+
+
+
+
+
+
(null)
private var androidApps by mutableStateOf>(emptyList())
+ private var androidAppsListing = 0
+ private val packageCallback = object : android.content.pm.LauncherApps.Callback() {
+ private fun changed() {
+ refreshAndroidApps()
+ if (HomeWidgets.providers != null) HomeWidgets.refreshProviders(this@FrontEndActivity)
+ }
+ override fun onPackageRemoved(packageName: String, user: android.os.UserHandle) = changed()
+ override fun onPackageAdded(packageName: String, user: android.os.UserHandle) = changed()
+ override fun onPackageChanged(packageName: String, user: android.os.UserHandle) = changed()
+ override fun onPackagesAvailable(packageNames: Array, user: android.os.UserHandle, replacing: Boolean) = changed()
+ override fun onPackagesUnavailable(packageNames: Array, user: android.os.UserHandle, replacing: Boolean) = changed()
+ override fun onShortcutsChanged(packageName: String, shortcuts: MutableList, user: android.os.UserHandle) {
+ if (homeAppSelected) HomePins.refresh(this@FrontEndActivity)
+ }
+ }
private var secondScreenDisplays by mutableStateOf>(emptyList())
private lateinit var displayManager: DisplayManager
private val secondScreenDisplayListener = object : DisplayManager.DisplayListener {
@@ -1568,6 +1583,15 @@ class FrontEndActivity : ComponentActivity() {
})
displayManager.registerDisplayListener(secondScreenDisplayListener, ui)
refreshSecondScreenDisplays()
+ getSystemService(android.content.pm.LauncherApps::class.java).registerCallback(packageCallback, ui)
+ HomeWidgets.startListening(this)
+ }
+
+ @Suppress("OVERRIDE_DEPRECATION")
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ if (HomeWidgets.onActivityResult(this, requestCode, resultCode)) return
+ @Suppress("DEPRECATION")
+ super.onActivityResult(requestCode, resultCode, data)
}
override fun onSaveInstanceState(outState: Bundle) {
@@ -1593,6 +1617,8 @@ class FrontEndActivity : ComponentActivity() {
override fun onStop() {
unregisterReceiver(wifiLocationReceiver)
unregisterReceiver(mediaReceiver)
+ getSystemService(android.content.pm.LauncherApps::class.java).unregisterCallback(packageCallback)
+ HomeWidgets.stopListening(this)
ui.removeCallbacks(mediaRefresh)
com.droiddeck.launcher.stores.StoresState.libraryListener = null
com.droiddeck.launcher.stores.battlenet.BattleNetClient.opener = null
@@ -1608,10 +1634,24 @@ class FrontEndActivity : ComponentActivity() {
homeScreenEnabled = HomeApp.isHomeScreenEnabled(this)
homeAppSelected = homeScreenEnabled && HomeApp.isDefault(this)
defaultHomeLabel = HomeApp.defaultLabel(this)
- androidApps = if (homeAppSelected) HomeApp.launchableApps(this) else emptyList()
+ refreshAndroidApps()
integrationState = readIntegration()
}
+ private fun refreshAndroidApps() {
+ if (!homeAppSelected) {
+ ++androidAppsListing
+ androidApps = emptyList()
+ return
+ }
+ val seq = ++androidAppsListing
+ HomePins.refresh(this)
+ Thread({
+ val apps = runCatching { HomeApp.launchableApps(this) }.getOrDefault(emptyList())
+ ui.post { if (seq == androidAppsListing) androidApps = apps }
+ }, "home-apps").start()
+ }
+
private fun refreshSecondScreenDisplays() {
if (!::displayManager.isInitialized) return
// The launcher can itself be shown on a non-default display (DeX, an external monitor); that one is not a "second screen".
diff --git a/app/src/main/java/com/droiddeck/launcher/HomeApp.kt b/app/src/main/java/com/droiddeck/launcher/HomeApp.kt
index 819a6525d..9d0ae53c1 100644
--- a/app/src/main/java/com/droiddeck/launcher/HomeApp.kt
+++ b/app/src/main/java/com/droiddeck/launcher/HomeApp.kt
@@ -6,7 +6,15 @@ import android.graphics.Bitmap
import android.content.ComponentName
import android.content.Context
import android.content.Intent
+import android.content.pm.ApplicationInfo
+import android.content.pm.LauncherApps
+import android.content.pm.ShortcutInfo
+import android.graphics.Rect
+import android.os.Process
+import android.os.UserHandle
+import android.os.UserManager
import android.content.pm.PackageManager
+import android.net.Uri
import android.os.Build
import android.provider.Settings
import androidx.core.graphics.drawable.toBitmap
@@ -22,7 +30,17 @@ object HomeApp {
val packageName: String,
val className: String,
val icon: Bitmap?,
- )
+ val system: Boolean = false,
+ val game: Boolean = false,
+ val installedAt: Long = 0L,
+ val updatedAt: Long = 0L,
+ val user: UserHandle? = null,
+ val userSerial: Long = 0L,
+ ) {
+ val component get() = ComponentName(packageName, className)
+ val key get() = "$packageName/$className" + if (userSerial != 0L) "#$userSerial" else ""
+ val work get() = userSerial != 0L
+ }
fun isHomeScreenEnabled(context: Context): Boolean =
context.packageManager.getComponentEnabledSetting(ComponentName(context, HomeActivity::class.java)) ==
@@ -72,34 +90,79 @@ object HomeApp {
fun settingsIntent(): Intent = Intent(Settings.ACTION_HOME_SETTINGS)
fun launchableApps(context: Context): List {
+ val launcher = context.getSystemService(LauncherApps::class.java)
+ val users = context.getSystemService(UserManager::class.java)
val pm = context.packageManager
- val launch = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER)
+ val own = Process.myUserHandle()
val iconPx = (ICON_DP * context.resources.displayMetrics.density).roundToInt()
- val apps = pm.queryIntentActivities(launch, 0)
- .asSequence()
- .mapNotNull { result ->
- val activity = result.activityInfo ?: return@mapNotNull null
- if (!activity.exported || activity.packageName == context.packageName) return@mapNotNull null
- val label = result.loadLabel(pm)?.toString()?.trim().orEmpty().ifEmpty { activity.packageName }
- val icon = runCatching { result.loadIcon(pm).toBitmap(iconPx, iconPx) }.getOrNull()
- LaunchableApp(label, activity.packageName, activity.name, icon)
+ val apps = users.userProfiles.flatMap { user ->
+ val serial = if (user == own) 0L else users.getSerialNumberForUser(user)
+ runCatching { launcher.getActivityList(null, user) }.getOrDefault(emptyList()).mapNotNull { info ->
+ val component = info.componentName
+ if (component.packageName == context.packageName && user == own) return@mapNotNull null
+ val label = info.label?.toString()?.trim().orEmpty().ifEmpty { component.packageName }
+ val icon = runCatching { info.getBadgedIcon(0).toBitmap(iconPx, iconPx) }.getOrNull()
+ val app = info.applicationInfo
+ val updated = if (user == own) runCatching { pm.getPackageInfo(component.packageName, 0).lastUpdateTime }.getOrNull() else null
+ @Suppress("DEPRECATION")
+ val game = app.category == ApplicationInfo.CATEGORY_GAME || app.flags and ApplicationInfo.FLAG_IS_GAME != 0
+ LaunchableApp(
+ label, component.packageName, component.className, icon,
+ system = app.flags and (ApplicationInfo.FLAG_SYSTEM or ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0,
+ game = game,
+ installedAt = info.firstInstallTime,
+ updatedAt = updated ?: info.firstInstallTime,
+ user = user,
+ userSerial = serial,
+ )
}
- .distinctBy { it.packageName }
- .toList()
- return apps.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.label })
+ }
+ return apps.distinctBy { it.key }.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.label })
}
- fun launch(context: Context, app: LaunchableApp, displayId: Int? = null) {
- val intent = Intent(Intent.ACTION_MAIN)
- .addCategory(Intent.CATEGORY_LAUNCHER)
- .setComponent(ComponentName(app.packageName, app.className))
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- if (displayId == null) {
- context.startActivity(intent)
- return
- }
- val options = ActivityOptions.makeBasic().setLaunchDisplayId(displayId)
- context.startActivity(intent, options.toBundle())
+ fun launch(context: Context, app: LaunchableApp, displayId: Int? = null, bounds: Rect? = null) {
+ val options = displayId?.let { ActivityOptions.makeBasic().setLaunchDisplayId(it).toBundle() }
+ val launcher = context.getSystemService(LauncherApps::class.java)
+ launcher.startMainActivity(app.component, app.user ?: Process.myUserHandle(), bounds, options)
+ }
+
+ fun canUseShortcuts(context: Context): Boolean =
+ runCatching { context.getSystemService(LauncherApps::class.java).hasShortcutHostPermission() }.getOrDefault(false)
+
+ fun shortcuts(context: Context, app: LaunchableApp): List {
+ if (!canUseShortcuts(context)) return emptyList()
+ val query = LauncherApps.ShortcutQuery()
+ .setPackage(app.packageName)
+ .setActivity(app.component)
+ .setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC or LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST)
+ return runCatching { context.getSystemService(LauncherApps::class.java).getShortcuts(query, app.user ?: Process.myUserHandle()) }
+ .getOrNull().orEmpty()
+ .filter { it.isEnabled }
+ .sortedWith(compareBy { if (it.isDeclaredInManifest) 0 else 1 }.thenBy { it.rank })
+ .take(5)
+ }
+
+ fun shortcutIcon(context: Context, info: ShortcutInfo, px: Int = (ICON_DP * context.resources.displayMetrics.density).roundToInt()): Bitmap? = runCatching {
+ context.getSystemService(LauncherApps::class.java)
+ .getShortcutIconDrawable(info, context.resources.displayMetrics.densityDpi)?.toBitmap(px, px)
+ }.getOrNull()
+
+ fun startShortcut(context: Context, info: ShortcutInfo) {
+ context.getSystemService(LauncherApps::class.java).startShortcut(info, null, null)
+ }
+
+ fun openAppInfo(context: Context, app: LaunchableApp) {
+ context.getSystemService(LauncherApps::class.java)
+ .startAppDetailsActivity(app.component, app.user ?: Process.myUserHandle(), null, null)
+ }
+
+ fun canUninstall(app: LaunchableApp): Boolean = !app.system && !app.work
+
+ fun uninstall(context: Context, app: LaunchableApp) {
+ context.startActivity(
+ Intent(Intent.ACTION_DELETE, Uri.fromParts("package", app.packageName, null))
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
+ )
}
fun isActiveSteamSession(): Boolean =
diff --git a/app/src/main/java/com/droiddeck/launcher/HomeFolders.kt b/app/src/main/java/com/droiddeck/launcher/HomeFolders.kt
new file mode 100644
index 000000000..dc1f0bca4
--- /dev/null
+++ b/app/src/main/java/com/droiddeck/launcher/HomeFolders.kt
@@ -0,0 +1,65 @@
+package com.droiddeck.launcher
+
+import android.content.Context
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import org.json.JSONArray
+import org.json.JSONObject
+import java.util.UUID
+
+object HomeFolders {
+ data class Folder(val id: String, val name: String, val apps: List)
+
+ private const val PREFS = "home_launcher"
+ private const val KEY = "folders"
+
+ var folders by mutableStateOf>(emptyList())
+ private set
+ private var loaded = false
+
+ fun load(context: Context) {
+ if (loaded) return
+ loaded = true
+ val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(KEY, null) ?: return
+ folders = runCatching {
+ val array = JSONArray(raw)
+ (0 until array.length()).map { i ->
+ val o = array.getJSONObject(i)
+ val apps = o.getJSONArray("apps")
+ Folder(o.getString("id"), o.getString("name"), (0 until apps.length()).map { apps.getString(it) })
+ }
+ }.getOrDefault(emptyList())
+ }
+
+ fun create(context: Context, name: String, appKey: String): Folder {
+ val folder = Folder(UUID.randomUUID().toString(), name, listOf(appKey))
+ update(context, without(appKey) + folder)
+ return folder
+ }
+
+ fun add(context: Context, folderId: String, appKey: String) =
+ update(context, without(appKey).map { if (it.id == folderId) it.copy(apps = it.apps + appKey) else it })
+
+ fun remove(context: Context, appKey: String) = update(context, without(appKey))
+
+ fun rename(context: Context, folderId: String, name: String) =
+ update(context, folders.map { if (it.id == folderId) it.copy(name = name) else it })
+
+ fun delete(context: Context, folderId: String) = update(context, folders.filterNot { it.id == folderId })
+
+ fun migrateMembers(context: Context, keyFor: (String) -> String?) {
+ if (folders.none { f -> f.apps.any { '/' !in it } }) return
+ update(context, folders.map { f -> f.copy(apps = f.apps.map { if ('/' in it) it else keyFor(it) ?: it }.distinct()) })
+ }
+
+ private fun without(appKey: String): List =
+ folders.map { it.copy(apps = it.apps - appKey) }.filter { it.apps.isNotEmpty() }
+
+ private fun update(context: Context, next: List) {
+ folders = next
+ val array = JSONArray()
+ for (f in next) array.put(JSONObject().put("id", f.id).put("name", f.name).put("apps", JSONArray(f.apps)))
+ context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().putString(KEY, array.toString()).apply()
+ }
+}
diff --git a/app/src/main/java/com/droiddeck/launcher/HomeHidden.kt b/app/src/main/java/com/droiddeck/launcher/HomeHidden.kt
new file mode 100644
index 000000000..70f98f484
--- /dev/null
+++ b/app/src/main/java/com/droiddeck/launcher/HomeHidden.kt
@@ -0,0 +1,26 @@
+package com.droiddeck.launcher
+
+import android.content.Context
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+
+object HomeHidden {
+ private const val PREFS = "home_launcher"
+ private const val KEY = "hidden"
+
+ var keys by mutableStateOf>(emptySet())
+ private set
+ private var loaded = false
+
+ fun load(context: Context) {
+ if (loaded) return
+ loaded = true
+ keys = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getStringSet(KEY, emptySet()).orEmpty().toSet()
+ }
+
+ fun set(context: Context, key: String, hidden: Boolean) {
+ keys = if (hidden) keys + key else keys - key
+ context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().putStringSet(KEY, keys).apply()
+ }
+}
diff --git a/app/src/main/java/com/droiddeck/launcher/HomePins.kt b/app/src/main/java/com/droiddeck/launcher/HomePins.kt
new file mode 100644
index 000000000..1040995e8
--- /dev/null
+++ b/app/src/main/java/com/droiddeck/launcher/HomePins.kt
@@ -0,0 +1,75 @@
+package com.droiddeck.launcher
+
+import android.content.Context
+import android.content.pm.LauncherApps
+import android.content.pm.ShortcutInfo
+import android.graphics.Bitmap
+import android.os.Handler
+import android.os.Looper
+import android.os.Process
+import android.os.UserManager
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+
+/** Shortcuts other apps asked to put on the home screen. The system keeps the pinned set per launcher. */
+object HomePins {
+ class Pinned(val info: ShortcutInfo, val icon: Bitmap?, val appLabel: String, val userSerial: Long) {
+ val key get() = "pin:${info.`package`}/${info.id}" + if (userSerial != 0L) "#$userSerial" else ""
+ val label get() = info.shortLabel?.toString()?.trim().orEmpty().ifEmpty { appLabel }
+ val work get() = userSerial != 0L
+ }
+
+ var pinned by mutableStateOf>(emptyList())
+ private set
+ private val main = Handler(Looper.getMainLooper())
+ private var listing = 0
+
+ fun refresh(context: Context) {
+ val app = context.applicationContext
+ val seq = ++listing
+ Thread({
+ val list = runCatching { load(app) }.getOrDefault(emptyList())
+ main.post { if (seq == listing) pinned = list }
+ }, "home-pins").start()
+ }
+
+ private fun load(context: Context): List {
+ if (!HomeApp.canUseShortcuts(context)) return emptyList()
+ val launcher = context.getSystemService(LauncherApps::class.java)
+ val users = context.getSystemService(UserManager::class.java)
+ val pm = context.packageManager
+ val own = Process.myUserHandle()
+ val query = LauncherApps.ShortcutQuery().setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED)
+ return users.userProfiles.flatMap { user ->
+ val serial = if (user == own) 0L else users.getSerialNumberForUser(user)
+ runCatching { launcher.getShortcuts(query, user) }.getOrNull().orEmpty()
+ .filter { it.isEnabled && it.isPinned }
+ .map { info ->
+ val appLabel = runCatching { pm.getApplicationLabel(pm.getApplicationInfo(info.`package`, 0)).toString() }
+ .getOrDefault(info.`package`)
+ Pinned(info, HomeApp.shortcutIcon(context, info), appLabel, serial)
+ }
+ }
+ }
+
+ fun accept(context: Context, request: LauncherApps.PinItemRequest): Boolean {
+ val ok = runCatching { request.isValid && request.accept() }.getOrDefault(false)
+ if (ok) refresh(context)
+ return ok
+ }
+
+ fun unpin(context: Context, pinned: Pinned) {
+ val info = pinned.info
+ val launcher = context.getSystemService(LauncherApps::class.java)
+ val query = LauncherApps.ShortcutQuery().setPackage(info.`package`).setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED)
+ val unpinned = runCatching {
+ val current = checkNotNull(launcher.getShortcuts(query, info.userHandle))
+ val keep = current.map { it.id } - info.id
+ launcher.pinShortcuts(info.`package`, keep, info.userHandle)
+ }.isSuccess
+ if (!unpinned) return
+ listing++
+ this.pinned = this.pinned.filterNot { it.key == pinned.key }
+ }
+}
diff --git a/app/src/main/java/com/droiddeck/launcher/HomeWidgets.kt b/app/src/main/java/com/droiddeck/launcher/HomeWidgets.kt
new file mode 100644
index 000000000..8f79f4354
--- /dev/null
+++ b/app/src/main/java/com/droiddeck/launcher/HomeWidgets.kt
@@ -0,0 +1,317 @@
+package com.droiddeck.launcher
+
+import android.app.Activity
+import android.appwidget.AppWidgetHost
+import android.appwidget.AppWidgetHostView
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProviderInfo
+import android.content.Context
+import android.content.Intent
+import android.content.pm.LauncherApps
+import android.graphics.Bitmap
+import android.os.Build
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.util.SizeF
+import android.view.HapticFeedbackConstants
+import android.view.MotionEvent
+import android.view.ViewConfiguration
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.core.graphics.drawable.toBitmap
+import org.json.JSONArray
+import org.json.JSONObject
+import kotlin.math.hypot
+import kotlin.math.roundToInt
+
+object HomeWidgets {
+ data class Placed(val id: Int, val widthDp: Int, val heightDp: Int)
+
+ data class Provider(
+ val info: AppWidgetProviderInfo,
+ val label: String,
+ val appLabel: String,
+ val preview: Bitmap?,
+ val widthDp: Int,
+ val heightDp: Int,
+ )
+
+ private const val HOST_ID = 0x0DDE
+ private const val PREFS = "home_launcher"
+ private const val KEY = "widgets"
+ private const val REQUEST_BIND = 0x0DD1
+ private const val REQUEST_CONFIGURE = 0x0DD2
+ private const val REQUEST_RECONFIGURE = 0x0DD3
+ const val MIN_WIDTH_DP = 96
+ const val MIN_HEIGHT_DP = 56
+
+ var widgets by mutableStateOf>(emptyList())
+ private set
+ var providers by mutableStateOf?>(null)
+ private set
+ private var loaded = false
+ private var host: Host? = null
+ private var pending: AppWidgetProviderInfo? = null
+ private var pendingId = AppWidgetManager.INVALID_APPWIDGET_ID
+ private var pinRequest: LauncherApps.PinItemRequest? = null
+ private val main = Handler(Looper.getMainLooper())
+
+ private class Host(context: Context) : AppWidgetHost(context, HOST_ID) {
+ override fun onCreateView(context: Context, appWidgetId: Int, appWidget: AppWidgetProviderInfo?): AppWidgetHostView =
+ HomeWidgetView(context)
+ }
+
+ class HomeWidgetView(context: Context) : AppWidgetHostView(context) {
+ var onLongPress: (() -> Unit)? = null
+ private var fired = false
+ private var downX = 0f
+ private var downY = 0f
+ private val slop = ViewConfiguration.get(context).scaledTouchSlop
+ private val longPress = Runnable {
+ fired = true
+ performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
+ onLongPress?.invoke()
+ }
+
+ override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
+ when (ev.actionMasked) {
+ MotionEvent.ACTION_DOWN -> {
+ fired = false
+ downX = ev.x
+ downY = ev.y
+ postDelayed(longPress, ViewConfiguration.getLongPressTimeout().toLong())
+ }
+ MotionEvent.ACTION_MOVE -> if (hypot(ev.x - downX, ev.y - downY) > slop) removeCallbacks(longPress)
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_POINTER_DOWN -> removeCallbacks(longPress)
+ }
+ return fired
+ }
+
+ override fun onTouchEvent(event: MotionEvent): Boolean {
+ if (fired) {
+ if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_CANCEL) fired = false
+ return true
+ }
+ return super.onTouchEvent(event)
+ }
+ }
+
+ private fun host(context: Context): Host = host ?: Host(context.applicationContext).also { host = it }
+
+ fun load(context: Context) {
+ if (loaded) return
+ loaded = true
+ val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(KEY, null) ?: return
+ val manager = AppWidgetManager.getInstance(context)
+ val parsed = runCatching {
+ val array = JSONArray(raw)
+ (0 until array.length()).map { i ->
+ val o = array.getJSONObject(i)
+ Placed(o.getInt("id"), o.getInt("w"), o.getInt("h"))
+ }
+ }.getOrDefault(emptyList())
+ val live = parsed.filter { manager.getAppWidgetInfo(it.id) != null }
+ parsed.filterNot { it in live }.forEach { host(context).deleteAppWidgetId(it.id) }
+ widgets = live
+ if (live.size != parsed.size) save(context)
+ }
+
+ fun startListening(context: Context) {
+ load(context)
+ runCatching { host(context).startListening() }
+ }
+
+ fun stopListening(context: Context) {
+ runCatching { host(context).stopListening() }
+ }
+
+ fun info(context: Context, id: Int): AppWidgetProviderInfo? = AppWidgetManager.getInstance(context).getAppWidgetInfo(id)
+
+ fun createView(context: Context, id: Int): HomeWidgetView? {
+ val info = info(context, id) ?: return null
+ return host(context).createView(context, id, info) as? HomeWidgetView
+ }
+
+ fun applySize(view: AppWidgetHostView, widthDp: Int, heightDp: Int) {
+ if (Build.VERSION.SDK_INT >= 31) {
+ view.updateAppWidgetSize(Bundle(), listOf(SizeF(widthDp.toFloat(), heightDp.toFloat())))
+ } else {
+ @Suppress("DEPRECATION")
+ view.updateAppWidgetSize(null, widthDp, heightDp, widthDp, heightDp)
+ }
+ }
+
+ fun loadProviders(context: Context) {
+ if (providers != null) return
+ val app = context.applicationContext
+ Thread({
+ val pm = app.packageManager
+ val manager = AppWidgetManager.getInstance(app)
+ val dpi = app.resources.displayMetrics.densityDpi
+ val list = manager.installedProviders.mapNotNull { info ->
+ runCatching {
+ val appLabel = runCatching { pm.getApplicationLabel(pm.getApplicationInfo(info.provider.packageName, 0)).toString() }
+ .getOrDefault(info.provider.packageName)
+ val drawable = info.loadPreviewImage(app, dpi) ?: info.loadIcon(app, dpi)
+ val preview = drawable?.let { d ->
+ val w = d.intrinsicWidth.takeIf { it > 0 } ?: 192
+ val h = d.intrinsicHeight.takeIf { it > 0 } ?: 192
+ val scale = minOf(1f, 360f / maxOf(w, h))
+ d.toBitmap((w * scale).roundToInt().coerceAtLeast(1), (h * scale).roundToInt().coerceAtLeast(1))
+ }
+ val (w, h) = defaultSize(app, info)
+ Provider(info, info.loadLabel(pm)?.toString()?.trim().orEmpty().ifEmpty { appLabel }, appLabel, preview, w, h)
+ }.getOrNull()
+ }.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, Provider::appLabel).thenBy(String.CASE_INSENSITIVE_ORDER, Provider::label))
+ main.post { providers = list }
+ }, "widget-providers").start()
+ }
+
+ fun refreshProviders(context: Context) {
+ providers = null
+ loadProviders(context)
+ }
+
+ private fun defaultSize(context: Context, info: AppWidgetProviderInfo): Pair {
+ val density = context.resources.displayMetrics.density
+ var w = (info.minWidth / density).roundToInt()
+ var h = (info.minHeight / density).roundToInt()
+ if (Build.VERSION.SDK_INT >= 31) {
+ if (info.targetCellWidth > 0) w = maxOf(w, info.targetCellWidth * 80)
+ if (info.targetCellHeight > 0) h = maxOf(h, info.targetCellHeight * 80)
+ }
+ return w.coerceIn(160, 560) to h.coerceIn(MIN_HEIGHT_DP, 360)
+ }
+
+ fun minSize(context: Context, id: Int): Pair {
+ val info = info(context, id) ?: return MIN_WIDTH_DP to MIN_HEIGHT_DP
+ val density = context.resources.displayMetrics.density
+ val w = (if (info.minResizeWidth > 0) info.minResizeWidth else info.minWidth) / density
+ val h = (if (info.minResizeHeight > 0) info.minResizeHeight else info.minHeight) / density
+ return w.roundToInt().coerceAtLeast(MIN_WIDTH_DP) to h.roundToInt().coerceAtLeast(MIN_HEIGHT_DP)
+ }
+
+ fun add(activity: Activity, provider: Provider) {
+ if (pendingId == AppWidgetManager.INVALID_APPWIDGET_ID) add(activity, provider.info, null)
+ }
+
+ /** Places a widget another app asked for; its own app has already configured it. */
+ fun addPinned(activity: Activity, request: LauncherApps.PinItemRequest): Boolean {
+ if (pendingId != AppWidgetManager.INVALID_APPWIDGET_ID) return false
+ val info = request.getAppWidgetProviderInfo(activity) ?: return false
+ load(activity)
+ add(activity, info, request)
+ return pending != null
+ }
+
+ private fun add(activity: Activity, info: AppWidgetProviderInfo, request: LauncherApps.PinItemRequest?) {
+ val h = host(activity)
+ val id = h.allocateAppWidgetId()
+ pending = info
+ pendingId = id
+ pinRequest = request
+ val manager = AppWidgetManager.getInstance(activity)
+ val bound = runCatching { manager.bindAppWidgetIdIfAllowed(id, info.profile, info.provider, null) }.getOrDefault(false)
+ if (bound) {
+ bound(activity, id, info)
+ return
+ }
+ val bind = Intent(AppWidgetManager.ACTION_APPWIDGET_BIND)
+ .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id)
+ .putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, info.provider)
+ .putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER_PROFILE, info.profile)
+ @Suppress("DEPRECATION")
+ runCatching { activity.startActivityForResult(bind, REQUEST_BIND) }.onFailure { abandon(activity) }
+ }
+
+ private fun bound(activity: Activity, id: Int, info: AppWidgetProviderInfo) {
+ val request = pinRequest
+ if (request == null) {
+ configure(activity, id, info)
+ return
+ }
+ val accepted = runCatching { request.isValid && request.accept(Bundle().apply { putInt(AppWidgetManager.EXTRA_APPWIDGET_ID, id) }) }
+ .getOrDefault(false)
+ if (accepted) place(activity, id, info) else abandon(activity)
+ }
+
+ private fun configure(activity: Activity, id: Int, info: AppWidgetProviderInfo) {
+ val optional = Build.VERSION.SDK_INT >= 31 &&
+ info.widgetFeatures and AppWidgetProviderInfo.WIDGET_FEATURE_CONFIGURATION_OPTIONAL != 0
+ if (info.configure == null || optional) {
+ place(activity, id, info)
+ return
+ }
+ runCatching { host(activity).startAppWidgetConfigureActivityForResult(activity, id, 0, REQUEST_CONFIGURE, null) }
+ .onFailure { place(activity, id, info) }
+ }
+
+ fun canReconfigure(context: Context, id: Int): Boolean {
+ val info = info(context, id) ?: return false
+ if (info.configure == null) return false
+ return Build.VERSION.SDK_INT < 28 || info.widgetFeatures and AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE != 0
+ }
+
+ fun reconfigure(activity: Activity, id: Int) {
+ runCatching { host(activity).startAppWidgetConfigureActivityForResult(activity, id, 0, REQUEST_RECONFIGURE, null) }
+ }
+
+ fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int): Boolean {
+ when (requestCode) {
+ REQUEST_BIND -> {
+ val info = pending
+ if (resultCode == Activity.RESULT_OK && info != null) bound(activity, pendingId, info) else abandon(activity)
+ }
+ REQUEST_CONFIGURE -> {
+ val info = pending
+ if (resultCode == Activity.RESULT_OK && info != null) place(activity, pendingId, info) else abandon(activity)
+ }
+ REQUEST_RECONFIGURE -> Unit
+ else -> return false
+ }
+ return true
+ }
+
+ private fun abandon(context: Context) {
+ if (pendingId != AppWidgetManager.INVALID_APPWIDGET_ID) host(context).deleteAppWidgetId(pendingId)
+ pending = null
+ pinRequest = null
+ pendingId = AppWidgetManager.INVALID_APPWIDGET_ID
+ }
+
+ private fun place(context: Context, id: Int, info: AppWidgetProviderInfo) {
+ val (w, h) = defaultSize(context, info)
+ pending = null
+ pinRequest = null
+ pendingId = AppWidgetManager.INVALID_APPWIDGET_ID
+ widgets = widgets + Placed(id, w, h)
+ save(context)
+ }
+
+ fun resize(context: Context, id: Int, widthDp: Int, heightDp: Int) {
+ widgets = widgets.map { if (it.id == id) it.copy(widthDp = widthDp, heightDp = heightDp) else it }
+ save(context)
+ }
+
+ fun move(context: Context, id: Int, by: Int) {
+ val i = widgets.indexOfFirst { it.id == id }
+ val j = i + by
+ if (i < 0 || j !in widgets.indices) return
+ widgets = widgets.toMutableList().apply { add(j, removeAt(i)) }
+ save(context)
+ }
+
+ fun remove(context: Context, id: Int) {
+ host(context).deleteAppWidgetId(id)
+ widgets = widgets.filterNot { it.id == id }
+ save(context)
+ }
+
+ private fun save(context: Context) {
+ val array = JSONArray()
+ for (w in widgets) array.put(JSONObject().put("id", w.id).put("w", w.widthDp).put("h", w.heightDp))
+ context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().putString(KEY, array.toString()).apply()
+ }
+}
diff --git a/app/src/main/java/com/droiddeck/launcher/PinRequestActivity.kt b/app/src/main/java/com/droiddeck/launcher/PinRequestActivity.kt
new file mode 100644
index 000000000..ca8d382fe
--- /dev/null
+++ b/app/src/main/java/com/droiddeck/launcher/PinRequestActivity.kt
@@ -0,0 +1,82 @@
+package com.droiddeck.launcher
+
+import android.content.Intent
+import android.content.pm.LauncherApps
+import android.graphics.Bitmap
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.core.graphics.drawable.toBitmap
+import com.droiddeck.launcher.core.AppUiPrefs
+import com.droiddeck.launcher.ui.DroidDeckTheme
+import com.droiddeck.launcher.ui.PinRequestPrompt
+
+class PinRequestActivity : ComponentActivity() {
+ override fun attachBaseContext(newBase: android.content.Context) {
+ super.attachBaseContext(newBase)
+ com.droiddeck.launcher.core.AppLanguage.applyTo(this, newBase)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val request = runCatching { getSystemService(LauncherApps::class.java).getPinItemRequest(intent) }.getOrNull()
+ if (request == null || !request.isValid) {
+ finish()
+ return
+ }
+ val prompt = when (request.requestType) {
+ LauncherApps.PinItemRequest.REQUEST_TYPE_SHORTCUT -> request.shortcutInfo?.let { info ->
+ val appLabel = appLabel(info.`package`)
+ Triple(info.shortLabel?.toString().orEmpty().ifEmpty { appLabel }, appLabel, HomeApp.shortcutIcon(this, info))
+ }
+ LauncherApps.PinItemRequest.REQUEST_TYPE_APPWIDGET -> request.getAppWidgetProviderInfo(this)?.let { info ->
+ val appLabel = appLabel(info.provider.packageName)
+ val dpi = resources.displayMetrics.densityDpi
+ val icon = runCatching { (info.loadPreviewImage(this, dpi) ?: info.loadIcon(this, dpi))?.let(::scaled) }.getOrNull()
+ Triple(info.loadLabel(packageManager)?.toString().orEmpty().ifEmpty { appLabel }, appLabel, icon)
+ }
+ else -> null
+ }
+ if (prompt == null) {
+ finish()
+ return
+ }
+ val (label, appLabel, icon) = prompt
+ val widget = request.requestType == LauncherApps.PinItemRequest.REQUEST_TYPE_APPWIDGET
+ setContent {
+ DroidDeckTheme(appScale = AppUiPrefs.scale(this)) {
+ PinRequestPrompt(
+ label = label, appLabel = appLabel, icon = icon, wide = widget,
+ onAdd = {
+ if (!widget) {
+ HomePins.accept(this, request)
+ finish()
+ } else if (!HomeWidgets.addPinned(this, request)) finish()
+ },
+ onCancel = { finish() },
+ )
+ }
+ }
+ }
+
+ @Suppress("OVERRIDE_DEPRECATION")
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ if (HomeWidgets.onActivityResult(this, requestCode, resultCode)) {
+ finish()
+ return
+ }
+ @Suppress("DEPRECATION")
+ super.onActivityResult(requestCode, resultCode, data)
+ }
+
+ private fun appLabel(packageName: String): String =
+ runCatching { packageManager.getApplicationLabel(packageManager.getApplicationInfo(packageName, 0)).toString() }
+ .getOrDefault(packageName)
+
+ private fun scaled(d: android.graphics.drawable.Drawable): Bitmap {
+ val w = d.intrinsicWidth.takeIf { it > 0 } ?: 192
+ val h = d.intrinsicHeight.takeIf { it > 0 } ?: 192
+ val k = minOf(1f, 480f / maxOf(w, h))
+ return d.toBitmap((w * k).toInt().coerceAtLeast(1), (h * k).toInt().coerceAtLeast(1))
+ }
+}
diff --git a/app/src/main/java/com/droiddeck/launcher/ui/AndroidAppsPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/AndroidAppsPage.kt
new file mode 100644
index 000000000..fdd0fdd8c
--- /dev/null
+++ b/app/src/main/java/com/droiddeck/launcher/ui/AndroidAppsPage.kt
@@ -0,0 +1,959 @@
+package com.droiddeck.launcher.ui
+
+import android.app.Activity
+import android.content.Context
+import android.content.ContextWrapper
+import android.view.KeyEvent
+import androidx.activity.compose.BackHandler
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.scaleIn
+import androidx.compose.animation.scaleOut
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.LocalIndication
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.hoverable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsPressedAsState
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.GridItemSpan
+import androidx.compose.foundation.lazy.grid.LazyGridState
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.foundation.lazy.grid.rememberLazyGridState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.FilterAlt
+import androidx.compose.material.icons.filled.Sort
+import androidx.compose.material.icons.filled.Widgets
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.key.onPreviewKeyEvent
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.layout.boundsInRoot
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.compose.ui.zIndex
+import com.droiddeck.launcher.HomeApp
+import com.droiddeck.launcher.HomeFolders
+import com.droiddeck.launcher.HomeHidden
+import com.droiddeck.launcher.HomePins
+import android.content.pm.ShortcutInfo
+import android.graphics.Bitmap
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import com.droiddeck.launcher.HomeWidgets
+import com.droiddeck.launcher.R
+import kotlinx.coroutines.launch
+import kotlin.math.roundToInt
+
+private sealed interface AppEntry : AppEntryKey {
+ val key: String
+
+ class App(val app: HomeApp.LaunchableApp) : AppEntry {
+ override val label get() = app.label
+ override val key get() = "app:" + app.key
+ override val installedAt get() = app.installedAt
+ override val updatedAt get() = app.updatedAt
+ }
+
+ class Folder(val folder: HomeFolders.Folder, val apps: List) : AppEntry {
+ override val label get() = folder.name
+ override val key get() = "folder:" + folder.id
+ override val installedAt get() = apps.maxOf { it.installedAt }
+ override val updatedAt get() = apps.maxOf { it.updatedAt }
+ }
+
+ class Shortcut(val pinned: HomePins.Pinned) : AppEntry {
+ override val label get() = pinned.label
+ override val key get() = pinned.key
+ override val installedAt get() = 0L
+ override val updatedAt get() = 0L
+ }
+}
+
+private fun indexLetter(label: String): String {
+ val c = label.trim().firstOrNull()?.uppercaseChar() ?: return "#"
+ return if (c in 'A'..'Z') c.toString() else if (c.isLetter()) c.toString() else "#"
+}
+
+private fun searchRank(label: String, q: String): Int? {
+ if (label.startsWith(q, ignoreCase = true)) return 0
+ if (label.split(' ', '-', '_', '.').any { it.startsWith(q, ignoreCase = true) }) return 1
+ if (label.contains(q, ignoreCase = true)) return 2
+ return null
+}
+
+internal fun Context.findActivity(): Activity? {
+ var c: Context? = this
+ while (c is ContextWrapper) {
+ if (c is Activity) return c
+ c = c.baseContext
+ }
+ return null
+}
+
+private fun Modifier.menuKey(onMenu: () -> Unit): Modifier = onPreviewKeyEvent { event ->
+ val k = event.nativeKeyEvent
+ if (k.keyCode != KeyEvent.KEYCODE_BUTTON_Y && k.keyCode != KeyEvent.KEYCODE_MENU) false
+ else {
+ if (k.action == KeyEvent.ACTION_DOWN && k.repeatCount == 0) onMenu()
+ true
+ }
+}
+
+internal object AppsView {
+ const val ALL = "all"
+ const val GAMES = "games"
+ const val DOWNLOADED = "downloaded"
+ const val SYSTEM = "system"
+ const val HIDDEN = "hidden"
+ val FILTERS = listOf(ALL, GAMES, DOWNLOADED, SYSTEM, HIDDEN)
+ const val NAME_ASC = "nameAsc"
+ const val NAME_DESC = "nameDesc"
+ const val INSTALLED = "installed"
+ const val UPDATED = "updated"
+ val SORTS = listOf(NAME_ASC, NAME_DESC, INSTALLED, UPDATED)
+
+ var filter by mutableStateOf(ALL)
+ var sort by mutableStateOf(NAME_ASC)
+ private var loaded = false
+
+ private fun prefs(context: Context) = context.getSharedPreferences("home_launcher", Context.MODE_PRIVATE)
+
+ fun load(context: Context) {
+ if (loaded) return
+ loaded = true
+ val p = prefs(context)
+ filter = p.getString("filter", ALL)?.takeIf { it in FILTERS } ?: ALL
+ sort = p.getString("sort", NAME_ASC)?.takeIf { it in SORTS } ?: NAME_ASC
+ }
+
+ fun setFilter(context: Context, value: String) {
+ filter = value
+ prefs(context).edit().putString("filter", value).apply()
+ }
+
+ fun setSort(context: Context, value: String) {
+ sort = value
+ prefs(context).edit().putString("sort", value).apply()
+ }
+
+ fun keeps(app: HomeApp.LaunchableApp): Boolean {
+ val hidden = app.key in HomeHidden.keys
+ return when (filter) {
+ HIDDEN -> hidden
+ GAMES -> !hidden && app.game
+ DOWNLOADED -> !hidden && !app.system
+ SYSTEM -> !hidden && app.system
+ else -> !hidden
+ }
+ }
+
+ fun comparator(): Comparator = when (sort) {
+ NAME_DESC -> compareBy(String.CASE_INSENSITIVE_ORDER) { it.label }.reversed()
+ INSTALLED -> compareByDescending { it.installedAt }.thenBy(String.CASE_INSENSITIVE_ORDER) { it.label }
+ UPDATED -> compareByDescending { it.updatedAt }.thenBy(String.CASE_INSENSITIVE_ORDER) { it.label }
+ else -> compareBy(String.CASE_INSENSITIVE_ORDER) { it.label }
+ }
+}
+
+internal interface AppEntryKey {
+ val label: String
+ val installedAt: Long
+ val updatedAt: Long
+}
+
+@OptIn(ExperimentalLayoutApi::class)
+@Composable
+internal fun AndroidAppsPage(s: FrontEndState, modifier: Modifier, onLaunch: (HomeApp.LaunchableApp) -> Unit) {
+ val ctx = LocalContext.current
+ remember { HomeFolders.load(ctx); HomeWidgets.load(ctx); HomeHidden.load(ctx); AppsView.load(ctx); true }
+ val colors = MaterialTheme.colorScheme
+ val narrow = LocalNarrowPane.current
+ val padH = if (narrow) 16.dp else 22.dp
+ var query by rememberSaveable { mutableStateOf("") }
+ var openFolder by rememberSaveable { mutableStateOf(null) }
+ var renameOnOpen by rememberSaveable { mutableStateOf(false) }
+ var picking by rememberSaveable { mutableStateOf(false) }
+ val folders = HomeFolders.folders
+ val byKey = remember(s.androidApps) { s.androidApps.associateBy { it.key } }
+ val byPkg = remember(s.androidApps) { s.androidApps.filterNot { it.work }.groupBy { it.packageName }.mapNotNull { (pkg, apps) -> apps.singleOrNull()?.let { pkg to it } }.toMap() }
+ LaunchedEffect(byPkg) { if (byPkg.isNotEmpty()) HomeFolders.migrateMembers(ctx) { byPkg[it]?.key } }
+ val member: (String) -> HomeApp.LaunchableApp? = { byKey[it] ?: byPkg[it] }
+ val pins = HomePins.pinned
+ val hidden = HomeHidden.keys
+ val q = query.trim()
+ val filter = AppsView.filter
+ val sort = AppsView.sort
+ val entries: List = remember(s.androidApps, folders, q, filter, sort, pins, hidden) {
+ val kept = s.androidApps.filter { AppsView.keeps(it) }
+ val shortcuts = if (filter == AppsView.ALL) pins.map { AppEntry.Shortcut(it) } else emptyList()
+ if (q.isNotEmpty()) {
+ (kept.map { AppEntry.App(it) } + shortcuts).mapNotNull { e -> searchRank(e.label, q)?.let { it to e } }
+ .sortedWith(compareBy> { it.first }.thenBy(String.CASE_INSENSITIVE_ORDER) { it.second.label })
+ .map { it.second }
+ } else if (filter != AppsView.ALL) {
+ kept.map { AppEntry.App(it) }.sortedWith(AppsView.comparator())
+ } else {
+ val folderEntries = folders.mapNotNull { f ->
+ val apps = f.apps.mapNotNull(member).filter { it.key !in hidden }.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.label })
+ if (apps.isEmpty()) null else AppEntry.Folder(f, apps)
+ }
+ val grouped = folderEntries.flatMap { f -> f.apps.map { it.key } }.toSet()
+ (folderEntries + kept.filter { it.key !in grouped }.map { AppEntry.App(it) } + shortcuts).sortedWith(AppsView.comparator())
+ }
+ }
+ val grid = rememberLazyGridState()
+ LaunchedEffect(q, filter, sort) { grid.scrollToItem(0) }
+ val widgetsShown = q.isEmpty() && filter == AppsView.ALL && HomeWidgets.widgets.isNotEmpty()
+ val leadItems = if (widgetsShown) 1 else 0
+ val byName = sort == AppsView.NAME_ASC || sort == AppsView.NAME_DESC
+ val newFolder: (HomeApp.LaunchableApp) -> Unit = { app ->
+ val f = HomeFolders.create(ctx, ctx.getString(R.string.apps_folder_default), app.key)
+ renameOnOpen = true
+ openFolder = f.id
+ }
+
+ Box(modifier) {
+ Column(Modifier.fillMaxSize().imePadding()) {
+ Box(Modifier.fillMaxWidth().padding(start = padH, end = padH, top = if (narrow) 8.dp else 12.dp)) {
+ ModeHeader(
+ stringResource(R.string.content_android_apps), query, { query = it },
+ placeholder = stringResource(R.string.apps_search_hint),
+ clearSearchFocus = openFolder != null || picking,
+ lead = {
+ Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(start = 2.dp, end = 4.dp)) {
+ Text(stringResource(R.string.content_android_apps), fontSize = if (narrow) 20.sp else 24.sp, fontWeight = FontWeight.Bold, color = colors.onBackground, maxLines = 1)
+ val shownCount = s.androidApps.count { AppsView.keeps(it) } + if (filter == AppsView.ALL) pins.size else 0
+ Text(androidx.compose.ui.res.pluralStringResource(R.plurals.apps_count, shownCount, shownCount), fontSize = 13.sp, color = colors.onSurfaceVariant, maxLines = 1)
+ }
+ },
+ ) {
+ AppsFilterButton()
+ AppsSortButton()
+ HeaderIconButton(stringResource(R.string.apps_add_widget), Icons.Filled.Widgets, "add-widget") {
+ HomeWidgets.loadProviders(ctx)
+ picking = true
+ }
+ }
+ }
+ Box(Modifier.weight(1f).fillMaxWidth()) {
+ val scroller = q.isEmpty() && byName && entries.size > 16
+ LazyVerticalGrid(
+ columns = GridCells.Adaptive(if (narrow) 84.dp else 96.dp),
+ state = grid,
+ horizontalArrangement = Arrangement.spacedBy(10.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp),
+ contentPadding = PaddingValues(start = padH, end = if (scroller) padH + 22.dp else padH, top = 12.dp, bottom = 16.dp),
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ if (widgetsShown) item(key = "widgets", span = { GridItemSpan(maxLineSpan) }) {
+ BoxWithConstraints(Modifier.fillMaxWidth()) {
+ val maxW = maxWidth
+ FlowRow(
+ horizontalArrangement = Arrangement.spacedBy(10.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp),
+ modifier = Modifier.fillMaxWidth().padding(bottom = 6.dp),
+ ) {
+ for (w in HomeWidgets.widgets) key(w.id) { WidgetCard(w, maxW.value.toInt()) }
+ }
+ }
+ }
+ if (entries.isEmpty()) item(key = "empty", span = { GridItemSpan(maxLineSpan) }) {
+ Note(
+ when {
+ q.isNotEmpty() -> stringResource(R.string.apps_no_match, q)
+ filter == AppsView.HIDDEN -> stringResource(R.string.apps_hidden_none)
+ else -> stringResource(R.string.content_no_android_apps)
+ },
+ )
+ }
+ items(entries, key = { it.key }) { entry ->
+ when (entry) {
+ is AppEntry.App -> AppTile(
+ entry.app, folderId = null, onLaunch = onLaunch, onNewFolder = newFolder,
+ first = entry === entries.first(),
+ )
+ is AppEntry.Shortcut -> PinnedTile(entry.pinned, first = entry === entries.first())
+ is AppEntry.Folder -> FolderTile(entry, first = entry === entries.first(), onOpen = { renameOnOpen = false; openFolder = entry.folder.id }) {
+ renameOnOpen = true
+ openFolder = entry.folder.id
+ }
+ }
+ }
+ }
+ if (scroller) AlphabetScroller(entries, grid, leadItems, Modifier.align(Alignment.CenterEnd).fillMaxHeight().padding(end = 4.dp, top = 8.dp, bottom = 8.dp))
+ }
+ }
+
+ val folder = folders.firstOrNull { it.id == openFolder }
+ LaunchedEffect(folder == null, openFolder) { if (folder == null && openFolder != null) openFolder = null }
+ AnimatedVisibility(
+ folder != null, enter = fadeIn(Motion.tw(180)), exit = fadeOut(Motion.tw(160)),
+ modifier = Modifier.fillMaxSize().zIndex(2f),
+ ) {
+ var last by remember { mutableStateOf(folder) }
+ if (folder != null) last = folder
+ last?.let { f ->
+ FolderSheet(f, f.apps.mapNotNull(member).filter { it.key !in hidden }.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.label }), renameOnOpen, onLaunch, newFolder) {
+ openFolder = null
+ renameOnOpen = false
+ }
+ }
+ }
+ AnimatedVisibility(
+ picking, enter = fadeIn(Motion.tw(180)), exit = fadeOut(Motion.tw(160)),
+ modifier = Modifier.fillMaxSize().zIndex(3f),
+ ) { WidgetPicker(padH) { picking = false } }
+ }
+}
+
+@Composable
+private fun SwipeLock() {
+ DisposableEffect(Unit) {
+ PageSwipe.locks++
+ onDispose { PageSwipe.locks-- }
+ }
+}
+
+@Composable
+private fun appsFilterLabel(filter: String): String = stringResource(
+ when (filter) {
+ AppsView.GAMES -> R.string.apps_filter_games
+ AppsView.DOWNLOADED -> R.string.apps_filter_downloaded
+ AppsView.SYSTEM -> R.string.apps_filter_system
+ AppsView.HIDDEN -> R.string.apps_filter_hidden
+ else -> R.string.apps_filter_all
+ },
+)
+
+@Composable
+private fun appsSortLabel(sort: String): String = stringResource(
+ when (sort) {
+ AppsView.NAME_DESC -> R.string.apps_sort_name_desc
+ AppsView.INSTALLED -> R.string.apps_sort_installed
+ AppsView.UPDATED -> R.string.apps_sort_updated
+ else -> R.string.apps_sort_name_asc
+ },
+)
+
+@Composable
+private fun AppsFilterButton() = HeaderChoiceButton(
+ stringResource(R.string.apps_filter), Icons.Filled.FilterAlt, "apps-filter",
+ AppsView.FILTERS, AppsView.filter, { appsFilterLabel(it) }, AppsView::setFilter,
+)
+
+@Composable
+private fun AppsSortButton() = HeaderChoiceButton(
+ stringResource(R.string.stores_sort), Icons.Filled.Sort, "apps-sort",
+ AppsView.SORTS, AppsView.sort, { appsSortLabel(it) }, AppsView::setSort,
+)
+
+@Composable
+private fun HeaderChoiceButton(
+ title: String, icon: androidx.compose.ui.graphics.vector.ImageVector, key: String,
+ options: List, current: String, label: @Composable (String) -> String, onPick: (Context, String) -> Unit,
+) {
+ val pal = LocalPalette.current
+ val ctx = LocalContext.current
+ var at by remember { mutableStateOf(null) }
+ Box(Modifier.onGloballyPositioned { at = it.boundsInRoot() }) {
+ HeaderIconButton("$title · ${label(current)}", icon, key) {
+ val from = at ?: return@HeaderIconButton
+ Steps.ask(StepAsk(
+ anchor = from, side = StepSide.Left, accent = pal.signal, pillCorner = 8.dp, items = options.size,
+ handle = { Icon(icon, null, tint = pal.signal, modifier = Modifier.size(18.dp)) },
+ ) {
+ StepTitle(title)
+ StepList {
+ options.forEachIndexed { i, option ->
+ StepChoice(
+ label(option), selected = option == current, enabled = open,
+ modifier = Modifier.fillMaxWidth().stepItem(i).then(if (option == current) Modifier.focusRequester(first) else Modifier),
+ ) {
+ onPick(ctx, option)
+ Steps.fold()
+ }
+ }
+ }
+ })
+ }
+ }
+}
+
+@Composable
+private fun AlphabetScroller(entries: List, grid: LazyGridState, leadItems: Int, modifier: Modifier) {
+ val colors = MaterialTheme.colorScheme
+ val pal = LocalPalette.current
+ val scope = rememberCoroutineScope()
+ val firstAt = remember(entries) {
+ val map = LinkedHashMap()
+ entries.forEachIndexed { i, e -> map.putIfAbsent(indexLetter(e.label), i) }
+ map
+ }
+ val letters = remember(firstAt) { firstAt.keys.toList() }
+ var dragging by remember { mutableStateOf(null) }
+ val current by remember(entries, leadItems) {
+ derivedStateOf {
+ val i = (grid.firstVisibleItemIndex - leadItems).coerceIn(0, (entries.size - 1).coerceAtLeast(0))
+ entries.getOrNull(i)?.let { indexLetter(it.label) }
+ }
+ }
+ var height by remember { mutableIntStateOf(1) }
+ val jump: (Float) -> Unit = { y ->
+ val i = ((y / height) * letters.size).toInt().coerceIn(0, letters.size - 1)
+ val letter = letters[i]
+ if (letter != dragging) {
+ dragging = letter
+ val at = firstAt[letter] ?: 0
+ scope.launch { grid.scrollToItem(if (at == 0) 0 else leadItems + at) }
+ }
+ }
+ Box(modifier.width(22.dp)) {
+ Column(
+ verticalArrangement = Arrangement.SpaceEvenly, horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.fillMaxSize()
+ .clip(RoundedCornerShape(11.dp))
+ .background(if (dragging != null) colors.surface.copy(alpha = 0.85f) else Color.Transparent)
+ .onSizeChanged { height = it.height.coerceAtLeast(1) }
+ .pointerInput(letters) {
+ awaitEachGesture {
+ val down = awaitFirstDown()
+ down.consume()
+ jump(down.position.y)
+ while (true) {
+ val event = awaitPointerEvent()
+ val c = event.changes.firstOrNull { it.id == down.id } ?: break
+ if (!c.pressed) break
+ c.consume()
+ jump(c.position.y)
+ }
+ dragging = null
+ }
+ },
+ ) {
+ for (l in letters) {
+ val on = l == (dragging ?: current)
+ Text(
+ l, fontSize = 10.sp, lineHeight = 11.sp, fontWeight = if (on) FontWeight.Bold else FontWeight.Medium,
+ color = if (on) pal.signal else colors.onSurfaceVariant, textAlign = TextAlign.Center,
+ )
+ }
+ }
+ dragging?.let { l ->
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier.align(Alignment.Center).offset { IntOffset(-64.dp.roundToPx(), 0) }.size(56.dp)
+ .clip(CircleShape).background(pal.signal),
+ ) { Text(l, fontSize = 26.sp, fontWeight = FontWeight.Bold, color = pal.onPrimary) }
+ }
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun TileFrame(
+ id: String, first: Boolean, onClick: () -> Unit, onMenu: () -> Unit,
+ content: @Composable (hot: Boolean) -> Unit,
+) {
+ val colors = MaterialTheme.colorScheme
+ val pal = LocalPalette.current
+ val src = remember { MutableInteractionSource() }
+ val hot = rememberHot(src)
+ val pressed by src.collectIsPressedAsState()
+ val scale by animateFloatAsState(if (pressed) 0.95f else if (hot) 1.04f else 1f, Motion.sp(0.55f, Spring.StiffnessMedium), label = "appScale")
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.fillMaxWidth()
+ .paneItem("tile:$id").then(if (first) Modifier.firstTile().paneEntry() else Modifier)
+ .graphicsLayer { scaleX = scale; scaleY = scale }
+ .clip(Shape12)
+ .background(if (hot) colors.surface else colors.surface.copy(alpha = 0.55f))
+ .glideBorder(hot, Shape12, pal.signal, hotWidth = 2.5.dp)
+ .hoverable(src)
+ .combinedClickable(interactionSource = src, indication = LocalIndication.current, role = Role.Button, onLongClick = onMenu, onClick = onClick)
+ .controllerConfirm(onClick = onClick)
+ .menuKey(onMenu)
+ .padding(horizontal = 6.dp, vertical = 10.dp),
+ ) { content(hot) }
+}
+
+@Composable
+private fun TileLabel(text: String) = Text(
+ text, fontSize = 12.sp, lineHeight = 15.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onBackground,
+ maxLines = 1, overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
+)
+
+@Composable
+private fun AppIcon(app: HomeApp.LaunchableApp, size: androidx.compose.ui.unit.Dp) {
+ val icon = app.icon
+ if (icon != null) Image(icon.asImageBitmap(), null, contentScale = ContentScale.Fit, modifier = Modifier.size(size))
+ else Box(Modifier.size(size).clip(CircleShape).background(artBrush(hueOf(app.label))))
+}
+
+@Composable
+private fun AppTile(
+ app: HomeApp.LaunchableApp, folderId: String?, first: Boolean,
+ onLaunch: (HomeApp.LaunchableApp) -> Unit, onNewFolder: (HomeApp.LaunchableApp) -> Unit,
+) {
+ var menu by remember { mutableStateOf(false) }
+ Box {
+ TileFrame(app.key, first, onClick = { onLaunch(app) }, onMenu = { menu = true }) {
+ AppIcon(app, 52.dp)
+ TileLabel(app.label)
+ }
+ AppMenu(app, folderId, menu, onDismiss = { menu = false }, onNewFolder = onNewFolder)
+ }
+}
+
+@Composable
+private fun AppMenu(
+ app: HomeApp.LaunchableApp, folderId: String?, open: Boolean,
+ onDismiss: () -> Unit, onNewFolder: (HomeApp.LaunchableApp) -> Unit,
+) {
+ val ctx = LocalContext.current
+ var choosing by remember { mutableStateOf(false) }
+ var shortcuts by remember { mutableStateOf>>(emptyList()) }
+ LaunchedEffect(open) {
+ if (!open) {
+ choosing = false
+ return@LaunchedEffect
+ }
+ shortcuts = withContext(Dispatchers.IO) {
+ HomeApp.shortcuts(ctx, app).map { it to HomeApp.shortcutIcon(ctx, it, 64) }
+ }
+ }
+ val isHidden = app.key in HomeHidden.keys
+ AnchoredMenu(open, onDismiss = onDismiss, title = app.label, compact = true, minWidth = 200.dp) { first ->
+ if (choosing) {
+ val others = HomeFolders.folders.filter { it.id != folderId }
+ var firstUsed = false
+ for (f in others) key(f.id) {
+ MenuItem(f.name, checked = false, focusRequester = if (!firstUsed) first.also { firstUsed = true } else null) {
+ HomeFolders.add(ctx, f.id, app.key)
+ onDismiss()
+ }
+ }
+ MenuItem(stringResource(R.string.apps_new_folder), checked = false, focusRequester = if (others.isEmpty()) first else null) {
+ onDismiss()
+ onNewFolder(app)
+ }
+ } else {
+ shortcuts.forEachIndexed { i, (info, icon) ->
+ key(info.id) {
+ MenuItem(
+ info.shortLabel?.toString().orEmpty(), checked = false,
+ focusRequester = if (i == 0) first else null,
+ leading = { MenuIcon(icon) },
+ ) {
+ onDismiss()
+ runCatching { HomeApp.startShortcut(ctx, info) }
+ }
+ }
+ }
+ if (shortcuts.isNotEmpty()) Box(Modifier.fillMaxWidth().padding(vertical = 4.dp).height(1.dp).background(LocalPalette.current.line))
+ if (!isHidden) MenuItem(stringResource(R.string.apps_add_to_folder), checked = false, focusRequester = if (shortcuts.isEmpty()) first else null) { choosing = true }
+ if (folderId != null) MenuItem(stringResource(R.string.apps_remove_from_folder), checked = false) {
+ HomeFolders.remove(ctx, app.key)
+ onDismiss()
+ }
+ MenuItem(stringResource(if (isHidden) R.string.apps_unhide else R.string.apps_hide), checked = false,
+ focusRequester = if (isHidden && shortcuts.isEmpty()) first else null) {
+ onDismiss()
+ HomeHidden.set(ctx, app.key, !isHidden)
+ }
+ MenuItem(stringResource(R.string.apps_app_info), checked = false) {
+ onDismiss()
+ runCatching { HomeApp.openAppInfo(ctx, app) }
+ }
+ if (HomeApp.canUninstall(app)) MenuItem(stringResource(R.string.apps_uninstall), checked = false) {
+ onDismiss()
+ runCatching { HomeApp.uninstall(ctx, app) }
+ }
+ }
+ }
+}
+
+@Composable
+private fun MenuIcon(icon: Bitmap?) {
+ if (icon != null) Image(icon.asImageBitmap(), null, modifier = Modifier.size(20.dp))
+ else Spacer(Modifier.size(20.dp))
+}
+
+@Composable
+private fun PinnedTile(pinned: HomePins.Pinned, first: Boolean) {
+ val ctx = LocalContext.current
+ var menu by remember { mutableStateOf(false) }
+ Box {
+ TileFrame(pinned.key, first, onClick = { runCatching { HomeApp.startShortcut(ctx, pinned.info) } }, onMenu = { menu = true }) {
+ Box {
+ val icon = pinned.icon
+ if (icon != null) Image(icon.asImageBitmap(), null, contentScale = ContentScale.Fit, modifier = Modifier.size(52.dp))
+ else Box(Modifier.size(52.dp).clip(CircleShape).background(artBrush(hueOf(pinned.label))))
+ }
+ TileLabel(pinned.label)
+ }
+ AnchoredMenu(menu, onDismiss = { menu = false }, title = pinned.label, compact = true, minWidth = 180.dp) { firstItem ->
+ MenuItem(stringResource(R.string.apps_shortcut_remove), checked = false, detail = pinned.appLabel, focusRequester = firstItem) {
+ menu = false
+ HomePins.unpin(ctx, pinned)
+ }
+ }
+ }
+}
+
+@Composable
+private fun FolderIcon(apps: List, size: androidx.compose.ui.unit.Dp) {
+ val colors = MaterialTheme.colorScheme
+ val pal = LocalPalette.current
+ val cell = (size - 14.dp) / 2
+ Box(
+ Modifier.size(size).clip(RoundedCornerShape(size / 3.5f)).background(colors.surfaceVariant)
+ .glideBorder(false, RoundedCornerShape(size / 3.5f), pal.signal, pal.line2)
+ .padding(5.dp),
+ ) {
+ Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
+ for (row in apps.take(4).chunked(2)) Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
+ for (a in row) AppIcon(a, cell)
+ }
+ }
+ }
+}
+
+@Composable
+private fun FolderTile(entry: AppEntry.Folder, first: Boolean, onOpen: () -> Unit, onRename: () -> Unit) {
+ val ctx = LocalContext.current
+ var menu by remember { mutableStateOf(false) }
+ Box {
+ TileFrame(entry.key, first, onClick = onOpen, onMenu = { menu = true }) {
+ FolderIcon(entry.apps, 52.dp)
+ TileLabel(entry.folder.name)
+ }
+ AnchoredMenu(menu, onDismiss = { menu = false }, title = entry.folder.name, compact = true, minWidth = 160.dp) { firstItem ->
+ MenuItem(stringResource(R.string.apps_folder_rename), checked = false, focusRequester = firstItem) {
+ menu = false
+ onRename()
+ }
+ MenuItem(stringResource(R.string.apps_folder_ungroup), checked = false) {
+ menu = false
+ HomeFolders.delete(ctx, entry.folder.id)
+ }
+ }
+ }
+}
+
+@Composable
+private fun FolderSheet(
+ folder: HomeFolders.Folder, apps: List, renameFirst: Boolean,
+ onLaunch: (HomeApp.LaunchableApp) -> Unit, onNewFolder: (HomeApp.LaunchableApp) -> Unit, onClose: () -> Unit,
+) {
+ SwipeLock()
+ val ctx = LocalContext.current
+ val colors = MaterialTheme.colorScheme
+ val pal = LocalPalette.current
+ var renaming by remember(folder.id) { mutableStateOf(renameFirst) }
+ var name by remember(folder.id) { mutableStateOf(if (renameFirst) "" else folder.name) }
+ val commit = {
+ val n = name.trim()
+ if (n.isNotEmpty() && n != folder.name) HomeFolders.rename(ctx, folder.id, n)
+ renaming = false
+ }
+ val close = { if (renaming) commit(); onClose() }
+ BackHandler { close() }
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.55f))
+ .pointerInput(Unit) { awaitEachGesture { val d = awaitFirstDown(); d.consume(); close() } },
+ ) {
+ val shown = remember { androidx.compose.animation.core.MutableTransitionState(false) }.apply { targetState = true }
+ AnimatedVisibility(shown, enter = scaleIn(Motion.sp(0.75f), initialScale = 0.92f) + fadeIn(Motion.tw(160)), exit = scaleOut(Motion.tw(140)) + fadeOut(Motion.tw(140))) {
+ Column(
+ modifier = Modifier.widthIn(max = 620.dp).fillMaxWidth(0.9f).heightIn(max = 520.dp)
+ .clip(Shape16).background(pal.surfaceVariant.copy(alpha = 0.97f))
+ .glideBorder(false, Shape16, pal.signal, pal.signal.copy(alpha = 0.22f))
+ .pointerInput(Unit) { awaitEachGesture { awaitFirstDown(requireUnconsumed = false).consume() } }
+ .controllerBack(close)
+ .padding(16.dp),
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth().padding(bottom = 10.dp)) {
+ if (renaming) Box(Modifier.weight(1f)) {
+ AdbTextField(
+ value = name, onValueChange = { name = it.take(40) }, label = stringResource(R.string.apps_folder_name),
+ keyboardType = KeyboardType.Text, imeAction = ImeAction.Done, compact = true, showLabel = false,
+ placeholder = folder.name, focusOnShow = true, onDone = { commit() },
+ )
+ } else {
+ Text(folder.name, fontSize = 20.sp, fontWeight = FontWeight.Bold, color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f, fill = false))
+ SmallIconButton(Icons.Filled.Edit, stringResource(R.string.apps_folder_rename), tint = colors.onSurfaceVariant) { renaming = true }
+ Spacer(Modifier.weight(1f))
+ }
+ SmallIconButton(Icons.Filled.Close, stringResource(R.string.common_close), tint = colors.onSurfaceVariant) { close() }
+ }
+ LazyVerticalGrid(
+ columns = GridCells.Adaptive(92.dp),
+ horizontalArrangement = Arrangement.spacedBy(10.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp),
+ ) {
+ items(apps, key = { it.key }) { app ->
+ AppTile(app, folderId = folder.id, first = false, onLaunch = { onClose(); onLaunch(it) }, onNewFolder = onNewFolder)
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun WidgetCard(w: HomeWidgets.Placed, maxWidthDp: Int) {
+ val ctx = LocalContext.current
+ val colors = MaterialTheme.colorScheme
+ val pal = LocalPalette.current
+ var menu by remember { mutableStateOf(false) }
+ val src = remember { MutableInteractionSource() }
+ val hot = rememberHot(src)
+ val width = w.widthDp.coerceAtMost(maxWidthDp)
+ val activity = remember(ctx) { ctx.findActivity() }
+ Box {
+ Box(
+ Modifier.size(width.dp, w.heightDp.dp)
+ .clip(Shape16)
+ .background(colors.surface.copy(alpha = 0.35f))
+ .glideBorder(hot || menu, Shape16, pal.signal)
+ .hoverable(src).focusable(interactionSource = src)
+ .controllerConfirm { menu = true }
+ .menuKey { menu = true },
+ ) {
+ AndroidView(
+ factory = { c ->
+ (HomeWidgets.createView(c, w.id) ?: android.view.View(c)).apply {
+ val radius = 16 * c.resources.displayMetrics.density
+ outlineProvider = object : android.view.ViewOutlineProvider() {
+ override fun getOutline(view: android.view.View, outline: android.graphics.Outline) =
+ outline.setRoundRect(0, 0, view.width, view.height, radius)
+ }
+ clipToOutline = true
+ }
+ },
+ update = { v ->
+ if (v is HomeWidgets.HomeWidgetView) {
+ v.onLongPress = { menu = true }
+ HomeWidgets.applySize(v, width, w.heightDp)
+ }
+ },
+ modifier = Modifier.fillMaxSize(),
+ )
+ }
+ val (minW, minH) = remember(w.id) { HomeWidgets.minSize(ctx, w.id) }
+ val index = HomeWidgets.widgets.indexOfFirst { it.id == w.id }
+ val label = remember(w.id) { HomeWidgets.info(ctx, w.id)?.loadLabel(ctx.packageManager) ?: "" }
+ AnchoredMenu(menu, onDismiss = { menu = false }, title = label, compact = true, minWidth = 180.dp) { first ->
+ val step = 48
+ MenuItem(stringResource(R.string.apps_widget_wider), checked = false, enabled = width < maxWidthDp, focusRequester = first) {
+ HomeWidgets.resize(ctx, w.id, (width + step).coerceAtMost(maxWidthDp), w.heightDp)
+ }
+ MenuItem(stringResource(R.string.apps_widget_narrower), checked = false, enabled = width > minW) {
+ HomeWidgets.resize(ctx, w.id, (width - step).coerceAtLeast(minW), w.heightDp)
+ }
+ MenuItem(stringResource(R.string.apps_widget_taller), checked = false, enabled = w.heightDp < 480) {
+ HomeWidgets.resize(ctx, w.id, width, (w.heightDp + step).coerceAtMost(480))
+ }
+ MenuItem(stringResource(R.string.apps_widget_shorter), checked = false, enabled = w.heightDp > minH) {
+ HomeWidgets.resize(ctx, w.id, width, (w.heightDp - step).coerceAtLeast(minH))
+ }
+ if (index > 0) MenuItem(stringResource(R.string.apps_widget_move_earlier), checked = false) { HomeWidgets.move(ctx, w.id, -1) }
+ if (index in 0 until HomeWidgets.widgets.size - 1) MenuItem(stringResource(R.string.apps_widget_move_later), checked = false) { HomeWidgets.move(ctx, w.id, 1) }
+ if (activity != null && HomeWidgets.canReconfigure(ctx, w.id)) MenuItem(stringResource(R.string.apps_widget_configure), checked = false) {
+ menu = false
+ HomeWidgets.reconfigure(activity, w.id)
+ }
+ MenuItem(stringResource(R.string.apps_widget_remove), checked = false) {
+ menu = false
+ HomeWidgets.remove(ctx, w.id)
+ }
+ }
+ }
+}
+
+@Composable
+private fun WidgetPicker(padH: androidx.compose.ui.unit.Dp, onClose: () -> Unit) {
+ SwipeLock()
+ val ctx = LocalContext.current
+ val colors = MaterialTheme.colorScheme
+ val pal = LocalPalette.current
+ val activity = remember(ctx) { ctx.findActivity() }
+ BackHandler { onClose() }
+ val providers = HomeWidgets.providers
+ Column(Modifier.fillMaxSize().background(colors.background).pointerInput(Unit) { awaitEachGesture { awaitFirstDown(requireUnconsumed = false) } }) {
+ LazyVerticalGrid(
+ columns = GridCells.Adaptive(150.dp),
+ horizontalArrangement = Arrangement.spacedBy(10.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp),
+ contentPadding = PaddingValues(horizontal = padH, vertical = 14.dp),
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ item(key = "back", span = { GridItemSpan(maxLineSpan) }) {
+ Column {
+ Box(Modifier.padding(bottom = 10.dp)) { BackLink(stringResource(R.string.rail_apps), compact = true, onClick = onClose) }
+ PageHeader(stringResource(R.string.apps_add_widget)) {
+ if (providers != null) Chip(providers.size.toString(), ok = false)
+ }
+ }
+ }
+ when {
+ providers == null -> item(key = "loading", span = { GridItemSpan(maxLineSpan) }) { Note(stringResource(R.string.apps_widgets_loading)) }
+ providers.isEmpty() -> item(key = "none", span = { GridItemSpan(maxLineSpan) }) { Note(stringResource(R.string.apps_widgets_none)) }
+ else -> items(providers, key = { it.info.provider.flattenToString() }) { p ->
+ ProviderCard(p, first = p === providers.first()) {
+ onClose()
+ if (activity != null) HomeWidgets.add(activity, p)
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun ProviderCard(p: HomeWidgets.Provider, first: Boolean, onClick: () -> Unit) {
+ val colors = MaterialTheme.colorScheme
+ val pal = LocalPalette.current
+ val src = remember { MutableInteractionSource() }
+ val hot = rememberHot(src)
+ Column(
+ modifier = Modifier.fillMaxWidth().paneItem("widget:" + p.info.provider.flattenToString()).then(if (first) Modifier.firstTile() else Modifier)
+ .clip(Shape12).background(colors.surface)
+ .glideBorder(hot, Shape12, pal.signal, hotWidth = 2.5.dp)
+ .hoverable(src)
+ .clickable(interactionSource = src, indication = LocalIndication.current, role = Role.Button, onClick = onClick)
+ .controllerConfirm(onClick = onClick),
+ ) {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier.fillMaxWidth().aspectRatio(1.6f).background(colors.surfaceVariant).padding(10.dp),
+ ) {
+ val preview = p.preview
+ if (preview != null) Image(preview.asImageBitmap(), null, contentScale = ContentScale.Fit, modifier = Modifier.fillMaxSize())
+ }
+ Column(Modifier.padding(horizontal = 10.dp, vertical = 8.dp)) {
+ Text(p.label, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis)
+ val cells = "${(p.widthDp / 80f).roundToInt().coerceAtLeast(1)} × ${(p.heightDp / 80f).roundToInt().coerceAtLeast(1)}"
+ Text(if (p.appLabel == p.label) cells else "${p.appLabel} · $cells", fontSize = 12.sp, color = colors.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis)
+ }
+ }
+}
+
+@Composable
+fun PinRequestPrompt(label: String, appLabel: String, icon: Bitmap?, wide: Boolean, onAdd: () -> Unit, onCancel: () -> Unit) {
+ val colors = MaterialTheme.colorScheme
+ val pal = LocalPalette.current
+ BackHandler { onCancel() }
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.55f))
+ .pointerInput(Unit) { awaitEachGesture { awaitFirstDown().consume(); onCancel() } },
+ ) {
+ val shown = remember { androidx.compose.animation.core.MutableTransitionState(false) }.apply { targetState = true }
+ AnimatedVisibility(shown, enter = scaleIn(Motion.sp(0.75f), initialScale = 0.92f) + fadeIn(Motion.tw(160))) {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(14.dp),
+ modifier = Modifier.widthIn(min = 280.dp, max = 420.dp)
+ .clip(Shape16).background(pal.surfaceVariant.copy(alpha = 0.97f))
+ .glideBorder(false, Shape16, pal.signal, pal.signal.copy(alpha = 0.22f))
+ .pointerInput(Unit) { awaitEachGesture { awaitFirstDown(requireUnconsumed = false).consume() } }
+ .controllerBack(onCancel)
+ .padding(18.dp),
+ ) {
+ Text(
+ stringResource(R.string.apps_pin_title).uppercase(), fontSize = 12.sp, fontWeight = FontWeight.SemiBold,
+ letterSpacing = 1.5.sp, color = colors.onSurfaceVariant,
+ )
+ if (wide && icon != null) Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier.fillMaxWidth().heightIn(max = 180.dp).clip(Shape12).background(MaterialTheme.colorScheme.surface).padding(10.dp),
+ ) { Image(icon.asImageBitmap(), null, contentScale = ContentScale.Fit, modifier = Modifier.fillMaxWidth()) }
+ Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(14.dp)) {
+ if (!wide) {
+ if (icon != null) Image(icon.asImageBitmap(), null, contentScale = ContentScale.Fit, modifier = Modifier.size(52.dp))
+ else Box(Modifier.size(52.dp).clip(CircleShape).background(artBrush(hueOf(label))))
+ }
+ Column(Modifier.weight(1f)) {
+ Text(label, fontSize = 17.sp, fontWeight = FontWeight.Bold, color = colors.onBackground, maxLines = 2, overflow = TextOverflow.Ellipsis)
+ Text(appLabel, fontSize = 13.sp, color = colors.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis)
+ }
+ }
+ Row(horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.End), modifier = Modifier.fillMaxWidth()) {
+ SecondaryButton(stringResource(R.string.common_cancel), onClick = onCancel)
+ PrimaryButton(stringResource(R.string.apps_pin_add), onClick = onAdd)
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/droiddeck/launcher/ui/ComponentsPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/ComponentsPage.kt
index 364650b90..a934d39cf 100644
--- a/app/src/main/java/com/droiddeck/launcher/ui/ComponentsPage.kt
+++ b/app/src/main/java/com/droiddeck/launcher/ui/ComponentsPage.kt
@@ -699,7 +699,7 @@ fun ComponentsPage(
Box(Modifier.fillMaxSize().onGloballyPositioned { pageAt = it.positionInRoot() }.onFocusChanged { pageFocused = it.hasFocus }) {
Column(
- Modifier.fillMaxSize().padding(horizontal = if (narrow) 16.dp else 22.dp, vertical = if (narrow) 12.dp else 18.dp)
+ Modifier.fillMaxSize().padding(horizontal = if (narrow) 16.dp else 22.dp).padding(top = if (narrow) 12.dp else 18.dp)
.then(if (selectedGame == null) Modifier.bumpers(
onPrevious = { pick(layerStep(layer, -1)) },
onNext = { pick(layerStep(layer, 1)) },
@@ -972,7 +972,7 @@ private fun Stack(
onFocus = { onFocus(l) }, onClick = { onOpen(l) },
)
}
- Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(3.dp)) {
+ Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState()).padding(bottom = 16.dp), verticalArrangement = Arrangement.spacedBy(3.dp)) {
StackHeading(stringResource(R.string.comp_this_device))
card(GPU_TAB)
Spacer(Modifier.height(6.dp))
diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndContent.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndContent.kt
index 419329016..62c6a862e 100644
--- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndContent.kt
+++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndContent.kt
@@ -10,6 +10,8 @@ import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
import androidx.compose.animation.scaleIn
+import androidx.compose.animation.slideInHorizontally
+import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.scaleOut
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.rememberUpdatedState
@@ -138,12 +140,19 @@ internal fun Pane(
before[0] = target
val targetNow by rememberUpdatedState(target)
val leave = if (Hops.linking) PageLeave.Link else PageLeave.Ordinary
- LaunchedEffect(target) { Hops.linking = false }
+ LaunchedEffect(target) {
+ Hops.linking = false
+ PageSwipe.direction = 0
+ }
AnimatedContent(
targetState = target,
modifier = Modifier.onGloballyPositioned { paneAt[0] = it.positionInRoot() },
transitionSpec = {
+ val swipe = PageSwipe.direction
when {
+ swipe != 0 -> (fadeIn(Motion.tw(260, 40)) + slideInHorizontally(Motion.tw(360)) { it * swipe / 8 })
+ .togetherWith(fadeOut(Motion.tw(160)) + slideOutHorizontally(Motion.tw(220)) { -it * swipe / 8 })
+ .apply { targetContentZIndex = 1f }
targetState in origins -> (fadeIn(Motion.tw(120)))
.togetherWith(fadeOut(Motion.tw(320, 80)) + scaleOut(Motion.tw(480), targetScale = 0.95f))
.apply { targetContentZIndex = 1f }
@@ -232,13 +241,13 @@ private fun Content(
val padH = if (narrow) 16.dp else 22.dp
val padV = if (narrow) 12.dp else 18.dp
if (selected == "setup") {
- Column(modifier = modifier.padding(horizontal = padH, vertical = padV)) {
+ Column(modifier = modifier.padding(horizontal = padH).padding(top = padV)) {
SetupPanel(s, a, onOpenDeveloperOptions, onRequestWirelessAdb, onPairWirelessDebugging)
}
return
}
if (selected == "updates") {
- UpdatesPage(s, a, modifier.padding(horizontal = padH, vertical = padV))
+ UpdatesPage(s, a, modifier.padding(horizontal = padH).padding(top = padV))
return
}
if (selected == "steam") {
@@ -261,24 +270,12 @@ private fun Content(
DesktopPage(s, a, modifier, onSelect)
return
}
+ if (selected == "android-apps" && s.isHomeApp) {
+ AndroidAppsPage(s, modifier, onAndroidAppClick)
+ return
+ }
Column(modifier = modifier.verticalScroll(rememberScrollState()).padding(horizontal = padH, vertical = padV)) {
when {
- selected == "android-apps" && s.isHomeApp -> {
- Rise(0) { PageHeader(stringResource(R.string.content_android_apps)) { Chip(stringResource(R.string.content_apps_count, s.androidApps.size), ok = false) } }
- if (s.androidApps.isEmpty()) Rise(3) { Note(stringResource(R.string.content_no_android_apps)) }
- else Rise(3, Modifier.fillMaxWidth()) {
- ArtGrid(s.androidApps.map { app ->
- Tile(
- app.label,
- null,
- null,
- "android:${app.packageName}",
- onClick = { onAndroidAppClick(app) },
- iconBitmap = app.icon,
- )
- })
- }
- }
selected.startsWith("user:") -> {
val app = UserAppsState.items.firstOrNull { "user:${it.key}" == selected }
if (app == null) Note(stringResource(R.string.user_apps_gone)) else UserAppPage(app, s, a, onSelect)
diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndRail.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndRail.kt
index 108724000..b7ac8ee43 100644
--- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndRail.kt
+++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndRail.kt
@@ -118,7 +118,7 @@ internal fun BottomBar(
}
}
-private fun navigationKeys(s: FrontEndState) = buildList {
+internal fun navigationKeys(s: FrontEndState) = buildList {
add("steam"); add("games")
if (s.gameStoresEnabled) add("stores")
add("desktop"); add("components")
diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt
index 3223826f3..69abceb38 100644
--- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt
+++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt
@@ -507,6 +507,7 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com
selected.startsWith("emu:") || selected.startsWith("rom:") || selected.startsWith("user:") || selected == "store" -> "desktop"
else -> s.pageKey ?: selected
}
+ val railSelectionNow = androidx.compose.runtime.rememberUpdatedState(railSelection)
var railPicks by remember { mutableStateOf(0) }
val showRailPage: (String, Boolean) -> Unit = { key, focusContent ->
if (key == "components") a.onComponents(focusContent)
@@ -609,7 +610,16 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com
},
) {
Pane(
- s, selected, a, page, Modifier.fillMaxSize().then(paneFocus), { selected = it },
+ s, selected, a, page,
+ Modifier.fillMaxSize().then(paneFocus).pageSwipe({ !processSettingsPageVisible && appToChooseDisplay == null }) { dir ->
+ val keys = navigationKeys(s)
+ val at = keys.indexOf(railSelectionNow.value)
+ if (at >= 0) keys.getOrNull(at + dir)?.let { key ->
+ PageSwipe.direction = dir
+ showRailPage(key, false)
+ }
+ },
+ { selected = it },
onAndroidAppClick = { app ->
if (s.secondScreenDisplays.isEmpty()) a.onAndroidApp(app, null)
else appToChooseDisplay = app
diff --git a/app/src/main/java/com/droiddeck/launcher/ui/LibraryPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/LibraryPage.kt
index 4a3b24ce6..0f1b579c9 100644
--- a/app/src/main/java/com/droiddeck/launcher/ui/LibraryPage.kt
+++ b/app/src/main/java/com/droiddeck/launcher/ui/LibraryPage.kt
@@ -304,7 +304,7 @@ internal fun LibraryPage(s: FrontEndState, a: FrontEndActions, selected: String,
open, detailScroll,
gridScrolled = { grid.firstVisibleItemIndex > 0 || grid.firstVisibleItemScrollOffset > 0 },
onDetailFocus = { detailFocused = it },
- modifier = modifier.padding(horizontal = if (narrow) 14.dp else 22.dp, vertical = if (narrow) 10.dp else 14.dp),
+ modifier = modifier.padding(horizontal = if (narrow) 14.dp else 22.dp).padding(top = if (narrow) 10.dp else 14.dp),
wrap = { key, content -> StoreTheme(GameRef.parse(key).let { r -> r.store ?: r.game(s.steamGames)?.source?.let { Store.byId(it) } }) { content() } },
grid = {
LibraryGrid(
@@ -638,7 +638,7 @@ private fun LibraryRow(e: LibraryEntry, first: Boolean, navigation: Modifier, ch
}
@Composable
-private fun HeaderIconButton(description: String, icon: androidx.compose.ui.graphics.vector.ImageVector, key: String, onClick: () -> Unit) {
+internal fun HeaderIconButton(description: String, icon: androidx.compose.ui.graphics.vector.ImageVector, key: String, onClick: () -> Unit) {
val colors = MaterialTheme.colorScheme
val pal = LocalPalette.current
val src = remember { MutableInteractionSource() }
@@ -655,7 +655,7 @@ private fun HeaderIconButton(description: String, icon: androidx.compose.ui.grap
/** Choosing Library in the rail lands on the first game, as Play Steam does on the Steam page. */
@Composable
-private fun Modifier.paneEntry(): Modifier {
+internal fun Modifier.paneEntry(): Modifier {
val ff = LocalFrontFocus.current ?: return this
androidx.compose.runtime.DisposableEffect(ff) {
ff.primaryAttached++
diff --git a/app/src/main/java/com/droiddeck/launcher/ui/PageSwipe.kt b/app/src/main/java/com/droiddeck/launcher/ui/PageSwipe.kt
new file mode 100644
index 000000000..94c7a8cf3
--- /dev/null
+++ b/app/src/main/java/com/droiddeck/launcher/ui/PageSwipe.kt
@@ -0,0 +1,60 @@
+package com.droiddeck.launcher.ui
+
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.input.pointer.util.VelocityTracker
+import androidx.compose.ui.input.pointer.util.addPointerInputChange
+import androidx.compose.ui.unit.dp
+import kotlin.math.abs
+
+internal object PageSwipe {
+ var locks by mutableIntStateOf(0)
+ var direction by mutableIntStateOf(0)
+}
+
+// Runs on the Final pass so carousels, sliders, scrollers and widgets that consumed the drag keep it.
+@Composable
+internal fun Modifier.pageSwipe(enabled: () -> Boolean, onSwipe: (Int) -> Unit): Modifier {
+ val enabledNow by rememberUpdatedState(enabled)
+ val onSwipeNow by rememberUpdatedState(onSwipe)
+ return pointerInput(Unit) {
+ val distance = 72.dp.toPx()
+ val flingDistance = 32.dp.toPx()
+ val flingVelocity = 900.dp.toPx()
+ val slop = viewConfiguration.touchSlop
+ awaitEachGesture {
+ val down = awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Final)
+ if (!enabledNow() || PageSwipe.locks > 0) return@awaitEachGesture
+ val tracker = VelocityTracker()
+ tracker.addPointerInputChange(down)
+ var total = Offset.Zero
+ var mine = false
+ while (true) {
+ val event = awaitPointerEvent(PointerEventPass.Final)
+ if (event.changes.size > 1) return@awaitEachGesture
+ val change = event.changes.firstOrNull { it.id == down.id } ?: return@awaitEachGesture
+ if (!change.pressed) break
+ if (!mine && change.isConsumed) return@awaitEachGesture
+ total += change.position - change.previousPosition
+ tracker.addPointerInputChange(change)
+ if (!mine && abs(total.x) > slop && abs(total.x) > abs(total.y) * 2f) mine = true
+ if (mine) change.consume()
+ }
+ if (!mine || PageSwipe.locks > 0) return@awaitEachGesture
+ val vx = tracker.calculateVelocity().x
+ val horizontal = abs(total.x) > abs(total.y) * 1.5f
+ val far = abs(total.x) > distance
+ val flung = abs(total.x) > flingDistance && abs(vx) > flingVelocity && (vx > 0) == (total.x > 0)
+ if (horizontal && (far || flung)) onSwipeNow(if (total.x < 0) 1 else -1)
+ }
+ }
+}
diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StorePage.kt b/app/src/main/java/com/droiddeck/launcher/ui/StorePage.kt
index cd3a905c7..e3724dd83 100644
--- a/app/src/main/java/com/droiddeck/launcher/ui/StorePage.kt
+++ b/app/src/main/java/com/droiddeck/launcher/ui/StorePage.kt
@@ -157,7 +157,7 @@ private fun FlathubHero(s: FrontEndState, ring: Int) {
StoreState.sections.values.flatten().mapNotNull { it.icon }.distinct().take(48).takeIf { it.size >= 8 }.orEmpty()
}
val busy = StoreState.busy == "setup"
- BoxWithConstraints(Modifier.fillMaxSize().padding(horizontal = if (narrow) 14.dp else 22.dp, vertical = if (narrow) 10.dp else 14.dp)) {
+ BoxWithConstraints(Modifier.fillMaxSize().padding(horizontal = if (narrow) 14.dp else 22.dp).padding(top = if (narrow) 10.dp else 14.dp)) {
val shape = RoundedCornerShape(20.dp)
Box(Modifier.fillMaxWidth().height(maxHeight.coerceAtLeast(320.dp)).clip(shape).background(colors.background).border(1.dp, pal.line, shape)) {
androidx.compose.animation.Crossfade(icons, animationSpec = Motion.tw(600), label = "flathubWall") { tiles ->
@@ -316,7 +316,7 @@ internal fun Storefront(s: FrontEndState, a: FrontEndActions) {
}
Column(
- Modifier.fillMaxSize().padding(horizontal = padH, vertical = if (narrow) 10.dp else 14.dp)
+ Modifier.fillMaxSize().padding(horizontal = padH).padding(top = if (narrow) 10.dp else 14.dp)
.bumpers(
onPrevious = { if (openApp == null) mode = modes[(modes.indexOf(mode) + modes.size - 1) % modes.size] },
onNext = { if (openApp == null) mode = modes[(modes.indexOf(mode) + 1) % modes.size] },
diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt
index 916e44e43..92b118d54 100644
--- a/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt
+++ b/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt
@@ -311,7 +311,7 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier
val backLabel = stringResource(R.string.common_back)
Box(modifier.onGloballyPositioned { hostAt = it.positionInRoot() }) {
Column(
- modifier = Modifier.fillMaxSize().padding(horizontal = padH, vertical = if (narrow) 10.dp else 14.dp)
+ modifier = Modifier.fillMaxSize().padding(horizontal = padH).padding(top = if (narrow) 10.dp else 14.dp)
.then(if (openGame != null && !arranging) Modifier else Modifier.bumpers(
onPrevious = {
val picked = Store.byId(focusedChip)
diff --git a/app/src/main/java/com/droiddeck/launcher/ui/UpdatesPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/UpdatesPage.kt
index 927a2c470..dd73939a7 100644
--- a/app/src/main/java/com/droiddeck/launcher/ui/UpdatesPage.kt
+++ b/app/src/main/java/com/droiddeck/launcher/ui/UpdatesPage.kt
@@ -115,7 +115,7 @@ internal fun UpdatesPage(s: FrontEndState, a: FrontEndActions, modifier: Modifie
}
BoxWithConstraints(Modifier.weight(1f).fillMaxWidth()) {
val stacked = LocalNarrowPane.current || maxWidth < 600.dp
- Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(bottom = 8.dp)) {
+ Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(bottom = 16.dp)) {
if (stacked) {
StatusPanel(s, u, ua, me)
Box(Modifier.height(18.dp))
diff --git a/app/src/main/java/com/droiddeck/launcher/ui/WirelessAdbFixPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/WirelessAdbFixPage.kt
index 5c719ca90..7e67b352b 100644
--- a/app/src/main/java/com/droiddeck/launcher/ui/WirelessAdbFixPage.kt
+++ b/app/src/main/java/com/droiddeck/launcher/ui/WirelessAdbFixPage.kt
@@ -360,6 +360,7 @@ internal fun AdbTextField(
onNext: (() -> Unit)? = null,
onDone: (() -> Unit)? = null,
focusOnShow: Boolean = false,
+ showLabel: Boolean = true,
) {
val context = LocalContext.current
val focusAsked = remember { booleanArrayOf(false) }
@@ -374,7 +375,7 @@ internal fun AdbTextField(
val colors = MaterialTheme.colorScheme
Column(modifier = modifier.fillMaxWidth()) {
- Text(
+ if (showLabel) Text(
label,
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Medium,
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index f78cf3014..e07f0fade 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -3221,4 +3221,50 @@
المجموعات
سحابية
إعدادات السحابة
+ البحث في التطبيقات
+ لا توجد تطبيقات تطابق ”%1$s“.
+ إضافة أداة
+ جارٍ تحميل الأدوات…
+ لا توجد تطبيقات على هذا الجهاز توفّر أدوات.
+ أعرض
+ أضيق
+ أطول
+ أقصر
+ نقل إلى الأمام
+ نقل إلى الخلف
+ إعدادات الأداة
+ إزالة الأداة
+ إضافة إلى مجلد
+ مجلد جديد
+ إزالة من المجلد
+ معلومات التطبيق
+ إلغاء التثبيت
+ إعادة التسمية
+ فك التجميع
+ مجلد
+ اسم المجلد
+ عرض
+ كل التطبيقات
+ الألعاب
+ المنزّلة
+ النظام
+ المخفية
+ إخفاء
+ إظهار في التطبيقات
+ إزالة الاختصار
+ إضافة إلى التطبيقات
+ إضافة
+ لا توجد تطبيقات مخفية.
+ الاسم أ–ي
+ الاسم ي–أ
+ المثبّتة مؤخرًا
+ المحدّثة مؤخرًا
+
+ - %d تطبيق
+ - تطبيق واحد
+ - تطبيقان
+ - %d تطبيقات
+ - %d تطبيقًا
+ - %d تطبيق
+
diff --git a/app/src/main/res/values-b+es+419/strings.xml b/app/src/main/res/values-b+es+419/strings.xml
index 09e76582e..0861e3525 100644
--- a/app/src/main/res/values-b+es+419/strings.xml
+++ b/app/src/main/res/values-b+es+419/strings.xml
@@ -3071,4 +3071,47 @@
Colecciones
Nube
Configuración de nube
+ Buscar apps
+ Ninguna app coincide con «%1$s».
+ Agregar widget
+ Cargando widgets…
+ Ninguna app de este dispositivo ofrece widgets.
+ Más ancho
+ Más angosto
+ Más alto
+ Más bajo
+ Mover antes
+ Mover después
+ Configuración del widget
+ Quitar widget
+ Agregar a carpeta
+ Carpeta nueva
+ Quitar de la carpeta
+ Información de la app
+ Desinstalar
+ Cambiar nombre
+ Desagrupar
+ Carpeta
+ Nombre de la carpeta
+ Mostrar
+ Todas las apps
+ Juegos
+ Descargadas
+ Sistema
+ Ocultas
+ Ocultar
+ Mostrar en apps
+ Quitar acceso directo
+ Agregar a Apps
+ Agregar
+ No hay apps ocultas.
+ Nombre A–Z
+ Nombre Z–A
+ Instaladas recientemente
+ Actualizadas recientemente
+
+ - %d app
+ - %d apps
+ - %d apps
+
diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml
index 314b01f30..fecfd3a04 100644
--- a/app/src/main/res/values-bg/strings.xml
+++ b/app/src/main/res/values-bg/strings.xml
@@ -3085,4 +3085,46 @@
Колекции
Облак
Настройки на облака
+ Търсене на приложения
+ Няма приложения, отговарящи на „%1$s“.
+ Добавяне на приспособление
+ Приспособленията се зареждат…
+ Няма приложения с приспособления на това устройство.
+ По-широко
+ По-тясно
+ По-високо
+ По-ниско
+ Преместване по-рано
+ Преместване по-късно
+ Настройки на приспособлението
+ Премахване на приспособлението
+ Добавяне в папка
+ Нова папка
+ Премахване от папката
+ Информация за приложението
+ Деинсталиране
+ Преименуване
+ Разгрупиране
+ Папка
+ Име на папката
+ Показване
+ Всички приложения
+ Игри
+ Изтеглени
+ Системни
+ Скрити
+ Скриване
+ Показване в приложенията
+ Премахване на прекия път
+ Добавяне към приложенията
+ Добавяне
+ Няма скрити приложения.
+ Име А–Я
+ Име Я–А
+ Наскоро инсталирани
+ Наскоро актуализирани
+
+ - %d приложение
+ - %d приложения
+
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index e0278767d..b9694894e 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -3153,4 +3153,48 @@
Kolekce
Cloud
Nastavení cloudu
+ Hledat aplikace
+ Výrazu „%1$s“ neodpovídá žádná aplikace.
+ Přidat widget
+ Načítání widgetů…
+ Žádná aplikace v zařízení nenabízí widgety.
+ Širší
+ Užší
+ Vyšší
+ Nižší
+ Posunout dříve
+ Posunout později
+ Nastavení widgetu
+ Odebrat widget
+ Přidat do složky
+ Nová složka
+ Odebrat ze složky
+ Informace o aplikaci
+ Odinstalovat
+ Přejmenovat
+ Zrušit složku
+ Složka
+ Název složky
+ Zobrazit
+ Všechny aplikace
+ Hry
+ Stažené
+ Systémové
+ Skryté
+ Skrýt
+ Zobrazit v aplikacích
+ Odebrat zkratku
+ Přidat do aplikací
+ Přidat
+ Žádné skryté aplikace.
+ Název A–Z
+ Název Z–A
+ Nedávno nainstalované
+ Nedávno aktualizované
+
+ - %d aplikace
+ - %d aplikace
+ - %d aplikace
+ - %d aplikací
+
diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml
index 789cbc762..af13b452d 100644
--- a/app/src/main/res/values-da/strings.xml
+++ b/app/src/main/res/values-da/strings.xml
@@ -3085,4 +3085,46 @@
Samlinger
Sky
Skyindstillinger
+ Søg i apps
+ Ingen apps matcher “%1$s”.
+ Tilføj widget
+ Indlæser widgets…
+ Ingen apps på enheden tilbyder widgets.
+ Bredere
+ Smallere
+ Højere
+ Lavere
+ Flyt tidligere
+ Flyt senere
+ Widgetindstillinger
+ Fjern widget
+ Føj til mappe
+ Ny mappe
+ Fjern fra mappe
+ Appinfo
+ Afinstaller
+ Omdøb
+ Opløs
+ Mappe
+ Mappenavn
+ Vis
+ Alle apps
+ Spil
+ Downloadet
+ System
+ Skjulte
+ Skjul
+ Vis i apps
+ Fjern genvej
+ Føj til Apps
+ Tilføj
+ Ingen skjulte apps.
+ Navn A–Å
+ Navn Å–A
+ Senest installeret
+ Senest opdateret
+
+ - %d app
+ - %d apps
+
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 1ddc2655d..9ffcfe1fd 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -3085,4 +3085,46 @@
Sammlungen
Cloud
Cloud-Einstellungen
+ Apps durchsuchen
+ Keine Apps passen zu „%1$s“.
+ Widget hinzufügen
+ Widgets werden geladen…
+ Keine App auf diesem Gerät bietet Widgets an.
+ Breiter
+ Schmaler
+ Höher
+ Niedriger
+ Nach vorne
+ Nach hinten
+ Widget-Einstellungen
+ Widget entfernen
+ Zu Ordner hinzufügen
+ Neuer Ordner
+ Aus Ordner entfernen
+ App-Info
+ Deinstallieren
+ Umbenennen
+ Auflösen
+ Ordner
+ Ordnername
+ Anzeigen
+ Alle Apps
+ Spiele
+ Heruntergeladen
+ System
+ Ausgeblendet
+ Ausblenden
+ In Apps anzeigen
+ Verknüpfung entfernen
+ Zu Apps hinzufügen
+ Hinzufügen
+ Keine ausgeblendeten Apps.
+ Name A–Z
+ Name Z–A
+ Zuletzt installiert
+ Zuletzt aktualisiert
+
+ - %d App
+ - %d Apps
+
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index 85e3befcd..c8588ac9c 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -3085,4 +3085,46 @@
Συλλογές
Cloud
Ρυθμίσεις cloud
+ Αναζήτηση εφαρμογών
+ Καμία εφαρμογή δεν ταιριάζει με «%1$s».
+ Προσθήκη γραφικού στοιχείου
+ Φόρτωση γραφικών στοιχείων…
+ Καμία εφαρμογή στη συσκευή δεν προσφέρει γραφικά στοιχεία.
+ Πλατύτερο
+ Στενότερο
+ Ψηλότερο
+ Χαμηλότερο
+ Μετακίνηση νωρίτερα
+ Μετακίνηση αργότερα
+ Ρυθμίσεις γραφικού στοιχείου
+ Αφαίρεση γραφικού στοιχείου
+ Προσθήκη σε φάκελο
+ Νέος φάκελος
+ Αφαίρεση από φάκελο
+ Πληροφορίες εφαρμογής
+ Απεγκατάσταση
+ Μετονομασία
+ Κατάργηση ομάδας
+ Φάκελος
+ Όνομα φακέλου
+ Εμφάνιση
+ Όλες οι εφαρμογές
+ Παιχνίδια
+ Ληφθείσες
+ Συστήματος
+ Κρυφές
+ Απόκρυψη
+ Εμφάνιση στις εφαρμογές
+ Αφαίρεση συντόμευσης
+ Προσθήκη στις Εφαρμογές
+ Προσθήκη
+ Δεν υπάρχουν κρυφές εφαρμογές.
+ Όνομα Α–Ω
+ Όνομα Ω–Α
+ Πρόσφατα εγκατεστημένες
+ Πρόσφατα ενημερωμένες
+
+ - %d εφαρμογή
+ - %d εφαρμογές
+
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index f6d49f6f9..82af91332 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -3121,4 +3121,47 @@
Colecciones
Nube
Ajustes de nube
+ Buscar aplicaciones
+ Ninguna aplicación coincide con «%1$s».
+ Añadir widget
+ Cargando widgets…
+ Ninguna aplicación de este dispositivo ofrece widgets.
+ Más ancho
+ Más estrecho
+ Más alto
+ Más bajo
+ Mover antes
+ Mover después
+ Ajustes del widget
+ Quitar widget
+ Añadir a carpeta
+ Carpeta nueva
+ Quitar de la carpeta
+ Información de la aplicación
+ Desinstalar
+ Cambiar nombre
+ Desagrupar
+ Carpeta
+ Nombre de la carpeta
+ Mostrar
+ Todas las aplicaciones
+ Juegos
+ Descargadas
+ Sistema
+ Ocultas
+ Ocultar
+ Mostrar en aplicaciones
+ Quitar acceso directo
+ Añadir a Aplicaciones
+ Añadir
+ No hay aplicaciones ocultas.
+ Nombre A–Z
+ Nombre Z–A
+ Instaladas recientemente
+ Actualizadas recientemente
+
+ - %d aplicación
+ - %d aplicaciones
+ - %d aplicaciones
+
diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml
index 79737e2e0..3ad1e9247 100644
--- a/app/src/main/res/values-fi/strings.xml
+++ b/app/src/main/res/values-fi/strings.xml
@@ -3085,4 +3085,46 @@
Kokoelmat
Pilvi
Pilviasetukset
+ Hae sovelluksia
+ Mikään sovellus ei vastaa hakua ”%1$s”.
+ Lisää widget
+ Ladataan widgettejä…
+ Mikään tämän laitteen sovellus ei tarjoa widgettejä.
+ Leveämpi
+ Kapeampi
+ Korkeampi
+ Matalampi
+ Siirrä aiemmaksi
+ Siirrä myöhemmäksi
+ Widgetin asetukset
+ Poista widget
+ Lisää kansioon
+ Uusi kansio
+ Poista kansiosta
+ Sovelluksen tiedot
+ Poista asennus
+ Nimeä uudelleen
+ Pura kansio
+ Kansio
+ Kansion nimi
+ Näytä
+ Kaikki sovellukset
+ Pelit
+ Ladatut
+ Järjestelmä
+ Piilotetut
+ Piilota
+ Näytä sovelluksissa
+ Poista pikakuvake
+ Lisää sovelluksiin
+ Lisää
+ Ei piilotettuja sovelluksia.
+ Nimi A–Ö
+ Nimi Ö–A
+ Äskettäin asennetut
+ Äskettäin päivitetyt
+
+ - %d sovellus
+ - %d sovellusta
+
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 97cb78aa5..ef4fdb8f5 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -3121,4 +3121,47 @@
Collections
Cloud
Paramètres cloud
+ Rechercher des applications
+ Aucune application ne correspond à « %1$s ».
+ Ajouter un widget
+ Chargement des widgets…
+ Aucune application de cet appareil ne propose de widgets.
+ Plus large
+ Plus étroit
+ Plus haut
+ Moins haut
+ Avancer
+ Reculer
+ Paramètres du widget
+ Retirer le widget
+ Ajouter à un dossier
+ Nouveau dossier
+ Retirer du dossier
+ Infos sur l’application
+ Désinstaller
+ Renommer
+ Dégrouper
+ Dossier
+ Nom du dossier
+ Afficher
+ Toutes les applications
+ Jeux
+ Téléchargées
+ Système
+ Masquées
+ Masquer
+ Afficher dans les applications
+ Retirer le raccourci
+ Ajouter aux applications
+ Ajouter
+ Aucune application masquée.
+ Nom A–Z
+ Nom Z–A
+ Installées récemment
+ Mises à jour récemment
+
+ - %d application
+ - %d applications
+ - %d applications
+
diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml
index a1edf261e..ea458fa23 100644
--- a/app/src/main/res/values-hi/strings.xml
+++ b/app/src/main/res/values-hi/strings.xml
@@ -3085,4 +3085,46 @@
संग्रह
क्लाउड
क्लाउड सेटिंग्स
+ ऐप खोजें
+ “%1$s” से कोई ऐप मेल नहीं खाता।
+ विजेट जोड़ें
+ विजेट लोड हो रहे हैं…
+ इस डिवाइस पर कोई ऐप विजेट नहीं देता।
+ चौड़ा
+ पतला
+ लंबा
+ छोटा
+ पहले ले जाएँ
+ बाद में ले जाएँ
+ विजेट सेटिंग
+ विजेट हटाएँ
+ फ़ोल्डर में जोड़ें
+ नया फ़ोल्डर
+ फ़ोल्डर से हटाएँ
+ ऐप की जानकारी
+ अनइंस्टॉल करें
+ नाम बदलें
+ समूह तोड़ें
+ फ़ोल्डर
+ फ़ोल्डर का नाम
+ दिखाएँ
+ सभी ऐप
+ गेम
+ डाउनलोड किए गए
+ सिस्टम
+ छिपे हुए
+ छिपाएँ
+ ऐप में दिखाएँ
+ शॉर्टकट हटाएँ
+ ऐप में जोड़ें
+ जोड़ें
+ कोई छिपा हुआ ऐप नहीं।
+ नाम A–Z
+ नाम Z–A
+ हाल ही में इंस्टॉल किए गए
+ हाल ही में अपडेट किए गए
+
+ - %d ऐप
+ - %d ऐप
+
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index 747b7ed5c..8d8f2774d 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -3085,4 +3085,46 @@
Gyűjtemények
Felhő
Felhőbeállítások
+ Alkalmazások keresése
+ Egy alkalmazás sem felel meg ennek: „%1$s”.
+ Modul hozzáadása
+ Modulok betöltése…
+ Az eszközön egyik alkalmazás sem kínál modult.
+ Szélesebb
+ Keskenyebb
+ Magasabb
+ Alacsonyabb
+ Előrébb
+ Hátrébb
+ Modulbeállítások
+ Modul eltávolítása
+ Hozzáadás mappához
+ Új mappa
+ Eltávolítás a mappából
+ Alkalmazásinformáció
+ Eltávolítás
+ Átnevezés
+ Szétbontás
+ Mappa
+ Mappa neve
+ Megjelenítés
+ Összes alkalmazás
+ Játékok
+ Letöltött
+ Rendszer
+ Rejtett
+ Elrejtés
+ Megjelenítés az alkalmazások között
+ Parancsikon eltávolítása
+ Hozzáadás az alkalmazásokhoz
+ Hozzáadás
+ Nincsenek rejtett alkalmazások.
+ Név A–Z
+ Név Z–A
+ Nemrég telepített
+ Nemrég frissített
+
+ - %d alkalmazás
+ - %d alkalmazás
+
diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml
index 79291d8e7..c0c4fa21f 100644
--- a/app/src/main/res/values-in/strings.xml
+++ b/app/src/main/res/values-in/strings.xml
@@ -3075,4 +3075,45 @@
Koleksi
Cloud
Pengaturan cloud
+ Cari aplikasi
+ Tidak ada aplikasi yang cocok dengan “%1$s”.
+ Tambahkan widget
+ Memuat widget…
+ Tidak ada aplikasi di perangkat ini yang menyediakan widget.
+ Lebih lebar
+ Lebih sempit
+ Lebih tinggi
+ Lebih pendek
+ Pindahkan ke depan
+ Pindahkan ke belakang
+ Setelan widget
+ Hapus widget
+ Tambahkan ke folder
+ Folder baru
+ Keluarkan dari folder
+ Info aplikasi
+ Uninstal
+ Ganti nama
+ Pisahkan
+ Folder
+ Nama folder
+ Tampilkan
+ Semua aplikasi
+ Game
+ Didownload
+ Sistem
+ Tersembunyi
+ Sembunyikan
+ Tampilkan di aplikasi
+ Hapus pintasan
+ Tambahkan ke Aplikasi
+ Tambahkan
+ Tidak ada aplikasi tersembunyi.
+ Nama A–Z
+ Nama Z–A
+ Baru diinstal
+ Baru diperbarui
+
+ - %d aplikasi
+
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index bbe535146..c20bdc151 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -3119,4 +3119,47 @@
Raccolte
Cloud
Impostazioni cloud
+ Cerca app
+ Nessuna app corrisponde a «%1$s».
+ Aggiungi widget
+ Caricamento widget…
+ Nessuna app su questo dispositivo offre widget.
+ Più largo
+ Più stretto
+ Più alto
+ Più basso
+ Sposta prima
+ Sposta dopo
+ Impostazioni widget
+ Rimuovi widget
+ Aggiungi a cartella
+ Nuova cartella
+ Rimuovi dalla cartella
+ Info app
+ Disinstalla
+ Rinomina
+ Separa
+ Cartella
+ Nome cartella
+ Mostra
+ Tutte le app
+ Giochi
+ Scaricate
+ Sistema
+ Nascoste
+ Nascondi
+ Mostra nelle app
+ Rimuovi scorciatoia
+ Aggiungi ad App
+ Aggiungi
+ Nessuna app nascosta.
+ Nome A–Z
+ Nome Z–A
+ Installate di recente
+ Aggiornate di recente
+
+ - %d app
+ - %d app
+ - %d app
+
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 19f6dbfae..87a910cae 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -3062,4 +3062,45 @@
コレクション
クラウド
クラウド設定
+ アプリを検索
+ 「%1$s」に一致するアプリはありません。
+ ウィジェットを追加
+ ウィジェットを読み込み中…
+ このデバイスにはウィジェットを提供するアプリがありません。
+ 幅を広く
+ 幅を狭く
+ 高く
+ 低く
+ 前へ移動
+ 後ろへ移動
+ ウィジェットの設定
+ ウィジェットを削除
+ フォルダに追加
+ 新しいフォルダ
+ フォルダから削除
+ アプリ情報
+ アンインストール
+ 名前を変更
+ グループを解除
+ フォルダ
+ フォルダ名
+ 表示
+ すべてのアプリ
+ ゲーム
+ ダウンロード済み
+ システム
+ 非表示
+ 非表示にする
+ アプリに表示
+ ショートカットを削除
+ アプリに追加
+ 追加
+ 非表示のアプリはありません。
+ 名前 A–Z
+ 名前 Z–A
+ 最近インストール
+ 最近更新
+
+ - %d 個のアプリ
+
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index 9163a3a7d..10d6fd46d 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -3062,4 +3062,45 @@
컬렉션
클라우드
클라우드 설정
+ 앱 검색
+ ‘%1$s’와(과) 일치하는 앱이 없습니다.
+ 위젯 추가
+ 위젯 불러오는 중…
+ 이 기기에 위젯을 제공하는 앱이 없습니다.
+ 더 넓게
+ 더 좁게
+ 더 높게
+ 더 낮게
+ 앞으로 이동
+ 뒤로 이동
+ 위젯 설정
+ 위젯 삭제
+ 폴더에 추가
+ 새 폴더
+ 폴더에서 제거
+ 앱 정보
+ 제거
+ 이름 변경
+ 그룹 해제
+ 폴더
+ 폴더 이름
+ 표시
+ 모든 앱
+ 게임
+ 다운로드됨
+ 시스템
+ 숨김
+ 숨기기
+ 앱에 표시
+ 바로가기 삭제
+ 앱에 추가
+ 추가
+ 숨긴 앱이 없습니다.
+ 이름 A–Z
+ 이름 Z–A
+ 최근 설치
+ 최근 업데이트
+
+ - 앱 %d개
+
diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml
index eb41beb9b..098f2429a 100644
--- a/app/src/main/res/values-ms/strings.xml
+++ b/app/src/main/res/values-ms/strings.xml
@@ -3060,4 +3060,45 @@
Koleksi
Awan
Tetapan awan
+ Cari apl
+ Tiada apl sepadan dengan “%1$s”.
+ Tambah widget
+ Memuatkan widget…
+ Tiada apl pada peranti ini yang menawarkan widget.
+ Lebih lebar
+ Lebih sempit
+ Lebih tinggi
+ Lebih rendah
+ Alih ke depan
+ Alih ke belakang
+ Tetapan widget
+ Alih keluar widget
+ Tambah pada folder
+ Folder baharu
+ Alih keluar daripada folder
+ Maklumat apl
+ Nyahpasang
+ Namakan semula
+ Leraikan
+ Folder
+ Nama folder
+ Tunjukkan
+ Semua apl
+ Permainan
+ Dimuat turun
+ Sistem
+ Tersembunyi
+ Sembunyikan
+ Tunjukkan dalam apl
+ Alih keluar pintasan
+ Tambah pada Apl
+ Tambah
+ Tiada apl tersembunyi.
+ Nama A–Z
+ Nama Z–A
+ Dipasang baru-baru ini
+ Dikemas kini baru-baru ini
+
+ - %d apl
+
diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml
index 75653c225..e95a57c22 100644
--- a/app/src/main/res/values-nb/strings.xml
+++ b/app/src/main/res/values-nb/strings.xml
@@ -3085,4 +3085,46 @@
Samlinger
Sky
Skyinnstillinger
+ Søk i apper
+ Ingen apper samsvarer med «%1$s».
+ Legg til modul
+ Laster inn moduler…
+ Ingen apper på enheten tilbyr moduler.
+ Bredere
+ Smalere
+ Høyere
+ Lavere
+ Flytt tidligere
+ Flytt senere
+ Modulinnstillinger
+ Fjern modul
+ Legg til i mappe
+ Ny mappe
+ Fjern fra mappe
+ Appinfo
+ Avinstaller
+ Gi nytt navn
+ Løs opp
+ Mappe
+ Mappenavn
+ Vis
+ Alle apper
+ Spill
+ Nedlastet
+ System
+ Skjult
+ Skjul
+ Vis i apper
+ Fjern snarvei
+ Legg til i Apper
+ Legg til
+ Ingen skjulte apper.
+ Navn A–Å
+ Navn Å–A
+ Nylig installert
+ Nylig oppdatert
+
+ - %d app
+ - %d apper
+
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index 53748c36a..5cd7fdf36 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -3085,4 +3085,46 @@
Collecties
Cloud
Cloudinstellingen
+ Apps zoeken
+ Geen apps gevonden voor ‘%1$s’.
+ Widget toevoegen
+ Widgets laden…
+ Geen enkele app op dit apparaat biedt widgets.
+ Breder
+ Smaller
+ Hoger
+ Lager
+ Naar voren
+ Naar achteren
+ Widgetinstellingen
+ Widget verwijderen
+ Toevoegen aan map
+ Nieuwe map
+ Uit map verwijderen
+ App-info
+ Verwijderen
+ Naam wijzigen
+ Opheffen
+ Map
+ Mapnaam
+ Tonen
+ Alle apps
+ Games
+ Gedownload
+ Systeem
+ Verborgen
+ Verbergen
+ Tonen in apps
+ Snelkoppeling verwijderen
+ Toevoegen aan Apps
+ Toevoegen
+ Geen verborgen apps.
+ Naam A–Z
+ Naam Z–A
+ Recent geïnstalleerd
+ Recent bijgewerkt
+
+ - %d app
+ - %d apps
+
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 37619c513..4e87191fe 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -3156,4 +3156,48 @@
Kolekcje
Chmura
Ustawienia chmury
+ Szukaj aplikacji
+ Żadna aplikacja nie pasuje do „%1$s”.
+ Dodaj widżet
+ Wczytywanie widżetów…
+ Żadna aplikacja na tym urządzeniu nie oferuje widżetów.
+ Szerszy
+ Węższy
+ Wyższy
+ Niższy
+ Przesuń wcześniej
+ Przesuń później
+ Ustawienia widżetu
+ Usuń widżet
+ Dodaj do folderu
+ Nowy folder
+ Usuń z folderu
+ Informacje o aplikacji
+ Odinstaluj
+ Zmień nazwę
+ Rozgrupuj
+ Folder
+ Nazwa folderu
+ Pokaż
+ Wszystkie aplikacje
+ Gry
+ Pobrane
+ Systemowe
+ Ukryte
+ Ukryj
+ Pokaż w aplikacjach
+ Usuń skrót
+ Dodaj do aplikacji
+ Dodaj
+ Brak ukrytych aplikacji.
+ Nazwa A–Z
+ Nazwa Z–A
+ Ostatnio zainstalowane
+ Ostatnio zaktualizowane
+
+ - %d aplikacja
+ - %d aplikacje
+ - %d aplikacji
+ - %d aplikacji
+
diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml
index 177799c1a..ceac1c534 100644
--- a/app/src/main/res/values-pt-rPT/strings.xml
+++ b/app/src/main/res/values-pt-rPT/strings.xml
@@ -3119,4 +3119,47 @@
Coleções
Nuvem
Definições da nuvem
+ Pesquisar aplicações
+ Nenhuma aplicação corresponde a “%1$s”.
+ Adicionar widget
+ A carregar widgets…
+ Nenhuma aplicação neste dispositivo oferece widgets.
+ Mais largo
+ Mais estreito
+ Mais alto
+ Mais baixo
+ Mover para antes
+ Mover para depois
+ Definições do widget
+ Remover widget
+ Adicionar a pasta
+ Nova pasta
+ Remover da pasta
+ Informações da aplicação
+ Desinstalar
+ Mudar o nome
+ Desagrupar
+ Pasta
+ Nome da pasta
+ Mostrar
+ Todas as aplicações
+ Jogos
+ Transferidas
+ Sistema
+ Ocultas
+ Ocultar
+ Mostrar nas aplicações
+ Remover atalho
+ Adicionar às Aplicações
+ Adicionar
+ Nenhuma aplicação oculta.
+ Nome A–Z
+ Nome Z–A
+ Instaladas recentemente
+ Atualizadas recentemente
+
+ - %d aplicação
+ - %d aplicações
+ - %d aplicações
+
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index 2bdda9aac..825ef7b95 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -3119,4 +3119,47 @@
Coleções
Nuvem
Configurações da nuvem
+ Pesquisar apps
+ Nenhum app corresponde a “%1$s”.
+ Adicionar widget
+ Carregando widgets…
+ Nenhum app neste dispositivo oferece widgets.
+ Mais largo
+ Mais estreito
+ Mais alto
+ Mais baixo
+ Mover para antes
+ Mover para depois
+ Configurações do widget
+ Remover widget
+ Adicionar à pasta
+ Nova pasta
+ Remover da pasta
+ Informações do app
+ Desinstalar
+ Renomear
+ Desagrupar
+ Pasta
+ Nome da pasta
+ Mostrar
+ Todos os apps
+ Jogos
+ Baixados
+ Sistema
+ Ocultos
+ Ocultar
+ Mostrar nos apps
+ Remover atalho
+ Adicionar aos Apps
+ Adicionar
+ Nenhum app oculto.
+ Nome A–Z
+ Nome Z–A
+ Instalados recentemente
+ Atualizados recentemente
+
+ - %d app
+ - %d apps
+ - %d apps
+
diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml
index 49a0503c6..b95ef4857 100644
--- a/app/src/main/res/values-ro/strings.xml
+++ b/app/src/main/res/values-ro/strings.xml
@@ -3119,4 +3119,47 @@
Colecții
Cloud
Setări cloud
+ Caută aplicații
+ Nicio aplicație nu se potrivește cu „%1$s”.
+ Adaugă widget
+ Se încarcă widgeturile…
+ Nicio aplicație de pe acest dispozitiv nu oferă widgeturi.
+ Mai lat
+ Mai îngust
+ Mai înalt
+ Mai scund
+ Mută mai devreme
+ Mută mai târziu
+ Setări widget
+ Elimină widgetul
+ Adaugă în dosar
+ Dosar nou
+ Elimină din dosar
+ Informații despre aplicație
+ Dezinstalează
+ Redenumește
+ Degrupează
+ Dosar
+ Numele dosarului
+ Afișează
+ Toate aplicațiile
+ Jocuri
+ Descărcate
+ Sistem
+ Ascunse
+ Ascunde
+ Afișează în aplicații
+ Elimină comanda rapidă
+ Adaugă în Aplicații
+ Adaugă
+ Nicio aplicație ascunsă.
+ Nume A–Z
+ Nume Z–A
+ Instalate recent
+ Actualizate recent
+
+ - %d aplicație
+ - %d aplicații
+ - %d de aplicații
+
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index b4d440f2c..483d86ab5 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -3155,4 +3155,48 @@
Коллекции
Облако
Настройки облака
+ Поиск приложений
+ Нет приложений по запросу «%1$s».
+ Добавить виджет
+ Загрузка виджетов…
+ На этом устройстве нет приложений с виджетами.
+ Шире
+ Уже
+ Выше
+ Ниже
+ Переместить раньше
+ Переместить позже
+ Настройки виджета
+ Удалить виджет
+ Добавить в папку
+ Новая папка
+ Убрать из папки
+ О приложении
+ Удалить
+ Переименовать
+ Разгруппировать
+ Папка
+ Название папки
+ Показать
+ Все приложения
+ Игры
+ Загруженные
+ Системные
+ Скрытые
+ Скрыть
+ Показывать в приложениях
+ Удалить ярлык
+ Добавить в приложения
+ Добавить
+ Скрытых приложений нет.
+ Название А–Я
+ Название Я–А
+ Недавно установленные
+ Недавно обновлённые
+
+ - %d приложение
+ - %d приложения
+ - %d приложений
+ - %d приложения
+
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index 35c26e8f2..72c27705b 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -3085,4 +3085,46 @@
Samlingar
Moln
Molninställningar
+ Sök appar
+ Inga appar matchar ”%1$s”.
+ Lägg till widget
+ Läser in widgetar…
+ Inga appar på enheten erbjuder widgetar.
+ Bredare
+ Smalare
+ Högre
+ Lägre
+ Flytta tidigare
+ Flytta senare
+ Widgetinställningar
+ Ta bort widget
+ Lägg till i mapp
+ Ny mapp
+ Ta bort från mapp
+ Appinfo
+ Avinstallera
+ Byt namn
+ Dela upp
+ Mapp
+ Mappnamn
+ Visa
+ Alla appar
+ Spel
+ Nedladdade
+ System
+ Dolda
+ Dölj
+ Visa i appar
+ Ta bort genväg
+ Lägg till i Appar
+ Lägg till
+ Inga dolda appar.
+ Namn A–Ö
+ Namn Ö–A
+ Senast installerade
+ Senast uppdaterade
+
+ - %d app
+ - %d appar
+
diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml
index 348740686..9cdb0a55b 100644
--- a/app/src/main/res/values-th/strings.xml
+++ b/app/src/main/res/values-th/strings.xml
@@ -3060,4 +3060,45 @@
คอลเลกชัน
คลาวด์
การตั้งค่าคลาวด์
+ ค้นหาแอป
+ ไม่มีแอปที่ตรงกับ “%1$s”
+ เพิ่มวิดเจ็ต
+ กำลังโหลดวิดเจ็ต…
+ ไม่มีแอปในอุปกรณ์นี้ที่มีวิดเจ็ต
+ กว้างขึ้น
+ แคบลง
+ สูงขึ้น
+ เตี้ยลง
+ ย้ายไปก่อน
+ ย้ายไปหลัง
+ การตั้งค่าวิดเจ็ต
+ นำวิดเจ็ตออก
+ เพิ่มลงในโฟลเดอร์
+ โฟลเดอร์ใหม่
+ นำออกจากโฟลเดอร์
+ ข้อมูลแอป
+ ถอนการติดตั้ง
+ เปลี่ยนชื่อ
+ ยกเลิกกลุ่ม
+ โฟลเดอร์
+ ชื่อโฟลเดอร์
+ แสดง
+ แอปทั้งหมด
+ เกม
+ ที่ดาวน์โหลด
+ ระบบ
+ ที่ซ่อนไว้
+ ซ่อน
+ แสดงในแอป
+ นำทางลัดออก
+ เพิ่มลงในแอป
+ เพิ่ม
+ ไม่มีแอปที่ซ่อนไว้
+ ชื่อ A–Z
+ ชื่อ Z–A
+ ติดตั้งล่าสุด
+ อัปเดตล่าสุด
+
+ - %d แอป
+
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 78cdc1f70..3dea1e5bc 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -3087,4 +3087,46 @@
Koleksiyonlar
Bulut
Bulut ayarları
+ Uygulama ara
+ “%1$s” ile eşleşen uygulama yok.
+ Widget ekle
+ Widget’lar yükleniyor…
+ Bu cihazdaki hiçbir uygulama widget sunmuyor.
+ Daha geniş
+ Daha dar
+ Daha uzun
+ Daha kısa
+ Öne taşı
+ Arkaya taşı
+ Widget ayarları
+ Widget’ı kaldır
+ Klasöre ekle
+ Yeni klasör
+ Klasörden çıkar
+ Uygulama bilgileri
+ Kaldır
+ Yeniden adlandır
+ Grubu çöz
+ Klasör
+ Klasör adı
+ Göster
+ Tüm uygulamalar
+ Oyunlar
+ İndirilenler
+ Sistem
+ Gizli
+ Gizle
+ Uygulamalarda göster
+ Kısayolu kaldır
+ Uygulamalara ekle
+ Ekle
+ Gizli uygulama yok.
+ Ad A–Z
+ Ad Z–A
+ Son yüklenenler
+ Son güncellenenler
+
+ - %d uygulama
+ - %d uygulama
+
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index 050fd0fdc..3084c36ba 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -3153,4 +3153,48 @@
Колекції
Хмара
Налаштування хмари
+ Пошук застосунків
+ Немає застосунків за запитом «%1$s».
+ Додати віджет
+ Завантаження віджетів…
+ На цьому пристрої немає застосунків із віджетами.
+ Ширше
+ Вужче
+ Вище
+ Нижче
+ Перемістити раніше
+ Перемістити пізніше
+ Налаштування віджета
+ Вилучити віджет
+ Додати до папки
+ Нова папка
+ Вилучити з папки
+ Про застосунок
+ Видалити
+ Перейменувати
+ Розгрупувати
+ Папка
+ Назва папки
+ Показати
+ Усі застосунки
+ Ігри
+ Завантажені
+ Системні
+ Приховані
+ Приховати
+ Показувати в застосунках
+ Вилучити ярлик
+ Додати до застосунків
+ Додати
+ Прихованих застосунків немає.
+ Назва А–Я
+ Назва Я–А
+ Нещодавно встановлені
+ Нещодавно оновлені
+
+ - %d застосунок
+ - %d застосунки
+ - %d застосунків
+ - %d застосунку
+
diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml
index 9c75292d7..0cfe20866 100644
--- a/app/src/main/res/values-vi/strings.xml
+++ b/app/src/main/res/values-vi/strings.xml
@@ -3060,4 +3060,45 @@
Bộ sưu tập
Đám mây
Cài đặt đám mây
+ Tìm ứng dụng
+ Không có ứng dụng nào khớp với “%1$s”.
+ Thêm tiện ích
+ Đang tải tiện ích…
+ Không có ứng dụng nào trên thiết bị này cung cấp tiện ích.
+ Rộng hơn
+ Hẹp hơn
+ Cao hơn
+ Thấp hơn
+ Chuyển lên trước
+ Chuyển ra sau
+ Cài đặt tiện ích
+ Xóa tiện ích
+ Thêm vào thư mục
+ Thư mục mới
+ Xóa khỏi thư mục
+ Thông tin ứng dụng
+ Gỡ cài đặt
+ Đổi tên
+ Bỏ nhóm
+ Thư mục
+ Tên thư mục
+ Hiển thị
+ Tất cả ứng dụng
+ Trò chơi
+ Đã tải xuống
+ Hệ thống
+ Đã ẩn
+ Ẩn
+ Hiện trong ứng dụng
+ Xóa lối tắt
+ Thêm vào Ứng dụng
+ Thêm
+ Không có ứng dụng nào bị ẩn.
+ Tên A–Z
+ Tên Z–A
+ Mới cài đặt
+ Mới cập nhật
+
+ - %d ứng dụng
+
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index ade2de5de..416839104 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -3064,4 +3064,45 @@
收藏集
云
云设置
+ 搜索应用
+ 没有与“%1$s”匹配的应用。
+ 添加微件
+ 正在加载微件…
+ 此设备上没有提供微件的应用。
+ 加宽
+ 缩窄
+ 加高
+ 降低
+ 前移
+ 后移
+ 微件设置
+ 移除微件
+ 添加到文件夹
+ 新建文件夹
+ 从文件夹中移除
+ 应用信息
+ 卸载
+ 重命名
+ 取消分组
+ 文件夹
+ 文件夹名称
+ 显示
+ 所有应用
+ 游戏
+ 已下载
+ 系统
+ 已隐藏
+ 隐藏
+ 在应用中显示
+ 移除快捷方式
+ 添加到应用
+ 添加
+ 没有隐藏的应用。
+ 名称 A–Z
+ 名称 Z–A
+ 最近安装
+ 最近更新
+
+ - %d 个应用
+
diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml
index 94ed816a9..ee0daff39 100644
--- a/app/src/main/res/values-zh-rHK/strings.xml
+++ b/app/src/main/res/values-zh-rHK/strings.xml
@@ -3063,4 +3063,45 @@
收藏集
雲端
雲端設定
+ 搜尋應用程式
+ 沒有符合「%1$s」的應用程式。
+ 新增小工具
+ 正在載入小工具…
+ 此裝置上沒有提供小工具的應用程式。
+ 加闊
+ 收窄
+ 加高
+ 降低
+ 向前移
+ 向後移
+ 小工具設定
+ 移除小工具
+ 加入資料夾
+ 新資料夾
+ 從資料夾移除
+ 應用程式資料
+ 解除安裝
+ 重新命名
+ 取消組合
+ 資料夾
+ 資料夾名稱
+ 顯示
+ 所有應用程式
+ 遊戲
+ 已下載
+ 系統
+ 已隱藏
+ 隱藏
+ 在應用程式中顯示
+ 移除捷徑
+ 加入應用程式
+ 新增
+ 沒有隱藏的應用程式。
+ 名稱 A–Z
+ 名稱 Z–A
+ 最近安裝
+ 最近更新
+
+ - %d 個應用程式
+
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 0c3257ead..27effeacb 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -3063,4 +3063,45 @@
收藏集
雲端
雲端設定
+ 搜尋應用程式
+ 沒有符合「%1$s」的應用程式。
+ 新增小工具
+ 正在載入小工具…
+ 這部裝置上沒有提供小工具的應用程式。
+ 加寬
+ 縮窄
+ 加高
+ 降低
+ 往前移
+ 往後移
+ 小工具設定
+ 移除小工具
+ 新增至資料夾
+ 新資料夾
+ 從資料夾移除
+ 應用程式資訊
+ 解除安裝
+ 重新命名
+ 取消群組
+ 資料夾
+ 資料夾名稱
+ 顯示
+ 所有應用程式
+ 遊戲
+ 已下載
+ 系統
+ 已隱藏
+ 隱藏
+ 在應用程式中顯示
+ 移除捷徑
+ 新增至應用程式
+ 新增
+ 沒有隱藏的應用程式。
+ 名稱 A–Z
+ 名稱 Z–A
+ 最近安裝
+ 最近更新
+
+ - %d 個應用程式
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 955caad10..8979e00b6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1081,6 +1081,48 @@
Android apps
%1$d apps
No launchable Android apps found.
+ Search apps
+ No apps match “%1$s”.
+ Add widget
+ Loading widgets…
+ No apps on this device offer widgets.
+ Wider
+ Narrower
+ Taller
+ Shorter
+ Move earlier
+ Move later
+ Widget settings
+ Remove widget
+ Add to folder
+ New folder
+ Remove from folder
+ App info
+ Uninstall
+ Rename
+ Ungroup
+ Folder
+ Folder name
+ Show
+ All apps
+ Games
+ Downloaded
+ System
+ Hidden
+ Hide
+ Show in apps
+ Remove shortcut
+ Add to Apps
+ Add
+ No hidden apps.
+
+ - %d app
+ - %d apps
+
+ Name A–Z
+ Name Z–A
+ Recently installed
+ Recently updated
● Desktop installed
Installs on first open
Available to install