Spaces:
Runtime error
Runtime error
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 |
-
//
|
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(
|
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 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
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 |
-
|
154 |
const fileList = modal.content.querySelector("#file-list");
|
155 |
fileList.innerHTML = "";
|
156 |
|
157 |
-
|
|
|
|
|
|
|
|
|
158 |
fileList.innerHTML = "<p>保存されたファイルが見つかりません</p>";
|
159 |
return;
|
160 |
}
|
161 |
|
162 |
-
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
173 |
-
const
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
186 |
}
|
187 |
|
188 |
-
|
189 |
-
fileName
|
190 |
-
fileName.
|
|
|
|
|
191 |
fileName.style.textAlign = "center";
|
192 |
fileItem.appendChild(fileName);
|
193 |
|
194 |
-
//
|
195 |
-
const
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
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 |
-
|
218 |
-
|
219 |
-
|
|
|
220 |
});
|
221 |
|
222 |
-
|
223 |
-
|
224 |
-
fileItem.appendChild(
|
225 |
|
226 |
// 操作ボタン
|
227 |
-
const
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
buttonsContainer.style.marginTop = "0.5rem";
|
232 |
|
233 |
const loadButton = document.createElement("button");
|
234 |
loadButton.textContent = "読み込む";
|
235 |
loadButton.className = "button";
|
236 |
-
loadButton.style.
|
237 |
-
loadButton.style.padding = "0.3rem";
|
238 |
|
239 |
-
loadButton.addEventListener("click", () => {
|
240 |
-
|
241 |
-
|
|
|
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.
|
250 |
-
replaceButton.style.padding = "0.3rem";
|
251 |
|
252 |
-
replaceButton.addEventListener("click", () => {
|
253 |
-
|
254 |
-
|
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.
|
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 |
-
|
274 |
-
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
.
|
281 |
-
|
282 |
-
|
283 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
284 |
}
|
285 |
});
|
286 |
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
fileItem.appendChild(
|
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("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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: {
|