972 lines
56 KiB
HTML
972 lines
56 KiB
HTML
@include(snippets/header)
|
|
<div class="pagewrapper">
|
|
<div id="main">
|
|
<div class="settings">
|
|
<h1><i class="fa-solid fa-sliders"></i> {{ t('settings.title') }}</h1>
|
|
|
|
<!-- Quick navigation -->
|
|
<nav id="settings-quicknav" aria-label="Settings sections">
|
|
<a href="#profile"><i class="fa-solid fa-user-pen"></i> {{ t('settings.profile') }}</a>
|
|
<a href="#preferences"><i class="fa-solid fa-sliders"></i> {{ t('settings.preferences') }}</a>
|
|
@if(enable_data_export)
|
|
<a href="#export"><i class="fa-solid fa-file-export"></i> {{ t('settings.export_data_title') }}</a>
|
|
@endif
|
|
<a href="#account"><i class="fa-solid fa-shield-halved"></i> {{ t('settings.account') }}</a>
|
|
@if(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)
|
|
<a href="#apikey"><i class="fa-solid fa-key"></i> API Key</a>
|
|
@endif
|
|
@if(enable_user_invites)
|
|
<a href="#invites"><i class="fa-solid fa-ticket"></i> {{ t('invites.section_title') }}</a>
|
|
@endif
|
|
</nav>
|
|
<style>
|
|
/* Push content down so the fixed nav doesn't overlap section headings */
|
|
.settings { padding-top: 56px; }
|
|
</style>
|
|
<script>
|
|
(function(){
|
|
var nav = document.getElementById('settings-quicknav');
|
|
if (!nav) return;
|
|
|
|
function setActive(href) {
|
|
nav.querySelectorAll('a').forEach(function(a) {
|
|
a.classList.toggle('active', a.getAttribute('href') === href);
|
|
});
|
|
}
|
|
|
|
function highlightTarget(hash) {
|
|
if (!hash) return;
|
|
var el = document.querySelector(hash);
|
|
if (!el) return;
|
|
document.querySelectorAll('.anchor-target').forEach(function(c) {
|
|
c.classList.remove('anchor-target');
|
|
});
|
|
el.classList.add('anchor-target');
|
|
}
|
|
|
|
function initFromHash() {
|
|
var hash = location.hash;
|
|
if (hash && nav.querySelector('a[href="' + hash + '"]')) {
|
|
setActive(hash);
|
|
highlightTarget(hash);
|
|
} else {
|
|
var first = nav.querySelector('a[href^="#"]');
|
|
if (first) setActive(first.getAttribute('href'));
|
|
}
|
|
}
|
|
|
|
// Run now (direct page load) and after a tick (AJAX: URL updated after script runs)
|
|
initFromHash();
|
|
setTimeout(initFromHash, 50);
|
|
|
|
// Click: set active + highlight target immediately
|
|
nav.addEventListener('click', function(e) {
|
|
var link = e.target.closest('a[href^="#"]');
|
|
if (!link) return;
|
|
var href = link.getAttribute('href');
|
|
setActive(href);
|
|
requestAnimationFrame(function() { highlightTarget(href); });
|
|
});
|
|
})();
|
|
</script>
|
|
|
|
|
|
<!-- ═══════════════════════════════ PROFILE ═══════════════════════════════ -->
|
|
<h2 id="profile"><i class="fa-solid fa-user-pen"></i> {{ t('settings.profile') }}</h2>
|
|
|
|
<p class="text-muted" style="margin-bottom: 16px; font-size: 0.9em; display: flex; align-items: center; gap: 8px;">
|
|
<i class="fa-solid fa-wand-magic-sparkles" style="color: var(--accent);"></i> Click directly on your profile card below to edit your avatar, banner, name, color, or bio.
|
|
</p>
|
|
|
|
<div class="direct-edit-profile-card-wrapper" style="position: relative; max-width: 750px; margin-bottom: 30px;">
|
|
|
|
<!-- Interactive User Card -->
|
|
<div class="user-infobox-block editable-profile-card" id="live-profile-preview-box"
|
|
style="--author-accent: {{ session.username_color || 'var(--accent)' }}; --author-border: {{ session.username_color || 'var(--accent)' }}; @if(banner_file && user_banner_enabled) --author-banner: url('/a/{{ banner_file }}?t={{ Date.now() }}'); --author-banner-position: {{ banner_position === 'center' ? 'center top' : (banner_position || 'center top') }}; --author-banner-size: {{ banner_size || 'cover' }}; @endif">
|
|
|
|
<!-- Avatar with Hover Overlay -->
|
|
<div class="user-infobox-avatar editable-avatar-container" id="avatar-choose-btn" title="Click to change avatar">
|
|
<a href="#" onclick="event.preventDefault(); document.getElementById('avatar-file-input').click();" style="display:block; position:relative; width:100%; height:100%;">
|
|
@if(avatar_file)
|
|
<img id="live-preview-avatar" class="avatar-preview-img" src="/a/{{ avatar_file }}" />
|
|
@elseif(session.avatar && session.avatar > 0)
|
|
<img id="live-preview-avatar" class="avatar-preview-img" src="/t/{{ session.avatar }}.webp" />
|
|
@else
|
|
<img id="live-preview-avatar" class="avatar-preview-img" src="/a/default.png" style="border-color: #444;" />
|
|
@endif
|
|
<div class="avatar-hover-overlay">
|
|
<i class="fa-solid fa-camera"></i>
|
|
</div>
|
|
</a>
|
|
@if(avatar_file)
|
|
<button type="button" class="avatar-quick-remove" id="avatar-remove-btn" title="Remove custom avatar">
|
|
<i class="fa-solid fa-xmark"></i>
|
|
</button>
|
|
@endif
|
|
</div>
|
|
|
|
<div class="user-infobox-info">
|
|
<div class="user-infobox-header">
|
|
<div class="user-infobox-username-container" style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
|
|
|
|
<!-- Editable Display Name -->
|
|
<div class="inline-edit-username-wrapper">
|
|
<span id="live-preview-username" class="user-infobox-username" contenteditable="true" spellcheck="false" title="Click to edit display name">{!! session.display_name || session.user !!}</span>
|
|
<i class="fa-solid fa-pen" style="font-size: 0.65em; opacity: 0.5; margin-left: 2px;" title="Editable"></i>
|
|
</div>
|
|
|
|
<!-- Color Picker Badge -->
|
|
<div class="inline-color-picker-wrapper" style="display: inline-flex; align-items: center;">
|
|
<label for="username_color_picker" class="color-picker-badge" id="color-picker-badge" style="background: {{ session.username_color || 'var(--accent)' }};" title="Click to change username color">
|
|
<i class="fa-solid fa-palette"></i>
|
|
</label>
|
|
<input type="color" id="username_color_picker" value="{{ session.username_color || '#ffffff' }}" style="position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;">
|
|
<input type="text" id="username_color_hex" value="{{ session.username_color || '#ffffff' }}" style="display: none;">
|
|
</div>
|
|
|
|
<!-- Hidden Backing Save Buttons for JS event trigger -->
|
|
<button type="button" id="btn-update-display-name" class="inline-save-btn" style="display: none;"><i class="fa-solid fa-check"></i> Save Name</button>
|
|
<button type="button" id="btn-save-username-color" style="display: none;">Save Color</button>
|
|
<button type="button" id="btn-reset-username-color" style="display: none;">Reset</button>
|
|
|
|
</div>
|
|
<span class="user-infobox-timestamp"><a href="#" class="timestamp-link"><time>just now</time></a></span>
|
|
</div>
|
|
|
|
<div class="user-infobox-body">
|
|
|
|
<!-- Editable Description -->
|
|
<div class="inline-edit-description-wrapper">
|
|
<div class="user-infobox-description" id="live-preview-description" contenteditable="true" spellcheck="false" title="Click to edit description" data-placeholder="Click to write a bio...">
|
|
{!! session.description || '' !!}
|
|
</div>
|
|
<div class="description-edit-controls" id="desc-edit-controls" style="display: flex; margin-top: 10px; gap: 8px; align-items: center; flex-wrap: wrap;">
|
|
<button type="button" id="btn-save-description" class="button button-sm" style="padding: 4px 10px; font-size: 0.8em;"><i class="fa-solid fa-floppy-disk"></i> {{ t('settings.save_description') }}</button>
|
|
<button type="button" id="btn-clear-description" class="button button-danger button-sm" style="padding: 4px 10px; font-size: 0.8em;"><i class="fa-solid fa-trash"></i> {{ t('settings.clear') }}</button>
|
|
@if(user_banner_enabled)
|
|
<button type="button" id="banner-choose-btn" class="button button-sm" style="padding: 4px 10px; font-size: 0.8em;"><i class="fa-solid fa-camera"></i> Change Banner</button>
|
|
@if(banner_file)
|
|
<button type="button" id="banner-edit-btn" class="button button-sm" style="padding: 4px 10px; font-size: 0.8em;"><i class="fa-solid fa-arrows-up-down-left-right"></i> Reposition</button>
|
|
<button type="button" id="banner-remove-btn" class="button button-danger button-sm" style="padding: 4px 10px; font-size: 0.8em;"><i class="fa-solid fa-trash"></i> Remove Banner</button>
|
|
@endif
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
<div class="user-infobox-actions">
|
|
<i class="iconset fa-solid fa-heart" title="Favorite"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- Hidden Backing Elements for JavaScript compatibility -->
|
|
<input type="file" id="avatar-file-input" accept="image/gif,image/jpeg,image/png,image/webp" style="display: none;">
|
|
<span id="avatar-filename" style="display: none;"></span>
|
|
<button type="button" id="avatar-upload-btn" style="display: none;"></button>
|
|
<div id="avatar-progress-wrapper" style="display: none;">
|
|
<div id="avatar-progress-fill"></div>
|
|
<span id="avatar-progress-text"></span>
|
|
</div>
|
|
|
|
<input type="file" id="banner-file-input" accept="image/gif,image/jpeg,image/png,image/webp" style="display: none;">
|
|
<span id="banner-filename" style="display: none;"></span>
|
|
<button type="button" id="banner-upload-btn" style="display: none;"></button>
|
|
<div id="banner-progress-wrapper" style="display: none;">
|
|
<div id="banner-progress-fill"></div>
|
|
<span id="banner-progress-text"></span>
|
|
</div>
|
|
|
|
<input type="text" id="display_name_input" value="{!! session.display_name || '' !!}" style="display: none;">
|
|
@if(enable_profile_description)
|
|
<textarea id="profile_description" style="display: none;">{!! session.description || '' !!}</textarea>
|
|
@endif
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- ═══════════════════════════════ PREFERENCES ═══════════════════════════════ -->
|
|
<h2 id="preferences"><i class="fa-solid fa-sliders"></i> {{ t('settings.preferences') }}</h2>
|
|
<div class="preferences-settings-wrapper">
|
|
|
|
<!-- 1. Appearance & Theme -->
|
|
<fieldset style="border: 1px solid var(--nav-border-color); padding: 12px 16px; border-radius: 6px; margin-bottom: 20px; background: rgba(0,0,0,0.06);">
|
|
<legend style="width: auto; padding: 0 8px; font-size: 1.05em; font-weight: bold;"><i class="fa-solid fa-palette"></i> {{ t('settings.appearance_section') }}</legend>
|
|
|
|
<div class="setting-item" style="margin-top: 10px;">
|
|
<label for="website_theme_select" style="display: block; margin-bottom: 5px; font-weight: bold;">{{ t('settings.theme') }}</label>
|
|
<div style="display: flex; align-items: center; gap: 10px;">
|
|
<select id="website_theme_select" class="input" style="flex: 1; max-width: 300px;">
|
|
@each(themes as t)
|
|
<option value="{{ t }}" @if(theme===t) selected @endif>{{ t }}</option>
|
|
@endeach
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="setting-item" style="margin-top: 18px;">
|
|
<label for="website_font_select" style="display: block; margin-bottom: 5px; font-weight: bold;">{{ t('settings.website_font') }}</label>
|
|
<div style="display: flex; align-items: center; gap: 10px;">
|
|
<select id="website_font_select" class="input" style="flex: 1; max-width: 300px;">
|
|
<option value="">{{ t('settings.font_default') }}</option>
|
|
@each(fonts as font)
|
|
<option value="{{ font.file }}" @if(session.font===font.file) selected @endif>{{ font.name }}</option>
|
|
@endeach
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
@if(allow_language_change)
|
|
<div class="setting-item" style="margin-top: 18px;">
|
|
<label for="language_select" style="display: block; margin-bottom: 5px; font-weight: bold;">{{ t('settings.language') }}</label>
|
|
<select id="language_select" class="input" style="padding: 6px 10px; max-width: 300px;">
|
|
<option value="" @if(!session.language) selected @endif>{{ t('settings.language_default') }}</option>
|
|
<option value="en" @if(session.language==='en' ) selected @endif>{{ t('settings.language_en') }}</option>
|
|
<option value="de" @if(session.language==='de' ) selected @endif>{{ t('settings.language_de') }}</option>
|
|
<option value="nl" @if(session.language==='nl' ) selected @endif>{{ t('settings.language_nl') }}</option>
|
|
<option value="zange" @if(session.language==='zange' ) selected @endif>{{ t('settings.language_zange') }}</option>
|
|
</select>
|
|
<small class="text-muted" style="display: block; margin-top: 4px;">{{ t('settings.language_hint') }}</small>
|
|
</div>
|
|
@endif
|
|
|
|
<div class="setting-item" style="margin-top: 18px;">
|
|
<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>
|
|
<span>{{ t('settings.enable_bg_blur') }}</span>
|
|
</label>
|
|
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.enable_bg_blur_hint') }}</small>
|
|
</div>
|
|
|
|
<div class="setting-item" style="margin-top: 15px;">
|
|
<label for="show_motd_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
|
<input type="checkbox" id="show_motd_toggle" @if(session.show_motd !==false) checked @endif>
|
|
<span>{{ t('settings.show_motd') }}</span>
|
|
</label>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<!-- 2. Interface & Layout -->
|
|
<fieldset style="border: 1px solid var(--nav-border-color); padding: 12px 16px; border-radius: 6px; margin-bottom: 20px; background: rgba(0,0,0,0.06);">
|
|
<legend style="width: auto; padding: 0 8px; font-size: 1.05em; font-weight: bold;"><i class="fa-solid fa-desktop"></i> {{ t('settings.ui_section') }}</legend>
|
|
|
|
<div class="setting-item">
|
|
<label for="use_new_layout_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
|
<input type="checkbox" id="use_new_layout_toggle" @if(session.use_new_layout===true) checked @endif>
|
|
<span>{{ t('settings.modern_layout') }}</span>
|
|
</label>
|
|
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.modern_layout_hint') }}</small>
|
|
</div>
|
|
|
|
@if(!session.use_new_layout)
|
|
<div class="setting-item" style="margin-top: 15px;">
|
|
<label for="alternative_infobox_toggle"
|
|
style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
|
<input type="checkbox" id="alternative_infobox_toggle" @if(session.use_alternative_infobox===true) checked @endif>
|
|
<span>{{ t('settings.alternative_infobox') }}</span>
|
|
</label>
|
|
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.alternative_infobox_hint') }}</small>
|
|
</div>
|
|
<div class="setting-item" style="margin-top: 15px;">
|
|
<label for="alternative_steuerung_toggle"
|
|
style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
|
<input type="checkbox" id="alternative_steuerung_toggle" @if(session.use_alternative_steuerung===true) checked @endif>
|
|
<span>{{ t('settings.alternative_steuerung') }}</span>
|
|
</label>
|
|
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.alternative_steuerung_hint') }}</small>
|
|
</div>
|
|
@endif
|
|
|
|
@if(show_koepfe)
|
|
<div class="setting-item" style="margin-top: 15px;">
|
|
<label for="hide_koepfe_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
|
<input type="checkbox" id="hide_koepfe_toggle" @if(session.hide_koepfe===true) checked @endif>
|
|
<span>{{ t('settings.hide_koepfe') }}</span>
|
|
</label>
|
|
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.hide_koepfe_hint') }}</small>
|
|
</div>
|
|
@endif
|
|
</fieldset>
|
|
|
|
<!-- 3. Playback & Navigation Controls -->
|
|
<fieldset style="border: 1px solid var(--nav-border-color); padding: 12px 16px; border-radius: 6px; margin-bottom: 20px; background: rgba(0,0,0,0.06);">
|
|
<legend style="width: auto; padding: 0 8px; font-size: 1.05em; font-weight: bold;"><i class="fa-solid fa-play"></i> Playback & Navigation</legend>
|
|
|
|
<div class="setting-item">
|
|
<label for="disable_autoplay_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
|
<input type="checkbox" id="disable_autoplay_toggle" @if(session.disable_autoplay===true) checked @endif>
|
|
<span>{{ t('settings.disable_autoplay') }}</span>
|
|
</label>
|
|
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.disable_autoplay_hint') }}</small>
|
|
</div>
|
|
|
|
<div class="setting-item" style="margin-top: 15px;">
|
|
<label for="disable_swiping_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
|
<input type="checkbox" id="disable_swiping_toggle" @if(session.disable_swiping===true) checked @endif>
|
|
<span>{{ t('settings.disable_swiping') }}</span>
|
|
</label>
|
|
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.disable_swiping_hint') }}</small>
|
|
</div>
|
|
|
|
<div class="setting-item" style="margin-top: 15px;">
|
|
<label for="wheel_nav_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
|
<input type="checkbox" id="wheel_nav_toggle">
|
|
<span>{{ t('settings.scroll_nav') }}</span>
|
|
</label>
|
|
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.scroll_nav_hint') }}</small>
|
|
</div>
|
|
|
|
<div class="setting-item" style="margin-top: 15px;">
|
|
<label for="image_expand_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
|
<input type="checkbox" id="image_expand_toggle">
|
|
<span>{{ t('settings.image_expand_on_click') || 'Expand images inline on click' }}</span>
|
|
</label>
|
|
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.image_expand_on_click_hint') || 'Instead of opening the scroll zoom modal, clicking an image will expand it to full size within the page.' }}</small>
|
|
</div>
|
|
|
|
<div class="setting-item" style="margin-top: 15px;">
|
|
<label for="f_for_fullscreen_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
|
<input type="checkbox" id="f_for_fullscreen_toggle">
|
|
<span>{{ t('settings.f_for_fullscreen') || 'F is for Fullscreen' }}</span>
|
|
</label>
|
|
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.f_for_fullscreen_hint') || 'Pressing F will toggle fullscreen in the video player instead of favoriting the post.' }}</small>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<!-- 4. Comments & Discussion -->
|
|
<fieldset style="border: 1px solid var(--nav-border-color); padding: 12px 16px; border-radius: 6px; margin-bottom: 20px; background: rgba(0,0,0,0.06);">
|
|
<legend style="width: auto; padding: 0 8px; font-size: 1.05em; font-weight: bold;"><i class="fa-solid fa-comments"></i> Comments & Discussion</legend>
|
|
|
|
<div class="setting-item">
|
|
<label for="comment_display_mode_select" style="display: block; margin-bottom: 5px; font-weight: bold;">{{ t('settings.comment_display_mode') }}</label>
|
|
<select id="comment_display_mode_select" class="input" style="padding: 6px 10px; max-width: 300px;" @if(session.force_comment_display_mode) disabled @endif>
|
|
<option value="0" @if(session.comment_display_mode==0) selected @endif>{{ t('settings.comment_display_tree') }}</option>
|
|
<option value="1" @if(session.comment_display_mode==1) selected @endif>{{ t('settings.comment_display_linear') }}</option>
|
|
</select>
|
|
<small class="text-muted" style="display: block; margin-top: 4px;">
|
|
@if(session.force_comment_display_mode)
|
|
<strong>{{ t('settings.forced_mode_notice') || 'This setting is managed by an administrator.' }}</strong>
|
|
@else
|
|
{{ t('settings.comment_display_mode_hint') }}
|
|
@endif
|
|
</small>
|
|
</div>
|
|
|
|
<div class="setting-item" style="margin-top: 18px;">
|
|
<label for="quote_emojis_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
|
<input type="checkbox" id="quote_emojis_toggle" @if(session.quote_emojis !==false) checked @endif>
|
|
<span>{{ t('settings.render_emojis') }}</span>
|
|
</label>
|
|
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.render_emojis_hint') }}</small>
|
|
</div>
|
|
|
|
<div class="setting-item" style="margin-top: 15px;">
|
|
<label for="embed_youtube_in_comments_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
|
<input type="checkbox" id="embed_youtube_in_comments_toggle" @if(session.embed_youtube_in_comments !==false) checked @endif>
|
|
<span>{{ t('settings.embed_yt') }}</span>
|
|
</label>
|
|
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.embed_yt_hint') }}</small>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<!-- 5. Content Preferences & Blurring -->
|
|
<fieldset style="border: 1px solid var(--nav-border-color); padding: 12px 16px; border-radius: 6px; margin-bottom: 20px; background: rgba(0,0,0,0.06);">
|
|
<legend style="width: auto; padding: 0 8px; font-size: 1.05em; font-weight: bold;"><i class="fa-solid fa-eye-slash"></i> {{ t('settings.content_preferences_section') }}</legend>
|
|
|
|
<div class="setting-item">
|
|
<label for="blur_sfw_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
|
<input type="checkbox" id="blur_sfw_toggle">
|
|
<span>{{ t('settings.blur_sfw') }}</span>
|
|
</label>
|
|
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.blur_sfw_hint') }}</small>
|
|
</div>
|
|
|
|
<div class="setting-item" style="margin-top: 15px;">
|
|
<label for="blur_nsfw_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
|
<input type="checkbox" id="blur_nsfw_toggle">
|
|
<span>{{ t('settings.blur_nsfw') }}</span>
|
|
</label>
|
|
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.blur_nsfw_hint') }}</small>
|
|
</div>
|
|
|
|
<div class="setting-item" style="margin-top: 15px;">
|
|
<label for="blur_nsfl_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
|
<input type="checkbox" id="blur_nsfl_toggle">
|
|
<span>{{ t('settings.blur_nsfl') }}</span>
|
|
</label>
|
|
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.blur_nsfl_hint') }}</small>
|
|
</div>
|
|
|
|
<div class="setting-item" style="margin-top: 15px;">
|
|
<label for="blur_untagged_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
|
<input type="checkbox" id="blur_untagged_toggle">
|
|
<span>{{ t('settings.blur_untagged') }}</span>
|
|
</label>
|
|
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.blur_untagged_hint') }}</small>
|
|
</div>
|
|
|
|
<div class="setting-item" style="margin-top: 15px;">
|
|
<label for="blur_detail_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
|
<input type="checkbox" id="blur_detail_toggle">
|
|
<span>{{ t('settings.blur_detail') || 'Click to reveal item on detail page' }}</span>
|
|
</label>
|
|
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.blur_detail_hint') || 'Require clicking to reveal blurred media on the post detail page instead of showing it immediately.' }}</small>
|
|
</div>
|
|
|
|
<div class="setting-item" style="margin-top: 15px;">
|
|
<label for="hide_item_ratings_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
|
<input type="checkbox" id="hide_item_ratings_toggle">
|
|
<span>{{ t('settings.hide_item_ratings') || 'Hide item ratings' }}</span>
|
|
</label>
|
|
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.hide_item_ratings_hint') || 'Don\'t show SFW/NSFW rating indicators on main page items' }}</small>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<!-- 6. Privacy & Upload Defaults -->
|
|
<fieldset style="border: 1px solid var(--nav-border-color); padding: 12px 16px; border-radius: 6px; margin-bottom: 20px; background: rgba(0,0,0,0.06);">
|
|
<legend style="width: auto; padding: 0 8px; font-size: 1.05em; font-weight: bold;"><i class="fa-solid fa-user-shield"></i> Privacy & Defaults</legend>
|
|
|
|
<div class="setting-item">
|
|
<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 class="sub-setting-item" style="margin-left: 25px; margin-top: 10px;">
|
|
<label for="hide_fav_badge_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
|
<input type="checkbox" id="hide_fav_badge_toggle" @if(session.hide_fav_badge===true) checked @endif>
|
|
<span>{{ t('settings.hide_fav_badge') }}</span>
|
|
</label>
|
|
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.hide_fav_badge_hint') }}</small>
|
|
</div>
|
|
</div>
|
|
|
|
@if(allow_user_upload_visibility !== false)
|
|
<div class="setting-item" style="margin-top: 18px;">
|
|
<label for="default_upload_visibility_select" style="display: block; font-weight: bold; margin-bottom: 5px;">
|
|
<span>{{ t('settings.default_upload_visibility') }}</span>
|
|
</label>
|
|
<select id="default_upload_visibility_select" class="form-control" style="width: 220px; padding: 6px 10px; background: var(--bg-color); color: var(--text-color); border: 1px solid var(--nav-border-color); border-radius: 4px;">
|
|
<option value="0" @if(session.default_upload_visibility === 0 || (!session.default_upload_visibility && session.default_upload_visibility !== 0)) selected @endif>{{ t('visibility.public') }}</option>
|
|
<option value="1" @if(session.default_upload_visibility === 1) selected @endif>{{ t('visibility.unlisted') }}</option>
|
|
<option value="2" @if(session.default_upload_visibility === 2) selected @endif>{{ t('visibility.private') }}</option>
|
|
</select>
|
|
<small class="text-muted" style="display: block; margin-top: 5px;">{{ t('settings.default_upload_visibility_hint') }}</small>
|
|
</div>
|
|
@endif
|
|
</fieldset>
|
|
|
|
<!-- 7. Notifications -->
|
|
<fieldset style="border: 1px solid var(--nav-border-color); padding: 12px 16px; border-radius: 6px; margin-bottom: 20px; background: rgba(0,0,0,0.06);">
|
|
<legend style="width: auto; padding: 0 8px; font-size: 1.05em; font-weight: bold;"><i class="fa-solid fa-bell"></i> {{ t('settings.notifications_section') }}</legend>
|
|
|
|
<div class="setting-item">
|
|
<label class="checkbox-container" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
|
<input type="checkbox" id="chk-receive-system-notifications" @if(session.receive_system_notifications !==false) checked @endif>
|
|
<span>{{ t('settings.receive_system_notifications') }}</span>
|
|
</label>
|
|
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.receive_system_notifications_hint') }}</small>
|
|
</div>
|
|
|
|
<div class="setting-item" style="margin-top: 15px;">
|
|
<label class="checkbox-container" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
|
<input type="checkbox" id="chk-receive-user-notifications" @if(session.receive_user_notifications !==false) checked @endif>
|
|
<span>{{ t('settings.receive_user_notifications') }}</span>
|
|
</label>
|
|
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.receive_user_notifications_hint') }}</small>
|
|
</div>
|
|
|
|
<div class="setting-item" style="margin-top: 15px;">
|
|
<label class="checkbox-container" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
|
<input type="checkbox" id="chk-do-not-disturb" @if(session.do_not_disturb===true) checked @endif>
|
|
<span>{{ t('settings.do_not_disturb') }}</span>
|
|
</label>
|
|
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.do_not_disturb_hint') }}</small>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<!-- 8. Flash / SWF Settings (if enabled) -->
|
|
@if(enable_swf)
|
|
<fieldset style="border: 1px solid var(--nav-border-color); padding: 12px 16px; border-radius: 6px; margin-bottom: 20px; background: rgba(0,0,0,0.06);">
|
|
<legend style="width: auto; padding: 0 8px; font-size: 1.05em; font-weight: bold;"><i class="fa-solid fa-bolt"></i> {{ t('settings.flash_section') }}</legend>
|
|
|
|
<div class="setting-item">
|
|
<label for="ruffle_background_toggle"
|
|
style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
|
<input type="checkbox" id="ruffle_background_toggle" @if(session.ruffle_background !==false) checked @endif>
|
|
<span>{{ t('settings.flash_bg') }}</span>
|
|
</label>
|
|
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.flash_bg_hint') }}</small>
|
|
</div>
|
|
<div id="ruffle-settings-status" class="avatar-status" style="margin-top: 10px;"></div>
|
|
</fieldset>
|
|
@endif
|
|
|
|
</div>
|
|
|
|
@if(enable_data_export)
|
|
<!-- ═══════════════════════════════ EXPORT ═══════════════════════════════ -->
|
|
<h2 id="export"><i class="fa-solid fa-file-export"></i> {{ t('settings.export_data_title') || 'Export Data' }}</h2>
|
|
<div class="export-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>{{ t('settings.export_data_desc') || 'Download a copy of your data. This process happens entirely in your browser to protect your privacy and save server resources.' }}</p>
|
|
|
|
<div class="setting-item" style="margin-bottom: 15px;">
|
|
<label class="checkbox-container" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
|
<input type="checkbox" id="export_uploads" checked>
|
|
<span>{{ t('settings.export_uploads') || 'Your Uploads' }}</span>
|
|
</label>
|
|
</div>
|
|
<div class="setting-item" style="margin-bottom: 20px;">
|
|
<label class="checkbox-container" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
|
<input type="checkbox" id="export_favorites" checked>
|
|
<span>{{ t('settings.export_favorites') || 'Your Favorites' }}</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div id="export-progress-container" style="display: none; margin-bottom: 20px;">
|
|
<div class="progress-bar-wrapper" style="height: 20px; background: rgba(255,255,255,0.1); border-radius: 10px; overflow: hidden; margin-bottom: 5px;">
|
|
<div id="export-progress-bar" style="height: 100%; width: 0%; background: var(--accent); transition: width 0.3s;"></div>
|
|
</div>
|
|
<div id="export-status-text"
|
|
style="font-size: 0.9em; color: var(--text-muted);"
|
|
data-fetching="{{ t('settings.export_fetching_data') }}"
|
|
data-processing="{{ t('settings.export_processing_files') }}"
|
|
data-generating="{{ t('settings.export_generating_zip') }}"
|
|
data-complete="{{ t('settings.export_complete') }}"
|
|
data-failed="{{ t('settings.export_failed') }}"
|
|
data-select-option="{{ t('settings.export_select_option') }}"
|
|
data-no-data="{{ t('settings.export_no_data') }}"
|
|
data-failed-alert="{{ t('settings.export_failed_alert') }}"
|
|
data-domain="{{ site_domain }}"
|
|
><span id="export-status-msg">{{ t('settings.export_preparing') || 'Preparing...' }}</span><span class="export-dots" id="export-animated-dots" style="display:none;"><span>.</span><span>.</span><span>.</span></span></div>
|
|
</div>
|
|
|
|
<button type="button" id="btn-start-export" class="button button-primary">
|
|
<i class="fa-solid fa-download" style="margin-right: 5px;"></i> {{ t('settings.start_export') || 'Generate Export (ZIP)' }}
|
|
</button>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- ═══════════════════════════════ ACCOUNT ═══════════════════════════════ -->
|
|
<h2 id="account"><i class="fa-solid fa-shield-halved"></i> {{ t('settings.account') }}</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;">
|
|
<table class="table account-info-table"
|
|
style="margin-bottom: 30px; border-collapse: separate; border-spacing: 0 5px;">
|
|
<tbody>
|
|
<tr>
|
|
<td style="width: 150px; font-weight: bold; color: var(--text-muted); border: none;">{{ t('settings.user_id') }}</td>
|
|
<td style="border: none;">{{ session.id }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="font-weight: bold; color: var(--text-muted); border: none;">{{ t('settings.username') }}</td>
|
|
<td style="border: none;">{!! session.user !!}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="font-weight: bold; color: var(--text-muted); border: none;">{{ t('settings.email') }}</td>
|
|
<td id="display-email" style="border: none;">{{ email || t('settings.email_not_set') }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="font-weight: bold; color: var(--text-muted); border: none;">{{ t('settings.joined') }}</td>
|
|
<td style="border: none;">@if(joined){{ new Date(joined).toLocaleDateString() }}@else {{ t('settings.joined_unknown') }} @endif</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<div class="account-actions-grid"
|
|
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 30px;">
|
|
<div class="password-change-section" style="display: flex; flex-direction: column; gap: 15px;">
|
|
<h4 style="margin-top: 0; color: var(--accent);"><i class="fa-solid fa-key"></i> {{ t('settings.change_password') }}</h4>
|
|
<form id="password-change-form" style="display: flex; flex-direction: column; gap: 10px;">
|
|
<input type="password" id="current_password" class="input"
|
|
placeholder="{{ t('settings.current_password') }}" required autocomplete="current-password">
|
|
<input type="password" id="new_password" class="input" placeholder="{{ t('settings.new_password') }}"
|
|
required minlength="20" autocomplete="new-password">
|
|
<input type="password" id="new_password_confirm" class="input"
|
|
placeholder="{{ t('settings.confirm_new_password') }}" required minlength="20"
|
|
autocomplete="new-password">
|
|
<button type="submit" id="btn-update-password" class="button">{{ t('settings.update_password') }}</button>
|
|
</form>
|
|
<div id="password-status" class="avatar-status"></div>
|
|
</div>
|
|
|
|
<div class="email-update-section" style="display: flex; flex-direction: column; gap: 15px;">
|
|
<h4 style="margin-top: 0; color: var(--accent);"><i class="fa-solid fa-envelope"></i> {{ t('settings.update_email') }}</h4>
|
|
<form id="email-update-form" style="display: flex; flex-direction: column; gap: 10px;">
|
|
<input type="email" id="email_input" class="input" placeholder="{{ t('settings.new_email') }}"
|
|
value="{{ email }}" required>
|
|
<button type="submit" id="btn-update-email" class="button">{{ t('settings.update_email_btn') }}</button>
|
|
</form>
|
|
@if(smtp_enabled)
|
|
{!! t('settings.email_warning_smtp') !!}
|
|
@else
|
|
{!! t('settings.email_info_no_smtp') !!}
|
|
@endif
|
|
<div id="email-status" class="avatar-status"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@if(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;">
|
|
<p>{{ t('settings.matrix_link_desc') }}</p>
|
|
|
|
<div id="linked-accounts-container" style="margin-bottom: 1rem;">
|
|
<strong>{{ t('settings.active_links') }}</strong>
|
|
<div id="linked-accounts-list" style="margin-top: 0.5rem; display: flex; flex-direction: column; gap: 5px;">
|
|
<span class="text-muted">{{ t('settings.loading') }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="link-action-area">
|
|
<h4 style="margin-top: 20px;">{{ t('settings.add_new_link') }}</h4>
|
|
<p style="margin-bottom: 10px; color: var(--text-muted); font-size: 0.9em;">
|
|
{!! t('settings.matrix_instructions') !!}
|
|
</p>
|
|
@if(telegram_enabled)
|
|
<p style="margin-bottom: 15px; color: var(--text-muted); font-size: 0.9em;">
|
|
<strong>Telegram:</strong> Generate a token below, then send <code>!link <TOKEN></code> to the Telegram bot.
|
|
</p>
|
|
@endif
|
|
|
|
<div id="token-display-area" style="display: none; margin-bottom: 15px;">
|
|
<div class="alert alert-info">
|
|
<strong>{{ t('settings.your_token') }}</strong> <code id="generated-token-code"
|
|
style="font-size: 1.2em; user-select: all; margin-left: 10px;"></code>
|
|
<br><small>{{ t('settings.one_time_use') }}</small>
|
|
</div>
|
|
</div>
|
|
|
|
<button id="btn-gen-link-token" class="button button-primary">{{ t('settings.generate_token') }}</button>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
@if(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"
|
|
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: 15px;">
|
|
Use this key to upload files via external tools or scripts like ShareX.
|
|
The key grants <strong>upload access only</strong> to your account.
|
|
</p>
|
|
|
|
<div id="api-key-status-box" style="margin-bottom: 15px;">
|
|
<span class="text-muted" id="api-key-loading-text">Loading…</span>
|
|
</div>
|
|
|
|
<!-- Shown only after regenerate — full key revealed once for copying -->
|
|
<div id="api-key-reveal" style="display:none; margin-bottom: 15px;">
|
|
<div style="background: rgba(0,180,100,0.12); border: 1px solid rgba(0,180,100,0.4); border-radius: 4px; padding: 14px;">
|
|
<strong style="display:block; margin-bottom: 6px;">⚠ Copy your key now — it will not be shown again in full:</strong>
|
|
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
|
|
<code id="api-key-full-display"
|
|
style="flex: 1; word-break: break-all; font-size: 0.85em; background: rgba(0,0,0,0.25); padding: 8px 10px; border-radius: 4px; user-select: all;"></code>
|
|
<button type="button" id="btn-copy-api-key" class="button"
|
|
style="white-space: nowrap; padding: 6px 14px;">Copy</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display: flex; gap: 10px; flex-wrap: wrap; align-items: center;">
|
|
<button type="button" id="btn-regen-api-key" class="button button-primary">Generate / Regenerate Key</button>
|
|
<a id="btn-sharex-download" href="/api/v2/settings/api-key/sharex-config" download style="display:none; box-sizing:border-box; line-height:normal;" class="button">⭳ ShareX Config</a>
|
|
<button type="button" id="btn-revoke-api-key" class="button button-danger" style="display:none;">Revoke Key</button>
|
|
</div>
|
|
<div id="api-key-action-status" class="avatar-status" style="margin-top: 10px;"></div>
|
|
</div>
|
|
@endif
|
|
|
|
@if(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"
|
|
style="background: rgba(0,0,0,0.1); padding: 20px; border-radius: 6px; border: 1px solid var(--nav-border-color); margin-bottom: 30px;">
|
|
|
|
<p id="invite-section-desc" style="color: var(--text-muted); margin-bottom: 15px;">{{ t('invites.section_desc') }}</p>
|
|
|
|
<!-- Criteria grid -->
|
|
<div id="invite-criteria-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 10px; margin-bottom: 18px;">
|
|
<div class="invite-criterion" id="ic-uploads">
|
|
<span class="ic-icon">⋯</span>
|
|
<span class="ic-label">{{ t('invites.criteria_uploads') }}</span>
|
|
<span class="ic-values"></span>
|
|
</div>
|
|
<div class="invite-criterion" id="ic-age">
|
|
<span class="ic-icon">⋯</span>
|
|
<span class="ic-label">{{ t('invites.criteria_age') }}</span>
|
|
<span class="ic-values"></span>
|
|
</div>
|
|
<div class="invite-criterion" id="ic-comments">
|
|
<span class="ic-icon">⋯</span>
|
|
<span class="ic-label">{{ t('invites.criteria_comments') }}</span>
|
|
<span class="ic-values"></span>
|
|
</div>
|
|
<div class="invite-criterion" id="ic-tags">
|
|
<span class="ic-icon">⋯</span>
|
|
<span class="ic-label">{{ t('invites.criteria_tags') }}</span>
|
|
<span class="ic-values"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Eligibility badge + slot indicator -->
|
|
<div id="invite-status-line" style="margin-bottom: 16px; display: flex; align-items: center; gap: 14px; flex-wrap: wrap;">
|
|
<span id="invite-eligible-badge" style="font-weight: bold; font-size: 0.95em;"></span>
|
|
<span id="invite-slots-info" style="color: var(--text-muted); font-size: 0.9em;"></span>
|
|
</div>
|
|
|
|
<!-- Token list -->
|
|
<div id="invite-token-list" style="margin-bottom: 16px; display: flex; flex-direction: column; gap: 8px;">
|
|
<span class="text-muted" style="font-size: 0.9em;">{{ t('invites.loading') }}</span>
|
|
</div>
|
|
|
|
<!-- Generate button + status -->
|
|
<div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
|
|
<button type="button" id="btn-gen-invite" class="button button-primary" disabled>{{ t('invites.generate_btn') }}</button>
|
|
</div>
|
|
<div id="invite-action-status" class="avatar-status" style="margin-top: 10px;"></div>
|
|
</div>
|
|
|
|
<style>
|
|
.invite-criterion {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
padding: 10px 14px;
|
|
border-radius: 6px;
|
|
border: 1px solid var(--nav-border-color);
|
|
background: rgba(255,255,255,0.03);
|
|
font-size: 0.88em;
|
|
}
|
|
.invite-criterion.met {
|
|
border-color: rgba(80, 200, 120, 0.5);
|
|
background: rgba(80, 200, 120, 0.07);
|
|
}
|
|
.invite-criterion.unmet {
|
|
border-color: rgba(255, 90, 90, 0.35);
|
|
background: rgba(255, 90, 90, 0.05);
|
|
}
|
|
.ic-icon { font-size: 1.2em; }
|
|
.ic-label { font-weight: bold; color: var(--text-muted); }
|
|
.ic-values { font-family: monospace; font-size: 1.05em; }
|
|
.invite-token-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
padding: 10px 14px;
|
|
border-radius: 6px;
|
|
border: 1px solid var(--nav-border-color);
|
|
background: rgba(255,255,255,0.02);
|
|
font-size: 0.88em;
|
|
}
|
|
.invite-token-code {
|
|
font-family: monospace;
|
|
font-size: 1.05em;
|
|
letter-spacing: 0.04em;
|
|
color: var(--accent);
|
|
user-select: all;
|
|
flex: 1;
|
|
word-break: break-all;
|
|
}
|
|
.invite-token-status-used { color: #ff6b6b; }
|
|
.invite-token-status-unused { color: #51cf66; }
|
|
.invite-refresh-note { font-size: 0.82em; color: var(--text-muted); }
|
|
</style>
|
|
|
|
<!-- i18n strings for invite JS (rendered server-side) -->
|
|
<div id="invite-i18n" style="display:none"
|
|
data-eligible="{{ t('invites.eligible') }}"
|
|
data-not-eligible="{{ t('invites.not_eligible') }}"
|
|
data-slots-used="{{ t('invites.slots_used') }}"
|
|
data-days="{{ t('invites.criteria_days') }}"
|
|
data-no-invites="{{ t('invites.no_invites') }}"
|
|
data-status-unused="{{ t('invites.status_unused') }}"
|
|
data-status-used-by="{{ t('invites.status_used_by') }}"
|
|
data-delete-btn="{{ t('invites.delete_btn') }}"
|
|
data-delete-confirm="{{ t('invites.delete_confirm') }}"
|
|
data-slot-refreshes-on="{{ t('invites.slot_refreshes_on') }}"
|
|
data-slot-refreshed="{{ t('invites.slot_refreshed') }}"
|
|
data-generating="{{ t('invites.generating') }}"
|
|
data-admin-desc="{{ t('invites.admin_desc') }}"
|
|
></div>
|
|
|
|
<script>
|
|
(function() {
|
|
const ds = document.getElementById('invite-i18n')?.dataset || {};
|
|
const T = (key, vars) => {
|
|
let str = ds[key] || key;
|
|
if (vars) Object.entries(vars).forEach(([k, v]) => { str = str.replaceAll('{' + k + '}', String(v)); });
|
|
return str;
|
|
};
|
|
|
|
const statusEl = document.getElementById('invite-action-status');
|
|
const genBtn = document.getElementById('btn-gen-invite');
|
|
const tokenList = document.getElementById('invite-token-list');
|
|
|
|
const setStatus = (msg, ok) => {
|
|
statusEl.textContent = msg;
|
|
statusEl.style.color = ok ? '#51cf66' : '#ff6b6b';
|
|
};
|
|
|
|
const formatDate = ts => {
|
|
if (!ts) return '—';
|
|
const d = (typeof ts === 'number' || /^\d+$/.test(String(ts)))
|
|
? new Date(parseInt(ts) * 1000)
|
|
: new Date(ts);
|
|
return d.toLocaleDateString();
|
|
};
|
|
|
|
const loadInvites = async () => {
|
|
try {
|
|
const res = await fetch('/api/v2/settings/invites');
|
|
const data = await res.json();
|
|
if (!data.success) {
|
|
tokenList.innerHTML = '<span style="color:#ff6b6b">' + (data.msg || 'Error') + '</span>';
|
|
return;
|
|
}
|
|
|
|
if (data.is_admin) {
|
|
const desc = document.getElementById('invite-section-desc');
|
|
if (desc) desc.textContent = T('adminDesc');
|
|
const grid = document.getElementById('invite-criteria-grid');
|
|
const statusLine = document.getElementById('invite-status-line');
|
|
if (grid) grid.style.display = 'none';
|
|
if (statusLine) statusLine.style.display = 'none';
|
|
} else {
|
|
const criteriaMap = {
|
|
'ic-uploads': data.criteria.uploads,
|
|
'ic-age': data.criteria.age_days,
|
|
'ic-comments': data.criteria.comments,
|
|
'ic-tags': data.criteria.tags,
|
|
};
|
|
Object.entries(criteriaMap).forEach(([id, c]) => {
|
|
const el = document.getElementById(id);
|
|
if (!el) return;
|
|
el.classList.toggle('met', c.met);
|
|
el.classList.toggle('unmet', !c.met);
|
|
el.querySelector('.ic-icon').textContent = c.met ? '✓' : '✗';
|
|
const unit = id === 'ic-age' ? ' ' + T('days') : '';
|
|
el.querySelector('.ic-values').textContent = c.current + ' / ' + c.required + unit;
|
|
});
|
|
}
|
|
|
|
const badge = document.getElementById('invite-eligible-badge');
|
|
badge.textContent = data.eligible ? T('eligible') : T('notEligible');
|
|
badge.style.color = data.eligible ? '#51cf66' : '#ff6b6b';
|
|
|
|
document.getElementById('invite-slots-info').textContent =
|
|
T('slotsUsed', { used: data.slots_consumed, total: data.slots_total });
|
|
|
|
genBtn.disabled = !data.eligible || (!data.is_admin && data.slots_available <= 0);
|
|
|
|
if (!data.tokens || data.tokens.length === 0) {
|
|
tokenList.innerHTML = '<span class="text-muted" style="font-size:0.9em;">' + T('noInvites') + '</span>';
|
|
} else {
|
|
tokenList.innerHTML = data.tokens.map(tok => {
|
|
const isUsed = tok.is_used;
|
|
const usedAt = tok.used_at ? formatDate(tok.used_at) : null;
|
|
let refreshNote = '';
|
|
if (isUsed && tok.used_at) {
|
|
const usedAtMs = (typeof tok.used_at === 'number' || /^\d+$/.test(String(tok.used_at)))
|
|
? parseInt(tok.used_at) * 1000
|
|
: new Date(tok.used_at).getTime();
|
|
const refreshDate = new Date(usedAtMs + 30 * 24 * 60 * 60 * 1000);
|
|
refreshNote = '<span class="invite-refresh-note"> — ' +
|
|
(refreshDate > new Date()
|
|
? T('slotRefreshesOn', { date: refreshDate.toLocaleDateString() })
|
|
: T('slotRefreshed')) +
|
|
'</span>';
|
|
}
|
|
const userLink = tok.used_by_name
|
|
? '<a href="/user/' + encodeURIComponent(tok.used_by_name.toLowerCase()) + '" style="color:inherit;text-decoration:underline;">' + tok.used_by_name + '</a>'
|
|
: '?';
|
|
const statusHtml = isUsed
|
|
? '<span class="invite-token-status-used">' + T('statusUsedBy', { user: userLink }) + ' (' + (usedAt || '?') + ')</span>' + refreshNote
|
|
: '<span class="invite-token-status-unused">' + T('statusUnused') + '</span>';
|
|
const deleteBtn = !isUsed
|
|
? '<button class="button button-danger" style="padding:3px 10px;font-size:0.8em;" data-invite-delete="' + tok.id + '">' + T('deleteBtn') + '</button>'
|
|
: '';
|
|
return '<div class="invite-token-row">' +
|
|
'<span class="invite-token-code">' + tok.token + '</span>' +
|
|
statusHtml + deleteBtn +
|
|
'</div>';
|
|
}).join('');
|
|
|
|
tokenList.querySelectorAll('[data-invite-delete]').forEach(btn => {
|
|
btn.addEventListener('click', async () => {
|
|
if (!confirm(T('deleteConfirm'))) return;
|
|
const res = await fetch('/api/v2/settings/invites/delete', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token },
|
|
body: JSON.stringify({ id: +btn.dataset.inviteDelete })
|
|
});
|
|
const d = await res.json();
|
|
if (d.success) { loadInvites(); } else { setStatus(d.msg || 'Error', false); }
|
|
});
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.error('[Invites] load error:', e);
|
|
tokenList.innerHTML = '<span style="color:#ff6b6b">Failed to load</span>';
|
|
}
|
|
};
|
|
|
|
genBtn.addEventListener('click', async () => {
|
|
const origLabel = genBtn.textContent;
|
|
genBtn.disabled = true;
|
|
genBtn.textContent = T('generating');
|
|
statusEl.textContent = '';
|
|
try {
|
|
const res = await fetch('/api/v2/settings/invites/create', {
|
|
method: 'POST',
|
|
headers: { 'X-CSRF-Token': window.f0ckSession?.csrf_token }
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
setStatus('✓', true);
|
|
loadInvites();
|
|
} else {
|
|
setStatus(data.msg || 'Error', false);
|
|
genBtn.disabled = false;
|
|
}
|
|
} catch (e) {
|
|
setStatus('Network error', false);
|
|
genBtn.disabled = false;
|
|
} finally {
|
|
genBtn.textContent = origLabel;
|
|
}
|
|
});
|
|
|
|
loadInvites();
|
|
})();
|
|
</script>
|
|
@endif
|
|
|
|
<style>
|
|
@keyframes exportDotBounce {
|
|
0%, 80%, 100% { transform: translateY(0); opacity: 0.3; }
|
|
40% { transform: translateY(-5px); opacity: 1; }
|
|
}
|
|
.export-dots span {
|
|
display: inline-block;
|
|
animation: exportDotBounce 1.2s infinite ease-in-out;
|
|
}
|
|
.export-dots span:nth-child(2) { animation-delay: 0.2s; }
|
|
.export-dots span:nth-child(3) { animation-delay: 0.4s; }
|
|
</style>
|
|
<script src="/s/js/jszip.min.js"></script>
|
|
<script src="/s/js/settings.js?v=@mtime(/public/s/js/settings.js)"></script>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@include(snippets/footer) |