瀏覽代碼

Add notifications plugin

Fixes issue CLIENT-70

Depends-on: I75eedbd9966d08d1f600b78f63ece76ad0b27126
Change-Id: I155eede0f29c9dcf2155542c6cc7817ccea096b0
Reviewed-on: http://gerrit.dmdirc.com/1543
Automatic-Compile: Greg Holmes <greg@dmdirc.com>
Reviewed-by: Chris Smith <chris@dmdirc.com>
tags/0.6.5
Greg Holmes 13 年之前
父節點
當前提交
0a50d61679

+ 163
- 0
src/com/dmdirc/addons/notifications/NotificationCommand.java 查看文件

@@ -0,0 +1,163 @@
1
+/*
2
+ * Copyright (c) 2006-2010 Chris Smith, Shane Mc Cormack, Gregory Holmes
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ * of this software and associated documentation files (the "Software"), to deal
6
+ * in the Software without restriction, including without limitation the rights
7
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ * copies of the Software, and to permit persons to whom the Software is
9
+ * furnished to do so, subject to the following conditions:
10
+ *
11
+ * The above copyright notice and this permission notice shall be included in
12
+ * all copies or substantial portions of the Software.
13
+ *
14
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ * SOFTWARE.
21
+ */
22
+
23
+package com.dmdirc.addons.notifications;
24
+
25
+import com.dmdirc.FrameContainer;
26
+import com.dmdirc.commandparser.CommandArguments;
27
+import com.dmdirc.commandparser.CommandInfo;
28
+import com.dmdirc.commandparser.CommandType;
29
+import com.dmdirc.commandparser.commands.Command;
30
+import com.dmdirc.commandparser.commands.IntelligentCommand;
31
+import com.dmdirc.commandparser.commands.context.CommandContext;
32
+import com.dmdirc.plugins.ExportedService;
33
+import com.dmdirc.plugins.PluginInfo;
34
+import com.dmdirc.ui.input.AdditionalTabTargets;
35
+import com.dmdirc.ui.input.TabCompleter;
36
+
37
+import java.util.List;
38
+
39
+/**
40
+ * Notification command, delegating notification to one of the registered
41
+ * notification commands as preferred by the end user.
42
+ */
43
+public class NotificationCommand extends Command implements
44
+        IntelligentCommand, CommandInfo {
45
+
46
+    /** The plugin that's using this command. */
47
+    private final NotificationsPlugin parent;
48
+
49
+    /**
50
+     * Creates a new instance of this notification command.
51
+     *
52
+     * @param parent The plugin that's instansiating this command
53
+     */
54
+    public NotificationCommand(final NotificationsPlugin parent) {
55
+        super();
56
+
57
+        this.parent = parent;
58
+    }
59
+
60
+    /** {@inheritDoc} */
61
+    @Override
62
+    public void execute(final FrameContainer<?> origin,
63
+            final CommandArguments args, final CommandContext context) {
64
+        if (args.getArguments().length > 0 && args.getArguments()[0]
65
+                .equalsIgnoreCase("--methods")) {
66
+            doMethodList(origin, args.isSilent());
67
+        } else if (args.getArguments().length > 0 && args.getArguments()[0]
68
+                .equalsIgnoreCase("--method")) {
69
+            if (args.getArguments().length > 1) {
70
+                final String sourceName = args.getArguments()[1];
71
+                final ExportedService source = parent.getMethod(sourceName)
72
+                        .getExportedService("showNotification");
73
+
74
+                if (source == null) {
75
+                    sendLine(origin, args.isSilent(), FORMAT_ERROR,
76
+                            "Method not found.");
77
+                } else {
78
+                    source.execute("DMDirc", args.getArgumentsAsString(2));
79
+                }
80
+            } else {
81
+                sendLine(origin, args.isSilent(), FORMAT_ERROR,
82
+                        "You must specify a method when using --method.");
83
+            }
84
+        } else if (parent.hasActiveMethod()) {
85
+            parent.getPreferredMethod().getExportedService("showNotification")
86
+                    .execute("DMDirc", args.getArgumentsAsString(0));
87
+        } else {
88
+            sendLine(origin, args.isSilent(), FORMAT_ERROR,
89
+                    "No active notification methods available.");
90
+        }
91
+    }
92
+
93
+    /**
94
+     * Outputs a list of methods for the notifcation command.
95
+     *
96
+     * @param origin The input window where the command was entered
97
+     * @param isSilent Whether this command is being silenced
98
+     */
99
+    private void doMethodList(final FrameContainer<?> origin,
100
+            final boolean isSilent) {
101
+        final List<PluginInfo> methods = parent.getMethods();
102
+
103
+        if (methods.isEmpty()) {
104
+            sendLine(origin, isSilent, FORMAT_ERROR, "No notification "
105
+                    + "methods available.");
106
+        } else {
107
+            final String[] headers = {"Method"};
108
+            final String[][] data = new String[methods.size()][1];
109
+            int i = 0;
110
+            for (PluginInfo method : methods) {
111
+                data[i][0] = method.getName();
112
+                i++;
113
+            }
114
+
115
+            sendLine(origin, isSilent, FORMAT_OUTPUT, doTable(headers, data));
116
+        }
117
+    }
118
+
119
+    /** {@inheritDoc} */
120
+    @Override
121
+    public AdditionalTabTargets getSuggestions(final int arg,
122
+            final IntelligentCommandContext context) {
123
+        final AdditionalTabTargets res = new AdditionalTabTargets();
124
+        res.excludeAll();
125
+        if (arg == 0) {
126
+            res.add("--methods");
127
+            res.add("--method");
128
+            return res;
129
+        } else if (arg == 1 && context.getPreviousArgs().get(0)
130
+                .equalsIgnoreCase("--method")) {
131
+            for (PluginInfo source : parent.getMethods()) {
132
+                res.add(source.getName());
133
+            }
134
+            return res;
135
+        }
136
+        return res;
137
+    }
138
+
139
+    /** {@inheritDoc} */
140
+    @Override
141
+    public String getName() {
142
+        return "notification";
143
+    }
144
+
145
+    /** {@inheritDoc} */
146
+    @Override
147
+    public boolean showInHelp() {
148
+        return true;
149
+    }
150
+
151
+    /** {@inheritDoc} */
152
+    @Override
153
+    public String getHelp() {
154
+        return "notification [--methods|--method <method>] text - "
155
+                + "Notifies you of the text";
156
+    }
157
+
158
+    /** {@inheritDoc} */
159
+    @Override
160
+    public CommandType getType() {
161
+        return CommandType.TYPE_GLOBAL;
162
+    }
163
+}

+ 134
- 0
src/com/dmdirc/addons/notifications/NotificationConfig.java 查看文件

@@ -0,0 +1,134 @@
1
+/*
2
+ * Copyright (c) 2006-2010 Chris Smith, Shane Mc Cormack, Gregory Holmes
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ * of this software and associated documentation files (the "Software"), to deal
6
+ * in the Software without restriction, including without limitation the rights
7
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ * copies of the Software, and to permit persons to whom the Software is
9
+ * furnished to do so, subject to the following conditions:
10
+ *
11
+ * The above copyright notice and this permission notice shall be included in
12
+ * all copies or substantial portions of the Software.
13
+ *
14
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ * SOFTWARE.
21
+ */
22
+
23
+package com.dmdirc.addons.notifications;
24
+
25
+import com.dmdirc.addons.ui_swing.components.reorderablelist.ListReorderButtonPanel;
26
+import com.dmdirc.addons.ui_swing.components.reorderablelist.ReorderableJList;
27
+import com.dmdirc.config.prefs.PreferencesInterface;
28
+
29
+import java.util.Enumeration;
30
+import java.util.LinkedList;
31
+import java.util.List;
32
+
33
+import javax.swing.BorderFactory;
34
+import javax.swing.JLabel;
35
+import javax.swing.JPanel;
36
+import javax.swing.JScrollPane;
37
+import javax.swing.UIManager;
38
+
39
+import net.miginfocom.swing.MigLayout;
40
+
41
+/**
42
+ * Notification method configuration panel.
43
+ */
44
+public class NotificationConfig extends JPanel implements PreferencesInterface {
45
+
46
+    /**
47
+     * A version number for this class. It should be changed whenever the class
48
+     * structure is changed (or anything else that would prevent serialized
49
+     * objects being unserialized with the new class).
50
+     */
51
+    private static final long serialVersionUID = 1;
52
+    /** Notification method order list. */
53
+    private ReorderableJList list;
54
+    /** Notification methods. */
55
+    private final List<String> methods;
56
+    /** The plugin that owns this panel. */
57
+    private final NotificationsPlugin plugin;
58
+
59
+    /**
60
+     * Creates a new instance of NotificationConfig panel.
61
+     *
62
+     * @param plugin The plugin that owns this panel
63
+     * @param methods A list of methods to be used in the panel
64
+     */
65
+    public NotificationConfig(final NotificationsPlugin plugin,
66
+            final List<String> methods) {
67
+        super();
68
+
69
+        if (methods == null) {
70
+            this.methods = new LinkedList<String>();
71
+        } else {
72
+            this.methods = new LinkedList<String>(methods);
73
+        }
74
+        this.plugin = plugin;
75
+
76
+        initComponents();
77
+    }
78
+
79
+    /**
80
+     * Initialises the components.
81
+     */
82
+    private void initComponents() {
83
+        list = new ReorderableJList();
84
+
85
+        for (String method : methods) {
86
+            list.getModel().addElement(method);
87
+        }
88
+
89
+        setLayout(new MigLayout("fillx, ins 0"));
90
+
91
+        JPanel panel = new JPanel();
92
+
93
+        panel.setBorder(BorderFactory.createTitledBorder(UIManager.getBorder(
94
+                "TitledBorder.border"), "Source order"));
95
+        panel.setLayout(new MigLayout("fillx, ins 5"));
96
+
97
+        panel.add(new JLabel("Drag and drop items to reorder"), "wrap");
98
+        panel.add(new JScrollPane(list), "growx, pushx");
99
+        panel.add(new ListReorderButtonPanel(list), "");
100
+
101
+        add(panel, "growx, wrap");
102
+
103
+        panel = new JPanel();
104
+
105
+        panel.setBorder(BorderFactory.createTitledBorder(UIManager.getBorder(
106
+                "TitledBorder.border"), "Output format"));
107
+        panel.setLayout(new MigLayout("fillx, ins 5"));
108
+
109
+        add(panel, "growx, wrap");
110
+    }
111
+
112
+    /**
113
+     * Retrieves the (new) notification method order from this config panel.
114
+     *
115
+     * @return An ordered list of methods
116
+     */
117
+    public List<String> getMethods() {
118
+        final List<String> newMethods = new LinkedList<String>();
119
+
120
+        final Enumeration<?> values = list.getModel().elements();
121
+
122
+        while (values.hasMoreElements()) {
123
+            newMethods.add((String) values.nextElement());
124
+        }
125
+
126
+        return newMethods;
127
+    }
128
+
129
+    /** {@inheritDoc} */
130
+    @Override
131
+    public void save() {
132
+        plugin.saveSettings(getMethods());
133
+    }
134
+}

+ 225
- 0
src/com/dmdirc/addons/notifications/NotificationsPlugin.java 查看文件

@@ -0,0 +1,225 @@
1
+/*
2
+ * Copyright (c) 2006-2010 Chris Smith, Shane Mc Cormack, Gregory Holmes
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ * of this software and associated documentation files (the "Software"), to deal
6
+ * in the Software without restriction, including without limitation the rights
7
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ * copies of the Software, and to permit persons to whom the Software is
9
+ * furnished to do so, subject to the following conditions:
10
+ *
11
+ * The above copyright notice and this permission notice shall be included in
12
+ * all copies or substantial portions of the Software.
13
+ *
14
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ * SOFTWARE.
21
+ */
22
+package com.dmdirc.addons.notifications;
23
+
24
+import com.dmdirc.actions.ActionManager;
25
+import com.dmdirc.actions.CoreActionType;
26
+import com.dmdirc.actions.interfaces.ActionType;
27
+import com.dmdirc.addons.ui_swing.UIUtilities;
28
+import com.dmdirc.commandparser.CommandManager;
29
+import com.dmdirc.config.IdentityManager;
30
+import com.dmdirc.config.prefs.PluginPreferencesCategory;
31
+import com.dmdirc.config.prefs.PreferencesCategory;
32
+import com.dmdirc.config.prefs.PreferencesManager;
33
+import com.dmdirc.interfaces.ActionListener;
34
+import com.dmdirc.plugins.Plugin;
35
+import com.dmdirc.plugins.PluginInfo;
36
+import com.dmdirc.plugins.PluginManager;
37
+import com.dmdirc.util.ReturnableThread;
38
+
39
+import java.util.ArrayList;
40
+import java.util.List;
41
+
42
+/**
43
+ * Notification Manager plugin, aggregates notification sources exposing them
44
+ * via a single command.
45
+ */
46
+public class NotificationsPlugin extends Plugin implements ActionListener {
47
+
48
+    /** The notification methods that we know of. */
49
+    private final List<String> methods = new ArrayList<String>();
50
+    /** The command we're registering. */
51
+    private NotificationCommand command;
52
+    /** The user's preferred order for method usage. */
53
+    private List<String> order;
54
+
55
+    /**
56
+     * Creates a new notifications plugin.
57
+     */
58
+    public NotificationsPlugin() {
59
+        super();
60
+    }
61
+
62
+    /** {@inheritDoc} */
63
+    @Override
64
+    public void onLoad() {
65
+        methods.clear();
66
+        loadSettings();
67
+        ActionManager.addListener(this, CoreActionType.PLUGIN_LOADED,
68
+                CoreActionType.PLUGIN_UNLOADED);
69
+        for (PluginInfo target : PluginManager.getPluginManager()
70
+                .getPluginInfos()) {
71
+            if (target.isLoaded()) {
72
+                addPlugin(target);
73
+            }
74
+        }
75
+        command = new NotificationCommand(this);
76
+        CommandManager.registerCommand(command);
77
+    }
78
+
79
+    /** {@inheritDoc} */
80
+    @Override
81
+    public void onUnload() {
82
+        methods.clear();
83
+        ActionManager.removeListener(this);
84
+        CommandManager.unregisterCommand(command);
85
+    }
86
+
87
+    /** {@inheritDoc} */
88
+    @Override
89
+    public void showConfig(final PreferencesManager manager) {
90
+        final NotificationConfig configPanel = UIUtilities.invokeAndWait(
91
+                new ReturnableThread<NotificationConfig>() {
92
+
93
+            /** {@inheritDoc} */
94
+            @Override
95
+            public void run() {
96
+                setObject(new NotificationConfig(NotificationsPlugin.this,
97
+                        order));
98
+            }
99
+        });
100
+
101
+        final PreferencesCategory category = new PluginPreferencesCategory(
102
+                getPluginInfo(), "Notifications", "", "category-notifications",
103
+                configPanel);
104
+        manager.getCategory("Plugins").addSubCategory(category);
105
+    }
106
+
107
+    /** Loads the plugins settings. */
108
+    private void loadSettings() {
109
+        if (IdentityManager.getGlobalConfig().hasOptionString(getDomain(),
110
+                "methodOrder")) {
111
+            order = IdentityManager.getGlobalConfig().getOptionList(
112
+                    getDomain(), "methodOrder");
113
+        } else {
114
+            order = new ArrayList<String>();
115
+        }
116
+    }
117
+
118
+    /** {@inheritDoc} */
119
+    @Override
120
+    public void processEvent(final ActionType type, final StringBuffer format,
121
+            final Object... arguments) {
122
+        if (type == CoreActionType.PLUGIN_LOADED) {
123
+            addPlugin((PluginInfo) arguments[0]);
124
+        } else if (type == CoreActionType.PLUGIN_UNLOADED) {
125
+            removePlugin((PluginInfo) arguments[0]);
126
+        }
127
+    }
128
+
129
+    /**
130
+     * Checks to see if a plugin implements the notification method interface
131
+     * and if it does, adds the method to our list.
132
+     *
133
+     * @param target The plugin to be tested
134
+     */
135
+    private void addPlugin(final PluginInfo target) {
136
+        if (target.hasExportedService("showNotification")) {
137
+            methods.add(target.getName());
138
+            addMethodToOrder(target);
139
+        }
140
+    }
141
+
142
+    /**
143
+     * Checks to see if the specified notification method needs to be added to
144
+     * our order list, and adds it if neccessary.
145
+     *
146
+     * @param source The notification method to be tested
147
+     */
148
+    private void addMethodToOrder(final PluginInfo source) {
149
+        if (!order.contains(source.getName())) {
150
+            order.add(source.getName());
151
+        }
152
+    }
153
+
154
+    /**
155
+     * Checks to see if a plugin implements the notification method interface
156
+     * and if it does, removes the method from our list.
157
+     *
158
+     * @param target The plugin to be tested
159
+     */
160
+    private void removePlugin(final PluginInfo target) {
161
+        methods.remove(target.getName());
162
+    }
163
+
164
+    /**
165
+     * Retrieves a method based on its name.
166
+     *
167
+     * @param name The name to search for
168
+     * @return The method with the specified name or null if none were found.
169
+     */
170
+    public PluginInfo getMethod(final String name) {
171
+        return PluginManager.getPluginManager().getPluginInfoByName(name);
172
+    }
173
+
174
+    /**
175
+     * Retrieves all the methods registered with this plugin.
176
+     *
177
+     * @return All known notification sources
178
+     */
179
+    public List<PluginInfo> getMethods() {
180
+        final List<PluginInfo> plugins = new ArrayList<PluginInfo>();
181
+        for (String method : methods) {
182
+            plugins.add(PluginManager.getPluginManager()
183
+                    .getPluginInfoByName(method));
184
+        }
185
+        return plugins;
186
+    }
187
+
188
+    /**
189
+     * Does this plugin have any active notification methods?
190
+     *
191
+     * @return true iif active notification methods are registered
192
+     */
193
+    public boolean hasActiveMethod() {
194
+        return !methods.isEmpty();
195
+    }
196
+
197
+    /**
198
+     * Returns the user's preferred method if loaded, or null if none loaded.
199
+     *
200
+     * @return Preferred notification method
201
+     */
202
+    public PluginInfo getPreferredMethod() {
203
+        if (methods.isEmpty()) {
204
+            return null;
205
+        }
206
+        for (String method : order) {
207
+            if (methods.contains(method)) {
208
+                return PluginManager.getPluginManager().getPluginInfoByName(
209
+                    method);
210
+            }
211
+        }
212
+        return null;
213
+    }
214
+
215
+    /**
216
+     * Saves the plugins settings.
217
+     *
218
+     * @param newOrder The new order for methods
219
+     */
220
+    protected void saveSettings(final List<String> newOrder) {
221
+        order = newOrder;
222
+        IdentityManager.getConfigIdentity().setOption(getDomain(),
223
+                "methodOrder", order);
224
+    }
225
+}

+ 30
- 0
src/com/dmdirc/addons/notifications/plugin.config 查看文件

@@ -0,0 +1,30 @@
1
+# This is a DMDirc configuration file.
2
+
3
+# This section indicates which sections below take key/value
4
+# pairs, rather than a simple list. It should be placed above
5
+# any sections that take key/values.
6
+keysections:
7
+  metadata
8
+  version
9
+  requires
10
+  defaults
11
+
12
+metadata:
13
+  author=Greg <greg@dmdirc.com>
14
+  mainclass=com.dmdirc.addons.notifications.NotificationsPlugin
15
+  description=Provides a notification command exposing notifications plugins centrally.
16
+  name=notifications
17
+  nicename=Notification Manager
18
+
19
+version:
20
+  friendly=0.1
21
+
22
+requires:
23
+  parent=ui_swing
24
+
25
+required-services:
26
+  swing ui
27
+
28
+provides:
29
+  notification command
30
+  notification manager

+ 11
- 0
src/com/dmdirc/addons/osd/OsdPlugin.java 查看文件

@@ -195,4 +195,15 @@ public final class OsdPlugin extends Plugin implements CategoryChangeListener,
195 195
         }
196 196
     }
197 197
 
198
+    /**
199
+     * Shows an OSD with the specified message, title is ignored, exported
200
+     * method used for showNotification.
201
+     *
202
+     * @param title Ignored
203
+     * @param message Message to show
204
+     */
205
+    public void showOSD(final String title, final String message) {
206
+        osdManager.showWindow(message);
207
+    }
208
+
198 209
 }

+ 1
- 1
src/com/dmdirc/addons/osd/plugin.config 查看文件

@@ -45,7 +45,7 @@ defaults:
45 45
   maxWindows=false:5
46 46
 
47 47
 exports:
48
-  showOSD in com.dmdirc.addons.osd.OsdCommand as showNotification
48
+  showOSD in com.dmdirc.addons.osd.OsdPlugin as showNotification
49 49
 
50 50
 icons:
51 51
   category-osd=plugin://osd:com/dmdirc/addons/osd/icon.png

+ 11
- 0
src/com/dmdirc/addons/systray/SystrayPlugin.java 查看文件

@@ -103,6 +103,17 @@ public final class SystrayPlugin extends Plugin implements ActionListener,
103 103
         notify(title, message, TrayIcon.MessageType.NONE);
104 104
     }
105 105
 
106
+    /**
107
+     * Proxy method for notify, this method is used for the exported command to
108
+     * avoid ambiguity when performing reflection.
109
+     *
110
+     * @param title Title for the notification
111
+     * @param message Text for the notification
112
+     */
113
+    public void showPopup(final String title, final String message) {
114
+        notify(title, message);
115
+    }
116
+
106 117
     /**
107 118
      * {@inheritDoc}
108 119
      *

+ 1
- 1
src/com/dmdirc/addons/systray/plugin.config 查看文件

@@ -36,4 +36,4 @@ defaults:
36 36
   autominimise=false
37 37
 
38 38
 exports:
39
-  showPopup in com.dmdirc.addons.systray.SystrayPlugin.PopupCommand as showNotification
39
+  showPopup in com.dmdirc.addons.systray.SystrayPlugin as showNotification

Loading…
取消
儲存