diff --git a/.gitignore b/.gitignore index a2749853..1b4369d9 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ /addon/dist/ /addon/dist-zip/ /addon/package-lock.json + +*.DS_Store diff --git a/addon/package.json b/addon/package.json index b30f45f5..d0b483eb 100644 --- a/addon/package.json +++ b/addon/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "vue": "^2.7.14", + "vue-simple-accordion": "^0.1.0", "vue-swatches": "^2.1.0" }, "devDependencies": { diff --git a/addon/src/_locales/en/messages.json b/addon/src/_locales/en/messages.json index 9391939c..9ac63880 100644 --- a/addon/src/_locales/en/messages.json +++ b/addon/src/_locales/en/messages.json @@ -23,6 +23,14 @@ "message": "Move tab to group", "description": "Move tab to group title" }, + "transcendGroup": { + "message": "Transcend", + "description": "Transcend group" + }, + "moveGroupToParentDisabledTitle": { + "message": "Move group to parent group", + "description": "Move group to parent title" + }, "moveTabToGroupMessage": { "message": "Tab \"$tabtitle$\" was moved to group \"$grouptitle$\".\n\nClick here to show this tab.", "description": "Tab moved popup title", @@ -53,6 +61,14 @@ "message": "Save", "description": "Save" }, + "rename": { + "message": "Rename", + "description": "Rename" + }, + "renameParent": { + "message": "Rename Parent Group", + "description": "Rename Parent Group" + }, "delete": { "message": "Delete", "description": "Delete" @@ -157,6 +173,16 @@ } } }, + "newParentTitle": { + "message": "Parent $id$", + "description": "parent id", + "placeholders": { + "id": { + "content": "$1", + "example": "1" + } + } + }, "searchNotFoundTitle": { "message": "No results found for \"$search$\"", "description": "No results found", @@ -197,6 +223,20 @@ } } }, + "deleteParent": { + "message": "Delete parent group", + "description": "Delete parent group" + }, + "confirmDeleteParent": { + "message": "Do you want to delete parent group \"$parenttitle$\" ?
Removing parent group also removes all its tab groups!", + "description": "Delete parent group?", + "placeholders": { + "parenttitle": { + "content": "$1", + "example": "Parent Group #4" + } + } + }, "groupTabsCount": { "message": "$count$ tabs", "description": "Group tabs count", @@ -221,6 +261,30 @@ } } }, + "parentGroupsCount": { + "message": "$count$ groups", + "description": "Parent group count", + "placeholders": { + "count": { + "content": "$1", + "example": "4" + } + } + }, + "parentGroupsCountActive": { + "message": "$active$/$count$ groups active", + "description": "Parent groups count active", + "placeholders": { + "active": { + "content": "$1", + "example": "2" + }, + "count": { + "content": "$2", + "example": "4" + } + } + }, "deleteTab": { "message": "Delete tab", "description": "Delete tab" @@ -233,6 +297,10 @@ "message": "Create new group", "description": "Create new group" }, + "createNewParent": { + "message": "Create new parent group", + "description": "Create new parent group" + }, "goBackButtonTitle": { "message": "Go back", "description": "Go back" @@ -469,6 +537,14 @@ "message": "Open group in new window", "description": "Open group in new window" }, + "openParentInNewWindows": { + "message": "Open group in new windows", + "description": "Open parent group in new windows" + }, + "switchToContext": { + "message": "Switch to Context", + "description": "Switch to this parent group" + }, "muteTabsWhenGroupCloseAndRestoreWhenOpen": { "message": "Mute tabs when group close and restore when open", "description": "Mute tabs when group close and restore when open" diff --git a/addon/src/components/context-menu-group.vue b/addon/src/components/context-menu-group.vue index 3b217e71..7fe389c0 100644 --- a/addon/src/components/context-menu-group.vue +++ b/addon/src/components/context-menu-group.vue @@ -1,5 +1,6 @@ + + diff --git a/addon/src/components/popup-parent.vue b/addon/src/components/popup-parent.vue new file mode 100644 index 00000000..f6282f78 --- /dev/null +++ b/addon/src/components/popup-parent.vue @@ -0,0 +1,156 @@ + + + + + diff --git a/addon/src/icons/arrow-right.svg b/addon/src/icons/arrow-right.svg new file mode 100644 index 00000000..bf4e5001 --- /dev/null +++ b/addon/src/icons/arrow-right.svg @@ -0,0 +1,4 @@ + + + + diff --git a/addon/src/icons/parent-new.svg b/addon/src/icons/parent-new.svg new file mode 100644 index 00000000..8d477420 --- /dev/null +++ b/addon/src/icons/parent-new.svg @@ -0,0 +1,7 @@ + + + + + Svg Vector Icons : http://www.onlinewebfonts.com/icon + + \ No newline at end of file diff --git a/addon/src/js/constants.js b/addon/src/js/constants.js index 436705e5..85620b00 100644 --- a/addon/src/js/constants.js +++ b/addon/src/js/constants.js @@ -266,6 +266,7 @@ export const EXTENSIONS_WHITE_LIST = Object.freeze({ export const DEFAULT_OPTIONS = Object.freeze({ version: MANIFEST.version, groups: [], + parents: [], lastCreatedGroupPosition: 0, // options @@ -310,6 +311,14 @@ export const DEFAULT_OPTIONS = Object.freeze({ 'archive', 'rename', 'reload-all-tabs', + 'move-group-to-parent', + 'transcend-group', + ], + contextMenuParent: [ + 'open-in-new-windows', + 'switch-to-context', + 'rename', + 'remove', ], autoBackupEnable: true, diff --git a/addon/src/js/groups.js b/addon/src/js/groups.js index 5c5a70c8..ab01d90d 100644 --- a/addon/src/js/groups.js +++ b/addon/src/js/groups.js @@ -23,7 +23,7 @@ export async function load(groupId = null, withTabs = false, includeFavIconUrl, if (withTabs) { let groupTabs = groups.reduce((acc, group) => (acc[group.id] = [], acc), {}); - await Promise.all(allTabs.map(async function(tab) { + await Promise.all(allTabs.map(async function (tab) { if (tab.groupId) { if (groupTabs[tab.groupId]) { groupTabs[tab.groupId].push(tab); @@ -34,7 +34,7 @@ export async function load(groupId = null, withTabs = false, includeFavIconUrl, } })); - groups = groups.map(function(group) { + groups = groups.map(function (group) { if (!group.isArchive) { group.tabs = groupTabs[group.id].sort(Utils.sortBy('index')); } @@ -80,7 +80,7 @@ export async function save(groups, withMessage = false) { return groups; } -export function create(id, title, defaultGroupProps = {}) { +export function create(id, title, defaultGroupProps = {}, parentId = null) { const group = { id, title: null, @@ -106,6 +106,8 @@ export function create(id, title, defaultGroupProps = {}) { showOnlyActiveTabAfterMovingItIntoThisGroup: false, showNotificationAfterMovingTabIntoThisGroup: true, bookmarkId: null, + parentId, + isTranscend: false, ...defaultGroupProps, }; @@ -151,7 +153,12 @@ export async function saveDefault(defaultGroupProps) { log.stop(); } -export async function add(windowId, tabIds = [], title = null) { +export async function addUnderParent(parentId) { + const log = logger.start('addUnderParent', parentId); + return add(null, [], null, parentId); +} + +export async function add(windowId, tabIds = [], title = null, parentId = null) { tabIds = tabIds?.slice?.() || []; title = title?.slice(0, 256); @@ -179,7 +186,7 @@ export async function add(windowId, tabIds = [], title = null) { lastCreatedGroupPosition++; const {groups} = await load(), - newGroup = create(lastCreatedGroupPosition, title, defaultGroupProps); + newGroup = create(lastCreatedGroupPosition, title, defaultGroupProps, parentId); groups.push(newGroup); @@ -351,12 +358,13 @@ const KEYS_RESPONSIBLE_VIEW = Object.freeze([ 'prependTitleToWindow', ]); -export async function move(groupId, newGroupIndex) { +export async function move(groupId, newGroupIndex, newParentId) { const log = logger.start('move', {groupId, newGroupIndex}); let {groups, groupIndex} = await load(groupId); - - groups.splice(newGroupIndex, 0, groups.splice(groupIndex, 1)[0]); + let group = groups.splice(groupIndex, 1)[0]; + group.parentId = newParentId; + groups.splice(newGroupIndex, 0, group); await save(groups, true); @@ -522,6 +530,33 @@ export async function archiveToggle(groupId) { log.stop(); } +export async function transcendToggle(groupId) { + const log = logger.start('transcendToggle', groupId); + + await backgroundSelf.loadingBrowserAction(); + + let {group, groups} = await load(groupId, true), + tabsToRemove = []; + + log.log('group.isTranscend', group.isTranscend, '=>', !group.isTranscend); + + group.isTranscend = !group.isTranscend; + + await save(groups); + + backgroundSelf.sendMessage('groups-updated'); + + backgroundSelf.sendExternalMessage('group-updated', { + group: mapForExternalExtension(group), + }); + + backgroundSelf.loadingBrowserAction(false).catch(log.onCatch('loadingBrowserAction')); + + backgroundSelf.updateMoveTabMenus(); + + log.stop(); +} + const ExternalExtensionGroupDependentKeys = new Set([ 'title', 'isArchive', @@ -574,7 +609,8 @@ function isCatchedUrl(url, catchTabRules) { .some(regExpStr => { try { return new RegExp(regExpStr).test(url); - } catch (e) {} + } catch (e) { + } }); } @@ -644,7 +680,13 @@ export function getCatchedForTab(notArchivedGroups, currentGroup, {cookieStoreId } export function isNeedBlockBeforeRequest(groups) { - return groups.some(function({isArchive, catchTabContainers, catchTabRules, ifDifferentContainerReOpen, newTabContainer}) { + return groups.some(function ({ + isArchive, + catchTabContainers, + catchTabRules, + ifDifferentContainerReOpen, + newTabContainer + }) { if (isArchive) { return false; } diff --git a/addon/src/js/mixins/start-up-data.mixin.js b/addon/src/js/mixins/start-up-data.mixin.js index 2db1266c..6476b1ae 100644 --- a/addon/src/js/mixins/start-up-data.mixin.js +++ b/addon/src/js/mixins/start-up-data.mixin.js @@ -1,6 +1,7 @@ import * as Windows from '/js/windows.js'; import * as Groups from '/js/groups.js'; +import * as Parents from '/js/parents.js'; export default { methods: { @@ -11,10 +12,12 @@ export default { windows, currendWindow, {groups}, + {parents}, ] = await Promise.all([ Windows.load(true, true, includeThumbnail), Windows.get(), Groups.load(null, true, true, includeThumbnail), + Parents.load(null) ]); const unSyncTabs = windows @@ -30,6 +33,7 @@ export default { windows, currendWindow, groups, + parents, unSyncTabs, }; } diff --git a/addon/src/js/parents.js b/addon/src/js/parents.js new file mode 100644 index 00000000..8d7a5b2f --- /dev/null +++ b/addon/src/js/parents.js @@ -0,0 +1,531 @@ +import Logger from './logger.js'; +import backgroundSelf from './background.js'; +import * as Constants from './constants.js'; +import * as Storage from './storage.js'; +import * as Cache from './cache.js'; +import * as Containers from './containers.js'; +// import Messages from './messages.js'; +// import JSON from './json.js'; +import * as Groups from './groups.js'; +import * as Utils from './utils.js'; + +const logger = new Logger('ParentGroups'); + +// if set return {parent, parents, parentIndex} +export async function load(parentId = null) { + const log = logger.start('load', parentId); + + let {parents} = await Storage.get('parents') + + log.stop(); + + const parentIndex = parents.findIndex(parent => parent.id === parentId); + + return { + parent: parents[parentIndex], + parents, + parentIndex, + archivedGroups: parents.filter(parent => parent.isArchive), + notArchivedGroups: parents.filter(parent => !parent.isArchive), + }; +} + +export async function save(parents, withMessage = false) { + const log = logger.start('save', {withMessage}); + + if (!Array.isArray(parents)) { + log.throwError('parents has invalid type'); + } + + await Storage.set({parents}); + + if (withMessage) { + backgroundSelf.sendMessage('parents-updated'); + } + + log.stop(); + + return parents; +} + +export function create(id, title, defaultParentProps = {}, groupIds = []) { + const parent = { + id, + title: null, + iconColor: null, + iconUrl: null, + iconViewType: Constants.DEFAULT_GROUP_ICON_VIEW_TYPE, + groupIds: groupIds.slice(0), + isArchive: false, + + ...defaultParentProps, + }; + + if (id) { // create title for group + parent.title = createTitle(title, id, defaultParentProps); + } else { // create title for default group, if needed + parent.title ??= createTitle(title, id, defaultParentProps); + } + + parent.iconColor ??= Utils.randomColor(); + + return parent; +} + +export async function getDefaults() { + const {defaultParentProps} = await Storage.get('defaultParentProps'); + + const defaultParent = create(undefined, undefined, defaultParentProps); + const defaultCleanParent = create(undefined, undefined, {}); + + delete defaultParent.id; + delete defaultParent.groupIds; + + delete defaultCleanParent.id; + delete defaultCleanParent.groupIds; + + defaultParent.iconColor = defaultParentProps.iconColor || ''; + defaultCleanParent.iconColor = ''; + + return { + defaultParent, + defaultCleanParent, + defaultParentProps, + }; +} + +export async function saveDefault(defaultParentProps) { + const log = logger.start('saveDefault', defaultParentProps); + + await Storage.set({defaultParentProps}); + + log.stop(); +} + +export async function add(parentId, groupIds = [], title = null) { + groupIds = groupIds?.slice?.() || []; + title = title?.slice(0, 256); + + const log = logger.start('add', {parentId, groupIds, title}); + + const {parents} = await load(); + const newParent = create('p-' + Date.now(), title, {}, groupIds); + + parents.push(newParent); + + await save(parents); + + if (groupIds.length) { + newParent.groupIds = groupIds + } + + backgroundSelf.sendMessage('parent-added', { + parent: newParent, + }); + + backgroundSelf.sendExternalMessage('parent-added', { + parent: mapForExternalExtension(newParent), + }); + + log.stop(newParent); + + return newParent; +} + +export async function remove(parentId) { + const log = logger.start('remove', parentId); + + const {parent, parents, parentIndex} = await load(parentId); + + if (!parent) { + log.stopError('parentId', parentId, 'not found'); + return; + } + + parents.splice(parentIndex, 1); + + + await save(parents); + + backgroundSelf.sendMessage('parent-removed', { + parentId: parentId, + }); + + backgroundSelf.sendExternalMessage('parent-removed', { + parentId: parent, + }); + + log.stop(); +} + +export async function update(parentId, updateData) { + const log = logger.start('update', {parentId, updateData}); + + if (updateData.iconUrl?.startsWith('chrome:')) { + Utils.notify('Icon not supported'); + delete updateData.iconUrl; + } + + const updateDataKeys = Object.keys(updateData); + + if (!updateDataKeys.length) { + return log.stop(null, 'no updateData keys to update'); + } + + const {parent, parents} = await load(parentId); + + if (!parent) { + log.throwError(['group', parentId, 'not found for update it']); + } + + // updateData = JSON.clone(updateData); // clone need for fix bug: dead object after close tab which create object + + if (updateData.title) { + updateData.title = updateData.title.slice(0, 256); + } + + Object.assign(parent, updateData); + + await save(parents); + + backgroundSelf.sendMessage('parent-updated', { + parent: { + id: parentId, + ...updateData, + }, + }); + + if (updateDataKeys.some(key => ExternalExtensionGroupDependentKeys.has(key))) { + backgroundSelf.sendExternalMessage('parent-updated', { + group: mapForExternalExtension(parent), + }); + } + + if (KEYS_RESPONSIBLE_VIEW.some(key => updateData.hasOwnProperty(key))) { + backgroundSelf.updateMoveTabMenus(); + + await backgroundSelf.updateBrowserActionData(parentId); + } + + if (updateData.hasOwnProperty('title')) { + backgroundSelf.updateGroupBookmarkTitle(parent); + } + + log.stop(); +} + +const KEYS_RESPONSIBLE_VIEW = Object.freeze([ + 'title', + 'iconUrl', + 'iconColor', + 'iconViewType', + 'isArchive', +]); + +export async function move(parentId, newParentIndex) { + const log = logger.start('move', {parentId, newParentIndex}); + + let {parents, parentIndex} = await load(parentId); + + parents.splice(newParentIndex, 0, parents.splice(parentIndex, 1)[0]); + + await save(parents, true); + + backgroundSelf.updateMoveTabMenus(); + + log.stop(); +} + +export async function sort(vector = 'asc') { + const log = logger.start('sort', vector); + + if (!['asc', 'desc'].includes(vector)) { + log.throwError(`invalid sort vector: ${vector}`); + } + + let {parents} = await load(); + + if ('asc' === vector) { + parents.sort(Utils.sortBy('title')); + } else { + parents.sort(Utils.sortBy('title', undefined, true)); + } + + await save(parents, true); + + backgroundSelf.updateMoveTabMenus(); + + log.stop(); +} + +export async function unload(parentId) { + const log = logger.start('unload', parentId); + + if (!parentId) { + Utils.notify(['parentNotFound'], 7, 'parentNotFound'); + return log.stopError(false, 'parentNotFound'); + } + + + + return log.stop(true); +} + +export async function archiveToggle(parentId) { + const log = logger.start('archiveToggle', parentId); + + await backgroundSelf.loadingBrowserAction(); + + let {parent, parents} = await load(parentId, true), + tabsToRemove = []; + + log.log('parent.isArchive', parent.isArchive, '=>', !parent.isArchive); + + if (parent.isArchive) { + parent.isArchive = false; + + parent.groupIds = []; + } else { + + } + + await save(parents); + + backgroundSelf.sendMessage('parents-updated'); + + backgroundSelf.sendExternalMessage('parents-updated', { + parent: mapForExternalExtension(parent), + }); + + backgroundSelf.loadingBrowserAction(false).catch(log.onCatch('loadingBrowserAction')); + + backgroundSelf.updateMoveTabMenus(); + + log.stop(); +} + +const ExternalExtensionGroupDependentKeys = new Set([ + 'title', + 'isArchive', + 'isSticky', + 'iconColor', + 'iconUrl', + 'iconViewType', + 'newTabContainer', +]); + +export function mapForExternalExtension(parent) { + return { + id: parent.id, + title: getTitle(parent), + isArchive: parent.isArchive, + iconUrl: getIconUrl(parent), + }; +} + +export async function getNextTitle() { + const [ + {lastCreatedParentPosition}, + {defaultParentProps}, + ] = await Promise.all([ + Storage.get('lastCreatedParentPosition'), + getDefaults(), + ]); + + return createTitle(null, lastCreatedParentPosition + 1, defaultParentProps); +} + +function isCatchedUrl(url, catchTabRules) { + return catchTabRules + .split(/\s*\n\s*/) + .map(regExpStr => regExpStr.trim()) + .filter(Boolean) + .some(regExpStr => { + try { + return new RegExp(regExpStr).test(url); + } catch (e) {} + }); +} + +export async function setIconUrl(parentId, iconUrl) { + try { + await update(parentId, { + iconViewType: null, + iconUrl: await Utils.normalizeGroupIcon(iconUrl), + }); + } catch (e) { + Utils.notify(e); + } +} + +const emojiRegExp = /\p{RI}\p{RI}|\p{Emoji}(\p{EMod}+|\u{FE0F}\u{20E3}?|[\u{E0020}-\u{E007E}]+\u{E007F})?(\u{200D}\p{Emoji}(\p{EMod}+|\u{FE0F}\u{20E3}?|[\u{E0020}-\u{E007E}]+\u{E007F})?)+|\p{EPres}(\p{EMod}+|\u{FE0F}\u{20E3}?|[\u{E0020}-\u{E007E}]+\u{E007F})?|\p{Emoji}(\p{EMod}+|\u{FE0F}\u{20E3}?|[\u{E0020}-\u{E007E}]+\u{E007F})/u; +const firstCharEmojiRegExp = new RegExp(`^(${emojiRegExp.source})`, emojiRegExp.flags); + +export function getEmojiIcon(group) { + if (group.iconViewType === 'title') { + const [emoji] = firstCharEmojiRegExp.exec(group.title) || []; + return emoji; + } +} + +const UNKNOWN_GROUP_ICON_PROPS = { + title: '❓', + iconViewType: 'title', + iconColor: 'gray', +}; + +export function getIconUrl(group, keyInObj = null) { + group ??= UNKNOWN_GROUP_ICON_PROPS; + + let result = null; + + if (group.iconUrl) { + result = group.iconUrl; + } else { + if (!group.iconColor) { + group.iconColor = 'transparent'; + } + + const stroke = 'transparent' === group.iconColor ? 'stroke="#606060" stroke-width="1"' : '', + emoji = getEmojiIcon(group), + title = emoji || group.title; + + const icons = { + 'main-squares': ` + + + + + + + + + + + + + + + `, + circle: ` + + + + `, + squares: ` + + + + + + + + + `, + 'old-tab-groups': ` + + + + + + + + + `, + 'title': ` + + ${title} + + `, + }; + + try { + result = Utils.convertSvgToUrl(icons[group.iconViewType].trim()); + } catch (e) { + result = getIconUrl(UNKNOWN_GROUP_ICON_PROPS); + } + } + + if (keyInObj) { + return { + [keyInObj]: result, + }; + } + + return result; +} + +export function createTitle(title = null, parentId = null, defaultParentProps = {}) { + if (title) { + return String(title); + } + + if (defaultParentProps.title && parentId) { + return Utils.format(defaultParentProps.title, {index: parentId}, Utils.DATE_LOCALE_VARIABLES); + } + + return browser.i18n.getMessage('newParentTitle', parentId || '{index}'); +} + +export function getTitle({id, title, isArchive, groups, iconViewType}, args = '') { + let withActiveParent = args.includes('withActiveParent}'), + withCountGroups = args.includes('withCountGroups'), + withContainer = args.includes('withContainer'), + withGroups = args.includes('withGroups'), + beforeTitle = []; + + if (withActiveParent) { + if (Cache.getWindowId(id)) { + beforeTitle.push(Constants.ACTIVE_SYMBOL); + } else if (isArchive) { + beforeTitle.push(Constants.DISCARDED_SYMBOL); + } + } + + // replace first emoji to empty string + if (iconViewType === 'title') { + title = title.replace(firstCharEmojiRegExp, ''); + } + + if (beforeTitle.length) { + title = beforeTitle.join(' ') + ' ' + title; + } + + if (withCountGroups) { + title += ' (' + groupsCountMessage(groups.slice(), isArchive) + ')'; + } + + if (withGroups) { + if (groups.length) { + title += ':\n' + groups + .slice(0, 30) + .map(group => Groups.getTitle(group, false, 70, !isArchive)) + .join('\n'); + + if (groups.length > 30) { + title += '\n...'; + } + } + } + + if (window.localStorage.enableDebug) { + let windowId = Cache.getWindowId(id) || groups?.[0]?.windowId || 'no window'; + title = `@${windowId}:#${id} ${title}`; + } + + return title; +} + +export function groupsCountMessage(groups, groupIsArchived, lang = true) { + if (groupIsArchived) { + return lang ? browser.i18n.getMessage('parentGroupCount', groups.length) : groups.length; + } + + let activeGroupCount = groups.filter(tab => !tab.discarded).length; + + if (lang) { + return browser.i18n.getMessage('parentGroupsCountActive', [activeGroupCount, groups.length]); + } + + return activeGroupCount ? (activeGroupCount + '/' + groups.length) : groups.length; +} diff --git a/addon/src/js/tabs.js b/addon/src/js/tabs.js index 7bf0a083..c2dcc00a 100644 --- a/addon/src/js/tabs.js +++ b/addon/src/js/tabs.js @@ -7,6 +7,7 @@ import * as Cache from './cache.js'; import * as Containers from './containers.js'; import * as Groups from './groups.js'; import * as Windows from './windows.js'; +import {emptyUrlsArray} from "./utils.js"; const logger = new Logger('Tabs'); @@ -16,7 +17,7 @@ export async function createNative({url, active, pinned, title, index, windowId, if (url) { if (Utils.isUrlAllowToCreate(url)) { tab.url = url; - } else if (url !== 'about:newtab') { + } else if (!emptyUrlsArray.has(url)) { tab.url = Urls.HELP_PAGE_UNSUPPORTED_URL + '#' + url; } } diff --git a/addon/src/js/utils.js b/addon/src/js/utils.js index 48bb7260..8ba50022 100644 --- a/addon/src/js/utils.js +++ b/addon/src/js/utils.js @@ -237,7 +237,7 @@ export function isWindowAllow({type}) { return browser.windows.WindowType.NORMAL === type; } -const createTabUrlRegexp = /^((http|ftp|moz-extension)|about:blank)/, +export const createTabUrlRegexp = /^((http|ftp|moz-extension)|about:blank)/, emptyUrlsArray = new Set(['about:blank', 'about:newtab', 'about:home']); export function isUrlEmpty(url) { diff --git a/addon/src/manage/Manage.vue b/addon/src/manage/Manage.vue index 916a4e69..af5ffcb8 100644 --- a/addon/src/manage/Manage.vue +++ b/addon/src/manage/Manage.vue @@ -1,1014 +1,1147 @@ + +

Tab Groups

+
+ + @@ -1291,52 +1619,63 @@ + + :menu="options.contextMenuGroup" + :groups="groups" + :parents="parents" + :opened-windows="openedWindows" + :show-rename="false" + :show-settings="false" + :show-remove="false" + @open-in-new-window="(group, tab) => applyGroup(group, tab, true)" + @sort="sortGroups" + @discard="discardGroup" + @discard-other="discardOtherGroups" + @export-to-bookmarks="exportGroupToBookmarks" + @unload="unloadGroup" + @archive="toggleArchiveGroup" + @unarchive="toggleArchiveGroup" + @reload-all-tabs="reloadAllTabsInGroup" + @move-group="moveGroup" + @move-group-new-parent="moveGroupToNewParent" + @transcend-group="toggleTranscendGroup" + > + :menu="options.contextMenuTab" + :groups="groups" + :multiple-tab-ids="multipleTabIds" + :show-update-thumbnail="options.showTabsWithThumbnailsInManageGroups" + @open-in-new-window="(group, tab) => applyGroup(group, tab, true)" + @reload="reloadTab" + @discard="discardTab" + @remove="removeTab" + @update-thumbnail="updateTabThumbnail" + @set-group-icon="setTabIconAsGroupIcon" + @move-tab="moveTabs" + @move-tab-new-group="moveTabToNewGroup" + > + @save-group="() => $refs.editDefaultGroup.triggerChanges()" + @close-popup="openEditDefaultGroup = false" + > + ref="editDefaultGroup" + :group-to-edit="defaultGroup" + :is-default-group="true" + :group-to-compare="defaultCleanGroup" + @changes="saveDefaultGroup"> + @close-popup="groupToEdit = null" + @save-group="() => $refs.editGroup.triggerChanges()" + > + ref="editGroup" + :group-to-edit="groupToEdit.$data" + :group-to-compare="groupToEdit.$data" + @changes="changes => saveEditedGroup(groupToEdit.id, changes)">
- +
+ +
+ +
+
- :root { - --margin: 5px; - --is-in-multiple-drop-text-color: #ffffff; - --border-radius: 3px; - - --group-bg-color: #f5f5f5; - --group-active-shadow-color: rgba(3, 102, 214, 0.3); - --group-active-shadow: 0 0 0 3.5px var(--group-active-shadow-color); - --group-active-border-color: #2188ff; - --group-active-border: 1px solid var(--group-active-border-color); - - --tab-active-shadow: var(--group-active-shadow); - --tab-active-border: var(--group-active-border); - // --tab-hover-outline-color: #cfcfcf; - - --tab-inner-padding: 3px; - --tab-inner-border-color: #c6ced4; - --tab-border-width: 1px; - --tab-buttons-radius: 75%; - --tab-buttons-size: 25px; - --active-tab-bg-color: #e4e4e4; - --multiple-drag-tab-bg-color: #1e88e5; - } - - html[data-theme="dark"] { - --text-color: #e0e0e0; - - --group-bg-color: #444444; - --group-active-shadow-color: rgba(255, 255, 255, 0.3); - --group-active-border-color: #e0e0e0; - - --discarded-text-color: #979797; +:root { + --margin: 5px; + --is-in-multiple-drop-text-color: #ffffff; + --border-radius: 3px; + + --group-bg-color: #f5f5f5; + --group-active-shadow-color: rgba(3, 102, 214, 0.3); + --group-active-shadow: 0 0 0 3.5px var(--group-active-shadow-color); + --group-active-border-color: #2188ff; + --group-active-border: 1px solid var(--group-active-border-color); + + --tab-active-shadow: var(--group-active-shadow); + --tab-active-border: var(--group-active-border); + // --tab-hover-outline-color: #cfcfcf; + + --tab-inner-padding: 3px; + --tab-inner-border-color: #c6ced4; + --tab-border-width: 1px; + --tab-buttons-radius: 75%; + --tab-buttons-size: 25px; + --active-tab-bg-color: #e4e4e4; + --multiple-drag-tab-bg-color: #1e88e5; +} + +html[data-theme="dark"] { + --text-color: #e0e0e0; + + --group-bg-color: #444444; + --group-active-shadow-color: rgba(255, 255, 255, 0.3); + --group-active-border-color: #e0e0e0; + + --discarded-text-color: #979797; +} + +.fade-enter-active, .fade-leave-active { + transition: opacity .5s; +} + +.fade-enter, .fade-leave-to /* .fade-leave-active до версии 2.1.8 */ +{ + opacity: 0; +} + +#stg-manage { + padding: var(--indent) var(--indent) calc(var(--indent) * 10); + + > header { + > :not(:first-child) { + margin-left: 20px; } - .fade-enter-active, .fade-leave-active { - transition: opacity .5s; + .page-title { + font-size: 18px; } - .fade-enter, .fade-leave-to /* .fade-leave-active до версии 2.1.8 */ { - opacity: 0; + } + + > main { + margin-top: var(--indent); + } +} + +#search-wrapper { + width: 300px; +} + +.loading { + height: 50px; + width: 50px; + position: absolute; + top: calc(100vh / 2 - 25px); + left: calc(100vw / 2 - 25px); + fill: #6e6e6e; +} + +#multipleTabsText { + position: fixed; + text-align: center; + color: #000; + background-color: #fff; + border-radius: 10px; + left: -1000%; + max-width: 450px; + padding: 15px; + pointer-events: none; + box-shadow: 10px 5px rgba(0, 0, 0, 0.6); +} + +#result { + .parent { + background-color: #777; + color: white; + cursor: pointer; + padding: 18px; + width: 100%; + border: none; + text-align: left; + outline: none; + font-size: 15px; + } + + .active, .parent:hover { + background-color: #555; + } + + .parent:after { + content: url("/icons/arrow-right.svg"); + color: white; + font-weight: bold; + float: right; + margin-left: 5px; + height: 20px; + width: 20px; + } + + .parent.active:after { + content: url("/icons/arrow-down.svg"); + } + + .content.expanded { + height: auto; + } + + .content { + padding: 0; + height: 0; + overflow: hidden; + transition: min-height 0.2s ease-out, max-height 0.2s ease-out; + background-color: #2c2c2c; + // GRID VIEW + + .grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); + /* grid-template-rows: minmax(auto, 600px) minmax(auto, 600px); */ + grid-gap: 10px; + padding: var(--margin); } - #stg-manage { - padding: var(--indent) var(--indent) calc(var(--indent) * 10); - - > header { - > :not(:first-child) { - margin-left: 20px; - } - - .page-title { - font-size: 18px; - } + .group { + display: flex; + flex-direction: column; + border: 1px solid rgba(0, 0, 0, 0.15); + max-height: 600px; + background-color: var(--group-bg-color); + border-radius: var(--border-radius); + + &.drag-over { + outline-offset: 3px; + } + + > .header { + display: flex; + align-items: center; + padding: var(--margin); + + > * { + display: flex; } - > main { - margin-top: var(--indent); + > .group-title { + flex-grow: 1; } - } - - #search-wrapper { - width: 300px; - } - - .loading { - height: 50px; - width: 50px; - position: absolute; - top: calc(100vh / 2 - 25px); - left: calc(100vw / 2 - 25px); - fill: #6e6e6e; - } - #multipleTabsText { - position: fixed; - text-align: center; - color: #000; - background-color: #fff; - border-radius: 10px; - left: -1000%; - max-width: 450px; - padding: 15px; - pointer-events: none; - box-shadow: 10px 5px rgba(0, 0, 0, 0.6); - } - - #result { - // GRID VIEW - .grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); - /* grid-template-rows: minmax(auto, 600px) minmax(auto, 600px); */ - grid-gap: 10px; + > .delete-group-button { + line-height: 0; } - .group { - display: flex; - flex-direction: column; - border: 1px solid rgba(0, 0, 0, 0.15); - max-height: 600px; - background-color: var(--group-bg-color); - border-radius: var(--border-radius); - - &.drag-over { - outline-offset: 3px; - } - - > .header { - display: flex; - align-items: center; - padding: var(--margin); - - > * { - display: flex; - } - - > .group-title { - flex-grow: 1; - } - - > .delete-group-button { - line-height: 0; - } - - > :not(:first-child) { - padding-left: var(--margin); - } - - > .group-icon { - position: relative; - } - - > .group-icon > *, - > .other-icon > * { - pointer-events: none; - } - } - - > .body { - user-select: none; - - overflow-y: auto; - padding: var(--margin); - min-height: 110px; - /* flex-grow: 1; */ - - scrollbar-width: thin; - - &:not(.in-list-view) { - display: grid; - grid-gap: var(--margin); - /* grid-gap: 10px; */ - grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); - grid-auto-rows: 100px; - } - - &.in-list-view { - display: flex; - flex-direction: column; - margin-bottom: 30px; - } - } - - .tab { - position: relative; - } + > :not(:first-child) { + padding-left: var(--margin); + } - &:not(.is-archive) .tab { - cursor: pointer; - } + > .group-icon { + position: relative; + } - > .body:not(.in-list-view) > .tab { - padding: var(--tab-inner-padding); - border-radius: var(--border-radius); + > .group-icon > *, + > .other-icon > * { + pointer-events: none; + } + } - > * { - border: 0 solid var(--identity-tab-color, var(--tab-inner-border-color)); - background-color: var(--group-bg-color); - } + > .body { + user-select: none; - > .tab-icon, - > .delete-tab-button, - > .cookie-container, - > .refresh-icon, - > .tab-title { - position: absolute; - } + overflow-y: auto; + padding: var(--margin); + min-height: 110px; + /* flex-grow: 1; */ - &:not(.has-thumbnail) > .tab-icon { - display: flex; - width: 16px; - height: 16px; - top: calc((calc(100% - 1em - var(--tab-inner-padding)) / 2) - 8px); - left: calc((100% / 2) - 8px); - } + scrollbar-width: thin; - &.has-thumbnail > .tab-icon { - display: flex; - align-items: start; - justify-content: left; - top: var(--tab-inner-padding); - left: var(--tab-inner-padding); - width: var(--tab-buttons-size); - height: var(--tab-buttons-size); - border-bottom-width: var(--tab-border-width); - border-right-width: var(--tab-border-width); - border-bottom-right-radius: var(--tab-buttons-radius); - } + &:not(.in-list-view) { + display: grid; + grid-gap: var(--margin); + /* grid-gap: 10px; */ + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + grid-auto-rows: 100px; + } - > .delete-tab-button { - display: flex; - visibility: hidden; - align-items: start; - justify-content: right; - top: var(--tab-inner-padding); - right: var(--tab-inner-padding); - height: var(--tab-buttons-size); - width: var(--tab-buttons-size); - line-height: 0; - border-bottom-width: var(--tab-border-width); - border-left-width: var(--tab-border-width); - border-bottom-left-radius: var(--tab-buttons-radius); - } + &.in-list-view { + display: flex; + flex-direction: column; + margin-bottom: 30px; + } + } - &:hover > .delete-tab-button { - visibility: visible; - } + .tab { + position: relative; + } - > .cookie-container { - display: flex; - align-items: end; - justify-content: left; - left: var(--tab-inner-padding); - bottom: calc(1em + var(--tab-inner-padding) * 2); - width: var(--tab-buttons-size); - height: var(--tab-buttons-size); - border-right-width: var(--tab-border-width); - border-top-width: var(--tab-border-width); - border-top-right-radius: var(--tab-buttons-radius); - padding-bottom: 1px; - } + &:not(.is-archive) .tab { + cursor: pointer; + } - > .refresh-icon { - display: flex; - align-items: end; - justify-content: right; - bottom: calc(1em + var(--tab-inner-padding) * 2); - right: var(--tab-inner-padding); - width: var(--tab-buttons-size); - height: var(--tab-buttons-size); - border-left-width: var(--tab-border-width); - border-top-width: var(--tab-border-width); - border-top-left-radius: var(--tab-buttons-radius); - } + > .body:not(.in-list-view) > .tab { + padding: var(--tab-inner-padding); + border-radius: var(--border-radius); - > .tab-title { - line-height: 1.3em; - position: absolute; - text-align: center; - left: var(--tab-inner-padding); - right: var(--tab-inner-padding); - bottom: var(--tab-inner-padding); - white-space: nowrap; - } + > * { + border: 0 solid var(--identity-tab-color, var(--tab-inner-border-color)); + background-color: var(--group-bg-color); + } - > .screenshot { - height: calc(100% - 1em - var(--tab-inner-padding) - 1px); - overflow: hidden; - border-width: var(--tab-border-width); - border-radius: var(--border-radius); + > .tab-icon, + > .delete-tab-button, + > .cookie-container, + > .refresh-icon, + > .tab-title { + position: absolute; + } - > img { - width: 100%; - height: 100%; + &:not(.has-thumbnail) > .tab-icon { + display: flex; + width: 16px; + height: 16px; + top: calc((calc(100% - 1em - var(--tab-inner-padding)) / 2) - 8px); + left: calc((100% / 2) - 8px); + } - &[src=""] { - display: none; - } - } - } + &.has-thumbnail > .tab-icon { + display: flex; + align-items: start; + justify-content: left; + top: var(--tab-inner-padding); + left: var(--tab-inner-padding); + width: var(--tab-buttons-size); + height: var(--tab-buttons-size); + border-bottom-width: var(--tab-border-width); + border-right-width: var(--tab-border-width); + border-bottom-right-radius: var(--tab-buttons-radius); + } - &.new > .screenshot { - display: flex; - justify-content: center; - align-items: center; - border-style: dashed; - border-width: var(--tab-border-width); + > .delete-tab-button { + display: flex; + visibility: hidden; + align-items: start; + justify-content: right; + top: var(--tab-inner-padding); + right: var(--tab-inner-padding); + height: var(--tab-buttons-size); + width: var(--tab-buttons-size); + line-height: 0; + border-bottom-width: var(--tab-border-width); + border-left-width: var(--tab-border-width); + border-bottom-left-radius: var(--tab-buttons-radius); + } - > img { - width: 16px; - opacity: 0.7; - } - } + &:hover > .delete-tab-button { + visibility: visible; + } - // &:hover, - // &:hover > * { - // background-color: var(--active-tab-bg-color); - // } + > .cookie-container { + display: flex; + align-items: end; + justify-content: left; + left: var(--tab-inner-padding); + bottom: calc(1em + var(--tab-inner-padding) * 2); + width: var(--tab-buttons-size); + height: var(--tab-buttons-size); + border-right-width: var(--tab-border-width); + border-top-width: var(--tab-border-width); + border-top-right-radius: var(--tab-buttons-radius); + padding-bottom: 1px; + } - &.is-active-element { - box-shadow: var(--tab-active-shadow); - outline: var(--tab-active-border); - outline-offset: -1px; - -moz-outline-radius: var(--border-radius); - } + > .refresh-icon { + display: flex; + align-items: end; + justify-content: right; + bottom: calc(1em + var(--tab-inner-padding) * 2); + right: var(--tab-inner-padding); + width: var(--tab-buttons-size); + height: var(--tab-buttons-size); + border-left-width: var(--tab-border-width); + border-top-width: var(--tab-border-width); + border-top-left-radius: var(--tab-buttons-radius); + } - // &:not(.is-active-element):not(.drag-moving):hover { - // outline: 1px solid var(--tab-hover-outline-color); - // outline-offset: 1px; - // } + > .tab-title { + line-height: 1.3em; + position: absolute; + text-align: center; + left: var(--tab-inner-padding); + right: var(--tab-inner-padding); + bottom: var(--tab-inner-padding); + white-space: nowrap; + } - &.is-in-multiple-drop, - &.is-in-multiple-drop > * { - --fill-color: var(--is-in-multiple-drop-text-color); - background-color: var(--multiple-drag-tab-bg-color); - } + > .screenshot { + height: calc(100% - 1em - var(--tab-inner-padding) - 1px); + overflow: hidden; + border-width: var(--tab-border-width); + border-radius: var(--border-radius); - &.drag-over { - &.drag-moving, - &.is-in-multiple-drop { - outline-offset: 4px; - } - } + > img { + width: 100%; + height: 100%; - &.is-in-multiple-drop > .tab-title { - color: var(--is-in-multiple-drop-text-color); - } + &[src=""] { + display: none; } + } + } - > .body.in-list-view > .tab { - display: flex; - align-items: center; - justify-content: left; - height: 27px; - padding: var(--tab-inner-padding); - - &.new { - justify-content: center; - border: var(--tab-border-width) dashed var(--tab-inner-border-color); + &.new > .screenshot { + display: flex; + justify-content: center; + align-items: center; + border-style: dashed; + border-width: var(--tab-border-width); + + > img { + width: 16px; + opacity: 0.7; + } + } - > .tab-title { - flex-grow: 0; - } - } + // &:hover, + // &:hover > * { + // background-color: var(--active-tab-bg-color); + // } - &:hover { - background-color: rgba(126, 126, 126, 0.3); - } + &.is-active-element { + box-shadow: var(--tab-active-shadow); + outline: var(--tab-active-border); + outline-offset: -1px; + -moz-outline-radius: var(--border-radius); + } - > .tab-icon { - display: flex; - } + // &:not(.is-active-element):not(.drag-moving):hover { + // outline: 1px solid var(--tab-hover-outline-color); + // outline-offset: 1px; + // } - > .delete-tab-button { - display: none; - justify-content: right; - } + &.is-in-multiple-drop, + &.is-in-multiple-drop > * { + --fill-color: var(--is-in-multiple-drop-text-color); + background-color: var(--multiple-drag-tab-bg-color); + } - &:hover > .delete-tab-button { - display: flex; - } + &.drag-over { + &.drag-moving, + &.is-in-multiple-drop { + outline-offset: 4px; + } + } - > .cookie-container { - padding-left: var(--margin); - display: flex; - align-items: center; - } + &.is-in-multiple-drop > .tab-title { + color: var(--is-in-multiple-drop-text-color); + } + } + + > .body.in-list-view > .tab { + display: flex; + align-items: center; + justify-content: left; + height: 27px; + padding: var(--tab-inner-padding); + + &.new { + justify-content: center; + border: var(--tab-border-width) dashed var(--tab-inner-border-color); + + > .tab-title { + flex-grow: 0; + } + } - > .refresh-icon { - display: flex; - padding-left: var(--margin); - } + &:hover { + background-color: rgba(126, 126, 126, 0.3); + } - > .tab-title { - flex-grow: 1; - padding: 0 var(--margin); - white-space: nowrap; - } + > .tab-icon { + display: flex; + } - &.is-active-element { - outline: 1px solid var(--identity-tab-color, var(--group-active-border-color)); - outline-offset: -1px; - -moz-outline-radius: var(--border-radius); - } + > .delete-tab-button { + display: none; + justify-content: right; + } - &.is-in-multiple-drop, - &.is-in-multiple-drop > * { - --fill-color: var(--is-in-multiple-drop-text-color); - background-color: var(--multiple-drag-tab-bg-color); - } + &:hover > .delete-tab-button { + display: flex; + } - &.is-in-multiple-drop > .tab-title { - color: var(--is-in-multiple-drop-text-color); - } - } + > .cookie-container { + padding-left: var(--margin); + display: flex; + align-items: center; + } - &.is-opened { - box-shadow: var(--group-active-shadow); - border: var(--group-active-border); - } + > .refresh-icon { + display: flex; + padding-left: var(--margin); + } - &.new { - display: flex; - align-content: center; - justify-content: center; - min-height: 250px; - border: 2px dashed var(--tab-inner-border-color); - background-color: transparent; - - > .body { - display: block; - text-align: center; - - > img { - width: 100px; - opacity: 0.7; - } - } - } + > .tab-title { + flex-grow: 1; + padding: 0 var(--margin); + white-space: nowrap; } - .group, - .group .tab { - transition: opacity 0.3s; + &.is-active-element { + outline: 1px solid var(--identity-tab-color, var(--group-active-border-color)); + outline-offset: -1px; + -moz-outline-radius: var(--border-radius); } - .drag-tab .tab > *, - .drag-tab .group > *, - .drag-group .group > * { - pointer-events: none; + &.is-in-multiple-drop, + &.is-in-multiple-drop > * { + --fill-color: var(--is-in-multiple-drop-text-color); + background-color: var(--multiple-drag-tab-bg-color); } - .drag-tab .group > .body > *:not(.new) { - pointer-events: all; + &.is-in-multiple-drop > .tab-title { + color: var(--is-in-multiple-drop-text-color); + } + } + + &.is-opened { + box-shadow: var(--group-active-shadow); + border: var(--group-active-border); + } + + &.new { + display: flex; + align-content: center; + justify-content: center; + min-height: 250px; + border: 2px dashed var(--tab-inner-border-color); + background-color: transparent; + + > .body { + display: block; + text-align: center; + + > img { + width: 100px; + opacity: 0.7; + } } + } + } + + .group, + .group .tab { + transition: opacity 0.3s; + } - /* Drag & Drop Styles */ + .drag-tab .tab > *, + .drag-tab .group > *, + .drag-group .group > * { + pointer-events: none; + } - .drag-moving, - .drag-tab .tab.is-in-multiple-drop { - opacity: 0.4; - } + .drag-tab .group > .body > *:not(.new) { + pointer-events: all; + } + + /* Drag & Drop Styles */ + + .drag-moving, + .drag-tab .tab.is-in-multiple-drop { + opacity: 0.4; } + } + + +} diff --git a/addon/src/popup/Popup.vue b/addon/src/popup/Popup.vue index a3e22a15..813f9f35 100644 --- a/addon/src/popup/Popup.vue +++ b/addon/src/popup/Popup.vue @@ -1,1323 +1,1489 @@ + + +
+ + + +
-
- - - -
-
-
Parent Groups + +
+ +
+
-
- -
+ + +
- +
- +
+ v-if="showMuteIconGroup(group)" + class="image is-16x16" + @click.stop="toggleMuteGroup(group)" + :title="group.tabs.some(tab => tab.audible) ? lang('muteGroup') : lang('unMuteGroup')" + > + :src="group.tabs.some(tab => tab.audible) ? '/icons/audio.svg' : '/icons/audio-mute.svg'" + class="align-text-bottom"/>
- - - + + +
-
- - +
+ +
-
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+ draggable="true" + @dragstart="dragHandle($event, 'group', ['group'], {item: group})" + @dragenter="dragHandle($event, 'group', ['group'], {item: group})" + @dragover="dragHandle($event, 'group', ['group'], {item: group})" + @dragleave="dragHandle($event, 'group', ['group'], {item: group})" + @drop="dragHandle($event, 'group', ['group'], {item: group})" + @dragend="dragHandle($event, 'group', ['group'], {item: group})" + + @contextmenu="$refs.contextMenuGroup.open($event, {group})" + @click="!group.isArchive && applyGroup(group)" + @keydown.enter="!group.isArchive && applyGroup(group, undefined, true)" + @keydown.right.stop="showSectionGroupTabs(group);" + @keydown.up="focusToNextElement" + @keydown.down="focusToNextElement" + @keydown.f2.stop="renameGroup(group)" + tabindex="0" + :title="getGroupTitle(group, 'withCountTabs withTabs withContainer')" + > +
+ + + +
+
+
+ +
+ +
+ +
+ + + + + +
+
+ + +
+
+
+
+
+ +
+
+
+
+
+
+
+ @click="addParent" + @keydown.enter="addParent">
- +
-
+

-
+
- +
@@ -1532,48 +1866,60 @@
  • - +
  • - +
  • - +
  • - +

+ :title="getTabTitle(tab, true)" + tabindex="0" + >
- - + +
- + :title="tab.audible ? lang('muteTab') : lang('unMuteTab')"> + - +
- - + +
@@ -1584,9 +1930,10 @@
-
+
- +
@@ -1595,38 +1942,46 @@
- +
- +
- - + +
- - + + - - + +
+ v-for="(tab, tabIndex) in groupToShow.tabs" + :key="tabIndex" + class="tab item is-unselectable" + :title="getTabTitle(tab, true)" + @mousedown.middle.prevent + >
- +
- +
@@ -1635,20 +1990,20 @@