1use crate::dataloading::dataprovider::song_data_provider::{
2 SongChange, SongDataEdit, SongDataSource,
3};
4use crate::traktor_api::{TraktorNextMode, TraktorSyncMode, TRAKTOR_SERVER_DEFAULT_ADDR};
5use crate::ui::material_icon;
6use crate::ui::widget::dynamic_text_input::DynamicTextInput;
7use crate::{DanceInterpreter, Message, Window};
8use iced::advanced::Widget;
9use iced::alignment::Vertical;
10use iced::border::Radius;
11use iced::widget::scrollable::{Direction, RelativeOffset, Scrollbar};
12use iced::widget::{
13 button, checkbox, column as col, radio, row, scrollable, text, Button, Column, Row,
14 Scrollable, Space,
15};
16use iced::{font, window, Border, Color, Element, Font, Length, Renderer, Size, Theme};
17use iced_aw::menu::Item;
18use iced_aw::style::{menu_bar::primary, Status};
19use iced_aw::widget::InnerBounds;
20use iced_aw::{iced_aw_font, menu, menu_bar, menu_items, quad, Menu, MenuBar};
21use network_interface::Addr::V4;
22use network_interface::{NetworkInterface, NetworkInterfaceConfig};
23use std::sync::LazyLock;
24
25pub struct ConfigWindow {
26 pub id: window::Id,
27 pub closed: bool,
28 pub size: Size,
29
30 pub enable_autoscroll: bool,
31 pub theme: Theme,
32}
33
34pub static PLAYLIST_SCROLLABLE_ID: LazyLock<iced::widget::Id> =
35 LazyLock::new(iced::widget::Id::unique);
36
37impl Window for ConfigWindow {
38 fn new(id: window::Id) -> Self {
39 Self {
40 id,
41 closed: false,
42 size: Size::default(),
43
44 enable_autoscroll: true,
45 theme: Theme::Dark,
46 }
47 }
48
49 fn on_resize(&mut self, size: Size) {
50 self.size = size;
51 }
52
53 fn on_close(&mut self) {
54 self.closed = true;
55 }
56
57 fn is_closed(&self) -> bool {
58 self.closed
59 }
60}
61
62impl ConfigWindow {
63 pub fn view<'a>(&'a self, dance_interpreter: &'a DanceInterpreter) -> Element<'a, Message> {
64 let menu_bar = self.build_menu_bar(dance_interpreter);
65 let playlist_view = self.build_playlist_view(dance_interpreter);
66 let statics_view = self.build_statics_view(dance_interpreter);
67
68 let content = col![menu_bar, playlist_view, statics_view];
69 content.into()
70 }
71
72 fn get_play_state(
73 &self,
74 dance_interpreter: &DanceInterpreter,
75 playlist_index: usize,
76 ) -> (bool, bool, bool, bool) {
77 let mut is_current = false;
78 let mut is_next = false;
79 let mut is_traktor = false;
80 let is_played = dance_interpreter
81 .data_provider
82 .playlist_played
83 .get(playlist_index)
84 .copied()
85 .unwrap_or(false);
86
87 if let SongDataSource::Playlist(i) = dance_interpreter.data_provider.current {
88 is_current = playlist_index == i;
89 is_next = playlist_index == (i + 1);
90 }
91
92 if let Some(SongDataSource::Playlist(i)) = dance_interpreter.data_provider.next {
93 is_next = playlist_index == i;
94 }
95
96 if matches!(
97 dance_interpreter.data_provider.current,
98 SongDataSource::Traktor
99 ) && let Some(index) = dance_interpreter.data_provider.get_current_traktor_index()
100 {
101 is_traktor = playlist_index == index;
102 }
103
104 (is_current, is_next, is_traktor, is_played)
105 }
106
107 fn build_playlist_view(&'_ self, dance_interpreter: &DanceInterpreter) -> Column<'_, Message> {
108 let trow: Row<_> = row![
109 text!("#").width(Length::Fixed(24.0)),
110 text!("Title").width(Length::Fill),
111 text!("Artist").width(Length::Fill),
112 text!("Dance").width(Length::Fill),
113 Space::new().width(Length::Fill).height(Length::Shrink),
114 Space::new()
115 .width(Length::Fixed(10.0))
116 .height(Length::Shrink),
117 ]
118 .spacing(5);
119
120 let mut playlist_column: Column<'_, _, _, _> = col!().spacing(5);
121
122 for (i, song) in dance_interpreter
123 .data_provider
124 .playlist_songs
125 .iter()
126 .enumerate()
127 {
128 let (is_current, is_next, is_traktor, is_played) =
129 self.get_play_state(dance_interpreter, i);
130 let icon: Element<Message> = if is_traktor {
131 material_icon("agriculture")
132 .width(Length::Fixed(24.0))
133 .into()
134 } else if is_current {
135 material_icon("play_arrow")
136 .width(Length::Fixed(24.0))
137 .into()
138 } else if is_next {
139 material_icon("skip_next").width(Length::Fixed(24.0)).into()
140 } else if is_played {
141 material_icon("check").width(Length::Fixed(24.0)).into()
142 } else {
143 Space::new()
144 .width(Length::Fixed(24.0))
145 .height(Length::Shrink)
146 .into()
147 };
148
149 let song_row = row![
150 icon,
151 DynamicTextInput::<'_, Message>::new("Title", &song.title)
152 .width(Length::Fill)
153 .on_change(move |v| Message::SongDataEdit(i, SongDataEdit::Title(v))),
154 DynamicTextInput::<'_, Message>::new("Artist", &song.artist)
155 .width(Length::Fill)
156 .on_change(move |v| Message::SongDataEdit(i, SongDataEdit::Artist(v))),
157 DynamicTextInput::<'_, Message>::new("Dance", &song.dance)
158 .width(Length::Fill)
159 .on_change(move |v| Message::SongDataEdit(i, SongDataEdit::Dance(v))),
160 row![
161 Space::new().width(Length::Fill).height(Length::Shrink),
162 material_icon_message_button(
163 "smart_display",
164 Message::SongChanged(SongChange::PlaylistAbsolute(i))
165 ),
166 material_icon_message_button(
167 "queue_play_next",
168 Message::SetNextSong(SongDataSource::Playlist(i))
169 ),
170 material_icon_message_button(
171 "delete",
172 Message::DeleteSong(SongDataSource::Playlist(i))
173 ),
174 ]
175 .spacing(5)
176 .width(Length::Fill),
177 ]
178 .spacing(5);
179
180 if !playlist_column.children().is_empty() {
181 playlist_column = playlist_column.push(separator());
182 }
183
184 playlist_column = playlist_column.push(song_row);
185 }
186
187 let playlist_scrollable: Scrollable<'_, Message> = scrollable(playlist_column)
188 .width(Length::Fill)
189 .height(Length::Fill)
190 .spacing(5)
191 .id(PLAYLIST_SCROLLABLE_ID.clone());
192
193 col!(trow, playlist_scrollable).spacing(5)
194 }
195
196 fn build_statics_view<'a>(
197 &self,
198 dance_interpreter: &'a DanceInterpreter,
199 ) -> Scrollable<'a, Message> {
200 let bold_font = Font {
201 family: font::Family::SansSerif,
202 weight: font::Weight::Bold,
203 stretch: font::Stretch::Normal,
204 style: font::Style::Normal,
205 };
206
207 let btn_blank: Button<Message> =
208 button(text("Blank").align_y(Vertical::Center).font(bold_font))
209 .style(button::secondary)
210 .on_press(Message::SongChanged(SongChange::Blank));
211 let btn_traktor: Button<Message> =
212 button(text("Traktor").align_y(Vertical::Center).font(bold_font))
213 .style(button::secondary)
214 .on_press(Message::SongChanged(SongChange::Traktor));
215 let mut statics: Vec<Element<_>> = dance_interpreter
216 .data_provider
217 .statics
218 .iter()
219 .enumerate()
220 .map(|(idx, s)| {
221 button(text(&s.dance).font(bold_font))
222 .style(button::secondary)
223 .on_press(Message::SongChanged(SongChange::StaticAbsolute(idx)))
224 .into()
225 })
226 .collect();
227
228 statics.insert(0, btn_blank.into());
229 statics.insert(1, btn_traktor.into());
230
231 scrollable(row(statics).spacing(5))
232 .direction(Direction::Horizontal(Scrollbar::new()))
233 .spacing(5)
234 .width(Length::Fill)
235 }
236
237 fn build_menu_bar<'a>(
238 &self,
239 dance_interpreter: &'a DanceInterpreter,
240 ) -> MenuBar<'a, Message, Theme, Renderer> {
241 let menu_tpl_1 = |items| Menu::new(items).max_width(150.0).offset(15.0).spacing(5.0);
242 let menu_tpl_2 = |items| Menu::new(items).max_width(150.0).offset(0.0).spacing(5.0);
243
244 #[rustfmt::skip]
245 let mb = menu_bar!
246 (
247 (
248 label_message_button_shrink("File", Message::Noop),
249 menu_tpl_1(
250 menu_items!(
251 (label_message_button_fill("Open Playlist File", Message::OpenPlaylist)),
252 (label_message_button_fill("Exit", Message::WindowClosed(self.id))),
253 )
254 )
255 .spacing(5.0)
256 ),
257 (
258 label_message_button_shrink("Edit", Message::Noop),
259 menu_tpl_1(
260 menu_items!(
261 (labeled_message_checkbox("Autoscroll", self.enable_autoscroll, Message::EnableAutoscroll)),
262 (label_message_button_fill("Reload Statics", Message::ReloadStatics)),
263 (label_message_button_fill("Add blank song", Message::AddBlankSong(RelativeOffset::END))),
264 )
265 )
266 .spacing(5.0)
267 ),
268 (
269 label_message_button_shrink("SongWindow", Message::Noop),
270 menu_tpl_1(
271 menu_items!(
272 (labeled_message_checkbox("Show Thumbnails", dance_interpreter.song_window.enable_image, Message::EnableImage)),
273 (labeled_message_checkbox("Show Next Dance", dance_interpreter.song_window.enable_next_dance, Message::EnableNextDance)),
274 )
275 )
276 .spacing(5.0)
277 ),
278 (
279 label_message_button_shrink("Traktor", Message::Noop),
280 menu_tpl_1(
281 menu_items!(
282 (labeled_message_checkbox("Enable Server", dance_interpreter.data_provider.traktor_provider.is_enabled, Message::TraktorEnableServer)),
283 (
284 labeled_dynamic_text_input("Server Address", TRAKTOR_SERVER_DEFAULT_ADDR, dance_interpreter.data_provider.traktor_provider.address.as_str(),
285 Message::TraktorChangeAddress, Some(Message::TraktorSubmitAddress)),
286 menu_tpl_2(get_network_interface_menu(dance_interpreter))
287 ),
288 (separator()),
289 (labeled_message_checkbox_opt("Enable Debug Logging", dance_interpreter.data_provider.traktor_provider.debug_logging,
290 dance_interpreter.data_provider.traktor_provider.is_enabled.then_some(Message::TraktorEnableDebugLogging))),
291 (label_message_button_fill_opt("Reset Connection", dance_interpreter.data_provider.traktor_provider.is_enabled.then_some(Message::TraktorReconnect))),
292 (separator()),
293 (
294 submenu_button("Sync Mode"),
295 menu_tpl_2(
296 menu_items!(
297 (labeled_message_radio("None", true,
298 Some(dance_interpreter.data_provider.traktor_provider.sync_mode.is_none()), |_| Message::TraktorSetSyncMode(None))),
299 (labeled_message_radio("X Fader", TraktorSyncMode::Relative,
300 dance_interpreter.data_provider.traktor_provider.sync_mode, |v| Message::TraktorSetSyncMode(Some(v)))),
301 (labeled_message_radio("By Track Number", TraktorSyncMode::AbsoluteByNumber,
302 dance_interpreter.data_provider.traktor_provider.sync_mode, |v| Message::TraktorSetSyncMode(Some(v)))),
303 (labeled_message_radio("By Title / Artist", TraktorSyncMode::AbsoluteByName,
304 dance_interpreter.data_provider.traktor_provider.sync_mode, |v| Message::TraktorSetSyncMode(Some(v)))),
305 )
306 )
307 ),
308 (
309 submenu_button("Next Song Mode"),
310 menu_tpl_2(
311 menu_items!(
312 (labeled_message_radio("None", true,
313 Some(dance_interpreter.data_provider.traktor_provider.next_mode.is_none()), |_| Message::TraktorSetNextMode(None))),
314 (labeled_message_radio("From other Deck (by Position)", TraktorNextMode::DeckByPosition,
315 dance_interpreter.data_provider.traktor_provider.next_mode, |v| Message::TraktorSetNextMode(Some(v)))),
316 (labeled_message_radio("From other Deck (by Track Number)", TraktorNextMode::DeckByNumber,
317 dance_interpreter.data_provider.traktor_provider.next_mode, |v| Message::TraktorSetNextMode(Some(v)))),
318 (labeled_message_radio("From Playlist (by Track Number)", TraktorNextMode::PlaylistByNumber,
319 dance_interpreter.data_provider.traktor_provider.next_mode, |v| Message::TraktorSetNextMode(Some(v)))),
320 (labeled_message_radio("From Playlist (by Title / Artist)", TraktorNextMode::PlaylistByName,
321 dance_interpreter.data_provider.traktor_provider.next_mode, |v| Message::TraktorSetNextMode(Some(v)))),
322 )
323 )
324 ),
325 (
326 submenu_button("Next Song Mode (Fallback)"),
327 menu_tpl_2(
328 menu_items!(
329 (labeled_message_radio("None", true,
330 Some(dance_interpreter.data_provider.traktor_provider.next_mode_fallback.is_none()), |_| Message::TraktorSetNextModeFallback(None))),
331 (labeled_message_radio("From other Deck (by Position)", TraktorNextMode::DeckByPosition,
332 dance_interpreter.data_provider.traktor_provider.next_mode_fallback, |v| Message::TraktorSetNextModeFallback(Some(v)))),
333 (labeled_message_radio("From other Deck (by Track Number)", TraktorNextMode::DeckByNumber,
334 dance_interpreter.data_provider.traktor_provider.next_mode_fallback, |v| Message::TraktorSetNextModeFallback(Some(v)))),
335 (labeled_message_radio("From Playlist (by Track Number)", TraktorNextMode::PlaylistByNumber,
336 dance_interpreter.data_provider.traktor_provider.next_mode_fallback, |v| Message::TraktorSetNextModeFallback(Some(v)))),
337 (labeled_message_radio("From Playlist (by Title / Artist)", TraktorNextMode::PlaylistByName,
338 dance_interpreter.data_provider.traktor_provider.next_mode_fallback, |v| Message::TraktorSetNextModeFallback(Some(v)))),
339 )
340 )
341 )
342 )
343 )
344 .spacing(5.0)
345 )
346 )
347 .spacing(5.0)
348 .draw_path(menu::DrawPath::Backdrop)
349 .style(|theme:&iced::Theme, status: Status | menu::Style{
350 path_border: Border{
351 radius: Radius::new(6.0),
352 ..Default::default()
353 },
354 ..primary(theme, status)
355 });
356
357 mb
358 }
359}
360
361fn label_message_button_fill<'a>(
362 label: impl text::IntoFragment<'a>,
363 message: Message,
364) -> button::Button<'a, Message> {
365 label_message_button(label, message).width(Length::Fill)
366}
367
368fn label_message_button_shrink<'a>(
369 label: impl text::IntoFragment<'a>,
370 message: Message,
371) -> button::Button<'a, Message> {
372 label_message_button(label, message).width(Length::Shrink)
373}
374
375fn label_message_button<'a>(
376 label: impl text::IntoFragment<'a>,
377 message: Message,
378) -> button::Button<'a, Message> {
379 button(text(label).align_y(Vertical::Center))
380 .padding([4, 8])
381 .style(button::secondary)
382 .on_press(message)
383}
384
385fn submenu_button(label: &'_ str) -> button::Button<'_, Message, iced::Theme, iced::Renderer> {
386 button(
387 row![
388 text(label).width(Length::Fill).align_y(Vertical::Center),
389 iced_aw_font::right_open()
390 .width(Length::Shrink)
391 .align_y(Vertical::Center),
392 ]
393 .align_y(iced::Alignment::Center),
394 )
395 .padding([4, 8])
396 .style(button::text)
397 .on_press(Message::Noop)
398 .width(Length::Fill)
399}
400
401fn label_message_button_opt(
402 label: &'_ str,
403 message: Option<Message>,
404) -> button::Button<'_, Message> {
405 if let Some(message) = message {
406 label_message_button(label, message)
407 } else {
408 button(text(label).align_y(Vertical::Center))
409 .padding([4, 8])
410 .style(button::secondary)
411 }
412}
413
414fn label_message_button_fill_opt(
415 label: &'_ str,
416 message: Option<Message>,
417) -> button::Button<'_, Message> {
418 label_message_button_opt(label, message).width(Length::Fill)
419}
420
421fn material_icon_message_button(icon_id: &'_ str, message: Message) -> button::Button<'_, Message> {
422 button(material_icon(icon_id))
423 .padding([4, 8])
424 .style(button::secondary)
425 .on_press(message)
426 .width(Length::Shrink)
427}
428
429fn labeled_message_checkbox(
430 label: &'_ str,
431 checked: bool,
432 message: fn(bool) -> Message,
433) -> checkbox::Checkbox<'_, Message> {
434 checkbox(checked)
435 .label(label)
436 .on_toggle(message)
437 .width(Length::Fill)
438 }
440
441fn labeled_message_radio<T: Copy + Eq>(
442 label: &'_ str,
443 value: T,
444 selection: Option<T>,
445 message: fn(T) -> Message,
446) -> radio::Radio<'_, Message> {
447 radio(label, value, selection, message).width(Length::Fill)
448 }
450
451fn labeled_message_checkbox_opt(
452 label: &'_ str,
453 checked: bool,
454 message: Option<fn(bool) -> Message>,
455) -> checkbox::Checkbox<'_, Message> {
456 if let Some(message) = message {
457 labeled_message_checkbox(label, checked, message)
458 } else {
459 checkbox(checked).label(label).width(Length::Fill)
460 }
462}
463
464fn labeled_dynamic_text_input<'a>(
465 label: &'a str,
466 placeholder: &'a str,
467 value: &'a str,
468 message: fn(String) -> Message,
469 submit_message: Option<Message>,
470) -> Column<'a, Message> {
471 let mut input = DynamicTextInput::<Message>::new(placeholder, value)
472 .width(Length::Fill)
473 .on_change(message);
474
475 if let Some(submit_message) = submit_message {
476 input = input.on_submit(submit_message);
477 }
478
479 col!(text(label).width(Length::Fill), input,).width(Length::Fill)
480}
481
482fn separator() -> quad::Quad {
483 quad::Quad {
484 quad_color: Color::from([0.5; 3]).into(),
485 quad_border: Border {
486 radius: Radius::new(2.0),
487 ..Default::default()
488 },
489 inner_bounds: InnerBounds::Ratio(1.0, 0.2),
490 height: Length::Fixed(5.0),
491 width: Length::Fill,
492 ..Default::default()
493 }
494}
495
496fn get_network_interface_menu(
497 dance_interpreter: &'_ DanceInterpreter,
498) -> Vec<Item<'_, Message, Theme, Renderer>> {
499 let mut interfaces = vec![("any".to_owned(), "0.0.0.0".to_owned())];
500
501 if let Ok(network_interfaces) = NetworkInterface::show() {
502 for i in network_interfaces {
503 for addr in i.addr {
504 let V4(ipv4_addr) = addr else {
505 continue;
506 };
507
508 interfaces.push((i.name.clone(), ipv4_addr.ip.to_string()));
509 }
510 }
511 }
512
513 let original_addr = dance_interpreter
514 .data_provider
515 .traktor_provider
516 .get_socket_addr()
517 .unwrap_or(TRAKTOR_SERVER_DEFAULT_ADDR.parse().unwrap());
518 let original_port = original_addr.port();
519
520 let interfaces = interfaces
521 .into_iter()
522 .map(|(name, addr)| (name, addr.clone(), format!("{}:{}", addr, original_port)));
523
524 interfaces
525 .into_iter()
526 .map(|(name, addr, addr_with_port)| {
527 Item::new(label_message_button_fill(
528 format!("{}: {}", name, addr),
529 Message::TraktorChangeAndSubmitAddress(addr_with_port),
530 ))
531 })
532 .collect()
533}