├── .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 |
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 |
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 |
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #
Panorama View
2 |
3 | [](https://addons.mozilla.org/firefox/addon/panorama-view/)
4 | [](https://addons.mozilla.org/firefox/addon/panorama-view/)
5 | [](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 |
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 |
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 |
--------------------------------------------------------------------------------