From ce75cd55bb72bd00606ccd3664174dcb03eaf70b Mon Sep 17 00:00:00 2001 From: Daniel Viegas Date: Sat, 29 Nov 2025 21:42:10 +0100 Subject: [PATCH] Convert plugin to Event-based view and improve filter dialog - Convert plugin from Family-based to Event-based view * Change category from Families to Events * Update navigation_type to 'Event' * Replace FamilyBookmarks with EventBookmarks * Rewrite collect_events() to show all events in database * Update goto_handle() to work with event handles - Update filter dialog to show families with members * Restructure person filter page with expandable families * Each family shows father, mother, and children * Add helper method to generate family display names - Restore person connection lines * Re-enable visual connections between events of selected person * Clicking an event selects the person and shows connections - Add uninstall script * Remove plugin files and backup directories * Clean up any plugin files in subdirectories --- MyTimeline.gpr.py | 4 +- MyTimeline.py | 421 ++++++++++++++++++++++++++++++----------- uninstall_from_snap.sh | 154 +++++++++++++++ 3 files changed, 466 insertions(+), 113 deletions(-) create mode 100755 uninstall_from_snap.sh diff --git a/MyTimeline.gpr.py b/MyTimeline.gpr.py index 334c3ec..f2de1e1 100644 --- a/MyTimeline.gpr.py +++ b/MyTimeline.gpr.py @@ -34,14 +34,14 @@ register( VIEW, id="mytimelineview", name=_("MyTimeline"), - description=_("A vertical timeline view showing family events including birth, death, and marriage"), + description=_("A vertical timeline view showing all events in the database"), version="1.0", gramps_target_version=MODULE_VERSION, status=STABLE, fname="MyTimeline.py", authors=["Daniel Viegas"], authors_email=["dlviegas@gmail.com"], - category=("Families", _("Families")), + category=("Events", _("Events")), viewclass="MyTimelineView", ) diff --git a/MyTimeline.py b/MyTimeline.py index 39ad5db..dc7df9b 100644 --- a/MyTimeline.py +++ b/MyTimeline.py @@ -19,7 +19,7 @@ # """ -MyTimeline View - A vertical timeline showing family events +MyTimeline View - A vertical timeline showing all events in the database """ # ------------------------------------------------------------------------- @@ -29,6 +29,8 @@ MyTimeline View - A vertical timeline showing family events # ------------------------------------------------------------------------- import cairo import logging + +logger = logging.getLogger("plugin.mytimeline") import math from dataclasses import dataclass from typing import Optional, List, Tuple, Any, Set, Dict @@ -54,7 +56,7 @@ from gramps.gen.utils.db import ( from gramps.gen.datehandler import get_date from gramps.gen.display.name import displayer as name_displayer from gramps.gui.views.navigationview import NavigationView -from gramps.gui.views.bookmarks import FamilyBookmarks +from gramps.gui.views.bookmarks import EventBookmarks _ = glocale.translation.sgettext @@ -340,8 +342,9 @@ class TimelineEvent: # ------------------------------------------------------------------------- class MyTimelineView(NavigationView): """ - View for displaying a vertical timeline of family events. - Shows all events for family members with modern design and interactivity. + View for displaying a vertical timeline of all events in the database. + Shows all events with modern design and interactivity, allowing selection + and filtering of events by type, category, person, and date range. """ def __init__(self, pdata, dbstate, uistate, nav_group=0): @@ -355,14 +358,14 @@ class MyTimelineView(NavigationView): nav_group: Navigation group identifier. """ NavigationView.__init__( - self, _("MyTimeline"), pdata, dbstate, uistate, FamilyBookmarks, nav_group + self, _("MyTimeline"), pdata, dbstate, uistate, EventBookmarks, nav_group ) self.dbstate = dbstate self.uistate = uistate - # Current family handle - self.active_family_handle = None + # Current event handle (for selection/highlighting) + self.active_event_handle = None self.events: List[TimelineEvent] = [] # List of TimelineEvent objects # UI components @@ -419,20 +422,26 @@ class MyTimelineView(NavigationView): Return the navigation type for this view. Returns: - str: The navigation type, always "Family" for this view. + str: The navigation type, always "Event" for this view. """ - return "Family" + return "Event" def change_page(self) -> None: """ Called when the page changes. - Updates the view to show the active family's timeline. + Updates the view to show all events and highlight the active event if selected. """ NavigationView.change_page(self) active_handle = self.get_active() if active_handle: self.goto_handle(active_handle) + else: + # If no event is selected, ensure timeline is displayed + if not self.events and self.dbstate.is_open(): + self.collect_events() + if self.drawing_area: + self.drawing_area.queue_draw() def family_updated(self, handle_list: List[str]) -> None: """ @@ -441,10 +450,10 @@ class MyTimelineView(NavigationView): Args: handle_list: List of family handles that were updated. """ - if self.active_family_handle in handle_list: - self.collect_events() - if self.drawing_area: - self.drawing_area.queue_draw() + # Refresh timeline since family updates may affect event display + self.collect_events() + if self.drawing_area: + self.drawing_area.queue_draw() def person_updated(self, handle_list: List[str]) -> None: """ @@ -453,25 +462,10 @@ class MyTimelineView(NavigationView): Args: handle_list: List of person handles that were updated. """ - # Check if any updated person is related to current family - if self.active_family_handle: - try: - family = self.dbstate.db.get_family_from_handle(self.active_family_handle) - if family: - father_handle = family.get_father_handle() - mother_handle = family.get_mother_handle() - child_handles = [ref.ref for ref in family.get_child_ref_list()] - if ( - (father_handle and father_handle in handle_list) - or (mother_handle and mother_handle in handle_list) - or any(h in handle_list for h in child_handles) - ): - self.collect_events() - if self.drawing_area: - self.drawing_area.queue_draw() - except (AttributeError, KeyError) as e: - # Skip if family handle is invalid - logger.warning(f"Error accessing family in person_updated: {e}", exc_info=True) + # Refresh timeline since person updates may affect event display + self.collect_events() + if self.drawing_area: + self.drawing_area.queue_draw() def event_updated(self, handle_list: List[str]) -> None: """ @@ -480,11 +474,14 @@ class MyTimelineView(NavigationView): Args: handle_list: List of event handles that were updated. """ - # Re-collect events if we have an active family - if self.active_family_handle: - self.collect_events() - if self.drawing_area: - self.drawing_area.queue_draw() + # Re-collect events when events are updated + self.collect_events() + if self.drawing_area: + self.drawing_area.queue_draw() + + # If the active event was updated, ensure it's still highlighted + if self.active_event_handle in handle_list: + self._scroll_to_event(self.active_event_handle) def change_db(self, db) -> None: """ @@ -493,7 +490,7 @@ class MyTimelineView(NavigationView): Args: db: The new database object. """ - self.active_family_handle = None + self.active_event_handle = None self.all_events = [] self.events = [] @@ -745,7 +742,7 @@ class MyTimelineView(NavigationView): def _build_person_filter_page(self) -> Gtk.Widget: """ - Build the person filter page with checkboxes for each family member. + Build the person filter page with expandable families showing their members. Returns: Gtk.Widget: The person filter page. @@ -763,8 +760,10 @@ class MyTimelineView(NavigationView): person_checkboxes = {} self._filter_widgets['person_checkboxes'] = person_checkboxes self._filter_widgets['person_container'] = box + # Store expanders by family handle + self._filter_widgets['family_expanders'] = {} - info_label = Gtk.Label(label=_("Select family members to include in the timeline.")) + info_label = Gtk.Label(label=_("Select families and their members to include in the timeline.")) info_label.set_line_wrap(True) box.pack_start(info_label, False, False, 0) @@ -929,50 +928,97 @@ class MyTimelineView(NavigationView): else: checkbox.set_active(category in self.category_filter) - # Update person checkboxes + # Update person checkboxes with families if 'person_checkboxes' in self._filter_widgets and 'person_container' in self._filter_widgets: - # Clear existing person checkboxes + # Clear existing person checkboxes and family expanders container = self._filter_widgets['person_container'] + + # Remove all existing expanders + if 'family_expanders' in self._filter_widgets: + for expander in list(self._filter_widgets['family_expanders'].values()): + container.remove(expander) + expander.destroy() + self._filter_widgets['family_expanders'].clear() + + # Remove all existing checkboxes for checkbox in list(self._filter_widgets['person_checkboxes'].values()): container.remove(checkbox) checkbox.destroy() self._filter_widgets['person_checkboxes'].clear() - # Add current family members - if self.active_family_handle: + # Collect all families and create expanders + if self.dbstate.is_open(): try: - family = self.dbstate.db.get_family_from_handle(self.active_family_handle) - if family: - # Father + # Initialize family_expanders if not exists + if 'family_expanders' not in self._filter_widgets: + self._filter_widgets['family_expanders'] = {} + + # Iterate through all families + for family in self.dbstate.db.iter_families(): + family_handle = family.get_handle() + + # Get family display name + family_name = self._get_family_display_name(family) + + # Create expander for this family + expander = Gtk.Expander(label=family_name) + expander.set_expanded(False) + + # Create container for family members + members_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=3) + members_box.set_margin_start(20) + members_box.set_margin_top(5) + members_box.set_margin_bottom(5) + + # Add father checkbox father_handle = family.get_father_handle() if father_handle: - father = self.dbstate.db.get_person_from_handle(father_handle) - if father: - checkbox = Gtk.CheckButton(label=name_displayer.display(father)) - checkbox.set_active(True if not self.person_filter else father_handle in self.person_filter) - self._filter_widgets['person_checkboxes'][father_handle] = checkbox - container.pack_start(checkbox, False, False, 0) + try: + father = self.dbstate.db.get_person_from_handle(father_handle) + if father: + father_label = f" {_('Father')}: {name_displayer.display(father)}" + checkbox = Gtk.CheckButton(label=father_label) + checkbox.set_active(True if not self.person_filter else father_handle in self.person_filter) + self._filter_widgets['person_checkboxes'][father_handle] = checkbox + members_box.pack_start(checkbox, False, False, 0) + except (AttributeError, KeyError): + pass - # Mother + # Add mother checkbox mother_handle = family.get_mother_handle() if mother_handle: - mother = self.dbstate.db.get_person_from_handle(mother_handle) - if mother: - checkbox = Gtk.CheckButton(label=name_displayer.display(mother)) - checkbox.set_active(True if not self.person_filter else mother_handle in self.person_filter) - self._filter_widgets['person_checkboxes'][mother_handle] = checkbox - container.pack_start(checkbox, False, False, 0) + try: + mother = self.dbstate.db.get_person_from_handle(mother_handle) + if mother: + mother_label = f" {_('Mother')}: {name_displayer.display(mother)}" + checkbox = Gtk.CheckButton(label=mother_label) + checkbox.set_active(True if not self.person_filter else mother_handle in self.person_filter) + self._filter_widgets['person_checkboxes'][mother_handle] = checkbox + members_box.pack_start(checkbox, False, False, 0) + except (AttributeError, KeyError): + pass - # Children + # Add children checkboxes for child_ref in family.get_child_ref_list(): - child = self.dbstate.db.get_person_from_handle(child_ref.ref) - if child: - checkbox = Gtk.CheckButton(label=name_displayer.display(child)) - checkbox.set_active(True if not self.person_filter else child_ref.ref in self.person_filter) - self._filter_widgets['person_checkboxes'][child_ref.ref] = checkbox - container.pack_start(checkbox, False, False, 0) + child_handle = child_ref.ref + try: + child = self.dbstate.db.get_person_from_handle(child_handle) + if child: + child_label = f" {_('Child')}: {name_displayer.display(child)}" + checkbox = Gtk.CheckButton(label=child_label) + checkbox.set_active(True if not self.person_filter else child_handle in self.person_filter) + self._filter_widgets['person_checkboxes'][child_handle] = checkbox + members_box.pack_start(checkbox, False, False, 0) + except (AttributeError, KeyError): + pass - container.show_all() + # Only add expander if there are members to show + if len(members_box.get_children()) > 0: + expander.add(members_box) + self._filter_widgets['family_expanders'][family_handle] = expander + container.pack_start(expander, False, False, 0) + + container.show_all() except (AttributeError, KeyError) as e: logger.warning(f"Error updating person filter: {e}", exc_info=True) @@ -1357,24 +1403,71 @@ class MyTimelineView(NavigationView): """ Rebuilds the current display. Called when the view becomes visible. """ + # Collect all events if not already done + if not self.all_events and self.dbstate.is_open(): + self.collect_events() + + # Highlight active event if one is selected active_handle = self.get_active() if active_handle: self.goto_handle(active_handle) + elif self.drawing_area: + self.drawing_area.queue_draw() def goto_handle(self, handle: str) -> None: """ - Called when the active family changes. + Called when the active event changes. Args: - handle: The handle of the family to display. + handle: The handle of the event to highlight/select. """ - if handle == self.active_family_handle: + if handle == self.active_event_handle: return - self.active_family_handle = handle - self.collect_events() - if self.drawing_area: - self.drawing_area.queue_draw() + self.active_event_handle = handle + + # Ensure events are collected + if not self.all_events: + self.collect_events() + else: + # Just refresh display to highlight the selected event + if self.drawing_area: + self.drawing_area.queue_draw() + + # Scroll to the selected event if possible + self._scroll_to_event(handle) + + def _scroll_to_event(self, event_handle: str) -> None: + """ + Scroll the timeline to show the selected event. + + Args: + event_handle: The handle of the event to scroll to. + """ + if not self.scrolledwindow or not event_handle: + return + + # Find the event in our event list + event_index = None + for i, timeline_event in enumerate(self.events): + if timeline_event.event.get_handle() == event_handle: + event_index = i + break + + if event_index is None: + return + + # Calculate the y position of the event + if event_index < len(self.events): + event_y = TIMELINE_MARGIN_TOP + (event_index * EVENT_SPACING) + + # Get the adjustment and scroll to the event + vadj = self.scrolledwindow.get_vadjustment() + if vadj: + # Center the event in the viewport + viewport_height = self.scrolledwindow.get_allocation().height + scroll_to = max(0, event_y - (viewport_height / 2)) + vadj.set_value(scroll_to) def _collect_person_event_refs(self, person) -> List: """ @@ -1492,36 +1585,138 @@ class MyTimelineView(NavigationView): if self.drawing_area: self.drawing_area.set_size_request(800, self.timeline_height) + def _process_event(self, event, person_obj: Optional[Any] = None) -> Optional[TimelineEvent]: + """ + Process a single event and create a TimelineEvent. + + Args: + event: The event object to process. + person_obj: Optional person object associated with this event. + + Returns: + Optional[TimelineEvent]: The created TimelineEvent, or None if invalid. + """ + try: + if event and event.get_date_object(): + date_obj = event.get_date_object() + event_type = event.get_type() + return TimelineEvent( + date_sort=date_obj.get_sort_value(), + date_obj=date_obj, + event=event, + person=person_obj, + event_type=event_type, + y_pos=0.0 + ) + except (AttributeError, KeyError, ValueError) as e: + logger.debug(f"Skipping invalid event: {e}") + return None + + def _find_person_for_event(self, event) -> Optional[Any]: + """ + Find a primary person associated with an event by searching through people. + + Args: + event: The event object to find a person for. + + Returns: + Optional person object, or None if not found or if event is family-only. + """ + if not event: + return None + + event_handle = event.get_handle() + + # Search through people to find who references this event + # For performance, we'll search through a limited set or use event references + try: + # Try to find a person who has this event as a primary event + for person_handle in self.dbstate.db.get_person_handles(): + try: + person = self.dbstate.db.get_person_from_handle(person_handle) + if person: + # Check primary events first + for event_ref in person.get_primary_event_ref_list(): + if event_ref.ref == event_handle: + return person + # Check all events + for event_ref in person.get_event_ref_list(): + if event_ref.ref == event_handle: + return person + except (AttributeError, KeyError): + continue + except (AttributeError, KeyError): + pass + + return None + + def _get_family_display_name(self, family) -> str: + """ + Get a display name for a family showing parent names. + + Args: + family: The family object. + + Returns: + str: Display name for the family. + """ + if not family: + return _("Unknown Family") + + father_handle = family.get_father_handle() + mother_handle = family.get_mother_handle() + + father_name = None + mother_name = None + + if father_handle: + try: + father = self.dbstate.db.get_person_from_handle(father_handle) + if father: + father_name = name_displayer.display(father) + except (AttributeError, KeyError): + pass + + if mother_handle: + try: + mother = self.dbstate.db.get_person_from_handle(mother_handle) + if mother: + mother_name = name_displayer.display(mother) + except (AttributeError, KeyError): + pass + + if father_name and mother_name: + return f"{father_name} & {mother_name}" + elif father_name: + return f"{father_name} & {_('Unknown')}" + elif mother_name: + return f"{_('Unknown')} & {mother_name}" + else: + return _("Unknown Family") + def collect_events(self) -> None: - """Collect all events for the active family.""" + """Collect all events from the database.""" self.all_events = [] self.events = [] - if not self.active_family_handle: + if not self.dbstate.is_open(): return try: - family = self.dbstate.db.get_family_from_handle(self.active_family_handle) + # Iterate through all events in the database + for event in self.dbstate.db.iter_events(): + # Try to find an associated person for this event + person_obj = self._find_person_for_event(event) + + # Process the event + timeline_event = self._process_event(event, person_obj) + if timeline_event: + self.all_events.append(timeline_event) + except (AttributeError, KeyError) as e: - logger.warning(f"Error accessing family: {e}", exc_info=True) + logger.warning(f"Error collecting events: {e}", exc_info=True) return - if not family: - return - - # Get family events (marriage, divorce, etc.) - self._collect_family_events(family) - - # Get father's events - self._collect_family_member_events(family.get_father_handle(), "father") - - # Get mother's events - self._collect_family_member_events(family.get_mother_handle(), "mother") - - # Get children's events - for child_ref in family.get_child_ref_list(): - self._collect_family_member_events(child_ref.ref, "child") - # Sort events by date self.all_events.sort(key=lambda x: x.date_sort) @@ -1997,19 +2192,23 @@ class MyTimelineView(NavigationView): clicked_index = self.find_event_at_position(event.x, event.y) if clicked_index is not None: clicked_event_data = self.events[clicked_index] + event_handle = clicked_event_data.event.get_handle() - # Clicking anywhere on the event line selects the person (like selecting the event) - if clicked_event_data.person: - person_handle = clicked_event_data.person.get_handle() - if self.selected_person_handle == person_handle: - # Deselect if clicking same person - self.selected_person_handle = None - else: - # Select this person - self.selected_person_handle = person_handle - else: - # No person for this event, deselect + # Select/highlight this event + if self.active_event_handle == event_handle: + # Deselect if clicking same event + self.active_event_handle = None self.selected_person_handle = None + else: + # Select this event + self.active_event_handle = event_handle + # Set selected person based on this event's person + if clicked_event_data.person: + self.selected_person_handle = clicked_event_data.person.get_handle() + else: + self.selected_person_handle = None + # Navigate to this event in the view + self.uistate.set_active(event_handle, "Event") self.drawing_area.queue_draw() return False @@ -2416,9 +2615,9 @@ class MyTimelineView(NavigationView): # Check if this event is hovered is_hovered = (i == self.hovered_event_index) - # Check if this event belongs to selected person - is_selected = (self.selected_person_handle is not None and - self._person_matches_handle(event_data.person, self.selected_person_handle)) + # Check if this event is the active/selected event + is_selected = (self.active_event_handle is not None and + event_data.event.get_handle() == self.active_event_handle) # Draw event marker with modern styling self.draw_event_marker(context, timeline_x, event_data.y_pos, @@ -2475,7 +2674,7 @@ class MyTimelineView(NavigationView): # Draw events self._draw_events(context, events_with_y_pos, timeline_x) - # Draw visual connections for selected person + # Draw visual connections for selected person (from selected event) if self.selected_person_handle is not None: self.draw_person_connections(context, events_with_y_pos, timeline_x, timeline_y_start, timeline_y_end) diff --git a/uninstall_from_snap.sh b/uninstall_from_snap.sh new file mode 100755 index 0000000..2601f0e --- /dev/null +++ b/uninstall_from_snap.sh @@ -0,0 +1,154 @@ +#!/bin/bash +# +# Script to uninstall MyTimeline plugin from snap-installed Gramps +# +# This script removes the MyTimeline plugin files from the Gramps snap plugin directory +# + +set -e # Exit on error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Plugin files to remove +PLUGIN_FILES=("MyTimeline.gpr.py" "MyTimeline.py") + +# Target directory for snap-installed Gramps +PLUGIN_DEST_DIR="$HOME/snap/gramps/current/.local/share/gramps/gramps60/plugins" + +# Alternative locations to try +ALTERNATIVE_DIRS=( + "$HOME/snap/gramps/current/.local/share/gramps/gramps60/plugins" + "$HOME/snap/gramps/current/.gramps/gramps60/plugins" + "$HOME/snap/gramps/current/.gramps/plugins" + "$HOME/snap/gramps/common/.local/share/gramps/gramps60/plugins" + "$HOME/.local/share/gramps/gramps60/plugins" + "$HOME/.gramps/gramps60/plugins" +) + +echo "==========================================" +echo "MyTimeline Plugin Uninstaller for Snap Gramps" +echo "==========================================" +echo "" + +# Function to find the correct plugin directory +find_plugin_dir() { + # Try the primary location first + if [ -d "$PLUGIN_DEST_DIR" ]; then + echo "$PLUGIN_DEST_DIR" + return 0 + fi + + # Try alternative locations + for dir in "${ALTERNATIVE_DIRS[@]}"; do + if [ -d "$dir" ]; then + echo "$dir" + return 0 + fi + done + + return 1 +} + +# Find the target directory +echo "Locating Gramps plugin directory..." +TARGET_DIR=$(find_plugin_dir) + +if [ -z "$TARGET_DIR" ]; then + echo -e "${YELLOW}Warning: Plugin directory not found.${NC}" + echo "The plugin may already be uninstalled or Gramps may not be installed via snap." + exit 0 +fi + +echo -e "${GREEN}✓${NC} Target directory: $TARGET_DIR" +echo "" + +# Check if plugin files exist +FILES_FOUND=0 +FILES_REMOVED=0 + +for file in "${PLUGIN_FILES[@]}"; do + if [ -f "$TARGET_DIR/$file" ]; then + FILES_FOUND=$((FILES_FOUND + 1)) + fi +done + +if [ $FILES_FOUND -eq 0 ]; then + echo -e "${YELLOW}No plugin files found. The plugin may already be uninstalled.${NC}" + exit 0 +fi + +echo "Found $FILES_FOUND plugin file(s)." +echo "" + +# Remove plugin files +echo "Removing plugin files..." +for file in "${PLUGIN_FILES[@]}"; do + if [ -f "$TARGET_DIR/$file" ]; then + rm "$TARGET_DIR/$file" + echo -e "${GREEN}✓${NC} Removed: $file" + FILES_REMOVED=$((FILES_REMOVED + 1)) + fi +done + +# Remove backup directories created by the installer +BACKUP_DIRS_REMOVED=0 +echo "" +echo "Searching for backup directories..." +for backup_dir in "$TARGET_DIR"/mytimeline_backup_*; do + if [ -d "$backup_dir" ]; then + echo "Found backup directory: $(basename "$backup_dir")" + rm -rf "$backup_dir" + echo -e "${GREEN}✓${NC} Removed backup directory: $(basename "$backup_dir")" + BACKUP_DIRS_REMOVED=$((BACKUP_DIRS_REMOVED + 1)) + fi +done + +# Also check for any plugin files in subdirectories that might be loaded +echo "" +echo "Checking for plugin files in subdirectories..." +SUBDIR_FILES_REMOVED=0 +for subdir in "$TARGET_DIR"/*; do + if [ -d "$subdir" ]; then + for file in "${PLUGIN_FILES[@]}"; do + if [ -f "$subdir/$file" ]; then + echo -e "${YELLOW}Warning: Found $file in subdirectory: $(basename "$subdir")${NC}" + rm -f "$subdir/$file" + echo -e "${GREEN}✓${NC} Removed: $(basename "$subdir")/$file" + SUBDIR_FILES_REMOVED=$((SUBDIR_FILES_REMOVED + 1)) + fi + done + fi +done + +TOTAL_REMOVED=$((FILES_REMOVED + BACKUP_DIRS_REMOVED + SUBDIR_FILES_REMOVED)) + +echo "" +echo "==========================================" +if [ $TOTAL_REMOVED -gt 0 ]; then + echo -e "${GREEN}Uninstallation completed successfully!${NC}" + echo "==========================================" + echo "" + echo "Removed:" + echo " - $FILES_REMOVED plugin file(s) from main directory" + if [ $BACKUP_DIRS_REMOVED -gt 0 ]; then + echo " - $BACKUP_DIRS_REMOVED backup directory/directories" + fi + if [ $SUBDIR_FILES_REMOVED -gt 0 ]; then + echo " - $SUBDIR_FILES_REMOVED plugin file(s) from subdirectories" + fi + echo "" + echo "Location: $TARGET_DIR" + echo "" + echo "Next steps:" + echo " 1. Restart Gramps if it's currently running" + echo " 2. The plugin should no longer appear in the Plugins menu" +else + echo -e "${YELLOW}No files or directories were removed.${NC}" + echo "==========================================" +fi +echo "" +