fix(android): pin WebView port + persist scan-prompt server-side

This commit is contained in:
Sarto
2026-05-03 17:06:06 +03:30
parent 920e6077f8
commit d489387d06
3 changed files with 65 additions and 14 deletions
@@ -74,7 +74,9 @@ class ThefeedService : Service() {
val dataDir = File(filesDir, "thefeeddata")
if (!dataDir.exists()) dataDir.mkdirs()
val selectedPort = findFreePort()
// Reuse the last port so the WebView origin stays
// stable — keeps localStorage state across launches.
val selectedPort = pickPort()
currentPort = selectedPort
savePort(selectedPort)
@@ -140,6 +142,20 @@ class ThefeedService : Service() {
}
}
// Try the last port first; fall back to a new free one if it's
// taken. Keeps localStorage origin stable across launches.
private fun pickPort(): Int {
val prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val last = prefs.getInt(PREF_PORT, -1)
if (last in 1024..65535) {
try {
ServerSocket(last).use { it.reuseAddress = true }
return last
} catch (_: Exception) { }
}
return findFreePort()
}
private fun savePort(port: Int) {
val prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit().putInt(PREF_PORT, port).apply()
+34 -8
View File
@@ -3542,6 +3542,15 @@
applyLang();
}
if (s.version) { appVersion = s.version; renderAppVersion(s.version, s.commit); }
// Sync the server-persisted "don't show scan prompt" flag
// into localStorage. Android picks a new 127.0.0.1 port on
// each launch, so localStorage alone wouldn't survive
// restart — the server-side flag is the source of truth.
if (s.scanPromptOff === true) {
localStorage.setItem('thefeed_scan_prompt_off', '1');
} else if (s.scanPromptOff === false) {
localStorage.removeItem('thefeed_scan_prompt_off');
}
renderLatestVersion();
} catch (e) { }
}
@@ -3721,19 +3730,27 @@
if (promptEl) promptEl.checked = localStorage.getItem('thefeed_scan_prompt_off') !== '1';
}
// Toggle the localStorage flag that controls whether the startup
// scan prompt is shown. Cleared when the box is checked, set to
// "1" when unchecked.
// Toggle the "show startup scan prompt" preference. Persists
// both client-side (localStorage for fast boot) and server-side
// (so Android keeps it across launches even when the WebView
// origin port changes).
function setShowScanPrompt(enabled) {
if (enabled) {
localStorage.removeItem('thefeed_scan_prompt_off');
// Also clear the per-session "already shown" flag so the
// prompt appears next time the user reloads — without this
// re-enabling does nothing until they restart the browser.
// Clear the per-session "already shown" flag so the prompt
// re-appears on the next reload — without this, re-enabling
// does nothing until the browser tab is closed.
sessionStorage.removeItem('thefeed_scan_prompt_shown');
} else {
localStorage.setItem('thefeed_scan_prompt_off', '1');
}
try {
fetch('/api/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ scanPromptOff: !enabled })
});
} catch (e) { }
}
function closeSettings() { document.getElementById('settingsModal').classList.remove('active') }
async function saveSettings() {
@@ -4406,9 +4423,18 @@
sessionStorage.setItem('thefeed_scan_prompt_shown', '1');
}
function savedResolversNever() {
// "Don't show again" — persist across sessions. The user can
// un-tick it from Settings → Show startup scan prompt.
// "Don't show again" — persist across sessions. localStorage
// for fast access, /api/settings for survival across Android
// restarts (the WebView origin port changes each launch, so
// localStorage alone gets reset).
localStorage.setItem('thefeed_scan_prompt_off', '1');
try {
fetch('/api/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ scanPromptOff: true })
});
} catch (e) { }
savedResolversSkip();
showToast(t('startup_scan_off') || 'Startup scan prompt disabled');
}
+14 -5
View File
@@ -115,6 +115,11 @@ type ProfileList struct {
// single list named "Default".
ActiveLists []ActiveList `json:"activeLists,omitempty"`
SelectedList string `json:"selectedList,omitempty"`
// ScanPromptOff suppresses the startup "scan resolvers?" prompt.
// Persisted server-side so it survives Android's per-launch port
// changes (each launch picks a fresh port → different localStorage
// origin → flag was lost on every restart).
ScanPromptOff bool `json:"scanPromptOff,omitempty"`
}
// lastScanData is the on-disk structure for last_scan.json.
@@ -2581,14 +2586,15 @@ func (s *Server) handleSettings(w http.ResponseWriter, r *http.Request) {
if pl == nil {
pl = &ProfileList{}
}
writeJSON(w, map[string]any{"fontSize": pl.FontSize, "debug": pl.Debug, "theme": pl.Theme, "lang": pl.Lang, "version": version.Version, "commit": version.Commit})
writeJSON(w, map[string]any{"fontSize": pl.FontSize, "debug": pl.Debug, "theme": pl.Theme, "lang": pl.Lang, "scanPromptOff": pl.ScanPromptOff, "version": version.Version, "commit": version.Commit})
case http.MethodPost:
var req struct {
FontSize int `json:"fontSize"`
Debug bool `json:"debug"`
Theme string `json:"theme"`
Lang string `json:"lang"`
FontSize int `json:"fontSize"`
Debug bool `json:"debug"`
Theme string `json:"theme"`
Lang string `json:"lang"`
ScanPromptOff *bool `json:"scanPromptOff"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON", 400)
@@ -2612,6 +2618,9 @@ func (s *Server) handleSettings(w http.ResponseWriter, r *http.Request) {
if req.Lang == "fa" || req.Lang == "en" {
pl.Lang = req.Lang
}
if req.ScanPromptOff != nil {
pl.ScanPromptOff = *req.ScanPromptOff
}
if err := s.saveProfiles(pl); err != nil {
http.Error(w, fmt.Sprintf("save: %v", err), 500)
return