This commit is contained in:
2026-09-12 10:10:34 +02:00
parent 217f1be72d
commit 53055ea5c6
35 changed files with 2261 additions and 138 deletions
+3 -5
View File
@@ -158,14 +158,14 @@
<div class="gapRight">
@if(session)
@if(!user_alternative_infobox)
@if(!user_alternative_infobox && session)
@if(user_has_favorited)
<i class="iconset fa-solid fa-heart" id="a_favo" data-item-id="{{ item.id }}" title="Favorite"></i>
@else
<i class="iconset fa-regular fa-heart" id="a_favo" data-item-id="{{ item.id }}" title="Favorite"></i>
@endif
@endif
@if(session)
<i class="iconset fa-solid fa-circle-info" id="a_info" data-item-id="{{ item.id }}" title="{{ t('info_modal.button_title') || 'Post & File Info' }}"></i>
<i class="iconset {{ isSubscribed ? 'fa-solid' : 'fa-regular' }} fa-bell" id="subscribe-btn" data-item-id="{{ item.id }}" title="{{ isSubscribed ? 'Subscribed' : 'Subscribe' }}"></i>
<i class="iconset fa-solid fa-triangle-exclamation report-item-btn" data-item-id="{{ item.id }}" title="Report this post"></i>
@@ -252,12 +252,10 @@
@if(item.is_comments_locked) data-is-locked="true" @endif>
@if(item.is_comments_locked && !is_mod_or_admin)
<div class="lock-notice">🔒 Comments are disabled on this thread.</div>
@elseif(session)
@else
<div class="comment-input main-input">
<textarea disabled></textarea>
</div>
@else
<div class="login-placeholder"><a href="/login" class="login-trigger-btn">Login</a> to comment</div>
@endif
</div>
@endif
+3 -3
View File
@@ -21,12 +21,10 @@
@if(item.is_comments_locked) data-is-locked="true" @endif>
@if(item.is_comments_locked && !is_mod_or_admin)
<div class="lock-notice">🔒 Comments are disabled on this thread.</div>
@elseif(session)
@else
<div class="comment-input main-input">
<textarea disabled></textarea>
</div>
@else
<div class="login-placeholder"><a href="/login" class="login-trigger-btn">Login</a> to comment</div>
@endif
</div>
@endif
@@ -158,6 +156,8 @@
@else
<i class="iconset fa-regular fa-heart" id="a_favo" data-item-id="{{ item.id }}" title="Favorite"></i>
@endif
@endif
@if(session)
<i class="iconset fa-solid fa-circle-info" id="a_info" data-item-id="{{ item.id }}" title="{{ t('info_modal.button_title') || 'Post & File Info' }}"></i>
<i class="iconset {{ isSubscribed ? 'fa-solid' : 'fa-regular' }} fa-bell" id="subscribe-btn" data-item-id="{{ item.id }}" title="{{ isSubscribed ? 'Subscribed' : 'Subscribe' }}"></i>
<i class="iconset fa-solid fa-triangle-exclamation report-item-btn" data-item-id="{{ item.id }}" title="Report this post"></i>
+67 -7
View File
@@ -6,19 +6,26 @@
<!-- Quick navigation -->
<nav id="settings-quicknav" aria-label="Settings sections">
@if(!session.is_anon)
<a href="#profile"><i class="fa-solid fa-user-pen"></i> {{ t('settings.profile') }}</a>
@endif
<a href="#preferences"><i class="fa-solid fa-sliders"></i> {{ t('settings.preferences') }}</a>
@if(enable_data_export)
@if(session.is_anon)
<a href="#anon-ssh"><i class="fa-solid fa-key"></i> SSH Identity</a>
@endif
@if(!session.is_anon && enable_data_export)
<a href="#export"><i class="fa-solid fa-file-export"></i> {{ t('settings.export_data_title') }}</a>
@endif
@if(!session.is_anon)
<a href="#account"><i class="fa-solid fa-shield-halved"></i> {{ t('settings.account') }}</a>
@if(matrix_enabled || telegram_enabled)
@endif
@if(!session.is_anon && (matrix_enabled || telegram_enabled))
<a href="#linked"><i class="fa-solid fa-link"></i> {{ t('settings.linked_accounts') }}</a>
@endif
@if(enable_user_api_keys)
@if(!session.is_anon && enable_user_api_keys)
<a href="#apikey"><i class="fa-solid fa-key"></i> API Key</a>
@endif
@if(enable_user_invites)
@if(!session.is_anon && enable_user_invites)
<a href="#invites"><i class="fa-solid fa-ticket"></i> {{ t('invites.section_title') }}</a>
@endif
</nav>
@@ -74,6 +81,7 @@
</script>
@if(!session.is_anon)
<!-- ═══════════════════════════════ PROFILE ═══════════════════════════════ -->
<h2 id="profile"><i class="fa-solid fa-user-pen"></i> {{ t('settings.profile') }}</h2>
@@ -187,6 +195,7 @@
@endif
</div>
@endif
@@ -510,6 +519,55 @@
</div>
@if(session.is_anon)
<!-- ═══════════════════════════════ ANONYMOUS SSH IDENTITY ═══════════════════════════════ -->
<h2 id="anon-ssh"><i class="fa-solid fa-key"></i> Anonymous SSH Identity</h2>
<div class="account-settings-wrapper" style="background: rgba(0,0,0,0.1); padding: 20px; border-radius: 6px; border: 1px solid var(--nav-border-color); margin-bottom: 30px;">
<p style="color: var(--text-muted); margin-bottom: 16px;">
You are currently browsing anonymously using an OpenSSH Ed25519 keypair. Your favorites and comments are tied to this cryptographic identity without requiring a username, password, or email.
</p>
<div style="margin-bottom: 16px;">
<label style="font-weight: bold; color: var(--text-muted); display: block; margin-bottom: 6px;">OpenSSH SHA256 Fingerprint</label>
<code id="settings-anon-fp" style="background: rgba(0,0,0,0.3); padding: 6px 12px; border-radius: 4px; display: inline-block; color: #5bc0be; font-family: monospace;">{{ session.fingerprint || 'Loading...' }}</code>
</div>
<div style="margin-bottom: 20px;">
<label style="font-weight: bold; color: var(--text-muted); display: block; margin-bottom: 6px;">OpenSSH Public Key</label>
<div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
<input type="text" id="settings-anon-pubkey" readonly class="input" style="flex: 1; min-width: 250px; font-family: monospace; font-size: 0.85em;" value="" placeholder="Loading public key...">
<button type="button" class="button" onclick="if(window.f0ckAnonSSH) window.f0ckAnonSSH.copyPublicKey();"><i class="fa-solid fa-copy"></i> Copy</button>
</div>
</div>
<div style="display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 20px;">
<button type="button" class="button button-primary" onclick="if(window.f0ckAnonSSH) window.f0ckAnonSSH.downloadPrivateKey();"><i class="fa-solid fa-download"></i> Download id_ed25519</button>
<button type="button" class="button" onclick="if(window.f0ckAnonSSH) window.f0ckAnonSSH.downloadPublicKey();"><i class="fa-solid fa-download"></i> Download id_ed25519.pub</button>
<button type="button" class="button" onclick="const m = document.getElementById('anon-ssh-modal'); if(m) m.style.display='flex';"><i class="fa-solid fa-key"></i> Import / Supply Key</button>
</div>
<div style="padding: 12px 16px; background: rgba(255,255,255,0.03); border-radius: 6px; border: 1px solid rgba(255,255,255,0.08); font-size: 0.88em; color: var(--text-muted);">
<i class="fa-solid fa-circle-info" style="color: var(--accent); margin-right: 6px;"></i>
<strong>Want full features?</strong> Registered accounts can upload items, customize avatars, create API keys, and invite friends. You can <a href="#" onclick="event.preventDefault(); const m=document.getElementById('register-modal'); if(m) m.style.display='flex';" style="color: var(--accent); text-decoration: underline;">register an account</a> at any time.
</div>
</div>
<script>
(function() {
const syncKey = () => {
if (window.f0ckAnonSSH) {
const pk = document.getElementById('settings-anon-pubkey');
if (pk && window.f0ckAnonSSH.pubkey) pk.value = window.f0ckAnonSSH.pubkey;
const fp = document.getElementById('settings-anon-fp');
if (fp && window.f0ckAnonSSH.fingerprint) fp.textContent = window.f0ckAnonSSH.fingerprint;
}
};
syncKey();
window.addEventListener('f0ck:anon_session_ready', syncKey);
document.addEventListener('DOMContentLoaded', syncKey);
})();
</script>
@endif
@if(enable_data_export)
<!-- ═══════════════════════════════ EXPORT ═══════════════════════════════ -->
<h2 id="export"><i class="fa-solid fa-file-export"></i> {{ t('settings.export_data_title') || 'Export Data' }}</h2>
@@ -553,6 +611,7 @@
</div>
@endif
@if(!session.is_anon)
<!-- ═══════════════════════════════ ACCOUNT ═══════════════════════════════ -->
<h2 id="account"><i class="fa-solid fa-shield-halved"></i> {{ t('settings.account') }}</h2>
<div class="account-settings-wrapper"
@@ -612,8 +671,9 @@
</div>
</div>
</div>
@endif
@if(matrix_enabled || telegram_enabled)
@if(!session.is_anon && (matrix_enabled || telegram_enabled))
<!-- ═══════════════════════════════ LINKED ACCOUNTS ═══════════════════════════════ -->
<h2 id="linked"><i class="fa-solid fa-link"></i> {{ t('settings.linked_accounts') }}</h2>
<div class="linked-accounts-wrapper" style="background: rgba(0,0,0,0.1); padding: 20px; border-radius: 6px; border: 1px solid var(--nav-border-color); margin-bottom: 30px;">
@@ -650,7 +710,7 @@
</div>
@endif
@if(enable_user_api_keys)
@if(!session.is_anon && enable_user_api_keys)
<!-- ═══════════════════════════════ API KEY ═══════════════════════════════ -->
<h2 id="apikey"><i class="fa-solid fa-key"></i> Upload API Key</h2>
<div id="api-key-section" class="account-settings-wrapper"
@@ -686,7 +746,7 @@
</div>
@endif
@if(enable_user_invites)
@if(!session.is_anon && enable_user_invites)
<!-- ═══════════════════════════════ INVITES ═══════════════════════════════ -->
<h2 id="invites"><i class="fa-solid fa-ticket"></i> {{ t('invites.section_title') }}</h2>
<div id="invite-section" class="account-settings-wrapper"
+16 -2
View File
@@ -178,7 +178,7 @@
@endif
@if(private_society && !session)
<script>
window.f0ckSession = { logged_in: false, onara: @if(onara) true @else false @endif, enable_item_slugs: @if(enable_item_slugs) true @else false @endif, hide_comments_from_public: @if(hide_comments_from_public) true @else false @endif, guest_anonymize: @if(guest_anonymize) true @else false @endif, enable_xd_score: @if(enable_xd_score) true @else false @endif, default_theme: "{{ default_theme }}", show_content_warning: @if(show_content_warning) true @else false @endif, use_new_layout: @if(default_layout === 'legacy')false @else true @endif, comment_display_mode: {{ comment_display_mode }}, comment_max_length: {{ comment_max_length !== null && comment_max_length !== undefined ? comment_max_length : 'null' }}, development: @if(development) true @else false @endif, allow_comment_deletion: @if(allow_comment_deletion) true @else false @endif, hide_sidebar_default: @if(hide_sidebar_default) true @else false @endif, public_untagged: @if(public_untagged) true @else false @endif, public_nsfw: @if(public_nsfw) true @else false @endif };
window.f0ckSession = { logged_in: false, onara: @if(onara) true @else false @endif, enable_item_slugs: @if(enable_item_slugs) true @else false @endif, enable_anonymous_access: @if(enable_anonymous_access) true @else false @endif, hide_comments_from_public: @if(hide_comments_from_public) true @else false @endif, guest_anonymize: @if(guest_anonymize) true @else false @endif, enable_xd_score: @if(enable_xd_score) true @else false @endif, default_theme: "{{ default_theme }}", show_content_warning: @if(show_content_warning) true @else false @endif, use_new_layout: @if(default_layout === 'legacy')false @else true @endif, comment_display_mode: {{ comment_display_mode }}, comment_max_length: {{ comment_max_length !== null && comment_max_length !== undefined ? comment_max_length : 'null' }}, development: @if(development) true @else false @endif, allow_comment_deletion: @if(allow_comment_deletion) true @else false @endif, hide_sidebar_default: @if(hide_sidebar_default) true @else false @endif, public_untagged: @if(public_untagged) true @else false @endif, public_nsfw: @if(public_nsfw) true @else false @endif };
window.f0ckDebug = window.f0ckSession.development ? console.log.bind(console) : () => {};
window.f0ckDefaultThumbSize = "{{ default_thumb_size }}";
(() => {
@@ -421,6 +421,7 @@
public_untagged: @if(public_untagged) true @else false @endif,
public_nsfw: @if(public_nsfw) true @else false @endif,
enable_item_slugs: @if(enable_item_slugs) true @else false @endif,
enable_anonymous_access: @if(enable_anonymous_access) true @else false @endif,
strict_mode: @if(session && session.strict_mode) true @else false @endif,
logged_in: @if(session) true @else false @endif,
hide_comments_from_public: @if(hide_comments_from_public) true @else false @endif,
@@ -436,6 +437,9 @@
nsfl_tag_id: {{ nsfl_tag_id }},
enable_nsfl: @if(enable_nsfl) true @else false @endif,
user: @if(session) "{{ session.user }}" @else null @endif,
login: @if(session) "{{ session.login }}" @else null @endif,
anon_login: @if(session && session.anon_login) "{{ session.anon_login }}" @else null @endif,
is_anon: @if(session && session.is_anon) true @else false @endif,
display_name: @if(session && session.display_name) "{!! session.display_name !!}" @else null @endif,
username_color: @if(session && session.username_color) "{!! session.username_color !!}" @else null @endif,
id: @if(session && session.id) {{ session.id }} @else null @endif,
@@ -661,9 +665,19 @@
url_tracker_extracting: "{{ t('upload.url_tracker_extracting') || 'Extracting' }}",
url_tracker_complete: "{{ t('upload.url_tracker_complete') || 'Complete!' }}",
url_tracker_failed: "{{ t('upload.url_tracker_failed') || 'Upload failed' }}",
url_tracker_view: "{{ t('upload.url_tracker_view') || 'View →' }}"
url_tracker_view: "{{ t('upload.url_tracker_view') || 'View →' }}",
// favorites
fav_added: "{{ t('notifications.fav_added') || 'ADDED TO FAVORITES' }}",
fav_removed: "{{ t('notifications.fav_removed') || 'REMOVED FROM FAVORITES' }}",
no_favs: "{{ t('profile.no_favs') || 'no favorites' }}",
favs_label: "{{ t('profile.favs_label') || 'Favorites' }}",
guest_favs_saved: "{{ t('profile.guest_favs_saved') || 'You have {count} guest favorites saved on this device.' }}",
sync_guest_favs: "{{ t('profile.sync_guest_favs') || 'Import to Account' }}",
guest_favs_imported: "{{ t('profile.guest_favs_imported') || 'Imported favorites to your account!' }}",
dismiss: "{{ t('common.dismiss') || 'Dismiss' }}"
};
</script>
<script src="/s/js/anon_ssh.js?v={{ ts }}"></script>
<script src="/s/js/f0ckm.js?v={{ ts }}"></script>
<script src="/s/js/sidebar-activity.js?v={{ ts }}"></script>
<script src="/s/js/flash_yank.js?v={{ ts }}"></script>
+1
View File
@@ -8,6 +8,7 @@
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="{{ domain }}">
<meta name="csrf-token" content="{{ csrf_token }}">
<script>(function(){var h=document.documentElement;if(localStorage.getItem('hideItemRatings')==='true')h.classList.add('hide-item-ratings-active');if(localStorage.getItem('blurNsfw')==='true')h.classList.add('blur-nsfw-active');if(localStorage.getItem('blurNsfl')==='true')h.classList.add('blur-nsfl-active');if(localStorage.getItem('blurSfw')==='true')h.classList.add('blur-sfw-active');if(localStorage.getItem('blurUntagged')==='true')h.classList.add('blur-untagged-active');if(localStorage.getItem('blurDetail')!=='false')h.classList.add('blur-detail-active');if(localStorage.getItem('imageExpandOnClick')!=='false')h.classList.add('image-expand-active');var ts=localStorage.getItem('scroller_thumb_size');if(ts&&['s','m','l','xl'].includes(ts))h.dataset.thumbSize=ts;})();</script>
<style>
html { background-color: #000; color: #fff; }
+212 -7
View File
@@ -14,7 +14,9 @@
<!-- Nav links: desktop always-on, mobile hidden until toggled -->
<div class="nav-collapse" id="navbarContent">
<div class="nav-links">
@if(!session.is_anon)
<a id="nav-upload-link" style="cursor:pointer;"><i class="fa-solid fa-angle-up"></i> {{ t('nav.upload') }}</a>
@endif
@if(meme_creator)
<a id="nav-meme-link" href="/meme"><i class="fa-regular fa-image"></i> {{ t('nav.meme') }}</a>
@endif
@@ -42,7 +44,9 @@
<!-- Avatar dropdown -->
<div class="nav-user-dropdown">
<button class="nav-user-btn nav-avatar-btn" id="nav-user-toggle">
@if(session.avatar_file)
@if(session.is_anon)
<span class="nav-avatar-icon" style="font-size: 1.2em; margin-right: 6px; display: inline-flex; align-items: center; cursor: pointer;" onclick="event.stopPropagation(); const u = '{{ session.login.toLowerCase() }}'; if(typeof window.loadPageAjax==='function'){ window.loadPageAjax('/user/' + u, true); } else { window.location.href='/user/' + u; }" title="{{ session.login }}"><i class="fa-solid fa-user-secret"></i></span>
@elseif(session.avatar_file)
<img class="nav-avatar-img" src="/a/{{ session.avatar_file }}" onerror="this.src='/a/default.png'" onclick="event.stopPropagation(); if(typeof window.loadPageAjax==='function'){ window.loadPageAjax('/user/{!! session.user.toLowerCase() !!}', true); } else { window.location.href='/user/{!! session.user.toLowerCase() !!}'; }" title="{{ session.user }}">
@elseif(session.avatar && session.avatar > 0)
<img class="nav-avatar-img" src="/t/{{ session.avatar }}.webp" onerror="this.src='/a/default.png'" onclick="event.stopPropagation(); if(typeof window.loadPageAjax==='function'){ window.loadPageAjax('/user/{!! session.user.toLowerCase() !!}', true); } else { window.location.href='/user/{!! session.user.toLowerCase() !!}'; }" title="{{ session.user }}">
@@ -53,11 +57,18 @@
<span class="nav-avatar-caret"></span>
</button>
<div class="nav-user-menu" id="nav-user-menu">
@if(session.is_anon)
<a href="/user/{{ session.login.toLowerCase() }}">{{ t('nav.profile') }}</a>
@else
<a href="/user/{!! session.user.toLowerCase() !!}">{{ t('nav.profile') }}</a>
@if(userhalls_enabled)
<a href="/user/{!! session.user.toLowerCase() !!}/halls">{{ t('nav.my_halls') }}</a>
@endif
<a href="/user/{{ session.user.toLowerCase() }}/favs" class="mobile-only">{{ t('nav.favs') }}</a>
@endif
@if(enable_anonymous_access && session.is_anon)
<a href="#" id="nav-user-anon-identity-btn" onclick="event.preventDefault(); if(window.f0ckAnonSSH) { window.f0ckAnonSSH.openModal(); } else { const m = document.getElementById('anon-ssh-modal'); if (m) m.style.display='flex'; }"><i class="fa-solid fa-key"></i> Key Management</a>
@endif
<a href="/user/{{ (session.is_anon && session.login ? session.login : session.user).toLowerCase() }}/favs" class="mobile-only">{{ t('nav.favs') }}</a>
<a href="/settings" class="mobile-only">{{ t('nav.settings') }}</a>
<div class="nav-user-divider"></div>
<a href="/logout" class="mobile-only">{{ t('nav.logout') }}</a>
@@ -91,10 +102,10 @@
</div>
<!-- Quick Favs -->
<a href="/user/{{ session.user.toLowerCase() }}/favs" title="Favorites" class="desktop-only"><i class="fa-solid fa-heart"></i></a>
<a href="/user/{{ (session.is_anon && session.login ? session.login : session.user).toLowerCase() }}/favs" title="Favorites" class="desktop-only"><i class="fa-solid fa-heart"></i></a>
<!-- DM -->
@if(private_messages)
@if(private_messages && !session.is_anon)
<div id="nav-dm" class="nav-item-rel">
<a href="/messages" id="nav-dm-btn" title="Direct Messages">
<i class="fa-solid fa-envelope"></i>
@@ -261,13 +272,32 @@
<div class="nav-right-group">
<div class="nav-user-dropdown">
<button class="nav-user-btn" id="nav-visitor-toggle"><i class="fa-solid fa-user-secret"></i> {{ t('nav.guest') }} <span class="nav-avatar-caret"></span></button>
<div class="nav-user-menu" id="nav-visitor-menu">
<button class="nav-user-btn" id="nav-visitor-toggle">
<i id="nav-visitor-icon" class="fa-solid @if(enable_anonymous_access && session && session.is_anon) fa-user-secret @else fa-user @endif"></i>
<span id="nav-anon-label">@if(enable_anonymous_access && session && session.is_anon)anonymous@else guest @endif</span>
<span class="nav-avatar-caret"></span>
</button>
<div class="nav-user-menu" id="nav-visitor-menu" style="min-width: max-content; width: max-content; white-space: nowrap;">
@if(enable_anonymous_access && session && session.is_anon)
<a href="/favs" id="nav-guest-favs-link" class="mobile-only">{{ t('nav.favs') }}</a>
@endif
@if(enable_anonymous_access)
<a href="#" id="nav-login-anon-btn" @if(session && session.is_anon) style="display:none;" @endif><i class="fa-solid fa-user-secret"></i> Login as anonymous</a>
<a href="#" id="nav-anon-identity-btn" @if(!session || !session.is_anon) style="display:none;" @endif onclick="event.preventDefault(); if(window.f0ckAnonSSH) { window.f0ckAnonSSH.openModal(); } else { const m = document.getElementById('anon-ssh-modal'); if (m) m.style.display='flex'; }"><i class="fa-solid fa-key"></i> Key Management</a>
<a href="/settings" id="nav-anon-settings-btn" @if(!session || !session.is_anon) style="display:none;" @endif><i class="fa-solid fa-gear"></i> Settings</a>
@endif
<a href="#" id="nav-login-btn">{{ t('nav.login') }}</a>
<a href="#" id="nav-register-btn">{{ t('nav.register') }}</a>
<div class="nav-user-divider"></div>
@if(enable_anonymous_access)
<div class="nav-user-divider" id="nav-anon-divider" @if(!session || !session.is_anon) style="display:none;" @endif></div>
<a href="#" id="nav-anon-logout-btn" @if(!session || !session.is_anon) style="display:none;" @endif><i class="fa-solid fa-right-from-bracket"></i> Logout (Guest)</a>
@endif
</div>
</div>
@if(enable_anonymous_access && session && session.is_anon)
<!-- Quick Favs for Anon -->
<a href="/favs" id="nav-guest-favs" title="{{ t('nav.favs') }}" class="desktop-only"><i class="fa-solid fa-heart"></i></a>
@endif
<button class="navbar-toggler" type="button" id="nav-toggler"
onclick="document.getElementById('navbarContent').classList.toggle('show'); this.classList.toggle('is-open')">
<span></span><span></span><span></span>
@@ -309,6 +339,13 @@
{{ t('auth.no_account') }} <a href="#" id="login-to-register" style="color: var(--accent); text-decoration: underline;">{{ t('auth.register_now') }}</a>
</p>
@endif
@if(enable_anonymous_access)
<div style="margin-top: 15px; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 12px; text-align: center;">
<button type="button" id="modal-login-as-anon-btn" class="btn btn-sm" style="background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.2); color: #ddd; border-radius: 4px; padding: 7px 14px; font-size: 0.88em; cursor: pointer; width: 100%; transition: background 0.2s;">
<i class="fa-solid fa-user-secret"></i> Login as anonymous
</button>
</div>
@endif
</form>
</div>
@@ -346,6 +383,174 @@
</div>
</div>
@if(enable_anonymous_access)
<!-- Anonymous OpenSSH Ed25519 Identity Modal -->
<div id="anon-ssh-modal" style="display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.75); backdrop-filter: blur(4px); z-index: 99999; align-items: center; justify-content: center;">
<div class="login-modal-content" style="max-width: 540px; width: 92vw; max-height: 90vh; overflow-y: auto; text-align: left; padding: 25px; border: 1px solid rgba(255,255,255,0.15); border-radius: 8px; background: var(--bg-primary, #111); box-shadow: 0 10px 40px rgba(0,0,0,0.8);">
<button id="anon-ssh-modal-close" style="position: absolute; top: 15px; right: 15px; background: none; border: none; color: var(--text-muted, #aaa); font-size: 1.4em; cursor: pointer; line-height: 1;">&times;</button>
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 8px;">
<i class="fa-solid fa-key" style="color: var(--accent, #0096ff); font-size: 1.3em;"></i>
<h3 style="margin: 0; font-size: 1.25em; color: var(--text-color, #fff);">Anonymous SSH Identity</h3>
</div>
<p style="margin: 0 0 15px 0; font-size: 0.85em; color: var(--text-muted, #aaa); line-height: 1.4;">
Your browser holds an <strong>OpenSSH Ed25519</strong> private key. Your comments and favorites belong to this key without needing a password.
</p>
<!-- Tabs -->
<div style="display: flex; gap: 8px; margin-bottom: 15px; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 8px;">
<button type="button" id="anon-tab-btn-identity" class="btn btn-sm btn-primary" style="font-size: 0.85em; padding: 5px 12px;">My Identity</button>
<button type="button" id="anon-tab-btn-import" class="btn btn-sm btn-secondary" style="font-size: 0.85em; padding: 5px 12px;">Import / Supply Key</button>
</div>
<!-- Tab 1: Current Identity -->
<div id="anon-tab-identity">
<div style="margin-bottom: 12px;">
<label style="display: block; font-size: 0.8em; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-muted, #888); margin-bottom: 4px;">Fingerprint</label>
<div style="display: flex; align-items: center; gap: 8px; background: rgba(0,0,0,0.4); padding: 8px 12px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.08);">
<code id="anon-ssh-fp-display" style="font-family: monospace; font-size: 0.88em; color: var(--accent, #00d2ff); word-break: break-all; flex: 1;">Generating...</code>
<button type="button" id="anon-copy-fp-btn" title="Copy Fingerprint" class="btn btn-sm" style="background: transparent; border: none; color: #aaa; cursor: pointer; padding: 4px;"><i class="fa-solid fa-copy"></i></button>
</div>
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; font-size: 0.8em; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-muted, #888); margin-bottom: 4px;">OpenSSH Public Key (<code>id_ed25519.pub</code>)</label>
<textarea id="anon-ssh-pub-display" readonly rows="2" style="width: 100%; box-sizing: border-box; font-family: monospace; font-size: 0.82em; background: rgba(0,0,0,0.4); color: #ddd; border: 1px solid rgba(255,255,255,0.08); border-radius: 4px; padding: 8px; resize: none;"></textarea>
</div>
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
<button type="button" id="anon-copy-pub-btn" class="btn btn-sm btn-secondary" style="font-size: 0.85em; padding: 6px 12px;"><i class="fa-solid fa-copy"></i> Copy Public Key</button>
<button type="button" id="anon-dl-priv-btn" class="btn btn-sm btn-primary" style="font-size: 0.85em; padding: 6px 12px;"><i class="fa-solid fa-download"></i> Download id_ed25519</button>
<button type="button" id="anon-dl-pub-btn" class="btn btn-sm btn-secondary" style="font-size: 0.85em; padding: 6px 12px;"><i class="fa-solid fa-download"></i> Download id_ed25519.pub</button>
</div>
</div>
<!-- Tab 2: Import Key -->
<div id="anon-tab-import" style="display: none;">
<p style="font-size: 0.85em; color: var(--text-muted, #aaa); margin-top: 0; margin-bottom: 10px;">
Paste your existing <code>id_ed25519</code> OpenSSH private key or raw 32-byte seed to restore your anonymous identity on this browser.
</p>
<textarea id="anon-import-key-input" rows="4" placeholder="-----BEGIN OPENSSH PRIVATE KEY-----&#10;...&#10;-----END OPENSSH PRIVATE KEY-----" style="width: 100%; box-sizing: border-box; font-family: monospace; font-size: 0.82em; background: rgba(0,0,0,0.4); color: #ddd; border: 1px solid rgba(255,255,255,0.08); border-radius: 4px; padding: 8px; resize: vertical; margin-bottom: 10px;"></textarea>
<input type="file" id="anon-import-file-elem" style="display: none;" />
<div style="display: flex; gap: 8px; align-items: center;">
<button type="button" id="anon-upload-key-btn" class="btn btn-sm btn-secondary" style="font-size: 0.85em; padding: 6px 12px;"><i class="fa-solid fa-upload"></i> Upload File</button>
<button type="button" id="anon-submit-import-btn" class="btn btn-sm btn-primary" style="font-size: 0.85em; padding: 6px 16px;"><i class="fa-solid fa-check"></i> Activate Key</button>
</div>
<div id="anon-import-status" style="margin-top: 10px; font-size: 0.85em; display: none;"></div>
</div>
</div>
</div>
<script>
(function(){
// Tab switching
const tabBtnId = document.getElementById('anon-tab-btn-identity');
const tabBtnImp = document.getElementById('anon-tab-btn-import');
const tabId = document.getElementById('anon-tab-identity');
const tabImp = document.getElementById('anon-tab-import');
if(tabBtnId && tabBtnImp && tabId && tabImp){
tabBtnId.addEventListener('click', function(){
tabId.style.display = 'block';
tabImp.style.display = 'none';
tabBtnId.classList.remove('btn-secondary'); tabBtnId.classList.add('btn-primary');
tabBtnImp.classList.remove('btn-primary'); tabBtnImp.classList.add('btn-secondary');
});
tabBtnImp.addEventListener('click', function(){
tabId.style.display = 'none';
tabImp.style.display = 'block';
tabBtnImp.classList.remove('btn-secondary'); tabBtnImp.classList.add('btn-primary');
tabBtnId.classList.remove('btn-primary'); tabBtnId.classList.add('btn-secondary');
});
}
// Copy buttons
const copyPubBtn = document.getElementById('anon-copy-pub-btn');
if(copyPubBtn){
copyPubBtn.addEventListener('click', function(){
if(window.f0ckAnonSSH) window.f0ckAnonSSH.copyPublicKey();
});
}
const copyFpBtn = document.getElementById('anon-copy-fp-btn');
if(copyFpBtn){
copyFpBtn.addEventListener('click', function(){
const fp = document.getElementById('anon-ssh-fp-display')?.textContent;
if(fp && navigator.clipboard){
navigator.clipboard.writeText(fp).then(function(){
if(typeof window.showToastNotification === 'function') window.showToastNotification('Fingerprint copied!');
});
}
});
}
// Download buttons
const dlPriv = document.getElementById('anon-dl-priv-btn');
if(dlPriv){
dlPriv.addEventListener('click', function(){
if(window.f0ckAnonSSH) window.f0ckAnonSSH.downloadPrivateKey();
});
}
const dlPub = document.getElementById('anon-dl-pub-btn');
if(dlPub){
dlPub.addEventListener('click', function(){
if(window.f0ckAnonSSH) window.f0ckAnonSSH.downloadPublicKey();
});
}
// File upload trigger
const uploadBtn = document.getElementById('anon-upload-key-btn');
const fileElem = document.getElementById('anon-import-file-elem');
const inputElem = document.getElementById('anon-import-key-input');
if(uploadBtn && fileElem){
uploadBtn.addEventListener('click', function(){ fileElem.click(); });
fileElem.addEventListener('change', function(){
if(fileElem.files && fileElem.files[0]){
const reader = new FileReader();
reader.onload = function(e){
if(inputElem) inputElem.value = e.target.result;
};
reader.readAsText(fileElem.files[0]);
}
});
}
// Submit import
const submitImpBtn = document.getElementById('anon-submit-import-btn');
const statusEl = document.getElementById('anon-import-status');
if(submitImpBtn){
submitImpBtn.addEventListener('click', async function(){
const keyVal = inputElem?.value;
if(!keyVal || !keyVal.trim()){
if(statusEl){ statusEl.style.display = 'block'; statusEl.style.color = '#ff4444'; statusEl.textContent = 'Please paste a key or select a file'; }
return;
}
try {
submitImpBtn.disabled = true;
submitImpBtn.textContent = 'Importing...';
await window.f0ckAnonSSH.importKey(keyVal);
if(statusEl){
statusEl.style.display = 'block';
statusEl.style.color = '#00C851';
statusEl.textContent = 'Key activated successfully! Reloading...';
}
setTimeout(function(){ window.location.reload(); }, 800);
} catch(err){
submitImpBtn.disabled = false;
submitImpBtn.innerHTML = '<i class="fa-solid fa-check"></i> Activate Key';
if(statusEl){
statusEl.style.display = 'block';
statusEl.style.color = '#ff4444';
statusEl.textContent = 'Import error: ' + (err.message || err);
}
}
});
}
})();
</script>
@endif
@if(!private_society || session)
<!-- Shortcuts Modal -->
<div id="shortcuts-modal" style="display: none;">