Spaces:
Sleeping
Sleeping
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Admin File Manager</title> | |
<style> | |
body { | |
font-family: Arial, sans-serif; | |
margin: 0; | |
padding: 20px; | |
background-color: #f5f5f5; | |
} | |
.container { | |
max-width: 1200px; | |
margin: 0 auto; | |
background-color: white; | |
padding: 20px; | |
border-radius: 8px; | |
box-shadow: 0 2px 10px rgba(0,0,0,0.1); | |
} | |
h1 { | |
color: #333; | |
border-bottom: 1px solid #eee; | |
padding-bottom: 10px; | |
} | |
.breadcrumb { | |
padding: 10px 0; | |
margin-bottom: 20px; | |
font-size: 16px; | |
} | |
.breadcrumb a { | |
color: #3498db; | |
text-decoration: none; | |
} | |
.search-box { | |
margin-bottom: 20px; | |
} | |
.search-box input { | |
width: 100%; | |
padding: 10px; | |
border: 1px solid #ddd; | |
border-radius: 4px; | |
font-size: 16px; | |
} | |
.file-list { | |
list-style: none; | |
padding: 0; | |
margin-top: 20px; | |
} | |
.file-item, .folder-item { | |
padding: 12px 15px; | |
border-bottom: 1px solid #eee; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
transition: background-color 0.2s; | |
} | |
.folder-item { | |
background-color: #f9f9f9; | |
} | |
.file-item:hover, .folder-item:hover { | |
background-color: #f0f0f0; | |
} | |
.item-info { | |
flex: 1; | |
display: flex; | |
align-items: center; | |
} | |
.item-icon { | |
margin-right: 10px; | |
font-size: 20px; | |
} | |
.folder-icon { | |
color: #ffb74d; | |
} | |
.file-icon { | |
color: #64b5f6; | |
} | |
.item-name { | |
font-weight: bold; | |
} | |
.item-path { | |
color: #666; | |
font-size: 0.9em; | |
margin-left: 10px; | |
} | |
.item-actions { | |
display: flex; | |
gap: 8px; | |
} | |
.action-btn { | |
padding: 6px 12px; | |
border-radius: 4px; | |
text-decoration: none; | |
font-size: 14px; | |
color: white; | |
cursor: pointer; | |
border: none; | |
} | |
.open-btn { | |
background-color: #4caf50; | |
} | |
.delete-btn { | |
background-color: #f44336; | |
} | |
.rename-btn { | |
background-color: #ff9800; | |
} | |
.move-btn { | |
background-color: #9c27b0; | |
} | |
.upload-form { | |
margin: 25px 0; | |
padding: 20px; | |
background-color: #f8f9fa; | |
border-radius: 8px; | |
border: 1px solid #eee; | |
} | |
.form-title { | |
margin-top: 0; | |
margin-bottom: 15px; | |
font-size: 18px; | |
} | |
.form-group { | |
margin-bottom: 15px; | |
} | |
.form-group label { | |
display: block; | |
margin-bottom: 5px; | |
font-weight: bold; | |
} | |
.form-control { | |
width: 100%; | |
padding: 10px; | |
border: 1px solid #ddd; | |
border-radius: 4px; | |
font-size: 16px; | |
} | |
.form-actions { | |
margin-top: 15px; | |
} | |
.btn { | |
padding: 10px 15px; | |
background-color: #3498db; | |
color: white; | |
border: none; | |
border-radius: 4px; | |
cursor: pointer; | |
font-size: 16px; | |
} | |
.btn:hover { | |
background-color: #2980b9; | |
} | |
.modal { | |
display: none; | |
position: fixed; | |
z-index: 1000; | |
left: 0; | |
top: 0; | |
width: 100%; | |
height: 100%; | |
overflow: auto; | |
background-color: rgba(0,0,0,0.7); | |
} | |
.modal-content { | |
background-color: white; | |
margin: 5% auto; | |
padding: 25px; | |
border-radius: 8px; | |
width: 80%; | |
max-width: 800px; | |
box-shadow: 0 5px 15px rgba(0,0,0,0.3); | |
} | |
.modal-title { | |
margin-top: 0; | |
} | |
.close { | |
color: #aaa; | |
float: right; | |
font-size: 28px; | |
font-weight: bold; | |
cursor: pointer; | |
} | |
.close:hover { | |
color: #333; | |
} | |
.dialog { | |
display: none; | |
position: fixed; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
background: white; | |
padding: 25px; | |
border-radius: 8px; | |
box-shadow: 0 0 20px rgba(0,0,0,0.2); | |
z-index: 1001; | |
width: 450px; | |
max-width: 90%; | |
} | |
.dialog-title { | |
margin-top: 0; | |
margin-bottom: 20px; | |
} | |
.dialog-footer { | |
margin-top: 20px; | |
display: flex; | |
justify-content: flex-end; | |
gap: 10px; | |
} | |
.btn-secondary { | |
background-color: #6c757d; | |
} | |
.no-items { | |
padding: 30px; | |
text-align: center; | |
color: #666; | |
font-size: 16px; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<h1>Admin File Manager</h1> | |
<div class="breadcrumb" id="breadcrumb"> | |
<a href="#" onclick="loadDirectory('')">Home</a> | |
</div> | |
<div class="search-box"> | |
<input type="text" id="searchInput" class="form-control" placeholder="ファイルまたはフォルダを検索..." oninput="filterFiles()"> | |
</div> | |
<form id="uploadForm" action="{{ url_for('upload_file') }}" method="post" enctype="multipart/form-data"> | |
<input type="hidden" name="dir" id="currentDir" value=""> | |
<div class="form-group"> | |
<input type="file" name="file" class="form-control" required> | |
</div> | |
<div class="form-actions"> | |
<button type="submit" class="btn">アップロード</button> | |
</div> | |
</form> | |
<div class="upload-form"> | |
<h3 class="form-title">フォルダを作成</h3> | |
<form id="createFolderForm" action="{{ url_for('create_folder') }}" method="post"> | |
<div class="form-group"> | |
<input type="text" name="folder_name" class="form-control" placeholder="フォルダ名を入力" required> | |
</div> | |
<div class="form-actions"> | |
<button type="submit" class="btn">作成</button> | |
</div> | |
</form> | |
</div> | |
<ul class="file-list" id="fileList"></ul> | |
</div> | |
<!-- プレビューモーダル --> | |
<div id="previewModal" class="modal"> | |
<div class="modal-content"> | |
<span class="close" onclick="closeModal()">×</span> | |
<div id="modalContent"></div> | |
<div id="modalCaption" style="margin-top: 15px; text-align: center;"></div> | |
</div> | |
</div> | |
<!-- リネームダイアログ --> | |
<div id="renameDialog" class="dialog"> | |
<h3 class="dialog-title">名前を変更</h3> | |
<form id="renameForm" method="post" action="{{ url_for('rename_file') }}"> | |
<input type="hidden" name="old_name" id="oldName"> | |
<div class="form-group"> | |
<label for="newName">新しい名前:</label> | |
<input type="text" name="new_name" id="newName" class="form-control" required> | |
</div> | |
<div class="dialog-footer"> | |
<button type="button" class="btn btn-secondary" onclick="closeDialog('renameDialog')">キャンセル</button> | |
<button type="submit" class="btn">変更</button> | |
</div> | |
</form> | |
</div> | |
<!-- 移動ダイアログ --> | |
<div id="moveDialog" class="dialog"> | |
<h3 class="dialog-title">移動</h3> | |
<form id="moveForm" method="post" action="{{ url_for('move_file') }}"> | |
<input type="hidden" name="filename" id="moveName"> | |
<div class="form-group"> | |
<label for="destination">移動先のパス:</label> | |
<input type="text" name="destination" id="destination" class="form-control" required> | |
</div> | |
<div class="dialog-footer"> | |
<button type="button" class="btn btn-secondary" onclick="closeDialog('moveDialog')">キャンセル</button> | |
<button type="submit" class="btn">移動</button> | |
</div> | |
</form> | |
</div> | |
<script> | |
let allFiles = []; | |
let allDirs = []; | |
let currentDir = ''; | |
// ページ読み込み時にディレクトリを読み込む | |
document.addEventListener('DOMContentLoaded', function() { | |
const params = new URLSearchParams(window.location.search); | |
const dir = params.get('dir') || ''; | |
loadDirectory(dir); | |
// フォームの送信をインターセプトしてリロードを防ぐ | |
document.getElementById('uploadForm').addEventListener('submit', function(e) { | |
e.preventDefault(); | |
uploadFile(this); | |
}); | |
document.getElementById('createFolderForm').addEventListener('submit', function(e) { | |
e.preventDefault(); | |
createFolder(this); | |
}); | |
}); | |
// ディレクトリを読み込む関数 | |
function loadDirectory(dir = '') { | |
document.getElementById('currentDir').value = dir; | |
fetch(`/get_file_json?dir=${encodeURIComponent(dir)}`) | |
.then(response => response.json()) | |
.then(data => { | |
if (data.error) { | |
alert(data.error); | |
return; | |
} | |
currentDir = dir; | |
allFiles = data.all_files || []; | |
allDirs = data.all_dirs || []; | |
updateBreadcrumb(data.current_dir); | |
renderFileList(data.files, data.dirs, data.current_dir); | |
// URLを更新 | |
const newUrl = new URL(window.location); | |
newUrl.searchParams.set('dir', dir); | |
window.history.pushState({}, '', newUrl); | |
}) | |
.catch(error => { | |
console.error('Error loading directory:', error); | |
alert('ディレクトリの読み込みに失敗しました。'); | |
}); | |
} | |
// ファイルをアップロード | |
function uploadFile(form) { | |
const formData = new FormData(form); | |
fetch(form.action, { | |
method: 'POST', | |
body: formData | |
}) | |
.then(response => { | |
if (response.ok) { | |
loadDirectory(currentDir); | |
form.reset(); | |
} else { | |
alert('ファイルのアップロードに失敗しました。'); | |
} | |
}) | |
.catch(error => { | |
console.error('Error:', error); | |
alert('ファイルのアップロード中にエラーが発生しました。'); | |
}); | |
} | |
// フォルダを作成 | |
function createFolder(form) { | |
const formData = new FormData(form); | |
fetch(form.action, { | |
method: 'POST', | |
body: formData | |
}) | |
.then(response => response.json()) | |
.then(data => { | |
if (data.success) { | |
// 変更: 作成したフォルダのあるディレクトリを再読み込み | |
const parentDir = data.folder_path.split('/').slice(0, -1).join('/'); | |
loadDirectory(parentDir); | |
form.reset(); | |
} else { | |
alert('フォルダ作成エラー: ' + (data.error || '不明なエラー')); | |
} | |
}) | |
.catch(error => { | |
console.error('Error:', error); | |
alert('フォルダ作成中にエラーが発生しました。'); | |
}); | |
} | |
// ファイルをフィルタリング | |
function filterFiles() { | |
const searchTerm = document.getElementById('searchInput').value.toLowerCase(); | |
if (!searchTerm) { | |
loadDirectory(currentDir); | |
return; | |
} | |
const filteredFiles = allFiles.filter(file => | |
file.name.toLowerCase().includes(searchTerm) || | |
file.path.toLowerCase().includes(searchTerm) | |
); | |
const filteredDirs = allDirs.filter(dir => | |
dir.name.toLowerCase().includes(searchTerm) || | |
dir.path.toLowerCase().includes(searchTerm) | |
); | |
renderFileList(filteredFiles, filteredDirs, currentDir); | |
} | |
// パンくずリストを更新 | |
function updateBreadcrumb(currentDir) { | |
const breadcrumb = document.getElementById('breadcrumb'); | |
let html = '<a href="#" onclick="loadDirectory(\'\')">Home</a>'; | |
let path = ''; | |
if (currentDir) { | |
currentDir.split('/').forEach(part => { | |
if (part) { | |
path = path ? `${path}/${part}` : part; | |
html += ` / <a href="#" onclick="loadDirectory('${path}')">${part}</a>`; | |
} | |
}); | |
} | |
breadcrumb.innerHTML = html; | |
} | |
// ファイルリストを表示 | |
function renderFileList(files, dirs, currentDir) { | |
const fileList = document.getElementById('fileList'); | |
if (files.length === 0 && dirs.length === 0) { | |
fileList.innerHTML = '<li class="no-items">ファイルまたはフォルダが見つかりません</li>'; | |
return; | |
} | |
let html = ''; | |
// フォルダを表示 | |
dirs.forEach(dir => { | |
html += ` | |
<li class="folder-item" data-path="${dir.path}"> | |
<div class="item-info"> | |
<span class="item-icon folder-icon">📁</span> | |
<span class="item-name">${dir.name}</span> | |
<span class="item-path">${dir.path}</span> | |
</div> | |
<div class="item-actions"> | |
<button class="action-btn open-btn" onclick="loadDirectory('${dir.path}')">開く</button> | |
<button class="action-btn rename-btn" onclick="showRenameDialog('${dir.name}', '${dir.path}')">名前変更</button> | |
<button class="action-btn delete-btn" onclick="deleteItem('${dir.path}')">削除</button> | |
<button class="action-btn move-btn" onclick="showMoveDialog('${dir.path}')">移動</button> | |
</div> | |
</li> | |
`; | |
}); | |
// ファイルを表示 | |
files.forEach(file => { | |
html += ` | |
<li class="file-item" data-path="${file.path}"> | |
<div class="item-info"> | |
<span class="item-icon file-icon">📄</span> | |
<span class="item-name">${file.name}</span> | |
<span class="item-path">${file.path}</span> | |
</div> | |
<div class="item-actions"> | |
<button class="action-btn open-btn" onclick="previewFile('${file.path}', '${file.name}')">開く</button> | |
<a href="/download/${file.path}" class="action-btn open-btn" download>ダウンロード</a> | |
<button class="action-btn rename-btn" onclick="showRenameDialog('${file.name}', '${file.path}')">名前変更</button> | |
<button class="action-btn delete-btn" onclick="deleteItem('${file.path}')">削除</button> | |
<button class="action-btn move-btn" onclick="showMoveDialog('${file.path}')">移動</button> | |
</div> | |
</li> | |
`; | |
}); | |
fileList.innerHTML = html; | |
} | |
// プレビューを表示 | |
function previewFile(filepath, filename) { | |
const modal = document.getElementById('previewModal'); | |
const modalContent = document.getElementById('modalContent'); | |
const modalCaption = document.getElementById('modalCaption'); | |
const ext = filename.split('.').pop().toLowerCase(); | |
modalCaption.textContent = `${filename} (${filepath})`; | |
if (['jpg', 'jpeg', 'png', 'gif'].includes(ext)) { | |
modalContent.innerHTML = `<img src="/shared_files/${filepath}" style="max-width:100%; max-height:70vh;">`; | |
} else if (['mp4', 'webm', 'ogg'].includes(ext)) { | |
modalContent.innerHTML = ` | |
<video controls autoplay style="max-width:100%; max-height:70vh;"> | |
<source src="/shared_files/${filepath}" type="video/${ext}"> | |
</video> | |
`; | |
} else if (['mp3', 'wav', 'ogg'].includes(ext)) { | |
modalContent.innerHTML = ` | |
<audio controls autoplay> | |
<source src="/shared_files/${filepath}" type="audio/${ext}"> | |
</audio> | |
`; | |
} else if (ext === 'pdf') { | |
modalContent.innerHTML = ` | |
<iframe src="https://mozilla.github.io/pdf.js/web/viewer.html?file=${encodeURIComponent(window.location.origin + '/shared_files/' + filepath)}" | |
style="width:100%; height:70vh; border:none;"></iframe> | |
`; | |
} else { | |
modalContent.innerHTML = ` | |
<p>プレビューできません。 <a href="/download/${filepath}">ダウンロード</a></p> | |
`; | |
} | |
modal.style.display = "block"; | |
} | |
// モーダルを閉じる | |
function closeModal() { | |
document.getElementById('previewModal').style.display = "none"; | |
const mediaElements = document.querySelectorAll('#modalContent video, #modalContent audio'); | |
mediaElements.forEach(el => { | |
el.pause(); | |
el.currentTime = 0; | |
}); | |
} | |
// ダイアログを表示 | |
function showRenameDialog(name, path) { | |
document.getElementById('oldName').value = path; | |
document.getElementById('newName').value = name; | |
document.getElementById('renameDialog').style.display = 'block'; | |
} | |
function showMoveDialog(path) { | |
document.getElementById('moveName').value = path; | |
document.getElementById('destination').value = ''; | |
document.getElementById('moveDialog').style.display = 'block'; | |
} | |
// ダイアログを閉じる | |
function closeDialog(id) { | |
document.getElementById(id).style.display = 'none'; | |
} | |
// アイテムを削除 | |
function deleteItem(path) { | |
if (confirm(`"${path}"を削除してもよろしいですか?`)) { | |
fetch(`/delete/${path}`, { | |
method: 'GET' | |
}) | |
.then(response => { | |
if (response.ok) { | |
loadDirectory(currentDir); | |
} else { | |
alert('削除に失敗しました。'); | |
} | |
}) | |
.catch(error => { | |
console.error('Error:', error); | |
alert('削除中にエラーが発生しました。'); | |
}); | |
} | |
} | |
// ブラウザの戻る/進むボタン対応 | |
window.addEventListener('popstate', () => { | |
const params = new URLSearchParams(window.location.search); | |
const dir = params.get('dir') || ''; | |
loadDirectory(dir); | |
}); | |
// モーダルの外側をクリックで閉じる | |
window.onclick = function(event) { | |
const modal = document.getElementById('previewModal'); | |
if (event.target === modal) { | |
closeModal(); | |
} | |
const renameDialog = document.getElementById('renameDialog'); | |
const moveDialog = document.getElementById('moveDialog'); | |
if (event.target === renameDialog) { | |
closeDialog('renameDialog'); | |
} | |
if (event.target === moveDialog) { | |
closeDialog('moveDialog'); | |
} | |
}; | |
// 移動処理の修正 | |
function moveItem(path) { | |
const destination = document.getElementById('destination').value; | |
if (!destination) { | |
alert('移動先を指定してください'); | |
return; | |
} | |
const formData = new FormData(); | |
formData.append('filename', path); | |
formData.append('destination', destination); | |
fetch("{{ url_for('move_file') }}", { | |
method: 'POST', | |
body: formData | |
}) | |
.then(response => { | |
if (response.redirected) { | |
window.location.href = response.url; | |
} else { | |
return response.json(); | |
} | |
}) | |
.then(data => { | |
if (data && !data.success) { | |
alert('移動エラー: ' + (data.error || '不明なエラー')); | |
} | |
}) | |
.catch(error => { | |
console.error('Error:', error); | |
alert('移動中にエラーが発生しました。'); | |
}); | |
} | |
if ('serviceWorker' in navigator) { | |
window.addEventListener('load', function() { | |
navigator.serviceWorker.register('/sw.js') | |
.then(function(registration) { | |
console.log('ServiceWorker registration successful with scope: ', registration.scope); | |
// 定期的にキャッシュを更新 | |
setInterval(() => { | |
registration.update(); | |
}, 60 * 60 * 1000); // 1時間ごとに更新 | |
}) | |
.catch(function(err) { | |
console.log('ServiceWorker registration failed: ', err); | |
}); | |
}); | |
} | |
</script> | |
</body> | |
</html> |