soiz1 commited on
Commit
465064d
·
verified ·
1 Parent(s): 2b07202

Update src/components/menu-bar/google-drive-save.jsx

Browse files
src/components/menu-bar/google-drive-save.jsx CHANGED
@@ -20,14 +20,24 @@ class GoogleDriveSave extends React.Component {
20
  isProcessing: false,
21
  newFileName: props.projectTitle || '無題',
22
  showNewFileInput: false,
23
- sharePermission: 'reader', // 'reader', 'writer', or 'owner'
24
- selectedFileId: null
 
 
 
 
 
 
25
  };
26
  this.modalContentRef = React.createRef();
 
27
  }
28
 
29
  componentDidMount() {
30
- // 初期化処理
 
 
 
31
  }
32
 
33
  handleClick = () => {
@@ -36,7 +46,11 @@ class GoogleDriveSave extends React.Component {
36
 
37
  handleCloseModal = () => {
38
  if (!this.state.isProcessing) {
39
- this.setState({isModalOpen: false, showNewFileInput: false});
 
 
 
 
40
  }
41
  };
42
 
@@ -67,6 +81,10 @@ class GoogleDriveSave extends React.Component {
67
  isModalOpen: true
68
  });
69
 
 
 
 
 
70
  this.fetchDriveFiles(event.data.token);
71
  }
72
  };
@@ -98,11 +116,19 @@ class GoogleDriveSave extends React.Component {
98
  this.setState({files: data.files || [], isLoading: false});
99
  } catch (error) {
100
  console.error("ファイル一覧取得エラー:", error);
101
- alert("error", "ファイル一覧の取得に失敗しました");
102
  this.setState({isLoading: false});
103
  }
104
  };
105
 
 
 
 
 
 
 
 
 
106
  renderModal() {
107
  if (!this.state.isModalOpen) return null;
108
 
@@ -218,7 +244,7 @@ class GoogleDriveSave extends React.Component {
218
  <button
219
  onClick={() => this.setState({
220
  showNewFileInput: true,
221
- newFileName: window.vm.runtime.projectName || '無題',
222
  sharePermission: 'reader'
223
  })}
224
  className={styles.newFileButton}
@@ -255,102 +281,67 @@ class GoogleDriveSave extends React.Component {
255
  );
256
  }
257
 
258
- renderFileItem(project, thumbnailFiles) {
259
- const thumbnail = thumbnailFiles.find(
260
- thumb => thumb.name === `Scratch-Thumbnail-${project.id}.png`
261
- );
262
-
263
- return (
264
- <div key={project.id} className={styles.fileItem}>
265
- <div className={styles.thumbnailContainer}>
266
- {thumbnail ? (
267
- <img
268
- src={`https://drive.google.com/thumbnail?id=${thumbnail.id}&sz=w300`}
269
- alt="プロジェクトサムネイル"
270
- className={styles.thumbnail}
271
- />
272
- ) : (
273
- <div className={styles.thumbnailPlaceholder}>
274
- サムネイルなし
275
- </div>
276
- )}
277
- </div>
278
-
279
- <h3 className={styles.fileName}>
280
- {project.name.replace('.s4s.txt', '')}
281
- </h3>
282
-
283
- {this.renderShareLink(project.id)}
284
-
285
- <div className={styles.buttonGroup}>
286
- <button
287
- onClick={() => this.handleLoadFile(project)}
288
- className={styles.actionButton}
289
- disabled={this.state.isProcessing}
290
- >
291
- 読み込む
292
- </button>
293
- <button
294
- onClick={() => this.handleReplaceFile(project)}
295
- className={styles.actionButton}
296
- disabled={this.state.isProcessing}
297
- >
298
- 上書き
299
- </button>
300
- <button
301
- onClick={() => this.handleShareFile(project.id)}
302
- className={classNames(styles.actionButton, styles.shareButton)}
303
- disabled={this.state.isProcessing}
304
- >
305
- 共有
306
- </button>
307
- <button
308
- onClick={() => this.handleDeleteFile(project, thumbnailFiles)}
309
- className={classNames(styles.actionButton, styles.deleteButton)}
310
- disabled={this.state.isProcessing}
311
- >
312
- 削除
313
- </button>
314
- </div>
315
 
316
- {/* ここにアクセス権限設定のドロップダウンを追加 */}
317
- <div className={styles.permissionDropdown}>
318
- <label>アクセス権限: </label>
319
- <select
320
- value={this.state.sharePermission}
321
- onChange={(e) => this.updateFilePermission(project.id, e.target.value)}
322
- disabled={this.state.isProcessing}
323
- >
324
- <option value="reader">閲覧のみ</option>
325
- <option value="writer">編集可能</option>
326
- <option value="owner">所有者</option>
327
- </select>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
  </div>
329
- </div>
330
- );
331
- }
332
-
333
- updateFilePermission = async (fileId, permission) => {
334
- this.setState({isProcessing: true});
335
- try {
336
- await fetch(`https://www.googleapis.com/drive/v3/files/${fileId}/permissions/anyone`, {
337
- method: "PATCH",
338
- headers: {
339
- Authorization: `Bearer ${this.state.accessToken}`,
340
- "Content-Type": "application/json",
341
- },
342
- body: JSON.stringify({
343
- role: permission,
344
- }),
345
- });
346
- alert("success", "アクセス権限を更新しました");
347
- } catch (error) {
348
- console.error("権限更新エラー:", error);
349
- alert("error", "アクセス権限の更新に失敗しました");
350
- } finally {
351
- this.setState({isProcessing: false});
352
  }
353
- };
354
  renderShareLink(fileId) {
355
  const SHORT_URL = "https://s4.rf.gd/";
356
 
@@ -391,6 +382,239 @@ updateFilePermission = async (fileId, permission) => {
391
  );
392
  }
393
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  render() {
395
  return (
396
  <div>
@@ -409,6 +633,7 @@ updateFilePermission = async (fileId, permission) => {
409
  </Button>
410
 
411
  {this.renderModal()}
 
412
  </div>
413
  );
414
  }
@@ -419,7 +644,8 @@ updateFilePermission = async (fileId, permission) => {
419
  this.setState({
420
  accessToken: null,
421
  currentAccountEmail: null,
422
- currentAccountName: null
 
423
  });
424
  localStorage.removeItem('googleDriveAccessToken');
425
  localStorage.removeItem('googleDriveAccountEmail');
@@ -430,12 +656,12 @@ updateFilePermission = async (fileId, permission) => {
430
  this.setState({isProcessing: true});
431
  try {
432
  await this.saveToGoogleDrive(null, `${this.state.newFileName}.s4s.txt`, this.state.sharePermission);
433
- alert("success", "新規保存しました");
434
  this.setState({showNewFileInput: false});
435
  this.fetchDriveFiles(this.state.accessToken);
436
  } catch (error) {
437
  console.error("新規保存エラー:", error);
438
- alert("error", "新規保存に失敗しました");
439
  } finally {
440
  this.setState({isProcessing: false});
441
  }
@@ -459,11 +685,11 @@ updateFilePermission = async (fileId, permission) => {
459
  this.setState({isProcessing: true});
460
  try {
461
  await this.saveToGoogleDrive(project.id, project.name);
462
- alert("success", "上書き保存しました");
463
  this.fetchDriveFiles(this.state.accessToken);
464
  } catch (error) {
465
  console.error("ファイル上書きエラー:", error);
466
- alert("error", "ファイルの上書きに失敗しました");
467
  } finally {
468
  this.setState({isProcessing: false});
469
  }
@@ -493,11 +719,11 @@ updateFilePermission = async (fileId, permission) => {
493
  await this.deleteFile(thumbnailToDelete.id);
494
  }
495
 
496
- alert("success", "ファイルを削除しました");
497
  this.fetchDriveFiles(this.state.accessToken);
498
  } catch (error) {
499
  console.error("削除エラー:", error);
500
- alert("error", "ファイルの削除に失敗しました");
501
  } finally {
502
  this.setState({isProcessing: false});
503
  }
@@ -508,8 +734,8 @@ updateFilePermission = async (fileId, permission) => {
508
  if (this.state.isProcessing) return;
509
 
510
  navigator.clipboard.writeText(text)
511
- .then(() => alert("success", "リンクをクリップボードにコピーしました"))
512
- .catch(() => alert("error", "リンクのコピーに失敗しました"));
513
  };
514
 
515
  async deleteFile(fileId) {
@@ -526,6 +752,10 @@ updateFilePermission = async (fileId, permission) => {
526
  }
527
 
528
  async saveToGoogleDrive(fileId, fileName, permission = 'reader') {
 
 
 
 
529
  const blob = await window.vm.saveProjectSb3();
530
 
531
  const metadata = {
@@ -606,18 +836,25 @@ updateFilePermission = async (fileId, permission) => {
606
  "Content-Type": "application/json",
607
  },
608
  body: JSON.stringify({
609
- role: permission, // ここで公開設定を使用
610
  type: "anyone",
611
  }),
612
  });
613
  }
 
 
614
  }
615
 
616
  getProjectThumbnail() {
617
- return new Promise(resolve => {
618
- window.vm.renderer.requestSnapshot(uri => {
619
- resolve(uri);
620
- });
 
 
 
 
 
621
  });
622
  }
623
  }
@@ -627,7 +864,9 @@ GoogleDriveSave.propTypes = {
627
  showAlert: PropTypes.func.isRequired,
628
  projectTitle: PropTypes.string
629
  };
 
630
  const mapStateToProps = state => ({
631
  projectTitle: state.scratchGui.projectTitle
632
  });
 
633
  export default connect(mapStateToProps)(GoogleDriveSave);
 
20
  isProcessing: false,
21
  newFileName: props.projectTitle || '無題',
22
  showNewFileInput: false,
23
+ sharePermission: 'reader',
24
+ selectedFileId: null,
25
+ // 共有モーダル用の状態
26
+ isShareModalOpen: false,
27
+ currentSharingFileId: null,
28
+ emailPermissions: [{ email: '', role: 'reader' }],
29
+ linkPermission: 'reader',
30
+ groupPermission: 'reader'
31
  };
32
  this.modalContentRef = React.createRef();
33
+ this.shareModalContentRef = React.createRef();
34
  }
35
 
36
  componentDidMount() {
37
+ // アクセストークンがある場合はファイル一覧を取得
38
+ if (this.state.accessToken) {
39
+ this.fetchDriveFiles(this.state.accessToken);
40
+ }
41
  }
42
 
43
  handleClick = () => {
 
46
 
47
  handleCloseModal = () => {
48
  if (!this.state.isProcessing) {
49
+ this.setState({
50
+ isModalOpen: false,
51
+ showNewFileInput: false,
52
+ isShareModalOpen: false
53
+ });
54
  }
55
  };
56
 
 
81
  isModalOpen: true
82
  });
83
 
84
+ localStorage.setItem('googleDriveAccessToken', event.data.token);
85
+ if (event.data.email) localStorage.setItem('googleDriveAccountEmail', event.data.email);
86
+ if (event.data.name) localStorage.setItem('googleDriveAccountName', event.data.name);
87
+
88
  this.fetchDriveFiles(event.data.token);
89
  }
90
  };
 
116
  this.setState({files: data.files || [], isLoading: false});
117
  } catch (error) {
118
  console.error("ファイル一覧取得エラー:", error);
119
+ this.showAlert("error", "ファイル一覧の取得に失敗しました");
120
  this.setState({isLoading: false});
121
  }
122
  };
123
 
124
+ showAlert = (type, message) => {
125
+ if (this.props.showAlert) {
126
+ this.props.showAlert(type, message);
127
+ } else {
128
+ alert(`${type}: ${message}`);
129
+ }
130
+ };
131
+
132
  renderModal() {
133
  if (!this.state.isModalOpen) return null;
134
 
 
244
  <button
245
  onClick={() => this.setState({
246
  showNewFileInput: true,
247
+ newFileName: window.vm && window.vm.runtime ? window.vm.runtime.projectName || '無題' : '無題',
248
  sharePermission: 'reader'
249
  })}
250
  className={styles.newFileButton}
 
281
  );
282
  }
283
 
284
+ renderFileItem(project, thumbnailFiles) {
285
+ const thumbnail = thumbnailFiles.find(
286
+ thumb => thumb.name === `Scratch-Thumbnail-${project.id}.png`
287
+ );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
 
289
+ return (
290
+ <div key={project.id} className={styles.fileItem}>
291
+ <div className={styles.thumbnailContainer}>
292
+ {thumbnail ? (
293
+ <img
294
+ src={`https://drive.google.com/thumbnail?id=${thumbnail.id}&sz=w300`}
295
+ alt="プロジェクトサムネイル"
296
+ className={styles.thumbnail}
297
+ />
298
+ ) : (
299
+ <div className={styles.thumbnailPlaceholder}>
300
+ サムネイルなし
301
+ </div>
302
+ )}
303
+ </div>
304
+
305
+ <h3 className={styles.fileName}>
306
+ {project.name.replace('.s4s.txt', '')}
307
+ </h3>
308
+
309
+ {this.renderShareLink(project.id)}
310
+
311
+ <div className={styles.buttonGroup}>
312
+ <button
313
+ onClick={() => this.handleLoadFile(project)}
314
+ className={styles.actionButton}
315
+ disabled={this.state.isProcessing}
316
+ >
317
+ 読み込む
318
+ </button>
319
+ <button
320
+ onClick={() => this.handleReplaceFile(project)}
321
+ className={styles.actionButton}
322
+ disabled={this.state.isProcessing}
323
+ >
324
+ 上書き
325
+ </button>
326
+ <button
327
+ onClick={() => this.openShareModal(project.id)}
328
+ className={classNames(styles.actionButton, styles.shareButton)}
329
+ disabled={this.state.isProcessing}
330
+ >
331
+ 共有
332
+ </button>
333
+ <button
334
+ onClick={() => this.handleDeleteFile(project, thumbnailFiles)}
335
+ className={classNames(styles.actionButton, styles.deleteButton)}
336
+ disabled={this.state.isProcessing}
337
+ >
338
+ 削除
339
+ </button>
340
+ </div>
341
  </div>
342
+ );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  }
344
+
345
  renderShareLink(fileId) {
346
  const SHORT_URL = "https://s4.rf.gd/";
347
 
 
382
  );
383
  }
384
 
385
+ // 共有モーダルを開く
386
+ openShareModal = (fileId) => {
387
+ this.setState({
388
+ isShareModalOpen: true,
389
+ currentSharingFileId: fileId,
390
+ emailPermissions: [{ email: '', role: 'reader' }],
391
+ linkPermission: 'reader',
392
+ groupPermission: 'reader'
393
+ });
394
+ };
395
+
396
+ // 共有モーダルを閉じる
397
+ closeShareModal = () => {
398
+ if (!this.state.isProcessing) {
399
+ this.setState({ isShareModalOpen: false });
400
+ }
401
+ };
402
+
403
+ // オーバーレイクリックで共有モーダルを閉じる
404
+ handleShareOverlayClick = (e) => {
405
+ if (!this.state.isProcessing && this.shareModalContentRef.current &&
406
+ !this.shareModalContentRef.current.contains(e.target)) {
407
+ this.closeShareModal();
408
+ }
409
+ };
410
+
411
+ // メール権限入力欄を追加
412
+ addEmailPermission = () => {
413
+ this.setState(prevState => ({
414
+ emailPermissions: [...prevState.emailPermissions, { email: '', role: 'reader' }]
415
+ }));
416
+ };
417
+
418
+ // メール権限入力欄を更新
419
+ updateEmailPermission = (index, field, value) => {
420
+ this.setState(prevState => ({
421
+ emailPermissions: prevState.emailPermissions.map((item, i) =>
422
+ i === index ? { ...item, [field]: value } : item
423
+ )
424
+ }));
425
+ };
426
+
427
+ // メール権限入力欄を削除
428
+ removeEmailPermission = (index) => {
429
+ this.setState(prevState => ({
430
+ emailPermissions: prevState.emailPermissions.filter((_, i) => i !== index)
431
+ }));
432
+ };
433
+
434
+ // 権限設定を適用
435
+ applyPermissions = async () => {
436
+ this.setState({ isProcessing: true });
437
+ try {
438
+ const { currentSharingFileId, accessToken, emailPermissions, linkPermission } = this.state;
439
+
440
+ // メールごとの権限設定
441
+ for (const permission of emailPermissions) {
442
+ if (permission.email.trim()) {
443
+ await this.setFilePermission(
444
+ currentSharingFileId,
445
+ accessToken,
446
+ permission.email.trim(),
447
+ permission.role
448
+ );
449
+ }
450
+ }
451
+
452
+ // リンクを知っている全員への権限設定
453
+ await fetch(`https://www.googleapis.com/drive/v3/files/${currentSharingFileId}/permissions`, {
454
+ method: "POST",
455
+ headers: {
456
+ "Authorization": `Bearer ${accessToken}`,
457
+ "Content-Type": "application/json",
458
+ },
459
+ body: JSON.stringify({
460
+ type: "anyone",
461
+ role: linkPermission,
462
+ }),
463
+ });
464
+
465
+ this.showAlert("success", "共有設定を適用しました");
466
+ this.closeShareModal();
467
+ } catch (error) {
468
+ console.error("権限設定エラー:", error);
469
+ this.showAlert("error", "共有設定の適用に失敗しました");
470
+ } finally {
471
+ this.setState({ isProcessing: false });
472
+ }
473
+ };
474
+
475
+ // ファイル権限設定メソッド
476
+ async setFilePermission(fileId, accessToken, email, role = "reader") {
477
+ const response = await fetch(
478
+ `https://www.googleapis.com/drive/v3/files/${fileId}/permissions`,
479
+ {
480
+ method: "POST",
481
+ headers: {
482
+ "Authorization": `Bearer ${accessToken}`,
483
+ "Content-Type": "application/json",
484
+ },
485
+ body: JSON.stringify({
486
+ type: "user",
487
+ role: role,
488
+ emailAddress: email
489
+ }),
490
+ }
491
+ );
492
+
493
+ if (!response.ok) {
494
+ throw new Error(await response.text());
495
+ }
496
+ return await response.json();
497
+ }
498
+
499
+ // 共有モーダルのレンダリング
500
+ renderShareModal() {
501
+ if (!this.state.isShareModalOpen) return null;
502
+
503
+ return (
504
+ <div className={styles.modalOverlay} onClick={this.handleShareOverlayClick}>
505
+ <div className={styles.modalContent} ref={this.shareModalContentRef} style={{maxWidth: '500px'}}>
506
+ <div className={styles.modalHeader}>
507
+ <h2>共有設定</h2>
508
+ <button
509
+ onClick={this.closeShareModal}
510
+ className={styles.closeButton}
511
+ disabled={this.state.isProcessing}
512
+ >
513
+ ×
514
+ </button>
515
+ </div>
516
+
517
+ <div className={styles.modalBody}>
518
+ <div className={styles.shareSection}>
519
+ <h3>ユーザーごとの共有</h3>
520
+ {this.state.emailPermissions.map((permission, index) => (
521
+ <div key={index} className={styles.permissionRow}>
522
+ <input
523
+ type="email"
524
+ placeholder="メールアドレス"
525
+ value={permission.email}
526
+ onChange={(e) => this.updateEmailPermission(index, 'email', e.target.value)}
527
+ className={styles.emailInput}
528
+ disabled={this.state.isProcessing}
529
+ />
530
+ <select
531
+ value={permission.role}
532
+ onChange={(e) => this.updateEmailPermission(index, 'role', e.target.value)}
533
+ className={styles.roleSelect}
534
+ disabled={this.state.isProcessing}
535
+ >
536
+ <option value="reader">閲覧可能</option>
537
+ <option value="writer">編集可能</option>
538
+ </select>
539
+ {this.state.emailPermissions.length > 1 && (
540
+ <button
541
+ onClick={() => this.removeEmailPermission(index)}
542
+ className={styles.removeButton}
543
+ disabled={this.state.isProcessing}
544
+ >
545
+ ×
546
+ </button>
547
+ )}
548
+ </div>
549
+ ))}
550
+ <button
551
+ onClick={this.addEmailPermission}
552
+ className={styles.addButton}
553
+ disabled={this.state.isProcessing}
554
+ >
555
+
556
+ </button>
557
+ </div>
558
+
559
+ <div className={styles.shareSection}>
560
+ <h3>リンクを知っている全員</h3>
561
+ <div className={styles.permissionRow}>
562
+ <select
563
+ value={this.state.linkPermission}
564
+ onChange={(e) => this.setState({ linkPermission: e.target.value })}
565
+ className={styles.roleSelect}
566
+ disabled={this.state.isProcessing}
567
+ >
568
+ <option value="reader">閲覧のみ</option>
569
+ <option value="writer">編集可能</option>
570
+ </select>
571
+ </div>
572
+ </div>
573
+
574
+ <div className={styles.shareSection}>
575
+ <h3>グループ内</h3>
576
+ <div className={styles.permissionRow}>
577
+ <select
578
+ value={this.state.groupPermission}
579
+ onChange={(e) => this.setState({ groupPermission: e.target.value })}
580
+ className={styles.roleSelect}
581
+ disabled={this.state.isProcessing}
582
+ >
583
+ <option value="reader">閲覧のみ</option>
584
+ <option value="writer">編集可能</option>
585
+ </select>
586
+ </div>
587
+ </div>
588
+ </div>
589
+
590
+ <div className={styles.modalFooter}>
591
+ <button
592
+ onClick={this.applyPermissions}
593
+ className={styles.applyButton}
594
+ disabled={this.state.isProcessing}
595
+ >
596
+ 適用
597
+ </button>
598
+ <button
599
+ onClick={this.closeShareModal}
600
+ className={styles.cancelButton}
601
+ disabled={this.state.isProcessing}
602
+ >
603
+ キャンセル
604
+ </button>
605
+ </div>
606
+
607
+ {this.state.isProcessing && (
608
+ <div className={styles.processingOverlay}>
609
+ <div className={styles.spinner}></div>
610
+ <div>処理中...</div>
611
+ </div>
612
+ )}
613
+ </div>
614
+ </div>
615
+ );
616
+ }
617
+
618
  render() {
619
  return (
620
  <div>
 
633
  </Button>
634
 
635
  {this.renderModal()}
636
+ {this.renderShareModal()}
637
  </div>
638
  );
639
  }
 
644
  this.setState({
645
  accessToken: null,
646
  currentAccountEmail: null,
647
+ currentAccountName: null,
648
+ files: []
649
  });
650
  localStorage.removeItem('googleDriveAccessToken');
651
  localStorage.removeItem('googleDriveAccountEmail');
 
656
  this.setState({isProcessing: true});
657
  try {
658
  await this.saveToGoogleDrive(null, `${this.state.newFileName}.s4s.txt`, this.state.sharePermission);
659
+ this.showAlert("success", "新規保存しました");
660
  this.setState({showNewFileInput: false});
661
  this.fetchDriveFiles(this.state.accessToken);
662
  } catch (error) {
663
  console.error("新規保存エラー:", error);
664
+ this.showAlert("error", "新規保存に失敗しました");
665
  } finally {
666
  this.setState({isProcessing: false});
667
  }
 
685
  this.setState({isProcessing: true});
686
  try {
687
  await this.saveToGoogleDrive(project.id, project.name);
688
+ this.showAlert("success", "上書き保存しました");
689
  this.fetchDriveFiles(this.state.accessToken);
690
  } catch (error) {
691
  console.error("ファイル上書きエラー:", error);
692
+ this.showAlert("error", "ファイルの上書きに失敗しました");
693
  } finally {
694
  this.setState({isProcessing: false});
695
  }
 
719
  await this.deleteFile(thumbnailToDelete.id);
720
  }
721
 
722
+ this.showAlert("success", "ファイルを削除しました");
723
  this.fetchDriveFiles(this.state.accessToken);
724
  } catch (error) {
725
  console.error("削除エラー:", error);
726
+ this.showAlert("error", "ファイルの削除に失敗しました");
727
  } finally {
728
  this.setState({isProcessing: false});
729
  }
 
734
  if (this.state.isProcessing) return;
735
 
736
  navigator.clipboard.writeText(text)
737
+ .then(() => this.showAlert("success", "リンクをクリップボードにコピーしました"))
738
+ .catch(() => this.showAlert("error", "リンクのコピーに失敗しました"));
739
  };
740
 
741
  async deleteFile(fileId) {
 
752
  }
753
 
754
  async saveToGoogleDrive(fileId, fileName, permission = 'reader') {
755
+ if (!window.vm) {
756
+ throw new Error("VMが初期化されていません");
757
+ }
758
+
759
  const blob = await window.vm.saveProjectSb3();
760
 
761
  const metadata = {
 
836
  "Content-Type": "application/json",
837
  },
838
  body: JSON.stringify({
839
+ role: permission,
840
  type: "anyone",
841
  }),
842
  });
843
  }
844
+
845
+ return fileData;
846
  }
847
 
848
  getProjectThumbnail() {
849
+ return new Promise((resolve) => {
850
+ if (window.vm && window.vm.renderer && window.vm.renderer.requestSnapshot) {
851
+ window.vm.renderer.requestSnapshot(uri => {
852
+ resolve(uri);
853
+ });
854
+ } else {
855
+ // デフォルトのサムネイル
856
+ resolve('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==');
857
+ }
858
  });
859
  }
860
  }
 
864
  showAlert: PropTypes.func.isRequired,
865
  projectTitle: PropTypes.string
866
  };
867
+
868
  const mapStateToProps = state => ({
869
  projectTitle: state.scratchGui.projectTitle
870
  });
871
+
872
  export default connect(mapStateToProps)(GoogleDriveSave);