|
|
@@ -74,6 +74,12 @@ BibliothecaWindow::BibliothecaWindow(DatabaseManager& db, BookList& bookList)
|
|
|
background-color: #26a269;
|
|
|
color: white;
|
|
|
}
|
|
|
+ .batch-action-bar {
|
|
|
+ background-color: @theme_bg_color;
|
|
|
+ border-radius: 8px 8px 0 0;
|
|
|
+ padding: 8px 16px;
|
|
|
+ border-top: 1px solid @borders;
|
|
|
+ }
|
|
|
|
|
|
)CSS");
|
|
|
|
|
|
@@ -83,6 +89,7 @@ BibliothecaWindow::BibliothecaWindow(DatabaseManager& db, BookList& bookList)
|
|
|
// Build the main views
|
|
|
buildPlaceholder();
|
|
|
buildToast();
|
|
|
+ buildBatchActionBar();
|
|
|
|
|
|
m_noResults.set_orientation(Gtk::Orientation::VERTICAL);
|
|
|
m_noResults.set_spacing(12);
|
|
|
@@ -95,6 +102,7 @@ BibliothecaWindow::BibliothecaWindow(DatabaseManager& db, BookList& bookList)
|
|
|
|
|
|
m_shelf = std::make_unique<BookShelf>(m_bookList, /*coverSize*/ 180);
|
|
|
m_shelf->signalBookActivated().connect(sigc::mem_fun(*this, &BibliothecaWindow::onBookActivated));
|
|
|
+ m_shelf->signalSelectionChanged().connect(sigc::mem_fun(*this, &BibliothecaWindow::updateBatchActionBar));
|
|
|
|
|
|
m_searchEntry.set_placeholder_text("Search your library…");
|
|
|
m_searchEntry.signal_changed().connect(sigc::mem_fun(*this, &BibliothecaWindow::onSearchChanged));
|
|
|
@@ -134,6 +142,7 @@ BibliothecaWindow::BibliothecaWindow(DatabaseManager& db, BookList& bookList)
|
|
|
m_mainBox.set_spacing(0);
|
|
|
m_mainBox.append(m_searchBar);
|
|
|
m_mainBox.append(m_overlay);
|
|
|
+ m_mainBox.append(m_batchActionRevealer);
|
|
|
|
|
|
set_child(m_mainBox);
|
|
|
|
|
|
@@ -266,6 +275,131 @@ void BibliothecaWindow::hideToast() {
|
|
|
m_toastRevealer.set_reveal_child(false);
|
|
|
}
|
|
|
|
|
|
+void BibliothecaWindow::buildBatchActionBar() {
|
|
|
+ m_batchActionRevealer.set_transition_type(Gtk::RevealerTransitionType::SLIDE_UP);
|
|
|
+ m_batchActionRevealer.set_transition_duration(150);
|
|
|
+ m_batchActionRevealer.set_reveal_child(false);
|
|
|
+
|
|
|
+ m_batchActionBox.set_spacing(8);
|
|
|
+ m_batchActionBox.add_css_class("batch-action-bar");
|
|
|
+ m_batchActionBox.set_halign(Gtk::Align::CENTER);
|
|
|
+ m_batchActionBox.set_margin_top(8);
|
|
|
+ m_batchActionBox.set_margin_bottom(8);
|
|
|
+ m_batchActionBox.set_margin_start(12);
|
|
|
+ m_batchActionBox.set_margin_end(12);
|
|
|
+
|
|
|
+ m_batchSelectionLabel.set_hexpand(false);
|
|
|
+
|
|
|
+ m_batchSelectAllButton.set_label("Select All");
|
|
|
+ m_batchSelectAllButton.add_css_class("flat");
|
|
|
+ m_batchSelectAllButton.signal_clicked().connect(sigc::mem_fun(*this, &BibliothecaWindow::onBatchSelectAllClicked));
|
|
|
+
|
|
|
+ m_batchClearButton.set_label("Clear");
|
|
|
+ m_batchClearButton.add_css_class("flat");
|
|
|
+ m_batchClearButton.signal_clicked().connect(sigc::mem_fun(*this, &BibliothecaWindow::onBatchClearSelectionClicked));
|
|
|
+
|
|
|
+ m_batchTagButton.set_label("Add Tag");
|
|
|
+ m_batchTagButton.set_icon_name("tag-symbolic");
|
|
|
+ m_batchTagButton.add_css_class("suggested-action");
|
|
|
+ m_batchTagButton.signal_clicked().connect(sigc::mem_fun(*this, &BibliothecaWindow::onBatchTagClicked));
|
|
|
+
|
|
|
+ m_batchDeleteButton.set_label("Delete");
|
|
|
+ m_batchDeleteButton.set_icon_name("user-trash-symbolic");
|
|
|
+ m_batchDeleteButton.add_css_class("destructive-action");
|
|
|
+ m_batchDeleteButton.signal_clicked().connect(sigc::mem_fun(*this, &BibliothecaWindow::onBatchDeleteClicked));
|
|
|
+
|
|
|
+ // Build tag popover
|
|
|
+ m_tagPopoverBox.set_spacing(6);
|
|
|
+ m_tagPopoverBox.set_margin_top(6);
|
|
|
+ m_tagPopoverBox.set_margin_bottom(6);
|
|
|
+ m_tagPopoverBox.set_margin_start(6);
|
|
|
+ m_tagPopoverBox.set_margin_end(6);
|
|
|
+
|
|
|
+ m_tagPopoverEntry.set_placeholder_text("Enter tag name...");
|
|
|
+ m_tagPopoverEntry.signal_activate().connect([this]() {
|
|
|
+ m_tagPopoverApplyButton.activate();
|
|
|
+ });
|
|
|
+
|
|
|
+ m_tagPopoverApplyButton.set_label("Apply");
|
|
|
+ m_tagPopoverApplyButton.add_css_class("suggested-action");
|
|
|
+ m_tagPopoverApplyButton.signal_clicked().connect([this]() {
|
|
|
+ const auto tag = m_tagPopoverEntry.get_text();
|
|
|
+ if (tag.empty()) return;
|
|
|
+
|
|
|
+ auto selected = m_shelf->getSelectedBooks();
|
|
|
+ if (selected.empty()) return;
|
|
|
+
|
|
|
+ for (const auto& book : selected) {
|
|
|
+ m_db.add_tag_to_book(book.id(), tag.raw());
|
|
|
+ }
|
|
|
+
|
|
|
+ m_tagPopover.popdown();
|
|
|
+ m_tagPopoverEntry.set_text("");
|
|
|
+ m_bookList.loadAll();
|
|
|
+ showToast(Glib::ustring::compose("Tagged %1 book%2", selected.size(), selected.size() == 1 ? "" : "s"), false);
|
|
|
+ });
|
|
|
+
|
|
|
+ m_tagPopoverBox.append(m_tagPopoverEntry);
|
|
|
+ m_tagPopoverBox.append(m_tagPopoverApplyButton);
|
|
|
+ m_tagPopover.set_child(m_tagPopoverBox);
|
|
|
+ m_tagPopover.set_parent(m_batchTagButton);
|
|
|
+ m_tagPopover.set_position(Gtk::PositionType::TOP);
|
|
|
+
|
|
|
+ m_batchActionBox.append(m_batchSelectionLabel);
|
|
|
+ m_batchActionBox.append(m_batchSelectAllButton);
|
|
|
+ m_batchActionBox.append(m_batchClearButton);
|
|
|
+ m_batchActionBox.append(m_batchTagButton);
|
|
|
+ m_batchActionBox.append(m_batchDeleteButton);
|
|
|
+
|
|
|
+ m_batchActionRevealer.set_child(m_batchActionBox);
|
|
|
+}
|
|
|
+
|
|
|
+void BibliothecaWindow::updateBatchActionBar() {
|
|
|
+ if (!m_shelf) return;
|
|
|
+
|
|
|
+ const auto count = m_shelf->getSelectionCount();
|
|
|
+ const bool has_selection = count > 0;
|
|
|
+ const auto visible_child = m_stack.get_visible_child_name();
|
|
|
+
|
|
|
+ // Only show action bar on shelf view with selection
|
|
|
+ m_batchActionRevealer.set_reveal_child(has_selection && visible_child == "shelf");
|
|
|
+
|
|
|
+ if (has_selection) {
|
|
|
+ m_batchSelectionLabel.set_text(Glib::ustring::compose("%1 selected", count));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void BibliothecaWindow::onBatchTagClicked() {
|
|
|
+ m_tagPopoverEntry.set_text("");
|
|
|
+ m_tagPopover.popup();
|
|
|
+ m_tagPopoverEntry.grab_focus();
|
|
|
+}
|
|
|
+
|
|
|
+void BibliothecaWindow::onBatchDeleteClicked() {
|
|
|
+ auto selected = m_shelf->getSelectedBooks();
|
|
|
+ if (selected.empty()) return;
|
|
|
+
|
|
|
+ // Delete all selected books
|
|
|
+ for (const auto& book : selected) {
|
|
|
+ m_bookList.removeById(book.id());
|
|
|
+ }
|
|
|
+
|
|
|
+ showToast(Glib::ustring::compose("Deleted %1 book%2", selected.size(), selected.size() == 1 ? "" : "s"), false);
|
|
|
+ updateVisibleView();
|
|
|
+}
|
|
|
+
|
|
|
+void BibliothecaWindow::onBatchSelectAllClicked() {
|
|
|
+ if (m_shelf) {
|
|
|
+ m_shelf->selectAll();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void BibliothecaWindow::onBatchClearSelectionClicked() {
|
|
|
+ if (m_shelf) {
|
|
|
+ m_shelf->clearSelection();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
void BibliothecaWindow::showPlaceholderIfEmpty() {
|
|
|
updateVisibleView();
|
|
|
}
|