Spaces:
Sleeping
Sleeping
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>File Viewer</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; | |
} | |
.breadcrumb a { | |
color: #3498db; | |
text-decoration: none; | |
} | |
.search-box { | |
margin-bottom: 20px; | |
display: flex; | |
gap: 10px; | |
} | |
.search-box input { | |
flex: 1; | |
padding: 8px; | |
border: 1px solid #ddd; | |
border-radius: 4px; | |
font-size: 16px; | |
} | |
.search-options { | |
display: flex; | |
align-items: center; | |
gap: 10px; | |
} | |
.search-options label { | |
white-space: nowrap; | |
} | |
.search-options select { | |
padding: 8px; | |
border: 1px solid #ddd; | |
border-radius: 4px; | |
} | |
.file-list { | |
list-style: none; | |
padding: 0; | |
} | |
.file-item, .folder-item { | |
padding: 10px; | |
border-bottom: 1px solid #eee; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
} | |
.file-item:hover, .folder-item:hover { | |
background-color: #f9f9f9; | |
} | |
.file-actions a, .folder-actions a { | |
margin-left: 10px; | |
color: white; | |
background-color: #3498db; | |
padding: 5px 10px; | |
border-radius: 4px; | |
text-decoration: none; | |
font-size: 14px; | |
} | |
.file-actions a:hover, .folder-actions a: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: 20px; | |
border-radius: 8px; | |
width: 80%; | |
max-width: 800px; | |
text-align: center; | |
box-shadow: 0 5px 15px rgba(0,0,0,0.3); | |
} | |
.modal-content img, | |
.modal-content video, | |
.modal-content audio { | |
max-width: 100%; | |
max-height: 70vh; | |
border-radius: 4px; | |
} | |
.pdf-container { | |
width: 100%; | |
height: 70vh; | |
border: none; | |
} | |
.close { | |
position: absolute; | |
top: 20px; | |
right: 30px; | |
color: #fff; | |
font-size: 28px; | |
font-weight: bold; | |
cursor: pointer; | |
z-index: 1001; | |
} | |
.close:hover { | |
color: #ccc; | |
} | |
.no-items { | |
padding: 20px; | |
text-align: center; | |
color: #666; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<h1>File Viewer</h1> | |
<div class="breadcrumb" id="breadcrumb"> | |
<a href="#" onclick="loadDirectory('')">Home</a> | |
</div> | |
<div class="search-box"> | |
<input type="text" id="searchInput" placeholder="Search files..." oninput="filterFiles()"> | |
<div class="search-options"> | |
<label for="searchScope">Search in:</label> | |
<select id="searchScope"> | |
<option value="current">Current folder</option> | |
</select> | |
</div> | |
</div> | |
<ul class="file-list" id="fileList"></ul> | |
</div> | |
<div id="previewModal" class="modal" style="display:none;"> | |
<span class="close" onclick="closeModal()">×</span> | |
<div id="modalContent" class="modal-content"></div> | |
</div> | |
<script> | |
let currentDir = ''; | |
let currentFiles = []; | |
let currentDirs = []; | |
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); | |
}); | |
}); | |
} | |
// ディレクトリを読み込む関数 | |
function loadDirectory(dir = '') { | |
fetch(`/get_file_json?dir=${encodeURIComponent(dir)}`) | |
.then(response => response.json()) | |
.then(data => { | |
currentDir = dir; | |
currentFiles = data.files || []; | |
currentDirs = data.dirs || []; | |
updateBreadcrumb(data.current_dir); | |
renderFileList(currentFiles, currentDirs); | |
// URLを更新 | |
const newUrl = new URL(window.location); | |
newUrl.searchParams.set('dir', dir); | |
window.history.pushState({}, '', newUrl); | |
}) | |
.catch(error => { | |
console.error('Error:', error); | |
// オフライン時はキャッシュから読み込む | |
caches.match(`/get_file_json?dir=${encodeURIComponent(dir)}`) | |
.then(response => { | |
if (response) { | |
return response.json(); | |
} | |
return { files: [], dirs: [], current_dir: dir }; | |
}) | |
.then(data => { | |
currentDir = dir; | |
currentFiles = data.files || []; | |
currentDirs = data.dirs || []; | |
updateBreadcrumb(data.current_dir); | |
renderFileList(currentFiles, currentDirs); | |
}); | |
}); | |
} | |
// ファイルをフィルタリングする関数 | |
function filterFiles() { | |
const searchTerm = document.getElementById('searchInput').value.toLowerCase(); | |
if (!searchTerm) { | |
renderFileList(currentFiles, currentDirs); | |
return; | |
} | |
const filteredFiles = currentFiles.filter(file => | |
file.name.toLowerCase().includes(searchTerm) | |
); | |
const filteredDirs = currentDirs.filter(dir => | |
dir.name.toLowerCase().includes(searchTerm) | |
); | |
renderFileList(filteredFiles, filteredDirs); | |
} | |
// パンくずリストを更新 | |
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) { | |
const fileList = document.getElementById('fileList'); | |
if (files.length === 0 && dirs.length === 0) { | |
fileList.innerHTML = '<li class="no-items">No files or directories found</li>'; | |
return; | |
} | |
let html = ''; | |
// ディレクトリを表示 | |
dirs.forEach(dir => { | |
html += ` | |
<li class="folder-item"> | |
<div> | |
<strong>${dir.name}/</strong> | |
</div> | |
<div class="folder-actions"> | |
<a href="#" onclick="loadDirectory('${dir.path}')">Open</a> | |
</div> | |
</li> | |
`; | |
}); | |
// ファイルを表示 | |
files.forEach(file => { | |
html += ` | |
<li class="file-item"> | |
<div> | |
<strong>${file.name}</strong> | |
</div> | |
<div class="file-actions"> | |
<a href="#" onclick="previewFile('${file.path}', '${file.name}')">Preview</a> | |
<a href="/download/${file.path}" download>Download</a> | |
</div> | |
</li> | |
`; | |
}); | |
fileList.innerHTML = html; | |
} | |
// プレビュー機能 | |
function previewFile(filepath, filename) { | |
const modal = document.getElementById('previewModal'); | |
const modalContent = document.getElementById('modalContent'); | |
const ext = filename.split('.').pop().toLowerCase(); | |
if (['jpg', 'jpeg', 'png', 'gif'].includes(ext)) { | |
modalContent.innerHTML = `<img src="/shared_files/${filepath}">`; | |
} else if (['mp4', 'webm', 'ogg'].includes(ext)) { | |
modalContent.innerHTML = ` | |
<video controls autoplay> | |
<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 class="pdf-container" | |
src="https://mozilla.github.io/pdf.js/web/viewer.html?file=${encodeURIComponent(window.location.origin + '/shared_files/' + filepath)}"> | |
</iframe> | |
`; | |
} else { | |
modalContent.innerHTML = ` | |
<p>Preview not available. <a href="/download/${filepath}">Download</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; | |
}); | |
} | |
// 初期読み込み | |
window.addEventListener('load', () => { | |
const params = new URLSearchParams(window.location.search); | |
const dir = params.get('dir') || ''; | |
loadDirectory(dir); | |
}); | |
// ブラウザの戻る/進むボタン対応 | |
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(); | |
} | |
}; | |
</script> | |
</body> | |
</html> |