use anyhow::Result;
use crate::config::get_drafts_dir;
use crate::draft::Draft;
#[derive(Debug, Clone, PartialEq)]
pub enum Tab {
Drafts,
Posts,
Media,
}
#[derive(Debug, Clone)]
pub struct DraftItem {
pub id: String,
pub title: String,
pub post_type: String,
pub categories: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct PostItem {
pub url: String,
pub content: String,
pub name: Option<String>,
pub published: String,
pub categories: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct MediaItem {
pub url: String,
pub name: Option<String>,
pub uploaded: String,
}
pub enum ConfirmationAction {
DeleteDraft(String),
PublishDraft(String),
BackdateDraft(String),
None,
}
pub struct App {
pub current_tab: Tab,
pub drafts: Vec<DraftItem>,
pub posts: Vec<PostItem>,
pub media: Vec<MediaItem>,
pub selected_draft: usize,
pub selected_post: usize,
pub selected_media: usize,
pub preview_content: Option<String>,
pub error_message: Option<String>,
pub status_message: Option<String>,
pub confirmation_action: ConfirmationAction,
pub quit_requested: bool,
pub date_input: String,
}
impl App {
pub async fn new() -> Result<Self> {
let mut app = App {
current_tab: Tab::Drafts,
drafts: Vec::new(),
posts: Vec::new(),
media: Vec::new(),
selected_draft: 0,
selected_post: 0,
selected_media: 0,
preview_content: None,
error_message: None,
status_message: None,
confirmation_action: ConfirmationAction::None,
quit_requested: false,
date_input: String::new(),
};
app.load_drafts()?;
app.load_posts().await?;
app.load_media().await?;
app.update_preview();
Ok(app)
}
fn load_drafts(&mut self) -> Result<()> {
self.drafts.clear();
let draft_ids = Draft::list_all()?;
for id in draft_ids {
if let Ok(draft) = Draft::load(&id) {
let title = draft
.metadata
.name
.unwrap_or_else(|| "[untitled]".to_string());
self.drafts.push(DraftItem {
id: id.clone(),
title,
post_type: draft.metadata.post_type.clone(),
categories: draft.metadata.category.clone(),
});
}
}
Ok(())
}
async fn load_posts(&mut self) -> Result<()> {
self.posts.clear();
match crate::operations::fetch_posts(20, 0).await {
Ok(posts) => {
for post in posts {
self.posts.push(PostItem {
url: post.url,
content: post.content,
name: post.name,
published: post.published,
categories: post.categories,
});
}
Ok(())
}
Err(e) => {
self.error_message = Some(format!("Failed to load posts: {}", e));
Ok(())
}
}
}
async fn load_media(&mut self) -> Result<()> {
self.media.clear();
match crate::operations::fetch_media(20, 0).await {
Ok(media_items) => {
for media in media_items {
self.media.push(MediaItem {
url: media.url,
name: media.name,
uploaded: media.uploaded,
});
}
Ok(())
}
Err(e) => {
self.error_message = Some(format!("Failed to load media: {}", e));
Ok(())
}
}
}
pub async fn refresh(&mut self) -> Result<()> {
self.status_message = Some("Refreshing...".to_string());
match self.current_tab {
Tab::Drafts => {
self.load_drafts()?;
self.update_preview();
self.status_message = Some("Drafts refreshed".to_string());
}
Tab::Posts => {
self.load_posts().await?;
self.update_preview();
self.status_message = Some("Posts refreshed".to_string());
}
Tab::Media => {
self.load_media().await?;
self.update_preview();
self.status_message = Some("Media refreshed".to_string());
}
}
Ok(())
}
pub fn next_tab(&mut self) {
self.current_tab = match self.current_tab {
Tab::Drafts => Tab::Posts,
Tab::Posts => Tab::Media,
Tab::Media => Tab::Drafts,
};
self.update_preview();
}
pub fn previous_tab(&mut self) {
self.current_tab = match self.current_tab {
Tab::Drafts => Tab::Media,
Tab::Posts => Tab::Drafts,
Tab::Media => Tab::Posts,
};
self.update_preview();
}
pub fn next_item(&mut self) {
match self.current_tab {
Tab::Drafts => {
if !self.drafts.is_empty() {
self.selected_draft = (self.selected_draft + 1) % self.drafts.len();
self.update_preview();
}
}
Tab::Posts => {
if !self.posts.is_empty() {
self.selected_post = (self.selected_post + 1) % self.posts.len();
self.update_preview();
}
}
Tab::Media => {
if !self.media.is_empty() {
self.selected_media = (self.selected_media + 1) % self.media.len();
self.update_preview();
}
}
}
}
pub fn previous_item(&mut self) {
match self.current_tab {
Tab::Drafts => {
if !self.drafts.is_empty() {
self.selected_draft = if self.selected_draft == 0 {
self.drafts.len() - 1
} else {
self.selected_draft - 1
};
self.update_preview();
}
}
Tab::Posts => {
if !self.posts.is_empty() {
self.selected_post = if self.selected_post == 0 {
self.posts.len() - 1
} else {
self.selected_post - 1
};
self.update_preview();
}
}
Tab::Media => {
if !self.media.is_empty() {
self.selected_media = if self.selected_media == 0 {
self.media.len() - 1
} else {
self.selected_media - 1
};
self.update_preview();
}
}
}
}
fn update_preview(&mut self) {
self.preview_content = None;
if self.current_tab == Tab::Drafts && !self.drafts.is_empty() {
if let Some(draft_item) = self.drafts.get(self.selected_draft) {
if let Ok(draft) = Draft::load(&draft_item.id) {
if let Ok(content) = draft.to_string() {
self.preview_content = Some(content);
}
}
}
} else if self.current_tab == Tab::Posts && !self.posts.is_empty() {
if let Some(post_item) = self.posts.get(self.selected_post) {
let mut preview = String::new();
if let Some(ref name) = post_item.name {
preview.push_str(&format!("Title: {}\n\n", name));
}
preview.push_str(&format!("URL: {}\n", post_item.url));
preview.push_str(&format!("Published: {}\n", post_item.published));
if !post_item.categories.is_empty() {
preview.push_str(&format!(
"Categories: {}\n",
post_item.categories.join(", ")
));
}
preview.push_str("\n---\n\n");
preview.push_str(&post_item.content);
self.preview_content = Some(preview);
}
} else if self.current_tab == Tab::Media && !self.media.is_empty() {
if let Some(media_item) = self.media.get(self.selected_media) {
let mut preview = String::new();
preview.push_str(&format!("URL: {}\n", media_item.url));
preview.push_str(&format!("Uploaded: {}\n", media_item.uploaded));
if let Some(ref name) = media_item.name {
preview.push_str(&format!("\nName/Alt Text:\n{}\n", name));
}
self.preview_content = Some(preview);
}
}
}
pub async fn select_item(&mut self) -> Result<()> {
self.status_message = Some("Item selected".to_string());
Ok(())
}
pub async fn publish_draft(&mut self) -> Result<()> {
if self.current_tab != Tab::Drafts || self.drafts.is_empty() {
return Ok(());
}
if let Some(draft_item) = self.drafts.get(self.selected_draft) {
self.confirmation_action = ConfirmationAction::PublishDraft(draft_item.id.clone());
self.status_message = Some("Publish draft? (y/n)".to_string());
}
Ok(())
}
pub fn edit_item(&mut self) -> Result<Option<String>> {
match self.current_tab {
Tab::Drafts => {
if let Some(draft_item) = self.drafts.get(self.selected_draft) {
Ok(Some(draft_item.id.clone()))
} else {
Ok(None)
}
}
_ => {
self.error_message = Some("Edit not available for this view".to_string());
Ok(None)
}
}
}
pub async fn delete_item(&mut self) -> Result<()> {
if self.current_tab != Tab::Drafts || self.drafts.is_empty() {
return Ok(());
}
if let Some(draft_item) = self.drafts.get(self.selected_draft) {
self.confirmation_action = ConfirmationAction::DeleteDraft(draft_item.id.clone());
self.status_message = Some("Delete draft? (y/n)".to_string());
}
Ok(())
}
pub async fn backdate_draft(&mut self) -> Result<()> {
if self.current_tab != Tab::Drafts || self.drafts.is_empty() {
return Ok(());
}
if let Some(draft_item) = self.drafts.get(self.selected_draft) {
self.confirmation_action = ConfirmationAction::BackdateDraft(draft_item.id.clone());
self.date_input.clear();
self.status_message =
Some("Enter date (ISO 8601, e.g., 2024-01-15T10:30:00Z):".to_string());
}
Ok(())
}
pub fn new_draft(&mut self) -> Result<String> {
Ok(crate::draft::generate_draft_id())
}
pub fn reload_and_select_draft(&mut self, draft_id: &str) -> Result<()> {
self.load_drafts()?;
if let Some(index) = self.drafts.iter().position(|d| d.id == draft_id) {
self.selected_draft = index;
self.update_preview();
self.status_message = Some(format!("Draft created: {}", draft_id));
} else {
self.error_message = Some("Draft created but not found in list".to_string());
}
Ok(())
}
pub fn confirm_quit(&mut self) -> bool {
if !self.quit_requested {
self.quit_requested = true;
self.status_message = Some("Press 'q' again to quit".to_string());
false
} else {
true
}
}
pub fn awaiting_confirmation(&self) -> bool {
!matches!(self.confirmation_action, ConfirmationAction::None)
}
pub async fn confirm_action(&mut self) -> Result<()> {
match &self.confirmation_action {
ConfirmationAction::PublishDraft(draft_id) => {
self.status_message = Some("Publishing...".to_string());
let draft_path = get_drafts_dir()?.join(format!("{}.md", draft_id));
let draft_path_str = draft_path.to_string_lossy().to_string();
match crate::publish::cmd_publish(&draft_path_str, None).await {
Ok(_uploads) => {
self.status_message = Some("Draft published successfully!".to_string());
self.load_drafts()?;
if self.selected_draft >= self.drafts.len() && self.selected_draft > 0 {
self.selected_draft -= 1;
}
self.update_preview();
}
Err(e) => {
self.error_message = Some(format!("Failed to publish: {}", e));
}
}
}
ConfirmationAction::BackdateDraft(draft_id) => {
use chrono::DateTime;
match DateTime::parse_from_rfc3339(&self.date_input) {
Ok(parsed_date) => {
self.status_message = Some("Publishing with backdate...".to_string());
let parsed_date_utc = parsed_date.with_timezone(&chrono::Utc);
let draft_path = get_drafts_dir()?.join(format!("{}.md", draft_id));
let draft_path_str = draft_path.to_string_lossy().to_string();
match crate::publish::cmd_publish(&draft_path_str, Some(parsed_date_utc))
.await
{
Ok(_uploads) => {
self.status_message =
Some("Draft published with backdate successfully!".to_string());
self.load_drafts()?;
if self.selected_draft >= self.drafts.len()
&& self.selected_draft > 0
{
self.selected_draft -= 1;
}
self.update_preview();
}
Err(e) => {
self.error_message = Some(format!("Failed to publish: {}", e));
}
}
}
Err(_) => {
self.error_message = Some(
"Invalid date format. Use ISO 8601 (e.g., 2024-01-15T10:30:00Z)"
.to_string(),
);
}
}
}
ConfirmationAction::DeleteDraft(draft_id) => {
let draft_path = get_drafts_dir()?.join(format!("{}.md", draft_id));
match std::fs::remove_file(&draft_path) {
Ok(_) => {
self.status_message = Some("Draft deleted".to_string());
self.load_drafts()?;
if self.selected_draft >= self.drafts.len() && self.selected_draft > 0 {
self.selected_draft -= 1;
}
self.update_preview();
}
Err(e) => {
self.error_message = Some(format!("Failed to delete: {}", e));
}
}
}
ConfirmationAction::None => {}
}
self.confirmation_action = ConfirmationAction::None;
self.date_input.clear();
Ok(())
}
pub fn cancel_action(&mut self) {
self.confirmation_action = ConfirmationAction::None;
self.date_input.clear();
self.status_message = Some("Action cancelled".to_string());
}
pub fn clear_error(&mut self) {
self.error_message = None;
self.status_message = None;
self.quit_requested = false;
}
pub fn awaiting_date_input(&self) -> bool {
matches!(
self.confirmation_action,
ConfirmationAction::BackdateDraft(_)
)
}
pub fn add_date_char(&mut self, c: char) {
self.date_input.push(c);
}
pub fn delete_date_char(&mut self) {
self.date_input.pop();
}
}