Support for updating clarity when a track is modified
[gtkpod:phantomjinx-gtkpod-plugin.git] / plugins / clarity / clarity_canvas.c
1 /*
2  |  Copyright (C) 2002-2011 Jorg Schuler <jcsjcs at users sourceforge net>
3  |                                             Paul Richardson <phantom_sf at users.sourceforge.net>
4  |  Part of the gtkpod project.
5  |
6  |  URL: http://www.gtkpod.org/
7  |  URL: http://gtkpod.sourceforge.net/
8  |
9  |  This program is free software; you can redistribute it and/or modify
10  |  it under the terms of the GNU General Public License as published by
11  |  the Free Software Foundation; either version 2 of the License, or
12  |  (at your option) any later version.
13  |
14  |  This program is distributed in the hope that it will be useful,
15  |  but WITHOUT ANY WARRANTY; without even the implied warranty of
16  |  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  |  GNU General Public License for more details.
18  |
19  |  You should have received a copy of the GNU General Public License
20  |  along with this program; if not, write to the Free Software
21  |  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22  |
23  |  iTunes and iPod are trademarks of Apple
24  |
25  |  This product is not supported/written/published by Apple!
26  |
27  */
28 #include <clutter-gtk/clutter-gtk.h>
29 #include "libgtkpod/gp_itdb.h"
30 #include "plugin.h"
31 #include "clarity_cover.h"
32 #include "clarity_canvas.h"
33 #include "clarity_preview.h"
34 #include "clarity_utils.h"
35
36 G_DEFINE_TYPE( ClarityCanvas, clarity_canvas, GTK_TYPE_BOX);
37
38 #define CLARITY_CANVAS_GET_PRIVATE(obj) \
39   (G_TYPE_INSTANCE_GET_PRIVATE ((obj), CLARITY_TYPE_CANVAS, ClarityCanvasPrivate))
40
41 #define MAX_ANGLE                       70
42 #define COVER_SPACE                    50
43 #define FRONT_COVER_SPACE     150
44 #define MAX_SCALE                          1.4
45 #define VISIBLE_ITEMS                     8
46
47 struct _ClarityCanvasPrivate {
48
49     AlbumModel *model;
50
51     // clutter embed widget
52     GtkWidget *embed;
53
54     // clutter items
55     GList *covers;
56     ClutterActor *container;
57     ClutterTimeline *timeline;
58     ClutterAlpha *alpha;
59     ClutterActor *title_text;
60     ClutterActor *artist_text;
61
62     gint curr_index;
63
64     gulong preview_signal;
65
66     gboolean loading_complete;
67 };
68
69 enum DIRECTION {
70     MOVE_LEFT = -1,
71     MOVE_RIGHT = 1
72 };
73
74 static void clarity_canvas_finalize(GObject *gobject) {
75     ClarityCanvasPrivate *priv = CLARITY_CANVAS(gobject)->priv;
76
77     //FIXME
78 //    g_list_free_full(priv->covers, clarity_cover_destroy);
79
80     if (G_IS_OBJECT(priv->alpha))
81         g_object_unref(priv->alpha);
82
83     if (G_IS_OBJECT(priv->timeline))
84         g_object_unref(priv->timeline);
85
86     if (GTK_IS_WIDGET(priv->embed))
87         gtk_widget_destroy(priv->embed);
88
89     /* call the parent class' finalize() method */
90     G_OBJECT_CLASS(clarity_canvas_parent_class)->finalize(gobject);
91 }
92
93 static void clarity_canvas_class_init(ClarityCanvasClass *klass) {
94     GObjectClass *gobject_class;
95
96     gobject_class = G_OBJECT_CLASS (klass);
97     gobject_class->finalize = clarity_canvas_finalize;
98
99     g_type_class_add_private(klass, sizeof(ClarityCanvasPrivate));
100 }
101
102 static gboolean _preview_cover_cb(GtkWidget *widget, GdkEvent *event, gpointer user_data) {
103     ClarityCanvas *ccanvas = CLARITY_CANVAS(widget);
104     ClarityCanvasPrivate *priv = ccanvas->priv;
105
106     if (!priv->model)
107         return TRUE;
108
109     AlbumItem *item = album_model_get_item_with_index(priv->model, priv->curr_index);
110
111     GtkWidget *dialog = clarity_preview_new(item);
112
113     /* Display the dialog */
114     gtk_widget_show_all(dialog);
115
116     return TRUE;
117 }
118
119 /**
120  * embed_widget_size_allocated_cb
121  *
122  * Ensures that when the embed gtk widget is resized or moved
123  * around the clutter animations are centred correctly.
124  *
125  * This finds the new dimensions of the stage each time and centres
126  * the group container accordingly.
127  *
128  */
129 void _embed_widget_size_allocated_cb(GtkWidget *widget,
130                       GtkAllocation *allocation,
131                       gpointer data) {
132     ClarityCanvasPrivate *priv = (ClarityCanvasPrivate *) data;
133     ClutterActor *stage = gtk_clutter_embed_get_stage(GTK_CLUTTER_EMBED(widget));
134
135     gint centreX = clutter_actor_get_width(stage) / 2;
136     gint centreY = clutter_actor_get_height(stage) / 2;
137     clutter_actor_set_position(priv->container, centreX, centreY);
138 }
139
140 static void clarity_canvas_init(ClarityCanvas *self) {
141     ClarityCanvasPrivate *priv;
142
143     self->priv = CLARITY_CANVAS_GET_PRIVATE (self);
144
145     priv = self->priv;
146
147     priv->title_text = clutter_text_new();
148     clutter_text_set_font_name(CLUTTER_TEXT(priv->title_text), "Sans");
149
150     priv->artist_text = clutter_text_new();
151     clutter_text_set_font_name(CLUTTER_TEXT(priv->title_text), "Sans");
152
153     priv->container = clutter_group_new();
154     clutter_actor_set_reactive(priv->container, TRUE);
155     priv->preview_signal = g_signal_connect (self,
156                                 "button-press-event",
157                                 G_CALLBACK (_preview_cover_cb),
158                                 priv);
159     clutter_container_add(CLUTTER_CONTAINER(priv->container), priv->title_text, priv->artist_text, NULL);
160
161     priv->embed = gtk_clutter_embed_new();
162     /*
163      * Minimum size before the scrollbars of the parent window
164      * are displayed.
165      */
166     gtk_widget_set_size_request(GTK_WIDGET(priv->embed), DEFAULT_IMG_SIZE * 4, DEFAULT_IMG_SIZE * 2.5);
167     /*
168      * Ensure that things are always centred when the embed
169      * widget is resized.
170      */
171     g_signal_connect(priv->embed, "size-allocate",
172                   G_CALLBACK(_embed_widget_size_allocated_cb), priv);
173
174     ClutterActor *stage = gtk_clutter_embed_get_stage(GTK_CLUTTER_EMBED(priv->embed));
175     clutter_container_add_actor(CLUTTER_CONTAINER(stage), priv->container);
176
177     gtk_widget_show(priv->embed);
178
179     gtk_box_pack_start(GTK_BOX(self), priv->embed, TRUE, TRUE, 0);
180
181     priv->covers = NULL;
182     priv->timeline = clutter_timeline_new(1600);
183     priv->alpha = clutter_alpha_new_full(priv->timeline, CLUTTER_EASE_OUT_EXPO);
184     priv->curr_index = 0;
185     priv->loading_complete = FALSE;
186
187 }
188
189 GtkWidget *clarity_canvas_new() {
190     return g_object_new(CLARITY_TYPE_CANVAS, NULL);
191 }
192
193 /**
194  * clarity_canvas_get_background_display_color:
195  *
196  * Returns the background color of the clarity canvas.
197  *
198  * The return value is a GdkRGBA
199  *
200  */
201 GdkRGBA *clarity_canvas_get_background_color(ClarityCanvas *self) {
202     g_return_val_if_fail(CLARITY_IS_CANVAS(self), NULL);
203
204     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
205
206     ClutterActor *stage = gtk_clutter_embed_get_stage(GTK_CLUTTER_EMBED(priv->embed));
207
208     ClutterColor *ccolor;
209     ccolor = g_malloc(sizeof(ClutterColor));
210
211     clutter_stage_get_color(CLUTTER_STAGE(stage), ccolor);
212     g_return_val_if_fail(ccolor, NULL);
213
214     GdkRGBA *rgba;
215     rgba = g_malloc(sizeof(GdkRGBA));
216     rgba->red = ((gdouble) ccolor->red) / 255;
217     rgba->green = ((gdouble) ccolor->green) / 255;
218     rgba->blue = ((gdouble) ccolor->blue) / 255;
219     rgba->alpha = ((gdouble) ccolor->alpha) / 255;
220
221     return rgba;
222 }
223
224 /**
225  * clarity_canvas_get_text_color:
226  *
227  * Returns the text color of the clarity text.
228  *
229  * The return value is a GdkRGBA
230  *
231  */
232 GdkRGBA *clarity_canvas_get_text_color(ClarityCanvas *self) {
233     g_return_val_if_fail(CLARITY_IS_CANVAS(self), NULL);
234
235     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
236
237     ClutterColor *ccolor;
238     ccolor = g_malloc(sizeof(ClutterColor));
239
240     clutter_text_get_color(CLUTTER_TEXT(priv->title_text), ccolor);
241     g_return_val_if_fail(ccolor, NULL);
242
243     GdkRGBA *rgba;
244     rgba = g_malloc(sizeof(GdkRGBA));
245     rgba->red = ((gdouble) ccolor->red) / 255;
246     rgba->green = ((gdouble) ccolor->green) / 255;
247     rgba->blue = ((gdouble) ccolor->blue) / 255;
248     rgba->alpha = ((gdouble) ccolor->alpha) / 255;
249
250     return rgba;
251 }
252
253 void clarity_canvas_set_background_color(ClarityCanvas *self, const gchar *color_string) {
254     g_return_if_fail(self);
255     g_return_if_fail(color_string);
256
257     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
258
259     ClutterActor *stage = gtk_clutter_embed_get_stage(GTK_CLUTTER_EMBED(priv->embed));
260
261     ClutterColor *ccolor;
262     ccolor = g_malloc(sizeof(ClutterColor));
263
264     clutter_color_from_string(ccolor, color_string);
265     clutter_stage_set_color(CLUTTER_STAGE(stage), ccolor);
266 }
267
268 void clarity_canvas_set_text_color(ClarityCanvas *self, const gchar *color_string) {
269     g_return_if_fail(self);
270     g_return_if_fail(color_string);
271
272     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
273
274     ClutterColor *ccolor;
275     ccolor = g_malloc(sizeof(ClutterColor));
276
277     clutter_color_from_string(ccolor, color_string);
278
279     clutter_text_set_color(CLUTTER_TEXT(priv->title_text), ccolor);
280     clutter_text_set_color(CLUTTER_TEXT(priv->artist_text), ccolor);
281 }
282
283 void clarity_canvas_clear(ClarityCanvas *self) {
284     g_return_if_fail(self);
285     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
286
287     if (CLUTTER_IS_ACTOR(priv->container)) {
288         GList *iter = priv->covers;
289         while(iter) {
290             ClarityCover *ccover = iter->data;
291             // cover is not referenced anywhere else so it should be destroyed too
292             clutter_container_remove(CLUTTER_CONTAINER(priv->container), CLUTTER_ACTOR(ccover), NULL);
293             iter = iter->next;
294         }
295
296         if (CLUTTER_IS_ACTOR(priv->artist_text))
297             clutter_text_set_text(CLUTTER_TEXT(priv->artist_text), "");
298
299         if (CLUTTER_IS_ACTOR(priv->title_text))
300             clutter_text_set_text(CLUTTER_TEXT(priv->title_text), "");
301     }
302
303     priv->covers = NULL;
304     priv->model = NULL;
305     priv->curr_index = 0;
306 }
307
308 static void _calculate_index_angle_and_dir (gint dist_from_front, enum DIRECTION dir, gint *angle, ClutterRotateDirection *rotation_dir) {
309     /* The front item direction depends on the direction we came from */
310     if (dist_from_front == 0) {
311         *rotation_dir =  (dir == MOVE_RIGHT ? CLUTTER_ROTATE_CCW : CLUTTER_ROTATE_CW);
312         *angle = 0;
313     }
314
315     /* Item on the right */
316     else if (dist_from_front > 0) {
317         *rotation_dir = CLUTTER_ROTATE_CCW;
318         *angle = 360 - MAX_ANGLE;
319     }
320
321     /* Item on the left */
322     else if (dist_from_front < 0) {
323         *rotation_dir = CLUTTER_ROTATE_CW;
324         *angle = MAX_ANGLE;
325     }
326 }
327
328 static gint _calculate_index_distance (gint dist_from_front) {
329     gint dist = ((ABS(dist_from_front) - 1) * COVER_SPACE) + FRONT_COVER_SPACE;
330
331     if (dist_from_front == 0)
332         return 0;
333
334     return (dist_from_front > 0 ? dist : 0 - dist);
335 }
336
337 static float _calculate_index_scale(gint dist_from_front) {
338     if (dist_from_front == 0)
339         return MAX_SCALE;
340     else
341         return 1;
342 }
343
344 static gint _calculate_index_opacity (gint dist_from_front) {
345     return CLAMP ( 255 * (VISIBLE_ITEMS - ABS(dist_from_front)) / VISIBLE_ITEMS, 0, 255);
346 }
347
348 static void _update_text(ClarityCanvasPrivate *priv) {
349     g_return_if_fail(priv);
350
351     if (g_list_length(priv->covers) == 0)
352             return;
353
354     ClarityCover *ccover = g_list_nth_data(priv->covers, priv->curr_index);
355
356     gchar *title = clarity_cover_get_title(ccover);
357     gchar *artist = clarity_cover_get_artist(ccover);
358
359     clutter_text_set_text(CLUTTER_TEXT(priv->title_text), title);
360     clutter_text_set_text(CLUTTER_TEXT(priv->artist_text), artist);
361
362     g_free(title);
363     g_free(artist);
364
365     clutter_actor_raise_top(priv->title_text);
366     clutter_actor_raise_top(priv->artist_text);
367
368     gfloat artistx = (clutter_actor_get_width(priv->artist_text) / 2) * -1;
369     gfloat artisty = ((clutter_actor_get_height(CLUTTER_ACTOR(ccover)) / 2) - 25) * -1;
370     clutter_actor_set_position(priv->artist_text, artistx, artisty);
371
372     gfloat titlex = (clutter_actor_get_width(priv->title_text) / 2) * -1;
373     gfloat titley = artisty - clutter_actor_get_height(priv->artist_text) - 2;
374     clutter_actor_set_position(priv->title_text, titlex, titley);
375 }
376
377 static void _display_clarity_cover(ClarityCover *ccover, gint index) {
378     ClutterTimeline  *timeline = clutter_timeline_new(1600 * 5);
379     ClutterAlpha *alpha = clutter_alpha_new_full (timeline, CLUTTER_EASE_OUT_EXPO);
380
381     gint opacity = _calculate_index_opacity(index);
382     clutter_actor_animate_with_alpha(CLUTTER_ACTOR(ccover), alpha, "opacity", opacity, NULL);
383     clutter_timeline_start (timeline);
384 }
385
386 static void _set_loading_complete(ClarityCanvasPrivate *priv, gboolean value) {
387     priv->loading_complete = value;
388
389     if (value) {
390         _update_text(priv);
391     }
392 }
393
394 static gboolean _create_cover_actors(ClarityCanvasPrivate *priv, AlbumItem *album_item, gint index) {
395     g_return_val_if_fail(priv, FALSE);
396
397     _set_loading_complete(priv, FALSE);
398
399     ClarityCover *ccover = clarity_cover_new();
400     clutter_actor_set_opacity(CLUTTER_ACTOR(ccover), 0);
401     priv->covers = g_list_insert(priv->covers, ccover, index);
402
403     clutter_container_add_actor(
404                             CLUTTER_CONTAINER(priv->container),
405                             CLUTTER_ACTOR(ccover));
406
407     clarity_cover_set_album_item(ccover, album_item);
408
409     gint pos = _calculate_index_distance(index);
410     clutter_actor_set_position(
411                 CLUTTER_ACTOR(ccover),
412                 pos - clutter_actor_get_width(CLUTTER_ACTOR(ccover)) / 2,
413                 110 - clutter_actor_get_height(CLUTTER_ACTOR(ccover)));
414
415     if((priv->curr_index + VISIBLE_ITEMS < index) ||
416             (priv->curr_index - VISIBLE_ITEMS > index)) {
417         _set_loading_complete(priv, TRUE);
418         return FALSE;
419     }
420
421     float scale = _calculate_index_scale(index);
422
423     gint angle;
424     ClutterRotateDirection rotation_dir;
425     _calculate_index_angle_and_dir(index, MOVE_LEFT, &angle, &rotation_dir);
426
427     clutter_actor_set_rotation(
428             CLUTTER_ACTOR(ccover),
429             CLUTTER_Y_AXIS,
430             angle,
431             clutter_actor_get_width(CLUTTER_ACTOR(ccover)) / 2,
432             0, 0);
433
434     clutter_actor_set_scale_full(
435             CLUTTER_ACTOR(ccover),
436             scale,
437             scale,
438             clutter_actor_get_width(CLUTTER_ACTOR(ccover)) / 2,
439             clutter_actor_get_height(CLUTTER_ACTOR(ccover)) / 2);
440
441     clutter_actor_lower_bottom(CLUTTER_ACTOR(ccover));
442
443     _display_clarity_cover(ccover, index);
444
445     _set_loading_complete(priv, TRUE);
446
447     return FALSE;
448 }
449
450 void _init_album_item(gpointer value, gint index, gpointer user_data) {
451     AlbumItem *item = (AlbumItem *) value;
452     ClarityCanvas *cc = CLARITY_CANVAS(user_data);
453     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(cc);
454
455     Track *track = g_list_nth_data(item->tracks, 0);
456     item->albumart = _get_track_image(track);
457     item->data = cc;
458
459     _create_cover_actors(priv, item, index);
460 }
461
462 void _destroy_cover(ClarityCanvas *cc, gint index) {
463     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(cc);
464
465     ClarityCover *ccover = (ClarityCover *) g_list_nth_data(priv->covers, index);
466     if (!ccover)
467         return;
468
469     priv->covers = g_list_remove(priv->covers, ccover);
470
471     clutter_container_remove_actor(
472                                CLUTTER_CONTAINER(priv->container),
473                                CLUTTER_ACTOR(ccover));
474
475     clarity_cover_destroy(CLUTTER_ACTOR(ccover));
476
477     return;
478 }
479
480 static gpointer _init_album_model(gpointer data) {
481     g_return_val_if_fail(CLARITY_IS_CANVAS(data), NULL);
482
483     ClarityCanvas *cc = CLARITY_CANVAS(data);
484     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(cc);
485     AlbumModel *model = priv->model;
486
487     album_model_foreach(model, _init_album_item, cc);
488
489     return NULL;
490 }
491
492 void clarity_canvas_init_album_model(ClarityCanvas *self, AlbumModel *model) {
493     g_return_if_fail(self);
494     g_return_if_fail(model);
495
496     if (album_model_get_size(model) == 0)
497         return;
498
499     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
500     priv->model = model;
501
502     _init_album_model(self);
503
504 }
505
506 static void _clear_rotation_behaviours(GList *covers) {
507     //Clear rotation behaviours
508     GList *iter = covers;
509     while (iter) {
510         ClarityCover *ccover = iter->data;
511         clarity_cover_clear_rotation_behaviour(ccover);
512         iter = iter->next;
513     }
514 }
515
516 static void _animate_indices(ClarityCanvasPrivate *priv, gint direction, gint increment) {
517
518     for (gint i = 0; i < g_list_length(priv->covers); ++i) {
519
520         ClarityCover *ccover = g_list_nth_data(priv->covers, i);
521
522         gint dist = i - priv->curr_index + (direction * increment);
523         gfloat depth = 1;
524         gint pos = 0;
525         gint opacity = 0;
526         gint angle = 0;
527         ClutterRotateDirection rotation_dir;
528
529         opacity = _calculate_index_opacity(dist);
530         depth = _calculate_index_scale(dist);
531         pos = _calculate_index_distance(dist);
532         _calculate_index_angle_and_dir(dist, direction, &angle, &rotation_dir);
533
534         /*Rotation*/
535         clarity_cover_set_rotation_behaviour(ccover, priv->alpha, angle, rotation_dir);
536
537         /* Opacity */
538         clutter_actor_animate_with_alpha (CLUTTER_ACTOR(ccover), priv->alpha,
539                         "opacity", opacity,
540                         NULL);
541
542         /* Position and scale */
543         clutter_actor_animate_with_alpha (CLUTTER_ACTOR(ccover), priv->alpha,
544                         "scale-x",          depth,
545                         "scale-y",          depth,
546                         "scale-center-x" ,  clutter_actor_get_width(CLUTTER_ACTOR(ccover)) / 2,
547                         "scale-center-y" ,  clutter_actor_get_height(CLUTTER_ACTOR(ccover)) / 2,
548                         "x", pos - clutter_actor_get_width(CLUTTER_ACTOR(ccover)) / 2,
549                         NULL);
550      }
551 }
552
553 static void _restore_z_order(ClarityCanvasPrivate *priv) {
554     g_return_if_fail(priv);
555
556     if (g_list_length(priv->covers) == 0)
557         return;
558
559     GList *main_cover = g_list_nth(priv->covers, priv->curr_index);
560     g_return_if_fail(main_cover);
561
562     GList *iter = main_cover ->prev;
563     while(iter) {
564         ClarityCover *ccover = iter->data;
565         clutter_actor_lower_bottom(CLUTTER_ACTOR(ccover));
566         iter = iter->prev;
567     }
568
569     iter = main_cover->next;
570     while(iter) {
571         ClarityCover *ccover = iter->data;
572         clutter_actor_lower_bottom(CLUTTER_ACTOR(ccover));
573         iter = iter->next;
574     }
575 }
576
577 static void _move(ClarityCanvasPrivate *priv, enum DIRECTION direction, gint increment) {
578
579     _set_loading_complete(priv, FALSE);
580
581     /* Stop any animation */
582     clutter_timeline_stop(priv->timeline);
583
584     /* Clear all current rotation behaviours */
585     _clear_rotation_behaviours(priv->covers);
586
587     /* Animate to move left */
588     _animate_indices (priv, direction, increment);
589
590     /* Begin the animation */
591     clutter_timeline_start(priv->timeline);
592
593     priv->curr_index += ((direction * -1) * increment);
594
595     _restore_z_order(priv);
596
597     _set_loading_complete(priv, TRUE);
598 }
599
600 void clarity_canvas_move_left(ClarityCanvas *self, gint increment) {
601     g_return_if_fail(self);
602     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
603
604     if(priv->curr_index == g_list_length(priv->covers) - 1)
605         return;
606
607     _move(priv, MOVE_LEFT, increment);
608 }
609
610 void clarity_canvas_move_right(ClarityCanvas *self, gint increment) {
611     g_return_if_fail(self);
612     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
613
614     if(priv->curr_index == 0)
615         return;
616
617     _move(priv, MOVE_RIGHT, increment);
618 }
619
620 gint clarity_canvas_get_current_index(ClarityCanvas *self) {
621     g_return_val_if_fail(self, 0);
622     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
623
624     return priv->curr_index;
625 }
626
627 gboolean clarity_canvas_is_loading(ClarityCanvas *self) {
628     g_return_val_if_fail(self, FALSE);
629     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
630     return !priv->loading_complete;
631 }
632
633 void clarity_canvas_add_album_item(ClarityCanvas *self, AlbumItem *item) {
634     g_return_if_fail(self);
635     g_return_if_fail(item);
636
637     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
638     gint index = album_model_get_index_with_album_item(priv->model, item);
639
640     _set_loading_complete(priv, FALSE);
641
642     _init_album_item(item, index, self);
643
644     _animate_indices(priv, 0, 0);
645
646     _set_loading_complete(priv, TRUE);
647 }
648
649 void clarity_canvas_remove_album_item(ClarityCanvas *self, AlbumItem *item) {
650     g_return_if_fail(self);
651     g_return_if_fail(item);
652
653     ClarityCanvasPrivate *priv = CLARITY_CANVAS_GET_PRIVATE(self);
654     gint index = album_model_get_index_with_album_item(priv->model, item);
655
656     _set_loading_complete(priv, FALSE);
657
658     _destroy_cover(self, index);
659
660     _animate_indices(priv, 0, 0);
661
662     _set_loading_complete(priv, TRUE);
663 }