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 (
);
}
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);