diff --git a/src/import.rs b/src/import.rs new file mode 100644 index 0000000..0aeb5c8 --- /dev/null +++ b/src/import.rs @@ -0,0 +1,52 @@ +// Module for handling OPML feed list imports + +use std::io::Cursor; + +use rocket::data::ToByteUnit; +use rocket::http::Status; +use rocket::serde::json::Json; +use rocket::serde::{Deserialize, Serialize}; +use rocket::{post, Data}; +use rocket_db_pools::Connection; +use tracing::error; + +use crate::user::AuthenticatedUser; +use crate::Db; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct ImportResponse { + success: bool, + message: String, + feeds_imported: Option, +} + +/// Import feeds from an OPML file +#[post("/import/opml", data = "")] +pub async fn import_opml( + mut _db: Connection, + _user: AuthenticatedUser, + file: Data<'_>, +) -> Result, Status> { + // Limit file size to 1MB + let file_data = file.open(1.mebibytes()).into_bytes().await.map_err(|e| { + error!("Failed to read OPML file: {}", e); + Status::BadRequest + })?; + + if !file_data.is_complete() { + error!("OPML file too large"); + return Err(Status::PayloadTooLarge); + } + + let bytes = file_data.value; + let _cursor = Cursor::new(bytes); + + // TODO: Parse OPML and import feeds + // For now just return a placeholder response + Ok(Json(ImportResponse { + success: true, + message: "OPML file received successfully. Import not yet implemented.".to_string(), + feeds_imported: None, + })) +} diff --git a/src/main.rs b/src/main.rs index b12963d..354cefb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ use tracing_subscriber::FmtSubscriber; mod demo; mod feed_utils; mod feeds; +mod import; mod poll; mod poll_utils; mod session_store; @@ -162,6 +163,7 @@ fn rocket() -> _ { feeds::delete_feed, poll::poll_feed, poll::update_entry_state, + import::import_opml, ], ) .mount("/static", FileServer::from("static")) diff --git a/static/js/app.js b/static/js/app.js index 8049f9d..d0be6a3 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1,20 +1,23 @@ -// Add at the beginning of the file document.addEventListener('DOMContentLoaded', function() { // Mobile menu functionality const hamburgerMenu = document.getElementById('hamburgerMenu'); const sidebarClose = document.getElementById('sidebarClose'); const sidebar = document.getElementById('sidebar'); + const userMenuButton = document.getElementById('userMenuButton'); + const userMenuDropdown = document.getElementById('userMenuDropdown'); - // Add OPML import button handler - const importOpmlButton = document.getElementById('importOpmlButton'); - importOpmlButton.addEventListener('click', function() { - console.log('OPML import button clicked - functionality coming soon!'); - }); + // Create a hidden file input for OPML upload + const opmlFileInput = document.createElement('input'); + opmlFileInput.type = 'file'; + opmlFileInput.accept = '.opml,.xml'; + opmlFileInput.style.display = 'none'; + document.body.appendChild(opmlFileInput); function toggleSidebar() { sidebar.classList.toggle('active'); } + // Mobile menu handlers hamburgerMenu.addEventListener('click', toggleSidebar); sidebarClose.addEventListener('click', toggleSidebar); @@ -28,6 +31,80 @@ document.addEventListener('DOMContentLoaded', function() { sidebar.classList.remove('active'); } }); + + // OPML import handlers + opmlFileInput.addEventListener('change', async (e) => { + if (e.target.files.length > 0) { + const file = e.target.files[0]; + const formData = new FormData(); + formData.append('file', file); + + try { + const response = await fetch('/import/opml', { + method: 'POST', + body: formData + }); + + if (response.ok) { + const result = await response.json(); + if (result.success) { + alert('OPML import successful: ' + result.message); + // Refresh the feed list if feeds were imported + if (result.feeds_imported) { + handleFeeds(); + } + } else { + alert('OPML import failed: ' + result.message); + } + } else { + alert('Failed to import OPML file. Please try again.'); + } + } catch (error) { + console.error('OPML import failed:', error); + alert('Failed to import OPML file. Please try again.'); + } + } + // Clear the input so the same file can be selected again + e.target.value = ''; + }); + + // User menu handlers + userMenuButton.addEventListener('click', (e) => { + e.stopPropagation(); + userMenuDropdown.classList.toggle('show'); + }); + + document.addEventListener('click', (e) => { + if (!e.target.closest('.user-menu')) { + userMenuDropdown.classList.remove('show'); + } + }); + + userMenuDropdown.addEventListener('click', (e) => { + e.stopPropagation(); + }); + + document.getElementById('importOpmlButton').addEventListener('click', () => { + opmlFileInput.click(); + userMenuDropdown.classList.remove('show'); + }); + + document.getElementById('logoutButton').addEventListener('click', async () => { + try { + const response = await fetch('/logout', { + method: 'POST', + }); + + if (response.ok) { + window.location.href = '/login'; + } + } catch (error) { + console.error('Logout failed:', error); + } + }); + + // Initialize feeds + handleFeeds(); }); // Fetch and display feeds @@ -348,46 +425,6 @@ async function handleFeeds() { } } -// Load feeds when page loads -document.addEventListener('DOMContentLoaded', handleFeeds); - -// User menu functionality -const userMenuButton = document.getElementById('userMenuButton'); -const userMenuDropdown = document.getElementById('userMenuDropdown'); - -// Toggle menu on button click -userMenuButton.addEventListener('click', (e) => { - e.stopPropagation(); - userMenuDropdown.classList.toggle('show'); -}); - -// Close menu when clicking outside -document.addEventListener('click', (e) => { - if (!e.target.closest('.user-menu')) { - userMenuDropdown.classList.remove('show'); - } -}); - -// Prevent menu from closing when clicking inside it -userMenuDropdown.addEventListener('click', (e) => { - e.stopPropagation(); -}); - -// Logout functionality -document.getElementById('logoutButton').addEventListener('click', async () => { - try { - const response = await fetch('/logout', { - method: 'POST', - }); - - if (response.ok) { - window.location.href = '/login'; - } - } catch (error) { - console.error('Logout failed:', error); - } -}); - // Modal functionality const modal = document.getElementById('addFeedModal'); const addFeedButton = document.getElementById('addFeedButton');