import { connect } from 'react-redux'; import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import {FormattedMessage} from 'react-intl'; import Button from '../button/button.jsx'; import styles from './google-drive-save.css'; import { getProjectFilename } from '/src/containers/sb3-downloader.jsx'; class GoogleDriveSave extends React.Component { constructor(props) { super(props); this.state = { accessToken: localStorage.getItem('googleDriveAccessToken') || null, currentAccountEmail: localStorage.getItem('googleDriveAccountEmail') || null, currentAccountName: localStorage.getItem('googleDriveAccountName') || null, files: [], isModalOpen: false, isLoading: false, isProcessing: false, newFileName: getProjectFilename || '無題', showNewFileInput: false, sharePermission: 'reader', // 'reader', 'writer', or 'owner' selectedFileId: null }; this.modalContentRef = React.createRef(); } componentDidMount() { // 初期化処理 } handleClick = () => { this.setState({isModalOpen: true}); }; handleCloseModal = () => { if (!this.state.isProcessing) { this.setState({isModalOpen: false, showNewFileInput: false}); } }; handleOverlayClick = (e) => { if (!this.state.isProcessing && this.modalContentRef.current && !this.modalContentRef.current.contains(e.target)) { this.handleCloseModal(); } }; startGoogleLogin = () => { if (this.state.isProcessing) return; localStorage.removeItem('googleDriveAccessToken'); localStorage.removeItem('googleDriveAccountEmail'); localStorage.removeItem('googleDriveAccountName'); const CLIENT_ID = "169451419993-v1b3s315s8dkui950j2nm15hetr5i0qk.apps.googleusercontent.com"; const REDIRECT_URI = "https://s-4-s-auth.hf.space/close2"; const SCOPES = "https://www.googleapis.com/auth/drive.file"; const messageListener = (event) => { if (event.data.token) { window.removeEventListener("message", messageListener); this.setState({ accessToken: event.data.token, currentAccountEmail: event.data.email || null, currentAccountName: event.data.name || null, isModalOpen: true }); this.fetchDriveFiles(event.data.token); } }; window.addEventListener("message", messageListener); const authUrl = `https://accounts.google.com/o/oauth2/auth?` + `client_id=${CLIENT_ID}` + `&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` + `&response_type=token` + `&scope=${encodeURIComponent(SCOPES)}`; window.open(authUrl, "_blank", "width=500,height=600"); }; fetchDriveFiles = async (accessToken) => { this.setState({isLoading: true}); try { const response = await fetch("https://www.googleapis.com/drive/v3/files?q=(mimeType='application/x-scratch' or mimeType='image/png')", { headers: { Authorization: `Bearer ${accessToken}`, }, }); if (!response.ok) { throw new Error(await response.text()); } const data = await response.json(); this.setState({files: data.files || [], isLoading: false}); } catch (error) { console.error("ファイル一覧取得エラー:", error); alert("error", "ファイル一覧の取得に失敗しました"); this.setState({isLoading: false}); } }; renderModal() { if (!this.state.isModalOpen) return null; return (

Googleドライブに保存

{this.renderAuthSection()} {this.state.accessToken && this.renderNewFileSection()} {this.state.accessToken && this.renderFileList()}
{this.state.isProcessing && (
処理中...
)}
); } renderAuthSection() { if (this.state.accessToken) { return (
ログイン中: {this.state.currentAccountName || this.state.currentAccountEmail || 'Googleアカウント'}
); } return (

Googleでログインして、プロジェクトを保存または更新します。

); } renderNewFileSection() { if (this.state.showNewFileInput) { return (
this.setState({newFileName: e.target.value})} className={styles.newFileNameInput} placeholder="ファイル名を入力" disabled={this.state.isProcessing} />
); } return (
); } renderFileList() { if (this.state.isLoading) { return
読み込み中...
; } const projectFiles = this.state.files.filter(file => file.mimeType === 'application/x-scratch'); const thumbnailFiles = this.state.files.filter(file => file.mimeType === 'image/png'); if (projectFiles.length === 0) { return
保存されたファイルが見つかりません
; } return (

保存済みプロジェクト

{projectFiles.map(project => this.renderFileItem(project, thumbnailFiles))}
); } renderFileItem(project, thumbnailFiles) { const thumbnail = thumbnailFiles.find( thumb => thumb.name === `Scratch-Thumbnail-${project.id}.png` ); return (
{thumbnail ? ( プロジェクトサムネイル ) : (
サムネイルなし
)}

{project.name.replace('.s4s.txt', '')}

{this.renderShareLink(project.id)}
{/* ここにアクセス権限設定のドロップダウンを追加 */}
); } updateFilePermission = async (fileId, permission) => { this.setState({isProcessing: true}); try { await fetch(`https://www.googleapis.com/drive/v3/files/${fileId}/permissions/anyone`, { method: "PATCH", headers: { Authorization: `Bearer ${this.state.accessToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ role: permission, }), }); alert("success", "アクセス権限を更新しました"); } catch (error) { console.error("権限更新エラー:", error); alert("error", "アクセス権限の更新に失敗しました"); } finally { this.setState({isProcessing: false}); } }; renderShareLink(fileId) { const SHORT_URL = "https://s4.rf.gd/"; return (
{`${SHORT_URL}${fileId}`}
); } render() { return (
{this.renderModal()}
); } handleChangeAccount = () => { if (this.state.isProcessing) return; this.setState({ accessToken: null, currentAccountEmail: null, currentAccountName: null }); localStorage.removeItem('googleDriveAccessToken'); localStorage.removeItem('googleDriveAccountEmail'); localStorage.removeItem('googleDriveAccountName'); }; handleNewFileSave = async () => { this.setState({isProcessing: true}); try { await this.saveToGoogleDrive(null, `${this.state.newFileName}.s4s.txt`, this.state.sharePermission); alert("success", "新規保存しました"); this.setState({showNewFileInput: false}); this.fetchDriveFiles(this.state.accessToken); } catch (error) { console.error("新規保存エラー:", error); alert("error", "新規保存に失敗しました"); } finally { this.setState({isProcessing: false}); } }; handleLoadFile = (project) => { if (this.state.isProcessing) return; const PROXY_URL = "https://soiz1-drive-proxy.hf.space/?file_id="; if (confirm(`"${project.name}"を読み込みますか?現在のプロジェクトは失われます。`)) { const url = `${PROXY_URL}${project.id}`; window.location.href = `?project_url=${encodeURIComponent(url)}`; } }; handleReplaceFile = async (project) => { if (this.state.isProcessing) return; if (confirm(`"${project.name}"を現在のプロジェクトで上書きしますか?`)) { this.setState({isProcessing: true}); try { await this.saveToGoogleDrive(project.id, project.name); alert("success", "上書き保存しました"); this.fetchDriveFiles(this.state.accessToken); } catch (error) { console.error("ファイル上書きエラー:", error); alert("error", "ファイルの上書きに失敗しました"); } finally { this.setState({isProcessing: false}); } } }; handleShareFile = (fileId) => { if (this.state.isProcessing) return; const SHARE_URL = "https://scratch-school.ct.ws/upload?id="; window.open(`${SHARE_URL}${fileId}`, "_blank"); }; handleDeleteFile = async (project, thumbnailFiles) => { if (this.state.isProcessing) return; if (confirm(`"${project.name}"とそのサムネイルを完全に削除しますか?この操作は元に戻せません。`)) { this.setState({isProcessing: true}); try { await this.deleteFile(project.id); const thumbnailToDelete = thumbnailFiles.find( thumb => thumb.name === `Scratch-Thumbnail-${project.id}.png` ); if (thumbnailToDelete) { await this.deleteFile(thumbnailToDelete.id); } alert("success", "ファイルを削除しました"); this.fetchDriveFiles(this.state.accessToken); } catch (error) { console.error("削除エラー:", error); alert("error", "ファイルの削除に失敗しました"); } finally { this.setState({isProcessing: false}); } } }; copyToClipboard = (text) => { if (this.state.isProcessing) return; navigator.clipboard.writeText(text) .then(() => alert("success", "リンクをクリップボードにコピーしました")) .catch(() => alert("error", "リンクのコピーに失敗しました")); }; async deleteFile(fileId) { const response = await fetch(`https://www.googleapis.com/drive/v3/files/${fileId}`, { method: "DELETE", headers: { Authorization: `Bearer ${this.state.accessToken}`, }, }); if (!response.ok) { throw new Error(await response.text()); } } async saveToGoogleDrive(fileId, fileName, permission = 'reader') { const blob = await window.vm.saveProjectSb3(); const metadata = { name: fileName, mimeType: "application/x-scratch", }; const url = fileId ? `https://www.googleapis.com/upload/drive/v3/files/${fileId}?uploadType=multipart` : "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart"; const form = new FormData(); form.append("metadata", new Blob([JSON.stringify(metadata)], { type: "application/json" })); form.append("file", blob); const method = fileId ? "PATCH" : "POST"; const uploadResponse = await fetch(url, { method, headers: { Authorization: `Bearer ${this.state.accessToken}`, }, body: form, }); if (!uploadResponse.ok) { throw new Error(await uploadResponse.text()); } const fileData = await uploadResponse.json(); try { const thumbnailDataUrl = await this.getProjectThumbnail(); const thumbnailBlob = await (await fetch(thumbnailDataUrl)).blob(); const thumbnailMetadata = { name: `Scratch-Thumbnail-${fileData.id}.png`, mimeType: "image/png", }; const existingThumbnailResponse = await fetch( `https://www.googleapis.com/drive/v3/files?q=name='${thumbnailMetadata.name}'`, { headers: { Authorization: `Bearer ${this.state.accessToken}`, }, } ); const existingThumbnailData = await existingThumbnailResponse.json(); const thumbnailFileId = existingThumbnailData.files?.[0]?.id; const thumbnailUrl = thumbnailFileId ? `https://www.googleapis.com/upload/drive/v3/files/${thumbnailFileId}?uploadType=multipart` : "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart"; const thumbnailForm = new FormData(); thumbnailForm.append("metadata", new Blob([JSON.stringify(thumbnailMetadata)], { type: "application/json" })); thumbnailForm.append("file", thumbnailBlob); const thumbnailMethod = thumbnailFileId ? "PATCH" : "POST"; await fetch(thumbnailUrl, { method: thumbnailMethod, headers: { Authorization: `Bearer ${this.state.accessToken}`, }, body: thumbnailForm, }); } catch (thumbnailError) { console.warn("サムネイルの保存に失敗しました:", thumbnailError); } if (!fileId) { await fetch(`https://www.googleapis.com/drive/v3/files/${fileData.id}/permissions`, { method: "POST", headers: { Authorization: `Bearer ${this.state.accessToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ role: permission, // ここで公開設定を使用 type: "anyone", }), }); } } getProjectThumbnail() { return new Promise(resolve => { window.vm.renderer.requestSnapshot(uri => { resolve(uri); }); }); } } GoogleDriveSave.propTypes = { className: PropTypes.string, showAlert: PropTypes.func.isRequired, projectTitle: PropTypes.string }; const mapStateToProps = state => ({ projectTitle: state.scratchGui.projectTitle }); export default connect(mapStateToProps)(GoogleDriveSave);