- Add Radio France presets (France Inter, France Culture, France Info, FIP, Mouv) - Add international radio (BBC World, NPR) - Add tech podcasts (Darknet Diaries, Changelog, Syntax.fm) - Add 'radio' category to feed selector - One-click quick-add buttons for all radio presets Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
449 lines
15 KiB
JavaScript
449 lines
15 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require dom';
|
|
'require ui';
|
|
'require cyberfeed.api as api';
|
|
|
|
return view.extend({
|
|
title: _('Feed Sources'),
|
|
|
|
load: function() {
|
|
var cssLink = document.createElement('link');
|
|
cssLink.rel = 'stylesheet';
|
|
cssLink.href = L.resource('cyberfeed/dashboard.css');
|
|
document.head.appendChild(cssLink);
|
|
|
|
return api.getFeeds();
|
|
},
|
|
|
|
render: function(feeds) {
|
|
var self = this;
|
|
feeds = Array.isArray(feeds) ? feeds : [];
|
|
|
|
var content = [];
|
|
|
|
// Page Header
|
|
content.push(E('div', { 'class': 'cf-card' }, [
|
|
E('div', { 'class': 'cf-card-header' }, [
|
|
E('div', { 'class': 'cf-card-title' }, [
|
|
E('span', { 'class': 'cf-card-title-icon' }, '\uD83D\uDCE1'),
|
|
'Feed Sources'
|
|
]),
|
|
E('a', {
|
|
'href': L.url('admin/services/cyberfeed/overview'),
|
|
'class': 'cf-btn cf-btn-sm cf-btn-secondary'
|
|
}, ['\u2190', ' Back'])
|
|
])
|
|
]));
|
|
|
|
// Add Feed Card
|
|
content.push(E('div', { 'class': 'cf-card' }, [
|
|
E('div', { 'class': 'cf-card-header' }, [
|
|
E('div', { 'class': 'cf-card-title' }, [
|
|
E('span', { 'class': 'cf-card-title-icon' }, '\u2795'),
|
|
'Add New Feed'
|
|
])
|
|
]),
|
|
E('div', { 'class': 'cf-card-body' }, [
|
|
E('div', { 'class': 'cf-grid cf-grid-2', 'style': 'gap: 16px;' }, [
|
|
E('div', { 'class': 'cf-form-group' }, [
|
|
E('label', { 'class': 'cf-form-label' }, 'Feed Name'),
|
|
E('input', {
|
|
'type': 'text',
|
|
'id': 'new-name',
|
|
'class': 'cf-form-input',
|
|
'placeholder': 'my_feed (alphanumeric, no spaces)'
|
|
})
|
|
]),
|
|
E('div', { 'class': 'cf-form-group' }, [
|
|
E('label', { 'class': 'cf-form-label' }, 'Feed URL'),
|
|
E('input', {
|
|
'type': 'url',
|
|
'id': 'new-url',
|
|
'class': 'cf-form-input',
|
|
'placeholder': 'https://example.com/feed.xml'
|
|
})
|
|
]),
|
|
E('div', { 'class': 'cf-form-group' }, [
|
|
E('label', { 'class': 'cf-form-label' }, 'Type'),
|
|
E('select', { 'id': 'new-type', 'class': 'cf-form-input' }, [
|
|
E('option', { 'value': 'rss' }, 'RSS/Atom'),
|
|
E('option', { 'value': 'rss-bridge' }, 'RSS-Bridge')
|
|
])
|
|
]),
|
|
E('div', { 'class': 'cf-form-group' }, [
|
|
E('label', { 'class': 'cf-form-label' }, 'Category'),
|
|
E('select', { 'id': 'new-category', 'class': 'cf-form-input' }, [
|
|
E('option', { 'value': 'custom' }, 'Custom'),
|
|
E('option', { 'value': 'security' }, 'Security'),
|
|
E('option', { 'value': 'tech' }, 'Tech'),
|
|
E('option', { 'value': 'social' }, 'Social'),
|
|
E('option', { 'value': 'news' }, 'News'),
|
|
E('option', { 'value': 'radio' }, 'Radio/Podcasts')
|
|
])
|
|
])
|
|
]),
|
|
E('div', { 'style': 'margin-top: 16px;' }, [
|
|
E('button', {
|
|
'class': 'cf-btn cf-btn-primary',
|
|
'click': function() { self.handleAddFeed(); }
|
|
}, ['\u2795', ' Add Feed'])
|
|
])
|
|
])
|
|
]));
|
|
|
|
// RSS-Bridge Templates
|
|
content.push(E('div', { 'class': 'cf-card cf-rssbridge-card' }, [
|
|
E('div', { 'class': 'cf-card-header' }, [
|
|
E('div', { 'class': 'cf-card-title' }, [
|
|
E('span', { 'class': 'cf-card-title-icon' }, '\uD83C\uDF09'),
|
|
'RSS-Bridge Templates'
|
|
])
|
|
]),
|
|
E('div', { 'class': 'cf-card-body' }, [
|
|
E('p', { 'style': 'margin-bottom: 16px; color: var(--cf-text-dim);' },
|
|
'Quick-add social media feeds (requires RSS-Bridge to be running)'),
|
|
E('div', { 'class': 'cf-grid cf-grid-3', 'style': 'gap: 12px;' }, [
|
|
E('button', {
|
|
'class': 'cf-btn cf-btn-sm',
|
|
'click': function() { self.showBridgeModal('Facebook'); }
|
|
}, ['\uD83D\uDCD8', ' Facebook']),
|
|
E('button', {
|
|
'class': 'cf-btn cf-btn-sm',
|
|
'click': function() { self.showBridgeModal('Twitter'); }
|
|
}, ['\uD83D\uDC26', ' Twitter/X']),
|
|
E('button', {
|
|
'class': 'cf-btn cf-btn-sm',
|
|
'click': function() { self.showBridgeModal('Youtube'); }
|
|
}, ['\uD83D\uDCFA', ' YouTube']),
|
|
E('button', {
|
|
'class': 'cf-btn cf-btn-sm',
|
|
'click': function() { self.showBridgeModal('Mastodon'); }
|
|
}, ['\uD83D\uDC18', ' Mastodon']),
|
|
E('button', {
|
|
'class': 'cf-btn cf-btn-sm',
|
|
'click': function() { self.showBridgeModal('Reddit'); }
|
|
}, ['\uD83E\uDD16', ' Reddit']),
|
|
E('button', {
|
|
'class': 'cf-btn cf-btn-sm',
|
|
'click': function() { self.showBridgeModal('Instagram'); }
|
|
}, ['\uD83D\uDCF7', ' Instagram'])
|
|
])
|
|
])
|
|
]));
|
|
|
|
// Radio Presets
|
|
content.push(E('div', { 'class': 'cf-card' }, [
|
|
E('div', { 'class': 'cf-card-header' }, [
|
|
E('div', { 'class': 'cf-card-title' }, [
|
|
E('span', { 'class': 'cf-card-title-icon' }, '\uD83D\uDCFB'),
|
|
'Radio & Podcast Presets'
|
|
])
|
|
]),
|
|
E('div', { 'class': 'cf-card-body' }, [
|
|
E('p', { 'style': 'margin-bottom: 16px; color: var(--cf-text-dim);' },
|
|
'Quick-add popular radio stations and podcasts'),
|
|
E('div', { 'style': 'margin-bottom: 12px;' }, [
|
|
E('strong', { 'style': 'color: var(--cf-neon-cyan);' }, 'Radio France')
|
|
]),
|
|
E('div', { 'class': 'cf-grid cf-grid-3', 'style': 'gap: 12px; margin-bottom: 16px;' }, [
|
|
E('button', {
|
|
'class': 'cf-btn cf-btn-sm',
|
|
'click': function() { self.addRadioPreset('franceinter', 'https://radiofrance-podcast.net/podcast09/rss_10239.xml', 'France Inter'); }
|
|
}, '\uD83C\uDDEB\uD83C\uDDF7 France Inter'),
|
|
E('button', {
|
|
'class': 'cf-btn cf-btn-sm',
|
|
'click': function() { self.addRadioPreset('franceculture', 'https://radiofrance-podcast.net/podcast09/rss_10351.xml', 'France Culture'); }
|
|
}, '\uD83C\uDDEB\uD83C\uDDF7 France Culture'),
|
|
E('button', {
|
|
'class': 'cf-btn cf-btn-sm',
|
|
'click': function() { self.addRadioPreset('franceinfo', 'https://radiofrance-podcast.net/podcast09/rss_10134.xml', 'France Info'); }
|
|
}, '\uD83C\uDDEB\uD83C\uDDF7 France Info'),
|
|
E('button', {
|
|
'class': 'cf-btn cf-btn-sm',
|
|
'click': function() { self.addRadioPreset('fip', 'https://radiofrance-podcast.net/podcast09/rss_18981.xml', 'FIP'); }
|
|
}, '\uD83C\uDFA7 FIP'),
|
|
E('button', {
|
|
'class': 'cf-btn cf-btn-sm',
|
|
'click': function() { self.addRadioPreset('mouv', 'https://radiofrance-podcast.net/podcast09/rss_21649.xml', 'Mouv'); }
|
|
}, '\uD83C\uDFA4 Mouv')
|
|
]),
|
|
E('div', { 'style': 'margin-bottom: 12px;' }, [
|
|
E('strong', { 'style': 'color: var(--cf-neon-cyan);' }, 'International & Tech')
|
|
]),
|
|
E('div', { 'class': 'cf-grid cf-grid-3', 'style': 'gap: 12px;' }, [
|
|
E('button', {
|
|
'class': 'cf-btn cf-btn-sm',
|
|
'click': function() { self.addRadioPreset('bbc_world', 'https://feeds.bbci.co.uk/news/world/rss.xml', 'BBC World'); }
|
|
}, '\uD83C\uDDEC\uD83C\uDDE7 BBC World'),
|
|
E('button', {
|
|
'class': 'cf-btn cf-btn-sm',
|
|
'click': function() { self.addRadioPreset('npr', 'https://feeds.npr.org/1001/rss.xml', 'NPR News'); }
|
|
}, '\uD83C\uDDFA\uD83C\uDDF8 NPR'),
|
|
E('button', {
|
|
'class': 'cf-btn cf-btn-sm',
|
|
'click': function() { self.addRadioPreset('darknet_diaries', 'https://feeds.megaphone.fm/darknetdiaries', 'Darknet Diaries'); }
|
|
}, '\uD83D\uDD75 Darknet Diaries'),
|
|
E('button', {
|
|
'class': 'cf-btn cf-btn-sm',
|
|
'click': function() { self.addRadioPreset('changelog', 'https://changelog.com/podcast/feed', 'The Changelog'); }
|
|
}, '\uD83D\uDCBB Changelog'),
|
|
E('button', {
|
|
'class': 'cf-btn cf-btn-sm',
|
|
'click': function() { self.addRadioPreset('syntax', 'https://feed.syntax.fm/rss', 'Syntax.fm'); }
|
|
}, '\uD83D\uDCBB Syntax.fm')
|
|
])
|
|
])
|
|
]));
|
|
|
|
// Feeds List
|
|
var feedsContent;
|
|
if (feeds.length === 0) {
|
|
feedsContent = E('div', { 'class': 'cf-empty' }, [
|
|
E('div', { 'class': 'cf-empty-icon' }, '\uD83D\uDCE1'),
|
|
E('div', { 'class': 'cf-empty-text' }, 'No feeds configured'),
|
|
E('div', { 'class': 'cf-empty-hint' }, 'Add a feed above to get started')
|
|
]);
|
|
} else {
|
|
feedsContent = E('table', { 'class': 'cf-table' }, [
|
|
E('thead', {}, [
|
|
E('tr', {}, [
|
|
E('th', {}, 'Name'),
|
|
E('th', {}, 'URL'),
|
|
E('th', {}, 'Type'),
|
|
E('th', {}, 'Category'),
|
|
E('th', { 'style': 'width: 100px; text-align: right;' }, 'Actions')
|
|
])
|
|
]),
|
|
E('tbody', {}, feeds.map(function(feed) {
|
|
return E('tr', {}, [
|
|
E('td', { 'style': 'font-weight: 600;' }, feed.name),
|
|
E('td', {}, [
|
|
E('span', { 'style': 'font-size: 0.85rem; color: var(--cf-text-dim); word-break: break-all;' },
|
|
feed.url.length > 50 ? feed.url.substring(0, 50) + '...' : feed.url)
|
|
]),
|
|
E('td', {}, [
|
|
E('span', { 'class': 'cf-badge cf-badge-info' }, feed.type || 'rss')
|
|
]),
|
|
E('td', {}, [
|
|
E('span', { 'class': 'cf-badge cf-badge-category' }, feed.category || 'custom')
|
|
]),
|
|
E('td', { 'style': 'text-align: right;' }, [
|
|
E('button', {
|
|
'class': 'cf-btn cf-btn-sm cf-btn-danger',
|
|
'click': function() { self.handleDeleteFeed(feed.name); }
|
|
}, 'Delete')
|
|
])
|
|
]);
|
|
}))
|
|
]);
|
|
}
|
|
|
|
content.push(E('div', { 'class': 'cf-card' }, [
|
|
E('div', { 'class': 'cf-card-header' }, [
|
|
E('div', { 'class': 'cf-card-title' }, [
|
|
E('span', { 'class': 'cf-card-title-icon' }, '\uD83D\uDCCB'),
|
|
'Configured Feeds (' + feeds.length + ')'
|
|
])
|
|
]),
|
|
E('div', { 'class': 'cf-card-body no-padding' }, [feedsContent])
|
|
]));
|
|
|
|
return E('div', { 'class': 'cyberfeed-dashboard' }, content);
|
|
},
|
|
|
|
handleAddFeed: function() {
|
|
var self = this;
|
|
var name = document.getElementById('new-name').value.trim();
|
|
var url = document.getElementById('new-url').value.trim();
|
|
var type = document.getElementById('new-type').value;
|
|
var category = document.getElementById('new-category').value;
|
|
|
|
if (!name) {
|
|
self.showToast('Please enter a feed name', 'error');
|
|
return;
|
|
}
|
|
|
|
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
self.showToast('Name must be alphanumeric (underscores/dashes allowed)', 'error');
|
|
return;
|
|
}
|
|
|
|
if (!url) {
|
|
self.showToast('Please enter a feed URL', 'error');
|
|
return;
|
|
}
|
|
|
|
return api.addFeed(name, url, type, category).then(function(res) {
|
|
if (res && res.success) {
|
|
self.showToast('Feed added successfully', 'success');
|
|
window.location.reload();
|
|
} else {
|
|
self.showToast('Failed: ' + (res.error || 'Unknown error'), 'error');
|
|
}
|
|
});
|
|
},
|
|
|
|
addRadioPreset: function(name, url, displayName) {
|
|
var self = this;
|
|
|
|
return api.addFeed(name, url, 'rss', 'radio').then(function(res) {
|
|
if (res && res.success) {
|
|
self.showToast(displayName + ' added', 'success');
|
|
window.location.reload();
|
|
} else {
|
|
self.showToast('Failed: ' + (res.error || 'Unknown error'), 'error');
|
|
}
|
|
});
|
|
},
|
|
|
|
handleDeleteFeed: function(name) {
|
|
var self = this;
|
|
|
|
ui.showModal('Delete Feed', [
|
|
E('div', { 'style': 'margin-bottom: 16px;' }, [
|
|
E('p', {}, 'Are you sure you want to delete this feed?'),
|
|
E('div', {
|
|
'style': 'margin-top: 12px; padding: 12px; background: rgba(0,255,255,0.1); border-radius: 8px; font-family: monospace;'
|
|
}, name)
|
|
]),
|
|
E('div', { 'style': 'display: flex; justify-content: flex-end; gap: 12px;' }, [
|
|
E('button', {
|
|
'class': 'cf-btn cf-btn-secondary',
|
|
'click': ui.hideModal
|
|
}, 'Cancel'),
|
|
E('button', {
|
|
'class': 'cf-btn cf-btn-danger',
|
|
'click': function() {
|
|
ui.hideModal();
|
|
api.deleteFeed(name).then(function(res) {
|
|
if (res && res.success) {
|
|
self.showToast('Feed deleted', 'success');
|
|
window.location.reload();
|
|
} else {
|
|
self.showToast('Failed: ' + (res.error || 'Unknown error'), 'error');
|
|
}
|
|
});
|
|
}
|
|
}, 'Delete')
|
|
])
|
|
]);
|
|
},
|
|
|
|
showBridgeModal: function(bridge) {
|
|
var self = this;
|
|
var fields = {
|
|
'Facebook': { param: 'u', label: 'Page/User Name', placeholder: 'CyberMindFR' },
|
|
'Twitter': { param: 'u', label: 'Username', placeholder: 'TheHackersNews' },
|
|
'Youtube': { param: 'c', label: 'Channel ID', placeholder: 'UCxxxxxxx' },
|
|
'Mastodon': { param: 'username', label: 'Username', placeholder: 'user', extra: 'instance', extraLabel: 'Instance', extraPlaceholder: 'mastodon.social' },
|
|
'Reddit': { param: 'r', label: 'Subreddit', placeholder: 'netsec' },
|
|
'Instagram': { param: 'u', label: 'Username', placeholder: 'username' }
|
|
};
|
|
|
|
var config = fields[bridge] || { param: 'u', label: 'Username', placeholder: 'username' };
|
|
|
|
var modalContent = [
|
|
E('div', { 'style': 'margin-bottom: 16px;' }, [
|
|
E('p', { 'style': 'color: var(--cf-text-dim);' },
|
|
'Add a ' + bridge + ' feed via RSS-Bridge')
|
|
]),
|
|
E('div', { 'class': 'cf-form-group' }, [
|
|
E('label', { 'class': 'cf-form-label' }, 'Feed Name'),
|
|
E('input', {
|
|
'type': 'text',
|
|
'id': 'bridge-name',
|
|
'class': 'cf-form-input',
|
|
'placeholder': bridge.toLowerCase() + '_feed'
|
|
})
|
|
]),
|
|
E('div', { 'class': 'cf-form-group' }, [
|
|
E('label', { 'class': 'cf-form-label' }, config.label),
|
|
E('input', {
|
|
'type': 'text',
|
|
'id': 'bridge-param',
|
|
'class': 'cf-form-input',
|
|
'placeholder': config.placeholder
|
|
})
|
|
])
|
|
];
|
|
|
|
if (config.extra) {
|
|
modalContent.push(E('div', { 'class': 'cf-form-group' }, [
|
|
E('label', { 'class': 'cf-form-label' }, config.extraLabel),
|
|
E('input', {
|
|
'type': 'text',
|
|
'id': 'bridge-extra',
|
|
'class': 'cf-form-input',
|
|
'placeholder': config.extraPlaceholder
|
|
})
|
|
]));
|
|
}
|
|
|
|
modalContent.push(E('div', { 'style': 'display: flex; justify-content: flex-end; gap: 12px; margin-top: 20px;' }, [
|
|
E('button', {
|
|
'class': 'cf-btn cf-btn-secondary',
|
|
'click': ui.hideModal
|
|
}, 'Cancel'),
|
|
E('button', {
|
|
'class': 'cf-btn cf-btn-primary',
|
|
'click': function() {
|
|
var name = document.getElementById('bridge-name').value.trim();
|
|
var param = document.getElementById('bridge-param').value.trim();
|
|
var extra = config.extra ? document.getElementById('bridge-extra').value.trim() : '';
|
|
|
|
if (!name || !param) {
|
|
self.showToast('Please fill in all fields', 'error');
|
|
return;
|
|
}
|
|
|
|
var url = 'http://localhost:3000/?action=display&bridge=' + bridge;
|
|
url += '&' + config.param + '=' + encodeURIComponent(param);
|
|
if (config.extra && extra) {
|
|
url += '&' + config.extra + '=' + encodeURIComponent(extra);
|
|
}
|
|
url += '&format=Atom';
|
|
|
|
ui.hideModal();
|
|
api.addFeed(name, url, 'rss-bridge', 'social').then(function(res) {
|
|
if (res && res.success) {
|
|
self.showToast('Feed added successfully', 'success');
|
|
window.location.reload();
|
|
} else {
|
|
self.showToast('Failed: ' + (res.error || 'Unknown error'), 'error');
|
|
}
|
|
});
|
|
}
|
|
}, 'Add Feed')
|
|
]));
|
|
|
|
ui.showModal(bridge + ' Feed', modalContent);
|
|
},
|
|
|
|
showToast: function(message, type) {
|
|
var existing = document.querySelector('.cf-toast');
|
|
if (existing) existing.remove();
|
|
|
|
var iconMap = {
|
|
'success': '\u2705',
|
|
'error': '\u274C',
|
|
'warning': '\u26A0\uFE0F',
|
|
'info': '\u2139\uFE0F'
|
|
};
|
|
|
|
var toast = E('div', { 'class': 'cf-toast ' + (type || '') }, [
|
|
E('span', {}, iconMap[type] || '\u2139\uFE0F'),
|
|
message
|
|
]);
|
|
document.body.appendChild(toast);
|
|
|
|
setTimeout(function() {
|
|
toast.remove();
|
|
}, 4000);
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|