diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 2078b63..bea7084 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -27,23 +27,13 @@ - - - + android:launchMode="singleTask" + android:windowSoftInputMode="adjustResize"> - + diff --git a/android/app/src/main/java/com/thefeed/android/AndroidBridge.kt b/android/app/src/main/java/com/thefeed/android/AndroidBridge.kt index 1ad8567..6be3eac 100644 --- a/android/app/src/main/java/com/thefeed/android/AndroidBridge.kt +++ b/android/app/src/main/java/com/thefeed/android/AndroidBridge.kt @@ -1,19 +1,8 @@ package com.thefeed.android import android.app.Activity -import android.content.ComponentName import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.util.Base64 import android.webkit.JavascriptInterface -import androidx.core.content.pm.ShortcutInfoCompat -import androidx.core.content.pm.ShortcutManagerCompat -import androidx.core.graphics.drawable.IconCompat -import java.io.File -import java.io.FileOutputStream import java.security.MessageDigest class AndroidBridge(private val activity: Activity) { @@ -27,94 +16,25 @@ class AndroidBridge(private val activity: Activity) { @JavascriptInterface fun isAndroid(): Boolean = true - /** - * Change the app's launcher icon and name. - * If iconBase64 is non-empty, disables the default launcher alias and - * pins a new shortcut with the custom image — the result looks like - * the app itself changed. - * If iconBase64 is empty, only saves the custom name (used on lock screen). - */ + /** Set a preset display name for the app (shown on lock screen). */ @JavascriptInterface - fun setAppIdentity(name: String, iconBase64: String): Boolean { - return try { - prefs.edit().putString(PREF_CUSTOM_APP_NAME, name).apply() - - if (iconBase64.isBlank()) return true // name-only change - - val raw = if (iconBase64.contains(",")) iconBase64.substringAfter(",") else iconBase64 - val bytes = Base64.decode(raw, Base64.DEFAULT) - val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: return false - - val iconFile = File(activity.filesDir, "custom_icon.png") - FileOutputStream(iconFile).use { out -> - bitmap.compress(Bitmap.CompressFormat.PNG, 100, out) - } - prefs.edit().putString(PREF_CUSTOM_ICON_PATH, iconFile.absolutePath).apply() - - // Create pinned shortcut with custom icon/name - val icon = IconCompat.createWithBitmap(bitmap) - val shortcut = ShortcutInfoCompat.Builder(activity, "custom_launcher") - .setShortLabel(name) - .setLongLabel(name) - .setIcon(icon) - .setIntent( - Intent(activity, MainActivity::class.java).apply { - action = Intent.ACTION_MAIN - } - ) - .build() - ShortcutManagerCompat.requestPinShortcut(activity, shortcut, null) - - // Hide original launcher icon - activity.packageManager.setComponentEnabledSetting( - ComponentName(activity, "${activity.packageName}.DefaultLauncher"), - PackageManager.COMPONENT_ENABLED_STATE_DISABLED, - PackageManager.DONT_KILL_APP - ) - true - } catch (_: Exception) { - false - } + fun setPresetName(name: String) { + prefs.edit().putString(PREF_CUSTOM_APP_NAME, name).apply() } - /** Returns the custom display name, or empty string if using defaults. */ + /** Returns the preset display name, or empty string if using defaults. */ @JavascriptInterface - fun getCustomAppName(): String { + fun getPresetName(): String { return prefs.getString(PREF_CUSTOM_APP_NAME, "") ?: "" } - /** Returns the display name for the app — custom name if set, otherwise "thefeed". */ + /** Returns the display name for the app — preset name if set, otherwise "thefeed". */ @JavascriptInterface fun getAppDisplayName(): String { val custom = prefs.getString(PREF_CUSTOM_APP_NAME, null) return if (!custom.isNullOrBlank()) custom else "thefeed" } - /** Restore original app icon and name. */ - @JavascriptInterface - fun resetAppIdentity() { - prefs.edit() - .remove(PREF_CUSTOM_APP_NAME) - .remove(PREF_CUSTOM_ICON_PATH) - .apply() - val iconFile = File(activity.filesDir, "custom_icon.png") - if (iconFile.exists()) iconFile.delete() - - // Restore original launcher icon - try { - activity.packageManager.setComponentEnabledSetting( - ComponentName(activity, "${activity.packageName}.DefaultLauncher"), - PackageManager.COMPONENT_ENABLED_STATE_ENABLED, - PackageManager.DONT_KILL_APP - ) - } catch (_: Exception) { } - - // Remove dynamic shortcut - try { - ShortcutManagerCompat.removeDynamicShortcuts(activity, listOf("custom_launcher")) - } catch (_: Exception) { } - } - // ===== Language ===== @JavascriptInterface @@ -163,7 +83,6 @@ class AndroidBridge(private val activity: Activity) { companion object { const val PREF_CUSTOM_APP_NAME = "custom_app_name" - const val PREF_CUSTOM_ICON_PATH = "custom_icon_path" const val PREF_PASSWORD_HASH = "password_hash" const val PREF_LANG = "app_lang" } diff --git a/android/app/src/main/java/com/thefeed/android/MainActivity.kt b/android/app/src/main/java/com/thefeed/android/MainActivity.kt index 580e2bf..8f5b1d8 100644 --- a/android/app/src/main/java/com/thefeed/android/MainActivity.kt +++ b/android/app/src/main/java/com/thefeed/android/MainActivity.kt @@ -67,11 +67,12 @@ class MainActivity : ComponentActivity() { controller.isAppearanceLightNavigationBars = false setContentView(R.layout.activity_main) - // Apply top inset as padding so content isn't hidden behind the status bar + // Apply insets so content isn't hidden behind the status bar or keyboard val rootView = findViewById(android.R.id.content) ViewCompat.setOnApplyWindowInsetsListener(rootView) { v, insets -> val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) - v.setPadding(0, systemBars.top, 0, systemBars.bottom) + val ime = insets.getInsets(WindowInsetsCompat.Type.ime()) + v.setPadding(0, systemBars.top, 0, maxOf(systemBars.bottom, ime.bottom)) insets } // Trigger inset dispatch explicitly — required on some older Android versions @@ -98,6 +99,23 @@ class MainActivity : ComponentActivity() { return prefs.getString(AndroidBridge.PREF_PASSWORD_HASH, null) != null } + private fun resolvePresetName(key: String?, isPersian: Boolean): String { + if (key == null) return getString(R.string.app_name) + val presets = mapOf( + "weather" to ("Weather" to "آب و هوا"), + "calculator" to ("Calculator" to "ماشین‌حساب"), + "calendar" to ("Calendar" to "تقویم"), + "notes" to ("Notes" to "یادداشت"), + "clock" to ("Clock" to "ساعت"), + "camera" to ("Camera" to "دوربین"), + "compass" to ("Compass" to "قطب‌نما"), + "gallery" to ("Gallery" to "گالری"), + "recorder" to ("Recorder" to "ضبط صدا"), + ) + val pair = presets[key] ?: return key + return if (isPersian) pair.second else pair.first + } + @SuppressLint("SetTextI18n") private fun showLockScreen() { lockScreenVisible = true @@ -109,10 +127,11 @@ class MainActivity : ComponentActivity() { val lockError = findViewById(R.id.lockError) val prefs = getSharedPreferences(ThefeedService.PREFS_NAME, Context.MODE_PRIVATE) - val appName = prefs.getString(AndroidBridge.PREF_CUSTOM_APP_NAME, null) - ?.takeIf { it.isNotBlank() } ?: getString(R.string.app_name) val lang = prefs.getString(AndroidBridge.PREF_LANG, "fa") ?: "fa" val isPersian = lang == "fa" + val presetKey = prefs.getString(AndroidBridge.PREF_CUSTOM_APP_NAME, null) + ?.takeIf { it.isNotBlank() } + val appName = resolvePresetName(presetKey, isPersian) lockTitle.text = appName lockSubtitle.text = if (isPersian) "رمز عبور را وارد کنید" else "Enter password to unlock" diff --git a/internal/web/static/index.html b/internal/web/static/index.html index bc11bab..ca783fe 100644 --- a/internal/web/static/index.html +++ b/internal/web/static/index.html @@ -768,6 +768,119 @@ background: var(--hover) } + /* ===== SETTINGS SECTIONS ===== */ + .settings-section { + margin-top: 14px; + padding-top: 12px; + border-top: 1px solid var(--border) + } + + .settings-section-title { + font-size: 13px; + font-weight: 600; + color: var(--text); + margin-bottom: 8px + } + + .settings-info-row { + font-size: 11px; + color: var(--text-dim); + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 10px + } + + .settings-info-row span:last-child { + font-family: monospace; + color: var(--text) + } + + /* Preset name grid */ + .preset-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 6px; + margin-top: 6px + } + + .preset-btn { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + padding: 12px 4px 8px; + border: 1px solid var(--border); + border-radius: 12px; + background: var(--bg); + color: var(--text-dim); + font-size: 11px; + cursor: pointer; + transition: all .15s; + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis + } + + .preset-btn svg { + width: 32px; + height: 32px; + flex-shrink: 0 + } + + .preset-btn:hover { + background: var(--hover) + } + + .preset-btn.active-preset { + border-color: var(--accent); + background: rgba(51, 144, 236, .12); + color: var(--accent); + font-weight: 600 + } + + /* Experimental badge */ + .experimental-badge { + display: inline-block; + font-size: 10px; + padding: 2px 8px; + border-radius: 4px; + background: rgba(241, 196, 15, .15); + color: #f1c40f; + border: 1px solid rgba(241, 196, 15, .3); + margin-inline-start: 8px; + vertical-align: middle; + font-weight: 600 + } + + /* Password status */ + .password-status { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 12px; + border-radius: 8px; + background: var(--bg); + border: 1px solid var(--border); + margin-top: 8px + } + + .password-status-left { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: var(--text) + } + + .password-status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--accent) + } + .theme-btn.active-theme { border-color: var(--accent); background: rgba(51, 144, 236, .12); @@ -1558,57 +1671,42 @@ -
- Version - - -
-
- Latest Version - - -
-
- Check for Updates - -
-
- Clear Cache - +
+
+ Version + - +
+
+ Latest Version + - +
+
+ Check for Updates + +
+
+ Clear Cache + +
- +
- -