Spaces:
Build error
Build error
Create google-drive-save.css
Browse files
src/components/menu-bar/google-drive-save.css
ADDED
@@ -0,0 +1,502 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// google-drive-save.jsx
|
2 |
+
import React from 'react';
|
3 |
+
import PropTypes from 'prop-types';
|
4 |
+
import classNames from 'classnames';
|
5 |
+
import {FormattedMessage} from 'react-intl';
|
6 |
+
import Button from '../button/button.jsx';
|
7 |
+
import styles from './google-drive-save.css';
|
8 |
+
|
9 |
+
class GoogleDriveSave extends React.Component {
|
10 |
+
constructor(props) {
|
11 |
+
super(props);
|
12 |
+
this.state = {
|
13 |
+
accessToken: localStorage.getItem('googleDriveAccessToken') || null,
|
14 |
+
currentAccountEmail: localStorage.getItem('googleDriveAccountEmail') || null,
|
15 |
+
currentAccountName: localStorage.getItem('googleDriveAccountName') || null,
|
16 |
+
files: [],
|
17 |
+
isModalOpen: false,
|
18 |
+
isLoading: false
|
19 |
+
};
|
20 |
+
}
|
21 |
+
|
22 |
+
componentDidMount() {
|
23 |
+
// 初期化処理
|
24 |
+
}
|
25 |
+
|
26 |
+
handleClick = () => {
|
27 |
+
this.setState({isModalOpen: true});
|
28 |
+
};
|
29 |
+
|
30 |
+
handleCloseModal = () => {
|
31 |
+
this.setState({isModalOpen: false});
|
32 |
+
};
|
33 |
+
|
34 |
+
startGoogleLogin = () => {
|
35 |
+
const CLIENT_ID = "1033286471224-n9mv8l869fqikubj2e8q92n8ige3qr6r.apps.googleusercontent.com";
|
36 |
+
const REDIRECT_URI = "https://soiz1-s4s-upload.hf.space/close2";
|
37 |
+
const SCOPES = "https://www.googleapis.com/auth/drive.file";
|
38 |
+
|
39 |
+
const messageListener = (event) => {
|
40 |
+
if (event.origin === "https://soiz1-penguin-upload.hf.space" && event.data.token) {
|
41 |
+
window.removeEventListener("message", messageListener);
|
42 |
+
this.setState({
|
43 |
+
accessToken: event.data.token,
|
44 |
+
currentAccountEmail: event.data.email || null,
|
45 |
+
currentAccountName: event.data.name || null,
|
46 |
+
isModalOpen: true
|
47 |
+
});
|
48 |
+
|
49 |
+
localStorage.setItem('googleDriveAccessToken', event.data.token);
|
50 |
+
if (event.data.email) {
|
51 |
+
localStorage.setItem('googleDriveAccountEmail', event.data.email);
|
52 |
+
}
|
53 |
+
if (event.data.name) {
|
54 |
+
localStorage.setItem('googleDriveAccountName', event.data.name);
|
55 |
+
}
|
56 |
+
|
57 |
+
this.fetchDriveFiles(event.data.token);
|
58 |
+
}
|
59 |
+
};
|
60 |
+
window.addEventListener("message", messageListener);
|
61 |
+
|
62 |
+
const authUrl = `https://accounts.google.com/o/oauth2/auth?` +
|
63 |
+
`client_id=${CLIENT_ID}` +
|
64 |
+
`&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` +
|
65 |
+
`&response_type=token` +
|
66 |
+
`&scope=${encodeURIComponent(SCOPES)}`;
|
67 |
+
|
68 |
+
window.open(authUrl, "_blank", "width=500,height=600");
|
69 |
+
};
|
70 |
+
|
71 |
+
fetchDriveFiles = async (accessToken) => {
|
72 |
+
this.setState({isLoading: true});
|
73 |
+
try {
|
74 |
+
const response = await fetch("https://www.googleapis.com/drive/v3/files?q=(mimeType='application/x-scratch' or mimeType='image/png')", {
|
75 |
+
headers: {
|
76 |
+
Authorization: `Bearer ${accessToken}`,
|
77 |
+
},
|
78 |
+
});
|
79 |
+
|
80 |
+
if (!response.ok) {
|
81 |
+
throw new Error(await response.text());
|
82 |
+
}
|
83 |
+
|
84 |
+
const data = await response.json();
|
85 |
+
this.setState({files: data.files || [], isLoading: false});
|
86 |
+
} catch (error) {
|
87 |
+
console.error("ファイル一覧取得エラー:", error);
|
88 |
+
this.props.showAlert("error", "ファイル一覧の取得に失敗しました");
|
89 |
+
this.setState({isLoading: false});
|
90 |
+
}
|
91 |
+
};
|
92 |
+
|
93 |
+
renderModal() {
|
94 |
+
if (!this.state.isModalOpen) return null;
|
95 |
+
|
96 |
+
return (
|
97 |
+
<div className={styles.modalOverlay}>
|
98 |
+
<div className={styles.modalContent}>
|
99 |
+
<div className={styles.modalHeader}>
|
100 |
+
<h2>Googleドライブに保存</h2>
|
101 |
+
<button onClick={this.handleCloseModal} className={styles.closeButton}>×</button>
|
102 |
+
</div>
|
103 |
+
|
104 |
+
<div className={styles.modalBody}>
|
105 |
+
{this.renderAuthSection()}
|
106 |
+
{this.state.accessToken && this.renderFileList()}
|
107 |
+
</div>
|
108 |
+
</div>
|
109 |
+
</div>
|
110 |
+
);
|
111 |
+
}
|
112 |
+
|
113 |
+
renderAuthSection() {
|
114 |
+
if (this.state.accessToken) {
|
115 |
+
return (
|
116 |
+
<div className={styles.authSection}>
|
117 |
+
<div className={styles.accountInfo}>
|
118 |
+
ログイン中: {this.state.currentAccountName || this.state.currentAccountEmail || 'Googleアカウント'}
|
119 |
+
</div>
|
120 |
+
<button
|
121 |
+
onClick={this.handleChangeAccount}
|
122 |
+
className={styles.changeAccountButton}
|
123 |
+
>
|
124 |
+
アカウントを変更
|
125 |
+
</button>
|
126 |
+
</div>
|
127 |
+
);
|
128 |
+
}
|
129 |
+
|
130 |
+
return (
|
131 |
+
<div className={styles.authSection}>
|
132 |
+
<p>Google��ログインして、プロジェクトを保存または更新します。</p>
|
133 |
+
<button
|
134 |
+
onClick={this.startGoogleLogin}
|
135 |
+
className={styles.loginButton}
|
136 |
+
>
|
137 |
+
Googleでログイン
|
138 |
+
</button>
|
139 |
+
</div>
|
140 |
+
);
|
141 |
+
}
|
142 |
+
|
143 |
+
renderFileList() {
|
144 |
+
if (this.state.isLoading) {
|
145 |
+
return <div className={styles.loading}>読み込み中...</div>;
|
146 |
+
}
|
147 |
+
|
148 |
+
// プロジェクトファイルとサムネイルを関連付ける
|
149 |
+
const projectFiles = this.state.files.filter(file => file.mimeType === 'application/x-scratch');
|
150 |
+
const thumbnailFiles = this.state.files.filter(file => file.mimeType === 'image/png');
|
151 |
+
|
152 |
+
if (projectFiles.length === 0) {
|
153 |
+
return <div className={styles.noFiles}>保存されたファイルが見つかりません</div>;
|
154 |
+
}
|
155 |
+
|
156 |
+
return (
|
157 |
+
<div className={styles.fileListContainer}>
|
158 |
+
<div className={styles.fileListHeader}>
|
159 |
+
<h3>プロジェクト: {this.renderProjectTitle()}</h3>
|
160 |
+
<button
|
161 |
+
onClick={this.handleNewFile}
|
162 |
+
className={styles.newFileButton}
|
163 |
+
>
|
164 |
+
新規保存
|
165 |
+
</button>
|
166 |
+
</div>
|
167 |
+
|
168 |
+
<div className={styles.fileList}>
|
169 |
+
{projectFiles.map(project => this.renderFileItem(project, thumbnailFiles))}
|
170 |
+
</div>
|
171 |
+
</div>
|
172 |
+
);
|
173 |
+
}
|
174 |
+
|
175 |
+
renderProjectTitle() {
|
176 |
+
const projectName = window.vm.runtime.projectName || "無題";
|
177 |
+
|
178 |
+
return (
|
179 |
+
<span
|
180 |
+
className={styles.projectTitle}
|
181 |
+
onDoubleClick={this.handleEditProjectTitle}
|
182 |
+
>
|
183 |
+
{projectName}
|
184 |
+
</span>
|
185 |
+
);
|
186 |
+
}
|
187 |
+
|
188 |
+
renderFileItem(project, thumbnailFiles) {
|
189 |
+
const thumbnail = thumbnailFiles.find(
|
190 |
+
thumb => thumb.name === `Penguin-Thumbnail-${project.id}.png`
|
191 |
+
);
|
192 |
+
|
193 |
+
return (
|
194 |
+
<div key={project.id} className={styles.fileItem}>
|
195 |
+
<div className={styles.thumbnailContainer}>
|
196 |
+
{thumbnail ? (
|
197 |
+
<img
|
198 |
+
src={`https://drive.google.com/thumbnail?id=${thumbnail.id}&sz=w300`}
|
199 |
+
alt="プロジェクトサムネイル"
|
200 |
+
className={styles.thumbnail}
|
201 |
+
/>
|
202 |
+
) : (
|
203 |
+
<div className={styles.thumbnailPlaceholder}>
|
204 |
+
サムネイルなし
|
205 |
+
</div>
|
206 |
+
)}
|
207 |
+
</div>
|
208 |
+
|
209 |
+
<h3 className={styles.fileName}>
|
210 |
+
{project.name.replace('.s4s.txt', '')}
|
211 |
+
</h3>
|
212 |
+
|
213 |
+
{this.renderShareLink(project.id)}
|
214 |
+
|
215 |
+
<div className={styles.buttonGroup}>
|
216 |
+
<button
|
217 |
+
onClick={() => this.handleLoadFile(project)}
|
218 |
+
className={styles.actionButton}
|
219 |
+
>
|
220 |
+
読み込む
|
221 |
+
</button>
|
222 |
+
<button
|
223 |
+
onClick={() => this.handleReplaceFile(project)}
|
224 |
+
className={styles.actionButton}
|
225 |
+
>
|
226 |
+
上書き
|
227 |
+
</button>
|
228 |
+
<button
|
229 |
+
onClick={() => this.handleShareFile(project.id)}
|
230 |
+
className={classNames(styles.actionButton, styles.shareButton)}
|
231 |
+
>
|
232 |
+
共有
|
233 |
+
</button>
|
234 |
+
<button
|
235 |
+
onClick={() => this.handleDeleteFile(project, thumbnailFiles)}
|
236 |
+
className={classNames(styles.actionButton, styles.deleteButton)}
|
237 |
+
>
|
238 |
+
削除
|
239 |
+
</button>
|
240 |
+
</div>
|
241 |
+
</div>
|
242 |
+
);
|
243 |
+
}
|
244 |
+
|
245 |
+
renderShareLink(fileId) {
|
246 |
+
const SHORT_URL = "https://s4.rf.gd/";
|
247 |
+
|
248 |
+
return (
|
249 |
+
<div className={styles.linkContainer}>
|
250 |
+
<div className={styles.linkHeader}>
|
251 |
+
<span>共有リンク:</span>
|
252 |
+
<button
|
253 |
+
onClick={() => this.copyToClipboard(`${SHORT_URL}${fileId}`)}
|
254 |
+
className={styles.copyButton}
|
255 |
+
>
|
256 |
+
コピー
|
257 |
+
</button>
|
258 |
+
</div>
|
259 |
+
<a
|
260 |
+
href={`${SHORT_URL}${fileId}`}
|
261 |
+
target="_blank"
|
262 |
+
rel="noopener noreferrer"
|
263 |
+
className={styles.linkUrl}
|
264 |
+
>
|
265 |
+
{`${SHORT_URL}${fileId}`}
|
266 |
+
</a>
|
267 |
+
</div>
|
268 |
+
);
|
269 |
+
}
|
270 |
+
|
271 |
+
render() {
|
272 |
+
return (
|
273 |
+
<div>
|
274 |
+
<Button
|
275 |
+
className={classNames(
|
276 |
+
this.props.className,
|
277 |
+
styles.saveButton
|
278 |
+
)}
|
279 |
+
onClick={this.handleClick}
|
280 |
+
>
|
281 |
+
<FormattedMessage
|
282 |
+
defaultMessage="Googleドライブに保存"
|
283 |
+
description="Label for Google Drive save button"
|
284 |
+
id="google.drive.saveButton"
|
285 |
+
/>
|
286 |
+
</Button>
|
287 |
+
|
288 |
+
{this.renderModal()}
|
289 |
+
</div>
|
290 |
+
);
|
291 |
+
}
|
292 |
+
|
293 |
+
// イベントハンドラメソッド
|
294 |
+
handleChangeAccount = () => {
|
295 |
+
this.setState({
|
296 |
+
accessToken: null,
|
297 |
+
currentAccountEmail: null,
|
298 |
+
currentAccountName: null
|
299 |
+
});
|
300 |
+
localStorage.removeItem('googleDriveAccessToken');
|
301 |
+
localStorage.removeItem('googleDriveAccountEmail');
|
302 |
+
localStorage.removeItem('googleDriveAccountName');
|
303 |
+
};
|
304 |
+
|
305 |
+
handleEditProjectTitle = () => {
|
306 |
+
const currentName = window.vm.runtime.projectName || "無題";
|
307 |
+
const newName = prompt("プロジェクト名を入力してください", currentName);
|
308 |
+
|
309 |
+
if (newName !== null) {
|
310 |
+
window.vm.runtime.projectName = newName.trim() || "無題";
|
311 |
+
this.forceUpdate();
|
312 |
+
}
|
313 |
+
};
|
314 |
+
|
315 |
+
handleNewFile = async () => {
|
316 |
+
try {
|
317 |
+
await this.saveToGoogleDrive(null, null);
|
318 |
+
this.props.showAlert("success", "新規保存しました");
|
319 |
+
} catch (error) {
|
320 |
+
console.error("新規保存エラー:", error);
|
321 |
+
this.props.showAlert("error", "新規保存に失敗しました");
|
322 |
+
}
|
323 |
+
};
|
324 |
+
|
325 |
+
handleLoadFile = (project) => {
|
326 |
+
const PROXY_URL = "https://soiz1-drive-proxy.hf.space/?file_id=";
|
327 |
+
|
328 |
+
if (confirm(`"${project.name}"を読み込みますか?現在のプロジェクトは失われます。`)) {
|
329 |
+
const url = `${PROXY_URL}${project.id}`;
|
330 |
+
window.location.href = `?project_url=${encodeURIComponent(url)}`;
|
331 |
+
}
|
332 |
+
};
|
333 |
+
|
334 |
+
handleReplaceFile = async (project) => {
|
335 |
+
if (confirm(`"${project.name}"を現在のプロジェクトで上書きしますか?`)) {
|
336 |
+
try {
|
337 |
+
await this.saveToGoogleDrive(project.id, project.name);
|
338 |
+
this.props.showAlert("success", "上書き保存しました");
|
339 |
+
this.fetchDriveFiles(this.state.accessToken);
|
340 |
+
} catch (error) {
|
341 |
+
console.error("ファイル上書きエラー:", error);
|
342 |
+
this.props.showAlert("error", "ファイルの上書きに失敗しました");
|
343 |
+
}
|
344 |
+
}
|
345 |
+
};
|
346 |
+
|
347 |
+
handleShareFile = (fileId) => {
|
348 |
+
const SHARE_URL = "https://scratch-school.ct.ws/upload?id=";
|
349 |
+
window.open(`${SHARE_URL}${fileId}`, "_blank");
|
350 |
+
};
|
351 |
+
|
352 |
+
handleDeleteFile = async (project, thumbnailFiles) => {
|
353 |
+
if (confirm(`"${project.name}"とそのサムネイルを完全に削除しますか?この操作は元に戻せません。`)) {
|
354 |
+
try {
|
355 |
+
// プロジェクトファイルを削除
|
356 |
+
await this.deleteFile(project.id);
|
357 |
+
|
358 |
+
// 対応するサムネイルを探して削除
|
359 |
+
const thumbnailToDelete = thumbnailFiles.find(
|
360 |
+
thumb => thumb.name === `Penguin-Thumbnail-${project.id}.png`
|
361 |
+
);
|
362 |
+
|
363 |
+
if (thumbnailToDelete) {
|
364 |
+
await this.deleteFile(thumbnailToDelete.id);
|
365 |
+
}
|
366 |
+
|
367 |
+
this.props.showAlert("success", "ファイルを削除しました");
|
368 |
+
this.fetchDriveFiles(this.state.accessToken);
|
369 |
+
} catch (error) {
|
370 |
+
console.error("削除エラー:", error);
|
371 |
+
this.props.showAlert("error", "ファイルの削除に失敗しました");
|
372 |
+
}
|
373 |
+
}
|
374 |
+
};
|
375 |
+
|
376 |
+
copyToClipboard = (text) => {
|
377 |
+
navigator.clipboard.writeText(text)
|
378 |
+
.then(() => this.props.showAlert("success", "リンクをクリップボードにコピーしました"))
|
379 |
+
.catch(() => this.props.showAlert("error", "リンクのコピーに失敗しました"));
|
380 |
+
};
|
381 |
+
|
382 |
+
// API操作メソッド
|
383 |
+
async deleteFile(fileId) {
|
384 |
+
const response = await fetch(`https://www.googleapis.com/drive/v3/files/${fileId}`, {
|
385 |
+
method: "DELETE",
|
386 |
+
headers: {
|
387 |
+
Authorization: `Bearer ${this.state.accessToken}`,
|
388 |
+
},
|
389 |
+
});
|
390 |
+
|
391 |
+
if (!response.ok) {
|
392 |
+
throw new Error(await response.text());
|
393 |
+
}
|
394 |
+
}
|
395 |
+
|
396 |
+
async saveToGoogleDrive(fileId, fileName) {
|
397 |
+
// プロジェクトを保存
|
398 |
+
const blob = await window.vm.saveProjectSb3();
|
399 |
+
const projectName = window.vm.runtime.projectName || "無題";
|
400 |
+
const nameToUse = fileName || `${projectName}.s4s.txt`;
|
401 |
+
|
402 |
+
const metadata = {
|
403 |
+
name: nameToUse,
|
404 |
+
mimeType: "application/x-scratch",
|
405 |
+
};
|
406 |
+
|
407 |
+
const url = fileId
|
408 |
+
? `https://www.googleapis.com/upload/drive/v3/files/${fileId}?uploadType=multipart`
|
409 |
+
: "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart";
|
410 |
+
|
411 |
+
const form = new FormData();
|
412 |
+
form.append("metadata", new Blob([JSON.stringify(metadata)], { type: "application/json" }));
|
413 |
+
form.append("file", blob);
|
414 |
+
|
415 |
+
const method = fileId ? "PATCH" : "POST";
|
416 |
+
|
417 |
+
const uploadResponse = await fetch(url, {
|
418 |
+
method,
|
419 |
+
headers: {
|
420 |
+
Authorization: `Bearer ${this.state.accessToken}`,
|
421 |
+
},
|
422 |
+
body: form,
|
423 |
+
});
|
424 |
+
|
425 |
+
if (!uploadResponse.ok) {
|
426 |
+
throw new Error(await uploadResponse.text());
|
427 |
+
}
|
428 |
+
|
429 |
+
const fileData = await uploadResponse.json();
|
430 |
+
|
431 |
+
// サムネイルを保存
|
432 |
+
try {
|
433 |
+
const thumbnailDataUrl = await this.getProjectThumbnail();
|
434 |
+
const thumbnailBlob = await (await fetch(thumbnailDataUrl)).blob();
|
435 |
+
const thumbnailMetadata = {
|
436 |
+
name: `Penguin-Thumbnail-${fileData.id}.png`,
|
437 |
+
mimeType: "image/png",
|
438 |
+
};
|
439 |
+
|
440 |
+
const existingThumbnailResponse = await fetch(
|
441 |
+
`https://www.googleapis.com/drive/v3/files?q=name='${thumbnailMetadata.name}'`,
|
442 |
+
{
|
443 |
+
headers: {
|
444 |
+
Authorization: `Bearer ${this.state.accessToken}`,
|
445 |
+
},
|
446 |
+
}
|
447 |
+
);
|
448 |
+
|
449 |
+
const existingThumbnailData = await existingThumbnailResponse.json();
|
450 |
+
const thumbnailFileId = existingThumbnailData.files?.[0]?.id;
|
451 |
+
|
452 |
+
const thumbnailUrl = thumbnailFileId
|
453 |
+
? `https://www.googleapis.com/upload/drive/v3/files/${thumbnailFileId}?uploadType=multipart`
|
454 |
+
: "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart";
|
455 |
+
|
456 |
+
const thumbnailForm = new FormData();
|
457 |
+
thumbnailForm.append("metadata", new Blob([JSON.stringify(thumbnailMetadata)], { type: "application/json" }));
|
458 |
+
thumbnailForm.append("file", thumbnailBlob);
|
459 |
+
|
460 |
+
const thumbnailMethod = thumbnailFileId ? "PATCH" : "POST";
|
461 |
+
|
462 |
+
await fetch(thumbnailUrl, {
|
463 |
+
method: thumbnailMethod,
|
464 |
+
headers: {
|
465 |
+
Authorization: `Bearer ${this.state.accessToken}`,
|
466 |
+
},
|
467 |
+
body: thumbnailForm,
|
468 |
+
});
|
469 |
+
} catch (thumbnailError) {
|
470 |
+
console.warn("サムネイルの保存に失敗しました:", thumbnailError);
|
471 |
+
}
|
472 |
+
|
473 |
+
if (!fileId) {
|
474 |
+
await fetch(`https://www.googleapis.com/drive/v3/files/${fileData.id}/permissions`, {
|
475 |
+
method: "POST",
|
476 |
+
headers: {
|
477 |
+
Authorization: `Bearer ${this.state.accessToken}`,
|
478 |
+
"Content-Type": "application/json",
|
479 |
+
},
|
480 |
+
body: JSON.stringify({
|
481 |
+
role: "reader",
|
482 |
+
type: "anyone",
|
483 |
+
}),
|
484 |
+
});
|
485 |
+
}
|
486 |
+
}
|
487 |
+
|
488 |
+
getProjectThumbnail() {
|
489 |
+
return new Promise(resolve => {
|
490 |
+
window.vm.renderer.requestSnapshot(uri => {
|
491 |
+
resolve(uri);
|
492 |
+
});
|
493 |
+
});
|
494 |
+
}
|
495 |
+
}
|
496 |
+
|
497 |
+
GoogleDriveSave.propTypes = {
|
498 |
+
className: PropTypes.string,
|
499 |
+
showAlert: PropTypes.func.isRequired
|
500 |
+
};
|
501 |
+
|
502 |
+
export default GoogleDriveSave;
|