soiz1 commited on
Commit
f03c247
·
verified ·
1 Parent(s): 4b3a3d6

Upload 4 files

Browse files
src/addons/addons/save-to-google/_manifest_entry.js ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* generated by pull.js */
2
+ const manifest = {
3
+ "name": "Save project to Google drive",
4
+ "description": " \"Googleに保存\" を追加し、Googleドライブに保存するようにします。",
5
+ "tags": [
6
+ "recommended"
7
+ ],
8
+ "userscripts": [
9
+ {
10
+ "url": "userscript.js"
11
+ }
12
+ ],
13
+ "enabledByDefault": true
14
+ };
15
+ import {mediaRecorderSupported} from "../../environment";
16
+ if (!mediaRecorderSupported) manifest.unsupported = true;
17
+ export default manifest;
src/addons/addons/save-to-google/_runtime_entry.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ /* generated by pull.js */
2
+ import _js from "./userscript.js";
3
+ import _css from "!css-loader!./style.css";
4
+ export const resources = {
5
+ "userscript.js": _js,
6
+ "style.css": _css,
7
+ };
src/addons/addons/save-to-google/style.css ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .mediaRecorderPopup {
2
+ box-sizing: border-box;
3
+ width: 700px;
4
+ max-height: min(800px, 80vh);
5
+ max-width: 85%;
6
+ margin-top: 12vh;
7
+ overflow-y: auto;
8
+ margin-left: auto;
9
+ margin-right: auto;
10
+ }
11
+
12
+ .mediaRecorderPopupContent {
13
+ padding: 1.5rem 2.25rem;
14
+ }
15
+
16
+ .mediaRecorderPopup p {
17
+ font-size: 1rem;
18
+ margin: 0.5rem auto;
19
+ }
20
+
21
+ .mediaRecorderPopup p :last-child {
22
+ margin-left: 1rem;
23
+ }
24
+
25
+ .mediaRecorderPopup[dir="rtl"] p :last-child {
26
+ margin-left: 0;
27
+ margin-right: 1rem;
28
+ }
29
+
30
+ p.mediaRecorderPopupOption {
31
+ display: flex;
32
+ align-items: center;
33
+ }
34
+
35
+ .mediaRecorderPopupOption input[type="checkbox"] {
36
+ height: 1.5rem;
37
+ }
38
+
39
+ #recordOptionSecondsInput,
40
+ #recordOptionDelayInput {
41
+ width: 6rem;
42
+ }
43
+
44
+ .mediaRecorderPopupButtons {
45
+ margin-top: 1.5rem;
46
+ }
47
+
48
+ .mediaRecorderPopupButtons button {
49
+ margin-left: 0.5rem;
50
+ }
51
+
52
+ /* TW: Fixes cancel button in dark mode */
53
+ .mediaRecorderPopupButtons button:nth-of-type(1) {
54
+ color: black;
55
+ }
src/addons/addons/save-to-google/userscript.js ADDED
@@ -0,0 +1,307 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import SB3Downloader from "/src/containers/sb3-downloader.jsx";
2
+ import { getProjectFilename } from '/src/containers/sb3-downloader.jsx';
3
+ export default async ({ addon, console, msg }) => {
4
+ const CLIENT_ID = "1033286471224-n9mv8l869fqikubj2e8q92n8ige3qr6r.apps.googleusercontent.com";
5
+ const REDIRECT_URI = "https://soiz1-penguin-upload.hf.space/close";
6
+ const SCOPES = "https://www.googleapis.com/auth/drive.file";
7
+ const PROXY_URL = "https://soiz1-drive-proxy.hf.space/?file_id=";
8
+ const SHORT_URL = "https://s4.rf.gd/";
9
+
10
+ let accessToken = null;
11
+
12
+ while (true) {
13
+ const targetElem = await addon.tab.waitForElement(
14
+ 'div[class*="menu-bar_file-group"] > div:last-child:not(.sa-record)',
15
+ { markAsSeen: true }
16
+ );
17
+
18
+ if (!document.querySelector('.sa-custom-modal-button')) {
19
+ const button = document.createElement("div");
20
+ button.className = "sa-custom-modal-button " + targetElem.className;
21
+ button.textContent = "保存";
22
+ button.style.cursor = "pointer";
23
+
24
+ button.addEventListener("click", () => {
25
+ showMainModal(addon);
26
+ });
27
+
28
+ targetElem.parentElement.appendChild(button);
29
+ }
30
+ }
31
+
32
+ function showMainModal(addon) {
33
+ const modal = addon.tab.createModal("Googleドライブに保存", { isOpen: true, useEditorClasses: true });
34
+
35
+ modal.content.innerHTML = `
36
+ <div style="padding: 1rem;">
37
+ <h1>Googleドライブに接続</h1>
38
+ <p>Googleでログインして、プロジェクトを保存または更新します。</p>
39
+ <button id="google-login-button" class="button">Googleでログイン</button>
40
+ <div id="file-list-container" style="margin-top: 1rem; display: none;">
41
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
42
+ <h2 style="margin: 0;">プロジェクト: <span id="project-title" style="cursor: pointer; border-bottom: 1px dashed #000;">${window.vm.runtime.projectName || "無題"}</span></h2>
43
+ <button id="new-file-button" class="button">新規保存</button>
44
+ </div>
45
+ <div id="file-list" style="max-height: 300px; overflow-y: auto; border: 1px solid #ddd; padding: 0.5rem;"></div>
46
+ </div>
47
+ </div>
48
+ `;
49
+
50
+ const loginButton = modal.content.querySelector("#google-login-button");
51
+ const fileListContainer = modal.content.querySelector("#file-list-container");
52
+ const fileList = modal.content.querySelector("#file-list");
53
+ const newFileButton = modal.content.querySelector("#new-file-button");
54
+ const projectTitle = modal.content.querySelector("#project-title");
55
+
56
+ // プロジェクトタイトルの編集機能
57
+ if (projectTitle) {
58
+ projectTitle.addEventListener("dblclick", () => {
59
+ const currentName = projectTitle.textContent;
60
+ const input = document.createElement("input");
61
+ input.type = "text";
62
+ input.value = currentName;
63
+ input.style.width = "200px";
64
+
65
+ projectTitle.replaceWith(input);
66
+ input.focus();
67
+
68
+ const handleBlur = () => {
69
+ const newName = input.value.trim() || "無題";
70
+ window.vm.runtime.projectName = newName;
71
+ projectTitle.textContent = newName;
72
+ input.replaceWith(projectTitle);
73
+ };
74
+
75
+ input.addEventListener("blur", handleBlur);
76
+ input.addEventListener("keypress", (e) => {
77
+ if (e.key === "Enter") {
78
+ handleBlur();
79
+ }
80
+ });
81
+ });
82
+ }
83
+
84
+ if (loginButton) {
85
+ loginButton.addEventListener("click", () => {
86
+ const messageListener = (event) => {
87
+ if (event.origin === "https://soiz1-penguin-upload.hf.space" && event.data.token) {
88
+ window.removeEventListener("message", messageListener);
89
+ accessToken = event.data.token;
90
+
91
+ loginButton.style.display = "none";
92
+
93
+ fetchDriveFiles(accessToken)
94
+ .then(files => {
95
+ displayFileList(files, accessToken, modal, addon);
96
+ fileListContainer.style.display = "block";
97
+ })
98
+ .catch(error => {
99
+ console.error("ファイル一覧取得エラー:", error);
100
+ showAlert(addon, "error", "ファイル一覧の取得に失敗しました");
101
+ });
102
+ }
103
+ };
104
+ window.addEventListener("message", messageListener);
105
+
106
+ const authUrl = `https://accounts.google.com/o/oauth2/auth?` +
107
+ `client_id=${CLIENT_ID}` +
108
+ `&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` +
109
+ `&response_type=token` +
110
+ `&scope=${encodeURIComponent(SCOPES)}`;
111
+
112
+ window.open(authUrl, "_blank", "width=500,height=600");
113
+ });
114
+ }
115
+
116
+ if (newFileButton) {
117
+ newFileButton.addEventListener("click", () => {
118
+ saveToGoogleDrive(null, null, modal.remove, addon)
119
+ .catch(error => {
120
+ console.error("新規保存エラー:", error);
121
+ showAlert(addon, "error", "新規保存に失敗しました");
122
+ });
123
+ });
124
+ }
125
+
126
+ modal.backdrop.addEventListener("click", modal.remove);
127
+ modal.closeButton.addEventListener("click", modal.remove);
128
+ }
129
+
130
+ async function fetchDriveFiles(accessToken) {
131
+ const response = await fetch("https://www.googleapis.com/drive/v3/files?q=mimeType='application/x-scratch'", {
132
+ headers: {
133
+ Authorization: `Bearer ${accessToken}`,
134
+ },
135
+ });
136
+
137
+ if (!response.ok) {
138
+ throw new Error(await response.text());
139
+ }
140
+
141
+ const data = await response.json();
142
+ return data.files || [];
143
+ }
144
+
145
+ function displayFileList(files, accessToken, modal, addon) {
146
+ const fileList = modal.content.querySelector("#file-list");
147
+ fileList.innerHTML = "";
148
+
149
+ if (files.length === 0) {
150
+ fileList.innerHTML = "<p>保存されたファイルが見つかりません</p>";
151
+ return;
152
+ }
153
+
154
+ files.forEach(file => {
155
+ const fileItem = document.createElement("div");
156
+ fileItem.style.padding = "0.5rem";
157
+ fileItem.style.borderBottom = "1px solid #eee";
158
+ fileItem.style.display = "grid";
159
+ fileItem.style.gridTemplateColumns = "1fr auto auto";
160
+ fileItem.style.gap = "1rem";
161
+ fileItem.style.alignItems = "center";
162
+
163
+ const fileName = document.createElement("span");
164
+ fileName.textContent = file.name.replace('.sb3.txt', '');
165
+
166
+ const loadButton = document.createElement("button");
167
+ loadButton.textContent = "読み込む";
168
+ loadButton.className = "button";
169
+ loadButton.style.width = "80px";
170
+
171
+ loadButton.addEventListener("click", (e) => {
172
+ e.stopPropagation();
173
+ if (confirm(`"${file.name}"を読み込みますか?現在のプロジェクトは失われます。`)) {
174
+ const url = `${PROXY_URL}${file.id}`;
175
+ window.location.href = `?project_url=${encodeURIComponent(url)}`;
176
+ }
177
+ });
178
+
179
+ const replaceButton = document.createElement("button");
180
+ replaceButton.textContent = "上書き保存";
181
+ replaceButton.className = "button";
182
+ replaceButton.style.width = "100px";
183
+
184
+ replaceButton.addEventListener("click", (e) => {
185
+ e.stopPropagation();
186
+ if (confirm(`"${file.name}"を現在のプロジェクトで上書きしますか?`)) {
187
+ saveToGoogleDrive(file.id, file.name, modal.remove, addon)
188
+ .catch(error => {
189
+ console.error("ファイル上書きエラー:", error);
190
+ showAlert(addon, "error", "ファイルの上書きに失敗しました");
191
+ });
192
+ }
193
+ });
194
+
195
+ // 共有リンクとコピーボタン
196
+ const linkContainer = document.createElement("div");
197
+ linkContainer.style.gridColumn = "1 / -1";
198
+ linkContainer.style.display = "flex";
199
+ linkContainer.style.alignItems = "center";
200
+ linkContainer.style.gap = "0.5rem";
201
+ linkContainer.style.fontSize = "0.9em";
202
+
203
+ const link = document.createElement("a");
204
+ link.href = `${SHORT_URL}${file.id}`;
205
+ link.textContent = `${SHORT_URL}${file.id}`;
206
+ link.target = "_blank";
207
+ link.rel = "noopener noreferrer";
208
+
209
+ const copyButton = document.createElement("button");
210
+ copyButton.textContent = "リンクをコピー";
211
+ copyButton.className = "button";
212
+ copyButton.style.fontSize = "0.8em";
213
+ copyButton.style.padding = "0.2rem 0.5rem";
214
+
215
+ copyButton.addEventListener("click", (e) => {
216
+ e.stopPropagation();
217
+ navigator.clipboard.writeText(`${SHORT_URL}${file.id}`)
218
+ .then(() => showAlert(addon, "success", "リンクをクリップボードにコピーしました"))
219
+ .catch(() => showAlert(addon, "error", "リンクのコピーに失敗しました"));
220
+ });
221
+
222
+ linkContainer.appendChild(document.createTextNode("共有リンク: "));
223
+ linkContainer.appendChild(link);
224
+ linkContainer.appendChild(copyButton);
225
+
226
+ fileItem.appendChild(fileName);
227
+ fileItem.appendChild(loadButton);
228
+ fileItem.appendChild(replaceButton);
229
+ fileItem.appendChild(linkContainer);
230
+ fileList.appendChild(fileItem);
231
+ });
232
+ }
233
+
234
+ async function saveToGoogleDrive(fileId, fileName, onSuccess, addon) {
235
+ try {
236
+ const blob = await window.vm.saveProjectSb3();
237
+ const projectName = window.vm.runtime.projectName || getProjectFilename || "無題";
238
+ const nameToUse = fileName || `${projectName}.sb3.txt`;
239
+
240
+ const metadata = {
241
+ name: nameToUse,
242
+ mimeType: "application/x-scratch",
243
+ };
244
+
245
+ const url = fileId
246
+ ? `https://www.googleapis.com/upload/drive/v3/files/${fileId}?uploadType=multipart`
247
+ : "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart";
248
+
249
+ const form = new FormData();
250
+ form.append("metadata", new Blob([JSON.stringify(metadata)], { type: "application/json" }));
251
+ form.append("file", blob);
252
+
253
+ const method = fileId ? "PATCH" : "POST";
254
+
255
+ const uploadResponse = await fetch(url, {
256
+ method,
257
+ headers: {
258
+ Authorization: `Bearer ${accessToken}`,
259
+ },
260
+ body: form,
261
+ });
262
+
263
+ if (!uploadResponse.ok) {
264
+ throw new Error(await uploadResponse.text());
265
+ }
266
+
267
+ const fileData = await uploadResponse.json();
268
+ console.log("ファイルがGoogleドライブに保存/更新されました:", fileData);
269
+
270
+ if (!fileId) {
271
+ const permissionResponse = await fetch(`https://www.googleapis.com/drive/v3/files/${fileData.id}/permissions`, {
272
+ method: "POST",
273
+ headers: {
274
+ Authorization: `Bearer ${accessToken}`,
275
+ "Content-Type": "application/json",
276
+ },
277
+ body: JSON.stringify({
278
+ role: "reader",
279
+ type: "anyone",
280
+ }),
281
+ });
282
+
283
+ if (!permissionResponse.ok) {
284
+ console.warn("権限の設定に失敗しましたが、ファイルは保存されました");
285
+ }
286
+ }
287
+
288
+ const action = fileId ? "上書き保存" : "新規保存";
289
+ showAlert(addon, "success", `Googleドライブに${action}しました`);
290
+ if (onSuccess) onSuccess();
291
+ } catch (error) {
292
+ console.error("保存エラー:", error);
293
+ showAlert(addon, "error", `保存に失敗しました: ${error.message}`);
294
+ throw error;
295
+ }
296
+ }
297
+
298
+ function showAlert(addon, type, message) {
299
+ addon.tab.redux.dispatch({
300
+ type: "scratch-gui/alerts/SHOW_ALERT",
301
+ payload: {
302
+ alertType: type,
303
+ message: message
304
+ }
305
+ });
306
+ }
307
+ };