├── .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 | [![Support via Buy Me A Coffee](https://www.buymeacoffee.com/assets/img/guidelines/download-assets-sm-1.svg)](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 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /icons/private-mode-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 | 9 | 10 | 11 | 12 | 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 | --------------------------------------------------------------------------------