soiz1 commited on
Commit
4bf29aa
·
verified ·
1 Parent(s): ed0c7fb

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;