mirror of
https://github.com/sartoopjj/thefeed.git
synced 2026-05-18 06:44:34 +03:00
feat: implement app name presets and enhance password management UI
This commit is contained in:
@@ -27,23 +27,13 @@
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
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">
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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<View>(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<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"
|
||||
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"
|
||||
|
||||
+277
-119
@@ -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 @@
|
||||
<button class="theme-btn" id="themeLight" onclick="setTheme('light')" data-i18n="theme_light">Light</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style="margin-top:14px;padding-top:12px;border-top:1px solid var(--border);font-size:11px;color:var(--text-dim);display:flex;align-items:center;justify-content:space-between">
|
||||
<span data-i18n="version">Version</span>
|
||||
<span id="appVersionEl" style="font-family:monospace;color:var(--text)">-</span>
|
||||
</div>
|
||||
<div
|
||||
style="margin-top:10px;font-size:11px;color:var(--text-dim);display:flex;align-items:center;justify-content:space-between">
|
||||
<span data-i18n="latest_version">Latest Version</span>
|
||||
<span id="latestVersionEl" style="font-family:monospace;color:var(--text)">-</span>
|
||||
</div>
|
||||
<div style="margin-top:10px;display:flex;align-items:center;justify-content:space-between">
|
||||
<span style="font-size:12px;color:var(--text-dim)" data-i18n="check_latest_version">Check for Updates</span>
|
||||
<button class="btn btn-outline" id="checkVersionBtn" onclick="checkLatestVersion()"
|
||||
style="font-size:11px;padding:4px 12px" data-i18n="check_now">Check Now</button>
|
||||
</div>
|
||||
<div style="margin-top:10px;display:flex;align-items:center;justify-content:space-between">
|
||||
<span style="font-size:12px;color:var(--text-dim)" data-i18n="clear_cache">Clear Cache</span>
|
||||
<button class="btn btn-flat" onclick="clearCache()"
|
||||
style="font-size:11px;padding:4px 12px;color:var(--danger,#e74c3c)" data-i18n="clear_cache">Clear
|
||||
Cache</button>
|
||||
<div class="settings-section">
|
||||
<div class="settings-info-row" style="margin-top:0">
|
||||
<span data-i18n="version">Version</span>
|
||||
<span id="appVersionEl">-</span>
|
||||
</div>
|
||||
<div class="settings-info-row">
|
||||
<span data-i18n="latest_version">Latest Version</span>
|
||||
<span id="latestVersionEl">-</span>
|
||||
</div>
|
||||
<div class="settings-info-row">
|
||||
<span data-i18n="check_latest_version">Check for Updates</span>
|
||||
<button class="btn btn-outline btn-sm" id="checkVersionBtn" onclick="checkLatestVersion()"
|
||||
data-i18n="check_now">Check Now</button>
|
||||
</div>
|
||||
<div class="settings-info-row">
|
||||
<span data-i18n="clear_cache">Clear Cache</span>
|
||||
<button class="btn btn-flat btn-sm" onclick="clearCache()"
|
||||
style="color:var(--danger,#e74c3c)" data-i18n="clear_cache">Clear Cache</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-top:12px">
|
||||
<label data-i18n="bg_image">Background Image</label>
|
||||
<div style="display:flex;gap:6px;align-items:center">
|
||||
<input type="file" id="bgImageInput" accept="image/*" style="flex:1;font-size:12px;color:var(--text)" onchange="applyBgImage()">
|
||||
<button class="btn btn-flat" onclick="clearBgImage()" style="font-size:11px;padding:4px 10px;color:var(--error)" data-i18n="clear_bg">Clear</button>
|
||||
<button class="btn btn-flat btn-sm" onclick="clearBgImage()" style="color:var(--error)" data-i18n="clear_bg">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Android-only: App Customization -->
|
||||
<div id="androidAppSection" style="display:none;margin-top:14px;padding-top:12px;border-top:1px solid var(--border)">
|
||||
<label style="font-size:13px;font-weight:600;color:var(--text)" data-i18n="app_customization">App Customization</label>
|
||||
<div style="margin-top:8px">
|
||||
<label style="font-size:12px;color:var(--text-dim)" data-i18n="app_name_label">App Name</label>
|
||||
<input type="text" id="customAppName" placeholder="thefeed"
|
||||
style="width:100%;margin-top:4px;padding:8px 12px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:6px;font-size:13px;box-sizing:border-box">
|
||||
</div>
|
||||
<div style="margin-top:8px">
|
||||
<label style="font-size:12px;color:var(--text-dim)" data-i18n="app_icon_label">App Icon</label>
|
||||
<div style="display:flex;gap:6px;align-items:center;margin-top:4px">
|
||||
<img id="customIconPreview" src="" style="width:40px;height:40px;border-radius:8px;border:1px solid var(--border);display:none;object-fit:cover">
|
||||
<input type="file" id="customIconInput" accept="image/*" style="flex:1;font-size:12px;color:var(--text)">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;margin-top:10px">
|
||||
<button class="btn btn-primary" onclick="applyAppCustomization()" style="font-size:12px;padding:6px 14px" data-i18n="app_apply">Apply</button>
|
||||
<button class="btn btn-flat" onclick="resetAppCustomization()" style="font-size:12px;padding:6px 14px;color:var(--error)" data-i18n="app_reset">Reset</button>
|
||||
</div>
|
||||
<!-- Android-only: App Name Preset -->
|
||||
<div id="androidAppSection" class="settings-section" style="display:none">
|
||||
<div class="settings-section-title"><span data-i18n="app_customization">App Customization</span><span class="experimental-badge" data-i18n="experimental">Experimental</span></div>
|
||||
<div style="font-size:12px;color:var(--text-dim);margin-bottom:6px" data-i18n="app_preset_hint">Choose a display name for the app (shown on lock screen)</div>
|
||||
<div class="preset-grid" id="presetNameGrid"></div>
|
||||
</div>
|
||||
<!-- Android-only: App Password -->
|
||||
<div id="androidPasswordSection" style="display:none;margin-top:14px;padding-top:12px;border-top:1px solid var(--border)">
|
||||
<label style="font-size:13px;font-weight:600;color:var(--text)" data-i18n="app_password">App Password</label>
|
||||
<div id="androidPasswordSection" class="settings-section" style="display:none">
|
||||
<div class="settings-section-title"><span data-i18n="app_password">App Password</span><span class="experimental-badge" data-i18n="experimental">Experimental</span></div>
|
||||
<div id="passwordSetSection" style="margin-top:8px">
|
||||
<input type="password" id="appPasswordInput" placeholder=""
|
||||
data-i18n-ph="password_new_ph"
|
||||
@@ -1617,18 +1715,28 @@
|
||||
data-i18n-ph="password_confirm_ph"
|
||||
style="width:100%;padding:8px 12px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:6px;font-size:13px;box-sizing:border-box">
|
||||
<div id="passwordError" style="color:var(--error);font-size:12px;margin-top:4px;display:none"></div>
|
||||
<div style="display:flex;gap:6px;margin-top:8px">
|
||||
<div style="margin-top:8px">
|
||||
<button class="btn btn-primary" onclick="setAppPassword()" style="font-size:12px;padding:6px 14px" data-i18n="password_set">Set Password</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="passwordRemoveSection" style="margin-top:8px;display:none">
|
||||
<div style="font-size:12px;color:var(--text-dim);margin-bottom:6px" data-i18n="password_active">Password is active</div>
|
||||
<input type="password" id="appPasswordCurrent" placeholder=""
|
||||
data-i18n-ph="password_current_ph"
|
||||
style="width:100%;padding:8px 12px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:6px;font-size:13px;box-sizing:border-box">
|
||||
<div id="passwordRemoveError" style="color:var(--error);font-size:12px;margin-top:4px;display:none"></div>
|
||||
<div style="display:flex;gap:6px;margin-top:8px">
|
||||
<button class="btn btn-flat" onclick="removeAppPassword()" style="font-size:12px;padding:6px 14px;color:var(--error)" data-i18n="password_remove">Remove Password</button>
|
||||
<div id="passwordRemoveSection" style="display:none">
|
||||
<div class="password-status">
|
||||
<div class="password-status-left">
|
||||
<span class="password-status-dot"></span>
|
||||
<span data-i18n="password_active">Password is active</span>
|
||||
</div>
|
||||
<button class="btn btn-flat btn-sm" onclick="showPasswordRemovePrompt()" style="color:var(--error)" data-i18n="password_change_remove">Change / Remove</button>
|
||||
</div>
|
||||
<div id="passwordRemovePrompt" style="display:none;margin-top:8px">
|
||||
<input type="password" id="appPasswordCurrent" placeholder=""
|
||||
data-i18n-ph="password_current_ph"
|
||||
style="width:100%;padding:8px 12px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:6px;font-size:13px;box-sizing:border-box">
|
||||
<div id="passwordRemoveError" style="color:var(--error);font-size:12px;margin-top:4px;display:none"></div>
|
||||
<div style="display:flex;gap:6px;margin-top:8px">
|
||||
<button class="btn btn-primary" onclick="changeAppPassword()" style="font-size:12px;padding:6px 14px" data-i18n="password_change">Change</button>
|
||||
<button class="btn btn-flat" onclick="removeAppPassword()" style="font-size:12px;padding:6px 14px;color:var(--error)" data-i18n="password_remove">Remove</button>
|
||||
<button class="btn btn-flat" onclick="hidePasswordRemovePrompt()" style="font-size:12px;padding:6px 14px" data-i18n="cancel">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2042,20 +2150,18 @@
|
||||
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_preset_hint: 'نام نمایشی برنامه را انتخاب کنید (در صفحه قفل نمایش داده میشود)',
|
||||
app_name_saved: 'نام برنامه ذخیره شد!',
|
||||
app_password: 'رمز عبور برنامه',
|
||||
password_new_ph: 'رمز عبور جدید',
|
||||
password_confirm_ph: 'تکرار رمز عبور',
|
||||
password_current_ph: 'رمز عبور فعلی',
|
||||
password_set: 'تنظیم رمز',
|
||||
password_change_remove: 'تغییر / حذف',
|
||||
password_change: 'تغییر رمز',
|
||||
password_remove: 'حذف رمز',
|
||||
password_remove_confirm: 'آیا مطمئن هستید که میخواهید رمز عبور را حذف کنید؟',
|
||||
password_enter_new: 'رمز عبور جدید را وارد کنید',
|
||||
password_active: 'رمز عبور فعال است',
|
||||
password_set_ok: 'رمز عبور تنظیم شد!',
|
||||
password_removed: 'رمز عبور حذف شد!',
|
||||
@@ -2195,20 +2301,19 @@
|
||||
auto_scan: 'Automatic hourly resolver check',
|
||||
scanner_clear_targets: '\uD83D\uDDD1 Clear',
|
||||
app_customization: 'App Customization',
|
||||
app_name_label: 'App Name',
|
||||
app_icon_label: 'App Icon',
|
||||
app_apply: 'Apply',
|
||||
app_reset: 'Reset',
|
||||
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!',
|
||||
experimental: 'Experimental',
|
||||
app_preset_hint: 'Choose a display name for the app (shown on lock screen)',
|
||||
app_name_saved: 'App name saved!',
|
||||
app_password: 'App Password',
|
||||
password_new_ph: 'New password',
|
||||
password_confirm_ph: 'Confirm password',
|
||||
password_current_ph: 'Current password',
|
||||
password_set: 'Set Password',
|
||||
password_remove: 'Remove Password',
|
||||
password_change_remove: 'Change / Remove',
|
||||
password_change: 'Change',
|
||||
password_remove: 'Remove',
|
||||
password_remove_confirm: 'Are you sure you want to remove the password?',
|
||||
password_enter_new: 'Enter your new password',
|
||||
password_active: 'Password is active',
|
||||
password_set_ok: 'Password set!',
|
||||
password_removed: 'Password removed!',
|
||||
@@ -2433,74 +2538,81 @@
|
||||
}
|
||||
|
||||
// ===== ANDROID SETTINGS =====
|
||||
var _presetIcons = {
|
||||
weather: '<svg viewBox="0 0 32 32" fill="none"><circle cx="16" cy="14" r="6" fill="#f39c12"/><path d="M16 4v3M16 21v3M6 14H3M29 14h-3M8.1 6.1l2.1 2.1M21.8 20.8l2.1 2.1M8.1 21.9l2.1-2.1M21.8 7.2l2.1-2.1" stroke="#f39c12" stroke-width="2" stroke-linecap="round"/><path d="M22 22c0-3.3 2.7-6 6-6a5 5 0 01-1 10H10a4 4 0 010-8c.3-2.8 2.6-5 5.5-5 2.4 0 4.4 1.5 5.2 3.6" stroke="#bdc3c7" stroke-width="1.5" fill="rgba(189,195,199,.2)"/></svg>',
|
||||
calculator: '<svg viewBox="0 0 32 32" fill="none"><rect x="6" y="3" width="20" height="26" rx="3" fill="#2c3e50" stroke="#7f8c8d" stroke-width="1"/><rect x="9" y="6" width="14" height="6" rx="1.5" fill="#1abc9c"/><circle cx="11.5" cy="16.5" r="1.5" fill="#ecf0f1"/><circle cx="16" cy="16.5" r="1.5" fill="#ecf0f1"/><circle cx="20.5" cy="16.5" r="1.5" fill="#ecf0f1"/><circle cx="11.5" cy="21" r="1.5" fill="#ecf0f1"/><circle cx="16" cy="21" r="1.5" fill="#ecf0f1"/><circle cx="20.5" cy="21" r="1.5" fill="#e74c3c"/><circle cx="11.5" cy="25.5" r="1.5" fill="#ecf0f1"/><rect x="14.5" y="24" width="7.5" height="3" rx="1.5" fill="#3498db"/></svg>',
|
||||
calendar: '<svg viewBox="0 0 32 32" fill="none"><rect x="4" y="6" width="24" height="22" rx="3" fill="#e74c3c"/><rect x="4" y="12" width="24" height="16" rx="0" fill="#ecf0f1"/><rect x="10" y="3" width="2" height="6" rx="1" fill="#7f8c8d"/><rect x="20" y="3" width="2" height="6" rx="1" fill="#7f8c8d"/><text x="16" y="24" text-anchor="middle" font-size="12" font-weight="bold" fill="#2c3e50">17</text></svg>',
|
||||
notes: '<svg viewBox="0 0 32 32" fill="none"><rect x="6" y="3" width="20" height="26" rx="2" fill="#f1c40f"/><line x1="10" y1="10" x2="22" y2="10" stroke="#7f6c00" stroke-width="1.5" stroke-linecap="round"/><line x1="10" y1="14" x2="22" y2="14" stroke="#7f6c00" stroke-width="1.5" stroke-linecap="round"/><line x1="10" y1="18" x2="18" y2="18" stroke="#7f6c00" stroke-width="1.5" stroke-linecap="round"/></svg>',
|
||||
clock: '<svg viewBox="0 0 32 32" fill="none"><circle cx="16" cy="16" r="13" fill="#2c3e50" stroke="#ecf0f1" stroke-width="1.5"/><circle cx="16" cy="16" r="11" fill="#1a1a2e"/><line x1="16" y1="16" x2="16" y2="8" stroke="#ecf0f1" stroke-width="2" stroke-linecap="round"/><line x1="16" y1="16" x2="22" y2="16" stroke="#e74c3c" stroke-width="1.5" stroke-linecap="round"/><circle cx="16" cy="16" r="1.5" fill="#ecf0f1"/></svg>',
|
||||
camera: '<svg viewBox="0 0 32 32" fill="none"><rect x="3" y="9" width="26" height="18" rx="3" fill="#2c3e50"/><path d="M11 9l1.5-4h7L21 9" fill="#34495e"/><circle cx="16" cy="18" r="5.5" stroke="#ecf0f1" stroke-width="2" fill="none"/><circle cx="16" cy="18" r="3" fill="#3498db"/><circle cx="24" cy="13" r="1.5" fill="#e74c3c"/></svg>',
|
||||
compass: '<svg viewBox="0 0 32 32" fill="none"><circle cx="16" cy="16" r="13" fill="#2c3e50" stroke="#7f8c8d" stroke-width="1"/><circle cx="16" cy="16" r="11" fill="#1a252f"/><polygon points="16,6 18,15 16,17 14,15" fill="#e74c3c"/><polygon points="16,26 14,17 16,15 18,17" fill="#ecf0f1"/><circle cx="16" cy="16" r="2" fill="#7f8c8d"/></svg>',
|
||||
gallery: '<svg viewBox="0 0 32 32" fill="none"><rect x="4" y="5" width="24" height="22" rx="3" fill="#2980b9"/><circle cx="12" cy="13" r="3" fill="#f1c40f"/><path d="M4 22l7-7 5 5 4-3 8 6v2a3 3 0 01-3 3H7a3 3 0 01-3-3v-3z" fill="#27ae60"/></svg>',
|
||||
recorder: '<svg viewBox="0 0 32 32" fill="none"><rect x="11" y="4" width="10" height="16" rx="5" fill="#e74c3c"/><path d="M8 18c0 4.4 3.6 8 8 8s8-3.6 8-8" stroke="#7f8c8d" stroke-width="2" fill="none" stroke-linecap="round"/><line x1="16" y1="26" x2="16" y2="30" stroke="#7f8c8d" stroke-width="2" stroke-linecap="round"/><line x1="12" y1="30" x2="20" y2="30" stroke="#7f8c8d" stroke-width="2" stroke-linecap="round"/></svg>',
|
||||
};
|
||||
var _presetNames = [
|
||||
{ key: 'thefeed', en: 'thefeed', fa: 'thefeed', icon: '' },
|
||||
{ key: 'weather', en: 'Weather', fa: 'آب و هوا', icon: 'weather' },
|
||||
{ key: 'calculator', en: 'Calculator', fa: 'ماشینحساب', icon: 'calculator' },
|
||||
{ key: 'calendar', en: 'Calendar', fa: 'تقویم', icon: 'calendar' },
|
||||
{ key: 'notes', en: 'Notes', fa: 'یادداشت', icon: 'notes' },
|
||||
{ key: 'clock', en: 'Clock', fa: 'ساعت', icon: 'clock' },
|
||||
{ key: 'camera', en: 'Camera', fa: 'دوربین', icon: 'camera' },
|
||||
{ key: 'compass', en: 'Compass', fa: 'قطبنما', icon: 'compass' },
|
||||
{ key: 'gallery', en: 'Gallery', fa: 'گالری', icon: 'gallery' },
|
||||
{ key: 'recorder', en: 'Recorder', fa: 'ضبط صدا', icon: 'recorder' },
|
||||
];
|
||||
|
||||
function initAndroidSettings() {
|
||||
if (typeof Android === 'undefined') return;
|
||||
// Show Android-only sections
|
||||
document.getElementById('androidAppSection').style.display = '';
|
||||
document.getElementById('androidPasswordSection').style.display = '';
|
||||
// Populate current custom name
|
||||
try {
|
||||
var name = Android.getCustomAppName();
|
||||
if (name) document.getElementById('customAppName').value = name;
|
||||
} catch (e) { }
|
||||
// Build preset name grid
|
||||
var grid = document.getElementById('presetNameGrid');
|
||||
var current = '';
|
||||
try { current = Android.getPresetName(); } catch (e) { }
|
||||
grid.innerHTML = '';
|
||||
_presetNames.forEach(function (p) {
|
||||
var btn = document.createElement('button');
|
||||
btn.className = 'preset-btn' + (current === p.key ? ' active-preset' : '');
|
||||
var iconHtml = p.icon && _presetIcons[p.icon] ? _presetIcons[p.icon] : '<svg viewBox="0 0 32 32" fill="none"><rect x="4" y="4" width="24" height="24" rx="6" fill="var(--accent)"/><text x="16" y="21" text-anchor="middle" font-size="14" font-weight="bold" fill="#fff">tf</text></svg>';
|
||||
btn.innerHTML = iconHtml + '<span>' + (currentLang === 'fa' ? p.fa : p.en) + '</span>';
|
||||
btn.onclick = function () { selectPresetName(p.key); };
|
||||
grid.appendChild(btn);
|
||||
});
|
||||
// Show correct password section
|
||||
try {
|
||||
var hasPw = Android.hasPassword();
|
||||
document.getElementById('passwordSetSection').style.display = hasPw ? 'none' : '';
|
||||
document.getElementById('passwordRemoveSection').style.display = hasPw ? '' : 'none';
|
||||
if (!hasPw) document.getElementById('passwordRemovePrompt').style.display = 'none';
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
var _customIconData = null;
|
||||
(function () {
|
||||
// Listen for icon file selection
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var inp = document.getElementById('customIconInput');
|
||||
if (!inp) return;
|
||||
inp.addEventListener('change', function () {
|
||||
var file = this.files && this.files[0];
|
||||
if (!file) return;
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
_customIconData = e.target.result;
|
||||
var preview = document.getElementById('customIconPreview');
|
||||
preview.src = _customIconData;
|
||||
preview.style.display = '';
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
function applyAppCustomization() {
|
||||
if (typeof Android === 'undefined') return;
|
||||
var name = document.getElementById('customAppName').value.trim() || 'thefeed';
|
||||
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() {
|
||||
function selectPresetName(key) {
|
||||
if (typeof Android === 'undefined') return;
|
||||
try {
|
||||
Android.resetAppIdentity();
|
||||
document.getElementById('customAppName').value = '';
|
||||
document.getElementById('customIconPreview').style.display = 'none';
|
||||
document.getElementById('customIconInput').value = '';
|
||||
_customIconData = null;
|
||||
showToast(t('app_reset_done'));
|
||||
Android.setPresetName(key === 'thefeed' ? '' : key);
|
||||
var btns = document.querySelectorAll('.preset-btn');
|
||||
var idx = _presetNames.findIndex(function (p) { return p.key === key; });
|
||||
btns.forEach(function (b, i) {
|
||||
b.classList.toggle('active-preset', i === idx);
|
||||
});
|
||||
var preset = _presetNames[idx];
|
||||
var displayName = preset ? (currentLang === 'fa' ? preset.fa : preset.en) : key;
|
||||
showToast(t('app_name_saved') + ' — ' + displayName);
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
function showPasswordRemovePrompt() {
|
||||
document.getElementById('passwordRemovePrompt').style.display = '';
|
||||
document.getElementById('appPasswordCurrent').value = '';
|
||||
document.getElementById('passwordRemoveError').style.display = 'none';
|
||||
}
|
||||
|
||||
function hidePasswordRemovePrompt() {
|
||||
document.getElementById('passwordRemovePrompt').style.display = 'none';
|
||||
}
|
||||
|
||||
function setAppPassword() {
|
||||
if (typeof Android === 'undefined') return;
|
||||
var pw = document.getElementById('appPasswordInput').value;
|
||||
@@ -2527,6 +2639,7 @@
|
||||
var errEl = document.getElementById('passwordRemoveError');
|
||||
errEl.style.display = 'none';
|
||||
if (!pw) { errEl.textContent = t('password_empty'); errEl.style.display = ''; return }
|
||||
if (!confirm(t('password_remove_confirm'))) return;
|
||||
try {
|
||||
var ok = Android.removePassword(pw);
|
||||
if (ok) {
|
||||
@@ -2534,6 +2647,7 @@
|
||||
document.getElementById('appPasswordCurrent').value = '';
|
||||
document.getElementById('passwordSetSection').style.display = '';
|
||||
document.getElementById('passwordRemoveSection').style.display = 'none';
|
||||
document.getElementById('passwordRemovePrompt').style.display = 'none';
|
||||
} else {
|
||||
errEl.textContent = t('password_wrong');
|
||||
errEl.style.display = '';
|
||||
@@ -2541,6 +2655,29 @@
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
function changeAppPassword() {
|
||||
if (typeof Android === 'undefined') return;
|
||||
var pw = document.getElementById('appPasswordCurrent').value;
|
||||
var errEl = document.getElementById('passwordRemoveError');
|
||||
errEl.style.display = 'none';
|
||||
if (!pw) { errEl.textContent = t('password_empty'); errEl.style.display = ''; return }
|
||||
try {
|
||||
var ok = Android.checkPassword(pw);
|
||||
if (!ok) {
|
||||
errEl.textContent = t('password_wrong');
|
||||
errEl.style.display = '';
|
||||
return;
|
||||
}
|
||||
// Remove old and show set section for new password
|
||||
Android.removePassword(pw);
|
||||
document.getElementById('appPasswordCurrent').value = '';
|
||||
document.getElementById('passwordRemoveSection').style.display = 'none';
|
||||
document.getElementById('passwordRemovePrompt').style.display = 'none';
|
||||
document.getElementById('passwordSetSection').style.display = '';
|
||||
showToast(t('password_enter_new'));
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
async function checkLatestVersion() {
|
||||
var btn = document.getElementById('checkVersionBtn');
|
||||
var prevText = btn ? btn.textContent : '';
|
||||
@@ -4215,6 +4352,27 @@
|
||||
});
|
||||
})();
|
||||
|
||||
// Close modals (bottom sheets) when clicking outside
|
||||
(function () {
|
||||
var modalMap = {
|
||||
settingsModal: function () { closeSettings() },
|
||||
profilesModal: function () { closeProfiles() },
|
||||
profileEditorModal: function () { closeProfileEditor && closeProfileEditor() },
|
||||
exportModal: function () { closeExportModal() },
|
||||
resolversModal: function () { closeResolversModal() },
|
||||
scannerModal: function () { closeScanner() },
|
||||
savedResolversModal: function () { savedResolversSkip && savedResolversSkip() },
|
||||
};
|
||||
document.addEventListener('click', function (e) {
|
||||
var overlay = e.target;
|
||||
if (!overlay.classList.contains('modal-overlay') || !overlay.classList.contains('active')) return;
|
||||
// Only close if user clicked directly on the overlay backdrop, not the modal content
|
||||
if (e.target !== overlay) return;
|
||||
var fn = modalMap[overlay.id];
|
||||
if (fn) fn();
|
||||
});
|
||||
})();
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user