mirror of
https://github.com/svemagie/indiekit-endpoint-blogroll.git
synced 2026-04-02 15:34:59 +02:00
Adds FeedLand (feedland.com or self-hosted) as a new source type alongside OPML and Microsub. Syncs subscriptions via FeedLand's public OPML endpoint with optional category filtering and AJAX category discovery in the admin UI.
195 lines
10 KiB
Plaintext
195 lines
10 KiB
Plaintext
{% extends "layouts/blogroll.njk" %}
|
|
|
|
{% block blogroll %}
|
|
<form method="post" action="{% if isNew %}{{ baseUrl }}/sources{% else %}{{ baseUrl }}/sources/{{ source._id }}{% endif %}" class="blogroll-form">
|
|
<div class="blogroll-field">
|
|
<label class="label" for="name">{{ __("blogroll.sources.form.name") }}</label>
|
|
<input class="input" type="text" id="name" name="name" value="{{ source.name if source else '' }}" required>
|
|
</div>
|
|
|
|
<div class="blogroll-field">
|
|
<label class="label" for="type">{{ __("blogroll.sources.form.type") }}</label>
|
|
<select class="select" id="type" name="type" required onchange="toggleTypeFields()">
|
|
<option value="opml_url" {% if source.type == 'opml_url' %}selected{% endif %}>OPML URL (auto-sync)</option>
|
|
<option value="opml_file" {% if source.type == 'opml_file' %}selected{% endif %}>OPML File (one-time import)</option>
|
|
{% if microsubAvailable %}
|
|
<option value="microsub" {% if source.type == 'microsub' %}selected{% endif %}>Microsub Subscriptions</option>
|
|
{% endif %}
|
|
<option value="feedland" {% if source.type == 'feedland' %}selected{% endif %}>FeedLand</option>
|
|
</select>
|
|
<span class="hint">{{ __("blogroll.sources.form.typeHint") }}</span>
|
|
</div>
|
|
|
|
<div class="blogroll-field" id="urlField">
|
|
<label class="label" for="url">{{ __("blogroll.sources.form.url") }}</label>
|
|
<input class="input" type="url" id="url" name="url" value="{{ source.url if source else '' }}" placeholder="https://...">
|
|
<span class="hint">{{ __("blogroll.sources.form.urlHint") }}</span>
|
|
</div>
|
|
|
|
<div class="blogroll-field" id="opmlContentField" style="display: none;">
|
|
<label class="label" for="opmlContent">{{ __("blogroll.sources.form.opmlContent") }}</label>
|
|
<textarea class="textarea" id="opmlContent" name="opmlContent" placeholder="<?xml version="1.0"?>...">{{ source.opmlContent if source else '' }}</textarea>
|
|
<span class="hint">{{ __("blogroll.sources.form.opmlContentHint") }}</span>
|
|
</div>
|
|
|
|
<div class="blogroll-field" id="microsubChannelField" style="display: none;">
|
|
<label class="label" for="channelFilter">{{ __("blogroll.sources.form.microsubChannel") | default("Microsub Channel") }}</label>
|
|
<select class="select" id="channelFilter" name="channelFilter">
|
|
<option value="">All channels</option>
|
|
{% for channel in microsubChannels %}
|
|
<option value="{{ channel.uid }}" {% if source.channelFilter == channel.uid %}selected{% endif %}>{{ channel.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<span class="hint">{{ __("blogroll.sources.form.microsubChannelHint") | default("Sync feeds from a specific channel, or all channels") }}</span>
|
|
</div>
|
|
|
|
<div class="blogroll-field" id="categoryPrefixField" style="display: none;">
|
|
<label class="label" for="categoryPrefix">{{ __("blogroll.sources.form.categoryPrefix") | default("Category Prefix") }}</label>
|
|
<input class="input" type="text" id="categoryPrefix" name="categoryPrefix" value="{{ source.categoryPrefix if source else '' }}" placeholder="e.g., Microsub: ">
|
|
<span class="hint">{{ __("blogroll.sources.form.categoryPrefixHint") | default("Optional prefix for blog categories (e.g., 'Following: ')") }}</span>
|
|
</div>
|
|
|
|
<div class="blogroll-field" id="feedlandInstanceField" style="display: none;">
|
|
<label class="label" for="feedlandInstance">{{ __("blogroll.sources.form.feedlandInstance") | default("FeedLand Instance URL") }}</label>
|
|
<input class="input" type="url" id="feedlandInstance" name="feedlandInstance" value="{{ source.feedlandInstance if source else 'https://feedland.com' }}" placeholder="https://feedland.com">
|
|
<span class="hint">{{ __("blogroll.sources.form.feedlandInstanceHint") | default("FeedLand instance URL (feedland.com or self-hosted)") }}</span>
|
|
</div>
|
|
|
|
<div class="blogroll-field" id="feedlandUsernameField" style="display: none;">
|
|
<label class="label" for="feedlandUsername">{{ __("blogroll.sources.form.feedlandUsername") | default("FeedLand Username") }}</label>
|
|
<input class="input" type="text" id="feedlandUsername" name="feedlandUsername" value="{{ source.feedlandUsername if source else '' }}" placeholder="e.g., davewiner">
|
|
<span class="hint">{{ __("blogroll.sources.form.feedlandUsernameHint") | default("Your FeedLand screen name") }}</span>
|
|
</div>
|
|
|
|
<div class="blogroll-field" id="feedlandCategoryField" style="display: none;">
|
|
<label class="label" for="feedlandCategory">{{ __("blogroll.sources.form.feedlandCategory") | default("FeedLand Category") }}</label>
|
|
<div style="display: flex; gap: 0.5rem; align-items: flex-start;">
|
|
<select class="select" id="feedlandCategory" name="feedlandCategory" style="flex: 1;">
|
|
<option value="">{{ __("blogroll.sources.form.feedlandCategoryAll") | default("All subscriptions") }}</option>
|
|
{% if source.feedlandCategory %}
|
|
<option value="{{ source.feedlandCategory }}" selected>{{ source.feedlandCategory }}</option>
|
|
{% endif %}
|
|
</select>
|
|
<button type="button" class="button button--secondary" onclick="loadFeedlandCategories()" id="feedlandLoadBtn">{{ __("blogroll.sources.form.feedlandLoadCategories") | default("Load") }}</button>
|
|
</div>
|
|
<span class="hint" id="feedlandCategoryHint">{{ __("blogroll.sources.form.feedlandCategoryHint") | default("Optional: sync only feeds from a specific category") }}</span>
|
|
</div>
|
|
|
|
<div class="blogroll-field">
|
|
<label class="label" for="syncInterval">{{ __("blogroll.sources.form.syncInterval") }}</label>
|
|
<select class="select" id="syncInterval" name="syncInterval">
|
|
<option value="30" {% if source.syncInterval == 30 %}selected{% endif %}>30 minutes</option>
|
|
<option value="60" {% if not source or source.syncInterval == 60 %}selected{% endif %}>1 hour</option>
|
|
<option value="180" {% if source.syncInterval == 180 %}selected{% endif %}>3 hours</option>
|
|
<option value="360" {% if source.syncInterval == 360 %}selected{% endif %}>6 hours</option>
|
|
<option value="720" {% if source.syncInterval == 720 %}selected{% endif %}>12 hours</option>
|
|
<option value="1440" {% if source.syncInterval == 1440 %}selected{% endif %}>24 hours</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="blogroll-field blogroll-field--inline">
|
|
<input type="checkbox" id="enabled" name="enabled" {% if not source or source.enabled %}checked{% endif %}>
|
|
<label for="enabled">{{ __("blogroll.sources.form.enabled") }}</label>
|
|
</div>
|
|
|
|
<div class="blogroll-actions">
|
|
{{ button({ type: "submit", text: __("blogroll.sources.create") if isNew else __("blogroll.sources.save") }) }}
|
|
{{ button({ href: baseUrl + "/sources", text: __("blogroll.cancel"), classes: "button--secondary" }) }}
|
|
</div>
|
|
</form>
|
|
|
|
<script>
|
|
function toggleTypeFields() {
|
|
const type = document.getElementById('type').value;
|
|
const urlField = document.getElementById('urlField');
|
|
const opmlContentField = document.getElementById('opmlContentField');
|
|
const microsubChannelField = document.getElementById('microsubChannelField');
|
|
const categoryPrefixField = document.getElementById('categoryPrefixField');
|
|
const feedlandInstanceField = document.getElementById('feedlandInstanceField');
|
|
const feedlandUsernameField = document.getElementById('feedlandUsernameField');
|
|
const feedlandCategoryField = document.getElementById('feedlandCategoryField');
|
|
|
|
// Hide all type-specific fields first
|
|
urlField.style.display = 'none';
|
|
opmlContentField.style.display = 'none';
|
|
if (microsubChannelField) microsubChannelField.style.display = 'none';
|
|
if (categoryPrefixField) categoryPrefixField.style.display = 'none';
|
|
if (feedlandInstanceField) feedlandInstanceField.style.display = 'none';
|
|
if (feedlandUsernameField) feedlandUsernameField.style.display = 'none';
|
|
if (feedlandCategoryField) feedlandCategoryField.style.display = 'none';
|
|
|
|
// Show fields based on type
|
|
if (type === 'opml_url') {
|
|
urlField.style.display = 'flex';
|
|
} else if (type === 'opml_file') {
|
|
opmlContentField.style.display = 'flex';
|
|
} else if (type === 'microsub') {
|
|
if (microsubChannelField) microsubChannelField.style.display = 'flex';
|
|
if (categoryPrefixField) categoryPrefixField.style.display = 'flex';
|
|
} else if (type === 'feedland') {
|
|
if (feedlandInstanceField) feedlandInstanceField.style.display = 'flex';
|
|
if (feedlandUsernameField) feedlandUsernameField.style.display = 'flex';
|
|
if (feedlandCategoryField) feedlandCategoryField.style.display = 'flex';
|
|
}
|
|
}
|
|
|
|
function loadFeedlandCategories() {
|
|
const instance = document.getElementById('feedlandInstance').value;
|
|
const username = document.getElementById('feedlandUsername').value;
|
|
const select = document.getElementById('feedlandCategory');
|
|
const btn = document.getElementById('feedlandLoadBtn');
|
|
const hint = document.getElementById('feedlandCategoryHint');
|
|
const currentValue = select.value;
|
|
|
|
if (!instance || !username) {
|
|
hint.textContent = 'Please enter instance URL and username first';
|
|
return;
|
|
}
|
|
|
|
btn.disabled = true;
|
|
btn.textContent = '...';
|
|
hint.textContent = 'Loading categories...';
|
|
|
|
const baseUrl = '{{ baseUrl }}';
|
|
const url = baseUrl + '/api/feedland-categories?instance=' + encodeURIComponent(instance) + '&username=' + encodeURIComponent(username);
|
|
|
|
fetch(url, { credentials: 'same-origin' })
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
if (data.error) {
|
|
hint.textContent = 'Error: ' + data.error;
|
|
return;
|
|
}
|
|
|
|
// Clear existing options and rebuild safely using DOM methods
|
|
while (select.options.length > 0) select.remove(0);
|
|
var allOpt = document.createElement('option');
|
|
allOpt.value = '';
|
|
allOpt.textContent = '{{ __("blogroll.sources.form.feedlandCategoryAll") | default("All subscriptions") }}';
|
|
select.appendChild(allOpt);
|
|
|
|
var cats = data.categories || [];
|
|
cats.forEach(function(cat) {
|
|
var opt = document.createElement('option');
|
|
opt.value = cat;
|
|
opt.textContent = cat;
|
|
if (cat === currentValue) opt.selected = true;
|
|
select.appendChild(opt);
|
|
});
|
|
|
|
hint.textContent = cats.length + ' categories found' + (data.screenname ? ' for ' + data.screenname : '');
|
|
})
|
|
.catch(function(err) {
|
|
hint.textContent = 'Failed to load: ' + err.message;
|
|
})
|
|
.finally(function() {
|
|
btn.disabled = false;
|
|
btn.textContent = '{{ __("blogroll.sources.form.feedlandLoadCategories") | default("Load") }}';
|
|
});
|
|
}
|
|
|
|
// Initialize on load
|
|
toggleTypeFields();
|
|
</script>
|
|
{% endblock %}
|