Commit 60d524b3 authored by Lucas Beeler's avatar Lucas Beeler

Introduces the new MediaPage class that treats photos and videos uniformly. As...

Introduces the new MediaPage class that treats photos and videos uniformly. As a consequence, allows editing titles and setting ratings for videos. Closes #2584.
parent e0fd446e
......@@ -128,7 +128,8 @@ SRC_FILES = \
Tombstone.vala \
MetadataWriter.vala \
Application.vala \
DelayedQueue.vala
DelayedQueue.vala \
MediaPage.vala
ifndef LINUX
SRC_FILES += \
......@@ -156,6 +157,7 @@ RESOURCE_FILES = \
tags.ui \
trash.ui \
offline.ui \
media.ui \
shotwell.glade
SYS_INTEGRATION_FILES = \
......
This diff is collapsed.
......@@ -437,19 +437,19 @@ public class EditTitleCommand : SingleDataSourceCommand {
private string new_title;
private string? old_title;
public EditTitleCommand(LibraryPhoto photo, string new_title) {
base(photo, Resources.EDIT_TITLE_LABEL, Resources.EDIT_TITLE_TOOLTIP);
public EditTitleCommand(MediaSource source, string new_title) {
base(source, Resources.EDIT_TITLE_LABEL, Resources.EDIT_TITLE_TOOLTIP);
this.new_title = new_title;
old_title = photo.get_title();
old_title = source.get_title();
}
public override void execute() {
((LibraryPhoto) source).set_title(new_title);
((MediaSource) source).set_title(new_title);
}
public override void undo() {
((LibraryPhoto) source).set_title(old_title);
((MediaSource) source).set_title(old_title);
}
}
......@@ -863,22 +863,22 @@ public class SetRatingSingleCommand : SingleDataSourceCommand {
set_direct = false;
incrementing = is_incrementing;
last_rating = ((LibraryPhoto)source).get_rating();
last_rating = ((MediaSource) source).get_rating();
}
public override void execute() {
if (set_direct)
((LibraryPhoto) source).set_rating(new_rating);
((MediaSource) source).set_rating(new_rating);
else {
if (incrementing)
((LibraryPhoto) source).increase_rating();
((MediaSource) source).increase_rating();
else
((LibraryPhoto) source).decrease_rating();
((MediaSource) source).decrease_rating();
}
}
public override void undo() {
((LibraryPhoto) source).set_rating(last_rating);
((MediaSource) source).set_rating(last_rating);
}
}
......@@ -915,7 +915,7 @@ public class SetRatingCommand : MultipleDataSourceCommand {
foreach (DataView view in iter) {
DataSource source = view.get_source();
last_rating_map[source] = ((LibraryPhoto)source).get_rating();
last_rating_map[source] = ((MediaSource) source).get_rating();
}
}
......@@ -935,12 +935,12 @@ public class SetRatingCommand : MultipleDataSourceCommand {
public override void execute_on_source(DataSource source) {
if (set_direct)
((LibraryPhoto) source).set_rating(new_rating);
((MediaSource) source).set_rating(new_rating);
else {
if (incrementing)
((LibraryPhoto) source).increase_rating();
((MediaSource) source).increase_rating();
else
((LibraryPhoto) source).decrease_rating();
((MediaSource) source).decrease_rating();
}
// TODO: Replace this system with a mass set rating function (like Photo.set_event_many)
......@@ -951,7 +951,7 @@ public class SetRatingCommand : MultipleDataSourceCommand {
}
public override void undo_on_source(DataSource source) {
((LibraryPhoto) source).set_rating(last_rating_map[source]);
((MediaSource) source).set_rating(last_rating_map[source]);
if (++action_count % 50 == 0) {
LibraryPhoto.global.thaw_notifications();
......
......@@ -445,7 +445,7 @@ public class Config {
public void get_library_photos_sort(out bool sort_order, out int sort_by) {
sort_order = get_bool("/apps/shotwell/preferences/ui/library_photos_sort_ascending", false);
sort_by = get_int("/apps/shotwell/preferences/ui/library_photos_sort_by", CollectionPage.SortBy.EXPOSURE_DATE);
sort_by = get_int("/apps/shotwell/preferences/ui/library_photos_sort_by", MediaPage.SortBy.EXPOSURE_DATE);
}
public bool set_library_photos_sort(bool sort_order, int sort_by) {
......@@ -455,7 +455,7 @@ public class Config {
public void get_event_photos_sort(out bool sort_order, out int sort_by) {
sort_order = get_bool("/apps/shotwell/preferences/ui/event_photos_sort_ascending", true);
sort_by = get_int("/apps/shotwell/preferences/ui/event_photos_sort_by", CollectionPage.SortBy.EXPOSURE_DATE);
sort_by = get_int("/apps/shotwell/preferences/ui/event_photos_sort_by", MediaPage.SortBy.EXPOSURE_DATE);
}
public bool set_event_photos_sort(bool sort_order, int sort_by) {
......
......@@ -951,6 +951,16 @@ public abstract class MediaSource : ThumbnailSource {
}
public abstract File get_file();
public abstract string? get_title();
public abstract void set_title(string? title);
public abstract Rating get_rating();
public abstract void set_rating(Rating rating);
public abstract void increase_rating();
public abstract void decrease_rating();
public abstract Dimensions get_dimensions();
}
public abstract class PhotoSource : MediaSource {
......@@ -960,8 +970,6 @@ public abstract class PhotoSource : MediaSource {
public abstract time_t get_exposure_time();
public abstract Dimensions get_dimensions();
public abstract uint64 get_filesize();
public abstract PhotoMetadata? get_metadata();
......
......@@ -2555,7 +2555,11 @@ public class VideoTable : DatabaseTable {
public void set_exposure_time(VideoID video_id, time_t time) throws DatabaseError {
update_int64_by_id_2(video_id.id, "exposure_time", (int64) time);
}
public void set_rating(VideoID video_id, Rating rating) throws DatabaseError {
update_int64_by_id_2(video_id.id, "rating", rating.serialize());
}
public void remove_by_file(File file) throws DatabaseError {
Sqlite.Statement stmt;
int res = db.prepare_v2("DELETE FROM VideoTable WHERE filename=?", -1, out stmt);
......
......@@ -435,7 +435,7 @@ public class EventPage : CollectionPage {
init_page_context_menu("/EventContextMenu");
// hide this command in CollectionPage, as it does not apply here
set_item_hidden("/CollectionMenuBar/EventsMenu/JumpToEvent");
set_item_hidden("/MediaMenuBar/MenubarExtrasPlaceholder/EventsMenu/JumpToEvent");
set_item_hidden("/CollectionContextMenu/ContextJumpToEvent");
Event.global.items_altered.connect(on_events_altered);
......@@ -481,7 +481,7 @@ public class EventPage : CollectionPage {
}
protected override void on_photos_menu() {
set_item_sensitive("/CollectionMenuBar/PhotosMenu/MakePrimary",
set_item_sensitive("/MediaMenuBar/PhotosMenu/MakePrimary",
get_view().get_selected_count() == 1);
base.on_photos_menu();
......
......@@ -432,7 +432,7 @@ public class LibraryWindow : AppWindow {
Config.get_instance().set_sidebar_position(client_paned.position);
Config.get_instance().set_photo_thumbnail_scale(CollectionPage.get_photo_thumbnail_scale());
Config.get_instance().set_photo_thumbnail_scale(MediaPage.get_global_thumbnail_scale());
base.on_quit();
}
......
This diff is collapsed.
......@@ -83,6 +83,30 @@ public abstract class PageStub : Object, SidebarPage {
public abstract class Page : Gtk.ScrolledWindow, SidebarPage {
private const int CONSIDER_CONFIGURE_HALTED_MSEC = 400;
protected struct InjectedUIElement {
public string name;
public string action;
public Gtk.UIManagerItemType kind;
private InjectedUIElement(string name, string action, Gtk.UIManagerItemType kind) {
this.name = name;
this.action = action;
this.kind = kind;
}
public static InjectedUIElement create_menu_item(string name, string action) {
return InjectedUIElement(name, action, Gtk.UIManagerItemType.MENUITEM);
}
public static InjectedUIElement create_menu(string name, string action) {
return InjectedUIElement(name, action, Gtk.UIManagerItemType.MENU);
}
public static InjectedUIElement create_separator() {
return InjectedUIElement("", "", Gtk.UIManagerItemType.SEPARATOR);
}
}
public Gtk.UIManager ui = new Gtk.UIManager();
public Gtk.ActionGroup action_group = null;
public Gtk.ActionGroup common_action_group = null;
......@@ -476,6 +500,13 @@ public abstract class Page : Gtk.ScrolledWindow, SidebarPage {
}
}
protected void init_ui_inject_elements(string where, InjectedUIElement[] elements) {
foreach (InjectedUIElement element in elements) {
ui.add_ui(ui.new_merge_id(), where, element.name, element.action, element.kind,
false);
}
}
protected void init_ui_start(string ui_filename, string action_group_name,
Gtk.ActionEntry[]? entries = null, Gtk.ToggleActionEntry[]? toggle_entries = null) {
init_load_ui(ui_filename);
......
......@@ -1416,13 +1416,13 @@ public abstract class Photo : PhotoSource {
notify_altered(new Alteration("metadata", "master-dirty"));
}
public Rating get_rating() {
public override Rating get_rating() {
lock (row) {
return row.rating;
}
}
public void set_rating(Rating rating) {
public override void set_rating(Rating rating) {
bool committed = false;
lock (row) {
......@@ -1437,13 +1437,13 @@ public abstract class Photo : PhotoSource {
notify_altered(new Alteration("metadata", "rating"));
}
public void increase_rating() {
public override void increase_rating() {
lock (row) {
set_rating(row.rating.increase());
}
}
public void decrease_rating() {
public override void decrease_rating() {
lock (row) {
set_rating(row.rating.decrease());
}
......@@ -1607,13 +1607,13 @@ public abstract class Photo : PhotoSource {
}
}
public string? get_title() {
public override string? get_title() {
lock (row) {
return row.title;
}
}
public void set_title(string? title) {
public override void set_title(string? title) {
bool committed;
lock (row) {
committed = PhotoTable.get_instance().set_title(row.photo_id, title);
......
......@@ -93,17 +93,17 @@ public class TagPage : CollectionPage {
protected override void on_tags_menu() {
int selected_count = get_view().get_selected_count();
set_item_display("/CollectionMenuBar/TagsMenu/DeleteTag",
set_item_display("/MediaMenuBar/MenubarExtrasPlaceholder/TagsMenu/DeleteTag",
Resources.delete_tag_menu(tag.get_name()),
Resources.delete_tag_tooltip(tag.get_name(), tag.get_photos_count()),
true);
set_item_display("/CollectionMenuBar/TagsMenu/RenameTag",
set_item_display("/MediaMenuBar/MenubarExtrasPlaceholder/TagsMenu/RenameTag",
Resources.rename_tag_menu(tag.get_name()),
Resources.rename_tag_tooltip(tag.get_name()),
true);
set_item_display("/CollectionMenuBar/TagsMenu/RemoveTagFromPhotos",
set_item_display("/MediaMenuBar/MenubarExtrasPlaceholder/TagsMenu/RemoveTagFromPhotos",
Resources.untag_photos_menu(tag.get_name(), selected_count),
Resources.untag_photos_tooltip(tag.get_name(), selected_count),
selected_count > 0);
......
......@@ -35,31 +35,27 @@ public class Thumbnail : CheckerboardItem {
// was showing up in sysprof
private bool exposure = false;
public Thumbnail(LibraryPhoto photo, int scale = DEFAULT_SCALE) {
base(photo, photo.get_dimensions().get_scaled(scale, true), photo.get_name());
public Thumbnail(MediaSource source, int scale = DEFAULT_SCALE) {
base(source, source.get_dimensions(), source.get_name());
this.video = null;
this.photo = photo;
if (source is LibraryPhoto) {
this.video = null;
this.photo = (LibraryPhoto) source;
Tag.global.container_contents_altered.connect(on_tag_contents_altered);
Tag.global.items_altered.connect(on_tags_altered);
} else if (source is Video) {
this.video = (Video) source;
this.photo = null;
}
this.scale = scale;
original_dim = photo.get_dimensions();
original_dim = source.get_dimensions();
dim = original_dim.get_scaled(scale, true);
// if the photo's tags changes, update it here
Tag.global.container_contents_altered.connect(on_tag_contents_altered);
Tag.global.items_altered.connect(on_tags_altered);
LibraryPhoto.global.items_altered.connect(on_photos_altered);
}
public Thumbnail.for_video(Video video, int scale = DEFAULT_SCALE) {
base(video, video.get_frame_dimensions().get_scaled(scale, true), video.get_name());
this.video = video;
this.photo = null;
this.scale = scale;
original_dim = video.get_frame_dimensions();
dim = original_dim.get_scaled(scale, true);
LibraryPhoto.global.items_altered.connect(on_sources_altered);
Video.global.items_altered.connect(on_sources_altered);
}
~Thumbnail() {
......@@ -68,7 +64,8 @@ public class Thumbnail : CheckerboardItem {
Tag.global.container_contents_altered.disconnect(on_tag_contents_altered);
Tag.global.items_altered.disconnect(on_tags_altered);
LibraryPhoto.global.items_altered.disconnect(on_photos_altered);
LibraryPhoto.global.items_altered.disconnect(on_sources_altered);
Video.global.items_altered.disconnect(on_sources_altered);
}
private void update_tags() {
......@@ -114,18 +111,18 @@ public class Thumbnail : CheckerboardItem {
}
private void update_title() {
string title = (is_video()) ? video.get_name() : photo.get_name();
string title = get_media_source().get_name();
if (is_string_empty(title))
clear_title();
else
set_title(title);
}
private void on_photos_altered(Gee.Map<DataObject, Alteration> map) {
if (!exposure || !map.has_key(photo))
private void on_sources_altered(Gee.Map<DataObject, Alteration> map) {
if (!exposure || !map.has_key(get_media_source()))
return;
if ((!is_video()) && map.get(photo).has_detail("metadata", "name"))
if (map.get(get_media_source()).has_detail("metadata", "name"))
update_title();
}
......@@ -138,6 +135,13 @@ public class Thumbnail : CheckerboardItem {
return video;
}
public MediaSource get_media_source() {
if (is_video())
return video;
else
return photo;
}
//
// Comparators
//
......@@ -409,6 +413,6 @@ public class Thumbnail : CheckerboardItem {
}
public Rating get_rating() {
return (is_video()) ? Rating.UNRATED : photo.get_rating();
return get_media_source().get_rating();
}
}
......@@ -365,9 +365,75 @@ public class Video : VideoSource {
}
public override string get_name() {
lock (backing_row) {
if (!is_string_empty(backing_row.title))
return backing_row.title;
}
return get_basename();
}
public override string? get_title() {
lock (backing_row) {
return backing_row.title;
}
}
public override void set_title(string? title) {
lock (backing_row) {
if (backing_row.title == title)
return;
try {
VideoTable.get_instance().set_title(backing_row.video_id, title);
} catch (DatabaseError e) {
AppWindow.database_error(e);
return;
}
// if we didn't short-circuit return in the catch clause above, then the change was
// successfully committed to the database, so update it in the in-memory row cache
backing_row.title = title;
}
notify_altered(new Alteration("metadata", "name"));
}
public override Rating get_rating() {
lock (backing_row) {
return backing_row.rating;
}
}
public override void set_rating(Rating rating) {
lock (backing_row) {
if ((!rating.is_valid()) || (rating == backing_row.rating))
return;
try {
VideoTable.get_instance().set_rating(get_video_id(), rating);
} catch (DatabaseError e) {
AppWindow.database_error(e);
return;
}
// if we didn't short-circuit return in the catch clause above, then the change was
// successfully committed to the database, so update it in the in-memory row cache
backing_row.rating = rating;
}
notify_altered(new Alteration("metadata", "rating"));
}
public override void increase_rating() {
lock (backing_row) {
set_rating(backing_row.rating.increase());
}
}
public override void decrease_rating() {
lock (backing_row) {
set_rating(backing_row.rating.decrease());
}
}
public string get_basename() {
lock (backing_row) {
return Filename.display_basename(backing_row.filepath);
......@@ -397,6 +463,10 @@ public class Video : VideoSource {
return Dimensions(backing_row.width, backing_row.height);
}
}
public override Dimensions get_dimensions() {
return get_frame_dimensions();
}
public uint64 get_filesize() {
lock (backing_row) {
......
......@@ -4,7 +4,7 @@
* See the COPYING file in this distribution.
*/
public class VideosPage : CheckerboardPage {
public class VideosPage : MediaPage {
public class Stub : PageStub {
public Stub() {
}
......@@ -24,19 +24,28 @@ public class VideosPage : CheckerboardPage {
private class VideoView : Thumbnail {
public VideoView(Video video) {
base.for_video(video);
base(video);
}
}
private Gtk.HScale slider = null;
private int scale;
private ExporterUI exporter = null;
public VideosPage() {
base (_("Videos"));
init_ui("videos.ui", "/VideosMenuBar", "VideosActionGroup", create_actions(),
create_toggle_actions());
// add actions and customize MediaPage UI elements for videos
action_group.add_actions(create_actions(), this);
action_group.get_action("PlayVideo").is_important = true;
action_group.get_action("PhotosMenu").set_label(_("Vi_deos"));
action_group.get_action("FilterPhotos").set_label(_("_Filter Videos"));
action_group.get_action("SortPhotos").set_label(_("Sort _Videos"));
action_group.get_action("DisplayUnratedOrHigher").set_label(_("_All Videos"));
// inject menu extras
init_ui_inject_elements("/MediaMenuBar/EditMenu/EditExtrasPlaceholder",
create_edit_menu_injectables());
init_ui_inject_elements("/MediaMenuBar/PhotosMenu/PhotosExtrasExternalsPlaceholder",
create_videos_menu_injectables());
Gtk.Toolbar toolbar = get_toolbar();
......@@ -52,76 +61,60 @@ public class VideosPage : CheckerboardPage {
separator.set_draw(false);
toolbar.insert(separator, -1);
// thumbnail size slider
slider = new Gtk.HScale(CollectionPage.get_global_slider_adjustment());
slider.value_changed.connect(on_slider_changed);
slider.set_draw_value(false);
Gtk.ToolItem toolitem = new Gtk.ToolItem();
toolitem.add(slider);
toolitem.set_expand(false);
toolitem.set_size_request(200, -1);
toolitem.set_tooltip_text(_("Adjust the size of the thumbnails"));
// ratings filter button
MediaPage.FilterButton filter_button = create_filter_button();
connect_filter_button(filter_button);
toolbar.insert(filter_button, -1);
Gtk.SeparatorToolItem drawn_separator = new Gtk.SeparatorToolItem();
drawn_separator.set_expand(false);
drawn_separator.set_draw(true);
toolbar.insert(toolitem, -1);
toolbar.insert(drawn_separator, -1);
set_thumb_size(CollectionPage.slider_to_scale(slider.get_value()));
// zoom slider assembly
MediaPage.ZoomSliderAssembly zoom_slider_assembly = create_zoom_slider_assembly();
connect_slider(zoom_slider_assembly);
toolbar.insert(zoom_slider_assembly, -1);
get_view().monitor_source_collection(Video.global, new VideoViewManager(this), null);
get_view().selection_group_altered.connect(on_selection_altered);
on_selection_altered();
}
private Gtk.ToggleActionEntry[] create_toggle_actions() {
Gtk.ToggleActionEntry[] toggle_actions = new Gtk.ToggleActionEntry[0];
private static Page.InjectedUIElement[] create_edit_menu_injectables() {
Page.InjectedUIElement[] result = new Page.InjectedUIElement[0];
result += Page.InjectedUIElement.create_separator();
result += Page.InjectedUIElement.create_menu_item("DeleteVideo", "DeleteVideo");
Gtk.ToggleActionEntry titles = { "ViewTitle", null, TRANSLATABLE, "<Ctrl><Shift>T",
TRANSLATABLE, on_display_titles, Config.get_instance().get_display_photo_titles() };
titles.label = _("_Titles");
titles.tooltip = _("Display the title of each video");
toggle_actions += titles;
return result;
}
private static Page.InjectedUIElement[] create_videos_menu_injectables() {
Page.InjectedUIElement[] result = new Page.InjectedUIElement[0];
result += Page.InjectedUIElement.create_menu_item("PlayVideo", "PlayVideo");
return toggle_actions;
return result;
}
private static Gtk.ActionEntry[] create_actions() {
Gtk.ActionEntry[] actions = new Gtk.ActionEntry[0];
Gtk.ActionEntry file = { "FileMenu", null, TRANSLATABLE, null, TRANSLATABLE, null };
file.label = _("_File");
actions += file;
Gtk.ActionEntry export_video = { "ExportVideo", Gtk.STOCK_SAVE_AS, TRANSLATABLE, "<Ctrl><Shift>E",
TRANSLATABLE, on_export_video };
export_video.label = Resources.EXPORT_MENU;
export_video.tooltip = _("Export the selected videos to disk");
actions += export_video;
Gtk.ActionEntry delete_video = { "DeleteVideo", Gtk.STOCK_DELETE, TRANSLATABLE, "Delete",
TRANSLATABLE, on_delete_video };
delete_video.label = _("_Delete");
delete_video.tooltip = _("Deletes the selected videos from your library and from disk");
actions += delete_video;
Gtk.ActionEntry edit = { "EditMenu", null, TRANSLATABLE, null, TRANSLATABLE, on_edit_menu };
edit.label = _("_Edit");
actions += edit;
Gtk.ActionEntry play = { "PlayVideo", Gtk.STOCK_MEDIA_PLAY, TRANSLATABLE, "<Ctrl>P",
TRANSLATABLE, on_play_video };
play.label = _("_Play Video");
play.tooltip = _("Open the selected videos in the system video player");
actions += play;
Gtk.ActionEntry view = { "ViewMenu", null, TRANSLATABLE, null, TRANSLATABLE, null };
view.label = _("_View");
actions += view;
Gtk.ActionEntry help = { "HelpMenu", null, TRANSLATABLE, null, TRANSLATABLE, null };
help.label = _("_Help");
actions += help;
return actions;
}
......@@ -129,17 +122,6 @@ public class VideosPage : CheckerboardPage {
return new Stub();
}
protected override void init_actions(int selected_count, int count) {
base.init_actions(selected_count, count);
action_group.get_action("PlayVideo").is_important = true;
}
protected override void switched_to() {
set_thumb_size(CollectionPage.slider_to_scale(slider.get_value()));
base.switched_to();
}
protected override void on_item_activated(CheckerboardItem item, CheckerboardPage.Activator
activator, CheckerboardPage.KeyboardModifiers modifiers) {
on_play_video();
......@@ -147,18 +129,16 @@ public class VideosPage : CheckerboardPage {
private void on_selection_altered() {
set_action_sensitive("PlayVideo", get_view().get_selected_count() == 1);
bool export_delete_sensitive = (get_view().get_selected_count() > 0);
set_action_sensitive("ExportVideo", export_delete_sensitive);
set_action_sensitive("DeleteVideo", export_delete_sensitive);
set_action_sensitive("DeleteVideo", get_view().get_selected_count() > 0);
}
private void on_display_titles(Gtk.Action action) {
bool display = ((Gtk.ToggleAction) action).get_active();
set_display_titles(display);
Config.get_instance().set_display_photo_titles(display);
protected override void get_config_photos_sort(out bool sort_order, out int sort_by) {
Config.get_instance().get_library_photos_sort(out sort_order, out sort_by);
}
protected override void set_config_photos_sort(bool sort_order, int sort_by) {
Config.get_instance().set_library_photos_sort(sort_order, sort_by);
}
private void on_play_video() {
......@@ -188,7 +168,7 @@ public class VideosPage : CheckerboardPage {
Video.global.destroy_marked(destroy_marker, true);
}
private void on_export_video() {
protected override void on_export() {
if (exporter != null)
return;
......@@ -234,40 +214,10 @@ public class VideosPage : CheckerboardPage {
private void on_export_completed() {
exporter = null;
}
private void on_edit_menu() {
decorate_undo_item("/VideosMenuBar/EditMenu/Undo");
decorate_redo_item("/VideosMenuBar/EditMenu/Redo");
}
private void on_slider_changed() {
set_thumb_size(CollectionPage.slider_to_scale(slider.get_value()));
}
public override CheckerboardItem? get_fullscreen_photo() {
return null;
}
public void increase_thumb_size() {
set_thumb_size(scale + CollectionPage.MANUAL_STEPPING);
}
public void decrease_thumb_size() {
set_thumb_size(scale - CollectionPage.MANUAL_STEPPING);
}
public void set_thumb_size(int new_scale) {
scale = new_scale.clamp(Thumbnail.MIN_SCALE, Thumbnail.MAX_SCALE);
get_checkerboard_layout().set_scale(scale);
get_view().freeze_notifications();
get_view().set_property(Thumbnail.PROP_SIZE, scale);
get_view().thaw_notifications();
}