content_copyaddcompare_arrowsopen_in_full
(function () {
const BUTTON_ID = "listendltool_download";
const STORAGE_KEY = "transcript_selection";
const SORT_ORDER_KEY = "transcript_sort_order"; // ソート順序を保存するためのキー
const DOWNLOAD_CONTAINER_ID = "listendltool_download_container"; // ダウンロードボタンとスピナーを囲むコンテナのID
const CLEAR_STORAGE_BUTTON_ID = "listendltool_clear_storage"; // ローカルストレージをクリアするボタンのID
function addCheckBoxes(params) {
if (isMyPodcast(window.location.href.split("?")[0])) {
// ダウンロードボタンとスピナーを囲むコンテナを作成
let downloadContainer = document.createElement("div");
downloadContainer.id = DOWNLOAD_CONTAINER_ID;
document.querySelector("main").appendChild(downloadContainer);
// 自分が管理しているポッドキャストなら、チェックボックスと一括ダウンロードボタンを作成
let button = document.createElement("button");
button.textContent = "文字起こしの一括ダウンロード";
button.addEventListener("click", () => do_download());
button.id = BUTTON_ID;
downloadContainer.appendChild(button);
// スピナーの追加
let sortSelect = document.createElement("select");
sortSelect.id = "listendltool_sort_order";
let optionDesc = document.createElement("option");
optionDesc.value = "desc";
optionDesc.text = "降順";
let optionAsc = document.createElement("option");
optionAsc.value = "asc";
optionAsc.text = "昇順";
sortSelect.appendChild(optionDesc);
sortSelect.appendChild(optionAsc);
sortSelect.addEventListener("change", handleSortOrderChange);
downloadContainer.appendChild(sortSelect);
// ローカルストレージをクリアするボタンを追加
let clearStorageButton = document.createElement("button");
clearStorageButton.id = CLEAR_STORAGE_BUTTON_ID;
clearStorageButton.textContent = "選択をクリア";
clearStorageButton.addEventListener("click", clearLocalStorage);
downloadContainer.appendChild(clearStorageButton);
// ソート順序を復元
restoreSortOrder(sortSelect);
Array.from(document.querySelectorAll(".playable-episode")).forEach(e => {
let check = document.createElement("input");
check.type = "checkbox";
check.id = `check_${e.dataset.episodeId}`;
check.className = "transcript-checkbox";
check.addEventListener("change", handleCheckboxChange); // イベントリスナーを更新
e.querySelector("h2").insertBefore(check, e.querySelector("h2 > a"));
// ローカルストレージから状態を復元
restoreCheckboxState(check);
});
}
}
function dateToStr(date, separator = "") {
return date.getFullYear() + separator +
String(date.getMonth() + 1).padStart(2, "0") + separator +
String(date.getDate()).padStart(2, "0");
}
function updateButtonVisibility() {
const button = document.getElementById(BUTTON_ID);
if (button) {
const container = document.getElementById(DOWNLOAD_CONTAINER_ID);
const hasData = Object.keys(loadStorageData()).length > 0;
// opacityを制御
container.style.opacity = hasData ? 1 : 0;
// displayを制御。opacityのトランジション終了後に設定
if (hasData) {
container.style.display = "flex";
} else {
// transitionend イベントでdisplayをnoneに切り替える
container.addEventListener('transitionend', function onTransitionEnd() {
container.style.display = "none";
container.removeEventListener('transitionend', onTransitionEnd);
});
}
}
}
// チェックボックスの状態変更時にローカルストレージに保存
function handleCheckboxChange(event) {
const checkbox = event.target;
saveCheckboxState(checkbox);
updateButtonVisibility();
}
// チェックボックスの状態をローカルストレージに保存
function saveCheckboxState(checkbox) {
const storageData = loadStorageData();
const parent = checkbox.parentElement;
const summaryElement = parent.parentElement?.parentElement?.querySelector("p");
const summary = summaryElement ? summaryElement.textContent.trim() : "概要なし";
const anchor = parent.querySelector("a");
if (!anchor) return;
const title = anchor.textContent.trim();
const url = anchor.href;
// 配信日を取得
let dateDiv = summaryElement?.previousElementSibling;
let rawDate = dateDiv ? dateDiv.childNodes[0].textContent.trim() : null;
let formattedEpisodeDate = rawDate ? dateToStr(new Date(rawDate), "-") : "日付不明";
if (checkbox.checked) {
storageData[checkbox.id] = { summary, title, url, date: formattedEpisodeDate };
} else {
delete storageData[checkbox.id];
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(storageData));
}
// ローカルストレージからチェックボックスの状態を復元
function restoreCheckboxState(checkbox) {
const storageData = loadStorageData();
if (storageData[checkbox.id]) {
checkbox.checked = true;
}
updateButtonVisibility();
}
// ローカルストレージからデータをロード
function loadStorageData() {
const data = localStorage.getItem(STORAGE_KEY);
return data ? JSON.parse(data) : {};
}
// ソート順序の変更をローカルストレージに保存
function handleSortOrderChange(event) {
const sortOrder = event.target.value;
localStorage.setItem(SORT_ORDER_KEY, sortOrder);
}
// ソート順序をローカルストレージから復元
function restoreSortOrder(selectElement) {
const sortOrder = localStorage.getItem(SORT_ORDER_KEY);
if (sortOrder) {
selectElement.value = sortOrder;
} else {
selectElement.value = "desc"; // デフォルトは降順
}
}
async function do_download(p) {
const storageData = loadStorageData();
const today = new Date();
const formattedDate = dateToStr(today);
let transcriptData = [];
const checkedIds = Object.keys(storageData);
if (checkedIds.length === 0) {
alert("選択された文字起こしがありません。");
return;
}
// ソート順序を取得
const sortOrder = localStorage.getItem(SORT_ORDER_KEY) || "desc"; // デフォルトは降順
// 日付順にソート
const sortedData = Object.entries(storageData).sort(([, a], [, b]) => {
if (a.date === "日付不明") return 1; // 日付不明は最後に配置
if (b.date === "日付不明") return -1; // 日付不明は最後に配置
const dateA = new Date(a.date);
const dateB = new Date(b.date);
if (sortOrder === "asc") {
return dateA - dateB; // 昇順
} else {
return dateB - dateA; // 降順
}
});
for (const [id, { summary, title, url, date }] of sortedData) {
const transcriptUrl = url + "/transcript.txt";
try {
const response = await fetch(transcriptUrl);
if (!response.ok) throw new Error("Failed to download: " + transcriptUrl);
const text = await response.text();
transcriptData.push(`# ${date} ${title}\\n${url}\\n\\n${summary}\\n\\n${text}`);
} catch (error) {
console.error(error);
}
}
if (transcriptData.length === 0) {
alert("選択された文字起こしがありません。");
return;
}
const finalText = transcriptData.join("\\n\\n");
const blob = new Blob([finalText], { type: "text/plain" });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = `${formattedDate}_summary.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
localStorage.removeItem(STORAGE_KEY); //ダウンロード完了後にローカルストレージをクリア
const checkboxes = document.querySelectorAll("input[class^='transcript-checkbox']");
checkboxes.forEach(checkbox => {
checkbox.checked = false
});
updateButtonVisibility();
}
// ローカルストレージをクリアする関数
function clearLocalStorage() {
localStorage.removeItem(STORAGE_KEY);
const checkboxes = document.querySelectorAll("input[class^='transcript-checkbox']");
checkboxes.forEach(checkbox => {
checkbox.checked = false;
});
updateButtonVisibility();
}
function isMyPodcast(url) {
let myPodcasts = Array.from(document.querySelectorAll("body > div > nav > div a"))
.map(a => a.href)
.filter(href => href.startsWith("<https://listen.style/p/>"))
.map(href => href.split("?")[0])
.filter((href, index, self) => self.indexOf(href) === index)
return myPodcasts.some(href => url.startsWith(href));
}
const observer = new MutationObserver(() => {
if (!document.getElementById(DOWNLOAD_CONTAINER_ID) && document.querySelector('div[x-data=newPlayer]')) {
addCheckBoxes();
}
});
observer.observe(document.body, { childList: true, subtree: true });
})();
変更点と解説:
updateButtonVisibility()
関数の変更:
display
プロパティの制御を、opacity
プロパティの制御から分離しました。opacity
を 1
(表示) または 0
(非表示) に設定します。hasData
が true
の場合: container.style.display = "flex";
と即座に設定します。これにより、opacityが1
になる前にdisplayがnone
になることによるアニメーション消失を防ぎます。hasData
が false
の場合:
container.style.display = "none";
を直接設定するのではなく、transitionend
イベントリスナーを追加します。transitionend
イベントは、CSS のトランジション(ここでは opacity: all 0.5s;
)が完了したときに発生します。display: "none"
を設定します。removeEventListener
でイベントリスナーを削除します。なぜこの修正が必要だったのか?
元のコードでは、container.style.display
が opacity
と同時に変更されていました。 以下のように推移すると想定できます。
loadStorageData()
が空になるupdateButtonVisibility()
が呼ばれる。container.style.opacity = 0;
が実行container.style.display = "none";
が実行 上記がほぼ同時に実行されるため、opacityのアニメーションが適用される前に、display: none;
が設定されてしまい、アニメーションが見えなくなっていました。修正後のコードでは、transitionend
イベントを利用して、opacity
のトランジションが完了した後に display: "none"
が設定されるようにしました。これにより、opacity
のアニメーションが正常に表示され、フェードアウト効果が見られるようになります。
display:flex;
の変更は特に問題ないため、そのまま実行することでアニメーションが正常に行われます。 これでopacityのアニメーションが表示されなくなる問題は解消されます。
Okay, I understand the problem. The issue is that the fade-in animation (opacity from 0 to 1) is not working, while the fade-out animation (opacity from 1 to 0) is working correctly. This is because of the timing of display: flex;
being set.
Here's the breakdown of why it's happening and how to fix it:
Why the Fade-In Animation Fails
#listendltool_download_container
has display: none;
and opacity: 0;
in styles.css
.updateButtonVisibility()
is called.hasData
is true
: Since there's now data in localStorage
, hasData
becomes true
.container.style.opacity = 1;
: This correctly sets the opacity to 1, initiating a transition from 0 to 1.