summaryrefslogtreecommitdiffstats
path: root/deluge/plugins/Notifications/deluge_notifications
diff options
context:
space:
mode:
Diffstat (limited to 'deluge/plugins/Notifications/deluge_notifications')
-rw-r--r--deluge/plugins/Notifications/deluge_notifications/__init__.py41
-rw-r--r--deluge/plugins/Notifications/deluge_notifications/common.py117
-rw-r--r--deluge/plugins/Notifications/deluge_notifications/core.py232
-rw-r--r--deluge/plugins/Notifications/deluge_notifications/data/config.ui651
-rw-r--r--deluge/plugins/Notifications/deluge_notifications/data/config.ui~643
-rw-r--r--deluge/plugins/Notifications/deluge_notifications/data/notifications.js522
-rw-r--r--deluge/plugins/Notifications/deluge_notifications/gtkui.py742
-rw-r--r--deluge/plugins/Notifications/deluge_notifications/test.py89
-rw-r--r--deluge/plugins/Notifications/deluge_notifications/webui.py35
9 files changed, 3072 insertions, 0 deletions
diff --git a/deluge/plugins/Notifications/deluge_notifications/__init__.py b/deluge/plugins/Notifications/deluge_notifications/__init__.py
new file mode 100644
index 0000000..810e284
--- /dev/null
+++ b/deluge/plugins/Notifications/deluge_notifications/__init__.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
+# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+from __future__ import unicode_literals
+
+from deluge.plugins.init import PluginInitBase
+
+
+class CorePlugin(PluginInitBase):
+ def __init__(self, plugin_name):
+ from .core import Core as _pluginCls
+
+ self._plugin_cls = _pluginCls
+ super(CorePlugin, self).__init__(plugin_name)
+
+
+class GtkUIPlugin(PluginInitBase):
+ def __init__(self, plugin_name):
+ from .gtkui import GtkUI as _pluginCls
+
+ self._plugin_cls = _pluginCls
+ super(GtkUIPlugin, self).__init__(plugin_name)
+
+
+class WebUIPlugin(PluginInitBase):
+ def __init__(self, plugin_name):
+ from .webui import WebUI as _pluginCls
+
+ self._plugin_cls = _pluginCls
+ super(WebUIPlugin, self).__init__(plugin_name)
diff --git a/deluge/plugins/Notifications/deluge_notifications/common.py b/deluge/plugins/Notifications/deluge_notifications/common.py
new file mode 100644
index 0000000..6966122
--- /dev/null
+++ b/deluge/plugins/Notifications/deluge_notifications/common.py
@@ -0,0 +1,117 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
+# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+from __future__ import unicode_literals
+
+import logging
+import os.path
+
+from pkg_resources import resource_filename
+from twisted.internet import defer
+
+from deluge import component
+from deluge.event import known_events
+
+log = logging.getLogger(__name__)
+
+
+def get_resource(filename):
+ return resource_filename(__package__, os.path.join('data', filename))
+
+
+class CustomNotifications(object):
+ def __init__(self, plugin_name=None):
+ self.custom_notifications = {'email': {}, 'popup': {}, 'blink': {}, 'sound': {}}
+
+ def enable(self):
+ pass
+
+ def disable(self):
+ for kind in self.custom_notifications:
+ for eventtype in list(self.custom_notifications[kind]):
+ wrapper, handler = self.custom_notifications[kind][eventtype]
+ self._deregister_custom_provider(kind, eventtype)
+
+ def _handle_custom_providers(self, kind, eventtype, *args, **kwargs):
+ log.debug(
+ 'Calling CORE custom %s providers for %s: %s %s',
+ kind,
+ eventtype,
+ args,
+ kwargs,
+ )
+ if eventtype in self.config['subscriptions'][kind]:
+ wrapper, handler = self.custom_notifications[kind][eventtype]
+ log.debug('Found handler for kind %s: %s', kind, handler)
+ custom_notif_func = getattr(self, 'handle_custom_%s_notification' % kind)
+ d = defer.maybeDeferred(handler, *args, **kwargs)
+ d.addCallback(custom_notif_func, eventtype)
+ d.addCallback(self._on_notify_sucess, kind)
+ d.addErrback(self._on_notify_failure, kind)
+ return d
+
+ def _register_custom_provider(self, kind, eventtype, handler):
+ if not self._handled_eventtype(eventtype, handler):
+ return defer.succeed('Event not handled')
+ if eventtype not in self.custom_notifications:
+
+ def wrapper(*args, **kwargs):
+ return self._handle_custom_providers(kind, eventtype, *args, **kwargs)
+
+ self.custom_notifications[kind][eventtype] = (wrapper, handler)
+ else:
+ wrapper, handler = self.custom_notifications[kind][eventtype]
+ try:
+ component.get('EventManager').register_event_handler(eventtype, wrapper)
+ except KeyError:
+ from deluge.ui.client import client
+
+ client.register_event_handler(eventtype, wrapper)
+
+ def _deregister_custom_provider(self, kind, eventtype):
+ try:
+ wrapper, handler = self.custom_notifications[kind][eventtype]
+ try:
+ component.get('EventManager').deregister_event_handler(
+ eventtype, wrapper
+ )
+ except KeyError:
+ from deluge.ui.client import client
+
+ client.deregister_event_handler(eventtype, wrapper)
+ self.custom_notifications[kind].pop(eventtype)
+ except KeyError:
+ pass
+
+ def _handled_eventtype(self, eventtype, handler):
+ if eventtype not in known_events:
+ log.error('The event "%s" is not known', eventtype)
+ return False
+ if known_events[eventtype].__module__.startswith('deluge.event'):
+ if handler.__self__ is self:
+ return True
+ log.error(
+ 'You cannot register custom notification providers '
+ 'for built-in event types.'
+ )
+ return False
+ return True
+
+ def _on_notify_sucess(self, result, kind):
+ log.debug('Notification success using %s: %s', kind, result)
+ return result
+
+ def _on_notify_failure(self, failure, kind):
+ log.debug('Notification failure using %s: %s', kind, failure)
+ return failure
diff --git a/deluge/plugins/Notifications/deluge_notifications/core.py b/deluge/plugins/Notifications/deluge_notifications/core.py
new file mode 100644
index 0000000..123f9cf
--- /dev/null
+++ b/deluge/plugins/Notifications/deluge_notifications/core.py
@@ -0,0 +1,232 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
+# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+from __future__ import unicode_literals
+
+import logging
+import smtplib
+from email.utils import formatdate
+
+from twisted.internet import defer, threads
+
+import deluge.configmanager
+from deluge import component
+from deluge.core.rpcserver import export
+from deluge.event import known_events
+from deluge.plugins.pluginbase import CorePluginBase
+
+from .common import CustomNotifications
+
+log = logging.getLogger(__name__)
+
+DEFAULT_PREFS = {
+ 'smtp_enabled': False,
+ 'smtp_host': '',
+ 'smtp_port': 25,
+ 'smtp_user': '',
+ 'smtp_pass': '',
+ 'smtp_from': '',
+ 'smtp_tls': False, # SSL or TLS
+ 'smtp_recipients': [],
+ # Subscriptions
+ 'subscriptions': {'email': []},
+}
+
+
+class CoreNotifications(CustomNotifications):
+ def __init__(self, plugin_name=None):
+ CustomNotifications.__init__(self, plugin_name)
+
+ def enable(self):
+ CustomNotifications.enable(self)
+ self.register_custom_email_notification(
+ 'TorrentFinishedEvent', self._on_torrent_finished_event
+ )
+
+ def disable(self):
+ self.deregister_custom_email_notification('TorrentFinishedEvent')
+ CustomNotifications.disable(self)
+
+ def register_custom_email_notification(self, eventtype, handler):
+ """This is used to register email notifications for custom event types.
+
+ :param event: str, the event name
+ :param handler: function, to be called when `:param:event` is emitted
+
+ Your handler should return a tuple of (email_subject, email_contents).
+ """
+ self._register_custom_provider('email', eventtype, handler)
+
+ def deregister_custom_email_notification(self, eventtype):
+ self._deregister_custom_provider('email', eventtype)
+
+ def handle_custom_email_notification(self, result, eventtype):
+ if not self.config['smtp_enabled']:
+ return defer.succeed('SMTP notification not enabled.')
+ subject, message = result
+ log.debug(
+ 'Spawning new thread to send email with subject: %s: %s', subject, message
+ )
+ # Spawn thread because we don't want Deluge to lock up while we send the
+ # email.
+ return threads.deferToThread(self._notify_email, subject, message)
+
+ def get_handled_events(self):
+ handled_events = []
+ for evt in sorted(known_events):
+ if known_events[evt].__module__.startswith('deluge.event'):
+ if evt not in ('TorrentFinishedEvent',):
+ # Skip all un-handled built-in events
+ continue
+ classdoc = known_events[evt].__doc__.strip()
+ handled_events.append((evt, classdoc))
+ log.debug('Handled Notification Events: %s', handled_events)
+ return handled_events
+
+ def _notify_email(self, subject='', message=''):
+ log.debug('Email prepared')
+ to_addrs = self.config['smtp_recipients']
+ to_addrs_str = ', '.join(self.config['smtp_recipients'])
+ headers_dict = {
+ 'smtp_from': self.config['smtp_from'],
+ 'subject': subject,
+ 'smtp_recipients': to_addrs_str,
+ 'date': formatdate(),
+ }
+ headers = (
+ """\
+From: %(smtp_from)s
+To: %(smtp_recipients)s
+Subject: %(subject)s
+Date: %(date)s
+
+
+"""
+ % headers_dict
+ )
+
+ message = '\r\n'.join((headers + message).splitlines())
+
+ try:
+ # Python 2.6
+ server = smtplib.SMTP(
+ self.config['smtp_host'], self.config['smtp_port'], timeout=60
+ )
+ except Exception as ex:
+ err_msg = _('There was an error sending the notification email: %s') % ex
+ log.error(err_msg)
+ return ex
+
+ security_enabled = self.config['smtp_tls']
+
+ if security_enabled:
+ server.ehlo()
+ if 'starttls' not in server.esmtp_features:
+ log.warning('TLS/SSL enabled but server does not support it')
+ else:
+ server.starttls()
+ server.ehlo()
+
+ if self.config['smtp_user'] and self.config['smtp_pass']:
+ try:
+ server.login(self.config['smtp_user'], self.config['smtp_pass'])
+ except smtplib.SMTPHeloError as ex:
+ err_msg = _('Server did not reply properly to HELO greeting: %s') % ex
+ log.error(err_msg)
+ return ex
+ except smtplib.SMTPAuthenticationError as ex:
+ err_msg = _('Server refused username/password combination: %s') % ex
+ log.error(err_msg)
+ return ex
+
+ try:
+ try:
+ server.sendmail(self.config['smtp_from'], to_addrs, message)
+ except smtplib.SMTPException as ex:
+ err_msg = (
+ _('There was an error sending the notification email: %s') % ex
+ )
+ log.error(err_msg)
+ return ex
+ finally:
+ if security_enabled:
+ # avoid false failure detection when the server closes
+ # the SMTP connection with TLS enabled
+ import socket
+
+ try:
+ server.quit()
+ except socket.sslerror:
+ pass
+ else:
+ server.quit()
+ return _('Notification email sent.')
+
+ def _on_torrent_finished_event(self, torrent_id):
+ log.debug('Handler for TorrentFinishedEvent called for CORE')
+ torrent = component.get('TorrentManager')[torrent_id]
+ torrent_status = torrent.get_status({})
+ # Email
+ subject = _('Finished Torrent "%(name)s"') % torrent_status
+ message = (
+ _(
+ 'This email is to inform you that Deluge has finished '
+ 'downloading "%(name)s", which includes %(num_files)i files.'
+ '\nTo stop receiving these alerts, simply turn off email '
+ "notification in Deluge's preferences.\n\n"
+ 'Thank you,\nDeluge.'
+ )
+ % torrent_status
+ )
+ return subject, message
+
+ # d = defer.maybeDeferred(self.handle_custom_email_notification,
+ # [subject, message],
+ # 'TorrentFinishedEvent')
+ # d.addCallback(self._on_notify_sucess, 'email')
+ # d.addErrback(self._on_notify_failure, 'email')
+ # return d
+
+
+class Core(CorePluginBase, CoreNotifications):
+ def __init__(self, plugin_name):
+ CorePluginBase.__init__(self, plugin_name)
+ CoreNotifications.__init__(self)
+
+ def enable(self):
+ CoreNotifications.enable(self)
+ self.config = deluge.configmanager.ConfigManager(
+ 'notifications-core.conf', DEFAULT_PREFS
+ )
+ log.debug('ENABLING CORE NOTIFICATIONS')
+
+ def disable(self):
+ log.debug('DISABLING CORE NOTIFICATIONS')
+ CoreNotifications.disable(self)
+
+ @export
+ def set_config(self, config):
+ """Sets the config dictionary."""
+ for key in config:
+ self.config[key] = config[key]
+ self.config.save()
+
+ @export
+ def get_config(self):
+ """Returns the config dictionary."""
+ return self.config.config
+
+ @export
+ def get_handled_events(self):
+ return CoreNotifications.get_handled_events(self)
diff --git a/deluge/plugins/Notifications/deluge_notifications/data/config.ui b/deluge/plugins/Notifications/deluge_notifications/data/config.ui
new file mode 100644
index 0000000..c16b37a
--- /dev/null
+++ b/deluge/plugins/Notifications/deluge_notifications/data/config.ui
@@ -0,0 +1,651 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <object class="GtkAdjustment" id="adjustment1">
+ <property name="lower">1</property>
+ <property name="upper">65535</property>
+ <property name="value">25</property>
+ <property name="step_increment">1</property>
+ <property name="page_increment">10</property>
+ </object>
+ <object class="GtkWindow" id="window">
+ <property name="can_focus">False</property>
+ <child type="titlebar">
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkBox" id="prefs_box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkViewport" id="viewport1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="resize_mode">queue</property>
+ <child>
+ <object class="GtkNotebook" id="notebook1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="show_border">False</property>
+ <child>
+ <object class="GtkBox" id="vbox2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">start</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkFrame" id="frame2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">start</property>
+ <property name="margin_left">9</property>
+ <property name="label_xalign">0</property>
+ <property name="shadow_type">none</property>
+ <child>
+ <object class="GtkAlignment" id="alignment2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="left_padding">12</property>
+ <property name="right_padding">10</property>
+ <child>
+ <object class="GtkBox" id="vbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkCheckButton" id="blink_enabled">
+ <property name="label" translatable="yes">Tray icon blinks enabled</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="popup_enabled">
+ <property name="label" translatable="yes">Popups enabled</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="hbox2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkCheckButton" id="sound_enabled">
+ <property name="label" translatable="yes">Sound enabled</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="halign">start</property>
+ <property name="draw_indicator">True</property>
+ <signal name="toggled" handler="on_sound_enabled_toggled" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFileChooserButton" id="sound_path">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="create_folders">False</property>
+ <signal name="update-preview" handler="on_sound_path_update_preview" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="padding">2</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label6">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin_top">5</property>
+ <property name="xpad">5</property>
+ <property name="label" translatable="yes">&lt;b&gt;UI Notifications&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFrame" id="frame3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">start</property>
+ <property name="margin_left">10</property>
+ <property name="margin_top">7</property>
+ <property name="label_xalign">0</property>
+ <property name="shadow_type">none</property>
+ <child>
+ <object class="GtkAlignment" id="alignment3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="left_padding">12</property>
+ <property name="right_padding">10</property>
+ <child>
+ <object class="GtkTable" id="prefs_table">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="n_rows">7</property>
+ <property name="n_columns">4</property>
+ <property name="column_spacing">2</property>
+ <property name="row_spacing">2</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Hostname:</property>
+ </object>
+ <packing>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="smtp_host">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Port:</property>
+ <property name="justify">right</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton" id="smtp_port">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="max_length">5</property>
+ <property name="width_chars">5</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="adjustment">adjustment1</property>
+ <property name="climb_rate">1</property>
+ <property name="numeric">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">3</property>
+ <property name="right_attach">4</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Username:</property>
+ </object>
+ <packing>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="smtp_user">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">4</property>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Password:</property>
+ </object>
+ <packing>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="smtp_pass">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="visibility">False</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">4</property>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFrame" id="frame1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">start</property>
+ <property name="label_xalign">0</property>
+ <property name="shadow_type">none</property>
+ <child>
+ <object class="GtkAlignment" id="alignment4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="bottom_padding">10</property>
+ <child>
+ <object class="GtkBox" id="hbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">2</property>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="shadow_type">in</property>
+ <child>
+ <object class="GtkTreeView" id="smtp_recipients">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="headers_visible">False</property>
+ <property name="enable_grid_lines">horizontal</property>
+ <child internal-child="selection">
+ <object class="GtkTreeSelection"/>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkVButtonBox" id="vbuttonbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">5</property>
+ <property name="layout_style">start</property>
+ <child>
+ <object class="GtkButton" id="add_button">
+ <property name="label">gtk-add</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ <signal name="clicked" handler="on_add_button_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="delete_button">
+ <property name="label">gtk-delete</property>
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ <signal name="clicked" handler="on_delete_button_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">3</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label5">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_bottom">3</property>
+ <property name="ypad">0</property>
+ <property name="label" translatable="yes">&lt;b&gt;Recipients&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="right_attach">4</property>
+ <property name="top_attach">6</property>
+ <property name="bottom_attach">7</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="smtp_tls">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="halign">start</property>
+ <property name="draw_indicator">True</property>
+ <child>
+ <object class="GtkLabel" id="label9">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Server requires TLS/SSL</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">4</property>
+ <property name="top_attach">5</property>
+ <property name="bottom_attach">6</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label8">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">From:</property>
+ </object>
+ <packing>
+ <property name="top_attach">4</property>
+ <property name="bottom_attach">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="smtp_from">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">4</property>
+ <property name="top_attach">4</property>
+ <property name="bottom_attach">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="smtp_enabled">
+ <property name="label" translatable="yes">Enabled</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ <signal name="toggled" handler="on_enabled_toggled" swapped="no"/>
+ </object>
+ <packing>
+ <property name="right_attach">4</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label7">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="xpad">5</property>
+ <property name="label" translatable="yes">&lt;b&gt;Email Notifications&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="settings_page_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Settings</property>
+ </object>
+ <packing>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="vbox5">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">15</property>
+ <property name="margin_right">10</property>
+ <property name="margin_bottom">10</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow2">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <child>
+ <object class="GtkTreeView" id="subscriptions_treeview">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="enable_grid_lines">horizontal</property>
+ <child internal-child="selection">
+ <object class="GtkTreeSelection"/>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label12">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">This configuration does not mean that you'll actually receive notifications for all these events.</property>
+ <property name="justify">fill</property>
+ <property name="wrap">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">2</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="subscriptions_page_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Subscriptions</property>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="sounds_page">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">15</property>
+ <property name="margin_right">10</property>
+ <property name="margin_bottom">10</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow3">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <child>
+ <object class="GtkTreeView" id="sounds_treeview">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <child internal-child="selection">
+ <object class="GtkTreeSelection"/>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHButtonBox" id="hbuttonbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">10</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="sounds_revert_button">
+ <property name="label">gtk-revert-to-saved</property>
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ <signal name="clicked" handler="on_sounds_revert_button_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="sounds_edit_button">
+ <property name="label">gtk-edit</property>
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ <signal name="clicked" handler="on_sounds_edit_button_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="sounds_page_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Sound Customization</property>
+ </object>
+ <packing>
+ <property name="position">2</property>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/deluge/plugins/Notifications/deluge_notifications/data/config.ui~ b/deluge/plugins/Notifications/deluge_notifications/data/config.ui~
new file mode 100644
index 0000000..f26275b
--- /dev/null
+++ b/deluge/plugins/Notifications/deluge_notifications/data/config.ui~
@@ -0,0 +1,643 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.0 -->
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <object class="GtkAdjustment" id="adjustment1">
+ <property name="lower">1</property>
+ <property name="upper">65535</property>
+ <property name="value">25</property>
+ <property name="step_increment">1</property>
+ <property name="page_increment">10</property>
+ </object>
+ <object class="GtkWindow" id="window">
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkVBox" id="prefs_box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkViewport" id="viewport1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="resize_mode">queue</property>
+ <child>
+ <object class="GtkNotebook" id="notebook1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="show_border">False</property>
+ <child>
+ <object class="GtkVBox" id="vbox2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">start</property>
+ <child>
+ <object class="GtkFrame" id="frame2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">start</property>
+ <property name="margin_left">9</property>
+ <property name="label_xalign">0</property>
+ <property name="shadow_type">none</property>
+ <child>
+ <object class="GtkAlignment" id="alignment2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="left_padding">12</property>
+ <property name="right_padding">10</property>
+ <child>
+ <object class="GtkVBox" id="vbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkCheckButton" id="blink_enabled">
+ <property name="label" translatable="yes">Tray icon blinks enabled</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="popup_enabled">
+ <property name="label" translatable="yes">Popups enabled</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHBox" id="hbox2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkCheckButton" id="sound_enabled">
+ <property name="label" translatable="yes">Sound enabled</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="halign">start</property>
+ <property name="draw_indicator">True</property>
+ <signal name="toggled" handler="on_sound_enabled_toggled" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFileChooserButton" id="sound_path">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="create_folders">False</property>
+ <signal name="update-preview" handler="on_sound_path_update_preview" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="padding">2</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label6">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin_top">5</property>
+ <property name="xpad">5</property>
+ <property name="label" translatable="yes">&lt;b&gt;UI Notifications&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFrame" id="frame3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">start</property>
+ <property name="margin_left">10</property>
+ <property name="margin_top">7</property>
+ <property name="label_xalign">0</property>
+ <property name="shadow_type">none</property>
+ <child>
+ <object class="GtkAlignment" id="alignment3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="left_padding">12</property>
+ <property name="right_padding">10</property>
+ <child>
+ <object class="GtkTable" id="prefs_table">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="n_rows">7</property>
+ <property name="n_columns">4</property>
+ <property name="column_spacing">2</property>
+ <property name="row_spacing">2</property>
+ <child>
+ <object class="GtkLabel" id="label1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Hostname:</property>
+ </object>
+ <packing>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="smtp_host">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Port:</property>
+ <property name="justify">right</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton" id="smtp_port">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="max_length">5</property>
+ <property name="width_chars">5</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="adjustment">adjustment1</property>
+ <property name="climb_rate">1</property>
+ <property name="numeric">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">3</property>
+ <property name="right_attach">4</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Username:</property>
+ </object>
+ <packing>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="smtp_user">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">4</property>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Password:</property>
+ </object>
+ <packing>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="smtp_pass">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="visibility">False</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">4</property>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFrame" id="frame1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">start</property>
+ <property name="label_xalign">0</property>
+ <property name="shadow_type">none</property>
+ <child>
+ <object class="GtkAlignment" id="alignment4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="bottom_padding">10</property>
+ <child>
+ <object class="GtkHBox" id="hbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">2</property>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="shadow_type">in</property>
+ <child>
+ <object class="GtkTreeView" id="smtp_recipients">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="headers_visible">False</property>
+ <property name="enable_grid_lines">horizontal</property>
+ <child internal-child="selection">
+ <object class="GtkTreeSelection"/>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkVButtonBox" id="vbuttonbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">5</property>
+ <property name="layout_style">start</property>
+ <child>
+ <object class="GtkButton" id="add_button">
+ <property name="label">gtk-add</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ <signal name="clicked" handler="on_add_button_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="delete_button">
+ <property name="label">gtk-delete</property>
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ <signal name="clicked" handler="on_delete_button_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">3</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label5">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;b&gt;Recipients&lt;/b&gt;</property>
+ <property name="margin_bottom">3</property>
+ <property name="use_markup">True</property>
+ <property name="ypad">0</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="right_attach">4</property>
+ <property name="top_attach">6</property>
+ <property name="bottom_attach">7</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="smtp_tls">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="halign">start</property>
+ <property name="draw_indicator">True</property>
+ <child>
+ <object class="GtkLabel" id="label9">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Server requires TLS/SSL</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">4</property>
+ <property name="top_attach">5</property>
+ <property name="bottom_attach">6</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label8">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">From:</property>
+ </object>
+ <packing>
+ <property name="top_attach">4</property>
+ <property name="bottom_attach">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="smtp_from">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">4</property>
+ <property name="top_attach">4</property>
+ <property name="bottom_attach">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="smtp_enabled">
+ <property name="label" translatable="yes">Enabled</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ <signal name="toggled" handler="on_enabled_toggled" swapped="no"/>
+ </object>
+ <packing>
+ <property name="right_attach">4</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label7">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="xpad">5</property>
+ <property name="label" translatable="yes">&lt;b&gt;Email Notifications&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="settings_page_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Settings</property>
+ </object>
+ <packing>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkVBox" id="vbox5">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">15</property>
+ <property name="margin_right">10</property>
+ <property name="margin_bottom">10</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow2">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <child>
+ <object class="GtkTreeView" id="subscriptions_treeview">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="enable_grid_lines">horizontal</property>
+ <child internal-child="selection">
+ <object class="GtkTreeSelection"/>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label12">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">This configuration does not mean that you'll actually receive notifications for all these events.</property>
+ <property name="justify">fill</property>
+ <property name="wrap">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">2</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="subscriptions_page_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Subscriptions</property>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkVBox" id="sounds_page">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">15</property>
+ <property name="margin_right">10</property>
+ <property name="margin_bottom">10</property>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow3">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <child>
+ <object class="GtkTreeView" id="sounds_treeview">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <child internal-child="selection">
+ <object class="GtkTreeSelection"/>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHButtonBox" id="hbuttonbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">10</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="sounds_revert_button">
+ <property name="label">gtk-revert-to-saved</property>
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ <signal name="clicked" handler="on_sounds_revert_button_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="sounds_edit_button">
+ <property name="label">gtk-edit</property>
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ <signal name="clicked" handler="on_sounds_edit_button_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="sounds_page_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Sound Customization</property>
+ </object>
+ <packing>
+ <property name="position">2</property>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child type="titlebar">
+ <placeholder/>
+ </child>
+ </object>
+</interface>
diff --git a/deluge/plugins/Notifications/deluge_notifications/data/notifications.js b/deluge/plugins/Notifications/deluge_notifications/data/notifications.js
new file mode 100644
index 0000000..c9fb71f
--- /dev/null
+++ b/deluge/plugins/Notifications/deluge_notifications/data/notifications.js
@@ -0,0 +1,522 @@
+/**
+ * notifications.js
+ *
+ * Copyright (c) Omar Alvarez 2014 <omar.alvarez@udc.es>
+ *
+ * This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+ * the additional special exception to link portions of this program with the OpenSSL library.
+ * See LICENSE for more details.
+ *
+ */
+
+Ext.ns('Deluge.ux.preferences');
+
+/**
+ * @class Deluge.ux.preferences.NotificationsPage
+ * @extends Ext.Panel
+ */
+Deluge.ux.preferences.NotificationsPage = Ext.extend(Ext.Panel, {
+ title: _('Notifications'),
+ header: false,
+ layout: 'fit',
+ border: false,
+
+ initComponent: function() {
+ Deluge.ux.preferences.NotificationsPage.superclass.initComponent.call(
+ this
+ );
+
+ this.emailNotiFset = new Ext.form.FieldSet({
+ xtype: 'fieldset',
+ border: false,
+ title: _('Email Notifications'),
+ autoHeight: true,
+ defaultType: 'textfield',
+ style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
+ width: '85%',
+ labelWidth: 1,
+ });
+
+ this.chkEnableEmail = this.emailNotiFset.add({
+ fieldLabel: '',
+ labelSeparator: '',
+ name: 'enable_email',
+ xtype: 'checkbox',
+ boxLabel: _('Enabled'),
+ listeners: {
+ check: function(object, checked) {
+ this.setSmtpDisabled(!checked);
+ },
+ scope: this,
+ },
+ });
+
+ this.hBoxHost = this.emailNotiFset.add({
+ fieldLabel: '',
+ labelSeparator: '',
+ name: 'host',
+ xtype: 'container',
+ layout: 'hbox',
+ disabled: true,
+ items: [
+ {
+ xtype: 'label',
+ text: _('Hostname:'),
+ margins: '6 0 0 6',
+ },
+ {
+ xtype: 'textfield',
+ margins: '2 0 0 4',
+ },
+ ],
+ });
+
+ this.hBoxPort = this.emailNotiFset.add({
+ fieldLabel: '',
+ labelSeparator: '',
+ name: 'port',
+ xtype: 'container',
+ layout: 'hbox',
+ disabled: true,
+ items: [
+ {
+ xtype: 'label',
+ text: _('Port:'),
+ margins: '6 0 0 6',
+ },
+ {
+ xtype: 'spinnerfield',
+ margins: '2 0 0 34',
+ width: 64,
+ decimalPrecision: 0,
+ minValue: 0,
+ maxValue: 65535,
+ },
+ ],
+ });
+
+ this.hBoxUser = this.emailNotiFset.add({
+ fieldLabel: '',
+ labelSeparator: '',
+ name: 'username',
+ xtype: 'container',
+ layout: 'hbox',
+ disabled: true,
+ items: [
+ {
+ xtype: 'label',
+ text: _('Username:'),
+ margins: '6 0 0 6',
+ },
+ {
+ xtype: 'textfield',
+ margins: '2 0 0 3',
+ },
+ ],
+ });
+
+ this.hBoxPassword = this.emailNotiFset.add({
+ fieldLabel: '',
+ labelSeparator: '',
+ name: 'password',
+ xtype: 'container',
+ layout: 'hbox',
+ disabled: true,
+ items: [
+ {
+ xtype: 'label',
+ text: _('Password:'),
+ margins: '6 0 0 6',
+ },
+ {
+ xtype: 'textfield',
+ inputType: 'password',
+ margins: '2 0 0 5',
+ },
+ ],
+ });
+
+ this.hBoxFrom = this.emailNotiFset.add({
+ fieldLabel: '',
+ labelSeparator: '',
+ name: 'from',
+ xtype: 'container',
+ layout: 'hbox',
+ disabled: true,
+ items: [
+ {
+ xtype: 'label',
+ text: _('From:'),
+ margins: '6 0 0 6',
+ },
+ {
+ xtype: 'textfield',
+ margins: '2 0 0 28',
+ },
+ ],
+ });
+
+ this.chkTLS = this.emailNotiFset.add({
+ fieldLabel: '',
+ labelSeparator: '',
+ name: 'enable_tls_ssl',
+ xtype: 'checkbox',
+ disabled: true,
+ boxLabel: _('Server requires TLS/SSL'),
+ });
+
+ this.recipientsFset = new Ext.form.FieldSet({
+ xtype: 'fieldset',
+ border: false,
+ title: _('Recipients'),
+ autoHeight: true,
+ defaultType: 'editorgrid',
+ style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
+ autoWidth: true,
+ items: [
+ {
+ fieldLabel: '',
+ name: 'recipients',
+ margins: '2 0 5 5',
+ height: 130,
+ hideHeaders: true,
+ width: 260,
+ disabled: true,
+ autoExpandColumn: 'recipient',
+ bbar: {
+ items: [
+ {
+ text: _('Add'),
+ iconCls: 'icon-add',
+ handler: this.onAddClick,
+ scope: this,
+ },
+ {
+ text: _('Remove'),
+ iconCls: 'icon-remove',
+ handler: this.onRemoveClick,
+ scope: this,
+ },
+ ],
+ },
+ viewConfig: {
+ emptyText: _('Add an recipient...'),
+ deferEmptyText: false,
+ },
+ colModel: new Ext.grid.ColumnModel({
+ columns: [
+ {
+ id: 'recipient',
+ header: _('Recipient'),
+ dataIndex: 'recipient',
+ sortable: true,
+ hideable: false,
+ editable: true,
+ editor: {
+ xtype: 'textfield',
+ },
+ },
+ ],
+ }),
+ selModel: new Ext.grid.RowSelectionModel({
+ singleSelect: false,
+ moveEditorOnEnter: false,
+ }),
+ store: new Ext.data.ArrayStore({
+ autoDestroy: true,
+ fields: [{ name: 'recipient' }],
+ }),
+ listeners: {
+ afteredit: function(e) {
+ e.record.commit();
+ },
+ },
+ setEmptyText: function(text) {
+ if (this.viewReady) {
+ this.getView().emptyText = text;
+ this.getView().refresh();
+ } else {
+ Ext.apply(this.viewConfig, { emptyText: text });
+ }
+ },
+ loadData: function(data) {
+ this.getStore().loadData(data);
+ if (this.viewReady) {
+ this.getView().updateHeaders();
+ }
+ },
+ },
+ ],
+ });
+
+ this.edGridSubs = new Ext.grid.EditorGridPanel({
+ xtype: 'editorgrid',
+ autoHeight: true,
+ autoExpandColumn: 'event',
+ viewConfig: {
+ emptyText: _('Loading events...'),
+ deferEmptyText: false,
+ },
+ colModel: new Ext.grid.ColumnModel({
+ defaults: {
+ renderer: function(
+ value,
+ meta,
+ record,
+ rowIndex,
+ colIndex,
+ store
+ ) {
+ if (Ext.isNumber(value) && parseInt(value) !== value) {
+ return value.toFixed(6);
+ } else if (Ext.isBoolean(value)) {
+ return (
+ '<div class="x-grid3-check-col' +
+ (value ? '-on' : '') +
+ '" style="width: 20px;">&#160;</div>'
+ );
+ }
+ return value;
+ },
+ },
+ columns: [
+ {
+ id: 'event',
+ header: 'Event',
+ dataIndex: 'event',
+ sortable: true,
+ hideable: false,
+ },
+ {
+ id: 'email',
+ header: _('Email'),
+ dataIndex: 'email',
+ sortable: true,
+ hideable: false,
+ menuDisabled: true,
+ width: 40,
+ },
+ ],
+ }),
+ store: new Ext.data.ArrayStore({
+ autoDestroy: true,
+ fields: [
+ {
+ name: 'event',
+ },
+ {
+ name: 'email',
+ },
+ ],
+ }),
+ listeners: {
+ cellclick: function(grid, rowIndex, colIndex, e) {
+ var record = grid.getStore().getAt(rowIndex);
+ var field = grid.getColumnModel().getDataIndex(colIndex);
+ var value = record.get(field);
+
+ if (colIndex == 1) {
+ if (Ext.isBoolean(value)) {
+ record.set(field, !value);
+ record.commit();
+ }
+ }
+ },
+ beforeedit: function(e) {
+ if (Ext.isBoolean(e.value)) {
+ return false;
+ }
+
+ return e.record.get('enabled');
+ },
+ afteredit: function(e) {
+ e.record.commit();
+ },
+ },
+ setEmptyText: function(text) {
+ if (this.viewReady) {
+ this.getView().emptyText = text;
+ this.getView().refresh();
+ } else {
+ Ext.apply(this.viewConfig, { emptyText: text });
+ }
+ },
+ setSub: function(eventName) {
+ var store = this.getStore();
+ var index = store.find('event', eventName);
+ store.getAt(index).set('email', true);
+ store.getAt(index).commit();
+ },
+ loadData: function(data) {
+ this.getStore().loadData(data);
+ if (this.viewReady) {
+ this.getView().updateHeaders();
+ }
+ },
+ });
+
+ this.tabPanSettings = this.add({
+ xtype: 'tabpanel',
+ activeTab: 0,
+ items: [
+ {
+ title: _('Settings'),
+ items: [this.emailNotiFset, this.recipientsFset],
+ autoScroll: true,
+ },
+ {
+ title: _('Subscriptions'),
+ items: this.edGridSubs,
+ },
+ ],
+ });
+
+ this.on('show', this.updateConfig, this);
+ },
+
+ updateConfig: function() {
+ deluge.client.notifications.get_handled_events({
+ success: function(events) {
+ var data = [];
+ var keys = Ext.keys(events);
+ for (var i = 0; i < keys.length; i++) {
+ var key = keys[i];
+ data.push([events[key][0], false]);
+ }
+ this.edGridSubs.loadData(data);
+ },
+ scope: this,
+ });
+ deluge.client.notifications.get_config({
+ success: function(config) {
+ this.chkEnableEmail.setValue(config['smtp_enabled']);
+ this.setSmtpDisabled(!config['smtp_enabled']);
+
+ this.hBoxHost.getComponent(1).setValue(config['smtp_host']);
+ this.hBoxPort.getComponent(1).setValue(config['smtp_port']);
+ this.hBoxUser.getComponent(1).setValue(config['smtp_user']);
+ this.hBoxPassword.getComponent(1).setValue(config['smtp_pass']);
+ this.hBoxFrom.getComponent(1).setValue(config['smtp_from']);
+ this.chkTLS.setValue(config['smtp_tls']);
+
+ var data = [];
+ var keys = Ext.keys(config['smtp_recipients']);
+ for (var i = 0; i < keys.length; i++) {
+ var key = keys[i];
+ data.push([config['smtp_recipients'][key]]);
+ }
+ this.recipientsFset.getComponent(0).loadData(data);
+
+ data = [];
+ keys = Ext.keys(config['subscriptions']['email']);
+ for (var i = 0; i < keys.length; i++) {
+ var key = keys[i];
+ this.edGridSubs.setSub(
+ config['subscriptions']['email'][key]
+ );
+ }
+ },
+ scope: this,
+ });
+ },
+
+ onApply: function() {
+ var config = {};
+
+ config['smtp_enabled'] = this.chkEnableEmail.getValue();
+ config['smtp_host'] = this.hBoxHost.getComponent(1).getValue();
+ config['smtp_port'] = Number(this.hBoxPort.getComponent(1).getValue());
+ config['smtp_user'] = this.hBoxUser.getComponent(1).getValue();
+ config['smtp_pass'] = this.hBoxPassword.getComponent(1).getValue();
+ config['smtp_from'] = this.hBoxFrom.getComponent(1).getValue();
+ config['smtp_tls'] = this.chkTLS.getValue();
+
+ var recipientsList = [];
+ var store = this.recipientsFset.getComponent(0).getStore();
+
+ for (var i = 0; i < store.getCount(); i++) {
+ var record = store.getAt(i);
+ var recipient = record.get('recipient');
+ recipientsList.push(recipient);
+ }
+
+ config['smtp_recipients'] = recipientsList;
+
+ var subscriptions = {};
+ var eventList = [];
+ store = this.edGridSubs.getStore();
+
+ for (var i = 0; i < store.getCount(); i++) {
+ var record = store.getAt(i);
+ var ev = record.get('event');
+ var email = record.get('email');
+ if (email) {
+ eventList.push(ev);
+ }
+ }
+
+ subscriptions['email'] = eventList;
+ config['subscriptions'] = subscriptions;
+
+ deluge.client.notifications.set_config(config);
+ },
+
+ onOk: function() {
+ this.onApply();
+ },
+
+ onAddClick: function() {
+ var store = this.recipientsFset.getComponent(0).getStore();
+ var Recipient = store.recordType;
+ var i = new Recipient({
+ recipient: '',
+ });
+ this.recipientsFset.getComponent(0).stopEditing();
+ store.insert(0, i);
+ this.recipientsFset.getComponent(0).startEditing(0, 0);
+ },
+
+ onRemoveClick: function() {
+ var selections = this.recipientsFset
+ .getComponent(0)
+ .getSelectionModel()
+ .getSelections();
+ var store = this.recipientsFset.getComponent(0).getStore();
+
+ this.recipientsFset.getComponent(0).stopEditing();
+ for (var i = 0; i < selections.length; i++) store.remove(selections[i]);
+ store.commitChanges();
+ },
+
+ setSmtpDisabled: function(disable) {
+ this.hBoxHost.setDisabled(disable);
+ this.hBoxPort.setDisabled(disable);
+ this.hBoxUser.setDisabled(disable);
+ this.hBoxPassword.setDisabled(disable);
+ this.hBoxFrom.setDisabled(disable);
+ this.chkTLS.setDisabled(disable);
+ this.recipientsFset.getComponent(0).setDisabled(disable);
+ },
+
+ onDestroy: function() {
+ deluge.preferences.un('show', this.updateConfig, this);
+
+ Deluge.ux.preferences.NotificationsPage.superclass.onDestroy.call(this);
+ },
+});
+
+Deluge.plugins.NotificationsPlugin = Ext.extend(Deluge.Plugin, {
+ name: 'Notifications',
+
+ onDisable: function() {
+ deluge.preferences.removePage(this.prefsPage);
+ },
+
+ onEnable: function() {
+ this.prefsPage = deluge.preferences.addPage(
+ new Deluge.ux.preferences.NotificationsPage()
+ );
+ },
+});
+
+Deluge.registerPlugin('Notifications', Deluge.plugins.NotificationsPlugin);
diff --git a/deluge/plugins/Notifications/deluge_notifications/gtkui.py b/deluge/plugins/Notifications/deluge_notifications/gtkui.py
new file mode 100644
index 0000000..816cb36
--- /dev/null
+++ b/deluge/plugins/Notifications/deluge_notifications/gtkui.py
@@ -0,0 +1,742 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
+# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+from __future__ import unicode_literals
+
+import logging
+from os.path import basename
+
+from gi import require_version
+from gi.repository import Gtk
+from twisted.internet import defer
+
+import deluge.common
+import deluge.component as component
+import deluge.configmanager
+from deluge.plugins.pluginbase import Gtk3PluginBase
+from deluge.ui.client import client
+
+from .common import CustomNotifications, get_resource
+
+# Relative imports
+
+log = logging.getLogger(__name__)
+
+try:
+ import pygame
+
+ SOUND_AVAILABLE = True
+except ImportError:
+ SOUND_AVAILABLE = False
+
+try:
+ require_version('Notify', '0.7')
+ from gi.repository import Notify
+except (ValueError, ImportError):
+ POPUP_AVAILABLE = False
+else:
+ POPUP_AVAILABLE = not deluge.common.windows_check()
+
+
+DEFAULT_PREFS = {
+ # BLINK
+ 'blink_enabled': False,
+ # FLASH
+ 'flash_enabled': False,
+ # POPUP
+ 'popup_enabled': False,
+ # SOUND
+ 'sound_enabled': False,
+ 'sound_path': '',
+ 'custom_sounds': {},
+ # Subscriptions
+ 'subscriptions': {'popup': [], 'blink': [], 'sound': []},
+}
+
+RECIPIENT_FIELD, RECIPIENT_EDIT = list(range(2))
+(
+ SUB_EVENT,
+ SUB_EVENT_DOC,
+ SUB_NOT_EMAIL,
+ SUB_NOT_POPUP,
+ SUB_NOT_BLINK,
+ SUB_NOT_SOUND,
+) = list(range(6))
+SND_EVENT, SND_EVENT_DOC, SND_NAME, SND_PATH = list(range(4))
+
+
+class GtkUiNotifications(CustomNotifications):
+ def __init__(self, plugin_name=None):
+ CustomNotifications.__init__(self, plugin_name)
+
+ def enable(self):
+ CustomNotifications.enable(self)
+ self.register_custom_blink_notification(
+ 'TorrentFinishedEvent', self._on_torrent_finished_event_blink
+ )
+ self.register_custom_sound_notification(
+ 'TorrentFinishedEvent', self._on_torrent_finished_event_sound
+ )
+ self.register_custom_popup_notification(
+ 'TorrentFinishedEvent', self._on_torrent_finished_event_popup
+ )
+
+ def disable(self):
+ self.deregister_custom_blink_notification('TorrentFinishedEvent')
+ self.deregister_custom_sound_notification('TorrentFinishedEvent')
+ self.deregister_custom_popup_notification('TorrentFinishedEvent')
+ CustomNotifications.disable(self)
+
+ def register_custom_popup_notification(self, eventtype, handler):
+ """This is used to register popup notifications for custom event types.
+
+ :param event: the event name
+ :param type: string
+ :param handler: function, to be called when `:param:event` is emitted
+
+ Your handler should return a tuple of (popup_title, popup_contents).
+ """
+ self._register_custom_provider('popup', eventtype, handler)
+
+ def deregister_custom_popup_notification(self, eventtype):
+ self._deregister_custom_provider('popup', eventtype)
+
+ def register_custom_blink_notification(self, eventtype, handler):
+ """This is used to register blink notifications for custom event types.
+
+ :param event: str, the event name
+ :param handler: function, to be called when `:param:event` is emitted
+
+ Your handler should return `True` or `False` to blink or not the
+ trayicon.
+ """
+ self._register_custom_provider('blink', eventtype, handler)
+
+ def deregister_custom_blink_notification(self, eventtype):
+ self._deregister_custom_provider('blink', eventtype)
+
+ def register_custom_sound_notification(self, eventtype, handler):
+ """This is used to register sound notifications for custom event types.
+
+ :param event: the event name
+ :type event: string
+ :param handler: function to be called when `:param:event` is emitted
+
+ Your handler should return either '' to use the sound defined on the
+ notification preferences, the path to a sound file, which will then be
+ played or None, where no sound will be played at all.
+ """
+ self._register_custom_provider('sound', eventtype, handler)
+
+ def deregister_custom_sound_notification(self, eventtype):
+ self._deregister_custom_provider('sound', eventtype)
+
+ def handle_custom_popup_notification(self, result, eventtype):
+ title, message = result
+ return defer.maybeDeferred(self.__popup, title, message)
+
+ def handle_custom_blink_notification(self, result, eventtype):
+ if result:
+ return defer.maybeDeferred(self.__blink)
+ return defer.succeed(
+ 'Will not blink. The returned value from the custom '
+ 'handler was: %s' % result
+ )
+
+ def handle_custom_sound_notification(self, result, eventtype):
+ if isinstance(result, ''.__class__):
+ if not result and eventtype in self.config['custom_sounds']:
+ return defer.maybeDeferred(
+ self.__play_sound, self.config['custom_sounds'][eventtype]
+ )
+ return defer.maybeDeferred(self.__play_sound, result)
+ return defer.succeed(
+ 'Will not play sound. The returned value from the '
+ 'custom handler was: %s' % result
+ )
+
+ def __blink(self):
+ self.systray.blink(True)
+ return defer.succeed(_('Notification Blink shown'))
+
+ def __popup(self, title='', message=''):
+ if not self.config['popup_enabled']:
+ return defer.succeed(_('Popup notification is not enabled.'))
+ if not POPUP_AVAILABLE:
+ return defer.fail(_('libnotify is not installed'))
+
+ if Notify.init('Deluge'):
+ self.note = Notify.Notification.new(title, message, 'deluge-panel')
+ self.note.set_hint('desktop-entry', 'deluge')
+ if not self.note.show():
+ err_msg = _('Failed to popup notification')
+ log.warning(err_msg)
+ return defer.fail(err_msg)
+ return defer.succeed(_('Notification popup shown'))
+
+ def __play_sound(self, sound_path=''):
+ if not self.config['sound_enabled']:
+ return defer.succeed(_('Sound notification not enabled'))
+ if not SOUND_AVAILABLE:
+ err_msg = _('pygame is not installed')
+ log.warning(err_msg)
+ return defer.fail(err_msg)
+
+ pygame.init()
+ try:
+ if not sound_path:
+ sound_path = self.config['sound_path']
+ alert_sound = pygame.mixer.music
+ alert_sound.load(sound_path)
+ alert_sound.play()
+ except pygame.error as ex:
+ err_msg = _('Sound notification failed %s') % ex
+ log.warning(err_msg)
+ return defer.fail(err_msg)
+ else:
+ msg = _('Sound notification Success')
+ log.info(msg)
+ return defer.succeed(msg)
+
+ def _on_torrent_finished_event_blink(self, torrent_id):
+ return True # Yes, Blink
+
+ def _on_torrent_finished_event_sound(self, torrent_id):
+ # Since there's no custom sound hardcoded, just return ''
+ return ''
+
+ def _on_torrent_finished_event_popup(self, torrent_id):
+ d = client.core.get_torrent_status(torrent_id, ['name', 'file_progress'])
+ d.addCallback(self._on_torrent_finished_event_got_torrent_status)
+ d.addErrback(self._on_torrent_finished_event_torrent_status_failure)
+ return d
+
+ def _on_torrent_finished_event_torrent_status_failure(self, failure):
+ log.debug('Failed to get torrent status to be able to show the popup')
+
+ def _on_torrent_finished_event_got_torrent_status(self, torrent_status):
+ log.debug(
+ 'Handler for TorrentFinishedEvent GTKUI called. ' 'Got Torrent Status'
+ )
+ title = _('Finished Torrent')
+ torrent_status['num_files'] = torrent_status['file_progress'].count(1.0)
+ message = (
+ _(
+ 'The torrent "%(name)s" including %(num_files)i file(s) '
+ 'has finished downloading.'
+ )
+ % torrent_status
+ )
+ return title, message
+
+
+class GtkUI(Gtk3PluginBase, GtkUiNotifications):
+ def __init__(self, plugin_name):
+ Gtk3PluginBase.__init__(self, plugin_name)
+ GtkUiNotifications.__init__(self)
+
+ def enable(self):
+ self.config = deluge.configmanager.ConfigManager(
+ 'notifications-gtk.conf', DEFAULT_PREFS
+ )
+ self.builder = Gtk.Builder()
+ self.builder.add_from_file(get_resource('config.ui'))
+ self.builder.get_object('smtp_port').set_value(25)
+ self.prefs = self.builder.get_object('prefs_box')
+ self.prefs.show_all()
+
+ self.build_recipients_model_populate_treeview()
+ self.build_sounds_model_populate_treeview()
+ self.build_notifications_model_populate_treeview()
+
+ client.notifications.get_handled_events().addCallback(
+ self.popuplate_what_needs_handled_events
+ )
+
+ self.builder.connect_signals(
+ {
+ 'on_add_button_clicked': (
+ self.on_add_button_clicked,
+ self.recipients_treeview,
+ ),
+ 'on_delete_button_clicked': (
+ self.on_delete_button_clicked,
+ self.recipients_treeview,
+ ),
+ 'on_enabled_toggled': self.on_enabled_toggled,
+ 'on_sound_enabled_toggled': self.on_sound_enabled_toggled,
+ 'on_sounds_edit_button_clicked': self.on_sounds_edit_button_clicked,
+ 'on_sounds_revert_button_clicked': self.on_sounds_revert_button_clicked,
+ 'on_sound_path_update_preview': self.on_sound_path_update_preview,
+ }
+ )
+
+ component.get('Preferences').add_page(_('Notifications'), self.prefs)
+
+ component.get('PluginManager').register_hook(
+ 'on_apply_prefs', self.on_apply_prefs
+ )
+ component.get('PluginManager').register_hook(
+ 'on_show_prefs', self.on_show_prefs
+ )
+
+ if not POPUP_AVAILABLE:
+ self.builder.get_object('popup_enabled').set_property('sensitive', False)
+ if not SOUND_AVAILABLE:
+ # for widget_name in ('sound_enabled', 'sound_path', 'sounds_page', 'sounds_page_label'):
+ # self.builder.get_object(widget_name).set_property('sensitive', False)
+ self.builder.get_object('sound_enabled').set_property('sensitive', False)
+ self.builder.get_object('sound_path').set_property('sensitive', False)
+ self.builder.get_object('sounds_page').set_property('sensitive', False)
+ self.builder.get_object('sounds_page_label').set_property(
+ 'sensitive', False
+ )
+
+ self.systray = component.get('SystemTray')
+ if not hasattr(self.systray, 'tray'):
+ # Tray is not beeing used
+ self.builder.get_object('blink_enabled').set_property('sensitive', False)
+
+ GtkUiNotifications.enable(self)
+
+ def disable(self):
+ GtkUiNotifications.disable(self)
+ component.get('Preferences').remove_page(_('Notifications'))
+ component.get('PluginManager').deregister_hook(
+ 'on_apply_prefs', self.on_apply_prefs
+ )
+ component.get('PluginManager').deregister_hook(
+ 'on_show_prefs', self.on_show_prefs
+ )
+
+ def build_recipients_model_populate_treeview(self):
+ # SMTP Recipients treeview/model
+ self.recipients_treeview = self.builder.get_object('smtp_recipients')
+ treeview_selection = self.recipients_treeview.get_selection()
+ treeview_selection.connect(
+ 'changed', self.on_recipients_treeview_selection_changed
+ )
+ self.recipients_model = Gtk.ListStore(str, bool)
+
+ renderer = Gtk.CellRendererText()
+ renderer.connect('edited', self.on_cell_edited, self.recipients_model)
+ renderer.recipient = RECIPIENT_FIELD
+ column = Gtk.TreeViewColumn(
+ 'Recipients', renderer, text=RECIPIENT_FIELD, editable=RECIPIENT_EDIT
+ )
+ column.set_expand(True)
+ self.recipients_treeview.append_column(column)
+ self.recipients_treeview.set_model(self.recipients_model)
+
+ def build_sounds_model_populate_treeview(self):
+ # Sound customisation treeview/model
+ self.sounds_treeview = self.builder.get_object('sounds_treeview')
+ sounds_selection = self.sounds_treeview.get_selection()
+ sounds_selection.connect('changed', self.on_sounds_treeview_selection_changed)
+
+ self.sounds_treeview.set_tooltip_column(SND_EVENT_DOC)
+ self.sounds_model = Gtk.ListStore(str, str, str, str)
+
+ renderer = Gtk.CellRendererText()
+ renderer.event = SND_EVENT
+ column = Gtk.TreeViewColumn('Event', renderer, text=SND_EVENT)
+ column.set_expand(True)
+ self.sounds_treeview.append_column(column)
+
+ renderer = Gtk.CellRendererText()
+ renderer.event_doc = SND_EVENT_DOC
+ column = Gtk.TreeViewColumn('Doc', renderer, text=SND_EVENT_DOC)
+ column.set_property('visible', False)
+ self.sounds_treeview.append_column(column)
+
+ renderer = Gtk.CellRendererText()
+ renderer.sound_name = SND_NAME
+ column = Gtk.TreeViewColumn('Name', renderer, text=SND_NAME)
+ self.sounds_treeview.append_column(column)
+
+ renderer = Gtk.CellRendererText()
+ renderer.sound_path = SND_PATH
+ column = Gtk.TreeViewColumn('Path', renderer, text=SND_PATH)
+ column.set_property('visible', False)
+ self.sounds_treeview.append_column(column)
+
+ self.sounds_treeview.set_model(self.sounds_model)
+
+ def build_notifications_model_populate_treeview(self):
+ # Notification Subscriptions treeview/model
+ self.subscriptions_treeview = self.builder.get_object('subscriptions_treeview')
+ subscriptions_selection = self.subscriptions_treeview.get_selection()
+ subscriptions_selection.connect(
+ 'changed', self.on_subscriptions_treeview_selection_changed
+ )
+ self.subscriptions_treeview.set_tooltip_column(SUB_EVENT_DOC)
+ self.subscriptions_model = Gtk.ListStore(str, str, bool, bool, bool, bool)
+
+ renderer = Gtk.CellRendererText()
+ setattr(renderer, 'event', SUB_EVENT)
+ column = Gtk.TreeViewColumn('Event', renderer, text=SUB_EVENT)
+ column.set_expand(True)
+ self.subscriptions_treeview.append_column(column)
+
+ renderer = Gtk.CellRendererText()
+ setattr(renderer, 'event_doc', SUB_EVENT)
+ column = Gtk.TreeViewColumn('Doc', renderer, text=SUB_EVENT_DOC)
+ column.set_property('visible', False)
+ self.subscriptions_treeview.append_column(column)
+
+ renderer = Gtk.CellRendererToggle()
+ renderer.set_property('activatable', True)
+ renderer.connect('toggled', self._on_email_col_toggled)
+ column = Gtk.TreeViewColumn('Email', renderer, active=SUB_NOT_EMAIL)
+ column.set_clickable(True)
+ self.subscriptions_treeview.append_column(column)
+
+ renderer = Gtk.CellRendererToggle()
+ renderer.set_property('activatable', True)
+ renderer.connect('toggled', self._on_popup_col_toggled)
+ column = Gtk.TreeViewColumn('Popup', renderer, active=SUB_NOT_POPUP)
+ column.set_clickable(True)
+ self.subscriptions_treeview.append_column(column)
+
+ renderer = Gtk.CellRendererToggle()
+ renderer.set_property('activatable', True)
+ renderer.connect('toggled', self._on_blink_col_toggled)
+ column = Gtk.TreeViewColumn('Blink', renderer, active=SUB_NOT_BLINK)
+ column.set_clickable(True)
+ self.subscriptions_treeview.append_column(column)
+
+ renderer = Gtk.CellRendererToggle()
+ renderer.set_property('activatable', True)
+ renderer.connect('toggled', self._on_sound_col_toggled)
+ column = Gtk.TreeViewColumn('Sound', renderer, active=SUB_NOT_SOUND)
+ column.set_clickable(True)
+ self.subscriptions_treeview.append_column(column)
+ self.subscriptions_treeview.set_model(self.subscriptions_model)
+
+ def popuplate_what_needs_handled_events(
+ self, handled_events, email_subscriptions=None
+ ):
+ if email_subscriptions is None:
+ email_subscriptions = []
+ self.populate_subscriptions(handled_events, email_subscriptions)
+ self.populate_sounds(handled_events)
+
+ def populate_sounds(self, handled_events):
+ self.sounds_model.clear()
+ for event_name, event_doc in handled_events:
+ if event_name in self.config['custom_sounds']:
+ snd_path = self.config['custom_sounds'][event_name]
+ else:
+ snd_path = self.config['sound_path']
+
+ if snd_path:
+ self.sounds_model.set(
+ self.sounds_model.append(),
+ SND_EVENT,
+ event_name,
+ SND_EVENT_DOC,
+ event_doc,
+ SND_NAME,
+ basename(snd_path),
+ SND_PATH,
+ snd_path,
+ )
+
+ def populate_subscriptions(self, handled_events, email_subscriptions=None):
+ if email_subscriptions is None:
+ email_subscriptions = []
+ subscriptions_dict = self.config['subscriptions']
+ self.subscriptions_model.clear()
+ # self.handled_events = handled_events
+ for event_name, event_doc in handled_events:
+ self.subscriptions_model.set(
+ self.subscriptions_model.append(),
+ SUB_EVENT,
+ event_name,
+ SUB_EVENT_DOC,
+ event_doc,
+ SUB_NOT_EMAIL,
+ event_name in email_subscriptions,
+ SUB_NOT_POPUP,
+ event_name in subscriptions_dict['popup'],
+ SUB_NOT_BLINK,
+ event_name in subscriptions_dict['blink'],
+ SUB_NOT_SOUND,
+ event_name in subscriptions_dict['sound'],
+ )
+
+ def on_apply_prefs(self):
+ log.debug('applying prefs for Notifications')
+
+ current_popup_subscriptions = []
+ current_blink_subscriptions = []
+ current_sound_subscriptions = []
+ current_email_subscriptions = []
+ for event, doc, email, popup, blink, sound in self.subscriptions_model:
+ if email:
+ current_email_subscriptions.append(event)
+ if popup:
+ current_popup_subscriptions.append(event)
+ if blink:
+ current_blink_subscriptions.append(event)
+ if sound:
+ current_sound_subscriptions.append(event)
+
+ old_sound_file = self.config['sound_path']
+ new_sound_file = self.builder.get_object('sound_path').get_filename()
+ log.debug(
+ 'Old Default sound file: %s New one: %s', old_sound_file, new_sound_file
+ )
+ custom_sounds = {}
+ for event_name, event_doc, filename, filepath in self.sounds_model:
+ log.debug('Custom sound for event "%s": %s', event_name, filename)
+ if filepath == old_sound_file:
+ continue
+ custom_sounds[event_name] = filepath
+
+ self.config.config.update(
+ {
+ 'popup_enabled': self.builder.get_object('popup_enabled').get_active(),
+ 'blink_enabled': self.builder.get_object('blink_enabled').get_active(),
+ 'sound_enabled': self.builder.get_object('sound_enabled').get_active(),
+ 'sound_path': new_sound_file,
+ 'subscriptions': {
+ 'popup': current_popup_subscriptions,
+ 'blink': current_blink_subscriptions,
+ 'sound': current_sound_subscriptions,
+ },
+ 'custom_sounds': custom_sounds,
+ }
+ )
+ self.config.save()
+
+ core_config = {
+ 'smtp_enabled': self.builder.get_object('smtp_enabled').get_active(),
+ 'smtp_host': self.builder.get_object('smtp_host').get_text(),
+ 'smtp_port': self.builder.get_object('smtp_port').get_value(),
+ 'smtp_user': self.builder.get_object('smtp_user').get_text(),
+ 'smtp_pass': self.builder.get_object('smtp_pass').get_text(),
+ 'smtp_from': self.builder.get_object('smtp_from').get_text(),
+ 'smtp_tls': self.builder.get_object('smtp_tls').get_active(),
+ 'smtp_recipients': [
+ dest[0] for dest in self.recipients_model if dest[0] != 'USER@HOST'
+ ],
+ 'subscriptions': {'email': current_email_subscriptions},
+ }
+
+ client.notifications.set_config(core_config)
+ client.notifications.get_config().addCallback(self.cb_get_config)
+
+ def on_show_prefs(self):
+ client.notifications.get_config().addCallback(self.cb_get_config)
+
+ def cb_get_config(self, core_config):
+ """Callback for on show_prefs."""
+ self.builder.get_object('smtp_host').set_text(core_config['smtp_host'])
+ self.builder.get_object('smtp_port').set_value(core_config['smtp_port'])
+ self.builder.get_object('smtp_user').set_text(core_config['smtp_user'])
+ self.builder.get_object('smtp_pass').set_text(core_config['smtp_pass'])
+ self.builder.get_object('smtp_from').set_text(core_config['smtp_from'])
+ self.builder.get_object('smtp_tls').set_active(core_config['smtp_tls'])
+ self.recipients_model.clear()
+ for recipient in core_config['smtp_recipients']:
+ self.recipients_model.set(
+ self.recipients_model.append(),
+ RECIPIENT_FIELD,
+ recipient,
+ RECIPIENT_EDIT,
+ False,
+ )
+ self.builder.get_object('smtp_enabled').set_active(core_config['smtp_enabled'])
+ self.builder.get_object('sound_enabled').set_active(
+ self.config['sound_enabled']
+ )
+ self.builder.get_object('popup_enabled').set_active(
+ self.config['popup_enabled']
+ )
+ self.builder.get_object('blink_enabled').set_active(
+ self.config['blink_enabled']
+ )
+ if self.config['sound_path']:
+ sound_path = self.config['sound_path']
+ else:
+ sound_path = deluge.common.get_default_download_dir()
+ self.builder.get_object('sound_path').set_filename(sound_path)
+ # Force toggle
+ self.on_enabled_toggled(self.builder.get_object('smtp_enabled'))
+ self.on_sound_enabled_toggled(self.builder.get_object('sound_enabled'))
+
+ client.notifications.get_handled_events().addCallback(
+ self.popuplate_what_needs_handled_events,
+ core_config['subscriptions']['email'],
+ )
+
+ def on_sound_path_update_preview(self, filechooser):
+ client.notifications.get_handled_events().addCallback(self.populate_sounds)
+
+ def on_add_button_clicked(self, widget, treeview):
+ model = treeview.get_model()
+ model.set(model.append(), RECIPIENT_FIELD, 'USER@HOST', RECIPIENT_EDIT, True)
+
+ def on_delete_button_clicked(self, widget, treeview):
+ selection = treeview.get_selection()
+ model, selected_iter = selection.get_selected()
+ if selected_iter:
+ model.remove(selected_iter)
+
+ def on_cell_edited(self, cell, path_string, new_text, model):
+ str_iter = model.get_iter_from_string(path_string)
+ model.set(str_iter, RECIPIENT_FIELD, new_text)
+
+ def on_recipients_treeview_selection_changed(self, selection):
+ model, selected_connection_iter = selection.get_selected()
+ if selected_connection_iter:
+ self.builder.get_object('delete_button').set_property('sensitive', True)
+ else:
+ self.builder.get_object('delete_button').set_property('sensitive', False)
+
+ def on_subscriptions_treeview_selection_changed(self, selection):
+ model, selected_connection_iter = selection.get_selected()
+ if selected_connection_iter:
+ self.builder.get_object('delete_button').set_property('sensitive', True)
+ else:
+ self.builder.get_object('delete_button').set_property('sensitive', False)
+
+ def on_sounds_treeview_selection_changed(self, selection):
+ model, selected_iter = selection.get_selected()
+ if selected_iter:
+ self.builder.get_object('sounds_edit_button').set_property(
+ 'sensitive', True
+ )
+ path = model.get(selected_iter, SND_PATH)[0]
+ log.debug('Sound selection changed: %s', path)
+ if path != self.config['sound_path']:
+ self.builder.get_object('sounds_revert_button').set_property(
+ 'sensitive', True
+ )
+ else:
+ self.builder.get_object('sounds_revert_button').set_property(
+ 'sensitive', False
+ )
+ else:
+ self.builder.get_object('sounds_edit_button').set_property(
+ 'sensitive', False
+ )
+ self.builder.get_object('sounds_revert_button').set_property(
+ 'sensitive', False
+ )
+
+ def on_sounds_revert_button_clicked(self, widget):
+ log.debug('on_sounds_revert_button_clicked')
+ selection = self.sounds_treeview.get_selection()
+ model, selected_iter = selection.get_selected()
+ if selected_iter:
+ log.debug('on_sounds_revert_button_clicked: got iter')
+ model.set(
+ selected_iter,
+ SND_PATH,
+ self.config['sound_path'],
+ SND_NAME,
+ basename(self.config['sound_path']),
+ )
+
+ def on_sounds_edit_button_clicked(self, widget):
+ log.debug('on_sounds_edit_button_clicked')
+ selection = self.sounds_treeview.get_selection()
+ model, selected_iter = selection.get_selected()
+ if selected_iter:
+ path = model.get(selected_iter, SND_PATH)[0]
+ dialog = Gtk.FileChooserDialog(
+ title=_('Choose Sound File'),
+ buttons=(
+ Gtk.STOCK_CANCEL,
+ Gtk.ResponseType.CANCEL,
+ Gtk.STOCK_OPEN,
+ Gtk.ResponseType.OK,
+ ),
+ )
+ dialog.set_filename(path)
+
+ def update_model(response):
+ if response == Gtk.ResponseType.OK:
+ new_filename = dialog.get_filename()
+ dialog.destroy()
+ log.debug(new_filename)
+ model.set(
+ selected_iter,
+ SND_PATH,
+ new_filename,
+ SND_NAME,
+ basename(new_filename),
+ )
+
+ d = defer.maybeDeferred(dialog.run)
+ d.addCallback(update_model)
+
+ log.debug('dialog should have been shown')
+
+ def on_enabled_toggled(self, widget):
+ for widget_name in (
+ 'smtp_host',
+ 'smtp_port',
+ 'smtp_user',
+ 'smtp_pass',
+ 'smtp_pass',
+ 'smtp_tls',
+ 'smtp_from',
+ 'smtp_recipients',
+ ):
+ self.builder.get_object(widget_name).set_property(
+ 'sensitive', widget.get_active()
+ )
+
+ def on_sound_enabled_toggled(self, widget):
+ if widget.get_active():
+ self.builder.get_object('sound_path').set_property('sensitive', True)
+ self.builder.get_object('sounds_page').set_property('sensitive', True)
+ self.builder.get_object('sounds_page_label').set_property('sensitive', True)
+ else:
+ self.builder.get_object('sound_path').set_property('sensitive', False)
+ self.builder.get_object('sounds_page').set_property('sensitive', False)
+ self.builder.get_object('sounds_page_label').set_property(
+ 'sensitive', False
+ )
+
+ # for widget_name in ('sounds_path', 'sounds_page', 'sounds_page_label'):
+ # self.builder.get_object(widget_name).set_property('sensitive',
+ # widget.get_active())
+
+ def _on_email_col_toggled(self, cell, path):
+ self.subscriptions_model[path][SUB_NOT_EMAIL] = not self.subscriptions_model[
+ path
+ ][SUB_NOT_EMAIL]
+ return
+
+ def _on_popup_col_toggled(self, cell, path):
+ self.subscriptions_model[path][SUB_NOT_POPUP] = not self.subscriptions_model[
+ path
+ ][SUB_NOT_POPUP]
+ return
+
+ def _on_blink_col_toggled(self, cell, path):
+ self.subscriptions_model[path][SUB_NOT_BLINK] = not self.subscriptions_model[
+ path
+ ][SUB_NOT_BLINK]
+ return
+
+ def _on_sound_col_toggled(self, cell, path):
+ self.subscriptions_model[path][SUB_NOT_SOUND] = not self.subscriptions_model[
+ path
+ ][SUB_NOT_SOUND]
+ return
diff --git a/deluge/plugins/Notifications/deluge_notifications/test.py b/deluge/plugins/Notifications/deluge_notifications/test.py
new file mode 100644
index 0000000..2e6f975
--- /dev/null
+++ b/deluge/plugins/Notifications/deluge_notifications/test.py
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+# vim: sw=4 ts=4 fenc=utf-8 et
+# ==============================================================================
+# Copyright © 2009-2010 UfSoft.org - Pedro Algarvio <pedro@algarvio.me>
+#
+# License: BSD - Please view the LICENSE file for additional information.
+# ==============================================================================
+
+from __future__ import unicode_literals
+
+import logging
+
+from twisted.internet import task
+
+from deluge import component
+from deluge.event import DelugeEvent
+
+log = logging.getLogger(__name__)
+
+
+class FooEvent(DelugeEvent):
+ """foo Event"""
+
+
+class CustomEvent(DelugeEvent):
+ """Just a custom event to test"""
+
+
+class TestEmailNotifications(component.Component):
+ def __init__(self, imp):
+ component.Component.__init__(self, self.__class__.__name__, 5)
+ self.__imp = imp
+ self.lc = task.LoopingCall(self.update)
+ self.n = 1
+ self.events = [FooEvent(), CustomEvent()]
+ self.events_classes = []
+
+ def enable(self):
+ log.debug('\n\nEnabling %s', self.__class__.__name__)
+ for event in self.events:
+ if self.__imp == 'core':
+ # component.get('CorePlugin.Notifications').register_custom_email_notification(
+ component.get('Notifications').register_custom_email_notification(
+ event.__class__.__name__, self.custom_email_message_provider
+ )
+ elif self.__imp == 'gtk':
+ notifications_component = component.get('Notifications')
+ notifications_component.register_custom_popup_notification(
+ event.__class__.__name__, self.custom_popup_message_provider
+ )
+ notifications_component.register_custom_blink_notification(
+ event.__class__.__name__, self.custom_blink_message_provider
+ )
+ notifications_component.register_custom_sound_notification(
+ event.__class__.__name__, self.custom_sound_message_provider
+ )
+
+ self.lc.start(60, False)
+
+ def disable(self):
+ log.debug('\n\nDisabling %s', self.__class__.__name__)
+ self.lc.stop()
+
+ def update(self):
+ if self.__imp == 'core':
+ log.debug('\n\nUpdating %s', self.__class__.__name__)
+ self.events.append(self.events.pop(0)) # Re-Queue
+ self.n += 1
+ component.get('EventManager').emit(self.events[0])
+
+ def custom_email_message_provider(self, *evt_args, **evt_kwargs):
+ log.debug('Running custom email message provider: %s %s', evt_args, evt_kwargs)
+ subject = '%s Email Subject: %s' % (self.events[0].__class__.__name__, self.n)
+ message = '%s Email Message: %s' % (self.events[0].__class__.__name__, self.n)
+ return subject, message
+
+ def custom_popup_message_provider(self, *evt_args, **evt_kwargs):
+ log.debug('Running custom popup message provider: %s %s', evt_args, evt_kwargs)
+ title = '%s Popup Title: %s' % (self.events[0].__class__.__name__, self.n)
+ message = '%s Popup Message: %s' % (self.events[0].__class__.__name__, self.n)
+ return title, message
+
+ def custom_blink_message_provider(self, *evt_args, **evt_kwargs):
+ log.debug('Running custom blink message provider: %s %s', evt_args, evt_kwargs)
+ return True
+
+ def custom_sound_message_provider(self, *evt_args, **evt_kwargs):
+ log.debug('Running custom sound message provider: %s %s', evt_args, evt_kwargs)
+ return ''
diff --git a/deluge/plugins/Notifications/deluge_notifications/webui.py b/deluge/plugins/Notifications/deluge_notifications/webui.py
new file mode 100644
index 0000000..d3529c4
--- /dev/null
+++ b/deluge/plugins/Notifications/deluge_notifications/webui.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me>
+#
+# Basic plugin template created by:
+# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
+# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
+# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+from __future__ import unicode_literals
+
+import logging
+
+from deluge.plugins.pluginbase import WebPluginBase
+
+from .common import get_resource
+
+log = logging.getLogger(__name__)
+
+
+class WebUI(WebPluginBase):
+
+ scripts = [get_resource('notifications.js')]
+ debug_scripts = scripts
+
+ def enable(self):
+ log.debug('Enabling Web UI notifications')
+
+ def disable(self):
+ log.debug('Disabling Web UI notifications')