Merge pull request 'master' (#39) from master into 98

Reviewed-on: #39
This commit is contained in:
Kibi Kelburton
2026-08-06 20:46:41 +00:00
15 changed files with 193 additions and 14 deletions

View File

@@ -141,11 +141,6 @@ services:
networks: networks:
f0ckm-net: f0ckm-net:
driver: bridge driver: bridge
enable_ipv6: true
ipam:
config:
- subnet: 172.28.0.0/16
- subnet: fd00:f0c0:1337::/64
volumes: volumes:
certs: certs:

View File

@@ -0,0 +1,5 @@
-- Add favorites_private column to user_options table
ALTER TABLE user_options
ADD COLUMN IF NOT EXISTS favorites_private boolean DEFAULT false;
COMMENT ON COLUMN public.user_options.favorites_private IS 'When true, only the owner and administrators can view the user''s favorites list';

View File

@@ -1498,7 +1498,8 @@ CREATE TABLE public.user_options (
receive_user_notifications boolean DEFAULT true, receive_user_notifications boolean DEFAULT true,
do_not_disturb boolean DEFAULT false, do_not_disturb boolean DEFAULT false,
comment_display_mode integer DEFAULT 1, comment_display_mode integer DEFAULT 1,
force_comment_display_mode integer DEFAULT 0 force_comment_display_mode integer DEFAULT 0,
favorites_private boolean DEFAULT false
); );

View File

@@ -1119,6 +1119,37 @@
}); });
} }
const favsPrivateToggle = document.getElementById('favorites_private_toggle');
if (favsPrivateToggle) {
favsPrivateToggle.addEventListener('change', async () => {
const favorites_private = favsPrivateToggle.checked;
try {
const res = await fetch('/api/v2/settings/favorites_private', {
method: 'PUT',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-Token': window.f0ckSession ? window.f0ckSession.csrf_token : ''
},
body: new URLSearchParams({ favorites_private })
});
const data = await res.json();
if (data.success) {
showStatus('Favorites privacy preference updated!', 'success');
if (window.f0ckSession) {
window.f0ckSession.favorites_private = favorites_private;
}
} else {
alert(data.msg || 'Error saving preference');
favsPrivateToggle.checked = !favorites_private; // Revert
}
} catch (err) {
console.error('Update Favorites Privacy error:', err);
alert('Connection error');
favsPrivateToggle.checked = !favorites_private; // Revert
}
});
}
// New Dual Column Layout Toggle // New Dual Column Layout Toggle
const layoutToggle = document.getElementById('use_new_layout_toggle'); const layoutToggle = document.getElementById('use_new_layout_toggle');
if (layoutToggle) { if (layoutToggle) {

View File

@@ -165,6 +165,8 @@
"hide_item_ratings_hint": "Zeige keine SFW/NSFW-Bewertungsindikatoren bei Elementen auf der Hauptseite", "hide_item_ratings_hint": "Zeige keine SFW/NSFW-Bewertungsindikatoren bei Elementen auf der Hauptseite",
"disable_swiping": "Wischen deaktivieren", "disable_swiping": "Wischen deaktivieren",
"disable_swiping_hint": "Navigation per Wischen auf Mobilgeräten deaktivieren", "disable_swiping_hint": "Navigation per Wischen auf Mobilgeräten deaktivieren",
"favorites_private": "Private Favoriten",
"favorites_private_hint": "Nur du und Administratoren können deine Favoritenliste sehen.",
"image_expand_on_click": "Bilder beim Klicken inline erweitern", "image_expand_on_click": "Bilder beim Klicken inline erweitern",
"image_expand_on_click_hint": "Anstatt das Scroll-Zoom-Modal zu öffnen, wird ein Bild beim Klicken innerhalb der Seite auf volle Größe erweitert.", "image_expand_on_click_hint": "Anstatt das Scroll-Zoom-Modal zu öffnen, wird ein Bild beim Klicken innerhalb der Seite auf volle Größe erweitert.",
"enable_bg_blur": "Hintergrundunschärfe aktivieren", "enable_bg_blur": "Hintergrundunschärfe aktivieren",
@@ -535,6 +537,7 @@
"subscribe_uploads_btn": "Benutzer für Uploads abonnieren", "subscribe_uploads_btn": "Benutzer für Uploads abonnieren",
"no_uploads": "Keine Uploads gefunden", "no_uploads": "Keine Uploads gefunden",
"no_favs": "Keine Favoriten", "no_favs": "Keine Favoriten",
"private_favorites": "private Favoriten",
"back_to_profile": "Zurück zum Profil", "back_to_profile": "Zurück zum Profil",
"ban_modal_title": "Benutzer sperren", "ban_modal_title": "Benutzer sperren",
"ban_modal_reason": "Grund:", "ban_modal_reason": "Grund:",

View File

@@ -165,6 +165,8 @@
"hide_item_ratings_hint": "Don't show SFW/NSFW rating indicators on main page items", "hide_item_ratings_hint": "Don't show SFW/NSFW rating indicators on main page items",
"disable_swiping": "Disable Swiping", "disable_swiping": "Disable Swiping",
"disable_swiping_hint": "Disable swipe-to-navigate on mobile devices", "disable_swiping_hint": "Disable swipe-to-navigate on mobile devices",
"favorites_private": "Private Favorites",
"favorites_private_hint": "Only you and administrators can view your favorites list.",
"image_expand_on_click": "Expand images inline on click", "image_expand_on_click": "Expand images inline on click",
"image_expand_on_click_hint": "Instead of opening the scroll zoom modal, clicking an image will expand it to full size within the page.", "image_expand_on_click_hint": "Instead of opening the scroll zoom modal, clicking an image will expand it to full size within the page.",
"enable_bg_blur": "Enable Background blur", "enable_bg_blur": "Enable Background blur",
@@ -539,6 +541,7 @@
"subscribe_uploads_btn": "Subscribe user to uploads", "subscribe_uploads_btn": "Subscribe user to uploads",
"no_uploads": "no uploads found", "no_uploads": "no uploads found",
"no_favs": "no favorites", "no_favs": "no favorites",
"private_favorites": "private favorites",
"back_to_profile": "Back to Profile", "back_to_profile": "Back to Profile",
"ban_modal_title": "Ban User", "ban_modal_title": "Ban User",
"ban_modal_reason": "Reason:", "ban_modal_reason": "Reason:",

View File

@@ -165,6 +165,8 @@
"hide_item_ratings_hint": "Toon geen SFW/NSFW beoordelingsindicatoren op hoofdpagina items", "hide_item_ratings_hint": "Toon geen SFW/NSFW beoordelingsindicatoren op hoofdpagina items",
"disable_swiping": "Swipen uitschakelen", "disable_swiping": "Swipen uitschakelen",
"disable_swiping_hint": "Swipe-to-navigate uitschakelen op mobiele apparaten", "disable_swiping_hint": "Swipe-to-navigate uitschakelen op mobiele apparaten",
"favorites_private": "Privé favorieten",
"favorites_private_hint": "Alleen jij en beheerders kunnen je favorietenlijst bekijken.",
"image_expand_on_click": "Afbeeldingen inline vergroten bij klikken", "image_expand_on_click": "Afbeeldingen inline vergroten bij klikken",
"image_expand_on_click_hint": "In plaats van de scroll-zoom-modal te openen, wordt een afbeelding bij klikken vergroot tot volledige grootte binnen de pagina.", "image_expand_on_click_hint": "In plaats van de scroll-zoom-modal te openen, wordt een afbeelding bij klikken vergroot tot volledige grootte binnen de pagina.",
"enable_bg_blur": "Achtergrondvervaging inschakelen", "enable_bg_blur": "Achtergrondvervaging inschakelen",
@@ -535,6 +537,7 @@
"subscribe_uploads_btn": "Gebruiker abonneren op uploads", "subscribe_uploads_btn": "Gebruiker abonneren op uploads",
"no_uploads": "geen uploads gevonden", "no_uploads": "geen uploads gevonden",
"no_favs": "geen favorieten", "no_favs": "geen favorieten",
"private_favorites": "privé favorieten",
"back_to_profile": "Terug naar Profiel", "back_to_profile": "Terug naar Profiel",
"ban_modal_title": "Gebruiker Bannen", "ban_modal_title": "Gebruiker Bannen",
"ban_modal_reason": "Reden:", "ban_modal_reason": "Reden:",

View File

@@ -165,6 +165,8 @@
"hide_item_ratings_hint": "Zeige keine SFW/NSFW-Bewertungsindikatoren bei Elementen auf der Hauptseite", "hide_item_ratings_hint": "Zeige keine SFW/NSFW-Bewertungsindikatoren bei Elementen auf der Hauptseite",
"disable_swiping": "Wischen deaktivieren", "disable_swiping": "Wischen deaktivieren",
"disable_swiping_hint": "Deaktivieren der Wisch-Navigation auf Mobilgeräten", "disable_swiping_hint": "Deaktivieren der Wisch-Navigation auf Mobilgeräten",
"favorites_private": "Private Favoriten",
"favorites_private_hint": "Nur du und Administratoren können deine Favoritenliste sehen.",
"image_expand_on_click": "Bildli beim Klickle inline uffblähe", "image_expand_on_click": "Bildli beim Klickle inline uffblähe",
"image_expand_on_click_hint": "Anstatt dat Scroll-Zoom-Moped aufzumache, wird n Bild beim Klickle uff volle Größ im Bereich uffgepumpt.", "image_expand_on_click_hint": "Anstatt dat Scroll-Zoom-Moped aufzumache, wird n Bild beim Klickle uff volle Größ im Bereich uffgepumpt.",
"enable_bg_blur": "Hintergrundunschärfe aktivieren", "enable_bg_blur": "Hintergrundunschärfe aktivieren",
@@ -536,6 +538,7 @@
"subscribe_uploads_btn": "Benutzer für Aufladierungen abonnieren", "subscribe_uploads_btn": "Benutzer für Aufladierungen abonnieren",
"no_uploads": "keine Aufladierungen gefunden", "no_uploads": "keine Aufladierungen gefunden",
"no_favs": "keine Favoriten", "no_favs": "keine Favoriten",
"private_favorites": "private Favoriten",
"back_to_profile": "Zurück zum Profil", "back_to_profile": "Zurück zum Profil",
"ban_modal_title": "Benutzer sperren", "ban_modal_title": "Benutzer sperren",
"ban_modal_reason": "Grund:", "ban_modal_reason": "Grund:",

View File

@@ -136,8 +136,60 @@ const xdScoreMeta = (score) => {
return { tier: 5, label: 'xDDDDD+' }; return { tier: 5, label: 'xDDDDD+' };
}; };
async function checkFavoritesAccess(rawUser, { session, user_id, is_admin } = {}) {
if (!rawUser) return { isPrivate: false, isAllowed: true };
const decodedUser = decodeURI(rawUser);
const targetUserRows = await db`
select u.id, u."user", u.admin, uo.favorites_private
from "user" u
left join user_options uo on uo.user_id = u.id
where u."user" ilike ${decodedUser} or u.login ilike ${decodedUser}
limit 1
`;
if (!targetUserRows.length || !targetUserRows[0].favorites_private) {
return { isPrivate: false, isAllowed: true };
}
const targetUser = targetUserRows[0];
let reqUserId = user_id;
let reqIsAdmin = is_admin;
if (typeof session === 'object' && session !== null) {
if (reqUserId === undefined) reqUserId = session.id;
if (reqIsAdmin === undefined) reqIsAdmin = !!session.admin;
}
let isAllowed = false;
if (reqUserId) {
if (+reqUserId === +targetUser.id) {
isAllowed = true;
} else if (reqIsAdmin === true) {
isAllowed = true;
} else if (reqIsAdmin === undefined) {
const reqUserRows = await db`select admin from "user" where id = ${+reqUserId} limit 1`;
if (reqUserRows.length > 0 && reqUserRows[0].admin) {
isAllowed = true;
}
}
}
return { isPrivate: true, isAllowed };
}
export default { export default {
getf0cks: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, page, mode, ratings, fav, session, limit, strict, newer, exclude, user_id, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, minXdScore, tagger: rawTagger } = {}) => { getf0cks: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, page, mode, ratings, fav, session, limit, strict, newer, exclude, user_id, is_admin, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, minXdScore, tagger: rawTagger } = {}) => {
if (fav && rawUser) {
const { isPrivate, isAllowed } = await checkFavoritesAccess(rawUser, { session, user_id, is_admin });
if (isPrivate && !isAllowed) {
return {
success: false,
is_private: true,
message: "private favorites"
};
}
}
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null; const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
// --- title: prefix — search items.title instead of the tags table --- // --- title: prefix — search items.title instead of the tags table ---
@@ -428,7 +480,18 @@ export default {
view_mode: fav ? 'favs' : 'uploads' view_mode: fav ? 'favs' : 'uploads'
}; };
}, },
getf0ck: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, itemid: rawItemid, mode, ratings, session, strict, exclude, user_id, fav, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, lang } = {}) => { getf0ck: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, itemid: rawItemid, mode, ratings, session, strict, exclude, user_id, is_admin, fav, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, lang } = {}) => {
if (fav && rawUser) {
const { isPrivate, isAllowed } = await checkFavoritesAccess(rawUser, { session, user_id, is_admin });
if (isPrivate && !isAllowed) {
return {
success: false,
is_private: true,
message: "private favorites"
};
}
}
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null; const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
// --- title: prefix — search items.title instead of the tags table --- // --- title: prefix — search items.title instead of the tags table ---
@@ -884,7 +947,14 @@ export default {
tmp tmp
}; };
return data; return data;
}, getRandom: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, mode, ratings, fav, session, strict, exclude, userHall: rawUserHall, userHallOwner: rawUserHallOwner } = {}) => { },
getRandom: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, mode, ratings, fav, session, strict, exclude, user_id, is_admin, userHall: rawUserHall, userHallOwner: rawUserHallOwner } = {}) => {
if (fav && rawUser) {
const { isPrivate, isAllowed } = await checkFavoritesAccess(rawUser, { session, user_id, is_admin });
if (isPrivate && !isAllowed) {
return null;
}
}
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null; const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
const hall = rawHall || null; const hall = rawHall || null;

View File

@@ -218,9 +218,10 @@ export default (router, tpl) => {
mime: query.mime || (req.cookies.mime || null), mime: query.mime || (req.cookies.mime || null),
mode: query.mode !== undefined ? +query.mode : req.mode, mode: query.mode !== undefined ? +query.mode : req.mode,
ratings: ratingsArr, ratings: ratingsArr,
session: !!req.session, session: req.session,
exclude: req.session ? (req.session.excluded_tags || []) : [],
user_id: req.session?.id, user_id: req.session?.id,
is_admin: req.session?.admin,
exclude: req.session ? (req.session.excluded_tags || []) : [],
fav: query.fav === 'true', fav: query.fav === 'true',
random: isRandom, random: isRandom,
strict: query.strict === '1' || query.strict === 'true' || req.session?.strict_mode, strict: query.strict === '1' || query.strict === 'true' || req.session?.strict_mode,
@@ -231,6 +232,19 @@ export default (router, tpl) => {
}); });
if (!data.success) { if (!data.success) {
if (data.is_private) {
const { t: tErr } = createI18n(req.session?.language || req.lang || 'en');
const privateHtml = `<div class="private-favs-msg" style="padding: 30px; text-align: center; color: var(--text-muted); font-size: 1.1em;">${tErr('profile.private_favorites')}</div>`;
return res.reply({
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
success: false,
is_private: true,
html: privateHtml,
hasMore: false
})
});
}
return res.reply({ return res.reply({
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({

View File

@@ -315,6 +315,25 @@ export default router => {
} }
}); });
// Update Favorites Privacy preference
group.put(/\/favorites_private/, lib.loggedin, async (req, res) => {
const favorites_private = req.post.favorites_private === true || req.post.favorites_private === 'true';
try {
await db`
update user_options
set favorites_private = ${favorites_private}
where user_id = ${+req.session.id}
`;
// Sync session immediately
if (req.session) req.session.favorites_private = favorites_private;
return res.json({ success: true, favorites_private }, 200);
} catch (e) {
console.error('Update Favorites Privacy pref error:', e);
return res.json({ success: false, msg: 'Error updating preference' }, 500);
}
});
// Update Username Color preference // Update Username Color preference
group.put(/\/username_color/, lib.loggedin, async (req, res) => { group.put(/\/username_color/, lib.loggedin, async (req, res) => {
const { color } = req.post; const { color } = req.post;

View File

@@ -99,8 +99,9 @@ export default (router, tpl) => {
ratings: ratingsArr, ratings: ratingsArr,
mime: mime, mime: mime,
fav: true, fav: true,
session: !!req.session, session: req.session,
user_id: req.session?.id, user_id: req.session?.id,
is_admin: req.session?.admin,
random: isRandom random: isRandom
}); });
if (favs && 'items' in favs) { if (favs && 'items' in favs) {
@@ -236,8 +237,9 @@ export default (router, tpl) => {
fav: req.params.mode == 'favs', fav: req.params.mode == 'favs',
mode: req.mode, mode: req.mode,
ratings: (() => { const r = req.cookies.ratings; return r ? decodeURIComponent(r).split(/[|,]/).filter(x => ['sfw','nsfw','nsfl','untagged'].includes(x)) : null; })(), ratings: (() => { const r = req.cookies.ratings; return r ? decodeURIComponent(r).split(/[|,]/).filter(x => ['sfw','nsfw','nsfl','untagged'].includes(x)) : null; })(),
session: !!req.session, session: req.session,
user_id: req.session?.id, user_id: req.session?.id,
is_admin: req.session?.admin,
exclude: req.session ? (req.session.excluded_tags || []) : [], exclude: req.session ? (req.session.excluded_tags || []) : [],
url: decodeURIComponent(req.url.pathname || req.url), url: decodeURIComponent(req.url.pathname || req.url),
strict: !!(req.query?.strict || req.url.qs?.strict || req.session?.strict_mode), strict: !!(req.query?.strict || req.url.qs?.strict || req.session?.strict_mode),
@@ -251,6 +253,20 @@ export default (router, tpl) => {
console.log(`[${new Date().toISOString()}] [ROUTE] Data fetch complete in ${Date.now() - tRouteStart}ms`); console.log(`[${new Date().toISOString()}] [ROUTE] Data fetch complete in ${Date.now() - tRouteStart}ms`);
if (!data.success) { if (!data.success) {
if (data.is_private) {
const { t: tErr } = createI18n(req.session?.language || req.lang || 'en');
return res.reply({
code: 403,
body: tpl.render('error', {
message: tErr('profile.private_favorites'),
domain: cfg.main.url.domain,
tmp: null,
session: req.session ? { ...req.session } : false,
error_filter_hint: null,
error_filter_hint_link: null
}, req)
});
}
// For index/grid views with zero items (empty DB), render an empty grid instead of error // For index/grid views with zero items (empty DB), render an empty grid instead of error
if (mode !== 'item') { if (mode !== 'item') {
data.items = []; data.items = [];

View File

@@ -672,7 +672,7 @@ process.on('uncaughtException', err => {
user = [_cachedRow]; user = [_cachedRow];
} else { } else {
user = await db` user = await db`
select "user".id, "user".login, "user".user, "user".admin, "user".is_moderator, "user".banned, "user".ban_reason, "user".ban_expires, "user".force_password_change, "user_sessions".id as sess_id, "user_sessions".csrf_token, "user_options".mode, "user_options".theme, "user_options".fullscreen, "user_options".excluded_tags, "user_options".avatar, "user_options".avatar_file, "user_options".show_motd, "user_options".strict_mode, "user_options".show_background, "user_options".use_new_layout, "user_options".username_color, "user_options".font, "user_options".disable_autoplay, "user_options".disable_swiping, "user_options".description, "user_options".display_name, COALESCE("user_options".min_xd_score, 0) as min_xd_score, "user_options".ruffle_volume, "user_options".ruffle_background, "user_options".quote_emojis, "user_options".embed_youtube_in_comments, "user_options".hide_koepfe, "user_options".language, "user_options".use_alternative_infobox, "user_options".use_alternative_steuerung, "user_options".receive_system_notifications, "user_options".receive_user_notifications, "user_options".do_not_disturb, "user_options".comment_display_mode, "user_options".force_comment_display_mode select "user".id, "user".login, "user".user, "user".admin, "user".is_moderator, "user".banned, "user".ban_reason, "user".ban_expires, "user".force_password_change, "user_sessions".id as sess_id, "user_sessions".csrf_token, "user_options".mode, "user_options".theme, "user_options".fullscreen, "user_options".excluded_tags, "user_options".avatar, "user_options".avatar_file, "user_options".show_motd, "user_options".strict_mode, "user_options".show_background, "user_options".use_new_layout, "user_options".username_color, "user_options".font, "user_options".disable_autoplay, "user_options".disable_swiping, "user_options".favorites_private, "user_options".description, "user_options".display_name, COALESCE("user_options".min_xd_score, 0) as min_xd_score, "user_options".ruffle_volume, "user_options".ruffle_background, "user_options".quote_emojis, "user_options".embed_youtube_in_comments, "user_options".hide_koepfe, "user_options".language, "user_options".use_alternative_infobox, "user_options".use_alternative_steuerung, "user_options".receive_system_notifications, "user_options".receive_user_notifications, "user_options".do_not_disturb, "user_options".comment_display_mode, "user_options".force_comment_display_mode
from "user_sessions" from "user_sessions"
left join "user" on "user".id = "user_sessions".user_id left join "user" on "user".id = "user_sessions".user_id
left join "user_options" on "user_options".user_id = "user_sessions".user_id left join "user_options" on "user_options".user_id = "user_sessions".user_id

View File

@@ -320,6 +320,13 @@
</label> </label>
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.disable_swiping_hint') }}</small> <small class="text-muted" style="margin-left: 25px;">{{ t('settings.disable_swiping_hint') }}</small>
</div> </div>
<div class="setting-item" style="margin-top: 15px;">
<label for="favorites_private_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="favorites_private_toggle" @if(session.favorites_private===true) checked @endif>
<span>{{ t('settings.favorites_private') }}</span>
</label>
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.favorites_private_hint') }}</small>
</div>
<div class="setting-item" style="margin-top: 15px;"> <div class="setting-item" style="margin-top: 15px;">
<label for="show_background_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;"> <label for="show_background_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="show_background_toggle" @if(session.show_background !==false) checked @endif> <input type="checkbox" id="show_background_toggle" @if(session.show_background !==false) checked @endif>

View File

@@ -142,6 +142,14 @@
</div> </div>
@if(!user.is_ghost) @if(!user.is_ghost)
<div class="favs"> <div class="favs">
@if(favs && favs.is_private)
<div class="favs-header">
{{ t('profile.favs_label') }}
</div>
<div class="private-favs-msg" style="text-align: center;">
{{ t('profile.private_favorites') }}
</div>
@else
<div class="favs-header"> <div class="favs-header">
{{ t('profile.favs_label') }}: {{ count.favs }} <a href="@if(favs.link && favs.link.main){{ favs.link.main }}@else#@endif">{{ t('profile.view_all') }}</a> {{ t('profile.favs_label') }}: {{ count.favs }} <a href="@if(favs.link && favs.link.main){{ favs.link.main }}@else#@endif">{{ t('profile.view_all') }}</a>
</div> </div>
@@ -164,6 +172,7 @@
@else @else
{{ t('profile.no_favs') }} {{ t('profile.no_favs') }}
@endif @endif
@endif
</div> </div>
@endif @endif
</div> </div>