// ==UserScript==
// @name AnMe
// @author zjw
// @version 10.0.5
// @namespace https://github.com/Zhu-junwei/AnMe
// @description 通用多网站多账号切换器
// @description:zh 通用多网站多账号切换器
// @description:en Universal Multi-Site Account Switcher
// @description:es Conmutador universal de múltiples cuentas para múltiples sitios web
// @icon data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMWVtIiBoZWlnaHQ9IjFlbSIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik0yMSAxNy41QzIxIDE5LjQzMyAxOS40MzMgMjEgMTcuNSAyMUMxNS41NjcgMjEgMTQgMTkuNDMzIDE0IDE3LjVDMTQgMTUuNTY3IDE1LjU2NyAxNCAxNy41IDE0QzE5LjQzMyAxNCAyMSAxNS41NjcgMjEgMTcuNVoiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2Utd2lkdGg9IjEuNSIvPjxwYXRoIGQ9Ik0yIDExSDIyIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjxwYXRoIGQ9Ik00IDExTDQuNjEzOCA4LjU0NDc5QzUuMTU5NDcgNi4zNjIxMSA1LjQzMjMxIDUuMjcwNzcgNi4yNDYwOSA0LjYzNTM4QzcuMDU5ODggNCA4LjE4NDggNCAxMC40MzQ3IDRIMTMuNTY1M0MxNS44MTUyIDQgMTYuOTQwMSA0IDE3Ljc1MzkgNC42MzUzOEMxOC41Njc3IDUuMjcwNzcgMTguODQwNSA2LjM2MjExIDE5LjM4NjIgOC41NDQ3OUwyMCAxMSIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48cGF0aCBkPSJNMTAgMTcuNUMxMCAxOS40MzMgOC40MzMgMjEgNi41IDIxQzQuNTY3IDIxIDMgMTkuNDMzIDMgMTcuNUMzIDE1LjU2NyA0LjU2NyAxNCA2LjUgMTRDOC40MzMgMTQgMTAgMTUuNTY3IDEwIDE3LjVaIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIxLjUiLz48cGF0aCBkPSJNMTAgMTcuNDk5OUwxMC42NTg0IDE3LjE3MDdDMTEuNTAyOSAxNi43NDg0IDEyLjQ5NzEgMTYuNzQ4NCAxMy4zNDE2IDE3LjE3MDdMMTQgMTcuNDk5OSIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48L3N2Zz4=
// @match *://*/*
// @license MIT
// @run-at document-end
// @grant GM_cookie
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_listValues
// @grant GM_deleteValue
// @grant GM_registerMenuCommand
// @grant GM_info
// @grant GM_xmlhttpRequest
// @connect *
// @downloadURL none
// ==/UserScript==
(() => {
// src/app/config.js
var CONST = {
PREFIX: "acc_stable_",
ORDER_PREFIX: "acc_order_",
SITE_NAME_PREFIX: "acc_site_name_",
CFG: {
LANG: "cfg_lang",
FAB_MODE: "cfg_fab_mode",
FAB_POS: "cfg_fab_pos",
HOST_DISPLAY_MODE: "cfg_host_display_mode",
HOST_ICON_CACHE: "cfg_host_icon_cache",
WEBDAV_CONFIG: "cfg_webdav_config",
WEBDAV_SECRET: "cfg_webdav_secret",
WEBDAV_BACKUPS_CACHE: "cfg_webdav_backups_cache"
},
HOST: location.hostname,
META: {
NAME: GM_info.script.name,
VERSION: GM_info.script.version,
AUTHOR: GM_info.script.author,
LINKS: {
PROJECT: "https://github.com/Zhu-junwei/AnMe",
DONATE: "https://www.cnblogs.com/zjw-blog/p/19466109"
}
},
ICONS: {
LOGO: ` `,
GITHUB: ` `,
CLOUD: ` `,
USER: ` `,
SETTINGS: ` `,
HELP: ` `,
LOCK: ` `,
EXPORT: ` `,
MUTIEXPORT: ` `,
IMPORT: ` `,
DELETE: ` `,
DONATE: ` `,
SAVE: ` `,
SEARCH: ` `,
EDIT: ` `,
CLOSE: ` `,
CLEAN: ` `,
BACK: ` `,
HOME: ` `,
NOTICE: ` `
}
};
var I18N_DATA = {
zh: { _name: "简体中文", nav_set: "高级设置", nav_notice: "使用声明", nav_about: "关于脚本", back_current_host: "返回当前网站账号", open_site: "打开网站", edit_site_name: "编辑站点名字", search_site: "搜索网站...", search_accounts: "搜索账号", close_search_accounts: "关闭搜索", search_accounts_placeholder: "搜索当前网站账号...", account_settings: "账号设置", site_name: "站点名称", account_name: "账号名称", save_changes: "保存修改", btn_delete_account: "删除该账号", danger_zone: "危险操作", rename_conflict: "该名称已存在,请换一个名称。", confirm_delete: "确定要删除该账号记录吗?", placeholder_site_name: "给当前网站命名...", placeholder_name: "给新账号命名...", tip_help: "切换登录失败?尝试勾选 LocalStorage 和 SessionStorage。", tip_lock: "为保证正常读取Cookie,请在篡改猴高级模式下,设置允许脚本访问 Cookie: ALL", btn_save: "保存当前账号", confirm_overwrite: "⚠️ 该名称已存在,确定要覆盖原有记录吗?", btn_clean: "切换新环境 (清空本站痕迹)", save_empty_err: "⚠️ 没有检测到可保存的数据", copy_account_name: "复制账号名", copy_failed: "复制失败,请手动复制。", toast_saved: "账号已保存", toast_renamed: "账号名称已更新", toast_deleted: "账号已删除", toast_copied: "账号名已复制", toast_site_name_updated: "站点名称已更新", set_fab_mode: "悬浮球显示模式", fab_auto: "智能", fab_show: "常驻", fab_hide: "隐藏", fab_auto_title: "有账号记录时自动显示,无记录时隐藏", fab_show_title: "始终显示悬浮球", fab_hide_title: "平时不显示,仅能通过菜单唤起", set_lang: "语言设置 / Language", set_host_display_mode: "站点列表显示模式", host_display_mode_site_name: "站点名字", host_display_mode_domain: "域名", set_backup: "数据备份与还原", btn_exp_curr: "导出当前网站数据", btn_exp_all: "导出全部网站数据", btn_imp: "导入备份文件", donate: "支持作者", btn_clear_all: "清空脚本所有数据 (慎用)", notice_title: "《使用声明与免责条款》", back: "← 返回上一级", no_data: "🍃 暂无账号记录", confirm_clean: "确定清空当前网站所有痕迹并开启新环境?", confirm_clear_all: "⚠️ 警告:这将删除本脚本保存的所有网站的所有账号数据!且无法恢复!", import_ok: "✅ 成功导入/更新 {count} 个账号!", import_err: "❌ 导入失败:文件格式错误", export_err: "⚠️ 没有可导出的数据", menu_open: "🚀 开启账号管理", dlg_ok: "确定", dlg_cancel: "取消", about_desc: "通用多网站多账号切换器", notice_content: `
1. 脚本功能说明 本脚本通过篡改猴插件提供的存储API,将当前网站的 Cookie、LocalStorage 和 SessionStorage 进行快照保存。当您点击切换时,脚本会清空当前痕迹并还原选中的快照数据,从而实现多账号快速登录。
2. 数据存储与联网说明 账号数据默认存储在您浏览器的篡改猴插件内部管理器中(GM_setValue)。脚本默认不会主动联网或上传数据;只有在您手动配置并使用 WebDAV 云同步功能时,脚本才会按您的操作访问您指定的远程服务并上传或下载备份文件。
3. 风险提示 由于浏览器环境的开放性,本脚本无法阻止同域名下的其他恶意脚本通过篡改猴 API 或存储机制尝试获取这些数据。请勿在公共电脑或不可信的设备环境中使用本脚本保存重要账号。
4. 免责声明 本脚本仅供学习交流使用。因使用本脚本导致的账号被封禁、数据泄露或任何形式的损失,作者不承担任何法律责任。
` },
en: { _name: "English", nav_set: "Settings", nav_notice: "Disclaimer", nav_about: "About", back_current_host: "Back to current site", open_site: "Open site", edit_site_name: "Edit site name", search_site: "Search sites...", search_accounts: "Search accounts", close_search_accounts: "Close search", search_accounts_placeholder: "Search accounts on this site...", account_settings: "Account Settings", site_name: "Site Name", account_name: "Account Name", save_changes: "Save Changes", btn_delete_account: "Delete Account", danger_zone: "Danger Zone", rename_conflict: "This name already exists. Please choose another one.", confirm_delete: "Are you sure you want to delete this account?", placeholder_site_name: "Name this site...", placeholder_name: "Name this account...", tip_help: "Switch failed? Try checking LocalStorage/SessionStorage.", tip_lock: "To ensure cookies can be read correctly, open Tampermonkey’s Advanced Settings and change “Allow scripts to access cookies” to “ALL”.", btn_save: "Save Current", confirm_overwrite: "⚠️ Name already exists. Do you want to overwrite it?", btn_clean: "Switch to a new environment (clear all data for this site)", save_empty_err: "⚠️ No data detected to save", copy_account_name: "Copy account name", copy_failed: "Copy failed. Please copy it manually.", toast_saved: "Account saved", toast_renamed: "Account name updated", toast_deleted: "Account deleted", toast_copied: "Account name copied", toast_site_name_updated: "Site name updated", set_fab_mode: "Float Button Mode", fab_auto: "Auto", fab_show: "Show", fab_hide: "Hide", fab_auto_title: "Automatically show when accounts exist, hide when none", fab_show_title: "Always show the floating button", fab_hide_title: "Hidden by default, can only be activated via the menu", set_lang: "语言设置 / Language", set_host_display_mode: "Site List Display", host_display_mode_site_name: "Site Name", host_display_mode_domain: "Domain", set_backup: "Backup & Restore", btn_exp_curr: "Export Current Site", btn_exp_all: "Export All Sites Data", btn_imp: "Import Backup", donate: "Buy me a coffee", btn_clear_all: "Clear all script data (use with caution)", notice_title: "Disclaimer & Terms", back: "← Back", no_data: "🍃 No accounts", confirm_clean: "Are you sure you want to clear all traces of the current website and start a new environment?", confirm_clear_all: "⚠️ Warning: This will delete all account data for all websites saved by this script, and cannot be undone!", import_ok: "✅ Successfully imported/updated {count} account(s)!", import_err: "❌ Invalid format", export_err: "⚠️ No data", menu_open: "🚀 Open Manager", dlg_ok: "OK", dlg_cancel: "Cancel", about_desc: "Universal Multi-Site Account Switcher", notice_content: `1. Script Functionality This script utilizes the storage API provided by Tampermonkey to take snapshots of the current website's Cookies, LocalStorage, and SessionStorage. When switching accounts, the script clears current session data and restores the selected snapshot, enabling rapid multi-account login.
2. Data Storage & Network Access Account data is stored locally in your browser's Tampermonkey extension manager (via GM_setValue) by default. The script does not proactively upload data or access remote services unless you explicitly configure and use the WebDAV sync feature; only then will it connect to the WebDAV server you specified to upload or download backup files.
3. Risk Warning Due to the open nature of browser environments, this script cannot prevent other malicious scripts on the same domain from attempting to access data via storage mechanisms. Please avoid using this script to save sensitive accounts on public or untrusted devices.
4. Disclaimer This script is intended for educational and exchange purposes only. The author shall not be held legally responsible for any account bans, data breaches, or any form of loss resulting from the use of this script.
` },
es: { _name: "Español", nav_set: "Configuración", nav_notice: "Aviso legal", nav_about: "Acerca de", back_current_host: "Volver al sitio actual", open_site: "Abrir sitio", edit_site_name: "Editar nombre del sitio", search_site: "Buscar sitios...", search_accounts: "Buscar cuentas", close_search_accounts: "Cerrar búsqueda", search_accounts_placeholder: "Buscar cuentas en este sitio...", account_settings: "Configuración de la cuenta", site_name: "Nombre del sitio", account_name: "Nombre de la cuenta", save_changes: "Guardar cambios", btn_delete_account: "Eliminar cuenta", danger_zone: "Zona peligrosa", rename_conflict: "Ese nombre ya existe. Usa otro nombre.", confirm_delete: "¿Estás seguro de que deseas eliminar esta cuenta?", placeholder_site_name: "Nombre para este sitio...", placeholder_name: "Nombre para esta cuenta...", tip_help: "¿Falló el cambio de cuenta? Intenta marcar LocalStorage y SessionStorage.", tip_lock: "Para garantizar la correcta lectura de cookies, abre la configuración avanzada de Tampermonkey y establece “Permitir que los scripts accedan a cookies” en “ALL”.", btn_save: "Guardar cuenta actual", confirm_overwrite: "⚠️ El nombre ya existe. ¿Deseas sobrescribirlo?", btn_clean: "Cambiar a un nuevo entorno (borrar datos del sitio)", save_empty_err: "⚠️ No se detectaron datos para guardar", copy_account_name: "Copiar nombre de la cuenta", copy_failed: "No se pudo copiar. Cópialo manualmente.", toast_saved: "Cuenta guardada", toast_renamed: "Nombre de cuenta actualizado", toast_deleted: "Cuenta eliminada", toast_copied: "Nombre de cuenta copiado", toast_site_name_updated: "Nombre del sitio actualizado", set_fab_mode: "Modo del botón flotante", fab_auto: "Automático", fab_show: "Siempre visible", fab_hide: "Oculto", fab_auto_title: "Se muestra automáticamente cuando hay cuentas guardadas; se oculta si no hay ninguna", fab_show_title: "El botón flotante se muestra siempre", fab_hide_title: "Oculto por defecto, solo accesible desde el menú", set_lang: "Idioma / Language", set_host_display_mode: "Modo de lista de sitios", host_display_mode_site_name: "Nombre del sitio", host_display_mode_domain: "Dominio", set_backup: "Copia de seguridad y restauración", btn_exp_curr: "Exportar datos del sitio actual", btn_exp_all: "Exportar todos los datos de los sitios", btn_imp: "Importar archivo de respaldo", donate: "Apoyar al autor", btn_clear_all: "Borrar todos los datos del script (usar con precaución)", notice_title: "Términos de uso y descargo de responsabilidad", back: "← Volver", no_data: "🍃 No hay cuentas", confirm_clean: "¿Seguro que deseas borrar todos los rastros del sitio actual y comenzar un nuevo entorno?", confirm_clear_all: "⚠️ Advertencia: Esto eliminará todos los datos de cuentas de todos los sitios guardados por este script. ¡Esta acción no se puede deshacer!", import_ok: "✅ Se importaron/actualizaron correctamente {count} cuenta(s)", import_err: "❌ Error de importación: formato de archivo inválido", export_err: "⚠️ No hay datos para exportar", menu_open: "🚀 Abrir gestor de cuentas", dlg_ok: "Aceptar", dlg_cancel: "Cancelar", about_desc: "Conmutador universal de múltiples cuentas para múltiples sitios", notice_content: `1. Funcionalidad del script Este script utiliza la API de almacenamiento proporcionada por Tampermonkey para guardar instantáneas de las Cookies, LocalStorage y SessionStorage del sitio web actual. Al cambiar de cuenta, el script borra los datos actuales y restaura la instantánea seleccionada, permitiendo un inicio de sesión rápido con múltiples cuentas.
2. Almacenamiento y acceso de red Los datos de las cuentas se almacenan localmente en el administrador interno de Tampermonkey (mediante GM_setValue) de forma predeterminada. El script no sube datos ni accede a servicios remotos por iniciativa propia; solo se conectará al servidor WebDAV que configures cuando habilites y utilices explícitamente la función de sincronización para subir o descargar copias de seguridad.
3. Advertencia de riesgo Debido a la naturaleza abierta del entorno del navegador, este script no puede impedir que otros scripts maliciosos bajo el mismo dominio intenten acceder a estos datos mediante mecanismos de almacenamiento. Evita guardar cuentas sensibles en equipos públicos o no confiables.
4. Descargo de responsabilidad Este script se proporciona únicamente con fines educativos y de intercambio. El autor no asume ninguna responsabilidad legal por bloqueos de cuentas, fugas de datos o cualquier tipo de pérdida derivada del uso de este script.
` }
};
Object.assign(I18N_DATA.zh, {
nav_webdav: "WebDAV 同步",
default_account_prefix: "账号",
account_note: "备注",
placeholder_note: "给该账号添加备注(可选)...",
view_note: "查看备注",
toast_account_updated: "账号信息已更新",
webdav_account: "WebDAV 账号",
webdav_config: "设置",
webdav_not_configured: "尚未配置 WebDAV",
webdav_connected_as: "当前账号:{user}",
webdav_url: "服务地址",
webdav_url_placeholder: "例如: https://dav.example.com/remote.php/dav/files/user",
webdav_username: "用户名",
webdav_username_placeholder: "请输入 WebDAV 用户名",
webdav_password: "密码",
webdav_password_placeholder: "请输入 WebDAV 密码",
webdav_password_keep_placeholder: "已保存密码,留空则保持不变",
webdav_verify_save: "验证并保存",
webdav_sync: "云同步",
webdav_sync_now: "备份",
webdav_refresh: "刷新列表",
webdav_refresh_ok: "刷新成功",
webdav_backup_list: "云端备份列表",
webdav_restore: "恢复",
webdav_delete: "删除",
webdav_no_backups: "暂无云端备份",
webdav_need_config: "请先填写并验证 WebDAV 配置。",
webdav_loading: "正在加载云端备份...",
webdav_validating: "正在验证 WebDAV...",
webdav_verified: "WebDAV 验证成功",
webdav_verify_err: "WebDAV 验证失败",
webdav_missing_config: "请完整填写 WebDAV 服务地址、用户名和密码。",
webdav_syncing: "正在同步到 WebDAV...",
webdav_sync_ok: "同步已完成",
webdav_sync_err: "同步失败",
webdav_list_err: "获取云端备份列表失败",
webdav_delete_confirm: "确定删除云端备份:{name}?",
webdav_delete_ok: "云端备份已删除",
webdav_delete_err: "删除云端备份失败",
webdav_restore_confirm: "确定用该云端备份恢复本地数据:{name}?",
webdav_restoring: "正在从 WebDAV 恢复...",
webdav_logout: "退出 WebDAV",
webdav_logout_confirm: "确定退出 WebDAV 并删除本地保存的账号信息吗?",
webdav_logout_ok: "已退出 WebDAV",
webdav_timeout: "WebDAV 请求超时,请检查网络或服务状态。",
webdav_timeout_check_settings: "已超时,请检查 WebDAV 设置。",
sync_restore_ok: "✅ 已从云端同步恢复 {count} 个账号!",
sync_restore_err: "云端恢复失败,压缩包或数据文件无效。"
});
Object.assign(I18N_DATA.en, {
nav_webdav: "WebDAV Sync",
default_account_prefix: "Account",
account_note: "Note",
placeholder_note: "Add an optional note for this account...",
view_note: "View note",
toast_account_updated: "Account details updated",
webdav_account: "WebDAV Account",
webdav_config: "Settings",
webdav_not_configured: "WebDAV is not configured yet",
webdav_connected_as: "Current account: {user}",
webdav_url: "Server URL",
webdav_url_placeholder: "Example: https://dav.example.com/remote.php/dav/files/user",
webdav_username: "Username",
webdav_username_placeholder: "Enter WebDAV username",
webdav_password: "Password",
webdav_password_placeholder: "Enter WebDAV password",
webdav_password_keep_placeholder: "Password saved. Leave blank to keep it unchanged",
webdav_verify_save: "Verify and Save",
webdav_sync: "Cloud Sync",
webdav_sync_now: "Backup",
webdav_refresh: "Refresh List",
webdav_refresh_ok: "List refreshed",
webdav_backup_list: "Cloud Backup List",
webdav_restore: "Restore",
webdav_delete: "Delete",
webdav_no_backups: "No cloud backups yet",
webdav_need_config: "Fill in and verify your WebDAV settings first.",
webdav_loading: "Loading cloud backups...",
webdav_validating: "Validating WebDAV...",
webdav_verified: "WebDAV verified",
webdav_verify_err: "WebDAV validation failed",
webdav_missing_config: "Please fill in the WebDAV URL, username, and password.",
webdav_syncing: "Syncing to WebDAV...",
webdav_sync_ok: "Sync completed",
webdav_sync_err: "Sync failed",
webdav_list_err: "Failed to load cloud backups",
webdav_delete_confirm: "Delete cloud backup: {name}?",
webdav_delete_ok: "Cloud backup deleted",
webdav_delete_err: "Failed to delete cloud backup",
webdav_restore_confirm: "Restore local data from cloud backup: {name}?",
webdav_restoring: "Restoring from WebDAV...",
webdav_logout: "Sign out of WebDAV",
webdav_logout_confirm: "Sign out of WebDAV and remove the saved local account info?",
webdav_logout_ok: "Signed out of WebDAV",
webdav_timeout: "WebDAV request timed out. Check the network or server status.",
webdav_timeout_check_settings: "Request timed out. Please check your WebDAV settings.",
sync_restore_ok: "✅ Restored {count} account(s) from cloud sync!",
sync_restore_err: "Cloud restore failed. The archive or data file is invalid."
});
Object.assign(I18N_DATA.es, {
nav_webdav: "Sincronización WebDAV",
default_account_prefix: "Cuenta",
account_note: "Nota",
placeholder_note: "Agrega una nota opcional para esta cuenta...",
view_note: "Ver nota",
toast_account_updated: "Información de la cuenta actualizada",
webdav_account: "Cuenta WebDAV",
webdav_config: "Configurar",
webdav_not_configured: "WebDAV aún no está configurado",
webdav_connected_as: "Cuenta actual: {user}",
webdav_url: "URL del servidor",
webdav_url_placeholder: "Ejemplo: https://dav.example.com/remote.php/dav/files/user",
webdav_username: "Usuario",
webdav_username_placeholder: "Introduce el usuario de WebDAV",
webdav_password: "Contraseña",
webdav_password_placeholder: "Introduce la contraseña de WebDAV",
webdav_password_keep_placeholder: "La contraseña ya está guardada. Déjalo vacío para conservarla",
webdav_verify_save: "Verificar y guardar",
webdav_sync: "Sincronización en la nube",
webdav_sync_now: "Respaldar",
webdav_refresh: "Actualizar lista",
webdav_refresh_ok: "Lista actualizada",
webdav_backup_list: "Lista de copias en la nube",
webdav_restore: "Restaurar",
webdav_delete: "Eliminar",
webdav_no_backups: "Todavía no hay copias en la nube",
webdav_need_config: "Primero completa y verifica la configuración de WebDAV.",
webdav_loading: "Cargando copias en la nube...",
webdav_validating: "Validando WebDAV...",
webdav_verified: "WebDAV verificado",
webdav_verify_err: "La validación de WebDAV falló",
webdav_missing_config: "Completa la URL, el usuario y la contraseña de WebDAV.",
webdav_syncing: "Sincronizando con WebDAV...",
webdav_sync_ok: "Sincronización completada",
webdav_sync_err: "La sincronización falló",
webdav_list_err: "No se pudieron cargar las copias en la nube",
webdav_delete_confirm: "¿Eliminar la copia en la nube: {name}?",
webdav_delete_ok: "Copia en la nube eliminada",
webdav_delete_err: "No se pudo eliminar la copia en la nube",
webdav_restore_confirm: "¿Restaurar los datos locales desde la copia en la nube: {name}?",
webdav_restoring: "Restaurando desde WebDAV...",
webdav_logout: "Cerrar sesión de WebDAV",
webdav_logout_confirm: "¿Cerrar sesión de WebDAV y eliminar la información guardada localmente?",
webdav_logout_ok: "WebDAV desconectado",
webdav_timeout: "La solicitud de WebDAV agotó el tiempo de espera. Revisa la red o el servidor.",
webdav_timeout_check_settings: "Se agotó el tiempo de espera. Revisa la configuración de WebDAV.",
sync_restore_ok: "✅ Se restauraron {count} cuenta(s) desde la sincronización en la nube.",
sync_restore_err: "La restauración en la nube falló. El archivo comprimido o los datos no son válidos."
});
var STYLE_CSS = `
:host {
all: initial; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif!important;font-size: 14px!important;line-height: 1.5;color: #333!important;z-index: 2147483647; position: fixed;
top: 0;left: 0;width: 0;height: 0;pointer-events: none;
}
* { box-sizing: border-box; }
a { text-decoration:none; }
#acc-mgr-fab, .acc-panel, .acc-dialog-mask, .acc-floating-note-tooltip { pointer-events: auto; }
#acc-mgr-fab { padding: 10px;position: fixed; bottom: 100px; right: 30px; width: 44px; height: 44px; background: #2196F3; color: white; border-radius: 50%; display: none; align-items: center; justify-content: center; font-size: 20px; cursor: move; z-index: 1000000; box-shadow: 0 8px 30px rgba(0,0,0,0.25); user-select: none; border: none; touch-action: none; transition: transform 0.1s; }
#acc-mgr-fab:active { transform: scale(0.95); }
.acc-panel { position: fixed; width: 340px; background: white; border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,0.25); z-index: 1000001; display: flex; flex-direction: column; font-family: inherit; border: 1px solid #ddd; overflow: hidden; height: 480px; overscroll-behavior: none !important; opacity: 0; visibility: hidden; transition: opacity 0.12s ease, visibility 0.12s ease; pointer-events: none; }
.acc-panel.show { opacity: 1; visibility: visible; pointer-events: auto; }
.acc-header { display: flex; align-items: center; justify-content: center; padding: 8px 15px; border-bottom: 1px solid #eee; background: #fff; position: relative; flex-shrink: 0; min-height: 44px; }
.acc-header-actions { position: absolute; left: 15px; top: 50%; transform: translateY(-50%); display: none; gap: 6px; align-items: center; }
.acc-header-right-actions { position:absolute; right:15px; top:50%; transform:translateY(-50%); display:flex; gap:6px; align-items:center; }
.acc-header-title { font-size: 14px; font-weight: bold; color: #333; text-align: center; }
.acc-tab-content { flex: 1; display: none; padding: 15px 15px 0 15px; overflow: hidden; flex-direction: column; background: #fff; }
.acc-tab-content.active { display: flex; }
.acc-mgr-toolbar { display:flex; gap:8px; margin-bottom:10px; align-items:center; min-height:30px; }
.acc-mgr-host-row { display:flex; gap:5px; align-items:center; flex:1; min-width:0; position:relative; min-height:30px; }
.acc-host-picker { position:relative; flex:1; min-width:0; min-height:30px; }
.acc-host-search-input { display:none; width:100%; height:30px; box-sizing:border-box; border:1px solid #d0d5dd; border-radius:6px; padding:7px 10px; font-size:12px; outline:none; color:#333; background:#fff; }
.acc-host-search-input:focus { border-color:#2196F3; box-shadow:0 0 0 2px rgba(33, 150, 243, 0.12); }
.acc-host-picker.open .acc-host-trigger { display:none; }
.acc-host-picker.open .acc-host-search-input { display:block; }
.acc-account-search-box { display:none; flex:1; min-width:0; min-height:30px; }
.acc-mgr-host-row.searching .acc-host-picker { display:none; }
.acc-mgr-host-row.searching .acc-account-search-box { display:block; }
.acc-host-trigger { width:100%; min-width:0; height:30px; box-sizing:border-box; padding:6px 28px 6px 10px; font-size:12px; border:1px solid #d0d5dd; border-radius:4px; outline:none; cursor:pointer; background:#fff; color:#333; text-align:left; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; position:relative; }
.acc-host-trigger-content,
.acc-host-option-content { display:flex; align-items:center; gap:8px; min-width:0; width:100%; }
.acc-host-trigger-label,
.acc-host-option-label { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.acc-host-icon { width:16px; height:16px; min-width:16px; border-radius:4px; overflow:hidden; border:none; background:transparent; display:inline-flex; align-items:center; justify-content:center; position:relative; flex-shrink:0; box-sizing:border-box; }
.acc-host-favicon { width:100%; height:100%; display:block; object-fit:cover; background:transparent; }
.acc-host-icon-fallback { position:absolute; inset:0; display:none; align-items:center; justify-content:center; font-size:9px; font-weight:700; color:#667085; text-transform:uppercase; background:#eef2f6; border-radius:4px; }
.acc-host-icon.is-fallback .acc-host-favicon { display:none; }
.acc-host-icon.is-fallback .acc-host-icon-fallback { display:flex; }
.acc-host-trigger::after { content:""; position:absolute; right:10px; top:50%; width:7px; height:7px; border-right:1.5px solid currentColor; border-bottom:1.5px solid currentColor; transform:translateY(-65%) rotate(45deg); opacity:0.7; transition:transform 0.15s ease; }
.acc-host-picker.open .acc-host-trigger { border-color:#2196F3; box-shadow:0 0 0 2px rgba(33, 150, 243, 0.12); }
.acc-host-picker.open .acc-host-trigger::after { transform:translateY(-30%) rotate(225deg); }
.acc-host-menu { position:absolute; top:calc(100% + 4px); left:0; right:0; background:#fff; border:1px solid #d0d5dd; border-radius:8px; box-shadow:0 10px 24px rgba(15, 23, 42, 0.12); padding:6px; display:none; max-height:320px; overflow:hidden; z-index:20; overscroll-behavior:contain; }
.acc-host-picker.open .acc-host-menu { display:block; }
.acc-account-search-input { width:100%; height:30px; box-sizing:border-box; border:1px solid #d0d5dd; border-radius:6px; padding:7px 10px; font-size:12px; outline:none; color:#333; background:#fff; }
.acc-account-search-input:focus { border-color:#2196F3; box-shadow:0 0 0 2px rgba(33, 150, 243, 0.12); }
.acc-host-list { max-height:266px; overflow-y:auto; overscroll-behavior:contain; }
.acc-host-list::-webkit-scrollbar { width:6px; }
.acc-host-list::-webkit-scrollbar-thumb { background:#d3d9e2; border-radius:999px; }
.acc-host-option-row { display:flex; align-items:center; gap:6px; border-radius:6px; flex-wrap:wrap; }
.acc-host-option-row:hover { background:#f2f8fd; }
.acc-host-option-row.active { background:#e3f2fd; }
.acc-host-option { flex:1; min-width:0; border:none; background:transparent; color:#333; display:flex; align-items:center; gap:6px; padding:8px 10px; border-radius:6px; cursor:pointer; font-size:12px; text-align:left; }
.acc-host-option-row:hover .acc-host-option { color:#2196F3; }
.acc-host-option-row.active .acc-host-option { color:#1976D2; font-weight:600; }
.acc-host-edit-link,
.acc-host-open-link { flex-shrink:0; margin-right:10px; border:none; background:transparent; color:#7d93a8; font-size:12px; line-height:1; cursor:pointer; padding:0; opacity:0; visibility:hidden; text-decoration:underline; text-underline-offset:2px; transition:color 0.15s ease, opacity 0.15s ease; }
.acc-host-edit-link { margin-right:0; text-decoration:none; display:flex; align-items:center; justify-content:center; width:18px; height:18px; }
.acc-host-edit-link svg { font-size:13px; }
.acc-host-option-row:hover .acc-host-edit-link,
.acc-host-option-row:hover .acc-host-open-link,
.acc-host-edit-link:focus-visible,
.acc-host-open-link:focus-visible { opacity:1; visibility:visible; }
.acc-host-edit-link:hover,
.acc-host-edit-link:focus-visible,
.acc-host-open-link:hover,
.acc-host-open-link:focus-visible { color:#2196F3; outline:none; }
.acc-host-edit-box { width:100%; display:flex; gap:6px; padding:0 8px 8px 8px; }
.acc-host-edit-input { flex:1; min-width:0; border:1px solid #d9e2ec; border-radius:6px; padding:6px 8px; font-size:12px; outline:none; background:#fff; }
.acc-host-edit-input:focus { border-color:#2196F3; box-shadow:0 0 0 2px rgba(33, 150, 243, 0.12); }
.acc-host-edit-save,
.acc-host-edit-cancel { border:1px solid #d9e2ec; background:#fff; color:#475467; border-radius:6px; padding:0 8px; font-size:12px; cursor:pointer; }
.acc-host-edit-save:hover,
.acc-host-edit-cancel:hover { border-color:#2196F3; color:#2196F3; background:#f5fbff; }
.acc-host-empty { padding:10px; font-size:12px; color:#8a94a3; text-align:center; }
.acc-toolbar-btn { width:30px; height:30px; border:1px solid #ddd; background:#fff; border-radius:6px; cursor:pointer; display:flex; align-items:center; justify-content:center; color:#555; padding:0; transition:0.2s; }
.acc-toolbar-btn:disabled { opacity:.5; cursor:not-allowed; }
.acc-toolbar-btn svg { font-size:16px; }
.acc-toolbar-btn:hover { background:#e3f2fd; border-color:#2196F3; color:#2196F3; }
.acc-scroll-area { flex: 1; overflow-y: auto; padding-right: 4px; margin-top: 2px; overscroll-behavior: contain;}
#switch-area { padding-left:12px; padding-right:12px; margin-left:-12px; margin-right:-12px; }
.acc-scroll-area::-webkit-scrollbar { width: 4px; }
.acc-scroll-area::-webkit-scrollbar-thumb { background: #ddd; border-radius: 10px; }
/* --- Customized Elements --- */
.acc-backup-row { display: flex; gap: 8px; margin-bottom: 10px; justify-content: space-between; }
.acc-icon-btn { flex: 1; height: 38px; padding: 0; border-radius: 6px; border: 1px solid #eee; background: #f9f9f9; cursor: pointer; font-size: 18px; transition: 0.2s; display: flex; align-items: center; justify-content: center; color: #555; }
.acc-icon-btn:hover { background: #e3f2fd; border-color: #2196F3; color:#2196F3}
.acc-icon-btn.danger:hover { background: #ffebee; border-color: #f44336; color: #f44336; }
.acc-about-content { padding: 5px; color: #444 !important; font-size: 13px !important; line-height: 1.6 !important; text-align: left !important; }
.acc-about-header { text-align: center !important; margin-bottom: 20px !important; }
.acc-about-logo { font-size: 20px !important; display: inline-flex !important; margin-bottom: 5px !important; }
.acc-about-name { font-weight: bold !important; font-size: 16px !important; color: #333 !important; }
.acc-about-ver { color: #999 !important; font-size: 12px !important; }
.acc-about-item { display: flex !important; justify-content: space-between !important; padding: 3px 0 !important; border-bottom: 1px solid #f5f5f5 !important; }
.acc-about-label { color: #888 !important; font-weight: bold !important; }
/* Custom Dialog UI */
.acc-dialog-mask { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.3); z-index: 2000007; display: none; align-items: center; justify-content: center; backdrop-filter: blur(2px); }
.acc-dialog-box { background: white; width: 280px; border-radius: 12px; padding: 20px; box-shadow: 0 10px 25px rgba(0,0,0,0.2); animation: accPop 0.05s ease-out; display: flex; flex-direction: column; }
@keyframes accPop { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.acc-dialog-msg { font-size: 14px; color: #333; margin-bottom: 20px; line-height: 1.5; text-align: center; white-space: pre-wrap; font-weight: 500; }
.acc-dialog-footer { display: flex; gap: 10px; }
.acc-dialog-btn { flex: 1; padding: 8px 0; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; font-size: 13px; transition: 0.1s; }
.acc-dialog-btn:disabled { opacity:.5; cursor:not-allowed; }
.acc-dialog-btn-ok { background: #2196F3; color: white; }
.acc-dialog-btn-ok:hover { background: #1976D2; }
.acc-dialog-btn-ok.is-loading { display:flex; align-items:center; justify-content:center; }
.acc-inline-spinner { width:14px; height:14px; border:2px solid rgba(255,255,255,.35); border-top-color:#fff; border-radius:50%; animation:acc-spin .8s linear infinite; }
.acc-dialog-btn-cancel { background: #f5f5f5; color: #666; }
.acc-dialog-btn-cancel:hover { background: #e0e0e0; }
.acc-form-mask { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.3); z-index: 2000006; display: none; align-items: center; justify-content: center; backdrop-filter: blur(2px); }
.acc-form-box { background: white; width: 300px; border-radius: 12px; padding: 18px; box-shadow: 0 10px 25px rgba(0,0,0,0.2); display: flex; flex-direction: column; gap: 10px; animation: accPop 0.05s ease-out; }
.acc-form-title { font-size: 14px; font-weight: 700; color: #333; }
.acc-form-label { font-size: 12px; font-weight: 700; color: #667085; margin-bottom: -4px; }
.acc-required { color:#ef4444; margin-left:4px; }
.acc-form-footer { display: flex; gap: 10px; margin-top: 4px; }
.acc-toast { position:absolute; top:12px; left:50%; transform:translateX(-50%) translateY(-8px); display:flex; align-items:center; gap:6px; max-width:260px; padding:7px 10px; border:1px solid #d7e5f5; border-radius:999px; background:rgba(255,255,255,0.96); color:#36506b; box-shadow:0 10px 24px rgba(15, 23, 42, 0.12); font-size:12px; line-height:1; opacity:0; visibility:hidden; transition:opacity 0.18s ease, transform 0.18s ease; z-index:2000012; pointer-events:none; white-space:nowrap; }
.acc-toast.show { opacity:1; visibility:visible; transform:translateX(-50%) translateY(0); }
.acc-toast-icon { width:14px; height:14px; display:flex; align-items:center; justify-content:center; color:#2196F3; flex-shrink:0; }
.acc-toast-icon svg { font-size:14px; }
.acc-toast-text { overflow:hidden; text-overflow:ellipsis; }
/* Others ... */
.acc-switch-item { display:flex; align-items:stretch; margin-bottom:8px; position:relative; }
.acc-switch-item::before { content:""; position:absolute; left:-12px; top:0; bottom:0; width:16px; }
.acc-switch-card { flex:1; min-width:0; padding: 12px; padding-right: 40px; border: 1px solid #d0d5dd; border-radius: 8px; cursor: pointer; transition: 0.2s; position: relative; background: #fff; }
.acc-switch-card:hover { border-color: #2196F3; }
.acc-switch-card:hover .acc-card-name svg {fill: #2196F3 !important;stroke: #2196F3 !important;transition: all 0.2s ease;}
.acc-switch-card-static { cursor: default; }
.acc-switch-handle { position:absolute; left:-8px; top:0; bottom:0; width:6px; flex-shrink:0; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:1px; color:#c4c4c4; font-size:9px; font-weight:700; user-select:none; cursor:grab; line-height:1; border-radius:4px; padding:0; opacity:0; visibility:hidden; pointer-events:none; transition:opacity 0.15s ease, color 0.15s ease, background 0.15s ease; z-index:2; }
.acc-switch-handle span { display:block; letter-spacing:0; }
.acc-switch-item:hover .acc-switch-handle,
.acc-switch-item.dragging-source .acc-switch-handle { color:#2196F3; background:#f2f8fd; opacity:1; visibility:visible; pointer-events:auto; }
.acc-switch-ghost { box-shadow: 0 10px 24px rgba(15, 23, 42, 0.18); opacity: 0.96; }
.acc-switch-item.dragging-source .acc-switch-card { border:1px dashed #2196F3; opacity:0.45; background:#fff; }
.acc-switch-list-sorting .acc-switch-card:hover { border-color:#d0d5dd; background:#fff; }
.acc-switch-list-sorting .acc-switch-card:hover .acc-card-name svg { fill: currentColor !important; stroke: currentColor !important; }
.acc-switch-note-wrap { position:absolute; right:8px; bottom:38px; display:flex; flex-direction:column; align-items:flex-end; opacity:0; visibility:hidden; pointer-events:none; transition:all 0.15s ease; }
.acc-switch-item:hover .acc-switch-note-wrap,
.acc-switch-item.acc-note-active .acc-switch-note-wrap,
.acc-switch-note-wrap:focus-within { opacity:1; visibility:visible; pointer-events:auto; }
.acc-switch-note-btn { width:24px; height:24px; border:1px solid #ddd; border-radius:6px; background:transparent; color:#7d93a8; display:flex; align-items:center; justify-content:center; padding:0; cursor:pointer; transition:all 0.15s ease; }
.acc-switch-note-btn svg { font-size:14px; }
.acc-switch-note-btn:hover,
.acc-switch-note-btn:active,
.acc-switch-note-btn:focus-visible { color:#2196F3; border-color:#2196F3; background:#e3f2fd; outline:none; }
.acc-floating-note-tooltip { position:fixed; left:0; top:0; min-width:180px; max-width:280px; padding:8px 10px; border:1px solid #d7e5f5; border-radius:10px; background:rgba(255,255,255,0.98); color:#36506b; box-shadow:0 10px 24px rgba(15, 23, 42, 0.14); font-size:12px; line-height:1.45; opacity:0; visibility:hidden; transform:translateX(4px); transition:all 0.15s ease; pointer-events:auto; user-select:text; cursor:text; z-index:2000011; --acc-note-arrow-top:18px; overflow:visible; }
.acc-floating-note-tooltip.show { opacity:1; visibility:visible; transform:translateX(0); }
.acc-floating-note-tooltip-content { max-height:220px; overflow-y:auto; overflow-x:hidden; scrollbar-gutter:stable; white-space:pre-wrap; word-break:break-word; padding-right:2px; }
.acc-floating-note-tooltip::before,
.acc-floating-note-tooltip::after { content:""; position:absolute; left:100%; top:var(--acc-note-arrow-top); width:0; height:0; transform:translateY(-50%); border-style:solid; }
.acc-floating-note-tooltip::before { border-width:8px 0 8px 9px; border-color:transparent transparent transparent #d7e5f5; }
.acc-floating-note-tooltip::after { margin-left:-1px; border-width:7px 0 7px 8px; border-color:transparent transparent transparent rgba(255,255,255,0.98); }
.acc-floating-note-tooltip-content::-webkit-scrollbar { width:6px; }
.acc-floating-note-tooltip-content::-webkit-scrollbar-thumb { background:#d3d9e2; border-radius:999px; }
.acc-switch-settings-btn { position:absolute; right:8px; bottom:8px; width:24px; height:24px; border:1px solid #ddd; border-radius:6px; background:transparent; color:#7d93a8; display:flex; align-items:center; justify-content:center; padding:0; cursor:pointer; opacity:0; visibility:hidden; transition:all 0.15s ease; }
.acc-switch-settings-btn svg { font-size:14px; }
.acc-switch-item:hover .acc-switch-settings-btn,
.acc-switch-settings-btn:focus-visible { opacity:1; visibility:visible; }
.acc-switch-settings-btn:hover,
.acc-switch-settings-btn:active,
.acc-switch-settings-btn:focus-visible { color:#2196F3; border-color:#2196F3; background:#e3f2fd; }
.acc-card-body { flex:1; min-width:0; }
.acc-card-name { font-weight: 600; font-size: 14px; display: flex; align-items: center; gap: 6px; margin-bottom: 6px; color: #333; min-width:0; }
.acc-card-name-icon { flex-shrink:0; display:flex; align-items:center; justify-content:center; color:inherit; background:transparent; border:none; padding:0; cursor:pointer; transition:color 0.15s ease; }
.acc-card-name-icon:hover,
.acc-card-name-icon:focus-visible { color:#2196F3; outline:none; }
.acc-card-name-text { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.acc-card-meta { font-size: 10px; color: #999; display: flex; flex-wrap: wrap; gap: 4px; }
.acc-mini-tag { background: #f0f0f0; padding: 1px 5px; border-radius: 3px; color: #777; transition: all 0.2s; border: 1px solid transparent; }
.acc-click-tag { cursor: pointer; text-decoration: none; position: relative; }
.acc-click-tag:hover { background: #2196F3; color: white; border-color: #1976D2; z-index: 2; }
.acc-row-btn { display: flex; gap: 8px; align-items: center; margin-bottom:3px}
.acc-input-text { flex: 1; width:100%; padding: 8px; margin-bottom:8px; border: 1px solid #ddd; border-radius: 6px; font-size: 13px; box-sizing: border-box; background: #fff; color: #333; outline: none; transition: all 0.2s; }
.acc-input-text:focus { border-color: #2196F3; box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.2); }
.acc-password-mask-input { -webkit-text-security: disc; }
.acc-input-note { min-height:72px; resize:vertical; line-height:1.45; overflow-y:auto; overflow-x:hidden; overscroll-behavior:contain; }
.acc-btn { border: none; padding: 10px; border-radius: 6px; cursor: pointer; font-size: 13px; display: flex; align-items: center; justify-content: center; gap: 5px; transition: 0.2s; }
.acc-btn:disabled { opacity:.5; cursor:not-allowed; }
.acc-btn.is-loading { pointer-events:none; }
.acc-btn-blue { flex: 1; background: #2196F3; color: white; }
.acc-btn.is-loading .acc-inline-spinner { border-color: rgba(255,255,255,.35); border-top-color:#fff; }
.acc-btn-light.is-loading .acc-inline-spinner { border-color: rgba(102,102,102,.2); border-top-color:#666; }
.acc-btn-danger { width:100%; background:#ffebee; color:#c62828; border:1px solid #ffcdd2; }
.acc-btn-danger:hover { background:#ffcdd2; border-color:#ef9a9a; }
.acc-help-tip, .acc-lock-tip { display: inline-block; width: 16px; height: 16px; line-height: 16px; text-align: center; cursor: help; font-size: 16px; }
.acc-help-tip { color:#f5a623; margin-left:10px; margin-right:4px}
.acc-lock-tip { color: #999; }
.acc-set-group { margin-bottom: 10px; }
.acc-set-title { font-size: 12px; font-weight: bold; color: #999; margin-bottom: 8px; }
.acc-set-row { display: flex; gap: 10px; margin-bottom: 10px; padding: 0 3px;}
.acc-btn-light { background: #f5f5f5; color: #333; flex: 1; border: 1px solid #eee; }
.acc-btn-light:hover { background: #e0e0e0; }
.acc-btn-active { background: #2196F3 !important; color: white !important; border-color: #2196F3 !important; }
.acc-notice-content { line-height: 1.6; color: #444; font-size: 13px; }
.acc-notice-content h4 { margin: 15px 0 8px 0; color: #333; border-left: 3px solid #2196F3; padding-left: 8px; }
.acc-link-btn { color: #2196F3; cursor: pointer; font-size: 12px; }
.acc-select-ui { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 6px; font-size: 13px; background: #fff; cursor: pointer; outline: none; color: #333; }
.acc-chk {display:flex; align-items:center; flex-wrap:wrap; font-size:11px; color:#666; margin:5px 0;-webkit-user-select: none;}
.acc-chk-label { display: inline-flex !important; align-items: center !important; cursor: pointer !important; margin-right:4px; font-size: 12px; color: #666; }
.acc-chk-label.disabled { opacity: 0.45; cursor: not-allowed !important; }
.acc-custom-chk { appearance: none !important; width: 14px !important; height: 14px !important; border: 1px solid #ccc !important; border-radius: 3px !important; margin-right: 4px !important; cursor: pointer !important; position: relative !important; background: #fff; }
.acc-custom-chk:disabled { cursor: not-allowed !important; background: #f3f4f6 !important; border-color: #d0d5dd !important; }
.acc-custom-chk:checked { background-color: #2196F3 !important; border-color: #2196F3 !important; }
.acc-custom-chk:checked::after { content: ''; position: absolute !important; left: 4px !important; top: 1px !important; width: 3px !important; height: 7px !important; border: solid white !important; border-width: 0 2px 2px 0 !important; transform: rotate(45deg) !important; }
.acc-loading-mask{position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(255,255,255,.7);backdrop-filter:blur(2px);display:none;flex-direction:column;align-items:center;justify-content:center;z-index:2000010;border-radius:12px}
.acc-spinner{width:30px;height:30px;border:3px solid #f3f3f3;border-top:3px solid #2196F3;border-radius:50%;animation:acc-spin 1s linear infinite}
@keyframes acc-spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}
.acc-loading-text{margin-top:10px;font-size:12px;color:#2196F3;font-weight:700}
.acc-webdav-list { display:flex; flex-direction:column; gap:8px; }
.acc-webdav-head { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; }
.acc-webdav-status-row { display:flex; align-items:center; gap:6px; }
.acc-webdav-status { font-size:12px; color:#667085; }
.acc-webdav-logout-btn { width:24px; height:24px; min-width:24px; }
.acc-webdav-logout-btn:disabled { opacity:.5; cursor:not-allowed; }
.acc-webdav-config-btn { flex:0 0 auto; padding:8px 12px; }
.acc-webdav-item { border:1px solid #e5e7eb; border-radius:8px; padding:10px; background:#fff; display:flex; flex-direction:column; gap:8px; position:relative; }
.acc-webdav-item-main { min-width:0; }
.acc-webdav-item-name { font-size:12px; font-weight:700; color:#344054; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.acc-webdav-item-meta { display:flex; gap:4px; flex-wrap:wrap; margin-top:4px; }
.acc-webdav-meta-tag { font-size:10px; padding:0 4px; line-height:16px; }
.acc-webdav-item-actions { position:absolute; right:8px; bottom:8px; display:flex; gap:6px; opacity:0; visibility:hidden; transition:opacity .15s ease; }
.acc-webdav-item:hover .acc-webdav-item-actions,
.acc-webdav-item-actions:focus-within { opacity:1; visibility:visible; }
.acc-webdav-action-btn { width:24px; height:24px; min-width:24px; }
.acc-webdav-action-btn svg { font-size:14px; }
.acc-webdav-action-btn.danger { color:#c62828; border-color:#ffcdd2; background:#fff5f5; }
.acc-webdav-action-btn.danger:hover { background:#ffebee; border-color:#ef9a9a; color:#c62828; }
.acc-webdav-empty { padding:14px 10px; color:#98a2b3; text-align:center; font-size:12px; border:1px dashed #d0d5dd; border-radius:8px; background:#fafafa; }
`;
// src/app/state.js
function createState({ constants, i18nData }) {
const navLang = navigator.language.split("-")[0];
let currentLang = GM_getValue(constants.CFG.LANG, i18nData[navLang] ? navLang : "en");
const storedHostIconCache = GM_getValue(constants.CFG.HOST_ICON_CACHE, {});
if (!i18nData[currentLang]) {
currentLang = "en";
}
const hostIconCache = storedHostIconCache && typeof storedHostIconCache === "object" && !Array.isArray(storedHostIconCache) ? storedHostIconCache : {};
return {
currentLang,
currentViewingHost: constants.HOST,
hostDisplayMode: GM_getValue(constants.CFG.HOST_DISPLAY_MODE, "siteName"),
hostIconCache,
hostEditingHost: null,
hostEditingValue: "",
isForcedShow: false,
activePage: "pg-switch",
settingsReturnPage: "pg-switch",
accountSettingsKey: null,
accountSettingsReturnPage: "pg-switch",
accountSettingsHost: constants.HOST,
hostSearchQuery: "",
accountSearchQuery: "",
accountSearchActive: false,
webdavBackups: [],
uiRoot: null,
fab: null,
panel: null,
isFullscreenHidden: false,
dialogMask: null,
saveFormMask: null,
noteTooltipEl: null,
noteTooltipTarget: null,
noteTooltipItem: null,
toastEl: null,
toastTimer: null
};
}
// src/app/utils.js
function createUtils({ state, constants, i18nData }) {
let trustedHtmlPolicy;
return {
normalizeText(value) {
return String(value || "").replace(/\s+/g, " ").trim();
},
normalizeNoteText(value) {
return String(value || "").replace(/\r\n?/g, "\n").split("\n").map((line) => line.trim()).join("\n").trim();
},
t(key) {
return i18nData[state.currentLang][key] || key;
},
isWebDavTimeoutError(error) {
const message = String(error?.message || error || "").trim();
return message === this.t("webdav_timeout");
},
isWebDavRequestError(error) {
const message = String(error?.message || error || "").trim();
return /^(GET|PUT|POST|DELETE|PROPFIND|MKCOL|HEAD|OPTIONS|PATCH)\s+/i.test(message);
},
getWebDavErrorMessage(error, fallbackKey = "") {
const message = String(error?.message || error || "").trim();
if (this.isWebDavTimeoutError(error) || this.isWebDavRequestError(error)) {
return this.t("webdav_timeout_check_settings");
}
return message || (fallbackKey ? this.t(fallbackKey) : "");
},
escapeHtml(value) {
return String(value || "").replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'");
},
toTrustedHtml(html) {
const normalized = String(html || "");
if (trustedHtmlPolicy === void 0) {
try {
trustedHtmlPolicy = typeof trustedTypes !== "undefined" && typeof trustedTypes.createPolicy === "function" ? trustedTypes.getPolicy?.("anme-html") || trustedTypes.createPolicy("anme-html", {
createHTML: (value) => String(value || "")
}) : null;
} catch {
trustedHtmlPolicy = null;
}
}
return trustedHtmlPolicy ? trustedHtmlPolicy.createHTML(normalized) : normalized;
},
setHTML(element, html) {
if (!element) return;
element.innerHTML = this.toTrustedHtml(html);
},
extractName(key) {
return key.split("::")[1] || key;
},
makeKey(name, host = constants.HOST) {
return `${constants.PREFIX}${host}::${name}`;
},
listAllHosts() {
return [
...new Set(
GM_listValues().filter((value) => value.startsWith(constants.PREFIX)).map((value) => value.split("::")[0].replace(constants.PREFIX, ""))
)
];
},
getSortedKeysByHost(host) {
const allKeys = GM_listValues().filter((key) => key.startsWith(`${constants.PREFIX}${host}::`));
const savedOrder = GM_getValue(constants.ORDER_PREFIX + host, []);
return allKeys.sort((a, b) => {
const nameA = this.extractName(a);
const nameB = this.extractName(b);
let idxA = savedOrder.indexOf(nameA);
let idxB = savedOrder.indexOf(nameB);
if (idxA === -1) idxA = 9999;
if (idxB === -1) idxB = 9999;
if (idxA !== idxB) return idxA - idxB;
const dataA = GM_getValue(a);
const dataB = GM_getValue(b);
return new Date(dataB.time || 0) - new Date(dataA.time || 0);
});
},
formatTime(timestamp) {
if (!timestamp) return "";
if (typeof timestamp === "number") {
return new Date(timestamp).toLocaleString();
}
const parsed = new Date(timestamp);
return Number.isNaN(parsed.getTime()) ? String(timestamp) : parsed.toLocaleString();
},
formatBytes(size) {
const bytes = Number(size) || 0;
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
},
normalizeSiteName(siteName, host = constants.HOST) {
return this.normalizeText(siteName) || host;
},
getPageTitle() {
return typeof document !== "undefined" ? document.title : "";
},
findStoredSiteName(host) {
const savedSiteName = this.normalizeText(GM_getValue(constants.SITE_NAME_PREFIX + host, ""));
if (savedSiteName) {
return savedSiteName;
}
const keys = this.getSortedKeysByHost(host);
for (const key of keys) {
const siteName = this.normalizeText(GM_getValue(key)?.siteName);
if (siteName) {
return siteName;
}
}
return "";
},
getSiteNameByHost(host, fallbackTitle = "") {
const storedSiteName = this.findStoredSiteName(host);
if (storedSiteName) {
return storedSiteName;
}
return host;
},
getHostDisplayName(host, mode = state.hostDisplayMode || "siteName") {
return mode === "domain" ? host : this.getSiteNameByHost(host);
},
getCachedHostIcon(host) {
const cacheEntry = state.hostIconCache?.[host];
if (!cacheEntry || typeof cacheEntry !== "object") return "";
return typeof cacheEntry.dataUrl === "string" ? cacheEntry.dataUrl : "";
},
getHostIconFallbackText(host, label = "") {
const normalized = this.normalizeText(label || host).replace(/^www\./i, "");
const firstChar = Array.from(normalized).find((char) => /[A-Za-z0-9\u4E00-\u9FFF]/.test(char));
return (firstChar || "#").toUpperCase();
},
suggestSiteName(pageTitle = this.getPageTitle(), host = constants.HOST) {
const storedSiteName = this.findStoredSiteName(host);
if (storedSiteName) {
return storedSiteName;
}
return this.normalizeSiteName(pageTitle, host);
},
suggestAccountName(host = constants.HOST) {
const existingNames = this.getSortedKeysByHost(host).map((key) => this.extractName(key));
const translatedPrefix = this.t("default_account_prefix");
const fallbackPrefixes = {
zh: "账号",
en: "Account",
es: "Cuenta"
};
const prefix = this.normalizeText(
translatedPrefix && translatedPrefix !== "default_account_prefix" ? translatedPrefix : fallbackPrefixes[state.currentLang]
) || "Account";
let index = existingNames.length + 1;
let candidate = "";
do {
candidate = `${prefix}-${String(index).padStart(2, "0")}`;
index += 1;
} while (existingNames.includes(candidate));
return candidate;
}
};
}
// src/app/templates.js
function createTemplates({ state, constants, i18nData, utils }) {
return {
panel() {
const langOptions = Object.keys(i18nData).map(
(code) => `${i18nData[code]._name} `
).join("");
return `
`;
},
switchCard(key, data) {
const switchable = state.currentViewingHost === constants.HOST;
const accountName = utils.extractName(key);
const escapedAccountName = utils.escapeHtml(accountName);
const accountNote = utils.normalizeNoteText(data?.note);
const escapedAccountNote = utils.escapeHtml(accountNote);
return `
:: ::
${accountNote ? `
${constants.ICONS.NOTICE}
` : ""}
${constants.ICONS.SETTINGS}
${constants.ICONS.USER}
${escapedAccountName}
${utils.formatTime(data.time)}
${data.cookies?.length || 0 ? `CK: ${data.cookies.length} ` : ""}
${Object.keys(data.localStorage || {}).length ? `LS: ${Object.keys(data.localStorage).length} ` : ""}
${Object.keys(data.sessionStorage || {}).length ? `SS: ${Object.keys(data.sessionStorage).length} ` : ""}
`;
},
noData() {
return `${utils.t("no_data")}
`;
}
};
}
// src/app/core/accounts.js
function createAccountMethods({ constants, utils, getUI, getCore, shared }) {
return {
async detectAvailableSnapshotSources() {
const cookies = await shared.listCookies();
return {
ck: Array.isArray(cookies) && cookies.length > 0,
ls: Object.keys(localStorage || {}).length > 0,
ss: Object.keys(sessionStorage || {}).length > 0
};
},
async saveAccount(name, siteName, options = { ck: true, ls: false, ss: false, note: "" }) {
const ui = getUI();
const snapshot = {
time: Date.now(),
siteName: utils.normalizeSiteName(siteName),
note: utils.normalizeNoteText(options.note),
localStorage: options.ls ? { ...localStorage } : {},
sessionStorage: options.ss ? { ...sessionStorage } : {},
cookies: []
};
if (options.ck) {
snapshot.cookies = await shared.listCookies();
}
const hasCookies = snapshot.cookies && snapshot.cookies.length > 0;
const hasLS = Object.keys(snapshot.localStorage).length > 0;
const hasSS = Object.keys(snapshot.sessionStorage).length > 0;
if (!hasCookies && !hasLS && !hasSS) {
await ui.alert(utils.t("save_empty_err"));
return false;
}
GM_setValue(utils.makeKey(name), snapshot);
this.updateSiteName(constants.HOST, snapshot.siteName);
const currentOrder = GM_getValue(constants.ORDER_PREFIX + constants.HOST, []);
if (!currentOrder.includes(name)) {
currentOrder.push(name);
GM_setValue(constants.ORDER_PREFIX + constants.HOST, currentOrder);
}
getCore()?.syncHostIconCache?.();
return true;
},
renameAccount(oldKey, newName, host) {
return this.updateAccount(oldKey, { name: newName }, host);
},
updateAccount(oldKey, nextValues, host) {
const data = GM_getValue(oldKey);
if (!data) return oldKey;
const nextName = utils.normalizeText(nextValues?.name || utils.extractName(oldKey));
const nextKey = utils.makeKey(nextName, host);
const nextData = {
...data,
note: utils.normalizeNoteText(nextValues?.note ?? data.note)
};
if (nextKey !== oldKey) {
GM_deleteValue(oldKey);
}
GM_setValue(nextKey, nextData);
const orderKey = constants.ORDER_PREFIX + host;
if (nextKey !== oldKey) {
const order = GM_getValue(orderKey, []);
const idx = order.indexOf(utils.extractName(oldKey));
if (idx !== -1) {
order[idx] = nextName;
GM_setValue(orderKey, order);
}
}
return nextKey;
},
updateSiteName(host, siteName) {
const normalizedSiteName = utils.normalizeSiteName(siteName, host);
GM_setValue(constants.SITE_NAME_PREFIX + host, normalizedSiteName);
utils.getSortedKeysByHost(host).forEach((key) => {
const data = GM_getValue(key);
if (!data) return;
GM_setValue(key, {
...data,
siteName: normalizedSiteName
});
});
},
deleteAccount(key, host) {
GM_deleteValue(key);
const orderKey = constants.ORDER_PREFIX + host;
const name = utils.extractName(key);
const order = GM_getValue(orderKey, []);
const newOrder = order.filter((item) => item !== name);
if (newOrder.length === 0) {
GM_deleteValue(orderKey);
getCore()?.removeHostIconCache?.(host);
} else {
GM_setValue(orderKey, newOrder);
}
},
updateOrder(host, nameList) {
GM_setValue(constants.ORDER_PREFIX + host, nameList);
}
};
}
// src/app/core/backup.js
function createBackupMethods({ constants, utils, getUI }) {
return {
buildExportObject(scope) {
const exportObj = {};
const allKeys = GM_listValues();
const targetAccountKeys = scope === "current" ? allKeys.filter((key) => key.startsWith(`${constants.PREFIX}${constants.HOST}::`)) : allKeys.filter((key) => key.startsWith(constants.PREFIX));
if (targetAccountKeys.length === 0) {
return null;
}
targetAccountKeys.forEach((key) => {
exportObj[key] = GM_getValue(key);
});
if (scope === "current") {
const orderKey = constants.ORDER_PREFIX + constants.HOST;
const orderValue = GM_getValue(orderKey);
if (orderValue) exportObj[orderKey] = orderValue;
const siteNameKey = constants.SITE_NAME_PREFIX + constants.HOST;
const siteNameValue = GM_getValue(siteNameKey);
if (siteNameValue) exportObj[siteNameKey] = siteNameValue;
} else {
allKeys.filter((key) => key.startsWith(constants.ORDER_PREFIX) || key.startsWith(constants.SITE_NAME_PREFIX)).forEach((key) => {
exportObj[key] = GM_getValue(key);
});
}
return exportObj;
},
async exportData(scope) {
const ui = getUI();
const exportObj = this.buildExportObject(scope);
if (!exportObj) {
await ui.alert(utils.t("export_err"));
return;
}
const blob = new Blob([JSON.stringify(exportObj, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const downloadSite = scope === "current" ? constants.HOST : "All_Sites";
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = `${constants.META.NAME}_Backup_${downloadSite}_${(/* @__PURE__ */ new Date()).toLocaleString("sv-SE").replace(" ", "_").replace(/:/g, "-")}.json`;
anchor.click();
URL.revokeObjectURL(url);
},
importBackupObject(data, source = "import") {
let count = 0;
const hostsInFile = [
...new Set(
Object.keys(data).filter((key) => key.startsWith(constants.PREFIX)).map((key) => key.replace(constants.PREFIX, "").split("::")[0])
)
];
hostsInFile.forEach((host) => {
const orderKey = constants.ORDER_PREFIX + host;
const siteNameKey = constants.SITE_NAME_PREFIX + host;
const fileOrder = data[orderKey] || [];
const localOrder = GM_getValue(orderKey, []);
if (data[siteNameKey]) {
GM_setValue(siteNameKey, data[siteNameKey]);
}
const namesToImport = fileOrder.length > 0 ? fileOrder : Object.keys(data).filter((key) => key.startsWith(`${constants.PREFIX}${host}::`)).map((key) => key.split("::")[1]);
namesToImport.forEach((name) => {
const fullKey = `${constants.PREFIX}${host}::${name}`;
if (!data[fullKey]) return;
GM_setValue(fullKey, data[fullKey]);
if (!localOrder.includes(name)) {
localOrder.push(name);
}
count += 1;
});
GM_setValue(orderKey, localOrder);
});
return {
count,
messageKey: source === "sync" ? "sync_restore_ok" : "import_ok"
};
},
async importData(file) {
const ui = getUI();
const reader = new FileReader();
reader.onload = async (event) => {
try {
const data = JSON.parse(event.target.result);
const result = this.importBackupObject(data, "import");
await ui.alert(utils.t(result.messageKey).replace("{count}", result.count));
ui.refresh();
} catch {
await ui.alert(utils.t("import_err"));
}
};
reader.readAsText(file);
},
clearAllData() {
GM_listValues().forEach((key) => {
if (key.startsWith(constants.PREFIX) || key.startsWith(constants.ORDER_PREFIX) || key.startsWith(constants.SITE_NAME_PREFIX) || key === constants.CFG.HOST_ICON_CACHE) {
GM_deleteValue(key);
}
});
}
};
}
// src/app/core/environment.js
function createEnvironmentMethods({ getUI, shared }) {
return {
async loadAccount(key) {
const ui = getUI();
const data = GM_getValue(key);
if (!data) return;
ui.toggleLoading(true, "Switching...");
try {
shared.clearBrowserStorage();
await shared.deleteAllCookies();
Object.entries(data.localStorage || {}).forEach(([storageKey, value]) => localStorage.setItem(storageKey, value));
Object.entries(data.sessionStorage || {}).forEach(([storageKey, value]) => sessionStorage.setItem(storageKey, value));
for (const cookie of data.cookies || []) {
const cookieData = { ...cookie };
delete cookieData.hostOnly;
delete cookieData.session;
await shared.setCookie(cookieData);
}
location.reload();
} catch (error) {
console.error(error);
ui.toggleLoading(false);
ui.alert("Switch failed!");
}
},
async cleanEnvironment() {
const ui = getUI();
ui.toggleLoading(true, "Cleaning...");
shared.clearBrowserStorage();
await shared.deleteAllCookies();
location.reload();
}
};
}
// src/app/core/inspector.js
function createInspectorMethods({ constants, utils }) {
return {
inspectData(key, type) {
const data = GM_getValue(key);
if (!data) return;
let content = null;
if (type === "cookies") content = data.cookies;
if (type === "localStorage") content = data.localStorage;
if (type === "sessionStorage") content = data.sessionStorage;
if (!content) return;
const inspectorWindow = window.open("", "_blank");
if (!inspectorWindow) return;
utils.setHTML(
inspectorWindow.document.head,
` `
);
const noDataHtml = "No data to display.
";
const escapeHtml = (value) => {
if (value === null || value === void 0) return "";
return String(value).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
};
const createTable = (headers, dataRows, rowClasses = [], options = {}) => {
const wrap = (cellContent, className = "") => `${cellContent}
`;
const extraClass = headers.length === 2 ? " kv-table" : "";
const columnWidths = options.columnWidths || headers.map(() => "");
const cellClasses = options.cellClasses || headers.map(() => "");
const colGroup = columnWidths.some(Boolean) ? `${columnWidths.map((width) => ` `).join("")} ` : "";
let table = `";
return table;
};
let inspectorHtml;
if (type === "cookies") {
if (Array.isArray(content) && content.length > 0) {
const originalHeaders = Object.keys(content[0]);
const preferredOrder = ["name", "value", "expirationDate"];
const headers = [
...preferredOrder,
...originalHeaders.filter((header) => !preferredOrder.includes(header) && header !== "partitionKey")
];
const rowClasses = content.map((cookie) => cookie.httpOnly ? "http-only" : "");
const dataRows = content.map(
(cookie) => headers.map((header) => {
const value = cookie[header];
if (value === void 0 || value === null) return "";
if (header === "expirationDate" && typeof value === "number") {
return {
value: new Date(value * 1e3).toLocaleString(),
className: value * 1e3 < Date.now() ? "cell-expired" : ""
};
}
return typeof value === "object" ? JSON.stringify(value) : value;
})
);
inspectorHtml = createTable(headers, dataRows, rowClasses, {
columnWidths: headers.map((header) => {
if (header === "name") return "180px";
if (header === "value") return "min(560px, 48vw)";
if (header === "expirationDate") return "180px";
return "140px";
}),
cellClasses: headers.map((header) => header === "value" || header === "domain" ? "cell-break" : "")
});
} else {
inspectorHtml = noDataHtml;
}
} else if (typeof content === "object" && Object.keys(content).length > 0) {
inspectorHtml = createTable(["Key", "Value"], Object.entries(content), [], {
columnWidths: ["260px", "auto"],
cellClasses: ["", "cell-break"]
});
} else {
inspectorHtml = noDataHtml;
}
inspectorWindow.document.title = "AnMe Inspector";
const style = inspectorWindow.document.createElement("style");
style.textContent = `
body { font-family: system-ui, -apple-system, sans-serif; padding: 20px; background: #f8f9fa; color: #212529; margin: 0; }
h3 { color: #212529; border-bottom: 1px solid #dee2e6; padding-bottom: 10px; margin-top: 0; }
.table-container { border: 1px solid #dee2e6; border-radius: 8px; background: #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.05); overflow: auto; max-height: 90vh; }
table { width: 100%; min-width: 100%; border-collapse: collapse; table-layout: fixed; }
th, td { border: 1px solid #e9ecef; text-align: left; vertical-align: top; font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace; font-size: 13px; padding: 0; }
.cell-content { display: block; width: 100%; min-width: 0; box-sizing: border-box; padding: 12px 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cell-break { white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; text-overflow: clip; }
.cell-expired { color: #d92d20; font-weight: 600; }
th { background-color: #f1f3f5; font-weight: 600; position: sticky; top: 0; z-index: 10; text-align: center; padding: 12px 15px; white-space: nowrap; }
tr:nth-child(even) { background-color: #f8f9fa; }
tr.http-only { background-color: #e2e6ea; border-bottom: 1px solid #d6d8db; }
p { margin-top: 20px; }
`;
inspectorWindow.document.head.appendChild(style);
utils.setHTML(inspectorWindow.document.body, `
${utils.extractName(key)} - ${type}
${inspectorHtml}
`);
}
};
}
// src/app/core/shared.js
function createCoreShared() {
return {
listCookies() {
return new Promise((resolve) => GM_cookie.list({}, resolve));
},
deleteCookie(name) {
return new Promise((resolve) => GM_cookie.delete({ name }, resolve));
},
setCookie(cookieData) {
return new Promise((resolve) => GM_cookie.set(cookieData, resolve));
},
async deleteAllCookies() {
const cookies = await this.listCookies();
for (const cookie of cookies || []) {
await this.deleteCookie(cookie.name);
}
},
clearBrowserStorage() {
localStorage.clear();
sessionStorage.clear();
}
};
}
// src/app/core/webdav.js
function encodeBasicAuth(username, password) {
return `Basic ${btoa(unescape(encodeURIComponent(`${username}:${password}`)))}`;
}
var textEncoder = new TextEncoder();
var textDecoder = new TextDecoder();
var WEBDAV_PASSWORD_PREFIX = "enc:v2:";
function createRandomHex(length = 32) {
const bytes = new Uint8Array(Math.ceil(length / 2));
if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
crypto.getRandomValues(bytes);
} else {
for (let index = 0; index < bytes.length; index += 1) {
bytes[index] = Math.floor(Math.random() * 256);
}
}
return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("").slice(0, length);
}
function toBinaryText(value) {
return unescape(encodeURIComponent(String(value || "")));
}
function fromBinaryText(value) {
try {
return decodeURIComponent(escape(value));
} catch {
return "";
}
}
function xorBinaryText(sourceText, secret) {
const source = String(sourceText || "");
const key = toBinaryText(secret || "anme-webdav") || "anme-webdav";
let masked = "";
for (let index = 0; index < source.length; index += 1) {
masked += String.fromCharCode(source.charCodeAt(index) ^ key.charCodeAt(index % key.length));
}
return masked;
}
function encryptWebDavPassword(password, secret) {
const plainText = String(password || "");
if (!plainText) return "";
return `${WEBDAV_PASSWORD_PREFIX}${btoa(xorBinaryText(toBinaryText(plainText), secret))}`;
}
function decryptWebDavPassword(passwordCipher, secret) {
const cipherText = String(passwordCipher || "");
if (!cipherText) return "";
if (!cipherText.startsWith(WEBDAV_PASSWORD_PREFIX)) return "";
try {
return fromBinaryText(xorBinaryText(atob(cipherText.slice(WEBDAV_PASSWORD_PREFIX.length)), secret));
} catch {
return "";
}
}
function normalizeBaseUrl(url) {
return String(url || "").trim().replace(/\/+$/, "");
}
function normalizeDirectory(directory) {
return String(directory || "").trim().replace(/^\/+/, "").replace(/\/+$/, "");
}
function getManagedDirectory(constants) {
return normalizeDirectory(
`${String(constants.META.NAME || "anme").toLowerCase().replace(/[^a-z0-9]+/g, "-")}-webdav`
);
}
function getBackupExtension(constants) {
return ".anme";
}
function withManagedDirectory(config, constants) {
return {
...config,
directory: getManagedDirectory(constants)
};
}
function joinRemoteUrl(remoteUrl, fileName) {
return `${remoteUrl.replace(/\/+$/, "")}/${encodeURIComponent(fileName)}`;
}
function appendCacheBust(url) {
const separator = url.includes("?") ? "&" : "?";
return `${url}${separator}_=${Date.now()}`;
}
function toRemoteUrl(config) {
const baseUrl = normalizeBaseUrl(config.url);
const directory = normalizeDirectory(config.directory);
return directory ? `${baseUrl}/${directory}` : baseUrl;
}
function isBackupArchiveFile(fileName, constants) {
const normalizedName = String(fileName || "").toLowerCase();
return normalizedName.endsWith(".zip") || normalizedName.endsWith(getBackupExtension(constants));
}
function isGzipData(bytes) {
return bytes.length >= 2 && bytes[0] === 31 && bytes[1] === 139;
}
function isZipData(bytes) {
return bytes.length >= 4 && bytes[0] === 80 && bytes[1] === 75 && bytes[2] === 3 && bytes[3] === 4;
}
async function readStreamToUint8Array(stream) {
const response = new Response(stream);
return new Uint8Array(await response.arrayBuffer());
}
async function gzipBytes(bytes) {
if (typeof CompressionStream === "undefined") {
return bytes;
}
return readStreamToUint8Array(new Blob([bytes]).stream().pipeThrough(new CompressionStream("gzip")));
}
async function gunzipBytes(bytes, utils) {
if (typeof DecompressionStream === "undefined") {
throw new Error(utils.t("sync_restore_err"));
}
return readStreamToUint8Array(new Blob([bytes]).stream().pipeThrough(new DecompressionStream("gzip")));
}
async function encodeBackupPayload(exportObj, constants) {
const payload = {
format: "anme-webdav-v2",
meta: {
name: constants.META.NAME,
version: constants.META.VERSION,
exportedAt: (/* @__PURE__ */ new Date()).toISOString()
},
backup: exportObj
};
return gzipBytes(textEncoder.encode(JSON.stringify(payload)));
}
async function decodeBackupPayload(arrayBuffer, utils) {
const bytes = new Uint8Array(arrayBuffer);
if (isZipData(bytes)) {
throw new Error(utils.t("sync_restore_err"));
}
const rawBytes = isGzipData(bytes) ? await gunzipBytes(bytes, utils) : bytes;
const parsed = JSON.parse(textDecoder.decode(rawBytes));
return parsed && typeof parsed === "object" && parsed.backup ? parsed.backup : parsed;
}
function parseWebDavList(xmlText, baseUrl, constants) {
const doc = new DOMParser().parseFromString(xmlText, "application/xml");
const responses = [...doc.getElementsByTagNameNS("*", "response")];
const basePath = new URL(baseUrl).pathname.replace(/\/+$/, "");
return responses.map((response) => {
const href = response.getElementsByTagNameNS("*", "href")[0]?.textContent || "";
const prop = response.getElementsByTagNameNS("*", "prop")[0];
const resourcetype = prop?.getElementsByTagNameNS("*", "resourcetype")[0];
const isCollection = Boolean(resourcetype?.getElementsByTagNameNS("*", "collection")[0]);
const lastModified = prop?.getElementsByTagNameNS("*", "getlastmodified")[0]?.textContent || "";
const contentLength = prop?.getElementsByTagNameNS("*", "getcontentlength")[0]?.textContent || "0";
const hrefUrl = href ? new URL(href, baseUrl) : null;
const pathname = hrefUrl?.pathname.replace(/\/+$/, "") || "";
const fileName = hrefUrl ? decodeURIComponent(pathname.split("/").pop() || "") : "";
return {
pathname,
fileName,
isCollection,
lastModified,
size: Number(contentLength) || 0
};
}).filter((item) => item.fileName && item.pathname !== basePath && !item.isCollection && isBackupArchiveFile(item.fileName, constants)).sort((a, b) => new Date(b.lastModified || 0) - new Date(a.lastModified || 0));
}
function createWebDavMethods({ constants, utils, getUI, getCore }) {
const noCacheHeaders = {
"Cache-Control": "no-store, no-cache, max-age=0",
Pragma: "no-cache"
};
const getOrCreateWebDavSecret = () => {
const existingSecret = String(GM_getValue(constants.CFG.WEBDAV_SECRET, "") || "");
if (existingSecret) {
return existingSecret;
}
const nextSecret = createRandomHex(32);
GM_setValue(constants.CFG.WEBDAV_SECRET, nextSecret);
return nextSecret;
};
const request = (config, { method = "GET", url, headers = {}, data, responseType = "text", fetch = false }) => new Promise((resolve, reject) => {
let settled = false;
let timedOut = false;
let timeoutTimer = null;
const finish = (handler) => (payload) => {
if (settled) return;
settled = true;
if (timeoutTimer) {
clearTimeout(timeoutTimer);
timeoutTimer = null;
}
handler(payload);
};
const requestOptions = {
method,
url,
fetch,
responseType,
headers: {
Authorization: encodeBasicAuth(config.username, config.password),
...headers
},
onload: finish((response) => {
if (response.status >= 200 && response.status < 300) {
resolve(response);
return;
}
reject(new Error(`${method} ${url} failed with ${response.status}`));
}),
onerror: finish(
() => reject(new Error(timedOut ? utils.t("webdav_timeout") : `${method} ${url} failed`))
)
};
if (data !== void 0 && data !== null) {
requestOptions.data = data;
}
const xhr = GM_xmlhttpRequest(requestOptions);
timeoutTimer = setTimeout(() => {
timedOut = true;
try {
xhr?.abort?.();
} catch {
}
finish(() => reject(new Error(utils.t("webdav_timeout"))))();
}, 5e3);
});
return {
getCachedWebDavBackups() {
const backups = GM_getValue(constants.CFG.WEBDAV_BACKUPS_CACHE, []);
return Array.isArray(backups) ? backups : [];
},
saveCachedWebDavBackups(backups) {
const normalizedBackups = Array.isArray(backups) ? backups.map((item) => ({
fileName: String(item.fileName || ""),
lastModified: String(item.lastModified || ""),
size: Number(item.size) || 0
})) : [];
GM_setValue(constants.CFG.WEBDAV_BACKUPS_CACHE, normalizedBackups);
return normalizedBackups;
},
getWebDavConfig() {
const config = GM_getValue(constants.CFG.WEBDAV_CONFIG, {});
const secret = getOrCreateWebDavSecret();
const password = decryptWebDavPassword(String(config.passwordCipher || ""), secret);
return {
url: String(config.url || ""),
username: String(config.username || ""),
password,
directory: getManagedDirectory(constants)
};
},
saveWebDavConfig(config) {
const normalizedConfig = withManagedDirectory(config, constants);
const password = String(normalizedConfig.password || "");
GM_setValue(constants.CFG.WEBDAV_CONFIG, {
url: normalizeBaseUrl(normalizedConfig.url),
username: String(normalizedConfig.username || "").trim(),
passwordCipher: encryptWebDavPassword(password, getOrCreateWebDavSecret())
});
},
clearWebDavConfig() {
GM_deleteValue(constants.CFG.WEBDAV_CONFIG);
GM_deleteValue(constants.CFG.WEBDAV_SECRET);
GM_deleteValue(constants.CFG.WEBDAV_BACKUPS_CACHE);
},
hasWebDavConfig() {
const config = this.getWebDavConfig();
return Boolean(config.url && config.username && config.password);
},
async verifyWriteAccess(config) {
const remoteUrl = toRemoteUrl(config);
const tempFileName = `.anme-webdav-check-${Date.now()}.tmp`;
const tempFileUrl = joinRemoteUrl(remoteUrl, tempFileName);
const uploadTempFile = async () => request(config, {
method: "PUT",
url: tempFileUrl,
data: "anme-webdav-check",
headers: {
"Content-Type": "text/plain;charset=utf-8"
}
});
if (!config.directory) {
await uploadTempFile();
} else {
try {
await uploadTempFile();
} catch (uploadError) {
try {
await request(config, {
method: "MKCOL",
url: remoteUrl
});
} catch {
}
try {
await uploadTempFile();
} catch (retryError) {
throw retryError || uploadError;
}
}
}
try {
await request(config, {
method: "DELETE",
url: tempFileUrl
});
} catch {
}
return remoteUrl;
},
async readWebDavDirectory(config) {
const remoteUrl = toRemoteUrl(config);
const response = await request(config, {
method: "PROPFIND",
url: appendCacheBust(remoteUrl),
headers: {
Depth: "1",
...noCacheHeaders
}
});
return parseWebDavList(response.responseText || "", remoteUrl, constants).map((item) => ({
fileName: item.fileName,
lastModified: item.lastModified ? new Date(item.lastModified).toISOString() : "",
size: item.size
}));
},
normalizeWebDavConfig(config) {
return withManagedDirectory({
url: normalizeBaseUrl(config.url),
username: String(config.username || "").trim(),
password: String(config.password || "")
}, constants);
},
async validateWebDavConfig(config) {
const normalizedConfig = this.normalizeWebDavConfig(config);
if (!normalizedConfig.url || !normalizedConfig.username || !normalizedConfig.password) {
throw new Error(utils.t("webdav_missing_config"));
}
await this.verifyWriteAccess(normalizedConfig);
await this.readWebDavDirectory(normalizedConfig);
return normalizedConfig;
},
async getValidatedWebDavConfig() {
return this.validateWebDavConfig(this.getWebDavConfig());
},
async listWebDavBackups() {
const config = await this.getValidatedWebDavConfig();
let backups = await this.readWebDavDirectory(config);
backups = backups.sort((a, b) => new Date(b.lastModified || 0) - new Date(a.lastModified || 0));
return this.saveCachedWebDavBackups(backups);
},
async uploadWebDavBackup() {
const config = await this.getValidatedWebDavConfig();
const core = getCore();
const exportObj = core.buildExportObject("all");
if (!exportObj) {
await getUI().alert(utils.t("export_err"));
return null;
}
const remoteUrl = toRemoteUrl(config);
const archiveBytes = await encodeBackupPayload(exportObj, constants);
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
const fileName = `${constants.META.NAME}_Sync_${timestamp}${getBackupExtension(constants)}`;
await request(config, {
method: "PUT",
url: joinRemoteUrl(remoteUrl, fileName),
data: archiveBytes.buffer,
headers: {
"Content-Type": "application/octet-stream"
}
});
const backups = await this.readWebDavDirectory(config);
const nextBackups = [
{
fileName,
lastModified: (/* @__PURE__ */ new Date()).toISOString(),
size: archiveBytes.byteLength
},
...backups.filter((item) => item.fileName !== fileName)
].sort((a, b) => new Date(b.lastModified || 0) - new Date(a.lastModified || 0));
this.saveCachedWebDavBackups(nextBackups);
return fileName;
},
async restoreWebDavBackup(fileName) {
const config = await this.getValidatedWebDavConfig();
const remoteUrl = toRemoteUrl(config);
const response = await request(config, {
method: "GET",
url: joinRemoteUrl(remoteUrl, fileName),
responseType: "arraybuffer",
fetch: true,
headers: {
Accept: "application/octet-stream",
"X-Requested-With": "XMLHttpRequest",
"Cache-Control": "no-store"
}
});
const backupData = await decodeBackupPayload(response.response, utils);
const result = getCore().importBackupObject(backupData, "sync");
getUI().refresh();
return result;
},
async deleteWebDavBackup(fileName) {
const config = await this.getValidatedWebDavConfig();
const remoteUrl = toRemoteUrl(config);
const backups = await this.readWebDavDirectory(config);
await request(config, {
method: "DELETE",
url: joinRemoteUrl(remoteUrl, fileName)
});
const nextBackups = backups.filter((item) => item.fileName !== fileName);
this.saveCachedWebDavBackups(nextBackups);
}
};
}
// src/app/core.js
function createCore({ state, constants, utils }) {
let ui = null;
const shared = createCoreShared();
const hostIconFetchJobs = /* @__PURE__ */ new Map();
const getHostIconCache = () => {
if (!state.hostIconCache || typeof state.hostIconCache !== "object" || Array.isArray(state.hostIconCache)) {
state.hostIconCache = {};
}
return state.hostIconCache;
};
const syncHostIconCache = () => {
const nextCache = {};
Object.entries(getHostIconCache()).forEach(([host, entry]) => {
if (host && entry?.dataUrl && utils.getSortedKeysByHost(host).length > 0) {
nextCache[host] = { dataUrl: entry.dataUrl };
}
});
GM_setValue(constants.CFG.HOST_ICON_CACHE, nextCache);
};
const getCachedHostIcon = (host) => {
const entry = host ? getHostIconCache()[host] : null;
return entry && typeof entry === "object" && entry.dataUrl ? entry.dataUrl : "";
};
const blobToDataUrl = (blob, mimeType) => new Promise((resolve, reject) => {
try {
const normalizedBlob = mimeType && blob.type !== mimeType ? blob.slice(0, blob.size, mimeType) : blob;
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result || ""));
reader.onerror = () => reject(new Error("favicon_read_failed"));
reader.readAsDataURL(normalizedBlob);
} catch (error) {
reject(error);
}
});
const buildCurrentHostIconSources = () => {
const sources = [];
if (typeof document !== "undefined") {
document.querySelectorAll('link[rel*="icon"][href]').forEach((element) => {
try {
const rawHref = String(element.getAttribute("href") || "").trim();
if (!rawHref) return;
if (/^data:/i.test(rawHref)) {
sources.push({ type: "inline", value: rawHref });
return;
}
const iconUrl = new URL(rawHref, location.href);
if (iconUrl.protocol === "http:" || iconUrl.protocol === "https:") {
sources.push({ type: "request", value: iconUrl.href });
}
} catch (_) {
return;
}
});
}
sources.push({ type: "request", value: new URL("/favicon.ico", location.origin).href });
return [...new Map(sources.filter((source) => source?.value).map((source) => [source.value, source])).values()];
};
const requestHostIcon = (url) => new Promise((resolve, reject) => {
const xhr = GM_xmlhttpRequest({
method: "GET",
url,
responseType: "blob",
timeout: 5e3,
headers: {
Accept: "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"
},
onload: async (response) => {
if (response.status < 200 || response.status >= 300 || !response.response) {
reject(new Error(`favicon_http_${response.status || 0}`));
return;
}
try {
const blob = response.response;
if (!blob.size) {
reject(new Error("favicon_empty"));
return;
}
const dataUrl = await blobToDataUrl(blob, blob.type || "image/x-icon");
if (!dataUrl) {
reject(new Error("favicon_empty"));
return;
}
resolve(dataUrl);
} catch (error) {
reject(error);
}
},
ontimeout: () => reject(new Error("favicon_timeout")),
onerror: () => reject(new Error("favicon_request_failed"))
});
if (!xhr) {
reject(new Error("favicon_request_failed"));
}
});
const core = {
setUI(nextUi) {
ui = nextUi;
},
syncHostIconCache() {
syncHostIconCache();
},
removeHostIconCache(host) {
if (!host) return;
delete getHostIconCache()[host];
hostIconFetchJobs.delete(host);
syncHostIconCache();
},
async ensureHostIcon(host) {
if (!host) return "";
const cachedIcon = getCachedHostIcon(host);
if (cachedIcon) {
return cachedIcon;
}
if (host !== constants.HOST) {
return "";
}
if (hostIconFetchJobs.has(host)) {
return hostIconFetchJobs.get(host);
}
const job = (async () => {
try {
for (const source of buildCurrentHostIconSources()) {
try {
const dataUrl = source.type === "inline" ? source.value : await requestHostIcon(source.value);
getHostIconCache()[host] = { dataUrl };
syncHostIconCache();
return dataUrl;
} catch (_) {
continue;
}
}
return "";
} finally {
hostIconFetchJobs.delete(host);
}
})();
hostIconFetchJobs.set(host, job);
return job;
}
};
const getUI = () => ui;
const getCore = () => core;
Object.assign(
core,
createAccountMethods({ constants, utils, getUI, getCore, shared }),
createEnvironmentMethods({ getUI, shared }),
createInspectorMethods({ constants, utils }),
createBackupMethods({ constants, utils, getUI }),
createWebDavMethods({ constants, utils, getUI, getCore })
);
syncHostIconCache();
return core;
}
// src/app/ui/events.js
function createEventMethods({ state, constants, utils, core, ui }) {
return {
ensureNoteTooltip() {
if (state.noteTooltipEl || !state.uiRoot) return state.noteTooltipEl;
const tooltip = document.createElement("div");
tooltip.className = "acc-floating-note-tooltip";
const content = document.createElement("div");
content.className = "acc-floating-note-tooltip-content";
["mousedown", "mouseup", "click"].forEach((eventName) => {
tooltip.addEventListener(eventName, (event) => {
event.stopPropagation();
});
});
content.addEventListener(
"wheel",
(event) => {
event.stopPropagation();
if (ui.shouldPreventWheelLeak(content, event.deltaY)) {
event.preventDefault();
}
},
{ passive: false }
);
tooltip.appendChild(content);
tooltip.addEventListener("mouseenter", () => {
if (state.noteTooltipEl) {
state.noteTooltipEl.classList.add("show");
}
});
tooltip.addEventListener("mouseleave", (event) => {
if (state.noteTooltipTarget?.contains(event.relatedTarget)) return;
ui.hideNoteTooltip();
});
state.uiRoot.appendChild(tooltip);
state.noteTooltipEl = tooltip;
return tooltip;
},
showNoteTooltip(button) {
const note = String(button?.dataset?.note || "").trim();
if (!note) return;
const tooltip = ui.ensureNoteTooltip();
if (!tooltip) return;
const content = tooltip.querySelector(".acc-floating-note-tooltip-content");
if (!content) return;
state.noteTooltipItem?.classList.remove("acc-note-active");
state.noteTooltipItem = button.closest(".acc-switch-item");
state.noteTooltipItem?.classList.add("acc-note-active");
content.textContent = note;
tooltip.style.display = "block";
const buttonRect = button.getBoundingClientRect();
const tooltipRect = tooltip.getBoundingClientRect();
const left = Math.max(8, buttonRect.left - tooltipRect.width - 8);
const top = Math.min(
Math.max(8, buttonRect.top - 4),
window.innerHeight - tooltipRect.height - 8
);
const arrowTop = Math.min(
Math.max(12, buttonRect.top - top + 6),
tooltipRect.height - 12
);
tooltip.style.left = `${left}px`;
tooltip.style.top = `${top}px`;
tooltip.style.setProperty("--acc-note-arrow-top", `${arrowTop}px`);
tooltip.classList.add("show");
state.noteTooltipTarget = button;
},
hideNoteTooltip() {
if (!state.noteTooltipEl) return;
state.noteTooltipEl.classList.remove("show");
state.noteTooltipEl.style.display = "none";
state.noteTooltipTarget = null;
state.noteTooltipItem?.classList.remove("acc-note-active");
state.noteTooltipItem = null;
},
shouldPreventWheelLeak(scrollArea, deltaY) {
if (!scrollArea || scrollArea.scrollHeight <= scrollArea.clientHeight) {
return true;
}
if (deltaY < 0 && scrollArea.scrollTop <= 0) {
return true;
}
if (deltaY > 0 && scrollArea.scrollTop + scrollArea.clientHeight >= scrollArea.scrollHeight - 1) {
return true;
}
return false;
},
getHosts() {
const hosts = utils.listAllHosts();
if (!hosts.includes(constants.HOST)) hosts.push(constants.HOST);
return hosts;
},
resetHostPicker($, closePicker = true) {
state.hostSearchQuery = "";
state.hostEditingHost = null;
state.hostEditingValue = "";
if (closePicker) {
$("#host-picker")?.classList.remove("open");
}
},
bindPanelShellEvents({ $, $$ }) {
["keydown", "keyup", "keypress", "input", "contextmenu", "wheel"].forEach((eventName) => {
state.panel.addEventListener(
eventName,
(event) => {
event.stopPropagation();
if (eventName === "wheel") {
const scrollArea = event.target.closest(".acc-scroll-area, .acc-host-menu, .acc-host-list, .acc-floating-note-tooltip-content, .acc-input-note");
if (ui.shouldPreventWheelLeak(scrollArea, event.deltaY)) {
event.preventDefault();
}
}
},
{ passive: false }
);
});
$("#acc-close-btn").onclick = ui.closePanel;
state.panel.onclick = (event) => {
event.stopPropagation();
if (document.activeElement !== state.panel && !state.panel.contains(state.uiRoot.activeElement)) {
state.panel.focus();
}
};
$$(".fab-mode-btn").forEach((button) => {
button.addEventListener("click", () => {
GM_setValue(constants.CFG.FAB_MODE, button.dataset.val);
ui.refresh();
});
});
$("#lang-sel").onchange = (event) => {
const activePageBeforeRebuild = state.activePage;
const isPanelShown = Boolean(state.panel && state.panel.classList.contains("show"));
state.currentLang = event.target.value;
GM_setValue(constants.CFG.LANG, state.currentLang);
if (state.toastTimer) {
clearTimeout(state.toastTimer);
}
document.body.removeChild(document.getElementById("anme-app-host"));
state.uiRoot = null;
state.panel = null;
state.fab = null;
state.dialogMask = null;
state.saveFormMask = null;
state.toastEl = null;
state.toastTimer = null;
ui.init();
const newPanel = ui.qs("#acc-mgr-panel");
if (!newPanel) return;
if (isPanelShown) {
newPanel.classList.add("show");
ui.syncPanelPos();
}
ui.activatePage(activePageBeforeRebuild, ui.getPageTitle(activePageBeforeRebuild));
};
},
bindSwitchEvents({ $, getHosts, resetHostPicker }) {
$("#host-display-mode-sel").onchange = (event) => {
state.hostDisplayMode = event.target.value || "siteName";
if (state.hostDisplayMode !== "siteName") {
state.hostEditingHost = null;
state.hostEditingValue = "";
}
GM_setValue(constants.CFG.HOST_DISPLAY_MODE, state.hostDisplayMode);
ui.refresh();
};
$("#switch-area").onclick = (event) => {
if (event.target.closest(".acc-switch-handle")) {
event.stopPropagation();
return;
}
const avatarBtn = event.target.closest(".acc-card-name-icon");
if (avatarBtn) {
event.stopPropagation();
ui.copyText(avatarBtn.dataset.name || "").then((copied) => {
if (!copied) {
ui.alert(utils.t("copy_failed"));
return;
}
ui.showToast(utils.t("toast_copied"));
});
return;
}
const settingsBtn = event.target.closest(".acc-switch-settings-btn");
if (settingsBtn) {
event.stopPropagation();
ui.openAccountSettings(settingsBtn.dataset.key);
return;
}
const noteBtn = event.target.closest(".acc-switch-note-btn");
if (noteBtn) {
event.stopPropagation();
return;
}
const tag = event.target.closest(".acc-click-tag");
if (tag) {
event.stopPropagation();
const card2 = tag.closest(".acc-switch-card");
core.inspectData(card2.dataset.key, tag.dataset.type);
return;
}
if (state.currentViewingHost !== constants.HOST) {
event.stopPropagation();
return;
}
const card = event.target.closest(".acc-switch-card");
if (card) {
core.loadAccount(card.dataset.key);
}
};
$("#switch-area").addEventListener("mouseover", (event) => {
const noteBtn = event.target.closest(".acc-switch-note-btn");
if (!noteBtn) return;
ui.showNoteTooltip(noteBtn);
});
$("#switch-area").addEventListener("mouseout", (event) => {
const noteBtn = event.target.closest(".acc-switch-note-btn");
if (!noteBtn) return;
if (noteBtn.contains(event.relatedTarget) || state.noteTooltipEl?.contains(event.relatedTarget)) return;
ui.hideNoteTooltip();
});
$("#switch-area").addEventListener("focusin", (event) => {
const noteBtn = event.target.closest(".acc-switch-note-btn");
if (!noteBtn) return;
ui.showNoteTooltip(noteBtn);
});
$("#switch-area").addEventListener("focusout", (event) => {
const noteBtn = event.target.closest(".acc-switch-note-btn");
if (!noteBtn) return;
if (noteBtn.contains(event.relatedTarget) || state.noteTooltipEl?.contains(event.relatedTarget)) return;
ui.hideNoteTooltip();
});
$("#host-trigger").onclick = (event) => {
event.stopPropagation();
const picker = $("#host-picker");
if (!picker) return;
const willOpen = !picker.classList.contains("open");
picker.classList.toggle("open", willOpen);
if (willOpen) {
resetHostPicker(false);
ui.renderHostSelector(getHosts());
ui.qs("#host-search-input")?.focus();
}
};
$("#host-search-input").oninput = (event) => {
state.hostSearchQuery = event.target.value;
ui.renderHostSelector(getHosts());
ui.qs("#host-search-input")?.focus();
ui.qs("#host-search-input")?.setSelectionRange(state.hostSearchQuery.length, state.hostSearchQuery.length);
};
$("#host-search-input").onkeydown = (event) => {
if (event.key !== "Escape") return;
event.preventDefault();
event.stopPropagation();
resetHostPicker();
ui.renderHostSelector(getHosts());
ui.qs("#host-search-input")?.blur();
state.panel?.focus();
};
$("#host-menu").onclick = (event) => {
const editToggle = event.target.closest(".acc-host-edit-link");
if (editToggle) {
event.stopPropagation();
state.hostEditingHost = editToggle.dataset.editHost;
state.hostEditingValue = utils.getSiteNameByHost(state.hostEditingHost);
ui.renderHostSelector(getHosts());
ui.qs(`.acc-host-edit-input[data-host="${state.hostEditingHost}"]`)?.focus();
ui.qs(`.acc-host-edit-input[data-host="${state.hostEditingHost}"]`)?.select();
return;
}
const editCancel = event.target.closest(".acc-host-edit-cancel");
if (editCancel) {
event.stopPropagation();
state.hostEditingHost = null;
state.hostEditingValue = "";
ui.renderHostSelector(getHosts());
return;
}
const editSave = event.target.closest(".acc-host-edit-save");
if (editSave) {
event.stopPropagation();
const host = editSave.dataset.saveHost;
core.updateSiteName(host, state.hostEditingValue);
state.hostEditingHost = null;
state.hostEditingValue = "";
ui.refresh();
ui.showToast(utils.t("toast_site_name_updated"));
return;
}
const openLink = event.target.closest(".acc-host-open-link");
if (openLink) {
event.stopPropagation();
const host = openLink.dataset.openHost;
if (host) {
window.open(`https://${host}`, "_blank", "noopener,noreferrer");
}
return;
}
const option = event.target.closest(".acc-host-option");
if (!option) return;
state.currentViewingHost = option.dataset.host;
resetHostPicker();
ui.refresh();
};
$("#host-menu").addEventListener("input", (event) => {
const editInput = event.target.closest(".acc-host-edit-input");
if (!editInput) return;
state.hostEditingValue = editInput.value;
});
$("#host-menu").addEventListener("keydown", (event) => {
const editInput = event.target.closest(".acc-host-edit-input");
if (!editInput) return;
if (event.key === "Escape") {
event.preventDefault();
event.stopPropagation();
state.hostEditingHost = null;
state.hostEditingValue = "";
ui.renderHostSelector(getHosts());
ui.qs(".acc-host-edit-input")?.blur();
state.panel?.focus();
return;
}
if (event.key === "Enter") {
event.preventDefault();
event.stopPropagation();
core.updateSiteName(editInput.dataset.host, editInput.value);
state.hostEditingHost = null;
state.hostEditingValue = "";
ui.refresh();
ui.showToast(utils.t("toast_site_name_updated"));
}
});
$("#btn-account-search-toggle").onclick = (event) => {
event.stopPropagation();
state.accountSearchActive = !state.accountSearchActive;
if (!state.accountSearchActive) {
state.accountSearchQuery = "";
}
resetHostPicker();
ui.refresh();
if (state.accountSearchActive) {
ui.qs("#account-search-input")?.focus();
}
};
$("#account-search-input").oninput = (event) => {
state.accountSearchQuery = event.target.value;
ui.renderSwitchView();
};
$("#account-search-input").onkeydown = (event) => {
if (event.key !== "Escape") return;
event.preventDefault();
event.stopPropagation();
state.accountSearchActive = false;
state.accountSearchQuery = "";
ui.refresh();
ui.qs("#account-search-input")?.blur();
state.panel?.focus();
};
state.panel.addEventListener("click", (event) => {
if (event.target.closest("#host-picker")) return;
resetHostPicker();
});
},
bindNavigationEvents({ $, resetHostPicker }) {
$("#btn-open-settings").onclick = () => {
if (state.activePage !== "pg-set") {
state.settingsReturnPage = state.activePage;
}
ui.activatePage("pg-set", utils.t("nav_set"));
};
$("#btn-open-project").onclick = () => {
window.open(constants.META.LINKS.PROJECT, "_blank", "noopener,noreferrer");
};
$("#btn-open-webdav").onclick = () => {
state.settingsReturnPage = state.activePage || "pg-switch";
ui.activatePage("pg-webdav", utils.t("nav_webdav"));
};
$("#btn-go-current-host").onclick = () => {
state.currentViewingHost = constants.HOST;
resetHostPicker();
ui.refresh();
};
$("#btn-header-back").onclick = () => {
if (state.activePage === "pg-notice" || state.activePage === "pg-about") {
ui.activatePage("pg-set", utils.t("nav_set"));
return;
}
if (state.activePage === "pg-account-settings") {
const targetPage2 = state.accountSettingsReturnPage || "pg-switch";
ui.activatePage(targetPage2, ui.getPageTitle(targetPage2));
return;
}
const targetPage = state.settingsReturnPage || "pg-switch";
ui.activatePage(targetPage, ui.getPageTitle(targetPage));
};
$("#go-about").onclick = () => {
ui.activatePage("pg-about", utils.t("nav_about"));
};
$("#go-notice").onclick = () => {
ui.activatePage("pg-notice", utils.t("nav_notice"));
};
},
bindAccountSettingsEvents({ $ }) {
$("#btn-open-save-modal").onclick = () => {
ui.showSaveAccountModal();
};
$("#btn-clean-env").onclick = async () => {
if (await ui.confirm(utils.t("confirm_clean"))) {
core.cleanEnvironment();
}
};
$("#btn-export-curr").onclick = () => core.exportData("current");
$("#btn-export-all").onclick = () => core.exportData("all");
$("#btn-import-trigger").onclick = () => $("#inp-import-file").click();
$("#btn-account-rename-save").onclick = async () => {
const oldKey = state.accountSettingsKey;
const nameInput = $("#account-settings-name");
const noteInput = $("#account-settings-note");
if (!oldKey || !nameInput || !noteInput) return;
const newName = nameInput.value.trim();
const newNote = utils.normalizeNoteText(noteInput.value);
const targetHost = state.accountSettingsHost || constants.HOST;
const originalName = utils.extractName(oldKey);
const originalNote = utils.normalizeNoteText(GM_getValue(oldKey)?.note);
if (!newName || newName === originalName && newNote === originalNote) return;
const newKey = utils.makeKey(newName, targetHost);
if (newKey !== oldKey && GM_getValue(newKey)) {
await ui.alert(utils.t("rename_conflict"));
return;
}
state.accountSettingsKey = core.updateAccount(oldKey, { name: newName, note: newNote }, targetHost);
ui.refresh();
ui.activatePage("pg-account-settings", utils.t("account_settings"));
ui.showToast(utils.t("toast_account_updated"));
};
$("#btn-account-delete").onclick = async () => {
const key = state.accountSettingsKey;
if (!key) return;
if (await ui.confirm(utils.t("confirm_delete"))) {
core.deleteAccount(key, state.accountSettingsHost || constants.HOST);
state.accountSettingsKey = null;
ui.refresh();
const targetPage = state.accountSettingsReturnPage || "pg-switch";
ui.activatePage(targetPage, ui.getPageTitle(targetPage));
ui.showToast(utils.t("toast_deleted"));
}
};
$("#inp-import-file").onchange = (event) => {
if (event.target.files.length) {
core.importData(event.target.files[0]);
}
event.target.value = "";
};
$("#btn-clear-all").onclick = async () => {
if (await ui.confirm(utils.t("confirm_clear_all"))) {
core.clearAllData();
ui.refresh();
}
};
},
bindWebDavEvents({ $ }) {
$("#btn-webdav-config").onclick = async () => {
await ui.showWebDavConfigModal();
};
$("#btn-webdav-sync").onclick = async () => {
const syncBtn = $("#btn-webdav-sync");
await ui.runUiAction({
button: syncBtn,
idleText: utils.t("webdav_sync_now"),
errorKey: "webdav_sync_err",
successMessage: utils.t("webdav_sync_ok"),
action: () => core.uploadWebDavBackup(),
onSuccess: (fileName) => {
if (fileName) {
state.webdavBackups = core.getCachedWebDavBackups();
ui.renderWebDavBackupList(state.webdavBackups);
}
}
});
};
$("#btn-webdav-refresh").onclick = async () => {
await ui.loadWebDavBackups();
};
$("#btn-webdav-logout").onclick = async () => {
if (!core.hasWebDavConfig()) return;
const confirmed = await ui.confirm(utils.t("webdav_logout_confirm"));
if (!confirmed) return;
core.clearWebDavConfig();
state.webdavBackups = [];
ui.renderWebDavView();
ui.showToast(utils.t("webdav_logout_ok"));
};
$("#webdav-backup-list").onclick = async (event) => {
const actionBtn = event.target.closest("[data-action][data-file]");
if (!actionBtn) return;
event.preventDefault();
event.stopPropagation();
const fileName = actionBtn.dataset.file;
const action = actionBtn.dataset.action;
if (!fileName || !action) return;
if (action === "delete") {
const confirmed = await ui.confirm(utils.t("webdav_delete_confirm").replace("{name}", fileName));
if (!confirmed) return;
await ui.runUiAction({
loadingText: utils.t("webdav_loading"),
errorKey: "webdav_delete_err",
successMessage: utils.t("webdav_delete_ok"),
action: () => core.deleteWebDavBackup(fileName),
onSuccess: () => {
state.webdavBackups = core.getCachedWebDavBackups();
ui.renderWebDavBackupList(state.webdavBackups);
}
});
return;
}
if (action === "restore") {
const confirmed = await ui.confirm(utils.t("webdav_restore_confirm").replace("{name}", fileName));
if (!confirmed) return;
await ui.runUiAction({
loadingText: utils.t("webdav_restoring"),
errorKey: "sync_restore_err",
action: () => core.restoreWebDavBackup(fileName),
onSuccess: (result) => {
ui.showToast(utils.t(result.messageKey).replace("{count}", result.count), 2400);
}
});
}
};
},
bindPanelEvents() {
const $ = (selector) => ui.qs(selector);
const $$ = (selector) => ui.qsa(selector);
const getHosts = () => ui.getHosts();
const resetHostPicker = (closePicker = true) => {
ui.resetHostPicker($, closePicker);
};
ui.bindPanelShellEvents({ $, $$ });
ui.bindSwitchEvents({ $, getHosts, resetHostPicker });
ui.bindNavigationEvents({ $, resetHostPicker });
ui.bindAccountSettingsEvents({ $ });
ui.bindWebDavEvents({ $ });
}
};
}
// src/app/ui/feedback.js
function createFeedbackMethods({ state, constants, utils, core, ui }) {
return {
async copyText(text) {
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return true;
}
} catch {
}
const tempInput = document.createElement("textarea");
tempInput.value = text;
tempInput.setAttribute("readonly", "true");
tempInput.style.position = "fixed";
tempInput.style.top = "-9999px";
tempInput.style.left = "-9999px";
document.body.appendChild(tempInput);
tempInput.select();
let copied = false;
try {
copied = document.execCommand("copy");
} catch {
copied = false;
}
document.body.removeChild(tempInput);
return copied;
},
showToast(message, duration = 1800) {
if (!state.panel || !message) return;
if (!state.toastEl) {
state.toastEl = document.createElement("div");
state.toastEl.className = "acc-toast";
utils.setHTML(state.toastEl, `
${constants.ICONS.NOTICE}
`);
state.panel.appendChild(state.toastEl);
}
const textNode = state.toastEl.querySelector(".acc-toast-text");
if (textNode) textNode.textContent = message;
state.toastEl.classList.add("show");
if (state.toastTimer) {
clearTimeout(state.toastTimer);
}
state.toastTimer = setTimeout(() => {
if (state.toastEl) {
state.toastEl.classList.remove("show");
}
state.toastTimer = null;
}, duration);
},
setButtonLoading(button, loading, idleText = "", spinnerClassName = "acc-inline-spinner") {
if (!button) return;
if (loading) {
button.style.minWidth = `${button.offsetWidth}px`;
button.style.minHeight = `${button.offsetHeight}px`;
}
button.disabled = loading;
button.classList.toggle("is-loading", loading);
utils.setHTML(button, loading ? ` ` : idleText);
if (!loading) {
button.style.minWidth = "";
button.style.minHeight = "";
}
},
async runUiAction({
button = null,
idleText = "",
loadingText = "",
errorKey = "",
successMessage = "",
successDuration = 1800,
action,
onSuccess,
onError
}) {
try {
if (button) {
ui.setButtonLoading(button, true, idleText);
} else if (loadingText) {
ui.toggleLoading(true, loadingText);
}
const result = await action();
if (onSuccess) {
await onSuccess(result);
}
if (successMessage) {
ui.showToast(successMessage, successDuration);
}
return result;
} catch (error) {
if (onError) {
const handled = await onError(error);
if (handled === false) {
return null;
}
}
ui.showToast(utils.getWebDavErrorMessage(error, errorKey));
return null;
} finally {
if (button) {
ui.setButtonLoading(button, false, idleText);
} else if (loadingText) {
ui.toggleLoading(false);
}
}
},
async alert(message) {
return ui.showDialog(message, false);
},
async confirm(message) {
return ui.showDialog(message, true);
},
showDialog(message, isConfirm) {
return new Promise((resolve) => {
if (!state.dialogMask) {
const currentPanel = ui.qs(".acc-panel");
state.dialogMask = document.createElement("div");
state.dialogMask.className = "acc-dialog-mask";
currentPanel.appendChild(state.dialogMask);
}
utils.setHTML(state.dialogMask, `
`);
state.dialogMask.style.display = "flex";
const okBtn = ui.qs("#acc-dlg-ok");
const cancelBtn = ui.qs("#acc-dlg-cancel");
const close = (result) => {
state.dialogMask.style.display = "none";
resolve(result);
};
okBtn.onclick = () => close(true);
if (cancelBtn) cancelBtn.onclick = () => close(false);
});
},
ensureFormMask() {
if (state.saveFormMask) return state.saveFormMask;
state.saveFormMask = document.createElement("div");
state.saveFormMask.className = "acc-form-mask";
state.panel.appendChild(state.saveFormMask);
return state.saveFormMask;
},
hideFormModal() {
if (state.saveFormMask) {
state.saveFormMask.style.display = "none";
}
},
async showFormModal({ title, contentHtml, submitText, onOpen }) {
const mask = ui.ensureFormMask();
utils.setHTML(mask, `
`);
mask.style.display = "flex";
const context = {
mask,
qs: (selector) => mask.querySelector(selector),
qsa: (selector) => [...mask.querySelectorAll(selector)],
cancelBtn: mask.querySelector("#acc-form-cancel"),
submitBtn: mask.querySelector("#acc-form-submit"),
close: () => ui.hideFormModal(),
setSubmitting: (loading, idleText = submitText) => {
if (context.cancelBtn) {
context.cancelBtn.disabled = loading;
}
ui.setButtonLoading(context.submitBtn, loading, idleText);
}
};
if (context.cancelBtn) {
context.cancelBtn.onclick = context.close;
}
if (onOpen) {
await onOpen(context);
}
return context;
},
async showSaveAccountModal() {
await ui.showFormModal({
title: utils.t("btn_save"),
submitText: utils.t("btn_save"),
contentHtml: `
Cookie
LS
SS
${constants.ICONS.HELP}
${constants.ICONS.LOCK}
${utils.t("site_name")}*
${utils.t("account_name")}*
${utils.t("account_note")}
`,
onOpen: async ({ qs, submitBtn, close }) => {
const nameInput = qs("#form-acc-name");
const siteNameInput = qs("#form-site-name");
const noteInput = qs("#form-acc-note");
siteNameInput.value = utils.suggestSiteName(utils.getPageTitle(), constants.HOST);
nameInput.value = utils.suggestAccountName(constants.HOST);
const toggleAvailability = (selector, available) => {
const input = qs(selector);
const label = input?.closest(".acc-chk-label");
if (!input || !label) return;
input.disabled = !available;
input.checked = available && input.id === "form-c-ck";
label.classList.toggle("disabled", !available);
};
const updateState = () => {
const ck = qs("#form-c-ck")?.checked;
const ls = qs("#form-c-ls")?.checked;
const ss = qs("#form-c-ss")?.checked;
const canSave = (ck || ls || ss) && nameInput.value.trim().length > 0 && siteNameInput.value.trim().length > 0;
submitBtn.disabled = !canSave;
};
["#form-c-ck", "#form-c-ls", "#form-c-ss"].forEach((selector) => {
qs(selector)?.addEventListener("change", updateState);
});
siteNameInput.addEventListener("input", updateState);
nameInput.addEventListener("input", updateState);
nameInput.addEventListener("keydown", (event) => {
if (event.key !== "Enter" || submitBtn.disabled) return;
event.preventDefault();
event.stopPropagation();
submitBtn.click();
});
submitBtn.onclick = async () => {
const name = nameInput.value.trim();
const siteName = siteNameInput.value.trim();
if (!name || !siteName) return;
const targetKey = utils.makeKey(name);
if (GM_getValue(targetKey)) {
const confirmed = await ui.confirm(utils.t("confirm_overwrite"));
if (!confirmed) return;
}
const saved = await core.saveAccount(name, siteName, {
ck: qs("#form-c-ck").checked,
ls: qs("#form-c-ls").checked,
ss: qs("#form-c-ss").checked,
note: noteInput.value
});
if (!saved) return;
close();
ui.refresh();
ui.showToast(utils.t("toast_saved"));
};
const availableSources = await core.detectAvailableSnapshotSources();
toggleAvailability("#form-c-ck", availableSources.ck);
toggleAvailability("#form-c-ls", availableSources.ls);
toggleAvailability("#form-c-ss", availableSources.ss);
updateState();
if (utils.getSortedKeysByHost(constants.HOST).length > 0) {
nameInput.focus();
nameInput.select();
} else {
siteNameInput.focus();
siteNameInput.select();
}
}
});
},
async showWebDavConfigModal() {
const config = core.getWebDavConfig();
const hasSavedPassword = Boolean(config.password);
const maskedPassword = "******";
await ui.showFormModal({
title: utils.t("nav_webdav"),
submitText: utils.t("webdav_verify_save"),
contentHtml: `
${utils.t("webdav_url")}
${utils.t("webdav_username")}
${utils.t("webdav_password")}
`,
onOpen: async ({ qs, submitBtn, setSubmitting, close }) => {
const urlInput = qs("#form-webdav-url");
const usernameInput = qs("#form-webdav-username");
const passwordInput = qs("#form-webdav-password");
let isSaving = false;
let passwordDirty = false;
const updateState = () => {
const canSave = urlInput.value.trim().length > 0 && usernameInput.value.trim().length > 0 && (passwordDirty && passwordInput.value.length > 0 || !passwordDirty && hasSavedPassword || !hasSavedPassword && passwordInput.value.length > 0);
[urlInput, usernameInput, passwordInput].forEach((input) => {
input.disabled = isSaving;
});
setSubmitting(isSaving, utils.t("webdav_verify_save"));
if (!isSaving) {
submitBtn.disabled = !canSave;
}
};
const setSavingState = (saving) => {
isSaving = saving;
updateState();
};
const beginPasswordEdit = () => {
if (!hasSavedPassword || passwordDirty || passwordInput.value !== maskedPassword) return;
passwordInput.value = "";
passwordDirty = true;
updateState();
};
const restoreMaskedPassword = () => {
if (!hasSavedPassword || !passwordDirty || passwordInput.value.length > 0) return;
passwordDirty = false;
passwordInput.value = maskedPassword;
updateState();
};
[urlInput, usernameInput, passwordInput].forEach((input) => {
input.addEventListener("input", updateState);
input.addEventListener("keydown", (event) => {
if (input === passwordInput && passwordInput.value === maskedPassword && event.key.length === 1) {
beginPasswordEdit();
}
if (event.key === "Enter" && !submitBtn.disabled) {
event.preventDefault();
submitBtn.click();
}
});
});
passwordInput.addEventListener("focus", beginPasswordEdit);
passwordInput.addEventListener("mousedown", beginPasswordEdit);
passwordInput.addEventListener("paste", beginPasswordEdit);
passwordInput.addEventListener("blur", restoreMaskedPassword);
submitBtn.onclick = async () => {
const nextConfig = {
url: urlInput.value.trim(),
username: usernameInput.value.trim(),
password: passwordDirty && passwordInput.value ? passwordInput.value : config.password
};
try {
setSavingState(true);
const validatedConfig = await core.validateWebDavConfig(nextConfig);
core.saveWebDavConfig(validatedConfig);
close();
state.webdavBackups = core.getCachedWebDavBackups();
ui.renderWebDavView();
ui.showToast(utils.t("webdav_verified"));
} catch (error) {
setSavingState(false);
ui.showToast(utils.getWebDavErrorMessage(error, "webdav_verify_err"));
return;
}
setSavingState(false);
};
updateState();
if (config.username) {
usernameInput.focus();
usernameInput.select();
} else {
urlInput.focus();
urlInput.select();
}
}
});
}
};
}
// src/app/fullscreen.js
var YOUTUBE_HOST_RE = /(^|\.)youtube\.com$/i;
var YOUTUBE_FULLSCREEN_SELECTORS = [
"#movie_player.ytp-fullscreen",
".html5-video-player.ytp-fullscreen",
"ytd-watch-flexy[fullscreen]",
"ytd-player[fullscreen]",
"ytd-reel-video-renderer[fullscreen]"
];
function safeQuerySelector(doc, selector) {
if (!doc || typeof doc.querySelector !== "function") {
return null;
}
try {
return doc.querySelector(selector);
} catch (_) {
return null;
}
}
function isFullscreenPlaybackActive({
doc = typeof document !== "undefined" ? document : null,
host = typeof location !== "undefined" ? location.hostname : ""
} = {}) {
if (!doc) {
return false;
}
const fullscreenElement = doc.fullscreenElement || doc.webkitFullscreenElement || doc.mozFullScreenElement || doc.msFullscreenElement;
if (fullscreenElement) {
return true;
}
if (YOUTUBE_HOST_RE.test(host)) {
return YOUTUBE_FULLSCREEN_SELECTORS.some((selector) => Boolean(safeQuerySelector(doc, selector)));
}
return false;
}
// src/app/ui/panel.js
function createPanelMethods({ state, constants, utils, templates, styleCss, ui }) {
return {
isFullscreenPlaybackActive() {
return isFullscreenPlaybackActive();
},
syncFloatingUiVisibility() {
if (!state.fab || !state.panel || !state.uiRoot?.host) return;
const isFullscreen = ui.isFullscreenPlaybackActive();
state.uiRoot.host.style.display = isFullscreen ? "none" : "";
if (isFullscreen) {
if (!state.isFullscreenHidden) {
ui.hideNoteTooltip?.();
state.panel.classList.remove("show");
}
state.isFullscreenHidden = true;
return;
}
state.isFullscreenHidden = false;
const fabMode = GM_getValue(constants.CFG.FAB_MODE, "auto");
const hasAccounts = utils.getSortedKeysByHost(constants.HOST).length > 0;
const isPanelOpen = state.panel.classList.contains("show");
state.fab.style.display = isPanelOpen || state.isForcedShow || fabMode === "show" || fabMode === "auto" && hasAccounts ? "flex" : "none";
},
updateHeaderActionsVisibility() {
const headerActions = ui.qs("#acc-header-actions");
const switchPage = ui.qs("#pg-switch");
const setPage = ui.qs("#pg-set");
const noticePage = ui.qs("#pg-notice");
const aboutPage = ui.qs("#pg-about");
const accountSettingsPage = ui.qs("#pg-account-settings");
const webdavPage = ui.qs("#pg-webdav");
if (!headerActions || !switchPage || !setPage || !noticePage || !aboutPage || !accountSettingsPage || !webdavPage) return;
const isSwitchActive = switchPage.classList.contains("active");
const isSetActive = setPage.classList.contains("active");
const isNoticeActive = noticePage.classList.contains("active");
const isAboutActive = aboutPage.classList.contains("active");
const isAccountSettingsActive = accountSettingsPage.classList.contains("active");
const isWebDavActive = webdavPage.classList.contains("active");
const canOperateCurrentHost = state.currentViewingHost === constants.HOST;
headerActions.style.display = "flex";
const backBtn = ui.qs("#btn-header-back");
const homeBtn = ui.qs("#btn-go-current-host");
const cleanBtn = ui.qs("#btn-clean-env");
const saveBtn = ui.qs("#btn-open-save-modal");
const settingsBtn = ui.qs("#btn-open-settings");
const projectBtn = ui.qs("#btn-open-project");
const webdavBtn = ui.qs("#btn-open-webdav");
if (backBtn) backBtn.style.display = isSetActive || isNoticeActive || isAboutActive || isAccountSettingsActive || isWebDavActive ? "flex" : "none";
if (homeBtn) homeBtn.style.display = isSwitchActive && !canOperateCurrentHost ? "flex" : "none";
if (settingsBtn) settingsBtn.style.display = isSwitchActive ? "flex" : "none";
if (projectBtn) projectBtn.style.display = isSwitchActive ? "flex" : "none";
if (webdavBtn) webdavBtn.style.display = isSwitchActive ? "flex" : "none";
if (cleanBtn) cleanBtn.style.display = isSwitchActive && canOperateCurrentHost ? "flex" : "none";
if (saveBtn) saveBtn.style.display = isSwitchActive && canOperateCurrentHost ? "flex" : "none";
},
activatePage(pageId, title = ui.getPageTitle(pageId)) {
ui.hideNoteTooltip?.();
ui.qsa(".acc-tab-content").forEach((element) => element.classList.remove("active"));
const page = ui.qs(`#${pageId}`);
if (page) page.classList.add("active");
const headerText = ui.qs("#acc-header-text");
if (headerText) headerText.innerText = title;
state.activePage = pageId;
if (pageId === "pg-account-settings") {
ui.renderAccountSettingsView();
}
if (pageId === "pg-webdav") {
ui.renderWebDavView();
}
ui.updateHeaderActionsVisibility();
},
toggleLoading(show, text = "") {
let loader = ui.qs(".acc-loading-mask");
if (!loader) {
loader = document.createElement("div");
loader.className = "acc-loading-mask";
utils.setHTML(loader, `
`);
state.panel.appendChild(loader);
}
loader.querySelector(".acc-loading-text").innerText = text;
loader.style.display = show ? "flex" : "none";
},
refresh() {
if (!state.fab || !state.panel) return;
ui.hideNoteTooltip?.();
ui.renderSwitchView();
ui.renderAccountSettingsView();
if (state.activePage === "pg-webdav") {
ui.renderWebDavView();
}
const hosts = utils.listAllHosts();
if (!hosts.includes(constants.HOST)) hosts.push(constants.HOST);
if (!hosts.includes(state.currentViewingHost)) state.currentViewingHost = constants.HOST;
if (ui.qs("#host-trigger") && ui.qs("#host-menu")) {
ui.renderHostSelector(hosts);
}
ui.updateSwitchToolbar();
ui.updateHeaderActionsVisibility();
const fabMode = GM_getValue(constants.CFG.FAB_MODE, "auto");
const hasAccounts = utils.getSortedKeysByHost(constants.HOST).length > 0;
state.panel.querySelectorAll(".fab-mode-btn").forEach((button) => button.classList.toggle("acc-btn-active", button.dataset.val === fabMode));
ui.syncFloatingUiVisibility();
const eyes = state.fab.querySelectorAll("path:nth-of-type(1), path:nth-of-type(4)");
eyes.forEach((path) => {
path.style.fill = hasAccounts ? "#555" : "none";
path.style.stroke = "";
});
},
syncPanelPos() {
if (!state.fab || !state.panel) return;
const rect = state.fab.getBoundingClientRect();
state.panel.style.bottom = `${window.innerHeight - rect.top + 10}px`;
state.panel.style.left = `${Math.max(10, rect.left - 290)}px`;
},
closePanel() {
ui.hideNoteTooltip?.();
if (state.panel) state.panel.classList.remove("show");
state.isForcedShow = false;
ui.refresh();
},
createShadowHost() {
const existingHost = document.getElementById("anme-app-host");
if (existingHost) {
state.uiRoot = existingHost.shadowRoot;
return;
}
const host = document.createElement("div");
host.id = "anme-app-host";
document.body.appendChild(host);
state.uiRoot = host.attachShadow({ mode: "open" });
const styleEl = document.createElement("style");
styleEl.textContent = styleCss;
state.uiRoot.appendChild(styleEl);
},
createFab() {
const existingFab = ui.qs("#acc-mgr-fab");
if (existingFab) {
state.fab = existingFab;
return;
}
state.fab = document.createElement("div");
state.fab.id = "acc-mgr-fab";
utils.setHTML(state.fab, constants.ICONS.LOGO);
state.uiRoot.appendChild(state.fab);
const savedPos = GM_getValue(constants.CFG.FAB_POS);
if (savedPos && savedPos.left !== void 0) {
state.fab.style.left = `${Math.max(0, Math.min(savedPos.left, window.innerWidth - 44))}px`;
state.fab.style.top = `${Math.max(0, Math.min(savedPos.top, window.innerHeight - 44))}px`;
state.fab.style.bottom = "auto";
state.fab.style.right = "auto";
}
let isDrag = false;
const dragThreshold = 4;
state.fab.onmousedown = (event) => {
isDrag = false;
const startX = event.clientX;
const startY = event.clientY;
const baseX = state.fab.offsetLeft;
const baseY = state.fab.offsetTop;
const move = (moveEvent) => {
const deltaX = moveEvent.clientX - startX;
const deltaY = moveEvent.clientY - startY;
if (!isDrag && Math.hypot(deltaX, deltaY) < dragThreshold) {
return;
}
isDrag = true;
const newLeft = Math.max(0, Math.min(baseX + moveEvent.clientX - startX, window.innerWidth - 44));
const newTop = Math.max(0, Math.min(baseY + moveEvent.clientY - startY, window.innerHeight - 44));
state.fab.style.left = `${newLeft}px`;
state.fab.style.top = `${newTop}px`;
state.fab.style.bottom = "auto";
state.fab.style.right = "auto";
if (state.panel && state.panel.classList.contains("show")) ui.syncPanelPos();
};
const up = () => {
document.removeEventListener("mousemove", move);
document.removeEventListener("mouseup", up);
if (isDrag) {
GM_setValue(constants.CFG.FAB_POS, {
left: parseInt(state.fab.style.left, 10),
top: parseInt(state.fab.style.top, 10)
});
}
};
document.addEventListener("mousemove", move);
document.addEventListener("mouseup", up);
};
state.fab.onclick = (event) => {
if (isDrag || !state.panel) return;
event.stopPropagation();
const willOpen = !state.panel.classList.contains("show");
if (willOpen) {
ui.refresh();
ui.syncPanelPos();
state.panel.classList.add("show");
state.panel.focus();
} else {
state.panel.classList.remove("show");
state.isForcedShow = false;
ui.refresh();
}
};
},
createPanel() {
const existingPanel = ui.qs("#acc-mgr-panel");
if (existingPanel) {
state.panel = existingPanel;
return;
}
state.panel = document.createElement("div");
state.panel.id = "acc-mgr-panel";
state.panel.className = "acc-panel";
state.panel.setAttribute("tabindex", "-1");
utils.setHTML(state.panel, templates.panel());
state.uiRoot.appendChild(state.panel);
ui.bindPanelEvents();
}
};
}
// src/app/ui/switching.js
function createSwitchingMethods({ state, constants, utils, templates, core, ui }) {
return {
renderHostSelector(hosts) {
const hostTrigger = ui.qs("#host-trigger");
const hostMenu = ui.qs("#host-menu");
const hostSearchInput = ui.qs("#host-search-input");
if (!hostTrigger || !hostMenu) return;
const buildHostIcon = (host, label, className = "") => {
const fallbackText = utils.escapeHtml(utils.getHostIconFallbackText(host, label));
const cachedIconUrl = utils.escapeHtml(utils.getCachedHostIcon(host));
const hasCachedIcon = Boolean(cachedIconUrl);
return `
${fallbackText}
`;
};
const query = state.hostSearchQuery.trim().toLowerCase();
const visibleHosts = hosts.filter((host) => {
const siteName = utils.getSiteNameByHost(host).toLowerCase();
return host.toLowerCase().includes(query) || siteName.includes(query);
});
const currentDisplayName = utils.getHostDisplayName(state.currentViewingHost);
utils.setHTML(hostTrigger, `
${buildHostIcon(state.currentViewingHost, currentDisplayName)}
${utils.escapeHtml(currentDisplayName)}
`);
if (hostSearchInput) {
hostSearchInput.value = state.hostSearchQuery;
}
utils.setHTML(hostMenu, `
${visibleHosts.length ? visibleHosts.map((host) => {
const isActive = host === state.currentViewingHost ? " active" : "";
const displayName = utils.getHostDisplayName(host);
const label = utils.escapeHtml(displayName);
const isEditing = state.hostDisplayMode === "siteName" && state.hostEditingHost === host;
const editValue = utils.escapeHtml(state.hostEditingValue || utils.getSiteNameByHost(host));
return `
${buildHostIcon(host, displayName)}
${label}
${state.hostDisplayMode === "siteName" ? `
${constants.ICONS.EDIT} ` : ""}
↗
${isEditing ? `
${utils.t("save_changes")}
${utils.t("dlg_cancel")}
` : ""}
`;
}).join("") : `
${utils.t("no_data")}
`}
`);
core.ensureHostIcon(constants.HOST).catch(() => {
});
},
initPointerSortableList({ containerSelector, itemSelector, keySelector, orderHost, afterSort, handleSelector }) {
const container = ui.qs(containerSelector);
if (!container) return;
const scrollArea = container.closest(".acc-scroll-area");
if (container._psCleanup) {
container._psCleanup();
}
let dragState = null;
let autoScrollRaf = null;
let latestPointerY = 0;
const updateDraggedItemPosition = () => {
if (!dragState) return;
const ghostRect = dragState.ghost.getBoundingClientRect();
const dragMidY = ghostRect.top + ghostRect.height / 2;
const siblingItems = [...container.querySelectorAll(itemSelector)].filter((item) => item !== dragState.item);
const nextItem = siblingItems.find((item) => {
const box = item.getBoundingClientRect();
return dragMidY < box.top + box.height / 2;
});
if (nextItem) {
container.insertBefore(dragState.item, nextItem);
} else {
container.appendChild(dragState.item);
}
};
const stopAutoScroll = () => {
if (!autoScrollRaf) return;
cancelAnimationFrame(autoScrollRaf);
autoScrollRaf = null;
};
const runAutoScroll = () => {
if (!dragState || !scrollArea) {
stopAutoScroll();
return;
}
const rect = scrollArea.getBoundingClientRect();
const threshold = 44;
let delta = 0;
if (latestPointerY < rect.top + threshold) {
delta = -Math.ceil((rect.top + threshold - latestPointerY) / threshold * 14);
} else if (latestPointerY > rect.bottom - threshold) {
delta = Math.ceil((latestPointerY - (rect.bottom - threshold)) / threshold * 14);
}
if (delta !== 0) {
scrollArea.scrollTop += delta;
updateDraggedItemPosition();
autoScrollRaf = requestAnimationFrame(runAutoScroll);
return;
}
autoScrollRaf = null;
};
const cleanupDragState = () => {
if (!dragState) return;
const { ghost, container: dragContainer, item } = dragState;
stopAutoScroll();
item.classList.remove("dragging-source");
if (dragContainer) {
dragContainer.classList.remove("acc-switch-list-sorting");
}
if (ghost && ghost.parentNode) {
ghost.parentNode.removeChild(ghost);
}
dragState = null;
};
const syncOrder = () => {
const items = [...container.querySelectorAll(keySelector)];
const newOrder = items.map((element) => utils.extractName(element.dataset.key || element.dataset.cardKey));
core.updateOrder(orderHost(), newOrder);
if (afterSort) afterSort();
};
const onPointerMove = (event) => {
if (!dragState || event.pointerId !== dragState.pointerId) return;
event.preventDefault();
latestPointerY = event.clientY;
dragState.ghost.style.top = `${event.clientY - dragState.offsetY}px`;
dragState.ghost.style.left = `${dragState.left}px`;
updateDraggedItemPosition();
if (!autoScrollRaf) {
autoScrollRaf = requestAnimationFrame(runAutoScroll);
}
};
const onPointerUp = (event) => {
if (!dragState || event.pointerId !== dragState.pointerId) return;
event.preventDefault();
cleanupDragState();
syncOrder();
};
const onPointerDown = (event) => {
if (event.button !== 0) return;
const handle = event.target.closest(handleSelector);
if (!handle) return;
const item = handle.closest(itemSelector);
if (!item) return;
event.preventDefault();
event.stopPropagation();
const rect = item.getBoundingClientRect();
const ghost = item.cloneNode(true);
ghost.classList.add("acc-switch-ghost");
ghost.style.position = "fixed";
ghost.style.top = `${rect.top}px`;
ghost.style.left = `${rect.left}px`;
ghost.style.width = `${rect.width}px`;
ghost.style.zIndex = "1000002";
ghost.style.pointerEvents = "none";
state.uiRoot.appendChild(ghost);
item.classList.add("dragging-source");
container.classList.add("acc-switch-list-sorting");
dragState = {
container,
ghost,
item,
pointerId: event.pointerId,
offsetY: event.clientY - rect.top,
left: rect.left
};
latestPointerY = event.clientY;
};
container.addEventListener("pointerdown", onPointerDown);
document.addEventListener("pointermove", onPointerMove);
document.addEventListener("pointerup", onPointerUp);
document.addEventListener("pointercancel", onPointerUp);
container._psCleanup = () => {
container.removeEventListener("pointerdown", onPointerDown);
document.removeEventListener("pointermove", onPointerMove);
document.removeEventListener("pointerup", onPointerUp);
document.removeEventListener("pointercancel", onPointerUp);
cleanupDragState();
};
},
renderSwitchView() {
const searchQuery = state.accountSearchQuery.trim().toLowerCase();
const currentKeys = utils.getSortedKeysByHost(state.currentViewingHost).filter((key) => !searchQuery || utils.extractName(key).toLowerCase().includes(searchQuery));
const switchArea = ui.qs("#switch-area");
if (!switchArea) return;
utils.setHTML(
switchArea,
currentKeys.length === 0 ? templates.noData() : currentKeys.map((key) => templates.switchCard(key, GM_getValue(key))).join("")
);
ui.initPointerSortableList({
containerSelector: "#switch-area",
itemSelector: ".acc-switch-item",
keySelector: ".acc-switch-item",
handleSelector: ".acc-switch-handle",
orderHost: () => state.currentViewingHost,
afterSort: () => {
ui.renderSwitchView();
}
});
},
renderAccountSettingsView() {
const input = ui.qs("#account-settings-name");
const noteInput = ui.qs("#account-settings-note");
const saveBtn = ui.qs("#btn-account-rename-save");
const deleteBtn = ui.qs("#btn-account-delete");
if (!input || !noteInput || !saveBtn || !deleteBtn) return;
const key = state.accountSettingsKey;
const data = key ? GM_getValue(key) : null;
const originalName = data ? utils.extractName(key) : "";
const originalNote = utils.normalizeNoteText(data?.note);
input.value = originalName;
noteInput.value = originalNote;
input.disabled = !data;
noteInput.disabled = !data;
deleteBtn.disabled = !data;
const updateSaveState = () => {
const canSave = Boolean(data) && input.value.trim().length > 0 && (input.value.trim() !== originalName || utils.normalizeNoteText(noteInput.value) !== originalNote);
saveBtn.disabled = !canSave;
};
input.oninput = updateSaveState;
noteInput.oninput = updateSaveState;
input.onkeydown = (event) => {
if (event.key === "Enter" && !saveBtn.disabled) {
event.preventDefault();
saveBtn.click();
}
};
updateSaveState();
},
updateSwitchToolbar() {
const hostRow = ui.qs("#acc-host-row");
const searchInput = ui.qs("#account-search-input");
const searchToggleBtn = ui.qs("#btn-account-search-toggle");
if (!hostRow || !searchInput || !searchToggleBtn) return;
hostRow.classList.toggle("searching", state.accountSearchActive);
searchInput.value = state.accountSearchQuery;
utils.setHTML(searchToggleBtn, state.accountSearchActive ? constants.ICONS.CLOSE : constants.ICONS.SEARCH);
searchToggleBtn.title = state.accountSearchActive ? utils.t("close_search_accounts") : utils.t("search_accounts");
},
openAccountSettings(key) {
state.accountSettingsKey = key;
state.accountSettingsHost = state.currentViewingHost;
state.accountSettingsReturnPage = state.activePage || "pg-switch";
ui.renderAccountSettingsView();
ui.activatePage("pg-account-settings", utils.t("account_settings"));
}
};
}
// src/app/ui/webdav.js
function createWebDavUiMethods({ state, constants, utils, core, ui }) {
return {
renderWebDavView() {
const config = core.getWebDavConfig();
const hasConfig = core.hasWebDavConfig();
const statusEl = ui.qs("#webdav-status");
const syncBtn = ui.qs("#btn-webdav-sync");
const logoutBtn = ui.qs("#btn-webdav-logout");
if (!statusEl || !syncBtn || !logoutBtn) return;
statusEl.textContent = hasConfig ? utils.t("webdav_connected_as").replace("{user}", config.username) : utils.t("webdav_not_configured");
syncBtn.disabled = !hasConfig;
logoutBtn.disabled = !hasConfig;
if (hasConfig) {
state.webdavBackups = core.getCachedWebDavBackups();
} else {
state.webdavBackups = [];
}
ui.renderWebDavBackupList(state.webdavBackups);
},
renderWebDavBackupList(backups = [], errorMessage = "") {
const container = ui.qs("#webdav-backup-list");
if (!container) return;
if (errorMessage) {
utils.setHTML(container, `${utils.escapeHtml(errorMessage)}
`);
return;
}
if (!backups.length) {
utils.setHTML(container, `${utils.t("webdav_no_backups")}
`);
return;
}
utils.setHTML(
container,
backups.map(
(backup) => `
${utils.escapeHtml(backup.fileName)}
${utils.escapeHtml(utils.formatTime(backup.lastModified))}
${utils.escapeHtml(utils.formatBytes(backup.size))}
${constants.ICONS.IMPORT}
${constants.ICONS.DELETE}
`
).join("")
);
},
async loadWebDavBackups() {
const config = core.getWebDavConfig();
const refreshBtn = ui.qs("#btn-webdav-refresh");
if (!config.url || !config.username || !config.password) {
state.webdavBackups = [];
ui.renderWebDavBackupList([], utils.t("webdav_need_config"));
return;
}
await ui.runUiAction({
button: refreshBtn,
idleText: utils.t("webdav_refresh"),
errorKey: "webdav_list_err",
successMessage: utils.t("webdav_refresh_ok"),
action: async () => {
state.webdavBackups = await core.listWebDavBackups();
return state.webdavBackups;
},
onSuccess: (backups) => {
ui.renderWebDavBackupList(backups);
},
onError: (error) => {
const toastMessage = utils.getWebDavErrorMessage(error, "webdav_list_err");
state.webdavBackups = core.getCachedWebDavBackups();
if (state.webdavBackups.length) {
ui.renderWebDavBackupList(state.webdavBackups);
} else {
ui.renderWebDavBackupList([], toastMessage);
}
}
});
}
};
}
// src/app/ui.js
function createUI({ state, constants, utils, templates, core, styleCss }) {
const ui = {
getPageTitle(pageId) {
if (pageId === "pg-set") return utils.t("nav_set");
if (pageId === "pg-notice") return utils.t("nav_notice");
if (pageId === "pg-about") return utils.t("nav_about");
if (pageId === "pg-account-settings") return utils.t("account_settings");
if (pageId === "pg-webdav") return utils.t("nav_webdav");
return "";
},
qs(selector) {
return state.uiRoot ? state.uiRoot.querySelector(selector) : null;
},
qsa(selector) {
return state.uiRoot ? state.uiRoot.querySelectorAll(selector) : [];
}
};
Object.assign(
ui,
createFeedbackMethods({ state, constants, utils, core, ui }),
createSwitchingMethods({ state, constants, utils, templates, core, ui }),
createWebDavUiMethods({ state, constants, utils, core, ui }),
createPanelMethods({ state, constants, utils, templates, styleCss, ui }),
createEventMethods({ state, constants, utils, core, ui }),
{
init() {
ui.createShadowHost();
ui.createFab();
ui.createPanel();
ui.refresh();
}
}
);
return ui;
}
// src/main.js
(() => {
"use strict";
if (window.self !== window.top) return;
const state = createState({ constants: CONST, i18nData: I18N_DATA });
const utils = createUtils({ state, constants: CONST, i18nData: I18N_DATA });
const templates = createTemplates({ state, constants: CONST, i18nData: I18N_DATA, utils });
const core = createCore({ state, constants: CONST, utils });
const ui = createUI({ state, constants: CONST, utils, templates, core, styleCss: STYLE_CSS });
core.setUI(ui);
const start = () => {
if (!document.body) {
setTimeout(start, 200);
return;
}
ui.init();
new MutationObserver(() => {
if (!document.getElementById("anme-app-host")) {
ui.init();
}
}).observe(document.body, { childList: true });
};
window.addEventListener("resize", () => {
if (!state.fab) return;
if (state.fab.style.left) {
state.fab.style.left = `${Math.min(Math.max(0, parseFloat(state.fab.style.left)), window.innerWidth - 44)}px`;
state.fab.style.top = `${Math.min(Math.max(0, parseFloat(state.fab.style.top)), window.innerHeight - 44)}px`;
}
if (state.panel && state.panel.classList.contains("show")) {
ui.syncPanelPos();
}
ui.syncFloatingUiVisibility?.();
});
["fullscreenchange", "webkitfullscreenchange", "mozfullscreenchange", "MSFullscreenChange"].forEach((eventName) => {
document.addEventListener(eventName, () => {
ui.syncFloatingUiVisibility?.();
});
});
document.addEventListener("click", (event) => {
if (!state.panel || !state.panel.classList.contains("show")) return;
const path = event.composedPath();
const isInsideNoteTooltip = Boolean(state.noteTooltipEl) && (path.includes(state.noteTooltipEl) || path.some((node) => typeof state.noteTooltipEl?.contains === "function" && state.noteTooltipEl.contains(node)));
if (!path.includes(state.panel) && !path.includes(state.fab) && !path.includes(state.dialogMask) && !isInsideNoteTooltip) {
ui.closePanel();
}
});
GM_registerMenuCommand(utils.t("menu_open"), () => {
state.isForcedShow = true;
ui.init();
if (state.fab) state.fab.style.display = "flex";
if (state.panel && !state.panel.classList.contains("show")) {
state.panel.classList.add("show");
ui.syncPanelPos();
}
ui.refresh();
});
if (document.readyState === "complete" || document.readyState === "interactive") {
start();
} else {
window.addEventListener("DOMContentLoaded", start);
}
})();
})();