Improve posts UI in three iterative passes

This commit is contained in:
2026-02-24 23:02:36 +01:00
parent 2c54c96cc7
commit ad6e156268
3 changed files with 309 additions and 22 deletions

View File

@@ -357,6 +357,8 @@ const bookmarkSortSelect = document.getElementById('bookmarkSortSelect');
const bookmarkSortDirectionToggle = document.getElementById('bookmarkSortDirectionToggle');
const profileSelectElement = document.getElementById('profileSelect');
const includeExpiredToggle = document.getElementById('includeExpiredToggle');
const searchInput = document.getElementById('searchInput');
const searchClearBtn = document.getElementById('searchClearBtn');
const mergeControls = document.getElementById('mergeControls');
const mergeModeToggle = document.getElementById('mergeModeToggle');
const mergeSubmitBtn = document.getElementById('mergeSubmitBtn');
@@ -540,6 +542,8 @@ let pendingAutoOpenTimerId = null;
let pendingAutoOpenCountdownIntervalId = null;
let pendingProcessingBatch = false;
let pendingOpenCooldownMap = loadPendingOpenCooldownMap(currentProfile);
let pendingBulkStatusResetTimerId = null;
let lastPendingBulkHint = { openableCount: 0, cooldownBlocked: 0 };
function updateIncludeExpiredToggleVisibility() {
if (!includeExpiredToggle) {
@@ -583,27 +587,89 @@ function updateMergeControlsUI() {
}
}
function updatePendingBulkControls(filteredCount = 0) {
function clearPendingBulkStatusResetTimer() {
if (pendingBulkStatusResetTimerId) {
clearTimeout(pendingBulkStatusResetTimerId);
pendingBulkStatusResetTimerId = null;
}
}
function buildPendingBulkHintText({ openableCount = 0, cooldownBlocked = 0 } = {}) {
const openable = Math.max(0, openableCount || 0);
const blocked = Math.max(0, cooldownBlocked || 0);
if (blocked > 0) {
return `Öffnungsbereit: ${openable} · Cooldown: ${blocked}`;
}
return `Öffnungsbereit: ${openable}`;
}
function applyPendingBulkHintStatus() {
if (!pendingBulkStatus) {
return;
}
if (currentTab !== 'pending') {
pendingBulkStatus.textContent = '';
pendingBulkStatus.classList.remove('bulk-status--hint', 'bulk-status--error');
return;
}
pendingBulkStatus.textContent = buildPendingBulkHintText(lastPendingBulkHint);
pendingBulkStatus.classList.add('bulk-status--hint');
pendingBulkStatus.classList.remove('bulk-status--error');
}
function updatePendingBulkControls(filteredCount = 0, stats = null) {
if (!pendingBulkControls) {
return;
}
const isPendingTab = currentTab === 'pending';
pendingBulkControls.hidden = !isPendingTab;
pendingBulkControls.style.display = isPendingTab ? 'flex' : 'none';
if (!isPendingTab) {
clearPendingBulkStatusResetTimer();
if (pendingBulkStatus) {
pendingBulkStatus.textContent = '';
pendingBulkStatus.classList.remove('bulk-status--hint', 'bulk-status--error');
}
}
const normalizedStats = stats && typeof stats === 'object'
? {
openableCount: Math.max(0, parseInt(stats.openableCount, 10) || 0),
cooldownBlocked: Math.max(0, parseInt(stats.cooldownBlocked, 10) || 0)
}
: { openableCount: Math.max(0, filteredCount || 0), cooldownBlocked: 0 };
lastPendingBulkHint = normalizedStats;
if (pendingBulkOpenBtn) {
pendingBulkOpenBtn.disabled = !isPendingTab || pendingProcessingBatch || filteredCount === 0;
pendingBulkOpenBtn.disabled = !isPendingTab || pendingProcessingBatch || normalizedStats.openableCount === 0;
}
if (isPendingTab) {
applyPendingBulkHintStatus();
}
}
function setPendingBulkStatus(message = '', isError = false) {
clearPendingBulkStatusResetTimer();
if (!pendingBulkStatus) {
if (message) {
showToast(message, isError ? 'error' : 'info');
}
return;
}
pendingBulkStatus.textContent = '';
pendingBulkStatus.classList.remove('bulk-status--error');
if (message) {
showToast(message, isError ? 'error' : 'info');
if (!message) {
applyPendingBulkHintStatus();
return;
}
pendingBulkStatus.textContent = message;
pendingBulkStatus.classList.toggle('bulk-status--error', !!isError);
pendingBulkStatus.classList.toggle('bulk-status--hint', !isError);
showToast(message, isError ? 'error' : 'info');
pendingBulkStatusResetTimerId = setTimeout(() => {
pendingBulkStatusResetTimerId = null;
applyPendingBulkHintStatus();
}, 4000);
}
function initializeFocusParams() {
@@ -1949,15 +2015,65 @@ function normalizeRequiredProfiles(post) {
return Array.from({ length: count }, (_, index) => index + 1);
}
function getTabButtons() {
return Array.from(document.querySelectorAll('.tab-btn[data-tab]'));
}
function updateTabButtons() {
document.querySelectorAll('.tab-btn').forEach((button) => {
if (!button.dataset.tab) {
return;
}
button.classList.toggle('active', button.dataset.tab === currentTab);
getTabButtons().forEach((button) => {
const isActive = button.dataset.tab === currentTab;
button.classList.toggle('active', isActive);
button.setAttribute('aria-selected', isActive ? 'true' : 'false');
button.setAttribute('tabindex', isActive ? '0' : '-1');
});
}
function handleTabKeydown(event) {
if (!event || (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight' && event.key !== 'Home' && event.key !== 'End')) {
return;
}
const tabs = getTabButtons();
if (!tabs.length) {
return;
}
const currentIndex = tabs.findIndex((tab) => tab === event.currentTarget);
if (currentIndex === -1) {
return;
}
event.preventDefault();
let nextIndex = currentIndex;
if (event.key === 'Home') {
nextIndex = 0;
} else if (event.key === 'End') {
nextIndex = tabs.length - 1;
} else if (event.key === 'ArrowRight') {
nextIndex = (currentIndex + 1) % tabs.length;
} else if (event.key === 'ArrowLeft') {
nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
}
const nextTab = tabs[nextIndex];
if (!nextTab || !nextTab.dataset.tab) {
return;
}
setTab(nextTab.dataset.tab, { updateUrl: true });
nextTab.focus();
}
function updateSearchClearButtonVisibility() {
if (!searchInput || !searchClearBtn) {
return;
}
const hasValue = typeof searchInput.value === 'string' && searchInput.value.trim().length > 0;
searchClearBtn.hidden = !hasValue;
searchClearBtn.disabled = !hasValue;
}
function isPostsViewActive() {
const postsView = document.querySelector('[data-view="posts"]');
return Boolean(postsView && postsView.classList.contains('app-view--active'));
@@ -2026,7 +2142,6 @@ function getPostListState() {
const tabTotalCount = filteredItems.length;
const searchInput = document.getElementById('searchInput');
const searchValue = searchInput && typeof searchInput.value === 'string' ? searchInput.value.trim() : '';
const searchActive = Boolean(searchValue);
@@ -3651,10 +3766,11 @@ if (pendingAutoOpenToggle) {
}
// Tab switching
document.querySelectorAll('.tab-btn').forEach(btn => {
getTabButtons().forEach((btn) => {
btn.addEventListener('click', () => {
setTab(btn.dataset.tab, { updateUrl: true });
});
btn.addEventListener('keydown', handleTabKeydown);
});
window.addEventListener('app:view-change', (event) => {
@@ -3715,14 +3831,44 @@ if (manualRefreshBtn) {
});
}
const searchInput = document.getElementById('searchInput');
if (searchInput) {
searchInput.addEventListener('input', () => {
updateSearchClearButtonVisibility();
resetVisibleCount();
renderPosts();
});
document.addEventListener('keydown', (event) => {
if (!event || event.key !== '/' || event.metaKey || event.ctrlKey || event.altKey) {
return;
}
const target = event.target;
const tagName = target && target.tagName ? target.tagName.toLowerCase() : '';
const isEditable = !!(target && (target.isContentEditable || tagName === 'input' || tagName === 'textarea' || tagName === 'select'));
if (isEditable || !isPostsViewActive()) {
return;
}
event.preventDefault();
searchInput.focus();
searchInput.select();
});
}
if (searchClearBtn) {
searchClearBtn.addEventListener('click', () => {
if (!searchInput) {
return;
}
searchInput.value = '';
updateSearchClearButtonVisibility();
resetVisibleCount();
renderPosts();
searchInput.focus();
});
}
updateSearchClearButtonVisibility();
if (mergeModeToggle) {
mergeModeToggle.addEventListener('click', () => {
if (currentTab !== 'all') {
@@ -4129,7 +4275,22 @@ function renderPosts() {
}
updateFilteredCount(currentTab, filteredItems.length);
updatePendingBulkControls(filteredItems.length);
let pendingBulkStats = null;
if (currentTab === 'pending') {
pendingBulkStats = filteredItems.reduce((stats, item) => {
const post = item && item.post ? item.post : null;
if (!post || !post.url) {
return stats;
}
if (isPendingOpenCooldownActive(post.id)) {
stats.cooldownBlocked += 1;
} else {
stats.openableCount += 1;
}
return stats;
}, { openableCount: 0, cooldownBlocked: 0 });
}
updatePendingBulkControls(filteredItems.length, pendingBulkStats);
const visibleCount = Math.min(filteredItems.length, getVisibleCount(currentTab));
const visibleItems = filteredItems.slice(0, visibleCount);