A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita
audio
rust
zig
deno
mpris
rockbox
mpd
1/***************************************************************************
2 * __________ __ ___.
3 * Open \______ \ ____ ____ | | _\_ |__ _______ ___
4 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
5 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
6 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
7 * \/ \/ \/ \/ \/
8 * $Id$
9 *
10 * Copyright (C) 2002 Daniel Stenberg
11 *
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU General Public License
14 * as published by the Free Software Foundation; either version 2
15 * of the License, or (at your option) any later version.
16 *
17 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
18 * KIND, either express or implied.
19 *
20 ****************************************************************************/
21#include <stdio.h>
22#include <stdlib.h>
23#include <stdbool.h>
24#include "string-extra.h"
25#include "panic.h"
26
27#include "applimits.h"
28#include "dir.h"
29#include "file.h"
30#include "lcd.h"
31#include "font.h"
32#include "button.h"
33#include "kernel.h"
34#include "usb.h"
35#include "tree.h"
36#include "audio.h"
37#include "playlist.h"
38#include "menu.h"
39#include "skin_engine/skin_engine.h"
40#include "settings.h"
41#include "debug.h"
42#include "storage.h"
43#include "rolo.h"
44#include "icons.h"
45#include "lang.h"
46#include "screens.h"
47#include "keyboard.h"
48#include "bookmark.h"
49#include "onplay.h"
50#include "core_alloc.h"
51#include "power.h"
52#include "action.h"
53#include "talk.h"
54#include "filetypes.h"
55#include "misc.h"
56#include "pathfuncs.h"
57#include "filetree.h"
58#include "tagtree.h"
59#ifdef HAVE_RECORDING
60#include "recorder/recording.h"
61#endif
62#include "rtc.h"
63#include "dircache.h"
64#ifdef HAVE_TAGCACHE
65#include "tagcache.h"
66#endif
67#include "yesno.h"
68#include "eeprom_settings.h"
69#include "playlist_catalog.h"
70
71/* gui api */
72#include "list.h"
73#include "splash.h"
74#include "quickscreen.h"
75#include "shortcuts.h"
76#include "appevents.h"
77
78#include "root_menu.h"
79
80static struct gui_synclist tree_lists;
81
82/* I put it here because other files doesn't use it yet,
83 * but should be elsewhere since it will be used mostly everywhere */
84static struct tree_context tc;
85
86char lastfile[MAX_PATH];
87static char lastdir[MAX_PATH];
88#ifdef HAVE_TAGCACHE
89static int lasttable, lastextra;
90#endif
91
92static bool reload_dir = false;
93
94static bool start_wps = false;
95static int curr_context = false;/* id3db or tree*/
96
97static int dirbrowse(void);
98static int ft_play_dirname(char* name);
99static int ft_play_filename(char *dir, char *file, int attr);
100static void say_filetype(int attr);
101
102struct entry* tree_get_entries(struct tree_context *t)
103{
104 return core_get_data(t->cache.entries_handle);
105}
106
107struct entry* tree_get_entry_at(struct tree_context *t, int index)
108{
109 if(index < 0 || index >= t->cache.max_entries)
110 return NULL; /* no entry */
111 struct entry* entries = tree_get_entries(t);
112 return &entries[index];
113}
114
115static struct entry *get_valid_entry(const char* funcname,
116 struct tree_context *t, int index)
117{
118 struct entry *entry = tree_get_entry_at(t, index);
119 if (!entry)
120 panicf("Invalid tree entry %s", funcname);
121 /*DEBUGF("%s tc: %x idx: %d\n", funcname, t, index);*/
122 return entry;
123}
124
125static bool ext_stripit(bool isdir, int attr, int dirfilter)
126{
127 if((dirfilter != SHOW_ID3DB) && !isdir)
128 {
129 switch(global_settings.show_filename_ext)
130 {
131 case 0:
132 /* show file extension: off */
133 return true;
134 break;
135 case 1:
136 /* show file extension: on */
137 break;
138 case 2:
139 /* show file extension: only unknown types */
140 return filetype_supported(attr);
141 case 3:
142 default:
143 /* show file extension: only when viewing all */
144 return (dirfilter != SHOW_ALL);
145 }
146 }
147 return false;
148}
149
150static const char* tree_get_filename(int selected_item, void *data,
151 char *buffer, size_t buffer_len)
152{
153 struct tree_context * local_tc=(struct tree_context *)data;
154 char *name;
155 int attr=0;
156#ifdef HAVE_TAGCACHE
157 bool id3db = *(local_tc->dirfilter) == SHOW_ID3DB;
158
159 if (id3db)
160 {
161 return tagtree_get_entry_name(&tc, selected_item, buffer, buffer_len);
162 }
163 else
164#endif
165 {
166 struct entry *entry = get_valid_entry(__func__, local_tc, selected_item);
167 name = entry->name;
168 attr = entry->attr;
169 }
170
171 if(ext_stripit((attr & ATTR_DIRECTORY), attr, *(local_tc->dirfilter)))
172 {
173 return(strip_extension(buffer, buffer_len, name));
174 }
175 return(name);
176}
177
178#ifdef HAVE_LCD_COLOR
179static int tree_get_filecolor(int selected_item, void * data)
180{
181 if (*tc.dirfilter == SHOW_ID3DB)
182 return -1;
183 struct tree_context * local_tc=(struct tree_context *)data;
184 struct entry *entry = get_valid_entry(__func__, local_tc, selected_item);
185
186 return filetype_get_color(entry->name, entry->attr);
187}
188#endif
189
190static enum themable_icons tree_get_fileicon(int selected_item, void * data)
191{
192 struct tree_context * local_tc=(struct tree_context *)data;
193#ifdef HAVE_TAGCACHE
194 bool id3db = *(local_tc->dirfilter) == SHOW_ID3DB;
195 if (id3db) {
196 return tagtree_get_icon(&tc);
197 }
198 else
199#endif
200 {
201 struct entry *entry = get_valid_entry(__func__, local_tc, selected_item);
202
203 return filetype_get_icon(entry->attr);
204 }
205}
206
207static int tree_voice_cb(int selected_item, void * data)
208{
209 struct tree_context * local_tc=(struct tree_context *)data;
210 unsigned char *name;
211 int attr=0;
212 int customaction = ONPLAY_NO_CUSTOMACTION;
213#ifdef HAVE_TAGCACHE
214 bool id3db = *(local_tc->dirfilter) == SHOW_ID3DB;
215 char buf[AVERAGE_FILENAME_LENGTH*2];
216
217 if (id3db)
218 {
219 attr = tagtree_get_attr(local_tc);
220 name = tagtree_get_entry_name(local_tc, selected_item, buf, sizeof(buf));
221 customaction = tagtree_get_custom_action(local_tc);
222
223 /* See if name is an encoded ID, if it is, then speak it normally */
224 int lang_id = P2ID(name);
225 /*debugf("%s Found name %s id %d\n", __func__, P2STR(name), lang_id);*/
226 if (lang_id >= 0) {
227 if (global_settings.talk_menu)
228 talk_id(lang_id, true);
229 return 0;
230 }
231
232 /* Otherwise, it is either a custom "header" or a database entry,
233 so try to look up a talk clip for it. */
234
235 // XXX this needs further work, so disable it for now
236 // -- need to distinguish between "headers" and entries
237 // each entry type ("artist", "album", etc) should be delineated
238 // so we can split the clips into subdirs.
239#if 0
240 if (global_settings.talk_file_clip) {
241 if (talk_file(LANG_DIR"/database/", NULL,
242 P2STR(name), file_thumbnail_ext, NULL, true) > 0)
243 return 0;
244
245 // XXX fall back to spelling it out?
246 return 0;
247 }
248#endif
249 }
250 else
251#endif
252 {
253 struct entry *entry = get_valid_entry(__func__, local_tc, selected_item);
254 name = entry->name;
255 attr = entry->attr;
256 }
257 bool is_dir = (attr & ATTR_DIRECTORY);
258 bool did_clip = false;
259 /* First the .talk clip case */
260 if(is_dir)
261 {
262 if(global_settings.talk_dir_clip)
263 {
264 did_clip = true;
265 if (ft_play_dirname(name) <= 0)
266 /* failed, not existing */
267 did_clip = false;
268 }
269 } else { /* it's a file */
270 if (global_settings.talk_file_clip && (attr & FILE_ATTR_THUMBNAIL))
271 {
272 did_clip = true;
273 if (ft_play_filename(local_tc->currdir, name, attr) <= 0)
274 /* failed, not existing */
275 did_clip = false;
276 }
277 }
278 bool spell_name = (customaction == ONPLAY_CUSTOMACTION_FIRSTLETTER);
279 if(!did_clip)
280 {
281 /* say the number or spell if required or as a fallback */
282 switch (is_dir ? global_settings.talk_dir : global_settings.talk_file)
283 {
284 case 1: /* as numbers */
285 talk_id(is_dir ? VOICE_DIR : VOICE_FILE, false);
286 talk_number(selected_item+1 - (is_dir ? 0 : local_tc->dirsindir),
287 true);
288 break;
289 case 2: /* spelled */
290 talk_shutup();
291 if(global_settings.talk_filetype)
292 {
293 if(is_dir)
294 talk_id(VOICE_DIR, true);
295 }
296 spell_name = true;
297 break;
298 }
299 }
300
301 if(global_settings.talk_filetype && !is_dir
302 && *local_tc->dirfilter < NUM_FILTER_MODES)
303 {
304 say_filetype(attr);
305 }
306
307 /* spell name AFTER voicing filetype */
308 if (spell_name) {
309 bool stripit = ext_stripit(is_dir, attr, *(local_tc->dirfilter));
310 char *ext = NULL;
311
312 /* Don't spell the extension if it's not displayed */
313
314 if (stripit) {
315 ext = strrchr(name, '.');
316 if (ext)
317 *ext = 0;
318 }
319
320 talk_spell(name, true);
321
322 if (stripit && ext)
323 *ext = '.';
324 }
325
326 return 0;
327}
328
329bool check_rockboxdir(void)
330{
331 if(!dir_exists(ROCKBOX_DIR))
332 { /* No need to localise this message.
333 If .rockbox is missing, it wouldn't work anyway */
334 FOR_NB_SCREENS(i)
335 screens[i].clear_display();
336 splash(HZ*2, "No .rockbox directory");
337 FOR_NB_SCREENS(i)
338 screens[i].clear_display();
339 splash(HZ*2, "Installation incomplete");
340 return false;
341 }
342 return true;
343}
344
345/* do this really late in the init sequence */
346void tree_init(void)
347{
348 check_rockboxdir();
349 strcpy(tc.currdir, "/");
350}
351
352struct tree_context* tree_get_context(void)
353{
354 return &tc;
355}
356
357void tree_lock_cache(struct tree_context *t)
358{
359 core_pin(t->cache.name_buffer_handle);
360 core_pin(t->cache.entries_handle);
361}
362
363void tree_unlock_cache(struct tree_context *t)
364{
365 core_unpin(t->cache.name_buffer_handle);
366 core_unpin(t->cache.entries_handle);
367}
368
369/*
370 * Returns the position of a given file in the current directory
371 * returns -1 if not found
372 */
373static int tree_get_file_position(char * filename)
374{
375 int i, ret = -1;/* no file match, return undefined */
376
377 tree_lock_cache(&tc);
378 struct entry *entries = tree_get_entries(&tc);
379
380 /* use lastfile to determine the selected item (default=0) */
381 for (i=0; i < tc.filesindir; i++)
382 {
383#if ((CONFIG_PLATFORM & PLATFORM_NATIVE) || defined(__APPLE__) || defined(_WIN32) || defined(__CYGWIN__))
384 if (!strcasecmp(entries[i].name, filename))
385#else
386 if (!strcmp(entries[i].name, filename))
387#endif
388 {
389 ret = i;
390 break;
391 }
392 }
393 tree_unlock_cache(&tc);
394 return(ret);
395}
396
397/*
398 * Called when a new dir is loaded (for example when returning from other apps ...)
399 * also completely redraws the tree
400 */
401static int update_dir(void)
402{
403 struct gui_synclist * const list = &tree_lists;
404 int show_path_in_browser = global_settings.show_path_in_browser;
405 bool changed = false;
406
407 const char* title = NULL;/* Must clear the title as the list is reused */
408 int icon = NOICON;
409
410#ifdef HAVE_TAGCACHE
411 bool id3db = *tc.dirfilter == SHOW_ID3DB;
412#else
413 const bool id3db = false;
414#endif
415
416#ifdef HAVE_TAGCACHE
417 /* Checks for changes */
418 if (id3db) {
419 if (tc.currtable != lasttable ||
420 tc.currextra != lastextra ||
421 reload_dir)
422 {
423 if (tagtree_load(&tc) < 0)
424 return -1;
425
426 lasttable = tc.currtable;
427 lastextra = tc.currextra;
428 changed = true;
429 }
430 }
431 else
432#endif
433 {
434 tc.sort_dir = global_settings.sort_dir;
435 /* if the tc.currdir has been changed, reload it ...*/
436 if (reload_dir || strncmp(tc.currdir, lastdir, sizeof(lastdir)))
437 {
438 if (ft_load(&tc, NULL) < 0)
439 return -1;
440 strmemccpy(lastdir, tc.currdir, MAX_PATH);
441 changed = true;
442 }
443 }
444 /* if selected item is undefined */
445 if (tc.selected_item == -1)
446 {
447 if (!id3db)
448 /* use lastfile to determine the selected item */
449 tc.selected_item = tree_get_file_position(lastfile);
450
451 /* If the file doesn't exists, select the first one (default) */
452 if(tc.selected_item < 0)
453 tc.selected_item = 0;
454 changed = true;
455 }
456 if (changed)
457 {
458 if( !id3db && tc.dirfull )
459 {
460 splash(HZ, ID2P(LANG_SHOWDIR_BUFFER_FULL));
461 }
462 }
463
464 gui_synclist_init(list, &tree_get_filename, &tc, false, 1, NULL);
465
466#ifdef HAVE_TAGCACHE
467 if (id3db)
468 {
469 if (show_path_in_browser == SHOW_PATH_FULL
470 || show_path_in_browser == SHOW_PATH_CURRENT)
471 {
472 title = tagtree_get_title(&tc);
473 icon = filetype_get_icon(ATTR_DIRECTORY);
474 }
475 }
476 else
477#endif
478 {
479 if (tc.browse && tc.browse->title)
480 {
481 title = tc.browse->title;
482 icon = tc.browse->icon;
483 if (icon == NOICON)
484 icon = filetype_get_icon(ATTR_DIRECTORY);
485 /* display sub directories in the title of plugin browser */
486 if (tc.dirlevel > 0 && *tc.dirfilter == SHOW_PLUGINS)
487 {
488 char *subdir = strrchr(tc.currdir, '/');
489 if (subdir != NULL)
490 title = subdir + 1; /* step past the separator */
491 }
492 }
493 else
494 {
495 if (show_path_in_browser == SHOW_PATH_FULL)
496 {
497 title = tc.currdir;
498 icon = filetype_get_icon(ATTR_DIRECTORY);
499 }
500 else if (show_path_in_browser == SHOW_PATH_CURRENT)
501 {
502 title = strrchr(tc.currdir, '/');
503 if (title != NULL)
504 {
505 title++; /* step past the separator */
506 if (*title == '\0')
507 {
508 /* Display "Files" for the root dir */
509 title = ID2P(LANG_DIR_BROWSER);
510 }
511 icon = filetype_get_icon(ATTR_DIRECTORY);
512 }
513 }
514 }
515 }
516
517 /* set title and icon, if nothing is set, clear the title
518 * with NULL and icon as NOICON as the list is reused */
519 gui_synclist_set_title(list, P2STR((unsigned char*)title), icon);
520
521 gui_synclist_set_nb_items(list, tc.filesindir);
522 gui_synclist_set_icon_callback(list,
523 global_settings.show_icons?tree_get_fileicon:NULL);
524 gui_synclist_set_voice_callback(list, &tree_voice_cb);
525#ifdef HAVE_LCD_COLOR
526 gui_synclist_set_color_callback(list, &tree_get_filecolor);
527#endif
528 if( tc.selected_item >= tc.filesindir)
529 tc.selected_item=tc.filesindir-1;
530
531 gui_synclist_select_item(list, tc.selected_item);
532 gui_synclist_draw(list);
533 gui_synclist_speak_item(list);
534 return tc.filesindir;
535}
536
537/* load tracks from specified directory to resume play */
538void resume_directory(const char *dir)
539{
540 int dirfilter = *tc.dirfilter;
541 int ret;
542#ifdef HAVE_TAGCACHE
543 bool id3db = *tc.dirfilter == SHOW_ID3DB;
544#else
545 const bool id3db = false;
546#endif
547 /* make sure the dirfilter is sane. The only time it should be possible
548 * thats its not is when resume playlist is called from a plugin
549 */
550 if (!id3db)
551 *tc.dirfilter = global_settings.dirfilter;
552 ret = ft_load(&tc, dir);
553 *tc.dirfilter = dirfilter;
554 if (ret < 0)
555 return;
556 lastdir[0] = 0;
557
558 ft_build_playlist(&tc, 0);
559
560#ifdef HAVE_TAGCACHE
561 if (id3db)
562 tagtree_load(&tc);
563#endif
564}
565
566/* Returns the current working directory and also writes cwd to buf if
567 non-NULL. In case of error, returns NULL. */
568#ifdef CTRU
569char *__wrap_getcwd(char *buf, getcwd_size_t size)
570#else
571char *getcwd(char *buf, getcwd_size_t size)
572#endif
573{
574 if (!buf)
575 return tc.currdir;
576 else if (size)
577 {
578 if (strmemccpy(buf, tc.currdir, size) != NULL)
579 return buf;
580 }
581 /* size == 0, or truncation in strmemccpy */
582 return NULL;
583}
584
585/* Force a reload of the directory next time directory browser is called */
586void reload_directory(void)
587{
588 reload_dir = true;
589}
590
591char* get_current_file(char* buffer, size_t buffer_len)
592{
593#ifdef HAVE_TAGCACHE
594 /* in ID3DB mode it is a bad idea to call this function */
595 /* (only happens with `follow playlist') */
596 if( *tc.dirfilter == SHOW_ID3DB )
597 return NULL;
598#endif
599
600 struct entry *entry = tree_get_entry_at(&tc, tc.selected_item);
601 if (entry && getcwd(buffer, buffer_len))
602 {
603 if (!tc.dirlength)
604 return buffer;
605
606 size_t usedlen = strlen(buffer);
607
608 if (usedlen + 2 < buffer_len) /* ensure enough room for '/' + '\0' */
609 {
610 if (buffer[usedlen-1] != '/')
611 {
612 buffer[usedlen] = '/';
613 /* strmemccpy will zero terminate if we run out of space after */
614 usedlen++;
615 }
616 buffer_len -= usedlen;
617 if (strmemccpy(buffer + usedlen, entry->name, buffer_len) != NULL)
618 return buffer;
619 }
620 }
621 return NULL;
622}
623
624/* Allow apps to change our dirfilter directly (required for sub browsers)
625 if they're suddenly going to become a file browser for example */
626void set_dirfilter(int l_dirfilter)
627{
628 *tc.dirfilter = l_dirfilter;
629}
630
631/* Selects a path + file and update tree context properly */
632static void set_current_file_ex(const char *path, const char *filename)
633{
634 int i;
635
636#ifdef HAVE_TAGCACHE
637 /* in ID3DB mode it is a bad idea to call this function */
638 /* (only happens with `follow playlist') */
639 if( *tc.dirfilter == SHOW_ID3DB )
640 return;
641#endif
642
643 if (!filename) /* path and filename supplied combined */
644 {
645 /* separate directory from filename */
646 /* gets the directory's name and put it into tc.currdir */
647 filename = strrchr(path+1,'/');
648 size_t endpos = filename - path;
649 if (filename && endpos < MAX_PATH - 1)
650 {
651 strmemccpy(tc.currdir, path, endpos + 1);
652 filename++;
653 }
654 else
655 {
656 if (strlen(tc.currdir) <= 1)
657 {
658 strcpy(tc.currdir, "/");
659 }
660 filename = path+1;
661 }
662 }
663 else /* path and filename came in separate ensure an ending '/' */
664 {
665 char *end_p = strmemccpy(tc.currdir, path, MAX_PATH);
666 size_t endpos = end_p - tc.currdir;
667 if (endpos < MAX_PATH)
668 {
669 if (tc.currdir[endpos - 2] != '/')
670 {
671 tc.currdir[endpos - 1] = '/';
672 tc.currdir[endpos] = '\0';
673 }
674 }
675 }
676 strmemccpy(lastfile, filename, MAX_PATH);
677
678
679 /* If we changed dir we must recalculate the dirlevel
680 and adjust the selected history properly */
681 if (strncmp(tc.currdir,lastdir,sizeof(lastdir)))
682 {
683 tc.dirlevel = 0;
684 tc.selected_item_history[tc.dirlevel] = -1;
685
686 /* use '/' to calculate dirlevel */
687 for (i = 1; path[i] != '\0'; i++)
688 {
689 if (path[i] == '/')
690 {
691 tc.dirlevel++;
692 tc.selected_item_history[tc.dirlevel] = -1;
693 }
694 }
695 }
696 if (ft_load(&tc, NULL) >= 0)
697 {
698 tc.selected_item = tree_get_file_position(lastfile);
699 if (!tc.is_browsing && tc.out_of_tree == 0)
700 {
701 /* the browser is closed */
702 /* don't allow the previous items to overwrite what we just loaded */
703 tc.out_of_tree = tc.selected_item + 1;
704 }
705 }
706}
707
708/* Selects a file and update tree context properly */
709void set_current_file(const char *path)
710{
711 set_current_file_ex(path, NULL);
712}
713
714
715static int exit_to_new_screen(int screen)
716{
717 gui_synclist_scroll_stop(&tree_lists);
718 return screen;
719}
720
721/* main loop, handles key events */
722static int dirbrowse(void)
723{
724 int numentries=0;
725 char buf[MAX_PATH];
726 int button;
727 int oldbutton;
728 bool reload_root = false;
729 int lastfilter = *tc.dirfilter;
730 bool lastsortcase = global_settings.sort_case;
731 bool exit_func = false;
732
733 char* currdir = tc.currdir; /* just a shortcut */
734#ifdef HAVE_TAGCACHE
735 bool id3db = *tc.dirfilter == SHOW_ID3DB;
736
737 if (id3db)
738 curr_context=CONTEXT_ID3DB;
739 else
740#endif
741 curr_context=CONTEXT_TREE;
742 if (tc.selected_item < 0)
743 tc.selected_item = 0;
744#ifdef HAVE_TAGCACHE
745 lasttable = -1;
746 lastextra = -1;
747#endif
748
749 start_wps = false;
750 numentries = update_dir();
751 reload_dir = false;
752 if (numentries == -1)
753 return exit_to_new_screen(GO_TO_PREVIOUS); /* currdir is not a directory */
754
755 if (*tc.dirfilter > NUM_FILTER_MODES && numentries==0)
756 {
757 splash(HZ*2, *tc.dirfilter == SHOW_M3U ?
758 ID2P(LANG_CATALOG_NO_PLAYLISTS) : ID2P(LANG_NO_FILES));
759 return exit_to_new_screen(GO_TO_PREVIOUS); /* No files found for rockbox_browse() */
760 }
761
762 while(tc.browse && tc.is_browsing) {
763 bool restore = false;
764 if (tc.dirlevel < 0)
765 tc.dirlevel = 0; /* shouldnt be needed.. this code needs work! */
766
767 keyclick_set_callback(gui_synclist_keyclick_callback, &tree_lists);
768 button = get_action(CONTEXT_TREE|ALLOW_SOFTLOCK,
769 list_do_action_timeout(&tree_lists, HZ/2));
770 oldbutton = button;
771 gui_synclist_do_button(&tree_lists, &button);
772 tc.selected_item = gui_synclist_get_sel_pos(&tree_lists);
773 int customaction = ONPLAY_NO_CUSTOMACTION;
774 bool do_restore_display = true;
775 #ifdef HAVE_TAGCACHE
776 if (id3db && (button == ACTION_STD_OK || button == ACTION_STD_CONTEXT))
777 {
778 customaction = tagtree_get_custom_action(&tc);
779 if (customaction == ONPLAY_CUSTOMACTION_SHUFFLE_SONGS)
780 {
781 /* The code to insert shuffled is on the context branch of the switch so we always go here */
782 button = ACTION_STD_CONTEXT;
783 do_restore_display = false;
784 }
785 }
786 #endif
787 switch ( button ) {
788 case ACTION_STD_OK:
789 /* nothing to do if no files to display */
790 if ( numentries == 0 )
791 break;
792 if (tc.browse->flags & BROWSE_SELECTONLY)
793 {
794 struct entry *entry =
795 get_valid_entry(__func__, &tc, tc.selected_item);
796 short attr = entry->attr;
797 if(!(attr & ATTR_DIRECTORY))
798 {
799 tc.browse->flags |= BROWSE_SELECTED;
800 get_current_file(tc.browse->buf, tc.browse->bufsize);
801 return exit_to_new_screen(GO_TO_PREVIOUS);
802 }
803 }
804#ifdef HAVE_TAGCACHE
805 switch (id3db ? tagtree_enter(&tc, true) : ft_enter(&tc))
806#else
807 switch (ft_enter(&tc))
808#endif
809 {
810 case GO_TO_FILEBROWSER: reload_dir = true; break;
811 case GO_TO_PLUGIN:
812 return exit_to_new_screen(GO_TO_PLUGIN);
813 case GO_TO_WPS:
814 return exit_to_new_screen(GO_TO_WPS);
815#if CONFIG_TUNER
816 case GO_TO_FM:
817 return exit_to_new_screen(GO_TO_FM);
818#endif
819 case GO_TO_ROOT: exit_func = true; break;
820 default:
821 break;
822 }
823 restore = do_restore_display;
824 break;
825
826 case ACTION_STD_CANCEL:
827 exit_to_new_screen(0);
828 if (*tc.dirfilter > NUM_FILTER_MODES && tc.dirlevel < 1) {
829 exit_func = true;
830 break;
831 }
832 if ((*tc.dirfilter == SHOW_ID3DB && tc.dirlevel == 0) ||
833 ((*tc.dirfilter != SHOW_ID3DB && !strcmp(currdir,"/"))))
834 {
835 if (oldbutton == ACTION_TREE_PGLEFT)
836 break;
837 else
838 return exit_to_new_screen(GO_TO_ROOT);
839 }
840
841#ifdef HAVE_TAGCACHE
842 if (id3db)
843 tagtree_exit(&tc, true);
844 else
845#endif
846 if (ft_exit(&tc) == 3)
847 exit_func = true;
848
849 restore = do_restore_display;
850 break;
851
852 case ACTION_TREE_STOP:
853 if (list_stop_handler())
854 restore = do_restore_display;
855 break;
856
857 case ACTION_STD_MENU:
858 return exit_to_new_screen(GO_TO_ROOT);
859 break;
860
861#ifdef HAVE_RECORDING
862 case ACTION_STD_REC:
863 return exit_to_new_screen(GO_TO_RECSCREEN);
864#endif
865
866 case ACTION_TREE_WPS:
867 return exit_to_new_screen(GO_TO_PREVIOUS_MUSIC);
868 break;
869#ifdef HAVE_QUICKSCREEN
870 case ACTION_STD_QUICKSCREEN:
871 {
872 bool enter_shortcuts_menu = global_settings.shortcuts_replaces_qs;
873 if (enter_shortcuts_menu && *tc.dirfilter >= NUM_FILTER_MODES)
874 break;
875 else if (!enter_shortcuts_menu)
876 {
877 int ret = quick_screen_quick(button);
878 if (ret == QUICKSCREEN_IN_USB)
879 reload_dir = true;
880 else if (ret == QUICKSCREEN_GOTO_SHORTCUTS_MENU)
881 enter_shortcuts_menu = true;
882 }
883
884 if (enter_shortcuts_menu && *tc.dirfilter < NUM_FILTER_MODES)
885 {
886 int last_screen = global_status.last_screen;
887 global_status.last_screen = GO_TO_SHORTCUTMENU;
888 int shortcut_ret = do_shortcut_menu(NULL);
889 if (shortcut_ret == GO_TO_PREVIOUS)
890 global_status.last_screen = last_screen;
891 else
892 return exit_to_new_screen(shortcut_ret);
893 }
894 else if (enter_shortcuts_menu) /* currently disabled */
895 {
896 /* QuickScreen defers skin updates, popping its activity, when
897 switching to Shortcuts Menu, so make up for that here: */
898 FOR_NB_SCREENS(i)
899 skin_update(CUSTOM_STATUSBAR, i, SKIN_REFRESH_ALL);
900 }
901
902 restore = do_restore_display;
903 break;
904 }
905#endif
906
907#ifdef HAVE_HOTKEY
908 case ACTION_TREE_HOTKEY:
909 if (!global_settings.hotkey_tree)
910 break;
911 /* fall through */
912#endif
913 case ACTION_STD_CONTEXT:
914 {
915 bool hotkey = button == ACTION_TREE_HOTKEY;
916 int onplay_result;
917 int attr = 0;
918
919 if (tc.browse->flags & BROWSE_NO_CONTEXT_MENU)
920 break;
921
922 if(!numentries)
923 onplay_result = onplay(NULL, 0, curr_context, hotkey, customaction);
924 else {
925#ifdef HAVE_TAGCACHE
926 if (id3db)
927 {
928 if (tagtree_get_attr(&tc) == FILE_ATTR_AUDIO)
929 {
930 attr = FILE_ATTR_AUDIO;
931
932 /* Look up the filename only once it is needed, so we
933 don't have to wait for the disk to wake up here. */
934 buf[0] = '\0';
935 }
936 else
937 {
938 attr = ATTR_DIRECTORY;
939 int title_len = 0;
940
941 /* In case of "special entries", add table title as
942 prefix, e.g. "The Beatles [All Tracks]", instead
943 of just "[All Tracks]", to improve the suggested
944 playlist filename.
945 */
946 if (tc.selected_item < tc.special_entry_count)
947 {
948 title_len = snprintf(buf, sizeof(buf), "%s ",
949 tagtree_get_title(&tc));
950 if (title_len < 0)
951 title_len = 0;
952 }
953
954 if (title_len < (int) sizeof(buf))
955 tagtree_get_entry_name(&tc, tc.selected_item,
956 buf + title_len,
957 sizeof(buf) - title_len);
958
959 fix_path_part(buf, 0, sizeof(buf) - 1);
960 }
961 }
962 else
963#endif
964 {
965 struct entry *entry =
966 get_valid_entry(__func__, &tc, tc.selected_item);
967
968 attr = entry->attr;
969
970 ft_assemble_path(buf, sizeof(buf), currdir, entry->name);
971
972 }
973 onplay_result = onplay(buf, attr, curr_context, hotkey, customaction);
974 }
975 switch (onplay_result)
976 {
977 case ONPLAY_MAINMENU:
978 return exit_to_new_screen(GO_TO_ROOT);
979 break;
980
981 case ONPLAY_OK:
982 restore = do_restore_display;
983 break;
984
985 case ONPLAY_RELOAD_DIR:
986 reload_dir = true;
987 break;
988
989 case ONPLAY_START_PLAY:
990 return exit_to_new_screen(GO_TO_WPS);
991 break;
992
993 case ONPLAY_PLUGIN:
994 return exit_to_new_screen(GO_TO_PLUGIN);
995 break;
996 }
997 break;
998 }
999
1000#ifdef HAVE_HOTSWAP
1001 case SYS_FS_CHANGED:
1002#ifdef HAVE_TAGCACHE
1003 if (!id3db)
1004#endif
1005 reload_dir = true;
1006 /* The 'dir no longer valid' situation will be caught later
1007 * by checking the showdir() result. */
1008 break;
1009#endif
1010
1011 default:
1012 if (tc.browse->disable_gui == true) {
1013 #ifdef HAVE_TAGCACHE
1014 switch (id3db ? tagtree_enter(&tc, true) : ft_enter(&tc))
1015 #else
1016 switch (ft_enter(&tc))
1017 #endif
1018 {
1019 case GO_TO_FILEBROWSER: reload_dir = true; break;
1020 case GO_TO_PLUGIN:
1021 return GO_TO_PLUGIN;
1022 case GO_TO_WPS:
1023 return GO_TO_WPS;
1024 #if CONFIG_TUNER
1025 case GO_TO_FM:
1026 return GO_TO_FM;
1027 #endif
1028 case GO_TO_ROOT: exit_func = true; break;
1029 default:
1030 break;
1031 }
1032 restore = do_restore_display;
1033 return GO_TO_ROOT;
1034 }
1035 if (default_event_handler(button) == SYS_USB_CONNECTED)
1036 {
1037 if(*tc.dirfilter > NUM_FILTER_MODES)
1038 /* leave sub-browsers after usb, doing otherwise
1039 might be confusing to the user */
1040 exit_func = true;
1041 else
1042 reload_dir = true;
1043 }
1044 break;
1045 }
1046 if (start_wps)
1047 return exit_to_new_screen(GO_TO_WPS);
1048 if (button && !IS_SYSEVENT(button))
1049 {
1050 storage_spin();
1051 }
1052
1053
1054 check_rescan:
1055 /* do we need to rescan dir? */
1056 if (reload_dir || reload_root ||
1057 lastfilter != *tc.dirfilter ||
1058 lastsortcase != global_settings.sort_case)
1059 {
1060 if (reload_root) {
1061 strcpy(currdir, "/");
1062 tc.dirlevel = 0;
1063#ifdef HAVE_TAGCACHE
1064 tc.currtable = 0;
1065 tc.currextra = 0;
1066 lasttable = -1;
1067 lastextra = -1;
1068#endif
1069 reload_root = false;
1070 }
1071
1072 if (!reload_dir)
1073 {
1074 gui_synclist_select_item(&tree_lists, 0);
1075 gui_synclist_draw(&tree_lists);
1076 tc.selected_item = 0;
1077 lastdir[0] = 0;
1078 }
1079
1080 lastfilter = *tc.dirfilter;
1081 lastsortcase = global_settings.sort_case;
1082 restore = do_restore_display;
1083 }
1084
1085 if (exit_func)
1086 return exit_to_new_screen(GO_TO_PREVIOUS);
1087
1088 if (restore || reload_dir) {
1089 FOR_NB_SCREENS(i)
1090 screens[i].scroll_stop();
1091 /* restore display */
1092 numentries = update_dir();
1093 reload_dir = false;
1094 if (currdir[1] && (numentries < 0))
1095 { /* not in root and reload failed */
1096 reload_root = true; /* try root */
1097 goto check_rescan;
1098 }
1099 }
1100 }
1101 return exit_to_new_screen(GO_TO_ROOT);
1102}
1103
1104int create_playlist(void)
1105{
1106 bool ret;
1107 trigger_cpu_boost();
1108 ret = catalog_add_to_a_playlist(PATH_ROOTSTR, ATTR_DIRECTORY, true, NULL, NULL);
1109 cancel_cpu_boost();
1110
1111 return (ret) ? 1 : 0;
1112}
1113
1114#define NUM_TC_BACKUP 3
1115static struct tree_context backups[NUM_TC_BACKUP];
1116/* do not make backup if it is not recursive call */
1117static int backup_count = -1;
1118int rockbox_browse(struct browse_context *browse)
1119{
1120 tc.is_browsing = (browse != NULL);
1121 int ret_val = 0;
1122 int dirfilter = SHOW_ALL;
1123 if (tc.is_browsing)
1124 dirfilter = browse->dirfilter;
1125 else
1126 {
1127 DEBUGF("%s browse is [NULL] \n", __func__);
1128 browse = tc.browse;
1129 }
1130 if (backup_count >= NUM_TC_BACKUP)
1131 return GO_TO_PREVIOUS;
1132 if (backup_count >= 0)
1133 backups[backup_count] = tc;
1134 backup_count++;
1135 int *prev_dirfilter = tc.dirfilter;
1136 tc.dirfilter = &dirfilter;
1137 tc.sort_dir = global_settings.sort_dir;
1138
1139 reload_dir = true;
1140
1141 if (tc.out_of_tree > 0)
1142 {
1143 /* an item has already been loaded out_of_tree holds the selected index
1144 * what happens with the item is dependent on the browse context */
1145 tc.selected_item = tc.out_of_tree - 1;
1146 tc.out_of_tree = 0;
1147 ret_val = ft_enter(&tc);
1148 }
1149 else
1150 {
1151 if (*tc.dirfilter >= NUM_FILTER_MODES)
1152 {
1153 int last_context;
1154 /* don't reset if its the same browse already loaded */
1155 if (tc.browse != browse ||
1156 !(tc.currdir[1] && strstr(tc.currdir, browse->root) != NULL))
1157 {
1158 tc.browse = browse;
1159 tc.selected_item = 0;
1160 tc.dirlevel = 0;
1161
1162 strmemccpy(tc.currdir, browse->root, sizeof(tc.currdir));
1163 }
1164
1165 start_wps = false;
1166 last_context = curr_context;
1167
1168 if (browse->selected)
1169 {
1170 set_current_file_ex(browse->root, browse->selected);
1171 /* set_current_file changes dirlevel, change it back */
1172 tc.dirlevel = 0;
1173 }
1174
1175 ret_val = dirbrowse();
1176 curr_context = last_context;
1177 }
1178 else
1179 {
1180 if (dirfilter != SHOW_ID3DB && (browse->flags & BROWSE_DIRFILTER) == 0)
1181 tc.dirfilter = &global_settings.dirfilter;
1182 tc.browse = browse;
1183 set_current_file(browse->root);
1184 if (browse->flags&BROWSE_RUNFILE)
1185 ret_val = ft_enter(&tc);
1186 else
1187 ret_val = dirbrowse();
1188 }
1189 }
1190
1191 tc.is_browsing = false;
1192 tc.dirfilter = prev_dirfilter; /* Bugfix restore dirfilter*/
1193
1194 backup_count--;
1195 if (backup_count >= 0)
1196 tc = backups[backup_count];
1197
1198 return ret_val;
1199}
1200
1201int rockbox_browse_at(const char* path)
1202{
1203 struct browse_context browse = {
1204 .dirfilter = SHOW_SUPPORTED,
1205 .icon = Icon_NOICON,
1206 .root = path,
1207 .disable_gui = true,
1208 };
1209 strcpy(tc.currdir, path);
1210
1211 return rockbox_browse(&browse);
1212}
1213
1214static int move_callback(int handle, void* current, void* new)
1215{
1216 struct tree_cache* cache = &tc.cache;
1217 ptrdiff_t diff = new - current;
1218 /* FIX_PTR makes sure to not accidentally update static allocations */
1219#define FIX_PTR(x) \
1220 { if ((void*)x >= current && (void*)x < (current+cache->name_buffer_size)) x+= diff; }
1221
1222 if (handle == cache->name_buffer_handle)
1223 { /* update entry structs, *even if they are struct tagentry */
1224 struct entry *this = core_get_data(cache->entries_handle);
1225 struct entry *last = this + cache->max_entries;
1226 for(; this < last; this++)
1227 FIX_PTR(this->name);
1228 }
1229 /* nothing to do if entries moved */
1230 return BUFLIB_CB_OK;
1231}
1232
1233static struct buflib_callbacks ops = {
1234 .move_callback = move_callback,
1235 .shrink_callback = NULL,
1236};
1237
1238void tree_mem_init(void)
1239{
1240 /* initialize tree context struct */
1241 struct tree_cache* cache = &tc.cache;
1242 memset(&tc, 0, sizeof(tc));
1243 tc.dirfilter = &global_settings.dirfilter;
1244 tc.sort_dir = global_settings.sort_dir;
1245
1246 cache->name_buffer_size = AVERAGE_FILENAME_LENGTH *
1247 global_settings.max_files_in_dir;
1248 cache->name_buffer_handle = core_alloc_ex(cache->name_buffer_size, &ops);
1249
1250 cache->max_entries = global_settings.max_files_in_dir;
1251 cache->entries_handle =
1252 core_alloc_ex(cache->max_entries*(sizeof(struct entry)), &ops);
1253}
1254
1255bool bookmark_play(char *resume_file, int index, unsigned long elapsed,
1256 unsigned long offset, int seed, char *filename)
1257{
1258 int i;
1259 char* suffix = strrchr(resume_file, '.');
1260 bool started = false;
1261
1262 if (suffix != NULL && !strncasecmp(suffix, ".m3u", sizeof(".m3u") - 1)) /* gets m3u8 too */
1263 {
1264 /* Playlist playback */
1265 char* slash;
1266 /* check that the file exists */
1267 if (!file_exists(resume_file))
1268 return false;
1269
1270 slash = strrchr(resume_file,'/');
1271 if (slash)
1272 {
1273 char* cp;
1274 *slash=0;
1275
1276 cp=resume_file;
1277 if (!cp[0])
1278 cp="/";
1279
1280 if (playlist_create(cp, slash+1) != -1)
1281 {
1282 if (global_settings.playlist_shuffle)
1283 playlist_shuffle(seed, -1);
1284 started = true;
1285 }
1286 *slash='/';
1287 }
1288 }
1289 else
1290 {
1291 /* Directory playback */
1292 lastdir[0]='\0';
1293 if (playlist_create(resume_file, NULL) != -1)
1294 {
1295 char filename_buf[MAX_PATH + 1];
1296 const char* peek_filename;
1297 resume_directory(resume_file);
1298 if (global_settings.playlist_shuffle)
1299 playlist_shuffle(seed, -1);
1300
1301 /* Check if the file is at the same spot in the directory,
1302 else search for it */
1303 int amt = playlist_amount();
1304 for ( i=0; i < amt; i++ )
1305 {
1306 int modidx = (i + index) % amt;
1307 peek_filename = playlist_peek(modidx, filename_buf,
1308 sizeof(filename_buf));
1309
1310 if (peek_filename == NULL)
1311 {
1312 if (index == 0) /* searched every entry didn't find a match */
1313 return false;
1314 /* playlist has shrunk, search from the top */
1315 i = 0;
1316 amt = index;
1317 index = 0;
1318 }
1319 else if (!strcmp(strrchr(peek_filename, '/') + 1, filename))
1320 {
1321 started = true;
1322 index = modidx;
1323 break;
1324 }
1325 }
1326 }
1327 }
1328
1329 if (started)
1330 {
1331 playlist_start(index, elapsed, offset);
1332 start_wps = true;
1333 }
1334 return started;
1335}
1336
1337static void say_filetype(int attr)
1338{
1339 talk_id(tree_get_filetype_voiceclip(attr), true);
1340}
1341
1342static int ft_play_dirname(char* name)
1343{
1344#ifdef HAVE_MULTIVOLUME
1345 int vol = path_get_volume_id(name);
1346 if (talk_volume_id(vol))
1347 return 1;
1348#endif
1349
1350 return talk_file(tc.currdir, name, dir_thumbnail_name, NULL,
1351 global_settings.talk_filetype ?
1352 TALK_IDARRAY(VOICE_DIR) : NULL,
1353 false);
1354}
1355
1356static int ft_play_filename(char *dir, char *file, int attr)
1357{
1358 if (strlen(file) >= strlen(file_thumbnail_ext)
1359 && strcasecmp(&file[strlen(file) - strlen(file_thumbnail_ext)],
1360 file_thumbnail_ext))
1361 /* file has no .talk extension */
1362 return talk_file(dir, NULL, file, file_thumbnail_ext,
1363 TALK_IDARRAY(tree_get_filetype_voiceclip(attr)), false);
1364
1365 /* it already is a .talk file, play this directly, but prefix it. */
1366 return talk_file(dir, NULL, file, NULL,
1367 TALK_IDARRAY(LANG_VOICE_DIR_HOVER), false);
1368}
1369
1370/* These two functions are called by the USB and shutdown handlers */
1371void tree_flush(void)
1372{
1373 tc.is_browsing = false;/* clear browse to prevent reentry to a possibly missing file */
1374#ifdef HAVE_TAGCACHE
1375 tagcache_shutdown();
1376#endif
1377
1378#ifdef HAVE_TC_RAMCACHE
1379 tagcache_unload_ramcache();
1380#endif
1381
1382#ifdef HAVE_DIRCACHE
1383 int old_val = global_status.dircache_size;
1384#ifdef HAVE_EEPROM_SETTINGS
1385 bool savecache = false;
1386#endif
1387
1388 if (global_settings.dircache)
1389 {
1390 dircache_suspend();
1391
1392 struct dircache_info info;
1393 dircache_get_info(&info);
1394
1395 global_status.dircache_size = info.last_size;
1396 #ifdef HAVE_EEPROM_SETTINGS
1397 savecache = firmware_settings.initialized;
1398 #endif
1399 }
1400 else
1401 {
1402 global_status.dircache_size = 0;
1403 }
1404
1405 if (old_val != global_status.dircache_size)
1406 status_save(true);
1407
1408 #ifdef HAVE_EEPROM_SETTINGS
1409 if (savecache)
1410 dircache_save();
1411 #endif
1412#endif /* HAVE_DIRCACHE */
1413}
1414
1415void tree_restore(void)
1416{
1417#ifdef HAVE_EEPROM_SETTINGS
1418 firmware_settings.disk_clean = false;
1419#endif
1420
1421#ifdef HAVE_TC_RAMCACHE
1422 tagcache_remove_statefile();
1423#endif
1424
1425#ifdef HAVE_DIRCACHE
1426 if (global_settings.dircache && dircache_resume() > 0)
1427 {
1428 /* Print "Scanning disk..." to the display. */
1429 splash(0, str(LANG_SCANNING_DISK));
1430 dircache_wait();
1431 }
1432#endif
1433
1434#ifdef HAVE_TAGCACHE
1435 tagcache_start_scan();
1436#endif
1437}