diff --git a/custom_components/file_shell/__init__.py b/custom_components/file_shell/__init__.py index 6254017..bcea427 100644 --- a/custom_components/file_shell/__init__.py +++ b/custom_components/file_shell/__init__.py @@ -28,11 +28,13 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.storage import Store import voluptuous as vol NAME = "File Shell" DOMAIN = "file_shell" DOMAIN_REG = "file_shell_reg" +DOMAIN_OPT = "file_shell.json" CONF_BASE_DIR = "base_dir" _LOGGER = logging.getLogger(__name__) @@ -92,6 +94,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.http.register_view(FileShellStreamView(hass, entry)) hass.http.register_view(FileShellTerminalView(hass, entry)) entry.async_on_unload(entry.add_update_listener(async_update_options)) + store = Store(hass, version=1, key=DOMAIN_OPT) + hass.data[DOMAIN_OPT] = store hass.data[DOMAIN_REG] = True return True @@ -245,7 +249,6 @@ async def read_json(self, request: web.Request) -> dict[str, Any]: raise web.HTTPBadRequest(text="Invalid JSON body") from err def _get_token_from_request(self, request: web.Request) -> str | None: - """Extract bearer token from Authorization header or query string.""" auth_header = request.headers.get("Authorization", "") if auth_header.lower().startswith("bearer "): return auth_header[7:].strip() @@ -253,7 +256,6 @@ def _get_token_from_request(self, request: web.Request) -> str | None: return request.query.get("token") or request.query.get("authorization") def _admin_user(self, request: web.Request) -> bool: - """Return True if request is authenticated as an active admin user.""" token = self._get_token_from_request(request) if not token: return False @@ -265,6 +267,18 @@ def _admin_user(self, request: web.Request) -> bool: pass return False + async def _storage(self, config: dict[str, Any] | None = None) -> dict[str, Any]: + default = {"wrap": True, "bulb": False, "space": False, "favs": {}, "recentmax": 10, "recentlist": []} + if not (store := self.hass.data.get(DOMAIN_OPT)): + _LOGGER.warning("Settings not initialized") + return default + data = {**default, **((await store.async_load()) or {})} + if isinstance(config, dict): + data |= config + await store.async_save(data) + return data + + class FileShellApiView(HomeAssistantView, FileShellBase): url = "/api/file_shell" name = "api:file_shell" @@ -293,6 +307,7 @@ async def post(self, request: web.Request) -> web.Response: "chmod": self._chmod, "symlink": self._symlink, "valid": self._valid, + "config": self._config, "copy": lambda req: self._move(req, is_copy=True), "move": lambda req: self._move(req, is_copy=False), }, @@ -740,6 +755,10 @@ def tagger(r, n): return r.construct_scalar(n) return json_ok(valid=True) return json_ok(valid=False, **result) + async def _config(self, request: web.Request) -> web.Response: + data = await self.read_json(request) + return json_ok(opt=await self._storage(data if isinstance(data, dict) else None)) + class FileShellStreamView(HomeAssistantView, FileShellBase): url = "/api/file_shell_stream" name = "api:file_shell_stream" @@ -829,6 +848,7 @@ async def _upload(self, request: web.Request) -> web.Response: msg = f"Uploaded: {', '.join(uploaded)}" if uploaded else "No files uploaded" return json_ok(msg=msg) + class FileShellTerminalView(HomeAssistantView, FileShellBase): url = "/api/file_shell_terminal" name = "api:file_shell_terminal" diff --git a/custom_components/file_shell/manifest.json b/custom_components/file_shell/manifest.json index 5ead120..3fb7c5a 100644 --- a/custom_components/file_shell/manifest.json +++ b/custom_components/file_shell/manifest.json @@ -9,5 +9,5 @@ "iot_class": "local_push", "issue_tracker": "https://github.com/junkfix/file-shell/issues", "requirements": [], - "version": "1.0.3" + "version": "1.0.4" } diff --git a/custom_components/file_shell/www/file_shell.js b/custom_components/file_shell/www/file_shell.js index fc1d166..020de3f 100644 --- a/custom_components/file_shell/www/file_shell.js +++ b/custom_components/file_shell/www/file_shell.js @@ -1,7 +1,7 @@ (function () { "use strict"; -console.log("File Shell 1.0.3"); +console.log("File Shell 1.0.4"); const _id = (id) => document.getElementById(id); const _qsa = (q, el) => Array.from((el || document).querySelectorAll(q)); const _qs = (q, el) => (el || document).querySelector(q); @@ -55,17 +55,7 @@ const localGet = (e) => localStorage.getItem(localTag+e); const localSet = (k, v) => v == null ? localStorage.removeItem(localTag + k) : localStorage.setItem(localTag + k, v); -const opt = { - multi: 0, - sel: {}, - dark: false, - wrap: localGet("wrap"), - bulb: localGet("bulb"), - space: localGet("space"), - font: Number(localGet('font')) || 100, - find: 0, - favs: {}, -}; +let opt = {}; const app = { auth: null, entities: [], @@ -83,6 +73,11 @@ const app = { uploads: 0, cmd: 0, cmenu: 0, + dark: false, + font: Number(localGet('font')) || 100, + find: 0, + multi: 0, + sel: {}, }; const els = { @@ -401,11 +396,11 @@ const colorMode = () => { }; function checkTheme(manual){ - opt.dark = (theme === 'mode') ? !!window.matchMedia?.("(prefers-color-scheme: dark)").matches : (theme === 'dark'); - document.documentElement.classList.toggle("dark", opt.dark); + app.dark = (theme === 'mode') ? !!window.matchMedia?.("(prefers-color-scheme: dark)").matches : (theme === 'dark'); + document.documentElement.classList.toggle("dark", app.dark); if(manual){ if (edit.cm) { - edit.cm.dispatch({effects: edit.theme.reconfigure(opt.dark ? CM.darkTheme : CM.lightTheme)}); + edit.cm.dispatch({effects: edit.theme.reconfigure(app.dark ? CM.darkTheme : CM.lightTheme)}); } } } @@ -413,7 +408,7 @@ function checkTheme(manual){ function wordWrap(css){ if(css!==1){ opt.wrap = (opt.wrap)? null : 1; - localSet("wrap",opt.wrap); + MyConfig(opt); edit.cm.dispatch({effects: edit.wrap.reconfigure(opt.wrap ? CM.EditorView.lineWrapping : [])}); } _id('wrapButton').classList.toggle("primary", !!opt.wrap); @@ -422,7 +417,7 @@ function wordWrap(css){ function spaces(css){ if(css!==1){ opt.space = (opt.space)? null : 1; - localSet("space",opt.space); + MyConfig(opt); edit.cm.dispatch({effects: edit.space.reconfigure(opt.space ? CM.highlightWhitespace() : [])}); } _id('spaceButton').classList.toggle("primary", !!opt.space); @@ -472,21 +467,21 @@ async function validate(){ function autoComp(css){ if(css!==1){ opt.bulb = (opt.bulb)? null : 1; - localSet("bulb",opt.bulb); + MyConfig(opt); } _id('bulbButton').classList.toggle("primary", !!opt.bulb); } function txtSize(e=0){ if(e){ - opt.font += e; - localSet('font', opt.font); - toast(opt.font+'%',{timeout: 0.3,theme:'black'}); + app.font += e; + localSet('font', app.font); + toast(app.font+'%',{timeout: 0.3,theme:'black'}); } edit.cm.dispatch({effects: edit.font.reconfigure(txtSizeGo())}); } -const txtSizeGo = ()=>CM.EditorView.theme({'&': {fontSize: opt.font + '%'}}); +const txtSizeGo = ()=>CM.EditorView.theme({'&': {fontSize: app.font + '%'}}); async function authHeaders(extra) { const headers = extra || {}; @@ -544,8 +539,6 @@ async function apiPost(act, body) { } return await apiMsg(res); - - } function fixpath(p) { @@ -633,7 +626,7 @@ function switchToTab(tabId) { edit.cm.dispatch({ effects: [ - edit.theme.reconfigure(opt.dark ? CM.darkTheme : CM.lightTheme), + edit.theme.reconfigure(app.dark ? CM.darkTheme : CM.lightTheme), edit.wrap.reconfigure(opt.wrap ? CM.EditorView.lineWrapping : []), edit.space.reconfigure(opt.space ? CM.highlightWhitespace() : []), edit.font.reconfigure(txtSizeGo()), @@ -746,7 +739,7 @@ async function openTextFile(e) { CM.search(), CM.autocompletion(), CM.EditorState.languageData.of(() => [{autocomplete: entityCompletion}]), - edit.theme.of(opt.dark ? CM.darkTheme : CM.lightTheme), + edit.theme.of(app.dark ? CM.darkTheme : CM.lightTheme), edit.lang.of(edit.ext[fileExt] ? edit.ext[fileExt]() : []), edit.wrap.of(opt.wrap ? CM.EditorView.lineWrapping : []), edit.space.of(opt.space ? CM.highlightWhitespace() : []), @@ -783,7 +776,7 @@ async function openTextFile(e) { if(s){edit.cm.scrollDOM.scrollTo(s.left, s.top);} sidebar(0); - opt.find = 0; + app.find = 0; buildTabs(); } @@ -815,7 +808,7 @@ async function saveTextFile() { async function loadDir(path) { switchToTab('files'); const t = fixpath(path.replace(/^\.+|\.+$/g, "")); - opt.sel = {}; + app.sel = {}; hideMenu(); const data = await getDir(t); app.curDir = fixpath(t); @@ -880,7 +873,7 @@ function buildList() { if(app.cmenu){return hideMenu();} loadDir(getparent(app.curDir)); } }, [ - (opt.multi ? _ce("td", 0, 0) : null ), + (app.multi ? _ce("td", 0, 0) : null ), _ce("td", 0, 0,[ _ce('i',{class:'ico-folder'}), _ce('strong', 0, {textContent: ".."}) @@ -893,10 +886,10 @@ function buildList() { let ticked = 0; let tot = 0; for (const e of sorted) { const dt = new Date(e.mtime).toLocaleString('en-GB', { hour12: false }); - if(opt.sel[e.path]){ticked++;} + if(app.sel[e.path]){ticked++;} tot++; let x = (e.type === "dir" || isText(e))? 'c' : 'n'; - const row = _ce("tr", {"data-sel": (opt.sel[e.path]? '1': '0'), class: x}, { + const row = _ce("tr", {"data-sel": (app.sel[e.path]? '1': '0'), class: x}, { onclick: () => { if(app.cmenu){return hideMenu();} if (e.type === "dir") { @@ -912,14 +905,14 @@ function buildList() { showContextMenu(q.clientX, q.clientY, e, row); } }, [ - (opt.multi ? _ce("td", {class:"tick"}, { onclick: (q) => { + (app.multi ? _ce("td", {class:"tick"}, { onclick: (q) => { q.stopPropagation(); let d='1'; const p = q.target.parentNode; - if(opt.sel[e.path]){ + if(app.sel[e.path]){ d='0'; - delete opt.sel[e.path]; + delete app.sel[e.path]; }else{ - opt.sel[e.path]=e; + app.sel[e.path]=e; } _att(p,'data-sel',d); } }) : null ), @@ -938,10 +931,10 @@ function buildList() { const table = _ce("table", {class: 'exp'}, 0, [ _ce("thead", 0, 0, [ _ce("tr", {"data-sel": (ticked { + ((app.multi) ? _ce("th", {class:"tick"}, { onclick: () => { const add = ticked {loadDir(app.curDir);}}), _ce('button',{title:'Select', class: 'ico-tickbox'},{onclick:(e)=>{ - e.target.classList.toggle('primary',(opt.multi = !opt.multi)); + e.target.classList.toggle('primary',(app.multi = !app.multi)); buildList(); }}), _ce('button',{title:'Upload', class: 'ico-upload'},{onclick: ()=>{uploadui();}}), @@ -2051,8 +2052,8 @@ async function init() { _ce('button',{title: "Undo", class:"ico-undo"}, {onclick: ()=>{CM.undo(edit.cm);}}), _ce('button',{title: "Redo", class:"ico-redo"}, {onclick: ()=>{CM.redo(edit.cm);}}), _ce('button',{title: "Find in file", class:"ico-search"}, {onclick: ()=>{ - opt.find = !opt.find; - if(opt.find){ + app.find = !app.find; + if(app.find){ CM.openSearchPanel(edit.cm); }else{ CM.closeSearchPanel(edit.cm); @@ -2111,7 +2112,6 @@ async function init() { dirContext(e.clientX, e.clientY); }); - loadFavs(); favList(0); buildTabs(); loadDir("/");