├── .gitignore ├── LICENSE ├── README.md ├── assets ├── Untitled-2024-06-13-0228.png ├── ba221f08ea6fa4f224889186c12606d7.png ├── image-20231214154748461.png ├── image-20231214195533699.png ├── image-20231216154341755.png ├── image-20231216234628300.png ├── image-20231216235029002.png ├── image-20231229004156479.png ├── image-20231229004839082.png ├── image-20231229163509526.png ├── image-20240126130251886.png ├── image-20240210193023137.png └── image-20240217172403274.png ├── doc └── README_EN.md ├── lib ├── BootUp.ahk ├── CharsetDetect.ahk ├── JSON.ahk ├── MyTool.ahk ├── NetRequest.ahk ├── PotplayerControl.ahk ├── PotplayerFragment.ahk ├── Render.ahk ├── TemplateParser.ahk ├── TimeTool.ahk ├── entity │ ├── Config.ahk │ └── MediaData.ahk ├── gui │ ├── Gui.ahk │ ├── GuiControl.ahk │ ├── GuiExample.ahk │ └── i18n │ │ ├── I18n.ahk │ │ ├── LCID.ahk │ │ ├── en-US.ini │ │ └── zh-CN.ini ├── ico.ico ├── icon.png ├── note2potplayer │ ├── RegisterUrlProtocol.ahk │ ├── note2potplayer.ahk │ └── sqlite │ │ ├── Class_SQLiteDB.ahk │ │ └── SqliteControl.ahk ├── socket │ ├── Socket.ahk │ ├── SocketService.ahk │ └── example │ │ ├── _socket.ahk │ │ └── _socket_example.ahk ├── sqlite │ ├── Class_SQLiteDB.ahk │ ├── SQLite3.dll │ ├── Sample_Class_SQLiteDB.ahk │ └── SqliteControl.ahk ├── srt.ahk └── word │ └── word.ahk └── markdown2potplayer.ahk /.gitignore: -------------------------------------------------------------------------------- 1 | config.db 2 | markdown2potplayer.exe 3 | note2potplayer.exe 4 | word.exe -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 livelycode 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [README_EN](./doc/README_EN.md) 2 | 3 | # 使用说明 4 | 5 | 效果: 6 | 7 | ![image-20240210193023137](./assets/image-20240210193023137.png) 8 | 9 | ## 1 前置准备 10 | 11 | 1. 运行脚本文件:`markdown2potplayer` 12 | 2. 双击右下角的托盘图标![image-20231229163509526](./assets/image-20231229163509526.png) 13 | 14 | ![image-20240126130251886](./assets/image-20240126130251886.png) 15 | 16 | **修改1**:修改Potplayer的主程序路径,为你本机的路径 17 | 18 | **修改2**:指定笔记软件的软件名称 19 | 20 | - `说明`:只会按照从上至下的顺序,给1个笔记软件粘贴回链 21 | - 例如:此处同时配置了obsidian和typora 22 | - 情况1:在obsidian和typora同时打开的情况下,只会粘贴到obsidian中 23 | - 情况2:只有typora打开,则粘贴到typora中 24 | 25 | 26 | ## 2 使用 27 | 28 | 1. 打开`markdown2potplayer` 29 | 1. 打开obsidian 30 | 1. 打开potplayer 31 | 32 | 3. 在笔记软件、或者potplayer`窗口激活`的状态下,按热键Alt+G(默认),即可自动粘贴**视频的回链**到obsidian中 33 | 4. 在笔记软件、或者potplayer`窗口激活`的状态下,按热键Ctrl+Alt+G(默认),即可自动粘贴**图片+视频的回链**到obsidian中 34 | 35 | # 高级设置 36 | 37 | ## 关于notion 38 | 39 | 1. notion是运行在浏览器中,目前浏览器众多 40 | 2. 暂时支持如下 41 | 1. 微软Edge:msedge.exe 42 | 2. 谷歌:chrome.exe 43 | 3. 360极速版:360chrome.exe 44 | 4. 火狐:firefox.exe 45 | 46 | 3. **请 鼠标左键 点击notion中的链接,不要使用新建标签页打开 例如:Ctrl + 鼠标左键、鼠标中键** 47 | 48 | 49 | 50 | ![image-20240217172403274](./assets/image-20240217172403274.png) 51 | 52 | 53 | 54 | ## 模板的修改 55 | 56 | ![image-20231229004156479](./assets/image-20231229004156479.png) 57 | 58 | 此处修改的是**粘贴数据的模板**,一共有`6`个模板项。 59 | 60 | 61 | 62 | **注意**:这5项,不是哪个位置都可以用 63 | 64 | - 字幕模板:只能用`{subtitle}` 65 | - 回链的名称:只能用`{name}`、`{time}`、`{subtitleTemplate}` 66 | - 回链模板:只能用`{title}`、`{subtitleTemplate}` 67 | - 视频回链模板:只能用`{image}`、`{title}`、`{subtitleTemplate}` 68 | 69 | 70 | 71 | 逐一说明: 72 | 73 | - `{name}`:代表视频的文件名称,也就是`[`视频**名称**`]` 74 | - `{time}`:代表当前播放视频的时间,也就是`[`视频**时间**`]` 75 | - `{title}`:**代表整个markdown格式的链接**,例如`[百度](https://www.baidu.com)`也就是说,此处是markdown格式的potplayer回链 76 | - `{image}`:代表**图片粘贴的位置** 77 | - `{subtitle}`: 代表的是当前在potplayer播放的视频中,能够复制的字幕 78 | - `{subtitleTemplate}`: 代表的是 字幕模板 ,**如果当前播放的视频中没有字幕时,字幕模板将不会有数据产生**,也就是说没有字幕,则`{subtitleTemplate}`消失 79 | 80 | 关于字幕模板: 81 | - `{time}`: 代表 potplayer中的当前时间戳 82 | - `{subtitle}`: 代表 potplayer播放器中的当前字幕 83 | - `{subtitleOrigin}`: 代表 .str字幕文件中的 当前时间戳的字幕 84 | - `{subtitleTimeRange}`: 代表 .str字幕文件中的 开始和结束时间。例如: 01:02-03:04 85 | - `{subtitleTimeStart}`: 代表 .str字幕文件中的 开始时间。例如: 01:02 86 | - `{subtitleTimeEnd}`: 代表 .str字幕文件中的 结束时间。例如: 03:04 87 | 88 | **注意**: `srt转md`功能中的数据样式,受到 字幕模板 中的样式进行控制 89 | 90 | 91 | ### 示例1 92 | 93 | 我想要`Alt+G`是这个效果 94 | 95 | ![image-20231216234628300](./assets/image-20231216234628300.png) 96 | 97 | 此处应该这么填 98 | 99 | 1. 先确定**回链中的`[]`内的名称** 100 | 101 | ``` 102 | {name} | {time} 103 | ``` 104 | 105 | 2. 再确定**整个模板的数据** 106 | 107 | ```` 108 | ```Video 109 | title: {title} 110 | ``` 111 | ```` 112 | 113 | 最终效果 114 | 115 | ![image-20231229004839082](./assets/image-20231229004839082.png) 116 | 117 | ### 示例2 118 | 119 | 我想要`Ctrl+Alt+G`是这个效果 120 | 121 | ![image-20231216235029002](./assets/image-20231216235029002.png) 122 | 123 | 视频回链模板此处应该这么填 124 | 125 | ````ini 126 | ```video 127 | title:{title} 128 | image:{image} 129 | ``` 130 | ```` 131 | 132 | 133 | 134 | ### 示例3 135 | 136 | **思源笔记的换行**请使用html的`
`标签,参考:[#16](https://github.com/livelycode36/markdown2potplayer/issues/16) 137 | 138 | ```html 139 | {title}
140 | {image} 141 | 142 | ``` 143 | 144 | ### 示例4 145 | ![](./assets/Untitled-2024-06-13-0228.png) 146 | 147 | 148 | ## 播放B站视频 149 | 150 | 1. Potplayer需要提前安装插件:[chen310/BilibiliPotPlayer](https://github.com/chen310/BilibiliPotPlayer) 151 | 152 | 2. 按照插件的使用文档,在potplayer中播放视频 153 | 3. 使用快捷键打时间戳即可 154 | 155 | 156 | 157 | ## 字幕导航 158 | 159 | 需要在视频的同目录下, 同名的srt字幕文件, 例如视频名称为test.mp4 字幕文件为 test.srt 160 | 161 | - 【单次】使用srt字幕快速定位当前时间的上一句、当前句、下一句 162 | - 【循环】使用srt字幕快速定位当前时间的上一句、当前句、下一句 163 | 164 | 实际上potplayer也有这个功能 165 | 166 | - Home键: 上一对白 167 | - End键: 下一对白 168 | - Ctrl+Home: 当前字幕起点 169 | 170 | 171 | ## AB片段 172 | 173 | **使用**: 174 | 175 | 1. 首次,按快捷键记录起点 176 | 177 | 2. 再次,按快捷键记录终点,并生成回链,插入到笔记软件中 178 | 179 | 180 | 181 | **注意**: 182 | 183 | 1. 当起点**大于**终点时,例如:起点05:00,终点01:00,则`互换起点终点`,起点01:00,终点05:00 184 | 2. 当按下起点的快捷键,**想取消**,按`Esc`即可 185 | 186 | 187 | 188 | ### AB片段 189 | 190 | 播放**单次**,在起点播放,在终点暂停 191 | 192 | 播放之后,不想在终点暂停,按`Esc`即可取消终点暂停 193 | 194 | 195 | 196 | ### AB循环 197 | 198 | 使用Potplayer自带的"AB区段循环"实现,默认**无限播放** 199 | 200 | **关闭AB区段循环**:Potplayer**默认快捷键`\`** 201 | 202 | 203 | 204 | ## 视频文件的后缀名 205 | 206 | 控制名称中是否包含文件名的后缀 207 | 208 | ![image-20231216154341755](./assets/image-20231216154341755.png) 209 | 210 | 211 | 212 | ## 地址是否编码 213 | 214 | 控制视频地址,是否使用编码 215 | 216 | **关闭编码的效果** 217 | 218 | ![image-20231214195533699](./assets/image-20231214195533699.png) 219 | 220 | 注意 221 | 222 | - 目前发现的bug: 223 | 1. 全系urlencode的bug:如果路径中存在`\[`、`\!`会让,在【ob的预览模式】下(回链会被ob自动urlencode),`\[`中的`\`消失变为,`[`;例如:`G:\BaiduSyncdisk\123\[456]789.mp4` 在bug下变为:`G:\BaiduSyncdisk\123[456]789.mp4` <= `\[`丢失了`\`,所以即使关闭编码也会强制在`\[`出现在路径中,将`\[`中的`\`进行编码。如果不想被编码,请不要这样给视频文件命名 或 使用`-`、`_`等替代 224 | 2. 关闭编码之后,假如视频的路径中有`空格`,在obsidian的预览模式,回链`不会渲染为链接`,所以即使关闭编码也会强制将空格进行编码。如果不想空格也被编码,可以去掉文件中的空格 或 使用`-`、`_`等替代空格 225 | - 可能还有其他符号也有类似的问题,但暂未发现 226 | 227 | 228 | ## 为什么有时候跳转的时间不够准确? 229 | 因为在跳转时间之后, potplayer会根据关键帧, 对时间进行修正,所以会有误差。 230 | 在Potplayer的设置中关闭此项,即可解决。 231 | 232 | ![img](./assets/ba221f08ea6fa4f224889186c12606d7.png) 233 | 234 | 235 | ## 自定义跳转协议 236 | 237 | 适合自定义协议的人使用【谨慎】 238 | 239 | 修改的是此处 240 | 241 | ![image-20231214154748461](./assets/image-20231214154748461.png) 242 | 243 | 244 | 245 | ## 多国语言 246 | 247 | 1. 在这里有语言的代号:[LCID.ahk](./lib/gui/i18n/LCID.ahk) 248 | 2. 这是示例 249 | 1. [en-US.ini](./lib/gui/i18n/en-US.ini) 250 | 2. [zh-CN.ini](./lib/gui/i18n/zh-CN.ini) 251 | 252 | - **注意:ini文件请使用使用系统的默认 ANSI 编码!** 253 | - 参考:[IniRead| AutoHotkey v2](https://www.autohotkey.com/docs/v2/lib/IniRead.htm) 254 | 255 | 256 | 257 | # 开发 258 | 259 | 1. 克隆仓库 260 | 2. 下载安装:[AutoHotkey](https://www.autohotkey.com/)的v2版本 261 | 3. 打开AutoHotkey Dash,点击`Compile`按照提示安装`Ahk2Exe` 262 | 4. 使用`Ahk2Exe`编译,如下文件 263 | 1. 主程序:`markdown2potplayer.ahk` 264 | 2. 控制potplayer:`\lib\note2potplayer\note2potplayer.ahk` 265 | 3. word文档链接的形式:`\lib\word\word.ahk` 266 | 267 | 268 | 269 | # 鸣谢 270 | 271 | 感谢 272 | 273 | - [金](https://github.com/fireflysss) 274 | - [YIRU69](https://github.com/YIRU69) 275 | - 蚕子 276 | 277 | 给予的帮助与建议! -------------------------------------------------------------------------------- /assets/Untitled-2024-06-13-0228.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelycode36/markdown2potplayer/34dba23ba397ac893a3e0372dfa53fd8805eef4b/assets/Untitled-2024-06-13-0228.png -------------------------------------------------------------------------------- /assets/ba221f08ea6fa4f224889186c12606d7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelycode36/markdown2potplayer/34dba23ba397ac893a3e0372dfa53fd8805eef4b/assets/ba221f08ea6fa4f224889186c12606d7.png -------------------------------------------------------------------------------- /assets/image-20231214154748461.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelycode36/markdown2potplayer/34dba23ba397ac893a3e0372dfa53fd8805eef4b/assets/image-20231214154748461.png -------------------------------------------------------------------------------- /assets/image-20231214195533699.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelycode36/markdown2potplayer/34dba23ba397ac893a3e0372dfa53fd8805eef4b/assets/image-20231214195533699.png -------------------------------------------------------------------------------- /assets/image-20231216154341755.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelycode36/markdown2potplayer/34dba23ba397ac893a3e0372dfa53fd8805eef4b/assets/image-20231216154341755.png -------------------------------------------------------------------------------- /assets/image-20231216234628300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelycode36/markdown2potplayer/34dba23ba397ac893a3e0372dfa53fd8805eef4b/assets/image-20231216234628300.png -------------------------------------------------------------------------------- /assets/image-20231216235029002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelycode36/markdown2potplayer/34dba23ba397ac893a3e0372dfa53fd8805eef4b/assets/image-20231216235029002.png -------------------------------------------------------------------------------- /assets/image-20231229004156479.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelycode36/markdown2potplayer/34dba23ba397ac893a3e0372dfa53fd8805eef4b/assets/image-20231229004156479.png -------------------------------------------------------------------------------- /assets/image-20231229004839082.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelycode36/markdown2potplayer/34dba23ba397ac893a3e0372dfa53fd8805eef4b/assets/image-20231229004839082.png -------------------------------------------------------------------------------- /assets/image-20231229163509526.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelycode36/markdown2potplayer/34dba23ba397ac893a3e0372dfa53fd8805eef4b/assets/image-20231229163509526.png -------------------------------------------------------------------------------- /assets/image-20240126130251886.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelycode36/markdown2potplayer/34dba23ba397ac893a3e0372dfa53fd8805eef4b/assets/image-20240126130251886.png -------------------------------------------------------------------------------- /assets/image-20240210193023137.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelycode36/markdown2potplayer/34dba23ba397ac893a3e0372dfa53fd8805eef4b/assets/image-20240210193023137.png -------------------------------------------------------------------------------- /assets/image-20240217172403274.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelycode36/markdown2potplayer/34dba23ba397ac893a3e0372dfa53fd8805eef4b/assets/image-20240217172403274.png -------------------------------------------------------------------------------- /doc/README_EN.md: -------------------------------------------------------------------------------- 1 | # Usage Instructions 2 | 3 | ## 1. Preparation 4 | 5 | 1. Run the script file: `markdown2potplayer` 6 | 2. Double-click the tray icon in the bottom right corner ![image-20231229163509526](../assets/image-20231229163509526.png) 7 | 8 | ![image-20240126130251886](../assets/image-20240126130251886.png) 9 | 10 | **Modification 1**: Update the path to Potplayer's main program(potplayer 播放器的路径) to match your local path. 11 | 12 | **Modification 2**: Specify the name of your note-taking software(笔记软件的程序名称). 13 | 14 | - `Note`: The backlink will only be pasted into one note-taking software, following the order from top to bottom. 15 | - For example, if both Obsidian and Typora are configured: 16 | - Case 1: If both Obsidian and Typora are open, the backlink will only be pasted into Obsidian. 17 | - Case 2: If only Typora is open, the backlink will be pasted into Typora. 18 | 19 | ## 2. Usage 20 | 21 | 1. Open `markdown2potplayer`. 22 | 2. Open Obsidian. 23 | 3. Open Potplayer. 24 | 25 | 4. When the note-taking software or Potplayer window is active, press the hotkey Alt+G (default) to automatically paste the **video backlink** (视频回链) into Obsidian. 26 | 5. When the note-taking software or Potplayer window is active, press the hotkey Ctrl+Alt+G (default) to automatically paste the **image + video backlink** into Obsidian. 27 | 28 | # Advanced Settings 29 | 30 | ## About Notion 31 | 32 | 1. Notion runs in a browser, and there are many browsers available. 33 | 2. Currently supported browsers include: 34 | - Microsoft Edge: `msedge.exe` 35 | - Google Chrome: `chrome.exe` 36 | - 360 Speed Browser: `360chrome.exe` 37 | - Firefox: `firefox.exe` 38 | 39 | 3. **Please click on the link in Notion with the left mouse button, do not use the open in a new tab option, such as Ctrl + left mouse button or middle mouse button.** 40 | 41 | ![image-20240217172403274](../assets/image-20240217172403274.png) 42 | 43 | ## Modifying Templates 44 | 45 | ![image-20231229004156479](../assets/image-20231229004156479.png) 46 | 47 | There are `5` template items for **pasting templates**. 48 | 49 | **Note**: Not all positions can use these four items. 50 | 51 | - Subtitle Template: Only `{subtitle}` can be used. 52 | - Backlink Name: Only `{name}` and `{time}` can be used. 53 | - Backlink Template: Only `{title}` can be used. 54 | - Video Backlink Template: Only `{image}` and `{title}` can be used. 55 | 56 | Explanation: 57 | 58 | - `{name}`: Represents the video file name, i.e., `[Video **Name**]`. 59 | - `{time}`: Represents the current playback time of the video, i.e., `[Video **Time**]`. 60 | - `{title}`: **Represents the entire Markdown format link**, e.g., `[google](https://www.google.com)`. This means that this is the Markdown format backlink for Potplayer. 61 | - `{image}`: Represents the **position for pasting the image**. 62 | - `{subtitle}`: represents the subtitles that can be copied from the video currently being played in PotPlayer. 63 | - `{subtitleTemplate}`: represents the subtitle template. **If there are no subtitles in the currently playing video, no data will be generated for the subtitle template**, meaning if there are no subtitles, `{subtitleTemplate}` will disappear. 64 | 65 | Subtitle Template Variables: 66 | - `{time}`: Current timestamp in PotPlayer 67 | - `{subtitle}`: Currently displayed subtitle in PotPlayer 68 | - `{subtitleOrigin}`: Original subtitle text at current timestamp from .srt file 69 | - `{subtitleTimeRange}`: Time range in .srt file (format: HH:MM:SS-HH:MM:SS) 70 | - `{subtitleTimeStart}`: Start time in .srt file (e.g. 00:01:02) 71 | - `{subtitleTimeEnd}`: End time in .srt file (e.g. 00:03:04) 72 | 73 | **Note**: The data styling in `SRT-to-Markdown` conversion is controlled by these template variables. 74 | 75 | 76 | ### Example 1 77 | 78 | I want the effect of `Alt+G` to be this: 79 | 80 | ![image-20231216234628300](../assets/image-20231216234628300.png) 81 | 82 | This should be filled in as follows: 83 | 84 | 1. First, determine the **name inside the `[]` of the backlink**: 85 | 86 | ``` 87 | {name} | {time} 88 | ``` 89 | 90 | 2. Then, determine the **data for the entire template**: 91 | 92 | ```` 93 | ```Video 94 | title: {title} 95 | ``` 96 | ```` 97 | 98 | Final effect: 99 | 100 | ![image-20231229004839082](../assets/image-20231229004839082.png) 101 | 102 | ### Example 2 103 | 104 | I want the effect of `Ctrl+Alt+G` to be this: 105 | 106 | ![image-20231216235029002](../assets/image-20231216235029002.png) 107 | 108 | The video backlink template should be filled in as follows: 109 | 110 | ````ini 111 | ```video 112 | title:{title} 113 | image:{image} 114 | ``` 115 | ```` 116 | 117 | ## Playing Bilibili Videos 118 | 119 | 1. Potplayer needs to have the plugin installed: [chen310/BilibiliPotPlayer](https://github.com/chen310/BilibiliPotPlayer). 120 | 2. Follow the plugin's usage documentation to play videos in Potplayer. 121 | 3. Use the hotkey to mark timestamps. 122 | 123 | 124 | ## Subtitle Navigation 125 | 126 | Requirements: 127 | - An SRT file with identical name as the video must exist in the same directory 128 | (e.g. `test.mp4` requires `test.srt`) 129 | 130 | Navigation Modes: 131 | - **Single-play**: Jump to previous/current/next subtitle line based on current timestamp 132 | - **Loop-mode**: Continuously cycle through previous/current/next subtitle lines 133 | 134 | Native PotPlayer Shortcuts: 135 | - `Home`: Previous dialogue line 136 | - `End`: Next dialogue line 137 | - `Ctrl+Home`: Jump to start timestamp of current subtitle 138 | 139 | 140 | ## AB Segments 141 | 142 | **Usage**: 143 | 144 | 1. Press the hotkey once to record the start point. 145 | 2. Press the hotkey again to record the end point and generate the backlink, which is then inserted into the note-taking software. 146 | 147 | **Note 148 | 149 | **: 150 | 151 | 1. When the start point is **greater than** the end point, e.g., start point 05:00, end point 01:00, the start and end points will be **swapped**, so the start point becomes 01:00 and the end point 05:00. 152 | 2. If you press the hotkey for the start point and **want to cancel**, press `Esc`. 153 | 154 | ### AB Segment 155 | 156 | Play **once**, start playing at the start point, and pause at the end point. 157 | 158 | After playing, if you don't want to pause at the end point, press `Esc` to cancel the end point pause. 159 | 160 | ### AB Loop 161 | 162 | Use Potplayer's built-in "AB Section Loop" for **infinite playback**. 163 | 164 | **To close the AB section loop**: Use Potplayer's **default hotkey `\`**. 165 | 166 | ## Video File Extensions 167 | 168 | Control whether the file name includes the file extension. 169 | 170 | ![image-20231216154341755](../assets/image-20231216154341755.png) 171 | 172 | ## URL Encoding 173 | 174 | Control whether the video URL is encoded. 175 | 176 | **Effect of disabling encoding**: 177 | 178 | ![image-20231214195533699](../assets/image-20231214195533699.png) 179 | 180 | Note: 181 | 182 | - Known bugs: 183 | - Full URL encoding bug: If the path contains `\[` or `\!`, in **Obsidian's preview mode** (where the backlink is automatically URL-encoded by Obsidian), `\[` will lose the `\` and become `[`. For example: `G:\BaiduSyncdisk\123\[456]789.mp4` becomes `G:\BaiduSyncdisk\123[456]789.mp4` under the bug, so even if encoding is disabled, `\[` in the path will be forcibly encoded. To avoid this, please do not name your video files this way, or use `-`, `_`, etc., as alternatives. 184 | - After disabling encoding, if the video path contains `spaces`, the backlink will **not render as a link** in Obsidian's preview mode, so spaces will also be forcibly encoded. To avoid this, you can remove spaces from the file name or use `-`, `_`, etc., as alternatives. 185 | - There may be other symbols with similar issues, but none have been discovered so far. 186 | 187 | ## Why isn't the seek time always precise? 188 | This happens because Potplayer adjusts the timing based on keyframes after seeking, which may cause slight inaccuracies. 189 | You can fix this by disabling this feature in Potplayer's settings. 190 | 191 | ![img](../assets/ba221f08ea6fa4f224889186c12606d7.png) 192 | 193 | 194 | ## Custom Protocol 195 | 196 | For those who use custom protocols (use with caution): 197 | 198 | Modify here: 199 | 200 | ![image-20231214154748461](../assets/image-20231214154748461.png) 201 | 202 | ## Multilingual Support 203 | 204 | 1. Language codes can be found here: [LCID.ahk](../lib/gui/i18n/LCID.ahk). 205 | 2. Examples: 206 | - [en-US.ini](../lib/gui/i18n/en-US.ini) 207 | - [zh-CN.ini](../lib/gui/i18n/zh-CN.ini) 208 | 209 | - **Note: Please use the system's default ANSI encoding for the ini files!** 210 | - Reference: [IniRead | AutoHotkey v2](https://www.autohotkey.com/docs/v2/lib/IniRead.htm) 211 | 212 | 213 | 214 | # Development 215 | 216 | 1. Clone the repository. 217 | 2. Download and install the v2 version of [AutoHotkey](https://www.autohotkey.com/). 218 | 3. Open AutoHotkey Dash, click on `Compile`, and follow the prompts to install `Ahk2Exe`. 219 | 4. Compile using `Ahk2Exe` with the following files: 220 | 1. Main program: `markdown2potplayer.ahk` 221 | 2. PotPlayer control: `\lib\note2potplayer\note2potplayer.ahk` 222 | 3. Word document link format: `\lib\word\word.ahk` 223 | 224 | 225 | 226 | # Acknowledgments 227 | 228 | Special thanks to: 229 | 230 | - [Jin](https://github.com/fireflysss) 231 | - [YIRU69](https://github.com/YIRU69) 232 | - Silkworm 233 | 234 | for their help and suggestions! -------------------------------------------------------------------------------- /lib/BootUp.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2.0 2 | 3 | key := "markdown2potplayer" 4 | value := "`"" A_ScriptDir "\" key ".exe`"" 5 | 6 | set_boot_up(){ 7 | RegWrite value, "REG_SZ", "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run", key 8 | } 9 | 10 | get_boot_up(){ 11 | try 12 | regValue := RegRead("HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run", key) 13 | catch as OSError ; if the key doesn't exist 14 | return false 15 | 16 | if (regValue == value) 17 | return true 18 | else 19 | return false 20 | } 21 | 22 | remove_boot_up(){ 23 | try 24 | RegDelete("HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run", key) 25 | catch as OSError ; if the key doesn't exist 26 | return false 27 | } 28 | 29 | adaptive_bootup(){ 30 | if get_boot_up(){ 31 | remove_boot_up() 32 | MsgBox "Startup launch: OFF" 33 | } 34 | else{ 35 | set_boot_up() 36 | MsgBox "Startup launch: ON" 37 | } 38 | } -------------------------------------------------------------------------------- /lib/CharsetDetect.ahk: -------------------------------------------------------------------------------- 1 | class TextEncodingDetect { 2 | UTF8Bom := [0xEF, 0xBB, 0xBF] 3 | UTF16LeBom := [0xFF, 0xFE] 4 | UTF16BeBom := [0xFE, 0xFF] 5 | UTF32LeBom := [0xFF, 0xFE, 0x00, 0x00] 6 | 7 | DetectEncoding(fileName) { 8 | if !FileExist(fileName) { 9 | MsgBox("File not found") 10 | return "" 11 | } 12 | 13 | buffer := FileRead(fileName, "RAW") 14 | size := buffer.Size 15 | 16 | encodingType := this.DetectWithBom(buffer) 17 | if (encodingType == "Utf8Bom") { 18 | encodingType := "UTF-8" 19 | } else if (encodingType == "UnicodeBom") { 20 | encodingType := "UTF-16" 21 | } 22 | 23 | if encodingType != "None" { 24 | return encodingType 25 | } 26 | 27 | encodingType := this.DetectWithoutBom(buffer, size) 28 | if (encodingType == "GBK" || 29 | encodingType == "Ansi") { 30 | ; CP0 是 系统默认编码 31 | encodingType := "CP0" 32 | } 33 | 34 | return encodingType != "None" ? encodingType : "None" 35 | } 36 | 37 | DetectWithBom(buffer) { 38 | if (buffer.Size >= 3 && this.ByteAt(buffer, 0) = this.UTF8Bom[1] && this.ByteAt(buffer, 1) = this.UTF8Bom[2] && this.ByteAt(buffer, 2) = this.UTF8Bom[3]) { 39 | return "Utf8Bom" 40 | } 41 | 42 | if (buffer.Size >= 2 && this.ByteAt(buffer, 0) = this.UTF16LeBom[1] && this.ByteAt(buffer, 1) = this.UTF16LeBom[2]) { 43 | return "UnicodeBom" 44 | } 45 | 46 | if (buffer.Size >= 2 && this.ByteAt(buffer, 0) = this.UTF16BeBom[1] && this.ByteAt(buffer, 1) = this.UTF16BeBom[2]) { 47 | if (buffer.Size >= 4 && this.ByteAt(buffer, 2) = this.UTF32LeBom[3] && this.ByteAt(buffer, 3) = this.UTF32LeBom[4]) { 48 | return "Utf32Bom" 49 | } 50 | return "BigEndianUnicodeBom" 51 | } 52 | 53 | return "None" 54 | } 55 | 56 | DetectWithoutBom(buffer, size) { 57 | ; Check for UTF-8 encoding 58 | encoding := this.CheckUtf8(buffer, size) 59 | if encoding != "None" { 60 | return encoding 61 | } 62 | 63 | ; Check for ANSI encoding 64 | if !this.ContainsZero(buffer, size) { 65 | return this.CheckChinese(buffer, size) ? "GBK" : "Ansi" 66 | } 67 | 68 | return "None" 69 | } 70 | 71 | CheckUtf8(buffer, size) { 72 | pos := 0 73 | while pos < size { 74 | ch := this.ByteAt(buffer, pos) 75 | pos++ 76 | 77 | if ch < 0x80 { 78 | continue 79 | } 80 | 81 | if ch >= 0xC2 && ch <= 0xDF { 82 | if !this.IsContinuationByte(this.ByteAt(buffer, pos)) { 83 | return "None" 84 | } 85 | pos++ 86 | continue 87 | } 88 | 89 | if ch >= 0xE0 && ch <= 0xEF { 90 | if !this.IsContinuationByte(this.ByteAt(buffer, pos)) || !this.IsContinuationByte(this.ByteAt(buffer, pos + 1)) { 91 | return "None" 92 | } 93 | pos += 2 94 | continue 95 | } 96 | 97 | if ch >= 0xF0 && ch <= 0xF4 { 98 | if !this.IsContinuationByte(this.ByteAt(buffer, pos)) || !this.IsContinuationByte(this.ByteAt(buffer, pos + 1)) || !this.IsContinuationByte(this.ByteAt(buffer, pos + 2)) { 99 | return "None" 100 | } 101 | pos += 3 102 | continue 103 | } 104 | 105 | return "None" 106 | } 107 | ; return "Utf8Nobom" 108 | return "UTF-8" 109 | } 110 | 111 | IsContinuationByte(byte) { 112 | return byte >= 0x80 && byte <= 0xBF 113 | } 114 | 115 | ContainsZero(buffer, size) { 116 | pos := 0 117 | while pos < size { 118 | if this.ByteAt(buffer, pos) = 0 { 119 | return true 120 | } 121 | pos++ 122 | } 123 | return false 124 | } 125 | 126 | CheckChinese(buffer, size) { 127 | pos := 0 128 | while pos < size - 1 { 129 | ch1 := this.ByteAt(buffer, pos) 130 | ch2 := this.ByteAt(buffer, pos + 1) 131 | pos += 2 132 | 133 | if (ch1 >= 176 && ch1 <= 247 && ch2 >= 160 && ch2 <= 254) ; GB2312 134 | || (ch1 >= 129 && ch1 <= 254 && ch2 >= 64 && ch2 <= 254) ; GBK 135 | || (ch1 >= 129 && ((ch2 >= 64 && ch2 <= 126) || (ch2 >= 161 && ch2 <= 254))) { ; Big5 136 | return true 137 | } 138 | } 139 | return false 140 | } 141 | 142 | ByteAt(buffer, index) { 143 | return NumGet(buffer, index, "UChar") 144 | } 145 | } 146 | 147 | ; 使用示例: 148 | ; detect := TextEncodingDetect() 149 | ; encoding := detect.DetectEncoding("C:\Users\Thunder\Downloads\Compressed\02 音名与钢琴键盘.srt") 150 | ; MsgBox "Detected Encoding: " encoding 151 | -------------------------------------------------------------------------------- /lib/JSON.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2.0 2 | 3 | ; From: https: // github.com / dcazrael / autohotkey_libraries 4 | class JSON { 5 | ; Example =================================================================================== 6 | ; =========================================================================================== 7 | 8 | ; Msgbox "The idea here is to create several nested arrays, save to text with jxon_dump(), and then reload the array with jxon_load(). The resulting array should be the same.`r`n`r`nThis is what this example shows." 9 | ; a := Map(), b := Map(), c := Map(), d := Map(), e := Map(), f := Map() ; Object() is more technically correct than {} but both will work. 10 | 11 | ; d["g"] := 1, d["h"] := 2, d["i"] := ["purple","pink","pippy red"] 12 | ; e["g"] := 1, e["h"] := 2, e["i"] := Map("1","test1","2","test2","3","test3") 13 | ; f["g"] := 1, f["h"] := 2, f["i"] := [1,2,Map("a",1.0009,"b",2.0003,"c",3.0001)] 14 | 15 | ; a["test1"] := "test11", a["d"] := d 16 | ; b["test3"] := "test33", b["e"] := e 17 | ; c["test5"] := "test55", c["f"] := f 18 | 19 | ; myObj := Map() 20 | ; myObj["a"] := a, myObj["b"] := b, myObj["c"] := c, myObj["test7"] := "test77", myObj["test8"] := "test88" 21 | 22 | ; g := ["blue","green","red"], myObj["h"] := g ; add linear array for testing 23 | 24 | ; q := Chr(34) 25 | ; textData2 := Jxon_dump(myObj,4) ; ===> convert array to JSON 26 | ; msgbox "JSON output text:`r`n===========================================`r`n(Should match second output.)`r`n`r`n" textData2 27 | 28 | ; newObj := Jxon_load(&textData2) ; ===> convert json back to array 29 | 30 | ; textData3 := Jxon_dump(newObj,4) ; ===> break down array into 2D layout again, should be identical 31 | ; msgbox "Second output text:`r`n===========================================`r`n(should be identical to first output)`r`n`r`n" textData3 32 | 33 | ; msgbox "textData2 = textData3: " ((textData2=textData3) ? "true" : "false") 34 | 35 | ; =========================================================================================== 36 | ; End Example ; ============================================================================= 37 | ; =========================================================================================== 38 | 39 | ; originally posted by user coco on AutoHotkey.com 40 | ; https://github.com/cocobelgica/AutoHotkey-JSON 41 | 42 | static Load(&src, args*) { 43 | key := "", is_key := false 44 | stack := [ tree := [] ] 45 | next := '"{[01234567890-tfn' 46 | pos := 0 47 | 48 | while ( (ch := SubStr(src, ++pos, 1)) != "" ) { 49 | if InStr(" `t`n`r", ch) 50 | continue 51 | if !InStr(next, ch, true) { 52 | testArr := StrSplit(SubStr(src, 1, pos), "`n") 53 | 54 | ln := testArr.Length 55 | col := pos - InStr(src, "`n",, -(StrLen(src)-pos+1)) 56 | 57 | msg := Format("{}: line {} col {} (char {})" 58 | , (next == "") ? ["Extra data", ch := SubStr(src, pos)][1] 59 | : (next == "'") ? "Unterminated string starting at" 60 | : (next == "\") ? "Invalid \escape" 61 | : (next == ":") ? "Expecting ':' delimiter" 62 | : (next == '"') ? "Expecting object key enclosed in double quotes" 63 | : (next == '"}') ? "Expecting object key enclosed in double quotes or object closing '}'" 64 | : (next == ",}") ? "Expecting ',' delimiter or object closing '}'" 65 | : (next == ",]") ? "Expecting ',' delimiter or array closing ']'" 66 | : [ "Expecting JSON value(string, number, [true, false, null], object or array)" 67 | , ch := SubStr(src, pos, (SubStr(src, pos)~="[\]\},\s]|$")-1) ][1] 68 | , ln, col, pos) 69 | 70 | throw Error(msg, -1, ch) 71 | } 72 | 73 | obj := stack[1] 74 | is_array := (obj is Array) 75 | 76 | if i := InStr("{[", ch) { ; start new object / map? 77 | val := (i = 1) ? Map() : Array() ; ahk v2 78 | 79 | is_array ? obj.Push(val) : obj[key] := val 80 | stack.InsertAt(1,val) 81 | 82 | next := '"' ((is_key := (ch == "{")) ? "}" : "{[]0123456789-tfn") 83 | } else if InStr("}]", ch) { 84 | stack.RemoveAt(1) 85 | next := (stack[1]==tree) ? "" : (stack[1] is Array) ? ",]" : ",}" 86 | } else if InStr(",:", ch) { 87 | is_key := (!is_array && ch == ",") 88 | next := is_key ? '"' : '"{[0123456789-tfn' 89 | } else { ; string | number | true | false | null 90 | if (ch == '"') { ; string 91 | i := pos 92 | while i := InStr(src, '"',, i+1) { 93 | val := StrReplace(SubStr(src, pos+1, i-pos-1), "\\", "\u005C") 94 | if (SubStr(val, -1) != "\") 95 | break 96 | } 97 | if !i ? (pos--, next := "'") : 0 98 | continue 99 | 100 | pos := i ; update pos 101 | 102 | val := StrReplace(val, "\/", "/") 103 | val := StrReplace(val, '\"', '"') 104 | , val := StrReplace(val, "\b", "`b") 105 | , val := StrReplace(val, "\f", "`f") 106 | , val := StrReplace(val, "\n", "`n") 107 | , val := StrReplace(val, "\r", "`r") 108 | , val := StrReplace(val, "\t", "`t") 109 | 110 | i := 0 111 | while i := InStr(val, "\",, i+1) { 112 | if (SubStr(val, i+1, 1) != "u") ? (pos -= StrLen(SubStr(val, i)), next := "\") : 0 113 | continue 2 114 | 115 | xxxx := Abs("0x" . SubStr(val, i+2, 4)) ; \uXXXX - JSON unicode escape sequence 116 | if (xxxx < 0x100) 117 | val := SubStr(val, 1, i-1) . Chr(xxxx) . SubStr(val, i+6) 118 | } 119 | 120 | if is_key { 121 | key := val, next := ":" 122 | continue 123 | } 124 | } else { ; number | true | false | null 125 | val := SubStr(src, pos, i := RegExMatch(src, "[\]\},\s]|$",, pos)-pos) 126 | 127 | if IsInteger(val) 128 | val += 0 129 | else if IsFloat(val) 130 | val += 0 131 | else if (val == "true" || val == "false") 132 | val := (val == "true") 133 | else if (val == "null") 134 | val := "" 135 | else if is_key { 136 | pos--, next := "#" 137 | continue 138 | } 139 | 140 | pos += i-1 141 | } 142 | 143 | is_array ? obj.Push(val) : obj[key] := val 144 | next := obj == tree ? "" : is_array ? ",]" : ",}" 145 | } 146 | } 147 | 148 | return tree[1] 149 | } 150 | 151 | static Dump(obj, indent:="", lvl:=1) { 152 | if IsObject(obj) { 153 | If !(obj is Array || obj is Map || obj is String || obj is Number) 154 | throw Error("Object type not supported.", -1, Format("", ObjPtr(obj))) 155 | 156 | if IsInteger(indent) 157 | { 158 | if (indent < 0) 159 | throw Error("Indent parameter must be a postive integer.", -1, indent) 160 | spaces := indent, indent := "" 161 | 162 | Loop spaces ; ===> changed 163 | indent .= " " 164 | } 165 | indt := "" 166 | 167 | Loop indent ? lvl : 0 168 | indt .= indent 169 | 170 | is_array := (obj is Array) 171 | 172 | lvl += 1, out := "" ; Make #Warn happy 173 | for k, v in obj { 174 | if IsObject(k) || (k == "") 175 | throw Error("Invalid object key.", -1, k ? Format("", ObjPtr(obj)) : "") 176 | 177 | if !is_array ;// key ; ObjGetCapacity([k], 1) 178 | out .= (ObjGetCapacity([k]) ? JSON.Dump(k) : JSON.escape_str(k)) (indent ? ": " : ":") ; token + padding 179 | 180 | out .= JSON.Dump(v, indent, lvl) ; value 181 | . ( indent ? ",`n" . indt : "," ) ; token + indent 182 | } 183 | 184 | if (out != "") { 185 | out := Trim(out, ",`n" . indent) 186 | if (indent != "") 187 | out := "`n" . indt . out . "`n" . SubStr(indt, StrLen(indent)+1) 188 | } 189 | 190 | return is_array ? "[" . out . "]" : "{" . out . "}" 191 | 192 | } Else If (obj is Number) 193 | return obj 194 | 195 | Else ; String 196 | return escape_str(obj) 197 | 198 | static escape_str(obj) { 199 | obj := StrReplace(obj,"\","\\") 200 | obj := StrReplace(obj,"`t","\t") 201 | obj := StrReplace(obj,"`r","\r") 202 | obj := StrReplace(obj,"`n","\n") 203 | obj := StrReplace(obj,"`b","\b") 204 | obj := StrReplace(obj,"`f","\f") 205 | obj := StrReplace(obj,"/","\/") 206 | obj := StrReplace(obj,'"','\"') 207 | 208 | return '"' obj '"' 209 | } 210 | } 211 | } -------------------------------------------------------------------------------- /lib/MyTool.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2.0 2 | #Include ./NetRequest.ahk 3 | 4 | log_toggle := false 5 | 6 | ; 获取路径中的文件名带扩展名 7 | GetNameForPath(program_path) { 8 | SplitPath program_path, &name, &dir, &ext, &name_no_ext, &drive 9 | return name 10 | } 11 | 12 | GetNameForPathWithoutExt(program_path) { 13 | SplitPath program_path, &name, &dir, &ext, &name_no_ext, &drive 14 | return dir "\" name_no_ext 15 | } 16 | 17 | SearchProgram(target_app_path) { 18 | ; 程序正在运行 19 | if (WinExist("ahk_exe " GetNameForPath(target_app_path))) { 20 | return true 21 | } else { 22 | return false 23 | } 24 | } 25 | 26 | SelectedNoteProgram(note_app_names) { 27 | Loop Parse note_app_names, "`n" { 28 | note_program := A_LoopField 29 | if (WinExist("ahk_exe " note_program)) { 30 | return note_program 31 | } 32 | } 33 | MsgBox "note software not found" 34 | Exit 35 | } 36 | 37 | ActivateProgram(process_name) { 38 | if WinActive("ahk_exe " process_name) { 39 | return 40 | } 41 | 42 | if (WinExist("ahk_exe " process_name)) { 43 | WinActivate ("ahk_exe " process_name) 44 | Sleep 300 ; 给程序切换窗口的时间 45 | } else { 46 | MsgBox process_name " is not running" 47 | Exit 48 | } 49 | } 50 | 51 | IsPotplayerRunning(media_player_path) { 52 | if SearchProgram(media_player_path) 53 | return true 54 | else 55 | return false 56 | } 57 | 58 | ; 获取Potplayer的标题 59 | ; 获取失败的原因:因为Potplayer的exe可能存在多个线程,而WinGetTitle其中一个线程的标题,结果可能为空字符串 60 | ; 参考:https://stackoverflow.com/questions/54570212/why-is-my-call-to-wingettitle-returning-an-empty-string 61 | GetPotplayerTitle(potplayer_process_name) { 62 | ids := WinGetList("ahk_exe " potplayer_process_name) 63 | 64 | title := "" 65 | for id in ids { 66 | try { 67 | title := WinGetTitle("ahk_id " id) 68 | } catch TargetError as err { 69 | ; 目标窗口未找到 70 | continue 71 | } 72 | if title == "" 73 | continue 74 | else 75 | return title 76 | } 77 | Assert(title == "", "Error: Maybe Potplayer isn't running!") 78 | } 79 | 80 | MyLog(message) { 81 | if log_toggle 82 | MsgBox message 83 | } 84 | 85 | ; 安全的递归 86 | global running_count := 0 87 | SafeRecursion() { 88 | global running_count 89 | running_count++ 90 | ToolTip("Retrying... Attempt: " running_count) 91 | SetTimer () => ToolTip(), -1000 92 | if (running_count > 5) { 93 | running_count := 0 94 | MsgBox "error: failed!" 95 | Exit 96 | } 97 | } 98 | 99 | ; 等待释放指定按键 100 | ReleaseKeyboard(keyName) { 101 | if GetKeyState(keyName) { 102 | if KeyWait(keyName, "T2") == 0 { 103 | SafeRecursion() 104 | ReleaseKeyboard(keyName) 105 | } 106 | } 107 | running_count := 0 108 | } 109 | 110 | ReleaseCommonUseKeyboard() { 111 | ReleaseKeyboard("Control") 112 | ReleaseKeyboard("Shift") 113 | ReleaseKeyboard("Alt") 114 | } 115 | 116 | UrlEncode(str, sExcepts := "-_.", enc := "UTF-8") { 117 | hex := "00", func := "msvcrt\swprintf" 118 | buff := Buffer(StrPut(str, enc)), StrPut(str, buff, enc) ;转码 119 | encoded := "" 120 | Loop { 121 | if (!b := NumGet(buff, A_Index - 1, "UChar")) 122 | break 123 | ch := Chr(b) 124 | ; "is alnum" is not used because it is locale dependent. 125 | if (b >= 0x41 && b <= 0x5A ; A-Z 126 | || b >= 0x61 && b <= 0x7A ; a-z 127 | || b >= 0x30 && b <= 0x39 ; 0-9 128 | || InStr(sExcepts, Chr(b), true)) 129 | encoded .= Chr(b) 130 | else { 131 | DllCall(func, "Str", hex, "Str", "%%%02X", "UChar", b, "Cdecl") 132 | encoded .= hex 133 | } 134 | } 135 | return encoded 136 | } 137 | 138 | UrlDecode(Url, Enc := "UTF-8") { 139 | Pos := 1 140 | Loop { 141 | Pos := RegExMatch(Url, "i)(?:%[\da-f]{2})+", &code, Pos++) 142 | If (Pos = 0) 143 | Break 144 | code := code[0] 145 | var := Buffer(StrLen(code) // 3, 0) 146 | code := SubStr(code, 2) 147 | loop Parse code, "`%" 148 | NumPut("UChar", Integer("0x" . A_LoopField), var, A_Index - 1) 149 | Url := StrReplace(Url, "`%" code, StrGet(var, Enc)) 150 | } 151 | Return Url 152 | } 153 | 154 | Assert(condition, exception_message) { 155 | if (condition) { 156 | MsgBox exception_message 157 | Exit 158 | } 159 | } 160 | 161 | getLatestVersionFromGithub() { 162 | try { 163 | myRequest := NetRequest() 164 | 165 | myRequest.apiSetup("https://api.github.com", "repos/livelycode36/markdown2potplayer/releases/latest") 166 | 167 | response := myRequest.apiRequest("GET") 168 | 169 | latest_tag := "N/A" 170 | if IsObject(response) { 171 | ; Check if tag_name exists 172 | if response.Has("tag_name") { 173 | latest_tag := response["tag_name"] 174 | return latest_tag 175 | } else { 176 | return latest_tag 177 | } 178 | } else { 179 | ; MsgBox("API Request Failed. Unknown error.", "Error", 16) 180 | return latest_tag 181 | } 182 | } catch { 183 | ; MsgBox("API Request Failed. Unknown error.", "Error", 16) 184 | return latest_tag 185 | } 186 | } -------------------------------------------------------------------------------- /lib/NetRequest.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2.0 2 | #Include "./JSON.ahk" 3 | 4 | /** 5 | * From: https://github.com/dcazrael/autohotkey_libraries 6 | * Allows us to requests data from the web. 7 | * You can use either WinHttpRequests or create a com instance of a browser 8 | * 9 | * @Parameters 10 | * url - [String] expects URL 11 | * 12 | * @Remarks 13 | * For better output control JSON.ahk has been included. 14 | * This is by no means necessary, but it's easier dealing with 15 | * JSON objects, than working with the string responses. 16 | */ 17 | Class NetRequest { 18 | static request_header := Map("GET", "application/json", "POST", "application/x-www-form-urlencoded;charset=utf-8") 19 | static method := Map("GET", "GET", "POST", "POST") 20 | status := "instantiated" 21 | 22 | __New() { 23 | } 24 | 25 | /** 26 | * Sets the host and endpoint for the API you want to use. 27 | * 28 | * @Parameters 29 | * host - [String] expects valid URL 30 | * endpoint - [String] valid endpoint for API you want to hit 31 | * e.g., /rest/asset/v1/programs.json 32 | * 33 | * @Return 34 | * Nothing 35 | */ 36 | apiSetup(host, endpoint) { 37 | this.host := host 38 | this.endpoint := endpoint 39 | } 40 | 41 | /** 42 | * Used for requesting data from APIs. 43 | * Returns the response text from the HTTP request. 44 | * 45 | * @Parameters 46 | * method - [String] "GET" or "POST" (default: "GET") 47 | * body - [String] Data for POST requests (optional for GET) 48 | * params - [Array] Parameters for the API call. 49 | * Strings without "=" are treated as path segments (e.g., "id"). 50 | * Strings with "=" are treated as query parameters (e.g., "name=Alf"). 51 | * 52 | * @Return 53 | * [Object] JSON Object or [String] error message or [Boolean] false on failure. 54 | */ 55 | apiRequest(method := "GET", body := "", params*) { 56 | local parameters := "" 57 | local start_query := true 58 | if params.Length > 0 { 59 | for parm in params { 60 | if (!InStr(parm, "=")) { 61 | parameters .= "/" . parm 62 | } else { 63 | parameters .= (start_query ? "?" : "&") . parm 64 | start_query := false 65 | } 66 | } 67 | } 68 | 69 | request := this.createRequestObj(this.host . "/" . this.endpoint . parameters, method, body) 70 | if (!IsObject(request)) { ; Could be an error string or false 71 | return request 72 | } 73 | 74 | try { 75 | ; The following line calls JSON.Load(). If you see a warning like "This local variable appears to never be assigned a value. Specifically: JSON", 76 | ; it likely means that the JSON.ahk library is not v2 compatible, or does not correctly define a class named 'JSON' with a static 'Load' method. 77 | ; The #Include "./JSON.ahk" directive at the top of this script is supposed to make the JSON class available. 78 | ; If JSON.ahk is correct and v2 compatible, this warning might be a linter/parser quirk. 79 | ; If the script fails at runtime due to 'JSON' being an unknown variable, the issue lies with JSON.ahk. 80 | responseText := request.ResponseText 81 | json_response := JSON.Load(&responseText) 82 | return json_response 83 | } Catch Error as e { 84 | ; Msgbox("JSON Parsing Error: " e.Message, "Error", 16) 85 | return "JSON Parsing Error: " e.Message 86 | } 87 | } 88 | 89 | /** 90 | * Checks status of URL by returning status code. 91 | * 92 | * @Parameters 93 | * URL - [String] expects valid URL 94 | * 95 | * @Return 96 | * [String] HTTP status code or error message. 97 | */ 98 | checkStatus(URL) { 99 | request := this.createRequestObj(URL) 100 | if IsObject(request) 101 | return request.status 102 | return request ; Error message 103 | } 104 | 105 | /** 106 | * Opens Internet Explorer and navigates to URL. 107 | * Can be visible or hidden. 108 | * Returns COM Object to interact with. 109 | * 110 | * @Parameters 111 | * URL - [String] expects valid URL 112 | * visibility - [Boolean] expects true or false (default: false) 113 | * 114 | * @Return 115 | * [Object] COM Object for Internet Explorer. 116 | */ 117 | webBrowser(URL, visibility := false) { 118 | wb := ComObject("InternetExplorer.Application") 119 | wb.Visible := visibility 120 | wb.Navigate(URL) 121 | while wb.Busy || wb.ReadyState != 4 { 122 | Sleep(100) 123 | } 124 | return wb 125 | } 126 | 127 | /** 128 | * Creates a COM object used for HTTP requests. 129 | * Can be used with both "GET" and "POST". 130 | * 131 | * @Parameters 132 | * endpoint - [String] expects valid URL or endpoint 133 | * method - [String] expects "GET" or "POST" (default: "GET") 134 | * body - [String] values we want to pass via POST (optional for GET) 135 | * timeouts - [Map] expects timeouts passed in key:value pairs, e.g.: 136 | * Map("resolve_timeout", 0, "connect_timeout", 30000, 137 | * "send_timeout", 30000, "receive_timeout", 60000) 138 | * 139 | * @Return 140 | * [Object] WinHttpRequest COM object or [String] error message or [Boolean] false. 141 | */ 142 | createRequestObj(endpoint := "", method := "GET", body := "", timeouts := "") { 143 | local request_obj 144 | try { 145 | request_obj := ComObject("WinHttp.WinHttpRequest.5.1") 146 | 147 | ; Attempt to set system proxy 148 | ; Constants for SetProxy 149 | WINHTTP_ACCESS_TYPE_DEFAULT_PROXY := 0 150 | WINHTTP_ACCESS_TYPE_NO_PROXY := 1 151 | WINHTTP_ACCESS_TYPE_NAMED_PROXY := 3 152 | 153 | ; Try to get system proxy settings using default (WINHTTP_ACCESS_TYPE_DEFAULT_PROXY) 154 | ; This tells WinHTTP to use the proxy settings configured in Internet Explorer or via netsh winhttp set proxy. 155 | ; If no proxy is configured, it will attempt a direct connection. 156 | Try request_obj.SetProxy(WINHTTP_ACCESS_TYPE_DEFAULT_PROXY) ; Use system default proxy settings 157 | Catch Error as e_proxy ; Specify Error class for clarity and correctness 158 | { 159 | ; If setting default proxy fails, log it or notify, but continue (might work without proxy or with specific proxy later) 160 | ; Msgbox("Failed to set system default proxy: " e_proxy.Message, "Proxy Warning", 48) ; 48 for Warning Icon 161 | } 162 | } Catch Error as e { 163 | ; Msgbox("Fatal Error: Unable to create HTTP object. " e.Message, "Error", 16) 164 | return "Fatal Error. Unable to create HTTP object" 165 | } 166 | 167 | this.defineTimeout(&request_obj, timeouts) 168 | 169 | try { 170 | request_obj.Open(method, endpoint) 171 | } Catch Error as e { 172 | ; Msgbox("Error opening request: " e.Message, "Error", 16) 173 | current_time := FormatTime(,"yyyy.MM.dd hh:mm:ss") 174 | ; FileAppend(current_time ": " e.Message ", line:" e.Line "`n`nEndpoint: " endpoint, A_ScriptDir "\netrequest_error.txt") 175 | return "Error opening request: " e.Message 176 | } 177 | 178 | try request_obj.SetRequestHeader("Content-Type", NetRequest.request_header[method]) 179 | Catch Error as e { 180 | ; Msgbox("Error setting request header: " e.Message, "Error", 16) 181 | return "Error setting request header: " e.Message 182 | } 183 | 184 | try { 185 | if (body == "") { 186 | request_obj.Send() 187 | } else { 188 | request_obj.Send(body) 189 | } 190 | } Catch Error as e { 191 | local user_friendly_message := "Error sending request: " e.Message 192 | if InStr(e.Message, "0x80072EE7") { 193 | user_friendly_message := "网络错误 (0x80072EE7): 服务器名称或地址无法解析。`n`n请检查您的网络连接和 DNS 设置。" 194 | } 195 | ; Msgbox(user_friendly_message, "请求错误", 16) ; Use 16 for Stop/Error Icon 196 | current_time := FormatTime(,"yyyy.MM.dd hh:mm:ss") 197 | ; FileAppend(current_time ": " e.Message " (User Msg: " user_friendly_message "), line:" e.Line "`n`nEndpoint: " endpoint, A_ScriptDir "\netrequest_error.txt") 198 | return user_friendly_message ; Return the potentially more friendly message 199 | } 200 | 201 | try { 202 | request_obj.WaitForResponse() 203 | } Catch Error as e { 204 | ; Msgbox("Error waiting for response: " e.Message, "Error", 16) 205 | current_time := FormatTime(,"yyyy.MM.dd hh:mm:ss") 206 | ; FileAppend(current_time ": " e.Message ", line:" e.Line "`n`nEndpoint: " endpoint, A_ScriptDir "\netrequest_error.txt") 207 | return "Error waiting for response: " e.Message 208 | } 209 | 210 | if (request_obj.ResponseText == "") { ; Check ResponseText as request_obj itself will be an object 211 | ; Msgbox("Fatal Error: Couldn't receive response or response was empty.", "Error", "IconError") 212 | return "Fatal Error. Couldn't receive response." 213 | } 214 | return request_obj 215 | } 216 | 217 | /** 218 | * Defines the default timeouts or allows them to be adjusted. 219 | * Timeouts are always defined in milliseconds. 220 | * 221 | * @Parameters 222 | * com_obj - [Object] expects valid COM object for HTTP requests (passed by reference) 223 | * timeouts - [Map] expects timeouts as key-value pairs (see createRequestObj for format) 224 | * If not a Map, default timeouts are used. 225 | * 226 | * @Return 227 | * Nothing 228 | */ 229 | defineTimeout(&com_obj, timeouts) { 230 | local resolve_timeout, connect_timeout, send_timeout, receive_timeout 231 | if (timeouts is Map) { 232 | resolve_timeout := timeouts.Has("resolve_timeout") ? timeouts["resolve_timeout"] : 0 233 | connect_timeout := timeouts.Has("connect_timeout") ? timeouts["connect_timeout"] : 30000 234 | send_timeout := timeouts.Has("send_timeout") ? timeouts["send_timeout"] : 30000 235 | receive_timeout := timeouts.Has("receive_timeout") ? timeouts["receive_timeout"] : 60000 ; Corrected from 600000 236 | } else { 237 | resolve_timeout := 0 238 | connect_timeout := 30000 239 | send_timeout := 30000 240 | receive_timeout := 60000 ; Corrected from 600000 241 | } 242 | com_obj.SetTimeouts(resolve_timeout, connect_timeout, send_timeout, receive_timeout) 243 | } 244 | 245 | /** 246 | * Searches the DOM for tags with passed attributes. 247 | * (Note: This method relies on Internet Explorer COM object, which is deprecated and may not work reliably.) 248 | * 249 | * @Parameters 250 | * browser_obj - [Object] expects browser_obj.document (e.g., from webBrowser() method) 251 | * item - [String] expects valid CSS selector (e.g., "[attribute=value]") 252 | * 253 | * @Return 254 | * [Array] Array of matching DOM elements. 255 | */ 256 | searchForMatches(browser_obj, item) { 257 | local matching_elements := [] 258 | local all_elements := browser_obj.querySelectorAll(item) 259 | Loop all_elements.length { 260 | if (all_elements[A_Index-1]) { 261 | matching_elements.Push(all_elements[A_Index-1]) 262 | } 263 | } 264 | return matching_elements 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /lib/PotplayerControl.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2.0 2 | #Include MyTool.ahk 3 | 4 | class PotplayerControl { 5 | __New(potplayer_process_name) { 6 | this._PotplayerProcessName := potplayer_process_name 7 | this.COMMAND_TYPE := 273 8 | this.REQUEST_TYPE := 1024 9 | this.hwnd := this.GetPotplayerHwnd() 10 | } 11 | 12 | SendCommand(msg_type, wParam, lParam) { 13 | try{ 14 | hwnd := this.GetPotplayerHwnd() 15 | return SendMessage(msg_type, wParam, lParam, this.getHwnd()) 16 | } catch { 17 | return 0 18 | } 19 | } 20 | PostCommand(msg_type, wParam, lParam) { 21 | try{ 22 | hwnd := this.GetPotplayerHwnd() 23 | return PostMessage(msg_type, wParam, lParam, this.getHwnd()) 24 | } catch { 25 | return 0 26 | } 27 | } 28 | 29 | getHwnd() { 30 | ; potplayer 运行中 31 | if (this.hwnd > 0 && WinExist("ahk_id " this.hwnd)){ 32 | return this.hwnd 33 | } 34 | 35 | ; 上一次获取potplayer的hwnd失效了, 可能是potplayer关闭重开了, 再次获取hwnd 36 | this.hwnd := this.GetPotplayerHwnd() 37 | return this.hwnd 38 | } 39 | 40 | GetPotplayerHwnd() { 41 | ids := WinGetList("ahk_exe " this._PotplayerProcessName) 42 | for id in ids { 43 | try{ 44 | title := WinGetTitle("ahk_id " id) 45 | } catch { 46 | continue 47 | } 48 | if (InStr(title, "PotPlayer")) { 49 | this.hwnd := id 50 | return id 51 | } 52 | } 53 | ; potplayer 未运行 54 | return 0 55 | } 56 | 57 | GetMediaPathToClipboard() { 58 | this.PostCommand(this.COMMAND_TYPE, 10928, 0) 59 | } 60 | 61 | GetMediaTimestampToClipboard() { 62 | this.PostCommand(this.COMMAND_TYPE, 10924, 0) 63 | } 64 | 65 | GetSubtitleToClipboard() { 66 | this.PostCommand(this.COMMAND_TYPE, 10624, 0) 67 | } 68 | SaveImageToClipboard() { 69 | this.SendCommand(this.COMMAND_TYPE, 10223, 0) 70 | } 71 | 72 | ; 状态 73 | GetPlayStatus() { 74 | status := this.SendCommand(this.REQUEST_TYPE, 20486, 0) 75 | switch status { 76 | case -1: 77 | return "Stopped" 78 | case 1: 79 | return "Paused" 80 | case 2: 81 | return "Running" 82 | } 83 | return "Undefined" 84 | } 85 | PlayOrPause() { 86 | this.PostCommand(this.COMMAND_TYPE, 20001, 0) 87 | } 88 | Play() { 89 | status := this.GetPlayStatus() 90 | if (status != "Running") { 91 | this.PostCommand(this.COMMAND_TYPE, 20000, 0) 92 | } 93 | } 94 | PlayPause() { 95 | status := this.GetPlayStatus() 96 | if (status == "Running") { 97 | this.PostCommand(this.COMMAND_TYPE, 20000, 0) 98 | } 99 | } 100 | Stop() { 101 | this.PostCommand(this.COMMAND_TYPE, 20002, 0) 102 | } 103 | 104 | ; 速度 105 | ; PreviousFrame() { 106 | ; this.PostCommand(this.COMMAND_TYPE, 10242, 0) 107 | ; } 108 | ; NextFrame() { 109 | ; this.PostCommand(this.COMMAND_TYPE, 10241, 0) 110 | ; } 111 | ; Forward() { 112 | ; this.PostCommand(this.COMMAND_TYPE, 10060, 0) 113 | ; } 114 | ; Backward() { 115 | ; this.PostCommand(this.COMMAND_TYPE, 10059, 0) 116 | ; } 117 | ; SpeedUp() { 118 | ; this.PostCommand(this.COMMAND_TYPE, 10248, 0) 119 | ; } 120 | ; SpeedDown() { 121 | ; this.PostCommand(this.COMMAND_TYPE, 10247, 0) 122 | ; } 123 | ; SpeedNormal() { 124 | ; this.PostCommand(this.COMMAND_TYPE, 10246, 0) 125 | ; } 126 | 127 | ; 时间 128 | GetMediaTimeMilliseconds() { 129 | return this.SendCommand(this.REQUEST_TYPE, 20484, 0) 130 | } 131 | ; 受【选项-播放-时间跨度-如果存在关键帧数据则以关键帧为移动单位】的potplayer后处理影响不够精准,关掉此选项则非常精准 132 | SetMediaTimeMilliseconds(ms) { 133 | this.PostCommand(this.REQUEST_TYPE, 20485, ms) 134 | } 135 | GetMediaTimeSecondsTime() { 136 | return Integer(this.GetMediaTimeMilliseconds() / 1000) 137 | } 138 | GetTotalTimeSeconds() { 139 | return Integer(this.SendCommand(this.REQUEST_TYPE, 20482, 0) / 1000) 140 | } 141 | SetCurrentSecondsTime(seconds) { 142 | if (seconds < 0) { 143 | seconds := 0 144 | } 145 | this.SetMediaTimeMilliseconds(seconds * 1000) 146 | } 147 | 148 | ; A-B 循环 149 | SetStartPointOfTheABCycle() { 150 | this.PostCommand(this.COMMAND_TYPE, 10249, 0) 151 | } 152 | SetEndPointOfTheABCycle() { 153 | this.PostCommand(this.COMMAND_TYPE, 10250, 0) 154 | } 155 | CancelTheABCycle() { 156 | ; 解除区段循环:起点 157 | this.PostCommand(this.COMMAND_TYPE, 10251, 0) 158 | ; 解除区段循环:终点 159 | this.PostCommand(this.COMMAND_TYPE, 10252, 0) 160 | } 161 | } 162 | 163 | ; 示例 164 | ; potplayer := PotplayerControl("PotplayerMini64.exe") 165 | 166 | ; potplayer.SpeedUp() 167 | ; potplayer.SpeedDown() 168 | ; potplayer.SpeedNormal() 169 | -------------------------------------------------------------------------------- /lib/PotplayerFragment.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2.0 2 | ; PotplayerFragment.ahk 3 | ; 提供AB片段/循环播放、同视频判断、AB循环取消等可复用函数 4 | 5 | ; 判断当前播放的视频,是否是跳转的视频 6 | IsSameVideo(potplayer_control, jump_path) { 7 | ; 判断网络视频 8 | if (InStr(jump_path, "http")) { 9 | current_path := GetPotplayerpath(potplayer_control) 10 | if (InStr(jump_path, current_path)) { 11 | return true 12 | } 13 | return false 14 | } 15 | ; 判断本地视频 16 | potplayer_title := WinGetTitle("ahk_id " potplayer_control.getHwnd()) 17 | video_name := GetNameForPath(UrlDecode(jump_path)) 18 | if (InStr(potplayer_title, video_name)) { 19 | return true 20 | } 21 | return false 22 | } 23 | 24 | GetPotplayerpath(potplayer_control) { 25 | A_Clipboard := "" 26 | potplayer_control.GetMediaPathToClipboard() 27 | if (!ClipWait(1, 0)) { 28 | return "" 29 | } 30 | return A_Clipboard 31 | } 32 | 33 | ; 取消A-B循环(如果已设置) 34 | CancelABCycleIfNeeded(potplayer_control, potplayer_path, jump_path) { 35 | if (IsPotplayerRunning(potplayer_path) 36 | && potplayer_control.GetPlayStatus() != "Stopped" 37 | && IsSameVideo(potplayer_control, jump_path)) { 38 | potplayer_control.CancelTheABCycle() 39 | } 40 | } 41 | 42 | ; 单次播放AB片段 43 | JumpToAbFragment(potplayer_control, potplayer_path, media_path, time_start, time_end, ab_fragment_detection_delays) { 44 | ; 1. 跳转到片段起点 45 | if (IsPotplayerRunning(potplayer_path) 46 | && potplayer_control.GetPlayStatus() != "Stopped" 47 | && IsSameVideo(potplayer_control, media_path)) { 48 | 49 | potplayer_control.SetMediaTimeMilliseconds(TimestampToMilliseconds(time_start)) 50 | potplayer_control.Play() 51 | } else { 52 | run_command := potplayer_path . " `"" . media_path . "`" /seek=" . time_start . " /current" 53 | try { 54 | Run run_command 55 | } catch Error as err { 56 | MsgBox "错误:" err.Extra 57 | MsgBox run_command 58 | return 59 | } 60 | } 61 | ; 等待 62 | Sleep 1000 63 | 64 | flag_ab_fragment := true 65 | 66 | Hotkey "Esc", CancelAbFragment 67 | CancelAbFragment(*) { 68 | flag_ab_fragment := false 69 | Hotkey "Esc", "off" 70 | } 71 | 72 | ; 2. 检查结束时间 73 | ; duration := TimestampToMilliseconds(time_end) - TimestampToMilliseconds(time_start) 74 | ; past := 0 75 | ; 3. 检查结束时间 76 | while (flag_ab_fragment) { 77 | ; 异常情况:用户关闭Potplayer 78 | if (!IsPotplayerRunning(potplayer_path)) { 79 | ; MsgBox("异常情况:用户关闭Potplayer") 80 | break 81 | ; 异常情况:用户停止播放视频 82 | } else if (potplayer_control.GetPlayStatus() != "Running") { 83 | ; MsgBox("异常情况:用户停止播放视频") 84 | break 85 | } 86 | 87 | ; todo: 异常情况:不是同一个视频 88 | ; - 在播放B站视频时,可以加载视频列表,这样用户就会切换视频,此时就要结束循环 else if (!IsSameVideo(media_path)) {break} 89 | ; - 另一种思路:当前循环的时间超过了时间期间,就结束循环; +1是为了防止误差 90 | ; else if (past >= duration + 1000) { 91 | ; break 92 | ; } 93 | 94 | ; 正常情况:当前播放时间超过了结束时间、用户手动调整时间,超过了结束时间 95 | current_time := potplayer_control.GetMediaTimeMilliseconds() 96 | if (current_time >= TimestampToMilliseconds(time_end)) { 97 | potplayer_control.PlayPause() 98 | Hotkey "Esc", "off" 99 | flag_ab_fragment := false 100 | ; MsgBox("正常情况:当前播放时间超过了结束时间、用户手动调整时间,超过了结束时间") 101 | break 102 | } 103 | Sleep ab_fragment_detection_delays 104 | ; past += ab_fragment_detection_delays 105 | } 106 | 107 | ; potplayer_control.PlayPause() 108 | ; Hotkey "Esc", "off" 109 | } 110 | 111 | ; 循环播放AB片段 112 | JumpToAbCirculation(potplayer_control, potplayer_path, media_path, time_start, time_end) { 113 | if (IsPotplayerRunning(potplayer_path) 114 | && potplayer_control.GetPlayStatus() != "Stopped" 115 | && IsSameVideo(potplayer_control, media_path)) { 116 | potplayer_control.SetMediaTimeMilliseconds(TimestampToMilliseconds(time_start)) 117 | potplayer_control.SetStartPointOfTheABCycle() 118 | potplayer_control.SetMediaTimeMilliseconds(TimestampToMilliseconds(time_end)) 119 | potplayer_control.SetEndPointOfTheABCycle() 120 | potplayer_control.Play() 121 | } else { 122 | run_command := potplayer_path . " `"" . media_path . "`" /seek=" . time_start . " /current" 123 | try { 124 | Run run_command 125 | } catch Error as err { 126 | MsgBox "错误:" err.Extra 127 | MsgBox run_command 128 | return 129 | } 130 | Sleep 1000 131 | potplayer_control.SetMediaTimeMilliseconds(TimestampToMilliseconds(time_start)) 132 | potplayer_control.SetStartPointOfTheABCycle() 133 | potplayer_control.SetMediaTimeMilliseconds(TimestampToMilliseconds(time_end)) 134 | potplayer_control.SetEndPointOfTheABCycle() 135 | potplayer_control.Play() 136 | } 137 | } -------------------------------------------------------------------------------- /lib/Render.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2.0 2 | 3 | RenderTemplate(backlink_template, media_data, current_hotkey := "") { 4 | backlink_template := RenderSrtTemplate(backlink_template, media_data, "") 5 | 6 | markdown_link_data := RenderNameAndTimeAndLink(app_config, media_data) 7 | 8 | if (InStr(backlink_template, "{title}") != 0) { 9 | rendered_link := RenderTitle(app_config, markdown_link_data) 10 | backlink_template := StrReplace(backlink_template, "{title}", rendered_link) 11 | } 12 | 13 | if (InStr(backlink_template, "{link}") != 0) { 14 | backlink_template := StrReplace(backlink_template, "{link}", markdown_link_data.link) 15 | } 16 | 17 | if (InStr(backlink_template, "{name}") != 0) { 18 | backlink_template := StrReplace(backlink_template, "{name}", media_data.Path) 19 | } 20 | 21 | if (InStr(backlink_template, "{time}") != 0) { 22 | backlink_template := StrReplace(backlink_template, "{time}", media_data.Time) 23 | } 24 | 25 | if (InStr(backlink_template, "{userNote}") != 0) { 26 | userNote := "" 27 | 28 | if(current_hotkey == (app_config.HotkeyUserNote " Up")) { 29 | value := InputBox("Please enter a note.", "note") 30 | if (value.Result != "Cancel") { 31 | userNote := value.Value 32 | } 33 | } 34 | 35 | ; 用户没有输入, 删除userNote占位符 36 | if (userNote == "") { 37 | userNoteOneLineFirst := "{userNote}`n" 38 | userNoteOneLine := "`n{userNote}`n" 39 | userNoteOneLineLast := "`n{userNote}" 40 | 41 | if (InStr(backlink_template, userNoteOneLineFirst) != 0) { 42 | backlink_template := StrReplace(backlink_template, userNoteOneLineFirst, "") 43 | } 44 | if (InStr(backlink_template, userNoteOneLine) != 0) { 45 | backlink_template := StrReplace(backlink_template, userNoteOneLine, "") 46 | } 47 | if (InStr(backlink_template, userNoteOneLineLast) != 0) { 48 | backlink_template := StrReplace(backlink_template, userNoteOneLineLast, "") 49 | } 50 | } 51 | 52 | backlink_template := StrReplace(backlink_template, "{userNote}", userNote) 53 | } 54 | 55 | if (InStr(backlink_template, "{subtitle}") != 0) { 56 | backlink_template := RenderSubtitleTemplate(backlink_template, media_data) 57 | } 58 | 59 | return backlink_template 60 | } 61 | 62 | RenderSubtitleTemplate(target_string, media_data) { 63 | if (InStr(target_string, "{subtitle}") != 0) { 64 | if (media_data.Subtitle == "") { 65 | media_data.Subtitle := GetMediaSubtitle() 66 | } 67 | target_string := StrReplace(target_string, "{subtitle}", media_data.Subtitle) 68 | } 69 | return target_string 70 | } 71 | 72 | RenderTitle(app_config, markdown_link_data) { 73 | result_link := "" 74 | ; 生成word链接 75 | if (IsWordProgram()) { 76 | result_link := "" markdown_link_data.title "" 77 | 78 | } else { 79 | ; 生成MarkDown链接 80 | result_link := GenerateMarkdownLink(markdown_link_data.title, markdown_link_data.link) 81 | } 82 | return result_link 83 | } 84 | 85 | RenderSrtTemplate(backlink_template, media_data, subtitle_data) { 86 | if (InStr(backlink_template, '{subtitleTimeRange}') != 0 || 87 | InStr(backlink_template, '{subtitleTimeStart}') != 0 || 88 | InStr(backlink_template, '{subtitleTimeEnd}') != 0 || 89 | InStr(backlink_template, '{subtitleOrigin}') != 0) { 90 | 91 | if (!subtitle_data) { 92 | SplitPath media_data.Path, &name, &dir, &ext, &name_no_ext, &drive 93 | subtitles_data := SubtitlesDataFromSrt(dir "/" name_no_ext ".srt") 94 | subtitle_data := FindSubtitleByTimestamp(GetMediaTimeMilliseconds() + 1, subtitles_data) 95 | } 96 | 97 | if (subtitle_data) { 98 | if (InStr(backlink_template, '{subtitleTimeRange}') != 0) { 99 | ; 1. 修改渲染回链的 link 中的 time 100 | media_data.Time := MillisecondsToTimestamp(subtitle_data.timeStart + 1) "-" MillisecondsToTimestamp(subtitle_data.timeEnd - 1) 101 | ; 2. 修改渲染回链的 title 中的 time 102 | backlink_template := StrReplace(backlink_template, "{subtitleTimeRange}", RemoveMillisecondsFromTimestamp(media_data.Time)) 103 | } else if (InStr(backlink_template, '{subtitleTimeStart}') != 0) { 104 | media_data.Time := MillisecondsToTimestamp(subtitle_data.timeStart + 1) 105 | backlink_template := StrReplace(backlink_template, "{subtitleTimeStart}", RemoveMillisecondsFromTimestamp(media_data.Time)) 106 | } else if (InStr(backlink_template, '{subtitleTimeEnd}') != 0) { 107 | media_data.Time := MillisecondsToTimestamp(subtitle_data.timeEnd - 1) 108 | backlink_template := StrReplace(backlink_template, "{subtitleTimeEnd}", RemoveMillisecondsFromTimestamp(media_data.Time)) 109 | } 110 | 111 | if (InStr(backlink_template, '{subtitleOrigin}') != 0) { 112 | backlink_template := StrReplace(backlink_template, "{subtitleOrigin}", subtitle_data.subtitle) 113 | } 114 | 115 | return backlink_template 116 | } 117 | } 118 | return backlink_template 119 | } 120 | 121 | ; // [用户想要的标题格式](mk-potplayer://open?path=1&aaa=123&time=456) 122 | RenderNameAndTimeAndLink(app_config, media_data) { 123 | ; B站的视频 124 | if (InStr(media_data.Path, "https://www.bilibili.com/video/") || 125 | InStr(media_data.Path, ".youtube.com/watch")) { 126 | ; 正常播放的情况 127 | name := StrReplace(GetPotplayerTitle(app_config.PotplayerProcessName), " - PotPlayer", "") 128 | 129 | ; 视频没有播放,已经停止的情况,不是暂停是停止 130 | if name == "PotPlayer" { 131 | name := GetFileNameInPath(media_data.Path) 132 | } 133 | } else { 134 | ; 本地视频 135 | name := GetFileNameInPath(media_data.Path) 136 | } 137 | ; 渲染 title 138 | title := app_config.MarkdownTitle 139 | title := StrReplace(title, "{name}", name) 140 | if(InStr(media_data.Time, "-") != 0 || 141 | InStr(media_data.Time, "∞") != 0) { 142 | title := StrReplace(title, "{time}", RemoveMillisecondsFromTimeRange(media_data.Time)) 143 | } else { 144 | title := StrReplace(title, "{time}", RemoveMillisecondsFromTimestamp(media_data.Time)) 145 | } 146 | ; 渲染 title 中的 字幕模板 147 | title := RenderSubtitleTemplate(title, media_data) 148 | 149 | markdown2potplayer_link := GenerateMarkdownLink2PotplayerLink(app_config, media_data) 150 | 151 | result := { 152 | title: title, 153 | link: markdown2potplayer_link 154 | } 155 | return result 156 | } 157 | 158 | GenerateMarkdownLink(markdown_title, markdown_link) { 159 | if (IsNotionProgram()) { 160 | result := "[" markdown_title "](http://127.0.0.1:33660/" markdown_link ")" 161 | } else { 162 | result := "[" markdown_title "](" markdown_link ")" 163 | } 164 | return result 165 | } 166 | 167 | GenerateMarkdownLink2PotplayerLink(app_config, media_data) { 168 | return app_config.UrlProtocol "?path=" ProcessUrl(media_data.Path) "&time=" media_data.Time 169 | } 170 | 171 | RenderImage(markdown_image_template, media_data, image, hotkey:="") { 172 | identifier := "{image}" 173 | image_templates := TemplateConvertedToTemplates(markdown_image_template, identifier) 174 | For index, image_template in image_templates { 175 | if (image_template == identifier) { 176 | SendImage2NoteApp(image) 177 | } else { 178 | rendered_template := RenderTemplate(image_template, media_data, hotkey) 179 | if (IsWordProgram() && InStr(image_template, "{title}")) { 180 | SendText2wordApp(rendered_template) 181 | } else { 182 | SendText2NoteApp(rendered_template) 183 | } 184 | } 185 | } 186 | } -------------------------------------------------------------------------------- /lib/TemplateParser.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2.0 2 | 3 | /** 4 | * 使用指定的分隔符将字符串分成子字符串数组 5 | * 例如:{image}123123{image}{image}456456{image},以{image}分割,转为数组 => ["{image}", "123123", "{image}", "{image}", "456456", "{image}"] 6 | * 7 | * @param template 用户模板 8 | * @param identifier 分隔符 9 | */ 10 | TemplateConvertedToTemplates(template, delimiter) { 11 | result := [] 12 | searchIndex := 1 13 | 14 | while (true) { 15 | delimiterIndex := InStr(template, delimiter, false, searchIndex) 16 | ; 情况1: 搜索结束 17 | if (delimiterIndex = 0) { 18 | if (searchIndex > 0 && searchIndex < StrLen(template)) { 19 | result.Push(SubStr(template, searchIndex)) 20 | } 21 | break 22 | } 23 | ; 情况2: 搜索到 identifier自身 24 | if (delimiterIndex == searchIndex) { 25 | result.Push(delimiter) 26 | } else { 27 | ; 情况3:正常分割 28 | result.Push(SubStr(template, searchIndex, delimiterIndex - searchIndex)) 29 | result.Push(delimiter) 30 | } 31 | 32 | ; 移动游标 33 | searchIndex := delimiterIndex + StrLen(delimiter) 34 | } 35 | 36 | return result 37 | } 38 | 39 | ; test 40 | ; testString := "{image}123123{image}{image}456456{image}" 41 | ; delimiterString := "{image}" 42 | ; result := TemplateConvertedToTemplates(testString, delimiterString) 43 | 44 | ; for index, item in result 45 | ; MsgBox("Item " . index . ": " . item) 46 | -------------------------------------------------------------------------------- /lib/TimeTool.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2.0 2 | 3 | ; ====================================================================================================================== 4 | ; 函数: MillisecondsToTimestamp 5 | ; 描述: 将毫秒转换为自定义时间戳格式 (HH:MM:SS.mmm 或 MM:SS.mmm). 6 | ; 参数: ms - 输入的毫秒数 (整数). 7 | ; 返回: 格式化的时间戳字符串. 如果输入无效则返回 "Invalid input". 8 | ; ====================================================================================================================== 9 | MillisecondsToTimestamp(ms) { 10 | if !IsInteger(ms) || ms < 0 { 11 | return "Invalid input" 12 | } 13 | 14 | 15 | local milliseconds := Mod(ms, 1000) 16 | local totalSeconds := Floor(ms / 1000) 17 | local seconds := Mod(totalSeconds, 60) 18 | local totalMinutes := Floor(totalSeconds / 60) 19 | local minutes := Mod(totalMinutes, 60) 20 | local hours := Floor(totalMinutes / 60) 21 | 22 | local formattedMs := Format("{:03}", milliseconds) 23 | local formattedS := Format("{:02}", seconds) 24 | local formattedM := Format("{:02}", minutes) 25 | 26 | if (hours > 0) 27 | return Format("{:02}:{}:{}.{}", hours, formattedM, formattedS, formattedMs) 28 | else 29 | return Format("{}:{}.{}", formattedM, formattedS, formattedMs) 30 | } 31 | 32 | ; ====================================================================================================================== 33 | ; 函数: TimestampToMilliseconds 34 | ; 描述: 将自定义时间戳格式转换为毫秒,支持多种灵活格式: 35 | ; - 纯秒数: "123" 36 | ; - HH:MM:SS.mmm 或 HH:MM:SS (可选毫秒) 37 | ; - MM:SS.mmm 或 MM:SS (可选毫秒) 38 | ; - SS.mmm (秒.毫秒,毫秒1-3位) 39 | ; - 支持前导零可选,毫秒位数1-3位 40 | ; 参数: timestamp - 格式化的时间戳字符串或纯秒数. 41 | ; 返回: 毫秒数 (整数). 如果格式无效则返回空字符串 "" (AHK中常用于表示失败或无效). 42 | ; ====================================================================================================================== 43 | TimestampToMilliseconds(timestamp) { 44 | if (!IsObject(timestamp) && !timestamp == "") { 45 | return "" 46 | } 47 | 48 | ; 检查是否为纯秒数格式 (只包含数字,无冒号和小数点) 49 | if (RegExMatch(timestamp, "^\d+$")) { 50 | try { 51 | local seconds := Integer(timestamp) 52 | if (seconds >= 0) 53 | return seconds * 1000 54 | else 55 | return "" 56 | } catch Error as e { 57 | return "" 58 | } 59 | } 60 | 61 | local hours := 0, minutes := 0, seconds := 0, milliseconds := 0 62 | 63 | try { 64 | ; 检查是否包含冒号 65 | if (InStr(timestamp, ":")) { 66 | local parts := StrSplit(timestamp, ":") 67 | 68 | if (parts.Length == 3) { ; HH:MM:SS.mmm 或 HH:MM:SS 69 | hours := Integer(parts[1]) 70 | minutes := Integer(parts[2]) 71 | 72 | ; 检查第三部分是否包含毫秒 73 | if (InStr(parts[3], ".")) { 74 | local secMsParts := StrSplit(parts[3], ".") 75 | if (secMsParts.Length != 2) 76 | return "" 77 | seconds := Integer(secMsParts[1]) 78 | local msStr := secMsParts[2] 79 | ; 毫秒可以是1-3位,需要补齐到3位进行计算 80 | if (RegExMatch(msStr, "^\d{1,3}$")) { 81 | if (StrLen(msStr) == 1) 82 | milliseconds := Integer(msStr) * 100 83 | else if (StrLen(msStr) == 2) 84 | milliseconds := Integer(msStr) * 10 85 | else 86 | milliseconds := Integer(msStr) 87 | } else { 88 | return "" 89 | } 90 | } else { 91 | ; 没有毫秒部分 92 | seconds := Integer(parts[3]) 93 | milliseconds := 0 94 | } 95 | 96 | } else if (parts.Length == 2) { ; MM:SS.mmm 或 MM:SS 97 | minutes := Integer(parts[1]) 98 | 99 | ; 检查第二部分是否包含毫秒 100 | if (InStr(parts[2], ".")) { 101 | local secMsParts := StrSplit(parts[2], ".") 102 | if (secMsParts.Length != 2) 103 | return "" 104 | seconds := Integer(secMsParts[1]) 105 | local msStr := secMsParts[2] 106 | ; 毫秒可以是1-3位,需要补齐到3位进行计算 107 | if (RegExMatch(msStr, "^\d{1,3}$")) { 108 | if (StrLen(msStr) == 1) 109 | milliseconds := Integer(msStr) * 100 110 | else if (StrLen(msStr) == 2) 111 | milliseconds := Integer(msStr) * 10 112 | else 113 | milliseconds := Integer(msStr) 114 | } else { 115 | return "" 116 | } 117 | } else { 118 | ; 没有毫秒部分 119 | seconds := Integer(parts[2]) 120 | milliseconds := 0 121 | } 122 | 123 | } else { 124 | return "" ; 无效格式 125 | } 126 | 127 | } else if (InStr(timestamp, ".")) { 128 | ; SS.mmm 格式(只有秒和毫秒) 129 | local secMsParts := StrSplit(timestamp, ".") 130 | if (secMsParts.Length != 2) 131 | return "" 132 | 133 | seconds := Integer(secMsParts[1]) 134 | local msStr := secMsParts[2] 135 | ; 毫秒可以是1-3位,需要补齐到3位进行计算 136 | if (RegExMatch(msStr, "^\d{1,3}$")) { 137 | if (StrLen(msStr) == 1) 138 | milliseconds := Integer(msStr) * 100 139 | else if (StrLen(msStr) == 2) 140 | milliseconds := Integer(msStr) * 10 141 | else 142 | milliseconds := Integer(msStr) 143 | } else { 144 | return "" 145 | } 146 | 147 | } else { 148 | return "" ; 无效格式 149 | } 150 | 151 | ; 验证数值范围 152 | if (hours < 0 || minutes < 0 || minutes >= 60 || seconds < 0 || seconds >= 60 || milliseconds < 0 || milliseconds >= 1000) 153 | return "" ; 数值范围无效 154 | 155 | } catch Error as e { 156 | ; MsgBox "Error during conversion: " e.Message 157 | return "" ; 转换出错 158 | } 159 | 160 | return (hours * 3600 + minutes * 60 + seconds) * 1000 + milliseconds 161 | } 162 | 163 | ; ====================================================================================================================== 164 | ; 函数: FormatDuration 165 | ; 描述: 格式化毫秒为易读的持续时间字符串 (一个方便操作的函数示例). 166 | ; 这个函数与 MillisecondsToTimestamp 类似,但可以根据需要进行扩展. 167 | ; 参数: ms - 输入的毫秒数 (整数). 168 | ; 返回: 格式化的持续时间字符串. 169 | ; ====================================================================================================================== 170 | FormatDuration(ms) { 171 | ; 对于这个示例,我们让它和 MillisecondsToTimestamp 的行为一致 172 | return MillisecondsToTimestamp(ms) 173 | } 174 | 175 | ; 函数: RemoveMillisecondsFromTimestamp 176 | ; 描述: 从时间戳字符串或时间片段中移除毫秒部分. 177 | ; 支持单个时间戳: "01:01.012" 变为 "01:01", "25:01:01.012" 变为 "25:01:01" 178 | ; 支持时间片段: "00:24.271-00:31.089" 变为 "00:24-00:31" 179 | ; 参数: timestamp - 输入的时间戳字符串或时间片段字符串. 180 | ; 返回: 移除了毫秒部分的时间戳字符串或时间片段. 如果输入不包含 '.', 则原样返回. 181 | ; ====================================================================================================================== 182 | RemoveMillisecondsFromTimestamp(timestamp) { 183 | if timestamp = "" { 184 | return timestamp ; 如果为空字符串,直接返回原值 185 | } 186 | 187 | ; 检查是否包含连字符,判断是否为时间片段格式 188 | local dashPos := InStr(timestamp, "-") 189 | if (dashPos > 0) { 190 | ; 处理时间片段格式 "00:24.271-00:31.089" 191 | local startTime := SubStr(timestamp, 1, dashPos - 1) 192 | local endTime := SubStr(timestamp, dashPos + 1) 193 | 194 | ; 分别处理开始时间和结束时间的毫秒 195 | local processedStart := RemoveMillisecondsFromSingleTimestamp(startTime) 196 | local processedEnd := RemoveMillisecondsFromSingleTimestamp(endTime) 197 | 198 | return processedStart "-" processedEnd 199 | } else { 200 | ; 处理单个时间戳格式 201 | return RemoveMillisecondsFromSingleTimestamp(timestamp) 202 | } 203 | } 204 | 205 | ; 辅助函数: 处理单个时间戳的毫秒移除 206 | ; 参数: singleTimestamp - 单个时间戳字符串 207 | ; 返回: 移除毫秒后的时间戳 208 | RemoveMillisecondsFromSingleTimestamp(singleTimestamp) { 209 | if singleTimestamp = "" { 210 | return singleTimestamp 211 | } 212 | 213 | local pos := InStr(singleTimestamp, ".") 214 | if (pos > 0) 215 | return SubStr(singleTimestamp, 1, pos - 1) 216 | else 217 | return singleTimestamp ; 没有找到 '.', 原样返回 218 | } 219 | 220 | ; ====================================================================================================================== 221 | ; 函数: RemoveMillisecondsFromTimeRange 222 | ; 描述: 从时间段字符串中移除毫秒部分,支持两种分隔符:- 和 ∞ 223 | ; 例如 "09:19.237-14:13.934" 变为 "09:19-14:13" 224 | ; 例如 "19:33.470∞25:10.986" 变为 "19:33∞25:10" 225 | ; 如果输入是单个时间戳,则调用 RemoveMillisecondsFromTimestamp 处理 226 | ; 参数: timeRange - 输入的时间段字符串或单个时间戳 227 | ; 返回: 移除了毫秒部分的时间段字符串 228 | ; ====================================================================================================================== 229 | RemoveMillisecondsFromTimeRange(timeRange) { 230 | if timeRange = "" { 231 | return timeRange 232 | } 233 | 234 | ; 检查是否包含 "-" 分隔符 235 | local dashPos := InStr(timeRange, "-") 236 | if (dashPos > 0) { 237 | ; 分割时间段 238 | local startTime := SubStr(timeRange, 1, dashPos - 1) 239 | local endTime := SubStr(timeRange, dashPos + 1) 240 | 241 | ; 分别处理起始和结束时间 242 | local cleanStartTime := RemoveMillisecondsFromTimestamp(startTime) 243 | local cleanEndTime := RemoveMillisecondsFromTimestamp(endTime) 244 | 245 | return cleanStartTime "-" cleanEndTime 246 | } 247 | 248 | ; 检查是否包含 "∞" 分隔符 249 | local infinityPos := InStr(timeRange, "∞") 250 | if (infinityPos > 0) { 251 | ; 分割时间段 252 | local startTime := SubStr(timeRange, 1, infinityPos - 1) 253 | local endTime := SubStr(timeRange, infinityPos + 1) 254 | 255 | ; 分别处理起始和结束时间 256 | local cleanStartTime := RemoveMillisecondsFromTimestamp(startTime) 257 | local cleanEndTime := RemoveMillisecondsFromTimestamp(endTime) 258 | 259 | return cleanStartTime "∞" cleanEndTime 260 | } 261 | 262 | ; 如果没有分隔符,当作单个时间戳处理 263 | return RemoveMillisecondsFromTimestamp(timeRange) 264 | } 265 | 266 | IsInteger(val) { 267 | return RegExMatch(val, "^[-+]?\d+$") 268 | } 269 | 270 | ; ====================================================================================================================== 271 | ; 示例用法 272 | ; ====================================================================================================================== 273 | ; MsgBox "--- 移除时间戳中的毫秒部分 ---" Chr(10) 274 | ; . "Input: `" 01: 01.012`" -> Output: `" " RemoveMillisecondsFromTimestamp(" 01: 01.012 ") "`"" Chr(10) 275 | ; . "Input: `" 25: 01: 01.012`" -> Output: `" " RemoveMillisecondsFromTimestamp(" 25: 01: 01.012 ") "`"" Chr(10) 276 | ; . "Input: `" 00: 59.999`" -> Output: `" " RemoveMillisecondsFromTimestamp(" 00: 59.999 ") "`"" Chr(10) 277 | ; . "Input: `" 10: 30`" (no ms) -> Output: `" " RemoveMillisecondsFromTimestamp(" 10: 30 ") "`"" Chr(10) 278 | ; . "Input: `" Invalid.Format`" -> Output: `" " RemoveMillisecondsFromTimestamp(" Invalid.Format ") "`"" Chr(10) 279 | ; . "Input: `" AnotherInvalid`" -> Output: `" " RemoveMillisecondsFromTimestamp(" AnotherInvalid ") "`"" Chr(10) 280 | ; . "Input: 123 (not a string) -> Output: " RemoveMillisecondsFromTimestamp(123) ; 数字输入 281 | 282 | ; MsgBox "--- 毫秒转时间戳 ---" Chr(10) 283 | ; . "61012 ms: " MillisecondsToTimestamp(61012) Chr(10) ; 01:01.012 284 | ; . "3661012 ms: " MillisecondsToTimestamp(3661012) Chr(10) ; 01:01:01.012 285 | ; . "90061012 ms: " MillisecondsToTimestamp(90061012) Chr(10) ; 25:01:01.012 286 | ; . "123 ms: " MillisecondsToTimestamp(123) Chr(10) ; 00:00.123 287 | ; . "59999 ms: " MillisecondsToTimestamp(59999) Chr(10) ; 00:59.999 288 | ; . "0 ms: " MillisecondsToTimestamp(0) Chr(10) ; 00:00.000 289 | ; . "-100 ms (invalid): " MillisecondsToTimestamp(-100) Chr(10) 290 | ; . "A_ScreenWidth (invalid type): " MillisecondsToTimestamp(A_ScreenWidth) ; 演示非数字输入 291 | 292 | ; MsgBox "--- 时间戳转毫秒 ---" Chr(10) 293 | ; . "`" 01: 01.012`": " TimestampToMilliseconds("01:01.012") Chr(10) ; 61012 294 | ; . "`" 01: 01: 01.012`": " TimestampToMilliseconds("01:01:01.012") Chr(10) ; 3661012 295 | ; . "`" 25: 01: 01.012`": " TimestampToMilliseconds("25:01:01.012") Chr(10) ; 90061012 296 | ; . "`" 00: 00.123`": " TimestampToMilliseconds("00:00.123") Chr(10) ; 123 297 | ; . "`" 00: 59.999`": " TimestampToMilliseconds("00:59.999") Chr(10) ; 59999 298 | ; . "`" 00: 00.000`": " TimestampToMilliseconds("00:00.000") Chr(10) ; 0 299 | ; . "`" 1: 1.1`" (invalid format): " TimestampToMilliseconds("1:1.1") Chr(10) 300 | ; . "`" 60: 00.000`" (invalid minutes): " TimestampToMilliseconds("60:00.000") Chr(10) 301 | ; . "`" 00: 60.000`" (invalid seconds): " TimestampToMilliseconds("00:60.000") Chr(10) 302 | ; . "`" 00: 00.1000`" (invalid ms): " TimestampToMilliseconds("00:00.1000") Chr(10) 303 | ; . "`" 10: 20: 30`" (invalid format, missing ms): " TimestampToMilliseconds("10:20:30") Chr(10) 304 | ; . "`" - 01: 00: 00.000`" (invalid format): " TimestampToMilliseconds("-01:00:00.000") 305 | 306 | ; MsgBox "--- 时间戳转毫秒 ---" Chr(10) 307 | ; . "`" 01: 01.012`": " TimestampToMilliseconds("01:01.012") Chr(10) ; 61012 308 | ; . "`" 01: 01: 01.012`": " TimestampToMilliseconds("01:01:01.012") Chr(10) ; 3661012 309 | ; . "`" 25: 01: 01.012`": " TimestampToMilliseconds("25:01:01.012") Chr(10) ; 90061012 310 | ; . "`" 00: 00.123`": " TimestampToMilliseconds("00:00.123") Chr(10) ; 123 311 | ; . "`" 00: 59.999`": " TimestampToMilliseconds("00:59.999") Chr(10) ; 59999 312 | ; . "`" 00: 00.000`": " TimestampToMilliseconds("00:00.000") Chr(10) ; 0 313 | 314 | ; MsgBox "--- 纯秒数格式测试 (新功能) ---" Chr(10) 315 | ; . "`"0`": " TimestampToMilliseconds("0") Chr(10) ; 0 316 | ; . "`"1`": " TimestampToMilliseconds("1") Chr(10) ; 1000 317 | ; . "`"30`": " TimestampToMilliseconds("30") Chr(10) ; 30000 318 | ; . "`"60`": " TimestampToMilliseconds("60") Chr(10) ; 60000 319 | ; . "`"3600`": " TimestampToMilliseconds("3600") Chr(10) ; 3600000 320 | ; . "`"7200`": " TimestampToMilliseconds("7200") Chr(10) ; 7200000 321 | 322 | ; MsgBox "--- 边界值测试 ---" Chr(10) 323 | ; . "`"00:00.000`" (最小值): " TimestampToMilliseconds("00:00.000") Chr(10) ; 0 324 | ; . "`"59:59.999`" (MM:SS最大值): " TimestampToMilliseconds("59:59.999") Chr(10) ; 3599999 325 | ; . "`"23:59:59.999`" (HH:MM:SS最大值): " TimestampToMilliseconds("23:59:59.999") Chr(10) ; 86399999 326 | ; . "`"00:01.001`" (最小非零): " TimestampToMilliseconds("00:01.001") Chr(10) ; 1001 327 | ; . "`"12:34:56.789`" (常规值): " TimestampToMilliseconds("12:34:56.789") Chr(10) ; 45296789 328 | 329 | ; MsgBox "--- 错误格式测试 ---" Chr(10) 330 | ; . "`"`" (空字符串): " TimestampToMilliseconds("") Chr(10) ; 应该返回 "" 331 | ; . "`"abc`": " TimestampToMilliseconds("abc") Chr(10) ; 应该返回 "" 332 | ; . "`"12a`": " TimestampToMilliseconds("12a") Chr(10) ; 应该返回 "" 333 | ; . "`"-5`": " TimestampToMilliseconds("-5") Chr(10) ; 应该返回 "" 334 | ; . "`"1.5`" (小数点但不是时间): " TimestampToMilliseconds("1.5") Chr(10) ; 这现在是合法的SS.mmm格式! 335 | ; . "`"01.30`" (秒.毫秒超范围): " TimestampToMilliseconds("01.30") Chr(10) ; 这现在是合法的1秒300毫秒 336 | ; . "`"01:30:99`" (秒超范围): " TimestampToMilliseconds("01:30:99") Chr(10) ; 应该返回 "" 337 | 338 | ; MsgBox "--- 超出范围测试 ---" Chr(10) 339 | ; . "`"60:00.000`" (分钟超范围): " TimestampToMilliseconds("60:00.000") Chr(10) ; 应该返回 "" 340 | ; . "`"00:60.000`" (秒超范围): " TimestampToMilliseconds("00:60.000") Chr(10) ; 应该返回 "" 341 | ; . "`"00:00.1000`" (毫秒超范围): " TimestampToMilliseconds("00:00.1000") Chr(10) ; 应该返回 "" 342 | ; . "`"24:00:00.000`" (小时边界): " TimestampToMilliseconds("24:00:00.000") Chr(10) ; 应该返回 "" 343 | ; . "`"-01:00.000`" (负分钟): " TimestampToMilliseconds("-01:00.000") Chr(10) ; 应该返回 "" 344 | 345 | ; MsgBox "--- 格式错误测试 ---" Chr(10) 346 | ; . "`"1:1.1`" (格式不正确): " TimestampToMilliseconds("1:1.1") Chr(10) ; 应该返回 "" 347 | ; . "`"01:01.01.01`" (多余小数点): " TimestampToMilliseconds("01:01.01.01") Chr(10) ; 应该返回 "" 348 | ; . "`"01::01.000`" (双冒号): " TimestampToMilliseconds("01::01.000") Chr(10) ; 应该返回 "" 349 | ; . "`"01;01.000`" (错误分隔符): " TimestampToMilliseconds("01;01.000") Chr(10) ; 应该返回 "" 350 | ; . "`"01:01,000`" (逗号而非点): " TimestampToMilliseconds("01:01,000") Chr(10) ; 应该返回 "" 351 | 352 | ; MsgBox "--- 空格和特殊字符测试 ---" Chr(10) 353 | ; . "`" 01:01.012 `" (前后空格): " TimestampToMilliseconds(" 01:01.012 ") Chr(10) ; 应该返回 "" 354 | ; . "`"01 : 01.012`" (内部空格): " TimestampToMilliseconds("01 : 01.012") Chr(10) ; 应该返回 "" 355 | ; . "`"01:01 .012`" (点前空格): " TimestampToMilliseconds("01:01 .012") Chr(10) ; 应该返回 "" 356 | ; . "`"01:01. 012`" (点后空格): " TimestampToMilliseconds("01:01. 012") Chr(10) ; 应该返回 "" 357 | 358 | ; MsgBox "--- 过长输入测试 ---" Chr(10) 359 | ; . "`"01:01:01:01.000`" (四段时间): " TimestampToMilliseconds("01:01:01:01.000") Chr(10) ; 应该返回 "" 360 | ; . "`"001:001.000`" (前导零过多): " TimestampToMilliseconds("001:001.000") Chr(10) ; 应该返回 "" 361 | ; . "`"12345678`" (纯数字过长): " TimestampToMilliseconds("12345678") Chr(10) ; 应该是合法的 362 | ; . "`"00:00.0000`" (毫秒位数过多): " TimestampToMilliseconds("00:00.0000") Chr(10) ; 应该返回 "" 363 | 364 | ; MsgBox "--- 数字类型测试 ---" Chr(10) 365 | ; . "数字 123 (非字符串): " TimestampToMilliseconds(123) Chr(10) ; 应该返回 "" 366 | ; . "数字 0 (非字符串): " TimestampToMilliseconds(0) Chr(10) ; 应该返回 "" 367 | ; . "浮点数 12.5 (非字符串): " TimestampToMilliseconds(12.5) Chr(10) ; 应该返回 "" 368 | 369 | ; MsgBox "--- 极值测试 ---" Chr(10) 370 | ; . "`"99999999`" (超大秒数): " TimestampToMilliseconds("99999999") Chr(10) ; 应该是合法的 371 | ; . "`"999:59.999`" (超大分钟): " TimestampToMilliseconds("999:59.999") Chr(10) ; 应该是合法的 372 | ; . "`"999:59:59.999`" (超大小时): " TimestampToMilliseconds("999:59:59.999") Chr(10) ; 应该是合法的 373 | 374 | ; MsgBox "--- 前导零测试 ---" Chr(10) 375 | ; . "`"00000`" (多个前导零): " TimestampToMilliseconds("00000") Chr(10) ; 应该是0 376 | ; . "`"000123`" (前导零+数字): " TimestampToMilliseconds("000123") Chr(10) ; 应该是123000 377 | ; . "`"00:00.000`" (正常前导零): " TimestampToMilliseconds("00:00.000") Chr(10) ; 应该是0 378 | 379 | ; MsgBox "--- formatDuration (方便操作的函数) ---" Chr(10) 380 | ; . "90061012 ms using formatDuration: " FormatDuration(90061012) ; 25:01:01.012 381 | 382 | ; MsgBox "--- 新支持的灵活格式测试 ---" Chr(10) 383 | ; . "`"01:01:01`" (HH:MM:SS无毫秒): " TimestampToMilliseconds("01:01:01") Chr(10) ; 3661000 384 | ; . "`"01:01.1`" (MM:SS.1位毫秒): " TimestampToMilliseconds("01:01.1") Chr(10) ; 61100 385 | ; . "`"01:01.01`" (MM:SS.2位毫秒): " TimestampToMilliseconds("01:01.01") Chr(10) ; 61010 386 | ; . "`"01:01.001`" (MM:SS.3位毫秒): " TimestampToMilliseconds("01:01.001") Chr(10) ; 61001 387 | ; . "`"01.1`" (SS.1位毫秒): " TimestampToMilliseconds("01.1") Chr(10) ; 1100 388 | ; . "`"01.01`" (SS.2位毫秒): " TimestampToMilliseconds("01.01") Chr(10) ; 1010 389 | ; . "`"01.001`" (SS.3位毫秒): " TimestampToMilliseconds("01.001") Chr(10) ; 1001 390 | ; . "`"1.001`" (无前导零): " TimestampToMilliseconds("1.001") Chr(10) ; 1001 391 | ; . "`"12:34:56`" (HH:MM:SS无毫秒): " TimestampToMilliseconds("12:34:56") Chr(10) ; 45296000 392 | ; . "`"59:59`" (MM:SS无毫秒): " TimestampToMilliseconds("59:59") Chr(10) ; 3599000 393 | 394 | ; MsgBox "--- 边界值测试 ---" Chr(10) 395 | ; . "`"00:00.000`" (最小值): " TimestampToMilliseconds("00:00.000") Chr(10) ; 0 396 | ; . "`"59:59.999`" (MM:SS最大值): " TimestampToMilliseconds("59:59.999") Chr(10) ; 3599999 397 | ; . "`"23:59:59.999`" (HH:MM:SS最大值): " TimestampToMilliseconds("23:59:59.999") Chr(10) ; 86399999 398 | ; . "`"00:01.001`" (最小非零): " TimestampToMilliseconds("00:01.001") Chr(10) ; 1001 399 | ; . "`"12:34:56.789`" (常规值): " TimestampToMilliseconds("12:34:56.789") Chr(10) ; 45296789 400 | 401 | ; MsgBox "--- 时间段毫秒移除测试 ---" Chr(10) 402 | ; . "Input: `"09:19.237-14:13.934`" -> Output: `"" RemoveMillisecondsFromTimeRange("09:19.237-14:13.934") "`"" Chr(10) 403 | ; . "Input: `"19:33.470∞25:10.986`" -> Output: `"" RemoveMillisecondsFromTimeRange("19:33.470∞25:10.986") "`"" Chr(10) 404 | ; . "Input: `"01:23:45.678-02:34:56.789`" -> Output: `"" RemoveMillisecondsFromTimeRange("01:23:45.678-02:34:56.789") "`"" Chr(10) 405 | ; . "Input: `"12:34.567∞23:45.890`" -> Output: `"" RemoveMillisecondsFromTimeRange("12:34.567∞23:45.890") "`"" Chr(10) 406 | ; . "Input: `"05:15-10:30`" (已无毫秒) -> Output: `"" RemoveMillisecondsFromTimeRange("05:15-10:30") "`"" Chr(10) 407 | ; . "Input: `"08:20∞12:40`" (已无毫秒) -> Output: `"" RemoveMillisecondsFromTimeRange("08:20∞12:40") "`"" Chr(10) 408 | ; . "Input: `"01:23.456`" (单个时间戳) -> Output: `"" RemoveMillisecondsFromTimeRange("01:23.456") "`"" Chr(10) 409 | ; . "Input: `"02:34`" (单个时间戳无毫秒) -> Output: `"" RemoveMillisecondsFromTimeRange("02:34") "`"" Chr(10) 410 | ; . "Input: `"`" (空字符串) -> Output: `"" RemoveMillisecondsFromTimeRange("") "`"" Chr(10) -------------------------------------------------------------------------------- /lib/entity/Config.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2.0 2 | #Include ../sqlite/SqliteControl.ahk 3 | #Include ../MyTool.ahk 4 | 5 | Class Config { 6 | ; 其他微调设置 7 | PotplayerPath { 8 | get => GetKey("path") 9 | set => UpdateOrInsertConfig("path", Value) 10 | } 11 | PotplayerProcessName { 12 | get => GetNameForPath(this.PotplayerPath) 13 | } 14 | 15 | IsStop { 16 | get => GetKey("is_stop") 17 | set => UpdateOrInsertConfig("is_stop", Value) 18 | } 19 | 20 | ReduceTime { 21 | get => GetKey("reduce_time") 22 | set => UpdateOrInsertConfig("reduce_time", Value) 23 | } 24 | 25 | NoteAppName { 26 | get => GetKey("app_name") 27 | set => UpdateOrInsertConfig("app_name", Value) 28 | } 29 | 30 | MarkdownPathIsEncode { 31 | get => GetKey("path_is_encode") 32 | set => UpdateOrInsertConfig("path_is_encode", Value) 33 | } 34 | 35 | MarkdownRemoveSuffixOfVideoFile { 36 | get => GetKey("remove_suffix_of_video_file") 37 | set => UpdateOrInsertConfig("remove_suffix_of_video_file", Value) 38 | } 39 | 40 | UrlProtocol { 41 | get => GetKey("url_protocol") 42 | set => UpdateOrInsertConfig("url_protocol", Value) 43 | } 44 | 45 | SendImageDelays { 46 | get => GetKey("send_image_delays") 47 | set => UpdateOrInsertConfig("send_image_delays", Value) 48 | } 49 | 50 | ; ============回链的设置================ 51 | SubtitleTemplate { 52 | get => GetKey("subtitle_template") 53 | set => UpdateOrInsertConfig("subtitle_template", Value) 54 | } 55 | ; backlink_template 56 | MarkdownTemplate { 57 | get => GetKey("template") 58 | set => UpdateOrInsertConfig("template", Value) 59 | } 60 | 61 | MarkdownImageTemplate { 62 | get => GetKey("image_template") 63 | set => UpdateOrInsertConfig("image_template", Value) 64 | } 65 | 66 | MarkdownTitle { 67 | get => GetKey("title") 68 | set => UpdateOrInsertConfig("title", Value) 69 | } 70 | 71 | ; ============回链快捷键相关================ 72 | HotkeySubtitle { 73 | get => GetKey("hotkey_subtitle") 74 | set => UpdateOrInsertConfig("hotkey_subtitle", Value) 75 | } 76 | HotkeySubtitlePreviousOnce{ 77 | get => GetKey("hotkey_subtitle_previous_once") 78 | set => UpdateOrInsertConfig("hotkey_subtitle_previous_once", Value) 79 | } 80 | HotkeySubtitleCurrentOnce { 81 | get => GetKey("hotkey_subtitle_current_once") 82 | set => UpdateOrInsertConfig("hotkey_subtitle_current_once", Value) 83 | } 84 | HotkeySubtitleNextOnce { 85 | get => GetKey("hotkey_subtitle_next_once") 86 | set => UpdateOrInsertConfig("hotkey_subtitle_next_once", Value) 87 | } 88 | HotkeySubtitlePreviousLoop { 89 | get => GetKey("hotkey_subtitle_previous_loop") 90 | set => UpdateOrInsertConfig("hotkey_subtitle_previous_loop", Value) 91 | } 92 | HotkeySubtitleCurrentLoop { 93 | get => GetKey("hotkey_subtitle_current_loop") 94 | set => UpdateOrInsertConfig("hotkey_subtitle_current_loop", Value) 95 | } 96 | HotkeySubtitleNextLoop { 97 | get => GetKey("hotkey_subtitle_next_loop") 98 | set => UpdateOrInsertConfig("hotkey_subtitle_next_loop", Value) 99 | } 100 | HotkeyUserNote { 101 | get => GetKey("hotkey_user_note") 102 | set => UpdateOrInsertConfig("hotkey_user_note", Value) 103 | } 104 | HotkeyBacklink { 105 | get => GetKey("hotkey_backlink") 106 | set => UpdateOrInsertConfig("hotkey_backlink", Value) 107 | } 108 | HotkeyIamgeBacklink { 109 | get => GetKey("hotkey_iamge_backlink") 110 | set => UpdateOrInsertConfig("hotkey_iamge_backlink", Value) 111 | } 112 | HotkeyScreenshotToolHotkeys { 113 | get => GetKey("hotkey_image_screenshot_tool_hotkeys") 114 | set => UpdateOrInsertConfig("hotkey_image_screenshot_tool_hotkeys", Value) 115 | } 116 | HotkeyImageEdit { 117 | get => GetKey("hotkey_image_edit") 118 | set => UpdateOrInsertConfig("hotkey_image_edit", Value) 119 | } 120 | ImageEditDetectionTime { 121 | get => GetKey("image_edit_detection_time") 122 | set => UpdateOrInsertConfig("image_edit_detection_time", Value) 123 | } 124 | HotkeyAbFragment { 125 | get => GetKey("hotkey_ab_fragment") 126 | set => UpdateOrInsertConfig("hotkey_ab_fragment", Value) 127 | } 128 | 129 | AbFragmentDetectionDelays { 130 | get => GetKey("ab_fragment_detection_delays") 131 | set => UpdateOrInsertConfig("ab_fragment_detection_delays", Value) 132 | } 133 | 134 | LoopAbFragment { 135 | get => GetKey("loop_ab_fragment") 136 | set => UpdateOrInsertConfig("loop_ab_fragment", Value) 137 | } 138 | HotkeyAbCirculation { 139 | get => GetKey("hotkey_ab_circulation") 140 | set => UpdateOrInsertConfig("hotkey_ab_circulation", Value) 141 | } 142 | } -------------------------------------------------------------------------------- /lib/entity/MediaData.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2.0 2 | 3 | class MediaData{ 4 | __New(path, time, subtitle) { 5 | this.Path := path 6 | this.Time := time 7 | this.Subtitle := subtitle 8 | } 9 | } -------------------------------------------------------------------------------- /lib/gui/Gui.ahk: -------------------------------------------------------------------------------- 1 | #Requires Autohotkey v2 2 | #Include i18n\I18n.ahk 3 | #Include ..\MyTool.ahk 4 | 5 | ;AutoGUI creator: Alguimist autohotkey.com/boards/viewtopic.php?f=64&t=89901 6 | ;AHKv2converter creator: github.com/mmikeww/AHK-v2-script-converter 7 | ;EasyAutoGUI-AHKv2 github.com/samfisherirl/Easy-Auto-GUI-for-AHK-v2 8 | 9 | creationGui() { 10 | i18n_local := I18n(A_WorkingDir "\lib\gui\i18n") 11 | 12 | guiData := Constructor(i18n_local) 13 | 14 | ; =======界面设置========= 15 | guiData.myGui.OnEvent('Close', (*) => guiData.myGui.Hide()) 16 | guiData.myGui.OnEvent('Escape', (*) => guiData.myGui.Hide()) 17 | guiData.myGui.Title := "markdown2potpalyer" 18 | 19 | ; =======托盘菜单========= 20 | myMenu := A_TrayMenu 21 | 22 | window_size := "w477 h755" 23 | 24 | myMenu.Add("&Open", (*) => guiData.myGui.Show(window_size)) 25 | myMenu.Default := "&Open" 26 | myMenu.ClickCount := 2 27 | 28 | myMenu.Rename("&Open", i18n_local.Gui_open) 29 | myMenu.Rename("E&xit", i18n_local.Gui_exit) 30 | myMenu.Rename("&Pause Script", i18n_local.Gui_pause_script) 31 | myMenu.Rename("&Suspend Hotkeys", i18n_local.Gui_suspend_hotkeys) 32 | 33 | return guiData 34 | } 35 | 36 | Constructor(i18n_local) { 37 | guiData := { 38 | myGui: {}, 39 | controls: {} 40 | } 41 | 42 | myGui := Gui() 43 | myGui.BackColor := "0xffffff" 44 | Tab := myGui.Add("Tab3", "x0 y0 w493 h725", [i18n_local.Gui_Tab_backlink_setting]) 45 | Tab.UseTab(1) 46 | 47 | myGui.Add("Text", "x40 y24 w132 h20", i18n_local.Gui_potplayer_path) 48 | ; =======模板========= 49 | Edit_potplayer := myGui.Add("Edit", "x160 y22 w215 h25", "C:\Program Files\DAUM\PotPlayer\PotPlayerMini64.exe") 50 | Button_potplayer := myGui.Add("Button", "x380 y22 w90 h30", i18n_local.Gui_choose_potplayer) 51 | 52 | myGui.Add("Text", "x40 y49 w132 h25", i18n_local.Gui_note_names) 53 | Edit_note_app_name := myGui.Add("Edit", "x160 y48 w190 h45 +Multi", "Obsidian.exe`nTypora.exe") 54 | myGui.SetFont("s8") 55 | myGui.Add("Text", "x160 y100 w150 h20", i18n_local.Gui_note_names_tips) 56 | myGui.SetFont() 57 | 58 | myGui.Add("Text", "x40 y125 w150 h20", i18n_local.Gui_subtitle_template_name) 59 | Edit_subtitle_template := myGui.Add("Edit", "x160 y125 w190 h30 +Multi", "字幕:`n{subtitle}") 60 | Button_srt_to_backlink_mdfile := myGui.Add("Button", "x380 y125 w90 h20", i18n_local.Gui_srt_to_backlink) 61 | myGui.SetFont("s8") 62 | myGui.Add("Text", "x40 y160 w115 h20", i18n_local.Gui_subtitle_navigation_single) 63 | hk_subtitle_previous_once := myGui.Add("Hotkey", "x160 y160 w90 h13", "^j") 64 | hk_subtitle_current_once := myGui.Add("Hotkey", "x260 y160 w90 h13", "^k") 65 | hk_subtitle_next_once := myGui.Add("Hotkey", "x360 y160 w90 h13", "^l") 66 | myGui.Add("Text", "x40 y175 w115 h20", i18n_local.Gui_subtitle_navigation_loop) 67 | hk_subtitle_previous_loop := myGui.Add("Hotkey", "x160 y175 w90 h13", "^!j") 68 | hk_subtitle_current_loop := myGui.Add("Hotkey", "x260 y175 w90 h13", "^!k") 69 | hk_subtitle_next_loop := myGui.Add("Hotkey", "x360 y175 w90 h13", "^!l") 70 | myGui.SetFont() 71 | 72 | myGui.Add("Text", "x40 y195 w150 h23", i18n_local.Gui_backlink_name) 73 | Edit_title := myGui.Add("Edit", "x160 y195 w190 h17", "{name} | {time}") 74 | 75 | myGui.Add("Text", "x40 y218 w150 h20", i18n_local.Gui_backlink_template) 76 | Edit_markdown_template := myGui.Add("Edit", "x160 y218 w190 h40 +Multi", "`n视频:{title}`n") 77 | 78 | myGui.Add("Text", "x40 y265 w110 h40", i18n_local.Gui_image_backlink_tempalte_name) 79 | Edit_image_template := myGui.Add("Edit", "x160 y265 w190 h50 +Multi", "`n图片:{image}`n视频:{title}`n") 80 | 81 | myGui.Add("Text", "x40 y320 w111", i18n_local.Gui_send_image_delays) 82 | Edit_send_image_delays := myGui.Add("Edit", "x160 y320 w120 h17") 83 | myGui.Add("Text", "x288 y320 w22 h20", "ms") 84 | 85 | ; =======快捷键========= 86 | myGui.Add("Text", "x40 y343 w105 h20", i18n_local.Gui_hotkey_backlink) 87 | hk_backlink := myGui.Add("Hotkey", "x160 y343 w120 h17", "!g") 88 | 89 | myGui.Add("Text", "x40 y365 w105 h20", i18n_local.Gui_hotkey_user_note) 90 | hk_user_note := myGui.Add("Hotkey", "x160 y365 w120 h17", "!n") 91 | 92 | myGui.Add("Text", "x40 y387 w105 h20", i18n_local.Gui_hotkey_subtitle) 93 | hk_subtitle := myGui.Add("Hotkey", "x160 y387 w120 h17", "!t") 94 | 95 | myGui.SetFont("s8") 96 | myGui.Add("Text", "x40 y409 w105", i18n_local.Gui_hotkey_image_and_backlink) 97 | hk_image_backlink := myGui.Add("Hotkey", "x160 y409 w120 h17", "^!g") 98 | myGui.Add("Text", "x40 y431 w105 h32", i18n_local.Gui_hotkey_screenshot_tool_hotkeys) 99 | myGui.SetFont() 100 | hk_image_screenshot_tool_hotkeys := myGui.Add("Hotkey", "x160 y431 w120 h17", "!a") 101 | myGui.Add("Text", "x40 y453 w105 h32", i18n_local.Gui_hotkey_image_edit_and_backlink) 102 | hk_image_edit := myGui.Add("Hotkey", "x160 y453 w120 h17", "!a") 103 | myGui.SetFont("s8") 104 | myGui.Add("Text", "x40 y475 w105 h32", i18n_local.Gui_image_edit_detection_time) 105 | myGui.SetFont() 106 | Edit_image_edit_detection_time := myGui.Add("Edit", "x160 y475 w120 h17", "20") 107 | myGui.Add("Text", "x288 y475 w31 h20", "s") 108 | 109 | myGui.Add("Text", "x40 y497 w114 h20", i18n_local.Gui_hotkey_ab_fragment) 110 | hk_ab_fragment := myGui.Add("Hotkey", "x160 y497 w120 h17", "F1") 111 | 112 | myGui.SetFont("s8") 113 | myGui.Add("Text", "x40 y519 w98 h20", i18n_local.Gui_hotkey_ab_fragment_detection_delays) 114 | myGui.SetFont() 115 | Edit_ab_fragment_detection_delays := myGui.Add("Edit", "x160 y519 w120 h17", "1000") 116 | myGui.Add("Text", "x288 y519 w31 h20", "ms") 117 | 118 | CheckBox_loop_ab_fragment := myGui.Add("CheckBox", "x160 y541 w120 h20", i18n_local.Gui_is_loop_ab_fragment) 119 | 120 | myGui.Add("Text", "x40 y563 w105 h12", i18n_local.Gui_hotkey_ab_circulation) 121 | hk_ab_circulation := myGui.Add("Hotkey", "x160 y563 w190 h17") 122 | 123 | ; =======其他设置========= 124 | myGui.SetFont("s8") 125 | myGui.Add("Text", "x40 y585 w105 h36", i18n_local.Gui_edit_url_protocol) 126 | Edit_url_protocol := myGui.Add("Edit", "x160 y585 w190 h17", "jv://open") 127 | myGui.SetFont() 128 | 129 | myGui.Add("Text", "x40 y607 w150 h20", i18n_local.Gui_reduce_time) 130 | Edit_reduce_time := myGui.Add("Edit", "x160 y607 w120 h17", "0") 131 | 132 | CheckBox_is_stop := myGui.Add("CheckBox", "x160 y629 w69 h17", i18n_local.Gui_is_stop) 133 | CheckBox_remove_suffix_of_video_file := myGui.Add("CheckBox", "x160 y651 w250 h17", i18n_local.Gui_remove_suffix_of_video_file) 134 | CheckBox_path_is_encode := myGui.Add("CheckBox", "x160 y673 w120 h17", i18n_local.Gui_is_path_encode) 135 | CheckBox_bootup := myGui.Add("CheckBox", "x160 y695 w120 h17", i18n_local.Gui_bootup) 136 | 137 | Tab.UseTab() 138 | myGui.Add("Text", "x332 y725 w150 h12", i18n_local.Gui_current_version ":0.2.7") 139 | myGui.Add("Link", "x332 y740 w150 h12", i18n_local.Gui_latest_version ":" getLatestVersionFromGithub() "") 140 | ; myGui.Add("Link", "x432 y755 w48 h12", "去下载") 141 | 142 | guiData.myGui := myGui 143 | guiData.controls.Edit_potplayer := Edit_potplayer 144 | guiData.controls.Button_potplayer := Button_potplayer 145 | guiData.controls.Edit_note_app_name := Edit_note_app_name 146 | guiData.controls.Edit_subtitle_template := Edit_subtitle_template 147 | guiData.controls.Button_srt_to_backlink_mdfile := Button_srt_to_backlink_mdfile 148 | guiData.controls.hk_subtitle_previous_once := hk_subtitle_previous_once 149 | guiData.controls.hk_subtitle_current_once := hk_subtitle_current_once 150 | guiData.controls.hk_subtitle_next_once := hk_subtitle_next_once 151 | guiData.controls.hk_subtitle_previous_loop := hk_subtitle_previous_loop 152 | guiData.controls.hk_subtitle_current_loop := hk_subtitle_current_loop 153 | guiData.controls.hk_subtitle_next_loop := hk_subtitle_next_loop 154 | guiData.controls.Edit_title := Edit_title 155 | guiData.controls.Edit_markdown_template := Edit_markdown_template 156 | guiData.controls.Edit_image_template := Edit_image_template 157 | guiData.controls.Edit_send_image_delays := Edit_send_image_delays 158 | guiData.controls.hk_backlink := hk_backlink 159 | guiData.controls.hk_user_note := hk_user_note 160 | guiData.controls.hk_image_backlink := hk_image_backlink 161 | guiData.controls.hk_image_screenshot_tool_hotkeys := hk_image_screenshot_tool_hotkeys 162 | guiData.controls.hk_image_edit := hk_image_edit 163 | guiData.controls.Edit_image_edit_detection_time := Edit_image_edit_detection_time 164 | guiData.controls.hk_ab_fragment := hk_ab_fragment 165 | guiData.controls.Edit_ab_fragment_detection_delays := Edit_ab_fragment_detection_delays 166 | guiData.controls.CheckBox_loop_ab_fragment := CheckBox_loop_ab_fragment 167 | guiData.controls.hk_ab_circulation := hk_ab_circulation 168 | guiData.controls.Edit_url_protocol := Edit_url_protocol 169 | guiData.controls.Edit_reduce_time := Edit_reduce_time 170 | guiData.controls.CheckBox_is_stop := CheckBox_is_stop 171 | guiData.controls.CheckBox_remove_suffix_of_video_file := CheckBox_remove_suffix_of_video_file 172 | guiData.controls.CheckBox_path_is_encode := CheckBox_path_is_encode 173 | guiData.controls.CheckBox_bootup := CheckBox_bootup 174 | 175 | return guiData 176 | } -------------------------------------------------------------------------------- /lib/gui/GuiControl.ahk: -------------------------------------------------------------------------------- 1 | #Requires Autohotkey v2 2 | #Include Gui.ahk ; 加载Gui 3 | #Include ..\BootUp.ahk 4 | #Include ..\..\markdown2potplayer.ahk 5 | 6 | InitGui(app_config, potplayer_control) { 7 | guiData := creationGui() 8 | 9 | ; 回显:Potplayer路径 10 | guiData.controls.Edit_potplayer.Value := app_config.PotplayerPath 11 | ; 点击选择potplayer路径 12 | guiData.controls.Button_potplayer.OnEvent("Click", SelectPotplayerProgram) 13 | SelectPotplayerProgram(*) { 14 | SelectedFile := FileSelect(1, , "Open a file", "Text Documents (*.exe)") 15 | if SelectedFile { 16 | guiData.controls.edit_potplayer.Value := SelectedFile 17 | } 18 | } 19 | guiData.controls.Button_potplayer.OnEvent("LoseFocus", (*) => app_config.PotplayerPath := guiData.controls.edit_potplayer.Value) 20 | 21 | ; 回显:笔记软件名称 22 | guiData.controls.Edit_note_app_name.Value := app_config.NoteAppName 23 | guiData.controls.Edit_note_app_name.OnEvent("LoseFocus", (*) => app_config.NoteAppName := guiData.controls.Edit_note_app_name.Value) 24 | 25 | ; 回显:字幕模板 26 | guiData.controls.Edit_subtitle_template.Value := app_config.SubtitleTemplate 27 | guiData.controls.Edit_subtitle_template.OnEvent("LoseFocus", (*) => app_config.SubtitleTemplate := guiData.controls.Edit_subtitle_template.Value) 28 | 29 | ; srt转回链md文件 30 | guiData.controls.Button_srt_to_backlink_mdfile.OnEvent("Click", SelectSrtFiles) 31 | SelectSrtFiles(*) { 32 | SelectedFiles := FileSelect("M3", , "Open a file", "Text Documents (*.srt)") 33 | if SelectedFiles.Length = 0 { 34 | return 35 | } 36 | 37 | InputBoxObj := InputBox("Please enter the video file extension (default: .mp4)", "tips", "", ".mp4") 38 | videoFileExtension := ".mp4" 39 | if InputBoxObj.Result = "Cancel" 40 | return 41 | else 42 | videoFileExtension := InputBoxObj.Value 43 | 44 | for FileName in SelectedFiles { 45 | subtitles := SubtitlesDataFromSrt(FileName) 46 | if (subtitles) { 47 | md_content := "" 48 | for subtitle in subtitles { 49 | SplitPath FileName, &name, &dir, &ext, &name_no_ext, &drive 50 | videoFilePath := dir "\" name_no_ext videoFileExtension 51 | 52 | media_data := MediaData(videoFilePath, MillisecondsToTimestamp(subtitle.timeStart), subtitle.subtitle) 53 | rendered_template := RenderSrtTemplate(app_config.SubtitleTemplate, media_data, subtitle) 54 | rendered_template := RenderTemplate(rendered_template, media_data) "`r`n`r`n" 55 | 56 | md_content := md_content rendered_template 57 | } 58 | if (md_content != "") { 59 | SplitPath FileName, &name, &dir, &ext, &name_no_ext, &drive 60 | md_path := dir "\" name_no_ext ".md" 61 | 62 | if (FileExist(md_path)) { 63 | result := MsgBox("file:" md_path " already exists, overwrite?", , "YesNo") 64 | if (result == "No") 65 | return 66 | FileDelete(md_path) 67 | } 68 | FileEncoding("CP0") 69 | FileAppend(md_content, md_path) 70 | 71 | ToolTip("file: " md_path " generated successfully!") 72 | SetTimer () => ToolTip(), -2000 73 | } 74 | } 75 | } 76 | } 77 | 78 | ; 字幕导航 79 | ; 回显:使用字幕文件定位上一句 80 | guiData.controls.hk_subtitle_previous_once.Value := app_config.HotkeySubtitlePreviousOnce 81 | guiData.controls.hk_subtitle_previous_once.OnEvent("Change", Update_Hk_Subtitle_Previous_Once) 82 | Update_Hk_Subtitle_Previous_Once(GuiCtrlObj, Info) { 83 | RefreshHotkeyWithoutUp(app_config.HotkeySubtitlePreviousOnce, GuiCtrlObj.Value, (*) => SubtitleFragmentPlay("prev", "single")) 84 | app_config.HotkeySubtitlePreviousOnce := GuiCtrlObj.Value 85 | } 86 | ; 回显:使用字幕文件定位当前句 87 | guiData.controls.hk_subtitle_current_once.Value := app_config.HotkeySubtitleCurrentOnce 88 | guiData.controls.hk_subtitle_current_once.OnEvent("Change", Update_Hk_Subtitle_Current_Once) 89 | Update_Hk_Subtitle_Current_Once(GuiCtrlObj, Info) { 90 | RefreshHotkeyWithoutUp(app_config.HotkeySubtitleCurrentOnce, GuiCtrlObj.Value, (*) => SubtitleFragmentPlay("current", "single")) 91 | app_config.HotkeySubtitleCurrentOnce := GuiCtrlObj.Value 92 | } 93 | ; 回显:使用字幕文件定位下一句 94 | guiData.controls.hk_subtitle_next_once.Value := app_config.HotkeySubtitleNextOnce 95 | guiData.controls.hk_subtitle_next_once.OnEvent("Change", Update_Hk_Subtitle_Next_Once) 96 | Update_Hk_Subtitle_Next_Once(GuiCtrlObj, Info) { 97 | RefreshHotkeyWithoutUp(app_config.HotkeySubtitleNextOnce, GuiCtrlObj.Value, (*) => SubtitleFragmentPlay("next", "single")) 98 | app_config.HotkeySubtitleNextOnce := GuiCtrlObj.Value 99 | } 100 | ; 回显:使用字幕文件定位上一句(循环) 101 | guiData.controls.hk_subtitle_previous_loop.Value := app_config.HotkeySubtitlePreviousLoop 102 | guiData.controls.hk_subtitle_previous_loop.OnEvent("Change", Update_Hk_Subtitle_Previous_Loop) 103 | Update_Hk_Subtitle_Previous_Loop(GuiCtrlObj, Info) { 104 | RefreshHotkeyWithoutUp(app_config.HotkeySubtitlePreviousLoop, GuiCtrlObj.Value, (*) => SubtitleFragmentPlay("prev", "loop")) 105 | app_config.HotkeySubtitlePreviousLoop := GuiCtrlObj.Value 106 | } 107 | ; 回显:使用字幕文件定位当前句(循环) 108 | guiData.controls.hk_subtitle_current_loop.Value := app_config.HotkeySubtitleCurrentLoop 109 | guiData.controls.hk_subtitle_current_loop.OnEvent("Change", Update_Hk_Subtitle_Current_Loop) 110 | Update_Hk_Subtitle_Current_Loop(GuiCtrlObj, Info) { 111 | RefreshHotkeyWithoutUp(app_config.HotkeySubtitleCurrentLoop, GuiCtrlObj.Value, (*) => SubtitleFragmentPlay("current", "loop")) 112 | app_config.HotkeySubtitleCurrentLoop := GuiCtrlObj.Value 113 | } 114 | ; 回显:使用字幕文件定位下一句(循环) 115 | guiData.controls.hk_subtitle_next_loop.Value := app_config.HotkeySubtitleNextLoop 116 | guiData.controls.hk_subtitle_next_loop.OnEvent("Change", Update_Hk_Subtitle_Next_Loop) 117 | Update_Hk_Subtitle_Next_Loop(GuiCtrlObj, Info) { 118 | RefreshHotkeyWithoutUp(app_config.HotkeySubtitleNextLoop, GuiCtrlObj.Value, (*) => SubtitleFragmentPlay("next", "loop")) 119 | app_config.HotkeySubtitleNextLoop := GuiCtrlObj.Value 120 | } 121 | 122 | ; 回显:回链标题 123 | guiData.controls.Edit_title.Value := app_config.MarkdownTitle 124 | guiData.controls.Edit_title.OnEvent("LoseFocus", (*) => app_config.MarkdownTitle := guiData.controls.Edit_title.Value) 125 | 126 | ; 回显:回链模板 127 | guiData.controls.Edit_markdown_template.Value := app_config.MarkdownTemplate 128 | guiData.controls.Edit_markdown_template.OnEvent("LoseFocus", (*) => app_config.MarkdownTemplate := guiData.controls.Edit_markdown_template.Value) 129 | 130 | ; 回显:图片回链模板 131 | guiData.controls.Edit_image_template.Value := app_config.MarkdownImageTemplate 132 | guiData.controls.Edit_image_template.OnEvent("LoseFocus", (*) => app_config.MarkdownImageTemplate := guiData.controls.Edit_image_template.Value) 133 | 134 | ; 回显:发送图片延迟 135 | guiData.controls.Edit_send_image_delays.Value := app_config.SendImageDelays 136 | guiData.controls.Edit_send_image_delays.OnEvent("LoseFocus", (*) => app_config.SendImageDelays := guiData.controls.Edit_send_image_delays.Value) 137 | 138 | ; 回显: 回链快捷键 139 | guiData.controls.hk_backlink.Value := app_config.HotkeyBacklink 140 | guiData.controls.hk_backlink.OnEvent("Change", Update_Hk_Backlink) 141 | Update_Hk_Backlink(GuiCtrlObj, Info) { 142 | RefreshHotkey(app_config.HotkeyBacklink, GuiCtrlObj.Value, Potplayer2Obsidian) 143 | app_config.HotkeyBacklink := GuiCtrlObj.Value 144 | } 145 | 146 | ; 回显: 用户笔记快捷键 147 | guiData.controls.hk_user_note.Value := app_config.HotkeyUserNote 148 | guiData.controls.hk_user_note.OnEvent("Change", Update_Hk_UserNote) 149 | Update_Hk_UserNote(GuiCtrlObj, Info) { 150 | RefreshHotkey(app_config.HotkeyUserNote, GuiCtrlObj.Value, Potplayer2Obsidian) 151 | app_config.HotkeyUserNote := GuiCtrlObj.Value 152 | } 153 | 154 | ; 回显: 图片+回链快捷键 155 | guiData.controls.hk_image_backlink.Value := app_config.HotkeyIamgeBacklink 156 | guiData.controls.hk_image_backlink.OnEvent("Change", Update_Hk_Image_Backlink) 157 | Update_Hk_Image_Backlink(GuiCtrlObj, Info) { 158 | RefreshHotkey(app_config.HotkeyIamgeBacklink, GuiCtrlObj.Value, Potplayer2ObsidianImage) 159 | app_config.HotkeyIamgeBacklink := GuiCtrlObj.Value 160 | } 161 | 162 | ; 回显: 第三方截图工具的快捷键 163 | guiData.controls.hk_image_screenshot_tool_hotkeys.Value := app_config.HotkeyScreenshotToolHotkeys 164 | guiData.controls.hk_image_screenshot_tool_hotkeys.OnEvent("Change", Update_Hk_Image_Screenshot_Tool_Hotkeys) 165 | Update_Hk_Image_Screenshot_Tool_Hotkeys(GuiCtrlObj, Info) { 166 | app_config.HotkeyScreenshotToolHotkeys := GuiCtrlObj.Value 167 | } 168 | 169 | ; 回显: 图片编辑快捷键 170 | guiData.controls.hk_image_edit.Value := app_config.HotkeyImageEdit 171 | guiData.controls.hk_image_edit.OnEvent("Change", Update_Hk_Image_Edit) 172 | Update_Hk_Image_Edit(GuiCtrlObj, Info) { 173 | RefreshHotkey(app_config.HotkeyImageEdit, GuiCtrlObj.Value, Potplayer2ObsidianImage) 174 | app_config.HotkeyImageEdit := GuiCtrlObj.Value 175 | } 176 | 177 | ; 回显: 检测图片编辑延迟 178 | guiData.controls.Edit_image_edit_detection_time.Value := app_config.ImageEditDetectionTime 179 | guiData.controls.Edit_image_edit_detection_time.OnEvent("Change", Update_Image_Edit_Detection_Time) 180 | Update_Image_Edit_Detection_Time(GuiCtrlObj, Info) { 181 | app_config.ImageEditDetectionTime := GuiCtrlObj.Value 182 | } 183 | 184 | ; 回显: ab片段快捷键 185 | guiData.controls.hk_ab_fragment.Value := app_config.HotkeyAbFragment 186 | guiData.controls.hk_ab_fragment.OnEvent("Change", Update_Hk_Ab_Fragment) 187 | Update_Hk_Ab_Fragment(GuiCtrlObj, Info) { 188 | RefreshHotkey(app_config.HotkeyAbFragment, GuiCtrlObj.Value, Potplayer2ObsidianFragment) 189 | app_config.HotkeyAbFragment := GuiCtrlObj.Value 190 | } 191 | 192 | ; 回显: ab片段检测延迟 193 | guiData.controls.Edit_ab_fragment_detection_delays.Value := app_config.AbFragmentDetectionDelays 194 | guiData.controls.Edit_ab_fragment_detection_delays.OnEvent("Change", (*) => app_config.AbFragmentDetectionDelays := guiData.controls.Edit_ab_fragment_detection_delays.Value) 195 | 196 | ; 回显: 是否 循环ab片段 197 | guiData.controls.CheckBox_loop_ab_fragment.Value := app_config.LoopAbFragment 198 | guiData.controls.CheckBox_loop_ab_fragment.OnEvent("Click", (*) => app_config.LoopAbFragment := guiData.controls.CheckBox_loop_ab_fragment.Value) 199 | 200 | ; 回显: ab循环快捷键 201 | guiData.controls.hk_ab_circulation.Value := app_config.HotkeyAbCirculation 202 | guiData.controls.hk_ab_circulation.OnEvent("Change", Update_Hk_Ab_Circulation) 203 | Update_Hk_Ab_Circulation(GuiCtrlObj, Info) { 204 | RefreshHotkey(app_config.HotkeyAbCirculation, GuiCtrlObj.Value, Potplayer2ObsidianFragment) 205 | app_config.HotkeyAbCirculation := GuiCtrlObj.Value 206 | } 207 | 208 | ;=================其他设置================= 209 | ; 回显: Url协议 210 | guiData.controls.Edit_url_protocol.Value := app_config.UrlProtocol 211 | guiData.controls.Edit_url_protocol.OnEvent("LoseFocus", (*) => app_config.UrlProtocol := guiData.controls.Edit_url_protocol.Value) 212 | 213 | ; 回显:减少的时间 214 | guiData.controls.Edit_reduce_time.Value := app_config.ReduceTime 215 | guiData.controls.Edit_reduce_time.OnEvent("LoseFocus", (*) => app_config.ReduceTime := guiData.controls.Edit_reduce_time.Value) 216 | 217 | ; 回显:是否暂停 218 | guiData.controls.CheckBox_is_stop.Value := app_config.IsStop 219 | guiData.controls.CheckBox_is_stop.OnEvent("Click", (*) => app_config.IsStop := guiData.controls.CheckBox_is_stop.Value) 220 | 221 | guiData.controls.CheckBox_remove_suffix_of_video_file.Value := app_config.MarkdownRemoveSuffixOfVideoFile 222 | guiData.controls.CheckBox_remove_suffix_of_video_file.OnEvent("Click", (*) => app_config.MarkdownRemoveSuffixOfVideoFile := guiData.controls.CheckBox_remove_suffix_of_video_file.Value) 223 | 224 | ; 回显:路径是否编码 225 | guiData.controls.CheckBox_path_is_encode.Value := app_config.MarkdownPathIsEncode 226 | guiData.controls.checkBox_path_is_encode.OnEvent("Click", (*) => app_config.MarkdownPathIsEncode := guiData.controls.checkBox_path_is_encode.Value) 227 | 228 | ; 回显: 是否开机启动 229 | guiData.controls.CheckBox_bootup.Value := get_boot_up() 230 | guiData.controls.CheckBox_bootup.OnEvent("Click", (*) => adaptive_bootup()) 231 | } -------------------------------------------------------------------------------- /lib/gui/GuiExample.ahk: -------------------------------------------------------------------------------- 1 | #Requires Autohotkey v2 2 | #SingleInstance force 3 | 4 | ;AutoGUI creator: Alguimist autohotkey.com/boards/viewtopic.php?f=64&t=89901 5 | ;AHKv2converter creator: github.com/mmikeww/AHK-v2-script-converter 6 | ;EasyAutoGUI-AHKv2 github.com/samfisherirl/Easy-Auto-GUI-for-AHK-v2 7 | 8 | if A_LineFile = A_ScriptFullPath && !A_IsCompiled { 9 | myGui := Constructor() 10 | 11 | ; =======界面设置========= 12 | myGui.OnEvent('Close', (*) => myGui.Hide()) 13 | myGui.OnEvent('Escape', (*) => myGui.Hide()) 14 | myGui.Title := "markdown2potpalyer" 15 | 16 | ; =======托盘菜单========= 17 | myMenu := A_TrayMenu 18 | 19 | myMenu.Default := "&Open" 20 | myMenu.ClickCount := 2 21 | 22 | myMenu.Rename("&Open", "打开") 23 | myMenu.Rename("E&xit", "退出") 24 | myMenu.Rename("&Pause Script", "暂停脚本") 25 | myMenu.Rename("&Suspend Hotkeys", "暂停热键") 26 | 27 | myGui.Show("w477 h755") 28 | } 29 | 30 | Constructor() { 31 | myGui := Gui() 32 | myGui.BackColor := "0xffffff" 33 | ; Tab := myGui.Add("Tab3", "x0 y0 w493 h745", ["回链设置", "Potplayer控制"]) 34 | Tab := myGui.Add("Tab3", "x0 y0 w493 h725", ["回链设置"]) 35 | Tab.UseTab(1) 36 | 37 | myGui.Add("Text", "x40 y24 w132 h20", "potplayer的路径") 38 | ; =======模板========= 39 | Edit_potplayer := myGui.Add("Edit", "x160 y22 w215 h25", "C:\Program Files\DAUM\PotPlayer\PotPlayerMini64.exe") 40 | Button_potplayer := myGui.Add("Button", "x380 y22 w90 h20", "选择Potplayer") 41 | 42 | myGui.Add("Text", "x40 y49 w132 h20", "笔记软件的程序名称") 43 | Edit_note_app_name := myGui.Add("Edit", "x160 y48 w190 h45 +Multi", "Obsidian.exe`nTypora.exe") 44 | myGui.Add("Text", "x160 y100 w123 h20", "多个笔记软件每行一个") 45 | 46 | myGui.Add("Text", "x40 y125 w63 h20", "字幕模板") 47 | Edit_note_app_name := myGui.Add("Edit", "x160 y125 w190 h30 +Multi", "字幕:`n{subtitle}") 48 | Button_srt_to_backlink_mdfile := myGui.Add("Button", "x380 y125 w90 h20", "srt转回链md") 49 | myGui.SetFont("s8") 50 | myGui.Add("Text", "x40 y160 w115 h20", "上/当前/下(单次)") 51 | hk_subtitle_previous_once := myGui.Add("Hotkey", "x160 y160 w90 h13", "^j") 52 | hk_subtitle_current_once := myGui.Add("Hotkey", "x260 y160 w90 h13", "^k") 53 | hk_subtitle_next_once := myGui.Add("Hotkey", "x360 y160 w90 h13", "^l") 54 | myGui.Add("Text", "x40 y175 w115 h20", "上/当前/下(循环)") 55 | hk_subtitle_previous_loop := myGui.Add("Hotkey", "x160 y175 w90 h13", "^!j") 56 | hk_subtitle_current_loop := myGui.Add("Hotkey", "x260 y175 w90 h13", "^!k") 57 | hk_subtitle_next_loop := myGui.Add("Hotkey", "x360 y175 w90 h13", "^!l") 58 | myGui.SetFont() 59 | 60 | myGui.Add("Text", "x40 y195 w63 h20", "回链的名称") 61 | Edit_title := myGui.Add("Edit", "x160 y195 w190 h17", "{name} | {time}") 62 | 63 | myGui.Add("Text", "x40 y218 w51 h20", "回链模板") 64 | Edit_markdown_template := myGui.Add("Edit", "x160 y218 w190 h40 +Multi", "`n视频:{title}`n") 65 | 66 | myGui.Add("Text", "x40 y265 w77 h20", "图片回链模板") 67 | Edit_image_template := myGui.Add("Edit", "x160 y265 w190 h50 +Multi", "`n图片:{image}`n视频:{title}`n") 68 | 69 | myGui.Add("Text", "x40 y320 w111", "图片粘贴延迟") 70 | Edit6 := myGui.Add("Edit", "x160 y320 w120 h17") 71 | myGui.Add("Text", "x288 y320 w22 h20", "ms") 72 | 73 | ; =======快捷键========= 74 | myGui.Add("Text", "x40 y343 w105 h20", "回链快捷键") 75 | hk_backlink := myGui.Add("Hotkey", "x160 y343 w120 h17", "!g") 76 | 77 | myGui.Add("Text", "x40 y365 w105 h20", "笔记快捷键") 78 | hk_backlink := myGui.Add("Hotkey", "x160 y365 w120 h17", "!n") 79 | 80 | myGui.Add("Text", "x40 y387 w105 h20", "字幕快捷键") 81 | hk_subtitle := myGui.Add("Hotkey", "x160 y387 w120 h17", "!t") 82 | 83 | myGui.Add("Text", "x40 y409 w105 h32", "图片+回链快捷键") 84 | hk_image_backlink := myGui.Add("Hotkey", "x160 y409 w120 h17", "^!g") 85 | myGui.SetFont("s8") 86 | myGui.Add("Text", "x40 y431 w105 h32", "外部工具截图快捷键") 87 | myGui.SetFont() 88 | hk_image_other_tool_screenshot := myGui.Add("Hotkey", "x160 y431 w120 h17", "!a") 89 | myGui.Add("Text", "x40 y453 w105 h32", "图片编辑") 90 | hk_image_edit := myGui.Add("Hotkey", "x160 y453 w120 h17", "!e") 91 | myGui.Add("Text", "x40 y475 w105 h32", "图片检测超时") 92 | Edit_Image_screenshot_detection_delays := myGui.Add("Edit", "x160 y475 w120 h17", "1000") 93 | myGui.Add("Text", "x288 y475 w31 h20", "s") 94 | 95 | myGui.Add("Text", "x40 y497 w114 h20", "A-B片段快捷键") 96 | hk_ab_fragment := myGui.Add("Hotkey", "x160 y497 w120 h17", "F1") 97 | 98 | myGui.Add("Text", "x40 y519 w98 h20", "A-B片段检测延迟") 99 | Edit_ab_fragment_detection_delays := myGui.Add("Edit", "x160 y519 w120 h17", "1000") 100 | myGui.Add("Text", "x288 y519 w31 h20", "ms") 101 | 102 | CheckBox_loop_ab_fragment := myGui.Add("CheckBox", "x160 y541 w120 h20", "循环播放片段") 103 | 104 | myGui.Add("Text", "x40 y563 w105 h12", "A-B循环快捷键") 105 | hk_ab_circulation := myGui.Add("Hotkey", "x160 y563 w190 h17") 106 | 107 | ; =======其他设置========= 108 | myGui.SetFont("s8") 109 | myGui.Add("Text", "x40 y585 w105 h36", "修改协议【谨慎】此项重启生效") 110 | Edit_url_protocol := myGui.Add("Edit", "x160 y585 w190 h17", "jv://open") 111 | myGui.SetFont() 112 | 113 | myGui.Add("Text", "x40 y607 w60 h20", "减少的时间") 114 | Edit_reduce_time := myGui.Add("Edit", "x160 y607 w120 h17", "0") 115 | 116 | CheckBox_is_stop := myGui.Add("CheckBox", "x160 y629 w69 h17", "暂停") 117 | CheckBox_remove_suffix_of_video_file := myGui.Add("CheckBox", "x160 y651 w150 h17", "本地视频移除文件后缀名") 118 | CheckBox_path_is_encode := myGui.Add("CheckBox", "x160 y673 w120 h17", "路径编码") 119 | CheckBox_bootup := myGui.Add("CheckBox", "x160 y695 w120 h17", "开机启动") 120 | 121 | ; Tab.UseTab(2) 122 | ; myGui.Add("Text", "x86 y24 w42 h20", "上一帧") 123 | ; hk_previous_frame := myGui.Add("Hotkey", "x152 y24 w120 h17") 124 | ; myGui.Add("Text", "x86 y48 w38 h20", "下一帧") 125 | ; hk_next_frame := myGui.Add("Hotkey", "x152 y48 w120 h17") 126 | ; myGui.Add("Text", "x86 y80", "快进") 127 | ; hk_forward := myGui.Add("Hotkey", "x152 y80 w120 h17") 128 | ; Edit_forward_seconds := myGui.Add("Edit", "x280 y80 w37 h17") 129 | ; myGui.Add("Text", "x322 y80 w17", "秒") 130 | ; myGui.Add("Text", "x86 y104", "快退") 131 | ; hk_backward := myGui.Add("Hotkey", "x152 y104 w120 h17") 132 | ; Edit_backward_seconds := myGui.Add("Edit", "x281 y104 w36 h17") 133 | ; myGui.Add("Text", "x322 y102", "秒") 134 | ; myGui.Add("Text", "x86 y133", "播放/暂停") 135 | ; hk_play_or_pause := myGui.Add("Hotkey", "x152 y129 w120 h17") 136 | ; myGui.Add("Text", "x86 y153 w24 h17", "停止") 137 | ; hk_stop := myGui.Add("Hotkey", "x152 y153 w120 h17") 138 | 139 | Tab.UseTab() 140 | myGui.Add("Text", "x332 y725 w150 h12", "当前版本:0.2.7") 141 | myGui.Add("Link", "x332 y740 w150 h12", "最新版本:0.2.6") 142 | 143 | return myGui 144 | } -------------------------------------------------------------------------------- /lib/gui/i18n/I18n.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2.0 2 | #Include LCID.ahk 3 | /* 4 | i18n for AutoHotkey 5 | Version: 1.0.0 6 | Author: Maël Schweighardt (https://github.com/iammael/i18n-autohotkey) 7 | License: MIT (https://github.com/iammael/i18n-autohotkey/blob/master/LICENSE) 8 | */ 9 | 10 | class I18n { 11 | __New(languageFolder) { 12 | this.LanguageFolder := languageFolder 13 | languageFile := LCID[A_Language] 14 | this.LanguageFile := languageFolder "\" languageFile ".ini" 15 | if (!FileExist(this.LanguageFile)) { 16 | this.LanguageFile := languageFolder "\en-US.ini" 17 | } 18 | 19 | this._init() 20 | } 21 | 22 | _init() { 23 | Section := IniRead(this.LanguageFile, "Strings") 24 | keys_values := StrSplit(Section, "`n") 25 | 26 | for key_value in keys_values { 27 | key := StrSplit(key_value, "=")[1] 28 | value := StrSplit(key_value, "=")[2] 29 | this.DefineProp(key, {Value:value}) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/gui/i18n/LCID.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2.0 2 | 3 | LCID := Map( 4 | "0036", "af", 5 | "0436", "af-ZA", 6 | "001C", "sq", 7 | "041C", "sq-AL", 8 | "0484", "gsw-FR", 9 | "005E", "am", 10 | "045E", "am-ET", 11 | "0001", "ar", 12 | "1401", "ar-DZ", 13 | "3C01", "ar-BH", 14 | "0C01", "ar-EG", 15 | "0801", "ar-IQ", 16 | "2C01", "ar-JO", 17 | "3401", "ar-KW", 18 | "3001", "ar-LB", 19 | "1001", "ar-LY", 20 | "1801", "ar-MA", 21 | "2001", "ar-OM", 22 | "4001", "ar-QA", 23 | "0401", "ar-SA", 24 | "2801", "ar-SY", 25 | "1C01", "ar-TN", 26 | "3801", "ar-AE", 27 | "2401", "ar-YE", 28 | "002B", "hy", 29 | "042B", "hy-AM", 30 | "004D", "as", 31 | "044D", "as-IN", 32 | "002C", "az", 33 | "742C", "az-Cyrl", 34 | "082C", "az-Cyrl-AZ", 35 | "782C", "az-Latn", 36 | "042C", "az-Latn-AZ", 37 | "0045", "bn", 38 | "0845", "bn-BD", 39 | "006D", "ba", 40 | "046D", "ba-RU", 41 | "002D", "eu", 42 | "042D", "eu-ES", 43 | "0023", "be", 44 | "0423", "be-BY", 45 | "0445", "bn-IN", 46 | "781A", "bs", 47 | "641A", "bs-Cyrl", 48 | "201A", "bs-Cyrl-BA", 49 | "681A", "bs-Latn", 50 | "141A", "bs-Latn-BA", 51 | "007E", "br", 52 | "047E", "br-FR", 53 | "0002", "bg", 54 | "0402", "bg-BG", 55 | "0055", "my", 56 | "0455", "my-MM", 57 | "0003", "ca", 58 | "0403", "ca-ES", 59 | "005F", "tzm", 60 | "045F", "tzm-Arab-MA", 61 | "7C5F", "tzm-Latn", 62 | "085F", "tzm-Latn-DZ", 63 | "785F", "tzm-Tfng", 64 | "105F", "tzm-Tfng-MA", 65 | "0092", "ku", 66 | "7C92", "ku-Arab", 67 | "0492", "ku-Arab-IQ", 68 | "005C", "chr", 69 | "7C5C", "chr-Cher", 70 | "045C", "chr-Cher-US", 71 | "7804", "zh", 72 | "0004", "zh-Hans", 73 | "0804", "zh-CN", 74 | "1004", "zh-SG", 75 | "7C04", "zh-Hant", 76 | "0C04", "zh-HK", 77 | "1404", "zh-MO", 78 | "0404", "zh-TW", 79 | "0083", "co", 80 | "0483", "co-FR", 81 | "001A", "hr", 82 | "101A", "hr-BA", 83 | "041A", "hr-HR", 84 | "0005", "cs", 85 | "0405", "cs-CZ", 86 | "0006", "da", 87 | "0406", "da-DK", 88 | "0065", "dv", 89 | "0465", "dv-MV", 90 | "0013", "nl", 91 | "0813", "nl-BE", 92 | "0413", "nl-NL", 93 | "0C51", "dz-BT", 94 | "0066", "bin", 95 | "0466", "bin-NG", 96 | "0009", "en", 97 | "0C09", "en-AU", 98 | "2809", "en-BZ", 99 | "1009", "en-CA", 100 | "2409", "en-029", 101 | "3C09", "en-HK", 102 | "4009", "en-IN", 103 | "3809", "en-ID", 104 | "1809", "en-IE", 105 | "2009", "en-JM", 106 | "4409", "en-MY", 107 | "1409", "en-NZ", 108 | "3409", "en-PH", 109 | "4809", "en-SG", 110 | "1C09", "en-ZA", 111 | "2C09", "en-TT", 112 | "4C09", "en-AE", 113 | "0809", "en-GB", 114 | "0409", "en-US", 115 | "3009", "en-ZW", 116 | "0025", "et", 117 | "0425", "et-EE", 118 | "0038", "fo", 119 | "0438", "fo-FO", 120 | "0064", "fil", 121 | "0464", "fil-PH", 122 | "000B", "fi", 123 | "040B", "fi-FI", 124 | "000C", "fr", 125 | "080C", "fr-BE", 126 | "2C0C", "fr-CM", 127 | "0C0C", "fr-CA", 128 | "1C0C", "fr-029", 129 | "300C", "fr-CI", 130 | "040C", "fr-FR", 131 | "3C0C", "fr-HT", 132 | "140C", "fr-LU", 133 | "340C", "fr-ML", 134 | "180C", "fr-MC", 135 | "380C", "fr-MA", 136 | "200C", "fr-RE", 137 | "280C", "fr-SN", 138 | "100C", "fr-CH", 139 | "240C", "fr-CD", 140 | "0067", "ff", 141 | "7C67", "ff-Latn", 142 | "0467", "ff-Latn-NG", 143 | "0867", "ff-Latn-SN", 144 | "0056", "gl", 145 | "0456", "gl-ES", 146 | "0037", "ka", 147 | "0437", "ka-GE", 148 | "0007", "de", 149 | "0C07", "de-AT", 150 | "0407", "de-DE", 151 | "1407", "de-LI", 152 | "1007", "de-LU", 153 | "0807", "de-CH", 154 | "0008", "el", 155 | "0408", "el-GR", 156 | "0074", "gn", 157 | "0474", "gn-PY", 158 | "0047", "gu", 159 | "0447", "gu-IN", 160 | "0068", "bin", 161 | "0468", "bin-NG", 162 | "0075", "haw", 163 | "0475", "haw-US", 164 | "000D", "he", 165 | "040D", "he-IL", 166 | "0039", "hi", 167 | "0439", "hi-IN", 168 | "000E", "hu", 169 | "040E", "hu-HU", 170 | "0069", "ibb", 171 | "0469", "ibb-NG", 172 | "000F", "is", 173 | "040F", "is-IS", 174 | "0076", "la", 175 | "0476", "la-VA", 176 | "0026", "lv", 177 | "0426", "lv-LV", 178 | "0027", "lt", 179 | "0427", "lt-LT", 180 | "7C2E", "dsb", 181 | "082E", "dsb-DE", 182 | "006E", "lb", 183 | "046E", "lb-LU", 184 | "002F", "mk", 185 | "042F", "mk-MK", 186 | "003E", "ms", 187 | "083E", "ms-BN", 188 | "043E", "ms-MY", 189 | "004C", "ml", 190 | "044C", "ml-IN", 191 | "003A", "mt", 192 | "043A", "mt-MT", 193 | "0058", "mni", 194 | "0458", "mni-IN", 195 | "0081", "mi", 196 | "0481", "mi-NZ", 197 | "007A", "arn", 198 | "047A", "arn-CL", 199 | "004E", "mr", 200 | "044E", "mr-IN", 201 | "007C", "moh", 202 | "047C", "moh-CA", 203 | "0050", "mn", 204 | "7850", "mn-Cyrl", 205 | "0450", "mn-MN", 206 | "7C50", "mn-Mong", 207 | "0850", "mn-Mong-CN", 208 | "0C50", "mn-Mong-MN", 209 | "0061", "ne", 210 | "0861", "ne-IN", 211 | "0461", "ne-NP", 212 | "003B", "se", 213 | "0014", "no", 214 | "7C14", "nb", 215 | "0414", "nb-NO", 216 | "7814", "nn", 217 | "0814", "nn-NO", 218 | "0082", "oc", 219 | "0482", "oc-FR", 220 | "0048", "or", 221 | "0448", "or-IN", 222 | "0072", "om", 223 | "0472", "om-ET", 224 | "0079", "pap", 225 | "0479", "pap-029", 226 | "0063", "ps", 227 | "0463", "ps-AF", 228 | "0029", "fa", 229 | "008C", "fa", 230 | "048C", "fa-AF", 231 | "0429", "fa-IR", 232 | "0015", "pl", 233 | "0415", "pl-PL", 234 | "0016", "pt", 235 | "0416", "pt-BR", 236 | "0816", "pt-PT", 237 | "05FE", "qps-ploca", 238 | "09FF", "qps-plocm", 239 | "0901", "qps-Latn-x-sh", 240 | "0501", "qps-ploc", 241 | "0046", "pa", 242 | "7C46", "pa-Arab", 243 | "0446", "pa-IN", 244 | "0846", "pa-Arab-PK", 245 | "006B", "quz", 246 | "046B", "quz-BO", 247 | "086B", "quz-EC", 248 | "0C6B", "quz-PE", 249 | "0018", "ro", 250 | "0818", "ro-MD", 251 | "0418", "ro-RO", 252 | "0017", "rm", 253 | "0417", "rm-CH", 254 | "0019", "ru", 255 | "0819", "ru-MD", 256 | "0419", "ru-RU", 257 | "0085", "sah", 258 | "0485", "sah-RU", 259 | "703B", "smn", 260 | "7C3B", "smj", 261 | "743B", "sms", 262 | "783B", "sma", 263 | "243B", "smn-FI", 264 | "103B", "smj-NO", 265 | "143B", "smj-SE", 266 | "0C3B", "se-FI", 267 | "043B", "se-NO", 268 | "083B", "se-SE", 269 | "203B", "sms-FI", 270 | "183B", "sma-NO", 271 | "1C3B", "sma-SE", 272 | "004F", "sa", 273 | "044F", "sa-IN", 274 | "0091", "gd", 275 | "0491", "gd-GB", 276 | "7C1A", "sr", 277 | "6C1A", "sr-Cyrl", 278 | "1C1A", "sr-Cyrl-BA", 279 | "301A", "sr-Cyrl-ME", 280 | "0C1A", "sr-Cyrl-CS", 281 | "281A", "sr-Cyrl-RS", 282 | "701A", "sr-Latn", 283 | "181A", "sr-Latn-BA", 284 | "2C1A", "sr-Latn-ME", 285 | "081A", "sr-Latn-CS", 286 | "241A", "sr-Latn-RS", 287 | "0030", "st", 288 | "0430", "st-ZA", 289 | "006C", "nso", 290 | "046C", "nso-ZA", 291 | "0059", "sd", 292 | "7C59", "sd-Arab", 293 | "0459", "sd-Deva-IN", 294 | "0859", "sd-Arab-PK", 295 | "005B", "si", 296 | "045B", "si-LK", 297 | "001B", "sk", 298 | "041B", "sk-SK", 299 | "0024", "sl", 300 | "0424", "sl-SI", 301 | "0077", "so", 302 | "0477", "so-SO", 303 | "000A", "es", 304 | "2C0A", "es-AR", 305 | "400A", "es-BO", 306 | "340A", "es-CL", 307 | "240A", "es-CO", 308 | "140A", "es-CR", 309 | "5C0A", "es-CU", 310 | "1C0A", "es-DO", 311 | "300A", "es-EC", 312 | "440A", "es-SV", 313 | "100A", "es-GT", 314 | "480A", "es-HN", 315 | "580A", "es-419", 316 | "080A", "es-MX", 317 | "4C0A", "es-NI", 318 | "180A", "es-PA", 319 | "3C0A", "es-PY", 320 | "280A", "es-PE", 321 | "500A", "es-PR", 322 | "0C0A", "es-ES", 323 | "040A", "es-ES_tradnl", 324 | "540A", "es-US", 325 | "380A", "es-UY", 326 | "200A", "es-VE", 327 | "001D", "sv", 328 | "081D", "sv-FI", 329 | "041D", "sv-SE", 330 | "0084", "gsw", 331 | "005A", "syr", 332 | "045A", "syr-SY", 333 | "0028", "tg", 334 | "7C28", "tg-Cyrl", 335 | "0428", "tg-Cyrl-TJ", 336 | "0049", "ta", 337 | "0449", "ta-IN", 338 | "0849", "ta-LK", 339 | "0044", "tt", 340 | "0444", "tt-RU", 341 | "004A", "te", 342 | "044A", "te-IN", 343 | "001E", "th", 344 | "041E", "th-TH", 345 | "0051", "bo", 346 | "0451", "bo-CN", 347 | "0073", "ti", 348 | "0873", "ti-ER", 349 | "0473", "ti-ET", 350 | "001F", "tr", 351 | "041F", "tr-TR", 352 | "0042", "tk", 353 | "0442", "tk-TM", 354 | "0022", "uk", 355 | "0422", "uk-UA", 356 | "002E", "hsb", 357 | "042E", "hsb-DE", 358 | "0020", "ur", 359 | "0820", "ur-IN", 360 | "0420", "ur-PK", 361 | "0080", "ug", 362 | "0480", "ug-CN", 363 | "0043", "uz", 364 | "7843", "uz-Cyrl", 365 | "0843", "uz-Cyrl-UZ", 366 | "7C43", "uz-Latn", 367 | "0443", "uz-Latn-UZ", 368 | "0803", "ca-ES-valencia", 369 | "0033", "ve", 370 | "0433", "ve-ZA", 371 | "002A", "vi", 372 | "042A", "vi-VN", 373 | "0052", "cy", 374 | "0452", "cy-GB", 375 | "0062", "fy", 376 | "0462", "fy-NL", 377 | "0088", "wo", 378 | "0488", "wo-SN", 379 | "0031", "ts", 380 | "0431", "ts-ZA", 381 | "0078", "ii", 382 | "0478", "ii-CN", 383 | "003D", "yi", 384 | "043D", "yi-001", 385 | "006A", "yo", 386 | "046A", "yo-NG" 387 | ) -------------------------------------------------------------------------------- /lib/gui/i18n/en-US.ini: -------------------------------------------------------------------------------- 1 | [Strings] 2 | tips_ab_start=Start time recorded! Press the hotkey again to record end time. (Esc to cancel) 3 | tips_ab_failed=Start time not set! Cannot generate A-B segment link. 4 | tips_save_image_failed=Video is not playing - cannot capture screenshot! 5 | Gui_Tab_backlink_setting=Backlink Settings 6 | Gui_potplayer_path=Path of Potplayer 7 | Gui_choose_potplayer=Select Potplayer 8 | Gui_note_names=Note Software Program Names 9 | Gui_note_names_tips=One per line for multiple note software 10 | Gui_subtitle_template_name=Subtitle Template 11 | Gui_srt_to_backlink=srt to md 12 | Gui_subtitle_navigation_single=Pre/Curr/Next(Single) 13 | Gui_subtitle_navigation_loop=Pre/Curr/Next(Loop) 14 | Gui_backlink_name=Backlink Name 15 | Gui_backlink_template=Backlink Template 16 | Gui_image_backlink_tempalte_name=Image Backlink Template 17 | Gui_send_image_delays=Delays after pasting images 18 | Gui_hotkey_subtitle=Subtitle Hotkey 19 | Gui_hotkey_backlink=Backlink Hotkey 20 | Gui_hotkey_user_note=Note Hotkey 21 | Gui_hotkey_image_and_backlink=Image + Backlink Hotkey 22 | Gui_hotkey_screenshot_tool_hotkeys=Screenshot Tool Hotkeys 23 | Gui_hotkey_image_edit_and_backlink=Edit Image Hotkey 24 | Gui_image_edit_detection_time=Image Edit Detection Time 25 | Gui_hotkey_ab_fragment=A-B Segment Hotkey 26 | Gui_hotkey_ab_fragment_detection_delays=A-B Segment Detection delays 27 | Gui_is_loop_ab_fragment=Loop Play Segment 28 | Gui_hotkey_ab_circulation=A-B Loop Hotkey 29 | Gui_edit_url_protocol=Modify Protocol restart effective 30 | Gui_reduce_time=Reduced Time 31 | Gui_is_stop=Pause 32 | Gui_remove_suffix_of_video_file=Remove File Suffix for Local Video 33 | Gui_is_path_encode=Path Encoding 34 | Gui_bootup=Boot on Startup 35 | Gui_open=Open 36 | Gui_exit=Exit 37 | Gui_pause_script=Pause Script 38 | Gui_suspend_hotkeys=Suspend Hotkeys 39 | Gui_current_version=Current Version 40 | Gui_latest_version=Latest Version -------------------------------------------------------------------------------- /lib/gui/i18n/zh-CN.ini: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelycode36/markdown2potplayer/34dba23ba397ac893a3e0372dfa53fd8805eef4b/lib/gui/i18n/zh-CN.ini -------------------------------------------------------------------------------- /lib/ico.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelycode36/markdown2potplayer/34dba23ba397ac893a3e0372dfa53fd8805eef4b/lib/ico.ico -------------------------------------------------------------------------------- /lib/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelycode36/markdown2potplayer/34dba23ba397ac893a3e0372dfa53fd8805eef4b/lib/icon.png -------------------------------------------------------------------------------- /lib/note2potplayer/RegisterUrlProtocol.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2.0 2 | 3 | ; 注册协议 4 | RegisterUrlProtocol(url_protocol){ 5 | protocol_name := GetUrlProtocolName(url_protocol) 6 | RegistrationUrlProtocol(protocol_name) 7 | } 8 | 9 | GetUrlProtocolName(url_protocol){ 10 | index_of := InStr(url_protocol, ":") 11 | if (index_of = 0){ 12 | MsgBox "error: protocol no ':' found" 13 | Exit 14 | } 15 | result := SubStr(url_protocol, 1,index_of-1) 16 | return result 17 | } 18 | 19 | RegistrationUrlProtocol(protocol_name){ 20 | RegCreateKey "HKEY_CURRENT_USER\Software\Classes\" protocol_name 21 | RegWrite "", "REG_SZ", "HKEY_CURRENT_USER\Software\Classes\" protocol_name, "URL Protocol" 22 | RegCreateKey "HKEY_CURRENT_USER\Software\Classes\" protocol_name "\shell" 23 | RegCreateKey "HKEY_CURRENT_USER\Software\Classes\" protocol_name "\shell\open" 24 | RegCreateKey "HKEY_CURRENT_USER\Software\Classes\" protocol_name "\shell\open\command" 25 | RegWrite A_ScriptDir "\lib\note2potplayer\note2potplayer.exe `"%1`"", "REG_SZ", "HKEY_CURRENT_USER\Software\Classes\" protocol_name "\shell\open\command" 26 | } -------------------------------------------------------------------------------- /lib/note2potplayer/note2potplayer.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2.0 2 | #SingleInstance force 3 | #Include ..\MyTool.ahk 4 | #Include sqlite\SqliteControl.ahk 5 | #Include ..\PotplayerControl.ahk 6 | #Include ..\TimeTool.ahk 7 | #Include ..\PotplayerFragment.ahk 8 | 9 | ; 1. init 10 | potplayer_path := GetKeyName("path") 11 | 12 | potplayer := { 13 | info: EnrichInfo(potplayer_path), 14 | control: PotplayerControl(potplayer_path) 15 | } 16 | 17 | EnrichInfo(potplayer_path) { 18 | info := { 19 | path: potplayer_path, 20 | isRunning: IsPotplayerRunning(potplayer_path), 21 | openWindow: IsPotplayerRunning(potplayer_path) ? "/current" : "/new" 22 | } 23 | 24 | return info 25 | } 26 | 27 | ; 2. 主逻辑 28 | AppMain() 29 | AppMain() { 30 | CallbackPotplayer() 31 | } 32 | 33 | ; 【主逻辑】Potplayer的回调函数(回链) 34 | CallbackPotplayer() { 35 | url := ReceivParameter() 36 | if url { 37 | ParseUrl(url) 38 | } else { 39 | MsgBox "至少传递1个参数" 40 | } 41 | ExitApp 42 | } 43 | 44 | ReceivParameter() { 45 | ; 获取命令行参数的数量 46 | paramCount := A_Args.Length 47 | 48 | ; 如果没有参数,显示提示信息 49 | if (paramCount = 0) { 50 | return false 51 | } 52 | 53 | params := "" 54 | ; 循环遍历参数并显示在控制台 55 | for n, param in A_Args { 56 | params .= param " " 57 | } 58 | return Trim(params) 59 | } 60 | 61 | ParseUrl(url) { 62 | ;url := "jv://open?path=https://www.bilibili.com/video/123456/?spm_id_from=..search-card.all.click&time=00:01:53.824" 63 | ; MsgBox url 64 | url := UrlDecode(url) 65 | index_of := InStr(url, "?") 66 | parameters_of_url := SubStr(url, index_of + 1) 67 | 68 | ; 1. 解析键值对 69 | parameters := StrSplit(parameters_of_url, "&") 70 | parameters_map := Map() 71 | 72 | ; 1.1 常规解析 73 | for index, pair in parameters { 74 | index_of := InStr(pair, "=") 75 | if (index_of > 0) { 76 | key := SubStr(pair, 1, index_of - 1) 77 | value := SubStr(pair, index_of + 1) 78 | parameters_map[key] := value 79 | } 80 | } 81 | 82 | ; 1.2 对path参数特殊处理,因为路径中可能是网址 83 | path := SubStr(parameters_of_url, 1, InStr(parameters_of_url, "&time=") - 1) 84 | path := StrReplace(path, "path=", "") 85 | parameters_map["path"] := path 86 | 87 | ; 2. 跳转Potplayer 88 | ; D:\PotPlayer64\PotPlayerMini64.exe "D:\123.mp4" /seek=00:01:53.824 /new 89 | potplayer.jump := { 90 | time: "", 91 | path: parameters_map["path"], 92 | timeSpan: parameters_map["time"] 93 | } 94 | 95 | ; 情况0:是同一个视频进行跳转,之前可能设置了AB循环,所以此处先取消A-B循环 96 | CancelABCycleIfNeeded(potplayer.control, potplayer.info.path, potplayer.jump.path) 97 | ; 情况1:单个时间戳 00:01:53 98 | if (IsSingleTimestamp(potplayer.jump.timeSpan)) { 99 | JumpToSingleTimestamp(potplayer.jump.path, potplayer.jump.timeSpan) 100 | } else { 101 | ; 情况2:时间戳片段 102 | time := TimeSpanToTime(potplayer.jump.timeSpan) 103 | time_start := time.start 104 | time_end := time.end 105 | if (IsAbFragment(potplayer.jump.timeSpan)) { 106 | if (GetKeyName("loop_ab_fragment")) { 107 | JumpToAbCirculation(potplayer.control, potplayer_path, potplayer.jump.path, time_start, time_end) 108 | } else { 109 | JumpToAbFragment(potplayer.control, potplayer_path, potplayer.jump.path, time_start, time_end, GetKeyName("ab_fragment_detection_delays")) 110 | } 111 | } else if (IsAbCirculation(potplayer.jump.timeSpan)) { 112 | ; 情况3:时间戳循环 113 | JumpToAbCirculation(potplayer.control, potplayer_path, potplayer.jump.path, time_start, time_end) 114 | } 115 | } 116 | ExitApp() 117 | } 118 | 119 | ; 解析时间片段字符串 120 | TimeSpanToTime(media_time) { 121 | ; 1. 解析时间戳 122 | time_separator := ["∞", "-"] 123 | 124 | index_of := "" 125 | for index, separator in time_separator { 126 | index_of := InStr(media_time, separator) 127 | if (index_of > 0) { 128 | break 129 | } 130 | } 131 | Assert(index_of == "", "时间戳格式错误") 132 | 133 | time := { 134 | start: SubStr(media_time, 1, index_of - 1), 135 | end: SubStr(media_time, index_of + 1) 136 | } 137 | return time 138 | } 139 | 140 | ; 字符串中不包含"-、∞",则为单个时间戳 141 | IsSingleTimestamp(media_time) { 142 | if (InStr(media_time, "-") > 0 || InStr(media_time, "∞") > 0) 143 | return false 144 | else 145 | return true 146 | } 147 | 148 | ; 使用时间戳跳转 149 | OpenPotplayerAndJumpToTimestamp(media_path, media_time) { 150 | run_command := potplayer_path . " `"" . media_path . "`" /seek=" . media_time . " " . potplayer.info.openWindow 151 | try { 152 | Run run_command 153 | } catch Error as err 154 | if err.Extra { 155 | MsgBox "错误:" err.Extra 156 | MsgBox run_command 157 | } else { 158 | throw err 159 | } 160 | } 161 | 162 | IsAbFragment(media_time) { 163 | if (InStr(media_time, "-") > 0) 164 | return true 165 | else 166 | return false 167 | } 168 | 169 | JumpToSingleTimestamp(path, time) { 170 | if (potplayer.info.isRunning 171 | && potplayer.control.GetPlayStatus() != "Stopped" 172 | && IsSameVideo(potplayer.control, potplayer.jump.path)) { 173 | potplayer.control.SetMediaTimeMilliseconds(TimestampToMilliseconds(time)) 174 | potplayer.control.Play() 175 | } else { 176 | OpenPotplayerAndJumpToTimestamp(path, time) 177 | } 178 | } 179 | 180 | IsAbCirculation(time_span) { 181 | if (InStr(time_span, "∞") > 0) 182 | return true 183 | else 184 | return false 185 | } 186 | 187 | CallPotplayer() { 188 | if (potplayer.info.isRunning 189 | && potplayer.control.GetPlayStatus() != "Stopped" 190 | && IsSameVideo(potplayer.control, potplayer.jump.path)) { 191 | potplayer.control.SetMediaTimeMilliseconds(TimestampToMilliseconds(potplayer.jump.time.start)) 192 | potplayer.control.Play() 193 | } else { 194 | ; 播放指定视频 195 | PlayVideo(potplayer.jump.path, potplayer.jump.time.start) 196 | } 197 | } 198 | PlayVideo(media_path, time_start) { 199 | if (potplayer.info.isRunning && potplayer.control.GetPlayStatus() != "Stopped") { 200 | potplayer.control.Stop() 201 | } 202 | OpenPotplayerAndJumpToTimestamp(media_path, time_start) 203 | WaitForPotplayerToFinishLoadingTheVideo(GetNameForPath(media_path)) 204 | potplayer.control.Play() 205 | } 206 | ; 已开Potplayer跳转到下一个视频,判断当前potplayer播放器的状态 207 | WaitForPotplayerToFinishLoadingTheVideo(video_name) { 208 | WinWaitActive("ahk_exe " GetNameForPath(potplayer_path)) 209 | 210 | while (true) { 211 | if (WinGetTitle("ahk_id " potplayer.control.getHwnd()) != "PotPlayer" 212 | && potplayer.control.GetPlayStatus() == "Running") { 213 | break 214 | } 215 | Sleep 1000 216 | } 217 | } -------------------------------------------------------------------------------- /lib/note2potplayer/sqlite/SqliteControl.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2.0.0 2 | #Include "Class_SQLiteDB.ahk" 3 | 4 | ; 数据库文件路径 5 | db_file_path := SubStr(A_ScriptDir, 1,InStr(A_ScriptDir,"\lib",,1) -1 ) "\config.db" 6 | table_name := "config" 7 | 8 | OpenLocalDB(){ 9 | ; 创建 SQLiteDB 实例 10 | DB := SQLiteDB() 11 | 12 | ; 打开或创建数据库 13 | if !DB.OpenDB(db_file_path) { 14 | MsgBox("无法打开或创建数据库: " db_file_path "`n错误信息: " DB.ErrorMsg) 15 | ExitApp 16 | } 17 | return DB 18 | } 19 | 20 | GetKeyName(key){ 21 | DB := OpenLocalDB() 22 | 23 | ; 读取 key 为 'app_name' 的值 24 | SQL_SelectValue := "SELECT value FROM " table_name " WHERE key = '" key "';" 25 | Result := "" 26 | if !DB.GetTable(SQL_SelectValue, &Result) { 27 | MsgBox("无法读取配置项 '" key "'`n错误信息: " . DB.ErrorMsg) 28 | DB.CloseDB() 29 | ExitApp 30 | } 31 | 32 | ; 显示结果 33 | if Result.RowCount > 0 { 34 | ; MsgBox("配置项 '" key "' 的值为: " . Result.Rows[1][1]) ; 获取第一行第一列的值 35 | return Result.Rows[1][1] 36 | } else { 37 | ; MsgBox("配置项 '" key "' 不存在。") 38 | return false 39 | } 40 | 41 | DB.CloseDB() 42 | } -------------------------------------------------------------------------------- /lib/socket/Socket.ahk: -------------------------------------------------------------------------------- 1 | ; Socket class 2 | ; Big thanks to GeekDude for remaking this. 3 | ; Original script by GeekDude: https://github.com/G33kDude/Socket.ahk/blob/master/Socket.ahk 4 | ; AHK Forum Post by GeekDude: https://www.autohotkey.com/boards/viewtopic.php?f=6&t=35120 5 | ; 6 | ; And thanks to Bentschi's work which GeekDude expanded on. 7 | ; https://autohotkey.com/board/topic/94376-socket-class-%C3%BCberarbeitet/ 8 | ; 9 | ; Helpful info: 10 | ; Event Objects vs ComplitionIO ports 11 | ; https://stackoverflow.com/questions/44664511/windows-event-based-overlapped-io-vs-io-completion-ports-real-world-performanc 12 | ; 13 | ; Thanks to winsockdotnetworkprogramming.com 14 | ; https://www.winsocketdotnetworkprogramming.com/winsock2programming/winsock2advancediomethod5g.html 15 | ; - this gave a workaround for using overlapped I/O 16 | ; 17 | ; MS Help Docs: 18 | ; 19 | ; ===================================================================================================== 20 | ; 21 | ; 22 | 23 | class winsock { 24 | Static sockets := Map(), wm_msg := 0x9987 25 | 26 | Static __New() { ; init WS2 lib on script start 27 | invert("family","family_2") ; for looking up family name by number 28 | 29 | this.hModule := DllCall("LoadLibrary", "Str", "Ws2_32", "UPtr") 30 | this.WSAData := Buffer(394+A_PtrSize) 31 | if (Err := DllCall("Ws2_32\WSAStartup", "UShort", 0x0202, "UPtr", this.WSAData.ptr)) 32 | throw Error("Error starting Winsock",, Err) 33 | if (NumGet(this.WSAData, 2, "UShort") != 0x0202) 34 | throw Error("Winsock version 2.2 not available") 35 | 36 | this.socketMonitor := ObjBindMethod(this,"WM_SOCKET") 37 | OnMessage(this.wm_msg,this.socketMonitor) ; 0x9987 38 | 39 | this.sockaddr.Prototype.DefineProp("insert" ,{Call:(o,buf,dest)=>insert(buf,dest)}) 40 | this.sockaddr.Prototype.DefineProp("extract",{Call:(o,src,len)=>extract(src,len)}) 41 | 42 | insert(buf,dest) => DllCall("RtlCopyMemory","UPtr",dest,"UPtr",buf.ptr,"UPtr",buf.size) 43 | extract(src,len) { 44 | buf := Buffer(len,0) 45 | DllCall("RtlCopyMemory","UPtr",buf.ptr,"UPtr",src,"UPtr",len) 46 | return buf 47 | } 48 | invert(name,name_2) { 49 | this.%name_2% := {} 50 | For name, value in this.%name%.OwnProps() 51 | this.%name_2%.%value% := name 52 | } 53 | } 54 | 55 | 56 | Static flags := { Passive:0x1 , NumericHost:0x4 , Secure:0x8000 , NonAuthoritative:0x4000 57 | , CanonName:0x2 , AddrConfig:0x400 , FQDN:0x20000, ReturnPreferredNames:0x10000 58 | , V4Mapped:0x800, FileServer:0x40000, All:0x100} 59 | 60 | , family := {Unspec:0, IPV4:2, NetBIOS:17, IPv6:23, IRDA:26, BTH:32} 61 | , fam_size := {Unspec:-1, IPv4:16,NetBIOS:-1, IPv6:28,IRDA:-1, BTH:-1} 62 | , fam_len := {Unspec:-1, IPv4:4, NetBIOS:-1, IPv6:8, IRDA:-1, BTH:-1} 63 | , fam_addr_off := {Unspec:-1, IPv4:4, NetBIOS:-1, IPv6:8, IRDA:-1, BTH:-1} 64 | , socktype := {Stream:1, DGram:2, RAW:3, RDM:4, SeqPacket:5} 65 | , protocol := {TCP:6, UDP:17, RM:113} 66 | 67 | , events := {1:"Read",2:"Write",4:"OOB",8:"Accept",16:"Connect",32:"Close",64:"QOS" 68 | ,128:"GroupQOS",256:"RoutingInterfaceChange",512:"AddressListChange"} 69 | 70 | , errors := {6:"WSA_INVALID_HANDLE", 8:"WSA_NOT_ENOUGH_MEMORY", 87:"WSA_INVALID_PARAMETER", 995:"WSA_OPERATION_ABORTED" 71 | , 996:"WSA_IO_INCOMPLETE", 997:"WSA_IO_PENDING", 10004:"WSAEINTR", 10009:"WSAEBADF", 10013:"WSAEACCES" 72 | , 10014:"WSAEFAULT", 10022:"WSAEINVAL", 10024:"WSAEMFILE", 10035:"WSAEWOULDBLOCK", 10036:"WSAEINPROGRESS" 73 | , 10037:"WSAEALREADY", 10038:"WSAENOTSOCK", 10039:"WSAEDESTADDRREQ", 10040:"WSAEMSGSIZE", 10041:"WSAEPROTOTYPE" 74 | , 10042:"WSAENOPROTOOPT", 10043:"WSAEPROTONOSUPPORT", 10044:"WSAESOCKTNOSUPPORT", 10045:"WSAEOPNOTSUPP" 75 | , 10046:"WSAEPFNOSUPPORT", 10047:"WSAEAFNOSUPPORT", 10048:"WSAEADDRINUSE", 10049:"WSAEADDRNOTAVAIL", 10050:"WSAENETDOWN" 76 | , 10051:"WSAENETUNREACH", 10052:"WSAENETRESET", 10053:"WSAECONNABORTED", 10054:"WSAECONNRESET", 10055:"WSAENOBUFS" 77 | , 10056:"WSAEISCONN", 10057:"WSAENOTCONN", 10058:"WSAESHUTDOWN", 10059:"WSAETOOMANYREFS", 10060:"WSAETIMEDOUT" 78 | , 10061:"WSAECONNREFUSED", 10062:"WSAELOOP", 10063:"WSAENAMETOOLONG", 10064:"WSAEHOSTDOWN", 10065:"WSAEHOSTUNREACH" 79 | , 10066:"WSAENOTEMPTY", 10067:"WSAEPROCLIM", 10068:"WSAEUSERS", 10069:"WSAEDQUOT", 10070:"WSAESTALE", 10071:"WSAEREMOTE" 80 | , 10091:"WSASYSNOTREADY", 10092:"WSAVERNOTSUPPORTED", 10093:"WSANOTINITIALISED", 10101:"WSAEDISCON", 10102:"WSAENOMORE" 81 | , 10103:"WSAECANCELLED", 10104:"WSAEINVALIDPROCTABLE", 10105:"WSAEINVALIDPROVIDER", 10106:"WSAEPROVIDERFAILEDINIT" 82 | , 10107:"WSASYSCALLFAILURE", 10108:"WSASERVICE_NOT_FOUND", 10109:"WSATYPE_NOT_FOUND", 10110:"WSA_E_NO_MORE" 83 | , 10111:"WSA_E_CANCELLED", 10112:"WSAEREFUSED", 11001:"WSAHOST_NOT_FOUND", 11002:"WSATRY_AGAIN", 11003:"WSANO_RECOVERY" 84 | , 11004:"WSANO_DATA", 11005:"WSA_QOS_RECEIVERS", 11006:"WSA_QOS_SENDERS", 11007:"WSA_QOS_NO_SENDERS", 11008:"WSA_QOS_NO_RECEIVERS" 85 | , 11009:"WSA_QOS_REQUEST_CONFIRMED", 11010:"WSA_QOS_ADMISSION_FAILURE", 11011:"WSA_QOS_POLICY_FAILURE", 11012:"WSA_QOS_BAD_STYLE" 86 | , 11013:"WSA_QOS_BAD_OBJECT", 11014:"WSA_QOS_TRAFFIC_CTRL_ERROR", 11015:"WSA_QOS_GENERIC_ERROR", 11016:"WSA_QOS_ESERVICETYPE" 87 | , 11017:"WSA_QOS_EFLOWSPEC", 11018:"WSA_QOS_EPROVSPECBUF", 11019:"WSA_QOS_EFILTERSTYLE", 11020:"WSA_QOS_EFILTERTYPE" 88 | , 11021:"WSA_QOS_EFILTERCOUNT", 11022:"WSA_QOS_EOBJLENGTH", 11023:"WSA_QOS_EFLOWCOUNT", 11024:"WSA_QOS_EUNKOWNPSOBJ" 89 | , 11025:"WSA_QOS_EPOLICYOBJ", 11026:"WSA_QOS_EFLOWDESC", 11027:"WSA_QOS_EPSFLOWSPEC", 11028:"WSA_QOS_EPSFILTERSPEC" 90 | , 11029:"WSA_QOS_ESDMODEOBJ", 11030:"WSA_QOS_ESHAPERATEOBJ", 11031:"WSA_QOS_RESERVED_PETYPE"} 91 | 92 | Static WM_SOCKET(sockDesc, lParam, msg, hwnd) { ; socket monitor 93 | event_cd := lParam & 0xFFFF 94 | errCode := (lParam >> 16) & 0xFFFF 95 | event := (winsock.events.HasProp(event_cd) ? winsock.events.%event_cd% : event_cd) 96 | socket := winsock.sockets[String(sockDesc)] 97 | cb := socket.cb 98 | 99 | If (event="Connect") { 100 | socket.statusCode := errCode 101 | If !(errCode) 102 | socket.ConnectFinish(), socket.status := "Connected" 103 | Else If socket.ConAddrStruct.next 104 | socket.ConnectNext() ; try next addr from GetAddrInfo 105 | Else If !socket.ConAddrStruct.next { 106 | socket.status := "Failed", socket.ConnectFinish() ; all addresses tried, and failed 107 | socket.addr := 0, socket.port := 0, socket.ConAddrIndex := 0 108 | } 109 | } 110 | 111 | if (cb) 112 | cb(socket,event,errCode) 113 | } 114 | 115 | family := 0, protocol := 0, socktype := 0 116 | callback := 0, desc := -1, name := "" 117 | err := "", errnum := 0, LastOp := "" 118 | 119 | host := "", port := 0, addr := "", status := "", statusCode := 0 120 | ConAddrStruct := 0, ConAddrIndex := 0, ConAddrRoot := 0 121 | 122 | block := false ; internal, don't use directly, use this.blocking 123 | 124 | ; sock := {name:"client", family:"Unspec", protocol:"TCP", type:"Stream", block:false, callback:fnc} ; init obj?? 125 | 126 | __New(name, callback:=0, family:="Unspec", protocol:="TCP", socktype:="Stream", desc:=-1) { 127 | 128 | this.name := name, this.desc := desc, this.cb := callback 129 | 130 | If !winsock.family.HasProp(family) ; IPV4:2, NetBIOS:17, IPV6:23, IRDA:26, BTH:32 131 | throw Error("Invalid socket family specified.",-1,"Valid values are:`n`nIPV4,IPV6,NetBIOS,IRDA,BTH") 132 | 133 | this.family := winsock.family.%family% , this.familyName := family 134 | this.socktype := winsock.socktype.%socktype%, this.socktypeName := socktype 135 | this.protocol := winsock.protocol.%protocol%, this.protocolName := protocol 136 | 137 | if !this.CreateSocket(this.desc) 138 | throw Error("Error creating socket.",-1,"Error Code: " this.errnum " / " this.err "`nLast Op: " this.LastOp) 139 | else 140 | winsock.sockets[String(this.desc)] := this 141 | } 142 | 143 | Accept(&addr:=0,&newsock:=0,block:=false) { ; optional VarRef to capture the sockaddr struct of new connection 144 | retCode := 1 145 | sockaddr := winsock.sockaddr(,this.familyName) ; new approach ... 146 | 147 | If ((newdesc := DllCall("Ws2_32\accept","UInt",this.desc,"UPtr",sockaddr.ptr,"Int*",sockaddr.size)) = -1) { 148 | this.WsaLastErr("accept"), retCode := 0, this.Close() 149 | return 0 150 | } 151 | 152 | newsock := winsock("serving-" newdesc, this.cb, this.familyName, this.protocolName, this.socktypeName, newdesc) 153 | (!block) ? newsock.RegisterEvents() : "" ; enable non-blocking mode (default) 154 | 155 | strAddr := newsock.AddrToStr(sockaddr), cSep := InStr(strAddr,":",,,-1) 156 | newsock.addr := RegExReplace(SubStr(strAddr,1,cSep-1),"[\[\]]","") ; record connecting address 157 | newsock.port := SubStr(strAddr,cSep+1) ; record connecting port 158 | winsock.sockets[String(newsock.desc)] := newsock ; catalog new socket 159 | 160 | return retcode 161 | } 162 | 163 | AddrToStr(sock_addr, protocol_info:=0) { 164 | DllCall("Ws2_32\WSAAddressToString","UPtr",sock_addr.ptr ,"UInt",sock_addr.size 165 | ,"UPtr",protocol_info ,"UPtr",0 ,"UInt*",&strSize:=0) 166 | 167 | strbuf := Buffer(strSize * (StrLen(0xFFFF)?2:1),0) 168 | If (DllCall("Ws2_32\WSAAddressToString","UPtr",sock_addr.ptr ,"UInt",sock_addr.size 169 | ,"UPtr",protocol_info ,"UPtr",strbuf.ptr ,"Int*",strSize) = -1) { 170 | this.WsaLastErr() 171 | return "" 172 | } 173 | return StrGet(strbuf) 174 | } 175 | 176 | RegisterEvents(lEvent:=0x3FF) { ; FD_ALL_EVENTS = 0x3FF 177 | this.block := !lEvent ? false : true 178 | 179 | If (result:=this._WSAAsyncSelect(this.desc, A_ScriptHwnd, winsock.wm_msg, lEvent) = -1) 180 | throw Error("WSAASyncSelect failed.", -1) 181 | 182 | ; If !lEvent 183 | ; result := this._ioctlsocket(this.desc, 0x8004667E, &r:=0) ; FIONBIO := 0x8004667E (non-blocking mode) 184 | 185 | return !result 186 | } 187 | 188 | _WSAAsyncSelect(sock_desc, hwnd, msg, lEvent) => 189 | DllCall("Ws2_32\WSAAsyncSelect", "UInt", sock_desc, "UPtr", hwnd, "UInt", msg, "UInt", lEvent) 190 | 191 | Bind(host:=0,port:=0) { 192 | Static AI_PASSIVE:=0x1 ; required flag for calling Bind() 193 | 194 | result := this.GetAddrInfo(host,port,AI_PASSIVE) ; addrinfo struct 195 | retCode := 1 196 | If (DllCall("Ws2_32\bind", "UInt", this.desc, "UPtr", result.addr, "UInt", result.addrlen) = -1) 197 | this.WsaLastErr("bind"), retCode := 0, this.Close() ; close socket 198 | DllCall("Ws2_32\FreeAddrInfo", "UPtr", result.ptr) ; free memory of addrinfo chain 199 | return retCode 200 | } 201 | 202 | Close() { 203 | result := 1 204 | if (DllCall("Ws2_32\closesocket","Int",this.desc) = -1) 205 | this.WsaLastErr("closesocket"), result := 0 206 | Else 207 | winsock.sockets.Delete(String(this.desc)), this.status := "Closed" 208 | return result 209 | } 210 | 211 | Connect(host:=0,port:=0,block:=false) { ; init connect process 212 | (!block) ? (this.RegisterEvents()) : "" 213 | 214 | If (this.ConAddrStruct := this.GetAddrInfo(this.host:=host,port)) { 215 | this.ConAddrRoot := this.ConAddrStruct.ptr 216 | 217 | ; dbg("root: " this.ConAddrRoot " / next: " this.ConAddrStruct.next) 218 | 219 | If !block { 220 | result := this.ConnectNext() 221 | } Else { 222 | While (result := this.ConnectNext()) { ; initiate connect process 223 | ; dbg("still looping?") 224 | Sleep(10) 225 | } 226 | this.ConnectFinish() ; cleanup 227 | } 228 | } 229 | 230 | If (result=-1) && this.WsaLastErr("connect") && (this.err = "WSAEWOULDBLOCK") 231 | result := !result 232 | 233 | return !result 234 | } 235 | 236 | ConnectFinish() { ; free memory of addrinfo chain 237 | DllCall("Ws2_32\FreeAddrInfo", "UPtr", this.ConAddrRoot) 238 | this.ConAddrRoot := 0, this.ConAddrStruct := "" 239 | } 240 | 241 | ConnectNext() { ; ConAddrIndex is initiated at 0. The first call of ConnectNext() sets ConAddrIndex to 1. 242 | this.status := "Connecting" 243 | 244 | this.ConAddrIndex++ ; When ConAddrIndex is > 1, the next line prepares the next address to try, if any. 245 | (this.ConAddrIndex>1) ? (this.ConAddrStruct := winsock.addrinfo(this.ConAddrStruct.next)) : "" ; this.ConAddrStruct.next checked in WM_SOCKET 246 | 247 | ; dbg("ConAddrIndex: " this.ConAddrIndex) 248 | 249 | sockaddr := winsock.sockaddr(this.ConAddrStruct.addr,this.ConAddrStruct.addrlen) ; get sockaddr struct 250 | strAddr := this.AddrToStr(sockaddr), cSep := InStr(strAddr,":",,,-1) 251 | this.addr := RegExReplace(SubStr(strAddr,1,cSep-1), "[\[\]]", "") ; record connecting address 252 | this.port := SubStr(strAddr,cSep+1) ; record connecting port 253 | 254 | If (r := DllCall("Ws2_32\connect","UInt",this.desc,"UPtr",this.ConAddrStruct.addr,"UInt",this.ConAddrStruct.addrlen) = -1) 255 | this.WsaLastErr("connect") 256 | 257 | ; dbg("connect result: " r " / err: " this.err) 258 | 259 | return r 260 | } 261 | 262 | CreateSocket(desc:=-1) { ; make new socket, or take given socket, then call WSAAsyncSelect 263 | result := 1 264 | If ((desc = -1) ; try to open the socket 265 | && (this.desc := DllCall("Ws2_32\socket","Int",this.family,"Int",this.socktype,"Int",this.protocol)) = -1) 266 | this.WsaLastErr("socket"), result := 0 267 | return result 268 | } 269 | 270 | GetAddrInfo(host,port:=0,flags:=0) { 271 | hints := winsock.addrinfo() 272 | hints.family := this.family , hints.protocol := this.protocol 273 | hints.socktype := this.socktype, hints.flags := flags 274 | 275 | if (err := DllCall("Ws2_32\GetAddrInfo",(host?"Str":"UPtr"),host 276 | ,(port?"Str":"UPtr"),(port?String(port):0) 277 | ,"UPtr",hints.ptr 278 | ,"UPtr*",&result:=0)) { 279 | this.WsaLastErr("GetAddrInfo") 280 | return winsock.addrinfo(0) ; return NULL ptr 281 | } 282 | 283 | return winsock.addrinfo(result) 284 | } 285 | 286 | Listen(backlog:=5,block:=false) { ; SOMAXCONN = 5 287 | (!block) ? this.RegisterEvents() : "" ; enable non-blocking mode (default) 288 | retCode := 1 289 | If (DllCall("Ws2_32\listen","UInt",this.desc,"Int",backlog) = -1) 290 | this.WsaLastErr("bind"), retCode := 0, this.Close() ; close socket 291 | return retCode 292 | } 293 | 294 | ; QueueData(buf) => this.OutQueue.Push(buf) ; ??? 295 | 296 | Recv(flags:=0) { 297 | If (this._ioctlsocket(this.desc, 0x4004667F, &bytes) = -1) { ; FIONREAD := 0x4004667F 298 | this.WsaLastErr("ioctlsocket") 299 | If (this.err != "WSAEWOULDBLOCK") 300 | return Buffer(0) 301 | } 302 | 303 | buf := Buffer(bytes,0) 304 | If (DllCall("Ws2_32\recv","UInt",this.desc,"UPtr",buf.ptr,"Int",buf.size,"Int",flags) = -1) { 305 | this.WsaLastErr("recv") 306 | if (this.err != "WSAEWOULDBLOCK") 307 | return Buffer(0) 308 | } 309 | 310 | return buf 311 | } 312 | 313 | _ioctlsocket(socket, cmd, &agrp:=0) => DllCall("Ws2_32\ioctlsocket", "UInt", socket, "UInt", cmd, "UInt*", &agrp:=0) 314 | 315 | Send(buf,flags:=0) { 316 | If (DllCall("Ws2_32\send","UInt",this.desc,"UPtr",buf.ptr,"UInt",buf.size,"UInt",flags) = -1) { 317 | this.WsaLastErr("send") 318 | return 0 319 | } Else return 1 320 | } 321 | 322 | WsaLastErr(LastOp:="") { 323 | If (result := DllCall("Ws2_32\WSAGetLastError")) 324 | this.errnum := result, this.LastOp := LastOp, this.err := winsock.errors.%result% 325 | Else 326 | this.errnum := 0 , this.LastOp := "" , this.err := "" 327 | return result 328 | } 329 | 330 | __Delete() => this.Close() ; close socket if open 331 | 332 | class addrinfo { 333 | Static __New() { 334 | off := {flags: {off:0, type:"Int"} ,addrlen: {off:16, type:"UPtr"} 335 | ,family: {off:4, type:"Int"} ,cannonname:{off:16+(p:=A_PtrSize),type:"UPtr"} 336 | ,socktype: {off:8, type:"Int"} ,addr: {off:16+(p*2),type:"UPtr"} 337 | ,protocol: {off:12, type:"Int"} ,next: {off:16+(p*3),type:"UPtr"}} 338 | this.Prototype.DefineProp("s",{Value:off}) 339 | } 340 | 341 | __New(buf := 0) { 342 | this.DefineProp("_struct",{Value:(!buf) ? Buffer(16 + (A_PtrSize*4),0) : {ptr:buf}}) 343 | this.DefineProp("Ptr",{Get:(o)=>this._struct.ptr}) 344 | } 345 | 346 | __Get(name,p) => NumGet(this.ptr, this.s.%name%.off, this.s.%name%.type) 347 | __Set(name,p,value) => NumPut(this.s.%name%.type, value, this.ptr, this.s.%name%.off) 348 | } 349 | 350 | class sockaddr { ; family := {Unspec:0, IPV4:2, NetBIOS:17, IPV6:23, IRDA:26, BTH:32} 351 | __New(buf := 0, fam := "IPv4") { ; buf=0 && fam=family_text OR buf=ptr && fam=size 352 | Static _fs := winsock.fam_size, _fo := winsock.fam_addr_off, _fL := winsock.fam_len 353 | , _f := winsock.family, _f2 := winsock.family_2 354 | 355 | If !buf && !winsock.family.HasOwnProp(fam) 356 | throw Error("Invalid family for sockaddr creation: " fam, -1) 357 | 358 | this.DefineProp("size",{Value:size := (Type(fam)="String")?(_fs.%fam%):fam}) 359 | this.DefineProp("_struct",{Value:(!buf) ? Buffer(size,0) : {ptr:buf}}) 360 | this.DefineProp("Ptr",{Get:(o)=>this._struct.ptr}) 361 | 362 | (buf) ? (fam := _f2.%NumGet(buf,0,"UShort")%) : NumPut("UShort",_f.%fam%,this.Ptr) ; init family, or get fam name 363 | 364 | ; off := {family: {off:0,type:"UShort"},address: {off:_fo.%family% , len:_fL.%family%} 365 | ; ,port: {off:2,type:"UShort"},flowinfo:{off:4,type:"UInt"} 366 | ; ,scopeid: {off:24,type:"UInt"}} 367 | 368 | off := {family: {off:0,type:"UShort"},flowinfo:{off:4 ,type:"UInt"}, address:{off:_fo.%fam% , len:_fL.%fam%} 369 | , port: {off:2,type:"UShort"}, scopeid:{off:24,type:"UInt"}} 370 | 371 | this.DefineProp("s",{Value:off}) 372 | } 373 | 374 | __Get(name,p) => (this.s.%name%.HasOwnProp("len")) ? this.extract(this.ptr + this.s.%name%.off,this.s.%name%.len) 375 | : NumGet(this.ptr,this.s.%name%.off,this.s.%name%.type) 376 | 377 | __Set(name,p,value) => (this.s.%name%.HasOwnProp("len")) ? this.insert(value,this.ptr + this.s.%name%.off) 378 | : NumPut(this.s.%name%.type,value,this.ptr,this.s.%name%.off) 379 | } 380 | 381 | ; class ProtoInfo { 382 | ; Static __New() { 383 | ; off := 384 | ; } 385 | 386 | ; __New(buf:=0) { 387 | ; Static u := StrLen(Chr(0xFFFF)) 388 | ; this._struct := (!buf) ? Buffer(u?:,0) : {ptr:buf} 389 | 390 | ; this.DefineProp("Ptr",{Get:(o)=>this._struct.ptr}) 391 | ; } 392 | ; } 393 | 394 | } 395 | 396 | 397 | ; typedef struct _WSAPROTOCOL_INFOA { 398 | ; DWORD dwServiceFlags1; 399 | ; DWORD dwServiceFlags2; 400 | ; DWORD dwServiceFlags3; 401 | ; DWORD dwServiceFlags4; 402 | ; DWORD dwProviderFlags; 403 | ; GUID ProviderId; 404 | ; DWORD dwCatalogEntryId; 405 | ; WSAPROTOCOLCHAIN ProtocolChain; 406 | ; int iVersion; 407 | ; int iAddressFamily; 408 | ; int iMaxSockAddr; 409 | ; int iMinSockAddr; 410 | ; int iSocketType; 411 | ; int iProtocol; 412 | ; int iProtocolMaxOffset; 413 | ; int iNetworkByteOrder; 414 | ; int iSecurityScheme; 415 | ; DWORD dwMessageSize; 416 | ; DWORD dwProviderReserved; 417 | ; CHAR szProtocol[WSAPROTOCOL_LEN + 1]; 418 | ; } WSAPROTOCOL_INFOA, *LPWSAPROTOCOL_INFOA; 419 | 420 | 421 | ; ======================================================================== 422 | ; Old methods 423 | ; ======================================================================== 424 | 425 | ; MsgSize() { 426 | ; static FIONREAD := 0x4004667F 427 | ; if (DllCall("Ws2_32\ioctlsocket", "UInt", this.Socket, "UInt", FIONREAD, "UInt*", &argp:=0) == -1) 428 | ; throw Error("Error calling ioctlsocket",, this.GetLastError()) 429 | ; return argp 430 | ; } 431 | 432 | 433 | 434 | ; MsgMonitor(wParam, lParam, Msg, hWnd) { 435 | ; dbg("msg: " Format("0x{:X}",msg) " / hwnd: " hWnd " / wParam: " wParam " / lParam: " lParam) 436 | 437 | ; if (Msg != Socket.WM_SOCKET || wParam != this.Socket) 438 | ; return 439 | 440 | ; dbg("it worked? / " Type(this.recvCB) " / ID: " this.SockID) 441 | 442 | ; If (RecvCB := this.recvCB) { ; data, date, EventType, socket 443 | ; if (lParam & Socket.FD_READ) { 444 | ; dbg("reading... / ID: " this.SockID) 445 | 446 | 447 | ; RecvCB("", "read", this) ; this.Recv() 448 | ; } else if (lParam & Socket.FD_ACCEPT) { 449 | ; dbg("accepting... / ID: " this.SockID) 450 | ; RecvCB("", "accept", this) ; this.Accept() 451 | 452 | ; } else if (lParam & Socket.FD_CLOSE) { 453 | ; dbg("closing... / ID: " this.SockID) 454 | ; this.EventProcUnregister(), this.Disconnect(), RecvCB("", "close", this) 455 | ; } 456 | ; } 457 | ; } 458 | 459 | ; EventProcRegister(lEvent) { 460 | ; this.AsyncSelect(lEvent) 461 | ; if !this.Bound { 462 | ; this.Bound := ObjBindMethod(this,"MsgMonitor") 463 | ; OnMessage Socket.WM_SOCKET, this.Bound ; register event function 464 | ; } 465 | ; } 466 | 467 | ; EventProcUnregister() { 468 | ; this.AsyncSelect(0) 469 | ; if this.Bound { 470 | ; OnMessage Socket.WM_SOCKET, this.Bound, 0 ; unregister event function 471 | ; this.Bound := False 472 | ; } 473 | ; } 474 | 475 | ; AsyncSelect(lEvent) { 476 | ; if (r := DllCall("Ws2_32\WSAAsyncSelect" 477 | ; , "UInt", this.Socket ; s 478 | ; , "Ptr", A_ScriptHwnd ; hWnd 479 | ; , "UInt", Socket.WM_SOCKET ; wMsg 480 | ; , "UInt", lEvent) == -1) ; lEvent 481 | ; throw Error("Error calling WSAAsyncSelect ---> SockID: " this.SockID " / " this.Socket,, this.GetLastError()) 482 | ; } 483 | 484 | ; GetLastError() { 485 | ; return DllCall("Ws2_32\WSAGetLastError") 486 | ; } 487 | 488 | ; SetBroadcast(Enable) { ; for UDP sockets only -- don't know what this does yet 489 | ; static SOL_SOCKET := 0xFFFF, SO_BROADCAST := 0x20 490 | ; if (DllCall("Ws2_32\setsockopt" 491 | ; , "UInt", this.Socket ; SOCKET s 492 | ; , "Int", SOL_SOCKET ; int level 493 | ; , "Int", SO_BROADCAST ; int optname 494 | ; , "UInt", !!Enable ; *char optval 495 | ; , "Int", 4) == -1) ; int optlen 496 | ; throw Error("Error calling setsockopt",, this.GetLastError()) 497 | ; } -------------------------------------------------------------------------------- /lib/socket/SocketService.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2.0 2 | 3 | #Include Socket.ahk 4 | 5 | InitServer() { 6 | sock := winsock("server", callback, "IPV4") 7 | sock.Bind("0.0.0.0", 33660) 8 | sock.Listen() 9 | 10 | callback(sock, event, err) { 11 | if (sock.name = "server") || instr(sock.name, "serving-") { 12 | if (event = "accept") { 13 | sock.Accept(&addr, &newsock) ; pass &addr param to extract addr of connected machine 14 | } else if (event = "close") { 15 | } else if (event = "read") { 16 | If !(buf := sock.Recv()).size 17 | return 18 | 19 | ; 返回html 20 | html_body := '

open potplayer...

' 21 | httpResponse := "HTTP/1.1 200 0K`r`n" 22 | . "Content-Type: text/html; charset=UTF-8`r`n" 23 | . "Content-Length: " StrLen(html_body) "`r`n" 24 | . "`r`n" 25 | httpResponse := httpResponse html_body 26 | strbuf := Buffer(StrPut(httpResponse, "UTF-8")) 27 | StrPut(httpResponse, strbuf, "UTF-8") 28 | sock.Send(strbuf) 29 | sock.ConnectFinish() 30 | 31 | ; 得到回链 32 | request := strget(buf, "UTF-8") 33 | RegExMatch(request, "GET /(.+?) HTTP/1.1", &match) 34 | if (match == "") { 35 | return 36 | } 37 | backlink := match[1] 38 | if (!InStr(backlink, "path=")) { 39 | return 40 | } 41 | 42 | ; 打开potplayer 43 | Run(backlink) 44 | ; 关闭 notion打开的网页标签 45 | Send "^w" 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /lib/socket/example/_socket_example.ahk: -------------------------------------------------------------------------------- 1 | #Include ./_socket.ahk 2 | 3 | #Requires AutoHotkey v2.0-beta ; 32-bit 4 | 5 | 6 | g := Gui() 7 | g.OnEvent("close",gui_close) 8 | g.OnEvent("escape",gui_close) 9 | g.Add("Edit","vMyEdit Multi w500 h500 ReadOnly","") 10 | g.Show() 11 | 12 | 13 | gui_close(*) { 14 | ExitApp 15 | } 16 | 17 | print_gui(str) { 18 | global g 19 | AppendText(g["MyEdit"].hwnd,str) 20 | } 21 | 22 | client_data := "" 23 | 24 | F1::test_google() 25 | F2::test_server() 26 | F3::test_client() 27 | F4::{ ; clear log 28 | client_data := "" 29 | g["MyEdit"].Value := "" 30 | } 31 | F5::test_error() 32 | 33 | F6::test_send() 34 | 35 | F7::test_google_blocking() 36 | 37 | test_error() { 38 | sock := winsock("client-err",cb,"IPV4") 39 | 40 | ; sock.Connect("www.google.com",80) ; 127.0.0.1",33660 ; www.google.com",80 41 | result := sock.Connect("localhost",5678) ; error is returned in callback 42 | } 43 | 44 | test_google() { 45 | sock := winsock("client",cb,"IPV6") 46 | 47 | sock.Connect("www.google.com",80) ; 127.0.0.1",33660 ; www.google.com",80 48 | ; sock.Connect("www.autohotkey.com",80) ; www.autohotkey.com/download/2.0/ 49 | 50 | print_gui("Client connecting...`r`n`r`n") 51 | } 52 | 53 | test_server() { ; server - uses async to accept sockets 54 | sock := winsock("server",cb,"IPV4") 55 | sock.Bind("0.0.0.0",33660) ; "0.0.0.0",33660 56 | 57 | sock.Listen() 58 | 59 | print_gui("Server listening...`r`n`r`n") 60 | } 61 | 62 | test_client() { ; client 63 | sock := winsock("client",cb,"IPV4") 64 | 65 | sock.Connect("127.0.0.1",33660) ; 127.0.0.1",33660 ; www.google.com",80 66 | 67 | print_gui("Client connecting...`r`n`r`n") 68 | } 69 | 70 | test_google_blocking() { ; this uses no async at all 71 | sock := winsock("client",cb,"IPV6") 72 | 73 | domain := "www.autohotkey.com" ; www.google.com / www.autohotkey.com 74 | port := 443 ; 443 / 80 75 | 76 | If !(r1 := sock.Connect(domain,port,true)) { ; www.google.com 77 | msgbox "Could not connect: " sock.err 78 | return 79 | } 80 | 81 | get_req := "GET / HTTP/1.1`r`n" 82 | . "Host: " domain "`r`n`r`n" 83 | 84 | strbuf := Buffer(StrPut(get_req,"UTF-8"),0) 85 | StrPut(get_req,strbuf,"UTF-8") 86 | 87 | If !(r2 := sock.Send(strbuf)) { 88 | Msgbox "Send failed." 89 | return 90 | } 91 | 92 | While !(buf:=sock.Recv()).size ; wait for data 93 | Sleep 10 94 | 95 | result := "" 96 | while buf.size { 97 | result .= StrGet(buf,"UTF-8") ; collect data 98 | buf:=sock.Recv() 99 | } 100 | 101 | sock.Close() 102 | 103 | A_Clipboard := result 104 | msgbox "Done. Check clipboard." 105 | } 106 | 107 | test_send() { ; the connecting client doesn't use async 108 | sock := winsock("client", cb, "IPV4") 109 | If !(r := sock.Connect("127.0.0.1", 33660, true)) { ; connect as blocking, setting param #3 to TRUE 110 | msgbox "Could not connect." 111 | return ; handle a failed connect properly 112 | } 113 | 114 | msg := "abc" 115 | strbuf := Buffer(StrLen(msg) + 1) ; for UTF-8, take strLen() + 1 as the buffer size 116 | StrPut(msg, strbuf, "UTF-8") 117 | 118 | r := sock.Send(strbuf) ; check send result if necessary 119 | 120 | sock.Close() 121 | } 122 | 123 | cb(sock, event, err) { 124 | global client_data 125 | 126 | dbg("sock.name: " sock.name " / event: " event " / err: " err " ===========================") 127 | 128 | if (sock.name = "client") { 129 | 130 | if (event = "close") { 131 | msgbox "Address: " sock.addr "`n" "Port: " sock.port "`n`n" client_data 132 | A_Clipboard := client_data 133 | client_data := "" 134 | sock.close() 135 | 136 | } else if (event = "connect") { ; connection complete, if (err = 0) then it is a success 137 | print_gui("Client...`r`nConnect Addr: " sock.addr "`r`nConnect Port: " sock.port "`r`n`r`n") ; this does not check for failure 138 | 139 | } else if (event = "write") { ; client ready to send/write 140 | get_req := "GET / HTTP/1.1`r`n" 141 | . "Host: www.google.com`r`n`r`n" 142 | 143 | strbuf := Buffer(StrPut(get_req,"UTF-8"),0) 144 | StrPut(get_req,strbuf,"UTF-8") 145 | 146 | 147 | sock.Send(strbuf) 148 | sock.Close() 149 | 150 | print_gui("Client sends to server:`r`n" 151 | . "======================`r`n" 152 | . Trim(get_req,"`r`n") "`r`n" 153 | . "======================`r`n`r`n") 154 | 155 | } else if (event = "read") { ; there is data to be read 156 | buf := sock.Recv() 157 | client_data .= StrGet(buf,"UTF-8") 158 | } 159 | 160 | 161 | 162 | } else if (sock.name = "server") || instr(sock.name,"serving-") { 163 | 164 | if (event = "accept") { 165 | sock.Accept(&addr,&newsock) ; pass &addr param to extract addr of connected machine 166 | 167 | print_gui("======================`r`n" 168 | . "Server processes client connection:`r`n" 169 | . "address: " newsock.addr "`r`n" 170 | . "======================`r`n`r`n" ) 171 | 172 | } else if (event = "close") { 173 | 174 | 175 | } else if (event = "read") { 176 | 177 | If !(buf := sock.Recv()).size ; get buffer, check size, return on zero-size buffer 178 | return 179 | 180 | dbg("buf size: " buf.size) 181 | 182 | buf_string := strget(buf,"UTF-8") 183 | RegExMatch(buf_string, "GET /(.+?) HTTP/1.1", &match) 184 | 185 | if (match == "") { 186 | return 187 | } 188 | backlink := match[1] 189 | 190 | if (backlink == "favicon.ico") { 191 | return 192 | } 193 | 194 | print_gui("======================`r`n" 195 | . "Server recieved from client:`r`n" 196 | . Trim(backlink,"`r`n") "`r`n" 197 | . "======================`r`n`r`n") 198 | 199 | ; 标准响应头 200 | get_req := "HTTP/1.1 200 0K`r`n" 201 | . "Content-Type: text/html`r`n" 202 | . "Content-Length: 26`r`n" 203 | . "`r`n" 204 | get_req := get_req '

open potplayer...

' 205 | strbuf := Buffer(StrPut(get_req,"UTF-8")) 206 | StrPut(get_req,strbuf,"UTF-8") 207 | Send "^w" 208 | ; 使用命令访问指定网址 209 | ; Run("D:\Code\project\autohotkey\markdown2potplayer\lib\note2potplayer\note2potplayer.exe jv://open?path=d:\test.mp4",,"Hide",,) 210 | MsgBox "send finish" 211 | } 212 | 213 | } else if (sock.name = "client-err") { ; this is how you catch an error with async / non-blocking 214 | 215 | if (event = "connect") && err 216 | msgbox sock.name ": " event ": err: " err 217 | 218 | } 219 | 220 | ; dbg(sock.name ": " event ": err: " err) ; to make it easier to see all the events 221 | } 222 | 223 | 224 | 225 | ; ========================================================== 226 | ; support funcs 227 | ; ========================================================== 228 | 229 | AppendText(EditHwnd, sInput, loc := "bottom") { ; Posted by TheGood: https://autohotkey.com/board/topic/52441-append-text-to-an-edit-control/#entry328342 230 | insertPos := (loc="bottom") ? SendMessage(0x000E, 0, 0,, "ahk_id " EditHwnd) : 0 ; WM_GETTEXTLENGTH 231 | r1 := SendMessage(0x00B1, insertPos, insertPos,, "ahk_id " EditHwnd) ; EM_SETSEL - place cursor for insert 232 | r2 := SendMessage(0x00C2, False, StrPtr(sInput),, "ahk_id " EditHwnd) ; EM_REPLACESEL - insert text at cursor 233 | } 234 | 235 | dbg(_in) { ; AHK v2 236 | Loop Parse _in, "`n", "`r" 237 | OutputDebug "AHK: " A_LoopField 238 | } -------------------------------------------------------------------------------- /lib/sqlite/SQLite3.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livelycode36/markdown2potplayer/34dba23ba397ac893a3e0372dfa53fd8805eef4b/lib/sqlite/SQLite3.dll -------------------------------------------------------------------------------- /lib/sqlite/SqliteControl.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2.0.0 2 | #Include "Class_SQLiteDB.ahk" 3 | 4 | ; 数据库文件路径 5 | db_file_path := "config.db" 6 | table_name_config := "config" 7 | 8 | InitSqlite() { 9 | initTableConfig() 10 | } 11 | 12 | initTableConfig(){ 13 | ; 定义 config 表的列 14 | config_columns := "key TEXT PRIMARY KEY, value TEXT" 15 | EnsureTableExists(table_name_config, config_columns) 16 | 17 | ; 初始化插入数据 18 | config_data := { 19 | ; 其他微调设置 20 | path: "C:\Program Files\DAUM\PotPlayer\PotPlayerMini64.exe", 21 | is_stop: "0", 22 | reduce_time: "0", 23 | app_name: "Obsidian.exe", 24 | url_protocol: "jv://open", 25 | path_is_encode: "0", 26 | remove_suffix_of_video_file: "1", 27 | send_image_delays: "1000", 28 | ; 字幕模板 29 | subtitle_template: "subtitle: [{subtitleTimeRange}]({link}) {subtitleOrigin}", 30 | ; 回链的设置 31 | title: "{name} | {time}", 32 | template: 33 | "{userNote}" 34 | . "`n" 35 | . "video:{title}" 36 | . "`n", 37 | image_template: 38 | "{userNote}" 39 | . "`n" 40 | . "image:{image}" 41 | . "`n" 42 | . "video:{title}" 43 | . "`n", 44 | ; 回链快捷键相关 45 | hotkey_subtitle: "!t", 46 | hotkey_subtitle_previous_once: "^j", 47 | hotkey_subtitle_current_once: "^k", 48 | hotkey_subtitle_next_once: "^l", 49 | hotkey_subtitle_previous_loop: "^!j", 50 | hotkey_subtitle_current_loop: "^!k", 51 | hotkey_subtitle_next_loop: "^!l", 52 | hotkey_user_note: "!n", 53 | hotkey_backlink: "!g", 54 | hotkey_iamge_backlink: "^!g", 55 | hotkey_image_screenshot_tool_hotkeys: "!a", 56 | hotkey_image_edit: "!e", 57 | image_edit_detection_time: "20", 58 | hotkey_ab_fragment: "F1", 59 | ab_fragment_detection_delays: "1000", 60 | loop_ab_fragment: "0", 61 | hotkey_ab_circulation: "F2", 62 | } 63 | 64 | DB := OpenLocalDB() 65 | ; 插入数据 66 | for key, value in config_data.OwnProps() { 67 | if CheckKeyExist(key) { 68 | continue 69 | } 70 | 71 | UpdateOrInsertConfig(key, value) 72 | } 73 | } 74 | 75 | UpdateOrInsertConfig(key, value) { 76 | DB := OpenLocalDB() 77 | 78 | ; 插入或更新配置项 79 | SQL_InsertOrUpdate := "INSERT OR REPLACE INTO " table_name_config " (key, value) VALUES ('" key "', '" value "');" 80 | if !DB.Exec(SQL_InsertOrUpdate) { 81 | MsgBox("无法插入或更新配置项 '" table_name_config "'`n错误信息: " . DB.ErrorMsg) 82 | DB.CloseDB() 83 | ExitApp 84 | } 85 | DB.CloseDB() 86 | } 87 | 88 | EnsureTableExists(tableName, tableColumns) { 89 | if !TableExist(tableName) { 90 | DB := OpenLocalDB() 91 | ; 创建表 92 | SQL_CreateTable := "CREATE TABLE IF NOT EXISTS " tableName " (" tableColumns ");" 93 | 94 | if !DB.Exec(SQL_CreateTable) { 95 | MsgBox("无法创建表 " tableName "`n错误信息: " DB.ErrorMsg) 96 | DB.CloseDB() 97 | ExitApp 98 | } 99 | DB.CloseDB() 100 | } 101 | } 102 | 103 | OpenLocalDB() { 104 | ; 创建 SQLiteDB 实例 105 | DB := SQLiteDB() 106 | 107 | ; 打开或创建数据库 108 | if !DB.OpenDB(db_file_path) { 109 | MsgBox("无法打开或创建数据库: " db_file_path "`n错误信息: " DB.ErrorMsg) 110 | ExitApp 111 | } 112 | return DB 113 | } 114 | 115 | TableExist(table_name) { 116 | DB := OpenLocalDB() 117 | 118 | ; 检查 config 表是否存在 119 | SQL_CheckTable := "SELECT name FROM sqlite_master WHERE type='table' AND name='" table_name "';" 120 | Result := "" 121 | if !DB.GetTable(SQL_CheckTable, &Result) { 122 | MsgBox("无法检查表 " table_name " 是否存在`n错误信息: " . DB.ErrorMsg) 123 | DB.CloseDB() 124 | ExitApp 125 | } 126 | 127 | DB.CloseDB() 128 | ; 判断表是否存在 129 | if Result.RowCount > 0 { 130 | ; MsgBox("表 " table_name " 存在。") 131 | return true 132 | } else { 133 | ; MsgBox("表 " table_name " 不存在。") 134 | return false 135 | } 136 | } 137 | 138 | CheckKeyExist(key) { 139 | DB := OpenLocalDB() 140 | 141 | SQL_Check_Key := "SELECT COUNT(*) FROM " table_name_config " WHERE key = '" key "'" 142 | Result := "" 143 | If !DB.GetTable(SQL_Check_Key, &Result) { 144 | MsgBox "打开数据表" table_name_config "失败!" 145 | ExitApp 146 | } 147 | 148 | DB.CloseDB() 149 | 150 | ; 如果 key 不存在 151 | If Result.RowCount = 0 || Result.Rows[1][1] = 0 { 152 | return false 153 | } else { 154 | return true 155 | } 156 | } 157 | 158 | GetKey(key) { 159 | DB := OpenLocalDB() 160 | 161 | ; 读取 key 为 'app_name' 的值 162 | SQL_SelectValue := "SELECT value FROM " table_name_config " WHERE key = '" key "';" 163 | Result := "" 164 | if !DB.GetTable(SQL_SelectValue, &Result) { 165 | MsgBox("无法读取配置项 '" key "'`n错误信息: " . DB.ErrorMsg) 166 | DB.CloseDB() 167 | ExitApp 168 | } 169 | 170 | ; 显示结果 171 | if Result.RowCount > 0 { 172 | ; MsgBox("配置项 '" key "' 的值为: " . Result.Rows[1][1]) ; 获取第一行第一列的值 173 | return Result.Rows[1][1] 174 | } else { 175 | ; MsgBox("配置项 '" key "' 不存在。") 176 | return false 177 | } 178 | 179 | DB.CloseDB() 180 | } -------------------------------------------------------------------------------- /lib/srt.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2.0 2 | #Include CharsetDetect.ahk 3 | 4 | SubtitlesDataFromSrt(srtPath) { 5 | if not FileExist(srtPath) { 6 | MsgBox "The target file:" srtPath " does not exist." 7 | Exit 8 | } 9 | 10 | detect := TextEncodingDetect() 11 | FileEncoding(detect.DetectEncoding(srtPath)) 12 | 13 | srtContent := FileRead(srtPath) 14 | 15 | pattern := "(\d{2}:\d{2}:\d{2},\d{3}) --> (\d{2}:\d{2}:\d{2},\d{3})\R([\s\S]*?)(?:\R\R|\z)" 16 | 17 | ; 查找所有匹配项并转换时间为毫秒 18 | subtitles_data := [] 19 | pos := 1 20 | while (pos := RegExMatch(srtContent, pattern, &match, pos)) { 21 | subtitles_data.Push({ 22 | timeStart: SrtTimeToMs(match[1]), 23 | timeEnd: SrtTimeToMs(match[2]), 24 | subtitle: Trim(match[3]) 25 | }) 26 | pos += match.Len 27 | } 28 | return subtitles_data 29 | } 30 | 31 | ; 函数:根据时间戳查找对应的字幕 32 | FindSubtitleByTimestamp(timestamp, subtitles_data) { 33 | for subtitle_data in subtitles_data { 34 | if (subtitle_data.timeStart <= timestamp && timestamp <= subtitle_data.timeEnd) { 35 | return subtitle_data 36 | } 37 | } 38 | return false 39 | } 40 | 41 | ; 测试 42 | ; subtitles := SubtitlesFromSrt("../test.srt") 43 | 44 | ; for subtitle in subtitles { 45 | ; MsgBox("找到匹配的字幕:`n开始时间: " MsToSrtTime(subtitle.startTime) 46 | ; "`n结束时间: " MsToSrtTime(subtitle.endTime) 47 | ; "`n字幕内容: " subtitle.subtitle) 48 | ; } 49 | 50 | ; userTimestamp := 180000 ; 假设用户输入的时间戳是180秒(180000毫秒) 51 | ; result := FindSubtitleByTimestamp(userTimestamp, subtitles) 52 | 53 | ; if (result) { 54 | ; MsgBox("找到匹配的字幕:`n开始时间: " MsToSrtTime(result.startTime) 55 | ; "`n结束时间: " MsToSrtTime(result.endTime) 56 | ; "`n字幕内容: " result.subtitle) 57 | ; } else { 58 | ; MsgBox("未找到匹配的字幕。") 59 | ; } 60 | 61 | ; 辅助函数 62 | ; 将毫秒转换回SRT时间格式 63 | MsToSrtTime(ms) { 64 | hours := Floor(ms / 3600000) 65 | minutes := Floor((ms - hours * 3600000) / 60000) 66 | seconds := Floor((ms - hours * 3600000 - minutes * 60000) / 1000) 67 | milliseconds := ms - hours * 3600000 - minutes * 60000 - seconds * 1000 68 | 69 | return Format("{:02d}:{:02d}:{:02d},{:03d}", hours, minutes, seconds, milliseconds) 70 | } 71 | 72 | ; SrtTimeToMs 函数定义(与之前相同) 73 | SrtTimeToMs(timeStr) { 74 | timeParts := StrSplit(timeStr, ":") 75 | ms := StrSplit(timeParts[3], ",")[2] 76 | 77 | return (Integer(timeParts[1]) * 3600000) + ; 小时 78 | (Integer(timeParts[2]) * 60000) + ; 分钟 79 | (Integer(StrSplit(timeParts[3], ",")[1]) * 1000) + ; 秒 80 | Integer(ms) ; 毫秒 81 | } 82 | 83 | ; 获取当前/上一/下一字幕片段 84 | ; mode: "current" | "prev" | "next" 85 | GetSubtitleSegmentByTimestamp(subtitles_data, milliseconds, mode := "current") { 86 | if subtitles_data.Length = 0 { 87 | return false 88 | } 89 | 90 | ; 初始化索引变量 91 | idx := 0 92 | 93 | ; 遍历字幕数据,查找时间戳对应的位置 94 | for i, subtitle in subtitles_data { 95 | ; 如果时间戳在当前字幕的时间范围内 96 | if (subtitle.timeStart <= milliseconds && milliseconds <= subtitle.timeEnd) { 97 | idx := i ; 记录当前字幕索引 98 | break 99 | } 100 | ; 如果时间戳小于当前字幕的开始时间(说明时间戳在两个字幕之间) 101 | if (milliseconds < subtitle.timeStart) { 102 | idx := i ; 记录当前字幕索引(时间戳位于前一个字幕之后) 103 | break 104 | } 105 | } 106 | 107 | ; 处理时间戳在所有字幕之前的情况 108 | if (idx = 0) { 109 | ; timestamp 在所有字幕之前 110 | if (mode = "current" || mode = "next") 111 | return subtitles_data[1] ; 返回第一个字幕 112 | else 113 | return false ; prev模式下返回false 114 | } 115 | 116 | ; 根据不同模式处理 117 | if (mode = "current") { 118 | ; 当前模式:返回包含时间戳的字幕,或最接近的前一个字幕 119 | if (subtitles_data[idx].timeStart <= milliseconds && milliseconds <= subtitles_data[idx].timeEnd) { 120 | return subtitles_data[idx] ; 时间戳在当前字幕范围内 121 | } else if (idx > 1) { 122 | return subtitles_data[idx-1] ; 返回前一个字幕 123 | } else { 124 | return subtitles_data[1] ; 如果是第一个字幕,返回第一个 125 | } 126 | } else if (mode = "prev") { 127 | ; 前一个模式:返回前一个字幕 128 | if (idx = 1) { 129 | return subtitles_data[1] ; 如果已经是第一个,返回第一个 130 | } else { 131 | return subtitles_data[idx-1] ; 返回前一个字幕 132 | } 133 | } else if (mode = "next") { 134 | ; 下一个模式:返回下一个字幕 135 | if (idx >= subtitles_data.Length) { 136 | return subtitles_data[subtitles_data.Length] ; 如果已经是最后一个,返回最后一个 137 | } else { 138 | return subtitles_data[idx+1] ; 返回下一个字幕 139 | } 140 | } 141 | 142 | return false ; 默认返回false 143 | } -------------------------------------------------------------------------------- /lib/word/word.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2.0 2 | #SingleInstance force 3 | 4 | AppMain() 5 | AppMain(){ 6 | text := ReceivParameter() 7 | 8 | result := DllCall("user32.dll\OpenClipboard") 9 | If(result = 0){ 10 | MsgBox "OpenClipboard failed" 11 | return 12 | } 13 | DllCall("user32.dll\EmptyClipboard") 14 | 15 | html_code := DllCall("user32.dll\RegisterClipboardFormat", "Ptr", StrBuf("HTML Format","UTF-16")) 16 | 17 | DllCall("user32.dll\SetClipboardData", "UInt", html_code, "Ptr", StrBuf(text,"UTF-8")) 18 | DllCall("user32.dll\CloseClipboard") 19 | 20 | Send "{LCtrl down}" 21 | Send "{v}" 22 | Send "{LCtrl up}" 23 | 24 | ; 复制或转换字符串. 25 | StrBuf(str, encoding) { 26 | ; 计算所需的大小并分配缓冲. 27 | buf := Buffer(StrPut(str, encoding)) 28 | ; 复制或转换字符串. 29 | StrPut(str, buf, encoding) 30 | return buf 31 | } 32 | } 33 | ReceivParameter(){ 34 | ; 如果没有参数 35 | if (A_Args.Length = 0) { 36 | return false 37 | } 38 | 39 | params := "" 40 | ; 循环遍历参数并显示在控制台 41 | for n, param in A_Args{ 42 | params .= param " " 43 | } 44 | return Trim(params) 45 | } 46 | 47 | -------------------------------------------------------------------------------- /markdown2potplayer.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2.0 2 | #SingleInstance force 3 | #Include "lib\note2potplayer\RegisterUrlProtocol.ahk" 4 | #Include "lib\MyTool.ahk" 5 | #Include "lib\TimeTool.ahk" 6 | #Include "lib\TemplateParser.ahk" 7 | #Include "lib\sqlite\SqliteControl.ahk" 8 | #Include lib\srt.ahk 9 | #Include lib\Render.ahk 10 | #Include lib\socket\SocketService.ahk 11 | #Include lib\entity\Config.ahk 12 | #Include lib\entity\MediaData.ahk 13 | #Include lib\PotplayerControl.ahk 14 | #Include lib\gui\GuiControl.ahk 15 | #Include lib\gui\i18n\I18n.ahk 16 | #Include lib/PotplayerFragment.ahk 17 | 18 | main() 19 | 20 | main() { 21 | global 22 | TraySetIcon("lib/icon.png", 1, false) 23 | 24 | InitSqlite() 25 | app_config := Config() 26 | potplayer_control := PotplayerControl(app_config.PotplayerProcessName) 27 | 28 | InitGui(app_config, potplayer_control) 29 | 30 | InitServer() 31 | 32 | RegisterUrlProtocol(app_config.UrlProtocol) 33 | 34 | RegisterHotKey() 35 | } 36 | 37 | RegisterHotKey() { 38 | HotIf CheckCurrentProgram 39 | if(app_config.HotkeySubtitle!= ""){ 40 | Hotkey(app_config.HotkeySubtitle " Up", Potplayer2Obsidian) 41 | } 42 | if (app_config.HotkeyUserNote != "") { 43 | Hotkey(app_config.HotkeyUserNote " Up", Potplayer2Obsidian) 44 | } 45 | if (app_config.HotkeyBacklink!= ""){ 46 | Hotkey(app_config.HotkeyBacklink " Up", Potplayer2Obsidian) 47 | } 48 | if (app_config.HotkeyIamgeBacklink!= ""){ 49 | Hotkey(app_config.HotkeyIamgeBacklink " Up", Potplayer2ObsidianImage) 50 | } 51 | if (app_config.HotkeyImageEdit != "") { 52 | Hotkey(app_config.HotkeyImageEdit " Up", Potplayer2ObsidianImage) 53 | } 54 | if (app_config.HotkeyAbFragment!= ""){ 55 | Hotkey(app_config.HotkeyAbFragment " Up", Potplayer2ObsidianFragment) 56 | } 57 | if (app_config.HotkeyAbCirculation!= ""){ 58 | Hotkey(app_config.HotkeyAbCirculation " Up", Potplayer2ObsidianFragment) 59 | } 60 | if (app_config.HotkeySubtitlePreviousOnce!= ""){ 61 | Hotkey(app_config.HotkeySubtitlePreviousOnce, (*) => SubtitleFragmentPlay("prev", "single")) 62 | } 63 | if (app_config.HotkeySubtitleCurrentOnce != "") { 64 | Hotkey(app_config.HotkeySubtitleCurrentOnce, (*) => SubtitleFragmentPlay("current", "single")) 65 | } 66 | if (app_config.HotkeySubtitleNextOnce!= ""){ 67 | Hotkey(app_config.HotkeySubtitleNextOnce, (*) => SubtitleFragmentPlay("next", "single")) 68 | } 69 | if (app_config.HotkeySubtitlePreviousLoop!= ""){ 70 | Hotkey(app_config.HotkeySubtitlePreviousLoop, (*) => SubtitleFragmentPlay("prev", "loop")) 71 | } 72 | if (app_config.HotkeySubtitleCurrentLoop!= "") { 73 | Hotkey(app_config.HotkeySubtitleCurrentLoop, (*) => SubtitleFragmentPlay("current", "loop")) 74 | } 75 | if (app_config.HotkeySubtitleNextLoop!= ""){ 76 | Hotkey(app_config.HotkeySubtitleNextLoop, (*) => SubtitleFragmentPlay("next", "loop")) 77 | } 78 | } 79 | 80 | RefreshHotkey(old_hotkey, new_hotkey, callback) { 81 | try { 82 | ; 情况1:用户删除热键 83 | if new_hotkey == "" { 84 | if (old_hotkey != "") { 85 | Hotkey old_hotkey " Up", "off" 86 | } 87 | } else { 88 | ; 情况2:用户重设热键 89 | if (old_hotkey != "") { 90 | Hotkey old_hotkey " Up", "off" 91 | } 92 | HotIf CheckCurrentProgram 93 | Hotkey new_hotkey " Up", callback 94 | } 95 | } 96 | catch Error as err { 97 | ; 热键设置无效 98 | ; 防止无效的快捷键产生报错,中断程序 99 | Exit 100 | } 101 | } 102 | 103 | RefreshHotkeyWithoutUp(old_hotkey, new_hotkey, callback) { 104 | try { 105 | ; 情况1:用户删除热键 106 | if new_hotkey == "" { 107 | if (old_hotkey != "") { 108 | Hotkey old_hotkey, "off" 109 | } 110 | } else { 111 | ; 情况2:用户重设热键 112 | if (old_hotkey != "") { 113 | Hotkey old_hotkey, "off" 114 | } 115 | HotIf CheckCurrentProgram 116 | Hotkey new_hotkey, callback 117 | } 118 | } 119 | catch Error as err { 120 | ; 热键设置无效 121 | ; 防止无效的快捷键产生报错,中断程序 122 | Exit 123 | } 124 | } 125 | 126 | CheckCurrentProgram(*) { 127 | programs := app_config.PotplayerProcessName "`n" app_config.NoteAppName 128 | Loop Parse programs, "`n" { 129 | program := A_LoopField 130 | if program { 131 | if WinActive("ahk_exe " program) { 132 | return true 133 | } 134 | } 135 | } 136 | return false 137 | } 138 | 139 | lastMediaPath := "" 140 | ; 【主逻辑】将Potplayer的播放链接粘贴到Obsidian中 141 | Potplayer2Obsidian(hotkey) { 142 | global lastMediaPath 143 | ; 取消可能正在进行的截图等待 144 | CancelScreenshotWaiting() 145 | 146 | ReleaseCommonUseKeyboard() 147 | 148 | if (lastMediaPath == "") { 149 | lastMediaPath := UrlDecode(GetMediaPath()) 150 | } 151 | 152 | nameInPath := UrlDecode(GetNameForPath(lastMediaPath)) 153 | nameInPotplayer := StrReplace(GetPotplayerTitle(app_config.PotplayerProcessName), " - PotPlayer", "") 154 | 155 | if (nameInPath == nameInPotplayer) { 156 | media_path := lastMediaPath 157 | } else { 158 | lastMediaPath := UrlDecode(GetMediaPath()) 159 | media_path := lastMediaPath 160 | } 161 | 162 | media_data := MediaData(media_path, GetMediaTime(), "") 163 | 164 | if (hotkey == (app_config.HotkeySubtitle " Up")) { 165 | backlink_template := app_config.SubtitleTemplate 166 | 167 | rendered_template := RenderTemplate(app_config.SubtitleTemplate, media_data, hotkey) 168 | } else { 169 | rendered_template := RenderTemplate(app_config.MarkdownTemplate, media_data, hotkey) 170 | } 171 | 172 | PauseMedia() 173 | 174 | if (IsWordProgram()) { 175 | SendText2wordApp(rendered_template) 176 | } else { 177 | SendText2NoteApp(rendered_template) 178 | } 179 | } 180 | 181 | ; 【主逻辑】粘贴图像 182 | Potplayer2ObsidianImage(hotkey) { 183 | ReleaseCommonUseKeyboard() 184 | 185 | ; 取消可能正在进行的截图等待 186 | CancelScreenshotWaiting() 187 | 188 | iamgeData := SaveImage(hotkey) 189 | 190 | ; 对于 HotkeyIamgeBacklink,仍然是同步的 191 | if (hotkey == (app_config.HotkeyIamgeBacklink " Up")) { 192 | if (iamgeData == "edit image timeout") { 193 | return 194 | } 195 | 196 | media_data := MediaData(GetMediaPath(), GetMediaTime(), "") 197 | PauseMedia() 198 | RenderImage(app_config.MarkdownImageTemplate, media_data, iamgeData) 199 | } 200 | 201 | ; 对于 HotkeyImageEdit,现在是异步的,SaveImage 已经启动了异步流程 202 | ; ProcessScreenshotResult 会在检测到图片时自动调用 203 | } 204 | 205 | GetMediaPath() { 206 | return PressDownHotkey(potplayer_control.GetMediaPathToClipboard) 207 | } 208 | GetMediaTime() { 209 | milliseconds := potplayer_control.GetMediaTimeMilliseconds() 210 | 211 | if (app_config.ReduceTime != "0") { 212 | milliseconds := (milliseconds - (app_config.ReduceTime * 1000)) 213 | } 214 | 215 | if (milliseconds < 0) { 216 | milliseconds := 0 217 | } 218 | 219 | timestamp := MillisecondsToTimestamp(milliseconds) 220 | 221 | return timestamp 222 | } 223 | 224 | GetMediaTimeMilliseconds() { 225 | return potplayer_control.GetMediaTimeMilliseconds() 226 | } 227 | GetMediaSubtitle() { 228 | subtitle_from_potplayer := "" 229 | subtitle_from_potplayer := PressDownHotkey(potplayer_control.GetSubtitleToClipboard) 230 | return subtitle_from_potplayer 231 | } 232 | PressDownHotkey(operate_potplayer) { 233 | ; 先让剪贴板为空, 这样可以使用 ClipWait 检测文本什么时候被复制到剪贴板中. 234 | A_Clipboard := "" 235 | ; 调用函数会丢失this,将对象传入,以便不会丢失this => https://wyagd001.github.io/v2/docs/Objects.htm#Custom_Classes_method 236 | operate_potplayer(potplayer_control) 237 | result := "" 238 | if(!ClipWait(1, 0)){ 239 | return result 240 | } 241 | result := A_Clipboard 242 | ; MyLog "剪切板的值是:" . result 243 | 244 | ; 解决:一旦potplayer左上角出现提示,快捷键不生效的问题 245 | ; if (result == "") { 246 | ; SafeRecursion() 247 | ; ; 无限重试! 248 | ; result := PressDownHotkey(operate_potplayer) 249 | ; } 250 | ; running_count := 0 251 | return result 252 | } 253 | 254 | PauseMedia() { 255 | if (app_config.IsStop != "0") { 256 | potplayer_control.PlayPause() 257 | } 258 | } 259 | 260 | IsWordProgram() { 261 | target_program := SelectedNoteProgram(app_config.NoteAppName) 262 | return target_program == "wps.exe" || target_program == "winword.exe" 263 | } 264 | IsNotionProgram() { 265 | target_program := StrLower(SelectedNoteProgram(app_config.NoteAppName)) 266 | return target_program == "msedge.exe" 267 | || target_program == "chrome.exe" 268 | || target_program == "360chrome.exe" 269 | || target_program == "firefox.exe" 270 | } 271 | 272 | GetFileNameInPath(path) { 273 | name := GetNameForPath(path) 274 | if (app_config.MarkdownRemoveSuffixOfVideoFile != "0") { 275 | name := RemoveSuffix(name) 276 | } 277 | return name 278 | } 279 | 280 | RemoveSuffix(name) { 281 | index_of := InStr(name, ".", , -1) 282 | if (index_of = 0) { 283 | return name 284 | } 285 | result := SubStr(name, 1, index_of - 1) 286 | return result 287 | } 288 | 289 | ; 路径地址处理 290 | ProcessUrl(media_path) { 291 | ; 进行Url编码 292 | if (app_config.MarkdownPathIsEncode != "0" || 293 | ; 全系urlencode的bug:如果路径中存在"\["会让,在【ob的预览模式】下(回链会被ob自动urlencode),"\"离奇消失变为,"[";例如:G:\BaiduSyncdisk\123\[456] 在bug下变为:G:\BaiduSyncdisk\123[456] <= 丢失了"\" 294 | ; 所以先将"\["替换为"%5C["(\的urlencode编码%5C)。变为:G:\BaiduSyncdisk\123%5C[456] 295 | ; Typora能打开 296 | InStr(media_path, "\[") != 0 || 297 | InStr(media_path, "\!") != 0) { 298 | media_path := UrlEncode(media_path) 299 | } else { 300 | ; ob 能打开,Typora打不开 301 | ; media_path := StrReplace(media_path, "\[", "%5C[") 302 | ; media_path := StrReplace(media_path, "\!", "%5C!") 303 | 304 | ; 但是 obidian中的potplayer回链路径有空格,在obsidian的预览模式【无法渲染】,所以将空格进行Url编码 305 | media_path := StrReplace(media_path, " ", "%20") 306 | } 307 | 308 | return media_path 309 | } 310 | 311 | SendText2NoteApp(text) { 312 | selected_note_program := SelectedNoteProgram(app_config.NoteAppName) 313 | ActivateProgram(selected_note_program) 314 | 315 | A_Clipboard := "" 316 | A_Clipboard := text 317 | ; 超时 318 | if(!ClipWait(2, 0)){ 319 | return 320 | } 321 | Send "{LCtrl down}" 322 | Send "{v}" 323 | Send "{LCtrl up}" 324 | ; 粘贴文字需要等待一下obsidian有延迟,不然会出现粘贴的文字【消失】 325 | Sleep 300 326 | } 327 | SendText2wordApp(text) { 328 | selected_note_program := SelectedNoteProgram(app_config.NoteAppName) 329 | ActivateProgram(selected_note_program) 330 | Run(A_ScriptDir "\lib\word\word.exe " text, , "Hide", ,) 331 | } 332 | 333 | ; 添加截图等待状态跟踪变量 334 | IsWaitingForScreenshot := false 335 | ScreenshotContext := {hotkey: "", media_data: ""} 336 | SaveImage(hotkey) { 337 | global IsWaitingForScreenshot 338 | 339 | ; 如果已经在等待截图,先取消之前的等待 340 | if (IsWaitingForScreenshot) { 341 | SetTimer(ScreenshotTimeout, 0) 342 | IsWaitingForScreenshot := false 343 | } 344 | 345 | Assert(potplayer_control.GetPlayStatus() == "Stopped", "Please start video playback to take screenshots.") 346 | 347 | if (hotkey == (app_config.HotkeyIamgeBacklink " Up")) { 348 | A_Clipboard := "" 349 | potplayer_control.SaveImageToClipboard() 350 | if !ClipWait(2, 1) { 351 | SafeRecursion() 352 | } 353 | running_count := 0 354 | return ClipboardAll() 355 | } 356 | 357 | if (hotkey == (app_config.HotkeyImageEdit " Up")) { 358 | ActivateProgram(app_config.PotplayerProcessName) 359 | Send app_config.HotkeyScreenshotToolHotkeys 360 | A_Clipboard := "" 361 | 362 | ; 设置等待状态和上下文 363 | IsWaitingForScreenshot := true 364 | ScreenshotContext.hotkey := hotkey 365 | 366 | ; 启动异步检查定时器 - 每250ms检查一次剪切板 367 | SetTimer(CheckClipboardForImage, 250) 368 | 369 | ; 设置超时定时器 370 | SetTimer(ScreenshotTimeout, app_config.ImageEditDetectionTime * 1000) 371 | 372 | ; 立即返回,不阻塞快捷键 373 | return "async_started" 374 | } 375 | } 376 | 377 | ; 截图超时处理函数 378 | ScreenshotTimeout() { 379 | global IsWaitingForScreenshot 380 | if (IsWaitingForScreenshot) { 381 | IsWaitingForScreenshot := false 382 | ; 停止剪切板检查定时器 383 | SetTimer(CheckClipboardForImage, 0) 384 | ; 这里可以添加超时提示,比如: 385 | ; ToolTip("截图已超时或被取消") 386 | ; SetTimer(() => ToolTip(), -2000) 387 | } 388 | } 389 | 390 | ; 异步检查剪切板是否有图片 391 | CheckClipboardForImage() { 392 | global IsWaitingForScreenshot, ScreenshotContext 393 | 394 | if (!IsWaitingForScreenshot) { 395 | ; 停止检查 396 | SetTimer(CheckClipboardForImage, 0) 397 | return 398 | } 399 | 400 | ; 检查剪切板是否有图片 401 | if ClipWait(0, 1) { 402 | try { 403 | clipboard_data := ClipboardAll() 404 | ; 停止所有定时器并重置状态 405 | SetTimer(CheckClipboardForImage, 0) 406 | SetTimer(ScreenshotTimeout, 0) 407 | IsWaitingForScreenshot := false 408 | 409 | ; 继续处理截图 - 直接调用后续流程 410 | ProcessScreenshotResult(clipboard_data, ScreenshotContext.hotkey) 411 | 412 | } catch { 413 | ; 获取剪切板数据失败,继续等待 414 | } 415 | } 416 | } 417 | 418 | ; 处理截图结果 419 | ProcessScreenshotResult(image_data, hotkey) { 420 | ; 获取媒体数据 421 | media_data := MediaData(GetMediaPath(), GetMediaTime(), "") 422 | 423 | ; 暂停媒体 424 | PauseMedia() 425 | 426 | ; 渲染图片 427 | RenderImage(app_config.MarkdownImageTemplate, media_data, image_data) 428 | } 429 | 430 | ; 取消截图等待(用于防止其他操作与截图冲突) 431 | CancelScreenshotWaiting() { 432 | global IsWaitingForScreenshot 433 | if (IsWaitingForScreenshot) { 434 | SetTimer(CheckClipboardForImage, 0) 435 | SetTimer(ScreenshotTimeout, 0) 436 | IsWaitingForScreenshot := false 437 | } 438 | } 439 | 440 | SendImage2NoteApp(image) { 441 | selected_note_program := SelectedNoteProgram(app_config.NoteAppName) 442 | ActivateProgram(selected_note_program) 443 | A_Clipboard := "" 444 | A_Clipboard := ClipboardAll(image) 445 | if(!ClipWait(2, 1)){ 446 | return 447 | } 448 | Send "{LCtrl down}" 449 | Send "{v}" 450 | Send "{LCtrl up}" 451 | ; 给Obsidian图片插件处理图片的时间 452 | Sleep app_config.SendImageDelays 453 | } 454 | 455 | ; 【A-B片段、循环】 456 | PressHotkeyCount := 0 457 | Potplayer2ObsidianFragment(HotkeyName) { 458 | global 459 | 460 | ; 取消可能正在进行的截图等待 461 | CancelScreenshotWaiting() 462 | 463 | ReleaseCommonUseKeyboard() 464 | 465 | i18n_strings := I18n(A_WorkingDir "\lib\gui\i18n") 466 | 467 | PressHotkeyCount += 1 468 | 469 | if (PressHotkeyCount == 1) { 470 | ; 第一次按下快捷键,记录时间 471 | fragment_time_start := GetMediaTime() 472 | ; 通知用户 473 | ToolTip(i18n_strings.tips_ab_start) 474 | SetTimer () => ToolTip(), -2000 475 | 476 | HotIf CheckCurrentProgram 477 | Hotkey("Escape Up", cancel, "On") 478 | cancel(*) { 479 | ; 重置计数器 480 | PressHotkeyCount := 0 481 | Hotkey("Escape Up", "off") 482 | } 483 | } else if (PressHotkeyCount == 2) { 484 | Assert(fragment_time_start == "", i18n_strings.tips_ab_failed) 485 | ; 重置计数器 486 | PressHotkeyCount := 0 487 | Hotkey("Escape Up", "off") 488 | 489 | ; 第二次按下快捷键,记录时间 490 | fragment_time_end := GetMediaTime() 491 | 492 | ; 如果终点时间小于起点时间,就交换两个时间 493 | if (TimestampToMilliseconds(fragment_time_end) < TimestampToMilliseconds(fragment_time_start)) { 494 | temp := fragment_time_start 495 | fragment_time_start := fragment_time_end 496 | fragment_time_end := temp 497 | ; 释放内存 498 | temp := "" 499 | } 500 | 501 | media_path := GetMediaPath() 502 | if fragment_time_start == fragment_time_end { 503 | fragment_time := fragment_time_start 504 | } else if HotkeyName == app_config.HotkeyAbFragment " Up" { 505 | fragment_time := fragment_time_start "-" fragment_time_end 506 | } else if HotkeyName == app_config.HotkeyAbCirculation " Up" { 507 | fragment_time := fragment_time_start "∞" fragment_time_end 508 | } 509 | 510 | ; 生成片段链接 511 | markdown_link := RenderTemplate(app_config.MarkdownTemplate, MediaData(media_path, fragment_time, "")) 512 | PauseMedia() 513 | 514 | ; 发送到笔记软件 515 | if (IsWordProgram()) { 516 | SendText2wordApp(markdown_link) 517 | } else { 518 | SendText2NoteApp(markdown_link) 519 | } 520 | } 521 | } 522 | 523 | SubtitleFragmentPlay(mode, playtype) { 524 | global app_config, potplayer_control 525 | media_path := GetMediaPath() 526 | if (media_path == "") { 527 | MsgBox "Video path not found" 528 | return 529 | } 530 | srt_path := GetNameForPathWithoutExt(media_path) ".srt" 531 | if !FileExist(srt_path) { 532 | MsgBox "Matching SRT subtitle file not found: " srt_path 533 | return 534 | } 535 | subtitles_data := SubtitlesDataFromSrt(srt_path) 536 | milliseconds := potplayer_control.GetMediaTimeMilliseconds() 537 | frag := GetSubtitleSegmentByTimestamp(subtitles_data, milliseconds, mode) 538 | if !frag { 539 | MsgBox "Subtitle segment not found" 540 | return 541 | } 542 | time_start := MsToSrtTime(frag.timeStart) 543 | time_end := MsToSrtTime(frag.timeEnd) 544 | ; 转为00:00:00.000格式 545 | time_start := StrReplace(time_start, ",", ".") 546 | time_end := StrReplace(time_end, ",", ".") 547 | ; 取消AB循环 548 | CancelABCycleIfNeeded(potplayer_control, app_config.PotplayerProcessName, media_path) 549 | if (playtype = "single") { 550 | ; JumpToAbFragment(potplayer_control, app_config.PotplayerProcessName, media_path, time_start, time_end, app_config.AbFragmentDetectionDelays) 551 | text := app_config.UrlProtocol "?path=" ProcessUrl(media_path) "&time=" time_start "-" time_end 552 | Run A_ScriptDir "\lib\note2potplayer\note2potplayer.exe " text 553 | } else { 554 | JumpToAbCirculation(potplayer_control, app_config.PotplayerProcessName, media_path, time_start, time_end) 555 | } 556 | } --------------------------------------------------------------------------------