|
<?php
|
|
|
|
ini_set('log_errors', 1);
|
|
ini_set('error_log', __DIR__ . '/logs.txt');
|
|
ini_set('display_errors', 0);
|
|
header('Content-Type: text/html; charset=UTF-8');
|
|
mb_internal_encoding('UTF-8');
|
|
|
|
|
|
session_start();
|
|
|
|
try {
|
|
|
|
require_once 'config.php';
|
|
|
|
|
|
if (!isset($_SESSION['user_id'])) {
|
|
header('Location: login.php');
|
|
exit;
|
|
}
|
|
|
|
$drive_id = $_GET['id'] ?? '';
|
|
$error = '';
|
|
$success = '';
|
|
|
|
|
|
$conn = new mysqli($host, $username, $password, $dbname);
|
|
if ($conn->connect_error) {
|
|
throw new Exception("データベース接続失敗: " . $conn->connect_error);
|
|
}
|
|
|
|
if (!$conn->set_charset("utf8mb4")) {
|
|
throw new Exception("文字コード設定失敗: " . $conn->error);
|
|
}
|
|
|
|
$project = null;
|
|
if ($drive_id) {
|
|
$stmt = $conn->prepare("SELECT * FROM projects WHERE drive_id = ?");
|
|
if (!$stmt) {
|
|
throw new Exception("プリペアドステートメント作成失敗: " . $conn->error);
|
|
}
|
|
|
|
$stmt->bind_param("s", $drive_id);
|
|
if (!$stmt->execute()) {
|
|
throw new Exception("クエリ実行失敗: " . $stmt->error);
|
|
}
|
|
|
|
$result = $stmt->get_result();
|
|
$project = $result->fetch_assoc();
|
|
|
|
if (!$project) {
|
|
$project_name = str_replace('.sb3.txt', '', 'プロジェクト名');
|
|
} else {
|
|
$project_name = $project['project_name'];
|
|
}
|
|
}
|
|
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
$project_name = $_POST['project_name'] ?? '';
|
|
$description = $_POST['description'] ?? '';
|
|
$is_public = isset($_POST['is_public']);
|
|
$is_accessible = isset($_POST['is_accessible']);
|
|
$can_comment = isset($_POST['can_comment']);
|
|
$password = $_POST['password'] ?? '';
|
|
|
|
if (empty($project_name)) {
|
|
$error = 'プロジェクト名は必須です';
|
|
} else {
|
|
|
|
if ($is_public) {
|
|
$is_accessible = true;
|
|
$password = '';
|
|
}
|
|
|
|
|
|
if (!$is_accessible) {
|
|
$can_comment = false;
|
|
$password = '';
|
|
}
|
|
|
|
|
|
|
|
if (!(!$is_public && $is_accessible)) {
|
|
$password = '';
|
|
}
|
|
|
|
|
|
$comment_data = [
|
|
'can_comment' => $can_comment,
|
|
'history' => $project ? json_decode($project['comment'], true)['history'] ?? [] : []
|
|
];
|
|
|
|
|
|
if (!$project) {
|
|
$unique_id = false;
|
|
$max_attempts = 10;
|
|
$attempts = 0;
|
|
|
|
while (!$unique_id && $attempts < $max_attempts) {
|
|
$new_id = str_pad(mt_rand(0, 9999999), 7, '0', STR_PAD_LEFT);
|
|
|
|
$check_stmt = $conn->prepare("SELECT id FROM projects WHERE id = ?");
|
|
$check_stmt->bind_param("s", $new_id);
|
|
$check_stmt->execute();
|
|
$check_result = $check_stmt->get_result();
|
|
|
|
if ($check_result->num_rows === 0) {
|
|
$unique_id = true;
|
|
}
|
|
|
|
$attempts++;
|
|
$check_stmt->close();
|
|
}
|
|
|
|
if (!$unique_id) {
|
|
throw new Exception("一意のIDを生成できませんでした");
|
|
}
|
|
}
|
|
|
|
if ($project) {
|
|
$stmt = $conn->prepare("UPDATE projects SET project_name = ?, description = ?, is_public = ?, comment = ?, password = ? WHERE id = ?");
|
|
if (!$stmt) {
|
|
throw new Exception("更新ステートメント作成失敗: " . $conn->error);
|
|
}
|
|
$stmt->bind_param("ssisss", $project_name, $description, $is_public, json_encode($comment_data), $password, $project['id']);
|
|
} else {
|
|
$stmt = $conn->prepare("INSERT INTO projects (id, drive_id, project_name, description, author_name, author_id, is_public, comment, password) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
if (!$stmt) {
|
|
throw new Exception("挿入ステートメント作成失敗: " . $conn->error);
|
|
}
|
|
$stmt->bind_param("ssssssiss", $new_id, $drive_id, $project_name, $description, $_SESSION['user_name'], $_SESSION['user_id'], $is_public, json_encode($comment_data), $password);
|
|
}
|
|
|
|
if (!$stmt->execute()) {
|
|
throw new Exception("クエリ実行失敗: " . $stmt->error);
|
|
}
|
|
|
|
$success = 'プロジェクト情報を保存しました';
|
|
|
|
|
|
|
|
if (isset($_FILES['thumbnail']) && $_FILES['thumbnail']['error'] === UPLOAD_ERR_OK) {
|
|
|
|
if ($_FILES['thumbnail']['size'] > 10 * 1024 * 1024) {
|
|
$error = 'サムネイル画像は10MB以下にしてください';
|
|
} else {
|
|
$thumbnail_tmp = $_FILES['thumbnail']['tmp_name'];
|
|
$thumbnail_ext = strtolower(pathinfo($_FILES['thumbnail']['name'], PATHINFO_EXTENSION));
|
|
$thumbnail_name = "Scratch-Thumbnail-$drive_id." . ($thumbnail_ext === 'gif' ? 'gif' : 'png');
|
|
|
|
|
|
if ($thumbnail_ext === 'gif') {
|
|
$image = imagecreatefromgif($thumbnail_tmp);
|
|
} elseif ($thumbnail_ext === 'png') {
|
|
$image = imagecreatefrompng($thumbnail_tmp);
|
|
} else {
|
|
$image = imagecreatefromjpeg($thumbnail_tmp);
|
|
}
|
|
|
|
|
|
$temp_file = tempnam(sys_get_temp_dir(), 'thumb') . '.png';
|
|
imagepng($image, $temp_file);
|
|
imagedestroy($image);
|
|
$file_content = file_get_contents($temp_file);
|
|
unlink($temp_file);
|
|
|
|
|
|
$access_token = $_SESSION['access_token'];
|
|
|
|
|
|
$thumbnailResponse = file_get_contents("https://www.googleapis.com/drive/v3/files?q=name='$thumbnail_name'", false, stream_context_create([
|
|
'http' => [
|
|
'header' => "Authorization: Bearer $access_token\r\n"
|
|
]
|
|
]));
|
|
|
|
$thumbnailData = json_decode($thumbnailResponse, true);
|
|
$thumbnailId = null;
|
|
|
|
if ($thumbnailData['files'] && count($thumbnailData['files']) > 0) {
|
|
$thumbnailId = $thumbnailData['files'][0]['id'];
|
|
}
|
|
|
|
|
|
$boundary = '-------' . md5(microtime());
|
|
$mime_type = 'image/png';
|
|
|
|
$headers = [
|
|
'Authorization: Bearer ' . $access_token,
|
|
'Content-Type: multipart/related; boundary="' . $boundary . '"'
|
|
];
|
|
|
|
$post_data = "--$boundary\r\n";
|
|
$post_data .= "Content-Type: application/json; charset=UTF-8\r\n\r\n";
|
|
$post_data .= json_encode([
|
|
'name' => $thumbnail_name,
|
|
'mimeType' => $mime_type,
|
|
'parents' => ['root']
|
|
]) . "\r\n";
|
|
|
|
$post_data .= "--$boundary\r\n";
|
|
$post_data .= "Content-Type: $mime_type\r\n\r\n";
|
|
$post_data .= $file_content . "\r\n";
|
|
$post_data .= "--$boundary--";
|
|
|
|
$ch = curl_init();
|
|
if ($thumbnailId) {
|
|
|
|
curl_setopt($ch, CURLOPT_URL, "https://www.googleapis.com/upload/drive/v3/files/$thumbnailId?uploadType=multipart");
|
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PATCH");
|
|
} else {
|
|
|
|
curl_setopt($ch, CURLOPT_URL, "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart");
|
|
curl_setopt($ch, CURLOPT_POST, 1);
|
|
}
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
$response = curl_exec($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
if ($httpCode >= 200 && $httpCode < 300) {
|
|
$thumbnailId = json_decode($response, true)['id'] ?? $thumbnailId;
|
|
|
|
|
|
if ($thumbnailId) {
|
|
$ch = curl_init();
|
|
curl_setopt($ch, CURLOPT_URL, "https://www.googleapis.com/drive/v3/files/$thumbnailId/permissions");
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
|
'Authorization: Bearer ' . $access_token,
|
|
'Content-Type: application/json'
|
|
]);
|
|
|
|
if ($is_accessible) {
|
|
|
|
curl_setopt($ch, CURLOPT_POST, 1);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
|
|
'role' => 'reader',
|
|
'type' => 'anyone'
|
|
]));
|
|
} else {
|
|
|
|
|
|
curl_setopt($ch, CURLOPT_URL, "https://www.googleapis.com/drive/v3/files/$thumbnailId/permissions?fields=permissions(id,type)");
|
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET");
|
|
$response = curl_exec($ch);
|
|
$permissions = json_decode($response, true)['permissions'] ?? [];
|
|
|
|
|
|
foreach ($permissions as $perm) {
|
|
if ($perm['type'] === 'anyone') {
|
|
$ch_del = curl_init();
|
|
curl_setopt($ch_del, CURLOPT_URL, "https://www.googleapis.com/drive/v3/files/$thumbnailId/permissions/" . $perm['id']);
|
|
curl_setopt($ch_del, CURLOPT_HTTPHEADER, [
|
|
'Authorization: Bearer ' . $access_token
|
|
]);
|
|
curl_setopt($ch_del, CURLOPT_CUSTOMREQUEST, "DELETE");
|
|
curl_exec($ch_del);
|
|
curl_close($ch_del);
|
|
}
|
|
}
|
|
}
|
|
curl_close($ch);
|
|
}
|
|
} else {
|
|
error_log("サムネイルアップロード失敗: " . $response);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isset($_SESSION['access_token']) && $drive_id) {
|
|
$access_token = $_SESSION['access_token'];
|
|
|
|
|
|
$ch = curl_init();
|
|
curl_setopt($ch, CURLOPT_URL, "https://www.googleapis.com/drive/v3/files/$drive_id/permissions");
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
|
'Authorization: Bearer ' . $access_token,
|
|
'Content-Type: application/json'
|
|
]);
|
|
|
|
if ($is_accessible) {
|
|
|
|
curl_setopt($ch, CURLOPT_POST, 1);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
|
|
'role' => 'reader',
|
|
'type' => 'anyone'
|
|
]));
|
|
} else {
|
|
|
|
|
|
curl_setopt($ch, CURLOPT_URL, "https://www.googleapis.com/drive/v3/files/$drive_id/permissions?fields=permissions(id,type)");
|
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET");
|
|
$response = curl_exec($ch);
|
|
$permissions = json_decode($response, true)['permissions'] ?? [];
|
|
|
|
|
|
foreach ($permissions as $perm) {
|
|
if ($perm['type'] === 'anyone') {
|
|
$ch_del = curl_init();
|
|
curl_setopt($ch_del, CURLOPT_URL, "https://www.googleapis.com/drive/v3/files/$drive_id/permissions/" . $perm['id']);
|
|
curl_setopt($ch_del, CURLOPT_HTTPHEADER, [
|
|
'Authorization: Bearer ' . $access_token
|
|
]);
|
|
curl_setopt($ch_del, CURLOPT_CUSTOMREQUEST, "DELETE");
|
|
curl_exec($ch_del);
|
|
curl_close($ch_del);
|
|
}
|
|
}
|
|
}
|
|
curl_close($ch);
|
|
}
|
|
|
|
$stmt = $conn->prepare("SELECT * FROM projects WHERE drive_id = ?");
|
|
if (!$stmt) {
|
|
throw new Exception("選択ステートメント作成失敗: " . $conn->error);
|
|
}
|
|
$stmt->bind_param("s", $drive_id);
|
|
if (!$stmt->execute()) {
|
|
throw new Exception("クエリ実行失敗: " . $stmt->error);
|
|
}
|
|
$result = $stmt->get_result();
|
|
$project = $result->fetch_assoc();
|
|
}
|
|
}
|
|
|
|
$conn->close();
|
|
|
|
} catch (Exception $e) {
|
|
error_log("[" . date('Y-m-d H:i:s') . "] エラー: " . $e->getMessage() . "\n", 3, __DIR__ . '/logs.txt');
|
|
$error = 'システムエラーが発生しました。管理者にお問い合わせください。';
|
|
http_response_code(500);
|
|
}
|
|
?>
|
|
|
|
<!DOCTYPE html>
|
|
<html lang="ja">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<!-- Google Tag Manager -->
|
|
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
|
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
|
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
|
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
|
})(window,document,'script','dataLayer','GTM-PJXRKX3F');</script>
|
|
<!-- End Google Tag Manager -->
|
|
<title>プロジェクト共有 - Scratch School</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;700&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.0/font/bootstrap-icons.css">
|
|
<style>
|
|
:root {
|
|
--primary-color:
|
|
--secondary-color:
|
|
--background-color:
|
|
--card-bg:
|
|
--text-color:
|
|
--error-color:
|
|
--success-color:
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Noto Sans JP', sans-serif;
|
|
background-color: var(--background-color);
|
|
color: var(--text-color);
|
|
line-height: 1.6;
|
|
padding: 20px;
|
|
}
|
|
|
|
.container {
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
background-color: var(--card-bg);
|
|
border-radius: 10px;
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
padding: 30px;
|
|
}
|
|
|
|
h1 {
|
|
color: var(--primary-color);
|
|
margin-bottom: 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
|
|
.modal-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: rgba(0, 0, 0, 0.7);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.modal-content {
|
|
background-color: white;
|
|
padding: 30px;
|
|
border-radius: 10px;
|
|
width: 90%;
|
|
max-width: 600px;
|
|
max-height: 80vh;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.modal-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.modal-header h2 {
|
|
margin: 0;
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
.close-button {
|
|
background: none;
|
|
border: none;
|
|
font-size: 24px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.auth-section {
|
|
margin-bottom: 20px;
|
|
padding: 15px;
|
|
background-color:
|
|
border-radius: 5px;
|
|
}
|
|
|
|
.login-button {
|
|
background-color:
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 20px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 16px;
|
|
font-weight: 500;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin: 10px auto;
|
|
}
|
|
|
|
.login-button:hover {
|
|
background-color:
|
|
}
|
|
|
|
.file-list {
|
|
margin-top: 20px;
|
|
border-top: 1px solid
|
|
padding-top: 20px;
|
|
}
|
|
|
|
.file-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 10px;
|
|
border-bottom: 1px solid
|
|
cursor: pointer;
|
|
}
|
|
|
|
.file-item:hover {
|
|
background-color:
|
|
}
|
|
|
|
.file-item img {
|
|
width: 50px;
|
|
height: 50px;
|
|
object-fit: cover;
|
|
margin-right: 15px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
|
|
.form-group {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
label {
|
|
display: block;
|
|
margin-bottom: 5px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
input[type="text"],
|
|
textarea,
|
|
input[type="password"] {
|
|
width: 100%;
|
|
padding: 10px;
|
|
border: 1px solid
|
|
border-radius: 4px;
|
|
font-family: inherit;
|
|
font-size: 16px;
|
|
}
|
|
|
|
textarea {
|
|
min-height: 120px;
|
|
resize: vertical;
|
|
}
|
|
|
|
.thumbnail-preview {
|
|
width: 200px;
|
|
height: 150px;
|
|
background-color:
|
|
border-radius: 4px;
|
|
margin-bottom: 10px;
|
|
overflow: hidden;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.thumbnail-preview img {
|
|
max-width: 100%;
|
|
max-height: 100%;
|
|
object-fit: contain;
|
|
}
|
|
|
|
.btn {
|
|
background-color: var(--primary-color);
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 20px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 16px;
|
|
font-weight: 500;
|
|
transition: background-color 0.3s;
|
|
}
|
|
|
|
.btn:hover {
|
|
background-color:
|
|
}
|
|
|
|
.btn-secondary {
|
|
background-color: var(--secondary-color);
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background-color:
|
|
}
|
|
|
|
.error {
|
|
color: var(--error-color);
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.success {
|
|
color: var(--success-color);
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.checkbox-group {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.checkbox-group input {
|
|
margin-right: 10px;
|
|
}
|
|
|
|
.setting-section {
|
|
margin: 25px 0;
|
|
padding: 15px;
|
|
background-color:
|
|
border-radius: 5px;
|
|
}
|
|
|
|
.setting-section h2 {
|
|
margin-top: 0;
|
|
font-size: 18px;
|
|
color: var(--secondary-color);
|
|
}
|
|
|
|
.password-note {
|
|
font-size: 14px;
|
|
color:
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.preview-container {
|
|
border: 1px solid
|
|
padding: 15px;
|
|
border-radius: 4px;
|
|
margin-top: 10px;
|
|
min-height: 100px;
|
|
background-color:
|
|
}
|
|
|
|
.tab-container {
|
|
display: flex;
|
|
margin-bottom: -1px;
|
|
}
|
|
|
|
.tab-button {
|
|
padding: 8px 16px;
|
|
background:
|
|
border: 1px solid
|
|
border-bottom: none;
|
|
cursor: pointer;
|
|
border-radius: 4px 4px 0 0;
|
|
margin-right: 5px;
|
|
}
|
|
|
|
.tab-button.active {
|
|
background:
|
|
border-bottom: 1px solid
|
|
}
|
|
|
|
.tab-content {
|
|
display: none;
|
|
}
|
|
|
|
.tab-content.active {
|
|
display: block;
|
|
}
|
|
|
|
.file-input-label {
|
|
display: inline-block;
|
|
padding: 8px 16px;
|
|
background-color:
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.file-input-label:hover {
|
|
background-color:
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- Google Tag Manager (noscript) -->
|
|
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PJXRKX3F"
|
|
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
|
|
<!-- End Google Tag Manager (noscript) -->
|
|
<?php if (!empty($error)): ?>
|
|
<div class="error"><?php echo htmlspecialchars($error); ?></div>
|
|
<?php endif; ?>
|
|
|
|
<?php if (!empty($success)): ?>
|
|
<div class="success"><?php echo htmlspecialchars($success); ?></div>
|
|
<?php endif; ?>
|
|
|
|
<?php if (isset($e)): ?>
|
|
<div class="system-error">
|
|
申し訳ありません、システムエラーが発生しました。問題が解決しない場合は管理者にお問い合わせください。
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if (empty($drive_id)): ?>
|
|
<!-- Googleドライブファイル選択モーダル -->
|
|
<div class="modal-overlay" id="driveModal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2>Googleドライブからプロジェクトを選択</h2>
|
|
<button class="close-button" onclick="document.getElementById('driveModal').style.display='none'">×</button>
|
|
</div>
|
|
<div class="auth-section">
|
|
<?php if (isset($_SESSION['access_token'])): ?>
|
|
<p>ログイン中: <?php echo htmlspecialchars($_SESSION['user_name'] ?? $_SESSION['user_email'] ?? 'Googleアカウント'); ?></p>
|
|
<?php else: ?>
|
|
<p>Googleでログインして、プロジェクトを選択してください</p>
|
|
<button class="login-button" onclick="startGoogleLogin()">
|
|
Googleでログイン
|
|
</button>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<?php if (isset($_SESSION['access_token'])): ?>
|
|
<div class="file-list" id="fileList">
|
|
<p>読み込み中...</p>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function startGoogleLogin() {
|
|
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 authUrl = `https:
|
|
`client_id=${CLIENT_ID}` +
|
|
`&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` +
|
|
`&response_type=token` +
|
|
`&scope=${encodeURIComponent(SCOPES)}` +
|
|
`&prompt=select_account`;
|
|
|
|
window.open(authUrl, "_blank", "width=500,height=600");
|
|
}
|
|
|
|
|
|
<?php if (isset($_SESSION['access_token'])): ?>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
fetchDriveFiles('<?php echo $_SESSION['access_token']; ?>');
|
|
});
|
|
|
|
function fetchDriveFiles(accessToken) {
|
|
fetch("https://www.googleapis.com/drive/v3/files?q=mimeType='application/x-scratch'", {
|
|
headers: {
|
|
'Authorization': `Bearer ${accessToken}`
|
|
}
|
|
})
|
|
.then(response => {
|
|
if (response.status === 401) {
|
|
|
|
handleAuthError();
|
|
return Promise.reject('認証エラー');
|
|
}
|
|
if (!response.ok) {
|
|
handleAuthError();
|
|
throw new Error('ファイル一覧の取得に失敗しました');
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
const fileList = document.getElementById('fileList');
|
|
if (data.files && data.files.length > 0) {
|
|
fileList.innerHTML = '';
|
|
data.files.forEach(file => {
|
|
const fileItem = document.createElement('div');
|
|
fileItem.className = 'file-item';
|
|
fileItem.innerHTML = `
|
|
<img src="https://drive.google.com/thumbnail?id=${file.id}&sz=w50" alt="サムネイル">
|
|
<div>
|
|
<h3>${file.name.replace('.s4s.txt', '')}</h3>
|
|
<p>最終更新: ${new Date(file.modifiedTime).toLocaleString()}</p>
|
|
</div>
|
|
`;
|
|
fileItem.onclick = function() {
|
|
window.location.href = `?id=${file.id}`;
|
|
};
|
|
fileList.appendChild(fileItem);
|
|
});
|
|
} else {
|
|
fileList.innerHTML = '<p>保存されたプロジェクトが見つかりません</p>';
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('エラー:', error);
|
|
document.getElementById('fileList').innerHTML =
|
|
`<p class="error">ファイル一覧の取得に失敗しました</p>`;
|
|
});
|
|
}
|
|
|
|
function handleAuthError() {
|
|
alert('認証エラーが発生しました。再ログインしてください。');
|
|
|
|
fetch('logout.php', { method: 'POST' })
|
|
.then(() => {
|
|
window.location.href = 'upload.php';
|
|
});
|
|
}
|
|
<?php endif; ?>
|
|
</script>
|
|
<?php endif; ?>
|
|
|
|
<div class="container">
|
|
<h1>プロジェクトを共有</h1>
|
|
|
|
<?php if ($error): ?>
|
|
<div class="error"><?php echo htmlspecialchars($error); ?></div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($success): ?>
|
|
<div class="success"><?php echo htmlspecialchars($success); ?></div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($drive_id): ?>
|
|
<form method="POST" enctype="multipart/form-data">
|
|
<div class="form-group">
|
|
<label for="project_name">プロジェクト名</label>
|
|
<input type="text" id="project_name" name="project_name" value="<?php echo htmlspecialchars($project_name ?? ''); ?>" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="description">説明</label>
|
|
<div class="tab-container">
|
|
<button type="button" class="tab-button active" onclick="switchTab('edit', 'preview')">編集</button>
|
|
<button type="button" class="tab-button" onclick="switchTab('preview', 'edit')">プレビュー</button>
|
|
</div>
|
|
<div class="tab-content active" id="edit-tab">
|
|
<textarea id="description" name="description"><?php echo htmlspecialchars($project['description'] ?? ''); ?></textarea>
|
|
</div>
|
|
<div class="tab-content" id="preview-tab">
|
|
<div class="preview-container" id="markdown-preview"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>サムネイル</label>
|
|
<div class="thumbnail-preview">
|
|
<?php if ($drive_id): ?>
|
|
<img id="thumbnail-image" src="" alt="プロジェクトサムネイル">
|
|
<?php else: ?>
|
|
<span>サムネイルなし</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
<label class="file-input-label">
|
|
<i class="bi bi-upload"></i> 画像を選択 (PNG/GIF, 10MB以下)
|
|
<input type="file" name="thumbnail" accept="image/png,image/jpeg,image/gif" style="display: none;">
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-section">
|
|
<h2>公開設定</h2>
|
|
|
|
<div class="checkbox-group">
|
|
<input type="checkbox" id="is_public" name="is_public" <?php echo (isset($project) && $project['is_public']) ? 'checked' : ''; ?>>
|
|
<label for="is_public">検索可能にする(プロジェクトを公開)</label>
|
|
</div>
|
|
|
|
<div class="checkbox-group">
|
|
<input type="checkbox" id="is_accessible" name="is_accessible" <?php echo (isset($project) && $project['is_public']) ? 'checked' : ''; ?> <?php echo (isset($project) && $project['is_public']) ? 'disabled' : ''; ?>>
|
|
<label for="is_accessible">すべての人がアクセス可能にする</label>
|
|
</div>
|
|
|
|
<div class="checkbox-group">
|
|
<input type="checkbox" id="can_comment" name="can_comment" <?php echo (isset($project) && json_decode($project['comment'], true)['can_comment'] ?? false) ? 'checked' : ''; ?> <?php echo (!isset($project) || !$project['is_public']) ? 'disabled' : ''; ?>>
|
|
<label for="can_comment">コメントを許可する</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="setting-section" id="password-section" style="<?php echo (isset($project) && $project['is_public']) ? 'display:none;' : ((isset($project) && !$project['is_public'] && !$project['is_accessible']) ? 'display:block;' : 'display:none;'); ?>">
|
|
<h2>非公開設定</h2>
|
|
|
|
<div class="form-group">
|
|
<label for="password">表示用パスワード(検索不可で非公開の場合のみ設定可能)</label>
|
|
<input type="password" id="password" name="password" value="<?php echo htmlspecialchars($project['password'] ?? ''); ?>" <?php echo (isset($project) && ($project['is_public'] || $project['is_accessible'])) ? 'disabled' : ''; ?>>
|
|
<p class="password-note">※このパスワードはプロジェクトへのアクセス権限とは関係ありません。表示用のパスワードです。</p>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="submit" class="btn">設定を適用</button>
|
|
</form>
|
|
|
|
<script>
|
|
// タブ切り替え関数
|
|
function switchTab(showId, hideId) {
|
|
document.getElementById(showId + '-tab').classList.add('active');
|
|
document.getElementById(hideId + '-tab').classList.remove('active');
|
|
|
|
const buttons = document.querySelectorAll('.tab-button');
|
|
buttons.forEach(btn => {
|
|
if (btn.textContent.toLowerCase() === showId) {
|
|
btn.classList.add('active');
|
|
} else {
|
|
btn.classList.remove('active');
|
|
}
|
|
});
|
|
|
|
|
|
if (showId === 'preview') {
|
|
updateMarkdownPreview();
|
|
}
|
|
}
|
|
|
|
|
|
function updateMarkdownPreview() {
|
|
const markdown = document.getElementById('description').value;
|
|
|
|
|
|
let html = markdown
|
|
.replace(/^# (.*$)/gm, '<h1>$1</h1>')
|
|
.replace(/^## (.*$)/gm, '<h2>$1</h2>')
|
|
.replace(/^### (.*$)/gm, '<h3>$1</h3>')
|
|
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
|
.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2">$1</a>')
|
|
.replace(/\n/g, '<br>');
|
|
|
|
document.getElementById('markdown-preview').innerHTML = html;
|
|
}
|
|
|
|
document.getElementById('is_public').addEventListener('change', function() {
|
|
const isPublic = this.checked;
|
|
const isAccessibleCheckbox = document.getElementById('is_accessible');
|
|
const canCommentCheckbox = document.getElementById('can_comment');
|
|
const passwordSection = document.getElementById('password-section');
|
|
const passwordInput = document.getElementById('password');
|
|
|
|
|
|
if (isPublic) {
|
|
isAccessibleCheckbox.checked = true;
|
|
isAccessibleCheckbox.disabled = true;
|
|
canCommentCheckbox.disabled = false;
|
|
passwordSection.style.display = 'none';
|
|
passwordInput.disabled = true;
|
|
passwordInput.value = '';
|
|
} else {
|
|
isAccessibleCheckbox.disabled = false;
|
|
canCommentCheckbox.disabled = !isAccessibleCheckbox.checked;
|
|
|
|
|
|
if (isAccessibleCheckbox.checked) {
|
|
passwordSection.style.display = 'block';
|
|
passwordInput.disabled = false;
|
|
} else {
|
|
passwordSection.style.display = 'none';
|
|
passwordInput.disabled = true;
|
|
passwordInput.value = '';
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
document.getElementById('is_accessible').addEventListener('change', function() {
|
|
const isAccessible = this.checked;
|
|
const isPublic = document.getElementById('is_public').checked;
|
|
const canCommentCheckbox = document.getElementById('can_comment');
|
|
const passwordSection = document.getElementById('password-section');
|
|
const passwordInput = document.getElementById('password');
|
|
|
|
|
|
canCommentCheckbox.disabled = !isAccessible;
|
|
if (!isAccessible) {
|
|
canCommentCheckbox.checked = false;
|
|
}
|
|
|
|
|
|
if (!isPublic && isAccessible) {
|
|
passwordSection.style.display = 'block';
|
|
passwordInput.disabled = false;
|
|
} else {
|
|
passwordSection.style.display = 'none';
|
|
passwordInput.disabled = true;
|
|
passwordInput.value = '';
|
|
}
|
|
});
|
|
|
|
|
|
document.querySelector('input[name="thumbnail"]').addEventListener('change', function(e) {
|
|
const file = e.target.files[0];
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
reader.onload = function(event) {
|
|
const thumbnailImage = document.getElementById('thumbnail-image');
|
|
thumbnailImage.src = event.target.result;
|
|
thumbnailImage.style.display = 'block';
|
|
|
|
|
|
const previewContainer = thumbnailImage.parentElement;
|
|
const textElements = previewContainer.querySelectorAll('span');
|
|
textElements.forEach(el => el.style.display = 'none');
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
});
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const driveId = '<?php echo $drive_id; ?>';
|
|
const accessToken = '<?php echo $_SESSION['access_token'] ?? ''; ?>';
|
|
const thumbnailImage = document.getElementById('thumbnail-image');
|
|
const previewContainer = thumbnailImage.parentElement;
|
|
|
|
if (driveId && accessToken) {
|
|
const thumbnailName = `Scratch-Thumbnail-${driveId}.png`;
|
|
|
|
|
|
fetch(`https://www.googleapis.com/drive/v3/files?q=name='${encodeURIComponent(thumbnailName)}'`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${accessToken}`
|
|
}
|
|
})
|
|
.then(response => {
|
|
if (response.status === 401) {
|
|
handleAuthError();
|
|
return Promise.reject('認証エラー');
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
if (data.files && data.files.length > 0) {
|
|
const thumbnailId = data.files[0].id;
|
|
thumbnailImage.src = `https:
|
|
thumbnailImage.style.display = 'block';
|
|
|
|
|
|
const textElements = previewContainer.querySelectorAll('span');
|
|
textElements.forEach(el => el.style.display = 'none');
|
|
} else {
|
|
thumbnailImage.style.display = 'none';
|
|
|
|
const textElements = previewContainer.querySelectorAll('span');
|
|
textElements.forEach(el => el.style.display = 'block');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('サムネイル取得エラー:', error);
|
|
thumbnailImage.style.display = 'none';
|
|
|
|
const textElements = previewContainer.querySelectorAll('span');
|
|
textElements.forEach(el => el.style.display = 'block');
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
<?php else: ?>
|
|
<p>Googleドライブからプロジェクトを選択してください。</p>
|
|
<button class="btn" onclick="document.getElementById('driveModal').style.display='flex'">
|
|
Googleドライブから選択
|
|
</button>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<?php if ($drive_id && isset($_SESSION['access_token'])): ?>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const driveId = '<?php echo $drive_id; ?>';
|
|
const accessToken = '<?php echo $_SESSION['access_token']; ?>';
|
|
const thumbnailImage = document.getElementById('thumbnail-image');
|
|
|
|
|
|
fetch(`https://www.googleapis.com/drive/v3/files/${driveId}?fields=name`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${accessToken}`
|
|
}
|
|
})
|
|
.then(response => {
|
|
if (response.status === 401) {
|
|
handleAuthError();
|
|
return Promise.reject('認証エラー');
|
|
}
|
|
if (!response.ok) {
|
|
throw new Error('ファイル情報の取得に失敗しました');
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(fileData => {
|
|
const fileName = fileData.name.replace('.s4s.txt', '');
|
|
const thumbnailName = `Scratch-Thumbnail-${driveId}.png`;
|
|
|
|
console.log(`Searching for thumbnail: ${thumbnailName}`);
|
|
|
|
// 2. 取得したファイル名を使ってサムネイルを検索
|
|
return fetch(`https://www.googleapis.com/drive/v3/files?q=name='${encodeURIComponent(thumbnailName)}'`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${accessToken}`
|
|
}
|
|
});
|
|
})
|
|
.then(response => {
|
|
if (response.status === 401) {
|
|
handleAuthError();
|
|
return Promise.reject('認証エラー');
|
|
}
|
|
if (!response.ok) {
|
|
throw new Error('サムネイル検索に失敗しました');
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
if (data.files && data.files.length > 0) {
|
|
const thumbnailId = data.files[0].id;
|
|
console.log(`Found thumbnail ID: ${thumbnailId}`);
|
|
thumbnailImage.src = `https:
|
|
} else {
|
|
console.log('サムネイルが見つかりませんでした');
|
|
thumbnailImage.src = 'https://via.placeholder.com/300x200?text=No+Thumbnail';
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('エラーが発生しました:', error);
|
|
if (error !== '認証エラー') {
|
|
thumbnailImage.src = 'https://via.placeholder.com/300x200?text=Error+Loading+Thumbnail';
|
|
}
|
|
});
|
|
});
|
|
|
|
function handleAuthError() {
|
|
|
|
fetch('logout.php', {
|
|
method: 'POST',
|
|
credentials: 'same-origin'
|
|
})
|
|
.then(() => {
|
|
|
|
window.location.href = '/login';
|
|
})
|
|
.catch(err => {
|
|
console.error('ログアウト処理中にエラーが発生しました:', err);
|
|
window.location.href = '/login';
|
|
});
|
|
}
|
|
</script>
|
|
<?php elseif ($drive_id): ?>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
console.error('Googleアクセストークンがありません');
|
|
document.getElementById('thumbnail-image').src = 'https://via.placeholder.com/300x200?text=Token+Missing';
|
|
});
|
|
</script>
|
|
<?php endif; ?>
|
|
|
|
<!-- Marked.js for markdown rendering -->
|
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
<script>
|
|
|
|
function updateMarkdownPreview() {
|
|
const markdown = document.getElementById('description').value;
|
|
document.getElementById('markdown-preview').innerHTML = marked.parse(markdown);
|
|
}
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
updateMarkdownPreview();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |