1use std::ffi::OsStr;
3use std::path::Path;
4
5use mime::IMAGE_STAR;
6
7use crate::file_source::FileSource;
8use crate::mods::{ModRef, Mods};
9use crate::prelude::*;
10use crate::types::id::{GameId, ModId};
11
12pub use crate::types::games::{
13 ApiAccessOptions, CommunityOptions, CurationOption, Downloads, Game, HeaderImage, Icon,
14 MaturityOptions, OtherUrl, Platform, PresentationOption, Statistics, SubmissionOption,
15 TagOption, TagType, Theme,
16};
17pub use crate::types::Logo;
18pub use crate::types::Status;
19
20#[derive(Clone)]
22pub struct Games {
23 modio: Modio,
24}
25
26impl Games {
27 pub(crate) fn new(modio: Modio) -> Self {
28 Self { modio }
29 }
30
31 pub fn search(&self, filter: Filter) -> Query<Game> {
35 let route = Route::GetGames {
36 show_hidden_tags: None,
37 };
38 Query::new(self.modio.clone(), route, filter)
39 }
40
41 pub fn get(&self, id: GameId) -> GameRef {
43 GameRef::new(self.modio.clone(), id)
44 }
45}
46
47#[derive(Clone)]
49pub struct GameRef {
50 modio: Modio,
51 id: GameId,
52}
53
54impl GameRef {
55 pub(crate) fn new(modio: Modio, id: GameId) -> Self {
56 Self { modio, id }
57 }
58
59 pub async fn get(self) -> Result<Game> {
61 let route = Route::GetGame {
62 id: self.id,
63 show_hidden_tags: None,
64 };
65 self.modio.request(route).send().await
66 }
67
68 pub fn mod_(&self, mod_id: ModId) -> ModRef {
70 ModRef::new(self.modio.clone(), self.id, mod_id)
71 }
72
73 pub fn mods(&self) -> Mods {
75 Mods::new(self.modio.clone(), self.id)
76 }
77
78 pub async fn statistics(self) -> Result<Statistics> {
80 let route = Route::GetGameStats { game_id: self.id };
81 self.modio.request(route).send().await
82 }
83
84 pub fn tags(&self) -> Tags {
86 Tags::new(self.modio.clone(), self.id)
87 }
88
89 pub async fn edit_media(self, media: EditMediaOptions) -> Result<()> {
91 let route = Route::AddGameMedia { game_id: self.id };
92 self.modio
93 .request(route)
94 .multipart(Form::from(media))
95 .send::<Message>()
96 .await?;
97 Ok(())
98 }
99}
100
101#[derive(Clone)]
103pub struct Tags {
104 modio: Modio,
105 game_id: GameId,
106}
107
108impl Tags {
109 fn new(modio: Modio, game_id: GameId) -> Self {
110 Self { modio, game_id }
111 }
112
113 pub async fn list(self) -> Result<Vec<TagOption>> {
115 let route = Route::GetGameTags {
116 game_id: self.game_id,
117 };
118 Query::new(self.modio, route, Filter::default())
119 .collect()
120 .await
121 }
122
123 #[allow(clippy::iter_not_returning_iterator)]
125 pub async fn iter(self) -> Result<impl Stream<Item = Result<TagOption>>> {
126 let route = Route::GetGameTags {
127 game_id: self.game_id,
128 };
129 let filter = Filter::default();
130 Query::new(self.modio, route, filter).iter().await
131 }
132
133 #[allow(clippy::should_implement_trait)]
135 pub async fn add(self, options: AddTagsOptions) -> Result<()> {
136 let route = Route::AddGameTags {
137 game_id: self.game_id,
138 };
139 self.modio
140 .request(route)
141 .form(&options)
142 .send::<Message>()
143 .await?;
144 Ok(())
145 }
146
147 pub async fn delete(self, options: DeleteTagsOptions) -> Result<Deletion> {
149 let route = Route::DeleteGameTags {
150 game_id: self.game_id,
151 };
152 self.modio.request(route).form(&options).send().await
153 }
154
155 pub async fn rename(self, from: String, to: String) -> Result<()> {
157 let route = Route::RenameGameTags {
158 game_id: self.game_id,
159 };
160 self.modio
161 .request(route)
162 .form(&[("from", from), ("to", to)])
163 .send::<()>()
164 .await?;
165
166 Ok(())
167 }
168}
169
170#[rustfmt::skip]
213pub mod filters {
214 #[doc(inline)]
215 pub use crate::filter::prelude::Fulltext;
216 #[doc(inline)]
217 pub use crate::filter::prelude::Id;
218 #[doc(inline)]
219 pub use crate::filter::prelude::Name;
220 #[doc(inline)]
221 pub use crate::filter::prelude::NameId;
222 #[doc(inline)]
223 pub use crate::filter::prelude::Status;
224 #[doc(inline)]
225 pub use crate::filter::prelude::DateAdded;
226 #[doc(inline)]
227 pub use crate::filter::prelude::DateUpdated;
228 #[doc(inline)]
229 pub use crate::filter::prelude::DateLive;
230 #[doc(inline)]
231 pub use crate::filter::prelude::SubmittedBy;
232
233 filter!(Summary, SUMMARY, "summary", Eq, NotEq, Like);
234 filter!(InstructionsUrl, INSTRUCTIONS_URL, "instructions_url", Eq, NotEq, In, Like);
235 filter!(UgcName, UGC_NAME, "ugc_name", Eq, NotEq, In, Like);
236 filter!(PresentationOption, PRESENTATION_OPTION, "presentation_option", Eq, NotEq, In, Cmp, Bit);
237 filter!(SubmissionOption, SUBMISSION_OPTION, "submission_option", Eq, NotEq, In, Cmp, Bit);
238 filter!(CurationOption, CURATION_OPTION, "curation_option", Eq, NotEq, In, Cmp, Bit);
239 filter!(CommunityOptions, COMMUNITY_OPTIONS, "community_options", Eq, NotEq, In, Cmp, Bit);
240 filter!(RevenueOptions, REVENUE_OPTIONS, "revenue_options", Eq, NotEq, In, Cmp, Bit);
241 filter!(ApiAccessOptions, API_ACCESS_OPTIONS, "api_access_options", Eq, NotEq, In, Cmp, Bit);
242 filter!(MaturityOptions, MATURITY_OPTIONS, "maturity_options", Eq, NotEq, In, Cmp, Bit);
243}
244
245pub struct AddTagsOptions {
246 name: String,
247 kind: TagType,
248 hidden: bool,
249 locked: bool,
250 tags: Vec<String>,
251}
252
253impl AddTagsOptions {
254 pub fn new<S: Into<String>>(name: S, kind: TagType, tags: &[String]) -> Self {
255 Self {
256 name: name.into(),
257 kind,
258 hidden: false,
259 locked: false,
260 tags: tags.to_vec(),
261 }
262 }
263
264 pub fn hidden(self, value: bool) -> Self {
265 Self {
266 hidden: value,
267 ..self
268 }
269 }
270
271 pub fn locked(self, value: bool) -> Self {
272 Self {
273 locked: value,
274 ..self
275 }
276 }
277}
278
279#[doc(hidden)]
280impl serde::ser::Serialize for AddTagsOptions {
281 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
282 where
283 S: serde::ser::Serializer,
284 {
285 use serde::ser::SerializeMap;
286
287 let len = 2 + usize::from(self.hidden) + usize::from(self.locked) + self.tags.len();
288 let mut map = serializer.serialize_map(Some(len))?;
289 map.serialize_entry("name", &self.name)?;
290 map.serialize_entry("type", &self.kind)?;
291 if self.hidden {
292 map.serialize_entry("hidden", &self.hidden)?;
293 }
294 if self.locked {
295 map.serialize_entry("locked", &self.locked)?;
296 }
297 for t in &self.tags {
298 map.serialize_entry("tags[]", t)?;
299 }
300 map.end()
301 }
302}
303
304pub struct DeleteTagsOptions {
305 name: String,
306 tags: Option<Vec<String>>,
307}
308
309impl DeleteTagsOptions {
310 pub fn all<S: Into<String>>(name: S) -> Self {
311 Self {
312 name: name.into(),
313 tags: None,
314 }
315 }
316
317 pub fn some<S: Into<String>>(name: S, tags: &[String]) -> Self {
318 Self {
319 name: name.into(),
320 tags: if tags.is_empty() {
321 None
322 } else {
323 Some(tags.to_vec())
324 },
325 }
326 }
327}
328
329#[doc(hidden)]
330impl serde::ser::Serialize for DeleteTagsOptions {
331 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
332 where
333 S: serde::ser::Serializer,
334 {
335 use serde::ser::SerializeMap;
336
337 let len = self.tags.as_ref().map_or(1, Vec::len);
338 let mut map = serializer.serialize_map(Some(len + 1))?;
339 map.serialize_entry("name", &self.name)?;
340 if let Some(ref tags) = self.tags {
341 for t in tags {
342 map.serialize_entry("tags[]", t)?;
343 }
344 } else {
345 map.serialize_entry("tags[]", "")?;
346 }
347 map.end()
348 }
349}
350
351#[derive(Default)]
352pub struct EditMediaOptions {
353 logo: Option<FileSource>,
354 icon: Option<FileSource>,
355 header: Option<FileSource>,
356}
357
358impl EditMediaOptions {
359 #[must_use]
360 pub fn logo<P: AsRef<Path>>(self, logo: P) -> Self {
361 let logo = logo.as_ref();
362 let filename = logo
363 .file_name()
364 .and_then(OsStr::to_str)
365 .map_or_else(String::new, ToString::to_string);
366
367 Self {
368 logo: Some(FileSource::new_from_file(logo, filename, IMAGE_STAR)),
369 ..self
370 }
371 }
372
373 #[must_use]
374 pub fn icon<P: AsRef<Path>>(self, icon: P) -> Self {
375 let icon = icon.as_ref();
376 let filename = icon
377 .file_name()
378 .and_then(OsStr::to_str)
379 .map_or_else(String::new, ToString::to_string);
380
381 Self {
382 icon: Some(FileSource::new_from_file(icon, filename, IMAGE_STAR)),
383 ..self
384 }
385 }
386
387 #[must_use]
388 pub fn header<P: AsRef<Path>>(self, header: P) -> Self {
389 let header = header.as_ref();
390 let filename = header
391 .file_name()
392 .and_then(OsStr::to_str)
393 .map_or_else(String::new, ToString::to_string);
394
395 Self {
396 header: Some(FileSource::new_from_file(header, filename, IMAGE_STAR)),
397 ..self
398 }
399 }
400}
401
402#[doc(hidden)]
403impl From<EditMediaOptions> for Form {
404 fn from(opts: EditMediaOptions) -> Form {
405 let mut form = Form::new();
406 if let Some(logo) = opts.logo {
407 form = form.part("logo", logo.into());
408 }
409 if let Some(icon) = opts.icon {
410 form = form.part("icon", icon.into());
411 }
412 if let Some(header) = opts.header {
413 form = form.part("header", header.into());
414 }
415 form
416 }
417}