mirror of
https://github.com/sartoopjj/thefeed.git
synced 2026-05-19 06:44:35 +03:00
feat: add app customization features including dynamic icon and name changes
This commit is contained in:
@@ -27,12 +27,23 @@
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask">
|
||||
android:launchMode="singleTask" />
|
||||
|
||||
<!-- Separate alias for launcher entry so it can be disabled
|
||||
when the user applies a custom icon/name shortcut -->
|
||||
<activity-alias
|
||||
android:name=".DefaultLauncher"
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</activity-alias>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
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
|
||||
@@ -26,31 +28,30 @@ class AndroidBridge(private val activity: Activity) {
|
||||
fun isAndroid(): Boolean = true
|
||||
|
||||
/**
|
||||
* Create a pinned shortcut on the home screen with a custom name and icon.
|
||||
* @param name The label shown under the shortcut
|
||||
* @param iconBase64 Base64-encoded image data (may include data:image/...;base64, prefix)
|
||||
* 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).
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun createAppShortcut(name: String, iconBase64: String): Boolean {
|
||||
fun setAppIdentity(name: String, iconBase64: String): Boolean {
|
||||
return try {
|
||||
val raw = if (iconBase64.contains(",")) {
|
||||
iconBase64.substringAfter(",")
|
||||
} else {
|
||||
iconBase64
|
||||
}
|
||||
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
|
||||
|
||||
// Save icon to internal storage for persistence
|
||||
val iconFile = File(activity.filesDir, "custom_shortcut_icon.png")
|
||||
val iconFile = File(activity.filesDir, "custom_icon.png")
|
||||
FileOutputStream(iconFile).use { out ->
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||||
}
|
||||
prefs.edit()
|
||||
.putString(PREF_CUSTOM_APP_NAME, name)
|
||||
.putString(PREF_CUSTOM_ICON_PATH, iconFile.absolutePath)
|
||||
.apply()
|
||||
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)
|
||||
@@ -63,25 +64,67 @@ class AndroidBridge(private val activity: Activity) {
|
||||
)
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the custom display name, or empty string if using defaults. */
|
||||
@JavascriptInterface
|
||||
fun getCustomAppName(): String {
|
||||
return prefs.getString(PREF_CUSTOM_APP_NAME, "") ?: ""
|
||||
}
|
||||
|
||||
/** Returns the display name for the app — custom name if set, otherwise "thefeed". */
|
||||
@JavascriptInterface
|
||||
fun resetAppShortcut() {
|
||||
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_shortcut_icon.png")
|
||||
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
|
||||
fun setLang(lang: String) {
|
||||
prefs.edit().putString(PREF_LANG, lang).apply()
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun getLang(): String {
|
||||
return prefs.getString(PREF_LANG, "fa") ?: "fa"
|
||||
}
|
||||
|
||||
// ===== Password =====
|
||||
@@ -108,7 +151,7 @@ class AndroidBridge(private val activity: Activity) {
|
||||
|
||||
@JavascriptInterface
|
||||
fun checkPassword(password: String): Boolean {
|
||||
val stored = prefs.getString(PREF_PASSWORD_HASH, null) ?: return true // no password set
|
||||
val stored = prefs.getString(PREF_PASSWORD_HASH, null) ?: return true
|
||||
return sha256(password) == stored
|
||||
}
|
||||
|
||||
@@ -122,5 +165,6 @@ class AndroidBridge(private val activity: Activity) {
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,15 +102,32 @@ class MainActivity : ComponentActivity() {
|
||||
private fun showLockScreen() {
|
||||
lockScreenVisible = true
|
||||
val lockOverlay = findViewById<LinearLayout>(R.id.lockOverlay)
|
||||
val lockTitle = findViewById<TextView>(R.id.lockTitle)
|
||||
val lockSubtitle = findViewById<TextView>(R.id.lockSubtitle)
|
||||
val lockInput = findViewById<EditText>(R.id.lockPasswordInput)
|
||||
val lockBtn = findViewById<Button>(R.id.lockUnlockBtn)
|
||||
val lockError = findViewById<TextView>(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"
|
||||
|
||||
lockTitle.text = appName
|
||||
lockSubtitle.text = if (isPersian) "رمز عبور را وارد کنید" else "Enter password to unlock"
|
||||
lockInput.hint = if (isPersian) "رمز عبور" else "Password"
|
||||
lockBtn.text = if (isPersian) "ورود" else "Unlock"
|
||||
if (isPersian) {
|
||||
lockOverlay.layoutDirection = View.LAYOUT_DIRECTION_RTL
|
||||
}
|
||||
|
||||
lockOverlay.visibility = View.VISIBLE
|
||||
webView.visibility = View.GONE
|
||||
txtStatus.visibility = View.GONE
|
||||
|
||||
val bridge = AndroidBridge(this)
|
||||
val wrongPwText = if (isPersian) "رمز عبور اشتباه است" else "Wrong password"
|
||||
|
||||
fun tryUnlock() {
|
||||
val pw = lockInput.text.toString()
|
||||
@@ -122,7 +139,7 @@ class MainActivity : ComponentActivity() {
|
||||
lockError.visibility = View.GONE
|
||||
waitForServerThenLoad()
|
||||
} else {
|
||||
lockError.text = "Wrong password"
|
||||
lockError.text = wrongPwText
|
||||
lockError.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,18 +37,18 @@
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/lockTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/app_name"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="28sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/lockSubtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/enter_password"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="14sp"
|
||||
android:layout_marginBottom="24dp" />
|
||||
@@ -57,7 +57,6 @@
|
||||
android:id="@+id/lockPasswordInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:hint="@string/password_hint"
|
||||
android:inputType="textPassword"
|
||||
android:imeOptions="actionDone"
|
||||
android:background="@color/bgPanel"
|
||||
@@ -80,7 +79,6 @@
|
||||
android:id="@+id/lockUnlockBtn"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:text="@string/unlock"
|
||||
android:background="@color/accent"
|
||||
android:textColor="@color/bg"
|
||||
android:textAllCaps="false"
|
||||
|
||||
@@ -2041,6 +2041,27 @@
|
||||
dns_timeout: 'تایماوت DNS (ثانیه)',
|
||||
auto_scan: 'بررسی خودکار ریزالورها (هر ساعت)',
|
||||
scanner_clear_targets: '\uD83D\uDDD1 پاک کردن',
|
||||
app_customization: 'شخصی\u200cسازی برنامه',
|
||||
app_name_label: 'نام برنامه',
|
||||
app_icon_label: 'آیکون برنامه',
|
||||
app_apply: 'اعمال',
|
||||
app_reset: 'بازنشانی',
|
||||
app_identity_applied: 'نام و آیکون برنامه تغییر کرد!',
|
||||
app_identity_failed: 'خطا در تغییر آیکون',
|
||||
app_identity_name_only: 'نام برنامه ذخیره شد!',
|
||||
app_reset_done: 'بازنشانی شد!',
|
||||
app_password: 'رمز عبور برنامه',
|
||||
password_new_ph: 'رمز عبور جدید',
|
||||
password_confirm_ph: 'تکرار رمز عبور',
|
||||
password_current_ph: 'رمز عبور فعلی',
|
||||
password_set: 'تنظیم رمز',
|
||||
password_remove: 'حذف رمز',
|
||||
password_active: 'رمز عبور فعال است',
|
||||
password_set_ok: 'رمز عبور تنظیم شد!',
|
||||
password_removed: 'رمز عبور حذف شد!',
|
||||
password_mismatch: 'رمزها مطابقت ندارند',
|
||||
password_wrong: 'رمز عبور اشتباه است',
|
||||
password_empty: 'رمز عبور نمی\u200cتواند خالی باشد',
|
||||
},
|
||||
en: {
|
||||
search: 'Search...', settings: 'Settings', profiles: 'Profiles',
|
||||
@@ -2178,8 +2199,9 @@
|
||||
app_icon_label: 'App Icon',
|
||||
app_apply: 'Apply',
|
||||
app_reset: 'Reset',
|
||||
app_shortcut_created: 'Shortcut created!',
|
||||
app_shortcut_failed: 'Failed to create shortcut',
|
||||
app_identity_applied: 'App icon and name changed!',
|
||||
app_identity_failed: 'Failed to change app icon',
|
||||
app_identity_name_only: 'App name saved!',
|
||||
app_reset_done: 'Reset done!',
|
||||
app_password: 'App Password',
|
||||
password_new_ph: 'New password',
|
||||
@@ -2215,6 +2237,7 @@
|
||||
lang = l;
|
||||
localStorage.setItem('thefeed_lang', l);
|
||||
applyLang();
|
||||
if (typeof Android !== 'undefined') try { Android.setLang(l) } catch (e) { }
|
||||
fetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ lang: l }) }).catch(function () { });
|
||||
}
|
||||
|
||||
@@ -2452,17 +2475,24 @@
|
||||
function applyAppCustomization() {
|
||||
if (typeof Android === 'undefined') return;
|
||||
var name = document.getElementById('customAppName').value.trim() || 'thefeed';
|
||||
if (!_customIconData) { showToast(t('app_icon_label')); return }
|
||||
try {
|
||||
var ok = Android.createAppShortcut(name, _customIconData);
|
||||
showToast(ok ? t('app_shortcut_created') : t('app_shortcut_failed'));
|
||||
} catch (e) { showToast(t('app_shortcut_failed')) }
|
||||
if (_customIconData) {
|
||||
try {
|
||||
var ok = Android.setAppIdentity(name, _customIconData);
|
||||
showToast(ok ? t('app_identity_applied') : t('app_identity_failed'));
|
||||
} catch (e) { showToast(t('app_identity_failed')) }
|
||||
} else {
|
||||
// Name-only change (no custom icon)
|
||||
try {
|
||||
Android.setAppIdentity(name, '');
|
||||
} catch (e) { }
|
||||
showToast(t('app_identity_name_only'));
|
||||
}
|
||||
}
|
||||
|
||||
function resetAppCustomization() {
|
||||
if (typeof Android === 'undefined') return;
|
||||
try {
|
||||
Android.resetAppShortcut();
|
||||
Android.resetAppIdentity();
|
||||
document.getElementById('customAppName').value = '';
|
||||
document.getElementById('customIconPreview').style.display = 'none';
|
||||
document.getElementById('customIconInput').value = '';
|
||||
|
||||
Reference in New Issue
Block a user