diff --git a/public/s/css/f0ck.css b/public/s/css/f0ck.css
index 0cc97d5..a845fb3 100644
--- a/public/s/css/f0ck.css
+++ b/public/s/css/f0ck.css
@@ -1197,7 +1197,9 @@ body {
overscroll-behavior-y: contain;
overflow: unset;
font-size: 14px;
- height: /* 100%; */auto;
+ height:
+ /* 100%; */
+ auto;
}
.wrapper {
@@ -1318,6 +1320,13 @@ div.posts>a:hover::after {
grid-template-columns: auto 1fr 0fr;
justify-content: start;
border-bottom: 1px solid var(--nav-border-color);
+ background: var(--nav-bg);
+ transition: background 0.2s ease;
+}
+
+.navbar.scrolled {
+ background: #000 !important;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
}
.navbar-brand {
@@ -1424,7 +1433,7 @@ ul.navbar-nav li.nav-item {
}
.nav-link[data-toggle="dropdown"].ddcontent::after {
- content: "\00a0("attr(content) ")\00a0\25bc";
+ content: "\00a0(" attr(content) ")\00a0\25bc";
}
.nav-link[data-toggle="dropdown"]:not(.ddcontent)::after {
@@ -1608,7 +1617,7 @@ span.placeholder {
}
@media (max-width: 1325px) {
-/* ranking page - idea */
+ /* ranking page - idea */
/* .ranking {
grid-template-columns: 1fr 1fr !important;
} */
@@ -1783,8 +1792,9 @@ span.placeholder {
.index-container {
width: 100%;
padding: 5px;
-/* background-color: var(--navigation-links-bg);
- */}
+ /* background-color: var(--navigation-links-bg);
+ */
+}
@media (min-width: 361px) {
.embed-responsive-image {
@@ -2779,7 +2789,9 @@ ul.navbar-nav-guests li.nav-item {
margin-right: .5rem;
}
-.navbar-expand-lg .navbar-nav-guests .nav-link, .pagination > a, .pagination > span {
+.navbar-expand-lg .navbar-nav-guests .nav-link,
+.pagination>a,
+.pagination>span {
padding-right: .5rem;
padding-left: .5rem;
}
@@ -2792,7 +2804,7 @@ ul.navbar-nav-guests li.nav-item {
margin: 0;
padding: 5px;
}
-
+
ul.navbar-nav-guests li.nav-item {
margin-right: unset;
}
@@ -2812,16 +2824,20 @@ ul.navbar-nav-guests li.nav-item {
/* Pagination Responsiveness */
@media (max-width: 799px) {
- .navbar-expand-lg .navbar-nav-guests .nav-link, .pagination > a, .pagination > span {
+
+ .navbar-expand-lg .navbar-nav-guests .nav-link,
+ .pagination>a,
+ .pagination>span {
padding-right: 2px;
padding-left: 2px;
}
-
- .pagination > a, .pagination > span {
+
+ .pagination>a,
+ .pagination>span {
margin-right: 2px;
margin-left: 2px;
}
-
+
.pagination {
justify-content: center !important;
}
@@ -2830,15 +2846,19 @@ ul.navbar-nav-guests li.nav-item {
/* fadeIn effect */
@keyframes fadeIn {
0% {
- opacity: 0;
+ opacity: 0;
}
+
100% {
- opacity: 1;
+ opacity: 1;
}
}
-img#f0ck-image, div.imageDoor, div.posts a, video {
- animation: 1s ease-out 0s 1 fadeInFX;
+img#f0ck-image,
+div.imageDoor,
+div.posts a,
+video {
+ animation: 1s ease-out 0s 1 fadeInFX;
}
/* f0ckgle */
@@ -2867,7 +2887,7 @@ img#f0ck-image, div.imageDoor, div.posts a, video {
background: var(--nav-bg);
padding: 5px;
}
-
+
.profile_head_avatar {
margin: 0;
}
@@ -2892,11 +2912,13 @@ img#f0ck-image, div.imageDoor, div.posts a, video {
padding: 5px;
}
-.f0cks h5, .favs h5 {
+.f0cks h5,
+.favs h5 {
background: var(--dropdown-bg);
}
-.f0cks-header, .favs-header {
+.f0cks-header,
+.favs-header {
display: grid;
grid-template-columns: 1fr auto;
background: var(--img-border-color);
@@ -2940,46 +2962,50 @@ button#togglebg {
0% {
opacity: 0;
}
-100% {
+
+ 100% {
opacity: 1;
- }
+ }
}
@keyframes fadeOutFX {
0% {
opacity: 1;
}
-100% {
+
+ 100% {
opacity: 0;
- }
+ }
}
@keyframes fadeIn {
0% {
- opacity: 0;
- }
+ opacity: 0;
+ }
+
100% {
- opacity: 0.2;
- }
+ opacity: 0.2;
+ }
}
@keyframes fadeOut {
- 0% {
- opacity: 0.2;
- }
- 100% {
- opacity: 0;
- }
+ 0% {
+ opacity: 0.2;
+ }
+
+ 100% {
+ opacity: 0;
+ }
}
.fader-in {
- animation: fadeIn .8s steps(100) forwards;
+ animation: fadeIn .8s steps(100) forwards;
}
.fader-out {
- animation: fadeOut .8s steps(100) forwards
-}
+ animation: fadeOut .8s steps(100) forwards
+}
.settings {
display: grid;
@@ -3004,6 +3030,7 @@ input#s_avatar {
#s_avatar:hover {
background: #ffffff0f;
}
+
.theforceofthree {
display: grid;
grid-template-columns: 0.4fr 1fr 0.4fr;
@@ -3011,4 +3038,24 @@ input#s_avatar {
.upload {
padding: 10px;
+}
+
+/* Infinite scroll loading indicator */
+.loading-spinner {
+ display: inline-block;
+ font-size: 12px;
+ color: var(--footbar-color);
+ animation: pulse 1s ease-in-out infinite;
+}
+
+@keyframes pulse {
+
+ 0%,
+ 100% {
+ opacity: 0.4;
+ }
+
+ 50% {
+ opacity: 1;
+ }
}
\ No newline at end of file
diff --git a/public/s/js/f0ck.js b/public/s/js/f0ck.js
index 8deb7b2..bc62ee0 100644
--- a/public/s/js/f0ck.js
+++ b/public/s/js/f0ck.js
@@ -68,6 +68,18 @@ window.requestAnimFrame = (function () {
}
};
+ // Navbar scroll effect - make background black when scrolling
+ const navbar = document.querySelector('.navbar');
+ if (navbar) {
+ window.addEventListener('scroll', () => {
+ if (window.scrollY > 50) {
+ navbar.classList.add('scrolled');
+ } else {
+ navbar.classList.remove('scrolled');
+ }
+ });
+ }
+
const loadItemAjax = async (url, inheritContext = true) => {
console.log("loadItemAjax called with:", url, "inheritContext:", inheritContext);
// Show loading indicator
@@ -335,15 +347,109 @@ window.requestAnimFrame = (function () {
}
//
- //
- let tts = 0;
- const scroll_treshold = 1;
- if ([...document.querySelectorAll("div.posts")].length === 1) {
+ //
+ const postsContainer = document.querySelector("div.posts");
+ if (postsContainer) {
+ // Infinite scroll state
+ let infiniteState = {
+ loading: false,
+ hasMore: true,
+ currentPage: 1
+ };
+
+ // Extract current page from URL
+ const pageMatch = window.location.pathname.match(/\/p\/(\d+)/);
+ if (pageMatch) infiniteState.currentPage = parseInt(pageMatch[1]);
+
+ // Extract context (tag/user/mime) from URL
+ const getContext = () => {
+ const ctx = {};
+ const tagMatch = window.location.pathname.match(/\/tag\/([^/]+)/);
+ if (tagMatch) ctx.tag = decodeURIComponent(tagMatch[1]);
+ const userMatch = window.location.pathname.match(/\/user\/([^/]+)/);
+ if (userMatch) ctx.user = decodeURIComponent(userMatch[1]);
+ const mimeMatch = window.location.pathname.match(/\/(image|audio|video)(?:\/|$)/);
+ if (mimeMatch) ctx.mime = mimeMatch[1];
+ return ctx;
+ };
+
+ // Build URL path for history
+ const buildUrl = (page) => {
+ const ctx = getContext();
+ let path = '/';
+ if (ctx.tag) path += `tag/${ctx.tag}/`;
+ if (ctx.user) path += `user/${ctx.user}/`;
+ if (ctx.mime) path += `${ctx.mime}/`;
+ if (page > 1) path += `p/${page}`;
+ return path.replace(/\/$/, '') || '/';
+ };
+
+ // Fetch and append more items
+ const loadMoreItems = async () => {
+ if (infiniteState.loading || !infiniteState.hasMore) return;
+
+ infiniteState.loading = true;
+ const foot = document.querySelector("div#footbar");
+ if (foot) {
+ foot.innerHTML = 'Loading...';
+ foot.style.color = 'var(--footbar-color)';
+ }
+
+ const nextPage = infiniteState.currentPage + 1;
+ const ctx = getContext();
+
+ const params = new URLSearchParams();
+ params.append('page', nextPage);
+ if (ctx.tag) params.append('tag', ctx.tag);
+ if (ctx.user) params.append('user', ctx.user);
+ if (ctx.mime) params.append('mime', ctx.mime);
+
+ try {
+ const response = await fetch(`/ajax/items?${params.toString()}`);
+ const data = await response.json();
+
+ if (data.success && data.html) {
+ // Append new thumbnails
+ postsContainer.insertAdjacentHTML('beforeend', data.html);
+
+ // Update state
+ infiniteState.currentPage = data.currentPage;
+ infiniteState.hasMore = data.hasMore;
+
+ // Update URL
+ history.replaceState({}, '', buildUrl(infiniteState.currentPage));
+
+ // Update pagination display if exists
+ const paginationLinks = document.querySelectorAll('.pagination .pagination-int-item, .pagination .btn');
+ paginationLinks.forEach(el => {
+ if (el.textContent.trim() == infiniteState.currentPage) {
+ el.classList.add('disabled');
+ }
+ });
+ } else {
+ infiniteState.hasMore = false;
+ }
+ } catch (err) {
+ console.error('Infinite scroll fetch error:', err);
+ } finally {
+ infiniteState.loading = false;
+ if (foot) {
+ foot.innerHTML = infiniteState.hasMore ? '▼' : '—';
+ foot.style.color = 'transparent';
+ }
+ }
+ };
+
+ // Scroll detection
+ let tts = 0;
+ const scroll_treshold = 1;
+
document.addEventListener("wheel", e => {
if (!document.querySelector('#main')) return;
- if (Math.ceil(window.innerHeight + window.scrollY) >= document.querySelector('#main').offsetHeight && e.deltaY > 0) { // down
- if (elem = document.querySelector(".pagination > .next:not(.disabled)")) {
+ if (Math.ceil(window.innerHeight + window.scrollY) >= document.querySelector('#main').offsetHeight && e.deltaY > 0) {
+ // Scrolling down at bottom
+ if (infiniteState.hasMore && !infiniteState.loading) {
if (tts < scroll_treshold) {
const foot = document.querySelector("div#footbar");
if (foot) {
@@ -351,43 +457,25 @@ window.requestAnimFrame = (function () {
foot.style.color = "var(--footbar-color)";
}
tts++;
+ } else {
+ loadMoreItems();
+ tts = 0;
}
- else
- changePage(elem);
}
- }
- else if (window.scrollY <= 0 && e.deltaY < 0) { // up
- if (elem = document.querySelector(".pagination > .prev:not(.disabled)")) {
- if (tts < scroll_treshold) {
- const nav = document.querySelector("nav.navbar");
- if (nav) {
- nav.style.boxShadow = "0px 2px 0px var(--loading-indicator-color)";
- nav.style.transition = ".2s ease-in-out";
- }
- tts++;
- }
- else
- changePage(elem);
- }
- }
- else {
+ } else if (window.scrollY <= 0 && e.deltaY < 0) {
+ // Scrolling up at top - could load previous page if needed (optional)
+ tts = 0;
+ } else {
tts = 0;
const foot = document.querySelector("div#footbar");
if (foot) {
foot.style.boxShadow = "unset";
foot.style.color = "transparent";
}
- const nav = document.querySelector("nav.navbar");
- if (nav) nav.style.boxShadow = "unset";
}
});
}
-
- const rmatch = /\/p\/(\d+?)/;
- if (document.referrer.match(rmatch) && document.location.href.match(rmatch))
- if (document.location.href.match(rmatch) < document.referrer.match(rmatch))
- document.body.scrollTop = document.body.scrollHeight;
- //
+ //
//
const swipeRT = {
diff --git a/src/inc/routes/ajax.mjs b/src/inc/routes/ajax.mjs
index a933050..cddbdbe 100644
--- a/src/inc/routes/ajax.mjs
+++ b/src/inc/routes/ajax.mjs
@@ -64,5 +64,58 @@ export default (router, tpl) => {
});
});
+ // Infinite scroll endpoint for index thumbnails
+ router.get(/\/ajax\/items/, async (req, res) => {
+ let query = {};
+ if (typeof req.url === 'string') {
+ const parsedUrl = url.parse(req.url, true);
+ query = parsedUrl.query;
+ } else {
+ query = req.url.qs || {};
+ }
+
+ const page = parseInt(query.page) || 1;
+
+ const data = await f0cklib.getf0cks({
+ page: page,
+ tag: query.tag || null,
+ user: query.user || null,
+ mime: query.mime || null,
+ mode: req.session.mode,
+ session: !!req.session,
+ fav: false
+ });
+
+ if (!data.success) {
+ return res.reply({
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ success: false,
+ html: '',
+ hasMore: false
+ })
+ });
+ }
+
+ // Render just the thumbnail items
+ const itemsHtml = tpl.render('snippets/items-grid', {
+ items: data.items,
+ link: data.link
+ });
+
+ const hasMore = data.pagination.next !== null;
+
+ return res.reply({
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ success: true,
+ html: itemsHtml,
+ hasMore: hasMore,
+ nextPage: data.pagination.next,
+ currentPage: data.pagination.page
+ })
+ });
+ });
+
return router;
};
diff --git a/views/snippets/items-grid.html b/views/snippets/items-grid.html
new file mode 100644
index 0000000..9c69cfd
--- /dev/null
+++ b/views/snippets/items-grid.html
@@ -0,0 +1,7 @@
+@each(items as item)
+
+
+
+@endeach
\ No newline at end of file