soiz1 commited on
Commit
b587e60
·
verified ·
1 Parent(s): a2e8533

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

Browse files
src/components/menu-bar/google-drive-save.css CHANGED
@@ -1,502 +1,197 @@
 
 
 
 
1
 
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;
 
 
 
 
1
+ /* google-drive-save.css */
2
+ .saveButton {
3
+ /* ボタンのスタイル */
4
+ }
5
 
6
+ .modalOverlay {
7
+ position: fixed;
8
+ top: 0;
9
+ left: 0;
10
+ right: 0;
11
+ bottom: 0;
12
+ background-color: rgba(0, 0, 0, 0.5);
13
+ display: flex;
14
+ justify-content: center;
15
+ align-items: center;
16
+ z-index: 1000;
17
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
+ .modalContent {
20
+ background-color: white;
21
+ border-radius: 8px;
22
+ width: 800px;
23
+ max-width: 90%;
24
+ max-height: 80vh;
25
+ overflow: hidden;
26
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
27
+ }
28
 
29
+ .modalHeader {
30
+ padding: 1rem;
31
+ border-bottom: 1px solid #eee;
32
+ display: flex;
33
+ justify-content: space-between;
34
+ align-items: center;
35
+ }
 
36
 
37
+ .closeButton {
38
+ background: none;
39
+ border: none;
40
+ font-size: 1.5rem;
41
+ cursor: pointer;
42
+ }
 
 
 
 
 
 
43
 
44
+ .modalBody {
45
+ padding: 1rem;
46
+ max-height: 70vh;
47
+ overflow-y: auto;
48
+ }
49
 
50
+ .authSection {
51
+ margin-bottom: 1rem;
52
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
+ .accountInfo {
55
+ margin-bottom: 0.5rem;
56
+ }
 
 
57
 
58
+ .loginButton, .changeAccountButton, .newFileButton {
59
+ padding: 0.5rem 1rem;
60
+ background-color: #4CAF50;
61
+ color: white;
62
+ border: none;
63
+ border-radius: 4px;
64
+ cursor: pointer;
65
+ }
66
 
67
+ .changeAccountButton {
68
+ background-color: #f0ad4e;
69
+ }
 
70
 
71
+ .loading, .noFiles {
72
+ text-align: center;
73
+ padding: 2rem;
74
+ color: #666;
75
+ }
 
 
 
 
 
76
 
77
+ .fileListContainer {
78
+ margin-top: 1rem;
79
+ }
80
 
81
+ .fileListHeader {
82
+ display: flex;
83
+ justify-content: space-between;
84
+ align-items: center;
85
+ margin-bottom: 1rem;
86
+ }
87
 
88
+ .projectTitle {
89
+ cursor: pointer;
90
+ border-bottom: 1px dashed #000;
91
+ color: #333;
92
+ }
93
 
94
+ .fileList {
95
+ display: grid;
96
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
97
+ gap: 1rem;
98
+ }
 
 
99
 
100
+ .fileItem {
101
+ border: 1px solid #ddd;
102
+ border-radius: 8px;
103
+ padding: 1rem;
104
+ display: flex;
105
+ flex-direction: column;
106
+ gap: 0.75rem;
107
+ background: #fff;
108
+ height: 100%;
109
+ }
110
 
111
+ .thumbnailContainer {
112
+ position: relative;
113
+ aspect-ratio: 4/3;
114
+ max-height: 150px;
115
+ overflow: hidden;
116
+ }
117
 
118
+ .thumbnail {
119
+ width: 100%;
120
+ height: 100%;
121
+ border-radius: 4px;
122
+ object-fit: contain;
123
+ background-color: #f0f0f0;
124
+ }
 
125
 
126
+ .thumbnailPlaceholder {
127
+ width: 100%;
128
+ height: 100%;
129
+ background-color: #f0f0f0;
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: center;
133
+ border-radius: 4px;
134
+ color: #666;
135
+ }
136
 
137
+ .fileName {
138
+ margin: 0;
139
+ text-align: center;
140
+ font-weight: bold;
141
+ color: #333;
142
+ }
143
 
144
+ .linkContainer {
145
+ display: flex;
146
+ flex-direction: column;
147
+ gap: 0.25rem;
148
+ margin-bottom: 0.5rem;
149
+ }
150
 
151
+ .linkHeader {
152
+ display: flex;
153
+ justify-content: space-between;
154
+ align-items: center;
155
+ }
156
 
157
+ .copyButton {
158
+ padding: 0.1rem 0.3rem;
159
+ font-size: 0.8em;
160
+ background-color: #e9e9e9;
161
+ color: #333;
162
+ border: none;
163
+ border-radius: 3px;
164
+ cursor: pointer;
165
+ }
166
 
167
+ .linkUrl {
168
+ font-size: 0.9em;
169
+ word-break: break-all;
170
+ color: #1155cc;
171
+ text-decoration: none;
172
+ border-bottom: 1px solid #1155cc;
173
+ }
 
 
 
174
 
175
+ .buttonGroup {
176
+ display: grid;
177
+ grid-template-columns: 1fr 1fr;
178
+ gap: 0.5rem;
179
+ }
 
 
 
 
 
 
 
 
 
180
 
181
+ .actionButton {
182
+ width: 100%;
183
+ padding: 0.5rem;
184
+ border: none;
185
+ border-radius: 4px;
186
+ cursor: pointer;
 
187
  }
188
 
189
+ .shareButton {
190
+ background-color: #4CAF50;
191
+ color: white;
192
+ }
193
 
194
+ .deleteButton {
195
+ background-color: #ff4444;
196
+ color: white;
197
+ }