Spaces:
Running
Running
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Zipファイルアップローダー</title> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js"></script> | |
<style> | |
body { | |
font-family: sans-serif; | |
background-color: #f4f4f9; | |
margin: 0; | |
padding: 20px; | |
} | |
h1 { | |
text-align: center; | |
color: #333; | |
} | |
#fileList { | |
display: grid; | |
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); | |
gap: 15px; | |
margin-top: 20px; | |
justify-items: center; | |
} | |
.file-tile img { | |
max-width: 200px; | |
max-height: 150px; | |
object-fit: contain; | |
border-radius: 5px; /* 画像の角を丸める */ | |
} | |
.file-name { | |
margin-top: 10px; | |
font-weight: bold; | |
word-break: break-word; | |
color: #333; | |
} | |
.file-tile span { | |
display: block; | |
margin-top: 5px; | |
color: #777; | |
font-size: 14px; | |
} | |
.file-actions { | |
margin-top: 15px; | |
display: flex; | |
justify-content: space-between; | |
width: 100%; | |
} | |
.file-actions button { | |
padding: 8px 15px; | |
background-color: #007bff; | |
border: none; | |
color: white; | |
border-radius: 5px; | |
cursor: pointer; | |
font-size: 14px; | |
} | |
.file-actions button:hover { | |
background-color: #0056b3; | |
} | |
.file-actions button:active { | |
background-color: #004085; | |
} | |
#modal { | |
display: none; | |
position: fixed; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background-color: rgba(0, 0, 0, 0.8); | |
align-items: center; | |
justify-content: center; | |
color: white; | |
} | |
#modalContent { | |
background-color: #333; | |
padding: 20px; | |
border-radius: 5px; | |
width: 80%; | |
max-width: 500px; | |
text-align: center; | |
} | |
#modalContent textarea { | |
width: 100%; | |
height: 200px; | |
color: black; | |
border-radius: 5px; | |
padding: 10px; | |
font-size: 14px; | |
border: 1px solid #ccc; | |
} | |
#modalContent button { | |
margin-top: 10px; | |
padding: 8px 15px; | |
background-color: #28a745; | |
border: none; | |
color: white; | |
border-radius: 5px; | |
cursor: pointer; | |
} | |
#modalContent button:hover { | |
background-color: #218838; | |
} | |
#modalContent button:active { | |
background-color: #1e7e34; | |
} | |
#stats { | |
margin-top: 15px; | |
font-weight: bold; | |
text-align: center; | |
font-size: 16px; | |
} | |
#deleteConditions { | |
margin-top: 20px; | |
padding: 15px; | |
background-color: #fff; | |
border: 1px solid #ddd; | |
border-radius: 10px; | |
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05); | |
} | |
#deleteConditions h3 { | |
margin-top: 0; | |
font-size: 18px; | |
} | |
select, input[type="text"] { | |
padding: 10px; | |
margin-right: 10px; | |
border-radius: 5px; | |
border: 1px solid #ccc; | |
font-size: 14px; | |
} | |
button { | |
padding: 10px 20px; | |
background-color: #007bff; | |
color: white; | |
border: none; | |
border-radius: 5px; | |
cursor: pointer; | |
} | |
button:hover { | |
background-color: #0056b3; | |
} | |
button:active { | |
background-color: #004085; | |
} | |
#fileList li { | |
display: flex; | |
flex-direction: column; | |
justify-content: space-between; | |
align-items: center; | |
background-color: #fff; | |
border: 1px solid #ddd; | |
border-radius: 10px; | |
padding: 15px; | |
text-align: center; | |
width: 100%; | |
min-height: 300px; | |
box-sizing: border-box; | |
transition: all 0.3s ease; | |
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); | |
} | |
#fileList li:hover { | |
transform: translateY(-5px); | |
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); | |
} | |
/* 初期状態では非表示に */ | |
.loader { | |
display: none; | |
width: 100px; | |
aspect-ratio: 1; | |
padding: 10px; | |
box-sizing: border-box; | |
display: grid; | |
background: #fff; | |
filter: blur(5px) contrast(10) hue-rotate(300deg); | |
mix-blend-mode: darken; | |
} | |
.loader:before, | |
.loader:after{ | |
content: ""; | |
grid-area: 1/1; | |
width: 40px; | |
height: 40px; | |
background: #ffff00; | |
animation: l7 2s infinite; | |
} | |
.loader:after{ | |
animation-delay: -1s; | |
} | |
@keyframes l7{ | |
0% {transform: translate( 0,0)} | |
25% {transform: translate(100%,0)} | |
50% {transform: translate(100%,100%)} | |
75% {transform: translate( 0,100%)} | |
100% {transform: translate( 0,0)} | |
} | |
</style> | |
</head> | |
<body> | |
<h1>scratch ファイルの内容を表示・編集</h1> | |
<input type="file" id="zipInput" /> | |
<button id="downloadZip" style="display: none;">ダウンロード</button> | |
<!-- 統計表示 --> | |
<div id="stats"></div> | |
<!-- ソート条件選択 --> | |
<!-- 削除条件選択 --> | |
<div id="deleteConditions"> | |
<h3>削除条件</h3> | |
<select id="targetType"> | |
<option value="filename-exact">ファイル名(全体一致)</option> | |
<option value="filename">ファイル名</option> | |
<option value="extension">拡張子</option> | |
</select> | |
<select id="matchType"> | |
<option value="includes">含む</option> | |
<option value="startsWith">で始まる</option> | |
<option value="endsWith">で終わる</option> | |
<option value="regex">正規表現</option> | |
</select> | |
<input type="text" id="deleteInput" placeholder="条件を入力"> | |
<button onclick="deleteByAdvancedCondition()">削除</button> | |
<div id="sortControls"> | |
<h3>並び替え</h3> | |
<select id="sortType"> | |
<option value="extension">拡張子順(初期)</option> | |
<option value="filename">ファイル名順</option> | |
<option value="size">ファイルサイズ順</option> | |
</select> | |
</div> | |
</div> | |
<ul id="fileList"></ul> | |
<!-- ローダーのHTML --> | |
<div class="loader" style="display: none;"></div> | |
<!-- モーダル --> | |
<div id="modal"> | |
<div id="modalContent"> | |
<h3>ファイルを編集</h3> | |
<textarea id="editor"></textarea> | |
<button id="okButton">OK</button> | |
<button id="cancelButton">キャンセル</button> | |
</div> | |
</div> | |
<script> | |
let zip = new JSZip(); | |
let files = []; | |
let currentFileName = null; | |
let zipFileName = "edited.zip"; | |
document.getElementById("zipInput").addEventListener("change", async (event) => { | |
const file = event.target.files[0]; | |
if (file) { | |
zipFileName = file.name; | |
try { | |
zip = await JSZip.loadAsync(file); | |
files = Object.keys(zip.files); | |
displayFileList(); | |
updateStats(); | |
} catch (error) { | |
alert("ZIPファイルの読み込み中にエラーが発生しました: " + error); | |
} | |
} | |
}); | |
function deleteByAdvancedCondition() { | |
const type = document.getElementById("targetType").value; | |
const match = document.getElementById("matchType").value; | |
const keyword = document.getElementById("deleteInput").value.trim(); | |
if (!keyword) { | |
alert("条件を入力してください"); | |
return; | |
} | |
let shouldDelete; | |
if (match === "regex") { | |
try { | |
const regex = new RegExp(keyword); | |
shouldDelete = name => { | |
const target = extractTarget(name, type); | |
return regex.test(target); | |
}; | |
} catch (e) { | |
alert("正規表現エラー: " + e.message); | |
return; | |
} | |
} else { | |
shouldDelete = name => { | |
const target = extractTarget(name, type); | |
if (match === "includes") return target.includes(keyword); | |
if (match === "startsWith") return target.startsWith(keyword); | |
if (match === "endsWith") return target.endsWith(keyword); | |
return false; | |
}; | |
} | |
files.forEach(file => { | |
if (shouldDelete(file)) { | |
zip.remove(file); | |
} | |
}); | |
files = Object.keys(zip.files); | |
displayFileList(); | |
updateStats(); | |
alert("条件に一致するファイルを削除しました"); | |
} | |
// ファイル名や拡張子から比較対象の文字列を抽出 | |
function extractTarget(name, type) { | |
if (type === "filename-exact") return name; | |
if (type === "filename") return name.split('/').pop().split('.').slice(0, -1).join('.'); | |
if (type === "extension") return name.split('.').pop().toLowerCase(); | |
return name; | |
} | |
async function displayFileList() { | |
const fileList = document.getElementById("fileList"); | |
fileList.innerHTML = ""; | |
const sortedFiles = sortFiles(); | |
for (const fileName of sortedFiles) { | |
const file = zip.file(fileName); | |
const fileExtension = fileName.split('.').pop().toLowerCase(); | |
const fileSize = file?._data.uncompressedSize || 0; | |
const listItem = document.createElement("li"); | |
listItem.className = "file-item"; | |
// listItem の定義後 | |
listItem.setAttribute("data-ext", fileExtension); | |
if (!document.getElementById(`ext-${fileExtension}`)) { | |
listItem.id = `ext-${fileExtension}`; | |
} | |
const nameSpan = document.createElement("div"); | |
nameSpan.className = "file-name"; | |
nameSpan.textContent = `${fileName} (${(fileSize / 1024).toFixed(1)} KB)`; | |
listItem.appendChild(nameSpan); | |
if (['jpg', 'jpeg', 'png', 'gif'].includes(fileExtension)) { | |
const img = document.createElement("img"); | |
const content = await file.async("blob"); | |
img.src = URL.createObjectURL(content); | |
img.onload = () => { | |
const width = 200; | |
const height = (img.height / img.width) * width; | |
img.width = width; | |
img.height = height; | |
}; | |
listItem.appendChild(img); | |
} else if (fileExtension === 'svg') { | |
const img = document.createElement("img"); | |
const content = await file.async("text"); | |
const imgDataUrl = await svgToBase64PNG(content); | |
img.src = imgDataUrl; | |
img.onload = () => { | |
const width = 200; | |
const height = (img.height / img.width) * width; | |
img.width = width; | |
img.height = height; | |
}; | |
listItem.appendChild(img); | |
} else if (['mp3', 'wav', 'ogg'].includes(fileExtension)) { | |
const audio = document.createElement("audio"); | |
audio.controls = true; | |
const content = await file.async("blob"); | |
audio.src = URL.createObjectURL(content); | |
listItem.appendChild(audio); | |
} else { | |
const span = document.createElement("span"); | |
span.textContent = "プレビュー不可"; | |
listItem.appendChild(span); | |
} | |
const deleteButton = document.createElement("button"); | |
deleteButton.textContent = "Delete"; | |
deleteButton.onclick = () => deleteFile(fileName); | |
const replaceButton = document.createElement("button"); | |
replaceButton.textContent = "Replace File"; | |
replaceButton.onclick = () => replaceFile(fileName); | |
const editButton = document.createElement("button"); | |
editButton.textContent = "Edit File"; | |
editButton.onclick = () => editFile(fileName); | |
const actionBox = document.createElement("div"); | |
actionBox.className = "file-actions"; | |
actionBox.appendChild(deleteButton); | |
actionBox.appendChild(replaceButton); | |
actionBox.appendChild(editButton); | |
listItem.appendChild(actionBox); | |
fileList.appendChild(listItem); | |
}; | |
document.getElementById("downloadZip").style.display = files.length ? "inline" : "none"; | |
} | |
async function svgToBase64PNG(svgData) { | |
return new Promise((resolve, reject) => { | |
const canvas = document.createElement("canvas"); | |
const ctx = canvas.getContext("2d"); | |
const img = new Image(); | |
const svgBlob = new Blob([svgData], { type: "image/svg+xml" }); | |
const url = URL.createObjectURL(svgBlob); | |
img.onload = () => { | |
canvas.width = img.width; | |
canvas.height = img.height; | |
ctx.drawImage(img, 0, 0); | |
const pngDataUrl = canvas.toDataURL("image/png"); | |
resolve(pngDataUrl); | |
}; | |
img.onerror = reject; | |
img.src = url; | |
}); | |
} | |
function deleteFile(fileName) { | |
zip.remove(fileName); | |
files = files.filter(file => file !== fileName); | |
displayFileList(); | |
} | |
async function replaceFile(fileName) { | |
const input = document.createElement("input"); | |
input.type = "file"; | |
input.accept = "*/*"; | |
input.onchange = async (event) => { | |
const newFile = event.target.files[0]; | |
if (newFile) { | |
const content = await newFile.arrayBuffer(); | |
zip.file(fileName, content); | |
alert(fileName + " を置き換えました"); | |
displayFileList(); | |
} | |
}; | |
input.click(); | |
} | |
async function editFile(fileName) { | |
currentFileName = fileName; | |
const fileContent = await zip.file(fileName).async("text"); | |
document.getElementById("editor").value = fileContent; | |
document.getElementById("modal").style.display = "flex"; | |
} | |
document.getElementById("okButton").addEventListener("click", () => { | |
const newContent = document.getElementById("editor").value; | |
zip.file(currentFileName, newContent); | |
alert(currentFileName + " を更新しました"); | |
document.getElementById("modal").style.display = "none"; | |
}); | |
document.getElementById("cancelButton").addEventListener("click", () => { | |
document.getElementById("modal").style.display = "none"; | |
}); | |
document.getElementById("downloadZip").addEventListener("click", async () => { | |
const blob = await zip.generateAsync({ type: "blob" }); | |
// ローダーを表示 | |
document.querySelector(".loader").style.display = "block"; | |
try { | |
// ファイルのURLを作成 | |
const downloadLink = document.createElement("a"); | |
downloadLink.href = URL.createObjectURL(blob); | |
downloadLink.download = zipFileName; // ZIPファイル名を設定 | |
downloadLink.click(); // ダウンロードをトリガー | |
// ダウンロード完了後、ローダーを非表示 | |
setTimeout(() => { | |
document.querySelector(".loader").style.display = "none"; | |
}, 1000); // ローダーを1秒後に非表示 | |
} catch (error) { | |
alert("保存中にエラーが発生しました: " + error); | |
document.querySelector(".loader").style.display = "none"; // エラー発生時にローダーを非表示 | |
} | |
}); | |
function sortFiles() { | |
const sortType = document.getElementById("sortType").value; | |
return [...files].sort((a, b) => { | |
const fileA = zip.file(a); | |
const fileB = zip.file(b); | |
if (sortType === "filename") { | |
return a.localeCompare(b); | |
} else if (sortType === "size") { | |
return (fileA?._data.uncompressedSize || 0) - (fileB?._data.uncompressedSize || 0); | |
} else if (sortType === "extension") { | |
const extA = a.split('.').pop().toLowerCase(); | |
const extB = b.split('.').pop().toLowerCase(); | |
return extA.localeCompare(extB); | |
} | |
return 0; | |
}); | |
} | |
document.getElementById("sortType").addEventListener("change", displayFileList); | |
function deleteBySelectedMode() { | |
const mode = document.getElementById("deleteMode").value; | |
const value = document.getElementById("deleteInput").value.trim(); | |
if (!value) { | |
alert("削除条件を入力してください"); | |
return; | |
} | |
files.forEach(fileName => { | |
let shouldDelete = false; | |
if (mode === "filename" && fileName === value) { | |
shouldDelete = true; | |
} else if (mode === "extension" && fileName.endsWith("." + value)) { | |
shouldDelete = true; | |
} else if (mode === "regex") { | |
try { | |
const regex = new RegExp(value); | |
if (regex.test(fileName)) { | |
shouldDelete = true; | |
} | |
} catch (e) { | |
alert("正規表現が無効です: " + e.message); | |
return; | |
} | |
} | |
if (shouldDelete) { | |
zip.remove(fileName); | |
} | |
}); | |
files = Object.keys(zip.files); | |
displayFileList(); | |
updateStats(); | |
alert("選択したモードに基づきファイルを削除しました"); | |
} | |
function updateStats() { | |
const statsContainer = document.getElementById("stats"); | |
const extensionCounts = {}; | |
files.forEach(file => { | |
const ext = file.split('.').pop().toLowerCase(); | |
extensionCounts[ext] = (extensionCounts[ext] || 0) + 1; | |
}); | |
const extInfo = Object.entries(extensionCounts) | |
.map(([ext, count]) => `<a href="#ext-${ext}" class="ext-link">.${ext}: ${count}</a>`) | |
.join(", "); | |
statsContainer.innerHTML = `総ファイル数: ${files.length} | 拡張子別: ${extInfo}`; | |
document.querySelectorAll(".ext-link").forEach(link => { | |
link.addEventListener("click", (e) => { | |
e.preventDefault(); | |
const target = document.querySelector(link.getAttribute("href")); | |
if (target) { | |
target.scrollIntoView({ behavior: "smooth", block: "center" }); | |
} | |
}); | |
}); | |
} | |
</script> | |
</body> | |
</html> |