PHP Classes

File: templates/admin/auto_feed.twig

Recommend this page to a friend!
  Packages of Matthew Asham   Binkterm PHP   templates/admin/auto_feed.twig   Download  
File: templates/admin/auto_feed.twig
Role: Auxiliary data
Content type: text/plain
Description: Auxiliary data
Class: Binkterm PHP
Bulletin board system based on the Web
Author: By
Last change:
Date: 5 days ago
Size: 26,747 bytes
 

Contents

Class file image Download
{% extends "base.twig" %} {% block title %}{{ t('ui.admin.auto_feed.page_title') }} - {{ parent() }}{% endblock %} {% block content %} <div class="d-flex justify-content-between align-items-center mb-4"> <h2> <i class="fas fa-rss"></i> {{ t('ui.admin.auto_feed.heading') }} </h2> <button class="btn btn-fidonet" onclick="showAddFeedModal()"> <i class="fas fa-plus"></i> {{ t('ui.admin.auto_feed.add_feed') }} </button> </div> <div class="row"> <div class="col-lg-9"> <div class="card"> <div class="card-header"> <h6 class="mb-0">{{ t('ui.admin.auto_feed.feed_sources') }}</h6> </div> <div class="card-body p-0"> <div id="feedsContainer"> <div class="loading-spinner"> <i class="fas fa-spinner fa-spin me-2"></i> {{ t('ui.admin.auto_feed.loading_feeds') }} </div> </div> </div> </div> </div> <div class="col-lg-3"> <div class="card"> <div class="card-header"> <h6 class="mb-0">{{ t('ui.admin.auto_feed.statistics') }}</h6> </div> <div class="card-body"> <dl class="row mb-0"> <dt class="col-8">{{ t('ui.admin.auto_feed.total_feeds') }}:</dt> <dd class="col-4" id="totalFeeds">-</dd> <dt class="col-8">{{ t('ui.admin.auto_feed.active_label') }}:</dt> <dd class="col-4" id="activeFeeds">-</dd> <dt class="col-8">{{ t('ui.admin.auto_feed.articles_posted') }}:</dt> <dd class="col-4" id="totalArticles">-</dd> </dl> </div> </div> <div class="card mt-3"> <div class="card-header"> <h6 class="mb-0">{{ t('ui.admin.auto_feed.about_title') }}</h6> </div> <div class="card-body"> <p class="small text-muted mb-2"> {{ t('ui.admin.auto_feed.about_text') }} </p> <p class="small text-muted mb-0"> {{ t('ui.admin.auto_feed.cron_hint_prefix') }} <code>php scripts/rss_poster.php</code> {{ t('ui.admin.auto_feed.cron_hint_suffix') }} </p> </div> </div> </div> </div> <!-- Add/Edit Feed Modal --> <div class="modal fade" id="feedModal" tabindex="-1"> <div class="modal-dialog modal-lg"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" id="feedModalTitle">{{ t('ui.admin.auto_feed.add_feed') }}</h5> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body"> <form id="feedForm"> <input type="hidden" id="feedId"> <div class="mb-3"> <label for="feedName" class="form-label">{{ t('ui.admin.auto_feed.feed_name') }}</label> <input type="text" class="form-control" id="feedName" name="feed_name" maxlength="100" placeholder="{{ t('ui.admin.auto_feed.feed_name_placeholder') }}"> <div class="form-text">{{ t('ui.admin.auto_feed.feed_name_help') }}</div> </div> <div class="mb-3"> <label for="feedUrl" class="form-label">{{ t('ui.admin.auto_feed.feed_url_required') }}</label> <input type="url" class="form-control" id="feedUrl" name="feed_url" required placeholder="{{ t('ui.admin.auto_feed.feed_url_placeholder', {}, locale, ['common']) }}"> <div class="form-text">{{ t('ui.admin.auto_feed.feed_url_help') }}</div> </div> <div class="mb-3"> <label for="feedSourceType" class="form-label">{{ t('ui.admin.auto_feed.source_type') }}</label> <select class="form-select" id="feedSourceType" name="source_type"> <option value="rss">{{ t('ui.admin.auto_feed.source_type_rss') }}</option> <option value="bluesky">{{ t('ui.admin.auto_feed.source_type_bluesky') }}</option> </select> <div class="form-text">{{ t('ui.admin.auto_feed.source_type_help') }}</div> </div> <div class="mb-3"> <label for="feedEchoarea" class="form-label">{{ t('ui.admin.auto_feed.echo_area_required') }}</label> <select class="form-select" id="feedEchoarea" name="echoarea_id" required> <option value="">{{ t('ui.admin.auto_feed.loading_echo_areas') }}</option> </select> <div class="form-text">{{ t('ui.admin.auto_feed.echo_area_help') }}</div> </div> <div class="row mb-3"> <div class="col-md-8"> <label for="feedUserSearch" class="form-label">{{ t('ui.admin.auto_feed.post_as_user_required') }}</label> <input type="hidden" id="feedUser" name="post_as_user_id" required> <div class="position-relative"> <input type="text" class="form-control" id="feedUserSearch" autocomplete="off" placeholder="{{ t('ui.admin.auto_feed.user_search_placeholder') }}"> <div id="feedUserResults" class="list-group position-absolute w-100 shadow-sm d-none" style="z-index: 1060;"></div> </div> <div class="form-text">{{ t('ui.admin.auto_feed.post_as_user_help') }}</div> </div> <div class="col-md-4"> <label for="feedMaxArticles" class="form-label">{{ t('ui.admin.auto_feed.max_articles') }}</label> <input type="number" class="form-control" id="feedMaxArticles" name="max_articles_per_check" value="10" min="1" max="50"> <div class="form-text">{{ t('ui.admin.auto_feed.per_check') }}</div> </div> </div> <div class="row mb-3"> <div class="col-md-8"> <div class="form-check mt-2"> <input class="form-check-input" type="checkbox" id="feedThreadReplies" name="thread_replies"> <label class="form-check-label fw-semibold" for="feedThreadReplies"> {{ t('ui.admin.auto_feed.thread_replies') }} </label> <div class="form-text">{{ t('ui.admin.auto_feed.thread_replies_help') }}</div> </div> </div> <div class="col-md-4" id="threadLookupLimitGroup" style="display:none;"> <label for="feedThreadLookupLimit" class="form-label">{{ t('ui.admin.auto_feed.thread_lookup_limit') }}</label> <input type="number" class="form-control" id="feedThreadLookupLimit" name="thread_lookup_limit" value="1000" min="100" max="10000"> <div class="form-text">{{ t('ui.admin.auto_feed.thread_lookup_limit_help') }}</div> </div> </div> <div class="form-check"> <input class="form-check-input" type="checkbox" id="feedActive" name="active" checked> <label class="form-check-label" for="feedActive"> {{ t('ui.admin.auto_feed.active_label') }} </label> </div> </form> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ t('ui.common.cancel') }}</button> <button type="button" class="btn btn-fidonet" onclick="saveFeed()"> <i class="fas fa-save"></i> {{ t('ui.common.save') }} </button> </div> </div> </div> </div> <!-- Delete Confirmation Modal --> <div class="modal fade" id="deleteModal" tabindex="-1"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">{{ t('ui.admin.auto_feed.confirm_delete_title') }}</h5> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body"> <p>{{ t('ui.admin.auto_feed.confirm_delete_text_prefix') }} <strong id="deleteFeedName"></strong>?</p> <p class="text-warning"><i class="fas fa-exclamation-triangle"></i> {{ t('ui.admin.auto_feed.confirm_delete_warning') }}</p> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ t('ui.common.cancel') }}</button> <button type="button" class="btn btn-danger" id="confirmDelete"> <i class="fas fa-trash"></i> {{ t('ui.common.delete') }} </button> </div> </div> </div> </div> {% endblock %} {% block scripts %} <script> let currentFeedId = null; let allFeeds = []; let selectedFeedUser = null; let userSearchTimer = null; function uiT(key, fallback, params = {}) { if (window.t) { return window.t(key, params, fallback); } return fallback; } $(document).ready(function() { const initAutoFeedPage = function() { loadFeeds(); loadStats(); loadEchoareas(); $('#feedUrl').on('input', function() { if (/^https?:\/\/(www\.)?bsky\.app\/profile\//i.test($(this).val())) { $('#feedSourceType').val('bluesky'); } }); $('#feedThreadReplies').on('change', function() { $('#threadLookupLimitGroup').toggle($(this).is(':checked')); }); $('#feedUserSearch').on('input', function() { selectedFeedUser = null; $('#feedUser').val(''); updateFeedUserValidity(); queueUserSearch($(this).val()); }); $('#feedUserSearch').on('focus', function() { queueUserSearch($(this).val()); }); $('#feedUserSearch').on('blur', function() { setTimeout(function() { $('#feedUserResults').addClass('d-none').empty(); updateFeedUserValidity(); }, 150); }); }; if (typeof loadUserSettings === 'function') { loadUserSettings().finally(initAutoFeedPage); return; } initAutoFeedPage(); }); function loadFeeds() { $('#feedsContainer').html(`<div class="loading-spinner"><i class="fas fa-spinner fa-spin me-2"></i>${uiT('ui.admin.auto_feed.loading_feeds', 'Loading feeds...')}</div>`); $.get('/admin/api/auto-feed/feeds') .done(function(data) { console.log('API response:', data); if (!data || !data.feeds) { console.error('Invalid response structure:', data); $('#feedsContainer').html(`<div class="text-center text-danger py-4">${uiT('ui.admin.auto_feed.invalid_api_response', 'Invalid API response')}</div>`); return; } allFeeds = data.feeds; displayFeeds(allFeeds); }) .fail(function(xhr, status, error) { console.error('API error:', status, error); $('#feedsContainer').html(`<div class="text-center text-danger py-4">${uiT('ui.admin.auto_feed.failed_to_load_feeds_prefix', 'Failed to load feeds:')} ${escapeHtml(error)}</div>`); }); } function displayFeeds(feeds) { const container = $('#feedsContainer'); let html = ''; if (feeds.length === 0) { html = `<div class="text-center text-muted py-4">${uiT('ui.admin.auto_feed.no_feeds_configured', 'No feeds configured')}</div>`; } else { html = ` <div class="table-responsive"> <table class="table table-hover mb-0"> <thead> <tr> <th width="5%"></th> <th width="18%">${uiT('ui.admin.auto_feed.name_url', 'Name/URL')}</th> <th width="10%">${uiT('ui.admin.auto_feed.source_type', 'Source Type')}</th> <th width="18%">${uiT('ui.admin.auto_feed.echo_area', 'Echo Area')}</th> <th width="14%">${uiT('ui.admin.auto_feed.post_as', 'Post As')}</th> <th width="10%">${uiT('ui.admin.auto_feed.articles', 'Articles')}</th> <th width="15%">${uiT('ui.admin.auto_feed.last_check', 'Last Check')}</th> <th width="10%">${uiT('ui.admin.auto_feed.actions', 'Actions')}</th> </tr> </thead> <tbody> `; feeds.forEach(function(feed) { const isActive = feed.active; const rowClass = isActive ? '' : 'table-secondary'; const statusIcon = isActive ? `<i class="fas fa-check-circle text-success" title="${uiT('ui.admin.auto_feed.active_label', 'Active')}"></i>` : `<i class="fas fa-pause-circle text-warning" title="${uiT('ui.admin.auto_feed.inactive_label', 'Inactive')}"></i>`; const lastCheck = feed.last_check ? new Date(feed.last_check).toLocaleString() : `<span class="text-muted">${uiT('ui.admin.auto_feed.never', 'Never')}</span>`; const feedName = escapeHtml(feed.feed_name || uiT('ui.admin.auto_feed.unnamed_feed', 'Unnamed Feed')); const feedUrl = escapeHtml(feed.feed_url); const truncatedUrl = feedUrl.length > 50 ? feedUrl.substring(0, 47) + '...' : feedUrl; html += ` <tr class="${rowClass}"> <td class="text-center">${statusIcon}</td> <td> <strong>${feedName}</strong><br> <small class="text-muted" title="${feedUrl}">${truncatedUrl}</small> ${feed.last_error ? `<br><small class="text-danger"><i class="fas fa-exclamation-triangle"></i> ${uiT('ui.common.error', 'Error')}</small>` : ''} </td> <td> <span class="badge bg-secondary">${formatSourceType(feed.source_type || 'rss')}</span> </td> <td> <span class="font-monospace">${escapeHtml(feed.echoarea_tag || uiT('ui.admin.auto_feed.unknown', 'Unknown'))}</span><br> <small class="text-muted">@ ${escapeHtml(feed.echoarea_domain || uiT('ui.admin.auto_feed.unknown', 'Unknown'))}</small> </td> <td> <small>${escapeHtml(feed.username || (uiT('ui.admin.auto_feed.user_prefix', 'User #') + feed.post_as_user_id))}</small> </td> <td> <span class="badge bg-secondary">${feed.articles_posted || 0}</span> </td> <td> <small>${lastCheck}</small> </td> <td> <div class="btn-group" role="group"> <button class="btn btn-sm btn-outline-success" onclick="checkFeed(${feed.id})" title="${uiT('ui.admin.auto_feed.check_now', 'Check now')}"> <i class="fas fa-sync"></i> </button> <button class="btn btn-sm btn-outline-primary" onclick="editFeed(${feed.id})" title="${uiT('ui.common.edit', 'Edit')}"> <i class="fas fa-edit"></i> </button> <button class="btn btn-sm btn-outline-danger" onclick="showDeleteModal(${feed.id}, '${escapeHtml(feedName)}')" title="${uiT('ui.common.delete', 'Delete')}"> <i class="fas fa-trash"></i> </button> </div> </td> </tr> `; }); html += ` </tbody> </table> </div> `; } container.html(html); } function showAddFeedModal() { currentFeedId = null; $('#feedModalTitle').text(uiT('ui.admin.auto_feed.add_feed', 'Add Feed')); $('#feedForm')[0].reset(); $('#feedId').val(''); $('#feedActive').prop('checked', true); $('#feedMaxArticles').val(10); $('#feedSourceType').val('rss'); $('#feedThreadReplies').prop('checked', false); $('#feedThreadLookupLimit').val(1000); $('#threadLookupLimitGroup').hide(); setSelectedFeedUser(null); $('#feedModal').modal('show'); } function editFeed(id) { currentFeedId = id; $('#feedModalTitle').text(uiT('ui.admin.auto_feed.edit_feed', 'Edit Feed')); $.get(`/admin/api/auto-feed/feeds/${id}`) .done(function(data) { const feed = data.feed; $('#feedName').val(feed.feed_name || ''); $('#feedUrl').val(feed.feed_url); $('#feedSourceType').val(feed.source_type || 'rss'); $('#feedEchoarea').val(feed.echoarea_id); setSelectedFeedUser({ id: feed.post_as_user_id, username: feed.username || '', real_name: feed.real_name || '' }); $('#feedMaxArticles').val(feed.max_articles_per_check || 10); $('#feedActive').prop('checked', feed.active); const threadReplies = !!feed.thread_replies; $('#feedThreadReplies').prop('checked', threadReplies); $('#feedThreadLookupLimit').val(feed.thread_lookup_limit || 1000); $('#threadLookupLimitGroup').toggle(threadReplies); $('#feedModal').modal('show'); }) .fail(function() { showError(uiT('ui.admin.auto_feed.load_details_failed', 'Failed to load feed details')); }); } function saveFeed() { const form = $('#feedForm'); updateFeedUserValidity(); if (!form[0].checkValidity()) { form[0].reportValidity(); return; } const formData = { feed_name: $('#feedName').val() || null, feed_url: $('#feedUrl').val(), source_type: $('#feedSourceType').val() || 'rss', echoarea_id: parseInt($('#feedEchoarea').val()), post_as_user_id: parseInt($('#feedUser').val(), 10), max_articles_per_check: parseInt($('#feedMaxArticles').val()), active: $('#feedActive').is(':checked'), thread_replies: $('#feedThreadReplies').is(':checked'), thread_lookup_limit: parseInt($('#feedThreadLookupLimit').val()) || 1000 }; const url = currentFeedId ? `/admin/api/auto-feed/feeds/${currentFeedId}` : '/admin/api/auto-feed/feeds'; const method = currentFeedId ? 'PUT' : 'POST'; $.ajax({ url: url, method: method, contentType: 'application/json', data: JSON.stringify(formData), success: function(response) { $('#feedModal').modal('hide'); const defaultMessage = currentFeedId ? uiT('ui.admin.auto_feed.updated_success', 'Feed updated successfully') : uiT('ui.admin.auto_feed.created_success', 'Feed created successfully'); const successMessage = window.getApiMessage ? window.getApiMessage(response, defaultMessage) : defaultMessage; showSuccess(successMessage); loadFeeds(); loadStats(); }, error: function(xhr) { const error = apiError(xhr.responseJSON, uiT('ui.admin.auto_feed.operation_failed', 'Operation failed')); showError(error); } }); } function showDeleteModal(id, name) { currentFeedId = id; $('#deleteFeedName').text(name); $('#deleteModal').modal('show'); $('#confirmDelete').off('click').on('click', function() { deleteFeed(); }); } function deleteFeed() { $.ajax({ url: `/admin/api/auto-feed/feeds/${currentFeedId}`, method: 'DELETE', success: function(response) { $('#deleteModal').modal('hide'); const successMessage = window.getApiMessage ? window.getApiMessage(response, uiT('ui.admin.auto_feed.deleted_success', 'Feed deleted successfully')) : uiT('ui.admin.auto_feed.deleted_success', 'Feed deleted successfully'); showSuccess(successMessage); loadFeeds(); loadStats(); }, error: function(xhr) { const error = apiError(xhr.responseJSON, uiT('ui.admin.auto_feed.delete_failed', 'Delete failed')); showError(error); } }); } function checkFeed(id) { showInfo(uiT('ui.admin.auto_feed.checking_feed', 'Checking feed...')); $.post(`/admin/api/auto-feed/check/${id}`) .done(function(data) { const successMessage = window.getApiMessage ? window.getApiMessage(data, uiT('ui.admin.auto_feed.checked_articles_posted', 'Feed checked: {count} new article(s) posted', { count: data.articles_posted })) : uiT('ui.admin.auto_feed.checked_articles_posted', 'Feed checked: {count} new article(s) posted', { count: data.articles_posted }); showSuccess(successMessage); loadFeeds(); loadStats(); }) .fail(function(xhr) { const error = apiError(xhr.responseJSON, uiT('ui.admin.auto_feed.check_failed', 'Check failed')); showError(error); }); } function loadStats() { $.get('/admin/api/auto-feed/stats') .done(function(data) { $('#totalFeeds').text(data.total_feeds || 0); $('#activeFeeds').text(data.active_feeds || 0); $('#totalArticles').text(data.total_articles || 0); }); } function loadEchoareas() { $.get('/api/echoareas') .done(function(data) { const select = $('#feedEchoarea'); select.empty(); select.append(`<option value="">${uiT('ui.admin.auto_feed.select_echo_area', 'Select echo area...')}</option>`); data.echoareas.forEach(function(area) { const label = `${area.tag} @ ${area.domain}${area.description ? ' - ' + area.description : ''}`; select.append(`<option value="${area.id}">${escapeHtml(label)}</option>`); }); }); } function apiError(payload, fallback) { if (window.getApiErrorMessage) { return window.getApiErrorMessage(payload, fallback); } if (payload && payload.error) { return String(payload.error); } return fallback; } function formatSourceType(sourceType) { if (sourceType === 'bluesky') { return uiT('ui.admin.auto_feed.source_type_bluesky', 'Bluesky'); } return uiT('ui.admin.auto_feed.source_type_rss', 'RSS/Atom'); } function queueUserSearch(query) { clearTimeout(userSearchTimer); userSearchTimer = setTimeout(function() { searchFeedUsers(query); }, 200); } function searchFeedUsers(query) { const search = (query || '').trim(); const results = $('#feedUserResults'); if (search.length < 2) { results.addClass('d-none').empty(); return; } results.removeClass('d-none').html( `<div class="list-group-item text-muted">${uiT('ui.admin.auto_feed.searching_users', 'Searching users...')}</div>` ); $.get('/admin/api/users', { search: search, limit: 10 }) .done(function(data) { const users = data && data.users ? data.users : []; displayUserResults(users); }) .fail(function() { results.removeClass('d-none').html( `<div class="list-group-item text-danger">${uiT('ui.admin.auto_feed.user_search_failed', 'User search failed')}</div>` ); }); } function displayUserResults(users) { const results = $('#feedUserResults'); results.empty(); if (!users.length) { results.removeClass('d-none').html( `<div class="list-group-item text-muted">${uiT('ui.admin.auto_feed.no_users_found', 'No users found')}</div>` ); return; } users.forEach(function(user) { const label = formatUserLabel(user); const item = $(` <button type="button" class="list-group-item list-group-item-action"> <div class="fw-semibold">${escapeHtml(user.username || '')}</div> <small class="text-muted">${escapeHtml(user.real_name || uiT('ui.admin.auto_feed.no_real_name', 'No real name'))}</small> </button> `); item.on('mousedown', function(event) { event.preventDefault(); setSelectedFeedUser(user); results.addClass('d-none').empty(); }); item.attr('title', label); results.append(item); }); results.removeClass('d-none'); } function setSelectedFeedUser(user) { selectedFeedUser = user && user.id ? user : null; if (!selectedFeedUser) { $('#feedUser').val(''); $('#feedUserSearch').val(''); updateFeedUserValidity(); return; } $('#feedUser').val(selectedFeedUser.id); $('#feedUserSearch').val(formatUserLabel(selectedFeedUser)); updateFeedUserValidity(); } function updateFeedUserValidity() { const input = document.getElementById('feedUserSearch'); if (!input) return; if ($('#feedUser').val()) { input.setCustomValidity(''); } else { input.setCustomValidity(uiT('ui.admin.auto_feed.select_valid_user', 'Select a valid user from the search results')); } } function formatUserLabel(user) { const username = user.username || (uiT('ui.admin.auto_feed.user_prefix', 'User #') + user.id); const realName = user.real_name || ''; return realName ? `${username} (${realName})` : username; } </script> {% endblock %}