非event卡推送,以及手动查看
This commit is contained in:
+55
-1
@@ -1,6 +1,7 @@
|
|||||||
use crate::CONFIG;
|
use crate::CONFIG;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
use std::fmt::Debug;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -41,6 +42,8 @@ pub struct State {
|
|||||||
pub git_hash: Option<String>,
|
pub git_hash: Option<String>,
|
||||||
pub master_hash: Option<String>,
|
pub master_hash: Option<String>,
|
||||||
pub event_id: Option<u64>,
|
pub event_id: Option<u64>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub unlinked_cards: Vec<u64>,
|
||||||
}
|
}
|
||||||
impl State {
|
impl State {
|
||||||
fn load(x: &str) -> Self {
|
fn load(x: &str) -> Self {
|
||||||
@@ -52,6 +55,7 @@ impl State {
|
|||||||
git_hash: None,
|
git_hash: None,
|
||||||
master_hash: None,
|
master_hash: None,
|
||||||
event_id: None,
|
event_id: None,
|
||||||
|
unlinked_cards: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +66,7 @@ impl State {
|
|||||||
self.git_hash = i.git_hash;
|
self.git_hash = i.git_hash;
|
||||||
self.master_hash = i.master_hash;
|
self.master_hash = i.master_hash;
|
||||||
self.event_id = i.event_id;
|
self.event_id = i.event_id;
|
||||||
|
self.unlinked_cards = i.unlinked_cards;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -199,6 +204,21 @@ impl Card {
|
|||||||
_ => "".to_string(),
|
_ => "".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn build_card_details(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"#{id} {attr}{rarity} {skill} [{prefix}]{name}",
|
||||||
|
id = self.id,
|
||||||
|
attr = self.attr_short_text(),
|
||||||
|
rarity = self.rarity_short_text(),
|
||||||
|
skill = self
|
||||||
|
.skill_id_short()
|
||||||
|
.map(|s| format!("({})", s))
|
||||||
|
.unwrap_or_default(),
|
||||||
|
prefix = self.prefix,
|
||||||
|
name = self.character_name()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -217,7 +237,41 @@ pub struct FetchResultCard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct FetchResult {
|
pub struct FetchResultEvent {
|
||||||
pub event_id: u64,
|
pub event_id: u64,
|
||||||
pub cards: Vec<FetchResultCard>,
|
pub cards: Vec<FetchResultCard>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct FetchResultUnlinkedCards {
|
||||||
|
pub cards: Vec<FetchResultCard>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[serde(tag = "type", content = "data")]
|
||||||
|
pub enum FetchResult {
|
||||||
|
Event(FetchResultEvent),
|
||||||
|
UnlinkedCard(FetchResultUnlinkedCards),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[serde(tag = "type", content = "data")]
|
||||||
|
pub enum ApiResult<T>
|
||||||
|
where
|
||||||
|
T: Debug + Clone + Serialize,
|
||||||
|
{
|
||||||
|
Ok(T),
|
||||||
|
Err(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ApiResult<T>
|
||||||
|
where
|
||||||
|
T: Debug + Clone + Serialize,
|
||||||
|
{
|
||||||
|
pub fn from(result: anyhow::Result<T>) -> Self {
|
||||||
|
match result {
|
||||||
|
Ok(v) => Self::Ok(v),
|
||||||
|
Err(e) => Self::Err(e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+71
-4
@@ -1,9 +1,19 @@
|
|||||||
use crate::{CONFIG, OPT_CONFIG};
|
use crate::data::{Card, FetchResultCard};
|
||||||
use crate::github::get_master_data;
|
use crate::github::get_master_data;
|
||||||
|
use crate::{CONFIG, OPT_CONFIG, ending_point};
|
||||||
|
use anyhow::anyhow;
|
||||||
use assets_updater::core::asset_execution::AssetExecutionContext;
|
use assets_updater::core::asset_execution::AssetExecutionContext;
|
||||||
use assets_updater::core::config::{AppConfig, CryptoConfig, ExecutionConfig, RegionConfig, RegionProviderConfig, RegionRuntimeConfig, ToolsConfig};
|
use assets_updater::core::config::{
|
||||||
|
AppConfig, CryptoConfig, ExecutionConfig, RegionConfig, RegionProviderConfig,
|
||||||
|
RegionRuntimeConfig, ToolsConfig,
|
||||||
|
};
|
||||||
use assets_updater::core::errors::AssetExecutionError;
|
use assets_updater::core::errors::AssetExecutionError;
|
||||||
|
use assets_updater::core::export_pipeline::find_files;
|
||||||
use common::updater::SyncContext;
|
use common::updater::SyncContext;
|
||||||
|
use futures::StreamExt;
|
||||||
|
use futures::stream::FuturesUnordered;
|
||||||
|
use log::debug;
|
||||||
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
pub struct Downloader {
|
pub struct Downloader {
|
||||||
@@ -21,8 +31,8 @@ impl Downloader {
|
|||||||
}
|
}
|
||||||
let app_config = AppConfig {
|
let app_config = AppConfig {
|
||||||
execution,
|
execution,
|
||||||
tools: ToolsConfig{
|
tools: ToolsConfig {
|
||||||
asset_studio_cli_path : Some(CONFIG.get().unwrap().as_path.clone()),
|
asset_studio_cli_path: Some(CONFIG.get().unwrap().as_path.clone()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -58,3 +68,60 @@ impl Downloader {
|
|||||||
self.exec.download(bundle_path, &self.app_config).await
|
self.exec.download(bundle_path, &self.app_config).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn download_cards(cards: &Vec<Card>) -> anyhow::Result<Vec<FetchResultCard>> {
|
||||||
|
if cards.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
let downloader = Downloader::new().await?;
|
||||||
|
ending_point!(ok);
|
||||||
|
let cache_dir = CONFIG.get().unwrap().cache_dir();
|
||||||
|
let mut futures = FuturesUnordered::new();
|
||||||
|
for card in cards {
|
||||||
|
futures.push(async {
|
||||||
|
ending_point!(ok);
|
||||||
|
debug!(
|
||||||
|
"Pulling card {} from bundle {}",
|
||||||
|
card.id, card.assetbundle_name
|
||||||
|
);
|
||||||
|
let dir = downloader
|
||||||
|
.pull(&format!("character/member/{}", card.assetbundle_name))
|
||||||
|
.await?;
|
||||||
|
ending_point!(ok_with {
|
||||||
|
fs::remove_dir_all(dir.0)?;
|
||||||
|
});
|
||||||
|
let files = find_files(&dir.0)?;
|
||||||
|
let mut local_images = Vec::new();
|
||||||
|
for file in files {
|
||||||
|
ending_point!(ok_with {
|
||||||
|
fs::remove_dir_all(dir.0)?;
|
||||||
|
});
|
||||||
|
if file.is_file() && file.extension().map(|ext| ext == "png").unwrap_or(false) {
|
||||||
|
let new_filename = format!(
|
||||||
|
"{}_{}",
|
||||||
|
card.assetbundle_name,
|
||||||
|
file.file_name().unwrap().to_str().unwrap()
|
||||||
|
);
|
||||||
|
let new_file = cache_dir.join(new_filename);
|
||||||
|
fs::copy(&file, &new_file)?;
|
||||||
|
fs::remove_file(&file)?;
|
||||||
|
local_images.push(new_file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fs::remove_dir_all(dir.0)?;
|
||||||
|
Ok::<_, anyhow::Error>((card.clone(), local_images))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let mut images = Vec::new();
|
||||||
|
while let Some(res) = futures.next().await {
|
||||||
|
images.push(res?);
|
||||||
|
}
|
||||||
|
images.sort_by(|a, b| a.0.id.cmp(&b.0.id)); // 小在前
|
||||||
|
Ok(images
|
||||||
|
.into_iter()
|
||||||
|
.map(|(card, path)| FetchResultCard {
|
||||||
|
text: card.build_card_details(),
|
||||||
|
path,
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|||||||
+21
-3
@@ -9,8 +9,8 @@ mod github;
|
|||||||
mod main_loop;
|
mod main_loop;
|
||||||
mod store;
|
mod store;
|
||||||
|
|
||||||
use crate::data::{Config, FetchResult, OptConfig, State};
|
use crate::data::{ApiResult, Config, FetchResult, OptConfig, State};
|
||||||
use crate::main_loop::main;
|
use crate::main_loop::{main, refetch_event_cards};
|
||||||
use crate::store::Store;
|
use crate::store::Store;
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
use fern::colors::{Color, ColoredLevelConfig};
|
use fern::colors::{Color, ColoredLevelConfig};
|
||||||
@@ -25,12 +25,13 @@ use tokio::sync::Notify;
|
|||||||
|
|
||||||
pub static CONFIG: OnceLock<Config> = OnceLock::new();
|
pub static CONFIG: OnceLock<Config> = OnceLock::new();
|
||||||
pub static OPT_CONFIG: RwLock<OptConfig> = RwLock::new(OptConfig::new());
|
pub static OPT_CONFIG: RwLock<OptConfig> = RwLock::new(OptConfig::new());
|
||||||
pub static PROVIDE: RwLock<Option<FetchResult>> = RwLock::new(None);
|
pub static PROVIDE: RwLock<Option<Vec<FetchResult>>> = RwLock::new(None);
|
||||||
pub static STORE: RwLock<Store> = RwLock::new(Store::new());
|
pub static STORE: RwLock<Store> = RwLock::new(Store::new());
|
||||||
pub static STATE: RwLock<State> = RwLock::new(State::new());
|
pub static STATE: RwLock<State> = RwLock::new(State::new());
|
||||||
pub static THREAD: RwLock<Option<thread::JoinHandle<()>>> = RwLock::new(None);
|
pub static THREAD: RwLock<Option<thread::JoinHandle<()>>> = RwLock::new(None);
|
||||||
pub static KILLING: AtomicBool = AtomicBool::new(false);
|
pub static KILLING: AtomicBool = AtomicBool::new(false);
|
||||||
pub static NOTIFY: Notify = Notify::const_new();
|
pub static NOTIFY: Notify = Notify::const_new();
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! ending_point {
|
macro_rules! ending_point {
|
||||||
(ok) => {
|
(ok) => {
|
||||||
@@ -194,6 +195,23 @@ pub unsafe extern "C" fn fetch_data() -> *mut c_char {
|
|||||||
c_str.into_raw()
|
c_str.into_raw()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn refetch_event() -> *mut c_char {
|
||||||
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.expect("Failed to create Tokio runtime");
|
||||||
|
|
||||||
|
let result = rt.block_on(refetch_event_cards());
|
||||||
|
let result = ApiResult::from(result);
|
||||||
|
|
||||||
|
let json_string = serde_json::to_string(&result).unwrap();
|
||||||
|
|
||||||
|
let c_str = CString::new(json_string).unwrap();
|
||||||
|
c_str.into_raw()
|
||||||
|
}
|
||||||
|
|
||||||
/// # Safety
|
/// # Safety
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
pub unsafe extern "C" fn free_string(s: *mut c_char) {
|
pub unsafe extern "C" fn free_string(s: *mut c_char) {
|
||||||
|
|||||||
+74
-71
@@ -1,13 +1,11 @@
|
|||||||
use crate::data::{FetchResult, FetchResultCard, State};
|
use crate::data::{
|
||||||
use crate::download::Downloader;
|
FetchResult, FetchResultEvent, FetchResultUnlinkedCards, State,
|
||||||
|
};
|
||||||
|
use crate::download::{download_cards};
|
||||||
use crate::github::{fetch_last_hash, sync_data};
|
use crate::github::{fetch_last_hash, sync_data};
|
||||||
use crate::{CONFIG, PROVIDE, STATE, STORE, ending_point};
|
use crate::{PROVIDE, STATE, STORE, ending_point};
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use assets_updater::core::export_pipeline::find_files;
|
|
||||||
use futures::StreamExt;
|
|
||||||
use futures::stream::FuturesUnordered;
|
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
use std::fs;
|
|
||||||
|
|
||||||
pub async fn main() -> anyhow::Result<()> {
|
pub async fn main() -> anyhow::Result<()> {
|
||||||
let current_hash = fetch_last_hash().await?;
|
let current_hash = fetch_last_hash().await?;
|
||||||
@@ -38,6 +36,53 @@ pub async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
ending_point!(ok);
|
ending_point!(ok);
|
||||||
sync_data().await?;
|
sync_data().await?;
|
||||||
|
|
||||||
|
// unlinked
|
||||||
|
let unlinked_cards = {
|
||||||
|
STORE
|
||||||
|
.read()
|
||||||
|
.map_err(|e| anyhow!("{}", e))?
|
||||||
|
.find_unlinked_cards()
|
||||||
|
};
|
||||||
|
{
|
||||||
|
let new_cards: Vec<_> = {
|
||||||
|
let state = STATE.read().map_err(|e| anyhow!("{}", e))?;
|
||||||
|
if !state.unlinked_cards.is_empty() {
|
||||||
|
unlinked_cards
|
||||||
|
.iter()
|
||||||
|
.filter(|c| !state.unlinked_cards.contains(&c.id))
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if !new_cards.is_empty() {
|
||||||
|
ending_point!(ok);
|
||||||
|
let images = download_cards(&new_cards).await?;
|
||||||
|
info!("collected {} unlinked cards", images.len());
|
||||||
|
ending_point!(ok);
|
||||||
|
let result = FetchResultUnlinkedCards { cards: images };
|
||||||
|
debug!("Providing result: {:?}", result);
|
||||||
|
{
|
||||||
|
let mut provide = PROVIDE.write().map_err(|e| anyhow!("{}", e))?;
|
||||||
|
if provide.is_none() {
|
||||||
|
*provide = Some(Vec::new());
|
||||||
|
}
|
||||||
|
provide
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.push(FetchResult::UnlinkedCard(result));
|
||||||
|
}
|
||||||
|
ending_point!(ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut state = STATE.write().map_err(|e| anyhow!("{}", e))?;
|
||||||
|
state.unlinked_cards = unlinked_cards.iter().map(|c| c.id).collect();
|
||||||
|
state.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// event
|
||||||
ending_point!(ok);
|
ending_point!(ok);
|
||||||
let store = STORE.read().map_err(|e| anyhow!("{}", e))?;
|
let store = STORE.read().map_err(|e| anyhow!("{}", e))?;
|
||||||
let event = store.find_last_event();
|
let event = store.find_last_event();
|
||||||
@@ -61,79 +106,24 @@ pub async fn main() -> anyhow::Result<()> {
|
|||||||
let cards = store.find_cards_by_event(event.event_id);
|
let cards = store.find_cards_by_event(event.event_id);
|
||||||
drop(store);
|
drop(store);
|
||||||
ending_point!(ok);
|
ending_point!(ok);
|
||||||
let downloader = Downloader::new().await?;
|
let images = download_cards(&cards).await?;
|
||||||
ending_point!(ok);
|
|
||||||
let cache_dir = CONFIG.get().unwrap().cache_dir();
|
|
||||||
let mut futures = FuturesUnordered::new();
|
|
||||||
for card in &cards {
|
|
||||||
futures.push(async {
|
|
||||||
ending_point!(ok);
|
|
||||||
debug!(
|
|
||||||
"Pulling card {} from bundle {}",
|
|
||||||
card.id, card.assetbundle_name
|
|
||||||
);
|
|
||||||
let dir = downloader
|
|
||||||
.pull(&format!("character/member/{}", card.assetbundle_name))
|
|
||||||
.await?;
|
|
||||||
ending_point!(ok_with {
|
|
||||||
fs::remove_dir_all(dir.0)?;
|
|
||||||
});
|
|
||||||
let files = find_files(&dir.0)?;
|
|
||||||
let mut local_images = Vec::new();
|
|
||||||
for file in files {
|
|
||||||
ending_point!(ok_with {
|
|
||||||
fs::remove_dir_all(dir.0)?;
|
|
||||||
});
|
|
||||||
if file.is_file() && file.extension().map(|ext| ext == "png").unwrap_or(false) {
|
|
||||||
let new_filename = format!(
|
|
||||||
"{}_{}",
|
|
||||||
card.assetbundle_name,
|
|
||||||
file.file_name().unwrap().to_str().unwrap()
|
|
||||||
);
|
|
||||||
let new_file = cache_dir.join(new_filename);
|
|
||||||
fs::copy(&file, &new_file)?;
|
|
||||||
fs::remove_file(&file)?;
|
|
||||||
local_images.push(new_file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fs::remove_dir_all(dir.0)?;
|
|
||||||
Ok::<_, anyhow::Error>((card.clone(), local_images))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let mut images = Vec::new();
|
|
||||||
while let Some(res) = futures.next().await {
|
|
||||||
images.push(res?);
|
|
||||||
}
|
|
||||||
images.sort_by(|a, b| a.0.id.cmp(&b.0.id)); // 小在前
|
|
||||||
let images: Vec<_> = images
|
|
||||||
.into_iter()
|
|
||||||
.map(|(card, path)| FetchResultCard {
|
|
||||||
text: format!(
|
|
||||||
"#{id} {attr}{rarity} {skill} [{prefix}]{name}",
|
|
||||||
id = card.id,
|
|
||||||
attr = card.attr_short_text(),
|
|
||||||
rarity = card.rarity_short_text(),
|
|
||||||
skill = card
|
|
||||||
.skill_id_short().map(|s| format!("({})", s))
|
|
||||||
.unwrap_or_default(),
|
|
||||||
prefix = card.prefix,
|
|
||||||
name = card.character_name()
|
|
||||||
),
|
|
||||||
path,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"In event {} collected {} cards",
|
"In event {} collected {} cards",
|
||||||
event.event_id,
|
event.event_id,
|
||||||
images.len()
|
images.len()
|
||||||
);
|
);
|
||||||
let result = FetchResult {
|
let result = FetchResultEvent {
|
||||||
event_id: event.event_id,
|
event_id: event.event_id,
|
||||||
cards: images,
|
cards: images,
|
||||||
};
|
};
|
||||||
debug!("Providing result: {:?}", result);
|
debug!("Providing result: {:?}", result);
|
||||||
*PROVIDE.write().map_err(|e| anyhow!("{}", e))? = Some(result);
|
{
|
||||||
|
let mut provide = PROVIDE.write().map_err(|e| anyhow!("{}", e))?;
|
||||||
|
if provide.is_none() {
|
||||||
|
*provide = Some(Vec::new());
|
||||||
|
}
|
||||||
|
provide.as_mut().unwrap().push(FetchResult::Event(result));
|
||||||
|
}
|
||||||
let mut state = STATE.write().map_err(|e| anyhow!("{}", e))?;
|
let mut state = STATE.write().map_err(|e| anyhow!("{}", e))?;
|
||||||
state.git_hash = Some(current_hash);
|
state.git_hash = Some(current_hash);
|
||||||
state.event_id = Some(event.event_id);
|
state.event_id = Some(event.event_id);
|
||||||
@@ -141,3 +131,16 @@ pub async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn refetch_event_cards() -> anyhow::Result<FetchResultEvent> {
|
||||||
|
let event_id = { STATE.read().map_err(|e| anyhow!("{}", e))?.event_id };
|
||||||
|
let event_id = event_id.ok_or(anyhow!("Event ID not found"))?;
|
||||||
|
let store = STORE.read().map_err(|e| anyhow!("{}", e))?;
|
||||||
|
let cards = store.find_cards_by_event(event_id);
|
||||||
|
ending_point!(ok);
|
||||||
|
let images = download_cards(&cards).await?;
|
||||||
|
Ok(FetchResultEvent {
|
||||||
|
event_id,
|
||||||
|
cards: images,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
+12
-4
@@ -85,11 +85,19 @@ impl Store {
|
|||||||
self.event_cards
|
self.event_cards
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|e| e.event_id == event_id)
|
.filter(|e| e.event_id == event_id)
|
||||||
.filter_map(|c| {
|
.filter_map(|c| self.cards.iter().rev().find(|e| e.id == c.card_id))
|
||||||
self.cards
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_unlinked_cards(&self) -> Vec<Card> {
|
||||||
|
self.cards
|
||||||
|
.iter()
|
||||||
|
.filter(|c| {
|
||||||
|
self.event_cards
|
||||||
.iter()
|
.iter()
|
||||||
.rev()
|
.find(|e| e.card_id == c.id)
|
||||||
.find(|e| e.id == c.card_id)
|
.is_none()
|
||||||
})
|
})
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect()
|
.collect()
|
||||||
|
|||||||
Reference in New Issue
Block a user