├── .gitignore
├── LICENSE
├── README.md
├── extension.js
├── icons
├── clipboard-symbolic.svg
└── private-mode-symbolic.svg
├── libs
├── preferences.js
├── qrcodegen.js
├── storage.js
├── utils.js
└── validator.js
├── metadata.json
├── pack.sh
├── po.sh
├── po
├── cs.po
├── de.po
├── example.pot
├── fa_IR.po
├── it.po
├── nl.po
├── pl.po
└── uk.po
├── prefs.js
├── schemas
└── org.gnome.shell.extensions.clipman.gschema.xml
└── stylesheet.css
/.gitignore:
--------------------------------------------------------------------------------
1 | *.compiled
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Eugene Popov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Clipman
2 |
3 | The main goal of developing the Clipman was to create a simple clipboard manager extension for GNOME that isn't overloaded with features and settings. Unfortunately, the current analogues of this type of extensions have redundant functionality, while not providing the functionality I need.
4 |
5 | | Shortcut | Action |
6 | | ----------------- |------------------------------------------------------ |
7 | | `Space` / `Enter` | Activate the selected entry |
8 | | `*` | Pin/unpin the selected entry |
9 | | `Delete` | Delete the selected entry |
10 | | `UpArrow` | Select the entry above |
11 | | `DownArrow` | Select the entry below |
12 | | `RightArrow` | Open the actions submenu |
13 | | `LeftArrow` | Close the actions submenu |
14 | | `/` | Move keyboard focus to the search field |
15 | | `Escape` | Clear the search field (if focused) or close the menu |
16 |
17 | ### Installation
18 |
19 | To install manually follow the steps below:
20 |
21 | - download the latest version of the extension from the [releases page](https://github.com/popov895/Clipman/releases)
22 | - run the following command:
23 |
24 | `$ gnome-extensions install clipman@popov895.ukr.net.zip`
25 |
26 | - restart your session (logout and login)
27 | - run the following command:
28 |
29 | `$ gnome-extensions enable clipman@popov895.ukr.net`
30 |
31 | This extension uses the following libraries:
32 |
33 | - [QR Code generator library](https://github.com/nayuki/QR-Code-generator)
34 | - [validator.js](https://github.com/validatorjs/validator.js/)
35 |
36 | This extension uses [dpaste.com](https://dpaste.com) to share text online.
37 |
38 | ### Support
39 |
40 | [](https://www.buymeacoffee.com/popov895a)
41 |
--------------------------------------------------------------------------------
/extension.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { Clutter, Cogl, Gio, GLib, GObject, Graphene, Meta, Pango, Shell, Soup, St } = imports.gi;
4 |
5 | const Util = imports.misc.util;
6 | const ExtensionUtils = imports.misc.extensionUtils;
7 | const Main = imports.ui.main;
8 | const MessageTray = imports.ui.messageTray;
9 | const ModalDialog = imports.ui.modalDialog;
10 | const PanelMenu = imports.ui.panelMenu;
11 | const PopupMenu = imports.ui.popupMenu;
12 | const SignalTracker = imports.misc.signalTracker;
13 |
14 | const Me = ExtensionUtils.getCurrentExtension();
15 | const { QrCode } = Me.imports.libs.qrcodegen.qrcodegen;
16 | const { Preferences } = Me.imports.libs.preferences;
17 | const { Storage } = Me.imports.libs.storage;
18 | const Validator = Me.imports.libs.validator.validator;
19 | const { _, log, ColorParser, SearchEngines } = Me.imports.libs.utils;
20 |
21 | const ClipboardManager = GObject.registerClass({
22 | Signals: {
23 | 'changed': {},
24 | 'destroy': {},
25 | },
26 | }, class ClipboardManager extends GObject.Object {
27 | constructor() {
28 | super();
29 |
30 | this._sensitiveMimeTypes = [
31 | `x-kde-passwordManagerHint`,
32 | ];
33 |
34 | this._clipboard = St.Clipboard.get_default();
35 | this._selection = global.get_display().get_selection();
36 | this._selection.connectObject(
37 | `owner-changed`,
38 | (...[, selectionType]) => {
39 | if (!this.blockSignals && selectionType === Meta.SelectionType.SELECTION_CLIPBOARD) {
40 | this.emit(`changed`);
41 | }
42 | },
43 | this
44 | );
45 | }
46 |
47 | destroy() {
48 | this.emit(`destroy`);
49 | }
50 |
51 | getText() {
52 | const mimeTypes = this._clipboard.get_mimetypes(St.ClipboardType.CLIPBOARD);
53 | const hasSensitiveMimeTypes = this._sensitiveMimeTypes.some((sensitiveMimeType) => {
54 | return mimeTypes.includes(sensitiveMimeType);
55 | });
56 | if (hasSensitiveMimeTypes) {
57 | return Promise.resolve(null);
58 | }
59 |
60 | return new Promise((resolve) => {
61 | this._clipboard.get_text(St.ClipboardType.CLIPBOARD, (...[, text]) => {
62 | resolve(text);
63 | });
64 | });
65 | }
66 |
67 | setText(text) {
68 | this._clipboard.set_text(St.ClipboardType.CLIPBOARD, text);
69 | }
70 |
71 | clear() {
72 | this._clipboard.set_content(St.ClipboardType.CLIPBOARD, ``, new GLib.Bytes(null));
73 | }
74 | });
75 |
76 | const PlaceholderMenuItem = class extends PopupMenu.PopupMenuSection {
77 | constructor(text, icon) {
78 | super();
79 |
80 | this.actor.add_style_class_name(`popup-menu-item`);
81 |
82 | const boxLayout = new St.BoxLayout({
83 | style_class: `clipman-placeholderpanel`,
84 | vertical: true,
85 | x_expand: true,
86 | });
87 | boxLayout.add(new St.Icon({
88 | gicon: icon,
89 | x_align: Clutter.ActorAlign.CENTER,
90 | }));
91 | boxLayout.add(new St.Label({
92 | text: text,
93 | x_align: Clutter.ActorAlign.CENTER,
94 | }));
95 | this.actor.add(boxLayout);
96 | }
97 | };
98 |
99 | const QrCodeDialog = GObject.registerClass(
100 | class QrCodeDialog extends ModalDialog.ModalDialog {
101 | constructor(text) {
102 | super();
103 |
104 | const image = this._generateQrCodeImage(text);
105 | if (image) {
106 | this.contentLayout.add_child(new St.Icon({
107 | gicon: image,
108 | icon_size: image.preferred_width,
109 | }));
110 | } else {
111 | this.contentLayout.add_child(new St.Label({
112 | text: _(`Failed to generate QR code`),
113 | }));
114 | }
115 |
116 | this.addButton({
117 | isDefault: true,
118 | key: Clutter.KEY_Escape,
119 | label: _(`Close`, `Close dialog`),
120 | action: () => {
121 | this.close();
122 | },
123 | });
124 | }
125 |
126 | _generateQrCodeImage(text) {
127 | let image;
128 | try {
129 | const bytesPerPixel = 3; // RGB
130 | const scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
131 | const minPixelsPerModule = 3;
132 | const maxPixelsPerModule = 10;
133 | const maxQuietZoneSize = 4 * maxPixelsPerModule;
134 | const maxIconSize = Math.round(Math.min(global.screen_width, global.screen_height) * 0.9 / scaleFactor);
135 | const qrCode = QrCode.encodeText(text, QrCode.Ecc.MEDIUM);
136 | const pixelsPerModule = Math.min(
137 | Math.round((maxIconSize - 2 * maxQuietZoneSize) / qrCode.size),
138 | maxPixelsPerModule
139 | );
140 | if (pixelsPerModule < minPixelsPerModule) {
141 | throw new Error(`QR code is too large`);
142 | }
143 | const quietZoneSize = Math.min(4 * pixelsPerModule, maxQuietZoneSize);
144 | const iconSize = qrCode.size * pixelsPerModule + 2 * quietZoneSize;
145 | const data = new Uint8Array(iconSize * iconSize * pixelsPerModule * bytesPerPixel);
146 | data.fill(255);
147 | for (let qrCodeY = 0; qrCodeY < qrCode.size; ++qrCodeY) {
148 | for (let i = 0; i < pixelsPerModule; ++i) {
149 | const dataY = quietZoneSize + qrCodeY * pixelsPerModule + i;
150 | for (let qrCodeX = 0; qrCodeX < qrCode.size; ++qrCodeX) {
151 | const color = qrCode.getModule(qrCodeX, qrCodeY) ? 0x00 : 0xff;
152 | for (let j = 0; j < pixelsPerModule; ++j) {
153 | const dataX = quietZoneSize + qrCodeX * pixelsPerModule + j;
154 | const dataI = iconSize * bytesPerPixel * dataY + bytesPerPixel * dataX;
155 | data[dataI] = color; // R
156 | data[dataI + 1] = color; // G
157 | data[dataI + 2] = color; // B
158 | }
159 | }
160 | }
161 | }
162 |
163 | image = new St.ImageContent({
164 | preferred_height: iconSize,
165 | preferred_width: iconSize,
166 | });
167 | image.set_bytes(
168 | new GLib.Bytes(data),
169 | Cogl.PixelFormat.RGB_888,
170 | iconSize,
171 | iconSize,
172 | iconSize * bytesPerPixel
173 | );
174 | } catch (error) {
175 | log(error);
176 | }
177 |
178 | return image;
179 | }
180 | });
181 |
182 | const HistoryMenuSection = class extends PopupMenu.PopupMenuSection {
183 | constructor() {
184 | super();
185 |
186 | const clearIcon = new St.Icon({
187 | icon_name: `edit-clear-symbolic`,
188 | style_class: `popup-menu-icon`,
189 | });
190 | this.entry = new St.Entry({
191 | can_focus: true,
192 | hint_text: _(`Type to search...`),
193 | secondary_icon: clearIcon,
194 | style_class: `clipman-popupsearchmenuitem`,
195 | x_expand: true,
196 | });
197 | this.entry.bind_property_full(
198 | `text`,
199 | clearIcon,
200 | `visible`,
201 | GObject.BindingFlags.DEFAULT | GObject.BindingFlags.SYNC_CREATE,
202 | () => {
203 | return [
204 | true,
205 | this.entry.text.length > 0,
206 | ];
207 | },
208 | null
209 | );
210 | this.entry.connectObject(
211 | `secondary-icon-clicked`, () => {
212 | this.entry.text = ``;
213 | },
214 | `notify::text`, this._onEntryTextChanged.bind(this)
215 | );
216 | const searchMenuItem = new PopupMenu.PopupBaseMenuItem({
217 | can_focus: false,
218 | reactive: false,
219 | style_class: `clipman-searchmenuitem`,
220 | });
221 | searchMenuItem._ornamentLabel.visible = false;
222 | searchMenuItem.add(this.entry);
223 | this.addMenuItem(searchMenuItem);
224 |
225 | const placeholderBoxLayout = new St.BoxLayout({
226 | vertical: true,
227 | x_expand: true,
228 | });
229 | placeholderBoxLayout.add(new St.Label({
230 | text: _(`No Matches`),
231 | x_align: Clutter.ActorAlign.CENTER,
232 | }));
233 | this._placeholderMenuItem = new PopupMenu.PopupMenuSection({
234 | reactive: false,
235 | });
236 | this._placeholderMenuItem.actor.style_class = `popup-menu-item`;
237 | this._placeholderMenuItem.actor.visible = false;
238 | this._placeholderMenuItem.actor.add(placeholderBoxLayout);
239 | this.addMenuItem(this._placeholderMenuItem);
240 |
241 | this.section = new PopupMenu.PopupMenuSection();
242 | this.section.moveMenuItem = (menuItem, position) => {
243 | Object.getPrototypeOf(this.section).moveMenuItem.call(this.section, menuItem, position);
244 | if (menuItem instanceof PopupMenu.PopupSubMenuMenuItem) {
245 | this.section.box.set_child_above_sibling(menuItem.menu.actor, menuItem.actor);
246 | }
247 | };
248 | this.section.box.connectObject(
249 | `actor-added`, (...[, actor]) => {
250 | if (actor instanceof HistoryMenuItem) {
251 | this._onMenuItemAdded(actor);
252 | }
253 | },
254 | `actor-removed`, (...[, actor]) => {
255 | if (actor instanceof HistoryMenuItem) {
256 | this._onMenuItemRemoved();
257 | }
258 | }
259 | );
260 | this.scrollView = new St.ScrollView({
261 | hscrollbar_policy: St.PolicyType.NEVER,
262 | vscrollbar_policy: St.PolicyType.EXTERNAL,
263 | });
264 | this.scrollView.add_actor(this.section.actor);
265 | this.scrollView.vscroll.adjustment.connectObject(`changed`, () => {
266 | Promise.resolve().then(() => {
267 | if (Math.floor(this.scrollView.vscroll.adjustment.upper) > this.scrollView.vscroll.adjustment.page_size) {
268 | this.scrollView.vscrollbar_policy = St.PolicyType.ALWAYS;
269 | } else {
270 | this.scrollView.vscrollbar_policy = St.PolicyType.EXTERNAL;
271 | }
272 | });
273 | });
274 | const menuSection = new PopupMenu.PopupMenuSection();
275 | menuSection.actor.add_actor(this.scrollView);
276 | this.addMenuItem(menuSection);
277 | }
278 |
279 | _onEntryTextChanged() {
280 | const searchText = this.entry.text.toLowerCase();
281 | const menuItems = this.section._getMenuItems();
282 | menuItems.forEach((menuItem) => {
283 | menuItem.actor.visible = menuItem.text.toLowerCase().includes(searchText);
284 | if (!menuItem.actor.visible) {
285 | menuItem.menu.close();
286 | }
287 | });
288 |
289 | if (searchText.length === 0) {
290 | this._placeholderMenuItem.actor.visible = false;
291 | } else {
292 | const hasVisibleMenuItems = menuItems.some((menuItem) => {
293 | return menuItem.actor.visible;
294 | });
295 | this._placeholderMenuItem.actor.visible = !hasVisibleMenuItems;
296 | }
297 | }
298 |
299 | _onMenuItemAdded(menuItem) {
300 | const searchText = this.entry.text.toLowerCase();
301 | if (searchText.length > 0) {
302 | menuItem.actor.visible = menuItem.text.toLowerCase().includes(searchText);
303 | if (menuItem.actor.visible) {
304 | this._placeholderMenuItem.actor.visible = false;
305 | }
306 | }
307 | menuItem.connectObject(`key-focus-in`, () => {
308 | const event = Clutter.get_current_event();
309 | if (event && event.type() === Clutter.EventType.KEY_PRESS) {
310 | Util.ensureActorVisibleInScrollView(this.scrollView, menuItem);
311 | }
312 | });
313 | }
314 |
315 | _onMenuItemRemoved() {
316 | const searchText = this.entry.text.toLowerCase();
317 | if (searchText.length > 0) {
318 | const menuItems = this.section._getMenuItems();
319 | const hasVisibleMenuItems = menuItems.some((menuItem) => {
320 | return menuItem.actor.visible;
321 | });
322 | this._placeholderMenuItem.actor.visible = !hasVisibleMenuItems;
323 | }
324 | }
325 | };
326 |
327 | const HistoryMenuItem = GObject.registerClass({
328 | Properties: {
329 | 'showSurroundingWhitespace': GObject.ParamSpec.boolean(
330 | `showSurroundingWhitespace`, ``, ``,
331 | GObject.ParamFlags.WRITABLE,
332 | true
333 | ),
334 | 'showColorPreview': GObject.ParamSpec.boolean(
335 | `showColorPreview`, ``, ``,
336 | GObject.ParamFlags.WRITABLE,
337 | true
338 | ),
339 | },
340 | Signals: {
341 | 'delete': {},
342 | 'pinned': {},
343 | 'submenuAboutToOpen': {},
344 | },
345 | }, class HistoryMenuItem extends PopupMenu.PopupSubMenuMenuItem {
346 | constructor(text, pinned = false, topMenu) {
347 | super(``);
348 |
349 | this.text = text;
350 | this.label.clutter_text.ellipsize = Pango.EllipsizeMode.END;
351 | this.menu.actor.enable_mouse_scrolling = false;
352 |
353 | this.setOrnament(PopupMenu.Ornament.NONE);
354 |
355 | this.menu.open = (animate) => {
356 | if (!this.menu.isOpen) {
357 | this.emit(`submenuAboutToOpen`);
358 | Object.getPrototypeOf(this.menu).open.call(this.menu, animate);
359 | }
360 | };
361 |
362 | this._topMenu = topMenu;
363 | this._topMenu.connectObject(`open-state-changed`, (...[, open]) => {
364 | if (!open) {
365 | this.menu.close();
366 | }
367 | });
368 |
369 | this.add_child(new St.Bin({
370 | style_class: `popup-menu-item-expander`,
371 | x_expand: true,
372 | }));
373 |
374 | const pinIcon = new St.Icon({
375 | style_class: `system-status-icon`,
376 | });
377 | this._pinButton = new St.Button({
378 | checked: pinned,
379 | can_focus: true,
380 | child: pinIcon,
381 | style_class: `clipman-toolbutton`,
382 | toggle_mode: true,
383 | });
384 | this._pinButton.bind_property_full(
385 | `checked`,
386 | pinIcon,
387 | `icon_name`,
388 | GObject.BindingFlags.DEFAULT | GObject.BindingFlags.SYNC_CREATE,
389 | () => {
390 | return [
391 | true,
392 | this._pinButton.checked ? `starred-symbolic` : `non-starred-symbolic`,
393 | ];
394 | },
395 | null
396 | );
397 | this._pinButton.connectObject(`notify::checked`, () => {
398 | this.emit(`pinned`);
399 | });
400 |
401 | const deleteButton = new St.Button({
402 | can_focus: true,
403 | child: new St.Icon({
404 | icon_name: `edit-delete-symbolic`,
405 | style_class: `system-status-icon`,
406 | }),
407 | style_class: `clipman-toolbutton`,
408 | });
409 | deleteButton.connectObject(`clicked`, () => {
410 | this.emit(`delete`);
411 | });
412 |
413 | this._triangleBin.hide();
414 |
415 | this.menu._arrow = new St.Icon({
416 | icon_name: `pan-end-symbolic`,
417 | pivot_point: new Graphene.Point({ x: 0.5, y: 0.6 }),
418 | style_class: `system-status-icon`,
419 | });
420 | const toggleSubMenuButton = new St.Button({
421 | can_focus: true,
422 | child: this.menu._arrow,
423 | style_class: `clipman-toolbutton`,
424 | });
425 | toggleSubMenuButton.connectObject(`clicked`, () => {
426 | this.menu.toggle();
427 | });
428 |
429 | const boxLayout = new St.BoxLayout({
430 | style_class: `clipman-toolbuttonnpanel`,
431 | });
432 | boxLayout.add(this._pinButton);
433 | boxLayout.add(deleteButton);
434 | boxLayout.add(toggleSubMenuButton);
435 | this.add_child(boxLayout);
436 |
437 | const clickAction = new Clutter.ClickAction({
438 | enabled: this._activatable,
439 | });
440 | clickAction.connectObject(`clicked`, () => {
441 | this.activate(Clutter.get_current_event());
442 | });
443 | clickAction.connectObject(`notify::pressed`, () => {
444 | if (clickAction.pressed) {
445 | this.add_style_pseudo_class(`active`);
446 | } else {
447 | this.remove_style_pseudo_class(`active`);
448 | }
449 | });
450 | this.add_action(clickAction);
451 | }
452 |
453 | get pinned() {
454 | return this._pinButton.checked;
455 | }
456 |
457 | set showSurroundingWhitespace(showSurroundingWhitespace) {
458 | let text = this.text;
459 | if (!showSurroundingWhitespace) {
460 | text = text.trim();
461 | }
462 | text = text.substring(0, 2000); // TODO : Use max width of the top menu
463 | if (showSurroundingWhitespace) {
464 | text = GLib.markup_escape_text(text, -1);
465 | text = text.replaceAll(/^\s+|\s+$/g, (match1) => {
466 | [[/ +/g, `␣`], [/\t+/g, `⇥`], [/\n+/g, `↵`]].forEach(([regExp, str]) => {
467 | match1 = match1.replaceAll(regExp, (match2) => {
468 | return `${str.repeat(match2.length)}`;
469 | });
470 | });
471 | return match1;
472 | });
473 | }
474 | text = text.replaceAll(/\s+/g, ` `);
475 | if (showSurroundingWhitespace) {
476 | this.label.clutter_text.set_markup(text);
477 | } else {
478 | this.label.text = text;
479 | }
480 | }
481 |
482 | set showColorPreview(showColorPreview) {
483 | if (showColorPreview) {
484 | // use lazy loadiing for color preview
485 | if (this._colorPreview === undefined) {
486 | this._colorPreview = this._generateColorPreview(this.text) ?? null;
487 | if (this._colorPreview) {
488 | this.colorPreviewIcon = new St.Icon({
489 | gicon: this._colorPreview,
490 | style_class: `clipman-colorpreview`,
491 | });
492 | this.insert_child_at_index(this.colorPreviewIcon, 1);
493 | }
494 | }
495 | this.colorPreviewIcon?.show();
496 | } else {
497 | this.colorPreviewIcon?.hide();
498 | }
499 | }
500 |
501 | _getTopMenu() {
502 | return this._topMenu;
503 | }
504 |
505 | _generateColorPreview(color) {
506 | let image;
507 | try {
508 | const rgba = ColorParser.parse(color);
509 | if (rgba) {
510 | const bytesPerPixel = 4; // RGBA
511 | const iconSize = 16;
512 | const data = new Uint8Array(iconSize * iconSize * bytesPerPixel);
513 | for (let y = 0; y < iconSize; ++y) {
514 | for (let x = 0; x < iconSize; ++x) {
515 | const i = iconSize * bytesPerPixel * y + bytesPerPixel * x;
516 | data[i] = rgba[0]; // R
517 | data[i + 1] = rgba[1]; // G
518 | data[i + 2] = rgba[2]; // B
519 | data[i + 3] = rgba[3] ?? 0xff; // A
520 | }
521 | }
522 |
523 | image = new St.ImageContent({
524 | preferred_height: iconSize,
525 | preferred_width: iconSize,
526 | });
527 | image.set_bytes(
528 | new GLib.Bytes(data),
529 | Cogl.PixelFormat.RGBA_8888,
530 | iconSize,
531 | iconSize,
532 | iconSize * bytesPerPixel
533 | );
534 | }
535 | } catch (error) {
536 | log(error);
537 | }
538 |
539 | return image;
540 | }
541 |
542 | activate(event) {
543 | this.emit(`activate`, event);
544 | }
545 |
546 | vfunc_key_press_event(event) {
547 | switch (event.keyval) {
548 | case Clutter.KEY_space:
549 | case Clutter.KEY_Return:
550 | case Clutter.KEY_KP_Enter: {
551 | this.activate(Clutter.get_current_event());
552 | return Clutter.EVENT_STOP;
553 | }
554 | case Clutter.KEY_Delete:
555 | case Clutter.KEY_KP_Delete: {
556 | this.emit(`delete`);
557 | return Clutter.EVENT_STOP;
558 | }
559 | case Clutter.KEY_asterisk:
560 | case Clutter.KEY_KP_Multiply: {
561 | this._pinButton.checked = !this._pinButton.checked;
562 | return Clutter.EVENT_STOP;
563 | }
564 | default:
565 | break;
566 | }
567 |
568 | return super.vfunc_key_press_event(event);
569 | }
570 |
571 | vfunc_button_press_event() {
572 | return Clutter.EVENT_PROPAGATE;
573 | }
574 |
575 | vfunc_button_release_event() {
576 | return Clutter.EVENT_PROPAGATE;
577 | }
578 |
579 | vfunc_touch_event() {
580 | return Clutter.EVENT_PROPAGATE;
581 | }
582 | });
583 |
584 | const HistoryKeepingMode = {
585 | None: 0,
586 | Pinned: 1,
587 | All: 2,
588 | };
589 |
590 | const PanelIndicator = GObject.registerClass(
591 | class PanelIndicator extends PanelMenu.Button {
592 | constructor() {
593 | super(0.5);
594 |
595 | this._lastUsedId = -1;
596 | this._lastUsedSortKey = -1;
597 | this._pinnedCount = 0;
598 |
599 | this._buildIcon();
600 | this._buildMenu();
601 |
602 | this.menu.actor.connectObject(`captured-event`, (...[, event]) => {
603 | if (event.type() === Clutter.EventType.KEY_PRESS) {
604 | switch (event.get_key_symbol()) {
605 | case Clutter.KEY_Escape: {
606 | if (
607 | this._historyMenuSection.entry.clutter_text.has_key_focus() &&
608 | this._historyMenuSection.entry.text.length > 0
609 | ) {
610 | this._historyMenuSection.entry.text = ``;
611 | return Clutter.EVENT_STOP;
612 | }
613 | break;
614 | }
615 | case Clutter.KEY_slash: {
616 | if (this._historyMenuSection.entry.get_paint_visibility()) {
617 | global.stage.set_key_focus(this._historyMenuSection.entry);
618 | this._historyMenuSection.entry.clutter_text.set_selection(-1, 0);
619 | return Clutter.EVENT_STOP;
620 | }
621 | break;
622 | }
623 | default:
624 | break;
625 | }
626 | }
627 |
628 | return Clutter.EVENT_PROPAGATE;
629 | });
630 |
631 | this._preferences = new Preferences();
632 | this._preferences.previousHistoryKeepingMode = this._preferences.historyKeepingMode;
633 | this._preferences.connectObject(
634 | `historySizeChanged`, this._onHistorySizeChanged.bind(this),
635 | `historyKeepingModeChanged`, this._onHistoryKeepingModeChanged.bind(this)
636 | );
637 |
638 | this._clipboard = new ClipboardManager();
639 | this._clipboard.connectObject(`changed`, () => {
640 | if (!this._privateModeMenuItem.state) {
641 | this._clipboard.getText().then((text) => {
642 | this._onClipboardTextChanged(text);
643 | });
644 | }
645 | });
646 |
647 | this._storage = new Storage();
648 |
649 | this._addKeybindings();
650 | this._loadState();
651 | this._updateMenuLayout();
652 | }
653 |
654 | destroy() {
655 | this._qrCodeDialog?.close();
656 |
657 | this._saveState();
658 | this._removeKeybindings();
659 |
660 | this._preferences.destroy();
661 | this._clipboard.destroy();
662 |
663 | super.destroy();
664 | }
665 |
666 | _buildIcon() {
667 | const mainIcon = new St.Icon({
668 | icon_name: `edit-paste-symbolic`,
669 | style_class: `system-status-icon`,
670 | });
671 | this._privateModeIcon = new St.Icon({
672 | icon_name: `view-conceal-symbolic`,
673 | style_class: `system-status-icon`,
674 | visible: false,
675 | });
676 | const boxLayout = new St.BoxLayout();
677 | boxLayout.add_child(mainIcon);
678 | boxLayout.add_child(this._privateModeIcon);
679 | this.add_child(boxLayout);
680 | }
681 |
682 | _buildMenu() {
683 | this._privateModePlaceholder = new PlaceholderMenuItem(
684 | _(`Private Mode is On`),
685 | Gio.icon_new_for_string(`${Me.path}/icons/private-mode-symbolic.svg`)
686 | );
687 | this.menu.addMenuItem(this._privateModePlaceholder);
688 |
689 | this._emptyPlaceholder = new PlaceholderMenuItem(
690 | _(`History is Empty`),
691 | Gio.icon_new_for_string(`${Me.path}/icons/clipboard-symbolic.svg`)
692 | );
693 | this.menu.addMenuItem(this._emptyPlaceholder);
694 |
695 | this._historyMenuSection = new HistoryMenuSection();
696 | this._historyMenuSection.section.box.connectObject(
697 | `actor-added`, (...[, actor]) => {
698 | if (actor instanceof HistoryMenuItem) {
699 | this._updateMenuLayout();
700 | }
701 | },
702 | `actor-removed`, (...[, actor]) => {
703 | if (actor instanceof HistoryMenuItem) {
704 | this._updateMenuLayout();
705 | }
706 | }
707 | );
708 | this.menu.addMenuItem(this._historyMenuSection);
709 |
710 | this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
711 |
712 | this._clearMenuItem = this.menu.addAction(_(`Clear History`), () => {
713 | this.menu.close();
714 | const menuItems = this._historyMenuSection.section._getMenuItems();
715 | const menuItemsToRemove = menuItems.slice(this._pinnedCount);
716 | if (menuItemsToRemove.length > 0) {
717 | menuItemsToRemove.forEach((menuItem) => {
718 | this._destroyMenuItem(menuItem);
719 | });
720 | this._saveHistory();
721 | }
722 | });
723 |
724 | this._privateModeMenuItem = new PopupMenu.PopupSwitchMenuItem(_(`Private Mode`), false, {
725 | reactive: true,
726 | });
727 | this._privateModeMenuItem.connectObject(`toggled`, (...[, state]) => {
728 | this.menu.close();
729 | if (!state) {
730 | this._updateCurrentItem();
731 | }
732 | this._updateMenuLayout();
733 | });
734 | this.menu.addMenuItem(this._privateModeMenuItem);
735 |
736 | this.menu.addAction(_(`Settings`, `Open settings`), () => {
737 | ExtensionUtils.openPrefs();
738 | });
739 | }
740 |
741 | _createMenuItem(text, pinned, id = ++this._lastUsedId, sortKey = ++this._lastUsedSortKey) {
742 | const menuItem = new HistoryMenuItem(text, pinned, this.menu);
743 | menuItem.id = id;
744 | menuItem.sortKey = sortKey;
745 | this._preferences.bind(
746 | this._preferences._keyShowSurroundingWhitespace,
747 | menuItem,
748 | `showSurroundingWhitespace`,
749 | Gio.SettingsBindFlags.GET
750 | );
751 | this._preferences.bind(
752 | this._preferences._keyShowColorPreview,
753 | menuItem,
754 | `showColorPreview`,
755 | Gio.SettingsBindFlags.GET
756 | );
757 | menuItem.connectObject(
758 | `activate`, () => {
759 | this.menu.close();
760 | this._clipboard.setText(menuItem.text);
761 | },
762 | `submenuAboutToOpen`, () => {
763 | this._ensureSubMenuPopulated(menuItem);
764 | },
765 | `pinned`, this._onMenuItemPinned.bind(this),
766 | `delete`, () => {
767 | if (this._historyMenuSection.section.numMenuItems === 1) {
768 | this.menu.close();
769 | }
770 | this._destroyMenuItem(menuItem);
771 | this._saveHistory();
772 | },
773 | `destroy`, () => {
774 | Gio.Settings.unbind(menuItem, `showColorPreview`);
775 | Gio.Settings.unbind(menuItem, `showSurroundingWhitespace`);
776 | if (this._currentMenuItem === menuItem) {
777 | delete this._currentMenuItem;
778 | }
779 | }
780 | );
781 |
782 | return menuItem;
783 | }
784 |
785 | _destroyMenuItem(menuItem) {
786 | if (this._currentMenuItem === menuItem) {
787 | this._clipboard.clear();
788 | }
789 | if (menuItem.pinned) {
790 | --this._pinnedCount;
791 | }
792 | if (menuItem.has_key_focus()) {
793 | const menuItems = this._historyMenuSection.section._getMenuItems();
794 | if (menuItems.length > 1) {
795 | const isLast = menuItems.indexOf(menuItem) === menuItems.length - 1;
796 | this._historyMenuSection.section.box.navigate_focus(
797 | menuItem,
798 | isLast ? St.DirectionType.UP : St.DirectionType.DOWN,
799 | false
800 | );
801 | }
802 | }
803 | if (this._preferences.historyKeepingMode === HistoryKeepingMode.All || (
804 | this._preferences.historyKeepingMode === HistoryKeepingMode.Pinned && menuItem.pinned
805 | )) {
806 | this._storage.deleteEntryContent(menuItem).catch(log);
807 | }
808 | menuItem.destroy();
809 | if (this._historyMenuSection.section.numMenuItems === 0) {
810 | this._lastUsedId = -1;
811 | this._lastUsedSortKey = -1;
812 | }
813 | }
814 |
815 | _addKeybindings() {
816 | Main.wm.addKeybinding(
817 | this._preferences._keyToggleMenuShortcut,
818 | this._preferences._settings,
819 | Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
820 | Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW | Shell.ActionMode.POPUP,
821 | () => {
822 | this.menu.toggle();
823 | }
824 | );
825 | Main.wm.addKeybinding(
826 | this._preferences._keyTogglePrivateModeShortcut,
827 | this._preferences._settings,
828 | Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
829 | Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW | Shell.ActionMode.POPUP,
830 | () => {
831 | this._privateModeMenuItem.toggle();
832 | }
833 | );
834 | Main.wm.addKeybinding(
835 | this._preferences._keyClearHistoryShortcut,
836 | this._preferences._settings,
837 | Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
838 | Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW | Shell.ActionMode.POPUP,
839 | () => {
840 | if (!this._privateModeMenuItem.state) {
841 | this._clearMenuItem.activate(Clutter.get_current_event());
842 | }
843 | }
844 | );
845 | }
846 |
847 | _removeKeybindings() {
848 | Main.wm.removeKeybinding(this._preferences._keyClearHistoryShortcut);
849 | Main.wm.removeKeybinding(this._preferences._keyTogglePrivateModeShortcut);
850 | Main.wm.removeKeybinding(this._preferences._keyToggleMenuShortcut);
851 | }
852 |
853 | _ensureSubMenuPopulated(menuItem) {
854 | if (!menuItem.menu.isEmpty()) {
855 | return;
856 | }
857 |
858 | if (!menuItem.colorPreviewIcon) {
859 | const actions = [
860 | {
861 | title: _(`Open`, `Open URL`),
862 | validator: (text) => {
863 | return Validator.isURL(text, {
864 | protocols: [
865 | `feed`,
866 | `ftp`,
867 | `git`,
868 | `gopher`,
869 | `http`,
870 | `https`,
871 | `irc6`,
872 | `irc`,
873 | `ircs`,
874 | `rsync`,
875 | `sftp`,
876 | `smb`,
877 | `ssh`,
878 | `telnet`,
879 | `vnc`,
880 | ],
881 | require_protocol: true,
882 | });
883 | },
884 | },
885 | {
886 | title: _(`Open`, `Open URL`),
887 | validator: (text) => {
888 | return Validator.isMagnetURI(text);
889 | },
890 | },
891 | {
892 | title: _(`Open`, `Open URL`),
893 | validator: (text) => {
894 | return /^tg:\/\/\S+$/i.test(text);
895 | },
896 | },
897 | {
898 | prefix: `mailto:`,
899 | regExp: /^mailto:/i,
900 | title: _(`Compose an Email`),
901 | validator: (text) => {
902 | return Validator.isEmail(text);
903 | },
904 | },
905 | {
906 | prefix: `tel:+`,
907 | regExp: /^(tel:)?\+/i,
908 | title: _(`Make a Call`),
909 | validator: (text) => {
910 | return Validator.isMobilePhone(text);
911 | },
912 | },
913 | {
914 | title: _(`Make a Call`),
915 | validator: (text) => {
916 | return /^callto:\S+$/i.test(text);
917 | },
918 | },
919 | ];
920 |
921 | const trimmedText = menuItem.text.trim();
922 | for (const action of actions) {
923 | const capturedText = action.regExp ? trimmedText.replace(action.regExp, ``) : trimmedText;
924 | if (action.validator(capturedText)) {
925 | menuItem.menu.addAction(action.title, () => {
926 | this.menu.close();
927 | this._launchUri((action.prefix ?? ``) + capturedText);
928 | });
929 | break;
930 | }
931 | }
932 | }
933 |
934 | menuItem.menu.addAction(_(`Search the Web`), () => {
935 | this.menu.close();
936 | this._searchTheWeb(menuItem.text);
937 | });
938 |
939 | menuItem.menu.addAction(_(`Send via Email`), () => {
940 | this.menu.close();
941 | this._launchUri(`mailto:?body=${encodeURIComponent(menuItem.text)}`);
942 | });
943 |
944 | menuItem.menu.addAction(_(`Share Online`), () => {
945 | this.menu.close();
946 | this._shareOnline(menuItem.text);
947 | });
948 |
949 | menuItem.menu.addAction(_(`Show QR Code`), () => {
950 | this.menu.close();
951 | this._showQrCode(menuItem.text);
952 | });
953 | }
954 |
955 | _showQrCode(text) {
956 | this._qrCodeDialog = new QrCodeDialog(text);
957 | this._qrCodeDialog.connectObject(`destroy`, () => {
958 | delete this._qrCodeDialog;
959 | });
960 | this._qrCodeDialog.open();
961 | }
962 |
963 | _launchUri(uri) {
964 | try {
965 | Gio.app_info_launch_default_for_uri(uri, global.create_app_launch_context(0, -1));
966 | } catch (error) {
967 | notifyError(_(`Failed to launch URI "%s"`).format(uri), error.message);
968 | }
969 | }
970 |
971 | _searchTheWeb(text) {
972 | const searchEngines = SearchEngines.get(this._preferences);
973 | const currentEngine = searchEngines.find(this._preferences.webSearchEngine);
974 |
975 | if (!currentEngine) {
976 | notifyError(_(`Failed to search the web`), _(`Unknown search engine`));
977 | return;
978 | }
979 |
980 | if (currentEngine.name === `custom`) {
981 | const validatorOptions = {
982 | protocols: [
983 | `http`,
984 | `https`,
985 | ],
986 | require_protocol: true,
987 | };
988 | if (!currentEngine.url.includes(`%s`) || !Validator.isURL(currentEngine.url, validatorOptions)) {
989 | notifyError(_(`Failed to search the web`), _(`Invalid search URL "%s"`).format(currentEngine.url));
990 | return;
991 | }
992 | }
993 |
994 | this._launchUri(currentEngine.url.replace(`%s`, encodeURIComponent(text)));
995 | }
996 |
997 | _shareOnline(text) {
998 | const formData = {
999 | content: text,
1000 | expiry_days: this._preferences.expiryDays.toString(),
1001 | };
1002 |
1003 | const message = Soup.Message.new(`POST`, `https://dpaste.com/api/v2/`);
1004 | message.set_request_body_from_bytes(
1005 | Soup.FORM_MIME_TYPE_URLENCODED,
1006 | new GLib.Bytes(Soup.form_encode_hash(formData))
1007 | );
1008 |
1009 | if (!this._soupSession) {
1010 | this._soupSession = new Soup.Session({
1011 | user_agent : Me.uuid,
1012 | });
1013 | if (Soup.get_major_version() < 3) {
1014 | this._soupSession.send_and_read_finish = (message) => {
1015 | return message.response_body.flatten().get_as_bytes();
1016 | };
1017 | }
1018 | }
1019 |
1020 | this._soupSession.send_and_read_async(
1021 | message,
1022 | GLib.PRIORITY_DEFAULT,
1023 | null,
1024 | (session, result) => {
1025 | try {
1026 | if (message.status_code !== Soup.Status.CREATED) {
1027 | throw new Error(message.reason_phrase);
1028 | }
1029 | const bytes = session.send_and_read_finish(result);
1030 | const uri = new TextDecoder().decode(bytes.get_data()).trim();
1031 | if (panelIndicator.instance && !this._privateModeMenuItem.state) {
1032 | this._clipboard.setText(uri);
1033 | }
1034 | notify(_(`The text was successfully shared online`), uri, false);
1035 | } catch (error) {
1036 | notifyError(_(`Failed to share the text online`), error.message);
1037 | }
1038 | }
1039 | );
1040 | }
1041 |
1042 | _loadState() {
1043 | this._privateModeMenuItem.setToggleState(panelIndicator.state.privateMode);
1044 |
1045 | if (!panelIndicator.state.history) {
1046 | this._loadHistory();
1047 | } else if (panelIndicator.state.history.length > 0) {
1048 | panelIndicator.state.history.forEach((entry) => {
1049 | const menuItem = this._createMenuItem(entry.text, entry.pinned, entry.id, entry.sortKey);
1050 | this._historyMenuSection.section.addMenuItem(menuItem);
1051 | if (menuItem.pinned) {
1052 | ++this._pinnedCount;
1053 | }
1054 | this._lastUsedId = Math.max(menuItem.id, this._lastUsedId);
1055 | this._lastUsedSortKey = Math.max(menuItem.sortKey, this._lastUsedSortKey);
1056 | });
1057 | panelIndicator.state.history.length = 0;
1058 |
1059 | if (!this._privateModeMenuItem.state) {
1060 | this._updateCurrentItem();
1061 | }
1062 | }
1063 | }
1064 |
1065 | _saveState() {
1066 | const menuItems = this._historyMenuSection.section._getMenuItems();
1067 | panelIndicator.state.history = menuItems.map((menuItem) => {
1068 | return {
1069 | text: menuItem.text,
1070 | pinned: menuItem.pinned,
1071 | id: menuItem.id,
1072 | sortKey: menuItem.sortKey,
1073 | };
1074 | });
1075 |
1076 | panelIndicator.state.privateMode = this._privateModeMenuItem.state;
1077 | }
1078 |
1079 | _loadHistory() {
1080 | if (this._preferences.historyKeepingMode === HistoryKeepingMode.None) {
1081 | return;
1082 | }
1083 |
1084 | this._clipboard.blockSignals = true;
1085 |
1086 | this._storage.loadEntries().then(async (entries) => {
1087 | if (this._preferences.historyKeepingMode === HistoryKeepingMode.Pinned) {
1088 | entries = entries.filter((entry) => {
1089 | return entry.pinned;
1090 | });
1091 | }
1092 |
1093 | if (entries.length === 0) {
1094 | return;
1095 | }
1096 |
1097 | entries.forEach((entry) => {
1098 | this._lastUsedId = Math.max(entry.id, this._lastUsedId);
1099 | this._lastUsedSortKey = Math.max(entry.sortKey, this._lastUsedSortKey);
1100 | });
1101 |
1102 | this._clipboard.blockSignals = false;
1103 |
1104 | for (const entry of entries) {
1105 | try {
1106 | await this._storage.loadEntryContent(entry);
1107 | if (!entry.text) {
1108 | continue;
1109 | }
1110 | const menuItem = this._createMenuItem(entry.text, entry.pinned, entry.id, entry.sortKey);
1111 | if (menuItem.pinned) {
1112 | this._historyMenuSection.section.addMenuItem(menuItem, this._pinnedCount++);
1113 | } else {
1114 | this._historyMenuSection.section.addMenuItem(menuItem);
1115 | if (this._historyMenuSection.section.numMenuItems - this._pinnedCount === this._preferences.historySize) {
1116 | break;
1117 | }
1118 | }
1119 | } catch (error) {
1120 | log(error);
1121 | }
1122 | }
1123 |
1124 | if (!this._privateModeMenuItem.state) {
1125 | this._updateCurrentItem();
1126 | }
1127 | }).catch(log).finally(() => {
1128 | this._clipboard.blockSignals = false;
1129 | });
1130 | }
1131 |
1132 | _saveHistory(force = false) {
1133 | if (this._preferences.historyKeepingMode === HistoryKeepingMode.None) {
1134 | if (force) {
1135 | this._storage.saveEntries([]).catch(log);
1136 | }
1137 | return;
1138 | }
1139 |
1140 | let menuItems = this._historyMenuSection.section._getMenuItems();
1141 | if (this._preferences.historyKeepingMode === HistoryKeepingMode.Pinned) {
1142 | menuItems = menuItems.filter((menuItem) => {
1143 | return menuItem.pinned;
1144 | });
1145 | }
1146 |
1147 | this._storage.saveEntries(menuItems).catch(log);
1148 | }
1149 |
1150 | _updateCurrentItem() {
1151 | this._clipboard.getText().then((text) => {
1152 | let currentMenuItem;
1153 | if (text && text.length > 0) {
1154 | const menuItems = this._historyMenuSection.section._getMenuItems();
1155 | currentMenuItem = menuItems.find((menuItem) => {
1156 | return menuItem.text === text;
1157 | });
1158 | if (currentMenuItem) {
1159 | currentMenuItem.sortKey = ++this._lastUsedSortKey;
1160 | if (!currentMenuItem.pinned) {
1161 | this._historyMenuSection.section.moveMenuItem(currentMenuItem, this._pinnedCount);
1162 | }
1163 | this._saveHistory();
1164 | }
1165 | }
1166 |
1167 | if (this._currentMenuItem !== currentMenuItem) {
1168 | this._currentMenuItem?.setOrnament(PopupMenu.Ornament.NONE);
1169 | this._currentMenuItem = currentMenuItem;
1170 | this._currentMenuItem?.setOrnament(PopupMenu.Ornament.DOT);
1171 | }
1172 | });
1173 | }
1174 |
1175 | _updateMenuLayout() {
1176 | const privateMode = this._privateModeMenuItem.state;
1177 | this._privateModePlaceholder.actor.visible = privateMode;
1178 | this._privateModeIcon.visible = privateMode;
1179 |
1180 | const menuItemsCount = this._historyMenuSection.section.numMenuItems;
1181 | this._emptyPlaceholder.actor.visible = !privateMode && menuItemsCount === 0;
1182 | this._historyMenuSection.actor.visible = !privateMode && menuItemsCount > 0;
1183 | this._clearMenuItem.actor.visible = !privateMode && menuItemsCount > this._pinnedCount;
1184 | }
1185 |
1186 | _onClipboardTextChanged(text) {
1187 | let currentMenuItem;
1188 | if (text && text.length > 0) {
1189 | const menuItems = this._historyMenuSection.section._getMenuItems();
1190 | currentMenuItem = menuItems.find((menuItem) => {
1191 | return menuItem.text === text;
1192 | });
1193 | if (currentMenuItem) {
1194 | currentMenuItem.sortKey = ++this._lastUsedSortKey;
1195 | if (!currentMenuItem.pinned) {
1196 | this._historyMenuSection.section.moveMenuItem(currentMenuItem, this._pinnedCount);
1197 | }
1198 | } else {
1199 | if (menuItems.length - this._pinnedCount === this._preferences.historySize) {
1200 | this._destroyMenuItem(menuItems.pop());
1201 | }
1202 | currentMenuItem = this._createMenuItem(text);
1203 | this._historyMenuSection.section.addMenuItem(currentMenuItem, this._pinnedCount);
1204 | if (this._preferences.historyKeepingMode === HistoryKeepingMode.All) {
1205 | this._storage.saveEntryContent(currentMenuItem).catch(log);
1206 | }
1207 | }
1208 | this._saveHistory();
1209 | }
1210 |
1211 | if (this._currentMenuItem !== currentMenuItem) {
1212 | this._currentMenuItem?.setOrnament(PopupMenu.Ornament.NONE);
1213 | this._currentMenuItem = currentMenuItem;
1214 | this._currentMenuItem?.setOrnament(PopupMenu.Ornament.DOT);
1215 | }
1216 | }
1217 |
1218 | _onHistorySizeChanged() {
1219 | const menuItems = this._historyMenuSection.section._getMenuItems();
1220 | const menuItemsToRemove = menuItems.slice(this._preferences.historySize + this._pinnedCount);
1221 | if (menuItemsToRemove.length > 0) {
1222 | menuItemsToRemove.forEach((menuItem) => {
1223 | this._destroyMenuItem(menuItem);
1224 | });
1225 | this._saveHistory();
1226 | }
1227 | }
1228 |
1229 | _onHistoryKeepingModeChanged() {
1230 | this._saveHistory(true);
1231 |
1232 | const menuItems = this._historyMenuSection.section._getMenuItems();
1233 | menuItems.forEach((menuItem) => {
1234 | switch (this._preferences.historyKeepingMode) {
1235 | case HistoryKeepingMode.None: {
1236 | if (this._preferences.previousHistoryKeepingMode === HistoryKeepingMode.All || (
1237 | this._preferences.previousHistoryKeepingMode === HistoryKeepingMode.Pinned && menuItem.pinned
1238 | )) {
1239 | this._storage.deleteEntryContent(menuItem).catch(log);
1240 | }
1241 | break;
1242 | }
1243 | case HistoryKeepingMode.Pinned: {
1244 | if (menuItem.pinned) {
1245 | if (this._preferences.previousHistoryKeepingMode === HistoryKeepingMode.None) {
1246 | this._storage.saveEntryContent(menuItem).catch(log);
1247 | }
1248 | } else {
1249 | if (this._preferences.previousHistoryKeepingMode === HistoryKeepingMode.All) {
1250 | this._storage.deleteEntryContent(menuItem).catch(log);
1251 | }
1252 | }
1253 | break;
1254 | }
1255 | case HistoryKeepingMode.All: {
1256 | if (this._preferences.previousHistoryKeepingMode === HistoryKeepingMode.None || (
1257 | this._preferences.previousHistoryKeepingMode === HistoryKeepingMode.Pinned && !menuItem.pinned
1258 | )) {
1259 | this._storage.saveEntryContent(menuItem).catch(log);
1260 | }
1261 | break;
1262 | }
1263 | default:
1264 | break;
1265 | }
1266 | });
1267 |
1268 | this._preferences.previousHistoryKeepingMode = this._preferences.historyKeepingMode;
1269 | }
1270 |
1271 | _onMenuItemPinned(menuItem) {
1272 | const menuItems = this._historyMenuSection.section._getMenuItems();
1273 | const currentIndex = menuItems.indexOf(menuItem);
1274 | if (menuItem.pinned) {
1275 | if (currentIndex < this._pinnedCount) {
1276 | return;
1277 | }
1278 | this._historyMenuSection.section.moveMenuItem(menuItem, 0);
1279 | ++this._pinnedCount;
1280 | if (this._preferences.historyKeepingMode === HistoryKeepingMode.Pinned) {
1281 | this._storage.saveEntryContent(menuItem).catch(log);
1282 | }
1283 | } else {
1284 | if (currentIndex >= this._pinnedCount) {
1285 | return;
1286 | }
1287 | let menuItemToDelete;
1288 | if (menuItems.length - this._pinnedCount === this._preferences.historySize) {
1289 | if (menuItem.sortKey < menuItems[menuItems.length - 1].sortKey) {
1290 | menuItemToDelete = menuItem;
1291 | } else {
1292 | menuItemToDelete = menuItems.pop();
1293 | }
1294 | }
1295 | if (menuItemToDelete) {
1296 | this._destroyMenuItem(menuItemToDelete);
1297 | }
1298 | if (menuItemToDelete !== menuItem) {
1299 | let indexToMove = menuItems.length;
1300 | for (let i = this._pinnedCount; i < menuItems.length; ++i) {
1301 | if (menuItems[i].sortKey < menuItem.sortKey) {
1302 | indexToMove = i;
1303 | break;
1304 | }
1305 | }
1306 | this._historyMenuSection.section.moveMenuItem(menuItem, indexToMove - 1);
1307 | }
1308 | --this._pinnedCount;
1309 | if (this._preferences.historyKeepingMode === HistoryKeepingMode.Pinned) {
1310 | this._storage.deleteEntryContent(menuItem).catch(log);
1311 | }
1312 | }
1313 |
1314 | this._saveHistory();
1315 | this._updateMenuLayout();
1316 | }
1317 |
1318 | _onOpenStateChanged(...[, open]) {
1319 | if (open) {
1320 | this.add_style_pseudo_class(`active`);
1321 |
1322 | this._historyMenuSection.scrollView.vscroll.adjustment.value = 0;
1323 | this._historyMenuSection.entry.text = ``;
1324 | Promise.resolve().then(() => {
1325 | global.stage.set_key_focus(this._historyMenuSection.entry);
1326 | });
1327 |
1328 | const workArea = Main.layoutManager.getWorkAreaForMonitor(Main.layoutManager.primaryIndex);
1329 | const scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
1330 | const margin = this.menu.actor.get_margin();
1331 | const minAvailableSize = Math.min(
1332 | (workArea.width - margin.left - margin.right) / scaleFactor,
1333 | (workArea.height - margin.top - margin.bottom) / scaleFactor,
1334 | );
1335 |
1336 | const [menuMaxWidthRatio, menuMaxHeightRatio] = [
1337 | [0.47, 0.6, 0.72],
1338 | [0.55, 0.7, 0.85],
1339 | ];
1340 | const [menuMaxWidth, menuMaxHeight] = [
1341 | Math.round(minAvailableSize * menuMaxWidthRatio[this._preferences.menuMaxSize]),
1342 | Math.round(minAvailableSize * menuMaxHeightRatio[this._preferences.menuMaxSize]),
1343 | ];
1344 | this.menu.actor.style = `max-width: ${menuMaxWidth}px; max-height: ${menuMaxHeight}px;`;
1345 |
1346 | const entryMinWidth = Math.min(300, Math.round(menuMaxWidth * 0.75));
1347 | this._historyMenuSection.entry.style = `min-width: ${entryMinWidth}px;`;
1348 | } else {
1349 | this.remove_style_pseudo_class(`active`);
1350 | }
1351 | }
1352 | });
1353 |
1354 | function notify(text, details, transient = true) {
1355 | const source = new MessageTray.SystemNotificationSource();
1356 | Main.messageTray.add(source);
1357 |
1358 | const notification = new MessageTray.Notification(source, text, details);
1359 | notification.setTransient(transient);
1360 | source.showNotification(notification);
1361 | };
1362 |
1363 | function notifyError(error, details, transient) {
1364 | log(error);
1365 | notify(error, details, transient);
1366 | };
1367 |
1368 | function init() {
1369 | SignalTracker.registerDestroyableType(ClipboardManager);
1370 | SignalTracker.registerDestroyableType(Preferences);
1371 |
1372 | ExtensionUtils.initTranslations(Me.uuid);
1373 | }
1374 |
1375 | const panelIndicator = {
1376 | instance: null,
1377 | state: {
1378 | history: null,
1379 | privateMode: false,
1380 | }
1381 | };
1382 |
1383 | function enable() {
1384 | panelIndicator.instance = new PanelIndicator();
1385 | Main.panel.addToStatusArea(`${Me.metadata.name}`, panelIndicator.instance);
1386 | }
1387 |
1388 | function disable() {
1389 | panelIndicator.instance.destroy();
1390 | delete panelIndicator.instance;
1391 | }
1392 |
--------------------------------------------------------------------------------
/icons/clipboard-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/icons/private-mode-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/libs/preferences.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { GObject } = imports.gi;
4 | const ExtensionUtils = imports.misc.extensionUtils;
5 |
6 | var Preferences = GObject.registerClass({
7 | Signals: {
8 | 'destroy': {},
9 | 'historySizeChanged': {},
10 | 'historyKeepingModeChanged': {},
11 | 'menuMaxSizeChanged': {},
12 | 'webSearchEngineChanged': {},
13 | 'shortcutChanged': {
14 | param_types: [GObject.TYPE_STRING],
15 | },
16 | },
17 | }, class Preferences extends GObject.Object {
18 | constructor() {
19 | super();
20 |
21 | this._keyHistorySize = `history-size`;
22 | this._keyHistoryKeepingMode = `history-keeping-mode`;
23 | this._keyShowSurroundingWhitespace = `show-surrounding-whitespace`;
24 | this._keyShowColorPreview = `show-color-preview`;
25 | this._keyMenuMaxSize = `menu-max-size`;
26 | this._keyWebSearchEngine = `web-search-engine`;
27 | this._keyCustomWebSearchUrl = `custom-web-search-url`;
28 | this._keyExpiryDays = `expiry-days`;
29 | this._keyToggleMenuShortcut = `toggle-menu-shortcut`;
30 | this._keyTogglePrivateModeShortcut = `toggle-private-mode-shortcut`;
31 | this._keyClearHistoryShortcut = `clear-history-shortcut`;
32 |
33 | this._settings = ExtensionUtils.getSettings();
34 | this._settingsChangedId = this._settings.connect(`changed`, (...[, key]) => {
35 | switch (key) {
36 | case this._keyHistorySize: {
37 | this.emit(`historySizeChanged`);
38 | break;
39 | }
40 | case this._keyHistoryKeepingMode: {
41 | this.emit(`historyKeepingModeChanged`);
42 | break;
43 | }
44 | case this._keyMenuMaxSize: {
45 | this.emit(`menuMaxSizeChanged`);
46 | break;
47 | }
48 | case this._keyWebSearchEngine: {
49 | this.emit(`webSearchEngineChanged`);
50 | break;
51 | }
52 | case this._keyToggleMenuShortcut:
53 | case this._keyTogglePrivateModeShortcut:
54 | case this._keyClearHistoryShortcut: {
55 | this.emit(`shortcutChanged`, key);
56 | break;
57 | }
58 | default:
59 | break;
60 | }
61 | });
62 | }
63 |
64 | destroy() {
65 | this._settings.disconnect(this._settingsChangedId);
66 |
67 | this.emit(`destroy`);
68 | }
69 |
70 | get historySize() {
71 | return this._settings.get_int(this._keyHistorySize);
72 | }
73 |
74 | get historyKeepingMode() {
75 | return this._settings.get_enum(this._keyHistoryKeepingMode);
76 | }
77 |
78 | set historyKeepingMode(historyKeepingMode) {
79 | this._settings.set_enum(this._keyHistoryKeepingMode, historyKeepingMode);
80 | }
81 |
82 | get menuMaxSize() {
83 | return this._settings.get_enum(this._keyMenuMaxSize);
84 | }
85 |
86 | set menuMaxSize(menuMaxSize) {
87 | this._settings.set_enum(this._keyMenuMaxSize, menuMaxSize);
88 | }
89 |
90 | get webSearchEngine() {
91 | return this._settings.get_string(this._keyWebSearchEngine);
92 | }
93 |
94 | set webSearchEngine(webSearchEngine) {
95 | this._settings.set_string(this._keyWebSearchEngine, webSearchEngine);
96 | }
97 |
98 | get customWebSearchUrl() {
99 | return this._settings.get_string(this._keyCustomWebSearchUrl);
100 | }
101 |
102 | get expiryDays() {
103 | return this._settings.get_int(this._keyExpiryDays);
104 | }
105 |
106 | bind(key, object, property, flags) {
107 | this._settings.bind(key, object, property, flags);
108 | }
109 |
110 | getShortcut(key) {
111 | return this._settings.get_strv(key)[0] ?? ``;
112 | }
113 |
114 | setShortcut(key, shortcut) {
115 | this._settings.set_strv(key, [shortcut]);
116 | }
117 | });
118 |
--------------------------------------------------------------------------------
/libs/qrcodegen.js:
--------------------------------------------------------------------------------
1 | /*
2 | * QR Code generator library (TypeScript)
3 | *
4 | * Copyright (c) Project Nayuki. (MIT License)
5 | * https://www.nayuki.io/page/qr-code-generator-library
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy of
8 | * this software and associated documentation files (the "Software"), to deal in
9 | * the Software without restriction, including without limitation the rights to
10 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
11 | * the Software, and to permit persons to whom the Software is furnished to do so,
12 | * subject to the following conditions:
13 | * - The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | * - The Software is provided "as is", without warranty of any kind, express or
16 | * implied, including but not limited to the warranties of merchantability,
17 | * fitness for a particular purpose and noninfringement. In no event shall the
18 | * authors or copyright holders be liable for any claim, damages or other
19 | * liability, whether in an action of contract, tort or otherwise, arising from,
20 | * out of or in connection with the Software or the use or other dealings in the
21 | * Software.
22 | */
23 | "use strict";
24 | var qrcodegen;
25 | (function (qrcodegen) {
26 | /*---- QR Code symbol class ----*/
27 | /*
28 | * A QR Code symbol, which is a type of two-dimension barcode.
29 | * Invented by Denso Wave and described in the ISO/IEC 18004 standard.
30 | * Instances of this class represent an immutable square grid of dark and light cells.
31 | * The class provides static factory functions to create a QR Code from text or binary data.
32 | * The class covers the QR Code Model 2 specification, supporting all versions (sizes)
33 | * from 1 to 40, all 4 error correction levels, and 4 character encoding modes.
34 | *
35 | * Ways to create a QR Code object:
36 | * - High level: Take the payload data and call QrCode.encodeText() or QrCode.encodeBinary().
37 | * - Mid level: Custom-make the list of segments and call QrCode.encodeSegments().
38 | * - Low level: Custom-make the array of data codeword bytes (including
39 | * segment headers and final padding, excluding error correction codewords),
40 | * supply the appropriate version number, and call the QrCode() constructor.
41 | * (Note that all ways require supplying the desired error correction level.)
42 | */
43 | class QrCode {
44 | /*-- Constructor (low level) and fields --*/
45 | // Creates a new QR Code with the given version number,
46 | // error correction level, data codeword bytes, and mask number.
47 | // This is a low-level API that most users should not use directly.
48 | // A mid-level API is the encodeSegments() function.
49 | constructor(
50 | // The version number of this QR Code, which is between 1 and 40 (inclusive).
51 | // This determines the size of this barcode.
52 | version,
53 | // The error correction level used in this QR Code.
54 | errorCorrectionLevel, dataCodewords, msk) {
55 | this.version = version;
56 | this.errorCorrectionLevel = errorCorrectionLevel;
57 | // The modules of this QR Code (false = light, true = dark).
58 | // Immutable after constructor finishes. Accessed through getModule().
59 | this.modules = [];
60 | // Indicates function modules that are not subjected to masking. Discarded when constructor finishes.
61 | this.isFunction = [];
62 | // Check scalar arguments
63 | if (version < QrCode.MIN_VERSION || version > QrCode.MAX_VERSION)
64 | throw new RangeError("Version value out of range");
65 | if (msk < -1 || msk > 7)
66 | throw new RangeError("Mask value out of range");
67 | this.size = version * 4 + 17;
68 | // Initialize both grids to be size*size arrays of Boolean false
69 | let row = [];
70 | for (let i = 0; i < this.size; i++)
71 | row.push(false);
72 | for (let i = 0; i < this.size; i++) {
73 | this.modules.push(row.slice()); // Initially all light
74 | this.isFunction.push(row.slice());
75 | }
76 | // Compute ECC, draw modules
77 | this.drawFunctionPatterns();
78 | const allCodewords = this.addEccAndInterleave(dataCodewords);
79 | this.drawCodewords(allCodewords);
80 | // Do masking
81 | if (msk == -1) { // Automatically choose best mask
82 | let minPenalty = 1000000000;
83 | for (let i = 0; i < 8; i++) {
84 | this.applyMask(i);
85 | this.drawFormatBits(i);
86 | const penalty = this.getPenaltyScore();
87 | if (penalty < minPenalty) {
88 | msk = i;
89 | minPenalty = penalty;
90 | }
91 | this.applyMask(i); // Undoes the mask due to XOR
92 | }
93 | }
94 | assert(0 <= msk && msk <= 7);
95 | this.mask = msk;
96 | this.applyMask(msk); // Apply the final choice of mask
97 | this.drawFormatBits(msk); // Overwrite old format bits
98 | this.isFunction = [];
99 | }
100 | /*-- Static factory functions (high level) --*/
101 | // Returns a QR Code representing the given Unicode text string at the given error correction level.
102 | // As a conservative upper bound, this function is guaranteed to succeed for strings that have 738 or fewer
103 | // Unicode code points (not UTF-16 code units) if the low error correction level is used. The smallest possible
104 | // QR Code version is automatically chosen for the output. The ECC level of the result may be higher than the
105 | // ecl argument if it can be done without increasing the version.
106 | static encodeText(text, ecl) {
107 | const segs = qrcodegen.QrSegment.makeSegments(text);
108 | return QrCode.encodeSegments(segs, ecl);
109 | }
110 | // Returns a QR Code representing the given binary data at the given error correction level.
111 | // This function always encodes using the binary segment mode, not any text mode. The maximum number of
112 | // bytes allowed is 2953. The smallest possible QR Code version is automatically chosen for the output.
113 | // The ECC level of the result may be higher than the ecl argument if it can be done without increasing the version.
114 | static encodeBinary(data, ecl) {
115 | const seg = qrcodegen.QrSegment.makeBytes(data);
116 | return QrCode.encodeSegments([seg], ecl);
117 | }
118 | /*-- Static factory functions (mid level) --*/
119 | // Returns a QR Code representing the given segments with the given encoding parameters.
120 | // The smallest possible QR Code version within the given range is automatically
121 | // chosen for the output. Iff boostEcl is true, then the ECC level of the result
122 | // may be higher than the ecl argument if it can be done without increasing the
123 | // version. The mask number is either between 0 to 7 (inclusive) to force that
124 | // mask, or -1 to automatically choose an appropriate mask (which may be slow).
125 | // This function allows the user to create a custom sequence of segments that switches
126 | // between modes (such as alphanumeric and byte) to encode text in less space.
127 | // This is a mid-level API; the high-level API is encodeText() and encodeBinary().
128 | static encodeSegments(segs, ecl, minVersion = 1, maxVersion = 40, mask = -1, boostEcl = true) {
129 | if (!(QrCode.MIN_VERSION <= minVersion && minVersion <= maxVersion && maxVersion <= QrCode.MAX_VERSION)
130 | || mask < -1 || mask > 7)
131 | throw new RangeError("Invalid value");
132 | // Find the minimal version number to use
133 | let version;
134 | let dataUsedBits;
135 | for (version = minVersion;; version++) {
136 | const dataCapacityBits = QrCode.getNumDataCodewords(version, ecl) * 8; // Number of data bits available
137 | const usedBits = QrSegment.getTotalBits(segs, version);
138 | if (usedBits <= dataCapacityBits) {
139 | dataUsedBits = usedBits;
140 | break; // This version number is found to be suitable
141 | }
142 | if (version >= maxVersion) // All versions in the range could not fit the given data
143 | throw new RangeError("Data too long");
144 | }
145 | // Increase the error correction level while the data still fits in the current version number
146 | for (const newEcl of [QrCode.Ecc.MEDIUM, QrCode.Ecc.QUARTILE, QrCode.Ecc.HIGH]) { // From low to high
147 | if (boostEcl && dataUsedBits <= QrCode.getNumDataCodewords(version, newEcl) * 8)
148 | ecl = newEcl;
149 | }
150 | // Concatenate all segments to create the data bit string
151 | let bb = [];
152 | for (const seg of segs) {
153 | appendBits(seg.mode.modeBits, 4, bb);
154 | appendBits(seg.numChars, seg.mode.numCharCountBits(version), bb);
155 | for (const b of seg.getData())
156 | bb.push(b);
157 | }
158 | assert(bb.length == dataUsedBits);
159 | // Add terminator and pad up to a byte if applicable
160 | const dataCapacityBits = QrCode.getNumDataCodewords(version, ecl) * 8;
161 | assert(bb.length <= dataCapacityBits);
162 | appendBits(0, Math.min(4, dataCapacityBits - bb.length), bb);
163 | appendBits(0, (8 - bb.length % 8) % 8, bb);
164 | assert(bb.length % 8 == 0);
165 | // Pad with alternating bytes until data capacity is reached
166 | for (let padByte = 0xEC; bb.length < dataCapacityBits; padByte ^= 0xEC ^ 0x11)
167 | appendBits(padByte, 8, bb);
168 | // Pack bits into bytes in big endian
169 | let dataCodewords = [];
170 | while (dataCodewords.length * 8 < bb.length)
171 | dataCodewords.push(0);
172 | bb.forEach((b, i) => dataCodewords[i >>> 3] |= b << (7 - (i & 7)));
173 | // Create the QR Code object
174 | return new QrCode(version, ecl, dataCodewords, mask);
175 | }
176 | /*-- Accessor methods --*/
177 | // Returns the color of the module (pixel) at the given coordinates, which is false
178 | // for light or true for dark. The top left corner has the coordinates (x=0, y=0).
179 | // If the given coordinates are out of bounds, then false (light) is returned.
180 | getModule(x, y) {
181 | return 0 <= x && x < this.size && 0 <= y && y < this.size && this.modules[y][x];
182 | }
183 | /*-- Private helper methods for constructor: Drawing function modules --*/
184 | // Reads this object's version field, and draws and marks all function modules.
185 | drawFunctionPatterns() {
186 | // Draw horizontal and vertical timing patterns
187 | for (let i = 0; i < this.size; i++) {
188 | this.setFunctionModule(6, i, i % 2 == 0);
189 | this.setFunctionModule(i, 6, i % 2 == 0);
190 | }
191 | // Draw 3 finder patterns (all corners except bottom right; overwrites some timing modules)
192 | this.drawFinderPattern(3, 3);
193 | this.drawFinderPattern(this.size - 4, 3);
194 | this.drawFinderPattern(3, this.size - 4);
195 | // Draw numerous alignment patterns
196 | const alignPatPos = this.getAlignmentPatternPositions();
197 | const numAlign = alignPatPos.length;
198 | for (let i = 0; i < numAlign; i++) {
199 | for (let j = 0; j < numAlign; j++) {
200 | // Don't draw on the three finder corners
201 | if (!(i == 0 && j == 0 || i == 0 && j == numAlign - 1 || i == numAlign - 1 && j == 0))
202 | this.drawAlignmentPattern(alignPatPos[i], alignPatPos[j]);
203 | }
204 | }
205 | // Draw configuration data
206 | this.drawFormatBits(0); // Dummy mask value; overwritten later in the constructor
207 | this.drawVersion();
208 | }
209 | // Draws two copies of the format bits (with its own error correction code)
210 | // based on the given mask and this object's error correction level field.
211 | drawFormatBits(mask) {
212 | // Calculate error correction code and pack bits
213 | const data = this.errorCorrectionLevel.formatBits << 3 | mask; // errCorrLvl is uint2, mask is uint3
214 | let rem = data;
215 | for (let i = 0; i < 10; i++)
216 | rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
217 | const bits = (data << 10 | rem) ^ 0x5412; // uint15
218 | assert(bits >>> 15 == 0);
219 | // Draw first copy
220 | for (let i = 0; i <= 5; i++)
221 | this.setFunctionModule(8, i, getBit(bits, i));
222 | this.setFunctionModule(8, 7, getBit(bits, 6));
223 | this.setFunctionModule(8, 8, getBit(bits, 7));
224 | this.setFunctionModule(7, 8, getBit(bits, 8));
225 | for (let i = 9; i < 15; i++)
226 | this.setFunctionModule(14 - i, 8, getBit(bits, i));
227 | // Draw second copy
228 | for (let i = 0; i < 8; i++)
229 | this.setFunctionModule(this.size - 1 - i, 8, getBit(bits, i));
230 | for (let i = 8; i < 15; i++)
231 | this.setFunctionModule(8, this.size - 15 + i, getBit(bits, i));
232 | this.setFunctionModule(8, this.size - 8, true); // Always dark
233 | }
234 | // Draws two copies of the version bits (with its own error correction code),
235 | // based on this object's version field, iff 7 <= version <= 40.
236 | drawVersion() {
237 | if (this.version < 7)
238 | return;
239 | // Calculate error correction code and pack bits
240 | let rem = this.version; // version is uint6, in the range [7, 40]
241 | for (let i = 0; i < 12; i++)
242 | rem = (rem << 1) ^ ((rem >>> 11) * 0x1F25);
243 | const bits = this.version << 12 | rem; // uint18
244 | assert(bits >>> 18 == 0);
245 | // Draw two copies
246 | for (let i = 0; i < 18; i++) {
247 | const color = getBit(bits, i);
248 | const a = this.size - 11 + i % 3;
249 | const b = Math.floor(i / 3);
250 | this.setFunctionModule(a, b, color);
251 | this.setFunctionModule(b, a, color);
252 | }
253 | }
254 | // Draws a 9*9 finder pattern including the border separator,
255 | // with the center module at (x, y). Modules can be out of bounds.
256 | drawFinderPattern(x, y) {
257 | for (let dy = -4; dy <= 4; dy++) {
258 | for (let dx = -4; dx <= 4; dx++) {
259 | const dist = Math.max(Math.abs(dx), Math.abs(dy)); // Chebyshev/infinity norm
260 | const xx = x + dx;
261 | const yy = y + dy;
262 | if (0 <= xx && xx < this.size && 0 <= yy && yy < this.size)
263 | this.setFunctionModule(xx, yy, dist != 2 && dist != 4);
264 | }
265 | }
266 | }
267 | // Draws a 5*5 alignment pattern, with the center module
268 | // at (x, y). All modules must be in bounds.
269 | drawAlignmentPattern(x, y) {
270 | for (let dy = -2; dy <= 2; dy++) {
271 | for (let dx = -2; dx <= 2; dx++)
272 | this.setFunctionModule(x + dx, y + dy, Math.max(Math.abs(dx), Math.abs(dy)) != 1);
273 | }
274 | }
275 | // Sets the color of a module and marks it as a function module.
276 | // Only used by the constructor. Coordinates must be in bounds.
277 | setFunctionModule(x, y, isDark) {
278 | this.modules[y][x] = isDark;
279 | this.isFunction[y][x] = true;
280 | }
281 | /*-- Private helper methods for constructor: Codewords and masking --*/
282 | // Returns a new byte string representing the given data with the appropriate error correction
283 | // codewords appended to it, based on this object's version and error correction level.
284 | addEccAndInterleave(data) {
285 | const ver = this.version;
286 | const ecl = this.errorCorrectionLevel;
287 | if (data.length != QrCode.getNumDataCodewords(ver, ecl))
288 | throw new RangeError("Invalid argument");
289 | // Calculate parameter numbers
290 | const numBlocks = QrCode.NUM_ERROR_CORRECTION_BLOCKS[ecl.ordinal][ver];
291 | const blockEccLen = QrCode.ECC_CODEWORDS_PER_BLOCK[ecl.ordinal][ver];
292 | const rawCodewords = Math.floor(QrCode.getNumRawDataModules(ver) / 8);
293 | const numShortBlocks = numBlocks - rawCodewords % numBlocks;
294 | const shortBlockLen = Math.floor(rawCodewords / numBlocks);
295 | // Split data into blocks and append ECC to each block
296 | let blocks = [];
297 | const rsDiv = QrCode.reedSolomonComputeDivisor(blockEccLen);
298 | for (let i = 0, k = 0; i < numBlocks; i++) {
299 | let dat = data.slice(k, k + shortBlockLen - blockEccLen + (i < numShortBlocks ? 0 : 1));
300 | k += dat.length;
301 | const ecc = QrCode.reedSolomonComputeRemainder(dat, rsDiv);
302 | if (i < numShortBlocks)
303 | dat.push(0);
304 | blocks.push(dat.concat(ecc));
305 | }
306 | // Interleave (not concatenate) the bytes from every block into a single sequence
307 | let result = [];
308 | for (let i = 0; i < blocks[0].length; i++) {
309 | blocks.forEach((block, j) => {
310 | // Skip the padding byte in short blocks
311 | if (i != shortBlockLen - blockEccLen || j >= numShortBlocks)
312 | result.push(block[i]);
313 | });
314 | }
315 | assert(result.length == rawCodewords);
316 | return result;
317 | }
318 | // Draws the given sequence of 8-bit codewords (data and error correction) onto the entire
319 | // data area of this QR Code. Function modules need to be marked off before this is called.
320 | drawCodewords(data) {
321 | if (data.length != Math.floor(QrCode.getNumRawDataModules(this.version) / 8))
322 | throw new RangeError("Invalid argument");
323 | let i = 0; // Bit index into the data
324 | // Do the funny zigzag scan
325 | for (let right = this.size - 1; right >= 1; right -= 2) { // Index of right column in each column pair
326 | if (right == 6)
327 | right = 5;
328 | for (let vert = 0; vert < this.size; vert++) { // Vertical counter
329 | for (let j = 0; j < 2; j++) {
330 | const x = right - j; // Actual x coordinate
331 | const upward = ((right + 1) & 2) == 0;
332 | const y = upward ? this.size - 1 - vert : vert; // Actual y coordinate
333 | if (!this.isFunction[y][x] && i < data.length * 8) {
334 | this.modules[y][x] = getBit(data[i >>> 3], 7 - (i & 7));
335 | i++;
336 | }
337 | // If this QR Code has any remainder bits (0 to 7), they were assigned as
338 | // 0/false/light by the constructor and are left unchanged by this method
339 | }
340 | }
341 | }
342 | assert(i == data.length * 8);
343 | }
344 | // XORs the codeword modules in this QR Code with the given mask pattern.
345 | // The function modules must be marked and the codeword bits must be drawn
346 | // before masking. Due to the arithmetic of XOR, calling applyMask() with
347 | // the same mask value a second time will undo the mask. A final well-formed
348 | // QR Code needs exactly one (not zero, two, etc.) mask applied.
349 | applyMask(mask) {
350 | if (mask < 0 || mask > 7)
351 | throw new RangeError("Mask value out of range");
352 | for (let y = 0; y < this.size; y++) {
353 | for (let x = 0; x < this.size; x++) {
354 | let invert;
355 | switch (mask) {
356 | case 0:
357 | invert = (x + y) % 2 == 0;
358 | break;
359 | case 1:
360 | invert = y % 2 == 0;
361 | break;
362 | case 2:
363 | invert = x % 3 == 0;
364 | break;
365 | case 3:
366 | invert = (x + y) % 3 == 0;
367 | break;
368 | case 4:
369 | invert = (Math.floor(x / 3) + Math.floor(y / 2)) % 2 == 0;
370 | break;
371 | case 5:
372 | invert = x * y % 2 + x * y % 3 == 0;
373 | break;
374 | case 6:
375 | invert = (x * y % 2 + x * y % 3) % 2 == 0;
376 | break;
377 | case 7:
378 | invert = ((x + y) % 2 + x * y % 3) % 2 == 0;
379 | break;
380 | default: throw new Error("Unreachable");
381 | }
382 | if (!this.isFunction[y][x] && invert)
383 | this.modules[y][x] = !this.modules[y][x];
384 | }
385 | }
386 | }
387 | // Calculates and returns the penalty score based on state of this QR Code's current modules.
388 | // This is used by the automatic mask choice algorithm to find the mask pattern that yields the lowest score.
389 | getPenaltyScore() {
390 | let result = 0;
391 | // Adjacent modules in row having same color, and finder-like patterns
392 | for (let y = 0; y < this.size; y++) {
393 | let runColor = false;
394 | let runX = 0;
395 | let runHistory = [0, 0, 0, 0, 0, 0, 0];
396 | for (let x = 0; x < this.size; x++) {
397 | if (this.modules[y][x] == runColor) {
398 | runX++;
399 | if (runX == 5)
400 | result += QrCode.PENALTY_N1;
401 | else if (runX > 5)
402 | result++;
403 | }
404 | else {
405 | this.finderPenaltyAddHistory(runX, runHistory);
406 | if (!runColor)
407 | result += this.finderPenaltyCountPatterns(runHistory) * QrCode.PENALTY_N3;
408 | runColor = this.modules[y][x];
409 | runX = 1;
410 | }
411 | }
412 | result += this.finderPenaltyTerminateAndCount(runColor, runX, runHistory) * QrCode.PENALTY_N3;
413 | }
414 | // Adjacent modules in column having same color, and finder-like patterns
415 | for (let x = 0; x < this.size; x++) {
416 | let runColor = false;
417 | let runY = 0;
418 | let runHistory = [0, 0, 0, 0, 0, 0, 0];
419 | for (let y = 0; y < this.size; y++) {
420 | if (this.modules[y][x] == runColor) {
421 | runY++;
422 | if (runY == 5)
423 | result += QrCode.PENALTY_N1;
424 | else if (runY > 5)
425 | result++;
426 | }
427 | else {
428 | this.finderPenaltyAddHistory(runY, runHistory);
429 | if (!runColor)
430 | result += this.finderPenaltyCountPatterns(runHistory) * QrCode.PENALTY_N3;
431 | runColor = this.modules[y][x];
432 | runY = 1;
433 | }
434 | }
435 | result += this.finderPenaltyTerminateAndCount(runColor, runY, runHistory) * QrCode.PENALTY_N3;
436 | }
437 | // 2*2 blocks of modules having same color
438 | for (let y = 0; y < this.size - 1; y++) {
439 | for (let x = 0; x < this.size - 1; x++) {
440 | const color = this.modules[y][x];
441 | if (color == this.modules[y][x + 1] &&
442 | color == this.modules[y + 1][x] &&
443 | color == this.modules[y + 1][x + 1])
444 | result += QrCode.PENALTY_N2;
445 | }
446 | }
447 | // Balance of dark and light modules
448 | let dark = 0;
449 | for (const row of this.modules)
450 | dark = row.reduce((sum, color) => sum + (color ? 1 : 0), dark);
451 | const total = this.size * this.size; // Note that size is odd, so dark/total != 1/2
452 | // Compute the smallest integer k >= 0 such that (45-5k)% <= dark/total <= (55+5k)%
453 | const k = Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1;
454 | assert(0 <= k && k <= 9);
455 | result += k * QrCode.PENALTY_N4;
456 | assert(0 <= result && result <= 2568888); // Non-tight upper bound based on default values of PENALTY_N1, ..., N4
457 | return result;
458 | }
459 | /*-- Private helper functions --*/
460 | // Returns an ascending list of positions of alignment patterns for this version number.
461 | // Each position is in the range [0,177), and are used on both the x and y axes.
462 | // This could be implemented as lookup table of 40 variable-length lists of integers.
463 | getAlignmentPatternPositions() {
464 | if (this.version == 1)
465 | return [];
466 | else {
467 | const numAlign = Math.floor(this.version / 7) + 2;
468 | const step = (this.version == 32) ? 26 :
469 | Math.ceil((this.version * 4 + 4) / (numAlign * 2 - 2)) * 2;
470 | let result = [6];
471 | for (let pos = this.size - 7; result.length < numAlign; pos -= step)
472 | result.splice(1, 0, pos);
473 | return result;
474 | }
475 | }
476 | // Returns the number of data bits that can be stored in a QR Code of the given version number, after
477 | // all function modules are excluded. This includes remainder bits, so it might not be a multiple of 8.
478 | // The result is in the range [208, 29648]. This could be implemented as a 40-entry lookup table.
479 | static getNumRawDataModules(ver) {
480 | if (ver < QrCode.MIN_VERSION || ver > QrCode.MAX_VERSION)
481 | throw new RangeError("Version number out of range");
482 | let result = (16 * ver + 128) * ver + 64;
483 | if (ver >= 2) {
484 | const numAlign = Math.floor(ver / 7) + 2;
485 | result -= (25 * numAlign - 10) * numAlign - 55;
486 | if (ver >= 7)
487 | result -= 36;
488 | }
489 | assert(208 <= result && result <= 29648);
490 | return result;
491 | }
492 | // Returns the number of 8-bit data (i.e. not error correction) codewords contained in any
493 | // QR Code of the given version number and error correction level, with remainder bits discarded.
494 | // This stateless pure function could be implemented as a (40*4)-cell lookup table.
495 | static getNumDataCodewords(ver, ecl) {
496 | return Math.floor(QrCode.getNumRawDataModules(ver) / 8) -
497 | QrCode.ECC_CODEWORDS_PER_BLOCK[ecl.ordinal][ver] *
498 | QrCode.NUM_ERROR_CORRECTION_BLOCKS[ecl.ordinal][ver];
499 | }
500 | // Returns a Reed-Solomon ECC generator polynomial for the given degree. This could be
501 | // implemented as a lookup table over all possible parameter values, instead of as an algorithm.
502 | static reedSolomonComputeDivisor(degree) {
503 | if (degree < 1 || degree > 255)
504 | throw new RangeError("Degree out of range");
505 | // Polynomial coefficients are stored from highest to lowest power, excluding the leading term which is always 1.
506 | // For example the polynomial x^3 + 255x^2 + 8x + 93 is stored as the uint8 array [255, 8, 93].
507 | let result = [];
508 | for (let i = 0; i < degree - 1; i++)
509 | result.push(0);
510 | result.push(1); // Start off with the monomial x^0
511 | // Compute the product polynomial (x - r^0) * (x - r^1) * (x - r^2) * ... * (x - r^{degree-1}),
512 | // and drop the highest monomial term which is always 1x^degree.
513 | // Note that r = 0x02, which is a generator element of this field GF(2^8/0x11D).
514 | let root = 1;
515 | for (let i = 0; i < degree; i++) {
516 | // Multiply the current product by (x - r^i)
517 | for (let j = 0; j < result.length; j++) {
518 | result[j] = QrCode.reedSolomonMultiply(result[j], root);
519 | if (j + 1 < result.length)
520 | result[j] ^= result[j + 1];
521 | }
522 | root = QrCode.reedSolomonMultiply(root, 0x02);
523 | }
524 | return result;
525 | }
526 | // Returns the Reed-Solomon error correction codeword for the given data and divisor polynomials.
527 | static reedSolomonComputeRemainder(data, divisor) {
528 | let result = divisor.map(_ => 0);
529 | for (const b of data) { // Polynomial division
530 | const factor = b ^ result.shift();
531 | result.push(0);
532 | divisor.forEach((coef, i) => result[i] ^= QrCode.reedSolomonMultiply(coef, factor));
533 | }
534 | return result;
535 | }
536 | // Returns the product of the two given field elements modulo GF(2^8/0x11D). The arguments and result
537 | // are unsigned 8-bit integers. This could be implemented as a lookup table of 256*256 entries of uint8.
538 | static reedSolomonMultiply(x, y) {
539 | if (x >>> 8 != 0 || y >>> 8 != 0)
540 | throw new RangeError("Byte out of range");
541 | // Russian peasant multiplication
542 | let z = 0;
543 | for (let i = 7; i >= 0; i--) {
544 | z = (z << 1) ^ ((z >>> 7) * 0x11D);
545 | z ^= ((y >>> i) & 1) * x;
546 | }
547 | assert(z >>> 8 == 0);
548 | return z;
549 | }
550 | // Can only be called immediately after a light run is added, and
551 | // returns either 0, 1, or 2. A helper function for getPenaltyScore().
552 | finderPenaltyCountPatterns(runHistory) {
553 | const n = runHistory[1];
554 | assert(n <= this.size * 3);
555 | const core = n > 0 && runHistory[2] == n && runHistory[3] == n * 3 && runHistory[4] == n && runHistory[5] == n;
556 | return (core && runHistory[0] >= n * 4 && runHistory[6] >= n ? 1 : 0)
557 | + (core && runHistory[6] >= n * 4 && runHistory[0] >= n ? 1 : 0);
558 | }
559 | // Must be called at the end of a line (row or column) of modules. A helper function for getPenaltyScore().
560 | finderPenaltyTerminateAndCount(currentRunColor, currentRunLength, runHistory) {
561 | if (currentRunColor) { // Terminate dark run
562 | this.finderPenaltyAddHistory(currentRunLength, runHistory);
563 | currentRunLength = 0;
564 | }
565 | currentRunLength += this.size; // Add light border to final run
566 | this.finderPenaltyAddHistory(currentRunLength, runHistory);
567 | return this.finderPenaltyCountPatterns(runHistory);
568 | }
569 | // Pushes the given value to the front and drops the last value. A helper function for getPenaltyScore().
570 | finderPenaltyAddHistory(currentRunLength, runHistory) {
571 | if (runHistory[0] == 0)
572 | currentRunLength += this.size; // Add light border to initial run
573 | runHistory.pop();
574 | runHistory.unshift(currentRunLength);
575 | }
576 | }
577 | /*-- Constants and tables --*/
578 | // The minimum version number supported in the QR Code Model 2 standard.
579 | QrCode.MIN_VERSION = 1;
580 | // The maximum version number supported in the QR Code Model 2 standard.
581 | QrCode.MAX_VERSION = 40;
582 | // For use in getPenaltyScore(), when evaluating which mask is best.
583 | QrCode.PENALTY_N1 = 3;
584 | QrCode.PENALTY_N2 = 3;
585 | QrCode.PENALTY_N3 = 40;
586 | QrCode.PENALTY_N4 = 10;
587 | QrCode.ECC_CODEWORDS_PER_BLOCK = [
588 | // Version: (note that index 0 is for padding, and is set to an illegal value)
589 | //0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level
590 | [-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
591 | [-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28],
592 | [-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
593 | [-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], // High
594 | ];
595 | QrCode.NUM_ERROR_CORRECTION_BLOCKS = [
596 | // Version: (note that index 0 is for padding, and is set to an illegal value)
597 | //0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level
598 | [-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25],
599 | [-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49],
600 | [-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68],
601 | [-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81], // High
602 | ];
603 | qrcodegen.QrCode = QrCode;
604 | // Appends the given number of low-order bits of the given value
605 | // to the given buffer. Requires 0 <= len <= 31 and 0 <= val < 2^len.
606 | function appendBits(val, len, bb) {
607 | if (len < 0 || len > 31 || val >>> len != 0)
608 | throw new RangeError("Value out of range");
609 | for (let i = len - 1; i >= 0; i--) // Append bit by bit
610 | bb.push((val >>> i) & 1);
611 | }
612 | // Returns true iff the i'th bit of x is set to 1.
613 | function getBit(x, i) {
614 | return ((x >>> i) & 1) != 0;
615 | }
616 | // Throws an exception if the given condition is false.
617 | function assert(cond) {
618 | if (!cond)
619 | throw new Error("Assertion error");
620 | }
621 | /*---- Data segment class ----*/
622 | /*
623 | * A segment of character/binary/control data in a QR Code symbol.
624 | * Instances of this class are immutable.
625 | * The mid-level way to create a segment is to take the payload data
626 | * and call a static factory function such as QrSegment.makeNumeric().
627 | * The low-level way to create a segment is to custom-make the bit buffer
628 | * and call the QrSegment() constructor with appropriate values.
629 | * This segment class imposes no length restrictions, but QR Codes have restrictions.
630 | * Even in the most favorable conditions, a QR Code can only hold 7089 characters of data.
631 | * Any segment longer than this is meaningless for the purpose of generating QR Codes.
632 | */
633 | class QrSegment {
634 | /*-- Constructor (low level) and fields --*/
635 | // Creates a new QR Code segment with the given attributes and data.
636 | // The character count (numChars) must agree with the mode and the bit buffer length,
637 | // but the constraint isn't checked. The given bit buffer is cloned and stored.
638 | constructor(
639 | // The mode indicator of this segment.
640 | mode,
641 | // The length of this segment's unencoded data. Measured in characters for
642 | // numeric/alphanumeric/kanji mode, bytes for byte mode, and 0 for ECI mode.
643 | // Always zero or positive. Not the same as the data's bit length.
644 | numChars,
645 | // The data bits of this segment. Accessed through getData().
646 | bitData) {
647 | this.mode = mode;
648 | this.numChars = numChars;
649 | this.bitData = bitData;
650 | if (numChars < 0)
651 | throw new RangeError("Invalid argument");
652 | this.bitData = bitData.slice(); // Make defensive copy
653 | }
654 | /*-- Static factory functions (mid level) --*/
655 | // Returns a segment representing the given binary data encoded in
656 | // byte mode. All input byte arrays are acceptable. Any text string
657 | // can be converted to UTF-8 bytes and encoded as a byte mode segment.
658 | static makeBytes(data) {
659 | let bb = [];
660 | for (const b of data)
661 | appendBits(b, 8, bb);
662 | return new QrSegment(QrSegment.Mode.BYTE, data.length, bb);
663 | }
664 | // Returns a segment representing the given string of decimal digits encoded in numeric mode.
665 | static makeNumeric(digits) {
666 | if (!QrSegment.isNumeric(digits))
667 | throw new RangeError("String contains non-numeric characters");
668 | let bb = [];
669 | for (let i = 0; i < digits.length;) { // Consume up to 3 digits per iteration
670 | const n = Math.min(digits.length - i, 3);
671 | appendBits(parseInt(digits.substr(i, n), 10), n * 3 + 1, bb);
672 | i += n;
673 | }
674 | return new QrSegment(QrSegment.Mode.NUMERIC, digits.length, bb);
675 | }
676 | // Returns a segment representing the given text string encoded in alphanumeric mode.
677 | // The characters allowed are: 0 to 9, A to Z (uppercase only), space,
678 | // dollar, percent, asterisk, plus, hyphen, period, slash, colon.
679 | static makeAlphanumeric(text) {
680 | if (!QrSegment.isAlphanumeric(text))
681 | throw new RangeError("String contains unencodable characters in alphanumeric mode");
682 | let bb = [];
683 | let i;
684 | for (i = 0; i + 2 <= text.length; i += 2) { // Process groups of 2
685 | let temp = QrSegment.ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)) * 45;
686 | temp += QrSegment.ALPHANUMERIC_CHARSET.indexOf(text.charAt(i + 1));
687 | appendBits(temp, 11, bb);
688 | }
689 | if (i < text.length) // 1 character remaining
690 | appendBits(QrSegment.ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)), 6, bb);
691 | return new QrSegment(QrSegment.Mode.ALPHANUMERIC, text.length, bb);
692 | }
693 | // Returns a new mutable list of zero or more segments to represent the given Unicode text string.
694 | // The result may use various segment modes and switch modes to optimize the length of the bit stream.
695 | static makeSegments(text) {
696 | // Select the most efficient segment encoding automatically
697 | if (text == "")
698 | return [];
699 | else if (QrSegment.isNumeric(text))
700 | return [QrSegment.makeNumeric(text)];
701 | else if (QrSegment.isAlphanumeric(text))
702 | return [QrSegment.makeAlphanumeric(text)];
703 | else
704 | return [QrSegment.makeBytes(QrSegment.toUtf8ByteArray(text))];
705 | }
706 | // Returns a segment representing an Extended Channel Interpretation
707 | // (ECI) designator with the given assignment value.
708 | static makeEci(assignVal) {
709 | let bb = [];
710 | if (assignVal < 0)
711 | throw new RangeError("ECI assignment value out of range");
712 | else if (assignVal < (1 << 7))
713 | appendBits(assignVal, 8, bb);
714 | else if (assignVal < (1 << 14)) {
715 | appendBits(0b10, 2, bb);
716 | appendBits(assignVal, 14, bb);
717 | }
718 | else if (assignVal < 1000000) {
719 | appendBits(0b110, 3, bb);
720 | appendBits(assignVal, 21, bb);
721 | }
722 | else
723 | throw new RangeError("ECI assignment value out of range");
724 | return new QrSegment(QrSegment.Mode.ECI, 0, bb);
725 | }
726 | // Tests whether the given string can be encoded as a segment in numeric mode.
727 | // A string is encodable iff each character is in the range 0 to 9.
728 | static isNumeric(text) {
729 | return QrSegment.NUMERIC_REGEX.test(text);
730 | }
731 | // Tests whether the given string can be encoded as a segment in alphanumeric mode.
732 | // A string is encodable iff each character is in the following set: 0 to 9, A to Z
733 | // (uppercase only), space, dollar, percent, asterisk, plus, hyphen, period, slash, colon.
734 | static isAlphanumeric(text) {
735 | return QrSegment.ALPHANUMERIC_REGEX.test(text);
736 | }
737 | /*-- Methods --*/
738 | // Returns a new copy of the data bits of this segment.
739 | getData() {
740 | return this.bitData.slice(); // Make defensive copy
741 | }
742 | // (Package-private) Calculates and returns the number of bits needed to encode the given segments at
743 | // the given version. The result is infinity if a segment has too many characters to fit its length field.
744 | static getTotalBits(segs, version) {
745 | let result = 0;
746 | for (const seg of segs) {
747 | const ccbits = seg.mode.numCharCountBits(version);
748 | if (seg.numChars >= (1 << ccbits))
749 | return Infinity; // The segment's length doesn't fit the field's bit width
750 | result += 4 + ccbits + seg.bitData.length;
751 | }
752 | return result;
753 | }
754 | // Returns a new array of bytes representing the given string encoded in UTF-8.
755 | static toUtf8ByteArray(str) {
756 | str = encodeURI(str);
757 | let result = [];
758 | for (let i = 0; i < str.length; i++) {
759 | if (str.charAt(i) != "%")
760 | result.push(str.charCodeAt(i));
761 | else {
762 | result.push(parseInt(str.substr(i + 1, 2), 16));
763 | i += 2;
764 | }
765 | }
766 | return result;
767 | }
768 | }
769 | /*-- Constants --*/
770 | // Describes precisely all strings that are encodable in numeric mode.
771 | QrSegment.NUMERIC_REGEX = /^[0-9]*$/;
772 | // Describes precisely all strings that are encodable in alphanumeric mode.
773 | QrSegment.ALPHANUMERIC_REGEX = /^[A-Z0-9 $%*+.\/:-]*$/;
774 | // The set of all legal characters in alphanumeric mode,
775 | // where each character value maps to the index in the string.
776 | QrSegment.ALPHANUMERIC_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
777 | qrcodegen.QrSegment = QrSegment;
778 | })(qrcodegen || (qrcodegen = {}));
779 | /*---- Public helper enumeration ----*/
780 | (function (qrcodegen) {
781 | var QrCode;
782 | (function (QrCode) {
783 | /*
784 | * The error correction level in a QR Code symbol. Immutable.
785 | */
786 | class Ecc {
787 | /*-- Constructor and fields --*/
788 | constructor(
789 | // In the range 0 to 3 (unsigned 2-bit integer).
790 | ordinal,
791 | // (Package-private) In the range 0 to 3 (unsigned 2-bit integer).
792 | formatBits) {
793 | this.ordinal = ordinal;
794 | this.formatBits = formatBits;
795 | }
796 | }
797 | /*-- Constants --*/
798 | Ecc.LOW = new Ecc(0, 1); // The QR Code can tolerate about 7% erroneous codewords
799 | Ecc.MEDIUM = new Ecc(1, 0); // The QR Code can tolerate about 15% erroneous codewords
800 | Ecc.QUARTILE = new Ecc(2, 3); // The QR Code can tolerate about 25% erroneous codewords
801 | Ecc.HIGH = new Ecc(3, 2); // The QR Code can tolerate about 30% erroneous codewords
802 | QrCode.Ecc = Ecc;
803 | })(QrCode = qrcodegen.QrCode || (qrcodegen.QrCode = {}));
804 | })(qrcodegen || (qrcodegen = {}));
805 | /*---- Public helper enumeration ----*/
806 | (function (qrcodegen) {
807 | var QrSegment;
808 | (function (QrSegment) {
809 | /*
810 | * Describes how a segment's data bits are interpreted. Immutable.
811 | */
812 | class Mode {
813 | /*-- Constructor and fields --*/
814 | constructor(
815 | // The mode indicator bits, which is a uint4 value (range 0 to 15).
816 | modeBits,
817 | // Number of character count bits for three different version ranges.
818 | numBitsCharCount) {
819 | this.modeBits = modeBits;
820 | this.numBitsCharCount = numBitsCharCount;
821 | }
822 | /*-- Method --*/
823 | // (Package-private) Returns the bit width of the character count field for a segment in
824 | // this mode in a QR Code at the given version number. The result is in the range [0, 16].
825 | numCharCountBits(ver) {
826 | return this.numBitsCharCount[Math.floor((ver + 7) / 17)];
827 | }
828 | }
829 | /*-- Constants --*/
830 | Mode.NUMERIC = new Mode(0x1, [10, 12, 14]);
831 | Mode.ALPHANUMERIC = new Mode(0x2, [9, 11, 13]);
832 | Mode.BYTE = new Mode(0x4, [8, 16, 16]);
833 | Mode.KANJI = new Mode(0x8, [8, 10, 12]);
834 | Mode.ECI = new Mode(0x7, [0, 0, 0]);
835 | QrSegment.Mode = Mode;
836 | })(QrSegment = qrcodegen.QrSegment || (qrcodegen.QrSegment = {}));
837 | })(qrcodegen || (qrcodegen = {}));
838 |
--------------------------------------------------------------------------------
/libs/storage.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { Gio, GLib } = imports.gi;
4 |
5 | const ExtensionUtils = imports.misc.extensionUtils;
6 | const Me = ExtensionUtils.getCurrentExtension();
7 |
8 | var Storage = class {
9 | constructor() {
10 | this._stateDir = Gio.File.new_for_path(GLib.build_filenamev([
11 | GLib.get_user_state_dir(),
12 | Me.uuid,
13 | ]));
14 | this._storageFile = this._stateDir.get_child(`storage.json`);
15 | }
16 |
17 | loadEntries() {
18 | if (!this._storageFile.query_exists(null)) {
19 | return Promise.resolve([]);
20 | }
21 | return new Promise(async (resolve, reject) => {
22 | try {
23 | const entries = JSON.parse(await this._loadFile(this._storageFile));
24 | resolve(entries);
25 | } catch (error) {
26 | reject(new Error(`Failed to load storage. ${error.message}`));
27 | }
28 | });
29 | }
30 |
31 | saveEntries(entries) {
32 | return new Promise(async (resolve, reject) => {
33 | try {
34 | const text = JSON.stringify(entries, [`pinned`, `id`, `sortKey`], 2);
35 | await this._saveFile(this._storageFile, text);
36 | resolve();
37 | } catch (error) {
38 | reject(new Error(`Failed to save storage. ${error.message}`));
39 | }
40 | });
41 | }
42 |
43 | loadEntryContent(entry) {
44 | return new Promise(async (resolve, reject) => {
45 | const file = this._stateDir.get_child(entry.id.toString());
46 | try {
47 | entry.text = await this._loadFile(file);
48 | resolve();
49 | } catch (error) {
50 | reject(new Error(`Failed to load entry content ${entry.id}. ${error.message}`));
51 | }
52 | });
53 | }
54 |
55 | saveEntryContent(entry) {
56 | return new Promise(async (resolve, reject) => {
57 | const file = this._stateDir.get_child(entry.id.toString());
58 | try {
59 | await this._saveFile(file, entry.text);
60 | resolve();
61 | } catch (error) {
62 | reject(new Error(`Failed to save entry content ${entry.id}. ${error.message}`));
63 | }
64 | });
65 | }
66 |
67 | deleteEntryContent(entry) {
68 | const file = this._stateDir.get_child(entry.id.toString());
69 | if (!file.query_exists(null)) {
70 | return Promise.resolve();
71 | }
72 | return new Promise(async (resolve, reject) => {
73 | file.delete_async(GLib.PRIORITY_DEFAULT, null, (...[, result]) => {
74 | try {
75 | if (!file.delete_finish(result)) {
76 | throw new Error(`Uknnown error`);
77 | }
78 | resolve();
79 | } catch (error) {
80 | reject(new Error(`Failed to delete entry content ${entry.id}. ${error.message}`));
81 | }
82 | });
83 | });
84 | }
85 |
86 | _loadFile(file) {
87 | return new Promise((resolve, reject) => {
88 | file.load_contents_async(null, (...[, result]) => {
89 | try {
90 | const [ok, bytes] = file.load_contents_finish(result);
91 | if (!ok) {
92 | throw new Error(`Uknnown error`);
93 | }
94 | const text = new TextDecoder().decode(bytes);
95 | resolve(text);
96 | } catch (error) {
97 | reject(error);
98 | }
99 | });
100 | });
101 | }
102 |
103 | _saveFile(file, content) {
104 | return new Promise(async (resolve, reject) => {
105 | const parentDir = file.get_parent();
106 | try {
107 | if (!parentDir.query_exists(null) && !parentDir.make_directory_with_parents(null)) {
108 | throw new Error(`Failed to create parent directory`);
109 | }
110 | await new Promise((resolve, reject) => {
111 | file.replace_contents_bytes_async(
112 | new GLib.Bytes(content),
113 | null,
114 | false,
115 | Gio.FileCreateFlags.REPLACE_DESTINATION,
116 | null,
117 | (...[, result]) => {
118 | try {
119 | const [ok] = file.replace_contents_finish(result);
120 | if (!ok) {
121 | throw new Error(`Uknnown error`);
122 | }
123 | resolve();
124 | } catch (error) {
125 | reject(error);
126 | }
127 | }
128 | );
129 | });
130 | resolve();
131 | } catch (error) {
132 | reject(error);
133 | }
134 | });
135 | }
136 | };
137 |
--------------------------------------------------------------------------------
/libs/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const ExtensionUtils = imports.misc.extensionUtils;
4 | const Me = ExtensionUtils.getCurrentExtension();
5 |
6 | var _ = function(text, context) {
7 | return context ? ExtensionUtils.pgettext(context, text) : ExtensionUtils.gettext(text);
8 | };
9 |
10 | var log = function(text) {
11 | console.log(`${Me.uuid}: ${text}`);
12 | };
13 |
14 | const knownColors = new Map();
15 |
16 | var ColorParser = {
17 | parse(color) {
18 | if (!color) {
19 | return null;
20 | }
21 |
22 | color = color.trim().toLowerCase();
23 |
24 | if (knownColors.size === 0) {
25 | // https://htmlcolorcodes.com/color-names/
26 | knownColors.set(`aliceblue`, [240, 248, 255]);
27 | knownColors.set(`antiquewhite`, [250, 235, 215]);
28 | knownColors.set(`aqua`, [0, 255, 255]);
29 | knownColors.set(`aquamarine`, [127, 255, 212]);
30 | knownColors.set(`azure`, [240, 255, 255]);
31 | knownColors.set(`beige`, [245, 245, 220]);
32 | knownColors.set(`bisque`, [255, 228, 196]);
33 | knownColors.set(`black`, [0, 0, 0]);
34 | knownColors.set(`blanchedalmond`, [255, 235, 205]);
35 | knownColors.set(`blue`, [0, 0, 255]);
36 | knownColors.set(`blueviolet`, [138, 43, 226]);
37 | knownColors.set(`brown`, [165, 42, 42]);
38 | knownColors.set(`burlywood`, [222, 184, 135]);
39 | knownColors.set(`cadetblue`, [95, 158, 160]);
40 | knownColors.set(`chartreuse`, [127, 255, 0]);
41 | knownColors.set(`chocolate`, [210, 105, 30]);
42 | knownColors.set(`coral`, [255, 127, 80]);
43 | knownColors.set(`cornflowerblue`, [100, 149, 237]);
44 | knownColors.set(`cornsilk`, [255, 248, 220]);
45 | knownColors.set(`crimson`, [220, 20, 60]);
46 | knownColors.set(`cyan`, [0, 255, 255]);
47 | knownColors.set(`darkblue`, [0, 0, 139]);
48 | knownColors.set(`darkcyan`, [0, 139, 139]);
49 | knownColors.set(`darkgoldenrod`, [184, 134, 11]);
50 | knownColors.set(`darkgray`, [169, 169, 169]);
51 | knownColors.set(`darkgreen`, [0, 100, 0]);
52 | knownColors.set(`darkgrey`, [169, 169, 169]);
53 | knownColors.set(`darkkhaki`, [189, 183, 107]);
54 | knownColors.set(`darkmagenta`, [139, 0, 139]);
55 | knownColors.set(`darkolivegreen`, [85, 107, 47]);
56 | knownColors.set(`darkorange`, [255, 140, 0]);
57 | knownColors.set(`darkorchid`, [153, 50, 204]);
58 | knownColors.set(`darkred`, [139, 0, 0]);
59 | knownColors.set(`darksalmon`, [233, 150, 122]);
60 | knownColors.set(`darkseagreen`, [143, 188, 139]);
61 | knownColors.set(`darkslateblue`, [72, 61, 139]);
62 | knownColors.set(`darkslategray`, [47, 79, 79]);
63 | knownColors.set(`darkslategrey`, [47, 79, 79]);
64 | knownColors.set(`darkturquoise`, [0, 206, 209]);
65 | knownColors.set(`darkviolet`, [148, 0, 211]);
66 | knownColors.set(`deeppink`, [255, 20, 147]);
67 | knownColors.set(`deepskyblue`, [0, 191, 255]);
68 | knownColors.set(`dimgray`, [105, 105, 105]);
69 | knownColors.set(`dimgrey`, [105, 105, 105]);
70 | knownColors.set(`dodgerblue`, [30, 144, 255]);
71 | knownColors.set(`firebrick`, [178, 34, 34]);
72 | knownColors.set(`floralwhite`, [255, 250, 240]);
73 | knownColors.set(`forestgreen`, [34, 139, 34]);
74 | knownColors.set(`fuchsia`, [255, 0, 255]);
75 | knownColors.set(`gainsboro`, [220, 220, 220]);
76 | knownColors.set(`ghostwhite`, [248, 248, 255]);
77 | knownColors.set(`gold`, [255, 215, 0]);
78 | knownColors.set(`goldenrod`, [218, 165, 32]);
79 | knownColors.set(`gray`, [128, 128, 128]);
80 | knownColors.set(`green`, [0, 128, 0]);
81 | knownColors.set(`greenyellow`, [173, 255, 47]);
82 | knownColors.set(`grey`, [128, 128, 128]);
83 | knownColors.set(`honeydew`, [240, 255, 240]);
84 | knownColors.set(`hotpink`, [255, 105, 180]);
85 | knownColors.set(`indianred`, [205, 92, 92]);
86 | knownColors.set(`indigo`, [75, 0, 130]);
87 | knownColors.set(`ivory`, [255, 255, 240]);
88 | knownColors.set(`khaki`, [240, 230, 140]);
89 | knownColors.set(`lavender`, [230, 230, 250]);
90 | knownColors.set(`lavenderblush`, [255, 240, 245]);
91 | knownColors.set(`lawngreen`, [124, 252, 0]);
92 | knownColors.set(`lemonchiffon`, [255, 250, 205]);
93 | knownColors.set(`lightblue`, [173, 216, 230]);
94 | knownColors.set(`lightcoral`, [240, 128, 128]);
95 | knownColors.set(`lightcyan`, [224, 255, 255]);
96 | knownColors.set(`lightgoldenrodyellow`, [250, 250, 210]);
97 | knownColors.set(`lightgray`, [211, 211, 211]);
98 | knownColors.set(`lightgreen`, [144, 238, 144]);
99 | knownColors.set(`lightgrey`, [211, 211, 211]);
100 | knownColors.set(`lightpink`, [255, 182, 193]);
101 | knownColors.set(`lightsalmon`, [255, 160, 122]);
102 | knownColors.set(`lightseagreen`, [32, 178, 170]);
103 | knownColors.set(`lightskyblue`, [135, 206, 250]);
104 | knownColors.set(`lightslategray`, [119, 136, 153]);
105 | knownColors.set(`lightslategrey`, [119, 136, 153]);
106 | knownColors.set(`lightsteelblue`, [176, 196, 222]);
107 | knownColors.set(`lightyellow`, [255, 255, 224]);
108 | knownColors.set(`lime`, [0, 255, 0]);
109 | knownColors.set(`limegreen`, [50, 205, 50]);
110 | knownColors.set(`linen`, [250, 240, 230]);
111 | knownColors.set(`magenta`, [255, 0, 255]);
112 | knownColors.set(`maroon`, [128, 0, 0]);
113 | knownColors.set(`mediumaquamarine`, [102, 205, 170]);
114 | knownColors.set(`mediumblue`, [0, 0, 205]);
115 | knownColors.set(`mediumorchid`, [186, 85, 211]);
116 | knownColors.set(`mediumpurple`, [147, 112, 219]);
117 | knownColors.set(`mediumseagreen`, [60, 179, 113]);
118 | knownColors.set(`mediumslateblue`, [123, 104, 238]);
119 | knownColors.set(`mediumspringgreen`, [0, 250, 154]);
120 | knownColors.set(`mediumturquoise`, [72, 209, 204]);
121 | knownColors.set(`mediumvioletred`, [199, 21, 133]);
122 | knownColors.set(`midnightblue`, [25, 25, 112]);
123 | knownColors.set(`mintcream`, [245, 255, 250]);
124 | knownColors.set(`mistyrose`, [255, 228, 225]);
125 | knownColors.set(`moccasin`, [255, 228, 181]);
126 | knownColors.set(`navajowhite`, [255, 222, 173]);
127 | knownColors.set(`navy`, [0, 0, 128]);
128 | knownColors.set(`oldlace`, [253, 245, 230]);
129 | knownColors.set(`olive`, [128, 128, 0]);
130 | knownColors.set(`olivedrab`, [107, 142, 35]);
131 | knownColors.set(`orange`, [255, 165, 0]);
132 | knownColors.set(`orangered`, [255, 69, 0]);
133 | knownColors.set(`orchid`, [218, 112, 214]);
134 | knownColors.set(`palegoldenrod`, [238, 232, 170]);
135 | knownColors.set(`palegreen`, [152, 251, 152]);
136 | knownColors.set(`paleturquoise`, [175, 238, 238]);
137 | knownColors.set(`palevioletred`, [219, 112, 147]);
138 | knownColors.set(`papayawhip`, [255, 239, 213]);
139 | knownColors.set(`peachpuff`, [255, 218, 185]);
140 | knownColors.set(`peru`, [205, 133, 63]);
141 | knownColors.set(`pink`, [255, 192, 203]);
142 | knownColors.set(`plum`, [221, 160, 221]);
143 | knownColors.set(`powderblue`, [176, 224, 230]);
144 | knownColors.set(`purple`, [128, 0, 128]);
145 | knownColors.set(`rebeccapurple`, [102, 51, 153]);
146 | knownColors.set(`red`, [255, 0, 0]);
147 | knownColors.set(`rosybrown`, [188, 143, 143]);
148 | knownColors.set(`royalblue`, [65, 105, 225]);
149 | knownColors.set(`saddlebrown`, [139, 69, 19]);
150 | knownColors.set(`salmon`, [250, 128, 114]);
151 | knownColors.set(`sandybrown`, [244, 164, 96]);
152 | knownColors.set(`seagreen`, [46, 139, 87]);
153 | knownColors.set(`seashell`, [255, 245, 238]);
154 | knownColors.set(`sienna`, [160, 82, 45]);
155 | knownColors.set(`silver`, [192, 192, 192]);
156 | knownColors.set(`skyblue`, [135, 206, 235]);
157 | knownColors.set(`slateblue`, [106, 90, 205]);
158 | knownColors.set(`slategray`, [112, 128, 144]);
159 | knownColors.set(`slategrey`, [112, 128, 144]);
160 | knownColors.set(`snow`, [255, 250, 250]);
161 | knownColors.set(`springgreen`, [0, 255, 127]);
162 | knownColors.set(`steelblue`, [70, 130, 180]);
163 | knownColors.set(`tan`, [210, 180, 140]);
164 | knownColors.set(`teal`, [0, 128, 128]);
165 | knownColors.set(`thistle`, [216, 191, 216]);
166 | knownColors.set(`tomato`, [255, 99, 71]);
167 | knownColors.set(`turquoise`, [64, 224, 208]);
168 | knownColors.set(`violet`, [238, 130, 238]);
169 | knownColors.set(`wheat`, [245, 222, 179]);
170 | knownColors.set(`white`, [255, 255, 255]);
171 | knownColors.set(`whitesmoke`, [245, 245, 245]);
172 | knownColors.set(`yellow`, [255, 255, 0]);
173 | knownColors.set(`yellowgreen`, [154, 205, 50]);
174 | }
175 |
176 | let rgba = knownColors.get(color);
177 | if (rgba) {
178 | return rgba;
179 | }
180 |
181 | const rgbColorRegExp = /^rgba?\((\d{1,3})(?:,\s*|\s+)(\d{1,3})(?:,\s*|\s+)(\d{1,3})(?:(?:,\s*|\s+)(\d{1,3}))?\)$/;
182 | const rgbColorMatch = color.match(rgbColorRegExp);
183 | if (rgbColorMatch) {
184 | return rgbColorMatch.slice(1, 5); // RGBA
185 | }
186 |
187 | const hexColorRegExp = /^#((?:[\da-f]{3}){1,2}|(?:[\da-f]{4}){1,2})$/;
188 | const hexColorMatch = color.match(hexColorRegExp);
189 | if (hexColorMatch) {
190 | const hexColor = hexColorMatch[1];
191 | switch (hexColor.length) {
192 | case 3:
193 | case 4: {
194 | rgba = [
195 | parseInt(hexColor[0] + hexColor[0], 16),
196 | parseInt(hexColor[1] + hexColor[1], 16),
197 | parseInt(hexColor[2] + hexColor[2], 16),
198 | ];
199 | if (hexColor.length === 4) {
200 | rgba.push(parseInt(hexColor[3] + hexColor[3], 16));
201 | }
202 | return rgba;
203 | }
204 | case 6:
205 | case 8: {
206 | rgba = [
207 | parseInt(hexColor.slice(0, 2), 16),
208 | parseInt(hexColor.slice(2, 4), 16),
209 | parseInt(hexColor.slice(4, 6), 16),
210 | ];
211 | if (hexColor.length === 8) {
212 | rgba.push(parseInt(hexColor.slice(6, 8), 16));
213 | }
214 | return rgba;
215 | }
216 | default:
217 | break;
218 | }
219 | }
220 |
221 | return null;
222 | },
223 | };
224 |
225 | const predefinedSearchEngines = [];
226 |
227 | var SearchEngines = {
228 | get(preferences) {
229 | // use lazy loading to ensure that translations are initialized
230 | if (predefinedSearchEngines.length === 0) {
231 | predefinedSearchEngines.push(
232 | { name: `duckduckgo`, title: _(`DuckDuckGo`), url: `https://duckduckgo.com/?q=%s` },
233 | { name: `brave`, title: _(`Brave`, `Brave search engine`), url: `https://search.brave.com/search?q=%s` },
234 | { name: `google`, title: _(`Google`), url: `https://www.google.com/search?q=%s` },
235 | { name: `bing`, title: _(`Bing`), url: `https://www.bing.com/search?q=%s` },
236 | { name: `baidu`, title: _(`Baidu`, `Baidu search engine`), url: `https://www.baidu.com/s?wd=%s` },
237 | { name: `yahoo`, title: _(`Yahoo`), url: `https://search.yahoo.com/search?p=%s` },
238 | { name: `ecosia`, title: _(`Ecosia`, `Ecosia search engine`), url: `https://www.ecosia.org/search?q=%s` },
239 | { name: `ask`, title: _(`Ask`, `Ask.com search engine`), url: `https://www.ask.com/web?q=%s` },
240 | { name: `aol`, title: _(`AOL`, `AOL search engine`), url: `https://search.aol.com/aol/search?q=%s` },
241 | { name: `naver`, title: _(`Naver`, `Naver search engine`), url: `https://search.naver.com/search.naver?query=%s` },
242 | );
243 | }
244 |
245 | const searchEngines = [
246 | ...predefinedSearchEngines,
247 | {
248 | name: `custom`,
249 | title: _(`Other`, `Other search engine`),
250 | url: preferences.customWebSearchUrl,
251 | },
252 | ];
253 | searchEngines.find = (engineName) => {
254 | return Object.getPrototypeOf(searchEngines).find.call(searchEngines, (engine) => {
255 | return engine.name === engineName;
256 | });
257 | };
258 | searchEngines.findIndex = (engineName) => {
259 | return Object.getPrototypeOf(searchEngines).findIndex.call(searchEngines, (engine) => {
260 | return engine.name === engineName;
261 | });
262 | };
263 | searchEngines.sort = () => {
264 | Object.getPrototypeOf(searchEngines).sort.call(searchEngines, (engine1, engine2) => {
265 | if (engine1.name === `custom`) {
266 | return 1;
267 | }
268 | if (engine2.name === `custom`) {
269 | return -1;
270 | }
271 | return engine1.title.localeCompare(engine2.title);
272 | });
273 | };
274 |
275 | return searchEngines;
276 | },
277 | };
278 |
--------------------------------------------------------------------------------
/metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "uuid": "clipman@popov895.ukr.net",
3 | "name": "Clipman",
4 | "description": "Simple but powerful clipboard manager.",
5 | "version": 57,
6 | "settings-schema": "org.gnome.shell.extensions.clipman",
7 | "shell-version": [
8 | "42",
9 | "43",
10 | "44"
11 | ],
12 | "url": "https://github.com/popov895/Clipman",
13 | "donations": {
14 | "buymeacoffee": "popov895a"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/pack.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | gnome-extensions pack --extra-source=icons --extra-source=libs --out-dir=..
4 |
--------------------------------------------------------------------------------
/po.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | mkdir -p po
4 |
5 | xgettext `find . -name \*.js` --keyword=_:1,2c --from-code=UTF-8 --output=po/example.pot
6 |
7 | for file in po/*.po
8 | do
9 | msgmerge -Uq --backup=off "$file" po/example.pot
10 | done
11 |
--------------------------------------------------------------------------------
/po/cs.po:
--------------------------------------------------------------------------------
1 | # Czech translation for Clipman
2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the PACKAGE package.
4 | # Amerey.eu , 2023.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2023-11-08 22:01+0200\n"
11 | "PO-Revision-Date: 2024-01-14 20:18+0100\n"
12 | "Last-Translator: Amerey \n"
13 | "Language-Team: \n"
14 | "Language: cs\n"
15 | "MIME-Version: 1.0\n"
16 | "Content-Type: text/plain; charset=UTF-8\n"
17 | "Content-Transfer-Encoding: 8bit\n"
18 | "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n>=2 && n<=4 ? 1 : 2);\n"
19 | "X-Generator: Poedit 3.4\n"
20 |
21 | #: extension.js:112
22 | msgid "Failed to generate QR code"
23 | msgstr "Vygenerování QR kódu se nezdařilo"
24 |
25 | #: extension.js:119
26 | msgctxt "Close dialog"
27 | msgid "Close"
28 | msgstr "Zavřít"
29 |
30 | #: extension.js:188
31 | msgid "Type to search..."
32 | msgstr "Pište pro vyhledávání ..."
33 |
34 | #: extension.js:207
35 | msgid "No Matches"
36 | msgstr "Žádné shody"
37 |
38 | #: extension.js:616
39 | msgid "Private Mode is On"
40 | msgstr "Soukromý režim je zapnutý"
41 |
42 | #: extension.js:622
43 | msgid "History is Empty"
44 | msgstr "Historie je prázdná"
45 |
46 | #: extension.js:644
47 | msgid "Clear History"
48 | msgstr "Vymazat historii"
49 |
50 | #: extension.js:656
51 | msgid "Private Mode"
52 | msgstr "Soukromý režim"
53 |
54 | #: extension.js:686
55 | msgctxt "Open settings"
56 | msgid "Settings"
57 | msgstr "Nastavení"
58 |
59 | #: extension.js:810 extension.js:835 extension.js:841
60 | msgctxt "Open URL"
61 | msgid "Open"
62 | msgstr "Otevřít"
63 |
64 | #: extension.js:849
65 | msgid "Compose an Email"
66 | msgstr "Napsat e-mail"
67 |
68 | #: extension.js:857 extension.js:863
69 | msgid "Make a Call"
70 | msgstr "Zavolat"
71 |
72 | #: extension.js:883
73 | msgid "Search the Web"
74 | msgstr "Prohledat internet"
75 |
76 | #: extension.js:888
77 | msgid "Send via Email"
78 | msgstr "Odeslat e-mailem"
79 |
80 | #: extension.js:893
81 | msgid "Share Online"
82 | msgstr "Sdílet online"
83 |
84 | #: extension.js:898
85 | msgid "Show QR Code"
86 | msgstr "Zobrazit QR kód"
87 |
88 | #: extension.js:916
89 | #, javascript-format
90 | msgid "Failed to launch URI \"%s\""
91 | msgstr "Nepodařilo se spustit URI \"%s\""
92 |
93 | #: extension.js:925 extension.js:938
94 | msgid "Failed to search the web"
95 | msgstr "Vyhledávání na webu se nezdařilo"
96 |
97 | #: extension.js:925
98 | msgid "Unknown search engine"
99 | msgstr "Neznámý vyhledávač"
100 |
101 | #: extension.js:938
102 | #, javascript-format
103 | msgid "Invalid search URL \"%s\""
104 | msgstr "Neplatná adresa URL pro vyhledávání „%s“"
105 |
106 | #: extension.js:983
107 | msgid "The text was successfully shared online"
108 | msgstr "Text byl úspěšně sdílen online"
109 |
110 | #: extension.js:985
111 | msgid "Failed to share the text online"
112 | msgstr "Sdílení textu online se nezdařilo"
113 |
114 | #: prefs.js:15
115 | msgid "Press Backspace to clear shortcut or Esc to cancel"
116 | msgstr "Stiskněte Backspace pro vymazání zástupce nebo Esc pro zrušení"
117 |
118 | #: prefs.js:16
119 | msgid "Enter a new shortcut"
120 | msgstr "Zadejte novou zkratku"
121 |
122 | #: prefs.js:71
123 | msgctxt "Keyboard shortcut is disabled"
124 | msgid "Disabled"
125 | msgstr "Vypnuto"
126 |
127 | #: prefs.js:123
128 | msgid "History size"
129 | msgstr "Velikost historie"
130 |
131 | #: prefs.js:129
132 | msgctxt "Don't keep entries"
133 | msgid "None"
134 | msgstr "Žádné"
135 |
136 | #: prefs.js:130
137 | msgctxt "Keep only pinned entries"
138 | msgid "Pinned"
139 | msgstr "Připnuté"
140 |
141 | #: prefs.js:131
142 | msgctxt "Keep all entries"
143 | msgid "All"
144 | msgstr "Vše"
145 |
146 | #: prefs.js:145
147 | msgid "History entries to keep"
148 | msgstr "Záznamy historie, které se mají uchovávat"
149 |
150 | #: prefs.js:161
151 | msgid "Show leading and trailing whitespace"
152 | msgstr "Zobrazit mezery na začátku a na konci"
153 |
154 | #: prefs.js:177
155 | msgid "Show color preview"
156 | msgstr "Zobrazit náhled barev"
157 |
158 | #: prefs.js:183
159 | msgctxt "Small menu size"
160 | msgid "Small"
161 | msgstr "Malá"
162 |
163 | #: prefs.js:184
164 | msgctxt "Medium menu size"
165 | msgid "Medium"
166 | msgstr "Střední"
167 |
168 | #: prefs.js:185
169 | msgctxt "Large menu size"
170 | msgid "Large"
171 | msgstr "Velká"
172 |
173 | #: prefs.js:199
174 | msgid "Maximum menu size"
175 | msgstr "Maximální velikost nabídky"
176 |
177 | #: prefs.js:204
178 | msgctxt "General options"
179 | msgid "General"
180 | msgstr "Obecné"
181 |
182 | #: prefs.js:213
183 | #, javascript-format
184 | msgid "URL with %s in place of query"
185 | msgstr "Adresa URL s %s na místě dotazu"
186 |
187 | #: prefs.js:226
188 | msgid "Search URL"
189 | msgstr "Vyhledat adresu URL"
190 |
191 | #: prefs.js:267
192 | msgid "Search Engine"
193 | msgstr "Vyhledávač"
194 |
195 | #: prefs.js:272
196 | msgid "Web Search"
197 | msgstr "Vyhledávání na webu"
198 |
199 | #: prefs.js:294
200 | msgctxt "The number of days to keep the shared text"
201 | msgid "Days to keep the shared text"
202 | msgstr "Počet dnů, do kterých bude sdílený text zachován"
203 |
204 | #: prefs.js:299
205 | msgid "Sharing Online"
206 | msgstr "Sdílení online"
207 |
208 | #: prefs.js:304
209 | msgid "Keyboard Shortcuts"
210 | msgstr "Klávesové zkratky"
211 |
212 | #: prefs.js:307
213 | msgid "Toggle menu"
214 | msgstr "Skrýt/Zobrazit nabídku"
215 |
216 | #: prefs.js:312
217 | msgid "Toggle private mode"
218 | msgstr "Soukromý režim"
219 |
220 | #: prefs.js:317
221 | msgid "Clear history"
222 | msgstr "Vymazat historii"
223 |
224 | #: libs/utils.js:232
225 | msgid "DuckDuckGo"
226 | msgstr "DuckDuckGo"
227 |
228 | #: libs/utils.js:233
229 | msgctxt "Brave search engine"
230 | msgid "Brave"
231 | msgstr "Brave"
232 |
233 | #: libs/utils.js:234
234 | msgid "Google"
235 | msgstr "Google"
236 |
237 | #: libs/utils.js:235
238 | msgid "Bing"
239 | msgstr "Bing"
240 |
241 | #: libs/utils.js:236
242 | msgctxt "Baidu search engine"
243 | msgid "Baidu"
244 | msgstr "Baidu"
245 |
246 | #: libs/utils.js:237
247 | msgid "Yahoo"
248 | msgstr "Yahoo"
249 |
250 | #: libs/utils.js:238
251 | msgctxt "Ecosia search engine"
252 | msgid "Ecosia"
253 | msgstr "Ecosia"
254 |
255 | #: libs/utils.js:239
256 | msgctxt "Ask.com search engine"
257 | msgid "Ask"
258 | msgstr "Ask"
259 |
260 | #: libs/utils.js:240
261 | msgctxt "AOL search engine"
262 | msgid "AOL"
263 | msgstr "AOL"
264 |
265 | #: libs/utils.js:241
266 | msgctxt "Naver search engine"
267 | msgid "Naver"
268 | msgstr "Naver"
269 |
270 | #: libs/utils.js:249
271 | msgctxt "Other search engine"
272 | msgid "Other"
273 | msgstr "Jiný"
274 |
275 | #~ msgctxt "Change current shortcut"
276 | #~ msgid "Change"
277 | #~ msgstr "Změnit"
278 |
--------------------------------------------------------------------------------
/po/de.po:
--------------------------------------------------------------------------------
1 | # German translation file for Clipman.
2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the Clipman package.
4 | # Onno Giesmann , 2022.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2023-11-08 22:01+0200\n"
11 | "PO-Revision-Date: 2023-07-03 13:42+0300\n"
12 | "Last-Translator: Onno Giesmann \n"
13 | "Language-Team: \n"
14 | "Language: de\n"
15 | "MIME-Version: 1.0\n"
16 | "Content-Type: text/plain; charset=UTF-8\n"
17 | "Content-Transfer-Encoding: 8bit\n"
18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n"
19 | "X-Generator: Poedit 3.2.2\n"
20 |
21 | #: extension.js:112
22 | msgid "Failed to generate QR code"
23 | msgstr "QR-Code konnte nicht erzeugt werden"
24 |
25 | #: extension.js:119
26 | msgctxt "Close dialog"
27 | msgid "Close"
28 | msgstr "Schließen"
29 |
30 | #: extension.js:188
31 | msgid "Type to search..."
32 | msgstr "Tippen, um zu suchen…"
33 |
34 | #: extension.js:207
35 | msgid "No Matches"
36 | msgstr "Keine Übereinstimmungen"
37 |
38 | #: extension.js:616
39 | msgid "Private Mode is On"
40 | msgstr "Privater Modus ist aktiviert"
41 |
42 | #: extension.js:622
43 | msgid "History is Empty"
44 | msgstr "Chronik ist leer"
45 |
46 | #: extension.js:644
47 | msgid "Clear History"
48 | msgstr "Chronik löschen"
49 |
50 | #: extension.js:656
51 | msgid "Private Mode"
52 | msgstr "Privater Modus"
53 |
54 | #: extension.js:686
55 | msgctxt "Open settings"
56 | msgid "Settings"
57 | msgstr "Einstellungen"
58 |
59 | #: extension.js:810 extension.js:835 extension.js:841
60 | msgctxt "Open URL"
61 | msgid "Open"
62 | msgstr ""
63 |
64 | #: extension.js:849
65 | msgid "Compose an Email"
66 | msgstr ""
67 |
68 | #: extension.js:857 extension.js:863
69 | msgid "Make a Call"
70 | msgstr ""
71 |
72 | #: extension.js:883
73 | msgid "Search the Web"
74 | msgstr ""
75 |
76 | #: extension.js:888
77 | msgid "Send via Email"
78 | msgstr ""
79 |
80 | #: extension.js:893
81 | msgid "Share Online"
82 | msgstr ""
83 |
84 | #: extension.js:898
85 | msgid "Show QR Code"
86 | msgstr ""
87 |
88 | #: extension.js:916
89 | #, javascript-format
90 | msgid "Failed to launch URI \"%s\""
91 | msgstr ""
92 |
93 | #: extension.js:925 extension.js:938
94 | #, fuzzy
95 | msgid "Failed to search the web"
96 | msgstr "QR-Code konnte nicht erzeugt werden"
97 |
98 | #: extension.js:925
99 | msgid "Unknown search engine"
100 | msgstr ""
101 |
102 | #: extension.js:938
103 | #, javascript-format
104 | msgid "Invalid search URL \"%s\""
105 | msgstr ""
106 |
107 | #: extension.js:983
108 | msgid "The text was successfully shared online"
109 | msgstr ""
110 |
111 | #: extension.js:985
112 | msgid "Failed to share the text online"
113 | msgstr ""
114 |
115 | #: prefs.js:15
116 | msgid "Press Backspace to clear shortcut or Esc to cancel"
117 | msgstr ""
118 |
119 | #: prefs.js:16
120 | msgid "Enter a new shortcut"
121 | msgstr "Neues Kürzel eingeben"
122 |
123 | #: prefs.js:71
124 | msgctxt "Keyboard shortcut is disabled"
125 | msgid "Disabled"
126 | msgstr "Deaktiviert"
127 |
128 | #: prefs.js:123
129 | msgid "History size"
130 | msgstr "Größe der Chronik"
131 |
132 | #: prefs.js:129
133 | msgctxt "Don't keep entries"
134 | msgid "None"
135 | msgstr ""
136 |
137 | #: prefs.js:130
138 | msgctxt "Keep only pinned entries"
139 | msgid "Pinned"
140 | msgstr ""
141 |
142 | #: prefs.js:131
143 | msgctxt "Keep all entries"
144 | msgid "All"
145 | msgstr ""
146 |
147 | #: prefs.js:145
148 | #, fuzzy
149 | msgid "History entries to keep"
150 | msgstr "Chronik ist leer"
151 |
152 | #: prefs.js:161
153 | msgid "Show leading and trailing whitespace"
154 | msgstr ""
155 |
156 | #: prefs.js:177
157 | msgid "Show color preview"
158 | msgstr ""
159 |
160 | #: prefs.js:183
161 | msgctxt "Small menu size"
162 | msgid "Small"
163 | msgstr ""
164 |
165 | #: prefs.js:184
166 | msgctxt "Medium menu size"
167 | msgid "Medium"
168 | msgstr ""
169 |
170 | #: prefs.js:185
171 | msgctxt "Large menu size"
172 | msgid "Large"
173 | msgstr ""
174 |
175 | #: prefs.js:199
176 | msgid "Maximum menu size"
177 | msgstr ""
178 |
179 | #: prefs.js:204
180 | #, fuzzy
181 | msgctxt "General options"
182 | msgid "General"
183 | msgstr "Allgemein"
184 |
185 | #: prefs.js:213
186 | #, javascript-format
187 | msgid "URL with %s in place of query"
188 | msgstr ""
189 |
190 | #: prefs.js:226
191 | msgid "Search URL"
192 | msgstr ""
193 |
194 | #: prefs.js:267
195 | msgid "Search Engine"
196 | msgstr ""
197 |
198 | #: prefs.js:272
199 | msgid "Web Search"
200 | msgstr ""
201 |
202 | #: prefs.js:294
203 | msgctxt "The number of days to keep the shared text"
204 | msgid "Days to keep the shared text"
205 | msgstr ""
206 |
207 | #: prefs.js:299
208 | msgid "Sharing Online"
209 | msgstr ""
210 |
211 | #: prefs.js:304
212 | msgid "Keyboard Shortcuts"
213 | msgstr "Tastenkombinationen"
214 |
215 | #: prefs.js:307
216 | msgid "Toggle menu"
217 | msgstr "Menü umschalten"
218 |
219 | #: prefs.js:312
220 | #, fuzzy
221 | msgid "Toggle private mode"
222 | msgstr "Privater Modus"
223 |
224 | #: prefs.js:317
225 | #, fuzzy
226 | msgid "Clear history"
227 | msgstr "Chronik löschen"
228 |
229 | #: libs/utils.js:232
230 | msgid "DuckDuckGo"
231 | msgstr ""
232 |
233 | #: libs/utils.js:233
234 | msgctxt "Brave search engine"
235 | msgid "Brave"
236 | msgstr ""
237 |
238 | #: libs/utils.js:234
239 | msgid "Google"
240 | msgstr ""
241 |
242 | #: libs/utils.js:235
243 | msgid "Bing"
244 | msgstr ""
245 |
246 | #: libs/utils.js:236
247 | msgctxt "Baidu search engine"
248 | msgid "Baidu"
249 | msgstr ""
250 |
251 | #: libs/utils.js:237
252 | msgid "Yahoo"
253 | msgstr ""
254 |
255 | #: libs/utils.js:238
256 | msgctxt "Ecosia search engine"
257 | msgid "Ecosia"
258 | msgstr ""
259 |
260 | #: libs/utils.js:239
261 | msgctxt "Ask.com search engine"
262 | msgid "Ask"
263 | msgstr ""
264 |
265 | #: libs/utils.js:240
266 | msgctxt "AOL search engine"
267 | msgid "AOL"
268 | msgstr ""
269 |
270 | #: libs/utils.js:241
271 | msgctxt "Naver search engine"
272 | msgid "Naver"
273 | msgstr ""
274 |
275 | #: libs/utils.js:249
276 | msgctxt "Other search engine"
277 | msgid "Other"
278 | msgstr ""
279 |
280 | #~ msgctxt "Change current shortcut"
281 | #~ msgid "Change"
282 | #~ msgstr "Ändern"
283 |
284 | #~ msgid "Track Changes"
285 | #~ msgstr "Änderungen überwachen"
286 |
287 | #~ msgid "Shortcut to toggle menu"
288 | #~ msgstr "Tastenkombination zum Umschalten des Menüs"
289 |
--------------------------------------------------------------------------------
/po/example.pot:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the PACKAGE package.
4 | # FIRST AUTHOR , YEAR.
5 | #
6 | #, fuzzy
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: PACKAGE VERSION\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2023-11-08 22:01+0200\n"
12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13 | "Last-Translator: FULL NAME \n"
14 | "Language-Team: LANGUAGE \n"
15 | "Language: \n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=CHARSET\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 |
20 | #: extension.js:112
21 | msgid "Failed to generate QR code"
22 | msgstr ""
23 |
24 | #: extension.js:119
25 | msgctxt "Close dialog"
26 | msgid "Close"
27 | msgstr ""
28 |
29 | #: extension.js:188
30 | msgid "Type to search..."
31 | msgstr ""
32 |
33 | #: extension.js:207
34 | msgid "No Matches"
35 | msgstr ""
36 |
37 | #: extension.js:616
38 | msgid "Private Mode is On"
39 | msgstr ""
40 |
41 | #: extension.js:622
42 | msgid "History is Empty"
43 | msgstr ""
44 |
45 | #: extension.js:644
46 | msgid "Clear History"
47 | msgstr ""
48 |
49 | #: extension.js:656
50 | msgid "Private Mode"
51 | msgstr ""
52 |
53 | #: extension.js:686
54 | msgctxt "Open settings"
55 | msgid "Settings"
56 | msgstr ""
57 |
58 | #: extension.js:810 extension.js:835 extension.js:841
59 | msgctxt "Open URL"
60 | msgid "Open"
61 | msgstr ""
62 |
63 | #: extension.js:849
64 | msgid "Compose an Email"
65 | msgstr ""
66 |
67 | #: extension.js:857 extension.js:863
68 | msgid "Make a Call"
69 | msgstr ""
70 |
71 | #: extension.js:883
72 | msgid "Search the Web"
73 | msgstr ""
74 |
75 | #: extension.js:888
76 | msgid "Send via Email"
77 | msgstr ""
78 |
79 | #: extension.js:893
80 | msgid "Share Online"
81 | msgstr ""
82 |
83 | #: extension.js:898
84 | msgid "Show QR Code"
85 | msgstr ""
86 |
87 | #: extension.js:916
88 | #, javascript-format
89 | msgid "Failed to launch URI \"%s\""
90 | msgstr ""
91 |
92 | #: extension.js:925 extension.js:938
93 | msgid "Failed to search the web"
94 | msgstr ""
95 |
96 | #: extension.js:925
97 | msgid "Unknown search engine"
98 | msgstr ""
99 |
100 | #: extension.js:938
101 | #, javascript-format
102 | msgid "Invalid search URL \"%s\""
103 | msgstr ""
104 |
105 | #: extension.js:983
106 | msgid "The text was successfully shared online"
107 | msgstr ""
108 |
109 | #: extension.js:985
110 | msgid "Failed to share the text online"
111 | msgstr ""
112 |
113 | #: prefs.js:15
114 | msgid "Press Backspace to clear shortcut or Esc to cancel"
115 | msgstr ""
116 |
117 | #: prefs.js:16
118 | msgid "Enter a new shortcut"
119 | msgstr ""
120 |
121 | #: prefs.js:71
122 | msgctxt "Keyboard shortcut is disabled"
123 | msgid "Disabled"
124 | msgstr ""
125 |
126 | #: prefs.js:123
127 | msgid "History size"
128 | msgstr ""
129 |
130 | #: prefs.js:129
131 | msgctxt "Don't keep entries"
132 | msgid "None"
133 | msgstr ""
134 |
135 | #: prefs.js:130
136 | msgctxt "Keep only pinned entries"
137 | msgid "Pinned"
138 | msgstr ""
139 |
140 | #: prefs.js:131
141 | msgctxt "Keep all entries"
142 | msgid "All"
143 | msgstr ""
144 |
145 | #: prefs.js:145
146 | msgid "History entries to keep"
147 | msgstr ""
148 |
149 | #: prefs.js:161
150 | msgid "Show leading and trailing whitespace"
151 | msgstr ""
152 |
153 | #: prefs.js:177
154 | msgid "Show color preview"
155 | msgstr ""
156 |
157 | #: prefs.js:183
158 | msgctxt "Small menu size"
159 | msgid "Small"
160 | msgstr ""
161 |
162 | #: prefs.js:184
163 | msgctxt "Medium menu size"
164 | msgid "Medium"
165 | msgstr ""
166 |
167 | #: prefs.js:185
168 | msgctxt "Large menu size"
169 | msgid "Large"
170 | msgstr ""
171 |
172 | #: prefs.js:199
173 | msgid "Maximum menu size"
174 | msgstr ""
175 |
176 | #: prefs.js:204
177 | msgctxt "General options"
178 | msgid "General"
179 | msgstr ""
180 |
181 | #: prefs.js:213
182 | #, javascript-format
183 | msgid "URL with %s in place of query"
184 | msgstr ""
185 |
186 | #: prefs.js:226
187 | msgid "Search URL"
188 | msgstr ""
189 |
190 | #: prefs.js:267
191 | msgid "Search Engine"
192 | msgstr ""
193 |
194 | #: prefs.js:272
195 | msgid "Web Search"
196 | msgstr ""
197 |
198 | #: prefs.js:294
199 | msgctxt "The number of days to keep the shared text"
200 | msgid "Days to keep the shared text"
201 | msgstr ""
202 |
203 | #: prefs.js:299
204 | msgid "Sharing Online"
205 | msgstr ""
206 |
207 | #: prefs.js:304
208 | msgid "Keyboard Shortcuts"
209 | msgstr ""
210 |
211 | #: prefs.js:307
212 | msgid "Toggle menu"
213 | msgstr ""
214 |
215 | #: prefs.js:312
216 | msgid "Toggle private mode"
217 | msgstr ""
218 |
219 | #: prefs.js:317
220 | msgid "Clear history"
221 | msgstr ""
222 |
223 | #: libs/utils.js:232
224 | msgid "DuckDuckGo"
225 | msgstr ""
226 |
227 | #: libs/utils.js:233
228 | msgctxt "Brave search engine"
229 | msgid "Brave"
230 | msgstr ""
231 |
232 | #: libs/utils.js:234
233 | msgid "Google"
234 | msgstr ""
235 |
236 | #: libs/utils.js:235
237 | msgid "Bing"
238 | msgstr ""
239 |
240 | #: libs/utils.js:236
241 | msgctxt "Baidu search engine"
242 | msgid "Baidu"
243 | msgstr ""
244 |
245 | #: libs/utils.js:237
246 | msgid "Yahoo"
247 | msgstr ""
248 |
249 | #: libs/utils.js:238
250 | msgctxt "Ecosia search engine"
251 | msgid "Ecosia"
252 | msgstr ""
253 |
254 | #: libs/utils.js:239
255 | msgctxt "Ask.com search engine"
256 | msgid "Ask"
257 | msgstr ""
258 |
259 | #: libs/utils.js:240
260 | msgctxt "AOL search engine"
261 | msgid "AOL"
262 | msgstr ""
263 |
264 | #: libs/utils.js:241
265 | msgctxt "Naver search engine"
266 | msgid "Naver"
267 | msgstr ""
268 |
269 | #: libs/utils.js:249
270 | msgctxt "Other search engine"
271 | msgid "Other"
272 | msgstr ""
273 |
--------------------------------------------------------------------------------
/po/fa_IR.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the PACKAGE package.
4 | # FIRST AUTHOR , YEAR.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2023-11-08 22:01+0200\n"
11 | "PO-Revision-Date: 2023-07-03 13:44+0300\n"
12 | "Last-Translator: MohammadSaleh Kamyab \n"
13 | "Language-Team: \n"
14 | "Language: fa_IR\n"
15 | "MIME-Version: 1.0\n"
16 | "Content-Type: text/plain; charset=UTF-8\n"
17 | "Content-Transfer-Encoding: 8bit\n"
18 | "Plural-Forms: nplurals=2; plural=(n==0 || n==1);\n"
19 | "X-Generator: Poedit 3.2.2\n"
20 |
21 | #: extension.js:112
22 | msgid "Failed to generate QR code"
23 | msgstr ""
24 |
25 | #: extension.js:119
26 | msgctxt "Close dialog"
27 | msgid "Close"
28 | msgstr ""
29 |
30 | #: extension.js:188
31 | msgid "Type to search..."
32 | msgstr "نوشتن برای جستوجو…"
33 |
34 | #: extension.js:207
35 | msgid "No Matches"
36 | msgstr "بدون نتیجه"
37 |
38 | #: extension.js:616
39 | msgid "Private Mode is On"
40 | msgstr ""
41 |
42 | #: extension.js:622
43 | msgid "History is Empty"
44 | msgstr "تاریخچه خالی است"
45 |
46 | #: extension.js:644
47 | msgid "Clear History"
48 | msgstr "پاکسازی تاریخچه"
49 |
50 | #: extension.js:656
51 | msgid "Private Mode"
52 | msgstr ""
53 |
54 | #: extension.js:686
55 | msgctxt "Open settings"
56 | msgid "Settings"
57 | msgstr "تنظیمات"
58 |
59 | #: extension.js:810 extension.js:835 extension.js:841
60 | msgctxt "Open URL"
61 | msgid "Open"
62 | msgstr ""
63 |
64 | #: extension.js:849
65 | msgid "Compose an Email"
66 | msgstr ""
67 |
68 | #: extension.js:857 extension.js:863
69 | msgid "Make a Call"
70 | msgstr ""
71 |
72 | #: extension.js:883
73 | msgid "Search the Web"
74 | msgstr ""
75 |
76 | #: extension.js:888
77 | msgid "Send via Email"
78 | msgstr ""
79 |
80 | #: extension.js:893
81 | msgid "Share Online"
82 | msgstr ""
83 |
84 | #: extension.js:898
85 | msgid "Show QR Code"
86 | msgstr ""
87 |
88 | #: extension.js:916
89 | #, javascript-format
90 | msgid "Failed to launch URI \"%s\""
91 | msgstr ""
92 |
93 | #: extension.js:925 extension.js:938
94 | msgid "Failed to search the web"
95 | msgstr ""
96 |
97 | #: extension.js:925
98 | msgid "Unknown search engine"
99 | msgstr ""
100 |
101 | #: extension.js:938
102 | #, javascript-format
103 | msgid "Invalid search URL \"%s\""
104 | msgstr ""
105 |
106 | #: extension.js:983
107 | msgid "The text was successfully shared online"
108 | msgstr ""
109 |
110 | #: extension.js:985
111 | msgid "Failed to share the text online"
112 | msgstr ""
113 |
114 | #: prefs.js:15
115 | msgid "Press Backspace to clear shortcut or Esc to cancel"
116 | msgstr ""
117 |
118 | #: prefs.js:16
119 | msgid "Enter a new shortcut"
120 | msgstr "یک میانبر جدید وارد کنید"
121 |
122 | #: prefs.js:71
123 | msgctxt "Keyboard shortcut is disabled"
124 | msgid "Disabled"
125 | msgstr "از کار افتاده است"
126 |
127 | #: prefs.js:123
128 | msgid "History size"
129 | msgstr "اندازهٔ تاریخچه"
130 |
131 | #: prefs.js:129
132 | msgctxt "Don't keep entries"
133 | msgid "None"
134 | msgstr ""
135 |
136 | #: prefs.js:130
137 | msgctxt "Keep only pinned entries"
138 | msgid "Pinned"
139 | msgstr ""
140 |
141 | #: prefs.js:131
142 | msgctxt "Keep all entries"
143 | msgid "All"
144 | msgstr ""
145 |
146 | #: prefs.js:145
147 | #, fuzzy
148 | msgid "History entries to keep"
149 | msgstr "تاریخچه خالی است"
150 |
151 | #: prefs.js:161
152 | msgid "Show leading and trailing whitespace"
153 | msgstr ""
154 |
155 | #: prefs.js:177
156 | msgid "Show color preview"
157 | msgstr ""
158 |
159 | #: prefs.js:183
160 | msgctxt "Small menu size"
161 | msgid "Small"
162 | msgstr ""
163 |
164 | #: prefs.js:184
165 | msgctxt "Medium menu size"
166 | msgid "Medium"
167 | msgstr ""
168 |
169 | #: prefs.js:185
170 | msgctxt "Large menu size"
171 | msgid "Large"
172 | msgstr ""
173 |
174 | #: prefs.js:199
175 | msgid "Maximum menu size"
176 | msgstr ""
177 |
178 | #: prefs.js:204
179 | msgctxt "General options"
180 | msgid "General"
181 | msgstr ""
182 |
183 | #: prefs.js:213
184 | #, javascript-format
185 | msgid "URL with %s in place of query"
186 | msgstr ""
187 |
188 | #: prefs.js:226
189 | msgid "Search URL"
190 | msgstr ""
191 |
192 | #: prefs.js:267
193 | msgid "Search Engine"
194 | msgstr ""
195 |
196 | #: prefs.js:272
197 | msgid "Web Search"
198 | msgstr ""
199 |
200 | #: prefs.js:294
201 | msgctxt "The number of days to keep the shared text"
202 | msgid "Days to keep the shared text"
203 | msgstr ""
204 |
205 | #: prefs.js:299
206 | msgid "Sharing Online"
207 | msgstr ""
208 |
209 | #: prefs.js:304
210 | msgid "Keyboard Shortcuts"
211 | msgstr ""
212 |
213 | #: prefs.js:307
214 | msgid "Toggle menu"
215 | msgstr ""
216 |
217 | #: prefs.js:312
218 | msgid "Toggle private mode"
219 | msgstr ""
220 |
221 | #: prefs.js:317
222 | #, fuzzy
223 | msgid "Clear history"
224 | msgstr "پاکسازی تاریخچه"
225 |
226 | #: libs/utils.js:232
227 | msgid "DuckDuckGo"
228 | msgstr ""
229 |
230 | #: libs/utils.js:233
231 | msgctxt "Brave search engine"
232 | msgid "Brave"
233 | msgstr ""
234 |
235 | #: libs/utils.js:234
236 | msgid "Google"
237 | msgstr ""
238 |
239 | #: libs/utils.js:235
240 | msgid "Bing"
241 | msgstr ""
242 |
243 | #: libs/utils.js:236
244 | msgctxt "Baidu search engine"
245 | msgid "Baidu"
246 | msgstr ""
247 |
248 | #: libs/utils.js:237
249 | msgid "Yahoo"
250 | msgstr ""
251 |
252 | #: libs/utils.js:238
253 | msgctxt "Ecosia search engine"
254 | msgid "Ecosia"
255 | msgstr ""
256 |
257 | #: libs/utils.js:239
258 | msgctxt "Ask.com search engine"
259 | msgid "Ask"
260 | msgstr ""
261 |
262 | #: libs/utils.js:240
263 | msgctxt "AOL search engine"
264 | msgid "AOL"
265 | msgstr ""
266 |
267 | #: libs/utils.js:241
268 | msgctxt "Naver search engine"
269 | msgid "Naver"
270 | msgstr ""
271 |
272 | #: libs/utils.js:249
273 | msgctxt "Other search engine"
274 | msgid "Other"
275 | msgstr ""
276 |
277 | #~ msgctxt "Change current shortcut"
278 | #~ msgid "Change"
279 | #~ msgstr "تغییر"
280 |
281 | #~ msgid "Track Changes"
282 | #~ msgstr "پیگیری تغییرات"
283 |
284 | #~ msgid "Shortcut to toggle menu"
285 | #~ msgstr "میانبر برای تغییر حالت فهرست"
286 |
--------------------------------------------------------------------------------
/po/it.po:
--------------------------------------------------------------------------------
1 | # Italian translation file for Clipman.
2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the Clipman package.
4 | # Federico Pierantoni , 2022.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2023-11-08 22:01+0200\n"
11 | "PO-Revision-Date: 2023-07-03 13:45+0300\n"
12 | "Last-Translator: Federico Pierantoni \n"
13 | "Language-Team: \n"
14 | "Language: it\n"
15 | "MIME-Version: 1.0\n"
16 | "Content-Type: text/plain; charset=UTF-8\n"
17 | "Content-Transfer-Encoding: 8bit\n"
18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n"
19 | "X-Generator: Poedit 3.2.2\n"
20 |
21 | #: extension.js:112
22 | msgid "Failed to generate QR code"
23 | msgstr ""
24 |
25 | #: extension.js:119
26 | msgctxt "Close dialog"
27 | msgid "Close"
28 | msgstr ""
29 |
30 | #: extension.js:188
31 | msgid "Type to search..."
32 | msgstr "Digita per cercare..."
33 |
34 | #: extension.js:207
35 | msgid "No Matches"
36 | msgstr "Nessuna Corrispondenza"
37 |
38 | #: extension.js:616
39 | msgid "Private Mode is On"
40 | msgstr ""
41 |
42 | #: extension.js:622
43 | msgid "History is Empty"
44 | msgstr "Cronologia vuota"
45 |
46 | #: extension.js:644
47 | msgid "Clear History"
48 | msgstr "Cancella Cronologia"
49 |
50 | #: extension.js:656
51 | msgid "Private Mode"
52 | msgstr ""
53 |
54 | #: extension.js:686
55 | msgctxt "Open settings"
56 | msgid "Settings"
57 | msgstr "Impostazioni"
58 |
59 | #: extension.js:810 extension.js:835 extension.js:841
60 | msgctxt "Open URL"
61 | msgid "Open"
62 | msgstr ""
63 |
64 | #: extension.js:849
65 | msgid "Compose an Email"
66 | msgstr ""
67 |
68 | #: extension.js:857 extension.js:863
69 | msgid "Make a Call"
70 | msgstr ""
71 |
72 | #: extension.js:883
73 | msgid "Search the Web"
74 | msgstr ""
75 |
76 | #: extension.js:888
77 | msgid "Send via Email"
78 | msgstr ""
79 |
80 | #: extension.js:893
81 | msgid "Share Online"
82 | msgstr ""
83 |
84 | #: extension.js:898
85 | msgid "Show QR Code"
86 | msgstr ""
87 |
88 | #: extension.js:916
89 | #, javascript-format
90 | msgid "Failed to launch URI \"%s\""
91 | msgstr ""
92 |
93 | #: extension.js:925 extension.js:938
94 | msgid "Failed to search the web"
95 | msgstr ""
96 |
97 | #: extension.js:925
98 | msgid "Unknown search engine"
99 | msgstr ""
100 |
101 | #: extension.js:938
102 | #, javascript-format
103 | msgid "Invalid search URL \"%s\""
104 | msgstr ""
105 |
106 | #: extension.js:983
107 | msgid "The text was successfully shared online"
108 | msgstr ""
109 |
110 | #: extension.js:985
111 | msgid "Failed to share the text online"
112 | msgstr ""
113 |
114 | #: prefs.js:15
115 | msgid "Press Backspace to clear shortcut or Esc to cancel"
116 | msgstr ""
117 |
118 | #: prefs.js:16
119 | msgid "Enter a new shortcut"
120 | msgstr "Inserisci la nuova scorciatoia"
121 |
122 | #: prefs.js:71
123 | msgctxt "Keyboard shortcut is disabled"
124 | msgid "Disabled"
125 | msgstr "Disabilitato"
126 |
127 | #: prefs.js:123
128 | msgid "History size"
129 | msgstr "Dimensione della Cronologia"
130 |
131 | #: prefs.js:129
132 | msgctxt "Don't keep entries"
133 | msgid "None"
134 | msgstr ""
135 |
136 | #: prefs.js:130
137 | msgctxt "Keep only pinned entries"
138 | msgid "Pinned"
139 | msgstr ""
140 |
141 | #: prefs.js:131
142 | msgctxt "Keep all entries"
143 | msgid "All"
144 | msgstr ""
145 |
146 | #: prefs.js:145
147 | #, fuzzy
148 | msgid "History entries to keep"
149 | msgstr "Cronologia vuota"
150 |
151 | #: prefs.js:161
152 | msgid "Show leading and trailing whitespace"
153 | msgstr ""
154 |
155 | #: prefs.js:177
156 | msgid "Show color preview"
157 | msgstr ""
158 |
159 | #: prefs.js:183
160 | msgctxt "Small menu size"
161 | msgid "Small"
162 | msgstr ""
163 |
164 | #: prefs.js:184
165 | msgctxt "Medium menu size"
166 | msgid "Medium"
167 | msgstr ""
168 |
169 | #: prefs.js:185
170 | msgctxt "Large menu size"
171 | msgid "Large"
172 | msgstr ""
173 |
174 | #: prefs.js:199
175 | msgid "Maximum menu size"
176 | msgstr ""
177 |
178 | #: prefs.js:204
179 | msgctxt "General options"
180 | msgid "General"
181 | msgstr ""
182 |
183 | #: prefs.js:213
184 | #, javascript-format
185 | msgid "URL with %s in place of query"
186 | msgstr ""
187 |
188 | #: prefs.js:226
189 | msgid "Search URL"
190 | msgstr ""
191 |
192 | #: prefs.js:267
193 | msgid "Search Engine"
194 | msgstr ""
195 |
196 | #: prefs.js:272
197 | msgid "Web Search"
198 | msgstr ""
199 |
200 | #: prefs.js:294
201 | msgctxt "The number of days to keep the shared text"
202 | msgid "Days to keep the shared text"
203 | msgstr ""
204 |
205 | #: prefs.js:299
206 | msgid "Sharing Online"
207 | msgstr ""
208 |
209 | #: prefs.js:304
210 | msgid "Keyboard Shortcuts"
211 | msgstr ""
212 |
213 | #: prefs.js:307
214 | msgid "Toggle menu"
215 | msgstr ""
216 |
217 | #: prefs.js:312
218 | msgid "Toggle private mode"
219 | msgstr ""
220 |
221 | #: prefs.js:317
222 | #, fuzzy
223 | msgid "Clear history"
224 | msgstr "Cancella Cronologia"
225 |
226 | #: libs/utils.js:232
227 | msgid "DuckDuckGo"
228 | msgstr ""
229 |
230 | #: libs/utils.js:233
231 | msgctxt "Brave search engine"
232 | msgid "Brave"
233 | msgstr ""
234 |
235 | #: libs/utils.js:234
236 | msgid "Google"
237 | msgstr ""
238 |
239 | #: libs/utils.js:235
240 | msgid "Bing"
241 | msgstr ""
242 |
243 | #: libs/utils.js:236
244 | msgctxt "Baidu search engine"
245 | msgid "Baidu"
246 | msgstr ""
247 |
248 | #: libs/utils.js:237
249 | msgid "Yahoo"
250 | msgstr ""
251 |
252 | #: libs/utils.js:238
253 | msgctxt "Ecosia search engine"
254 | msgid "Ecosia"
255 | msgstr ""
256 |
257 | #: libs/utils.js:239
258 | msgctxt "Ask.com search engine"
259 | msgid "Ask"
260 | msgstr ""
261 |
262 | #: libs/utils.js:240
263 | msgctxt "AOL search engine"
264 | msgid "AOL"
265 | msgstr ""
266 |
267 | #: libs/utils.js:241
268 | msgctxt "Naver search engine"
269 | msgid "Naver"
270 | msgstr ""
271 |
272 | #: libs/utils.js:249
273 | msgctxt "Other search engine"
274 | msgid "Other"
275 | msgstr ""
276 |
277 | #~ msgctxt "Change current shortcut"
278 | #~ msgid "Change"
279 | #~ msgstr "Modifica"
280 |
281 | #~ msgid "Track Changes"
282 | #~ msgstr "Tracciare le Modifiche"
283 |
284 | #~ msgid "Shortcut to toggle menu"
285 | #~ msgstr "Scorciatoia per attivare/disattivare il menu"
286 |
--------------------------------------------------------------------------------
/po/nl.po:
--------------------------------------------------------------------------------
1 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
2 | # This file is distributed under the same license as the PACKAGE package.
3 | #
4 | # Heimen Stoffels , 2022.
5 | msgid ""
6 | msgstr ""
7 | "Project-Id-Version: \n"
8 | "Report-Msgid-Bugs-To: \n"
9 | "POT-Creation-Date: 2023-11-08 22:01+0200\n"
10 | "PO-Revision-Date: 2023-09-30 14:04+0200\n"
11 | "Last-Translator: Heimen Stoffels \n"
12 | "Language-Team: Dutch \n"
13 | "Language: nl\n"
14 | "MIME-Version: 1.0\n"
15 | "Content-Type: text/plain; charset=UTF-8\n"
16 | "Content-Transfer-Encoding: 8bit\n"
17 | "Plural-Forms: nplurals=2; plural=(n != 1);\n"
18 | "X-Generator: Poedit 3.3.2\n"
19 |
20 | #: extension.js:112
21 | msgid "Failed to generate QR code"
22 | msgstr "De QR-code kan niet worden aangemaakt"
23 |
24 | #: extension.js:119
25 | msgctxt "Close dialog"
26 | msgid "Close"
27 | msgstr "Sluiten"
28 |
29 | #: extension.js:188
30 | msgid "Type to search..."
31 | msgstr "Typ om te zoeken…"
32 |
33 | #: extension.js:207
34 | msgid "No Matches"
35 | msgstr "Er zijn geen overeenkomsten"
36 |
37 | #: extension.js:616
38 | msgid "Private Mode is On"
39 | msgstr "De incognitomodus is ingeschakeld"
40 |
41 | #: extension.js:622
42 | msgid "History is Empty"
43 | msgstr "De geschiedenis is blanco"
44 |
45 | #: extension.js:644
46 | msgid "Clear History"
47 | msgstr "Geschiedenis wissen"
48 |
49 | #: extension.js:656
50 | msgid "Private Mode"
51 | msgstr "Incognitomodus"
52 |
53 | #: extension.js:686
54 | msgctxt "Open settings"
55 | msgid "Settings"
56 | msgstr "Voorkeuren"
57 |
58 | #: extension.js:810 extension.js:835 extension.js:841
59 | msgctxt "Open URL"
60 | msgid "Open"
61 | msgstr "Openen"
62 |
63 | #: extension.js:849
64 | msgid "Compose an Email"
65 | msgstr "E-mail opstellen"
66 |
67 | #: extension.js:857 extension.js:863
68 | msgid "Make a Call"
69 | msgstr "Gesprek starten"
70 |
71 | #: extension.js:883
72 | msgid "Search the Web"
73 | msgstr "Zoeken op internet"
74 |
75 | #: extension.js:888
76 | msgid "Send via Email"
77 | msgstr "Versturen per e-mail"
78 |
79 | #: extension.js:893
80 | msgid "Share Online"
81 | msgstr "Online delen"
82 |
83 | #: extension.js:898
84 | msgid "Show QR Code"
85 | msgstr "QR-code tonen"
86 |
87 | #: extension.js:916
88 | #, javascript-format
89 | msgid "Failed to launch URI \"%s\""
90 | msgstr "‘%s’ kan niet worden geopend"
91 |
92 | #: extension.js:925 extension.js:938
93 | #, fuzzy
94 | msgid "Failed to search the web"
95 | msgstr "De QR-code kan niet worden aangemaakt"
96 |
97 | #: extension.js:925
98 | #, fuzzy
99 | msgid "Unknown search engine"
100 | msgstr "Zoekmachine"
101 |
102 | #: extension.js:938
103 | #, javascript-format
104 | msgid "Invalid search URL \"%s\""
105 | msgstr "Ongeldige zoekurl: ‘%s’"
106 |
107 | #: extension.js:983
108 | msgid "The text was successfully shared online"
109 | msgstr ""
110 |
111 | #: extension.js:985
112 | msgid "Failed to share the text online"
113 | msgstr ""
114 |
115 | #: prefs.js:15
116 | msgid "Press Backspace to clear shortcut or Esc to cancel"
117 | msgstr "Druk op backspace om de sneltoets te wissen of esc om te annuleren"
118 |
119 | #: prefs.js:16
120 | msgid "Enter a new shortcut"
121 | msgstr "Druk op een nieuwe sneltoets"
122 |
123 | #: prefs.js:71
124 | msgctxt "Keyboard shortcut is disabled"
125 | msgid "Disabled"
126 | msgstr "Uitgeschakeld"
127 |
128 | #: prefs.js:123
129 | msgid "History size"
130 | msgstr "Geschiedenisomvang"
131 |
132 | #: prefs.js:129
133 | msgctxt "Don't keep entries"
134 | msgid "None"
135 | msgstr ""
136 |
137 | #: prefs.js:130
138 | msgctxt "Keep only pinned entries"
139 | msgid "Pinned"
140 | msgstr ""
141 |
142 | #: prefs.js:131
143 | msgctxt "Keep all entries"
144 | msgid "All"
145 | msgstr ""
146 |
147 | #: prefs.js:145
148 | #, fuzzy
149 | msgid "History entries to keep"
150 | msgstr "De geschiedenis is blanco"
151 |
152 | #: prefs.js:161
153 | msgid "Show leading and trailing whitespace"
154 | msgstr "Witruimte aan begin en einde tonen"
155 |
156 | #: prefs.js:177
157 | msgid "Show color preview"
158 | msgstr "Kleuren voorvertonen"
159 |
160 | #: prefs.js:183
161 | msgctxt "Small menu size"
162 | msgid "Small"
163 | msgstr "Klein"
164 |
165 | #: prefs.js:184
166 | msgctxt "Medium menu size"
167 | msgid "Medium"
168 | msgstr "Gemiddeld"
169 |
170 | #: prefs.js:185
171 | msgctxt "Large menu size"
172 | msgid "Large"
173 | msgstr "Groot"
174 |
175 | #: prefs.js:199
176 | msgid "Maximum menu size"
177 | msgstr "Maximale menu-omvang"
178 |
179 | #: prefs.js:204
180 | msgctxt "General options"
181 | msgid "General"
182 | msgstr "Algemeen"
183 |
184 | #: prefs.js:213
185 | #, javascript-format
186 | msgid "URL with %s in place of query"
187 | msgstr "De url, met %s op de plaats van de zoekopdracht"
188 |
189 | #: prefs.js:226
190 | msgid "Search URL"
191 | msgstr "Zoekurl"
192 |
193 | #: prefs.js:267
194 | msgid "Search Engine"
195 | msgstr "Zoekmachine"
196 |
197 | #: prefs.js:272
198 | msgid "Web Search"
199 | msgstr "Zoeken op internet"
200 |
201 | #: prefs.js:294
202 | msgctxt "The number of days to keep the shared text"
203 | msgid "Days to keep the shared text"
204 | msgstr "Aantal dagen dat gedeelde tekst bewaard moet worden"
205 |
206 | #: prefs.js:299
207 | msgid "Sharing Online"
208 | msgstr "Online-delen"
209 |
210 | #: prefs.js:304
211 | msgid "Keyboard Shortcuts"
212 | msgstr "Sneltoetsen"
213 |
214 | #: prefs.js:307
215 | msgid "Toggle menu"
216 | msgstr "Menu tonen/verbergen"
217 |
218 | #: prefs.js:312
219 | msgid "Toggle private mode"
220 | msgstr "Incognitomodus aan/uit"
221 |
222 | #: prefs.js:317
223 | msgid "Clear history"
224 | msgstr "Geschiedenis wissen"
225 |
226 | #: libs/utils.js:232
227 | msgid "DuckDuckGo"
228 | msgstr "DuckDuckGo"
229 |
230 | #: libs/utils.js:233
231 | msgctxt "Brave search engine"
232 | msgid "Brave"
233 | msgstr "Brave"
234 |
235 | #: libs/utils.js:234
236 | msgid "Google"
237 | msgstr "Google"
238 |
239 | #: libs/utils.js:235
240 | msgid "Bing"
241 | msgstr "Bing"
242 |
243 | #: libs/utils.js:236
244 | msgctxt "Baidu search engine"
245 | msgid "Baidu"
246 | msgstr "Baidu"
247 |
248 | #: libs/utils.js:237
249 | msgid "Yahoo"
250 | msgstr "Yahoo"
251 |
252 | #: libs/utils.js:238
253 | msgctxt "Ecosia search engine"
254 | msgid "Ecosia"
255 | msgstr "Ecosia"
256 |
257 | #: libs/utils.js:239
258 | msgctxt "Ask.com search engine"
259 | msgid "Ask"
260 | msgstr "Ask"
261 |
262 | #: libs/utils.js:240
263 | msgctxt "AOL search engine"
264 | msgid "AOL"
265 | msgstr "AOL"
266 |
267 | #: libs/utils.js:241
268 | msgctxt "Naver search engine"
269 | msgid "Naver"
270 | msgstr "Naver"
271 |
272 | #: libs/utils.js:249
273 | msgctxt "Other search engine"
274 | msgid "Other"
275 | msgstr "Andere"
276 |
277 | #~ msgctxt "Change current shortcut"
278 | #~ msgid "Change"
279 | #~ msgstr "Wijzigen"
280 |
281 | #~ msgid "Track Changes"
282 | #~ msgstr "Wijzigingen bijhouden"
283 |
284 | #~ msgid "Shortcut to toggle menu"
285 | #~ msgstr "Sneltoets om menu te openen"
286 |
--------------------------------------------------------------------------------
/po/pl.po:
--------------------------------------------------------------------------------
1 | # Polish translation file for Clipman.
2 | # Copyright (C) 2022
3 | # This file is distributed under the same license as the Clipman package.
4 | # Kamil Stępień , 2022.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2023-11-08 22:01+0200\n"
11 | "PO-Revision-Date: 2023-07-03 13:46+0300\n"
12 | "Last-Translator: Kamil Stępień \n"
13 | "Language-Team: \n"
14 | "Language: pl\n"
15 | "MIME-Version: 1.0\n"
16 | "Content-Type: text/plain; charset=UTF-8\n"
17 | "Content-Transfer-Encoding: 8bit\n"
18 | "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
19 | "|| n%100>=20) ? 1 : 2);\n"
20 | "X-Generator: Poedit 3.2.2\n"
21 |
22 | #: extension.js:112
23 | msgid "Failed to generate QR code"
24 | msgstr "Nie udało się wygenerować kodu QR"
25 |
26 | #: extension.js:119
27 | msgctxt "Close dialog"
28 | msgid "Close"
29 | msgstr "Zamknij"
30 |
31 | #: extension.js:188
32 | msgid "Type to search..."
33 | msgstr "Szukaj..."
34 |
35 | #: extension.js:207
36 | msgid "No Matches"
37 | msgstr "Brak wyników"
38 |
39 | #: extension.js:616
40 | msgid "Private Mode is On"
41 | msgstr "Tryb prywatny jest włączony"
42 |
43 | #: extension.js:622
44 | msgid "History is Empty"
45 | msgstr "Historia jest pusta"
46 |
47 | #: extension.js:644
48 | msgid "Clear History"
49 | msgstr "Wyczyść historię"
50 |
51 | #: extension.js:656
52 | msgid "Private Mode"
53 | msgstr "Tryb prywatny"
54 |
55 | #: extension.js:686
56 | msgctxt "Open settings"
57 | msgid "Settings"
58 | msgstr "Ustawienia"
59 |
60 | #: extension.js:810 extension.js:835 extension.js:841
61 | msgctxt "Open URL"
62 | msgid "Open"
63 | msgstr ""
64 |
65 | #: extension.js:849
66 | msgid "Compose an Email"
67 | msgstr ""
68 |
69 | #: extension.js:857 extension.js:863
70 | msgid "Make a Call"
71 | msgstr ""
72 |
73 | #: extension.js:883
74 | msgid "Search the Web"
75 | msgstr ""
76 |
77 | #: extension.js:888
78 | msgid "Send via Email"
79 | msgstr ""
80 |
81 | #: extension.js:893
82 | msgid "Share Online"
83 | msgstr ""
84 |
85 | #: extension.js:898
86 | msgid "Show QR Code"
87 | msgstr ""
88 |
89 | #: extension.js:916
90 | #, javascript-format
91 | msgid "Failed to launch URI \"%s\""
92 | msgstr ""
93 |
94 | #: extension.js:925 extension.js:938
95 | #, fuzzy
96 | msgid "Failed to search the web"
97 | msgstr "Nie udało się wygenerować kodu QR"
98 |
99 | #: extension.js:925
100 | msgid "Unknown search engine"
101 | msgstr ""
102 |
103 | #: extension.js:938
104 | #, javascript-format
105 | msgid "Invalid search URL \"%s\""
106 | msgstr ""
107 |
108 | #: extension.js:983
109 | msgid "The text was successfully shared online"
110 | msgstr ""
111 |
112 | #: extension.js:985
113 | msgid "Failed to share the text online"
114 | msgstr ""
115 |
116 | #: prefs.js:15
117 | msgid "Press Backspace to clear shortcut or Esc to cancel"
118 | msgstr ""
119 |
120 | #: prefs.js:16
121 | msgid "Enter a new shortcut"
122 | msgstr "Wprowadź nowy skrót"
123 |
124 | #: prefs.js:71
125 | msgctxt "Keyboard shortcut is disabled"
126 | msgid "Disabled"
127 | msgstr "Wyłączony"
128 |
129 | #: prefs.js:123
130 | msgid "History size"
131 | msgstr "Rozmiar historii"
132 |
133 | #: prefs.js:129
134 | msgctxt "Don't keep entries"
135 | msgid "None"
136 | msgstr ""
137 |
138 | #: prefs.js:130
139 | msgctxt "Keep only pinned entries"
140 | msgid "Pinned"
141 | msgstr ""
142 |
143 | #: prefs.js:131
144 | msgctxt "Keep all entries"
145 | msgid "All"
146 | msgstr ""
147 |
148 | #: prefs.js:145
149 | #, fuzzy
150 | msgid "History entries to keep"
151 | msgstr "Historia jest pusta"
152 |
153 | #: prefs.js:161
154 | msgid "Show leading and trailing whitespace"
155 | msgstr ""
156 |
157 | #: prefs.js:177
158 | msgid "Show color preview"
159 | msgstr ""
160 |
161 | #: prefs.js:183
162 | msgctxt "Small menu size"
163 | msgid "Small"
164 | msgstr ""
165 |
166 | #: prefs.js:184
167 | msgctxt "Medium menu size"
168 | msgid "Medium"
169 | msgstr ""
170 |
171 | #: prefs.js:185
172 | msgctxt "Large menu size"
173 | msgid "Large"
174 | msgstr ""
175 |
176 | #: prefs.js:199
177 | msgid "Maximum menu size"
178 | msgstr ""
179 |
180 | #: prefs.js:204
181 | #, fuzzy
182 | msgctxt "General options"
183 | msgid "General"
184 | msgstr "Ogólne"
185 |
186 | #: prefs.js:213
187 | #, javascript-format
188 | msgid "URL with %s in place of query"
189 | msgstr ""
190 |
191 | #: prefs.js:226
192 | msgid "Search URL"
193 | msgstr ""
194 |
195 | #: prefs.js:267
196 | msgid "Search Engine"
197 | msgstr ""
198 |
199 | #: prefs.js:272
200 | msgid "Web Search"
201 | msgstr ""
202 |
203 | #: prefs.js:294
204 | msgctxt "The number of days to keep the shared text"
205 | msgid "Days to keep the shared text"
206 | msgstr ""
207 |
208 | #: prefs.js:299
209 | msgid "Sharing Online"
210 | msgstr ""
211 |
212 | #: prefs.js:304
213 | msgid "Keyboard Shortcuts"
214 | msgstr "Skróty klawiaturowe"
215 |
216 | #: prefs.js:307
217 | msgid "Toggle menu"
218 | msgstr "Pokaż/ukryj menu"
219 |
220 | #: prefs.js:312
221 | #, fuzzy
222 | msgid "Toggle private mode"
223 | msgstr "Tryb prywatny"
224 |
225 | #: prefs.js:317
226 | #, fuzzy
227 | msgid "Clear history"
228 | msgstr "Wyczyść historię"
229 |
230 | #: libs/utils.js:232
231 | msgid "DuckDuckGo"
232 | msgstr ""
233 |
234 | #: libs/utils.js:233
235 | msgctxt "Brave search engine"
236 | msgid "Brave"
237 | msgstr ""
238 |
239 | #: libs/utils.js:234
240 | msgid "Google"
241 | msgstr ""
242 |
243 | #: libs/utils.js:235
244 | msgid "Bing"
245 | msgstr ""
246 |
247 | #: libs/utils.js:236
248 | msgctxt "Baidu search engine"
249 | msgid "Baidu"
250 | msgstr ""
251 |
252 | #: libs/utils.js:237
253 | msgid "Yahoo"
254 | msgstr ""
255 |
256 | #: libs/utils.js:238
257 | msgctxt "Ecosia search engine"
258 | msgid "Ecosia"
259 | msgstr ""
260 |
261 | #: libs/utils.js:239
262 | msgctxt "Ask.com search engine"
263 | msgid "Ask"
264 | msgstr ""
265 |
266 | #: libs/utils.js:240
267 | msgctxt "AOL search engine"
268 | msgid "AOL"
269 | msgstr ""
270 |
271 | #: libs/utils.js:241
272 | msgctxt "Naver search engine"
273 | msgid "Naver"
274 | msgstr ""
275 |
276 | #: libs/utils.js:249
277 | msgctxt "Other search engine"
278 | msgid "Other"
279 | msgstr ""
280 |
281 | #~ msgctxt "Change current shortcut"
282 | #~ msgid "Change"
283 | #~ msgstr "Zmień"
284 |
--------------------------------------------------------------------------------
/po/uk.po:
--------------------------------------------------------------------------------
1 | # Ukrainian translation file for Clipman.
2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the Clipman package.
4 | # Eugene Popov , 2022.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2023-11-08 22:01+0200\n"
11 | "PO-Revision-Date: 2023-10-31 11:47+0200\n"
12 | "Last-Translator: Eugene Popov \n"
13 | "Language-Team: \n"
14 | "Language: uk\n"
15 | "MIME-Version: 1.0\n"
16 | "Content-Type: text/plain; charset=UTF-8\n"
17 | "Content-Transfer-Encoding: 8bit\n"
18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n"
19 | "X-Generator: Poedit 3.4.1\n"
20 |
21 | #: extension.js:112
22 | msgid "Failed to generate QR code"
23 | msgstr "Не вдалося створити QR-код"
24 |
25 | #: extension.js:119
26 | msgctxt "Close dialog"
27 | msgid "Close"
28 | msgstr "Закрити"
29 |
30 | #: extension.js:188
31 | msgid "Type to search..."
32 | msgstr "Введіть для пошуку..."
33 |
34 | #: extension.js:207
35 | msgid "No Matches"
36 | msgstr "Немає збігів"
37 |
38 | #: extension.js:616
39 | msgid "Private Mode is On"
40 | msgstr "Приватний режим увімкнено"
41 |
42 | #: extension.js:622
43 | msgid "History is Empty"
44 | msgstr "Історія порожня"
45 |
46 | #: extension.js:644
47 | msgid "Clear History"
48 | msgstr "Очистити історію"
49 |
50 | #: extension.js:656
51 | msgid "Private Mode"
52 | msgstr "Приватний режим"
53 |
54 | #: extension.js:686
55 | msgctxt "Open settings"
56 | msgid "Settings"
57 | msgstr "Налаштування"
58 |
59 | #: extension.js:810 extension.js:835 extension.js:841
60 | msgctxt "Open URL"
61 | msgid "Open"
62 | msgstr "Відкрити"
63 |
64 | #: extension.js:849
65 | msgid "Compose an Email"
66 | msgstr "Створити електронний лист"
67 |
68 | #: extension.js:857 extension.js:863
69 | msgid "Make a Call"
70 | msgstr "Зателефонувати"
71 |
72 | #: extension.js:883
73 | msgid "Search the Web"
74 | msgstr "Шукати в Інтернеті"
75 |
76 | #: extension.js:888
77 | msgid "Send via Email"
78 | msgstr "Надіслати електронною поштою"
79 |
80 | #: extension.js:893
81 | msgid "Share Online"
82 | msgstr "Поширити в Інтернеті"
83 |
84 | #: extension.js:898
85 | msgid "Show QR Code"
86 | msgstr "Показати QR-код"
87 |
88 | #: extension.js:916
89 | #, javascript-format
90 | msgid "Failed to launch URI \"%s\""
91 | msgstr "Не вдалося запустити URI \"%s\""
92 |
93 | #: extension.js:925 extension.js:938
94 | msgid "Failed to search the web"
95 | msgstr "Не вдалося здійснити пошук в Інтернеті"
96 |
97 | #: extension.js:925
98 | msgid "Unknown search engine"
99 | msgstr "Невідома пошукова система"
100 |
101 | #: extension.js:938
102 | #, javascript-format
103 | msgid "Invalid search URL \"%s\""
104 | msgstr "Неправильна URL-адреса для пошуку \"%s\""
105 |
106 | #: extension.js:983
107 | msgid "The text was successfully shared online"
108 | msgstr "Текст був успішно поширений в Інтернеті"
109 |
110 | #: extension.js:985
111 | msgid "Failed to share the text online"
112 | msgstr "Не вдалося поширити текст в Інтернеті"
113 |
114 | #: prefs.js:15
115 | msgid "Press Backspace to clear shortcut or Esc to cancel"
116 | msgstr "Натисніть Backspace, щоб очистити поєднання клавіш, або Esc, щоб скасувати"
117 |
118 | #: prefs.js:16
119 | msgid "Enter a new shortcut"
120 | msgstr "Введіть нове поєднання клавіш"
121 |
122 | #: prefs.js:71
123 | msgctxt "Keyboard shortcut is disabled"
124 | msgid "Disabled"
125 | msgstr "Вимкнено"
126 |
127 | #: prefs.js:123
128 | msgid "History size"
129 | msgstr "Розмір історії"
130 |
131 | #: prefs.js:129
132 | msgctxt "Don't keep entries"
133 | msgid "None"
134 | msgstr "Жодного"
135 |
136 | #: prefs.js:130
137 | msgctxt "Keep only pinned entries"
138 | msgid "Pinned"
139 | msgstr "Закріплені"
140 |
141 | #: prefs.js:131
142 | msgctxt "Keep all entries"
143 | msgid "All"
144 | msgstr "Всі"
145 |
146 | #: prefs.js:145
147 | msgid "History entries to keep"
148 | msgstr "Записи історії, що слід зберігати"
149 |
150 | #: prefs.js:161
151 | msgid "Show leading and trailing whitespace"
152 | msgstr "Показувати початкові та кінцеві пробіли"
153 |
154 | #: prefs.js:177
155 | msgid "Show color preview"
156 | msgstr "Показати попередній перегляд кольорів"
157 |
158 | #: prefs.js:183
159 | msgctxt "Small menu size"
160 | msgid "Small"
161 | msgstr "Маленький"
162 |
163 | #: prefs.js:184
164 | msgctxt "Medium menu size"
165 | msgid "Medium"
166 | msgstr "Середній"
167 |
168 | #: prefs.js:185
169 | msgctxt "Large menu size"
170 | msgid "Large"
171 | msgstr "Великий"
172 |
173 | #: prefs.js:199
174 | msgid "Maximum menu size"
175 | msgstr "Максимальний розмір меню"
176 |
177 | #: prefs.js:204
178 | msgctxt "General options"
179 | msgid "General"
180 | msgstr "Загальне"
181 |
182 | #: prefs.js:213
183 | #, javascript-format
184 | msgid "URL with %s in place of query"
185 | msgstr "URL-адреса з %s замість запиту"
186 |
187 | #: prefs.js:226
188 | msgid "Search URL"
189 | msgstr "URL-адреса для пошуку"
190 |
191 | #: prefs.js:267
192 | msgid "Search Engine"
193 | msgstr "Пошукова система"
194 |
195 | #: prefs.js:272
196 | msgid "Web Search"
197 | msgstr "Пошук в Інтернеті"
198 |
199 | #: prefs.js:294
200 | msgctxt "The number of days to keep the shared text"
201 | msgid "Days to keep the shared text"
202 | msgstr "Скільки днів зберігати поширений текст"
203 |
204 | #: prefs.js:299
205 | msgid "Sharing Online"
206 | msgstr "Поширення в Інтернеті"
207 |
208 | #: prefs.js:304
209 | msgid "Keyboard Shortcuts"
210 | msgstr "Поєднання клавіш"
211 |
212 | #: prefs.js:307
213 | msgid "Toggle menu"
214 | msgstr "Переключити меню"
215 |
216 | #: prefs.js:312
217 | msgid "Toggle private mode"
218 | msgstr "Переключити приватний режим"
219 |
220 | #: prefs.js:317
221 | msgid "Clear history"
222 | msgstr "Очистити історію"
223 |
224 | #: libs/utils.js:232
225 | msgid "DuckDuckGo"
226 | msgstr ""
227 |
228 | #: libs/utils.js:233
229 | msgctxt "Brave search engine"
230 | msgid "Brave"
231 | msgstr ""
232 |
233 | #: libs/utils.js:234
234 | msgid "Google"
235 | msgstr ""
236 |
237 | #: libs/utils.js:235
238 | msgid "Bing"
239 | msgstr ""
240 |
241 | #: libs/utils.js:236
242 | msgctxt "Baidu search engine"
243 | msgid "Baidu"
244 | msgstr ""
245 |
246 | #: libs/utils.js:237
247 | msgid "Yahoo"
248 | msgstr ""
249 |
250 | #: libs/utils.js:238
251 | msgctxt "Ecosia search engine"
252 | msgid "Ecosia"
253 | msgstr ""
254 |
255 | #: libs/utils.js:239
256 | msgctxt "Ask.com search engine"
257 | msgid "Ask"
258 | msgstr ""
259 |
260 | #: libs/utils.js:240
261 | msgctxt "AOL search engine"
262 | msgid "AOL"
263 | msgstr ""
264 |
265 | #: libs/utils.js:241
266 | msgctxt "Naver search engine"
267 | msgid "Naver"
268 | msgstr ""
269 |
270 | #: libs/utils.js:249
271 | msgctxt "Other search engine"
272 | msgid "Other"
273 | msgstr "Інша"
274 |
275 | #~ msgid "Paste to Pastebin"
276 | #~ msgstr "Вставити в Pastebin"
277 |
278 | #~ msgctxt "The number of days the paste will expire"
279 | #~ msgid "Days the paste will expire"
280 | #~ msgstr "Скільки днів зберігати вставку"
281 |
282 | #~ msgctxt "Change current shortcut"
283 | #~ msgid "Change"
284 | #~ msgstr "Змінити"
285 |
286 | #~ msgid "Track Changes"
287 | #~ msgstr "Відстежувати Зміни"
288 |
289 | #~ msgid "Shortcut to toggle menu"
290 | #~ msgstr "Поєднання клавіш для переключення меню"
291 |
--------------------------------------------------------------------------------
/prefs.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { Adw, Gdk, Gio, GObject, Gtk } = imports.gi;
4 | const ExtensionUtils = imports.misc.extensionUtils;
5 |
6 | const Me = ExtensionUtils.getCurrentExtension();
7 | const { Preferences } = Me.imports.libs.preferences;
8 | const { _, SearchEngines } = Me.imports.libs.utils;
9 |
10 | const KeybindingWindow = GObject.registerClass(
11 | class KeybindingWindow extends Adw.Window {
12 | constructor(transientWindow) {
13 | super({
14 | content: new Adw.StatusPage({
15 | description: _(`Press Backspace to clear shortcut or Esc to cancel`),
16 | title: _(`Enter a new shortcut`),
17 | }),
18 | modal: true,
19 | resizable: false,
20 | transient_for: transientWindow,
21 | width_request: 450,
22 | });
23 |
24 | const keyController = new Gtk.EventControllerKey();
25 | keyController.connect(`key-pressed`, (...[, keyval, keycode, state]) => {
26 | switch (keyval) {
27 | case Gdk.KEY_Escape: {
28 | this.close();
29 | return Gdk.EVENT_STOP;
30 | }
31 | case Gdk.KEY_BackSpace: {
32 | this._keybinding = ``;
33 | this.close();
34 | return Gdk.EVENT_STOP;
35 | }
36 | default: {
37 | const mask = state & Gtk.accelerator_get_default_mod_mask();
38 | if (mask && Gtk.accelerator_valid(keyval, mask)) {
39 | const accelerator = Gtk.accelerator_name_with_keycode(null, keyval, keycode, mask);
40 | if (accelerator.length > 0) {
41 | this._keybinding = accelerator;
42 | this.close();
43 | return Gdk.EVENT_STOP;
44 | }
45 | }
46 | break;
47 | }
48 | }
49 | return Gdk.EVENT_PROPAGATE;
50 | });
51 | this.add_controller(keyController);
52 | }
53 |
54 | get keybinding() {
55 | return this._keybinding;
56 | }
57 | });
58 |
59 | const ShortcutRow = GObject.registerClass(
60 | class ShortcutRow extends Adw.ActionRow {
61 | constructor(title, preferences, preferencesKey) {
62 | super({
63 | title: title,
64 | });
65 |
66 | this._preferences = preferences;
67 | this._preferencesKey = preferencesKey;
68 |
69 | this.activatable_widget = new Gtk.ShortcutLabel({
70 | accelerator: this._preferences.getShortcut(this._preferencesKey),
71 | disabled_text: _(`Disabled`, `Keyboard shortcut is disabled`),
72 | valign: Gtk.Align.CENTER,
73 | });
74 | this.add_suffix(this.activatable_widget);
75 |
76 | this._preferences.connect(`shortcutChanged`, (...[, key]) => {
77 | if (key === this._preferencesKey) {
78 | this.activatable_widget.accelerator = this._preferences.getShortcut(key);
79 | }
80 | });
81 | }
82 |
83 | vfunc_activate() {
84 | const window = new KeybindingWindow(this.get_root());
85 | window.connect(`close-request`, () => {
86 | const shortcut = window.keybinding;
87 | if (shortcut !== undefined) {
88 | this._preferences.setShortcut(this._preferencesKey, shortcut);
89 | }
90 | window.destroy();
91 | });
92 | window.present();
93 | }
94 | });
95 |
96 | function init() {
97 | ExtensionUtils.initTranslations(Me.uuid);
98 | }
99 |
100 | function fillPreferencesWindow(window) {
101 | window._preferences = new Preferences();
102 | window.connect(`close-request`, () => {
103 | window._preferences.destroy();
104 | });
105 |
106 | const historySizeSpinBox = new Gtk.SpinButton({
107 | adjustment: new Gtk.Adjustment({
108 | lower: 1,
109 | upper: 500,
110 | step_increment: 1,
111 | }),
112 | valign: Gtk.Align.CENTER,
113 | });
114 | window._preferences.bind(
115 | window._preferences._keyHistorySize,
116 | historySizeSpinBox,
117 | `value`,
118 | Gio.SettingsBindFlags.DEFAULT
119 | );
120 |
121 | const historySizeRow = new Adw.ActionRow({
122 | activatable_widget: historySizeSpinBox,
123 | title: _(`History size`),
124 | });
125 | historySizeRow.add_suffix(historySizeSpinBox);
126 |
127 | const historyKeepingModeDropDown = new Gtk.DropDown({
128 | model: Gtk.StringList.new([
129 | _(`None`, `Don't keep entries`),
130 | _(`Pinned`, `Keep only pinned entries`),
131 | _(`All`, `Keep all entries`),
132 | ]),
133 | valign: Gtk.Align.CENTER,
134 | });
135 | historyKeepingModeDropDown.connect(`notify::selected`, () => {
136 | window._preferences.historyKeepingMode = historyKeepingModeDropDown.selected;
137 | });
138 | historyKeepingModeDropDown.selected = window._preferences.historyKeepingMode;
139 | window._preferences.connect(`historyKeepingModeChanged`, () => {
140 | historyKeepingModeDropDown.selected = window._preferences.historyKeepingMode;
141 | });
142 |
143 | const historyKeepingModeRow = new Adw.ActionRow({
144 | activatable_widget: historyKeepingModeDropDown,
145 | title: _(`History entries to keep`),
146 | });
147 | historyKeepingModeRow.add_suffix(historyKeepingModeDropDown);
148 |
149 | const surroundingWhitespaceSwitch = new Gtk.Switch({
150 | valign: Gtk.Align.CENTER,
151 | });
152 | window._preferences.bind(
153 | window._preferences._keyShowSurroundingWhitespace,
154 | surroundingWhitespaceSwitch,
155 | `active`,
156 | Gio.SettingsBindFlags.DEFAULT
157 | );
158 |
159 | const surroundingWhitespaceRow = new Adw.ActionRow({
160 | activatable_widget: surroundingWhitespaceSwitch,
161 | title: _(`Show leading and trailing whitespace`),
162 | });
163 | surroundingWhitespaceRow.add_suffix(surroundingWhitespaceSwitch);
164 |
165 | const colorPreviewSwitch = new Gtk.Switch({
166 | valign: Gtk.Align.CENTER,
167 | });
168 | window._preferences.bind(
169 | window._preferences._keyShowColorPreview,
170 | colorPreviewSwitch,
171 | `active`,
172 | Gio.SettingsBindFlags.DEFAULT
173 | );
174 |
175 | const colorPreviewRow = new Adw.ActionRow({
176 | activatable_widget: colorPreviewSwitch,
177 | title: _(`Show color preview`),
178 | });
179 | colorPreviewRow.add_suffix(colorPreviewSwitch);
180 |
181 | const menuMaxSizeDropDown = new Gtk.DropDown({
182 | model: Gtk.StringList.new([
183 | _(`Small`, `Small menu size`),
184 | _(`Medium`, `Medium menu size`),
185 | _(`Large`, `Large menu size`),
186 | ]),
187 | valign: Gtk.Align.CENTER,
188 | });
189 | menuMaxSizeDropDown.connect(`notify::selected`, () => {
190 | window._preferences.menuMaxSize = menuMaxSizeDropDown.selected;
191 | });
192 | menuMaxSizeDropDown.selected = window._preferences.menuMaxSize;
193 | window._preferences.connect(`menuMaxSizeChanged`, () => {
194 | menuMaxSizeDropDown.selected = window._preferences.menuMaxSize;
195 | });
196 |
197 | const menuMaxSizeRow = new Adw.ActionRow({
198 | activatable_widget: menuMaxSizeDropDown,
199 | title: _(`Maximum menu size`),
200 | });
201 | menuMaxSizeRow.add_suffix(menuMaxSizeDropDown);
202 |
203 | const generalGroup = new Adw.PreferencesGroup({
204 | title: _(`General`, `General options`),
205 | });
206 | generalGroup.add(historySizeRow);
207 | generalGroup.add(historyKeepingModeRow);
208 | generalGroup.add(surroundingWhitespaceRow);
209 | generalGroup.add(colorPreviewRow);
210 | generalGroup.add(menuMaxSizeRow);
211 |
212 | const customSearchUrlEntry = new Gtk.Entry({
213 | placeholder_text: _(`URL with %s in place of query`),
214 | valign: Gtk.Align.CENTER,
215 | });
216 | customSearchUrlEntry.set_size_request(300, -1);
217 | window._preferences.bind(
218 | window._preferences._keyCustomWebSearchUrl,
219 | customSearchUrlEntry,
220 | `text`,
221 | Gio.SettingsBindFlags.DEFAULT
222 | );
223 |
224 | const customSearchUrlRow = new Adw.ActionRow({
225 | activatable_widget: customSearchUrlEntry,
226 | title: _(`Search URL`),
227 | });
228 | customSearchUrlRow.add_suffix(customSearchUrlEntry);
229 | customSearchUrlRow.connect(`notify::visible`, () => {
230 | if (customSearchUrlRow.visible) {
231 | customSearchUrlEntry.grab_focus();
232 | }
233 | });
234 |
235 | const searchEngines = SearchEngines.get(window._preferences);
236 | searchEngines.sort();
237 |
238 | const searchEngineDropDown = new Gtk.DropDown({
239 | model: Gtk.StringList.new(searchEngines.map((engine) => {
240 | return engine.title;
241 | })),
242 | valign: Gtk.Align.CENTER,
243 | });
244 | searchEngineDropDown.bind_property_full(
245 | `selected`,
246 | customSearchUrlRow,
247 | `visible`,
248 | GObject.BindingFlags.DEFAULT | GObject.BindingFlags.SYNC_CREATE,
249 | () => {
250 | return [
251 | true,
252 | searchEngines[searchEngineDropDown.selected].name === `custom`,
253 | ];
254 | },
255 | null
256 | );
257 | searchEngineDropDown.connect(`notify::selected`, () => {
258 | window._preferences.webSearchEngine = searchEngines[searchEngineDropDown.selected].name;
259 | });
260 | searchEngineDropDown.selected = searchEngines.findIndex(window._preferences.webSearchEngine);
261 | window._preferences.connect(`webSearchEngineChanged`, () => {
262 | searchEngineDropDown.selected = searchEngines.findIndex(window._preferences.webSearchEngine);
263 | });
264 |
265 | const searchEngineRow = new Adw.ActionRow({
266 | activatable_widget: searchEngineDropDown,
267 | title: _(`Search Engine`),
268 | });
269 | searchEngineRow.add_suffix(searchEngineDropDown);
270 |
271 | const webSearchGroup = new Adw.PreferencesGroup({
272 | title: _(`Web Search`),
273 | });
274 | webSearchGroup.add(searchEngineRow);
275 | webSearchGroup.add(customSearchUrlRow);
276 |
277 | const expiryDaysSpinBox = new Gtk.SpinButton({
278 | adjustment: new Gtk.Adjustment({
279 | lower: 1,
280 | upper: 365,
281 | step_increment: 1,
282 | }),
283 | valign: Gtk.Align.CENTER,
284 | });
285 | window._preferences.bind(
286 | window._preferences._keyExpiryDays,
287 | expiryDaysSpinBox,
288 | `value`,
289 | Gio.SettingsBindFlags.DEFAULT
290 | );
291 |
292 | const expiryDaysRow = new Adw.ActionRow({
293 | activatable_widget: expiryDaysSpinBox,
294 | title: _(`Days to keep the shared text`, `The number of days to keep the shared text`),
295 | });
296 | expiryDaysRow.add_suffix(expiryDaysSpinBox);
297 |
298 | const sharingOnlineGroup = new Adw.PreferencesGroup({
299 | title: _(`Sharing Online`),
300 | });
301 | sharingOnlineGroup.add(expiryDaysRow);
302 |
303 | const keybindingGroup = new Adw.PreferencesGroup({
304 | title: _(`Keyboard Shortcuts`),
305 | });
306 | keybindingGroup.add(new ShortcutRow(
307 | _(`Toggle menu`),
308 | window._preferences,
309 | window._preferences._keyToggleMenuShortcut
310 | ));
311 | keybindingGroup.add(new ShortcutRow(
312 | _(`Toggle private mode`),
313 | window._preferences,
314 | window._preferences._keyTogglePrivateModeShortcut
315 | ));
316 | keybindingGroup.add(new ShortcutRow(
317 | _(`Clear history`),
318 | window._preferences,
319 | window._preferences._keyClearHistoryShortcut
320 | ));
321 |
322 | const page = new Adw.PreferencesPage();
323 | page.add(generalGroup);
324 | page.add(webSearchGroup);
325 | page.add(sharingOnlineGroup);
326 | page.add(keybindingGroup);
327 |
328 | window.add(page);
329 | }
330 |
--------------------------------------------------------------------------------
/schemas/org.gnome.shell.extensions.clipman.gschema.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
13 |
14 |
15 | 15
16 |
17 |
18 |
19 | "none"
20 |
21 |
22 | true
23 |
24 |
25 | true
26 |
27 |
28 | "medium"
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | "duckduckgo"
45 |
46 |
47 | ""
48 |
49 |
50 | 7
51 |
52 |
53 |
54 | Z']]]>
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/stylesheet.css:
--------------------------------------------------------------------------------
1 | .clipman-popupsearchmenuitem {
2 | border-radius: 18px;
3 | padding: 7px 12px;
4 | }
5 |
6 | .clipman-searchmenuitem {
7 | padding: 9px 12px;
8 | }
9 |
10 | .clipman-placeholderpanel {
11 | spacing: 4px;
12 | }
13 |
14 | .clipman-toolbuttonnpanel {
15 | spacing: 10px;
16 | }
17 |
18 | .clipman-toolbutton {
19 | padding: 2px;
20 | }
21 |
22 | .clipman-colorpreview {
23 | icon-size: 16px;
24 | }
25 |
26 | .clipman-toolbutton .system-status-icon {
27 | icon-size: 16px;
28 | }
29 |
--------------------------------------------------------------------------------