Initial possibly complete version.

This commit is contained in:
4onen
2022-12-15 10:10:56 -08:00
commit 89f3ec0410
9 changed files with 3564 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

2630
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

15
Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "workshop_uploader"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[[bin]]
name="4wu"
path="src/main.rs"
[dependencies]
steamworks = "0.9.0"
iced = "0.6"
native-dialog = "0.6.3"

49
src/err_dialog_types.rs Normal file
View File

@ -0,0 +1,49 @@
pub fn error_dialog(msg: &str) {
let _ = native_dialog::MessageDialog::new()
.set_type(native_dialog::MessageType::Error)
.set_title("Error")
.set_text(msg)
.show_alert();
}
pub fn confirm_dialog(msg: &str) -> bool {
let ans = native_dialog::MessageDialog::new()
.set_type(native_dialog::MessageType::Warning)
.set_title("Warning")
.set_text(msg)
.show_confirm();
match ans {
Ok(b) => b,
Err(e) => {
error_dialog(
format!("Error retrieving confirmation: {:?}\nAssuming False...", e).as_str(),
);
false
}
}
}
pub trait ErrorDialogUnwrapper<T> {
fn expect_or_dialog(self, msg: &str) -> T;
}
impl<T> ErrorDialogUnwrapper<T> for Option<T> {
#[track_caller]
fn expect_or_dialog(self, msg: &str) -> T {
self.unwrap_or_else(|| {
error_dialog(msg);
None.expect(msg)
})
}
}
impl<T, E: std::fmt::Debug> ErrorDialogUnwrapper<T> for Result<T, E> {
#[track_caller]
fn expect_or_dialog(self, msg: &str) -> T {
self.unwrap_or_else(|e| {
error_dialog(format!("{} {:?}", msg, &e).as_str());
Err(e).expect(msg)
})
}
}

91
src/file_field.rs Normal file
View File

@ -0,0 +1,91 @@
use super::err_dialog_types::error_dialog;
use iced::widget::{button, column, row, text, text_input};
use iced::Element;
use native_dialog::FileDialog;
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileField {
pub path: PathBuf,
}
impl FileField {
pub fn new() -> Self {
FileField {
path: PathBuf::new(),
}
}
pub fn view<'a, Message: Clone + 'a>(
&self,
label: &str,
placeholder: &str,
edit_msg: fn(String) -> Message,
browse_msg: Message,
) -> Element<'a, Message> {
column![
text(label),
row![
text_input(placeholder, &self.path.to_string_lossy(), edit_msg),
button("Browse",).on_press(browse_msg),
],
]
.into()
}
pub fn select_file(&mut self) {
let result = FileDialog::new()
.add_filter("JPG Files", &["*.jpg", "*.jpeg"])
.show_open_single_file();
if let Ok(pathbuf) = result {
if let Some(pathbuf) = pathbuf {
self.path = pathbuf;
};
} else {
error_dialog(
format!("Failed to select file. Error: {:?}", result.err().unwrap()).as_str(),
);
}
}
pub fn select_dir(&mut self) {
let result = FileDialog::new().show_open_single_dir();
if let Ok(pathbuf) = result {
if let Some(pathbuf) = pathbuf {
self.path = pathbuf;
};
} else {
error_dialog(
format!(
"Failed to select directory. Error: {:?}",
result.err().unwrap()
)
.as_str(),
);
}
}
}
impl From<PathBuf> for FileField {
fn from(path: PathBuf) -> Self {
FileField { path }
}
}
impl From<String> for FileField {
fn from(path: String) -> Self {
FileField {
path: PathBuf::from(path),
}
}
}
impl From<&str> for FileField {
fn from(path: &str) -> Self {
FileField {
path: PathBuf::from(path),
}
}
}

160
src/item_info.rs Normal file
View File

@ -0,0 +1,160 @@
use super::file_field::FileField;
use iced::widget::{column, text, text_input};
use iced::Element;
use std::path::PathBuf;
use steamworks::{PublishedFileId, QueryResult};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ItemInfoMessage {
EditName(String),
EditPreviewImage(String),
EditTargetFolder(String),
BrowsePreviewImage,
BrowseTargetFolder,
EditChangeNotes(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ItemInfoState {
name: String,
preview_image: FileField,
target_folder: FileField,
change_notes: String,
}
impl Default for ItemInfoState {
fn default() -> Self {
ItemInfoState {
name: String::new(),
preview_image: FileField::new(),
target_folder: FileField::new(),
change_notes: String::new(),
}
}
}
impl ItemInfoState {
pub fn update(&mut self, message: ItemInfoMessage) {
match message {
ItemInfoMessage::EditName(new_name) => self.name = new_name,
ItemInfoMessage::EditPreviewImage(new_path) => {
self.preview_image = FileField::from(new_path)
}
ItemInfoMessage::EditTargetFolder(new_path) => {
self.target_folder = FileField::from(new_path)
}
ItemInfoMessage::BrowsePreviewImage => {
self.preview_image.select_file();
}
ItemInfoMessage::BrowseTargetFolder => {
self.target_folder.select_dir();
}
ItemInfoMessage::EditChangeNotes(new_notes) => self.change_notes = new_notes,
}
}
pub fn view(&self, file_id: Option<PublishedFileId>) -> Element<ItemInfoMessage> {
column![
if let Some(file_id) = file_id {
text(format!("Updating item with ID: {}", file_id.0))
} else {
text("Creating new item:")
},
text_input("Name", &self.name, ItemInfoMessage::EditName,),
self.preview_image.view(
"Preview Image",
if file_id.is_some() { "Optional" } else { "" },
ItemInfoMessage::EditPreviewImage,
ItemInfoMessage::BrowsePreviewImage,
),
self.target_folder.view(
"Target Folder",
"",
ItemInfoMessage::EditTargetFolder,
ItemInfoMessage::BrowseTargetFolder,
),
text_input(
"Changenotes",
&self.change_notes,
ItemInfoMessage::EditChangeNotes
)
]
.into()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ItemInfo {
pub name: String,
pub preview_image: PathBuf,
pub target_folder: PathBuf,
pub change_notes: String,
}
impl From<ItemInfo> for ItemInfoState {
fn from(value: ItemInfo) -> Self {
ItemInfoState {
name: value.name,
preview_image: FileField::from(value.preview_image),
target_folder: FileField::from(value.target_folder),
change_notes: value.change_notes,
}
}
}
impl From<QueryResult> for ItemInfo {
fn from(value: QueryResult) -> Self {
ItemInfo {
name: value.title,
preview_image: PathBuf::new(),
target_folder: PathBuf::new(),
change_notes: String::new(),
}
}
}
impl TryFrom<ItemInfoState> for ItemInfo {
type Error = String;
fn try_from(value: ItemInfoState) -> Result<Self, Self::Error> {
if value.name.is_empty() {
return Err("Name cannot be empty.".to_string());
}
let preview_field_exists = value.preview_image.path.exists();
let has_preview = preview_field_exists && value.preview_image.path.is_file();
if !has_preview {
if !value.preview_image.path.to_string_lossy().is_empty() {
if !preview_field_exists {
return Err(format!(
"Preview image \"{}\" does not exist.",
value.preview_image.path.to_string_lossy()
));
} else {
return Err(format!(
"Preview image \"{}\" is not a file.",
value.preview_image.path.to_string_lossy()
));
}
}
}
if !value.target_folder.path.exists() {
if value.target_folder.path.to_string_lossy().is_empty() {
return Err("Target folder cannot be empty.".to_string());
} else {
return Err(format!(
"Target folder \"{}\" does not exist.",
value.target_folder.path.to_string_lossy()
));
}
}
Ok(ItemInfo {
name: value.name,
preview_image: value.preview_image.path,
target_folder: value.target_folder.path,
change_notes: value.change_notes,
})
}
}

358
src/main.rs Normal file
View File

@ -0,0 +1,358 @@
mod err_dialog_types;
mod file_field;
mod item_info;
mod my_steamworks;
use err_dialog_types::ErrorDialogUnwrapper;
use iced::widget::{button, column, row, text, text_input};
use iced::{Application, Command, Element, Settings};
use item_info::{ItemInfo, ItemInfoMessage, ItemInfoState};
use my_steamworks::WorkshopClient;
use std::num::IntErrorKind;
use steamworks::{AppId, PublishedFileId, SteamError};
const APP_ID_STR: &str = include_str!("../steam_appid.txt");
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Message {
SetExistingId(String),
EditItemData(ItemInfoMessage),
ReceiveFoundItemInfo(ItemInfo),
ReceiveItemId(PublishedFileId),
ReceiveSteamError(SteamError),
Proceed,
GoBack,
TermsLinkPressed,
}
impl Message {
fn receive_item_id(res: Result<(PublishedFileId, bool), SteamError>) -> Self {
match res {
Ok((id, _)) => Message::ReceiveItemId(id),
Err(err) => Message::ReceiveSteamError(err),
}
}
fn receive_item_info(res: Result<ItemInfo, SteamError>) -> Self {
match res {
Ok(item_info) => Message::ReceiveFoundItemInfo(item_info),
Err(err) => Message::ReceiveSteamError(err),
}
}
}
#[derive(Clone, PartialEq, Eq)]
enum ModelState {
Initial(String),
ExistingIdSearching(PublishedFileId, Option<SteamError>),
ItemForm(Option<PublishedFileId>, ItemInfoState),
CreatingItem(ItemInfo),
CreationError(ItemInfo, SteamError),
SendingItem(PublishedFileId, ItemInfo),
SendingError(PublishedFileId, ItemInfo, SteamError),
Done(PublishedFileId),
}
struct Model {
client: WorkshopClient,
state: ModelState,
}
fn initial_view<'a>(existing_id: &str) -> Element<'a, Message> {
let item_id = existing_id.parse::<u64>().map(PublishedFileId);
let mut res = column![
text("4onen's Steam Workshop Uploader"),
if let Err(error) = &item_id {
if *error.kind() == IntErrorKind::Empty {
button("Create new").on_press(Message::Proceed)
} else {
button("Update existing")
}
} else {
button("Update existing").on_press(Message::Proceed)
},
text_input("Existing item ID", existing_id, Message::SetExistingId)
.on_submit(Message::Proceed),
];
if let Err(error) = item_id {
if *error.kind() != IntErrorKind::Empty {
res = res.push(text(format!("Invalid item ID: {}.", error)));
}
}
res.into()
}
fn edit_item_view<'a>(
item_info: &'a ItemInfoState,
existing_id: Option<PublishedFileId>,
) -> Element<'a, Message> {
let ready_info = ItemInfo::try_from(item_info.clone());
let mut fwd_button = if existing_id.is_some() {
button("Update")
} else {
button("Create")
};
if let Ok(_) = &ready_info {
fwd_button = fwd_button.on_press(Message::Proceed);
}
column![
item_info
.view(existing_id)
.map(move |message| Message::EditItemData(message)),
column![
text("By submitting this item, you agree to the Steam workshop"),
button("Terms of Service").on_press(Message::TermsLinkPressed)
],
row![button("Go back").on_press(Message::GoBack), fwd_button],
match ready_info {
Ok(_) => text(""),
Err(error) => text(format!("{}", error)),
},
]
.into()
}
impl Model {
fn update_to_create_item(&mut self, item_info: ItemInfo) -> Command<Message> {
self.state = ModelState::CreatingItem(item_info);
Command::perform(self.client.clone().create_item(), Message::receive_item_id)
}
fn update_to_send_item(
&mut self,
item_id: PublishedFileId,
item_info: ItemInfo,
) -> Command<Message> {
self.state = ModelState::SendingItem(item_id, item_info.clone());
Command::perform(
self.client.clone().send_item(item_id, item_info),
Message::receive_item_id,
)
}
}
impl Application for Model {
type Message = Message;
type Executor = iced::executor::Default;
type Flags = WorkshopClient;
type Theme = iced::Theme;
fn new(client: Self::Flags) -> (Self, Command<Self::Message>) {
let state = ModelState::Initial(String::new());
(Model { client, state }, Command::none())
}
fn title(&self) -> String {
String::from("4onen's Workshop Uploader")
}
fn update(&mut self, message: Self::Message) -> Command<Message> {
const CMDN: Command<Message> = Command::none();
if std::mem::discriminant(&message) == std::mem::discriminant(&Message::TermsLinkPressed) {
self.client.open_terms();
return CMDN;
}
match self.state.clone() {
ModelState::Initial(idstr) => match message {
Message::SetExistingId(idstr) => {
self.state = ModelState::Initial(idstr);
CMDN
}
Message::Proceed => match idstr.parse::<u64>().map(PublishedFileId) {
Ok(item_id) => {
self.state = ModelState::ExistingIdSearching(item_id, None);
Command::perform(
self.client.clone().get_item_info(item_id),
Message::receive_item_info,
)
}
_ => {
self.state = ModelState::ItemForm(None, ItemInfoState::default());
CMDN
}
},
_ => CMDN,
},
ModelState::ExistingIdSearching(item_id, _) => {
match message {
Message::GoBack => self.state = ModelState::Initial(item_id.0.to_string()),
Message::ReceiveFoundItemInfo(item_info) => {
self.state = ModelState::ItemForm(Some(item_id), item_info.into())
}
Message::ReceiveSteamError(err) => {
self.state = ModelState::ExistingIdSearching(item_id, Some(err))
}
_ => (),
};
CMDN
}
ModelState::ItemForm(maybe_id, mut item_info) => match message {
Message::EditItemData(item_info_message) => {
item_info.update(item_info_message);
self.state = ModelState::ItemForm(maybe_id, item_info);
CMDN
}
Message::Proceed => match ItemInfo::try_from(item_info.clone()) {
Ok(item_info) => match maybe_id {
Some(item_id) => self.update_to_send_item(item_id, item_info),
None => self.update_to_create_item(item_info),
},
Err(error) => {
println!("Error: {}", error);
CMDN
}
},
Message::GoBack => {
self.state = ModelState::Initial(
maybe_id
.map(|id| id.0.to_string())
.unwrap_or(String::default()),
);
CMDN
}
_ => CMDN,
},
ModelState::CreatingItem(item_info) => match message {
Message::ReceiveItemId(item_id) => self.update_to_send_item(item_id, item_info),
Message::ReceiveSteamError(err) => {
self.state = ModelState::CreationError(item_info, err);
CMDN
}
_ => CMDN,
},
ModelState::CreationError(item_info, _err) => {
match message {
Message::GoBack => self.state = ModelState::ItemForm(None, item_info.into()),
_ => (),
};
CMDN
}
ModelState::SendingItem(item_id, item_info) => {
match message {
Message::ReceiveItemId(incoming_id) => {
if incoming_id != item_id {
println!(
"Not advancing due to non-matching ids. Expected {}, got {}.",
item_id.0, incoming_id.0,
);
} else {
self.state = ModelState::Done(item_id);
};
}
Message::ReceiveSteamError(err) => {
self.state = ModelState::SendingError(item_id, item_info, err);
}
_ => (),
};
CMDN
}
ModelState::SendingError(item_id, item_info, _err) => {
match message {
Message::GoBack => {
self.state = ModelState::ItemForm(item_id.into(), item_info.into())
}
_ => (),
};
CMDN
}
ModelState::Done(item_id) => {
match message {
Message::Proceed => {
let item_url = format!("steam://url/CommunityFilePage/{}", item_id.0);
self.client.open_url(item_url.as_str());
}
Message::GoBack => {
self.state = ModelState::Initial(String::default());
}
_ => (),
};
CMDN
}
}
}
fn view(&self) -> Element<Self::Message> {
match &self.state {
ModelState::Initial(existing_id) => initial_view(existing_id.as_str()),
ModelState::ExistingIdSearching(item_id, None) => column![
text(format!("Searching for item with ID {}...", item_id.0)),
button("Cancel").on_press(Message::GoBack),
]
.into(),
ModelState::ExistingIdSearching(item_id, Some(e)) => column![
text(format!(
"Search for item with ID {} failed.\nError: {:?}",
item_id.0, e
)),
button("Go Back").on_press(Message::GoBack),
]
.into(),
ModelState::ItemForm(item_id, item_state) => edit_item_view(item_state, *item_id),
ModelState::CreatingItem(item_info) => {
text(format!("Creating \"{}\" on Steam Workshop...", item_info.name).as_str()).into()
}
ModelState::CreationError(item_info, err) => column![text(format!(
"Error creating a new entry on the workshop:\n{:?}\n\"{}\" was not uploaded.",
err, item_info.name
)),
button("Go Back").on_press(Message::GoBack),
]
.into(),
ModelState::SendingItem(item_id, _item_info) => {
text(format!("Sending item {} to Steam Workshop...", item_id.0).as_str()).into()
}
ModelState::SendingError(item_id, item_info, err) => column![text(format!(
"Error uploading your item to the workshop:\n{:?}\n\"{}\" is created on the workshop with ID {}, but does not have your files in it.\nPlease resolve the issue and try uploading to this existing ID again.",
err, item_info.name, item_id.0
).as_str()),
button("Go Back").on_press(Message::GoBack),
].into(),
ModelState::Done(id) => column![
text(format!("Item ID {} uploaded to workshop.", id.0)),
button("Go to your item").on_press(Message::Proceed),
button("Restart").on_press(Message::GoBack),
]
.into(),
}
}
}
fn main() -> iced::Result {
let client = APP_ID_STR
.parse()
.map(AppId)
.map(WorkshopClient::init_app)
.expect_or_dialog("Failed to parse App ID. This build of the workshop uploader is corrupt.")
.expect_or_dialog("Failed to initialize Steam Workshop client.");
Model::run(Settings {
id: None,
window: iced::window::Settings {
size: (300, 400),
position: iced::window::Position::Centered,
min_size: None,
max_size: None,
visible: true,
resizable: true,
decorations: true,
transparent: false,
always_on_top: false,
icon: None,
},
flags: client,
default_font: None,
default_text_size: 20,
text_multithreading: false,
antialiasing: false,
exit_on_close_request: true,
try_opengles_first: false,
})
}

259
src/my_steamworks.rs Normal file
View File

@ -0,0 +1,259 @@
use super::item_info::ItemInfo;
use crate::err_dialog_types::confirm_dialog;
use std::ops::Deref;
use std::sync::{atomic::AtomicUsize, atomic::Ordering, Arc};
use std::thread::Thread;
use std::time::Duration;
use steamworks::{Client, PublishedFileId, QueryResult, QueryResults, SingleClient, SteamError};
#[derive(Debug, Clone)]
pub struct SingleClientExecutor {
watchers: Arc<AtomicUsize>,
handle: Thread,
}
impl SingleClientExecutor {
fn watch(&self) {
self.watchers.fetch_add(1, Ordering::Release);
self.handle.unpark()
}
fn unwatch(&self) {
self.watchers.fetch_sub(1, Ordering::Acquire);
}
}
fn start_executor(single_client: SingleClient) -> SingleClientExecutor {
let watchers: Arc<AtomicUsize> = Arc::default();
let thread_copy = watchers.clone();
let handle = std::thread::Builder::new()
.name("SingleClientExecutor".to_string())
.spawn(move || steamworks_worker(single_client, thread_copy))
.expect("Failed to start steamworks thread.")
.thread()
.clone();
SingleClientExecutor { watchers, handle }
}
fn steamworks_worker(single_client: SingleClient, mut watchers: Arc<AtomicUsize>) {
loop {
while watchers.load(Ordering::Acquire) > 0 {
single_client.run_callbacks();
}
std::thread::park_timeout(Duration::from_millis(100));
match Arc::try_unwrap(watchers) {
Ok(_) => return,
Err(arc) => watchers = arc,
}
}
}
#[derive(Debug)]
pub struct SingleClientExecutorWatcher {
executor: SingleClientExecutor,
}
impl SingleClientExecutorWatcher {
fn new(executor: SingleClientExecutor) -> Self {
executor.watch();
SingleClientExecutorWatcher { executor }
}
}
impl Drop for SingleClientExecutorWatcher {
fn drop(&mut self) {
self.executor.unwatch();
}
}
#[derive(Debug)]
pub struct CallbackSender<T> {
_watcher: SingleClientExecutorWatcher,
sender: iced::futures::channel::oneshot::Sender<T>,
}
impl<T> CallbackSender<T> {
fn get_channel(
executor: SingleClientExecutor,
) -> (Self, iced::futures::channel::oneshot::Receiver<T>) {
let (tx, rx) = iced::futures::channel::oneshot::channel();
let wtx = CallbackSender {
_watcher: SingleClientExecutorWatcher::new(executor),
sender: tx,
};
(wtx, rx)
}
fn send(self, value: T) -> Result<(), T> {
self.sender.send(value)
}
}
impl<T> Deref for CallbackSender<T> {
type Target = iced::futures::channel::oneshot::Sender<T>;
fn deref(&self) -> &Self::Target {
&self.sender
}
}
#[derive(Clone)]
pub struct WorkshopClient {
callback_executor: SingleClientExecutor,
steam_client: Client,
}
impl WorkshopClient {
pub fn init_app(id: steamworks::AppId) -> steamworks::SResult<Self> {
Client::init_app(id).map(|(client, single_client)| WorkshopClient {
callback_executor: start_executor(single_client),
steam_client: client,
})
}
pub fn open_url(&self, url: &str) -> () {
self.steam_client
.friends()
.activate_game_overlay_to_web_page(url)
}
pub fn open_terms(&self) -> () {
const STEAM_LEGAL_AGREEMENT: &str =
"https://steamcommunity.com/sharedfiles/workshoplegalagreement";
self.open_url(STEAM_LEGAL_AGREEMENT)
}
pub async fn get_item_info(
self: WorkshopClient,
item_id: steamworks::PublishedFileId,
) -> Result<ItemInfo, SteamError> {
let app_id = self.steam_client.utils().app_id();
let (tx, rx) = CallbackSender::get_channel(self.callback_executor.clone());
self.steam_client
.ugc()
.query_item(item_id)
.expect("Failed to generate single item query.")
.allow_cached_response(360)
.include_long_desc(false)
.include_children(false)
.include_metadata(false)
.include_additional_previews(false)
.fetch(move |res| {
let _ = tx.send(res.and_then(|res| res.get(0).ok_or(SteamError::NoMatch)));
});
rx.await
.map_err(|iced::futures::channel::oneshot::Canceled| SteamError::Cancelled)
.and_then(|x|x)
.and_then(|res| match res.file_type {
steamworks::FileType::Community => Ok(res),
_ => Err(SteamError::NoMatch),
})
.and_then(|res| {
if res.consumer_app_id != Some(app_id){
if confirm_dialog(format!("Found item\n\t\"{}\"\nappears to be for a different app than this uploader works with.\nYou may be blocked from uploading. Continue?",res.title).as_str()){
Ok(res)
}else{
Err(SteamError::Cancelled)
}
} else {
Ok(res)
}
} )
// .and_then(|res| {
// let user = self.steam_client.user().steam_id();
// if res.owner != user && !confirm_dialog("This Workshop entry appears to have been made by another user.\nYou may be blocked from uploading.\nContinue?"){
// // This check is, at present, not working.
// println!("\nOwner: {}\nUser: {}",res.owner.raw(), user.raw());
// Err(SteamError::AccessDenied)
// }else{
// Ok(res)
// }
// })
.map(Into::<ItemInfo>::into)
}
pub async fn create_item(self) -> Result<(PublishedFileId, bool), SteamError> {
let app_id = self.steam_client.utils().app_id();
let (tx, rx) = CallbackSender::get_channel(self.callback_executor.clone());
self.steam_client
.ugc()
.create_item(app_id, steamworks::FileType::Community, move |res| {
let _ = tx.send(res);
});
rx.await
.map_err(|iced::futures::channel::oneshot::Canceled| SteamError::Cancelled)
.and_then(|x| x)
}
pub async fn send_item(
self,
item_id: PublishedFileId,
item_info: ItemInfo,
) -> Result<(PublishedFileId, bool), SteamError> {
let rx = {
let app_id = self.steam_client.utils().app_id();
let change_notes = if item_info.change_notes.is_empty() {
None
} else {
Some(item_info.change_notes.as_str())
};
let mut update_handle = self
.steam_client
.ugc()
.start_item_update(app_id, item_id)
.title(item_info.name.as_str())
.content_path(&item_info.target_folder);
if item_info.preview_image.exists() {
update_handle = update_handle.preview_path(&item_info.preview_image)
}
let (tx, rx) = CallbackSender::get_channel(self.callback_executor.clone());
let _update_watch_handle = update_handle.submit(change_notes, move |res| {
let _ = tx.send(res);
});
rx
};
rx.await
.map_err(|iced::futures::channel::oneshot::Canceled| SteamError::Cancelled)
.and_then(|x| x)
}
}
fn _debug_query_result(result: QueryResult) {
println!(
"QueryResult: \"{}\" ({})",
result.title, result.published_file_id.0
);
println!("Owner: {}", result.owner.raw());
println!(
"Description: {} words",
result.description.split_whitespace().into_iter().count()
);
println!("File type: {:?}", result.file_type);
}
fn _debug_query_results(results: &QueryResults) {
println!("QueryResults: (FromCache: {})", results.was_cached());
let result_count = results.total_results();
for (i, result) in results.iter().enumerate() {
if let Some(result) = result {
println!("Result {}/{}", i, result_count);
_debug_query_result(result);
} else {
println!("Result #{}: None", i);
}
}
}

1
steam_appid.txt Normal file
View File

@ -0,0 +1 @@
571880