1use std::str::FromStr;
6
7use tosho_macros::AutoGetter;
8
9use crate::helper::SubscriptionPlan;
10
11use super::ChapterPosition;
12
13#[derive(Clone, AutoGetter, PartialEq, ::prost::Message)]
15pub struct Chapter {
16 #[prost(uint64, tag = "1")]
18 title_id: u64,
19 #[prost(uint64, tag = "2")]
21 chapter_id: u64,
22 #[prost(string, tag = "3")]
24 title: ::prost::alloc::string::String,
25 #[prost(string, optional, tag = "4")]
27 #[skip_field]
28 subtitle: ::core::option::Option<::prost::alloc::string::String>,
29 #[prost(string, tag = "5")]
31 thumbnail: ::prost::alloc::string::String,
32 #[prost(int64, tag = "6")]
34 published_at: i64,
35 #[prost(int64, optional, tag = "7")]
37 #[skip_field]
38 end_at: ::core::option::Option<i64>,
39 #[prost(bool, tag = "8")]
41 viewed: bool,
42 #[prost(bool, tag = "9")]
44 vertical_only: bool,
45 #[prost(int64, optional, tag = "10")]
47 #[skip_field]
48 ticket_end_at: ::core::option::Option<i64>,
49 #[prost(bool, tag = "11")]
51 free: bool,
52 #[prost(bool, tag = "12")]
54 horizontal_only: bool,
55 #[prost(uint64, tag = "13")]
57 view_count: u64,
58 #[prost(uint64, tag = "14")]
60 comment_count: u64,
61 #[prost(enumeration = "super::ChapterPosition", tag = "999")]
65 #[skip_field]
66 position: i32,
67}
68
69impl Chapter {
70 pub fn is_free(&self) -> bool {
72 if self.free {
73 return true;
74 }
75
76 if self.position() == ChapterPosition::First {
77 return true;
78 }
79
80 if self.position() != ChapterPosition::Middle {
81 if let Some(end_at) = self.end_at {
82 let current_time = chrono::Utc::now().timestamp();
83 current_time < end_at
84 } else {
85 false
86 }
87 } else {
88 false
89 }
90 }
91
92 pub fn is_ticketed(&self) -> bool {
94 if let Some(ticket_end_at) = self.ticket_end_at {
95 let current_time = chrono::Utc::now().timestamp();
96 current_time < ticket_end_at
97 } else {
98 false
99 }
100 }
101
102 pub fn default_view_mode(&self) -> &'static str {
104 if self.vertical_only {
105 "vertical"
106 } else {
107 "horizontal"
108 }
109 }
110
111 pub fn as_chapter_title(&self) -> String {
115 let base_title = self.title.clone();
116 if let Some(subtitle) = self.subtitle.clone() {
117 format!("{base_title} — {subtitle}")
118 } else {
119 base_title
120 }
121 }
122}
123
124#[derive(Clone, AutoGetter, PartialEq, ::prost::Message)]
126pub struct ChapterGroup {
127 #[prost(string, tag = "1")]
129 chapters: ::prost::alloc::string::String,
130 #[prost(message, repeated, tag = "2")]
132 first_chapters: ::prost::alloc::vec::Vec<Chapter>,
133 #[prost(message, repeated, tag = "3")]
135 mid_chapters: ::prost::alloc::vec::Vec<Chapter>,
136 #[prost(message, repeated, tag = "4")]
138 last_chapters: ::prost::alloc::vec::Vec<Chapter>,
139}
140
141impl ChapterGroup {
142 pub fn flatten(&self) -> Vec<Chapter> {
144 let mut chapters = Vec::new();
145 chapters.extend_from_slice(&self.first_chapters);
146 chapters.extend_from_slice(&self.mid_chapters);
147 chapters.extend_from_slice(&self.last_chapters);
148 chapters
149 }
150
151 pub fn first_chapters_mut(&mut self) -> &mut Vec<Chapter> {
153 &mut self.first_chapters
154 }
155
156 pub fn mid_chapters_mut(&mut self) -> &mut Vec<Chapter> {
158 &mut self.mid_chapters
159 }
160
161 pub fn last_chapters_mut(&mut self) -> &mut Vec<Chapter> {
163 &mut self.last_chapters
164 }
165}
166
167#[derive(Clone, AutoGetter, PartialEq, ::prost::Message)]
169pub struct ChapterPage {
170 #[prost(string, tag = "1")]
172 url: ::prost::alloc::string::String,
173 #[prost(uint64, tag = "2")]
175 width: u64,
176 #[prost(uint64, tag = "3")]
178 height: u64,
179 #[prost(enumeration = "super::PageType", tag = "4")]
181 #[skip_field]
182 kind: i32,
183 #[prost(string, optional, tag = "5")]
185 #[skip_field]
186 key: ::core::option::Option<::prost::alloc::string::String>,
187}
188
189impl ChapterPage {
190 pub fn file_name(&self) -> String {
195 let url = self.url.clone();
196 let split: Vec<&str> = url.rsplitn(2, '/').collect();
198 let file_name: Vec<&str> = split[0].split('?').collect();
200 file_name[0].to_string()
201 }
202
203 pub fn extension(&self) -> String {
209 let file_name = self.file_name();
210 let split: Vec<&str> = file_name.rsplitn(2, '.').collect();
212
213 if split.len() == 2 {
214 split[0].to_string()
215 } else {
216 "".to_string()
217 }
218 }
219
220 pub fn file_stem(&self) -> String {
225 let file_name = self.file_name();
226 let split: Vec<&str> = file_name.rsplitn(2, '.').collect();
228
229 if split.len() == 2 {
230 split[1].to_string()
231 } else {
232 file_name
233 }
234 }
235}
236
237#[derive(Clone, AutoGetter, PartialEq, ::prost::Message)]
239pub struct ChapterPageBanner {
240 #[prost(string, optional, tag = "1")]
242 #[skip_field]
243 title: ::core::option::Option<::prost::alloc::string::String>,
244 #[prost(message, repeated, tag = "2")]
246 banners: ::prost::alloc::vec::Vec<super::common::Banner>,
247}
248
249#[derive(Clone, AutoGetter, PartialEq, ::prost::Message)]
251pub struct ChapterPageLastPage {
252 #[prost(message, optional, tag = "1")]
254 chapter: ::core::option::Option<Chapter>,
255 #[prost(message, optional, tag = "2")]
257 next_chapter: ::core::option::Option<Chapter>,
258 #[prost(message, repeated, tag = "3")]
260 top_comments: ::prost::alloc::vec::Vec<super::comments::Comment>,
261 #[prost(bool, tag = "4")]
263 subscribed: bool,
264 #[prost(int64, optional, tag = "5")]
266 #[skip_field]
267 next_chapter_at: ::core::option::Option<i64>,
268 #[prost(enumeration = "super::ChapterType", tag = "6")]
270 #[skip_field]
271 chapter_type: i32,
272 #[prost(message, optional, tag = "9")]
277 banner: ::core::option::Option<super::common::Banner>,
278 #[prost(message, repeated, tag = "10")]
280 title_tickets: ::prost::alloc::vec::Vec<super::titles::Title>,
281 #[prost(message, optional, tag = "11")]
283 publisher_banner: ::core::option::Option<super::common::Banner>,
284 #[prost(message, optional, tag = "12")]
286 #[copyable]
287 user_tickets: ::core::option::Option<super::accounts::UserTickets>,
288 #[prost(bool, tag = "13")]
290 next_chapter_ticket: bool,
291 #[prost(bool, tag = "14")]
293 next_chapter_free: bool,
294 #[prost(bool, tag = "16")]
296 next_chapter_subscription: bool,
297}
298
299#[derive(Clone, AutoGetter, PartialEq, ::prost::Message)]
301pub struct ChapterPageResponse {
302 #[prost(message, optional, tag = "1")]
304 page: ::core::option::Option<ChapterPage>,
305 #[prost(message, optional, tag = "2")]
307 banner: ::core::option::Option<ChapterPageBanner>,
308 #[prost(message, optional, tag = "3")]
310 last_page: ::core::option::Option<ChapterPageLastPage>,
311 #[prost(message, optional, tag = "5")]
313 insert_banner: ::core::option::Option<ChapterPageBanner>,
314}
315
316#[derive(Clone, AutoGetter, PartialEq, ::prost::Message)]
318pub struct ChapterViewer {
319 #[prost(message, repeated, tag = "1")]
321 pages: ::prost::alloc::vec::Vec<ChapterPageResponse>,
322 #[prost(uint64, tag = "2")]
324 chapter_id: u64,
325 #[prost(message, repeated, tag = "3")]
327 chapters: ::prost::alloc::vec::Vec<Chapter>,
328 #[prost(string, tag = "5")]
331 title: ::prost::alloc::string::String,
332 #[prost(string, tag = "6")]
334 chapter_title: ::prost::alloc::string::String,
335 #[prost(uint64, tag = "7")]
337 comment_count: u64,
338 #[prost(bool, tag = "8")]
340 vertical_only: bool,
341 #[prost(uint64, tag = "9")]
343 title_id: u64,
344 #[prost(bool, tag = "10")]
346 first_page_right: bool,
347 #[prost(string, tag = "11")]
349 region_code: ::prost::alloc::string::String,
350 #[prost(bool, tag = "12")]
352 horizontal_only: bool,
353 #[prost(message, optional, tag = "13")]
355 user_subscription: ::core::option::Option<super::accounts::UserSubscription>,
356 #[prost(string, tag = "14")]
358 #[skip_field]
359 plan_type: ::prost::alloc::string::String,
360}
361
362impl ChapterViewer {
363 pub fn plan_type(&self) -> SubscriptionPlan {
368 match SubscriptionPlan::from_str(&self.plan_type) {
369 Ok(plan) => plan,
370 Err(_) => SubscriptionPlan::Basic,
371 }
372 }
373}