use crate::client::AnyListClient;
use crate::error::{AnyListError, Result};
use crate::lists::ListItem;
use crate::protobuf::anylist::{
pb_operation_metadata::OperationClass, PbListItem, PbListOperation, PbListOperationList,
PbOperationMetadata,
};
use crate::utils::{current_timestamp, generate_id};
use prost::Message;
impl AnyListClient {
pub async fn add_item(&self, list_id: &str, name: &str) -> Result<ListItem> {
self.add_item_with_details(list_id, name, None, None, None)
.await
}
pub async fn add_item_with_details(
&self,
list_id: &str,
name: &str,
quantity: Option<&str>,
details: Option<&str>,
category: Option<&str>,
) -> Result<ListItem> {
let item_id = generate_id();
let operation_id = generate_id();
let new_item = PbListItem {
identifier: item_id.clone(),
server_mod_time: Some(current_timestamp()),
list_id: Some(list_id.to_string()),
name: Some(name.to_string()),
quantity: quantity.map(|q| q.to_string()),
details: details.map(|d| d.to_string()),
checked: Some(false),
recipe_id: None,
raw_ingredient: None,
price_matchup_tag: None,
price_id: None,
category: category.map(|c| c.to_string()),
user_id: Some(self.user_id()),
category_match_id: None,
photo_ids: vec![],
event_id: None,
store_ids: vec![],
prices: vec![],
category_assignments: vec![],
manual_sort_index: Some(0),
product_upc: None,
};
let operation = PbListOperation {
metadata: Some(PbOperationMetadata {
operation_id: Some(operation_id),
handler_id: Some("add-shopping-list-item".to_string()),
user_id: Some(self.user_id()),
operation_class: Some(OperationClass::Undefined as i32),
}),
list_id: Some(list_id.to_string()),
list_item_id: Some(item_id.clone()),
list_item: Some(new_item),
..Default::default()
};
let operation_list = PbListOperationList {
operations: vec![operation],
};
let mut buf = Vec::new();
operation_list.encode(&mut buf).map_err(|e| {
AnyListError::ProtobufError(format!("Failed to encode operation: {}", e))
})?;
self.post("data/shopping-lists/update", buf).await?;
Ok(ListItem {
id: item_id,
list_id: list_id.to_string(),
name: name.to_string(),
details: details.unwrap_or("").to_string(),
is_checked: false,
quantity: quantity.map(|q| q.to_string()),
category: category.map(|c| c.to_string()),
user_id: Some(self.user_id()),
product_upc: None,
})
}
pub async fn update_item(
&self,
list_id: &str,
item_id: &str,
name: &str,
quantity: Option<&str>,
details: Option<&str>,
category: Option<&str>,
) -> Result<()> {
let operation_id = generate_id();
let updated_item = PbListItem {
identifier: item_id.to_string(),
server_mod_time: Some(current_timestamp()),
list_id: Some(list_id.to_string()),
name: Some(name.to_string()),
quantity: quantity.map(|q| q.to_string()),
details: details.map(|d| d.to_string()),
checked: Some(false),
recipe_id: None,
raw_ingredient: None,
price_matchup_tag: None,
price_id: None,
category: category.map(|c| c.to_string()),
user_id: Some(self.user_id()),
category_match_id: None,
photo_ids: vec![],
event_id: None,
store_ids: vec![],
prices: vec![],
category_assignments: vec![],
manual_sort_index: Some(0),
product_upc: None,
};
let operation = PbListOperation {
metadata: Some(PbOperationMetadata {
operation_id: Some(operation_id),
handler_id: Some("update-list-item".to_string()),
user_id: Some(self.user_id()),
operation_class: Some(OperationClass::Undefined as i32),
}),
list_id: Some(list_id.to_string()),
list_item_id: Some(item_id.to_string()),
list_item: Some(updated_item),
..Default::default()
};
let operation_list = PbListOperationList {
operations: vec![operation],
};
let mut buf = Vec::new();
operation_list.encode(&mut buf).map_err(|e| {
AnyListError::ProtobufError(format!("Failed to encode operation: {}", e))
})?;
self.post("data/shopping-lists/update", buf).await?;
Ok(())
}
pub async fn delete_item(&self, list_id: &str, item_id: &str) -> Result<()> {
self.bulk_delete_items(list_id, &[item_id]).await
}
pub async fn bulk_delete_items(&self, list_id: &str, item_ids: &[&str]) -> Result<()> {
if item_ids.is_empty() {
return Ok(());
}
let list = self.get_list_by_id(list_id).await?;
let items_to_remove: Vec<crate::operations::ItemToRemove> = item_ids
.iter()
.filter_map(|&item_id| {
list.items().iter().find(|i| i.id() == item_id).map(|item| {
crate::operations::ItemToRemove {
item_id: item.id().to_string(),
list_id: item.list_id().to_string(),
name: item.name().to_string(),
category: item.category().map(|s| s.to_string()),
user_id: self.user_id(),
category_match_id: item.category().map(|s| s.to_string()),
category_assignment: None,
}
})
})
.collect();
if items_to_remove.is_empty() {
return Err(AnyListError::NotFound(
"No matching items found".to_string(),
));
}
let operation_id = generate_id();
let params = crate::operations::BulkRemoveItemsParams {
operation_id,
user_id: self.user_id(),
list_id: list_id.to_string(),
items: items_to_remove,
};
let operation_list = crate::operations::build_bulk_remove_items_operation(params);
let mut buf = Vec::new();
operation_list.encode(&mut buf).map_err(|e| {
AnyListError::ProtobufError(format!("Failed to encode operation: {}", e))
})?;
self.post("data/shopping-lists/update", buf).await?;
Ok(())
}
pub async fn cross_off_item(&self, list_id: &str, item_id: &str) -> Result<()> {
self.set_item_checked(list_id, item_id, true).await
}
pub async fn uncheck_item(&self, list_id: &str, item_id: &str) -> Result<()> {
self.set_item_checked(list_id, item_id, false).await
}
async fn set_item_checked(&self, list_id: &str, item_id: &str, checked: bool) -> Result<()> {
let operation_id = generate_id();
let operation = PbListOperation {
metadata: Some(PbOperationMetadata {
operation_id: Some(operation_id),
handler_id: Some("set-list-item-checked".to_string()),
user_id: Some(self.user_id()),
operation_class: Some(OperationClass::Undefined as i32),
}),
list_id: Some(list_id.to_string()),
list_item_id: Some(item_id.to_string()),
updated_value: Some(if checked { "y" } else { "n" }.to_string()),
..Default::default()
};
let operation_list = PbListOperationList {
operations: vec![operation],
};
let mut buf = Vec::new();
operation_list.encode(&mut buf).map_err(|e| {
AnyListError::ProtobufError(format!("Failed to encode operation: {}", e))
})?;
self.post("data/shopping-lists/update", buf).await?;
Ok(())
}
pub async fn delete_all_crossed_off_items(&self, list_id: &str) -> Result<()> {
let list = self.get_list_by_id(list_id).await?;
let checked_items: Vec<&ListItem> =
list.items().iter().filter(|i| i.is_checked()).collect();
if checked_items.is_empty() {
return Ok(());
}
let operation_id = generate_id();
let items_to_remove: Vec<crate::operations::ItemToRemove> = checked_items
.iter()
.map(|item| crate::operations::ItemToRemove {
item_id: item.id().to_string(),
list_id: item.list_id().to_string(),
name: item.name().to_string(),
category: item.category().map(|s| s.to_string()),
user_id: self.user_id(),
category_match_id: item.category().map(|s| s.to_string()), category_assignment: None, })
.collect();
let params = crate::operations::BulkRemoveItemsParams {
operation_id,
user_id: self.user_id(),
list_id: list_id.to_string(),
items: items_to_remove,
};
let operation_list = crate::operations::build_bulk_remove_items_operation(params);
let mut buf = Vec::new();
operation_list.encode(&mut buf).map_err(|e| {
AnyListError::ProtobufError(format!("Failed to encode operation: {}", e))
})?;
self.post("data/shopping-lists/update", buf).await?;
Ok(())
}
}