Code

cmd_select_all added
[ncmpc.git] / src / screen_file.c
1 /* 
2  * $Id$
3  *
4  * (c) 2004 by Kalle Wallin <kaw@linux.se>
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  *
19  */
21 #include <ctype.h>
22 #include <stdlib.h>
23 #include <string.h>
24 #include <glib.h>
25 #include <ncurses.h>
27 #include "config.h"
28 #include "ncmpc.h"
29 #include "options.h"
30 #include "support.h"
31 #include "mpdclient.h"
32 #include "strfsong.h"
33 #include "command.h"
34 #include "screen.h"
35 #include "screen_utils.h"
36 #include "screen_browse.h"
37 #include "screen_play.h"
39 #define USE_OLD_LAYOUT
40 #undef  USE_OLD_ADD
42 #define BUFSIZE 1024
44 #define HIGHLIGHT  (0x01)
47 static list_window_t *lw = NULL;
48 static list_window_state_t *lw_state = NULL;
49 static mpdclient_filelist_t *filelist = NULL;
53 /* clear the highlight flag for all items in the filelist */
54 void
55 clear_highlights(mpdclient_filelist_t *filelist)
56 {
57   GList *list = g_list_first(filelist->list);
58   
59   while( list )
60     {
61       filelist_entry_t *entry = list->data;
63       entry->flags &= ~HIGHLIGHT;
64       list = list->next;
65     }
66 }
68 /* change the highlight flag for a song */
69 void
70 set_highlight(mpdclient_filelist_t *filelist, mpd_Song *song, int highlight)
71 {
72   GList *list = g_list_first(filelist->list);
74   if( !song )
75     return;
77   while( list )
78     {
79       filelist_entry_t *entry = list->data;
80       mpd_InfoEntity *entity  = entry->entity;
82       if( entity && entity->type==MPD_INFO_ENTITY_TYPE_SONG )
83         {
84           mpd_Song *song2 = entity->info.song;
86           if( strcmp(song->file, song2->file) == 0 )
87             {
88               if(highlight)
89                 entry->flags |= HIGHLIGHT;
90               else
91                 entry->flags &= ~HIGHLIGHT;
92             }
93         }
94       list = list->next;
95     }
96 }
98 /* sync highlight flags with playlist */
99 void
100 sync_highlights(mpdclient_t *c, mpdclient_filelist_t *filelist)
102   GList *list = g_list_first(filelist->list);
104   while(list)
105     {
106       filelist_entry_t *entry = list->data;
107       mpd_InfoEntity *entity = entry->entity;
109       if( entity && entity->type==MPD_INFO_ENTITY_TYPE_SONG )
110         {
111           mpd_Song *song = entity->info.song;
112           
113           if( playlist_get_index_from_file(c, song->file) >= 0 )
114             entry->flags |= HIGHLIGHT;
115           else
116             entry->flags &= ~HIGHLIGHT;
117         }
118       list=list->next;
119     }
122 /* the db have changed -> update the filelist */
123 static void 
124 file_changed_callback(mpdclient_t *c, int event, gpointer data)
126   D("screen_file.c> filelist_callback() [%d]\n", event);
127   filelist = mpdclient_filelist_update(c, filelist);
128   sync_highlights(c, filelist);
129   list_window_check_selected(lw, filelist->length);
132 /* the playlist have been updated -> fix highlights */
133 static void 
134 playlist_changed_callback(mpdclient_t *c, int event, gpointer data)
136   D("screen_file.c> playlist_callback() [%d]\n", event);
137   switch(event)
138     {
139     case PLAYLIST_EVENT_CLEAR:
140       clear_highlights(filelist);
141       break;
142     case PLAYLIST_EVENT_ADD:
143       set_highlight(filelist, (mpd_Song *) data, 1); 
144       break;
145     case PLAYLIST_EVENT_DELETE:
146       set_highlight(filelist, (mpd_Song *) data, 0); 
147       break;
148     case PLAYLIST_EVENT_MOVE:
149       break;
150     default:
151       sync_highlights(c, filelist);
152       break;
153     }
156 /* list_window callback */
157 char *
158 browse_lw_callback(int index, int *highlight, void *data)
160   static char buf[BUFSIZE];
161   mpdclient_filelist_t *filelist = (mpdclient_filelist_t *) data;
162   filelist_entry_t *entry;
163   mpd_InfoEntity *entity;
165   *highlight = 0;
166   if( (entry=(filelist_entry_t *)g_list_nth_data(filelist->list,index))==NULL )
167     return NULL;
169   entity = entry->entity;
170   *highlight = (entry->flags & HIGHLIGHT);
172   if( entity == NULL )
173     {
174       return "[..]";
175     }
176   if( entity->type==MPD_INFO_ENTITY_TYPE_DIRECTORY ) 
177     {
178       mpd_Directory *dir = entity->info.directory;
179       char *dirname = utf8_to_locale(basename(dir->path));
181       g_snprintf(buf, BUFSIZE, "[%s]", dirname);
182       g_free(dirname);
183       return buf;
184     }
185   else if( entity->type==MPD_INFO_ENTITY_TYPE_SONG )
186     {
187       mpd_Song *song = entity->info.song;
189       strfsong(buf, BUFSIZE, LIST_FORMAT, song);
190       return buf;
191     }
192   else if( entity->type==MPD_INFO_ENTITY_TYPE_PLAYLISTFILE )
193     {
194       mpd_PlaylistFile *plf = entity->info.playlistFile;
195       char *filename = utf8_to_locale(basename(plf->path));
197 #ifdef USE_OLD_LAYOUT      
198       g_snprintf(buf, BUFSIZE, "*%s*", filename);
199 #else 
200       g_snprintf(buf, BUFSIZE, "<Playlist> %s", filename);
201 #endif
202       g_free(filename);
203       return buf;
204     }
205   return "Error: Unknown entry!";
208 /* chdir */
209 static int
210 change_directory(screen_t *screen, mpdclient_t *c, filelist_entry_t *entry, char *new_path)
212   mpd_InfoEntity *entity = NULL;
213   gchar *path = NULL;
215   if( entry!=NULL )
216     entity = entry->entity;
217   else if( new_path==NULL )
218     return -1;
220   if( entity==NULL )
221     {
222       if( entry || 0==strcmp(new_path, "..") )
223         {
224           /* return to parent */
225           char *parent = g_path_get_dirname(filelist->path);
226           if( strcmp(parent, ".") == 0 )
227             {
228               parent[0] = '\0';
229             }
230           path = g_strdup(parent);
231           list_window_reset(lw);
232           /* restore previous list window state */
233           list_window_pop_state(lw_state,lw); 
234         }
235       else
236         {
237           /* entry==NULL, then new_path ("" is root) */
238           path = g_strdup(new_path);
239           list_window_reset(lw);
240           /* restore first list window state (pop while returning true) */
241           while(list_window_pop_state(lw_state,lw));
242         }
243     }
244   else
245     if( entity->type==MPD_INFO_ENTITY_TYPE_DIRECTORY)
246       {
247         /* enter sub */
248         mpd_Directory *dir = entity->info.directory;
249         path = utf8_to_locale(dir->path);      
250         /* save current list window state */
251         list_window_push_state(lw_state,lw); 
252       }
253     else
254       return -1;
256   filelist = mpdclient_filelist_free(filelist);
257   filelist = mpdclient_filelist_get(c, path);
258   sync_highlights(c, filelist);
259   list_window_check_selected(lw, filelist->length);
260   g_free(path);
261   return 0;
264 static int
265 load_playlist(screen_t *screen, mpdclient_t *c, filelist_entry_t *entry)
267   mpd_InfoEntity *entity = entry->entity;
268   mpd_PlaylistFile *plf = entity->info.playlistFile;
269   char *filename = utf8_to_locale(plf->path);
271   if( mpdclient_cmd_load_playlist_utf8(c, plf->path) == 0 )
272     screen_status_printf(_("Loading playlist %s..."), basename(filename));
273   g_free(filename);
274   return 0;
277 static int
278 handle_save(screen_t *screen, mpdclient_t *c)
280   filelist_entry_t *entry;
281   char *defaultname = NULL;
284   entry=( filelist_entry_t *) g_list_nth_data(filelist->list,lw->selected);
285   if( entry && entry->entity )
286     { 
287       mpd_InfoEntity *entity = entry->entity;
288       if( entity->type==MPD_INFO_ENTITY_TYPE_PLAYLISTFILE )
289         {
290           mpd_PlaylistFile *plf = entity->info.playlistFile;
291           defaultname = plf->path;
292         }
293     }
294   return playlist_save(screen, c, NULL, defaultname);
297 static int 
298 handle_delete(screen_t *screen, mpdclient_t *c)
300   filelist_entry_t *entry;
301   mpd_InfoEntity *entity;
302   mpd_PlaylistFile *plf;
303   char *str, *buf;
304   int key;
306   entry=( filelist_entry_t *) g_list_nth_data(filelist->list,lw->selected);
307   if( entry==NULL || entry->entity==NULL )
308     return -1;
310   entity = entry->entity;
312   if( entity->type!=MPD_INFO_ENTITY_TYPE_PLAYLISTFILE )
313     {
314       screen_status_printf(_("You can only delete playlists!"));
315       screen_bell();
316       return -1;
317     }
319   plf = entity->info.playlistFile;
320   str = utf8_to_locale(basename(plf->path));
321   buf = g_strdup_printf(_("Delete playlist %s [%s/%s] ? "), str, YES, NO);
322   g_free(str);  
323   key = tolower(screen_getch(screen->status_window.w, buf));
324   g_free(buf);
325   if( key==KEY_RESIZE )
326     screen_resize();
327   if( key != YES[0] )
328     {
329       screen_status_printf(_("Aborted!"));
330       return 0;
331     }
333   if( mpdclient_cmd_delete_playlist_utf8(c, plf->path) )
334     {
335       return -1;
336     }
337   screen_status_printf(_("Playlist deleted!"));
338   return 0;
341 static int
342 enqueue_and_play(screen_t *screen, mpdclient_t *c, filelist_entry_t *entry)
344   int index;
345   mpd_InfoEntity *entity = entry->entity;
346   mpd_Song *song = entity->info.song;
347   
348   if(!( entry->flags & HIGHLIGHT ))
349     {
350       if( mpdclient_cmd_add(c, song) == 0 )
351         {
352           char buf[BUFSIZE];
353           
354           entry->flags |= HIGHLIGHT;
355           strfsong(buf, BUFSIZE, LIST_FORMAT, song);
356           screen_status_printf(_("Adding \'%s\' to playlist\n"), buf);
357           mpdclient_update(c); /* get song id */
358         } 
359       else
360         return -1;
361     }
362   
363   index = playlist_get_index_from_file(c, song->file);
364   mpdclient_cmd_play(c, index);
365   return 0;
368 int
369 browse_handle_enter(screen_t *screen, 
370                     mpdclient_t *c,
371                     list_window_t *lw,
372                     mpdclient_filelist_t *filelist)
374   filelist_entry_t *entry;
375   mpd_InfoEntity *entity;
376   
377   if ( filelist==NULL )
378     return -1;
379   entry = ( filelist_entry_t *) g_list_nth_data(filelist->list, lw->selected);
380   if( entry==NULL )
381     return -1;
383   entity = entry->entity;
384   if( entity==NULL || entity->type==MPD_INFO_ENTITY_TYPE_DIRECTORY )
385     return change_directory(screen, c, entry, NULL);
386   else if( entity->type==MPD_INFO_ENTITY_TYPE_PLAYLISTFILE )
387     return load_playlist(screen, c, entry);
388   else if( entity->type==MPD_INFO_ENTITY_TYPE_SONG )
389     return enqueue_and_play(screen, c, entry);
390   return -1;
394 #ifdef USE_OLD_ADD
395 /* NOTE - The add_directory functions should move to mpdclient.c */
396 extern gint mpdclient_finish_command(mpdclient_t *c);
398 static int
399 add_directory(mpdclient_t *c, char *dir)
401   mpd_InfoEntity *entity;
402   GList *subdir_list = NULL;
403   GList *list = NULL;
404   char *dirname;
406   dirname = utf8_to_locale(dir);
407   screen_status_printf(_("Adding directory %s...\n"), dirname);
408   doupdate(); 
409   g_free(dirname);
410   dirname = NULL;
412   mpd_sendLsInfoCommand(c->connection, dir);
413   mpd_sendCommandListBegin(c->connection);
414   while( (entity=mpd_getNextInfoEntity(c->connection)) )
415     {
416       if( entity->type==MPD_INFO_ENTITY_TYPE_SONG )
417         {
418           mpd_Song *song = entity->info.song;
419           mpd_sendAddCommand(c->connection, song->file);
420           mpd_freeInfoEntity(entity);
421         }
422       else if( entity->type==MPD_INFO_ENTITY_TYPE_DIRECTORY )
423         {
424           subdir_list = g_list_append(subdir_list, (gpointer) entity); 
425         }
426       else
427         mpd_freeInfoEntity(entity);
428     }
429   mpd_sendCommandListEnd(c->connection);
430   mpdclient_finish_command(c);
431   c->need_update = TRUE;
432   
433   list = g_list_first(subdir_list);
434   while( list!=NULL )
435     {
436       mpd_Directory *dir;
438       entity = list->data;
439       dir = entity->info.directory;
440       add_directory(c, dir->path);
441       mpd_freeInfoEntity(entity);
442       list->data=NULL;
443       list=list->next;
444     }
445   g_list_free(subdir_list);
446   return 0;
448 #endif
450 int
451 browse_handle_select(screen_t *screen, 
452                      mpdclient_t *c,
453                      list_window_t *lw,
454                      mpdclient_filelist_t *filelist)
456   filelist_entry_t *entry;
458   if ( filelist==NULL )
459     return -1;
460   entry=( filelist_entry_t *) g_list_nth_data(filelist->list, lw->selected);
461   if( entry==NULL || entry->entity==NULL)
462     return -1;
464   if( entry->entity->type==MPD_INFO_ENTITY_TYPE_PLAYLISTFILE )
465     return load_playlist(screen, c, entry);
467   if( entry->entity->type==MPD_INFO_ENTITY_TYPE_DIRECTORY )
468     {
469       mpd_Directory *dir = entry->entity->info.directory;
470 #ifdef USE_OLD_ADD
471       add_directory(c, tmp);
472 #else
473       if( mpdclient_cmd_add_path_utf8(c, dir->path) == 0 )
474         {
475           char *tmp = utf8_to_locale(dir->path);
477           screen_status_printf(_("Adding \'%s\' to playlist\n"), tmp);
478           g_free(tmp);
479         }
480 #endif
481       return 0;
482     }
484   if( entry->entity->type!=MPD_INFO_ENTITY_TYPE_SONG )
485     return -1; 
487   if( entry->flags & HIGHLIGHT )
488     entry->flags &= ~HIGHLIGHT;
489   else
490     entry->flags |= HIGHLIGHT;
492   if( entry->flags & HIGHLIGHT )
493     {
494       if( entry->entity->type==MPD_INFO_ENTITY_TYPE_SONG )
495         {
496           mpd_Song *song = entry->entity->info.song;
498           if( mpdclient_cmd_add(c, song) == 0 )
499             {
500               char buf[BUFSIZE];
501               
502               strfsong(buf, BUFSIZE, LIST_FORMAT, song);
503               screen_status_printf(_("Adding \'%s\' to playlist\n"), buf);
504             }
505         }
506     }
507   else
508     {
509       /* remove song from playlist */
510       if( entry->entity->type==MPD_INFO_ENTITY_TYPE_SONG )
511         {
512           mpd_Song *song = entry->entity->info.song;
514           if( song )
515             {
516               int index = playlist_get_index_from_file(c, song->file);
517               
518               while( (index=playlist_get_index_from_file(c, song->file))>=0 )
519                 mpdclient_cmd_delete(c, index);
520             }
521         }
522     }
523   return 0;
526 int
527 browse_handle_select_all (screen_t *screen, 
528                      mpdclient_t *c,
529                      list_window_t *lw,
530                      mpdclient_filelist_t *filelist)
532   filelist_entry_t *entry;
533   GList *temp = filelist->list;
535   if ( filelist==NULL )
536     return -1;
537   for (filelist->list = g_list_first(filelist->list); 
538        filelist->list; 
539        filelist->list = g_list_next(filelist->list))
540     {
541   entry=( filelist_entry_t *) filelist->list->data;
542   if( entry==NULL || entry->entity==NULL)
543     return -1;
545   if( entry->entity->type==MPD_INFO_ENTITY_TYPE_PLAYLISTFILE )
546      load_playlist(screen, c, entry);
548   if( entry->entity->type==MPD_INFO_ENTITY_TYPE_DIRECTORY )
549     {
550       mpd_Directory *dir = entry->entity->info.directory;
551 #ifdef USE_OLD_ADD
552       add_directory(c, tmp);
553 #else
554       if( mpdclient_cmd_add_path_utf8(c, dir->path) == 0 )
555         {
556           char *tmp = utf8_to_locale(dir->path);
558           screen_status_printf(_("Adding \'%s\' to playlist\n"), tmp);
559           g_free(tmp);
560         }
561 #endif
562     }
564   if( entry->entity->type!=MPD_INFO_ENTITY_TYPE_SONG )
565     continue; 
567   if( entry->flags & HIGHLIGHT )
568     entry->flags &= ~HIGHLIGHT;
569   else
570     entry->flags |= HIGHLIGHT;
572   if( entry->flags & HIGHLIGHT )
573     {
574       if( entry->entity->type==MPD_INFO_ENTITY_TYPE_SONG )
575         {
576           mpd_Song *song = entry->entity->info.song;
578           if( mpdclient_cmd_add(c, song) == 0 )
579             {
580               char buf[BUFSIZE];
581               
582               strfsong(buf, BUFSIZE, LIST_FORMAT, song);
583               screen_status_printf(_("Adding \'%s\' to playlist\n"), buf);
584             }
585         }
586     }
587   /*else
588     {
589        //remove song from playlist 
590       if( entry->entity->type==MPD_INFO_ENTITY_TYPE_SONG )
591         {
592           mpd_Song *song = entry->entity->info.song;
594           if( song )
595             {
596               int index = playlist_get_index_from_file(c, song->file);
597               
598               while( (index=playlist_get_index_from_file(c, song->file))>=0 )
599                 mpdclient_cmd_delete(c, index);
600             }
601         }
602     }
603   return 0;*/
604     }
605   filelist->list = temp;
606   return 0;
609 static void
610 browse_init(WINDOW *w, int cols, int rows)
612   lw = list_window_init(w, cols, rows);
613   lw_state = list_window_init_state();
616 static void
617 browse_resize(int cols, int rows)
619   lw->cols = cols;
620   lw->rows = rows;
623 static void
624 browse_exit(void)
626   if( filelist )
627     filelist = mpdclient_filelist_free(filelist);
628   lw = list_window_free(lw);
629   lw_state = list_window_free_state(lw_state);
632 static void 
633 browse_open(screen_t *screen, mpdclient_t *c)
635   if( filelist == NULL )
636     {
637       filelist = mpdclient_filelist_get(c, "");
638       mpdclient_install_playlist_callback(c, playlist_changed_callback);
639       mpdclient_install_browse_callback(c, file_changed_callback);
640     }
643 static void
644 browse_close(void)
648 static char *
649 browse_title(char *str, size_t size)
651   char *pathcopy;
652   char *parentdir;
653   pathcopy = strdup(filelist->path);
654   parentdir = dirname(pathcopy);
655   parentdir = basename(parentdir);
656   if( parentdir[0] == '.' && strlen(parentdir) == 1 )
657     {
658       parentdir = NULL;
659     }
660   g_snprintf(str, size, _("Browse: %s%s%s"),
661              parentdir ? parentdir : "",
662              parentdir ? "/" : "",
663              basename(filelist->path));
664   free(pathcopy);
665   return str;
668 static void 
669 browse_paint(screen_t *screen, mpdclient_t *c)
671   lw->clear = 1;
672   
673   list_window_paint(lw, browse_lw_callback, (void *) filelist);
674   wnoutrefresh(lw->w);
677 static void 
678 browse_update(screen_t *screen, mpdclient_t *c)
680   if( filelist->updated )
681     {
682       browse_paint(screen, c);
683       filelist->updated = FALSE;
684       return;
685     }
686   list_window_paint(lw, browse_lw_callback, (void *) filelist);
687   wnoutrefresh(lw->w);
691 #ifdef HAVE_GETMOUSE
692 int
693 browse_handle_mouse_event(screen_t *screen, 
694                           mpdclient_t *c,
695                           list_window_t *lw,
696                           mpdclient_filelist_t *filelist)
698   int row;
699   int prev_selected = lw->selected;
700   unsigned long bstate;
701   int length;
703   if ( filelist )
704     length = filelist->length;
705   else
706     length = 0;
708   if( screen_get_mouse_event(c, lw, length, &bstate, &row) )
709     return 1;
711   lw->selected = lw->start+row;
712   list_window_check_selected(lw, length);
714   if( bstate & BUTTON1_CLICKED )
715     {
716       if( prev_selected == lw->selected )
717         browse_handle_enter(screen, c, lw, filelist);
718     }
719   else if( bstate & BUTTON3_CLICKED )
720     {
721       if( prev_selected == lw->selected )
722         browse_handle_select(screen, c, lw, filelist);
723     }
725   return 1;
727 #endif 
729 static int 
730 browse_cmd(screen_t *screen, mpdclient_t *c, command_t cmd)
732   switch(cmd)
733     {
734     case CMD_PLAY:
735       browse_handle_enter(screen, c, lw, filelist);
736       return 1;
737     case CMD_GO_ROOT_DIRECTORY:
738       return change_directory(screen, c, NULL, "");
739       break;
740     case CMD_GO_PARENT_DIRECTORY:
741       return change_directory(screen, c, NULL, "..");
742       break;
743     case CMD_SELECT:
744       if( browse_handle_select(screen, c, lw, filelist) == 0 )
745         {
746           /* continue and select next item... */
747           cmd = CMD_LIST_NEXT;
748         }
749       break;
750     case CMD_DELETE:
751       handle_delete(screen, c);
752       break;
753     case CMD_SAVE_PLAYLIST:
754       handle_save(screen, c);
755       break;
756     case CMD_SCREEN_UPDATE:
757       screen->painted = 0;
758       lw->clear = 1;
759       lw->repaint = 1;
760       filelist = mpdclient_filelist_update(c, filelist);
761       list_window_check_selected(lw, filelist->length);
762       screen_status_printf(_("Screen updated!"));
763       return 1;
764     case CMD_DB_UPDATE:
765       if( !c->status->updatingDb )
766         {
767           if( mpdclient_cmd_db_update_utf8(c,filelist->path)==0 )
768             {
769               if(strcmp(filelist->path,"")) {
770                  screen_status_printf(_("Database update of %s started!"),
771                                    filelist->path);
772               } else {
773                 screen_status_printf(_("Database update started!"));
774               }
775               /* set updatingDb to make shure the browse callback gets called
776                * even if the updated has finished before status is updated */
777               c->status->updatingDb = 1; 
778             }
779         }
780       else
781         screen_status_printf(_("Database update running..."));
782       return 1;
783     case CMD_LIST_FIND:
784     case CMD_LIST_RFIND:
785     case CMD_LIST_FIND_NEXT:
786     case CMD_LIST_RFIND_NEXT:
787       return screen_find(screen, c, 
788                          lw, filelist->length,
789                          cmd, browse_lw_callback, (void *) filelist);
790     case CMD_MOUSE_EVENT:
791       return browse_handle_mouse_event(screen,c,lw,filelist);
792     default:
793       break;
794     }
795   return list_window_cmd(lw, filelist->length, cmd);
799 list_window_t *
800 get_filelist_window()
802   return lw;
808 screen_functions_t *
809 get_screen_browse(void)
811   static screen_functions_t functions;
813   memset(&functions, 0, sizeof(screen_functions_t));
814   functions.init   = browse_init;
815   functions.exit   = browse_exit;
816   functions.open   = browse_open;
817   functions.close  = browse_close;
818   functions.resize = browse_resize;
819   functions.paint  = browse_paint;
820   functions.update = browse_update;
821   functions.cmd    = browse_cmd;
822   functions.get_lw = get_filelist_window;
823   functions.get_title = browse_title;
825   return &functions;