mirror of
https://github.com/sartoopjj/thefeed.git
synced 2026-05-19 09:34:35 +03:00
feat: implement password protection and app customization features for Android
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
package com.thefeed.android
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
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) {
|
||||
|
||||
private val prefs by lazy {
|
||||
activity.getSharedPreferences(ThefeedService.PREFS_NAME, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
// ===== Identity =====
|
||||
|
||||
@JavascriptInterface
|
||||
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)
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun createAppShortcut(name: String, iconBase64: String): Boolean {
|
||||
return try {
|
||||
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")
|
||||
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()
|
||||
|
||||
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)
|
||||
true
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun getCustomAppName(): String {
|
||||
return prefs.getString(PREF_CUSTOM_APP_NAME, "") ?: ""
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun resetAppShortcut() {
|
||||
prefs.edit()
|
||||
.remove(PREF_CUSTOM_APP_NAME)
|
||||
.remove(PREF_CUSTOM_ICON_PATH)
|
||||
.apply()
|
||||
val iconFile = File(activity.filesDir, "custom_shortcut_icon.png")
|
||||
if (iconFile.exists()) iconFile.delete()
|
||||
}
|
||||
|
||||
// ===== Password =====
|
||||
|
||||
@JavascriptInterface
|
||||
fun hasPassword(): Boolean {
|
||||
return prefs.getString(PREF_PASSWORD_HASH, null) != null
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun setPassword(password: String): Boolean {
|
||||
if (password.isEmpty()) return false
|
||||
prefs.edit().putString(PREF_PASSWORD_HASH, sha256(password)).apply()
|
||||
return true
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun removePassword(currentPassword: String): Boolean {
|
||||
val stored = prefs.getString(PREF_PASSWORD_HASH, null) ?: return false
|
||||
if (sha256(currentPassword) != stored) return false
|
||||
prefs.edit().remove(PREF_PASSWORD_HASH).apply()
|
||||
return true
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun checkPassword(password: String): Boolean {
|
||||
val stored = prefs.getString(PREF_PASSWORD_HASH, null) ?: return true // no password set
|
||||
return sha256(password) == stored
|
||||
}
|
||||
|
||||
private fun sha256(input: String): String {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
val hash = digest.digest(input.toByteArray(Charsets.UTF_8))
|
||||
return hash.joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.thefeed.android
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
@@ -11,12 +12,17 @@ import android.os.Looper
|
||||
import android.os.PowerManager
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import android.text.InputType
|
||||
import android.webkit.WebResourceError
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.webkit.JsResult
|
||||
import android.webkit.WebChromeClient
|
||||
@@ -38,6 +44,7 @@ class MainActivity : ComponentActivity() {
|
||||
private lateinit var txtStatus: TextView
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private var fileChooserCallback: ValueCallback<Array<Uri>>? = null
|
||||
private var lockScreenVisible = false
|
||||
|
||||
private val fileChooserLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.GetContent()
|
||||
@@ -78,7 +85,55 @@ class MainActivity : ComponentActivity() {
|
||||
configureWebView()
|
||||
registerBackHandler()
|
||||
startThefeedService()
|
||||
waitForServerThenLoad()
|
||||
|
||||
if (isPasswordSet()) {
|
||||
showLockScreen()
|
||||
} else {
|
||||
waitForServerThenLoad()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPasswordSet(): Boolean {
|
||||
val prefs = getSharedPreferences(ThefeedService.PREFS_NAME, Context.MODE_PRIVATE)
|
||||
return prefs.getString(AndroidBridge.PREF_PASSWORD_HASH, null) != null
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun showLockScreen() {
|
||||
lockScreenVisible = true
|
||||
val lockOverlay = findViewById<LinearLayout>(R.id.lockOverlay)
|
||||
val lockInput = findViewById<EditText>(R.id.lockPasswordInput)
|
||||
val lockBtn = findViewById<Button>(R.id.lockUnlockBtn)
|
||||
val lockError = findViewById<TextView>(R.id.lockError)
|
||||
|
||||
lockOverlay.visibility = View.VISIBLE
|
||||
webView.visibility = View.GONE
|
||||
txtStatus.visibility = View.GONE
|
||||
|
||||
val bridge = AndroidBridge(this)
|
||||
|
||||
fun tryUnlock() {
|
||||
val pw = lockInput.text.toString()
|
||||
if (bridge.checkPassword(pw)) {
|
||||
lockOverlay.visibility = View.GONE
|
||||
webView.visibility = View.VISIBLE
|
||||
lockScreenVisible = false
|
||||
lockInput.text.clear()
|
||||
lockError.visibility = View.GONE
|
||||
waitForServerThenLoad()
|
||||
} else {
|
||||
lockError.text = "Wrong password"
|
||||
lockError.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
lockBtn.setOnClickListener { tryUnlock() }
|
||||
lockInput.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
tryUnlock()
|
||||
true
|
||||
} else false
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerBackHandler() {
|
||||
@@ -86,11 +141,13 @@ class MainActivity : ComponentActivity() {
|
||||
override fun handleOnBackPressed() {
|
||||
// Check if the chat view is open (mobile nav). If yes, go back
|
||||
// to the channel list. If already on the channel list, minimize.
|
||||
// Uses openSidebar() directly instead of webView.goBack() to avoid
|
||||
// history-stack mismatches that can leave the UI stuck mid-transition.
|
||||
webView.evaluateJavascript(
|
||||
"(document.getElementById('app').classList.contains('chat-open')).toString()"
|
||||
) { result ->
|
||||
if (result.trim('"') == "true") {
|
||||
webView.goBack()
|
||||
webView.evaluateJavascript("openSidebar(); history.back();", null)
|
||||
} else {
|
||||
moveTaskToBack(true)
|
||||
}
|
||||
@@ -159,6 +216,7 @@ class MainActivity : ComponentActivity() {
|
||||
txtStatus.visibility = if (msg.isEmpty()) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
private fun configureWebView() {
|
||||
webView.webViewClient = object : WebViewClient() {
|
||||
override fun shouldOverrideUrlLoading(
|
||||
@@ -228,6 +286,8 @@ class MainActivity : ComponentActivity() {
|
||||
allowContentAccess = false
|
||||
mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW
|
||||
}
|
||||
|
||||
webView.addJavascriptInterface(AndroidBridge(this), "Android")
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,23 +1,89 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="@color/bg">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtStatus"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="10dp"
|
||||
android:textColor="@color/text"
|
||||
android:visibility="gone"
|
||||
android:text="@string/webview_loading" />
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webView"
|
||||
<TextView
|
||||
android:id="@+id/txtStatus"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="10dp"
|
||||
android:textColor="@color/text"
|
||||
android:visibility="gone"
|
||||
android:text="@string/webview_loading" />
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Password lock screen overlay -->
|
||||
<LinearLayout
|
||||
android:id="@+id/lockOverlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:padding="48dp"
|
||||
android:background="@color/bg"
|
||||
android:visibility="gone">
|
||||
|
||||
</LinearLayout>
|
||||
<TextView
|
||||
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: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" />
|
||||
|
||||
<EditText
|
||||
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"
|
||||
android:textColor="@color/text"
|
||||
android:textColorHint="#66EDF2FF"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/lockError"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="#E74C3C"
|
||||
android:textSize="13sp"
|
||||
android:visibility="gone"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<Button
|
||||
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"
|
||||
android:textSize="16sp" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
@@ -4,4 +4,7 @@
|
||||
<string name="start_service">Start</string>
|
||||
<string name="stop_service">Stop</string>
|
||||
<string name="reload_webview">Reload</string>
|
||||
<string name="enter_password">Enter password to unlock</string>
|
||||
<string name="password_hint">Password</string>
|
||||
<string name="unlock">Unlock</string>
|
||||
</resources>
|
||||
|
||||
@@ -1586,6 +1586,52 @@
|
||||
<button class="btn btn-flat" onclick="clearBgImage()" style="font-size:11px;padding:4px 10px;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>
|
||||
</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="passwordSetSection" style="margin-top:8px">
|
||||
<input type="password" id="appPasswordInput" placeholder=""
|
||||
data-i18n-ph="password_new_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;margin-bottom:6px">
|
||||
<input type="password" id="appPasswordConfirm" placeholder=""
|
||||
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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-flat" onclick="closeSettings()" data-i18n="cancel">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="saveSettings()" data-i18n="save">Save</button>
|
||||
@@ -1900,7 +1946,7 @@
|
||||
copied: 'کپی شد!', copy: 'کپی', active: 'فعال',
|
||||
private: 'خصوصی', x_posts: 'پستهای X', x_label: 'X', no_config: 'ابتدا پروفایل را ذخیره کنید',
|
||||
refreshing: 'در حال بروزرسانی...', fetching_channel: 'در حال دریافت کانال...',
|
||||
msg_copied: 'پیام کپی شد!', rescan_started: 'بررسی مجدد شروع شد',
|
||||
msg_copied: 'پیام کپی شد!', rescan_started: 'بررسی مجدد شروع شد', no_new_messages: 'پیام جدیدی نیست',
|
||||
add_manual: '✎ ساخت دستی', rescan: 'بررسی مجدد',
|
||||
new_messages: 'پیام جدید', missed_messages: '{n} پیام از دست رفته یا حذف شده',
|
||||
clear_cache: 'پاک کردن کش', cache_cleared: 'کش پاک شد!',
|
||||
@@ -2032,7 +2078,7 @@
|
||||
copied: 'URI copied!', copy: 'Copy', active: 'Active',
|
||||
private: 'Private', x_posts: 'X Posts', x_label: 'X', no_config: 'Save a profile first',
|
||||
refreshing: 'Refreshing...', fetching_channel: 'Fetching channel...',
|
||||
msg_copied: 'Message copied!', rescan_started: 'Rescan started',
|
||||
msg_copied: 'Message copied!', rescan_started: 'Rescan started', no_new_messages: 'No new messages',
|
||||
add_manual: '✎ Create Manually', rescan: 'Rescan',
|
||||
new_messages: 'New messages', missed_messages: '{n} messages missed or deleted',
|
||||
clear_cache: 'Clear Cache', cache_cleared: 'Cache cleared!',
|
||||
@@ -2127,6 +2173,26 @@
|
||||
dns_timeout: 'DNS Query Timeout (s)',
|
||||
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_shortcut_created: 'Shortcut created!',
|
||||
app_shortcut_failed: 'Failed to create shortcut',
|
||||
app_reset_done: 'Reset done!',
|
||||
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_active: 'Password is active',
|
||||
password_set_ok: 'Password set!',
|
||||
password_removed: 'Password removed!',
|
||||
password_mismatch: 'Passwords do not match',
|
||||
password_wrong: 'Wrong password',
|
||||
password_empty: 'Password cannot be empty',
|
||||
}
|
||||
};
|
||||
var lang = localStorage.getItem('thefeed_lang') || 'fa';
|
||||
@@ -2332,6 +2398,7 @@
|
||||
function openSettings() {
|
||||
renderLatestVersion();
|
||||
applyThemeButtons();
|
||||
initAndroidSettings();
|
||||
document.getElementById('settingsModal').classList.add('active');
|
||||
}
|
||||
function closeSettings() { document.getElementById('settingsModal').classList.remove('active') }
|
||||
@@ -2341,6 +2408,109 @@
|
||||
try { await fetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fontSize: fs, debug: dbg }) }) } catch (e) { }
|
||||
closeSettings();
|
||||
}
|
||||
|
||||
// ===== ANDROID SETTINGS =====
|
||||
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) { }
|
||||
// Show correct password section
|
||||
try {
|
||||
var hasPw = Android.hasPassword();
|
||||
document.getElementById('passwordSetSection').style.display = hasPw ? 'none' : '';
|
||||
document.getElementById('passwordRemoveSection').style.display = hasPw ? '' : '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) { 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')) }
|
||||
}
|
||||
|
||||
function resetAppCustomization() {
|
||||
if (typeof Android === 'undefined') return;
|
||||
try {
|
||||
Android.resetAppShortcut();
|
||||
document.getElementById('customAppName').value = '';
|
||||
document.getElementById('customIconPreview').style.display = 'none';
|
||||
document.getElementById('customIconInput').value = '';
|
||||
_customIconData = null;
|
||||
showToast(t('app_reset_done'));
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
function setAppPassword() {
|
||||
if (typeof Android === 'undefined') return;
|
||||
var pw = document.getElementById('appPasswordInput').value;
|
||||
var confirm = document.getElementById('appPasswordConfirm').value;
|
||||
var errEl = document.getElementById('passwordError');
|
||||
errEl.style.display = 'none';
|
||||
if (!pw) { errEl.textContent = t('password_empty'); errEl.style.display = ''; return }
|
||||
if (pw !== confirm) { errEl.textContent = t('password_mismatch'); errEl.style.display = ''; return }
|
||||
try {
|
||||
var ok = Android.setPassword(pw);
|
||||
if (ok) {
|
||||
showToast(t('password_set_ok'));
|
||||
document.getElementById('appPasswordInput').value = '';
|
||||
document.getElementById('appPasswordConfirm').value = '';
|
||||
document.getElementById('passwordSetSection').style.display = 'none';
|
||||
document.getElementById('passwordRemoveSection').style.display = '';
|
||||
}
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
function removeAppPassword() {
|
||||
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.removePassword(pw);
|
||||
if (ok) {
|
||||
showToast(t('password_removed'));
|
||||
document.getElementById('appPasswordCurrent').value = '';
|
||||
document.getElementById('passwordSetSection').style.display = '';
|
||||
document.getElementById('passwordRemoveSection').style.display = 'none';
|
||||
} else {
|
||||
errEl.textContent = t('password_wrong');
|
||||
errEl.style.display = '';
|
||||
}
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
async function checkLatestVersion() {
|
||||
var btn = document.getElementById('checkVersionBtn');
|
||||
var prevText = btn ? btn.textContent : '';
|
||||
@@ -2425,7 +2595,9 @@
|
||||
closeProfiles();
|
||||
await selectChannel(1); return
|
||||
}
|
||||
if (data && typeof data === 'object' && data.channel) {
|
||||
if (data && typeof data === 'object' && data.type === 'no_changes') {
|
||||
showToast(t('no_new_messages'));
|
||||
} else if (data && typeof data === 'object' && data.channel) {
|
||||
if (data.channel === snapChannel) await loadMessages(data.channel)
|
||||
} else if (snapChannel > 0) { await loadMessages(snapChannel) }
|
||||
updateSendPanel();
|
||||
|
||||
+1
-1
@@ -1121,7 +1121,7 @@ func (s *Server) refreshChannel(channelNum int) {
|
||||
s.mu.RUnlock()
|
||||
if prevID > 0 && ch.LastMsgID == prevID && ch.ContentHash == prevHash && len(prevMsgs) > 0 {
|
||||
s.addLog(fmt.Sprintf("Channel %s: no changes (last ID: %d)", ch.Name, prevID))
|
||||
s.broadcast(fmt.Sprintf("event: update\ndata: {\"type\":\"messages\",\"channel\":%d}\n\n", channelNum))
|
||||
s.broadcast(fmt.Sprintf("event: update\ndata: {\"type\":\"no_changes\",\"channel\":%d}\n\n", channelNum))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user