add sync action
This commit is contained in:
parent
31501d65d8
commit
23f912d845
@ -50,7 +50,7 @@ pub fn add_channel(ctx: &TrsEnv, args: &AddChannelArgs) -> Result<RssChannelD, T
|
||||
.ignore_invalid_encoding_declarations(true)
|
||||
.create_reader(&bytes[..]);
|
||||
let channel = parser::parse_rss_channel(xml_source_stream)?;
|
||||
ctx.db.add_channel(&channel)
|
||||
ctx.db.add_channel(&args.link, &channel)
|
||||
}
|
||||
|
||||
pub fn list_channels(ctx: &TrsEnv, args: &ListChannelArgs) -> Result<Vec<RssChannelD>, TrsError> {
|
||||
|
||||
@ -11,7 +11,7 @@ pub mod ui;
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let args = argh::from_env::<TrsArgs>();
|
||||
let db_name = "test3";
|
||||
let db_name = "test4";
|
||||
match args.sub_command {
|
||||
TrsSubCommand::AddChannel(args) => {
|
||||
let mut ctx = TrsEnv::new(db_name)?;
|
||||
|
||||
@ -12,6 +12,7 @@ const SCHEMA_CHANNELS: &'static str = "CREATE TABLE IF NOT EXISTS Channels ( \
|
||||
id INTEGER PRIMARY KEY, \
|
||||
name TEXT NOT NULL, \
|
||||
link TEXT NOT NULL UNIQUE, \
|
||||
feed_link TEXT NOT NULL UNIQUE, \
|
||||
description TEXT, \
|
||||
last_update INTEGER\
|
||||
)";
|
||||
@ -28,14 +29,14 @@ const SCHEMA_ARTICLES: &'static str = "CREATE TABLE IF NOT EXISTS Articles ( \
|
||||
FOREIGN KEY(channel_id) REFERENCES Channels(id) ON DELETE CASCADE \
|
||||
)";
|
||||
|
||||
const ADD_CHANNEL: &'static str = "INSERT INTO Channels (name, link, description, last_update) \
|
||||
VALUES (?1, ?2, ?3, ?4)\
|
||||
ON CONFLICT(link) DO UPDATE SET name=?1, description=?3, last_update=?4";
|
||||
const ADD_CHANNEL: &'static str = "INSERT INTO Channels (name, link, feed_link, description, last_update) \
|
||||
VALUES (?1, ?2, ?3, ?4, ?5)\
|
||||
ON CONFLICT(link) DO UPDATE SET name=?1, description=?4, last_update=?5";
|
||||
const REMOVE_CHANNEL: &'static str = "DELETE FROM Channels WHERE id = ?1";
|
||||
const LIST_CHANNELS: &'static str =
|
||||
"SELECT id, name, link, description, last_update FROM Channels LIMIT ?1";
|
||||
"SELECT id, name, link, feed_link, description, last_update FROM Channels order by last_update DESC LIMIT ?1";
|
||||
const GET_CHANNEL: &'static str =
|
||||
"SELECT id, name, link, description, last_update FROM Channels WHERE link = ?1";
|
||||
"SELECT id, name, link, feed_link, description, last_update FROM Channels WHERE link = ?1";
|
||||
|
||||
const ADD_ARTICLE: &'static str =
|
||||
"INSERT INTO Articles (channel_id, title, description, link, pub_date, last_update, unread) \
|
||||
@ -49,7 +50,7 @@ const GET_ARTICLE: &'static str =
|
||||
"SELECT id, channel_id, title, description, link, pub_date, last_update, unread FROM Articles WHERE link = ?1";
|
||||
|
||||
const LIST_ARTICLES: &'static str =
|
||||
"SELECT id, channel_id, title, description, link, pub_date, last_update, unread FROM Articles";
|
||||
"SELECT id, channel_id, title, description, link, pub_date, last_update, unread FROM Articles order by last_update DESC";
|
||||
|
||||
const MARK_ARTICLE_READ: &'static str = "UPDATE Articles SET unread = FALSE WHERE id = ?1";
|
||||
|
||||
@ -63,6 +64,7 @@ pub struct RssChannelD {
|
||||
pub id: i64,
|
||||
pub title: String,
|
||||
pub link: String,
|
||||
pub feed_link: String,
|
||||
pub description: String,
|
||||
pub last_update: OffsetDateTime,
|
||||
pub articles: Vec<RssArticleD>,
|
||||
@ -126,13 +128,14 @@ impl Db {
|
||||
Ok(channel)
|
||||
}
|
||||
|
||||
pub fn add_channel(&self, channel: &RssChannel) -> Result<RssChannelD> {
|
||||
pub fn add_channel(&self, feed_link: impl AsRef<str>, channel: &RssChannel) -> Result<RssChannelD> {
|
||||
self.connection
|
||||
.execute(
|
||||
ADD_CHANNEL,
|
||||
(
|
||||
&channel.title,
|
||||
&channel.link,
|
||||
feed_link.as_ref(),
|
||||
&channel.description,
|
||||
OffsetDateTime::now_utc().unix_timestamp(),
|
||||
),
|
||||
@ -256,7 +259,8 @@ impl Db {
|
||||
row.get(1)?,
|
||||
row.get(2)?,
|
||||
row.get(3)?,
|
||||
Db::read_datetime(4, &row)?,
|
||||
row.get(4)?,
|
||||
Db::read_datetime(5, &row)?,
|
||||
Vec::new(),
|
||||
))
|
||||
}
|
||||
@ -295,6 +299,7 @@ impl RssChannelD {
|
||||
id: i64,
|
||||
title: String,
|
||||
link: String,
|
||||
feed_link: String,
|
||||
description: String,
|
||||
last_update: OffsetDateTime,
|
||||
articles: Vec<RssArticleD>,
|
||||
@ -303,6 +308,7 @@ impl RssChannelD {
|
||||
id,
|
||||
title,
|
||||
link,
|
||||
feed_link,
|
||||
description,
|
||||
last_update,
|
||||
articles,
|
||||
|
||||
43
src/ui.rs
43
src/ui.rs
@ -1,9 +1,9 @@
|
||||
pub mod actions;
|
||||
pub mod articles;
|
||||
pub mod backend;
|
||||
pub mod channels;
|
||||
pub mod controls;
|
||||
pub mod debug;
|
||||
pub mod executor;
|
||||
pub mod title;
|
||||
|
||||
use std::{
|
||||
@ -22,7 +22,6 @@ use channels::ChannelsWidget;
|
||||
use controls::ControlsWidget;
|
||||
use crossterm::event::{self, KeyEventKind};
|
||||
use debug::DebugWidget;
|
||||
use executor::UiCommandExecutor;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
@ -66,6 +65,7 @@ pub enum UiAction {
|
||||
ShowAddChannelUi,
|
||||
RemoveChannel,
|
||||
ToggleReadStatus,
|
||||
SyncChannel,
|
||||
Exit,
|
||||
}
|
||||
|
||||
@ -86,11 +86,24 @@ pub enum UiCommandDispatchActions {
|
||||
ListChannels(args::ListChannelArgs),
|
||||
}
|
||||
|
||||
|
||||
/// APP
|
||||
/// - Listen Event
|
||||
/// - Publish UiCommandDispatchActions
|
||||
///
|
||||
/// BACKEND
|
||||
/// - Listen UiCommandDispatchActions
|
||||
/// - Publish BackendEvent
|
||||
///
|
||||
/// EVENT LOOP
|
||||
/// - Listen BackendEvent
|
||||
/// - Listen crossterm::event::Event
|
||||
/// - Publish Event
|
||||
pub async fn ui(args: &UiArgs, db_name: &str) -> Result<()> {
|
||||
let (app_dispatch, app_recv) = channel();
|
||||
let (executor_dispatch, executor_recv) = tokio::sync::mpsc::unbounded_channel();
|
||||
let event_recv = start_event_loop(executor_recv);
|
||||
let mut terminal = ratatui::init();
|
||||
let (ui_action_publisher, ui_action_receiver) = channel();
|
||||
let (backend_event_publisher, backend_event_receiver) = tokio::sync::mpsc::unbounded_channel();
|
||||
let event_receiver = start_event_loop(backend_event_receiver);
|
||||
|
||||
let mut app_state = AppState {
|
||||
channels: Vec::new(),
|
||||
exit: false,
|
||||
@ -102,15 +115,11 @@ pub async fn ui(args: &UiArgs, db_name: &str) -> Result<()> {
|
||||
last_action: None,
|
||||
show_add_channel_ui: false,
|
||||
add_channel: String::new(),
|
||||
dispatcher: app_dispatch,
|
||||
receiver: event_recv,
|
||||
dispatcher: ui_action_publisher,
|
||||
receiver: event_receiver,
|
||||
};
|
||||
|
||||
let db_name = db_name.to_string();
|
||||
std::thread::spawn(move || {
|
||||
let mut executor = UiCommandExecutor::new(app_recv, executor_dispatch);
|
||||
executor.run(db_name);
|
||||
});
|
||||
start_backend(db_name, ui_action_receiver, backend_event_publisher);
|
||||
|
||||
app_state
|
||||
.dispatcher
|
||||
@ -119,6 +128,7 @@ pub async fn ui(args: &UiArgs, db_name: &str) -> Result<()> {
|
||||
))
|
||||
.map_err(|e| TrsError::Error(format!("Unable to send initial app: {}", e)))?;
|
||||
|
||||
let mut terminal = ratatui::init();
|
||||
loop {
|
||||
draw(&app_state, &mut terminal)?;
|
||||
handle_events(&mut app_state).await?;
|
||||
@ -132,6 +142,13 @@ pub async fn ui(args: &UiArgs, db_name: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start_backend(db_name: &str, app_recv: std::sync::mpsc::Receiver<UiCommandDispatchActions>, executor_dispatch: tokio::sync::mpsc::UnboundedSender<BackendEvent>) {
|
||||
let db_name = db_name.to_string();
|
||||
std::thread::spawn(move || {
|
||||
backend::start(db_name, app_recv, executor_dispatch);
|
||||
});
|
||||
}
|
||||
|
||||
fn start_event_loop(
|
||||
mut executor_recv: UnboundedReceiver<BackendEvent>,
|
||||
) -> UnboundedReceiver<Event> {
|
||||
|
||||
@ -82,6 +82,18 @@ pub fn handle_action(
|
||||
}
|
||||
}
|
||||
}
|
||||
UiAction::SyncChannel => {
|
||||
let channel = get_highlighted_channel(app_state);
|
||||
if let Some(channel) = channel {
|
||||
let sync_channel_args = args::AddChannelArgs {
|
||||
link: channel.feed_link.clone(),
|
||||
};
|
||||
app_state
|
||||
.dispatcher
|
||||
.send(UiCommandDispatchActions::AddChannel(sync_channel_args))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
66
src/ui/backend.rs
Normal file
66
src/ui/backend.rs
Normal file
@ -0,0 +1,66 @@
|
||||
use std::sync::mpsc::Receiver;
|
||||
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use crate::{commands::TrsEnv, ui::BackendEvent};
|
||||
|
||||
use super::UiCommandDispatchActions;
|
||||
|
||||
// This one will have to run on the same thread as this manages the sqlite connection
|
||||
pub fn start(
|
||||
db_name: String,
|
||||
cmd_recv: Receiver<UiCommandDispatchActions>,
|
||||
backend_dispatch: UnboundedSender<BackendEvent>,
|
||||
) -> () {
|
||||
let ctx = TrsEnv::new(db_name.as_str()).unwrap();
|
||||
loop {
|
||||
let action = cmd_recv.recv();
|
||||
let Ok(action) = action else {
|
||||
break;
|
||||
};
|
||||
|
||||
match action {
|
||||
UiCommandDispatchActions::AddChannel(args) => {
|
||||
if let Ok(_) = crate::commands::add_channel(&ctx, &args) {
|
||||
send_new_state_default(&ctx, &backend_dispatch);
|
||||
};
|
||||
}
|
||||
UiCommandDispatchActions::RemoveChannel(args) => {
|
||||
if let Ok(_) = crate::commands::remove_channel(&ctx, &args) {
|
||||
send_new_state_default(&ctx, &backend_dispatch);
|
||||
}
|
||||
}
|
||||
UiCommandDispatchActions::MarkArticleRead(args) => {
|
||||
if let Ok(_) = crate::commands::mark_read(&ctx, &args) {
|
||||
send_new_state_default(&ctx, &backend_dispatch);
|
||||
}
|
||||
}
|
||||
UiCommandDispatchActions::ListChannels(args) => {
|
||||
send_new_state(&ctx, args, &backend_dispatch);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_new_state_default(
|
||||
ctx: &crate::commands::TrsEnv,
|
||||
dispatcher: &UnboundedSender<BackendEvent>,
|
||||
) {
|
||||
send_new_state(
|
||||
ctx,
|
||||
crate::args::ListChannelArgs { limit: None },
|
||||
dispatcher,
|
||||
);
|
||||
}
|
||||
|
||||
fn send_new_state(
|
||||
ctx: &crate::commands::TrsEnv,
|
||||
args: crate::args::ListChannelArgs,
|
||||
dispatcher: &UnboundedSender<BackendEvent>,
|
||||
) {
|
||||
if let Ok(channels) = crate::commands::list_channels(ctx, &args) {
|
||||
dispatcher
|
||||
.send(BackendEvent::ReloadState(channels))
|
||||
.unwrap_or_default();
|
||||
}
|
||||
}
|
||||
@ -57,11 +57,12 @@ pub fn parse_ui_action(raw_event: Event) -> UiAction {
|
||||
KeyCode::Char('h') => UiAction::FocusPaneLeft,
|
||||
KeyCode::Char('k') => UiAction::FocusEntryUp,
|
||||
KeyCode::Char('j') => UiAction::FocusEntryDown,
|
||||
KeyCode::Char('d') => UiAction::ToggleDebug,
|
||||
KeyCode::Char('n') => UiAction::ToggleDebug,
|
||||
KeyCode::Char('q') => UiAction::Exit,
|
||||
KeyCode::Char('a') => UiAction::ShowAddChannelUi,
|
||||
KeyCode::Char('r') => UiAction::RemoveChannel,
|
||||
KeyCode::Char('u') => UiAction::ToggleReadStatus,
|
||||
KeyCode::Char('d') => UiAction::RemoveChannel,
|
||||
KeyCode::Char('r') => UiAction::ToggleReadStatus,
|
||||
KeyCode::Char('s') => UiAction::SyncChannel,
|
||||
KeyCode::Enter => UiAction::OpenArticle,
|
||||
_ => UiAction::None,
|
||||
};
|
||||
@ -97,9 +98,11 @@ impl Widget for ControlsWidget {
|
||||
description!(" to switch between channels and articles, "),
|
||||
control!("a"),
|
||||
description!(" add a new RSS channel, "),
|
||||
control!("s"),
|
||||
description!(" sync channel, "),
|
||||
control!("d"),
|
||||
description!(" delete an RSS channel, "),
|
||||
control!("r"),
|
||||
description!(" remove an RSS channel, "),
|
||||
control!("u"),
|
||||
description!(" toggle read state of article, "),
|
||||
control!("q"),
|
||||
description!(" to exit"),
|
||||
|
||||
@ -1,72 +0,0 @@
|
||||
use std::sync::mpsc::Receiver;
|
||||
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use crate::{commands::TrsEnv, ui::BackendEvent};
|
||||
|
||||
use super::UiCommandDispatchActions;
|
||||
|
||||
pub struct UiCommandExecutor {
|
||||
pub app_recv: Receiver<UiCommandDispatchActions>,
|
||||
pub executor_dispatch: UnboundedSender<BackendEvent>,
|
||||
}
|
||||
|
||||
impl UiCommandExecutor {
|
||||
pub fn new(
|
||||
app_recv: Receiver<UiCommandDispatchActions>,
|
||||
executor_dispatch: UnboundedSender<BackendEvent>,
|
||||
) -> Self {
|
||||
UiCommandExecutor {
|
||||
app_recv,
|
||||
executor_dispatch,
|
||||
}
|
||||
}
|
||||
|
||||
// This one will have to run on the same thread as this manages the sqlite connection
|
||||
pub fn run(&mut self, db_name: String) -> () {
|
||||
let ctx = TrsEnv::new(db_name.as_str()).unwrap();
|
||||
loop {
|
||||
let action = self.app_recv.recv();
|
||||
let Ok(action) = action else {
|
||||
break;
|
||||
};
|
||||
|
||||
match action {
|
||||
UiCommandDispatchActions::AddChannel(args) => {
|
||||
if let Ok(_) = crate::commands::add_channel(&ctx, &args) {
|
||||
self.send_new_state_default(&ctx);
|
||||
};
|
||||
}
|
||||
UiCommandDispatchActions::RemoveChannel(args) => {
|
||||
if let Ok(_) = crate::commands::remove_channel(&ctx, &args) {
|
||||
self.send_new_state_default(&ctx);
|
||||
}
|
||||
}
|
||||
UiCommandDispatchActions::MarkArticleRead(args) => {
|
||||
if let Ok(_) = crate::commands::mark_read(&ctx, &args) {
|
||||
self.send_new_state_default(&ctx);
|
||||
}
|
||||
}
|
||||
UiCommandDispatchActions::ListChannels(args) => {
|
||||
self.send_new_state(&ctx, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_new_state_default(&mut self, ctx: &crate::commands::TrsEnv) {
|
||||
self.send_new_state(ctx, crate::args::ListChannelArgs { limit: None });
|
||||
}
|
||||
|
||||
fn send_new_state(
|
||||
&mut self,
|
||||
ctx: &crate::commands::TrsEnv,
|
||||
args: crate::args::ListChannelArgs,
|
||||
) {
|
||||
if let Ok(channels) = crate::commands::list_channels(ctx, &args) {
|
||||
self.executor_dispatch
|
||||
.send(BackendEvent::ReloadState(channels))
|
||||
.unwrap_or_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user