scratch-zip / index.html
soiz1's picture
Update index.html
5b07a6f verified
<!DOCTYPE html>
<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>