├── .gitignore
├── LICENSE
├── README.md
├── background.js
├── images
├── icon desing.svg
├── icon128.png
├── icon16.png
└── icon48.png
├── manifest.json
├── package-lock.json
├── package.json
├── popup
├── css
│ └── style.css
├── editor.html
├── help.html
├── icons
│ ├── bottomImg.png
│ ├── customOffset.png
│ ├── deleteImg.png
│ ├── downImg.png
│ ├── topImg.png
│ └── upImg.png
└── popup.html
├── readmeImgs
├── step1-1.png
├── step1-2.png
├── step2-1.png
├── step2-10.png
├── step2-2.png
├── step2-3.png
├── step2-4.png
├── step2-5.png
├── step2-6.png
├── step2-7.png
├── step2-8.png
├── step2-9.png
└── step3-1.png
└── scripts
├── editor.js
├── html2canvas.js
└── popup.js
/.gitignore:
--------------------------------------------------------------------------------
1 | /.vscode
2 | /node_modules
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 rlongdragon
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 |
字幕拼貼器
2 |
3 | 這個 Chrome Extension 可以讓您快速將 YouTube 影片片段和字幕拼貼成圖片。您可以輕鬆製作梗圖、分享精彩片段。它提供一鍵截取,讓您輕鬆製作完美的 YouTube 字幕截圖。
4 |
5 | # 使用說明
6 |
7 | ## 啟動擴充功能
8 |
9 | 你可以在影片網站(YouTube、巴哈姆特動畫瘋)中開啟這個擴充功能
10 |
11 | 
12 |
13 | 你可以看到兩個按鈕 `擷取關鍵影格`以及`開啟編輯器`,開始使用時請先按下`開啟編輯器`開啟如下圖的彈出視窗編輯器。
14 | 
15 |
16 | 當你找到想記錄的關鍵幀按下`擷取關鍵幀`即可將該畫面加入至編輯器。
17 |
18 | > 你也可以使用 預設`Alt`(mac 為 `Option`) + `S` 快速擷取關鍵影格
19 |
20 | ## 使用編輯器
21 |
22 | 當你選好圖片之後,可以用上方滑桿調整字幕與畫面下緣距離,滑桿左右兩輸入框是滑桿的最小以及最大值,可以調整兩數值來調正滑桿的精準度。20-60就足夠應付大部分的使用場景了。
23 |
24 | ### 右鍵選單
25 |
26 | 如果你想對特定圖片進行操作(移動圖片層級、單獨設定偏移、刪除圖片等)可以對目標圖片按下右鍵。
27 |
28 | 此時會彈出右鍵選單,而被選中的圖片則會有藍色的外框。此時你可以使用右鍵選單來對圖片進行操作。
29 |
30 | > 下面使用[人生魯宅x尊-第2頻道 - YouTube](https://www.youtube.com/@nerdzun) [【尊】我找了小玉一起來看小玉梗圖...【第二頻道】 - YouTube](https://youtu.be/Tkf8_8_nl68?si=aDExffHY4LtQBAHa) 實際應用
31 |
32 | #### 實際範例
33 |
34 | 首先,我已經捷好了一些圖片,並且已經調整好了字幕與畫面下緣距離。
35 |
36 | 我發現我第一張圖片不小心截到兩張了,我想要將第二張圖片刪除。
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | 後來我發現,影片中是先講「我覺得是時候」才講「跟你們一並告知了」。
46 |
47 | 所以我需要將兩張圖片交換位置。
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | 然後我發現影片中有些字幕大小比較大,那些圖片需要下移一點。
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | 下面還有幾張圖片也是要調整,這時我可以直接使用`沿用上次設定值`直接套用
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | 最後,最後一張圖片是需要完整顯示出來的,所以我使用`露出整張圖片`來調整。
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | ### 輸出以及儲存圖片
84 |
85 | 調整好後可以在輸出區按`產生圖片`,來渲染拼貼圖,並預覽。
86 |
87 |
88 |
89 | 渲染完後你可以直接使用`複製圖片`按鈕,快速將圖片複製到剪貼簿,或是使用`下載圖片`按鈕將圖片下載至本機。
90 |
91 | # 未來更新
92 |
93 | - 特殊排版
94 |
95 | - 兼容CC字幕
96 |
97 | - 偵測CC字幕自動生成
98 |
99 | - 生成 gif
100 |
101 | - 裁切字幕底部區域
102 |
103 | # Bug 回報、意見回饋與聯繫
104 |
105 | 本專案開發中,遇到Bug、意見回饋可不吝嗇開issus給我 :D
106 |
107 | 或是你可以使用 [discord](https://discordapp.com/users/601819508943880193) 或是 [Email](mailto:jz744335@gmail.com) 聯絡我
108 |
109 | 如果你覺得這個專案對你有幫助,歡迎給我一顆星星,這可以給我有很大的動力持續更新。
110 |
--------------------------------------------------------------------------------
/background.js:
--------------------------------------------------------------------------------
1 | function getVideoScreenshot() {
2 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
3 | const tab = tabs[0];
4 |
5 | function getVideoScreenshot() {
6 | v = document.querySelector('video');
7 | c = document.createElement('canvas');
8 | c.height = v.videoHeight || parseInt(v.style.height);
9 | c.width = v.videoWidth || parseInt(v.style.width);
10 | ctx = c.getContext('2d');
11 | ctx.drawImage(v, 0, 0);
12 | c.toDataURL()
13 |
14 | let option = {
15 | action: "getVideoData",
16 | videoData: c.toDataURL()
17 | };
18 |
19 | chrome.runtime.sendMessage(option)
20 | };
21 |
22 | chrome.scripting.executeScript({
23 | target: { tabId: tab.id },
24 | func: getVideoScreenshot,
25 |
26 | }).then(() => console.log('擷取關鍵影格'));
27 | });
28 | }
29 |
30 |
31 | chrome.commands.onCommand.addListener((command) => {
32 | console.log(`Command: ${command}`);
33 | switch (command) {
34 | case "screenshot":
35 | getVideoScreenshot();
36 | break;
37 | }
38 | });
39 |
40 | chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
41 | switch (request.action) {
42 | case "getVideoData":
43 | // check editor opened
44 | let isOpen = await (new Promise(async (resolve, reject) => {
45 | setTimeout(() => {
46 | resolve(0)
47 | }, 1000)
48 |
49 | let getRes = await chrome.runtime.sendMessage({ action: "checkEditorOpened" })
50 | if (getRes) {
51 | resolve(1)
52 | }
53 | }))
54 |
55 | if (isOpen) {
56 | break
57 | }
58 | // open window
59 | chrome.windows.create({
60 | url: chrome.runtime.getURL("./popup/editor.html"),
61 | type: "popup",
62 | width: 898,
63 | height: 400,
64 | focused: true,
65 | });
66 |
67 | let waitForWindowLoad = await (new Promise(async (resolve, reject) => {
68 | setTimeout(() => {
69 | reject(0)
70 | }, 5000)
71 | chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
72 | if (request.action == "editorWindowOnload") {
73 | resolve(1)
74 | }
75 | })
76 | }))
77 |
78 | if (waitForWindowLoad) {
79 | chrome.runtime.sendMessage(request)
80 | }
81 |
82 | break;
83 | }
84 |
85 | });
--------------------------------------------------------------------------------
/images/icon desing.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
627 |
--------------------------------------------------------------------------------
/images/icon128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/images/icon128.png
--------------------------------------------------------------------------------
/images/icon16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/images/icon16.png
--------------------------------------------------------------------------------
/images/icon48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/images/icon48.png
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "name": "字幕拼貼器",
4 | "version": "1.1.7",
5 | "description": "這個 Chrome Extension 可以讓您快速將 YouTube 影片片段和字幕拼貼成圖片。您可以輕鬆製作梗圖、分享精彩片段。它提供一鍵截取,讓您輕鬆製作完美的 YouTube 字幕截圖。",
6 | "icons": {
7 | "128": "images/icon128.png",
8 | "48": "images/icon48.png",
9 | "16": "images/icon16.png"
10 | },
11 | "action": {
12 | "default_icon": "images/icon16.png",
13 | "default_popup": "popup/popup.html"
14 | },
15 | "background": {
16 | "service_worker": "background.js",
17 | "type": "module"
18 | },
19 | "commands": {
20 | "screenshot": {
21 | "suggested_key": {
22 | "default": "Alt+S"
23 | },
24 | "description": "擷取關鍵影格的快捷鍵"
25 | }
26 | },
27 | "host_permissions": [
28 | "https://youtube.com/*"
29 | ],
30 | "permissions": [
31 | "activeTab",
32 | "scripting"
33 | ]
34 | }
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Captions_Stack_Generator",
3 | "lockfileVersion": 3,
4 | "requires": true,
5 | "packages": {
6 | "": {
7 | "dependencies": {
8 | "chrome-types": "^0.1.270"
9 | }
10 | },
11 | "node_modules/chrome-types": {
12 | "version": "0.1.270",
13 | "resolved": "https://registry.npmjs.org/chrome-types/-/chrome-types-0.1.270.tgz",
14 | "integrity": "sha512-N9NUF01Nz2+5WjRJsUKrz7BOo9rLDBbu6FOgyBH7uj20XAI751JxLzP8/dF6RWgUcSYj4+h3R1/6CVGTt7d2Ug=="
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "chrome-types": "^0.1.270"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/popup/css/style.css:
--------------------------------------------------------------------------------
1 | main {
2 | padding: 15px
3 | }
4 |
5 | .button-block {
6 | background-color: #4CAF50;
7 | color: white;
8 | border-radius: 5px;
9 | padding: 14px 20px;
10 | margin: 8px 0;
11 | border: none;
12 | cursor: pointer;
13 | width: 100%;
14 | font-size: 16px;
15 | transition: background-color 0.3s;
16 | }
17 |
18 | .button-block:hover {
19 | background-color: hsl(122, 39%, 36%);
20 | }
21 |
22 | .button-inline {
23 | background-color: #4CAF50;
24 | color: white;
25 | border-radius: 5px;
26 | padding: 7px 10px;
27 | border: none;
28 | cursor: pointer;
29 | font-size: 12px;
30 | transition: background-color 0.3s;
31 | vertical-align: middle;
32 | }
33 |
34 | .button-inline:hover {
35 | background-color: hsl(122, 39%, 36%);
36 | }
37 |
38 | .copyright {
39 | font-size: 12px;
40 | color: #999;
41 | margin-top: 10px;
42 | text-align: center
43 | }
44 |
45 | .container {
46 | width: 384px;
47 | display: inline-block;
48 | margin: 8px;
49 | border: 5px solid hsl(122, 39%, 49%);
50 | border-radius: 15px;
51 | padding: 8px;
52 | vertical-align: top;
53 | min-height: 100px;
54 | }
55 |
56 | .container h1 {
57 | margin: 0;
58 | }
59 |
60 | .number-ipt {
61 | width: 40px;
62 | border-radius: 5px;
63 | padding: 5px;
64 | border: hsl(122, 39%, 49%) 3px solid;
65 | vertical-align: middle;
66 | }
67 |
68 | .number-ipt :focus {
69 | border: hsl(122, 39%, 36%) 3px solid;
70 | }
71 |
72 | .custom-scrollbar::-webkit-scrollbar-track {
73 | background-color: #ffffff00;
74 | }
75 |
76 | .custom-scrollbar::-webkit-scrollbar {
77 | width: 10px;
78 | background-color: hsla(0, 0%, 79%, 0.25);
79 | }
80 |
81 | .custom-scrollbar::-webkit-scrollbar-thumb {
82 | background-color: #c9c9c9;
83 | border-radius: 5px;
84 | }
85 |
86 | .menu {
87 | position: fixed;
88 | display: none;
89 | background-color: #f0f0f0b0;
90 | border-radius: 5px;
91 | padding: 10px;
92 | z-index: 10000;
93 | backdrop-filter: blur(5px);
94 |
95 | flex-direction: column;
96 | }
97 |
98 | #menu button {
99 | background-color: #00000000;
100 | border: none;
101 | cursor: pointer;
102 | padding: 3px 6px;
103 | margin: 2px 0;
104 | padding-left: 0;
105 | width: 100%;
106 | text-align: left;
107 | }
108 |
109 | #menu button:hover {
110 | background-color: #ffffff11;
111 | outline: #4CAF50 2px solid;
112 | border-radius: 3px;
113 | }
114 |
115 | #menu button:focus {
116 | outline: #4CAF50 2px solid;
117 | border-radius: 3px;
118 | }
119 | #menu button:active {
120 | background-color: hsla(0, 0%, 0%, 0.068);
121 | }
122 |
123 | #menu button img {
124 | width: 25px;
125 | vertical-align: middle;
126 | margin-right: 5px;
127 | }
128 |
129 |
130 | #setCustomOffset h2 {
131 | margin: 0;
132 | }
133 | #setCustomOffset input {
134 | background-color: #00000000;
135 | }
--------------------------------------------------------------------------------
/popup/editor.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | 字幕拼貼器 編輯器
10 |
23 |
24 |
25 |
26 |
27 |
28 | YouTube 字幕拼貼器 V 1.1.7
29 |
39 |
49 |
57 |
66 |
67 |
68 | V 1.1.7 copyright (c) 2024 @rlongdragon
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/popup/help.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | 使用指引
9 |
10 |
11 |
12 |
13 |
14 |
15 | 使用指引
16 |
17 | 這個 Chrome Extension 可以讓您快速將 YouTube 影片片段和字幕拼貼成圖片。您可以輕鬆製作梗圖、分享精彩片段。它提供一鍵截取,讓您輕鬆製作完美的 YouTube 字幕截圖。
18 | 使用說明
19 | 啟動擴充功能
20 | 你可以在影片網站(YouTube、巴哈姆特動畫瘋)中開啟這個擴充功能
21 | 你可以看到兩個按鈕 擷取關鍵影格
以及開啟編輯器
,開始使用時請先按下開啟編輯器
開啟如下圖的彈出視窗編輯器。
22 |
當你找到想記錄的關鍵幀按下擷取關鍵幀
即可將該畫面加入至編輯器。
23 |
24 | 你也可以使用 預設Alt
(mac 為 Option
) + S
快速擷取關鍵影格
25 |
26 | 使用編輯器
27 | 當你選好圖片之後,可以用上方滑桿調整字幕與畫面下緣距離,滑桿左右兩輸入框是滑桿的最小以及最大值,可以調整兩數值來調正滑桿的精準度。20-60就足夠應付大部分的使用場景了。
28 | 右鍵選單
29 | 如果你想對特定圖片進行操作(移動圖片層級、單獨設定偏移、刪除圖片等)可以對目標圖片按下右鍵。
30 | 此時會彈出右鍵選單,而被選中的圖片則會有藍色的外框。此時你可以使用右鍵選單來對圖片進行操作。
31 |
32 | 下面使用人生魯宅x尊-第2頻道 - YouTube 【尊】我找了小玉一起來看小玉梗圖...【第二頻道】 - YouTube 實際應用
33 |
34 | 實際範例
35 | 首先,我已經捷好了一些圖片,並且已經調整好了字幕與畫面下緣距離。
36 | 我發現我第一張圖片不小心截到兩張了,我想要將第二張圖片刪除。
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | 後來我發現,影片中是先講「我覺得是時候」才講「跟你們一並告知了」。
45 | 所以我需要將兩張圖片交換位置。
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | 然後我發現影片中有些字幕大小比較大,那些圖片需要下移一點。
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | 下面還有幾張圖片也是要調整,這時我可以直接使用沿用上次設定值
直接套用
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | 最後,最後一張圖片是需要完整顯示出來的,所以我使用露出整張圖片
來調整。
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | 輸出以及儲存圖片
78 | 調整好後可以在輸出區按產生圖片
,來渲染拼貼圖,並預覽。
79 | 渲染完後你可以直接使用複製圖片
按鈕,快速將圖片複製到剪貼簿,或是使用下載圖片
按鈕將圖片下載至本機。
80 | 未來更新
81 |
82 | 特殊排版
83 |
84 | 兼容CC字幕
85 |
86 | 偵測CC字幕自動生成
87 |
88 |
89 | Bug 回報、意見回饋與聯繫
90 | 本專案開發中,遇到Bug、意見回饋可不吝嗇開issus給我 :D
91 | 或是你可以使用 discord 或是 Email 聯絡我
92 | 如果你覺得這個專案對你有幫助,歡迎給我一顆星星,這可以給我有很大的動力持續更新。
93 |
94 |
95 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/popup/icons/bottomImg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/popup/icons/bottomImg.png
--------------------------------------------------------------------------------
/popup/icons/customOffset.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/popup/icons/customOffset.png
--------------------------------------------------------------------------------
/popup/icons/deleteImg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/popup/icons/deleteImg.png
--------------------------------------------------------------------------------
/popup/icons/downImg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/popup/icons/downImg.png
--------------------------------------------------------------------------------
/popup/icons/topImg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/popup/icons/topImg.png
--------------------------------------------------------------------------------
/popup/icons/upImg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/popup/icons/upImg.png
--------------------------------------------------------------------------------
/popup/popup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Hello world
6 |
7 |
8 |
9 |
10 |
11 |
16 | 擷取關鍵影格
17 | 開啟編輯器
18 |
19 |
20 |
21 | 怎麼使用?
22 | V 1.1.7 copyright (c) 2024 @rlongdragon
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/readmeImgs/step1-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/readmeImgs/step1-1.png
--------------------------------------------------------------------------------
/readmeImgs/step1-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/readmeImgs/step1-2.png
--------------------------------------------------------------------------------
/readmeImgs/step2-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/readmeImgs/step2-1.png
--------------------------------------------------------------------------------
/readmeImgs/step2-10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/readmeImgs/step2-10.png
--------------------------------------------------------------------------------
/readmeImgs/step2-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/readmeImgs/step2-2.png
--------------------------------------------------------------------------------
/readmeImgs/step2-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/readmeImgs/step2-3.png
--------------------------------------------------------------------------------
/readmeImgs/step2-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/readmeImgs/step2-4.png
--------------------------------------------------------------------------------
/readmeImgs/step2-5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/readmeImgs/step2-5.png
--------------------------------------------------------------------------------
/readmeImgs/step2-6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/readmeImgs/step2-6.png
--------------------------------------------------------------------------------
/readmeImgs/step2-7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/readmeImgs/step2-7.png
--------------------------------------------------------------------------------
/readmeImgs/step2-8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/readmeImgs/step2-8.png
--------------------------------------------------------------------------------
/readmeImgs/step2-9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/readmeImgs/step2-9.png
--------------------------------------------------------------------------------
/readmeImgs/step3-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/readmeImgs/step3-1.png
--------------------------------------------------------------------------------
/scripts/editor.js:
--------------------------------------------------------------------------------
1 | window.onload = () => {
2 | const option = {
3 | action: "editorWindowOnload",
4 | };
5 | chrome.runtime.sendMessage(option)
6 | }
7 |
8 | let activeImg = 0;
9 | let activePosition = [{ x: 0, y: 0 }];
10 |
11 | function updateImgs() {
12 | let imgs = app.getElementsByClassName("screenshotImg");
13 | let offset = document.getElementById("offset").value;
14 | for (let i = 0; i < imgs.length; i++) {
15 | /**
16 | * @type {HTMLImageElement} img
17 | */
18 | let img = imgs[i];
19 | img.style.zIndex = 1000 - i;
20 |
21 | img.addEventListener("contextmenu", (e) => {
22 | e.preventDefault();
23 |
24 | let imgs = app.getElementsByClassName("screenshotImg");
25 | for (let i = 0; i < imgs.length; i++) {
26 | imgs[i].style.outline = "none";
27 | imgs[i].style.borderRadius = "0px";
28 | }
29 |
30 | document.getElementById("setCustomOffset").style.display = "none";
31 |
32 | img.style.outline = "5px solid #2453c2";
33 | img.style.borderRadius = "5px";
34 | let contextMenu = document.getElementById("menu");
35 | contextMenu.style.display = "flex";
36 | contextMenu.style.left = e.clientX + "px";
37 | if (e.clientY + parseInt((contextMenu.getBoundingClientRect()).height) > window.innerHeight) {
38 | contextMenu.style.top = (window.innerHeight - (contextMenu.getBoundingClientRect()).height) + "px"
39 | } else {
40 | contextMenu.style.top = e.clientY + "px";
41 | }
42 |
43 | activeImg = i;
44 | activePosition = [{ x: e.clientX, y: e.clientY }];
45 | });
46 | }
47 | document.getElementById("app").style.setProperty("--imageOffset", (216 - offset) + "px");
48 | }
49 |
50 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
51 | switch (request.action) {
52 | case "getVideoData":
53 | let img = document.createElement("img");
54 | img.className = "screenshotImg";
55 | img.src = request.videoData;
56 | document.getElementById("app").appendChild(img);
57 | updateImgs();
58 | break;
59 | case "checkEditorOpened":
60 | sendResponse(1)
61 | break
62 | }
63 |
64 | sendResponse({ farewell: "thanks for sending! goodbye" });
65 | });
66 |
67 | document.getElementById("offset").min = document.getElementById("min-offset").value;
68 | document.getElementById("min-offset").max = document.getElementById("offset").value;
69 | document.getElementById("offset").max = document.getElementById("max-offset").value;
70 | document.getElementById("max-offset").min = document.getElementById("offset").value;
71 |
72 | document.getElementById("min-offset").addEventListener("input", () => {
73 | document.getElementById("offset").min = document.getElementById("min-offset").value;
74 | document.getElementById("max-offset").max = document.getElementById("offset").value;
75 | document.getElementById("max-offset").min = document.getElementById("offset").value;
76 | });
77 |
78 | document.getElementById("max-offset").addEventListener("input", () => {
79 | document.getElementById("offset").max = document.getElementById("max-offset").value;
80 | document.getElementById("min-offset").max = document.getElementById("offset").value;
81 | document.getElementById("min-offset").min = document.getElementById("offset").value;
82 | });
83 |
84 | document.getElementById("offset").addEventListener("input", () => {
85 | document.getElementById("app").style.setProperty("--imageOffset", (216 - document.getElementById("offset").value) + "px");
86 | document.getElementById("min-offset").max = document.getElementById("offset").value;
87 | document.getElementById("max-offset").min = document.getElementById("offset").value;
88 | });
89 |
90 | let canvasBlob;
91 | async function generate() {
92 | return (new Promise((resole, reject) => {
93 | try {
94 | html2canvas(document.getElementById("app"), { backgroundColor: null, scale: parseInt(document.getElementById("scale").value) }).then(function (canvas) {
95 | document.getElementById("preview").innerHTML = "";
96 | document.getElementById("preview").appendChild(canvas);
97 | canvas.toBlob(function (blob) {
98 | canvasBlob = blob;
99 | resole(1)
100 | });
101 | });
102 | } catch {
103 | reject(0)
104 | }
105 | }))
106 | }
107 | document.getElementById("generate").addEventListener("click", generate);
108 |
109 | async function copyImg() {
110 | if (!canvasBlob) {
111 | console.log(await generate())
112 | }
113 |
114 | const item = new ClipboardItem({ "image/png": canvasBlob });
115 | navigator.clipboard.write([item]).then(function () {
116 | console.log("Image copied to clipboard");
117 | }).catch(function (error) {
118 | console.error("Unable to write to clipboard. Error:", error);
119 | });
120 | }
121 | document.getElementById("copyImg").addEventListener("click", copyImg);
122 |
123 | async function downloadImg() {
124 | if (!canvasBlob) {
125 | console.log(await generate())
126 | }
127 |
128 | const a = document.createElement("a");
129 | document.body.appendChild(a);
130 | a.href = URL.createObjectURL(canvasBlob);
131 | let date = new Date();
132 | let year = date.getFullYear();
133 | let month = date.getMonth() + 1;
134 | let day = date.getDate();
135 | let hour = date.getHours();
136 | let min = date.getMinutes();
137 | let sec = date.getSeconds();
138 | a.download = `screenshot_${year}${month}${day}_${hour}${min}${sec}.png`;
139 | a.click();
140 | document.body.removeChild(a);
141 | }
142 | document.getElementById("downloadImg").addEventListener("click", downloadImg);
143 |
144 | // menu
145 | document.body.addEventListener("click", (e) => {
146 | if ((e.target.className.includes("menuArea"))) return;
147 | console.log(e.target.className);
148 | if (document.getElementById("menu").style.display !== "none") {
149 | document.getElementById("menu").style.display = "none";
150 |
151 | if (document.getElementById("setCustomOffset").style.display === "none") {
152 | let app = document.getElementById("app");
153 | let imgs = app.getElementsByClassName("screenshotImg");
154 | for (let i = 0; i < imgs.length; i++) {
155 | imgs[i].style.outline = "none";
156 | imgs[i].style.borderRadius = "0px";
157 | }
158 | }
159 | } else if (document.getElementById("setCustomOffset").style.display !== "none") {
160 | document.getElementById("setCustomOffset").style.display = "none";
161 |
162 | let app = document.getElementById("app");
163 | let imgs = app.getElementsByClassName("screenshotImg");
164 | for (let i = 0; i < imgs.length; i++) {
165 | imgs[i].style.outline = "none";
166 | imgs[i].style.borderRadius = "0px";
167 | }
168 | }
169 | });
170 |
171 | document.getElementById("upImg").addEventListener("click", () => {
172 | if (activeImg > 0) {
173 | let app = document.getElementById("app");
174 | let imgs = app.children;
175 | let img = imgs[activeImg];
176 | let prevImg = imgs[activeImg - 1];
177 | app.insertBefore(img, prevImg);
178 | updateImgs();
179 | }
180 | });
181 | document.getElementById("downImg").addEventListener("click", () => {
182 | if (activeImg < app.children.length - 1) {
183 | let app = document.getElementById("app");
184 | let imgs = app.children;
185 | let img = imgs[activeImg];
186 | let nextImg = imgs[activeImg + 1];
187 | app.insertBefore(nextImg, img);
188 | updateImgs();
189 | }
190 | });
191 | document.getElementById("topImg").addEventListener("click", () => {
192 | if (activeImg > 0) {
193 | let app = document.getElementById("app");
194 | let imgs = app.children;
195 | let img = imgs[activeImg];
196 | app.insertBefore(img, imgs[0]);
197 | updateImgs();
198 | }
199 | });
200 | document.getElementById("bottomImg").addEventListener("click", () => {
201 | if (activeImg < app.children.length - 1) {
202 | let app = document.getElementById("app");
203 | let imgs = app.children;
204 | let img = imgs[activeImg];
205 | app.appendChild(img);
206 | updateImgs();
207 | }
208 | });
209 |
210 | // let lestSetCustomOffsetValue = document.querySelectorAll(".screenshotImg")[activeImg].style.getPropertyValue("--imageOffset");
211 | let lestSetCustomOffsetValue = 48;
212 | document.getElementById("customOffset").addEventListener("click", (e) => {
213 | lestSetCustomOffsetValue = document.getElementById("customOffsetValue").value;
214 | document.getElementById("lastOffsetValue").innerText = lestSetCustomOffsetValue;
215 |
216 | document.getElementById("customOffsetValue").value =
217 | document.querySelectorAll(".screenshotImg")[activeImg].style.getPropertyValue("--imageOffset")
218 | ?
219 | 216 - parseInt(document.querySelectorAll(".screenshotImg")[activeImg].style.getPropertyValue("--imageOffset"))
220 | :
221 | 216 - parseInt(document.getElementById("app").style.getPropertyValue("--imageOffset"));
222 |
223 | let setCustomOffset = document.getElementById("setCustomOffset")
224 | setCustomOffset.style.display = "block";
225 | setCustomOffset.style.left = activePosition[0].x + "px";
226 | console.log(parseInt((setCustomOffset.getBoundingClientRect()).height))
227 | if (activePosition[0].y + parseInt((setCustomOffset.getBoundingClientRect()).height) > window.innerHeight) {
228 | setCustomOffset.style.top = (window.innerHeight - (setCustomOffset.getBoundingClientRect()).height) + "px"
229 | } else {
230 | setCustomOffset.style.top = activePosition[0].y + "px";
231 | // setCustomOffset.style.top = e.clientY + "px";
232 | }
233 | });
234 | document.getElementById("lastOffset").addEventListener("click", () => {
235 | document.getElementById("customOffsetValue").value = lestSetCustomOffsetValue;
236 | document.getElementById("customOffsetValue").dispatchEvent(new Event("input"));
237 | });
238 | document.getElementById("showAll").addEventListener("click", () => {
239 | document.getElementById("customOffsetValue").value = 216;
240 | document.getElementById("customOffsetValue").dispatchEvent(new Event("input"));
241 | });
242 | document.getElementById("customOffsetValue").addEventListener("input", () => {
243 | document.querySelectorAll(".screenshotImg")[activeImg].style.setProperty("--imageOffset", (216 - document.getElementById("customOffsetValue").value) + "px");
244 | });
245 | document.getElementById("deleteImg").addEventListener("click", () => {
246 | let imgs = app.getElementsByClassName("screenshotImg");
247 | imgs[activeImg].remove();
248 | updateImgs();
249 | });
250 |
251 |
252 | let lastScrollTop = 0;
253 | window.addEventListener("scroll", () => {
254 | let dy = window.scrollY - lastScrollTop;
255 | console.log(dy);
256 | let menus = document.getElementsByClassName("menu");
257 | for (let i = 0; i < menus.length; i++) {
258 | let menu = menus[i];
259 | let top = parseInt(menu.style.top);
260 | menu.style.top = (top - dy) + "px";
261 | }
262 | lastScrollTop = window.scrollY;
263 | });
264 |
--------------------------------------------------------------------------------
/scripts/popup.js:
--------------------------------------------------------------------------------
1 | document.getElementById("screenshot").addEventListener('click', () => {
2 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
3 | const tab = tabs[0];
4 |
5 | function getVideoScreenshot() {
6 | v = document.querySelector('video');
7 | c = document.createElement('canvas');
8 | c.height = v.videoHeight || parseInt(v.style.height);
9 | c.width = v.videoWidth || parseInt(v.style.width);
10 | ctx = c.getContext('2d');
11 | ctx.drawImage(v, 0, 0);
12 | c.toDataURL()
13 |
14 | let option = {
15 | action: "getVideoData",
16 | videoData: c.toDataURL()
17 | };
18 |
19 | chrome.runtime.sendMessage(option);
20 | };
21 |
22 | chrome.scripting.executeScript({
23 | target: { tabId: tab.id },
24 | func: getVideoScreenshot,
25 |
26 | }).then(() => console.log('擷取關鍵影格'));
27 | });
28 | });
29 |
30 | document.getElementById("popEditor").addEventListener('click', () => {
31 | chrome.windows.create({
32 | url: chrome.runtime.getURL("./popup/editor.html"),
33 | type: "popup",
34 | width: 898,
35 | height: 400,
36 | focused: true,
37 | });
38 | window.top = window;
39 | });
40 |
41 | document.getElementById("howToUse").addEventListener("click", () => {
42 | chrome.tabs.create({ url: chrome.runtime.getURL("./popup/help.html") });
43 | })
44 |
45 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
46 | switch (request.action) {
47 | case "getVideoData":
48 | console.log("getVideoData");
49 | console.log(request.videoData);
50 | document.getElementById("screenshotImg").src = request.videoData;
51 | break;
52 | }
53 |
54 | });
--------------------------------------------------------------------------------