s4s-home / upload.php
soiz1's picture
Upload 14 files
240bcc5 verified
<?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';
// Googleログイン確認
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 = '';
}
// コメントデータをJSON形式で準備
$comment_data = [
'can_comment' => $can_comment,
'history' => $project ? json_decode($project['comment'], true)['history'] ?? [] : []
];
// ランダムな7桁のID生成 (重複確認付き)
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) {
// ファイルサイズチェック (10MB以下)
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);
// Google Driveにサムネイルをアップロード
$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'; // 常に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 {
// アクセス権限を削除 (本人のみアクセス可能)
// まず既存のanyone権限を取得
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'] ?? [];
// anyone権限を削除
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);
}
}
}
// Google Driveのアクセス権限設定
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 {
// アクセス権限を削除 (本人のみアクセス可能)
// まず既存のanyone権限を取得
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'] ?? [];
// anyone権限を削除
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: #4d97ff;
--secondary-color: #575e75;
--background-color: #f5f5f5;
--card-bg: #ffffff;
--text-color: #333333;
--error-color: #ff4444;
--success-color: #00c851;
}
* {
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: #f8f9fa;
border-radius: 5px;
}
.login-button {
background-color: #4285F4;
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: #3367D6;
}
.file-list {
margin-top: 20px;
border-top: 1px solid #eee;
padding-top: 20px;
}
.file-item {
display: flex;
align-items: center;
padding: 10px;
border-bottom: 1px solid #eee;
cursor: pointer;
}
.file-item:hover {
background-color: #f5f5f5;
}
.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 #ddd;
border-radius: 4px;
font-family: inherit;
font-size: 16px;
}
textarea {
min-height: 120px;
resize: vertical;
}
.thumbnail-preview {
width: 200px;
height: 150px;
background-color: #f0f0f0;
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: #3a7bd5;
}
.btn-secondary {
background-color: var(--secondary-color);
}
.btn-secondary:hover {
background-color: #424858;
}
.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: #f8f9fa;
border-radius: 5px;
}
.setting-section h2 {
margin-top: 0;
font-size: 18px;
color: var(--secondary-color);
}
.password-note {
font-size: 14px;
color: #666;
margin-top: 5px;
}
.preview-container {
border: 1px solid #ddd;
padding: 15px;
border-radius: 4px;
margin-top: 10px;
min-height: 100px;
background-color: #f9f9f9;
}
.tab-container {
display: flex;
margin-bottom: -1px;
}
.tab-button {
padding: 8px 16px;
background: #eee;
border: 1px solid #ddd;
border-bottom: none;
cursor: pointer;
border-radius: 4px 4px 0 0;
margin-right: 5px;
}
.tab-button.active {
background: #fff;
border-bottom: 1px solid #fff;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.file-input-label {
display: inline-block;
padding: 8px 16px;
background-color: #f0f0f0;
border-radius: 4px;
cursor: pointer;
margin-top: 5px;
}
.file-input-label:hover {
background-color: #e0e0e0;
}
</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://accounts.google.com/o/oauth2/auth?` +
`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) {
// 401 Unauthorized エラーの場合
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;
// ここでマークダウンをHTMLに変換
// 簡易的な変換 (実際にはmarked.jsなどのライブラリを使用するのが良い)
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://drive.google.com/thumbnail?id=${thumbnailId}&sz=w300`;
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');
// 1. まずドライブIDからファイル名を取得
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://drive.google.com/thumbnail?id=${thumbnailId}&sz=w300`;
} 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>