├── .gitignore ├── LICENSE ├── README.md ├── README.zh.md ├── bash ├── README.md └── install.sh ├── custom_components └── ha_file_explorer │ ├── __init__.py │ ├── config_flow.py │ ├── file_api.py │ ├── http_api.py │ ├── manifest.json │ ├── manifest.py │ ├── translations │ ├── en.json │ ├── ru.json │ └── zh-Hans.json │ └── www │ ├── assets │ ├── AppLayout.de7f2f43.css │ ├── AppLayout.e91c6a4e.js │ ├── editor.31b1244f.js │ ├── editor.eba3ec12.css │ ├── index.4255fc8d.css │ ├── index.5274938f.js │ ├── index.abe45c59.js │ ├── index.ad1752d2.css │ ├── material-icons-outlined.755ef314.woff │ ├── material-icons-outlined.828c436d.woff2 │ ├── material-icons-round.136587d6.woff │ ├── material-icons-round.9fcaafe6.woff2 │ ├── material-icons-sharp.84289da0.woff2 │ ├── material-icons-sharp.914e06ba.woff │ ├── material-icons-two-tone.517cb22e.woff │ ├── material-icons-two-tone.86555c87.woff2 │ ├── material-icons.18d2477b.woff2 │ ├── material-icons.66dca61a.woff │ ├── materialdesignicons-webfont.12845dec.woff2 │ ├── materialdesignicons-webfont.55c35c20.eot │ ├── materialdesignicons-webfont.5e9b23f9.ttf │ ├── materialdesignicons-webfont.d87b64d8.woff │ ├── source-sans-pro-all-400-normal.56cfd2ea.woff │ ├── source-sans-pro-cyrillic-400-normal.0acd59e1.woff2 │ ├── source-sans-pro-cyrillic-ext-400-normal.4ba425fa.woff2 │ ├── source-sans-pro-greek-400-normal.9755c83c.woff2 │ ├── source-sans-pro-greek-ext-400-normal.f2a19d8c.woff2 │ ├── source-sans-pro-latin-400-normal.c124c88c.woff2 │ ├── source-sans-pro-latin-ext-400-normal.ee519845.woff2 │ └── source-sans-pro-vietnamese-400-normal.6e0839c2.woff2 │ ├── favicon.ico │ ├── index.html │ └── js │ └── ace.js └── frontend ├── index.html ├── package.json ├── public ├── favicon.ico └── js │ └── ace.js ├── src ├── App.vue ├── api │ ├── index.ts │ └── service.ts ├── assets │ └── logo.png ├── components │ ├── dialogs │ │ ├── CreateFile.vue │ │ ├── RenameFile.vue │ │ ├── UploadFile.vue │ │ └── VersionInfo.vue │ ├── globel │ │ └── mdi-icon.vue │ ├── layouts │ │ └── AppLayout.vue │ └── list │ │ ├── FileList.vue │ │ └── FolderList.vue ├── env.d.ts ├── locales │ └── index.ts ├── main.ts ├── router │ └── index.ts ├── store │ ├── actions.ts │ ├── index.ts │ └── mutations.ts ├── style │ └── index.scss ├── types │ ├── api.d.ts │ ├── global.d.ts │ └── index.d.ts ├── utils │ ├── format.ts │ └── query.ts └── views │ ├── editor.vue │ └── index.vue ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 shaonianzhentan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # File Explorer 2 | 3 | file explorer for home assistant 4 | 5 | [![hacs_badge](https://img.shields.io/badge/Home-Assistant-%23049cdb)](https://www.home-assistant.io/) 6 | [![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg)](https://github.com/hacs/integration) 7 | 8 | ![forks](https://img.shields.io/github/forks/shaonianzhentan/ha_file_explorer) 9 | ![stars](https://img.shields.io/github/stars/shaonianzhentan/ha_file_explorer) 10 | ![license](https://img.shields.io/github/license/shaonianzhentan/ha_file_explorer) 11 | ![visit](https://visitor-badge.laobi.icu/badge?page_id=shaonianzhentan.ha_file_explorer&left_text=visit) 12 | 13 | English | [简体中文](README.zh.md) 14 | 15 | ## Installation 16 | 17 | After installation, restart home assistant, refresh the page and search for `ha_file_explorer` in the integration 18 | 19 | [![Add Integration](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start?domain=ha_file_explorer) 20 | 21 | ## Features 22 | 23 | - [x] create new file or new folder 24 | - [x] delete file or folder 25 | - [x] upload files or folder 26 | - [x] rename file or folder 27 | - [x] modify file content 28 | - [x] support homeassistant theme 29 | 30 | ## Screenshot 31 | 32 | ![pic](https://cdn.jsdelivr.net/gh/shaonianzhentan/image@main/ha_file_explorer/ha_file_explorer.png) 33 | 34 | ## If this project is helpful to you, please donate a cup of coffee milk tea 😘 35 | | |Paypal|Alipay|WeChat| 36 | |---|---|---|---| 37 | Milk tea | https://paypal.me/shaonianzhentan | Alipay | WeChat Pay 38 | 39 | ## Follow my wechat subscription number 40 | HomeAssistant家庭助理 -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | # File Explorer 2 | 在HA里使用的文件管理器 3 | 4 | [![hacs_badge](https://img.shields.io/badge/Home-Assistant-%23049cdb)](https://www.home-assistant.io/) 5 | [![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg)](https://github.com/hacs/integration) 6 | 7 | ![forks](https://img.shields.io/github/forks/shaonianzhentan/ha_file_explorer) 8 | ![stars](https://img.shields.io/github/stars/shaonianzhentan/ha_file_explorer) 9 | ![license](https://img.shields.io/github/license/shaonianzhentan/ha_file_explorer) 10 | ![visit](https://visitor-badge.laobi.icu/badge?page_id=shaonianzhentan.ha_file_explorer&left_text=visit) 11 | 12 | [English](README.md) | 简体中文 13 | 14 | ## 安装 15 | 16 | 安装完成重启HA,刷新一下页面,在集成里搜索`文件管理`即可 17 | 18 | [![Add Integration](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start?domain=ha_file_explorer) 19 | 20 | ## 功能 21 | 22 | - [x] 创建文件或文件夹 23 | - [x] 删除文件或文件夹 24 | - [x] 上传文件或文件夹 25 | - [x] 重命名文件或文件夹 26 | - [x] 修改文件内容 27 | - [x] 支持HA主题跟随 28 | 29 | ## 预览 30 | 31 | ![pic](https://github.com/shaonianzhentan/image/raw/main/ha_file_explorer/ha_file_explorer.png) 32 | 33 | 34 | ## 如果这个项目对你有帮助,请我喝杯咖啡奶茶吧😘 35 | | |支付宝|微信| 36 | |---|---|---| 37 | 奶茶= | 支付宝 | 微信支付 38 | 39 | ## 关注我的微信订阅号,了解更多HomeAssistant相关知识 40 | HomeAssistant家庭助理 41 | 42 | --- 43 | **在使用的过程之中,如果遇到无法解决的问题,付费咨询请加Q`635147515`** -------------------------------------------------------------------------------- /bash/README.md: -------------------------------------------------------------------------------- 1 | # Installtion 2 | 3 | ### English 4 | 5 | tips: change dir to homeassistant config folder 6 | ```bash 7 | curl https://github.com/shaonianzhentan/ha_file_explorer/raw/dev/bash/install.sh | bash 8 | ``` 9 | 10 | ### 中文 11 | 12 | 提示: 切换到HomeAssistant配置目录 13 | ```bash 14 | curl https://gitee.com/shaonianzhentan/ha_file_explorer/raw/dev/bash/install.sh | bash 15 | ``` -------------------------------------------------------------------------------- /bash/install.sh: -------------------------------------------------------------------------------- 1 | # clone project from gitee 2 | git clone -b dev https://gitee.com/shaonianzhentan/ha_file_explorer --depth=1 3 | # remove exist files 4 | rm -rf custom_components/ha_file_explorer 5 | # create custom_components dir if not exist 6 | if [ ! -d "custom_components" ]; then 7 | mkdir custom_components 8 | fi 9 | # copy folder to custom_components dir 10 | cp -r ./ha_file_explorer/custom_components/ha_file_explorer custom_components/ha_file_explorer 11 | # remove files 12 | rm -rf ha_file_explorer -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/__init__.py: -------------------------------------------------------------------------------- 1 | from homeassistant.config_entries import ConfigEntry 2 | from homeassistant.core import HomeAssistant 3 | import homeassistant.helpers.config_validation as cv 4 | from homeassistant.components.frontend import async_register_built_in_panel 5 | from homeassistant.components.http import StaticPathConfig 6 | from .http_api import HttpApi 7 | from .manifest import manifest 8 | 9 | DOMAIN = manifest.domain 10 | NAME = manifest.name 11 | CONFIG_SCHEMA = cv.deprecated(DOMAIN) 12 | 13 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 14 | entry_id = entry.entry_id 15 | url_path = f'/{entry_id}-local' 16 | await hass.http.async_register_static_paths( 17 | [ StaticPathConfig(url_path, hass.config.path("custom_components/" + DOMAIN + "/www"), False) ] 18 | ) 19 | 20 | async_register_built_in_panel(hass, "iframe", 21 | NAME, "mdi:folder", DOMAIN, {"url": f'{url_path}/index.html?v={manifest.version}'}, 22 | entry.data.get('require_admin') 23 | ) 24 | 25 | hass.http.register_view(HttpApi) 26 | return True 27 | 28 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 29 | hass.components.frontend.async_remove_panel(DOMAIN) 30 | return True -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/config_flow.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any 4 | import voluptuous as vol 5 | 6 | from homeassistant.config_entries import ConfigFlow 7 | from homeassistant.data_entry_flow import FlowResult 8 | 9 | from .manifest import manifest 10 | 11 | DOMAIN = manifest.domain 12 | DATA_SCHEMA = vol.Schema({ 13 | vol.Required("require_admin"): bool 14 | }) 15 | 16 | class SimpleConfigFlow(ConfigFlow, domain=DOMAIN): 17 | 18 | VERSION = 1 19 | 20 | async def async_step_user( 21 | self, user_input: dict[str, Any] | None = None 22 | ) -> FlowResult: 23 | """Handle the initial step.""" 24 | if self._async_current_entries(): 25 | return self.async_abort(reason="single_instance_allowed") 26 | 27 | if user_input is None: 28 | return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) 29 | 30 | return self.async_create_entry(title=DOMAIN, data=user_input) -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/file_api.py: -------------------------------------------------------------------------------- 1 | import os, shutil, uuid, yaml, logging, aiohttp, json, urllib, hashlib, datetime, asyncio, base64, re, zipfile, tempfile, time 2 | 3 | # 获取当前文件列表 4 | def get_dir_list(dir): 5 | allcontent = os.listdir(dir) 6 | dirItem = [] 7 | for item in allcontent: 8 | try: 9 | # 获取文件路径 10 | path_name = os.path.join(dir,item) 11 | # 判断当前路径是否存在 12 | if os.path.exists(path_name) == False: 13 | continue 14 | hashInfo = {} 15 | listInfo = os.stat(path_name) 16 | hashInfo['name'] = item 17 | hashInfo['path'] = item 18 | hashInfo['time'] = datetime.datetime.fromtimestamp(int(listInfo.st_mtime)).strftime('%Y-%m-%d %H:%M:%S') 19 | 20 | if os.path.isfile(path_name): 21 | hashInfo['type'] = 'file' 22 | hashInfo['size'] = int(listInfo.st_size) 23 | if os.path.isdir(path_name): 24 | hashInfo['type'] = 'dir' 25 | hashInfo['size'] = get_dir_size(path_name) 26 | # 显示格式化文件大小 27 | hashInfo['size_name'] = format_byte(hashInfo['size']) 28 | dirItem.append(hashInfo) 29 | except Exception as ex: 30 | print(ex) 31 | # 以名称排序 32 | dirItem.sort(key=lambda x: x['name'], reverse=True) 33 | return dirItem 34 | 35 | # 创建目录 36 | def mkdir(path): 37 | if os.path.isdir(path) == False: 38 | folders = [] 39 | while not os.path.isdir(path): 40 | path, suffix = os.path.split(path) 41 | folders.append(suffix) 42 | for folder in folders[::-1]: 43 | path = os.path.join(path, folder) 44 | os.mkdir(path) 45 | 46 | # 获取目录大小 47 | def get_dir_size(dir): 48 | size = 0 49 | for root, dirs, files in os.walk(dir): 50 | size += sum([os.path.getsize(os.path.join(root, name)) for name in files]) 51 | return size 52 | 53 | # 格式化文件大小的函数 54 | def format_byte(res): 55 | bu = 1024 56 | if res < bu: 57 | res = f'{bu}B' 58 | elif bu <= res < bu**2: 59 | res = f'{round(res / bu, 2)}KB' 60 | elif bu**2 <= res < bu**3: 61 | res = f'{round(res / bu**2, 2)}MB' 62 | elif bu**3 <= res < bu**4: 63 | res = f'{round(res / bu**3, 2)}GB' 64 | elif bu**4 <= res < bu**5: 65 | res = f'{round(res / bu**4, 2)}TB' 66 | return res 67 | 68 | # 删除文件 69 | def delete_file(file_path): 70 | if os.path.exists(file_path): 71 | if os.path.isfile(file_path): 72 | # 删除文件 73 | os.remove(file_path) 74 | elif os.path.isdir(file_path): 75 | # 删除目录 76 | shutil.rmtree(file_path, ignore_errors=True) 77 | 78 | # 移动文件 79 | def move_file(source_file, target_file): 80 | # 创建目录 81 | lastIndex = target_file.replace('\\','/').rindex('/') 82 | _dir = target_file[0:lastIndex] 83 | if os.path.isdir(_dir) == False: 84 | mkdir(_dir) 85 | shutil.move(source_file, target_file) 86 | 87 | # 复制文件 88 | def copy_file(source_file, target_file): 89 | # 创建目录 90 | lastIndex = target_file.replace('\\','/').rindex('/') 91 | _dir = target_file[0:lastIndex] 92 | if os.path.isdir(_dir) == False: 93 | mkdir(_dir) 94 | shutil.copy2(source_file, target_file) 95 | 96 | # 加载yaml 97 | def load_yaml(file_path): 98 | # 不存在则返回空字典 99 | if os.path.exists(file_path) == False: 100 | return {} 101 | fs = open(file_path, encoding="UTF-8") 102 | data = yaml.load(fs, Loader=yaml.FullLoader) 103 | return data 104 | 105 | # 存储为yaml 106 | def save_yaml(file_path, data): 107 | _dict = {} 108 | _dict.update(data) 109 | with open(file_path, 'w') as f: 110 | yaml.dump(_dict, f) 111 | 112 | # 加载json 113 | def load_json(file_path): 114 | # 不存在则返回空字典 115 | if os.path.exists(file_path) == False: 116 | return {} 117 | with open(file_path, 'r', encoding='utf-8') as f: 118 | data = json.load(f) 119 | return data 120 | 121 | # 存储为json 122 | def save_json(file_path, data): 123 | with open(file_path, 'w', encoding='utf-8') as f: 124 | json.dump(data, f, ensure_ascii=False) 125 | 126 | def read_file(file_path): 127 | ''' 读取文件 ''' 128 | with open(file_path, 'rb') as f: 129 | content = f.read() 130 | return content 131 | 132 | # 加载内容 133 | def load_content(file_path): 134 | fp = open(file_path, 'r', encoding='UTF-8') 135 | content = fp.read() 136 | fp.close() 137 | return content 138 | 139 | # 保存内容 140 | def save_content(file_path, data): 141 | fp = open(file_path, 'w+', encoding='UTF-8') 142 | fp.write(data) 143 | fp.close() 144 | 145 | # base64数据生成文件 146 | def base64_to_file(base64_data, file): 147 | ori_image_data = base64.b64decode(base64_data) 148 | fout = open(file, 'wb') 149 | fout.write(ori_image_data) 150 | fout.close() 151 | 152 | def dir_to_zip(path): 153 | ''' 文件夹压缩成ZIP文件 ''' 154 | if os.path.isdir(path): 155 | temp_dir = tempfile.gettempdir() 156 | zip_path = os.path.join(temp_dir, f"{os.path.basename(path) or 'ha'}.zip") 157 | with zipfile.ZipFile(zip_path, 'w') as zipf: 158 | for root, dirs, files in os.walk(path): 159 | for file in files: 160 | zipf.write(os.path.join(root, file), 161 | os.path.relpath(os.path.join(root, file), 162 | os.path.join(path, '..'))) 163 | return zip_path -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/http_api.py: -------------------------------------------------------------------------------- 1 | import os 2 | from homeassistant.components.http import HomeAssistantView 3 | from .file_api import delete_file, get_dir_list, mkdir, read_file, load_content, save_content, dir_to_zip 4 | from aiohttp import web 5 | from .manifest import manifest 6 | 7 | DOMAIN = manifest.domain 8 | API_URL = f"/{DOMAIN}-api" 9 | 10 | class HttpApi(HomeAssistantView): 11 | 12 | url = API_URL 13 | name = DOMAIN 14 | requires_auth = True 15 | 16 | # format path 17 | def get_config_path(self, path): 18 | config_path = path.lstrip('/') 19 | if config_path[:2] == './': 20 | config_path = config_path[2:] 21 | return config_path 22 | 23 | # get file list or file content 24 | async def get(self, request): 25 | hass = request.app["hass"] 26 | query = request.query 27 | act = query.get('act', '') 28 | config_path = self.get_config_path(query.get('path', '')) 29 | path = hass.config.path(config_path) 30 | if act == 'content': 31 | data = await hass.async_add_executor_job(load_content, path) 32 | return self.json({ 'code': 0, 'data': data}) 33 | elif act == 'download': 34 | if os.path.isdir(path): 35 | path = await hass.async_add_executor_job(dir_to_zip, path) 36 | if os.path.isfile(path): 37 | content = await hass.async_add_executor_job(read_file, path) 38 | return web.Response(body=content, headers={ 39 | 'Content-Disposition': f'attachment; filename="{os.path.basename(path)}"' 40 | }) 41 | 42 | data = await hass.async_add_executor_job(get_dir_list, path) 43 | return self.json(data) 44 | 45 | # delete file or folder 46 | async def delete(self, request): 47 | hass = request.app["hass"] 48 | query = request.query 49 | config_path = self.get_config_path(query.get('path', '')) 50 | path = hass.config.path(config_path) 51 | await hass.async_add_executor_job(delete_file, path) 52 | return self.json({ 'code': 0, 'msg': '删除成功'}) 53 | 54 | # add file or folder 55 | async def put(self, request): 56 | hass = request.app["hass"] 57 | query = request.query 58 | act = query.get('act', '') 59 | body = await request.json() 60 | config_path = self.get_config_path(body.get('path')) 61 | path = hass.config.path(config_path) 62 | 63 | # rename file or folder 64 | if act == 'rename': 65 | new_path = hass.config.path(self.get_config_path(body.get('new_path'))) 66 | if os.path.exists(new_path): 67 | return self.json({ 'code': 1, 'msg': '已存在相同名称'}) 68 | 69 | os.rename(path, new_path) 70 | return self.json({ 'code': 0, 'msg': '操作成功'}) 71 | 72 | # create file or folder 73 | if os.path.exists(path): 74 | return self.json({ 'code': 1, 'msg': '已存在相同名称'}) 75 | 76 | if act == 'file': 77 | await hass.async_add_executor_job(save_content, path, '') 78 | elif act == 'folder': 79 | await hass.async_add_executor_job(mkdir, path) 80 | 81 | return self.json({ 'code': 0, 'msg': '创建成功'}) 82 | 83 | async def post(self, request): 84 | # 文件限制调整到100MB 85 | request.app._client_max_size = 1024**2 * 100 86 | hass = request.app["hass"] 87 | # 上传文件 88 | query = request.query 89 | if query.get('path') is not None: 90 | config_path = self.get_config_path(query.get('path')) 91 | path = hass.config.path(config_path) 92 | dir_path = path[:path.rindex('/')] 93 | reader = await request.multipart() 94 | file = await reader.next() 95 | # print(file.filename) 96 | await hass.async_add_executor_job(mkdir, dir_path) 97 | # create file 98 | f = await hass.async_add_executor_job(open, path, 'wb') 99 | while True: 100 | chunk = await file.read_chunk() # 默认是8192个字节。 101 | if not chunk: 102 | break 103 | await hass.async_add_executor_job(f.write, chunk) 104 | f.close() 105 | 106 | return self.json({ 'code': 0, 'msg': '上传成功'}) 107 | else: 108 | body = await request.json() 109 | config_path = self.get_config_path(body.get('path')) 110 | path = hass.config.path(config_path) 111 | await hass.async_add_executor_job(save_content, path, body.get('data')) 112 | return self.json({ 'code': 0, 'msg': '保存成功'}) -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "ha_file_explorer", 3 | "name": "\u6587\u4EF6\u7BA1\u7406", 4 | "version": "2025.4.16", 5 | "config_flow": true, 6 | "documentation": "https://github.com/shaonianzhentan/ha_file_explorer", 7 | "requirements": [], 8 | "dependencies": [], 9 | "codeowners": [ 10 | "@shaonianzhentan" 11 | ] 12 | } -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/manifest.py: -------------------------------------------------------------------------------- 1 | import os 2 | from homeassistant.util.json import load_json 3 | 4 | CURRENT_PATH = os.path.dirname(__file__) 5 | 6 | class Manifest(): 7 | 8 | def __init__(self): 9 | self.manifest_path = f'{CURRENT_PATH}/manifest.json' 10 | self.update() 11 | 12 | def update(self): 13 | data = load_json(self.manifest_path, {}) 14 | self.domain = data.get('domain') 15 | self.name = data.get('name') 16 | self.version = data.get('version') 17 | self.documentation = data.get('documentation') 18 | 19 | manifest = Manifest() -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "File Explorer", 3 | "config": { 4 | "step": { 5 | "user": { 6 | "description": "", 7 | "data": { 8 | "require_admin": "Visible only to administrators" 9 | } 10 | } 11 | }, 12 | "error": {}, 13 | "abort": { 14 | "single_instance_allowed": "Single Instance Allowed." 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/translations/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Файловый менеджер", 3 | "config": { 4 | "step": { 5 | "user": { 6 | "description": "", 7 | "data": { 8 | "require_admin": "Виден только администраторам" 9 | } 10 | } 11 | }, 12 | "error": {}, 13 | "abort": { 14 | "single_instance_allowed": "Разрешен единственный экземпляр." 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/translations/zh-Hans.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "文件管理器", 3 | "config": { 4 | "step": { 5 | "user": { 6 | "description": "", 7 | "data": { 8 | "require_admin": "仅管理员可见" 9 | } 10 | } 11 | }, 12 | "error": {}, 13 | "abort": { 14 | "single_instance_allowed": "仅允许单个配置." 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/assets/AppLayout.de7f2f43.css: -------------------------------------------------------------------------------- 1 | .wrapper{position:relative;overflow:hidden;display:flex;flex-direction:column;height:100vh;width:100%}#va-app-bar-shadow{overflow:auto}.loading{position:fixed;width:100%;left:0px;bottom:2px;height:8px} 2 | -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/assets/AppLayout.e91c6a4e.js: -------------------------------------------------------------------------------- 1 | var A=Object.defineProperty,$=Object.defineProperties;var b=Object.getOwnPropertyDescriptors;var m=Object.getOwnPropertySymbols;var k=Object.prototype.hasOwnProperty,N=Object.prototype.propertyIsEnumerable;var p=(u,e,a)=>e in u?A(u,e,{enumerable:!0,configurable:!0,writable:!0,value:a}):u[e]=a,s=(u,e)=>{for(var a in e||(e={}))k.call(e,a)&&p(u,a,e[a]);if(m)for(var a of m(e))N.call(e,a)&&p(u,a,e[a]);return u},F=(u,e)=>$(u,b(e));import{n as y,d as S,a as n,o as x,f as Q,b as o,w as t,s as l,h as C,t as _,i as f,e as i,v as T,x as V}from"./index.5274938f.js";const d={en:{name:"File Explorer",file:"File",folder:"Folder",upload:"Upload",download:"Download",add:"Add",delete:"Delete",rename:"Rename",edit:"Edit",browse:"Browse",entities:"Entities",command:"Command",save:"Save",cancel:"Cancel",confirm:"Confirm",currentName:"Current Name",uploadTips:"Note: files with the same name will be overwritten",uploadSuccess(u,e){return`Successfully uploaded ${e} files, failed ${u-e} files`},newName(u){return`New ${u} Name`},downloadConfirm(u){return`Download\u3010${u}\u3011\uFF1F`},deleteConfirm(u){return`Confirm deletion\u3010${u}\u3011\uFF1F`}},cn:{name:"\u6587\u4EF6\u7BA1\u7406",file:"\u6587\u4EF6",folder:"\u6587\u4EF6\u5939",upload:"\u4E0A\u4F20",download:"\u4E0B\u8F7D",add:"\u65B0\u589E",delete:"\u5220\u9664",rename:"\u91CD\u547D\u540D",edit:"\u7F16\u8F91",browse:"\u6D4F\u89C8",entities:"\u5B9E\u4F53",command:"\u547D\u4EE4",save:"\u4FDD\u5B58",cancel:"\u53D6\u6D88",confirm:"\u786E\u5B9A",currentName:"\u5F53\u524D\u540D\u79F0",uploadTips:"\u6CE8\u610F\uFF1A\u76F8\u540C\u540D\u79F0\u6587\u4EF6\u4F1A\u88AB\u8986\u76D6",uploadSuccess(u,e){return`\u6210\u529F\u4E0A\u4F20${e}\u6587\u4EF6\uFF0C\u5931\u8D25${u-e}\u4E2A`},newName(u){return`\u65B0${u}\u540D\u79F0`},downloadConfirm(u){return`\u786E\u5B9A\u4E0B\u8F7D\u3010${u}\u3011\uFF1F`},deleteConfirm(u){return`\u786E\u5B9A\u5220\u9664\u3010${u}\u3011\uFF1F`}},ru:{name:"\u0424\u0430\u0439\u043B\u043E\u0432\u044B\u0439 \u043C\u0435\u043D\u0435\u0434\u0436\u0435\u0440",file:"\u0444\u0430\u0439\u043B",folder:"\u043F\u0430\u043F\u043A\u0430",upload:"\u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C",download:"\u0441\u043A\u0430\u0447\u0430\u0442\u044C",add:"\u0434\u043E\u0431\u0430\u0432\u0438\u0442\u044C",delete:"\u0443\u0434\u0430\u043B\u0438\u0442\u044C",rename:"\u043F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u0442\u044C",edit:"\u043F\u0440\u0430\u0432\u043A\u0430",browse:"\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440",entities:"\u043E\u0431\u044A\u0435\u043A\u0442",command:"\u043A\u043E\u043C\u0430\u043D\u0434\u0430",save:"\u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C",cancel:"\u043E\u0442\u043C\u0435\u043D\u0438\u0442\u044C",confirm:"\u043F\u043E\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044C",currentName:"\u0422\u0435\u043A\u0443\u0448\u0435\u0435 \u0438\u043C\u044F",uploadTips:"\u041F\u0440\u0438\u043C\u0435\u0447\u0430\u043D\u0438\u0435: \u0444\u0430\u0439\u043B\u044B \u0441 \u0442\u0430\u043A\u0438\u043C \u0436\u0435 \u0438\u043C\u0435\u043D\u0435\u043C \u0431\u0443\u0434\u0443\u0442 \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0438\u0441\u0430\u043D\u044B",uploadSuccess(u,e){return`\u0423\u0441\u043F\u0435\u0448\u043D\u043E \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043D\u043E ${e} \u0444\u0430\u0439\u043B\u043E\u0432, \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C ${u-e} \u0444\u0430\u0439\u043B\u043E\u0432`},newName(u){return`New ${u} Name`},downloadConfirm(u){return`\u0421\u043A\u0430\u0447\u0430\u0442\u044C\u3010${u}\u3011\uFF1F`},deleteConfirm(u){return`\u041F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u0443\u0434\u0430\u043B\u0435\u043D\u0438\u0435\u3010${u}\u3011\uFF1F`}}};let c=d.en;try{const u=parent.document.querySelector("home-assistant");u&&(u.hass.language.includes("zh-")?c=d.cn:u.hass.language=="ru"&&(c=d.ru))}catch{}var E=c;const z={class:"wrapper"},L=i("div",{class:"spacer"},null,-1),q={id:"va-app-bar-shadow"},R={class:"loading"},U={computed:s({},y(["loading"])),methods:{showQuickBarClick(u){this.api.showQuickBar(u)},menuClick(){this.api.fireEvent("hass-toggle-menu")}}},H=S(F(s({},U),{name:"AppLayout",setup(u){return(e,a)=>{const w=n("mdi-icon"),r=n("va-button"),D=n("va-button-group"),v=n("va-button-dropdown"),h=n("va-app-bar"),B=n("va-progress-bar");return x(),Q("div",z,[o(h,{"shadow-on-scroll":"","shadow-color":"primary",target:"#va-app-bar-shadow",style:{"z-index":"1","--va-app-bar-height":"56px"}},{default:t(()=>[o(r,{round:!0,onClick:e.menuClick,style:{"margin-left":"20px"}},{default:t(()=>[o(w,{name:"mdi-home-assistant",style:{color:"white"}})]),_:1},8,["onClick"]),l(e.$slots,"left"),L,l(e.$slots,"right"),o(v,{style:{"margin-right":"10px"}},{default:t(()=>[o(D,null,{default:t(()=>[o(r,{onClick:a[0]||(a[0]=g=>e.showQuickBarClick("e"))},{default:t(()=>[C(_(f(E).entities),1)]),_:1}),o(r,{onClick:a[1]||(a[1]=g=>e.showQuickBarClick("c"))},{default:t(()=>[C(_(f(E).command),1)]),_:1})]),_:1})]),_:1})]),_:3}),i("div",q,[l(e.$slots,"default"),i("div",R,[T(o(B,{indeterminate:""},null,512),[[V,e.loading]])])])])}}}));export{H as _,E as l}; 2 | -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/assets/editor.31b1244f.js: -------------------------------------------------------------------------------- 1 | var C=Object.defineProperty,b=Object.defineProperties;var y=Object.getOwnPropertyDescriptors;var c=Object.getOwnPropertySymbols;var F=Object.prototype.hasOwnProperty,g=Object.prototype.propertyIsEnumerable;var d=(o,e,t)=>e in o?C(o,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):o[e]=t,i=(o,e)=>{for(var t in e||(e={}))F.call(e,t)&&d(o,t,e[t]);if(c)for(var t of c(e))g.call(e,t)&&d(o,t,e[t]);return o},u=(o,e)=>b(o,y(e));import{d as k,p as B,y as D,a as f,z as $,o as h,c as m,w as a,b as _,h as r,t as l,v as V,i as p,e as H}from"./index.5274938f.js";import{_ as N,l as v}from"./AppLayout.e91c6a4e.js";const P=H("div",{id:"editor"},null,-1),S=k({data(){return{name:""}},computed:i({},B(["absolutePath"])),created(){},mounted(){this.loadData(),window.onbeforeunload=function(){return"\u786E\u5B9A\u79BB\u5F00\u5F53\u524D\u9875\u5417\uFF1F"}},beforeRouteLeave(){window.onbeforeunload=null},methods:{loadData(){const{name:o}=this.$route.params;if(!o)return this.$router.replace("/");this.name=o;const e=this.absolutePath(o),t=document.querySelector("#editor");t.innerHTML="",this.api.service.getHassFileContent(e).then(({code:s,data:n})=>{s>0||(t.textContent=n,window.editor=window.ace.edit("editor",{theme:"ace/theme/chrome",mode:D(this.name)}),document.body.scrollIntoView())})},cancelClick(){this.$router.back()},saveClick(){const o=this.absolutePath(this.name);let e=window.editor.getValue();this.api.service.setHassFileContent(o,e).then(t=>{this.$toast(t.msg)})}}}),q=k(u(i({},S),{name:"editor",setup(o){return(e,t)=>{const s=f("va-chip"),n=f("va-button"),w=$("shortkey");return h(),m(N,{class:"views-editor"},{left:a(()=>[_(s,{flat:"",color:"#fff"},{default:a(()=>[r(l(e.name),1)]),_:1})]),right:a(()=>[V((h(),m(n,{color:"#fff",flat:"",rounded:!1,onShortkey:t[0]||(t[0]=x=>e.saveClick()),onClick:e.saveClick},{default:a(()=>[r(l(p(v).save),1)]),_:1},8,["onClick"])),[[w,["ctrl","s"]]]),_(n,{color:"#fff",flat:"",rounded:!1,onClick:e.cancelClick},{default:a(()=>[r(l(p(v).cancel),1)]),_:1},8,["onClick"])]),default:a(()=>[P]),_:1})}}}));export{q as default}; 2 | -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/assets/editor.eba3ec12.css: -------------------------------------------------------------------------------- 1 | .views-editor #editor{width:100%;height:calc(100vh - 56px)}.views-editor .ace_print-margin-layer{display:none} 2 | -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/assets/index.abe45c59.js: -------------------------------------------------------------------------------- 1 | var W=Object.defineProperty,X=Object.defineProperties;var Y=Object.getOwnPropertyDescriptors;var R=Object.getOwnPropertySymbols;var Z=Object.prototype.hasOwnProperty,ee=Object.prototype.propertyIsEnumerable;var q=(l,e,i)=>e in l?W(l,e,{enumerable:!0,configurable:!0,writable:!0,value:i}):l[e]=i,L=(l,e)=>{for(var i in e||(e={}))Z.call(e,i)&&q(l,i,e[i]);if(R)for(var i of R(e))ee.call(e,i)&&q(l,i,e[i]);return l},z=(l,e)=>X(l,Y(e));import{d as D,r as I,a as o,o as _,c as y,w as t,b as a,e as U,f as P,g as j,F as A,h as d,u as B,t as s,i as r,j as te,k as H,_ as T,l as E,m as ae,n as M,p as K,q as N}from"./index.5274938f.js";import{l as n,_ as le}from"./AppLayout.e91c6a4e.js";const oe={class:"va-table"},ne=["href"],se={key:1},ie=d(" OK "),ce=D({name:"VersionInfo",props:["ok"],setup(l){const e=l;B();const i="File Explorer",c=I(!0),F=new URLSearchParams(location.search),V=I([{name:"Version",value:F.get("v")||"dev"},{name:"Author",value:"shaonianzhentan"},{name:"Link",value:"https://github.com/shaonianzhentan/ha_file_explorer",type:"link"},{name:"UI",value:"https://vuestic.dev",type:"link"},{name:"Icon",value:"https://unpkg.com/@mdi/font@latest/preview.html",type:"link"},{name:"Bilibili",value:"https://space.bilibili.com/39523884",type:"link"},{name:"HA notes",value:"https://ha.jiluxinqing.com",type:"link"}]),m=async()=>{e.ok({})};return(b,k)=>{const g=o("va-button"),$=o("va-modal");return _(),y($,{modelValue:c.value,"onUpdate:modelValue":k[0]||(k[0]=u=>c.value=u),title:i,"hide-default-actions":!0},{footer:t(()=>[a(g,{onClick:m},{default:t(()=>[ie]),_:1})]),default:t(()=>[U("table",oe,[U("tbody",null,[(_(!0),P(A,null,j(V.value,u=>(_(),P("tr",{key:u.name},[U("td",null,s(u.name),1),U("td",null,[u.type==="link"?(_(),P("a",{key:0,href:u.value,target:"_blank"},s(u.value),9,ne)):(_(),P("span",se,s(u.value),1))])]))),128))])])]),_:1},8,["modelValue"])}}}),re={},O=D(z(L({},re),{name:"CreateFile",props:["type","ok","cancel","app"],setup(l){const e=l,i=B(),c=e.type==="file",F=c?n.file:n.folder,V=e.app(),m=I(""),b=I(!0),k=()=>{e.cancel()},g=async()=>{if(!m.value)return;const $=i.getters.absolutePath(m.value);let u=await H.service.createHassFile(c?"file":"folder",$);V.$toast(u.msg),!(u.code>0)&&(i.dispatch("reloadFileList"),e.ok({}))};return($,u)=>{const p=o("va-input"),f=o("va-button"),v=o("va-modal");return _(),y(v,{modelValue:b.value,"onUpdate:modelValue":u[1]||(u[1]=w=>b.value=w),title:r(F),"hide-default-actions":!0},{footer:t(()=>[a(f,{outline:"",onClick:k,style:{"margin-right":"20px"}},{default:t(()=>[d(s(r(n).cancel),1)]),_:1}),a(f,{onClick:g,style:{"margin-left":"20px"}},{default:t(()=>[d(s(r(n).confirm),1)]),_:1})]),default:t(()=>[a(p,{modelValue:m.value,"onUpdate:modelValue":u[0]||(u[0]=w=>m.value=w),modelModifiers:{trim:!0},onKeypress:te(g,["enter"]),placeholder:r(n).newName(r(F))},null,8,["modelValue","onKeypress","placeholder"])]),_:1},8,["modelValue","title"])}}}));const ue={class:"file-panel"},de={},_e=D(z(L({},de),{name:"UploadFile",props:["type","ok","cancel","app"],setup(l){const e=l,i=B(),c=e.type==="file",F=`${n.upload} ${c?n.file:n.folder}`,V=e.app(),m=I([]),b=I(!0),k=I(!1),g=p=>{console.log(p.target.files);const f=[];for(let v of p.target.files)f.push(v);m.value=f},$=()=>{e.cancel()},u=async()=>{if(m.value.length===0)return;k.value=!0;let p=0;const f=m.value.length;for(let v=0;v{const v=o("va-progress-bar"),w=o("va-alert"),h=o("va-file-upload"),C=o("va-button"),S=o("va-modal");return _(),y(S,{modelValue:b.value,"onUpdate:modelValue":f[1]||(f[1]=x=>b.value=x),title:F,"hide-default-actions":!0},{footer:t(()=>[a(C,{disabled:k.value,outline:"",onClick:$,style:{"margin-right":"20px"}},{default:t(()=>[d(s(r(n).cancel),1)]),_:1},8,["disabled"]),a(C,{disabled:k.value,onClick:u,style:{"margin-left":"20px"}},{default:t(()=>[d(s(r(n).confirm),1)]),_:1},8,["disabled"])]),default:t(()=>[k.value?(_(),y(v,{key:0,indeterminate:""})):E("",!0),a(w,{color:"danger",class:"mb-4"},{default:t(()=>[d(s(r(n).uploadTips),1)]),_:1}),c?(_(),y(h,{key:1,disabled:k.value,modelValue:m.value,"onUpdate:modelValue":f[0]||(f[0]=x=>m.value=x)},null,8,["disabled","modelValue"])):(_(),P("input",{key:2,type:"file",onChange:g,webkitdirectory:""},null,32)),U("div",ue,[U("ol",null,[(_(!0),P(A,null,j(m.value,(x,Q)=>(_(),P("li",{key:Q},s(x.webkitRelativePath),1))),128))])])]),_:1},8,["modelValue"])}}}));var G=T(_e,[["__scopeId","data-v-47b7be92"]]);const pe={},J=D(z(L({},pe),{name:"RenameFile",props:["type","name","ok","cancel","app"],setup(l){const e=l,i=B(),c=e.type==="file",F=c?n.file:n.folder,V=ae(()=>{if(c)return e.name;const p=i.getters.absolutePath(e.name).split("/");return p.splice(p.length-1,1),p[p.length-1]}),m=e.app(),b=I(""),k=I(!0),g=()=>{e.cancel()},$=async()=>{if(!b.value)return;let u=i.getters.absolutePath(e.name),p=i.getters.absolutePath(b.value);if(!c){const v=u.split("/");v.splice(v.length-1,1),u=v.join("/"),v[v.length-1]=b.value,p=v.join("/")}let f=await H.service.rename(u,p);m.$toast(f.msg),!(f.code>0)&&(i.dispatch("reloadFileList",c?"":p),e.ok({}))};return(u,p)=>{const f=o("va-alert"),v=o("va-input"),w=o("va-button"),h=o("va-modal");return _(),y(h,{modelValue:k.value,"onUpdate:modelValue":p[1]||(p[1]=C=>k.value=C),title:r(F),"hide-default-actions":!0},{footer:t(()=>[a(w,{outline:"",onClick:g,style:{"margin-right":"20px"}},{default:t(()=>[d(s(r(n).cancel),1)]),_:1}),a(w,{onClick:$,style:{"margin-left":"20px"}},{default:t(()=>[d(s(r(n).confirm),1)]),_:1})]),default:t(()=>[a(f,{outline:""},{default:t(()=>[d(s(r(n).currentName)+"\uFF1A"+s(r(V)),1)]),_:1}),a(v,{modelValue:b.value,"onUpdate:modelValue":p[0]||(p[0]=C=>b.value=C),modelModifiers:{trim:!0},placeholder:r(n).newName(r(F))},null,8,["modelValue","placeholder"])]),_:1},8,["modelValue","title"])}}})),me=["onClick"],ve={computed:z(L(L({},M(["fileList"])),K(["absolutePath"])),{isLocalDir(){return this.absolutePath("")==="./www/"}}),methods:z(L({},N(["reloadFileList"])),{deleteClick(l){parent.confirm(n.deleteConfirm(l))&&this.api.service.deleteHassFile(this.absolutePath(l)).then(e=>{this.reloadFileList(),this.$toast(e.msg)})},browseClick(l){window.open(`/local/${l}`)},renameClick(l){this.$dialog(J,{type:"file",name:l})},editClick(l){let e=l.lastIndexOf(".");if(e>0){let i=l.substring(e+1);if(["db","db-shm","db-wal","pyc"].includes(i)){this.$toast("\u6587\u4EF6\u683C\u5F0F\u4E0D\u652F\u6301\u7F16\u8F91");return}}this.$router.push({name:"editor",params:{name:l}})},addClick(){this.$dialog(O,{type:"file"})},uploadClick(){this.$dialog(G,{type:"file"})},downloadClick(l){this.api.service.downloadFile(this.absolutePath(l)).then(e=>{this.$toast("Download Success")})}})},fe=D(z(L({},ve),{name:"FileList",setup(l){return(e,i)=>{const c=o("va-button"),F=o("va-button-group"),V=o("va-button-dropdown"),m=o("va-card-title"),b=o("mdi-icon"),k=o("va-avatar"),g=o("va-list-item-section"),$=o("va-list-item-label"),u=o("va-chip"),p=o("va-list-item"),f=o("va-list"),v=o("va-card-content"),w=o("va-card");return _(),y(w,null,{default:t(()=>[a(m,null,{default:t(()=>[d(s(r(n).file)+" ",1),a(V,{outline:"",size:"small"},{default:t(()=>[a(F,{size:"small"},{default:t(()=>[a(c,{onClick:e.uploadClick},{default:t(()=>[d(s(r(n).upload),1)]),_:1},8,["onClick"]),a(c,{onClick:e.addClick},{default:t(()=>[d(s(r(n).add),1)]),_:1},8,["onClick"])]),_:1})]),_:1})]),_:1}),a(v,null,{default:t(()=>[a(f,{style:{"padding-top":"0"}},{default:t(()=>[(_(!0),P(A,null,j(e.fileList,(h,C)=>(_(),y(p,{key:C},{default:t(()=>[a(g,{avatar:""},{default:t(()=>[a(k,{color:"var(--va-primary)"},{default:t(()=>[a(b,{name:h.icon},null,8,["name"])]),_:2},1024)]),_:2},1024),a(g,null,{default:t(()=>[a($,null,{default:t(()=>[U("a",{href:"javascript:;",onClick:S=>e.editClick(h.name),class:"link"},s(h.name),9,me)]),_:2},1024),a($,{caption:""},{default:t(()=>[d(s(h.time)+" ",1),h.size?(_(),y(u,{key:0,flat:"",size:"small"},{default:t(()=>[d(s(h.size),1)]),_:2},1024)):E("",!0)]),_:2},1024)]),_:2},1024),a(g,{icon:""},{default:t(()=>[a(V,{size:"small"},{default:t(()=>[a(F,{outline:"",size:"small"},{default:t(()=>[e.isLocalDir?(_(),y(c,{key:0,onClick:S=>e.browseClick(h.name)},{default:t(()=>[d(s(r(n).browse),1)]),_:2},1032,["onClick"])):E("",!0),a(c,{onClick:S=>e.downloadClick(h.name)},{default:t(()=>[d(s(r(n).download),1)]),_:2},1032,["onClick"]),a(c,{onClick:S=>e.renameClick(h.name)},{default:t(()=>[d(s(r(n).rename),1)]),_:2},1032,["onClick"]),a(c,{onClick:S=>e.deleteClick(h.name)},{default:t(()=>[d(s(r(n).delete),1)]),_:2},1032,["onClick"]),a(c,{onClick:S=>e.editClick(h.name)},{default:t(()=>[d(s(r(n).edit),1)]),_:2},1032,["onClick"])]),_:2},1024)]),_:2},1024)]),_:2},1024)]),_:2},1024))),128))]),_:1})]),_:1})]),_:1})}}}));const he=["src","onError"],ke={data(){return{brands:new Array}},computed:L(L({},M(["folderList","path"])),K(["pathList"])),methods:z(L({},N(["getFileList"])),{showClick(l){this.getFileList(l.name)},addClick(){this.$dialog(O,{type:"dir"})},uploadClick(){this.$dialog(G,{type:"dir"})},deleteClick(){const{pathList:l}=this,{name:e}=l[l.length-1];parent.confirm(n.deleteConfirm(e))&&this.api.service.deleteHassFile(this.path).then(i=>{this.getFileList(l.length-2),this.$toast(i.msg)})},renameClick(){this.$dialog(J,{type:"dir",name:""})},downloadClick(){const{pathList:l}=this,{name:e}=l[l.length-1];parent.confirm(n.downloadConfirm(e))&&this.api.service.downloadFile(this.path).then(i=>{this.$toast("Download Success")})},loadSrc(l){return this.brands.includes(l)?"https://brands.home-assistant.io/_/homeassistant/icon.png":l},loadIcon(l,e){this.brands.push(e),l.target.src="https://brands.home-assistant.io/_/homeassistant/icon.png"}})},Ce=D(z(L({},ke),{name:"FolderList",setup(l){return(e,i)=>{const c=o("va-button"),F=o("va-button-group"),V=o("va-button-dropdown"),m=o("va-card-title"),b=o("mdi-icon"),k=o("va-avatar"),g=o("va-list-item-section"),$=o("va-list-item-label"),u=o("va-chip"),p=o("va-icon"),f=o("va-list-item"),v=o("va-list"),w=o("va-card-content"),h=o("va-card");return _(),y(h,null,{default:t(()=>[a(m,null,{default:t(()=>[d(s(r(n).folder)+" ",1),a(V,{outline:"",size:"small"},{default:t(()=>[a(F,{size:"small"},{default:t(()=>[e.pathList.length>1?(_(),y(c,{key:0,onClick:e.renameClick},{default:t(()=>[d(s(r(n).rename),1)]),_:1},8,["onClick"])):E("",!0),a(c,{onClick:e.uploadClick},{default:t(()=>[d(s(r(n).upload),1)]),_:1},8,["onClick"]),a(c,{onClick:e.downloadClick},{default:t(()=>[d(s(r(n).download),1)]),_:1},8,["onClick"]),e.pathList.length>1?(_(),y(c,{key:1,onClick:e.deleteClick},{default:t(()=>[d(s(r(n).delete),1)]),_:1},8,["onClick"])):E("",!0),a(c,{onClick:e.addClick},{default:t(()=>[d(s(r(n).add),1)]),_:1},8,["onClick"])]),_:1})]),_:1})]),_:1}),a(w,null,{default:t(()=>[a(v,{style:{"padding-top":"0"}},{default:t(()=>[(_(!0),P(A,null,j(e.folderList,(C,S)=>(_(),y(f,{key:S},{default:t(()=>[a(g,{avatar:""},{default:t(()=>[C.iconType=="img"?(_(),P("img",{key:0,class:"brands",src:e.loadSrc(C.icon),onError:x=>e.loadIcon(x,C.icon)},null,40,he)):(_(),y(k,{key:1,color:"var(--va-primary)"},{default:t(()=>[a(b,{name:C.icon},null,8,["name"])]),_:2},1024))]),_:2},1024),a(g,null,{default:t(()=>[a($,null,{default:t(()=>[d(s(C.name),1)]),_:2},1024),a($,{caption:""},{default:t(()=>[d(s(C.time)+" ",1),C.size?(_(),y(u,{key:0,flat:"",size:"small"},{default:t(()=>[d(s(C.size),1)]),_:2},1024)):E("",!0)]),_:2},1024)]),_:2},1024),a(g,{icon:""},{default:t(()=>[a(p,{name:"remove_red_eye",color:"gray",onClick:x=>e.showClick(C)},null,8,["onClick"])]),_:2},1024)]),_:2},1024))),128))]),_:1})]),_:1})]),_:1})}}}));var be=T(Ce,[["__scopeId","data-v-6489c322"]]);const ge={class:"row"},ye={class:"flex md6"},$e={class:"flex md6"},we={computed:L({},K(["pathList"])),created(){this.changePathClick(0)},methods:z(L({},N(["getFileList"])),{changePathClick(l){this.getFileList(l)},versionClick(){this.$dialog(ce)}})},ze=D(z(L({},we),{name:"index",setup(l){return(e,i)=>{const c=o("va-button"),F=o("va-breadcrumbs-item"),V=o("va-breadcrumbs"),m=o("va-card-title"),b=o("va-card"),k=o("va-backtop");return _(),y(le,{class:"views-index"},{left:t(()=>[a(c,{color:"#fff",flat:"",rounded:!1,onClick:e.versionClick},{default:t(()=>[d(s(r(n).name),1)]),_:1},8,["onClick"])]),right:t(()=>[]),default:t(()=>[a(b,null,{default:t(()=>[a(m,null,{default:t(()=>[a(V,null,{default:t(()=>[(_(!0),P(A,null,j(e.pathList,(g,$)=>(_(),y(F,{label:g.name,key:$,onClick:u=>e.changePathClick($)},null,8,["label","onClick"]))),128))]),_:1})]),_:1})]),_:1}),U("div",ge,[U("div",ye,[a(be)]),U("div",$e,[a(fe)])]),a(k,{target:"#va-app-bar-shadow","vertical-offset":"20px","horizontal-offset":"20px","visibility-height":1,speed:50})]),_:1})}}}));export{ze as default}; 2 | -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/assets/index.ad1752d2.css: -------------------------------------------------------------------------------- 1 | .file-panel[data-v-47b7be92]{height:100px;overflow:auto}.brands[data-v-6489c322]{width:48px;height:48px}.views-index #va-app-bar-shadow{padding:5px 0 5px 5px}.views-index .va-card{margin:5px}.views-index .va-breadcrumb-item__label{cursor:pointer}.views-index .md6{width:100%}.views-index .md6 .va-card__title{justify-content:space-between;padding-right:40px;padding-left:30px}.views-index .va-list-item:hover{background:#eee}.views-index .va-list-item .va-list-item-label--caption{display:flex;align-items:center} 2 | -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/assets/material-icons-outlined.755ef314.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/ha_file_explorer/8549e33e575b673709413a9fa74f8c99ba120331/custom_components/ha_file_explorer/www/assets/material-icons-outlined.755ef314.woff -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/assets/material-icons-outlined.828c436d.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/ha_file_explorer/8549e33e575b673709413a9fa74f8c99ba120331/custom_components/ha_file_explorer/www/assets/material-icons-outlined.828c436d.woff2 -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/assets/material-icons-round.136587d6.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/ha_file_explorer/8549e33e575b673709413a9fa74f8c99ba120331/custom_components/ha_file_explorer/www/assets/material-icons-round.136587d6.woff -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/assets/material-icons-round.9fcaafe6.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/ha_file_explorer/8549e33e575b673709413a9fa74f8c99ba120331/custom_components/ha_file_explorer/www/assets/material-icons-round.9fcaafe6.woff2 -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/assets/material-icons-sharp.84289da0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/ha_file_explorer/8549e33e575b673709413a9fa74f8c99ba120331/custom_components/ha_file_explorer/www/assets/material-icons-sharp.84289da0.woff2 -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/assets/material-icons-sharp.914e06ba.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/ha_file_explorer/8549e33e575b673709413a9fa74f8c99ba120331/custom_components/ha_file_explorer/www/assets/material-icons-sharp.914e06ba.woff -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/assets/material-icons-two-tone.517cb22e.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/ha_file_explorer/8549e33e575b673709413a9fa74f8c99ba120331/custom_components/ha_file_explorer/www/assets/material-icons-two-tone.517cb22e.woff -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/assets/material-icons-two-tone.86555c87.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/ha_file_explorer/8549e33e575b673709413a9fa74f8c99ba120331/custom_components/ha_file_explorer/www/assets/material-icons-two-tone.86555c87.woff2 -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/assets/material-icons.18d2477b.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/ha_file_explorer/8549e33e575b673709413a9fa74f8c99ba120331/custom_components/ha_file_explorer/www/assets/material-icons.18d2477b.woff2 -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/assets/material-icons.66dca61a.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/ha_file_explorer/8549e33e575b673709413a9fa74f8c99ba120331/custom_components/ha_file_explorer/www/assets/material-icons.66dca61a.woff -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/assets/materialdesignicons-webfont.12845dec.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/ha_file_explorer/8549e33e575b673709413a9fa74f8c99ba120331/custom_components/ha_file_explorer/www/assets/materialdesignicons-webfont.12845dec.woff2 -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/assets/materialdesignicons-webfont.55c35c20.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/ha_file_explorer/8549e33e575b673709413a9fa74f8c99ba120331/custom_components/ha_file_explorer/www/assets/materialdesignicons-webfont.55c35c20.eot -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/assets/materialdesignicons-webfont.5e9b23f9.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/ha_file_explorer/8549e33e575b673709413a9fa74f8c99ba120331/custom_components/ha_file_explorer/www/assets/materialdesignicons-webfont.5e9b23f9.ttf -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/assets/materialdesignicons-webfont.d87b64d8.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/ha_file_explorer/8549e33e575b673709413a9fa74f8c99ba120331/custom_components/ha_file_explorer/www/assets/materialdesignicons-webfont.d87b64d8.woff -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/assets/source-sans-pro-all-400-normal.56cfd2ea.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/ha_file_explorer/8549e33e575b673709413a9fa74f8c99ba120331/custom_components/ha_file_explorer/www/assets/source-sans-pro-all-400-normal.56cfd2ea.woff -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/assets/source-sans-pro-cyrillic-400-normal.0acd59e1.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/ha_file_explorer/8549e33e575b673709413a9fa74f8c99ba120331/custom_components/ha_file_explorer/www/assets/source-sans-pro-cyrillic-400-normal.0acd59e1.woff2 -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/assets/source-sans-pro-cyrillic-ext-400-normal.4ba425fa.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/ha_file_explorer/8549e33e575b673709413a9fa74f8c99ba120331/custom_components/ha_file_explorer/www/assets/source-sans-pro-cyrillic-ext-400-normal.4ba425fa.woff2 -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/assets/source-sans-pro-greek-400-normal.9755c83c.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/ha_file_explorer/8549e33e575b673709413a9fa74f8c99ba120331/custom_components/ha_file_explorer/www/assets/source-sans-pro-greek-400-normal.9755c83c.woff2 -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/assets/source-sans-pro-greek-ext-400-normal.f2a19d8c.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/ha_file_explorer/8549e33e575b673709413a9fa74f8c99ba120331/custom_components/ha_file_explorer/www/assets/source-sans-pro-greek-ext-400-normal.f2a19d8c.woff2 -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/assets/source-sans-pro-latin-400-normal.c124c88c.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/ha_file_explorer/8549e33e575b673709413a9fa74f8c99ba120331/custom_components/ha_file_explorer/www/assets/source-sans-pro-latin-400-normal.c124c88c.woff2 -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/assets/source-sans-pro-latin-ext-400-normal.ee519845.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/ha_file_explorer/8549e33e575b673709413a9fa74f8c99ba120331/custom_components/ha_file_explorer/www/assets/source-sans-pro-latin-ext-400-normal.ee519845.woff2 -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/assets/source-sans-pro-vietnamese-400-normal.6e0839c2.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/ha_file_explorer/8549e33e575b673709413a9fa74f8c99ba120331/custom_components/ha_file_explorer/www/assets/source-sans-pro-vietnamese-400-normal.6e0839c2.woff2 -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/ha_file_explorer/8549e33e575b673709413a9fa74f8c99ba120331/custom_components/ha_file_explorer/www/favicon.ico -------------------------------------------------------------------------------- /custom_components/ha_file_explorer/www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 文件管理器 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 文件管理器 9 | 10 | 11 | 12 |
13 | 14 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vue-tsc --noEmit && vite build", 8 | "test": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@fontsource/source-sans-pro": "^4.5.10", 13 | "@mdi/font": "^6.7.96", 14 | "material-icons": "^1.11.5", 15 | "sass": "^1.52.2", 16 | "update": "^0.7.4", 17 | "vue": "^3.2.36", 18 | "vue-router": "^4.0.15", 19 | "vue-three-shortkey": "^4.0.1", 20 | "vuestic-ui": "^1.4.1", 21 | "vuex": "^4.0.2" 22 | }, 23 | "devDependencies": { 24 | "@vitejs/plugin-vue": "^2.3.3", 25 | "typescript": "^4.7.3", 26 | "vite": "^2.9.15", 27 | "vue-tsc": "^0.35.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/ha_file_explorer/8549e33e575b673709413a9fa74f8c99ba120331/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /frontend/src/api/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import service from './service'; 3 | import { querySelector } from '../utils/query' 4 | 5 | class API { 6 | constructor() { 7 | // console.log('api') 8 | this.hassFullScreen() 9 | } 10 | 11 | get service() { 12 | return service 13 | } 14 | 15 | async hassFullScreen() { 16 | const subpage = querySelector(parent.document, 'hass-subpage') 17 | let iframe = subpage.querySelector("iframe"); 18 | let toolbar = subpage.shadowRoot.querySelector('.toolbar') 19 | subpage.shadowRoot.querySelector('.content').style.height = '100vh' 20 | iframe.style.position = 'absolute' 21 | toolbar.style.display = 'none' 22 | iframe.style.top = '0' 23 | iframe.style.height = '100%' 24 | } 25 | 26 | fireEvent(type: string, data = {}, ele: any = null) { 27 | const win = parent as any 28 | const event = new win.Event(type, { 29 | bubbles: true, 30 | cancelable: false, 31 | composed: true 32 | }); 33 | event.detail = data; 34 | if (!ele) { 35 | ele = querySelector(win.document, 'home-assistant-main') 36 | } 37 | ele.dispatchEvent(event); 38 | } 39 | 40 | showQuickBar(key: string) { 41 | const event = new (parent as any).Event("hass-quick-bar-trigger", { 42 | bubbles: true, 43 | cancelable: false, 44 | composed: true 45 | }); 46 | event.detail = { 47 | key, 48 | composedPath() { 49 | return [{ 50 | tagName: '', 51 | parentElement: { 52 | tagName: '' 53 | } 54 | }] 55 | }, 56 | preventDefault(){} 57 | }; 58 | // console.log(kb) 59 | parent.window.dispatchEvent(event); 60 | } 61 | } 62 | const api = new API() 63 | export default api -------------------------------------------------------------------------------- /frontend/src/api/service.ts: -------------------------------------------------------------------------------- 1 | import store from '../store/index' 2 | async function requestApi(data: any, query = {} as any, method = 'post') { 3 | store.commit('loading', true) 4 | const ha = parent.document.querySelector('home-assistant') as any 5 | const params = { method } as any; 6 | if (toString.call(data) === '[object FormData]') { 7 | params.body = data 8 | } else if (data) { 9 | params.body = JSON.stringify(data) 10 | } 11 | if (method == 'get') { 12 | query['t'] = Date.now() 13 | } 14 | const res = await ha.hass.fetchWithAuth('/ha_file_explorer-api?' + new URLSearchParams(query).toString(), params) 15 | .finally(() => { 16 | store.commit('loading', false) 17 | }) 18 | // 判断是否下载 19 | if (method == 'get' && query.act == 'download') { 20 | const blob = await res.blob() 21 | const url = window.URL.createObjectURL(blob); 22 | const a = document.createElement('a'); 23 | a.href = url; 24 | // 获取文件名 25 | const contentDisposition = res.headers.get('Content-Disposition'); 26 | let filename = 'downloaded_file'; 27 | if (contentDisposition) { 28 | const match = contentDisposition.match(/filename="?([^"]+)"?/); 29 | if (match) { 30 | filename = match[1]; 31 | } 32 | } 33 | a.download = filename; 34 | a.click(); 35 | window.URL.revokeObjectURL(url); 36 | return {} 37 | } 38 | return res.json() 39 | } 40 | 41 | 42 | const service = new Proxy({ 43 | /** 44 | * 获取文件列表 45 | * @returns 46 | */ 47 | getHassFileList(path: string) { 48 | return requestApi(null, { act: 'get', path }, 'get') 49 | }, 50 | /** 51 | * 获取文件内容 52 | * @param path 53 | * @returns 54 | */ 55 | getHassFileContent(path: string) { 56 | return requestApi(null, { act: 'content', path }, 'get') 57 | }, 58 | /** 59 | * 设置文件内容 60 | * @param path 61 | * @returns 62 | */ 63 | setHassFileContent(path: string, data: string) { 64 | return requestApi({ path, data }, {}, 'post') 65 | }, 66 | /** 67 | * 设置文件内容 68 | * @param path 69 | * @returns 70 | */ 71 | uploadFile(path: string, file: File) { 72 | const formData = new FormData(); 73 | formData.append("file", file); 74 | return requestApi(formData, { path }, 'post') 75 | }, 76 | /** 77 | * create dir in hass config dir 78 | * @param path 79 | * @returns 80 | */ 81 | createHassFile(act: string, path: string) { 82 | return requestApi({ path }, { act }, 'put') 83 | }, 84 | rename(path: string, new_path: string) { 85 | return requestApi({ path, new_path }, { act: 'rename' }, 'put') 86 | }, 87 | deleteHassFile(path: string) { 88 | return requestApi(null, { path }, 'delete') 89 | }, 90 | downloadFile(path: string) { 91 | return requestApi(null, { act: 'download', path }, 'get') 92 | } 93 | }, { 94 | get(target: any, property: string) { 95 | // console.log(target, property) 96 | if (property in target) { 97 | return target[property] 98 | } else { 99 | throw new Error(`${property} not in target`) 100 | } 101 | } 102 | }) 103 | 104 | 105 | export default service as Service -------------------------------------------------------------------------------- /frontend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/ha_file_explorer/8549e33e575b673709413a9fa74f8c99ba120331/frontend/src/assets/logo.png -------------------------------------------------------------------------------- /frontend/src/components/dialogs/CreateFile.vue: -------------------------------------------------------------------------------- 1 | 28 | 41 | 45 | -------------------------------------------------------------------------------- /frontend/src/components/dialogs/RenameFile.vue: -------------------------------------------------------------------------------- 1 | 47 | 63 | 67 | -------------------------------------------------------------------------------- /frontend/src/components/dialogs/UploadFile.vue: -------------------------------------------------------------------------------- 1 | 54 | 77 | 81 | -------------------------------------------------------------------------------- /frontend/src/components/dialogs/VersionInfo.vue: -------------------------------------------------------------------------------- 1 | 57 | -------------------------------------------------------------------------------- /frontend/src/components/globel/mdi-icon.vue: -------------------------------------------------------------------------------- 1 | 4 | 7 | -------------------------------------------------------------------------------- /frontend/src/components/layouts/AppLayout.vue: -------------------------------------------------------------------------------- 1 | 4 | 31 | 47 | -------------------------------------------------------------------------------- /frontend/src/components/list/FileList.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 59 | -------------------------------------------------------------------------------- /frontend/src/components/list/FolderList.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 56 | 57 | 123 | -------------------------------------------------------------------------------- /frontend/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/locales/index.ts: -------------------------------------------------------------------------------- 1 | const language = { 2 | en: { 3 | name: 'File Explorer', 4 | file: 'File', 5 | folder: 'Folder', 6 | upload: 'Upload', 7 | download: 'Download', 8 | add: 'Add', 9 | delete: 'Delete', 10 | rename: 'Rename', 11 | edit: 'Edit', 12 | browse: 'Browse', 13 | entities: 'Entities', 14 | command: 'Command', 15 | save: 'Save', 16 | cancel: 'Cancel', 17 | confirm: 'Confirm', 18 | currentName: 'Current Name', 19 | uploadTips: 'Note: files with the same name will be overwritten', 20 | uploadSuccess(fileCount: number, uploadCount: number) { 21 | return `Successfully uploaded ${uploadCount} files, failed ${fileCount - uploadCount} files` 22 | }, 23 | newName(type: string) { 24 | return `New ${type} Name` 25 | }, 26 | downloadConfirm(name: string) { 27 | return `Download【${name}】?` 28 | }, 29 | deleteConfirm(name: string) { 30 | return `Confirm deletion【${name}】?` 31 | } 32 | }, 33 | cn: { 34 | name: '文件管理', 35 | file: '文件', 36 | folder: '文件夹', 37 | upload: '上传', 38 | download: '下载', 39 | add: '新增', 40 | delete: '删除', 41 | rename: '重命名', 42 | edit: '编辑', 43 | browse: '浏览', 44 | entities: '实体', 45 | command: '命令', 46 | save: '保存', 47 | cancel: '取消', 48 | confirm: '确定', 49 | currentName: '当前名称', 50 | uploadTips: '注意:相同名称文件会被覆盖', 51 | uploadSuccess(fileCount: number, uploadCount: number) { 52 | return `成功上传${uploadCount}文件,失败${fileCount - uploadCount}个` 53 | }, 54 | newName(type: string) { 55 | return `新${type}名称` 56 | }, 57 | downloadConfirm(name: string) { 58 | return `确定下载【${name}】?` 59 | }, 60 | deleteConfirm(name: string) { 61 | return `确定删除【${name}】?` 62 | } 63 | }, 64 | ru: { 65 | name: "Файловый менеджер", 66 | file: "файл", 67 | folder: "папка", 68 | upload: "загрузить", 69 | download: 'скачать', 70 | add: "добавить", 71 | delete: "удалить", 72 | rename: "переименовать", 73 | edit: "правка", 74 | browse: "просмотр", 75 | entities: "объект", 76 | command: "команда", 77 | save: "сохранить", 78 | cancel: "отменить", 79 | confirm: "потвердить", 80 | currentName: "Текушее имя", 81 | uploadTips: "Примечание: файлы с таким же именем будут перезаписаны", 82 | uploadSuccess(fileCount: number, uploadCount: number) { 83 | return `Успешно загружено ${uploadCount} файлов, не удалось ${fileCount - uploadCount} файлов` 84 | }, 85 | newName(type: string) { 86 | return `New ${type} Name` 87 | }, 88 | downloadConfirm(name: string) { 89 | return `Скачать【${name}】?` 90 | }, 91 | deleteConfirm(name: string) { 92 | return `Подтвердите удаление【${name}】?` 93 | } 94 | }, 95 | } 96 | 97 | let locales = language.en 98 | try { 99 | const ha = parent.document.querySelector('home-assistant') as any 100 | // 判断是否中文 101 | if (ha) { 102 | if (ha.hass.language.includes('zh-')) { 103 | locales = language.cn 104 | } 105 | else if (ha.hass.language == 'ru') { 106 | locales = language.ru 107 | } 108 | } 109 | } catch { } 110 | export default locales -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp, DefineComponent } from 'vue' 2 | import App from './App.vue' 3 | import { VaListItemLabel, VuesticPlugin } from 'vuestic-ui' 4 | import 'vuestic-ui/dist/vuestic-ui.css' 5 | import '@mdi/font/css/materialdesignicons.min.css' 6 | import './style/index.scss' 7 | import api from './api/index' 8 | 9 | import rotuer from './router/index' 10 | import store from './store/index' 11 | 12 | import MdiIcon from './components/globel/mdi-icon.vue' 13 | import VueThreeShortkey from 'vue-three-shortkey' 14 | 15 | const getParentColor = (cssVar: string) => { 16 | return getComputedStyle(parent.document.documentElement).getPropertyValue(cssVar).trim() 17 | } 18 | document.body.style.backgroundColor = getParentColor('--primary-background-color') 19 | const style = document.createElement('style') 20 | style.textContent = ` 21 | :root{ 22 | --va-list-item-label-color: ${getParentColor('--primary-text-color')} 23 | } 24 | ` 25 | document.head.appendChild(style) 26 | const vuesticConfig = { 27 | colors: { 28 | primary: getParentColor('--primary-color'), 29 | // background: getParentColor('--primary-background-color'), 30 | // text: getParentColor('--primary-text-color') 31 | }, 32 | components: { 33 | VaAppBar: { 34 | color: getParentColor('--app-header-background-color'), 35 | text: getParentColor('--app-header-text-color') 36 | }, 37 | VaCard: { 38 | color: getParentColor('--card-background-color') 39 | } 40 | } 41 | } 42 | 43 | const app = createApp(App) 44 | app.config.globalProperties.api = api 45 | app.config.globalProperties.$toast = (message: string, options = {}) => { 46 | app.config.globalProperties.$vaToast.init({ position: 'bottom-right', color: 'primary', message, ...options }) 47 | } 48 | app.config.globalProperties.$dialog = (com: DefineComponent, propsData = {}): Promise => { 49 | return new Promise((resolve, reject) => { 50 | const div = document.createElement('div') 51 | const comApp = createApp(com, Object.assign(propsData, { 52 | app() { 53 | return app.config.globalProperties 54 | }, 55 | ok(data: any) { 56 | comApp.unmount() 57 | resolve(data) 58 | }, 59 | cancel() { 60 | comApp.unmount() 61 | reject() 62 | } 63 | })) 64 | comApp.use(store).use(VuesticPlugin, vuesticConfig).component('mdi-icon', MdiIcon).mount(div) 65 | }) 66 | } 67 | app.use(rotuer).use(store).use(VuesticPlugin, vuesticConfig).component('mdi-icon', MdiIcon).use(VueThreeShortkey).mount('#app') 68 | -------------------------------------------------------------------------------- /frontend/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router' 2 | 3 | const routes = [ 4 | { 5 | path: '/', 6 | name: 'index', 7 | meta: { 8 | keepAlive: true 9 | }, 10 | component: () => import('../views/index.vue') 11 | }, 12 | { 13 | path: '/editor', 14 | name: 'editor', 15 | meta: { 16 | keepAlive: false 17 | }, component: () => import('../views/editor.vue') 18 | }, 19 | ] 20 | 21 | const router = createRouter({ 22 | history: createWebHashHistory(), 23 | routes, 24 | }) 25 | 26 | export default router -------------------------------------------------------------------------------- /frontend/src/store/actions.ts: -------------------------------------------------------------------------------- 1 | export default {} -------------------------------------------------------------------------------- /frontend/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex' 2 | import api from '../api/index' 3 | import { formatSize, formatFileIcon, formatFolderIcon } from '../utils/format' 4 | 5 | const store = createStore({ 6 | state: { 7 | loading: false, 8 | path: '', 9 | fileList: [], 10 | folderList: [] 11 | }, 12 | getters: { 13 | pathList(state) { 14 | return state.path.split('/').map(value => { 15 | if (!value || value === '.') { 16 | return { 17 | name: 'HA', 18 | value: '.' 19 | } 20 | } 21 | return { 22 | name: value, 23 | value 24 | } 25 | }) 26 | }, 27 | absolutePath(state, getters) { 28 | return (name: string) => { 29 | let arr = getters.pathList.map((ele: any) => ele.value) 30 | arr.push(name) 31 | return arr.join('/') 32 | } 33 | } 34 | }, 35 | mutations: { 36 | setFileList(state, { path, list }) { 37 | state.path = path 38 | const arr = list.map((ele: any) => { 39 | ele.size = formatSize(ele.size) 40 | let icon = null 41 | if (ele.type === 'file') { 42 | icon = formatFileIcon(path, ele.name) 43 | } else { 44 | icon = formatFolderIcon(path, ele.name) 45 | } 46 | return { 47 | ...ele, 48 | ...icon 49 | } 50 | }) 51 | arr.sort((a: any, b: any) => { 52 | if (a.name > b.name) return 1 53 | return -1 54 | }) 55 | state.fileList = arr.filter((ele: any) => ele.type === 'file') 56 | state.folderList = arr.filter((ele: any) => ele.type === 'dir') 57 | }, 58 | loading(state, loading: boolean) { 59 | state.loading = loading 60 | } 61 | }, 62 | actions: { 63 | getFileList({ commit, getters, state }, data: number | string) { 64 | if (state.loading) return; 65 | let arr = getters.pathList.map((ele: any) => ele.value) 66 | if (typeof data === 'number') { 67 | arr.splice(data + 1) 68 | } else { 69 | arr.push(data) 70 | } 71 | const path = arr.join('/') 72 | api.service.getHassFileList(path).then((list: any) => { 73 | commit('setFileList', { path, list }) 74 | }) 75 | }, 76 | reloadFileList({ commit, getters, state }, path = '') { 77 | if (path == '') path = getters.pathList.map((ele: any) => ele.value).join('/') 78 | api.service.getHassFileList(path).then((list: any) => { 79 | commit('setFileList', { path, list }) 80 | }) 81 | } 82 | } 83 | }) 84 | 85 | export default store -------------------------------------------------------------------------------- /frontend/src/store/mutations.ts: -------------------------------------------------------------------------------- 1 | export default {} -------------------------------------------------------------------------------- /frontend/src/style/index.scss: -------------------------------------------------------------------------------- 1 | // @import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:ital,wght@0,400;1,700&display=swap'); 2 | @import url('@fontsource/source-sans-pro/index.css'); 3 | @import 'material-icons/iconfont/material-icons.css'; 4 | 5 | html, 6 | body { 7 | margin: 0; 8 | padding: 0; 9 | } 10 | 11 | .va-list-item { 12 | transition: 0.3s; 13 | 14 | &:hover { 15 | --va-list-item-label-color: #000; 16 | --va-list-item-label-caption-color: #999; 17 | } 18 | } 19 | 20 | ::-webkit-scrollbar-track { 21 | background-color: transparent; 22 | } 23 | 24 | ::-webkit-scrollbar { 25 | width: 10px; 26 | background-color: transparent; 27 | } 28 | 29 | ::-webkit-scrollbar-thumb { 30 | background-color: #c2c2c2; 31 | border-radius: 5px; 32 | } -------------------------------------------------------------------------------- /frontend/src/types/api.d.ts: -------------------------------------------------------------------------------- 1 | declare interface API { 2 | service: Service, 3 | fireEvent(type: string, data = {}, ele: any = null), 4 | showQuickBar(key: string) 5 | } 6 | 7 | declare interface Service { 8 | getHassFileList(path: string): Promise, 9 | getHassFileContent(path: string): Promise, 10 | setHassFileContent(path: string, data: string): Promise, 11 | uploadFile(path: string, data: File): Promise, 12 | createHassFile(act: string, path: string): Promise, 13 | deleteHassFile(path: string): Promise, 14 | downloadFile(path: string): Promise, 15 | rename(path: string, new_path: string): Promise 16 | } -------------------------------------------------------------------------------- /frontend/src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | import '@vue/runtime-core' 2 | declare module '@vue/runtime-core' { 3 | interface ComponentCustomProperties { 4 | $dialog: any; 5 | $toast: any; 6 | api: API 7 | } 8 | } -------------------------------------------------------------------------------- /frontend/src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare interface Window { 2 | editor: any, 3 | ace: any 4 | } 5 | 6 | declare module 'vue-three-shortkey'; -------------------------------------------------------------------------------- /frontend/src/utils/format.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * get file extend 4 | * @param fileName 5 | * @returns 6 | */ 7 | export function getFileExt(fileName: string) { 8 | const arr = fileName.split('.') 9 | if (arr.length > 1) { 10 | return arr[arr.length - 1].toLocaleLowerCase(); 11 | } 12 | return '' 13 | } 14 | 15 | /** 16 | * display files size 17 | * @param size 18 | * @returns 19 | */ 20 | export function formatSize(size: number) { 21 | if (size == 0) return '' 22 | if (size >= 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(2)} MB` 23 | if (size >= 1024) return `${(size / 1024).toFixed(2)} KB` 24 | return `${size} B` 25 | } 26 | 27 | /** 28 | * Show file icon 29 | * @param fileName 30 | * @returns 31 | */ 32 | export function formatFileIcon(path: any, fileName: string) { 33 | // ha root dir 34 | if (['.', ''].includes(path)) { 35 | if ([ 36 | '.shopping_list.json', 37 | 'secrets.yaml', 38 | 'scripts.yaml', 39 | 'scenes.yaml', 40 | 'groups.yaml', 41 | 'customize.yaml', 42 | 'configuration.yaml', 43 | 'automations.yaml', 44 | 'customize.yaml' 45 | ].includes(fileName)) { 46 | return { icon: 'mdi-home-assistant' } 47 | } 48 | else if (/^home-assistant\.log./.test(fileName)) { 49 | return { icon: 'mdi-math-log' } 50 | } 51 | else if (/^home-assistant_v2\.db./.test(fileName)) { 52 | return { icon: 'mdi-database' } 53 | } 54 | } 55 | let icon = 'mdi-file' 56 | let ext = getFileExt(fileName) 57 | if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'ico'].includes(ext)) { 58 | ext = 'img' 59 | } else if (['yaml', 'DS_Store'].includes(ext)) { 60 | ext = 'code' 61 | } else if ([ 62 | 'ha_version', 63 | 'config_entries', 64 | 'device_registry', 65 | 'entity_registry', 66 | 'area_registry', 67 | 'restore_state', 68 | ].includes(ext)) { 69 | ext = 'homeassistant' 70 | } else if (['m3u8', 'mp4', 'flv', 'm3u'].includes(ext)) { 71 | ext = 'video' 72 | } else if (['mp3', 'm4a'].includes(ext)) { 73 | ext = 'audio' 74 | } else if (['gz', 'rar', 'tz'].includes(ext)) { 75 | ext = 'zip' 76 | } else if (['html', 'htm'].includes(ext)) { 77 | ext = 'html' 78 | } else if (['db-shm', 'db-wal'].includes(ext)) { 79 | ext = 'db' 80 | } 81 | const mode = { 82 | code: 'mdi-code-braces', 83 | img: 'mdi-file-image', 84 | py: 'mdi-language-python', 85 | log: 'mdi-math-log', 86 | db: 'mdi-database', 87 | js: 'mdi-nodejs', 88 | sh: 'mdi-bash', 89 | homeassistant: 'mdi-home-assistant', 90 | json: 'mdi-code-json', 91 | md: 'mdi-language-markdown', 92 | html: 'mdi-language-html5', 93 | video: 'mdi-file-video', 94 | audio: 'mdi-file-music', 95 | zip: 'mdi-folder-zip', 96 | svg: 'mdi-svg', 97 | css: 'mdi-language-css3' 98 | } as any; 99 | return { icon: mode[ext] || icon } 100 | } 101 | 102 | 103 | /** 104 | * Show folder Icon 105 | * @param folderName 106 | * @returns 107 | */ 108 | export function formatFolderIcon(path: any, folderName: string) { 109 | if (path == './custom_components') { 110 | return { 111 | iconType: 'img', 112 | icon: `https://brands.home-assistant.io/${folderName}/icon.png` 113 | } 114 | } 115 | let icon = 'mdi-folder' 116 | let ext = folderName 117 | if ([ 118 | 'www', 119 | 'tts', 120 | 'themes', 121 | 'media', 122 | 'custom_components', 123 | 'blueprints', 124 | '.storage', 125 | '.cloud', 126 | 'script', 127 | 'automation', 128 | 'homeassistant', 129 | 'python_scripts', 130 | 'packages' 131 | ].includes(ext)) { 132 | icon = 'mdi-home-assistant' 133 | } 134 | 135 | if (['deps', '__pycache__'].includes(ext)) { 136 | icon = 'mdi-language-python' 137 | } 138 | return { icon } 139 | } 140 | 141 | /** 142 | * Editor display mode 143 | * @param fileName 144 | * @returns 145 | */ 146 | export function editorMode(fileName: string) { 147 | let ext = getFileExt(fileName) 148 | if (["js", "ts", "jsx"].includes(ext)) { 149 | ext = "javascript"; 150 | } else if (["py", "pyc"].includes(ext)) { 151 | ext = "python"; 152 | } else if (["md"].includes(ext)) { 153 | ext = "markdown"; 154 | } else if (["yml", "yaml"].includes(ext)) { 155 | ext = "yaml"; 156 | } 157 | return `ace/mode/${ext}`; 158 | } 159 | 160 | /** 161 | * Judge whether the file is editable 162 | * @param fileName 163 | * @returns 164 | */ 165 | export function isEditable(fileName: string) { 166 | let ext = getFileExt(fileName) 167 | return !['jpg', 'jpg', 'jpg', 168 | 'mp3', 'mp4', 'db', 'jpg'].includes(ext) 169 | } -------------------------------------------------------------------------------- /frontend/src/utils/query.ts: -------------------------------------------------------------------------------- 1 | export function querySelector(ele: any, selector: any): any { 2 | // console.log('%O', ele) 3 | // 正常元素 4 | let findEle = ele.querySelector(selector) 5 | if (findEle) return findEle 6 | else if (ele.children.length > 0) { 7 | let children = ele.children 8 | for (let i = 0, j = children.length; i < j; i++) { 9 | let result = querySelector(children[i], selector) 10 | if (result) return result 11 | } 12 | } 13 | // 影子根 14 | if (ele.renderRoot) { 15 | findEle = ele.renderRoot.querySelector(selector) 16 | if (findEle) return findEle 17 | else if (ele.renderRoot.children.length > 0) { 18 | let children = ele.renderRoot.children 19 | for (let i = 0, j = children.length; i < j; i++) { 20 | let result = querySelector(children[i], selector) 21 | if (result) return result 22 | } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /frontend/src/views/editor.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 21 | 22 | 81 | 82 | 83 | 95 | -------------------------------------------------------------------------------- /frontend/src/views/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 40 | 41 | 62 | 63 | 98 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "lib": ["esnext", "dom"], 14 | "skipLibCheck": true 15 | }, 16 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 17 | "references": [{ "path": "./tsconfig.node.json" }] 18 | } 19 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | // https://vitejs.dev/config/ 4 | export default defineConfig({ 5 | base: './', 6 | build: { 7 | outDir: '../custom_components/ha_file_explorer/www', 8 | emptyOutDir: true 9 | }, 10 | plugins: [vue()], 11 | server: { 12 | proxy: { 13 | '/ha_file_explorer-api': 'http://localhost:8123' 14 | } 15 | }, 16 | resolve: { 17 | alias: { 18 | "@": '/src/' 19 | } 20 | } 21 | }) --------------------------------------------------------------------------------