Initial possibly complete version.
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
2630
Cargo.lock
generated
Normal file
2630
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
Cargo.toml
Normal file
15
Cargo.toml
Normal 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
49
src/err_dialog_types.rs
Normal 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
91
src/file_field.rs
Normal 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
160
src/item_info.rs
Normal 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
358
src/main.rs
Normal 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
259
src/my_steamworks.rs
Normal 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
1
steam_appid.txt
Normal file
@ -0,0 +1 @@
|
||||
571880
|
Reference in New Issue
Block a user