added notification history plugins
This commit is contained in:
18 files changed
+651
-80
No files matched your search
@@ -14,7 +14,7 @@ from utils.icon_resolver import IconResolver
|
||||
|
||||
def read_config():
|
||||
"""Read and return the full configuration from the JSON file, handling missing file."""
|
||||
config_path = get_relative_path("../../../config/dock.json")
|
||||
config_path = get_relative_path("../../../config/json/dock.json")
|
||||
try:
|
||||
with open(config_path, "r") as file:
|
||||
config_data = json.load(file)
|
||||
@@ -80,7 +80,7 @@ class Applications(Box):
|
||||
self.conn = get_hyprland_connection()
|
||||
self.icon_resolver = IconResolver()
|
||||
self.pinned = self.config.get("pinned_apps", [])
|
||||
self.config_path = get_relative_path("../../../config/dock.json")
|
||||
self.config_path = get_relative_path("../../../config/json/dock.json")
|
||||
self.app_map = {}
|
||||
self._all_apps = get_desktop_applications()
|
||||
self.app_identifiers = self._build_app_identifiers_map()
|
||||
@@ -254,7 +254,7 @@ class Applications(Box):
|
||||
)
|
||||
|
||||
def update_pinned_apps_file(self):
|
||||
config_path = get_relative_path("../../../config/dock.json")
|
||||
config_path = get_relative_path("../../../config/json/dock.json")
|
||||
try:
|
||||
with open(config_path, "w") as file:
|
||||
json.dump(self.config, file, indent=4)
|
||||
@@ -558,7 +558,7 @@ class Applications(Box):
|
||||
self.dock_instance.prevent_hiding(False)
|
||||
|
||||
# Check if the drag ended outside the dock
|
||||
if hasattr(self, '_dragged_widget') and self._dragged_widget:
|
||||
if hasattr(self, "_dragged_widget") and self._dragged_widget:
|
||||
children = self.get_children()
|
||||
source_index = children.index(self._dragged_widget)
|
||||
|
||||
@@ -579,8 +579,8 @@ class Applications(Box):
|
||||
self.update_applications()
|
||||
|
||||
# Clear the dragged widget reference
|
||||
if hasattr(self, '_dragged_widget'):
|
||||
delattr(self, '_dragged_widget')
|
||||
if hasattr(self, "_dragged_widget"):
|
||||
delattr(self, "_dragged_widget")
|
||||
|
||||
def get_focused(self):
|
||||
try:
|
||||
|
||||
+17
-13
@@ -791,10 +791,9 @@ class Launcher(Window):
|
||||
self.selected_index += 1
|
||||
self._update_selection()
|
||||
else:
|
||||
# At last result, go to header buttons
|
||||
self.focus_mode = "header"
|
||||
self.header_button_index = 0
|
||||
self._update_header_focus()
|
||||
# At last result, wrap around to first result
|
||||
self.selected_index = 0
|
||||
self._update_selection()
|
||||
elif self.results:
|
||||
# If not in results mode but have results, enter results mode at first item
|
||||
self.focus_mode = "results"
|
||||
@@ -810,13 +809,19 @@ class Launcher(Window):
|
||||
self.header_buttons[self.header_button_index].emit("clicked")
|
||||
return True
|
||||
|
||||
# Check for Shift+Enter to pin application
|
||||
# Check for Shift+Enter for alternative actions
|
||||
if event.state & Gdk.ModifierType.SHIFT_MASK:
|
||||
if self.results and 0 <= self.selected_index < len(self.results):
|
||||
result = self.results[self.selected_index]
|
||||
if result.data and result.data.get("pin_action"):
|
||||
result.data["pin_action"]()
|
||||
return True
|
||||
if result.data:
|
||||
# Check for generic alternative action first
|
||||
if result.data.get("alt_action"):
|
||||
result.data["alt_action"]()
|
||||
return True
|
||||
# Fallback to pin_action for backward compatibility
|
||||
elif result.data.get("pin_action"):
|
||||
result.data["pin_action"]()
|
||||
return True
|
||||
|
||||
# Check if the selected result has a custom widget with Entry fields
|
||||
if self.results and 0 <= self.selected_index < len(self.results):
|
||||
@@ -1197,16 +1202,15 @@ class Launcher(Window):
|
||||
self._focus_search_entry_without_selection()
|
||||
|
||||
def _navigate_results_forward(self):
|
||||
"""Navigate to next result or exit results mode."""
|
||||
"""Navigate to next result or wrap around to first result."""
|
||||
if self.results and self.selected_index < len(self.results) - 1:
|
||||
# Move to next result
|
||||
self.selected_index += 1
|
||||
self._update_selection()
|
||||
else:
|
||||
# Exit results mode and go to header
|
||||
self.focus_mode = "header"
|
||||
self.header_button_index = 0
|
||||
self._update_header_focus()
|
||||
# At last result, wrap around to first result
|
||||
self.selected_index = 0
|
||||
self._update_selection()
|
||||
|
||||
def _navigate_results_backward(self):
|
||||
"""Navigate to previous result or exit results mode."""
|
||||
|
||||
@@ -98,7 +98,8 @@ class PluginManager:
|
||||
"reminders",
|
||||
"network",
|
||||
"bluetooth",
|
||||
"otp"
|
||||
"otp",
|
||||
"notifications",
|
||||
]
|
||||
|
||||
for plugin_name in default_plugins:
|
||||
|
||||
@@ -24,7 +24,7 @@ class EmojiPlugin(PluginBase):
|
||||
self.display_name = "Emoji"
|
||||
self.description = "Search and copy emojis"
|
||||
self.emoji_data = {}
|
||||
self.emoji_path = get_relative_path("../../../config/emoji.json")
|
||||
self.emoji_path = get_relative_path("../../../config/json/emoji.json")
|
||||
|
||||
def initialize(self):
|
||||
"""Initialize the emoji plugin."""
|
||||
|
||||
@@ -0,0 +1,543 @@
|
||||
"""
|
||||
Notification History plugin for the launcher.
|
||||
Provides access to notification history with search, view, and management capabilities.
|
||||
"""
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from modules.launcher.plugin_base import PluginBase
|
||||
from modules.launcher.result import Result
|
||||
from services import notification_service
|
||||
from gi.repository import GdkPixbuf, GLib
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.button import Button
|
||||
import utils.icons as icons
|
||||
|
||||
|
||||
class NotificationDetailWidget(Box):
|
||||
"""Widget for displaying detailed notification content."""
|
||||
|
||||
def __init__(self, notification_data: dict, plugin_instance):
|
||||
super().__init__(name="notification-detail", orientation="v", spacing=8)
|
||||
|
||||
self.notification_data = notification_data
|
||||
self.plugin = plugin_instance
|
||||
|
||||
# Set size constraints to fit within launcher
|
||||
self.set_size_request(530, 200)
|
||||
self.set_vexpand(False)
|
||||
self.set_hexpand(True)
|
||||
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup the detail view UI."""
|
||||
# Header with title and close button
|
||||
header_box = Box(orientation="h", spacing=8)
|
||||
|
||||
# Title
|
||||
title_label = Label(
|
||||
label=f"📧 {self.notification_data.get('app_name', 'Unknown')}",
|
||||
name="notification-detail-title",
|
||||
h_align="start",
|
||||
h_expand=True
|
||||
)
|
||||
header_box.add(title_label)
|
||||
|
||||
# Close button
|
||||
close_btn = Button(
|
||||
child=Label(markup=icons.cancel, name="notification-close-icon"),
|
||||
name="notification-close-btn",
|
||||
on_clicked=lambda *_: self.plugin._close_detail_view()
|
||||
)
|
||||
header_box.add(close_btn)
|
||||
|
||||
self.add(header_box)
|
||||
|
||||
# Content area
|
||||
content_box = Box(orientation="v", spacing=4)
|
||||
|
||||
# Summary
|
||||
summary = self.notification_data.get('summary', 'No title')
|
||||
summary_label = Label(
|
||||
label=f"Subject: {summary}",
|
||||
name="notification-detail-summary",
|
||||
h_align="start",
|
||||
ellipsize="end"
|
||||
)
|
||||
content_box.add(summary_label)
|
||||
|
||||
# Body content
|
||||
body = self.notification_data.get('body', '')
|
||||
if body:
|
||||
body_label = Label(
|
||||
label=body,
|
||||
name="notification-detail-body",
|
||||
h_align="start",
|
||||
v_align="start",
|
||||
ellipsize="none"
|
||||
)
|
||||
# Set line wrapping using properties
|
||||
body_label.set_property("wrap", True)
|
||||
body_label.set_property("wrap-mode", 2) # WORD wrap mode
|
||||
content_box.add(body_label)
|
||||
|
||||
# Timestamp
|
||||
timestamp = self.notification_data.get('timestamp', time.time())
|
||||
time_str = self.plugin._format_timestamp(timestamp)
|
||||
time_label = Label(
|
||||
label=f"Time: {time_str}",
|
||||
name="notification-detail-time",
|
||||
h_align="start"
|
||||
)
|
||||
content_box.add(time_label)
|
||||
|
||||
self.add(content_box)
|
||||
|
||||
# Action buttons
|
||||
actions_box = Box(orientation="h", spacing=8)
|
||||
|
||||
# Remove button
|
||||
remove_btn = Button(
|
||||
child=Label(markup=f"{icons.trash} Remove", name="notification-action-label"),
|
||||
name="notification-action-btn",
|
||||
on_clicked=lambda *_: self._remove_notification()
|
||||
)
|
||||
actions_box.add(remove_btn)
|
||||
|
||||
self.add(actions_box)
|
||||
|
||||
def _remove_notification(self):
|
||||
"""Remove this notification."""
|
||||
notif_id = self.notification_data.get('notification_id', 0)
|
||||
self.plugin._clear_notification(notif_id)
|
||||
|
||||
|
||||
class NotificationsPlugin(PluginBase):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "notifications"
|
||||
self.display_name = "Notification History"
|
||||
self.description = "Search and manage notification history"
|
||||
|
||||
# Settings
|
||||
self.max_results = 15
|
||||
self.notification_service = notification_service
|
||||
self.showing_detail_for = None # Track which notification is showing details
|
||||
|
||||
def initialize(self):
|
||||
"""Initialize the plugin."""
|
||||
self.set_triggers(["notif", "notif "])
|
||||
|
||||
# Connect to notification service signals to refresh when notifications change
|
||||
self.notification_service.connect("notification_count", self._on_notification_count_changed)
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup the plugin."""
|
||||
# Disconnect from notification service signals
|
||||
try:
|
||||
self.notification_service.disconnect("notification_count", self._on_notification_count_changed)
|
||||
except:
|
||||
pass
|
||||
|
||||
def _format_timestamp(self, timestamp: float) -> str:
|
||||
"""Format timestamp to human-readable string."""
|
||||
try:
|
||||
dt = datetime.fromtimestamp(timestamp)
|
||||
now = datetime.now()
|
||||
|
||||
# If today, show time only
|
||||
if dt.date() == now.date():
|
||||
return dt.strftime("%H:%M")
|
||||
|
||||
# If this week, show day and time
|
||||
days_diff = (now.date() - dt.date()).days
|
||||
if days_diff < 7:
|
||||
return dt.strftime("%a %H:%M")
|
||||
|
||||
# Otherwise show date
|
||||
return dt.strftime("%m/%d %H:%M")
|
||||
except:
|
||||
return "Unknown"
|
||||
|
||||
def _get_notification_icon(self, notification) -> Optional[GdkPixbuf.Pixbuf]:
|
||||
"""Get icon for notification."""
|
||||
try:
|
||||
# Try to use notification's image first
|
||||
if hasattr(notification, 'image_pixbuf') and notification.image_pixbuf:
|
||||
return notification.image_pixbuf.scale_simple(
|
||||
32, 32, GdkPixbuf.InterpType.BILINEAR
|
||||
)
|
||||
|
||||
# Try app icon
|
||||
if hasattr(notification, 'app_icon') and notification.app_icon:
|
||||
if notification.app_icon.startswith("file://"):
|
||||
icon_path = notification.app_icon[7:]
|
||||
try:
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon_path)
|
||||
return pixbuf.scale_simple(32, 32, GdkPixbuf.InterpType.BILINEAR)
|
||||
except:
|
||||
pass
|
||||
|
||||
return None
|
||||
except:
|
||||
return None
|
||||
|
||||
def _truncate_text(self, text: str, max_length: int = 60) -> str:
|
||||
"""Truncate text to specified length."""
|
||||
if not text:
|
||||
return ""
|
||||
if len(text) <= max_length:
|
||||
return text
|
||||
return text[:max_length-3] + "..."
|
||||
|
||||
def _trigger_refresh(self):
|
||||
"""Trigger launcher refresh to return to default notification view."""
|
||||
try:
|
||||
from gi.repository import GLib
|
||||
|
||||
def trigger_refresh():
|
||||
try:
|
||||
# Try to access the launcher through the fabric Application
|
||||
from fabric import Application
|
||||
|
||||
app = Application.get_default()
|
||||
|
||||
if app and hasattr(app, "launcher"):
|
||||
launcher = app.launcher
|
||||
if launcher and hasattr(launcher, "search_entry"):
|
||||
# Get current search text to preserve the query
|
||||
current_text = launcher.search_entry.get_text()
|
||||
# Trigger the search to refresh results
|
||||
if hasattr(launcher, "_perform_search"):
|
||||
launcher._perform_search(current_text)
|
||||
return False
|
||||
|
||||
# Fallback: try to find launcher instance through other means
|
||||
import gc
|
||||
|
||||
for obj in gc.get_objects():
|
||||
if (
|
||||
hasattr(obj, "__class__")
|
||||
and obj.__class__.__name__ == "Launcher"
|
||||
):
|
||||
if hasattr(obj, "search_entry") and hasattr(obj, "_perform_search"):
|
||||
current_text = obj.search_entry.get_text()
|
||||
obj._perform_search(current_text)
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error forcing launcher refresh: {e}")
|
||||
|
||||
return False # Don't repeat
|
||||
|
||||
# Use immediate refresh
|
||||
GLib.timeout_add(10, trigger_refresh)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Could not trigger refresh: {e}")
|
||||
|
||||
def _clear_notification(self, notification_id: int):
|
||||
"""Remove a specific notification from history."""
|
||||
try:
|
||||
# Debug: Print notification removal
|
||||
print(f"[NotificationPlugin] Removing notification ID: {notification_id}")
|
||||
|
||||
# Get count before removal for comparison
|
||||
count_before = len(self.notification_service.all_notifications)
|
||||
|
||||
# Remove the notification
|
||||
self.notification_service.remove_notification(notification_id)
|
||||
|
||||
# Get count after removal
|
||||
count_after = len(self.notification_service.all_notifications)
|
||||
|
||||
# Debug: Print removal result
|
||||
print(f"[NotificationPlugin] Notifications before: {count_before}, after: {count_after}")
|
||||
|
||||
if count_after < count_before:
|
||||
print(f"[NotificationPlugin] Successfully removed notification {notification_id}")
|
||||
else:
|
||||
print(f"[NotificationPlugin] Warning: Notification {notification_id} may not have been removed")
|
||||
|
||||
# Trigger immediate refresh using the same pattern as OTP plugin
|
||||
self._trigger_refresh()
|
||||
|
||||
except Exception as e:
|
||||
print(f"[NotificationPlugin] Error removing notification {notification_id}: {e}")
|
||||
# Still try to refresh even if there was an error
|
||||
self._trigger_refresh()
|
||||
|
||||
def _clear_all_notifications(self):
|
||||
"""Clear all notifications from history."""
|
||||
self.notification_service.clear_all_notifications()
|
||||
|
||||
def _toggle_dnd(self, enable: bool = None):
|
||||
"""Toggle or set DND mode."""
|
||||
try:
|
||||
if enable is None:
|
||||
# Toggle current state
|
||||
new_state = not self.notification_service.dont_disturb
|
||||
else:
|
||||
# Set specific state
|
||||
new_state = enable
|
||||
|
||||
self.notification_service.dont_disturb = new_state
|
||||
status = "enabled" if new_state else "disabled"
|
||||
print(f"[NotificationPlugin] Do Not Disturb {status}")
|
||||
|
||||
# Trigger refresh to show updated status
|
||||
self._trigger_refresh()
|
||||
|
||||
except Exception as e:
|
||||
print(f"[NotificationPlugin] Error toggling DND: {e}")
|
||||
|
||||
def _on_notification_count_changed(self, service, count: int):
|
||||
"""Handle notification count changes (new notifications added or removed)."""
|
||||
try:
|
||||
print(f"[NotificationPlugin] Notification count changed to {count}, triggering refresh")
|
||||
self._trigger_refresh()
|
||||
except Exception as e:
|
||||
print(f"[NotificationPlugin] Error handling notification count change: {e}")
|
||||
|
||||
def _get_notification_count(self) -> int:
|
||||
"""Get total number of notifications."""
|
||||
try:
|
||||
return len(self.notification_service.all_notifications)
|
||||
except:
|
||||
return 0
|
||||
|
||||
def _show_notification_details(self, notification_data: dict):
|
||||
"""Show detailed view of a notification."""
|
||||
notif_id = notification_data.get('notification_id', 0)
|
||||
self.showing_detail_for = notif_id
|
||||
self._trigger_refresh()
|
||||
|
||||
def _close_detail_view(self):
|
||||
"""Close the detail view and return to list."""
|
||||
self.showing_detail_for = None
|
||||
self._trigger_refresh()
|
||||
|
||||
def query(self, query_string: str) -> List[Result]:
|
||||
"""Search notification history."""
|
||||
results = []
|
||||
|
||||
# Check if we should show detail view for a specific notification
|
||||
if self.showing_detail_for is not None:
|
||||
try:
|
||||
notifications = self.notification_service.get_deserialized()
|
||||
for notif in notifications:
|
||||
notif_data = notif.serialize() if hasattr(notif, 'serialize') else {}
|
||||
notif_id = notif_data.get('id', 0)
|
||||
|
||||
if notif_id == self.showing_detail_for:
|
||||
# Create detail widget for this notification
|
||||
detail_data = {
|
||||
'notification_id': notif_id,
|
||||
'app_name': getattr(notif, 'app_name', 'Unknown'),
|
||||
'timestamp': getattr(notif, 'timestamp', time.time()),
|
||||
'summary': getattr(notif, 'summary', 'No title'),
|
||||
'body': getattr(notif, 'body', '')
|
||||
}
|
||||
|
||||
detail_widget = NotificationDetailWidget(detail_data, self)
|
||||
|
||||
results.append(Result(
|
||||
title="Notification Details",
|
||||
subtitle="Press Escape to go back to list",
|
||||
icon_markup=icons.notifications,
|
||||
action=lambda: None,
|
||||
relevance=1.0,
|
||||
plugin_name=self.name,
|
||||
custom_widget=detail_widget,
|
||||
data={"type": "detail_view", "keep_launcher_open": True}
|
||||
))
|
||||
return results
|
||||
except Exception as e:
|
||||
print(f"Error showing notification details: {e}")
|
||||
self.showing_detail_for = None
|
||||
|
||||
# Handle special commands
|
||||
if query_string.lower() in ["clear"]:
|
||||
results.append(Result(
|
||||
title="Clear All Notifications",
|
||||
subtitle="Remove all notifications from history",
|
||||
description="This will permanently delete all notification history",
|
||||
icon_markup=icons.trash,
|
||||
relevance=1.0,
|
||||
plugin_name=self.name,
|
||||
action=self._clear_all_notifications
|
||||
))
|
||||
return results
|
||||
|
||||
# Handle DND commands
|
||||
if query_string.lower() in ["off"]:
|
||||
current_dnd = self.notification_service.dont_disturb
|
||||
results.append(Result(
|
||||
title="Enable Do Not Disturb",
|
||||
subtitle="Turn off notification popups" + (" (already enabled)" if current_dnd else ""),
|
||||
description="Notifications will still be saved to history but won't show popups",
|
||||
icon_markup=icons.notifications_off,
|
||||
relevance=1.0,
|
||||
plugin_name=self.name,
|
||||
action=lambda: self._toggle_dnd(True),
|
||||
data={"keep_launcher_open": True}
|
||||
))
|
||||
return results
|
||||
|
||||
if query_string.lower() in ["on"]:
|
||||
current_dnd = self.notification_service.dont_disturb
|
||||
results.append(Result(
|
||||
title="Disable Do Not Disturb",
|
||||
subtitle="Turn on notification popups" + (" (already disabled)" if not current_dnd else ""),
|
||||
description="Notifications will show popups normally",
|
||||
icon_markup=icons.notifications,
|
||||
relevance=1.0,
|
||||
plugin_name=self.name,
|
||||
action=lambda: self._toggle_dnd(False),
|
||||
data={"keep_launcher_open": True}
|
||||
))
|
||||
return results
|
||||
|
||||
# Handle remove command
|
||||
if query_string.lower().startswith("remove ") or query_string.lower().startswith("delete "):
|
||||
search_term = query_string[7:].strip() if query_string.lower().startswith("remove ") else query_string[7:].strip()
|
||||
if search_term:
|
||||
results.append(Result(
|
||||
title=f"Remove notifications containing '{search_term}'",
|
||||
subtitle="Search for notifications to remove",
|
||||
description="This will show notifications matching your search term for removal",
|
||||
icon_markup=icons.trash,
|
||||
relevance=1.0,
|
||||
plugin_name=self.name,
|
||||
action=lambda: None # Will be handled by showing matching notifications
|
||||
))
|
||||
|
||||
try:
|
||||
# Get notifications from service
|
||||
notifications = self.notification_service.get_deserialized()
|
||||
|
||||
if not notifications:
|
||||
results.append(Result(
|
||||
title="No notifications found",
|
||||
subtitle="Your notification history is empty",
|
||||
icon_markup=icons.notifications,
|
||||
relevance=0.5,
|
||||
plugin_name=self.name
|
||||
))
|
||||
return results
|
||||
|
||||
# Show count if no query
|
||||
if not query_string:
|
||||
count = len(notifications)
|
||||
dnd_status = self.notification_service.dont_disturb
|
||||
|
||||
# Add DND status result
|
||||
if dnd_status:
|
||||
results.append(Result(
|
||||
title="Do Not Disturb: ON",
|
||||
subtitle="Notification popups are disabled • Type 'on' to enable",
|
||||
icon_markup=icons.notifications_off,
|
||||
relevance=0.9,
|
||||
plugin_name=self.name,
|
||||
action=lambda: self._toggle_dnd(False),
|
||||
data={"keep_launcher_open": True}
|
||||
))
|
||||
else:
|
||||
results.append(Result(
|
||||
title="Do Not Disturb: OFF",
|
||||
subtitle="Notification popups are enabled • Type 'off' to disable",
|
||||
icon_markup=icons.notifications,
|
||||
relevance=0.9,
|
||||
plugin_name=self.name,
|
||||
action=lambda: self._toggle_dnd(True),
|
||||
data={"keep_launcher_open": True}
|
||||
))
|
||||
|
||||
results.append(Result(
|
||||
title=f"Notification History ({count} notifications)",
|
||||
subtitle="Type to search notifications or 'clear' to remove all",
|
||||
icon_markup=icons.notifications,
|
||||
relevance=0.8,
|
||||
plugin_name=self.name
|
||||
))
|
||||
|
||||
# Filter notifications based on query
|
||||
query_lower = query_string.lower() if query_string else ""
|
||||
filtered_notifications = []
|
||||
|
||||
for notif in notifications:
|
||||
# Get notification data
|
||||
notif_data = notif.serialize() if hasattr(notif, 'serialize') else {}
|
||||
notif_id = notif_data.get('id', 0)
|
||||
timestamp = getattr(notif, 'timestamp', time.time())
|
||||
|
||||
# Search in summary, body, and app name
|
||||
searchable_text = " ".join([
|
||||
getattr(notif, 'summary', ''),
|
||||
getattr(notif, 'body', ''),
|
||||
getattr(notif, 'app_name', '')
|
||||
]).lower()
|
||||
|
||||
if not query_lower or query_lower in searchable_text:
|
||||
filtered_notifications.append((notif, notif_id, timestamp))
|
||||
|
||||
# Sort by timestamp (newest first)
|
||||
filtered_notifications.sort(key=lambda x: x[2], reverse=True)
|
||||
|
||||
# Limit results (leave room for header if no query)
|
||||
max_notif_results = self.max_results - (1 if not query_string else 0)
|
||||
filtered_notifications = filtered_notifications[:max_notif_results]
|
||||
|
||||
# Create results
|
||||
for notif, notif_id, timestamp in filtered_notifications:
|
||||
summary = getattr(notif, 'summary', 'No title')
|
||||
body = getattr(notif, 'body', '')
|
||||
app_name = getattr(notif, 'app_name', 'Unknown')
|
||||
|
||||
# Create title and subtitle
|
||||
title = self._truncate_text(summary, 50)
|
||||
subtitle_parts = []
|
||||
|
||||
if body:
|
||||
subtitle_parts.append(self._truncate_text(body, 40))
|
||||
|
||||
subtitle_parts.append(f"from {app_name}")
|
||||
subtitle_parts.append(self._format_timestamp(timestamp))
|
||||
subtitle = " • ".join(subtitle_parts)
|
||||
|
||||
# Get icon
|
||||
icon = self._get_notification_icon(notif)
|
||||
|
||||
result = Result(
|
||||
title=title,
|
||||
subtitle=subtitle,
|
||||
description="", # Don't show description by default
|
||||
icon=icon,
|
||||
icon_markup=icons.notifications if not icon else None,
|
||||
relevance=1.0,
|
||||
plugin_name=self.name,
|
||||
action=lambda nd={"notification_id": notif_id, "app_name": app_name, "timestamp": timestamp, "summary": summary, "body": body}: self._show_notification_details(nd),
|
||||
data={
|
||||
"notification_id": notif_id,
|
||||
"app_name": app_name,
|
||||
"timestamp": timestamp,
|
||||
"can_remove": True,
|
||||
"alt_action": lambda nid=notif_id: self._clear_notification(nid),
|
||||
"keep_launcher_open": True
|
||||
}
|
||||
)
|
||||
results.append(result)
|
||||
|
||||
except Exception as e:
|
||||
results.append(Result(
|
||||
title="Error loading notifications",
|
||||
subtitle=str(e),
|
||||
icon_markup=icons.alert,
|
||||
relevance=0.0,
|
||||
plugin_name=self.name
|
||||
))
|
||||
|
||||
return results
|
||||
@@ -32,7 +32,9 @@ class OTPPlugin(PluginBase):
|
||||
self.display_name = "OTP Manager"
|
||||
self.description = "Manage TOTP codes and 2FA authentication"
|
||||
|
||||
self.secrets_file = Path(get_relative_path("../../../config/otp_accounts.json"))
|
||||
self.secrets_file = Path(
|
||||
get_relative_path("../../../config/json/accounts.json")
|
||||
)
|
||||
self.secrets: Dict[str, Dict] = {}
|
||||
self.last_update = 0
|
||||
|
||||
|
||||
@@ -10,11 +10,10 @@ import utils.icons as icons
|
||||
from fabric.utils import get_relative_path
|
||||
|
||||
|
||||
|
||||
class TriggerConfig:
|
||||
def __init__(self, config_path: str = None):
|
||||
if config_path is None:
|
||||
config_path = get_relative_path("../../config/launcher.json")
|
||||
config_path = get_relative_path("../../config/json/launcher.json")
|
||||
|
||||
self.config_path = config_path
|
||||
|
||||
@@ -39,8 +38,6 @@ class TriggerConfig:
|
||||
}
|
||||
self.settings = {**default_settings, **self.config.get("settings", {})}
|
||||
|
||||
|
||||
|
||||
def get_trigger_examples(self, trigger: str) -> List[str]:
|
||||
examples = self.launcher_config.get(trigger, {}).get("examples", [])
|
||||
if not examples:
|
||||
@@ -63,9 +60,3 @@ class TriggerConfig:
|
||||
|
||||
def get_all_triggers(self) -> Dict[str, Dict[str, Any]]:
|
||||
return self.launcher_config.copy()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -80,10 +80,10 @@ class NotificationWidget(Box):
|
||||
self.timeout_ms = timeout_ms
|
||||
self._timeout_id = None
|
||||
|
||||
|
||||
|
||||
self.close_button_animator = Animator(
|
||||
bezier_curve=(0.25, 0.1, 0.25, 1.0),
|
||||
duration=0.3,
|
||||
bezier_curve=(0.25, 0.1, 0.25, 1.0),
|
||||
duration=0.3,
|
||||
min_value=0.0,
|
||||
max_value=1.0,
|
||||
repeat=False,
|
||||
@@ -91,8 +91,8 @@ class NotificationWidget(Box):
|
||||
self.close_button_animator.connect("notify::value", self.on_hover_animation_value_changed)
|
||||
|
||||
self.timeout_progress_animator = Animator(
|
||||
bezier_curve=(1.0, 0.0, 1.0, 1.0),
|
||||
duration=timeout_ms / 1000.0,
|
||||
bezier_curve=(1.0, 0.0, 1.0, 1.0),
|
||||
duration=timeout_ms / 1000.0,
|
||||
min_value=0.0,
|
||||
max_value=1.0,
|
||||
repeat=False,
|
||||
@@ -234,7 +234,7 @@ class NotificationWidget(Box):
|
||||
line_width=2,
|
||||
start_angle=0,
|
||||
end_angle=360,
|
||||
value=0.0,
|
||||
value=0.0,
|
||||
min_value=0.0,
|
||||
max_value=1.0,
|
||||
child=close_icon,
|
||||
@@ -324,13 +324,13 @@ class NotificationWidget(Box):
|
||||
if self.is_expanded:
|
||||
# Show full text without ellipsization and enable wrapping
|
||||
self.body_label.set_markup(self.notification.body) # Keep original newlines
|
||||
self.body_label.set_property("ellipsize", 0)
|
||||
self.body_label.set_property("ellipsize", 0)
|
||||
self.body_label.set_property("wrap", True)
|
||||
self.body_label.set_property("wrap-mode", 2)
|
||||
self.body_label.set_property("wrap-mode", 2)
|
||||
else:
|
||||
# Show truncated text with ellipsization
|
||||
self.body_label.set_markup(self.notification.body.replace("\n", " "))
|
||||
self.body_label.set_property("ellipsize", 3)
|
||||
self.body_label.set_property("ellipsize", 3)
|
||||
self.body_label.set_property("wrap", False)
|
||||
|
||||
def on_hover_animation_value_changed(self, animator, value):
|
||||
@@ -406,9 +406,10 @@ class NotificationWidget(Box):
|
||||
|
||||
|
||||
class NotificationRevealer(Revealer):
|
||||
def __init__(self, notification: Notification, **kwargs):
|
||||
def __init__(self, notification: Notification, cache_service, **kwargs):
|
||||
self.notif_box = NotificationWidget(notification)
|
||||
self._notification = notification
|
||||
self.cache_service = cache_service
|
||||
super().__init__(
|
||||
child=Box(
|
||||
children=[self.notif_box],
|
||||
@@ -429,6 +430,9 @@ class NotificationRevealer(Revealer):
|
||||
notification: Notification,
|
||||
reason: NotificationCloseReason,
|
||||
):
|
||||
# Cache the notification to history when it's actually closed/dismissed
|
||||
self.cache_service.cache_notification(notification)
|
||||
logger.info(f"[Notification] Cached notification from {notification.app_name} (reason: {reason})")
|
||||
self.set_reveal_child(False)
|
||||
|
||||
|
||||
@@ -456,17 +460,22 @@ class NotificationPopup(Window):
|
||||
|
||||
def on_new_notification(self, fabric_notif, id):
|
||||
notification: Notification = fabric_notif.get_notification_from_id(id)
|
||||
self.cache_notification_service.cache_notification(notification)
|
||||
|
||||
# Only show popup if DND is not enabled
|
||||
if not self.cache_notification_service.dont_disturb:
|
||||
new_box = NotificationRevealer(fabric_notif.get_notification_from_id(id))
|
||||
new_box = NotificationRevealer(
|
||||
fabric_notif.get_notification_from_id(id),
|
||||
self.cache_notification_service
|
||||
)
|
||||
self.notifications.add(new_box)
|
||||
new_box.set_reveal_child(True)
|
||||
logger.info(f"[Notification] New notification from {notification.app_name}")
|
||||
else:
|
||||
# Even in DND mode, we should cache the notification when it would have been closed
|
||||
# For DND notifications, cache them immediately since they won't be shown
|
||||
self.cache_notification_service.cache_notification(notification)
|
||||
logger.info(
|
||||
f"[Notification] DND enabled, notification from {
|
||||
notification.app_name
|
||||
} not shown"
|
||||
} cached without showing popup"
|
||||
)
|
||||
Reference in new issue
Block a user