Implement loading spinner for marking as favorite/read, read/unread (#7564)

* Implement loading spinner for marking as favorite

* Ensure that the correct previous icon gets set

* Remove delay

* Improve compatibility with various parsers

Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>

* Support multiple icons (top, bottom)

* Remove preload for now

* Fix CSS, remove !important

* Implement read/unread and alt

* Ensure correct bookmark icon gets set after error

---------

Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
This commit is contained in:
Inverle
2025-05-10 21:06:58 +02:00
committed by GitHub
parent 5f74634c83
commit 84d4aeb9e6
9 changed files with 84 additions and 0 deletions
+1
View File
@@ -78,6 +78,7 @@ echo htmlspecialchars(json_encode([
'icons' => [
'read' => rawurlencode(_i('read')),
'unread' => rawurlencode(_i('unread')),
'spinner' => '../themes/icons/spinner.svg',
],
'extensions' => $extData,
], JSON_UNESCAPED_UNICODE) ?: '', ENT_NOQUOTES, 'UTF-8');
+26
View File
@@ -224,6 +224,10 @@ function send_mark_read_queue(queue, asRead, callback) {
req.responseType = 'json';
req.onerror = function (e) {
for (let i = queue.length - 1; i >= 0; i--) {
const div = document.getElementById('flux_' + queue[i]);
div.querySelectorAll('a.read > .icon').forEach(icon => {
icon.outerHTML = div.classList.contains('not_read') ? context.icons.unread : context.icons.read;
});
delete pending_entries['flux_' + queue[i]];
}
badAjax(this.status == 403);
@@ -315,6 +319,12 @@ function mark_read(div, only_not_read, asBatch) {
}
pending_entries[div.id] = true;
div.querySelectorAll('a.read > .icon').forEach(icon => {
icon.src = context.icons.spinner;
icon.alt = '⏳';
icon.classList.add('spinner');
});
const asRead = div.classList.contains('not_read');
const entryId = div.id.replace(/^flux_/, '');
if (asRead && asBatch) {
@@ -351,10 +361,26 @@ function mark_favorite(div) {
}
pending_entries[div.id] = true;
let originalIcon;
div.querySelectorAll('a.bookmark > .icon').forEach(icon => {
originalIcon = {
src: icon.getAttribute('src'),
alt: icon.getAttribute('alt')
};
icon.src = context.icons.spinner;
icon.alt = '⏳';
icon.classList.add('spinner');
});
const req = new XMLHttpRequest();
req.open('POST', url, true);
req.responseType = 'json';
req.onerror = function (e) {
div.querySelectorAll('a.bookmark > .icon').forEach(icon => {
icon.src = originalIcon.src;
icon.alt = originalIcon.alt;
});
delete pending_entries[div.id];
badAjax(this.status == 403);
};
+12
View File
@@ -274,6 +274,18 @@ th {
box-shadow: none;
}
.spinner {
filter: invert(1);
}
a:hover > .spinner {
filter: invert(1) brightness(2);
}
.flux .item.manage .read:hover .icon.spinner {
filter: invert(1) grayscale(0.8) brightness(1.7);
}
/*=== switches */
.switch.active {
background-color: var(--contrast-background-color);
+12
View File
@@ -274,6 +274,18 @@ th {
box-shadow: none;
}
.spinner {
filter: invert(1);
}
a:hover > .spinner {
filter: invert(1) brightness(2);
}
.flux .item.manage .read:hover .icon.spinner {
filter: invert(1) grayscale(0.8) brightness(1.7);
}
/*=== switches */
.switch.active {
background-color: var(--contrast-background-color);
+12
View File
@@ -118,6 +118,18 @@ p.help .icon,
filter: brightness(.6) contrast(1.2);
}
.spinner {
filter: invert(1) brightness(.6) contrast(1.2);
}
.bookmark:hover > .spinner {
filter: invert(1) brightness(1.1);
}
a:hover .icon.spinner {
filter: invert(1) brightness(1.5);
}
/*=== Forms */
legend {
border-bottom: 1px solid var(--dark-border-color);
+12
View File
@@ -118,6 +118,18 @@ p.help .icon,
filter: brightness(.6) contrast(1.2);
}
.spinner {
filter: invert(1) brightness(.6) contrast(1.2);
}
.bookmark:hover > .spinner {
filter: invert(1) brightness(1.1);
}
a:hover .icon.spinner {
filter: invert(1) brightness(1.5);
}
/*=== Forms */
legend {
border-bottom: 1px solid var(--dark-border-color);
+4
View File
@@ -1323,4 +1323,8 @@ a:hover .icon {
:root.darkMode_auto .btn:active .icon {
filter: brightness(1.4);
}
:root.darkMode_auto .spinner {
filter: invert(1) brightness(2);
}
}
+4
View File
@@ -1323,4 +1323,8 @@ a:hover .icon {
:root.darkMode_auto .btn:active .icon {
filter: brightness(1.4);
}
:root.darkMode_auto .spinner {
filter: invert(1) brightness(2);
}
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><style>@keyframes spinner_svv2{to{transform:rotate(360deg)}}</style><path d="M10.14 1.16a11 11 0 0 0-9 8.92A1.59 1.59 0 0 0 2.46 12a1.52 1.52 0 0 0 1.65-1.3 8 8 0 0 1 6.66-6.61A1.42 1.42 0 0 0 12 2.69a1.57 1.57 0 0 0-1.86-1.53Z" style="transform-origin:center;animation:spinner_svv2 .75s infinite linear"/></svg>

After

Width:  |  Height:  |  Size: 375 B