| <?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> |