├── .gitignore ├── logo ├── logo.png ├── logo-128.png ├── resize.sh ├── logo.svg └── logo.dxf ├── src ├── gfx │ ├── logo │ │ ├── logo-16.png │ │ ├── logo-24.png │ │ ├── logo-32.png │ │ ├── logo-48.png │ │ ├── logo-64.png │ │ └── logo-96.png │ └── icon.svg ├── options │ ├── dark.css │ ├── options.css │ ├── options.html │ ├── options.js │ └── options.backup.js ├── background │ ├── background.html │ ├── migrate.js │ ├── commands.js │ ├── core.js │ ├── addon.tabs.js │ ├── addon.js │ ├── addon.tabs.events.js │ ├── background.js │ ├── backup.js │ └── addon.tabGroups.js ├── panorama │ ├── gfx │ │ ├── close.svg │ │ └── add.svg │ ├── js │ │ ├── html.js │ │ ├── view.events.js │ │ ├── html.tabs.js │ │ ├── view.drag.js │ │ ├── view.js │ │ └── html.groups.js │ ├── css │ │ ├── dark.css │ │ ├── limbo │ │ │ └── backdrop.css │ │ ├── view.css │ │ ├── group.css │ │ └── tab.css │ └── view.html ├── common │ ├── html.js │ ├── mutex.js │ ├── plurals.js │ ├── colors.js │ ├── theme.js │ └── tabGroups-polyfill.js ├── manifest.json └── _locales │ ├── ja │ └── messages.json │ └── en │ └── messages.json ├── README.md ├── TRANSLATION.md ├── Tab Grouping API.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | git.sh 2 | archive/* 3 | testing 4 | -------------------------------------------------------------------------------- /logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/photodiode/panorama-view/HEAD/logo/logo.png -------------------------------------------------------------------------------- /logo/logo-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/photodiode/panorama-view/HEAD/logo/logo-128.png -------------------------------------------------------------------------------- /src/gfx/logo/logo-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/photodiode/panorama-view/HEAD/src/gfx/logo/logo-16.png -------------------------------------------------------------------------------- /src/gfx/logo/logo-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/photodiode/panorama-view/HEAD/src/gfx/logo/logo-24.png -------------------------------------------------------------------------------- /src/gfx/logo/logo-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/photodiode/panorama-view/HEAD/src/gfx/logo/logo-32.png -------------------------------------------------------------------------------- /src/gfx/logo/logo-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/photodiode/panorama-view/HEAD/src/gfx/logo/logo-48.png -------------------------------------------------------------------------------- /src/gfx/logo/logo-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/photodiode/panorama-view/HEAD/src/gfx/logo/logo-64.png -------------------------------------------------------------------------------- /src/gfx/logo/logo-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/photodiode/panorama-view/HEAD/src/gfx/logo/logo-96.png -------------------------------------------------------------------------------- /src/options/dark.css: -------------------------------------------------------------------------------- 1 | 2 | body.dark { 3 | --color-group: #23222b; 4 | --color-group-text: #bfbfc9; 5 | } 6 | -------------------------------------------------------------------------------- /src/background/background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/panorama/gfx/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/panorama/js/html.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | import * as commmon from '/common/html.js' 5 | import * as groups from './html.groups.js' 6 | import * as tabs from './html.tabs.js' 7 | 8 | export const html = { 9 | newElement: commmon.newElement, 10 | groups: groups, 11 | tabs: tabs 12 | }; 13 | -------------------------------------------------------------------------------- /logo/resize.sh: -------------------------------------------------------------------------------- 1 | 2 | convert logo.png -resize 96x96 ../src/gfx/logo/logo-96.png 3 | convert logo.png -resize 64x64 ../src/gfx/logo/logo-64.png 4 | convert logo.png -resize 48x48 ../src/gfx/logo/logo-48.png 5 | convert logo.png -resize 32x32 ../src/gfx/logo/logo-32.png 6 | convert logo.png -resize 24x24 ../src/gfx/logo/logo-24.png 7 | convert logo.png -resize 16x16 ../src/gfx/logo/logo-16.png 8 | -------------------------------------------------------------------------------- /src/panorama/gfx/add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/panorama/css/dark.css: -------------------------------------------------------------------------------- 1 | 2 | body.dark { 3 | --color-background: #1e1e1e; 4 | --color-shadow: rgba(255, 255, 255, 0.15); 5 | 6 | --color-group: #303030; 7 | --color-group-text: #f0f0f0; 8 | --color-group-border: #454545; 9 | 10 | --color-tab: #1e1e1e; 11 | --color-tab-text: #f0f0f0; 12 | --color-tab-overlay: rgba(0, 0, 0, 0.8); 13 | --color-tab-hover: rgba(255, 255, 255, 0.3); 14 | --color-tab-active: #45a1ff; 15 | } 16 | 17 | .dark .tab .thumbnail { 18 | opacity: 0.85; 19 | } 20 | -------------------------------------------------------------------------------- /src/common/html.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | export function newElement(name, attributes, children) { 5 | 6 | const e = document.createElement(name); 7 | 8 | for (const key in attributes) { 9 | if (key == 'content') { 10 | e.appendChild(document.createTextNode(attributes[key])); 11 | } else { 12 | e.setAttribute(key.replace(/_/g, '-'), attributes[key]); 13 | } 14 | } 15 | 16 | for (const child of children || []) { 17 | e.appendChild(child); 18 | } 19 | 20 | return e; 21 | } 22 | -------------------------------------------------------------------------------- /src/panorama/css/limbo/backdrop.css: -------------------------------------------------------------------------------- 1 | 2 | .backdrop #toolbar { 3 | background-color: rgba(249, 249, 250, 0.4); 4 | backdrop-filter: blur(20px); 5 | } 6 | 7 | .backdrop .group::before { 8 | background-color: rgba(255, 255, 255, 0.6); 9 | backdrop-filter: brightness(0.95) saturate(250%) blur(32px); 10 | } 11 | 12 | 13 | .backdrop .tab .favicon { 14 | backdrop-filter: saturate(250%) brightness(2) blur(3px); 15 | } 16 | 17 | .backdrop .tab .name span { 18 | backdrop-filter: saturate(250%) brightness(2) blur(3px); 19 | } 20 | -------------------------------------------------------------------------------- /src/options/options.css: -------------------------------------------------------------------------------- 1 | 2 | body.light { 3 | --color-group: #ffffff; 4 | --color-group-text: #111111; 5 | } 6 | 7 | body { 8 | background-color: var(--color-group); 9 | color: var(--color-group-text); 10 | } 11 | 12 | label { 13 | width: 35%; 14 | height: 2em; 15 | 16 | margin: 0.25em 0; 17 | 18 | display: inline-block; 19 | } 20 | 21 | span { 22 | font-weight: bold; 23 | } 24 | 25 | p { 26 | opacity: 0.75; 27 | } 28 | 29 | input[type="range"] { 30 | width: 40%; 31 | margin-right: 1ch; 32 | vertical-align: middle; 33 | } 34 | -------------------------------------------------------------------------------- /src/common/mutex.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | class Mutex { 5 | constructor() { 6 | this._locking = Promise.resolve(); 7 | this._locked = false; 8 | } 9 | 10 | isLocked() { 11 | return this._locked; 12 | } 13 | 14 | lock() { 15 | this._locked = true; 16 | 17 | let unlockNext; 18 | let willLock = new Promise(resolve => unlockNext = resolve); 19 | 20 | willLock.then(() => this._locked = false); 21 | 22 | let willUnlock = this._locking.then(() => unlockNext); 23 | this._locking = this._locking.then(() => willLock); 24 | 25 | return willUnlock; 26 | } 27 | } 28 | 29 | export default Mutex; 30 | -------------------------------------------------------------------------------- /src/gfx/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Panorama View 2 | 3 | [![Version](https://img.shields.io/amo/v/panorama-view?label=version)](https://addons.mozilla.org/firefox/addon/panorama-view/) 4 | [![Daily users](https://img.shields.io/amo/users/panorama-view)](https://addons.mozilla.org/firefox/addon/panorama-view/) 5 | [![Rating](https://img.shields.io/amo/rating/panorama-view)](https://addons.mozilla.org/firefox/addon/panorama-view/reviews/) 6 | 7 | Add-on for Firefox letting you organize tabs and tab groups visually. 8 | 9 | [https://addons.mozilla.org/firefox/addon/panorama-view/](https://addons.mozilla.org/firefox/addon/panorama-view/) 10 | -------------------------------------------------------------------------------- /src/panorama/view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Panorama View 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 |
23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /src/common/plurals.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | const pluralRule = new Intl.PluralRules(browser.i18n.getUILanguage()); 5 | const pluralCategories = pluralRule.resolvedOptions().pluralCategories; // zero, one, two, few, many, other 6 | 7 | export function parse(message) { 8 | 9 | const plurals = message.matchAll(/[0-9]+\{[^\}]*\}/g); 10 | 11 | for (const match of plurals) { 12 | // parse 13 | const plural = match[0].replaceAll(/\s*,\s*/g, ','); // clean up whitespace around commas 14 | 15 | const countString = plural.match(/[^\{]*/).pop(); 16 | const listString = plural.substring(countString.length+1, plural.length-1); 17 | 18 | const count = parseInt(countString, 10); 19 | const list = listString.split(','); 20 | // ---- 21 | 22 | const ruleIndex = pluralCategories.indexOf(pluralRule.select(count)); 23 | const output = list[ruleIndex] || list[0]; 24 | 25 | message = message.replace(match[0], output); 26 | } 27 | return message; 28 | } 29 | -------------------------------------------------------------------------------- /TRANSLATION.md: -------------------------------------------------------------------------------- 1 | # Translations 2 | To make a translation a new folder with the name of the [Language Code](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/i18n/LanguageCode) has to be made inside "_locales". 3 | This folder should contain a file called "messages.json" ([MDN documentation](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/i18n/Locale-Specific_Message_reference)) 4 | 5 | 6 | ## Plurals 7 | Plural list: `$1{tab, tabs}` 8 | 9 | The parsing uses JavaScript's [Intl.PluralRules.select()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/select) function with the current locale to get one of the categories `zero`, `one`, `two`, `few`, `many` and `other` according to [these rules](https://developer.mozilla.org.cach3.com/en/Localization_and_Plurals). 10 | 11 | Example sentence: 12 | 13 | `This group has $1{one tab, $1 tabs}.` 14 | 15 | English (en-*) only has two categories (`one`, `other`) so only two list entries are needed. 16 | 17 | Output: 18 | 19 | - 0: "This group has 0 tabs." 20 | - 1: "This group has one tab." 21 | - 2: "This group has 2 tabs." 22 | - 8: "This group has 8 tabs." 23 | -------------------------------------------------------------------------------- /logo/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/background/migrate.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | // migrate to transformable groups 5 | export async function migrate() { 6 | 7 | // migrate to version 0.9.0 8 | // remove old panorama view tabs 9 | let extensionTabs = await browser.tabs.query({url: browser.runtime.getURL('view.html')}); 10 | if (extensionTabs) { 11 | for (let tab of extensionTabs) { 12 | browser.tabs.remove(tab.id); 13 | } 14 | } 15 | // ---- 16 | 17 | const windows = await browser.windows.getAll({}); 18 | 19 | for (const window of windows) { 20 | let groups = await browser.sessions.getWindowValue(window.id, 'groups'); 21 | 22 | if (groups) { 23 | for (let group of groups) { 24 | if (group.title == undefined) { 25 | group.title = group.name; 26 | group.lastAccessed = group.lastMoved; 27 | } 28 | } 29 | await browser.sessions.setWindowValue(window.id, 'groups', groups); 30 | } 31 | } 32 | // ---- 33 | 34 | // migrate from 0.9.3 to 0.9.4 (copy rect data to new location) 35 | for (const window of windows) { 36 | let groups = await browser.sessions.getWindowValue(window.id, 'groups'); 37 | 38 | if (groups) { 39 | for (let group of groups) { 40 | if (!group.hasOwnProperty('sessionStorage') && group.hasOwnProperty('rect')) { 41 | group.sessionStorage = {}; 42 | group.sessionStorage[browser.runtime.id] = {}; 43 | group.sessionStorage[browser.runtime.id]['rect'] = JSON.stringify(group.rect); 44 | //delete group['rect']; // maybe at some point 45 | } 46 | } 47 | await browser.sessions.setWindowValue(window.id, 'groups', groups); 48 | } 49 | 50 | } 51 | // ---- 52 | } 53 | -------------------------------------------------------------------------------- /src/background/commands.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | import {addon} from './addon.js'; 5 | import * as core from './core.js'; 6 | 7 | export async function handleCommands(command) { 8 | switch (command) { 9 | case 'toggle_panorama_view': { 10 | core.toggleView(); 11 | break; 12 | } 13 | 14 | case 'new_tab_group': { 15 | const group = await addon.tabGroups.create({populate: true}, (await browser.windows.getCurrent()).id); 16 | break; 17 | } 18 | case 'next_group': { 19 | const windowId = (await browser.windows.getCurrent()).id; 20 | const groups = await browser.sessions.getWindowValue(windowId, 'groups'); 21 | 22 | let activeGroup = (await browser.sessions.getWindowValue(windowId, 'activeGroup')); 23 | let activeIndex = groups.findIndex(function(group){ return group.id === activeGroup; }); 24 | let newIndex = (activeIndex + 1) % groups.length; 25 | 26 | activeGroup = groups[newIndex].id; 27 | await browser.sessions.setWindowValue(windowId, 'activeGroup', activeGroup); 28 | 29 | await core.toggleVisibleTabs(windowId, activeGroup, true); 30 | 31 | break; 32 | } 33 | case 'previous_group': { 34 | const windowId = (await browser.windows.getCurrent()).id; 35 | const groups = await browser.sessions.getWindowValue(windowId, 'groups'); 36 | 37 | let activeGroup = (await browser.sessions.getWindowValue(windowId, 'activeGroup')); 38 | let activeIndex = groups.findIndex(function(group){ return group.id === activeGroup; }); 39 | let newIndex = activeIndex - 1; 40 | newIndex = (newIndex < 0) ? groups.length-1 : newIndex; 41 | 42 | activeGroup = groups[newIndex].id; 43 | await browser.sessions.setWindowValue(windowId, 'activeGroup', activeGroup); 44 | 45 | await core.toggleVisibleTabs(windowId, activeGroup, true); 46 | 47 | break; 48 | } 49 | default: 50 | console.log('Unknown command'); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/panorama/css/view.css: -------------------------------------------------------------------------------- 1 | 2 | html, body { 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | body.light { 8 | --color-background: #ebebeb; 9 | --color-shadow: rgba(0, 0, 0, 0.15); 10 | 11 | --color-group: #fbfbfb; 12 | --color-group-text: #111111; 13 | --color-group-border: rgba(0, 0, 0, 0.1); 14 | 15 | --color-tab: #ffffff; 16 | --color-tab-text: #111111; 17 | --color-tab-overlay: rgba(255, 255, 255, 0.8); 18 | --color-tab-hover: rgba(0, 0, 0, 0.2); 19 | --color-tab-active: #45a1ff; 20 | } 21 | 22 | body { 23 | margin: 0; 24 | 25 | background-color: var(--color-background); 26 | 27 | font-family: sans-serif; 28 | -moz-user-select: none; 29 | 30 | display: grid; 31 | grid-template-rows: [top] min-content [toolbar] auto [bottom]; 32 | } 33 | 34 | /*#search { 35 | margin: 0 7px 36 | } 37 | 38 | #search input { 39 | width: 100%; 40 | max-width: 40ch; 41 | height: 28px; 42 | 43 | margin: 8px auto 3px; 44 | padding: 0 8px; 45 | 46 | display: block; 47 | 48 | border-radius: 6px; 49 | border: none; 50 | 51 | background-color: var(--color-group-background); 52 | 53 | color: var(--color-text); 54 | 55 | transition: height 300ms ease-in-out, margin 300ms ease-in-out, opacity 300ms ease-in-out; 56 | } 57 | 58 | #search.hidden input { 59 | height: 0px; 60 | 61 | margin: 0px auto 0px; 62 | 63 | opacity: 0; 64 | }*/ 65 | 66 | #pinned, #groupless { 67 | display: none; 68 | } 69 | 70 | #groups { 71 | margin: 3px; 72 | 73 | position: relative; 74 | 75 | grid-row: toolbar / bottom; 76 | justify-self: stretch; 77 | } 78 | 79 | /* phantom new group button to lure people into double or middle clicking the background */ 80 | #groups::before { 81 | content: ''; 82 | 83 | width: 32px; 84 | height: 32px; 85 | 86 | display: block; 87 | 88 | position: absolute; 89 | top: 0; 90 | right: 0; 91 | 92 | background-color: var(--color-group-text); 93 | 94 | mask: no-repeat center / 16px; 95 | mask-image: url(../gfx/add.svg); 96 | 97 | opacity: 0.2; 98 | } 99 | -------------------------------------------------------------------------------- /src/common/colors.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | export function Color(input, input_b, input_c, input_d) { 5 | 6 | this.r = 0; 7 | this.g = 0; 8 | this.b = 0; 9 | this.a = 1; 10 | 11 | if (typeof input == 'string') { 12 | 13 | // get computed color 14 | const tmpElement = document.body.appendChild(document.createElement('tmpColorElement')); 15 | tmpElement.style.color = input; 16 | 17 | const computedColor = window.getComputedStyle(tmpElement).color; 18 | 19 | tmpElement.remove(); 20 | // ---- 21 | 22 | let color = computedColor.match(/[\.\d]+/g); 23 | 24 | if (!color) color = [0, 0, 0, 1]; 25 | 26 | if (color.length == 3) color.push(1); 27 | if (color.length != 4) color = [0, 0, 0, 1]; 28 | 29 | this.r = Number(color[0]) / 255; 30 | this.g = Number(color[1]) / 255; 31 | this.b = Number(color[2]) / 255; 32 | this.a = Number(color[3]); 33 | 34 | } else if (input != undefined && 35 | input_b != undefined && 36 | input_c != undefined && 37 | input_d != undefined) { 38 | 39 | this.r = input; 40 | this.g = input_b; 41 | this.b = input_c; 42 | this.a = input_d; 43 | } 44 | 45 | this.toCSS = function() { 46 | return `rgba(${this.r * 255}, ${this.g * 255}, ${this.b * 255}, ${this.a})`; 47 | }; 48 | 49 | this.mix = function(that) { 50 | const lerp = function(a, b, t){ 51 | return a * (1 - t) + b * t; 52 | } 53 | return new Color( 54 | lerp(this.r, that.r, that.a), 55 | lerp(this.g, that.g, that.a), 56 | lerp(this.b, that.b, that.a), 57 | Math.max(this.a, that.a), 58 | ); 59 | }; 60 | 61 | this.toGrayscale = function() { 62 | const gray = (0.2126 * this.r + 0.7152 * this.g + 0.0722 * this.b); 63 | return new Color(gray, gray, gray, this.a); 64 | }; 65 | 66 | this.distance = function(that) { 67 | const r = that.r - this.r; 68 | const g = that.g - this.g; 69 | const b = that.b - this.b; 70 | return Math.sqrt(r*r + g*g + b*b) / 1.7320508075688772; 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Panorama View", 4 | "description": "Easily group and organize tabs visually", 5 | "version": "0.9.8", 6 | "icons": { 7 | "16": "gfx/logo/logo-16.png", 8 | "24": "gfx/logo/logo-24.png", 9 | "32": "gfx/logo/logo-32.png", 10 | "48": "gfx/logo/logo-48.png", 11 | "64": "gfx/logo/logo-64.png", 12 | "96": "gfx/logo/logo-96.png" 13 | }, 14 | "applications": { 15 | "gecko": { 16 | "id": "{60e27487-c779-464c-8698-ad481b718d5f}" 17 | } 18 | }, 19 | "default_locale": "en", 20 | 21 | "commands": { 22 | "toggle_panorama_view": { 23 | "suggested_key": { 24 | "default": "Ctrl+Alt+F", 25 | "mac": "MacCtrl+Alt+F" 26 | }, 27 | "description": "__MSG_togglePanoramaView__" 28 | }, 29 | "new_tab_group": { 30 | "suggested_key": { 31 | "default": "Ctrl+Alt+G", 32 | "mac": "MacCtrl+Alt+G" 33 | }, 34 | "description": "__MSG_newTabGroup__" 35 | }, 36 | "next_group": { 37 | "suggested_key": { 38 | "default": "Ctrl+Alt+W", 39 | "mac": "MacCtrl+Alt+W" 40 | }, 41 | "description": "__MSG_nextTabGroup__" 42 | }, 43 | "previous_group": { 44 | "suggested_key": { 45 | "default": "Ctrl+Alt+Q", 46 | "mac": "MacCtrl+Alt+Q" 47 | }, 48 | "description": "__MSG_previousTabGroup__" 49 | } 50 | }, 51 | 52 | "background": { 53 | "page": "background/background.html" 54 | }, 55 | 56 | "browser_action": { 57 | "browser_style": true, 58 | "default_icon": "gfx/icon.svg", 59 | "theme_icons": [{ 60 | "light": "gfx/icon.svg", 61 | "dark": "gfx/icon.svg", 62 | "size": 16 63 | }, 64 | { 65 | "light": "gfx/icon.svg", 66 | "dark": "gfx/icon.svg", 67 | "size": 32 68 | }] 69 | }, 70 | 71 | "options_ui": { 72 | "page": "options/options.html", 73 | "browser_style": true 74 | }, 75 | 76 | "permissions": [ 77 | "", 78 | "tabs", 79 | "tabHide", 80 | "storage", 81 | "sessions", 82 | "cookies", 83 | "contextualIdentities", 84 | "downloads", 85 | "menus" 86 | ] 87 | } 88 | -------------------------------------------------------------------------------- /src/_locales/ja/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultGroupName": { 3 | "message": "無名グループ" 4 | }, 5 | 6 | "togglePanoramaView": { 7 | "message": "Panorama Viewを切り替える" 8 | }, 9 | 10 | "newTabGroup": { 11 | "message": "新しいグループ" 12 | }, 13 | "nextTabGroup": { 14 | "message": "次のグループへ" 15 | }, 16 | "previousTabGroup": { 17 | "message": "前のグループへ" 18 | }, 19 | 20 | 21 | "optionKeyboardShortcuts": { 22 | "message": "ショートカットキー" 23 | }, 24 | "optionKeyboardShortcutsDirections": { 25 | "message": "ショートカットキーを変更する場合は右上の「アドオンツール」に入ってる「拡張機能のショートカットキーの管理」を使ってください。" 26 | }, 27 | 28 | "optionGeneral": { 29 | "message": "一般" 30 | }, 31 | "optionTheme": { 32 | "message": "テーマ" 33 | }, 34 | "optionThemeAuto": { 35 | "message": "システムに合わせる" 36 | }, 37 | "optionThemeCustom": { 38 | "message": "ブラウザに合わせる" 39 | }, 40 | "optionThemeLight": { 41 | "message": "ライトモード(明るい)" 42 | }, 43 | "optionThemeDark": { 44 | "message": "ダークモード(暗い)" 45 | }, 46 | "optionListView": { 47 | "message": "小さなタブの一覧表示" 48 | }, 49 | 50 | "optionBackup": { 51 | "message": "バックアップ" 52 | }, 53 | "optionSaveBackup": { 54 | "message": "新しいバックアップを保存" 55 | }, 56 | "optionSaveBackupButton": { 57 | "message": "保存..." 58 | }, 59 | "optionLoadBackup": { 60 | "message": "バックアップを読み込む" 61 | }, 62 | "optionLoadBackupSelect": { 63 | "message": "バックアップを選択" 64 | }, 65 | "optionLoadBackupBrowse": { 66 | "message": "参照..." 67 | }, 68 | "optionLoadBackupButton": { 69 | "message": "開く" 70 | }, 71 | "optionLoadError": { 72 | "message": "無効なファイルタイプ" 73 | }, 74 | "optionAutoBackup": { 75 | "message": "自動バックアップ" 76 | }, 77 | "optionAutomaticBackupInterval": { 78 | "message": "自動バックアップの頻度" 79 | }, 80 | "optionAutomaticBackupIntervalNever": { 81 | "message": "しない" 82 | }, 83 | "optionAutomaticBackupIntervalValue": { 84 | "message": "$1時間$2分" 85 | }, 86 | 87 | "optionStatistics": { 88 | "message": "情報" 89 | }, 90 | "optionNumberOfTabs": { 91 | "message": "タブの数" 92 | }, 93 | "optionNumberOfTabsValue": { 94 | "message": "$1(有効状態、$2つ)" 95 | }, 96 | "optionThumbnailCache": { 97 | "message": " サムネイルのキャッシュ" 98 | }, 99 | 100 | 101 | "pvCloseGroupConfirmation": { 102 | "message": "グループの$1タブを閉じる?" 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/background/core.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | import {addon} from './addon.js'; 5 | 6 | export let openingPanoramaView = false; 7 | 8 | 9 | export function setOpeningPanoramaView(value) { 10 | openingPanoramaView = value; 11 | } 12 | 13 | 14 | export function getPanoramaViewURL() { 15 | return browser.runtime.getURL('panorama/view.html'); 16 | } 17 | 18 | 19 | export async function getPanoramaViewTab() { 20 | const tabs = await browser.tabs.query({url: getPanoramaViewURL(), currentWindow: true}); 21 | if (tabs.length > 0) { 22 | return tabs[0]; 23 | } else { 24 | return undefined; 25 | } 26 | } 27 | 28 | 29 | // Show and hide the appropriate tabs 30 | export async function toggleVisibleTabs(windowId, activeGroupId, noTabSelected) { 31 | 32 | const tabs = await browser.tabs.query({windowId: windowId}); 33 | 34 | let showTabIds = []; 35 | let hideTabIds = []; 36 | 37 | let showTabs = []; 38 | 39 | await Promise.all(tabs.map(async(tab) => { 40 | const groupId = await addon.tabs.getGroupId(tab.id); 41 | 42 | if (groupId != activeGroupId) { 43 | hideTabIds.push(tab.id); 44 | } else { 45 | showTabIds.push(tab.id); 46 | showTabs.push(tab); 47 | } 48 | })); 49 | 50 | if (noTabSelected) { 51 | showTabs.sort((tabA, tabB) => { 52 | return tabB.lastAccessed - tabA.lastAccessed; 53 | }); 54 | browser.tabs.update(showTabs[0].id, {active: true}); 55 | } 56 | 57 | browser.tabs.hide(hideTabIds); 58 | browser.tabs.show(showTabIds); 59 | } 60 | 61 | 62 | export async function toggleView() { 63 | 64 | const panoramaViewTab = await getPanoramaViewTab(); 65 | 66 | if (panoramaViewTab) { 67 | 68 | const currentTab = (await browser.tabs.query({active: true, currentWindow: true}))[0]; 69 | 70 | // switch to last accessed tab in window 71 | if (panoramaViewTab.id == currentTab.id) { 72 | let tabs = await browser.tabs.query({currentWindow: true}); 73 | 74 | tabs.sort((tabA, tabB) => { 75 | return tabB.lastAccessed - tabA.lastAccessed; 76 | }); 77 | // skip first tab which will be the panorama view 78 | if (tabs.length > 1) { 79 | browser.tabs.update(tabs[1].id, {active: true}); 80 | } 81 | // switch to Panorama View tab 82 | } else { 83 | browser.tabs.update(panoramaViewTab.id, {active: true}); 84 | } 85 | // if there is no Panorama View tab, make one 86 | } else { 87 | openingPanoramaView = true; 88 | addon.tabs.create({url: '/panorama/view.html', active: true, groupId: -1}); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultGroupName": { 3 | "message": "Unnamed" 4 | }, 5 | 6 | "newTabGroup": { 7 | "message": "New Tab Group" 8 | }, 9 | "nextTabGroup": { 10 | "message": "Next Tab Group" 11 | }, 12 | "previousTabGroup": { 13 | "message": "Previous Tab Group" 14 | }, 15 | 16 | "moveTabToGroup": { 17 | "message": "Move to Group" 18 | }, 19 | 20 | 21 | "optionKeyboardShortcuts": { 22 | "message": "Keyboard Shortcuts" 23 | }, 24 | "optionKeyboardShortcutsDirections": { 25 | "message": "To change shortcuts please go to the Gear Icon in the top right and choose \"Manage Extension Shortcuts\"" 26 | }, 27 | 28 | "optionGeneral": { 29 | "message": "General" 30 | }, 31 | "optionTheme": { 32 | "message": "Theme" 33 | }, 34 | "optionThemeAuto": { 35 | "message": "System" 36 | }, 37 | "optionThemeCustom": { 38 | "message": "Browser" 39 | }, 40 | "optionThemeLight": { 41 | "message": "Light" 42 | }, 43 | "optionThemeDark": { 44 | "message": "Dark" 45 | }, 46 | "optionListView": { 47 | "message": "List view for tiny tabs" 48 | }, 49 | 50 | "optionBackup": { 51 | "message": "Backup" 52 | }, 53 | "optionSaveBackup": { 54 | "message": "Save new Backup to file" 55 | }, 56 | "optionSaveBackupButton": { 57 | "message": "Save As..." 58 | }, 59 | "optionLoadBackup": { 60 | "message": "Load a Backup" 61 | }, 62 | "optionLoadBackupSelect": { 63 | "message": "Select Backup" 64 | }, 65 | "optionLoadBackupBrowse": { 66 | "message": "Browse..." 67 | }, 68 | "optionLoadBackupButton": { 69 | "message": "Load" 70 | }, 71 | "optionLoadError": { 72 | "message": "Invalid file type" 73 | }, 74 | "optionAutoBackup": { 75 | "message": "Auto Backup" 76 | }, 77 | "optionAutomaticBackupInterval": { 78 | "message": "Automatic Backup interval" 79 | }, 80 | "optionAutomaticBackupIntervalNever": { 81 | "message": "Never" 82 | }, 83 | "optionAutomaticBackupIntervalValue": { 84 | "message": "$1h $2min" 85 | }, 86 | 87 | "optionStatistics": { 88 | "message": "Statistics" 89 | }, 90 | "optionNumberOfTabs": { 91 | "message": "Number of Tabs" 92 | }, 93 | "optionNumberOfTabsValue": { 94 | "message": "$1 ($2 active)" 95 | }, 96 | "optionThumbnailCache": { 97 | "message": "Thumbnail Cache" 98 | }, 99 | 100 | 101 | "togglePanoramaView": { 102 | "message": "Toggle Panorama View" 103 | }, 104 | "pvCloseGroupConfirmation": { 105 | "message": "Close the $1 $1{tab, tabs} in this Group?" 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/options/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

14 |
15 |

16 |
17 |
18 |

19 |
20 |
21 | 22 | 28 |
29 |
30 | 31 | 32 |
33 |
34 |
35 |
36 |

37 |
38 |
39 | 40 | 41 |
42 |
43 | 44 | 45 | 49 | 50 |
51 |
52 | 53 | 54 | 55 |
56 |
57 |
58 |
59 |

60 |
61 |
62 | 63 | 64 |
65 |
66 | 67 | 68 |
69 |
70 |
71 | 72 | 73 | -------------------------------------------------------------------------------- /src/background/addon.tabs.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | export function setGroupId(tabId, groupId) { 5 | return browser.sessions.setTabValue(tabId, 'groupId', groupId); 6 | } 7 | 8 | export function getGroupId(tabId) { 9 | return browser.sessions.getTabValue(tabId, 'groupId'); 10 | } 11 | 12 | export async function getGroupIdTimeout(tabId, timeout) { 13 | let groupId = undefined; 14 | const start = (new Date).getTime(); 15 | while (groupId == undefined && (((new Date).getTime() - start) < timeout)) { 16 | groupId = await browser.sessions.getTabValue(tabId, 'groupId'); 17 | } 18 | return groupId; 19 | } 20 | 21 | 22 | export async function create(info) { 23 | 24 | let groupId = undefined; 25 | 26 | if (info.hasOwnProperty('groupId')) { 27 | groupId = info.groupId; 28 | delete info.groupId; 29 | } 30 | 31 | let tab; 32 | try { 33 | tab = await browser.tabs.create(info); 34 | } catch (error) { 35 | throw Error(error); 36 | } 37 | 38 | // wait for onCreated to add a groupId if none is set 39 | if (groupId == undefined) { 40 | groupId = await getGroupIdTimeout(tab.id, 100); // random timeout 41 | } 42 | 43 | setGroupId(tab.id, groupId); 44 | tab.groupId = groupId; 45 | 46 | return tab; 47 | } 48 | 49 | 50 | export async function get(tabId) { 51 | 52 | let tab; 53 | try { 54 | tab = await browser.tabs.get(tabId); 55 | } catch (error) { 56 | throw Error(error); 57 | } 58 | 59 | tab.groupId = await getGroupIdTimeout(tab.id, 100); 60 | 61 | return tab; 62 | } 63 | 64 | 65 | export async function move(tabIds, info) { 66 | 67 | let groupId = undefined; 68 | 69 | if (info.hasOwnProperty('groupId')) { 70 | groupId = info.groupId; 71 | delete info.groupId; 72 | } 73 | 74 | let tabs; 75 | try { 76 | tabs = await browser.tabs.move(tabIds, info); 77 | } catch (error) { 78 | throw Error(error); 79 | } 80 | 81 | if (groupId != undefined) { 82 | if (Array.isArray(tabIds)) { 83 | await Promise.all(tabIds.map(async(tabId) => { 84 | await setGroupId(tabId, groupId); 85 | })); 86 | } else { 87 | await setGroupId(tabIds, groupId); 88 | } 89 | } 90 | 91 | if (groupId != undefined) { 92 | for (let tab of tabs) { 93 | tab.groupId = groupId; 94 | } 95 | } else { 96 | await Promise.all(tabs.map(async(tab) => { 97 | tab.groupId = await getGroupId(tab.id); 98 | })); 99 | } 100 | 101 | return tabs; 102 | } 103 | 104 | 105 | export async function query(info) { 106 | 107 | let groupId = undefined; 108 | 109 | if (info.hasOwnProperty('groupId')) { 110 | groupId = info.groupId; 111 | delete info.groupId; 112 | } 113 | 114 | let tabs; 115 | try { 116 | tabs = await browser.tabs.query(info); 117 | } catch (error) { 118 | throw Error(error); 119 | } 120 | 121 | await Promise.all(tabs.map(async(tab) => { 122 | tab.groupId = await getGroupId(tab.id); 123 | })); 124 | 125 | if (groupId != undefined) { 126 | tabs = tabs.filter(tab => (tab.groupId == groupId)); 127 | } 128 | 129 | return tabs; 130 | } 131 | -------------------------------------------------------------------------------- /src/panorama/js/view.events.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | import {html} from './html.js' 5 | import * as core from './view.js' 6 | 7 | 8 | export async function groupCreated(group) { 9 | if (core.viewWindowId == group.windowId){ 10 | 11 | const tabGroupNode = html.groups.create(group); 12 | 13 | const rect = {x: 0.3, y: 0.3, w: 0.4, h: 0.4}; 14 | browser.sessions.setGroupValue(group.id, 'rect', rect); 15 | 16 | html.groups.resize(tabGroupNode, rect); 17 | html.groups.stack(tabGroupNode); 18 | 19 | document.getElementById('groups').appendChild(tabGroupNode); 20 | 21 | html.groups.fitTabs(tabGroupNode); 22 | } 23 | } 24 | 25 | 26 | export async function groupRemoved(tabGroupId, removeInfo) { 27 | if (core.viewWindowId == removeInfo.windowId) { 28 | const groupNode = await html.groups.get(tabGroupId); 29 | if (groupNode) { 30 | groupNode.remove(); 31 | } 32 | } 33 | } 34 | 35 | 36 | export async function tabCreated(tab) { 37 | if (core.viewWindowId == tab.windowId) { 38 | 39 | const tabGroupNode = html.groups.get(tab.groupId); 40 | 41 | const tabNode = html.tabs.create(tab); 42 | html.tabs.update(tabNode, tab); 43 | html.tabs.updateThumbnail(tabNode, tab.id); 44 | 45 | await html.tabs.insert(tabNode, tab); 46 | 47 | html.tabs.updateFavicon(tabNode, tab); 48 | html.groups.fitTabs(tabGroupNode); 49 | } 50 | } 51 | 52 | export async function tabRemoved(tabId, removeInfo) { 53 | if (core.viewWindowId == removeInfo.windowId && core.viewTabId != tabId){ 54 | let tabNode = html.tabs.get(tabId); 55 | 56 | tabNode.remove(); 57 | 58 | html.tabs.setActive(); 59 | html.groups.fitTabs(); 60 | } 61 | } 62 | 63 | export async function tabUpdated(tabId, changeInfo, tab) { 64 | 65 | const tabNode = html.tabs.get(tabId); 66 | 67 | if (core.viewWindowId == tab.windowId){ 68 | html.tabs.update(tabNode, tab); 69 | } 70 | 71 | if (changeInfo.pinned != undefined) { 72 | if (changeInfo.pinned) { 73 | document.getElementById('pinned').appendChild(tabNode); 74 | html.groups.fitTabs(); 75 | html.tabs.setActive(); 76 | } else { 77 | await html.tabs.insert(tabNode, tab); 78 | html.groups.fitTabs(); 79 | } 80 | } else { 81 | html.tabs.updateFavicon(tabNode, tab); 82 | } 83 | } 84 | 85 | export async function tabActivated(activeInfo) { 86 | html.tabs.setActive(); 87 | } 88 | 89 | export async function tabMoved(tabId, moveInfo) { 90 | if (core.viewWindowId == moveInfo.windowId) { 91 | 92 | const tab = await browser.tabs.get(tabId); 93 | 94 | const tabNode = html.tabs.get(tabId); 95 | 96 | await html.tabs.insert(tabNode, tab); 97 | 98 | html.groups.fitTabs(); 99 | } 100 | } 101 | 102 | export async function tabAttached(tabId, attachInfo) { 103 | if (core.viewWindowId == attachInfo.newWindowId) { 104 | const tab = await browser.tabs.get(tabId); 105 | await tabCreated(tab); 106 | core.captureThumbnail(tab.id); 107 | } 108 | } 109 | 110 | export function tabDetached(tabId, detachInfo) { 111 | if (core.viewWindowId == detachInfo.oldWindowId) { 112 | tabRemoved(tabId, {windowId: detachInfo.oldWindowId}); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/background/addon.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | import * as tabGroups from './addon.tabGroups.js'; 5 | import * as tabs from './addon.tabs.js'; 6 | import * as tabEvents from './addon.tabs.events.js' 7 | 8 | import * as backup from './backup.js' 9 | 10 | export const addon = { 11 | tabGroups: tabGroups, 12 | tabs: tabs, 13 | 14 | initialize: async() => { 15 | await tabGroups.initialize(); 16 | await tabEvents.initialize(); 17 | 18 | browser.runtime.onMessage.addListener(handleActions); 19 | } 20 | }; 21 | 22 | function handleActions(message, sender, sendResponse) { 23 | 24 | if (!message.action) return; 25 | 26 | let response; 27 | 28 | switch (message.action) { 29 | // browser.tabGroups 30 | case 'browser.tabGroups.create': { 31 | response = addon.tabGroups.create(message.info, sender.tab.windowId); 32 | break; 33 | } 34 | case 'browser.tabGroups.get': { 35 | response = addon.tabGroups.get(message.groupId); 36 | break; 37 | } 38 | /*case 'browser.tabGroups.move': { 39 | response = addon.tabGroups.move(message.groupId, message.info); 40 | break; 41 | }*/ 42 | case 'browser.tabGroups.query': { 43 | response = addon.tabGroups.query(message.info, sender.tab.windowId); 44 | break; 45 | } 46 | case 'browser.tabGroups.remove': { 47 | response = addon.tabGroups.remove(message.groupId); 48 | break; 49 | } 50 | case 'browser.tabGroups.update': { 51 | response = addon.tabGroups.update(message.groupId, message.info); 52 | break; 53 | } 54 | 55 | case 'browser.tabs.create': { 56 | response = addon.tabs.create(message.info); 57 | break; 58 | } 59 | case 'browser.tabs.get': { 60 | response = addon.tabs.get(message.tabId); 61 | break; 62 | } 63 | case 'browser.tabs.move': { 64 | response = addon.tabs.move(message.tabIds, message.info); 65 | break; 66 | } 67 | case 'browser.tabs.query': { 68 | response = addon.tabs.query(message.info); 69 | break; 70 | } 71 | // ---- 72 | 73 | // browser.sessions 74 | case 'browser.sessions.setGroupValue': { 75 | response = addon.tabGroups.setGroupValue(message.groupId, message.key, message.value, sender.id); 76 | break; 77 | } 78 | case 'browser.sessions.getGroupValue': { 79 | response = addon.tabGroups.getGroupValue(message.groupId, message.key, sender.id); 80 | break; 81 | } 82 | case 'browser.sessions.removeGroupValue': { 83 | response = addon.tabGroups.removeGroupValue(message.groupId, message.key, sender.id); 84 | break; 85 | } 86 | // ---- 87 | 88 | // backup actions 89 | case 'addon.backup.create': { 90 | response = backup.create(sender.id); 91 | break; 92 | } 93 | case 'addon.backup.open': { 94 | response = backup.open(message.data); 95 | break; 96 | } 97 | case 'addon.backup.getInterval': { 98 | response = backup.getInterval(); 99 | break; 100 | } 101 | case 'addon.backup.setInterval': { 102 | response = backup.setInterval(message.interval); 103 | break; 104 | } 105 | case 'addon.backup.getBackups': { 106 | response = backup.getBackups(); 107 | break; 108 | } 109 | // ---- 110 | 111 | default: 112 | console.log(`Unknown action (${message.action})`); 113 | } 114 | return response; 115 | } 116 | -------------------------------------------------------------------------------- /src/common/theme.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | import {Color} from '/common/colors.js' 5 | 6 | 7 | const useDark = window.matchMedia("(prefers-color-scheme: dark)"); 8 | 9 | 10 | function isNull(array) { 11 | for (let i = 0; i < array.length; i++) { 12 | if (array[i] == null || array[i] == null) { 13 | console.log(i); 14 | return true; 15 | } 16 | } 17 | return false; 18 | } 19 | 20 | 21 | export async function set(themeType, theme) { 22 | 23 | if (themeType == 'custom') { 24 | if (!theme) theme = await browser.theme.getCurrent(); 25 | 26 | if (theme && theme.colors && !isNull([ 27 | theme.colors.frame, 28 | theme.colors.toolbar_field_text, 29 | theme.colors.toolbar, 30 | theme.colors.toolbar_text, 31 | theme.colors.toolbar_field, 32 | theme.colors.tab_line 33 | ])) { 34 | setAll(theme); 35 | } else { 36 | themeType = undefined; 37 | } 38 | } 39 | 40 | if (themeType) { 41 | update(themeType); 42 | useDark.removeEventListener('change', darkToggle); 43 | } else { 44 | darkToggle(useDark); 45 | useDark.addEventListener('change', darkToggle); 46 | } 47 | } 48 | 49 | function darkToggle(dark) { 50 | if (dark.matches) { 51 | update('dark'); 52 | } else { 53 | update('light'); 54 | } 55 | } 56 | 57 | function update(themeType) { 58 | document.body.className = themeType; 59 | } 60 | 61 | function priority(array) { 62 | for (let i = 0; i < array.length; i++) { 63 | if (array[i] != undefined) { 64 | return array[i]; 65 | } 66 | } 67 | } 68 | 69 | async function setAll(theme) { 70 | 71 | if (!theme) theme = await browser.theme.getCurrent(); 72 | 73 | if (theme && theme.colors) { 74 | 75 | let background = new Color(theme.colors.frame); 76 | 77 | let shadow = new Color(theme.colors.toolbar_field_text); 78 | shadow.a = 0.15; 79 | 80 | let group = background.mix(new Color(theme.colors.toolbar)); 81 | 82 | let group_text = new Color(theme.colors.toolbar_text); 83 | 84 | let group_border = shadow; 85 | if (background.toGrayscale().r < 0.4) { 86 | group_border = group.mix(new Color(1, 1, 1, 0.1)); 87 | } 88 | 89 | let tab = new Color(theme.colors.toolbar_field); 90 | 91 | let tab_text = new Color(theme.colors.toolbar_field_text); 92 | 93 | let tab_overlay = new Color(theme.colors.toolbar_field); 94 | tab_overlay.a = 0.8; 95 | 96 | let tab_hover = new Color(theme.colors.toolbar_field_text); 97 | tab_hover.a = 0.3; 98 | 99 | let tab_active = new Color(theme.colors.tab_line); 100 | if ((tab_active.distance(tab) < 0.3 && 101 | tab_active.distance(group) < 0.3) || 102 | tab_active.a < 0.1) { 103 | tab_active = new Color(priority([theme.colors.toolbar_text, theme.colors.toolbar_field_text])); 104 | } 105 | 106 | const style = `.custom { 107 | --color-background: ${background.toCSS()}; 108 | --color-shadow: ${shadow.toCSS()}; 109 | 110 | --color-group: ${group.toCSS()}; 111 | --color-group-text: ${group_text.toCSS()}; 112 | --color-group-border: ${group_border.toCSS()}; 113 | 114 | --color-tab: ${tab.toCSS()}; 115 | --color-tab-text: ${tab_text.toCSS()}; 116 | --color-tab-overlay: ${tab_overlay.toCSS()}; 117 | --color-tab-hover: ${tab_hover.toCSS()}; 118 | --color-tab-active: ${tab_active.toCSS()}; 119 | }`; 120 | 121 | let stylesheet = new CSSStyleSheet(); 122 | stylesheet.insertRule(style); 123 | document.adoptedStyleSheets = [stylesheet]; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/panorama/css/group.css: -------------------------------------------------------------------------------- 1 | 2 | .group { 3 | min-width: 100px; 4 | min-height: 80px; 5 | 6 | position: absolute; 7 | } 8 | 9 | .group::before { 10 | content: ''; 11 | 12 | position: absolute; 13 | top: 4px; 14 | left: 4px; 15 | right: 4px; 16 | bottom: 4px; 17 | 18 | border-radius: 8px; 19 | 20 | background-color: var(--color-group); 21 | 22 | outline: 1px solid var(--color-group-border); 23 | } 24 | 25 | .group .header { 26 | height: 18px; 27 | 28 | margin: 10px 10px 10px 7px; 29 | 30 | position: relative; 31 | 32 | cursor: move; 33 | } 34 | 35 | .group .header input, .group .header .name { 36 | -moz-appearance: none; 37 | 38 | max-width: calc(100% - 60px); 39 | height: 16px; 40 | 41 | margin: 0; 42 | padding: 0; 43 | 44 | position: absolute; 45 | top: 0px; 46 | left: 6px; 47 | 48 | border: none; 49 | 50 | background: none; 51 | 52 | color: var(--color-group-text); 53 | font-size: 12px; 54 | line-height: 15px; 55 | font-family: inherit; 56 | text-overflow: ".."; 57 | } 58 | 59 | .group .header input { 60 | opacity: 0.5; 61 | } 62 | 63 | .group .header input:focus { 64 | outline: none; 65 | opacity: 1.0; 66 | } 67 | 68 | .group .header .name { 69 | padding-right: 6px; 70 | opacity: 0; 71 | } 72 | 73 | .group .header .tab_count { 74 | height: 16px; 75 | 76 | position: absolute; 77 | top: 0; 78 | right: 26px; 79 | 80 | color: var(--color-group-text); 81 | font-size: 11.5px; 82 | line-height: 15px; 83 | 84 | opacity: 0.5; 85 | } 86 | 87 | .group .header .close { 88 | width: 18px; 89 | height: 18px; 90 | 91 | position: absolute; 92 | top: 0; 93 | right: 0; 94 | 95 | cursor: default; 96 | 97 | background-color: var(--color-group-text); 98 | 99 | mask: url(../gfx/close.svg) no-repeat center / 10px; 100 | 101 | opacity: 0.5; 102 | } 103 | 104 | .group .header .close:hover { 105 | opacity: 1; 106 | } 107 | 108 | .group .tabs { 109 | position: absolute; 110 | top: 29px; 111 | right: 8px; 112 | left: 8px; 113 | bottom: 8px; 114 | 115 | overflow-y: hidden; 116 | scrollbar-width: thin; 117 | 118 | display: flex; 119 | flex-wrap: wrap; 120 | align-content: flex-start; 121 | } 122 | 123 | .group .tabs.scroll { 124 | overflow-y: scroll; 125 | } 126 | 127 | /* resize */ 128 | .group .resize > * { 129 | position: absolute; 130 | } 131 | 132 | .group .resize .top { 133 | height: 8px; 134 | 135 | top: 0; 136 | left: 20px; 137 | right: 20px; 138 | 139 | cursor: ns-resize; 140 | } 141 | 142 | .group .resize .right { 143 | width: 8px; 144 | 145 | top: 20px; 146 | bottom: 20px; 147 | right: 0; 148 | 149 | cursor: ew-resize; 150 | } 151 | 152 | .group .resize .bottom { 153 | height: 8px; 154 | 155 | bottom: 0; 156 | left: 20px; 157 | right: 20px; 158 | 159 | cursor: ns-resize; 160 | } 161 | 162 | .group .resize .left { 163 | width: 8px; 164 | 165 | top: 20px; 166 | bottom: 20px; 167 | left: 0; 168 | 169 | cursor: ew-resize; 170 | } 171 | 172 | .group .resize .top_right { 173 | width: 22px; 174 | height: 22px; 175 | 176 | top: 0; 177 | right: 0; 178 | 179 | cursor: nesw-resize; 180 | } 181 | 182 | .group .resize .bottom_right { 183 | width: 22px; 184 | height: 22px; 185 | 186 | bottom: 0; 187 | right: 0; 188 | 189 | cursor: nwse-resize; 190 | } 191 | 192 | .group .resize .bottom_left { 193 | width: 22px; 194 | height: 22px; 195 | 196 | bottom: 0; 197 | left: 0; 198 | 199 | cursor: nesw-resize; 200 | } 201 | 202 | .group .resize .top_left { 203 | width: 22px; 204 | height: 22px; 205 | 206 | top: 0; 207 | left: 0; 208 | 209 | cursor: nwse-resize; 210 | } 211 | -------------------------------------------------------------------------------- /src/background/addon.tabs.events.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | import {addon} from './addon.js'; 5 | import * as core from './core.js'; 6 | 7 | import * as backup from './backup.js'; 8 | 9 | 10 | export function initialize() { 11 | browser.tabs.onCreated.addListener(created); 12 | browser.tabs.onUpdated.addListener(updated); 13 | 14 | browser.tabs.onAttached.addListener(attached); 15 | 16 | browser.tabs.onActivated.addListener(activated); 17 | } 18 | 19 | 20 | 21 | 22 | async function created(tab) { 23 | 24 | tab.groupId = undefined; 25 | 26 | if (core.openingPanoramaView) { 27 | core.setOpeningPanoramaView(false); 28 | tab.groupId = -1; 29 | 30 | } else if (tab.pinned) { 31 | tab.groupId = -1; 32 | 33 | } else { 34 | if (tab.groupId == undefined) { 35 | tab.groupId = await addon.tabs.getGroupIdTimeout(tab.id, 100); // random timeout 36 | } 37 | // check if group exists 38 | const groups = await addon.tabGroups.query({windowId: tab.windowId}); 39 | const groupsExists = groups.find(group => group.id == tab.groupId); 40 | 41 | if (!groupsExists) { 42 | tab.groupId = undefined; 43 | while (tab.groupId == undefined) { 44 | tab.groupId = await addon.tabGroups.getActiveId(tab.windowId); 45 | } 46 | } 47 | } 48 | 49 | await addon.tabs.setGroupId(tab.id, tab.groupId); 50 | 51 | const sending = browser.runtime.sendMessage({event: 'browser.tabs.onCreated', tab: tab}); 52 | sending.catch(error => {}); 53 | } 54 | 55 | 56 | async function attached(tabId, attachInfo) { 57 | 58 | const panoramaViewTab = await core.getPanoramaViewTab(); 59 | 60 | if (panoramaViewTab && panoramaViewTab.active) { 61 | browser.tabs.hide(tabId); 62 | } 63 | 64 | const tabGroupId = await addon.tabs.getGroupId(tabId); 65 | 66 | if (tabGroupId == undefined) { 67 | 68 | let activeGroup = undefined; 69 | 70 | while (activeGroup == undefined) { 71 | activeGroup = await addon.tabGroups.getActiveId(attachInfo.newWindowId); 72 | } 73 | await addon.tabs.setGroupId(tabId, activeGroup); 74 | } 75 | } 76 | 77 | 78 | async function updated(tabId, changeInfo, tab) { 79 | 80 | tab.groupId = undefined; 81 | 82 | if (changeInfo.hasOwnProperty('pinned')) { 83 | if (changeInfo.pinned == true) { 84 | tab.groupId = -1; 85 | addon.tabs.setGroupId(tabId, tab.groupId); 86 | } else { 87 | const activeGroupId = await addon.tabGroups.getActiveId(tab.windowId); 88 | addon.tabs.setGroupId(tabId, activeGroupId); 89 | 90 | const panoramaViewTab = await core.getPanoramaViewTab(); 91 | 92 | if (panoramaViewTab && panoramaViewTab.active) { 93 | browser.tabs.hide(tabId); 94 | } 95 | } 96 | } 97 | 98 | if (tab.groupId == undefined) { 99 | const start = (new Date).getTime(); 100 | while (tab.groupId == undefined) { 101 | tab.groupId = await addon.tabs.getGroupId(tab.id); 102 | if (((new Date).getTime() - start) > 50) break; // timeout 103 | } 104 | 105 | } 106 | 107 | const sending = browser.runtime.sendMessage({event: 'browser.tabs.onUpdated', tabId: tabId, changeInfo: changeInfo, tab: tab}); 108 | sending.catch(error => {}); 109 | } 110 | 111 | 112 | async function activated(activeInfo) { 113 | 114 | const tab = await browser.tabs.get(activeInfo.tabId); 115 | 116 | if (!tab.pinned) { 117 | 118 | // Set the window's active group to the new active tab's group 119 | let tabGroupId = await addon.tabs.getGroupIdTimeout(activeInfo.tabId, 100); // random timeout 120 | 121 | if (tabGroupId != -1) { 122 | // check if group exists 123 | const groups = await addon.tabGroups.query({windowId: activeInfo.windowId}); 124 | const groupsExists = groups.find(group => group.id == tabGroupId); 125 | 126 | if (!groupsExists) { 127 | tabGroupId = undefined; 128 | while (tabGroupId == undefined) { 129 | tabGroupId = await addon.tabGroups.getActiveId(activeInfo.windowId); 130 | } 131 | } 132 | // ---- 133 | 134 | addon.tabGroups.setActiveId(tab.windowId, tabGroupId); 135 | } 136 | core.toggleVisibleTabs(tab.windowId, tabGroupId); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Tab Grouping API.md: -------------------------------------------------------------------------------- 1 | # Tab Grouping API 2 | 3 | ## browser.tabGroups 4 | 5 | ### Types 6 | + tabGroup 7 | - collapsed `boolean` Whether the group is collapsed. A collapsed group is one whose tabs are hidden. 8 | - color `color` 9 | - cookieStoreId `string` 10 | - id `integer` 11 | - title `string` 12 | - windowId `integer` 13 | 14 | 15 | ### Methods 16 | + **create** 17 | ```javascript 18 | var creating = browser.tabGroups.create( 19 | { 20 | collapsed, 21 | color, 22 | cookieStoreId, 23 | populate, // boolean, whether to add a new tab to the newly created group 24 | title, 25 | windowId 26 | } 27 | ) 28 | ``` 29 | + **get** 30 | Retrieves details about the specified group. 31 | ```javascript 32 | var getting = browser.tabGroups.get( 33 | groupId, // integer 34 | ) 35 | ``` 36 | + **move** 37 | Same as: 38 | - query for tabs with groupId 39 | - move them to some index and window 40 | - update group's windowId 41 | - issue tabGroups.onMoved event 42 | 43 | + **query** 44 | ```javascript 45 | var querying = browser.tabGroups.query( 46 | { 47 | collapsed, 48 | color, 49 | cookieStoreId, 50 | groupId, 51 | title, 52 | windowId 53 | } 54 | ) 55 | ``` 56 | + **remove** 57 | Remove the specified group. 58 | ```javascript 59 | var removing = browser.tabGroups.remove( 60 | groupId, // integer 61 | ) 62 | ``` 63 | + **update** 64 | ```javascript 65 | var updating = browser.tabGroups.update( 66 | groupId, // integer 67 | { 68 | collapsed, 69 | color, 70 | containerId, 71 | title 72 | } 73 | ) 74 | ``` 75 | 76 | ### Events 77 | + **onCreated** 78 | ```javascript 79 | browser.tabGroups.onCreated.addListener(listener) 80 | browser.tabGroups.onCreated.removeListener(listener) 81 | browser.tabGroups.onCreated.hasListener(listener) 82 | ``` 83 | 84 | + **onMoved** 85 | Fired when a group is moved within a window. 86 | Move events are still fired for the individual tabs within the group, as well as for the group itself. 87 | This event is not fired when a group is moved between windows; instead, it will be removed from one window and created in another. 88 | ```javascript 89 | browser.tabGroups.onMoved.addListener(listener) 90 | browser.tabGroups.onMoved.removeListener(listener) 91 | browser.tabGroups.onMoved.hasListener(listener) 92 | ``` 93 | + **onRemoved** 94 | ```javascript 95 | browser.tabGroups.onRemoved.addListener(listener) 96 | browser.tabGroups.onRemoved.removeListener(listener) 97 | browser.tabGroups.onRemoved.hasListener(listener) 98 | ``` 99 | + **onUpdated** 100 | ```javascript 101 | browser.tabGroups.onUpdated.addListener(listener) 102 | browser.tabGroups.onUpdated.removeListener(listener) 103 | browser.tabGroups.onUpdated.hasListener(listener) 104 | ``` 105 | 106 | 107 | ## browser.tabs 108 | 109 | ### Types 110 | - **tab** 111 | - groupId `integer` 112 | 113 | ### Methods 114 | - **create** 115 | add groupId option 116 | 117 | - **get** 118 | insert groupId 119 | 120 | - **getCurrent** 121 | insert groupId 122 | 123 | - **move** 124 | add groupId option 125 | 126 | - **query** 127 | add option for groupId 128 | insert groupId 129 | 130 | ### Events 131 | - **onCreated** 132 | insert groupId 133 | 134 | - **onUpdated** 135 | insert groupId into tab 136 | insert groupId into extraParameters.properties on addListener 137 | 138 | 139 | ## browser.sessions 140 | 141 | ### Methods 142 | + **setGroupValue** 143 | Store a key/value pair associated with a given group. 144 | ```javascript 145 | var setting = browser.sessions.setGroupValue( 146 | groupId, // integer 147 | key, // string 148 | value // string or object 149 | ) 150 | ``` 151 | + **getGroupValue** 152 | Retrieve a previously stored value for a given group, given its key. 153 | ```javascript 154 | var getting = browser.sessions.getGroupValue( 155 | groupId, // integer 156 | key // string 157 | ) 158 | ``` 159 | + **removeGroupValue** 160 | Remove a key/value pair from a given group. 161 | ```javascript 162 | var removing = browser.sessions.removeGroupValue( 163 | groupId, // integer 164 | key // string 165 | ) 166 | ``` 167 | -------------------------------------------------------------------------------- /src/background/background.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | import {addon} from './addon.js' 5 | import * as core from './core.js' 6 | 7 | import {handleCommands} from './commands.js' 8 | 9 | import * as backup from './backup.js' 10 | 11 | import {migrate} from './migrate.js' 12 | 13 | 14 | 15 | // make sure each window has a group 16 | async function setupWindows() { 17 | 18 | const windows = browser.windows.getAll({}); 19 | 20 | for (const window of await windows) { 21 | const groups = await addon.tabGroups.query({windowId: window.id}); 22 | if (groups.length == 0) { 23 | await addon.tabGroups.create({windowId: window.id}); 24 | } 25 | } 26 | } 27 | 28 | // put any tabs that do not have a group into the active group 29 | async function salvageGrouplessTabs() { 30 | 31 | const windows = await browser.windows.getAll({populate: true}); 32 | 33 | for (const window of windows) { 34 | const groups = await addon.tabGroups.query({windowId: window.id}); 35 | 36 | for (const tab of window.tabs) { 37 | 38 | if (tab.pinned) { 39 | addon.tabs.setGroupId(tab.id, -1); 40 | } else { 41 | const tabGroupId = await addon.tabs.getGroupId(tab.id); 42 | 43 | if (tabGroupId != -1) { 44 | const tabGroupExists = groups.find((tabGroup) => { return tabGroup.id == tabGroupId; }); 45 | if (!tabGroupExists) { 46 | const activeGroup = await addon.tabGroups.getActiveId(tab.windowId); 47 | addon.tabs.setGroupId(tab.id, activeGroup); 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | 56 | async function init() { 57 | 58 | // tab groups 59 | await migrate(); // keep until everyone's on 0.9.4 60 | 61 | await addon.initialize(); 62 | 63 | browser.commands.onCommand.addListener(handleCommands); 64 | 65 | await setupWindows(); 66 | await salvageGrouplessTabs(); 67 | 68 | // auto bakup 69 | backup.start(); 70 | // ---- 71 | 72 | 73 | // panorama view 74 | 75 | // new group menu entry 76 | browser.menus.create({ 77 | id: 'newTabGroup', 78 | title: browser.i18n.getMessage('newTabGroup'), 79 | contexts: ['browser_action'] 80 | }); 81 | 82 | browser.menus.onClicked.addListener(async(info, tab) => { 83 | if (info.menuItemId == 'newTabGroup') { 84 | const group = await addon.tabGroups.create({populate: true}, (await browser.windows.getCurrent()).id); 85 | } 86 | }); 87 | // ---- 88 | 89 | // move tab to group menu 90 | /*browser.menus.create({ 91 | id: 'moveTabToGroup', 92 | title: browser.i18n.getMessage('moveTabToGroup'), 93 | contexts: ['tab'] 94 | }); 95 | 96 | let groupList = []; 97 | 98 | browser.menus.onShown.addListener(async(info, clickedTab) => { 99 | if (info.menuIds.includes('moveTabToGroup')) { 100 | let groups = await addon.tabGroups.query(); 101 | 102 | groups = groups.sort((a, b) => a.title.localeCompare(b.title)); 103 | 104 | groups.forEach((group, i) => { 105 | browser.menus.create({ 106 | parentId: 'moveTabToGroup', 107 | id: `group ${i}`, 108 | title: group.title, 109 | onclick: async() => { 110 | const tabs = await addon.tabs.query({highlighted: true}); 111 | let tabIds = []; 112 | tabs.forEach((tab) => { 113 | tabIds.push(tab.id); 114 | }); 115 | if (tabIds.length == 1 && tabIds[0] != clickedTab.id) tabIds[0] = clickedTab.id; 116 | if (!tabIds.includes(clickedTab.id)) tabIds.push(clickedTab.id); 117 | await addon.tabs.move(tabIds, {index: -1, windowId: group.windowId, groupId: group.id}); 118 | browser.tabs.hide(tabIds); 119 | } 120 | }); 121 | groupList.push(`group ${i}`); 122 | }); 123 | 124 | browser.menus.refresh() 125 | } 126 | }); 127 | 128 | browser.menus.onHidden.addListener(() => { 129 | groupList.forEach((groupId, i) => { 130 | browser.menus.remove(groupId); 131 | }); 132 | groupList = []; 133 | });*/ 134 | // ---- 135 | 136 | // remove any panorama views there might be, we need a fresh connection to handle messages 137 | const extensionTabs = await browser.tabs.query({url: browser.runtime.getURL('panorama/view.html')}); 138 | if (extensionTabs) { 139 | for (const tab of extensionTabs) { 140 | browser.tabs.remove(tab.id); 141 | } 142 | } 143 | // ---- 144 | 145 | browser.browserAction.onClicked.addListener(core.toggleView); 146 | } 147 | 148 | init(); 149 | -------------------------------------------------------------------------------- /src/options/options.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | import * as html from '/common/html.js' 5 | import * as plurals from '/common/plurals.js' 6 | import * as theme from '/common/theme.js' 7 | 8 | import * as backup from './options.backup.js' 9 | 10 | 11 | // commands 12 | async function getCommands() { 13 | 14 | let commands = await browser.commands.getAll(); 15 | let fragment = document.createDocumentFragment(); 16 | 17 | for (let command of commands) { 18 | 19 | let platformInfo = await browser.runtime.getPlatformInfo(); 20 | let shortcut = command.shortcut || 'Not set'; 21 | 22 | if (platformInfo.os == 'mac') { 23 | shortcut = shortcut.replace('Ctrl', 'Command'); 24 | shortcut = shortcut.replace('MacCtrl', 'Ctrl'); 25 | shortcut = shortcut.replace('Alt', 'Option'); 26 | } 27 | 28 | shortcut = shortcut.replaceAll('+', ' + '); 29 | 30 | let span = html.newElement('span', {content: shortcut}); 31 | let label = html.newElement('label', {content: command.description}); 32 | 33 | let commandNode = html.newElement('div', {}, [label, span]); 34 | fragment.appendChild(commandNode); 35 | } 36 | 37 | document.getElementById('keyboardShortcuts').appendChild(fragment); 38 | } 39 | // ---- 40 | 41 | 42 | // statistics 43 | function formatByteSize(bytes) { 44 | if (bytes < 1024) return (bytes) + ' bytes'; 45 | else if (bytes < 1048576) return (bytes / 1024).toFixed(2) + ' KiB'; 46 | else if (bytes < 1073741824) return (bytes / 1048576).toFixed(2) + ' MiB'; 47 | else return (bytes / 1073741824).toFixed(2) + ' GiB'; 48 | }; 49 | 50 | async function getStatistics() { 51 | 52 | const tabs = await browser.tabs.query({}); 53 | 54 | let totalSize = 0; 55 | let numActiveTabs = 0; 56 | 57 | for (const tab of tabs) { 58 | const thumbnail = await browser.sessions.getTabValue(tab.id, 'thumbnail'); 59 | 60 | if (thumbnail) totalSize += thumbnail.length; 61 | if (!tab.discarded) numActiveTabs++; 62 | } 63 | 64 | document.getElementById('numberOfTabs').textContent = plurals.parse(browser.i18n.getMessage('optionNumberOfTabsValue', [tabs.length, numActiveTabs])); 65 | document.getElementById('thumbnailCacheSize').textContent = formatByteSize(totalSize); 66 | } 67 | // ---- 68 | 69 | 70 | document.addEventListener('DOMContentLoaded', async() => { 71 | 72 | document.querySelectorAll("[data-i18n-message-name]").forEach(element => { 73 | element.textContent = browser.i18n.getMessage(element.dataset.i18nMessageName); 74 | }); 75 | 76 | getCommands(); 77 | getStatistics(); 78 | 79 | const storage = await browser.storage.local.get(); 80 | 81 | // theme override select 82 | const themeSelect = document.getElementById('themeSelect'); 83 | 84 | if (storage.hasOwnProperty('themeOverride')) { 85 | themeSelect.value = storage.themeOverride; 86 | } 87 | 88 | theme.set(storage.themeOverride); 89 | 90 | browser.theme.onUpdated.addListener(({newTheme, windowId}) => { 91 | theme.set(storage.themeOverride, newTheme); 92 | }); 93 | 94 | themeSelect.addEventListener('input', async(e) => { 95 | 96 | storage.themeOverride = false; 97 | 98 | switch(e.target.value) { 99 | case 'custom': { 100 | storage.themeOverride = 'custom'; 101 | await browser.storage.local.set({themeOverride: storage.themeOverride}); 102 | break; 103 | } 104 | case 'light': { 105 | storage.themeOverride = 'light'; 106 | await browser.storage.local.set({themeOverride: storage.themeOverride}); 107 | break; 108 | } 109 | case 'dark': { 110 | storage.themeOverride = 'dark'; 111 | await browser.storage.local.set({themeOverride: storage.themeOverride}); 112 | break; 113 | } 114 | default: { 115 | await browser.storage.local.remove('themeOverride'); 116 | break; 117 | } 118 | } 119 | browser.runtime.sendMessage({event: 'addon.options.onUpdated', data: {themeOverride: storage.themeOverride}}); 120 | theme.set(storage.themeOverride); 121 | }); 122 | // ---- 123 | 124 | // list view 125 | const listView = document.getElementById('listView'); 126 | 127 | if (storage.hasOwnProperty('listView')) { 128 | listView.checked = storage.listView; 129 | } 130 | 131 | listView.addEventListener('change', async(e) => { 132 | await browser.storage.local.set({listView: e.target.checked}); 133 | browser.runtime.sendMessage({event: 'addon.options.onUpdated', data: {listView: e.target.checked}}); 134 | }); 135 | // ---- 136 | 137 | backup.createUI(); 138 | 139 | browser.tabs.onUpdated.addListener(getStatistics); 140 | }); 141 | -------------------------------------------------------------------------------- /src/background/backup.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | import {addon} from './addon.js'; 5 | 6 | 7 | export async function create(appId) { 8 | 9 | let makeTabData = (tab) => { 10 | return { 11 | url: tab.url, 12 | title: tab.title, 13 | cookieStoreId: tab.cookieStoreId 14 | }; 15 | }; 16 | 17 | let backup = { 18 | file: { 19 | type: 'panoramaView', 20 | version: 2 21 | }, 22 | windows: [] 23 | }; 24 | 25 | const windows = await browser.windows.getAll({populate: true}); 26 | 27 | for (const window of windows) { 28 | 29 | const groups = await browser.sessions.getWindowValue(window.id, 'groups'); 30 | groups.sort((a, b) => { 31 | return a.lastAccessed - b.lastAccessed; 32 | }); 33 | 34 | let pinnedTabs = []; 35 | let tabGroups = []; 36 | 37 | for (let tab of window.tabs) { 38 | tab.groupId = await browser.sessions.getTabValue(tab.id, 'groupId'); 39 | 40 | if (tab.pinned) { 41 | pinnedTabs.push(makeTabData(tab)); 42 | } 43 | } 44 | 45 | for (const group of groups) { 46 | 47 | let rect = await addon.tabGroups.getGroupValue(group.id, 'rect', browser.runtime.id); 48 | 49 | let tabGroup = { 50 | title: group.title, 51 | rect: {x: rect.x, y: rect.y, w: rect.w, h: rect.h}, 52 | tabs: [] 53 | } 54 | 55 | for (const tab of window.tabs) { 56 | if (tab.groupId == group.id) { 57 | tabGroup.tabs.push(makeTabData(tab)); 58 | } 59 | } 60 | 61 | tabGroups.push(tabGroup); 62 | } 63 | 64 | backup.windows.push({ 65 | pinnedTabs: pinnedTabs, 66 | tabGroups: tabGroups 67 | }); 68 | } 69 | 70 | return backup; 71 | } 72 | 73 | 74 | export let opening = false; 75 | 76 | export async function open(backup) { 77 | 78 | const createTab = async(_tab, windowId, tabGroupId) => { 79 | 80 | let tabFailed = false; 81 | 82 | const tab = await browser.tabs.create({ 83 | url: _tab.url, 84 | title: _tab.title, 85 | active: false, 86 | discarded: true, 87 | windowId: windowId, 88 | cookieStoreId: _tab.cookieStoreId 89 | }).catch((err) => { 90 | console.log(err); 91 | tabFailed = true; 92 | }); 93 | 94 | if (tabFailed) return; 95 | 96 | await addon.tabs.setGroupId(tab.id, tabGroupId); 97 | 98 | if (tabGroupId == -1) { 99 | await browser.tabs.update({pinned: true}); // it gets unpinned somehow 100 | } 101 | }; 102 | 103 | if (backup.file.type != 'panoramaView') return 'Unknown backup type'; 104 | if (backup.file.version != 2) return 'Unknown backup version'; 105 | 106 | opening = true; 107 | 108 | for (const _window of backup.windows) { 109 | 110 | const window = await browser.windows.create({}); 111 | const firstTabId = window.tabs[0].id; 112 | 113 | for (const tab of _window.pinnedTabs) { 114 | await createTab(tab, window.id, -1); 115 | } 116 | 117 | let z = 0; 118 | for (const _tabGroup of _window.tabGroups) { 119 | const tabGroup = await addon.tabGroups.create({ 120 | windowId: window.id, 121 | title: _tabGroup.title, 122 | lastAccessed: z 123 | }); 124 | await addon.tabGroups.setGroupValue(tabGroup.id, 'rect', _tabGroup.rect, browser.runtime.id); 125 | 126 | z++; 127 | 128 | for (const tab of _tabGroup.tabs) { 129 | await createTab(tab, window.id, tabGroup.id); 130 | } 131 | } 132 | 133 | browser.tabs.remove(firstTabId); 134 | } 135 | 136 | opening = false; 137 | } 138 | 139 | 140 | // auto backup 141 | let autoBackupInterval; 142 | 143 | export async function start() { 144 | const interval = await getInterval(); 145 | if (interval > 0) { 146 | autoBackupInterval = window.setInterval(autoBackup, interval * 60000); 147 | } 148 | } 149 | 150 | export function stop() { 151 | window.clearInterval(autoBackupInterval); 152 | } 153 | 154 | export async function setInterval(interval) { 155 | stop(); 156 | await browser.storage.local.set({autoBackupInterval: interval}); 157 | start(); 158 | } 159 | 160 | export async function getInterval() { 161 | return (await browser.storage.local.get('autoBackupInterval')).autoBackupInterval || 0; 162 | } 163 | 164 | export async function getBackups() { 165 | return (await browser.storage.local.get('autoBackup')).autoBackup || []; 166 | } 167 | 168 | async function autoBackup() { 169 | 170 | let storage = await browser.storage.local.get('autoBackup'); 171 | 172 | if (!storage.autoBackup) { 173 | storage.autoBackup = []; 174 | } 175 | 176 | let backup = { 177 | time: (new Date).getTime(), 178 | data: await create() 179 | } 180 | 181 | storage.autoBackup.unshift(backup); 182 | 183 | if (storage.autoBackup.length > 3) { 184 | storage.autoBackup.pop(); 185 | } 186 | 187 | browser.storage.local.set(storage); 188 | } 189 | 190 | -------------------------------------------------------------------------------- /src/panorama/js/html.tabs.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | import {newElement} from '/common/html.js' 5 | import * as drag from './view.drag.js' 6 | import * as core from './view.js' 7 | 8 | 9 | export function create(tab) { 10 | 11 | const thumbnail = newElement('img', {class: 'thumbnail'}); 12 | const favicon = newElement('img', {class: 'favicon'}); 13 | const close = newElement('div', {class: 'close'}); 14 | const title = newElement('span'); 15 | const titleSpan = newElement('div', {class: 'title'}, [title]); 16 | const context = newElement('div', {class: 'context'}) 17 | 18 | const container = newElement('div', {class: 'container'}, [favicon, thumbnail, close, titleSpan]); 19 | 20 | const node = newElement('div', {href: '', class: 'tab', draggable: 'true', 'data-id': tab.id, title: '', tabindex: 0}, [container, context]); 21 | 22 | node.addEventListener('click', (event) => { 23 | event.preventDefault(); 24 | if (event.ctrlKey) { 25 | drag.selectTab(tab.id); 26 | 27 | } else { 28 | browser.tabs.update(tab.id, {active: true}); 29 | } 30 | }, false); 31 | 32 | node.addEventListener('keydown', (event) => { 33 | if (event.key == 'Enter') { 34 | if (event.ctrlKey) { 35 | drag.selectTab(tab.id); 36 | 37 | } else { 38 | browser.tabs.update(tab.id, {active: true}); 39 | } 40 | } 41 | }, false); 42 | 43 | // showDefaults not yet working for tabs :C 44 | /*node.addEventListener('contextmenu', () => { 45 | browser.menus.overrideContext({ 46 | context: 'tab', 47 | tabId: tab.id 48 | }); 49 | }, { capture: true });*/ 50 | 51 | node.addEventListener('auxclick', (event) => { 52 | event.preventDefault(); 53 | if (event.button == 1) { // middle mouse 54 | browser.tabs.remove(tab.id); 55 | } 56 | }, false); 57 | 58 | node.addEventListener('keyup', (event) => { 59 | event.preventDefault(); 60 | if (event.ctrlKey && event.key == 'Delete') { 61 | browser.tabs.remove(tab.id); 62 | } 63 | }, false); 64 | 65 | close.addEventListener('click', (event) => { 66 | event.preventDefault(); 67 | event.stopPropagation(); 68 | browser.tabs.remove(tab.id); 69 | }, false); 70 | 71 | node.addEventListener('dragstart', drag.tabDragStart, false); 72 | node.addEventListener('drop', drag.tabDrop, false); 73 | node.addEventListener('dragend', drag.tabDragEnd, false); 74 | 75 | return node; 76 | } 77 | 78 | export function get(tabId) { 79 | return document.querySelector(`.tab[data-id="${tabId}"]`); 80 | } 81 | 82 | export async function update(tabNode, tab) { 83 | if (tabNode) { 84 | tabNode.querySelector('.title span').textContent = tab.title; 85 | 86 | if (tab.cookieStoreId != 'firefox-default') { 87 | const contextInfo = await browser.contextualIdentities.get(tab.cookieStoreId); 88 | 89 | tabNode.querySelector('.context').style.backgroundColor = contextInfo.colorCode; 90 | tabNode.querySelector('.context').title = contextInfo.name; 91 | } 92 | 93 | tabNode.title = tab.title + ((tab.url.substr(0, 5) != 'data:') ? ' - ' + decodeURI(tab.url) : ''); 94 | 95 | if (tab.discarded) { 96 | tabNode.classList.add('inactive'); 97 | } else { 98 | tabNode.classList.remove('inactive'); 99 | } 100 | } 101 | } 102 | 103 | export async function updateFavicon(tabNode, tab) { 104 | if (tabNode) { 105 | if (tab.favIconUrl && 106 | tab.favIconUrl.substr(0, 22) != 'chrome://mozapps/skin/' && 107 | tab.favIconUrl != tab.url) { 108 | tabNode.querySelector('.favicon').src = tab.favIconUrl; 109 | } 110 | } 111 | } 112 | 113 | 114 | export async function updateThumbnail(tabNode, tabId, thumbnail) { 115 | if (tabNode) { 116 | if (!thumbnail) thumbnail = await browser.sessions.getTabValue(tabId, 'thumbnail'); 117 | if (thumbnail) tabNode.querySelector('.thumbnail').src = thumbnail; 118 | } 119 | } 120 | 121 | export async function setActive() { 122 | 123 | let tabs; 124 | try { 125 | tabs = await browser.tabs.query({currentWindow: true}); 126 | } catch (error) { 127 | return; 128 | } 129 | 130 | tabs.sort((tabA, tabB) => { 131 | return tabB.lastAccessed - tabA.lastAccessed; 132 | }); 133 | 134 | const activeTabId = (tabs[0].url.includes(browser.runtime.getURL('panorama/view.html'))) ? tabs[1].id : tabs[0].id ; 135 | 136 | const tabNode = get(activeTabId); 137 | const activeNode = document.querySelector('.tab.active'); 138 | 139 | if (activeNode) activeNode.classList.remove('active'); 140 | if (tabNode) { 141 | tabNode.classList.add('active'); 142 | tabNode.focus(); 143 | } 144 | } 145 | 146 | export async function insert(tabNode, tab) { 147 | 148 | const tabGroupNode = document.querySelector(`.group[data-id="${tab.groupId}"]`); 149 | 150 | const tabs = await browser.tabs.query({windowId: core.viewWindowId}); 151 | 152 | let lastTab = undefined; 153 | for (const _tab of tabs) { 154 | 155 | if (_tab.groupId != tab.groupId) continue; 156 | 157 | if (_tab.id == tab.id) { 158 | 159 | const tabGroupTabsNode = tabGroupNode.querySelector('.tabs'); 160 | 161 | if (tabGroupTabsNode.children.length == 1) { 162 | tabGroupTabsNode.insertAdjacentElement('afterbegin', tabNode); 163 | } else { 164 | if (!lastTab) { 165 | tabGroupTabsNode.insertAdjacentElement('afterbegin', tabNode); 166 | } else if (lastTab.groupId == tab.groupId) { 167 | get(lastTab.id).insertAdjacentElement('afterend', tabNode); 168 | } else { 169 | tabGroupTabsNode.insertAdjacentElement('afterbegin', tabNode); 170 | } 171 | } 172 | } 173 | lastTab = _tab; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/common/tabGroups-polyfill.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | if (!browser.tabGroups) { 5 | 6 | function listenerObject() { 7 | this.listeners = []; 8 | this.functions = { 9 | addListener: (listener) => { 10 | this.listeners.push(listener); 11 | }, 12 | hasListener: (listener) => { 13 | return this.listeners.find(listener) != undefined; 14 | }, 15 | removeListener: (listener) => { 16 | for (const i in this.listeners) { 17 | if (listener === this.listeners[i]) { 18 | this.listeners.splice(i, 1); 19 | break; 20 | } 21 | } 22 | } 23 | }; 24 | this.call = (...data) => { 25 | this.listeners.forEach(listener => { 26 | listener(...data); 27 | }); 28 | } 29 | } 30 | 31 | // browser.tabGroups 32 | const tabGroups_onCreated = new listenerObject(); 33 | //const tabGroups_onMoved = new listenerObject(); 34 | const tabGroups_onRemoved = new listenerObject(); 35 | const tabGroups_onUpdated = new listenerObject(); 36 | 37 | const panoramaViewId = '{60e27487-c779-464c-8698-ad481b718d5f}'; 38 | 39 | browser.tabGroups = { 40 | create: (createInfo) => { 41 | return browser.runtime.sendMessage(panoramaViewId, { 42 | action: 'browser.tabGroups.create', 43 | info: createInfo 44 | }); 45 | }, 46 | get: (groupId) => { 47 | return browser.runtime.sendMessage(panoramaViewId, { 48 | action: 'browser.tabGroups.get', 49 | groupId: groupId 50 | }); 51 | }, 52 | /*move: (queryInfo) => { 53 | return browser.runtime.sendMessage(panoramaViewId, { 54 | action: 'browser.tabGroups.move', 55 | groupId: groupId, 56 | info: moveInfo 57 | }); 58 | },*/ 59 | query: (queryInfo) => { 60 | return browser.runtime.sendMessage(panoramaViewId, { 61 | action: 'browser.tabGroups.query', 62 | info: queryInfo 63 | }); 64 | }, 65 | remove: (groupId) => { 66 | return browser.runtime.sendMessage(panoramaViewId, { 67 | action: 'browser.tabGroups.remove', 68 | groupId: groupId 69 | }); 70 | }, 71 | update: (groupId, updateInfo) => { 72 | return browser.runtime.sendMessage(panoramaViewId, { 73 | action: 'browser.tabGroups.update', 74 | groupId: groupId, 75 | info: updateInfo 76 | }); 77 | }, 78 | onCreated: tabGroups_onCreated.functions, 79 | //onMoved: tabGroups_onMoved.functions, 80 | onRemoved: tabGroups_onRemoved.functions, 81 | onUpdated: tabGroups_onUpdated.functions 82 | } 83 | // ---- 84 | 85 | // browser.tabs hijack 86 | const tabs_onCreated = new listenerObject(); 87 | const tabs_onUpdated = new listenerObject(); 88 | 89 | browser.tabs.create = (createInfo) => { 90 | return browser.runtime.sendMessage(panoramaViewId, { 91 | action: 'browser.tabs.create', 92 | info: createInfo 93 | }); 94 | } 95 | browser.tabs.get = (tabId) => { 96 | return browser.runtime.sendMessage(panoramaViewId, { 97 | action: 'browser.tabs.get', 98 | tabId: tabId 99 | }); 100 | } 101 | const browser_tabs_getCurrent = browser.tabs.getCurrent; 102 | browser.tabs.getCurrent = () => { 103 | return browser_tabs_getCurrent().then(tab => browser.tabs.get(tab.id)); 104 | } 105 | browser.tabs.move = (tabIds, moveInfo) => { 106 | return browser.runtime.sendMessage(panoramaViewId, { 107 | action: 'browser.tabs.move', 108 | tabIds: tabIds, 109 | info: moveInfo 110 | }); 111 | } 112 | browser.tabs.query = (queryInfo) => { 113 | return browser.runtime.sendMessage(panoramaViewId, { 114 | action: 'browser.tabs.query', 115 | info: queryInfo 116 | }); 117 | } 118 | 119 | browser.tabs.onCreated = tabs_onCreated.functions; 120 | browser.tabs.onUpdated = tabs_onUpdated.functions; 121 | // ---- 122 | 123 | // browser.sessions additions 124 | browser.sessions.setGroupValue = (groupId, key, value) => { 125 | return browser.runtime.sendMessage(panoramaViewId, { 126 | action: 'browser.sessions.setGroupValue', 127 | groupId: groupId, 128 | key: key, 129 | value: value 130 | }); 131 | } 132 | browser.sessions.getGroupValue = (groupId, key) => { 133 | return browser.runtime.sendMessage(panoramaViewId, { 134 | action: 'browser.sessions.getGroupValue', 135 | groupId: groupId, 136 | key: key 137 | }); 138 | } 139 | browser.sessions.removeGroupValue = (groupId, key) => { 140 | return browser.runtime.sendMessage(panoramaViewId, { 141 | action: 'browser.sessions.removeGroupValue', 142 | groupId: groupId, 143 | key: key 144 | }); 145 | } 146 | // ---- 147 | 148 | // event listeners 149 | browser.runtime.onMessage.addListener((message, sender, sendResponse) => { 150 | if (sender.id != panoramaViewId) return; 151 | switch (message.event) { 152 | case 'browser.tabGroups.onCreated': { 153 | tabGroups_onCreated.call(message.group); 154 | break; 155 | } 156 | /*case 'browser.tabGroups.onMoved': { 157 | tabGroups_onMoved.call(message.groupId, message.moveInfo); 158 | break; 159 | }*/ 160 | case 'browser.tabGroups.onRemoved': { 161 | tabGroups_onRemoved.call(message.groupId, message.removeInfo); 162 | break; 163 | } 164 | case 'browser.tabGroups.onUpdated': { 165 | tabGroups_onUpdated.call(message.group); 166 | break; 167 | } 168 | 169 | case 'browser.tabs.onCreated': { 170 | tabs_onCreated.call(message.tab); 171 | break; 172 | } 173 | case 'browser.tabs.onUpdated': { 174 | tabs_onUpdated.call(message.tabId, message.changeInfo, message.tab); 175 | break; 176 | } 177 | default: 178 | break; 179 | } 180 | }); 181 | // ---- 182 | } 183 | -------------------------------------------------------------------------------- /src/panorama/css/tab.css: -------------------------------------------------------------------------------- 1 | 2 | .newtab { 3 | margin: 4px; 4 | 5 | position: relative 6 | } 7 | 8 | .tab { 9 | padding: 4px; 10 | 11 | display: block; 12 | 13 | position: relative; 14 | 15 | cursor: default; 16 | } 17 | 18 | .tab .container { 19 | width: 100%; 20 | height: 100%; 21 | 22 | box-sizing: border-box; 23 | 24 | position: relative; 25 | 26 | background-color: var(--color-tab); 27 | 28 | border-radius: 6px; 29 | border: 2px solid var(--color-tab); 30 | 31 | outline: 1px solid var(--color-shadow); 32 | 33 | overflow: hidden; 34 | 35 | transition: opacity 20ms linear; 36 | } 37 | 38 | .tab:hover .container { 39 | outline: 3px solid var(--color-tab-hover); 40 | } 41 | 42 | .tab.active .container { 43 | outline: 2px solid var(--color-tab-active); 44 | } 45 | 46 | .tab.selected .container { 47 | outline: 1px solid var(--color-tab-active); 48 | outline-offset: 1px; 49 | } 50 | 51 | .tab:focus .container { 52 | outline: 2px solid var(--color-tab-active); 53 | } 54 | 55 | .tab:focus { 56 | outline: none; 57 | } 58 | 59 | .tab.inactive .container { 60 | opacity: 0.6; 61 | } 62 | 63 | .tab.drag .container { 64 | outline: none; 65 | opacity: 0.2; 66 | box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.4); 67 | } 68 | 69 | .tab .context { 70 | height: 2px; 71 | width: 40%; 72 | 73 | margin: 4px auto; 74 | 75 | position: absolute; 76 | top: 0; 77 | left: 0; 78 | right: 0; 79 | 80 | z-index: 10; 81 | 82 | border-radius: 0 0 2px 2px; 83 | } 84 | 85 | .tab .thumbnail { 86 | width: 100.5%; 87 | height: 100.5%; 88 | object-fit: cover; 89 | object-position: top center; 90 | } 91 | 92 | .tab .thumbnail:not([src]) { 93 | display: none; 94 | } 95 | 96 | .tab .favicon { 97 | width: 16px; 98 | height: 16px; 99 | object-fit: cover; 100 | 101 | margin: 1px; 102 | padding: 2px; 103 | 104 | position: absolute; 105 | 106 | border-radius: 3px; 107 | 108 | background-color: var(--color-tab-overlay); 109 | backdrop-filter: blur(2px) saturate(2); 110 | 111 | z-index: 1; 112 | } 113 | 114 | .tab .favicon:not([src]) { 115 | display: none; 116 | } 117 | 118 | .tab .close { 119 | width: 16px; 120 | height: 16px; 121 | 122 | margin: 3px; 123 | padding: 0; 124 | 125 | position: absolute; 126 | right: 0; 127 | top: 0; 128 | 129 | mask: url(../gfx/close.svg) no-repeat center / 10px; 130 | backdrop-filter: blur(2px) invert(1) grayscale(1); 131 | /* backdrop-filter: invert(1) grayscale(1); */ 132 | 133 | border-radius: 4px; 134 | 135 | z-index: 10; 136 | 137 | opacity: 0.6; 138 | } 139 | 140 | .tab .close:hover { 141 | opacity: 1; 142 | } 143 | 144 | .tab .title { 145 | margin: 1px; 146 | 147 | position: absolute; 148 | bottom: 0; 149 | left: 0; 150 | right: 0; 151 | 152 | font-size: 11px; 153 | white-space: nowrap; 154 | 155 | z-index: 10; 156 | } 157 | 158 | .tab .title > span { 159 | max-width: 100%; 160 | width: fit-content; 161 | 162 | margin: 0 auto; 163 | padding: 1px 6px; 164 | 165 | display: block; 166 | box-sizing: border-box; 167 | 168 | border-radius: 3px; 169 | 170 | background-color: var(--color-tab-overlay); 171 | 172 | outline: 1px solid var(--color-shadow); 173 | backdrop-filter: blur(2px) saturate(2); 174 | 175 | color: var(--color-tab-text); 176 | text-overflow: ".."; 177 | 178 | overflow: hidden; 179 | } 180 | 181 | .newtab .border { 182 | position: absolute; 183 | top: 0; 184 | bottom: 0; 185 | left: 0; 186 | right: 0; 187 | 188 | border: var(--color-group-text) 2px dashed; 189 | border-radius: 5px; 190 | 191 | opacity: 0.15; 192 | } 193 | 194 | .newtab .border:hover { 195 | opacity: 0.3; 196 | } 197 | 198 | .newtab .border::after { 199 | content: ''; 200 | 201 | width: 70%; 202 | height: 70%; 203 | 204 | max-width: 16px; 205 | max-height: 16px; 206 | 207 | position: absolute; 208 | top: 50%; 209 | left: 50%; 210 | 211 | transform: translate(-50%, -50%); 212 | 213 | background-color: var(--color-group-text); 214 | 215 | mask: no-repeat center / cover; 216 | mask-image: url(../gfx/add.svg); 217 | } 218 | 219 | .small .tab .favicon[src] + .thumbnail, .tiny .tab .favicon[src] + .thumbnail { 220 | display: none; 221 | } 222 | 223 | .small .tab .favicon, .tiny .tab .favicon { 224 | width: 100%; 225 | height: 100%; 226 | 227 | margin: 0; 228 | padding: 2px; 229 | box-sizing: border-box; 230 | 231 | border-radius: 5px; 232 | 233 | background-color: transparent; 234 | box-shadow: none; 235 | 236 | backdrop-filter: none; 237 | } 238 | 239 | .small .tab .title { 240 | margin: 0; 241 | } 242 | 243 | .small .tab .title > span { 244 | padding: 1px 5px 2px 5px; 245 | 246 | border-radius: 0; 247 | 248 | outline: none; 249 | 250 | font-size: 10px; 251 | text-overflow: ""; 252 | } 253 | 254 | .small .tab .close, .tiny .tab .close { 255 | display: none; 256 | } 257 | 258 | .tiny .tab .title { 259 | display: none; 260 | } 261 | 262 | 263 | .list .tab, .list .newtab { 264 | width: calc(100% - 8px) !important; 265 | height: 22px !important; 266 | 267 | position: relative; 268 | top: 0px !important; 269 | left: 0px !important; 270 | 271 | outline: none; 272 | 273 | _background-color: transparent; 274 | } 275 | 276 | .list .tab .favicon { 277 | margin: 1px; 278 | padding: 0; 279 | 280 | background-color: transparent; 281 | backdrop-filter: none; 282 | } 283 | 284 | .list .tab .close { 285 | margin: 1px; 286 | } 287 | 288 | .list .tab .thumbnail { 289 | display: none; 290 | } 291 | 292 | .list .tab .title { 293 | margin: 1px; 294 | left: 20px; 295 | right: 20px; 296 | } 297 | 298 | .list .tab .title > span { 299 | margin: 0; 300 | padding: 0; 301 | 302 | border-radius: 0; 303 | 304 | background-color: transparent; 305 | backdrop-filter: none; 306 | 307 | outline: none; 308 | } 309 | 310 | .list .newtab .border::after { 311 | max-width: 12px; 312 | max-height: 12px; 313 | } 314 | -------------------------------------------------------------------------------- /src/panorama/js/view.drag.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | import {html} from './html.js' 5 | 6 | 7 | let selectedTabs = []; 8 | 9 | 10 | 11 | async function moveTabs(tabIds, windowId, tabGroupId, index) { 12 | browser.tabs.move(tabIds, {index: index, windowId: windowId, groupId: tabGroupId}); 13 | } 14 | 15 | 16 | function getTabIds(e) { 17 | return JSON.parse(e.dataTransfer.getData('application/x-panorama-view-tab-list')); 18 | } 19 | 20 | 21 | // view events 22 | export function viewDragOver(e) { 23 | e.preventDefault(); // Necessary. Allows us to drop. 24 | e.dataTransfer.dropEffect = 'move'; // See the section on the DataTransfer object. 25 | return false; 26 | } 27 | 28 | export async function viewDrop(e) { 29 | e.preventDefault(); 30 | const tabIds = getTabIds(e); 31 | e.stopPropagation(); 32 | 33 | const currentWindowId = (await browser.windows.getCurrent()).id; 34 | const tabGroup = await browser.tabGroups.create({windowId: currentWindowId}); 35 | 36 | // move the tab node 37 | const groupNode = html.groups.get(tabGroup.id); 38 | 39 | for (const tabId of tabIds) { 40 | const tabNode = html.tabs.get(tabId); 41 | if (tabNode) { 42 | groupNode.querySelector('.newtab').insertAdjacentElement('beforebegin', tabNode); 43 | } 44 | } 45 | 46 | html.groups.fitTabs(); 47 | // ---- 48 | 49 | moveTabs(tabIds, currentWindowId, tabGroup.id, -1); 50 | 51 | clearTabDragCSS(e); 52 | 53 | return false; 54 | } 55 | // ---- 56 | 57 | // group events 58 | export function groupDragOver(e) { 59 | e.preventDefault(); // Necessary. Allows us to drop. 60 | e.dataTransfer.dropEffect = 'move'; // See the section on the DataTransfer object. 61 | return false; 62 | } 63 | 64 | export async function groupDrop(e) { 65 | e.preventDefault(); 66 | const tabIds = getTabIds(e); 67 | e.stopPropagation(); 68 | 69 | let groupNode = e.target.closest('.group'); 70 | 71 | // move the tab node 72 | for (const tabId of tabIds) { 73 | const tabNode = html.tabs.get(tabId); 74 | if (tabNode) { 75 | groupNode.querySelector('.newtab').insertAdjacentElement('beforebegin', tabNode); 76 | } 77 | } 78 | 79 | html.groups.fitTabs(); 80 | // ---- 81 | 82 | const tabGroupId = parseInt(groupNode.dataset.id); 83 | const currentWindowId = (await browser.windows.getCurrent()).id; 84 | 85 | moveTabs(tabIds, currentWindowId, tabGroupId, -1); 86 | 87 | clearTabDragCSS(e); 88 | 89 | return false; 90 | } 91 | // ---- 92 | 93 | // tab events 94 | export function selectTab(tabId) { 95 | 96 | const tabNode = html.tabs.get(tabId); 97 | 98 | const i = selectedTabs.indexOf(tabId); 99 | 100 | if (i >= 0) { 101 | selectedTabs.splice(i, 1); 102 | tabNode.classList.remove('selected'); 103 | } else { 104 | selectedTabs.push(tabId); 105 | tabNode.classList.add('selected'); 106 | } 107 | 108 | // sort by index 109 | const tabNodes = document.querySelectorAll('.tab'); 110 | let tabIds = []; 111 | 112 | for (const node of tabNodes) { 113 | const id = parseInt(node.dataset.id); 114 | if (selectedTabs.includes(id)) { 115 | tabIds.push(id); 116 | } 117 | } 118 | 119 | selectedTabs = tabIds; 120 | // ---- 121 | } 122 | 123 | export function clearTabSelection(e) { 124 | if (selectedTabs.length > 0 && !e.ctrlKey) { 125 | 126 | for (const tabId of selectedTabs) { 127 | const tabNode = html.tabs.get(tabId); 128 | if (tabNode) { 129 | tabNode.classList.remove('selected'); 130 | } 131 | } 132 | selectedTabs = []; 133 | } 134 | } 135 | 136 | export function tabDragStart(e) { 137 | 138 | e.dataTransfer.effectAllowed = 'move'; 139 | 140 | const tabId = parseInt(this.dataset.id); 141 | if (!selectedTabs.includes(tabId)) { 142 | clearTabSelection(e); 143 | } 144 | 145 | if (selectedTabs.length == 0) { 146 | selectTab(tabId); 147 | } 148 | 149 | for (const i in selectedTabs) { 150 | const tabNode = html.tabs.get(selectedTabs[i]); 151 | tabNode.classList.add('drag'); 152 | } 153 | 154 | e.dataTransfer.setData('application/x-panorama-view-tab-list', JSON.stringify(selectedTabs)); 155 | 156 | //e.dataTransfer.setData('text/x-moz-url', urlTitleList.join('\r\n')); 157 | //e.dataTransfer.setData('text/uri-list', uriList.join('\r\n')); 158 | //e.dataTransfer.setData('text/plain', uriList.join('\r\n')); 159 | //e.dataTransfer.setData('text/html', links.join('\r\n')); // title 160 | 161 | const rect = this.getBoundingClientRect(); 162 | 163 | e.dataTransfer.setDragImage( 164 | this, 165 | (rect.width / 2), 166 | (rect.height / 2) 167 | ); 168 | } 169 | 170 | export async function tabDrop(e) { 171 | e.preventDefault(); 172 | const tabIds = getTabIds(e); 173 | e.stopPropagation(); 174 | 175 | // get target tab 176 | const tabNode = e.target.closest('.tab'); 177 | if (!tabNode) return false; 178 | 179 | const tab = await browser.tabs.get(parseInt(tabNode.dataset.id)); 180 | // ---- 181 | 182 | // abort if you drop over moved tab 183 | if (tabIds.includes(tab.id)) return false; 184 | // ---- 185 | 186 | // get taget tab group ID 187 | const groupNode = e.target.closest('.group'); 188 | const tabGroupId = parseInt(groupNode.dataset.id); 189 | // ---- 190 | 191 | 192 | const rect = tabNode.getBoundingClientRect(); 193 | let dropBefore = true; 194 | 195 | if (groupNode.querySelector('.tabs.list')) { 196 | if (e.clientY > rect.top+(rect.height/2)) { 197 | dropBefore = false; 198 | } 199 | } else { 200 | if (e.clientX > rect.left+(rect.width/2)) { 201 | dropBefore = false; 202 | } 203 | } 204 | 205 | 206 | // move the tab node 207 | for (const tabId of tabIds) { 208 | if (tabId == tab.id) return false; // abort if you drop over moved tab 209 | const _tabNode = html.tabs.get(tabId); 210 | if (_tabNode) { 211 | if (dropBefore) { 212 | tabNode.insertAdjacentElement('beforebegin', _tabNode); 213 | } else { 214 | tabNode.insertAdjacentElement('afterend', _tabNode); 215 | } 216 | } 217 | } 218 | 219 | html.groups.fitTabs(); 220 | // ---- 221 | 222 | const currentWindowId = (await browser.windows.getCurrent()).id; 223 | 224 | // find new index 225 | let toIndex = tab.index; 226 | 227 | const fromTabId = tabIds[0]; 228 | const fromIndex = (await browser.tabs.get(fromTabId)).index; 229 | 230 | if (fromIndex < toIndex) { 231 | if (dropBefore) { 232 | toIndex -= 1; 233 | } 234 | } else { 235 | if (!dropBefore) { 236 | toIndex += 1; 237 | } 238 | } 239 | // ---- 240 | 241 | moveTabs(tabIds, currentWindowId, tabGroupId, toIndex); 242 | 243 | clearTabDragCSS(e); 244 | 245 | return false; 246 | } 247 | 248 | function clearTabDragCSS(e) { 249 | const tabIds = getTabIds(e); 250 | for (const tabId of tabIds) { 251 | const tabNode = html.tabs.get(tabId); 252 | if (tabNode) { 253 | tabNode.classList.remove('drag'); 254 | } 255 | } 256 | } 257 | 258 | export function tabDragEnd(e) { 259 | clearTabSelection(e); 260 | } 261 | // ---- 262 | -------------------------------------------------------------------------------- /src/panorama/js/view.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | import '/common/tabGroups-polyfill.js' 5 | 6 | import {html} from './html.js' 7 | 8 | import * as theme from '/common/theme.js' 9 | import * as events from './view.events.js' 10 | import * as drag from './view.drag.js' 11 | 12 | 13 | export let viewWindowId = undefined; 14 | export let viewTabId = undefined; 15 | 16 | let viewLastAccessed = 0; 17 | 18 | export let options = { 19 | themeOverride: false, 20 | listView: false 21 | }; 22 | 23 | document.addEventListener('DOMContentLoaded', async() => { 24 | 25 | viewWindowId = (await browser.windows.getCurrent()).id; 26 | viewTabId = (await browser.tabs.getCurrent()).id; 27 | 28 | // get options 29 | const storage = await browser.storage.local.get(); 30 | if (storage.hasOwnProperty('themeOverride')) { 31 | options.themeOverride = storage.themeOverride; 32 | } 33 | if (storage.hasOwnProperty('listView')) { 34 | options.listView = storage.listView; 35 | } 36 | // ---- 37 | 38 | theme.set(options.themeOverride); 39 | 40 | await initializeTabGroupNodes(); 41 | await initializeTabNodes(); 42 | 43 | captureThumbnails(); 44 | 45 | // view events 46 | document.getElementById('groups').addEventListener('dblclick', (event) => { 47 | if (event.target == document.getElementById('groups')) { 48 | browser.tabGroups.create(); 49 | } 50 | }, false); 51 | 52 | document.getElementById('groups').addEventListener('auxclick', (event) => { 53 | if (event.target == document.getElementById('groups') && event.button == 1) { 54 | browser.tabGroups.create(); 55 | } 56 | }, false); 57 | 58 | document.addEventListener('visibilitychange', async() => { 59 | if (document.hidden) { 60 | browser.tabs.onUpdated.removeListener(captureThumbnail); 61 | viewLastAccessed = (new Date).getTime(); 62 | } else { 63 | await captureThumbnails(); 64 | browser.tabs.onUpdated.addListener(captureThumbnail, {properties: ['url', 'status']}); 65 | } 66 | }, false); 67 | 68 | browser.theme.onUpdated.addListener(({newTheme, windowId}) => { 69 | theme.set(options.themeOverride, newTheme); 70 | }); 71 | // ---- 72 | 73 | // theme update 74 | browser.runtime.onMessage.addListener((message, sender, sendResponse) => { 75 | switch (message.event) { 76 | case 'addon.options.onUpdated': { 77 | if (message.data.hasOwnProperty('themeOverride')) { 78 | options.themeOverride = message.data.themeOverride; 79 | theme.set(options.themeOverride); 80 | } 81 | if (message.data.hasOwnProperty('listView')) { 82 | options.listView = message.data.listView; 83 | html.groups.fitTabs(); 84 | } 85 | break; 86 | } 87 | default: 88 | break; 89 | } 90 | }); 91 | // ---- 92 | 93 | // search 94 | /*const search = document.querySelector('#search input'); 95 | let searchIndex = {}; 96 | 97 | search.addEventListener('focus', async function(event) { 98 | const tabNodes = document.querySelectorAll('.tab'); 99 | 100 | await tabNodes.forEach(async(tabNode) => { 101 | searchIndex[tabNode.id.substr(3)] = tabNode.querySelector('.title span').textContent.toLowerCase(); 102 | }); 103 | }, false); 104 | 105 | search.addEventListener('input', async function(event) { 106 | let hits = []; 107 | for (const [id, title] of Object.entries(searchIndex)) { 108 | if (title.includes(this.value.toLowerCase())) { 109 | hits.push(id); 110 | } 111 | } 112 | console.log(hits); 113 | }, false); 114 | 115 | search.addEventListener('keydown', function(event) { 116 | if(event.keyCode == 13) { 117 | search.blur(); 118 | } 119 | }, false);*/ 120 | // ---- 121 | 122 | // tab group events 123 | browser.tabGroups.onCreated.addListener(events.groupCreated); 124 | browser.tabGroups.onRemoved.addListener(events.groupRemoved); 125 | // ---- 126 | 127 | // tab events 128 | browser.tabs.onCreated.addListener(events.tabCreated); 129 | browser.tabs.onRemoved.addListener(events.tabRemoved); 130 | browser.tabs.onUpdated.addListener(events.tabUpdated), {properties: ['favIconUrl', 'pinned', 'title', 'url', 'discarded', 'status']}; 131 | 132 | browser.tabs.onActivated.addListener(events.tabActivated); 133 | 134 | browser.tabs.onMoved.addListener(events.tabMoved); 135 | 136 | browser.tabs.onAttached.addListener(events.tabAttached); 137 | browser.tabs.onDetached.addListener(events.tabDetached); 138 | // ---- 139 | 140 | // drag events 141 | document.addEventListener('click', drag.clearTabSelection, true); 142 | 143 | document.getElementById('groups').addEventListener('dragover', drag.viewDragOver, false); 144 | document.getElementById('groups').addEventListener('drop', drag.viewDrop, false); 145 | // ---- 146 | 147 | const groupResize = function () { 148 | html.groups.fitTabs(); 149 | } 150 | 151 | new ResizeObserver(groupResize).observe(document.getElementById('groups')); 152 | }); 153 | 154 | 155 | async function initializeTabGroupNodes() { 156 | 157 | const groups = await browser.tabGroups.query({windowId: viewWindowId}); 158 | 159 | await Promise.all(groups.map(async(group) => { 160 | const tabGroupNode = html.groups.create(group); 161 | 162 | let rect = await browser.sessions.getGroupValue(group.id, 'rect'); 163 | 164 | if (!rect) { 165 | rect = {x: 0, y: 0, w: 0.5, h: 0.5}; 166 | await browser.sessions.setGroupValue(group.id, 'rect', rect); 167 | } 168 | 169 | html.groups.resize(tabGroupNode, rect); 170 | html.groups.stack(tabGroupNode); 171 | 172 | document.getElementById('groups').appendChild(tabGroupNode); 173 | })); 174 | } 175 | 176 | 177 | async function initializeTabNodes() { 178 | 179 | const tabs = await browser.tabs.query({currentWindow: true}); 180 | 181 | let nodes = { 182 | 'pinned': document.getElementById('pinned'), 183 | 'groupless': document.getElementById('groupless') 184 | }; 185 | 186 | for (const groupNode of document.getElementById('groups').children) { 187 | nodes[groupNode.dataset.id] = groupNode.querySelector('.newtab'); 188 | } 189 | 190 | for (const tab of tabs) { 191 | 192 | let tabNode = html.tabs.create(tab); 193 | html.tabs.update(tabNode, tab); 194 | html.tabs.updateThumbnail(tabNode, tab.id); 195 | 196 | html.tabs.updateFavicon(tabNode, tab); 197 | 198 | if (tab.pinned == true) { 199 | nodes['pinned'].appendChild(tabNode); 200 | 201 | } else if (nodes.hasOwnProperty(tab.groupId)) { 202 | nodes[tab.groupId].insertAdjacentElement('beforebegin', tabNode); 203 | 204 | } else { 205 | nodes['groupless'].appendChild(tabNode); 206 | } 207 | } 208 | 209 | html.groups.fitTabs(); 210 | html.tabs.setActive(); 211 | } 212 | 213 | 214 | export async function captureThumbnail(tabId, changeInfo, tab) { 215 | const thumbnail = await browser.tabs.captureTab(tabId, {format: 'jpeg', quality: 80, scale: 0.25}); 216 | html.tabs.updateThumbnail(html.tabs.get(tabId), tabId, thumbnail); 217 | browser.sessions.setTabValue(tabId, 'thumbnail', thumbnail); 218 | } 219 | 220 | async function captureThumbnails() { 221 | const tabs = browser.tabs.query({currentWindow: true, discarded: false, pinned: false, highlighted: false}); 222 | 223 | for(const tab of await tabs) { 224 | if (tab.lastAccessed > viewLastAccessed) { 225 | await captureThumbnail(tab.id); 226 | } 227 | } 228 | } 229 | 230 | 231 | 232 | 233 | 234 | -------------------------------------------------------------------------------- /src/options/options.backup.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | import * as html from '/common/html.js' 5 | import * as plurals from '/common/plurals.js' 6 | 7 | 8 | function getDateString(date) { 9 | 10 | date.setMinutes(date.getMinutes() - date.getTimezoneOffset()); // time-zone fix 11 | 12 | let dateString = date.toISOString(); 13 | dateString = dateString.replaceAll('-', ''); 14 | dateString = dateString.replaceAll('T', '-'); 15 | dateString = dateString.replaceAll(':', ''); 16 | dateString = dateString.replace(/\..*/, ''); 17 | 18 | return dateString; 19 | } 20 | 21 | 22 | // backup 23 | async function saveBackup() { 24 | 25 | const backup = await browser.runtime.sendMessage({ 26 | action: 'addon.backup.create' 27 | }); 28 | 29 | const blob = new Blob([JSON.stringify(backup, null, '\t')], {type : 'application/json'}); 30 | const dataUrl = window.URL.createObjectURL(blob); 31 | const filename = 'panorama-view-backup-' + getDateString(new Date()) + '.json'; 32 | 33 | const onComplete = (delta) => { 34 | if (delta.state && delta.state.current == 'complete') { 35 | window.URL.revokeObjectURL(dataUrl); 36 | browser.downloads.onChanged.removeListener(onComplete); 37 | } 38 | }; 39 | 40 | browser.downloads.onChanged.addListener(onComplete); 41 | 42 | browser.downloads.download({ 43 | url: dataUrl, 44 | filename: filename, 45 | conflictAction: 'uniquify', 46 | saveAs: true 47 | }); 48 | } 49 | 50 | let autoBackups = []; 51 | 52 | async function loadBackup() { 53 | 54 | const selectBackup = document.getElementById('selectBackup'); 55 | 56 | if (selectBackup.value == 'false') { 57 | return; 58 | } else if (selectBackup.value == 'file') { 59 | 60 | const file = document.getElementById('backupFileInput').files[0]; 61 | 62 | if (!file) return; 63 | 64 | if (file.type != 'application/json') { 65 | alert(browser.i18n.getMessage('optionLoadError')); 66 | return; 67 | } 68 | 69 | const reader = new FileReader(); 70 | 71 | reader.onload = (json) => { 72 | let backup = JSON.parse(json.target.result); 73 | 74 | if ((backup.version && backup.version[0] == 'tabGroups' || backup.version && backup.version[0] == 'sessionrestore') && backup.version[1] == 1) { 75 | // convert from old tab groups backup to version 1 (legacy) 76 | backup = backups.convertTG(backup); 77 | } 78 | 79 | if (backup.file && backup.file.type == 'panoramaView' && backup.file.version == 1) { 80 | // convert from panormama view backup version 1 81 | backup = backups.convertV1(backup); 82 | } 83 | 84 | browser.runtime.sendMessage({ 85 | action: 'addon.backup.open', 86 | data: backup 87 | }); 88 | }; 89 | 90 | reader.readAsText(file); 91 | 92 | } else { 93 | 94 | const i = Number(selectBackup.value); 95 | 96 | if (i < 0 || i > 3) { 97 | return; 98 | } 99 | 100 | browser.runtime.sendMessage({ 101 | action: 'addon.backup.open', 102 | data: autoBackups[i].data 103 | }); 104 | } 105 | } 106 | 107 | export function convertV1(data) { 108 | 109 | let makeTabData = (tab) => { 110 | return { 111 | url: tab.url, 112 | title: tab.title, 113 | cookieStoreId: 'firefox-default' 114 | }; 115 | }; 116 | 117 | let backup = { 118 | file: { 119 | type: 'panoramaView', 120 | version: 2 121 | }, 122 | windows: [] 123 | }; 124 | 125 | for (const window of data.windows) { 126 | 127 | let pinnedTabs = []; 128 | let tabGroups = []; 129 | 130 | for (let tab of window.tabs) { 131 | if (tab.pinned) { 132 | pinnedTabs.push(makeTabData(tab)); 133 | } 134 | } 135 | 136 | for (const group of window.groups) { 137 | 138 | let tabGroup = { 139 | title: group.name, 140 | rect: {x: group.rect.x, y: group.rect.y, w: group.rect.w, h: group.rect.h}, 141 | tabs: [] 142 | } 143 | 144 | for (const tab of window.tabs) { 145 | if (tab.groupId == group.id) { 146 | tabGroup.tabs.push(makeTabData(tab)); 147 | } 148 | } 149 | 150 | tabGroups.push(tabGroup); 151 | } 152 | 153 | backup.windows.push({ 154 | pinnedTabs: pinnedTabs, 155 | tabGroups: tabGroups 156 | }); 157 | } 158 | 159 | return backup; 160 | } 161 | 162 | export function convertTG(tgData) { 163 | var data = { 164 | file: { 165 | type: 'panoramaView', 166 | version: 1 167 | }, 168 | windows: [] 169 | }; 170 | 171 | for(var wi in tgData.windows) { 172 | 173 | const tabviewGroup = JSON.parse(tgData.windows[wi].extData['tabview-group']); 174 | const tabviewGroups = JSON.parse(tgData.windows[wi].extData['tabview-groups']); 175 | 176 | data.windows[wi] = {groups: [], tabs: [], activeGroup: tabviewGroups.activeGroupId, groupIndex: tabviewGroups.nextID}; 177 | 178 | for(const gkey in tabviewGroup) { 179 | data.windows[wi].groups.push({ 180 | id: tabviewGroup[gkey].id, 181 | name: tabviewGroup[gkey].title, 182 | rect: {x: 0, y: 0, w: 0.25, h: 0.5}, 183 | }); 184 | } 185 | 186 | for(const ti in tgData.windows[wi].tabs) { 187 | 188 | var tab = tgData.windows[wi].tabs[ti]; 189 | 190 | data.windows[wi].tabs.push({ 191 | url: tab.entries[0].url, 192 | title: tab.entries[0].title, 193 | groupId: JSON.parse(tab.extData['tabview-tab']).groupID, 194 | index: Number(ti), 195 | pinned: false, 196 | }); 197 | } 198 | } 199 | 200 | return data; 201 | } 202 | 203 | 204 | function fillBackupSelection(autoBackups) { 205 | 206 | const elements = document.getElementById('selectBackup').querySelectorAll('.auto'); 207 | 208 | for (const element of elements) { 209 | element.remove(); 210 | } 211 | 212 | for (let i in autoBackups) { 213 | const time = new Date(autoBackups[i].time); 214 | selectBackup.appendChild(html.newElement('option', {content: `${browser.i18n.getMessage('optionAutoBackup')} ${time.toLocaleString()}`, value: i, class: 'auto'})); 215 | } 216 | } 217 | 218 | 219 | export async function createUI() { 220 | 221 | document.getElementById('saveBackup').addEventListener('click', saveBackup); 222 | 223 | // load backup list 224 | document.getElementById('loadBackup').addEventListener('click', loadBackup); 225 | 226 | const backupFileInput = document.getElementById('backupFileInput'); 227 | const selectBackup = document.getElementById('selectBackup'); 228 | 229 | autoBackups = await browser.runtime.sendMessage({ 230 | action: 'addon.backup.getBackups' 231 | }); 232 | fillBackupSelection(autoBackups); 233 | 234 | backupFileInput.addEventListener('change', (e) => { 235 | const filename = e.target.value.split(/(\\|\/)/g).pop(); 236 | const file = html.newElement('option', {content: filename, value: 'file'}); 237 | selectBackup.appendChild(file); 238 | selectBackup.value = 'file'; 239 | }); 240 | 241 | selectBackup.addEventListener('input', (e) => { 242 | if (e.target.value == 'browse') { 243 | backupFileInput.click(); 244 | } 245 | }); 246 | 247 | selectBackup.addEventListener('change', async(e) => { 248 | const file = e.target.querySelector('[value="file"]') 249 | if (file) file.remove(); 250 | backupFileInput.value = null; 251 | }, false); 252 | // ---- 253 | 254 | // Auto-Backup 255 | const autoBackup = document.getElementById('autoBackup'); 256 | const autoBackupInterval = document.getElementById('autoBackupInterval'); 257 | 258 | autoBackup.value = await browser.runtime.sendMessage({ 259 | action: 'addon.backup.getInterval' 260 | }); 261 | 262 | const formatTime = (value) => { 263 | const hours = Math.floor(value / 60); 264 | const minutes = value % 60; 265 | if (hours == 0 && minutes == 0) { 266 | return browser.i18n.getMessage('optionAutomaticBackupIntervalNever'); 267 | } else { 268 | return plurals.parse(browser.i18n.getMessage('optionAutomaticBackupIntervalValue', [hours, minutes])); 269 | } 270 | } 271 | 272 | autoBackupInterval.textContent = formatTime(autoBackup.value); 273 | 274 | autoBackup.addEventListener('input', function() { 275 | autoBackupInterval.textContent = formatTime(autoBackup.value); 276 | browser.runtime.sendMessage({ 277 | action: 'addon.backup.setInterval', 278 | interval: autoBackup.value 279 | }); 280 | }); 281 | // ---- 282 | } 283 | -------------------------------------------------------------------------------- /src/background/addon.tabGroups.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | import * as core from './core.js' 5 | import * as addon_tabs from './addon.tabs.js' 6 | import * as backup from './backup.js' 7 | 8 | import Mutex from '/common/mutex.js' 9 | 10 | // internal 11 | let groupUuid = 0; 12 | let groups = []; // cache 13 | let groupLock = new Mutex(); 14 | 15 | function newGroupId() { 16 | return groupUuid += 1; 17 | } 18 | 19 | async function saveGroups() { 20 | const windows = await browser.windows.getAll({}); 21 | 22 | await Promise.all(windows.map(async(window) => { 23 | let windowGroups = []; 24 | for (const group of groups) { 25 | if (group.windowId == window.id) { 26 | windowGroups.push(group); 27 | } 28 | } 29 | await browser.sessions.setWindowValue(window.id, 'groups', windowGroups); 30 | })); 31 | } 32 | 33 | function sanitizeGroup(group) { 34 | return { 35 | cookieStoreId: group.cookieStoreId, 36 | id: group.id, 37 | title: group.title, 38 | windowId: group.windowId, 39 | 40 | lastAccessed: group.lastAccessed, // temporary 41 | } 42 | } 43 | // ---- 44 | 45 | export async function setActiveId(windowId, groupId) { 46 | let unlock = await groupLock.lock(); 47 | if (!groups.find(group => group.id == groupId)) return undefined; 48 | await unlock(); 49 | return browser.sessions.setWindowValue(windowId, 'activeGroup', groupId); 50 | } 51 | 52 | export async function getActiveId(windowId) { 53 | return browser.sessions.getWindowValue(windowId, 'activeGroup'); 54 | } 55 | 56 | 57 | export async function initialize() { 58 | const windows = await browser.windows.getAll({}); 59 | 60 | for (const window of windows) { 61 | let windowGroups = await browser.sessions.getWindowValue(window.id, 'groups'); 62 | 63 | if (!windowGroups) continue; 64 | 65 | for (let group of windowGroups) { 66 | group.windowId = window.id; 67 | 68 | if (group.id > groupUuid) { 69 | groupUuid = group.id; 70 | } 71 | } 72 | 73 | if (windowGroups) { 74 | groups = groups.concat(windowGroups); 75 | } 76 | } 77 | 78 | browser.windows.onCreated.addListener(windowCreated); 79 | browser.windows.onRemoved.addListener(windowId => { 80 | groups = groups.filter(group => group.windowId != windowId); 81 | }); 82 | } 83 | 84 | 85 | function windowCreated(window) { 86 | if (!backup.opening) { 87 | create({}, window.id); 88 | } 89 | } 90 | 91 | 92 | export async function create(info = {}, currentWindowId) { 93 | 94 | info.windowId = info.windowId || currentWindowId; 95 | 96 | const group = { 97 | cookieStoreId: info.cookieStoreId || 'firefox-default', 98 | id: newGroupId(), 99 | sessionStorage: {}, 100 | title: info.title || browser.i18n.getMessage('defaultGroupName'), 101 | windowId: info.windowId, 102 | 103 | lastAccessed: (new Date).getTime(), // temporary 104 | }; 105 | 106 | let unlock = await groupLock.lock(); 107 | 108 | groups.push(group); 109 | await saveGroups(); 110 | 111 | await unlock(); 112 | 113 | await setActiveId(group.windowId, group.id); 114 | 115 | if (info.hasOwnProperty('populate') && info.populate == true) { 116 | addon_tabs.create({groupId: group.id, windowId: group.windowId}); 117 | } 118 | 119 | const sending = browser.runtime.sendMessage({event: 'browser.tabGroups.onCreated', group: sanitizeGroup(group)}); 120 | sending.catch(error => {}); 121 | 122 | return sanitizeGroup(group); 123 | } 124 | 125 | export async function get(groupId) { 126 | let unlock = await groupLock.lock(); 127 | const group = groups.find(group => group.id == groupId); 128 | await unlock(); 129 | if (!group) throw Error(`Invalid group ID: ${groupId}`); 130 | return sanitizeGroup(group); 131 | } 132 | 133 | 134 | export async function query(info = {}, currentWindowId) { 135 | 136 | if (info.currentWindow == true) { 137 | info.windowId = currentWindowId; 138 | } 139 | 140 | let unlock = await groupLock.lock(); 141 | let matchingGroups = groups; 142 | await unlock(); 143 | 144 | if (info.hasOwnProperty('windowId')) { 145 | matchingGroups = matchingGroups.filter(group => group.windowId == info.windowId); 146 | } 147 | 148 | if (info.hasOwnProperty('id')) { 149 | matchingGroups = matchingGroups.filter(group => group.id == info.id); 150 | } 151 | 152 | if (info.hasOwnProperty('title')) { 153 | matchingGroups = matchingGroups.filter(group => group.title == info.title); 154 | } 155 | 156 | return matchingGroups.map(group => sanitizeGroup(group)); 157 | } 158 | 159 | 160 | export async function remove(groupId) { 161 | 162 | let unlock = await groupLock.lock(); 163 | const group = groups.find(group => group.id == groupId); 164 | await unlock(); 165 | 166 | if (!group) { 167 | throw Error(`Invalid group ID: ${groupId}`); 168 | } 169 | 170 | // remove tabs in group 171 | const tabs = await browser.tabs.query({currentWindow: true}); 172 | let tabsToRemove = []; 173 | 174 | for (const tab of tabs) { 175 | tab.groupId = await browser.sessions.getTabValue(tab.id, 'groupId'); 176 | if (tab.groupId == groupId) { 177 | tabsToRemove.push(tab.id); 178 | } 179 | } 180 | await browser.tabs.remove(tabsToRemove); 181 | 182 | // check if tabs were removed and abort if not (beforeunload was called or something) 183 | for (const tabId of tabsToRemove) { 184 | try { 185 | tab = await browser.tabs.get(tabId); 186 | return undefined; 187 | } catch (error) { 188 | // all good, tab was removed 189 | } 190 | } 191 | // ---- 192 | 193 | unlock = await groupLock.lock(); 194 | 195 | groups = groups.filter(_group => _group.id != group.id); 196 | await saveGroups(); 197 | 198 | const windowGroups = groups.find(_group => _group.windowId == group.windowId); 199 | 200 | await unlock(); 201 | 202 | const sending = browser.runtime.sendMessage({event: 'browser.tabGroups.onRemoved', groupId: groupId, removeInfo: {windowId: group.windowId}}); 203 | sending.catch(error => {}); 204 | 205 | if (windowGroups == undefined) { 206 | await create({windowId: group.windowId}); 207 | } 208 | 209 | return groupId; 210 | } 211 | 212 | 213 | export async function update(groupId, info = {}) { 214 | 215 | let unlock = await groupLock.lock(); 216 | 217 | let group = groups.find(group => group.id == groupId); 218 | if (!group) { 219 | await unlock(); 220 | throw Error(`Invalid group ID: ${groupId}`); 221 | } 222 | 223 | if (info.hasOwnProperty('title')) { 224 | group.title = info.title; 225 | } 226 | 227 | if (info.hasOwnProperty('rect')) { 228 | group.rect = info.rect; 229 | } 230 | 231 | group.lastAccessed = (new Date).getTime(); 232 | 233 | await saveGroups(); 234 | 235 | await unlock(); 236 | 237 | const sending = browser.runtime.sendMessage({event: 'browser.tabGroups.onUpdated', group: sanitizeGroup(group)}); 238 | sending.catch(error => {}); 239 | 240 | return sanitizeGroup(group); 241 | } 242 | 243 | 244 | export async function setGroupValue(groupId, key, value, appId) { 245 | 246 | value = JSON.stringify(value); 247 | 248 | let unlock = await groupLock.lock(); 249 | 250 | let group = groups.find(group => group.id == groupId); 251 | if (!group) { 252 | await unlock(); 253 | throw Error(`Invalid group ID: ${groupId}`); 254 | } 255 | 256 | if (!group.hasOwnProperty('sessionStorage')) { 257 | group.sessionStorage = {}; 258 | } 259 | 260 | if (!group.sessionStorage.hasOwnProperty(appId)) { 261 | group.sessionStorage[appId] = {}; 262 | } 263 | 264 | group.sessionStorage[appId][key] = value; 265 | 266 | await saveGroups(); 267 | 268 | await unlock(); 269 | 270 | return; 271 | } 272 | 273 | 274 | export async function getGroupValue(groupId, key, appId) { 275 | 276 | let unlock = await groupLock.lock(); 277 | 278 | let group = groups.find(group => group.id == groupId); 279 | if (!group) { 280 | await unlock(); 281 | throw Error(`Invalid group ID: ${groupId}`); 282 | } 283 | 284 | if (group.hasOwnProperty('sessionStorage') && 285 | group.sessionStorage.hasOwnProperty(appId) && 286 | group.sessionStorage[appId].hasOwnProperty(key)) { 287 | 288 | let value; 289 | try { 290 | value = JSON.parse(group.sessionStorage[appId][key]); 291 | } catch (error) { 292 | value = undefined; 293 | } 294 | 295 | await unlock(); 296 | 297 | return value; 298 | } 299 | 300 | await unlock(); 301 | 302 | return undefined; 303 | } 304 | 305 | 306 | export async function removeGroupValue(groupId, key, appId) { 307 | 308 | let unlock = await groupLock.lock(); 309 | 310 | let group = groups.find(group => group.id == groupId); 311 | if (!group) { 312 | await unlock(); 313 | throw Error(`Invalid group ID: ${groupId}`); 314 | } 315 | 316 | if (group.hasOwnProperty('sessionStorage') && 317 | group.sessionStorage.hasOwnProperty(appId) && 318 | group.sessionStorage[appId].hasOwnProperty(key)) { 319 | 320 | delete group.sessionStorage[appId][key]; 321 | 322 | await saveGroups(); 323 | 324 | await unlock(); 325 | 326 | return; 327 | } 328 | 329 | await unlock(); 330 | 331 | throw Error(`Invalid key: ${key}`); 332 | } 333 | -------------------------------------------------------------------------------- /src/panorama/js/html.groups.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | import {newElement} from '/common/html.js' 5 | import * as plurals from '/common/plurals.js' 6 | 7 | import * as drag from './view.drag.js' 8 | import {options} from './view.js' 9 | 10 | 11 | export function create(group) { 12 | 13 | // edges 14 | const top = newElement('div', {class: 'top'}); 15 | const right = newElement('div', {class: 'right'}); 16 | const bottom = newElement('div', {class: 'bottom'}); 17 | const left = newElement('div', {class: 'left'}); 18 | 19 | // corners 20 | const top_right = newElement('div', {class: 'top_right'}); 21 | const bottom_right = newElement('div', {class: 'bottom_right'}); 22 | const bottom_left = newElement('div', {class: 'bottom_left'}); 23 | const top_left = newElement('div', {class: 'top_left'}); 24 | 25 | // header 26 | const name = newElement('span', {class: 'name', content: group.title}); 27 | const input = newElement('input', {type: 'text', value: group.title}); 28 | const tabCount = newElement('span', {class: 'tab_count'}); 29 | const close = newElement('div', {class: 'close'}); 30 | 31 | const header = newElement('div', {class: 'header'}, [name, input, tabCount, close]); 32 | // ---- 33 | 34 | // newtab 35 | const newtab = newElement('div', {class: 'newtab'}, [newElement('div', {class: 'border'})]); 36 | 37 | // group 38 | const tabs = newElement('div', {class: 'tabs transition'}, [newtab]); 39 | const resize = newElement('div', {class: 'resize'}, [top, right, bottom, left, top_right, bottom_right, bottom_left, top_left]); 40 | const node = newElement('div', {class: 'group', 'data-id': group.id}, [resize, header, tabs]); 41 | // ---- 42 | 43 | close.addEventListener('click', function(event) { 44 | event.stopPropagation(); 45 | 46 | const childNodes = tabs.childNodes; 47 | const tabCount = childNodes.length-1; 48 | 49 | let closing = false; 50 | 51 | if (tabCount > 0) { 52 | if (window.confirm(plurals.parse(browser.i18n.getMessage('pvCloseGroupConfirmation', tabCount)))) { 53 | closing = true; 54 | } 55 | } else { 56 | closing = true; 57 | } 58 | 59 | if (closing) { 60 | browser.tabGroups.remove(group.id).then((removedId) => { 61 | if (removedId != undefined) node.remove(); 62 | }); 63 | } 64 | }, false); 65 | 66 | tabs.addEventListener('click', function(event) { 67 | event.stopPropagation(); 68 | }, false); 69 | 70 | newtab.addEventListener('click', async function(event) { 71 | event.stopPropagation(); 72 | await browser.tabs.create({active: true, groupId: group.id}); 73 | }, false); 74 | 75 | // move 76 | var moveFunc = function(event) { 77 | if (event.buttons == 1) { // only move on left click 78 | event.preventDefault(); 79 | event.stopPropagation(); 80 | groupTransform(group, node, 1, 1, 1, 1, header); 81 | } 82 | }; 83 | header.addEventListener('mousedown', moveFunc, false); 84 | 85 | // renaming groups 86 | var editing = false; 87 | 88 | header.addEventListener('dblclick', function(event) { 89 | if(!editing) { 90 | editing = true; 91 | 92 | header.removeEventListener('mousedown', moveFunc, false); 93 | 94 | //header.classList.add('edit'); 95 | input.setSelectionRange(0, input.value.length); 96 | input.focus(); 97 | } 98 | }, false); 99 | 100 | input.addEventListener('input', function(event) { 101 | name.textContent = this.value; 102 | }, false); 103 | 104 | input.addEventListener('keydown', function(event) { 105 | if(event.key == 'Enter') { 106 | input.blur(); 107 | } 108 | }, false); 109 | 110 | input.addEventListener('blur', function(event) { 111 | input.setSelectionRange(0, 0); 112 | 113 | browser.tabGroups.update(group.id, {title: input.value}); 114 | 115 | header.addEventListener('mousedown', moveFunc, false); 116 | 117 | editing = false; 118 | }, false); 119 | 120 | const titleResize = function () { 121 | input.style.width = name.getBoundingClientRect().width + 'px'; 122 | } 123 | 124 | new ResizeObserver(titleResize).observe(name); 125 | // ---- 126 | 127 | tabs.addEventListener('dragover', drag.groupDragOver, false); 128 | tabs.addEventListener('drop', drag.groupDrop, false); 129 | 130 | // resize 131 | top.addEventListener('mousedown', function(event) { 132 | if (event.buttons == 1) { 133 | event.preventDefault(); 134 | event.stopPropagation(); 135 | groupTransform(group, node, 1, 0, 0, 0, this); 136 | } 137 | }, false); 138 | 139 | right.addEventListener('mousedown', function(event) { 140 | if (event.buttons == 1) { 141 | event.preventDefault(); 142 | event.stopPropagation(); 143 | groupTransform(group, node, 0, 1, 0, 0, this); 144 | } 145 | }, false); 146 | 147 | bottom.addEventListener('mousedown', function(event) { 148 | if (event.buttons == 1) { 149 | event.preventDefault(); 150 | event.stopPropagation(); 151 | groupTransform(group, node, 0, 0, 1, 0, this); 152 | } 153 | }, false); 154 | 155 | left.addEventListener('mousedown', function(event) { 156 | if (event.buttons == 1) { 157 | event.preventDefault(); 158 | event.stopPropagation(); 159 | groupTransform(group, node, 0, 0, 0, 1, this); 160 | } 161 | }, false); 162 | 163 | top_right.addEventListener('mousedown', function(event) { 164 | if (event.buttons == 1) { 165 | event.preventDefault(); 166 | event.stopPropagation(); 167 | groupTransform(group, node, 1, 1, 0, 0, this); 168 | } 169 | }, false); 170 | 171 | bottom_right.addEventListener('mousedown', function(event) { 172 | if (event.buttons == 1) { 173 | event.preventDefault(); 174 | event.stopPropagation(); 175 | groupTransform(group, node, 0, 1, 1, 0, this); 176 | } 177 | }, false); 178 | 179 | bottom_left.addEventListener('mousedown', function(event) { 180 | if (event.buttons == 1) { 181 | event.preventDefault(); 182 | event.stopPropagation(); 183 | groupTransform(group, node, 0, 0, 1, 1, this); 184 | } 185 | }, false); 186 | 187 | top_left.addEventListener('mousedown', function(event) { 188 | if (event.buttons == 1) { 189 | event.preventDefault(); 190 | event.stopPropagation(); 191 | groupTransform(group, node, 1, 0, 0, 1, this); 192 | } 193 | }, false); 194 | 195 | return node; 196 | } 197 | 198 | export function get(tabGroupId) { 199 | return document.querySelector(`.group[data-id="${tabGroupId}"]`); 200 | } 201 | 202 | export async function resize(node, rect) { 203 | node.style.top = (rect.y * 100) + '%'; 204 | node.style.left = (rect.x * 100) + '%'; 205 | node.style.width = (rect.w * 100) + '%'; 206 | node.style.height = (rect.h * 100) + '%'; 207 | } 208 | 209 | export async function stack(node, tabGroup) { 210 | const tabGroupId = parseInt(node.dataset.id); 211 | 212 | tabGroup = tabGroup || await browser.tabGroups.get(tabGroupId); 213 | 214 | node.style.zIndex = Math.floor(tabGroup.lastAccessed / 100).toString().substr(-9); 215 | } 216 | 217 | function getFit(param) { 218 | 219 | let w = 0; 220 | let h = 0; 221 | let area = 0; 222 | 223 | for (let x = param.amount; x >= 1; x--) { 224 | 225 | let y = Math.ceil(param.amount / x) 226 | 227 | let a = (param.width - (x * (param.marginX*2))) / x; 228 | a = Math.min(a, param.max); 229 | let b = (a * param.ratio); 230 | 231 | if (b * y > param.height - (y * (param.marginY*2)) || b > param.max) { 232 | b = (param.height - (y * (param.marginY*2))) / y 233 | b = Math.min(b, param.max); 234 | a = b / param.ratio; 235 | } 236 | 237 | let tmp_area = a * b; 238 | 239 | if (tmp_area > area) { 240 | area = tmp_area; 241 | w = a; 242 | h = b; 243 | } 244 | } 245 | 246 | let fits = true; 247 | if (w < param.min || h < param.min) { 248 | fits = false; 249 | } 250 | 251 | return {fits: fits, width: w - 0.1, ratio: param.ratio}; 252 | } 253 | 254 | export function fitTabs(tabGroupNode) { 255 | if (tabGroupNode == undefined) { 256 | document.getElementById('groups').childNodes.forEach(async(tabGroupNode) => { 257 | fitTabsInGroup(tabGroupNode); 258 | }); 259 | } else { 260 | fitTabsInGroup(tabGroupNode); 261 | } 262 | } 263 | 264 | export function fitTabsInGroup(tabGroupNode) { 265 | 266 | const tabsNode = tabGroupNode.querySelector('.tabs'); 267 | const tabNodes = tabsNode.childNodes; 268 | 269 | tabGroupNode.querySelector('.tab_count').textContent = tabNodes.length - 1; 270 | 271 | // fit 272 | let rect = tabsNode.getBoundingClientRect(); 273 | 274 | let fit = getFit({ 275 | width: rect.width, 276 | height: rect.height, 277 | 278 | marginX: 4, 279 | marginY: 4, 280 | 281 | min: 70, 282 | max: 375, 283 | 284 | ratio: window.innerHeight / window.innerWidth, 285 | 286 | amount: tabNodes.length, 287 | }); 288 | 289 | // squished view 290 | if (!fit.fits){ 291 | fit = getFit({ 292 | width: rect.width, 293 | height: rect.height, 294 | 295 | marginX: 4, 296 | marginY: 4, 297 | 298 | min: 60, 299 | max: 160, 300 | 301 | ratio: (1 + fit.ratio + fit.ratio) / 3, 302 | 303 | amount: tabNodes.length, 304 | }); 305 | } 306 | 307 | // square view 308 | if (!fit.fits){ 309 | fit = getFit({ 310 | width: rect.width, 311 | height: rect.height, 312 | 313 | marginX: 4, 314 | marginY: 4, 315 | 316 | min: 20, 317 | max: 100, 318 | 319 | ratio: 1, 320 | 321 | amount: tabNodes.length, 322 | }); 323 | } 324 | 325 | if (!fit.fits) { 326 | tabsNode.classList.add('scroll'); 327 | } else { 328 | tabsNode.classList.remove('scroll'); 329 | } 330 | 331 | 332 | let w = fit.width; 333 | let h = w * fit.ratio; 334 | if (w < 20) w = 20; 335 | if (h < 20) h = 20; 336 | 337 | // icon view 338 | let size = 'normal'; 339 | 340 | if (w < 56) size = 'small'; 341 | if (w < 32) size = 'tiny'; 342 | 343 | if (w < 24 && options.listView == true) { 344 | size = 'list'; 345 | } 346 | 347 | 348 | tabsNode.classList.remove('small'); 349 | tabsNode.classList.remove('tiny'); 350 | tabsNode.classList.remove('list'); 351 | 352 | switch (size) { 353 | case 'small': tabsNode.classList.add('small'); break; 354 | case 'tiny': tabsNode.classList.add('tiny'); break; 355 | case 'list': tabsNode.classList.add('list'); break; 356 | default: break; 357 | } 358 | 359 | tabNodes.forEach(async(tabNode, index) => { 360 | tabNode.style.width = w + 'px'; 361 | tabNode.style.height = h + 'px'; 362 | }); 363 | } 364 | 365 | 366 | async function groupTransform(group, node, top, right, bottom, left, elem) { 367 | 368 | const snapValue = function(a, b, dst) { 369 | if (a >= b - dst && a <= b + dst){ 370 | return b; 371 | } else { 372 | return a; 373 | } 374 | }; 375 | 376 | document.body.setAttribute('style', 'cursor: ' + window.getComputedStyle(elem).cursor); 377 | 378 | const groupsRect = document.getElementById('groups').getBoundingClientRect(); 379 | 380 | group.rect = await browser.sessions.getGroupValue(group.id, 'rect'); 381 | 382 | const minw = 100 / groupsRect.width; 383 | const minh = 80 / groupsRect.height; 384 | 385 | const snap_dstx = 5 / groupsRect.width; 386 | const snap_dsty = 5 / groupsRect.height; 387 | 388 | const clamp = function(num, min, max) { 389 | return num <= min ? min : num >= max ? max : num; 390 | }; 391 | 392 | let first = true; 393 | let x, y, lx, ly; 394 | 395 | let rect = {}; 396 | 397 | const onmousemove = function(event) { 398 | event.preventDefault(); 399 | x = event.pageX / groupsRect.width; 400 | y = event.pageY / groupsRect.height; 401 | 402 | if (first) { 403 | lx = x; 404 | ly = y; 405 | first = false; 406 | browser.sessions.setGroupValue(group.id, 'rect', group.rect); 407 | browser.tabGroups.update(group.id, {}).then(group => stack(node, group)); 408 | } 409 | 410 | rect.x = group.rect.x; 411 | rect.y = group.rect.y; 412 | rect.w = Math.max(group.rect.w, minw); 413 | rect.h = Math.max(group.rect.h, minh); 414 | 415 | let rect_i = rect.x + rect.w; 416 | let rect_j = rect.y + rect.h; 417 | 418 | if (top) { rect.y += (y - ly); } 419 | if (right) { rect_i += (x - lx); } 420 | if (bottom) { rect_j += (y - ly); } 421 | if (left) { rect.x += (x - lx); } 422 | 423 | // snap (seems a bit over complicated, but it works for now) 424 | for (const tabGroupNode of document.getElementById('groups').childNodes) { 425 | 426 | if (node != tabGroupNode) { 427 | 428 | const _rect = { 429 | x: parseFloat(tabGroupNode.style.left) / 100, 430 | y: parseFloat(tabGroupNode.style.top) / 100, 431 | w: parseFloat(tabGroupNode.style.width) / 100, 432 | h: parseFloat(tabGroupNode.style.height) / 100 433 | } 434 | 435 | if (top && bottom) { 436 | rect.y = snapValue(rect.y, _rect.y, snap_dsty); 437 | rect.y = snapValue(rect.y, _rect.y + _rect.h, snap_dsty); 438 | 439 | rect.y = snapValue(rect.y + rect.h, _rect.y, snap_dsty) - rect.h; 440 | rect.y = snapValue(rect.y + rect.h, _rect.y + _rect.h, snap_dsty) - rect.h; 441 | } else if (top) { 442 | rect.y = snapValue(rect.y, _rect.y, snap_dsty); 443 | rect.y = snapValue(rect.y, _rect.y + _rect.h, snap_dsty); 444 | } else if (bottom) { 445 | rect_j = snapValue(rect_j, _rect.y, snap_dsty); 446 | rect_j = snapValue(rect_j, _rect.y + _rect.h, snap_dsty); 447 | } 448 | 449 | if (left && right) { 450 | rect.x = snapValue(rect.x, _rect.x, snap_dstx); 451 | rect.x = snapValue(rect.x, _rect.x + _rect.w, snap_dstx); 452 | 453 | rect.x = snapValue(rect.x + rect.w, _rect.x, snap_dstx) - rect.w; 454 | rect.x = snapValue(rect.x + rect.w, _rect.x + _rect.w, snap_dstx) - rect.w; 455 | } else if (left) { 456 | rect.x = snapValue(rect.x, _rect.x, snap_dstx); 457 | rect.x = snapValue(rect.x, _rect.x + _rect.w, snap_dstx); 458 | } else if (right) { 459 | rect_i = snapValue(rect_i, _rect.x, snap_dstx); 460 | rect_i = snapValue(rect_i, _rect.x + _rect.w, snap_dstx); 461 | } 462 | } 463 | } 464 | // ---- 465 | 466 | if (top && right && bottom && left) { 467 | if (rect.x < 0) { 468 | rect.x = 0; 469 | rect_i = rect.x + rect.w; 470 | } 471 | if (rect_i > 1) { 472 | rect_i = 1; 473 | rect.x = rect_i - rect.w; 474 | } 475 | 476 | if (rect.y < 0) { 477 | rect.y = 0; 478 | rect_j = rect.y + rect.h; 479 | } 480 | if (rect_j > 1) { 481 | rect_j = 1; 482 | rect.y = rect_j - rect.h; 483 | } 484 | } else { 485 | if (left) { rect.x = clamp(rect.x, 0, rect_i-minw); } 486 | if (right) { rect_i = clamp(rect_i, rect.x+minw, 1); } 487 | 488 | if (top) { rect.y = clamp(rect.y, 0, rect_j-minh); } 489 | if (bottom) { rect_j = clamp(rect_j, rect.y+minh, 1); } 490 | 491 | rect.w = Math.max(rect_i - rect.x, minw); 492 | rect.h = Math.max(rect_j - rect.y, minh); 493 | } 494 | 495 | resize(node, rect); 496 | fitTabs(node); 497 | } 498 | 499 | const onmouseup = () => { 500 | if(rect.x !== undefined) { 501 | browser.sessions.setGroupValue(group.id, 'rect', rect); 502 | } 503 | 504 | document.body.removeAttribute('style'); 505 | 506 | document.removeEventListener('mousemove', onmousemove); 507 | document.removeEventListener('mouseup', onmouseup); 508 | }; 509 | 510 | document.addEventListener('mousemove', onmousemove, false); 511 | document.addEventListener('mouseup', onmouseup, false); 512 | 513 | } 514 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /logo/logo.dxf: -------------------------------------------------------------------------------- 1 | 999 2 | dxfrw 0.6.3 3 | 0 4 | SECTION 5 | 2 6 | HEADER 7 | 9 8 | $ACADVER 9 | 1 10 | AC1021 11 | 9 12 | $DWGCODEPAGE 13 | 3 14 | ANSI_1252 15 | 9 16 | $INSBASE 17 | 10 18 | 0 19 | 20 20 | 0 21 | 30 22 | 0 23 | 9 24 | $EXTMIN 25 | 10 26 | -108.2811699973091 27 | 20 28 | -28.54415529538397 29 | 30 30 | 0 31 | 9 32 | $EXTMAX 33 | 10 34 | 51.41183343126368 35 | 20 36 | 137.7498052780863 37 | 30 38 | 0 39 | 9 40 | $LIMMIN 41 | 10 42 | 0 43 | 20 44 | 0 45 | 9 46 | $LIMMAX 47 | 10 48 | 420 49 | 20 50 | 297 51 | 9 52 | $ORTHOMODE 53 | 70 54 | 0 55 | 9 56 | $REGENMODE 57 | 70 58 | 1 59 | 9 60 | $FILLMODE 61 | 70 62 | 1 63 | 9 64 | $QTEXTMODE 65 | 70 66 | 0 67 | 9 68 | $MIRRTEXT 69 | 70 70 | 0 71 | 9 72 | $LTSCALE 73 | 40 74 | 1 75 | 9 76 | $ATTMODE 77 | 70 78 | 0 79 | 9 80 | $TEXTSIZE 81 | 40 82 | 2.5 83 | 9 84 | $TRACEWID 85 | 40 86 | 15.68 87 | 9 88 | $TEXTSTYLE 89 | 7 90 | STANDARD 91 | 9 92 | $CLAYER 93 | 8 94 | logo 95 | 9 96 | $CELTYPE 97 | 6 98 | BYLAYER 99 | 9 100 | $CECOLOR 101 | 62 102 | 256 103 | 9 104 | $CELTSCALE 105 | 40 106 | 1 107 | 9 108 | $DISPSILH 109 | 70 110 | 0 111 | 9 112 | $DIMSCALE 113 | 40 114 | 2.5 115 | 9 116 | $DIMASZ 117 | 40 118 | 2.5 119 | 9 120 | $DIMEXO 121 | 40 122 | 0.625 123 | 9 124 | $DIMDLI 125 | 40 126 | 3.75 127 | 9 128 | $DIMRND 129 | 40 130 | 0 131 | 9 132 | $DIMDLE 133 | 40 134 | 0 135 | 9 136 | $DIMEXE 137 | 40 138 | 1.25 139 | 9 140 | $DIMTP 141 | 40 142 | 0 143 | 9 144 | $DIMTM 145 | 40 146 | 0 147 | 9 148 | $DIMTXT 149 | 40 150 | 2.5 151 | 9 152 | $DIMCEN 153 | 40 154 | 2.5 155 | 9 156 | $DIMTSZ 157 | 40 158 | 0 159 | 9 160 | $DIMTOL 161 | 70 162 | 0 163 | 9 164 | $DIMLIM 165 | 70 166 | 0 167 | 9 168 | $DIMTIH 169 | 70 170 | 0 171 | 9 172 | $DIMTOH 173 | 70 174 | 0 175 | 9 176 | $DIMSE1 177 | 70 178 | 0 179 | 9 180 | $DIMSE2 181 | 70 182 | 0 183 | 9 184 | $DIMTAD 185 | 70 186 | 1 187 | 9 188 | $DIMZIN 189 | 70 190 | 8 191 | 9 192 | $DIMBLK 193 | 1 194 | 195 | 9 196 | $DIMASO 197 | 70 198 | 1 199 | 9 200 | $DIMSHO 201 | 70 202 | 1 203 | 9 204 | $DIMPOST 205 | 1 206 | 207 | 9 208 | $DIMAPOST 209 | 1 210 | 211 | 9 212 | $DIMALT 213 | 70 214 | 0 215 | 9 216 | $DIMALTD 217 | 70 218 | 3 219 | 9 220 | $DIMALTF 221 | 40 222 | 0.03937 223 | 9 224 | $DIMLFAC 225 | 40 226 | 1 227 | 9 228 | $DIMTOFL 229 | 70 230 | 1 231 | 9 232 | $DIMTVP 233 | 40 234 | 0 235 | 9 236 | $DIMTIX 237 | 70 238 | 0 239 | 9 240 | $DIMSOXD 241 | 70 242 | 0 243 | 9 244 | $DIMSAH 245 | 70 246 | 0 247 | 9 248 | $DIMBLK1 249 | 1 250 | 251 | 9 252 | $DIMBLK2 253 | 1 254 | 255 | 9 256 | $DIMSTYLE 257 | 2 258 | STANDARD 259 | 9 260 | $DIMCLRD 261 | 70 262 | 0 263 | 9 264 | $DIMCLRE 265 | 70 266 | 0 267 | 9 268 | $DIMCLRT 269 | 70 270 | 0 271 | 9 272 | $DIMTFAC 273 | 40 274 | 1 275 | 9 276 | $DIMGAP 277 | 40 278 | 0.625 279 | 9 280 | $DIMJUST 281 | 70 282 | 0 283 | 9 284 | $DIMSD1 285 | 70 286 | 0 287 | 9 288 | $DIMSD2 289 | 70 290 | 0 291 | 9 292 | $DIMTOLJ 293 | 70 294 | 0 295 | 9 296 | $DIMTZIN 297 | 70 298 | 8 299 | 9 300 | $DIMALTZ 301 | 70 302 | 0 303 | 9 304 | $DIMALTTZ 305 | 70 306 | 0 307 | 9 308 | $DIMUPT 309 | 70 310 | 0 311 | 9 312 | $DIMDEC 313 | 70 314 | 2 315 | 9 316 | $DIMTDEC 317 | 70 318 | 2 319 | 9 320 | $DIMALTU 321 | 70 322 | 2 323 | 9 324 | $DIMALTTD 325 | 70 326 | 3 327 | 9 328 | $DIMTXSTY 329 | 7 330 | STANDARD 331 | 9 332 | $DIMAUNIT 333 | 70 334 | 0 335 | 9 336 | $DIMADEC 337 | 70 338 | 0 339 | 9 340 | $DIMALTRND 341 | 40 342 | 0 343 | 9 344 | $DIMAZIN 345 | 70 346 | 0 347 | 9 348 | $DIMDSEP 349 | 70 350 | 44 351 | 9 352 | $DIMATFIT 353 | 70 354 | 3 355 | 9 356 | $DIMFRAC 357 | 70 358 | 0 359 | 9 360 | $DIMLDRBLK 361 | 1 362 | STANDARD 363 | 9 364 | $DIMLUNIT 365 | 70 366 | 2 367 | 9 368 | $DIMLWD 369 | 70 370 | -2 371 | 9 372 | $DIMLWE 373 | 70 374 | -2 375 | 9 376 | $DIMTMOVE 377 | 70 378 | 0 379 | 9 380 | $DIMFXL 381 | 40 382 | 1 383 | 9 384 | $DIMFXLON 385 | 70 386 | 0 387 | 9 388 | $DIMJOGANG 389 | 40 390 | 0.7854 391 | 9 392 | $DIMTFILL 393 | 70 394 | 0 395 | 9 396 | $DIMTFILLCLR 397 | 70 398 | 0 399 | 9 400 | $DIMARCSYM 401 | 70 402 | 0 403 | 9 404 | $DIMLTYPE 405 | 6 406 | 407 | 9 408 | $DIMLTEX1 409 | 6 410 | 411 | 9 412 | $DIMLTEX2 413 | 6 414 | 415 | 9 416 | $LUNITS 417 | 70 418 | 2 419 | 9 420 | $LUPREC 421 | 70 422 | 4 423 | 9 424 | $SKETCHINC 425 | 40 426 | 1 427 | 9 428 | $FILLETRAD 429 | 40 430 | 0 431 | 9 432 | $AUNITS 433 | 70 434 | 0 435 | 9 436 | $AUPREC 437 | 70 438 | 2 439 | 9 440 | $MENU 441 | 1 442 | . 443 | 9 444 | $ELEVATION 445 | 40 446 | 0 447 | 9 448 | $PELEVATION 449 | 40 450 | 0 451 | 9 452 | $THICKNESS 453 | 40 454 | 0 455 | 9 456 | $LIMCHECK 457 | 70 458 | 0 459 | 9 460 | $CHAMFERA 461 | 40 462 | 0 463 | 9 464 | $CHAMFERB 465 | 40 466 | 0 467 | 9 468 | $CHAMFERC 469 | 40 470 | 0 471 | 9 472 | $CHAMFERD 473 | 40 474 | 0 475 | 9 476 | $SKPOLY 477 | 70 478 | 0 479 | 9 480 | $USRTIMER 481 | 70 482 | 1 483 | 9 484 | $ANGBASE 485 | 50 486 | 0 487 | 9 488 | $ANGDIR 489 | 70 490 | 0 491 | 9 492 | $PDMODE 493 | 70 494 | 34 495 | 9 496 | $PDSIZE 497 | 40 498 | 0 499 | 9 500 | $PLINEWID 501 | 40 502 | 0 503 | 9 504 | $SPLFRAME 505 | 70 506 | 0 507 | 9 508 | $SPLINETYPE 509 | 70 510 | 2 511 | 9 512 | $SPLINESEGS 513 | 70 514 | 8 515 | 9 516 | $HANDSEED 517 | 5 518 | 20000 519 | 9 520 | $SURFTAB1 521 | 70 522 | 6 523 | 9 524 | $SURFTAB2 525 | 70 526 | 6 527 | 9 528 | $SURFTYPE 529 | 70 530 | 6 531 | 9 532 | $SURFU 533 | 70 534 | 6 535 | 9 536 | $SURFV 537 | 70 538 | 6 539 | 9 540 | $UCSBASE 541 | 2 542 | 543 | 9 544 | $UCSNAME 545 | 2 546 | 547 | 9 548 | $UCSORG 549 | 10 550 | 0 551 | 20 552 | 0 553 | 30 554 | 0 555 | 9 556 | $UCSXDIR 557 | 10 558 | 1 559 | 20 560 | 0 561 | 30 562 | 0 563 | 9 564 | $UCSYDIR 565 | 10 566 | 0 567 | 20 568 | 1 569 | 30 570 | 0 571 | 9 572 | $UCSORTHOREF 573 | 2 574 | 575 | 9 576 | $UCSORTHOVIEW 577 | 70 578 | 0 579 | 9 580 | $UCSORGTOP 581 | 10 582 | 0 583 | 20 584 | 0 585 | 30 586 | 0 587 | 9 588 | $UCSORGBOTTOM 589 | 10 590 | 0 591 | 20 592 | 0 593 | 30 594 | 0 595 | 9 596 | $UCSORGLEFT 597 | 10 598 | 0 599 | 20 600 | 0 601 | 30 602 | 0 603 | 9 604 | $UCSORGRIGHT 605 | 10 606 | 0 607 | 20 608 | 0 609 | 30 610 | 0 611 | 9 612 | $UCSORGFRONT 613 | 10 614 | 0 615 | 20 616 | 0 617 | 30 618 | 0 619 | 9 620 | $UCSORGBACK 621 | 10 622 | 0 623 | 20 624 | 0 625 | 30 626 | 0 627 | 9 628 | $PUCSBASE 629 | 2 630 | 631 | 9 632 | $PUCSNAME 633 | 2 634 | 635 | 9 636 | $PUCSORG 637 | 10 638 | 0 639 | 20 640 | 0 641 | 30 642 | 0 643 | 9 644 | $PUCSXDIR 645 | 10 646 | 1 647 | 20 648 | 0 649 | 30 650 | 0 651 | 9 652 | $PUCSYDIR 653 | 10 654 | 0 655 | 20 656 | 1 657 | 30 658 | 0 659 | 9 660 | $PUCSORTHOREF 661 | 2 662 | 663 | 9 664 | $PUCSORTHOVIEW 665 | 70 666 | 0 667 | 9 668 | $PUCSORGTOP 669 | 10 670 | 0 671 | 20 672 | 0 673 | 30 674 | 0 675 | 9 676 | $PUCSORGBOTTOM 677 | 10 678 | 0 679 | 20 680 | 0 681 | 30 682 | 0 683 | 9 684 | $PUCSORGLEFT 685 | 10 686 | 0 687 | 20 688 | 0 689 | 30 690 | 0 691 | 9 692 | $PUCSORGRIGHT 693 | 10 694 | 0 695 | 20 696 | 0 697 | 30 698 | 0 699 | 9 700 | $PUCSORGFRONT 701 | 10 702 | 0 703 | 20 704 | 0 705 | 30 706 | 0 707 | 9 708 | $PUCSORGBACK 709 | 10 710 | 0 711 | 20 712 | 0 713 | 30 714 | 0 715 | 9 716 | $USERI1 717 | 70 718 | 0 719 | 9 720 | $USERI2 721 | 70 722 | 0 723 | 9 724 | $USERI3 725 | 70 726 | 0 727 | 9 728 | $USERI4 729 | 70 730 | 0 731 | 9 732 | $USERI5 733 | 70 734 | 0 735 | 9 736 | $USERR1 737 | 40 738 | 0 739 | 9 740 | $USERR2 741 | 40 742 | 0 743 | 9 744 | $USERR3 745 | 40 746 | 0 747 | 9 748 | $USERR4 749 | 40 750 | 0 751 | 9 752 | $USERR5 753 | 40 754 | 0 755 | 9 756 | $WORLDVIEW 757 | 70 758 | 1 759 | 9 760 | $SHADEDGE 761 | 70 762 | 3 763 | 9 764 | $SHADEDIF 765 | 70 766 | 70 767 | 9 768 | $TILEMODE 769 | 70 770 | 1 771 | 9 772 | $MAXACTVP 773 | 70 774 | 64 775 | 9 776 | $PINSBASE 777 | 10 778 | 0 779 | 20 780 | 0 781 | 30 782 | 0 783 | 9 784 | $PLIMCHECK 785 | 70 786 | 0 787 | 9 788 | $PEXTMIN 789 | 10 790 | 0 791 | 20 792 | 0 793 | 30 794 | 0 795 | 9 796 | $PEXTMAX 797 | 10 798 | 0 799 | 20 800 | 0 801 | 30 802 | 0 803 | 9 804 | $GRIDMODE 805 | 70 806 | 1 807 | 9 808 | $SNAPSTYLE 809 | 70 810 | 0 811 | 9 812 | $PLIMMIN 813 | 10 814 | 0 815 | 20 816 | 0 817 | 9 818 | $PLIMMAX 819 | 10 820 | 210 821 | 20 822 | 297 823 | 9 824 | $UNITMODE 825 | 70 826 | 0 827 | 9 828 | $VISRETAIN 829 | 70 830 | 1 831 | 9 832 | $PLINEGEN 833 | 70 834 | 0 835 | 9 836 | $PSLTSCALE 837 | 70 838 | 1 839 | 9 840 | $TREEDEPTH 841 | 70 842 | 3020 843 | 9 844 | $CMLSTYLE 845 | 2 846 | Standard 847 | 9 848 | $CMLJUST 849 | 70 850 | 0 851 | 9 852 | $CMLSCALE 853 | 40 854 | 20 855 | 9 856 | $PROXYGRAPHICS 857 | 70 858 | 1 859 | 9 860 | $MEASUREMENT 861 | 70 862 | 1 863 | 9 864 | $CELWEIGHT 865 | 370 866 | -1 867 | 9 868 | $ENDCAPS 869 | 280 870 | 0 871 | 9 872 | $JOINSTYLE 873 | 280 874 | 0 875 | 9 876 | $LWDISPLAY 877 | 290 878 | 0 879 | 9 880 | $INSUNITS 881 | 70 882 | 4 883 | 9 884 | $HYPERLINKBASE 885 | 1 886 | 887 | 9 888 | $STYLESHEET 889 | 1 890 | 891 | 9 892 | $XEDIT 893 | 290 894 | 1 895 | 9 896 | $CEPSNTYPE 897 | 380 898 | 0 899 | 9 900 | $PSTYLEMODE 901 | 290 902 | 1 903 | 9 904 | $EXTNAMES 905 | 290 906 | 1 907 | 9 908 | $PSVPSCALE 909 | 40 910 | 1 911 | 9 912 | $OLESTARTUP 913 | 290 914 | 0 915 | 9 916 | $SORTENTS 917 | 280 918 | 127 919 | 9 920 | $INDEXCTL 921 | 280 922 | 0 923 | 9 924 | $HIDETEXT 925 | 280 926 | 1 927 | 9 928 | $XCLIPFRAME 929 | 290 930 | 0 931 | 9 932 | $HALOGAP 933 | 280 934 | 0 935 | 9 936 | $OBSCOLOR 937 | 70 938 | 257 939 | 9 940 | $OBSLTYPE 941 | 280 942 | 0 943 | 9 944 | $INTERSECTIONDISPLAY 945 | 280 946 | 0 947 | 9 948 | $INTERSECTIONCOLOR 949 | 70 950 | 257 951 | 9 952 | $DIMASSOC 953 | 280 954 | 1 955 | 9 956 | $PROJECTNAME 957 | 1 958 | 959 | 9 960 | $CAMERADISPLAY 961 | 290 962 | 0 963 | 9 964 | $LENSLENGTH 965 | 40 966 | 50 967 | 9 968 | $CAMERAHEIGHT 969 | 40 970 | 0 971 | 9 972 | $STEPSPERSEC 973 | 40 974 | 2 975 | 9 976 | $STEPSIZE 977 | 40 978 | 50 979 | 9 980 | $3DDWFPREC 981 | 40 982 | 2 983 | 9 984 | $PSOLWIDTH 985 | 40 986 | 5 987 | 9 988 | $PSOLHEIGHT 989 | 40 990 | 80 991 | 9 992 | $LOFTANG1 993 | 40 994 | 1.570796326794897 995 | 9 996 | $LOFTANG2 997 | 40 998 | 1.570796326794897 999 | 9 1000 | $LOFTMAG1 1001 | 40 1002 | 0 1003 | 9 1004 | $LOFTMAG2 1005 | 40 1006 | 0 1007 | 9 1008 | $LOFTPARAM 1009 | 70 1010 | 7 1011 | 9 1012 | $LOFTNORMALS 1013 | 280 1014 | 1 1015 | 9 1016 | $LATITUDE 1017 | 40 1018 | 1 1019 | 9 1020 | $LONGITUDE 1021 | 40 1022 | 1 1023 | 9 1024 | $NORTHDIRECTION 1025 | 40 1026 | 0 1027 | 9 1028 | $TIMEZONE 1029 | 70 1030 | -8000 1031 | 9 1032 | $LIGHTGLYPHDISPLAY 1033 | 280 1034 | 1 1035 | 9 1036 | $TILEMODELIGHTSYNCH 1037 | 280 1038 | 1 1039 | 9 1040 | $SOLIDHIST 1041 | 280 1042 | 1 1043 | 9 1044 | $SHOWHIST 1045 | 280 1046 | 1 1047 | 9 1048 | $DWFFRAME 1049 | 280 1050 | 2 1051 | 9 1052 | $DGNFRAME 1053 | 280 1054 | 0 1055 | 9 1056 | $REALWORLDSCALE 1057 | 290 1058 | 1 1059 | 9 1060 | $INTERFERECOLOR 1061 | 62 1062 | 1 1063 | 9 1064 | $CSHADOW 1065 | 280 1066 | 0 1067 | 9 1068 | $SHADOWPLANELOCATION 1069 | 40 1070 | 0 1071 | 0 1072 | ENDSEC 1073 | 0 1074 | SECTION 1075 | 2 1076 | CLASSES 1077 | 0 1078 | ENDSEC 1079 | 0 1080 | SECTION 1081 | 2 1082 | TABLES 1083 | 0 1084 | TABLE 1085 | 2 1086 | VPORT 1087 | 5 1088 | 8 1089 | 330 1090 | 0 1091 | 100 1092 | AcDbSymbolTable 1093 | 70 1094 | 1 1095 | 0 1096 | VPORT 1097 | 5 1098 | 31 1099 | 330 1100 | 2 1101 | 100 1102 | AcDbSymbolTableRecord 1103 | 100 1104 | AcDbViewportTableRecord 1105 | 2 1106 | *ACTIVE 1107 | 70 1108 | 0 1109 | 10 1110 | 0 1111 | 20 1112 | 0 1113 | 11 1114 | 1 1115 | 21 1116 | 1 1117 | 12 1118 | 20.08011984195051 1119 | 22 1120 | 12.91606467600126 1121 | 13 1122 | 0 1123 | 23 1124 | 0 1125 | 14 1126 | 10 1127 | 24 1128 | 10 1129 | 15 1130 | 10 1131 | 25 1132 | 10 1133 | 16 1134 | 0 1135 | 26 1136 | 0 1137 | 36 1138 | 1 1139 | 17 1140 | 0 1141 | 27 1142 | 0 1143 | 37 1144 | 0 1145 | 40 1146 | 33.47378819568172 1147 | 41 1148 | 1.91439205955335 1149 | 42 1150 | 50 1151 | 43 1152 | 0 1153 | 44 1154 | 0 1155 | 50 1156 | 0 1157 | 51 1158 | 0 1159 | 71 1160 | 0 1161 | 72 1162 | 100 1163 | 73 1164 | 1 1165 | 74 1166 | 3 1167 | 75 1168 | 0 1169 | 76 1170 | 1 1171 | 77 1172 | 0 1173 | 78 1174 | 0 1175 | 281 1176 | 0 1177 | 65 1178 | 1 1179 | 110 1180 | 0 1181 | 120 1182 | 0 1183 | 130 1184 | 0 1185 | 111 1186 | 1 1187 | 121 1188 | 0 1189 | 131 1190 | 0 1191 | 112 1192 | 0 1193 | 122 1194 | 1 1195 | 132 1196 | 0 1197 | 79 1198 | 0 1199 | 146 1200 | 0 1201 | 348 1202 | 10020 1203 | 60 1204 | 7 1205 | 61 1206 | 5 1207 | 292 1208 | 1 1209 | 282 1210 | 1 1211 | 141 1212 | 0 1213 | 142 1214 | 0 1215 | 63 1216 | 250 1217 | 421 1218 | 3358443 1219 | 0 1220 | ENDTAB 1221 | 0 1222 | TABLE 1223 | 2 1224 | LTYPE 1225 | 5 1226 | 5 1227 | 330 1228 | 0 1229 | 100 1230 | AcDbSymbolTable 1231 | 70 1232 | 4 1233 | 0 1234 | LTYPE 1235 | 5 1236 | 14 1237 | 330 1238 | 5 1239 | 100 1240 | AcDbSymbolTableRecord 1241 | 100 1242 | AcDbLinetypeTableRecord 1243 | 2 1244 | ByBlock 1245 | 70 1246 | 0 1247 | 3 1248 | 1249 | 72 1250 | 65 1251 | 73 1252 | 0 1253 | 40 1254 | 0 1255 | 0 1256 | LTYPE 1257 | 5 1258 | 15 1259 | 330 1260 | 5 1261 | 100 1262 | AcDbSymbolTableRecord 1263 | 100 1264 | AcDbLinetypeTableRecord 1265 | 2 1266 | ByLayer 1267 | 70 1268 | 0 1269 | 3 1270 | 1271 | 72 1272 | 65 1273 | 73 1274 | 0 1275 | 40 1276 | 0 1277 | 0 1278 | LTYPE 1279 | 5 1280 | 16 1281 | 330 1282 | 5 1283 | 100 1284 | AcDbSymbolTableRecord 1285 | 100 1286 | AcDbLinetypeTableRecord 1287 | 2 1288 | Continuous 1289 | 70 1290 | 0 1291 | 3 1292 | Solid line 1293 | 72 1294 | 65 1295 | 73 1296 | 0 1297 | 40 1298 | 0 1299 | 0 1300 | LTYPE 1301 | 5 1302 | 32 1303 | 330 1304 | 5 1305 | 100 1306 | AcDbSymbolTableRecord 1307 | 100 1308 | AcDbLinetypeTableRecord 1309 | 2 1310 | DOT 1311 | 70 1312 | 0 1313 | 3 1314 | Dot . . . . . . . . . . . . . . . . . . . . . . 1315 | 72 1316 | 65 1317 | 73 1318 | 2 1319 | 40 1320 | 6.35 1321 | 49 1322 | 0 1323 | 74 1324 | 0 1325 | 49 1326 | -6.35 1327 | 74 1328 | 0 1329 | 0 1330 | LTYPE 1331 | 5 1332 | 33 1333 | 330 1334 | 5 1335 | 100 1336 | AcDbSymbolTableRecord 1337 | 100 1338 | AcDbLinetypeTableRecord 1339 | 2 1340 | DOTTINY 1341 | 70 1342 | 0 1343 | 3 1344 | Dot (.15x) ..................................... 1345 | 72 1346 | 65 1347 | 73 1348 | 2 1349 | 40 1350 | 0.9525 1351 | 49 1352 | 0 1353 | 74 1354 | 0 1355 | 49 1356 | -0.9525 1357 | 74 1358 | 0 1359 | 0 1360 | LTYPE 1361 | 5 1362 | 34 1363 | 330 1364 | 5 1365 | 100 1366 | AcDbSymbolTableRecord 1367 | 100 1368 | AcDbLinetypeTableRecord 1369 | 2 1370 | DOT2 1371 | 70 1372 | 0 1373 | 3 1374 | Dot (.5x) ..................................... 1375 | 72 1376 | 65 1377 | 73 1378 | 2 1379 | 40 1380 | 3.175 1381 | 49 1382 | 0 1383 | 74 1384 | 0 1385 | 49 1386 | -3.175 1387 | 74 1388 | 0 1389 | 0 1390 | LTYPE 1391 | 5 1392 | 35 1393 | 330 1394 | 5 1395 | 100 1396 | AcDbSymbolTableRecord 1397 | 100 1398 | AcDbLinetypeTableRecord 1399 | 2 1400 | DOTX2 1401 | 70 1402 | 0 1403 | 3 1404 | Dot (2x) . . . . . . . . . . . . . 1405 | 72 1406 | 65 1407 | 73 1408 | 2 1409 | 40 1410 | 12.7 1411 | 49 1412 | 0 1413 | 74 1414 | 0 1415 | 49 1416 | -12.7 1417 | 74 1418 | 0 1419 | 0 1420 | LTYPE 1421 | 5 1422 | 36 1423 | 330 1424 | 5 1425 | 100 1426 | AcDbSymbolTableRecord 1427 | 100 1428 | AcDbLinetypeTableRecord 1429 | 2 1430 | DASHED 1431 | 70 1432 | 0 1433 | 3 1434 | Dashed _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 1435 | 72 1436 | 65 1437 | 73 1438 | 2 1439 | 40 1440 | 19.05 1441 | 49 1442 | 12.7 1443 | 74 1444 | 0 1445 | 49 1446 | -6.35 1447 | 74 1448 | 0 1449 | 0 1450 | LTYPE 1451 | 5 1452 | 37 1453 | 330 1454 | 5 1455 | 100 1456 | AcDbSymbolTableRecord 1457 | 100 1458 | AcDbLinetypeTableRecord 1459 | 2 1460 | DASHEDTINY 1461 | 70 1462 | 0 1463 | 3 1464 | Dashed (.15x) _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 1465 | 72 1466 | 65 1467 | 73 1468 | 2 1469 | 40 1470 | 2.8575 1471 | 49 1472 | 1.905 1473 | 74 1474 | 0 1475 | 49 1476 | -0.9525 1477 | 74 1478 | 0 1479 | 0 1480 | LTYPE 1481 | 5 1482 | 38 1483 | 330 1484 | 5 1485 | 100 1486 | AcDbSymbolTableRecord 1487 | 100 1488 | AcDbLinetypeTableRecord 1489 | 2 1490 | DASHED2 1491 | 70 1492 | 0 1493 | 3 1494 | Dashed (.5x) _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 1495 | 72 1496 | 65 1497 | 73 1498 | 2 1499 | 40 1500 | 9.524999999999999 1501 | 49 1502 | 6.35 1503 | 74 1504 | 0 1505 | 49 1506 | -3.175 1507 | 74 1508 | 0 1509 | 0 1510 | LTYPE 1511 | 5 1512 | 39 1513 | 330 1514 | 5 1515 | 100 1516 | AcDbSymbolTableRecord 1517 | 100 1518 | AcDbLinetypeTableRecord 1519 | 2 1520 | DASHEDX2 1521 | 70 1522 | 0 1523 | 3 1524 | Dashed (2x) ____ ____ ____ ____ ____ ___ 1525 | 72 1526 | 65 1527 | 73 1528 | 2 1529 | 40 1530 | 38.09999999999999 1531 | 49 1532 | 25.4 1533 | 74 1534 | 0 1535 | 49 1536 | -12.7 1537 | 74 1538 | 0 1539 | 0 1540 | LTYPE 1541 | 5 1542 | 3A 1543 | 330 1544 | 5 1545 | 100 1546 | AcDbSymbolTableRecord 1547 | 100 1548 | AcDbLinetypeTableRecord 1549 | 2 1550 | DASHDOT 1551 | 70 1552 | 0 1553 | 3 1554 | Dash dot __ . __ . __ . __ . __ . __ . __ . __ 1555 | 72 1556 | 65 1557 | 73 1558 | 4 1559 | 40 1560 | 25.4 1561 | 49 1562 | 12.7 1563 | 74 1564 | 0 1565 | 49 1566 | -6.35 1567 | 74 1568 | 0 1569 | 49 1570 | 0 1571 | 74 1572 | 0 1573 | 49 1574 | -6.35 1575 | 74 1576 | 0 1577 | 0 1578 | LTYPE 1579 | 5 1580 | 3B 1581 | 330 1582 | 5 1583 | 100 1584 | AcDbSymbolTableRecord 1585 | 100 1586 | AcDbLinetypeTableRecord 1587 | 2 1588 | DASHDOTTINY 1589 | 70 1590 | 0 1591 | 3 1592 | Dash dot (.15x) _._._._._._._._._._._._._._._. 1593 | 72 1594 | 65 1595 | 73 1596 | 4 1597 | 40 1598 | 3.81 1599 | 49 1600 | 1.905 1601 | 74 1602 | 0 1603 | 49 1604 | -0.9525 1605 | 74 1606 | 0 1607 | 49 1608 | 0 1609 | 74 1610 | 0 1611 | 49 1612 | -0.9525 1613 | 74 1614 | 0 1615 | 0 1616 | LTYPE 1617 | 5 1618 | 3C 1619 | 330 1620 | 5 1621 | 100 1622 | AcDbSymbolTableRecord 1623 | 100 1624 | AcDbLinetypeTableRecord 1625 | 2 1626 | DASHDOT2 1627 | 70 1628 | 0 1629 | 3 1630 | Dash dot (.5x) _._._._._._._._._._._._._._._. 1631 | 72 1632 | 65 1633 | 73 1634 | 4 1635 | 40 1636 | 12.7 1637 | 49 1638 | 6.35 1639 | 74 1640 | 0 1641 | 49 1642 | -3.175 1643 | 74 1644 | 0 1645 | 49 1646 | 0 1647 | 74 1648 | 0 1649 | 49 1650 | -3.175 1651 | 74 1652 | 0 1653 | 0 1654 | LTYPE 1655 | 5 1656 | 3D 1657 | 330 1658 | 5 1659 | 100 1660 | AcDbSymbolTableRecord 1661 | 100 1662 | AcDbLinetypeTableRecord 1663 | 2 1664 | DASHDOTX2 1665 | 70 1666 | 0 1667 | 3 1668 | Dash dot (2x) ____ . ____ . ____ . ___ 1669 | 72 1670 | 65 1671 | 73 1672 | 4 1673 | 40 1674 | 50.8 1675 | 49 1676 | 25.4 1677 | 74 1678 | 0 1679 | 49 1680 | -12.7 1681 | 74 1682 | 0 1683 | 49 1684 | 0 1685 | 74 1686 | 0 1687 | 49 1688 | -12.7 1689 | 74 1690 | 0 1691 | 0 1692 | LTYPE 1693 | 5 1694 | 3E 1695 | 330 1696 | 5 1697 | 100 1698 | AcDbSymbolTableRecord 1699 | 100 1700 | AcDbLinetypeTableRecord 1701 | 2 1702 | DIVIDE 1703 | 70 1704 | 0 1705 | 3 1706 | Divide ____ . . ____ . . ____ . . ____ . . ____ 1707 | 72 1708 | 65 1709 | 73 1710 | 6 1711 | 40 1712 | 31.75 1713 | 49 1714 | 12.7 1715 | 74 1716 | 0 1717 | 49 1718 | -6.35 1719 | 74 1720 | 0 1721 | 49 1722 | 0 1723 | 74 1724 | 0 1725 | 49 1726 | -6.35 1727 | 74 1728 | 0 1729 | 49 1730 | 0 1731 | 74 1732 | 0 1733 | 49 1734 | -6.35 1735 | 74 1736 | 0 1737 | 0 1738 | LTYPE 1739 | 5 1740 | 3F 1741 | 330 1742 | 5 1743 | 100 1744 | AcDbSymbolTableRecord 1745 | 100 1746 | AcDbLinetypeTableRecord 1747 | 2 1748 | DIVIDETINY 1749 | 70 1750 | 0 1751 | 3 1752 | Divide (.15x) __..__..__..__..__..__..__..__.._ 1753 | 72 1754 | 65 1755 | 73 1756 | 6 1757 | 40 1758 | 4.7625 1759 | 49 1760 | 1.905 1761 | 74 1762 | 0 1763 | 49 1764 | -0.9525 1765 | 74 1766 | 0 1767 | 49 1768 | 0 1769 | 74 1770 | 0 1771 | 49 1772 | -0.9525 1773 | 74 1774 | 0 1775 | 49 1776 | 0 1777 | 74 1778 | 0 1779 | 49 1780 | -0.9525 1781 | 74 1782 | 0 1783 | 0 1784 | LTYPE 1785 | 5 1786 | 40 1787 | 330 1788 | 5 1789 | 100 1790 | AcDbSymbolTableRecord 1791 | 100 1792 | AcDbLinetypeTableRecord 1793 | 2 1794 | DIVIDE2 1795 | 70 1796 | 0 1797 | 3 1798 | Divide (.5x) __..__..__..__..__..__..__..__.._ 1799 | 72 1800 | 65 1801 | 73 1802 | 6 1803 | 40 1804 | 15.875 1805 | 49 1806 | 6.35 1807 | 74 1808 | 0 1809 | 49 1810 | -3.175 1811 | 74 1812 | 0 1813 | 49 1814 | 0 1815 | 74 1816 | 0 1817 | 49 1818 | -3.175 1819 | 74 1820 | 0 1821 | 49 1822 | 0 1823 | 74 1824 | 0 1825 | 49 1826 | -3.175 1827 | 74 1828 | 0 1829 | 0 1830 | LTYPE 1831 | 5 1832 | 41 1833 | 330 1834 | 5 1835 | 100 1836 | AcDbSymbolTableRecord 1837 | 100 1838 | AcDbLinetypeTableRecord 1839 | 2 1840 | DIVIDEX2 1841 | 70 1842 | 0 1843 | 3 1844 | Divide (2x) ________ . . ________ . . _ 1845 | 72 1846 | 65 1847 | 73 1848 | 6 1849 | 40 1850 | 63.5 1851 | 49 1852 | 25.4 1853 | 74 1854 | 0 1855 | 49 1856 | -12.7 1857 | 74 1858 | 0 1859 | 49 1860 | 0 1861 | 74 1862 | 0 1863 | 49 1864 | -12.7 1865 | 74 1866 | 0 1867 | 49 1868 | 0 1869 | 74 1870 | 0 1871 | 49 1872 | -12.7 1873 | 74 1874 | 0 1875 | 0 1876 | LTYPE 1877 | 5 1878 | 42 1879 | 330 1880 | 5 1881 | 100 1882 | AcDbSymbolTableRecord 1883 | 100 1884 | AcDbLinetypeTableRecord 1885 | 2 1886 | BORDER 1887 | 70 1888 | 0 1889 | 3 1890 | Border __ __ . __ __ . __ __ . __ __ . __ __ . 1891 | 72 1892 | 65 1893 | 73 1894 | 6 1895 | 40 1896 | 44.45 1897 | 49 1898 | 12.7 1899 | 74 1900 | 0 1901 | 49 1902 | -6.35 1903 | 74 1904 | 0 1905 | 49 1906 | 12.7 1907 | 74 1908 | 0 1909 | 49 1910 | -6.35 1911 | 74 1912 | 0 1913 | 49 1914 | 0 1915 | 74 1916 | 0 1917 | 49 1918 | -6.35 1919 | 74 1920 | 0 1921 | 0 1922 | LTYPE 1923 | 5 1924 | 43 1925 | 330 1926 | 5 1927 | 100 1928 | AcDbSymbolTableRecord 1929 | 100 1930 | AcDbLinetypeTableRecord 1931 | 2 1932 | BORDERTINY 1933 | 70 1934 | 0 1935 | 3 1936 | Border (.15x) __.__.__.__.__.__.__.__.__.__.__. 1937 | 72 1938 | 65 1939 | 73 1940 | 6 1941 | 40 1942 | 6.6675 1943 | 49 1944 | 1.905 1945 | 74 1946 | 0 1947 | 49 1948 | -0.9525 1949 | 74 1950 | 0 1951 | 49 1952 | 1.905 1953 | 74 1954 | 0 1955 | 49 1956 | -0.9525 1957 | 74 1958 | 0 1959 | 49 1960 | 0 1961 | 74 1962 | 0 1963 | 49 1964 | -0.9525 1965 | 74 1966 | 0 1967 | 0 1968 | LTYPE 1969 | 5 1970 | 44 1971 | 330 1972 | 5 1973 | 100 1974 | AcDbSymbolTableRecord 1975 | 100 1976 | AcDbLinetypeTableRecord 1977 | 2 1978 | BORDER2 1979 | 70 1980 | 0 1981 | 3 1982 | Border (.5x) __.__.__.__.__.__.__.__.__.__.__. 1983 | 72 1984 | 65 1985 | 73 1986 | 6 1987 | 40 1988 | 22.225 1989 | 49 1990 | 6.35 1991 | 74 1992 | 0 1993 | 49 1994 | -3.175 1995 | 74 1996 | 0 1997 | 49 1998 | 6.35 1999 | 74 2000 | 0 2001 | 49 2002 | -3.175 2003 | 74 2004 | 0 2005 | 49 2006 | 0 2007 | 74 2008 | 0 2009 | 49 2010 | -3.175 2011 | 74 2012 | 0 2013 | 0 2014 | LTYPE 2015 | 5 2016 | 45 2017 | 330 2018 | 5 2019 | 100 2020 | AcDbSymbolTableRecord 2021 | 100 2022 | AcDbLinetypeTableRecord 2023 | 2 2024 | BORDERX2 2025 | 70 2026 | 0 2027 | 3 2028 | Border (2x) ____ ____ . ____ ____ . ___ 2029 | 72 2030 | 65 2031 | 73 2032 | 6 2033 | 40 2034 | 88.89999999999999 2035 | 49 2036 | 25.4 2037 | 74 2038 | 0 2039 | 49 2040 | -12.7 2041 | 74 2042 | 0 2043 | 49 2044 | 25.4 2045 | 74 2046 | 0 2047 | 49 2048 | -12.7 2049 | 74 2050 | 0 2051 | 49 2052 | 0 2053 | 74 2054 | 0 2055 | 49 2056 | -12.7 2057 | 74 2058 | 0 2059 | 0 2060 | LTYPE 2061 | 5 2062 | 46 2063 | 330 2064 | 5 2065 | 100 2066 | AcDbSymbolTableRecord 2067 | 100 2068 | AcDbLinetypeTableRecord 2069 | 2 2070 | CENTER 2071 | 70 2072 | 0 2073 | 3 2074 | Center ____ _ ____ _ ____ _ ____ _ ____ _ ____ 2075 | 72 2076 | 65 2077 | 73 2078 | 4 2079 | 40 2080 | 50.8 2081 | 49 2082 | 31.75 2083 | 74 2084 | 0 2085 | 49 2086 | -6.35 2087 | 74 2088 | 0 2089 | 49 2090 | 6.35 2091 | 74 2092 | 0 2093 | 49 2094 | -6.35 2095 | 74 2096 | 0 2097 | 0 2098 | LTYPE 2099 | 5 2100 | 47 2101 | 330 2102 | 5 2103 | 100 2104 | AcDbSymbolTableRecord 2105 | 100 2106 | AcDbLinetypeTableRecord 2107 | 2 2108 | CENTERTINY 2109 | 70 2110 | 0 2111 | 3 2112 | Center (.15x) ___ _ ___ _ ___ _ ___ _ ___ _ ___ 2113 | 72 2114 | 65 2115 | 73 2116 | 4 2117 | 40 2118 | 7.619999999999999 2119 | 49 2120 | 4.7625 2121 | 74 2122 | 0 2123 | 49 2124 | -0.9525 2125 | 74 2126 | 0 2127 | 49 2128 | 0.9525 2129 | 74 2130 | 0 2131 | 49 2132 | -0.9525 2133 | 74 2134 | 0 2135 | 0 2136 | LTYPE 2137 | 5 2138 | 48 2139 | 330 2140 | 5 2141 | 100 2142 | AcDbSymbolTableRecord 2143 | 100 2144 | AcDbLinetypeTableRecord 2145 | 2 2146 | CENTER2 2147 | 70 2148 | 0 2149 | 3 2150 | Center (.5x) ___ _ ___ _ ___ _ ___ _ ___ _ ___ 2151 | 72 2152 | 65 2153 | 73 2154 | 4 2155 | 40 2156 | 28.575 2157 | 49 2158 | 19.05 2159 | 74 2160 | 0 2161 | 49 2162 | -3.175 2163 | 74 2164 | 0 2165 | 49 2166 | 3.175 2167 | 74 2168 | 0 2169 | 49 2170 | -3.175 2171 | 74 2172 | 0 2173 | 0 2174 | LTYPE 2175 | 5 2176 | 49 2177 | 330 2178 | 5 2179 | 100 2180 | AcDbSymbolTableRecord 2181 | 100 2182 | AcDbLinetypeTableRecord 2183 | 2 2184 | CENTERX2 2185 | 70 2186 | 0 2187 | 3 2188 | Center (2x) ________ __ ________ __ _____ 2189 | 72 2190 | 65 2191 | 73 2192 | 4 2193 | 40 2194 | 101.6 2195 | 49 2196 | 63.5 2197 | 74 2198 | 0 2199 | 49 2200 | -12.7 2201 | 74 2202 | 0 2203 | 49 2204 | 12.7 2205 | 74 2206 | 0 2207 | 49 2208 | -12.7 2209 | 74 2210 | 0 2211 | 0 2212 | ENDTAB 2213 | 0 2214 | TABLE 2215 | 2 2216 | LAYER 2217 | 5 2218 | 2 2219 | 330 2220 | 0 2221 | 100 2222 | AcDbSymbolTable 2223 | 70 2224 | 1 2225 | 0 2226 | LAYER 2227 | 5 2228 | 10 2229 | 330 2230 | 2 2231 | 100 2232 | AcDbSymbolTableRecord 2233 | 100 2234 | AcDbLayerTableRecord 2235 | 2 2236 | 0 2237 | 70 2238 | 0 2239 | 62 2240 | 7 2241 | 6 2242 | CONTINUOUS 2243 | 370 2244 | 0 2245 | 390 2246 | F 2247 | 0 2248 | LAYER 2249 | 5 2250 | 4A 2251 | 330 2252 | 2 2253 | 100 2254 | AcDbSymbolTableRecord 2255 | 100 2256 | AcDbLayerTableRecord 2257 | 2 2258 | background 2259 | 70 2260 | 0 2261 | 62 2262 | 8 2263 | 6 2264 | DASHEDTINY 2265 | 370 2266 | 0 2267 | 390 2268 | F 2269 | 0 2270 | LAYER 2271 | 5 2272 | 4B 2273 | 330 2274 | 2 2275 | 100 2276 | AcDbSymbolTableRecord 2277 | 100 2278 | AcDbLayerTableRecord 2279 | 2 2280 | logo 2281 | 70 2282 | 0 2283 | 62 2284 | 7 2285 | 6 2286 | CONTINUOUS 2287 | 370 2288 | 0 2289 | 390 2290 | F 2291 | 0 2292 | ENDTAB 2293 | 0 2294 | TABLE 2295 | 2 2296 | STYLE 2297 | 5 2298 | 3 2299 | 330 2300 | 0 2301 | 100 2302 | AcDbSymbolTable 2303 | 70 2304 | 3 2305 | 0 2306 | STYLE 2307 | 5 2308 | 4C 2309 | 330 2310 | 2 2311 | 100 2312 | AcDbSymbolTableRecord 2313 | 100 2314 | AcDbTextStyleTableRecord 2315 | 2 2316 | Standard 2317 | 70 2318 | 0 2319 | 40 2320 | 0 2321 | 41 2322 | 1 2323 | 50 2324 | 0 2325 | 71 2326 | 0 2327 | 42 2328 | 1 2329 | 3 2330 | txt 2331 | 4 2332 | 2333 | 0 2334 | ENDTAB 2335 | 0 2336 | TABLE 2337 | 2 2338 | VIEW 2339 | 5 2340 | 6 2341 | 330 2342 | 0 2343 | 100 2344 | AcDbSymbolTable 2345 | 70 2346 | 0 2347 | 0 2348 | ENDTAB 2349 | 0 2350 | TABLE 2351 | 2 2352 | UCS 2353 | 5 2354 | 7 2355 | 330 2356 | 0 2357 | 100 2358 | AcDbSymbolTable 2359 | 70 2360 | 0 2361 | 0 2362 | ENDTAB 2363 | 0 2364 | TABLE 2365 | 2 2366 | APPID 2367 | 5 2368 | 9 2369 | 330 2370 | 0 2371 | 100 2372 | AcDbSymbolTable 2373 | 70 2374 | 1 2375 | 0 2376 | APPID 2377 | 5 2378 | 12 2379 | 330 2380 | 9 2381 | 100 2382 | AcDbSymbolTableRecord 2383 | 100 2384 | AcDbRegAppTableRecord 2385 | 2 2386 | ACAD 2387 | 70 2388 | 0 2389 | 0 2390 | APPID 2391 | 5 2392 | 4D 2393 | 330 2394 | 9 2395 | 100 2396 | AcDbSymbolTableRecord 2397 | 100 2398 | AcDbRegAppTableRecord 2399 | 2 2400 | LibreCad 2401 | 70 2402 | 0 2403 | 0 2404 | ENDTAB 2405 | 0 2406 | TABLE 2407 | 2 2408 | DIMSTYLE 2409 | 5 2410 | A 2411 | 330 2412 | 0 2413 | 100 2414 | AcDbSymbolTable 2415 | 70 2416 | 1 2417 | 100 2418 | AcDbDimStyleTable 2419 | 71 2420 | 1 2421 | 0 2422 | DIMSTYLE 2423 | 105 2424 | 4E 2425 | 330 2426 | A 2427 | 100 2428 | AcDbSymbolTableRecord 2429 | 100 2430 | AcDbDimStyleTableRecord 2431 | 2 2432 | Standard 2433 | 70 2434 | 0 2435 | 40 2436 | 2.5 2437 | 41 2438 | 2.5 2439 | 42 2440 | 0.625 2441 | 43 2442 | 0.38 2443 | 44 2444 | 1.25 2445 | 45 2446 | 0 2447 | 46 2448 | 0 2449 | 47 2450 | 0 2451 | 48 2452 | 0 2453 | 49 2454 | 1 2455 | 140 2456 | 2.5 2457 | 141 2458 | 0.09 2459 | 142 2460 | 0 2461 | 143 2462 | 25.4 2463 | 144 2464 | 1 2465 | 145 2466 | 0 2467 | 146 2468 | 1 2469 | 147 2470 | 0.625 2471 | 148 2472 | 0 2473 | 71 2474 | 0 2475 | 72 2476 | 0 2477 | 73 2478 | 0 2479 | 74 2480 | 1 2481 | 75 2482 | 0 2483 | 76 2484 | 0 2485 | 77 2486 | 0 2487 | 78 2488 | 8 2489 | 79 2490 | 0 2491 | 170 2492 | 0 2493 | 171 2494 | 2 2495 | 172 2496 | 0 2497 | 173 2498 | 0 2499 | 174 2500 | 0 2501 | 175 2502 | 0 2503 | 176 2504 | 0 2505 | 177 2506 | 0 2507 | 178 2508 | 0 2509 | 179 2510 | 0 2511 | 271 2512 | 2 2513 | 272 2514 | 4 2515 | 273 2516 | 2 2517 | 274 2518 | 2 2519 | 275 2520 | 0 2521 | 276 2522 | 0 2523 | 277 2524 | 2 2525 | 278 2526 | 44 2527 | 279 2528 | 0 2529 | 280 2530 | 0 2531 | 281 2532 | 0 2533 | 282 2534 | 0 2535 | 283 2536 | 1 2537 | 284 2538 | 0 2539 | 285 2540 | 0 2541 | 286 2542 | 0 2543 | 288 2544 | 0 2545 | 289 2546 | 3 2547 | 340 2548 | STANDARD 2549 | 341 2550 | 2551 | 371 2552 | -2 2553 | 372 2554 | -2 2555 | 0 2556 | ENDTAB 2557 | 0 2558 | TABLE 2559 | 2 2560 | BLOCK_RECORD 2561 | 5 2562 | 1 2563 | 330 2564 | 0 2565 | 100 2566 | AcDbSymbolTable 2567 | 70 2568 | 2 2569 | 0 2570 | BLOCK_RECORD 2571 | 5 2572 | 1F 2573 | 330 2574 | 1 2575 | 100 2576 | AcDbSymbolTableRecord 2577 | 100 2578 | AcDbBlockTableRecord 2579 | 2 2580 | *Model_Space 2581 | 70 2582 | 0 2583 | 280 2584 | 1 2585 | 281 2586 | 0 2587 | 0 2588 | BLOCK_RECORD 2589 | 5 2590 | 1E 2591 | 330 2592 | 1 2593 | 100 2594 | AcDbSymbolTableRecord 2595 | 100 2596 | AcDbBlockTableRecord 2597 | 2 2598 | *Paper_Space 2599 | 70 2600 | 0 2601 | 280 2602 | 1 2603 | 281 2604 | 0 2605 | 0 2606 | ENDTAB 2607 | 0 2608 | ENDSEC 2609 | 0 2610 | SECTION 2611 | 2 2612 | BLOCKS 2613 | 0 2614 | BLOCK 2615 | 5 2616 | 20 2617 | 330 2618 | 1F 2619 | 100 2620 | AcDbEntity 2621 | 8 2622 | 0 2623 | 100 2624 | AcDbBlockBegin 2625 | 2 2626 | *Model_Space 2627 | 70 2628 | 0 2629 | 10 2630 | 0 2631 | 20 2632 | 0 2633 | 30 2634 | 0 2635 | 3 2636 | *Model_Space 2637 | 1 2638 | 2639 | 0 2640 | ENDBLK 2641 | 5 2642 | 21 2643 | 330 2644 | 1F 2645 | 100 2646 | AcDbEntity 2647 | 8 2648 | 0 2649 | 100 2650 | AcDbBlockEnd 2651 | 0 2652 | BLOCK 2653 | 5 2654 | 1C 2655 | 330 2656 | 1B 2657 | 100 2658 | AcDbEntity 2659 | 8 2660 | 0 2661 | 100 2662 | AcDbBlockBegin 2663 | 2 2664 | *Paper_Space 2665 | 70 2666 | 0 2667 | 10 2668 | 0 2669 | 20 2670 | 0 2671 | 30 2672 | 0 2673 | 3 2674 | *Paper_Space 2675 | 1 2676 | 2677 | 0 2678 | ENDBLK 2679 | 5 2680 | 1D 2681 | 330 2682 | 1F 2683 | 100 2684 | AcDbEntity 2685 | 8 2686 | 0 2687 | 100 2688 | AcDbBlockEnd 2689 | 0 2690 | ENDSEC 2691 | 0 2692 | SECTION 2693 | 2 2694 | ENTITIES 2695 | 0 2696 | LWPOLYLINE 2697 | 5 2698 | 4F 2699 | 100 2700 | AcDbEntity 2701 | 8 2702 | background 2703 | 6 2704 | ByLayer 2705 | 62 2706 | 256 2707 | 370 2708 | -1 2709 | 100 2710 | AcDbPolyline 2711 | 90 2712 | 4 2713 | 70 2714 | 1 2715 | 43 2716 | 0 2717 | 10 2718 | -1 2719 | 20 2720 | 17 2721 | 10 2722 | 17 2723 | 20 2724 | 17 2725 | 10 2726 | 17 2727 | 20 2728 | -1 2729 | 10 2730 | -1 2731 | 20 2732 | -1 2733 | 0 2734 | CIRCLE 2735 | 5 2736 | 50 2737 | 100 2738 | AcDbEntity 2739 | 8 2740 | background 2741 | 6 2742 | ByLayer 2743 | 62 2744 | 256 2745 | 370 2746 | -1 2747 | 100 2748 | AcDbCircle 2749 | 10 2750 | -31.57855688082447 2751 | 20 2752 | 58.66681906957261 2753 | 40 2754 | 64 2755 | 0 2756 | CIRCLE 2757 | 5 2758 | 51 2759 | 100 2760 | AcDbEntity 2761 | 8 2762 | background 2763 | 6 2764 | ByLayer 2765 | 62 2766 | 256 2767 | 370 2768 | -1 2769 | 100 2770 | AcDbCircle 2771 | 10 2772 | -12.58816656873632 2773 | 20 2774 | 73.74980527808631 2775 | 40 2776 | 64 2777 | 0 2778 | CIRCLE 2779 | 5 2780 | 52 2781 | 100 2782 | AcDbEntity 2783 | 8 2784 | background 2785 | 6 2786 | ByLayer 2787 | 62 2788 | 256 2789 | 370 2790 | -1 2791 | 100 2792 | AcDbCircle 2793 | 10 2794 | -44.2811699973091 2795 | 20 2796 | 35.45584470461603 2797 | 40 2798 | 64 2799 | 0 2800 | ARC 2801 | 5 2802 | 53 2803 | 100 2804 | AcDbEntity 2805 | 8 2806 | logo 2807 | 6 2808 | ByLayer 2809 | 62 2810 | 256 2811 | 370 2812 | -1 2813 | 100 2814 | AcDbCircle 2815 | 10 2816 | 14 2817 | 20 2818 | 2 2819 | 40 2820 | 2 2821 | 100 2822 | AcDbArc 2823 | 50 2824 | 270 2825 | 51 2826 | 0 2827 | 0 2828 | ARC 2829 | 5 2830 | 54 2831 | 100 2832 | AcDbEntity 2833 | 8 2834 | logo 2835 | 6 2836 | ByLayer 2837 | 62 2838 | 256 2839 | 370 2840 | -1 2841 | 100 2842 | AcDbCircle 2843 | 10 2844 | 14 2845 | 20 2846 | 14 2847 | 40 2848 | 2 2849 | 100 2850 | AcDbArc 2851 | 50 2852 | 0 2853 | 51 2854 | 90 2855 | 0 2856 | ARC 2857 | 5 2858 | 55 2859 | 100 2860 | AcDbEntity 2861 | 8 2862 | logo 2863 | 6 2864 | ByLayer 2865 | 62 2866 | 256 2867 | 370 2868 | -1 2869 | 100 2870 | AcDbCircle 2871 | 10 2872 | 2 2873 | 20 2874 | 14 2875 | 40 2876 | 2 2877 | 100 2878 | AcDbArc 2879 | 50 2880 | 90 2881 | 51 2882 | 180 2883 | 0 2884 | ARC 2885 | 5 2886 | 56 2887 | 100 2888 | AcDbEntity 2889 | 8 2890 | logo 2891 | 6 2892 | ByLayer 2893 | 62 2894 | 256 2895 | 370 2896 | -1 2897 | 100 2898 | AcDbCircle 2899 | 10 2900 | 2 2901 | 20 2902 | 2 2903 | 40 2904 | 2 2905 | 100 2906 | AcDbArc 2907 | 50 2908 | 180 2909 | 51 2910 | 270 2911 | 0 2912 | ARC 2913 | 5 2914 | 57 2915 | 100 2916 | AcDbEntity 2917 | 8 2918 | logo 2919 | 6 2920 | ByLayer 2921 | 62 2922 | 256 2923 | 370 2924 | -1 2925 | 100 2926 | AcDbCircle 2927 | 10 2928 | 8.5 2929 | 20 2930 | 9.5 2931 | 40 2932 | 0.5 2933 | 100 2934 | AcDbArc 2935 | 50 2936 | 270 2937 | 51 2938 | 0 2939 | 0 2940 | ARC 2941 | 5 2942 | 58 2943 | 100 2944 | AcDbEntity 2945 | 8 2946 | logo 2947 | 6 2948 | ByLayer 2949 | 62 2950 | 256 2951 | 370 2952 | -1 2953 | 100 2954 | AcDbCircle 2955 | 10 2956 | 11.5 2957 | 20 2958 | 9.5 2959 | 40 2960 | 0.5 2961 | 100 2962 | AcDbArc 2963 | 50 2964 | 180 2965 | 51 2966 | 270 2967 | 0 2968 | ARC 2969 | 5 2970 | 59 2971 | 100 2972 | AcDbEntity 2973 | 8 2974 | logo 2975 | 6 2976 | ByLayer 2977 | 62 2978 | 256 2979 | 370 2980 | -1 2981 | 100 2982 | AcDbCircle 2983 | 10 2984 | 7.5 2985 | 20 2986 | 6.5 2987 | 40 2988 | 0.5 2989 | 100 2990 | AcDbArc 2991 | 50 2992 | 90 2993 | 51 2994 | 180 2995 | 0 2996 | LINE 2997 | 5 2998 | 5A 2999 | 100 3000 | AcDbEntity 3001 | 8 3002 | logo 3003 | 6 3004 | ByLayer 3005 | 62 3006 | 256 3007 | 370 3008 | -1 3009 | 100 3010 | AcDbLine 3011 | 10 3012 | 7 3013 | 20 3014 | 6.5 3015 | 11 3016 | 7 3017 | 21 3018 | 0.5 3019 | 0 3020 | LINE 3021 | 5 3022 | 5B 3023 | 100 3024 | AcDbEntity 3025 | 8 3026 | logo 3027 | 6 3028 | ByLayer 3029 | 62 3030 | 256 3031 | 370 3032 | -1 3033 | 100 3034 | AcDbLine 3035 | 10 3036 | 7.5 3037 | 20 3038 | 0 3039 | 11 3040 | 14 3041 | 21 3042 | 0 3043 | 0 3044 | ARC 3045 | 5 3046 | 5C 3047 | 100 3048 | AcDbEntity 3049 | 8 3050 | logo 3051 | 6 3052 | ByLayer 3053 | 62 3054 | 256 3055 | 370 3056 | -1 3057 | 100 3058 | AcDbCircle 3059 | 10 3060 | 7.5 3061 | 20 3062 | 0.5 3063 | 40 3064 | 0.5 3065 | 100 3066 | AcDbArc 3067 | 50 3068 | 180 3069 | 51 3070 | 270 3071 | 0 3072 | LINE 3073 | 5 3074 | 5D 3075 | 100 3076 | AcDbEntity 3077 | 8 3078 | logo 3079 | 6 3080 | ByLayer 3081 | 62 3082 | 256 3083 | 370 3084 | -1 3085 | 100 3086 | AcDbLine 3087 | 10 3088 | 2 3089 | 20 3090 | 0 3091 | 11 3092 | 4.5 3093 | 21 3094 | 0 3095 | 0 3096 | ARC 3097 | 5 3098 | 5E 3099 | 100 3100 | AcDbEntity 3101 | 8 3102 | logo 3103 | 6 3104 | ByLayer 3105 | 62 3106 | 256 3107 | 370 3108 | -1 3109 | 100 3110 | AcDbCircle 3111 | 10 3112 | 4.5 3113 | 20 3114 | 0.5 3115 | 40 3116 | 0.5 3117 | 100 3118 | AcDbArc 3119 | 50 3120 | 270 3121 | 51 3122 | 0 3123 | 0 3124 | LINE 3125 | 5 3126 | 5F 3127 | 100 3128 | AcDbEntity 3129 | 8 3130 | logo 3131 | 6 3132 | ByLayer 3133 | 62 3134 | 256 3135 | 370 3136 | -1 3137 | 100 3138 | AcDbLine 3139 | 10 3140 | 5 3141 | 20 3142 | 0.5 3143 | 11 3144 | 5 3145 | 21 3146 | 6.5 3147 | 0 3148 | ARC 3149 | 5 3150 | 60 3151 | 100 3152 | AcDbEntity 3153 | 8 3154 | logo 3155 | 6 3156 | ByLayer 3157 | 62 3158 | 256 3159 | 370 3160 | -1 3161 | 100 3162 | AcDbCircle 3163 | 10 3164 | 4.5 3165 | 20 3166 | 6.5 3167 | 40 3168 | 0.5 3169 | 100 3170 | AcDbArc 3171 | 50 3172 | 0 3173 | 51 3174 | 90 3175 | 0 3176 | LINE 3177 | 5 3178 | 61 3179 | 100 3180 | AcDbEntity 3181 | 8 3182 | logo 3183 | 6 3184 | ByLayer 3185 | 62 3186 | 256 3187 | 370 3188 | -1 3189 | 100 3190 | AcDbLine 3191 | 10 3192 | 0.5 3193 | 20 3194 | 7 3195 | 11 3196 | 4.5 3197 | 21 3198 | 7 3199 | 0 3200 | LINE 3201 | 5 3202 | 62 3203 | 100 3204 | AcDbEntity 3205 | 8 3206 | logo 3207 | 6 3208 | ByLayer 3209 | 62 3210 | 256 3211 | 370 3212 | -1 3213 | 100 3214 | AcDbLine 3215 | 10 3216 | 0 3217 | 20 3218 | 6.5 3219 | 11 3220 | 0 3221 | 21 3222 | 2 3223 | 0 3224 | ARC 3225 | 5 3226 | 63 3227 | 100 3228 | AcDbEntity 3229 | 8 3230 | logo 3231 | 6 3232 | ByLayer 3233 | 62 3234 | 256 3235 | 370 3236 | -1 3237 | 100 3238 | AcDbCircle 3239 | 10 3240 | 0.5 3241 | 20 3242 | 6.5 3243 | 40 3244 | 0.5 3245 | 100 3246 | AcDbArc 3247 | 50 3248 | 90 3249 | 51 3250 | 180 3251 | 0 3252 | LINE 3253 | 5 3254 | 64 3255 | 100 3256 | AcDbEntity 3257 | 8 3258 | logo 3259 | 6 3260 | ByLayer 3261 | 62 3262 | 256 3263 | 370 3264 | -1 3265 | 100 3266 | AcDbLine 3267 | 10 3268 | 0.5 3269 | 20 3270 | 9 3271 | 11 3272 | 8.5 3273 | 21 3274 | 9 3275 | 0 3276 | LINE 3277 | 5 3278 | 65 3279 | 100 3280 | AcDbEntity 3281 | 8 3282 | logo 3283 | 6 3284 | ByLayer 3285 | 62 3286 | 256 3287 | 370 3288 | -1 3289 | 100 3290 | AcDbLine 3291 | 10 3292 | 0 3293 | 20 3294 | 14 3295 | 11 3296 | 0 3297 | 21 3298 | 9.5 3299 | 0 3300 | ARC 3301 | 5 3302 | 66 3303 | 100 3304 | AcDbEntity 3305 | 8 3306 | logo 3307 | 6 3308 | ByLayer 3309 | 62 3310 | 256 3311 | 370 3312 | -1 3313 | 100 3314 | AcDbCircle 3315 | 10 3316 | 0.5 3317 | 20 3318 | 9.5 3319 | 40 3320 | 0.5 3321 | 100 3322 | AcDbArc 3323 | 50 3324 | 180 3325 | 51 3326 | 270 3327 | 0 3328 | LINE 3329 | 5 3330 | 67 3331 | 100 3332 | AcDbEntity 3333 | 8 3334 | logo 3335 | 6 3336 | ByLayer 3337 | 62 3338 | 256 3339 | 370 3340 | -1 3341 | 100 3342 | AcDbLine 3343 | 10 3344 | 11.5 3345 | 20 3346 | 9 3347 | 11 3348 | 15.5 3349 | 21 3350 | 9 3351 | 0 3352 | LINE 3353 | 5 3354 | 68 3355 | 100 3356 | AcDbEntity 3357 | 8 3358 | logo 3359 | 6 3360 | ByLayer 3361 | 62 3362 | 256 3363 | 370 3364 | -1 3365 | 100 3366 | AcDbLine 3367 | 10 3368 | 16 3369 | 20 3370 | 9.5 3371 | 11 3372 | 16 3373 | 21 3374 | 14 3375 | 0 3376 | ARC 3377 | 5 3378 | 69 3379 | 100 3380 | AcDbEntity 3381 | 8 3382 | logo 3383 | 6 3384 | ByLayer 3385 | 62 3386 | 256 3387 | 370 3388 | -1 3389 | 100 3390 | AcDbCircle 3391 | 10 3392 | 15.5 3393 | 20 3394 | 9.5 3395 | 40 3396 | 0.5 3397 | 100 3398 | AcDbArc 3399 | 50 3400 | 270 3401 | 51 3402 | 0 3403 | 0 3404 | LINE 3405 | 5 3406 | 6A 3407 | 100 3408 | AcDbEntity 3409 | 8 3410 | logo 3411 | 6 3412 | ByLayer 3413 | 62 3414 | 256 3415 | 370 3416 | -1 3417 | 100 3418 | AcDbLine 3419 | 10 3420 | 7.5 3421 | 20 3422 | 7 3423 | 11 3424 | 15.5 3425 | 21 3426 | 7 3427 | 0 3428 | LINE 3429 | 5 3430 | 6B 3431 | 100 3432 | AcDbEntity 3433 | 8 3434 | logo 3435 | 6 3436 | ByLayer 3437 | 62 3438 | 256 3439 | 370 3440 | -1 3441 | 100 3442 | AcDbLine 3443 | 10 3444 | 16 3445 | 20 3446 | 2 3447 | 11 3448 | 16 3449 | 21 3450 | 6.5 3451 | 0 3452 | ARC 3453 | 5 3454 | 6C 3455 | 100 3456 | AcDbEntity 3457 | 8 3458 | logo 3459 | 6 3460 | ByLayer 3461 | 62 3462 | 256 3463 | 370 3464 | -1 3465 | 100 3466 | AcDbCircle 3467 | 10 3468 | 15.5 3469 | 20 3470 | 6.5 3471 | 40 3472 | 0.5 3473 | 100 3474 | AcDbArc 3475 | 50 3476 | 0 3477 | 51 3478 | 90 3479 | 0 3480 | LINE 3481 | 5 3482 | 6D 3483 | 100 3484 | AcDbEntity 3485 | 8 3486 | logo 3487 | 6 3488 | ByLayer 3489 | 62 3490 | 256 3491 | 370 3492 | -1 3493 | 100 3494 | AcDbLine 3495 | 10 3496 | 11 3497 | 20 3498 | 9.5 3499 | 11 3500 | 11 3501 | 21 3502 | 15.5 3503 | 0 3504 | LINE 3505 | 5 3506 | 6E 3507 | 100 3508 | AcDbEntity 3509 | 8 3510 | logo 3511 | 6 3512 | ByLayer 3513 | 62 3514 | 256 3515 | 370 3516 | -1 3517 | 100 3518 | AcDbLine 3519 | 10 3520 | 14 3521 | 20 3522 | 16 3523 | 11 3524 | 11.5 3525 | 21 3526 | 16 3527 | 0 3528 | ARC 3529 | 5 3530 | 6F 3531 | 100 3532 | AcDbEntity 3533 | 8 3534 | logo 3535 | 6 3536 | ByLayer 3537 | 62 3538 | 256 3539 | 370 3540 | -1 3541 | 100 3542 | AcDbCircle 3543 | 10 3544 | 11.5 3545 | 20 3546 | 15.5 3547 | 40 3548 | 0.5 3549 | 100 3550 | AcDbArc 3551 | 50 3552 | 90 3553 | 51 3554 | 180 3555 | 0 3556 | LINE 3557 | 5 3558 | 70 3559 | 100 3560 | AcDbEntity 3561 | 8 3562 | logo 3563 | 6 3564 | ByLayer 3565 | 62 3566 | 256 3567 | 370 3568 | -1 3569 | 100 3570 | AcDbLine 3571 | 10 3572 | 9 3573 | 20 3574 | 9.5 3575 | 11 3576 | 9 3577 | 21 3578 | 15.5 3579 | 0 3580 | LINE 3581 | 5 3582 | 71 3583 | 100 3584 | AcDbEntity 3585 | 8 3586 | logo 3587 | 6 3588 | ByLayer 3589 | 62 3590 | 256 3591 | 370 3592 | -1 3593 | 100 3594 | AcDbLine 3595 | 10 3596 | 8.5 3597 | 20 3598 | 16 3599 | 11 3600 | 2 3601 | 21 3602 | 16 3603 | 0 3604 | ARC 3605 | 5 3606 | 72 3607 | 100 3608 | AcDbEntity 3609 | 8 3610 | logo 3611 | 6 3612 | ByLayer 3613 | 62 3614 | 256 3615 | 370 3616 | -1 3617 | 100 3618 | AcDbCircle 3619 | 10 3620 | 8.5 3621 | 20 3622 | 15.5 3623 | 40 3624 | 0.5 3625 | 100 3626 | AcDbArc 3627 | 50 3628 | 0 3629 | 51 3630 | 90 3631 | 0 3632 | ENDSEC 3633 | 0 3634 | SECTION 3635 | 2 3636 | OBJECTS 3637 | 0 3638 | DICTIONARY 3639 | 5 3640 | C 3641 | 330 3642 | 0 3643 | 100 3644 | AcDbDictionary 3645 | 281 3646 | 1 3647 | 3 3648 | ACAD_GROUP 3649 | 350 3650 | D 3651 | 0 3652 | DICTIONARY 3653 | 5 3654 | D 3655 | 330 3656 | C 3657 | 100 3658 | AcDbDictionary 3659 | 281 3660 | 1 3661 | 0 3662 | PLOTSETTINGS 3663 | 5 3664 | 73 3665 | 100 3666 | AcDbPlotSettings 3667 | 6 3668 | 1x1 3669 | 40 3670 | 0 3671 | 41 3672 | 0 3673 | 42 3674 | 0 3675 | 43 3676 | 0 3677 | 0 3678 | ENDSEC 3679 | 0 3680 | EOF 3681 | --------------------------------------------------------------------------------