A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita
audio
rust
zig
deno
mpris
rockbox
mpd
1use crate::api::rockbox::v1alpha1::browse_service_client::BrowseServiceClient;
2use crate::api::rockbox::v1alpha1::playback_service_client::PlaybackServiceClient;
3use crate::api::rockbox::v1alpha1::playlist_service_client::PlaylistServiceClient;
4use crate::api::rockbox::v1alpha1::{
5 InsertDirectoryRequest, InsertTracksRequest, PlayDirectoryRequest, PlayTrackRequest,
6 TreeGetEntriesRequest, TreeGetEntriesResponse,
7};
8use crate::constants::*;
9use crate::state::AppState;
10use adw::prelude::*;
11use adw::subclass::prelude::*;
12use anyhow::Error;
13use glib::subclass;
14use gtk::glib;
15use gtk::{CompositeTemplate, Image, Label, ListBox, MenuButton};
16use std::cell::{Cell, RefCell};
17use std::{env, thread};
18
19mod imp {
20
21 use super::*;
22
23 #[derive(Debug, Default, CompositeTemplate)]
24 #[template(resource = "/io/github/tsirysndr/Rockbox/gtk/file.ui")]
25 pub struct File {
26 #[template_child]
27 pub file_icon: TemplateChild<Image>,
28 #[template_child]
29 pub file_name: TemplateChild<Label>,
30 #[template_child]
31 pub row: TemplateChild<gtk::Box>,
32 #[template_child]
33 pub file_menu: TemplateChild<MenuButton>,
34 #[template_child]
35 pub directory_menu: TemplateChild<MenuButton>,
36
37 pub files: RefCell<Option<ListBox>>,
38 pub go_back_button: RefCell<Option<gtk::Button>>,
39 pub path: RefCell<String>,
40 pub is_dir: Cell<bool>,
41 pub state: glib::WeakRef<AppState>,
42 }
43
44 #[glib::object_subclass]
45 impl ObjectSubclass for File {
46 const NAME: &'static str = "File";
47 type ParentType = gtk::Box;
48 type Type = super::File;
49
50 fn class_init(klass: &mut Self::Class) {
51 Self::bind_template(klass);
52
53 klass.install_action("app.dir.play-next", None, move |file, _action, _target| {
54 file.play_next();
55 });
56
57 klass.install_action("app.dir.play-last", None, move |file, _action, _target| {
58 file.play_last();
59 });
60
61 klass.install_action(
62 "app.dir.add-shuffled",
63 None,
64 move |file, _action, _target| {
65 file.add_shuffled();
66 },
67 );
68
69 klass.install_action(
70 "app.dir.play-last-shuffled",
71 None,
72 move |file, _action, _target| {
73 file.play_last_shuffled();
74 },
75 );
76
77 klass.install_action(
78 "app.dir.play-shuffled",
79 None,
80 move |file, _action, _target| {
81 file.play(true);
82 },
83 );
84
85 klass.install_action("app.dir.play", None, move |file, _action, _target| {
86 file.play(false);
87 });
88 }
89
90 fn instance_init(obj: &subclass::InitializingObject<Self>) {
91 obj.init_template();
92 }
93 }
94
95 impl ObjectImpl for File {
96 fn constructed(&self) {
97 self.parent_constructed();
98
99 let self_weak = self.downgrade();
100 let click = gtk::GestureClick::new();
101 click.connect_released(move |_, _, _, _| {
102 if let Some(self_) = self_weak.upgrade() {
103 let path = self_.path.borrow();
104 let path = path.clone();
105 let obj = self_.obj();
106
107 if !self_.is_dir.get() {
108 return;
109 }
110
111 let state = self_.state.upgrade().unwrap();
112 state.set_current_path(path.clone().as_str());
113
114 obj.load_files(Some(path));
115 let go_back_button = self_.go_back_button.borrow();
116 if let Some(go_back_button) = go_back_button.as_ref() {
117 go_back_button.set_visible(true);
118 }
119 }
120 });
121
122 self.row.add_controller(click);
123
124 let self_weak = self.downgrade();
125 let gesture = gtk::GestureClick::new();
126 let is_dir = self.is_dir.get();
127 gesture.connect_pressed(move |gestrure, n_press, _, _| {
128 if n_press == 2 && !is_dir {
129 if let Some(self_) = self_weak.upgrade() {
130 let obj = self_.obj();
131 obj.play(false);
132 }
133 }
134 });
135 }
136 }
137
138 impl WidgetImpl for File {}
139 impl BoxImpl for File {}
140
141 impl File {
142 pub fn set_files(&self, files: ListBox) {
143 self.files.replace(Some(files));
144 }
145
146 pub fn set_go_back_button(&self, go_back_button: Option<gtk::Button>) {
147 *self.go_back_button.borrow_mut() = go_back_button;
148 }
149
150 pub fn set_path(&self, path: String) {
151 *self.path.borrow_mut() = path;
152 }
153
154 pub fn set_is_dir(&self, is_dir: bool) {
155 self.is_dir.set(is_dir);
156 match is_dir {
157 true => self.file_icon.set_icon_name(Some("directory-symbolic")),
158 false => self.file_icon.set_icon_name(Some("music-alt-symbolic")),
159 };
160 }
161 }
162}
163
164glib::wrapper! {
165 pub struct File(ObjectSubclass<imp::File>)
166 @extends gtk::Widget, gtk::Box;
167}
168
169#[gtk::template_callbacks]
170impl File {
171 pub fn new() -> Self {
172 glib::Object::new()
173 }
174
175 pub fn load_files(&self, path: Option<String>) {
176 let rt = tokio::runtime::Runtime::new().unwrap();
177 let response_ = rt.block_on(async {
178 let url = build_url();
179 let mut client = BrowseServiceClient::connect(url).await?;
180 let response = client
181 .tree_get_entries(TreeGetEntriesRequest { path: path.clone() })
182 .await?
183 .into_inner();
184 Ok::<TreeGetEntriesResponse, Error>(response)
185 });
186
187 if let Ok(response) = response_ {
188 let files = self.imp().files.borrow();
189 let files_ref = files.as_ref();
190 let files_ref = files_ref.unwrap();
191
192 while let Some(file) = files_ref.first_child() {
193 files_ref.remove(&file);
194 }
195
196 let state = self.imp().state.upgrade().unwrap();
197
198 for entry in response.entries {
199 let file = File::new();
200 let filename = entry.name.split("/").last().unwrap();
201 file.imp().set_files(files_ref.clone());
202 file.imp()
203 .set_go_back_button(self.imp().go_back_button.borrow().clone());
204 file.imp().file_name.set_text(filename);
205 file.imp().state.set(Some(&state));
206 file.imp().set_path(entry.name.clone());
207 file.imp().set_is_dir(entry.attr == 16);
208
209 match entry.attr == 16 {
210 true => {
211 file.imp().file_menu.set_visible(false);
212 file.imp().directory_menu.set_visible(true);
213 }
214 false => {
215 file.imp().file_menu.set_visible(true);
216 file.imp().directory_menu.set_visible(false);
217 }
218 }
219
220 files_ref.append(&file);
221 }
222 }
223 }
224
225 pub fn play_next(&self) {
226 let path = self.imp().path.borrow();
227 let path = path.clone();
228 let is_dir = self.imp().is_dir.get();
229 thread::spawn(move || {
230 let rt = tokio::runtime::Runtime::new().unwrap();
231 let url = build_url();
232 let _ = rt.block_on(async {
233 let mut client = PlaylistServiceClient::connect(url).await?;
234 match is_dir {
235 true => {
236 client
237 .insert_directory(InsertDirectoryRequest {
238 directory: path,
239 position: PLAYLIST_INSERT_FIRST,
240 ..Default::default()
241 })
242 .await?;
243 }
244 false => {
245 client
246 .insert_tracks(InsertTracksRequest {
247 tracks: vec![path],
248 position: PLAYLIST_INSERT_FIRST,
249 ..Default::default()
250 })
251 .await?;
252 }
253 }
254 Ok::<(), Error>(())
255 });
256 });
257 }
258
259 pub fn play_last(&self) {
260 let path = self.imp().path.borrow();
261 let path = path.clone();
262 let is_dir = self.imp().is_dir.get();
263 thread::spawn(move || {
264 let rt = tokio::runtime::Runtime::new().unwrap();
265 let url = build_url();
266 let _ = rt.block_on(async {
267 let mut client = PlaylistServiceClient::connect(url).await?;
268 match is_dir {
269 true => {
270 client
271 .insert_directory(InsertDirectoryRequest {
272 directory: path,
273 position: PLAYLIST_INSERT_LAST,
274 ..Default::default()
275 })
276 .await?;
277 }
278 false => {
279 client
280 .insert_tracks(InsertTracksRequest {
281 tracks: vec![path],
282 position: PLAYLIST_INSERT_LAST,
283 ..Default::default()
284 })
285 .await?;
286 }
287 }
288 Ok::<(), Error>(())
289 });
290 });
291 }
292
293 pub fn add_shuffled(&self) {
294 let path = self.imp().path.borrow();
295 let path = path.clone();
296 let is_dir = self.imp().is_dir.get();
297 thread::spawn(move || {
298 let rt = tokio::runtime::Runtime::new().unwrap();
299 let url = build_url();
300 let _ = rt.block_on(async {
301 let mut client = PlaylistServiceClient::connect(url).await?;
302 match is_dir {
303 true => {
304 client
305 .insert_directory(InsertDirectoryRequest {
306 directory: path,
307 position: PLAYLIST_INSERT_SHUFFLED,
308 ..Default::default()
309 })
310 .await?;
311 }
312 false => {
313 client
314 .insert_tracks(InsertTracksRequest {
315 tracks: vec![path],
316 position: PLAYLIST_INSERT_SHUFFLED,
317 ..Default::default()
318 })
319 .await?;
320 }
321 }
322 Ok::<(), Error>(())
323 });
324 });
325 }
326
327 pub fn play_last_shuffled(&self) {
328 let path = self.imp().path.borrow();
329 let path = path.clone();
330 let is_dir = self.imp().is_dir.get();
331 thread::spawn(move || {
332 let rt = tokio::runtime::Runtime::new().unwrap();
333 let url = build_url();
334 let _ = rt.block_on(async {
335 let mut client = PlaylistServiceClient::connect(url).await?;
336 match is_dir {
337 true => {
338 client
339 .insert_directory(InsertDirectoryRequest {
340 directory: path,
341 position: PLAYLIST_INSERT_LAST_SHUFFLED,
342 ..Default::default()
343 })
344 .await?;
345 }
346 false => {
347 client
348 .insert_tracks(InsertTracksRequest {
349 tracks: vec![path],
350 position: PLAYLIST_INSERT_LAST_SHUFFLED,
351 ..Default::default()
352 })
353 .await?;
354 }
355 }
356 Ok::<(), Error>(())
357 });
358 });
359 }
360
361 pub fn play(&self, shuffle: bool) {
362 let path = self.imp().path.borrow();
363 let path = path.clone();
364 let is_dir = self.imp().is_dir.get();
365 thread::spawn(move || {
366 let rt = tokio::runtime::Runtime::new().unwrap();
367 let url = build_url();
368 let _ = rt.block_on(async {
369 let mut client = PlaybackServiceClient::connect(url).await?;
370 match is_dir {
371 true => {
372 client
373 .play_directory(PlayDirectoryRequest {
374 path,
375 shuffle: Some(shuffle),
376 recurse: Some(false),
377 ..Default::default()
378 })
379 .await?;
380 }
381 false => {
382 client.play_track(PlayTrackRequest { path }).await?;
383 }
384 }
385 Ok::<(), Error>(())
386 });
387 });
388 }
389}
390
391fn build_url() -> String {
392 let host = env::var("ROCKBOX_HOST").unwrap_or("localhost".to_string());
393 let port = env::var("ROCKBOX_PORT").unwrap_or("6061".to_string());
394 format!("tcp://{}:{}", host, port)
395}