feat: add background image handling and language/theme settings API

This commit is contained in:
Sarto
2026-04-14 03:33:21 +03:30
parent d5d8763b3a
commit 9ab82c33ba
2 changed files with 99 additions and 25 deletions
+38 -20
View File
@@ -2060,7 +2060,12 @@
// Re-render dynamic content
if (channels.length > 0) renderChannels();
}
function setLang(l) { lang = l; localStorage.setItem('thefeed_lang', l); applyLang() }
function setLang(l) {
lang = l;
localStorage.setItem('thefeed_lang', l);
applyLang();
fetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ lang: l }) }).catch(function () { });
}
// ===== STATE =====
var selectedChannel = 0, channels = [], eventSource = null, autoRefreshTimer = null, telegramLoggedIn = false, logVisible = false;
@@ -2124,6 +2129,16 @@
document.getElementById('fontSizeVal').textContent = s.fontSize;
}
if (s.debug) document.getElementById('cfgDebug').checked = true;
if (s.theme && (s.theme === 'dark' || s.theme === 'light')) {
localStorage.setItem('thefeed_theme', s.theme);
document.documentElement.setAttribute('data-theme', s.theme);
applyThemeButtons();
}
if (s.lang && (s.lang === 'fa' || s.lang === 'en')) {
lang = s.lang;
localStorage.setItem('thefeed_lang', s.lang);
applyLang();
}
if (s.version) { appVersion = s.version; renderAppVersion(s.version, s.commit); }
renderLatestVersion();
} catch (e) { }
@@ -2181,6 +2196,7 @@
localStorage.setItem('thefeed_theme', t);
document.documentElement.setAttribute('data-theme', t);
applyThemeButtons();
fetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ theme: t }) }).catch(function () { });
}
function applyThemeButtons() {
var cur = localStorage.getItem('thefeed_theme') || 'dark';
@@ -3620,34 +3636,36 @@
}
// ===== BACKGROUND IMAGE =====
function _setBg(data) {
function _setBg(url) {
var ca = document.querySelector('.chat-area');
ca.style.backgroundImage = data ? 'url("' + data + '")' : '';
ca.style.backgroundSize = data ? 'cover' : '';
ca.style.backgroundPosition = data ? 'center' : '';
ca.style.backgroundRepeat = data ? 'no-repeat' : '';
document.getElementById('messages').style.background = data ? 'transparent' : '';
ca.style.backgroundImage = url ? 'url("' + url + '")' : '';
ca.style.backgroundSize = url ? 'cover' : '';
ca.style.backgroundPosition = url ? 'center' : '';
ca.style.backgroundRepeat = url ? 'no-repeat' : '';
document.getElementById('messages').style.background = url ? 'transparent' : '';
}
function loadBgImage() {
var data = localStorage.getItem('thefeed_bg_image') || '';
if (data) _setBg(data);
// Use cache-busting query to ensure latest image.
var url = '/api/bg-image?t=' + Date.now();
fetch(url).then(function (r) {
if (r.status === 204 || !r.ok) return;
_setBg('/api/bg-image?t=' + Date.now());
}).catch(function () { });
}
function applyBgImage() {
async function applyBgImage() {
var inp = document.getElementById('bgImageInput');
if (!inp.files || !inp.files[0]) return;
var file = inp.files[0];
if (file.size > 5 * 1024 * 1024) { showToast('File too large (max 5MB)'); return }
var reader = new FileReader();
reader.onload = function (e) {
var data = e.target.result;
try { localStorage.setItem('thefeed_bg_image', data) } catch (ex) { showToast('File too large for storage'); return }
_setBg(data);
if (file.size > 10 * 1024 * 1024) { showToast('File too large (max 10MB)'); return }
try {
var r = await fetch('/api/bg-image', { method: 'POST', body: file });
if (!r.ok) { showToast(await r.text()); return }
_setBg('/api/bg-image?t=' + Date.now());
showToast(t('apply'));
};
reader.readAsDataURL(file);
} catch (e) { showToast(e.message); }
}
function clearBgImage() {
localStorage.removeItem('thefeed_bg_image');
async function clearBgImage() {
try { await fetch('/api/bg-image', { method: 'DELETE' }) } catch (e) { }
_setBg('');
document.getElementById('bgImageInput').value = '';
showToast(t('clear_bg'));
+61 -5
View File
@@ -8,6 +8,7 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"io"
"io/fs"
"log"
mrand "math/rand/v2"
@@ -54,8 +55,10 @@ type ProfileList struct {
Active string `json:"active"` // ID of active profile
Profiles []Profile `json:"profiles"`
// FontSize stores user's preferred font size (0 = default 14).
FontSize int `json:"fontSize,omitempty"`
Debug bool `json:"debug,omitempty"`
FontSize int `json:"fontSize,omitempty"`
Debug bool `json:"debug,omitempty"`
Theme string `json:"theme,omitempty"`
Lang string `json:"lang,omitempty"`
}
// lastScanData is the on-disk structure for last_scan.json.
@@ -186,6 +189,7 @@ func (s *Server) Run() error {
mux.HandleFunc("/api/settings", s.handleSettings)
mux.HandleFunc("/api/version-check", s.handleVersionCheck)
mux.HandleFunc("/api/cache/clear", s.handleClearCache)
mux.HandleFunc("/api/bg-image", s.handleBgImage)
mux.HandleFunc("/api/resolvers/apply-saved", s.handleApplySavedResolvers)
mux.HandleFunc("/api/resolvers/active", s.handleActiveResolvers)
mux.HandleFunc("/api/resolvers/remove", s.handleRemoveResolver)
@@ -1482,12 +1486,14 @@ 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, "version": version.Version, "commit": version.Commit})
writeJSON(w, map[string]any{"fontSize": pl.FontSize, "debug": pl.Debug, "theme": pl.Theme, "lang": pl.Lang, "version": version.Version, "commit": version.Commit})
case http.MethodPost:
var req struct {
FontSize int `json:"fontSize"`
Debug bool `json:"debug"`
FontSize int `json:"fontSize"`
Debug bool `json:"debug"`
Theme string `json:"theme"`
Lang string `json:"lang"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON", 400)
@@ -1505,6 +1511,12 @@ func (s *Server) handleSettings(w http.ResponseWriter, r *http.Request) {
}
pl.FontSize = req.FontSize
pl.Debug = req.Debug
if req.Theme == "dark" || req.Theme == "light" {
pl.Theme = req.Theme
}
if req.Lang == "fa" || req.Lang == "en" {
pl.Lang = req.Lang
}
if err := s.saveProfiles(pl); err != nil {
http.Error(w, fmt.Sprintf("save: %v", err), 500)
return
@@ -1524,6 +1536,50 @@ func (s *Server) handleSettings(w http.ResponseWriter, r *http.Request) {
}
}
func (s *Server) handleBgImage(w http.ResponseWriter, r *http.Request) {
bgPath := filepath.Join(s.dataDir, "bg_image")
switch r.Method {
case http.MethodGet:
data, err := os.ReadFile(bgPath)
if err != nil {
w.Header().Set("Content-Type", "application/octet-stream")
w.WriteHeader(204)
return
}
// Detect content type from file data.
ct := http.DetectContentType(data)
w.Header().Set("Content-Type", ct)
w.Header().Set("Cache-Control", "no-cache")
w.Write(data)
case http.MethodPost:
// Limit upload to 10 MB.
r.Body = http.MaxBytesReader(w, r.Body, 10<<20)
data, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "file too large (max 10MB)", 413)
return
}
ct := http.DetectContentType(data)
if !strings.HasPrefix(ct, "image/") {
http.Error(w, "not an image", 400)
return
}
if err := os.WriteFile(bgPath, data, 0600); err != nil {
http.Error(w, fmt.Sprintf("save: %v", err), 500)
return
}
writeJSON(w, map[string]any{"ok": true})
case http.MethodDelete:
os.Remove(bgPath)
writeJSON(w, map[string]any{"ok": true})
default:
http.Error(w, "method not allowed", 405)
}
}
func (s *Server) handleVersionCheck(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", 405)