soiz1 commited on
Commit
c9db4f8
·
verified ·
1 Parent(s): ced1b22

Update src/addons/addons/save-to-google/userscript.js

Browse files
src/addons/addons/save-to-google/userscript.js CHANGED
@@ -1,307 +1,402 @@
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/close2";
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
  };
 
1
  import SB3Downloader from "/src/containers/sb3-downloader.jsx";
2
  import { getProjectFilename } from '/src/containers/sb3-downloader.jsx';
3
+
4
+ // getProjectThumbnail関数を追加
5
+ const getProjectThumbnail = () => new Promise(resolve => {
6
+ window.vm.renderer.requestSnapshot(uri => {
7
+ resolve(uri);
8
+ });
9
+ });
10
+
11
  export default async ({ addon, console, msg }) => {
12
+ const CLIENT_ID = "1033286471224-n9mv8l869fqikubj2e8q92n8ige3qr6r.apps.googleusercontent.com";
13
+ const REDIRECT_URI = "https://soiz1-penguin-upload.hf.space/close2";
14
+ const SCOPES = "https://www.googleapis.com/auth/drive.file";
15
+ const PROXY_URL = "https://soiz1-drive-proxy.hf.space/?file_id=";
16
+ const SHORT_URL = "https://s4.rf.gd/";
17
+
18
+ let accessToken = null;
19
+
20
+ while (true) {
21
+ const targetElem = await addon.tab.waitForElement(
22
+ 'div[class*="menu-bar_file-group"] > div:last-child:not(.sa-record)',
23
+ { markAsSeen: true }
24
+ );
25
+
26
+ if (!document.querySelector('.sa-custom-modal-button')) {
27
+ const button = document.createElement("div");
28
+ button.className = "sa-custom-modal-button " + targetElem.className;
29
+ button.textContent = "保存";
30
+ button.style.cursor = "pointer";
31
+
32
+ button.addEventListener("click", () => {
33
+ showMainModal(addon);
34
+ });
35
+
36
+ targetElem.parentElement.appendChild(button);
37
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  }
39
 
40
+ function showMainModal(addon) {
41
+ const modal = addon.tab.createModal("Googleドライブに保存", { isOpen: true, useEditorClasses: true });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
+ modal.content.innerHTML = `
44
+ <div style="padding: 1rem;">
45
+ <h1>Googleドライブに接続</h1>
46
+ <p>Googleでログインして、プロジェクトを保存または更新します。</p>
47
+ <button id="google-login-button" class="button">Googleでログイン</button>
48
+ <div id="file-list-container" style="margin-top: 1rem; display: none;">
49
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
50
+ <h2 style="margin: 0;">プロジェクト: <span id="project-title" style="cursor: pointer; border-bottom: 1px dashed #000;">${window.vm.runtime.projectName || "無題"}</span></h2>
51
+ <button id="new-file-button" class="button">新規保存</button>
52
+ </div>
53
+ <div id="file-list" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 1rem; margin-top: 1rem;"></div>
54
+ </div>
55
+ </div>
56
+ `;
57
+
58
+ const loginButton = modal.content.querySelector("#google-login-button");
59
+ const fileListContainer = modal.content.querySelector("#file-list-container");
60
+ const fileList = modal.content.querySelector("#file-list");
61
+ const newFileButton = modal.content.querySelector("#new-file-button");
62
+ const projectTitle = modal.content.querySelector("#project-title");
63
+
64
+ // プロジェクトタイトルの編集機能
65
+ if (projectTitle) {
66
+ projectTitle.addEventListener("dblclick", () => {
67
+ const currentName = projectTitle.textContent;
68
+ const input = document.createElement("input");
69
+ input.type = "text";
70
+ input.value = currentName;
71
+ input.style.width = "200px";
72
+
73
+ projectTitle.replaceWith(input);
74
+ input.focus();
75
+
76
+ const handleBlur = () => {
77
+ const newName = input.value.trim() || "無題";
78
+ window.vm.runtime.projectName = newName;
79
+ projectTitle.textContent = newName;
80
+ input.replaceWith(projectTitle);
81
+ };
82
+
83
+ input.addEventListener("blur", handleBlur);
84
+ input.addEventListener("keypress", (e) => {
85
+ if (e.key === "Enter") {
86
+ handleBlur();
87
+ }
88
+ });
89
+ });
90
+ }
91
+
92
+ if (loginButton) {
93
+ loginButton.addEventListener("click", () => {
94
+ const messageListener = (event) => {
95
+ if (event.origin === "https://soiz1-penguin-upload.hf.space" && event.data.token) {
96
+ window.removeEventListener("message", messageListener);
97
+ accessToken = event.data.token;
98
+
99
+ loginButton.style.display = "none";
100
+
101
+ fetchDriveFiles(accessToken)
102
+ .then(files => {
103
+ displayFileList(files, accessToken, modal, addon);
104
+ fileListContainer.style.display = "block";
105
+ })
106
+ .catch(error => {
107
+ console.error("ファイル一覧取得エラー:", error);
108
+ showAlert(addon, "error", "ファイル一覧の取得に失敗しました");
109
+ });
110
+ }
111
+ };
112
+ window.addEventListener("message", messageListener);
113
+
114
+ const authUrl = `https://accounts.google.com/o/oauth2/auth?` +
115
+ `client_id=${CLIENT_ID}` +
116
+ `&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` +
117
+ `&response_type=token` +
118
+ `&scope=${encodeURIComponent(SCOPES)}`;
119
+
120
+ window.open(authUrl, "_blank", "width=500,height=600");
121
+ });
122
+ }
123
+
124
+ if (newFileButton) {
125
+ newFileButton.addEventListener("click", () => {
126
+ saveToGoogleDrive(null, null, modal.remove, addon)
127
+ .catch(error => {
128
+ console.error("新規保存エラー:", error);
129
+ showAlert(addon, "error", "新規保存に失敗しました");
130
+ });
131
+ });
132
+ }
133
 
134
+ modal.backdrop.addEventListener("click", modal.remove);
135
+ modal.closeButton.addEventListener("click", modal.remove);
 
 
 
 
 
 
136
  }
137
 
138
+ async function fetchDriveFiles(accessToken) {
139
+ const response = await fetch("https://www.googleapis.com/drive/v3/files?q=mimeType='application/x-scratch'", {
140
+ headers: {
141
+ Authorization: `Bearer ${accessToken}`,
142
+ },
143
+ });
144
 
145
+ if (!response.ok) {
146
+ throw new Error(await response.text());
147
+ }
 
 
 
148
 
149
+ const data = await response.json();
150
+ return data.files || [];
151
  }
152
 
153
+ async function displayFileList(files, accessToken, modal, addon) {
154
+ const fileList = modal.content.querySelector("#file-list");
155
+ fileList.innerHTML = "";
156
 
157
+ if (files.length === 0) {
158
+ fileList.innerHTML = "<p>保存されたファイルが見つかりません</p>";
159
+ return;
160
+ }
161
 
162
+ for (const file of files) {
163
+ const fileItem = document.createElement("div");
164
+ fileItem.style.border = "1px solid #ddd";
165
+ fileItem.style.borderRadius = "8px";
166
+ fileItem.style.padding = "1rem";
167
+ fileItem.style.display = "flex";
168
+ fileItem.style.flexDirection = "column";
169
+ fileItem.style.gap = "0.5rem";
170
+
171
+ // サムネイル画像を取得して表示
172
+ try {
173
+ const thumbnailUrl = await getThumbnailForFile(file.id, accessToken);
174
+ if (thumbnailUrl) {
175
+ const thumbnail = document.createElement("img");
176
+ thumbnail.src = thumbnailUrl;
177
+ thumbnail.style.width = "100%";
178
+ thumbnail.style.height = "auto";
179
+ thumbnail.style.borderRadius = "4px";
180
+ thumbnail.style.objectFit = "cover";
181
+ thumbnail.style.aspectRatio = "4/3";
182
+ fileItem.appendChild(thumbnail);
183
+ }
184
+ } catch (error) {
185
+ console.error("サムネイル取得エラー:", error);
186
+ }
187
+
188
+ const fileName = document.createElement("div");
189
+ fileName.textContent = file.name.replace('.sb3.txt', '');
190
+ fileName.style.fontWeight = "bold";
191
+ fileName.style.textAlign = "center";
192
+ fileItem.appendChild(fileName);
193
+
194
+ // 短縮URL表示
195
+ const shortUrl = `${SHORT_URL}${file.id}`;
196
+ const urlContainer = document.createElement("div");
197
+ urlContainer.style.display = "flex";
198
+ urlContainer.style.gap = "0.5rem";
199
+ urlContainer.style.alignItems = "center";
200
+ urlContainer.style.fontSize = "0.8em";
201
+
202
+ const urlInput = document.createElement("input");
203
+ urlInput.type = "text";
204
+ urlInput.value = shortUrl;
205
+ urlInput.readOnly = true;
206
+ urlInput.style.flexGrow = "1";
207
+ urlInput.style.padding = "0.2rem";
208
+ urlInput.style.fontSize = "0.8em";
209
+
210
+ const copyButton = document.createElement("button");
211
+ copyButton.textContent = "コピー";
212
+ copyButton.className = "button";
213
+ copyButton.style.fontSize = "0.8em";
214
+ copyButton.style.padding = "0.2rem 0.5rem";
215
+
216
+ copyButton.addEventListener("click", () => {
217
+ urlInput.select();
218
+ document.execCommand("copy");
219
+ showAlert(addon, "success", "リンクをクリップボードにコピーしました");
220
+ });
221
+
222
+ urlContainer.appendChild(urlInput);
223
+ urlContainer.appendChild(copyButton);
224
+ fileItem.appendChild(urlContainer);
225
+
226
+ // 操作ボタン
227
+ const buttonsContainer = document.createElement("div");
228
+ buttonsContainer.style.display = "grid";
229
+ buttonsContainer.style.gridTemplateColumns = "1fr 1fr";
230
+ buttonsContainer.style.gap = "0.5rem";
231
+ buttonsContainer.style.marginTop = "0.5rem";
232
+
233
+ const loadButton = document.createElement("button");
234
+ loadButton.textContent = "読み込む";
235
+ loadButton.className = "button";
236
+ loadButton.style.fontSize = "0.8em";
237
+ loadButton.style.padding = "0.3rem";
238
+
239
+ loadButton.addEventListener("click", () => {
240
+ if (confirm(`"${file.name}"を読み込みますか?現在のプロジェクトは失われます。`)) {
241
+ const url = `${PROXY_URL}${file.id}`;
242
+ window.location.href = `?project_url=${encodeURIComponent(url)}`;
243
+ }
244
+ });
245
+
246
+ const replaceButton = document.createElement("button");
247
+ replaceButton.textContent = "上書き";
248
+ replaceButton.className = "button";
249
+ replaceButton.style.fontSize = "0.8em";
250
+ replaceButton.style.padding = "0.3rem";
251
+
252
+ replaceButton.addEventListener("click", () => {
253
+ if (confirm(`"${file.name}"を現在のプロジェクトで上書きしますか?`)) {
254
+ saveToGoogleDrive(file.id, file.name, () => {
255
+ // 上書き後にファイルリストを更新
256
+ fetchDriveFiles(accessToken)
257
+ .then(files => displayFileList(files, accessToken, modal, addon))
258
+ .catch(error => console.error("更新エラー:", error));
259
+ }, addon);
260
+ }
261
+ });
262
+
263
+ const deleteButton = document.createElement("button");
264
+ deleteButton.textContent = "削除";
265
+ deleteButton.className = "button";
266
+ deleteButton.style.fontSize = "0.8em";
267
+ deleteButton.style.padding = "0.3rem";
268
+ deleteButton.style.gridColumn = "1 / -1";
269
+ deleteButton.style.backgroundColor = "#ff4444";
270
+ deleteButton.style.color = "white";
271
+
272
+ deleteButton.addEventListener("click", () => {
273
+ if (confirm(`"${file.name}"を完全に削除しますか?この操作は元に戻せません。`)) {
274
+ deleteFile(file.id, accessToken)
275
+ .then(() => {
276
+ showAlert(addon, "success", "ファイルを削除しました");
277
+ return fetchDriveFiles(accessToken);
278
+ })
279
+ .then(files => displayFileList(files, accessToken, modal, addon))
280
+ .catch(error => {
281
+ console.error("削除エラー:", error);
282
+ showAlert(addon, "error", "ファイルの削除に失敗しました");
283
+ });
284
+ }
285
+ });
286
 
287
+ buttonsContainer.appendChild(loadButton);
288
+ buttonsContainer.appendChild(replaceButton);
289
+ buttonsContainer.appendChild(deleteButton);
290
+ fileItem.appendChild(buttonsContainer);
291
+
292
+ fileList.appendChild(fileItem);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  }
294
+ }
295
+
296
+ async function getThumbnailForFile(fileId, accessToken) {
297
+ try {
298
+ const response = await fetch(`https://www.googleapis.com/drive/v3/files/${fileId}?fields=thumbnailLink`, {
299
+ headers: {
300
+ Authorization: `Bearer ${accessToken}`,
301
+ },
 
 
 
 
 
 
302
  });
303
+
304
+ if (!response.ok) {
305
+ throw new Error(await response.text());
306
+ }
307
+
308
+ const data = await response.json();
309
+ return data.thumbnailLink || null;
310
+ } catch (error) {
311
+ console.error("サムネイル取得エラー:", error);
312
+ return null;
313
  }
314
+ }
315
+
316
+ async function deleteFile(fileId, accessToken) {
317
+ const response = await fetch(`https://www.googleapis.com/drive/v3/files/${fileId}`, {
318
+ method: "DELETE",
319
+ headers: {
320
+ Authorization: `Bearer ${accessToken}`,
321
+ },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  });
323
 
324
+ if (!response.ok) {
325
+ throw new Error(await response.text());
326
  }
 
 
 
 
 
 
 
 
 
327
  }
328
+
329
+ async function saveToGoogleDrive(fileId, fileName, onSuccess, addon) {
330
+ try {
331
+ const blob = await window.vm.saveProjectSb3();
332
+ const projectName = window.vm.runtime.projectName || getProjectFilename || "無題";
333
+ const nameToUse = fileName || `${projectName}.sb3.txt`;
334
+
335
+ const metadata = {
336
+ name: nameToUse,
337
+ mimeType: "application/x-scratch",
338
+ };
339
+
340
+ const url = fileId
341
+ ? `https://www.googleapis.com/upload/drive/v3/files/${fileId}?uploadType=multipart`
342
+ : "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart";
343
+
344
+ const form = new FormData();
345
+ form.append("metadata", new Blob([JSON.stringify(metadata)], { type: "application/json" }));
346
+ form.append("file", blob);
347
+
348
+ const method = fileId ? "PATCH" : "POST";
349
+
350
+ const uploadResponse = await fetch(url, {
351
+ method,
352
+ headers: {
353
+ Authorization: `Bearer ${accessToken}`,
354
+ },
355
+ body: form,
356
+ });
357
+
358
+ if (!uploadResponse.ok) {
359
+ throw new Error(await uploadResponse.text());
360
+ }
361
+
362
+ const fileData = await uploadResponse.json();
363
+ console.log("ファイルがGoogleドライブに保存/更新されました:", fileData);
364
+
365
+ if (!fileId) {
366
+ const permissionResponse = await fetch(`https://www.googleapis.com/drive/v3/files/${fileData.id}/permissions`, {
367
+ method: "POST",
368
+ headers: {
369
+ Authorization: `Bearer ${accessToken}`,
370
+ "Content-Type": "application/json",
371
+ },
372
+ body: JSON.stringify({
373
+ role: "reader",
374
+ type: "anyone",
375
+ }),
376
+ });
377
+
378
+ if (!permissionResponse.ok) {
379
+ console.warn("権限の設定に失敗しましたが、ファイルは保存されました");
380
+ }
381
+ }
382
+
383
+ const action = fileId ? "上書き保存" : "新規保存";
384
+ showAlert(addon, "success", `Googleドライブに${action}しました`);
385
+ if (onSuccess) onSuccess();
386
+ } catch (error) {
387
+ console.error("保存エラー:", error);
388
+ showAlert(addon, "error", `保存に失敗しました: ${error.message}`);
389
+ throw error;
390
+ }
391
+ }
392
+
393
+ function showAlert(addon, type, message) {
394
+ addon.tab.redux.dispatch({
395
+ type: "scratch-gui/alerts/SHOW_ALERT",
396
+ payload: {
397
+ alertType: type,
398
+ message: message
399
+ }
400
+ });
401
+ }
402
  };