sing-song-player-3vx0f39v / savefile.html
soiz1's picture
Create savefile.html
f573274 verified
raw
history blame
5.36 kB
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>URL → showSaveFilePicker でダウンロード</title>
<style>
body {font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; padding: 2rem; background:#f7fafc; color:#0f172a}
.card {background:#fff; padding:1.25rem; border-radius:10px; box-shadow:0 6px 18px rgba(2,6,23,0.06); max-width:720px}
label {display:block; font-weight:600; margin-bottom:0.25rem}
input[type=text] {width:100%; padding:0.5rem; border:1px solid #e6edf3; border-radius:6px}
button {margin-top:0.75rem; padding:0.6rem 1rem; border-radius:8px; border:0; background:#2563eb; color:white; font-weight:600}
pre {background:#0b1220; color:#e6f2ff; padding:0.75rem; border-radius:6px; overflow:auto}
.note {margin-top:0.75rem; color:#374151}
</style>
</head>
<body>
<div class="card">
<h1>URL を入力してファイルを保存(showSaveFilePicker)</h1>
<p>指定した URL を fetch して、ブラウザのファイル保存ダイアログで保存します。<br>(対応ブラウザ:Chromium ベースの最新ブラウザなど)</p>
<label for="url">ダウンロードする URL</label>
<input id="url" type="text" placeholder="https://example.com/path/to/file.png" value="" />
<label for="suggest">保存時のファイル名(空欄なら URL から推測)</label>
<input id="suggest" type="text" placeholder="自動推測されます(例: file.png)" />
<div>
<button id="ok">OK — ダウンロード開始</button>
</div>
<div class="note" id="status" aria-live="polite"></div>
<h3>使い方メモ</h3>
<ul>
<li>クロスオリジン(CORS)が許可されていないサーバーからは fetch に失敗します。サーバー側で Access-Control-Allow-Origin を設定するか、プロキシ経由で取得してください。</li>
<li>showSaveFilePicker は一部のブラウザでしか動作しません。存在しない場合は代替処理(a.download)にフォールバックします。</li>
</ul>
</div>
<script>
function setStatus(text) {
document.getElementById('status').textContent = text;
}
async function downloadFromUrl(url, suggestedName) {
setStatus('fetch を開始します…');
const response = await fetch(url);
if (!response.ok) throw new Error('HTTP エラー: ' + response.status);
const blob = await response.blob();
// ヘッダからファイル名を取得(もしあれば)
let filename = suggestedName && suggestedName.trim() ? suggestedName.trim() : null;
const cd = response.headers.get('content-disposition');
if (!filename && cd) {
const match = cd.match(/filename\*?=(?:UTF-8'')?"?([^";]+)"?/i);
if (match) filename = decodeURIComponent(match[1]);
}
// URL から推測
if (!filename) {
try {
const u = new URL(url);
const name = u.pathname.split('/').filter(Boolean).pop();
if (name) filename = name;
} catch (e) {
// noop
}
}
if (!filename) filename = 'download';
// showSaveFilePicker が使えるか
if (window.showSaveFilePicker) {
setStatus('ファイル保存ダイアログを開いています…');
const opts = {suggestedName: filename};
// types を指定すると拡張子選択などが有効になりますが、省略しても動作します。
const handle = await window.showSaveFilePicker(opts);
const writable = await handle.createWritable();
// Blob 全体を書き込む
await writable.write(blob);
await writable.close();
setStatus('保存完了: ' + handle.name);
} else {
// フォールバック: a.download を使う
setStatus('showSaveFilePicker が使えないため、フォールバックでダウンロードします…');
const a = document.createElement('a');
const objectUrl = URL.createObjectURL(blob);
a.href = objectUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(objectUrl), 5000);
setStatus('ダウンロード開始(フォールバック): ' + filename);
}
}
document.getElementById('ok').addEventListener('click', async () => {
const btn = document.getElementById('ok');
const url = document.getElementById('url').value.trim();
const suggested = document.getElementById('suggest').value.trim();
if (!url) return setStatus('URL を入力してください。');
btn.disabled = true;
try {
await downloadFromUrl(url, suggested);
} catch (err) {
console.error(err);
setStatus('エラー: ' + (err.message || err));
// CORS の可能性を指摘
if (err instanceof TypeError) {
setStatus((document.getElementById('status').textContent || '') + '\n注意: CORS によるブロックやネットワークエラーの可能性があります。');
}
} finally {
btn.disabled = false;
}
});
</script>
</body>
</html>