diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..0cd2f2b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,24 @@ +name: Release Obsidian plugin + +on: + push: + tags: + - "*" + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Create release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + tag="${GITHUB_REF#refs/tags/}" + + gh release create "$tag" \ + --title="$tag" \ + --draft \ + main.js manifest.json styles.css \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..08ad576 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# vscode +.vscode +.history + +# Intellij +*.iml +.idea + +# npm +node_modules + +# Don't include the compiled main.js file in the repo. +# They should be uploaded to GitHub releases instead. + +# Exclude sourcemaps +*.map + +# obsidian +data.json + +# git +.git + +# Exclude macOS Finder (System Explorer) View States +.DS_Store +.history +main_副本.js +main-no-market.js +styles-no-market.css diff --git a/.hotreload b/.hotreload new file mode 100644 index 0000000..e69de29 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e18bd36 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 wish5115 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README-zh.md b/README-zh.md new file mode 100644 index 0000000..f5163d7 --- /dev/null +++ b/README-zh.md @@ -0,0 +1,50 @@ +# 介绍 + +打开悬浮设置面板或替换默认的设置窗口为悬浮面板。依赖于Hover Editor插件。 + +# 使用场景 + +当你需要设置某个样式或功能而需要频繁打开设置窗口时,这将非常有用。 + +# 特色 + +- 设置窗口可以悬浮打开,取消modal模式。 + +- 系统设置面板和悬浮设置面板可以共存,不相互影响。 + +- 可以打开多个插件市场面板。 + +- 左侧面板可隐藏或显示,选中标签自动滚动到可视窗口内。 + +# 安装 + +到这里下载 [Floating Settings](https://github.com/wish5115/obsidian-floating-settings/releases/) + +下载后把 obsidian-floating-settings.zip 解压,放到插件目录,重启 obsidian,然后别忘了到设置里开启插件即可。 + +亦可在obsidian的插件市场中搜索安装。(暂未上架) + + +# 使用 + +1. `ctrl+p` to open the command panel, select the command "Open Hover Settings". + +2. If "Replace the Default Settings" is checked in Settings, you can also turn it on via the Settings button in the bottom left corner. + +# 截图 + +![](https://cdn.jsdelivr.net/gh/wish5115/obsidian-floating-settings@main/screenshots/zh-preview.png) + +![](https://cdn.jsdelivr.net/gh/wish5115/obsidian-floating-settings@main/screenshots/zh-market.png) + +![](https://cdn.jsdelivr.net/gh/wish5115/obsidian-floating-settings@main/screenshots/zh-demo.gif) + + +# 反馈 + +您有任何问题都可以到 [issues](https://github.com/wish5115/obsidian-floating-settings/issues) 去反馈。 + + +# 鸣谢 + +灵感来自于:@knight 大佬的回复 参见 [设置的窗口 怎么悬浮?](https://forum-zh.obsidian.md/t/topic/35799/19) diff --git a/README.md b/README.md new file mode 100644 index 0000000..6dee62a --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# Obsidian Open in Top Window + +[中文帮助](https://github.com/wish5115/obsidian-floating-settings/blob/main/README-zh.md) + +# Introduction + +Open the Floating Settings panel or replace the default settings modal with a floating panel. Depends on the Hover Editor plugin. + +# Usage Scenarios + +This is useful when you need to set a certain style or feature and need to open the settings modal frequently. + +# Features + +- Settings window can be opened on hover and modal mode can be canceled. + +- System settings panel and hover settings panel can coexist without affecting each other. + +- Multiple plugin market panels can be opened. + +- The left panel can be hidden or shown. Selected tabs automatically scroll to the visible window. + +# Installation + +Go here to download [Floating Settings](https://github.com/wish5115/obsidian-floating-settings/releases/) + +After download, unzip obsidian-floating-settings.zip, put it into plugin directory, restart obsidian, and don't forget to open the plugin in settings. + +You can also search and install the plugin in obsidian's plugin market. (Not yet available) + + +# Use + +1. Right click menu to open, select "Open the Floating Settings". + +2. Command to open, select "Open current tab in top window" or "Move current tab to top window". + + +# Screenshot + +![](https://cdn.jsdelivr.net/gh/wish5115/obsidian-floating-settings@main/screenshots/en-preview.png) + +![](https://cdn.jsdelivr.net/gh/wish5115/obsidian-floating-settings@main/screenshots/en-market.png) + +![](https://cdn.jsdelivr.net/gh/wish5115/obsidian-floating-settings@main/screenshots/en-demopng) + + +# Feedback + +You can go to [issues](https://github.com/wish5115/obsidian-floating-settings/issues) to give feedback if you have any questions. + + +# Acknowledgments + +Inspired by: reply from @knight mogul. See [设置的窗口 怎么悬浮?](https://forum-zh.obsidian.md/t/topic/35799/19) + diff --git a/main.js b/main.js new file mode 100644 index 0000000..6763f0b --- /dev/null +++ b/main.js @@ -0,0 +1,701 @@ +const { Plugin, PluginSettingTab, Setting, ItemView, setIcon } = require('obsidian'); + +///////////////////////////////// 参数配置 /////////////////////////////////// + +// 多语言配置 +const langMap = { + "en": { + //settings + "Replacing the Default Settings": "Replacing the Default Settings", + "Replaces the default settings modal with a floating panel.": "Replaces the default settings modal with a floating panel.", + "Default Floating Panel Size": "Default Floating Panel Size", + "Default width and height of the float settings panel.": "Default width and height of the float settings panel.", + "Force display to specified size": "Force display to specified size", + "Strictly follow the specified width and height display, the default will be automatically adjusted according to the remaining space, the specified width and height is just a reference.": "Strictly follow the specified width and height display, the default will be automatically adjusted according to the remaining space, the specified width and height is just a reference.", + "Waiting for the Hover Editor to Load":"Waiting for the Hover Editor to Load", + "The unit is seconds. If this time is exceeded, it is assumed that the hover editor is not installed or enabled.": "The unit is seconds. If this time is exceeded, it is assumed that the hover editor is not installed or enabled.", + "The unit is seconds.":"The unit is seconds.", + "Notice me when Hover Editor not enabled or installed":"Notice me when Hover Editor not enabled or installed", + "If Hover Editor is not installed or enabled, obsidian will notify me every time it starts.": "If Hover Editor is not installed or enabled, obsidian will notify me every time it starts.", + "Floating Plug-Ins Market":"Floating Plug-Ins Market", + "Once enabled, opening the plugin market will also use the floating panel.": "Once enabled, opening the plugin market will also use the floating panel.", + "Plugin Market allows Multiple Instances": "Plugin Market allows Multiple Instances", + "Multiple plug-in marketplace floating windows can be opened.": "Multiple plug-in marketplace floating windows can be opened.", + "Sidebar selected tabs, auto-scroll to visual area": "Sidebar selected tabs, auto-scroll to visual area", + "When you open the setting, the last selected tab automatically scrolls to the visual area.": "When you open the setting, the last selected tab automatically scrolls to the visual area.", + "Left sidebar can be displayed or hidden": "Left sidebar can be displayed or hidden", + "Click the button in the upper right corner of the left sidebar to show or hide the left sidebar.": "Click the button in the upper right corner of the left sidebar to show or hide the left sidebar.", + "Note: All of the above actions take effect the next time you open the Settings panel!": "Note: All of the above actions take effect the next time you open the Settings panel!", + //plugin + "Open the Floating Settings": "Open the Floating Settings", + "Hover Editor is required for this plugin to work.": "Hover Editor is required for this plugin to work.", + //FloatSettingView + "Settings" : "Settings", + "Plugin Market": "Plugin Market", + }, + "zh-cn": { + "Replacing the Default Settings": "替换默认设置", + "Replaces the default settings modal with a floating panel.": "替换默认的设置窗口为悬浮面板。", + "Default Floating Panel Size": "悬浮窗口默认大小", + "Default width and height of the float settings panel.": "悬浮窗口默认宽度和高度。", + "Force display to specified size": "强制按照指定大小显示", + "Strictly follow the specified width and height display, the default will be automatically adjusted according to the remaining space, the specified width and height is just a reference.": "严格按照指定的宽高显示,默认会自动根据剩余空间调整,指定宽高只是参考。", + "Waiting for the Hover Editor to Load":"等待Hover Editor加载的时间", + "The unit is seconds. If this time is exceeded, it is assumed that the hover editor is not installed or enabled.": "单位是秒,如果超过这个时间,则认为hover editor未安装或未开启。", + "The unit is seconds.":"单位:秒", + "Notice me when Hover Editor not enabled or installed":"未安装或未开启Hover Editor时通知我", + "If Hover Editor is not installed or enabled, obsidian will notify me every time it starts.": "如果Hover Editor未安装或未开启,obsidian启动时会通知我。", + "Floating Plug-Ins Market":"悬浮插件市场", + "Once enabled, opening the plugin market will also use the floating panel.": "开启后,打开插件市场也会使用悬浮面板。", + "Plugin Market allows Multiple Instances": "插件市场允许多实例", + "Multiple plug-in marketplace floating windows can be opened.": "可以打开多个插件市场浮动窗口。", + "Sidebar selected tabs, auto-scroll to visual area": "侧边栏选中标签,自动滚动到可视区域", + "When you open the setting, the last selected tab automatically scrolls to the visual area.": "打开设置时,上次选中的标签自动滚动到可视区域。", + "Left sidebar can be displayed or hidden": "左侧栏可显示或隐藏", + "Click the button in the upper right corner of the left sidebar to show or hide the left sidebar.": "点击左侧栏右上角的按钮可显示或隐藏左侧栏。", + "Note: All of the above actions take effect the next time you open the Settings panel!": "注意:以上操作均是下次打开时生效!", + //plugin + "Open the Floating Settings": "打开悬浮设置", + "Hover Editor is required for this plugin to work.": "依赖插件Hover Editor未安装或未开启", + //FloatSettingView + "Settings" : "设置", + "Plugin Market": "插件市场", + }, +} + +// 设置默认值 +const DEFAULT_SETTINGS = { + isReplaceDefaultSettings: true, + width: 932.667, + height: 583.333, + maxWaitTime: 10, + isHoverEditorNotice: true, + isFloatMarket: true, + isMarketMultiIns: true, + isAutoScroll: true, + isAllowShowHide: true, +}; + +// 悬浮设置视图类型 +const FLOAT_SETTING_VIEW_TYPE = "floating-settings"; +// 悬浮设置视图类名 +const FLOAT_SETTING_POPOVER_CLASS = "floating-settings-popover"; +// 悬浮插件市场视图类型 +const FLOAT_PLUGIN_MARKET_VIEW_TYPE = "floating-plugin-market"; +// 悬浮插件市场视图类名 +const FLOAT_PLUGIN_MARKET_POPOVER_CLASS = "floating-plugin-market-popover"; + + +///////////////////////////////// 插件主体 /////////////////////////////////// + +module.exports = class extends Plugin { + // 当前popover实例 + popover = null; + // 当前popover所在leaf + popoverLeaf = null; + // 插件市场popover实例 + lastMarketPopover = null; + // 插件市场popover所在leaf + lastMarketPopoverLeaf = null; + // 默认打开设置面板函数 + originSettingOpen = this.app.setting.open.bind(this.app.setting); + // 打开悬浮设置视图函数 + floatingSettingOpen = () => { this.openFloatingSettingsView(); } + + // 默认设置面板onClose + originalOnClose = this.app.setting.onClose.bind(this.app.setting); + + // 默认打开设置面板tab函数 + originalOpenTab = this.app.setting.openTab.bind(this.app.setting); + + async onload() { + // 加载配置文件 + await this.loadSettings(); + + // 注册打开当前文档到置顶窗口命令 + this.addCommand({ + id: "open-the-floating-settings", + name: t("Open the Floating Settings"), + callback: () => { + this.openFloatingSettingsView(); + }, + }); + + // 注册悬浮设置视图 + if(!this.app.viewRegistry.getViewCreatorByType(FLOAT_SETTING_VIEW_TYPE)){ + this.registerView( + FLOAT_SETTING_VIEW_TYPE, + (leaf) => new FloatSettingView(leaf, this) + ); + } + + // 注册插件市场视图 + if(!this.app.viewRegistry.getViewCreatorByType(FLOAT_PLUGIN_MARKET_VIEW_TYPE)){ + this.registerView( + FLOAT_PLUGIN_MARKET_VIEW_TYPE, + (leaf) => new FloatPluginMarketView(leaf, this) + ); + } + + // 替换默认设置面板 + if(this.settings.isReplaceDefaultSettings) this.app.setting.open = this.floatingSettingOpen; + + // 监听布局加载完成事件 + this.app.workspace.onLayoutReady(async () => { + // 等待HoverEditor加载 + if(!this.hoverEditor()){ + // 等待Hover Editor加载完成 + const delay = 100; + const times = Math.ceil((this.settings.maxWaitTime * 1000) / delay); + let count = 0; + for(;;) { + if(this.hoverEditor()){ + break; + } + if(count >= times) { + if (this.settings.isHoverEditorNotice) new Notice(t("Hover Editor is required for this plugin to work.")); + break; + } + await sleep(delay); + count++; + } + } + }); + + // 监听默认设置关闭事件,关闭设置面板同时关闭悬浮设置面板(开发时对app.setting的修改,恢复理想状态需要重启) + this.app.setting.onClose = () => { + this.originalOnClose(); + if(this.popover) this.popover.hide(); + }; + + // 重置openTab事件(开发时对app.setting的修改,恢复理想状态需要重启) + this.app.setting.openTab = async (tab) => { + this.originalOpenTab(tab); + const i18n = i18next.getDataByLanguage(i18next.language||'en'); + // 监听about标签的注册和购买按钮事件 + if(tab.id === "about"){ + const registerBtnText = i18n.default.plugins.sync["button-sign-up"]; + const buyBtnText = i18n.default.plugins.sync["button-purchase-subscription"]; + await sleep(100); + tab.containerEl.querySelectorAll(".setting-item-control button").forEach(button => { + if(button.textContent === registerBtnText || button.textContent === buyBtnText) { + if(this.app.plugins.plugins['surfing']){ + button.addEventListener('click', async () => { + await sleep(40); + this.hoverEditor().dockPopoverToWorkspace(this.app.workspace.activeLeaf); + this.popover.titleEl.querySelector(".popover-title").textContent = t("Settings"); + }); + } + } + }); + } + // 监听插件市场,浏览按钮事件 + if(tab.id === "community-plugins" && this.settings.isFloatMarket){ + const browseBtnText = i18n.default.setting["third-party-plugin"]["button-browse"]; + await sleep(100); + tab.containerEl.parentElement.addEventListener('click', async (event) => { + if(event.target.textContent === browseBtnText && this.settings.isFloatMarket) { + if(!this.popover || !this.popover.hoverEl.contains(event.target)) return; + // 暂时隐藏插件市场 + //document.body.classList.add("with-plugin-market-hide"); + const onShow = (popover, leaf) => { + // onShow回调 + popover.hoverEl.querySelector(".community-modal-search-results").addEventListener('click', ()=>{ + const marketDetail = popover.hoverEl.querySelector(".community-modal-details"); + if(!marketDetail?.getAttribute("data-bind")){ + const optionText = i18n.default.setting.options; + const hotkeyText = i18n.default.setting.hotkeys.name; + marketDetail.addEventListener('click', (event) => { + if(event.target.textContent === optionText || event.target.textContent === hotkeyText){ + // 没有替换默认设置面板时,在popover插件市场里点击选项等,需要调用打开悬浮设置面板 + if(!this.settings.isReplaceDefaultSettings) { + // 先关闭默认设置的modal面板 + document.body.findAll(".modal-container:has(.mod-settings) .modal-bg") + .find(item => !item?.closest(".floating-settings-popover"))?.click(); + // 再打开悬浮设置面板 + this.openFloatingSettingsView(); + } + } + }); + marketDetail.setAttr("data-bind", true); + } + }); + // 恢复隐藏插件市场 + //document.body.classList.remove("with-plugin-market-hide"); + }; + // 打开插件市场面板 + this.openFloatingPluginMarketView(onShow); + } + }); + } + }; + + // 添加配置面板 + this.addSettingTab(new FloatingSettingsSettingTab(this.app, this)); + } + onunload() { + this.app.workspace.detachLeavesOfType(FLOAT_SETTING_VIEW_TYPE); + this.app.setting.open = this.originSettingOpen; + this.app.setting.onClose = this.originalOnClose; + this.app.setting.openTab = this.originalOpenTab; + } + + async loadSettings() { + this.settings = Object.assign( + {}, + DEFAULT_SETTINGS, + await this.loadData() + ); + } + async saveSettings() { + await this.saveData(this.settings); + } + + // 获取Hover Editor插件实例 + hoverEditor() { + return this.app.plugins.plugins['obsidian-hover-editor']; + } + + // 打开悬浮设置视图 + async openFloatingSettingsView(onShow = ()=>{}) { + const hoverEditor = this.hoverEditor(); + // 未开启或安装hover editor则使用默认设置面板打开 + if(!hoverEditor) { + this.originSettingOpen(); + return; + } + if(!document.querySelector('.' + FLOAT_SETTING_POPOVER_CLASS)){ + // 创建leaf和视图 + const leaf = app.workspace.createLeafInParent(app.workspace.floatingSplit); + await leaf.setViewState({ + type: FLOAT_SETTING_VIEW_TYPE, + active: false, + }); + this.popoverLeaf = leaf; + // 把leaf转换为悬浮面板 + app.setting.openTabById(app.setting.lastTabId||app.setting.settingTabs[0]?.id); + this.convertLeafToPopover(leaf, async () => { + // 获取当前悬浮窗实例 + const lastPopover = hoverEditor.activePopovers.last(); + // 赋值弹窗本次实例 + this.popover = lastPopover; + //设置弹窗信息 + const setPopoverReact = async () => { + await nextFrame(); + // 设置宽高 + lastPopover.hoverEl.style.width = this.settings.width+'px'; + const height = Number(this.settings.height) >= window.innerHeight ? window.innerHeight * 0.8 : this.settings.height; + lastPopover.hoverEl.style.height = height+'px'; + // 设置坐标 + const realStyle = getComputedStyle(lastPopover.hoverEl, null); + const top = (window.innerHeight - parseFloat(realStyle.height))/2; + lastPopover.hoverEl.style.top = (top > 0 ? top : 0) + 'px'; + const left = (window.innerWidth - parseFloat(realStyle.width))/2; + lastPopover.hoverEl.style.left= (left > 0 ? left : 0) + 'px'; + lastPopover.hoverEl.setAttribute("data-x", String(left)); + lastPopover.hoverEl.setAttribute("data-y", String(top)); + }; + // 设置弹窗样式 + lastPopover.hoverEl.classList.add(FLOAT_SETTING_POPOVER_CLASS); + setIcon(lastPopover.pinEl, "settings"); + setPopoverReact(); + + // 设置显示隐藏按钮 + const setShowHideBtn = () => { + // settings panel style + if(this.settings.isAllowShowHide){ + // 添加显示隐藏按钮 + if(app.setting.contentEl.querySelector(".side-show-hide-btn")) return; + const showHideBtn = createDiv({cls: "side-show-hide-btn"}); + setIcon(showHideBtn, "menu"); + showHideBtn.onclick = () => { + if(app.setting.tabHeadersEl.classList.contains("with-hide")){ + // 显示侧边栏 + app.setting.tabHeadersEl.classList.remove("with-hide"); + // 滚动选中标签到可视区域 + if(this.settings.isAutoScroll && !isInParentViewport(app.setting.activeTab.navEl, app.setting.tabHeadersEl)) { + app.setting.activeTab.navEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + } else { + // 隐藏侧边栏 + app.setting.tabHeadersEl.classList.add("with-hide"); + } + } + app.setting.tabContentContainer.classList.add("with-show-hide-btn"); + app.setting.contentEl.insertBefore(showHideBtn, app.setting.tabContentContainer); + } + } + setShowHideBtn(); + + //滚动侧边栏选中标签 + if(app.setting.activeTab.navEl && this.settings.isAutoScroll){ + app.setting.activeTab.navEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + + // popover标题点击和拖动事件 + lastPopover.titleEl.addEventListener('click', () => { + this.activePopover(leaf, lastPopover); + }); + + // onShow回调函数 + if(typeof onShow === 'function') onShow(lastPopover, leaf); + }); + } else { + //激活弹窗 + if(this.popoverLeaf && this.popover) this.activePopover(this.popoverLeaf, this.popover); + } + } + + // 打开悬浮插件市场视图 + async openFloatingPluginMarketView(onShow = ()=>{}) { + const hoverEditor = this.hoverEditor(); + // 未开启或安装hover editor则使用默认设置面板打开 + if(!hoverEditor) { + //document.body.classList.remove("with-plugin-market-hide"); + return; + } + // 如果单实例,则激活上次的popover + if(!this.settings.isMarketMultiIns && document.querySelector('.' + FLOAT_PLUGIN_MARKET_POPOVER_CLASS)) { + const marketModal = document.querySelector(".modal-container .mod-community-modal"); + if(marketModal){ + const modalBg = marketModal.parentElement.querySelector(".modal-bg"); + modalBg?.click(); + } + //document.body.classList.remove("with-plugin-market-hide"); + this.activePopover(this.lastMarketPopoverLeaf, this.lastMarketPopover); + return; + } + // 创建leaf和视图 + const leaf = app.workspace.createLeafInParent(app.workspace.floatingSplit); + await leaf.setViewState({ + type: FLOAT_PLUGIN_MARKET_VIEW_TYPE, + active: false, + }); + this.lastMarketPopoverLeaf = leaf; + this.convertLeafToPopover(leaf, async () => { + // 获取当前悬浮窗实例 + const lastPopover = hoverEditor.activePopovers.last(); + // 赋值弹窗本次实例 + this.lastMarketPopover = lastPopover; + //设置弹窗信息 + const setPopoverReact = async () => { + await nextFrame(); + // 设置宽高 + lastPopover.hoverEl.style.width = this.settings.width+'px'; + const height = Number(this.settings.height) >= window.innerHeight ? window.innerHeight * 0.8 : this.settings.height; + lastPopover.hoverEl.style.height = height+'px'; + // 设置坐标 + const realStyle = getComputedStyle(lastPopover.hoverEl, null); + const top = (window.innerHeight - parseFloat(realStyle.height))/2; + lastPopover.hoverEl.style.top = (top > 0 ? top : 0) + 'px'; + const left = (window.innerWidth - parseFloat(realStyle.width))/2; + lastPopover.hoverEl.style.left= (left > 0 ? left : 0) + 'px'; + lastPopover.hoverEl.setAttribute("data-x", String(left)); + lastPopover.hoverEl.setAttribute("data-y", String(top)); + }; + // 设置弹窗样式 + lastPopover.hoverEl.classList.add(FLOAT_PLUGIN_MARKET_POPOVER_CLASS); + setIcon(lastPopover.pinEl, "plug"); + setPopoverReact(); + + // popover标题点击和拖动事件 + lastPopover.titleEl.addEventListener('click', () => { + this.activePopover(leaf, lastPopover); + }); + + // onShow回调函数 + if(typeof onShow === 'function') onShow(lastPopover, leaf); + }); + } + + // 把leaf转换为悬浮面板 + convertLeafToPopover(oldLeaf, onShow) { + if (!oldLeaf) return; + const newLeaf = this.hoverEditor().spawnPopover(undefined, () => { + const { parentSplit: newParentSplit } = newLeaf; + const { parentSplit: oldParentSplit } = oldLeaf; + oldParentSplit.removeChild(oldLeaf); + newParentSplit.replaceChild(0, oldLeaf, true); + this.app.workspace.setActiveLeaf(oldLeaf, false, true); + if(typeof onShow === 'function') onShow(); + }); + return newLeaf; + } + + // 激活悬浮窗 + isActivating = false; + async activePopover(leaf, popover) { + if(this.isActivating) return; + if(!leaf || !popover) return; + if(popover.hoverEl.classList.contains("is-active")) return; + this.isActivating = true; + // 取消上一次激活的popover焦点 + this.hoverEditor().activePopovers.find(hover=>hover?.hoverEl?.classList?.contains("is-active"))?.hoverEl?.removeClass("is-active"); + // 激活当前popover + popover.hoverEl.addClass("is-active"); + // 根据leaf信息,设置当前焦点的窗口信息 + const titleEl = popover.hoverEl.querySelector(".popover-title"); + if (!titleEl) { this.isActivating = false; return }; + titleEl.textContent = leaf.view?.getDisplayText(); + if (leaf?.view?.getViewType()) { + popover.hoverEl.setAttribute("data-active-view-type", leaf.view.getViewType()); + } + if (leaf.view?.file?.path) { + titleEl.setAttribute("data-path", leaf.view.file.path); + } else { + titleEl.removeAttribute("data-path"); + } + const i18n = i18next.getDataByLanguage(i18next.language||'en'); + // 监听document点击事件 + const listenDocumentClick = () => { + document.addEventListener('click', function(event) { + const targetPopover = event.target.closest(".hover-popover.hover-editor"); + if(popover.hoverEl !== targetPopover) { + // targetPopover null或 在别的popover中,并且被点击的按钮不是 打开悬浮设置按钮(一般是用cmdr添加的按钮) + if(!event.target.classList.contains("floating-settings:open-the-floating-settings") && + !(event.target.classList.contains("lucide-maximize") || event.target.classList.contains("lucide-minimize")) && + event.target.textContent !== i18n.default.setting.options && + event.target.textContent !== i18n.default.setting["third-party-plugin"]["button-browse"] + ) { + popover.hoverEl?.removeClass("is-active"); + } else { + // 如果被点击的按钮不是 打开悬浮设置按钮,重新监听document点击事件 + listenDocumentClick(); + } + // 在别的popover中 + if(targetPopover && + event.target.textContent !== i18n.default.setting.options && + event.target.textContent !== i18n.default.setting["third-party-plugin"]["button-browse"] + ) { + targetPopover?.addClass("is-active"); + } + } else { + // 点击在当前popover,什么都不做,再次监听document点击事件 + listenDocumentClick(); + } + }, {once: true}); + }; + listenDocumentClick(); + await sleep(100); + this.isActivating = false; + } +} + +///////////////////////////////// 悬浮设置面板 /////////////////////////////////// + +class FloatSettingView extends ItemView { + icon = "settings"; + constructor(leaf, plugin) { + super(leaf); + this.plugin = plugin; + } + getViewType() { + return FLOAT_SETTING_VIEW_TYPE; + } + getDisplayText() { + return t("Settings"); + } + async onOpen() { + const container = this.containerEl.children[1]; + container.empty(); + // 这里不是通过api或点击事件打开的,所以这里不会进入modal模式 + container.appendChild(app.setting.modalEl.parentElement); + } + async onClose() { + this.plugin.popover = null; + this.plugin.popoverLeaf = null; + // 关闭悬浮设置面板同时关闭默认设置面板 + app.setting.close(); + } +}; + +///////////////////////////////// 插件市场面板 /////////////////////////////////// + +class FloatPluginMarketView extends ItemView { + icon = "plug"; + constructor(leaf, plugin) { + super(leaf); + this.plugin = plugin; + } + getViewType() { + return FLOAT_PLUGIN_MARKET_VIEW_TYPE; + } + getDisplayText() { + return t("Plugin Market"); + } + async onOpen() { + const container = this.containerEl.children[1]; + container.empty(); + // 这里获取主要内容后取消modal模式 + const marketModal = document.querySelector(".modal-container .mod-community-modal"); + if(marketModal){ + const modalBg = marketModal.parentElement.find(".modal-bg"); + container.appendChild(marketModal); + modalBg?.click(); + } + } + async onClose() {} +}; + +///////////////////////////////// 插件配置 /////////////////////////////////// + +// 插件配置页面 +class FloatingSettingsSettingTab extends PluginSettingTab { + constructor(app, plugin) { + super(app, plugin); + this.plugin = plugin; + } + display() { + const { containerEl } = this; + containerEl.empty(); + + // 替换默认的设置窗口为悬浮窗口 + new Setting(containerEl).setName(t("Replacing the Default Settings")) + .setDesc(t("Replaces the default settings modal with a floating panel.")) + .addToggle((toggle) => { + toggle.setValue(this.plugin.settings.isReplaceDefaultSettings) + .onChange(async (value) => { + this.plugin.settings.isReplaceDefaultSettings = value; + await this.plugin.saveSettings(); + if(value === true){ + this.app.setting.open = this.plugin.floatingSettingOpen; + } else { + this.app.setting.open = this.plugin.originSettingOpen; + } + }); + }); + + // 设置默认窗口宽高 + const windowSizeControlEl = new Setting(containerEl) + .setName(t("Default Floating Panel Size")) + .setDesc(t("Default width and height of the float settings panel.")) + .setClass("panel-size") + .controlEl; + const windowWidth = windowSizeControlEl.createEl("input", { attr: { type: "number", value: Number(this.plugin.settings.width), placeholder: "width" } }); + windowWidth.onchange = () => { + this.plugin.settings.width = Number(windowWidth.value); + this.plugin.saveSettings(); + } + windowSizeControlEl.createSpan({text: "×"}); + const windowHeight = windowSizeControlEl.createEl("input", { attr: { type: "number", value: Number(this.plugin.settings.height), placeholder: "height" } }); + windowHeight.onchange = () => { + this.plugin.settings.height = Number(windowHeight.value); + this.plugin.saveSettings(); + } + + // 插件市场悬浮 + new Setting(containerEl).setName(t("Floating Plug-Ins Market")) + .setDesc(t("Once enabled, opening the plugin market will also use the floating panel.")) + .addToggle((toggle) => { + toggle.setValue(this.plugin.settings.isFloatMarket) + .onChange(async (value) => { + this.plugin.settings.isFloatMarket = value; + await this.plugin.saveSettings(); + }); + }); + + // 插件市场允许多实例 + new Setting(containerEl).setName(t("Plugin Market allows Multiple Instances")) + .setDesc(t("Multiple plug-in marketplace floating windows can be opened.")) + .addToggle((toggle) => { + toggle.setValue(this.plugin.settings.isMarketMultiIns) + .onChange(async (value) => { + this.plugin.settings.isMarketMultiIns = value; + await this.plugin.saveSettings(); + }); + }); + + // 左侧栏可显示隐藏 + new Setting(containerEl).setName(t("Left sidebar can be displayed or hidden")) + .setDesc(t("Click the button in the upper right corner of the left sidebar to show or hide the left sidebar.")) + .addToggle((toggle) => { + toggle.setValue(this.plugin.settings.isAllowShowHide) + .onChange(async (value) => { + this.plugin.settings.isAllowShowHide = value; + await this.plugin.saveSettings(); + }); + }); + + // 侧边栏选中标签自动滚动到可视区域 + new Setting(containerEl).setName(t("Sidebar selected tabs, auto-scroll to visual area")) + .setDesc(t("When you open the setting, the last selected tab automatically scrolls to the visual area.")) + .addToggle((toggle) => { + toggle.setValue(this.plugin.settings.isAutoScroll) + .onChange(async (value) => { + this.plugin.settings.isAutoScroll = value; + await this.plugin.saveSettings(); + }); + }); + + // 如果hover editor未安装或未开启是否提示 + new Setting(containerEl).setName(t("Notice me when Hover Editor not enabled or installed")) + .setDesc(t("If Hover Editor is not installed or enabled, obsidian will notify me every time it starts.")) + .addToggle((toggle) => { + toggle.setValue(this.plugin.settings.isHoverEditorNotice) + .onChange(async (value) => { + this.plugin.settings.isHoverEditorNotice = value; + await this.plugin.saveSettings(); + }); + }); + + // 等待hover editor加载时间 + new Setting(containerEl).setName(t("Waiting for the Hover Editor to Load")) + .setDesc(t("The unit is seconds. If this time is exceeded, it is assumed that the hover editor is not installed or enabled.")) + .addText((text) => { + text.setValue(this.plugin.settings.maxWaitTime) + .setPlaceholder(t("The unit is seconds.")) + .onChange(async (value) => { + this.plugin.settings.maxWaitTime = value; + await this.plugin.saveSettings(); + }); + }) + .controlEl.find("input") + .setAttr("type", "number"); + + // 提醒说明 + new Setting(containerEl).setName(t("Note: All of the above actions take effect the next time you open the Settings panel!")) + } +} + +///////////////////////////////// 功能函数 /////////////////////////////////// + +// 翻译文本为对应语言 +function t(str) { + const lang = moment.locale(); + if(langMap[lang] && langMap[lang][str]) { + return langMap[lang][str]; + } + if(langMap["en"] && langMap["en"][str]) { + return langMap["en"][str]; + } + return str; +} + +// 判断某子元素是否在父元素可视区内 +function isInParentViewport(childEl, parentEl) { + // 获取子元素相对父元素的位置 + const childRect = childEl.getBoundingClientRect(); + const parentRect = parentEl.getBoundingClientRect(); + + // 判断子元素是否在父元素的可视区域内 + // 这里假设“可视区域”指的是父元素的客户端高度和宽度范围内 + return ( + childRect.top >= parentRect.top && + childRect.bottom <= parentRect.bottom && + childRect.left >= parentRect.left && + childRect.right <= parentRect.right + ); +} + +// 输出调试信息 +function debug(data){ + console.log(data); + if(typeof data !== 'string') { + try{ + data = JSON.stringify(data); + }catch(e){ + data = data.toString() + } + } + new Notice(data, 0); +} \ No newline at end of file diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..1c29652 --- /dev/null +++ b/manifest.json @@ -0,0 +1,11 @@ +{ + "id": "floating-settings", + "name": "Floating Settings", + "version": "0.0.1", + "minAppVersion": "1.5.8", + "description": "Open the Floating Settings panel or replace the default settings modal with a floating panel. Depends on the Hover Editor.", + "author": "Wilson", + "authorUrl": "https://github.com/wish5115", + "helpUrl": "https://github.com/wish5115/obsidian-floating-settings", + "isDesktopOnly": true +} diff --git a/screenshots/en-demo.gif b/screenshots/en-demo.gif new file mode 100644 index 0000000..71e4445 Binary files /dev/null and b/screenshots/en-demo.gif differ diff --git a/screenshots/en-market.png b/screenshots/en-market.png new file mode 100644 index 0000000..ddde2e4 Binary files /dev/null and b/screenshots/en-market.png differ diff --git a/screenshots/en-preview.png b/screenshots/en-preview.png new file mode 100644 index 0000000..5e7b2ff Binary files /dev/null and b/screenshots/en-preview.png differ diff --git a/screenshots/zh-demo.gif b/screenshots/zh-demo.gif new file mode 100644 index 0000000..2c66946 Binary files /dev/null and b/screenshots/zh-demo.gif differ diff --git a/screenshots/zh-market.png b/screenshots/zh-market.png new file mode 100644 index 0000000..b22a3fb Binary files /dev/null and b/screenshots/zh-market.png differ diff --git a/screenshots/zh-preview.png b/screenshots/zh-preview.png new file mode 100644 index 0000000..9d51d29 Binary files /dev/null and b/screenshots/zh-preview.png differ diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..1bb3a35 --- /dev/null +++ b/styles.css @@ -0,0 +1,56 @@ +/* 隐藏原设置窗口的背景遮罩和关闭按钮 */ +:is(.floating-settings-popover, .floating-plugin-market-popover) :is(.modal-bg, .modal-close-button) { + display: none; +} +/* 修改默认原设置窗口的宽高以适应hoverEditor */ +:is(.floating-settings-popover, .floating-plugin-market-popover) .modal.mod-sidebar-layout { + max-width: 100%; + max-height: 100%; + width: 100%; + height: 100%; + border: 0; + border-radius: 0; +} +/* 隐藏popover顶部的show-navbar按钮 */ +:is(.floating-settings-popover, .floating-plugin-market-popover) .popover-action.mod-show-navbar{ + display: none; +} +/* 隐藏popover中非hoverEditor的视图 */ +.floating-settings-popover .workspace-leaf:has([data-type='surfing-view']) { + display: none; +} +/* 设置左侧栏显示隐藏按钮 */ +.floating-settings-popover .side-show-hide-btn{ + margin-top: 5px; + margin-left: 5px; + padding: 5px; + border-radius: 5px; + width: fit-content; + height: fit-content; +} +.floating-settings-popover .side-show-hide-btn svg.svg-icon{ + vertical-align: text-top; +} +.floating-settings-popover .side-show-hide-btn:hover{ + background-color: var(--background-modifier-hover); +} +/* 侧边栏隐藏 */ +.floating-settings-popover .mod-settings .vertical-tab-header.with-hide { + display: none; +} +/* 右侧内区边距减去显示隐藏按钮宽度 */ +.floating-settings-popover .mod-settings .vertical-tab-content.with-show-hide-btn{ + padding-inline-start: calc(var(--size-4-12) - 28px); +} +/* pin按钮禁用 */ +:is(.floating-settings-popover, .floating-plugin-market-popover) .popover-header-icon.mod-pin-popover{ + pointer-events: none; +} +/* 临时隐藏插件市场 */ +/* body.with-plugin-market-hide .modal-container:has(.mod-community-modal) { + display: none; +} */ +/* 插件市场弹窗边距 */ +.floating-plugin-market-popover .workspace-leaf-content .view-content { + padding: 0; +} \ No newline at end of file