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

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

Browse files
src/addons/addons/save-to-google/userscript.js CHANGED
@@ -1,7 +1,7 @@
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);
@@ -50,7 +50,7 @@ export default async ({ addon, console, msg }) => {
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
  `;
@@ -122,12 +122,13 @@ export default async ({ addon, console, msg }) => {
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
 
@@ -136,7 +137,7 @@ export default async ({ addon, console, msg }) => {
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
  },
@@ -150,16 +151,25 @@ export default async ({ addon, console, msg }) => {
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";
@@ -168,44 +178,50 @@ export default async ({ addon, console, msg }) => {
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 = "コピー";
@@ -213,32 +229,32 @@ export default async ({ addon, console, msg }) => {
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
  });
@@ -246,71 +262,66 @@ export default async ({ addon, console, msg }) => {
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) {
@@ -328,6 +339,7 @@ export default async ({ addon, console, msg }) => {
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`;
@@ -360,9 +372,55 @@ export default async ({ addon, console, msg }) => {
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: {
 
1
  import SB3Downloader from "/src/containers/sb3-downloader.jsx";
2
  import { getProjectFilename } from '/src/containers/sb3-downloader.jsx';
3
 
4
+ // サムネイル取得関数
5
  const getProjectThumbnail = () => new Promise(resolve => {
6
  window.vm.renderer.requestSnapshot(uri => {
7
  resolve(uri);
 
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(300px, 1fr)); gap: 1rem; margin-top: 1rem;"></div>
54
  </div>
55
  </div>
56
  `;
 
122
  }
123
 
124
  if (newFileButton) {
125
+ newFileButton.addEventListener("click", async () => {
126
+ try {
127
+ await saveToGoogleDrive(null, null, modal.remove, addon);
128
+ } catch (error) {
129
+ console.error("新規保存エラー:", error);
130
+ showAlert(addon, "error", "新規保存に失敗しました");
131
+ }
132
  });
133
  }
134
 
 
137
  }
138
 
139
  async function fetchDriveFiles(accessToken) {
140
+ const response = await fetch("https://www.googleapis.com/drive/v3/files?q=(mimeType='application/x-scratch' or mimeType='image/png')", {
141
  headers: {
142
  Authorization: `Bearer ${accessToken}`,
143
  },
 
151
  return data.files || [];
152
  }
153
 
154
+ function displayFileList(files, accessToken, modal, addon) {
155
  const fileList = modal.content.querySelector("#file-list");
156
  fileList.innerHTML = "";
157
 
158
+ // プロジェクトファイルとサムネイルを関連付ける
159
+ const projectFiles = files.filter(file => file.mimeType === 'application/x-scratch');
160
+ const thumbnailFiles = files.filter(file => file.mimeType === 'image/png');
161
+
162
+ if (projectFiles.length === 0) {
163
  fileList.innerHTML = "<p>保存されたファイルが見つかりません</p>";
164
  return;
165
  }
166
 
167
+ projectFiles.forEach(project => {
168
+ // 対応するサムネイルを探す
169
+ const thumbnail = thumbnailFiles.find(
170
+ thumb => thumb.name === `Penguin-Thumbnail-${project.id}.png`
171
+ );
172
+
173
  const fileItem = document.createElement("div");
174
  fileItem.style.border = "1px solid #ddd";
175
  fileItem.style.borderRadius = "8px";
 
178
  fileItem.style.flexDirection = "column";
179
  fileItem.style.gap = "0.5rem";
180
 
181
+ // サムネイル表示
182
+ if (thumbnail) {
183
+ const thumbnailImg = document.createElement("img");
184
+ thumbnailImg.src = `https://drive.google.com/thumbnail?id=${thumbnail.id}&sz=w300`;
185
+ thumbnailImg.style.width = "100%";
186
+ thumbnailImg.style.height = "auto";
187
+ thumbnailImg.style.borderRadius = "4px";
188
+ thumbnailImg.style.objectFit = "cover";
189
+ thumbnailImg.style.aspectRatio = "4/3";
190
+ fileItem.appendChild(thumbnailImg);
191
+ } else {
192
+ const thumbnailPlaceholder = document.createElement("div");
193
+ thumbnailPlaceholder.style.width = "100%";
194
+ thumbnailPlaceholder.style.height = "150px";
195
+ thumbnailPlaceholder.style.backgroundColor = "#f0f0f0";
196
+ thumbnailPlaceholder.style.display = "flex";
197
+ thumbnailPlaceholder.style.alignItems = "center";
198
+ thumbnailPlaceholder.style.justifyContent = "center";
199
+ thumbnailPlaceholder.style.borderRadius = "4px";
200
+ thumbnailPlaceholder.textContent = "サムネイルなし";
201
+ fileItem.appendChild(thumbnailPlaceholder);
202
  }
203
 
204
+ // プロジェクト名
205
+ const fileName = document.createElement("h3");
206
+ fileName.textContent = project.name.replace('.sb3.txt', '');
207
+ fileName.style.margin = "0";
208
+ fileName.style.fontSize = "1rem";
209
  fileName.style.textAlign = "center";
210
  fileItem.appendChild(fileName);
211
 
212
+ // 共有リンク
213
+ const linkContainer = document.createElement("div");
214
+ linkContainer.style.display = "flex";
215
+ linkContainer.style.gap = "0.5rem";
216
+ linkContainer.style.alignItems = "center";
217
+ linkContainer.style.justifyContent = "center";
218
+
219
+ const link = document.createElement("a");
220
+ link.href = `${SHORT_URL}${project.id}`;
221
+ link.textContent = "共有リンク";
222
+ link.target = "_blank";
223
+ link.rel = "noopener noreferrer";
224
+ link.style.fontSize = "0.9em";
 
 
225
 
226
  const copyButton = document.createElement("button");
227
  copyButton.textContent = "コピー";
 
229
  copyButton.style.fontSize = "0.8em";
230
  copyButton.style.padding = "0.2rem 0.5rem";
231
 
232
+ copyButton.addEventListener("click", (e) => {
233
+ e.stopPropagation();
234
+ navigator.clipboard.writeText(`${SHORT_URL}${project.id}`)
235
+ .then(() => showAlert(addon, "success", "リンクをクリップボードにコピーしました"))
236
+ .catch(() => showAlert(addon, "error", "リンクのコピーに失敗しました"));
237
  });
238
 
239
+ linkContainer.appendChild(link);
240
+ linkContainer.appendChild(copyButton);
241
+ fileItem.appendChild(linkContainer);
242
 
243
  // 操作ボタン
244
+ const buttonContainer = document.createElement("div");
245
+ buttonContainer.style.display = "flex";
246
+ buttonContainer.style.gap = "0.5rem";
247
+ buttonContainer.style.justifyContent = "center";
 
248
 
249
  const loadButton = document.createElement("button");
250
  loadButton.textContent = "読み込む";
251
  loadButton.className = "button";
252
+ loadButton.style.flex = "1";
 
253
 
254
+ loadButton.addEventListener("click", (e) => {
255
+ e.stopPropagation();
256
+ if (confirm(`"${project.name}"を読み込みますか?現在のプロジェクトは失われます。`)) {
257
+ const url = `${PROXY_URL}${project.id}`;
258
  window.location.href = `?project_url=${encodeURIComponent(url)}`;
259
  }
260
  });
 
262
  const replaceButton = document.createElement("button");
263
  replaceButton.textContent = "上書き";
264
  replaceButton.className = "button";
265
+ replaceButton.style.flex = "1";
 
266
 
267
+ replaceButton.addEventListener("click", (e) => {
268
+ e.stopPropagation();
269
+ if (confirm(`"${project.name}"を現在のプロジェクトで上書きしますか?`)) {
270
+ saveToGoogleDrive(project.id, project.name, () => {
271
+ // モーダルを閉じずにリストを更新
272
  fetchDriveFiles(accessToken)
273
  .then(files => displayFileList(files, accessToken, modal, addon))
274
  .catch(error => console.error("更新エラー:", error));
275
+ }, addon)
276
+ .catch(error => {
277
+ console.error("ファイル上書きエラー:", error);
278
+ showAlert(addon, "error", "ファイルの上書きに失敗しました");
279
+ });
280
  }
281
  });
282
 
283
  const deleteButton = document.createElement("button");
284
  deleteButton.textContent = "削除";
285
  deleteButton.className = "button";
286
+ deleteButton.style.flex = "1";
 
 
287
  deleteButton.style.backgroundColor = "#ff4444";
288
  deleteButton.style.color = "white";
289
 
290
+ deleteButton.addEventListener("click", async (e) => {
291
+ e.stopPropagation();
292
+ if (confirm(`"${project.name}"とそのサムネイルを完全に削除しますか?この操作は元に戻せません。`)) {
293
+ try {
294
+ // プロジェクトファイルを削除
295
+ await deleteFile(project.id, accessToken);
296
+
297
+ // 対応するサムネイルを探して削除
298
+ const thumbnailToDelete = thumbnailFiles.find(
299
+ thumb => thumb.name === `Penguin-Thumbnail-${project.id}.png`
300
+ );
301
+
302
+ if (thumbnailToDelete) {
303
+ await deleteFile(thumbnailToDelete.id, accessToken);
304
+ }
305
+
306
+ showAlert(addon, "success", "ファイルを削除しました");
307
+ // リストを更新
308
+ fetchDriveFiles(accessToken)
309
+ .then(files => displayFileList(files, accessToken, modal, addon))
310
+ .catch(error => console.error("更新エラー:", error));
311
+ } catch (error) {
312
+ console.error("削除エラー:", error);
313
+ showAlert(addon, "error", "ファイルの削除に失敗しました");
314
+ }
315
  }
316
  });
317
 
318
+ buttonContainer.appendChild(loadButton);
319
+ buttonContainer.appendChild(replaceButton);
320
+ buttonContainer.appendChild(deleteButton);
321
+ fileItem.appendChild(buttonContainer);
322
 
323
  fileList.appendChild(fileItem);
324
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
  }
326
 
327
  async function deleteFile(fileId, accessToken) {
 
339
 
340
  async function saveToGoogleDrive(fileId, fileName, onSuccess, addon) {
341
  try {
342
+ // プロジェクトを保存
343
  const blob = await window.vm.saveProjectSb3();
344
  const projectName = window.vm.runtime.projectName || getProjectFilename || "無題";
345
  const nameToUse = fileName || `${projectName}.sb3.txt`;
 
372
  }
373
 
374
  const fileData = await uploadResponse.json();
375
+ console.log("プロジェクトファイルがGoogleドライブに保存/更新されました:", fileData);
376
+
377
+ // サムネイルを保存
378
+ try {
379
+ const thumbnailDataUrl = await getProjectThumbnail();
380
+ const thumbnailBlob = await (await fetch(thumbnailDataUrl)).blob();
381
+ const thumbnailMetadata = {
382
+ name: `Penguin-Thumbnail-${fileData.id}.png`,
383
+ mimeType: "image/png",
384
+ };
385
+
386
+ // 既存のサムネイルを探す
387
+ const existingThumbnailResponse = await fetch(
388
+ `https://www.googleapis.com/drive/v3/files?q=name='${thumbnailMetadata.name}'`,
389
+ {
390
+ headers: {
391
+ Authorization: `Bearer ${accessToken}`,
392
+ },
393
+ }
394
+ );
395
+
396
+ const existingThumbnailData = await existingThumbnailResponse.json();
397
+ const thumbnailFileId = existingThumbnailData.files?.[0]?.id;
398
+
399
+ const thumbnailUrl = thumbnailFileId
400
+ ? `https://www.googleapis.com/upload/drive/v3/files/${thumbnailFileId}?uploadType=multipart`
401
+ : "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart";
402
+
403
+ const thumbnailForm = new FormData();
404
+ thumbnailForm.append("metadata", new Blob([JSON.stringify(thumbnailMetadata)], { type: "application/json" }));
405
+ thumbnailForm.append("file", thumbnailBlob);
406
+
407
+ const thumbnailMethod = thumbnailFileId ? "PATCH" : "POST";
408
+
409
+ await fetch(thumbnailUrl, {
410
+ method: thumbnailMethod,
411
+ headers: {
412
+ Authorization: `Bearer ${accessToken}`,
413
+ },
414
+ body: thumbnailForm,
415
+ });
416
+
417
+ console.log("サムネイルがGoogleドライブに保存/更新されました");
418
+ } catch (thumbnailError) {
419
+ console.warn("サムネイルの保存に失敗しましたが、プロジェクトは保存されました:", thumbnailError);
420
+ }
421
 
422
  if (!fileId) {
423
+ // 新規保存の場合、公開設定
424
  const permissionResponse = await fetch(`https://www.googleapis.com/drive/v3/files/${fileData.id}/permissions`, {
425
  method: "POST",
426
  headers: {