diff options
Diffstat (limited to 'deluge/plugins/Label')
-rw-r--r-- | deluge/plugins/Label/TODO | 11 | ||||
-rw-r--r-- | deluge/plugins/Label/deluge_label/__init__.py | 37 | ||||
-rw-r--r-- | deluge/plugins/Label/deluge_label/common.py | 20 | ||||
-rw-r--r-- | deluge/plugins/Label/deluge_label/core.py | 348 | ||||
-rw-r--r-- | deluge/plugins/Label/deluge_label/data/label.js | 635 | ||||
-rw-r--r-- | deluge/plugins/Label/deluge_label/data/label_add.ui | 172 | ||||
-rw-r--r-- | deluge/plugins/Label/deluge_label/data/label_options.ui | 723 | ||||
-rw-r--r-- | deluge/plugins/Label/deluge_label/data/label_pref.ui | 56 | ||||
-rw-r--r-- | deluge/plugins/Label/deluge_label/gtkui/__init__.py | 74 | ||||
-rw-r--r-- | deluge/plugins/Label/deluge_label/gtkui/label_config.py | 58 | ||||
-rw-r--r-- | deluge/plugins/Label/deluge_label/gtkui/sidebar_menu.py | 259 | ||||
-rw-r--r-- | deluge/plugins/Label/deluge_label/gtkui/submenu.py | 62 | ||||
-rw-r--r-- | deluge/plugins/Label/deluge_label/test.py | 47 | ||||
-rw-r--r-- | deluge/plugins/Label/deluge_label/webui.py | 24 | ||||
-rw-r--r-- | deluge/plugins/Label/setup.py | 45 |
15 files changed, 2571 insertions, 0 deletions
diff --git a/deluge/plugins/Label/TODO b/deluge/plugins/Label/TODO new file mode 100644 index 0000000..c6a5daa --- /dev/null +++ b/deluge/plugins/Label/TODO @@ -0,0 +1,11 @@ +*grey bars are hard-coded , use theme to get bg-color. +*label sub-menu is broken on 1'st popup. +*replacing/restoring the sidebar model is a hack +*config should save a label on bottom ok-button, not a seperate save-button per label +*filters : add "Traffic" , use label-core for filtering ; needs hooks in torrentview. +*torrentview: bugs/hacks in adding and removing columns +*webui is functional but not polished. +*move_torrent_to is not implemeted +*no client-side validation (could be solved by a ui.aclient exception-plugin) +*expand/arrows in sidebar are disabled to save space, fix the space issue or implement an alternative +*fix and move tracker_host column+status-field to core. diff --git a/deluge/plugins/Label/deluge_label/__init__.py b/deluge/plugins/Label/deluge_label/__init__.py new file mode 100644 index 0000000..a6c72f8 --- /dev/null +++ b/deluge/plugins/Label/deluge_label/__init__.py @@ -0,0 +1,37 @@ +# +# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> +# Copyright (C) 2007-2008 Andrew Resch <andrewresch@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 deluge.plugins.init import PluginInitBase + + +class CorePlugin(PluginInitBase): + def __init__(self, plugin_name): + from .core import Core as _pluginCls + + self._plugin_cls = _pluginCls + super().__init__(plugin_name) + + +class GtkUIPlugin(PluginInitBase): + def __init__(self, plugin_name): + from .gtkui import GtkUI as _pluginCls + + self._plugin_cls = _pluginCls + super().__init__(plugin_name) + + +class WebUIPlugin(PluginInitBase): + def __init__(self, plugin_name): + from .webui import WebUI as _pluginCls + + self._plugin_cls = _pluginCls + super().__init__(plugin_name) diff --git a/deluge/plugins/Label/deluge_label/common.py b/deluge/plugins/Label/deluge_label/common.py new file mode 100644 index 0000000..eb47f13 --- /dev/null +++ b/deluge/plugins/Label/deluge_label/common.py @@ -0,0 +1,20 @@ +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> +# 2007-2009 Andrew Resch <andrewresch@gmail.com> +# 2009 Damien Churchill <damoxc@gmail.com> +# 2010 Pedro Algarvio <pedro@algarvio.me> +# 2017 Calum Lind <calumlind+deluge@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. +# + +import os.path + +from pkg_resources import resource_filename + + +def get_resource(filename): + return resource_filename(__package__, os.path.join('data', filename)) diff --git a/deluge/plugins/Label/deluge_label/core.py b/deluge/plugins/Label/deluge_label/core.py new file mode 100644 index 0000000..c28490b --- /dev/null +++ b/deluge/plugins/Label/deluge_label/core.py @@ -0,0 +1,348 @@ +# +# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> +# Copyright (C) 2007-2008 Andrew Resch <andrewresch@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. +# + +""" +torrent-label core plugin. +adds a status field for tracker. +""" +import logging +import re + +import deluge.component as component +from deluge.configmanager import ConfigManager +from deluge.core.rpcserver import export +from deluge.plugins.pluginbase import CorePluginBase + +log = logging.getLogger(__name__) + +RE_VALID = re.compile(r'[a-z0-9_\-\.]*\Z') + +KNOWN_STATES = ['Downloading', 'Seeding', 'Paused', 'Checking', 'Queued', 'Error'] +STATE = 'state' +TRACKER = 'tracker' +KEYWORD = 'keyword' +LABEL = 'label' +CONFIG_DEFAULTS = { + 'torrent_labels': {}, # torrent_id:label_id + 'labels': {}, # label_id:{name:value} +} + +CORE_OPTIONS = ['auto_add_trackers'] + +OPTIONS_DEFAULTS = { + 'apply_max': False, + 'max_download_speed': -1, + 'max_upload_speed': -1, + 'max_connections': -1, + 'max_upload_slots': -1, + 'prioritize_first_last': False, + 'apply_queue': False, + 'is_auto_managed': False, + 'stop_at_ratio': False, + 'stop_ratio': 2.0, + 'remove_at_ratio': False, + 'apply_move_completed': False, + 'move_completed': False, + 'move_completed_path': '', + 'auto_add': False, + 'auto_add_trackers': [], +} + +NO_LABEL = 'No Label' + + +def check_input(cond, message): + if not cond: + raise Exception(message) + + +class Core(CorePluginBase): + """ + self.labels = {label_id:label_options_dict} + self.torrent_labels = {torrent_id:label_id} + """ + + def enable(self): + log.info('*** Start Label plugin ***') + self.plugin = component.get('CorePluginManager') + self.plugin.register_status_field('label', self._status_get_label) + + # __init__ + core = component.get('Core') + self.config = ConfigManager('label.conf', defaults=CONFIG_DEFAULTS) + self.core_cfg = ConfigManager('core.conf') + + # reduce typing, assigning some values to self... + self.torrents = core.torrentmanager.torrents + self.labels = self.config['labels'] + self.torrent_labels = self.config['torrent_labels'] + + self.clean_initial_config() + + component.get('EventManager').register_event_handler( + 'TorrentAddedEvent', self.post_torrent_add + ) + component.get('EventManager').register_event_handler( + 'TorrentRemovedEvent', self.post_torrent_remove + ) + + # register tree: + component.get('FilterManager').register_tree_field( + 'label', self.init_filter_dict + ) + + log.debug('Label plugin enabled..') + + def disable(self): + self.plugin.deregister_status_field('label') + component.get('FilterManager').deregister_tree_field('label') + component.get('EventManager').deregister_event_handler( + 'TorrentAddedEvent', self.post_torrent_add + ) + component.get('EventManager').deregister_event_handler( + 'TorrentRemovedEvent', self.post_torrent_remove + ) + + def update(self): + pass + + def init_filter_dict(self): + filter_dict = {label: 0 for label in self.labels} + filter_dict['All'] = len(self.torrents) + return filter_dict + + # Plugin hooks # + def post_torrent_add(self, torrent_id, from_state): + if from_state: + return + log.debug('post_torrent_add') + torrent = self.torrents[torrent_id] + + for label_id, options in self.labels.items(): + if options['auto_add']: + if self._has_auto_match(torrent, options): + self.set_torrent(torrent_id, label_id) + return + + def post_torrent_remove(self, torrent_id): + log.debug('post_torrent_remove') + if torrent_id in self.torrent_labels: + del self.torrent_labels[torrent_id] + self.config.save() + + # Utils # + def clean_config(self): + """remove invalid data from config-file""" + for torrent_id, label_id in list(self.torrent_labels.items()): + if (label_id not in self.labels) or (torrent_id not in self.torrents): + log.debug('label: rm %s:%s', torrent_id, label_id) + del self.torrent_labels[torrent_id] + + def clean_initial_config(self): + """ + *add any new keys in OPTIONS_DEFAULTS + *set all None values to default <-fix development config + """ + log.debug(list(self.labels)) + for key in self.labels: + options = dict(OPTIONS_DEFAULTS) + options.update(self.labels[key]) + self.labels[key] = options + + for label, options in self.labels.items(): + for key, value in options.items(): + if value is None: + self.labels[label][key] = OPTIONS_DEFAULTS[key] + + def save_config(self): + self.clean_config() + self.config.save() + + @export + def get_labels(self): + return sorted(self.labels) + + # Labels: + @export + def add(self, label_id): + """add a label + see label_set_options for more options. + """ + label_id = label_id.lower() + check_input( + RE_VALID.match(label_id), _('Invalid label, valid characters:[a-z0-9_-]') + ) + check_input(label_id, _('Empty Label')) + check_input(not (label_id in self.labels), _('Label already exists')) + + self.labels[label_id] = dict(OPTIONS_DEFAULTS) + self.config.save() + + @export + def remove(self, label_id): + """remove a label""" + check_input(label_id in self.labels, _('Unknown Label')) + del self.labels[label_id] + self.save_config() + + def _set_torrent_options(self, torrent_id, label_id): + options = self.labels[label_id] + torrent = self.torrents[torrent_id] + + if not options['move_completed_path']: + options['move_completed_path'] = '' # no None. + + if options['apply_max']: + torrent.set_max_download_speed(options['max_download_speed']) + torrent.set_max_upload_speed(options['max_upload_speed']) + torrent.set_max_connections(options['max_connections']) + torrent.set_max_upload_slots(options['max_upload_slots']) + torrent.set_prioritize_first_last_pieces(options['prioritize_first_last']) + + if options['apply_queue']: + torrent.set_auto_managed(options['is_auto_managed']) + torrent.set_stop_at_ratio(options['stop_at_ratio']) + torrent.set_stop_ratio(options['stop_ratio']) + torrent.set_remove_at_ratio(options['remove_at_ratio']) + + if options['apply_move_completed']: + torrent.set_options( + { + 'move_completed': options['move_completed'], + 'move_completed_path': options['move_completed_path'], + } + ) + + def _unset_torrent_options(self, torrent_id, label_id): + options = self.labels[label_id] + torrent = self.torrents[torrent_id] + + if options['apply_max']: + torrent.set_max_download_speed( + self.core_cfg.config['max_download_speed_per_torrent'] + ) + torrent.set_max_upload_speed( + self.core_cfg.config['max_upload_speed_per_torrent'] + ) + torrent.set_max_connections( + self.core_cfg.config['max_connections_per_torrent'] + ) + torrent.set_max_upload_slots( + self.core_cfg.config['max_upload_slots_per_torrent'] + ) + torrent.set_prioritize_first_last_pieces( + self.core_cfg.config['prioritize_first_last_pieces'] + ) + + if options['apply_queue']: + torrent.set_auto_managed(self.core_cfg.config['auto_managed']) + torrent.set_stop_at_ratio(self.core_cfg.config['stop_seed_at_ratio']) + torrent.set_stop_ratio(self.core_cfg.config['stop_seed_ratio']) + torrent.set_remove_at_ratio(self.core_cfg.config['remove_seed_at_ratio']) + + if options['apply_move_completed']: + torrent.set_options( + { + 'move_completed': self.core_cfg.config['move_completed'], + 'move_completed_path': self.core_cfg.config['move_completed_path'], + } + ) + + def _has_auto_match(self, torrent, label_options): + """match for auto_add fields""" + for tracker_match in label_options['auto_add_trackers']: + for tracker in torrent.trackers: + if tracker_match in tracker['url']: + return True + return False + + @export + def set_options(self, label_id, options_dict): + """update the label options + + options_dict : + {"max_download_speed":float(), + "max_upload_speed":float(), + "max_connections":int(), + "max_upload_slots":int(), + #"prioritize_first_last":bool(), + "apply_max":bool(), + "move_completed_to":string() or None + } + """ + check_input(label_id in self.labels, _('Unknown Label')) + for key in options_dict: + if key not in OPTIONS_DEFAULTS: + raise Exception('label: Invalid options_dict key:%s' % key) + + self.labels[label_id].update(options_dict) + + # apply + for torrent_id, label in self.torrent_labels.items(): + if label_id == label and torrent_id in self.torrents: + self._set_torrent_options(torrent_id, label_id) + + # auto add + options = self.labels[label_id] + if options['auto_add']: + for torrent_id, torrent in self.torrents.items(): + if self._has_auto_match(torrent, options): + self.set_torrent(torrent_id, label_id) + + self.config.save() + + @export + def get_options(self, label_id): + """returns the label options""" + return self.labels[label_id] + + @export + def set_torrent(self, torrent_id, label_id): + """ + assign a label to a torrent + removes a label if the label_id parameter is empty. + """ + if label_id == NO_LABEL: + label_id = None + + check_input((not label_id) or (label_id in self.labels), _('Unknown Label')) + check_input(torrent_id in self.torrents, _('Unknown Torrent')) + + if torrent_id in self.torrent_labels: + self._unset_torrent_options(torrent_id, self.torrent_labels[torrent_id]) + del self.torrent_labels[torrent_id] + self.clean_config() + if label_id: + self.torrent_labels[torrent_id] = label_id + self._set_torrent_options(torrent_id, label_id) + + self.config.save() + + @export + def get_config(self): + """see : label_set_config""" + return { + key: self.config[key] for key in CORE_OPTIONS if key in self.config.config + } + + @export + def set_config(self, options): + """global_options:""" + if options: + for key, value in options.items: + if key in CORE_OPTIONS: + self.config[key] = value + + self.config.save() + + def _status_get_label(self, torrent_id): + return self.torrent_labels.get(torrent_id) or '' diff --git a/deluge/plugins/Label/deluge_label/data/label.js b/deluge/plugins/Label/deluge_label/data/label.js new file mode 100644 index 0000000..a0327e3 --- /dev/null +++ b/deluge/plugins/Label/deluge_label/data/label.js @@ -0,0 +1,635 @@ +/** + * label.js + * + * Copyright (C) Damien Churchill 2010 <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. + * + */ + +Ext.ns('Deluge.ux.preferences'); + +/** + * @class Deluge.ux.preferences.LabelPage + * @extends Ext.Panel + */ +Deluge.ux.preferences.LabelPage = Ext.extend(Ext.Panel, { + title: _('Label'), + layout: 'fit', + border: false, + + initComponent: function () { + Deluge.ux.preferences.LabelPage.superclass.initComponent.call(this); + fieldset = this.add({ + xtype: 'fieldset', + border: false, + title: _('Label Preferences'), + autoHeight: true, + labelWidth: 1, + defaultType: 'panel', + }); + fieldset.add({ + border: false, + bodyCfg: { + html: _( + '<p>The Label plugin is enabled.</p><br>' + + '<p>To add, remove or edit labels right-click on the Label filter ' + + 'entry in the sidebar.</p><br>' + + '<p>To apply a label right-click on torrent(s).<p>' + ), + }, + }); + }, +}); + +Ext.ns('Deluge.ux'); + +/** + * @class Deluge.ux.AddLabelWindow + * @extends Ext.Window + */ +Deluge.ux.AddLabelWindow = Ext.extend(Ext.Window, { + title: _('Add Label'), + width: 300, + height: 100, + closeAction: 'hide', + + initComponent: function () { + Deluge.ux.AddLabelWindow.superclass.initComponent.call(this); + this.addButton(_('Cancel'), this.onCancelClick, this); + this.addButton(_('Ok'), this.onOkClick, this); + + this.form = this.add({ + xtype: 'form', + height: 35, + baseCls: 'x-plain', + bodyStyle: 'padding:5px 5px 0', + defaultType: 'textfield', + labelWidth: 50, + items: [ + { + fieldLabel: _('Name'), + name: 'name', + allowBlank: false, + width: 220, + listeners: { + specialkey: { + fn: function (field, e) { + if (e.getKey() == 13) this.onOkClick(); + }, + scope: this, + }, + }, + }, + ], + }); + }, + + onCancelClick: function () { + this.hide(); + }, + + onOkClick: function () { + var label = this.form.getForm().getValues().name; + deluge.client.label.add(label, { + success: function () { + deluge.ui.update(); + this.fireEvent('labeladded', label); + }, + scope: this, + }); + this.hide(); + }, + + onHide: function (comp) { + Deluge.ux.AddLabelWindow.superclass.onHide.call(this, comp); + this.form.getForm().reset(); + }, + + onShow: function (comp) { + Deluge.ux.AddLabelWindow.superclass.onShow.call(this, comp); + this.form.getForm().findField('name').focus(false, 150); + }, +}); + +/** + * @class Deluge.ux.LabelOptionsWindow + * @extends Ext.Window + */ +Deluge.ux.LabelOptionsWindow = Ext.extend(Ext.Window, { + title: _('Label Options'), + width: 325, + height: 240, + closeAction: 'hide', + + initComponent: function () { + Deluge.ux.LabelOptionsWindow.superclass.initComponent.call(this); + this.addButton(_('Cancel'), this.onCancelClick, this); + this.addButton(_('Ok'), this.onOkClick, this); + + this.form = this.add({ + xtype: 'form', + }); + + this.tabs = this.form.add({ + xtype: 'tabpanel', + height: 175, + border: false, + items: [ + { + title: _('Maximum'), + items: [ + { + border: false, + items: [ + { + xtype: 'fieldset', + border: false, + labelWidth: 1, + style: 'margin-bottom: 0px; padding-bottom: 0px;', + items: [ + { + xtype: 'checkbox', + name: 'apply_max', + fieldLabel: '', + boxLabel: _( + 'Apply per torrent max settings:' + ), + listeners: { + check: this.onFieldChecked, + }, + }, + ], + }, + { + xtype: 'fieldset', + border: false, + defaultType: 'spinnerfield', + style: 'margin-top: 0px; padding-top: 0px;', + items: [ + { + fieldLabel: _('Download Speed'), + name: 'max_download_speed', + width: 80, + disabled: true, + value: -1, + minValue: -1, + }, + { + fieldLabel: _('Upload Speed'), + name: 'max_upload_speed', + width: 80, + disabled: true, + value: -1, + minValue: -1, + }, + { + fieldLabel: _('Upload Slots'), + name: 'max_upload_slots', + width: 80, + disabled: true, + value: -1, + minValue: -1, + }, + { + fieldLabel: _('Connections'), + name: 'max_connections', + width: 80, + disabled: true, + value: -1, + minValue: -1, + }, + ], + }, + ], + }, + ], + }, + { + title: _('Queue'), + items: [ + { + border: false, + items: [ + { + xtype: 'fieldset', + border: false, + labelWidth: 1, + style: 'margin-bottom: 0px; padding-bottom: 0px;', + items: [ + { + xtype: 'checkbox', + name: 'apply_queue', + fieldLabel: '', + boxLabel: _( + 'Apply queue settings:' + ), + listeners: { + check: this.onFieldChecked, + }, + }, + ], + }, + { + xtype: 'fieldset', + border: false, + labelWidth: 1, + defaultType: 'checkbox', + style: 'margin-top: 0px; padding-top: 0px;', + defaults: { + style: 'margin-left: 20px', + }, + items: [ + { + boxLabel: _('Auto Managed'), + name: 'is_auto_managed', + disabled: true, + }, + { + boxLabel: _('Stop seed at ratio:'), + name: 'stop_at_ratio', + disabled: true, + }, + { + xtype: 'spinnerfield', + name: 'stop_ratio', + width: 60, + decimalPrecision: 2, + incrementValue: 0.1, + style: 'position: relative; left: 100px', + disabled: true, + }, + { + boxLabel: _('Remove at ratio'), + name: 'remove_at_ratio', + disabled: true, + }, + ], + }, + ], + }, + ], + }, + { + title: _('Folders'), + items: [ + { + border: false, + items: [ + { + xtype: 'fieldset', + border: false, + labelWidth: 1, + style: 'margin-bottom: 0px; padding-bottom: 0px;', + items: [ + { + xtype: 'checkbox', + name: 'apply_move_completed', + fieldLabel: '', + boxLabel: _( + 'Apply folder settings:' + ), + listeners: { + check: this.onFieldChecked, + }, + }, + ], + }, + { + xtype: 'fieldset', + border: false, + labelWidth: 1, + defaultType: 'checkbox', + labelWidth: 1, + style: 'margin-top: 0px; padding-top: 0px;', + defaults: { + style: 'margin-left: 20px', + }, + items: [ + { + boxLabel: _('Move completed to:'), + name: 'move_completed', + disabled: true, + }, + { + xtype: 'textfield', + name: 'move_completed_path', + width: 250, + disabled: true, + }, + ], + }, + ], + }, + ], + }, + { + title: _('Trackers'), + items: [ + { + border: false, + items: [ + { + xtype: 'fieldset', + border: false, + labelWidth: 1, + style: 'margin-bottom: 0px; padding-bottom: 0px;', + items: [ + { + xtype: 'checkbox', + name: 'auto_add', + fieldLabel: '', + boxLabel: _( + 'Automatically apply label:' + ), + listeners: { + check: this.onFieldChecked, + }, + }, + ], + }, + { + xtype: 'fieldset', + border: false, + labelWidth: 1, + style: 'margin-top: 0px; padding-top: 0px;', + defaults: { + style: 'margin-left: 20px', + }, + defaultType: 'textarea', + items: [ + { + boxLabel: _('Move completed to:'), + name: 'auto_add_trackers', + width: 250, + height: 100, + disabled: true, + }, + ], + }, + ], + }, + ], + }, + ], + }); + }, + + getLabelOptions: function () { + deluge.client.label.get_options(this.label, { + success: this.gotOptions, + scope: this, + }); + }, + + gotOptions: function (options) { + this.form.getForm().setValues(options); + }, + + show: function (label) { + Deluge.ux.LabelOptionsWindow.superclass.show.call(this); + this.label = label; + this.setTitle(_('Label Options') + ': ' + this.label); + this.tabs.setActiveTab(0); + this.getLabelOptions(); + }, + + onCancelClick: function () { + this.hide(); + }, + + onOkClick: function () { + var values = this.form.getForm().getFieldValues(); + if (values['auto_add_trackers']) { + values['auto_add_trackers'] = + values['auto_add_trackers'].split('\n'); + } + deluge.client.label.set_options(this.label, values); + this.hide(); + }, + + onFieldChecked: function (field, checked) { + var fs = field.ownerCt.nextSibling(); + fs.items.each(function (field) { + field.setDisabled(!checked); + }); + }, +}); + +Ext.ns('Deluge.plugins'); + +/** + * @class Deluge.plugins.LabelPlugin + * @extends Deluge.Plugin + */ +Deluge.plugins.LabelPlugin = Ext.extend(Deluge.Plugin, { + name: 'Label', + + createMenu: function () { + this.labelMenu = new Ext.menu.Menu({ + items: [ + { + text: _('Add Label'), + iconCls: 'icon-add', + handler: this.onLabelAddClick, + scope: this, + }, + { + text: _('Remove Label'), + disabled: true, + iconCls: 'icon-remove', + handler: this.onLabelRemoveClick, + scope: this, + }, + { + text: _('Label Options'), + disabled: true, + handler: this.onLabelOptionsClick, + scope: this, + }, + ], + }); + }, + + setFilter: function (filter) { + filter.show_zero = true; + + filter.list.on('contextmenu', this.onLabelContextMenu, this); + filter.header.on('contextmenu', this.onLabelHeaderContextMenu, this); + this.filter = filter; + }, + + updateTorrentMenu: function (states) { + this.torrentMenu.removeAll(true); + this.torrentMenu.addMenuItem({ + text: _('No Label'), + label: '', + handler: this.onTorrentMenuClick, + scope: this, + }); + for (var state in states) { + if (!state || state == 'All') continue; + this.torrentMenu.addMenuItem({ + text: state, + label: state, + handler: this.onTorrentMenuClick, + scope: this, + }); + } + }, + + onDisable: function () { + deluge.sidebar.un('filtercreate', this.onFilterCreate); + deluge.sidebar.un('afterfiltercreate', this.onAfterFilterCreate); + delete Deluge.FilterPanel.templates.label; + this.deregisterTorrentStatus('label'); + deluge.menus.torrent.remove(this.tmSep); + deluge.menus.torrent.remove(this.tm); + deluge.preferences.removePage(this.prefsPage); + }, + + onEnable: function () { + this.prefsPage = deluge.preferences.addPage( + new Deluge.ux.preferences.LabelPage() + ); + this.torrentMenu = new Ext.menu.Menu(); + + this.tmSep = deluge.menus.torrent.add({ + xtype: 'menuseparator', + }); + + this.tm = deluge.menus.torrent.add({ + text: _('Label'), + menu: this.torrentMenu, + }); + + var lbltpl = + '<div class="x-deluge-filter">' + + '<tpl if="filter">{filter}</tpl>' + + '<tpl if="!filter">No Label</tpl>' + + ' ({count})' + + '</div>'; + + if (deluge.sidebar.hasFilter('label')) { + var filter = deluge.sidebar.getFilter('label'); + filter.list.columns[0].tpl = new Ext.XTemplate(lbltpl); + this.setFilter(filter); + this.updateTorrentMenu(filter.getStates()); + filter.list.refresh(); + } else { + deluge.sidebar.on('filtercreate', this.onFilterCreate, this); + deluge.sidebar.on( + 'afterfiltercreate', + this.onAfterFilterCreate, + this + ); + Deluge.FilterPanel.templates.label = lbltpl; + } + this.registerTorrentStatus('label', _('Label')); + }, + + onAfterFilterCreate: function (sidebar, filter) { + if (filter.filter != 'label') return; + this.updateTorrentMenu(filter.getStates()); + }, + + onFilterCreate: function (sidebar, filter) { + if (filter.filter != 'label') return; + this.setFilter(filter); + }, + + onLabelAddClick: function () { + if (!this.addWindow) { + this.addWindow = new Deluge.ux.AddLabelWindow(); + this.addWindow.on('labeladded', this.onLabelAdded, this); + } + this.addWindow.show(); + }, + + onLabelAdded: function (label) { + var filter = deluge.sidebar.getFilter('label'); + var states = filter.getStates(); + var statesArray = []; + + for (state in states) { + if (!state || state == 'All') continue; + statesArray.push(state); + } + + statesArray.push(label.toLowerCase()); + statesArray.sort(); + + //console.log(states); + //console.log(statesArray); + + states = {}; + + for (i = 0; i < statesArray.length; ++i) { + states[statesArray[i]] = 0; + } + + this.updateTorrentMenu(states); + }, + + onLabelContextMenu: function (dv, i, node, e) { + e.preventDefault(); + if (!this.labelMenu) this.createMenu(); + var r = dv.getRecord(node).get('filter'); + if (!r || r == 'All') { + this.labelMenu.items.get(1).setDisabled(true); + this.labelMenu.items.get(2).setDisabled(true); + } else { + this.labelMenu.items.get(1).setDisabled(false); + this.labelMenu.items.get(2).setDisabled(false); + } + dv.select(i); + this.labelMenu.showAt(e.getXY()); + }, + + onLabelHeaderContextMenu: function (e, t) { + e.preventDefault(); + if (!this.labelMenu) this.createMenu(); + this.labelMenu.items.get(1).setDisabled(true); + this.labelMenu.items.get(2).setDisabled(true); + this.labelMenu.showAt(e.getXY()); + }, + + onLabelOptionsClick: function () { + if (!this.labelOpts) + this.labelOpts = new Deluge.ux.LabelOptionsWindow(); + this.labelOpts.show(this.filter.getState()); + }, + + onLabelRemoveClick: function () { + var state = this.filter.getState(); + deluge.client.label.remove(state, { + success: function () { + deluge.ui.update(); + this.torrentMenu.items.each(function (item) { + if (item.text != state) return; + this.torrentMenu.remove(item); + var i = item; + }, this); + }, + scope: this, + }); + }, + + onTorrentMenuClick: function (item, e) { + var ids = deluge.torrents.getSelectedIds(); + Ext.each(ids, function (id, i) { + if (ids.length == i + 1) { + deluge.client.label.set_torrent(id, item.label, { + success: function () { + deluge.ui.update(); + }, + }); + } else { + deluge.client.label.set_torrent(id, item.label); + } + }); + }, +}); +Deluge.registerPlugin('Label', Deluge.plugins.LabelPlugin); diff --git a/deluge/plugins/Label/deluge_label/data/label_add.ui b/deluge/plugins/Label/deluge_label/data/label_add.ui new file mode 100644 index 0000000..e550675 --- /dev/null +++ b/deluge/plugins/Label/deluge_label/data/label_add.ui @@ -0,0 +1,172 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkDialog" id="dlg_label_add"> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="title" translatable="yes">Add Label</property> + <property name="resizable">False</property> + <property name="modal">True</property> + <property name="window_position">mouse</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <property name="skip_taskbar_hint">True</property> + <child> + <placeholder/> + </child> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">2</property> + <child internal-child="action_area"> + <object class="GtkButtonBox" id="dialog-action_area2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="button2"> + <property name="label">gtk-cancel</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_cancel" 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="button1"> + <property name="label">gtk-ok</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + <signal name="clicked" handler="on_add_ok" 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="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox" id="hbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkImage" id="image1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="stock">gtk-add</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label10"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes"><b>Add Label</b></property> + <property name="use_markup">True</property> + </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">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHSeparator" id="hseparator2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</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> + <property name="spacing">5</property> + <child> + <object class="GtkLabel" id="label11"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Name:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="txt_add"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="activates_default">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="0">button2</action-widget> + <action-widget response="0">button1</action-widget> + </action-widgets> + </object> +</interface> diff --git a/deluge/plugins/Label/deluge_label/data/label_options.ui b/deluge/plugins/Label/deluge_label/data/label_options.ui new file mode 100644 index 0000000..d390865 --- /dev/null +++ b/deluge/plugins/Label/deluge_label/data/label_options.ui @@ -0,0 +1,723 @@ +<?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">9999</property> + <property name="value">-1</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment2"> + <property name="lower">-1</property> + <property name="upper">9999</property> + <property name="value">-1</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment3"> + <property name="lower">-1</property> + <property name="upper">9999</property> + <property name="value">-1</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment4"> + <property name="lower">-1</property> + <property name="upper">9999</property> + <property name="value">-1</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment5"> + <property name="lower">0.10000000000000001</property> + <property name="upper">100</property> + <property name="value">2</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkTextBuffer" id="textbuffer1"> + <property name="text" translatable="yes">tracker1.org</property> + </object> + <object class="GtkDialog" id="dlg_label_options"> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="title" translatable="yes">Label Options</property> + <property name="modal">True</property> + <property name="window_position">mouse</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <property name="skip_taskbar_hint">True</property> + <child> + <placeholder/> + </child> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child internal-child="action_area"> + <object class="GtkButtonBox" id="dialog-action_area1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="button4"> + <property name="label">gtk-cancel</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_options_cancel" 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="button3"> + <property name="label">gtk-ok</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_options_ok" 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="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="hbox3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkImage" id="image2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="stock">gtk-preferences</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_header"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes"><b>Label Options</b></property> + <property name="use_markup">True</property> + <property name="xalign">0</property> + </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">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkHSeparator" id="hseparator1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkNotebook" id="notebook2"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkFrame" id="frame1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">2</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">12</property> + <child> + <object class="GtkTable" id="table1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_rows">5</property> + <property name="n_columns">4</property> + <property name="column_spacing">5</property> + <property name="row_spacing">5</property> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="left_attach">3</property> + <property name="right_attach">4</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="max_upload_speed"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">adjustment1</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> + <property name="x_options"/> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkLabel" id="label24"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">KiB/s</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="max_download_speed"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">adjustment2</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="x_options"/> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkLabel" id="label6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Upload Slots:</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Upload Speed:</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Download Speed:</property> + <property name="use_markup">True</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">KiB/s</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> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="max_upload_slots"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">adjustment3</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options"/> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkLabel" id="label2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Connections:</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="max_connections"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">adjustment4</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + <property name="x_options"/> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkLabel" id="label14"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="right_attach">2</property> + <property name="top_attach">4</property> + <property name="bottom_attach">5</property> + <property name="x_options">GTK_FILL</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkCheckButton" id="apply_max"> + <property name="label" translatable="yes">Apply per torrent max settings:</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> + </child> + </object> + </child> + <child type="tab"> + <object class="GtkLabel" id="label7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Maximum</property> + </object> + <packing> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">2</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> + <child> + <object class="GtkTable" id="table2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_rows">4</property> + <property name="n_columns">3</property> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <object class="GtkLabel" id="label16"> + <property name="visible">True</property> + <property name="can_focus">False</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> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child> + <object class="GtkCheckButton" id="is_auto_managed"> + <property name="label" translatable="yes">Auto Managed</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="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="stop_at_ratio"> + <property name="label" translatable="yes">Stop seed at ratio:</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="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="remove_at_ratio"> + <property name="label" translatable="yes">Remove at ratio</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="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="stop_ratio"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">adjustment5</property> + <property name="digits">2</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> + <property name="x_options"/> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label13"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="right_attach">2</property> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + <property name="x_options">GTK_FILL</property> + </packing> + </child> + <child> + <placeholder/> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkCheckButton" id="apply_queue"> + <property name="label" translatable="yes">Apply Queue settings:</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> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="label8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Queue</property> + </object> + <packing> + <property name="position">1</property> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">2</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> + <child> + <object class="GtkTable" id="table3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_rows">3</property> + <property name="n_columns">2</property> + <child> + <object class="GtkCheckButton" id="move_completed"> + <property name="label" translatable="yes">Move completed to:</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="right_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label9"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="right_attach">2</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + </packing> + </child> + <child> + <object class="GtkFileChooserButton" id="move_completed_path"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="action">select-folder</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="move_completed_path_entry"> + <property name="can_focus">True</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> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkCheckButton" id="apply_move_completed"> + <property name="label" translatable="yes">Apply folder settings:</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> + </child> + </object> + <packing> + <property name="position">2</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="label21"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Folders</property> + </object> + <packing> + <property name="position">2</property> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">2</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="left_padding">12</property> + <child> + <object class="GtkBox" id="vbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</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="GtkTextView" id="auto_add_trackers"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="buffer">textbuffer1</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="GtkLabel" id="label23"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes"><i>(1 line per tracker)</i></property> + <property name="use_markup">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkCheckButton" id="auto_add"> + <property name="label" translatable="yes">Automatically apply label:</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + </object> + </child> + </object> + <packing> + <property name="position">3</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="label99"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Trackers</property> + </object> + <packing> + <property name="position">3</property> + <property name="tab_fill">False</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="0">button4</action-widget> + <action-widget response="0">button3</action-widget> + </action-widgets> + </object> +</interface> diff --git a/deluge/plugins/Label/deluge_label/data/label_pref.ui b/deluge/plugins/Label/deluge_label/data/label_pref.ui new file mode 100644 index 0000000..81edc37 --- /dev/null +++ b/deluge/plugins/Label/deluge_label/data/label_pref.ui @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkWindow" id="window1"> + <property name="can_focus">False</property> + <child> + <placeholder/> + </child> + <child> + <object class="GtkBox" id="label_prefs_box"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkFrame" id="frame3"> + <property name="visible">True</property> + <property name="can_focus">False</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> + <child> + <object class="GtkLabel" id="label6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes"><i>Use the sidebar to add,edit and remove labels. </i> +</property> + <property name="use_markup">True</property> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes"><b>Labels</b></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> + </object> + </child> + </object> +</interface> diff --git a/deluge/plugins/Label/deluge_label/gtkui/__init__.py b/deluge/plugins/Label/deluge_label/gtkui/__init__.py new file mode 100644 index 0000000..6170716 --- /dev/null +++ b/deluge/plugins/Label/deluge_label/gtkui/__init__.py @@ -0,0 +1,74 @@ +# +# Copyright (C) 2008 Martijn Voncken <mvoncken@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. +# + +import logging + +from deluge import component # for systray +from deluge.plugins.pluginbase import Gtk3PluginBase + +from . import label_config, sidebar_menu, submenu + +log = logging.getLogger(__name__) + +NO_LABEL = 'No Label' + + +def cell_data_label(column, cell, model, row, data): + cell.set_property('text', str(model.get_value(row, data))) + + +class GtkUI(Gtk3PluginBase): + def start(self): + if self.label_menu: + self.label_menu.on_show() + + def enable(self): + self.plugin = component.get('PluginManager') + self.torrentmenu = component.get('MenuBar').torrentmenu + self.label_menu = None + self.labelcfg = None + self.sidebar_menu = None + self.load_interface() + + def disable(self): + if self.label_menu in self.torrentmenu.get_children(): + self.torrentmenu.remove(self.label_menu) + + self.labelcfg.unload() + self.sidebar_menu.unload() + del self.sidebar_menu + + component.get('TorrentView').remove_column(_('Label')) + + def load_interface(self): + # sidebar + # disabled + if not self.sidebar_menu: + self.sidebar_menu = sidebar_menu.LabelSidebarMenu() + # self.sidebar.load() + + # menu: + log.debug('add items to torrentview-popup menu.') + self.label_menu = submenu.LabelMenu() + self.torrentmenu.append(self.label_menu) + self.label_menu.show_all() + + # columns: + self.load_columns() + + # config: + if not self.labelcfg: + self.labelcfg = label_config.LabelConfig(self.plugin) + self.labelcfg.load() + + log.debug('Finished loading Label plugin') + + def load_columns(self): + log.debug('add columns') + + component.get('TorrentView').add_text_column(_('Label'), status_field=['label']) diff --git a/deluge/plugins/Label/deluge_label/gtkui/label_config.py b/deluge/plugins/Label/deluge_label/gtkui/label_config.py new file mode 100644 index 0000000..26c827e --- /dev/null +++ b/deluge/plugins/Label/deluge_label/gtkui/label_config.py @@ -0,0 +1,58 @@ +# +# Copyright (C) 2008 Martijn Voncken <mvoncken@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. +# + +import logging + +from gi.repository.Gtk import Builder + +from deluge.ui.client import client + +from ..common import get_resource + +log = logging.getLogger(__name__) + + +class LabelConfig: + """ + there used to be some options here... + """ + + def __init__(self, plugin): + self.plugin = plugin + + def load(self): + log.debug('Adding Label Preferences page') + builder = Builder() + builder.add_from_file(get_resource('label_pref.ui')) + + self.plugin.add_preferences_page( + _('Label'), builder.get_object('label_prefs_box') + ) + self.plugin.register_hook('on_show_prefs', self.load_settings) + self.plugin.register_hook('on_apply_prefs', self.on_apply_prefs) + + self.load_settings() + + def unload(self): + self.plugin.remove_preferences_page(_('Label')) + self.plugin.deregister_hook('on_apply_prefs', self.on_apply_prefs) + self.plugin.deregister_hook('on_show_prefs', self.load_settings) + + def load_settings(self, widget=None, data=None): + client.label.get_config().addCallback(self.cb_global_options) + + def cb_global_options(self, options): + log.debug('options=%s', options) + + # for id in self.chk_ids: + # self.glade.get_widget(id).set_active(bool(options[id])) + + def on_apply_prefs(self): + options = {} + # update options dict here. + client.label.set_config(options) diff --git a/deluge/plugins/Label/deluge_label/gtkui/sidebar_menu.py b/deluge/plugins/Label/deluge_label/gtkui/sidebar_menu.py new file mode 100644 index 0000000..9d164b2 --- /dev/null +++ b/deluge/plugins/Label/deluge_label/gtkui/sidebar_menu.py @@ -0,0 +1,259 @@ +# +# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> +# Copyright (C) 2007 Andrew Resch <andrewresch@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. +# + +import logging + +import gi # isort:skip (Required before Gtk import). + +gi.require_version('Gtk', '3.0') + +# isort:imports-thirdparty +from gi.repository import Gtk + +# isort:imports-firstparty +import deluge.component as component +from deluge.ui.client import client + +# isort:imports-localfolder +from ..common import get_resource + +log = logging.getLogger(__name__) + +NO_LABEL = 'No Label' + + +# menu +class LabelSidebarMenu: + def __init__(self): + self.treeview = component.get('FilterTreeView') + self.menu = self.treeview.menu + self.items = [] + + # add items, in reverse order, because they are prepended. + sep = Gtk.SeparatorMenuItem() + self.items.append(sep) + self.menu.prepend(sep) + self._add_item('options', _('Label _Options')) + self._add_item('remove', _('_Remove Label')) + self._add_item('add', _('_Add Label')) + + self.menu.show_all() + # dialogs: + self.add_dialog = AddDialog() + self.options_dialog = OptionsDialog() + # hooks: + self.menu.connect('show', self.on_show, None) + + def _add_item(self, item_id, label): + """ + id is automatically-added as self.item_<id> + """ + item = Gtk.MenuItem.new_with_mnemonic(label) + func = getattr(self, 'on_%s' % item_id) + item.connect('activate', func) + self.menu.prepend(item) + setattr(self, 'item_%s' % item_id, item) + self.items.append(item) + return item + + def on_add(self, event=None): + self.add_dialog.show() + + def on_remove(self, event=None): + client.label.remove(self.treeview.value) + + def on_options(self, event=None): + self.options_dialog.show(self.treeview.value) + + def on_show(self, widget=None, data=None): + """No Label:disable options/del.""" + log.debug('label-sidebar-popup:on-show') + + cat = self.treeview.cat + label = self.treeview.value + if cat == 'label' or (cat == 'cat' and label == 'label'): + # is a label : show menu-items + for item in self.items: + item.show() + # default items + sensitive = (label not in (NO_LABEL, None, '', 'All')) and (cat != 'cat') + for item in self.items: + item.set_sensitive(sensitive) + + # add is always enabled. + self.item_add.set_sensitive(True) + else: + # not a label -->hide everything. + for item in self.items: + item.hide() + + def unload(self): + log.debug('disable01') + for item in list(self.items): + item.hide() + item.destroy() + log.debug('disable02') + self.items = [] + + +# dialogs: +class AddDialog: + def __init__(self): + pass + + def show(self): + self.builder = Gtk.Builder() + self.builder.add_from_file(get_resource('label_add.ui')) + self.dialog = self.builder.get_object('dlg_label_add') + self.dialog.set_transient_for(component.get('MainWindow').window) + + self.builder.connect_signals(self) + self.dialog.run() + + def on_add_ok(self, event=None): + value = self.builder.get_object('txt_add').get_text() + client.label.add(value) + self.dialog.destroy() + + def on_add_cancel(self, event=None): + self.dialog.destroy() + + +class OptionsDialog: + spin_ids = ['max_download_speed', 'max_upload_speed', 'stop_ratio'] + spin_int_ids = ['max_upload_slots', 'max_connections'] + chk_ids = [ + 'apply_max', + 'apply_queue', + 'stop_at_ratio', + 'apply_queue', + 'remove_at_ratio', + 'apply_move_completed', + 'move_completed', + 'is_auto_managed', + 'auto_add', + ] + + # list of tuples, because order matters when nesting. + sensitive_groups = [ + ( + 'apply_max', + [ + 'max_download_speed', + 'max_upload_speed', + 'max_upload_slots', + 'max_connections', + ], + ), + ('apply_queue', ['is_auto_managed', 'stop_at_ratio']), + ('stop_at_ratio', ['remove_at_ratio', 'stop_ratio']), # nested + ('apply_move_completed', ['move_completed']), + ('move_completed', ['move_completed_path']), # nested + ('auto_add', ['auto_add_trackers']), + ] + + def __init__(self): + pass + + def show(self, label): + self.label = label + self.builder = Gtk.Builder() + self.builder.add_from_file(get_resource('label_options.ui')) + self.dialog = self.builder.get_object('dlg_label_options') + self.dialog.set_transient_for(component.get('MainWindow').window) + self.builder.connect_signals(self) + # Show the label name in the header label + self.builder.get_object('label_header').set_markup( + '<b>{}:</b> {}'.format(_('Label Options'), self.label) + ) + + for chk_id, group in self.sensitive_groups: + chk = self.builder.get_object(chk_id) + chk.connect('toggled', self.apply_sensitivity) + + client.label.get_options(self.label).addCallback(self.load_options) + + self.dialog.run() + + def load_options(self, options): + log.debug(list(options)) + + for spin_id in self.spin_ids + self.spin_int_ids: + self.builder.get_object(spin_id).set_value(options[spin_id]) + for chk_id in self.chk_ids: + self.builder.get_object(chk_id).set_active(bool(options[chk_id])) + + if client.is_localhost(): + self.builder.get_object('move_completed_path').set_filename( + options['move_completed_path'] + ) + self.builder.get_object('move_completed_path').show() + self.builder.get_object('move_completed_path_entry').hide() + else: + self.builder.get_object('move_completed_path_entry').set_text( + options['move_completed_path'] + ) + self.builder.get_object('move_completed_path_entry').show() + self.builder.get_object('move_completed_path').hide() + + self.builder.get_object('auto_add_trackers').get_buffer().set_text( + '\n'.join(options['auto_add_trackers']) + ) + + self.apply_sensitivity() + + def on_options_ok(self, event=None): + """Save options.""" + options = {} + + for spin_id in self.spin_ids: + options[spin_id] = self.builder.get_object(spin_id).get_value() + for spin_int_id in self.spin_int_ids: + options[spin_int_id] = self.builder.get_object( + spin_int_id + ).get_value_as_int() + for chk_id in self.chk_ids: + options[chk_id] = self.builder.get_object(chk_id).get_active() + + if client.is_localhost(): + options['move_completed_path'] = self.builder.get_object( + 'move_completed_path' + ).get_filename() + else: + options['move_completed_path'] = self.builder.get_object( + 'move_completed_path_entry' + ).get_text() + + buff = self.builder.get_object( + 'auto_add_trackers' + ).get_buffer() # sometimes I hate gtk... + tracker_lst = ( + buff.get_text( + buff.get_start_iter(), buff.get_end_iter(), include_hidden_chars=False + ) + .strip() + .split('\n') + ) + options['auto_add_trackers'] = [ + x for x in tracker_lst if x + ] # filter out empty lines. + + log.debug(options) + client.label.set_options(self.label, options) + self.dialog.destroy() + + def apply_sensitivity(self, event=None): + for chk_id, sensitive_list in self.sensitive_groups: + chk = self.builder.get_object(chk_id) + sens = chk.get_active() and chk.get_property('sensitive') + for widget_id in sensitive_list: + self.builder.get_object(widget_id).set_sensitive(sens) + + def on_options_cancel(self, event=None): + self.dialog.destroy() diff --git a/deluge/plugins/Label/deluge_label/gtkui/submenu.py b/deluge/plugins/Label/deluge_label/gtkui/submenu.py new file mode 100644 index 0000000..54b6594 --- /dev/null +++ b/deluge/plugins/Label/deluge_label/gtkui/submenu.py @@ -0,0 +1,62 @@ +# +# Copyright (C) 2008 Martijn Voncken <mvoncken@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. +# + + +import logging + +from gi.repository.Gtk import Menu, MenuItem + +from deluge import component # for systray +from deluge.ui.client import client + +log = logging.getLogger(__name__) + + +# Deferred Translation +def _(message): + return message + + +NO_LABEL = _('No Label') +del _ + + +class LabelMenu(MenuItem): + def __init__(self): + MenuItem.__init__(self, _('Label')) # noqa: F821 + + self.sub_menu = Menu() + self.set_submenu(self.sub_menu) + self.items = [] + + # attach.. + self.sub_menu.connect('show', self.on_show, None) + + def get_torrent_ids(self): + return component.get('TorrentView').get_selected_torrents() + + def on_show(self, widget=None, data=None): + log.debug('label-on-show') + client.label.get_labels().addCallback(self.cb_labels) + + def cb_labels(self, labels): + for child in self.sub_menu.get_children(): + self.sub_menu.remove(child) + for label in [NO_LABEL] + list(labels): + if label == NO_LABEL: + item = MenuItem(_(NO_LABEL)) # noqa: F821 + else: + item = MenuItem(label) + item.connect('activate', self.on_select_label, label) + self.sub_menu.append(item) + self.show_all() + + def on_select_label(self, widget=None, label_id=None): + log.debug('select label:%s,%s', label_id, self.get_torrent_ids()) + for torrent_id in self.get_torrent_ids(): + client.label.set_torrent(torrent_id, label_id) diff --git a/deluge/plugins/Label/deluge_label/test.py b/deluge/plugins/Label/deluge_label/test.py new file mode 100644 index 0000000..739bae4 --- /dev/null +++ b/deluge/plugins/Label/deluge_label/test.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# +# -*- coding: utf-8 -*- +# +# Copyright (C) 2008 Martijn Voncken <mvoncken@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 deluge.ui.client import sclient + +sclient.set_core_uri() + +print(sclient.get_enabled_plugins()) + +# enable plugin. +if 'label' not in sclient.get_enabled_plugins(): + sclient.enable_plugin('label') + + +# test labels. +print('#init labels') +try: + sclient.label_remove('test') +except Exception: + pass +sess_id = sclient.get_session_state()[0] + +print('#add') +sclient.label_add('test') +print('#set') +sclient.label_set_torrent(id, 'test') + +print(sclient.get_torrents_status({'label': 'test'}, 'name')) + + +print('#set options') +sclient.label_set_options('test', {'max_download_speed': 999}, True) +print(sclient.get_torrent_status(sess_id, ['max_download_speed']), '999') +sclient.label_set_options('test', {'max_download_speed': 9}, True) +print(sclient.get_torrent_status(sess_id, ['max_download_speed']), '9') +sclient.label_set_options('test', {'max_download_speed': 888}, False) +print(sclient.get_torrent_status(sess_id, ['max_download_speed']), '9 (888)') + +print(sclient.get_torrent_status(sess_id, ['name', 'tracker_host', 'label'])) diff --git a/deluge/plugins/Label/deluge_label/webui.py b/deluge/plugins/Label/deluge_label/webui.py new file mode 100644 index 0000000..9ccfa92 --- /dev/null +++ b/deluge/plugins/Label/deluge_label/webui.py @@ -0,0 +1,24 @@ +# +# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> +# Copyright (C) 2007-2008 Andrew Resch <andrewresch@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. +# + +import logging + +from deluge.plugins.pluginbase import WebPluginBase + +from .common import get_resource + +log = logging.getLogger(__name__) + + +class WebUI(WebPluginBase): + scripts = [get_resource('label.js')] + debug_scripts = scripts diff --git a/deluge/plugins/Label/setup.py b/deluge/plugins/Label/setup.py new file mode 100644 index 0000000..f8f2c5d --- /dev/null +++ b/deluge/plugins/Label/setup.py @@ -0,0 +1,45 @@ +# +# Copyright (C) 2008 Martijn Voncken <mvoncken@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 setuptools import find_packages, setup + +__plugin_name__ = 'Label' +__author__ = 'Martijn Voncken' +__author_email__ = 'mvoncken@gmail.com' +__version__ = '0.3' +__url__ = 'http://deluge-torrent.org' +__license__ = 'GPLv3' +__description__ = 'Allows labels to be assigned to torrents' +__long_description__ = """ +Allows labels to be assigned to torrents + +Also offers filters on state, tracker and keywords +""" +__pkg_data__ = {'deluge_' + __plugin_name__.lower(): ['data/*']} + +setup( + name=__plugin_name__, + version=__version__, + description=__description__, + author=__author__, + author_email=__author_email__, + url=__url__, + license=__license__, + long_description=__long_description__, + packages=find_packages(), + package_data=__pkg_data__, + entry_points=""" + [deluge.plugin.core] + %s = deluge_%s:CorePlugin + [deluge.plugin.gtk3ui] + %s = deluge_%s:GtkUIPlugin + [deluge.plugin.web] + %s = deluge_%s:WebUIPlugin + """ + % ((__plugin_name__, __plugin_name__.lower()) * 3), +) |