izum0 commited on
Commit
0ffec5f
·
1 Parent(s): 109464c

import s4s-editor from huggingface space

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. app.py +43 -0
  2. src/addons/addons/save-to-google/_manifest_entry.js +17 -0
  3. src/addons/addons/save-to-google/_runtime_entry.js +7 -0
  4. src/addons/addons/save-to-google/style.css +55 -0
  5. src/addons/addons/save-to-google/userscript.js +544 -0
  6. src/lib/libraries/decks/steps/add-backdrop.LTR.png +0 -0
  7. src/lib/libraries/decks/steps/add-backdrop.RTL.png +0 -0
  8. src/lib/libraries/decks/steps/add-effects.am.png +0 -0
  9. src/lib/libraries/decks/steps/add-effects.ar.png +0 -0
  10. src/lib/libraries/decks/steps/add-effects.en.png +0 -0
  11. src/lib/libraries/decks/steps/add-effects.es.png +0 -0
  12. src/lib/libraries/decks/steps/add-effects.fr.png +0 -0
  13. src/lib/libraries/decks/steps/add-effects.ja.png +0 -0
  14. src/lib/libraries/decks/steps/add-effects.pt_BR.png +0 -0
  15. src/lib/libraries/decks/steps/add-effects.sw.png +0 -0
  16. src/lib/libraries/decks/steps/add-effects.tr.png +0 -0
  17. src/lib/libraries/decks/steps/add-effects.uk.png +0 -0
  18. src/lib/libraries/decks/steps/add-effects.zh_CN.png +0 -0
  19. src/lib/libraries/decks/steps/add-effects.zh_TW.png +0 -0
  20. src/lib/libraries/decks/steps/add-effects.zu.png +0 -0
  21. src/lib/libraries/decks/steps/animate-char-add-sound.am.png +0 -0
  22. src/lib/libraries/decks/steps/animate-char-add-sound.ar.png +0 -0
  23. src/lib/libraries/decks/steps/animate-char-add-sound.en.png +0 -0
  24. src/lib/libraries/decks/steps/animate-char-add-sound.es.png +0 -0
  25. src/lib/libraries/decks/steps/animate-char-add-sound.fr.png +0 -0
  26. src/lib/libraries/decks/steps/animate-char-add-sound.ja.png +0 -0
  27. src/lib/libraries/decks/steps/animate-char-add-sound.pt_BR.png +0 -0
  28. src/lib/libraries/decks/steps/animate-char-add-sound.sw.png +0 -0
  29. src/lib/libraries/decks/steps/animate-char-add-sound.tr.png +0 -0
  30. src/lib/libraries/decks/steps/animate-char-add-sound.uk.png +0 -0
  31. src/lib/libraries/decks/steps/animate-char-add-sound.zh_CN.png +0 -0
  32. src/lib/libraries/decks/steps/animate-char-add-sound.zh_TW.png +0 -0
  33. src/lib/libraries/decks/steps/animate-char-add-sound.zu.png +0 -0
  34. src/lib/libraries/decks/steps/animate-char-change-color.am.png +0 -0
  35. src/lib/libraries/decks/steps/animate-char-change-color.ar.png +0 -0
  36. src/lib/libraries/decks/steps/animate-char-change-color.en.png +0 -0
  37. src/lib/libraries/decks/steps/animate-char-change-color.es.png +0 -0
  38. src/lib/libraries/decks/steps/animate-char-change-color.fr.png +0 -0
  39. src/lib/libraries/decks/steps/animate-char-change-color.ja.png +0 -0
  40. src/lib/libraries/decks/steps/animate-char-change-color.pt_BR.png +0 -0
  41. src/lib/libraries/decks/steps/animate-char-change-color.sw.png +0 -0
  42. src/lib/libraries/decks/steps/animate-char-change-color.tr.png +0 -0
  43. src/lib/libraries/decks/steps/animate-char-change-color.uk.png +0 -0
  44. src/lib/libraries/decks/steps/animate-char-change-color.zh_CN.png +0 -0
  45. src/lib/libraries/decks/steps/animate-char-change-color.zh_TW.png +0 -0
  46. src/lib/libraries/decks/steps/animate-char-change-color.zu.png +0 -0
  47. src/lib/libraries/decks/steps/animate-char-jump.am.png +0 -0
  48. src/lib/libraries/decks/steps/animate-char-jump.ar.png +0 -0
  49. src/lib/libraries/decks/steps/animate-char-jump.en.png +0 -0
  50. src/lib/libraries/decks/steps/animate-char-jump.es.png +0 -0
app.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import subprocess
3
+ import shutil
4
+
5
+ # GitHubのリポジトリURL
6
+ repo_url = "https://github.com/izum00/penguinmod-s4s"
7
+
8
+ # 環境変数からトークンを取得
9
+ token = os.getenv("github_token")
10
+ if not token:
11
+ raise RuntimeError("環境変数 'github_token' が設定されていません。")
12
+
13
+ # 認証情報付きのURLを作成
14
+ authed_url = repo_url.replace("https://", f"https://{token}@")
15
+
16
+ # カレントディレクトリのパス
17
+ current_dir = os.getcwd()
18
+
19
+ # Gitユーザー情報の設定
20
+ subprocess.run(["git", "config", "--global", "user.name", "izum00"], check=True)
21
+ subprocess.run([
22
+ "git", "config", "--global", "user.email",
23
+ os.getenv("mail") or "[email protected]"
24
+ ], check=True)
25
+
26
+ # Gitの安全設定
27
+ subprocess.run(["git", "config", "--global", "--add", "safe.directory", "/app"], check=True)
28
+
29
+ # .gitが存在していれば削除(Gitオブジェクト壊れ防止)
30
+ git_dir = os.path.join(current_dir, ".git")
31
+ if os.path.exists(git_dir):
32
+ shutil.rmtree(git_dir)
33
+
34
+ # Gitの初期化とLFSの初期化(LFSが導入されていることが前提)
35
+ subprocess.run(["git", "init"], check=True)
36
+ subprocess.run(["git", "lfs", "install"], check=True) # LFSが使われているなら必要
37
+ subprocess.run(["git", "remote", "add", "origin", authed_url], check=True)
38
+
39
+ # Gitの操作
40
+ subprocess.run(["git", "add", "."], check=True)
41
+ subprocess.run(["git", "commit", "-m", "auto commit from script"], check=True)
42
+ subprocess.run(["git", "branch", "-M", "main"], check=True)
43
+ subprocess.run(["git", "push", "-u", "origin", "main", "--force"], check=True)
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,544 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import SB3Downloader from "/src/containers/sb3-downloader.jsx";
2
+ import { getProjectFilename } from '/src/containers/sb3-downloader.jsx';
3
+
4
+ const getProjectThumbnail = () => new Promise(resolve => {
5
+ window.vm.renderer.requestSnapshot(uri => {
6
+ resolve(uri);
7
+ });
8
+ });
9
+
10
+ export default async ({ addon, console, msg }) => {
11
+ const CLIENT_ID = "1033286471224-n9mv8l869fqikubj2e8q92n8ige3qr6r.apps.googleusercontent.com";
12
+ const REDIRECT_URI = "https://soiz1-s4s-upload.hf.space/close2";
13
+ const SCOPES = "https://www.googleapis.com/auth/drive.file";
14
+ const PROXY_URL = "https://soiz1-drive-proxy.hf.space/?file_id=";
15
+ const SHORT_URL = "https://s4.rf.gd/";
16
+ const SHARE_URL = "https://scratch-school.ct.ws/upload?id=";
17
+
18
+ let accessToken = localStorage.getItem('googleDriveAccessToken') || null;
19
+ let currentAccountEmail = localStorage.getItem('googleDriveAccountEmail') || null;
20
+ let currentAccountName = localStorage.getItem('googleDriveAccountName') || null;
21
+
22
+ while (true) {
23
+ const targetElem = await addon.tab.waitForElement(
24
+ 'div[class*="menu-bar_file-group"] > div:last-child:not(.sa-record)',
25
+ { markAsSeen: true }
26
+ );
27
+
28
+ if (!document.querySelector('.sa-custom-modal-button')) {
29
+ const button = document.createElement("div");
30
+ button.className = "sa-custom-modal-button " + targetElem.className;
31
+ button.textContent = "保存";
32
+ button.style.cursor = "pointer";
33
+
34
+ button.addEventListener("click", () => {
35
+ showMainModal(addon);
36
+ });
37
+
38
+ targetElem.parentElement.appendChild(button);
39
+ }
40
+ // 「フィードバックを送信」ボタンを作成
41
+ const feedbackButton = document.createElement("div");
42
+ feedbackButton.className = "sa-feedback-button " + targetElem.className;
43
+ feedbackButton.textContent = "フィードバックを送信";
44
+ feedbackButton.style.cursor = "pointer";
45
+ feedbackButton.style.marginLeft = "10px"; // 少し間隔を空ける
46
+
47
+ feedbackButton.addEventListener("click", () => {
48
+ window.open("https://forms.gle/mP9U2biYcYUmupiY9", "_blank");
49
+ });
50
+
51
+ targetElem.parentElement.appendChild(feedbackButton);
52
+
53
+ }
54
+
55
+ function showMainModal(addon) {
56
+ const modal = addon.tab.createModal("Googleドライブに保存", {
57
+ isOpen: true,
58
+ useEditorClasses: true,
59
+ maxWidth: "800px",
60
+ maxHeight: "80vh"
61
+ });
62
+
63
+ modal.content.innerHTML = `
64
+ <div style="padding: 1rem; max-height: 70vh; overflow-y: auto;">
65
+ <h1 style="font-size: 1.5rem; margin-bottom: 1rem;">Googleドライブに接続</h1>
66
+ ${accessToken ? `
67
+ <div style="margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;">
68
+ <div>ログイン中: ${currentAccountName || currentAccountEmail || 'Googleアカウント'}</div>
69
+ <button id="change-account-button" class="button" style="padding: 0.25rem 0.5rem; font-size: 0.9rem;">アカウントを変更</button>
70
+ </div>
71
+ ` : `
72
+ <p style="margin-bottom: 1rem;">Googleでログインして、プロジェクトを保存または更新します。</p>
73
+ <button id="google-login-button" class="button">Googleでログイン</button>
74
+ `}
75
+ <div id="file-list-container" style="margin-top: 1rem; ${accessToken ? '' : 'display: none;'}">
76
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
77
+ <h2 style="margin: 0; font-size: 1.2rem;">プロジェクト: <span id="project-title" style="cursor: pointer; border-bottom: 1px dashed #000; color: #333;">${window.vm.runtime.projectName || "無題"}</span></h2>
78
+ <button id="new-file-button" class="button">新規保存</button>
79
+ </div>
80
+ <div id="file-list" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 1rem; margin-top: 1rem;"></div>
81
+ </div>
82
+ </div>
83
+ `;
84
+
85
+ const loginButton = modal.content.querySelector("#google-login-button");
86
+ const changeAccountButton = modal.content.querySelector("#change-account-button");
87
+ const fileListContainer = modal.content.querySelector("#file-list-container");
88
+ const fileList = modal.content.querySelector("#file-list");
89
+ const newFileButton = modal.content.querySelector("#new-file-button");
90
+ const projectTitle = modal.content.querySelector("#project-title");
91
+
92
+ // プロジェクトタイトルの編集機能
93
+ projectTitle?.addEventListener("dblclick", () => {
94
+ const currentName = projectTitle.textContent;
95
+ const input = document.createElement("input");
96
+ input.type = "text";
97
+ input.value = currentName;
98
+ input.style.width = "200px";
99
+
100
+ projectTitle.replaceWith(input);
101
+ input.focus();
102
+
103
+ const handleBlur = () => {
104
+ const newName = input.value.trim() || "無題";
105
+ window.vm.runtime.projectName = newName;
106
+ projectTitle.textContent = newName;
107
+ input.replaceWith(projectTitle);
108
+ };
109
+
110
+ input.addEventListener("blur", handleBlur);
111
+ input.addEventListener("keypress", (e) => {
112
+ if (e.key === "Enter") handleBlur();
113
+ });
114
+ });
115
+
116
+ if (loginButton) {
117
+ loginButton.addEventListener("click", () => {
118
+ startGoogleLogin(modal, addon);
119
+ });
120
+ }
121
+
122
+ if (changeAccountButton) {
123
+ changeAccountButton.addEventListener("click", () => {
124
+ accessToken = null;
125
+ currentAccountEmail = null;
126
+ currentAccountName = null;
127
+ localStorage.removeItem('googleDriveAccessToken');
128
+ localStorage.removeItem('googleDriveAccountEmail');
129
+ localStorage.removeItem('googleDriveAccountName');
130
+ showMainModal(addon);
131
+ modal.remove();
132
+ });
133
+ }
134
+
135
+ newFileButton?.addEventListener("click", async () => {
136
+ try {
137
+ await saveToGoogleDrive(null, null, modal.remove, addon);
138
+ } catch (error) {
139
+ console.error("新規保存エラー:", error);
140
+ showAlert(addon, "error", "新規保存に失敗しました");
141
+ }
142
+ });
143
+
144
+ if (accessToken) {
145
+ fetchDriveFiles(accessToken)
146
+ .then(files => {
147
+ displayFileList(files, accessToken, modal, addon);
148
+ fileListContainer.style.display = "block";
149
+ })
150
+ .catch(error => {
151
+ console.error("ファイル一覧取得エラー:", error);
152
+ showAlert(addon, "error", "ファイル一覧の取得に失敗しました");
153
+ });
154
+ }
155
+
156
+ modal.backdrop.addEventListener("click", modal.remove);
157
+ modal.closeButton.addEventListener("click", modal.remove);
158
+ }
159
+
160
+ function startGoogleLogin(modal, addon) {
161
+ const messageListener = (event) => {
162
+ if (event.origin === "https://soiz1-penguin-upload.hf.space" && event.data.token) {
163
+ window.removeEventListener("message", messageListener);
164
+ accessToken = event.data.token;
165
+ currentAccountEmail = event.data.email || null;
166
+ currentAccountName = event.data.name || null;
167
+
168
+ localStorage.setItem('googleDriveAccessToken', accessToken);
169
+ if (currentAccountEmail) {
170
+ localStorage.setItem('googleDriveAccountEmail', currentAccountEmail);
171
+ }
172
+ if (currentAccountName) {
173
+ localStorage.setItem('googleDriveAccountName', currentAccountName);
174
+ }
175
+
176
+ showMainModal(addon);
177
+ modal.remove();
178
+ }
179
+ };
180
+ window.addEventListener("message", messageListener);
181
+
182
+ const authUrl = `https://accounts.google.com/o/oauth2/auth?` +
183
+ `client_id=${CLIENT_ID}` +
184
+ `&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` +
185
+ `&response_type=token` +
186
+ `&scope=${encodeURIComponent(SCOPES)}`;
187
+
188
+ window.open(authUrl, "_blank", "width=500,height=600");
189
+ }
190
+
191
+ async function fetchDriveFiles(accessToken) {
192
+ const response = await fetch("https://www.googleapis.com/drive/v3/files?q=(mimeType='application/x-scratch' or mimeType='image/png')", {
193
+ headers: {
194
+ Authorization: `Bearer ${accessToken}`,
195
+ },
196
+ });
197
+
198
+ if (!response.ok) {
199
+ throw new Error(await response.text());
200
+ }
201
+
202
+ const data = await response.json();
203
+ return data.files || [];
204
+ }
205
+
206
+ function displayFileList(files, accessToken, modal, addon) {
207
+ const fileList = modal.content.querySelector("#file-list");
208
+ fileList.innerHTML = "";
209
+
210
+ // プロジェクトファイルとサムネイルを関連付ける
211
+ const projectFiles = files.filter(file => file.mimeType === 'application/x-scratch');
212
+ const thumbnailFiles = files.filter(file => file.mimeType === 'image/png');
213
+
214
+ if (projectFiles.length === 0) {
215
+ fileList.innerHTML = "<p style='grid-column: 1 / -1; text-align: center;'>保存されたファイルが見つかりません</p>";
216
+ return;
217
+ }
218
+
219
+ projectFiles.forEach(project => {
220
+ // 対応するサムネイルを探す
221
+ const thumbnail = thumbnailFiles.find(
222
+ thumb => thumb.name === `Penguin-Thumbnail-${project.id}.png`
223
+ );
224
+
225
+ const fileItem = document.createElement("div");
226
+ fileItem.style.border = "1px solid #ddd";
227
+ fileItem.style.borderRadius = "8px";
228
+ fileItem.style.padding = "1rem";
229
+ fileItem.style.display = "flex";
230
+ fileItem.style.flexDirection = "column";
231
+ fileItem.style.gap = "0.75rem";
232
+ fileItem.style.background = "#fff";
233
+ fileItem.style.height = "100%";
234
+
235
+ // サムネイル表示
236
+ const thumbnailContainer = document.createElement("div");
237
+ thumbnailContainer.style.position = "relative";
238
+ thumbnailContainer.style.aspectRatio = "4/3";
239
+ thumbnailContainer.style.maxHeight = "150px";
240
+ thumbnailContainer.style.overflow = "hidden";
241
+
242
+ if (thumbnail) {
243
+ const thumbnailImg = document.createElement("img");
244
+ thumbnailImg.src = `https://drive.google.com/thumbnail?id=${thumbnail.id}&sz=w300`;
245
+ thumbnailImg.style.width = "100%";
246
+ thumbnailImg.style.height = "100%";
247
+ thumbnailImg.style.borderRadius = "4px";
248
+ thumbnailImg.style.objectFit = "contain";
249
+ thumbnailImg.style.backgroundColor = "#f0f0f0";
250
+ thumbnailContainer.appendChild(thumbnailImg);
251
+ } else {
252
+ const thumbnailPlaceholder = document.createElement("div");
253
+ thumbnailPlaceholder.style.width = "100%";
254
+ thumbnailPlaceholder.style.height = "100%";
255
+ thumbnailPlaceholder.style.backgroundColor = "#f0f0f0";
256
+ thumbnailPlaceholder.style.display = "flex";
257
+ thumbnailPlaceholder.style.alignItems = "center";
258
+ thumbnailPlaceholder.style.justifyContent = "center";
259
+ thumbnailPlaceholder.style.borderRadius = "4px";
260
+ thumbnailPlaceholder.textContent = "サムネイルなし";
261
+ thumbnailContainer.appendChild(thumbnailPlaceholder);
262
+ }
263
+ fileItem.appendChild(thumbnailContainer);
264
+
265
+ // プロジェクト名
266
+ const fileName = document.createElement("h3");
267
+ fileName.textContent = project.name.replace('.s4s.txt', '');
268
+ fileName.style.margin = "0";
269
+ fileName.style.fontSize = "1rem";
270
+ fileName.style.textAlign = "center";
271
+ fileName.style.fontWeight = "bold";
272
+ fileName.style.color = "#333";
273
+ fileItem.appendChild(fileName);
274
+
275
+ // 共有リンク
276
+ const linkContainer = document.createElement("div");
277
+ linkContainer.style.display = "flex";
278
+ linkContainer.style.flexDirection = "column";
279
+ linkContainer.style.gap = "0.25rem";
280
+ linkContainer.style.marginBottom = "0.5rem";
281
+
282
+ const linkHeader = document.createElement("div");
283
+ linkHeader.style.display = "flex";
284
+ linkHeader.style.justifyContent = "space-between";
285
+ linkHeader.style.alignItems = "center";
286
+
287
+ const linkLabel = document.createElement("span");
288
+ linkLabel.textContent = "共有リンク:";
289
+ linkLabel.style.fontSize = "0.8em";
290
+ linkLabel.style.color = "#555";
291
+
292
+ const copyButton = document.createElement("button");
293
+ copyButton.textContent = "コピー";
294
+ copyButton.className = "button";
295
+ copyButton.style.fontSize = "0.8em";
296
+ copyButton.style.padding = "0.1rem 0.3rem";
297
+ copyButton.style.backgroundColor = "#e9e9e9";
298
+ copyButton.style.color = "#333";
299
+
300
+ copyButton.addEventListener("click", (e) => {
301
+ e.stopPropagation();
302
+ navigator.clipboard.writeText(`${SHORT_URL}${project.id}`)
303
+ .then(() => showAlert(addon, "success", "リンクをクリップボードにコピーしました"))
304
+ .catch(() => showAlert(addon, "error", "リンクのコピーに失敗しました"));
305
+ });
306
+
307
+ linkHeader.appendChild(linkLabel);
308
+ linkHeader.appendChild(copyButton);
309
+ linkContainer.appendChild(linkHeader);
310
+
311
+ const linkUrl = document.createElement("a");
312
+ linkUrl.href = `${SHORT_URL}${project.id}`;
313
+ linkUrl.textContent = `${SHORT_URL}${project.id}`;
314
+ linkUrl.target = "_blank";
315
+ linkUrl.rel = "noopener noreferrer";
316
+ linkUrl.style.fontSize = "0.9em";
317
+ linkUrl.style.wordBreak = "break-all";
318
+ linkUrl.style.color = "#1155cc";
319
+ linkUrl.style.textDecoration = "none";
320
+ linkUrl.style.borderBottom = "1px solid #1155cc";
321
+ linkContainer.appendChild(linkUrl);
322
+
323
+ fileItem.appendChild(linkContainer);
324
+
325
+ // 操作ボタン
326
+ const buttonContainer = document.createElement("div");
327
+ buttonContainer.style.display = "grid";
328
+ buttonContainer.style.gridTemplateColumns = "1fr 1fr";
329
+ buttonContainer.style.gap = "0.5rem";
330
+
331
+ const loadButton = document.createElement("button");
332
+ loadButton.textContent = "読み込む";
333
+ loadButton.className = "button";
334
+ loadButton.style.width = "100%";
335
+
336
+ loadButton.addEventListener("click", (e) => {
337
+ e.stopPropagation();
338
+ if (confirm(`"${project.name}"を読み込みますか?現在のプロジェクトは失われます。`)) {
339
+ const url = `${PROXY_URL}${project.id}`;
340
+ window.location.href = `?project_url=${encodeURIComponent(url)}`;
341
+ }
342
+ });
343
+
344
+ const replaceButton = document.createElement("button");
345
+ replaceButton.textContent = "上書き";
346
+ replaceButton.className = "button";
347
+ replaceButton.style.width = "100%";
348
+
349
+ replaceButton.addEventListener("click", (e) => {
350
+ e.stopPropagation();
351
+ if (confirm(`"${project.name}"を現在のプロジェクトで上書きしますか?`)) {
352
+ saveToGoogleDrive(project.id, project.name, () => {
353
+ fetchDriveFiles(accessToken)
354
+ .then(files => displayFileList(files, accessToken, modal, addon))
355
+ .catch(error => console.error("更新エラー:", error));
356
+ }, addon)
357
+ .catch(error => {
358
+ console.error("ファイル上書きエラー:", error);
359
+ showAlert(addon, "error", "ファイルの上書きに失敗しました");
360
+ });
361
+ }
362
+ });
363
+
364
+ const shareButton = document.createElement("button");
365
+ shareButton.textContent = "共有";
366
+ shareButton.className = "button";
367
+ shareButton.style.width = "100%";
368
+ shareButton.style.backgroundColor = "#4CAF50";
369
+ shareButton.style.color = "white";
370
+
371
+ shareButton.addEventListener("click", (e) => {
372
+ e.stopPropagation();
373
+ window.open(`${SHARE_URL}${project.id}`, "_blank");
374
+ });
375
+
376
+ const deleteButton = document.createElement("button");
377
+ deleteButton.textContent = "削除";
378
+ deleteButton.className = "button";
379
+ deleteButton.style.width = "100%";
380
+ deleteButton.style.backgroundColor = "#ff4444";
381
+ deleteButton.style.color = "white";
382
+
383
+ deleteButton.addEventListener("click", async (e) => {
384
+ e.stopPropagation();
385
+ if (confirm(`"${project.name}"とそのサムネイルを完全に削除しますか?この操作は元に戻せません。`)) {
386
+ try {
387
+ // プロジェクトファイルを削除
388
+ await deleteFile(project.id, accessToken);
389
+
390
+ // 対応するサムネイルを探して削除
391
+ const thumbnailToDelete = thumbnailFiles.find(
392
+ thumb => thumb.name === `Penguin-Thumbnail-${project.id}.png`
393
+ );
394
+
395
+ if (thumbnailToDelete) {
396
+ await deleteFile(thumbnailToDelete.id, accessToken);
397
+ }
398
+
399
+ showAlert(addon, "success", "ファイルを削除しました");
400
+ // リストを更新
401
+ fetchDriveFiles(accessToken)
402
+ .then(files => displayFileList(files, accessToken, modal, addon))
403
+ .catch(error => console.error("更新エラー:", error));
404
+ } catch (error) {
405
+ console.error("削除エラー:", error);
406
+ showAlert(addon, "error", "ファイルの削除に失敗しました");
407
+ }
408
+ }
409
+ });
410
+
411
+ buttonContainer.appendChild(loadButton);
412
+ buttonContainer.appendChild(replaceButton);
413
+ buttonContainer.appendChild(shareButton);
414
+ buttonContainer.appendChild(deleteButton);
415
+ fileItem.appendChild(buttonContainer);
416
+
417
+ fileList.appendChild(fileItem);
418
+ });
419
+ }
420
+
421
+ async function deleteFile(fileId, accessToken) {
422
+ const response = await fetch(`https://www.googleapis.com/drive/v3/files/${fileId}`, {
423
+ method: "DELETE",
424
+ headers: {
425
+ Authorization: `Bearer ${accessToken}`,
426
+ },
427
+ });
428
+
429
+ if (!response.ok) {
430
+ throw new Error(await response.text());
431
+ }
432
+ }
433
+
434
+ async function saveToGoogleDrive(fileId, fileName, onSuccess, addon) {
435
+ try {
436
+ // プロジェクトを���存
437
+ const blob = await window.vm.saveProjectSb3();
438
+ const projectName = window.vm.runtime.projectName || getProjectFilename || "無題";
439
+ const nameToUse = fileName || `${projectName}.s4s.txt`;
440
+
441
+ const metadata = {
442
+ name: nameToUse,
443
+ mimeType: "application/x-scratch",
444
+ };
445
+
446
+ const url = fileId
447
+ ? `https://www.googleapis.com/upload/drive/v3/files/${fileId}?uploadType=multipart`
448
+ : "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart";
449
+
450
+ const form = new FormData();
451
+ form.append("metadata", new Blob([JSON.stringify(metadata)], { type: "application/json" }));
452
+ form.append("file", blob);
453
+
454
+ const method = fileId ? "PATCH" : "POST";
455
+
456
+ const uploadResponse = await fetch(url, {
457
+ method,
458
+ headers: {
459
+ Authorization: `Bearer ${accessToken}`,
460
+ },
461
+ body: form,
462
+ });
463
+
464
+ if (!uploadResponse.ok) {
465
+ throw new Error(await uploadResponse.text());
466
+ }
467
+
468
+ const fileData = await uploadResponse.json();
469
+
470
+ // サムネイルを保存
471
+ try {
472
+ const thumbnailDataUrl = await getProjectThumbnail();
473
+ const thumbnailBlob = await (await fetch(thumbnailDataUrl)).blob();
474
+ const thumbnailMetadata = {
475
+ name: `Penguin-Thumbnail-${fileData.id}.png`,
476
+ mimeType: "image/png",
477
+ };
478
+
479
+ const existingThumbnailResponse = await fetch(
480
+ `https://www.googleapis.com/drive/v3/files?q=name='${thumbnailMetadata.name}'`,
481
+ {
482
+ headers: {
483
+ Authorization: `Bearer ${accessToken}`,
484
+ },
485
+ }
486
+ );
487
+
488
+ const existingThumbnailData = await existingThumbnailResponse.json();
489
+ const thumbnailFileId = existingThumbnailData.files?.[0]?.id;
490
+
491
+ const thumbnailUrl = thumbnailFileId
492
+ ? `https://www.googleapis.com/upload/drive/v3/files/${thumbnailFileId}?uploadType=multipart`
493
+ : "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart";
494
+
495
+ const thumbnailForm = new FormData();
496
+ thumbnailForm.append("metadata", new Blob([JSON.stringify(thumbnailMetadata)], { type: "application/json" }));
497
+ thumbnailForm.append("file", thumbnailBlob);
498
+
499
+ const thumbnailMethod = thumbnailFileId ? "PATCH" : "POST";
500
+
501
+ await fetch(thumbnailUrl, {
502
+ method: thumbnailMethod,
503
+ headers: {
504
+ Authorization: `Bearer ${accessToken}`,
505
+ },
506
+ body: thumbnailForm,
507
+ });
508
+ } catch (thumbnailError) {
509
+ console.warn("サムネイルの保存に失敗しました:", thumbnailError);
510
+ }
511
+
512
+ if (!fileId) {
513
+ await fetch(`https://www.googleapis.com/drive/v3/files/${fileData.id}/permissions`, {
514
+ method: "POST",
515
+ headers: {
516
+ Authorization: `Bearer ${accessToken}`,
517
+ "Content-Type": "application/json",
518
+ },
519
+ body: JSON.stringify({
520
+ role: "reader",
521
+ type: "anyone",
522
+ }),
523
+ });
524
+ }
525
+
526
+ showAlert(addon, "success", fileId ? "上書き保存しました" : "新規保存しました");
527
+ if (onSuccess) onSuccess();
528
+ } catch (error) {
529
+ console.error("保存エラー:", error);
530
+ showAlert(addon, "error", `保存に失敗しました: ${error.message}`);
531
+ throw error;
532
+ }
533
+ }
534
+
535
+ function showAlert(addon, type, message) {
536
+ addon.tab.redux.dispatch({
537
+ type: "scratch-gui/alerts/SHOW_ALERT",
538
+ payload: {
539
+ alertType: type,
540
+ message: message
541
+ }
542
+ });
543
+ }
544
+ };
src/lib/libraries/decks/steps/add-backdrop.LTR.png CHANGED

Git LFS Details

  • SHA256: bd091a159f1544c7f576c2c069ea1a8577bc38aa17eb05b83e1ed8d4fe6ced30
  • Pointer size: 130 Bytes
  • Size of remote file: 35.8 kB
src/lib/libraries/decks/steps/add-backdrop.RTL.png CHANGED

Git LFS Details

  • SHA256: f60290a073f132b5b05754909b1dba26e628b3f3b081efbc81fd8804459f1ff4
  • Pointer size: 130 Bytes
  • Size of remote file: 17.2 kB
src/lib/libraries/decks/steps/add-effects.am.png CHANGED

Git LFS Details

  • SHA256: 1275a4ec5c6aa9c3b659280fe67dd984cd8a45028307ad0fc56e170229e5e4fb
  • Pointer size: 130 Bytes
  • Size of remote file: 14.3 kB
src/lib/libraries/decks/steps/add-effects.ar.png CHANGED

Git LFS Details

  • SHA256: c7f6c3c80914626d3654b40272ca8628736026ac81cf11d213d32f3bba63d2cf
  • Pointer size: 130 Bytes
  • Size of remote file: 19.5 kB
src/lib/libraries/decks/steps/add-effects.en.png CHANGED

Git LFS Details

  • SHA256: b0eecec31ffb25ae790abe6528e263bc99874daa371e67190a86f79469f44c56
  • Pointer size: 130 Bytes
  • Size of remote file: 15 kB
src/lib/libraries/decks/steps/add-effects.es.png CHANGED

Git LFS Details

  • SHA256: 1e7e93389c530221606974a74c2c6f448c89d399fc7e2d3bf423f7d153071b93
  • Pointer size: 130 Bytes
  • Size of remote file: 91.3 kB
src/lib/libraries/decks/steps/add-effects.fr.png CHANGED

Git LFS Details

  • SHA256: 6071e9ce5a93a9643ad14eb2a86d8af08e5622662588c9fc8906428a0d8fcf77
  • Pointer size: 130 Bytes
  • Size of remote file: 14.4 kB
src/lib/libraries/decks/steps/add-effects.ja.png CHANGED

Git LFS Details

  • SHA256: 80edebc0fd7f770e7ca4d6e91ad12fa2f0416a427a08eac9d8dd28a53deda119
  • Pointer size: 130 Bytes
  • Size of remote file: 20.1 kB
src/lib/libraries/decks/steps/add-effects.pt_BR.png CHANGED

Git LFS Details

  • SHA256: 7a2960118f9937828c430351cc5ac27d9594f27ccc4893ca44df91dbf9166873
  • Pointer size: 130 Bytes
  • Size of remote file: 15.4 kB
src/lib/libraries/decks/steps/add-effects.sw.png CHANGED

Git LFS Details

  • SHA256: 6dff65a857bf370052a280014c46cb66ae63649ff7df82f76289b103d382f7a8
  • Pointer size: 130 Bytes
  • Size of remote file: 15.1 kB
src/lib/libraries/decks/steps/add-effects.tr.png CHANGED

Git LFS Details

  • SHA256: 55f69af54b7eb791d444fdab2c8153c5f39931d9cef81e2f080665178b564cd6
  • Pointer size: 130 Bytes
  • Size of remote file: 14.6 kB
src/lib/libraries/decks/steps/add-effects.uk.png CHANGED

Git LFS Details

  • SHA256: d042fc9c6f4190563c783b8b01a5f7a09b3c7ec2f94e3c002f2d06a8b8e0f9e9
  • Pointer size: 130 Bytes
  • Size of remote file: 15.1 kB
src/lib/libraries/decks/steps/add-effects.zh_CN.png CHANGED

Git LFS Details

  • SHA256: c03e2e68309c8c5485a92b87e562fd5cbf20617f73b229bf461f946458c6457b
  • Pointer size: 130 Bytes
  • Size of remote file: 20.5 kB
src/lib/libraries/decks/steps/add-effects.zh_TW.png CHANGED

Git LFS Details

  • SHA256: 47596142fefa1f64f9061c4d207fe9bdc5b0cb9105b8d309f56d518bd637e744
  • Pointer size: 130 Bytes
  • Size of remote file: 21.2 kB
src/lib/libraries/decks/steps/add-effects.zu.png CHANGED

Git LFS Details

  • SHA256: a5c4ef53f1b357d55a6146a2545cbafc229e61d6d0dc07665412965bfff78f46
  • Pointer size: 130 Bytes
  • Size of remote file: 14.7 kB
src/lib/libraries/decks/steps/animate-char-add-sound.am.png CHANGED

Git LFS Details

  • SHA256: 298103f2b7ba2f46a4cdd2b119bf6fab2e470e96f2911f1988cbe22cf40470e1
  • Pointer size: 130 Bytes
  • Size of remote file: 38.6 kB
src/lib/libraries/decks/steps/animate-char-add-sound.ar.png CHANGED

Git LFS Details

  • SHA256: 30dc11fd63464a44be0a2e157458f4858515b5afd7a455ec82d7f243cecfc305
  • Pointer size: 130 Bytes
  • Size of remote file: 44.1 kB
src/lib/libraries/decks/steps/animate-char-add-sound.en.png CHANGED

Git LFS Details

  • SHA256: d9dbb17d86a77916a7ffe499422b876a9728481cac5092c2e915c5c50fd98074
  • Pointer size: 130 Bytes
  • Size of remote file: 37 kB
src/lib/libraries/decks/steps/animate-char-add-sound.es.png CHANGED

Git LFS Details

  • SHA256: f16f3c3d12842ef03f5d97bd9dd147528a29bb17f5e5e4eee19a2218a11a589a
  • Pointer size: 130 Bytes
  • Size of remote file: 36.8 kB
src/lib/libraries/decks/steps/animate-char-add-sound.fr.png CHANGED

Git LFS Details

  • SHA256: 5ae66b239adc46294582d51f1fc801950079bcf103ed8c8d8e1d42fec673378a
  • Pointer size: 130 Bytes
  • Size of remote file: 38.9 kB
src/lib/libraries/decks/steps/animate-char-add-sound.ja.png CHANGED

Git LFS Details

  • SHA256: b5b5fd4ce1d1952c92433672f6cee5bdd22f740902e22bff59bb07a5d8a03669
  • Pointer size: 130 Bytes
  • Size of remote file: 44 kB
src/lib/libraries/decks/steps/animate-char-add-sound.pt_BR.png CHANGED

Git LFS Details

  • SHA256: be2a6333650eea00e4ad12d9c9132fa3e61299cdc6e8a6737db91baeac62f9ab
  • Pointer size: 130 Bytes
  • Size of remote file: 45 kB
src/lib/libraries/decks/steps/animate-char-add-sound.sw.png CHANGED

Git LFS Details

  • SHA256: a2e0a6a9085348994d2546588bc7b9d463757e032a036ec19f78639640f4f0e8
  • Pointer size: 130 Bytes
  • Size of remote file: 43.9 kB
src/lib/libraries/decks/steps/animate-char-add-sound.tr.png CHANGED

Git LFS Details

  • SHA256: ff7f8ad7d198704271e82a3a1414e3d3a97186d08d88684645eecb7516480ca6
  • Pointer size: 130 Bytes
  • Size of remote file: 37.4 kB
src/lib/libraries/decks/steps/animate-char-add-sound.uk.png CHANGED

Git LFS Details

  • SHA256: 34ee4359a5ea95f45e241d2d41d3b4dfa8af92935b65985ae547699b087aa8cc
  • Pointer size: 130 Bytes
  • Size of remote file: 34.8 kB
src/lib/libraries/decks/steps/animate-char-add-sound.zh_CN.png CHANGED

Git LFS Details

  • SHA256: 6bd5e241da03afc7526b9c36950e709eb82b0e86e9f0e491c41ba3df62c4a4d8
  • Pointer size: 130 Bytes
  • Size of remote file: 42.1 kB
src/lib/libraries/decks/steps/animate-char-add-sound.zh_TW.png CHANGED

Git LFS Details

  • SHA256: 53fd73e597784ecb38f45855f7c3563605b2622a5092eb38b2ea94096f167ac9
  • Pointer size: 130 Bytes
  • Size of remote file: 43.7 kB
src/lib/libraries/decks/steps/animate-char-add-sound.zu.png CHANGED

Git LFS Details

  • SHA256: 21a26f1aad36ac70b9e079d6f7d7a8c922889c944cec578397031f04ce91d3d2
  • Pointer size: 130 Bytes
  • Size of remote file: 36.1 kB
src/lib/libraries/decks/steps/animate-char-change-color.am.png CHANGED

Git LFS Details

  • SHA256: 996c5423501221ecd5a64138f7955683732a55f5d9b5ec4ebf947e216c8de10a
  • Pointer size: 130 Bytes
  • Size of remote file: 17.7 kB
src/lib/libraries/decks/steps/animate-char-change-color.ar.png CHANGED

Git LFS Details

  • SHA256: d76f9d768204175d34380340b188d730a014736da87749b29c26cb1496839787
  • Pointer size: 130 Bytes
  • Size of remote file: 19.7 kB
src/lib/libraries/decks/steps/animate-char-change-color.en.png CHANGED

Git LFS Details

  • SHA256: a8550039b809279c2d24e6caeb40142cbd3b256228203b723164a4304a8435a9
  • Pointer size: 130 Bytes
  • Size of remote file: 17.2 kB
src/lib/libraries/decks/steps/animate-char-change-color.es.png CHANGED

Git LFS Details

  • SHA256: f083bc6eeda9da6507b3665af1d6242329a72930b12b9271201a134396f1dc6b
  • Pointer size: 130 Bytes
  • Size of remote file: 18.7 kB
src/lib/libraries/decks/steps/animate-char-change-color.fr.png CHANGED

Git LFS Details

  • SHA256: ff5410322d06ade7984bc2d1aa0e7e94ba1d36c5b879c957fae79d4ed9895a0e
  • Pointer size: 130 Bytes
  • Size of remote file: 17.5 kB
src/lib/libraries/decks/steps/animate-char-change-color.ja.png CHANGED

Git LFS Details

  • SHA256: 8f8497c551b3ded2470d654519d83836a53344d30249d7c6e13d3e53e3698b34
  • Pointer size: 130 Bytes
  • Size of remote file: 22.8 kB
src/lib/libraries/decks/steps/animate-char-change-color.pt_BR.png CHANGED

Git LFS Details

  • SHA256: b799299baaaccc8b20a39cc184c5baf49ef04b31c7bf1259061b499eef8ecbbe
  • Pointer size: 130 Bytes
  • Size of remote file: 17.4 kB
src/lib/libraries/decks/steps/animate-char-change-color.sw.png CHANGED

Git LFS Details

  • SHA256: 4370cf1db29384b08d2e6c76ad15df8f78fa13a52a561fb770e8a5073c419b6f
  • Pointer size: 130 Bytes
  • Size of remote file: 17.7 kB
src/lib/libraries/decks/steps/animate-char-change-color.tr.png CHANGED

Git LFS Details

  • SHA256: 0e30ef3d1b561d5f963128934227c831b6665836eab0cf1ca2f63a6fdfc2dce0
  • Pointer size: 130 Bytes
  • Size of remote file: 17.2 kB
src/lib/libraries/decks/steps/animate-char-change-color.uk.png CHANGED

Git LFS Details

  • SHA256: 91ad360e72259be983946885fbf9760206898975be70a16a8943f5e2b281ca8d
  • Pointer size: 130 Bytes
  • Size of remote file: 17.2 kB
src/lib/libraries/decks/steps/animate-char-change-color.zh_CN.png CHANGED

Git LFS Details

  • SHA256: ec988ba43a818ef964ad00b4b488495c7706f288b6fb418cdafa2cb7cdd2346d
  • Pointer size: 130 Bytes
  • Size of remote file: 21.8 kB
src/lib/libraries/decks/steps/animate-char-change-color.zh_TW.png CHANGED

Git LFS Details

  • SHA256: ea649ae2bab8cd6528721f8b440894965bfacdda605adac7ca536affcb015013
  • Pointer size: 130 Bytes
  • Size of remote file: 22 kB
src/lib/libraries/decks/steps/animate-char-change-color.zu.png CHANGED

Git LFS Details

  • SHA256: c3b7c1170997c21c8b092338372c9f0f67f1e434c5b4d0e9ba4634da9aa135d5
  • Pointer size: 130 Bytes
  • Size of remote file: 17.7 kB
src/lib/libraries/decks/steps/animate-char-jump.am.png CHANGED

Git LFS Details

  • SHA256: 897e14170cf5e8ba26a7c2b8afd0ae1a39699e6df851a898d11c7f46335e2382
  • Pointer size: 130 Bytes
  • Size of remote file: 63.5 kB
src/lib/libraries/decks/steps/animate-char-jump.ar.png CHANGED

Git LFS Details

  • SHA256: 2b9be3316dd692330dd64c67a6f7d131ebe5ec76b3c4d2710f41bb46d7f70a8e
  • Pointer size: 130 Bytes
  • Size of remote file: 77.1 kB
src/lib/libraries/decks/steps/animate-char-jump.en.png CHANGED

Git LFS Details

  • SHA256: cd90953c27d4b17871987ef42af64beece3f4a9d05d42a2d02b13a7854b8cc41
  • Pointer size: 130 Bytes
  • Size of remote file: 62.6 kB
src/lib/libraries/decks/steps/animate-char-jump.es.png CHANGED

Git LFS Details

  • SHA256: d7c79237f8558fbb1d5dee988083d8d9fabda044e5e177b1920c7a5d6db99723
  • Pointer size: 130 Bytes
  • Size of remote file: 64 kB