From 22878dc468686c98731789155745716a6fc09188 Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Tue, 2 Apr 2019 04:31:01 +0300 Subject: [PATCH 01/32] version 3.1.10 Greatly improved RAM usage for history auto-save (Firefox storage bug) History saving now keeps track of closed windows Fixed several visual bugs --- data/scripts/components/config-panel.js | 7 +- .../components/session-bookmark-events.js | 442 ++++++++++++++++++ data/scripts/components/session-bookmark.js | 434 +---------------- data/scripts/components/session-container.js | 87 ++-- data/scripts/components/session-filtering.js | 4 +- .../components/session-folder-events.js | 272 +++++++++++ data/scripts/components/session-folder.js | 231 ++------- data/scripts/components/session-history.js | 55 ++- data/scripts/components/session-list.js | 102 ++-- data/scripts/components/session-sync-model.js | 2 - data/scripts/components/trash-can.js | 48 +- data/scripts/config.js | 7 +- data/scripts/session/bookmarks.js | 4 +- data/scripts/session/management.js | 81 ++-- data/session-sync.css | 22 +- dev/css/config-panel.css | 6 +- dev/css/context-menu.css | 2 +- dev/css/sessions-list.css | 4 +- dev/css/trash-can.css | 10 +- manifest.json | 5 +- scripts/development.js | 19 + scripts/main.js | 6 +- scripts/session-history.js | 175 ++++--- 23 files changed, 1110 insertions(+), 915 deletions(-) create mode 100644 data/scripts/components/session-bookmark-events.js create mode 100644 data/scripts/components/session-folder-events.js create mode 100644 scripts/development.js diff --git a/data/scripts/components/config-panel.js b/data/scripts/components/config-panel.js index cf9051c..ea7529f 100644 --- a/data/scripts/components/config-panel.js +++ b/data/scripts/components/config-panel.js @@ -220,9 +220,6 @@ define(function(require, exports) { (function() { - if (!AppConfig.isAddonContext()) - return; - if (typeof browser === 'object' && browser.commands.update == undefined) return; @@ -313,8 +310,8 @@ define(function(require, exports) { // Auto-save expire time var expireAfterRG = new DOMComponent.RangeControl({ value: historyConfig.expireTimeHours, - description: 'Delete after [...] hours', - minValue: 0, + description: 'Delete older than [...] hours', + minValue: 1, onChange: function(value) { historyConfig.expireTimeHours = value; SessionHistory.updateConfig(historyConfig); diff --git a/data/scripts/components/session-bookmark-events.js b/data/scripts/components/session-bookmark-events.js new file mode 100644 index 0000000..f6dd6c6 --- /dev/null +++ b/data/scripts/components/session-bookmark-events.js @@ -0,0 +1,442 @@ +define(function(require, exports) { + + 'use strict'; + + // ***************************************************************************** + // Custom Modules + + const { AppConfig } = require('../config'); + const { WindowEvents } = require('../utils/global-events'); + const { AutoScroll } = require('../utils/auto-scroll'); + const { DragContext } = require('../utils/drag-context'); + const { SessionSyncModel } = require('./session-sync-model'); + + const { SessionManager } = require('../session/management'); + const { BookmarkManager } = require('../session/bookmarks'); + + // ***************************************************************************** + // API + + // ---------------------------------------------------------------------------- + // Session Events - delegated + + var SessionBookmarkEvents = function SessionBookmarkEvents(container) + { + var startDragEvent = null; + var trackingTelemetry = new Date(); + var offsetRect = {}; + var bookmarkContext = null; + var bookmarkHoverContextID = 0; + var SyncModel = SessionSyncModel.getModel(document); + + function getAttribute(target, attribute) { + try { + return target.getAttribute(attribute); + } + catch (err) { + console.log(err); + return null; + } + } + + function getTargetType(e) { + return getAttribute(e.target, 'type'); + } + + function getSessionID(e) { + return getAttribute(e.target, 'sessionID'); + } + + function getBookmarkID(e) { + return getAttribute(e.target, 'bookmarkID'); + } + + function getTabURL(e) { + return getAttribute(e.target, 'url'); + } + + function getTabID(e) { + var ID = getAttribute(e.target, 'tabID'); + return ID ? ID | 0 : undefined; + } + + function getWindowID(e) { + var ID = getAttribute(e.target, 'windowID'); + return ID ? ID | 0 : undefined; + } + + // -------------------------------------------------------------- + // Session level + + var bookmarkDblClick = function sessionDblClick(e) + { + if (SyncModel.state.session == 'restore') + { + var bookmarkID = getBookmarkID(e); + if (bookmarkID != null) { + WindowEvents.emit(document, 'BookmarkCtxMenu-EditBookmark', bookmarkID); + } + } + }; + + var mouseOver = function mouseOver(e) + { + if (!e.target.hasAttribute('over')) { + return; + } + + switch(SyncModel.state.session) + { + // Bookmark entry + case 'restore': { + var bookmarkID = getBookmarkID(e); + if (bookmarkHoverContextID == bookmarkID) + return; + + bookmarkHoverContextID = bookmarkID; + WindowEvents.emit(document, 'ShowUrlBar', SessionSyncModel.bookmarks[bookmarkID].url); + return; + } + + // Window tab + case 'current': { + var tabID = getTabID(e); + if (tabID != undefined) { + let url = getTabURL(e); + WindowEvents.emit(document, 'ShowUrlBar', url); + } + return; + } + + // History tab + case 'history': { + let url = getTabURL(e); + WindowEvents.emit(document, 'ShowUrlBar', url); + return; + } + } + }; + + var mouseLeave = function mouseLeave() + { + WindowEvents.emit(document, 'HideUrlBar'); + }; + + // -------------------------------------------------------------- + // Mouse Events + + var trackDragging = function _mouseMove(e) + { + var delta = Math.abs(startDragEvent.clientX - e.clientX) > 5 || Math.abs(startDragEvent.clientY - e.clientY) > 5; + if (delta) + { + DragContext.setContext(bookmarkContext); + trackingTelemetry = new Date(); + + offsetRect = container.parentElement.getBoundingClientRect(); + offsetRect.relativeY = offsetRect.y - container.parentElement.offsetTop; + offsetRect.relativeX = offsetRect.x - container.parentElement.offsetLeft; + + bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.relativeY + 'px'; + bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.relativeX + 'px'; + bookmarkContext.DOMRoot.setAttribute('dragging', ''); + + // Events + window.addEventListener('mousemove', mouseDragging); + window.removeEventListener('mousemove', trackDragging); + } + }; + + var cancelDragging = function cancelDragging(e) + { + AutoScroll.stop(); + + bookmarkContext.DOMRoot.setAttribute('dragging', 'drop'); + bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.y + container.scrollTop + 'px'; + bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.x + 'px'; + + setTimeout(function() { + bookmarkContext.DOMRoot.removeAttribute('dragging'); + bookmarkContext.restorePosition(); + }, 50); + window.removeEventListener('mousemove', mouseDragging); + + DragContext.clearContext(); + }; + + var mouseDragging = function mouseMove(e) + { + if (this != window) + return; + + var deltaTime = new Date() - trackingTelemetry; + if (deltaTime < 16) { + return; + } + + var tryScroll = e.clientX > offsetRect.left && e.clientX < offsetRect.right; + + if (tryScroll) + { + if (e.clientY < offsetRect.top) { + AutoScroll.scroll(container, (e.clientY - offsetRect.top)); + } + else if (e.clientY > offsetRect.bottom) { + AutoScroll.scroll(container, (e.clientY - offsetRect.bottom)); + } else { + AutoScroll.stop(); + } + } else { + AutoScroll.stop(); + } + + bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.relativeY + 'px'; + bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.relativeX + 'px'; + trackingTelemetry = new Date(); + }; + + var mouseUp = function _mouseUp(e) + { + switch(SyncModel.state.session) + { + // if restore session preview + case 'restore': { + var bookmarkID = getBookmarkID(e); + + // Test if changing + if (bookmarkID != null) + { + if (bookmarkContext.bookmarkID == bookmarkID) { + // Preview the content if a click event was registered (mouse-up on the same DOM node) + if (!DragContext.hasContext()) { + BookmarkManager.openBookmark({ + url: SessionSyncModel.bookmarks[bookmarkID].url, + mode: BookmarkManager.getOpenMode(0), + favicon: true, + }); + } + } + else + { + if (SessionSyncModel.bookmarks[bookmarkID] != undefined) + { + SessionSyncModel.moveBookmark(bookmarkContext.bookmarkID, bookmarkID); + } + } + } + else + { + if (getTargetType(e) === 'folder') + { + var sessionID = getSessionID(e); + if (sessionID != null && SessionSyncModel.bookmarks[sessionID] != undefined) { + SessionSyncModel.moveBookmarkTo(bookmarkContext.bookmarkID, sessionID); + } + } + } + break; + } + + case 'current': { + let tabID = getTabID(e); + let tabContext = SyncModel.tabs[tabID]; + + if (tabContext) + { + if (tabContext != bookmarkContext) + { + SessionManager.moveTab(bookmarkContext.tab.id, tabContext.tab.index, tabContext.tab.windowId); + } + else + { + if (tabID) { + SessionManager.activateTab(tabID); + var windowID = getWindowID(e); + if (windowID) { + SessionManager.activateWindow(windowID); + } + } + } + } + else + { + if (getTargetType(e) === 'folder') + { + var savingSessionID = getSessionID(e); + if (savingSessionID != null) { + BookmarkManager.createBookmarkFromTab(bookmarkContext.tab, savingSessionID) + .then(function() { + WindowEvents.emit(document, 'Notification', { + message: 'Saved', + }); + }); + } + } + } + } + } + + if (DragContext.hasContext()) + { + cancelDragging(e); + } + else + { + window.removeEventListener('mousemove', trackDragging); + } + + window.removeEventListener('mouseup', mouseUp); + }; + + var bookmarkMouseDown = function _bookmarkMouseDown(e) + { + var target = e.target; + if (DragContext.hasContext()) { + cancelDragging(e); + return; + } + + switch(SyncModel.state.session) + { + // if restore session preview + case 'restore': { + var bookmarkID = getBookmarkID(e); + if (bookmarkID == null) { + return; + } + + bookmarkContext = SyncModel.getBookmark(bookmarkID); + + if (e.button == 0) + { + startDragEvent = e; + window.addEventListener('mousemove', trackDragging); + window.addEventListener('mouseup', mouseUp); + return; + } + + // Middle Click + if (e.button == 1) + { + BookmarkManager.openBookmark({ + url: SessionSyncModel.bookmarks[bookmarkID].url, + mode: BookmarkManager.getOpenMode(1), + favicon: true, + }); + return; + } + + return; + } + + // If active session preview + case 'current': { + + if (e.button == 0) + { + let tabID = getTabID(e); + if (tabID != undefined) { + bookmarkContext = SyncModel.tabs[tabID]; + if (bookmarkContext != undefined) { + startDragEvent = e; + window.addEventListener('mousemove', trackDragging); + window.addEventListener('mouseup', mouseUp); + return; + } + } + else { + let windowID = getWindowID(e); + if (windowID) { + SessionManager.activateWindow(windowID); + } + } + } + return; + } + + // if history session preview + case 'history': { + if (target.hasAttribute('url')) + { + let url = getTabURL(e); + + switch (e.button) + { + case 0: + case 1: { + BookmarkManager.openBookmark({ + url: url, + mode: BookmarkManager.getOpenMode(e.button) + }); + + break; + } + } + } + else + { + if (e.button == 0) + { + let windowID = getWindowID(e); + if (windowID != undefined) { + WindowEvents.emit(document, 'HistorySessionRestoreWindow', windowID); + } + } + } + return; + } + } + }; + + var bookmarkContextMenu = function bookmarkContextMenu(e) + { + switch (SyncModel.state.session) + { + case 'restore': { + var bookmarkID = getBookmarkID(e); + if (bookmarkID == null) + return; + + WindowEvents.emit(document, 'BookmarkCtxMenu-Open', { + context: bookmarkID, + event: e + }); + break; + } + + case 'history': { + let url = getTabURL(e); + if (url) { + WindowEvents.emit(document, 'HistoryMarksCtxMenu-Open', { + context: url, + event: e + }); + } + } + } + }; + + // -------------------------------------------------------------- + // App Events + + + // ------------------------------------------------------------------------ + // Init Code Events + + container.addEventListener('scroll', function () { + AppConfig.set('state.scrollTop.' + SyncModel.state.session, container.scrollTop); + }); + container.addEventListener('contextmenu', bookmarkContextMenu); + container.addEventListener('mousedown', bookmarkMouseDown); + + container.addEventListener('mouseover', mouseOver); + container.addEventListener('mouseleave', mouseLeave); + + container.addEventListener('dblclick', bookmarkDblClick); + }; + + // ***************************************************************************** + // Public API + + exports.SessionBookmarkEvents = SessionBookmarkEvents; +}); \ No newline at end of file diff --git a/data/scripts/components/session-bookmark.js b/data/scripts/components/session-bookmark.js index 95cc679..52e9b7d 100644 --- a/data/scripts/components/session-bookmark.js +++ b/data/scripts/components/session-bookmark.js @@ -5,19 +5,16 @@ define(function(require, exports) { // ***************************************************************************** // Custom Modules - const { AppConfig } = require('../config'); const { HTMLCreator } = require('../utils/dom'); - const { WindowEvents } = require('../utils/global-events'); const { AutoScroll } = require('../utils/auto-scroll'); - const { DragContext } = require('../utils/drag-context'); - const { SessionSyncModel } = require('./session-sync-model'); - const { SessionManager } = require('../session/management'); const { BookmarkManager } = require('../session/bookmarks'); // ***************************************************************************** // API + const bookmarkOffset = 2.2; // height + 2 in em + function setTabFavicon(faviconNode, faviconURL) { faviconNode.style.background = 'url("' + faviconURL + '")'; @@ -26,8 +23,6 @@ define(function(require, exports) { faviconNode.style.backgroundSize = 'contain'; } - const bookmarkOffset = 2.2; // height + 2 in em - /* * Session Bookmark * Used for interacting with a Firefox bookmark item @@ -104,424 +99,6 @@ define(function(require, exports) { AutoScroll.scrollTo(parentNode, offset, 0.25); }; - // ---------------------------------------------------------------------------- - // Session Events - delegated - - var SessionBookmarkEvents = function SessionBookmarkEvents(document, container) - { - var startDragEvent = null; - var trackingTelemetry = new Date(); - var offsetRect = {}; - var bookmarkContext = null; - var bookmarkHoverContextID = 0; - var SyncModel = SessionSyncModel.getModel(document); - - function getAttribute(target, attribute) { - try { - return target.getAttribute(attribute); - } - catch (err) { - console.log(err); - return null; - } - } - - function getTargetType(e) { - return getAttribute(e.target, 'type'); - } - - function getSessionID(e) { - return getAttribute(e.target, 'sessionID'); - } - - function getBookmarkID(e) { - return getAttribute(e.target, 'bookmarkID'); - } - - function getTabURL(e) { - return getAttribute(e.target, 'url'); - } - - function getTabID(e) { - var ID = getAttribute(e.target, 'tabID'); - return ID ? ID | 0 : undefined; - } - - function getWindowID(e) { - var ID = getAttribute(e.target, 'windowID'); - return ID ? ID | 0 : undefined; - } - - // -------------------------------------------------------------- - // Session level - - var bookmarkDblClick = function sessionDblClick(e) - { - if (SyncModel.state.session == 'restore') - { - var bookmarkID = getBookmarkID(e); - if (bookmarkID != null) { - WindowEvents.emit(document, 'BookmarkCtxMenu-EditBookmark', bookmarkID); - } - } - }; - - var mouseOver = function mouseOver(e) - { - if (!e.target.hasAttribute('over')) { - return; - } - - switch(SyncModel.state.session) - { - // Bookmark entry - case 'restore': { - var bookmarkID = getBookmarkID(e); - if (bookmarkHoverContextID == bookmarkID) - return; - - bookmarkHoverContextID = bookmarkID; - WindowEvents.emit(document, 'ShowUrlBar', SessionSyncModel.bookmarks[bookmarkID].url); - return; - } - - // Window tab - case 'current': { - var tabID = getTabID(e); - if (tabID != undefined) { - let url = getTabURL(e); - WindowEvents.emit(document, 'ShowUrlBar', url); - } - return; - } - - // History tab - case 'history': { - let url = getTabURL(e); - WindowEvents.emit(document, 'ShowUrlBar', url); - return; - } - } - }; - - var mouseLeave = function mouseLeave() - { - WindowEvents.emit(document, 'HideUrlBar'); - }; - - // -------------------------------------------------------------- - // Mouse Events - - var trackDragging = function _mouseMove(e) - { - var delta = Math.abs(startDragEvent.clientX - e.clientX) > 5 || Math.abs(startDragEvent.clientY - e.clientY) > 5; - if (delta) - { - DragContext.setContext(bookmarkContext); - trackingTelemetry = new Date(); - - offsetRect = container.parentElement.getBoundingClientRect(); - offsetRect.relativeY = offsetRect.y - container.parentElement.offsetTop; - offsetRect.relativeX = offsetRect.x - container.parentElement.offsetLeft; - - bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.relativeY + 'px'; - bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.relativeX + 'px'; - bookmarkContext.DOMRoot.setAttribute('dragging', ''); - - // Events - window.addEventListener('mousemove', mouseDragging); - window.removeEventListener('mousemove', trackDragging); - } - }; - - var cancelDragging = function cancelDragging(e) - { - AutoScroll.stop(); - - bookmarkContext.DOMRoot.setAttribute('dragging', 'drop'); - bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.y + container.scrollTop + 'px'; - bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.x + 'px'; - - setTimeout(function() { - bookmarkContext.DOMRoot.removeAttribute('dragging'); - bookmarkContext.restorePosition(); - }, 50); - window.removeEventListener('mousemove', mouseDragging); - - DragContext.clearContext(); - }; - - var mouseDragging = function mouseMove(e) - { - if (this != window) - return; - - var deltaTime = new Date() - trackingTelemetry; - if (deltaTime < 16) { - return; - } - - var tryScroll = e.clientX > offsetRect.left && e.clientX < offsetRect.right; - - if (tryScroll) - { - if (e.clientY < offsetRect.top) { - AutoScroll.scroll(container, (e.clientY - offsetRect.top)); - } - else if (e.clientY > offsetRect.bottom) { - AutoScroll.scroll(container, (e.clientY - offsetRect.bottom)); - } else { - AutoScroll.stop(); - } - } else { - AutoScroll.stop(); - } - - bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.relativeY + 'px'; - bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.relativeX + 'px'; - trackingTelemetry = new Date(); - }; - - var mouseUp = function _mouseUp(e) - { - switch(SyncModel.state.session) - { - // if restore session preview - case 'restore': { - var bookmarkID = getBookmarkID(e); - - // Test if changing - if (bookmarkID != null) - { - if (bookmarkContext.bookmarkID == bookmarkID) { - // Preview the content if a click event was registered (mouse-up on the same DOM node) - if (!DragContext.hasContext()) { - BookmarkManager.openBookmark({ - url: SessionSyncModel.bookmarks[bookmarkID].url, - mode: BookmarkManager.getOpenMode(0), - favicon: true, - }); - } - } - else - { - if (SessionSyncModel.bookmarks[bookmarkID] != undefined) - { - SessionSyncModel.moveBookmark(bookmarkContext.bookmarkID, bookmarkID); - } - } - } - else - { - if (getTargetType(e) === 'folder') - { - var sessionID = getSessionID(e); - if (sessionID != null && SessionSyncModel.bookmarks[sessionID] != undefined) { - SessionSyncModel.moveBookmarkTo(bookmarkContext.bookmarkID, sessionID); - } - } - } - break; - } - - case 'current': { - let tabID = getTabID(e); - let tabContext = SyncModel.tabs[tabID]; - - if (tabContext) - { - if (tabContext != bookmarkContext) - { - SessionManager.moveTab(bookmarkContext.tab.id, tabContext.tab.index, tabContext.tab.windowId); - } - else - { - if (tabID) { - SessionManager.activateTab(tabID); - var windowID = getWindowID(e); - if (windowID) { - SessionManager.activateWindow(windowID); - } - } - } - } - else - { - if (getTargetType(e) === 'folder') - { - var savingSessionID = getSessionID(e); - if (savingSessionID != null) { - BookmarkManager.createBookmarkFromTab(bookmarkContext.tab, savingSessionID) - .then(function() { - WindowEvents.emit(document, 'Notification', { - message: 'Saved', - }); - }); - } - } - } - } - } - - if (DragContext.hasContext()) - { - cancelDragging(e); - } - else - { - window.removeEventListener('mousemove', trackDragging); - } - - window.removeEventListener('mouseup', mouseUp); - }; - - var bookmarkMouseDown = function _bookmarkMouseDown(e) - { - var target = e.target; - if (DragContext.hasContext()) { - cancelDragging(e); - return; - } - - switch(SyncModel.state.session) - { - // if restore session preview - case 'restore': { - var bookmarkID = getBookmarkID(e); - if (bookmarkID == null) { - return; - } - - bookmarkContext = SyncModel.getBookmark(bookmarkID); - - if (e.button == 0) - { - startDragEvent = e; - window.addEventListener('mousemove', trackDragging); - window.addEventListener('mouseup', mouseUp); - return; - } - - // Middle Click - if (e.button == 1) - { - BookmarkManager.openBookmark({ - url: SessionSyncModel.bookmarks[bookmarkID].url, - mode: BookmarkManager.getOpenMode(1), - favicon: true, - }); - return; - } - - return; - } - - // If active session preview - case 'current': { - - if (e.button == 0) - { - let tabID = getTabID(e); - if (tabID != undefined) { - bookmarkContext = SyncModel.tabs[tabID]; - if (bookmarkContext != undefined) { - startDragEvent = e; - window.addEventListener('mousemove', trackDragging); - window.addEventListener('mouseup', mouseUp); - return; - } - } - else { - let windowID = getWindowID(e); - if (windowID) { - SessionManager.activateWindow(windowID); - } - } - } - return; - } - - // if history session preview - case 'history': { - if (target.hasAttribute('url')) - { - let url = getTabURL(e); - - switch (e.button) - { - case 0: - case 1: { - BookmarkManager.openBookmark({ - url: url, - mode: BookmarkManager.getOpenMode(e.button) - }); - - break; - } - } - } - else - { - if (e.button == 0) - { - let windowID = getWindowID(e); - if (windowID != undefined) { - WindowEvents.emit(document, 'HistorySessionRestoreWindow', windowID); - } - } - } - return; - } - } - }; - - var bookmarkContextMenu = function bookmarkContextMenu(e) - { - switch (SyncModel.state.session) - { - case 'restore': { - var bookmarkID = getBookmarkID(e); - if (bookmarkID == null) - return; - - WindowEvents.emit(document, 'BookmarkCtxMenu-Open', { - context: bookmarkID, - event: e - }); - break; - } - - case 'history': { - let url = getTabURL(e); - if (url) { - WindowEvents.emit(document, 'HistoryMarksCtxMenu-Open', { - context: url, - event: e - }); - } - } - } - }; - - // -------------------------------------------------------------- - // App Events - - - // ------------------------------------------------------------------------ - // Init Code Events - - container.addEventListener('scroll', function () { - AppConfig.set('state.scrollTop.' + SyncModel.state.session, container.scrollTop); - }); - container.addEventListener('contextmenu', bookmarkContextMenu); - container.addEventListener('mousedown', bookmarkMouseDown); - - container.addEventListener('mouseover', mouseOver); - container.addEventListener('mouseleave', mouseLeave); - - container.addEventListener('dblclick', bookmarkDblClick); - }; - /* * SessionWindow * Used as separator for different windows @@ -593,7 +170,7 @@ define(function(require, exports) { * Used as separator for different windows */ - function HistoryWindow(windowID, index) + function HistoryWindow(sessionWindowInfo, windowID, index) { var DomElem = HTMLCreator(); @@ -605,6 +182,10 @@ define(function(require, exports) { text.setAttribute('windowID', windowID); text.textContent = 'Window ' + windowID; + if (!sessionWindowInfo.active) { + text.textContent += ' (closed)'; + } + box.appendChild(text); this.DOMRoot = box; @@ -674,7 +255,6 @@ define(function(require, exports) { // Public API exports.SessionBookmark = SessionBookmark; - exports.SessionBookmarkEvents = SessionBookmarkEvents; exports.SessionTab = SessionTab; exports.SessionWindow = SessionWindow; exports.HistoryWindow = HistoryWindow; diff --git a/data/scripts/components/session-container.js b/data/scripts/components/session-container.js index 13884dc..001fc59 100644 --- a/data/scripts/components/session-container.js +++ b/data/scripts/components/session-container.js @@ -6,7 +6,8 @@ define(function(require, exports) { // App const { AppConfig } = require('../config'); - const { SessionBookmark, SessionBookmarkEvents, SessionTab, SessionWindow, HistoryWindow, HistoryTab } = require('./session-bookmark'); + const { SessionBookmark, SessionTab, SessionWindow, HistoryWindow, HistoryTab } = require('./session-bookmark'); + const { SessionBookmarkEvents } = require('./session-bookmark-events'); const { SessionSyncModel } = require('./session-sync-model'); // Utils @@ -49,18 +50,38 @@ define(function(require, exports) { SessionSyncModel.bookmarks[ID].parentId == AppConfig.get('storageID')); } + function restoreHistoryWindow(sessionInfo) + { + var list = []; + sessionInfo.windows.forEach(historyWindow => { + list.push(historyWindow.tabs); + }); + SessionManager.loadBookmarksNewWindow(list); + } + WindowEvents.on(document, 'HistorySessionSave', this.saveHistorySession.bind(this)); WindowEvents.on(document, 'HistorySessionRestore', function (index) { - SessionHistory.getHistory(function (historySessions) { - SessionManager.loadBookmarksNewWindow(historySessions[index].windows); + SessionHistory.getHistorySession(index, function (sessionInfo) { + restoreHistoryWindow(sessionInfo); }); - - }.bind(this)); + }); WindowEvents.on(document, 'HistorySessionRestoreWindow', function (windowID) { if (this.SyncModel.state.session === 'history') { - SessionManager.loadBookmarksNewWindow([this.selectedHistorySession.windows[windowID]]); + if (this.selectedHistorySession.active == true) + { + var activeSessionWindows = Object.values(this.selectedHistorySession.windows); + if (activeSessionWindows[windowID].active) { + SessionManager.activateWindow(activeSessionWindows[windowID].id); + } + else { + SessionManager.loadBookmarksNewWindow([activeSessionWindows[windowID].tabs]); + } + } + else { + SessionManager.loadBookmarksNewWindow([this.selectedHistorySession.windows[windowID].tabs]); + } } }.bind(this)); @@ -78,7 +99,7 @@ define(function(require, exports) { // history session if (this.SyncModel.state.session === 'history') { - SessionManager.loadBookmarksNewWindow(this.selectedHistorySession.windows); + restoreHistoryWindow(this.selectedHistorySession); } }.bind(this)); @@ -166,7 +187,7 @@ define(function(require, exports) { // Tracked events - SessionBookmarkEvents(document, bookmarks); + SessionBookmarkEvents(bookmarks); var updateTimeout = null; function updateOnTabEvent() @@ -183,12 +204,10 @@ define(function(require, exports) { } // Tab updates tracking - if (AppConfig.isAddonContext()) { - var trackedEvents = ['onUpdated', 'onCreated', 'onDetached', 'onMoved', 'onAttached', 'onRemoved']; - trackedEvents.forEach(function (eventType) { - browser.tabs[eventType].addListener(updateOnTabEvent); - }.bind(this)); - } + var trackedEvents = ['onUpdated', 'onCreated', 'onDetached', 'onMoved', 'onAttached', 'onRemoved']; + trackedEvents.forEach(function (eventType) { + browser.tabs[eventType].addListener(updateOnTabEvent); + }.bind(this)); // ------------------------------------------------------------------------ // Public data @@ -282,19 +301,20 @@ define(function(require, exports) { var tabIndex = 0; var windowIndex = 0; - for (let historyTabs of sessionInfo.windows) + for (let key in sessionInfo.windows) { // Add window separator - var sessionWindow = new HistoryWindow(windowIndex, tabIndex); + var sessionWindow = new HistoryWindow(sessionInfo.windows[key], windowIndex, tabIndex); this.DOMBookmarks.appendChild(sessionWindow.DOMRoot); tabIndex++; var globalOffset = tabIndex; // Add window tabs - for (var i in historyTabs) + var windowTabs = sessionInfo.windows[key].tabs; + for (var i in windowTabs) { - var tab = historyTabs[i]; + var tab = windowTabs[i]; tab.index = i | 0; var sessionTab = new HistoryTab(tab, globalOffset); this.DOMBookmarks.appendChild(sessionTab.DOMRoot); @@ -383,37 +403,16 @@ define(function(require, exports) { { if (sessionIndex == undefined) { - save(this.selectedHistorySession); + // When Save button from toolbar is pressed + SessionManager.saveHistorySession(this.selectedHistorySession); } else { - SessionHistory.getHistory(function (historySessions) { - save(historySessions[sessionIndex]); + // Save from context menu + SessionHistory.getHistorySession(sessionIndex, function (sessionInfo) { + SessionManager.saveHistorySession(sessionInfo); }); } - - function save(sessionInfo) { - var windowCount = sessionInfo.windows.length; - - var windowID = 0; - - function onSave(bookmark) { - WindowEvents.emit(document, 'SetPromiseSession', { sessionID: bookmark.id, edit: false, update: true } ); - } - - // save all history windows - for (let tabs of sessionInfo.windows) - { - var date = new Date(sessionInfo.lastSave); - var sessionTitle = date.toLocaleString() + ((windowCount > 1) ? (' #' + windowID) : ''); - windowID++; - - SessionManager.saveHistorySession({ - title: sessionTitle, - tabs: tabs - }, onSave); - } - } }; // ***************************************************************************** diff --git a/data/scripts/components/session-filtering.js b/data/scripts/components/session-filtering.js index 7a3bd96..706709a 100644 --- a/data/scripts/components/session-filtering.js +++ b/data/scripts/components/session-filtering.js @@ -45,7 +45,7 @@ define(function(require, exports) { if (searchInput.value.length) { prevValue = ''; searchInput.value = prevValue; - WindowEvents.emit(document, 'FilterSessions', searchInput.value.toLowerCase()); + WindowEvents.emit(document, 'FilterSessions', searchInput.value); } searchInput.blur(); @@ -66,7 +66,7 @@ define(function(require, exports) { WindowEvents.emit(document, 'SetUIState', {'search-box-content': searchInput.value != ''}); clearTimeout(timeOut); timeOut = setTimeout(function () { - WindowEvents.emit(document, 'FilterSessions', searchInput.value.toLowerCase()); + WindowEvents.emit(document, 'FilterSessions', searchInput.value); }, 200); }; diff --git a/data/scripts/components/session-folder-events.js b/data/scripts/components/session-folder-events.js new file mode 100644 index 0000000..923f45f --- /dev/null +++ b/data/scripts/components/session-folder-events.js @@ -0,0 +1,272 @@ +define(function(require, exports) { + 'use strict'; + + // ***************************************************************************** + // Custom Modules + + // App + const { AppConfig } = require('../config'); + const { SessionSyncModel } = require('./session-sync-model'); + + // Utils + const { WindowEvents } = require('../utils/global-events'); + const { AutoScroll } = require('../utils/auto-scroll'); + const { DragContext } = require('../utils/drag-context'); + + // ***************************************************************************** + // API + + // ---------------------------------------------------------------------------- + // Session Folder Events - delegated + + var SessionFolderEvents = function SessionFolderEvents(container) + { + var startDragEvent = null; + var trackingTelemetry; + var offsetRect = {}; + var bookmarkContext = null; + var SyncModel = SessionSyncModel.getModel(document); + + function getAttribute(target, attribute) { + try { + return target.getAttribute(attribute); + } + catch (err) { + console.log(err); + return null; + } + } + + function getBookmarkID(e) { + return getAttribute(e.target, 'sessionID'); + } + + // -------------------------------------------------------------- + // Drag Events + + var trackDragging = function _mouseMove(e) + { + var delta = Math.abs(startDragEvent.clientX - e.clientX) > 5 || Math.abs(startDragEvent.clientY - e.clientY) > 5; + if (delta) { + DragContext.setContext(bookmarkContext); + trackingTelemetry = new Date(); + + offsetRect = container.parentElement.parentElement.getBoundingClientRect(); + offsetRect.relativeY = offsetRect.y - container.parentElement.offsetTop; + offsetRect.relativeX = offsetRect.x - container.parentElement.offsetLeft; + + bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.relativeY + 'px'; + bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.relativeX + 'px'; + bookmarkContext.DOMRoot.setAttribute('dragging', ''); + + // Events + window.addEventListener('mousemove', mouseDragging); + window.removeEventListener('mousemove', trackDragging); + } + }; + + var cancelDragging = function cancelDragging(e) + { + AutoScroll.stop(); + + bookmarkContext.DOMRoot.setAttribute('dragging', 'drop'); + bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.y + container.scrollTop + 'px'; + bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.x + 'px'; + + setTimeout(function() { + bookmarkContext.DOMRoot.removeAttribute('dragging'); + bookmarkContext.restorePosition(); + }, 50); + window.removeEventListener('mousemove', mouseDragging); + + DragContext.clearContext(); + }; + + var mouseDragging = function mouseMove(e) + { + if (this != window) + return; + var deltaTime = new Date() - trackingTelemetry; + if (deltaTime < 16) { + return; + } + + var tryScroll = e.clientX > offsetRect.left && e.clientX < offsetRect.right; + + if (tryScroll) + { + if (e.clientY < offsetRect.top) { + AutoScroll.scroll(container, (e.clientY - offsetRect.top)); + } + else if (e.clientY > offsetRect.bottom) { + AutoScroll.scroll(container, (e.clientY - offsetRect.bottom)); + } else { + AutoScroll.stop(); + } + } else { + AutoScroll.stop(); + } + + bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.relativeY + 'px'; + bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.relativeX + 'px'; + trackingTelemetry = new Date(); + }; + + var mouseUp = function _mouseUp(e) + { + // Test if changing + if (e.target.className == 'folder') + { + var sessionID = getBookmarkID(e); + + // Preview the content if a click event was registered (mouse-up on the same DOM node) + if (bookmarkContext.bookmarkID == sessionID) + { + if (!DragContext.hasContext()) { + WindowEvents.emit(document, 'SelectSyncSession', bookmarkContext); + } + } + else + { + if (AppConfig.get('session.sorting') === 'position-asc') + { + SessionSyncModel.moveBookmark(bookmarkContext.bookmarkID, sessionID); + } + else + { + WindowEvents.emit(document, 'Notification', { + message: 'Rearage function available only when items are sorted ascending by position', + timeout: 3000, + }); + } + } + } + + if (DragContext.hasContext()) + { + cancelDragging(e); + } + else + { + window.removeEventListener('mousemove', trackDragging); + } + + window.removeEventListener('mouseup', mouseUp); + }; + + var folderMouseDown = function _folderMouseDown(e) + { + if (DragContext.hasContext()) { + cancelDragging(e); + return; + } + + var sessionID = getBookmarkID(e); + bookmarkContext = SyncModel.getBookmark(sessionID); + + if (e.button == 0) { + startDragEvent = e; + window.addEventListener('mousemove', trackDragging); + window.addEventListener('mouseup', mouseUp); + return; + } + }; + + var folderContextMenu = function folderContextMenu(e) + { + if (bookmarkContext) + { + WindowEvents.emit(document, 'SessionContextMenu-Open', { + context: bookmarkContext.bookmark.id, + event: e + }); + } + else { + WindowEvents.emit(document, 'SessionListMenu-Open', { event: e }); + } + }; + + var sessionDblClick = function sessionDblClick(e) + { + if (bookmarkContext) + { + var type = e.target.getAttribute('type'); + switch (type) + { + case 'folder': + WindowEvents.emit(document, 'SessionContextMenu-EditSession', bookmarkContext.bookmarkID); + break; + + case 'bookmark': + WindowEvents.emit(document, 'BookmarkCtxMenu-EditBookmark', bookmarkContext.bookmarkID); + break; + } + } + }; + + // ------------------------------------------------------------------------ + // Init Code Events + + container.addEventListener('dblclick', sessionDblClick); + container.addEventListener('mousedown', folderMouseDown); + container.addEventListener('contextmenu', folderContextMenu); + }; + + // ---------------------------------------------------------------------------- + // History Session Folder Events - delegated + + var SessionHistoryEvents = function SessionHistoryEvents(container) + { + var historyContext = null; + + function getAttribute(target, attribute) { + try { + return target.getAttribute(attribute); + } + catch (err) { + console.log(err); + return null; + } + } + + var onMouseDown = function onMouseDown(e) + { + if (e.button != 2) + return; + + var target = e.target; + if (target.className == 'history-node') { + WindowEvents.emit(document, 'HistorySessionCtxMenu-Open', { + context: e.target, + event: e + }); + } + else + { + WindowEvents.emit(document, 'HistoryListCtxMenu-Open', {event: e }); + } + }; + + var onClick = function onClick(e) + { + if (e.target.className === 'history-node') + { + // WindowEvents.emit(document, 'HistoryListCtxMenu-Open', {event: e }); + // this.selectHistorySession(e.target); + } + }; + + // ------------------------------------------------------------------------ + // Init Code Events + + container.addEventListener('click', onClick); + container.addEventListener('mousedown', onMouseDown); + }; + + // ***************************************************************************** + // Public API + + exports.SessionFolderEvents = SessionFolderEvents; + exports.SessionHistoryEvents = SessionHistoryEvents; + +}); \ No newline at end of file diff --git a/data/scripts/components/session-folder.js b/data/scripts/components/session-folder.js index 5598c01..53b2fbc 100644 --- a/data/scripts/components/session-folder.js +++ b/data/scripts/components/session-folder.js @@ -4,15 +4,8 @@ define(function(require, exports) { // ***************************************************************************** // Custom Modules - // App - const { AppConfig } = require('../config'); - const { SessionSyncModel } = require('./session-sync-model'); - // Utils const { HTMLCreator } = require('../utils/dom'); - const { WindowEvents } = require('../utils/global-events'); - const { AutoScroll } = require('../utils/auto-scroll'); - const { DragContext } = require('../utils/drag-context'); // ***************************************************************************** // API @@ -98,203 +91,6 @@ define(function(require, exports) { box.parentElement.removeChild(box); }; - // ---------------------------------------------------------------------------- - // Session Events - delegated - - var SessionFolderEvents = function SessionFolderEvents(document, container, SessionList) - { - var startDragEvent = null; - var trackingTelemetry; - var offsetRect = {}; - var bookmarkContext = null; - var SyncModel = SessionSyncModel.getModel(document); - - function getAttribute(target, attribute) { - try { - return target.getAttribute(attribute); - } - catch (err) { - console.log(err); - return null; - } - } - - function getBookmarkID(e) { - return getAttribute(e.target, 'sessionID'); - } - - // -------------------------------------------------------------- - // Drag Events - - var trackDragging = function _mouseMove(e) - { - var delta = Math.abs(startDragEvent.clientX - e.clientX) > 5 || Math.abs(startDragEvent.clientY - e.clientY) > 5; - if (delta) { - DragContext.setContext(bookmarkContext); - trackingTelemetry = new Date(); - - offsetRect = container.parentElement.parentElement.getBoundingClientRect(); - offsetRect.relativeY = offsetRect.y - container.parentElement.offsetTop; - offsetRect.relativeX = offsetRect.x - container.parentElement.offsetLeft; - - bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.relativeY + 'px'; - bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.relativeX + 'px'; - bookmarkContext.DOMRoot.setAttribute('dragging', ''); - - // Events - window.addEventListener('mousemove', mouseDragging); - window.removeEventListener('mousemove', trackDragging); - } - }; - - var cancelDragging = function cancelDragging(e) - { - AutoScroll.stop(); - - bookmarkContext.DOMRoot.setAttribute('dragging', 'drop'); - bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.y + container.scrollTop + 'px'; - bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.x + 'px'; - - setTimeout(function() { - bookmarkContext.DOMRoot.removeAttribute('dragging'); - bookmarkContext.restorePosition(); - }, 50); - window.removeEventListener('mousemove', mouseDragging); - - DragContext.clearContext(); - }; - - var mouseDragging = function mouseMove(e) - { - if (this != window) - return; - var deltaTime = new Date() - trackingTelemetry; - if (deltaTime < 16) { - return; - } - - var tryScroll = e.clientX > offsetRect.left && e.clientX < offsetRect.right; - - if (tryScroll) - { - if (e.clientY < offsetRect.top) { - AutoScroll.scroll(container, (e.clientY - offsetRect.top)); - } - else if (e.clientY > offsetRect.bottom) { - AutoScroll.scroll(container, (e.clientY - offsetRect.bottom)); - } else { - AutoScroll.stop(); - } - } else { - AutoScroll.stop(); - } - - bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.relativeY + 'px'; - bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.relativeX + 'px'; - trackingTelemetry = new Date(); - }; - - var mouseUp = function _mouseUp(e) - { - // Test if changing - if (e.target.className == 'folder') - { - var sessionID = getBookmarkID(e); - - // Preview the content if a click event was registered (mouse-up on the same DOM node) - if (bookmarkContext.bookmarkID == sessionID) - { - if (!DragContext.hasContext()) { - SessionList.selectSyncSession(bookmarkContext, false); - } - } - else - { - if (AppConfig.get('session.sorting') === 'position-asc') - { - SessionSyncModel.moveBookmark(bookmarkContext.bookmarkID, sessionID); - } - else - { - WindowEvents.emit(document, 'Notification', { - message: 'Rearage function available only when items are sorted ascending by position', - timeout: 3000, - }); - } - } - } - - if (DragContext.hasContext()) - { - cancelDragging(e); - } - else - { - window.removeEventListener('mousemove', trackDragging); - } - - window.removeEventListener('mouseup', mouseUp); - }; - - var folderMouseDown = function _folderMouseDown(e) - { - if (DragContext.hasContext()) { - cancelDragging(e); - return; - } - - var sessionID = getBookmarkID(e); - bookmarkContext = SyncModel.getBookmark(sessionID); - - if (e.button == 0) { - startDragEvent = e; - window.addEventListener('mousemove', trackDragging); - window.addEventListener('mouseup', mouseUp); - return; - } - }; - - var folderContextMenu = function folderContextMenu(e) - { - if (bookmarkContext) - { - WindowEvents.emit(document, 'SessionContextMenu-Open', { - context: bookmarkContext.bookmark.id, - event: e - }); - } - else { - WindowEvents.emit(document, 'SessionListMenu-Open', { event: e }); - } - }; - - var sessionDblClick = function sessionDblClick(e) - { - if (bookmarkContext) - { - var type = e.target.getAttribute('type'); - switch (type) - { - case 'folder': - WindowEvents.emit(document, 'SessionContextMenu-EditSession', bookmarkContext.bookmarkID); - break; - - case 'bookmark': - WindowEvents.emit(document, 'BookmarkCtxMenu-EditBookmark', bookmarkContext.bookmarkID); - break; - } - } - }; - - // ------------------------------------------------------------------------ - // Init Code Events - - container.addEventListener('dblclick', sessionDblClick); - container.addEventListener('mousedown', folderMouseDown); - container.addEventListener('contextmenu', folderContextMenu); - }; - - var SortMethods = { 'name-asc' : function sortByNameAscending(a, b) { @@ -322,10 +118,35 @@ define(function(require, exports) { } }; + + /* + * SessionHistoryFolder + * Used as separator for different windows + */ + + function SessionHistoryFolder(sessionInfo, index) + { + var DomElem = HTMLCreator(); + var folder = DomElem('div', { class: 'history-node' }); + + folder.textContent = new Date(sessionInfo.lastSave).toLocaleString(); + folder.setAttribute('index', index); + + // Public data + + this.DOMRoot = folder; + this.sessionInfo = sessionInfo; + } + + SessionHistoryFolder.prototype.setTitle = function setTitle(title) + { + this.DOMRoot.textContent = title; + }; + // ***************************************************************************** // Public API exports.SessionFolder = SessionFolder; exports.SessionFolderSortBy = SortMethods; - exports.SessionFolderEvents = SessionFolderEvents; + exports.SessionHistoryFolder = SessionHistoryFolder; }); \ No newline at end of file diff --git a/data/scripts/components/session-history.js b/data/scripts/components/session-history.js index 9fde48a..cabcdb0 100644 --- a/data/scripts/components/session-history.js +++ b/data/scripts/components/session-history.js @@ -12,37 +12,56 @@ define(function(require, exports) { var SessionHistory = (function SessionHistory() { - var configKey = 'history.config'; - var sessionsKey = 'history.sessions'; + var storageKey = { + config: 'history.config', + active: 'history.active', + list: 'history.sessions' + }; + + var sessions = []; - var getHistory = function getHistory(callback) + var init = function init() { - if (AppConfig.isAddonContext()) { - browser.storage.local.get(sessionsKey).then(function (obj) { - if(obj[sessionsKey]) { - callback(obj[sessionsKey]); - } + browser.storage.local.get(storageKey.list).then(function (obj) { + if(obj[storageKey.list]) { + sessions = obj[storageKey.list]; + } + }); + }; + + var getFullHistory = function getFullHistory(callback) + { + browser.storage.local.get(storageKey.active).then(function (obj) { + callback(sessions, obj[storageKey.active]); + }); + }; + + var getHistorySession = function getHistorySession(index, callback) { + if (index >=0 && index < sessions.length) { + callback(sessions[index]); + } + else { + browser.storage.local.get(storageKey.active).then(function (obj) { + callback(obj[storageKey.active]); }); } }; var getConfig = function getConfig(callback) { - if (AppConfig.isAddonContext()) { - browser.storage.local.get(configKey).then(function (obj) { - callback(obj[configKey]); - }); - } + browser.storage.local.get(storageKey.config).then(function (obj) { + callback(obj[storageKey.config]); + }); }; var updateConfig = function updateConfig(info) { - browser.storage.local.set({ [configKey] : info}); + browser.storage.local.set({ [storageKey.config] : info}); }; var updateSessions = function updateSessions(sessions) { - browser.storage.local.set({ [sessionsKey] : sessions}) + browser.storage.local.set({ [storageKey.list] : sessions}) .then(function () { WindowEvents.emit(document, 'ShowHistoryList'); }); @@ -50,7 +69,7 @@ define(function(require, exports) { GlobalEvents.on('HistorySessionDelete', function(index) { - getHistory(function (sessions) { + getFullHistory(function (sessions) { if (index >= 0 && index < sessions.length) { if (index == sessions.length - 1) { @@ -73,8 +92,10 @@ define(function(require, exports) { // Public API return { + init: init, getConfig: getConfig, - getHistory: getHistory, + getFullHistory: getFullHistory, + getHistorySession: getHistorySession, updateConfig: updateConfig }; diff --git a/data/scripts/components/session-list.js b/data/scripts/components/session-list.js index 7d0298d..3b0a237 100644 --- a/data/scripts/components/session-list.js +++ b/data/scripts/components/session-list.js @@ -10,7 +10,8 @@ define(function(require, exports) { const { WindowEvents, GlobalEvents } = require('../utils/global-events'); const { AutoScroll } = require('../utils/auto-scroll'); - const { SessionFolder, SessionFolderSortBy, SessionFolderEvents } = require('./session-folder'); + const { SessionFolder, SessionHistoryFolder, SessionFolderSortBy } = require('./session-folder'); + const { SessionFolderEvents, SessionHistoryEvents } = require('./session-folder-events'); const { SessionSyncModel } = require('./session-sync-model'); const { SessionHistory } = require('./session-history'); @@ -40,8 +41,8 @@ define(function(require, exports) { root.appendChild(historyList); // Session list - var list = DomElem('div', {class: 'list'}); - root.appendChild(list); + var sessionList = DomElem('div', {class: 'session-list'}); + root.appendChild(sessionList); // ------------------------------------------------------------------------ // Events @@ -85,6 +86,10 @@ define(function(require, exports) { WindowEvents.emit(document, 'ShowSyncList', { update: sessionInfo.update }); }.bind(this)); + WindowEvents.on(document, 'SelectSyncSession', function(context) { + this.selectSyncSession(context, false); + }.bind(this)); + WindowEvents.on(document, 'FilterSessions', this.filterSessions.bind(this)); WindowEvents.on(document, 'SortSessionsBy', this.sortSessionsBy.bind(this)); @@ -143,6 +148,10 @@ define(function(require, exports) { } }.bind(this)); + WindowEvents.on(document, 'SelectHistorySession', function(context) { + this.selectHistorySession(context); + }.bind(this)); + AppConfig.onChange('session.selected', function(value) { if (value) { WindowEvents.emit(document, 'SetPromiseSession', { sessionID: value, update: false } ); @@ -150,16 +159,18 @@ define(function(require, exports) { }); // ------------------------------------------------------------------------ - // Init + // Event Initialization - SessionFolderEvents(document, list, this); + SessionFolderEvents(sessionList); + SessionHistoryEvents(historyList); + SessionHistory.init(); // ------------------------------------------------------------------------ // Public data this.sessions = []; this.promiseInfo = {}; - this.DOMList = list; + this.DOMList = sessionList; this.DOMRoot = root; this.document = document; @@ -184,79 +195,48 @@ define(function(require, exports) { // ------------------------------------------------------------------------ // Events - function createHistoryList() + function refreshHistoryList() { var snap = AppConfig.isInitState(); var selectedIndex = AppConfig.get('session.history.selected'); historyList.textContent = ''; - SessionHistory.getHistory(function (historySessions) { + SessionHistory.getFullHistory(function (historySessions, activeSession) { + + if (activeSession) { + let activeSnapshot = new SessionHistoryFolder(activeSession, -1); + activeSnapshot.setTitle('Active snapshot'); + historyList.appendChild(activeSnapshot.DOMRoot); + } + for (let i = historySessions.length - 1; i >= 0; i--) { if (historySessions[i] && historySessions[i].tabCount) { - var node = DomElem('div', {class: 'history-node'}); - node.textContent = new Date(historySessions[i].lastSave).toLocaleString(); - if (historySessions[i].active == true) { - node.textContent = 'Current session'; - } - node.setAttribute('index', i); - historyList.appendChild(node); + let historySession = new SessionHistoryFolder(historySessions[i], i); + historyList.appendChild(historySession.DOMRoot); if (i == selectedIndex) { - selectHistorySession(node); + var node = historySession.DOMRoot; AutoScroll.scrollTo(historyList, node.offsetTop - historyList.parentNode.clientHeight / 2 + node.clientHeight, snap === true ? 0 : 0.25); } } } - }); + }.bind(this)); } - var selectHistorySession = function selectHistorySession(node) - { - this.setSelectedNode(node); - var index = node.getAttribute('index') | 0; - - SessionHistory.getHistory(function (sessions) { - if (AppConfig.get('session.history.selected') != index) { - AppConfig.set('session.history.selected', index); - AppConfig.set('state.scrollTop.history', 0); - } - - WindowEvents.emit(document, 'ShowHistorySession', sessions[index]); - }); - }.bind(this); - GlobalEvents.on('UpdateHistoryList', function() { - createHistoryList(); + refreshHistoryList(); }); WindowEvents.on(document, 'ShowHistoryList', function() { this.setState('history'); - createHistoryList(); + refreshHistoryList(); }.bind(this)); - historyList.addEventListener('mousedown', function(e) { - - if (e.button != 2) - return; - - var target = e.target; - if (target.className == 'history-node') { - WindowEvents.emit(document, 'HistorySessionCtxMenu-Open', { - context: e.target, - event: e - }); - } - else - { - WindowEvents.emit(document, 'HistoryListCtxMenu-Open', {event: e }); - } - }); - historyList.addEventListener('click', function(e) { if (e.target.className === 'history-node') { - selectHistorySession(e.target); + this.selectHistorySession(e.target); } }.bind(this)); @@ -266,6 +246,21 @@ define(function(require, exports) { return historyList; }; + SessionList.prototype.selectHistorySession = function selectHistorySession(node) + { + this.setSelectedNode(node); + var index = node.getAttribute('index') | 0; + + SessionHistory.getHistorySession(index, function (sessionInfo) { + WindowEvents.emit(document, 'ShowHistorySession', sessionInfo); + + if (AppConfig.get('session.history.selected') != index) { + AppConfig.set('session.history.selected', index); + AppConfig.set('state.scrollTop.history', 0); + } + }); + }; + SessionList.prototype.setSessions = function setSessions() { this.sessions = []; @@ -385,6 +380,7 @@ define(function(require, exports) { if (expression.length) { + expression = expression.toLowerCase(); var exactMatch = false; var expLength = expression.length; var document = this.document; diff --git a/data/scripts/components/session-sync-model.js b/data/scripts/components/session-sync-model.js index 5d338c3..88080ea 100644 --- a/data/scripts/components/session-sync-model.js +++ b/data/scripts/components/session-sync-model.js @@ -311,7 +311,6 @@ define(function(require, exports) { undoEvents.push(undoEvent); AppConfig.set('undo.events', undoEvents); - GlobalEvents.emit('TrashCan-Items', undoEvents.length); GlobalEvents.emit('BookmarkDeleted', bookmarkID); // Trigger reordering @@ -367,7 +366,6 @@ define(function(require, exports) { AppConfig.set('undo.events', undoEvents); WindowEvents.emit(document, 'SetPromiseSession', { sessionID: sessionID, update: false }); GlobalEvents.emit('update-sessions'); - GlobalEvents.emit('TrashCan-Items', undoEvents.length); } if (undoEvents.length) diff --git a/data/scripts/components/trash-can.js b/data/scripts/components/trash-can.js index 6b02e23..522d289 100644 --- a/data/scripts/components/trash-can.js +++ b/data/scripts/components/trash-can.js @@ -22,16 +22,30 @@ define(function(require, exports) { var DomElem = HTMLCreator(); // ------------------------------------------------------------------------ - var block = DomElem('div', {class: 'trash-can', tooltip: 'Drop item to delete'}); - block.setAttribute('items', '0'); + var block = DomElem('div', { + class: 'trash-can', + items: 0, + dismiss: true, + tooltip: 'Drop item to delete' + }); var count = DomElem('div', {class: 'count', tooltip: 'Click to restore'}); count.textContent = '0'; block.appendChild(count); - var dismiss = DomElem('div', {class: 'dismiss css-close-button', tooltip: 'Hide'}); + var dismiss = DomElem('div', { + class: 'dismiss css-close-button', + tooltip: 'Hide' + }); + block.appendChild(dismiss); + var configKey = { + hide: 'trashcan.hide', + hideCount: 'trashcan.hide-count', + undoEvents: 'undo.events', + }; + // ------------------------------------------------------------------------ // Events @@ -43,17 +57,9 @@ define(function(require, exports) { function updateItemCount(value) { count.textContent = value; block.setAttribute('items', value); - block.removeAttribute('dismiss'); + AppConfig.set(configKey.hide, value == 0 || value == AppConfig.get(configKey.hideCount)); } - GlobalEvents.on('TrashCan-Items', function(value) { - updateItemCount(value); - }); - - GlobalEvents.on('TrashCan-Dismiss', function() { - block.setAttribute('dismiss', ''); - }); - block.addEventListener('mouseup', function (e) { var item = DragContext.getContext(); if (item && (item instanceof SessionFolder || item instanceof SessionBookmark)) { @@ -66,8 +72,9 @@ define(function(require, exports) { } }); - dismiss.addEventListener('click', function (e) { - GlobalEvents.emit('TrashCan-Dismiss'); + dismiss.addEventListener('click', function () { + AppConfig.set(configKey.hide, true); + AppConfig.set(configKey.hideCount, AppConfig.get(configKey.undoEvents).length); }); // Tooltip events @@ -78,7 +85,7 @@ define(function(require, exports) { }); }); - block.addEventListener('mouseleave', function(e) { + block.addEventListener('mouseleave', function() { WindowEvents.emit(document, 'HideTooltip'); }); @@ -89,14 +96,17 @@ define(function(require, exports) { // ------------------------------------------------------------------------ // Init code - block.setAttribute('dismiss', ''); // ------------------------------------------------------------------------ // Events - AppConfig.onChange('undo.events', function (value) { - updateItemCount(value ? value.length : 0); - }) + AppConfig.onChange(configKey.hide, function (value) { + block.setAttribute('dismiss', value); + }); + + AppConfig.onChange(configKey.undoEvents, function (value) { + updateItemCount(value.length); + }); // ------------------------------------------------------------------------ // Public properties diff --git a/data/scripts/config.js b/data/scripts/config.js index d785979..23e6d29 100644 --- a/data/scripts/config.js +++ b/data/scripts/config.js @@ -88,7 +88,8 @@ define(function(require, exports) { // General settings getConfigValue('context.menu.icons', true); - getConfigValue('hide.trash.can', true); + getConfigValue('trashcan.hide', true); + getConfigValue('trashcan.hide-count', 0); getConfigValue('undo.events', []); }; @@ -150,7 +151,7 @@ define(function(require, exports) { var onChange = function onChange(topic, callback) { configEvents.subscribe(topic, callback) - } + }; // ------------------------------------------------------------------------ // Startup @@ -158,6 +159,7 @@ define(function(require, exports) { var init = function init() { setupConfig(); + // console.log(localSettings); if (isAddonContext()) { browser.commands.getAll(). @@ -177,7 +179,6 @@ define(function(require, exports) { // console.log(manifest.name + ' v' + manifest.version); // console.log('------------------------------------------'); - setupConfig(); initConfig = false; diff --git a/data/scripts/session/bookmarks.js b/data/scripts/session/bookmarks.js index a9d7a14..7949ad5 100644 --- a/data/scripts/session/bookmarks.js +++ b/data/scripts/session/bookmarks.js @@ -17,8 +17,8 @@ define(function(require, exports) { function getLazyLoadingParameters(url) { let paramater = {}; - let paras = url.split('?')[1].split('&'); - for (let p of paras) { + let params = url.split('?')[1].split('&'); + for (let p of params) { paramater[p.split('=')[0]] = decodeURIComponent(p.split('=')[1]); } return paramater; diff --git a/data/scripts/session/management.js b/data/scripts/session/management.js index 612960f..15038c8 100644 --- a/data/scripts/session/management.js +++ b/data/scripts/session/management.js @@ -98,7 +98,10 @@ define(function(require, exports) { if (mozWindow.incognito == true || AppConfig.get('session.save').allWindows == false) { var sessionTitle = (new Date()).toLocaleString(); - saveWindowSession(mozWindow, sessionTitle, function onSuccess(folder) { + saveWindowSession({ + title: sessionTitle, + tabs: mozWindow.tabs + }, function onSuccess(folder) { sessionID = folder.id; sessionCount++; updateOnSave(); @@ -125,7 +128,10 @@ define(function(require, exports) { { windowID++; let sessionTitle = date.toLocaleString() + ((sessionCount > 1) ? (' #' + windowID) : ''); - saveWindowSession(mozWindow, sessionTitle, function(folder) { + saveWindowSession({ + title: sessionTitle, + tabs: mozWindow.tabs, + }, function(folder) { sessionID = folder.id; updateEvent.advance(); }); @@ -136,38 +142,6 @@ define(function(require, exports) { }); }; - var saveWindowSession = function saveWindowSession(mozWindow, folderName, callback) - { - BookmarkManager.createBookmark({ - title: folderName, - parentId: AppConfig.get('storageID') - }) - .then(function (folder) { - - var trackSaving = new LimitCounter(mozWindow.tabs.length, function () { - callback(folder); - }); - - // TODO - add option to discard duplicate tabs - - var savePinned = AppConfig.get('session.save').pinned; - - for (let i = mozWindow.tabs.length - 1; i >= 0; i--) - { - let tab = mozWindow.tabs[i]; - if (tab.pinned && savePinned == false) - { - trackSaving.offsetLimit(-1); - } - else - { - BookmarkManager.createBookmarkFromTab(tab, folder.id) - .then(trackSaving.advance, trackSaving.advance); - } - } - }); - }; - var mergeSessions = function mergeSessions(sessionID) { // Exclude all pages that are already saved @@ -239,17 +213,48 @@ define(function(require, exports) { }); }; - var saveHistorySession = function saveHistorySession(sessionInfo, callback) + var saveHistorySession = function saveHistorySession(sessionInfo) + { + var windowCount = sessionInfo.windows.length; + + var windowID = 0; + + function onSave(bookmark) { + WindowEvents.emit(document, 'SetPromiseSession', { + sessionID: bookmark.id, + edit: false, + update: true + }); + } + + // save all history windows + for (var key in sessionInfo.windows) + { + var date = new Date(sessionInfo.lastSave); + var sessionTitle = date.toLocaleString() + ((windowCount > 1) ? (' #' + windowID) : ''); + windowID++; + + saveWindowSession({ + title: sessionTitle, + tabs: sessionInfo.windows[key].tabs + }, onSave); + } + }; + + var saveWindowSession = function saveWindowSession(sessionInfo, callback) { BookmarkManager.createBookmark({ title: sessionInfo.title, parentId: AppConfig.get('storageID') }) .then(function (folder) { + var trackSaving = new LimitCounter(sessionInfo.tabs.length, function () { callback(folder); }); + // TODO - add option to discard duplicate tabs + var savePinned = AppConfig.get('session.save').pinned; for (let i = sessionInfo.tabs.length - 1; i >= 0; i--) @@ -275,7 +280,10 @@ define(function(require, exports) { parentId: AppConfig.get('storageID') }) .then(function success(session) { - WindowEvents.emit(document, 'SetPromiseSession', { sessionID: session.id, edit: true, update: true }); + WindowEvents.emit(document, 'SetPromiseSession', { + sessionID: session.id, + edit: true, + update: true }); }, function error() { }); }; @@ -321,7 +329,6 @@ define(function(require, exports) { mergeSessions: mergeSessions, overwriteSession: overwriteSession, saveActiveSession: saveActiveSession, - saveWindowSession: saveWindowSession, createNewSession: createNewSession, saveHistorySession: saveHistorySession, }; diff --git a/data/session-sync.css b/data/session-sync.css index b7d5597..b580876 100644 --- a/data/session-sync.css +++ b/data/session-sync.css @@ -201,7 +201,7 @@ */ #session-sync .config-panel { - width: 300px; + width: 350px; padding: 30px 0 0 0; font-size: 12px; border-right: 1px solid #CCC; @@ -213,7 +213,7 @@ position: absolute; top: 1px; bottom: 1px; - right: -305px; + right: -355px; z-index: 2000; box-shadow: 0 0 10px 0 #CCC; @@ -227,7 +227,7 @@ } #session-sync[show="false"] .config-panel { - right: -305px !important; + right: -355px !important; } /* @@ -634,7 +634,7 @@ width: 100%; padding: 5px 5px 5px 30px; - background-image: url("images/icons/cloud.png"); + background-image: url("images/icons/heart.png"); background-size: 20px; background-repeat: no-repeat; background-position: 3px 3px; @@ -2273,11 +2273,11 @@ * Session list */ -#session-sync[list="history"] .sessions .list { +#session-sync[list="history"] .sessions .session-list { flex: 0; } -#session-sync .sessions .list { +#session-sync .sessions .session-list { width: calc(100% + 12px); margin: 0 0 0 0; @@ -2480,10 +2480,6 @@ opacity: 1; } -#session-sync .trash-can[dismiss] { - opacity: 0; -} - #session-sync .trash-can:hover { cursor: pointer; font-size: 16px; @@ -2522,15 +2518,15 @@ right: 0.5em; } -#session-sync .trash-can[dismiss] { +#session-sync .trash-can[dismiss="true"] { opacity: 0; } -#session-sync .trash-can[dismiss] .dismiss { +#session-sync .trash-can[dismiss="true"] .dismiss { display: none; } -#session-sync .trash-can[dismiss]:hover { +#session-sync .trash-can[dismiss="true"]:hover { opacity: 1; } diff --git a/dev/css/config-panel.css b/dev/css/config-panel.css index a8a86f0..f193bf2 100644 --- a/dev/css/config-panel.css +++ b/dev/css/config-panel.css @@ -3,7 +3,7 @@ */ #session-sync .config-panel { - width: 300px; + width: 350px; padding: 30px 0 0 0; font-size: 12px; border-right: 1px solid #CCC; @@ -15,7 +15,7 @@ position: absolute; top: 1px; bottom: 1px; - right: -305px; + right: -355px; z-index: 2000; box-shadow: 0 0 10px 0 #CCC; @@ -29,7 +29,7 @@ } #session-sync[show="false"] .config-panel { - right: -305px !important; + right: -355px !important; } /* diff --git a/dev/css/context-menu.css b/dev/css/context-menu.css index 2f3e2db..38b31c8 100644 --- a/dev/css/context-menu.css +++ b/dev/css/context-menu.css @@ -36,7 +36,7 @@ width: 100%; padding: 5px 5px 5px 30px; - background-image: url("images/icons/cloud.png"); + background-image: url("images/icons/heart.png"); background-size: 20px; background-repeat: no-repeat; background-position: 3px 3px; diff --git a/dev/css/sessions-list.css b/dev/css/sessions-list.css index 3542223..849586f 100644 --- a/dev/css/sessions-list.css +++ b/dev/css/sessions-list.css @@ -109,11 +109,11 @@ * Session list */ -#session-sync[list="history"] .sessions .list { +#session-sync[list="history"] .sessions .session-list { flex: 0; } -#session-sync .sessions .list { +#session-sync .sessions .session-list { width: calc(100% + 12px); margin: 0 0 0 0; diff --git a/dev/css/trash-can.css b/dev/css/trash-can.css index e1a0566..1a17d0d 100644 --- a/dev/css/trash-can.css +++ b/dev/css/trash-can.css @@ -54,10 +54,6 @@ opacity: 1; } -#session-sync .trash-can[dismiss] { - opacity: 0; -} - #session-sync .trash-can:hover { cursor: pointer; font-size: 16px; @@ -96,14 +92,14 @@ right: 0.5em; } -#session-sync .trash-can[dismiss] { +#session-sync .trash-can[dismiss="true"] { opacity: 0; } -#session-sync .trash-can[dismiss] .dismiss { +#session-sync .trash-can[dismiss="true"] .dismiss { display: none; } -#session-sync .trash-can[dismiss]:hover { +#session-sync .trash-can[dismiss="true"]:hover { opacity: 1; } diff --git a/manifest.json b/manifest.json index d94b034..89deba1 100644 --- a/manifest.json +++ b/manifest.json @@ -1,6 +1,6 @@ { "name": "Session Sync", - "version": "3.1.6", + "version": "3.1.10", "author": "Gabriel Ivanica", "description": "Save sessions as bookmarks and sync them through Firefox Sync (or any other sync engine).\nPowerful session management - organize, edit, change, save, restore, etc\n", "homepage_url": "https://github.com/ReDEnergy/SessionSync", @@ -24,7 +24,8 @@ "scripts/main.js", "scripts/favicon-service.js", "scripts/session-management.js", - "scripts/session-history.js" + "scripts/session-history.js", + "scripts/development.js" ] }, "permissions": [ diff --git a/scripts/development.js b/scripts/development.js new file mode 100644 index 0000000..dd844b3 --- /dev/null +++ b/scripts/development.js @@ -0,0 +1,19 @@ +'use strict'; + +(function DevelopmentMode() +{ + // ------------------------------------------------------------------------ + // Init + + browser.commands.onCommand.addListener(function(command) { + if (command == 'session-sync-save-history') { + for (var i = 0; i < 10; i++) { + SessionAutoSave.activeSession.update(); + } + } + }); + + // ------------------------------------------------------------------------ + // Events + +})(); \ No newline at end of file diff --git a/scripts/main.js b/scripts/main.js index e149729..a80b939 100644 --- a/scripts/main.js +++ b/scripts/main.js @@ -102,8 +102,8 @@ var SessionSync = (function () { // top: 0, url: 'data/session-sync.html', type: 'panel' - }).then(function (window) { - activeWindow = window; + }).then(function (newWindow) { + activeWindow = newWindow; // WindowUtils.centerWindow(window); }); }); @@ -188,6 +188,8 @@ var WindowUtils = (function () // Delete previous undo events browser.storage.local.set({'undo.events' : []}); +browser.storage.local.set({'trashcan.hide' : true}); +browser.storage.local.set({'trashcan.hide-count' : 0}); browser.storage.local.set({'session.active.filter' : ''}); browser.storage.local.set({'session.selected' : null}); browser.storage.local.set({'session.history.selected' : null}); diff --git a/scripts/session-history.js b/scripts/session-history.js index 6059f2c..acd470a 100644 --- a/scripts/session-history.js +++ b/scripts/session-history.js @@ -3,12 +3,19 @@ var BrowserSession = function BrowserSession() { this.timer; + this.active = true; this.tabCount = 0; this.startTime = (new Date()).getTime(); this.lastSave = this.startTime; - this.updateInterval = 15; - this.windows = []; - this.onUpdate = function () {}; + this.windows = { }; +}; + +var WindowSession = function WindowSession(id) +{ + this.id = id; + this.tabs = []; + this.active = true; + this.saveTime = (new Date()).getTime(); }; BrowserSession.prototype.update = function update() @@ -18,15 +25,19 @@ BrowserSession.prototype.update = function update() populate: true, windowTypes: ['normal'] }) - .then(function (windows) { - var sessions = []; + .then(function (mozWindows) { var tabCount = 0; - for (let mozWindow of windows) + for (var key in this.windows) + { + this.windows[key].active = false; + } + + for (let mozWindow of mozWindows) { if (mozWindow.incognito == false) { - var tabs = []; + var windowInfo = new WindowSession(mozWindow.id); // Add window tabs for (var i in mozWindow.tabs) @@ -40,29 +51,28 @@ BrowserSession.prototype.update = function update() pinned: tab.pinned, }; tabCount++; - tabs[tab.index] = sessionTab; + windowInfo.tabs[tab.index] = sessionTab; } - sessions.push(tabs); + this.windows[mozWindow.id] = windowInfo; } } // Update internal storage this.lastSave = (new Date()).getTime(); this.tabCount = tabCount; - this.windows = sessions; - this.onUpdate(); - // console.log(this); + // Save new session + browser.storage.local.set({ 'history.active' : this}); }.bind(this)); }; -BrowserSession.prototype.start = function start() +BrowserSession.prototype.start = function start(updateInterval) { if (this.timer == undefined) { // console.log('Session auto-save: start'); - this.timer = setInterval(this.update.bind(this), this.updateInterval * 1000); + this.timer = setInterval(this.update.bind(this), updateInterval * 1000); this.update(); } }; @@ -78,15 +88,20 @@ BrowserSession.prototype.stop = function stop() BrowserSession.prototype.resetInterval = function resetInterval(updateInterval) { - this.updateInterval = updateInterval; if (this.timer != undefined) { // console.log('Session auto-save: reset interval'); this.stop(); - this.start(); + this.start(updateInterval); } }; -(function SessionAutoSave() +BrowserSession.prototype.setConfig = function setConfig(config) +{ + this.resetInterval(config.interval); + config.enabled ? this.start(config.interval) : this.stop(); +}; + +var SessionAutoSave = (function SessionAutoSave() { // ------------------------------------------------------------------------ // Init @@ -94,16 +109,19 @@ BrowserSession.prototype.resetInterval = function resetInterval(updateInterval) var activeSession = new BrowserSession(); var sessions = []; var sessionsKey = 'history.sessions'; + var activeSessionKey = 'history.active'; var config = { key: 'history.config', enabled: true, interval: 15, + saveBuffer: 3, savingSlots: 10, expireTimeHours: 48, // hours }; - browser.storage.local.get(config.key).then(function (obj) { + browser.storage.local.get(config.key) + .then(function onSuccess(obj) { if (obj[config.key]) { config = obj[config.key]; @@ -115,80 +133,99 @@ BrowserSession.prototype.resetInterval = function resetInterval(updateInterval) [config.key] : config }).then(init); } + + }, function onError() { + init(); }); - var init = function init() + var upgradeStorage = function upgradeStorage() { - browser.storage.local.get(sessionsKey).then(function (obj) { - if (obj[sessionsKey]) { - sessions = obj[sessionsKey]; - } - - updateStorage(); - - activeSession.resetInterval(config.interval); - activeSession.onUpdate = updateTrackedSession; - - updateState(); - - browser.storage.onChanged.addListener(function onChange(object) { - if (object[config.key]) { - config = object[config.key].newValue; - - activeSession.resetInterval(config.interval); - config.enabled ? activeSession.start() : activeSession.stop(); - } - if (object[sessionsKey]) { - sessions = object[sessionsKey].newValue; + for (var i = 0; i < sessions.length; i++) + { + // New storage works with WindowSession objects + if (Array.isArray(sessions[i].windows[0]) == true) + { + for (var j = 0; j < sessions[i].windows.length; j++) { + var session = new WindowSession(0); + session.state = 'history'; + session.saveTime = sessions[i].lastSave; + session.tabs = sessions[i].windows[j]; + sessions[i].windows[j] = session; } - }); - }); - }; - - // ------------------------------------------------------------------------ - // Events - - var updateTrackedSession = function updateTrackedSession() - { - sessions[sessions.length ? (sessions.length - 1) : 0] = { - tabCount: activeSession.tabCount, - startDate: activeSession.startTime, - lastSave: activeSession.lastSave, - windows: activeSession.windows, - active: true, - }; - - browser.storage.local.set({ [sessionsKey] : sessions}); + } + } }; var updateStorage = function updateStorage() { + var removeCount = sessions.length - config.savingSlots; + if (removeCount <= 0) + return; + var expireTime = new Date(new Date() - config.expireTimeHours * 3600 * 1000); var list = []; // compact free space and delete expired sessions sessions.forEach(function (session) { - if (session && session.tabCount) { - delete session.active; - if (config.expireTimeHours > 0) - { - if (session.lastSave < expireTime) { - return; - } + if (removeCount > 0) { + removeCount--; + if (session.lastSave < expireTime) { + return; } list.push(session); } + else { + list.push(session); + } }); // get session list - sessions = list.slice(-(config.savingSlots - 1)); - sessions[list.length] = undefined; + sessions = list; }; - var updateState = function updateState() + var init = function init() { - config.enabled ? activeSession.start() : activeSession.stop(); + browser.storage.onChanged.addListener(onConfigChanged); + + browser.storage.local.get([activeSessionKey, sessionsKey]) + .then(function(obj) { + + if (obj[sessionsKey]) { + sessions = obj[sessionsKey]; + updateStorage(); + } + + if (obj[activeSessionKey]) { + obj[activeSessionKey].windows = Object.values(obj[activeSessionKey].windows); + delete obj[activeSessionKey].active; + sessions.push(obj[activeSessionKey]); + } + + upgradeStorage(); + + browser.storage.local.set({ [sessionsKey] : sessions}); + + activeSession.setConfig(config); + }); + }; + + // ------------------------------------------------------------------------ + // Events + + var onConfigChanged = function onConfigChanged(object) + { + if (object[config.key]) { + config = object[config.key].newValue; + activeSession.setConfig(config); + } + }; + + // ------------------------------------------------------------------------ + // Public API + + return { + activeSession : activeSession }; })(); \ No newline at end of file From 8fc623e39be889b6589674479e8734b736e4844a Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Sat, 18 May 2019 14:34:14 +0300 Subject: [PATCH 02/32] added lazy-loading bookmark fix --- data/scripts/components/session-container.js | 6 ++++ data/scripts/session/management.js | 6 ++++ dev/css/session-toolbar.css | 4 +++ scripts/session-management.js | 36 ++++++++++++++++++-- 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/data/scripts/components/session-container.js b/data/scripts/components/session-container.js index 001fc59..f6445ca 100644 --- a/data/scripts/components/session-container.js +++ b/data/scripts/components/session-container.js @@ -152,6 +152,12 @@ define(function(require, exports) { }.bind(this)); + WindowEvents.on(document, 'MenuFixLazySession', function() { + if (isValidSessionID(this.activeSessionID)) { + SessionManager.fixLazySession(this.activeSessionID); + } + }.bind(this)); + WindowEvents.on(document, 'UpdateCurrentSession', function() { if (this.SyncModel.state.session == 'current') { this.showCurrentSession(); diff --git a/data/scripts/session/management.js b/data/scripts/session/management.js index 15038c8..6882642 100644 --- a/data/scripts/session/management.js +++ b/data/scripts/session/management.js @@ -303,6 +303,11 @@ define(function(require, exports) { browser.runtime.sendMessage({event: 'restore-sessions', windows: windows }); }; + var fixLazySession = function fixLazySession(folderID) + { + browser.runtime.sendMessage({event: 'fix-lazy-session', bookmarkID: folderID }); + }; + var createNewSession = function createNewSession() { createSession((new Date()).toLocaleString()); }; @@ -324,6 +329,7 @@ define(function(require, exports) { getCurrentWindow: getCurrentWindow, getAllWindows: getAllWindows, restoreSession: restoreSession, + fixLazySession: fixLazySession, restoreNewWindow: restoreNewWindow, loadBookmarksNewWindow: loadBookmarksNewWindow, mergeSessions: mergeSessions, diff --git a/dev/css/session-toolbar.css b/dev/css/session-toolbar.css index e3edade..5cdf85d 100644 --- a/dev/css/session-toolbar.css +++ b/dev/css/session-toolbar.css @@ -155,6 +155,10 @@ background: url("images/icons/frames.png") no-repeat center; } +#session-sync .session-toolbar .menu-bar .fix-session { + background: url("images/icons/tools.png") no-repeat center; +} + /* * Menu state */ diff --git a/scripts/session-management.js b/scripts/session-management.js index 93fcad1..d5fce2f 100644 --- a/scripts/session-management.js +++ b/scripts/session-management.js @@ -13,9 +13,12 @@ var SessionManagement = (function SessionManagement() function getLazyLoadingParameters(url) { let paramater = {}; - let paras = url.split('?')[1].split('&'); - for (let p of paras) { - paramater[p.split('=')[0]] = decodeURIComponent(p.split('=')[1]); + let params = url.split('?'); + if (params.length > 1) { + let paras = params[1].split('&'); + for (let p of paras) { + paramater[p.split('=')[0]] = decodeURIComponent(p.split('=')[1]); + } } return paramater; } @@ -255,6 +258,29 @@ var SessionManagement = (function SessionManagement() } }; + var fixLazySession = function fixLazySession(folderID) + { + var result = browser.bookmarks.getChildren(folderID); + result.then(function (bookmarks) { + console.log(bookmarks); + + bookmarks.forEach(function (bookmark) { + if (bookmark.url.startsWith('moz-extension')) + { + let redirectInfo = getLazyLoadingParameters(bookmark.url); + if (redirectInfo.url != undefined) { + console.log(bookmark, redirectInfo); + browser.bookmarks.update(bookmark.id, { + url: redirectInfo.url + }); + } + } + }); + }, function fail() { + console.log('Error getting bookmarks from folder ID: ', folderID); + }); + }; + browser.runtime.onMessage.addListener(function (message) { switch (message.event) { @@ -274,6 +300,10 @@ var SessionManagement = (function SessionManagement() openTab(message.options); break; + case 'fix-lazy-session': + fixLazySession(message.bookmarkID); + break; + } }); From 5297fa6c041c5fab32a39164f848c1e021811609 Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Sat, 25 May 2019 15:29:01 +0300 Subject: [PATCH 03/32] refactoring Addon menu --- data/scripts/components/session-header-bar.js | 51 ++++++++++++------- data/session-sync.css | 34 ++----------- dev/css/session-header-bar.css | 30 ----------- 3 files changed, 37 insertions(+), 78 deletions(-) diff --git a/data/scripts/components/session-header-bar.js b/data/scripts/components/session-header-bar.js index 817bc92..2afe822 100644 --- a/data/scripts/components/session-header-bar.js +++ b/data/scripts/components/session-header-bar.js @@ -105,20 +105,22 @@ define(function(require, exports) { var title = DomElem('div', {class: 'title'}); title.textContent = options.title; - button.setAttribute('id', options.id); - button.appendChild(icon); - button.appendChild(title); + + var url = 'url("images/' + options.icon + '")'; + icon.style.backgroundImage = url; // Events var callback = JSUtils.getValidFunction(options.callback); button.addEventListener('click', callback); + button.appendChild(icon); + button.appendChild(title); return button; } var openConfig = MenuButton({ - id: 'config', title: 'Options', + icon: 'icons/gear.png', callback: function () { var model = SessionSyncModel.getModel(document); var state = model.state['config'] == undefined ? 'on' : undefined; @@ -127,24 +129,24 @@ define(function(require, exports) { }); var openTutorial = MenuButton({ - id: 'help', title: 'Tutorial', + icon: 'icons/brightness.png', callback: function () { browser.runtime.sendMessage({event: 'session-sync-tutorial'}); } }); var leaveFeedback = MenuButton({ - id: 'feedback', title: 'Feedback', + icon: 'icons/mail.png', callback: function () { browser.runtime.sendMessage({event: 'session-sync-leave-feedback'}); } }); var exportImport = MenuButton({ - id: 'export', title: 'Export/Import', + icon: 'icons/booklet.png', callback: function () { browser.tabs.create({ url: 'home/home.html#export-import', @@ -154,24 +156,24 @@ define(function(require, exports) { }); var buttonDetachInTab = MenuButton({ - id: 'tab-view', title: 'Tab View', + icon: 'icons/document.png', callback: function () { browser.runtime.sendMessage({event: 'session-sync-detach-tab'}); } }); var buttonDetachInWindow = MenuButton({ - id: 'window-view', title: 'Window View', + icon: 'icons/browser.png', callback: function () { browser.runtime.sendMessage({event: 'session-sync-detach-window'}); } }); var githubPage = MenuButton({ - id: 'github', title: 'Dev page', + icon: 'github-logo.png', callback: function () { browser.runtime.sendMessage({event: 'session-sync-open-github'}); } @@ -211,11 +213,11 @@ define(function(require, exports) { function initDevMode() { - var devMenu = DomElem('div', {class: 'menu-row'}); + var devRow1 = DomElem('div', {class: 'menu-row'}); var activeTabInfo = MenuButton({ - id: 'tabInfo', title: 'Tab Info', + icon: 'icons/document.png', callback: function () { browser.tabs.query({active: true, windowId: browser.windows.WINDOW_ID_CURRENT}) .then(tabs => browser.tabs.get(tabs[0].id)) @@ -226,8 +228,8 @@ define(function(require, exports) { }); var localStorage = MenuButton({ - id: 'localStorage', title: 'Local Storage', + icon: 'icons/bookshelf.png', callback: function () { browser.storage.local.get().then(function (data) { console.log(data); @@ -236,8 +238,8 @@ define(function(require, exports) { }); var clearFaviconCache = MenuButton({ - id: 'clearFaviconCache', title: 'Clear Favicons', + icon: 'icons/x.png', callback: function () { browser.storage.local.get().then(function (data) { for (var key in data) { @@ -249,10 +251,23 @@ define(function(require, exports) { } }); - devMenu.appendChild(activeTabInfo); - devMenu.appendChild(localStorage); - devMenu.appendChild(clearFaviconCache); - menuArea.appendChild(devMenu); + var fixLazySession = MenuButton({ + title: 'Fix session', + icon: 'icons/tools.png', + callback: function() { + WindowEvents.emit(document, 'MenuFixLazySession'); + } + }); + + devRow1.appendChild(activeTabInfo); + devRow1.appendChild(localStorage); + devRow1.appendChild(clearFaviconCache); + menuArea.appendChild(devRow1); + + + var devRow2 = DomElem('div', {class: 'menu-row'}); + devRow2.appendChild(fixLazySession); + menuArea.appendChild(devRow2); } if (AppConfig.devMode()) diff --git a/data/session-sync.css b/data/session-sync.css index b580876..8da35b1 100644 --- a/data/session-sync.css +++ b/data/session-sync.css @@ -1776,34 +1776,6 @@ background-position: center; } -#session-sync .header-bar .header-menu .menu-button[id="help"] .icon { - background-image: url("images/icons/brightness.png"); -} - -#session-sync .header-bar .header-menu .menu-button[id="config"] .icon { - background-image: url("images/icons/gear.png"); -} - -#session-sync .header-bar .header-menu .menu-button[id="window-view"] .icon { - background-image: url("images/icons/browser.png"); -} - -#session-sync .header-bar .header-menu .menu-button[id="tab-view"] .icon { - background-image: url("images/icons/document.png"); -} - -#session-sync .header-bar .header-menu .menu-button[id="feedback"] .icon { - background-image: url("images/icons/mail.png"); -} - -#session-sync .header-bar .header-menu .menu-button[id="export"] .icon { - background-image: url("images/icons/booklet.png"); -} - -#session-sync .header-bar .header-menu .menu-button[id="github"] .icon { - background-image: url("images/github-logo.png"); -} - #session-sync .header-bar .header-menu .menu-button .title { height: 1.4em; margin-top: 0.4em; @@ -1813,8 +1785,6 @@ color: #555; } - - /* * Sorting method */ @@ -2069,6 +2039,10 @@ background: url("images/icons/frames.png") no-repeat center; } +#session-sync .session-toolbar .menu-bar .fix-session { + background: url("images/icons/tools.png") no-repeat center; +} + /* * Menu state */ diff --git a/dev/css/session-header-bar.css b/dev/css/session-header-bar.css index 395e16c..7150a2a 100644 --- a/dev/css/session-header-bar.css +++ b/dev/css/session-header-bar.css @@ -185,34 +185,6 @@ background-position: center; } -#session-sync .header-bar .header-menu .menu-button[id="help"] .icon { - background-image: url("images/icons/brightness.png"); -} - -#session-sync .header-bar .header-menu .menu-button[id="config"] .icon { - background-image: url("images/icons/gear.png"); -} - -#session-sync .header-bar .header-menu .menu-button[id="window-view"] .icon { - background-image: url("images/icons/browser.png"); -} - -#session-sync .header-bar .header-menu .menu-button[id="tab-view"] .icon { - background-image: url("images/icons/document.png"); -} - -#session-sync .header-bar .header-menu .menu-button[id="feedback"] .icon { - background-image: url("images/icons/mail.png"); -} - -#session-sync .header-bar .header-menu .menu-button[id="export"] .icon { - background-image: url("images/icons/booklet.png"); -} - -#session-sync .header-bar .header-menu .menu-button[id="github"] .icon { - background-image: url("images/github-logo.png"); -} - #session-sync .header-bar .header-menu .menu-button .title { height: 1.4em; margin-top: 0.4em; @@ -221,5 +193,3 @@ text-align: center; color: #555; } - - From b3db210100500dd8b752b8468f1e41df953bbdc4 Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Sat, 25 May 2019 17:38:29 +0300 Subject: [PATCH 04/32] bugfix history update --- scripts/session-history.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/session-history.js b/scripts/session-history.js index acd470a..b6d415c 100644 --- a/scripts/session-history.js +++ b/scripts/session-history.js @@ -143,7 +143,7 @@ var SessionAutoSave = (function SessionAutoSave() for (var i = 0; i < sessions.length; i++) { // New storage works with WindowSession objects - if (Array.isArray(sessions[i].windows[0]) == true) + if (sessions[i] != undefined && Array.isArray(sessions[i].windows[0]) == true) { for (var j = 0; j < sessions[i].windows.length; j++) { var session = new WindowSession(0); From 32a87cff3c1c591331cdf0d6deb6883216d36646 Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Sat, 25 May 2019 17:38:59 +0300 Subject: [PATCH 05/32] refactoring + added tab discarding using TrashCan --- data/scripts/components/session-container.js | 4 ++-- data/scripts/components/trash-can.js | 9 +++++++-- data/scripts/session/management.js | 18 +++++++++++++++++- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/data/scripts/components/session-container.js b/data/scripts/components/session-container.js index f6445ca..a3b4360 100644 --- a/data/scripts/components/session-container.js +++ b/data/scripts/components/session-container.js @@ -158,7 +158,7 @@ define(function(require, exports) { } }.bind(this)); - WindowEvents.on(document, 'UpdateCurrentSession', function() { + GlobalEvents.on('UpdateCurrentSession', function() { if (this.SyncModel.state.session == 'current') { this.showCurrentSession(); } @@ -203,7 +203,7 @@ define(function(require, exports) { if (updateTimeout == null) { updateTimeout = setTimeout(function () { updateTimeout = null; - WindowEvents.emit(document, 'UpdateCurrentSession'); + GlobalEvents.emit('UpdateCurrentSession'); }, 1000); } } diff --git a/data/scripts/components/trash-can.js b/data/scripts/components/trash-can.js index 522d289..e235bc5 100644 --- a/data/scripts/components/trash-can.js +++ b/data/scripts/components/trash-can.js @@ -11,7 +11,7 @@ define(function(require, exports) { const { HTMLCreator } = require('../utils/dom'); const { DragContext } = require('../utils/drag-context'); const { SessionFolder } = require('./session-folder'); - const { SessionBookmark } = require('./session-bookmark'); + const { SessionBookmark, SessionTab } = require('./session-bookmark'); // ***************************************************************************** // API @@ -62,13 +62,18 @@ define(function(require, exports) { block.addEventListener('mouseup', function (e) { var item = DragContext.getContext(); - if (item && (item instanceof SessionFolder || item instanceof SessionBookmark)) { + if (item instanceof SessionFolder || item instanceof SessionBookmark) { block.setAttribute('action', 'drop'); GlobalEvents.emit('DeleteBookmarkItem', item.bookmarkID); WindowEvents.emit(document, 'ShowTooltip', { node: e.target, message: 'Click to restore' }); + return; + } + + if (item instanceof SessionTab) { + GlobalEvents.emit('management.close-tab', item.tab.id); } }); diff --git a/data/scripts/session/management.js b/data/scripts/session/management.js index 6882642..a824f25 100644 --- a/data/scripts/session/management.js +++ b/data/scripts/session/management.js @@ -57,7 +57,7 @@ define(function(require, exports) { index: newIndex, windowId: windowID }).then(function() { - WindowEvents.emit(document, 'UpdateCurrentSession'); + GlobalEvents.emit('UpdateCurrentSession'); trackTabs = true; }, function onError() { trackTabs = true; @@ -71,6 +71,11 @@ define(function(require, exports) { }); }; + var closeTab = function closeTab(tabID) + { + browser.tabs.remove(tabID); + }; + var activateWindow = function activateWindow(windowID) { browser.windows.update(windowID, { @@ -318,9 +323,20 @@ define(function(require, exports) { createSession(title); }); + // Events + GlobalEvents.on('management.close-tab', function(tabID) { + trackTabs = false; + browser.tabs.remove(tabID) + .then(function () { + GlobalEvents.emit('UpdateCurrentSession'); + trackTabs = true; + }); + }); + // Public API return { moveTab: moveTab, + closeTab: closeTab, activateTab: activateTab, tabTracking: tabTracking, activateWindow: activateWindow, From ed88497aae01d0627d096613b3c0217301a01567 Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Fri, 12 Jul 2019 20:47:11 +0300 Subject: [PATCH 06/32] added option to enable troubleshooting (fix for incorrect saved session) improved auto-saved history storage upgrading and updating --- data/scripts/components/config-panel.js | 8 +++ data/scripts/components/session-header-bar.js | 46 ++++++++++++---- data/scripts/components/session-history.js | 34 +++++++----- data/scripts/config.js | 2 + manifest.json | 2 +- scripts/session-history.js | 52 ++++++++++++------- 6 files changed, 103 insertions(+), 41 deletions(-) diff --git a/data/scripts/components/config-panel.js b/data/scripts/components/config-panel.js index ea7529f..422feac 100644 --- a/data/scripts/components/config-panel.js +++ b/data/scripts/components/config-panel.js @@ -79,6 +79,7 @@ define(function(require, exports) { } }, }); + options.parent.addItem(toggleBtn.DOMRoot); AppConfig.onChange(options.key, function(value) { if (isSwitch) @@ -213,6 +214,13 @@ define(function(require, exports) { type: 'switch', }); + ToggleOptionConfig({ + parent: section, + name: 'Troubleshooting', + key: 'troubleshooting', + type: 'switch', + }); + })(); // ------------------------ diff --git a/data/scripts/components/session-header-bar.js b/data/scripts/components/session-header-bar.js index 2afe822..89ddf3e 100644 --- a/data/scripts/components/session-header-bar.js +++ b/data/scripts/components/session-header-bar.js @@ -251,6 +251,21 @@ define(function(require, exports) { } }); + devRow1.appendChild(activeTabInfo); + devRow1.appendChild(localStorage); + devRow1.appendChild(clearFaviconCache); + menuArea.appendChild(devRow1); + } + + if (AppConfig.devMode()) + { + initDevMode(); + } + + var Troubleshooting = (function Troubleshooting() + { + var isInit = false; + var fixLazySession = MenuButton({ title: 'Fix session', icon: 'icons/tools.png', @@ -259,19 +274,24 @@ define(function(require, exports) { } }); - devRow1.appendChild(activeTabInfo); - devRow1.appendChild(localStorage); - devRow1.appendChild(clearFaviconCache); - menuArea.appendChild(devRow1); + function init() + { + if (isInit == true) + { + return; + } + isInit = true; + var devRow = DomElem('div', {class: 'menu-row'}); + devRow.appendChild(fixLazySession); + menuArea.appendChild(devRow); + } - var devRow2 = DomElem('div', {class: 'menu-row'}); - devRow2.appendChild(fixLazySession); - menuArea.appendChild(devRow2); - } + return { + init: init + }; - if (AppConfig.devMode()) - initDevMode(); + })(); // ------------------------------------------------------------------------ // Events @@ -307,6 +327,12 @@ define(function(require, exports) { closeMenu(); }); + AppConfig.onChange('troubleshooting', function(value) { + if (value == true) { + Troubleshooting.init(); + } + }); + // ------------------------------------------------------------------------ // Public diff --git a/data/scripts/components/session-history.js b/data/scripts/components/session-history.js index cabcdb0..faa592c 100644 --- a/data/scripts/components/session-history.js +++ b/data/scripts/components/session-history.js @@ -67,27 +67,37 @@ define(function(require, exports) { }); }; + var onStorageChanged = function onStorageChanged(object) + { + if (object[storageKey.list]) { + init(); + } + }; + GlobalEvents.on('HistorySessionDelete', function(index) { - getFullHistory(function (sessions) { - if (index >= 0 && index < sessions.length) { - if (index == sessions.length - 1) - { - sessions[index] = undefined; - } - else - { + if (index == -1) + { + browser.runtime.sendMessage({event: 'history.deleteActive'}); + } + else + { + getFullHistory(function (sessions) { + if (index >= 0 && index < sessions.length) { sessions.splice(index, 1); + updateSessions(sessions); } - updateSessions(sessions); - } - }); + }); + } }); GlobalEvents.on('HistorySessionDeleteAll', function() { - updateSessions([]); + sessions = []; + updateSessions(sessions); }); + browser.storage.onChanged.addListener(onStorageChanged); + // ------------------------------------------------------------------------ // Public API diff --git a/data/scripts/config.js b/data/scripts/config.js index 23e6d29..2bf5552 100644 --- a/data/scripts/config.js +++ b/data/scripts/config.js @@ -91,6 +91,8 @@ define(function(require, exports) { getConfigValue('trashcan.hide', true); getConfigValue('trashcan.hide-count', 0); getConfigValue('undo.events', []); + + getConfigValue('troubleshooting', false); }; var set = function set(key, value, onSuccess) diff --git a/manifest.json b/manifest.json index 89deba1..1c36fbe 100644 --- a/manifest.json +++ b/manifest.json @@ -1,6 +1,6 @@ { "name": "Session Sync", - "version": "3.1.10", + "version": "3.1.12", "author": "Gabriel Ivanica", "description": "Save sessions as bookmarks and sync them through Firefox Sync (or any other sync engine).\nPowerful session management - organize, edit, change, save, restore, etc\n", "homepage_url": "https://github.com/ReDEnergy/SessionSync", diff --git a/scripts/session-history.js b/scripts/session-history.js index b6d415c..b23abc4 100644 --- a/scripts/session-history.js +++ b/scripts/session-history.js @@ -68,6 +68,13 @@ BrowserSession.prototype.update = function update() }.bind(this)); }; +BrowserSession.prototype.clear = function clear() +{ + // Save new session + this.windows = {}; + browser.storage.local.set({ 'history.active' : this}); +}; + BrowserSession.prototype.start = function start(updateInterval) { if (this.timer == undefined) { @@ -116,7 +123,7 @@ var SessionAutoSave = (function SessionAutoSave() enabled: true, interval: 15, saveBuffer: 3, - savingSlots: 10, + savingSlots: 20, expireTimeHours: 48, // hours }; @@ -158,36 +165,38 @@ var SessionAutoSave = (function SessionAutoSave() var updateStorage = function updateStorage() { - var removeCount = sessions.length - config.savingSlots; - if (removeCount <= 0) - return; + upgradeStorage(); + var removeCount = Math.max(sessions.length - config.savingSlots, 0); var expireTime = new Date(new Date() - config.expireTimeHours * 3600 * 1000); var list = []; + // console.log('To remove', removeCount); // compact free space and delete expired sessions sessions.forEach(function (session) { - if (removeCount > 0) { - removeCount--; - if (session.lastSave < expireTime) { - return; - } + if (session != undefined && (removeCount <= 0 || session.lastSave > expireTime)) + { list.push(session); } - else { - list.push(session); + else + { + if (removeCount > 0) + removeCount--; } }); - // get session list + if (removeCount > 0) + { + list = list.slice(removeCount); + } + + // update session list sessions = list; }; var init = function init() { - browser.storage.onChanged.addListener(onConfigChanged); - browser.storage.local.get([activeSessionKey, sessionsKey]) .then(function(obj) { @@ -196,16 +205,15 @@ var SessionAutoSave = (function SessionAutoSave() updateStorage(); } + // console.log(sessions); + if (obj[activeSessionKey]) { obj[activeSessionKey].windows = Object.values(obj[activeSessionKey].windows); delete obj[activeSessionKey].active; sessions.push(obj[activeSessionKey]); } - upgradeStorage(); - - browser.storage.local.set({ [sessionsKey] : sessions}); - + browser.storage.local.set({ [sessionsKey] : sessions }); activeSession.setConfig(config); }); }; @@ -213,6 +221,12 @@ var SessionAutoSave = (function SessionAutoSave() // ------------------------------------------------------------------------ // Events + browser.runtime.onMessage.addListener(function (message) { + if (message.event == 'history.deleteActive') { + activeSession.clear(); + } + }); + var onConfigChanged = function onConfigChanged(object) { if (object[config.key]) { @@ -221,6 +235,8 @@ var SessionAutoSave = (function SessionAutoSave() } }; + browser.storage.onChanged.addListener(onConfigChanged); + // ------------------------------------------------------------------------ // Public API From 5b3da7af4f1e4bfa43d40324ec184e21166518d7 Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Thu, 26 Dec 2019 00:49:18 +0200 Subject: [PATCH 07/32] added config option to save private windows --- data/scripts/components/config-panel.js | 18 ++++++++++- scripts/session-history.js | 41 ++++++++++++++----------- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/data/scripts/components/config-panel.js b/data/scripts/components/config-panel.js index 422feac..772af30 100644 --- a/data/scripts/components/config-panel.js +++ b/data/scripts/components/config-panel.js @@ -289,7 +289,19 @@ define(function(require, exports) { } }); section.addItem(contextMenuIconsTB.DOMRoot); - WindowEvents.emit(document, 'SetUIState', {'history': historyConfig.enabled}); + + // Context menu icons + var autoSavePrivateTB = new DOMComponent.ToggleSwitch({ + state: historyConfig.savePrivate, + description: 'Save private windows', + onState: 'Enabled', + offState: 'Disabled', + callback: function(value) { + historyConfig.savePrivate = value; + SessionHistory.updateConfig(historyConfig); + } + }); + section.addItem(autoSavePrivateTB.DOMRoot); // Auto-save interval var saveIntervalRG = new DOMComponent.RangeControl({ @@ -326,6 +338,10 @@ define(function(require, exports) { }, }); section.addItem(expireAfterRG.DOMRoot); + + // Events + WindowEvents.emit(document, 'SetUIState', {'history': historyConfig.enabled}); + }); })(); diff --git a/scripts/session-history.js b/scripts/session-history.js index b23abc4..aca8025 100644 --- a/scripts/session-history.js +++ b/scripts/session-history.js @@ -35,27 +35,30 @@ BrowserSession.prototype.update = function update() for (let mozWindow of mozWindows) { - if (mozWindow.incognito == false) + if (mozWindow.incognito && this.config.savePrivate == false) { - var windowInfo = new WindowSession(mozWindow.id); - - // Add window tabs - for (var i in mozWindow.tabs) - { - var tab = mozWindow.tabs[i]; - var info = SessionManagement.getTabInfo(tab.url); - var sessionTab = { - title: tab.title, - url: info.url, - favIconUrl: info.lazyLoading ? info.favIconUrl : tab.favIconUrl, - pinned: tab.pinned, - }; - tabCount++; - windowInfo.tabs[tab.index] = sessionTab; - } + delete this.windows[mozWindow.id]; + continue; + } + + var windowInfo = new WindowSession(mozWindow.id); - this.windows[mozWindow.id] = windowInfo; + // Add window tabs + for (var i in mozWindow.tabs) + { + var tab = mozWindow.tabs[i]; + var info = SessionManagement.getTabInfo(tab.url); + var sessionTab = { + title: tab.title, + url: info.url, + favIconUrl: info.lazyLoading ? info.favIconUrl : tab.favIconUrl, + pinned: tab.pinned, + }; + tabCount++; + windowInfo.tabs[tab.index] = sessionTab; } + + this.windows[mozWindow.id] = windowInfo; } // Update internal storage @@ -104,6 +107,7 @@ BrowserSession.prototype.resetInterval = function resetInterval(updateInterval) BrowserSession.prototype.setConfig = function setConfig(config) { + this.config = config; this.resetInterval(config.interval); config.enabled ? this.start(config.interval) : this.stop(); }; @@ -125,6 +129,7 @@ var SessionAutoSave = (function SessionAutoSave() saveBuffer: 3, savingSlots: 20, expireTimeHours: 48, // hours + savePrivate: false, }; browser.storage.local.get(config.key) From 2623725168c91611ab6ad05043b9917b039316bb Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Thu, 26 Dec 2019 01:08:37 +0200 Subject: [PATCH 08/32] added replace session toolbar-options --- data/images/material_icons/repeat.svg | 1 + data/images/material_icons/repeat_one.svg | 1 + data/scripts/components/session-container.js | 12 +++++ data/scripts/components/session-toolbar.js | 23 ++++++++- data/scripts/session-sync-ui.js | 4 ++ data/scripts/session/management.js | 33 +++++++++++- dev/css/context-menu.css | 8 +++ dev/css/session-toolbar.css | 8 +++ scripts/session-management.js | 54 +++++++++++++++++--- 9 files changed, 134 insertions(+), 10 deletions(-) create mode 100644 data/images/material_icons/repeat.svg create mode 100644 data/images/material_icons/repeat_one.svg diff --git a/data/images/material_icons/repeat.svg b/data/images/material_icons/repeat.svg new file mode 100644 index 0000000..1bfbc2d --- /dev/null +++ b/data/images/material_icons/repeat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/images/material_icons/repeat_one.svg b/data/images/material_icons/repeat_one.svg new file mode 100644 index 0000000..0979480 --- /dev/null +++ b/data/images/material_icons/repeat_one.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/scripts/components/session-container.js b/data/scripts/components/session-container.js index a3b4360..747ad60 100644 --- a/data/scripts/components/session-container.js +++ b/data/scripts/components/session-container.js @@ -103,6 +103,18 @@ define(function(require, exports) { } }.bind(this)); + WindowEvents.on(document, 'ReplaceSessionAll', function() { + if (isValidSessionID(this.activeSessionID)) { + SessionManager.replaceSessionAll(this.activeSessionID); + } + }.bind(this)); + + WindowEvents.on(document, 'ReplaceSession', function() { + if (isValidSessionID(this.activeSessionID)) { + SessionManager.replaceSession(this.activeSessionID); + } + }.bind(this)); + WindowEvents.on(document, 'MenuMergeSessions', function(e) { if (isValidSessionID(this.activeSessionID)) { WindowEvents.emit(document, 'ConfirmBox-Open', { diff --git a/data/scripts/components/session-toolbar.js b/data/scripts/components/session-toolbar.js index f48e4c6..7a9c9f9 100644 --- a/data/scripts/components/session-toolbar.js +++ b/data/scripts/components/session-toolbar.js @@ -147,7 +147,7 @@ define(function(require, exports) { }); var restoreW = ActionButton({ - tooltip: 'Restore in new window', + tooltip: 'Restore (new window)', className: 'restore-new-win', event: 'MenuRestoreNewWindow', state: { @@ -156,6 +156,24 @@ define(function(require, exports) { } }); + var replaceSession = ActionButton({ + tooltip: 'Restore (close active)', + className: 'repalce-session', + event: 'ReplaceSession', + state: { + restore: true + } + }); + + var replaceSessionAll = ActionButton({ + tooltip: 'Restore (close all)', + className: 'repalce-session-all', + event: 'ReplaceSessionAll', + state: { + restore: true + } + }); + var mergeSession = ActionButton({ tooltip: 'Merge sessions', className: 'merge-sessions', @@ -210,6 +228,8 @@ define(function(require, exports) { menu.appendChild(restore); menu.appendChild(restoreW); + menu.appendChild(replaceSession); + menu.appendChild(replaceSessionAll); menu.appendChild(separator1); menu.appendChild(mergeSession); menu.appendChild(overwriteSession); @@ -218,6 +238,7 @@ define(function(require, exports) { menu.appendChild(save); menu.appendChild(saveConfig); + // ------------------------------------------------------------------------ // Events diff --git a/data/scripts/session-sync-ui.js b/data/scripts/session-sync-ui.js index c9f1942..4edaa4e 100644 --- a/data/scripts/session-sync-ui.js +++ b/data/scripts/session-sync-ui.js @@ -138,6 +138,8 @@ define(function(require, exports) { var SM = new ContextMenu({name : 'SessionContextMenu'}); SM.addMenuEntry({value: 'Restore', event: 'RestoreSession', icon: 'same-window'}); SM.addMenuEntry({value: 'Restore (new window)', event: 'RestoreInNewWindow', icon: 'new-window'}); + SM.addMenuEntry({value: 'Restore (close active)', event: 'ReplaceSession', icon: 'replace-current'}); + SM.addMenuEntry({value: 'Restore (close all)', event: 'ReplaceSessionAll', icon: 'replace-all'}); SM.addMenuEntry({value: 'Edit', event: 'EditSession', icon: 'edit'}); SM.addMenuEntry({value: 'Delete', globalEvent: 'DeleteBookmarkItem', icon: 'delete'}); SM.addMenuEntry({value: 'Create new session', callback: SessionManager.createNewSession, icon: 'new-session', separator: 'top'}); @@ -262,6 +264,8 @@ define(function(require, exports) { WindowEvents.on(document, 'SessionContextMenu-RestoreSession', SessionManager.restoreSession); WindowEvents.on(document, 'SessionContextMenu-RestoreInNewWindow', SessionManager.restoreNewWindow); + WindowEvents.on(document, 'SessionContextMenu-ReplaceSession', SessionManager.replaceSession); + WindowEvents.on(document, 'SessionContextMenu-ReplaceSessionAll', SessionManager.replaceSessionAll); // BookmarksCtxMenu events diff --git a/data/scripts/session/management.js b/data/scripts/session/management.js index a824f25..b5204b4 100644 --- a/data/scripts/session/management.js +++ b/data/scripts/session/management.js @@ -300,7 +300,35 @@ define(function(require, exports) { var restoreNewWindow = function restoreNewWindow(folderID) { - browser.runtime.sendMessage({event: 'restore-session', bookmarkID: folderID, inNewWindow: true}); + var options = { + newWindow: true + }; + browser.runtime.sendMessage({event: 'restore-session', bookmarkID: folderID, options: options}); + }; + + var restoreAdvanced = function restoreAdvanced(folderID, options) + { + browser.runtime.sendMessage({event: 'restore-session', bookmarkID: folderID, options: options}); + }; + + var replaceSession = function replaceSession(folderID) + { + getCurrentWindow(function(mozWindow) { + var options = { + newWindow: true, + closeWindow: mozWindow.id, + }; + restoreAdvanced(folderID, options); + }); + }; + + var replaceSessionAll = function replaceSessionAll(folderID) + { + var options = { + newWindow: true, + closeAll: true, + }; + restoreAdvanced(folderID, options); }; var loadBookmarksNewWindow = function loadBookmarksNewWindow(windows) @@ -345,6 +373,9 @@ define(function(require, exports) { getCurrentWindow: getCurrentWindow, getAllWindows: getAllWindows, restoreSession: restoreSession, + replaceSession: replaceSession, + restoreAdvanced: restoreAdvanced, + replaceSessionAll: replaceSessionAll, fixLazySession: fixLazySession, restoreNewWindow: restoreNewWindow, loadBookmarksNewWindow: loadBookmarksNewWindow, diff --git a/dev/css/context-menu.css b/dev/css/context-menu.css index 38b31c8..64dc537 100644 --- a/dev/css/context-menu.css +++ b/dev/css/context-menu.css @@ -64,6 +64,14 @@ background-image: url("images/icons/frames.png"); } +#session-sync .context-menu .button[icon="replace-current"] { + background-image: url("images/material_icons/repeat_one.svg"); +} + +#session-sync .context-menu .button[icon="replace-all"] { + background-image: url("images/material_icons/repeat.svg"); +} + #session-sync .context-menu .button[icon="copy"] { background-image: url("images/icons/clipboard.png"); } diff --git a/dev/css/session-toolbar.css b/dev/css/session-toolbar.css index 5cdf85d..3be6b5c 100644 --- a/dev/css/session-toolbar.css +++ b/dev/css/session-toolbar.css @@ -155,6 +155,14 @@ background: url("images/icons/frames.png") no-repeat center; } +#session-sync .session-toolbar .menu-bar .repalce-session-all { + background: url("images/material_icons/repeat.svg") no-repeat center; +} + +#session-sync .session-toolbar .menu-bar .repalce-session { + background: url("images/material_icons/repeat_one.svg") no-repeat center; +} + #session-sync .session-toolbar .menu-bar .fix-session { background: url("images/icons/tools.png") no-repeat center; } diff --git a/scripts/session-management.js b/scripts/session-management.js index d5fce2f..885acd1 100644 --- a/scripts/session-management.js +++ b/scripts/session-management.js @@ -153,14 +153,40 @@ var SessionManagement = (function SessionManagement() } }; - var restoreSession = function restoreSession(folderID, newWindow) + var restoreSession = function restoreSession(folderID, options) { browser.bookmarks.getChildren(folderID) .then(function (bookmarks) { - - if (newWindow) + if (options && options.newWindow) { - restoreSessions([bookmarks]); + createSessionWindow(bookmarks) + .then(function(createdWindow) { + if (createdWindow) + { + if (options.closeAll) + { + browser.windows.getAll({ + populate: true, + windowTypes: ['normal'] + }) + .then(function (windows) { + console.log(windows, createdWindow); + windows.forEach(function (mozWidnow) { + if (mozWidnow.id != createdWindow.id) { + browser.windows.remove(mozWidnow.id); + } + }); + }); + } + + if (options.closeWindow) + { + browser.windows.remove(options.closeWindow); + } + } + }, function onError() { + console.log('strage error'); + }); } else { @@ -189,14 +215,14 @@ var SessionManagement = (function SessionManagement() }); }; - var restoreSessions = function restoreSessions(windows) + var createSessionWindow = function createSessionWindow(bookmarks) { - windows.forEach (function (bookmarks) { + var action = new Promise((resolve, reject) => { if (bookmarks.length > 0) { browser.windows.create({}) .then(function(mozWindow) { - + resolve(mozWindow); var windowID = mozWindow.id; windowsTabsToKill[windowID] = mozWindow.tabs[0].id; @@ -221,6 +247,18 @@ var SessionManagement = (function SessionManagement() }); }); } + else + { + reject(); + } + }); + return action; + }; + + var restoreSessions = function restoreSessions(windows) + { + windows.forEach (function (bookmarks) { + createSessionWindow(bookmarks); }); }; @@ -285,7 +323,7 @@ var SessionManagement = (function SessionManagement() switch (message.event) { case 'restore-session': - restoreSession(message.bookmarkID, message.inNewWindow); + restoreSession(message.bookmarkID, message.options); break; case 'restore-sessions': From 5fcbe3e9a82fbf79e39584054539add2366518f4 Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Thu, 26 Dec 2019 01:09:39 +0200 Subject: [PATCH 09/32] changed icons --- data/images/icons_v2/menu.png | Bin 0 -> 260 bytes data/images/icons_v2/more.png | Bin 0 -> 406 bytes dev/css/session-header-bar.css | 4 ++-- dev/css/session-toolbar.css | 4 ++++ 4 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 data/images/icons_v2/menu.png create mode 100644 data/images/icons_v2/more.png diff --git a/data/images/icons_v2/menu.png b/data/images/icons_v2/menu.png new file mode 100644 index 0000000000000000000000000000000000000000..4229391a855e396cff502c91aee4e2d80441088d GIT binary patch literal 260 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGooCO|{#S9F3${@^GvDCf{D7ekj z#WAGf*4vv0d7Ai^+^+h*&jTYtpcitfPJ%0 zC>y!Q)z7PZyXd6W{=S~B`%mtkRDH}~o+@5^-r~TL7fH){n0dPN(mrrnnZ+JsTqpPN zk#bD>U(T%)?LLM4S>9atmIY)~S-S8x&f6_V)pCTzW^eF~$T?(|2okTG?t24SY$H$+ fBS-=W8a_ub9Es>zE4xGS6o~KX>gTe~DWM4fZgE~s literal 0 HcmV?d00001 diff --git a/data/images/icons_v2/more.png b/data/images/icons_v2/more.png new file mode 100644 index 0000000000000000000000000000000000000000..b94ad7ffc3726cf21723409609bfd7df0b190808 GIT binary patch literal 406 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGooCO|{#S9F3${@^GvDChdfq~J( z)5S5Q;?~<+j($vz0<0Hq?tIAg;+XOyNe=f3&JP5iaGdky=`3WurE-=@ST(>nO!%Jn zzcz!}ua-1EDwhQs%D~`Iy-d|#_4TChvr21ovuqyEoU&)i8?SwJC)e#uPl`FbOZ(-2 zi9hGRe3fL|WRv`(U8gr!qF?nkH(x^ZeASG)gCF>3O2^-ce!x)mxcfJc%z^fgM{}9j zHyGN5U$`30!k3U(Q^?=Ae&P3H<~`~5M?QrMKCcmraP3LhpAcQTe0t&Pb4Ga~zpHDn z$*x}<|2q3!4Ff}i;0spOi`-sI#HaY^YfOv_FxtBy<=+(lWgz|u(URL=nkKzuHFeNm z(HPnSmUVl<%E-X*$9V4ol`6+hu(awWZZDvQGxI;41oL+`<;@1GWqZMDx}oa=NcIZH fUIlOvJXlr395*$@@qFpO3m^ecS3j3^P6 Date: Thu, 26 Dec 2019 01:10:02 +0200 Subject: [PATCH 10/32] refactoring --- data/scripts/components/session-header-bar.js | 2 +- data/scripts/components/session-history.js | 1 - data/scripts/utils/confirm-box.js | 14 +------------- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/data/scripts/components/session-header-bar.js b/data/scripts/components/session-header-bar.js index 89ddf3e..cd968bc 100644 --- a/data/scripts/components/session-header-bar.js +++ b/data/scripts/components/session-header-bar.js @@ -10,7 +10,7 @@ define(function(require, exports) { // Utils const JSUtils = require('../utils/general'); const { HTMLCreator } = require('../utils/dom'); - const { WindowEvents, GlobalEvents } = require('../utils/global-events'); + const { WindowEvents } = require('../utils/global-events'); // ------------------------------------------------------------------------ // API diff --git a/data/scripts/components/session-history.js b/data/scripts/components/session-history.js index faa592c..dccb759 100644 --- a/data/scripts/components/session-history.js +++ b/data/scripts/components/session-history.js @@ -4,7 +4,6 @@ define(function(require, exports) { // ***************************************************************************** // Custom Modules - const { AppConfig } = require('../config'); const { WindowEvents, GlobalEvents } = require('../utils/global-events'); // ***************************************************************************** diff --git a/data/scripts/utils/confirm-box.js b/data/scripts/utils/confirm-box.js index b300d8f..2837122 100644 --- a/data/scripts/utils/confirm-box.js +++ b/data/scripts/utils/confirm-box.js @@ -5,9 +5,7 @@ define(function(require, exports) { // Custom Modules const { HTMLCreator } = require('./dom'); - const { WindowEvents, GlobalEvents } = require('./global-events'); - - var ID = 0; + const { WindowEvents } = require('./global-events'); // ***************************************************************************** // Bookmark Confirm Box @@ -37,7 +35,6 @@ define(function(require, exports) { panel.appendChild(info); panel.appendChild(controls); - var contextID = ID++; var callback; var defaultCallback = function defaultCallback() {}; @@ -64,7 +61,6 @@ define(function(require, exports) { info.textContent = options.message; setCallback(options.callback); - GlobalEvents.emit('ConfimBoxOpen', contextID); }; var hidePanel = function hidePanel() @@ -74,11 +70,6 @@ define(function(require, exports) { callback = defaultCallback; }; - var hideIfNotInvoked = function hideIfNotInvoked(ctxID) - { - if (contextID !== ctxID) hidePanel(); - }; - var blurMenu = function hideWhenClickOutside(e) { var target = e.target; @@ -94,9 +85,6 @@ define(function(require, exports) { callback = typeof func === 'function' ? func : defaultCallback; } - // probably used for closing other opened confirm boxes ? - GlobalEvents.on('ConfimBoxOpen', hideIfNotInvoked); - // Register custom events WindowEvents.on(document, options.name + '-Open', openPanel); WindowEvents.on(document, 'ConfimBox-Hide', hidePanel); From d5d1e495f23f73c2704fd662bde2170097e00811 Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Thu, 26 Dec 2019 01:13:01 +0200 Subject: [PATCH 11/32] improved session creation and context menus --- .../components/session-folder-events.js | 24 ++++++- data/scripts/session-sync-ui.js | 26 ++++++-- data/scripts/session/management.js | 64 ++++++++++++++----- 3 files changed, 90 insertions(+), 24 deletions(-) diff --git a/data/scripts/components/session-folder-events.js b/data/scripts/components/session-folder-events.js index 923f45f..5f2ca8a 100644 --- a/data/scripts/components/session-folder-events.js +++ b/data/scripts/components/session-folder-events.js @@ -176,7 +176,26 @@ define(function(require, exports) { { if (bookmarkContext) { - WindowEvents.emit(document, 'SessionContextMenu-Open', { + var contextMenu = undefined; + switch (bookmarkContext.bookmark.type) + { + case 'folder': + contextMenu = 'SessionContextMenu'; + break; + + case 'bookmark': + contextMenu = 'SessionContextBookmarkMenu'; + break; + + case 'separator': + contextMenu = 'SessionContextSeparatorMenu'; + break; + + default: + return; + } + + WindowEvents.emit(document, contextMenu + '-Open', { context: bookmarkContext.bookmark.id, event: e }); @@ -190,8 +209,7 @@ define(function(require, exports) { { if (bookmarkContext) { - var type = e.target.getAttribute('type'); - switch (type) + switch (bookmarkContext.bookmark.type) { case 'folder': WindowEvents.emit(document, 'SessionContextMenu-EditSession', bookmarkContext.bookmarkID); diff --git a/data/scripts/session-sync-ui.js b/data/scripts/session-sync-ui.js index 4edaa4e..7c8ccaf 100644 --- a/data/scripts/session-sync-ui.js +++ b/data/scripts/session-sync-ui.js @@ -142,11 +142,25 @@ define(function(require, exports) { SM.addMenuEntry({value: 'Restore (close all)', event: 'ReplaceSessionAll', icon: 'replace-all'}); SM.addMenuEntry({value: 'Edit', event: 'EditSession', icon: 'edit'}); SM.addMenuEntry({value: 'Delete', globalEvent: 'DeleteBookmarkItem', icon: 'delete'}); - SM.addMenuEntry({value: 'Create new session', callback: SessionManager.createNewSession, icon: 'new-session', separator: 'top'}); + SM.addMenuEntry({value: 'Create session', callback: SessionManager.createNewSession, icon: 'new-session', separator: 'top'}); + SM.addMenuEntry({value: 'Insert separator', callback: SessionManager.createSeparator, icon: 'new-session'}); content.appendChild(SM.DOMRoot); + var SCBM = new ContextMenu({name : 'SessionContextBookmarkMenu'}); + SCBM.addMenuEntry({value: 'Edit', event: 'EditBookmark', icon: 'edit'}); + SCBM.addMenuEntry({value: 'Delete', globalEvent: 'DeleteBookmarkItem', icon: 'delete'}); + SCBM.addMenuEntry({value: 'Create session', callback: SessionManager.createNewSession, icon: 'new-session', separator: 'top'}); + SCBM.addMenuEntry({value: 'Insert separator', callback: SessionManager.createSeparator, icon: 'new-session'}); + content.appendChild(SCBM.DOMRoot); + + var SCSM = new ContextMenu({name : 'SessionContextSeparatorMenu'}); + SCSM.addMenuEntry({value: 'Delete', globalEvent: 'DeleteBookmarkItem', icon: 'delete'}); + SCSM.addMenuEntry({value: 'Create session', callback: SessionManager.createNewSession, icon: 'new-session', separator: 'top'}); + SCSM.addMenuEntry({value: 'Insert separator', callback: SessionManager.createSeparator, icon: 'new-session'}); + content.appendChild(SCSM.DOMRoot); + var SLM = new ContextMenu({name : 'SessionListMenu'}); - SLM.addMenuEntry({value: 'Create new session', callback: SessionManager.createNewSession, icon: 'new-session'}); + SLM.addMenuEntry({value: 'Create session', callback: SessionManager.createNewSession, icon: 'new-session'}); content.appendChild(SLM.DOMRoot); // History List Context Menu @@ -269,7 +283,8 @@ define(function(require, exports) { // BookmarksCtxMenu events - WindowEvents.on(document, 'BookmarkCtxMenu-EditBookmark', function (bookmarkID) { + function InvokeBookmarkEdit(bookmarkID) + { WindowEvents.emit(document, 'BookmarkEditWidget-Invoke', { context : bookmarkID, fields : { @@ -277,7 +292,10 @@ define(function(require, exports) { url : SessionSyncModel.bookmarks[bookmarkID].url, } }); - }); + } + + WindowEvents.on(document, 'SessionContextBookmarkMenu-EditBookmark', InvokeBookmarkEdit); + WindowEvents.on(document, 'BookmarkCtxMenu-EditBookmark', InvokeBookmarkEdit); WindowEvents.on(document, 'BookmarkCtxMenu-CopyURL', function (bookmarkID) { WindowEvents.emit(document, 'CopyURL', SessionSyncModel.bookmarks[bookmarkID].url); diff --git a/data/scripts/session/management.js b/data/scripts/session/management.js index b5204b4..6fa938d 100644 --- a/data/scripts/session/management.js +++ b/data/scripts/session/management.js @@ -278,21 +278,6 @@ define(function(require, exports) { }); }; - var createSession = function createSession(title) - { - BookmarkManager.createBookmark({ - title: title, - parentId: AppConfig.get('storageID') - }) - .then(function success(session) { - WindowEvents.emit(document, 'SetPromiseSession', { - sessionID: session.id, - edit: true, - update: true }); - }, function error() { - }); - }; - var restoreSession = function restoreSession(folderID) { browser.runtime.sendMessage({event: 'restore-session', bookmarkID: folderID}); @@ -341,8 +326,52 @@ define(function(require, exports) { browser.runtime.sendMessage({event: 'fix-lazy-session', bookmarkID: folderID }); }; - var createNewSession = function createNewSession() { - createSession((new Date()).toLocaleString()); + var createSeparator = function createSeparator(folderID) { + browser.bookmarks.get(folderID) + .then(function (marks) { + BookmarkManager.createBookmark({ + index: marks[0].index + 1, + type: 'separator', + parentId: AppConfig.get('storageID') + }) + .then(function success(separator) { + WindowEvents.emit(document, 'SetPromiseSession', { sessionID: separator.id, update: true }); + }); + }, function onError(msg) { + console.log(msg); + }); + }; + + var createSession = function createSession(title, index) + { + BookmarkManager.createBookmark({ + index: index, + title: title, + parentId: AppConfig.get('storageID') + }) + .then(function success(session) { + WindowEvents.emit(document, 'SetPromiseSession', { + sessionID: session.id, + edit: true, + update: true + }); + }, function error() { + }); + }; + + var createNewSession = function createNewSession(folderID) { + var sessionTitle = (new Date()).toLocaleString(); + if (folderID) + { + browser.bookmarks.get(folderID) + .then(function (marks) { + createSession(sessionTitle, marks[0].index + 1); + }); + } + else + { + createSession(sessionTitle); + } }; // Events @@ -382,6 +411,7 @@ define(function(require, exports) { mergeSessions: mergeSessions, overwriteSession: overwriteSession, saveActiveSession: saveActiveSession, + createSeparator: createSeparator, createNewSession: createNewSession, saveHistorySession: saveHistorySession, }; From 8d32951d91338d18b6fc49b0c21f9400197f874c Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Wed, 29 Jan 2025 22:29:30 +0200 Subject: [PATCH 12/32] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bda30e4..a7cb943 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ SessionSync [![Session Sync](/data/images/icon32.png)](https://addons.mozilla.or ##### Firefox Add-on for storing browser sessions into bookmarks and sync them across multiple devices -[![alt tag](https://ffp4g1ylyit3jdyti1hqcvtb-wpengine.netdna-ssl.com/addons/files/2015/11/AMO-button_1.png)](https://addons.mozilla.org/en-US/firefox/addon/session-sync/) +[Install from Add-ons Website](https://addons.mozilla.org/en-US/firefox/addon/session-sync/) #### Description From c1ebf507f1f1baa31f77f1e692bdb5b49ac44818 Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Wed, 29 Jan 2025 22:32:07 +0200 Subject: [PATCH 13/32] Update home.html --- data/home/home.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/home/home.html b/data/home/home.html index 3ba3127..bf8cb14 100644 --- a/data/home/home.html +++ b/data/home/home.html @@ -59,7 +59,7 @@ You can focus on the search bar by pressing CTRL + F or F3
- Press left click to access aditional context menus for + Press right click to access aditional context menus for sessions or bookmarks
From 85ff3cffd312742625ea67357ef7fc8ea2eef2f7 Mon Sep 17 00:00:00 2001 From: Nicolas B <34476051+Nico04@users.noreply.github.com> Date: Sun, 5 Apr 2026 22:41:12 +0200 Subject: [PATCH 14/32] Support Firefox Multi-Account Containers (#127) * Handle Firefox tab Containers * refactor using containerSuffix * update manifest to 2025 requirement * add cookies permission * handle restore container from single tab restoration --- .gitignore | 1 + .../components/session-bookmark-events.js | 10 +- data/scripts/session-sync-ui.js | 15 ++- data/scripts/session/bookmarks.js | 26 ++++- manifest.json | 13 ++- scripts/session-history.js | 1 + scripts/session-management.js | 103 +++++++++++++----- 7 files changed, 131 insertions(+), 38 deletions(-) diff --git a/.gitignore b/.gitignore index 2b20e3e..afbc453 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /*.project /node_modules .vscode +.idea diff --git a/data/scripts/components/session-bookmark-events.js b/data/scripts/components/session-bookmark-events.js index f6dd6c6..c37ea56 100644 --- a/data/scripts/components/session-bookmark-events.js +++ b/data/scripts/components/session-bookmark-events.js @@ -209,10 +209,13 @@ define(function(require, exports) { if (bookmarkContext.bookmarkID == bookmarkID) { // Preview the content if a click event was registered (mouse-up on the same DOM node) if (!DragContext.hasContext()) { + var mark = SessionSyncModel.bookmarks[bookmarkID]; + var parsed = BookmarkManager.parseContainerFromTitle(mark.title); BookmarkManager.openBookmark({ - url: SessionSyncModel.bookmarks[bookmarkID].url, + url: mark.url, mode: BookmarkManager.getOpenMode(0), favicon: true, + cookieStoreId: parsed.cookieStoreId, }); } } @@ -318,10 +321,13 @@ define(function(require, exports) { // Middle Click if (e.button == 1) { + var mark = SessionSyncModel.bookmarks[bookmarkID]; + var parsed = BookmarkManager.parseContainerFromTitle(mark.title); BookmarkManager.openBookmark({ - url: SessionSyncModel.bookmarks[bookmarkID].url, + url: mark.url, mode: BookmarkManager.getOpenMode(1), favicon: true, + cookieStoreId: parsed.cookieStoreId, }); return; } diff --git a/data/scripts/session-sync-ui.js b/data/scripts/session-sync-ui.js index 7c8ccaf..79abaa2 100644 --- a/data/scripts/session-sync-ui.js +++ b/data/scripts/session-sync-ui.js @@ -306,26 +306,35 @@ define(function(require, exports) { }); WindowEvents.on(document, 'BookmarkCtxMenu-OpenInNewTab', function (bookmarkID) { + var mark = SessionSyncModel.bookmarks[bookmarkID]; + var parsed = BookmarkManager.parseContainerFromTitle(mark.title); BookmarkManager.openBookmark({ - url: SessionSyncModel.bookmarks[bookmarkID].url, + url: mark.url, mode: 'newTab', favicon: true, + cookieStoreId: parsed.cookieStoreId, }); }); WindowEvents.on(document, 'BookmarkCtxMenu-OpenInActiveTab', function (bookmarkID) { + var mark = SessionSyncModel.bookmarks[bookmarkID]; + var parsed = BookmarkManager.parseContainerFromTitle(mark.title); BookmarkManager.openBookmark({ - url: SessionSyncModel.bookmarks[bookmarkID].url, + url: mark.url, mode: 'activeTab', favicon: true, + cookieStoreId: parsed.cookieStoreId, }); }); WindowEvents.on(document, 'BookmarkCtxMenu-OpenInNewWindow', function (bookmarkID) { + var mark = SessionSyncModel.bookmarks[bookmarkID]; + var parsed = BookmarkManager.parseContainerFromTitle(mark.title); BookmarkManager.openBookmark({ - url: SessionSyncModel.bookmarks[bookmarkID].url, + url: mark.url, mode: 'newWindow', favicon: true, + cookieStoreId: parsed.cookieStoreId, }); }); diff --git a/data/scripts/session/bookmarks.js b/data/scripts/session/bookmarks.js index 7949ad5..6d74517 100644 --- a/data/scripts/session/bookmarks.js +++ b/data/scripts/session/bookmarks.js @@ -61,6 +61,12 @@ define(function(require, exports) { return state ? 'newTab' : 'activeTab'; }; + var containerSuffix = { + encode: function(id) { return ' [container:' + id + ']'; }, + regex: /^(.*) \[container:([^\]]+)\]$/, + default: 'firefox-default', + }; + var createBookmarkFromTab = function createBookmarkFromTab(tab, parentId) { var url = tab.url; @@ -74,14 +80,31 @@ define(function(require, exports) { browser.runtime.sendMessage({event: 'save-favicon', tab: tab}); } + var title = tab.title; + if (tab.cookieStoreId && tab.cookieStoreId !== containerSuffix.default) + { + title += containerSuffix.encode(tab.cookieStoreId); + } + return browser.bookmarks.create({ url: url, - title: tab.title, + title: title, parentId: parentId }); }; + // Parse a [container:] suffix from a bookmark title. + // Returns { title, cookieStoreId } — cookieStoreId is null when absent. + var parseContainerFromTitle = function parseContainerFromTitle(rawTitle) + { + var match = rawTitle && rawTitle.match(containerSuffix.regex); + if (match) { + return { title: match[1], cookieStoreId: match[2] }; + } + return { title: rawTitle, cookieStoreId: null }; + }; + var createBookmark = function createBookmark(options) { return browser.bookmarks.create(options); @@ -155,6 +178,7 @@ define(function(require, exports) { openBookmark: openBookmark, createBookmark: createBookmark, createBookmarkFromTab: createBookmarkFromTab, + parseContainerFromTitle: parseContainerFromTitle, searchBookmarks: searchBookmarks, setTitle: setTitle, diff --git a/manifest.json b/manifest.json index 1c36fbe..9a76f15 100644 --- a/manifest.json +++ b/manifest.json @@ -1,14 +1,17 @@ { "name": "Session Sync", - "version": "3.1.12", + "version": "3.2.0", "author": "Gabriel Ivanica", "description": "Save sessions as bookmarks and sync them through Firefox Sync (or any other sync engine).\nPowerful session management - organize, edit, change, save, restore, etc\n", "homepage_url": "https://github.com/ReDEnergy/SessionSync", "manifest_version": 2, "default_locale": "en", - "applications": { + "browser_specific_settings": { "gecko": { - "id": "session-sync@gabrielivanica.com" + "id": "session-sync@gabrielivanica.com", + "data_collection_permissions": { + "required": ["none"] + } } }, "icons": { @@ -33,7 +36,9 @@ "bookmarks", "tabs", "clipboardWrite", - "unlimitedStorage" + "unlimitedStorage", + "cookies", + "contextualIdentities" ], "browser_action": { "browser_style": true, diff --git a/scripts/session-history.js b/scripts/session-history.js index aca8025..9e59297 100644 --- a/scripts/session-history.js +++ b/scripts/session-history.js @@ -53,6 +53,7 @@ BrowserSession.prototype.update = function update() url: info.url, favIconUrl: info.lazyLoading ? info.favIconUrl : tab.favIconUrl, pinned: tab.pinned, + cookieStoreId: tab.cookieStoreId, }; tabCount++; windowInfo.tabs[tab.index] = sessionTab; diff --git a/scripts/session-management.js b/scripts/session-management.js index 885acd1..191d443 100644 --- a/scripts/session-management.js +++ b/scripts/session-management.js @@ -36,6 +36,50 @@ var SessionManagement = (function SessionManagement() }; } + // Container suffix helpers — canonical source: data/scripts/session/bookmarks.js + var containerSuffix = { + regex: /^(.*) \[container:([^\]]+)\]$/, + }; + + // Parse a [container:] suffix from a bookmark title. + // Returns { title, cookieStoreId } — cookieStoreId is null when absent. + function parseContainerFromTitle(rawTitle) + { + var match = rawTitle && rawTitle.match(containerSuffix.regex); + if (match) { + return { title: match[1], cookieStoreId: match[2] }; + } + return { title: rawTitle, cookieStoreId: null }; + } + + // Create a tab with a cookieStoreId; silently falls back to default container + // if the container doesn't exist or the API rejects it. + // onError is called only if creation fails even without a container. + function createTabWithContainer(options, cookieStoreId, onSuccess, onError) + { + var tryCreate = function(withContainer) { + var tabOptions = Object.assign({}, options); + if (withContainer && cookieStoreId) { + tabOptions.cookieStoreId = cookieStoreId; + } + return browser.tabs.create(tabOptions).then( + function(tabInfo) { + if (onSuccess) onSuccess(tabInfo); + }, + function(err) { + if (withContainer) { + // Container not available — retry without it + tryCreate(false); + } else { + if (onError) onError(err); + else console.log('createTabWithContainer error:', err); + } + } + ); + }; + tryCreate(cookieStoreId != null); + } + var setLazyLoading = function setLazyLoading(state) { lazyLoading = state; @@ -64,17 +108,23 @@ var SessionManagement = (function SessionManagement() var checkFavicon = options.favicon === true ? FaviconService.checkTabFaviconSaved : () => {}; delete options.favicon; + var cookieStoreId = options.cookieStoreId || null; + delete options.cookieStoreId; + var mode = options.mode; delete options.mode; switch (mode) { case 'newTab': { - browser.tabs.create(options).then(checkFavicon); + createTabWithContainer(options, cookieStoreId, checkFavicon); break; } case 'newWindow': { + if (cookieStoreId) { + options.cookieStoreId = cookieStoreId; + } browser.windows.create(options).then(function (windowInfo) { checkFavicon(windowInfo.tabs[0]); }); @@ -106,24 +156,19 @@ var SessionManagement = (function SessionManagement() title += '&favIconUrl=' + encodeURIComponent(bookmark.favIconUrl); } - browser.tabs.create({ + var tabOptions = { url: title, active: false, pinned: bookmark.pinned, windowId: bookmark.windowID, - }) - .then( - function success() { - }, + }; + + createTabWithContainer(tabOptions, bookmark.cookieStoreId, + function success() {}, function error() { - browser.tabs.create({ - url: title, - active: false, - pinned: bookmark.pinned, - windowId: bookmark.windowID, - }) + browser.tabs.create(tabOptions) .then(function (tab) { - browser.tabs.update([tab.id], { + browser.tabs.update(tab.id, { title: bookmark.title, }); }); @@ -133,6 +178,11 @@ var SessionManagement = (function SessionManagement() var openLazyTab = function openLazyTab(bookmark) { + // Extract container info encoded in the title + var parsed = parseContainerFromTitle(bookmark.title); + bookmark.title = parsed.title; + bookmark.cookieStoreId = parsed.cookieStoreId; + if (bookmark.favIconUrl) { createLazyTab(bookmark); } @@ -199,13 +249,12 @@ var SessionManagement = (function SessionManagement() } else { - browser.tabs.create({ - url: bookmark.url, - active: false, - }) - .then(function(tabInfo) { - FaviconService.checkTabFaviconSaved(tabInfo); - }); + var parsed = parseContainerFromTitle(bookmark.title); + createTabWithContainer( + { url: bookmark.url, active: false }, + parsed.cookieStoreId, + function(tabInfo) { FaviconService.checkTabFaviconSaved(tabInfo); } + ); } }); } @@ -235,14 +284,12 @@ var SessionManagement = (function SessionManagement() } else { - browser.tabs.create({ - url: bookmark.url, - pinned: bookmark.pinned, - windowId: windowID, - }) - .then(function(tabInfo) { - FaviconService.checkTabFaviconSaved(tabInfo); - }); + var parsed = parseContainerFromTitle(bookmark.title); + createTabWithContainer( + { url: bookmark.url, pinned: bookmark.pinned, windowId: windowID }, + parsed.cookieStoreId, + function(tabInfo) { FaviconService.checkTabFaviconSaved(tabInfo); } + ); } }); }); From 4d61ce96813463a36692fe0cc0b184e3ed89272c Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Sun, 5 Apr 2026 23:13:52 +0300 Subject: [PATCH 15/32] Refactoring --- data/scripts/app.js | 8 +++++--- data/scripts/components/session-filtering.js | 2 +- data/scripts/session-sync-ui.js | 8 ++++---- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/data/scripts/app.js b/data/scripts/app.js index 8555ead..8472aaa 100644 --- a/data/scripts/app.js +++ b/data/scripts/app.js @@ -12,22 +12,24 @@ define(function(require, exports) { const { BookmarkManager } = require('./session/bookmarks'); + const sessionRoot = 'SessionSync'; + // ------------------------------------------------------------------------ // API var findSessionSync = function findSessionSync(callback) { - BookmarkManager.searchBookmarks({title: 'SessionSync'}) + BookmarkManager.searchBookmarks({title: sessionRoot}) .then( function success(bookmarks) { if (bookmarks.length == 0) { BookmarkManager.createBookmark({ - title: 'SessionSync', + title: sessionRoot, parentId: 'menu________' }).then(callback, function error() { BookmarkManager.createBookmark({ - title: 'SessionSync', + title: sessionRoot, }).then(callback); }); } diff --git a/data/scripts/components/session-filtering.js b/data/scripts/components/session-filtering.js index 706709a..fff5ae9 100644 --- a/data/scripts/components/session-filtering.js +++ b/data/scripts/components/session-filtering.js @@ -89,7 +89,7 @@ define(function(require, exports) { createSession.addEventListener('mouseover', function(e) { WindowEvents.emit(document, 'ShowTooltip', { node: e.target, - message: 'Create session', + message: 'Create new session', }); }); diff --git a/data/scripts/session-sync-ui.js b/data/scripts/session-sync-ui.js index 79abaa2..a6f5ed2 100644 --- a/data/scripts/session-sync-ui.js +++ b/data/scripts/session-sync-ui.js @@ -142,25 +142,25 @@ define(function(require, exports) { SM.addMenuEntry({value: 'Restore (close all)', event: 'ReplaceSessionAll', icon: 'replace-all'}); SM.addMenuEntry({value: 'Edit', event: 'EditSession', icon: 'edit'}); SM.addMenuEntry({value: 'Delete', globalEvent: 'DeleteBookmarkItem', icon: 'delete'}); - SM.addMenuEntry({value: 'Create session', callback: SessionManager.createNewSession, icon: 'new-session', separator: 'top'}); + SM.addMenuEntry({value: 'Create new session', callback: SessionManager.createNewSession, icon: 'new-session', separator: 'top'}); SM.addMenuEntry({value: 'Insert separator', callback: SessionManager.createSeparator, icon: 'new-session'}); content.appendChild(SM.DOMRoot); var SCBM = new ContextMenu({name : 'SessionContextBookmarkMenu'}); SCBM.addMenuEntry({value: 'Edit', event: 'EditBookmark', icon: 'edit'}); SCBM.addMenuEntry({value: 'Delete', globalEvent: 'DeleteBookmarkItem', icon: 'delete'}); - SCBM.addMenuEntry({value: 'Create session', callback: SessionManager.createNewSession, icon: 'new-session', separator: 'top'}); + SCBM.addMenuEntry({value: 'Create new session', callback: SessionManager.createNewSession, icon: 'new-session', separator: 'top'}); SCBM.addMenuEntry({value: 'Insert separator', callback: SessionManager.createSeparator, icon: 'new-session'}); content.appendChild(SCBM.DOMRoot); var SCSM = new ContextMenu({name : 'SessionContextSeparatorMenu'}); SCSM.addMenuEntry({value: 'Delete', globalEvent: 'DeleteBookmarkItem', icon: 'delete'}); - SCSM.addMenuEntry({value: 'Create session', callback: SessionManager.createNewSession, icon: 'new-session', separator: 'top'}); + SCSM.addMenuEntry({value: 'Create new session', callback: SessionManager.createNewSession, icon: 'new-session', separator: 'top'}); SCSM.addMenuEntry({value: 'Insert separator', callback: SessionManager.createSeparator, icon: 'new-session'}); content.appendChild(SCSM.DOMRoot); var SLM = new ContextMenu({name : 'SessionListMenu'}); - SLM.addMenuEntry({value: 'Create session', callback: SessionManager.createNewSession, icon: 'new-session'}); + SLM.addMenuEntry({value: 'Create new session', callback: SessionManager.createNewSession, icon: 'new-session'}); content.appendChild(SLM.DOMRoot); // History List Context Menu From a51b5ffe2dd485739e41ff711a31d7a841a7e838 Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Sun, 5 Apr 2026 23:22:39 +0300 Subject: [PATCH 16/32] Upgraded project to use Web Extension Framework (WXT) --- .env | 2 + .eslintrc.json | 33 + .gitignore | 8 + _locales/en/messages.json | 5 - data/scripts/3rd/RequireJS/require.js | 5 - data/scripts/3rd/pubsub.js | 361 ++++--- data/scripts/app.js | 131 ++- data/scripts/components/clipboard.js | 53 +- data/scripts/components/config-panel.js | 627 ++++++------ .../scripts/components/notification-system.js | 72 +- data/scripts/components/overlay-system.js | 86 +- .../components/session-bookmark-events.js | 694 +++++++------- data/scripts/components/session-bookmark.js | 229 ++--- data/scripts/components/session-container.js | 888 ++++++++--------- data/scripts/components/session-filtering.js | 246 +++-- .../components/session-folder-events.js | 583 +++++------ data/scripts/components/session-folder.js | 176 ++-- data/scripts/components/session-header-bar.js | 510 +++++----- data/scripts/components/session-history.js | 191 ++-- .../components/session-hotkey-manager.js | 359 ++++--- data/scripts/components/session-hotkeys.js | 106 +- data/scripts/components/session-list.js | 323 +++---- data/scripts/components/session-sorting.js | 185 ++-- data/scripts/components/session-sync-model.js | 903 +++++++++--------- data/scripts/components/session-toolbar.js | 470 ++++----- data/scripts/components/tooltip-system.js | 69 +- data/scripts/components/trash-can.js | 187 ++-- data/scripts/components/url-bar.js | 153 ++- data/scripts/config.js | 344 ++++--- data/scripts/require-config.js | 19 - data/scripts/session-sync-ui.js | 811 ++++++++-------- data/scripts/session/bookmarks.js | 368 ++++--- data/scripts/session/management.js | 843 ++++++++-------- data/scripts/template.js | 26 - data/scripts/utils/auto-scroll.js | 140 +-- data/scripts/utils/components.js | 554 ++++++----- data/scripts/utils/confirm-box.js | 142 ++- data/scripts/utils/context-menu.js | 230 +++-- data/scripts/utils/dom.js | 38 +- data/scripts/utils/drag-context.js | 70 +- data/scripts/utils/field-edit-widget.js | 212 ++-- data/scripts/utils/general.js | 80 +- data/scripts/utils/global-events.js | 176 ++-- data/session-sync.css | 31 +- dev/css/session-toolbar.css | 7 +- entrypoints/background.js | 14 + entrypoints/popup/index.html | 15 + entrypoints/popup/main.js | 23 + gulpfile.js | 124 --- manifest.json | 73 -- pack_addon.bat | 16 - package.json | 44 +- public/_locales/en/messages.json | 14 + {data => public}/home/home.css | 0 {data => public}/home/home.html | 56 +- {data => public}/home/home.js | 32 +- {data => public}/lazy/lazy.css | 0 {data => public}/lazy/lazy.html | 0 {data => public}/lazy/lazy.js | 0 scripts/development.js | 25 +- scripts/favicon-service.js | 83 +- scripts/main.js | 167 ++-- scripts/session-history.js | 356 +++---- scripts/session-management.js | 405 ++++---- wxt.config.js | 69 ++ 65 files changed, 6446 insertions(+), 6816 deletions(-) create mode 100644 .env create mode 100644 .eslintrc.json delete mode 100644 _locales/en/messages.json delete mode 100644 data/scripts/3rd/RequireJS/require.js delete mode 100644 data/scripts/require-config.js delete mode 100644 data/scripts/template.js create mode 100644 entrypoints/background.js create mode 100644 entrypoints/popup/index.html create mode 100644 entrypoints/popup/main.js delete mode 100644 gulpfile.js delete mode 100644 manifest.json delete mode 100644 pack_addon.bat create mode 100644 public/_locales/en/messages.json rename {data => public}/home/home.css (100%) rename {data => public}/home/home.html (67%) rename {data => public}/home/home.js (90%) rename {data => public}/lazy/lazy.css (100%) rename {data => public}/lazy/lazy.html (100%) rename {data => public}/lazy/lazy.js (100%) create mode 100644 wxt.config.js diff --git a/.env b/.env new file mode 100644 index 0000000..744eeec --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +FIREFOX_BINARY="D:\Program Files\Firefox Developer Edition\firefox.exe" +FIREFOX_PROFILE="D:\Programs\Firefox\Addon" \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..e17bdbf --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,33 @@ +{ + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module" + }, + "env": { + "browser": true + }, + "extends": "eslint:recommended", + "rules": { + "indent": [ + "error", + "tab", + { + "SwitchCase": 1 + } + ], + "linebreak-style": [ + "error", + "windows" + ], + "quotes": [ + "error", + "single" + ], + "semi": [ + "error", + "always" + ], + "no-console": "off", + "no-undef": "off" + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index afbc453..ae33555 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,11 @@ /node_modules .vscode .idea + +.wxt +.output + +/dev/tutorial +/dev/experiments +/releases +/data/session-sync.css diff --git a/_locales/en/messages.json b/_locales/en/messages.json deleted file mode 100644 index 1fd8eb3..0000000 --- a/_locales/en/messages.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "test": { - "message": "Restore in new window" - } -} \ No newline at end of file diff --git a/data/scripts/3rd/RequireJS/require.js b/data/scripts/3rd/RequireJS/require.js deleted file mode 100644 index a3ca583..0000000 --- a/data/scripts/3rd/RequireJS/require.js +++ /dev/null @@ -1,5 +0,0 @@ -/** vim: et:ts=4:sw=4:sts=4 - * @license RequireJS 2.3.5 Copyright jQuery Foundation and other contributors. - * Released under MIT license, https://github.com/requirejs/requirejs/blob/master/LICENSE - */ -var requirejs,require,define;!function(global,setTimeout){function commentReplace(e,t){return t||""}function isFunction(e){return"[object Function]"===ostring.call(e)}function isArray(e){return"[object Array]"===ostring.call(e)}function each(e,t){if(e){var i;for(i=0;i-1&&(!e[i]||!t(e[i],i,e));i-=1);}}function hasProp(e,t){return hasOwn.call(e,t)}function getOwn(e,t){return hasProp(e,t)&&e[t]}function eachProp(e,t){var i;for(i in e)if(hasProp(e,i)&&t(e[i],i))break}function mixin(e,t,i,r){return t&&eachProp(t,function(t,n){!i&&hasProp(e,n)||(!r||"object"!=typeof t||!t||isArray(t)||isFunction(t)||t instanceof RegExp?e[n]=t:(e[n]||(e[n]={}),mixin(e[n],t,i,r)))}),e}function bind(e,t){return function(){return t.apply(e,arguments)}}function scripts(){return document.getElementsByTagName("script")}function defaultOnError(e){throw e}function getGlobal(e){if(!e)return e;var t=global;return each(e.split("."),function(e){t=t[e]}),t}function makeError(e,t,i,r){var n=new Error(t+"\nhttp://requirejs.org/docs/errors.html#"+e);return n.requireType=e,n.requireModules=r,i&&(n.originalError=i),n}function newContext(e){function t(e){var t,i;for(t=0;t0&&(e.splice(t-1,2),t-=2)}}function i(e,i,r){var n,o,a,s,u,c,d,p,f,l,h=i&&i.split("/"),m=y.map,g=m&&m["*"];if(e&&(c=(e=e.split("/")).length-1,y.nodeIdCompat&&jsSuffixRegExp.test(e[c])&&(e[c]=e[c].replace(jsSuffixRegExp,"")),"."===e[0].charAt(0)&&h&&(e=h.slice(0,h.length-1).concat(e)),t(e),e=e.join("/")),r&&m&&(h||g)){e:for(a=(o=e.split("/")).length;a>0;a-=1){if(u=o.slice(0,a).join("/"),h)for(s=h.length;s>0;s-=1)if((n=getOwn(m,h.slice(0,s).join("/")))&&(n=getOwn(n,u))){d=n,p=a;break e}!f&&g&&getOwn(g,u)&&(f=getOwn(g,u),l=a)}!d&&f&&(d=f,p=l),d&&(o.splice(0,p,d),e=o.join("/"))}return getOwn(y.pkgs,e)||e}function r(e){isBrowser&&each(scripts(),function(t){if(t.getAttribute("data-requiremodule")===e&&t.getAttribute("data-requirecontext")===q.contextName)return t.parentNode.removeChild(t),!0})}function n(e){var t=getOwn(y.paths,e);if(t&&isArray(t)&&t.length>1)return t.shift(),q.require.undef(e),q.makeRequire(null,{skipMap:!0})([e]),!0}function o(e){var t,i=e?e.indexOf("!"):-1;return i>-1&&(t=e.substring(0,i),e=e.substring(i+1,e.length)),[t,e]}function a(e,t,r,n){var a,s,u,c,d=null,p=t?t.name:null,f=e,l=!0,h="";return e||(l=!1,e="_@r"+(T+=1)),c=o(e),d=c[0],e=c[1],d&&(d=i(d,p,n),s=getOwn(j,d)),e&&(d?h=r?e:s&&s.normalize?s.normalize(e,function(e){return i(e,p,n)}):-1===e.indexOf("!")?i(e,p,n):e:(d=(c=o(h=i(e,p,n)))[0],h=c[1],r=!0,a=q.nameToUrl(h))),u=!d||s||r?"":"_unnormalized"+(A+=1),{prefix:d,name:h,parentMap:t,unnormalized:!!u,url:a,originalName:f,isDefine:l,id:(d?d+"!"+h:h)+u}}function s(e){var t=e.id,i=getOwn(S,t);return i||(i=S[t]=new q.Module(e)),i}function u(e,t,i){var r=e.id,n=getOwn(S,r);!hasProp(j,r)||n&&!n.defineEmitComplete?(n=s(e)).error&&"error"===t?i(n.error):n.on(t,i):"defined"===t&&i(j[r])}function c(e,t){var i=e.requireModules,r=!1;t?t(e):(each(i,function(t){var i=getOwn(S,t);i&&(i.error=e,i.events.error&&(r=!0,i.emit("error",e)))}),r||req.onError(e))}function d(){globalDefQueue.length&&(each(globalDefQueue,function(e){var t=e[0];"string"==typeof t&&(q.defQueueMap[t]=!0),O.push(e)}),globalDefQueue=[])}function p(e){delete S[e],delete k[e]}function f(e,t,i){var r=e.map.id;e.error?e.emit("error",e.error):(t[r]=!0,each(e.depMaps,function(r,n){var o=r.id,a=getOwn(S,o);!a||e.depMatched[n]||i[o]||(getOwn(t,o)?(e.defineDep(n,j[o]),e.check()):f(a,t,i))}),i[r]=!0)}function l(){var e,t,i=1e3*y.waitSeconds,o=i&&q.startTime+i<(new Date).getTime(),a=[],s=[],u=!1,d=!0;if(!x){if(x=!0,eachProp(k,function(e){var i=e.map,c=i.id;if(e.enabled&&(i.isDefine||s.push(e),!e.error))if(!e.inited&&o)n(c)?(t=!0,u=!0):(a.push(c),r(c));else if(!e.inited&&e.fetched&&i.isDefine&&(u=!0,!i.prefix))return d=!1}),o&&a.length)return e=makeError("timeout","Load timeout for modules: "+a,null,a),e.contextName=q.contextName,c(e);d&&each(s,function(e){f(e,{},{})}),o&&!t||!u||!isBrowser&&!isWebWorker||w||(w=setTimeout(function(){w=0,l()},50)),x=!1}}function h(e){hasProp(j,e[0])||s(a(e[0],null,!0)).init(e[1],e[2])}function m(e,t,i,r){e.detachEvent&&!isOpera?r&&e.detachEvent(r,t):e.removeEventListener(i,t,!1)}function g(e){var t=e.currentTarget||e.srcElement;return m(t,q.onScriptLoad,"load","onreadystatechange"),m(t,q.onScriptError,"error"),{node:t,id:t&&t.getAttribute("data-requiremodule")}}function v(){var e;for(d();O.length;){if(null===(e=O.shift())[0])return c(makeError("mismatch","Mismatched anonymous define() module: "+e[e.length-1]));h(e)}q.defQueueMap={}}var x,b,q,E,w,y={waitSeconds:7,baseUrl:"./",paths:{},bundles:{},pkgs:{},shim:{},config:{}},S={},k={},M={},O=[],j={},P={},R={},T=1,A=1;return E={require:function(e){return e.require?e.require:e.require=q.makeRequire(e.map)},exports:function(e){if(e.usingExports=!0,e.map.isDefine)return e.exports?j[e.map.id]=e.exports:e.exports=j[e.map.id]={}},module:function(e){return e.module?e.module:e.module={id:e.map.id,uri:e.map.url,config:function(){return getOwn(y.config,e.map.id)||{}},exports:e.exports||(e.exports={})}}},b=function(e){this.events=getOwn(M,e.id)||{},this.map=e,this.shim=getOwn(y.shim,e.id),this.depExports=[],this.depMaps=[],this.depMatched=[],this.pluginMaps={},this.depCount=0},b.prototype={init:function(e,t,i,r){r=r||{},this.inited||(this.factory=t,i?this.on("error",i):this.events.error&&(i=bind(this,function(e){this.emit("error",e)})),this.depMaps=e&&e.slice(0),this.errback=i,this.inited=!0,this.ignore=r.ignore,r.enabled||this.enabled?this.enable():this.check())},defineDep:function(e,t){this.depMatched[e]||(this.depMatched[e]=!0,this.depCount-=1,this.depExports[e]=t)},fetch:function(){if(!this.fetched){this.fetched=!0,q.startTime=(new Date).getTime();var e=this.map;if(!this.shim)return e.prefix?this.callPlugin():this.load();q.makeRequire(this.map,{enableBuildCallback:!0})(this.shim.deps||[],bind(this,function(){return e.prefix?this.callPlugin():this.load()}))}},load:function(){var e=this.map.url;P[e]||(P[e]=!0,q.load(this.map.id,e))},check:function(){if(this.enabled&&!this.enabling){var e,t,i=this.map.id,r=this.depExports,n=this.exports,o=this.factory;if(this.inited){if(this.error)this.emit("error",this.error);else if(!this.defining){if(this.defining=!0,this.depCount<1&&!this.defined){if(isFunction(o)){if(this.events.error&&this.map.isDefine||req.onError!==defaultOnError)try{n=q.execCb(i,o,r,n)}catch(t){e=t}else n=q.execCb(i,o,r,n);if(this.map.isDefine&&void 0===n&&((t=this.module)?n=t.exports:this.usingExports&&(n=this.exports)),e)return e.requireMap=this.map,e.requireModules=this.map.isDefine?[this.map.id]:null,e.requireType=this.map.isDefine?"define":"require",c(this.error=e)}else n=o;if(this.exports=n,this.map.isDefine&&!this.ignore&&(j[i]=n,req.onResourceLoad)){var a=[];each(this.depMaps,function(e){a.push(e.normalizedMap||e)}),req.onResourceLoad(q,this.map,a)}p(i),this.defined=!0}this.defining=!1,this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}else hasProp(q.defQueueMap,i)||this.fetch()}},callPlugin:function(){var e=this.map,t=e.id,r=a(e.prefix);this.depMaps.push(r),u(r,"defined",bind(this,function(r){var n,o,d,f=getOwn(R,this.map.id),l=this.map.name,h=this.map.parentMap?this.map.parentMap.name:null,m=q.makeRequire(e.parentMap,{enableBuildCallback:!0});return this.map.unnormalized?(r.normalize&&(l=r.normalize(l,function(e){return i(e,h,!0)})||""),o=a(e.prefix+"!"+l,this.map.parentMap,!0),u(o,"defined",bind(this,function(e){this.map.normalizedMap=o,this.init([],function(){return e},null,{enabled:!0,ignore:!0})})),void((d=getOwn(S,o.id))&&(this.depMaps.push(o),this.events.error&&d.on("error",bind(this,function(e){this.emit("error",e)})),d.enable()))):f?(this.map.url=q.nameToUrl(f),void this.load()):((n=bind(this,function(e){this.init([],function(){return e},null,{enabled:!0})})).error=bind(this,function(e){this.inited=!0,this.error=e,e.requireModules=[t],eachProp(S,function(e){0===e.map.id.indexOf(t+"_unnormalized")&&p(e.map.id)}),c(e)}),n.fromText=bind(this,function(i,r){var o=e.name,u=a(o),d=useInteractive;r&&(i=r),d&&(useInteractive=!1),s(u),hasProp(y.config,t)&&(y.config[o]=y.config[t]);try{req.exec(i)}catch(e){return c(makeError("fromtexteval","fromText eval for "+t+" failed: "+e,e,[t]))}d&&(useInteractive=!0),this.depMaps.push(u),q.completeLoad(o),m([o],n)}),void r.load(e.name,m,n,y))})),q.enable(r,this),this.pluginMaps[r.id]=r},enable:function(){k[this.map.id]=this,this.enabled=!0,this.enabling=!0,each(this.depMaps,bind(this,function(e,t){var i,r,n;if("string"==typeof e){if(e=a(e,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap),this.depMaps[t]=e,n=getOwn(E,e.id))return void(this.depExports[t]=n(this));this.depCount+=1,u(e,"defined",bind(this,function(e){this.undefed||(this.defineDep(t,e),this.check())})),this.errback?u(e,"error",bind(this,this.errback)):this.events.error&&u(e,"error",bind(this,function(e){this.emit("error",e)}))}i=e.id,r=S[i],hasProp(E,i)||!r||r.enabled||q.enable(e,this)})),eachProp(this.pluginMaps,bind(this,function(e){var t=getOwn(S,e.id);t&&!t.enabled&&q.enable(e,this)})),this.enabling=!1,this.check()},on:function(e,t){var i=this.events[e];i||(i=this.events[e]=[]),i.push(t)},emit:function(e,t){each(this.events[e],function(e){e(t)}),"error"===e&&delete this.events[e]}},q={config:y,contextName:e,registry:S,defined:j,urlFetched:P,defQueue:O,defQueueMap:{},Module:b,makeModuleMap:a,nextTick:req.nextTick,onError:c,configure:function(e){if(e.baseUrl&&"/"!==e.baseUrl.charAt(e.baseUrl.length-1)&&(e.baseUrl+="/"),"string"==typeof e.urlArgs){var t=e.urlArgs;e.urlArgs=function(e,i){return(-1===i.indexOf("?")?"?":"&")+t}}var i=y.shim,r={paths:!0,bundles:!0,config:!0,map:!0};eachProp(e,function(e,t){r[t]?(y[t]||(y[t]={}),mixin(y[t],e,!0,!0)):y[t]=e}),e.bundles&&eachProp(e.bundles,function(e,t){each(e,function(e){e!==t&&(R[e]=t)})}),e.shim&&(eachProp(e.shim,function(e,t){isArray(e)&&(e={deps:e}),!e.exports&&!e.init||e.exportsFn||(e.exportsFn=q.makeShimExports(e)),i[t]=e}),y.shim=i),e.packages&&each(e.packages,function(e){var t;t=(e="string"==typeof e?{name:e}:e).name,e.location&&(y.paths[t]=e.location),y.pkgs[t]=e.name+"/"+(e.main||"main").replace(currDirRegExp,"").replace(jsSuffixRegExp,"")}),eachProp(S,function(e,t){e.inited||e.map.unnormalized||(e.map=a(t,null,!0))}),(e.deps||e.callback)&&q.require(e.deps||[],e.callback)},makeShimExports:function(e){return function(){var t;return e.init&&(t=e.init.apply(global,arguments)),t||e.exports&&getGlobal(e.exports)}},makeRequire:function(t,n){function o(i,r,u){var d,p,f;return n.enableBuildCallback&&r&&isFunction(r)&&(r.__requireJsBuild=!0),"string"==typeof i?isFunction(r)?c(makeError("requireargs","Invalid require call"),u):t&&hasProp(E,i)?E[i](S[t.id]):req.get?req.get(q,i,t,o):(p=a(i,t,!1,!0),d=p.id,hasProp(j,d)?j[d]:c(makeError("notloaded",'Module name "'+d+'" has not been loaded yet for context: '+e+(t?"":". Use require([])")))):(v(),q.nextTick(function(){v(),(f=s(a(null,t))).skipMap=n.skipMap,f.init(i,r,u,{enabled:!0}),l()}),o)}return n=n||{},mixin(o,{isBrowser:isBrowser,toUrl:function(e){var r,n=e.lastIndexOf("."),o=e.split("/")[0],a="."===o||".."===o;return-1!==n&&(!a||n>1)&&(r=e.substring(n,e.length),e=e.substring(0,n)),q.nameToUrl(i(e,t&&t.id,!0),r,!0)},defined:function(e){return hasProp(j,a(e,t,!1,!0).id)},specified:function(e){return e=a(e,t,!1,!0).id,hasProp(j,e)||hasProp(S,e)}}),t||(o.undef=function(e){d();var i=a(e,t,!0),n=getOwn(S,e);n.undefed=!0,r(e),delete j[e],delete P[i.url],delete M[e],eachReverse(O,function(t,i){t[0]===e&&O.splice(i,1)}),delete q.defQueueMap[e],n&&(n.events.defined&&(M[e]=n.events),p(e))}),o},enable:function(e){getOwn(S,e.id)&&s(e).enable()},completeLoad:function(e){var t,i,r,o=getOwn(y.shim,e)||{},a=o.exports;for(d();O.length;){if(null===(i=O.shift())[0]){if(i[0]=e,t)break;t=!0}else i[0]===e&&(t=!0);h(i)}if(q.defQueueMap={},r=getOwn(S,e),!t&&!hasProp(j,e)&&r&&!r.inited){if(!(!y.enforceDefine||a&&getGlobal(a)))return n(e)?void 0:c(makeError("nodefine","No define call for "+e,null,[e]));h([e,o.deps||[],o.exportsFn])}l()},nameToUrl:function(e,t,i){var r,n,o,a,s,u,c,d=getOwn(y.pkgs,e);if(d&&(e=d),c=getOwn(R,e))return q.nameToUrl(c,t,i);if(req.jsExtRegExp.test(e))s=e+(t||"");else{for(r=y.paths,o=(n=e.split("/")).length;o>0;o-=1)if(a=n.slice(0,o).join("/"),u=getOwn(r,a)){isArray(u)&&(u=u[0]),n.splice(0,o,u);break}s=n.join("/"),s=("/"===(s+=t||(/^data\:|^blob\:|\?/.test(s)||i?"":".js")).charAt(0)||s.match(/^[\w\+\.\-]+:/)?"":y.baseUrl)+s}return y.urlArgs&&!/^blob\:/.test(s)?s+y.urlArgs(e,s):s},load:function(e,t){req.load(q,e,t)},execCb:function(e,t,i,r){return t.apply(r,i)},onScriptLoad:function(e){if("load"===e.type||readyRegExp.test((e.currentTarget||e.srcElement).readyState)){interactiveScript=null;var t=g(e);q.completeLoad(t.id)}},onScriptError:function(e){var t=g(e);if(!n(t.id)){var i=[];return eachProp(S,function(e,r){0!==r.indexOf("_@r")&&each(e.depMaps,function(e){if(e.id===t.id)return i.push(r),!0})}),c(makeError("scripterror",'Script error for "'+t.id+(i.length?'", needed by: '+i.join(", "):'"'),e,[t.id]))}}},q.require=q.makeRequire(),q}function getInteractiveScript(){return interactiveScript&&"interactive"===interactiveScript.readyState?interactiveScript:(eachReverse(scripts(),function(e){if("interactive"===e.readyState)return interactiveScript=e}),interactiveScript)}var req,s,head,baseElement,dataMain,src,interactiveScript,currentlyAddingScript,mainScript,subPath,version="2.3.5",commentRegExp=/\/\*[\s\S]*?\*\/|([^:"'=]|^)\/\/.*$/gm,cjsRequireRegExp=/[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,jsSuffixRegExp=/\.js$/,currDirRegExp=/^\.\//,op=Object.prototype,ostring=op.toString,hasOwn=op.hasOwnProperty,isBrowser=!("undefined"==typeof window||"undefined"==typeof navigator||!window.document),isWebWorker=!isBrowser&&"undefined"!=typeof importScripts,readyRegExp=isBrowser&&"PLAYSTATION 3"===navigator.platform?/^complete$/:/^(complete|loaded)$/,defContextName="_",isOpera="undefined"!=typeof opera&&"[object Opera]"===opera.toString(),contexts={},cfg={},globalDefQueue=[],useInteractive=!1;if(void 0===define){if(void 0!==requirejs){if(isFunction(requirejs))return;cfg=requirejs,requirejs=void 0}void 0===require||isFunction(require)||(cfg=require,require=void 0),req=requirejs=function(e,t,i,r){var n,o,a=defContextName;return isArray(e)||"string"==typeof e||(o=e,isArray(t)?(e=t,t=i,i=r):e=[]),o&&o.context&&(a=o.context),(n=getOwn(contexts,a))||(n=contexts[a]=req.s.newContext(a)),o&&n.configure(o),n.require(e,t,i)},req.config=function(e){return req(e)},req.nextTick=void 0!==setTimeout?function(e){setTimeout(e,4)}:function(e){e()},require||(require=req),req.version=version,req.jsExtRegExp=/^\/|:|\?|\.js$/,req.isBrowser=isBrowser,s=req.s={contexts:contexts,newContext:newContext},req({}),each(["toUrl","undef","defined","specified"],function(e){req[e]=function(){var t=contexts[defContextName];return t.require[e].apply(t,arguments)}}),isBrowser&&(head=s.head=document.getElementsByTagName("head")[0],(baseElement=document.getElementsByTagName("base")[0])&&(head=s.head=baseElement.parentNode)),req.onError=defaultOnError,req.createNode=function(e,t,i){var r=e.xhtml?document.createElementNS("http://www.w3.org/1999/xhtml","html:script"):document.createElement("script");return r.type=e.scriptType||"text/javascript",r.charset="utf-8",r.async=!0,r},req.load=function(e,t,i){var r,n=e&&e.config||{};if(isBrowser)return(r=req.createNode(n,t,i)).setAttribute("data-requirecontext",e.contextName),r.setAttribute("data-requiremodule",t),!r.attachEvent||r.attachEvent.toString&&r.attachEvent.toString().indexOf("[native code")<0||isOpera?(r.addEventListener("load",e.onScriptLoad,!1),r.addEventListener("error",e.onScriptError,!1)):(useInteractive=!0,r.attachEvent("onreadystatechange",e.onScriptLoad)),r.src=i,n.onNodeCreated&&n.onNodeCreated(r,n,t,i),currentlyAddingScript=r,baseElement?head.insertBefore(r,baseElement):head.appendChild(r),currentlyAddingScript=null,r;if(isWebWorker)try{setTimeout(function(){},0),importScripts(i),e.completeLoad(t)}catch(r){e.onError(makeError("importscripts","importScripts failed for "+t+" at "+i,r,[t]))}},isBrowser&&!cfg.skipDataMain&&eachReverse(scripts(),function(e){if(head||(head=e.parentNode),dataMain=e.getAttribute("data-main"))return mainScript=dataMain,cfg.baseUrl||-1!==mainScript.indexOf("!")||(src=mainScript.split("/"),mainScript=src.pop(),subPath=src.length?src.join("/")+"/":"./",cfg.baseUrl=subPath),mainScript=mainScript.replace(jsSuffixRegExp,""),req.jsExtRegExp.test(mainScript)&&(mainScript=dataMain),cfg.deps=cfg.deps?cfg.deps.concat(mainScript):[mainScript],!0}),define=function(e,t,i){var r,n;"string"!=typeof e&&(i=t,t=e,e=null),isArray(t)||(i=t,t=null),!t&&isFunction(i)&&(t=[],i.length&&(i.toString().replace(commentRegExp,commentReplace).replace(cjsRequireRegExp,function(e,i){t.push(i)}),t=(1===i.length?["require"]:["require","exports","module"]).concat(t))),useInteractive&&(r=currentlyAddingScript||getInteractiveScript())&&(e||(e=r.getAttribute("data-requiremodule")),n=contexts[r.getAttribute("data-requirecontext")]),n?(n.defQueue.push([e,t,i]),n.defQueueMap[e]=!0):globalDefQueue.push([e,t,i])},define.amd={jQuery:!0},req.exec=function(text){return eval(text)},req(cfg)}}(this,"undefined"==typeof setTimeout?void 0:setTimeout); \ No newline at end of file diff --git a/data/scripts/3rd/pubsub.js b/data/scripts/3rd/pubsub.js index 0126059..c64ead4 100644 --- a/data/scripts/3rd/pubsub.js +++ b/data/scripts/3rd/pubsub.js @@ -1,219 +1,214 @@ -define(function(require, exports, module) { +'use strict'; - // Modified version of https://github.com/mroderick/PubSubJS to return different PubSub objects +// Modified version of https://github.com/mroderick/PubSubJS to return different PubSub objects - 'use strict'; +export function PubSub() { - function PubSub() { + var messages = {}, + lastUid = -1; - var messages = {}, - lastUid = -1; + function hasKeys(obj) { + var key; - function hasKeys(obj){ - var key; - - for (key in obj){ - if ( obj.hasOwnProperty(key) ){ - return true; - } + for (key in obj) { + if (obj.hasOwnProperty(key)) { + return true; } - return false; - } - - /** - * Returns a function that throws the passed exception, for use as argument for setTimeout - * @param { Object } ex An Error object - */ - function throwException( ex ){ - return function reThrowException(){ - throw ex; - }; } + return false; + } - function callSubscriberWithDelayedExceptions( subscriber, message, data ){ - try { - // subscriber( message, data ); - subscriber( data ); - } catch( ex ){ - setTimeout( throwException( ex ), 0); - } - } + /** + * Returns a function that throws the passed exception, for use as argument for setTimeout + * @param { Object } ex An Error object + */ + function throwException(ex) { + return function reThrowException() { + throw ex; + }; + } - function callSubscriberWithImmediateExceptions( subscriber, message, data ){ + function callSubscriberWithDelayedExceptions(subscriber, message, data) { + try { // subscriber( message, data ); - subscriber( data ); - } - - function deliverMessage( originalMessage, matchedMessage, data, immediateExceptions ){ - var subscribers = messages[matchedMessage], - callSubscriber = immediateExceptions ? callSubscriberWithImmediateExceptions : callSubscriberWithDelayedExceptions, - s; - - if ( !messages.hasOwnProperty( matchedMessage ) ) { - return; - } - - for (s in subscribers){ - if ( subscribers.hasOwnProperty(s)){ - callSubscriber( subscribers[s], originalMessage, data ); - } - } + subscriber(data); + } catch (ex) { + setTimeout(throwException(ex), 0); } + } - function createDeliveryFunction( message, data, immediateExceptions ){ - return function deliverNamespaced(){ - var topic = String( message ), - position = topic.lastIndexOf( '.' ); + function callSubscriberWithImmediateExceptions(subscriber, message, data) { + // subscriber( message, data ); + subscriber(data); + } - // deliver the message as it is now - deliverMessage(message, message, data, immediateExceptions); + function deliverMessage(originalMessage, matchedMessage, data, immediateExceptions) { + var subscribers = messages[matchedMessage], + callSubscriber = immediateExceptions ? callSubscriberWithImmediateExceptions : callSubscriberWithDelayedExceptions, + s; - // trim the hierarchy and deliver message to each level - while( position !== -1 ){ - topic = topic.substr( 0, position ); - position = topic.lastIndexOf('.'); - deliverMessage( message, topic, data ); - } - }; + if (!messages.hasOwnProperty(matchedMessage)) { + return; } - function messageHasSubscribers( message ){ - var topic = String( message ), - found = Boolean(messages.hasOwnProperty( topic ) && hasKeys(messages[topic])), - position = topic.lastIndexOf( '.' ); - - while ( !found && position !== -1 ){ - topic = topic.substr( 0, position ); - position = topic.lastIndexOf( '.' ); - found = Boolean(messages.hasOwnProperty( topic ) && hasKeys(messages[topic])); + for (s in subscribers) { + if (subscribers.hasOwnProperty(s)) { + callSubscriber(subscribers[s], originalMessage, data); } - - return found; } + } - function publish( message, data, sync, immediateExceptions ){ - var deliver = createDeliveryFunction( message, data, immediateExceptions ), - hasSubscribers = messageHasSubscribers( message ); + function createDeliveryFunction(message, data, immediateExceptions) { + return function deliverNamespaced() { + var topic = String(message), + position = topic.lastIndexOf('.'); - if ( !hasSubscribers ){ - return false; - } + // deliver the message as it is now + deliverMessage(message, message, data, immediateExceptions); - if ( sync === true ){ - deliver(); - } else { - setTimeout( deliver, 0 ); + // trim the hierarchy and deliver message to each level + while (position !== -1) { + topic = topic.substr(0, position); + position = topic.lastIndexOf('.'); + deliverMessage(message, topic, data); } - return true; - } - - /** - * PubSub.publish( message[, data] ) -> Boolean - * - message (String): The message to publish - * - data: The data to pass to subscribers - * Publishes the the message, passing the data to it's subscribers - **/ - this.publish = function( message, data ){ - return publish( message, data, false, this.immediateExceptions ); }; + } - /** - * PubSub.publishSync( message[, data] ) -> Boolean - * - message (String): The message to publish - * - data: The data to pass to subscribers - * Publishes the the message synchronously, passing the data to it's subscribers - **/ - this.publishSync = function( message, data ){ - return publish( message, data, true, this.immediateExceptions ); - }; + function messageHasSubscribers(message) { + var topic = String(message), + found = Boolean(messages.hasOwnProperty(topic) && hasKeys(messages[topic])), + position = topic.lastIndexOf('.'); - /** - * PubSub.subscribe( message, func ) -> String - * - message (String): The message to subscribe to - * - func (Function): The function to call when a new message is published - * Subscribes the passed function to the passed message. Every returned token is unique and should be stored if - * you need to unsubscribe - **/ - this.subscribe = function( message, func ){ - if ( typeof func !== 'function'){ - return false; - } + while (!found && position !== -1) { + topic = topic.substr(0, position); + position = topic.lastIndexOf('.'); + found = Boolean(messages.hasOwnProperty(topic) && hasKeys(messages[topic])); + } - // message is not registered yet - if ( !messages.hasOwnProperty( message ) ){ - messages[message] = {}; - } + return found; + } - // forcing token as String, to allow for future expansions without breaking usage - // and allow for easy use as key names for the 'messages' object - var token = 'uid_' + String(++lastUid); - messages[message][token] = func; + function publish(message, data, sync, immediateExceptions) { + var deliver = createDeliveryFunction(message, data, immediateExceptions), + hasSubscribers = messageHasSubscribers(message); - // return token for unsubscribing - return token; - }; + if (!hasSubscribers) { + return false; + } - /* Public: Clears all subscriptions - */ - this.clearAllSubscriptions = function clearSubscriptions(){ - messages = {}; - }; + if (sync === true) { + deliver(); + } else { + setTimeout(deliver, 0); + } + return true; + } - /* Public: removes subscriptions. - * When passed a token, removes a specific subscription. - * When passed a function, removes all subscriptions for that function - * When passed a topic, removes all subscriptions for that topic (hierarchy) - * - * value - A token, function or topic to unsubscribe. - * - * Examples - * - * // Example 1 - unsubscribing with a token - * var token = PubSub.subscribe('mytopic', myFunc); - * PubSub.unsubscribe(token); - * - * // Example 2 - unsubscribing with a function - * PubSub.unsubscribe(myFunc); - * - * // Example 3 - unsubscribing a topic - * PubSub.unsubscribe('mytopic'); - */ - this.unsubscribe = function(value){ - var isTopic = typeof value === 'string' && messages.hasOwnProperty(value), - isToken = !isTopic && typeof value === 'string', - isFunction = typeof value === 'function', - result = false, - m, message, t; - - if (isTopic){ - delete messages[value]; - return; - } + /** + * PubSub.publish( message[, data] ) -> Boolean + * - message (String): The message to publish + * - data: The data to pass to subscribers + * Publishes the the message, passing the data to it's subscribers + **/ + this.publish = function (message, data) { + return publish(message, data, false, this.immediateExceptions); + }; + + /** + * PubSub.publishSync( message[, data] ) -> Boolean + * - message (String): The message to publish + * - data: The data to pass to subscribers + * Publishes the the message synchronously, passing the data to it's subscribers + **/ + this.publishSync = function (message, data) { + return publish(message, data, true, this.immediateExceptions); + }; + + /** + * PubSub.subscribe( message, func ) -> String + * - message (String): The message to subscribe to + * - func (Function): The function to call when a new message is published + * Subscribes the passed function to the passed message. Every returned token is unique and should be stored if + * you need to unsubscribe + **/ + this.subscribe = function (message, func) { + if (typeof func !== 'function') { + return false; + } + + // message is not registered yet + if (!messages.hasOwnProperty(message)) { + messages[message] = {}; + } - for ( m in messages ){ - if ( messages.hasOwnProperty( m ) ){ - message = messages[m]; - - if ( isToken && message[value] ){ - delete message[value]; - result = value; - // tokens are unique, so we can just stop here - break; - } else if (isFunction) { - for ( t in message ){ - if (message.hasOwnProperty(t) && message[t] === value){ - delete message[t]; - result = true; - } + // forcing token as String, to allow for future expansions without breaking usage + // and allow for easy use as key names for the 'messages' object + var token = 'uid_' + String(++lastUid); + messages[message][token] = func; + + // return token for unsubscribing + return token; + }; + + /* Public: Clears all subscriptions + */ + this.clearAllSubscriptions = function clearSubscriptions() { + messages = {}; + }; + + /* Public: removes subscriptions. + * When passed a token, removes a specific subscription. + * When passed a function, removes all subscriptions for that function + * When passed a topic, removes all subscriptions for that topic (hierarchy) + * + * value - A token, function or topic to unsubscribe. + * + * Examples + * + * // Example 1 - unsubscribing with a token + * var token = PubSub.subscribe('mytopic', myFunc); + * PubSub.unsubscribe(token); + * + * // Example 2 - unsubscribing with a function + * PubSub.unsubscribe(myFunc); + * + * // Example 3 - unsubscribing a topic + * PubSub.unsubscribe('mytopic'); + */ + this.unsubscribe = function (value) { + var isTopic = typeof value === 'string' && messages.hasOwnProperty(value), + isToken = !isTopic && typeof value === 'string', + isFunction = typeof value === 'function', + result = false, + m, message, t; + + if (isTopic) { + delete messages[value]; + return; + } + + for (m in messages) { + if (messages.hasOwnProperty(m)) { + message = messages[m]; + + if (isToken && message[value]) { + delete message[value]; + result = value; + // tokens are unique, so we can just stop here + break; + } else if (isFunction) { + for (t in message) { + if (message.hasOwnProperty(t) && message[t] === value) { + delete message[t]; + result = true; } } } } + } - return result; - }; - this.messages = messages; - } - - exports.PubSub = PubSub; -}); \ No newline at end of file + return result; + }; + this.messages = messages; +} diff --git a/data/scripts/app.js b/data/scripts/app.js index 8472aaa..e9147ce 100644 --- a/data/scripts/app.js +++ b/data/scripts/app.js @@ -1,79 +1,64 @@ -define(function(require, exports) { - 'use strict'; - - // ------------------------------------------------------------------------ - // Modules - - const { AppConfig } = require('./config'); - const { WindowEvents, GlobalEvents } = require('./utils/global-events'); - - const { SessionSyncUI } = require('./session-sync-ui'); - const { SessionSyncModel } = require ('./components/session-sync-model'); - - const { BookmarkManager } = require('./session/bookmarks'); - - const sessionRoot = 'SessionSync'; - - // ------------------------------------------------------------------------ - // API - - var findSessionSync = function findSessionSync(callback) - { - BookmarkManager.searchBookmarks({title: sessionRoot}) - .then( - function success(bookmarks) { - if (bookmarks.length == 0) - { +'use strict'; + +// ------------------------------------------------------------------------ +// Modules + +import { AppConfig } from './config'; +import { WindowEvents, GlobalEvents } from './utils/global-events'; +import { SessionSyncUI } from './session-sync-ui'; +import { SessionSyncModel } from './components/session-sync-model'; +import { BookmarkManager } from './session/bookmarks'; + +const sessionRoot = 'SessionSync'; + +// ------------------------------------------------------------------------ +// API + +const findSessionSync = function findSessionSync(callback) +{ + BookmarkManager.searchBookmarks({title: sessionRoot}) + .then( + function success(bookmarks) { + if (bookmarks.length == 0) + { + BookmarkManager.createBookmark({ + title: sessionRoot, + parentId: 'menu________' + }).then(callback, function error() { BookmarkManager.createBookmark({ title: sessionRoot, - parentId: 'menu________' - }).then(callback, function error() { - BookmarkManager.createBookmark({ - title: sessionRoot, - }).then(callback); - }); - } - else - { - callback(bookmarks[0]); - } - }, - function fail() { + }).then(callback); + }); } - ); - }; - - var listSessions = function listSessions() - { - BookmarkManager.getFolderBookmarks(AppConfig.get('storageID'), function(sessions) { - SessionSyncModel.sessions = sessions; - WindowEvents.emit(document, 'ListSessions'); - }); - }; - - var init = function init() - { - SessionSyncUI(); - - findSessionSync(function(bookmark) { - AppConfig.set('storageID', bookmark.id, function() { - listSessions(true); - }); + else + { + callback(bookmarks[0]); + } + }, + function fail() { + } + ); +}; + +const listSessions = function listSessions() +{ + BookmarkManager.getFolderBookmarks(AppConfig.get('storageID'), function(sessions) { + SessionSyncModel.sessions = sessions; + WindowEvents.emit(document, 'ListSessions'); + }); +}; + +export const init = function init() +{ + SessionSyncUI(); + + findSessionSync(function(bookmark) { + AppConfig.set('storageID', bookmark.id, function() { + listSessions(true); }); + }); - AppConfig.init(); - - GlobalEvents.on('update-sessions', listSessions.bind(null, false, false)); - }; - - // ------------------------------------------------------------------------ - // Events - - // ------------------------------------------------------------------------ - // Init - - // ------------------------------------------------------------------------ - // Module exports + AppConfig.init(); - exports.init = init; -}); + GlobalEvents.on('update-sessions', listSessions.bind(null, false, false)); +}; diff --git a/data/scripts/components/clipboard.js b/data/scripts/components/clipboard.js index da39d9b..450f108 100644 --- a/data/scripts/components/clipboard.js +++ b/data/scripts/components/clipboard.js @@ -1,42 +1,33 @@ -define(function(require, exports) { - 'use strict'; +'use strict'; - // ------------------------------------------------------------------------ - // Modules - - const { HTMLCreator } = require('../utils/dom'); - const { WindowEvents } = require('../utils/global-events'); - - // ------------------------------------------------------------------------ - // API +// ------------------------------------------------------------------------ +// Modules - function Clipboard() - { - var DomElem = HTMLCreator(); +import { HTMLCreator } from '../utils/dom'; +import { WindowEvents } from '../utils/global-events'; - var input = DomElem('input', {id: 'clipboard'}); - document.body.appendChild(input); +// ------------------------------------------------------------------------ +// API - // ------------------------------------------------------------------------ - // Events +export function Clipboard() { + var DomElem = HTMLCreator(); - function copyText(text) - { - input.value = text; - input.select(); - document.execCommand('copy'); - } + var input = DomElem('input', { id: 'clipboard' }); + document.body.appendChild(input); - WindowEvents.on(document, 'CopyToClipboard', copyText); - - // ------------------------------------------------------------------------ - // Public data + // ------------------------------------------------------------------------ + // Events - this.DOMRoot = input; + function copyText(text) { + input.value = text; + input.select(); + document.execCommand('copy'); } + WindowEvents.on(document, 'CopyToClipboard', copyText); + // ------------------------------------------------------------------------ - // Module exports + // Public data - exports.Clipboard = Clipboard; -}); + this.DOMRoot = input; +} diff --git a/data/scripts/components/config-panel.js b/data/scripts/components/config-panel.js index 772af30..51cf836 100644 --- a/data/scripts/components/config-panel.js +++ b/data/scripts/components/config-panel.js @@ -1,366 +1,369 @@ -define(function(require, exports) { - 'use strict'; - - // ***************************************************************************** - // Custom Modules - - // App - const { AppConfig } = require('../config'); - const { SessionHistory } = require('./session-history'); - - // Utils - const { WindowEvents, GlobalEvents } = require('../utils/global-events'); - const { HTMLCreator } = require('../utils/dom'); - const DOMComponent = require('../utils/components'); - - // ***************************************************************************** - // Custom Modules - - /* - * Config panel - */ - - function ConfigPanel() - { - // Create DomHealper - var DomElem = HTMLCreator(); - - // ------------------------------------------------------------------------ - // Create UI - - var panel = DomElem('div', {class: 'config-panel'}); - - // Close button - var closeBtn = DomElem('div', {class: 'css-close-button'}); - panel.appendChild(closeBtn); - // ------------------------------------------------------------------------ - // Methods - - function RangeOptionConfig(options) - { - var optionRG = new DOMComponent.RangeControl({ - value: AppConfig.get(options.key), - description: options.name, - step: options.step ? options.step : 1, - minValue: options.minValue ? options.minValue : 1, - maxValue: options.maxValue ? options.maxValue : 1, - onChange: function(value) { - AppConfig.set(options.key, value); - if (options.callback) { - options.callback(value); - } - }, - }); - options.parent.addItem(optionRG.DOMRoot); - AppConfig.onChange(options.key, function(value) { - optionRG.setValue(value, false); - }); +'use strict'; + +// ***************************************************************************** +// Custom Modules + +// App +import { AppConfig } from '../config'; +import { SessionHistory } from './session-history'; + +// Utils +import { WindowEvents, GlobalEvents } from '../utils/global-events'; +import { HTMLCreator } from '../utils/dom'; +import * as DOMComponent from '../utils/components'; + +// ***************************************************************************** +// Custom Modules + +/* +* Config panel +*/ + +export function ConfigPanel() { + // Create DomHealper + var DomElem = HTMLCreator(); + + // ------------------------------------------------------------------------ + // Create UI + + var panel = DomElem('div', { class: 'config-panel' }); + + // Close button + var closeBtn = DomElem('div', { class: 'css-close-button' }); + panel.appendChild(closeBtn); + // ------------------------------------------------------------------------ + // Methods + + function RangeOptionConfig(options) { + var optionRG = new DOMComponent.RangeControl({ + value: AppConfig.get(options.key), + description: options.name, + step: options.step ? options.step : 1, + minValue: options.minValue ? options.minValue : 1, + maxValue: options.maxValue ? options.maxValue : 1, + onChange: function (value) { + AppConfig.set(options.key, value); + if (options.callback) { + options.callback(value); + } + }, + }); + options.parent.addItem(optionRG.DOMRoot); + AppConfig.onChange(options.key, function (value) { + optionRG.setValue(value, false); + }); + } + + function ToggleOptionConfig(options) { + var isSwitch = (options.type === 'switch'); + var Toggle = DOMComponent.ToggleButton; + if (isSwitch) { + Toggle = DOMComponent.ToggleSwitch; + options.onState = 'Enabled'; + options.offState = 'Disabled'; } - function ToggleOptionConfig(options) - { - var isSwitch = (options.type === 'switch'); - var Toggle = DOMComponent.ToggleButton; + var toggleBtn = new Toggle({ + state: AppConfig.get(options.key), + description: options.name, + onState: options.onState, + offState: options.offState, + callback: function (value) { + AppConfig.set(options.key, value); + if (options.callback) { + options.callback(value); + } + }, + }); + + options.parent.addItem(toggleBtn.DOMRoot); + AppConfig.onChange(options.key, function (value) { if (isSwitch) { - Toggle = DOMComponent.ToggleSwitch; - options.onState = 'Enabled'; - options.offState = 'Disabled'; + toggleBtn.setState(value); } + else { + toggleBtn.setValue(value, false); + } + }); + } - var toggleBtn = new Toggle({ - state: AppConfig.get(options.key), - description: options.name, - onState: options.onState, - offState: options.offState, - callback: function(value) { - AppConfig.set(options.key, value); - if (options.callback) { - options.callback(value); - } - }, - }); + function ConfigSection(options) { + var sectionContainer = DomElem('div', { class: 'section-container' }); + var sectionTitle = DomElem('div', { class: 'section-title' }); + var title = DomElem('div', { class: 'title' }); + var dropIcon = DomElem('div', { class: 'drop-icon' }); + var sectionBody = DomElem('div', { class: 'section-body' }); - options.parent.addItem(toggleBtn.DOMRoot); - AppConfig.onChange(options.key, function(value) { - if (isSwitch) - { - toggleBtn.setState(value); - } - else - { - toggleBtn.setValue(value, false); - } - }); + if (options.id) { + sectionContainer.setAttribute('id', options.id); } - function ConfigSection(options) - { - var sectionContainer = DomElem('div', {class: 'section-container'}); - var sectionTitle = DomElem('div', {class: 'section-title'}); - var title = DomElem('div', {class: 'title'}); - var dropIcon = DomElem('div', {class: 'drop-icon'}); - var sectionBody = DomElem('div', {class: 'section-body'}); + title.textContent = options.title; + sectionTitle.appendChild(dropIcon); + sectionTitle.appendChild(title); + + sectionContainer.appendChild(sectionTitle); + sectionContainer.appendChild(sectionBody); + + sectionTitle.addEventListener('click', function () { - if (options.id) { - sectionContainer.setAttribute('id', options.id); + // manual mode + var isCollapsed = sectionContainer.hasAttribute('collapsed'); + if (isCollapsed) { + sectionContainer.removeAttribute('collapsed'); + } else { + sectionBody.style.height = sectionBody.clientHeight + 'px'; + sectionContainer.setAttribute('collapsed', ''); } + }); - title.textContent = options.title; - sectionTitle.appendChild(dropIcon); - sectionTitle.appendChild(title); + this.container = sectionContainer; + this.addItem = function (item) { + sectionBody.appendChild(item); + }; + } - sectionContainer.appendChild(sectionTitle); - sectionContainer.appendChild(sectionBody); + // ------------------------ + // Section: UI scaling - sectionTitle.addEventListener('click', function() { + (function () { - // manual mode - var isCollapsed = sectionContainer.hasAttribute('collapsed'); - if (isCollapsed) { - sectionContainer.removeAttribute('collapsed'); - } else { - sectionBody.style.height = sectionBody.clientHeight + 'px'; - sectionContainer.setAttribute('collapsed', ''); - } - }); + var section = new ConfigSection({ title: 'UI Scaling' }); + panel.appendChild(section.container); - this.container = sectionContainer; - this.addItem = function(item) { - sectionBody.appendChild(item); - }; + if (AppConfig.isPanel()) { + RangeOptionConfig({ parent: section, name: 'Panel width (px)', key: 'style.panel.width', minValue: 500, maxValue: 800, step: 5 }); + RangeOptionConfig({ parent: section, name: 'Panel height (px)', key: 'style.panel.height', minValue: 400, maxValue: 600, step: 5 }); } + RangeOptionConfig({ parent: section, name: 'Header bar', key: 'style.scale.header', minValue: 10, maxValue: 50 }); + RangeOptionConfig({ parent: section, name: 'Toolbar', key: 'style.scale.toolbar', minValue: 10, maxValue: 50 }); + RangeOptionConfig({ parent: section, name: 'Session list', key: 'style.scale.sessions', minValue: 10, maxValue: 50 }); + RangeOptionConfig({ parent: section, name: 'Bookmarks list', key: 'style.scale.bookmarks', minValue: 10, maxValue: 50 }); - // ------------------------ - // Section: UI scaling + })(); - (function() { + // ------------------------ + // Section: General settings - var section = new ConfigSection({ title: 'UI Scaling' }); - panel.appendChild(section.container); + (function () { - if (AppConfig.isPanel()) - { - RangeOptionConfig({ parent: section, name: 'Panel width (px)', key: 'style.panel.width', minValue: 500, maxValue: 800, step: 5}); - RangeOptionConfig({ parent: section, name: 'Panel height (px)', key: 'style.panel.height', minValue: 400, maxValue: 600, step: 5}); - } - RangeOptionConfig({ parent: section, name: 'Header bar', key: 'style.scale.header', minValue: 10, maxValue: 50}); - RangeOptionConfig({ parent: section, name: 'Toolbar', key: 'style.scale.toolbar', minValue: 10, maxValue: 50}); - RangeOptionConfig({ parent: section, name: 'Session list', key: 'style.scale.sessions', minValue: 10, maxValue: 50}); - RangeOptionConfig({ parent: section, name: 'Bookmarks list', key: 'style.scale.bookmarks', minValue: 10, maxValue: 50}); + var section = new ConfigSection({ title: 'Theme Options' }); + panel.appendChild(section.container); - })(); + ToggleOptionConfig({ + parent: section, + name: 'Grayscale Icons', + key: 'theme.toolbar.grayscale', + type: 'switch', + }); - // ------------------------ - // Section: General settings + })(); - (function() { + // ------------------------ + // Section: General settings - var section = new ConfigSection({ title: 'Session Management' }); - panel.appendChild(section.container); + (function () { - ToggleOptionConfig({ - parent: section, - name: 'Lazy loading restore', - key: 'restore.lazy.loading', - type: 'switch', - }); + var section = new ConfigSection({ title: 'Session Management' }); + panel.appendChild(section.container); - ToggleOptionConfig({ - parent: section, - name: 'Reverse restore order', - key: 'restore.reverse.order', - type: 'switch', - }); + ToggleOptionConfig({ + parent: section, + name: 'Lazy loading restore', + key: 'restore.lazy.loading', + type: 'switch', + }); - })(); + ToggleOptionConfig({ + parent: section, + name: 'Reverse restore order', + key: 'restore.reverse.order', + type: 'switch', + }); - // ------------------------ - // Section: General settings + })(); - (function() { + // ------------------------ + // Section: General settings - var section = new ConfigSection({ title: 'General' }); - panel.appendChild(section.container); + (function () { - ToggleOptionConfig({ - parent: section, - name: 'Left click', - key: 'bookmark.click.newTab', - onState: 'New tab', - offState: 'Same tab', - }); + var section = new ConfigSection({ title: 'General' }); + panel.appendChild(section.container); - ToggleOptionConfig({ - parent: section, - name: 'Middle click', - key: 'bookmark.middleClick.newTab', - onState: 'New tab', - offState: 'Same tab', - }); + ToggleOptionConfig({ + parent: section, + name: 'Left click', + key: 'bookmark.click.newTab', + onState: 'New tab', + offState: 'Same tab', + }); - ToggleOptionConfig({ - parent: section, - type: 'switch', - name: 'Context menu icons', - key: 'context.menu.icons', - onState: 'Show', - offState: 'Disabled', - }); + ToggleOptionConfig({ + parent: section, + name: 'Middle click', + key: 'bookmark.middleClick.newTab', + onState: 'New tab', + offState: 'Same tab', + }); - ToggleOptionConfig({ - parent: section, - name: 'Favicon service', - key: 'services.favicon.enabled', - type: 'switch', - }); + ToggleOptionConfig({ + parent: section, + type: 'switch', + name: 'Context menu icons', + key: 'context.menu.icons', + onState: 'Show', + offState: 'Disabled', + }); + + ToggleOptionConfig({ + parent: section, + name: 'Favicon service', + key: 'services.favicon.enabled', + type: 'switch', + }); - ToggleOptionConfig({ - parent: section, - name: 'Troubleshooting', - key: 'troubleshooting', - type: 'switch', + ToggleOptionConfig({ + parent: section, + name: 'Troubleshooting', + key: 'troubleshooting', + type: 'switch', + }); + + })(); + + // ------------------------ + // Section: Hotkeys + + (function () { + + if (typeof browser === 'object' && browser.commands && browser.commands.update == undefined) + return; + + var section = new ConfigSection({ title: 'Hotkeys' }); + panel.appendChild(section.container); + + function HotkeyOptionConfig(command) { + var button = new DOMComponent.ActionButton({ + value: command.shortcut, + description: command.description, + callback: function () { + WindowEvents.emit(document, 'ChangeHotkey', { + title: command.description, + shortcut: button.getValue(), + callback: function (value) { + button.setValue(value); + command.shortcut = value; + GlobalEvents.emit('hotkey.update', command); + } + }); + }, }); - })(); - - // ------------------------ - // Section: Hotkeys - - (function() { - - if (typeof browser === 'object' && browser.commands.update == undefined) - return; - - var section = new ConfigSection({ title: 'Hotkeys' }); - panel.appendChild(section.container); - - function HotkeyOptionConfig(command) - { - var button = new DOMComponent.ActionButton({ - value: command.shortcut, - description: command.description, - callback: function() { - WindowEvents.emit(document, 'ChangeHotkey', { - title: command.description, - shortcut: button.getValue(), - callback: function (value) { - button.setValue(value); - command.shortcut = value; - GlobalEvents.emit('hotkey.update', command); - } - }); - }, - }); - - return button.DOMRoot; - } + return button.DOMRoot; + } - AppConfig.onChange('config.hotkey.init', function(commands) { - commands.forEach(function (command) { - var action = HotkeyOptionConfig(command); - section.addItem(action); - }); + AppConfig.onChange('config.hotkey.init', function (commands) { + commands.forEach(function (command) { + var action = HotkeyOptionConfig(command); + section.addItem(action); }); - })(); + }); + })(); - // ------------------------ - // Section: History settings + // ------------------------ + // Section: History settings - (function() { + (function () { - var section = new ConfigSection({ title: 'Auto-save History', id: 'history-config' }); - panel.appendChild(section.container); + var section = new ConfigSection({ title: 'Auto-save History', id: 'history-config' }); + panel.appendChild(section.container); - SessionHistory.getConfig(function (historyConfig) { + SessionHistory.getConfig(function (historyConfig) { + if (!historyConfig) return; - // Context menu icons - var contextMenuIconsTB = new DOMComponent.ToggleSwitch({ - state: historyConfig.enabled, - description: 'Auto-save sessions', - onState: 'Enabled', - offState: 'Disabled', - callback: function(value) { - historyConfig.enabled = value; - SessionHistory.updateConfig(historyConfig); - if (value == false) { - WindowEvents.emit(document, 'ShowSyncList', { update: true }); - } - WindowEvents.emit(document, 'SetUIState', {'history': value}); - } - }); - section.addItem(contextMenuIconsTB.DOMRoot); - - // Context menu icons - var autoSavePrivateTB = new DOMComponent.ToggleSwitch({ - state: historyConfig.savePrivate, - description: 'Save private windows', - onState: 'Enabled', - offState: 'Disabled', - callback: function(value) { - historyConfig.savePrivate = value; - SessionHistory.updateConfig(historyConfig); + // Context menu icons + var contextMenuIconsTB = new DOMComponent.ToggleSwitch({ + state: historyConfig.enabled, + description: 'Auto-save sessions', + onState: 'Enabled', + offState: 'Disabled', + callback: function (value) { + historyConfig.enabled = value; + SessionHistory.updateConfig(historyConfig); + if (value == false) { + WindowEvents.emit(document, 'ShowSyncList', { update: true }); } - }); - section.addItem(autoSavePrivateTB.DOMRoot); - - // Auto-save interval - var saveIntervalRG = new DOMComponent.RangeControl({ - value: historyConfig.interval, - description: 'Save interval (seconds)', - minValue: 10, - onChange: function(value) { - historyConfig.interval = value; - SessionHistory.updateConfig(historyConfig); - }, - }); - section.addItem(saveIntervalRG.DOMRoot); - - // Auto-save slots - var saveSlotsRG = new DOMComponent.RangeControl({ - value: historyConfig.savingSlots, - description: 'Saving entries (max)', - minValue: 2, - onChange: function(value) { - historyConfig.savingSlots = value; - SessionHistory.updateConfig(historyConfig); - }, - }); - section.addItem(saveSlotsRG.DOMRoot); - - // Auto-save expire time - var expireAfterRG = new DOMComponent.RangeControl({ - value: historyConfig.expireTimeHours, - description: 'Delete older than [...] hours', - minValue: 1, - onChange: function(value) { - historyConfig.expireTimeHours = value; - SessionHistory.updateConfig(historyConfig); - }, - }); - section.addItem(expireAfterRG.DOMRoot); - - // Events - WindowEvents.emit(document, 'SetUIState', {'history': historyConfig.enabled}); + WindowEvents.emit(document, 'SetUIState', { 'history': value }); + } + }); + section.addItem(contextMenuIconsTB.DOMRoot); + // Context menu icons + var autoSavePrivateTB = new DOMComponent.ToggleSwitch({ + state: historyConfig.savePrivate, + description: 'Save private windows', + onState: 'Enabled', + offState: 'Disabled', + callback: function (value) { + historyConfig.savePrivate = value; + SessionHistory.updateConfig(historyConfig); + } + }); + section.addItem(autoSavePrivateTB.DOMRoot); + + // Auto-save interval + var saveIntervalRG = new DOMComponent.RangeControl({ + value: historyConfig.interval, + description: 'Save interval (seconds)', + minValue: 10, + onChange: function (value) { + historyConfig.interval = value; + SessionHistory.updateConfig(historyConfig); + }, + }); + section.addItem(saveIntervalRG.DOMRoot); + + // Auto-save slots + var saveSlotsRG = new DOMComponent.RangeControl({ + value: historyConfig.savingSlots, + description: 'Saving entries (max)', + minValue: 2, + onChange: function (value) { + historyConfig.savingSlots = value; + SessionHistory.updateConfig(historyConfig); + }, + }); + section.addItem(saveSlotsRG.DOMRoot); + + // Auto-save expire time + var expireAfterRG = new DOMComponent.RangeControl({ + value: historyConfig.expireTimeHours, + description: 'Delete older than [...] hours', + minValue: 1, + onChange: function (value) { + historyConfig.expireTimeHours = value; + SessionHistory.updateConfig(historyConfig); + }, }); - })(); + section.addItem(expireAfterRG.DOMRoot); - // ------------------------------------------------------------------------ - // Events + // Events + WindowEvents.emit(document, 'SetUIState', { 'history': historyConfig.enabled }); - closeBtn.addEventListener('click', function() { - WindowEvents.emit(document, 'SetUIState', { config : undefined}); }); + })(); - // ------------------------------------------------------------------------ - // Public properties + // ------------------------------------------------------------------------ + // Events - this.document = document; - this.DOMRoot = panel; - } + closeBtn.addEventListener('click', function () { + WindowEvents.emit(document, 'SetUIState', { config: undefined }); + }); - // ***************************************************************************** - // Public API + // ------------------------------------------------------------------------ + // Public properties - exports.ConfigPanel = ConfigPanel; -}); \ No newline at end of file + this.document = document; + this.DOMRoot = panel; +} \ No newline at end of file diff --git a/data/scripts/components/notification-system.js b/data/scripts/components/notification-system.js index 76e67ab..663bf09 100644 --- a/data/scripts/components/notification-system.js +++ b/data/scripts/components/notification-system.js @@ -1,54 +1,44 @@ -define(function(require, exports) { - 'use strict'; +'use strict'; - // ***************************************************************************** - // Custom Modules +// ***************************************************************************** +// Custom Modules - const { HTMLCreator } = require('../utils/dom'); - const { WindowEvents } = require('../utils/global-events'); +import { HTMLCreator } from '../utils/dom'; +import { WindowEvents } from '../utils/global-events'; - // ***************************************************************************** - // NotificationSystem +// ***************************************************************************** +// NotificationSystem - function NotificationSystem() - { - var DomElem = HTMLCreator(); +export function NotificationSystem() { + var DomElem = HTMLCreator(); - var panel = DomElem('div', {class : 'notification-system'}); - var info = DomElem('div', {class : 'info'}); - var warning = DomElem('div', {class : 'warning'}); + var panel = DomElem('div', { class: 'notification-system' }); + var info = DomElem('div', { class: 'info' }); + var warning = DomElem('div', { class: 'warning' }); - panel.appendChild(info); - panel.appendChild(warning); + panel.appendChild(info); + panel.appendChild(warning); - // ------------------------------------------------------------------------ - // Events + // ------------------------------------------------------------------------ + // Events - var infoTimeout = 0; + var infoTimeout = 0; - var closeInfoBox = function closeInfoBox() - { - info.removeAttribute('active'); - }; + var closeInfoBox = function closeInfoBox() { + info.removeAttribute('active'); + }; - var showInfo = function showInfo(options) - { - info.textContent = options.message; - info.setAttribute('active', ''); - clearTimeout(infoTimeout); - infoTimeout = setTimeout(closeInfoBox, options.timeout ? options.timeout : 1500); - }; + var showInfo = function showInfo(options) { + info.textContent = options.message; + info.setAttribute('active', ''); + clearTimeout(infoTimeout); + infoTimeout = setTimeout(closeInfoBox, options.timeout ? options.timeout : 1500); + }; - WindowEvents.on(document, 'Notification', showInfo); + WindowEvents.on(document, 'Notification', showInfo); - // ------------------------------------------------------------------------ - // Public data + // ------------------------------------------------------------------------ + // Public data - this.DOMRoot = panel; - } - - // ***************************************************************************** - // Public API - - exports.NotificationSystem = NotificationSystem; -}); \ No newline at end of file + this.DOMRoot = panel; +} \ No newline at end of file diff --git a/data/scripts/components/overlay-system.js b/data/scripts/components/overlay-system.js index b38ab6d..e360bb8 100644 --- a/data/scripts/components/overlay-system.js +++ b/data/scripts/components/overlay-system.js @@ -1,66 +1,58 @@ -define(function(require, exports) { - 'use strict'; +'use strict'; - // ***************************************************************************** - // Custom Modules +// ***************************************************************************** +// Custom Modules - const { AppConfig } = require('../config'); - const { HTMLCreator } = require('../utils/dom'); +import { AppConfig } from '../config'; +import { HTMLCreator } from '../utils/dom'; - // ***************************************************************************** - // API +// ***************************************************************************** +// API - /* - * Session Hotkeys - */ +/* +* Session Hotkeys +*/ - function OverlaySystem() - { - // Create DomHealper - var DomElem = HTMLCreator(); +export function OverlaySystem() { + // Create DomHealper + var DomElem = HTMLCreator(); - // ------------------------------------------------------------------------ - // Create UI + // ------------------------------------------------------------------------ + // Create UI - var overlay = DomElem('div', { id : 'overlay' }); + var overlay = DomElem('div', { id: 'overlay' }); - // ------------------------------------------------------------------------ - // Events + // ------------------------------------------------------------------------ + // Events - var state = false; + var state = false; - function setState(options) { - if (AppConfig.isPanel()) { - state = options.state; - overlay.setAttribute('state', state); + function setState(options) { + if (AppConfig.isPanel()) { + state = options.state; + overlay.setAttribute('state', state); - if (options.zIndex) { - overlay.style.zIndex = options.zIndex; - } - - if (state == false) { - overlay.removeAttribute('style'); - } + if (options.zIndex) { + overlay.style.zIndex = options.zIndex; } - } - function autoHide() { - if (state == true) - setState(false); + if (state == false) { + overlay.removeAttribute('style'); + } } + } - // document.addEventListener('click', autoHide); - - // WindowEvents.on(document, 'OverlaySystem', setState); + function autoHide() { + if (state == true) + setState(false); + } - // ------------------------------------------------------------------------ - // Public properties + // document.addEventListener('click', autoHide); - this.DOMRoot = overlay; - } + // WindowEvents.on(document, 'OverlaySystem', setState); - // ***************************************************************************** - // Public API + // ------------------------------------------------------------------------ + // Public properties - exports.OverlaySystem = OverlaySystem; -}); \ No newline at end of file + this.DOMRoot = overlay; +} \ No newline at end of file diff --git a/data/scripts/components/session-bookmark-events.js b/data/scripts/components/session-bookmark-events.js index c37ea56..4a22615 100644 --- a/data/scripts/components/session-bookmark-events.js +++ b/data/scripts/components/session-bookmark-events.js @@ -1,448 +1,448 @@ -define(function(require, exports) { +'use strict'; + +// ***************************************************************************** +// Custom Modules + +import { AppConfig } from '../config'; +import { WindowEvents } from '../utils/global-events'; +import { AutoScroll } from '../utils/auto-scroll'; +import { DragContext } from '../utils/drag-context'; +import { SessionSyncModel } from './session-sync-model'; + +import { SessionManager } from '../session/management'; +import { BookmarkManager } from '../session/bookmarks'; + +// ***************************************************************************** +// API + +// ---------------------------------------------------------------------------- +// Session Events - delegated + +export function SessionBookmarkEvents(container) +{ + var startDragEvent = null; + var trackingTelemetry = new Date(); + var offsetRect = {}; + var bookmarkContext = null; + var bookmarkHoverContextID = 0; + var SyncModel = SessionSyncModel.getModel(typeof document !== 'undefined' ? document : null); + + function getAttribute(target, attribute) { + try { + return target.getAttribute(attribute); + } + catch (err) { + console.log(err); + return null; + } + } + + function getTargetType(e) { + return getAttribute(e.target, 'type'); + } - 'use strict'; + function getSessionID(e) { + return getAttribute(e.target, 'sessionID'); + } - // ***************************************************************************** - // Custom Modules + function getBookmarkID(e) { + return getAttribute(e.target, 'bookmarkID'); + } - const { AppConfig } = require('../config'); - const { WindowEvents } = require('../utils/global-events'); - const { AutoScroll } = require('../utils/auto-scroll'); - const { DragContext } = require('../utils/drag-context'); - const { SessionSyncModel } = require('./session-sync-model'); + function getTabURL(e) { + return getAttribute(e.target, 'url'); + } - const { SessionManager } = require('../session/management'); - const { BookmarkManager } = require('../session/bookmarks'); + function getTabID(e) { + var ID = getAttribute(e.target, 'tabID'); + return ID ? ID | 0 : undefined; + } - // ***************************************************************************** - // API + function getWindowID(e) { + var ID = getAttribute(e.target, 'windowID'); + return ID ? ID | 0 : undefined; + } - // ---------------------------------------------------------------------------- - // Session Events - delegated + // -------------------------------------------------------------- + // Session level - var SessionBookmarkEvents = function SessionBookmarkEvents(container) + var bookmarkDblClick = function sessionDblClick(e) { - var startDragEvent = null; - var trackingTelemetry = new Date(); - var offsetRect = {}; - var bookmarkContext = null; - var bookmarkHoverContextID = 0; - var SyncModel = SessionSyncModel.getModel(document); - - function getAttribute(target, attribute) { - try { - return target.getAttribute(attribute); - } - catch (err) { - console.log(err); - return null; + if (SyncModel.state.session == 'restore') + { + var bookmarkID = getBookmarkID(e); + if (bookmarkID != null && typeof document !== 'undefined') { + WindowEvents.emit(document, 'BookmarkCtxMenu-EditBookmark', bookmarkID); } } + }; - function getTargetType(e) { - return getAttribute(e.target, 'type'); - } - - function getSessionID(e) { - return getAttribute(e.target, 'sessionID'); - } - - function getBookmarkID(e) { - return getAttribute(e.target, 'bookmarkID'); - } - - function getTabURL(e) { - return getAttribute(e.target, 'url'); - } - - function getTabID(e) { - var ID = getAttribute(e.target, 'tabID'); - return ID ? ID | 0 : undefined; - } - - function getWindowID(e) { - var ID = getAttribute(e.target, 'windowID'); - return ID ? ID | 0 : undefined; + var mouseOver = function mouseOver(e) + { + if (!e.target.hasAttribute('over')) { + return; } - // -------------------------------------------------------------- - // Session level + if (typeof document === 'undefined') return; - var bookmarkDblClick = function sessionDblClick(e) + switch(SyncModel.state.session) { - if (SyncModel.state.session == 'restore') - { + // Bookmark entry + case 'restore': { var bookmarkID = getBookmarkID(e); - if (bookmarkID != null) { - WindowEvents.emit(document, 'BookmarkCtxMenu-EditBookmark', bookmarkID); - } - } - }; + if (bookmarkHoverContextID == bookmarkID) + return; - var mouseOver = function mouseOver(e) - { - if (!e.target.hasAttribute('over')) { + bookmarkHoverContextID = bookmarkID; + WindowEvents.emit(document, 'ShowUrlBar', SessionSyncModel.bookmarks[bookmarkID].url); return; } - switch(SyncModel.state.session) - { - // Bookmark entry - case 'restore': { - var bookmarkID = getBookmarkID(e); - if (bookmarkHoverContextID == bookmarkID) - return; - - bookmarkHoverContextID = bookmarkID; - WindowEvents.emit(document, 'ShowUrlBar', SessionSyncModel.bookmarks[bookmarkID].url); - return; - } - - // Window tab - case 'current': { - var tabID = getTabID(e); - if (tabID != undefined) { - let url = getTabURL(e); - WindowEvents.emit(document, 'ShowUrlBar', url); - } - return; - } - - // History tab - case 'history': { + // Window tab + case 'current': { + var tabID = getTabID(e); + if (tabID != undefined) { let url = getTabURL(e); WindowEvents.emit(document, 'ShowUrlBar', url); - return; } + return; } - }; - var mouseLeave = function mouseLeave() - { + // History tab + case 'history': { + let url = getTabURL(e); + WindowEvents.emit(document, 'ShowUrlBar', url); + return; + } + } + }; + + var mouseLeave = function mouseLeave() + { + if (typeof document !== 'undefined') { WindowEvents.emit(document, 'HideUrlBar'); - }; + } + }; - // -------------------------------------------------------------- - // Mouse Events + // -------------------------------------------------------------- + // Mouse Events - var trackDragging = function _mouseMove(e) + var trackDragging = function _mouseMove(e) + { + var delta = Math.abs(startDragEvent.clientX - e.clientX) > 5 || Math.abs(startDragEvent.clientY - e.clientY) > 5; + if (delta) { - var delta = Math.abs(startDragEvent.clientX - e.clientX) > 5 || Math.abs(startDragEvent.clientY - e.clientY) > 5; - if (delta) - { - DragContext.setContext(bookmarkContext); - trackingTelemetry = new Date(); - - offsetRect = container.parentElement.getBoundingClientRect(); - offsetRect.relativeY = offsetRect.y - container.parentElement.offsetTop; - offsetRect.relativeX = offsetRect.x - container.parentElement.offsetLeft; - - bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.relativeY + 'px'; - bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.relativeX + 'px'; - bookmarkContext.DOMRoot.setAttribute('dragging', ''); - - // Events - window.addEventListener('mousemove', mouseDragging); - window.removeEventListener('mousemove', trackDragging); - } - }; + DragContext.setContext(bookmarkContext); + trackingTelemetry = new Date(); - var cancelDragging = function cancelDragging(e) - { - AutoScroll.stop(); + offsetRect = container.parentElement.getBoundingClientRect(); + offsetRect.relativeY = offsetRect.y - container.parentElement.offsetTop; + offsetRect.relativeX = offsetRect.x - container.parentElement.offsetLeft; - bookmarkContext.DOMRoot.setAttribute('dragging', 'drop'); - bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.y + container.scrollTop + 'px'; - bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.x + 'px'; + bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.relativeY + 'px'; + bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.relativeX + 'px'; + bookmarkContext.DOMRoot.setAttribute('dragging', ''); - setTimeout(function() { - bookmarkContext.DOMRoot.removeAttribute('dragging'); - bookmarkContext.restorePosition(); - }, 50); - window.removeEventListener('mousemove', mouseDragging); + // Events + window.addEventListener('mousemove', mouseDragging); + window.removeEventListener('mousemove', trackDragging); + } + }; - DragContext.clearContext(); - }; + var cancelDragging = function cancelDragging(e) + { + AutoScroll.stop(); - var mouseDragging = function mouseMove(e) - { - if (this != window) - return; + bookmarkContext.DOMRoot.setAttribute('dragging', 'drop'); + bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.y + container.scrollTop + 'px'; + bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.x + 'px'; - var deltaTime = new Date() - trackingTelemetry; - if (deltaTime < 16) { - return; - } + setTimeout(function() { + bookmarkContext.DOMRoot.removeAttribute('dragging'); + bookmarkContext.restorePosition(); + }, 50); + window.removeEventListener('mousemove', mouseDragging); - var tryScroll = e.clientX > offsetRect.left && e.clientX < offsetRect.right; + DragContext.clearContext(); + }; - if (tryScroll) - { - if (e.clientY < offsetRect.top) { - AutoScroll.scroll(container, (e.clientY - offsetRect.top)); - } - else if (e.clientY > offsetRect.bottom) { - AutoScroll.scroll(container, (e.clientY - offsetRect.bottom)); - } else { - AutoScroll.stop(); - } + var mouseDragging = function mouseMove(e) + { + if (this != window) + return; + + var deltaTime = new Date() - trackingTelemetry; + if (deltaTime < 16) { + return; + } + + var tryScroll = e.clientX > offsetRect.left && e.clientX < offsetRect.right; + + if (tryScroll) + { + if (e.clientY < offsetRect.top) { + AutoScroll.scroll(container, (e.clientY - offsetRect.top)); + } + else if (e.clientY > offsetRect.bottom) { + AutoScroll.scroll(container, (e.clientY - offsetRect.bottom)); } else { AutoScroll.stop(); } + } else { + AutoScroll.stop(); + } - bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.relativeY + 'px'; - bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.relativeX + 'px'; - trackingTelemetry = new Date(); - }; + bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.relativeY + 'px'; + bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.relativeX + 'px'; + trackingTelemetry = new Date(); + }; - var mouseUp = function _mouseUp(e) + var mouseUp = function _mouseUp(e) + { + switch(SyncModel.state.session) { - switch(SyncModel.state.session) - { - // if restore session preview - case 'restore': { - var bookmarkID = getBookmarkID(e); - - // Test if changing - if (bookmarkID != null) - { - if (bookmarkContext.bookmarkID == bookmarkID) { - // Preview the content if a click event was registered (mouse-up on the same DOM node) - if (!DragContext.hasContext()) { - var mark = SessionSyncModel.bookmarks[bookmarkID]; - var parsed = BookmarkManager.parseContainerFromTitle(mark.title); - BookmarkManager.openBookmark({ - url: mark.url, - mode: BookmarkManager.getOpenMode(0), - favicon: true, - cookieStoreId: parsed.cookieStoreId, - }); - } - } - else - { - if (SessionSyncModel.bookmarks[bookmarkID] != undefined) - { - SessionSyncModel.moveBookmark(bookmarkContext.bookmarkID, bookmarkID); - } + // if restore session preview + case 'restore': { + var bookmarkID = getBookmarkID(e); + + // Test if changing + if (bookmarkID != null) + { + if (bookmarkContext.bookmarkID == bookmarkID) { + // Preview the content if a click event was registered (mouse-up on the same DOM node) + if (!DragContext.hasContext()) { + var mark = SessionSyncModel.bookmarks[bookmarkID]; + var parsed = BookmarkManager.parseContainerFromTitle(mark.title); + BookmarkManager.openBookmark({ + url: mark.url, + mode: BookmarkManager.getOpenMode(0), + favicon: true, + cookieStoreId: parsed.cookieStoreId, + }); } } else { - if (getTargetType(e) === 'folder') + if (SessionSyncModel.bookmarks[bookmarkID] != undefined) { - var sessionID = getSessionID(e); - if (sessionID != null && SessionSyncModel.bookmarks[sessionID] != undefined) { - SessionSyncModel.moveBookmarkTo(bookmarkContext.bookmarkID, sessionID); - } + SessionSyncModel.moveBookmark(bookmarkContext.bookmarkID, bookmarkID); + } + } + } + else + { + if (getTargetType(e) === 'folder') + { + var sessionID = getSessionID(e); + if (sessionID != null && SessionSyncModel.bookmarks[sessionID] != undefined) { + SessionSyncModel.moveBookmarkTo(bookmarkContext.bookmarkID, sessionID); } } - break; } + break; + } - case 'current': { - let tabID = getTabID(e); - let tabContext = SyncModel.tabs[tabID]; + case 'current': { + let tabID = getTabID(e); + let tabContext = SyncModel.tabs[tabID]; - if (tabContext) + if (tabContext) + { + if (tabContext != bookmarkContext) { - if (tabContext != bookmarkContext) - { - SessionManager.moveTab(bookmarkContext.tab.id, tabContext.tab.index, tabContext.tab.windowId); - } - else - { - if (tabID) { - SessionManager.activateTab(tabID); - var windowID = getWindowID(e); - if (windowID) { - SessionManager.activateWindow(windowID); - } + SessionManager.moveTab(bookmarkContext.tab.id, tabContext.tab.index, tabContext.tab.windowId); + } + else + { + if (tabID) { + SessionManager.activateTab(tabID); + var windowID = getWindowID(e); + if (windowID) { + SessionManager.activateWindow(windowID); } } } - else + } + else + { + if (getTargetType(e) === 'folder') { - if (getTargetType(e) === 'folder') - { - var savingSessionID = getSessionID(e); - if (savingSessionID != null) { - BookmarkManager.createBookmarkFromTab(bookmarkContext.tab, savingSessionID) - .then(function() { + var savingSessionID = getSessionID(e); + if (savingSessionID != null) { + BookmarkManager.createBookmarkFromTab(bookmarkContext.tab, savingSessionID) + .then(function() { + if (typeof document !== 'undefined') { WindowEvents.emit(document, 'Notification', { message: 'Saved', }); - }); - } + } + }); } } } } + } - if (DragContext.hasContext()) - { - cancelDragging(e); - } - else - { - window.removeEventListener('mousemove', trackDragging); - } - - window.removeEventListener('mouseup', mouseUp); - }; - - var bookmarkMouseDown = function _bookmarkMouseDown(e) + if (DragContext.hasContext()) { - var target = e.target; - if (DragContext.hasContext()) { - cancelDragging(e); - return; - } + cancelDragging(e); + } + else + { + window.removeEventListener('mousemove', trackDragging); + } - switch(SyncModel.state.session) - { - // if restore session preview - case 'restore': { - var bookmarkID = getBookmarkID(e); - if (bookmarkID == null) { - return; - } + window.removeEventListener('mouseup', mouseUp); + }; - bookmarkContext = SyncModel.getBookmark(bookmarkID); + var bookmarkMouseDown = function _bookmarkMouseDown(e) + { + var target = e.target; + if (DragContext.hasContext()) { + cancelDragging(e); + return; + } - if (e.button == 0) - { - startDragEvent = e; - window.addEventListener('mousemove', trackDragging); - window.addEventListener('mouseup', mouseUp); - return; - } + switch(SyncModel.state.session) + { + // if restore session preview + case 'restore': { + var bookmarkID = getBookmarkID(e); + if (bookmarkID == null) { + return; + } - // Middle Click - if (e.button == 1) - { - var mark = SessionSyncModel.bookmarks[bookmarkID]; - var parsed = BookmarkManager.parseContainerFromTitle(mark.title); - BookmarkManager.openBookmark({ - url: mark.url, - mode: BookmarkManager.getOpenMode(1), - favicon: true, - cookieStoreId: parsed.cookieStoreId, - }); - return; - } + bookmarkContext = SyncModel.getBookmark(bookmarkID); + if (e.button == 0) + { + startDragEvent = e; + window.addEventListener('mousemove', trackDragging); + window.addEventListener('mouseup', mouseUp); return; } - // If active session preview - case 'current': { + // Middle Click + if (e.button == 1) + { + var mark = SessionSyncModel.bookmarks[bookmarkID]; + var parsed = BookmarkManager.parseContainerFromTitle(mark.title); + BookmarkManager.openBookmark({ + url: mark.url, + mode: BookmarkManager.getOpenMode(1), + favicon: true, + cookieStoreId: parsed.cookieStoreId, + }); + return; + } - if (e.button == 0) - { - let tabID = getTabID(e); - if (tabID != undefined) { - bookmarkContext = SyncModel.tabs[tabID]; - if (bookmarkContext != undefined) { - startDragEvent = e; - window.addEventListener('mousemove', trackDragging); - window.addEventListener('mouseup', mouseUp); - return; - } + return; + } + + // If active session preview + case 'current': { + + if (e.button == 0) + { + let tabID = getTabID(e); + if (tabID != undefined) { + bookmarkContext = SyncModel.tabs[tabID]; + if (bookmarkContext != undefined) { + startDragEvent = e; + window.addEventListener('mousemove', trackDragging); + window.addEventListener('mouseup', mouseUp); + return; } - else { - let windowID = getWindowID(e); - if (windowID) { - SessionManager.activateWindow(windowID); - } + } + else { + let windowID = getWindowID(e); + if (windowID) { + SessionManager.activateWindow(windowID); } } - return; } + return; + } - // if history session preview - case 'history': { - if (target.hasAttribute('url')) - { - let url = getTabURL(e); + // if history session preview + case 'history': { + if (target.hasAttribute('url')) + { + let url = getTabURL(e); - switch (e.button) - { - case 0: - case 1: { - BookmarkManager.openBookmark({ - url: url, - mode: BookmarkManager.getOpenMode(e.button) - }); - - break; - } + switch (e.button) + { + case 0: + case 1: { + BookmarkManager.openBookmark({ + url: url, + mode: BookmarkManager.getOpenMode(e.button) + }); + + break; } } - else + } + else + { + if (e.button == 0) { - if (e.button == 0) - { - let windowID = getWindowID(e); - if (windowID != undefined) { - WindowEvents.emit(document, 'HistorySessionRestoreWindow', windowID); - } + let windowID = getWindowID(e); + if (windowID != undefined && typeof document !== 'undefined') { + WindowEvents.emit(document, 'HistorySessionRestoreWindow', windowID); } } - return; } + return; } - }; + } + }; - var bookmarkContextMenu = function bookmarkContextMenu(e) + var bookmarkContextMenu = function bookmarkContextMenu(e) + { + if (typeof document === 'undefined') return; + + switch (SyncModel.state.session) { - switch (SyncModel.state.session) - { - case 'restore': { - var bookmarkID = getBookmarkID(e); - if (bookmarkID == null) - return; - - WindowEvents.emit(document, 'BookmarkCtxMenu-Open', { - context: bookmarkID, + case 'restore': { + var bookmarkID = getBookmarkID(e); + if (bookmarkID == null) + return; + + WindowEvents.emit(document, 'BookmarkCtxMenu-Open', { + context: bookmarkID, + event: e + }); + break; + } + + case 'history': { + let url = getTabURL(e); + if (url) { + WindowEvents.emit(document, 'HistoryMarksCtxMenu-Open', { + context: url, event: e }); - break; - } - - case 'history': { - let url = getTabURL(e); - if (url) { - WindowEvents.emit(document, 'HistoryMarksCtxMenu-Open', { - context: url, - event: e - }); - } } } - }; - - // -------------------------------------------------------------- - // App Events - + } + }; - // ------------------------------------------------------------------------ - // Init Code Events + // -------------------------------------------------------------- + // App Events - container.addEventListener('scroll', function () { - AppConfig.set('state.scrollTop.' + SyncModel.state.session, container.scrollTop); - }); - container.addEventListener('contextmenu', bookmarkContextMenu); - container.addEventListener('mousedown', bookmarkMouseDown); - container.addEventListener('mouseover', mouseOver); - container.addEventListener('mouseleave', mouseLeave); + // ------------------------------------------------------------------------ + // Init Code Events - container.addEventListener('dblclick', bookmarkDblClick); - }; + container.addEventListener('scroll', function () { + AppConfig.set('state.scrollTop.' + SyncModel.state.session, container.scrollTop); + }); + container.addEventListener('contextmenu', bookmarkContextMenu); + container.addEventListener('mousedown', bookmarkMouseDown); - // ***************************************************************************** - // Public API + container.addEventListener('mouseover', mouseOver); + container.addEventListener('mouseleave', mouseLeave); - exports.SessionBookmarkEvents = SessionBookmarkEvents; -}); \ No newline at end of file + container.addEventListener('dblclick', bookmarkDblClick); +} \ No newline at end of file diff --git a/data/scripts/components/session-bookmark.js b/data/scripts/components/session-bookmark.js index 52e9b7d..9483b7c 100644 --- a/data/scripts/components/session-bookmark.js +++ b/data/scripts/components/session-bookmark.js @@ -1,45 +1,41 @@ -define(function(require, exports) { +'use strict'; - 'use strict'; +// ***************************************************************************** +// Custom Modules - // ***************************************************************************** - // Custom Modules +import { HTMLCreator } from '../utils/dom'; +import { AutoScroll } from '../utils/auto-scroll'; +import { BookmarkManager } from '../session/bookmarks'; - const { HTMLCreator } = require('../utils/dom'); - const { AutoScroll } = require('../utils/auto-scroll'); +// ***************************************************************************** +// API - const { BookmarkManager } = require('../session/bookmarks'); +const bookmarkOffset = 2.2; // height + 2 in em - // ***************************************************************************** - // API +function setTabFavicon(faviconNode, faviconURL) { + faviconNode.style.background = 'url("' + faviconURL + '")'; + faviconNode.style.backgroundRepeat = 'no-repeat'; + faviconNode.style.backgroundPosition = 'center center'; + faviconNode.style.backgroundSize = 'contain'; +} - const bookmarkOffset = 2.2; // height + 2 in em +/* +* Session Bookmark +* Used for interacting with a Firefox bookmark item +*/ - function setTabFavicon(faviconNode, faviconURL) - { - faviconNode.style.background = 'url("' + faviconURL + '")'; - faviconNode.style.backgroundRepeat = 'no-repeat'; - faviconNode.style.backgroundPosition = 'center center'; - faviconNode.style.backgroundSize = 'contain'; - } - - /* - * Session Bookmark - * Used for interacting with a Firefox bookmark item - */ +export class SessionBookmark { + constructor(bookmark) { + const DomElem = HTMLCreator(); - function SessionBookmark(bookmark) - { - var DomElem = HTMLCreator(); - - var box = DomElem('div', {class : 'bookmark'}); + const box = DomElem('div', { class: 'bookmark' }); box.setAttribute('pinned', bookmark.pinned); - var favicon = DomElem('div', {class : 'favicon'}); + const favicon = DomElem('div', { class: 'favicon' }); favicon.setAttribute('bookmarkID', bookmark.id); favicon.setAttribute('over', ''); - var title = DomElem('div', {class : 'text'}); + const title = DomElem('div', { class: 'text' }); title.setAttribute('bookmarkID', bookmark.id); title.setAttribute('over', ''); title.textContent = bookmark.title; @@ -47,13 +43,10 @@ define(function(require, exports) { box.appendChild(favicon); box.appendChild(title); - BookmarkManager.getFaviconUrl(bookmark.url, function (url) { + BookmarkManager.getFaviconUrl(bookmark.url, (url) => { setTabFavicon(favicon, url); }); - // ------------------------------------------------------------------------ - // Public data - this.bookmark = bookmark; this.DOMtitle = title; this.DOMfavicon = favicon; @@ -61,57 +54,52 @@ define(function(require, exports) { this.DOMRoot = box; } - SessionBookmark.prototype.setTitle = function setTitle(title) - { + setTitle(title) { this.DOMtitle.textContent = title; - }; + } - SessionBookmark.prototype.restorePosition = function restorePosition() - { + restorePosition() { this.DOMRoot.style.left = '0px'; this.DOMRoot.style.top = this.position * bookmarkOffset + 'em'; - }; + } - SessionBookmark.prototype.setVirtualPosition = function setVirtualPosition(position) - { + setVirtualPosition(position) { this.position = position; - // this.DOMtitle.textContent = '[' + position + '] ' + this.bookmark.title, this.DOMRoot.style.top = position * bookmarkOffset + 'em'; - }; + } - SessionBookmark.prototype.remove = function remove() - { - var box = this.DOMRoot; - if (box.parentElement) + remove() { + const box = this.DOMRoot; + if (box.parentElement) { box.parentElement.removeChild(box); - }; + } + } - SessionBookmark.prototype.highlight = function highlight() - { + highlight() { this.DOMRoot.setAttribute('highlight', 'true'); - setTimeout(function() { + setTimeout(() => { this.DOMRoot.removeAttribute('highlight'); - }.bind(this), 2000); + }, 2000); - var offsetStep = this.DOMRoot.clientHeight; - var parentNode = this.DOMRoot.parentElement; - var offset = this.DOMRoot.offsetTop - parentNode.clientHeight / 2 + offsetStep; + const offsetStep = this.DOMRoot.clientHeight; + const parentNode = this.DOMRoot.parentElement; + const offset = this.DOMRoot.offsetTop - parentNode.clientHeight / 2 + offsetStep; AutoScroll.scrollTo(parentNode, offset, 0.25); - }; - - /* - * SessionWindow - * Used as separator for different windows - */ + } +} - function SessionWindow(windowID, windowIndex, index) - { - var DomElem = HTMLCreator(); +/* +* SessionWindow +* Used as separator for different windows +*/ +export class SessionWindow { + constructor(windowID, windowIndex, index) { + const DomElem = HTMLCreator(); - var box = DomElem('div', {class : 'window-separator'}); + const box = DomElem('div', { class: 'window-separator' }); box.style.top = index * bookmarkOffset + 'em'; - var text = DomElem('div', {class : 'text'}); + const text = DomElem('div', { class: 'text' }); text.textContent = 'Window ' + (windowIndex + 1); text.setAttribute('windowID', windowID); @@ -119,28 +107,28 @@ define(function(require, exports) { this.DOMRoot = box; } +} - /* - * SessionTab - * Used for displaying tabs from the current window - */ - - function SessionTab(tab, indexOffset) - { - var DomElem = HTMLCreator(); +/* +* SessionTab +* Used for displaying tabs from the current window +*/ +export class SessionTab { + constructor(tab, indexOffset) { + const DomElem = HTMLCreator(); - var box = DomElem('div', {class : 'bookmark'}); + const box = DomElem('div', { class: 'bookmark' }); box.style.top = (tab.index + indexOffset) * bookmarkOffset + 'em'; box.setAttribute('pinned', tab.pinned); box.setAttribute('url', tab.url); - var favicon = DomElem('div', {class : 'favicon'}); + const favicon = DomElem('div', { class: 'favicon' }); favicon.setAttribute('tabID', tab.id); favicon.setAttribute('windowID', tab.windowId); favicon.setAttribute('url', tab.url); favicon.setAttribute('over', ''); - var text = DomElem('div', {class : 'text'}); + const text = DomElem('div', { class: 'text' }); text.setAttribute('tabID', tab.id); text.setAttribute('windowID', tab.windowId); text.setAttribute('url', tab.url); @@ -159,26 +147,25 @@ define(function(require, exports) { this.DOMRoot = box; } - SessionTab.prototype.restorePosition = function restorePosition() - { + restorePosition() { this.DOMRoot.style.left = '0px'; this.DOMRoot.style.top = this.position * bookmarkOffset + 'em'; - }; - - /* - * SessionSeparator - * Used as separator for different windows - */ + } +} - function HistoryWindow(sessionWindowInfo, windowID, index) - { - var DomElem = HTMLCreator(); +/* +* HistoryWindow +* Used as separator for different windows +*/ +export class HistoryWindow { + constructor(sessionWindowInfo, windowID, index) { + const DomElem = HTMLCreator(); - var box = DomElem('div', {class : 'history-window-separator'}); + const box = DomElem('div', { class: 'history-window-separator' }); box.style.top = index * bookmarkOffset + 'em'; box.setAttribute('windowID', windowID); - var text = DomElem('div', {class : 'text'}); + const text = DomElem('div', { class: 'text' }); text.setAttribute('windowID', windowID); text.textContent = 'Window ' + windowID; @@ -190,31 +177,31 @@ define(function(require, exports) { this.DOMRoot = box; } +} - /* - * HistoryTab - * Used for displaying tabs from the history windows - */ - - function HistoryTab(tab, indexOffset) - { - var DomElem = HTMLCreator(); +/* +* HistoryTab +* Used for displaying tabs from the history windows +*/ +export class HistoryTab { + constructor(tab, indexOffset) { + const DomElem = HTMLCreator(); - var box = DomElem('div', {class : 'bookmark'}); + const box = DomElem('div', { class: 'bookmark' }); box.style.top = (tab.index + indexOffset) * bookmarkOffset + 'em'; box.setAttribute('url', tab.url); box.setAttribute('pinned', tab.pinned); - var favicon = DomElem('div', {class : 'favicon'}); + const favicon = DomElem('div', { class: 'favicon' }); favicon.setAttribute('url', tab.url); favicon.setAttribute('over', ''); - var text = DomElem('div', {class : 'text'}); + const text = DomElem('div', { class: 'text' }); text.setAttribute('url', tab.url); text.setAttribute('over', ''); text.textContent = tab.title; - if (tab.favIconUrl) { + if (tab.favIconUrl) { setTabFavicon(favicon, tab.favIconUrl); } @@ -223,40 +210,4 @@ define(function(require, exports) { this.DOMRoot = box; } - - // var SortMethods = { - - // 'name-asc' : function sortByNameAscending(a, b) { - // return a.title_lowercase > b.title_lowercase; - // }, - - // 'name-desc' : function sortByNameDescending(a, b) { - // return a.title_lowercase < b.title_lowercase; - // }, - - // 'position-asc' : function sortByPositionAscending(a, b) { - // return a.index > b.index; - // }, - - // 'position-desc' : function sortByPositionDescending(a, b) { - // return a.index < b.index; - // }, - - // 'date-asc' : function sortByDateAscending(a, b) { - // return a.date > b.date; - // }, - - // 'date-desc' : function sortByDateDescending(a, b) { - // return a.date < b.date; - // } - // }; - - // ***************************************************************************** - // Public API - - exports.SessionBookmark = SessionBookmark; - exports.SessionTab = SessionTab; - exports.SessionWindow = SessionWindow; - exports.HistoryWindow = HistoryWindow; - exports.HistoryTab = HistoryTab; -}); \ No newline at end of file +} diff --git a/data/scripts/components/session-container.js b/data/scripts/components/session-container.js index 747ad60..1055fd2 100644 --- a/data/scripts/components/session-container.js +++ b/data/scripts/components/session-container.js @@ -1,440 +1,448 @@ -define(function(require, exports) { - 'use strict'; - - // ***************************************************************************** - // Custom Modules - - // App - const { AppConfig } = require('../config'); - const { SessionBookmark, SessionTab, SessionWindow, HistoryWindow, HistoryTab } = require('./session-bookmark'); - const { SessionBookmarkEvents } = require('./session-bookmark-events'); - const { SessionSyncModel } = require('./session-sync-model'); - - // Utils - const { HTMLCreator } = require('../utils/dom'); - const { WindowEvents, GlobalEvents } = require('../utils/global-events'); - - // Components - const { BookmarkManager } = require('../session/bookmarks'); - const { SessionManager } = require('../session/management'); - const { SessionHistory } = require('./session-history'); - - // ***************************************************************************** - // APIconst { WindowEvents, GlobalEvents } = require('../utils/global-events'); - - /* - * SessionContainer UI - * Panel for listing session information (Firefox Bookmark Folder) and children bookmarks - */ - - function SessionContainer() - { - var DomElem = HTMLCreator(); - - // ------------------------------------------------------------------------ - // Init UI - - var container = DomElem('div', {class: 'session-container'}); - container.style.fontSize = AppConfig.get('style.scale.bookmarks') + 'px'; - - var bookmarks = DomElem('div', {class: 'bookmarks'}); - container.appendChild(bookmarks); - - // ------------------------------------------------------------------------ - // User events - - function isValidSessionID(ID) - { - return (SessionSyncModel.bookmarks[ID] !== undefined && - SessionSyncModel.bookmarks[ID].type === 'folder' && - SessionSyncModel.bookmarks[ID].parentId == AppConfig.get('storageID')); - } - - function restoreHistoryWindow(sessionInfo) - { - var list = []; - sessionInfo.windows.forEach(historyWindow => { - list.push(historyWindow.tabs); - }); - SessionManager.loadBookmarksNewWindow(list); - } - - WindowEvents.on(document, 'HistorySessionSave', this.saveHistorySession.bind(this)); - - WindowEvents.on(document, 'HistorySessionRestore', function (index) { - SessionHistory.getHistorySession(index, function (sessionInfo) { - restoreHistoryWindow(sessionInfo); - }); - }); - - WindowEvents.on(document, 'HistorySessionRestoreWindow', function (windowID) { - if (this.SyncModel.state.session === 'history') { - if (this.selectedHistorySession.active == true) - { - var activeSessionWindows = Object.values(this.selectedHistorySession.windows); - if (activeSessionWindows[windowID].active) { - SessionManager.activateWindow(activeSessionWindows[windowID].id); - } - else { - SessionManager.loadBookmarksNewWindow([activeSessionWindows[windowID].tabs]); - } - } - else { - SessionManager.loadBookmarksNewWindow([this.selectedHistorySession.windows[windowID].tabs]); - } - } - }.bind(this)); - - WindowEvents.on(document, 'MenuRestoreClick', function() { - if (isValidSessionID(this.activeSessionID)) { - SessionManager.restoreSession(this.activeSessionID); - } - }.bind(this)); - - WindowEvents.on(document, 'MenuRestoreNewWindow', function() { - - if (isValidSessionID(this.activeSessionID)) { - SessionManager.restoreNewWindow(this.activeSessionID); - } - - // history session - if (this.SyncModel.state.session === 'history') { - restoreHistoryWindow(this.selectedHistorySession); - } - }.bind(this)); - - WindowEvents.on(document, 'ReplaceSessionAll', function() { - if (isValidSessionID(this.activeSessionID)) { - SessionManager.replaceSessionAll(this.activeSessionID); - } - }.bind(this)); - - WindowEvents.on(document, 'ReplaceSession', function() { - if (isValidSessionID(this.activeSessionID)) { - SessionManager.replaceSession(this.activeSessionID); - } - }.bind(this)); - - WindowEvents.on(document, 'MenuMergeSessions', function(e) { - if (isValidSessionID(this.activeSessionID)) { - WindowEvents.emit(document, 'ConfirmBox-Open', { - event: e, - message: 'Merge selected session and active window session?', - callback: function() { - SessionManager.mergeSessions(this.activeSessionID); - }.bind(this) - }); - } - }.bind(this)); - - WindowEvents.on(document, 'MenuReplaceSession', function(e) { - if (isValidSessionID(this.activeSessionID)) { - WindowEvents.emit(document, 'ConfirmBox-Open', { - event: e, - message: 'Overwrite the selected session with active window session?', - callback: function() { - SessionManager.overwriteSession(this.activeSessionID); - }.bind(this) - }); - } - }.bind(this)); - - WindowEvents.on(document, 'MenuAddCurrentTab', function() { - if (isValidSessionID(this.activeSessionID)) { - this.bookmarkCurrentTab(); - } - }.bind(this)); - - WindowEvents.on(document, 'MenuSaveSession', function() { - // do not save if the session is already saved - if (isValidSessionID(this.activeSessionID)) { - return; - } - - switch(this.SyncModel.state.session) - { - case 'current': - SessionManager.saveActiveSession(); - break; - - case 'history': - this.saveHistorySession(); - break; - } - - }.bind(this)); - - WindowEvents.on(document, 'MenuFixLazySession', function() { - if (isValidSessionID(this.activeSessionID)) { - SessionManager.fixLazySession(this.activeSessionID); - } - }.bind(this)); - - GlobalEvents.on('UpdateCurrentSession', function() { - if (this.SyncModel.state.session == 'current') { - this.showCurrentSession(); - } - }.bind(this)); - - WindowEvents.on(document, 'EditSelectedSession', function() { - WindowEvents.emit(document, 'SessionContextMenu-EditSession', this.activeSessionID); - }.bind(this)); - - WindowEvents.on(document, 'ShowCurrentSession', this.showCurrentSession.bind(this)); - - WindowEvents.on(document, 'ShowHistorySession', this.showHistorySession.bind(this)); - - WindowEvents.on(document, 'ViewSession', this.previewSession.bind(this)); - - AppConfig.onChange('style.scale.bookmarks', function(size) { - container.style.fontSize = size + 'px'; - }); - - AppConfig.onChange('session.save', function(obj) { - if (this.SyncModel.state.session == 'current') { - this.showCurrentSession(); - } - }.bind(this)); - - WindowEvents.on(document, 'SessionDescriptionChanged', function(sessionID) { - if (this.activeSessionID == sessionID) - { - this.updateSessionInfo(); - } - }.bind(this)); - - // Tracked events - - SessionBookmarkEvents(bookmarks); - - var updateTimeout = null; - function updateOnTabEvent() - { - if (SessionManager.tabTracking()) - { - if (updateTimeout == null) { - updateTimeout = setTimeout(function () { - updateTimeout = null; - GlobalEvents.emit('UpdateCurrentSession'); - }, 1000); - } - } - } - - // Tab updates tracking - var trackedEvents = ['onUpdated', 'onCreated', 'onDetached', 'onMoved', 'onAttached', 'onRemoved']; - trackedEvents.forEach(function (eventType) { - browser.tabs[eventType].addListener(updateOnTabEvent); - }.bind(this)); - - // ------------------------------------------------------------------------ - // Public data - - this.DOMBookmarks = bookmarks; - this.DOMRoot = container; - - this.SyncModel = SessionSyncModel.getModel(document); - } - - SessionContainer.prototype.setUIState = function setUIState(state, headerTitle, sessionID) - { - WindowEvents.emit(document, 'SetUIState', {'session': state}); - WindowEvents.emit(document, 'SetSessionDescription', headerTitle); - - this.DOMBookmarks.textContent = ''; - this.activeSessionID = sessionID; - this.selectedHistorySession = undefined; - }; - - SessionContainer.prototype.updateSessionInfo = function updateSessionInfo() - { - var session = SessionSyncModel.bookmarks[this.activeSessionID]; - WindowEvents.emit(document, 'SetSessionDescription', session.title); - WindowEvents.emit(document, 'SetSessionDate', session.dateAdded); - }; - - SessionContainer.prototype.showCurrentSession = function showCurrentSession() - { - var DOMBookmarks = this.DOMBookmarks; - this.setUIState('current', 'Active session', 0); - - SessionManager.getCurrentWindow(function(mozWindow) { - - // Get non-private windows - if (mozWindow.incognito == true || AppConfig.get('session.save').allWindows == false) - { - mozWindow.tabs.forEach(function (tab) { - let sessionTab = new SessionTab(tab, 0, -1); - this.SyncModel.tabs[sessionTab.tab.id] = sessionTab; - DOMBookmarks.appendChild(sessionTab.DOMRoot); - }.bind(this)); - - this.restoreScrollTop('current'); - } - else - { - // Get non-private windows - var tabIndex = 0; - var windowIndex = 0; // display index in UI - ignores private windows - - SessionManager.getAllWindows(function(windows) { - for (let mozWindow of windows) - { - if (!mozWindow.incognito) - { - // Add window separator - var sessionWindow = new SessionWindow(mozWindow.id, windowIndex, tabIndex); - DOMBookmarks.appendChild(sessionWindow.DOMRoot); - tabIndex++; - - var globalOffset = tabIndex; - - // Add window tabs - for (let i in mozWindow.tabs) - { - let tab = mozWindow.tabs[i]; - let sessionTab = new SessionTab(tab, globalOffset); - this.SyncModel.tabs[sessionTab.tab.id] = sessionTab; - DOMBookmarks.appendChild(sessionTab.DOMRoot); - tabIndex++; - } - - windowIndex++; - } - } - - this.restoreScrollTop('current'); - }.bind(this)); - } - }.bind(this)); - }; - - SessionContainer.prototype.showHistorySession = function showHistorySession(sessionInfo) - { - var newSession = (this.selectedHistorySession != undefined) && (this.selectedHistorySession != sessionInfo); - - this.setUIState('history', (new Date(sessionInfo.lastSave)).toLocaleString(), -1); - this.selectedHistorySession = sessionInfo; - - var tabIndex = 0; - var windowIndex = 0; - - for (let key in sessionInfo.windows) - { - // Add window separator - var sessionWindow = new HistoryWindow(sessionInfo.windows[key], windowIndex, tabIndex); - this.DOMBookmarks.appendChild(sessionWindow.DOMRoot); - tabIndex++; - - var globalOffset = tabIndex; - - // Add window tabs - var windowTabs = sessionInfo.windows[key].tabs; - for (var i in windowTabs) - { - var tab = windowTabs[i]; - tab.index = i | 0; - var sessionTab = new HistoryTab(tab, globalOffset); - this.DOMBookmarks.appendChild(sessionTab.DOMRoot); - tabIndex++; - } - - windowIndex++; - } - - this.restoreScrollTop('history', newSession ? 0 : undefined); - }; - - SessionContainer.prototype.previewSession = function previewSession(sessionID) - { - var session = SessionSyncModel.bookmarks[sessionID]; - this.setUIState('restore', '', sessionID); - - BookmarkManager.getFolderBookmarks(session.id, function (marks) { - - WindowEvents.emit(document, 'SetPromiseSession', { sessionID: session.id, update: false } ); - - var len = marks.length; - for (var i = 0; i < len; i++) - { - var bookmark = new SessionBookmark(marks[i]); - bookmark.setVirtualPosition(marks[i].index); - - SessionSyncModel.bookmarks[marks[i].id] = marks[i]; - this.SyncModel.setBookmark(marks[i].id, bookmark); - this.DOMBookmarks.appendChild(bookmark.DOMRoot); - } - - this.updateSessionInfo(); - this.restoreScrollTop('restore'); - - }.bind(this)); - }; - - SessionContainer.prototype.restoreScrollTop = function restoreScrollTop(state, forcedValue) - { - var scrollTop = AppConfig.get('state.scrollTop.' + state); - this.DOMBookmarks.scrollTop = (forcedValue !== undefined) ? forcedValue : scrollTop; - }; - - SessionContainer.prototype.bookmarkCurrentTab = function bookmarkCurrentTab() - { - // Test if already saved in this session - SessionManager.getCurrentTab(function(activeTab) { - - var sessionID = this.activeSessionID; - var bookmarks = SessionSyncModel.bookmarks; - - for (var i in bookmarks) - { - if (bookmarks[i].parentId == sessionID && bookmarks[i].url == activeTab.url) - { - WindowEvents.emit(document, 'Notification', { - message: 'Already Saved', - }); - - var bookmarkElem = this.SyncModel.getBookmark(bookmarks[i].id); - bookmarkElem.highlight(); - return; - } - } - - // Add bookmark into the session - - BookmarkManager.createBookmarkFromTab(activeTab, sessionID) - .then(function (mark) { - var bookmark = new SessionBookmark(mark); - - SessionSyncModel.bookmarks[mark.id] = mark; - this.SyncModel.setBookmark(mark.id, bookmark); - this.DOMBookmarks.appendChild(bookmark.DOMRoot); - - bookmark.setVirtualPosition(mark.index); - bookmark.highlight(); - - }.bind(this)); - - }.bind(this)); - }; - - SessionContainer.prototype.saveHistorySession = function saveHistorySession(sessionIndex) - { - if (sessionIndex == undefined) - { - // When Save button from toolbar is pressed - SessionManager.saveHistorySession(this.selectedHistorySession); - } - else - { - // Save from context menu - SessionHistory.getHistorySession(sessionIndex, function (sessionInfo) { - SessionManager.saveHistorySession(sessionInfo); - }); - } - }; - - // ***************************************************************************** - // Public API - - exports.SessionContainer = SessionContainer; -}); \ No newline at end of file +'use strict'; + +// ***************************************************************************** +// Custom Modules + +// App +import { AppConfig } from '../config'; +import { SessionBookmark, SessionTab, SessionWindow, HistoryWindow, HistoryTab } from './session-bookmark'; +import { SessionBookmarkEvents } from './session-bookmark-events'; +import { SessionSyncModel } from './session-sync-model'; + +// Utils +import { HTMLCreator } from '../utils/dom'; +import { WindowEvents, GlobalEvents } from '../utils/global-events'; + +// Components +import { BookmarkManager } from '../session/bookmarks'; +import { SessionManager } from '../session/management'; +import { SessionHistory } from './session-history'; + +// ***************************************************************************** +// API + +/* +* SessionContainer UI +* Panel for listing session information (Firefox Bookmark Folder) and children bookmarks +*/ + +export class SessionContainer +{ + constructor() + { + var DomElem = HTMLCreator(); + + // ------------------------------------------------------------------------ + // Init UI + + var container = DomElem('div', {class: 'session-container'}); + container.style.fontSize = AppConfig.get('style.scale.bookmarks') + 'px'; + + var bookmarks = DomElem('div', {class: 'bookmarks'}); + container.appendChild(bookmarks); + + // ------------------------------------------------------------------------ + // User events + + function isValidSessionID(ID) + { + return (SessionSyncModel.bookmarks[ID] !== undefined && + SessionSyncModel.bookmarks[ID].type === 'folder' && + SessionSyncModel.bookmarks[ID].parentId == AppConfig.get('storageID')); + } + + function restoreHistoryWindow(sessionInfo) + { + var list = []; + sessionInfo.windows.forEach(historyWindow => { + list.push(historyWindow.tabs); + }); + SessionManager.loadBookmarksNewWindow(list); + } + + if (typeof document !== 'undefined') { + WindowEvents.on(document, 'HistorySessionSave', this.saveHistorySession.bind(this)); + + WindowEvents.on(document, 'HistorySessionRestore', (index) => { + SessionHistory.getHistorySession(index, (sessionInfo) => { + restoreHistoryWindow(sessionInfo); + }); + }); + + WindowEvents.on(document, 'HistorySessionRestoreWindow', (windowID) => { + if (this.SyncModel.state.session === 'history') { + if (this.selectedHistorySession.active == true) + { + var activeSessionWindows = Object.values(this.selectedHistorySession.windows); + if (activeSessionWindows[windowID].active) { + SessionManager.activateWindow(activeSessionWindows[windowID].id); + } + else { + SessionManager.loadBookmarksNewWindow([activeSessionWindows[windowID].tabs]); + } + } + else { + SessionManager.loadBookmarksNewWindow([this.selectedHistorySession.windows[windowID].tabs]); + } + } + }); + + WindowEvents.on(document, 'MenuRestoreClick', () => { + if (isValidSessionID(this.activeSessionID)) { + SessionManager.restoreSession(this.activeSessionID); + } + }); + + WindowEvents.on(document, 'MenuRestoreNewWindow', () => { + + if (isValidSessionID(this.activeSessionID)) { + SessionManager.restoreNewWindow(this.activeSessionID); + } + + // history session + if (this.SyncModel.state.session === 'history') { + restoreHistoryWindow(this.selectedHistorySession); + } + }); + + WindowEvents.on(document, 'ReplaceSessionAll', () => { + if (isValidSessionID(this.activeSessionID)) { + SessionManager.replaceSessionAll(this.activeSessionID); + } + }); + + WindowEvents.on(document, 'ReplaceSession', () => { + if (isValidSessionID(this.activeSessionID)) { + SessionManager.replaceSession(this.activeSessionID); + } + }); + + WindowEvents.on(document, 'MenuMergeSessions', (e) => { + if (isValidSessionID(this.activeSessionID)) { + WindowEvents.emit(document, 'ConfirmBox-Open', { + event: e, + message: 'Merge selected session and active window session?', + callback: () => { + SessionManager.mergeSessions(this.activeSessionID); + } + }); + } + }); + + WindowEvents.on(document, 'MenuReplaceSession', (e) => { + if (isValidSessionID(this.activeSessionID)) { + WindowEvents.emit(document, 'ConfirmBox-Open', { + event: e, + message: 'Overwrite the selected session with active window session?', + callback: () => { + SessionManager.overwriteSession(this.activeSessionID); + } + }); + } + }); + + WindowEvents.on(document, 'MenuAddCurrentTab', () => { + if (isValidSessionID(this.activeSessionID)) { + this.bookmarkCurrentTab(); + } + }); + + WindowEvents.on(document, 'MenuSaveSession', () => { + // do not save if the session is already saved + if (isValidSessionID(this.activeSessionID)) { + return; + } + + switch(this.SyncModel.state.session) + { + case 'current': + SessionManager.saveActiveSession(); + break; + + case 'history': + this.saveHistorySession(); + break; + } + + }); + + WindowEvents.on(document, 'MenuFixLazySession', () => { + if (isValidSessionID(this.activeSessionID)) { + SessionManager.fixLazySession(this.activeSessionID); + } + }); + + WindowEvents.on(document, 'EditSelectedSession', () => { + WindowEvents.emit(document, 'SessionContextMenu-EditSession', this.activeSessionID); + }); + + WindowEvents.on(document, 'ShowCurrentSession', this.showCurrentSession.bind(this)); + + WindowEvents.on(document, 'ShowHistorySession', this.showHistorySession.bind(this)); + + WindowEvents.on(document, 'ViewSession', this.previewSession.bind(this)); + + WindowEvents.on(document, 'SessionDescriptionChanged', (sessionID) => { + if (this.activeSessionID == sessionID) + { + this.updateSessionInfo(); + } + }); + } + + GlobalEvents.on('UpdateCurrentSession', () => { + if (this.SyncModel.state.session == 'current') { + this.showCurrentSession(); + } + }); + + AppConfig.onChange('style.scale.bookmarks', function(size) { + container.style.fontSize = size + 'px'; + }); + + AppConfig.onChange('session.save', (obj) => { + if (this.SyncModel.state.session == 'current') { + this.showCurrentSession(); + } + }); + + // Tracked events + + SessionBookmarkEvents(bookmarks); + + var updateTimeout = null; + function updateOnTabEvent() + { + if (SessionManager.tabTracking()) + { + if (updateTimeout == null) { + updateTimeout = setTimeout(function () { + updateTimeout = null; + GlobalEvents.emit('UpdateCurrentSession'); + }, 1000); + } + } + } + + // Tab updates tracking + if (typeof browser !== 'undefined' && browser.tabs) { + var trackedEvents = ['onUpdated', 'onCreated', 'onDetached', 'onMoved', 'onAttached', 'onRemoved']; + trackedEvents.forEach((eventType) => { + browser.tabs[eventType].addListener(updateOnTabEvent); + }); + } + + // ------------------------------------------------------------------------ + // Public data + + this.DOMBookmarks = bookmarks; + this.DOMRoot = container; + + this.SyncModel = SessionSyncModel.getModel(typeof document !== 'undefined' ? document : null); + } + + setUIState(state, headerTitle, sessionID) + { + if (typeof document !== 'undefined') { + WindowEvents.emit(document, 'SetUIState', {'session': state}); + WindowEvents.emit(document, 'SetSessionDescription', headerTitle); + } + + this.DOMBookmarks.textContent = ''; + this.activeSessionID = sessionID; + this.selectedHistorySession = undefined; + } + + updateSessionInfo() + { + var session = SessionSyncModel.bookmarks[this.activeSessionID]; + if (typeof document !== 'undefined') { + WindowEvents.emit(document, 'SetSessionDescription', session.title); + WindowEvents.emit(document, 'SetSessionDate', session.dateAdded); + } + } + + showCurrentSession() + { + var DOMBookmarks = this.DOMBookmarks; + this.setUIState('current', 'Active session', 0); + + SessionManager.getCurrentWindow((mozWindow) => { + + // Get non-private windows + if (mozWindow.incognito == true || AppConfig.get('session.save').allWindows == false) + { + mozWindow.tabs.forEach((tab) => { + let sessionTab = new SessionTab(tab, 0, -1); + this.SyncModel.tabs[sessionTab.tab.id] = sessionTab; + DOMBookmarks.appendChild(sessionTab.DOMRoot); + }); + + this.restoreScrollTop('current'); + } + else + { + // Get non-private windows + var tabIndex = 0; + var windowIndex = 0; // display index in UI - ignores private windows + + SessionManager.getAllWindows((windows) => { + for (let mozWindow of windows) + { + if (!mozWindow.incognito) + { + // Add window separator + var sessionWindow = new SessionWindow(mozWindow.id, windowIndex, tabIndex); + DOMBookmarks.appendChild(sessionWindow.DOMRoot); + tabIndex++; + + var globalOffset = tabIndex; + + // Add window tabs + for (let i in mozWindow.tabs) + { + let tab = mozWindow.tabs[i]; + let sessionTab = new SessionTab(tab, globalOffset); + this.SyncModel.tabs[sessionTab.tab.id] = sessionTab; + DOMBookmarks.appendChild(sessionTab.DOMRoot); + tabIndex++; + } + + windowIndex++; + } + } + + this.restoreScrollTop('current'); + }); + } + }); + } + + showHistorySession(sessionInfo) + { + var newSession = (this.selectedHistorySession != undefined) && (this.selectedHistorySession != sessionInfo); + + this.setUIState('history', (new Date(sessionInfo.lastSave)).toLocaleString(), -1); + this.selectedHistorySession = sessionInfo; + + var tabIndex = 0; + var windowIndex = 0; + + for (let key in sessionInfo.windows) + { + // Add window separator + var sessionWindow = new HistoryWindow(sessionInfo.windows[key], windowIndex, tabIndex); + this.DOMBookmarks.appendChild(sessionWindow.DOMRoot); + tabIndex++; + + var globalOffset = tabIndex; + + // Add window tabs + var windowTabs = sessionInfo.windows[key].tabs; + for (var i in windowTabs) + { + var tab = windowTabs[i]; + tab.index = i | 0; + var sessionTab = new HistoryTab(tab, globalOffset); + this.DOMBookmarks.appendChild(sessionTab.DOMRoot); + tabIndex++; + } + + windowIndex++; + } + + this.restoreScrollTop('history', newSession ? 0 : undefined); + } + + previewSession(sessionID) + { + var session = SessionSyncModel.bookmarks[sessionID]; + this.setUIState('restore', '', sessionID); + + BookmarkManager.getFolderBookmarks(session.id, (marks) => { + + if (typeof document !== 'undefined') { + WindowEvents.emit(document, 'SetPromiseSession', { sessionID: session.id, update: false } ); + } + + var len = marks.length; + for (var i = 0; i < len; i++) + { + var bookmark = new SessionBookmark(marks[i]); + bookmark.setVirtualPosition(marks[i].index); + + SessionSyncModel.bookmarks[marks[i].id] = marks[i]; + this.SyncModel.setBookmark(marks[i].id, bookmark); + this.DOMBookmarks.appendChild(bookmark.DOMRoot); + } + + this.updateSessionInfo(); + this.restoreScrollTop('restore'); + + }); + } + + restoreScrollTop(state, forcedValue) + { + var scrollTop = AppConfig.get('state.scrollTop.' + state); + this.DOMBookmarks.scrollTop = (forcedValue !== undefined) ? forcedValue : scrollTop; + } + + bookmarkCurrentTab() + { + // Test if already saved in this session + SessionManager.getCurrentTab((activeTab) => { + + var sessionID = this.activeSessionID; + var bookmarks = SessionSyncModel.bookmarks; + + for (var i in bookmarks) + { + if (bookmarks[i].parentId == sessionID && bookmarks[i].url == activeTab.url) + { + if (typeof document !== 'undefined') { + WindowEvents.emit(document, 'Notification', { + message: 'Already Saved', + }); + } + + var bookmarkElem = this.SyncModel.getBookmark(bookmarks[i].id); + bookmarkElem.highlight(); + return; + } + } + + // Add bookmark into the session + + BookmarkManager.createBookmarkFromTab(activeTab, sessionID) + .then((mark) => { + var bookmark = new SessionBookmark(mark); + + SessionSyncModel.bookmarks[mark.id] = mark; + this.SyncModel.setBookmark(mark.id, bookmark); + this.DOMBookmarks.appendChild(bookmark.DOMRoot); + + bookmark.setVirtualPosition(mark.index); + bookmark.highlight(); + + }); + + }); + } + + saveHistorySession(sessionIndex) + { + if (sessionIndex == undefined) + { + // When Save button from toolbar is pressed + SessionManager.saveHistorySession(this.selectedHistorySession); + } + else + { + // Save from context menu + SessionHistory.getHistorySession(sessionIndex, function (sessionInfo) { + SessionManager.saveHistorySession(sessionInfo); + }); + } + } +} \ No newline at end of file diff --git a/data/scripts/components/session-filtering.js b/data/scripts/components/session-filtering.js index fff5ae9..bc0a1ff 100644 --- a/data/scripts/components/session-filtering.js +++ b/data/scripts/components/session-filtering.js @@ -1,139 +1,131 @@ -define(function(require, exports) { - 'use strict'; - - // ***************************************************************************** - // Custom Modules - - const { AppConfig } = require('../config'); - const { WindowEvents, GlobalEvents } = require('../utils/global-events'); - const { HTMLCreator } = require('../utils/dom'); - - // ***************************************************************************** - // API - - /* - * SessionFiltering - */ - - function SessionFiltering() - { - var DomElem = HTMLCreator(); - - // ------------------------------------------------------------------------ - // Create UI - - var panel = DomElem('div', {class: 'search-bar', content: 'false', match: 'true'}); - - var createSession = DomElem('div', {class: 'create-session' }); - panel.appendChild(createSession); - - var close = DomElem('div', {class: 'close' }); - panel.appendChild(close); - - var searchInput = DomElem('input', {type: 'text', class: 'search', placeholder:'Search'}); - panel.appendChild(searchInput); - - // ------------------------------------------------------------------------ - // Methods - - var timeOut = null; - var prevValue = ''; - - var dismiss = function dismiss() - { - clearTimeout(timeOut); - if (searchInput.value.length) { - prevValue = ''; - searchInput.value = prevValue; - WindowEvents.emit(document, 'FilterSessions', searchInput.value); - } - - searchInput.blur(); - onBlur(); - - panel.setAttribute('content', searchInput.value != ''); - WindowEvents.emit(document, 'SetUIState', {'search-box-content': searchInput.value != ''}); - }; - - var onKeyDown = function() - { - if (prevValue == searchInput.value) - return; - - prevValue = searchInput.value; - panel.setAttribute('match', 'false'); - panel.setAttribute('content', searchInput.value != ''); - WindowEvents.emit(document, 'SetUIState', {'search-box-content': searchInput.value != ''}); - clearTimeout(timeOut); - timeOut = setTimeout(function () { - WindowEvents.emit(document, 'FilterSessions', searchInput.value); - }, 200); - }; - - var onFocus = function onFocus() { - panel.setAttribute('active', true); - document.addEventListener('keyup', onKeyDown); - }; - - var onBlur = function onBlur() { - if (searchInput.value == '') { - panel.setAttribute('active', false); - } - document.removeEventListener('keyup', onKeyDown); - }; - - // ------------------------------------------------------------------------ - // Events - - // Tooltip events - createSession.addEventListener('mouseover', function(e) { - WindowEvents.emit(document, 'ShowTooltip', { - node: e.target, - message: 'Create new session', - }); +'use strict'; + +// ***************************************************************************** +// Custom Modules + +import { AppConfig } from '../config'; +import { WindowEvents, GlobalEvents } from '../utils/global-events'; +import { HTMLCreator } from '../utils/dom'; + +// ***************************************************************************** +// API + +/* +* SessionFiltering +*/ + +export function SessionFiltering() { + var DomElem = HTMLCreator(); + + // ------------------------------------------------------------------------ + // Create UI + + var panel = DomElem('div', { class: 'search-bar', content: 'false', match: 'true' }); + + var createSession = DomElem('div', { class: 'create-session' }); + panel.appendChild(createSession); + + var close = DomElem('div', { class: 'close' }); + panel.appendChild(close); + + var searchInput = DomElem('input', { type: 'text', class: 'search', placeholder: 'Search' }); + panel.appendChild(searchInput); + + // ------------------------------------------------------------------------ + // Methods + + var timeOut = null; + var prevValue = ''; + + var dismiss = function dismiss() { + clearTimeout(timeOut); + if (searchInput.value.length) { + prevValue = ''; + searchInput.value = prevValue; + WindowEvents.emit(document, 'FilterSessions', searchInput.value); + } + + searchInput.blur(); + onBlur(); + + panel.setAttribute('content', searchInput.value != ''); + WindowEvents.emit(document, 'SetUIState', { 'search-box-content': searchInput.value != '' }); + }; + + var onKeyDown = function () { + if (prevValue == searchInput.value) + return; + + prevValue = searchInput.value; + panel.setAttribute('match', 'false'); + panel.setAttribute('content', searchInput.value != ''); + WindowEvents.emit(document, 'SetUIState', { 'search-box-content': searchInput.value != '' }); + clearTimeout(timeOut); + timeOut = setTimeout(function () { + WindowEvents.emit(document, 'FilterSessions', searchInput.value); + }, 200); + }; + + var onFocus = function onFocus() { + panel.setAttribute('active', true); + document.addEventListener('keyup', onKeyDown); + }; + + var onBlur = function onBlur() { + if (searchInput.value == '') { + panel.setAttribute('active', false); + } + document.removeEventListener('keyup', onKeyDown); + }; + + // ------------------------------------------------------------------------ + // Events + + // Tooltip events + createSession.addEventListener('mouseover', function (e) { + WindowEvents.emit(document, 'ShowTooltip', { + node: e.target, + message: 'Create new session', }); + }); - createSession.addEventListener('mouseleave', function() { - WindowEvents.emit(document, 'HideTooltip'); - }); + createSession.addEventListener('mouseleave', function () { + WindowEvents.emit(document, 'HideTooltip'); + }); - createSession.addEventListener('click', function() { - GlobalEvents.emit('CreateNewSession', searchInput.value); - WindowEvents.emit(document, 'HideTooltip'); - }); - - close.addEventListener('click', dismiss); + createSession.addEventListener('click', function () { + GlobalEvents.emit('CreateNewSession', searchInput.value); + WindowEvents.emit(document, 'HideTooltip'); + }); - searchInput.addEventListener('focus', onFocus); - searchInput.addEventListener('blur', onBlur); + close.addEventListener('click', dismiss); - WindowEvents.on(document, 'FilterSessionsExactMatch', function(isMatch) { - panel.setAttribute('match', isMatch); - }); + searchInput.addEventListener('focus', onFocus); + searchInput.addEventListener('blur', onBlur); - WindowEvents.on(document, 'FocusSessionFilter', function () { - searchInput.focus(); - }); + WindowEvents.on(document, 'FilterSessionsExactMatch', function (isMatch) { + panel.setAttribute('match', isMatch); + }); - // ------------------------------------------------------------------------ - // Init state + WindowEvents.on(document, 'FocusSessionFilter', function () { + searchInput.focus(); + }); - var settingsKey = 'session.active.filter'; + // ------------------------------------------------------------------------ + // Init state - AppConfig.onChange(settingsKey, function(value) { - if (value && value.length > 0) { - searchInput.value = value; - panel.setAttribute('active', true); - onKeyDown(); - } - }); + var settingsKey = 'session.active.filter'; - // ------------------------------------------------------------------------ - // Public properties + AppConfig.onChange(settingsKey, function (value) { + if (value && value.length > 0) { + searchInput.value = value; + panel.setAttribute('active', true); + onKeyDown(); + } + }); - this.DOMRoot = panel; - } + // ------------------------------------------------------------------------ + // Public properties - // Public API - exports.SessionFiltering = SessionFiltering; -}); \ No newline at end of file + this.DOMRoot = panel; +} \ No newline at end of file diff --git a/data/scripts/components/session-folder-events.js b/data/scripts/components/session-folder-events.js index 5f2ca8a..1d369d9 100644 --- a/data/scripts/components/session-folder-events.js +++ b/data/scripts/components/session-folder-events.js @@ -1,290 +1,293 @@ -define(function(require, exports) { - 'use strict'; - - // ***************************************************************************** - // Custom Modules - - // App - const { AppConfig } = require('../config'); - const { SessionSyncModel } = require('./session-sync-model'); - - // Utils - const { WindowEvents } = require('../utils/global-events'); - const { AutoScroll } = require('../utils/auto-scroll'); - const { DragContext } = require('../utils/drag-context'); - - // ***************************************************************************** - // API - - // ---------------------------------------------------------------------------- - // Session Folder Events - delegated - - var SessionFolderEvents = function SessionFolderEvents(container) - { - var startDragEvent = null; - var trackingTelemetry; - var offsetRect = {}; - var bookmarkContext = null; - var SyncModel = SessionSyncModel.getModel(document); - - function getAttribute(target, attribute) { - try { - return target.getAttribute(attribute); - } - catch (err) { - console.log(err); - return null; - } - } - - function getBookmarkID(e) { - return getAttribute(e.target, 'sessionID'); - } - - // -------------------------------------------------------------- - // Drag Events - - var trackDragging = function _mouseMove(e) - { - var delta = Math.abs(startDragEvent.clientX - e.clientX) > 5 || Math.abs(startDragEvent.clientY - e.clientY) > 5; - if (delta) { - DragContext.setContext(bookmarkContext); - trackingTelemetry = new Date(); - - offsetRect = container.parentElement.parentElement.getBoundingClientRect(); - offsetRect.relativeY = offsetRect.y - container.parentElement.offsetTop; - offsetRect.relativeX = offsetRect.x - container.parentElement.offsetLeft; - - bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.relativeY + 'px'; - bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.relativeX + 'px'; - bookmarkContext.DOMRoot.setAttribute('dragging', ''); - - // Events - window.addEventListener('mousemove', mouseDragging); - window.removeEventListener('mousemove', trackDragging); - } - }; - - var cancelDragging = function cancelDragging(e) - { - AutoScroll.stop(); - - bookmarkContext.DOMRoot.setAttribute('dragging', 'drop'); - bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.y + container.scrollTop + 'px'; - bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.x + 'px'; - - setTimeout(function() { - bookmarkContext.DOMRoot.removeAttribute('dragging'); - bookmarkContext.restorePosition(); - }, 50); - window.removeEventListener('mousemove', mouseDragging); - - DragContext.clearContext(); - }; - - var mouseDragging = function mouseMove(e) - { - if (this != window) - return; - var deltaTime = new Date() - trackingTelemetry; - if (deltaTime < 16) { - return; - } - - var tryScroll = e.clientX > offsetRect.left && e.clientX < offsetRect.right; - - if (tryScroll) - { - if (e.clientY < offsetRect.top) { - AutoScroll.scroll(container, (e.clientY - offsetRect.top)); - } - else if (e.clientY > offsetRect.bottom) { - AutoScroll.scroll(container, (e.clientY - offsetRect.bottom)); - } else { - AutoScroll.stop(); - } - } else { - AutoScroll.stop(); - } - - bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.relativeY + 'px'; - bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.relativeX + 'px'; - trackingTelemetry = new Date(); - }; - - var mouseUp = function _mouseUp(e) - { - // Test if changing - if (e.target.className == 'folder') - { - var sessionID = getBookmarkID(e); - - // Preview the content if a click event was registered (mouse-up on the same DOM node) - if (bookmarkContext.bookmarkID == sessionID) - { - if (!DragContext.hasContext()) { - WindowEvents.emit(document, 'SelectSyncSession', bookmarkContext); - } - } - else - { - if (AppConfig.get('session.sorting') === 'position-asc') - { - SessionSyncModel.moveBookmark(bookmarkContext.bookmarkID, sessionID); - } - else - { - WindowEvents.emit(document, 'Notification', { - message: 'Rearage function available only when items are sorted ascending by position', - timeout: 3000, - }); - } - } - } - - if (DragContext.hasContext()) - { - cancelDragging(e); - } - else - { - window.removeEventListener('mousemove', trackDragging); - } - - window.removeEventListener('mouseup', mouseUp); - }; - - var folderMouseDown = function _folderMouseDown(e) - { - if (DragContext.hasContext()) { - cancelDragging(e); - return; - } - - var sessionID = getBookmarkID(e); - bookmarkContext = SyncModel.getBookmark(sessionID); - - if (e.button == 0) { - startDragEvent = e; - window.addEventListener('mousemove', trackDragging); - window.addEventListener('mouseup', mouseUp); - return; - } - }; - - var folderContextMenu = function folderContextMenu(e) - { - if (bookmarkContext) - { - var contextMenu = undefined; - switch (bookmarkContext.bookmark.type) - { - case 'folder': - contextMenu = 'SessionContextMenu'; - break; - - case 'bookmark': - contextMenu = 'SessionContextBookmarkMenu'; - break; - - case 'separator': - contextMenu = 'SessionContextSeparatorMenu'; - break; - - default: - return; - } - - WindowEvents.emit(document, contextMenu + '-Open', { - context: bookmarkContext.bookmark.id, - event: e - }); - } - else { - WindowEvents.emit(document, 'SessionListMenu-Open', { event: e }); - } - }; - - var sessionDblClick = function sessionDblClick(e) - { - if (bookmarkContext) - { - switch (bookmarkContext.bookmark.type) - { - case 'folder': - WindowEvents.emit(document, 'SessionContextMenu-EditSession', bookmarkContext.bookmarkID); - break; - - case 'bookmark': - WindowEvents.emit(document, 'BookmarkCtxMenu-EditBookmark', bookmarkContext.bookmarkID); - break; - } - } - }; - - // ------------------------------------------------------------------------ - // Init Code Events - - container.addEventListener('dblclick', sessionDblClick); - container.addEventListener('mousedown', folderMouseDown); - container.addEventListener('contextmenu', folderContextMenu); - }; - - // ---------------------------------------------------------------------------- - // History Session Folder Events - delegated - - var SessionHistoryEvents = function SessionHistoryEvents(container) - { - var historyContext = null; - - function getAttribute(target, attribute) { - try { - return target.getAttribute(attribute); - } - catch (err) { - console.log(err); - return null; - } - } - - var onMouseDown = function onMouseDown(e) - { - if (e.button != 2) - return; - - var target = e.target; - if (target.className == 'history-node') { - WindowEvents.emit(document, 'HistorySessionCtxMenu-Open', { - context: e.target, - event: e - }); - } - else - { - WindowEvents.emit(document, 'HistoryListCtxMenu-Open', {event: e }); - } - }; - - var onClick = function onClick(e) - { - if (e.target.className === 'history-node') - { - // WindowEvents.emit(document, 'HistoryListCtxMenu-Open', {event: e }); - // this.selectHistorySession(e.target); - } - }; - - // ------------------------------------------------------------------------ - // Init Code Events - - container.addEventListener('click', onClick); - container.addEventListener('mousedown', onMouseDown); - }; - - // ***************************************************************************** - // Public API - - exports.SessionFolderEvents = SessionFolderEvents; - exports.SessionHistoryEvents = SessionHistoryEvents; - -}); \ No newline at end of file +'use strict'; + +// ***************************************************************************** +// Custom Modules + +// App +import { AppConfig } from '../config'; +import { SessionSyncModel } from './session-sync-model'; + +// Utils +import { WindowEvents } from '../utils/global-events'; +import { AutoScroll } from '../utils/auto-scroll'; +import { DragContext } from '../utils/drag-context'; + +// ***************************************************************************** +// API + +// ---------------------------------------------------------------------------- +// Session Folder Events - delegated + +export function SessionFolderEvents(container) +{ + var startDragEvent = null; + var trackingTelemetry; + var offsetRect = {}; + var bookmarkContext = null; + var SyncModel = SessionSyncModel.getModel(typeof document !== 'undefined' ? document : null); + + function getAttribute(target, attribute) { + try { + return target.getAttribute(attribute); + } + catch (err) { + console.log(err); + return null; + } + } + + function getBookmarkID(e) { + return getAttribute(e.target, 'sessionID'); + } + + // -------------------------------------------------------------- + // Drag Events + + var trackDragging = function _mouseMove(e) + { + var delta = Math.abs(startDragEvent.clientX - e.clientX) > 5 || Math.abs(startDragEvent.clientY - e.clientY) > 5; + if (delta) { + DragContext.setContext(bookmarkContext); + trackingTelemetry = new Date(); + + offsetRect = container.parentElement.parentElement.getBoundingClientRect(); + offsetRect.relativeY = offsetRect.y - container.parentElement.offsetTop; + offsetRect.relativeX = offsetRect.x - container.parentElement.offsetLeft; + + bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.relativeY + 'px'; + bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.relativeX + 'px'; + bookmarkContext.DOMRoot.setAttribute('dragging', ''); + + // Events + window.addEventListener('mousemove', mouseDragging); + window.removeEventListener('mousemove', trackDragging); + } + }; + + var cancelDragging = function cancelDragging(e) + { + AutoScroll.stop(); + + bookmarkContext.DOMRoot.setAttribute('dragging', 'drop'); + bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.y + container.scrollTop + 'px'; + bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.x + 'px'; + + setTimeout(function() { + bookmarkContext.DOMRoot.removeAttribute('dragging'); + bookmarkContext.restorePosition(); + }, 50); + window.removeEventListener('mousemove', mouseDragging); + + DragContext.clearContext(); + }; + + var mouseDragging = function mouseMove(e) + { + if (this != window) + return; + var deltaTime = new Date() - trackingTelemetry; + if (deltaTime < 16) { + return; + } + + var tryScroll = e.clientX > offsetRect.left && e.clientX < offsetRect.right; + + if (tryScroll) + { + if (e.clientY < offsetRect.top) { + AutoScroll.scroll(container, (e.clientY - offsetRect.top)); + } + else if (e.clientY > offsetRect.bottom) { + AutoScroll.scroll(container, (e.clientY - offsetRect.bottom)); + } else { + AutoScroll.stop(); + } + } else { + AutoScroll.stop(); + } + + bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.relativeY + 'px'; + bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.relativeX + 'px'; + trackingTelemetry = new Date(); + }; + + var mouseUp = function _mouseUp(e) + { + // Test if changing + if (e.target.className == 'folder') + { + var sessionID = getBookmarkID(e); + + // Preview the content if a click event was registered (mouse-up on the same DOM node) + if (bookmarkContext.bookmarkID == sessionID) + { + if (!DragContext.hasContext()) { + if (typeof document !== 'undefined') { + WindowEvents.emit(document, 'SelectSyncSession', bookmarkContext); + } + } + } + else + { + if (AppConfig.get('session.sorting') === 'position-asc') + { + SessionSyncModel.moveBookmark(bookmarkContext.bookmarkID, sessionID); + } + else + { + if (typeof document !== 'undefined') { + WindowEvents.emit(document, 'Notification', { + message: 'Rearage function available only when items are sorted ascending by position', + timeout: 3000, + }); + } + } + } + } + + if (DragContext.hasContext()) + { + cancelDragging(e); + } + else + { + window.removeEventListener('mousemove', trackDragging); + } + + window.removeEventListener('mouseup', mouseUp); + }; + + var folderMouseDown = function _folderMouseDown(e) + { + if (DragContext.hasContext()) { + cancelDragging(e); + return; + } + + var sessionID = getBookmarkID(e); + bookmarkContext = SyncModel.getBookmark(sessionID); + + if (e.button == 0) { + startDragEvent = e; + window.addEventListener('mousemove', trackDragging); + window.addEventListener('mouseup', mouseUp); + return; + } + }; + + var folderContextMenu = function folderContextMenu(e) + { + if (typeof document === 'undefined') return; + + if (bookmarkContext) + { + var contextMenu = undefined; + switch (bookmarkContext.bookmark.type) + { + case 'folder': + contextMenu = 'SessionContextMenu'; + break; + + case 'bookmark': + contextMenu = 'SessionContextBookmarkMenu'; + break; + + case 'separator': + contextMenu = 'SessionContextSeparatorMenu'; + break; + + default: + return; + } + + WindowEvents.emit(document, contextMenu + '-Open', { + context: bookmarkContext.bookmark.id, + event: e + }); + } + else { + WindowEvents.emit(document, 'SessionListMenu-Open', { event: e }); + } + }; + + var sessionDblClick = function sessionDblClick(e) + { + if (typeof document === 'undefined') return; + + if (bookmarkContext) + { + switch (bookmarkContext.bookmark.type) + { + case 'folder': + WindowEvents.emit(document, 'SessionContextMenu-EditSession', bookmarkContext.bookmarkID); + break; + + case 'bookmark': + WindowEvents.emit(document, 'BookmarkCtxMenu-EditBookmark', bookmarkContext.bookmarkID); + break; + } + } + }; + + // ------------------------------------------------------------------------ + // Init Code Events + + container.addEventListener('dblclick', sessionDblClick); + container.addEventListener('mousedown', folderMouseDown); + container.addEventListener('contextmenu', folderContextMenu); +} + +// ---------------------------------------------------------------------------- +// History Session Folder Events - delegated + +export function SessionHistoryEvents(container) +{ + var historyContext = null; + + function getAttribute(target, attribute) { + try { + return target.getAttribute(attribute); + } + catch (err) { + console.log(err); + return null; + } + } + + var onMouseDown = function onMouseDown(e) + { + if (e.button != 2) + return; + + var target = e.target; + if (target.className == 'history-node') { + if (typeof document !== 'undefined') { + WindowEvents.emit(document, 'HistorySessionCtxMenu-Open', { + context: e.target, + event: e + }); + } + } + else + { + if (typeof document !== 'undefined') { + WindowEvents.emit(document, 'HistoryListCtxMenu-Open', {event: e }); + } + } + }; + + var onClick = function onClick(e) + { + if (e.target.className === 'history-node') + { + // WindowEvents.emit(document, 'HistoryListCtxMenu-Open', {event: e }); + // this.selectHistorySession(e.target); + } + }; + + // ------------------------------------------------------------------------ + // Init Code Events + + container.addEventListener('click', onClick); + container.addEventListener('mousedown', onMouseDown); +} \ No newline at end of file diff --git a/data/scripts/components/session-folder.js b/data/scripts/components/session-folder.js index 53b2fbc..712a289 100644 --- a/data/scripts/components/session-folder.js +++ b/data/scripts/components/session-folder.js @@ -1,59 +1,50 @@ -define(function(require, exports) { - 'use strict'; - - // ***************************************************************************** - // Custom Modules - - // Utils - const { HTMLCreator } = require('../utils/dom'); - - // ***************************************************************************** - // API - - /* - * Session Bookmark - * Used for interacting with a Firefox bookmark item - */ - function SessionFolder(bookmark) - { - var DomElem = HTMLCreator(); - var folder = DomElem('div', {class: 'folder', type: bookmark.type}); - folder.setAttribute('sessionID', bookmark.id); +'use strict'; - this.DOMRoot = folder; - this.updateFrom(bookmark); +// ***************************************************************************** +// Custom Modules + +// Utils +import { HTMLCreator } from '../utils/dom'; - // ------------------------------------------------------------------------ - // Public data +// ***************************************************************************** +// API +/* +* Session Folder +* Used for interacting with a Firefox bookmark item +*/ +export class SessionFolder { + static folderOffset = 2.7; // em; + + constructor(bookmark) { + const DomElem = HTMLCreator(); + const folder = DomElem('div', { class: 'folder', type: bookmark.type }); + folder.setAttribute('sessionID', bookmark.id); + + this.DOMRoot = folder; this.visible = true; this.bookmarkID = bookmark.id; + this.updateFrom(bookmark); } - SessionFolder.prototype.folderOffset = 2.7; // em; - - SessionFolder.prototype.updateFrom = function updateFrom(bookmark) - { + updateFrom(bookmark) { this.bookmark = bookmark; this.bookmark.title_lowercase = this.bookmark.title.toLowerCase(); this.setTitle(bookmark.title); - }; + } - SessionFolder.prototype.isVisible = function isVisible() - { + isVisible() { return this.visible; - }; + } - SessionFolder.prototype.setVisible = function setVisible() - { + setVisible() { this.visible = true; this.DOMRoot.removeAttribute('filter'); - }; + } - SessionFolder.prototype.match = function match(expression) - { - var name = this.bookmark.title_lowercase; - if (name.indexOf(expression) != -1) { + match(expression) { + const name = this.bookmark.title_lowercase; + if (name.indexOf(expression) !== -1) { this.visible = true; this.DOMRoot.removeAttribute('filter'); return true; @@ -62,91 +53,58 @@ define(function(require, exports) { this.visible = false; this.DOMRoot.setAttribute('filter', ''); return false; - }; + } - SessionFolder.prototype.restorePosition = function restorePosition() - { + restorePosition() { this.DOMRoot.style.left = '0px'; - this.DOMRoot.style.top = this.position * this.folderOffset + 'em'; - }; + this.DOMRoot.style.top = this.position * SessionFolder.folderOffset + 'em'; + } - SessionFolder.prototype.setVirtualPosition = function setVirtualPosition(position) - { - if (this.position == position) - return; + setVirtualPosition(position) { + if (this.position === position) return; this.position = position; - this.DOMRoot.style.top = (position * this.folderOffset).toFixed(2) + 'em'; - }; + this.DOMRoot.style.top = (position * SessionFolder.folderOffset).toFixed(2) + 'em'; + } - SessionFolder.prototype.setTitle = function setTitle(title) - { + setTitle(title) { this.DOMRoot.textContent = title; - }; + } - SessionFolder.prototype.remove = function remove() - { - var box = this.DOMRoot; - if (box.parentElement) + remove() { + const box = this.DOMRoot; + if (box.parentElement) { box.parentElement.removeChild(box); - }; - - var SortMethods = { - - 'name-asc' : function sortByNameAscending(a, b) { - return a.bookmark.title_lowercase > b.bookmark.title_lowercase; - }, - - 'name-desc' : function sortByNameDescending(a, b) { - return a.bookmark.title_lowercase < b.bookmark.title_lowercase; - }, - - 'position-asc' : function sortByPositionAscending(a, b) { - return a.bookmark.index > b.bookmark.index; - }, - - 'position-desc' : function sortByPositionDescending(a, b) { - return a.bookmark.index < b.bookmark.index; - }, - - 'date-asc' : function sortByDateAscending(a, b) { - return a.bookmark.dateAdded > b.bookmark.dateAdded; - }, - - 'date-desc' : function sortByDateDescending(a, b) { - return a.bookmark.dateAdded < b.bookmark.dateAdded; } - }; - - - /* - * SessionHistoryFolder - * Used as separator for different windows - */ - - function SessionHistoryFolder(sessionInfo, index) - { - var DomElem = HTMLCreator(); - var folder = DomElem('div', { class: 'history-node' }); + } +} + +export const SessionFolderSortBy = { + 'name-asc': (a, b) => a.bookmark.title_lowercase > b.bookmark.title_lowercase, + 'name-desc': (a, b) => a.bookmark.title_lowercase < b.bookmark.title_lowercase, + 'position-asc': (a, b) => a.bookmark.index > b.bookmark.index, + 'position-desc': (a, b) => a.bookmark.index < b.bookmark.index, + 'date-asc': (a, b) => a.bookmark.dateAdded > b.bookmark.dateAdded, + 'date-desc': (a, b) => a.bookmark.dateAdded < b.bookmark.dateAdded +}; + +/* +* SessionHistoryFolder +* Used as separator for different windows +*/ +export class SessionHistoryFolder { + constructor(sessionInfo, index) { + const DomElem = HTMLCreator(); + const folder = DomElem('div', { class: 'history-node' }); folder.textContent = new Date(sessionInfo.lastSave).toLocaleString(); folder.setAttribute('index', index); - // Public data - this.DOMRoot = folder; this.sessionInfo = sessionInfo; } - SessionHistoryFolder.prototype.setTitle = function setTitle(title) - { + setTitle(title) { this.DOMRoot.textContent = title; - }; - - // ***************************************************************************** - // Public API - exports.SessionFolder = SessionFolder; - exports.SessionFolderSortBy = SortMethods; - exports.SessionHistoryFolder = SessionHistoryFolder; - -}); \ No newline at end of file + } +} diff --git a/data/scripts/components/session-header-bar.js b/data/scripts/components/session-header-bar.js index cd968bc..8736aab 100644 --- a/data/scripts/components/session-header-bar.js +++ b/data/scripts/components/session-header-bar.js @@ -1,246 +1,274 @@ -define(function(require, exports) { - 'use strict'; +'use strict'; - // ------------------------------------------------------------------------ - // Modules +// ------------------------------------------------------------------------ +// Modules - const { AppConfig } = require('../config'); - const { SessionSyncModel } = require ('./session-sync-model'); +import { AppConfig } from '../config'; +import { SessionSyncModel } from './session-sync-model'; - // Utils - const JSUtils = require('../utils/general'); - const { HTMLCreator } = require('../utils/dom'); - const { WindowEvents } = require('../utils/global-events'); +// Utils +import * as JSUtils from '../utils/general'; +import { HTMLCreator } from '../utils/dom'; +import { WindowEvents } from '../utils/global-events'; - // ------------------------------------------------------------------------ - // API +// ------------------------------------------------------------------------ +// API - function SessionHeaderBar() - { - var DomElem = HTMLCreator(); +export function SessionHeaderBar() +{ + var DomElem = HTMLCreator(); - // ------------------------------------------------------------------------ - // Create UI + // ------------------------------------------------------------------------ + // Create UI - var headerBar = DomElem('div', {class: 'header-bar'}); - headerBar.style.fontSize = AppConfig.get('style.scale.header') + 'px'; + var headerBar = DomElem('div', {class: 'header-bar'}); + headerBar.style.fontSize = AppConfig.get('style.scale.header') + 'px'; - // Display information about the selected session - var description = DomElem('div', {class: 'description'}); - headerBar.appendChild(description); + // Display information about the selected session + var description = DomElem('div', {class: 'description'}); + headerBar.appendChild(description); - // Active session button - var activeSessionBtn = DomElem('div', {class: 'active-session-btn', tooltip: 'Active session'}); - headerBar.appendChild(activeSessionBtn); + // Active session button + var activeSessionBtn = DomElem('div', {class: 'active-session-btn', tooltip: 'Active session'}); + headerBar.appendChild(activeSessionBtn); - // Header Menu - var headerMenu = CreateHeaderMenu(); - headerBar.appendChild(headerMenu); + // Header Menu + var headerMenu = CreateHeaderMenu(); + headerBar.appendChild(headerMenu); - // ------------------------------------------------------------------------ - // Events + // ------------------------------------------------------------------------ + // Events - AppConfig.onChange('style.scale.header', function(value) { - headerBar.style.fontSize = value + 'px'; - }); + AppConfig.onChange('style.scale.header', function(value) { + headerBar.style.fontSize = value + 'px'; + }); - activeSessionBtn.addEventListener('click', function() { + activeSessionBtn.addEventListener('click', function() { + if (typeof document !== 'undefined') { WindowEvents.emit(document, 'ShowCurrentSession'); - }); + } + }); + if (typeof document !== 'undefined') { WindowEvents.on(document, 'SetSessionDescription', function(value) { description.textContent = value; }); + } - // Tooltip events - headerBar.addEventListener('mouseover', function(e) { - if (e.target.hasAttribute('tooltip')) { + // Tooltip events + headerBar.addEventListener('mouseover', function(e) { + if (e.target.hasAttribute('tooltip')) { + if (typeof document !== 'undefined') { WindowEvents.emit(document, 'ShowTooltip', { node: e.target, message: e.target.getAttribute('tooltip') }); - } else { + } + } else { + if (typeof document !== 'undefined') { WindowEvents.emit(document, 'HideTooltip'); } - }); + } + }); - headerBar.addEventListener('mouseleave', function() { + headerBar.addEventListener('mouseleave', function() { + if (typeof document !== 'undefined') { WindowEvents.emit(document, 'HideTooltip'); - }); + } + }); - // ------------------------------------------------------------------------ - // Public properties + // ------------------------------------------------------------------------ + // Public properties - this.DOMRoot = headerBar; - } + this.DOMRoot = headerBar; +} - var CreateHeaderMenu = function CreateHeaderMenu() - { - var DomElem = HTMLCreator(); +var CreateHeaderMenu = function CreateHeaderMenu() +{ + var DomElem = HTMLCreator(); - // ------------------------------------------------------------------------ - // Create UI + // ------------------------------------------------------------------------ + // Create UI - var headerMenu = DomElem('div', {class: 'header-menu'}); - headerMenu.setAttribute('active', 'false'); + var headerMenu = DomElem('div', {class: 'header-menu'}); + headerMenu.setAttribute('active', 'false'); - var menuArea = DomElem('div', {class: 'menu-area'}); - headerMenu.appendChild(menuArea); + var menuArea = DomElem('div', {class: 'menu-area'}); + headerMenu.appendChild(menuArea); - var menuRow1 = DomElem('div', {class: 'menu-row'}); - var menuRow2 = DomElem('div', {class: 'menu-row'}); - var menuRow3 = DomElem('div', {class: 'menu-row'}); - menuArea.appendChild(menuRow1); - menuArea.appendChild(menuRow2); - menuArea.appendChild(menuRow3); + var menuRow1 = DomElem('div', {class: 'menu-row'}); + var menuRow2 = DomElem('div', {class: 'menu-row'}); + var menuRow3 = DomElem('div', {class: 'menu-row'}); + menuArea.appendChild(menuRow1); + menuArea.appendChild(menuRow2); + menuArea.appendChild(menuRow3); - // ------------------------------------------------------------------------ - // Methods + // ------------------------------------------------------------------------ + // Methods - function MenuButton(options) { + function MenuButton(options) { - var button = DomElem('div', {class: 'menu-button'}); + var button = DomElem('div', {class: 'menu-button'}); - var icon = DomElem('div', {class: 'icon'}); - var title = DomElem('div', {class: 'title'}); + var icon = DomElem('div', {class: 'icon'}); + var title = DomElem('div', {class: 'title'}); - title.textContent = options.title; + title.textContent = options.title; - var url = 'url("images/' + options.icon + '")'; - icon.style.backgroundImage = url; + var url = 'url("images/' + options.icon + '")'; + icon.style.backgroundImage = url; - // Events - var callback = JSUtils.getValidFunction(options.callback); - button.addEventListener('click', callback); + // Events + var callback = (typeof JSUtils.getValidFunction === 'function') ? JSUtils.getValidFunction(options.callback) : options.callback; + button.addEventListener('click', callback); - button.appendChild(icon); - button.appendChild(title); - return button; - } + button.appendChild(icon); + button.appendChild(title); + return button; + } - var openConfig = MenuButton({ - title: 'Options', - icon: 'icons/gear.png', - callback: function () { + var openConfig = MenuButton({ + title: 'Options', + icon: 'icons/gear.png', + callback: function () { + if (typeof document !== 'undefined') { var model = SessionSyncModel.getModel(document); var state = model.state['config'] == undefined ? 'on' : undefined; WindowEvents.emit(document, 'SetUIState', { config : state}); } - }); - - var openTutorial = MenuButton({ - title: 'Tutorial', - icon: 'icons/brightness.png', - callback: function () { - browser.runtime.sendMessage({event: 'session-sync-tutorial'}); + } + }); + + var openTutorial = MenuButton({ + title: 'Tutorial', + icon: 'icons/brightness.png', + callback: function () { + if (typeof browser !== 'undefined' && browser.runtime) { + browser.runtime.sendMessage({event: 'session_sync_tutorial'}); } - }); - - var leaveFeedback = MenuButton({ - title: 'Feedback', - icon: 'icons/mail.png', - callback: function () { - browser.runtime.sendMessage({event: 'session-sync-leave-feedback'}); + } + }); + + var leaveFeedback = MenuButton({ + title: 'Feedback', + icon: 'icons/mail.png', + callback: function () { + if (typeof browser !== 'undefined' && browser.runtime) { + browser.runtime.sendMessage({event: 'session_sync_leave_feedback'}); } - }); + } + }); - var exportImport = MenuButton({ - title: 'Export/Import', - icon: 'icons/booklet.png', - callback: function () { + var exportImport = MenuButton({ + title: 'Export/Import', + icon: 'icons/booklet.png', + callback: function () { + if (typeof browser !== 'undefined' && browser.tabs) { browser.tabs.create({ url: 'home/home.html#export-import', active: true, }); } - }); - - var buttonDetachInTab = MenuButton({ - title: 'Tab View', - icon: 'icons/document.png', - callback: function () { - browser.runtime.sendMessage({event: 'session-sync-detach-tab'}); + } + }); + + var buttonDetachInTab = MenuButton({ + title: 'Tab View', + icon: 'icons/document.png', + callback: function () { + if (typeof browser !== 'undefined' && browser.runtime) { + browser.runtime.sendMessage({event: 'session_sync_detach_tab'}); } - }); - - var buttonDetachInWindow = MenuButton({ - title: 'Window View', - icon: 'icons/browser.png', - callback: function () { - browser.runtime.sendMessage({event: 'session-sync-detach-window'}); + } + }); + + var buttonDetachInWindow = MenuButton({ + title: 'Window View', + icon: 'icons/browser.png', + callback: function () { + if (typeof browser !== 'undefined' && browser.runtime) { + browser.runtime.sendMessage({event: 'session_sync_detach_window'}); } - }); - - var githubPage = MenuButton({ - title: 'Dev page', - icon: 'github-logo.png', - callback: function () { - browser.runtime.sendMessage({event: 'session-sync-open-github'}); + } + }); + + var githubPage = MenuButton({ + title: 'Dev page', + icon: 'github-logo.png', + callback: function () { + if (typeof browser !== 'undefined' && browser.runtime) { + browser.runtime.sendMessage({event: 'session_sync_open_github'}); } - }); + } + }); - menuRow1.appendChild(openConfig); - menuRow2.appendChild(openTutorial); - menuRow2.appendChild(leaveFeedback); - menuRow2.appendChild(githubPage); - menuRow3.appendChild(exportImport); - menuRow3.appendChild(buttonDetachInTab); - menuRow3.appendChild(buttonDetachInWindow); - - // ------------------------------------------------------------------------ - // Dev actions - - var DebugMode = { - init: AppConfig.devMode(), - manualTrigger: 0, - triggerDate: new Date(), - action : function () { - if (this.init == false) - { - if (new Date() - this.triggerDate < 5000) { - this.manualTrigger++; - if (this.manualTrigger > 10) { - this.init = true; - initDevMode(); - } - } - else { - this.manualTrigger = 0; + menuRow1.appendChild(openConfig); + menuRow2.appendChild(openTutorial); + menuRow2.appendChild(leaveFeedback); + menuRow2.appendChild(githubPage); + menuRow3.appendChild(exportImport); + menuRow3.appendChild(buttonDetachInTab); + menuRow3.appendChild(buttonDetachInWindow); + + // ------------------------------------------------------------------------ + // Dev actions + + var DebugMode = { + init: AppConfig.devMode(), + manualTrigger: 0, + triggerDate: new Date(), + action : function () { + if (this.init == false) + { + if (new Date() - this.triggerDate < 5000) { + this.manualTrigger++; + if (this.manualTrigger > 10) { + this.init = true; + initDevMode(); } } + else { + this.manualTrigger = 0; + } } - }; + } + }; - function initDevMode() - { - var devRow1 = DomElem('div', {class: 'menu-row'}); + function initDevMode() + { + var devRow1 = DomElem('div', {class: 'menu-row'}); - var activeTabInfo = MenuButton({ - title: 'Tab Info', - icon: 'icons/document.png', - callback: function () { + var activeTabInfo = MenuButton({ + title: 'Tab Info', + icon: 'icons/document.png', + callback: function () { + if (typeof browser !== 'undefined' && browser.tabs) { browser.tabs.query({active: true, windowId: browser.windows.WINDOW_ID_CURRENT}) .then(tabs => browser.tabs.get(tabs[0].id)) .then(tab => { console.log(tab); }); } - }); + } + }); - var localStorage = MenuButton({ - title: 'Local Storage', - icon: 'icons/bookshelf.png', - callback: function () { + var localStorage = MenuButton({ + title: 'Local Storage', + icon: 'icons/bookshelf.png', + callback: function () { + if (typeof browser !== 'undefined' && browser.storage) { browser.storage.local.get().then(function (data) { console.log(data); }); } - }); + } + }); - var clearFaviconCache = MenuButton({ - title: 'Clear Favicons', - icon: 'icons/x.png', - callback: function () { + var clearFaviconCache = MenuButton({ + title: 'Clear Favicons', + icon: 'icons/x.png', + callback: function () { + if (typeof browser !== 'undefined' && browser.storage) { browser.storage.local.get().then(function (data) { for (var key in data) { if (key.startsWith('@favIconUrl')) { @@ -249,105 +277,99 @@ define(function(require, exports) { } }); } - }); + } + }); - devRow1.appendChild(activeTabInfo); - devRow1.appendChild(localStorage); - devRow1.appendChild(clearFaviconCache); - menuArea.appendChild(devRow1); - } + devRow1.appendChild(activeTabInfo); + devRow1.appendChild(localStorage); + devRow1.appendChild(clearFaviconCache); + menuArea.appendChild(devRow1); + } - if (AppConfig.devMode()) - { - initDevMode(); - } + if (AppConfig.devMode()) + { + initDevMode(); + } - var Troubleshooting = (function Troubleshooting() - { - var isInit = false; + var Troubleshooting = (function Troubleshooting() + { + var isInit = false; - var fixLazySession = MenuButton({ - title: 'Fix session', - icon: 'icons/tools.png', - callback: function() { + var fixLazySession = MenuButton({ + title: 'Fix session', + icon: 'icons/tools.png', + callback: function() { + if (typeof document !== 'undefined') { WindowEvents.emit(document, 'MenuFixLazySession'); } - }); + } + }); - function init() + function init() + { + if (isInit == true) { - if (isInit == true) - { - return; - } - - isInit = true; - var devRow = DomElem('div', {class: 'menu-row'}); - devRow.appendChild(fixLazySession); - menuArea.appendChild(devRow); + return; } - return { - init: init - }; + isInit = true; + var devRow = DomElem('div', {class: 'menu-row'}); + devRow.appendChild(fixLazySession); + menuArea.appendChild(devRow); + } - })(); + return { + init: init + }; - // ------------------------------------------------------------------------ - // Events + })(); - var closeMenu = function closeMenu() { - headerMenu.setAttribute('active', '0'); - }; + // ------------------------------------------------------------------------ + // Events - var closeOnMiss = function closeOnMiss(e) - { - if (e && (e.target == headerMenu || e.target == menuArea)) { - return; - } - closeMenu(); - document.removeEventListener('click', closeOnMiss); - }; + var closeMenu = function closeMenu() { + headerMenu.setAttribute('active', '0'); + }; - headerMenu.addEventListener('click', function(e) { - if (e && e.target == headerMenu) - { - var state = headerMenu.getAttribute('active') | 0; - var newState = 1 - state; - headerMenu.setAttribute('active', newState); - if (newState == 1) { - document.addEventListener('click', closeOnMiss); - } + var closeOnMiss = function closeOnMiss(e) + { + if (e && (e.target == headerMenu || e.target == menuArea)) { + return; + } + closeMenu(); + if (typeof document !== 'undefined') { + document.removeEventListener('click', closeOnMiss); + } + }; - DebugMode.action(); + headerMenu.addEventListener('click', function(e) { + if (e && e.target == headerMenu) + { + var state = headerMenu.getAttribute('active') | 0; + var newState = 1 - state; + headerMenu.setAttribute('active', newState); + if (newState == 1 && typeof document !== 'undefined') { + document.addEventListener('click', closeOnMiss); } - }); + DebugMode.action(); + } + }); + + if (typeof document !== 'undefined') { WindowEvents.on(document, 'CloseUI', function() { closeMenu(); }); + } - AppConfig.onChange('troubleshooting', function(value) { - if (value == true) { - Troubleshooting.init(); - } - }); - - // ------------------------------------------------------------------------ - // Public - - return headerMenu; - }; - - // ------------------------------------------------------------------------ - // Events - - // ------------------------------------------------------------------------ - // Init + AppConfig.onChange('troubleshooting', function(value) { + if (value == true) { + Troubleshooting.init(); + } + }); // ------------------------------------------------------------------------ - // Module exports - - exports.SessionHeaderBar = SessionHeaderBar; + // Public -}); + return headerMenu; +}; diff --git a/data/scripts/components/session-history.js b/data/scripts/components/session-history.js index dccb759..e6601f6 100644 --- a/data/scripts/components/session-history.js +++ b/data/scripts/components/session-history.js @@ -1,117 +1,118 @@ -define(function(require, exports) { - 'use strict'; +'use strict'; - // ***************************************************************************** - // Custom Modules +// ***************************************************************************** +// Custom Modules - const { WindowEvents, GlobalEvents } = require('../utils/global-events'); +import { WindowEvents, GlobalEvents } from '../utils/global-events'; - // ***************************************************************************** - // API +// ***************************************************************************** +// API - var SessionHistory = (function SessionHistory() - { - var storageKey = { - config: 'history.config', - active: 'history.active', - list: 'history.sessions' - }; +export const SessionHistory = (function SessionHistory() { + var storageKey = { + config: 'history.config', + active: 'history.active', + list: 'history.sessions' + }; - var sessions = []; + var sessions = []; - var init = function init() - { + var init = function init() { + if (typeof browser !== 'undefined' && browser.storage) { browser.storage.local.get(storageKey.list).then(function (obj) { - if(obj[storageKey.list]) { + if (obj[storageKey.list]) { sessions = obj[storageKey.list]; } }); - }; + } + }; - var getFullHistory = function getFullHistory(callback) - { + var getFullHistory = function getFullHistory(callback) { + if (typeof browser !== 'undefined' && browser.storage) { browser.storage.local.get(storageKey.active).then(function (obj) { callback(sessions, obj[storageKey.active]); }); - }; - - var getHistorySession = function getHistorySession(index, callback) { - if (index >=0 && index < sessions.length) { - callback(sessions[index]); - } - else { - browser.storage.local.get(storageKey.active).then(function (obj) { - callback(obj[storageKey.active]); - }); - } - }; + } + }; + + var getHistorySession = function getHistorySession(index, callback) { + if (index >= 0 && index < sessions.length) { + callback(sessions[index]); + } + else if (typeof browser !== 'undefined' && browser.storage) { + browser.storage.local.get(storageKey.active).then(function (obj) { + callback(obj[storageKey.active]); + }); + } + }; - var getConfig = function getConfig(callback) - { + var getConfig = function getConfig(callback) { + if (typeof browser !== 'undefined' && browser.storage) { browser.storage.local.get(storageKey.config).then(function (obj) { callback(obj[storageKey.config]); }); - }; - - var updateConfig = function updateConfig(info) - { - browser.storage.local.set({ [storageKey.config] : info}); - }; - - var updateSessions = function updateSessions(sessions) - { - browser.storage.local.set({ [storageKey.list] : sessions}) - .then(function () { - WindowEvents.emit(document, 'ShowHistoryList'); - }); - }; - - var onStorageChanged = function onStorageChanged(object) - { - if (object[storageKey.list]) { - init(); - } - }; - - GlobalEvents.on('HistorySessionDelete', function(index) - { - if (index == -1) - { - browser.runtime.sendMessage({event: 'history.deleteActive'}); - } - else - { - getFullHistory(function (sessions) { - if (index >= 0 && index < sessions.length) { - sessions.splice(index, 1); - updateSessions(sessions); - } + } + }; + + var updateConfig = function updateConfig(info) { + if (typeof browser !== 'undefined' && browser.storage) { + browser.storage.local.set({ [storageKey.config]: info }); + } + }; + + var updateSessions = function updateSessions(sessions) { + if (typeof browser !== 'undefined' && browser.storage) { + browser.storage.local.set({ [storageKey.list]: sessions }) + .then(function () { + WindowEvents.emit(document, 'ShowHistoryList'); }); + } + }; + + var onStorageChanged = function onStorageChanged(object) { + if (object[storageKey.list]) { + init(); + } + }; + + GlobalEvents.on('HistorySessionDelete', function (index) { + if (index == -1) { + if (typeof browser !== 'undefined' && browser.runtime) { + browser.runtime.sendMessage({ event: 'history.deleteActive' }); } - }); - - GlobalEvents.on('HistorySessionDeleteAll', function() { - sessions = []; - updateSessions(sessions); - }); - - browser.storage.onChanged.addListener(onStorageChanged); - - // ------------------------------------------------------------------------ - // Public API - - return { - init: init, - getConfig: getConfig, - getFullHistory: getFullHistory, - getHistorySession: getHistorySession, - updateConfig: updateConfig - }; - - })(); - - // ***************************************************************************** + } + else { + getFullHistory(function (sessions) { + if (index >= 0 && index < sessions.length) { + sessions.splice(index, 1); + updateSessions(sessions); + } + }); + } + }); + + GlobalEvents.on('HistorySessionDeleteAll', function () { + sessions = []; + updateSessions(sessions); + }); + + if (typeof browser !== 'undefined' && browser.storage) { + try { + browser.storage.onChanged.addListener(onStorageChanged); + } catch (e) { + console.warn('browser.storage.onChanged.addListener failed', e); + } + } + + // ------------------------------------------------------------------------ // Public API - exports.SessionHistory = SessionHistory; -}); \ No newline at end of file + return { + init: init, + getConfig: getConfig, + getFullHistory: getFullHistory, + getHistorySession: getHistorySession, + updateConfig: updateConfig + }; + +})(); diff --git a/data/scripts/components/session-hotkey-manager.js b/data/scripts/components/session-hotkey-manager.js index 70fe27a..8616ffc 100644 --- a/data/scripts/components/session-hotkey-manager.js +++ b/data/scripts/components/session-hotkey-manager.js @@ -1,229 +1,220 @@ -define(function(require, exports) { - 'use strict'; +'use strict'; - // ***************************************************************************** - // Custom Modules +// ***************************************************************************** +// Custom Modules - const { BookmarkManager } = require('../session/bookmarks'); +import { BookmarkManager } from '../session/bookmarks'; - // Utils - const { DropDown } = require('../utils/components'); - const { HTMLCreator } = require('../utils/dom'); - const { WindowEvents } = require('../utils/global-events'); +// Utils +import { DropDown } from '../utils/components'; +import { HTMLCreator } from '../utils/dom'; +import { WindowEvents } from '../utils/global-events'; - // ***************************************************************************** - // API +// ***************************************************************************** +// API - /* - * Session Hotkeys - */ +/* +* Session Hotkeys +*/ - function SessionHotkeyManager() - { - // Create DomHealper - var DomElem = HTMLCreator(); +export function SessionHotkeyManager() { + // Create DomHealper + var DomElem = HTMLCreator(); - // ------------------------------------------------------------------------ - // Create UI + // ------------------------------------------------------------------------ + // Create UI - var overlay = DomElem('div', { class : 'hotkey-overlay' }); - var panel = DomElem('div', { class : 'hotkey-manager' }); + var overlay = DomElem('div', { class: 'hotkey-overlay' }); + var panel = DomElem('div', { class: 'hotkey-manager' }); - var header = DomElem('div', { class : 'header' }); - header.textContent = 'Change hotkey'; + var header = DomElem('div', { class: 'header' }); + header.textContent = 'Change hotkey'; - var infoBox = DomElem('div', { class : 'info-box' }); - var message = DomElem('div', { class : 'message' }); - var url = DomElem('div', { class: 'link' }); - var supportLink = 'https://support.mozilla.org/en-US/kb/keyboard-shortcuts-perform-firefox-tasks-quickly'; + var infoBox = DomElem('div', { class: 'info-box' }); + var message = DomElem('div', { class: 'message' }); + var url = DomElem('div', { class: 'link' }); + var supportLink = 'https://support.mozilla.org/en-US/kb/keyboard-shortcuts-perform-firefox-tasks-quickly'; - url.textContent = 'Click to see Firefox default shortcuts'; - message.textContent = 'Default Firefox shortcuts cannot be overrided'; - infoBox.appendChild(message); - infoBox.appendChild(url); + url.textContent = 'Click to see Firefox default shortcuts'; + message.textContent = 'Default Firefox shortcuts cannot be overrided'; + infoBox.appendChild(message); + infoBox.appendChild(url); - function addPlusSeparator(parent) { - var separator = DomElem('div', { class : 'plus' }); - separator.textContent = '+'; - parent.appendChild(separator); - } - - function HotkeyInfo(title) - { - var box = DomElem('div', { class : 'hotkey-info' }); - var header = DomElem('div', { class : 'title' }); - var value = DomElem('div', { class : 'value' }); + function addPlusSeparator(parent) { + var separator = DomElem('div', { class: 'plus' }); + separator.textContent = '+'; + parent.appendChild(separator); + } - header.textContent = title; - box.appendChild(header); - box.appendChild(value); + function HotkeyInfo(title) { + var box = DomElem('div', { class: 'hotkey-info' }); + var header = DomElem('div', { class: 'title' }); + var value = DomElem('div', { class: 'value' }); - function setValue(newValue) { - value.textContent = newValue; - } + header.textContent = title; + box.appendChild(header); + box.appendChild(value); - return { - DOMRoot: box, - setValue: setValue - }; + function setValue(newValue) { + value.textContent = newValue; } - var currentHotKey = HotkeyInfo('Current'); - var newHotKey = HotkeyInfo('New'); - - panel.appendChild(header); - panel.appendChild(infoBox); - panel.appendChild(currentHotKey.DOMRoot); - panel.appendChild(newHotKey.DOMRoot); - - var shortcutConfig = DomElem('div', {class : 'shortcut-config'}); - panel.appendChild(shortcutConfig); - - // -------------------------------------------------------------------- - // Modifier key - - var modifierKey = new DropDown({ - id: 'modifier-key', - description: 'Special keys', - onChange: updateNewHotkey - }); - shortcutConfig.appendChild(modifierKey.container); - addPlusSeparator(shortcutConfig); - - modifierKey.addOption('Ctrl'); - modifierKey.addOption('Alt'); - modifierKey.addOption('Command'); - modifierKey.addOption('MacCtrl'); - modifierKey.setValue('Ctrl'); - - // -------------------------------------------------------------------- - // Optional Modifier - - var optionalModifier = new DropDown({ - id: 'optional-modifier', - description: 'Optional modifier', - onChange: updateNewHotkey - }); - shortcutConfig.appendChild(optionalModifier.container); - addPlusSeparator(shortcutConfig); - - optionalModifier.addOption('None', { value: '' }); - optionalModifier.addOption('Shift'); - optionalModifier.setValue('Shift'); - - // -------------------------------------------------------------------- - // Shortcut key - - var shortcutKey = new DropDown({ - id: 'shortcut-key', - description: 'Key', - onChange: updateNewHotkey - }); + return { + DOMRoot: box, + setValue: setValue + }; + } - // Add A-Z literals - for (let i = 65; i < 91; i++) { - shortcutKey.addOption(String.fromCharCode(i)); - } + var currentHotKey = HotkeyInfo('Current'); + var newHotKey = HotkeyInfo('New'); + + panel.appendChild(header); + panel.appendChild(infoBox); + panel.appendChild(currentHotKey.DOMRoot); + panel.appendChild(newHotKey.DOMRoot); + + var shortcutConfig = DomElem('div', { class: 'shortcut-config' }); + panel.appendChild(shortcutConfig); + + // -------------------------------------------------------------------- + // Modifier key + + var modifierKey = new DropDown({ + id: 'modifier-key', + description: 'Special keys', + onChange: updateNewHotkey + }); + shortcutConfig.appendChild(modifierKey.container); + addPlusSeparator(shortcutConfig); + + modifierKey.addOption('Ctrl'); + modifierKey.addOption('Alt'); + modifierKey.addOption('Command'); + modifierKey.addOption('MacCtrl'); + modifierKey.setValue('Ctrl'); + + // -------------------------------------------------------------------- + // Optional Modifier + + var optionalModifier = new DropDown({ + id: 'optional-modifier', + description: 'Optional modifier', + onChange: updateNewHotkey + }); + shortcutConfig.appendChild(optionalModifier.container); + addPlusSeparator(shortcutConfig); + + optionalModifier.addOption('None', { value: '' }); + optionalModifier.addOption('Shift'); + optionalModifier.setValue('Shift'); + + // -------------------------------------------------------------------- + // Shortcut key + + var shortcutKey = new DropDown({ + id: 'shortcut-key', + description: 'Key', + onChange: updateNewHotkey + }); + + // Add A-Z literals + for (let i = 65; i < 91; i++) { + shortcutKey.addOption(String.fromCharCode(i)); + } - // Add 0-9 literals - for (let i = 0; i < 10; i++) { - shortcutKey.addOption('' + i); - } + // Add 0-9 literals + for (let i = 0; i < 10; i++) { + shortcutKey.addOption('' + i); + } - // Add F1-F12 literals - for (let i = 1; i <= 12; i++) { - shortcutKey.addOption('F' + i); - } + // Add F1-F12 literals + for (let i = 1; i <= 12; i++) { + shortcutKey.addOption('F' + i); + } - // Add special characters - shortcutKey.addOption(',', { value: 'Comma'}); - shortcutKey.addOption('.', { value: 'Period'}); - var values = ['Home', 'End', 'PageUp', 'PageDown', 'Space', 'Insert', 'Delete', 'Up', 'Down', 'Left', 'Right']; - for (let i in values) { - shortcutKey.addOption(values[i], { property: 'wide' }); - } - shortcutConfig.appendChild(shortcutKey.container); + // Add special characters + shortcutKey.addOption(',', { value: 'Comma' }); + shortcutKey.addOption('.', { value: 'Period' }); + var values = ['Home', 'End', 'PageUp', 'PageDown', 'Space', 'Insert', 'Delete', 'Up', 'Down', 'Left', 'Right']; + for (let i in values) { + shortcutKey.addOption(values[i], { property: 'wide' }); + } + shortcutConfig.appendChild(shortcutKey.container); - // -------------------------------------------------------------------- - // Action bar + // -------------------------------------------------------------------- + // Action bar - var actionBar = DomElem('div', {class : 'action-bar'}); + var actionBar = DomElem('div', { class: 'action-bar' }); - var ok = DomElem('div', {class : 'button ok'}); - ok.textContent = 'Confirm'; + var ok = DomElem('div', { class: 'button ok' }); + ok.textContent = 'Confirm'; - var cancel = DomElem('div', {class : 'button cancel'}); - cancel.textContent = 'Cancel'; + var cancel = DomElem('div', { class: 'button cancel' }); + cancel.textContent = 'Cancel'; - var flexArea1 = DomElem('div', {class : 'flex'}); - var flexArea2 = DomElem('div', {class : 'flex'}); + var flexArea1 = DomElem('div', { class: 'flex' }); + var flexArea2 = DomElem('div', { class: 'flex' }); - actionBar.appendChild(flexArea1); - actionBar.appendChild(ok); - actionBar.appendChild(cancel); - actionBar.appendChild(flexArea2); + actionBar.appendChild(flexArea1); + actionBar.appendChild(ok); + actionBar.appendChild(cancel); + actionBar.appendChild(flexArea2); - panel.appendChild(actionBar); - overlay.appendChild(panel); + panel.appendChild(actionBar); + overlay.appendChild(panel); - // ------------------------------------------------------------------------ - // Events + // ------------------------------------------------------------------------ + // Events - var changeCallback; + var changeCallback; - function updateNewHotkey() { - var optional = ' + ' + (optionalModifier.getValue() ? optionalModifier.getValue() + ' + ' : ''); - newHotKey.setValue(modifierKey.getValue() + optional + shortcutKey.getValue()); - } + function updateNewHotkey() { + var optional = ' + ' + (optionalModifier.getValue() ? optionalModifier.getValue() + ' + ' : ''); + newHotKey.setValue(modifierKey.getValue() + optional + shortcutKey.getValue()); + } - function setState(state) { - overlay.setAttribute('state', state); - } + function setState(state) { + overlay.setAttribute('state', state); + } - cancel.addEventListener('click', function () { - setState(false); - }); + cancel.addEventListener('click', function () { + setState(false); + }); - ok.addEventListener('click', function () { - setState(false); + ok.addEventListener('click', function () { + setState(false); - var optional = '+' + (optionalModifier.getValue() ? optionalModifier.getValue() + '+' : ''); - changeCallback(modifierKey.getValue() + optional + shortcutKey.getValue()); - }); + var optional = '+' + (optionalModifier.getValue() ? optionalModifier.getValue() + '+' : ''); + changeCallback(modifierKey.getValue() + optional + shortcutKey.getValue()); + }); - url.addEventListener('click', function () { - BookmarkManager.openBookmark({ - url: supportLink, - mode: 'newTab', - active: true - }); + url.addEventListener('click', function () { + BookmarkManager.openBookmark({ + url: supportLink, + mode: 'newTab', + active: true }); + }); - function changeHotKey(options) { + function changeHotKey(options) { - changeCallback = options.callback; - var tokens = options.shortcut.split('+'); + changeCallback = options.callback; + var tokens = options.shortcut.split('+'); - header.textContent = options.title, + header.textContent = options.title, currentHotKey.setValue(tokens.join(' + ')); - newHotKey.setValue(''); - modifierKey.setValue(tokens[0]); - optionalModifier.setValue(tokens.length == 3 ? tokens[1] : ''); - shortcutKey.setValue(tokens.length == 3 ? tokens[2] : tokens[1]); - - setState(true); - } - - WindowEvents.on(document, 'ChangeHotkey', changeHotKey); + newHotKey.setValue(''); + modifierKey.setValue(tokens[0]); + optionalModifier.setValue(tokens.length == 3 ? tokens[1] : ''); + shortcutKey.setValue(tokens.length == 3 ? tokens[2] : tokens[1]); - // ------------------------------------------------------------------------ - // Public properties - - this.DOMRoot = overlay; + setState(true); } - // ***************************************************************************** - // Public API + WindowEvents.on(document, 'ChangeHotkey', changeHotKey); + + // ------------------------------------------------------------------------ + // Public properties - exports.SessionHotkeyManager = SessionHotkeyManager; -}); \ No newline at end of file + this.DOMRoot = overlay; +} diff --git a/data/scripts/components/session-hotkeys.js b/data/scripts/components/session-hotkeys.js index e05913d..d4d6d04 100644 --- a/data/scripts/components/session-hotkeys.js +++ b/data/scripts/components/session-hotkeys.js @@ -1,71 +1,67 @@ -define(function(require, exports) { - 'use strict'; +'use strict'; - // ***************************************************************************** - // Custom Modules +// ***************************************************************************** +// Custom Modules - // Utils - const { WindowEvents } = require('../utils/global-events'); +// Utils +import { WindowEvents } from '../utils/global-events'; - // ***************************************************************************** - // API +// ***************************************************************************** +// API - /* - * Session Hotkeys - */ +/* +* Session Hotkeys +*/ - function SessionHotkeys() - { - // ------------------------------------------------------------------------ - // Events +export function SessionHotkeys() { + // ------------------------------------------------------------------------ + // Events - var onKeyDown = function(e) - { - switch(e.keyCode) - { - // Key: F1 - case 112: { - WindowEvents.emit(document, 'SessionScrollToSelected'); - break; - } + var onKeyDown = function (e) { + switch (e.keyCode) { + // Key: F1 + case 112: { + WindowEvents.emit(document, 'SessionScrollToSelected'); + break; + } - // Key: F2 - case 113: { - WindowEvents.emit(document, 'EditSelectedSession'); - break; - } + // Key: F2 + case 113: { + WindowEvents.emit(document, 'EditSelectedSession'); + break; + } + + // Key: F3 + case 114: { + e.preventDefault(); + WindowEvents.emit(document, 'FocusSessionFilter'); + break; + } - // Key: F3 - case 114: { + // Key: F6 + case 117: { + e.preventDefault(); + WindowEvents.emit(document, 'DeleteHistorySessions'); + break; + } + + // Key: CTRL + F + case 70: { + if (e.ctrlKey == true && e.shiftKey == false && e.altKey == false) { e.preventDefault(); WindowEvents.emit(document, 'FocusSessionFilter'); - break; - } - - // Key: CTRL + F - case 70: { - if (e.ctrlKey == true && e.shiftKey == false && e.altKey == false) - { - e.preventDefault(); - WindowEvents.emit(document, 'FocusSessionFilter'); - } - break; } + break; } - }; - - // ------------------------------------------------------------------------ - // Events - - document.addEventListener('keydown', onKeyDown); + } + }; - // ------------------------------------------------------------------------ - // Public properties + // ------------------------------------------------------------------------ + // Events - } + document.addEventListener('keydown', onKeyDown); - // ***************************************************************************** - // Public API + // ------------------------------------------------------------------------ + // Public properties - exports.SessionHotkeys = SessionHotkeys; -}); \ No newline at end of file +} \ No newline at end of file diff --git a/data/scripts/components/session-list.js b/data/scripts/components/session-list.js index 3b0a237..3e49f4d 100644 --- a/data/scripts/components/session-list.js +++ b/data/scripts/components/session-list.js @@ -1,39 +1,38 @@ -define(function(require, exports) { - 'use strict'; +'use strict'; - // ************************************************************************ - // Modules +// ************************************************************************ +// Modules - const { AppConfig } = require('../config'); +import { AppConfig } from '../config'; - const { HTMLCreator } = require('../utils/dom'); - const { WindowEvents, GlobalEvents } = require('../utils/global-events'); - const { AutoScroll } = require('../utils/auto-scroll'); +import { HTMLCreator } from '../utils/dom'; +import { WindowEvents, GlobalEvents } from '../utils/global-events'; +import { AutoScroll } from '../utils/auto-scroll'; - const { SessionFolder, SessionHistoryFolder, SessionFolderSortBy } = require('./session-folder'); - const { SessionFolderEvents, SessionHistoryEvents } = require('./session-folder-events'); - const { SessionSyncModel } = require('./session-sync-model'); - const { SessionHistory } = require('./session-history'); +import { SessionFolder, SessionHistoryFolder, SessionFolderSortBy } from './session-folder'; +import { SessionFolderEvents, SessionHistoryEvents } from './session-folder-events'; +import { SessionSyncModel } from './session-sync-model'; +import { SessionHistory } from './session-history'; - // ************************************************************************ - // API +// ************************************************************************ +// API - /* - * Session List - * UI Module for interacting with all the session - */ +/* +* Session List +* UI Module for interacting with all the session +*/ - function SessionList() - { +export class SessionList { + constructor() { var DomElem = HTMLCreator(); // ------------------------------------------------------------------------ // Init UI // Session Area - var root = DomElem('div', {class: 'sessions'}); + var root = DomElem('div', { class: 'sessions' }); - var resizeHandle = DomElem('div', {class: 'resize-handle'}); + var resizeHandle = DomElem('div', { class: 'resize-handle' }); root.appendChild(resizeHandle); // History list @@ -41,22 +40,21 @@ define(function(require, exports) { root.appendChild(historyList); // Session list - var sessionList = DomElem('div', {class: 'session-list'}); + var sessionList = DomElem('div', { class: 'session-list' }); root.appendChild(sessionList); // ------------------------------------------------------------------------ // Events - AppConfig.onChange('style.sessions.list.width', function(width) { + AppConfig.onChange('style.sessions.list.width', function (width) { root.style.width = width + 'px'; }); - AppConfig.onChange('style.scale.sessions', function(value) { + AppConfig.onChange('style.scale.sessions', function (value) { root.style.fontSize = value + 'px'; }); - var startResize = function startResize(e) - { + var startResize = function startResize(e) { var startPos = e.clientX; var initWidth = root.clientWidth; resizeHandle.setAttribute('resizing', 'true'); @@ -79,82 +77,80 @@ define(function(require, exports) { resizeHandle.addEventListener('mousedown', startResize); - WindowEvents.on(document, 'ListSessions', this.setSessions.bind(this)); + if (typeof document !== 'undefined') { + WindowEvents.on(document, 'ListSessions', this.setSessions.bind(this)); - WindowEvents.on(document, 'SetPromiseSession', function(sessionInfo) { - this.promiseInfo = sessionInfo; - WindowEvents.emit(document, 'ShowSyncList', { update: sessionInfo.update }); - }.bind(this)); + WindowEvents.on(document, 'SetPromiseSession', function (sessionInfo) { + this.promiseInfo = sessionInfo; + WindowEvents.emit(document, 'ShowSyncList', { update: sessionInfo.update }); + }.bind(this)); - WindowEvents.on(document, 'SelectSyncSession', function(context) { - this.selectSyncSession(context, false); - }.bind(this)); + WindowEvents.on(document, 'SelectSyncSession', function (context) { + this.selectSyncSession(context, false); + }.bind(this)); - WindowEvents.on(document, 'FilterSessions', this.filterSessions.bind(this)); - WindowEvents.on(document, 'SortSessionsBy', this.sortSessionsBy.bind(this)); + WindowEvents.on(document, 'FilterSessions', this.filterSessions.bind(this)); + WindowEvents.on(document, 'SortSessionsBy', this.sortSessionsBy.bind(this)); - WindowEvents.on(document, 'SessionsPositionChanged', function(indices) { - if (AppConfig.get('session.sorting') == 'position-asc' && AppConfig.get('session.active.filter').length == 0) - { - var len = indices.length; - for (var i = 0; i < len; i++) { - var node = this.SyncModel.getBookmark(indices[i]); - node.setVirtualPosition(SessionSyncModel.bookmarks[node.bookmarkID].index); + WindowEvents.on(document, 'SessionsPositionChanged', function (indices) { + if (AppConfig.get('session.sorting') == 'position-asc' && AppConfig.get('session.active.filter').length == 0) { + var len = indices.length; + for (var i = 0; i < len; i++) { + var node = this.SyncModel.getBookmark(indices[i]); + node.setVirtualPosition(SessionSyncModel.bookmarks[node.bookmarkID].index); + } } - } - }.bind(this)); + }.bind(this)); - WindowEvents.on(document, 'ShowCurrentSession', function() { - if (AppConfig.get('session.selected') != null) { - AppConfig.set('session.selected', null); - AppConfig.set('state.scrollTop.current', 0); - } - this.setState('active'); - this.setSelectedNode(undefined); - }.bind(this)); + WindowEvents.on(document, 'ShowCurrentSession', function () { + if (AppConfig.get('session.selected') != null) { + AppConfig.set('session.selected', null); + AppConfig.set('state.scrollTop.current', 0); + } + this.setState('active'); + this.setSelectedNode(undefined); + }.bind(this)); - WindowEvents.on(document, 'ShowSyncList', function(options) { - // Select first visible session - if (this.promiseInfo.sessionID == null) - { - for (var i=0; i < this.sessions.length; i++) - { - if (this.sessions[i].isVisible()) - { - this.selectSyncSession(this.sessions[i], true, false); - break; + WindowEvents.on(document, 'ShowSyncList', function (options) { + // Select first visible session + if (this.promiseInfo.sessionID == null) { + for (var i = 0; i < this.sessions.length; i++) { + if (this.sessions[i].isVisible()) { + this.selectSyncSession(this.sessions[i], true, false); + break; + } } } - } - this.setState('sync'); - if (options.update == true) { - GlobalEvents.emit('update-sessions'); - } - }.bind(this)); + this.setState('sync'); + if (options.update == true) { + GlobalEvents.emit('update-sessions'); + } + }.bind(this)); - WindowEvents.on(document, 'SessionDescriptionChanged', function() { - this.sortSessionsBy(AppConfig.get('session.sorting')); - this.scrollToSelected(); - }.bind(this)); + WindowEvents.on(document, 'SessionDescriptionChanged', function () { + this.sortSessionsBy(AppConfig.get('session.sorting')); + this.scrollToSelected(); + }.bind(this)); - WindowEvents.on(document, 'SessionScrollToSelected', function() { - this.scrollToSelected(); - }.bind(this)); + WindowEvents.on(document, 'SessionScrollToSelected', function () { + this.scrollToSelected(); + }.bind(this)); - GlobalEvents.on('BookmarkDeleted', function (itemID) { - if (AppConfig.get('session.selected') == itemID) { - WindowEvents.emit(this.document, 'ShowCurrentSession'); - } - }.bind(this)); + GlobalEvents.on('BookmarkDeleted', function (itemID) { + if (AppConfig.get('session.selected') == itemID) { + WindowEvents.emit(this.document, 'ShowCurrentSession'); + } + }.bind(this)); - WindowEvents.on(document, 'SelectHistorySession', function(context) { - this.selectHistorySession(context); - }.bind(this)); + WindowEvents.on(document, 'SelectHistorySession', function (context) { + this.selectHistorySession(context); + }.bind(this)); + } - AppConfig.onChange('session.selected', function(value) { - if (value) { - WindowEvents.emit(document, 'SetPromiseSession', { sessionID: value, update: false } ); + AppConfig.onChange('session.selected', function (value) { + if (value && typeof document !== 'undefined') { + WindowEvents.emit(document, 'SetPromiseSession', { sessionID: value, update: false }); } }); @@ -172,36 +168,35 @@ define(function(require, exports) { this.promiseInfo = {}; this.DOMList = sessionList; this.DOMRoot = root; - this.document = document; + this.document = typeof document !== 'undefined' ? document : null; - this.SyncModel = SessionSyncModel.getModel(document); + this.SyncModel = SessionSyncModel.getModel(this.document); } - SessionList.prototype.setState = function setState(state) - { + setState(state) { AppConfig.set('session.view', state); - WindowEvents.emit(document, 'SetUIState', { list: state }); - }; + if (this.document) { + WindowEvents.emit(this.document, 'SetUIState', { list: state }); + } + } - SessionList.prototype.createHistoryList = function createHistoryList() - { + createHistoryList() { var DomElem = HTMLCreator(); // ------------------------------------------------------------------------ // Create UI - var historyList = DomElem('div', {class: 'history-list'}); + var historyList = DomElem('div', { class: 'history-list' }); // ------------------------------------------------------------------------ // Events - function refreshHistoryList() - { + var refreshHistoryList = () => { var snap = AppConfig.isInitState(); var selectedIndex = AppConfig.get('session.history.selected'); historyList.textContent = ''; - SessionHistory.getFullHistory(function (historySessions, activeSession) { + SessionHistory.getFullHistory((historySessions, activeSession) => { if (activeSession) { let activeSnapshot = new SessionHistoryFolder(activeSession, -1); @@ -210,8 +205,7 @@ define(function(require, exports) { } for (let i = historySessions.length - 1; i >= 0; i--) { - if (historySessions[i] && historySessions[i].tabCount) - { + if (historySessions[i] && historySessions[i].tabCount) { let historySession = new SessionHistoryFolder(historySessions[i], i); historyList.appendChild(historySession.DOMRoot); @@ -221,48 +215,49 @@ define(function(require, exports) { } } } - }.bind(this)); + }); } - GlobalEvents.on('UpdateHistoryList', function() { + GlobalEvents.on('UpdateHistoryList', function () { refreshHistoryList(); }); - WindowEvents.on(document, 'ShowHistoryList', function() { - this.setState('history'); - refreshHistoryList(); - }.bind(this)); + if (this.document) { + WindowEvents.on(this.document, 'ShowHistoryList', () => { + this.setState('history'); + refreshHistoryList(); + }); + } - historyList.addEventListener('click', function(e) { - if (e.target.className === 'history-node') - { + historyList.addEventListener('click', (e) => { + if (e.target.className === 'history-node') { this.selectHistorySession(e.target); } - }.bind(this)); + }); // ------------------------------------------------------------------------ // Public data return historyList; - }; + } - SessionList.prototype.selectHistorySession = function selectHistorySession(node) - { + selectHistorySession(node) { this.setSelectedNode(node); var index = node.getAttribute('index') | 0; - SessionHistory.getHistorySession(index, function (sessionInfo) { - WindowEvents.emit(document, 'ShowHistorySession', sessionInfo); + SessionHistory.getHistorySession(index, (sessionInfo) => { + if (this.document) { + WindowEvents.emit(this.document, 'ShowHistorySession', sessionInfo); + } if (AppConfig.get('session.history.selected') != index) { AppConfig.set('session.history.selected', index); AppConfig.set('state.scrollTop.history', 0); } }); - }; + } - SessionList.prototype.setSessions = function setSessions() - { + setSessions() { this.sessions = []; this.DOMList.textContent = ''; @@ -271,30 +266,27 @@ define(function(require, exports) { var selectedSession; var filterExpression = AppConfig.get('session.active.filter'); - SessionSyncModel.sessions.forEach(function(session) { + SessionSyncModel.sessions.forEach(function (session) { SessionSyncModel.bookmarks[session.id] = session; // find if a cache instance is already created and update it var sessionFolder = this.SyncModel.getBookmark(session.id); - if (sessionFolder instanceof SessionFolder) - { + if (sessionFolder instanceof SessionFolder) { sessionFolder.updateFrom(session); } - else - { + else { sessionFolder = new SessionFolder(session); sessionFolder.setVirtualPosition(session.index); this.SyncModel.setBookmark(session.id, sessionFolder); } - if (session.id === this.promiseInfo.sessionID) - { + if (session.id === this.promiseInfo.sessionID) { selectedSession = sessionFolder; } - if (filterExpression.length) { - if (sessionFolder.match(this.filter)) { + if (filterExpression.length > 0) { + if (sessionFolder.match(filterExpression)) { sessionFolder.setVirtualPosition(position); position++; } @@ -312,36 +304,34 @@ define(function(require, exports) { this.sortSessionsBy(AppConfig.get('session.sorting')); // Switch to History View if this was the state before closing the UI - if (AppConfig.get('session.view') == 'history') - { - WindowEvents.emit(document, 'ShowHistoryList'); + if (AppConfig.get('session.view') == 'history' && this.document) { + WindowEvents.emit(this.document, 'ShowHistoryList'); return; } // Preview the selected session if available - if (selectedSession) - { + if (selectedSession) { this.selectSyncSession(selectedSession, true, AppConfig.isInitState()); return; } // Otherwise, preview the current session AppConfig.isInitState(); - WindowEvents.emit(this.document, 'ShowCurrentSession'); - }; + if (this.document) { + WindowEvents.emit(this.document, 'ShowCurrentSession'); + } + } - SessionList.prototype.scrollToSelected = function scrollToSelected(snap) - { + scrollToSelected(snap) { if (this.selectedNode == undefined) return; var offset = this.selectedNode.offsetTop - this.DOMList.clientHeight / 2 + this.selectedNode.clientHeight; AutoScroll.scrollTo(this.DOMList, offset, snap === true ? 0 : 0.25); - }; + } - SessionList.prototype.setSelectedNode = function setSelectedNode(node) - { + setSelectedNode(node) { if (this.selectedNode != undefined) { this.selectedNode.removeAttribute('active'); } @@ -351,42 +341,40 @@ define(function(require, exports) { if (node) { node.setAttribute('active', 'true'); } - }; + } - SessionList.prototype.selectSyncSession = function selectSyncSession(selectedSession, scrollTo, snap) - { - if (AppConfig.get('session.selected') != selectedSession.bookmarkID) - { + selectSyncSession(selectedSession, scrollTo, snap) { + if (AppConfig.get('session.selected') != selectedSession.bookmarkID) { AppConfig.set('session.selected', selectedSession.bookmarkID); AppConfig.set('state.scrollTop.restore', 0); } this.setSelectedNode(selectedSession.DOMRoot); - WindowEvents.emit(this.document, 'ViewSession', selectedSession.bookmarkID); - if (this.promiseInfo.edit == true) { - WindowEvents.emit(this.document, 'SessionContextMenu-EditSession', selectedSession.bookmarkID); + if (this.document) { + WindowEvents.emit(this.document, 'ViewSession', selectedSession.bookmarkID); + if (this.promiseInfo.edit == true) { + WindowEvents.emit(this.document, 'SessionContextMenu-EditSession', selectedSession.bookmarkID); + } } // auto-scroll to the selected node if (scrollTo) { this.scrollToSelected(snap); } - }; + } - SessionList.prototype.filterSessions = function filterSessions(expression) - { + filterSessions(expression) { AppConfig.set('session.active.filter', expression); - if (expression.length) - { + if (expression.length) { expression = expression.toLowerCase(); var exactMatch = false; var expLength = expression.length; - var document = this.document; + var doc = this.document; var position = 0; - this.sessions.forEach(function(session) { + this.sessions.forEach(function (session) { if (session.match(expression)) { session.setVirtualPosition(position); position++; @@ -399,32 +387,27 @@ define(function(require, exports) { }); this.DOMList.parentNode.scrollTop = 0; - WindowEvents.emit(document, 'FilterSessionsExactMatch', exactMatch); + if (doc) { + WindowEvents.emit(doc, 'FilterSessionsExactMatch', exactMatch); + } } else { - this.sessions.forEach(function(session) { + this.sessions.forEach(function (session) { session.setVisible(); }); this.sortSessionsBy(AppConfig.get('session.sorting')); } - }; + } - SessionList.prototype.sortSessionsBy = function sortSessionsBy(method) - { + sortSessionsBy(method) { AppConfig.set('session.sorting', method); this.sessions.sort(SessionFolderSortBy[method]); var position = 0; - this.sessions.forEach(function(session) { + this.sessions.forEach(function (session) { if (session.isVisible()) { session.setVirtualPosition(position); position++; } }); - }; - - // ------------------------------------------------------------------------ - // Module exports - - exports.SessionList = SessionList; - -}); \ No newline at end of file + } +} \ No newline at end of file diff --git a/data/scripts/components/session-sorting.js b/data/scripts/components/session-sorting.js index 435b988..ace0cad 100644 --- a/data/scripts/components/session-sorting.js +++ b/data/scripts/components/session-sorting.js @@ -1,118 +1,109 @@ -define(function(require, exports) { - 'use strict'; +'use strict'; - // ***************************************************************************** - // Custom Modules +// ***************************************************************************** +// Custom Modules - // Utils - const { AppConfig } = require('../config'); - const { HTMLCreator } = require('../utils/dom'); - const { WindowEvents, GlobalEvents } = require('../utils/global-events'); +// Utils +import { AppConfig } from '../config'; +import { HTMLCreator } from '../utils/dom'; +import { WindowEvents, GlobalEvents } from '../utils/global-events'; - // ***************************************************************************** - // API +// ***************************************************************************** +// API - function SessionSorting() - { - // Create DomHealper - var DomElem = HTMLCreator(); +export function SessionSorting() { + // Create DomHealper + var DomElem = HTMLCreator(); - // ------------------------------------------------------------------------ - // UI + // ------------------------------------------------------------------------ + // UI - // Session ordering - var sortControl = DomElem('div', {class: 'sorting-method', tooltip: 'Sort sessions'}); + // Session ordering + var sortControl = DomElem('div', { class: 'sorting-method', tooltip: 'Sort sessions' }); - // Sorting dropdown - var optionList = DomElem('div', {class: 'option-list'}); + // Sorting dropdown + var optionList = DomElem('div', { class: 'option-list' }); - var options = { - 'name-asc' : { description: 'name ↑', index: 0 }, - 'name-desc' : { description: 'name ↓', index: 1 }, - 'position-asc' : { description: 'position ↑', index: 2 }, - 'position-desc' : { description: 'position ↓', index: 3 }, - 'date-asc' : { description: 'date ↑', index: 4 }, - 'date-desc' : { description: 'date ↓', index: 5 } - }; + var options = { + 'name-asc': { description: 'name ↑', index: 0 }, + 'name-desc': { description: 'name ↓', index: 1 }, + 'position-asc': { description: 'position ↑', index: 2 }, + 'position-desc': { description: 'position ↓', index: 3 }, + 'date-asc': { description: 'date ↑', index: 4 }, + 'date-desc': { description: 'date ↓', index: 5 } + }; - for (var key in options) - { - var option = DomElem('div', {class: 'option', value: key}); - option.textContent = options[key].description; - optionList.appendChild(option); + for (var key in options) { + var option = DomElem('div', { class: 'option', value: key }); + option.textContent = options[key].description; + optionList.appendChild(option); - } + } - var dropdown = DomElem('div', {class: 'dropdown'}); - dropdown.appendChild(optionList); - sortControl.appendChild(dropdown); + var dropdown = DomElem('div', { class: 'dropdown' }); + dropdown.appendChild(optionList); + sortControl.appendChild(dropdown); - var activeButton = optionList.children[0]; + var activeButton = optionList.children[0]; - // ------------------------------------------------------------------------ - // Events + // ------------------------------------------------------------------------ + // Events - AppConfig.onChange('session.sorting', function (value) { - if (options[value]) { - activeButton = optionList.children[options[value].index]; + AppConfig.onChange('session.sorting', function (value) { + if (options[value]) { + activeButton = optionList.children[options[value].index]; + activeButton.setAttribute('active', ''); + } + }); + + optionList.addEventListener('click', function (e) { + if (e.target.className == 'option') { + if (e.target != activeButton) { + activeButton.removeAttribute('active'); + activeButton = e.target; + var value = activeButton.getAttribute('value'); activeButton.setAttribute('active', ''); } - }); - - optionList.addEventListener('click', function(e) { - if (e.target.className == 'option') { - if (e.target != activeButton) { - activeButton.removeAttribute('active'); - activeButton = e.target; - var value = activeButton.getAttribute('value'); - activeButton.setAttribute('active', ''); - } - WindowEvents.emit(document, 'SortSessionsBy', value); - } - }); - - var state = false; - var toggleState = function toggleState() { - state = !state; - sortControl.setAttribute('active', state); - WindowEvents.emit(document, 'OverlaySystem', { state: state, zIndex: 1 }); - }; - - document.addEventListener('click', function(e) { - if (e.target.className == 'sorting-method') { + WindowEvents.emit(document, 'SortSessionsBy', value); + } + }); + + var state = false; + var toggleState = function toggleState() { + state = !state; + sortControl.setAttribute('active', state); + WindowEvents.emit(document, 'OverlaySystem', { state: state, zIndex: 1 }); + }; + + document.addEventListener('click', function (e) { + if (e.target.className == 'sorting-method') { + toggleState(); + } + else { + if (state == true) toggleState(); - } - else { - if (state == true) - toggleState(); - } - }); - - // Tooltip events - sortControl.addEventListener('mouseover', function(e) { - var tooltipMsg = e.target.getAttribute('tooltip'); - if (tooltipMsg) { - WindowEvents.emit(document, 'ShowTooltip', { - node: e.target, - message: tooltipMsg - }); - } else { - WindowEvents.emit(document, 'HideTooltip'); - } - }); - - sortControl.addEventListener('mouseleave', function() { + } + }); + + // Tooltip events + sortControl.addEventListener('mouseover', function (e) { + var tooltipMsg = e.target.getAttribute('tooltip'); + if (tooltipMsg) { + WindowEvents.emit(document, 'ShowTooltip', { + node: e.target, + message: tooltipMsg + }); + } else { WindowEvents.emit(document, 'HideTooltip'); - }); - - // ------------------------------------------------------------------------ - // Data + } + }); - this.DOMRoot = sortControl; - } + sortControl.addEventListener('mouseleave', function () { + WindowEvents.emit(document, 'HideTooltip'); + }); - // ***************************************************************************** - // Public API + // ------------------------------------------------------------------------ + // Data - exports.SessionSorting = SessionSorting; -}); \ No newline at end of file + this.DOMRoot = sortControl; +} diff --git a/data/scripts/components/session-sync-model.js b/data/scripts/components/session-sync-model.js index 88080ea..de82054 100644 --- a/data/scripts/components/session-sync-model.js +++ b/data/scripts/components/session-sync-model.js @@ -1,457 +1,446 @@ -define(function(require, exports) { - 'use strict'; - - // ***************************************************************************** - // Custom Modules - - const { AppConfig } = require('../config'); - - // Components - const { BookmarkManager } = require('../session/bookmarks'); - const { WindowEvents, GlobalEvents } = require('../utils/global-events'); - - // Utils - const { LimitCounter } = require('../utils/general'); - - // ***************************************************************************** - // API - - var SessionSyncDOMModel = function SessionSyncDOMModel(document) - { - this.bookmarks = {}; - this.tabs = {}; - this.state = {}; - - // ------------------------------------------------------------------------ - // Events - - var OrderBookmarks = function OrderBookmarks(indices) - { - var len = indices.length; - for (let i = 0; i < len; i++) { - var node = this.bookmarks[indices[i]]; - if (node) { - node.setVirtualPosition(SessionSyncModel.bookmarks[node.bookmarkID].index); - } - } - }.bind(this); - - WindowEvents.on(document, 'BookmarksPositionChanged', OrderBookmarks); - - WindowEvents.on(document, 'BookmarkTitleChanged', function(bookmarkID) { - var node = this.bookmarks[bookmarkID]; - if (node) { - node.setTitle(SessionSyncModel.bookmarks[bookmarkID].title); - } - }.bind(this)); - - // ------------------------------------------------------------------------ - // Register - - SessionSyncModel.registerModel(document, this); - }; - - SessionSyncDOMModel.prototype.getBookmark = function getBookmark(id) - { - return this.bookmarks[id]; - }; - - SessionSyncDOMModel.prototype.setBookmark = function setBookmark(id, bookmark) - { - this.bookmarks[id] = bookmark; - }; - - SessionSyncDOMModel.prototype.removeBookmark = function removeBookmark(id) - { - if (this.bookmarks[id]) - { - this.bookmarks[id].remove(); - delete this.bookmarks[id]; - } - }; - - var SessionSyncModel = (function SessionSync() { - - // Bookmarks storage - var sessions = {}; - var bookmarks = {}; - - // Per document DOM cache - var domModels = new Map(); - - // ------------------------------------------------------------------------ - // API - - var getModel = function getModel(document) - { - var model = domModels.get(document); - if ((model instanceof SessionSyncDOMModel) === false) { - model = new SessionSyncDOMModel(document); - } - return model; - }; - - var registerModel = function registerModel(document, model) - { - if (model instanceof SessionSyncDOMModel) { - domModels.set(document, model); - } - }; - - var unregisterModel = function unregisterModel(document) - { - var model = domModels.get(document); - if (model instanceof SessionSyncDOMModel) { - domModels.delete(namespace); - } - }; - - function onActionError() - { - WindowEvents.emitLocal('Notification', { - message: 'Error when updating bookmarks' - }); - } - - function isSession(parentID) { - return parentID === AppConfig.get('storageID'); - } - - // Assumes moving in the same parent directory - var moveBookmark = function moveBookmark(idA, idB) - { - var parentID = bookmarks[idA].parentId; - var indexA = bookmarks[idA].index; - var indexB = bookmarks[idB].index; - var direction = indexA > indexB ? 1 : -1; - - BookmarkManager.moveItem(idA, parentID, indexA < indexB ? indexB : indexB, onSuccessMove, onActionError); - - function onSuccessMove() - { - var indices = []; - var min = Math.min(indexA, indexB); - var max = Math.max(indexA, indexB); - - for (var id in bookmarks) - { - var B = bookmarks[id]; - if (B.parentId == parentID && B.index > min && B.index < max) - { - indices.push(B.id); - B.index += direction; - } - } - - indices.push(idA); - indices.push(idB); - bookmarks[idA].index = indexB; - bookmarks[idB].index += direction; - - WindowEvents.broadcast(isSession(parentID) ? 'SessionsPositionChanged' : 'BookmarksPositionChanged', indices); - } - }; - - // Moving a bookmark to a new session directory - var moveBookmarkTo = function moveBookmarkTo(bookmarkID, folderID) - { - var parentID = bookmarks[bookmarkID].parentId; - if (parentID == folderID) - return; - - BookmarkManager.moveItem(bookmarkID, folderID, undefined, onSuccess, onActionError); - - function onSuccess() - { - bookmarks[bookmarkID].parentId = folderID; - - // TODO: set position based on the length of the new parent list - // TODO: trigger update for folderID - - var index = bookmarks[bookmarkID].index; - bookmarks[bookmarkID].index = -1; - - // Remove the bookmark from all UIs - domModels.forEach(function (model) { - model.removeBookmark(bookmarkID); - }); - - // Reorder all bookmarks after the removed one - var indices = []; - for (var id in bookmarks) - { - var B = bookmarks[id]; - if (B.parentId == parentID && B.index > index) - { - indices.push(B.id); - B.index--; - } - } - - // Trigger reordering - WindowEvents.broadcast('BookmarksPositionChanged', indices); - } - }; - - // all items must have the same parent - var deleteBookmarkList = function deleteBookmarkList(bookmarkIDs, parentID, callback) - { - if (bookmarkIDs.length == 0) - { - callback(); - return; - } - - var reorderList = []; - - // Get position ordered list of all bookmarks from the parrentID folder - for (var id in bookmarks) - { - var B = bookmarks[id]; - if (B.parentId == parentID) { - reorderList[B.index] = B; - } - } - - var updateEvent = new LimitCounter(bookmarkIDs.length, onFinish); - - bookmarkIDs.forEach(function(bookmarkID) - { - BookmarkManager.deleteItem(bookmarkID, false, onDeleteSuccess.bind(null, bookmarkID), updateEvent.advance); - }); - - function onFinish() - { - // compute reordering - var indices = []; - var position = 0; - for (var i = 0; i < reorderList.length; i++) { - if (reorderList[i]) { - indices.push(reorderList[i].id); - reorderList[i].index = position; - position++; - } - } - - if (indices.length) - { - // Trigger reordering - WindowEvents.broadcast('BookmarksPositionChanged', indices); - } - - callback(); - } - - function onDeleteSuccess(bookmarkID) - { - var index = bookmarks[bookmarkID].index; - - // Remove the bookmark from all UIs - domModels.forEach(function (model) { - model.removeBookmark(bookmarkID); - }); - - delete reorderList[index]; - delete bookmarks[bookmarkID]; - GlobalEvents.emit('BookmarkRemoved'); - - updateEvent.advance(); - } - }; - - var deleteBookmarkItem = function deleteBookmarkItem(bookmarkID) - { - var isFolder = bookmarks[bookmarkID].type === 'folder'; - BookmarkManager.deleteItem(bookmarkID, isFolder, onDeleteSuccess, onActionError); - - function onDeleteSuccess() - { - var parentID = bookmarks[bookmarkID].parentId; - var index = bookmarks[bookmarkID].index; - - // Remove the bookmark from all UIs - domModels.forEach(function (model) { - model.removeBookmark(bookmarkID); - }); - - // Reorder all bookmarks after the removed one - var indices = []; - for (let id in bookmarks) - { - let B = bookmarks[id]; - if (B.parentId == parentID && B.index > index) { - indices.push(B.id); - B.index--; - } - } - - // Save undo history - var undoEvent = { type: 'delete', bookmark: bookmarks[bookmarkID], items: [] }; - - if (isFolder) - { - var folderID = bookmarks[bookmarkID].id; - for (let id in bookmarks) - { - let B = bookmarks[id]; - if (B.parentId == folderID) { - undoEvent.items.push(Object.assign({}, B)); - } - } - } - - // Remove model references - delete bookmarks[bookmarkID]; - undoEvent.items.forEach(function(B) { - delete bookmarks[B.id]; - }); - - // Save undo event - var undoEvents = AppConfig.get('undo.events'); - undoEvents.push(undoEvent); - - AppConfig.set('undo.events', undoEvents); - GlobalEvents.emit('BookmarkDeleted', bookmarkID); - - // Trigger reordering - WindowEvents.broadcast('BookmarksPositionChanged', indices); - } - }; - - var updateBookmarkItem = function updateBookmarkItem(bookmarkID, options) - { - try { - if (options.title) { - BookmarkManager.setTitle(bookmarks[bookmarkID], options.title, function() { - WindowEvents.broadcast('BookmarkTitleChanged', bookmarkID); - WindowEvents.broadcast('SessionDescriptionChanged', bookmarkID); - }); - } - - if (options.url) { - BookmarkManager.setLocation(bookmarks[bookmarkID], options.url); - } - - WindowEvents.emitLocal('Notification', { - message: 'Success' - }); - - } catch(err) { - console.log(err); - WindowEvents.emitLocal('Notification', { - message: 'Error while saving bookmark' - }); - } - }; - - // ------------------------------------------------------------------------ - // Events - - var recreateBookmark = function recreateBookmark(item) - { - delete item.id; - delete item.dateAdded; - delete item.dateGroupModified; - delete item.title_lowercase; - return BookmarkManager.createBookmark(item); - }; - - var undoHistoryEvent = function undoHistoryEvent() - { - var sessionID = undefined; - var undoEvents = AppConfig.get('undo.events'); - - function onSuccess() - { - AppConfig.set('undo.events', undoEvents); - WindowEvents.emit(document, 'SetPromiseSession', { sessionID: sessionID, update: false }); - GlobalEvents.emit('update-sessions'); - } - - if (undoEvents.length) - { - var historyEvent = undoEvents.pop(); - - if (historyEvent.type == 'delete') - { - switch (historyEvent.bookmark.type) - { - case 'bookmark': - case 'separator': { - recreateBookmark(historyEvent.bookmark) - .then(function (bookmark) { - sessionID = (bookmark.parentId == AppConfig.get('storageID')) ? bookmark.id : bookmark.parentId; - onSuccess(); - }); - break; - } - - case 'folder': { - // Restore deleted folder - var oldID = historyEvent.bookmark.id; - - recreateBookmark(historyEvent.bookmark) - .then(function (node) { - sessionID = node.id; - var newID = node.id; - var updateEvent = new LimitCounter(historyEvent.items.length, onSuccess); - - // correct parent for each bookmark - if (oldID != newID) - { - // correct restore event - historyEvent.items.forEach(function (bookmark) { - bookmark.parentId = newID; - recreateBookmark(bookmark) - .then(updateEvent.advance, updateEvent.advance); - }); - - // correct all undo events - undoEvents.forEach(function (event) { - event.items.forEach(function (bookmark) { - if (bookmark.parentId == oldID) { - bookmark.parentId = newID; - } - }); - }); - } - - if (historyEvent.items.length == 0) { - onSuccess(); - } - }); - break; - } - } - } - } - }; - - // Subscribe to management events - GlobalEvents.on('DeleteBookmarkItem', deleteBookmarkItem); - GlobalEvents.on('HistoryUndo', undoHistoryEvent); - - // ------------------------------------------------------------------------ - // Public API - - return { - getModel: getModel, - unregisterModel: unregisterModel, - registerModel: registerModel, - - domModels: domModels, - bookmarks: bookmarks, - moveBookmark: moveBookmark, - moveBookmarkTo: moveBookmarkTo, - - deleteBookmarkList: deleteBookmarkList, - updateBookmarkItem: updateBookmarkItem, - }; - - })(); - - // ***************************************************************************** - // Public API - - exports.SessionSyncModel = SessionSyncModel; -}); \ No newline at end of file +'use strict'; + +// ***************************************************************************** +// Custom Modules + +import { AppConfig } from '../config'; + +// Components +import { BookmarkManager } from '../session/bookmarks'; +import { WindowEvents, GlobalEvents } from '../utils/global-events'; + +// Utils +import { LimitCounter } from '../utils/general'; + +// ***************************************************************************** +// API + +export class SessionSyncDOMModel { + constructor(document) { + this.bookmarks = {}; + this.tabs = {}; + this.state = {}; + + // ------------------------------------------------------------------------ + // Events + + const OrderBookmarks = (indices) => { + var len = indices.length; + for (let i = 0; i < len; i++) { + var node = this.bookmarks[indices[i]]; + if (node) { + node.setVirtualPosition(SessionSyncModel.bookmarks[node.bookmarkID].index); + } + } + }; + + WindowEvents.on(document, 'BookmarksPositionChanged', OrderBookmarks); + + WindowEvents.on(document, 'BookmarkTitleChanged', (bookmarkID) => { + var node = this.bookmarks[bookmarkID]; + if (node) { + node.setTitle(SessionSyncModel.bookmarks[bookmarkID].title); + } + }); + + // ------------------------------------------------------------------------ + // Register + + SessionSyncModel.registerModel(document, this); + } + + getBookmark(id) { + return this.bookmarks[id]; + } + + setBookmark(id, bookmark) { + this.bookmarks[id] = bookmark; + } + + removeBookmark(id) { + if (this.bookmarks[id]) { + this.bookmarks[id].remove(); + delete this.bookmarks[id]; + } + } +} + +export const SessionSyncModel = (function SessionSync() { + + // Bookmarks storage + var sessions = {}; + var bookmarks = {}; + + // Per document DOM cache + var domModels = new Map(); + + // ------------------------------------------------------------------------ + // API + + var getModel = function getModel(doc) + { + var model = domModels.get(doc); + if ((model instanceof SessionSyncDOMModel) === false) { + model = new SessionSyncDOMModel(doc); + } + return model; + }; + + var registerModel = function registerModel(doc, model) + { + if (model instanceof SessionSyncDOMModel) { + domModels.set(doc, model); + } + }; + + var unregisterModel = function unregisterModel(doc) + { + var model = domModels.get(doc); + if (model instanceof SessionSyncDOMModel) { + domModels.delete(doc); + } + }; + + function onActionError() + { + WindowEvents.emitLocal('Notification', { + message: 'Error when updating bookmarks' + }); + } + + function isSession(parentID) { + return parentID === AppConfig.get('storageID'); + } + + // Assumes moving in the same parent directory + var moveBookmark = function moveBookmark(idA, idB) + { + var parentID = bookmarks[idA].parentId; + var indexA = bookmarks[idA].index; + var indexB = bookmarks[idB].index; + var direction = indexA > indexB ? 1 : -1; + + BookmarkManager.moveItem(idA, parentID, indexA < indexB ? indexB : indexB, onSuccessMove, onActionError); + + function onSuccessMove() + { + var indices = []; + var min = Math.min(indexA, indexB); + var max = Math.max(indexA, indexB); + + for (var id in bookmarks) + { + var B = bookmarks[id]; + if (B.parentId == parentID && B.index > min && B.index < max) + { + indices.push(B.id); + B.index += direction; + } + } + + indices.push(idA); + indices.push(idB); + bookmarks[idA].index = indexB; + bookmarks[idB].index += direction; + + WindowEvents.broadcast(isSession(parentID) ? 'SessionsPositionChanged' : 'BookmarksPositionChanged', indices); + } + }; + + // Moving a bookmark to a new session directory + var moveBookmarkTo = function moveBookmarkTo(bookmarkID, folderID) + { + var parentID = bookmarks[bookmarkID].parentId; + if (parentID == folderID) + return; + + BookmarkManager.moveItem(bookmarkID, folderID, undefined, onSuccess, onActionError); + + function onSuccess() + { + bookmarks[bookmarkID].parentId = folderID; + + // TODO: set position based on the length of the new parent list + // TODO: trigger update for folderID + + var index = bookmarks[bookmarkID].index; + bookmarks[bookmarkID].index = -1; + + // Remove the bookmark from all UIs + domModels.forEach(function (model) { + model.removeBookmark(bookmarkID); + }); + + // Reorder all bookmarks after the removed one + var indices = []; + for (var id in bookmarks) + { + var B = bookmarks[id]; + if (B.parentId == parentID && B.index > index) + { + indices.push(B.id); + B.index--; + } + } + + // Trigger reordering + WindowEvents.broadcast('BookmarksPositionChanged', indices); + } + }; + + // all items must have the same parent + var deleteBookmarkList = function deleteBookmarkList(bookmarkIDs, parentID, callback) + { + if (bookmarkIDs.length == 0) + { + callback(); + return; + } + + var reorderList = []; + + // Get position ordered list of all bookmarks from the parrentID folder + for (var id in bookmarks) + { + var B = bookmarks[id]; + if (B.parentId == parentID) { + reorderList[B.index] = B; + } + } + + var updateEvent = new LimitCounter(bookmarkIDs.length, onFinish); + + bookmarkIDs.forEach(function(bookmarkID) + { + BookmarkManager.deleteItem(bookmarkID, false, onDeleteSuccess.bind(null, bookmarkID), updateEvent.advance); + }); + + function onFinish() + { + // compute reordering + var indices = []; + var position = 0; + for (var i = 0; i < reorderList.length; i++) { + if (reorderList[i]) { + indices.push(reorderList[i].id); + reorderList[i].index = position; + position++; + } + } + + if (indices.length) + { + // Trigger reordering + WindowEvents.broadcast('BookmarksPositionChanged', indices); + } + + callback(); + } + + function onDeleteSuccess(bookmarkID) + { + var index = bookmarks[bookmarkID].index; + + // Remove the bookmark from all UIs + domModels.forEach(function (model) { + model.removeBookmark(bookmarkID); + }); + + delete reorderList[index]; + delete bookmarks[bookmarkID]; + GlobalEvents.emit('BookmarkRemoved'); + + updateEvent.advance(); + } + }; + + var deleteBookmarkItem = function deleteBookmarkItem(bookmarkID) + { + var isFolder = bookmarks[bookmarkID].type === 'folder'; + BookmarkManager.deleteItem(bookmarkID, isFolder, onDeleteSuccess, onActionError); + + function onDeleteSuccess() + { + var parentID = bookmarks[bookmarkID].parentId; + var index = bookmarks[bookmarkID].index; + + // Remove the bookmark from all UIs + domModels.forEach(function (model) { + model.removeBookmark(bookmarkID); + }); + + // Reorder all bookmarks after the removed one + var indices = []; + for (let id in bookmarks) + { + let B = bookmarks[id]; + if (B.parentId == parentID && B.index > index) { + indices.push(B.id); + B.index--; + } + } + + // Save undo history + var undoEvent = { type: 'delete', bookmark: bookmarks[bookmarkID], items: [] }; + + if (isFolder) + { + var folderID = bookmarks[bookmarkID].id; + for (let id in bookmarks) + { + let B = bookmarks[id]; + if (B.parentId == folderID) { + undoEvent.items.push(Object.assign({}, B)); + } + } + } + + // Remove model references + delete bookmarks[bookmarkID]; + undoEvent.items.forEach(function(B) { + delete bookmarks[B.id]; + }); + + // Save undo event + var undoEvents = AppConfig.get('undo.events'); + undoEvents.push(undoEvent); + + AppConfig.set('undo.events', undoEvents); + GlobalEvents.emit('BookmarkDeleted', bookmarkID); + + // Trigger reordering + WindowEvents.broadcast('BookmarksPositionChanged', indices); + } + }; + + var updateBookmarkItem = function updateBookmarkItem(bookmarkID, options) + { + try { + if (options.title) { + BookmarkManager.setTitle(bookmarks[bookmarkID], options.title, function() { + WindowEvents.broadcast('BookmarkTitleChanged', bookmarkID); + WindowEvents.broadcast('SessionDescriptionChanged', bookmarkID); + }); + } + + if (options.url) { + BookmarkManager.setLocation(bookmarks[bookmarkID], options.url); + } + + WindowEvents.emitLocal('Notification', { + message: 'Success' + }); + + } catch(err) { + console.log(err); + WindowEvents.emitLocal('Notification', { + message: 'Error while saving bookmark' + }); + } + }; + + // ------------------------------------------------------------------------ + // Events + + var recreateBookmark = function recreateBookmark(item) + { + delete item.id; + delete item.dateAdded; + delete item.dateGroupModified; + delete item.title_lowercase; + return BookmarkManager.createBookmark(item); + }; + + var undoHistoryEvent = function undoHistoryEvent() + { + var sessionID = undefined; + var undoEvents = AppConfig.get('undo.events'); + + function onSuccess() + { + AppConfig.set('undo.events', undoEvents); + WindowEvents.emit(document, 'SetPromiseSession', { sessionID: sessionID, update: false }); + GlobalEvents.emit('update-sessions'); + } + + if (undoEvents.length) + { + var historyEvent = undoEvents.pop(); + + if (historyEvent.type == 'delete') + { + switch (historyEvent.bookmark.type) + { + case 'bookmark': + case 'separator': { + recreateBookmark(historyEvent.bookmark) + .then(function (bookmark) { + sessionID = (bookmark.parentId == AppConfig.get('storageID')) ? bookmark.id : bookmark.parentId; + onSuccess(); + }); + break; + } + + case 'folder': { + // Restore deleted folder + var oldID = historyEvent.bookmark.id; + + recreateBookmark(historyEvent.bookmark) + .then(function (node) { + sessionID = node.id; + var newID = node.id; + var updateEvent = new LimitCounter(historyEvent.items.length, onSuccess); + + // correct parent for each bookmark + if (oldID != newID) + { + // correct restore event + historyEvent.items.forEach(function (bookmark) { + bookmark.parentId = newID; + recreateBookmark(bookmark) + .then(updateEvent.advance, updateEvent.advance); + }); + + // correct all undo events + undoEvents.forEach(function (event) { + event.items.forEach(function (bookmark) { + if (bookmark.parentId == oldID) { + bookmark.parentId = newID; + } + }); + }); + } + + if (historyEvent.items.length == 0) { + onSuccess(); + } + }); + break; + } + } + } + } + }; + + // Subscribe to management events + GlobalEvents.on('DeleteBookmarkItem', deleteBookmarkItem); + GlobalEvents.on('HistoryUndo', undoHistoryEvent); + + // ------------------------------------------------------------------------ + // Public API + + return { + getModel: getModel, + unregisterModel: unregisterModel, + registerModel: registerModel, + + domModels: domModels, + bookmarks: bookmarks, + moveBookmark: moveBookmark, + moveBookmarkTo: moveBookmarkTo, + + deleteBookmarkList: deleteBookmarkList, + updateBookmarkItem: updateBookmarkItem, + }; + +})(); \ No newline at end of file diff --git a/data/scripts/components/session-toolbar.js b/data/scripts/components/session-toolbar.js index 7a9c9f9..04a2207 100644 --- a/data/scripts/components/session-toolbar.js +++ b/data/scripts/components/session-toolbar.js @@ -1,277 +1,297 @@ -define(function(require, exports) { - 'use strict'; +'use strict'; - // ***************************************************************************** - // Custom Modules +// ***************************************************************************** +// Custom Modules - // App - const { AppConfig } = require('../config'); +// App +import { AppConfig } from '../config'; - // Utils - const { HTMLCreator } = require('../utils/dom'); - const { WindowEvents, GlobalEvents } = require('../utils/global-events'); - const DOMComponent = require('../utils/components'); +// Utils +import { HTMLCreator } from '../utils/dom'; +import { WindowEvents, GlobalEvents } from '../utils/global-events'; +import * as DOMComponent from '../utils/components'; - // ***************************************************************************** - // API +// ***************************************************************************** +// API - /* - * Command bar - */ +/* +* Command bar +*/ - function SessionToolbar() - { - // Create DomHealper - var DomElem = HTMLCreator(); +export function SessionToolbar() +{ + // Create DomHealper + var DomElem = HTMLCreator(); - // ------------------------------------------------------------------------ - // Create UI + // ------------------------------------------------------------------------ + // Create UI - var toolbar = DomElem('div', {class: 'session-toolbar'}); + var toolbar = DomElem('div', {class: 'session-toolbar'}); - // ------------------------------------------------------------------------ - // List settings + // ------------------------------------------------------------------------ + // List settings - var container = DomElem('div', {class: 'session-selector'}); - container.style.width = AppConfig.get('style.scale.bookmarks') + 'px'; + var container = DomElem('div', {class: 'session-selector'}); + container.style.width = AppConfig.get('style.scale.bookmarks') + 'px'; - var activeBtn = DomElem('div', {class: 'button active-session'}); - activeBtn.textContent = ''; + var activeBtn = DomElem('div', {class: 'button active-session'}); + activeBtn.textContent = ''; - var syncBtn = DomElem('div', {class: 'button sync'}); - syncBtn.textContent = 'Sessions'; + var syncBtn = DomElem('div', {class: 'button sync'}); + syncBtn.textContent = 'Sessions'; - var historyBtn = DomElem('div', {class: 'button history'}); - historyBtn.textContent = 'History'; + var historyBtn = DomElem('div', {class: 'button history'}); + historyBtn.textContent = 'History'; - container.appendChild(activeBtn); - container.appendChild(syncBtn); - container.appendChild(historyBtn); - toolbar.appendChild(container); + container.appendChild(activeBtn); + container.appendChild(syncBtn); + container.appendChild(historyBtn); + toolbar.appendChild(container); - // ------------------------------------------------------------------------ - // Session toolbar menu + // ------------------------------------------------------------------------ + // Session toolbar menu - var sessionMenu = this.createMenu(); - toolbar.appendChild(sessionMenu); + var sessionMenu = this.createMenu(); + toolbar.appendChild(sessionMenu); - var sessionDate = DomElem('div', {class: 'session-date'}); - toolbar.appendChild(sessionDate); + var sessionDate = DomElem('div', {class: 'session-date'}); + toolbar.appendChild(sessionDate); - // ------------------------------------------------------------------------ - // Events + // ------------------------------------------------------------------------ + // Events + if (typeof document !== 'undefined') { WindowEvents.on(document, 'SetSessionDate', function(date) { sessionDate.textContent = (new Date(date)).toLocaleString(); }); + } - activeBtn.addEventListener('click', function() { + activeBtn.addEventListener('click', function() { + if (typeof document !== 'undefined') { WindowEvents.emit(document, 'ShowCurrentSession'); - }); + } + }); - syncBtn.addEventListener('click', function() { + syncBtn.addEventListener('click', function() { + if (typeof document !== 'undefined') { WindowEvents.emit(document, 'ShowSyncList', { update: true }); - }); + } + }); - historyBtn.addEventListener('click', function() { + historyBtn.addEventListener('click', function() { + if (typeof document !== 'undefined') { WindowEvents.emit(document, 'ShowHistoryList'); - }); - - AppConfig.onChange('style.sessions.list.width', function(width) { - container.style.width = width + 'px'; - }); - - AppConfig.onChange('style.scale.toolbar', function(value) { - toolbar.style.fontSize = value + 'px'; - }); - - // ------------------------------------------------------------------------ - // Public properties - - this.document = document; - this.DOMRoot = toolbar; - } - - SessionToolbar.prototype.createMenu = function createMenu() - { - var DomElem = HTMLCreator(); + } + }); - // ------------------------------------------------------------------------ - // UI - SessionContainer menu + AppConfig.onChange('style.sessions.list.width', function(width) { + container.style.width = width + 'px'; + }); - var menu = DomElem('div', {class: 'menu-bar'}); + AppConfig.onChange('style.scale.toolbar', function(value) { + toolbar.style.fontSize = value + 'px'; + }); - var ActionButton = function actionButton(options) - { - var button = DomElem('div', { - class: 'button ' + options.className, - 'state-current': options.state.current, - 'state-restore': options.state.restore, - 'state-history': options.state.history, - }); - button.setAttribute('tooltip', options.tooltip); + // ------------------------------------------------------------------------ + // Public properties - button.addEventListener('click', function (e) { - WindowEvents.emit(document, options.event, e); - }); - - return button; - }; - - var save = ActionButton({ - tooltip: 'Save session', - className: 'save', - event: 'MenuSaveSession', - state: { - current : true, - history: true - } - }); - - var add = ActionButton({ - tooltip: 'Add current tab', - className: 'add', - event: 'MenuAddCurrentTab', - state: { - restore: true - } - }); - - var restore = ActionButton({ - tooltip: 'Restore', - className: 'restore', - event: 'MenuRestoreClick', - state: { - restore: true - } - }); + this.document = typeof document !== 'undefined' ? document : null; + this.DOMRoot = toolbar; +} - var restoreW = ActionButton({ - tooltip: 'Restore (new window)', - className: 'restore-new-win', - event: 'MenuRestoreNewWindow', - state: { - restore : true, - history: true - } - }); +SessionToolbar.prototype.createMenu = function createMenu() +{ + var DomElem = HTMLCreator(); - var replaceSession = ActionButton({ - tooltip: 'Restore (close active)', - className: 'repalce-session', - event: 'ReplaceSession', - state: { - restore: true - } - }); + // ------------------------------------------------------------------------ + // UI - SessionContainer menu - var replaceSessionAll = ActionButton({ - tooltip: 'Restore (close all)', - className: 'repalce-session-all', - event: 'ReplaceSessionAll', - state: { - restore: true - } - }); + var menu = DomElem('div', {class: 'menu-bar'}); - var mergeSession = ActionButton({ - tooltip: 'Merge sessions', - className: 'merge-sessions', - event: 'MenuMergeSessions', - state: { - restore: true - } + var ActionButton = function actionButton(options) + { + var button = DomElem('div', { + class: 'button ' + options.className, + 'state-current': options.state.current, + 'state-restore': options.state.restore, + 'state-history': options.state.history, }); + button.setAttribute('tooltip', options.tooltip); - var overwriteSession = ActionButton({ - tooltip: 'Overwrite session', - className: 'replace-session', - event: 'MenuReplaceSession', - state: { - restore: true + button.addEventListener('click', function (e) { + if (typeof document !== 'undefined') { + WindowEvents.emit(document, options.event, e); } }); - var separator1 = DomElem('div', {class: 'separator'}); - var separator2 = DomElem('div', {class: 'separator'}); - var saveConfig = DomElem('div', {class: 'save-config'}); - - // Pin tabs action - - var saveCfgKey = 'session.save'; - var saveCfg = AppConfig.get(saveCfgKey); - var cfgSavePinned = new DOMComponent.ToggleSwitch({ - state: saveCfg.pinned, - tooltip: 'Save pinned tabs', - attribute: 'pin', - onState: '', - offState: '', - callback: function(value) { - saveCfg.pinned = value; - AppConfig.set(saveCfgKey, saveCfg); - } - }); - saveConfig.appendChild(cfgSavePinned.DOMRoot); - - var cfgAllWindows = new DOMComponent.ToggleSwitch({ - state: saveCfg.allWindows, - tooltip: 'Show all windows', - attribute: 'windows', - onState: '', - offState: '', - callback: function(value) { - saveCfg.allWindows = value; - AppConfig.set(saveCfgKey, saveCfg); - } - }); - saveConfig.appendChild(cfgAllWindows.DOMRoot); - - menu.appendChild(restore); - menu.appendChild(restoreW); - menu.appendChild(replaceSession); - menu.appendChild(replaceSessionAll); - menu.appendChild(separator1); - menu.appendChild(mergeSession); - menu.appendChild(overwriteSession); - menu.appendChild(separator2); - menu.appendChild(add); - menu.appendChild(save); - menu.appendChild(saveConfig); - - - // ------------------------------------------------------------------------ - // Events - - AppConfig.onChange(saveCfgKey, function(value) { - cfgSavePinned.setState(value.pinned); - cfgAllWindows.setState(value.allWindows); - }); + return button; + }; - // Tooltip events - menu.addEventListener('mouseover', function(e) { - var tooltipMsg = e.target.getAttribute('tooltip'); - if (tooltipMsg) { + var save = ActionButton({ + tooltip: 'Save session', + className: 'save', + event: 'MenuSaveSession', + state: { + current : true, + history: true + } + }); + + var add = ActionButton({ + tooltip: 'Add current tab', + className: 'add', + event: 'MenuAddCurrentTab', + state: { + restore: true + } + }); + + var restore = ActionButton({ + tooltip: 'Restore', + className: 'restore', + event: 'MenuRestoreClick', + state: { + restore: true + } + }); + + var restoreW = ActionButton({ + tooltip: 'Restore (new window)', + className: 'restore-new-win', + event: 'MenuRestoreNewWindow', + state: { + restore : true, + history: true + } + }); + + var replaceSession = ActionButton({ + tooltip: 'Restore (close active)', + className: 'repalce-session', + event: 'ReplaceSession', + state: { + restore: true + } + }); + + var replaceSessionAll = ActionButton({ + tooltip: 'Restore (close all)', + className: 'repalce-session-all', + event: 'ReplaceSessionAll', + state: { + restore: true + } + }); + + var mergeSession = ActionButton({ + tooltip: 'Merge sessions', + className: 'merge-sessions', + event: 'MenuMergeSessions', + state: { + restore: true + } + }); + + var overwriteSession = ActionButton({ + tooltip: 'Overwrite session', + className: 'replace-session', + event: 'MenuReplaceSession', + state: { + restore: true + } + }); + + var separator1 = DomElem('div', {class: 'separator'}); + var separator2 = DomElem('div', {class: 'separator'}); + var saveConfig = DomElem('div', {class: 'save-config'}); + + // Pin tabs action + + var saveCfgKey = 'session.save'; + var saveCfg = AppConfig.get(saveCfgKey); + var cfgSavePinned = new DOMComponent.ToggleSwitch({ + state: saveCfg.pinned, + tooltip: 'Save pinned tabs', + attribute: 'pin', + onState: '', + offState: '', + callback: function(value) { + saveCfg.pinned = value; + AppConfig.set(saveCfgKey, saveCfg); + } + }); + saveConfig.appendChild(cfgSavePinned.DOMRoot); + + var cfgAllWindows = new DOMComponent.ToggleSwitch({ + state: saveCfg.allWindows, + tooltip: 'Show all windows', + attribute: 'windows', + onState: '', + offState: '', + callback: function(value) { + saveCfg.allWindows = value; + AppConfig.set(saveCfgKey, saveCfg); + } + }); + saveConfig.appendChild(cfgAllWindows.DOMRoot); + + menu.appendChild(restore); + menu.appendChild(restoreW); + // menu.appendChild(replaceSession); + // menu.appendChild(replaceSessionAll); + menu.appendChild(separator1); + menu.appendChild(mergeSession); + menu.appendChild(overwriteSession); + menu.appendChild(separator2); + menu.appendChild(add); + menu.appendChild(save); + menu.appendChild(saveConfig); + + // Toolbar customization + var customizeToolbar = ActionButton({ + tooltip: 'Customize Toolbar', + className: 'customize-toolbar', + event: 'OpenCustomizationToolbar', + state: { + restore: true + } + }); + + menu.appendChild(customizeToolbar); + + // ------------------------------------------------------------------------ + // Events + + AppConfig.onChange(saveCfgKey, function(value) { + cfgSavePinned.setState(value.pinned); + cfgAllWindows.setState(value.allWindows); + }); + + // Tooltip events + menu.addEventListener('mouseover', function(e) { + var tooltipMsg = e.target.getAttribute('tooltip'); + if (tooltipMsg) { + if (typeof document !== 'undefined') { WindowEvents.emit(document, 'ShowTooltip', { node: e.target, message: tooltipMsg }); - } else { + } + } else { + if (typeof document !== 'undefined') { WindowEvents.emit(document, 'HideTooltip'); } - }); + } + }); - menu.addEventListener('mouseleave', function() { + menu.addEventListener('mouseleave', function() { + if (typeof document !== 'undefined') { WindowEvents.emit(document, 'HideTooltip'); - }); - - // ------------------------------------------------------------------------ - // Data - - return menu; - }; + } + }); - // ***************************************************************************** - // Public API + // ------------------------------------------------------------------------ + // Data - exports.SessionToolbar = SessionToolbar; -}); \ No newline at end of file + return menu; +}; \ No newline at end of file diff --git a/data/scripts/components/tooltip-system.js b/data/scripts/components/tooltip-system.js index 7fcad39..090ebb9 100644 --- a/data/scripts/components/tooltip-system.js +++ b/data/scripts/components/tooltip-system.js @@ -1,52 +1,41 @@ -define(function(require, exports) { - 'use strict'; +'use strict'; - // ***************************************************************************** - // Modules +// ***************************************************************************** +// Modules - const { HTMLCreator } = require('../utils/dom'); - const { WindowEvents } = require('../utils/global-events'); +import { HTMLCreator } from '../utils/dom'; +import { WindowEvents } from '../utils/global-events'; - // ***************************************************************************** - // TooltipSystem +// ***************************************************************************** +// TooltipSystem - function TooltipSystem() - { - var DomElem = HTMLCreator(); +export function TooltipSystem() { + var DomElem = HTMLCreator(); - var tooltip = DomElem('div', {class : 'tooltip-system'}); + var tooltip = DomElem('div', { class: 'tooltip-system' }); - // ------------------------------------------------------------------------ - // Events + // ------------------------------------------------------------------------ + // Events - var hideTooltip = function hideTooltip() - { - tooltip.removeAttribute('active'); - }; + var hideTooltip = function hideTooltip() { + tooltip.removeAttribute('active'); + }; - var showTooltip = function showTooltip(options) - { - tooltip.textContent = options.message; - tooltip.setAttribute('active', ''); - var position = options.node.getBoundingClientRect(); + var showTooltip = function showTooltip(options) { + tooltip.textContent = options.message; + tooltip.setAttribute('active', ''); + var position = options.node.getBoundingClientRect(); - tooltip.setAttribute('active', ''); - tooltip.style.left = position.x - Math.floor((tooltip.clientWidth - options.node.clientWidth) / 2) + 'px'; - tooltip.style.top = position.y - tooltip.clientHeight + 'px'; - }; + tooltip.setAttribute('active', ''); + tooltip.style.left = position.x - Math.floor((tooltip.clientWidth - options.node.clientWidth) / 2) + 'px'; + tooltip.style.top = position.y - tooltip.clientHeight + 'px'; + }; - WindowEvents.on(document, 'ShowTooltip', showTooltip); - WindowEvents.on(document, 'HideTooltip', hideTooltip); + WindowEvents.on(document, 'ShowTooltip', showTooltip); + WindowEvents.on(document, 'HideTooltip', hideTooltip); - // ------------------------------------------------------------------------ - // Public data + // ------------------------------------------------------------------------ + // Public data - this.DOMRoot = tooltip; - } - - // ***************************************************************************** - // Public API - - exports.TooltipSystem = TooltipSystem; - -}); \ No newline at end of file + this.DOMRoot = tooltip; +} \ No newline at end of file diff --git a/data/scripts/components/trash-can.js b/data/scripts/components/trash-can.js index e235bc5..f428fc4 100644 --- a/data/scripts/components/trash-can.js +++ b/data/scripts/components/trash-can.js @@ -1,126 +1,115 @@ -define(function(require, exports) { - 'use strict'; +'use strict'; - // ***************************************************************************** - // Custom Modules +// ***************************************************************************** +// Custom Modules - // App - const { AppConfig } = require('../config'); +// App +import { AppConfig } from '../config'; - const { WindowEvents, GlobalEvents } = require('../utils/global-events'); - const { HTMLCreator } = require('../utils/dom'); - const { DragContext } = require('../utils/drag-context'); - const { SessionFolder } = require('./session-folder'); - const { SessionBookmark, SessionTab } = require('./session-bookmark'); +import { WindowEvents, GlobalEvents } from '../utils/global-events'; +import { HTMLCreator } from '../utils/dom'; +import { DragContext } from '../utils/drag-context'; +import { SessionFolder } from './session-folder'; +import { SessionBookmark, SessionTab } from './session-bookmark'; - // ***************************************************************************** - // API +// ***************************************************************************** +// API - function TrashCan() { +export function TrashCan() { - // Create DomHealper - var DomElem = HTMLCreator(); + // Create DomHealper + var DomElem = HTMLCreator(); - // ------------------------------------------------------------------------ - var block = DomElem('div', { - class: 'trash-can', - items: 0, - dismiss: true, - tooltip: 'Drop item to delete' - }); - - var count = DomElem('div', {class: 'count', tooltip: 'Click to restore'}); - count.textContent = '0'; - block.appendChild(count); + // ------------------------------------------------------------------------ + var block = DomElem('div', { + class: 'trash-can', + items: 0, + dismiss: true, + tooltip: 'Drop item to delete' + }); - var dismiss = DomElem('div', { - class: 'dismiss css-close-button', - tooltip: 'Hide' - }); + var count = DomElem('div', { class: 'count', tooltip: 'Click to restore' }); + count.textContent = '0'; + block.appendChild(count); - block.appendChild(dismiss); + var dismiss = DomElem('div', { + class: 'dismiss css-close-button', + tooltip: 'Hide' + }); - var configKey = { - hide: 'trashcan.hide', - hideCount: 'trashcan.hide-count', - undoEvents: 'undo.events', - }; + block.appendChild(dismiss); - // ------------------------------------------------------------------------ - // Events - - // History Undo/Redo events - count.addEventListener('click', function() { - GlobalEvents.emit('HistoryUndo'); - }); + var configKey = { + hide: 'trashcan.hide', + hideCount: 'trashcan.hide-count', + undoEvents: 'undo.events', + }; - function updateItemCount(value) { - count.textContent = value; - block.setAttribute('items', value); - AppConfig.set(configKey.hide, value == 0 || value == AppConfig.get(configKey.hideCount)); - } + // ------------------------------------------------------------------------ + // Events - block.addEventListener('mouseup', function (e) { - var item = DragContext.getContext(); - if (item instanceof SessionFolder || item instanceof SessionBookmark) { - block.setAttribute('action', 'drop'); - GlobalEvents.emit('DeleteBookmarkItem', item.bookmarkID); - WindowEvents.emit(document, 'ShowTooltip', { - node: e.target, - message: 'Click to restore' - }); - return; - } - - if (item instanceof SessionTab) { - GlobalEvents.emit('management.close-tab', item.tab.id); - } - }); + // History Undo/Redo events + count.addEventListener('click', function () { + GlobalEvents.emit('HistoryUndo'); + }); - dismiss.addEventListener('click', function () { - AppConfig.set(configKey.hide, true); - AppConfig.set(configKey.hideCount, AppConfig.get(configKey.undoEvents).length); - }); + function updateItemCount(value) { + count.textContent = value; + block.setAttribute('items', value); + AppConfig.set(configKey.hide, value == 0 || value == AppConfig.get(configKey.hideCount)); + } - // Tooltip events - block.addEventListener('mouseover', function(e) { + block.addEventListener('mouseup', function (e) { + var item = DragContext.getContext(); + if (item instanceof SessionFolder || item instanceof SessionBookmark) { + block.setAttribute('action', 'drop'); + GlobalEvents.emit('DeleteBookmarkItem', item.bookmarkID); WindowEvents.emit(document, 'ShowTooltip', { node: e.target, - message: e.target.getAttribute('tooltip') + message: 'Click to restore' }); - }); - - block.addEventListener('mouseleave', function() { - WindowEvents.emit(document, 'HideTooltip'); - }); + return; + } - WindowEvents.on(document, 'TrashCan-Droppable', function(value) { - block.setAttribute('droppable', value); + if (item instanceof SessionTab) { + GlobalEvents.emit('management.close-tab', item.tab.id); + } + }); + + dismiss.addEventListener('click', function () { + AppConfig.set(configKey.hide, true); + AppConfig.set(configKey.hideCount, AppConfig.get(configKey.undoEvents).length); + }); + + // Tooltip events + block.addEventListener('mouseover', function (e) { + WindowEvents.emit(document, 'ShowTooltip', { + node: e.target, + message: e.target.getAttribute('tooltip') }); + }); - // ------------------------------------------------------------------------ - // Init code + block.addEventListener('mouseleave', function () { + WindowEvents.emit(document, 'HideTooltip'); + }); + WindowEvents.on(document, 'TrashCan-Droppable', function (value) { + block.setAttribute('droppable', value); + }); - // ------------------------------------------------------------------------ - // Events + // ------------------------------------------------------------------------ + // Events - AppConfig.onChange(configKey.hide, function (value) { - block.setAttribute('dismiss', value); - }); + AppConfig.onChange(configKey.hide, function (value) { + block.setAttribute('dismiss', value); + }); - AppConfig.onChange(configKey.undoEvents, function (value) { - updateItemCount(value.length); - }); - - // ------------------------------------------------------------------------ - // Public properties - - this.DOMRoot = block; - } + AppConfig.onChange(configKey.undoEvents, function (value) { + updateItemCount(value.length); + }); - // ***************************************************************************** - // Public API - exports.TrashCan = TrashCan; + // ------------------------------------------------------------------------ + // Public properties -}); \ No newline at end of file + this.DOMRoot = block; +} diff --git a/data/scripts/components/url-bar.js b/data/scripts/components/url-bar.js index 15b10b1..4e23570 100644 --- a/data/scripts/components/url-bar.js +++ b/data/scripts/components/url-bar.js @@ -1,91 +1,80 @@ -define(function(require, exports) { - 'use strict'; +'use strict'; - // ------------------------------------------------------------------------ - // Modules +// ------------------------------------------------------------------------ +// Modules + +import { HTMLCreator } from '../utils/dom'; +import { WindowEvents } from '../utils/global-events'; + +// ------------------------------------------------------------------------ +// API + +export function UrlBar() { + var DomElem = HTMLCreator(); - const { HTMLCreator } = require('../utils/dom'); - const { WindowEvents } = require('../utils/global-events'); + var url_bar = DomElem('div', { class: 'url-bar' }); + var url_arrow = DomElem('div', { class: 'arrow' }); + var url_value = DomElem('div', { class: 'url' }); + var copy_feedback = DomElem('div', { class: 'copy-feedback' }); + var input = DomElem('input', { class: 'copy-input' }); + + copy_feedback.textContent = 'Copied'; + url_arrow.textContent = 'url'; + url_bar.appendChild(input); + url_bar.appendChild(url_arrow); + url_bar.appendChild(url_value); + url_bar.appendChild(copy_feedback); // ------------------------------------------------------------------------ - // API - - function UrlBar() - { - var DomElem = HTMLCreator(); - - var url_bar = DomElem('div', {class: 'url-bar'}); - var url_arrow = DomElem('div', {class: 'arrow'}); - var url_value = DomElem('div', {class: 'url'}); - var copy_feedback = DomElem('div', {class: 'copy-feedback'}); - var input = DomElem('input', {class: 'copy-input'}); - - copy_feedback.textContent = 'Copied'; - url_arrow.textContent = 'url'; - url_bar.appendChild(input); - url_bar.appendChild(url_arrow); - url_bar.appendChild(url_value); - url_bar.appendChild(copy_feedback); - - // ------------------------------------------------------------------------ - // Events - - var timeoutID = 0; - var feedbackTimeoutID = 0; - - function clearTimeID() { - clearTimeout(timeoutID); - } - - function showURLInfoBar(url) - { - clearTimeout(timeoutID); - url_value.textContent = url; - url_bar.setAttribute('data-active', 'true'); - url_bar.addEventListener('mouseenter', clearTimeID); - url_bar.addEventListener('mouseleave', hideURLInfoBar); - } - - function hideURLInfoBar() - { - timeoutID = setTimeout(function() { - url_bar.removeAttribute('data-active'); - url_bar.removeEventListener('mouseleave', clearTimeID); - url_bar.removeEventListener('mouseenter', hideURLInfoBar); - }, 1000); - } - - function copyURLToClipboard() - { - input.value = url_value.textContent; - input.select(); - document.execCommand('copy'); - - clearTimeout(feedbackTimeoutID); - copy_feedback.setAttribute('active', ''); - feedbackTimeoutID = setTimeout(function() { - copy_feedback.removeAttribute('active'); - }, 1500); - } - - WindowEvents.on(document, 'ShowUrlBar', showURLInfoBar); - WindowEvents.on(document, 'HideUrlBar', hideURLInfoBar); - WindowEvents.on(document, 'UrlBar-CopyURL', copyURLToClipboard); - - url_bar.addEventListener('click', copyURLToClipboard); - - // ------------------------------------------------------------------------ - // Public data - - this.DOMRoot = url_bar; + // Events + + var timeoutID = 0; + var feedbackTimeoutID = 0; + + function clearTimeID() { + clearTimeout(timeoutID); + } + + function showURLInfoBar(url) { + clearTimeout(timeoutID); + url_value.textContent = url; + url_bar.setAttribute('data-active', 'true'); + url_bar.addEventListener('mouseenter', clearTimeID); + url_bar.addEventListener('mouseleave', hideURLInfoBar); + } + + function hideURLInfoBar() { + timeoutID = setTimeout(function () { + url_bar.removeAttribute('data-active'); + url_bar.removeEventListener('mouseleave', clearTimeID); + url_bar.removeEventListener('mouseenter', hideURLInfoBar); + }, 1000); + } + + function copyURLToClipboard() { + input.value = url_value.textContent; + input.select(); + document.execCommand('copy'); + + clearTimeout(feedbackTimeoutID); + copy_feedback.setAttribute('active', ''); + feedbackTimeoutID = setTimeout(function () { + copy_feedback.removeAttribute('active'); + }, 1500); } - UrlBar.prototype.setAutoHideDelay = function setAutoHideDelay(miliseconds) { - this.hide_delay = miliseconds; - }; + WindowEvents.on(document, 'ShowUrlBar', showURLInfoBar); + WindowEvents.on(document, 'HideUrlBar', hideURLInfoBar); + WindowEvents.on(document, 'UrlBar-CopyURL', copyURLToClipboard); + + url_bar.addEventListener('click', copyURLToClipboard); // ------------------------------------------------------------------------ - // Module exports + // Public data + + this.DOMRoot = url_bar; +} - exports.UrlBar = UrlBar; -}); +UrlBar.prototype.setAutoHideDelay = function setAutoHideDelay(miliseconds) { + this.hide_delay = miliseconds; +}; diff --git a/data/scripts/config.js b/data/scripts/config.js index 2bf5552..c7b0429 100644 --- a/data/scripts/config.js +++ b/data/scripts/config.js @@ -1,111 +1,107 @@ -define(function(require, exports) { - 'use strict'; +'use strict'; - // ***************************************************************************** - // Custom Modules +// ***************************************************************************** +// Custom Modules - const { PubSub } = require('./3rd/pubsub'); +import { PubSub } from './3rd/pubsub'; - // Utils - const { GlobalEvents } = require('./utils/global-events'); +// Utils +import { GlobalEvents } from './utils/global-events'; - // ***************************************************************************** - // API +// ***************************************************************************** +// API - var Config = (function() { +const Config = (function () { - var initState = true; - var initConfig = true; - var localSettings = {}; - var configEvents = new PubSub(); - var isPanelUI = (document.body.clientWidth == 0); + var initState = true; + var initConfig = true; + var localSettings = {}; + var configEvents = new PubSub(); + var isPanelUI = (typeof document !== 'undefined' && document.body) ? (document.body.clientWidth == 0) : false; - // ------------------------------------------------------------------------ - // API + // ------------------------------------------------------------------------ + // API - function getConfigValue(key, defaultValue) - { - if (initConfig && localSettings[key] == undefined) { - localSettings[key] = defaultValue; - return; - } + function getConfigValue(key, defaultValue) { + if (initConfig && localSettings[key] == undefined) { + localSettings[key] = defaultValue; + return; + } - browser.storage.local.get(key).then(function (obj) { - if (obj.hasOwnProperty(key)) - { - // console.log('[LocalStorage][Read]', key, obj[key]); - localSettings[key] = obj[key]; - configEvents.publish(key, obj[key]); - } - else - { - // console.log('Not found [LocalStorage][Set]', key, defaultValue); - browser.storage.local.set({ [key] : defaultValue }) + browser.storage.local.get(key).then(function (obj) { + if (obj.hasOwnProperty(key)) { + // console.log('[LocalStorage][Read]', key, obj[key]); + localSettings[key] = obj[key]; + configEvents.publish(key, obj[key]); + } + else { + // console.log('Not found [LocalStorage][Set]', key, defaultValue); + browser.storage.local.set({ [key]: defaultValue }) .then(function success() { localSettings[key] = defaultValue; configEvents.publish(key, defaultValue); }); - } - }); - } - - var setupConfig = function setupConfig() - { - // Styling - getConfigValue('style.panel.width', 800); - getConfigValue('style.panel.height', 600); - getConfigValue('style.sessions.list.width', 220); - - getConfigValue('style.scale.toolbar', 12); - getConfigValue('style.scale.header', 18); - getConfigValue('style.scale.sessions', 12); - getConfigValue('style.scale.bookmarks', 14); - - getConfigValue('state.scrollTop.current', 0); - getConfigValue('state.scrollTop.restore', 0); - getConfigValue('state.scrollTop.history', 0); - - // Session saving settings - getConfigValue('session.save', { - pinned: false, - allWindows: true - }); - - // Management configuration - getConfigValue('session.view', undefined); - getConfigValue('session.sorting', 'position-asc'); - getConfigValue('session.active.filter', ''); - getConfigValue('session.selected', null); - getConfigValue('session.history.selected', null); - getConfigValue('services.favicon.enabled', true); - - // Bookmarks configuration - getConfigValue('bookmark.click.newTab', false); - getConfigValue('bookmark.middleClick.newTab', true); - getConfigValue('restore.lazy.loading', true); - getConfigValue('restore.reverse.order', false); - - // General settings - getConfigValue('context.menu.icons', true); - - getConfigValue('trashcan.hide', true); - getConfigValue('trashcan.hide-count', 0); - getConfigValue('undo.events', []); - - getConfigValue('troubleshooting', false); - }; - - var set = function set(key, value, onSuccess) - { - if (typeof value != 'object' && localSettings[key] == value) - { - if (typeof onSuccess === 'function') { - onSuccess(); - } - return; } + }); + } + + var setupConfig = function setupConfig() { + // Styling + getConfigValue('style.panel.width', 800); + getConfigValue('style.panel.height', 600); + getConfigValue('style.sessions.list.width', 220); + + getConfigValue('style.scale.toolbar', 12); + getConfigValue('style.scale.header', 18); + getConfigValue('style.scale.sessions', 12); + getConfigValue('style.scale.bookmarks', 14); + + getConfigValue('state.scrollTop.current', 0); + getConfigValue('state.scrollTop.restore', 0); + getConfigValue('state.scrollTop.history', 0); + + // Session saving settings + getConfigValue('session.save', { + pinned: false, + allWindows: true + }); + + // Management configuration + getConfigValue('session.view', undefined); + getConfigValue('session.sorting', 'position-asc'); + getConfigValue('session.active.filter', ''); + getConfigValue('session.selected', null); + getConfigValue('session.history.selected', null); + getConfigValue('services.favicon.enabled', true); + + // Bookmarks configuration + getConfigValue('bookmark.click.newTab', false); + getConfigValue('bookmark.middleClick.newTab', true); + getConfigValue('restore.lazy.loading', true); + getConfigValue('restore.reverse.order', false); + + // General settings + getConfigValue('context.menu.icons', true); + + getConfigValue('trashcan.hide', true); + getConfigValue('trashcan.hide-count', 0); + getConfigValue('undo.events', []); + + getConfigValue('troubleshooting', false); + + // Theme configuration + getConfigValue('theme.toolbar.grayscale', false); + }; + + var set = function set(key, value, onSuccess) { + if (typeof value != 'object' && localSettings[key] == value) { + if (typeof onSuccess === 'function') { + onSuccess(); + } + return; + } - browser.storage.local.set({ [key] : value }) + browser.storage.local.set({ [key]: value }) .then(function success() { localSettings[key] = value; // console.log('[Change config]', key, value); @@ -116,105 +112,89 @@ define(function(require, exports) { }, function onError(error) { console.log(error); }); - }; + }; - var get = function get(key) - { - if (localSettings[key] === undefined) { - console.log('[ERROR] Settings key [' + key + '] was not found!'); - } - // console.log(key, localSettings[key]); - return localSettings[key]; - }; - - var isInitState = function isInitState() - { - if (initState == true) { - initState = false; - return true; - } - return false; - }; - - var isPanel = function isPanel() - { - return isPanelUI; - }; - - var devMode = function devMode() - { - return false; - }; - - var isAddonContext = function isAddonContext() - { - return typeof browser === 'object'; - }; - - var onChange = function onChange(topic, callback) { - configEvents.subscribe(topic, callback) - }; - - // ------------------------------------------------------------------------ - // Startup - - var init = function init() - { - setupConfig(); - // console.log(localSettings); - - if (isAddonContext()) { - browser.commands.getAll(). - then(function (commands) { - configEvents.publish('config.hotkey.init', commands); - }); - } - }; + var get = function get(key) { + if (localSettings[key] === undefined) { + console.log('[ERROR] Settings key [' + key + '] was not found!'); + } + // console.log(key, localSettings[key]); + return localSettings[key]; + }; + + var isInitState = function isInitState() { + if (initState == true) { + initState = false; + return true; + } + return false; + }; - if (isAddonContext()) { + var isPanel = function isPanel() { + return isPanelUI; + }; + + var devMode = function devMode() { + return false; + }; - // console.log(''); - // console.log('------------------------------------------'); + var isAddonContext = function isAddonContext() { + return typeof browser === 'object'; + }; - // var manifest = browser.runtime.getManifest(); - // console.log(manifest); - // console.log(manifest.name + ' v' + manifest.version); - // console.log('------------------------------------------'); + var onChange = function onChange(topic, callback) { + configEvents.subscribe(topic, callback) + }; + + // ------------------------------------------------------------------------ + // Startup - setupConfig(); - initConfig = false; + var init = function init() { + setupConfig(); + // console.log(localSettings); - GlobalEvents.on('hotkey.update', function (command) { - browser.commands.update({ - name: command.name, - shortcut: command.shortcut + if (isAddonContext()) { + browser.commands.getAll(). + then(function (commands) { + configEvents.publish('config.hotkey.init', commands); }); - }); } + }; - // ------------------------------------------------------------------------ - // Public API - - return { - set: set, - get: get, - init : init, - onChange: onChange, - devMode: devMode, - isPanel: isPanel, - isInitState: isInitState, - isAddonContext: isAddonContext - }; - })(); + if (isAddonContext()) { - // ------------------------------------------------------------------------ - // Events + // console.log(''); + // console.log('------------------------------------------'); - // ------------------------------------------------------------------------ - // Init + // var manifest = browser.runtime.getManifest(); + // console.log(manifest); + // console.log(manifest.name + ' v' + manifest.version); + // console.log('------------------------------------------'); - // ------------------------------------------------------------------------ - // Module exports + setupConfig(); + initConfig = false; - exports.AppConfig = Config; -}); \ No newline at end of file + GlobalEvents.on('hotkey.update', function (command) { + browser.commands.update({ + name: command.name, + shortcut: command.shortcut + }); + }); + } + + // ------------------------------------------------------------------------ + // Public API + + return { + set: set, + get: get, + init: init, + onChange: onChange, + devMode: devMode, + isPanel: isPanel, + isInitState: isInitState, + isAddonContext: isAddonContext + }; +})(); + +export const AppConfig = Config; \ No newline at end of file diff --git a/data/scripts/require-config.js b/data/scripts/require-config.js deleted file mode 100644 index e3220e5..0000000 --- a/data/scripts/require-config.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -require.config({ - - baseURL: '/scripts', - - nodeIdCompat : true, - -}); - -define(function(require) { - - // Load Modules - var App = require('app'); - - // Init - App.init(); - -}); \ No newline at end of file diff --git a/data/scripts/session-sync-ui.js b/data/scripts/session-sync-ui.js index a6f5ed2..3b67ae0 100644 --- a/data/scripts/session-sync-ui.js +++ b/data/scripts/session-sync-ui.js @@ -1,479 +1,462 @@ -define(function(require, exports) { - 'use strict'; +'use strict'; - // ************************************************************************ - // Modules +// ************************************************************************ +// Modules - // App - const { AppConfig } = require('./config'); +// App +import { AppConfig } from './config'; - // Utils - const { HTMLCreator } = require('./utils/dom'); - const { WindowEvents, GlobalEvents } = require('./utils/global-events'); +// Utils +import { HTMLCreator } from './utils/dom'; +import { WindowEvents, GlobalEvents } from './utils/global-events'; - const { ContextMenu } = require('./utils/context-menu'); - const { ConfirmBox } = require('./utils/confirm-box'); - const { FieldEditWdiget } = require('./utils/field-edit-widget'); +import { ContextMenu } from './utils/context-menu'; +import { ConfirmBox } from './utils/confirm-box'; +import { FieldEditWdiget } from './utils/field-edit-widget'; - // Components - const { Clipboard } = require('./components/clipboard'); - const { UrlBar } = require('./components/url-bar'); - const { TrashCan } = require('./components/trash-can'); - const { TooltipSystem } = require('./components/tooltip-system'); - const { SessionHotkeys } = require('./components/session-hotkeys'); - const { NotificationSystem } = require('./components/notification-system'); - const { ConfigPanel } = require('./components/config-panel'); - const { OverlaySystem } = require('./components/overlay-system'); +// Components +import { Clipboard } from './components/clipboard'; +import { UrlBar } from './components/url-bar'; +import { TrashCan } from './components/trash-can'; +import { TooltipSystem } from './components/tooltip-system'; +import { SessionHotkeys } from './components/session-hotkeys'; +import { NotificationSystem } from './components/notification-system'; +import { ConfigPanel } from './components/config-panel'; +import { OverlaySystem } from './components/overlay-system'; - const { SessionHotkeyManager } = require('./components/session-hotkey-manager'); - const { SessionSorting } = require('./components/session-sorting'); - const { SessionFiltering } = require('./components/session-filtering'); +import { SessionHotkeyManager } from './components/session-hotkey-manager'; +import { SessionSorting } from './components/session-sorting'; +import { SessionFiltering } from './components/session-filtering'; - const { SessionHeaderBar } = require('./components/session-header-bar'); - const { SessionList } = require('./components/session-list'); - const { SessionContainer } = require('./components/session-container'); - const { SessionSyncModel } = require('./components/session-sync-model'); - const { SessionToolbar } = require('./components/session-toolbar'); +import { SessionHeaderBar } from './components/session-header-bar'; +import { SessionList } from './components/session-list'; +import { SessionContainer } from './components/session-container'; +import { SessionSyncModel } from './components/session-sync-model'; +import { SessionToolbar } from './components/session-toolbar'; - // Bookmarks - const { SessionManager } = require('./session/management'); - const { BookmarkManager } = require('./session/bookmarks'); +// Bookmarks +import { SessionManager } from './session/management'; +import { BookmarkManager } from './session/bookmarks'; - // ************************************************************************ - // UI initilization +// ************************************************************************ +// UI initilization - var SessionSyncUI = function SessionSyncUI() { +export const SessionSyncUI = function SessionSyncUI() { - var DomElem = HTMLCreator(); + var DomElem = HTMLCreator(); - document.body.setAttribute('panel', AppConfig.isPanel()); - var content = document.getElementById('body'); + document.body.setAttribute('panel', AppConfig.isPanel()); + var content = document.getElementById('body'); - // ------------------------------------------------------------------------ - // UI Modules - - var configPanel = new ConfigPanel(); - content.appendChild(configPanel.DOMRoot); - - var tooltipSystem = new TooltipSystem(); - content.appendChild(tooltipSystem.DOMRoot); - - var notificationSystem = new NotificationSystem(); - content.appendChild(notificationSystem.DOMRoot); - - var hotekyManager = new SessionHotkeyManager(); - content.appendChild(hotekyManager.DOMRoot); - - var overlaySystem = new OverlaySystem(); - content.appendChild(overlaySystem.DOMRoot); - - // ------------------------------------------------------------- - // Create the UI - var centralArea = DomElem('div', {class: 'sessions-area'}); - - // ------------------------------------------------------------- - // Clipboard - - new Clipboard(); - - // ------------------------------------------------------------- - // GlobalHotkeys - - new SessionHotkeys(); - - // -------------------------------------------------------------------- - // Center area - - var headerBar = new SessionHeaderBar(); - var toolbar = new SessionToolbar(); - var sessionList = new SessionList(); - var searchBar = new SessionFiltering(); - var sessionContainer = new SessionContainer(); - var sessionSorting = new SessionSorting(); - - headerBar.DOMRoot.appendChild(searchBar.DOMRoot); - sessionList.DOMRoot.appendChild(sessionSorting.DOMRoot); - - var body = DomElem('div', {class: 'sessions-area-body'}); - body.appendChild(sessionList.DOMRoot); - body.appendChild(sessionContainer.DOMRoot); - - var resizeHandle = DomElem('div', {class: 'panel-resize-handle'}); - - // ------------------------------------------------------------- - // Trash can - - var trashCan = new TrashCan(); - centralArea.appendChild(trashCan.DOMRoot); - - // -------------------------------------------------------------------- - // URL bar - - var urlBar = new UrlBar(); - headerBar.DOMRoot.appendChild(urlBar.DOMRoot); - - centralArea.appendChild(resizeHandle); - centralArea.appendChild(headerBar.DOMRoot); - centralArea.appendChild(toolbar.DOMRoot); - centralArea.appendChild(body); - content.appendChild(centralArea); - - // ------------------------------------------------------------------------ - // Context Menues - - // Bookmark Item Context Menu - var BM = new ContextMenu({name : 'BookmarkCtxMenu'}); - BM.addMenuEntry({value: 'Open (new tab)', event: 'OpenInNewTab', icon: 'new-tab'}); - if (AppConfig.isPanel()) { - BM.addMenuEntry({value: 'Open (current tab)', event: 'OpenInActiveTab', icon: 'same-tab'}); - } - BM.addMenuEntry({value: 'Open (new window)', event: 'OpenInNewWindow', icon: 'new-window'}); - BM.addMenuEntry({value: 'Copy URL', event: 'CopyURL', icon: 'copy'}); - BM.addMenuEntry({value: 'Copy Title', event: 'CopyTitle', icon: 'copy'}); - BM.addMenuEntry({value: 'Edit', event: 'EditBookmark', icon: 'edit'}); - BM.addMenuEntry({value: 'Delete', globalEvent: 'DeleteBookmarkItem', icon: 'delete', separator: 'top'}); - content.appendChild(BM.DOMRoot); - - // Session Context Menu - var SM = new ContextMenu({name : 'SessionContextMenu'}); - SM.addMenuEntry({value: 'Restore', event: 'RestoreSession', icon: 'same-window'}); - SM.addMenuEntry({value: 'Restore (new window)', event: 'RestoreInNewWindow', icon: 'new-window'}); - SM.addMenuEntry({value: 'Restore (close active)', event: 'ReplaceSession', icon: 'replace-current'}); - SM.addMenuEntry({value: 'Restore (close all)', event: 'ReplaceSessionAll', icon: 'replace-all'}); - SM.addMenuEntry({value: 'Edit', event: 'EditSession', icon: 'edit'}); - SM.addMenuEntry({value: 'Delete', globalEvent: 'DeleteBookmarkItem', icon: 'delete'}); - SM.addMenuEntry({value: 'Create new session', callback: SessionManager.createNewSession, icon: 'new-session', separator: 'top'}); - SM.addMenuEntry({value: 'Insert separator', callback: SessionManager.createSeparator, icon: 'new-session'}); - content.appendChild(SM.DOMRoot); - - var SCBM = new ContextMenu({name : 'SessionContextBookmarkMenu'}); - SCBM.addMenuEntry({value: 'Edit', event: 'EditBookmark', icon: 'edit'}); - SCBM.addMenuEntry({value: 'Delete', globalEvent: 'DeleteBookmarkItem', icon: 'delete'}); - SCBM.addMenuEntry({value: 'Create new session', callback: SessionManager.createNewSession, icon: 'new-session', separator: 'top'}); - SCBM.addMenuEntry({value: 'Insert separator', callback: SessionManager.createSeparator, icon: 'new-session'}); - content.appendChild(SCBM.DOMRoot); - - var SCSM = new ContextMenu({name : 'SessionContextSeparatorMenu'}); - SCSM.addMenuEntry({value: 'Delete', globalEvent: 'DeleteBookmarkItem', icon: 'delete'}); - SCSM.addMenuEntry({value: 'Create new session', callback: SessionManager.createNewSession, icon: 'new-session', separator: 'top'}); - SCSM.addMenuEntry({value: 'Insert separator', callback: SessionManager.createSeparator, icon: 'new-session'}); - content.appendChild(SCSM.DOMRoot); - - var SLM = new ContextMenu({name : 'SessionListMenu'}); - SLM.addMenuEntry({value: 'Create new session', callback: SessionManager.createNewSession, icon: 'new-session'}); - content.appendChild(SLM.DOMRoot); - - // History List Context Menu - var confirmDeleteAll = function confirmDeleteAll(context, event) - { - WindowEvents.emit(document, 'ConfirmBox-Open', { - event: event, - message: 'Clear history?', - callback : function() { - GlobalEvents.emit('HistorySessionDeleteAll'); - } - }); - }; - - var HSML = new ContextMenu({name : 'HistoryListCtxMenu'}); - HSML.addMenuEntry({value: 'Delete All', callback: confirmDeleteAll, icon: 'delete'}); - content.appendChild(HSML.DOMRoot); - - // History Items Context Menu - - var confirmDeleteOne = function confirmDeleteOne(context, event) - { - WindowEvents.emit(document, 'ConfirmBox-Open', { - event: event, - message: 'Confirm delete?', - callback : function() { - GlobalEvents.emit('HistorySessionDelete', context.getAttribute('index')); - } - }); - }; - - var HSM = new ContextMenu({name : 'HistorySessionCtxMenu'}); - HSM.addMenuEntry({value: 'Save', event: 'Save', icon: 'heart'}); - HSM.addMenuEntry({value: 'Restore (new window)', event: 'Restore', icon: 'new-window'}); - HSM.addMenuEntry({value: 'Delete', callback: confirmDeleteOne, icon: 'delete', separator: 'top'}); - content.appendChild(HSM.DOMRoot); - - // History Context Menu - var HBM = new ContextMenu({name : 'HistoryMarksCtxMenu'}); - HBM.addMenuEntry({value: 'Open (new tab)', event: 'OpenInNewTab', icon: 'new-tab'}); - if (AppConfig.isPanel()) { - HBM.addMenuEntry({value: 'Open (current tab)', event: 'OpenInActiveTab', icon: 'same-tab'}); - } - HBM.addMenuEntry({value: 'Open (new window)', event: 'OpenInNewWindow', icon: 'new-window'}); - HBM.addMenuEntry({value: 'Copy URL', event: 'CopyURL', icon: 'copy'}); - content.appendChild(HBM.DOMRoot); - - // Field edit widgets - var BEP = new FieldEditWdiget({name: 'BookmarkEditWidget'}); - BEP.addField({label: 'Name', name: 'title'}); - BEP.addField({label: 'Location', name: 'url'}); - // BEP.addField({label: 'Description', name: 'desc'}); - centralArea.appendChild(BEP.DOMRoot); - - // Field edit Info - var SEP = new FieldEditWdiget({name: 'SessionFolderEditWidget'}); - SEP.addField({label: 'Title', name: 'title'}); - // SEP.addField({label: 'Description', name: 'desc'}); - centralArea.appendChild(SEP.DOMRoot); - - // Confirm Box - var CB = new ConfirmBox({name: 'ConfirmBox'}); - content.appendChild(CB.DOMRoot); - - // ------------------------------------------------------------------------ - // Panel States - - var UI = content.parentElement; - function setUIState(props) - { - var model = SessionSyncModel.getModel(document); - for (var key in props) { - model.state[key] = props[key]; - props[key] !== undefined ? UI.setAttribute(key, props[key]) : UI.removeAttribute(key); + // ------------------------------------------------------------------------ + // UI Modules + + var configPanel = new ConfigPanel(); + content.appendChild(configPanel.DOMRoot); + + var tooltipSystem = new TooltipSystem(); + content.appendChild(tooltipSystem.DOMRoot); + + var notificationSystem = new NotificationSystem(); + content.appendChild(notificationSystem.DOMRoot); + + var hotekyManager = new SessionHotkeyManager(); + content.appendChild(hotekyManager.DOMRoot); + + var overlaySystem = new OverlaySystem(); + content.appendChild(overlaySystem.DOMRoot); + + // ------------------------------------------------------------- + // Create the UI + var centralArea = DomElem('div', { class: 'sessions-area' }); + + // ------------------------------------------------------------- + // Clipboard + + new Clipboard(); + + // ------------------------------------------------------------- + // GlobalHotkeys + + new SessionHotkeys(); + + // -------------------------------------------------------------------- + // Center area + + var headerBar = new SessionHeaderBar(); + var toolbar = new SessionToolbar(); + var sessionList = new SessionList(); + var searchBar = new SessionFiltering(); + var sessionContainer = new SessionContainer(); + var sessionSorting = new SessionSorting(); + + headerBar.DOMRoot.appendChild(searchBar.DOMRoot); + sessionList.DOMRoot.appendChild(sessionSorting.DOMRoot); + + var body = DomElem('div', { class: 'sessions-area-body' }); + body.appendChild(sessionList.DOMRoot); + body.appendChild(sessionContainer.DOMRoot); + + var resizeHandle = DomElem('div', { class: 'panel-resize-handle' }); + + // ------------------------------------------------------------- + // Trash can + + var trashCan = new TrashCan(); + centralArea.appendChild(trashCan.DOMRoot); + + // -------------------------------------------------------------------- + // URL bar + + var urlBar = new UrlBar(); + headerBar.DOMRoot.appendChild(urlBar.DOMRoot); + + centralArea.appendChild(resizeHandle); + centralArea.appendChild(headerBar.DOMRoot); + centralArea.appendChild(toolbar.DOMRoot); + centralArea.appendChild(body); + content.appendChild(centralArea); + + // ------------------------------------------------------------------------ + // Context Menues + + // Bookmark Item Context Menu + var BM = new ContextMenu({ name: 'BookmarkCtxMenu' }); + BM.addMenuEntry({ value: 'Open (new tab)', event: 'OpenInNewTab', icon: 'new-tab' }); + if (AppConfig.isPanel()) { + BM.addMenuEntry({ value: 'Open (current tab)', event: 'OpenInActiveTab', icon: 'same-tab' }); + } + BM.addMenuEntry({ value: 'Open (new window)', event: 'OpenInNewWindow', icon: 'new-window' }); + BM.addMenuEntry({ value: 'Copy URL', event: 'CopyURL', icon: 'copy' }); + BM.addMenuEntry({ value: 'Copy Title', event: 'CopyTitle', icon: 'copy' }); + BM.addMenuEntry({ value: 'Edit', event: 'EditBookmark', icon: 'edit' }); + BM.addMenuEntry({ value: 'Delete', globalEvent: 'DeleteBookmarkItem', icon: 'delete', separator: 'top' }); + content.appendChild(BM.DOMRoot); + + // Session Context Menu + var SM = new ContextMenu({ name: 'SessionContextMenu' }); + SM.addMenuEntry({ value: 'Restore', event: 'RestoreSession', icon: 'same-window' }); + SM.addMenuEntry({ value: 'Restore (new window)', event: 'RestoreInNewWindow', icon: 'new-window' }); + SM.addMenuEntry({ value: 'Restore (close active)', event: 'ReplaceSession', icon: 'replace-current' }); + SM.addMenuEntry({ value: 'Restore (close all)', event: 'ReplaceSessionAll', icon: 'replace-all' }); + SM.addMenuEntry({ value: 'Edit', event: 'EditSession', icon: 'edit' }); + SM.addMenuEntry({ value: 'Delete', globalEvent: 'DeleteBookmarkItem', icon: 'delete' }); + SM.addMenuEntry({ value: 'Create new session', callback: SessionManager.createNewSession, icon: 'new-session', separator: 'top' }); + SM.addMenuEntry({ value: 'Insert separator', callback: SessionManager.createSeparator, icon: 'new-session' }); + content.appendChild(SM.DOMRoot); + + var SCBM = new ContextMenu({ name: 'SessionContextBookmarkMenu' }); + SCBM.addMenuEntry({ value: 'Edit', event: 'EditBookmark', icon: 'edit' }); + SCBM.addMenuEntry({ value: 'Delete', globalEvent: 'DeleteBookmarkItem', icon: 'delete' }); + SCBM.addMenuEntry({ value: 'Create new session', callback: SessionManager.createNewSession, icon: 'new-session', separator: 'top' }); + SCBM.addMenuEntry({ value: 'Insert separator', callback: SessionManager.createSeparator, icon: 'new-session' }); + content.appendChild(SCBM.DOMRoot); + + var SCSM = new ContextMenu({ name: 'SessionContextSeparatorMenu' }); + SCSM.addMenuEntry({ value: 'Delete', globalEvent: 'DeleteBookmarkItem', icon: 'delete' }); + SCSM.addMenuEntry({ value: 'Create new session', callback: SessionManager.createNewSession, icon: 'new-session', separator: 'top' }); + SCSM.addMenuEntry({ value: 'Insert separator', callback: SessionManager.createSeparator, icon: 'new-session' }); + content.appendChild(SCSM.DOMRoot); + + var SLM = new ContextMenu({ name: 'SessionListMenu' }); + SLM.addMenuEntry({ value: 'Create new session', callback: SessionManager.createNewSession, icon: 'new-session' }); + content.appendChild(SLM.DOMRoot); + + // History List Context Menu + var confirmDeleteAll = function confirmDeleteAll(context, event) { + WindowEvents.emit(document, 'ConfirmBox-Open', { + event: event, + message: 'Clear history?', + callback: function () { + GlobalEvents.emit('HistorySessionDeleteAll'); } - } - - // ------------------------------------------------------------------------ - // App Events - - WindowEvents.on(document, 'SetUIState', setUIState); - - WindowEvents.on(document, 'CopyURL', function (url) { - WindowEvents.emit(document, 'ShowUrlBar', url); - WindowEvents.emit(document, 'UrlBar-CopyURL'); }); + }; - WindowEvents.on(document, 'CopyText', function () { - WindowEvents.emit(document, 'UrlBar-CopyURL'); - }); + var HSML = new ContextMenu({ name: 'HistoryListCtxMenu' }); + HSML.addMenuEntry({ value: 'Delete All', callback: confirmDeleteAll, icon: 'delete' }); + content.appendChild(HSML.DOMRoot); - // SessionContextMenu events - - WindowEvents.on(document, 'SessionContextMenu-EditSession', function (sessionID) { - if (SessionSyncModel.bookmarks[sessionID]) - { - var type = SessionSyncModel.bookmarks[sessionID].type; - switch (type) - { - case 'folder': - WindowEvents.emit(document, 'SessionFolderEditWidget-Invoke', { - context : sessionID, - fields : { - title : SessionSyncModel.bookmarks[sessionID].title, - } - }); - break; - - case 'bookmark': - WindowEvents.emit(document, 'BookmarkCtxMenu-EditBookmark', sessionID); - break; - } + // History Items Context Menu + + var confirmDeleteOne = function confirmDeleteOne(context, event) { + WindowEvents.emit(document, 'ConfirmBox-Open', { + event: event, + message: 'Confirm delete?', + callback: function () { + GlobalEvents.emit('HistorySessionDelete', context.getAttribute('index')); } }); + }; - WindowEvents.on(document, 'SessionContextMenu-RestoreSession', SessionManager.restoreSession); - WindowEvents.on(document, 'SessionContextMenu-RestoreInNewWindow', SessionManager.restoreNewWindow); - WindowEvents.on(document, 'SessionContextMenu-ReplaceSession', SessionManager.replaceSession); - WindowEvents.on(document, 'SessionContextMenu-ReplaceSessionAll', SessionManager.replaceSessionAll); - - // BookmarksCtxMenu events - - function InvokeBookmarkEdit(bookmarkID) - { - WindowEvents.emit(document, 'BookmarkEditWidget-Invoke', { - context : bookmarkID, - fields : { - title : SessionSyncModel.bookmarks[bookmarkID].title, - url : SessionSyncModel.bookmarks[bookmarkID].url, - } - }); + var HSM = new ContextMenu({ name: 'HistorySessionCtxMenu' }); + HSM.addMenuEntry({ value: 'Save', event: 'Save', icon: 'heart' }); + HSM.addMenuEntry({ value: 'Restore (new window)', event: 'Restore', icon: 'new-window' }); + HSM.addMenuEntry({ value: 'Delete', callback: confirmDeleteOne, icon: 'delete', separator: 'top' }); + content.appendChild(HSM.DOMRoot); + + // History Context Menu + var HBM = new ContextMenu({ name: 'HistoryMarksCtxMenu' }); + HBM.addMenuEntry({ value: 'Open (new tab)', event: 'OpenInNewTab', icon: 'new-tab' }); + if (AppConfig.isPanel()) { + HBM.addMenuEntry({ value: 'Open (current tab)', event: 'OpenInActiveTab', icon: 'same-tab' }); + } + HBM.addMenuEntry({ value: 'Open (new window)', event: 'OpenInNewWindow', icon: 'new-window' }); + HBM.addMenuEntry({ value: 'Copy URL', event: 'CopyURL', icon: 'copy' }); + content.appendChild(HBM.DOMRoot); + + // Field edit widgets + var BEP = new FieldEditWdiget({ name: 'BookmarkEditWidget' }); + BEP.addField({ label: 'Name', name: 'title' }); + BEP.addField({ label: 'Location', name: 'url' }); + // BEP.addField({label: 'Description', name: 'desc'}); + centralArea.appendChild(BEP.DOMRoot); + + // Field edit Info + var SEP = new FieldEditWdiget({ name: 'SessionFolderEditWidget' }); + SEP.addField({ label: 'Title', name: 'title' }); + // SEP.addField({label: 'Description', name: 'desc'}); + centralArea.appendChild(SEP.DOMRoot); + + // Confirm Box + var CB = new ConfirmBox({ name: 'ConfirmBox' }); + content.appendChild(CB.DOMRoot); + + // ------------------------------------------------------------------------ + // Panel States + + var UI = content.parentElement; + function setUIState(props) { + var model = SessionSyncModel.getModel(document); + for (var key in props) { + model.state[key] = props[key]; + props[key] !== undefined ? UI.setAttribute(key, props[key]) : UI.removeAttribute(key); } + } + + // ------------------------------------------------------------------------ + // App Events + + WindowEvents.on(document, 'SetUIState', setUIState); + + WindowEvents.on(document, 'CopyURL', function (url) { + WindowEvents.emit(document, 'ShowUrlBar', url); + WindowEvents.emit(document, 'UrlBar-CopyURL'); + }); + + WindowEvents.on(document, 'CopyText', function () { + WindowEvents.emit(document, 'UrlBar-CopyURL'); + }); + + // SessionContextMenu events + + WindowEvents.on(document, 'SessionContextMenu-EditSession', function (sessionID) { + if (SessionSyncModel.bookmarks[sessionID]) { + var type = SessionSyncModel.bookmarks[sessionID].type; + switch (type) { + case 'folder': + WindowEvents.emit(document, 'SessionFolderEditWidget-Invoke', { + context: sessionID, + fields: { + title: SessionSyncModel.bookmarks[sessionID].title, + } + }); + break; + + case 'bookmark': + WindowEvents.emit(document, 'BookmarkCtxMenu-EditBookmark', sessionID); + break; + } + } + }); - WindowEvents.on(document, 'SessionContextBookmarkMenu-EditBookmark', InvokeBookmarkEdit); - WindowEvents.on(document, 'BookmarkCtxMenu-EditBookmark', InvokeBookmarkEdit); + WindowEvents.on(document, 'SessionContextMenu-RestoreSession', SessionManager.restoreSession); + WindowEvents.on(document, 'SessionContextMenu-RestoreInNewWindow', SessionManager.restoreNewWindow); + WindowEvents.on(document, 'SessionContextMenu-ReplaceSession', SessionManager.replaceSession); + WindowEvents.on(document, 'SessionContextMenu-ReplaceSessionAll', SessionManager.replaceSessionAll); - WindowEvents.on(document, 'BookmarkCtxMenu-CopyURL', function (bookmarkID) { - WindowEvents.emit(document, 'CopyURL', SessionSyncModel.bookmarks[bookmarkID].url); - }); + // BookmarksCtxMenu events - WindowEvents.on(document, 'BookmarkCtxMenu-CopyTitle', function (bookmarkID) { - WindowEvents.emit(document, 'CopyToClipboard', SessionSyncModel.bookmarks[bookmarkID].title); + function InvokeBookmarkEdit(bookmarkID) { + WindowEvents.emit(document, 'BookmarkEditWidget-Invoke', { + context: bookmarkID, + fields: { + title: SessionSyncModel.bookmarks[bookmarkID].title, + url: SessionSyncModel.bookmarks[bookmarkID].url, + } }); - - WindowEvents.on(document, 'BookmarkCtxMenu-OpenInNewTab', function (bookmarkID) { - var mark = SessionSyncModel.bookmarks[bookmarkID]; - var parsed = BookmarkManager.parseContainerFromTitle(mark.title); - BookmarkManager.openBookmark({ - url: mark.url, - mode: 'newTab', - favicon: true, - cookieStoreId: parsed.cookieStoreId, - }); + } + + WindowEvents.on(document, 'SessionContextBookmarkMenu-EditBookmark', InvokeBookmarkEdit); + WindowEvents.on(document, 'BookmarkCtxMenu-EditBookmark', InvokeBookmarkEdit); + + WindowEvents.on(document, 'BookmarkCtxMenu-CopyURL', function (bookmarkID) { + WindowEvents.emit(document, 'CopyURL', SessionSyncModel.bookmarks[bookmarkID].url); + }); + + WindowEvents.on(document, 'BookmarkCtxMenu-CopyTitle', function (bookmarkID) { + WindowEvents.emit(document, 'CopyToClipboard', SessionSyncModel.bookmarks[bookmarkID].title); + }); + + WindowEvents.on(document, 'BookmarkCtxMenu-OpenInNewTab', function (bookmarkID) { + var mark = SessionSyncModel.bookmarks[bookmarkID]; + var parsed = BookmarkManager.parseContainerFromTitle(mark.title); + BookmarkManager.openBookmark({ + url: mark.url, + mode: 'newTab', + favicon: true, + cookieStoreId: parsed.cookieStoreId, }); - - WindowEvents.on(document, 'BookmarkCtxMenu-OpenInActiveTab', function (bookmarkID) { - var mark = SessionSyncModel.bookmarks[bookmarkID]; - var parsed = BookmarkManager.parseContainerFromTitle(mark.title); - BookmarkManager.openBookmark({ - url: mark.url, - mode: 'activeTab', - favicon: true, - cookieStoreId: parsed.cookieStoreId, - }); + }); + + WindowEvents.on(document, 'BookmarkCtxMenu-OpenInActiveTab', function (bookmarkID) { + var mark = SessionSyncModel.bookmarks[bookmarkID]; + var parsed = BookmarkManager.parseContainerFromTitle(mark.title); + BookmarkManager.openBookmark({ + url: mark.url, + mode: 'activeTab', + favicon: true, + cookieStoreId: parsed.cookieStoreId, }); - - WindowEvents.on(document, 'BookmarkCtxMenu-OpenInNewWindow', function (bookmarkID) { - var mark = SessionSyncModel.bookmarks[bookmarkID]; - var parsed = BookmarkManager.parseContainerFromTitle(mark.title); - BookmarkManager.openBookmark({ - url: mark.url, - mode: 'newWindow', - favicon: true, - cookieStoreId: parsed.cookieStoreId, - }); + }); + + WindowEvents.on(document, 'BookmarkCtxMenu-OpenInNewWindow', function (bookmarkID) { + var mark = SessionSyncModel.bookmarks[bookmarkID]; + var parsed = BookmarkManager.parseContainerFromTitle(mark.title); + BookmarkManager.openBookmark({ + url: mark.url, + mode: 'newWindow', + favicon: true, + cookieStoreId: parsed.cookieStoreId, }); + }); - // HistoryMarksCtxMenu events + // HistoryMarksCtxMenu events - WindowEvents.on(document, 'HistoryMarksCtxMenu-CopyURL', function (url) { - WindowEvents.emit(document, 'CopyURL', url); - }); + WindowEvents.on(document, 'HistoryMarksCtxMenu-CopyURL', function (url) { + WindowEvents.emit(document, 'CopyURL', url); + }); - WindowEvents.on(document, 'HistoryMarksCtxMenu-OpenInNewTab', function (url) { - BookmarkManager.openBookmark({ - url: url, - mode: 'newTab' - }); + WindowEvents.on(document, 'HistoryMarksCtxMenu-OpenInNewTab', function (url) { + BookmarkManager.openBookmark({ + url: url, + mode: 'newTab' }); + }); - WindowEvents.on(document, 'HistoryMarksCtxMenu-OpenInActiveTab', function (url) { - BookmarkManager.openBookmark({ - url: url, - mode: 'activeTab' - }); + WindowEvents.on(document, 'HistoryMarksCtxMenu-OpenInActiveTab', function (url) { + BookmarkManager.openBookmark({ + url: url, + mode: 'activeTab' }); + }); - WindowEvents.on(document, 'HistoryMarksCtxMenu-OpenInNewWindow', function (url) { - BookmarkManager.openBookmark({ - url: url, - mode: 'newWindow' - }); + WindowEvents.on(document, 'HistoryMarksCtxMenu-OpenInNewWindow', function (url) { + BookmarkManager.openBookmark({ + url: url, + mode: 'newWindow' }); + }); - // HistorySessionCtxMenu events + // HistorySessionCtxMenu events - WindowEvents.on(document, 'HistorySessionCtxMenu-Save', function (context) { - WindowEvents.emit(document, 'HistorySessionSave', context.getAttribute('index')); - }); + WindowEvents.on(document, 'HistorySessionCtxMenu-Save', function (context) { + WindowEvents.emit(document, 'HistorySessionSave', context.getAttribute('index')); + }); - WindowEvents.on(document, 'HistorySessionCtxMenu-Restore', function (context) { - WindowEvents.emit(document, 'HistorySessionRestore', context.getAttribute('index')); - }); + WindowEvents.on(document, 'HistorySessionCtxMenu-Restore', function (context) { + WindowEvents.emit(document, 'HistorySessionRestore', context.getAttribute('index')); + }); - WindowEvents.on(document, 'HistorySessionCtxMenu-Delete', function (context) { - GlobalEvents.emit('HistorySessionDelete', context.getAttribute('index')); - }); + WindowEvents.on(document, 'HistorySessionCtxMenu-Delete', function (context) { + GlobalEvents.emit('HistorySessionDelete', context.getAttribute('index')); + }); - // Edit events + // Edit events - function updateBookmarkInfo(data) - { - SessionSyncModel.updateBookmarkItem(data.context, data.fields); - } + function updateBookmarkInfo(data) { + SessionSyncModel.updateBookmarkItem(data.context, data.fields); + } - WindowEvents.on(document, 'SessionFolderEditWidget-Save', updateBookmarkInfo); - WindowEvents.on(document, 'BookmarkEditWidget-Save', updateBookmarkInfo); + WindowEvents.on(document, 'SessionFolderEditWidget-Save', updateBookmarkInfo); + WindowEvents.on(document, 'BookmarkEditWidget-Save', updateBookmarkInfo); - var startResize = function startResize(e) - { - var startPosX = e.clientX; - var startPosY = e.clientY; - var initWidth = document.body.clientWidth; - var initHeight = document.body.clientHeight; + var startResize = function startResize(e) { + var startPosX = e.clientX; + var startPosY = e.clientY; + var initWidth = document.body.clientWidth; + var initHeight = document.body.clientHeight; - resizeHandle.setAttribute('resizing', 'true'); - WindowEvents.emit(document, 'SetUIState', {resize: 'true'}); + resizeHandle.setAttribute('resizing', 'true'); + WindowEvents.emit(document, 'SetUIState', { resize: 'true' }); - function eventMove(e) - { - var deltaX = e.clientX - startPosX; - var deltaY = e.clientY - startPosY; + function eventMove(e) { + var deltaX = e.clientX - startPosX; + var deltaY = e.clientY - startPosY; - var newWidth = Math.max(Math.min(initWidth - deltaX, 800), 500); - var newHeight = Math.max(Math.min(initHeight + deltaY, 600), 400); + var newWidth = Math.max(Math.min(initWidth - deltaX, 800), 500); + var newHeight = Math.max(Math.min(initHeight + deltaY, 600), 400); - // This is required only for when document body is changing at the same time - // such is the case with Webextensions panels - initWidth = newWidth; + // This is required only for when document body is changing at the same time + // such is the case with Webextensions panels + initWidth = newWidth; - AppConfig.set('style.panel.width', newWidth); - AppConfig.set('style.panel.height', newHeight); - } + AppConfig.set('style.panel.width', newWidth); + AppConfig.set('style.panel.height', newHeight); + } - function evenUp() - { - WindowEvents.emit(document, 'SetUIState', {resize: undefined}); - resizeHandle.removeAttribute('resizing'); - document.removeEventListener('mouseup', evenUp); - document.removeEventListener('mousemove', eventMove); - } + function evenUp() { + WindowEvents.emit(document, 'SetUIState', { resize: undefined }); + resizeHandle.removeAttribute('resizing'); + document.removeEventListener('mouseup', evenUp); + document.removeEventListener('mousemove', eventMove); + } - document.addEventListener('mouseup', evenUp); - document.addEventListener('mousemove', eventMove); - }; + document.addEventListener('mouseup', evenUp); + document.addEventListener('mousemove', eventMove); + }; - if (AppConfig.isPanel()) - { - AppConfig.onChange('style.panel.width', function (size) { - document.body.style.width = size + 'px'; - }); + if (AppConfig.isPanel()) { + AppConfig.onChange('style.panel.width', function (size) { + document.body.style.width = size + 'px'; + }); - AppConfig.onChange('style.panel.height', function (size) { - document.body.style.height = size + 'px'; - }); + AppConfig.onChange('style.panel.height', function (size) { + document.body.style.height = size + 'px'; + }); - resizeHandle.addEventListener('mousedown', startResize); - } - else - { - window.addEventListener('resize', function () { - var newWidth = Math.max(window.innerWidth, 500); - var newHeight = Math.max(window.innerHeight, 400); + resizeHandle.addEventListener('mousedown', startResize); + } + else { + window.addEventListener('resize', function () { + var newWidth = Math.max(window.innerWidth, 500); + var newHeight = Math.max(window.innerHeight, 400); - browser.storage.local.set({'style.window.detach' : { + browser.storage.local.set({ + 'style.window.detach': { // left: window.screenX, // top: window.screenY, width: newWidth, height: newHeight - }}); + } }); - document.body.style.minWidth = '500px'; - } - - function preventDefault(e) { - e.preventDefault(); - e.stopPropagation(); - } - - document.addEventListener('contextmenu', preventDefault); - document.addEventListener('mousedown', function (e) { - // Middle Click - if (e.button == 1) - preventDefault(e); }); + document.body.style.minWidth = '500px'; + } - // ------------------------------------------------------------------------ - // App Init + function preventDefault(e) { + e.preventDefault(); + e.stopPropagation(); + } - }; + document.body.addEventListener('contextmenu', preventDefault); + document.body.addEventListener('mousedown', function (e) { + // Middle Click + if (e.button == 1) + preventDefault(e); + }); - // ************************************************************************ - // Module exports + // ------------------------------------------------------------------------ + // App Init - exports.SessionSyncUI = SessionSyncUI; -}); +}; diff --git a/data/scripts/session/bookmarks.js b/data/scripts/session/bookmarks.js index 6d74517..d0732d0 100644 --- a/data/scripts/session/bookmarks.js +++ b/data/scripts/session/bookmarks.js @@ -1,204 +1,202 @@ -define(function(require, exports) { - 'use strict'; +'use strict'; - // ************************************************************************ - // Modules +// ************************************************************************ +// Modules - const { AppConfig } = require('../config'); +import { AppConfig } from '../config'; - // ************************************************************************ - // API +// ************************************************************************ +// API - var BookmarkManager = (function BookmarkManager() { +export const BookmarkManager = (function BookmarkManager() { - var urlParser = document.createElement('a'); - var lazyLoadingUrl = browser.extension.getURL('data/lazy/lazy.html'); + var lazyLoadingUrl = browser.runtime.getURL('data/lazy/lazy.html'); - function getLazyLoadingParameters(url) - { - let paramater = {}; - let params = url.split('?')[1].split('&'); - for (let p of params) { - paramater[p.split('=')[0]] = decodeURIComponent(p.split('=')[1]); - } - return paramater; + function getLazyLoadingParameters(url) + { + let paramater = {}; + let splits = url.split('?'); + if (splits.length < 2) return paramater; + let query = splits[1]; + let params = query.split('&'); + for (let p of params) { + paramater[p.split('=')[0]] = decodeURIComponent(p.split('=')[1]); } - - var getHostName = function getHostName(url) - { - urlParser.href = url; - return urlParser.hostname; - }; - - var getFaviconUrl = function getFaviconUrl(favIconUrl, callback) - { - var key = '@favIconUrl:' + getHostName(favIconUrl); - browser.storage.local.get(key) - .then(function (obj) { - if (obj[key]) { - callback(obj[key]); - } - }); - }; - - var getFolderBookmarks = function getFolderBookmarks(folderID, callback) - { - var result = browser.bookmarks.getChildren(folderID); - result.then(callback, function fail() { - console.log('Error getting bookmarks from folder ID: ', folderID); - }); - }; - - var searchBookmarks = function searchBookmarks(keyword) - { - return browser.bookmarks.search(keyword); - }; - - var getOpenMode = function getOpenMode(index) - { - var buttons = ['click', 'middleClick']; - var state = AppConfig.get('bookmark.' + buttons[index] + '.newTab'); - return state ? 'newTab' : 'activeTab'; - }; - - var containerSuffix = { - encode: function(id) { return ' [container:' + id + ']'; }, - regex: /^(.*) \[container:([^\]]+)\]$/, - default: 'firefox-default', - }; - - var createBookmarkFromTab = function createBookmarkFromTab(tab, parentId) - { - var url = tab.url; - if (url.startsWith(lazyLoadingUrl, 0)) - { - var info = getLazyLoadingParameters(url); - url = info.url; - } - else - { - browser.runtime.sendMessage({event: 'save-favicon', tab: tab}); - } - - var title = tab.title; - if (tab.cookieStoreId && tab.cookieStoreId !== containerSuffix.default) - { - title += containerSuffix.encode(tab.cookieStoreId); + return paramater; + } + + var getHostName = function getHostName(url) + { + try { + return new URL(url).hostname; + } catch (e) { + return ''; + } + }; + + var getFaviconUrl = function getFaviconUrl(favIconUrl, callback) + { + var key = '@favIconUrl:' + getHostName(favIconUrl); + browser.storage.local.get(key) + .then(function (obj) { + if (obj[key]) { + callback(obj[key]); } - - return browser.bookmarks.create({ - url: url, - title: title, - parentId: parentId - }); - - }; - - // Parse a [container:] suffix from a bookmark title. - // Returns { title, cookieStoreId } — cookieStoreId is null when absent. - var parseContainerFromTitle = function parseContainerFromTitle(rawTitle) + }); + }; + + var getFolderBookmarks = function getFolderBookmarks(folderID, callback) + { + var result = browser.bookmarks.getChildren(folderID); + result.then(callback, function fail() { + console.log('Error getting bookmarks from folder ID: ', folderID); + }); + }; + + var searchBookmarks = function searchBookmarks(keyword) + { + return browser.bookmarks.search(keyword); + }; + + var getOpenMode = function getOpenMode(index) + { + var buttons = ['click', 'middleClick']; + var state = AppConfig.get('bookmark.' + buttons[index] + '.newTab'); + return state ? 'newTab' : 'activeTab'; + }; + + var containerSuffix = { + encode: function(id) { return ' [container:' + id + ']'; }, + regex: /^(.*) \[container:([^\]]+)\]$/, + default: 'firefox-default', + }; + + var createBookmarkFromTab = function createBookmarkFromTab(tab, parentId) + { + var url = tab.url; + if (url.startsWith(lazyLoadingUrl, 0)) { - var match = rawTitle && rawTitle.match(containerSuffix.regex); - if (match) { - return { title: match[1], cookieStoreId: match[2] }; - } - return { title: rawTitle, cookieStoreId: null }; - }; + var info = getLazyLoadingParameters(url); + url = info.url; + } + else + { + browser.runtime.sendMessage({event: 'save-favicon', tab: tab}); + } - var createBookmark = function createBookmark(options) + var title = tab.title; + if (tab.cookieStoreId && tab.cookieStoreId !== containerSuffix.default) { - return browser.bookmarks.create(options); - }; + title += containerSuffix.encode(tab.cookieStoreId); + } - var openBookmark = function openBookmark(options) + return browser.bookmarks.create({ + url: url, + title: title, + parentId: parentId + }); + + }; + + // Parse a [container:] suffix from a bookmark title. + // Returns { title, cookieStoreId } — cookieStoreId is null when absent. + var parseContainerFromTitle = function parseContainerFromTitle(rawTitle) + { + var match = rawTitle && rawTitle.match(containerSuffix.regex); + if (match) { + return { title: match[1], cookieStoreId: match[2] }; + } + return { title: rawTitle, cookieStoreId: null }; + }; + + var createBookmark = function createBookmark(options) + { + // parentId possible values + // root________ + // menu________ + // toolbar_____ + // unfiled_____ + + return browser.bookmarks.create(options); + }; + + var openBookmark = function openBookmark(options) + { + if (AppConfig.isPanel() == false) { - if (AppConfig.isPanel() == false) + if (options.mode == 'activeTab') { - if (options.mode == 'activeTab') - { - options.mode = 'newTab'; - options.active = false; - } + options.mode = 'newTab'; + options.active = false; } + } - browser.runtime.sendMessage({event: 'open-tab', options: options}); - }; - - var setTitle = function setTitle(bookmark, title, callback) + browser.runtime.sendMessage({event: 'open-tab', options: options}); + }; + + var setTitle = function setTitle(bookmark, title, callback) + { + browser.bookmarks.update(bookmark.id, { + title: title + }).then(function success() { + bookmark.title = title; + bookmark.title_lowercase = title.toLowerCase(); + if (typeof callback === 'function') + callback(true); + }, function error() { + throw 'error'; + }); + }; + + var setLocation = function setLocation(bookmark, url) + { + browser.bookmarks.update(bookmark.id, { + url: url + }).then(function success() { + bookmark.url = url; + }, function error() { + throw 'error'; + }); + }; + + var moveItem = function moveItem(itemID, newParentID, index, callback, errorCallback) + { + browser.bookmarks.move(itemID, { + parentId: newParentID, + index: index + }) + .then(callback, errorCallback); + }; + + var deleteItem = function deleteItem(itemID, isFolder, callback, errorCallback) + { + if (isFolder) { - browser.bookmarks.update(bookmark.id, { - title: title - }).then(function success() { - bookmark.title = title; - bookmark.title_lowercase = title.toLowerCase(); - if (typeof callback === 'function') - callback(true); - }, function error() { - throw 'error'; - }); - }; - - var setLocation = function setLocation(bookmark, url) - { - browser.bookmarks.update(bookmark.id, { - url: url - }).then(function success() { - bookmark.url = url; - }, function error() { - throw 'error'; - }); - }; - - var moveItem = function moveItem(itemID, newParentID, index, callback, errorCallback) - { - browser.bookmarks.move(itemID, { - parentId: newParentID, - index: index - }) + browser.bookmarks.removeTree(itemID) .then(callback, errorCallback); - }; - - var deleteItem = function deleteItem(itemID, isFolder, callback, errorCallback) + } + else { - if (isFolder) - { - browser.bookmarks.removeTree(itemID) - .then(callback, errorCallback); - } - else - { - browser.bookmarks.remove(itemID) - .then(callback, errorCallback); - } - }; - - return { - getFaviconUrl: getFaviconUrl, - getOpenMode: getOpenMode, - openBookmark: openBookmark, - createBookmark: createBookmark, - createBookmarkFromTab: createBookmarkFromTab, - parseContainerFromTitle: parseContainerFromTitle, - searchBookmarks: searchBookmarks, - - setTitle: setTitle, - setLocation: setLocation, - moveItem: moveItem, - deleteItem: deleteItem, - - getFolderBookmarks : getFolderBookmarks - }; - - })(); - - // ************************************************************************ - // Events - - // ************************************************************************ - // Init - - // ************************************************************************ - // Module exports - - exports.BookmarkManager = BookmarkManager; -}); \ No newline at end of file + browser.bookmarks.remove(itemID) + .then(callback, errorCallback); + } + }; + + return { + getFaviconUrl: getFaviconUrl, + getOpenMode: getOpenMode, + openBookmark: openBookmark, + createBookmark: createBookmark, + createBookmarkFromTab: createBookmarkFromTab, + parseContainerFromTitle: parseContainerFromTitle, + searchBookmarks: searchBookmarks, + + setTitle: setTitle, + setLocation: setLocation, + moveItem: moveItem, + deleteItem: deleteItem, + + getFolderBookmarks : getFolderBookmarks + }; + +})(); \ No newline at end of file diff --git a/data/scripts/session/management.js b/data/scripts/session/management.js index 6fa938d..9c9f4a7 100644 --- a/data/scripts/session/management.js +++ b/data/scripts/session/management.js @@ -1,425 +1,418 @@ -define(function(require, exports) { - 'use strict'; - - // ***************************************************************************** - // Custom Modules - - const { AppConfig } = require('../config'); - const { BookmarkManager } = require('./bookmarks'); - const { WindowEvents, GlobalEvents } = require('../utils/global-events'); - const { LimitCounter } = require('../utils/general'); - const { SessionSyncModel } = require ('../components/session-sync-model'); - - // ***************************************************************************** - - var SessionManager = (function SessionManager() { - - var trackTabs = true; - - var getCurrentTab = function getCurrentTab(callback) - { - browser.tabs.query({active: true, windowId: browser.windows.WINDOW_ID_CURRENT}) - .then(function(tabs) { - callback(tabs[0]); - }); - }; - - var getWindowTab = function getWindowTab(tabIndex, windowId, callback) - { - browser.tabs.query({index: tabIndex, windowId: windowId}) - .then(function(tabs) { - callback(tabs[0]); - }); - }; - - var getCurrentWindow = function getCurrentWindow(callback) { - browser.windows.getCurrent({ populate: true }) - .then(callback); - }; - - var getAllWindows = function getAllWindows(callback) { - browser.windows.getAll({ - populate: true, - windowTypes: ['normal'] - }) - .then(callback); - }; - - var tabTracking = function tabTracking() - { - return trackTabs; - }; - - var moveTab = function moveTab(tabID, newIndex, windowID) - { - trackTabs = false; - browser.tabs.move([tabID], { - index: newIndex, - windowId: windowID - }).then(function() { - GlobalEvents.emit('UpdateCurrentSession'); - trackTabs = true; - }, function onError() { - trackTabs = true; - }); - }; - - var activateTab = function activateTab(tabID) - { - browser.tabs.update(tabID, { - active: true - }); - }; - - var closeTab = function closeTab(tabID) - { - browser.tabs.remove(tabID); - }; - - var activateWindow = function activateWindow(windowID) - { - browser.windows.update(windowID, { - focused: true - }); - }; - - var saveActiveSession = function saveActiveSession() - { - getCurrentWindow(function(mozWindow) { - - var sessionID = 0; - var sessionCount = 0; - - function updateOnSave() - { - WindowEvents.emit(document, 'SetPromiseSession', { - update: true, - sessionID: sessionID, - edit: sessionCount == 1, - }); - } - - // Get non-private windows - if (mozWindow.incognito == true || AppConfig.get('session.save').allWindows == false) - { - var sessionTitle = (new Date()).toLocaleString(); - saveWindowSession({ - title: sessionTitle, - tabs: mozWindow.tabs - }, function onSuccess(folder) { - sessionID = folder.id; - sessionCount++; - updateOnSave(); - }); - } - else - { - // Get all non private windows - getAllWindows(function(windows) { - // Save windows as separate sessions - var date = new Date(); - - windows.forEach(function (mozWindow) { - if (mozWindow.incognito == false) { - sessionCount++; - } - }); - - var windowID = 0; - var updateEvent = new LimitCounter(sessionCount, updateOnSave); - - windows.forEach(function (mozWindow) { - if (mozWindow.incognito == false) - { - windowID++; - let sessionTitle = date.toLocaleString() + ((sessionCount > 1) ? (' #' + windowID) : ''); - saveWindowSession({ - title: sessionTitle, - tabs: mozWindow.tabs, - }, function(folder) { - sessionID = folder.id; - updateEvent.advance(); - }); - } - }); - }); - } - }); - }; - - var mergeSessions = function mergeSessions(sessionID) - { - // Exclude all pages that are already saved - getCurrentWindow(function (mozWindow) { - - var toSave = {}; - - mozWindow.tabs.forEach(tab => { - let url = tab.url; - toSave[url] = tab; - }); - - // Exclude all pages that are already saved - BookmarkManager.getFolderBookmarks(sessionID, function(bookmarks) { - bookmarks.forEach(function (bookmark) { - delete toSave[bookmark.url]; - }); - - var updateEvent = new LimitCounter(Object.keys(toSave).length, function () { - WindowEvents.emit(document, 'ViewSession', sessionID); - }); - - // TODO - reverse saving order - var savingList = []; - for (let key in toSave) - { - let tab = toSave[key]; - savingList.push(tab); - } - - for (let i = savingList.length - 1; i >= 0; i--) - { - var tab = savingList[i]; - BookmarkManager.createBookmarkFromTab(tab, sessionID) - .then(updateEvent.advance, updateEvent.advance); - } - }); - }); - }; - - var overwriteSession = function overwriteSession(sessionID) - { - var toRemove = []; - - BookmarkManager.getFolderBookmarks(sessionID, function(bookmarks) { - - // Remove all bookmarks from the current session - bookmarks.forEach(function (bookmark) { - toRemove.push(bookmark.id); - }); - - SessionSyncModel.deleteBookmarkList(toRemove, sessionID, function () { - getCurrentWindow(function(mozWindow) { - - var updateFinish = new LimitCounter(mozWindow.tabs.length, function () { - WindowEvents.emit(document, 'ViewSession', sessionID); - }); - - // TODO - add option to discard duplicate tabs and or pinned tabs - - for (let i = mozWindow.tabs.length - 1; i >= 0; i--) - { - var tab = mozWindow.tabs[i]; - BookmarkManager.createBookmarkFromTab(tab, sessionID) - .then(updateFinish.advance, updateFinish.advance); - } - }); - }); - }); - }; - - var saveHistorySession = function saveHistorySession(sessionInfo) - { - var windowCount = sessionInfo.windows.length; - - var windowID = 0; - - function onSave(bookmark) { - WindowEvents.emit(document, 'SetPromiseSession', { - sessionID: bookmark.id, - edit: false, - update: true - }); - } - - // save all history windows - for (var key in sessionInfo.windows) - { - var date = new Date(sessionInfo.lastSave); - var sessionTitle = date.toLocaleString() + ((windowCount > 1) ? (' #' + windowID) : ''); - windowID++; - - saveWindowSession({ - title: sessionTitle, - tabs: sessionInfo.windows[key].tabs - }, onSave); - } - }; - - var saveWindowSession = function saveWindowSession(sessionInfo, callback) - { - BookmarkManager.createBookmark({ - title: sessionInfo.title, - parentId: AppConfig.get('storageID') - }) - .then(function (folder) { - - var trackSaving = new LimitCounter(sessionInfo.tabs.length, function () { - callback(folder); - }); - - // TODO - add option to discard duplicate tabs - - var savePinned = AppConfig.get('session.save').pinned; - - for (let i = sessionInfo.tabs.length - 1; i >= 0; i--) - { - let tab = sessionInfo.tabs[i]; - if (tab.pinned && savePinned == false) - { - trackSaving.offsetLimit(-1); - } - else - { - BookmarkManager.createBookmarkFromTab(tab, folder.id) - .then(trackSaving.advance, trackSaving.advance); - } - } - }); - }; - - var restoreSession = function restoreSession(folderID) - { - browser.runtime.sendMessage({event: 'restore-session', bookmarkID: folderID}); - }; - - var restoreNewWindow = function restoreNewWindow(folderID) - { - var options = { - newWindow: true - }; - browser.runtime.sendMessage({event: 'restore-session', bookmarkID: folderID, options: options}); - }; - - var restoreAdvanced = function restoreAdvanced(folderID, options) - { - browser.runtime.sendMessage({event: 'restore-session', bookmarkID: folderID, options: options}); - }; - - var replaceSession = function replaceSession(folderID) - { - getCurrentWindow(function(mozWindow) { - var options = { - newWindow: true, - closeWindow: mozWindow.id, - }; - restoreAdvanced(folderID, options); - }); - }; - - var replaceSessionAll = function replaceSessionAll(folderID) - { - var options = { - newWindow: true, - closeAll: true, - }; - restoreAdvanced(folderID, options); - }; - - var loadBookmarksNewWindow = function loadBookmarksNewWindow(windows) - { - browser.runtime.sendMessage({event: 'restore-sessions', windows: windows }); - }; - - var fixLazySession = function fixLazySession(folderID) - { - browser.runtime.sendMessage({event: 'fix-lazy-session', bookmarkID: folderID }); - }; - - var createSeparator = function createSeparator(folderID) { - browser.bookmarks.get(folderID) - .then(function (marks) { - BookmarkManager.createBookmark({ - index: marks[0].index + 1, - type: 'separator', - parentId: AppConfig.get('storageID') - }) - .then(function success(separator) { - WindowEvents.emit(document, 'SetPromiseSession', { sessionID: separator.id, update: true }); - }); - }, function onError(msg) { - console.log(msg); - }); - }; - - var createSession = function createSession(title, index) - { - BookmarkManager.createBookmark({ - index: index, - title: title, - parentId: AppConfig.get('storageID') - }) - .then(function success(session) { - WindowEvents.emit(document, 'SetPromiseSession', { - sessionID: session.id, - edit: true, - update: true - }); - }, function error() { - }); - }; - - var createNewSession = function createNewSession(folderID) { - var sessionTitle = (new Date()).toLocaleString(); - if (folderID) - { - browser.bookmarks.get(folderID) - .then(function (marks) { - createSession(sessionTitle, marks[0].index + 1); - }); - } - else - { - createSession(sessionTitle); - } - }; - - // Events - GlobalEvents.on('CreateNewSession', function(name) { - var title = name ? name : (new Date()).toLocaleString(); - createSession(title); - }); - - // Events - GlobalEvents.on('management.close-tab', function(tabID) { - trackTabs = false; - browser.tabs.remove(tabID) - .then(function () { - GlobalEvents.emit('UpdateCurrentSession'); - trackTabs = true; - }); - }); - - // Public API - return { - moveTab: moveTab, - closeTab: closeTab, - activateTab: activateTab, - tabTracking: tabTracking, - activateWindow: activateWindow, - getWindowTab: getWindowTab, - getCurrentTab: getCurrentTab, - getCurrentWindow: getCurrentWindow, - getAllWindows: getAllWindows, - restoreSession: restoreSession, - replaceSession: replaceSession, - restoreAdvanced: restoreAdvanced, - replaceSessionAll: replaceSessionAll, - fixLazySession: fixLazySession, - restoreNewWindow: restoreNewWindow, - loadBookmarksNewWindow: loadBookmarksNewWindow, - mergeSessions: mergeSessions, - overwriteSession: overwriteSession, - saveActiveSession: saveActiveSession, - createSeparator: createSeparator, - createNewSession: createNewSession, - saveHistorySession: saveHistorySession, - }; - - })(); - - - // ***************************************************************************** - // Public API - exports.SessionManager = SessionManager; -}); \ No newline at end of file +'use strict'; + +// ***************************************************************************** +// Custom Modules + +import { AppConfig } from '../config'; +import { BookmarkManager } from './bookmarks'; +import { WindowEvents, GlobalEvents } from '../utils/global-events'; +import { LimitCounter } from '../utils/general'; +import { SessionSyncModel } from '../components/session-sync-model'; + +// ***************************************************************************** + +export const SessionManager = (function SessionManager() { + + var trackTabs = true; + + var getCurrentTab = function getCurrentTab(callback) + { + browser.tabs.query({active: true, windowId: browser.windows.WINDOW_ID_CURRENT}) + .then(function(tabs) { + callback(tabs[0]); + }); + }; + + var getWindowTab = function getWindowTab(tabIndex, windowId, callback) + { + browser.tabs.query({index: tabIndex, windowId: windowId}) + .then(function(tabs) { + callback(tabs[0]); + }); + }; + + var getCurrentWindow = function getCurrentWindow(callback) { + browser.windows.getCurrent({ populate: true }) + .then(callback); + }; + + var getAllWindows = function getAllWindows(callback) { + browser.windows.getAll({ + populate: true, + windowTypes: ['normal'] + }) + .then(callback); + }; + + var tabTracking = function tabTracking() + { + return trackTabs; + }; + + var moveTab = function moveTab(tabID, newIndex, windowID) + { + trackTabs = false; + browser.tabs.move([tabID], { + index: newIndex, + windowId: windowID + }).then(function() { + GlobalEvents.emit('UpdateCurrentSession'); + trackTabs = true; + }, function onError() { + trackTabs = true; + }); + }; + + var activateTab = function activateTab(tabID) + { + browser.tabs.update(tabID, { + active: true + }); + }; + + var closeTab = function closeTab(tabID) + { + browser.tabs.remove(tabID); + }; + + var activateWindow = function activateWindow(windowID) + { + browser.windows.update(windowID, { + focused: true + }); + }; + + var saveActiveSession = function saveActiveSession() + { + getCurrentWindow(function(mozWindow) { + + var sessionID = 0; + var sessionCount = 0; + + function updateOnSave() + { + WindowEvents.emit(document, 'SetPromiseSession', { + update: true, + sessionID: sessionID, + edit: sessionCount == 1, + }); + } + + // Get non-private windows + if (mozWindow.incognito == true || AppConfig.get('session.save').allWindows == false) + { + var sessionTitle = (new Date()).toLocaleString(); + saveWindowSession({ + title: sessionTitle, + tabs: mozWindow.tabs + }, function onSuccess(folder) { + sessionID = folder.id; + sessionCount++; + updateOnSave(); + }); + } + else + { + // Get all non private windows + getAllWindows(function(windows) { + // Save windows as separate sessions + var date = new Date(); + + windows.forEach(function (mozWindow) { + if (mozWindow.incognito == false) { + sessionCount++; + } + }); + + var windowID = 0; + var updateEvent = new LimitCounter(sessionCount, updateOnSave); + + windows.forEach(function (mozWindow) { + if (mozWindow.incognito == false) + { + windowID++; + let sessionTitle = date.toLocaleString() + ((sessionCount > 1) ? (' #' + windowID) : ''); + saveWindowSession({ + title: sessionTitle, + tabs: mozWindow.tabs, + }, function(folder) { + sessionID = folder.id; + updateEvent.advance(); + }); + } + }); + }); + } + }); + }; + + var mergeSessions = function mergeSessions(sessionID) + { + // Exclude all pages that are already saved + getCurrentWindow(function (mozWindow) { + + var toSave = {}; + + mozWindow.tabs.forEach(tab => { + let url = tab.url; + toSave[url] = tab; + }); + + // Exclude all pages that are already saved + BookmarkManager.getFolderBookmarks(sessionID, function(bookmarks) { + bookmarks.forEach(function (bookmark) { + delete toSave[bookmark.url]; + }); + + var updateEvent = new LimitCounter(Object.keys(toSave).length, function () { + WindowEvents.emit(document, 'ViewSession', sessionID); + }); + + // TODO - reverse saving order + var savingList = []; + for (let key in toSave) + { + let tab = toSave[key]; + savingList.push(tab); + } + + for (let i = savingList.length - 1; i >= 0; i--) + { + var tab = savingList[i]; + BookmarkManager.createBookmarkFromTab(tab, sessionID) + .then(updateEvent.advance, updateEvent.advance); + } + }); + }); + }; + + var overwriteSession = function overwriteSession(sessionID) + { + var toRemove = []; + + BookmarkManager.getFolderBookmarks(sessionID, function(bookmarks) { + + // Remove all bookmarks from the current session + bookmarks.forEach(function (bookmark) { + toRemove.push(bookmark.id); + }); + + SessionSyncModel.deleteBookmarkList(toRemove, sessionID, function () { + getCurrentWindow(function(mozWindow) { + + var updateFinish = new LimitCounter(mozWindow.tabs.length, function () { + WindowEvents.emit(document, 'ViewSession', sessionID); + }); + + // TODO - add option to discard duplicate tabs and or pinned tabs + + for (let i = mozWindow.tabs.length - 1; i >= 0; i--) + { + var tab = mozWindow.tabs[i]; + BookmarkManager.createBookmarkFromTab(tab, sessionID) + .then(updateFinish.advance, updateFinish.advance); + } + }); + }); + }); + }; + + var saveHistorySession = function saveHistorySession(sessionInfo) + { + var windowCount = sessionInfo.windows.length; + + var windowID = 0; + + function onSave(bookmark) { + WindowEvents.emit(document, 'SetPromiseSession', { + sessionID: bookmark.id, + edit: false, + update: true + }); + } + + // save all history windows + for (var key in sessionInfo.windows) + { + var date = new Date(sessionInfo.lastSave); + var sessionTitle = date.toLocaleString() + ((windowCount > 1) ? (' #' + windowID) : ''); + windowID++; + + saveWindowSession({ + title: sessionTitle, + tabs: sessionInfo.windows[key].tabs + }, onSave); + } + }; + + var saveWindowSession = function saveWindowSession(sessionInfo, callback) + { + BookmarkManager.createBookmark({ + title: sessionInfo.title, + parentId: AppConfig.get('storageID') + }) + .then(function (folder) { + + var trackSaving = new LimitCounter(sessionInfo.tabs.length, function () { + callback(folder); + }); + + // TODO - add option to discard duplicate tabs + + var savePinned = AppConfig.get('session.save').pinned; + + for (let i = sessionInfo.tabs.length - 1; i >= 0; i--) + { + let tab = sessionInfo.tabs[i]; + if (tab.pinned && savePinned == false) + { + trackSaving.offsetLimit(-1); + } + else + { + BookmarkManager.createBookmarkFromTab(tab, folder.id) + .then(trackSaving.advance, trackSaving.advance); + } + } + }); + }; + + var restoreSession = function restoreSession(folderID) + { + browser.runtime.sendMessage({event: 'restore-session', bookmarkID: folderID}); + }; + + var restoreNewWindow = function restoreNewWindow(folderID) + { + var options = { + newWindow: true + }; + browser.runtime.sendMessage({event: 'restore-session', bookmarkID: folderID, options: options}); + }; + + var restoreAdvanced = function restoreAdvanced(folderID, options) + { + browser.runtime.sendMessage({event: 'restore-session', bookmarkID: folderID, options: options}); + }; + + var replaceSession = function replaceSession(folderID) + { + getCurrentWindow(function(mozWindow) { + var options = { + newWindow: true, + closeWindow: mozWindow.id, + }; + restoreAdvanced(folderID, options); + }); + }; + + var replaceSessionAll = function replaceSessionAll(folderID) + { + var options = { + newWindow: true, + closeAll: true, + }; + restoreAdvanced(folderID, options); + }; + + var loadBookmarksNewWindow = function loadBookmarksNewWindow(windows) + { + browser.runtime.sendMessage({event: 'restore-sessions', windows: windows }); + }; + + var fixLazySession = function fixLazySession(folderID) + { + browser.runtime.sendMessage({event: 'fix-lazy-session', bookmarkID: folderID }); + }; + + var createSeparator = function createSeparator(folderID) { + browser.bookmarks.get(folderID) + .then(function (marks) { + BookmarkManager.createBookmark({ + index: marks[0].index + 1, + type: 'separator', + parentId: AppConfig.get('storageID') + }) + .then(function success(separator) { + WindowEvents.emit(document, 'SetPromiseSession', { sessionID: separator.id, update: true }); + }); + }, function onError(msg) { + console.log(msg); + }); + }; + + var createSession = function createSession(title, index) + { + BookmarkManager.createBookmark({ + index: index, + title: title, + parentId: AppConfig.get('storageID') + }) + .then(function success(session) { + WindowEvents.emit(document, 'SetPromiseSession', { + sessionID: session.id, + edit: true, + update: true + }); + }, function error() { + }); + }; + + var createNewSession = function createNewSession(folderID) { + var sessionTitle = (new Date()).toLocaleString(); + if (folderID) + { + browser.bookmarks.get(folderID) + .then(function (marks) { + createSession(sessionTitle, marks[0].index + 1); + }); + } + else + { + createSession(sessionTitle); + } + }; + + // Events + GlobalEvents.on('CreateNewSession', function(name) { + var title = name ? name : (new Date()).toLocaleString(); + createSession(title); + }); + + // Events + GlobalEvents.on('management.close-tab', function(tabID) { + trackTabs = false; + browser.tabs.remove(tabID) + .then(function () { + GlobalEvents.emit('UpdateCurrentSession'); + trackTabs = true; + }); + }); + + // Public API + return { + moveTab: moveTab, + closeTab: closeTab, + activateTab: activateTab, + tabTracking: tabTracking, + activateWindow: activateWindow, + getWindowTab: getWindowTab, + getCurrentTab: getCurrentTab, + getCurrentWindow: getCurrentWindow, + getAllWindows: getAllWindows, + restoreSession: restoreSession, + replaceSession: replaceSession, + restoreAdvanced: restoreAdvanced, + replaceSessionAll: replaceSessionAll, + fixLazySession: fixLazySession, + restoreNewWindow: restoreNewWindow, + loadBookmarksNewWindow: loadBookmarksNewWindow, + mergeSessions: mergeSessions, + overwriteSession: overwriteSession, + saveActiveSession: saveActiveSession, + createSeparator: createSeparator, + createNewSession: createNewSession, + saveHistorySession: saveHistorySession, + }; + +})(); \ No newline at end of file diff --git a/data/scripts/template.js b/data/scripts/template.js deleted file mode 100644 index 39d5c90..0000000 --- a/data/scripts/template.js +++ /dev/null @@ -1,26 +0,0 @@ -define(function(require, exports, module) { - 'use strict'; - - // ************************************************************************ - // Modules - - var ModuleName = require('./path/module-name'); - - // ************************************************************************ - // API - - function init() { - - } - - // ************************************************************************ - // Events - - // ************************************************************************ - // Init - - // ************************************************************************ - // Module exports - - exports.init = init; -}); diff --git a/data/scripts/utils/auto-scroll.js b/data/scripts/utils/auto-scroll.js index 6687d97..f6a21e3 100644 --- a/data/scripts/utils/auto-scroll.js +++ b/data/scripts/utils/auto-scroll.js @@ -1,94 +1,94 @@ -define(function(require, exports) { - 'use strict'; +'use strict'; - // ***************************************************************************** - // AutoScroll +// ***************************************************************************** +// AutoScroll - var AutoScroll = (function AutoScroll() { +export const AutoScroll = (function AutoScroll() { - var autoScroll = false; - var prevScroll = 0; - var scrollSpeed = 0; - var scrollingNode; - var scrollTarget; - var scrollDir = 0; + var autoScroll = false; + var prevScroll = 0; + var scrollSpeed = 0; + var scrollingNode; + var scrollTarget; + var scrollDir = 0; - function scrollingAnimation() + function scrollingAnimation() + { + if (autoScroll == false) { - if (autoScroll == false) - { - return; - } + return; + } - prevScroll = scrollingNode.scrollTop; - scrollingNode.scrollTop += scrollSpeed; + prevScroll = scrollingNode.scrollTop; + scrollingNode.scrollTop += scrollSpeed; - if (scrollingNode.scrollTop != prevScroll) - { - if (scrollTarget != undefined) { - if (scrollDir * scrollingNode.scrollTop > scrollTarget) { - scrollingNode.scrollTop = scrollDir * scrollTarget; - autoScroll = false; - return; - } + if (scrollingNode.scrollTop != prevScroll) + { + if (scrollTarget != undefined) { + if (scrollDir * scrollingNode.scrollTop > scrollTarget) { + scrollingNode.scrollTop = scrollDir * scrollTarget; + autoScroll = false; + return; } - requestAnimationFrame(scrollingAnimation); } - else - { - autoScroll = false; + if (typeof requestAnimationFrame !== 'undefined') { + requestAnimationFrame(scrollingAnimation); } } - - // scroll a DOM node with a specified speed - function scroll(node, speed) + else { - scrollSpeed = speed / 10; + autoScroll = false; + } + } - if (autoScroll == true) - { - return; - } + // scroll a DOM node with a specified speed + function scroll(node, speed) + { + scrollSpeed = speed / 10; + + if (autoScroll == true) + { + return; + } - scrollTarget = undefined; - autoScroll = true; - scrollingNode = node; + scrollTarget = undefined; + autoScroll = true; + scrollingNode = node; + if (typeof requestAnimationFrame !== 'undefined') { requestAnimationFrame(scrollingAnimation); } + } - function stop() - { - autoScroll = false; + function stop() + { + autoScroll = false; + } + + function scrollTo(node, offset, time) + { + if (time == 0) { + node.scrollTop = offset; + return; } - function scrollTo(node, offset, time) + if (autoScroll == true) { - if (time == 0) { - node.scrollTop = offset; - return; - } - - if (autoScroll == true) - { - return; - } + return; + } - autoScroll = true; - scrollingNode = node; - scrollSpeed = (offset - node.scrollTop) / (time * 60); - scrollDir = scrollSpeed > 0 ? 1 : -1; - scrollTarget = offset * scrollDir; + autoScroll = true; + scrollingNode = node; + scrollSpeed = (offset - node.scrollTop) / (time * 60); + scrollDir = scrollSpeed > 0 ? 1 : -1; + scrollTarget = offset * scrollDir; + if (typeof requestAnimationFrame !== 'undefined') { requestAnimationFrame(scrollingAnimation); } + } - return { - scroll : scroll, - scrollTo: scrollTo, - stop : stop - }; - })(); - - // ***************************************************************************** - // Public API - exports.AutoScroll = AutoScroll; -}); \ No newline at end of file + return { + scroll : scroll, + scrollTo: scrollTo, + stop : stop + }; +})(); \ No newline at end of file diff --git a/data/scripts/utils/components.js b/data/scripts/utils/components.js index ede7ee7..dc8418c 100644 --- a/data/scripts/utils/components.js +++ b/data/scripts/utils/components.js @@ -1,369 +1,347 @@ -define(function(require, exports) { - 'use strict'; +'use strict'; - // ***************************************************************************** - // Custom Modules +// ***************************************************************************** +// Custom Modules - const { HTMLCreator } = require('./dom'); - const JSUtils = require('./general'); +import { HTMLCreator } from './dom'; +import * as JSUtils from './general'; - // ***************************************************************************** - // API +// ***************************************************************************** +// API - /* - * Toggle switch - */ +/* +* Toggle switch +*/ - function ToggleSwitch(options) - { - var DomElem = HTMLCreator(); +export function ToggleSwitch(options) { + var DomElem = HTMLCreator(); - var state = options.state; + var state = options.state; - var entry = DomElem('div', {class: 'comp-toggle-switch'}); - var title = DomElem('div', {class: 'label'}); + var entry = DomElem('div', { class: 'comp-toggle-switch' }); + var title = DomElem('div', { class: 'label' }); - if (options.attribute) { - entry.setAttribute(options.attribute, ''); - } - - if (options.description) { - title.textContent = options.description; - } - - var button = DomElem('div', {class: 'switch'}); - button.setAttribute('state', state); - button.textContent = state ? options.onState : options.offState; + if (options.attribute) { + entry.setAttribute(options.attribute, ''); + } - if (options.tooltip) { - entry.setAttribute('tooltip', options.tooltip); - title.setAttribute('tooltip', options.tooltip); - button.setAttribute('tooltip', options.tooltip); - } + if (options.description) { + title.textContent = options.description; + } - entry.appendChild(title); - entry.appendChild(button); + var button = DomElem('div', { class: 'switch' }); + button.setAttribute('state', state); + button.textContent = state ? options.onState : options.offState; - function setState(newState) - { - if (state == newState) - return; + if (options.tooltip) { + entry.setAttribute('tooltip', options.tooltip); + title.setAttribute('tooltip', options.tooltip); + button.setAttribute('tooltip', options.tooltip); + } - state = newState; - options.callback(state); - button.setAttribute('state', state); - button.textContent = state ? options.onState : options.offState; - } + entry.appendChild(title); + entry.appendChild(button); - entry.addEventListener('click', function() { - setState(!state); - }); + function setState(newState) { + if (state == newState) + return; - this.setState = setState; - this.DOMRoot = entry; + state = newState; + options.callback(state); + button.setAttribute('state', state); + button.textContent = state ? options.onState : options.offState; } - function ToggleButton(options) - { - var DomElem = HTMLCreator(); + entry.addEventListener('click', function () { + setState(!state); + }); - // ------------------------------------------------------------------------ - // Create UI + this.setState = setState; + this.DOMRoot = entry; +} - var state = options.state; - var callback = JSUtils.getValidFunction(options.callback); +export function ToggleButton(options) { + var DomElem = HTMLCreator(); - var container = DomElem('div', {class: 'comp-toggle-button'}); - var info = DomElem('div', {class: 'label'}); - info.textContent = options.description; + // ------------------------------------------------------------------------ + // Create UI - var button = DomElem('div', {class: 'button'}); - button.setAttribute('state', state); - button.textContent = state ? options.onState : options.offState; + var state = options.state; + var callback = JSUtils.getValidFunction(options.callback); - container.appendChild(info); - container.appendChild(button); + var container = DomElem('div', { class: 'comp-toggle-button' }); + var info = DomElem('div', { class: 'label' }); + info.textContent = options.description; - // ------------------------------------------------------------------------ - // Events + var button = DomElem('div', { class: 'button' }); + button.setAttribute('state', state); + button.textContent = state ? options.onState : options.offState; - var toggle = function toggle() { - setValue(!state, true); - }; + container.appendChild(info); + container.appendChild(button); - var setValue = function setValue(value, trigger) { - if (value != state) { - state = value; - button.setAttribute('state', state); - button.textContent = state ? options.onState : options.offState; - if (trigger) { - callback(state); - } - } - }; + // ------------------------------------------------------------------------ + // Events - button.addEventListener('click', toggle); + var toggle = function toggle() { + setValue(!state, true); + }; - // ------------------------------------------------------------------------ - // Public properties + var setValue = function setValue(value, trigger) { + if (value != state) { + state = value; + button.setAttribute('state', state); + button.textContent = state ? options.onState : options.offState; + if (trigger) { + callback(state); + } + } + }; - this.toggle = toggle; - this.setValue = setValue; - this.DOMRoot = container; - } + button.addEventListener('click', toggle); - /* - * Action Button - */ + // ------------------------------------------------------------------------ + // Public properties - function ActionButton(options) - { - var DomElem = HTMLCreator(); + this.toggle = toggle; + this.setValue = setValue; + this.DOMRoot = container; +} - var entry = DomElem('div', {class: 'comp-button'}); - var title = DomElem('div', {class: 'label'}); +/* +* Action Button +*/ - if (options.description) { - title.textContent = options.description; - } +export function ActionButton(options) { + var DomElem = HTMLCreator(); - var button = DomElem('div', {class: 'button'}); - button.textContent = options.value; + var entry = DomElem('div', { class: 'comp-button' }); + var title = DomElem('div', { class: 'label' }); - if (options.tooltip) { - entry.setAttribute('tooltip', options.tooltip); - title.setAttribute('tooltip', options.tooltip); - button.setAttribute('tooltip', options.tooltip); - } + if (options.description) { + title.textContent = options.description; + } - entry.appendChild(title); - entry.appendChild(button); + var button = DomElem('div', { class: 'button' }); + button.textContent = options.value; - function setValue(value) - { - button.textContent = value; - } + if (options.tooltip) { + entry.setAttribute('tooltip', options.tooltip); + title.setAttribute('tooltip', options.tooltip); + button.setAttribute('tooltip', options.tooltip); + } - function getValue() - { - return button.textContent; - } + entry.appendChild(title); + entry.appendChild(button); - button.addEventListener('click', function() { - options.callback(); - }); + function setValue(value) { + button.textContent = value; + } - this.setValue = setValue; - this.getValue = getValue; - this.DOMRoot = entry; + function getValue() { + return button.textContent; } - /* - * RageControl - */ + button.addEventListener('click', function () { + options.callback(); + }); - function RangeControl(options) - { - var DomElem = HTMLCreator(); + this.setValue = setValue; + this.getValue = getValue; + this.DOMRoot = entry; +} - // ------------------------------------------------------------------------ - // Create UI +/* +* RageControl +*/ - var value = options.value; - var step = options.step ? options.step : 1; - var callback = JSUtils.getValidFunction(options.onChange); +export function RangeControl(options) { + var DomElem = HTMLCreator(); - var root = DomElem('div', {class: 'comp-range-control'}); + // ------------------------------------------------------------------------ + // Create UI - var sectionInfo = DomElem('div', {class: 'label'}); - sectionInfo.textContent = options.description; + var value = options.value; + var step = options.step ? options.step : 1; + var callback = JSUtils.getValidFunction(options.onChange); - var container = DomElem('div', {class: 'range-input'}); - var incButton = DomElem('div', {class: 'button inc'}); - var decButton = DomElem('div', {class: 'button dec'}); - var valueBox = DomElem('input', {type: 'text', class: 'value'}); - valueBox.value = value; + var root = DomElem('div', { class: 'comp-range-control' }); - root.appendChild(sectionInfo); - root.appendChild(container); - container.appendChild(decButton); - container.appendChild(valueBox); - container.appendChild(incButton); + var sectionInfo = DomElem('div', { class: 'label' }); + sectionInfo.textContent = options.description; - var setValue = function setValue(newValue, trigger) - { - // revent update trigger with the same value - if (value == newValue) { - return; - } + var container = DomElem('div', { class: 'range-input' }); + var incButton = DomElem('div', { class: 'button inc' }); + var decButton = DomElem('div', { class: 'button dec' }); + var valueBox = DomElem('input', { type: 'text', class: 'value' }); + valueBox.value = value; - // restrict to specified range - if (options.minValue > newValue) { newValue = options.minValue; } - if (options.maxValue < newValue) { newValue = options.maxValue; } + root.appendChild(sectionInfo); + root.appendChild(container); + container.appendChild(decButton); + container.appendChild(valueBox); + container.appendChild(incButton); - if (newValue % step != 0) { - newValue -= newValue % step; - } + var setValue = function setValue(newValue, trigger) { + // revent update trigger with the same value + if (value == newValue) { + return; + } - // update value - var diff = newValue - value; - value = newValue; - valueBox.value = newValue; + // restrict to specified range + if (options.minValue > newValue) { newValue = options.minValue; } + if (options.maxValue < newValue) { newValue = options.maxValue; } - if (trigger) { - callback(newValue, diff); - } - }; + if (newValue % step != 0) { + newValue -= newValue % step; + } - var onChange = function onChange() - { - var newValue = valueBox.value | 0; - if (parseInt(valueBox.value) != newValue) { - valueBox.value = value; - return; - } + // update value + var diff = newValue - value; + value = newValue; + valueBox.value = newValue; - setValue(newValue, true); - }; + if (trigger) { + callback(newValue, diff); + } + }; - valueBox.addEventListener('change', function() { - onChange(true); - }); + var onChange = function onChange() { + var newValue = valueBox.value | 0; + if (parseInt(valueBox.value) != newValue) { + valueBox.value = value; + return; + } - incButton.addEventListener('click', function() { - setValue(value + step, true); - }); + setValue(newValue, true); + }; - decButton.addEventListener('click', function() { - setValue(value - step, true); - }); + valueBox.addEventListener('change', function () { + onChange(true); + }); - // ------------------------------------------------------------------------ - // Public properties + incButton.addEventListener('click', function () { + setValue(value + step, true); + }); - this.DOMRoot = root; - this.setValue = setValue; - } + decButton.addEventListener('click', function () { + setValue(value - step, true); + }); - function DropDown(options) - { - var DomElem = HTMLCreator(); + // ------------------------------------------------------------------------ + // Public properties - // ------------------------------------------------------------------------ - // Create UI + this.DOMRoot = root; + this.setValue = setValue; +} - var container = DomElem('div', {class: 'comp-dropdown'}); - if (options.id) { - container.setAttribute('id', options.id); - } +export function DropDown(options) { + var DomElem = HTMLCreator(); - var info = DomElem('div', {class: 'label'}); - info.textContent = options.description; + // ------------------------------------------------------------------------ + // Create UI - var select = DomElem('div', {class: 'select'}); - var selectValue = DomElem('div', {class: 'select-value'}); - selectValue.textContent = options.value; + var container = DomElem('div', { class: 'comp-dropdown' }); + if (options.id) { + container.setAttribute('id', options.id); + } - var dropList = DomElem('div', {class: 'drop-list', visible: 'false'}); + var info = DomElem('div', { class: 'label' }); + info.textContent = options.description; - container.appendChild(info); - container.appendChild(select); - select.appendChild(selectValue); - select.appendChild(dropList); + var select = DomElem('div', { class: 'select' }); + var selectValue = DomElem('div', { class: 'select-value' }); + selectValue.textContent = options.value; - function addOption(label, options) { - if (options == undefined) - options = {}; + var dropList = DomElem('div', { class: 'drop-list', visible: 'false' }); - var option = DomElem('div', {class: 'option'}); - option.textContent = label; - option.setAttribute('value', (options.value != undefined) ? options.value : label); + container.appendChild(info); + container.appendChild(select); + select.appendChild(selectValue); + select.appendChild(dropList); - if (options.property) { - option.setAttribute(options.property, ''); - } + function addOption(label, options) { + if (options == undefined) + options = {}; - if (options.selected === true) { - setSelected(option); - } + var option = DomElem('div', { class: 'option' }); + option.textContent = label; + option.setAttribute('value', (options.value != undefined) ? options.value : label); - dropList.appendChild(option); + if (options.property) { + option.setAttribute(options.property, ''); } - // ------------------------------------------------------------------------ - // Events + if (options.selected === true) { + setSelected(option); + } - var selectedOption; - var visible = false; - var callback = JSUtils.getValidFunction(options.onChange); + dropList.appendChild(option); + } - var setValue = function(value, trigger) { - for (var i = 0; i < dropList.children.length; i++) { - if (dropList.children[i].getAttribute('value') == value) { - setSelected(dropList.children[i], trigger); - return; - } - } - }; - - var getValue = function() { - return selectedOption.getAttribute('value'); - }; - - var setSelected = function setSelected(node, trigger) { - if (selectedOption) - selectedOption.removeAttribute('selected'); - - selectedOption = node; - - if (selectedOption) { - var value = selectedOption.getAttribute('value'); - selectValue.textContent = selectedOption.textContent; - container.setAttribute('value', value); - selectedOption.setAttribute('selected', ''); - if (trigger) { - callback(value); - } - } - }; - - var closeDropdown = function closeDropdown(e) { - e.stopPropagation(); - visible = false; - dropList.setAttribute('visible', visible); - document.removeEventListener('click', closeDropdown); - }; - - select.addEventListener('click', function(e) { - e.stopPropagation(); - visible = !visible; - dropList.setAttribute('visible', visible); - document.addEventListener('click', closeDropdown); - }); - - dropList.addEventListener('click', function(e) { - if (e.target.className == 'option') { - setSelected(e.target, true); - closeDropdown(e); + // ------------------------------------------------------------------------ + // Events + + var selectedOption; + var visible = false; + var callback = JSUtils.getValidFunction(options.onChange); + + var setValue = function (value, trigger) { + for (var i = 0; i < dropList.children.length; i++) { + if (dropList.children[i].getAttribute('value') == value) { + setSelected(dropList.children[i], trigger); + return; } - }); + } + }; - // ------------------------------------------------------------------------ - // Public properties + var getValue = function () { + return selectedOption.getAttribute('value'); + }; - this.container = container; - this.addOption = addOption; - this.setValue = setValue; - this.getValue = getValue; - } + var setSelected = function setSelected(node, trigger) { + if (selectedOption) + selectedOption.removeAttribute('selected'); - // ***************************************************************************** - // Public API + selectedOption = node; + + if (selectedOption) { + var value = selectedOption.getAttribute('value'); + selectValue.textContent = selectedOption.textContent; + container.setAttribute('value', value); + selectedOption.setAttribute('selected', ''); + if (trigger) { + callback(value); + } + } + }; + + var closeDropdown = function closeDropdown(e) { + e.stopPropagation(); + visible = false; + dropList.setAttribute('visible', visible); + document.removeEventListener('click', closeDropdown); + }; + + select.addEventListener('click', function (e) { + e.stopPropagation(); + visible = !visible; + dropList.setAttribute('visible', visible); + document.addEventListener('click', closeDropdown); + }); + + dropList.addEventListener('click', function (e) { + if (e.target.className == 'option') { + setSelected(e.target, true); + closeDropdown(e); + } + }); - exports.DropDown = DropDown; - exports.ActionButton = ActionButton; - exports.ToggleSwitch = ToggleSwitch; - exports.RangeControl = RangeControl; - exports.ToggleButton = ToggleButton; + // ------------------------------------------------------------------------ + // Public properties -}); \ No newline at end of file + this.container = container; + this.addOption = addOption; + this.setValue = setValue; + this.getValue = getValue; +} diff --git a/data/scripts/utils/confirm-box.js b/data/scripts/utils/confirm-box.js index 2837122..ee9d97a 100644 --- a/data/scripts/utils/confirm-box.js +++ b/data/scripts/utils/confirm-box.js @@ -1,101 +1,91 @@ -define(function(require, exports) { - 'use strict'; +'use strict'; - // ***************************************************************************** - // Custom Modules +// ***************************************************************************** +// Custom Modules - const { HTMLCreator } = require('./dom'); - const { WindowEvents } = require('./global-events'); +import { HTMLCreator } from './dom'; +import { WindowEvents } from './global-events'; - // ***************************************************************************** - // Bookmark Confirm Box +// ***************************************************************************** +// Bookmark Confirm Box - function ConfirmBox(options) - { - var DomElem = HTMLCreator(); +export function ConfirmBox(options) { + var DomElem = HTMLCreator(); - var panel = DomElem('div', {class : 'confirm-box'}); + var panel = DomElem('div', { class: 'confirm-box' }); - var info = DomElem('div', {class : 'info'}); - var controls = DomElem('div', {class : 'controls'}); + var info = DomElem('div', { class: 'info' }); + var controls = DomElem('div', { class: 'controls' }); - var ok = DomElem('div', {class : 'button ok'}); - ok.textContent = 'Confirm'; + var ok = DomElem('div', { class: 'button ok' }); + ok.textContent = 'Confirm'; - var cancel = DomElem('div', {class : 'button cancel'}); - cancel.textContent = 'Cancel'; + var cancel = DomElem('div', { class: 'button cancel' }); + cancel.textContent = 'Cancel'; - var flexArea1 = DomElem('div', {class : 'flex'}); - var flexArea2 = DomElem('div', {class : 'flex'}); + var flexArea1 = DomElem('div', { class: 'flex' }); + var flexArea2 = DomElem('div', { class: 'flex' }); - controls.appendChild(flexArea1); - controls.appendChild(ok); - controls.appendChild(cancel); - controls.appendChild(flexArea2); - panel.appendChild(info); - panel.appendChild(controls); + controls.appendChild(flexArea1); + controls.appendChild(ok); + controls.appendChild(cancel); + controls.appendChild(flexArea2); + panel.appendChild(info); + panel.appendChild(controls); - var callback; - var defaultCallback = function defaultCallback() {}; + var callback; + var defaultCallback = function defaultCallback() { }; - // ------------------------------------------------------------------------ - // Events + // ------------------------------------------------------------------------ + // Events - ok.addEventListener('click', function() { - callback(); - hidePanel(); - }); + ok.addEventListener('click', function () { + callback(); + hidePanel(); + }); - cancel.addEventListener('click', function() { - hidePanel(); - }); + cancel.addEventListener('click', function () { + hidePanel(); + }); - var openPanel = function openPanel(options) - { - panel.setAttribute('data-active', 'true'); - panel.style.left = options.event.pageX + 'px'; - panel.style.top = options.event.pageY + 'px'; + var openPanel = function openPanel(options) { + panel.setAttribute('data-active', 'true'); + panel.style.left = options.event.pageX + 'px'; + panel.style.top = options.event.pageY + 'px'; - document.addEventListener('mousedown', blurMenu); + document.addEventListener('mousedown', blurMenu); - info.textContent = options.message; - setCallback(options.callback); + info.textContent = options.message; + setCallback(options.callback); - }; + }; - var hidePanel = function hidePanel() - { - panel.removeAttribute('data-active'); - document.removeEventListener('mousedown', blurMenu); - callback = defaultCallback; - }; + var hidePanel = function hidePanel() { + panel.removeAttribute('data-active'); + document.removeEventListener('mousedown', blurMenu); + callback = defaultCallback; + }; - var blurMenu = function hideWhenClickOutside(e) - { - var target = e.target; - while (target) { - if (target === panel) - return; - target = target.parentElement; - } - hidePanel(); - }; - - function setCallback(func) { - callback = typeof func === 'function' ? func : defaultCallback; + var blurMenu = function hideWhenClickOutside(e) { + var target = e.target; + while (target) { + if (target === panel) + return; + target = target.parentElement; } + hidePanel(); + }; - // Register custom events - WindowEvents.on(document, options.name + '-Open', openPanel); - WindowEvents.on(document, 'ConfimBox-Hide', hidePanel); + function setCallback(func) { + callback = typeof func === 'function' ? func : defaultCallback; + } - // ------------------------------------------------------------------------ - // Public properties + // Register custom events + WindowEvents.on(document, options.name + '-Open', openPanel); + WindowEvents.on(document, 'ConfimBox-Hide', hidePanel); - this.DOMRoot = panel; - } + // ------------------------------------------------------------------------ + // Public properties - // ***************************************************************************** - // Public API - exports.ConfirmBox = ConfirmBox; -}); \ No newline at end of file + this.DOMRoot = panel; +} diff --git a/data/scripts/utils/context-menu.js b/data/scripts/utils/context-menu.js index 9dde989..26f45f1 100644 --- a/data/scripts/utils/context-menu.js +++ b/data/scripts/utils/context-menu.js @@ -1,142 +1,128 @@ -define(function(require, exports) { - 'use strict'; +'use strict'; - // ***************************************************************************** - // Custom Modules +// ***************************************************************************** +// Custom Modules - // App - const { AppConfig } = require('../config'); +// App +import { AppConfig } from '../config'; +import { HTMLCreator } from './dom'; - const { HTMLCreator } = require('./dom'); +// TODO - remove window/global events +import { WindowEvents, GlobalEvents } from './global-events'; - // TODO - remove window/global events - const { WindowEvents, GlobalEvents } = require('./global-events'); +// ***************************************************************************** +// Bookmark Context Menu - // var ID = 0; +var previousContextMenu = null; - // ***************************************************************************** - // Bookmark Context Menu +export function ContextMenu(options) { + var DomElem = HTMLCreator(); - var previousContextMenu = null; + // ------------------------------------------------------------------------ + // Create UI - function ContextMenu(options) - { - var DomElem = HTMLCreator(); - - // ------------------------------------------------------------------------ - // Create UI - - // var self = this; - var menu = DomElem('div', { class : 'context-menu' }); - if (options.width != undefined) { - menu.style.width = options.width + 'px'; - } - - var context = undefined; - - // ------------------------------------------------------------------------ - // Events - - var openMenu = function openMenu(event) - { - if (previousContextMenu && previousContextMenu != this) { - previousContextMenu.hideMenu(); - } - previousContextMenu = this; - - menu.setAttribute('data-active', 'true'); - var padding = 10; - menu.style.left = Math.min(event.pageX, window.innerWidth - menu.clientWidth - padding) + 'px'; - menu.style.top = Math.min(event.pageY, window.innerHeight - menu.clientHeight - padding) + 'px'; - - document.addEventListener('mousedown', blurMenu); + // var self = this; + var menu = DomElem('div', { class: 'context-menu' }); + if (options.width != undefined) { + menu.style.width = options.width + 'px'; + } - WindowEvents.emit(document, 'ConfimBox-Hide'); - }.bind(this); + var context = undefined; - var hideMenu = function hideMenu(e) - { - menu.removeAttribute('data-active'); - document.removeEventListener('mousedown', blurMenu); - WindowEvents.emit(document, 'OverlaySystem', { state: false }); - }; + // ------------------------------------------------------------------------ + // Events - var blurMenu = function hideWhenClickOutside(e) { - var target = e.target; - while (target) { - if (target === menu) - return; - target = target.parentElement; - } - hideMenu(); - }; - - // Actions - // TODO - investigate switch to event delegation - var NewAction = function NewAction(opt) { - if (opt.func) { - return function actionEvent(e) { - context[opt.func](e); - hideMenu(); - }; - } - if (opt.callback) { - return function actionEvent(e) { - opt.callback(context, e); - hideMenu(); - }; - } - if (opt.globalEvent) { - return function action() { - GlobalEvents.emit(opt.globalEvent, context); - hideMenu(); - }; - } + var openMenu = (event) => { + if (previousContextMenu && previousContextMenu != this) { + previousContextMenu.hideMenu(); + } + previousContextMenu = this; + + menu.setAttribute('data-active', 'true'); + var padding = 10; + menu.style.left = Math.min(event.pageX, window.innerWidth - menu.clientWidth - padding) + 'px'; + menu.style.top = Math.min(event.pageY, window.innerHeight - menu.clientHeight - padding) + 'px'; + + document.addEventListener('mousedown', blurMenu); + + WindowEvents.emit(document, 'ConfimBox-Hide'); + }; + + var hideMenu = function hideMenu(e) { + menu.removeAttribute('data-active'); + document.removeEventListener('mousedown', blurMenu); + WindowEvents.emit(document, 'OverlaySystem', { state: false }); + }; + + var blurMenu = function hideWhenClickOutside(e) { + var target = e.target; + while (target) { + if (target === menu) + return; + target = target.parentElement; + } + hideMenu(); + }; + + // Actions + // TODO - investigate switch to event delegation + var NewAction = function NewAction(opt) { + if (opt.func) { + return function actionEvent(e) { + context[opt.func](e); + hideMenu(); + }; + } + if (opt.callback) { + return function actionEvent(e) { + opt.callback(context, e); + hideMenu(); + }; + } + if (opt.globalEvent) { return function action() { - WindowEvents.emit(document, options.name + '-' + opt.event, context); + GlobalEvents.emit(opt.globalEvent, context); hideMenu(); }; + } + return function action() { + WindowEvents.emit(document, options.name + '-' + opt.event, context); + hideMenu(); }; + }; + + var addMenuEntry = function AddMenuEntry(opt) { + var event = NewAction(opt); + var entry = DomElem('div', { class: 'button' }); + entry.textContent = opt.value; + entry.setAttribute('icon', opt.icon); + menu.appendChild(entry); + + var addSeparator = opt.separator; + if (addSeparator != undefined) { + var separator = DomElem('div', { class: 'separator' }); + menu.insertBefore(separator, addSeparator == 'top' ? entry : undefined); + } - var addMenuEntry = function AddMenuEntry(opt) - { - var event = NewAction(opt); - var entry = DomElem('div', {class : 'button'}); - entry.textContent = opt.value; - entry.setAttribute('icon', opt.icon); - menu.appendChild(entry); - - var addSeparator = opt.separator; - if (addSeparator != undefined) - { - var separator = DomElem('div', {class : 'separator'}); - menu.insertBefore(separator, addSeparator == 'top' ? entry : undefined); - } - - entry.addEventListener('click', event); - }; - - // Register custom events - WindowEvents.on(document, options.name + '-Open', function(options) { - // menu.setAttribute('icons', options.showIcons ? 'yes' : 'no'); - openMenu(options.event); - context = options.context; - WindowEvents.emit(document, 'OverlaySystem', { state: true }); - }); + entry.addEventListener('click', event); + }; - AppConfig.onChange('context.menu.icons', function(value) { - menu.setAttribute('icons', value ? 'yes' : 'no'); - }); + // Register custom events + WindowEvents.on(document, options.name + '-Open', function (options) { + // menu.setAttribute('icons', options.showIcons ? 'yes' : 'no'); + openMenu(options.event); + context = options.context; + WindowEvents.emit(document, 'OverlaySystem', { state: true }); + }); - // ------------------------------------------------------------------------ - // Public properties + AppConfig.onChange('context.menu.icons', function (value) { + menu.setAttribute('icons', value ? 'yes' : 'no'); + }); - this.DOMRoot = menu; - this.hideMenu = hideMenu; - this.addMenuEntry = addMenuEntry; - } + // ------------------------------------------------------------------------ + // Public properties - // ***************************************************************************** - // Public API - exports.ContextMenu = ContextMenu; -}); \ No newline at end of file + this.DOMRoot = menu; + this.hideMenu = hideMenu; + this.addMenuEntry = addMenuEntry; +} diff --git a/data/scripts/utils/dom.js b/data/scripts/utils/dom.js index 2cdd432..878de2d 100644 --- a/data/scripts/utils/dom.js +++ b/data/scripts/utils/dom.js @@ -1,30 +1,20 @@ -define(function(require, exports, module) { - 'use strict'; +'use strict'; - const NS_HTML = 'http://www.w3.org/1999/xhtml'; +const NS_HTML = 'http://www.w3.org/1999/xhtml'; - // ------------------------------------------------------------------------ - // Generic Node Creator +// ------------------------------------------------------------------------ +// Generic Node Creator - function Node(ns, type, attrs) { - var elem = document.createElementNS(ns, type); +function Node(ns, type, attrs) { + var elem = document.createElementNS(ns, type); - attrs && Object.keys(attrs).forEach(function(attr) { - elem.setAttribute(attr, attrs[attr]); - }); + attrs && Object.keys(attrs).forEach(function(attr) { + elem.setAttribute(attr, attrs[attr]); + }); - return elem; - } + return elem; +} - function HTMLCreator() { - return Node.bind(null, NS_HTML); - } - - // ------------------------------------------------------------------------ - // Public API - - exports.HTMLCreator = HTMLCreator; -}); - -// ES6 module -// export { HTMLCreator , XULCreator }; +export function HTMLCreator() { + return Node.bind(null, NS_HTML); +} diff --git a/data/scripts/utils/drag-context.js b/data/scripts/utils/drag-context.js index 85c8c1b..bfe3ccc 100644 --- a/data/scripts/utils/drag-context.js +++ b/data/scripts/utils/drag-context.js @@ -1,46 +1,40 @@ -define(function(require, exports) { - 'use strict'; +'use strict'; - // ***************************************************************************** - // Custom Modules +// ***************************************************************************** +// Custom Modules - const { WindowEvents } = require('./global-events'); +import { WindowEvents } from './global-events'; - // ***************************************************************************** - // API +// ***************************************************************************** +// API - var DragContext = (function DragContext() { +export const DragContext = (function DragContext() { - var context = null; + var context = null; - function setContext(ctx) { - context = ctx; + function setContext(ctx) { + context = ctx; + if (typeof document !== 'undefined') { WindowEvents.emit(document, 'TrashCan-Droppable', ctx != null); } - - function getContext() { - return context; - } - - function hasContext() { - return context != null; - } - - function clearContext() { - setContext(null); - } - - return { - hasContext: hasContext, - clearContext: clearContext, - setContext: setContext, - getContext: getContext - }; - })(); - - - // ***************************************************************************** - // Public API - exports.DragContext = DragContext; - -}); \ No newline at end of file + } + + function getContext() { + return context; + } + + function hasContext() { + return context != null; + } + + function clearContext() { + setContext(null); + } + + return { + hasContext: hasContext, + clearContext: clearContext, + setContext: setContext, + getContext: getContext + }; +})(); \ No newline at end of file diff --git a/data/scripts/utils/field-edit-widget.js b/data/scripts/utils/field-edit-widget.js index 186baaa..b4d3870 100644 --- a/data/scripts/utils/field-edit-widget.js +++ b/data/scripts/utils/field-edit-widget.js @@ -1,154 +1,140 @@ -define(function(require, exports) { - 'use strict'; +'use strict'; - // ***************************************************************************** - // Custom Modules - const { HTMLCreator } = require('./dom'); - const { WindowEvents, GlobalEvents } = require('./global-events'); +// ***************************************************************************** +// Custom Modules +import { HTMLCreator } from './dom'; +import { WindowEvents, GlobalEvents } from './global-events'; - // ***************************************************************************** - // Global Variables +// ***************************************************************************** +// Global Variables - var counterID = 0; +var counterID = 0; - function FieldEditWdiget(options) - { - var DomElem = HTMLCreator(); +export function FieldEditWdiget(options) { + var DomElem = HTMLCreator(); - // ------------------------------------------------------------------------ - // Create UI + // ------------------------------------------------------------------------ + // Create UI - // Context will be the session folderID or bookmarkID - var context = undefined; - // Context data represents fields that need to be edited - var context_data = null; + // Context will be the session folderID or bookmarkID + var context = undefined; + // Context data represents fields that need to be edited + var context_data = null; - var widgetID = counterID++; - var fields = {}; - var first_field = null; + var widgetID = counterID++; + var fields = {}; + var first_field = null; - // Edit session widget - var widget = DomElem('div', {class: 'field-edit-widget'}); - var wfields = DomElem('div', {class: 'fields'}); - var save = DomElem('div', {class: 'button save'}); - var cancel = DomElem('div', {class: 'button cancel'}); + // Edit session widget + var widget = DomElem('div', { class: 'field-edit-widget' }); + var wfields = DomElem('div', { class: 'fields' }); + var save = DomElem('div', { class: 'button save' }); + var cancel = DomElem('div', { class: 'button cancel' }); - save.textContent = 'Save'; - cancel.textContent = 'Cancel'; + save.textContent = 'Save'; + cancel.textContent = 'Cancel'; - widget.appendChild(wfields); - widget.appendChild(cancel); - widget.appendChild(save); + widget.appendChild(wfields); + widget.appendChild(cancel); + widget.appendChild(save); - // ------------------------------------------------------------------------ - // Methods + // ------------------------------------------------------------------------ + // Methods - function addField(options) - { - var group = DomElem('div', {class: 'group'}); + function addField(options) { + var group = DomElem('div', { class: 'group' }); - var input = DomElem('input'); + var input = DomElem('input'); - var label = DomElem('label'); - label.textContent = options.label; + var label = DomElem('label'); + label.textContent = options.label; - group.appendChild(label); - group.appendChild(input); - wfields.appendChild(group); + group.appendChild(label); + group.appendChild(input); + wfields.appendChild(group); - fields[options.name] = input; + fields[options.name] = input; - if (first_field == null) { - first_field = input; - } + if (first_field == null) { + first_field = input; } + } - function keyBoardShortcut(e) - { - if (e.keyCode == 13) { - saveValues(); - } + function keyBoardShortcut(e) { + if (e.keyCode == 13) { + saveValues(); } + } - function hideWidget() - { - first_field.blur(); - widget.removeAttribute('data-active'); - document.removeEventListener('keydown', keyBoardShortcut); - } + function hideWidget() { + if (first_field) first_field.blur(); + widget.removeAttribute('data-active'); + document.removeEventListener('keydown', keyBoardShortcut); + } - var hideIfNotInvoked = function hideIfNotInvoked(ID) - { - if (widgetID !== ID) hideWidget(); - }; + var hideIfNotInvoked = (ID) => { + if (widgetID !== ID) hideWidget(); + }; - function invokeWidget(data) - { - context_data = data.fields; - context = data.context; + function invokeWidget(data) { + context_data = data.fields; + context = data.context; - for (var key in data.fields) { - if (data.fields[key]) { - fields[key].value = data.fields[key]; - } - else { - if (fields.hasOwnProperty(key)) { - fields[key].value = ''; - data.fields[key] = ''; - } + for (var key in data.fields) { + if (data.fields[key]) { + fields[key].value = data.fields[key]; + } + else { + if (Object.prototype.hasOwnProperty.call(fields, key)) { + fields[key].value = ''; + data.fields[key] = ''; } } + } - widget.setAttribute('data-active', ''); - document.addEventListener('keydown', keyBoardShortcut); - WindowEvents.emit(document, 'FieldWidgetInvoked', widgetID); + widget.setAttribute('data-active', ''); + document.addEventListener('keydown', keyBoardShortcut); + WindowEvents.emit(document, 'FieldWidgetInvoked', widgetID); + if (first_field) { first_field.focus(); first_field.select(); } + } - function saveValues() - { - var obj = {}; - var prop_save = 0; - for (var key in fields) - { - if (context_data[key] != fields[key].value) - { - prop_save++; - obj[key] = fields[key].value; - } - } - - if (prop_save) - { - WindowEvents.emit(document, options.name + '-Save', {context: context, fields : obj}); + function saveValues() { + var obj = {}; + var prop_save = 0; + for (var key in fields) { + if (context_data[key] != fields[key].value) { + prop_save++; + obj[key] = fields[key].value; } - - hideWidget(); } - function destroy() - { - GlobalEvents.off('BookmarkRemoved', hideWidget); + if (prop_save) { + WindowEvents.emit(document, options.name + '-Save', { context: context, fields: obj }); } - // ------------------------------------------------------------------------ - // Create UI + hideWidget(); + } + + function destroy() { + GlobalEvents.off('BookmarkRemoved', hideWidget); + } - cancel.addEventListener('click', hideWidget); - save.addEventListener('click', saveValues); + // ------------------------------------------------------------------------ + // Create UI - GlobalEvents.on('BookmarkRemoved', hideWidget); - WindowEvents.on(document, options.name + '-Invoke', invokeWidget); - WindowEvents.on(document, 'FieldWidgetInvoked', hideIfNotInvoked); + cancel.addEventListener('click', hideWidget); + save.addEventListener('click', saveValues); - // ------------------------------------------------------------------------ - // Public properties + GlobalEvents.on('BookmarkRemoved', hideWidget); + WindowEvents.on(document, options.name + '-Invoke', invokeWidget); + WindowEvents.on(document, 'FieldWidgetInvoked', hideIfNotInvoked); - this.DOMRoot = widget; - this.addField = addField; - } + // ------------------------------------------------------------------------ + // Public properties - // Public API - exports.FieldEditWdiget = FieldEditWdiget; -}); \ No newline at end of file + this.DOMRoot = widget; + this.addField = addField; +} diff --git a/data/scripts/utils/general.js b/data/scripts/utils/general.js index 87b284d..45e338b 100644 --- a/data/scripts/utils/general.js +++ b/data/scripts/utils/general.js @@ -1,53 +1,41 @@ -define(function(require, exports) { - 'use strict'; +'use strict'; - function copyProperties(source, destination) { - for (var prop in source) { - if (source.hasOwnProperty(prop)) { - destination[prop] = source[prop]; - } +export function copyProperties(source, destination) { + for (var prop in source) { + if (Object.prototype.hasOwnProperty.call(source, prop)) { + destination[prop] = source[prop]; } } - - // Test if object is a valid javascript function - // Returns the same object if true or a new function otherwise - function getValidFunction(object) - { - var value = (typeof object === 'function' ? object : function () {}); - return value; - } - - function LimitCounter(limit, callback) - { - var func = getValidFunction(callback); - var counted = 0; - var triggered = false; - - function checkLimit() { - if (!triggered && counted >= limit) - { - triggered = true; - func(); - } +} + +// Test if object is a valid javascript function +// Returns the same object if true or a new function otherwise +export function getValidFunction(object) { + var value = (typeof object === 'function' ? object : function () { }); + return value; +} + +export function LimitCounter(limit, callback) { + var func = getValidFunction(callback); + var counted = 0; + var triggered = false; + + function checkLimit() { + if (!triggered && counted >= limit) { + triggered = true; + func(); } - - this.advance = function advance() { - counted++; - checkLimit(); - }.bind(this); - - this.offsetLimit = function offsetLimit(offset) { - limit += offset; - if (limit > 0) { - checkLimit(); - } - }; } - // ------------------------------------------------------------------------ - // Public API + this.advance = function advance() { + counted++; + checkLimit(); + }; - exports.LimitCounter = LimitCounter; - exports.copyProperties = copyProperties; - exports.getValidFunction = getValidFunction; -}); \ No newline at end of file + this.offsetLimit = function offsetLimit(offset) { + limit += offset; + if (limit > 0) { + checkLimit(); + } + }; +} \ No newline at end of file diff --git a/data/scripts/utils/global-events.js b/data/scripts/utils/global-events.js index 6b4668f..3e75b14 100644 --- a/data/scripts/utils/global-events.js +++ b/data/scripts/utils/global-events.js @@ -1,100 +1,88 @@ -define(function(require, exports) { - 'use strict'; - - // ------------------------------------------------------------------------ - // Modules - - const { PubSub } = require('../3rd/pubsub'); - - // ------------------------------------------------------------------------ - // API - - var GlobalEventSystem = (function GlobalEventSystem() { - - var globalEvents = new PubSub(); - - return { - on : globalEvents.subscribe, - off : globalEvents.unsubscribe, - emit: globalEvents.publish, - }; - })(); - - // Handles collection of PubSub events separated by diffrent namespaces - var NamespaceEvents = (function NamespaceEvents() { - - var map = new Map(); - - //------------------------------------------------------------------------- - // Public API - - var broadcastEvent = function broadcastEvent(topic, object) { - map.forEach(function(collection) { - collection.publish(topic, object); - }); - }; - - var emitEvent = function emitEvent(namespace, topic, object) { - var collection = map.get(namespace); - if (collection instanceof PubSub) { - collection.publish(topic, object); - } - }; - - var subscribe = function subscribe(namespace, topic, callback) { - var collection = map.get(namespace); - if (collection instanceof PubSub === false) { - collection = new PubSub(); - map.set(namespace, collection); - } - collection.subscribe(topic, callback); - }; - - var unsubscribe = function unsubscribe(namespace, topic, callback) { - var collection = map.get(namespace); - if (collection instanceof PubSub) { - collection.unsubscribe(topic, callback); - } - }; - - var remove = function remove(namespace) { - var collection = map.get(namespace); - if (collection instanceof PubSub) { - collection.messages = {}; - map.delete(namespace); - } - }; - - return { - on : subscribe, - off: unsubscribe, - emit : emitEvent, - broadcast : broadcastEvent, - remove: remove - }; - }); - - // WindowEvents based on window.document topic - - var WindowEvents = new NamespaceEvents(); - - WindowEvents.onLocal = function onLocal(topic, callback) { - this.on(document, topic, callback); +'use strict'; + +// ------------------------------------------------------------------------ +// Modules + +import { PubSub } from '../3rd/pubsub'; + +// ------------------------------------------------------------------------ +// API + +const GlobalEventSystem = (function GlobalEventSystem() { + + const globalEvents = new PubSub(); + + return { + on: globalEvents.subscribe.bind(globalEvents), + off: globalEvents.unsubscribe.bind(globalEvents), + emit: globalEvents.publish.bind(globalEvents), + }; +})(); + +// Handles collection of PubSub events separated by diffrent namespaces +function NamespaceEvents() { + + var map = new Map(); + + //------------------------------------------------------------------------- + // Public API + + var broadcastEvent = function broadcastEvent(topic, object) { + map.forEach(function (collection) { + collection.publish(topic, object); + }); }; - WindowEvents.emitLocal = function emitLocal(topic, object) { - this.emit(document, topic, object); + var emitEvent = function emitEvent(namespace, topic, object) { + var collection = map.get(namespace); + if (collection instanceof PubSub) { + collection.publish(topic, object); + } }; - // ------------------------------------------------------------------------ - // Events - - // ------------------------------------------------------------------------ - // Init + var subscribe = function subscribe(namespace, topic, callback) { + var collection = map.get(namespace); + if (collection instanceof PubSub === false) { + collection = new PubSub(); + map.set(namespace, collection); + } + collection.subscribe(topic, callback); + }; + + var unsubscribe = function unsubscribe(namespace, topic, callback) { + var collection = map.get(namespace); + if (collection instanceof PubSub) { + collection.unsubscribe(topic, callback); + } + }; + + var remove = function remove(namespace) { + var collection = map.get(namespace); + if (collection instanceof PubSub) { + collection.messages = {}; + map.delete(namespace); + } + }; + + return { + on: subscribe, + off: unsubscribe, + emit: emitEvent, + broadcast: broadcastEvent, + remove: remove + }; +} + +// WindowEvents based on window.document topic + +const WindowEvents = new NamespaceEvents(); + +WindowEvents.onLocal = function onLocal(topic, callback) { + this.on(document, topic, callback); +}; - // ------------------------------------------------------------------------ - // Module exports +WindowEvents.emitLocal = function emitLocal(topic, object) { + this.emit(document, topic, object); +}; - exports.GlobalEvents = GlobalEventSystem; - exports.WindowEvents = WindowEvents; -}); +export { GlobalEventSystem as GlobalEvents, WindowEvents }; diff --git a/data/session-sync.css b/data/session-sync.css index 8da35b1..eddd1d6 100644 --- a/data/session-sync.css +++ b/data/session-sync.css @@ -662,6 +662,14 @@ background-image: url("images/icons/frames.png"); } +#session-sync .context-menu .button[icon="replace-current"] { + background-image: url("images/material_icons/repeat_one.svg"); +} + +#session-sync .context-menu .button[icon="replace-all"] { + background-image: url("images/material_icons/repeat.svg"); +} + #session-sync .context-menu .button[icon="copy"] { background-image: url("images/icons/clipboard.png"); } @@ -1684,8 +1692,8 @@ #session-sync .header-bar .header-menu { width: 1.8em; height: 1.8em; - background-image: url("images/menu.png"); - background-size: 55%; + background-image: url("images/icons_v2/menu.png"); + background-size: 75% 80%; background-repeat: no-repeat; background-position: center center; border: 1px solid rgba(0, 0, 0, 0); @@ -1995,12 +2003,13 @@ display: none; float: left; - filter: saturate(100%); + opacity: 0.75; + /* filter: grayscale(100%); */ } #session-sync .session-toolbar .menu-bar .button:hover { cursor: pointer; - filter: saturate(100%); + opacity: 1; box-shadow: none; } @@ -2015,12 +2024,10 @@ #session-sync .session-toolbar .menu-bar .add { background: url("images/icons/star.png") no-repeat center; - filter: saturate(100%); } #session-sync .session-toolbar .menu-bar .save { background: url("images/icons/heart.png") no-repeat center; - filter: saturate(100%); } #session-sync .session-toolbar .menu-bar .merge-sessions { @@ -2039,10 +2046,22 @@ background: url("images/icons/frames.png") no-repeat center; } +#session-sync .session-toolbar .menu-bar .repalce-session-all { + background: url("images/material_icons/repeat.svg") no-repeat center; +} + +#session-sync .session-toolbar .menu-bar .repalce-session { + background: url("images/material_icons/repeat_one.svg") no-repeat center; +} + #session-sync .session-toolbar .menu-bar .fix-session { background: url("images/icons/tools.png") no-repeat center; } +#session-sync .session-toolbar .menu-bar .customize-toolbar { + background: url("images/icons_v2/more.png") no-repeat center; +} + /* * Menu state */ diff --git a/dev/css/session-toolbar.css b/dev/css/session-toolbar.css index e63484c..2940122 100644 --- a/dev/css/session-toolbar.css +++ b/dev/css/session-toolbar.css @@ -111,12 +111,13 @@ display: none; float: left; - filter: saturate(100%); + opacity: 0.75; + filter: grayscale(100%); } #session-sync .session-toolbar .menu-bar .button:hover { cursor: pointer; - filter: saturate(100%); + opacity: 1; box-shadow: none; } @@ -131,12 +132,10 @@ #session-sync .session-toolbar .menu-bar .add { background: url("images/icons/star.png") no-repeat center; - filter: saturate(100%); } #session-sync .session-toolbar .menu-bar .save { background: url("images/icons/heart.png") no-repeat center; - filter: saturate(100%); } #session-sync .session-toolbar .menu-bar .merge-sessions { diff --git a/entrypoints/background.js b/entrypoints/background.js new file mode 100644 index 0000000..6a17abf --- /dev/null +++ b/entrypoints/background.js @@ -0,0 +1,14 @@ +import { initBackground } from '../scripts/main'; +import { initDevelopment } from '../scripts/development'; +import { SessionManagement } from '../scripts/session-management'; +import { SessionAutoSave } from '../scripts/session-history'; + +export default defineBackground(() => { + // Initialize the background logic + initBackground(); + initDevelopment(); + SessionManagement.init(); + SessionAutoSave.init(); + + console.log('Session Sync Background script initialized.'); +}); diff --git a/entrypoints/popup/index.html b/entrypoints/popup/index.html new file mode 100644 index 0000000..9d4fb38 --- /dev/null +++ b/entrypoints/popup/index.html @@ -0,0 +1,15 @@ + + + + + + Session Sync + + + + +
+ + + + \ No newline at end of file diff --git a/entrypoints/popup/main.js b/entrypoints/popup/main.js new file mode 100644 index 0000000..cbcafb4 --- /dev/null +++ b/entrypoints/popup/main.js @@ -0,0 +1,23 @@ +import '../../dev/css/_style.css'; +import '../../dev/css/components.css'; +import '../../dev/css/config-panel.css'; +import '../../dev/css/confirm-box.css'; +import '../../dev/css/context-menu.css'; +import '../../dev/css/field-widget.css'; +import '../../dev/css/filter-panel.css'; +import '../../dev/css/generic.css'; +import '../../dev/css/hotkey-manager.css'; +import '../../dev/css/notification-system.css'; +import '../../dev/css/search-bar.css'; +import '../../dev/css/session-container.css'; +import '../../dev/css/session-header-bar.css'; +import '../../dev/css/session-sorting.css'; +import '../../dev/css/session-toolbar.css'; +import '../../dev/css/sessions-list.css'; +import '../../dev/css/tooltip-system.css'; +import '../../dev/css/trash-can.css'; +import '../../dev/css/url-bar.css'; + +import { init } from '../../data/scripts/app.js'; + +init(); diff --git a/gulpfile.js b/gulpfile.js deleted file mode 100644 index 3c3662e..0000000 --- a/gulpfile.js +++ /dev/null @@ -1,124 +0,0 @@ -var gulp = require('gulp'); -var gutil = require('gulp-util'); -var open = require('gulp-open'); -var watch = require('gulp-watch'); -var concat = require('gulp-concat'); -var plumber = require('gulp-plumber'); - -var sys = require('sys'); -// var exec = require('child_process').exec; -var express = require('express'); -var tiny_lr = require('tiny-lr')(); -var serve_index = require('serve-index'); -var connect_livereload = require('connect-livereload'); - -// ---------------------------------------------------------------------------- -// Config - -var EXPRESS_PORT = 8080; -var EXPRESS_ROOT = __dirname; -var LIVERELOAD_PORT = 35729; -var APP_ROOT = '.'; -var lr_files = [ - APP_ROOT + '/**/*.html', - APP_ROOT + '/**/*.js', - APP_ROOT + '/**/*.css', - '!' + APP_ROOT + '/node_modules/**/*.*', - '!' + APP_ROOT + '/bootstrap.js', - '!' + APP_ROOT + '/gulpfile.js', -]; - -function shellCmd(error, stdout, stderr) { sys.puts(stdout); } - -var io; -// function broadcast() -// { -// console.log('Trigger Firefox Addon CSS Update!'); -// io.sockets.emit('update'); -// } - -// ---------------------------------------------------------------------------- -// Gulp tasks - -gulp.task('default', ['server', 'socket-io', 'open', 'watch'], function() {}); - -gulp.task('socket-io', function() { - var app = require('express')(); - var server = require('http').Server(app); - io = require('socket.io')(server); - - server.listen(8888); - - app.use(express.static(__dirname + '/data/socket.io')); - app.get('/', function (req, res) { - res.sendFile(__dirname + '/index.html'); - }); - - io.on('connection', function (socket) { - - console.log('connected', socket.id); - - socket.on('request', function (data) { - socket.emit('update'); - }); - }); -}); - -gulp.task('server', function() { - var app = express(); - - app.use(connect_livereload({ - port: LIVERELOAD_PORT - })); - - // TODO inspect how to list files from directories - app.use(serve_index(__dirname + '/' + APP_ROOT, {'icons': true})); - app.use(express.static(__dirname + '/' + APP_ROOT)); - app.listen(EXPRESS_PORT); -}); - -gulp.task('open', function() { - var options = { - url: 'http://localhost:' + EXPRESS_PORT, - app: 'firefox' - }; - gulp.src(__filename).pipe(open('', options)); -}); - -// ---------------------------------------------------------------------------- -// Concat CSS - -gulp.task('css', function(event) { - var stream = gulp.src('dev/css/**/*.css') - .pipe(plumber()) - .pipe(concat('session-sync.css')) - .pipe(gulp.dest(APP_ROOT + '/data')); - return stream; -}); - -// ---------------------------------------------------------------------------- -// Watch + LiveReload - -gulp.task('watch', ['livereload'], function() { - watch(lr_files, notifyLivereload); - watch('./dev/css/**/*.css', function(events, done) { - console.log('Message', done); - gulp.start('css', function() { - // broadcast(); - }); - }); -}); - -gulp.task('livereload', function() { - tiny_lr.listen(LIVERELOAD_PORT); -}); - -function notifyLivereload(event) { - var fileName = require('path').relative(EXPRESS_ROOT, event.path); - tiny_lr.changed({ - body: { - files: [fileName] - } - }); - gutil.log('[' + event.event + '] :', gutil.colors.cyan(fileName)); -} diff --git a/manifest.json b/manifest.json deleted file mode 100644 index 9a76f15..0000000 --- a/manifest.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "name": "Session Sync", - "version": "3.2.0", - "author": "Gabriel Ivanica", - "description": "Save sessions as bookmarks and sync them through Firefox Sync (or any other sync engine).\nPowerful session management - organize, edit, change, save, restore, etc\n", - "homepage_url": "https://github.com/ReDEnergy/SessionSync", - "manifest_version": 2, - "default_locale": "en", - "browser_specific_settings": { - "gecko": { - "id": "session-sync@gabrielivanica.com", - "data_collection_permissions": { - "required": ["none"] - } - } - }, - "icons": { - "16": "data/images/icon16.png", - "32": "data/images/icon32.png", - "48": "data/images/icon48.png", - "64": "data/images/icon64.png", - "96": "data/images/icon96.png", - "128": "data/images/icon128.png" - }, - "background": { - "scripts": [ - "scripts/main.js", - "scripts/favicon-service.js", - "scripts/session-management.js", - "scripts/session-history.js", - "scripts/development.js" - ] - }, - "permissions": [ - "storage", - "bookmarks", - "tabs", - "clipboardWrite", - "unlimitedStorage", - "cookies", - "contextualIdentities" - ], - "browser_action": { - "browser_style": true, - "default_icon": { - "16": "data/images/icon16.png", - "32": "data/images/icon32.png", - "64": "data/images/icon64.png" - }, - "default_title": "Session Sync", - "default_popup": "data/session-sync.html" - }, - "commands": { - "_execute_browser_action": { - "suggested_key": { - "default": "Ctrl+Shift+F" - }, - "description": "Toggle Widget" - }, - "session-sync-detach-tab": { - "suggested_key": { - "default": "Ctrl+Shift+V" - }, - "description": "Tab View" - }, - "session-sync-detach-window": { - "suggested_key": { - "default": "Ctrl+Shift+U" - }, - "description": "Window View" - } - } -} \ No newline at end of file diff --git a/pack_addon.bat b/pack_addon.bat deleted file mode 100644 index 44ff8d2..0000000 --- a/pack_addon.bat +++ /dev/null @@ -1,16 +0,0 @@ -set EXEC_DIR=%~dp0 -set SOURCE_DIR=%~dp0 -cd %~dp0 - -:: Date date/time -SET ZIP="%SOURCE_DIR%\bin\7za.exe" -SET ARCHIVE="Session Sync.zip" - -:: Export as archive -DEL %ARCHIVE% -%ZIP% a -tzip -bt -mx1 %ARCHIVE% ^ - .\data ^ - .\scripts ^ - .\_locales ^ - .\manifest.json ^ - .\README.md diff --git a/package.json b/package.json index 4b4336b..7ff3fb1 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,24 @@ { - "name": "session-sync", - "author": "Gabriel Ivanica", - "url": "https://github.com/ReDEnergy/SessionSync", - "version": "3.1.1", - "description": "Save sessions as bookmarks and sync them through any sync engine.\nManage sessions, edit, save, restore.\nSession-Sync will automatically update sessions across multiple machines", - "license": "MPL-2.0", - "devDependencies": { - "connect-livereload": "^0.5.3", - "express": "^4.13.1", - "gulp": "^3.9.0", - "gulp-callback": "0.0.3", - "gulp-concat": "^2.6.0", - "gulp-open": "^1.0.0", - "gulp-plumber": "^1.0.1", - "gulp-util": "^3.0.6", - "gulp-watch": "^4.3.3", - "serve-index": "^1.7.1", - "socket.io": "^1.4.8", - "tiny-lr": "^0.1.6" - } -} + "name": "session-sync", + "author": "Gabriel Ivanica", + "url": "https://github.com/ReDEnergy/SessionSync", + "version": "3.2.0", + "description": "Save sessions as bookmarks and sync them through any sync engine.\nManage sessions, edit, save, restore.\nSession-Sync will automatically update sessions across multiple machines", + "license": "MPL-2.0", + "scripts": { + "dev": "wxt", + "dev:chrome": "wxt -b chrome --mv3", + "dev:firefox": "wxt -b firefox --mv2", + "build": "wxt build", + "build:chrome": "wxt build -b chrome --mv3", + "build:firefox": "wxt build -b firefox --mv2", + "clean": "wxt clean", + "zip": "wxt zip", + "zip:chrome": "wxt zip -b chrome --mv3", + "zip:firefox": "wxt zip -b firefox --mv2", + "postinstall": "wxt prepare" + }, + "devDependencies": { + "wxt": "^0.20.20" + } +} \ No newline at end of file diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json new file mode 100644 index 0000000..fefb238 --- /dev/null +++ b/public/_locales/en/messages.json @@ -0,0 +1,14 @@ +{ + "test": { + "message": "Restore in new window" + }, + "session_sync_tutorial": { + "message": "Tutorial" + }, + "session_sync_detach_tab": { + "message": "Tab View" + }, + "session_sync_detach_window": { + "message": "Window View" + } +} \ No newline at end of file diff --git a/data/home/home.css b/public/home/home.css similarity index 100% rename from data/home/home.css rename to public/home/home.css diff --git a/data/home/home.html b/public/home/home.html similarity index 67% rename from data/home/home.html rename to public/home/home.html index bf8cb14..5f93560 100644 --- a/data/home/home.html +++ b/public/home/home.html @@ -1,5 +1,6 @@ + @@ -46,17 +47,22 @@
- Press CTRL + SHIFT + F to open the panel or use the widget provided to open the panel + Press CTRL + SHIFT + F + to open the panel or use the widget provided to open the + panel
- You can find all your recent browsing history in the history tab.
- Restore the session or save it if you want to keep indefinitely
- Configure history settings in the configuration panel + You can find all your recent browsing history in the history tab. +
+ Restore the session or save it if you want to keep + indefinitely
+ Configure history settings in the configuration panel
Use the search bar to easy filter through saved sessions
- You can focus on the search bar by pressing CTRL + F or F3 + You can focus on the search bar by pressing CTRL + F or + F3
Press right click to access aditional context menus for @@ -74,15 +80,18 @@ green resize handle
- The addon offers multiple configuration options. Explore all of them in the configuration panel + The addon offers multiple configuration options. Explore all of them in the configuration + panel
Configuration panel can be accessed from the addon UI menu
- Drag and drop bookmark items over the trash bin drop area location (bottom-right corner) + Drag and drop bookmark items over the trash bin drop area + location (bottom-right corner) to delete them
- Click on the red button to restore the previous deleted items + Click on the red button to restore the previous deleted items
Session sync offers the ability to save (or not) pinned tabs and @@ -90,13 +99,17 @@ pinned state
- You can edit session and bookmarks information to provide more valuable future information + You can edit session and bookmarks information to provide more + valuable future information
- Sort sessions by name, position, or date using the sort-selection menu from the bottom-left corner of the UI + Sort sessions by name, position, or date using the sort-selection + menu from the bottom-left corner of the UI
- The session menu toolbar provides fast access to various commands such as restore, replace, merge or add the current tab as bookmark + The session menu toolbar provides fast access to various commands such as restore, replace, merge or add the + current tab as bookmark
Move your mouse above a bookmark to view the URL. @@ -104,14 +117,21 @@ Click on the URL bar to copy the link
- You can detach the UI into a separate window or tab
- Window Mode: CTRL + SHIFT + V   - Tab Mode: CTRL + SHIFT + U
+ You can detach the UI into a separate window or tab
+ Window Mode: CTRL + SHIFT + + V   + Tab Mode: CTRL + SHIFT + + U
or simply use the buttons from addon menu
- Saved session are stored as bookmarks. It's recommended to use Firefox Account to sync your bookmarks.
- Currenly you can only use Firefox Bookmark Manager (CTRL + SHIFT + B) to export/import all your bookmarks (which includes all Session Sync saved data).
+ Saved session are stored as bookmarks. It's recommended to use Firefox Account to sync your bookmarks.
+ Currenly you can only use Firefox Bookmark Manager (CTRL + SHIFT + + B) to export/import all your bookmarks (which includes all Session Sync saved data). +
Session Sync detects automatically all updated bookmarks after an import.
@@ -122,7 +142,5 @@ - - - + \ No newline at end of file diff --git a/data/home/home.js b/public/home/home.js similarity index 90% rename from data/home/home.js rename to public/home/home.js index 31ef313..bb8da27 100644 --- a/data/home/home.js +++ b/public/home/home.js @@ -15,7 +15,7 @@ function load() { bullets = document.getElementById('bullets'); tooltip = document.getElementById('tooltip'); - bullets.addEventListener('mouseleave', function() { + bullets.addEventListener('mouseleave', function () { tooltip.removeAttribute('active'); }); @@ -63,8 +63,7 @@ function updateActiveBullet(node) { } } -function setCarouselTo(index) -{ +function setCarouselTo(index) { if (index < 0 || index > (size - 1)) { return; } @@ -74,8 +73,7 @@ function setCarouselTo(index) updateActiveBullet(TutorialEntries.getEntryByIndex(index).bullet); } -function advanceCarousel(offset) -{ +function advanceCarousel(offset) { var position = Carousel.getAttribute('advance') | 0; position += offset; if (position < 0 || position > (size - 1) * 100) { @@ -86,8 +84,7 @@ function advanceCarousel(offset) updateActiveBullet(TutorialEntries.getEntryByIndex(position / 100).bullet); } -function initCarousel() -{ +function initCarousel() { var moveLeft = document.getElementById('scroll-left'); var moveRight = document.getElementById('scroll-right'); @@ -95,8 +92,7 @@ function initCarousel() moveRight.addEventListener('click', advanceCarousel.bind(null, 100)); } -function HelpEntry(options) -{ +function HelpEntry(options) { var box = document.createElement('div'); box.className = 'box'; box.setAttribute('info', options.key); @@ -115,7 +111,7 @@ function HelpEntry(options) var visual = document.createElement('div'); visual.className = 'tutorial-image'; - visual.style.backgroundImage = 'url("../images/tutorial/' + options.key + '.png")'; + visual.style.backgroundImage = 'url("/images/tutorial/' + options.key + '.png")'; box.appendChild(title); box.appendChild(info); @@ -129,10 +125,10 @@ function HelpEntry(options) bullet.className = 'bullet'; bullet.setAttribute('left', options.index); bullet.setAttribute('tooltip', options.title); - bullet.addEventListener('click', function() { + bullet.addEventListener('click', function () { setCarouselTo(options.index); }); - bullet.addEventListener('mouseover', function() { + bullet.addEventListener('mouseover', function () { var pos = bullet.getBoundingClientRect(); tooltip.setAttribute('active', ''); tooltip.style.left = pos.x + pos.width / 2 + 'px'; @@ -148,8 +144,7 @@ function HelpEntry(options) this.link = reference; } -var TutorialEntries = (function TutorialEntries() -{ +var TutorialEntries = (function TutorialEntries() { var entries = {}; var entryList = [ { @@ -157,7 +152,7 @@ var TutorialEntries = (function TutorialEntries() key: 'overview' }, { - title : 'Session history', + title: 'Session history', key: 'history-list' }, { @@ -205,7 +200,7 @@ var TutorialEntries = (function TutorialEntries() key: 'resize-sessions' }, { - title : 'Detach mode', + title: 'Detach mode', key: 'detach-mode' }, { @@ -214,10 +209,9 @@ var TutorialEntries = (function TutorialEntries() }, ]; - function init() - { + function init() { var index = 0; - entryList.forEach(function(info) { + entryList.forEach(function (info) { info.index = index; var entry = new HelpEntry(info); entries[info.key] = entry; diff --git a/data/lazy/lazy.css b/public/lazy/lazy.css similarity index 100% rename from data/lazy/lazy.css rename to public/lazy/lazy.css diff --git a/data/lazy/lazy.html b/public/lazy/lazy.html similarity index 100% rename from data/lazy/lazy.html rename to public/lazy/lazy.html diff --git a/data/lazy/lazy.js b/public/lazy/lazy.js similarity index 100% rename from data/lazy/lazy.js rename to public/lazy/lazy.js diff --git a/scripts/development.js b/scripts/development.js index dd844b3..b65dd53 100644 --- a/scripts/development.js +++ b/scripts/development.js @@ -1,19 +1,26 @@ 'use strict'; -(function DevelopmentMode() -{ +import { SessionAutoSave } from './session-history'; + +export function initDevelopment() { // ------------------------------------------------------------------------ // Init - browser.commands.onCommand.addListener(function(command) { - if (command == 'session-sync-save-history') { - for (var i = 0; i < 10; i++) { - SessionAutoSave.activeSession.update(); - } + try { + if (typeof browser !== 'undefined' && browser.commands && browser.commands.onCommand) { + browser.commands.onCommand.addListener(function (command) { + if (command == 'session-sync-save-history') { + for (var i = 0; i < 10; i++) { + SessionAutoSave.activeSession.update(); + } + } + }); } - }); + } catch (err) { + console.warn('browser.commands.onCommand.addListener failed in development.js', err); + } // ------------------------------------------------------------------------ // Events -})(); \ No newline at end of file +} \ No newline at end of file diff --git a/scripts/favicon-service.js b/scripts/favicon-service.js index 89641e5..fce03bf 100644 --- a/scripts/favicon-service.js +++ b/scripts/favicon-service.js @@ -1,18 +1,7 @@ 'use strict'; -var FaviconService = (function FaviconService() { - - // var ignoreFaviconSearch = { - // 'chrom' : true, - // 'javas' : true, - // 'data:' : true, - // 'file:' : true, - // 'about' : true, - // 'moz-e' : true, - // }; - - var database = {}; - var urlParser = document.createElement('a'); +const FaviconService = (function FaviconService() { + var serviceStatus = true; var serviceStatusKey = 'services.favicon.enabled'; @@ -27,47 +16,45 @@ var FaviconService = (function FaviconService() { resolve(reader.result); }, false); - reader.onerror = function() { + reader.onerror = function () { return reject(this); }; reader.readAsDataURL(blob); }); } - var getHostName = function getHostName(url) - { - urlParser.href = url; - return urlParser.hostname; + var getHostName = function getHostName(url) { + try { + return new URL(url).hostname; + } catch (e) { + return ''; + } } - var getFaviconUrl = function getFaviconUrl(url, callback) - { + var getFaviconUrl = function getFaviconUrl(url, callback) { var key = '@favIconUrl:' + getHostName(url); browser.storage.local.get(key) - .then(function (obj) { - callback(obj[key]); - }, callback); + .then(function (obj) { + callback(obj[key]); + }, callback); } - var setFaviconUrl = function setFaviconUrl(key, favIconUrl) - { + var setFaviconUrl = function setFaviconUrl(key, favIconUrl) { getBase64ImageFromUrl(favIconUrl) - .then(function(result) { - browser.storage.local.set({ [key] : result }); - }, - function onError(info) { - browser.storage.local.set({ [key] : favIconUrl }); - }); + .then(function (result) { + browser.storage.local.set({ [key]: result }); + }, + function onError(info) { + browser.storage.local.set({ [key]: favIconUrl }); + }); } - var checkTabFaviconSaved = function checkTabFaviconSaved(tab, waitToLoad) - { + var checkTabFaviconSaved = function checkTabFaviconSaved(tab, waitToLoad) { if (serviceStatus == false) { return; } - if (tab.favIconUrl) - { + if (tab.favIconUrl) { getFaviconUrl(tab.url, function (value) { if (value === undefined || tab.url != value) { var key = '@favIconUrl:' + getHostName(tab.url); @@ -75,10 +62,8 @@ var FaviconService = (function FaviconService() { } }); } - else if (waitToLoad != false) - { - function handle(tabId, changeInfo, tabInfo) - { + else if (waitToLoad != false) { + function handle(tabId, changeInfo, tabInfo) { browser.tabs.onUpdated.removeListener(handle); checkTabFaviconSaved(tabInfo, false); } @@ -90,33 +75,25 @@ var FaviconService = (function FaviconService() { } } - var initConfig = function initConfig() - { + var initConfig = function initConfig() { browser.storage.local.get(serviceStatusKey) - .then(function (obj) { - serviceStatus = obj[serviceStatusKey]; - }); + .then(function (obj) { + serviceStatus = obj[serviceStatusKey]; + }); }; - // ************************************************************************ - // Events - browser.storage.onChanged.addListener(function onChange(object) { if (object[serviceStatusKey]) { serviceStatus = object[serviceStatusKey].newValue; } }); - // ************************************************************************ - // Init - initConfig(); - // ************************************************************************ - // Public API - return { getFaviconUrl: getFaviconUrl, checkTabFaviconSaved: checkTabFaviconSaved } })(); + +export { FaviconService }; diff --git a/scripts/main.js b/scripts/main.js index a80b939..901cfb4 100644 --- a/scripts/main.js +++ b/scripts/main.js @@ -1,29 +1,15 @@ -// console.log(browser.runtime.getManifest()); +'use strict'; -var Commands = { +export const Commands = { toggleWidget: '_execute_browser_action', - openTutorial: 'session-sync-tutorial', - detachToTab: 'session-sync-detach-tab', - detachToWindow: 'session-sync-detach-window', - leaveFeedback: 'session-sync-leave-feedback', - openGithub: 'session-sync-open-github' + openTutorial: 'session_sync_tutorial', + detachToTab: 'session_sync_detach_tab', + detachToWindow: 'session_sync_detach_window', + leaveFeedback: 'session_sync_leave_feedback', + openGithub: 'session_sync_open_github' }; -browser.runtime.onInstalled.addListener(function (startInfo) { - if (startInfo.reason == 'install') { - SessionSync.checkEvent(Commands.openTutorial); - } -}); - -browser.runtime.onMessage.addListener(function (message, sender, sendResponse) { - SessionSync.checkEvent(message.event); -}); - -browser.commands.onCommand.addListener(function(command) { - SessionSync.checkEvent(command); -}); - -var SessionSync = (function () { +const SessionSync = (function () { var windowInfoKey = 'style.window.detach'; var activeWindow = null; @@ -32,16 +18,22 @@ var SessionSync = (function () { height: 600 }; - browser.windows.onRemoved.addListener(function (windowID) { - if (activeWindow && windowID == activeWindow.id) { - activeWindow = null; + function init() { + try { + if (typeof browser !== 'undefined' && browser.windows && browser.windows.onRemoved) { + browser.windows.onRemoved.addListener(function (windowID) { + if (activeWindow && windowID === activeWindow.id) { + activeWindow = null; + } + }); + } + } catch (err) { + console.warn('browser.windows.onRemoved.addListener failed', err); } - }); + } - function checkEvent(event) - { - switch (event) - { + function checkEvent(event) { + switch (event) { case Commands.detachToTab: detachedToTab(); break; @@ -56,7 +48,7 @@ var SessionSync = (function () { case Commands.openTutorial: browser.tabs.create({ - url: 'data/home/home.html', + url: 'home/home.html', active: true, }); break; @@ -70,8 +62,7 @@ var SessionSync = (function () { } } - function leaveFeedback() - { + function leaveFeedback() { browser.tabs.create({ url: 'mailto:gabriel.ivanica@gmail.com?Subject=[Session-Sync]%20User%20feedback' }).then(function (tab) { @@ -79,8 +70,7 @@ var SessionSync = (function () { }); } - function detachToWindow() - { + function detachToWindow() { if (activeWindow) { browser.windows.update(activeWindow.id, { focused: true @@ -90,80 +80,71 @@ var SessionSync = (function () { browser.storage.local.get([windowInfoKey]).then(function (sizeInfo) { var windowInfo = sizeInfo[windowInfoKey]; - if (windowInfo == undefined) - { + if (windowInfo == undefined) { windowInfo = defaultWindowProperties; } browser.windows.create({ width: windowInfo.width, height: windowInfo.height, - // left: 0, - // top: 0, - url: 'data/session-sync.html', + url: 'entrypoints/popup/index.html', type: 'panel' }).then(function (newWindow) { activeWindow = newWindow; - // WindowUtils.centerWindow(window); }); }); } - function detachedToTab() - { + function detachedToTab() { browser.tabs.query({ title: 'Session Sync', windowId: browser.windows.WINDOW_ID_CURRENT }) - .then(function (tabs) { - if (tabs.length > 0) { - for (let i in tabs) { - if (tabs[i].url.indexOf('moz-extension') == 0) { - browser.tabs.update(tabs[i].id, { - active: true - }); - return; + .then(function (tabs) { + if (tabs.length > 0) { + for (let i in tabs) { + if (tabs[i].url.indexOf('moz-extension') == 0) { + browser.tabs.update(tabs[i].id, { + active: true + }); + return; + } } } - } - browser.tabs.create({ - url: 'data/session-sync.html' + browser.tabs.create({ + url: 'entrypoints/popup/index.html' + }); }); - }); } return { checkEvent: checkEvent, + init: init }; })(); -var WindowUtils = (function () -{ +export { SessionSync }; + +export const WindowUtils = (function () { var windowMargins = { left: -8, top: 12 }; function resizeAndCenter(window, windowProperties) { - // console.log('Fullscreen', window); - var screen = { - width : window.width + 2 * windowMargins.left, + width: window.width + 2 * windowMargins.left, height: window.height + 2 * windowMargins.top }; windowProperties.left = window.left + (((screen.width - windowProperties.width) / 2) | 0) + windowMargins.left; windowProperties.top = window.top + (((screen.height - windowProperties.height) / 2) | 0) + windowMargins.top; - // console.log('Properties', windowProperties, 'Screen', screen); browser.windows.update(window.id, windowProperties); } function centerWindow(window) { - - // console.log(window); - var properties = { width: window.width, height: window.height, @@ -184,12 +165,52 @@ var WindowUtils = (function () })(); // ------------------------------------------------------------------------ -// App Init - -// Delete previous undo events -browser.storage.local.set({'undo.events' : []}); -browser.storage.local.set({'trashcan.hide' : true}); -browser.storage.local.set({'trashcan.hide-count' : 0}); -browser.storage.local.set({'session.active.filter' : ''}); -browser.storage.local.set({'session.selected' : null}); -browser.storage.local.set({'session.history.selected' : null}); +// App Init logic (to be called in background entry point) +export function initBackground() { + // Initialize SessionSync internal listeners + SessionSync.init(); + + try { + if (typeof browser !== 'undefined' && browser.runtime && browser.runtime.onInstalled) { + browser.runtime.onInstalled.addListener(function (startInfo) { + if (startInfo.reason == 'install') { + SessionSync.checkEvent(Commands.openTutorial); + } + }); + } + } catch (err) { + console.warn('browser.runtime.onInstalled.addListener failed', err); + } + + try { + if (typeof browser !== 'undefined' && browser.runtime && browser.runtime.onMessage) { + browser.runtime.onMessage.addListener(function (message, sender, sendResponse) { + if (message && message.event) { + SessionSync.checkEvent(message.event); + } + }); + } + } catch (err) { + console.warn('browser.runtime.onMessage.addListener failed', err); + } + + try { + if (typeof browser !== 'undefined' && browser.commands && browser.commands.onCommand) { + browser.commands.onCommand.addListener(function (command) { + SessionSync.checkEvent(command); + }); + } + } catch (err) { + console.warn('browser.commands.onCommand.addListener failed', err); + } + + // Delete previous undo events + if (typeof browser !== 'undefined' && browser.storage && browser.storage.local) { + browser.storage.local.set({ 'undo.events': [] }); + browser.storage.local.set({ 'trashcan.hide': true }); + browser.storage.local.set({ 'trashcan.hide-count': 0 }); + browser.storage.local.set({ 'session.active.filter': '' }); + browser.storage.local.set({ 'session.selected': null }); + browser.storage.local.set({ 'session.history.selected': null }); + } +} diff --git a/scripts/session-history.js b/scripts/session-history.js index 9e59297..a82aa21 100644 --- a/scripts/session-history.js +++ b/scripts/session-history.js @@ -1,86 +1,91 @@ 'use strict'; -var BrowserSession = function BrowserSession() -{ - this.timer; +import { SessionManagement } from './session-management'; + +const storageKey = { + config: 'history.config', + active: 'history.active', + list: 'history.sessions' +}; + +function BrowserSession() { + this.timer = undefined; this.active = true; this.tabCount = 0; this.startTime = (new Date()).getTime(); this.lastSave = this.startTime; - this.windows = { }; -}; + this.windows = {}; +} -var WindowSession = function WindowSession(id) -{ +function WindowSession(id) { this.id = id; this.tabs = []; this.active = true; this.saveTime = (new Date()).getTime(); -}; +} + +BrowserSession.prototype.update = function update() { + if (typeof browser === 'undefined' || !browser.windows) return; -BrowserSession.prototype.update = function update() -{ // Prepare session information browser.windows.getAll({ populate: true, windowTypes: ['normal'] }) - .then(function (mozWindows) { - var tabCount = 0; + .then(function (mozWindows) { + var tabCount = 0; - for (var key in this.windows) - { - this.windows[key].active = false; - } - - for (let mozWindow of mozWindows) - { - if (mozWindow.incognito && this.config.savePrivate == false) - { - delete this.windows[mozWindow.id]; - continue; + for (var key in this.windows) { + this.windows[key].active = false; } - var windowInfo = new WindowSession(mozWindow.id); - - // Add window tabs - for (var i in mozWindow.tabs) - { - var tab = mozWindow.tabs[i]; - var info = SessionManagement.getTabInfo(tab.url); - var sessionTab = { - title: tab.title, - url: info.url, - favIconUrl: info.lazyLoading ? info.favIconUrl : tab.favIconUrl, - pinned: tab.pinned, - cookieStoreId: tab.cookieStoreId, - }; - tabCount++; - windowInfo.tabs[tab.index] = sessionTab; - } + for (let mozWindow of mozWindows) { + if (mozWindow.incognito && (this.config && this.config.savePrivate == false)) { + delete this.windows[mozWindow.id]; + continue; + } - this.windows[mozWindow.id] = windowInfo; - } + var windowInfo = new WindowSession(mozWindow.id); + + // Add window tabs + for (var i in mozWindow.tabs) { + var tab = mozWindow.tabs[i]; + var info = SessionManagement.getTabInfo(tab.url); + var sessionTab = { + title: tab.title, + url: info.url, + favIconUrl: info.lazyLoading ? info.favIconUrl : tab.favIconUrl, + pinned: tab.pinned, + cookieStoreId: tab.cookieStoreId, + }; + tabCount++; + windowInfo.tabs[tab.index] = sessionTab; + } + + this.windows[mozWindow.id] = windowInfo; + } - // Update internal storage - this.lastSave = (new Date()).getTime(); - this.tabCount = tabCount; + // Update internal storage + this.lastSave = (new Date()).getTime(); + this.tabCount = tabCount; - // Save new session - browser.storage.local.set({ 'history.active' : this}); + // Save new session + if (browser.storage && browser.storage.local) { + browser.storage.local.set({ [storageKey.active]: this }); + } - }.bind(this)); + }.bind(this)); }; -BrowserSession.prototype.clear = function clear() -{ +BrowserSession.prototype.clear = function clear() { // Save new session this.windows = {}; - browser.storage.local.set({ 'history.active' : this}); + if (typeof browser !== 'undefined' && browser.storage && browser.storage.local) { + browser.storage.local.set({ [storageKey.active]: this }); + } }; -BrowserSession.prototype.start = function start(updateInterval) -{ +BrowserSession.prototype.start = function start(updateInterval) { if (this.timer == undefined) { // console.log('Session auto-save: start'); this.timer = setInterval(this.update.bind(this), updateInterval * 1000); @@ -88,8 +93,7 @@ BrowserSession.prototype.start = function start(updateInterval) } }; -BrowserSession.prototype.stop = function stop() -{ +BrowserSession.prototype.stop = function stop() { if (this.timer != undefined) { // console.log('Session auto-save: stop'); clearInterval(this.timer); @@ -97,8 +101,7 @@ BrowserSession.prototype.stop = function stop() } }; -BrowserSession.prototype.resetInterval = function resetInterval(updateInterval) -{ +BrowserSession.prototype.resetInterval = function resetInterval(updateInterval) { if (this.timer != undefined) { // console.log('Session auto-save: reset interval'); this.stop(); @@ -106,148 +109,147 @@ BrowserSession.prototype.resetInterval = function resetInterval(updateInterval) } }; -BrowserSession.prototype.setConfig = function setConfig(config) -{ - this.config = config; - this.resetInterval(config.interval); - config.enabled ? this.start(config.interval) : this.stop(); +BrowserSession.prototype.setConfig = function setConfig(conf) { + this.config = conf; + this.resetInterval(conf.interval); + conf.enabled ? this.start(conf.interval) : this.stop(); }; -var SessionAutoSave = (function SessionAutoSave() -{ - // ------------------------------------------------------------------------ - // Init - - var activeSession = new BrowserSession(); - var sessions = []; - var sessionsKey = 'history.sessions'; - var activeSessionKey = 'history.active'; - - var config = { - key: 'history.config', - enabled: true, - interval: 15, - saveBuffer: 3, - savingSlots: 20, - expireTimeHours: 48, // hours - savePrivate: false, - }; - - browser.storage.local.get(config.key) - .then(function onSuccess(obj) { - if (obj[config.key]) - { - config = obj[config.key]; - init(); - } - else - { - browser.storage.local.set({ - [config.key] : config - }).then(init); - } - - }, function onError() { - init(); - }); +let activeSession = new BrowserSession(); +let sessions = []; +let config = { + enabled: true, + interval: 15, + saveBuffer: 3, + savingSlots: 20, + expireTimeHours: 48, // hours + savePrivate: false, +}; - var upgradeStorage = function upgradeStorage() - { - for (var i = 0; i < sessions.length; i++) - { - // New storage works with WindowSession objects - if (sessions[i] != undefined && Array.isArray(sessions[i].windows[0]) == true) - { - for (var j = 0; j < sessions[i].windows.length; j++) { - var session = new WindowSession(0); - session.state = 'history'; - session.saveTime = sessions[i].lastSave; - session.tabs = sessions[i].windows[j]; - sessions[i].windows[j] = session; - } +const upgradeStorage = function upgradeStorage() { + for (var i = 0; i < sessions.length; i++) { + // New storage works with WindowSession objects + if (sessions[i] != undefined && Array.isArray(sessions[i].windows[0]) == true) { + for (var j = 0; j < sessions[i].windows.length; j++) { + var session = new WindowSession(0); + session.state = 'history'; + session.saveTime = sessions[i].lastSave; + session.tabs = sessions[i].windows[j]; + sessions[i].windows[j] = session; } } - }; - - var updateStorage = function updateStorage() - { - upgradeStorage(); + } +}; - var removeCount = Math.max(sessions.length - config.savingSlots, 0); - var expireTime = new Date(new Date() - config.expireTimeHours * 3600 * 1000); +const updateStorage = function updateStorage() { + upgradeStorage(); - var list = []; - // console.log('To remove', removeCount); + var removeCount = Math.max(sessions.length - config.savingSlots, 0); + var expireTime = new Date(new Date() - config.expireTimeHours * 3600 * 1000); - // compact free space and delete expired sessions - sessions.forEach(function (session) { - if (session != undefined && (removeCount <= 0 || session.lastSave > expireTime)) - { - list.push(session); - } - else - { - if (removeCount > 0) - removeCount--; - } - }); + var list = []; + // console.log('To remove', removeCount); - if (removeCount > 0) - { - list = list.slice(removeCount); + // compact free space and delete expired sessions + sessions.forEach(function (session) { + if (session != undefined && (removeCount <= 0 || session.lastSave > expireTime)) { + list.push(session); + } + else { + if (removeCount > 0) + removeCount--; } + }); + + if (removeCount > 0) { + list = list.slice(removeCount); + } + + // update session list + sessions = list; +}; - // update session list - sessions = list; - }; +const saveSessionsList = function saveSessionsList() { + if (typeof browser !== 'undefined' && browser.storage && browser.storage.local) { + browser.storage.local.set({ [storageKey.list]: sessions }); + } +}; - var init = function init() - { - browser.storage.local.get([activeSessionKey, sessionsKey]) - .then(function(obj) { +const internalInit = function internalInit() { + if (typeof browser === 'undefined' || !browser.storage || !browser.storage.local) return; - if (obj[sessionsKey]) { - sessions = obj[sessionsKey]; + browser.storage.local.get([storageKey.list]) + .then(function (obj) { + + if (obj[storageKey.list]) { + sessions = obj[storageKey.list]; updateStorage(); } // console.log(sessions); + saveSessionsList(); + + browser.storage.local.get([storageKey.active]) + .then(function (data) { + var session = data[storageKey.active]; + if (session) { + session.windows = Object.values(session.windows); + delete session.active; + sessions.push(session); + saveSessionsList(); + } + }); - if (obj[activeSessionKey]) { - obj[activeSessionKey].windows = Object.values(obj[activeSessionKey].windows); - delete obj[activeSessionKey].active; - sessions.push(obj[activeSessionKey]); - } - - browser.storage.local.set({ [sessionsKey] : sessions }); activeSession.setConfig(config); }); - }; - - // ------------------------------------------------------------------------ - // Events - - browser.runtime.onMessage.addListener(function (message) { - if (message.event == 'history.deleteActive') { - activeSession.clear(); - } - }); +}; - var onConfigChanged = function onConfigChanged(object) - { - if (object[config.key]) { - config = object[config.key].newValue; - activeSession.setConfig(config); - } - }; +const onConfigChanged = function onConfigChanged(object) { + if (object[storageKey.config]) { + config = object[storageKey.config].newValue; + activeSession.setConfig(config); + } +}; - browser.storage.onChanged.addListener(onConfigChanged); +export const SessionAutoSave = { + activeSession: activeSession, + init: function () { + if (typeof browser === 'undefined' || !browser.storage || !browser.storage.local) return; - // ------------------------------------------------------------------------ - // Public API + browser.storage.local.get(storageKey.config) + .then(function onSuccess(obj) { + if (obj[storageKey.config]) { + config = obj[storageKey.config]; + internalInit(); + } + else { + browser.storage.local.set({ + [storageKey.config]: config + }).then(internalInit); + } - return { - activeSession : activeSession - }; + }, function onError() { + internalInit(); + }); + + try { + if (browser.runtime && browser.runtime.onMessage) { + browser.runtime.onMessage.addListener(function (message) { + if (message && message.event == 'history.deleteActive') { + activeSession.clear(); + } + }); + } + } catch (err) { + console.warn('browser.runtime.onMessage.addListener failed in session-history', err); + } -})(); \ No newline at end of file + try { + if (browser.storage && browser.storage.onChanged) { + browser.storage.onChanged.addListener(onConfigChanged); + } + } catch (err) { + console.warn('browser.storage.onChanged.addListener failed in session-history', err); + } + } +}; \ No newline at end of file diff --git a/scripts/session-management.js b/scripts/session-management.js index 191d443..f008dc5 100644 --- a/scripts/session-management.js +++ b/scripts/session-management.js @@ -1,17 +1,17 @@ 'use strict'; -var SessionManagement = (function SessionManagement() -{ +import { FaviconService } from './favicon-service'; + +export const SessionManagement = (function SessionManagement() { var lazyLoadingKey = 'restore.lazy.loading'; - var lazyLoadingUrl = browser.extension.getURL('data/lazy/lazy.html'); + var lazyLoadingUrl = typeof browser !== 'undefined' && browser.runtime ? browser.runtime.getURL('data/lazy/lazy.html') : ''; var lazyLoading = false; var windowsTabsToKill = []; var reverseOrderKey = 'restore.reverse.order'; var reverseOrder = false; - function getLazyLoadingParameters(url) - { + function getLazyLoadingParameters(url) { let paramater = {}; let params = url.split('?'); if (params.length > 1) { @@ -23,10 +23,8 @@ var SessionManagement = (function SessionManagement() return paramater; } - function getTabInfo(url) - { - if (url.startsWith(lazyLoadingUrl)) - { + function getTabInfo(url) { + if (lazyLoadingUrl && url.startsWith(lazyLoadingUrl)) { var info = getLazyLoadingParameters(url); info.lazyLoading = true; return info; @@ -43,8 +41,7 @@ var SessionManagement = (function SessionManagement() // Parse a [container:] suffix from a bookmark title. // Returns { title, cookieStoreId } — cookieStoreId is null when absent. - function parseContainerFromTitle(rawTitle) - { + function parseContainerFromTitle(rawTitle) { var match = rawTitle && rawTitle.match(containerSuffix.regex); if (match) { return { title: match[1], cookieStoreId: match[2] }; @@ -55,18 +52,19 @@ var SessionManagement = (function SessionManagement() // Create a tab with a cookieStoreId; silently falls back to default container // if the container doesn't exist or the API rejects it. // onError is called only if creation fails even without a container. - function createTabWithContainer(options, cookieStoreId, onSuccess, onError) - { - var tryCreate = function(withContainer) { + function createTabWithContainer(options, cookieStoreId, onSuccess, onError) { + if (typeof browser === 'undefined' || !browser.tabs) return; + + var tryCreate = function (withContainer) { var tabOptions = Object.assign({}, options); if (withContainer && cookieStoreId) { tabOptions.cookieStoreId = cookieStoreId; } return browser.tabs.create(tabOptions).then( - function(tabInfo) { + function (tabInfo) { if (onSuccess) onSuccess(tabInfo); }, - function(err) { + function (err) { if (withContainer) { // Container not available — retry without it tryCreate(false); @@ -80,32 +78,36 @@ var SessionManagement = (function SessionManagement() tryCreate(cookieStoreId != null); } - var setLazyLoading = function setLazyLoading(state) - { + var setLazyLoading = function setLazyLoading(state) { + if (typeof browser === 'undefined' || !browser.tabs) return; + lazyLoading = state; - browser.tabs.onActivated.removeListener(onLazyTabActivated); - if (state) - { - browser.tabs.onActivated.addListener(onLazyTabActivated); + try { + browser.tabs.onActivated.removeListener(onLazyTabActivated); + if (state) { + browser.tabs.onActivated.addListener(onLazyTabActivated); + } + } catch (err) { + console.warn('browser.tabs.onActivated listener update failed', err); } }; - var initConfig = function initConfig() - { + var initConfig = function initConfig() { + if (typeof browser === 'undefined' || !browser.storage || !browser.storage.local) return; + browser.storage.local.get(lazyLoadingKey) - .then(function (obj) { - setLazyLoading(obj[lazyLoadingKey]); - }); + .then(function (obj) { + setLazyLoading(obj[lazyLoadingKey]); + }); browser.storage.local.get(reverseOrderKey) - .then(function (obj) { - reverseOrder = (obj[reverseOrderKey] == true); - }); + .then(function (obj) { + reverseOrder = (obj[reverseOrderKey] == true); + }); }; - var openTab = function openTab(options) - { - var checkFavicon = options.favicon === true ? FaviconService.checkTabFaviconSaved : () => {}; + var openTab = function openTab(options) { + var checkFavicon = options.favicon === true ? FaviconService.checkTabFaviconSaved : () => { }; delete options.favicon; var cookieStoreId = options.cookieStoreId || null; @@ -114,8 +116,7 @@ var SessionManagement = (function SessionManagement() var mode = options.mode; delete options.mode; - switch (mode) - { + switch (mode) { case 'newTab': { createTabWithContainer(options, cookieStoreId, checkFavicon); break; @@ -132,11 +133,11 @@ var SessionManagement = (function SessionManagement() } case 'activeTab': { - browser.tabs.query({active: true, windowId: browser.windows.WINDOW_ID_CURRENT}) - .then(tabs => browser.tabs.get(tabs[0].id)) - .then(tab => { - browser.tabs.update(tab.id, options).then(checkFavicon); - }); + browser.tabs.query({ active: true, windowId: browser.windows.WINDOW_ID_CURRENT }) + .then(tabs => browser.tabs.get(tabs[0].id)) + .then(tab => { + browser.tabs.update(tab.id, options).then(checkFavicon); + }); break; } @@ -145,12 +146,13 @@ var SessionManagement = (function SessionManagement() } }; - var createLazyTab = function createLazyTab(bookmark) - { - let title = 'data/lazy/lazy.html' + - '?state=redirect' + - '&title=' + encodeURIComponent(bookmark.title) + - '&url=' + encodeURIComponent(bookmark.url); + var createLazyTab = function createLazyTab(bookmark) { + if (typeof browser === 'undefined' || !browser.runtime) return; + + let title = browser.runtime.getURL('data/lazy/lazy.html') + + '?state=redirect' + + '&title=' + encodeURIComponent(bookmark.title) + + '&url=' + encodeURIComponent(bookmark.url); if (bookmark.favIconUrl) { title += '&favIconUrl=' + encodeURIComponent(bookmark.favIconUrl); @@ -164,20 +166,19 @@ var SessionManagement = (function SessionManagement() }; createTabWithContainer(tabOptions, bookmark.cookieStoreId, - function success() {}, + function success() { }, function error() { browser.tabs.create(tabOptions) - .then(function (tab) { - browser.tabs.update(tab.id, { - title: bookmark.title, + .then(function (tab) { + browser.tabs.update(tab.id, { + title: bookmark.title, + }); }); - }); } ); }; - var openLazyTab = function openLazyTab(bookmark) - { + var openLazyTab = function openLazyTab(bookmark) { // Extract container info encoded in the title var parsed = parseContainerFromTitle(bookmark.title); bookmark.title = parsed.title; @@ -186,8 +187,7 @@ var SessionManagement = (function SessionManagement() if (bookmark.favIconUrl) { createLazyTab(bookmark); } - else - { + else { FaviconService.getFaviconUrl(bookmark.url, function (favIconUrl) { bookmark.favIconUrl = favIconUrl; createLazyTab(bookmark); @@ -195,145 +195,129 @@ var SessionManagement = (function SessionManagement() } }; - var checkReverseOrder = function checkReverseOrder(bookmarks) - { - if (reverseOrder === true) - { + var checkReverseOrder = function checkReverseOrder(bookmarks) { + if (reverseOrder === true) { bookmarks = bookmarks.reverse(); } }; - var restoreSession = function restoreSession(folderID, options) - { - browser.bookmarks.getChildren(folderID) - .then(function (bookmarks) { - if (options && options.newWindow) - { - createSessionWindow(bookmarks) - .then(function(createdWindow) { - if (createdWindow) - { - if (options.closeAll) - { - browser.windows.getAll({ - populate: true, - windowTypes: ['normal'] - }) - .then(function (windows) { - console.log(windows, createdWindow); - windows.forEach(function (mozWidnow) { - if (mozWidnow.id != createdWindow.id) { - browser.windows.remove(mozWidnow.id); - } - }); - }); - } - - if (options.closeWindow) - { - browser.windows.remove(options.closeWindow); - } - } - }, function onError() { - console.log('strage error'); - }); - } - else - { - checkReverseOrder(bookmarks); - bookmarks.forEach(function (bookmark) { - - if (lazyLoading) - { - openLazyTab(bookmark); - } - else - { - var parsed = parseContainerFromTitle(bookmark.title); - createTabWithContainer( - { url: bookmark.url, active: false }, - parsed.cookieStoreId, - function(tabInfo) { FaviconService.checkTabFaviconSaved(tabInfo); } - ); - } - }); - } - }, - function fail() { - console.log('Error getting bookmarks from folder ID: ', folderID); - }); - }; - - var createSessionWindow = function createSessionWindow(bookmarks) - { - var action = new Promise((resolve, reject) => { - if (bookmarks.length > 0) - { - browser.windows.create({}) - .then(function(mozWindow) { - resolve(mozWindow); - var windowID = mozWindow.id; - windowsTabsToKill[windowID] = mozWindow.tabs[0].id; + var restoreSession = function restoreSession(folderID, options) { + if (typeof browser === 'undefined' || !browser.bookmarks) return; + browser.bookmarks.getChildren(folderID) + .then(function (bookmarks) { + if (options && options.newWindow) { + createSessionWindow(bookmarks) + .then(function (createdWindow) { + if (createdWindow) { + if (options.closeAll) { + browser.windows.getAll({ + populate: true, + windowTypes: ['normal'] + }) + .then(function (windows) { + windows.forEach(function (mozWidnow) { + if (mozWidnow.id != createdWindow.id) { + browser.windows.remove(mozWidnow.id); + } + }); + }); + } + + if (options.closeWindow) { + browser.windows.remove(options.closeWindow); + } + } + }, function onError() { + console.log('strage error'); + }); + } + else { checkReverseOrder(bookmarks); bookmarks.forEach(function (bookmark) { - if (lazyLoading) - { - bookmark.windowID = windowID; + + if (lazyLoading) { openLazyTab(bookmark); } - else - { + else { var parsed = parseContainerFromTitle(bookmark.title); createTabWithContainer( - { url: bookmark.url, pinned: bookmark.pinned, windowId: windowID }, + { url: bookmark.url, active: false }, parsed.cookieStoreId, - function(tabInfo) { FaviconService.checkTabFaviconSaved(tabInfo); } + function (tabInfo) { FaviconService.checkTabFaviconSaved(tabInfo); } ); } }); + } + }, + function fail() { + console.log('Error getting bookmarks from folder ID: ', folderID); }); + }; + + var createSessionWindow = function createSessionWindow(bookmarks) { + var action = new Promise((resolve, reject) => { + if (typeof browser === 'undefined' || !browser.windows) return reject(); + + if (bookmarks.length > 0) { + browser.windows.create({}) + .then(function (mozWindow) { + resolve(mozWindow); + var windowID = mozWindow.id; + windowsTabsToKill[windowID] = mozWindow.tabs[0].id; + + checkReverseOrder(bookmarks); + bookmarks.forEach(function (bookmark) { + if (lazyLoading) { + bookmark.windowID = windowID; + openLazyTab(bookmark); + } + else { + var parsed = parseContainerFromTitle(bookmark.title); + createTabWithContainer( + { url: bookmark.url, pinned: bookmark.pinned, windowId: windowID }, + parsed.cookieStoreId, + function (tabInfo) { FaviconService.checkTabFaviconSaved(tabInfo); } + ); + } + }); + }); } - else - { + else { reject(); } }); return action; }; - var restoreSessions = function restoreSessions(windows) - { - windows.forEach (function (bookmarks) { + var restoreSessions = function restoreSessions(windows) { + windows.forEach(function (bookmarks) { createSessionWindow(bookmarks); }); }; - var restoreLazyTab = function restoreLazyTab(tab, url) - { + var restoreLazyTab = function restoreLazyTab(tab, url) { + if (typeof browser === 'undefined' || !browser.tabs) return; + let redirectInfo = getLazyLoadingParameters(url ? url : tab.url); - if (redirectInfo.state == 'redirect') - { + if (redirectInfo.state == 'redirect') { browser.tabs.update(tab.id, { url: redirectInfo.url }) - .then(function(tabInfo) { - tabInfo.url = redirectInfo.url; - FaviconService.checkTabFaviconSaved(tabInfo); - }); + .then(function (tabInfo) { + tabInfo.url = redirectInfo.url; + FaviconService.checkTabFaviconSaved(tabInfo); + }); } }; // ************************************************************************ // Events - var onLazyTabActivated = function onLazyTabActivated(tab) - { - if (lazyLoading) - { + var onLazyTabActivated = function onLazyTabActivated(tab) { + if (lazyLoading && typeof browser !== 'undefined' && browser.tabs) { browser.tabs.get(tab.tabId).then(function (activeTab) { - if (activeTab.url.startsWith('moz-extension')) - { + if (activeTab.url.startsWith('moz-extension')) { restoreLazyTab(activeTab); } else if (activeTab.status == 'loading') { @@ -343,18 +327,15 @@ var SessionManagement = (function SessionManagement() } }; - var fixLazySession = function fixLazySession(folderID) - { + var fixLazySession = function fixLazySession(folderID) { + if (typeof browser === 'undefined' || !browser.bookmarks) return; + var result = browser.bookmarks.getChildren(folderID); result.then(function (bookmarks) { - console.log(bookmarks); - bookmarks.forEach(function (bookmark) { - if (bookmark.url.startsWith('moz-extension')) - { + if (bookmark.url.startsWith('moz-extension')) { let redirectInfo = getLazyLoadingParameters(bookmark.url); if (redirectInfo.url != undefined) { - console.log(bookmark, redirectInfo); browser.bookmarks.update(bookmark.id, { url: redirectInfo.url }); @@ -366,61 +347,79 @@ var SessionManagement = (function SessionManagement() }); }; - browser.runtime.onMessage.addListener(function (message) { - switch (message.event) - { - case 'restore-session': - restoreSession(message.bookmarkID, message.options); - break; + function init() { + if (typeof browser === 'undefined') return; - case 'restore-sessions': - restoreSessions(message.windows); - break; + try { + if (browser.runtime && browser.runtime.onMessage) { + browser.runtime.onMessage.addListener(function (message) { + switch (message.event) { + case 'restore-session': + restoreSession(message.bookmarkID, message.options); + break; - case 'save-favicon': - FaviconService.checkTabFaviconSaved(message.tab, false); - break; + case 'restore-sessions': + restoreSessions(message.windows); + break; - case 'open-tab': - openTab(message.options); - break; + case 'save-favicon': + FaviconService.checkTabFaviconSaved(message.tab, false); + break; - case 'fix-lazy-session': - fixLazySession(message.bookmarkID); - break; + case 'open-tab': + openTab(message.options); + break; - } - }); + case 'fix-lazy-session': + fixLazySession(message.bookmarkID); + break; - browser.storage.onChanged.addListener(function onChange(object) { - if (object[lazyLoadingKey]) { - setLazyLoading(object[lazyLoadingKey].newValue); - } - if (object[reverseOrderKey]) { - reverseOrder = object[reverseOrderKey].newValue == true; + } + }); + } + } catch (err) { + console.warn('browser.runtime.onMessage.addListener failed in session-management', err); } - }); - - browser.tabs.onCreated.addListener(function (tab) { - var killTabID = windowsTabsToKill[tab.windowId]; - if (killTabID != undefined && killTabID != tab.id) - { - browser.tabs.remove(killTabID); - windowsTabsToKill[tab.windowId] = undefined; - restoreLazyTab(tab, tab.title); + + try { + if (browser.storage && browser.storage.onChanged) { + browser.storage.onChanged.addListener(function onChange(object) { + if (object[lazyLoadingKey]) { + setLazyLoading(object[lazyLoadingKey].newValue); + } + if (object[reverseOrderKey]) { + reverseOrder = object[reverseOrderKey].newValue == true; + } + }); + } + } catch (err) { + console.warn('browser.storage.onChanged.addListener failed in session-management', err); } - }); - // ************************************************************************ - // Init + try { + if (browser.tabs && browser.tabs.onCreated) { + browser.tabs.onCreated.addListener(function (tab) { + var killTabID = windowsTabsToKill[tab.windowId]; + if (killTabID != undefined && killTabID != tab.id) { + browser.tabs.remove(killTabID); + windowsTabsToKill[tab.windowId] = undefined; + restoreLazyTab(tab, tab.title); + } + }); + } + } catch (err) { + console.warn('browser.tabs.onCreated.addListener failed in session-management', err); + } - initConfig(); + initConfig(); + } // ************************************************************************ // Public API return { - getTabInfo : getTabInfo + getTabInfo: getTabInfo, + init: init }; })(); diff --git a/wxt.config.js b/wxt.config.js new file mode 100644 index 0000000..d41877b --- /dev/null +++ b/wxt.config.js @@ -0,0 +1,69 @@ +import 'dotenv/config'; +import { defineConfig } from 'wxt'; + +console.log('FIREFOX_BINARY:', process.env.FIREFOX_BINARY); +console.log('FIREFOX_PROFILE:', process.env.FIREFOX_PROFILE); + +export default defineConfig(() => { + return { + browser: 'firefox', + webExt: { + binaries: { + firefox: process.env.FIREFOX_BINARY, + }, + firefoxProfile: process.env.FIREFOX_PROFILE, + keepProfileChanges: true, + }, + manifest: { + name: 'Session Sync', + version: '3.2.0', + author: 'Gabriel Ivanica', + description: 'Save sessions as bookmarks and sync them through Firefox Sync (or any other sync engine).\nPowerful session management - organize, edit, change, save, restore, etc\n', + homepage_url: 'https://github.com/ReDEnergy/SessionSync', + manifest_version: 2, + default_locale: 'en', + browser_specific_settings: { + gecko: { + data_collection_permissions: { + required: ['none'] + } + } + }, + permissions: [ + 'storage', + 'bookmarks', + 'tabs', + 'clipboardWrite', + 'unlimitedStorage', + 'cookies', + 'contextualIdentities' + ], + commands: { + '_execute_browser_action': { + suggested_key: { + 'default': 'Ctrl+Shift+F' + }, + description: 'Toggle Widget' + }, + 'session_sync_tutorial': { + suggested_key: { + 'default': 'Ctrl+Shift+L' + }, + description: 'Tutorial' + }, + 'session_sync_detach_tab': { + suggested_key: { + 'default': 'Ctrl+Shift+U' + }, + description: 'Tab View' + }, + 'session_sync_detach_window': { + suggested_key: { + 'default': 'Ctrl+Shift+V' + }, + description: 'Window View' + } + } + } + }; +}); From 7c1c3bfbbbb13af03428396a70af5feb0c022ede Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Tue, 7 Apr 2026 21:48:08 +0300 Subject: [PATCH 17/32] Optimized WXT config and project structure --- .env | 3 +- {dev => assets}/art/logo.psd | Bin {dev => assets}/art/menu-buttons.psd | Bin {dev => assets}/css/_style.css | 0 {dev => assets}/css/components.css | 0 {dev => assets}/css/config-panel.css | 0 {dev => assets}/css/confirm-box.css | 0 {dev => assets}/css/context-menu.css | 0 {dev => assets}/css/field-widget.css | 0 {dev => assets}/css/filter-panel.css | 0 {dev => assets}/css/generic.css | 0 {dev => assets}/css/hotkey-manager.css | 0 assets/css/index.css | 19 + {dev => assets}/css/notification-system.css | 0 {dev => assets}/css/search-bar.css | 0 {dev => assets}/css/session-container.css | 0 {dev => assets}/css/session-header-bar.css | 0 {dev => assets}/css/session-sorting.css | 0 {dev => assets}/css/session-toolbar.css | 0 {dev => assets}/css/sessions-list.css | 0 {dev => assets}/css/tooltip-system.css | 0 {dev => assets}/css/trash-can.css | 0 {dev => assets}/css/url-bar.css | 0 {dev => assets}/design/design.psd | Bin {data => assets}/images/icons/anchor.png | Bin {data => assets}/images/icons/aperture.png | Bin {data => assets}/images/icons/arrow-down.png | Bin {data => assets}/images/icons/arrow-up.png | Bin {data => assets}/images/icons/art.png | Bin {data => assets}/images/icons/barchart.png | Bin {data => assets}/images/icons/batteryfull.png | Bin {data => assets}/images/icons/batterylow.png | Bin {data => assets}/images/icons/bike.png | Bin {data => assets}/images/icons/biker.png | Bin {data => assets}/images/icons/bikewheel.png | Bin {data => assets}/images/icons/blimp.png | Bin {data => assets}/images/icons/bolt.png | Bin {data => assets}/images/icons/bomb.png | Bin .../images/icons/brush-pencil.png | Bin {data => assets}/images/icons/calculator.png | Bin {data => assets}/images/icons/calendar.png | Bin {data => assets}/images/icons/camera.png | Bin {data => assets}/images/icons/car.png | Bin {data => assets}/images/icons/cart.png | Bin {data => assets}/images/icons/carwheel.png | Bin {data => assets}/images/icons/caution.png | Bin {data => assets}/images/icons/chat.png | Bin {data => assets}/images/icons/check.png | Bin .../images/icons/circlecompass.png | Bin {data => assets}/images/icons/clapboard.png | Bin {data => assets}/images/icons/clock.png | Bin {data => assets}/images/icons/cloud.png | Bin {data => assets}/images/icons/cmyk.png | Bin {data => assets}/images/icons/colorwheel.png | Bin {data => assets}/images/icons/compass.png | Bin {data => assets}/images/icons/compose.png | Bin {data => assets}/images/icons/computer.png | Bin {data => assets}/images/icons/cone.png | Bin {data => assets}/images/icons/contacts.png | Bin {data => assets}/images/icons/contrast.png | Bin {data => assets}/images/icons/countdown.png | Bin {data => assets}/images/icons/creditcard.png | Bin {data => assets}/images/icons/crop.png | Bin {data => assets}/images/icons/crossroads.png | Bin {data => assets}/images/icons/cruise.png | Bin {data => assets}/images/icons/cursor.png | Bin {data => assets}/images/icons/dev.png | Bin {data => assets}/images/icons/die.png | Bin {data => assets}/images/icons/dolly.png | Bin {data => assets}/images/icons/door.png | Bin {data => assets}/images/icons/download.png | Bin {data => assets}/images/icons/easel.png | Bin {data => assets}/images/icons/email.png | Bin {data => assets}/images/icons/eye.png | Bin {data => assets}/images/icons/eyedropper.png | Bin {data => assets}/images/icons/fashion.png | Bin {data => assets}/images/icons/filmreel.png | Bin {data => assets}/images/icons/filmroll.png | Bin {data => assets}/images/icons/flag.png | Bin {data => assets}/images/icons/flame.png | Bin {data => assets}/images/icons/flash.png | Bin {data => assets}/images/icons/flower.png | Bin {data => assets}/images/icons/focus.png | Bin {data => assets}/images/icons/folder.png | Bin .../images/icons/gamecontroller.png | Bin {data => assets}/images/icons/gas.png | Bin {data => assets}/images/icons/genius.png | Bin {data => assets}/images/icons/global.png | Bin {data => assets}/images/icons/gps.png | Bin .../images/icons/half-loading.png | Bin {data => assets}/images/icons/hazard.png | Bin {data => assets}/images/icons/helicopter.png | Bin {data => assets}/images/icons/hotair.png | Bin {data => assets}/images/icons/hourglass.png | Bin {data => assets}/images/icons/image.png | Bin {data => assets}/images/icons/interstate.png | Bin {data => assets}/images/icons/key.png | Bin {data => assets}/images/icons/keyboard.png | Bin {data => assets}/images/icons/lens.png | Bin {data => assets}/images/icons/lightbulb.png | Bin {data => assets}/images/icons/loading.png | Bin {data => assets}/images/icons/location.png | Bin {data => assets}/images/icons/locked.png | Bin {data => assets}/images/icons/magicwand.png | Bin .../images/icons/magnifyingglass.png | Bin {data => assets}/images/icons/map.png | Bin {data => assets}/images/icons/megaphone.png | Bin {data => assets}/images/icons/megaphone2.png | Bin {data => assets}/images/icons/memorycard.png | Bin {data => assets}/images/icons/mic.png | Bin {data => assets}/images/icons/microphone.png | Bin {data => assets}/images/icons/money.png | Bin {data => assets}/images/icons/motorcycle.png | Bin {data => assets}/images/icons/music.png | Bin {data => assets}/images/icons/news.png | Bin {data => assets}/images/icons/paintbrush.png | Bin {data => assets}/images/icons/paintbrush2.png | Bin {data => assets}/images/icons/paintcan.png | Bin {data => assets}/images/icons/paintroller.png | Bin {data => assets}/images/icons/parachute.png | Bin {data => assets}/images/icons/pencil.png | Bin {data => assets}/images/icons/phone.png | Bin {data => assets}/images/icons/piechart.png | Bin {data => assets}/images/icons/plane.png | Bin {data => assets}/images/icons/play.png | Bin {data => assets}/images/icons/plugin.png | Bin {data => assets}/images/icons/polaroid.png | Bin .../images/icons/polaroidcamera.png | Bin {data => assets}/images/icons/polaroids.png | Bin {data => assets}/images/icons/present.png | Bin {data => assets}/images/icons/profle.png | Bin {data => assets}/images/icons/quote.png | Bin {data => assets}/images/icons/racingflags.png | Bin {data => assets}/images/icons/radio.png | Bin {data => assets}/images/icons/radiotower.png | Bin {data => assets}/images/icons/rainbow.png | Bin {data => assets}/images/icons/rgb.png | Bin {data => assets}/images/icons/roadblock.png | Bin {data => assets}/images/icons/rocket.png | Bin .../images/icons/rulertriangle.png | Bin {data => assets}/images/icons/running.png | Bin {data => assets}/images/icons/sailboat.png | Bin {data => assets}/images/icons/schooolbus.png | Bin {data => assets}/images/icons/scissors.png | Bin {data => assets}/images/icons/scooter.png | Bin {data => assets}/images/icons/security.png | Bin {data => assets}/images/icons/selftimer.png | Bin {data => assets}/images/icons/settings.png | Bin {data => assets}/images/icons/shipwheel.png | Bin {data => assets}/images/icons/shoeprints.png | Bin {data => assets}/images/icons/shop.png | Bin {data => assets}/images/icons/skateboard.png | Bin {data => assets}/images/icons/slr.png | Bin {data => assets}/images/icons/smartphone.png | Bin .../images/icons/spaceshuttle.png | Bin {data => assets}/images/icons/speaker.png | Bin {data => assets}/images/icons/speedometer.png | Bin {data => assets}/images/icons/spraypaint.png | Bin .../images/icons/steeringwheel.png | Bin {data => assets}/images/icons/stop.png | Bin {data => assets}/images/icons/sub.png | Bin {data => assets}/images/icons/submarine.png | Bin {data => assets}/images/icons/support.png | Bin {data => assets}/images/icons/swatches.png | Bin {data => assets}/images/icons/tablet.png | Bin {data => assets}/images/icons/takeoff.png | Bin {data => assets}/images/icons/target.png | Bin {data => assets}/images/icons/taxi.png | Bin {data => assets}/images/icons/toolbox.png | Bin {data => assets}/images/icons/tractor.png | Bin {data => assets}/images/icons/traffic.png | Bin {data => assets}/images/icons/train.png | Bin {data => assets}/images/icons/travelerbag.png | Bin {data => assets}/images/icons/trends.png | Bin {data => assets}/images/icons/tripod.png | Bin {data => assets}/images/icons/trophy.png | Bin {data => assets}/images/icons/truck.png | Bin {data => assets}/images/icons/tv.png | Bin {data => assets}/images/icons/typography.png | Bin {data => assets}/images/icons/ufo.png | Bin {data => assets}/images/icons/umbrella.png | Bin {data => assets}/images/icons/unicycle.png | Bin {data => assets}/images/icons/unlocked.png | Bin {data => assets}/images/icons/upload.png | Bin {data => assets}/images/icons/video.png | Bin .../images/icons/videocameraclassic.png | Bin .../images/icons/videocameracompact.png | Bin {data => assets}/images/icons/volume.png | Bin {data => assets}/images/icons/water.png | Bin {data => assets}/images/icons/weather.png | Bin {data => assets}/images/icons/windsock.png | Bin {data => assets}/images/icons/windy.png | Bin {data => assets}/images/icons/x.png | Bin {data => assets}/images/icons/zoomin.png | Bin {data => assets}/images/icons/zoomout.png | Bin data/session-sync.css | 2771 ----------------- data/session-sync.html | 16 - entrypoints/background.js | 14 - entrypoints/popup/main.js | 23 - package.json | 4 +- {data => public}/images/arrow-down.png | Bin {data => public}/images/arrow-left.png | Bin {data => public}/images/arrow-right.png | Bin {data => public}/images/arrow-up.png | Bin {data => public}/images/favicon.png | Bin {data => public}/images/github-logo.png | Bin {data => public}/images/github.png | Bin {data => public}/images/grain.png | Bin {data => public}/images/grain_header.png | Bin {data => public}/images/icon128.png | Bin {data => public}/images/icon16.png | Bin {data => public}/images/icon32.png | Bin {data => public}/images/icon48.png | Bin {data => public}/images/icon64.png | Bin {data => public}/images/icon96.png | Bin {data => public}/images/icons/booklet.png | Bin {data => public}/images/icons/bookshelf.png | Bin {data => public}/images/icons/briefcase.png | Bin {data => public}/images/icons/brightness.png | Bin {data => public}/images/icons/browser.png | Bin {data => public}/images/icons/clipboard.png | Bin {data => public}/images/icons/denied.png | Bin {data => public}/images/icons/document.png | Bin {data => public}/images/icons/frames.png | Bin {data => public}/images/icons/gear.png | Bin {data => public}/images/icons/globe.png | Bin {data => public}/images/icons/heart.png | Bin {data => public}/images/icons/mail.png | Bin {data => public}/images/icons/merge.png | Bin {data => public}/images/icons/pin.png | Bin {data => public}/images/icons/pin2.png | Bin {data => public}/images/icons/power.png | Bin {data => public}/images/icons/recycle.png | Bin {data => public}/images/icons/ribbon.png | Bin {data => public}/images/icons/stack.png | Bin {data => public}/images/icons/star.png | Bin {data => public}/images/icons/tools.png | Bin {data => public}/images/icons_v2/menu.png | Bin {data => public}/images/icons_v2/more.png | Bin {data => public}/images/logo.png | Bin {data => public}/images/mail.png | Bin .../images/material_icons/repeat.svg | 0 .../images/material_icons/repeat_one.svg | 0 {data => public}/images/menu.png | Bin {data => public}/images/resize.png | Bin {data => public}/images/sort.png | Bin {data => public}/images/trash-can.png | Bin .../images/tutorial/addon-menu.png | Bin .../images/tutorial/config-panel.png | Bin .../images/tutorial/context-menus.png | Bin .../images/tutorial/delete-item.png | Bin .../images/tutorial/detach-mode.png | Bin .../images/tutorial/export-import.png | Bin .../images/tutorial/history-list.png | Bin .../images/tutorial/main-menu.png | Bin {data => public}/images/tutorial/overview.png | Bin .../images/tutorial/resize-sessions.png | Bin .../images/tutorial/resize-ui.png | Bin .../images/tutorial/session-context-menu.png | Bin .../images/tutorial/session-edit.png | Bin .../images/tutorial/session-filter.png | Bin .../images/tutorial/session-menu.png | Bin .../images/tutorial/session-save-settings.png | Bin .../images/tutorial/session-sorting.png | Bin {data => public}/images/tutorial/sessions.png | Bin {data => public}/images/tutorial/url-bar.png | Bin {scripts => src/background}/development.js | 0 .../background}/favicon-service.js | 0 {scripts => src/background}/main.js | 6 +- .../background}/session-history.js | 0 .../background}/session-management.js | 4 +- src/entrypoints/background.js | 14 + .../entrypoints}/popup/index.html | 0 src/entrypoints/popup/main.js | 5 + {data => src}/scripts/3rd/pubsub.js | 0 {data => src}/scripts/app.js | 51 +- {data => src}/scripts/components/clipboard.js | 0 .../scripts/components/config-panel.js | 0 .../scripts/components/notification-system.js | 0 .../scripts/components/overlay-system.js | 0 .../components/session-bookmark-events.js | 0 .../scripts/components/session-bookmark.js | 0 .../scripts/components/session-container.js | 0 .../scripts/components/session-filtering.js | 0 .../components/session-folder-events.js | 0 .../scripts/components/session-folder.js | 0 .../scripts/components/session-header-bar.js | 0 .../scripts/components/session-history.js | 0 .../components/session-hotkey-manager.js | 0 .../scripts/components/session-hotkeys.js | 0 .../scripts/components/session-list.js | 0 .../scripts/components/session-sorting.js | 0 .../scripts/components/session-sync-model.js | 0 .../scripts/components/session-toolbar.js | 59 +- .../scripts/components/tooltip-system.js | 0 {data => src}/scripts/components/trash-can.js | 0 {data => src}/scripts/components/url-bar.js | 0 {data => src}/scripts/config.js | 122 +- {data => src}/scripts/session-sync-ui.js | 0 {data => src}/scripts/session/bookmarks.js | 93 +- {data => src}/scripts/session/management.js | 0 {data => src}/scripts/utils/auto-scroll.js | 0 {data => src}/scripts/utils/components.js | 0 {data => src}/scripts/utils/confirm-box.js | 0 {data => src}/scripts/utils/context-menu.js | 0 {data => src}/scripts/utils/dom.js | 0 {data => src}/scripts/utils/drag-context.js | 0 .../scripts/utils/field-edit-widget.js | 0 {data => src}/scripts/utils/general.js | 0 {data => src}/scripts/utils/global-events.js | 0 wxt.config.js | 37 +- 311 files changed, 216 insertions(+), 3025 deletions(-) rename {dev => assets}/art/logo.psd (100%) rename {dev => assets}/art/menu-buttons.psd (100%) rename {dev => assets}/css/_style.css (100%) rename {dev => assets}/css/components.css (100%) rename {dev => assets}/css/config-panel.css (100%) rename {dev => assets}/css/confirm-box.css (100%) rename {dev => assets}/css/context-menu.css (100%) rename {dev => assets}/css/field-widget.css (100%) rename {dev => assets}/css/filter-panel.css (100%) rename {dev => assets}/css/generic.css (100%) rename {dev => assets}/css/hotkey-manager.css (100%) create mode 100644 assets/css/index.css rename {dev => assets}/css/notification-system.css (100%) rename {dev => assets}/css/search-bar.css (100%) rename {dev => assets}/css/session-container.css (100%) rename {dev => assets}/css/session-header-bar.css (100%) rename {dev => assets}/css/session-sorting.css (100%) rename {dev => assets}/css/session-toolbar.css (100%) rename {dev => assets}/css/sessions-list.css (100%) rename {dev => assets}/css/tooltip-system.css (100%) rename {dev => assets}/css/trash-can.css (100%) rename {dev => assets}/css/url-bar.css (100%) rename {dev => assets}/design/design.psd (100%) rename {data => assets}/images/icons/anchor.png (100%) rename {data => assets}/images/icons/aperture.png (100%) rename {data => assets}/images/icons/arrow-down.png (100%) rename {data => assets}/images/icons/arrow-up.png (100%) rename {data => assets}/images/icons/art.png (100%) rename {data => assets}/images/icons/barchart.png (100%) rename {data => assets}/images/icons/batteryfull.png (100%) rename {data => assets}/images/icons/batterylow.png (100%) rename {data => assets}/images/icons/bike.png (100%) rename {data => assets}/images/icons/biker.png (100%) rename {data => assets}/images/icons/bikewheel.png (100%) rename {data => assets}/images/icons/blimp.png (100%) rename {data => assets}/images/icons/bolt.png (100%) rename {data => assets}/images/icons/bomb.png (100%) rename {data => assets}/images/icons/brush-pencil.png (100%) rename {data => assets}/images/icons/calculator.png (100%) rename {data => assets}/images/icons/calendar.png (100%) rename {data => assets}/images/icons/camera.png (100%) rename {data => assets}/images/icons/car.png (100%) rename {data => assets}/images/icons/cart.png (100%) rename {data => assets}/images/icons/carwheel.png (100%) rename {data => assets}/images/icons/caution.png (100%) rename {data => assets}/images/icons/chat.png (100%) rename {data => assets}/images/icons/check.png (100%) rename {data => assets}/images/icons/circlecompass.png (100%) rename {data => assets}/images/icons/clapboard.png (100%) rename {data => assets}/images/icons/clock.png (100%) rename {data => assets}/images/icons/cloud.png (100%) rename {data => assets}/images/icons/cmyk.png (100%) rename {data => assets}/images/icons/colorwheel.png (100%) rename {data => assets}/images/icons/compass.png (100%) rename {data => assets}/images/icons/compose.png (100%) rename {data => assets}/images/icons/computer.png (100%) rename {data => assets}/images/icons/cone.png (100%) rename {data => assets}/images/icons/contacts.png (100%) rename {data => assets}/images/icons/contrast.png (100%) rename {data => assets}/images/icons/countdown.png (100%) rename {data => assets}/images/icons/creditcard.png (100%) rename {data => assets}/images/icons/crop.png (100%) rename {data => assets}/images/icons/crossroads.png (100%) rename {data => assets}/images/icons/cruise.png (100%) rename {data => assets}/images/icons/cursor.png (100%) rename {data => assets}/images/icons/dev.png (100%) rename {data => assets}/images/icons/die.png (100%) rename {data => assets}/images/icons/dolly.png (100%) rename {data => assets}/images/icons/door.png (100%) rename {data => assets}/images/icons/download.png (100%) rename {data => assets}/images/icons/easel.png (100%) rename {data => assets}/images/icons/email.png (100%) rename {data => assets}/images/icons/eye.png (100%) rename {data => assets}/images/icons/eyedropper.png (100%) rename {data => assets}/images/icons/fashion.png (100%) rename {data => assets}/images/icons/filmreel.png (100%) rename {data => assets}/images/icons/filmroll.png (100%) rename {data => assets}/images/icons/flag.png (100%) rename {data => assets}/images/icons/flame.png (100%) rename {data => assets}/images/icons/flash.png (100%) rename {data => assets}/images/icons/flower.png (100%) rename {data => assets}/images/icons/focus.png (100%) rename {data => assets}/images/icons/folder.png (100%) rename {data => assets}/images/icons/gamecontroller.png (100%) rename {data => assets}/images/icons/gas.png (100%) rename {data => assets}/images/icons/genius.png (100%) rename {data => assets}/images/icons/global.png (100%) rename {data => assets}/images/icons/gps.png (100%) rename {data => assets}/images/icons/half-loading.png (100%) rename {data => assets}/images/icons/hazard.png (100%) rename {data => assets}/images/icons/helicopter.png (100%) rename {data => assets}/images/icons/hotair.png (100%) rename {data => assets}/images/icons/hourglass.png (100%) rename {data => assets}/images/icons/image.png (100%) rename {data => assets}/images/icons/interstate.png (100%) rename {data => assets}/images/icons/key.png (100%) rename {data => assets}/images/icons/keyboard.png (100%) rename {data => assets}/images/icons/lens.png (100%) rename {data => assets}/images/icons/lightbulb.png (100%) rename {data => assets}/images/icons/loading.png (100%) rename {data => assets}/images/icons/location.png (100%) rename {data => assets}/images/icons/locked.png (100%) rename {data => assets}/images/icons/magicwand.png (100%) rename {data => assets}/images/icons/magnifyingglass.png (100%) rename {data => assets}/images/icons/map.png (100%) rename {data => assets}/images/icons/megaphone.png (100%) rename {data => assets}/images/icons/megaphone2.png (100%) rename {data => assets}/images/icons/memorycard.png (100%) rename {data => assets}/images/icons/mic.png (100%) rename {data => assets}/images/icons/microphone.png (100%) rename {data => assets}/images/icons/money.png (100%) rename {data => assets}/images/icons/motorcycle.png (100%) rename {data => assets}/images/icons/music.png (100%) rename {data => assets}/images/icons/news.png (100%) rename {data => assets}/images/icons/paintbrush.png (100%) rename {data => assets}/images/icons/paintbrush2.png (100%) rename {data => assets}/images/icons/paintcan.png (100%) rename {data => assets}/images/icons/paintroller.png (100%) rename {data => assets}/images/icons/parachute.png (100%) rename {data => assets}/images/icons/pencil.png (100%) rename {data => assets}/images/icons/phone.png (100%) rename {data => assets}/images/icons/piechart.png (100%) rename {data => assets}/images/icons/plane.png (100%) rename {data => assets}/images/icons/play.png (100%) rename {data => assets}/images/icons/plugin.png (100%) rename {data => assets}/images/icons/polaroid.png (100%) rename {data => assets}/images/icons/polaroidcamera.png (100%) rename {data => assets}/images/icons/polaroids.png (100%) rename {data => assets}/images/icons/present.png (100%) rename {data => assets}/images/icons/profle.png (100%) rename {data => assets}/images/icons/quote.png (100%) rename {data => assets}/images/icons/racingflags.png (100%) rename {data => assets}/images/icons/radio.png (100%) rename {data => assets}/images/icons/radiotower.png (100%) rename {data => assets}/images/icons/rainbow.png (100%) rename {data => assets}/images/icons/rgb.png (100%) rename {data => assets}/images/icons/roadblock.png (100%) rename {data => assets}/images/icons/rocket.png (100%) rename {data => assets}/images/icons/rulertriangle.png (100%) rename {data => assets}/images/icons/running.png (100%) rename {data => assets}/images/icons/sailboat.png (100%) rename {data => assets}/images/icons/schooolbus.png (100%) rename {data => assets}/images/icons/scissors.png (100%) rename {data => assets}/images/icons/scooter.png (100%) rename {data => assets}/images/icons/security.png (100%) rename {data => assets}/images/icons/selftimer.png (100%) rename {data => assets}/images/icons/settings.png (100%) rename {data => assets}/images/icons/shipwheel.png (100%) rename {data => assets}/images/icons/shoeprints.png (100%) rename {data => assets}/images/icons/shop.png (100%) rename {data => assets}/images/icons/skateboard.png (100%) rename {data => assets}/images/icons/slr.png (100%) rename {data => assets}/images/icons/smartphone.png (100%) rename {data => assets}/images/icons/spaceshuttle.png (100%) rename {data => assets}/images/icons/speaker.png (100%) rename {data => assets}/images/icons/speedometer.png (100%) rename {data => assets}/images/icons/spraypaint.png (100%) rename {data => assets}/images/icons/steeringwheel.png (100%) rename {data => assets}/images/icons/stop.png (100%) rename {data => assets}/images/icons/sub.png (100%) rename {data => assets}/images/icons/submarine.png (100%) rename {data => assets}/images/icons/support.png (100%) rename {data => assets}/images/icons/swatches.png (100%) rename {data => assets}/images/icons/tablet.png (100%) rename {data => assets}/images/icons/takeoff.png (100%) rename {data => assets}/images/icons/target.png (100%) rename {data => assets}/images/icons/taxi.png (100%) rename {data => assets}/images/icons/toolbox.png (100%) rename {data => assets}/images/icons/tractor.png (100%) rename {data => assets}/images/icons/traffic.png (100%) rename {data => assets}/images/icons/train.png (100%) rename {data => assets}/images/icons/travelerbag.png (100%) rename {data => assets}/images/icons/trends.png (100%) rename {data => assets}/images/icons/tripod.png (100%) rename {data => assets}/images/icons/trophy.png (100%) rename {data => assets}/images/icons/truck.png (100%) rename {data => assets}/images/icons/tv.png (100%) rename {data => assets}/images/icons/typography.png (100%) rename {data => assets}/images/icons/ufo.png (100%) rename {data => assets}/images/icons/umbrella.png (100%) rename {data => assets}/images/icons/unicycle.png (100%) rename {data => assets}/images/icons/unlocked.png (100%) rename {data => assets}/images/icons/upload.png (100%) rename {data => assets}/images/icons/video.png (100%) rename {data => assets}/images/icons/videocameraclassic.png (100%) rename {data => assets}/images/icons/videocameracompact.png (100%) rename {data => assets}/images/icons/volume.png (100%) rename {data => assets}/images/icons/water.png (100%) rename {data => assets}/images/icons/weather.png (100%) rename {data => assets}/images/icons/windsock.png (100%) rename {data => assets}/images/icons/windy.png (100%) rename {data => assets}/images/icons/x.png (100%) rename {data => assets}/images/icons/zoomin.png (100%) rename {data => assets}/images/icons/zoomout.png (100%) delete mode 100644 data/session-sync.css delete mode 100644 data/session-sync.html delete mode 100644 entrypoints/background.js delete mode 100644 entrypoints/popup/main.js rename {data => public}/images/arrow-down.png (100%) rename {data => public}/images/arrow-left.png (100%) rename {data => public}/images/arrow-right.png (100%) rename {data => public}/images/arrow-up.png (100%) rename {data => public}/images/favicon.png (100%) rename {data => public}/images/github-logo.png (100%) rename {data => public}/images/github.png (100%) rename {data => public}/images/grain.png (100%) rename {data => public}/images/grain_header.png (100%) rename {data => public}/images/icon128.png (100%) rename {data => public}/images/icon16.png (100%) rename {data => public}/images/icon32.png (100%) rename {data => public}/images/icon48.png (100%) rename {data => public}/images/icon64.png (100%) rename {data => public}/images/icon96.png (100%) rename {data => public}/images/icons/booklet.png (100%) rename {data => public}/images/icons/bookshelf.png (100%) rename {data => public}/images/icons/briefcase.png (100%) rename {data => public}/images/icons/brightness.png (100%) rename {data => public}/images/icons/browser.png (100%) rename {data => public}/images/icons/clipboard.png (100%) rename {data => public}/images/icons/denied.png (100%) rename {data => public}/images/icons/document.png (100%) rename {data => public}/images/icons/frames.png (100%) rename {data => public}/images/icons/gear.png (100%) rename {data => public}/images/icons/globe.png (100%) rename {data => public}/images/icons/heart.png (100%) rename {data => public}/images/icons/mail.png (100%) rename {data => public}/images/icons/merge.png (100%) rename {data => public}/images/icons/pin.png (100%) rename {data => public}/images/icons/pin2.png (100%) rename {data => public}/images/icons/power.png (100%) rename {data => public}/images/icons/recycle.png (100%) rename {data => public}/images/icons/ribbon.png (100%) rename {data => public}/images/icons/stack.png (100%) rename {data => public}/images/icons/star.png (100%) rename {data => public}/images/icons/tools.png (100%) rename {data => public}/images/icons_v2/menu.png (100%) rename {data => public}/images/icons_v2/more.png (100%) rename {data => public}/images/logo.png (100%) rename {data => public}/images/mail.png (100%) rename {data => public}/images/material_icons/repeat.svg (100%) rename {data => public}/images/material_icons/repeat_one.svg (100%) rename {data => public}/images/menu.png (100%) rename {data => public}/images/resize.png (100%) rename {data => public}/images/sort.png (100%) rename {data => public}/images/trash-can.png (100%) rename {data => public}/images/tutorial/addon-menu.png (100%) rename {data => public}/images/tutorial/config-panel.png (100%) rename {data => public}/images/tutorial/context-menus.png (100%) rename {data => public}/images/tutorial/delete-item.png (100%) rename {data => public}/images/tutorial/detach-mode.png (100%) rename {data => public}/images/tutorial/export-import.png (100%) rename {data => public}/images/tutorial/history-list.png (100%) rename {data => public}/images/tutorial/main-menu.png (100%) rename {data => public}/images/tutorial/overview.png (100%) rename {data => public}/images/tutorial/resize-sessions.png (100%) rename {data => public}/images/tutorial/resize-ui.png (100%) rename {data => public}/images/tutorial/session-context-menu.png (100%) rename {data => public}/images/tutorial/session-edit.png (100%) rename {data => public}/images/tutorial/session-filter.png (100%) rename {data => public}/images/tutorial/session-menu.png (100%) rename {data => public}/images/tutorial/session-save-settings.png (100%) rename {data => public}/images/tutorial/session-sorting.png (100%) rename {data => public}/images/tutorial/sessions.png (100%) rename {data => public}/images/tutorial/url-bar.png (100%) rename {scripts => src/background}/development.js (100%) rename {scripts => src/background}/favicon-service.js (100%) rename {scripts => src/background}/main.js (97%) rename {scripts => src/background}/session-history.js (100%) rename {scripts => src/background}/session-management.js (98%) create mode 100644 src/entrypoints/background.js rename {entrypoints => src/entrypoints}/popup/index.html (100%) create mode 100644 src/entrypoints/popup/main.js rename {data => src}/scripts/3rd/pubsub.js (100%) rename {data => src}/scripts/app.js (57%) rename {data => src}/scripts/components/clipboard.js (100%) rename {data => src}/scripts/components/config-panel.js (100%) rename {data => src}/scripts/components/notification-system.js (100%) rename {data => src}/scripts/components/overlay-system.js (100%) rename {data => src}/scripts/components/session-bookmark-events.js (100%) rename {data => src}/scripts/components/session-bookmark.js (100%) rename {data => src}/scripts/components/session-container.js (100%) rename {data => src}/scripts/components/session-filtering.js (100%) rename {data => src}/scripts/components/session-folder-events.js (100%) rename {data => src}/scripts/components/session-folder.js (100%) rename {data => src}/scripts/components/session-header-bar.js (100%) rename {data => src}/scripts/components/session-history.js (100%) rename {data => src}/scripts/components/session-hotkey-manager.js (100%) rename {data => src}/scripts/components/session-hotkeys.js (100%) rename {data => src}/scripts/components/session-list.js (100%) rename {data => src}/scripts/components/session-sorting.js (100%) rename {data => src}/scripts/components/session-sync-model.js (100%) rename {data => src}/scripts/components/session-toolbar.js (80%) rename {data => src}/scripts/components/tooltip-system.js (100%) rename {data => src}/scripts/components/trash-can.js (100%) rename {data => src}/scripts/components/url-bar.js (100%) rename {data => src}/scripts/config.js (58%) rename {data => src}/scripts/session-sync-ui.js (100%) rename {data => src}/scripts/session/bookmarks.js (73%) rename {data => src}/scripts/session/management.js (100%) rename {data => src}/scripts/utils/auto-scroll.js (100%) rename {data => src}/scripts/utils/components.js (100%) rename {data => src}/scripts/utils/confirm-box.js (100%) rename {data => src}/scripts/utils/context-menu.js (100%) rename {data => src}/scripts/utils/dom.js (100%) rename {data => src}/scripts/utils/drag-context.js (100%) rename {data => src}/scripts/utils/field-edit-widget.js (100%) rename {data => src}/scripts/utils/general.js (100%) rename {data => src}/scripts/utils/global-events.js (100%) diff --git a/.env b/.env index 744eeec..f047c1e 100644 --- a/.env +++ b/.env @@ -1,2 +1,3 @@ FIREFOX_BINARY="D:\Program Files\Firefox Developer Edition\firefox.exe" -FIREFOX_PROFILE="D:\Programs\Firefox\Addon" \ No newline at end of file +FIREFOX_PROFILE="D:\Programs\Firefox\Addon" +WXT_DEV_SERVER_HOST=127.0.0.1 \ No newline at end of file diff --git a/dev/art/logo.psd b/assets/art/logo.psd similarity index 100% rename from dev/art/logo.psd rename to assets/art/logo.psd diff --git a/dev/art/menu-buttons.psd b/assets/art/menu-buttons.psd similarity index 100% rename from dev/art/menu-buttons.psd rename to assets/art/menu-buttons.psd diff --git a/dev/css/_style.css b/assets/css/_style.css similarity index 100% rename from dev/css/_style.css rename to assets/css/_style.css diff --git a/dev/css/components.css b/assets/css/components.css similarity index 100% rename from dev/css/components.css rename to assets/css/components.css diff --git a/dev/css/config-panel.css b/assets/css/config-panel.css similarity index 100% rename from dev/css/config-panel.css rename to assets/css/config-panel.css diff --git a/dev/css/confirm-box.css b/assets/css/confirm-box.css similarity index 100% rename from dev/css/confirm-box.css rename to assets/css/confirm-box.css diff --git a/dev/css/context-menu.css b/assets/css/context-menu.css similarity index 100% rename from dev/css/context-menu.css rename to assets/css/context-menu.css diff --git a/dev/css/field-widget.css b/assets/css/field-widget.css similarity index 100% rename from dev/css/field-widget.css rename to assets/css/field-widget.css diff --git a/dev/css/filter-panel.css b/assets/css/filter-panel.css similarity index 100% rename from dev/css/filter-panel.css rename to assets/css/filter-panel.css diff --git a/dev/css/generic.css b/assets/css/generic.css similarity index 100% rename from dev/css/generic.css rename to assets/css/generic.css diff --git a/dev/css/hotkey-manager.css b/assets/css/hotkey-manager.css similarity index 100% rename from dev/css/hotkey-manager.css rename to assets/css/hotkey-manager.css diff --git a/assets/css/index.css b/assets/css/index.css new file mode 100644 index 0000000..fb6b156 --- /dev/null +++ b/assets/css/index.css @@ -0,0 +1,19 @@ +@import './_style.css'; +@import './components.css'; +@import './config-panel.css'; +@import './confirm-box.css'; +@import './context-menu.css'; +@import './field-widget.css'; +@import './filter-panel.css'; +@import './generic.css'; +@import './hotkey-manager.css'; +@import './notification-system.css'; +@import './search-bar.css'; +@import './session-container.css'; +@import './session-header-bar.css'; +@import './session-sorting.css'; +@import './session-toolbar.css'; +@import './sessions-list.css'; +@import './tooltip-system.css'; +@import './trash-can.css'; +@import './url-bar.css'; \ No newline at end of file diff --git a/dev/css/notification-system.css b/assets/css/notification-system.css similarity index 100% rename from dev/css/notification-system.css rename to assets/css/notification-system.css diff --git a/dev/css/search-bar.css b/assets/css/search-bar.css similarity index 100% rename from dev/css/search-bar.css rename to assets/css/search-bar.css diff --git a/dev/css/session-container.css b/assets/css/session-container.css similarity index 100% rename from dev/css/session-container.css rename to assets/css/session-container.css diff --git a/dev/css/session-header-bar.css b/assets/css/session-header-bar.css similarity index 100% rename from dev/css/session-header-bar.css rename to assets/css/session-header-bar.css diff --git a/dev/css/session-sorting.css b/assets/css/session-sorting.css similarity index 100% rename from dev/css/session-sorting.css rename to assets/css/session-sorting.css diff --git a/dev/css/session-toolbar.css b/assets/css/session-toolbar.css similarity index 100% rename from dev/css/session-toolbar.css rename to assets/css/session-toolbar.css diff --git a/dev/css/sessions-list.css b/assets/css/sessions-list.css similarity index 100% rename from dev/css/sessions-list.css rename to assets/css/sessions-list.css diff --git a/dev/css/tooltip-system.css b/assets/css/tooltip-system.css similarity index 100% rename from dev/css/tooltip-system.css rename to assets/css/tooltip-system.css diff --git a/dev/css/trash-can.css b/assets/css/trash-can.css similarity index 100% rename from dev/css/trash-can.css rename to assets/css/trash-can.css diff --git a/dev/css/url-bar.css b/assets/css/url-bar.css similarity index 100% rename from dev/css/url-bar.css rename to assets/css/url-bar.css diff --git a/dev/design/design.psd b/assets/design/design.psd similarity index 100% rename from dev/design/design.psd rename to assets/design/design.psd diff --git a/data/images/icons/anchor.png b/assets/images/icons/anchor.png similarity index 100% rename from data/images/icons/anchor.png rename to assets/images/icons/anchor.png diff --git a/data/images/icons/aperture.png b/assets/images/icons/aperture.png similarity index 100% rename from data/images/icons/aperture.png rename to assets/images/icons/aperture.png diff --git a/data/images/icons/arrow-down.png b/assets/images/icons/arrow-down.png similarity index 100% rename from data/images/icons/arrow-down.png rename to assets/images/icons/arrow-down.png diff --git a/data/images/icons/arrow-up.png b/assets/images/icons/arrow-up.png similarity index 100% rename from data/images/icons/arrow-up.png rename to assets/images/icons/arrow-up.png diff --git a/data/images/icons/art.png b/assets/images/icons/art.png similarity index 100% rename from data/images/icons/art.png rename to assets/images/icons/art.png diff --git a/data/images/icons/barchart.png b/assets/images/icons/barchart.png similarity index 100% rename from data/images/icons/barchart.png rename to assets/images/icons/barchart.png diff --git a/data/images/icons/batteryfull.png b/assets/images/icons/batteryfull.png similarity index 100% rename from data/images/icons/batteryfull.png rename to assets/images/icons/batteryfull.png diff --git a/data/images/icons/batterylow.png b/assets/images/icons/batterylow.png similarity index 100% rename from data/images/icons/batterylow.png rename to assets/images/icons/batterylow.png diff --git a/data/images/icons/bike.png b/assets/images/icons/bike.png similarity index 100% rename from data/images/icons/bike.png rename to assets/images/icons/bike.png diff --git a/data/images/icons/biker.png b/assets/images/icons/biker.png similarity index 100% rename from data/images/icons/biker.png rename to assets/images/icons/biker.png diff --git a/data/images/icons/bikewheel.png b/assets/images/icons/bikewheel.png similarity index 100% rename from data/images/icons/bikewheel.png rename to assets/images/icons/bikewheel.png diff --git a/data/images/icons/blimp.png b/assets/images/icons/blimp.png similarity index 100% rename from data/images/icons/blimp.png rename to assets/images/icons/blimp.png diff --git a/data/images/icons/bolt.png b/assets/images/icons/bolt.png similarity index 100% rename from data/images/icons/bolt.png rename to assets/images/icons/bolt.png diff --git a/data/images/icons/bomb.png b/assets/images/icons/bomb.png similarity index 100% rename from data/images/icons/bomb.png rename to assets/images/icons/bomb.png diff --git a/data/images/icons/brush-pencil.png b/assets/images/icons/brush-pencil.png similarity index 100% rename from data/images/icons/brush-pencil.png rename to assets/images/icons/brush-pencil.png diff --git a/data/images/icons/calculator.png b/assets/images/icons/calculator.png similarity index 100% rename from data/images/icons/calculator.png rename to assets/images/icons/calculator.png diff --git a/data/images/icons/calendar.png b/assets/images/icons/calendar.png similarity index 100% rename from data/images/icons/calendar.png rename to assets/images/icons/calendar.png diff --git a/data/images/icons/camera.png b/assets/images/icons/camera.png similarity index 100% rename from data/images/icons/camera.png rename to assets/images/icons/camera.png diff --git a/data/images/icons/car.png b/assets/images/icons/car.png similarity index 100% rename from data/images/icons/car.png rename to assets/images/icons/car.png diff --git a/data/images/icons/cart.png b/assets/images/icons/cart.png similarity index 100% rename from data/images/icons/cart.png rename to assets/images/icons/cart.png diff --git a/data/images/icons/carwheel.png b/assets/images/icons/carwheel.png similarity index 100% rename from data/images/icons/carwheel.png rename to assets/images/icons/carwheel.png diff --git a/data/images/icons/caution.png b/assets/images/icons/caution.png similarity index 100% rename from data/images/icons/caution.png rename to assets/images/icons/caution.png diff --git a/data/images/icons/chat.png b/assets/images/icons/chat.png similarity index 100% rename from data/images/icons/chat.png rename to assets/images/icons/chat.png diff --git a/data/images/icons/check.png b/assets/images/icons/check.png similarity index 100% rename from data/images/icons/check.png rename to assets/images/icons/check.png diff --git a/data/images/icons/circlecompass.png b/assets/images/icons/circlecompass.png similarity index 100% rename from data/images/icons/circlecompass.png rename to assets/images/icons/circlecompass.png diff --git a/data/images/icons/clapboard.png b/assets/images/icons/clapboard.png similarity index 100% rename from data/images/icons/clapboard.png rename to assets/images/icons/clapboard.png diff --git a/data/images/icons/clock.png b/assets/images/icons/clock.png similarity index 100% rename from data/images/icons/clock.png rename to assets/images/icons/clock.png diff --git a/data/images/icons/cloud.png b/assets/images/icons/cloud.png similarity index 100% rename from data/images/icons/cloud.png rename to assets/images/icons/cloud.png diff --git a/data/images/icons/cmyk.png b/assets/images/icons/cmyk.png similarity index 100% rename from data/images/icons/cmyk.png rename to assets/images/icons/cmyk.png diff --git a/data/images/icons/colorwheel.png b/assets/images/icons/colorwheel.png similarity index 100% rename from data/images/icons/colorwheel.png rename to assets/images/icons/colorwheel.png diff --git a/data/images/icons/compass.png b/assets/images/icons/compass.png similarity index 100% rename from data/images/icons/compass.png rename to assets/images/icons/compass.png diff --git a/data/images/icons/compose.png b/assets/images/icons/compose.png similarity index 100% rename from data/images/icons/compose.png rename to assets/images/icons/compose.png diff --git a/data/images/icons/computer.png b/assets/images/icons/computer.png similarity index 100% rename from data/images/icons/computer.png rename to assets/images/icons/computer.png diff --git a/data/images/icons/cone.png b/assets/images/icons/cone.png similarity index 100% rename from data/images/icons/cone.png rename to assets/images/icons/cone.png diff --git a/data/images/icons/contacts.png b/assets/images/icons/contacts.png similarity index 100% rename from data/images/icons/contacts.png rename to assets/images/icons/contacts.png diff --git a/data/images/icons/contrast.png b/assets/images/icons/contrast.png similarity index 100% rename from data/images/icons/contrast.png rename to assets/images/icons/contrast.png diff --git a/data/images/icons/countdown.png b/assets/images/icons/countdown.png similarity index 100% rename from data/images/icons/countdown.png rename to assets/images/icons/countdown.png diff --git a/data/images/icons/creditcard.png b/assets/images/icons/creditcard.png similarity index 100% rename from data/images/icons/creditcard.png rename to assets/images/icons/creditcard.png diff --git a/data/images/icons/crop.png b/assets/images/icons/crop.png similarity index 100% rename from data/images/icons/crop.png rename to assets/images/icons/crop.png diff --git a/data/images/icons/crossroads.png b/assets/images/icons/crossroads.png similarity index 100% rename from data/images/icons/crossroads.png rename to assets/images/icons/crossroads.png diff --git a/data/images/icons/cruise.png b/assets/images/icons/cruise.png similarity index 100% rename from data/images/icons/cruise.png rename to assets/images/icons/cruise.png diff --git a/data/images/icons/cursor.png b/assets/images/icons/cursor.png similarity index 100% rename from data/images/icons/cursor.png rename to assets/images/icons/cursor.png diff --git a/data/images/icons/dev.png b/assets/images/icons/dev.png similarity index 100% rename from data/images/icons/dev.png rename to assets/images/icons/dev.png diff --git a/data/images/icons/die.png b/assets/images/icons/die.png similarity index 100% rename from data/images/icons/die.png rename to assets/images/icons/die.png diff --git a/data/images/icons/dolly.png b/assets/images/icons/dolly.png similarity index 100% rename from data/images/icons/dolly.png rename to assets/images/icons/dolly.png diff --git a/data/images/icons/door.png b/assets/images/icons/door.png similarity index 100% rename from data/images/icons/door.png rename to assets/images/icons/door.png diff --git a/data/images/icons/download.png b/assets/images/icons/download.png similarity index 100% rename from data/images/icons/download.png rename to assets/images/icons/download.png diff --git a/data/images/icons/easel.png b/assets/images/icons/easel.png similarity index 100% rename from data/images/icons/easel.png rename to assets/images/icons/easel.png diff --git a/data/images/icons/email.png b/assets/images/icons/email.png similarity index 100% rename from data/images/icons/email.png rename to assets/images/icons/email.png diff --git a/data/images/icons/eye.png b/assets/images/icons/eye.png similarity index 100% rename from data/images/icons/eye.png rename to assets/images/icons/eye.png diff --git a/data/images/icons/eyedropper.png b/assets/images/icons/eyedropper.png similarity index 100% rename from data/images/icons/eyedropper.png rename to assets/images/icons/eyedropper.png diff --git a/data/images/icons/fashion.png b/assets/images/icons/fashion.png similarity index 100% rename from data/images/icons/fashion.png rename to assets/images/icons/fashion.png diff --git a/data/images/icons/filmreel.png b/assets/images/icons/filmreel.png similarity index 100% rename from data/images/icons/filmreel.png rename to assets/images/icons/filmreel.png diff --git a/data/images/icons/filmroll.png b/assets/images/icons/filmroll.png similarity index 100% rename from data/images/icons/filmroll.png rename to assets/images/icons/filmroll.png diff --git a/data/images/icons/flag.png b/assets/images/icons/flag.png similarity index 100% rename from data/images/icons/flag.png rename to assets/images/icons/flag.png diff --git a/data/images/icons/flame.png b/assets/images/icons/flame.png similarity index 100% rename from data/images/icons/flame.png rename to assets/images/icons/flame.png diff --git a/data/images/icons/flash.png b/assets/images/icons/flash.png similarity index 100% rename from data/images/icons/flash.png rename to assets/images/icons/flash.png diff --git a/data/images/icons/flower.png b/assets/images/icons/flower.png similarity index 100% rename from data/images/icons/flower.png rename to assets/images/icons/flower.png diff --git a/data/images/icons/focus.png b/assets/images/icons/focus.png similarity index 100% rename from data/images/icons/focus.png rename to assets/images/icons/focus.png diff --git a/data/images/icons/folder.png b/assets/images/icons/folder.png similarity index 100% rename from data/images/icons/folder.png rename to assets/images/icons/folder.png diff --git a/data/images/icons/gamecontroller.png b/assets/images/icons/gamecontroller.png similarity index 100% rename from data/images/icons/gamecontroller.png rename to assets/images/icons/gamecontroller.png diff --git a/data/images/icons/gas.png b/assets/images/icons/gas.png similarity index 100% rename from data/images/icons/gas.png rename to assets/images/icons/gas.png diff --git a/data/images/icons/genius.png b/assets/images/icons/genius.png similarity index 100% rename from data/images/icons/genius.png rename to assets/images/icons/genius.png diff --git a/data/images/icons/global.png b/assets/images/icons/global.png similarity index 100% rename from data/images/icons/global.png rename to assets/images/icons/global.png diff --git a/data/images/icons/gps.png b/assets/images/icons/gps.png similarity index 100% rename from data/images/icons/gps.png rename to assets/images/icons/gps.png diff --git a/data/images/icons/half-loading.png b/assets/images/icons/half-loading.png similarity index 100% rename from data/images/icons/half-loading.png rename to assets/images/icons/half-loading.png diff --git a/data/images/icons/hazard.png b/assets/images/icons/hazard.png similarity index 100% rename from data/images/icons/hazard.png rename to assets/images/icons/hazard.png diff --git a/data/images/icons/helicopter.png b/assets/images/icons/helicopter.png similarity index 100% rename from data/images/icons/helicopter.png rename to assets/images/icons/helicopter.png diff --git a/data/images/icons/hotair.png b/assets/images/icons/hotair.png similarity index 100% rename from data/images/icons/hotair.png rename to assets/images/icons/hotair.png diff --git a/data/images/icons/hourglass.png b/assets/images/icons/hourglass.png similarity index 100% rename from data/images/icons/hourglass.png rename to assets/images/icons/hourglass.png diff --git a/data/images/icons/image.png b/assets/images/icons/image.png similarity index 100% rename from data/images/icons/image.png rename to assets/images/icons/image.png diff --git a/data/images/icons/interstate.png b/assets/images/icons/interstate.png similarity index 100% rename from data/images/icons/interstate.png rename to assets/images/icons/interstate.png diff --git a/data/images/icons/key.png b/assets/images/icons/key.png similarity index 100% rename from data/images/icons/key.png rename to assets/images/icons/key.png diff --git a/data/images/icons/keyboard.png b/assets/images/icons/keyboard.png similarity index 100% rename from data/images/icons/keyboard.png rename to assets/images/icons/keyboard.png diff --git a/data/images/icons/lens.png b/assets/images/icons/lens.png similarity index 100% rename from data/images/icons/lens.png rename to assets/images/icons/lens.png diff --git a/data/images/icons/lightbulb.png b/assets/images/icons/lightbulb.png similarity index 100% rename from data/images/icons/lightbulb.png rename to assets/images/icons/lightbulb.png diff --git a/data/images/icons/loading.png b/assets/images/icons/loading.png similarity index 100% rename from data/images/icons/loading.png rename to assets/images/icons/loading.png diff --git a/data/images/icons/location.png b/assets/images/icons/location.png similarity index 100% rename from data/images/icons/location.png rename to assets/images/icons/location.png diff --git a/data/images/icons/locked.png b/assets/images/icons/locked.png similarity index 100% rename from data/images/icons/locked.png rename to assets/images/icons/locked.png diff --git a/data/images/icons/magicwand.png b/assets/images/icons/magicwand.png similarity index 100% rename from data/images/icons/magicwand.png rename to assets/images/icons/magicwand.png diff --git a/data/images/icons/magnifyingglass.png b/assets/images/icons/magnifyingglass.png similarity index 100% rename from data/images/icons/magnifyingglass.png rename to assets/images/icons/magnifyingglass.png diff --git a/data/images/icons/map.png b/assets/images/icons/map.png similarity index 100% rename from data/images/icons/map.png rename to assets/images/icons/map.png diff --git a/data/images/icons/megaphone.png b/assets/images/icons/megaphone.png similarity index 100% rename from data/images/icons/megaphone.png rename to assets/images/icons/megaphone.png diff --git a/data/images/icons/megaphone2.png b/assets/images/icons/megaphone2.png similarity index 100% rename from data/images/icons/megaphone2.png rename to assets/images/icons/megaphone2.png diff --git a/data/images/icons/memorycard.png b/assets/images/icons/memorycard.png similarity index 100% rename from data/images/icons/memorycard.png rename to assets/images/icons/memorycard.png diff --git a/data/images/icons/mic.png b/assets/images/icons/mic.png similarity index 100% rename from data/images/icons/mic.png rename to assets/images/icons/mic.png diff --git a/data/images/icons/microphone.png b/assets/images/icons/microphone.png similarity index 100% rename from data/images/icons/microphone.png rename to assets/images/icons/microphone.png diff --git a/data/images/icons/money.png b/assets/images/icons/money.png similarity index 100% rename from data/images/icons/money.png rename to assets/images/icons/money.png diff --git a/data/images/icons/motorcycle.png b/assets/images/icons/motorcycle.png similarity index 100% rename from data/images/icons/motorcycle.png rename to assets/images/icons/motorcycle.png diff --git a/data/images/icons/music.png b/assets/images/icons/music.png similarity index 100% rename from data/images/icons/music.png rename to assets/images/icons/music.png diff --git a/data/images/icons/news.png b/assets/images/icons/news.png similarity index 100% rename from data/images/icons/news.png rename to assets/images/icons/news.png diff --git a/data/images/icons/paintbrush.png b/assets/images/icons/paintbrush.png similarity index 100% rename from data/images/icons/paintbrush.png rename to assets/images/icons/paintbrush.png diff --git a/data/images/icons/paintbrush2.png b/assets/images/icons/paintbrush2.png similarity index 100% rename from data/images/icons/paintbrush2.png rename to assets/images/icons/paintbrush2.png diff --git a/data/images/icons/paintcan.png b/assets/images/icons/paintcan.png similarity index 100% rename from data/images/icons/paintcan.png rename to assets/images/icons/paintcan.png diff --git a/data/images/icons/paintroller.png b/assets/images/icons/paintroller.png similarity index 100% rename from data/images/icons/paintroller.png rename to assets/images/icons/paintroller.png diff --git a/data/images/icons/parachute.png b/assets/images/icons/parachute.png similarity index 100% rename from data/images/icons/parachute.png rename to assets/images/icons/parachute.png diff --git a/data/images/icons/pencil.png b/assets/images/icons/pencil.png similarity index 100% rename from data/images/icons/pencil.png rename to assets/images/icons/pencil.png diff --git a/data/images/icons/phone.png b/assets/images/icons/phone.png similarity index 100% rename from data/images/icons/phone.png rename to assets/images/icons/phone.png diff --git a/data/images/icons/piechart.png b/assets/images/icons/piechart.png similarity index 100% rename from data/images/icons/piechart.png rename to assets/images/icons/piechart.png diff --git a/data/images/icons/plane.png b/assets/images/icons/plane.png similarity index 100% rename from data/images/icons/plane.png rename to assets/images/icons/plane.png diff --git a/data/images/icons/play.png b/assets/images/icons/play.png similarity index 100% rename from data/images/icons/play.png rename to assets/images/icons/play.png diff --git a/data/images/icons/plugin.png b/assets/images/icons/plugin.png similarity index 100% rename from data/images/icons/plugin.png rename to assets/images/icons/plugin.png diff --git a/data/images/icons/polaroid.png b/assets/images/icons/polaroid.png similarity index 100% rename from data/images/icons/polaroid.png rename to assets/images/icons/polaroid.png diff --git a/data/images/icons/polaroidcamera.png b/assets/images/icons/polaroidcamera.png similarity index 100% rename from data/images/icons/polaroidcamera.png rename to assets/images/icons/polaroidcamera.png diff --git a/data/images/icons/polaroids.png b/assets/images/icons/polaroids.png similarity index 100% rename from data/images/icons/polaroids.png rename to assets/images/icons/polaroids.png diff --git a/data/images/icons/present.png b/assets/images/icons/present.png similarity index 100% rename from data/images/icons/present.png rename to assets/images/icons/present.png diff --git a/data/images/icons/profle.png b/assets/images/icons/profle.png similarity index 100% rename from data/images/icons/profle.png rename to assets/images/icons/profle.png diff --git a/data/images/icons/quote.png b/assets/images/icons/quote.png similarity index 100% rename from data/images/icons/quote.png rename to assets/images/icons/quote.png diff --git a/data/images/icons/racingflags.png b/assets/images/icons/racingflags.png similarity index 100% rename from data/images/icons/racingflags.png rename to assets/images/icons/racingflags.png diff --git a/data/images/icons/radio.png b/assets/images/icons/radio.png similarity index 100% rename from data/images/icons/radio.png rename to assets/images/icons/radio.png diff --git a/data/images/icons/radiotower.png b/assets/images/icons/radiotower.png similarity index 100% rename from data/images/icons/radiotower.png rename to assets/images/icons/radiotower.png diff --git a/data/images/icons/rainbow.png b/assets/images/icons/rainbow.png similarity index 100% rename from data/images/icons/rainbow.png rename to assets/images/icons/rainbow.png diff --git a/data/images/icons/rgb.png b/assets/images/icons/rgb.png similarity index 100% rename from data/images/icons/rgb.png rename to assets/images/icons/rgb.png diff --git a/data/images/icons/roadblock.png b/assets/images/icons/roadblock.png similarity index 100% rename from data/images/icons/roadblock.png rename to assets/images/icons/roadblock.png diff --git a/data/images/icons/rocket.png b/assets/images/icons/rocket.png similarity index 100% rename from data/images/icons/rocket.png rename to assets/images/icons/rocket.png diff --git a/data/images/icons/rulertriangle.png b/assets/images/icons/rulertriangle.png similarity index 100% rename from data/images/icons/rulertriangle.png rename to assets/images/icons/rulertriangle.png diff --git a/data/images/icons/running.png b/assets/images/icons/running.png similarity index 100% rename from data/images/icons/running.png rename to assets/images/icons/running.png diff --git a/data/images/icons/sailboat.png b/assets/images/icons/sailboat.png similarity index 100% rename from data/images/icons/sailboat.png rename to assets/images/icons/sailboat.png diff --git a/data/images/icons/schooolbus.png b/assets/images/icons/schooolbus.png similarity index 100% rename from data/images/icons/schooolbus.png rename to assets/images/icons/schooolbus.png diff --git a/data/images/icons/scissors.png b/assets/images/icons/scissors.png similarity index 100% rename from data/images/icons/scissors.png rename to assets/images/icons/scissors.png diff --git a/data/images/icons/scooter.png b/assets/images/icons/scooter.png similarity index 100% rename from data/images/icons/scooter.png rename to assets/images/icons/scooter.png diff --git a/data/images/icons/security.png b/assets/images/icons/security.png similarity index 100% rename from data/images/icons/security.png rename to assets/images/icons/security.png diff --git a/data/images/icons/selftimer.png b/assets/images/icons/selftimer.png similarity index 100% rename from data/images/icons/selftimer.png rename to assets/images/icons/selftimer.png diff --git a/data/images/icons/settings.png b/assets/images/icons/settings.png similarity index 100% rename from data/images/icons/settings.png rename to assets/images/icons/settings.png diff --git a/data/images/icons/shipwheel.png b/assets/images/icons/shipwheel.png similarity index 100% rename from data/images/icons/shipwheel.png rename to assets/images/icons/shipwheel.png diff --git a/data/images/icons/shoeprints.png b/assets/images/icons/shoeprints.png similarity index 100% rename from data/images/icons/shoeprints.png rename to assets/images/icons/shoeprints.png diff --git a/data/images/icons/shop.png b/assets/images/icons/shop.png similarity index 100% rename from data/images/icons/shop.png rename to assets/images/icons/shop.png diff --git a/data/images/icons/skateboard.png b/assets/images/icons/skateboard.png similarity index 100% rename from data/images/icons/skateboard.png rename to assets/images/icons/skateboard.png diff --git a/data/images/icons/slr.png b/assets/images/icons/slr.png similarity index 100% rename from data/images/icons/slr.png rename to assets/images/icons/slr.png diff --git a/data/images/icons/smartphone.png b/assets/images/icons/smartphone.png similarity index 100% rename from data/images/icons/smartphone.png rename to assets/images/icons/smartphone.png diff --git a/data/images/icons/spaceshuttle.png b/assets/images/icons/spaceshuttle.png similarity index 100% rename from data/images/icons/spaceshuttle.png rename to assets/images/icons/spaceshuttle.png diff --git a/data/images/icons/speaker.png b/assets/images/icons/speaker.png similarity index 100% rename from data/images/icons/speaker.png rename to assets/images/icons/speaker.png diff --git a/data/images/icons/speedometer.png b/assets/images/icons/speedometer.png similarity index 100% rename from data/images/icons/speedometer.png rename to assets/images/icons/speedometer.png diff --git a/data/images/icons/spraypaint.png b/assets/images/icons/spraypaint.png similarity index 100% rename from data/images/icons/spraypaint.png rename to assets/images/icons/spraypaint.png diff --git a/data/images/icons/steeringwheel.png b/assets/images/icons/steeringwheel.png similarity index 100% rename from data/images/icons/steeringwheel.png rename to assets/images/icons/steeringwheel.png diff --git a/data/images/icons/stop.png b/assets/images/icons/stop.png similarity index 100% rename from data/images/icons/stop.png rename to assets/images/icons/stop.png diff --git a/data/images/icons/sub.png b/assets/images/icons/sub.png similarity index 100% rename from data/images/icons/sub.png rename to assets/images/icons/sub.png diff --git a/data/images/icons/submarine.png b/assets/images/icons/submarine.png similarity index 100% rename from data/images/icons/submarine.png rename to assets/images/icons/submarine.png diff --git a/data/images/icons/support.png b/assets/images/icons/support.png similarity index 100% rename from data/images/icons/support.png rename to assets/images/icons/support.png diff --git a/data/images/icons/swatches.png b/assets/images/icons/swatches.png similarity index 100% rename from data/images/icons/swatches.png rename to assets/images/icons/swatches.png diff --git a/data/images/icons/tablet.png b/assets/images/icons/tablet.png similarity index 100% rename from data/images/icons/tablet.png rename to assets/images/icons/tablet.png diff --git a/data/images/icons/takeoff.png b/assets/images/icons/takeoff.png similarity index 100% rename from data/images/icons/takeoff.png rename to assets/images/icons/takeoff.png diff --git a/data/images/icons/target.png b/assets/images/icons/target.png similarity index 100% rename from data/images/icons/target.png rename to assets/images/icons/target.png diff --git a/data/images/icons/taxi.png b/assets/images/icons/taxi.png similarity index 100% rename from data/images/icons/taxi.png rename to assets/images/icons/taxi.png diff --git a/data/images/icons/toolbox.png b/assets/images/icons/toolbox.png similarity index 100% rename from data/images/icons/toolbox.png rename to assets/images/icons/toolbox.png diff --git a/data/images/icons/tractor.png b/assets/images/icons/tractor.png similarity index 100% rename from data/images/icons/tractor.png rename to assets/images/icons/tractor.png diff --git a/data/images/icons/traffic.png b/assets/images/icons/traffic.png similarity index 100% rename from data/images/icons/traffic.png rename to assets/images/icons/traffic.png diff --git a/data/images/icons/train.png b/assets/images/icons/train.png similarity index 100% rename from data/images/icons/train.png rename to assets/images/icons/train.png diff --git a/data/images/icons/travelerbag.png b/assets/images/icons/travelerbag.png similarity index 100% rename from data/images/icons/travelerbag.png rename to assets/images/icons/travelerbag.png diff --git a/data/images/icons/trends.png b/assets/images/icons/trends.png similarity index 100% rename from data/images/icons/trends.png rename to assets/images/icons/trends.png diff --git a/data/images/icons/tripod.png b/assets/images/icons/tripod.png similarity index 100% rename from data/images/icons/tripod.png rename to assets/images/icons/tripod.png diff --git a/data/images/icons/trophy.png b/assets/images/icons/trophy.png similarity index 100% rename from data/images/icons/trophy.png rename to assets/images/icons/trophy.png diff --git a/data/images/icons/truck.png b/assets/images/icons/truck.png similarity index 100% rename from data/images/icons/truck.png rename to assets/images/icons/truck.png diff --git a/data/images/icons/tv.png b/assets/images/icons/tv.png similarity index 100% rename from data/images/icons/tv.png rename to assets/images/icons/tv.png diff --git a/data/images/icons/typography.png b/assets/images/icons/typography.png similarity index 100% rename from data/images/icons/typography.png rename to assets/images/icons/typography.png diff --git a/data/images/icons/ufo.png b/assets/images/icons/ufo.png similarity index 100% rename from data/images/icons/ufo.png rename to assets/images/icons/ufo.png diff --git a/data/images/icons/umbrella.png b/assets/images/icons/umbrella.png similarity index 100% rename from data/images/icons/umbrella.png rename to assets/images/icons/umbrella.png diff --git a/data/images/icons/unicycle.png b/assets/images/icons/unicycle.png similarity index 100% rename from data/images/icons/unicycle.png rename to assets/images/icons/unicycle.png diff --git a/data/images/icons/unlocked.png b/assets/images/icons/unlocked.png similarity index 100% rename from data/images/icons/unlocked.png rename to assets/images/icons/unlocked.png diff --git a/data/images/icons/upload.png b/assets/images/icons/upload.png similarity index 100% rename from data/images/icons/upload.png rename to assets/images/icons/upload.png diff --git a/data/images/icons/video.png b/assets/images/icons/video.png similarity index 100% rename from data/images/icons/video.png rename to assets/images/icons/video.png diff --git a/data/images/icons/videocameraclassic.png b/assets/images/icons/videocameraclassic.png similarity index 100% rename from data/images/icons/videocameraclassic.png rename to assets/images/icons/videocameraclassic.png diff --git a/data/images/icons/videocameracompact.png b/assets/images/icons/videocameracompact.png similarity index 100% rename from data/images/icons/videocameracompact.png rename to assets/images/icons/videocameracompact.png diff --git a/data/images/icons/volume.png b/assets/images/icons/volume.png similarity index 100% rename from data/images/icons/volume.png rename to assets/images/icons/volume.png diff --git a/data/images/icons/water.png b/assets/images/icons/water.png similarity index 100% rename from data/images/icons/water.png rename to assets/images/icons/water.png diff --git a/data/images/icons/weather.png b/assets/images/icons/weather.png similarity index 100% rename from data/images/icons/weather.png rename to assets/images/icons/weather.png diff --git a/data/images/icons/windsock.png b/assets/images/icons/windsock.png similarity index 100% rename from data/images/icons/windsock.png rename to assets/images/icons/windsock.png diff --git a/data/images/icons/windy.png b/assets/images/icons/windy.png similarity index 100% rename from data/images/icons/windy.png rename to assets/images/icons/windy.png diff --git a/data/images/icons/x.png b/assets/images/icons/x.png similarity index 100% rename from data/images/icons/x.png rename to assets/images/icons/x.png diff --git a/data/images/icons/zoomin.png b/assets/images/icons/zoomin.png similarity index 100% rename from data/images/icons/zoomin.png rename to assets/images/icons/zoomin.png diff --git a/data/images/icons/zoomout.png b/assets/images/icons/zoomout.png similarity index 100% rename from data/images/icons/zoomout.png rename to assets/images/icons/zoomout.png diff --git a/data/session-sync.css b/data/session-sync.css deleted file mode 100644 index eddd1d6..0000000 --- a/data/session-sync.css +++ /dev/null @@ -1,2771 +0,0 @@ -/** - * Dom Components - */ - -/** - * Action Button - */ - - #session-sync .comp-button { - width: 100%; - color: #333; - display: flex; -} - -#session-sync .comp-button .label { - flex: 1; -} - -#session-sync .comp-button .button { - text-align: center; - background: #38B751; - color: #FFF; - flex: 1; -} - -#session-sync .comp-button .button:hover { - cursor: pointer; -} - -/** - * Toggle Switch - */ - -#session-sync .comp-toggle-switch { - width: 100%; - color: #333; - display: flex; -} - -#session-sync .comp-toggle-switch:first-child { - margin-top: 0; -} - -#session-sync .comp-toggle-switch:hover { - cursor: pointer; - color: #4C91D6; -} - -#session-sync .comp-toggle-switch .label { - float: left; -} - -#session-sync .comp-toggle-switch .switch { - margin-left: 0.4em; - padding: 0 0 0.25em 0; - - background-color: #40B740; - background-size: 1.6em; - background-repeat: no-repeat; - background-position: center right 0.2em; - border-radius: 2em; - - text-align: center; - color: #FFF; - float: left; - - flex: 1; - transition: all 0.3s; - position: relative; - - box-sizing: border-box; -} - -#session-sync .comp-toggle-switch .switch:after { - content: " "; - width: 1.4em; - height: 1.4em; - margin: 0.3em 0 0 0.3em; - background: #444; - border-radius: 100%; - position: absolute; - transition-property: left, background; - transition-duration: 0.25s; - transition-timing-function: ease-out; -} - - -#session-sync .comp-toggle-switch .switch[state="true"] { - background-color: #40B740; -} - -#session-sync .comp-toggle-switch .switch[state="true"]:after { - left: calc(100% - 1.7em); - margin-left: 0; -} - -#session-sync .comp-toggle-switch .switch[state="false"]:after { - background: #E01616; - left: 0; -} - -#session-sync .comp-toggle-switch .switch[state="false"] { - background-color: #999; - background-position: center left 0.2em; -} - -/** - * Toggle Switch - */ - -#session-sync .comp-dropdown { - width: 100%; - height: 2.4em; - color: #555; - display: flex; - text-align: center; -} - -#session-sync .comp-dropdown .label { - width: 50%; - height: 100%; - line-height: 2.4em; - background: #F5F5F5; -} - -#session-sync .comp-dropdown .select { - height: 100%; - line-height: 2.4em; - width: 50%; - background: #38B751; - position: relative; -} - -#session-sync .comp-dropdown .select:hover { - background: #349E47; -} - -#session-sync .comp-dropdown .select-value { - height: 100%; - color: #FFF; - text-align: center; -} - -#session-sync .comp-dropdown .select-value:hover { - background: #349E47; - cursor: pointer; -} - -#session-sync .comp-dropdown .option { - height: 2.4em; -} - -#session-sync .comp-dropdown .option:hover { - cursor: pointer; -} - -#session-sync .comp-dropdown .drop-list { - width: 100%; - min-height: 100%; - background: #FFF; - color: #333; - border: 1px solid #208020; - - display: block; - position: absolute; - z-index: 100; - top: 100%; - left: 0; - - box-sizing: border-box; -} - -#session-sync .comp-dropdown .drop-list[visible="true"] { - display: block; -} - -#session-sync .comp-dropdown .drop-list[visible="false"] { - display: none; -} - -#session-sync .comp-dropdown .drop-list .option { - width: 100%; - transition: all 0.2s; - text-align: center; - float: left; -} - -#session-sync .comp-dropdown .drop-list .option:hover { - transition: none; - background: #EEE; -} - -#session-sync .comp-dropdown .drop-list .option[selected] { - background: #38B751; - background: #66AAEF; - color: #FFF; -} - -/** - * Configuration button - */ - -#session-sync .config-panel { - width: 350px; - padding: 30px 0 0 0; - font-size: 12px; - border-right: 1px solid #CCC; - background: #F5F5F5; - opacity: 0.95; - color: #000; - - overflow-y: auto; - position: absolute; - top: 1px; - bottom: 1px; - right: -355px; - z-index: 2000; - box-shadow: 0 0 10px 0 #CCC; - - transition: all 0.25s ease-out; - opacity: 0.5; -} - -#session-sync[config] .config-panel { - right: 0; - opacity: 1; -} - -#session-sync[show="false"] .config-panel { - right: -355px !important; -} - -/* - * Close button - */ - -#session-sync .config-panel .css-close-button { - width: 2.4em; - height: 2.4em; - top: .5em; - right: .5em; - opacity: 0.85 !important; - transition: all 0.2s ease-out; -} - -#session-sync .config-panel .css-close-button:hover { - cursor: pointer; - opacity: 0.7 !important; -} - -/* - * Logo - */ - -#session-sync .config-panel .logo { - width: 100px; - height: 64px; - margin: 2em auto; - padding: 0; - border: none; - background: url("images/icon64.png") center top no-repeat; -} - -#session-sync .config-panel .logo:hover { - cursor: pointer; -} - -/* - * Sctions - */ - -#session-sync .config-panel .section-container { - height: auto; - padding: 1em 0 0 0; - border-bottom: 1px dashed #CCC; - border: none; - position: relative; -} - -#session-sync .config-panel .section-container .section-title { - margin: 0 0 1em 0; - padding: 0; - font-weight: 600; - text-align: left; - color: #333; - border-bottom: 1px solid #DDD; - border-top: none; - font-size: 14px; -} - -#session-sync .config-panel .section-container .section-title:hover { - cursor: pointer; -} - -#session-sync .config-panel .section-container .section-body { - transition: all 0.2s ease-out; -} - -#session-sync .config-panel .section-container[collapsed] .section-body { - overflow: hidden; - height: 0px !important; -} - -#session-sync .config-panel .section-container .drop-icon { - width: 1.5em; - height: 1.5em; - margin: 0 0.5em; - background-image: url("images/arrow-up.png"); - background-size: contain; - float: left; -} - -#session-sync .config-panel .section-container[collapsed] .drop-icon { - background-image: url("images/arrow-down.png"); - background-size: contain; -} - -/* - * Range control - */ - -#session-sync .config-panel .comp-range-control { - width: 100%; - height: 2em; - margin: 0.5em 0; - padding: 0 1em 0 2em; - line-height: 1.8em; - overflow: hidden; - border: none; - - display: flex; - box-sizing: border-box; -} - -#session-sync .config-panel .comp-range-control .label -{ - height: 100%; - font-size: 1em; - color: #555; - border: none; - word-break: break-all; - float: left; - font-size: 1.1em; - - flex: 1; - box-sizing: border-box; -} - - -#session-sync .config-panel .comp-range-control .range-input { - width: 8em; - margin: 0 1em 0 0; - height: 100%; - box-sizing: border-box; - float: left; - display: flex; -} - -#session-sync .config-panel .comp-range-control .range-input > * { - float: left; -} - -#session-sync .config-panel .comp-range-control .range-input .value { - text-align: center !important; -} - -#session-sync .config-panel .comp-range-control .button { - width: 2em; - height: 100%; - filter: opacity(50%); - transition: 0.2s all; -} - -#session-sync .config-panel .comp-range-control .button:hover { - cursor: pointer; - filter: opacity(100%); -} - -#session-sync .config-panel .comp-range-control .button.inc { - background: url("images/arrow-right.png") center top no-repeat; - background-size: 100%; -} - -#session-sync .config-panel .comp-range-control .button.dec -{ - background: url("images/arrow-left.png") center top no-repeat; - background-size: 100%; -} - -#session-sync .config-panel .comp-range-control .value { - width: 2.8em; - margin: 0 0.2em; - padding: 0; - - background: #FFF; - border: 1px solid #CCC; - - font-size: 1.1em; - text-align: center; - line-height: 1.8em; - color: #2D92EB; -} - -/* - * Toggle control - */ - - #session-sync .config-panel .comp-button, - #session-sync .config-panel .comp-dropdown, - #session-sync .config-panel .comp-toggle-switch, - #session-sync .config-panel .comp-toggle-button { - width: 100%; - height: 2em; - margin: 5px 0; - padding: 0 1em 0 2em; - line-height: 1.8em; - display: flex; - box-sizing: border-box; -} - -#session-sync .config-panel .comp-button .label, -#session-sync .config-panel .comp-dropdown .label, -#session-sync .config-panel .comp-toggle-switch .label, -#session-sync .config-panel .comp-toggle-button .label { - height: 100%; - color: #555; - word-break: break-all; - font-size: 1.1em; - float: left; - - flex: 1; - box-sizing: border-box; -} - -#session-sync .config-panel .comp-toggle-switch .switch, -#session-sync .config-panel .comp-toggle-button .button { - width: 8em; - height: 100%; - margin: 0 1em 0 0; - border-radius: 1em; - line-height: 1.9em; - color: white; - text-align: center; - float: left; -} - -#session-sync .config-panel .comp-toggle-switch .switch { - flex: unset; -} - -#session-sync .config-panel .comp-toggle-button .button:hover { - cursor: pointer; -} - -#session-sync .config-panel .comp-toggle-button .button[state="true"] { - background: #38B751; -} - -#session-sync .config-panel .comp-toggle-button .button[state="false"] { - background: #66AAEF; -} - -#session-sync .config-panel .comp-toggle-button .button[state="disable"] { - background: #66AAEF; -} - -/* - * Button Component - */ - -#session-sync .config-panel .comp-button .button { - width: 8em; - height: 100%; - margin: 0 1em 0 0; - border-radius: 1em; - line-height: 1.9em; - overflow: hidden; - text-overflow: ellipsis; - flex: unset; - padding: 0 0.75em; - box-sizing: border-box; -} - -/* - * Dropdown Component - */ - -#session-sync .config-panel .comp-dropdown .select { - width: 8em; - height: 100%; - margin: 0 1em 0 0; - background: #38B751; - border-radius: 1em; - line-height: 1.9em; - color: white; - text-align: center; - float: left; - - transition: all 0.2s ease-out; -} - -#session-sync .config-panel .comp-dropdown .select:hover { - cursor: pointer; - background: #66AAEF; -} - -/* - * Custom states - */ - -#session-sync[history="false"] .config-panel .section-container[id="history-config"] .comp-range-control { - display: none; -} - -/** - * Confirm Box - */ - -#session-sync .confirm-box { - width: auto; - padding: 15px; - margin: -40px 0 0 0; - background: #FFF; - border: 1px #CCC solid; - - display: none; - position: fixed; - left: 0; - top: 0; - overflow: auto; - - box-shadow: 3px 3px 5px 0 #CCC; - transform: translate(-50%, 0); - z-index: 1; - - /* display: block; - z-index: 1001; - top: 120px; - left: 321px; */ -} - -#session-sync .confirm-box[data-active="true"] { - display: block; - z-index: 1001; -} - -#session-sync .confirm-box .info { - width: 100%; - margin: 0 0 5px 0; - padding: 5px 0; - color: #333; - text-align: center; - font-size: 16px; -} - -#session-sync .confirm-box .controls { - padding: 10px 0 0 0; - border-top: 1px solid #DDD; - overflow: hidden; - display: flex; -} - -#session-sync .confirm-box .controls .flex { - flex: 1; -} - -#session-sync .confirm-box .ok { - margin: 0 10px 0 0; - color: #FFF; - background: #E86A5C; /*#EA5F4D;*/ -} - -#session-sync .confirm-box .ok:hover { - background: #DC462C; -} - -#session-sync .confirm-box .cancel { - color: #FFF; - background: #999; -} - -#session-sync .confirm-box .cancel:hover { - background: #777; -} - -#session-sync .confirm-box .button { - width: 75px; - padding: 5px 0; - text-align: center; - float: left; - - box-sizing: border-box; -} - -#session-sync .confirm-box .button:hover { - cursor: pointer; -} - -/** - * Context Menu - */ - -#session-sync .context-menu { - width: 180px; - padding: 5px; - background: #FFF; - border: 1px #CCC solid; - - display: none; - position: fixed; - left: 0; - top: 0; - - box-shadow: 0px 0px 5px 0 #CCC; - transform: translateX(5px); - z-index: 3000; - - box-sizing: border-box; -} - -#session-sync .context-menu[data-active="true"] { - display: block; -} - -#session-sync .context-menu .separator { - width: 100%; - height: 1px; - margin: 3px 0; - background: #CCC; -} - - -#session-sync .context-menu .button { - width: 100%; - padding: 5px 5px 5px 30px; - - background-image: url("images/icons/heart.png"); - background-size: 20px; - background-repeat: no-repeat; - background-position: 3px 3px; - box-sizing: border-box; -} - -#session-sync .context-menu .button:hover { - background-color: #EEE; - cursor: pointer; -} - -#session-sync .context-menu .button[icon="save"] { - background-image: url("images/icons/heart.png"); -} - -#session-sync .context-menu .button[icon="new-tab"] { - background-image: url("images/icons/ribbon.png"); -} - -#session-sync .context-menu .button[icon="same-tab"] { - background-image: url("images/icons/recycle.png"); -} - -#session-sync .context-menu .button[icon="new-window"] { - background-image: url("images/icons/frames.png"); -} - -#session-sync .context-menu .button[icon="replace-current"] { - background-image: url("images/material_icons/repeat_one.svg"); -} - -#session-sync .context-menu .button[icon="replace-all"] { - background-image: url("images/material_icons/repeat.svg"); -} - -#session-sync .context-menu .button[icon="copy"] { - background-image: url("images/icons/clipboard.png"); -} - -#session-sync .context-menu .button[icon="edit"] { - background-image: url("images/icons/document.png"); -} - -#session-sync .context-menu .button[icon="delete"] { - background-image: url("images/icons/denied.png"); -} - -#session-sync .context-menu .button[icon="same-window"] { - background-image: url("images/icons/stack.png"); -} - -#session-sync .context-menu .button[icon="new-session"] { - background-image: url("images/icons/star.png"); -} - -#session-sync .context-menu[icons="no"] .button -{ - padding: 5px; - background-image: unset; -} - - -/** - * Edit Session Section - */ -#session-sync .field-edit-widget { - width: 100%; - padding: 10px; - background: #FFF; - border: 1px solid #CCC; - - opacity: 0; - - position: absolute; - - box-sizing: border-box; - box-shadow: 0px 0px 5px 0px #CCC; - - bottom: 0; - left: 0; - z-index: -1; - transition: 0.2s all ease-out; -} - -#session-sync .field-edit-widget[data-active] { - opacity: 1; - z-index: 1000; -} - -#session-sync .field-edit-widget .group { - width: 100%; - height: 2.4em; - margin: 3px 0; - line-height: 2.0em; - float: left; - display: flex; -} - -#session-sync .field-edit-widget label { - width: 80px; - padding: 0 20px 0 0; - text-decoration: underline; - font-size: 1.2em; - text-align: right; -} - -#session-sync .field-edit-widget input { - height: 100%; - padding: 0 1em 0.2em 1em !important; - color: #333; - box-sizing: border-box; - border: 1px solid #CCC; - border-radius: 1.2em !important; - flex: 1; -} - -#session-sync .field-edit-widget input:focus { - border-color: #2D92EB !important; - box-shadow: 0 0 3px 0 rgba(0, 131, 255, 0.5) inset !important; -} - -#session-sync .field-edit-widget .button { - height: 2em; - padding: 0 1.5em; - line-height: 1.8em; - margin: 3px 0 0 10px; - - font-size: 14px; - text-align: center; - color: #FFF; - border: unset; - border-radius: 1em; - background: #999; - - transition: all 0.2s; - float: right; -} - -#session-sync .field-edit-widget .button:hover { - color: #FFF; - cursor: pointer; -} - -#session-sync .field-edit-widget .button.save:hover { - background: #23A823; -} - -#session-sync .field-edit-widget .button.cancel:hover { - background: #DC462C; -} - -/** - * Filter Menu - */ - -#session-sync .filter-panel { - width: 150px; - height: 100%; - min-height: 10000px; - font-size: 16px; - border-right: 1px solid #DDD; - background: #F5F5F5; - opacity: 0.95; - color: #000; - - position: absolute; - top: 1px; - z-index: 100; - display: none; - - transition: all 0.3s; -} - -#session-sync[show="true"] .filter-panel { - left: 0px; -} - -#session-sync[show="false"] .filter-panel { - left: -151px; -} - -#session-sync .filter-panel:hover { - width: 200px; -} - -#session-sync .filter-panel .logo { - margin: 10px 0; - height: 64px; - width: 100%; - background: url("images/icon64.png") center top no-repeat; -} - -#session-sync .filter-panel .separator { - margin: 5px 0; - border-bottom: 1px solid #DDD; -} - - -#session-sync .filter-panel .session-box { - padding: 5px 0; - color: #555; - text-align: center; -} - -#session-sync .filter-panel .session-box:hover { - background: #DDD; - cursor: pointer; -} - -#session-sync .filter-panel .session-box[active] { - background: #2D92EB; - color: #FFF; -} - -#session-sync .filter-panel .filters { -} - -#session-sync .filter-panel .filters .title { - margin: 10px 0; - padding: 10px 0; - text-align: center; -} - -#session-sync .filter-panel .label { - height: 24px; - padding: 3px 0; - text-align: center; - font-size: 12px; - line-height: 22px; - word-break: break-all; - - display: flex; - position: relative; - transition: background 0.2s; -} - -#session-sync .filter-panel .label[edit-mode] div { - display: none; -} - -#session-sync .filter-panel .label[edit-mode] input { - display: block; -} - - -#session-sync .filter-panel .label:hover { - cursor: pointer; - background: #FFF; - transition: none; -} - -#session-sync .filter-panel .name { - width: 80%; - padding: 0 10px; - text-align: right; - overflow: hidden; - - flex: 1; -} - -#session-sync .filter-panel .count { - width: 30px; - height: 12px; - margin: 3px 10px 3px 0; - padding: 3px 0; - background: #DDD; - border-radius: 3px; - text-align: center; - line-height: 100%; - color: #333; -} - -#session-sync .filter-panel .label input { - margin: 0 10px; - color: #333; - display: none; -} - -#session-sync .filter-panel .label[active] { - background: #FFF; -} - -#session-sync .filter-panel .label[active] .name { - color: #4C91D6; - font-weight: bold; -} - -#session-sync .filter-panel .label[active] .count { - background: #23A823; - color: #FFF; -} - -#session-sync .filter-panel .label[active]:after { - content: " "; - width: 3px; - height: 100%; - position: absolute; - right: 0; - top: 0; - background: #1AB61A; -} - -#session-sync .filter-panel .add-label { - margin: 0 auto; - font-size: 12px; - padding: 4px 20px; - color: #555; - text-align: center; -} - -#session-sync .filter-panel .add-label:hover { - cursor: pointer; - background: #2D92EB; - color: #FFF; -} - - -/** - * Generic CSS - */ - - -#session-sync .css-close-button { - width: 2em; - height: 2em; - font-size: 12px; - border-radius: 2em; - position: absolute; - top: 0; - z-index: 0; - opacity: 0.75; - box-sizing: border-box; -} - -#session-sync .css-close-button:hover { - cursor: pointer; - opacity: 1; -} - -#session-sync .css-close-button:hover::after, -#session-sync .css-close-button:hover::before { - background: #333; -} - -#session-sync .css-close-button:after, -#session-sync .css-close-button:before { - content: ""; - width: 60%; - height: 16%; - background: #AAA; - position: absolute; - top: 42%; - left: 20%; - transform: rotate(45deg); -} - -#session-sync .css-close-button:after { - transform: rotate(-45deg); -} -/** - * Session Hotkey Manager - */ - - #session-sync .hotkey-overlay { - position: absolute; - background: rgba(0, 0, 0, 0.85); - width: 100%; - height: 100%; - z-index: 3000; - display: none; - } - - #session-sync .hotkey-overlay[state="true"] { - display: block; - } - -#session-sync .hotkey-manager { - width: 300px; - padding: 10px; - font-size: 12px; - border: 1px solid #CCC; - background: #FFF; - color: #000; - - position: absolute; - top: 30%; - left: 50%; - z-index: 1; - transform: translate(-50%, -50%); - - transition: all 0.25s ease-out; - box-sizing: content-box; -} - -#session-sync .hotkey-manager .header { - padding: 0.5em 10px; - margin: 0 0 0.5em 0; - font-size: 1.4em; - text-align: center; - background: #F5F5F5; - color: #555; -} - -#session-sync .hotkey-manager .info-box { - text-align: center; -} - -#session-sync .hotkey-manager .info-box .message { - color: #DC462C; - text-align: center; - font-weight: bold; -} - -#session-sync .hotkey-manager .info-box .link { - color: #509AE5; - text-align: center; - font-weight: bold; -} - -#session-sync .hotkey-manager .info-box .link:hover { - cursor: pointer; -} - -#session-sync .hotkey-manager .hotkey-info { - width: 100%; - margin: 10px auto; - border: 1px solid #DDD; - - text-align: center; - color: #555; - overflow: hidden; -} - -#session-sync .hotkey-manager .hotkey-info > div { - float: left; - padding: 0.5em 0; - text-align: center; -} - -#session-sync .hotkey-manager .hotkey-info .title { - width: 30%; - background: #38B751; - background: #999; - color: #FFF; -} - -#session-sync .hotkey-manager .hotkey-info .value { - width: 70%; -} - -#session-sync .hotkey-manager .comp-dropdown { - width: 30%; - float: left; -} - -#session-sync .hotkey-manager .comp-dropdown .select { - width: 100%; -} - -#session-sync .hotkey-manager .comp-dropdown .label { - display: none; -} - -#session-sync .hotkey-manager .plus { - width: 5%; - height: 2.4em; - line-height: 2.4em; - text-align: center; - float: left; -} - -#session-sync #shortcut-key.comp-dropdown .drop-list { - width: 300px; - left: unset; - right: 0; -} - -#session-sync #shortcut-key.comp-dropdown .option { - width: 10%; -} - -#session-sync #shortcut-key.comp-dropdown .option[wide] { - width: 25%; -} - -#session-sync .hotkey-manager .shortcut-config, -#session-sync .hotkey-manager .action-bar { - width: 100%; - height: 2.4em; - margin: 10px 0 0 0; - display: flex; -} - -#session-sync .hotkey-manager .action-bar { - margin: 20px 0 0 0; -} - -#session-sync .hotkey-manager .action-bar .flex { - flex: 1; -} - -#session-sync .hotkey-manager .action-bar .ok { - margin: 0 10px 0 0; - color: #FFF; - background: #E86A5C; /*#EA5F4D;*/ -} - -#session-sync .hotkey-manager .action-bar .ok:hover { - background: #DC462C; -} - -#session-sync .hotkey-manager .action-bar .cancel { - color: #FFF; - background: #999; -} - -#session-sync .hotkey-manager .action-bar .cancel:hover { - background: #777; -} - -#session-sync .hotkey-manager .action-bar .button { - width: 75px; - padding: 5px 0; - text-align: center; - float: left; - - box-sizing: border-box; -} - -#session-sync .hotkey-manager .action-bar .button:hover { - cursor: pointer; -} -/** - * Notification System - */ - -#session-sync .notification-system { - width: 100%; - height: 0px; - font-size: 16px; - - position: absolute; - top: 1px; - left: 50%; - z-index: 3000; - transform: translateX(-50%); -} - -#session-sync .notification-system .info -{ - width: 100%; - padding: 0.5em 2em; - padding-left: 3.5em; - background: #2D92EB; - color: #FFF; - /* background: #F5F5F5; */ - /* color: #333; */ - word-break: break-all; - text-align: center; - - position: absolute; - top: -150px; - - opacity: 0; - transition: all 0.2s ease-out; - box-sizing: border-box; - transform: translateX(0); -} - -#session-sync .notification-system .info[active] -{ - opacity: 1; - top: -1px; - border: 1px solid #CCC; -} - -#session-sync .notification-system .info:after { - content: " "; - width: 24px; - height: 24px; - background: url("images/logo.png") center no-repeat; - background-size: contain; - top: 50%; - left: 20px; - position: absolute; - transform: translateY(-50%); -} -/** - * Control Panel - */ - -#session-sync .search-bar { - width: 5em; - height: 1.5em; - margin: 0; - padding: 0; - - background: hsla(0, 0%, 100%, 0.5); - border-radius: 1.5em; - - overflow: hidden; - opacity: 1; - - line-height: 1em; - - position: absolute; - top: 0.5em; - left: 0.5em; - z-index: 2; - - transition: all 0.2s ease-out; -} - -#session-sync .search-bar[content="false"] { - width: 5em !important; -} - -#session-sync .search-bar[content="false"] .search { - text-align: center; -} - -#session-sync .search-bar[active="true"] { - width: 25%; - opacity: 1; -} - -#session-sync .search-bar:hover { - opacity: 1; -} - -#session-sync .search-bar .search { - width: 100%; - height: 100%; - padding: 0 0.75em !important; - background: hsla(0, 0%, 100%, 0.95); - border: 1px solid #CCC; - border-radius: 1em; - color: #333; - font-size: 0.75em; - - box-sizing: border-box; - transition: all 0.2s ease-out; -} - -#session-sync .search-bar .search:focus { - border-color: #2D92EB !important; -} - -#session-sync .search-bar[match="true"][content="true"] .search { - color: green; -} - -#session-sync .search-bar .create-session { - width: 1em; - height: 1em; - background-image: url("images/icons/star.png"); - background-size: 100%; - background-repeat: no-repeat; - background-position: center; - text-align: center; - color: #FFF; - opacity: 0; - - position: absolute; - top: 0.25em; - right: -2.5em; - z-index: 0; - - filter: saturate(0%); - transition: all 0.2s ease-out; - box-sizing: border-box; -} - -#session-sync .search-bar[match="false"][content="true"] .create-session { - opacity: 0.6; - display: block; - right: 1.6em; -} - -#session-sync .search-bar .create-session:hover { - filter: saturate(100%); - cursor: pointer; - opacity: 1; -} - -#session-sync .search-bar .close { - width: 1.2em; - height: 1.2em; - background: #FFF; - border-radius: 2em; - opacity: 1; - - position: absolute; - top: 0.15em; - right: -2em; - z-index: 0; - - box-sizing: border-box; - transition: opacity 0.2s ease-out; -} - -#session-sync .search-bar[active="true"][content="true"] .close { - opacity: 0.75; - right: 0.3em; -} - -#session-sync .search-bar .close:hover { - cursor: pointer; -} - -#session-sync .search-bar .close:hover::after, -#session-sync .search-bar .close:hover::before { - background: #333; -} - -#session-sync .search-bar .close:after, -#session-sync .search-bar .close:before { - content: ""; - width: 60%; - height: 16%; - background: #AAA; - position: absolute; - top: 42%; - left: 20%; - transform: rotate(45deg); -} - -#session-sync .search-bar .close:after { - transform: rotate(-45deg); -} - -/** - * Display info about a Session - */ - -#session-sync .session-container { - height: 100%; - background: #FEFEFE; - - font-size: 14px; - float: right; - overflow: hidden; - - /*CSS3*/ - box-sizing: border-box; - position: relative; - - display: flex; - flex-direction: column; - flex: 1; -} - -/* - * Save button - not used for the moment - */ - -#session-sync[session] .session-container .save-session { - display: none; -} - -#session-sync .session-container .save-session { - height: 3.4em; - width: 3.4em; - - background: url("images/icons/heart.png") no-repeat; - background-size: 2em; - background-position: 1em 0.4em; - background-color: #F5F5F5; - - border-radius: 0 0 0 100%; - border-left: 1px solid #CCC; - border-bottom: 1px solid #CCC; - - position: absolute; - z-index: 1; - top: 0; - right: -1px; - - filter: saturate(75%); -} - -#session-sync .session-container .save-session:hover { - cursor: pointer; - background-color: #FFF; - filter: saturate(100%); -} - -/* - * Style Tabs - */ - -#session-sync .session-container .bookmarks { - width: calc(100% + 10px); - margin: 2px 0 2px 2px; - - overflow-x: hidden; - overflow-y: scroll; - position: relative; - - /*CSS3*/ - box-sizing: border-box; - flex: 1; -} - -#session-sync .session-container .bookmarks:hover { - overflow-y: scroll; -} - -#session-sync .session-container .window-separator { - width: 100%; - height: 2em; - background: #F8F8F8; - color: #555; - - line-height: 1.9em; - position: absolute; - transition: all 0.2s; -} - -#session-sync .session-container .window-separator .text { - text-align: center; - font-weight: 600; -} - -#session-sync .session-container .window-separator:hover { - cursor: pointer; - text-decoration: underline; - color: #333; -} - -#session-sync .session-container .window-separator:hover:after { - content: "(switch to)"; - position: absolute; - font-size: 0.8em; - top: 0; - margin-left: 40px; - left: 50%; -} - -#session-sync .session-container .bookmark { - width: 100%; - height: 2em; - padding: 0 4px; - - line-height: 1.9em; - word-break: keep-all; - - overflow: hidden; - position: absolute; - left: 0; - top: 0; - - /*CSS3*/ - box-sizing: border-box; - transition-property: left, top, background, color; - transition-duration: 0.2s; - display: flex; - flex: 1; -} - -#session-sync .session-container .bookmark:hover { - background: #EEE; - cursor: pointer; - transition-property: left, top; -} - -#session-sync .session-container .bookmark[highlight] { - transition-duration: 0.5s; - background: #4396E9 !important; - color: #FFF; -} - -#session-sync .session-container .bookmark[dragging] { - width: 400px; - margin: 1px 0 0 1px; - background-color: #FBD4B0; - background: #4396E9 !important; - color: #FFF; - opacity: 0.9; - - transition: none; - position: fixed; - z-index: 1000; -} - -#session-sync .session-container .bookmark[dragging="drop"] { - background: #4396E9 !important; - color: #FFF; - opacity: 0.9; - transition: none; - position: absolute; - - z-index: 1000; -} - -#session-sync .session-container .bookmark[dragging]:hover { - cursor: default; -} - -#session-sync .session-container .bookmark[pinned='true']:before { - content: ""; - width: 1.5em; - height: 100%; - background-image: url("images/icons/pin.png"); - background-size: contain; - background-repeat: no-repeat; - background-position: center center; - color: green; - position: absolute; - top: 0; - right: 0.35em; - z-index: 0; - opacity: 0.85; -} - -#session-sync .session-container .bookmark * { - float: left; -} - -#session-sync .session-container .bookmark .favicon { - width: 1.33em; - height: 100%; - background: url("images/icons/globe.png"); - background-size: contain; - background-repeat: no-repeat; - background-position: center center; -} - -#session-sync .session-container .bookmark .text { - height: 100%; - padding: 0 5px; - word-break: keep-all; - white-space: nowrap; - overflow: hidden; - - box-sizing: border-box; - flex: 1; -} - -/* - * History Style - */ - -#session-sync .session-container .history-window-separator { - width: 100%; - height: 2em; - line-height: 1.9em; - background: #F8F8F8; - color: #555; - position: absolute; - transition: all 0.2s; -} - -#session-sync .session-container .history-window-separator .text { - text-align: center; -} - -#session-sync .session-container .history-window-separator:hover { - cursor: pointer; - text-decoration: underline; - color: #333; -} -/** - * Header Bar - */ - -#session-sync .header-bar { - width: 100%; - height: 2.5em; - - background: #F5F5F5; - border-bottom: 1px solid #DDD; - line-height: 2.4em; - font-size: 18px; - position: relative; - - box-sizing: border-box; -} - -/** - * Active session button - */ - -#session-sync .header-bar .active-session-btn { - width: 2.5em; - background: url("images/icon128.png") no-repeat center; - background-size: 1em; - height: 60%; - opacity: 0.5; - border: 1px solid red; - border: unset; - position: absolute; - top: 0.5em; - left: 0; - transition: all 0.2s ease-out; - box-sizing: border-box; - display: none; -} - -#session-sync[session="current"] .header-bar .active-session-btn, -#session-sync .header-bar .active-session-btn:hover { - background-size: 1.2em; - cursor: pointer; - opacity: 1; -} - -/** - * Active session description - */ - -#session-sync .header-bar .description { - width: 100%; - height: 100%; - - text-align: center; - white-space: nowrap; - overflow: hidden; - color: #555; - - box-sizing: border-box; -} - -/** - * Config button - */ - -#session-sync .header-bar .addon-config { - width: 1.8em; - height: 1.8em; - background-image: url("images/menu.png"); - background-size: 55%; - background-repeat: no-repeat; - background-position: center center; - border: 1px solid rgba(0, 0, 0, 0); - position: absolute; - top: 0.3em; - right: 0.3em; - z-index: 2; - opacity: 0.4; - box-sizing: border-box; - display: none; -} - -#session-sync .header-bar .addon-config:hover { - cursor: pointer; - background-color: #EEE; - border: 1px solid #CCC; - opacity: 1; -} - -/** - * Header Menu - */ - -#session-sync .header-bar .header-menu { - width: 1.8em; - height: 1.8em; - background-image: url("images/icons_v2/menu.png"); - background-size: 75% 80%; - background-repeat: no-repeat; - background-position: center center; - border: 1px solid rgba(0, 0, 0, 0); - position: absolute; - top: 0.3em; - right: 0.3em; - z-index: 2; - opacity: 0.4; - box-sizing: border-box; -} - - -#session-sync .header-bar .header-menu:hover { - cursor: pointer; - background-color: #EEE; - border: 1px solid #CCC; - opacity: 1; -} - -#session-sync .header-bar .header-menu[active="1"] { - opacity: 1; -} - -#session-sync .header-bar .header-menu .menu-area { - height: 0; - margin: 0; - padding: 0; - background: #FFF; - font-size: 0.75em; - border: 1px solid rgba(0, 0, 0, 0); - position: absolute; - top: 100%; - right: -1px; - overflow: hidden; - - opacity: 0; - display: flex; - flex-direction: column; - box-sizing: initial; - - transition-property: width, height, padding, opacity; - transition-duration: 0.2s; - transition-timing-function: ease-out; -} - -#session-sync .header-bar .header-menu[active="1"] .menu-area { - height: auto; - padding: 1em; - border: 1px solid #CCC; - - opacity: 1; -} - -#session-sync .header-bar .header-menu .menu-area:hover { - cursor: default; -} - -#session-sync .header-bar .header-menu .menu-row { - display: flex; -} - -#session-sync .header-bar .header-menu .menu-button { - width: 6em; - height: 3.5em; - margin: 0 0 0 0.6em; - padding: 0.5em 0; - border: 1px solid #FFF; - - box-sizing: content-box; - flex: 1; -} - -#session-sync .header-bar .header-menu .menu-button:first-child { - margin: 0; -} - -#session-sync .header-bar .header-menu .menu-button:hover { - cursor: pointer; - background-color: #F5F5F5; - border-color: #DDD; -} - -#session-sync .header-bar .header-menu .menu-button .icon { - height: 2em; - background-image: url("images/icons/power.png"); - background-size: contain !important; - background-repeat: no-repeat; - background-position: center; -} - -#session-sync .header-bar .header-menu .menu-button .title { - height: 1.4em; - margin-top: 0.4em; - font-size: 0.8em; - line-height: 100%; - text-align: center; - color: #555; -} - -/* - * Sorting method - */ - -#session-sync .sorting-method { - width: 2.5em; - height: 2.5em; - padding: 0; - display: flex; - - background-image: url("images/sort.png"); - background-size: 20px; - background-repeat: no-repeat; - background-position: center center; - - position: absolute; - bottom: 0px; - right: 4px; - opacity: 0.1; - - transition: all 0.1s ease-out; - box-sizing: border-box; -} - -#session-sync[list="history"] .sorting-method { - display: none; -} - -#session-sync .sorting-method:hover { - cursor: pointer; - opacity: 1; -} - -#session-sync .sorting-method[active="true"] { - opacity: 1; -} - -#session-sync .sorting-method[active="true"] .dropdown { - display: block; - background: #FFF; -} - -#session-sync .sorting-method .dropdown { - width: 8em; - margin: 0; - padding: 0; - - color: #333; - line-height: 1.8em; - text-align: left; - display: none; - - position: absolute; - bottom: 0; - left: -8em; - - flex: 1; - box-sizing: border-box; -} - -#session-sync .sorting-method .dropdown .option-list { - width: 100%; - - border: 1px solid #CCC; - border-bottom: none; - background: #FFF; - - position: absolute; - bottom: 0; - left: -1px; - z-index: 1; - box-sizing: content-box; -} - -#session-sync .sorting-method .dropdown .option-list .option { - width: 100%; - padding: 3px 10px; - text-align: left; - box-sizing: border-box; - transition-property: left, top, color, background; - transition-duration: 0.2s; - -} - -#session-sync .sorting-method .dropdown .option-list .option:hover { - background: #EEE; - border-color: #CCC; - cursor: pointer; - transition-property: left, top; -} - -#session-sync .sorting-method .dropdown .option-list .option[active] { - color: #FFF; - background: #2D92EB; -} - - -/** - * Control Panel - */ - -#session-sync .session-toolbar { - width: 100%; - height: 3em; - - background: #FFF; - color: #000; - display: flex; - font-size: 12px; - - position: relative; - opacity: 100; - border-bottom: 1px solid #CCC; - box-shadow: 0px 0px 3px 0px #CCC; - z-index: 1; - - transition: all 0.15s; -} - -#session-sync .session-toolbar .session-selector { - width: 220px; - min-width: 160px; - max-width: 60%; - height: 100%; - margin: 0; - padding: 0; - border-right: 1px solid #DDD; - - line-height: 3em; - word-break: break-all; - text-align: center; - color: #555; - - display: flex; - box-sizing: border-box; -} - -#session-sync .session-toolbar .session-selector .button { - text-align: center; - overflow: hidden; - display: block; - color: #333; - flex: 1; -} - -#session-sync .session-toolbar .session-selector .button:hover { - cursor: pointer; -} - -#session-sync .session-toolbar .session-selector .active-session { - background-image: url("images/icon128.png"); - background-repeat: no-repeat; - background-position: center center; - background-size: 1.8em; - background-color: #FFF; -} - -#session-sync[list="active"] .session-toolbar .session-selector .active-session, -#session-sync[list="sync"] .session-toolbar .session-selector .sync, -#session-sync[list="history"] .session-toolbar .session-selector .history { - display: block; - background-color: #2D92EB; - color: #FFF; -} - -#session-sync[history="false"] .session-toolbar .session-selector .history { - display: none; -} - -/* - * Session info - */ - -#session-sync:not([session="restore"]) .session-toolbar .session-date { - display: none; -} - -#session-sync .session-toolbar .session-date { - padding: 0.9em 1em; - color: #999; - text-align: right; - display: block; - position: absolute; - top: -1px; - right: 0; - z-index: 10; -} - -/** - * Menu Section - */ - -#session-sync .session-toolbar .menu-bar { - height: 100%; - padding: 0.5em; - - box-sizing: border-box; - flex: 1; -} - -#session-sync .session-toolbar .menu-bar .button { - width: 2.6em; - height: 100%; - - background: url("images/icons/none.png") no-repeat center; - background-size: contain !important; - - display: none; - float: left; - - opacity: 0.75; - /* filter: grayscale(100%); */ -} - -#session-sync .session-toolbar .menu-bar .button:hover { - cursor: pointer; - opacity: 1; - box-shadow: none; -} - -#session-sync .session-toolbar .menu-bar .separator { - height: 100%; - width: 1px; - margin: 0 0.5em; - background: #CCC; - float: left; - display: none; -} - -#session-sync .session-toolbar .menu-bar .add { - background: url("images/icons/star.png") no-repeat center; -} - -#session-sync .session-toolbar .menu-bar .save { - background: url("images/icons/heart.png") no-repeat center; -} - -#session-sync .session-toolbar .menu-bar .merge-sessions { - background: url("images/icons/merge.png") no-repeat center; -} - -#session-sync .session-toolbar .menu-bar .replace-session { - background: url("images/icons/recycle.png") no-repeat center; -} - -#session-sync .session-toolbar .menu-bar .restore { - background: url("images/icons/stack.png") no-repeat center; -} - -#session-sync .session-toolbar .menu-bar .restore-new-win { - background: url("images/icons/frames.png") no-repeat center; -} - -#session-sync .session-toolbar .menu-bar .repalce-session-all { - background: url("images/material_icons/repeat.svg") no-repeat center; -} - -#session-sync .session-toolbar .menu-bar .repalce-session { - background: url("images/material_icons/repeat_one.svg") no-repeat center; -} - -#session-sync .session-toolbar .menu-bar .fix-session { - background: url("images/icons/tools.png") no-repeat center; -} - -#session-sync .session-toolbar .menu-bar .customize-toolbar { - background: url("images/icons_v2/more.png") no-repeat center; -} - -/* - * Menu state - */ - - #session-sync[session="current"] .session-toolbar .menu-bar .button[state-current="true"] { - display: block; -} - -#session-sync[session="restore"] .session-toolbar .menu-bar .button[state-restore="true"] { - display: block; -} - -#session-sync[session="restore"] .session-toolbar .menu-bar .separator { - display: block; -} - -#session-sync[session="history"] .session-toolbar .menu-bar .button[state-history="true"] { - display: block; -} - -/* - * Save configuration - */ - -#session-sync .session-toolbar .menu-bar .save-config { - height: 100%; - padding: 0 40px 0 0; - display: block; -} - -#session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch { - opacity: 1; - transition: opacity 0.2s ease-out; -} - -#session-sync .session-toolbar .menu-bar:hover .save-config .comp-toggle-switch { - opacity: 1; -} - -#session-sync[session="restore"] .session-toolbar .menu-bar .save-config { - display: none; -} - -#session-sync[session="history"] .session-toolbar .menu-bar .save-config .comp-toggle-switch[windows] { - display: none; -} - -#session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch { - height: 100%; - width: 4em; - margin: 0; - float: right; -} - -#session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch .label { - display: none; -} - -#session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch .switch { - width: 2.6em; - height: 100%; - background-color: #40B740; - background-image: url("images/icons/gear.png"); - background-size: 1.6em; - background-repeat: no-repeat; - background-position: center right 0.2em; - border-radius: 2em; - display: block; - - box-sizing: border-box; -} - -#session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch .switch:after { - display: none; -} - -#session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch .switch[state="false"] { - background-color: #BBB; - background-position: center left 0.2em; -} - -#session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch[pin] .switch { - background-image: url("images/icons/pin.png"); -} - -#session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch[pin-state] .switch { - background-image: url("images/icons/pin2.png"); -} - -#session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch[windows] .switch { - background-image: url("images/icons/stack.png"); -} - -/** - * Display lsit of all sessions - */ - -#session-sync .sessions { - width: 200px; - min-width: 160px; - max-width: 60%; - height: 100%; - padding: 0px; - - border-width: 0 1px 0 0; - border-style: solid; - border-color: #CCC; - background: hsla(0, 0%, 100%, 0.95); - background: #FEFEFE; - float: left; - - overflow-x: hidden; - position: relative; - - display: flex; - flex-direction: column; - - /*CSS3*/ - box-sizing: border-box; -} - -/* - * List resize - */ - -#session-sync .sessions .resize-handle -{ - content: " "; - width: 4px; - height: 24px; - margin-top: -12px; - position: absolute; - background: #DDD; - top: 50%; - border: unset; - right: 0px; - cursor: w-resize; - z-index: 10; -} - -#session-sync .sessions .resize-handle:hover -{ - background: #1AB61A; -} - -#session-sync .sessions .resize-handle[resizing] -{ - background: #2D92EB !important; -} - -/* - * History list - */ - -#session-sync[list="history"] .sessions .history-list { - flex: 1; -} - -#session-sync .sessions .history-list { - width: calc(100% + 12px); - margin: 0 0 0 0; - - position: relative; - overflow-x: hidden; - overflow-y: scroll; - display: block; - - transition-property: width, margin, flex; - transition-duration: 0.2s; - box-sizing: border-box; - flex: 0; -} - -#session-sync .sessions .history-node { - width: 100%; - height: 2.5em; - margin: 0.2em; - padding: 0 0 0 10px; - - line-height: 2.4em; - word-break: break-all; - - overflow: hidden; - - box-sizing: border-box; - transition-property: left, top, color, background; - transition-duration: 0.2s; -} - -#session-sync .sessions .history-node:hover { - background: #EEE; - cursor: pointer; - transition-property: left, top; -} - -#session-sync .sessions .history-node[active="true"] { - background: #2D92EB; - color: #FFF; -} - -/* - * Session list - */ - -#session-sync[list="history"] .sessions .session-list { - flex: 0; -} - -#session-sync .sessions .session-list { - width: calc(100% + 12px); - margin: 0 0 0 0; - - position: relative; - overflow-x: hidden; - overflow-y: scroll; - - transition-property: width, margin, flex; - transition-duration: 0.2s; - box-sizing: border-box; - flex: 1; -} - -/* - * Style Folder Session - */ - -#session-sync .sessions .folder { - width: 100%; - height: 2.5em; - padding: 0 0 0 10px; - - line-height: 2.4em; - word-break: break-all; - - overflow: hidden; - position: absolute; - left: 0; - top: 0; - - box-sizing: border-box; - transition-property: left, top, color, background; - transition-duration: 0.2s; -} - -#session-sync .sessions .folder:hover { - background: #EEE; - cursor: pointer; - transition-property: left, top; -} - -#session-sync .sessions .folder[dragging] { - width: 200px; - margin: 1px 0 0 1px; - background: #4396E9 !important; - color: #FFF; - opacity: 0.9; - - transition: none; - position: fixed; - z-index: 1000; -} - -#session-sync .sessions .folder[dragging]:hover { - cursor: default; -} - -#session-sync .sessions .folder[dragging="drop"] { - background: #4396E9 !important; - color: #FFF; - opacity: 0.9; - transition: none; - position: absolute; - - z-index: 1000; -} - -#session-sync .sessions .folder[active="true"] { - background: #2D92EB; - color: #FFF; -} - -#session-sync .sessions .folder[filter] { - top: 0 !important; - left: -100% !important; -} - -#session-sync .sessions .folder[type='separator']:after { - content: " "; - width: 100%; - height: 2px; - margin: -1px 0 0 0; - background: #CCC; - position: absolute; - box-sizing: border-box; - top: 50%; - left: 1px; -} - -#session-sync .sessions .folder[active="true"][type='separator']:after { - background: #FFF; -} - -#session-sync .sessions .folder[type='bookmark'] { - text-decoration: underline; -} - - -/** - * Tooltip System - */ - -#session-sync .tooltip-system { - padding: 5px 10px; - margin: -16px 0 0 0; - - color: #555; - background: #FFF; - text-align: center; - line-height: 20px; - border: 1px solid #CCC; - border-radius: 5px; - - display: none; - position: fixed; - top: 1px; - left: 0; - z-index: 2000; - - box-shadow: 0 5px 7px 1px #CCC; - box-sizing: border-box; -} - -#session-sync .tooltip-system:after { - content: " "; - width: 16px; - height: 16px; - margin: -8px 0 0 -9px; - - background: #FFF; - border-style: solid; - border-color: #BBB; - border-width: 0 1px 1px 0; - - position: absolute; - left: 50%; - top: 100%; - - transform: rotate(45deg); - z-index: -100; -} - -#session-sync .tooltip-system[active] { - display: block; -} - -/** - * Trash-can - */ - -#session-sync .trash-can { - width: 8em; - height: 8em; - - background-image: url("images/trash-can.png"); - background-position: bottom 1.5em right 1.5em; - background-repeat: no-repeat; - background-size: 50%; - background-color: #FFF; - - border-top-left-radius: 8em; - - font-size: 12px; - - border: 1px solid #CCC; - border-bottom: none; - border-right: none; - - box-shadow: 0 0 5px 0 #FFF; - - opacity: 0; - - position: absolute; - bottom: 1px; - right: 0; - z-index: 100; - - box-sizing: border-box; - transition: all 0.2s ease-out; -} - -#session-sync[show="true"] .trash-can { - display: block; -} - -#session-sync[show="false"] .trash-can { - display: none; -} - -#session-sync .trash-can[droppable="true"] { - opacity: 1 !important; -} - -#session-sync .trash-can[action="enter"] { - opacity: 1; - cursor: pointer; -} - -#session-sync .trash-can:not([items="0"]) { - opacity: 1; -} - -#session-sync .trash-can:hover { - cursor: pointer; - font-size: 16px; - opacity: 1; -} - -#session-sync .count { - width: 2em; - height: 2em; - - background: #E54828; - text-align: center; - line-height: 1.9em; - color: #FFF; - border-radius: 100%; - opacity: 1; - - position: absolute; - top: 3.7em; - left: 1.75em; - z-index: 1; - box-sizing: border-box; - transition: background 0.5s ease-out; -} - -#session-sync .trash-can[items="0"] .count { - background: #1AB61A; -} - -#session-sync .trash-can[items="0"] .dismiss { - display: none; -} - -#session-sync .trash-can .dismiss { - display: block; - right: 0.5em; -} - -#session-sync .trash-can[dismiss="true"] { - opacity: 0; -} - -#session-sync .trash-can[dismiss="true"] .dismiss { - display: none; -} - -#session-sync .trash-can[dismiss="true"]:hover { - opacity: 1; -} - -/** - * URL Bar - */ - -#session-sync .url-bar { - width: calc(100% - 3em); - height: 1.5em; - margin: 0; - padding: 0; - - /* background: #2D92EB; */ - /* border: 1px solid #FFF; */ - border-radius: 1em; - - overflow: hidden; - opacity: 0; - - color: #FFF; - - position: absolute; - top: 0.5em; - left: 0.5em; - z-index: 1; - - box-sizing: content-box; - transition: all 0.2s ease-out; -} - -#session-sync .url-bar .arrow { - width: 2em; - height: 100%; - display: none; - - background: #FFF; - text-align: right; - color: #777; - float: left; -} - -#session-sync .url-bar .arrow:after { - content: " "; - width: 2em; - height: 2em; - margin: 2px 0; - background: inherit; - - position: absolute; - left: 0; - z-index: -2; - transform: translate(1em, -0.1em) rotate(45deg); -} - -#session-sync .url-bar .url { - height: 100%; - padding-left: 8em; - background: #2D92EB; - font-size: 0.75em; - line-height: 2em; - white-space: nowrap; - text-overflow: ellipsis; -} - -#session-sync[search-box-content="true"] .url { - padding-left: calc(25% + 2.5em); -} - -#session-sync .url-bar[data-active="true"] { - opacity: 1; - bottom: -1px; -} - -#session-sync .url-bar .copy-input { - position: absolute; - top: -5em; -} - -#session-sync .url-bar .copy-feedback { - width: 5em; - height: 100%; - - background: #38B751; - color: #FFF; - text-align: center; - font-size: 0.75em; - line-height: 2em; - - position: absolute; - top: 0; - right: -5em; - - transition: all 0.2s; -} - -#session-sync .url-bar .copy-feedback[active] { - right: 0; -} - -/** - * Session-Sync - */ -#session-sync { - width: 100%; - height: 100%; - min-height: 400px; - margin: 0; - - font-size: 12px; - font-family: "Open Sans", "Segoe UI", Arial, Helvetica, sans-serif; - - color: #000 !important; - overflow: hidden; - - -moz-user-select: none; -} - -#session-sync[panel="false"] { - position: absolute; -} - -#session-sync[panel="false"] .sessions-area .panel-resize-handle { - display: none; -} - -/* - * Only DOM nodes from here - */ - -#session-sync #overlay { - width: 100%; - height: 100%; - - background: rgba(255, 255, 255, 0.01); - - position: absolute; - top: 0; - left: 0; - z-index: 2500; - - display: none; -} - -#session-sync #overlay[state="true"] { - display: block; -} - -/* - * Panel Body - */ - - #session-sync #body { - height: 100%; - } - -#session-sync[config] .body { - background-color: rgba(255, 255, 255, 0.95); -} - -#session-sync .sessions-area { - width: 100%; - height: 100%; - - box-sizing: border-box; - - transition: all 0.2s; - - display: flex; - flex-direction: column; -} - -#session-sync[show="false"] .sessions-area { - display: none; -} - -#session-sync[state="pin"] .sessions-area { - top: 0; - right: 0; - margin: 0; - transform: translate(0); -} - -#session-sync[state="centered"] .sessions-area { - top: calc(50% - 50px); - left: 50%; - transform: translate(-50%, -50%); -} - -#session-sync[config] .sessions-area { - left: 400px; - top: 20%; - transform: translate(0); -} - -#session-sync .sessions-area-body { - width: 100%; - overflow: hidden; - - flex: 1; - display: flex; - flex-direction: row; -} - -/* - * Container resize - */ - -#session-sync[resize] .body { - background: rgba(255, 255, 255, 0.95) !important; - cursor: se-resize; -} - -#session-sync[resize] .sessions-area { - transition: none; -} - -#session-sync .sessions-area .panel-resize-handle -{ - content: " "; - width: 12px; - height: 12px; - margin-top: -12px; - background: url("images/resize.png") center no-repeat !important; - position: absolute; - background: #DDD; - bottom: 0; - left: 0px; - cursor: sw-resize; - z-index: 10; - - transform: rotate(90deg); -} - -#session-sync .sessions-area .panel-resize-handle:hover -{ - background: #1AB61A; -} - -#session-sync .sessions-area .panel-resize-handle[resizing] -{ - background: #2D92EB; -} - -#clipboard { - position: absolute; - top: -100px; - z-index: -100; -} \ No newline at end of file diff --git a/data/session-sync.html b/data/session-sync.html deleted file mode 100644 index 1dc4099..0000000 --- a/data/session-sync.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - Session Sync - - - - - - -
- - - \ No newline at end of file diff --git a/entrypoints/background.js b/entrypoints/background.js deleted file mode 100644 index 6a17abf..0000000 --- a/entrypoints/background.js +++ /dev/null @@ -1,14 +0,0 @@ -import { initBackground } from '../scripts/main'; -import { initDevelopment } from '../scripts/development'; -import { SessionManagement } from '../scripts/session-management'; -import { SessionAutoSave } from '../scripts/session-history'; - -export default defineBackground(() => { - // Initialize the background logic - initBackground(); - initDevelopment(); - SessionManagement.init(); - SessionAutoSave.init(); - - console.log('Session Sync Background script initialized.'); -}); diff --git a/entrypoints/popup/main.js b/entrypoints/popup/main.js deleted file mode 100644 index cbcafb4..0000000 --- a/entrypoints/popup/main.js +++ /dev/null @@ -1,23 +0,0 @@ -import '../../dev/css/_style.css'; -import '../../dev/css/components.css'; -import '../../dev/css/config-panel.css'; -import '../../dev/css/confirm-box.css'; -import '../../dev/css/context-menu.css'; -import '../../dev/css/field-widget.css'; -import '../../dev/css/filter-panel.css'; -import '../../dev/css/generic.css'; -import '../../dev/css/hotkey-manager.css'; -import '../../dev/css/notification-system.css'; -import '../../dev/css/search-bar.css'; -import '../../dev/css/session-container.css'; -import '../../dev/css/session-header-bar.css'; -import '../../dev/css/session-sorting.css'; -import '../../dev/css/session-toolbar.css'; -import '../../dev/css/sessions-list.css'; -import '../../dev/css/tooltip-system.css'; -import '../../dev/css/trash-can.css'; -import '../../dev/css/url-bar.css'; - -import { init } from '../../data/scripts/app.js'; - -init(); diff --git a/package.json b/package.json index 7ff3fb1..65fe5b6 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "license": "MPL-2.0", "scripts": { "dev": "wxt", - "dev:chrome": "wxt -b chrome --mv3", - "dev:firefox": "wxt -b firefox --mv2", + "dev:chrome": "wxt -b chrome --mv3 --host 127.0.0.1", + "dev:firefox": "wxt -b firefox --mv2 --host 127.0.0.1", "build": "wxt build", "build:chrome": "wxt build -b chrome --mv3", "build:firefox": "wxt build -b firefox --mv2", diff --git a/data/images/arrow-down.png b/public/images/arrow-down.png similarity index 100% rename from data/images/arrow-down.png rename to public/images/arrow-down.png diff --git a/data/images/arrow-left.png b/public/images/arrow-left.png similarity index 100% rename from data/images/arrow-left.png rename to public/images/arrow-left.png diff --git a/data/images/arrow-right.png b/public/images/arrow-right.png similarity index 100% rename from data/images/arrow-right.png rename to public/images/arrow-right.png diff --git a/data/images/arrow-up.png b/public/images/arrow-up.png similarity index 100% rename from data/images/arrow-up.png rename to public/images/arrow-up.png diff --git a/data/images/favicon.png b/public/images/favicon.png similarity index 100% rename from data/images/favicon.png rename to public/images/favicon.png diff --git a/data/images/github-logo.png b/public/images/github-logo.png similarity index 100% rename from data/images/github-logo.png rename to public/images/github-logo.png diff --git a/data/images/github.png b/public/images/github.png similarity index 100% rename from data/images/github.png rename to public/images/github.png diff --git a/data/images/grain.png b/public/images/grain.png similarity index 100% rename from data/images/grain.png rename to public/images/grain.png diff --git a/data/images/grain_header.png b/public/images/grain_header.png similarity index 100% rename from data/images/grain_header.png rename to public/images/grain_header.png diff --git a/data/images/icon128.png b/public/images/icon128.png similarity index 100% rename from data/images/icon128.png rename to public/images/icon128.png diff --git a/data/images/icon16.png b/public/images/icon16.png similarity index 100% rename from data/images/icon16.png rename to public/images/icon16.png diff --git a/data/images/icon32.png b/public/images/icon32.png similarity index 100% rename from data/images/icon32.png rename to public/images/icon32.png diff --git a/data/images/icon48.png b/public/images/icon48.png similarity index 100% rename from data/images/icon48.png rename to public/images/icon48.png diff --git a/data/images/icon64.png b/public/images/icon64.png similarity index 100% rename from data/images/icon64.png rename to public/images/icon64.png diff --git a/data/images/icon96.png b/public/images/icon96.png similarity index 100% rename from data/images/icon96.png rename to public/images/icon96.png diff --git a/data/images/icons/booklet.png b/public/images/icons/booklet.png similarity index 100% rename from data/images/icons/booklet.png rename to public/images/icons/booklet.png diff --git a/data/images/icons/bookshelf.png b/public/images/icons/bookshelf.png similarity index 100% rename from data/images/icons/bookshelf.png rename to public/images/icons/bookshelf.png diff --git a/data/images/icons/briefcase.png b/public/images/icons/briefcase.png similarity index 100% rename from data/images/icons/briefcase.png rename to public/images/icons/briefcase.png diff --git a/data/images/icons/brightness.png b/public/images/icons/brightness.png similarity index 100% rename from data/images/icons/brightness.png rename to public/images/icons/brightness.png diff --git a/data/images/icons/browser.png b/public/images/icons/browser.png similarity index 100% rename from data/images/icons/browser.png rename to public/images/icons/browser.png diff --git a/data/images/icons/clipboard.png b/public/images/icons/clipboard.png similarity index 100% rename from data/images/icons/clipboard.png rename to public/images/icons/clipboard.png diff --git a/data/images/icons/denied.png b/public/images/icons/denied.png similarity index 100% rename from data/images/icons/denied.png rename to public/images/icons/denied.png diff --git a/data/images/icons/document.png b/public/images/icons/document.png similarity index 100% rename from data/images/icons/document.png rename to public/images/icons/document.png diff --git a/data/images/icons/frames.png b/public/images/icons/frames.png similarity index 100% rename from data/images/icons/frames.png rename to public/images/icons/frames.png diff --git a/data/images/icons/gear.png b/public/images/icons/gear.png similarity index 100% rename from data/images/icons/gear.png rename to public/images/icons/gear.png diff --git a/data/images/icons/globe.png b/public/images/icons/globe.png similarity index 100% rename from data/images/icons/globe.png rename to public/images/icons/globe.png diff --git a/data/images/icons/heart.png b/public/images/icons/heart.png similarity index 100% rename from data/images/icons/heart.png rename to public/images/icons/heart.png diff --git a/data/images/icons/mail.png b/public/images/icons/mail.png similarity index 100% rename from data/images/icons/mail.png rename to public/images/icons/mail.png diff --git a/data/images/icons/merge.png b/public/images/icons/merge.png similarity index 100% rename from data/images/icons/merge.png rename to public/images/icons/merge.png diff --git a/data/images/icons/pin.png b/public/images/icons/pin.png similarity index 100% rename from data/images/icons/pin.png rename to public/images/icons/pin.png diff --git a/data/images/icons/pin2.png b/public/images/icons/pin2.png similarity index 100% rename from data/images/icons/pin2.png rename to public/images/icons/pin2.png diff --git a/data/images/icons/power.png b/public/images/icons/power.png similarity index 100% rename from data/images/icons/power.png rename to public/images/icons/power.png diff --git a/data/images/icons/recycle.png b/public/images/icons/recycle.png similarity index 100% rename from data/images/icons/recycle.png rename to public/images/icons/recycle.png diff --git a/data/images/icons/ribbon.png b/public/images/icons/ribbon.png similarity index 100% rename from data/images/icons/ribbon.png rename to public/images/icons/ribbon.png diff --git a/data/images/icons/stack.png b/public/images/icons/stack.png similarity index 100% rename from data/images/icons/stack.png rename to public/images/icons/stack.png diff --git a/data/images/icons/star.png b/public/images/icons/star.png similarity index 100% rename from data/images/icons/star.png rename to public/images/icons/star.png diff --git a/data/images/icons/tools.png b/public/images/icons/tools.png similarity index 100% rename from data/images/icons/tools.png rename to public/images/icons/tools.png diff --git a/data/images/icons_v2/menu.png b/public/images/icons_v2/menu.png similarity index 100% rename from data/images/icons_v2/menu.png rename to public/images/icons_v2/menu.png diff --git a/data/images/icons_v2/more.png b/public/images/icons_v2/more.png similarity index 100% rename from data/images/icons_v2/more.png rename to public/images/icons_v2/more.png diff --git a/data/images/logo.png b/public/images/logo.png similarity index 100% rename from data/images/logo.png rename to public/images/logo.png diff --git a/data/images/mail.png b/public/images/mail.png similarity index 100% rename from data/images/mail.png rename to public/images/mail.png diff --git a/data/images/material_icons/repeat.svg b/public/images/material_icons/repeat.svg similarity index 100% rename from data/images/material_icons/repeat.svg rename to public/images/material_icons/repeat.svg diff --git a/data/images/material_icons/repeat_one.svg b/public/images/material_icons/repeat_one.svg similarity index 100% rename from data/images/material_icons/repeat_one.svg rename to public/images/material_icons/repeat_one.svg diff --git a/data/images/menu.png b/public/images/menu.png similarity index 100% rename from data/images/menu.png rename to public/images/menu.png diff --git a/data/images/resize.png b/public/images/resize.png similarity index 100% rename from data/images/resize.png rename to public/images/resize.png diff --git a/data/images/sort.png b/public/images/sort.png similarity index 100% rename from data/images/sort.png rename to public/images/sort.png diff --git a/data/images/trash-can.png b/public/images/trash-can.png similarity index 100% rename from data/images/trash-can.png rename to public/images/trash-can.png diff --git a/data/images/tutorial/addon-menu.png b/public/images/tutorial/addon-menu.png similarity index 100% rename from data/images/tutorial/addon-menu.png rename to public/images/tutorial/addon-menu.png diff --git a/data/images/tutorial/config-panel.png b/public/images/tutorial/config-panel.png similarity index 100% rename from data/images/tutorial/config-panel.png rename to public/images/tutorial/config-panel.png diff --git a/data/images/tutorial/context-menus.png b/public/images/tutorial/context-menus.png similarity index 100% rename from data/images/tutorial/context-menus.png rename to public/images/tutorial/context-menus.png diff --git a/data/images/tutorial/delete-item.png b/public/images/tutorial/delete-item.png similarity index 100% rename from data/images/tutorial/delete-item.png rename to public/images/tutorial/delete-item.png diff --git a/data/images/tutorial/detach-mode.png b/public/images/tutorial/detach-mode.png similarity index 100% rename from data/images/tutorial/detach-mode.png rename to public/images/tutorial/detach-mode.png diff --git a/data/images/tutorial/export-import.png b/public/images/tutorial/export-import.png similarity index 100% rename from data/images/tutorial/export-import.png rename to public/images/tutorial/export-import.png diff --git a/data/images/tutorial/history-list.png b/public/images/tutorial/history-list.png similarity index 100% rename from data/images/tutorial/history-list.png rename to public/images/tutorial/history-list.png diff --git a/data/images/tutorial/main-menu.png b/public/images/tutorial/main-menu.png similarity index 100% rename from data/images/tutorial/main-menu.png rename to public/images/tutorial/main-menu.png diff --git a/data/images/tutorial/overview.png b/public/images/tutorial/overview.png similarity index 100% rename from data/images/tutorial/overview.png rename to public/images/tutorial/overview.png diff --git a/data/images/tutorial/resize-sessions.png b/public/images/tutorial/resize-sessions.png similarity index 100% rename from data/images/tutorial/resize-sessions.png rename to public/images/tutorial/resize-sessions.png diff --git a/data/images/tutorial/resize-ui.png b/public/images/tutorial/resize-ui.png similarity index 100% rename from data/images/tutorial/resize-ui.png rename to public/images/tutorial/resize-ui.png diff --git a/data/images/tutorial/session-context-menu.png b/public/images/tutorial/session-context-menu.png similarity index 100% rename from data/images/tutorial/session-context-menu.png rename to public/images/tutorial/session-context-menu.png diff --git a/data/images/tutorial/session-edit.png b/public/images/tutorial/session-edit.png similarity index 100% rename from data/images/tutorial/session-edit.png rename to public/images/tutorial/session-edit.png diff --git a/data/images/tutorial/session-filter.png b/public/images/tutorial/session-filter.png similarity index 100% rename from data/images/tutorial/session-filter.png rename to public/images/tutorial/session-filter.png diff --git a/data/images/tutorial/session-menu.png b/public/images/tutorial/session-menu.png similarity index 100% rename from data/images/tutorial/session-menu.png rename to public/images/tutorial/session-menu.png diff --git a/data/images/tutorial/session-save-settings.png b/public/images/tutorial/session-save-settings.png similarity index 100% rename from data/images/tutorial/session-save-settings.png rename to public/images/tutorial/session-save-settings.png diff --git a/data/images/tutorial/session-sorting.png b/public/images/tutorial/session-sorting.png similarity index 100% rename from data/images/tutorial/session-sorting.png rename to public/images/tutorial/session-sorting.png diff --git a/data/images/tutorial/sessions.png b/public/images/tutorial/sessions.png similarity index 100% rename from data/images/tutorial/sessions.png rename to public/images/tutorial/sessions.png diff --git a/data/images/tutorial/url-bar.png b/public/images/tutorial/url-bar.png similarity index 100% rename from data/images/tutorial/url-bar.png rename to public/images/tutorial/url-bar.png diff --git a/scripts/development.js b/src/background/development.js similarity index 100% rename from scripts/development.js rename to src/background/development.js diff --git a/scripts/favicon-service.js b/src/background/favicon-service.js similarity index 100% rename from scripts/favicon-service.js rename to src/background/favicon-service.js diff --git a/scripts/main.js b/src/background/main.js similarity index 97% rename from scripts/main.js rename to src/background/main.js index 901cfb4..01d9634 100644 --- a/scripts/main.js +++ b/src/background/main.js @@ -1,7 +1,7 @@ 'use strict'; export const Commands = { - toggleWidget: '_execute_browser_action', + openPopup: '_execute_browser_action', openTutorial: 'session_sync_tutorial', detachToTab: 'session_sync_detach_tab', detachToWindow: 'session_sync_detach_window', @@ -87,7 +87,7 @@ const SessionSync = (function () { browser.windows.create({ width: windowInfo.width, height: windowInfo.height, - url: 'entrypoints/popup/index.html', + url: 'popup.html', type: 'panel' }).then(function (newWindow) { activeWindow = newWindow; @@ -113,7 +113,7 @@ const SessionSync = (function () { } browser.tabs.create({ - url: 'entrypoints/popup/index.html' + url: 'popup.html' }); }); } diff --git a/scripts/session-history.js b/src/background/session-history.js similarity index 100% rename from scripts/session-history.js rename to src/background/session-history.js diff --git a/scripts/session-management.js b/src/background/session-management.js similarity index 98% rename from scripts/session-management.js rename to src/background/session-management.js index f008dc5..0932493 100644 --- a/scripts/session-management.js +++ b/src/background/session-management.js @@ -4,7 +4,7 @@ import { FaviconService } from './favicon-service'; export const SessionManagement = (function SessionManagement() { var lazyLoadingKey = 'restore.lazy.loading'; - var lazyLoadingUrl = typeof browser !== 'undefined' && browser.runtime ? browser.runtime.getURL('data/lazy/lazy.html') : ''; + var lazyLoadingUrl = typeof browser !== 'undefined' && browser.runtime ? browser.runtime.getURL('public/lazy/lazy.html') : ''; var lazyLoading = false; var windowsTabsToKill = []; @@ -149,7 +149,7 @@ export const SessionManagement = (function SessionManagement() { var createLazyTab = function createLazyTab(bookmark) { if (typeof browser === 'undefined' || !browser.runtime) return; - let title = browser.runtime.getURL('data/lazy/lazy.html') + + let title = browser.runtime.getURL('public/lazy/lazy.html') + '?state=redirect' + '&title=' + encodeURIComponent(bookmark.title) + '&url=' + encodeURIComponent(bookmark.url); diff --git a/src/entrypoints/background.js b/src/entrypoints/background.js new file mode 100644 index 0000000..26bec53 --- /dev/null +++ b/src/entrypoints/background.js @@ -0,0 +1,14 @@ +import { initBackground } from '../background/main'; +import { initDevelopment } from '../background/development'; +import { SessionManagement } from '../background/session-management'; +import { SessionAutoSave } from '../background/session-history'; + +export default defineBackground(() => { + // Initialize the background logic + initBackground(); + initDevelopment(); + SessionManagement.init(); + SessionAutoSave.init(); + + console.log('Session Sync Background script initialized.'); +}); diff --git a/entrypoints/popup/index.html b/src/entrypoints/popup/index.html similarity index 100% rename from entrypoints/popup/index.html rename to src/entrypoints/popup/index.html diff --git a/src/entrypoints/popup/main.js b/src/entrypoints/popup/main.js new file mode 100644 index 0000000..bccceec --- /dev/null +++ b/src/entrypoints/popup/main.js @@ -0,0 +1,5 @@ +import '../../../assets/css/index.css'; + +import { init } from '../../scripts/app.js'; + +init(); diff --git a/data/scripts/3rd/pubsub.js b/src/scripts/3rd/pubsub.js similarity index 100% rename from data/scripts/3rd/pubsub.js rename to src/scripts/3rd/pubsub.js diff --git a/data/scripts/app.js b/src/scripts/app.js similarity index 57% rename from data/scripts/app.js rename to src/scripts/app.js index e9147ce..3b77ca1 100644 --- a/data/scripts/app.js +++ b/src/scripts/app.js @@ -14,46 +14,41 @@ const sessionRoot = 'SessionSync'; // ------------------------------------------------------------------------ // API -const findSessionSync = function findSessionSync(callback) -{ - BookmarkManager.searchBookmarks({title: sessionRoot}) - .then( - function success(bookmarks) { - if (bookmarks.length == 0) - { - BookmarkManager.createBookmark({ - title: sessionRoot, - parentId: 'menu________' - }).then(callback, function error() { +const findSessionSync = function findSessionSync(callback) { + BookmarkManager.searchBookmarks({ title: sessionRoot }) + .then( + function success(bookmarks) { + if (bookmarks.length == 0) { BookmarkManager.createBookmark({ title: sessionRoot, - }).then(callback); - }); + parentId: 'menu________' + }).then(callback, function error() { + BookmarkManager.createBookmark({ + title: sessionRoot, + }).then(callback); + }); + } + else { + callback(bookmarks[0]); + } + }, + function fail() { } - else - { - callback(bookmarks[0]); - } - }, - function fail() { - } - ); + ); }; -const listSessions = function listSessions() -{ - BookmarkManager.getFolderBookmarks(AppConfig.get('storageID'), function(sessions) { +const listSessions = function listSessions() { + BookmarkManager.getFolderBookmarks(AppConfig.get('storageID'), function (sessions) { SessionSyncModel.sessions = sessions; WindowEvents.emit(document, 'ListSessions'); }); }; -export const init = function init() -{ +export const init = function init() { SessionSyncUI(); - findSessionSync(function(bookmark) { - AppConfig.set('storageID', bookmark.id, function() { + findSessionSync(function (bookmark) { + AppConfig.set('storageID', bookmark.id, function () { listSessions(true); }); }); diff --git a/data/scripts/components/clipboard.js b/src/scripts/components/clipboard.js similarity index 100% rename from data/scripts/components/clipboard.js rename to src/scripts/components/clipboard.js diff --git a/data/scripts/components/config-panel.js b/src/scripts/components/config-panel.js similarity index 100% rename from data/scripts/components/config-panel.js rename to src/scripts/components/config-panel.js diff --git a/data/scripts/components/notification-system.js b/src/scripts/components/notification-system.js similarity index 100% rename from data/scripts/components/notification-system.js rename to src/scripts/components/notification-system.js diff --git a/data/scripts/components/overlay-system.js b/src/scripts/components/overlay-system.js similarity index 100% rename from data/scripts/components/overlay-system.js rename to src/scripts/components/overlay-system.js diff --git a/data/scripts/components/session-bookmark-events.js b/src/scripts/components/session-bookmark-events.js similarity index 100% rename from data/scripts/components/session-bookmark-events.js rename to src/scripts/components/session-bookmark-events.js diff --git a/data/scripts/components/session-bookmark.js b/src/scripts/components/session-bookmark.js similarity index 100% rename from data/scripts/components/session-bookmark.js rename to src/scripts/components/session-bookmark.js diff --git a/data/scripts/components/session-container.js b/src/scripts/components/session-container.js similarity index 100% rename from data/scripts/components/session-container.js rename to src/scripts/components/session-container.js diff --git a/data/scripts/components/session-filtering.js b/src/scripts/components/session-filtering.js similarity index 100% rename from data/scripts/components/session-filtering.js rename to src/scripts/components/session-filtering.js diff --git a/data/scripts/components/session-folder-events.js b/src/scripts/components/session-folder-events.js similarity index 100% rename from data/scripts/components/session-folder-events.js rename to src/scripts/components/session-folder-events.js diff --git a/data/scripts/components/session-folder.js b/src/scripts/components/session-folder.js similarity index 100% rename from data/scripts/components/session-folder.js rename to src/scripts/components/session-folder.js diff --git a/data/scripts/components/session-header-bar.js b/src/scripts/components/session-header-bar.js similarity index 100% rename from data/scripts/components/session-header-bar.js rename to src/scripts/components/session-header-bar.js diff --git a/data/scripts/components/session-history.js b/src/scripts/components/session-history.js similarity index 100% rename from data/scripts/components/session-history.js rename to src/scripts/components/session-history.js diff --git a/data/scripts/components/session-hotkey-manager.js b/src/scripts/components/session-hotkey-manager.js similarity index 100% rename from data/scripts/components/session-hotkey-manager.js rename to src/scripts/components/session-hotkey-manager.js diff --git a/data/scripts/components/session-hotkeys.js b/src/scripts/components/session-hotkeys.js similarity index 100% rename from data/scripts/components/session-hotkeys.js rename to src/scripts/components/session-hotkeys.js diff --git a/data/scripts/components/session-list.js b/src/scripts/components/session-list.js similarity index 100% rename from data/scripts/components/session-list.js rename to src/scripts/components/session-list.js diff --git a/data/scripts/components/session-sorting.js b/src/scripts/components/session-sorting.js similarity index 100% rename from data/scripts/components/session-sorting.js rename to src/scripts/components/session-sorting.js diff --git a/data/scripts/components/session-sync-model.js b/src/scripts/components/session-sync-model.js similarity index 100% rename from data/scripts/components/session-sync-model.js rename to src/scripts/components/session-sync-model.js diff --git a/data/scripts/components/session-toolbar.js b/src/scripts/components/session-toolbar.js similarity index 80% rename from data/scripts/components/session-toolbar.js rename to src/scripts/components/session-toolbar.js index 04a2207..4730d60 100644 --- a/data/scripts/components/session-toolbar.js +++ b/src/scripts/components/session-toolbar.js @@ -18,29 +18,28 @@ import * as DOMComponent from '../utils/components'; * Command bar */ -export function SessionToolbar() -{ +export function SessionToolbar() { // Create DomHealper var DomElem = HTMLCreator(); // ------------------------------------------------------------------------ // Create UI - var toolbar = DomElem('div', {class: 'session-toolbar'}); + var toolbar = DomElem('div', { class: 'session-toolbar' }); // ------------------------------------------------------------------------ // List settings - var container = DomElem('div', {class: 'session-selector'}); + var container = DomElem('div', { class: 'session-selector' }); container.style.width = AppConfig.get('style.scale.bookmarks') + 'px'; - var activeBtn = DomElem('div', {class: 'button active-session'}); + var activeBtn = DomElem('div', { class: 'button active-session' }); activeBtn.textContent = ''; - var syncBtn = DomElem('div', {class: 'button sync'}); + var syncBtn = DomElem('div', { class: 'button sync' }); syncBtn.textContent = 'Sessions'; - var historyBtn = DomElem('div', {class: 'button history'}); + var historyBtn = DomElem('div', { class: 'button history' }); historyBtn.textContent = 'History'; container.appendChild(activeBtn); @@ -54,41 +53,41 @@ export function SessionToolbar() var sessionMenu = this.createMenu(); toolbar.appendChild(sessionMenu); - var sessionDate = DomElem('div', {class: 'session-date'}); + var sessionDate = DomElem('div', { class: 'session-date' }); toolbar.appendChild(sessionDate); // ------------------------------------------------------------------------ // Events if (typeof document !== 'undefined') { - WindowEvents.on(document, 'SetSessionDate', function(date) { + WindowEvents.on(document, 'SetSessionDate', function (date) { sessionDate.textContent = (new Date(date)).toLocaleString(); }); } - activeBtn.addEventListener('click', function() { + activeBtn.addEventListener('click', function () { if (typeof document !== 'undefined') { WindowEvents.emit(document, 'ShowCurrentSession'); } }); - syncBtn.addEventListener('click', function() { + syncBtn.addEventListener('click', function () { if (typeof document !== 'undefined') { WindowEvents.emit(document, 'ShowSyncList', { update: true }); } }); - historyBtn.addEventListener('click', function() { + historyBtn.addEventListener('click', function () { if (typeof document !== 'undefined') { WindowEvents.emit(document, 'ShowHistoryList'); } }); - AppConfig.onChange('style.sessions.list.width', function(width) { + AppConfig.onChange('style.sessions.list.width', function (width) { container.style.width = width + 'px'; }); - AppConfig.onChange('style.scale.toolbar', function(value) { + AppConfig.onChange('style.scale.toolbar', function (value) { toolbar.style.fontSize = value + 'px'; }); @@ -99,17 +98,15 @@ export function SessionToolbar() this.DOMRoot = toolbar; } -SessionToolbar.prototype.createMenu = function createMenu() -{ +SessionToolbar.prototype.createMenu = function createMenu() { var DomElem = HTMLCreator(); // ------------------------------------------------------------------------ // UI - SessionContainer menu - var menu = DomElem('div', {class: 'menu-bar'}); + var menu = DomElem('div', { class: 'menu-bar' }); - var ActionButton = function actionButton(options) - { + var ActionButton = function actionButton(options) { var button = DomElem('div', { class: 'button ' + options.className, 'state-current': options.state.current, @@ -132,7 +129,7 @@ SessionToolbar.prototype.createMenu = function createMenu() className: 'save', event: 'MenuSaveSession', state: { - current : true, + current: true, history: true } }); @@ -160,7 +157,7 @@ SessionToolbar.prototype.createMenu = function createMenu() className: 'restore-new-win', event: 'MenuRestoreNewWindow', state: { - restore : true, + restore: true, history: true } }); @@ -201,21 +198,21 @@ SessionToolbar.prototype.createMenu = function createMenu() } }); - var separator1 = DomElem('div', {class: 'separator'}); - var separator2 = DomElem('div', {class: 'separator'}); - var saveConfig = DomElem('div', {class: 'save-config'}); + var separator1 = DomElem('div', { class: 'separator' }); + var separator2 = DomElem('div', { class: 'separator' }); + var saveConfig = DomElem('div', { class: 'save-config' }); // Pin tabs action var saveCfgKey = 'session.save'; var saveCfg = AppConfig.get(saveCfgKey); var cfgSavePinned = new DOMComponent.ToggleSwitch({ - state: saveCfg.pinned, + state: saveCfg?.pinned, tooltip: 'Save pinned tabs', attribute: 'pin', onState: '', offState: '', - callback: function(value) { + callback: function (value) { saveCfg.pinned = value; AppConfig.set(saveCfgKey, saveCfg); } @@ -223,12 +220,12 @@ SessionToolbar.prototype.createMenu = function createMenu() saveConfig.appendChild(cfgSavePinned.DOMRoot); var cfgAllWindows = new DOMComponent.ToggleSwitch({ - state: saveCfg.allWindows, + state: saveCfg?.allWindows, tooltip: 'Show all windows', attribute: 'windows', onState: '', offState: '', - callback: function(value) { + callback: function (value) { saveCfg.allWindows = value; AppConfig.set(saveCfgKey, saveCfg); } @@ -262,13 +259,13 @@ SessionToolbar.prototype.createMenu = function createMenu() // ------------------------------------------------------------------------ // Events - AppConfig.onChange(saveCfgKey, function(value) { + AppConfig.onChange(saveCfgKey, function (value) { cfgSavePinned.setState(value.pinned); cfgAllWindows.setState(value.allWindows); }); // Tooltip events - menu.addEventListener('mouseover', function(e) { + menu.addEventListener('mouseover', function (e) { var tooltipMsg = e.target.getAttribute('tooltip'); if (tooltipMsg) { if (typeof document !== 'undefined') { @@ -284,7 +281,7 @@ SessionToolbar.prototype.createMenu = function createMenu() } }); - menu.addEventListener('mouseleave', function() { + menu.addEventListener('mouseleave', function () { if (typeof document !== 'undefined') { WindowEvents.emit(document, 'HideTooltip'); } diff --git a/data/scripts/components/tooltip-system.js b/src/scripts/components/tooltip-system.js similarity index 100% rename from data/scripts/components/tooltip-system.js rename to src/scripts/components/tooltip-system.js diff --git a/data/scripts/components/trash-can.js b/src/scripts/components/trash-can.js similarity index 100% rename from data/scripts/components/trash-can.js rename to src/scripts/components/trash-can.js diff --git a/data/scripts/components/url-bar.js b/src/scripts/components/url-bar.js similarity index 100% rename from data/scripts/components/url-bar.js rename to src/scripts/components/url-bar.js diff --git a/data/scripts/config.js b/src/scripts/config.js similarity index 58% rename from data/scripts/config.js rename to src/scripts/config.js index c7b0429..4b9f4f8 100644 --- a/data/scripts/config.js +++ b/src/scripts/config.js @@ -13,84 +13,68 @@ import { GlobalEvents } from './utils/global-events'; const Config = (function () { + const defaults = { + 'style.panel.width': 800, + 'style.panel.height': 600, + 'style.sessions.list.width': 220, + + 'style.scale.toolbar': 12, + 'style.scale.header': 18, + 'style.scale.sessions': 12, + 'style.scale.bookmarks': 14, + + 'state.scrollTop.current': 0, + 'state.scrollTop.restore': 0, + 'state.scrollTop.history': 0, + + 'session.save': { + pinned: false, + allWindows: true + }, + + 'session.view': undefined, + 'session.sorting': 'position-asc', + 'session.active.filter': '', + 'session.selected': null, + 'session.history.selected': null, + 'services.favicon.enabled': true, + + 'bookmark.click.newTab': false, + 'bookmark.middleClick.newTab': true, + 'restore.lazy.loading': true, + 'restore.reverse.order': false, + + 'context.menu.icons': true, + 'trashcan.hide': true, + 'trashcan.hide-count': 0, + 'undo.events': [], + 'troubleshooting': false, + 'theme.toolbar.grayscale': false + }; + var initState = true; var initConfig = true; - var localSettings = {}; + var localSettings = defaults; var configEvents = new PubSub(); var isPanelUI = (typeof document !== 'undefined' && document.body) ? (document.body.clientWidth == 0) : false; // ------------------------------------------------------------------------ // API - function getConfigValue(key, defaultValue) { - if (initConfig && localSettings[key] == undefined) { - localSettings[key] = defaultValue; - return; - } - - browser.storage.local.get(key).then(function (obj) { - if (obj.hasOwnProperty(key)) { - // console.log('[LocalStorage][Read]', key, obj[key]); - localSettings[key] = obj[key]; - configEvents.publish(key, obj[key]); - } - else { - // console.log('Not found [LocalStorage][Set]', key, defaultValue); - browser.storage.local.set({ [key]: defaultValue }) - .then(function success() { - localSettings[key] = defaultValue; - configEvents.publish(key, defaultValue); - }); - } - }); - } - var setupConfig = function setupConfig() { - // Styling - getConfigValue('style.panel.width', 800); - getConfigValue('style.panel.height', 600); - getConfigValue('style.sessions.list.width', 220); - - getConfigValue('style.scale.toolbar', 12); - getConfigValue('style.scale.header', 18); - getConfigValue('style.scale.sessions', 12); - getConfigValue('style.scale.bookmarks', 14); - - getConfigValue('state.scrollTop.current', 0); - getConfigValue('state.scrollTop.restore', 0); - getConfigValue('state.scrollTop.history', 0); - - // Session saving settings - getConfigValue('session.save', { - pinned: false, - allWindows: true + browser.storage.local.get(null).then(function (obj) { + console.log(obj); + for (let key in defaults) { + if (obj.hasOwnProperty(key)) { + localSettings[key] = obj[key]; + } else { + localSettings[key] = defaults[key]; + } + configEvents.publish(key, localSettings[key]); + } + initConfig = false; + configEvents.publish('config.init', localSettings); }); - - // Management configuration - getConfigValue('session.view', undefined); - getConfigValue('session.sorting', 'position-asc'); - getConfigValue('session.active.filter', ''); - getConfigValue('session.selected', null); - getConfigValue('session.history.selected', null); - getConfigValue('services.favicon.enabled', true); - - // Bookmarks configuration - getConfigValue('bookmark.click.newTab', false); - getConfigValue('bookmark.middleClick.newTab', true); - getConfigValue('restore.lazy.loading', true); - getConfigValue('restore.reverse.order', false); - - // General settings - getConfigValue('context.menu.icons', true); - - getConfigValue('trashcan.hide', true); - getConfigValue('trashcan.hide-count', 0); - getConfigValue('undo.events', []); - - getConfigValue('troubleshooting', false); - - // Theme configuration - getConfigValue('theme.toolbar.grayscale', false); }; var set = function set(key, value, onSuccess) { @@ -151,7 +135,7 @@ const Config = (function () { var init = function init() { setupConfig(); - // console.log(localSettings); + console.log(localSettings); if (isAddonContext()) { browser.commands.getAll(). diff --git a/data/scripts/session-sync-ui.js b/src/scripts/session-sync-ui.js similarity index 100% rename from data/scripts/session-sync-ui.js rename to src/scripts/session-sync-ui.js diff --git a/data/scripts/session/bookmarks.js b/src/scripts/session/bookmarks.js similarity index 73% rename from data/scripts/session/bookmarks.js rename to src/scripts/session/bookmarks.js index d0732d0..f608bb5 100644 --- a/data/scripts/session/bookmarks.js +++ b/src/scripts/session/bookmarks.js @@ -10,10 +10,9 @@ import { AppConfig } from '../config'; export const BookmarkManager = (function BookmarkManager() { - var lazyLoadingUrl = browser.runtime.getURL('data/lazy/lazy.html'); + var lazyLoadingUrl = browser.runtime.getURL('public/lazy/lazy.html'); - function getLazyLoadingParameters(url) - { + function getLazyLoadingParameters(url) { let paramater = {}; let splits = url.split('?'); if (splits.length < 2) return paramater; @@ -25,8 +24,7 @@ export const BookmarkManager = (function BookmarkManager() { return paramater; } - var getHostName = function getHostName(url) - { + var getHostName = function getHostName(url) { try { return new URL(url).hostname; } catch (e) { @@ -34,59 +32,51 @@ export const BookmarkManager = (function BookmarkManager() { } }; - var getFaviconUrl = function getFaviconUrl(favIconUrl, callback) - { + var getFaviconUrl = function getFaviconUrl(favIconUrl, callback) { var key = '@favIconUrl:' + getHostName(favIconUrl); browser.storage.local.get(key) - .then(function (obj) { - if (obj[key]) { - callback(obj[key]); - } - }); + .then(function (obj) { + if (obj[key]) { + callback(obj[key]); + } + }); }; - var getFolderBookmarks = function getFolderBookmarks(folderID, callback) - { + var getFolderBookmarks = function getFolderBookmarks(folderID, callback) { var result = browser.bookmarks.getChildren(folderID); result.then(callback, function fail() { console.log('Error getting bookmarks from folder ID: ', folderID); }); }; - var searchBookmarks = function searchBookmarks(keyword) - { + var searchBookmarks = function searchBookmarks(keyword) { return browser.bookmarks.search(keyword); }; - var getOpenMode = function getOpenMode(index) - { + var getOpenMode = function getOpenMode(index) { var buttons = ['click', 'middleClick']; var state = AppConfig.get('bookmark.' + buttons[index] + '.newTab'); return state ? 'newTab' : 'activeTab'; }; var containerSuffix = { - encode: function(id) { return ' [container:' + id + ']'; }, - regex: /^(.*) \[container:([^\]]+)\]$/, + encode: function (id) { return ' [container:' + id + ']'; }, + regex: /^(.*) \[container:([^\]]+)\]$/, default: 'firefox-default', }; - var createBookmarkFromTab = function createBookmarkFromTab(tab, parentId) - { + var createBookmarkFromTab = function createBookmarkFromTab(tab, parentId) { var url = tab.url; - if (url.startsWith(lazyLoadingUrl, 0)) - { + if (url.startsWith(lazyLoadingUrl, 0)) { var info = getLazyLoadingParameters(url); url = info.url; } - else - { - browser.runtime.sendMessage({event: 'save-favicon', tab: tab}); + else { + browser.runtime.sendMessage({ event: 'save-favicon', tab: tab }); } var title = tab.title; - if (tab.cookieStoreId && tab.cookieStoreId !== containerSuffix.default) - { + if (tab.cookieStoreId && tab.cookieStoreId !== containerSuffix.default) { title += containerSuffix.encode(tab.cookieStoreId); } @@ -100,8 +90,7 @@ export const BookmarkManager = (function BookmarkManager() { // Parse a [container:] suffix from a bookmark title. // Returns { title, cookieStoreId } — cookieStoreId is null when absent. - var parseContainerFromTitle = function parseContainerFromTitle(rawTitle) - { + var parseContainerFromTitle = function parseContainerFromTitle(rawTitle) { var match = rawTitle && rawTitle.match(containerSuffix.regex); if (match) { return { title: match[1], cookieStoreId: match[2] }; @@ -109,8 +98,7 @@ export const BookmarkManager = (function BookmarkManager() { return { title: rawTitle, cookieStoreId: null }; }; - var createBookmark = function createBookmark(options) - { + var createBookmark = function createBookmark(options) { // parentId possible values // root________ // menu________ @@ -120,22 +108,18 @@ export const BookmarkManager = (function BookmarkManager() { return browser.bookmarks.create(options); }; - var openBookmark = function openBookmark(options) - { - if (AppConfig.isPanel() == false) - { - if (options.mode == 'activeTab') - { + var openBookmark = function openBookmark(options) { + if (AppConfig.isPanel() == false) { + if (options.mode == 'activeTab') { options.mode = 'newTab'; options.active = false; } } - browser.runtime.sendMessage({event: 'open-tab', options: options}); + browser.runtime.sendMessage({ event: 'open-tab', options: options }); }; - var setTitle = function setTitle(bookmark, title, callback) - { + var setTitle = function setTitle(bookmark, title, callback) { browser.bookmarks.update(bookmark.id, { title: title }).then(function success() { @@ -148,8 +132,7 @@ export const BookmarkManager = (function BookmarkManager() { }); }; - var setLocation = function setLocation(bookmark, url) - { + var setLocation = function setLocation(bookmark, url) { browser.bookmarks.update(bookmark.id, { url: url }).then(function success() { @@ -159,26 +142,22 @@ export const BookmarkManager = (function BookmarkManager() { }); }; - var moveItem = function moveItem(itemID, newParentID, index, callback, errorCallback) - { + var moveItem = function moveItem(itemID, newParentID, index, callback, errorCallback) { browser.bookmarks.move(itemID, { parentId: newParentID, index: index }) - .then(callback, errorCallback); + .then(callback, errorCallback); }; - var deleteItem = function deleteItem(itemID, isFolder, callback, errorCallback) - { - if (isFolder) - { + var deleteItem = function deleteItem(itemID, isFolder, callback, errorCallback) { + if (isFolder) { browser.bookmarks.removeTree(itemID) - .then(callback, errorCallback); + .then(callback, errorCallback); } - else - { + else { browser.bookmarks.remove(itemID) - .then(callback, errorCallback); + .then(callback, errorCallback); } }; @@ -193,10 +172,10 @@ export const BookmarkManager = (function BookmarkManager() { setTitle: setTitle, setLocation: setLocation, - moveItem: moveItem, + moveItem: moveItem, deleteItem: deleteItem, - getFolderBookmarks : getFolderBookmarks + getFolderBookmarks: getFolderBookmarks }; })(); \ No newline at end of file diff --git a/data/scripts/session/management.js b/src/scripts/session/management.js similarity index 100% rename from data/scripts/session/management.js rename to src/scripts/session/management.js diff --git a/data/scripts/utils/auto-scroll.js b/src/scripts/utils/auto-scroll.js similarity index 100% rename from data/scripts/utils/auto-scroll.js rename to src/scripts/utils/auto-scroll.js diff --git a/data/scripts/utils/components.js b/src/scripts/utils/components.js similarity index 100% rename from data/scripts/utils/components.js rename to src/scripts/utils/components.js diff --git a/data/scripts/utils/confirm-box.js b/src/scripts/utils/confirm-box.js similarity index 100% rename from data/scripts/utils/confirm-box.js rename to src/scripts/utils/confirm-box.js diff --git a/data/scripts/utils/context-menu.js b/src/scripts/utils/context-menu.js similarity index 100% rename from data/scripts/utils/context-menu.js rename to src/scripts/utils/context-menu.js diff --git a/data/scripts/utils/dom.js b/src/scripts/utils/dom.js similarity index 100% rename from data/scripts/utils/dom.js rename to src/scripts/utils/dom.js diff --git a/data/scripts/utils/drag-context.js b/src/scripts/utils/drag-context.js similarity index 100% rename from data/scripts/utils/drag-context.js rename to src/scripts/utils/drag-context.js diff --git a/data/scripts/utils/field-edit-widget.js b/src/scripts/utils/field-edit-widget.js similarity index 100% rename from data/scripts/utils/field-edit-widget.js rename to src/scripts/utils/field-edit-widget.js diff --git a/data/scripts/utils/general.js b/src/scripts/utils/general.js similarity index 100% rename from data/scripts/utils/general.js rename to src/scripts/utils/general.js diff --git a/data/scripts/utils/global-events.js b/src/scripts/utils/global-events.js similarity index 100% rename from data/scripts/utils/global-events.js rename to src/scripts/utils/global-events.js diff --git a/wxt.config.js b/wxt.config.js index d41877b..37251c0 100644 --- a/wxt.config.js +++ b/wxt.config.js @@ -6,6 +6,16 @@ console.log('FIREFOX_PROFILE:', process.env.FIREFOX_PROFILE); export default defineConfig(() => { return { + srcDir: 'src', + publicDir: 'public', + vite: () => ({ + optimizeDeps: { + entries: [ + 'src/entrypoints/popup/main.js', + 'src/entrypoints/background.js', + ], + }, + }), browser: 'firefox', webExt: { binaries: { @@ -20,10 +30,10 @@ export default defineConfig(() => { author: 'Gabriel Ivanica', description: 'Save sessions as bookmarks and sync them through Firefox Sync (or any other sync engine).\nPowerful session management - organize, edit, change, save, restore, etc\n', homepage_url: 'https://github.com/ReDEnergy/SessionSync', - manifest_version: 2, default_locale: 'en', browser_specific_settings: { gecko: { + id: 'session-sync@gabrielivanica.com', data_collection_permissions: { required: ['none'] } @@ -38,6 +48,23 @@ export default defineConfig(() => { 'cookies', 'contextualIdentities' ], + action: { + default_icon: { + '16': 'images/icon16.png', + '32': 'images/icon32.png', + '48': 'images/icon48.png', + '128': 'images/icon128.png' + }, + default_title: 'Session Sync', + }, + icons: { + '16': 'images/icon16.png', + '32': 'images/icon32.png', + '48': 'images/icon48.png', + '64': 'images/icon64.png', + '96': 'images/icon96.png', + '128': 'images/icon128.png' + }, commands: { '_execute_browser_action': { suggested_key: { @@ -45,12 +72,6 @@ export default defineConfig(() => { }, description: 'Toggle Widget' }, - 'session_sync_tutorial': { - suggested_key: { - 'default': 'Ctrl+Shift+L' - }, - description: 'Tutorial' - }, 'session_sync_detach_tab': { suggested_key: { 'default': 'Ctrl+Shift+U' @@ -59,7 +80,7 @@ export default defineConfig(() => { }, 'session_sync_detach_window': { suggested_key: { - 'default': 'Ctrl+Shift+V' + 'default': 'Ctrl+Shift+2' }, description: 'Window View' } From ae9ac584f589746d69db7af634b7bd13890c262a Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Wed, 8 Apr 2026 02:09:51 +0300 Subject: [PATCH 18/32] Replaced custom lazy-loading implementation with browser native option (tab discard) --- public/lazy/lazy.css | 77 ------- public/lazy/lazy.html | 36 ---- public/lazy/lazy.js | 32 --- src/background/session-history.js | 72 ++++--- src/background/session-management.js | 201 ++++--------------- src/scripts/components/session-header-bar.js | 39 ---- src/scripts/session/bookmarks.js | 22 +- src/scripts/session/management.js | 1 - 8 files changed, 74 insertions(+), 406 deletions(-) delete mode 100644 public/lazy/lazy.css delete mode 100644 public/lazy/lazy.html delete mode 100644 public/lazy/lazy.js diff --git a/public/lazy/lazy.css b/public/lazy/lazy.css deleted file mode 100644 index 426db7c..0000000 --- a/public/lazy/lazy.css +++ /dev/null @@ -1,77 +0,0 @@ -:root { - --main-text: #0C0C0D; - --sub-text: #737373; -} - -body { - font-family: 'Segoe UI', 'San Francisco', 'Ubuntu', 'Fira Sans', 'Roboto', 'Arial', 'Helvetica', sans-serif; - font-size: 15px; - font-weight: 400; - color: var(--main-text); - background-color: var(--main-bg); - line-height: 1.5; - display: flex; - flex-direction: row; -} - -#container { - display: flex; - flex-direction: column; - width: 100%; - padding-left: 20px; -} - -#title { - font-size: 32px; - font-weight: 600; - color: var(--sub-text); - line-height: 2; - text-align: center; -} - -#url { - text-align: center; -} - -.caption { - font-size: 16px; - font-weight: 400; - color: var(--sub-text); -} - -hr { - width: 100%; - background-color: var(--line); - height: 1px; - border: none; - margin-top: 20px; - margin-bottom: 20px; -} - -#warning { - position: absolute; - bottom: 50%; - width: 100%; -} - -#warning .message { - margin: 0 auto; - max-width: 980px; - color: #333; -} - -#warning .separator { - height: 1em; -} - -#warning ul { - margin: 0 auto; -} - -#warning ul li { - text-align: left; -} - -#warning .highlight { - color: #EA1F1F; -} diff --git a/public/lazy/lazy.html b/public/lazy/lazy.html deleted file mode 100644 index 5ea1717..0000000 --- a/public/lazy/lazy.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - -
-
Title
- about:example -
- -
-
This page is used for lazy loading Session Sync restored pages
-
If the page was not loaded automatically please check that lazy loading is enabled in addon configuration panel.
-
-
-
Sadly, Firefox Addon API prevents loading certain URL types. This is not an addon issue but and API limitation.
-
    -
  • chrome: URLs
  • -
  • javascript: URLs
  • -
  • data: URLs
  • -
  • file: URLs (files on the filesystem)
  • -
  • priviledged about: URLs
  • -
-
-
-
- - - - - diff --git a/public/lazy/lazy.js b/public/lazy/lazy.js deleted file mode 100644 index c6dcf5b..0000000 --- a/public/lazy/lazy.js +++ /dev/null @@ -1,32 +0,0 @@ -function getLazyLoadingParameters(url) -{ - let paramater = {}; - let paras = url.split('?')[1].split('&'); - for (let p of paras) { - paramater[p.split('=')[0]] = decodeURIComponent(p.split('=')[1]); - } - return paramater; -} - -(function () -{ - var title = document.getElementById('title'); - var urlInfo = document.getElementById('url'); - - let paramater = getLazyLoadingParameters(location.href); - - document.title = paramater.title; - title.textContent = paramater.title; - - urlInfo.textContent = paramater.url; - urlInfo.href = paramater.url; - - if (paramater.favIconUrl) { - var favicon = document.createElement('link'); - favicon.rel = 'shortcut icon'; - favicon.href = paramater.favIconUrl; - document.head.appendChild(favicon); - } - -})(); - diff --git a/src/background/session-history.js b/src/background/session-history.js index a82aa21..83bfc58 100644 --- a/src/background/session-history.js +++ b/src/background/session-history.js @@ -31,50 +31,48 @@ BrowserSession.prototype.update = function update() { browser.windows.getAll({ populate: true, windowTypes: ['normal'] - }) - .then(function (mozWindows) { - var tabCount = 0; + }).then(function (mozWindows) { + var tabCount = 0; - for (var key in this.windows) { - this.windows[key].active = false; - } - - for (let mozWindow of mozWindows) { - if (mozWindow.incognito && (this.config && this.config.savePrivate == false)) { - delete this.windows[mozWindow.id]; - continue; - } + for (var key in this.windows) { + this.windows[key].active = false; + } - var windowInfo = new WindowSession(mozWindow.id); - - // Add window tabs - for (var i in mozWindow.tabs) { - var tab = mozWindow.tabs[i]; - var info = SessionManagement.getTabInfo(tab.url); - var sessionTab = { - title: tab.title, - url: info.url, - favIconUrl: info.lazyLoading ? info.favIconUrl : tab.favIconUrl, - pinned: tab.pinned, - cookieStoreId: tab.cookieStoreId, - }; - tabCount++; - windowInfo.tabs[tab.index] = sessionTab; - } + for (let mozWindow of mozWindows) { + if (mozWindow.incognito && (this.config && this.config.savePrivate == false)) { + delete this.windows[mozWindow.id]; + continue; + } - this.windows[mozWindow.id] = windowInfo; + var windowInfo = new WindowSession(mozWindow.id); + + // Add window tabs + for (var i in mozWindow.tabs) { + var tab = mozWindow.tabs[i]; + var sessionTab = { + title: tab.title, + url: tab.url, + favIconUrl: tab.favIconUrl, + pinned: tab.pinned, + cookieStoreId: tab.cookieStoreId, + }; + tabCount++; + windowInfo.tabs[tab.index] = sessionTab; } - // Update internal storage - this.lastSave = (new Date()).getTime(); - this.tabCount = tabCount; + this.windows[mozWindow.id] = windowInfo; + } - // Save new session - if (browser.storage && browser.storage.local) { - browser.storage.local.set({ [storageKey.active]: this }); - } + // Update internal storage + this.lastSave = (new Date()).getTime(); + this.tabCount = tabCount; + + // Save new session + if (browser.storage && browser.storage.local) { + browser.storage.local.set({ [storageKey.active]: this }); + } - }.bind(this)); + }.bind(this)); }; BrowserSession.prototype.clear = function clear() { diff --git a/src/background/session-management.js b/src/background/session-management.js index 0932493..d6bd04f 100644 --- a/src/background/session-management.js +++ b/src/background/session-management.js @@ -4,31 +4,15 @@ import { FaviconService } from './favicon-service'; export const SessionManagement = (function SessionManagement() { var lazyLoadingKey = 'restore.lazy.loading'; - var lazyLoadingUrl = typeof browser !== 'undefined' && browser.runtime ? browser.runtime.getURL('public/lazy/lazy.html') : ''; var lazyLoading = false; var windowsTabsToKill = []; var reverseOrderKey = 'restore.reverse.order'; var reverseOrder = false; - function getLazyLoadingParameters(url) { - let paramater = {}; - let params = url.split('?'); - if (params.length > 1) { - let paras = params[1].split('&'); - for (let p of paras) { - paramater[p.split('=')[0]] = decodeURIComponent(p.split('=')[1]); - } - } - return paramater; - } + const containerTag = 'container'; function getTabInfo(url) { - if (lazyLoadingUrl && url.startsWith(lazyLoadingUrl)) { - var info = getLazyLoadingParameters(url); - info.lazyLoading = true; - return info; - } return { url: url }; @@ -36,7 +20,7 @@ export const SessionManagement = (function SessionManagement() { // Container suffix helpers — canonical source: data/scripts/session/bookmarks.js var containerSuffix = { - regex: /^(.*) \[container:([^\]]+)\]$/, + regex: new RegExp('^(.*) \\[' + containerTag + ':([^\\]]+)\\]$'), }; // Parse a [container:] suffix from a bookmark title. @@ -49,19 +33,19 @@ export const SessionManagement = (function SessionManagement() { return { title: rawTitle, cookieStoreId: null }; } - // Create a tab with a cookieStoreId; silently falls back to default container - // if the container doesn't exist or the API rejects it. - // onError is called only if creation fails even without a container. - function createTabWithContainer(options, cookieStoreId, onSuccess, onError) { + function createTab(options, info, onSuccess, onError) { if (typeof browser === 'undefined' || !browser.tabs) return; var tryCreate = function (withContainer) { var tabOptions = Object.assign({}, options); - if (withContainer && cookieStoreId) { - tabOptions.cookieStoreId = cookieStoreId; + if (withContainer && info.container) { + tabOptions.cookieStoreId = info.container; } return browser.tabs.create(tabOptions).then( function (tabInfo) { + if (info.discard) { + browser.tabs.discard(tabInfo.id); + } if (onSuccess) onSuccess(tabInfo); }, function (err) { @@ -70,26 +54,17 @@ export const SessionManagement = (function SessionManagement() { tryCreate(false); } else { if (onError) onError(err); - else console.log('createTabWithContainer error:', err); + else console.log('createTab error:', err); } } ); }; - tryCreate(cookieStoreId != null); + tryCreate(info.container != null); } var setLazyLoading = function setLazyLoading(state) { if (typeof browser === 'undefined' || !browser.tabs) return; - lazyLoading = state; - try { - browser.tabs.onActivated.removeListener(onLazyTabActivated); - if (state) { - browser.tabs.onActivated.addListener(onLazyTabActivated); - } - } catch (err) { - console.warn('browser.tabs.onActivated listener update failed', err); - } }; var initConfig = function initConfig() { @@ -118,7 +93,7 @@ export const SessionManagement = (function SessionManagement() { switch (mode) { case 'newTab': { - createTabWithContainer(options, cookieStoreId, checkFavicon); + createTab(options, { container: cookieStoreId }, checkFavicon); break; } @@ -146,55 +121,6 @@ export const SessionManagement = (function SessionManagement() { } }; - var createLazyTab = function createLazyTab(bookmark) { - if (typeof browser === 'undefined' || !browser.runtime) return; - - let title = browser.runtime.getURL('public/lazy/lazy.html') + - '?state=redirect' + - '&title=' + encodeURIComponent(bookmark.title) + - '&url=' + encodeURIComponent(bookmark.url); - - if (bookmark.favIconUrl) { - title += '&favIconUrl=' + encodeURIComponent(bookmark.favIconUrl); - } - - var tabOptions = { - url: title, - active: false, - pinned: bookmark.pinned, - windowId: bookmark.windowID, - }; - - createTabWithContainer(tabOptions, bookmark.cookieStoreId, - function success() { }, - function error() { - browser.tabs.create(tabOptions) - .then(function (tab) { - browser.tabs.update(tab.id, { - title: bookmark.title, - }); - }); - } - ); - }; - - var openLazyTab = function openLazyTab(bookmark) { - // Extract container info encoded in the title - var parsed = parseContainerFromTitle(bookmark.title); - bookmark.title = parsed.title; - bookmark.cookieStoreId = parsed.cookieStoreId; - - if (bookmark.favIconUrl) { - createLazyTab(bookmark); - } - else { - FaviconService.getFaviconUrl(bookmark.url, function (favIconUrl) { - bookmark.favIconUrl = favIconUrl; - createLazyTab(bookmark); - }); - } - }; - var checkReverseOrder = function checkReverseOrder(bookmarks) { if (reverseOrder === true) { bookmarks = bookmarks.reverse(); @@ -235,18 +161,18 @@ export const SessionManagement = (function SessionManagement() { else { checkReverseOrder(bookmarks); bookmarks.forEach(function (bookmark) { - - if (lazyLoading) { - openLazyTab(bookmark); - } - else { - var parsed = parseContainerFromTitle(bookmark.title); - createTabWithContainer( - { url: bookmark.url, active: false }, - parsed.cookieStoreId, - function (tabInfo) { FaviconService.checkTabFaviconSaved(tabInfo); } - ); - } + const parsed = parseContainerFromTitle(bookmark.title); + createTab( + { + url: bookmark.url, + active: false, + discard: lazyLoading, + }, + { + container: parsed.cookieStoreId + }, + function (tabInfo) { FaviconService.checkTabFaviconSaved(tabInfo); } + ); }); } }, @@ -268,18 +194,19 @@ export const SessionManagement = (function SessionManagement() { checkReverseOrder(bookmarks); bookmarks.forEach(function (bookmark) { - if (lazyLoading) { - bookmark.windowID = windowID; - openLazyTab(bookmark); - } - else { - var parsed = parseContainerFromTitle(bookmark.title); - createTabWithContainer( - { url: bookmark.url, pinned: bookmark.pinned, windowId: windowID }, - parsed.cookieStoreId, - function (tabInfo) { FaviconService.checkTabFaviconSaved(tabInfo); } - ); - } + const parsed = parseContainerFromTitle(bookmark.title); + createTab( + { + url: bookmark.url, + pinned: bookmark.pinned, + windowId: windowID, + }, + { + discard: lazyLoading, + container: parsed.cookieStoreId + }, + function (tabInfo) { FaviconService.checkTabFaviconSaved(tabInfo); } + ); }); }); } @@ -296,57 +223,9 @@ export const SessionManagement = (function SessionManagement() { }); }; - var restoreLazyTab = function restoreLazyTab(tab, url) { - if (typeof browser === 'undefined' || !browser.tabs) return; - - let redirectInfo = getLazyLoadingParameters(url ? url : tab.url); - if (redirectInfo.state == 'redirect') { - browser.tabs.update(tab.id, { - url: redirectInfo.url - }) - .then(function (tabInfo) { - tabInfo.url = redirectInfo.url; - FaviconService.checkTabFaviconSaved(tabInfo); - }); - } - }; - // ************************************************************************ // Events - var onLazyTabActivated = function onLazyTabActivated(tab) { - if (lazyLoading && typeof browser !== 'undefined' && browser.tabs) { - browser.tabs.get(tab.tabId).then(function (activeTab) { - if (activeTab.url.startsWith('moz-extension')) { - restoreLazyTab(activeTab); - } - else if (activeTab.status == 'loading') { - restoreLazyTab(activeTab, activeTab.title); - } - }); - } - }; - - var fixLazySession = function fixLazySession(folderID) { - if (typeof browser === 'undefined' || !browser.bookmarks) return; - - var result = browser.bookmarks.getChildren(folderID); - result.then(function (bookmarks) { - bookmarks.forEach(function (bookmark) { - if (bookmark.url.startsWith('moz-extension')) { - let redirectInfo = getLazyLoadingParameters(bookmark.url); - if (redirectInfo.url != undefined) { - browser.bookmarks.update(bookmark.id, { - url: redirectInfo.url - }); - } - } - }); - }, function fail() { - console.log('Error getting bookmarks from folder ID: ', folderID); - }); - }; - function init() { if (typeof browser === 'undefined') return; @@ -369,11 +248,6 @@ export const SessionManagement = (function SessionManagement() { case 'open-tab': openTab(message.options); break; - - case 'fix-lazy-session': - fixLazySession(message.bookmarkID); - break; - } }); } @@ -402,8 +276,7 @@ export const SessionManagement = (function SessionManagement() { var killTabID = windowsTabsToKill[tab.windowId]; if (killTabID != undefined && killTabID != tab.id) { browser.tabs.remove(killTabID); - windowsTabsToKill[tab.windowId] = undefined; - restoreLazyTab(tab, tab.title); + delete windowsTabsToKill[tab.windowId]; } }); } diff --git a/src/scripts/components/session-header-bar.js b/src/scripts/components/session-header-bar.js index 8736aab..b4f9be8 100644 --- a/src/scripts/components/session-header-bar.js +++ b/src/scripts/components/session-header-bar.js @@ -291,39 +291,6 @@ var CreateHeaderMenu = function CreateHeaderMenu() initDevMode(); } - var Troubleshooting = (function Troubleshooting() - { - var isInit = false; - - var fixLazySession = MenuButton({ - title: 'Fix session', - icon: 'icons/tools.png', - callback: function() { - if (typeof document !== 'undefined') { - WindowEvents.emit(document, 'MenuFixLazySession'); - } - } - }); - - function init() - { - if (isInit == true) - { - return; - } - - isInit = true; - var devRow = DomElem('div', {class: 'menu-row'}); - devRow.appendChild(fixLazySession); - menuArea.appendChild(devRow); - } - - return { - init: init - }; - - })(); - // ------------------------------------------------------------------------ // Events @@ -362,12 +329,6 @@ var CreateHeaderMenu = function CreateHeaderMenu() }); } - AppConfig.onChange('troubleshooting', function(value) { - if (value == true) { - Troubleshooting.init(); - } - }); - // ------------------------------------------------------------------------ // Public diff --git a/src/scripts/session/bookmarks.js b/src/scripts/session/bookmarks.js index f608bb5..547920c 100644 --- a/src/scripts/session/bookmarks.js +++ b/src/scripts/session/bookmarks.js @@ -10,19 +10,7 @@ import { AppConfig } from '../config'; export const BookmarkManager = (function BookmarkManager() { - var lazyLoadingUrl = browser.runtime.getURL('public/lazy/lazy.html'); - - function getLazyLoadingParameters(url) { - let paramater = {}; - let splits = url.split('?'); - if (splits.length < 2) return paramater; - let query = splits[1]; - let params = query.split('&'); - for (let p of params) { - paramater[p.split('=')[0]] = decodeURIComponent(p.split('=')[1]); - } - return paramater; - } + const containerTag = 'container'; var getHostName = function getHostName(url) { try { @@ -67,13 +55,7 @@ export const BookmarkManager = (function BookmarkManager() { var createBookmarkFromTab = function createBookmarkFromTab(tab, parentId) { var url = tab.url; - if (url.startsWith(lazyLoadingUrl, 0)) { - var info = getLazyLoadingParameters(url); - url = info.url; - } - else { - browser.runtime.sendMessage({ event: 'save-favicon', tab: tab }); - } + browser.runtime.sendMessage({ event: 'save-favicon', tab: tab }); var title = tab.title; if (tab.cookieStoreId && tab.cookieStoreId !== containerSuffix.default) { diff --git a/src/scripts/session/management.js b/src/scripts/session/management.js index 9c9f4a7..5884337 100644 --- a/src/scripts/session/management.js +++ b/src/scripts/session/management.js @@ -404,7 +404,6 @@ export const SessionManager = (function SessionManager() { replaceSession: replaceSession, restoreAdvanced: restoreAdvanced, replaceSessionAll: replaceSessionAll, - fixLazySession: fixLazySession, restoreNewWindow: restoreNewWindow, loadBookmarksNewWindow: loadBookmarksNewWindow, mergeSessions: mergeSessions, From 37c2d8d6d86f6ea757c91734ee2245cdd78a4fc2 Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Wed, 8 Apr 2026 02:15:00 +0300 Subject: [PATCH 19/32] Refactoring --- .env | 5 +- assets/css/session-toolbar.css | 12 +- public/home/home.html | 2 +- {assets => public}/images/icons/x.png | Bin src/scripts/components/config-panel.js | 10 +- src/scripts/components/session-container.js | 127 ++-- src/scripts/components/session-header-bar.js | 665 +++++++++---------- src/scripts/components/session-toolbar.js | 13 +- src/scripts/config.js | 1 - src/scripts/session/bookmarks.js | 4 +- src/scripts/session/management.js | 231 +++---- 11 files changed, 493 insertions(+), 577 deletions(-) rename {assets => public}/images/icons/x.png (100%) diff --git a/.env b/.env index f047c1e..20ffee2 100644 --- a/.env +++ b/.env @@ -1,3 +1,2 @@ -FIREFOX_BINARY="D:\Program Files\Firefox Developer Edition\firefox.exe" -FIREFOX_PROFILE="D:\Programs\Firefox\Addon" -WXT_DEV_SERVER_HOST=127.0.0.1 \ No newline at end of file +FIREFOX_BINARY="D:\Program Files\Firefox Developer Edition\firefox.exe" +FIREFOX_PROFILE="D:\Programs\Firefox\Addon" diff --git a/assets/css/session-toolbar.css b/assets/css/session-toolbar.css index 2940122..dcee7d2 100644 --- a/assets/css/session-toolbar.css +++ b/assets/css/session-toolbar.css @@ -101,6 +101,10 @@ flex: 1; } +#session-sync .session-toolbar[session-type="separator"] .menu-bar { + display: none; +} + #session-sync .session-toolbar .menu-bar .button { width: 2.6em; height: 100%; @@ -111,8 +115,8 @@ display: none; float: left; - opacity: 0.75; - filter: grayscale(100%); + /* opacity: 0.75; + filter: grayscale(100%); */ } #session-sync .session-toolbar .menu-bar .button:hover { @@ -174,7 +178,7 @@ * Menu state */ - #session-sync[session="current"] .session-toolbar .menu-bar .button[state-current="true"] { +#session-sync[session="current"] .session-toolbar .menu-bar .button[state-current="true"] { display: block; } @@ -261,4 +265,4 @@ #session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch[windows] .switch { background-image: url("images/icons/stack.png"); -} +} \ No newline at end of file diff --git a/public/home/home.html b/public/home/home.html index 5f93560..4a10aa7 100644 --- a/public/home/home.html +++ b/public/home/home.html @@ -130,7 +130,7 @@ href="https://www.mozilla.org/en-US/firefox/accounts/" target="_blank">Firefox Account to sync your bookmarks.
Currenly you can only use Firefox Bookmark Manager (CTRL + SHIFT - + B) to export/import all your bookmarks (which includes all Session Sync saved data). + + O) to export/import all your bookmarks (which includes all Session Sync saved data).
Session Sync detects automatically all updated bookmarks after an import.
diff --git a/assets/images/icons/x.png b/public/images/icons/x.png similarity index 100% rename from assets/images/icons/x.png rename to public/images/icons/x.png diff --git a/src/scripts/components/config-panel.js b/src/scripts/components/config-panel.js index 51cf836..8c143f3 100644 --- a/src/scripts/components/config-panel.js +++ b/src/scripts/components/config-panel.js @@ -157,7 +157,7 @@ export function ConfigPanel() { type: 'switch', }); - })(); + }); // ------------------------ // Section: General settings @@ -222,14 +222,6 @@ export function ConfigPanel() { key: 'services.favicon.enabled', type: 'switch', }); - - ToggleOptionConfig({ - parent: section, - name: 'Troubleshooting', - key: 'troubleshooting', - type: 'switch', - }); - })(); // ------------------------ diff --git a/src/scripts/components/session-container.js b/src/scripts/components/session-container.js index 1055fd2..e9be80b 100644 --- a/src/scripts/components/session-container.js +++ b/src/scripts/components/session-container.js @@ -26,33 +26,29 @@ import { SessionHistory } from './session-history'; * Panel for listing session information (Firefox Bookmark Folder) and children bookmarks */ -export class SessionContainer -{ - constructor() - { +export class SessionContainer { + constructor() { var DomElem = HTMLCreator(); // ------------------------------------------------------------------------ // Init UI - var container = DomElem('div', {class: 'session-container'}); + var container = DomElem('div', { class: 'session-container' }); container.style.fontSize = AppConfig.get('style.scale.bookmarks') + 'px'; - var bookmarks = DomElem('div', {class: 'bookmarks'}); + var bookmarks = DomElem('div', { class: 'bookmarks' }); container.appendChild(bookmarks); // ------------------------------------------------------------------------ // User events - function isValidSessionID(ID) - { + function isValidSessionID(ID) { return (SessionSyncModel.bookmarks[ID] !== undefined && - SessionSyncModel.bookmarks[ID].type === 'folder' && - SessionSyncModel.bookmarks[ID].parentId == AppConfig.get('storageID')); + SessionSyncModel.bookmarks[ID].type === 'folder' && + SessionSyncModel.bookmarks[ID].parentId == AppConfig.get('storageID')); } - function restoreHistoryWindow(sessionInfo) - { + function restoreHistoryWindow(sessionInfo) { var list = []; sessionInfo.windows.forEach(historyWindow => { list.push(historyWindow.tabs); @@ -71,8 +67,7 @@ export class SessionContainer WindowEvents.on(document, 'HistorySessionRestoreWindow', (windowID) => { if (this.SyncModel.state.session === 'history') { - if (this.selectedHistorySession.active == true) - { + if (this.selectedHistorySession.active == true) { var activeSessionWindows = Object.values(this.selectedHistorySession.windows); if (activeSessionWindows[windowID].active) { SessionManager.activateWindow(activeSessionWindows[windowID].id); @@ -153,8 +148,7 @@ export class SessionContainer return; } - switch(this.SyncModel.state.session) - { + switch (this.SyncModel.state.session) { case 'current': SessionManager.saveActiveSession(); break; @@ -166,12 +160,6 @@ export class SessionContainer }); - WindowEvents.on(document, 'MenuFixLazySession', () => { - if (isValidSessionID(this.activeSessionID)) { - SessionManager.fixLazySession(this.activeSessionID); - } - }); - WindowEvents.on(document, 'EditSelectedSession', () => { WindowEvents.emit(document, 'SessionContextMenu-EditSession', this.activeSessionID); }); @@ -183,8 +171,7 @@ export class SessionContainer WindowEvents.on(document, 'ViewSession', this.previewSession.bind(this)); WindowEvents.on(document, 'SessionDescriptionChanged', (sessionID) => { - if (this.activeSessionID == sessionID) - { + if (this.activeSessionID == sessionID) { this.updateSessionInfo(); } }); @@ -196,7 +183,7 @@ export class SessionContainer } }); - AppConfig.onChange('style.scale.bookmarks', function(size) { + AppConfig.onChange('style.scale.bookmarks', function (size) { container.style.fontSize = size + 'px'; }); @@ -211,10 +198,8 @@ export class SessionContainer SessionBookmarkEvents(bookmarks); var updateTimeout = null; - function updateOnTabEvent() - { - if (SessionManager.tabTracking()) - { + function updateOnTabEvent() { + if (SessionManager.tabTracking()) { if (updateTimeout == null) { updateTimeout = setTimeout(function () { updateTimeout = null; @@ -241,10 +226,9 @@ export class SessionContainer this.SyncModel = SessionSyncModel.getModel(typeof document !== 'undefined' ? document : null); } - setUIState(state, headerTitle, sessionID) - { + setUIState(state, headerTitle, sessionID) { if (typeof document !== 'undefined') { - WindowEvents.emit(document, 'SetUIState', {'session': state}); + WindowEvents.emit(document, 'SetUIState', { 'session': state }); WindowEvents.emit(document, 'SetSessionDescription', headerTitle); } @@ -253,25 +237,22 @@ export class SessionContainer this.selectedHistorySession = undefined; } - updateSessionInfo() - { + updateSessionInfo() { var session = SessionSyncModel.bookmarks[this.activeSessionID]; if (typeof document !== 'undefined') { WindowEvents.emit(document, 'SetSessionDescription', session.title); - WindowEvents.emit(document, 'SetSessionDate', session.dateAdded); + WindowEvents.emit(document, 'SetSessionInfo', session); } } - showCurrentSession() - { + showCurrentSession() { var DOMBookmarks = this.DOMBookmarks; this.setUIState('current', 'Active session', 0); SessionManager.getCurrentWindow((mozWindow) => { // Get non-private windows - if (mozWindow.incognito == true || AppConfig.get('session.save').allWindows == false) - { + if (mozWindow.incognito == true || AppConfig.get('session.save').allWindows == false) { mozWindow.tabs.forEach((tab) => { let sessionTab = new SessionTab(tab, 0, -1); this.SyncModel.tabs[sessionTab.tab.id] = sessionTab; @@ -280,17 +261,14 @@ export class SessionContainer this.restoreScrollTop('current'); } - else - { + else { // Get non-private windows var tabIndex = 0; var windowIndex = 0; // display index in UI - ignores private windows SessionManager.getAllWindows((windows) => { - for (let mozWindow of windows) - { - if (!mozWindow.incognito) - { + for (let mozWindow of windows) { + if (!mozWindow.incognito) { // Add window separator var sessionWindow = new SessionWindow(mozWindow.id, windowIndex, tabIndex); DOMBookmarks.appendChild(sessionWindow.DOMRoot); @@ -299,8 +277,7 @@ export class SessionContainer var globalOffset = tabIndex; // Add window tabs - for (let i in mozWindow.tabs) - { + for (let i in mozWindow.tabs) { let tab = mozWindow.tabs[i]; let sessionTab = new SessionTab(tab, globalOffset); this.SyncModel.tabs[sessionTab.tab.id] = sessionTab; @@ -318,8 +295,7 @@ export class SessionContainer }); } - showHistorySession(sessionInfo) - { + showHistorySession(sessionInfo) { var newSession = (this.selectedHistorySession != undefined) && (this.selectedHistorySession != sessionInfo); this.setUIState('history', (new Date(sessionInfo.lastSave)).toLocaleString(), -1); @@ -328,8 +304,7 @@ export class SessionContainer var tabIndex = 0; var windowIndex = 0; - for (let key in sessionInfo.windows) - { + for (let key in sessionInfo.windows) { // Add window separator var sessionWindow = new HistoryWindow(sessionInfo.windows[key], windowIndex, tabIndex); this.DOMBookmarks.appendChild(sessionWindow.DOMRoot); @@ -339,8 +314,7 @@ export class SessionContainer // Add window tabs var windowTabs = sessionInfo.windows[key].tabs; - for (var i in windowTabs) - { + for (var i in windowTabs) { var tab = windowTabs[i]; tab.index = i | 0; var sessionTab = new HistoryTab(tab, globalOffset); @@ -354,20 +328,18 @@ export class SessionContainer this.restoreScrollTop('history', newSession ? 0 : undefined); } - previewSession(sessionID) - { + previewSession(sessionID) { var session = SessionSyncModel.bookmarks[sessionID]; this.setUIState('restore', '', sessionID); BookmarkManager.getFolderBookmarks(session.id, (marks) => { if (typeof document !== 'undefined') { - WindowEvents.emit(document, 'SetPromiseSession', { sessionID: session.id, update: false } ); + WindowEvents.emit(document, 'SetPromiseSession', { sessionID: session.id, update: false }); } var len = marks.length; - for (var i = 0; i < len; i++) - { + for (var i = 0; i < len; i++) { var bookmark = new SessionBookmark(marks[i]); bookmark.setVirtualPosition(marks[i].index); @@ -382,24 +354,20 @@ export class SessionContainer }); } - restoreScrollTop(state, forcedValue) - { + restoreScrollTop(state, forcedValue) { var scrollTop = AppConfig.get('state.scrollTop.' + state); this.DOMBookmarks.scrollTop = (forcedValue !== undefined) ? forcedValue : scrollTop; } - bookmarkCurrentTab() - { + bookmarkCurrentTab() { // Test if already saved in this session SessionManager.getCurrentTab((activeTab) => { var sessionID = this.activeSessionID; var bookmarks = SessionSyncModel.bookmarks; - for (var i in bookmarks) - { - if (bookmarks[i].parentId == sessionID && bookmarks[i].url == activeTab.url) - { + for (var i in bookmarks) { + if (bookmarks[i].parentId == sessionID && bookmarks[i].url == activeTab.url) { if (typeof document !== 'undefined') { WindowEvents.emit(document, 'Notification', { message: 'Already Saved', @@ -415,34 +383,31 @@ export class SessionContainer // Add bookmark into the session BookmarkManager.createBookmarkFromTab(activeTab, sessionID) - .then((mark) => { - var bookmark = new SessionBookmark(mark); + .then((mark) => { + var bookmark = new SessionBookmark(mark); - SessionSyncModel.bookmarks[mark.id] = mark; - this.SyncModel.setBookmark(mark.id, bookmark); - this.DOMBookmarks.appendChild(bookmark.DOMRoot); + SessionSyncModel.bookmarks[mark.id] = mark; + this.SyncModel.setBookmark(mark.id, bookmark); + this.DOMBookmarks.appendChild(bookmark.DOMRoot); - bookmark.setVirtualPosition(mark.index); - bookmark.highlight(); + bookmark.setVirtualPosition(mark.index); + bookmark.highlight(); - }); + }); }); } - saveHistorySession(sessionIndex) - { - if (sessionIndex == undefined) - { + saveHistorySession(sessionIndex) { + if (sessionIndex == undefined) { // When Save button from toolbar is pressed SessionManager.saveHistorySession(this.selectedHistorySession); } - else - { + else { // Save from context menu SessionHistory.getHistorySession(sessionIndex, function (sessionInfo) { SessionManager.saveHistorySession(sessionInfo); }); } } -} \ No newline at end of file +} diff --git a/src/scripts/components/session-header-bar.js b/src/scripts/components/session-header-bar.js index b4f9be8..8479f2b 100644 --- a/src/scripts/components/session-header-bar.js +++ b/src/scripts/components/session-header-bar.js @@ -1,336 +1,329 @@ -'use strict'; - -// ------------------------------------------------------------------------ -// Modules - -import { AppConfig } from '../config'; -import { SessionSyncModel } from './session-sync-model'; - -// Utils -import * as JSUtils from '../utils/general'; -import { HTMLCreator } from '../utils/dom'; -import { WindowEvents } from '../utils/global-events'; - -// ------------------------------------------------------------------------ -// API - -export function SessionHeaderBar() -{ - var DomElem = HTMLCreator(); - - // ------------------------------------------------------------------------ - // Create UI - - var headerBar = DomElem('div', {class: 'header-bar'}); - headerBar.style.fontSize = AppConfig.get('style.scale.header') + 'px'; - - // Display information about the selected session - var description = DomElem('div', {class: 'description'}); - headerBar.appendChild(description); - - // Active session button - var activeSessionBtn = DomElem('div', {class: 'active-session-btn', tooltip: 'Active session'}); - headerBar.appendChild(activeSessionBtn); - - // Header Menu - var headerMenu = CreateHeaderMenu(); - headerBar.appendChild(headerMenu); - - // ------------------------------------------------------------------------ - // Events - - AppConfig.onChange('style.scale.header', function(value) { - headerBar.style.fontSize = value + 'px'; - }); - - activeSessionBtn.addEventListener('click', function() { - if (typeof document !== 'undefined') { - WindowEvents.emit(document, 'ShowCurrentSession'); - } - }); - - if (typeof document !== 'undefined') { - WindowEvents.on(document, 'SetSessionDescription', function(value) { - description.textContent = value; - }); - } - - // Tooltip events - headerBar.addEventListener('mouseover', function(e) { - if (e.target.hasAttribute('tooltip')) { - if (typeof document !== 'undefined') { - WindowEvents.emit(document, 'ShowTooltip', { - node: e.target, - message: e.target.getAttribute('tooltip') - }); - } - } else { - if (typeof document !== 'undefined') { - WindowEvents.emit(document, 'HideTooltip'); - } - } - }); - - headerBar.addEventListener('mouseleave', function() { - if (typeof document !== 'undefined') { - WindowEvents.emit(document, 'HideTooltip'); - } - }); - - // ------------------------------------------------------------------------ - // Public properties - - this.DOMRoot = headerBar; -} - -var CreateHeaderMenu = function CreateHeaderMenu() -{ - var DomElem = HTMLCreator(); - - // ------------------------------------------------------------------------ - // Create UI - - var headerMenu = DomElem('div', {class: 'header-menu'}); - headerMenu.setAttribute('active', 'false'); - - var menuArea = DomElem('div', {class: 'menu-area'}); - headerMenu.appendChild(menuArea); - - var menuRow1 = DomElem('div', {class: 'menu-row'}); - var menuRow2 = DomElem('div', {class: 'menu-row'}); - var menuRow3 = DomElem('div', {class: 'menu-row'}); - menuArea.appendChild(menuRow1); - menuArea.appendChild(menuRow2); - menuArea.appendChild(menuRow3); - - // ------------------------------------------------------------------------ - // Methods - - function MenuButton(options) { - - var button = DomElem('div', {class: 'menu-button'}); - - var icon = DomElem('div', {class: 'icon'}); - var title = DomElem('div', {class: 'title'}); - - title.textContent = options.title; - - var url = 'url("images/' + options.icon + '")'; - icon.style.backgroundImage = url; - - // Events - var callback = (typeof JSUtils.getValidFunction === 'function') ? JSUtils.getValidFunction(options.callback) : options.callback; - button.addEventListener('click', callback); - - button.appendChild(icon); - button.appendChild(title); - return button; - } - - var openConfig = MenuButton({ - title: 'Options', - icon: 'icons/gear.png', - callback: function () { - if (typeof document !== 'undefined') { - var model = SessionSyncModel.getModel(document); - var state = model.state['config'] == undefined ? 'on' : undefined; - WindowEvents.emit(document, 'SetUIState', { config : state}); - } - } - }); - - var openTutorial = MenuButton({ - title: 'Tutorial', - icon: 'icons/brightness.png', - callback: function () { - if (typeof browser !== 'undefined' && browser.runtime) { - browser.runtime.sendMessage({event: 'session_sync_tutorial'}); - } - } - }); - - var leaveFeedback = MenuButton({ - title: 'Feedback', - icon: 'icons/mail.png', - callback: function () { - if (typeof browser !== 'undefined' && browser.runtime) { - browser.runtime.sendMessage({event: 'session_sync_leave_feedback'}); - } - } - }); - - var exportImport = MenuButton({ - title: 'Export/Import', - icon: 'icons/booklet.png', - callback: function () { - if (typeof browser !== 'undefined' && browser.tabs) { - browser.tabs.create({ - url: 'home/home.html#export-import', - active: true, - }); - } - } - }); - - var buttonDetachInTab = MenuButton({ - title: 'Tab View', - icon: 'icons/document.png', - callback: function () { - if (typeof browser !== 'undefined' && browser.runtime) { - browser.runtime.sendMessage({event: 'session_sync_detach_tab'}); - } - } - }); - - var buttonDetachInWindow = MenuButton({ - title: 'Window View', - icon: 'icons/browser.png', - callback: function () { - if (typeof browser !== 'undefined' && browser.runtime) { - browser.runtime.sendMessage({event: 'session_sync_detach_window'}); - } - } - }); - - var githubPage = MenuButton({ - title: 'Dev page', - icon: 'github-logo.png', - callback: function () { - if (typeof browser !== 'undefined' && browser.runtime) { - browser.runtime.sendMessage({event: 'session_sync_open_github'}); - } - } - }); - - menuRow1.appendChild(openConfig); - menuRow2.appendChild(openTutorial); - menuRow2.appendChild(leaveFeedback); - menuRow2.appendChild(githubPage); - menuRow3.appendChild(exportImport); - menuRow3.appendChild(buttonDetachInTab); - menuRow3.appendChild(buttonDetachInWindow); - - // ------------------------------------------------------------------------ - // Dev actions - - var DebugMode = { - init: AppConfig.devMode(), - manualTrigger: 0, - triggerDate: new Date(), - action : function () { - if (this.init == false) - { - if (new Date() - this.triggerDate < 5000) { - this.manualTrigger++; - if (this.manualTrigger > 10) { - this.init = true; - initDevMode(); - } - } - else { - this.manualTrigger = 0; - } - } - } - }; - - function initDevMode() - { - var devRow1 = DomElem('div', {class: 'menu-row'}); - - var activeTabInfo = MenuButton({ - title: 'Tab Info', - icon: 'icons/document.png', - callback: function () { - if (typeof browser !== 'undefined' && browser.tabs) { - browser.tabs.query({active: true, windowId: browser.windows.WINDOW_ID_CURRENT}) - .then(tabs => browser.tabs.get(tabs[0].id)) - .then(tab => { - console.log(tab); - }); - } - } - }); - - var localStorage = MenuButton({ - title: 'Local Storage', - icon: 'icons/bookshelf.png', - callback: function () { - if (typeof browser !== 'undefined' && browser.storage) { - browser.storage.local.get().then(function (data) { - console.log(data); - }); - } - } - }); - - var clearFaviconCache = MenuButton({ - title: 'Clear Favicons', - icon: 'icons/x.png', - callback: function () { - if (typeof browser !== 'undefined' && browser.storage) { - browser.storage.local.get().then(function (data) { - for (var key in data) { - if (key.startsWith('@favIconUrl')) { - browser.storage.local.remove(key); - } - } - }); - } - } - }); - - devRow1.appendChild(activeTabInfo); - devRow1.appendChild(localStorage); - devRow1.appendChild(clearFaviconCache); - menuArea.appendChild(devRow1); - } - - if (AppConfig.devMode()) - { - initDevMode(); - } - - // ------------------------------------------------------------------------ - // Events - - var closeMenu = function closeMenu() { - headerMenu.setAttribute('active', '0'); - }; - - var closeOnMiss = function closeOnMiss(e) - { - if (e && (e.target == headerMenu || e.target == menuArea)) { - return; - } - closeMenu(); - if (typeof document !== 'undefined') { - document.removeEventListener('click', closeOnMiss); - } - }; - - headerMenu.addEventListener('click', function(e) { - if (e && e.target == headerMenu) - { - var state = headerMenu.getAttribute('active') | 0; - var newState = 1 - state; - headerMenu.setAttribute('active', newState); - if (newState == 1 && typeof document !== 'undefined') { - document.addEventListener('click', closeOnMiss); - } - - DebugMode.action(); - } - }); - - if (typeof document !== 'undefined') { - WindowEvents.on(document, 'CloseUI', function() { - closeMenu(); - }); - } - - // ------------------------------------------------------------------------ - // Public - - return headerMenu; -}; +'use strict'; + +// ------------------------------------------------------------------------ +// Modules + +import { AppConfig } from '../config'; +import { SessionSyncModel } from './session-sync-model'; + +// Utils +import * as JSUtils from '../utils/general'; +import { HTMLCreator } from '../utils/dom'; +import { WindowEvents } from '../utils/global-events'; + +// ------------------------------------------------------------------------ +// API + +export function SessionHeaderBar() { + var DomElem = HTMLCreator(); + + // ------------------------------------------------------------------------ + // Create UI + + var headerBar = DomElem('div', { class: 'header-bar' }); + headerBar.style.fontSize = AppConfig.get('style.scale.header') + 'px'; + + // Display information about the selected session + var description = DomElem('div', { class: 'description' }); + headerBar.appendChild(description); + + // Active session button + var activeSessionBtn = DomElem('div', { class: 'active-session-btn', tooltip: 'Active session' }); + headerBar.appendChild(activeSessionBtn); + + // Header Menu + var headerMenu = CreateHeaderMenu(); + headerBar.appendChild(headerMenu); + + // ------------------------------------------------------------------------ + // Events + + AppConfig.onChange('style.scale.header', function (value) { + headerBar.style.fontSize = value + 'px'; + }); + + activeSessionBtn.addEventListener('click', function () { + if (typeof document !== 'undefined') { + WindowEvents.emit(document, 'ShowCurrentSession'); + } + }); + + if (typeof document !== 'undefined') { + WindowEvents.on(document, 'SetSessionDescription', function (value) { + description.textContent = value; + }); + } + + // Tooltip events + headerBar.addEventListener('mouseover', function (e) { + if (e.target.hasAttribute('tooltip')) { + if (typeof document !== 'undefined') { + WindowEvents.emit(document, 'ShowTooltip', { + node: e.target, + message: e.target.getAttribute('tooltip') + }); + } + } else { + if (typeof document !== 'undefined') { + WindowEvents.emit(document, 'HideTooltip'); + } + } + }); + + headerBar.addEventListener('mouseleave', function () { + if (typeof document !== 'undefined') { + WindowEvents.emit(document, 'HideTooltip'); + } + }); + + // ------------------------------------------------------------------------ + // Public properties + + this.DOMRoot = headerBar; +} + +var CreateHeaderMenu = function CreateHeaderMenu() { + var DomElem = HTMLCreator(); + + // ------------------------------------------------------------------------ + // Create UI + + var headerMenu = DomElem('div', { class: 'header-menu' }); + headerMenu.setAttribute('active', 'false'); + + var menuArea = DomElem('div', { class: 'menu-area' }); + headerMenu.appendChild(menuArea); + + var menuRow1 = DomElem('div', { class: 'menu-row' }); + var menuRow2 = DomElem('div', { class: 'menu-row' }); + var menuRow3 = DomElem('div', { class: 'menu-row' }); + menuArea.appendChild(menuRow1); + menuArea.appendChild(menuRow2); + menuArea.appendChild(menuRow3); + + // ------------------------------------------------------------------------ + // Methods + + function MenuButton(options) { + + var button = DomElem('div', { class: 'menu-button' }); + + var icon = DomElem('div', { class: 'icon' }); + var title = DomElem('div', { class: 'title' }); + + title.textContent = options.title; + + var url = 'url("images/' + options.icon + '")'; + icon.style.backgroundImage = url; + + // Events + var callback = (typeof JSUtils.getValidFunction === 'function') ? JSUtils.getValidFunction(options.callback) : options.callback; + button.addEventListener('click', callback); + + button.appendChild(icon); + button.appendChild(title); + return button; + } + + var openConfig = MenuButton({ + title: 'Options', + icon: 'icons/gear.png', + callback: function () { + if (typeof document !== 'undefined') { + var model = SessionSyncModel.getModel(document); + var state = model.state['config'] == undefined ? 'on' : undefined; + WindowEvents.emit(document, 'SetUIState', { config: state }); + } + } + }); + + var openTutorial = MenuButton({ + title: 'Tutorial', + icon: 'icons/brightness.png', + callback: function () { + if (typeof browser !== 'undefined' && browser.runtime) { + browser.runtime.sendMessage({ event: 'session_sync_tutorial' }); + } + } + }); + + var leaveFeedback = MenuButton({ + title: 'Feedback', + icon: 'icons/mail.png', + callback: function () { + if (typeof browser !== 'undefined' && browser.runtime) { + browser.runtime.sendMessage({ event: 'session_sync_leave_feedback' }); + } + } + }); + + var exportImport = MenuButton({ + title: 'Export/Import', + icon: 'icons/booklet.png', + callback: function () { + if (typeof browser !== 'undefined' && browser.tabs) { + browser.tabs.create({ + url: 'home/home.html#export-import', + active: true, + }); + } + } + }); + + var buttonDetachInTab = MenuButton({ + title: 'Tab View', + icon: 'icons/document.png', + callback: function () { + if (typeof browser !== 'undefined' && browser.runtime) { + browser.runtime.sendMessage({ event: 'session_sync_detach_tab' }); + } + } + }); + + var buttonDetachInWindow = MenuButton({ + title: 'Window View', + icon: 'icons/browser.png', + callback: function () { + if (typeof browser !== 'undefined' && browser.runtime) { + browser.runtime.sendMessage({ event: 'session_sync_detach_window' }); + } + } + }); + + var githubPage = MenuButton({ + title: 'Dev page', + icon: 'github-logo.png', + callback: function () { + if (typeof browser !== 'undefined' && browser.runtime) { + browser.runtime.sendMessage({ event: 'session_sync_open_github' }); + } + } + }); + + menuRow1.appendChild(openConfig); + menuRow2.appendChild(openTutorial); + menuRow2.appendChild(leaveFeedback); + menuRow2.appendChild(githubPage); + menuRow3.appendChild(exportImport); + menuRow3.appendChild(buttonDetachInTab); + menuRow3.appendChild(buttonDetachInWindow); + + // ------------------------------------------------------------------------ + // Dev actions + + var DebugMode = { + init: AppConfig.devMode(), + manualTrigger: 0, + triggerDate: new Date(), + action: function () { + if (this.init == false) { + if (new Date() - this.triggerDate < 5000) { + this.manualTrigger++; + if (this.manualTrigger > 10) { + this.init = true; + initDevMode(); + } + } + else { + this.manualTrigger = 0; + } + } + } + }; + + function initDevMode() { + var devRow1 = DomElem('div', { class: 'menu-row' }); + + var activeTabInfo = MenuButton({ + title: 'Tab Info', + icon: 'icons/document.png', + callback: function () { + if (typeof browser !== 'undefined' && browser.tabs) { + browser.tabs.query({ active: true, windowId: browser.windows.WINDOW_ID_CURRENT }) + .then(tabs => browser.tabs.get(tabs[0].id)) + .then(tab => { + console.log(tab); + }); + } + } + }); + + var localStorage = MenuButton({ + title: 'Local Storage', + icon: 'icons/bookshelf.png', + callback: function () { + if (typeof browser !== 'undefined' && browser.storage) { + browser.storage.local.get().then(function (data) { + console.log(data); + }); + } + } + }); + + var clearFaviconCache = MenuButton({ + title: 'Clear Favicons', + icon: 'icons/x.png', + callback: function () { + if (typeof browser !== 'undefined' && browser.storage) { + browser.storage.local.get().then(function (data) { + for (var key in data) { + if (key.startsWith('@favIconUrl')) { + browser.storage.local.remove(key); + } + } + }); + } + } + }); + + devRow1.appendChild(activeTabInfo); + devRow1.appendChild(localStorage); + devRow1.appendChild(clearFaviconCache); + menuArea.appendChild(devRow1); + } + + if (AppConfig.devMode()) { + initDevMode(); + } + + // ------------------------------------------------------------------------ + // Events + + var closeMenu = function closeMenu() { + headerMenu.setAttribute('active', '0'); + }; + + var closeOnMiss = function closeOnMiss(e) { + if (e && (e.target == headerMenu || e.target == menuArea)) { + return; + } + closeMenu(); + if (typeof document !== 'undefined') { + document.removeEventListener('click', closeOnMiss); + } + }; + + headerMenu.addEventListener('click', function (e) { + if (e && e.target == headerMenu) { + var state = headerMenu.getAttribute('active') | 0; + var newState = 1 - state; + headerMenu.setAttribute('active', newState); + if (newState == 1 && typeof document !== 'undefined') { + document.addEventListener('click', closeOnMiss); + } + + DebugMode.action(); + } + }); + + if (typeof document !== 'undefined') { + WindowEvents.on(document, 'CloseUI', function () { + closeMenu(); + }); + } + + // ------------------------------------------------------------------------ + // Public + + return headerMenu; +}; diff --git a/src/scripts/components/session-toolbar.js b/src/scripts/components/session-toolbar.js index 4730d60..f8e54cd 100644 --- a/src/scripts/components/session-toolbar.js +++ b/src/scripts/components/session-toolbar.js @@ -8,7 +8,7 @@ import { AppConfig } from '../config'; // Utils import { HTMLCreator } from '../utils/dom'; -import { WindowEvents, GlobalEvents } from '../utils/global-events'; +import { WindowEvents } from '../utils/global-events'; import * as DOMComponent from '../utils/components'; // ***************************************************************************** @@ -60,8 +60,9 @@ export function SessionToolbar() { // Events if (typeof document !== 'undefined') { - WindowEvents.on(document, 'SetSessionDate', function (date) { - sessionDate.textContent = (new Date(date)).toLocaleString(); + WindowEvents.on(document, 'SetSessionInfo', function (sessionInfo) { + sessionDate.textContent = (new Date(sessionInfo.dateAdded)).toLocaleString(); + toolbar.setAttribute('session-type', sessionInfo.type); }); } @@ -234,8 +235,8 @@ SessionToolbar.prototype.createMenu = function createMenu() { menu.appendChild(restore); menu.appendChild(restoreW); - // menu.appendChild(replaceSession); - // menu.appendChild(replaceSessionAll); + menu.appendChild(replaceSession); + menu.appendChild(replaceSessionAll); menu.appendChild(separator1); menu.appendChild(mergeSession); menu.appendChild(overwriteSession); @@ -254,7 +255,7 @@ SessionToolbar.prototype.createMenu = function createMenu() { } }); - menu.appendChild(customizeToolbar); + // menu.appendChild(customizeToolbar); // ------------------------------------------------------------------------ // Events diff --git a/src/scripts/config.js b/src/scripts/config.js index 4b9f4f8..7329851 100644 --- a/src/scripts/config.js +++ b/src/scripts/config.js @@ -63,7 +63,6 @@ const Config = (function () { var setupConfig = function setupConfig() { browser.storage.local.get(null).then(function (obj) { - console.log(obj); for (let key in defaults) { if (obj.hasOwnProperty(key)) { localSettings[key] = obj[key]; diff --git a/src/scripts/session/bookmarks.js b/src/scripts/session/bookmarks.js index 547920c..33f9816 100644 --- a/src/scripts/session/bookmarks.js +++ b/src/scripts/session/bookmarks.js @@ -48,8 +48,8 @@ export const BookmarkManager = (function BookmarkManager() { }; var containerSuffix = { - encode: function (id) { return ' [container:' + id + ']'; }, - regex: /^(.*) \[container:([^\]]+)\]$/, + encode: function (id) { return ` [${containerTag}:${id}]`; }, + regex: new RegExp('^(.*) \\[' + containerTag + ':([^\\]]+)\\]$'), default: 'firefox-default', }; diff --git a/src/scripts/session/management.js b/src/scripts/session/management.js index 5884337..701cca9 100644 --- a/src/scripts/session/management.js +++ b/src/scripts/session/management.js @@ -15,25 +15,23 @@ export const SessionManager = (function SessionManager() { var trackTabs = true; - var getCurrentTab = function getCurrentTab(callback) - { - browser.tabs.query({active: true, windowId: browser.windows.WINDOW_ID_CURRENT}) - .then(function(tabs) { - callback(tabs[0]); - }); + var getCurrentTab = function getCurrentTab(callback) { + browser.tabs.query({ active: true, windowId: browser.windows.WINDOW_ID_CURRENT }) + .then(function (tabs) { + callback(tabs[0]); + }); }; - var getWindowTab = function getWindowTab(tabIndex, windowId, callback) - { - browser.tabs.query({index: tabIndex, windowId: windowId}) - .then(function(tabs) { - callback(tabs[0]); - }); + var getWindowTab = function getWindowTab(tabIndex, windowId, callback) { + browser.tabs.query({ index: tabIndex, windowId: windowId }) + .then(function (tabs) { + callback(tabs[0]); + }); }; var getCurrentWindow = function getCurrentWindow(callback) { browser.windows.getCurrent({ populate: true }) - .then(callback); + .then(callback); }; var getAllWindows = function getAllWindows(callback) { @@ -41,21 +39,19 @@ export const SessionManager = (function SessionManager() { populate: true, windowTypes: ['normal'] }) - .then(callback); + .then(callback); }; - var tabTracking = function tabTracking() - { + var tabTracking = function tabTracking() { return trackTabs; }; - var moveTab = function moveTab(tabID, newIndex, windowID) - { + var moveTab = function moveTab(tabID, newIndex, windowID) { trackTabs = false; browser.tabs.move([tabID], { index: newIndex, windowId: windowID - }).then(function() { + }).then(function () { GlobalEvents.emit('UpdateCurrentSession'); trackTabs = true; }, function onError() { @@ -63,34 +59,29 @@ export const SessionManager = (function SessionManager() { }); }; - var activateTab = function activateTab(tabID) - { + var activateTab = function activateTab(tabID) { browser.tabs.update(tabID, { active: true }); }; - var closeTab = function closeTab(tabID) - { + var closeTab = function closeTab(tabID) { browser.tabs.remove(tabID); }; - var activateWindow = function activateWindow(windowID) - { + var activateWindow = function activateWindow(windowID) { browser.windows.update(windowID, { focused: true }); }; - var saveActiveSession = function saveActiveSession() - { - getCurrentWindow(function(mozWindow) { + var saveActiveSession = function saveActiveSession() { + getCurrentWindow(function (mozWindow) { var sessionID = 0; var sessionCount = 0; - function updateOnSave() - { + function updateOnSave() { WindowEvents.emit(document, 'SetPromiseSession', { update: true, sessionID: sessionID, @@ -99,8 +90,7 @@ export const SessionManager = (function SessionManager() { } // Get non-private windows - if (mozWindow.incognito == true || AppConfig.get('session.save').allWindows == false) - { + if (mozWindow.incognito == true || AppConfig.get('session.save').allWindows == false) { var sessionTitle = (new Date()).toLocaleString(); saveWindowSession({ title: sessionTitle, @@ -111,10 +101,9 @@ export const SessionManager = (function SessionManager() { updateOnSave(); }); } - else - { + else { // Get all non private windows - getAllWindows(function(windows) { + getAllWindows(function (windows) { // Save windows as separate sessions var date = new Date(); @@ -128,14 +117,13 @@ export const SessionManager = (function SessionManager() { var updateEvent = new LimitCounter(sessionCount, updateOnSave); windows.forEach(function (mozWindow) { - if (mozWindow.incognito == false) - { + if (mozWindow.incognito == false) { windowID++; let sessionTitle = date.toLocaleString() + ((sessionCount > 1) ? (' #' + windowID) : ''); saveWindowSession({ title: sessionTitle, tabs: mozWindow.tabs, - }, function(folder) { + }, function (folder) { sessionID = folder.id; updateEvent.advance(); }); @@ -146,8 +134,7 @@ export const SessionManager = (function SessionManager() { }); }; - var mergeSessions = function mergeSessions(sessionID) - { + var mergeSessions = function mergeSessions(sessionID) { // Exclude all pages that are already saved getCurrentWindow(function (mozWindow) { @@ -159,7 +146,7 @@ export const SessionManager = (function SessionManager() { }); // Exclude all pages that are already saved - BookmarkManager.getFolderBookmarks(sessionID, function(bookmarks) { + BookmarkManager.getFolderBookmarks(sessionID, function (bookmarks) { bookmarks.forEach(function (bookmark) { delete toSave[bookmark.url]; }); @@ -170,27 +157,24 @@ export const SessionManager = (function SessionManager() { // TODO - reverse saving order var savingList = []; - for (let key in toSave) - { + for (let key in toSave) { let tab = toSave[key]; savingList.push(tab); } - for (let i = savingList.length - 1; i >= 0; i--) - { + for (let i = savingList.length - 1; i >= 0; i--) { var tab = savingList[i]; BookmarkManager.createBookmarkFromTab(tab, sessionID) - .then(updateEvent.advance, updateEvent.advance); + .then(updateEvent.advance, updateEvent.advance); } }); }); }; - var overwriteSession = function overwriteSession(sessionID) - { + var overwriteSession = function overwriteSession(sessionID) { var toRemove = []; - BookmarkManager.getFolderBookmarks(sessionID, function(bookmarks) { + BookmarkManager.getFolderBookmarks(sessionID, function (bookmarks) { // Remove all bookmarks from the current session bookmarks.forEach(function (bookmark) { @@ -198,7 +182,7 @@ export const SessionManager = (function SessionManager() { }); SessionSyncModel.deleteBookmarkList(toRemove, sessionID, function () { - getCurrentWindow(function(mozWindow) { + getCurrentWindow(function (mozWindow) { var updateFinish = new LimitCounter(mozWindow.tabs.length, function () { WindowEvents.emit(document, 'ViewSession', sessionID); @@ -206,19 +190,17 @@ export const SessionManager = (function SessionManager() { // TODO - add option to discard duplicate tabs and or pinned tabs - for (let i = mozWindow.tabs.length - 1; i >= 0; i--) - { + for (let i = mozWindow.tabs.length - 1; i >= 0; i--) { var tab = mozWindow.tabs[i]; BookmarkManager.createBookmarkFromTab(tab, sessionID) - .then(updateFinish.advance, updateFinish.advance); + .then(updateFinish.advance, updateFinish.advance); } }); }); }); }; - var saveHistorySession = function saveHistorySession(sessionInfo) - { + var saveHistorySession = function saveHistorySession(sessionInfo) { var windowCount = sessionInfo.windows.length; var windowID = 0; @@ -232,8 +214,7 @@ export const SessionManager = (function SessionManager() { } // save all history windows - for (var key in sessionInfo.windows) - { + for (var key in sessionInfo.windows) { var date = new Date(sessionInfo.lastSave); var sessionTitle = date.toLocaleString() + ((windowCount > 1) ? (' #' + windowID) : ''); windowID++; @@ -245,59 +226,51 @@ export const SessionManager = (function SessionManager() { } }; - var saveWindowSession = function saveWindowSession(sessionInfo, callback) - { + var saveWindowSession = function saveWindowSession(sessionInfo, callback) { BookmarkManager.createBookmark({ title: sessionInfo.title, parentId: AppConfig.get('storageID') }) - .then(function (folder) { + .then(function (folder) { - var trackSaving = new LimitCounter(sessionInfo.tabs.length, function () { - callback(folder); - }); + var trackSaving = new LimitCounter(sessionInfo.tabs.length, function () { + callback(folder); + }); - // TODO - add option to discard duplicate tabs + // TODO - add option to discard duplicate tabs - var savePinned = AppConfig.get('session.save').pinned; + var savePinned = AppConfig.get('session.save').pinned; - for (let i = sessionInfo.tabs.length - 1; i >= 0; i--) - { - let tab = sessionInfo.tabs[i]; - if (tab.pinned && savePinned == false) - { - trackSaving.offsetLimit(-1); - } - else - { - BookmarkManager.createBookmarkFromTab(tab, folder.id) - .then(trackSaving.advance, trackSaving.advance); + for (let i = sessionInfo.tabs.length - 1; i >= 0; i--) { + let tab = sessionInfo.tabs[i]; + if (tab.pinned && savePinned == false) { + trackSaving.offsetLimit(-1); + } + else { + BookmarkManager.createBookmarkFromTab(tab, folder.id) + .then(trackSaving.advance, trackSaving.advance); + } } - } - }); + }); }; - var restoreSession = function restoreSession(folderID) - { - browser.runtime.sendMessage({event: 'restore-session', bookmarkID: folderID}); + var restoreSession = function restoreSession(folderID) { + browser.runtime.sendMessage({ event: 'restore-session', bookmarkID: folderID }); }; - var restoreNewWindow = function restoreNewWindow(folderID) - { + var restoreNewWindow = function restoreNewWindow(folderID) { var options = { newWindow: true }; - browser.runtime.sendMessage({event: 'restore-session', bookmarkID: folderID, options: options}); + browser.runtime.sendMessage({ event: 'restore-session', bookmarkID: folderID, options: options }); }; - var restoreAdvanced = function restoreAdvanced(folderID, options) - { - browser.runtime.sendMessage({event: 'restore-session', bookmarkID: folderID, options: options}); + var restoreAdvanced = function restoreAdvanced(folderID, options) { + browser.runtime.sendMessage({ event: 'restore-session', bookmarkID: folderID, options: options }); }; - var replaceSession = function replaceSession(folderID) - { - getCurrentWindow(function(mozWindow) { + var replaceSession = function replaceSession(folderID) { + getCurrentWindow(function (mozWindow) { var options = { newWindow: true, closeWindow: mozWindow.id, @@ -306,8 +279,7 @@ export const SessionManager = (function SessionManager() { }); }; - var replaceSessionAll = function replaceSessionAll(folderID) - { + var replaceSessionAll = function replaceSessionAll(folderID) { var options = { newWindow: true, closeAll: true, @@ -315,78 +287,69 @@ export const SessionManager = (function SessionManager() { restoreAdvanced(folderID, options); }; - var loadBookmarksNewWindow = function loadBookmarksNewWindow(windows) - { - browser.runtime.sendMessage({event: 'restore-sessions', windows: windows }); - }; - - var fixLazySession = function fixLazySession(folderID) - { - browser.runtime.sendMessage({event: 'fix-lazy-session', bookmarkID: folderID }); + var loadBookmarksNewWindow = function loadBookmarksNewWindow(windows) { + browser.runtime.sendMessage({ event: 'restore-sessions', windows: windows }); }; var createSeparator = function createSeparator(folderID) { browser.bookmarks.get(folderID) - .then(function (marks) { - BookmarkManager.createBookmark({ - index: marks[0].index + 1, - type: 'separator', - parentId: AppConfig.get('storageID') - }) - .then(function success(separator) { - WindowEvents.emit(document, 'SetPromiseSession', { sessionID: separator.id, update: true }); + .then(function (marks) { + BookmarkManager.createBookmark({ + index: marks[0].index + 1, + type: 'separator', + parentId: AppConfig.get('storageID') + }) + .then(function success(separator) { + WindowEvents.emit(document, 'SetPromiseSession', { sessionID: separator.id, update: true }); + }); + }, function onError(msg) { + console.log(msg); }); - }, function onError(msg) { - console.log(msg); - }); }; - var createSession = function createSession(title, index) - { + var createSession = function createSession(title, index) { BookmarkManager.createBookmark({ index: index, title: title, parentId: AppConfig.get('storageID') }) - .then(function success(session) { - WindowEvents.emit(document, 'SetPromiseSession', { - sessionID: session.id, - edit: true, - update: true + .then(function success(session) { + WindowEvents.emit(document, 'SetPromiseSession', { + sessionID: session.id, + edit: true, + update: true + }); + }, function error() { }); - }, function error() { - }); }; var createNewSession = function createNewSession(folderID) { var sessionTitle = (new Date()).toLocaleString(); - if (folderID) - { + if (folderID) { browser.bookmarks.get(folderID) - .then(function (marks) { - createSession(sessionTitle, marks[0].index + 1); - }); + .then(function (marks) { + createSession(sessionTitle, marks[0].index + 1); + }); } - else - { + else { createSession(sessionTitle); } }; // Events - GlobalEvents.on('CreateNewSession', function(name) { + GlobalEvents.on('CreateNewSession', function (name) { var title = name ? name : (new Date()).toLocaleString(); createSession(title); }); // Events - GlobalEvents.on('management.close-tab', function(tabID) { + GlobalEvents.on('management.close-tab', function (tabID) { trackTabs = false; browser.tabs.remove(tabID) - .then(function () { - GlobalEvents.emit('UpdateCurrentSession'); - trackTabs = true; - }); + .then(function () { + GlobalEvents.emit('UpdateCurrentSession'); + trackTabs = true; + }); }); // Public API @@ -414,4 +377,4 @@ export const SessionManager = (function SessionManager() { saveHistorySession: saveHistorySession, }; -})(); \ No newline at end of file +})(); From 38c446fe126083b438c3fae5704581825f61b7fe Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Wed, 15 Apr 2026 23:12:10 +0300 Subject: [PATCH 20/32] Improved support for firefox containers - added container UI elements - fixed session restore logic to support containers --- assets/css/session-container.css | 46 ++++- src/background/favicon-service.js | 3 +- src/background/session-history.js | 5 +- src/background/session-management.js | 169 ++++++++++-------- src/common/bookmark-properties.js | 64 +++++++ src/scripts/app.js | 2 + .../components/session-bookmark-events.js | 158 ++++++---------- src/scripts/components/session-bookmark.js | 115 ++++++++---- src/scripts/components/session-container.js | 37 ++-- src/scripts/components/session-header-bar.js | 1 + src/scripts/components/session-history.js | 1 + .../components/session-hotkey-manager.js | 6 +- src/scripts/components/session-list.js | 10 +- src/scripts/services/firefox-containers.js | 64 +++++++ src/scripts/session-sync-ui.js | 54 +++--- src/scripts/session/bookmarks.js | 99 +++++++--- src/scripts/session/history.js | 30 ++++ src/scripts/session/management.js | 5 + src/scripts/utils/action-popup.js | 100 +++++++++++ src/scripts/utils/dom.js | 6 +- wxt.config.js | 17 +- 21 files changed, 683 insertions(+), 309 deletions(-) create mode 100644 src/common/bookmark-properties.js create mode 100644 src/scripts/services/firefox-containers.js create mode 100644 src/scripts/session/history.js create mode 100644 src/scripts/utils/action-popup.js diff --git a/assets/css/session-container.css b/assets/css/session-container.css index 104209d..cc15f40 100644 --- a/assets/css/session-container.css +++ b/assets/css/session-container.css @@ -86,7 +86,7 @@ transition: all 0.2s; } -#session-sync .session-container .window-separator .text { +#session-sync .session-container .window-separator .title { text-align: center; font-weight: 600; } @@ -109,7 +109,7 @@ #session-sync .session-container .bookmark { width: 100%; height: 2em; - padding: 0 4px; + padding: 0 0.4em; line-height: 1.9em; word-break: keep-all; @@ -166,7 +166,7 @@ cursor: default; } -#session-sync .session-container .bookmark[pinned='true']:before { +#session-sync .session-container .bookmark[pinned]:before { content: ""; width: 1.5em; height: 100%; @@ -195,9 +195,9 @@ background-position: center center; } -#session-sync .session-container .bookmark .text { +#session-sync .session-container .bookmark .title { height: 100%; - padding: 0 5px; + padding: 0 0.5em; word-break: keep-all; white-space: nowrap; overflow: hidden; @@ -206,6 +206,40 @@ flex: 1; } +#session-sync .session-container .bookmark .properties { + height: 1.6em; + margin-top: 0.2em; +} + +#session-sync .session-container .bookmark .container { + height: 100%; + margin-right: 2em; + max-width: 10em; + border-radius: 1em; + color: #FFF; + padding: 0 0.5em 0 0.5em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + box-sizing: border-box; +} + +#session-sync .session-container .bookmark .container .name { + height: 100%; + padding: 0 0.5em 0 .1em; + line-height: 1.5em; +} + +#session-sync .session-container .bookmark .container .icon { + width: 1.6em; + height: 100%; + + background-size: 1.25em !important; + background-repeat: no-repeat !important; + background-position: center center !important; +} + /* * History Style */ @@ -220,7 +254,7 @@ transition: all 0.2s; } -#session-sync .session-container .history-window-separator .text { +#session-sync .session-container .history-window-separator .title { text-align: center; } diff --git a/src/background/favicon-service.js b/src/background/favicon-service.js index fce03bf..96f743a 100644 --- a/src/background/favicon-service.js +++ b/src/background/favicon-service.js @@ -44,7 +44,7 @@ const FaviconService = (function FaviconService() { .then(function (result) { browser.storage.local.set({ [key]: result }); }, - function onError(info) { + function onError() { browser.storage.local.set({ [key]: favIconUrl }); }); } @@ -91,7 +91,6 @@ const FaviconService = (function FaviconService() { initConfig(); return { - getFaviconUrl: getFaviconUrl, checkTabFaviconSaved: checkTabFaviconSaved } })(); diff --git a/src/background/session-history.js b/src/background/session-history.js index 83bfc58..a08c52f 100644 --- a/src/background/session-history.js +++ b/src/background/session-history.js @@ -1,7 +1,5 @@ 'use strict'; -import { SessionManagement } from './session-management'; - const storageKey = { config: 'history.config', active: 'history.active', @@ -52,7 +50,6 @@ BrowserSession.prototype.update = function update() { var sessionTab = { title: tab.title, url: tab.url, - favIconUrl: tab.favIconUrl, pinned: tab.pinned, cookieStoreId: tab.cookieStoreId, }; @@ -116,7 +113,7 @@ BrowserSession.prototype.setConfig = function setConfig(conf) { let activeSession = new BrowserSession(); let sessions = []; let config = { - enabled: true, + enabled: false, interval: 15, saveBuffer: 3, savingSlots: 20, diff --git a/src/background/session-management.js b/src/background/session-management.js index d6bd04f..8351c38 100644 --- a/src/background/session-management.js +++ b/src/background/session-management.js @@ -1,6 +1,8 @@ 'use strict'; import { FaviconService } from './favicon-service'; +import { BookmarkProperties } from '../common/bookmark-properties'; +import { FirefoxContainers } from '../scripts/services/firefox-containers'; export const SessionManagement = (function SessionManagement() { var lazyLoadingKey = 'restore.lazy.loading'; @@ -10,37 +12,23 @@ export const SessionManagement = (function SessionManagement() { var reverseOrderKey = 'restore.reverse.order'; var reverseOrder = false; - const containerTag = 'container'; - function getTabInfo(url) { return { url: url }; } - // Container suffix helpers — canonical source: data/scripts/session/bookmarks.js - var containerSuffix = { - regex: new RegExp('^(.*) \\[' + containerTag + ':([^\\]]+)\\]$'), - }; - - // Parse a [container:] suffix from a bookmark title. - // Returns { title, cookieStoreId } — cookieStoreId is null when absent. - function parseContainerFromTitle(rawTitle) { - var match = rawTitle && rawTitle.match(containerSuffix.regex); - if (match) { - return { title: match[1], cookieStoreId: match[2] }; - } - return { title: rawTitle, cookieStoreId: null }; - } - function createTab(options, info, onSuccess, onError) { if (typeof browser === 'undefined' || !browser.tabs) return; var tryCreate = function (withContainer) { + var tabOptions = Object.assign({}, options); + if (withContainer && info.container) { tabOptions.cookieStoreId = info.container; } + return browser.tabs.create(tabOptions).then( function (tabInfo) { if (info.discard) { @@ -59,6 +47,7 @@ export const SessionManagement = (function SessionManagement() { } ); }; + tryCreate(info.container != null); } @@ -81,28 +70,28 @@ export const SessionManagement = (function SessionManagement() { }); }; - var openTab = function openTab(options) { + var openTab = function openTab(options, info) { var checkFavicon = options.favicon === true ? FaviconService.checkTabFaviconSaved : () => { }; delete options.favicon; - var cookieStoreId = options.cookieStoreId || null; - delete options.cookieStoreId; - - var mode = options.mode; - delete options.mode; - - switch (mode) { + switch (info.mode) { case 'newTab': { - createTab(options, { container: cookieStoreId }, checkFavicon); + createTab(options, info, checkFavicon); break; } case 'newWindow': { - if (cookieStoreId) { - options.cookieStoreId = cookieStoreId; - } - browser.windows.create(options).then(function (windowInfo) { - checkFavicon(windowInfo.tabs[0]); + const windowOptions = { + url: options.url, + cookieStoreId: options.cookieStoreId + }; + + browser.windows.create(windowOptions).then(function (windowInfo) { + const tab = windowInfo.tabs[0]; + if (options.pinned) { + browser.tabs.update(tab.id, { pinned: true }); + } + checkFavicon(tab); }); break; } @@ -111,7 +100,16 @@ export const SessionManagement = (function SessionManagement() { browser.tabs.query({ active: true, windowId: browser.windows.WINDOW_ID_CURRENT }) .then(tabs => browser.tabs.get(tabs[0].id)) .then(tab => { - browser.tabs.update(tab.id, options).then(checkFavicon); + if ((options.cookieStoreId == undefined && tab.cookieStoreId == FirefoxContainers.defaultContainer) || + (tab.cookieStoreId === options.cookieStoreId)) { + delete options.cookieStoreId; + browser.tabs.update(tab.id, options).then(checkFavicon); + } else { + browser.tabs.create({ + url: options.url, + cookieStoreId: options.cookieStoreId + }); + } }); break; } @@ -127,13 +125,51 @@ export const SessionManagement = (function SessionManagement() { } }; + var createTabsFromBookmarks = function createTabsFromBookmarks(bookmarks, windowId) { + checkReverseOrder(bookmarks); + bookmarks.forEach(function (bookmark) { + const info = BookmarkProperties.parseTitle(bookmark.title); + createTab( + { + url: bookmark.url, + active: false, + windowId: windowId, + pinned: info.properties.pinned ? true : false + }, + { + discard: lazyLoading, + container: info.properties.container + }, + FaviconService.checkTabFaviconSaved + ); + }); + }; + + var createTabsFromHistoryTabs = function createTabsFromHistoryTabs(tabs, windowId) { + checkReverseOrder(tabs); + tabs.forEach(function (tab) { + createTab( + { + url: tab.url, + active: false, + windowId: windowId, + pinned: tab.pinned ? true : false + }, + { + discard: lazyLoading, + container: tab.cookieStoreId + } + ); + }); + }; + var restoreSession = function restoreSession(folderID, options) { if (typeof browser === 'undefined' || !browser.bookmarks) return; browser.bookmarks.getChildren(folderID) .then(function (bookmarks) { if (options && options.newWindow) { - createSessionWindow(bookmarks) + createSessionWindow((windowID) => createTabsFromBookmarks(bookmarks, windowID)) .then(function (createdWindow) { if (createdWindow) { if (options.closeAll) { @@ -159,21 +195,7 @@ export const SessionManagement = (function SessionManagement() { }); } else { - checkReverseOrder(bookmarks); - bookmarks.forEach(function (bookmark) { - const parsed = parseContainerFromTitle(bookmark.title); - createTab( - { - url: bookmark.url, - active: false, - discard: lazyLoading, - }, - { - container: parsed.cookieStoreId - }, - function (tabInfo) { FaviconService.checkTabFaviconSaved(tabInfo); } - ); - }); + createTabsFromBookmarks(bookmarks, browser.windows.WINDOW_ID_CURRENT); } }, function fail() { @@ -181,45 +203,30 @@ export const SessionManagement = (function SessionManagement() { }); }; - var createSessionWindow = function createSessionWindow(bookmarks) { + var createSessionWindow = function createSessionWindow(callback) { var action = new Promise((resolve, reject) => { if (typeof browser === 'undefined' || !browser.windows) return reject(); - if (bookmarks.length > 0) { - browser.windows.create({}) - .then(function (mozWindow) { - resolve(mozWindow); - var windowID = mozWindow.id; - windowsTabsToKill[windowID] = mozWindow.tabs[0].id; - - checkReverseOrder(bookmarks); - bookmarks.forEach(function (bookmark) { - const parsed = parseContainerFromTitle(bookmark.title); - createTab( - { - url: bookmark.url, - pinned: bookmark.pinned, - windowId: windowID, - }, - { - discard: lazyLoading, - container: parsed.cookieStoreId - }, - function (tabInfo) { FaviconService.checkTabFaviconSaved(tabInfo); } - ); - }); - }); - } - else { - reject(); - } + browser.windows.create({}) + .then(function (mozWindow) { + resolve(mozWindow); + var windowID = mozWindow.id; + windowsTabsToKill[windowID] = mozWindow.tabs[0].id; + callback(windowID); + }); }); return action; }; var restoreSessions = function restoreSessions(windows) { windows.forEach(function (bookmarks) { - createSessionWindow(bookmarks); + createSessionWindow((windowID) => createTabsFromBookmarks(bookmarks, windowID)); + }); + }; + + var restoreHistorySession = function restoreHistorySession(windows) { + windows.forEach(function (tabs) { + createSessionWindow((windowID) => createTabsFromHistoryTabs(tabs, windowID)); }); }; @@ -241,12 +248,16 @@ export const SessionManagement = (function SessionManagement() { restoreSessions(message.windows); break; + case 'restore-history-session': + restoreHistorySession(message.windows); + break; + case 'save-favicon': FaviconService.checkTabFaviconSaved(message.tab, false); break; case 'open-tab': - openTab(message.options); + openTab(message.options, message.info); break; } }); diff --git a/src/common/bookmark-properties.js b/src/common/bookmark-properties.js new file mode 100644 index 0000000..dca7d83 --- /dev/null +++ b/src/common/bookmark-properties.js @@ -0,0 +1,64 @@ +'use strict'; + +// ***************************************************************************** +// API + +/* +* Bookmark Properties +*/ + +export class BookmarkProperties { + + // ------------------------------------------------------------------------ + // Public properties + + static encode(bookmark) { + + var properties = {}; + + if (bookmark.cookieStoreId) { + properties.container = bookmark.cookieStoreId; + } + + if (bookmark.pinned) { + properties.pinned = 1; + } + + return ' [' + JSON.stringify(properties) + ']'; + } + + static decode(encoded) { + try { + return JSON.parse(encoded); + } catch (error) { + return []; + } + } + + static getTitle(bookmark) { + return splitTitle(bookmark.title).title; + } + + static getBookmarkProperties(bookmark) { + return splitTitle(bookmark.title).properties; + } + + static parseTitle(title) { + const match = title.match(/\s\[\{.+\}\]$/); + + if (match) { + var properties = this.decode(match[0]); + if (Array.isArray(properties) && properties.length == 1) { + return { + title: title.substring(0, match.index), + properties: properties[0] + } + } + } + + return { + title: title, + properties: {} + } + } +} \ No newline at end of file diff --git a/src/scripts/app.js b/src/scripts/app.js index 3b77ca1..9cf30c7 100644 --- a/src/scripts/app.js +++ b/src/scripts/app.js @@ -8,6 +8,7 @@ import { WindowEvents, GlobalEvents } from './utils/global-events'; import { SessionSyncUI } from './session-sync-ui'; import { SessionSyncModel } from './components/session-sync-model'; import { BookmarkManager } from './session/bookmarks'; +import { FirefoxContainers } from './services/firefox-containers'; const sessionRoot = 'SessionSync'; @@ -54,6 +55,7 @@ export const init = function init() { }); AppConfig.init(); + FirefoxContainers.loadContainers(); GlobalEvents.on('update-sessions', listSessions.bind(null, false, false)); }; diff --git a/src/scripts/components/session-bookmark-events.js b/src/scripts/components/session-bookmark-events.js index 4a22615..0a3af3c 100644 --- a/src/scripts/components/session-bookmark-events.js +++ b/src/scripts/components/session-bookmark-events.js @@ -11,6 +11,7 @@ import { SessionSyncModel } from './session-sync-model'; import { SessionManager } from '../session/management'; import { BookmarkManager } from '../session/bookmarks'; +import { HistoryManager } from '../session/history'; // ***************************************************************************** // API @@ -18,8 +19,7 @@ import { BookmarkManager } from '../session/bookmarks'; // ---------------------------------------------------------------------------- // Session Events - delegated -export function SessionBookmarkEvents(container) -{ +export function SessionBookmarkEvents(container) { var startDragEvent = null; var trackingTelemetry = new Date(); var offsetRect = {}; @@ -66,10 +66,8 @@ export function SessionBookmarkEvents(container) // -------------------------------------------------------------- // Session level - var bookmarkDblClick = function sessionDblClick(e) - { - if (SyncModel.state.session == 'restore') - { + var bookmarkDblClick = function sessionDblClick(e) { + if (SyncModel.state.session == 'restore') { var bookmarkID = getBookmarkID(e); if (bookmarkID != null && typeof document !== 'undefined') { WindowEvents.emit(document, 'BookmarkCtxMenu-EditBookmark', bookmarkID); @@ -77,20 +75,18 @@ export function SessionBookmarkEvents(container) } }; - var mouseOver = function mouseOver(e) - { + var mouseOver = function mouseOver(e) { if (!e.target.hasAttribute('over')) { return; } if (typeof document === 'undefined') return; - switch(SyncModel.state.session) - { + switch (SyncModel.state.session) { // Bookmark entry case 'restore': { var bookmarkID = getBookmarkID(e); - if (bookmarkHoverContextID == bookmarkID) + if (!bookmarkID || bookmarkHoverContextID == bookmarkID) return; bookmarkHoverContextID = bookmarkID; @@ -117,8 +113,7 @@ export function SessionBookmarkEvents(container) } }; - var mouseLeave = function mouseLeave() - { + var mouseLeave = function mouseLeave() { if (typeof document !== 'undefined') { WindowEvents.emit(document, 'HideUrlBar'); } @@ -127,11 +122,9 @@ export function SessionBookmarkEvents(container) // -------------------------------------------------------------- // Mouse Events - var trackDragging = function _mouseMove(e) - { + var trackDragging = function _mouseMove(e) { var delta = Math.abs(startDragEvent.clientX - e.clientX) > 5 || Math.abs(startDragEvent.clientY - e.clientY) > 5; - if (delta) - { + if (delta) { DragContext.setContext(bookmarkContext); trackingTelemetry = new Date(); @@ -140,7 +133,7 @@ export function SessionBookmarkEvents(container) offsetRect.relativeX = offsetRect.x - container.parentElement.offsetLeft; bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.relativeY + 'px'; - bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.relativeX + 'px'; + bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.relativeX + 'px'; bookmarkContext.DOMRoot.setAttribute('dragging', ''); // Events @@ -149,15 +142,14 @@ export function SessionBookmarkEvents(container) } }; - var cancelDragging = function cancelDragging(e) - { + var cancelDragging = function cancelDragging(e) { AutoScroll.stop(); bookmarkContext.DOMRoot.setAttribute('dragging', 'drop'); bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.y + container.scrollTop + 'px'; bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.x + 'px'; - setTimeout(function() { + setTimeout(function () { bookmarkContext.DOMRoot.removeAttribute('dragging'); bookmarkContext.restorePosition(); }, 50); @@ -166,8 +158,7 @@ export function SessionBookmarkEvents(container) DragContext.clearContext(); }; - var mouseDragging = function mouseMove(e) - { + var mouseDragging = function mouseMove(e) { if (this != window) return; @@ -178,8 +169,7 @@ export function SessionBookmarkEvents(container) var tryScroll = e.clientX > offsetRect.left && e.clientX < offsetRect.right; - if (tryScroll) - { + if (tryScroll) { if (e.clientY < offsetRect.top) { AutoScroll.scroll(container, (e.clientY - offsetRect.top)); } @@ -193,46 +183,37 @@ export function SessionBookmarkEvents(container) } bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.relativeY + 'px'; - bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.relativeX + 'px'; + bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.relativeX + 'px'; trackingTelemetry = new Date(); }; - var mouseUp = function _mouseUp(e) - { - switch(SyncModel.state.session) - { + var mouseUp = function _mouseUp(e) { + switch (SyncModel.state.session) { // if restore session preview case 'restore': { var bookmarkID = getBookmarkID(e); // Test if changing - if (bookmarkID != null) - { + if (bookmarkID != null) { if (bookmarkContext.bookmarkID == bookmarkID) { // Preview the content if a click event was registered (mouse-up on the same DOM node) if (!DragContext.hasContext()) { var mark = SessionSyncModel.bookmarks[bookmarkID]; - var parsed = BookmarkManager.parseContainerFromTitle(mark.title); - BookmarkManager.openBookmark({ - url: mark.url, + BookmarkManager.openBookmark(mark.url, { + title: mark.title, mode: BookmarkManager.getOpenMode(0), - favicon: true, - cookieStoreId: parsed.cookieStoreId, + favicon: true }); } } - else - { - if (SessionSyncModel.bookmarks[bookmarkID] != undefined) - { + else { + if (SessionSyncModel.bookmarks[bookmarkID] != undefined) { SessionSyncModel.moveBookmark(bookmarkContext.bookmarkID, bookmarkID); } } } - else - { - if (getTargetType(e) === 'folder') - { + else { + if (getTargetType(e) === 'folder') { var sessionID = getSessionID(e); if (sessionID != null && SessionSyncModel.bookmarks[sessionID] != undefined) { SessionSyncModel.moveBookmarkTo(bookmarkContext.bookmarkID, sessionID); @@ -246,14 +227,11 @@ export function SessionBookmarkEvents(container) let tabID = getTabID(e); let tabContext = SyncModel.tabs[tabID]; - if (tabContext) - { - if (tabContext != bookmarkContext) - { + if (tabContext) { + if (tabContext != bookmarkContext) { SessionManager.moveTab(bookmarkContext.tab.id, tabContext.tab.index, tabContext.tab.windowId); } - else - { + else { if (tabID) { SessionManager.activateTab(tabID); var windowID = getWindowID(e); @@ -263,48 +241,42 @@ export function SessionBookmarkEvents(container) } } } - else - { - if (getTargetType(e) === 'folder') - { + else { + if (getTargetType(e) === 'folder') { var savingSessionID = getSessionID(e); if (savingSessionID != null) { BookmarkManager.createBookmarkFromTab(bookmarkContext.tab, savingSessionID) - .then(function() { - if (typeof document !== 'undefined') { - WindowEvents.emit(document, 'Notification', { - message: 'Saved', - }); - } - }); + .then(function () { + if (typeof document !== 'undefined') { + WindowEvents.emit(document, 'Notification', { + message: 'Saved', + }); + } + }); } } } } } - if (DragContext.hasContext()) - { + if (DragContext.hasContext()) { cancelDragging(e); } - else - { + else { window.removeEventListener('mousemove', trackDragging); } window.removeEventListener('mouseup', mouseUp); }; - var bookmarkMouseDown = function _bookmarkMouseDown(e) - { + var bookmarkMouseDown = function _bookmarkMouseDown(e) { var target = e.target; if (DragContext.hasContext()) { cancelDragging(e); return; } - switch(SyncModel.state.session) - { + switch (SyncModel.state.session) { // if restore session preview case 'restore': { var bookmarkID = getBookmarkID(e); @@ -314,8 +286,7 @@ export function SessionBookmarkEvents(container) bookmarkContext = SyncModel.getBookmark(bookmarkID); - if (e.button == 0) - { + if (e.button == 0) { startDragEvent = e; window.addEventListener('mousemove', trackDragging); window.addEventListener('mouseup', mouseUp); @@ -323,15 +294,12 @@ export function SessionBookmarkEvents(container) } // Middle Click - if (e.button == 1) - { + if (e.button == 1) { var mark = SessionSyncModel.bookmarks[bookmarkID]; - var parsed = BookmarkManager.parseContainerFromTitle(mark.title); - BookmarkManager.openBookmark({ - url: mark.url, + BookmarkManager.openBookmark(mark.url, { + title: mark.title, mode: BookmarkManager.getOpenMode(1), - favicon: true, - cookieStoreId: parsed.cookieStoreId, + favicon: true }); return; } @@ -342,8 +310,7 @@ export function SessionBookmarkEvents(container) // If active session preview case 'current': { - if (e.button == 0) - { + if (e.button == 0) { let tabID = getTabID(e); if (tabID != undefined) { bookmarkContext = SyncModel.tabs[tabID]; @@ -366,27 +333,22 @@ export function SessionBookmarkEvents(container) // if history session preview case 'history': { - if (target.hasAttribute('url')) - { - let url = getTabURL(e); - - switch (e.button) - { + if (target.hasAttribute('url')) { + switch (e.button) { case 0: case 1: { - BookmarkManager.openBookmark({ - url: url, - mode: BookmarkManager.getOpenMode(e.button) - }); + let windowID = getWindowID(e); + let tabID = getTabID(e); + let tab = HistoryManager.getHistoryTab(windowID, tabID); + + BookmarkManager.openHistoryTab(tab, BookmarkManager.getOpenMode(e.button)); break; } } } - else - { - if (e.button == 0) - { + else { + if (e.button == 0) { let windowID = getWindowID(e); if (windowID != undefined && typeof document !== 'undefined') { WindowEvents.emit(document, 'HistorySessionRestoreWindow', windowID); @@ -398,12 +360,10 @@ export function SessionBookmarkEvents(container) } }; - var bookmarkContextMenu = function bookmarkContextMenu(e) - { + var bookmarkContextMenu = function bookmarkContextMenu(e) { if (typeof document === 'undefined') return; - switch (SyncModel.state.session) - { + switch (SyncModel.state.session) { case 'restore': { var bookmarkID = getBookmarkID(e); if (bookmarkID == null) diff --git a/src/scripts/components/session-bookmark.js b/src/scripts/components/session-bookmark.js index 9483b7c..71b21af 100644 --- a/src/scripts/components/session-bookmark.js +++ b/src/scripts/components/session-bookmark.js @@ -6,6 +6,8 @@ import { HTMLCreator } from '../utils/dom'; import { AutoScroll } from '../utils/auto-scroll'; import { BookmarkManager } from '../session/bookmarks'; +import { BookmarkProperties } from '../../common/bookmark-properties'; +import { FirefoxContainers } from '../services/firefox-containers'; // ***************************************************************************** // API @@ -19,6 +21,11 @@ function setTabFavicon(faviconNode, faviconURL) { faviconNode.style.backgroundSize = 'contain'; } +function configureHoverItem(item, url) { + item.setAttribute('over', ''); + item.setAttribute('url', url); +} + /* * Session Bookmark * Used for interacting with a Firefox bookmark item @@ -28,26 +35,44 @@ export class SessionBookmark { constructor(bookmark) { const DomElem = HTMLCreator(); + // Decode bookmark title + var info = BookmarkProperties.parseTitle(bookmark.title); + const box = DomElem('div', { class: 'bookmark' }); - box.setAttribute('pinned', bookmark.pinned); + + if (bookmark.pinned || info.properties.pinned) { + box.setAttribute('pinned', 'true'); + } const favicon = DomElem('div', { class: 'favicon' }); favicon.setAttribute('bookmarkID', bookmark.id); favicon.setAttribute('over', ''); - const title = DomElem('div', { class: 'text' }); + const title = DomElem('div', { class: 'title' }); title.setAttribute('bookmarkID', bookmark.id); title.setAttribute('over', ''); - title.textContent = bookmark.title; + title.textContent = info.title; + + const propertiesBox = DomElem('div', { class: 'properties' }); + propertiesBox.setAttribute('over', ''); + + if (info.properties.container) { + const container = FirefoxContainers.getContainerElement(info.properties.container); + if (container) { + propertiesBox.appendChild(container); + } + } box.appendChild(favicon); box.appendChild(title); + box.appendChild(propertiesBox); BookmarkManager.getFaviconUrl(bookmark.url, (url) => { setTabFavicon(favicon, url); }); this.bookmark = bookmark; + this.properties = info.properties; this.DOMtitle = title; this.DOMfavicon = favicon; this.bookmarkID = bookmark.id; @@ -99,11 +124,11 @@ export class SessionWindow { const box = DomElem('div', { class: 'window-separator' }); box.style.top = index * bookmarkOffset + 'em'; - const text = DomElem('div', { class: 'text' }); - text.textContent = 'Window ' + (windowIndex + 1); - text.setAttribute('windowID', windowID); + const title = DomElem('div', { class: 'title' }); + title.textContent = 'Window ' + (windowIndex + 1); + title.setAttribute('windowID', windowID); - box.appendChild(text); + box.appendChild(title); this.DOMRoot = box; } @@ -119,28 +144,40 @@ export class SessionTab { const box = DomElem('div', { class: 'bookmark' }); box.style.top = (tab.index + indexOffset) * bookmarkOffset + 'em'; - box.setAttribute('pinned', tab.pinned); box.setAttribute('url', tab.url); + if (tab.pinned) { + box.setAttribute('pinned', 'true'); + } + const favicon = DomElem('div', { class: 'favicon' }); favicon.setAttribute('tabID', tab.id); favicon.setAttribute('windowID', tab.windowId); - favicon.setAttribute('url', tab.url); - favicon.setAttribute('over', ''); + configureHoverItem(favicon, tab.url); - const text = DomElem('div', { class: 'text' }); - text.setAttribute('tabID', tab.id); - text.setAttribute('windowID', tab.windowId); - text.setAttribute('url', tab.url); - text.setAttribute('over', ''); + const title = DomElem('div', { class: 'title' }); + title.setAttribute('tabID', tab.id); + title.setAttribute('windowID', tab.windowId); + title.textContent = tab.title; + configureHoverItem(title, tab.url); if (tab.favIconUrl) { setTabFavicon(favicon, tab.favIconUrl); } - text.textContent = tab.title; + const propertiesBox = DomElem('div', { class: 'properties' }); + propertiesBox.setAttribute('over', ''); + + if (tab.cookieStoreId) { + const container = FirefoxContainers.getContainerElement(tab.cookieStoreId); + if (container) { + propertiesBox.appendChild(container); + } + } + box.appendChild(favicon); - box.appendChild(text); + box.appendChild(title); + box.appendChild(propertiesBox); this.tab = tab; this.position = tab.index + indexOffset; @@ -165,15 +202,15 @@ export class HistoryWindow { box.style.top = index * bookmarkOffset + 'em'; box.setAttribute('windowID', windowID); - const text = DomElem('div', { class: 'text' }); - text.setAttribute('windowID', windowID); - text.textContent = 'Window ' + windowID; + const title = DomElem('div', { class: 'title' }); + title.setAttribute('windowID', windowID); + title.textContent = 'Window ' + windowID; if (!sessionWindowInfo.active) { - text.textContent += ' (closed)'; + title.textContent += ' (closed)'; } - box.appendChild(text); + box.appendChild(title); this.DOMRoot = box; } @@ -190,23 +227,37 @@ export class HistoryTab { const box = DomElem('div', { class: 'bookmark' }); box.style.top = (tab.index + indexOffset) * bookmarkOffset + 'em'; box.setAttribute('url', tab.url); - box.setAttribute('pinned', tab.pinned); + + if (tab.pinned) { + box.setAttribute('pinned', 'true'); + } const favicon = DomElem('div', { class: 'favicon' }); - favicon.setAttribute('url', tab.url); - favicon.setAttribute('over', ''); + configureHoverItem(favicon, tab.url); - const text = DomElem('div', { class: 'text' }); - text.setAttribute('url', tab.url); - text.setAttribute('over', ''); - text.textContent = tab.title; + const title = DomElem('div', { class: 'title' }); + configureHoverItem(title, tab.url); + title.textContent = tab.title; + title.setAttribute('windowID', tab.windowId); + title.setAttribute('tabID', tab.id); - if (tab.favIconUrl) { - setTabFavicon(favicon, tab.favIconUrl); + BookmarkManager.getFaviconUrl(tab.url, (url) => { + setTabFavicon(favicon, url); + }); + + const propertiesBox = DomElem('div', { class: 'properties' }); + propertiesBox.setAttribute('over', ''); + + if (tab.cookieStoreId) { + const container = FirefoxContainers.getContainerElement(tab.cookieStoreId); + if (container) { + propertiesBox.appendChild(container); + } } box.appendChild(favicon); - box.appendChild(text); + box.appendChild(title); + box.appendChild(propertiesBox); this.DOMRoot = box; } diff --git a/src/scripts/components/session-container.js b/src/scripts/components/session-container.js index e9be80b..7b93791 100644 --- a/src/scripts/components/session-container.js +++ b/src/scripts/components/session-container.js @@ -8,6 +8,7 @@ import { AppConfig } from '../config'; import { SessionBookmark, SessionTab, SessionWindow, HistoryWindow, HistoryTab } from './session-bookmark'; import { SessionBookmarkEvents } from './session-bookmark-events'; import { SessionSyncModel } from './session-sync-model'; +import { HistoryManager } from '../session/history'; // Utils import { HTMLCreator } from '../utils/dom'; @@ -48,12 +49,12 @@ export class SessionContainer { SessionSyncModel.bookmarks[ID].parentId == AppConfig.get('storageID')); } - function restoreHistoryWindow(sessionInfo) { + function restoreHistorySession(sessionInfo) { var list = []; sessionInfo.windows.forEach(historyWindow => { list.push(historyWindow.tabs); }); - SessionManager.loadBookmarksNewWindow(list); + SessionManager.loadHistoryTabsNewWindow(list); } if (typeof document !== 'undefined') { @@ -61,7 +62,7 @@ export class SessionContainer { WindowEvents.on(document, 'HistorySessionRestore', (index) => { SessionHistory.getHistorySession(index, (sessionInfo) => { - restoreHistoryWindow(sessionInfo); + restoreHistorySession(sessionInfo); }); }); @@ -73,11 +74,11 @@ export class SessionContainer { SessionManager.activateWindow(activeSessionWindows[windowID].id); } else { - SessionManager.loadBookmarksNewWindow([activeSessionWindows[windowID].tabs]); + SessionManager.loadHistoryTabsNewWindow([activeSessionWindows[windowID].tabs]); } } else { - SessionManager.loadBookmarksNewWindow([this.selectedHistorySession.windows[windowID].tabs]); + SessionManager.loadHistoryTabsNewWindow([this.selectedHistorySession.windows[windowID].tabs]); } } }); @@ -96,7 +97,7 @@ export class SessionContainer { // history session if (this.SyncModel.state.session === 'history') { - restoreHistoryWindow(this.selectedHistorySession); + restoreHistorySession(this.selectedHistorySession); } }); @@ -300,22 +301,27 @@ export class SessionContainer { this.setUIState('history', (new Date(sessionInfo.lastSave)).toLocaleString(), -1); this.selectedHistorySession = sessionInfo; + HistoryManager.selectSession(sessionInfo); + + console.log('sessionInfo', sessionInfo); var tabIndex = 0; var windowIndex = 0; - for (let key in sessionInfo.windows) { + for (let windowID in sessionInfo.windows) { // Add window separator - var sessionWindow = new HistoryWindow(sessionInfo.windows[key], windowIndex, tabIndex); + var sessionWindow = new HistoryWindow(sessionInfo.windows[windowID], windowIndex, tabIndex); this.DOMBookmarks.appendChild(sessionWindow.DOMRoot); tabIndex++; var globalOffset = tabIndex; // Add window tabs - var windowTabs = sessionInfo.windows[key].tabs; + var windowTabs = sessionInfo.windows[windowID].tabs; for (var i in windowTabs) { var tab = windowTabs[i]; + tab.id = i; + tab.windowId = windowID; tab.index = i | 0; var sessionTab = new HistoryTab(tab, globalOffset); this.DOMBookmarks.appendChild(sessionTab.DOMRoot); @@ -338,15 +344,14 @@ export class SessionContainer { WindowEvents.emit(document, 'SetPromiseSession', { sessionID: session.id, update: false }); } - var len = marks.length; - for (var i = 0; i < len; i++) { - var bookmark = new SessionBookmark(marks[i]); - bookmark.setVirtualPosition(marks[i].index); + marks.forEach((mark) => { + var bookmark = new SessionBookmark(mark); + bookmark.setVirtualPosition(mark.index); - SessionSyncModel.bookmarks[marks[i].id] = marks[i]; - this.SyncModel.setBookmark(marks[i].id, bookmark); + SessionSyncModel.bookmarks[mark.id] = mark; + this.SyncModel.setBookmark(mark.id, bookmark); this.DOMBookmarks.appendChild(bookmark.DOMRoot); - } + }); this.updateSessionInfo(); this.restoreScrollTop('restore'); diff --git a/src/scripts/components/session-header-bar.js b/src/scripts/components/session-header-bar.js index 8479f2b..e660cc9 100644 --- a/src/scripts/components/session-header-bar.js +++ b/src/scripts/components/session-header-bar.js @@ -254,6 +254,7 @@ var CreateHeaderMenu = function CreateHeaderMenu() { callback: function () { if (typeof browser !== 'undefined' && browser.storage) { browser.storage.local.get().then(function (data) { + console.log(data); }); } diff --git a/src/scripts/components/session-history.js b/src/scripts/components/session-history.js index e6601f6..086d404 100644 --- a/src/scripts/components/session-history.js +++ b/src/scripts/components/session-history.js @@ -9,6 +9,7 @@ import { WindowEvents, GlobalEvents } from '../utils/global-events'; // API export const SessionHistory = (function SessionHistory() { + var storageKey = { config: 'history.config', active: 'history.active', diff --git a/src/scripts/components/session-hotkey-manager.js b/src/scripts/components/session-hotkey-manager.js index 8616ffc..d17a996 100644 --- a/src/scripts/components/session-hotkey-manager.js +++ b/src/scripts/components/session-hotkey-manager.js @@ -189,10 +189,8 @@ export function SessionHotkeyManager() { }); url.addEventListener('click', function () { - BookmarkManager.openBookmark({ - url: supportLink, - mode: 'newTab', - active: true + BookmarkManager.openBookmark(supportLink, { + mode: 'newTab' }); }); diff --git a/src/scripts/components/session-list.js b/src/scripts/components/session-list.js index 3e49f4d..be35340 100644 --- a/src/scripts/components/session-list.js +++ b/src/scripts/components/session-list.js @@ -222,12 +222,10 @@ export class SessionList { refreshHistoryList(); }); - if (this.document) { - WindowEvents.on(this.document, 'ShowHistoryList', () => { - this.setState('history'); - refreshHistoryList(); - }); - } + WindowEvents.on(document, 'ShowHistoryList', () => { + this.setState('history'); + refreshHistoryList(); + }); historyList.addEventListener('click', (e) => { if (e.target.className === 'history-node') { diff --git a/src/scripts/services/firefox-containers.js b/src/scripts/services/firefox-containers.js new file mode 100644 index 0000000..ef89c4b --- /dev/null +++ b/src/scripts/services/firefox-containers.js @@ -0,0 +1,64 @@ +'use strict'; + +import { DomElem } from '../utils/dom'; + +// ***************************************************************************** +// API + +/* +* Bookmark Properties +*/ + +export class FirefoxContainers { + + static defaultContainer = 'firefox-default'; + static containers = []; + static domContainers = {}; + + // ------------------------------------------------------------------------ + // Public properties + + static loadContainers() { + browser.contextualIdentities.query({}).then(FirefoxContainers.onGot, FirefoxContainers.onError); + } + + static onGot(contexts) { + FirefoxContainers.containers = contexts; + console.log('FirefoxContainers.containers', FirefoxContainers.containers); + + // create dom containers + for (const container of contexts) { + + const domContainer = DomElem('div', { class: 'container' }); + domContainer.style.backgroundColor = container.colorCode; + + const icon = DomElem('div', { class: 'icon' }); + icon.style.background = 'url("' + container.iconUrl + '")'; + + const name = DomElem('div', { class: 'name' }); + name.textContent = container.name; + + domContainer.appendChild(icon); + domContainer.appendChild(name); + + FirefoxContainers.domContainers[container.cookieStoreId] = domContainer; + } + } + + static onError(error) { + console.error(error); + } + + static getContainerElement(cookieStoreId) { + var node = FirefoxContainers.domContainers[cookieStoreId]; + if (node) { + return node.cloneNode(true); + } + + return null; + } + + static getContainerInfo(cookieStoreId) { + return FirefoxContainers.containers.find(container => container.cookieStoreId === cookieStoreId); + } +} \ No newline at end of file diff --git a/src/scripts/session-sync-ui.js b/src/scripts/session-sync-ui.js index 3b67ae0..d4379d4 100644 --- a/src/scripts/session-sync-ui.js +++ b/src/scripts/session-sync-ui.js @@ -37,6 +37,7 @@ import { SessionToolbar } from './components/session-toolbar'; // Bookmarks import { SessionManager } from './session/management'; import { BookmarkManager } from './session/bookmarks'; +import { BookmarkProperties } from '../common/bookmark-properties'; // ************************************************************************ // UI initilization @@ -300,34 +301,28 @@ export const SessionSyncUI = function SessionSyncUI() { WindowEvents.on(document, 'BookmarkCtxMenu-OpenInNewTab', function (bookmarkID) { var mark = SessionSyncModel.bookmarks[bookmarkID]; - var parsed = BookmarkManager.parseContainerFromTitle(mark.title); - BookmarkManager.openBookmark({ - url: mark.url, + BookmarkManager.openBookmark(mark.url, { + title: mark.title, mode: 'newTab', - favicon: true, - cookieStoreId: parsed.cookieStoreId, + favicon: true }); }); WindowEvents.on(document, 'BookmarkCtxMenu-OpenInActiveTab', function (bookmarkID) { var mark = SessionSyncModel.bookmarks[bookmarkID]; - var parsed = BookmarkManager.parseContainerFromTitle(mark.title); - BookmarkManager.openBookmark({ - url: mark.url, + BookmarkManager.openBookmark(mark.url, { + title: mark.title, mode: 'activeTab', - favicon: true, - cookieStoreId: parsed.cookieStoreId, + favicon: true }); }); WindowEvents.on(document, 'BookmarkCtxMenu-OpenInNewWindow', function (bookmarkID) { var mark = SessionSyncModel.bookmarks[bookmarkID]; - var parsed = BookmarkManager.parseContainerFromTitle(mark.title); - BookmarkManager.openBookmark({ - url: mark.url, + BookmarkManager.openBookmark(mark.url, { + title: mark.title, mode: 'newWindow', - favicon: true, - cookieStoreId: parsed.cookieStoreId, + favicon: true }); }); @@ -338,22 +333,19 @@ export const SessionSyncUI = function SessionSyncUI() { }); WindowEvents.on(document, 'HistoryMarksCtxMenu-OpenInNewTab', function (url) { - BookmarkManager.openBookmark({ - url: url, + BookmarkManager.openBookmark(url, { mode: 'newTab' }); }); WindowEvents.on(document, 'HistoryMarksCtxMenu-OpenInActiveTab', function (url) { - BookmarkManager.openBookmark({ - url: url, + BookmarkManager.openBookmark(url, { mode: 'activeTab' }); }); WindowEvents.on(document, 'HistoryMarksCtxMenu-OpenInNewWindow', function (url) { - BookmarkManager.openBookmark({ - url: url, + BookmarkManager.openBookmark(url, { mode: 'newWindow' }); }); @@ -416,14 +408,22 @@ export const SessionSyncUI = function SessionSyncUI() { document.addEventListener('mousemove', eventMove); }; + function setPanelWidth(width) { + document.body.style.width = width + 'px'; + } + + function setPanelHeight(height) { + document.body.style.height = height + 'px'; + } + if (AppConfig.isPanel()) { - AppConfig.onChange('style.panel.width', function (size) { - document.body.style.width = size + 'px'; - }); + // Apply immediately from current config (handles Chrome popup where onChange + // events from setupConfig fire before these listeners are registered) + setPanelWidth(AppConfig.get('style.panel.width')); + setPanelHeight(AppConfig.get('style.panel.height')); - AppConfig.onChange('style.panel.height', function (size) { - document.body.style.height = size + 'px'; - }); + AppConfig.onChange('style.panel.width', setPanelWidth); + AppConfig.onChange('style.panel.height', setPanelHeight); resizeHandle.addEventListener('mousedown', startResize); } diff --git a/src/scripts/session/bookmarks.js b/src/scripts/session/bookmarks.js index 33f9816..39647f1 100644 --- a/src/scripts/session/bookmarks.js +++ b/src/scripts/session/bookmarks.js @@ -4,14 +4,13 @@ // Modules import { AppConfig } from '../config'; +import { BookmarkProperties } from '../../common/bookmark-properties'; // ************************************************************************ // API export const BookmarkManager = (function BookmarkManager() { - const containerTag = 'container'; - var getHostName = function getHostName(url) { try { return new URL(url).hostname; @@ -20,9 +19,9 @@ export const BookmarkManager = (function BookmarkManager() { } }; - var getFaviconUrl = function getFaviconUrl(favIconUrl, callback) { + var getFaviconUrlFromStorage = function getFaviconUrlFromStorage(favIconUrl, callback) { var key = '@favIconUrl:' + getHostName(favIconUrl); - browser.storage.local.get(key) + return browser.storage.local.get(key) .then(function (obj) { if (obj[key]) { callback(obj[key]); @@ -30,6 +29,19 @@ export const BookmarkManager = (function BookmarkManager() { }); }; + var getFaviconUrl = function getFaviconUrl(url, callback) { + if (!url) return callback(''); + + if (import.meta.env.BROWSER === 'firefox') { + getFaviconUrlFromStorage(url, callback); + return; + } + + const faviconUrl = browser.runtime.getURL(`_favicon/?pageUrl=${encodeURIComponent(url)}&size=32`); + callback(faviconUrl); + return; + }; + var getFolderBookmarks = function getFolderBookmarks(folderID, callback) { var result = browser.bookmarks.getChildren(folderID); result.then(callback, function fail() { @@ -47,19 +59,14 @@ export const BookmarkManager = (function BookmarkManager() { return state ? 'newTab' : 'activeTab'; }; - var containerSuffix = { - encode: function (id) { return ` [${containerTag}:${id}]`; }, - regex: new RegExp('^(.*) \\[' + containerTag + ':([^\\]]+)\\]$'), - default: 'firefox-default', - }; - var createBookmarkFromTab = function createBookmarkFromTab(tab, parentId) { var url = tab.url; browser.runtime.sendMessage({ event: 'save-favicon', tab: tab }); var title = tab.title; - if (tab.cookieStoreId && tab.cookieStoreId !== containerSuffix.default) { - title += containerSuffix.encode(tab.cookieStoreId); + console.log('tab', tab); + if (tab.cookieStoreId || tab.pinned) { + title += BookmarkProperties.encode(tab); } return browser.bookmarks.create({ @@ -70,16 +77,6 @@ export const BookmarkManager = (function BookmarkManager() { }; - // Parse a [container:] suffix from a bookmark title. - // Returns { title, cookieStoreId } — cookieStoreId is null when absent. - var parseContainerFromTitle = function parseContainerFromTitle(rawTitle) { - var match = rawTitle && rawTitle.match(containerSuffix.regex); - if (match) { - return { title: match[1], cookieStoreId: match[2] }; - } - return { title: rawTitle, cookieStoreId: null }; - }; - var createBookmark = function createBookmark(options) { // parentId possible values // root________ @@ -90,15 +87,61 @@ export const BookmarkManager = (function BookmarkManager() { return browser.bookmarks.create(options); }; - var openBookmark = function openBookmark(options) { + var openBookmark = function openBookmark(url, info) { + + console.log('openBookmark', url, info); + + var options = { + url: url + }; + + if (AppConfig.isPanel() == false) { + options.active = false; + if (info.mode === 'activeTab') { + info.mode = 'newTab'; + } + } + + if (info.title) { + var bookmarkInfo = BookmarkProperties.parseTitle(info.title); + if (bookmarkInfo.properties.container) { + options.cookieStoreId = bookmarkInfo.properties.container; + } + + if (bookmarkInfo.properties.pinned) { + options.pinned = true; + } + + delete options.title; + } + + browser.runtime.sendMessage({ event: 'open-tab', options: options, info: info }); + }; + + var openHistoryTab = function openHistoryTab(tab, mode) { + + console.log('openHistoryTab', tab, mode); + + var options = { + url: tab.url + }; + if (AppConfig.isPanel() == false) { - if (options.mode == 'activeTab') { - options.mode = 'newTab'; - options.active = false; + options.active = false; + if (mode === 'activeTab') { + mode = 'newTab'; } } - browser.runtime.sendMessage({ event: 'open-tab', options: options }); + if (tab.cookieStoreId) { + options.cookieStoreId = tab.cookieStoreId; + } + + if (tab.pinned) { + options.pinned = true; + } + + browser.runtime.sendMessage({ event: 'open-tab', options: options, info: { mode: mode } }); }; var setTitle = function setTitle(bookmark, title, callback) { @@ -147,9 +190,9 @@ export const BookmarkManager = (function BookmarkManager() { getFaviconUrl: getFaviconUrl, getOpenMode: getOpenMode, openBookmark: openBookmark, + openHistoryTab: openHistoryTab, createBookmark: createBookmark, createBookmarkFromTab: createBookmarkFromTab, - parseContainerFromTitle: parseContainerFromTitle, searchBookmarks: searchBookmarks, setTitle: setTitle, diff --git a/src/scripts/session/history.js b/src/scripts/session/history.js new file mode 100644 index 0000000..618da9b --- /dev/null +++ b/src/scripts/session/history.js @@ -0,0 +1,30 @@ +'use strict'; + +// ************************************************************************ +// API + +export const HistoryManager = (function HistoryManager() { + + var selectedSessions; + + + var selectSession = function selectSession(sessionInfo) { + selectedSessions = sessionInfo; + console.log('selectedSessions', selectedSessions); + }; + + var getSelectedSession = function getSelectedSession() { + return selectedSessions; + }; + + var getHistoryTab = function getHistoryTab(windowID, tabID) { + return selectedSessions.windows[windowID].tabs[tabID]; + }; + + return { + selectSession: selectSession, + getSelectedSession: getSelectedSession, + getHistoryTab: getHistoryTab + }; + +})(); \ No newline at end of file diff --git a/src/scripts/session/management.js b/src/scripts/session/management.js index 701cca9..54719ff 100644 --- a/src/scripts/session/management.js +++ b/src/scripts/session/management.js @@ -291,6 +291,10 @@ export const SessionManager = (function SessionManager() { browser.runtime.sendMessage({ event: 'restore-sessions', windows: windows }); }; + var loadHistoryTabsNewWindow = function loadHistoryTabsNewWindow(windows) { + browser.runtime.sendMessage({ event: 'restore-history-session', windows: windows }); + }; + var createSeparator = function createSeparator(folderID) { browser.bookmarks.get(folderID) .then(function (marks) { @@ -369,6 +373,7 @@ export const SessionManager = (function SessionManager() { replaceSessionAll: replaceSessionAll, restoreNewWindow: restoreNewWindow, loadBookmarksNewWindow: loadBookmarksNewWindow, + loadHistoryTabsNewWindow: loadHistoryTabsNewWindow, mergeSessions: mergeSessions, overwriteSession: overwriteSession, saveActiveSession: saveActiveSession, diff --git a/src/scripts/utils/action-popup.js b/src/scripts/utils/action-popup.js new file mode 100644 index 0000000..5b2e257 --- /dev/null +++ b/src/scripts/utils/action-popup.js @@ -0,0 +1,100 @@ +'use strict'; + +// ***************************************************************************** +// Custom Modules + +import { HTMLCreator } from './dom'; +import { WindowEvents } from './global-events'; + +// ***************************************************************************** +// ActionPopup + +export function ActionPopup(options) +{ + var DomElem = HTMLCreator(); + + var panel = DomElem('div', {class : 'action-popoup'}); + + var info = DomElem('div', {class : 'info'}); + var controls = DomElem('div', {class : 'controls'}); + + var ok = DomElem('div', {class : 'button ok'}); + ok.textContent = 'Confirm'; + + var cancel = DomElem('div', {class : 'button cancel'}); + cancel.textContent = 'Cancel'; + + var flexArea1 = DomElem('div', {class : 'flex'}); + var flexArea2 = DomElem('div', {class : 'flex'}); + + controls.appendChild(flexArea1); + controls.appendChild(ok); + controls.appendChild(cancel); + controls.appendChild(flexArea2); + panel.appendChild(info); + panel.appendChild(controls); + + var callback; + var defaultCallback = function defaultCallback() {}; + + // ------------------------------------------------------------------------ + // Events + + ok.addEventListener('click', function() { + callback(); + hidePanel(); + }); + + cancel.addEventListener('click', function() { + hidePanel(); + }); + + var openPanel = function openPanel(options) + { + panel.setAttribute('data-active', 'true'); + panel.style.left = options.event.pageX + 'px'; + panel.style.top = options.event.pageY + 'px'; + + if (typeof document !== 'undefined') { + document.addEventListener('mousedown', blurMenu); + } + + info.textContent = options.message; + setCallback(options.callback); + }; + + var hidePanel = function hidePanel() + { + panel.removeAttribute('data-active'); + if (typeof document !== 'undefined') { + document.removeEventListener('mousedown', blurMenu); + } + callback = defaultCallback; + }; + + var blurMenu = function hideWhenClickOutside(e) + { + var target = e.target; + while (target) { + if (target === panel) + return; + target = target.parentElement; + } + hidePanel(); + }; + + function setCallback(func) { + callback = typeof func === 'function' ? func : defaultCallback; + } + + // Register custom events + if (typeof document !== 'undefined') { + WindowEvents.on(document, options.name + '-Open', openPanel); + WindowEvents.on(document, 'ActionPopup-Hide', hidePanel); + } + + // ------------------------------------------------------------------------ + // Public properties + + this.DOMRoot = panel; +} \ No newline at end of file diff --git a/src/scripts/utils/dom.js b/src/scripts/utils/dom.js index 878de2d..87699b2 100644 --- a/src/scripts/utils/dom.js +++ b/src/scripts/utils/dom.js @@ -8,7 +8,7 @@ const NS_HTML = 'http://www.w3.org/1999/xhtml'; function Node(ns, type, attrs) { var elem = document.createElementNS(ns, type); - attrs && Object.keys(attrs).forEach(function(attr) { + attrs && Object.keys(attrs).forEach(function (attr) { elem.setAttribute(attr, attrs[attr]); }); @@ -18,3 +18,7 @@ function Node(ns, type, attrs) { export function HTMLCreator() { return Node.bind(null, NS_HTML); } + +export function DomElem(type, attrs) { + return Node(NS_HTML, type, attrs); +} diff --git a/wxt.config.js b/wxt.config.js index 37251c0..8f995b1 100644 --- a/wxt.config.js +++ b/wxt.config.js @@ -1,13 +1,19 @@ import 'dotenv/config'; import { defineConfig } from 'wxt'; -console.log('FIREFOX_BINARY:', process.env.FIREFOX_BINARY); -console.log('FIREFOX_PROFILE:', process.env.FIREFOX_PROFILE); - export default defineConfig(() => { + + console.log('FIREFOX_BINARY:', process.env.FIREFOX_BINARY); + console.log('FIREFOX_PROFILE:', process.env.FIREFOX_PROFILE); + return { srcDir: 'src', publicDir: 'public', + imports: { + eslintrc: { + enabled: 9, + }, + }, vite: () => ({ optimizeDeps: { entries: [ @@ -46,7 +52,8 @@ export default defineConfig(() => { 'clipboardWrite', 'unlimitedStorage', 'cookies', - 'contextualIdentities' + 'contextualIdentities', + 'favicon' ], action: { default_icon: { @@ -66,7 +73,7 @@ export default defineConfig(() => { '128': 'images/icon128.png' }, commands: { - '_execute_browser_action': { + '_execute_action': { suggested_key: { 'default': 'Ctrl+Shift+F' }, From 5d98656d3e7fd616697efbe990b8d656d6a57681 Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Thu, 16 Apr 2026 00:38:43 +0300 Subject: [PATCH 21/32] Refactoring --- .eslintrc.json | 33 - .prettierrc | 8 + eslint.config.js | 56 ++ package.json | 15 +- src/background/favicon-service.js | 60 +- src/background/session-management.js | 96 +- src/common/bookmark-properties.js | 18 +- src/scripts/components/session-container.js | 2 +- .../components/session-folder-events.js | 550 ++++++----- src/scripts/components/session-header-bar.js | 41 +- .../components/session-hotkey-manager.js | 2 +- src/scripts/components/session-list.js | 2 +- src/scripts/components/session-sorting.js | 2 +- src/scripts/components/session-sync-model.js | 859 +++++++++--------- src/scripts/config.js | 2 +- src/scripts/session-sync-ui.js | 1 - src/scripts/utils/action-popup.js | 200 ++-- src/scripts/utils/field-edit-widget.js | 4 - 18 files changed, 959 insertions(+), 992 deletions(-) delete mode 100644 .eslintrc.json create mode 100644 .prettierrc create mode 100644 eslint.config.js diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index e17bdbf..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "parserOptions": { - "ecmaVersion": 6, - "sourceType": "module" - }, - "env": { - "browser": true - }, - "extends": "eslint:recommended", - "rules": { - "indent": [ - "error", - "tab", - { - "SwitchCase": 1 - } - ], - "linebreak-style": [ - "error", - "windows" - ], - "quotes": [ - "error", - "single" - ], - "semi": [ - "error", - "always" - ], - "no-console": "off", - "no-undef": "off" - } -} \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..8f25cf6 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": true, + "singleQuote": true, + "trailingComma": "es5", + "printWidth": 120, + "tabWidth": 4, + "useTabs": true +} \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..2f7cef6 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,56 @@ +import js from '@eslint/js'; +import globals from 'globals'; +import autoImports from './.wxt/eslint-auto-imports.mjs'; +import stylistic from '@stylistic/eslint-plugin'; + +/** @type {import('eslint').Linter.Config[]} */ +export default [ + { + ignores: [ + '.output/**', + '.wxt/**', + 'node_modules/**', + ], + }, + js.configs.recommended, + autoImports, + { + plugins: { + '@stylistic': stylistic, + }, + languageOptions: { + globals: { + ...globals.browser, + ...globals.webextensions, + }, + ecmaVersion: 'latest', + sourceType: 'module', + }, + rules: { + // Stylistic Rules + '@stylistic/indent': [ + 'error', + 'tab', + { + 'SwitchCase': 1, + }, + ], + '@stylistic/linebreak-style': [ + 'error', + 'windows', + ], + '@stylistic/quotes': [ + 'error', + 'single', + ], + '@stylistic/semi': [ + 'error', + 'always', + ], + + // Core Logic Rules + 'no-console': 'off', + 'no-undef': 'off', + }, + }, +]; diff --git a/package.json b/package.json index 65fe5b6..3fe31c3 100644 --- a/package.json +++ b/package.json @@ -8,17 +8,26 @@ "scripts": { "dev": "wxt", "dev:chrome": "wxt -b chrome --mv3 --host 127.0.0.1", - "dev:firefox": "wxt -b firefox --mv2 --host 127.0.0.1", + "dev:firefox": "wxt -b firefox --mv3 --host 127.0.0.1", "build": "wxt build", "build:chrome": "wxt build -b chrome --mv3", - "build:firefox": "wxt build -b firefox --mv2", + "build:firefox": "wxt build -b firefox --mv3", "clean": "wxt clean", "zip": "wxt zip", "zip:chrome": "wxt zip -b chrome --mv3", - "zip:firefox": "wxt zip -b firefox --mv2", + "zip:firefox": "wxt zip -b firefox --mv3", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier --write .", "postinstall": "wxt prepare" }, "devDependencies": { + "@eslint/js": "^9.39.4", + "@stylistic/eslint-plugin": "^5.10.0", + "eslint": "^9.39.4", + "eslint-config-prettier": "^10.1.8", + "globals": "^17.5.0", + "prettier": "^3.8.3", "wxt": "^0.20.20" } } \ No newline at end of file diff --git a/src/background/favicon-service.js b/src/background/favicon-service.js index 96f743a..3c8bdac 100644 --- a/src/background/favicon-service.js +++ b/src/background/favicon-service.js @@ -1,53 +1,55 @@ 'use strict'; const FaviconService = (function FaviconService() { - var serviceStatus = true; var serviceStatusKey = 'services.favicon.enabled'; var getBase64ImageFromUrl = async function getBase64ImageFromUrl(imageUrl) { - var res = await fetch(imageUrl); var blob = await res.blob(); return new Promise((resolve, reject) => { var reader = new FileReader(); - reader.addEventListener("load", function () { - resolve(reader.result); - }, false); + reader.addEventListener( + 'load', + function () { + resolve(reader.result); + }, + false + ); reader.onerror = function () { return reject(this); }; reader.readAsDataURL(blob); }); - } + }; var getHostName = function getHostName(url) { try { return new URL(url).hostname; - } catch (e) { + } catch { return ''; } - } + }; var getFaviconUrl = function getFaviconUrl(url, callback) { var key = '@favIconUrl:' + getHostName(url); - browser.storage.local.get(key) - .then(function (obj) { - callback(obj[key]); - }, callback); - } + browser.storage.local.get(key).then(function (obj) { + callback(obj[key]); + }, callback); + }; var setFaviconUrl = function setFaviconUrl(key, favIconUrl) { - getBase64ImageFromUrl(favIconUrl) - .then(function (result) { + getBase64ImageFromUrl(favIconUrl).then( + function (result) { browser.storage.local.set({ [key]: result }); }, - function onError() { - browser.storage.local.set({ [key]: favIconUrl }); - }); - } + function onError() { + browser.storage.local.set({ [key]: favIconUrl }); + } + ); + }; var checkTabFaviconSaved = function checkTabFaviconSaved(tab, waitToLoad) { if (serviceStatus == false) { @@ -58,11 +60,10 @@ const FaviconService = (function FaviconService() { getFaviconUrl(tab.url, function (value) { if (value === undefined || tab.url != value) { var key = '@favIconUrl:' + getHostName(tab.url); - setFaviconUrl(key, tab.favIconUrl) + setFaviconUrl(key, tab.favIconUrl); } }); - } - else if (waitToLoad != false) { + } else if (waitToLoad != false) { function handle(tabId, changeInfo, tabInfo) { browser.tabs.onUpdated.removeListener(handle); checkTabFaviconSaved(tabInfo, false); @@ -70,16 +71,15 @@ const FaviconService = (function FaviconService() { browser.tabs.onUpdated.addListener(handle, { tabId: tab.id, - properties: ['favIconUrl'] + properties: ['favIconUrl'], }); } - } + }; var initConfig = function initConfig() { - browser.storage.local.get(serviceStatusKey) - .then(function (obj) { - serviceStatus = obj[serviceStatusKey]; - }); + browser.storage.local.get(serviceStatusKey).then(function (obj) { + serviceStatus = obj[serviceStatusKey]; + }); }; browser.storage.onChanged.addListener(function onChange(object) { @@ -91,8 +91,8 @@ const FaviconService = (function FaviconService() { initConfig(); return { - checkTabFaviconSaved: checkTabFaviconSaved - } + checkTabFaviconSaved: checkTabFaviconSaved, + }; })(); export { FaviconService }; diff --git a/src/background/session-management.js b/src/background/session-management.js index 8351c38..9a58d98 100644 --- a/src/background/session-management.js +++ b/src/background/session-management.js @@ -14,7 +14,7 @@ export const SessionManagement = (function SessionManagement() { function getTabInfo(url) { return { - url: url + url: url, }; } @@ -22,7 +22,6 @@ export const SessionManagement = (function SessionManagement() { if (typeof browser === 'undefined' || !browser.tabs) return; var tryCreate = function (withContainer) { - var tabOptions = Object.assign({}, options); if (withContainer && info.container) { @@ -59,19 +58,17 @@ export const SessionManagement = (function SessionManagement() { var initConfig = function initConfig() { if (typeof browser === 'undefined' || !browser.storage || !browser.storage.local) return; - browser.storage.local.get(lazyLoadingKey) - .then(function (obj) { - setLazyLoading(obj[lazyLoadingKey]); - }); + browser.storage.local.get(lazyLoadingKey).then(function (obj) { + setLazyLoading(obj[lazyLoadingKey]); + }); - browser.storage.local.get(reverseOrderKey) - .then(function (obj) { - reverseOrder = (obj[reverseOrderKey] == true); - }); + browser.storage.local.get(reverseOrderKey).then(function (obj) { + reverseOrder = obj[reverseOrderKey] == true; + }); }; var openTab = function openTab(options, info) { - var checkFavicon = options.favicon === true ? FaviconService.checkTabFaviconSaved : () => { }; + var checkFavicon = options.favicon === true ? FaviconService.checkTabFaviconSaved : () => {}; delete options.favicon; switch (info.mode) { @@ -83,7 +80,7 @@ export const SessionManagement = (function SessionManagement() { case 'newWindow': { const windowOptions = { url: options.url, - cookieStoreId: options.cookieStoreId + cookieStoreId: options.cookieStoreId, }; browser.windows.create(windowOptions).then(function (windowInfo) { @@ -97,17 +94,21 @@ export const SessionManagement = (function SessionManagement() { } case 'activeTab': { - browser.tabs.query({ active: true, windowId: browser.windows.WINDOW_ID_CURRENT }) - .then(tabs => browser.tabs.get(tabs[0].id)) - .then(tab => { - if ((options.cookieStoreId == undefined && tab.cookieStoreId == FirefoxContainers.defaultContainer) || - (tab.cookieStoreId === options.cookieStoreId)) { + browser.tabs + .query({ active: true, windowId: browser.windows.WINDOW_ID_CURRENT }) + .then((tabs) => browser.tabs.get(tabs[0].id)) + .then((tab) => { + if ( + (options.cookieStoreId == undefined && + tab.cookieStoreId == FirefoxContainers.defaultContainer) || + tab.cookieStoreId === options.cookieStoreId + ) { delete options.cookieStoreId; browser.tabs.update(tab.id, options).then(checkFavicon); } else { browser.tabs.create({ url: options.url, - cookieStoreId: options.cookieStoreId + cookieStoreId: options.cookieStoreId, }); } }); @@ -121,7 +122,7 @@ export const SessionManagement = (function SessionManagement() { var checkReverseOrder = function checkReverseOrder(bookmarks) { if (reverseOrder === true) { - bookmarks = bookmarks.reverse(); + bookmarks.reverse(); } }; @@ -134,11 +135,11 @@ export const SessionManagement = (function SessionManagement() { url: bookmark.url, active: false, windowId: windowId, - pinned: info.properties.pinned ? true : false + pinned: info.properties.pinned ? true : false, }, { discard: lazyLoading, - container: info.properties.container + container: info.properties.container, }, FaviconService.checkTabFaviconSaved ); @@ -153,11 +154,11 @@ export const SessionManagement = (function SessionManagement() { url: tab.url, active: false, windowId: windowId, - pinned: tab.pinned ? true : false + pinned: tab.pinned ? true : false, }, { discard: lazyLoading, - container: tab.cookieStoreId + container: tab.cookieStoreId, } ); }); @@ -166,17 +167,18 @@ export const SessionManagement = (function SessionManagement() { var restoreSession = function restoreSession(folderID, options) { if (typeof browser === 'undefined' || !browser.bookmarks) return; - browser.bookmarks.getChildren(folderID) - .then(function (bookmarks) { + browser.bookmarks.getChildren(folderID).then( + function (bookmarks) { if (options && options.newWindow) { - createSessionWindow((windowID) => createTabsFromBookmarks(bookmarks, windowID)) - .then(function (createdWindow) { + createSessionWindow((windowID) => createTabsFromBookmarks(bookmarks, windowID)).then( + function (createdWindow) { if (createdWindow) { if (options.closeAll) { - browser.windows.getAll({ - populate: true, - windowTypes: ['normal'] - }) + browser.windows + .getAll({ + populate: true, + windowTypes: ['normal'], + }) .then(function (windows) { windows.forEach(function (mozWidnow) { if (mozWidnow.id != createdWindow.id) { @@ -190,30 +192,31 @@ export const SessionManagement = (function SessionManagement() { browser.windows.remove(options.closeWindow); } } - }, function onError() { + }, + function onError() { console.log('strage error'); - }); - } - else { + } + ); + } else { createTabsFromBookmarks(bookmarks, browser.windows.WINDOW_ID_CURRENT); } }, - function fail() { - console.log('Error getting bookmarks from folder ID: ', folderID); - }); + function fail() { + console.log('Error getting bookmarks from folder ID: ', folderID); + } + ); }; var createSessionWindow = function createSessionWindow(callback) { var action = new Promise((resolve, reject) => { if (typeof browser === 'undefined' || !browser.windows) return reject(); - browser.windows.create({}) - .then(function (mozWindow) { - resolve(mozWindow); - var windowID = mozWindow.id; - windowsTabsToKill[windowID] = mozWindow.tabs[0].id; - callback(windowID); - }); + browser.windows.create({}).then(function (mozWindow) { + resolve(mozWindow); + var windowID = mozWindow.id; + windowsTabsToKill[windowID] = mozWindow.tabs[0].id; + callback(windowID); + }); }); return action; }; @@ -303,7 +306,6 @@ export const SessionManagement = (function SessionManagement() { return { getTabInfo: getTabInfo, - init: init + init: init, }; - })(); diff --git a/src/common/bookmark-properties.js b/src/common/bookmark-properties.js index dca7d83..4143456 100644 --- a/src/common/bookmark-properties.js +++ b/src/common/bookmark-properties.js @@ -4,16 +4,14 @@ // API /* -* Bookmark Properties -*/ + * Bookmark Properties + */ export class BookmarkProperties { - // ------------------------------------------------------------------------ // Public properties static encode(bookmark) { - var properties = {}; if (bookmark.cookieStoreId) { @@ -30,7 +28,7 @@ export class BookmarkProperties { static decode(encoded) { try { return JSON.parse(encoded); - } catch (error) { + } catch { return []; } } @@ -51,14 +49,14 @@ export class BookmarkProperties { if (Array.isArray(properties) && properties.length == 1) { return { title: title.substring(0, match.index), - properties: properties[0] - } + properties: properties[0], + }; } } return { title: title, - properties: {} - } + properties: {}, + }; } -} \ No newline at end of file +} diff --git a/src/scripts/components/session-container.js b/src/scripts/components/session-container.js index 7b93791..62194d2 100644 --- a/src/scripts/components/session-container.js +++ b/src/scripts/components/session-container.js @@ -188,7 +188,7 @@ export class SessionContainer { container.style.fontSize = size + 'px'; }); - AppConfig.onChange('session.save', (obj) => { + AppConfig.onChange('session.save', () => { if (this.SyncModel.state.session == 'current') { this.showCurrentSession(); } diff --git a/src/scripts/components/session-folder-events.js b/src/scripts/components/session-folder-events.js index 1d369d9..4b61c36 100644 --- a/src/scripts/components/session-folder-events.js +++ b/src/scripts/components/session-folder-events.js @@ -1,293 +1,257 @@ -'use strict'; - -// ***************************************************************************** -// Custom Modules - -// App -import { AppConfig } from '../config'; -import { SessionSyncModel } from './session-sync-model'; - -// Utils -import { WindowEvents } from '../utils/global-events'; -import { AutoScroll } from '../utils/auto-scroll'; -import { DragContext } from '../utils/drag-context'; - -// ***************************************************************************** -// API - -// ---------------------------------------------------------------------------- -// Session Folder Events - delegated - -export function SessionFolderEvents(container) -{ - var startDragEvent = null; - var trackingTelemetry; - var offsetRect = {}; - var bookmarkContext = null; - var SyncModel = SessionSyncModel.getModel(typeof document !== 'undefined' ? document : null); - - function getAttribute(target, attribute) { - try { - return target.getAttribute(attribute); - } - catch (err) { - console.log(err); - return null; - } - } - - function getBookmarkID(e) { - return getAttribute(e.target, 'sessionID'); - } - - // -------------------------------------------------------------- - // Drag Events - - var trackDragging = function _mouseMove(e) - { - var delta = Math.abs(startDragEvent.clientX - e.clientX) > 5 || Math.abs(startDragEvent.clientY - e.clientY) > 5; - if (delta) { - DragContext.setContext(bookmarkContext); - trackingTelemetry = new Date(); - - offsetRect = container.parentElement.parentElement.getBoundingClientRect(); - offsetRect.relativeY = offsetRect.y - container.parentElement.offsetTop; - offsetRect.relativeX = offsetRect.x - container.parentElement.offsetLeft; - - bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.relativeY + 'px'; - bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.relativeX + 'px'; - bookmarkContext.DOMRoot.setAttribute('dragging', ''); - - // Events - window.addEventListener('mousemove', mouseDragging); - window.removeEventListener('mousemove', trackDragging); - } - }; - - var cancelDragging = function cancelDragging(e) - { - AutoScroll.stop(); - - bookmarkContext.DOMRoot.setAttribute('dragging', 'drop'); - bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.y + container.scrollTop + 'px'; - bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.x + 'px'; - - setTimeout(function() { - bookmarkContext.DOMRoot.removeAttribute('dragging'); - bookmarkContext.restorePosition(); - }, 50); - window.removeEventListener('mousemove', mouseDragging); - - DragContext.clearContext(); - }; - - var mouseDragging = function mouseMove(e) - { - if (this != window) - return; - var deltaTime = new Date() - trackingTelemetry; - if (deltaTime < 16) { - return; - } - - var tryScroll = e.clientX > offsetRect.left && e.clientX < offsetRect.right; - - if (tryScroll) - { - if (e.clientY < offsetRect.top) { - AutoScroll.scroll(container, (e.clientY - offsetRect.top)); - } - else if (e.clientY > offsetRect.bottom) { - AutoScroll.scroll(container, (e.clientY - offsetRect.bottom)); - } else { - AutoScroll.stop(); - } - } else { - AutoScroll.stop(); - } - - bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.relativeY + 'px'; - bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.relativeX + 'px'; - trackingTelemetry = new Date(); - }; - - var mouseUp = function _mouseUp(e) - { - // Test if changing - if (e.target.className == 'folder') - { - var sessionID = getBookmarkID(e); - - // Preview the content if a click event was registered (mouse-up on the same DOM node) - if (bookmarkContext.bookmarkID == sessionID) - { - if (!DragContext.hasContext()) { - if (typeof document !== 'undefined') { - WindowEvents.emit(document, 'SelectSyncSession', bookmarkContext); - } - } - } - else - { - if (AppConfig.get('session.sorting') === 'position-asc') - { - SessionSyncModel.moveBookmark(bookmarkContext.bookmarkID, sessionID); - } - else - { - if (typeof document !== 'undefined') { - WindowEvents.emit(document, 'Notification', { - message: 'Rearage function available only when items are sorted ascending by position', - timeout: 3000, - }); - } - } - } - } - - if (DragContext.hasContext()) - { - cancelDragging(e); - } - else - { - window.removeEventListener('mousemove', trackDragging); - } - - window.removeEventListener('mouseup', mouseUp); - }; - - var folderMouseDown = function _folderMouseDown(e) - { - if (DragContext.hasContext()) { - cancelDragging(e); - return; - } - - var sessionID = getBookmarkID(e); - bookmarkContext = SyncModel.getBookmark(sessionID); - - if (e.button == 0) { - startDragEvent = e; - window.addEventListener('mousemove', trackDragging); - window.addEventListener('mouseup', mouseUp); - return; - } - }; - - var folderContextMenu = function folderContextMenu(e) - { - if (typeof document === 'undefined') return; - - if (bookmarkContext) - { - var contextMenu = undefined; - switch (bookmarkContext.bookmark.type) - { - case 'folder': - contextMenu = 'SessionContextMenu'; - break; - - case 'bookmark': - contextMenu = 'SessionContextBookmarkMenu'; - break; - - case 'separator': - contextMenu = 'SessionContextSeparatorMenu'; - break; - - default: - return; - } - - WindowEvents.emit(document, contextMenu + '-Open', { - context: bookmarkContext.bookmark.id, - event: e - }); - } - else { - WindowEvents.emit(document, 'SessionListMenu-Open', { event: e }); - } - }; - - var sessionDblClick = function sessionDblClick(e) - { - if (typeof document === 'undefined') return; - - if (bookmarkContext) - { - switch (bookmarkContext.bookmark.type) - { - case 'folder': - WindowEvents.emit(document, 'SessionContextMenu-EditSession', bookmarkContext.bookmarkID); - break; - - case 'bookmark': - WindowEvents.emit(document, 'BookmarkCtxMenu-EditBookmark', bookmarkContext.bookmarkID); - break; - } - } - }; - - // ------------------------------------------------------------------------ - // Init Code Events - - container.addEventListener('dblclick', sessionDblClick); - container.addEventListener('mousedown', folderMouseDown); - container.addEventListener('contextmenu', folderContextMenu); -} - -// ---------------------------------------------------------------------------- -// History Session Folder Events - delegated - -export function SessionHistoryEvents(container) -{ - var historyContext = null; - - function getAttribute(target, attribute) { - try { - return target.getAttribute(attribute); - } - catch (err) { - console.log(err); - return null; - } - } - - var onMouseDown = function onMouseDown(e) - { - if (e.button != 2) - return; - - var target = e.target; - if (target.className == 'history-node') { - if (typeof document !== 'undefined') { - WindowEvents.emit(document, 'HistorySessionCtxMenu-Open', { - context: e.target, - event: e - }); - } - } - else - { - if (typeof document !== 'undefined') { - WindowEvents.emit(document, 'HistoryListCtxMenu-Open', {event: e }); - } - } - }; - - var onClick = function onClick(e) - { - if (e.target.className === 'history-node') - { - // WindowEvents.emit(document, 'HistoryListCtxMenu-Open', {event: e }); - // this.selectHistorySession(e.target); - } - }; - - // ------------------------------------------------------------------------ - // Init Code Events - - container.addEventListener('click', onClick); - container.addEventListener('mousedown', onMouseDown); -} \ No newline at end of file +'use strict'; + +// ***************************************************************************** +// Custom Modules + +// App +import { AppConfig } from '../config'; +import { SessionSyncModel } from './session-sync-model'; + +// Utils +import { WindowEvents } from '../utils/global-events'; +import { AutoScroll } from '../utils/auto-scroll'; +import { DragContext } from '../utils/drag-context'; + +// ***************************************************************************** +// API + +// ---------------------------------------------------------------------------- +// Session Folder Events - delegated + +export function SessionFolderEvents(container) { + var startDragEvent = null; + var trackingTelemetry; + var offsetRect = {}; + var bookmarkContext = null; + var SyncModel = SessionSyncModel.getModel(typeof document !== 'undefined' ? document : null); + + function getAttribute(target, attribute) { + try { + return target.getAttribute(attribute); + } + catch (err) { + console.log(err); + return null; + } + } + + function getBookmarkID(e) { + return getAttribute(e.target, 'sessionID'); + } + + // -------------------------------------------------------------- + // Drag Events + + var trackDragging = function _mouseMove(e) { + var delta = Math.abs(startDragEvent.clientX - e.clientX) > 5 || Math.abs(startDragEvent.clientY - e.clientY) > 5; + if (delta) { + DragContext.setContext(bookmarkContext); + trackingTelemetry = new Date(); + + offsetRect = container.parentElement.parentElement.getBoundingClientRect(); + offsetRect.relativeY = offsetRect.y - container.parentElement.offsetTop; + offsetRect.relativeX = offsetRect.x - container.parentElement.offsetLeft; + + bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.relativeY + 'px'; + bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.relativeX + 'px'; + bookmarkContext.DOMRoot.setAttribute('dragging', ''); + + // Events + window.addEventListener('mousemove', mouseDragging); + window.removeEventListener('mousemove', trackDragging); + } + }; + + var cancelDragging = function cancelDragging(e) { + AutoScroll.stop(); + + bookmarkContext.DOMRoot.setAttribute('dragging', 'drop'); + bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.y + container.scrollTop + 'px'; + bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.x + 'px'; + + setTimeout(function () { + bookmarkContext.DOMRoot.removeAttribute('dragging'); + bookmarkContext.restorePosition(); + }, 50); + window.removeEventListener('mousemove', mouseDragging); + + DragContext.clearContext(); + }; + + var mouseDragging = function mouseMove(e) { + if (this != window) + return; + var deltaTime = new Date() - trackingTelemetry; + if (deltaTime < 16) { + return; + } + + var tryScroll = e.clientX > offsetRect.left && e.clientX < offsetRect.right; + + if (tryScroll) { + if (e.clientY < offsetRect.top) { + AutoScroll.scroll(container, (e.clientY - offsetRect.top)); + } + else if (e.clientY > offsetRect.bottom) { + AutoScroll.scroll(container, (e.clientY - offsetRect.bottom)); + } else { + AutoScroll.stop(); + } + } else { + AutoScroll.stop(); + } + + bookmarkContext.DOMRoot.style.top = e.clientY - offsetRect.relativeY + 'px'; + bookmarkContext.DOMRoot.style.left = e.clientX - offsetRect.relativeX + 'px'; + trackingTelemetry = new Date(); + }; + + var mouseUp = function _mouseUp(e) { + // Test if changing + if (e.target.className == 'folder') { + var sessionID = getBookmarkID(e); + + // Preview the content if a click event was registered (mouse-up on the same DOM node) + if (bookmarkContext.bookmarkID == sessionID) { + if (!DragContext.hasContext()) { + if (typeof document !== 'undefined') { + WindowEvents.emit(document, 'SelectSyncSession', bookmarkContext); + } + } + } + else { + if (AppConfig.get('session.sorting') === 'position-asc') { + SessionSyncModel.moveBookmark(bookmarkContext.bookmarkID, sessionID); + } + else { + if (typeof document !== 'undefined') { + WindowEvents.emit(document, 'Notification', { + message: 'Rearage function available only when items are sorted ascending by position', + timeout: 3000, + }); + } + } + } + } + + if (DragContext.hasContext()) { + cancelDragging(e); + } + else { + window.removeEventListener('mousemove', trackDragging); + } + + window.removeEventListener('mouseup', mouseUp); + }; + + var folderMouseDown = function _folderMouseDown(e) { + if (DragContext.hasContext()) { + cancelDragging(e); + return; + } + + var sessionID = getBookmarkID(e); + bookmarkContext = SyncModel.getBookmark(sessionID); + + if (e.button == 0) { + startDragEvent = e; + window.addEventListener('mousemove', trackDragging); + window.addEventListener('mouseup', mouseUp); + return; + } + }; + + var folderContextMenu = function folderContextMenu(e) { + if (typeof document === 'undefined') return; + + if (bookmarkContext) { + var contextMenu = undefined; + switch (bookmarkContext.bookmark.type) { + case 'folder': + contextMenu = 'SessionContextMenu'; + break; + + case 'bookmark': + contextMenu = 'SessionContextBookmarkMenu'; + break; + + case 'separator': + contextMenu = 'SessionContextSeparatorMenu'; + break; + + default: + return; + } + + WindowEvents.emit(document, contextMenu + '-Open', { + context: bookmarkContext.bookmark.id, + event: e + }); + } + else { + WindowEvents.emit(document, 'SessionListMenu-Open', { event: e }); + } + }; + + var sessionDblClick = function sessionDblClick(e) { + if (typeof document === 'undefined') return; + + if (bookmarkContext) { + switch (bookmarkContext.bookmark.type) { + case 'folder': + WindowEvents.emit(document, 'SessionContextMenu-EditSession', bookmarkContext.bookmarkID); + break; + + case 'bookmark': + WindowEvents.emit(document, 'BookmarkCtxMenu-EditBookmark', bookmarkContext.bookmarkID); + break; + } + } + }; + + // ------------------------------------------------------------------------ + // Init Code Events + + container.addEventListener('dblclick', sessionDblClick); + container.addEventListener('mousedown', folderMouseDown); + container.addEventListener('contextmenu', folderContextMenu); +} + +// ---------------------------------------------------------------------------- +// History Session Folder Events - delegated + +export function SessionHistoryEvents(container) { + + var onMouseDown = function onMouseDown(e) { + if (e.button != 2) + return; + + var target = e.target; + if (target.className == 'history-node') { + if (typeof document !== 'undefined') { + WindowEvents.emit(document, 'HistorySessionCtxMenu-Open', { + context: e.target, + event: e + }); + } + } + else { + if (typeof document !== 'undefined') { + WindowEvents.emit(document, 'HistoryListCtxMenu-Open', { event: e }); + } + } + }; + + var onClick = function onClick(e) { + if (e.target.className === 'history-node') { + // WindowEvents.emit(document, 'HistoryListCtxMenu-Open', {event: e }); + // this.selectHistorySession(e.target); + } + }; + + // ------------------------------------------------------------------------ + // Init Code Events + + container.addEventListener('click', onClick); + container.addEventListener('mousedown', onMouseDown); +} diff --git a/src/scripts/components/session-header-bar.js b/src/scripts/components/session-header-bar.js index e660cc9..f332ff0 100644 --- a/src/scripts/components/session-header-bar.js +++ b/src/scripts/components/session-header-bar.js @@ -60,7 +60,7 @@ export function SessionHeaderBar() { if (typeof document !== 'undefined') { WindowEvents.emit(document, 'ShowTooltip', { node: e.target, - message: e.target.getAttribute('tooltip') + message: e.target.getAttribute('tooltip'), }); } } else { @@ -105,7 +105,6 @@ var CreateHeaderMenu = function CreateHeaderMenu() { // Methods function MenuButton(options) { - var button = DomElem('div', { class: 'menu-button' }); var icon = DomElem('div', { class: 'icon' }); @@ -117,7 +116,10 @@ var CreateHeaderMenu = function CreateHeaderMenu() { icon.style.backgroundImage = url; // Events - var callback = (typeof JSUtils.getValidFunction === 'function') ? JSUtils.getValidFunction(options.callback) : options.callback; + var callback = + typeof JSUtils.getValidFunction === 'function' + ? JSUtils.getValidFunction(options.callback) + : options.callback; button.addEventListener('click', callback); button.appendChild(icon); @@ -134,7 +136,7 @@ var CreateHeaderMenu = function CreateHeaderMenu() { var state = model.state['config'] == undefined ? 'on' : undefined; WindowEvents.emit(document, 'SetUIState', { config: state }); } - } + }, }); var openTutorial = MenuButton({ @@ -144,7 +146,7 @@ var CreateHeaderMenu = function CreateHeaderMenu() { if (typeof browser !== 'undefined' && browser.runtime) { browser.runtime.sendMessage({ event: 'session_sync_tutorial' }); } - } + }, }); var leaveFeedback = MenuButton({ @@ -154,7 +156,7 @@ var CreateHeaderMenu = function CreateHeaderMenu() { if (typeof browser !== 'undefined' && browser.runtime) { browser.runtime.sendMessage({ event: 'session_sync_leave_feedback' }); } - } + }, }); var exportImport = MenuButton({ @@ -167,7 +169,7 @@ var CreateHeaderMenu = function CreateHeaderMenu() { active: true, }); } - } + }, }); var buttonDetachInTab = MenuButton({ @@ -177,7 +179,7 @@ var CreateHeaderMenu = function CreateHeaderMenu() { if (typeof browser !== 'undefined' && browser.runtime) { browser.runtime.sendMessage({ event: 'session_sync_detach_tab' }); } - } + }, }); var buttonDetachInWindow = MenuButton({ @@ -187,7 +189,7 @@ var CreateHeaderMenu = function CreateHeaderMenu() { if (typeof browser !== 'undefined' && browser.runtime) { browser.runtime.sendMessage({ event: 'session_sync_detach_window' }); } - } + }, }); var githubPage = MenuButton({ @@ -197,7 +199,7 @@ var CreateHeaderMenu = function CreateHeaderMenu() { if (typeof browser !== 'undefined' && browser.runtime) { browser.runtime.sendMessage({ event: 'session_sync_open_github' }); } - } + }, }); menuRow1.appendChild(openConfig); @@ -223,12 +225,11 @@ var CreateHeaderMenu = function CreateHeaderMenu() { this.init = true; initDevMode(); } - } - else { + } else { this.manualTrigger = 0; } } - } + }, }; function initDevMode() { @@ -239,13 +240,14 @@ var CreateHeaderMenu = function CreateHeaderMenu() { icon: 'icons/document.png', callback: function () { if (typeof browser !== 'undefined' && browser.tabs) { - browser.tabs.query({ active: true, windowId: browser.windows.WINDOW_ID_CURRENT }) - .then(tabs => browser.tabs.get(tabs[0].id)) - .then(tab => { + browser.tabs + .query({ active: true, windowId: browser.windows.WINDOW_ID_CURRENT }) + .then((tabs) => browser.tabs.get(tabs[0].id)) + .then((tab) => { console.log(tab); }); } - } + }, }); var localStorage = MenuButton({ @@ -254,11 +256,10 @@ var CreateHeaderMenu = function CreateHeaderMenu() { callback: function () { if (typeof browser !== 'undefined' && browser.storage) { browser.storage.local.get().then(function (data) { - console.log(data); }); } - } + }, }); var clearFaviconCache = MenuButton({ @@ -274,7 +275,7 @@ var CreateHeaderMenu = function CreateHeaderMenu() { } }); } - } + }, }); devRow1.appendChild(activeTabInfo); diff --git a/src/scripts/components/session-hotkey-manager.js b/src/scripts/components/session-hotkey-manager.js index d17a996..87e28ae 100644 --- a/src/scripts/components/session-hotkey-manager.js +++ b/src/scripts/components/session-hotkey-manager.js @@ -200,7 +200,7 @@ export function SessionHotkeyManager() { var tokens = options.shortcut.split('+'); header.textContent = options.title, - currentHotKey.setValue(tokens.join(' + ')); + currentHotKey.setValue(tokens.join(' + ')); newHotKey.setValue(''); modifierKey.setValue(tokens[0]); optionalModifier.setValue(tokens.length == 3 ? tokens[1] : ''); diff --git a/src/scripts/components/session-list.js b/src/scripts/components/session-list.js index be35340..469462d 100644 --- a/src/scripts/components/session-list.js +++ b/src/scripts/components/session-list.js @@ -216,7 +216,7 @@ export class SessionList { } } }); - } + }; GlobalEvents.on('UpdateHistoryList', function () { refreshHistoryList(); diff --git a/src/scripts/components/session-sorting.js b/src/scripts/components/session-sorting.js index ace0cad..754f4ea 100644 --- a/src/scripts/components/session-sorting.js +++ b/src/scripts/components/session-sorting.js @@ -6,7 +6,7 @@ // Utils import { AppConfig } from '../config'; import { HTMLCreator } from '../utils/dom'; -import { WindowEvents, GlobalEvents } from '../utils/global-events'; +import { WindowEvents } from '../utils/global-events'; // ***************************************************************************** // API diff --git a/src/scripts/components/session-sync-model.js b/src/scripts/components/session-sync-model.js index de82054..bc33fce 100644 --- a/src/scripts/components/session-sync-model.js +++ b/src/scripts/components/session-sync-model.js @@ -1,446 +1,413 @@ -'use strict'; - -// ***************************************************************************** -// Custom Modules - -import { AppConfig } from '../config'; - -// Components -import { BookmarkManager } from '../session/bookmarks'; -import { WindowEvents, GlobalEvents } from '../utils/global-events'; - -// Utils -import { LimitCounter } from '../utils/general'; - -// ***************************************************************************** -// API - -export class SessionSyncDOMModel { - constructor(document) { - this.bookmarks = {}; - this.tabs = {}; - this.state = {}; - - // ------------------------------------------------------------------------ - // Events - - const OrderBookmarks = (indices) => { - var len = indices.length; - for (let i = 0; i < len; i++) { - var node = this.bookmarks[indices[i]]; - if (node) { - node.setVirtualPosition(SessionSyncModel.bookmarks[node.bookmarkID].index); - } - } - }; - - WindowEvents.on(document, 'BookmarksPositionChanged', OrderBookmarks); - - WindowEvents.on(document, 'BookmarkTitleChanged', (bookmarkID) => { - var node = this.bookmarks[bookmarkID]; - if (node) { - node.setTitle(SessionSyncModel.bookmarks[bookmarkID].title); - } - }); - - // ------------------------------------------------------------------------ - // Register - - SessionSyncModel.registerModel(document, this); - } - - getBookmark(id) { - return this.bookmarks[id]; - } - - setBookmark(id, bookmark) { - this.bookmarks[id] = bookmark; - } - - removeBookmark(id) { - if (this.bookmarks[id]) { - this.bookmarks[id].remove(); - delete this.bookmarks[id]; - } - } -} - -export const SessionSyncModel = (function SessionSync() { - - // Bookmarks storage - var sessions = {}; - var bookmarks = {}; - - // Per document DOM cache - var domModels = new Map(); - - // ------------------------------------------------------------------------ - // API - - var getModel = function getModel(doc) - { - var model = domModels.get(doc); - if ((model instanceof SessionSyncDOMModel) === false) { - model = new SessionSyncDOMModel(doc); - } - return model; - }; - - var registerModel = function registerModel(doc, model) - { - if (model instanceof SessionSyncDOMModel) { - domModels.set(doc, model); - } - }; - - var unregisterModel = function unregisterModel(doc) - { - var model = domModels.get(doc); - if (model instanceof SessionSyncDOMModel) { - domModels.delete(doc); - } - }; - - function onActionError() - { - WindowEvents.emitLocal('Notification', { - message: 'Error when updating bookmarks' - }); - } - - function isSession(parentID) { - return parentID === AppConfig.get('storageID'); - } - - // Assumes moving in the same parent directory - var moveBookmark = function moveBookmark(idA, idB) - { - var parentID = bookmarks[idA].parentId; - var indexA = bookmarks[idA].index; - var indexB = bookmarks[idB].index; - var direction = indexA > indexB ? 1 : -1; - - BookmarkManager.moveItem(idA, parentID, indexA < indexB ? indexB : indexB, onSuccessMove, onActionError); - - function onSuccessMove() - { - var indices = []; - var min = Math.min(indexA, indexB); - var max = Math.max(indexA, indexB); - - for (var id in bookmarks) - { - var B = bookmarks[id]; - if (B.parentId == parentID && B.index > min && B.index < max) - { - indices.push(B.id); - B.index += direction; - } - } - - indices.push(idA); - indices.push(idB); - bookmarks[idA].index = indexB; - bookmarks[idB].index += direction; - - WindowEvents.broadcast(isSession(parentID) ? 'SessionsPositionChanged' : 'BookmarksPositionChanged', indices); - } - }; - - // Moving a bookmark to a new session directory - var moveBookmarkTo = function moveBookmarkTo(bookmarkID, folderID) - { - var parentID = bookmarks[bookmarkID].parentId; - if (parentID == folderID) - return; - - BookmarkManager.moveItem(bookmarkID, folderID, undefined, onSuccess, onActionError); - - function onSuccess() - { - bookmarks[bookmarkID].parentId = folderID; - - // TODO: set position based on the length of the new parent list - // TODO: trigger update for folderID - - var index = bookmarks[bookmarkID].index; - bookmarks[bookmarkID].index = -1; - - // Remove the bookmark from all UIs - domModels.forEach(function (model) { - model.removeBookmark(bookmarkID); - }); - - // Reorder all bookmarks after the removed one - var indices = []; - for (var id in bookmarks) - { - var B = bookmarks[id]; - if (B.parentId == parentID && B.index > index) - { - indices.push(B.id); - B.index--; - } - } - - // Trigger reordering - WindowEvents.broadcast('BookmarksPositionChanged', indices); - } - }; - - // all items must have the same parent - var deleteBookmarkList = function deleteBookmarkList(bookmarkIDs, parentID, callback) - { - if (bookmarkIDs.length == 0) - { - callback(); - return; - } - - var reorderList = []; - - // Get position ordered list of all bookmarks from the parrentID folder - for (var id in bookmarks) - { - var B = bookmarks[id]; - if (B.parentId == parentID) { - reorderList[B.index] = B; - } - } - - var updateEvent = new LimitCounter(bookmarkIDs.length, onFinish); - - bookmarkIDs.forEach(function(bookmarkID) - { - BookmarkManager.deleteItem(bookmarkID, false, onDeleteSuccess.bind(null, bookmarkID), updateEvent.advance); - }); - - function onFinish() - { - // compute reordering - var indices = []; - var position = 0; - for (var i = 0; i < reorderList.length; i++) { - if (reorderList[i]) { - indices.push(reorderList[i].id); - reorderList[i].index = position; - position++; - } - } - - if (indices.length) - { - // Trigger reordering - WindowEvents.broadcast('BookmarksPositionChanged', indices); - } - - callback(); - } - - function onDeleteSuccess(bookmarkID) - { - var index = bookmarks[bookmarkID].index; - - // Remove the bookmark from all UIs - domModels.forEach(function (model) { - model.removeBookmark(bookmarkID); - }); - - delete reorderList[index]; - delete bookmarks[bookmarkID]; - GlobalEvents.emit('BookmarkRemoved'); - - updateEvent.advance(); - } - }; - - var deleteBookmarkItem = function deleteBookmarkItem(bookmarkID) - { - var isFolder = bookmarks[bookmarkID].type === 'folder'; - BookmarkManager.deleteItem(bookmarkID, isFolder, onDeleteSuccess, onActionError); - - function onDeleteSuccess() - { - var parentID = bookmarks[bookmarkID].parentId; - var index = bookmarks[bookmarkID].index; - - // Remove the bookmark from all UIs - domModels.forEach(function (model) { - model.removeBookmark(bookmarkID); - }); - - // Reorder all bookmarks after the removed one - var indices = []; - for (let id in bookmarks) - { - let B = bookmarks[id]; - if (B.parentId == parentID && B.index > index) { - indices.push(B.id); - B.index--; - } - } - - // Save undo history - var undoEvent = { type: 'delete', bookmark: bookmarks[bookmarkID], items: [] }; - - if (isFolder) - { - var folderID = bookmarks[bookmarkID].id; - for (let id in bookmarks) - { - let B = bookmarks[id]; - if (B.parentId == folderID) { - undoEvent.items.push(Object.assign({}, B)); - } - } - } - - // Remove model references - delete bookmarks[bookmarkID]; - undoEvent.items.forEach(function(B) { - delete bookmarks[B.id]; - }); - - // Save undo event - var undoEvents = AppConfig.get('undo.events'); - undoEvents.push(undoEvent); - - AppConfig.set('undo.events', undoEvents); - GlobalEvents.emit('BookmarkDeleted', bookmarkID); - - // Trigger reordering - WindowEvents.broadcast('BookmarksPositionChanged', indices); - } - }; - - var updateBookmarkItem = function updateBookmarkItem(bookmarkID, options) - { - try { - if (options.title) { - BookmarkManager.setTitle(bookmarks[bookmarkID], options.title, function() { - WindowEvents.broadcast('BookmarkTitleChanged', bookmarkID); - WindowEvents.broadcast('SessionDescriptionChanged', bookmarkID); - }); - } - - if (options.url) { - BookmarkManager.setLocation(bookmarks[bookmarkID], options.url); - } - - WindowEvents.emitLocal('Notification', { - message: 'Success' - }); - - } catch(err) { - console.log(err); - WindowEvents.emitLocal('Notification', { - message: 'Error while saving bookmark' - }); - } - }; - - // ------------------------------------------------------------------------ - // Events - - var recreateBookmark = function recreateBookmark(item) - { - delete item.id; - delete item.dateAdded; - delete item.dateGroupModified; - delete item.title_lowercase; - return BookmarkManager.createBookmark(item); - }; - - var undoHistoryEvent = function undoHistoryEvent() - { - var sessionID = undefined; - var undoEvents = AppConfig.get('undo.events'); - - function onSuccess() - { - AppConfig.set('undo.events', undoEvents); - WindowEvents.emit(document, 'SetPromiseSession', { sessionID: sessionID, update: false }); - GlobalEvents.emit('update-sessions'); - } - - if (undoEvents.length) - { - var historyEvent = undoEvents.pop(); - - if (historyEvent.type == 'delete') - { - switch (historyEvent.bookmark.type) - { - case 'bookmark': - case 'separator': { - recreateBookmark(historyEvent.bookmark) - .then(function (bookmark) { - sessionID = (bookmark.parentId == AppConfig.get('storageID')) ? bookmark.id : bookmark.parentId; - onSuccess(); - }); - break; - } - - case 'folder': { - // Restore deleted folder - var oldID = historyEvent.bookmark.id; - - recreateBookmark(historyEvent.bookmark) - .then(function (node) { - sessionID = node.id; - var newID = node.id; - var updateEvent = new LimitCounter(historyEvent.items.length, onSuccess); - - // correct parent for each bookmark - if (oldID != newID) - { - // correct restore event - historyEvent.items.forEach(function (bookmark) { - bookmark.parentId = newID; - recreateBookmark(bookmark) - .then(updateEvent.advance, updateEvent.advance); - }); - - // correct all undo events - undoEvents.forEach(function (event) { - event.items.forEach(function (bookmark) { - if (bookmark.parentId == oldID) { - bookmark.parentId = newID; - } - }); - }); - } - - if (historyEvent.items.length == 0) { - onSuccess(); - } - }); - break; - } - } - } - } - }; - - // Subscribe to management events - GlobalEvents.on('DeleteBookmarkItem', deleteBookmarkItem); - GlobalEvents.on('HistoryUndo', undoHistoryEvent); - - // ------------------------------------------------------------------------ - // Public API - - return { - getModel: getModel, - unregisterModel: unregisterModel, - registerModel: registerModel, - - domModels: domModels, - bookmarks: bookmarks, - moveBookmark: moveBookmark, - moveBookmarkTo: moveBookmarkTo, - - deleteBookmarkList: deleteBookmarkList, - updateBookmarkItem: updateBookmarkItem, - }; - -})(); \ No newline at end of file +'use strict'; + +// ***************************************************************************** +// Custom Modules + +import { AppConfig } from '../config'; + +// Components +import { BookmarkManager } from '../session/bookmarks'; +import { WindowEvents, GlobalEvents } from '../utils/global-events'; + +// Utils +import { LimitCounter } from '../utils/general'; + +// ***************************************************************************** +// API + +export class SessionSyncDOMModel { + constructor(document) { + this.bookmarks = {}; + this.tabs = {}; + this.state = {}; + + // ------------------------------------------------------------------------ + // Events + + const OrderBookmarks = (indices) => { + var len = indices.length; + for (let i = 0; i < len; i++) { + var node = this.bookmarks[indices[i]]; + if (node) { + node.setVirtualPosition(SessionSyncModel.bookmarks[node.bookmarkID].index); + } + } + }; + + WindowEvents.on(document, 'BookmarksPositionChanged', OrderBookmarks); + + WindowEvents.on(document, 'BookmarkTitleChanged', (bookmarkID) => { + var node = this.bookmarks[bookmarkID]; + if (node) { + node.setTitle(SessionSyncModel.bookmarks[bookmarkID].title); + } + }); + + // ------------------------------------------------------------------------ + // Register + + SessionSyncModel.registerModel(document, this); + } + + getBookmark(id) { + return this.bookmarks[id]; + } + + setBookmark(id, bookmark) { + this.bookmarks[id] = bookmark; + } + + removeBookmark(id) { + if (this.bookmarks[id]) { + this.bookmarks[id].remove(); + delete this.bookmarks[id]; + } + } +} + +export const SessionSyncModel = (function SessionSync() { + + // Bookmarks storage + var bookmarks = {}; + + // Per document DOM cache + var domModels = new Map(); + + // ------------------------------------------------------------------------ + // API + + var getModel = function getModel(doc) { + var model = domModels.get(doc); + if ((model instanceof SessionSyncDOMModel) === false) { + model = new SessionSyncDOMModel(doc); + } + return model; + }; + + var registerModel = function registerModel(doc, model) { + if (model instanceof SessionSyncDOMModel) { + domModels.set(doc, model); + } + }; + + var unregisterModel = function unregisterModel(doc) { + var model = domModels.get(doc); + if (model instanceof SessionSyncDOMModel) { + domModels.delete(doc); + } + }; + + function onActionError() { + WindowEvents.emitLocal('Notification', { + message: 'Error when updating bookmarks' + }); + } + + function isSession(parentID) { + return parentID === AppConfig.get('storageID'); + } + + // Assumes moving in the same parent directory + var moveBookmark = function moveBookmark(idA, idB) { + var parentID = bookmarks[idA].parentId; + var indexA = bookmarks[idA].index; + var indexB = bookmarks[idB].index; + var direction = indexA > indexB ? 1 : -1; + + BookmarkManager.moveItem(idA, parentID, indexA < indexB ? indexB : indexB, onSuccessMove, onActionError); + + function onSuccessMove() { + var indices = []; + var min = Math.min(indexA, indexB); + var max = Math.max(indexA, indexB); + + for (var id in bookmarks) { + var B = bookmarks[id]; + if (B.parentId == parentID && B.index > min && B.index < max) { + indices.push(B.id); + B.index += direction; + } + } + + indices.push(idA); + indices.push(idB); + bookmarks[idA].index = indexB; + bookmarks[idB].index += direction; + + WindowEvents.broadcast(isSession(parentID) ? 'SessionsPositionChanged' : 'BookmarksPositionChanged', indices); + } + }; + + // Moving a bookmark to a new session directory + var moveBookmarkTo = function moveBookmarkTo(bookmarkID, folderID) { + var parentID = bookmarks[bookmarkID].parentId; + if (parentID == folderID) + return; + + BookmarkManager.moveItem(bookmarkID, folderID, undefined, onSuccess, onActionError); + + function onSuccess() { + bookmarks[bookmarkID].parentId = folderID; + + // TODO: set position based on the length of the new parent list + // TODO: trigger update for folderID + + var index = bookmarks[bookmarkID].index; + bookmarks[bookmarkID].index = -1; + + // Remove the bookmark from all UIs + domModels.forEach(function (model) { + model.removeBookmark(bookmarkID); + }); + + // Reorder all bookmarks after the removed one + var indices = []; + for (var id in bookmarks) { + var B = bookmarks[id]; + if (B.parentId == parentID && B.index > index) { + indices.push(B.id); + B.index--; + } + } + + // Trigger reordering + WindowEvents.broadcast('BookmarksPositionChanged', indices); + } + }; + + // all items must have the same parent + var deleteBookmarkList = function deleteBookmarkList(bookmarkIDs, parentID, callback) { + if (bookmarkIDs.length == 0) { + callback(); + return; + } + + var reorderList = []; + + // Get position ordered list of all bookmarks from the parrentID folder + for (var id in bookmarks) { + var B = bookmarks[id]; + if (B.parentId == parentID) { + reorderList[B.index] = B; + } + } + + var updateEvent = new LimitCounter(bookmarkIDs.length, onFinish); + + bookmarkIDs.forEach(function (bookmarkID) { + BookmarkManager.deleteItem(bookmarkID, false, onDeleteSuccess.bind(null, bookmarkID), updateEvent.advance); + }); + + function onFinish() { + // compute reordering + var indices = []; + var position = 0; + for (var i = 0; i < reorderList.length; i++) { + if (reorderList[i]) { + indices.push(reorderList[i].id); + reorderList[i].index = position; + position++; + } + } + + if (indices.length) { + // Trigger reordering + WindowEvents.broadcast('BookmarksPositionChanged', indices); + } + + callback(); + } + + function onDeleteSuccess(bookmarkID) { + var index = bookmarks[bookmarkID].index; + + // Remove the bookmark from all UIs + domModels.forEach(function (model) { + model.removeBookmark(bookmarkID); + }); + + delete reorderList[index]; + delete bookmarks[bookmarkID]; + GlobalEvents.emit('BookmarkRemoved'); + + updateEvent.advance(); + } + }; + + var deleteBookmarkItem = function deleteBookmarkItem(bookmarkID) { + var isFolder = bookmarks[bookmarkID].type === 'folder'; + BookmarkManager.deleteItem(bookmarkID, isFolder, onDeleteSuccess, onActionError); + + function onDeleteSuccess() { + var parentID = bookmarks[bookmarkID].parentId; + var index = bookmarks[bookmarkID].index; + + // Remove the bookmark from all UIs + domModels.forEach(function (model) { + model.removeBookmark(bookmarkID); + }); + + // Reorder all bookmarks after the removed one + var indices = []; + for (let id in bookmarks) { + let B = bookmarks[id]; + if (B.parentId == parentID && B.index > index) { + indices.push(B.id); + B.index--; + } + } + + // Save undo history + var undoEvent = { type: 'delete', bookmark: bookmarks[bookmarkID], items: [] }; + + if (isFolder) { + var folderID = bookmarks[bookmarkID].id; + for (let id in bookmarks) { + let B = bookmarks[id]; + if (B.parentId == folderID) { + undoEvent.items.push(Object.assign({}, B)); + } + } + } + + // Remove model references + delete bookmarks[bookmarkID]; + undoEvent.items.forEach(function (B) { + delete bookmarks[B.id]; + }); + + // Save undo event + var undoEvents = AppConfig.get('undo.events'); + undoEvents.push(undoEvent); + + AppConfig.set('undo.events', undoEvents); + GlobalEvents.emit('BookmarkDeleted', bookmarkID); + + // Trigger reordering + WindowEvents.broadcast('BookmarksPositionChanged', indices); + } + }; + + var updateBookmarkItem = function updateBookmarkItem(bookmarkID, options) { + try { + if (options.title) { + BookmarkManager.setTitle(bookmarks[bookmarkID], options.title, function () { + WindowEvents.broadcast('BookmarkTitleChanged', bookmarkID); + WindowEvents.broadcast('SessionDescriptionChanged', bookmarkID); + }); + } + + if (options.url) { + BookmarkManager.setLocation(bookmarks[bookmarkID], options.url); + } + + WindowEvents.emitLocal('Notification', { + message: 'Success' + }); + + } catch (err) { + console.log(err); + WindowEvents.emitLocal('Notification', { + message: 'Error while saving bookmark' + }); + } + }; + + // ------------------------------------------------------------------------ + // Events + + var recreateBookmark = function recreateBookmark(item) { + delete item.id; + delete item.dateAdded; + delete item.dateGroupModified; + delete item.title_lowercase; + return BookmarkManager.createBookmark(item); + }; + + var undoHistoryEvent = function undoHistoryEvent() { + var sessionID = undefined; + var undoEvents = AppConfig.get('undo.events'); + + function onSuccess() { + AppConfig.set('undo.events', undoEvents); + WindowEvents.emit(document, 'SetPromiseSession', { sessionID: sessionID, update: false }); + GlobalEvents.emit('update-sessions'); + } + + if (undoEvents.length) { + var historyEvent = undoEvents.pop(); + + if (historyEvent.type == 'delete') { + switch (historyEvent.bookmark.type) { + case 'bookmark': + case 'separator': { + recreateBookmark(historyEvent.bookmark) + .then(function (bookmark) { + sessionID = (bookmark.parentId == AppConfig.get('storageID')) ? bookmark.id : bookmark.parentId; + onSuccess(); + }); + break; + } + + case 'folder': { + // Restore deleted folder + var oldID = historyEvent.bookmark.id; + + recreateBookmark(historyEvent.bookmark) + .then(function (node) { + sessionID = node.id; + var newID = node.id; + var updateEvent = new LimitCounter(historyEvent.items.length, onSuccess); + + // correct parent for each bookmark + if (oldID != newID) { + // correct restore event + historyEvent.items.forEach(function (bookmark) { + bookmark.parentId = newID; + recreateBookmark(bookmark) + .then(updateEvent.advance, updateEvent.advance); + }); + + // correct all undo events + undoEvents.forEach(function (event) { + event.items.forEach(function (bookmark) { + if (bookmark.parentId == oldID) { + bookmark.parentId = newID; + } + }); + }); + } + + if (historyEvent.items.length == 0) { + onSuccess(); + } + }); + break; + } + } + } + } + }; + + // Subscribe to management events + GlobalEvents.on('DeleteBookmarkItem', deleteBookmarkItem); + GlobalEvents.on('HistoryUndo', undoHistoryEvent); + + // ------------------------------------------------------------------------ + // Public API + + return { + getModel: getModel, + unregisterModel: unregisterModel, + registerModel: registerModel, + + domModels: domModels, + bookmarks: bookmarks, + moveBookmark: moveBookmark, + moveBookmarkTo: moveBookmarkTo, + + deleteBookmarkList: deleteBookmarkList, + updateBookmarkItem: updateBookmarkItem, + }; + +})(); diff --git a/src/scripts/config.js b/src/scripts/config.js index 7329851..ba710b1 100644 --- a/src/scripts/config.js +++ b/src/scripts/config.js @@ -126,7 +126,7 @@ const Config = (function () { }; var onChange = function onChange(topic, callback) { - configEvents.subscribe(topic, callback) + configEvents.subscribe(topic, callback); }; // ------------------------------------------------------------------------ diff --git a/src/scripts/session-sync-ui.js b/src/scripts/session-sync-ui.js index d4379d4..05bb16f 100644 --- a/src/scripts/session-sync-ui.js +++ b/src/scripts/session-sync-ui.js @@ -37,7 +37,6 @@ import { SessionToolbar } from './components/session-toolbar'; // Bookmarks import { SessionManager } from './session/management'; import { BookmarkManager } from './session/bookmarks'; -import { BookmarkProperties } from '../common/bookmark-properties'; // ************************************************************************ // UI initilization diff --git a/src/scripts/utils/action-popup.js b/src/scripts/utils/action-popup.js index 5b2e257..579285a 100644 --- a/src/scripts/utils/action-popup.js +++ b/src/scripts/utils/action-popup.js @@ -1,100 +1,100 @@ -'use strict'; - -// ***************************************************************************** -// Custom Modules - -import { HTMLCreator } from './dom'; -import { WindowEvents } from './global-events'; - -// ***************************************************************************** -// ActionPopup - -export function ActionPopup(options) -{ - var DomElem = HTMLCreator(); - - var panel = DomElem('div', {class : 'action-popoup'}); - - var info = DomElem('div', {class : 'info'}); - var controls = DomElem('div', {class : 'controls'}); - - var ok = DomElem('div', {class : 'button ok'}); - ok.textContent = 'Confirm'; - - var cancel = DomElem('div', {class : 'button cancel'}); - cancel.textContent = 'Cancel'; - - var flexArea1 = DomElem('div', {class : 'flex'}); - var flexArea2 = DomElem('div', {class : 'flex'}); - - controls.appendChild(flexArea1); - controls.appendChild(ok); - controls.appendChild(cancel); - controls.appendChild(flexArea2); - panel.appendChild(info); - panel.appendChild(controls); - - var callback; - var defaultCallback = function defaultCallback() {}; - - // ------------------------------------------------------------------------ - // Events - - ok.addEventListener('click', function() { - callback(); - hidePanel(); - }); - - cancel.addEventListener('click', function() { - hidePanel(); - }); - - var openPanel = function openPanel(options) - { - panel.setAttribute('data-active', 'true'); - panel.style.left = options.event.pageX + 'px'; - panel.style.top = options.event.pageY + 'px'; - - if (typeof document !== 'undefined') { - document.addEventListener('mousedown', blurMenu); - } - - info.textContent = options.message; - setCallback(options.callback); - }; - - var hidePanel = function hidePanel() - { - panel.removeAttribute('data-active'); - if (typeof document !== 'undefined') { - document.removeEventListener('mousedown', blurMenu); - } - callback = defaultCallback; - }; - - var blurMenu = function hideWhenClickOutside(e) - { - var target = e.target; - while (target) { - if (target === panel) - return; - target = target.parentElement; - } - hidePanel(); - }; - - function setCallback(func) { - callback = typeof func === 'function' ? func : defaultCallback; - } - - // Register custom events - if (typeof document !== 'undefined') { - WindowEvents.on(document, options.name + '-Open', openPanel); - WindowEvents.on(document, 'ActionPopup-Hide', hidePanel); - } - - // ------------------------------------------------------------------------ - // Public properties - - this.DOMRoot = panel; -} \ No newline at end of file +'use strict'; + +// ***************************************************************************** +// Custom Modules + +import { HTMLCreator } from './dom'; +import { WindowEvents } from './global-events'; + +// ***************************************************************************** +// ActionPopup + +export function ActionPopup(options) +{ + var DomElem = HTMLCreator(); + + var panel = DomElem('div', {class : 'action-popoup'}); + + var info = DomElem('div', {class : 'info'}); + var controls = DomElem('div', {class : 'controls'}); + + var ok = DomElem('div', {class : 'button ok'}); + ok.textContent = 'Confirm'; + + var cancel = DomElem('div', {class : 'button cancel'}); + cancel.textContent = 'Cancel'; + + var flexArea1 = DomElem('div', {class : 'flex'}); + var flexArea2 = DomElem('div', {class : 'flex'}); + + controls.appendChild(flexArea1); + controls.appendChild(ok); + controls.appendChild(cancel); + controls.appendChild(flexArea2); + panel.appendChild(info); + panel.appendChild(controls); + + var callback; + var defaultCallback = function defaultCallback() {}; + + // ------------------------------------------------------------------------ + // Events + + ok.addEventListener('click', function() { + callback(); + hidePanel(); + }); + + cancel.addEventListener('click', function() { + hidePanel(); + }); + + var openPanel = function openPanel(options) + { + panel.setAttribute('data-active', 'true'); + panel.style.left = options.event.pageX + 'px'; + panel.style.top = options.event.pageY + 'px'; + + if (typeof document !== 'undefined') { + document.addEventListener('mousedown', blurMenu); + } + + info.textContent = options.message; + setCallback(options.callback); + }; + + var hidePanel = function hidePanel() + { + panel.removeAttribute('data-active'); + if (typeof document !== 'undefined') { + document.removeEventListener('mousedown', blurMenu); + } + callback = defaultCallback; + }; + + var blurMenu = function hideWhenClickOutside(e) + { + var target = e.target; + while (target) { + if (target === panel) + return; + target = target.parentElement; + } + hidePanel(); + }; + + function setCallback(func) { + callback = typeof func === 'function' ? func : defaultCallback; + } + + // Register custom events + if (typeof document !== 'undefined') { + WindowEvents.on(document, options.name + '-Open', openPanel); + WindowEvents.on(document, 'ActionPopup-Hide', hidePanel); + } + + // ------------------------------------------------------------------------ + // Public properties + + this.DOMRoot = panel; +} diff --git a/src/scripts/utils/field-edit-widget.js b/src/scripts/utils/field-edit-widget.js index b4d3870..917c2bb 100644 --- a/src/scripts/utils/field-edit-widget.js +++ b/src/scripts/utils/field-edit-widget.js @@ -118,10 +118,6 @@ export function FieldEditWdiget(options) { hideWidget(); } - function destroy() { - GlobalEvents.off('BookmarkRemoved', hideWidget); - } - // ------------------------------------------------------------------------ // Create UI From 29b6665aff8c2b742160952b11e648a223993e41 Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Thu, 16 Apr 2026 01:09:56 +0300 Subject: [PATCH 22/32] Prettier - refactoring --- .prettierrc | 9 +- assets/css/_style.css | 41 ++- assets/css/components.css | 51 ++-- assets/css/config-panel.css | 65 +++-- assets/css/confirm-box.css | 18 +- assets/css/context-menu.css | 63 +++-- assets/css/field-widget.css | 20 +- assets/css/filter-panel.css | 41 ++- assets/css/generic.css | 7 +- assets/css/hotkey-manager.css | 34 +-- assets/css/index.css | 2 +- assets/css/notification-system.css | 18 +- assets/css/search-bar.css | 28 +- assets/css/session-container.css | 48 ++-- assets/css/session-header-bar.css | 37 ++- assets/css/session-sorting.css | 24 +- assets/css/session-toolbar.css | 80 +++--- assets/css/sessions-list.css | 60 ++--- assets/css/tooltip-system.css | 12 +- assets/css/trash-can.css | 34 +-- assets/css/url-bar.css | 18 +- eslint.config.js | 37 +-- package.json | 2 +- public/_locales/en/messages.json | 2 +- public/home/home.css | 53 ++-- public/home/home.html | 252 ++++++++---------- public/home/home.js | 11 +- src/background/development.js | 3 +- src/background/favicon-service.js | 4 +- src/background/main.js | 59 ++-- src/background/session-history.js | 158 +++++------ src/background/session-management.js | 21 +- src/common/bookmark-properties.js | 4 +- src/entrypoints/popup/index.html | 24 +- src/scripts/3rd/pubsub.js | 73 ++--- src/scripts/app.js | 33 ++- src/scripts/components/config-panel.js | 60 ++--- src/scripts/components/notification-system.js | 2 +- src/scripts/components/overlay-system.js | 9 +- .../components/session-bookmark-events.js | 59 ++-- src/scripts/components/session-bookmark.js | 30 +-- src/scripts/components/session-container.js | 63 ++--- src/scripts/components/session-filtering.js | 11 +- .../components/session-folder-events.js | 34 +-- src/scripts/components/session-folder.js | 12 +- src/scripts/components/session-header-bar.js | 31 +-- src/scripts/components/session-history.js | 15 +- .../components/session-hotkey-manager.js | 8 +- src/scripts/components/session-hotkeys.js | 7 +- src/scripts/components/session-list.js | 223 +++++++++------- src/scripts/components/session-sorting.js | 7 +- src/scripts/components/session-sync-model.js | 71 +++-- src/scripts/components/session-toolbar.js | 10 +- src/scripts/components/tooltip-system.js | 2 +- src/scripts/components/trash-can.js | 3 +- src/scripts/config.js | 25 +- src/scripts/services/firefox-containers.js | 10 +- src/scripts/session-sync-ui.js | 12 +- src/scripts/session/bookmarks.js | 79 +++--- src/scripts/session/history.js | 5 +- src/scripts/session/management.js | 208 +++++++-------- src/scripts/utils/action-popup.js | 33 +-- src/scripts/utils/auto-scroll.js | 35 +-- src/scripts/utils/components.js | 31 +-- src/scripts/utils/confirm-box.js | 6 +- src/scripts/utils/context-menu.js | 3 +- src/scripts/utils/dom.js | 7 +- src/scripts/utils/drag-context.js | 3 +- src/scripts/utils/field-edit-widget.js | 3 +- src/scripts/utils/general.js | 4 +- src/scripts/utils/global-events.js | 6 +- wxt.config.js | 64 ++--- 72 files changed, 1248 insertions(+), 1389 deletions(-) diff --git a/.prettierrc b/.prettierrc index 8f25cf6..cd36e65 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,8 +1,9 @@ { "semi": true, "singleQuote": true, - "trailingComma": "es5", - "printWidth": 120, + "trailingComma": "none", + "printWidth": 160, "tabWidth": 4, - "useTabs": true -} \ No newline at end of file + "useTabs": true, + "endOfLine": "crlf" +} diff --git a/assets/css/_style.css b/assets/css/_style.css index 708137f..8b96daa 100644 --- a/assets/css/_style.css +++ b/assets/css/_style.css @@ -8,7 +8,7 @@ margin: 0; font-size: 12px; - font-family: "Open Sans", "Segoe UI", Arial, Helvetica, sans-serif; + font-family: 'Open Sans', 'Segoe UI', Arial, Helvetica, sans-serif; color: #000 !important; overflow: hidden; @@ -16,11 +16,11 @@ -moz-user-select: none; } -#session-sync[panel="false"] { +#session-sync[panel='false'] { position: absolute; } -#session-sync[panel="false"] .sessions-area .panel-resize-handle { +#session-sync[panel='false'] .sessions-area .panel-resize-handle { display: none; } @@ -42,7 +42,7 @@ display: none; } -#session-sync #overlay[state="true"] { +#session-sync #overlay[state='true'] { display: block; } @@ -50,9 +50,9 @@ * Panel Body */ - #session-sync #body { - height: 100%; - } +#session-sync #body { + height: 100%; +} #session-sync[config] .body { background-color: rgba(255, 255, 255, 0.95); @@ -70,18 +70,18 @@ flex-direction: column; } -#session-sync[show="false"] .sessions-area { +#session-sync[show='false'] .sessions-area { display: none; } -#session-sync[state="pin"] .sessions-area { +#session-sync[state='pin'] .sessions-area { top: 0; right: 0; margin: 0; transform: translate(0); } -#session-sync[state="centered"] .sessions-area { +#session-sync[state='centered'] .sessions-area { top: calc(50% - 50px); left: 50%; transform: translate(-50%, -50%); @@ -115,15 +115,14 @@ transition: none; } -#session-sync .sessions-area .panel-resize-handle -{ - content: " "; +#session-sync .sessions-area .panel-resize-handle { + content: ' '; width: 12px; height: 12px; margin-top: -12px; - background: url("images/resize.png") center no-repeat !important; + background: url('images/resize.png') center no-repeat !important; position: absolute; - background: #DDD; + background: #ddd; bottom: 0; left: 0px; cursor: sw-resize; @@ -132,18 +131,16 @@ transform: rotate(90deg); } -#session-sync .sessions-area .panel-resize-handle:hover -{ - background: #1AB61A; +#session-sync .sessions-area .panel-resize-handle:hover { + background: #1ab61a; } -#session-sync .sessions-area .panel-resize-handle[resizing] -{ - background: #2D92EB; +#session-sync .sessions-area .panel-resize-handle[resizing] { + background: #2d92eb; } #clipboard { position: absolute; top: -100px; z-index: -100; -} \ No newline at end of file +} diff --git a/assets/css/components.css b/assets/css/components.css index 0716747..97206b9 100644 --- a/assets/css/components.css +++ b/assets/css/components.css @@ -6,7 +6,7 @@ * Action Button */ - #session-sync .comp-button { +#session-sync .comp-button { width: 100%; color: #333; display: flex; @@ -18,8 +18,8 @@ #session-sync .comp-button .button { text-align: center; - background: #38B751; - color: #FFF; + background: #38b751; + color: #fff; flex: 1; } @@ -43,7 +43,7 @@ #session-sync .comp-toggle-switch:hover { cursor: pointer; - color: #4C91D6; + color: #4c91d6; } #session-sync .comp-toggle-switch .label { @@ -54,14 +54,14 @@ margin-left: 0.4em; padding: 0 0 0.25em 0; - background-color: #40B740; + background-color: #40b740; background-size: 1.6em; background-repeat: no-repeat; background-position: center right 0.2em; border-radius: 2em; text-align: center; - color: #FFF; + color: #fff; float: left; flex: 1; @@ -72,7 +72,7 @@ } #session-sync .comp-toggle-switch .switch:after { - content: " "; + content: ' '; width: 1.4em; height: 1.4em; margin: 0.3em 0 0 0.3em; @@ -84,22 +84,21 @@ transition-timing-function: ease-out; } - -#session-sync .comp-toggle-switch .switch[state="true"] { - background-color: #40B740; +#session-sync .comp-toggle-switch .switch[state='true'] { + background-color: #40b740; } -#session-sync .comp-toggle-switch .switch[state="true"]:after { +#session-sync .comp-toggle-switch .switch[state='true']:after { left: calc(100% - 1.7em); margin-left: 0; } -#session-sync .comp-toggle-switch .switch[state="false"]:after { - background: #E01616; +#session-sync .comp-toggle-switch .switch[state='false']:after { + background: #e01616; left: 0; } -#session-sync .comp-toggle-switch .switch[state="false"] { +#session-sync .comp-toggle-switch .switch[state='false'] { background-color: #999; background-position: center left 0.2em; } @@ -120,29 +119,29 @@ width: 50%; height: 100%; line-height: 2.4em; - background: #F5F5F5; + background: #f5f5f5; } #session-sync .comp-dropdown .select { height: 100%; line-height: 2.4em; width: 50%; - background: #38B751; + background: #38b751; position: relative; } #session-sync .comp-dropdown .select:hover { - background: #349E47; + background: #349e47; } #session-sync .comp-dropdown .select-value { height: 100%; - color: #FFF; + color: #fff; text-align: center; } #session-sync .comp-dropdown .select-value:hover { - background: #349E47; + background: #349e47; cursor: pointer; } @@ -157,7 +156,7 @@ #session-sync .comp-dropdown .drop-list { width: 100%; min-height: 100%; - background: #FFF; + background: #fff; color: #333; border: 1px solid #208020; @@ -170,11 +169,11 @@ box-sizing: border-box; } -#session-sync .comp-dropdown .drop-list[visible="true"] { +#session-sync .comp-dropdown .drop-list[visible='true'] { display: block; } -#session-sync .comp-dropdown .drop-list[visible="false"] { +#session-sync .comp-dropdown .drop-list[visible='false'] { display: none; } @@ -187,11 +186,11 @@ #session-sync .comp-dropdown .drop-list .option:hover { transition: none; - background: #EEE; + background: #eee; } #session-sync .comp-dropdown .drop-list .option[selected] { - background: #38B751; - background: #66AAEF; - color: #FFF; + background: #38b751; + background: #66aaef; + color: #fff; } diff --git a/assets/css/config-panel.css b/assets/css/config-panel.css index f193bf2..9acdf23 100644 --- a/assets/css/config-panel.css +++ b/assets/css/config-panel.css @@ -6,8 +6,8 @@ width: 350px; padding: 30px 0 0 0; font-size: 12px; - border-right: 1px solid #CCC; - background: #F5F5F5; + border-right: 1px solid #ccc; + background: #f5f5f5; opacity: 0.95; color: #000; @@ -17,7 +17,7 @@ bottom: 1px; right: -355px; z-index: 2000; - box-shadow: 0 0 10px 0 #CCC; + box-shadow: 0 0 10px 0 #ccc; transition: all 0.25s ease-out; opacity: 0.5; @@ -28,7 +28,7 @@ opacity: 1; } -#session-sync[show="false"] .config-panel { +#session-sync[show='false'] .config-panel { right: -355px !important; } @@ -39,8 +39,8 @@ #session-sync .config-panel .css-close-button { width: 2.4em; height: 2.4em; - top: .5em; - right: .5em; + top: 0.5em; + right: 0.5em; opacity: 0.85 !important; transition: all 0.2s ease-out; } @@ -60,7 +60,7 @@ margin: 2em auto; padding: 0; border: none; - background: url("images/icon64.png") center top no-repeat; + background: url('images/icon64.png') center top no-repeat; } #session-sync .config-panel .logo:hover { @@ -74,7 +74,7 @@ #session-sync .config-panel .section-container { height: auto; padding: 1em 0 0 0; - border-bottom: 1px dashed #CCC; + border-bottom: 1px dashed #ccc; border: none; position: relative; } @@ -85,7 +85,7 @@ font-weight: 600; text-align: left; color: #333; - border-bottom: 1px solid #DDD; + border-bottom: 1px solid #ddd; border-top: none; font-size: 14px; } @@ -107,13 +107,13 @@ width: 1.5em; height: 1.5em; margin: 0 0.5em; - background-image: url("images/arrow-up.png"); + background-image: url('images/arrow-up.png'); background-size: contain; float: left; } #session-sync .config-panel .section-container[collapsed] .drop-icon { - background-image: url("images/arrow-down.png"); + background-image: url('images/arrow-down.png'); background-size: contain; } @@ -134,8 +134,7 @@ box-sizing: border-box; } -#session-sync .config-panel .comp-range-control .label -{ +#session-sync .config-panel .comp-range-control .label { height: 100%; font-size: 1em; color: #555; @@ -148,7 +147,6 @@ box-sizing: border-box; } - #session-sync .config-panel .comp-range-control .range-input { width: 8em; margin: 0 1em 0 0; @@ -179,13 +177,12 @@ } #session-sync .config-panel .comp-range-control .button.inc { - background: url("images/arrow-right.png") center top no-repeat; + background: url('images/arrow-right.png') center top no-repeat; background-size: 100%; } -#session-sync .config-panel .comp-range-control .button.dec -{ - background: url("images/arrow-left.png") center top no-repeat; +#session-sync .config-panel .comp-range-control .button.dec { + background: url('images/arrow-left.png') center top no-repeat; background-size: 100%; } @@ -194,23 +191,23 @@ margin: 0 0.2em; padding: 0; - background: #FFF; - border: 1px solid #CCC; + background: #fff; + border: 1px solid #ccc; font-size: 1.1em; text-align: center; line-height: 1.8em; - color: #2D92EB; + color: #2d92eb; } /* * Toggle control */ - #session-sync .config-panel .comp-button, - #session-sync .config-panel .comp-dropdown, - #session-sync .config-panel .comp-toggle-switch, - #session-sync .config-panel .comp-toggle-button { +#session-sync .config-panel .comp-button, +#session-sync .config-panel .comp-dropdown, +#session-sync .config-panel .comp-toggle-switch, +#session-sync .config-panel .comp-toggle-button { width: 100%; height: 2em; margin: 5px 0; @@ -254,16 +251,16 @@ cursor: pointer; } -#session-sync .config-panel .comp-toggle-button .button[state="true"] { - background: #38B751; +#session-sync .config-panel .comp-toggle-button .button[state='true'] { + background: #38b751; } -#session-sync .config-panel .comp-toggle-button .button[state="false"] { - background: #66AAEF; +#session-sync .config-panel .comp-toggle-button .button[state='false'] { + background: #66aaef; } -#session-sync .config-panel .comp-toggle-button .button[state="disable"] { - background: #66AAEF; +#session-sync .config-panel .comp-toggle-button .button[state='disable'] { + background: #66aaef; } /* @@ -291,7 +288,7 @@ width: 8em; height: 100%; margin: 0 1em 0 0; - background: #38B751; + background: #38b751; border-radius: 1em; line-height: 1.9em; color: white; @@ -303,13 +300,13 @@ #session-sync .config-panel .comp-dropdown .select:hover { cursor: pointer; - background: #66AAEF; + background: #66aaef; } /* * Custom states */ -#session-sync[history="false"] .config-panel .section-container[id="history-config"] .comp-range-control { +#session-sync[history='false'] .config-panel .section-container[id='history-config'] .comp-range-control { display: none; } diff --git a/assets/css/confirm-box.css b/assets/css/confirm-box.css index 70f9149..0716d97 100644 --- a/assets/css/confirm-box.css +++ b/assets/css/confirm-box.css @@ -6,8 +6,8 @@ width: auto; padding: 15px; margin: -40px 0 0 0; - background: #FFF; - border: 1px #CCC solid; + background: #fff; + border: 1px #ccc solid; display: none; position: fixed; @@ -15,7 +15,7 @@ top: 0; overflow: auto; - box-shadow: 3px 3px 5px 0 #CCC; + box-shadow: 3px 3px 5px 0 #ccc; transform: translate(-50%, 0); z-index: 1; @@ -25,7 +25,7 @@ left: 321px; */ } -#session-sync .confirm-box[data-active="true"] { +#session-sync .confirm-box[data-active='true'] { display: block; z-index: 1001; } @@ -41,7 +41,7 @@ #session-sync .confirm-box .controls { padding: 10px 0 0 0; - border-top: 1px solid #DDD; + border-top: 1px solid #ddd; overflow: hidden; display: flex; } @@ -52,16 +52,16 @@ #session-sync .confirm-box .ok { margin: 0 10px 0 0; - color: #FFF; - background: #E86A5C; /*#EA5F4D;*/ + color: #fff; + background: #e86a5c; /*#EA5F4D;*/ } #session-sync .confirm-box .ok:hover { - background: #DC462C; + background: #dc462c; } #session-sync .confirm-box .cancel { - color: #FFF; + color: #fff; background: #999; } diff --git a/assets/css/context-menu.css b/assets/css/context-menu.css index 64dc537..359ffdb 100644 --- a/assets/css/context-menu.css +++ b/assets/css/context-menu.css @@ -5,22 +5,22 @@ #session-sync .context-menu { width: 180px; padding: 5px; - background: #FFF; - border: 1px #CCC solid; + background: #fff; + border: 1px #ccc solid; display: none; position: fixed; left: 0; top: 0; - box-shadow: 0px 0px 5px 0 #CCC; + box-shadow: 0px 0px 5px 0 #ccc; transform: translateX(5px); z-index: 3000; box-sizing: border-box; } -#session-sync .context-menu[data-active="true"] { +#session-sync .context-menu[data-active='true'] { display: block; } @@ -28,15 +28,14 @@ width: 100%; height: 1px; margin: 3px 0; - background: #CCC; + background: #ccc; } - #session-sync .context-menu .button { width: 100%; padding: 5px 5px 5px 30px; - background-image: url("images/icons/heart.png"); + background-image: url('images/icons/heart.png'); background-size: 20px; background-repeat: no-repeat; background-position: 3px 3px; @@ -44,57 +43,55 @@ } #session-sync .context-menu .button:hover { - background-color: #EEE; + background-color: #eee; cursor: pointer; } -#session-sync .context-menu .button[icon="save"] { - background-image: url("images/icons/heart.png"); +#session-sync .context-menu .button[icon='save'] { + background-image: url('images/icons/heart.png'); } -#session-sync .context-menu .button[icon="new-tab"] { - background-image: url("images/icons/ribbon.png"); +#session-sync .context-menu .button[icon='new-tab'] { + background-image: url('images/icons/ribbon.png'); } -#session-sync .context-menu .button[icon="same-tab"] { - background-image: url("images/icons/recycle.png"); +#session-sync .context-menu .button[icon='same-tab'] { + background-image: url('images/icons/recycle.png'); } -#session-sync .context-menu .button[icon="new-window"] { - background-image: url("images/icons/frames.png"); +#session-sync .context-menu .button[icon='new-window'] { + background-image: url('images/icons/frames.png'); } -#session-sync .context-menu .button[icon="replace-current"] { - background-image: url("images/material_icons/repeat_one.svg"); +#session-sync .context-menu .button[icon='replace-current'] { + background-image: url('images/material_icons/repeat_one.svg'); } -#session-sync .context-menu .button[icon="replace-all"] { - background-image: url("images/material_icons/repeat.svg"); +#session-sync .context-menu .button[icon='replace-all'] { + background-image: url('images/material_icons/repeat.svg'); } -#session-sync .context-menu .button[icon="copy"] { - background-image: url("images/icons/clipboard.png"); +#session-sync .context-menu .button[icon='copy'] { + background-image: url('images/icons/clipboard.png'); } -#session-sync .context-menu .button[icon="edit"] { - background-image: url("images/icons/document.png"); +#session-sync .context-menu .button[icon='edit'] { + background-image: url('images/icons/document.png'); } -#session-sync .context-menu .button[icon="delete"] { - background-image: url("images/icons/denied.png"); +#session-sync .context-menu .button[icon='delete'] { + background-image: url('images/icons/denied.png'); } -#session-sync .context-menu .button[icon="same-window"] { - background-image: url("images/icons/stack.png"); +#session-sync .context-menu .button[icon='same-window'] { + background-image: url('images/icons/stack.png'); } -#session-sync .context-menu .button[icon="new-session"] { - background-image: url("images/icons/star.png"); +#session-sync .context-menu .button[icon='new-session'] { + background-image: url('images/icons/star.png'); } -#session-sync .context-menu[icons="no"] .button -{ +#session-sync .context-menu[icons='no'] .button { padding: 5px; background-image: unset; } - diff --git a/assets/css/field-widget.css b/assets/css/field-widget.css index e83be9e..f5ed17e 100644 --- a/assets/css/field-widget.css +++ b/assets/css/field-widget.css @@ -4,15 +4,15 @@ #session-sync .field-edit-widget { width: 100%; padding: 10px; - background: #FFF; - border: 1px solid #CCC; + background: #fff; + border: 1px solid #ccc; opacity: 0; position: absolute; box-sizing: border-box; - box-shadow: 0px 0px 5px 0px #CCC; + box-shadow: 0px 0px 5px 0px #ccc; bottom: 0; left: 0; @@ -29,7 +29,7 @@ width: 100%; height: 2.4em; margin: 3px 0; - line-height: 2.0em; + line-height: 2em; float: left; display: flex; } @@ -47,13 +47,13 @@ padding: 0 1em 0.2em 1em !important; color: #333; box-sizing: border-box; - border: 1px solid #CCC; + border: 1px solid #ccc; border-radius: 1.2em !important; flex: 1; } #session-sync .field-edit-widget input:focus { - border-color: #2D92EB !important; + border-color: #2d92eb !important; box-shadow: 0 0 3px 0 rgba(0, 131, 255, 0.5) inset !important; } @@ -65,7 +65,7 @@ font-size: 14px; text-align: center; - color: #FFF; + color: #fff; border: unset; border-radius: 1em; background: #999; @@ -75,14 +75,14 @@ } #session-sync .field-edit-widget .button:hover { - color: #FFF; + color: #fff; cursor: pointer; } #session-sync .field-edit-widget .button.save:hover { - background: #23A823; + background: #23a823; } #session-sync .field-edit-widget .button.cancel:hover { - background: #DC462C; + background: #dc462c; } diff --git a/assets/css/filter-panel.css b/assets/css/filter-panel.css index 4a55421..58d7811 100644 --- a/assets/css/filter-panel.css +++ b/assets/css/filter-panel.css @@ -7,8 +7,8 @@ height: 100%; min-height: 10000px; font-size: 16px; - border-right: 1px solid #DDD; - background: #F5F5F5; + border-right: 1px solid #ddd; + background: #f5f5f5; opacity: 0.95; color: #000; @@ -20,11 +20,11 @@ transition: all 0.3s; } -#session-sync[show="true"] .filter-panel { +#session-sync[show='true'] .filter-panel { left: 0px; } -#session-sync[show="false"] .filter-panel { +#session-sync[show='false'] .filter-panel { left: -151px; } @@ -36,15 +36,14 @@ margin: 10px 0; height: 64px; width: 100%; - background: url("images/icon64.png") center top no-repeat; + background: url('images/icon64.png') center top no-repeat; } #session-sync .filter-panel .separator { margin: 5px 0; - border-bottom: 1px solid #DDD; + border-bottom: 1px solid #ddd; } - #session-sync .filter-panel .session-box { padding: 5px 0; color: #555; @@ -52,13 +51,13 @@ } #session-sync .filter-panel .session-box:hover { - background: #DDD; + background: #ddd; cursor: pointer; } #session-sync .filter-panel .session-box[active] { - background: #2D92EB; - color: #FFF; + background: #2d92eb; + color: #fff; } #session-sync .filter-panel .filters { @@ -91,10 +90,9 @@ display: block; } - #session-sync .filter-panel .label:hover { cursor: pointer; - background: #FFF; + background: #fff; transition: none; } @@ -112,7 +110,7 @@ height: 12px; margin: 3px 10px 3px 0; padding: 3px 0; - background: #DDD; + background: #ddd; border-radius: 3px; text-align: center; line-height: 100%; @@ -126,27 +124,27 @@ } #session-sync .filter-panel .label[active] { - background: #FFF; + background: #fff; } #session-sync .filter-panel .label[active] .name { - color: #4C91D6; + color: #4c91d6; font-weight: bold; } #session-sync .filter-panel .label[active] .count { - background: #23A823; - color: #FFF; + background: #23a823; + color: #fff; } #session-sync .filter-panel .label[active]:after { - content: " "; + content: ' '; width: 3px; height: 100%; position: absolute; right: 0; top: 0; - background: #1AB61A; + background: #1ab61a; } #session-sync .filter-panel .add-label { @@ -159,7 +157,6 @@ #session-sync .filter-panel .add-label:hover { cursor: pointer; - background: #2D92EB; - color: #FFF; + background: #2d92eb; + color: #fff; } - diff --git a/assets/css/generic.css b/assets/css/generic.css index ad22b8a..2e24c1e 100644 --- a/assets/css/generic.css +++ b/assets/css/generic.css @@ -2,7 +2,6 @@ * Generic CSS */ - #session-sync .css-close-button { width: 2em; height: 2em; @@ -27,10 +26,10 @@ #session-sync .css-close-button:after, #session-sync .css-close-button:before { - content: ""; + content: ''; width: 60%; height: 16%; - background: #AAA; + background: #aaa; position: absolute; top: 42%; left: 20%; @@ -39,4 +38,4 @@ #session-sync .css-close-button:after { transform: rotate(-45deg); -} \ No newline at end of file +} diff --git a/assets/css/hotkey-manager.css b/assets/css/hotkey-manager.css index 22138e5..484ad9f 100644 --- a/assets/css/hotkey-manager.css +++ b/assets/css/hotkey-manager.css @@ -2,25 +2,25 @@ * Session Hotkey Manager */ - #session-sync .hotkey-overlay { +#session-sync .hotkey-overlay { position: absolute; background: rgba(0, 0, 0, 0.85); width: 100%; height: 100%; z-index: 3000; display: none; - } +} - #session-sync .hotkey-overlay[state="true"] { +#session-sync .hotkey-overlay[state='true'] { display: block; - } +} #session-sync .hotkey-manager { width: 300px; padding: 10px; font-size: 12px; - border: 1px solid #CCC; - background: #FFF; + border: 1px solid #ccc; + background: #fff; color: #000; position: absolute; @@ -38,7 +38,7 @@ margin: 0 0 0.5em 0; font-size: 1.4em; text-align: center; - background: #F5F5F5; + background: #f5f5f5; color: #555; } @@ -47,13 +47,13 @@ } #session-sync .hotkey-manager .info-box .message { - color: #DC462C; + color: #dc462c; text-align: center; font-weight: bold; } #session-sync .hotkey-manager .info-box .link { - color: #509AE5; + color: #509ae5; text-align: center; font-weight: bold; } @@ -65,7 +65,7 @@ #session-sync .hotkey-manager .hotkey-info { width: 100%; margin: 10px auto; - border: 1px solid #DDD; + border: 1px solid #ddd; text-align: center; color: #555; @@ -80,9 +80,9 @@ #session-sync .hotkey-manager .hotkey-info .title { width: 30%; - background: #38B751; + background: #38b751; background: #999; - color: #FFF; + color: #fff; } #session-sync .hotkey-manager .hotkey-info .value { @@ -142,16 +142,16 @@ #session-sync .hotkey-manager .action-bar .ok { margin: 0 10px 0 0; - color: #FFF; - background: #E86A5C; /*#EA5F4D;*/ + color: #fff; + background: #e86a5c; /*#EA5F4D;*/ } #session-sync .hotkey-manager .action-bar .ok:hover { - background: #DC462C; + background: #dc462c; } #session-sync .hotkey-manager .action-bar .cancel { - color: #FFF; + color: #fff; background: #999; } @@ -170,4 +170,4 @@ #session-sync .hotkey-manager .action-bar .button:hover { cursor: pointer; -} \ No newline at end of file +} diff --git a/assets/css/index.css b/assets/css/index.css index fb6b156..43e6b61 100644 --- a/assets/css/index.css +++ b/assets/css/index.css @@ -16,4 +16,4 @@ @import './sessions-list.css'; @import './tooltip-system.css'; @import './trash-can.css'; -@import './url-bar.css'; \ No newline at end of file +@import './url-bar.css'; diff --git a/assets/css/notification-system.css b/assets/css/notification-system.css index 311e7b7..3958f51 100644 --- a/assets/css/notification-system.css +++ b/assets/css/notification-system.css @@ -14,13 +14,12 @@ transform: translateX(-50%); } -#session-sync .notification-system .info -{ +#session-sync .notification-system .info { width: 100%; padding: 0.5em 2em; padding-left: 3.5em; - background: #2D92EB; - color: #FFF; + background: #2d92eb; + color: #fff; /* background: #F5F5F5; */ /* color: #333; */ word-break: break-all; @@ -35,21 +34,20 @@ transform: translateX(0); } -#session-sync .notification-system .info[active] -{ +#session-sync .notification-system .info[active] { opacity: 1; top: -1px; - border: 1px solid #CCC; + border: 1px solid #ccc; } #session-sync .notification-system .info:after { - content: " "; + content: ' '; width: 24px; height: 24px; - background: url("images/logo.png") center no-repeat; + background: url('images/logo.png') center no-repeat; background-size: contain; top: 50%; left: 20px; position: absolute; transform: translateY(-50%); -} \ No newline at end of file +} diff --git a/assets/css/search-bar.css b/assets/css/search-bar.css index d16b13f..73dba4e 100644 --- a/assets/css/search-bar.css +++ b/assets/css/search-bar.css @@ -24,15 +24,15 @@ transition: all 0.2s ease-out; } -#session-sync .search-bar[content="false"] { +#session-sync .search-bar[content='false'] { width: 5em !important; } -#session-sync .search-bar[content="false"] .search { +#session-sync .search-bar[content='false'] .search { text-align: center; } -#session-sync .search-bar[active="true"] { +#session-sync .search-bar[active='true'] { width: 25%; opacity: 1; } @@ -46,7 +46,7 @@ height: 100%; padding: 0 0.75em !important; background: hsla(0, 0%, 100%, 0.95); - border: 1px solid #CCC; + border: 1px solid #ccc; border-radius: 1em; color: #333; font-size: 0.75em; @@ -56,22 +56,22 @@ } #session-sync .search-bar .search:focus { - border-color: #2D92EB !important; + border-color: #2d92eb !important; } -#session-sync .search-bar[match="true"][content="true"] .search { +#session-sync .search-bar[match='true'][content='true'] .search { color: green; } #session-sync .search-bar .create-session { width: 1em; height: 1em; - background-image: url("images/icons/star.png"); + background-image: url('images/icons/star.png'); background-size: 100%; background-repeat: no-repeat; background-position: center; text-align: center; - color: #FFF; + color: #fff; opacity: 0; position: absolute; @@ -84,7 +84,7 @@ box-sizing: border-box; } -#session-sync .search-bar[match="false"][content="true"] .create-session { +#session-sync .search-bar[match='false'][content='true'] .create-session { opacity: 0.6; display: block; right: 1.6em; @@ -99,7 +99,7 @@ #session-sync .search-bar .close { width: 1.2em; height: 1.2em; - background: #FFF; + background: #fff; border-radius: 2em; opacity: 1; @@ -109,10 +109,10 @@ z-index: 0; box-sizing: border-box; - transition: opacity 0.2s ease-out; + transition: opacity 0.2s ease-out; } -#session-sync .search-bar[active="true"][content="true"] .close { +#session-sync .search-bar[active='true'][content='true'] .close { opacity: 0.75; right: 0.3em; } @@ -128,10 +128,10 @@ #session-sync .search-bar .close:after, #session-sync .search-bar .close:before { - content: ""; + content: ''; width: 60%; height: 16%; - background: #AAA; + background: #aaa; position: absolute; top: 42%; left: 20%; diff --git a/assets/css/session-container.css b/assets/css/session-container.css index cc15f40..6549445 100644 --- a/assets/css/session-container.css +++ b/assets/css/session-container.css @@ -4,7 +4,7 @@ #session-sync .session-container { height: 100%; - background: #FEFEFE; + background: #fefefe; font-size: 14px; float: right; @@ -31,14 +31,14 @@ height: 3.4em; width: 3.4em; - background: url("images/icons/heart.png") no-repeat; + background: url('images/icons/heart.png') no-repeat; background-size: 2em; background-position: 1em 0.4em; - background-color: #F5F5F5; + background-color: #f5f5f5; border-radius: 0 0 0 100%; - border-left: 1px solid #CCC; - border-bottom: 1px solid #CCC; + border-left: 1px solid #ccc; + border-bottom: 1px solid #ccc; position: absolute; z-index: 1; @@ -50,7 +50,7 @@ #session-sync .session-container .save-session:hover { cursor: pointer; - background-color: #FFF; + background-color: #fff; filter: saturate(100%); } @@ -78,7 +78,7 @@ #session-sync .session-container .window-separator { width: 100%; height: 2em; - background: #F8F8F8; + background: #f8f8f8; color: #555; line-height: 1.9em; @@ -98,7 +98,7 @@ } #session-sync .session-container .window-separator:hover:after { - content: "(switch to)"; + content: '(switch to)'; position: absolute; font-size: 0.8em; top: 0; @@ -128,23 +128,23 @@ } #session-sync .session-container .bookmark:hover { - background: #EEE; + background: #eee; cursor: pointer; transition-property: left, top; } #session-sync .session-container .bookmark[highlight] { transition-duration: 0.5s; - background: #4396E9 !important; - color: #FFF; + background: #4396e9 !important; + color: #fff; } #session-sync .session-container .bookmark[dragging] { width: 400px; margin: 1px 0 0 1px; - background-color: #FBD4B0; - background: #4396E9 !important; - color: #FFF; + background-color: #fbd4b0; + background: #4396e9 !important; + color: #fff; opacity: 0.9; transition: none; @@ -152,9 +152,9 @@ z-index: 1000; } -#session-sync .session-container .bookmark[dragging="drop"] { - background: #4396E9 !important; - color: #FFF; +#session-sync .session-container .bookmark[dragging='drop'] { + background: #4396e9 !important; + color: #fff; opacity: 0.9; transition: none; position: absolute; @@ -167,10 +167,10 @@ } #session-sync .session-container .bookmark[pinned]:before { - content: ""; + content: ''; width: 1.5em; height: 100%; - background-image: url("images/icons/pin.png"); + background-image: url('images/icons/pin.png'); background-size: contain; background-repeat: no-repeat; background-position: center center; @@ -189,7 +189,7 @@ #session-sync .session-container .bookmark .favicon { width: 1.33em; height: 100%; - background: url("images/icons/globe.png"); + background: url('images/icons/globe.png'); background-size: contain; background-repeat: no-repeat; background-position: center center; @@ -216,7 +216,7 @@ margin-right: 2em; max-width: 10em; border-radius: 1em; - color: #FFF; + color: #fff; padding: 0 0.5em 0 0.5em; white-space: nowrap; overflow: hidden; @@ -227,7 +227,7 @@ #session-sync .session-container .bookmark .container .name { height: 100%; - padding: 0 0.5em 0 .1em; + padding: 0 0.5em 0 0.1em; line-height: 1.5em; } @@ -248,7 +248,7 @@ width: 100%; height: 2em; line-height: 1.9em; - background: #F8F8F8; + background: #f8f8f8; color: #555; position: absolute; transition: all 0.2s; @@ -262,4 +262,4 @@ cursor: pointer; text-decoration: underline; color: #333; -} \ No newline at end of file +} diff --git a/assets/css/session-header-bar.css b/assets/css/session-header-bar.css index cc4d254..3c32a9e 100644 --- a/assets/css/session-header-bar.css +++ b/assets/css/session-header-bar.css @@ -6,8 +6,8 @@ width: 100%; height: 2.5em; - background: #F5F5F5; - border-bottom: 1px solid #DDD; + background: #f5f5f5; + border-bottom: 1px solid #ddd; line-height: 2.4em; font-size: 18px; position: relative; @@ -21,7 +21,7 @@ #session-sync .header-bar .active-session-btn { width: 2.5em; - background: url("images/icon128.png") no-repeat center; + background: url('images/icon128.png') no-repeat center; background-size: 1em; height: 60%; opacity: 0.5; @@ -35,7 +35,7 @@ display: none; } -#session-sync[session="current"] .header-bar .active-session-btn, +#session-sync[session='current'] .header-bar .active-session-btn, #session-sync .header-bar .active-session-btn:hover { background-size: 1.2em; cursor: pointer; @@ -65,7 +65,7 @@ #session-sync .header-bar .addon-config { width: 1.8em; height: 1.8em; - background-image: url("images/menu.png"); + background-image: url('images/menu.png'); background-size: 55%; background-repeat: no-repeat; background-position: center center; @@ -81,8 +81,8 @@ #session-sync .header-bar .addon-config:hover { cursor: pointer; - background-color: #EEE; - border: 1px solid #CCC; + background-color: #eee; + border: 1px solid #ccc; opacity: 1; } @@ -93,7 +93,7 @@ #session-sync .header-bar .header-menu { width: 1.8em; height: 1.8em; - background-image: url("images/icons_v2/menu.png"); + background-image: url('images/icons_v2/menu.png'); background-size: 75% 80%; background-repeat: no-repeat; background-position: center center; @@ -106,15 +106,14 @@ box-sizing: border-box; } - #session-sync .header-bar .header-menu:hover { cursor: pointer; - background-color: #EEE; - border: 1px solid #CCC; + background-color: #eee; + border: 1px solid #ccc; opacity: 1; } -#session-sync .header-bar .header-menu[active="1"] { +#session-sync .header-bar .header-menu[active='1'] { opacity: 1; } @@ -122,7 +121,7 @@ height: 0; margin: 0; padding: 0; - background: #FFF; + background: #fff; font-size: 0.75em; border: 1px solid rgba(0, 0, 0, 0); position: absolute; @@ -140,10 +139,10 @@ transition-timing-function: ease-out; } -#session-sync .header-bar .header-menu[active="1"] .menu-area { +#session-sync .header-bar .header-menu[active='1'] .menu-area { height: auto; padding: 1em; - border: 1px solid #CCC; + border: 1px solid #ccc; opacity: 1; } @@ -161,7 +160,7 @@ height: 3.5em; margin: 0 0 0 0.6em; padding: 0.5em 0; - border: 1px solid #FFF; + border: 1px solid #fff; box-sizing: content-box; flex: 1; @@ -173,13 +172,13 @@ #session-sync .header-bar .header-menu .menu-button:hover { cursor: pointer; - background-color: #F5F5F5; - border-color: #DDD; + background-color: #f5f5f5; + border-color: #ddd; } #session-sync .header-bar .header-menu .menu-button .icon { height: 2em; - background-image: url("images/icons/power.png"); + background-image: url('images/icons/power.png'); background-size: contain !important; background-repeat: no-repeat; background-position: center; diff --git a/assets/css/session-sorting.css b/assets/css/session-sorting.css index 827ca58..79a74d1 100644 --- a/assets/css/session-sorting.css +++ b/assets/css/session-sorting.css @@ -8,7 +8,7 @@ padding: 0; display: flex; - background-image: url("images/sort.png"); + background-image: url('images/sort.png'); background-size: 20px; background-repeat: no-repeat; background-position: center center; @@ -22,7 +22,7 @@ box-sizing: border-box; } -#session-sync[list="history"] .sorting-method { +#session-sync[list='history'] .sorting-method { display: none; } @@ -31,13 +31,13 @@ opacity: 1; } -#session-sync .sorting-method[active="true"] { +#session-sync .sorting-method[active='true'] { opacity: 1; } -#session-sync .sorting-method[active="true"] .dropdown { +#session-sync .sorting-method[active='true'] .dropdown { display: block; - background: #FFF; + background: #fff; } #session-sync .sorting-method .dropdown { @@ -61,9 +61,9 @@ #session-sync .sorting-method .dropdown .option-list { width: 100%; - border: 1px solid #CCC; + border: 1px solid #ccc; border-bottom: none; - background: #FFF; + background: #fff; position: absolute; bottom: 0; @@ -79,18 +79,16 @@ box-sizing: border-box; transition-property: left, top, color, background; transition-duration: 0.2s; - } #session-sync .sorting-method .dropdown .option-list .option:hover { - background: #EEE; - border-color: #CCC; + background: #eee; + border-color: #ccc; cursor: pointer; transition-property: left, top; } #session-sync .sorting-method .dropdown .option-list .option[active] { - color: #FFF; - background: #2D92EB; + color: #fff; + background: #2d92eb; } - diff --git a/assets/css/session-toolbar.css b/assets/css/session-toolbar.css index dcee7d2..ecb1a40 100644 --- a/assets/css/session-toolbar.css +++ b/assets/css/session-toolbar.css @@ -6,15 +6,15 @@ width: 100%; height: 3em; - background: #FFF; + background: #fff; color: #000; display: flex; font-size: 12px; position: relative; opacity: 100; - border-bottom: 1px solid #CCC; - box-shadow: 0px 0px 3px 0px #CCC; + border-bottom: 1px solid #ccc; + box-shadow: 0px 0px 3px 0px #ccc; z-index: 1; transition: all 0.15s; @@ -27,7 +27,7 @@ height: 100%; margin: 0; padding: 0; - border-right: 1px solid #DDD; + border-right: 1px solid #ddd; line-height: 3em; word-break: break-all; @@ -51,22 +51,22 @@ } #session-sync .session-toolbar .session-selector .active-session { - background-image: url("images/icon128.png"); + background-image: url('images/icon128.png'); background-repeat: no-repeat; background-position: center center; background-size: 1.8em; - background-color: #FFF; + background-color: #fff; } -#session-sync[list="active"] .session-toolbar .session-selector .active-session, -#session-sync[list="sync"] .session-toolbar .session-selector .sync, -#session-sync[list="history"] .session-toolbar .session-selector .history { +#session-sync[list='active'] .session-toolbar .session-selector .active-session, +#session-sync[list='sync'] .session-toolbar .session-selector .sync, +#session-sync[list='history'] .session-toolbar .session-selector .history { display: block; - background-color: #2D92EB; - color: #FFF; + background-color: #2d92eb; + color: #fff; } -#session-sync[history="false"] .session-toolbar .session-selector .history { +#session-sync[history='false'] .session-toolbar .session-selector .history { display: none; } @@ -74,7 +74,7 @@ * Session info */ -#session-sync:not([session="restore"]) .session-toolbar .session-date { +#session-sync:not([session='restore']) .session-toolbar .session-date { display: none; } @@ -101,7 +101,7 @@ flex: 1; } -#session-sync .session-toolbar[session-type="separator"] .menu-bar { +#session-sync .session-toolbar[session-type='separator'] .menu-bar { display: none; } @@ -109,7 +109,7 @@ width: 2.6em; height: 100%; - background: url("images/icons/none.png") no-repeat center; + background: url('images/icons/none.png') no-repeat center; background-size: contain !important; display: none; @@ -129,68 +129,68 @@ height: 100%; width: 1px; margin: 0 0.5em; - background: #CCC; + background: #ccc; float: left; display: none; } #session-sync .session-toolbar .menu-bar .add { - background: url("images/icons/star.png") no-repeat center; + background: url('images/icons/star.png') no-repeat center; } #session-sync .session-toolbar .menu-bar .save { - background: url("images/icons/heart.png") no-repeat center; + background: url('images/icons/heart.png') no-repeat center; } #session-sync .session-toolbar .menu-bar .merge-sessions { - background: url("images/icons/merge.png") no-repeat center; + background: url('images/icons/merge.png') no-repeat center; } #session-sync .session-toolbar .menu-bar .replace-session { - background: url("images/icons/recycle.png") no-repeat center; + background: url('images/icons/recycle.png') no-repeat center; } #session-sync .session-toolbar .menu-bar .restore { - background: url("images/icons/stack.png") no-repeat center; + background: url('images/icons/stack.png') no-repeat center; } #session-sync .session-toolbar .menu-bar .restore-new-win { - background: url("images/icons/frames.png") no-repeat center; + background: url('images/icons/frames.png') no-repeat center; } #session-sync .session-toolbar .menu-bar .repalce-session-all { - background: url("images/material_icons/repeat.svg") no-repeat center; + background: url('images/material_icons/repeat.svg') no-repeat center; } #session-sync .session-toolbar .menu-bar .repalce-session { - background: url("images/material_icons/repeat_one.svg") no-repeat center; + background: url('images/material_icons/repeat_one.svg') no-repeat center; } #session-sync .session-toolbar .menu-bar .fix-session { - background: url("images/icons/tools.png") no-repeat center; + background: url('images/icons/tools.png') no-repeat center; } #session-sync .session-toolbar .menu-bar .customize-toolbar { - background: url("images/icons_v2/more.png") no-repeat center; + background: url('images/icons_v2/more.png') no-repeat center; } /* * Menu state */ -#session-sync[session="current"] .session-toolbar .menu-bar .button[state-current="true"] { +#session-sync[session='current'] .session-toolbar .menu-bar .button[state-current='true'] { display: block; } -#session-sync[session="restore"] .session-toolbar .menu-bar .button[state-restore="true"] { +#session-sync[session='restore'] .session-toolbar .menu-bar .button[state-restore='true'] { display: block; } -#session-sync[session="restore"] .session-toolbar .menu-bar .separator { +#session-sync[session='restore'] .session-toolbar .menu-bar .separator { display: block; } -#session-sync[session="history"] .session-toolbar .menu-bar .button[state-history="true"] { +#session-sync[session='history'] .session-toolbar .menu-bar .button[state-history='true'] { display: block; } @@ -213,11 +213,11 @@ opacity: 1; } -#session-sync[session="restore"] .session-toolbar .menu-bar .save-config { +#session-sync[session='restore'] .session-toolbar .menu-bar .save-config { display: none; } -#session-sync[session="history"] .session-toolbar .menu-bar .save-config .comp-toggle-switch[windows] { +#session-sync[session='history'] .session-toolbar .menu-bar .save-config .comp-toggle-switch[windows] { display: none; } @@ -235,8 +235,8 @@ #session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch .switch { width: 2.6em; height: 100%; - background-color: #40B740; - background-image: url("images/icons/gear.png"); + background-color: #40b740; + background-image: url('images/icons/gear.png'); background-size: 1.6em; background-repeat: no-repeat; background-position: center right 0.2em; @@ -250,19 +250,19 @@ display: none; } -#session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch .switch[state="false"] { - background-color: #BBB; +#session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch .switch[state='false'] { + background-color: #bbb; background-position: center left 0.2em; } #session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch[pin] .switch { - background-image: url("images/icons/pin.png"); + background-image: url('images/icons/pin.png'); } #session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch[pin-state] .switch { - background-image: url("images/icons/pin2.png"); + background-image: url('images/icons/pin2.png'); } #session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch[windows] .switch { - background-image: url("images/icons/stack.png"); -} \ No newline at end of file + background-image: url('images/icons/stack.png'); +} diff --git a/assets/css/sessions-list.css b/assets/css/sessions-list.css index 849586f..04f6655 100644 --- a/assets/css/sessions-list.css +++ b/assets/css/sessions-list.css @@ -11,9 +11,9 @@ border-width: 0 1px 0 0; border-style: solid; - border-color: #CCC; + border-color: #ccc; background: hsla(0, 0%, 100%, 0.95); - background: #FEFEFE; + background: #fefefe; float: left; overflow-x: hidden; @@ -30,14 +30,13 @@ * List resize */ -#session-sync .sessions .resize-handle -{ - content: " "; +#session-sync .sessions .resize-handle { + content: ' '; width: 4px; height: 24px; margin-top: -12px; position: absolute; - background: #DDD; + background: #ddd; top: 50%; border: unset; right: 0px; @@ -45,21 +44,19 @@ z-index: 10; } -#session-sync .sessions .resize-handle:hover -{ - background: #1AB61A; +#session-sync .sessions .resize-handle:hover { + background: #1ab61a; } -#session-sync .sessions .resize-handle[resizing] -{ - background: #2D92EB !important; +#session-sync .sessions .resize-handle[resizing] { + background: #2d92eb !important; } /* * History list */ -#session-sync[list="history"] .sessions .history-list { +#session-sync[list='history'] .sessions .history-list { flex: 1; } @@ -95,21 +92,21 @@ } #session-sync .sessions .history-node:hover { - background: #EEE; + background: #eee; cursor: pointer; transition-property: left, top; } -#session-sync .sessions .history-node[active="true"] { - background: #2D92EB; - color: #FFF; +#session-sync .sessions .history-node[active='true'] { + background: #2d92eb; + color: #fff; } /* * Session list */ -#session-sync[list="history"] .sessions .session-list { +#session-sync[list='history'] .sessions .session-list { flex: 0; } @@ -150,7 +147,7 @@ } #session-sync .sessions .folder:hover { - background: #EEE; + background: #eee; cursor: pointer; transition-property: left, top; } @@ -158,8 +155,8 @@ #session-sync .sessions .folder[dragging] { width: 200px; margin: 1px 0 0 1px; - background: #4396E9 !important; - color: #FFF; + background: #4396e9 !important; + color: #fff; opacity: 0.9; transition: none; @@ -171,9 +168,9 @@ cursor: default; } -#session-sync .sessions .folder[dragging="drop"] { - background: #4396E9 !important; - color: #FFF; +#session-sync .sessions .folder[dragging='drop'] { + background: #4396e9 !important; + color: #fff; opacity: 0.9; transition: none; position: absolute; @@ -181,9 +178,9 @@ z-index: 1000; } -#session-sync .sessions .folder[active="true"] { - background: #2D92EB; - color: #FFF; +#session-sync .sessions .folder[active='true'] { + background: #2d92eb; + color: #fff; } #session-sync .sessions .folder[filter] { @@ -192,22 +189,21 @@ } #session-sync .sessions .folder[type='separator']:after { - content: " "; + content: ' '; width: 100%; height: 2px; margin: -1px 0 0 0; - background: #CCC; + background: #ccc; position: absolute; box-sizing: border-box; top: 50%; left: 1px; } -#session-sync .sessions .folder[active="true"][type='separator']:after { - background: #FFF; +#session-sync .sessions .folder[active='true'][type='separator']:after { + background: #fff; } #session-sync .sessions .folder[type='bookmark'] { text-decoration: underline; } - diff --git a/assets/css/tooltip-system.css b/assets/css/tooltip-system.css index 82ddb77..a6d4417 100644 --- a/assets/css/tooltip-system.css +++ b/assets/css/tooltip-system.css @@ -7,10 +7,10 @@ margin: -16px 0 0 0; color: #555; - background: #FFF; + background: #fff; text-align: center; line-height: 20px; - border: 1px solid #CCC; + border: 1px solid #ccc; border-radius: 5px; display: none; @@ -19,19 +19,19 @@ left: 0; z-index: 2000; - box-shadow: 0 5px 7px 1px #CCC; + box-shadow: 0 5px 7px 1px #ccc; box-sizing: border-box; } #session-sync .tooltip-system:after { - content: " "; + content: ' '; width: 16px; height: 16px; margin: -8px 0 0 -9px; - background: #FFF; + background: #fff; border-style: solid; - border-color: #BBB; + border-color: #bbb; border-width: 0 1px 1px 0; position: absolute; diff --git a/assets/css/trash-can.css b/assets/css/trash-can.css index 1a17d0d..ec7dfc2 100644 --- a/assets/css/trash-can.css +++ b/assets/css/trash-can.css @@ -6,21 +6,21 @@ width: 8em; height: 8em; - background-image: url("images/trash-can.png"); + background-image: url('images/trash-can.png'); background-position: bottom 1.5em right 1.5em; background-repeat: no-repeat; background-size: 50%; - background-color: #FFF; + background-color: #fff; border-top-left-radius: 8em; font-size: 12px; - border: 1px solid #CCC; + border: 1px solid #ccc; border-bottom: none; border-right: none; - box-shadow: 0 0 5px 0 #FFF; + box-shadow: 0 0 5px 0 #fff; opacity: 0; @@ -33,24 +33,24 @@ transition: all 0.2s ease-out; } -#session-sync[show="true"] .trash-can { +#session-sync[show='true'] .trash-can { display: block; } -#session-sync[show="false"] .trash-can { +#session-sync[show='false'] .trash-can { display: none; } -#session-sync .trash-can[droppable="true"] { +#session-sync .trash-can[droppable='true'] { opacity: 1 !important; } -#session-sync .trash-can[action="enter"] { +#session-sync .trash-can[action='enter'] { opacity: 1; cursor: pointer; } -#session-sync .trash-can:not([items="0"]) { +#session-sync .trash-can:not([items='0']) { opacity: 1; } @@ -64,10 +64,10 @@ width: 2em; height: 2em; - background: #E54828; + background: #e54828; text-align: center; line-height: 1.9em; - color: #FFF; + color: #fff; border-radius: 100%; opacity: 1; @@ -79,11 +79,11 @@ transition: background 0.5s ease-out; } -#session-sync .trash-can[items="0"] .count { - background: #1AB61A; +#session-sync .trash-can[items='0'] .count { + background: #1ab61a; } -#session-sync .trash-can[items="0"] .dismiss { +#session-sync .trash-can[items='0'] .dismiss { display: none; } @@ -92,14 +92,14 @@ right: 0.5em; } -#session-sync .trash-can[dismiss="true"] { +#session-sync .trash-can[dismiss='true'] { opacity: 0; } -#session-sync .trash-can[dismiss="true"] .dismiss { +#session-sync .trash-can[dismiss='true'] .dismiss { display: none; } -#session-sync .trash-can[dismiss="true"]:hover { +#session-sync .trash-can[dismiss='true']:hover { opacity: 1; } diff --git a/assets/css/url-bar.css b/assets/css/url-bar.css index 0f2d56e..70af50c 100644 --- a/assets/css/url-bar.css +++ b/assets/css/url-bar.css @@ -15,7 +15,7 @@ overflow: hidden; opacity: 0; - color: #FFF; + color: #fff; position: absolute; top: 0.5em; @@ -31,14 +31,14 @@ height: 100%; display: none; - background: #FFF; + background: #fff; text-align: right; color: #777; float: left; } #session-sync .url-bar .arrow:after { - content: " "; + content: ' '; width: 2em; height: 2em; margin: 2px 0; @@ -47,24 +47,24 @@ position: absolute; left: 0; z-index: -2; - transform: translate(1em, -0.1em) rotate(45deg); + transform: translate(1em, -0.1em) rotate(45deg); } #session-sync .url-bar .url { height: 100%; padding-left: 8em; - background: #2D92EB; + background: #2d92eb; font-size: 0.75em; line-height: 2em; white-space: nowrap; text-overflow: ellipsis; } -#session-sync[search-box-content="true"] .url { +#session-sync[search-box-content='true'] .url { padding-left: calc(25% + 2.5em); } -#session-sync .url-bar[data-active="true"] { +#session-sync .url-bar[data-active='true'] { opacity: 1; bottom: -1px; } @@ -78,8 +78,8 @@ width: 5em; height: 100%; - background: #38B751; - color: #FFF; + background: #38b751; + color: #fff; text-align: center; font-size: 0.75em; line-height: 2em; diff --git a/eslint.config.js b/eslint.config.js index 2f7cef6..b0d9570 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -6,25 +6,21 @@ import stylistic from '@stylistic/eslint-plugin'; /** @type {import('eslint').Linter.Config[]} */ export default [ { - ignores: [ - '.output/**', - '.wxt/**', - 'node_modules/**', - ], + ignores: ['.output/**', '.wxt/**', 'node_modules/**'] }, js.configs.recommended, autoImports, { plugins: { - '@stylistic': stylistic, + '@stylistic': stylistic }, languageOptions: { globals: { ...globals.browser, - ...globals.webextensions, + ...globals.webextensions }, ecmaVersion: 'latest', - sourceType: 'module', + sourceType: 'module' }, rules: { // Stylistic Rules @@ -32,25 +28,16 @@ export default [ 'error', 'tab', { - 'SwitchCase': 1, - }, - ], - '@stylistic/linebreak-style': [ - 'error', - 'windows', - ], - '@stylistic/quotes': [ - 'error', - 'single', - ], - '@stylistic/semi': [ - 'error', - 'always', + SwitchCase: 1 + } ], + '@stylistic/linebreak-style': ['error', 'windows'], + '@stylistic/quotes': ['error', 'single'], + '@stylistic/semi': ['error', 'always'], // Core Logic Rules 'no-console': 'off', - 'no-undef': 'off', - }, - }, + 'no-undef': 'off' + } + } ]; diff --git a/package.json b/package.json index 3fe31c3..f3a1b9a 100644 --- a/package.json +++ b/package.json @@ -30,4 +30,4 @@ "prettier": "^3.8.3", "wxt": "^0.20.20" } -} \ No newline at end of file +} diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json index fefb238..4a88268 100644 --- a/public/_locales/en/messages.json +++ b/public/_locales/en/messages.json @@ -11,4 +11,4 @@ "session_sync_detach_window": { "message": "Window View" } -} \ No newline at end of file +} diff --git a/public/home/home.css b/public/home/home.css index e946611..4bc69bb 100644 --- a/public/home/home.css +++ b/public/home/home.css @@ -6,14 +6,14 @@ ***********/ body { - margin:0; + margin: 0; - background-color: #EBF1F5; + background-color: #ebf1f5; background-image: url(../images/grain_header.png), url(../images/grain.png); background-repeat: repeat-x, repeat; font-size: 12px; - font-family: "Open Sans", "Segoe UI", Arial, Helvetica, sans-serif; + font-family: 'Open Sans', 'Segoe UI', Arial, Helvetica, sans-serif; cursor: default; } @@ -32,12 +32,11 @@ body { * Identity */ - #identity { width: auto; height: 120px; text-align: center; - font-family: "Open Sans Light", "Open Sans", Arial, Helvetica, sans-serif; + font-family: 'Open Sans Light', 'Open Sans', Arial, Helvetica, sans-serif; position: relative; } @@ -45,7 +44,7 @@ body { width: auto; height: auto; font-size: 60px; - color: #DF5600; + color: #df5600; overflow: visible; text-align: left; @@ -59,7 +58,7 @@ body { margin: 0 auto 0; width: 80px; height: 80px; - background-image: url("../images/logo.png"); + background-image: url('../images/logo.png'); background-position: center; background-repeat: no-repeat; background-size: contain; @@ -96,7 +95,7 @@ body { #github .logo { width: 50px; height: 100%; - background-image: url("../images/github-logo.png"); + background-image: url('../images/github-logo.png'); background-size: 40px; background-repeat: no-repeat; background-position: center center; @@ -106,7 +105,7 @@ body { #github .brand { width: 100px; height: 100%; - background-image: url("../images/github.png"); + background-image: url('../images/github.png'); background-size: contain; background-repeat: no-repeat; background-position: center right; @@ -125,10 +124,12 @@ body { #feedback .icon { width: 50px; height: 100%; - background-image: url("../images/mail.png"); + background-image: url('../images/mail.png'); background-size: contain; background-repeat: no-repeat; - background-position: center right, center left; + background-position: + center right, + center left; float: left; } @@ -145,7 +146,6 @@ body { opacity: 1; } - #tooltip { width: auto; font-size: 14px; @@ -157,7 +157,7 @@ body { background: rgba(255, 255, 255, 1); text-align: center; line-height: 20px; - border: 1px solid #CCC; + border: 1px solid #ccc; border-radius: 5px; display: none; @@ -172,14 +172,14 @@ body { } #tooltip:before { - content: " "; + content: ' '; width: 16px; height: 16px; margin: -8px 0 0 -8px; - background: #FFF; + background: #fff; border-style: solid; - border-color: #BBB; + border-color: #bbb; border-width: 0 1px 1px 0; position: absolute; @@ -194,7 +194,6 @@ body { display: block; } - /******************* * Box Containers *******************/ @@ -225,17 +224,17 @@ body { padding: 0.2em; margin: 0.5em; border-radius: 1em; - background: #CCC; + background: #ccc; float: left; } #container .bullet[active] { - background: #2D92EB; + background: #2d92eb; cursor: pointer; } #container .bullet:hover { - background: #2D92EB; + background: #2d92eb; cursor: pointer; } @@ -244,7 +243,7 @@ body { height: 100%; position: absolute; left: 0; - transition: 1.0s left ease; + transition: 1s left ease; } #container .helpers { @@ -277,11 +276,11 @@ body { } #container #scroll-left { - background-image: url("../images/arrow-left.png"); + background-image: url('../images/arrow-left.png'); } #container #scroll-right { - background-image: url("../images/arrow-right.png"); + background-image: url('../images/arrow-right.png'); right: 0; } @@ -289,7 +288,7 @@ body { width: 100%; padding: 10px; font-size: 30px; - border-bottom: 2px solid #FFF; + border-bottom: 2px solid #fff; text-align: center; box-sizing: border-box; } @@ -305,15 +304,15 @@ body { #container .info { width: 100%; - padding: 20px 0 ; + padding: 20px 0; font-size: 16px; text-align: center; } #container .highlight { - color: #DF5600 + color: #df5600; } #container a { text-decoration: none; -} \ No newline at end of file +} diff --git a/public/home/home.html b/public/home/home.html index 4a10aa7..380a2d7 100644 --- a/public/home/home.html +++ b/public/home/home.html @@ -1,146 +1,130 @@ - + + + + Session Sync - Tutorial + + + - + +
- - Session Sync - Tutorial - - - - - - - -
- -
- - -
-
- Session Sync - +
+ +
+
+ Session Sync + +
-
- + - + -
-
-
-
- -
-
-
-
- Press CTRL + SHIFT + F - to open the panel or use the widget provided to open the - panel -
-
- You can find all your recent browsing history in the history tab. -
- Restore the session or save it if you want to keep - indefinitely
- Configure history settings in the configuration panel -
-
- Use the search bar to easy filter through saved sessions -
- You can focus on the search bar by pressing CTRL + F or - F3 -
-
- Press right click to access aditional context menus for - sessions or - bookmarks -
-
-
- Resize the UI panel using the - resize-handle from the - bottom-left corner -
-
- Resize session list width by dragging around the - green resize handle -
-
- The addon offers multiple configuration options. Explore all of them in the configuration - panel -
- Configuration panel can be accessed from the addon UI menu -
-
- Drag and drop bookmark items over the trash bin drop area - location (bottom-right corner) - to delete them -
- Click on the red button to restore the previous deleted items -
-
- Session sync offers the ability to save (or not) pinned tabs and - also to remember the - pinned state -
-
- You can edit session and bookmarks information to provide more - valuable future information -
-
- Sort sessions by name, position, or date using the sort-selection - menu from the bottom-left corner of the UI -
-
- The session menu toolbar provides fast access to various commands such as restore, replace, merge or add the - current tab as bookmark -
-
- Move your mouse above a bookmark to view the URL. -
- Click on the URL bar to copy the link -
-
- You can detach the UI into a separate window or tab
- Window Mode: CTRL + SHIFT + - V   - Tab Mode: CTRL + SHIFT + - U
- or simply use the buttons from addon menu
-
-
- Saved session are stored as bookmarks. It's recommended to use Firefox Account to sync your bookmarks.
- Currenly you can only use Firefox Bookmark Manager (CTRL + SHIFT - + O) to export/import all your bookmarks (which includes all Session Sync saved data). -
- Session Sync detects automatically all updated bookmarks after an import. +
+
+
+
+ +
+
+
+
+ Press CTRL + SHIFT + F to open the panel or use the + widget provided to open the panel +
+
+ You can find all your recent browsing history in the history tab. +
+ Restore the session or save it if you want to keep indefinitely
+ Configure history settings in the configuration panel +
+
+ Use the search bar to easy filter through saved sessions +
+ You can focus on the search bar by pressing CTRL + F or + F3 +
+
+ Press right click to access aditional context menus for sessions or + bookmarks +
+
+
+ Resize the UI panel using the + resize-handle from the + bottom-left corner +
+
+ Resize session list width by dragging around the + green resize handle +
+
+ The addon offers multiple configuration options. Explore all of them in the configuration panel +
+ Configuration panel can be accessed from the addon UI menu +
+
+ Drag and drop bookmark items over the trash bin drop area location (bottom-right corner) to + delete them +
+ Click on the red button to restore the previous deleted items +
+
+ Session sync offers the ability to save (or not) pinned tabs and also to + remember the + pinned state +
+
+ You can edit session and bookmarks information to provide more valuable future information +
+
+ Sort sessions by name, position, or date using the sort-selection menu from the bottom-left corner of + the UI +
+
+ The session menu toolbar provides fast access to various commands such as + restore, replace, merge or add the current tab as bookmark +
+
+ Move your mouse above a bookmark to view the URL. +
+ Click on the URL bar to copy the link +
+
+ You can detach the UI into a separate window or tab
+ Window Mode: CTRL + SHIFT + 2   Tab Mode: + CTRL + SHIFT + U
+ or simply use the buttons from addon menu
+
+
+ Saved session are stored as bookmarks. It's recommended to use + Firefox Account to sync + your bookmarks.
+ Currenly you can only use Firefox Bookmark Manager (CTRL + SHIFT + O) to export/import all + your bookmarks (which includes all Session Sync saved data). +
+ Session Sync detects automatically all updated bookmarks after an import. +
-
- - - - - \ No newline at end of file + + + diff --git a/public/home/home.js b/public/home/home.js index bb8da27..4fffb24 100644 --- a/public/home/home.js +++ b/public/home/home.js @@ -10,7 +10,6 @@ var tooltip; window.addEventListener('load', load); function load() { - Carousel = document.getElementById('carousel'); bullets = document.getElementById('bullets'); tooltip = document.getElementById('tooltip'); @@ -35,9 +34,7 @@ function load() { } var initTutorial = function initTutorial() { - - if (inited) - return; + if (inited) return; inited = true; @@ -48,7 +45,6 @@ var initTutorial = function initTutorial() { initCarousel(); updateActiveBullet(bullets.firstElementChild.firstElementChild); - }; function updateActiveBullet(node) { @@ -64,7 +60,7 @@ function updateActiveBullet(node) { } function setCarouselTo(index) { - if (index < 0 || index > (size - 1)) { + if (index < 0 || index > size - 1) { return; } @@ -206,7 +202,7 @@ var TutorialEntries = (function TutorialEntries() { { title: 'Export/Import Sessions', key: 'export-import' - }, + } ]; function init() { @@ -237,7 +233,6 @@ var TutorialEntries = (function TutorialEntries() { getEntryByIndex: getEntryByIndex, getEntryByKey: getEntryByKey }; - })(); // browser.commands.getAll(). diff --git a/src/background/development.js b/src/background/development.js index b65dd53..5501716 100644 --- a/src/background/development.js +++ b/src/background/development.js @@ -22,5 +22,4 @@ export function initDevelopment() { // ------------------------------------------------------------------------ // Events - -} \ No newline at end of file +} diff --git a/src/background/favicon-service.js b/src/background/favicon-service.js index 3c8bdac..cecd92a 100644 --- a/src/background/favicon-service.js +++ b/src/background/favicon-service.js @@ -71,7 +71,7 @@ const FaviconService = (function FaviconService() { browser.tabs.onUpdated.addListener(handle, { tabId: tab.id, - properties: ['favIconUrl'], + properties: ['favIconUrl'] }); } }; @@ -91,7 +91,7 @@ const FaviconService = (function FaviconService() { initConfig(); return { - checkTabFaviconSaved: checkTabFaviconSaved, + checkTabFaviconSaved: checkTabFaviconSaved }; })(); diff --git a/src/background/main.js b/src/background/main.js index 01d9634..3822e3e 100644 --- a/src/background/main.js +++ b/src/background/main.js @@ -10,7 +10,6 @@ export const Commands = { }; const SessionSync = (function () { - var windowInfoKey = 'style.window.detach'; var activeWindow = null; var defaultWindowProperties = { @@ -49,25 +48,27 @@ const SessionSync = (function () { case Commands.openTutorial: browser.tabs.create({ url: 'home/home.html', - active: true, + active: true }); break; case Commands.openGithub: browser.tabs.create({ url: 'https://github.com/ReDEnergy/SessionSync', - active: true, + active: true }); break; } } function leaveFeedback() { - browser.tabs.create({ - url: 'mailto:gabriel.ivanica@gmail.com?Subject=[Session-Sync]%20User%20feedback' - }).then(function (tab) { - browser.tabs.remove(tab.id); - }); + browser.tabs + .create({ + url: 'mailto:gabriel.ivanica@gmail.com?Subject=[Session-Sync]%20User%20feedback' + }) + .then(function (tab) { + browser.tabs.remove(tab.id); + }); } function detachToWindow() { @@ -84,22 +85,25 @@ const SessionSync = (function () { windowInfo = defaultWindowProperties; } - browser.windows.create({ - width: windowInfo.width, - height: windowInfo.height, - url: 'popup.html', - type: 'panel' - }).then(function (newWindow) { - activeWindow = newWindow; - }); + browser.windows + .create({ + width: windowInfo.width, + height: windowInfo.height, + url: 'popup.html', + type: 'panel' + }) + .then(function (newWindow) { + activeWindow = newWindow; + }); }); } function detachedToTab() { - browser.tabs.query({ - title: 'Session Sync', - windowId: browser.windows.WINDOW_ID_CURRENT - }) + browser.tabs + .query({ + title: 'Session Sync', + windowId: browser.windows.WINDOW_ID_CURRENT + }) .then(function (tabs) { if (tabs.length > 0) { for (let i in tabs) { @@ -148,20 +152,21 @@ export const WindowUtils = (function () { var properties = { width: window.width, height: window.height, - state: window.state, + state: window.state }; - browser.windows.update(window.id, { - state: 'maximized' - }).then(function (window) { - resizeAndCenter(window, properties); - }); + browser.windows + .update(window.id, { + state: 'maximized' + }) + .then(function (window) { + resizeAndCenter(window, properties); + }); } return { centerWindow: centerWindow }; - })(); // ------------------------------------------------------------------------ diff --git a/src/background/session-history.js b/src/background/session-history.js index a08c52f..ee2014a 100644 --- a/src/background/session-history.js +++ b/src/background/session-history.js @@ -10,7 +10,7 @@ function BrowserSession() { this.timer = undefined; this.active = true; this.tabCount = 0; - this.startTime = (new Date()).getTime(); + this.startTime = new Date().getTime(); this.lastSave = this.startTime; this.windows = {}; } @@ -19,57 +19,60 @@ function WindowSession(id) { this.id = id; this.tabs = []; this.active = true; - this.saveTime = (new Date()).getTime(); + this.saveTime = new Date().getTime(); } BrowserSession.prototype.update = function update() { if (typeof browser === 'undefined' || !browser.windows) return; // Prepare session information - browser.windows.getAll({ - populate: true, - windowTypes: ['normal'] - }).then(function (mozWindows) { - var tabCount = 0; - - for (var key in this.windows) { - this.windows[key].active = false; - } - - for (let mozWindow of mozWindows) { - if (mozWindow.incognito && (this.config && this.config.savePrivate == false)) { - delete this.windows[mozWindow.id]; - continue; - } + browser.windows + .getAll({ + populate: true, + windowTypes: ['normal'] + }) + .then( + function (mozWindows) { + var tabCount = 0; + + for (var key in this.windows) { + this.windows[key].active = false; + } - var windowInfo = new WindowSession(mozWindow.id); - - // Add window tabs - for (var i in mozWindow.tabs) { - var tab = mozWindow.tabs[i]; - var sessionTab = { - title: tab.title, - url: tab.url, - pinned: tab.pinned, - cookieStoreId: tab.cookieStoreId, - }; - tabCount++; - windowInfo.tabs[tab.index] = sessionTab; - } + for (let mozWindow of mozWindows) { + if (mozWindow.incognito && this.config && this.config.savePrivate == false) { + delete this.windows[mozWindow.id]; + continue; + } - this.windows[mozWindow.id] = windowInfo; - } + var windowInfo = new WindowSession(mozWindow.id); + + // Add window tabs + for (var i in mozWindow.tabs) { + var tab = mozWindow.tabs[i]; + var sessionTab = { + title: tab.title, + url: tab.url, + pinned: tab.pinned, + cookieStoreId: tab.cookieStoreId + }; + tabCount++; + windowInfo.tabs[tab.index] = sessionTab; + } - // Update internal storage - this.lastSave = (new Date()).getTime(); - this.tabCount = tabCount; + this.windows[mozWindow.id] = windowInfo; + } - // Save new session - if (browser.storage && browser.storage.local) { - browser.storage.local.set({ [storageKey.active]: this }); - } + // Update internal storage + this.lastSave = new Date().getTime(); + this.tabCount = tabCount; - }.bind(this)); + // Save new session + if (browser.storage && browser.storage.local) { + browser.storage.local.set({ [storageKey.active]: this }); + } + }.bind(this) + ); }; BrowserSession.prototype.clear = function clear() { @@ -117,8 +120,8 @@ let config = { interval: 15, saveBuffer: 3, savingSlots: 20, - expireTimeHours: 48, // hours - savePrivate: false, + expireTimeHours: 48, // hours + savePrivate: false }; const upgradeStorage = function upgradeStorage() { @@ -149,10 +152,8 @@ const updateStorage = function updateStorage() { sessions.forEach(function (session) { if (session != undefined && (removeCount <= 0 || session.lastSave > expireTime)) { list.push(session); - } - else { - if (removeCount > 0) - removeCount--; + } else { + if (removeCount > 0) removeCount--; } }); @@ -173,30 +174,27 @@ const saveSessionsList = function saveSessionsList() { const internalInit = function internalInit() { if (typeof browser === 'undefined' || !browser.storage || !browser.storage.local) return; - browser.storage.local.get([storageKey.list]) - .then(function (obj) { - - if (obj[storageKey.list]) { - sessions = obj[storageKey.list]; - updateStorage(); - } + browser.storage.local.get([storageKey.list]).then(function (obj) { + if (obj[storageKey.list]) { + sessions = obj[storageKey.list]; + updateStorage(); + } - // console.log(sessions); - saveSessionsList(); - - browser.storage.local.get([storageKey.active]) - .then(function (data) { - var session = data[storageKey.active]; - if (session) { - session.windows = Object.values(session.windows); - delete session.active; - sessions.push(session); - saveSessionsList(); - } - }); + // console.log(sessions); + saveSessionsList(); - activeSession.setConfig(config); + browser.storage.local.get([storageKey.active]).then(function (data) { + var session = data[storageKey.active]; + if (session) { + session.windows = Object.values(session.windows); + delete session.active; + sessions.push(session); + saveSessionsList(); + } }); + + activeSession.setConfig(config); + }); }; const onConfigChanged = function onConfigChanged(object) { @@ -211,21 +209,23 @@ export const SessionAutoSave = { init: function () { if (typeof browser === 'undefined' || !browser.storage || !browser.storage.local) return; - browser.storage.local.get(storageKey.config) - .then(function onSuccess(obj) { + browser.storage.local.get(storageKey.config).then( + function onSuccess(obj) { if (obj[storageKey.config]) { config = obj[storageKey.config]; internalInit(); + } else { + browser.storage.local + .set({ + [storageKey.config]: config + }) + .then(internalInit); } - else { - browser.storage.local.set({ - [storageKey.config]: config - }).then(internalInit); - } - - }, function onError() { + }, + function onError() { internalInit(); - }); + } + ); try { if (browser.runtime && browser.runtime.onMessage) { @@ -247,4 +247,4 @@ export const SessionAutoSave = { console.warn('browser.storage.onChanged.addListener failed in session-history', err); } } -}; \ No newline at end of file +}; diff --git a/src/background/session-management.js b/src/background/session-management.js index 9a58d98..dd5541e 100644 --- a/src/background/session-management.js +++ b/src/background/session-management.js @@ -14,7 +14,7 @@ export const SessionManagement = (function SessionManagement() { function getTabInfo(url) { return { - url: url, + url: url }; } @@ -80,7 +80,7 @@ export const SessionManagement = (function SessionManagement() { case 'newWindow': { const windowOptions = { url: options.url, - cookieStoreId: options.cookieStoreId, + cookieStoreId: options.cookieStoreId }; browser.windows.create(windowOptions).then(function (windowInfo) { @@ -99,8 +99,7 @@ export const SessionManagement = (function SessionManagement() { .then((tabs) => browser.tabs.get(tabs[0].id)) .then((tab) => { if ( - (options.cookieStoreId == undefined && - tab.cookieStoreId == FirefoxContainers.defaultContainer) || + (options.cookieStoreId == undefined && tab.cookieStoreId == FirefoxContainers.defaultContainer) || tab.cookieStoreId === options.cookieStoreId ) { delete options.cookieStoreId; @@ -108,7 +107,7 @@ export const SessionManagement = (function SessionManagement() { } else { browser.tabs.create({ url: options.url, - cookieStoreId: options.cookieStoreId, + cookieStoreId: options.cookieStoreId }); } }); @@ -135,11 +134,11 @@ export const SessionManagement = (function SessionManagement() { url: bookmark.url, active: false, windowId: windowId, - pinned: info.properties.pinned ? true : false, + pinned: info.properties.pinned ? true : false }, { discard: lazyLoading, - container: info.properties.container, + container: info.properties.container }, FaviconService.checkTabFaviconSaved ); @@ -154,11 +153,11 @@ export const SessionManagement = (function SessionManagement() { url: tab.url, active: false, windowId: windowId, - pinned: tab.pinned ? true : false, + pinned: tab.pinned ? true : false }, { discard: lazyLoading, - container: tab.cookieStoreId, + container: tab.cookieStoreId } ); }); @@ -177,7 +176,7 @@ export const SessionManagement = (function SessionManagement() { browser.windows .getAll({ populate: true, - windowTypes: ['normal'], + windowTypes: ['normal'] }) .then(function (windows) { windows.forEach(function (mozWidnow) { @@ -306,6 +305,6 @@ export const SessionManagement = (function SessionManagement() { return { getTabInfo: getTabInfo, - init: init, + init: init }; })(); diff --git a/src/common/bookmark-properties.js b/src/common/bookmark-properties.js index 4143456..13af44b 100644 --- a/src/common/bookmark-properties.js +++ b/src/common/bookmark-properties.js @@ -49,14 +49,14 @@ export class BookmarkProperties { if (Array.isArray(properties) && properties.length == 1) { return { title: title.substring(0, match.index), - properties: properties[0], + properties: properties[0] }; } } return { title: title, - properties: {}, + properties: {} }; } } diff --git a/src/entrypoints/popup/index.html b/src/entrypoints/popup/index.html index 9d4fb38..ca1cea5 100644 --- a/src/entrypoints/popup/index.html +++ b/src/entrypoints/popup/index.html @@ -1,15 +1,13 @@ - + + + + Session Sync + + - - - Session Sync - - - - -
- - - - \ No newline at end of file + +
+ + + diff --git a/src/scripts/3rd/pubsub.js b/src/scripts/3rd/pubsub.js index c64ead4..bd2b0d4 100644 --- a/src/scripts/3rd/pubsub.js +++ b/src/scripts/3rd/pubsub.js @@ -3,7 +3,6 @@ // Modified version of https://github.com/mroderick/PubSubJS to return different PubSub objects export function PubSub() { - var messages = {}, lastUid = -1; @@ -20,8 +19,8 @@ export function PubSub() { /** * Returns a function that throws the passed exception, for use as argument for setTimeout - * @param { Object } ex An Error object - */ + * @param { Object } ex An Error object + */ function throwException(ex) { return function reThrowException() { throw ex; @@ -107,31 +106,31 @@ export function PubSub() { /** * PubSub.publish( message[, data] ) -> Boolean - * - message (String): The message to publish - * - data: The data to pass to subscribers - * Publishes the the message, passing the data to it's subscribers - **/ + * - message (String): The message to publish + * - data: The data to pass to subscribers + * Publishes the the message, passing the data to it's subscribers + **/ this.publish = function (message, data) { return publish(message, data, false, this.immediateExceptions); }; /** * PubSub.publishSync( message[, data] ) -> Boolean - * - message (String): The message to publish - * - data: The data to pass to subscribers - * Publishes the the message synchronously, passing the data to it's subscribers - **/ + * - message (String): The message to publish + * - data: The data to pass to subscribers + * Publishes the the message synchronously, passing the data to it's subscribers + **/ this.publishSync = function (message, data) { return publish(message, data, true, this.immediateExceptions); }; /** * PubSub.subscribe( message, func ) -> String - * - message (String): The message to subscribe to - * - func (Function): The function to call when a new message is published - * Subscribes the passed function to the passed message. Every returned token is unique and should be stored if - * you need to unsubscribe - **/ + * - message (String): The message to subscribe to + * - func (Function): The function to call when a new message is published + * Subscribes the passed function to the passed message. Every returned token is unique and should be stored if + * you need to unsubscribe + **/ this.subscribe = function (message, func) { if (typeof func !== 'function') { return false; @@ -152,36 +151,38 @@ export function PubSub() { }; /* Public: Clears all subscriptions - */ + */ this.clearAllSubscriptions = function clearSubscriptions() { messages = {}; }; /* Public: removes subscriptions. - * When passed a token, removes a specific subscription. - * When passed a function, removes all subscriptions for that function - * When passed a topic, removes all subscriptions for that topic (hierarchy) - * - * value - A token, function or topic to unsubscribe. - * - * Examples - * - * // Example 1 - unsubscribing with a token - * var token = PubSub.subscribe('mytopic', myFunc); - * PubSub.unsubscribe(token); - * - * // Example 2 - unsubscribing with a function - * PubSub.unsubscribe(myFunc); - * - * // Example 3 - unsubscribing a topic - * PubSub.unsubscribe('mytopic'); - */ + * When passed a token, removes a specific subscription. + * When passed a function, removes all subscriptions for that function + * When passed a topic, removes all subscriptions for that topic (hierarchy) + * + * value - A token, function or topic to unsubscribe. + * + * Examples + * + * // Example 1 - unsubscribing with a token + * var token = PubSub.subscribe('mytopic', myFunc); + * PubSub.unsubscribe(token); + * + * // Example 2 - unsubscribing with a function + * PubSub.unsubscribe(myFunc); + * + * // Example 3 - unsubscribing a topic + * PubSub.unsubscribe('mytopic'); + */ this.unsubscribe = function (value) { var isTopic = typeof value === 'string' && messages.hasOwnProperty(value), isToken = !isTopic && typeof value === 'string', isFunction = typeof value === 'function', result = false, - m, message, t; + m, + message, + t; if (isTopic) { delete messages[value]; diff --git a/src/scripts/app.js b/src/scripts/app.js index 9cf30c7..9641673 100644 --- a/src/scripts/app.js +++ b/src/scripts/app.js @@ -16,26 +16,23 @@ const sessionRoot = 'SessionSync'; // API const findSessionSync = function findSessionSync(callback) { - BookmarkManager.searchBookmarks({ title: sessionRoot }) - .then( - function success(bookmarks) { - if (bookmarks.length == 0) { + BookmarkManager.searchBookmarks({ title: sessionRoot }).then( + function success(bookmarks) { + if (bookmarks.length == 0) { + BookmarkManager.createBookmark({ + title: sessionRoot, + parentId: 'menu________' + }).then(callback, function error() { BookmarkManager.createBookmark({ - title: sessionRoot, - parentId: 'menu________' - }).then(callback, function error() { - BookmarkManager.createBookmark({ - title: sessionRoot, - }).then(callback); - }); - } - else { - callback(bookmarks[0]); - } - }, - function fail() { + title: sessionRoot + }).then(callback); + }); + } else { + callback(bookmarks[0]); } - ); + }, + function fail() {} + ); }; const listSessions = function listSessions() { diff --git a/src/scripts/components/config-panel.js b/src/scripts/components/config-panel.js index 8c143f3..7b92cb3 100644 --- a/src/scripts/components/config-panel.js +++ b/src/scripts/components/config-panel.js @@ -16,8 +16,8 @@ import * as DOMComponent from '../utils/components'; // Custom Modules /* -* Config panel -*/ + * Config panel + */ export function ConfigPanel() { // Create DomHealper @@ -46,7 +46,7 @@ export function ConfigPanel() { if (options.callback) { options.callback(value); } - }, + } }); options.parent.addItem(optionRG.DOMRoot); AppConfig.onChange(options.key, function (value) { @@ -55,7 +55,7 @@ export function ConfigPanel() { } function ToggleOptionConfig(options) { - var isSwitch = (options.type === 'switch'); + var isSwitch = options.type === 'switch'; var Toggle = DOMComponent.ToggleButton; if (isSwitch) { Toggle = DOMComponent.ToggleSwitch; @@ -73,15 +73,14 @@ export function ConfigPanel() { if (options.callback) { options.callback(value); } - }, + } }); options.parent.addItem(toggleBtn.DOMRoot); AppConfig.onChange(options.key, function (value) { if (isSwitch) { toggleBtn.setState(value); - } - else { + } else { toggleBtn.setValue(value, false); } }); @@ -106,7 +105,6 @@ export function ConfigPanel() { sectionContainer.appendChild(sectionBody); sectionTitle.addEventListener('click', function () { - // manual mode var isCollapsed = sectionContainer.hasAttribute('collapsed'); if (isCollapsed) { @@ -127,7 +125,6 @@ export function ConfigPanel() { // Section: UI scaling (function () { - var section = new ConfigSection({ title: 'UI Scaling' }); panel.appendChild(section.container); @@ -139,14 +136,11 @@ export function ConfigPanel() { RangeOptionConfig({ parent: section, name: 'Toolbar', key: 'style.scale.toolbar', minValue: 10, maxValue: 50 }); RangeOptionConfig({ parent: section, name: 'Session list', key: 'style.scale.sessions', minValue: 10, maxValue: 50 }); RangeOptionConfig({ parent: section, name: 'Bookmarks list', key: 'style.scale.bookmarks', minValue: 10, maxValue: 50 }); - })(); // ------------------------ // Section: General settings - (function () { - var section = new ConfigSection({ title: 'Theme Options' }); panel.appendChild(section.container); @@ -154,16 +148,13 @@ export function ConfigPanel() { parent: section, name: 'Grayscale Icons', key: 'theme.toolbar.grayscale', - type: 'switch', + type: 'switch' }); - }); // ------------------------ // Section: General settings - (function () { - var section = new ConfigSection({ title: 'Session Management' }); panel.appendChild(section.container); @@ -171,23 +162,20 @@ export function ConfigPanel() { parent: section, name: 'Lazy loading restore', key: 'restore.lazy.loading', - type: 'switch', + type: 'switch' }); ToggleOptionConfig({ parent: section, name: 'Reverse restore order', key: 'restore.reverse.order', - type: 'switch', + type: 'switch' }); - })(); // ------------------------ // Section: General settings - (function () { - var section = new ConfigSection({ title: 'General' }); panel.appendChild(section.container); @@ -196,7 +184,7 @@ export function ConfigPanel() { name: 'Left click', key: 'bookmark.click.newTab', onState: 'New tab', - offState: 'Same tab', + offState: 'Same tab' }); ToggleOptionConfig({ @@ -204,7 +192,7 @@ export function ConfigPanel() { name: 'Middle click', key: 'bookmark.middleClick.newTab', onState: 'New tab', - offState: 'Same tab', + offState: 'Same tab' }); ToggleOptionConfig({ @@ -213,24 +201,21 @@ export function ConfigPanel() { name: 'Context menu icons', key: 'context.menu.icons', onState: 'Show', - offState: 'Disabled', + offState: 'Disabled' }); ToggleOptionConfig({ parent: section, name: 'Favicon service', key: 'services.favicon.enabled', - type: 'switch', + type: 'switch' }); })(); // ------------------------ // Section: Hotkeys - (function () { - - if (typeof browser === 'object' && browser.commands && browser.commands.update == undefined) - return; + if (typeof browser === 'object' && browser.commands && browser.commands.update == undefined) return; var section = new ConfigSection({ title: 'Hotkeys' }); panel.appendChild(section.container); @@ -249,7 +234,7 @@ export function ConfigPanel() { GlobalEvents.emit('hotkey.update', command); } }); - }, + } }); return button.DOMRoot; @@ -265,9 +250,7 @@ export function ConfigPanel() { // ------------------------ // Section: History settings - (function () { - var section = new ConfigSection({ title: 'Auto-save History', id: 'history-config' }); panel.appendChild(section.container); @@ -286,7 +269,7 @@ export function ConfigPanel() { if (value == false) { WindowEvents.emit(document, 'ShowSyncList', { update: true }); } - WindowEvents.emit(document, 'SetUIState', { 'history': value }); + WindowEvents.emit(document, 'SetUIState', { history: value }); } }); section.addItem(contextMenuIconsTB.DOMRoot); @@ -312,7 +295,7 @@ export function ConfigPanel() { onChange: function (value) { historyConfig.interval = value; SessionHistory.updateConfig(historyConfig); - }, + } }); section.addItem(saveIntervalRG.DOMRoot); @@ -324,7 +307,7 @@ export function ConfigPanel() { onChange: function (value) { historyConfig.savingSlots = value; SessionHistory.updateConfig(historyConfig); - }, + } }); section.addItem(saveSlotsRG.DOMRoot); @@ -336,13 +319,12 @@ export function ConfigPanel() { onChange: function (value) { historyConfig.expireTimeHours = value; SessionHistory.updateConfig(historyConfig); - }, + } }); section.addItem(expireAfterRG.DOMRoot); // Events - WindowEvents.emit(document, 'SetUIState', { 'history': historyConfig.enabled }); - + WindowEvents.emit(document, 'SetUIState', { history: historyConfig.enabled }); }); })(); @@ -358,4 +340,4 @@ export function ConfigPanel() { this.document = document; this.DOMRoot = panel; -} \ No newline at end of file +} diff --git a/src/scripts/components/notification-system.js b/src/scripts/components/notification-system.js index 663bf09..eeddbd1 100644 --- a/src/scripts/components/notification-system.js +++ b/src/scripts/components/notification-system.js @@ -41,4 +41,4 @@ export function NotificationSystem() { // Public data this.DOMRoot = panel; -} \ No newline at end of file +} diff --git a/src/scripts/components/overlay-system.js b/src/scripts/components/overlay-system.js index e360bb8..29abba3 100644 --- a/src/scripts/components/overlay-system.js +++ b/src/scripts/components/overlay-system.js @@ -10,8 +10,8 @@ import { HTMLCreator } from '../utils/dom'; // API /* -* Session Hotkeys -*/ + * Session Hotkeys + */ export function OverlaySystem() { // Create DomHealper @@ -43,8 +43,7 @@ export function OverlaySystem() { } function autoHide() { - if (state == true) - setState(false); + if (state == true) setState(false); } // document.addEventListener('click', autoHide); @@ -55,4 +54,4 @@ export function OverlaySystem() { // Public properties this.DOMRoot = overlay; -} \ No newline at end of file +} diff --git a/src/scripts/components/session-bookmark-events.js b/src/scripts/components/session-bookmark-events.js index 0a3af3c..51d181c 100644 --- a/src/scripts/components/session-bookmark-events.js +++ b/src/scripts/components/session-bookmark-events.js @@ -30,8 +30,7 @@ export function SessionBookmarkEvents(container) { function getAttribute(target, attribute) { try { return target.getAttribute(attribute); - } - catch (err) { + } catch (err) { console.log(err); return null; } @@ -86,8 +85,7 @@ export function SessionBookmarkEvents(container) { // Bookmark entry case 'restore': { var bookmarkID = getBookmarkID(e); - if (!bookmarkID || bookmarkHoverContextID == bookmarkID) - return; + if (!bookmarkID || bookmarkHoverContextID == bookmarkID) return; bookmarkHoverContextID = bookmarkID; WindowEvents.emit(document, 'ShowUrlBar', SessionSyncModel.bookmarks[bookmarkID].url); @@ -159,8 +157,7 @@ export function SessionBookmarkEvents(container) { }; var mouseDragging = function mouseMove(e) { - if (this != window) - return; + if (this != window) return; var deltaTime = new Date() - trackingTelemetry; if (deltaTime < 16) { @@ -171,10 +168,9 @@ export function SessionBookmarkEvents(container) { if (tryScroll) { if (e.clientY < offsetRect.top) { - AutoScroll.scroll(container, (e.clientY - offsetRect.top)); - } - else if (e.clientY > offsetRect.bottom) { - AutoScroll.scroll(container, (e.clientY - offsetRect.bottom)); + AutoScroll.scroll(container, e.clientY - offsetRect.top); + } else if (e.clientY > offsetRect.bottom) { + AutoScroll.scroll(container, e.clientY - offsetRect.bottom); } else { AutoScroll.stop(); } @@ -205,14 +201,12 @@ export function SessionBookmarkEvents(container) { favicon: true }); } - } - else { + } else { if (SessionSyncModel.bookmarks[bookmarkID] != undefined) { SessionSyncModel.moveBookmark(bookmarkContext.bookmarkID, bookmarkID); } } - } - else { + } else { if (getTargetType(e) === 'folder') { var sessionID = getSessionID(e); if (sessionID != null && SessionSyncModel.bookmarks[sessionID] != undefined) { @@ -230,8 +224,7 @@ export function SessionBookmarkEvents(container) { if (tabContext) { if (tabContext != bookmarkContext) { SessionManager.moveTab(bookmarkContext.tab.id, tabContext.tab.index, tabContext.tab.windowId); - } - else { + } else { if (tabID) { SessionManager.activateTab(tabID); var windowID = getWindowID(e); @@ -240,19 +233,17 @@ export function SessionBookmarkEvents(container) { } } } - } - else { + } else { if (getTargetType(e) === 'folder') { var savingSessionID = getSessionID(e); if (savingSessionID != null) { - BookmarkManager.createBookmarkFromTab(bookmarkContext.tab, savingSessionID) - .then(function () { - if (typeof document !== 'undefined') { - WindowEvents.emit(document, 'Notification', { - message: 'Saved', - }); - } - }); + BookmarkManager.createBookmarkFromTab(bookmarkContext.tab, savingSessionID).then(function () { + if (typeof document !== 'undefined') { + WindowEvents.emit(document, 'Notification', { + message: 'Saved' + }); + } + }); } } } @@ -261,8 +252,7 @@ export function SessionBookmarkEvents(container) { if (DragContext.hasContext()) { cancelDragging(e); - } - else { + } else { window.removeEventListener('mousemove', trackDragging); } @@ -309,7 +299,6 @@ export function SessionBookmarkEvents(container) { // If active session preview case 'current': { - if (e.button == 0) { let tabID = getTabID(e); if (tabID != undefined) { @@ -320,8 +309,7 @@ export function SessionBookmarkEvents(container) { window.addEventListener('mouseup', mouseUp); return; } - } - else { + } else { let windowID = getWindowID(e); if (windowID) { SessionManager.activateWindow(windowID); @@ -346,8 +334,7 @@ export function SessionBookmarkEvents(container) { break; } } - } - else { + } else { if (e.button == 0) { let windowID = getWindowID(e); if (windowID != undefined && typeof document !== 'undefined') { @@ -366,8 +353,7 @@ export function SessionBookmarkEvents(container) { switch (SyncModel.state.session) { case 'restore': { var bookmarkID = getBookmarkID(e); - if (bookmarkID == null) - return; + if (bookmarkID == null) return; WindowEvents.emit(document, 'BookmarkCtxMenu-Open', { context: bookmarkID, @@ -391,7 +377,6 @@ export function SessionBookmarkEvents(container) { // -------------------------------------------------------------- // App Events - // ------------------------------------------------------------------------ // Init Code Events @@ -405,4 +390,4 @@ export function SessionBookmarkEvents(container) { container.addEventListener('mouseleave', mouseLeave); container.addEventListener('dblclick', bookmarkDblClick); -} \ No newline at end of file +} diff --git a/src/scripts/components/session-bookmark.js b/src/scripts/components/session-bookmark.js index 71b21af..4246dc9 100644 --- a/src/scripts/components/session-bookmark.js +++ b/src/scripts/components/session-bookmark.js @@ -27,9 +27,9 @@ function configureHoverItem(item, url) { } /* -* Session Bookmark -* Used for interacting with a Firefox bookmark item -*/ + * Session Bookmark + * Used for interacting with a Firefox bookmark item + */ export class SessionBookmark { constructor(bookmark) { @@ -114,9 +114,9 @@ export class SessionBookmark { } /* -* SessionWindow -* Used as separator for different windows -*/ + * SessionWindow + * Used as separator for different windows + */ export class SessionWindow { constructor(windowID, windowIndex, index) { const DomElem = HTMLCreator(); @@ -135,9 +135,9 @@ export class SessionWindow { } /* -* SessionTab -* Used for displaying tabs from the current window -*/ + * SessionTab + * Used for displaying tabs from the current window + */ export class SessionTab { constructor(tab, indexOffset) { const DomElem = HTMLCreator(); @@ -191,9 +191,9 @@ export class SessionTab { } /* -* HistoryWindow -* Used as separator for different windows -*/ + * HistoryWindow + * Used as separator for different windows + */ export class HistoryWindow { constructor(sessionWindowInfo, windowID, index) { const DomElem = HTMLCreator(); @@ -217,9 +217,9 @@ export class HistoryWindow { } /* -* HistoryTab -* Used for displaying tabs from the history windows -*/ + * HistoryTab + * Used for displaying tabs from the history windows + */ export class HistoryTab { constructor(tab, indexOffset) { const DomElem = HTMLCreator(); diff --git a/src/scripts/components/session-container.js b/src/scripts/components/session-container.js index 62194d2..e5a51c6 100644 --- a/src/scripts/components/session-container.js +++ b/src/scripts/components/session-container.js @@ -23,9 +23,9 @@ import { SessionHistory } from './session-history'; // API /* -* SessionContainer UI -* Panel for listing session information (Firefox Bookmark Folder) and children bookmarks -*/ + * SessionContainer UI + * Panel for listing session information (Firefox Bookmark Folder) and children bookmarks + */ export class SessionContainer { constructor() { @@ -44,14 +44,16 @@ export class SessionContainer { // User events function isValidSessionID(ID) { - return (SessionSyncModel.bookmarks[ID] !== undefined && + return ( + SessionSyncModel.bookmarks[ID] !== undefined && SessionSyncModel.bookmarks[ID].type === 'folder' && - SessionSyncModel.bookmarks[ID].parentId == AppConfig.get('storageID')); + SessionSyncModel.bookmarks[ID].parentId == AppConfig.get('storageID') + ); } function restoreHistorySession(sessionInfo) { var list = []; - sessionInfo.windows.forEach(historyWindow => { + sessionInfo.windows.forEach((historyWindow) => { list.push(historyWindow.tabs); }); SessionManager.loadHistoryTabsNewWindow(list); @@ -72,12 +74,10 @@ export class SessionContainer { var activeSessionWindows = Object.values(this.selectedHistorySession.windows); if (activeSessionWindows[windowID].active) { SessionManager.activateWindow(activeSessionWindows[windowID].id); - } - else { + } else { SessionManager.loadHistoryTabsNewWindow([activeSessionWindows[windowID].tabs]); } - } - else { + } else { SessionManager.loadHistoryTabsNewWindow([this.selectedHistorySession.windows[windowID].tabs]); } } @@ -90,7 +90,6 @@ export class SessionContainer { }); WindowEvents.on(document, 'MenuRestoreNewWindow', () => { - if (isValidSessionID(this.activeSessionID)) { SessionManager.restoreNewWindow(this.activeSessionID); } @@ -158,7 +157,6 @@ export class SessionContainer { this.saveHistorySession(); break; } - }); WindowEvents.on(document, 'EditSelectedSession', () => { @@ -229,7 +227,7 @@ export class SessionContainer { setUIState(state, headerTitle, sessionID) { if (typeof document !== 'undefined') { - WindowEvents.emit(document, 'SetUIState', { 'session': state }); + WindowEvents.emit(document, 'SetUIState', { session: state }); WindowEvents.emit(document, 'SetSessionDescription', headerTitle); } @@ -251,7 +249,6 @@ export class SessionContainer { this.setUIState('current', 'Active session', 0); SessionManager.getCurrentWindow((mozWindow) => { - // Get non-private windows if (mozWindow.incognito == true || AppConfig.get('session.save').allWindows == false) { mozWindow.tabs.forEach((tab) => { @@ -261,11 +258,10 @@ export class SessionContainer { }); this.restoreScrollTop('current'); - } - else { + } else { // Get non-private windows var tabIndex = 0; - var windowIndex = 0; // display index in UI - ignores private windows + var windowIndex = 0; // display index in UI - ignores private windows SessionManager.getAllWindows((windows) => { for (let mozWindow of windows) { @@ -297,9 +293,9 @@ export class SessionContainer { } showHistorySession(sessionInfo) { - var newSession = (this.selectedHistorySession != undefined) && (this.selectedHistorySession != sessionInfo); + var newSession = this.selectedHistorySession != undefined && this.selectedHistorySession != sessionInfo; - this.setUIState('history', (new Date(sessionInfo.lastSave)).toLocaleString(), -1); + this.setUIState('history', new Date(sessionInfo.lastSave).toLocaleString(), -1); this.selectedHistorySession = sessionInfo; HistoryManager.selectSession(sessionInfo); @@ -339,7 +335,6 @@ export class SessionContainer { this.setUIState('restore', '', sessionID); BookmarkManager.getFolderBookmarks(session.id, (marks) => { - if (typeof document !== 'undefined') { WindowEvents.emit(document, 'SetPromiseSession', { sessionID: session.id, update: false }); } @@ -355,19 +350,17 @@ export class SessionContainer { this.updateSessionInfo(); this.restoreScrollTop('restore'); - }); } restoreScrollTop(state, forcedValue) { var scrollTop = AppConfig.get('state.scrollTop.' + state); - this.DOMBookmarks.scrollTop = (forcedValue !== undefined) ? forcedValue : scrollTop; + this.DOMBookmarks.scrollTop = forcedValue !== undefined ? forcedValue : scrollTop; } bookmarkCurrentTab() { // Test if already saved in this session SessionManager.getCurrentTab((activeTab) => { - var sessionID = this.activeSessionID; var bookmarks = SessionSyncModel.bookmarks; @@ -375,7 +368,7 @@ export class SessionContainer { if (bookmarks[i].parentId == sessionID && bookmarks[i].url == activeTab.url) { if (typeof document !== 'undefined') { WindowEvents.emit(document, 'Notification', { - message: 'Already Saved', + message: 'Already Saved' }); } @@ -387,19 +380,16 @@ export class SessionContainer { // Add bookmark into the session - BookmarkManager.createBookmarkFromTab(activeTab, sessionID) - .then((mark) => { - var bookmark = new SessionBookmark(mark); - - SessionSyncModel.bookmarks[mark.id] = mark; - this.SyncModel.setBookmark(mark.id, bookmark); - this.DOMBookmarks.appendChild(bookmark.DOMRoot); - - bookmark.setVirtualPosition(mark.index); - bookmark.highlight(); + BookmarkManager.createBookmarkFromTab(activeTab, sessionID).then((mark) => { + var bookmark = new SessionBookmark(mark); - }); + SessionSyncModel.bookmarks[mark.id] = mark; + this.SyncModel.setBookmark(mark.id, bookmark); + this.DOMBookmarks.appendChild(bookmark.DOMRoot); + bookmark.setVirtualPosition(mark.index); + bookmark.highlight(); + }); }); } @@ -407,8 +397,7 @@ export class SessionContainer { if (sessionIndex == undefined) { // When Save button from toolbar is pressed SessionManager.saveHistorySession(this.selectedHistorySession); - } - else { + } else { // Save from context menu SessionHistory.getHistorySession(sessionIndex, function (sessionInfo) { SessionManager.saveHistorySession(sessionInfo); diff --git a/src/scripts/components/session-filtering.js b/src/scripts/components/session-filtering.js index bc0a1ff..007b2ce 100644 --- a/src/scripts/components/session-filtering.js +++ b/src/scripts/components/session-filtering.js @@ -11,8 +11,8 @@ import { HTMLCreator } from '../utils/dom'; // API /* -* SessionFiltering -*/ + * SessionFiltering + */ export function SessionFiltering() { var DomElem = HTMLCreator(); @@ -53,8 +53,7 @@ export function SessionFiltering() { }; var onKeyDown = function () { - if (prevValue == searchInput.value) - return; + if (prevValue == searchInput.value) return; prevValue = searchInput.value; panel.setAttribute('match', 'false'); @@ -85,7 +84,7 @@ export function SessionFiltering() { createSession.addEventListener('mouseover', function (e) { WindowEvents.emit(document, 'ShowTooltip', { node: e.target, - message: 'Create new session', + message: 'Create new session' }); }); @@ -128,4 +127,4 @@ export function SessionFiltering() { // Public properties this.DOMRoot = panel; -} \ No newline at end of file +} diff --git a/src/scripts/components/session-folder-events.js b/src/scripts/components/session-folder-events.js index 4b61c36..f729336 100644 --- a/src/scripts/components/session-folder-events.js +++ b/src/scripts/components/session-folder-events.js @@ -28,8 +28,7 @@ export function SessionFolderEvents(container) { function getAttribute(target, attribute) { try { return target.getAttribute(attribute); - } - catch (err) { + } catch (err) { console.log(err); return null; } @@ -79,8 +78,7 @@ export function SessionFolderEvents(container) { }; var mouseDragging = function mouseMove(e) { - if (this != window) - return; + if (this != window) return; var deltaTime = new Date() - trackingTelemetry; if (deltaTime < 16) { return; @@ -90,10 +88,9 @@ export function SessionFolderEvents(container) { if (tryScroll) { if (e.clientY < offsetRect.top) { - AutoScroll.scroll(container, (e.clientY - offsetRect.top)); - } - else if (e.clientY > offsetRect.bottom) { - AutoScroll.scroll(container, (e.clientY - offsetRect.bottom)); + AutoScroll.scroll(container, e.clientY - offsetRect.top); + } else if (e.clientY > offsetRect.bottom) { + AutoScroll.scroll(container, e.clientY - offsetRect.bottom); } else { AutoScroll.stop(); } @@ -118,16 +115,14 @@ export function SessionFolderEvents(container) { WindowEvents.emit(document, 'SelectSyncSession', bookmarkContext); } } - } - else { + } else { if (AppConfig.get('session.sorting') === 'position-asc') { SessionSyncModel.moveBookmark(bookmarkContext.bookmarkID, sessionID); - } - else { + } else { if (typeof document !== 'undefined') { WindowEvents.emit(document, 'Notification', { message: 'Rearage function available only when items are sorted ascending by position', - timeout: 3000, + timeout: 3000 }); } } @@ -136,8 +131,7 @@ export function SessionFolderEvents(container) { if (DragContext.hasContext()) { cancelDragging(e); - } - else { + } else { window.removeEventListener('mousemove', trackDragging); } @@ -187,8 +181,7 @@ export function SessionFolderEvents(container) { context: bookmarkContext.bookmark.id, event: e }); - } - else { + } else { WindowEvents.emit(document, 'SessionListMenu-Open', { event: e }); } }; @@ -221,10 +214,8 @@ export function SessionFolderEvents(container) { // History Session Folder Events - delegated export function SessionHistoryEvents(container) { - var onMouseDown = function onMouseDown(e) { - if (e.button != 2) - return; + if (e.button != 2) return; var target = e.target; if (target.className == 'history-node') { @@ -234,8 +225,7 @@ export function SessionHistoryEvents(container) { event: e }); } - } - else { + } else { if (typeof document !== 'undefined') { WindowEvents.emit(document, 'HistoryListCtxMenu-Open', { event: e }); } diff --git a/src/scripts/components/session-folder.js b/src/scripts/components/session-folder.js index 712a289..0830264 100644 --- a/src/scripts/components/session-folder.js +++ b/src/scripts/components/session-folder.js @@ -10,9 +10,9 @@ import { HTMLCreator } from '../utils/dom'; // API /* -* Session Folder -* Used for interacting with a Firefox bookmark item -*/ + * Session Folder + * Used for interacting with a Firefox bookmark item + */ export class SessionFolder { static folderOffset = 2.7; // em; @@ -89,9 +89,9 @@ export const SessionFolderSortBy = { }; /* -* SessionHistoryFolder -* Used as separator for different windows -*/ + * SessionHistoryFolder + * Used as separator for different windows + */ export class SessionHistoryFolder { constructor(sessionInfo, index) { const DomElem = HTMLCreator(); diff --git a/src/scripts/components/session-header-bar.js b/src/scripts/components/session-header-bar.js index f332ff0..aa2b7d8 100644 --- a/src/scripts/components/session-header-bar.js +++ b/src/scripts/components/session-header-bar.js @@ -60,7 +60,7 @@ export function SessionHeaderBar() { if (typeof document !== 'undefined') { WindowEvents.emit(document, 'ShowTooltip', { node: e.target, - message: e.target.getAttribute('tooltip'), + message: e.target.getAttribute('tooltip') }); } } else { @@ -116,10 +116,7 @@ var CreateHeaderMenu = function CreateHeaderMenu() { icon.style.backgroundImage = url; // Events - var callback = - typeof JSUtils.getValidFunction === 'function' - ? JSUtils.getValidFunction(options.callback) - : options.callback; + var callback = typeof JSUtils.getValidFunction === 'function' ? JSUtils.getValidFunction(options.callback) : options.callback; button.addEventListener('click', callback); button.appendChild(icon); @@ -136,7 +133,7 @@ var CreateHeaderMenu = function CreateHeaderMenu() { var state = model.state['config'] == undefined ? 'on' : undefined; WindowEvents.emit(document, 'SetUIState', { config: state }); } - }, + } }); var openTutorial = MenuButton({ @@ -146,7 +143,7 @@ var CreateHeaderMenu = function CreateHeaderMenu() { if (typeof browser !== 'undefined' && browser.runtime) { browser.runtime.sendMessage({ event: 'session_sync_tutorial' }); } - }, + } }); var leaveFeedback = MenuButton({ @@ -156,7 +153,7 @@ var CreateHeaderMenu = function CreateHeaderMenu() { if (typeof browser !== 'undefined' && browser.runtime) { browser.runtime.sendMessage({ event: 'session_sync_leave_feedback' }); } - }, + } }); var exportImport = MenuButton({ @@ -166,10 +163,10 @@ var CreateHeaderMenu = function CreateHeaderMenu() { if (typeof browser !== 'undefined' && browser.tabs) { browser.tabs.create({ url: 'home/home.html#export-import', - active: true, + active: true }); } - }, + } }); var buttonDetachInTab = MenuButton({ @@ -179,7 +176,7 @@ var CreateHeaderMenu = function CreateHeaderMenu() { if (typeof browser !== 'undefined' && browser.runtime) { browser.runtime.sendMessage({ event: 'session_sync_detach_tab' }); } - }, + } }); var buttonDetachInWindow = MenuButton({ @@ -189,7 +186,7 @@ var CreateHeaderMenu = function CreateHeaderMenu() { if (typeof browser !== 'undefined' && browser.runtime) { browser.runtime.sendMessage({ event: 'session_sync_detach_window' }); } - }, + } }); var githubPage = MenuButton({ @@ -199,7 +196,7 @@ var CreateHeaderMenu = function CreateHeaderMenu() { if (typeof browser !== 'undefined' && browser.runtime) { browser.runtime.sendMessage({ event: 'session_sync_open_github' }); } - }, + } }); menuRow1.appendChild(openConfig); @@ -229,7 +226,7 @@ var CreateHeaderMenu = function CreateHeaderMenu() { this.manualTrigger = 0; } } - }, + } }; function initDevMode() { @@ -247,7 +244,7 @@ var CreateHeaderMenu = function CreateHeaderMenu() { console.log(tab); }); } - }, + } }); var localStorage = MenuButton({ @@ -259,7 +256,7 @@ var CreateHeaderMenu = function CreateHeaderMenu() { console.log(data); }); } - }, + } }); var clearFaviconCache = MenuButton({ @@ -275,7 +272,7 @@ var CreateHeaderMenu = function CreateHeaderMenu() { } }); } - }, + } }); devRow1.appendChild(activeTabInfo); diff --git a/src/scripts/components/session-history.js b/src/scripts/components/session-history.js index 086d404..989ba16 100644 --- a/src/scripts/components/session-history.js +++ b/src/scripts/components/session-history.js @@ -9,7 +9,6 @@ import { WindowEvents, GlobalEvents } from '../utils/global-events'; // API export const SessionHistory = (function SessionHistory() { - var storageKey = { config: 'history.config', active: 'history.active', @@ -39,8 +38,7 @@ export const SessionHistory = (function SessionHistory() { var getHistorySession = function getHistorySession(index, callback) { if (index >= 0 && index < sessions.length) { callback(sessions[index]); - } - else if (typeof browser !== 'undefined' && browser.storage) { + } else if (typeof browser !== 'undefined' && browser.storage) { browser.storage.local.get(storageKey.active).then(function (obj) { callback(obj[storageKey.active]); }); @@ -63,10 +61,9 @@ export const SessionHistory = (function SessionHistory() { var updateSessions = function updateSessions(sessions) { if (typeof browser !== 'undefined' && browser.storage) { - browser.storage.local.set({ [storageKey.list]: sessions }) - .then(function () { - WindowEvents.emit(document, 'ShowHistoryList'); - }); + browser.storage.local.set({ [storageKey.list]: sessions }).then(function () { + WindowEvents.emit(document, 'ShowHistoryList'); + }); } }; @@ -81,8 +78,7 @@ export const SessionHistory = (function SessionHistory() { if (typeof browser !== 'undefined' && browser.runtime) { browser.runtime.sendMessage({ event: 'history.deleteActive' }); } - } - else { + } else { getFullHistory(function (sessions) { if (index >= 0 && index < sessions.length) { sessions.splice(index, 1); @@ -115,5 +111,4 @@ export const SessionHistory = (function SessionHistory() { getHistorySession: getHistorySession, updateConfig: updateConfig }; - })(); diff --git a/src/scripts/components/session-hotkey-manager.js b/src/scripts/components/session-hotkey-manager.js index 87e28ae..0792862 100644 --- a/src/scripts/components/session-hotkey-manager.js +++ b/src/scripts/components/session-hotkey-manager.js @@ -14,8 +14,8 @@ import { WindowEvents } from '../utils/global-events'; // API /* -* Session Hotkeys -*/ + * Session Hotkeys + */ export function SessionHotkeyManager() { // Create DomHealper @@ -195,12 +195,10 @@ export function SessionHotkeyManager() { }); function changeHotKey(options) { - changeCallback = options.callback; var tokens = options.shortcut.split('+'); - header.textContent = options.title, - currentHotKey.setValue(tokens.join(' + ')); + ((header.textContent = options.title), currentHotKey.setValue(tokens.join(' + '))); newHotKey.setValue(''); modifierKey.setValue(tokens[0]); optionalModifier.setValue(tokens.length == 3 ? tokens[1] : ''); diff --git a/src/scripts/components/session-hotkeys.js b/src/scripts/components/session-hotkeys.js index d4d6d04..565b314 100644 --- a/src/scripts/components/session-hotkeys.js +++ b/src/scripts/components/session-hotkeys.js @@ -10,8 +10,8 @@ import { WindowEvents } from '../utils/global-events'; // API /* -* Session Hotkeys -*/ + * Session Hotkeys + */ export function SessionHotkeys() { // ------------------------------------------------------------------------ @@ -63,5 +63,4 @@ export function SessionHotkeys() { // ------------------------------------------------------------------------ // Public properties - -} \ No newline at end of file +} diff --git a/src/scripts/components/session-list.js b/src/scripts/components/session-list.js index 469462d..d81fb6b 100644 --- a/src/scripts/components/session-list.js +++ b/src/scripts/components/session-list.js @@ -18,9 +18,9 @@ import { SessionHistory } from './session-history'; // API /* -* Session List -* UI Module for interacting with all the session -*/ + * Session List + * UI Module for interacting with all the session + */ export class SessionList { constructor() { @@ -80,72 +80,107 @@ export class SessionList { if (typeof document !== 'undefined') { WindowEvents.on(document, 'ListSessions', this.setSessions.bind(this)); - WindowEvents.on(document, 'SetPromiseSession', function (sessionInfo) { - this.promiseInfo = sessionInfo; - WindowEvents.emit(document, 'ShowSyncList', { update: sessionInfo.update }); - }.bind(this)); - - WindowEvents.on(document, 'SelectSyncSession', function (context) { - this.selectSyncSession(context, false); - }.bind(this)); + WindowEvents.on( + document, + 'SetPromiseSession', + function (sessionInfo) { + this.promiseInfo = sessionInfo; + WindowEvents.emit(document, 'ShowSyncList', { update: sessionInfo.update }); + }.bind(this) + ); + + WindowEvents.on( + document, + 'SelectSyncSession', + function (context) { + this.selectSyncSession(context, false); + }.bind(this) + ); WindowEvents.on(document, 'FilterSessions', this.filterSessions.bind(this)); WindowEvents.on(document, 'SortSessionsBy', this.sortSessionsBy.bind(this)); - WindowEvents.on(document, 'SessionsPositionChanged', function (indices) { - if (AppConfig.get('session.sorting') == 'position-asc' && AppConfig.get('session.active.filter').length == 0) { - var len = indices.length; - for (var i = 0; i < len; i++) { - var node = this.SyncModel.getBookmark(indices[i]); - node.setVirtualPosition(SessionSyncModel.bookmarks[node.bookmarkID].index); + WindowEvents.on( + document, + 'SessionsPositionChanged', + function (indices) { + if (AppConfig.get('session.sorting') == 'position-asc' && AppConfig.get('session.active.filter').length == 0) { + var len = indices.length; + for (var i = 0; i < len; i++) { + var node = this.SyncModel.getBookmark(indices[i]); + node.setVirtualPosition(SessionSyncModel.bookmarks[node.bookmarkID].index); + } } - } - }.bind(this)); - - WindowEvents.on(document, 'ShowCurrentSession', function () { - if (AppConfig.get('session.selected') != null) { - AppConfig.set('session.selected', null); - AppConfig.set('state.scrollTop.current', 0); - } - this.setState('active'); - this.setSelectedNode(undefined); - }.bind(this)); - - WindowEvents.on(document, 'ShowSyncList', function (options) { - // Select first visible session - if (this.promiseInfo.sessionID == null) { - for (var i = 0; i < this.sessions.length; i++) { - if (this.sessions[i].isVisible()) { - this.selectSyncSession(this.sessions[i], true, false); - break; + }.bind(this) + ); + + WindowEvents.on( + document, + 'ShowCurrentSession', + function () { + if (AppConfig.get('session.selected') != null) { + AppConfig.set('session.selected', null); + AppConfig.set('state.scrollTop.current', 0); + } + this.setState('active'); + this.setSelectedNode(undefined); + }.bind(this) + ); + + WindowEvents.on( + document, + 'ShowSyncList', + function (options) { + // Select first visible session + if (this.promiseInfo.sessionID == null) { + for (var i = 0; i < this.sessions.length; i++) { + if (this.sessions[i].isVisible()) { + this.selectSyncSession(this.sessions[i], true, false); + break; + } } } - } - this.setState('sync'); - if (options.update == true) { - GlobalEvents.emit('update-sessions'); - } - }.bind(this)); - - WindowEvents.on(document, 'SessionDescriptionChanged', function () { - this.sortSessionsBy(AppConfig.get('session.sorting')); - this.scrollToSelected(); - }.bind(this)); - - WindowEvents.on(document, 'SessionScrollToSelected', function () { - this.scrollToSelected(); - }.bind(this)); - - GlobalEvents.on('BookmarkDeleted', function (itemID) { - if (AppConfig.get('session.selected') == itemID) { - WindowEvents.emit(this.document, 'ShowCurrentSession'); - } - }.bind(this)); - - WindowEvents.on(document, 'SelectHistorySession', function (context) { - this.selectHistorySession(context); - }.bind(this)); + this.setState('sync'); + if (options.update == true) { + GlobalEvents.emit('update-sessions'); + } + }.bind(this) + ); + + WindowEvents.on( + document, + 'SessionDescriptionChanged', + function () { + this.sortSessionsBy(AppConfig.get('session.sorting')); + this.scrollToSelected(); + }.bind(this) + ); + + WindowEvents.on( + document, + 'SessionScrollToSelected', + function () { + this.scrollToSelected(); + }.bind(this) + ); + + GlobalEvents.on( + 'BookmarkDeleted', + function (itemID) { + if (AppConfig.get('session.selected') == itemID) { + WindowEvents.emit(this.document, 'ShowCurrentSession'); + } + }.bind(this) + ); + + WindowEvents.on( + document, + 'SelectHistorySession', + function (context) { + this.selectHistorySession(context); + }.bind(this) + ); } AppConfig.onChange('session.selected', function (value) { @@ -197,7 +232,6 @@ export class SessionList { historyList.textContent = ''; SessionHistory.getFullHistory((historySessions, activeSession) => { - if (activeSession) { let activeSnapshot = new SessionHistoryFolder(activeSession, -1); activeSnapshot.setTitle('Active snapshot'); @@ -211,7 +245,11 @@ export class SessionList { if (i == selectedIndex) { var node = historySession.DOMRoot; - AutoScroll.scrollTo(historyList, node.offsetTop - historyList.parentNode.clientHeight / 2 + node.clientHeight, snap === true ? 0 : 0.25); + AutoScroll.scrollTo( + historyList, + node.offsetTop - historyList.parentNode.clientHeight / 2 + node.clientHeight, + snap === true ? 0 : 0.25 + ); } } } @@ -264,36 +302,35 @@ export class SessionList { var selectedSession; var filterExpression = AppConfig.get('session.active.filter'); - SessionSyncModel.sessions.forEach(function (session) { - - SessionSyncModel.bookmarks[session.id] = session; - - // find if a cache instance is already created and update it - var sessionFolder = this.SyncModel.getBookmark(session.id); - if (sessionFolder instanceof SessionFolder) { - sessionFolder.updateFrom(session); - } - else { - sessionFolder = new SessionFolder(session); - sessionFolder.setVirtualPosition(session.index); - this.SyncModel.setBookmark(session.id, sessionFolder); - } - - if (session.id === this.promiseInfo.sessionID) { - selectedSession = sessionFolder; - } + SessionSyncModel.sessions.forEach( + function (session) { + SessionSyncModel.bookmarks[session.id] = session; + + // find if a cache instance is already created and update it + var sessionFolder = this.SyncModel.getBookmark(session.id); + if (sessionFolder instanceof SessionFolder) { + sessionFolder.updateFrom(session); + } else { + sessionFolder = new SessionFolder(session); + sessionFolder.setVirtualPosition(session.index); + this.SyncModel.setBookmark(session.id, sessionFolder); + } - if (filterExpression.length > 0) { - if (sessionFolder.match(filterExpression)) { - sessionFolder.setVirtualPosition(position); - position++; + if (session.id === this.promiseInfo.sessionID) { + selectedSession = sessionFolder; } - } - this.sessions.push(sessionFolder); - this.DOMList.appendChild(sessionFolder.DOMRoot); + if (filterExpression.length > 0) { + if (sessionFolder.match(filterExpression)) { + sessionFolder.setVirtualPosition(position); + position++; + } + } - }.bind(this)); + this.sessions.push(sessionFolder); + this.DOMList.appendChild(sessionFolder.DOMRoot); + }.bind(this) + ); if (filterExpression.length > 0) { this.filterSessions(filterExpression); @@ -321,8 +358,9 @@ export class SessionList { } scrollToSelected(snap) { - if (this.selectedNode == undefined) + if (this.selectedNode == undefined) { return; + } var offset = this.selectedNode.offsetTop - this.DOMList.clientHeight / 2 + this.selectedNode.clientHeight; @@ -388,8 +426,7 @@ export class SessionList { if (doc) { WindowEvents.emit(doc, 'FilterSessionsExactMatch', exactMatch); } - } - else { + } else { this.sessions.forEach(function (session) { session.setVisible(); }); @@ -408,4 +445,4 @@ export class SessionList { } }); } -} \ No newline at end of file +} diff --git a/src/scripts/components/session-sorting.js b/src/scripts/components/session-sorting.js index 754f4ea..463d2ec 100644 --- a/src/scripts/components/session-sorting.js +++ b/src/scripts/components/session-sorting.js @@ -37,7 +37,6 @@ export function SessionSorting() { var option = DomElem('div', { class: 'option', value: key }); option.textContent = options[key].description; optionList.appendChild(option); - } var dropdown = DomElem('div', { class: 'dropdown' }); @@ -78,10 +77,8 @@ export function SessionSorting() { document.addEventListener('click', function (e) { if (e.target.className == 'sorting-method') { toggleState(); - } - else { - if (state == true) - toggleState(); + } else { + if (state == true) toggleState(); } }); diff --git a/src/scripts/components/session-sync-model.js b/src/scripts/components/session-sync-model.js index bc33fce..f5b1b3b 100644 --- a/src/scripts/components/session-sync-model.js +++ b/src/scripts/components/session-sync-model.js @@ -66,7 +66,6 @@ export class SessionSyncDOMModel { } export const SessionSyncModel = (function SessionSync() { - // Bookmarks storage var bookmarks = {}; @@ -78,7 +77,7 @@ export const SessionSyncModel = (function SessionSync() { var getModel = function getModel(doc) { var model = domModels.get(doc); - if ((model instanceof SessionSyncDOMModel) === false) { + if (model instanceof SessionSyncDOMModel === false) { model = new SessionSyncDOMModel(doc); } return model; @@ -141,8 +140,9 @@ export const SessionSyncModel = (function SessionSync() { // Moving a bookmark to a new session directory var moveBookmarkTo = function moveBookmarkTo(bookmarkID, folderID) { var parentID = bookmarks[bookmarkID].parentId; - if (parentID == folderID) + if (parentID == folderID) { return; + } BookmarkManager.moveItem(bookmarkID, folderID, undefined, onSuccess, onActionError); @@ -304,7 +304,6 @@ export const SessionSyncModel = (function SessionSync() { WindowEvents.emitLocal('Notification', { message: 'Success' }); - } catch (err) { console.log(err); WindowEvents.emitLocal('Notification', { @@ -341,11 +340,10 @@ export const SessionSyncModel = (function SessionSync() { switch (historyEvent.bookmark.type) { case 'bookmark': case 'separator': { - recreateBookmark(historyEvent.bookmark) - .then(function (bookmark) { - sessionID = (bookmark.parentId == AppConfig.get('storageID')) ? bookmark.id : bookmark.parentId; - onSuccess(); - }); + recreateBookmark(historyEvent.bookmark).then(function (bookmark) { + sessionID = bookmark.parentId == AppConfig.get('storageID') ? bookmark.id : bookmark.parentId; + onSuccess(); + }); break; } @@ -353,35 +351,33 @@ export const SessionSyncModel = (function SessionSync() { // Restore deleted folder var oldID = historyEvent.bookmark.id; - recreateBookmark(historyEvent.bookmark) - .then(function (node) { - sessionID = node.id; - var newID = node.id; - var updateEvent = new LimitCounter(historyEvent.items.length, onSuccess); - - // correct parent for each bookmark - if (oldID != newID) { - // correct restore event - historyEvent.items.forEach(function (bookmark) { - bookmark.parentId = newID; - recreateBookmark(bookmark) - .then(updateEvent.advance, updateEvent.advance); + recreateBookmark(historyEvent.bookmark).then(function (node) { + sessionID = node.id; + var newID = node.id; + var updateEvent = new LimitCounter(historyEvent.items.length, onSuccess); + + // correct parent for each bookmark + if (oldID != newID) { + // correct restore event + historyEvent.items.forEach(function (bookmark) { + bookmark.parentId = newID; + recreateBookmark(bookmark).then(updateEvent.advance, updateEvent.advance); + }); + + // correct all undo events + undoEvents.forEach(function (event) { + event.items.forEach(function (bookmark) { + if (bookmark.parentId == oldID) { + bookmark.parentId = newID; + } }); + }); + } - // correct all undo events - undoEvents.forEach(function (event) { - event.items.forEach(function (bookmark) { - if (bookmark.parentId == oldID) { - bookmark.parentId = newID; - } - }); - }); - } - - if (historyEvent.items.length == 0) { - onSuccess(); - } - }); + if (historyEvent.items.length == 0) { + onSuccess(); + } + }); break; } } @@ -407,7 +403,6 @@ export const SessionSyncModel = (function SessionSync() { moveBookmarkTo: moveBookmarkTo, deleteBookmarkList: deleteBookmarkList, - updateBookmarkItem: updateBookmarkItem, + updateBookmarkItem: updateBookmarkItem }; - })(); diff --git a/src/scripts/components/session-toolbar.js b/src/scripts/components/session-toolbar.js index f8e54cd..b28fb9e 100644 --- a/src/scripts/components/session-toolbar.js +++ b/src/scripts/components/session-toolbar.js @@ -15,8 +15,8 @@ import * as DOMComponent from '../utils/components'; // API /* -* Command bar -*/ + * Command bar + */ export function SessionToolbar() { // Create DomHealper @@ -61,7 +61,7 @@ export function SessionToolbar() { if (typeof document !== 'undefined') { WindowEvents.on(document, 'SetSessionInfo', function (sessionInfo) { - sessionDate.textContent = (new Date(sessionInfo.dateAdded)).toLocaleString(); + sessionDate.textContent = new Date(sessionInfo.dateAdded).toLocaleString(); toolbar.setAttribute('session-type', sessionInfo.type); }); } @@ -112,7 +112,7 @@ SessionToolbar.prototype.createMenu = function createMenu() { class: 'button ' + options.className, 'state-current': options.state.current, 'state-restore': options.state.restore, - 'state-history': options.state.history, + 'state-history': options.state.history }); button.setAttribute('tooltip', options.tooltip); @@ -292,4 +292,4 @@ SessionToolbar.prototype.createMenu = function createMenu() { // Data return menu; -}; \ No newline at end of file +}; diff --git a/src/scripts/components/tooltip-system.js b/src/scripts/components/tooltip-system.js index 090ebb9..25f58b6 100644 --- a/src/scripts/components/tooltip-system.js +++ b/src/scripts/components/tooltip-system.js @@ -38,4 +38,4 @@ export function TooltipSystem() { // Public data this.DOMRoot = tooltip; -} \ No newline at end of file +} diff --git a/src/scripts/components/trash-can.js b/src/scripts/components/trash-can.js index f428fc4..f886708 100644 --- a/src/scripts/components/trash-can.js +++ b/src/scripts/components/trash-can.js @@ -16,7 +16,6 @@ import { SessionBookmark, SessionTab } from './session-bookmark'; // API export function TrashCan() { - // Create DomHealper var DomElem = HTMLCreator(); @@ -42,7 +41,7 @@ export function TrashCan() { var configKey = { hide: 'trashcan.hide', hideCount: 'trashcan.hide-count', - undoEvents: 'undo.events', + undoEvents: 'undo.events' }; // ------------------------------------------------------------------------ diff --git a/src/scripts/config.js b/src/scripts/config.js index ba710b1..df3cea2 100644 --- a/src/scripts/config.js +++ b/src/scripts/config.js @@ -12,7 +12,6 @@ import { GlobalEvents } from './utils/global-events'; // API const Config = (function () { - const defaults = { 'style.panel.width': 800, 'style.panel.height': 600, @@ -48,7 +47,7 @@ const Config = (function () { 'trashcan.hide': true, 'trashcan.hide-count': 0, 'undo.events': [], - 'troubleshooting': false, + troubleshooting: false, 'theme.toolbar.grayscale': false }; @@ -56,7 +55,7 @@ const Config = (function () { var initConfig = true; var localSettings = defaults; var configEvents = new PubSub(); - var isPanelUI = (typeof document !== 'undefined' && document.body) ? (document.body.clientWidth == 0) : false; + var isPanelUI = typeof document !== 'undefined' && document.body ? document.body.clientWidth == 0 : false; // ------------------------------------------------------------------------ // API @@ -84,17 +83,19 @@ const Config = (function () { return; } - browser.storage.local.set({ [key]: value }) - .then(function success() { + browser.storage.local.set({ [key]: value }).then( + function success() { localSettings[key] = value; // console.log('[Change config]', key, value); configEvents.publish(key, value); if (typeof onSuccess === 'function') { onSuccess(); } - }, function onError(error) { + }, + function onError(error) { console.log(error); - }); + } + ); }; var get = function get(key) { @@ -137,15 +138,13 @@ const Config = (function () { console.log(localSettings); if (isAddonContext()) { - browser.commands.getAll(). - then(function (commands) { - configEvents.publish('config.hotkey.init', commands); - }); + browser.commands.getAll().then(function (commands) { + configEvents.publish('config.hotkey.init', commands); + }); } }; if (isAddonContext()) { - // console.log(''); // console.log('------------------------------------------'); @@ -180,4 +179,4 @@ const Config = (function () { }; })(); -export const AppConfig = Config; \ No newline at end of file +export const AppConfig = Config; diff --git a/src/scripts/services/firefox-containers.js b/src/scripts/services/firefox-containers.js index ef89c4b..2e48267 100644 --- a/src/scripts/services/firefox-containers.js +++ b/src/scripts/services/firefox-containers.js @@ -6,11 +6,10 @@ import { DomElem } from '../utils/dom'; // API /* -* Bookmark Properties -*/ + * Bookmark Properties + */ export class FirefoxContainers { - static defaultContainer = 'firefox-default'; static containers = []; static domContainers = {}; @@ -28,7 +27,6 @@ export class FirefoxContainers { // create dom containers for (const container of contexts) { - const domContainer = DomElem('div', { class: 'container' }); domContainer.style.backgroundColor = container.colorCode; @@ -59,6 +57,6 @@ export class FirefoxContainers { } static getContainerInfo(cookieStoreId) { - return FirefoxContainers.containers.find(container => container.cookieStoreId === cookieStoreId); + return FirefoxContainers.containers.find((container) => container.cookieStoreId === cookieStoreId); } -} \ No newline at end of file +} diff --git a/src/scripts/session-sync-ui.js b/src/scripts/session-sync-ui.js index 05bb16f..ebc9a8a 100644 --- a/src/scripts/session-sync-ui.js +++ b/src/scripts/session-sync-ui.js @@ -42,7 +42,6 @@ import { BookmarkManager } from './session/bookmarks'; // UI initilization export const SessionSyncUI = function SessionSyncUI() { - var DomElem = HTMLCreator(); document.body.setAttribute('panel', AppConfig.isPanel()); @@ -258,7 +257,7 @@ export const SessionSyncUI = function SessionSyncUI() { WindowEvents.emit(document, 'SessionFolderEditWidget-Invoke', { context: sessionID, fields: { - title: SessionSyncModel.bookmarks[sessionID].title, + title: SessionSyncModel.bookmarks[sessionID].title } }); break; @@ -282,7 +281,7 @@ export const SessionSyncUI = function SessionSyncUI() { context: bookmarkID, fields: { title: SessionSyncModel.bookmarks[bookmarkID].title, - url: SessionSyncModel.bookmarks[bookmarkID].url, + url: SessionSyncModel.bookmarks[bookmarkID].url } }); } @@ -425,8 +424,7 @@ export const SessionSyncUI = function SessionSyncUI() { AppConfig.onChange('style.panel.height', setPanelHeight); resizeHandle.addEventListener('mousedown', startResize); - } - else { + } else { window.addEventListener('resize', function () { var newWidth = Math.max(window.innerWidth, 500); var newHeight = Math.max(window.innerHeight, 400); @@ -451,11 +449,9 @@ export const SessionSyncUI = function SessionSyncUI() { document.body.addEventListener('contextmenu', preventDefault); document.body.addEventListener('mousedown', function (e) { // Middle Click - if (e.button == 1) - preventDefault(e); + if (e.button == 1) preventDefault(e); }); // ------------------------------------------------------------------------ // App Init - }; diff --git a/src/scripts/session/bookmarks.js b/src/scripts/session/bookmarks.js index 39647f1..8978aaa 100644 --- a/src/scripts/session/bookmarks.js +++ b/src/scripts/session/bookmarks.js @@ -10,7 +10,6 @@ import { BookmarkProperties } from '../../common/bookmark-properties'; // API export const BookmarkManager = (function BookmarkManager() { - var getHostName = function getHostName(url) { try { return new URL(url).hostname; @@ -21,12 +20,11 @@ export const BookmarkManager = (function BookmarkManager() { var getFaviconUrlFromStorage = function getFaviconUrlFromStorage(favIconUrl, callback) { var key = '@favIconUrl:' + getHostName(favIconUrl); - return browser.storage.local.get(key) - .then(function (obj) { - if (obj[key]) { - callback(obj[key]); - } - }); + return browser.storage.local.get(key).then(function (obj) { + if (obj[key]) { + callback(obj[key]); + } + }); }; var getFaviconUrl = function getFaviconUrl(url, callback) { @@ -74,7 +72,6 @@ export const BookmarkManager = (function BookmarkManager() { title: title, parentId: parentId }); - }; var createBookmark = function createBookmark(options) { @@ -88,7 +85,6 @@ export const BookmarkManager = (function BookmarkManager() { }; var openBookmark = function openBookmark(url, info) { - console.log('openBookmark', url, info); var options = { @@ -119,7 +115,6 @@ export const BookmarkManager = (function BookmarkManager() { }; var openHistoryTab = function openHistoryTab(tab, mode) { - console.log('openHistoryTab', tab, mode); var options = { @@ -145,44 +140,51 @@ export const BookmarkManager = (function BookmarkManager() { }; var setTitle = function setTitle(bookmark, title, callback) { - browser.bookmarks.update(bookmark.id, { - title: title - }).then(function success() { - bookmark.title = title; - bookmark.title_lowercase = title.toLowerCase(); - if (typeof callback === 'function') - callback(true); - }, function error() { - throw 'error'; - }); + browser.bookmarks + .update(bookmark.id, { + title: title + }) + .then( + function success() { + bookmark.title = title; + bookmark.title_lowercase = title.toLowerCase(); + if (typeof callback === 'function') callback(true); + }, + function error() { + throw 'error'; + } + ); }; var setLocation = function setLocation(bookmark, url) { - browser.bookmarks.update(bookmark.id, { - url: url - }).then(function success() { - bookmark.url = url; - }, function error() { - throw 'error'; - }); + browser.bookmarks + .update(bookmark.id, { + url: url + }) + .then( + function success() { + bookmark.url = url; + }, + function error() { + throw 'error'; + } + ); }; var moveItem = function moveItem(itemID, newParentID, index, callback, errorCallback) { - browser.bookmarks.move(itemID, { - parentId: newParentID, - index: index - }) + browser.bookmarks + .move(itemID, { + parentId: newParentID, + index: index + }) .then(callback, errorCallback); }; var deleteItem = function deleteItem(itemID, isFolder, callback, errorCallback) { if (isFolder) { - browser.bookmarks.removeTree(itemID) - .then(callback, errorCallback); - } - else { - browser.bookmarks.remove(itemID) - .then(callback, errorCallback); + browser.bookmarks.removeTree(itemID).then(callback, errorCallback); + } else { + browser.bookmarks.remove(itemID).then(callback, errorCallback); } }; @@ -202,5 +204,4 @@ export const BookmarkManager = (function BookmarkManager() { getFolderBookmarks: getFolderBookmarks }; - -})(); \ No newline at end of file +})(); diff --git a/src/scripts/session/history.js b/src/scripts/session/history.js index 618da9b..c67c57a 100644 --- a/src/scripts/session/history.js +++ b/src/scripts/session/history.js @@ -4,10 +4,8 @@ // API export const HistoryManager = (function HistoryManager() { - var selectedSessions; - var selectSession = function selectSession(sessionInfo) { selectedSessions = sessionInfo; console.log('selectedSessions', selectedSessions); @@ -26,5 +24,4 @@ export const HistoryManager = (function HistoryManager() { getSelectedSession: getSelectedSession, getHistoryTab: getHistoryTab }; - -})(); \ No newline at end of file +})(); diff --git a/src/scripts/session/management.js b/src/scripts/session/management.js index 54719ff..2b0e077 100644 --- a/src/scripts/session/management.js +++ b/src/scripts/session/management.js @@ -12,33 +12,30 @@ import { SessionSyncModel } from '../components/session-sync-model'; // ***************************************************************************** export const SessionManager = (function SessionManager() { - var trackTabs = true; var getCurrentTab = function getCurrentTab(callback) { - browser.tabs.query({ active: true, windowId: browser.windows.WINDOW_ID_CURRENT }) - .then(function (tabs) { - callback(tabs[0]); - }); + browser.tabs.query({ active: true, windowId: browser.windows.WINDOW_ID_CURRENT }).then(function (tabs) { + callback(tabs[0]); + }); }; var getWindowTab = function getWindowTab(tabIndex, windowId, callback) { - browser.tabs.query({ index: tabIndex, windowId: windowId }) - .then(function (tabs) { - callback(tabs[0]); - }); + browser.tabs.query({ index: tabIndex, windowId: windowId }).then(function (tabs) { + callback(tabs[0]); + }); }; var getCurrentWindow = function getCurrentWindow(callback) { - browser.windows.getCurrent({ populate: true }) - .then(callback); + browser.windows.getCurrent({ populate: true }).then(callback); }; var getAllWindows = function getAllWindows(callback) { - browser.windows.getAll({ - populate: true, - windowTypes: ['normal'] - }) + browser.windows + .getAll({ + populate: true, + windowTypes: ['normal'] + }) .then(callback); }; @@ -48,15 +45,20 @@ export const SessionManager = (function SessionManager() { var moveTab = function moveTab(tabID, newIndex, windowID) { trackTabs = false; - browser.tabs.move([tabID], { - index: newIndex, - windowId: windowID - }).then(function () { - GlobalEvents.emit('UpdateCurrentSession'); - trackTabs = true; - }, function onError() { - trackTabs = true; - }); + browser.tabs + .move([tabID], { + index: newIndex, + windowId: windowID + }) + .then( + function () { + GlobalEvents.emit('UpdateCurrentSession'); + trackTabs = true; + }, + function onError() { + trackTabs = true; + } + ); }; var activateTab = function activateTab(tabID) { @@ -77,7 +79,6 @@ export const SessionManager = (function SessionManager() { var saveActiveSession = function saveActiveSession() { getCurrentWindow(function (mozWindow) { - var sessionID = 0; var sessionCount = 0; @@ -85,23 +86,25 @@ export const SessionManager = (function SessionManager() { WindowEvents.emit(document, 'SetPromiseSession', { update: true, sessionID: sessionID, - edit: sessionCount == 1, + edit: sessionCount == 1 }); } // Get non-private windows if (mozWindow.incognito == true || AppConfig.get('session.save').allWindows == false) { - var sessionTitle = (new Date()).toLocaleString(); - saveWindowSession({ - title: sessionTitle, - tabs: mozWindow.tabs - }, function onSuccess(folder) { - sessionID = folder.id; - sessionCount++; - updateOnSave(); - }); - } - else { + var sessionTitle = new Date().toLocaleString(); + saveWindowSession( + { + title: sessionTitle, + tabs: mozWindow.tabs + }, + function onSuccess(folder) { + sessionID = folder.id; + sessionCount++; + updateOnSave(); + } + ); + } else { // Get all non private windows getAllWindows(function (windows) { // Save windows as separate sessions @@ -119,14 +122,17 @@ export const SessionManager = (function SessionManager() { windows.forEach(function (mozWindow) { if (mozWindow.incognito == false) { windowID++; - let sessionTitle = date.toLocaleString() + ((sessionCount > 1) ? (' #' + windowID) : ''); - saveWindowSession({ - title: sessionTitle, - tabs: mozWindow.tabs, - }, function (folder) { - sessionID = folder.id; - updateEvent.advance(); - }); + let sessionTitle = date.toLocaleString() + (sessionCount > 1 ? ' #' + windowID : ''); + saveWindowSession( + { + title: sessionTitle, + tabs: mozWindow.tabs + }, + function (folder) { + sessionID = folder.id; + updateEvent.advance(); + } + ); } }); }); @@ -137,10 +143,9 @@ export const SessionManager = (function SessionManager() { var mergeSessions = function mergeSessions(sessionID) { // Exclude all pages that are already saved getCurrentWindow(function (mozWindow) { - var toSave = {}; - mozWindow.tabs.forEach(tab => { + mozWindow.tabs.forEach((tab) => { let url = tab.url; toSave[url] = tab; }); @@ -164,8 +169,7 @@ export const SessionManager = (function SessionManager() { for (let i = savingList.length - 1; i >= 0; i--) { var tab = savingList[i]; - BookmarkManager.createBookmarkFromTab(tab, sessionID) - .then(updateEvent.advance, updateEvent.advance); + BookmarkManager.createBookmarkFromTab(tab, sessionID).then(updateEvent.advance, updateEvent.advance); } }); }); @@ -175,7 +179,6 @@ export const SessionManager = (function SessionManager() { var toRemove = []; BookmarkManager.getFolderBookmarks(sessionID, function (bookmarks) { - // Remove all bookmarks from the current session bookmarks.forEach(function (bookmark) { toRemove.push(bookmark.id); @@ -183,7 +186,6 @@ export const SessionManager = (function SessionManager() { SessionSyncModel.deleteBookmarkList(toRemove, sessionID, function () { getCurrentWindow(function (mozWindow) { - var updateFinish = new LimitCounter(mozWindow.tabs.length, function () { WindowEvents.emit(document, 'ViewSession', sessionID); }); @@ -192,8 +194,7 @@ export const SessionManager = (function SessionManager() { for (let i = mozWindow.tabs.length - 1; i >= 0; i--) { var tab = mozWindow.tabs[i]; - BookmarkManager.createBookmarkFromTab(tab, sessionID) - .then(updateFinish.advance, updateFinish.advance); + BookmarkManager.createBookmarkFromTab(tab, sessionID).then(updateFinish.advance, updateFinish.advance); } }); }); @@ -216,13 +217,16 @@ export const SessionManager = (function SessionManager() { // save all history windows for (var key in sessionInfo.windows) { var date = new Date(sessionInfo.lastSave); - var sessionTitle = date.toLocaleString() + ((windowCount > 1) ? (' #' + windowID) : ''); + var sessionTitle = date.toLocaleString() + (windowCount > 1 ? ' #' + windowID : ''); windowID++; - saveWindowSession({ - title: sessionTitle, - tabs: sessionInfo.windows[key].tabs - }, onSave); + saveWindowSession( + { + title: sessionTitle, + tabs: sessionInfo.windows[key].tabs + }, + onSave + ); } }; @@ -230,28 +234,24 @@ export const SessionManager = (function SessionManager() { BookmarkManager.createBookmark({ title: sessionInfo.title, parentId: AppConfig.get('storageID') - }) - .then(function (folder) { - - var trackSaving = new LimitCounter(sessionInfo.tabs.length, function () { - callback(folder); - }); + }).then(function (folder) { + var trackSaving = new LimitCounter(sessionInfo.tabs.length, function () { + callback(folder); + }); - // TODO - add option to discard duplicate tabs + // TODO - add option to discard duplicate tabs - var savePinned = AppConfig.get('session.save').pinned; + var savePinned = AppConfig.get('session.save').pinned; - for (let i = sessionInfo.tabs.length - 1; i >= 0; i--) { - let tab = sessionInfo.tabs[i]; - if (tab.pinned && savePinned == false) { - trackSaving.offsetLimit(-1); - } - else { - BookmarkManager.createBookmarkFromTab(tab, folder.id) - .then(trackSaving.advance, trackSaving.advance); - } + for (let i = sessionInfo.tabs.length - 1; i >= 0; i--) { + let tab = sessionInfo.tabs[i]; + if (tab.pinned && savePinned == false) { + trackSaving.offsetLimit(-1); + } else { + BookmarkManager.createBookmarkFromTab(tab, folder.id).then(trackSaving.advance, trackSaving.advance); } - }); + } + }); }; var restoreSession = function restoreSession(folderID) { @@ -273,7 +273,7 @@ export const SessionManager = (function SessionManager() { getCurrentWindow(function (mozWindow) { var options = { newWindow: true, - closeWindow: mozWindow.id, + closeWindow: mozWindow.id }; restoreAdvanced(folderID, options); }); @@ -282,7 +282,7 @@ export const SessionManager = (function SessionManager() { var replaceSessionAll = function replaceSessionAll(folderID) { var options = { newWindow: true, - closeAll: true, + closeAll: true }; restoreAdvanced(folderID, options); }; @@ -296,19 +296,20 @@ export const SessionManager = (function SessionManager() { }; var createSeparator = function createSeparator(folderID) { - browser.bookmarks.get(folderID) - .then(function (marks) { + browser.bookmarks.get(folderID).then( + function (marks) { BookmarkManager.createBookmark({ index: marks[0].index + 1, type: 'separator', parentId: AppConfig.get('storageID') - }) - .then(function success(separator) { - WindowEvents.emit(document, 'SetPromiseSession', { sessionID: separator.id, update: true }); - }); - }, function onError(msg) { + }).then(function success(separator) { + WindowEvents.emit(document, 'SetPromiseSession', { sessionID: separator.id, update: true }); + }); + }, + function onError(msg) { console.log(msg); - }); + } + ); }; var createSession = function createSession(title, index) { @@ -316,44 +317,42 @@ export const SessionManager = (function SessionManager() { index: index, title: title, parentId: AppConfig.get('storageID') - }) - .then(function success(session) { + }).then( + function success(session) { WindowEvents.emit(document, 'SetPromiseSession', { sessionID: session.id, edit: true, update: true }); - }, function error() { - }); + }, + function error() {} + ); }; var createNewSession = function createNewSession(folderID) { - var sessionTitle = (new Date()).toLocaleString(); + var sessionTitle = new Date().toLocaleString(); if (folderID) { - browser.bookmarks.get(folderID) - .then(function (marks) { - createSession(sessionTitle, marks[0].index + 1); - }); - } - else { + browser.bookmarks.get(folderID).then(function (marks) { + createSession(sessionTitle, marks[0].index + 1); + }); + } else { createSession(sessionTitle); } }; // Events GlobalEvents.on('CreateNewSession', function (name) { - var title = name ? name : (new Date()).toLocaleString(); + var title = name ? name : new Date().toLocaleString(); createSession(title); }); // Events GlobalEvents.on('management.close-tab', function (tabID) { trackTabs = false; - browser.tabs.remove(tabID) - .then(function () { - GlobalEvents.emit('UpdateCurrentSession'); - trackTabs = true; - }); + browser.tabs.remove(tabID).then(function () { + GlobalEvents.emit('UpdateCurrentSession'); + trackTabs = true; + }); }); // Public API @@ -379,7 +378,6 @@ export const SessionManager = (function SessionManager() { saveActiveSession: saveActiveSession, createSeparator: createSeparator, createNewSession: createNewSession, - saveHistorySession: saveHistorySession, + saveHistorySession: saveHistorySession }; - })(); diff --git a/src/scripts/utils/action-popup.js b/src/scripts/utils/action-popup.js index 579285a..42a864e 100644 --- a/src/scripts/utils/action-popup.js +++ b/src/scripts/utils/action-popup.js @@ -9,23 +9,22 @@ import { WindowEvents } from './global-events'; // ***************************************************************************** // ActionPopup -export function ActionPopup(options) -{ +export function ActionPopup(options) { var DomElem = HTMLCreator(); - var panel = DomElem('div', {class : 'action-popoup'}); + var panel = DomElem('div', { class: 'action-popoup' }); - var info = DomElem('div', {class : 'info'}); - var controls = DomElem('div', {class : 'controls'}); + var info = DomElem('div', { class: 'info' }); + var controls = DomElem('div', { class: 'controls' }); - var ok = DomElem('div', {class : 'button ok'}); + var ok = DomElem('div', { class: 'button ok' }); ok.textContent = 'Confirm'; - var cancel = DomElem('div', {class : 'button cancel'}); + var cancel = DomElem('div', { class: 'button cancel' }); cancel.textContent = 'Cancel'; - var flexArea1 = DomElem('div', {class : 'flex'}); - var flexArea2 = DomElem('div', {class : 'flex'}); + var flexArea1 = DomElem('div', { class: 'flex' }); + var flexArea2 = DomElem('div', { class: 'flex' }); controls.appendChild(flexArea1); controls.appendChild(ok); @@ -40,17 +39,16 @@ export function ActionPopup(options) // ------------------------------------------------------------------------ // Events - ok.addEventListener('click', function() { + ok.addEventListener('click', function () { callback(); hidePanel(); }); - cancel.addEventListener('click', function() { + cancel.addEventListener('click', function () { hidePanel(); }); - var openPanel = function openPanel(options) - { + var openPanel = function openPanel(options) { panel.setAttribute('data-active', 'true'); panel.style.left = options.event.pageX + 'px'; panel.style.top = options.event.pageY + 'px'; @@ -63,8 +61,7 @@ export function ActionPopup(options) setCallback(options.callback); }; - var hidePanel = function hidePanel() - { + var hidePanel = function hidePanel() { panel.removeAttribute('data-active'); if (typeof document !== 'undefined') { document.removeEventListener('mousedown', blurMenu); @@ -72,12 +69,10 @@ export function ActionPopup(options) callback = defaultCallback; }; - var blurMenu = function hideWhenClickOutside(e) - { + var blurMenu = function hideWhenClickOutside(e) { var target = e.target; while (target) { - if (target === panel) - return; + if (target === panel) return; target = target.parentElement; } hidePanel(); diff --git a/src/scripts/utils/auto-scroll.js b/src/scripts/utils/auto-scroll.js index f6a21e3..6cce8ff 100644 --- a/src/scripts/utils/auto-scroll.js +++ b/src/scripts/utils/auto-scroll.js @@ -4,7 +4,6 @@ // AutoScroll export const AutoScroll = (function AutoScroll() { - var autoScroll = false; var prevScroll = 0; var scrollSpeed = 0; @@ -12,18 +11,15 @@ export const AutoScroll = (function AutoScroll() { var scrollTarget; var scrollDir = 0; - function scrollingAnimation() - { - if (autoScroll == false) - { + function scrollingAnimation() { + if (autoScroll == false) { return; } prevScroll = scrollingNode.scrollTop; scrollingNode.scrollTop += scrollSpeed; - if (scrollingNode.scrollTop != prevScroll) - { + if (scrollingNode.scrollTop != prevScroll) { if (scrollTarget != undefined) { if (scrollDir * scrollingNode.scrollTop > scrollTarget) { scrollingNode.scrollTop = scrollDir * scrollTarget; @@ -34,20 +30,16 @@ export const AutoScroll = (function AutoScroll() { if (typeof requestAnimationFrame !== 'undefined') { requestAnimationFrame(scrollingAnimation); } - } - else - { + } else { autoScroll = false; } } // scroll a DOM node with a specified speed - function scroll(node, speed) - { + function scroll(node, speed) { scrollSpeed = speed / 10; - if (autoScroll == true) - { + if (autoScroll == true) { return; } @@ -59,20 +51,17 @@ export const AutoScroll = (function AutoScroll() { } } - function stop() - { + function stop() { autoScroll = false; } - function scrollTo(node, offset, time) - { + function scrollTo(node, offset, time) { if (time == 0) { node.scrollTop = offset; return; } - if (autoScroll == true) - { + if (autoScroll == true) { return; } @@ -87,8 +76,8 @@ export const AutoScroll = (function AutoScroll() { } return { - scroll : scroll, + scroll: scroll, scrollTo: scrollTo, - stop : stop + stop: stop }; -})(); \ No newline at end of file +})(); diff --git a/src/scripts/utils/components.js b/src/scripts/utils/components.js index dc8418c..1675fdf 100644 --- a/src/scripts/utils/components.js +++ b/src/scripts/utils/components.js @@ -10,8 +10,8 @@ import * as JSUtils from './general'; // API /* -* Toggle switch -*/ + * Toggle switch + */ export function ToggleSwitch(options) { var DomElem = HTMLCreator(); @@ -43,8 +43,7 @@ export function ToggleSwitch(options) { entry.appendChild(button); function setState(newState) { - if (state == newState) - return; + if (state == newState) return; state = newState; options.callback(state); @@ -109,8 +108,8 @@ export function ToggleButton(options) { } /* -* Action Button -*/ + * Action Button + */ export function ActionButton(options) { var DomElem = HTMLCreator(); @@ -152,8 +151,8 @@ export function ActionButton(options) { } /* -* RageControl -*/ + * RageControl + */ export function RangeControl(options) { var DomElem = HTMLCreator(); @@ -189,8 +188,12 @@ export function RangeControl(options) { } // restrict to specified range - if (options.minValue > newValue) { newValue = options.minValue; } - if (options.maxValue < newValue) { newValue = options.maxValue; } + if (options.minValue > newValue) { + newValue = options.minValue; + } + if (options.maxValue < newValue) { + newValue = options.maxValue; + } if (newValue % step != 0) { newValue -= newValue % step; @@ -261,12 +264,11 @@ export function DropDown(options) { select.appendChild(dropList); function addOption(label, options) { - if (options == undefined) - options = {}; + if (options == undefined) options = {}; var option = DomElem('div', { class: 'option' }); option.textContent = label; - option.setAttribute('value', (options.value != undefined) ? options.value : label); + option.setAttribute('value', options.value != undefined ? options.value : label); if (options.property) { option.setAttribute(options.property, ''); @@ -300,8 +302,7 @@ export function DropDown(options) { }; var setSelected = function setSelected(node, trigger) { - if (selectedOption) - selectedOption.removeAttribute('selected'); + if (selectedOption) selectedOption.removeAttribute('selected'); selectedOption = node; diff --git a/src/scripts/utils/confirm-box.js b/src/scripts/utils/confirm-box.js index ee9d97a..c19e236 100644 --- a/src/scripts/utils/confirm-box.js +++ b/src/scripts/utils/confirm-box.js @@ -34,7 +34,7 @@ export function ConfirmBox(options) { panel.appendChild(controls); var callback; - var defaultCallback = function defaultCallback() { }; + var defaultCallback = function defaultCallback() {}; // ------------------------------------------------------------------------ // Events @@ -57,7 +57,6 @@ export function ConfirmBox(options) { info.textContent = options.message; setCallback(options.callback); - }; var hidePanel = function hidePanel() { @@ -69,8 +68,7 @@ export function ConfirmBox(options) { var blurMenu = function hideWhenClickOutside(e) { var target = e.target; while (target) { - if (target === panel) - return; + if (target === panel) return; target = target.parentElement; } hidePanel(); diff --git a/src/scripts/utils/context-menu.js b/src/scripts/utils/context-menu.js index 26f45f1..eb04db4 100644 --- a/src/scripts/utils/context-menu.js +++ b/src/scripts/utils/context-menu.js @@ -57,8 +57,7 @@ export function ContextMenu(options) { var blurMenu = function hideWhenClickOutside(e) { var target = e.target; while (target) { - if (target === menu) - return; + if (target === menu) return; target = target.parentElement; } hideMenu(); diff --git a/src/scripts/utils/dom.js b/src/scripts/utils/dom.js index 87699b2..929bcb8 100644 --- a/src/scripts/utils/dom.js +++ b/src/scripts/utils/dom.js @@ -8,9 +8,10 @@ const NS_HTML = 'http://www.w3.org/1999/xhtml'; function Node(ns, type, attrs) { var elem = document.createElementNS(ns, type); - attrs && Object.keys(attrs).forEach(function (attr) { - elem.setAttribute(attr, attrs[attr]); - }); + attrs && + Object.keys(attrs).forEach(function (attr) { + elem.setAttribute(attr, attrs[attr]); + }); return elem; } diff --git a/src/scripts/utils/drag-context.js b/src/scripts/utils/drag-context.js index bfe3ccc..e7cf56b 100644 --- a/src/scripts/utils/drag-context.js +++ b/src/scripts/utils/drag-context.js @@ -9,7 +9,6 @@ import { WindowEvents } from './global-events'; // API export const DragContext = (function DragContext() { - var context = null; function setContext(ctx) { @@ -37,4 +36,4 @@ export const DragContext = (function DragContext() { setContext: setContext, getContext: getContext }; -})(); \ No newline at end of file +})(); diff --git a/src/scripts/utils/field-edit-widget.js b/src/scripts/utils/field-edit-widget.js index 917c2bb..b101ad8 100644 --- a/src/scripts/utils/field-edit-widget.js +++ b/src/scripts/utils/field-edit-widget.js @@ -83,8 +83,7 @@ export function FieldEditWdiget(options) { for (var key in data.fields) { if (data.fields[key]) { fields[key].value = data.fields[key]; - } - else { + } else { if (Object.prototype.hasOwnProperty.call(fields, key)) { fields[key].value = ''; data.fields[key] = ''; diff --git a/src/scripts/utils/general.js b/src/scripts/utils/general.js index 45e338b..1d6a833 100644 --- a/src/scripts/utils/general.js +++ b/src/scripts/utils/general.js @@ -11,7 +11,7 @@ export function copyProperties(source, destination) { // Test if object is a valid javascript function // Returns the same object if true or a new function otherwise export function getValidFunction(object) { - var value = (typeof object === 'function' ? object : function () { }); + var value = typeof object === 'function' ? object : function () {}; return value; } @@ -38,4 +38,4 @@ export function LimitCounter(limit, callback) { checkLimit(); } }; -} \ No newline at end of file +} diff --git a/src/scripts/utils/global-events.js b/src/scripts/utils/global-events.js index 3e75b14..cedca7f 100644 --- a/src/scripts/utils/global-events.js +++ b/src/scripts/utils/global-events.js @@ -9,19 +9,17 @@ import { PubSub } from '../3rd/pubsub'; // API const GlobalEventSystem = (function GlobalEventSystem() { - const globalEvents = new PubSub(); return { on: globalEvents.subscribe.bind(globalEvents), off: globalEvents.unsubscribe.bind(globalEvents), - emit: globalEvents.publish.bind(globalEvents), + emit: globalEvents.publish.bind(globalEvents) }; })(); // Handles collection of PubSub events separated by diffrent namespaces function NamespaceEvents() { - var map = new Map(); //------------------------------------------------------------------------- @@ -73,7 +71,7 @@ function NamespaceEvents() { }; } -// WindowEvents based on window.document topic +// WindowEvents based on window.document topic const WindowEvents = new NamespaceEvents(); diff --git a/wxt.config.js b/wxt.config.js index 8f995b1..1cab98a 100644 --- a/wxt.config.js +++ b/wxt.config.js @@ -2,7 +2,6 @@ import 'dotenv/config'; import { defineConfig } from 'wxt'; export default defineConfig(() => { - console.log('FIREFOX_BINARY:', process.env.FIREFOX_BINARY); console.log('FIREFOX_PROFILE:', process.env.FIREFOX_PROFILE); @@ -11,30 +10,28 @@ export default defineConfig(() => { publicDir: 'public', imports: { eslintrc: { - enabled: 9, - }, + enabled: 9 + } }, vite: () => ({ optimizeDeps: { - entries: [ - 'src/entrypoints/popup/main.js', - 'src/entrypoints/background.js', - ], - }, + entries: ['src/entrypoints/popup/main.js', 'src/entrypoints/background.js'] + } }), browser: 'firefox', webExt: { binaries: { - firefox: process.env.FIREFOX_BINARY, + firefox: process.env.FIREFOX_BINARY }, firefoxProfile: process.env.FIREFOX_PROFILE, - keepProfileChanges: true, + keepProfileChanges: true }, manifest: { name: 'Session Sync', version: '3.2.0', author: 'Gabriel Ivanica', - description: 'Save sessions as bookmarks and sync them through Firefox Sync (or any other sync engine).\nPowerful session management - organize, edit, change, save, restore, etc\n', + description: + 'Save sessions as bookmarks and sync them through Firefox Sync (or any other sync engine).\nPowerful session management - organize, edit, change, save, restore, etc\n', homepage_url: 'https://github.com/ReDEnergy/SessionSync', default_locale: 'en', browser_specific_settings: { @@ -45,49 +42,40 @@ export default defineConfig(() => { } } }, - permissions: [ - 'storage', - 'bookmarks', - 'tabs', - 'clipboardWrite', - 'unlimitedStorage', - 'cookies', - 'contextualIdentities', - 'favicon' - ], + permissions: ['storage', 'bookmarks', 'tabs', 'clipboardWrite', 'unlimitedStorage', 'cookies', 'contextualIdentities', 'favicon'], action: { default_icon: { - '16': 'images/icon16.png', - '32': 'images/icon32.png', - '48': 'images/icon48.png', - '128': 'images/icon128.png' + 16: 'images/icon16.png', + 32: 'images/icon32.png', + 48: 'images/icon48.png', + 128: 'images/icon128.png' }, - default_title: 'Session Sync', + default_title: 'Session Sync' }, icons: { - '16': 'images/icon16.png', - '32': 'images/icon32.png', - '48': 'images/icon48.png', - '64': 'images/icon64.png', - '96': 'images/icon96.png', - '128': 'images/icon128.png' + 16: 'images/icon16.png', + 32: 'images/icon32.png', + 48: 'images/icon48.png', + 64: 'images/icon64.png', + 96: 'images/icon96.png', + 128: 'images/icon128.png' }, commands: { - '_execute_action': { + _execute_action: { suggested_key: { - 'default': 'Ctrl+Shift+F' + default: 'Ctrl+Shift+F' }, description: 'Toggle Widget' }, - 'session_sync_detach_tab': { + session_sync_detach_tab: { suggested_key: { - 'default': 'Ctrl+Shift+U' + default: 'Ctrl+Shift+U' }, description: 'Tab View' }, - 'session_sync_detach_window': { + session_sync_detach_window: { suggested_key: { - 'default': 'Ctrl+Shift+2' + default: 'Ctrl+Shift+2' }, description: 'Window View' } From bd9ff909689df0da93f43d10bcc9abb29eec316e Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Sat, 18 Apr 2026 01:57:32 +0300 Subject: [PATCH 23/32] Added support for tab groups and improved support for bookmark properties --- assets/css/session-container.css | 97 ++++++++-- public/home/home.html | 48 +++-- public/images/material_icons/tab_group.svg | 1 + src/background/session-history.js | 94 ++++++---- src/background/session-management.js | 175 +++++++++++++----- src/common/bookmark-properties.js | 23 ++- .../services/firefox-containers.js | 9 +- src/common/services/tab-groups.js | 84 +++++++++ src/scripts/app.js | 4 +- src/scripts/components/config-panel.js | 42 ++++- .../components/session-bookmark-events.js | 6 +- src/scripts/components/session-bookmark.js | 53 ++++-- src/scripts/components/session-container.js | 6 + .../components/session-folder-events.js | 10 +- src/scripts/session-sync-ui.js | 39 ++-- src/scripts/session/bookmarks.js | 53 +++++- src/scripts/utils/context-menu.js | 3 + src/scripts/utils/global-events.js | 8 +- wxt.config.js | 11 +- 19 files changed, 575 insertions(+), 191 deletions(-) create mode 100644 public/images/material_icons/tab_group.svg rename src/{scripts => common}/services/firefox-containers.js (84%) create mode 100644 src/common/services/tab-groups.js diff --git a/assets/css/session-container.css b/assets/css/session-container.css index 6549445..3ad3a00 100644 --- a/assets/css/session-container.css +++ b/assets/css/session-container.css @@ -166,21 +166,9 @@ cursor: default; } -#session-sync .session-container .bookmark[pinned]:before { - content: ''; - width: 1.5em; - height: 100%; - background-image: url('images/icons/pin.png'); - background-size: contain; - background-repeat: no-repeat; - background-position: center center; - color: green; - position: absolute; - top: 0; - right: 0.35em; - z-index: 0; - opacity: 0.85; -} +/* + * Bookmark Properties + */ #session-sync .session-container .bookmark * { float: left; @@ -211,9 +199,26 @@ margin-top: 0.2em; } +#session-sync .session-container .bookmark .properties > div:not(:last-child) { + margin-right: 0.5em; +} + +/* Bookmark Pin */ + +#session-sync .session-container .bookmark .pin { + width: 1.5em; + height: 100%; + background-image: url('images/icons/pin.png'); + background-size: contain; + background-repeat: no-repeat; + background-position: center center; + color: #fff; +} + +/* Bookmark Container */ + #session-sync .session-container .bookmark .container { height: 100%; - margin-right: 2em; max-width: 10em; border-radius: 1em; color: #fff; @@ -240,6 +245,66 @@ background-position: center center !important; } +/* Tab Groups */ + +#session-sync .session-container .bookmark .tab-group-tag { + height: 100%; + max-width: 12em; + padding: 0 0.6em; + + /* padding: 0 0.6em 0 1.8em; + background-image: url('images/material_icons/tab_group.svg'); + background-size: 1em; + background-repeat: no-repeat; + background-position: 0.4em center; */ + + color: #fff; + white-space: nowrap; + text-overflow: ellipsis; + font-weight: 500; + font-size: 0.9em; + overflow: hidden; + + border-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.2); + + box-sizing: border-box; + display: flex; +} + +#session-sync .session-container .bookmark .tab-group-tag[color='grey'] { + background-color: #5f6368; +} +#session-sync .session-container .bookmark .tab-group-tag[color='blue'] { + background-color: #1a73e8; +} +#session-sync .session-container .bookmark .tab-group-tag[color='red'] { + background-color: #d93025; +} +#session-sync .session-container .bookmark .tab-group-tag[color='yellow'] { + background-color: #f29900; + color: #202124; +} +#session-sync .session-container .bookmark .tab-group-tag[color='green'] { + background-color: #1e8e3e; +} +#session-sync .session-container .bookmark .tab-group-tag[color='pink'] { + background-color: #d01884; +} +#session-sync .session-container .bookmark .tab-group-tag[color='purple'] { + background-color: #9334e6; +} +#session-sync .session-container .bookmark .tab-group-tag[color='cyan'] { + background-color: #007b83; +} +#session-sync .session-container .bookmark .tab-group-tag[color='orange'] { + background-color: #fa903e; +} + +#session-sync .session-container .bookmark .tab-group-tag .name { + line-height: 1.5em; +} + /* * History Style */ diff --git a/public/home/home.html b/public/home/home.html index 380a2d7..f013dfe 100644 --- a/public/home/home.html +++ b/public/home/home.html @@ -42,8 +42,9 @@
- Press CTRL + SHIFT + F to open the panel or use the - widget provided to open the panel + Press + CTRL + SHIFT + F + to open the panel or use the widget provided to open the panel
You can find all your recent browsing history in the history tab. @@ -52,9 +53,11 @@ Configure history settings in the configuration panel
- Use the search bar to easy filter through saved sessions + Use the search bar to easy + filter through saved sessions
- You can focus on the search bar by pressing CTRL + F or + You can focus on the search bar by pressing + CTRL + F or F3
@@ -72,15 +75,17 @@ green resize handle
- The addon offers multiple configuration options. Explore all of them in the configuration panel + The addon offers multiple configuration options. Explore all of them in the + configuration panel
Configuration panel can be accessed from the addon UI menu
- Drag and drop bookmark items over the trash bin drop area location (bottom-right corner) to - delete them + Drag and drop bookmark items over the trash bin drop area location + (bottom-right corner) to delete them
- Click on the red button to restore the previous deleted items + Click on the red button to restore the + previous deleted items
Session sync offers the ability to save (or not) pinned tabs and also to @@ -88,15 +93,17 @@ pinned state
- You can edit session and bookmarks information to provide more valuable future information + You can edit session and bookmarks information to provide more valuable future + information
- Sort sessions by name, position, or date using the sort-selection menu from the bottom-left corner of - the UI + Sort sessions by name, position, or date using the sort-selection menu from the + bottom-left corner of the UI
The session menu toolbar provides fast access to various commands such as - restore, replace, merge or add the current tab as bookmark + restore, replace, merge or add + the current tab as bookmark
Move your mouse above a bookmark to view the URL. @@ -104,19 +111,24 @@ Click on the URL bar to copy the link
- You can detach the UI into a separate window or tab
+ You can detach the UI into a separate window or + tab
Window Mode: CTRL + SHIFT + 2   Tab Mode: + >Window Mode: + CTRL + SHIFT + 2 +   Tab Mode: CTRL + SHIFT + U
or simply use the buttons from addon menu
Saved session are stored as bookmarks. It's recommended to use - Firefox Account to sync - your bookmarks.
- Currenly you can only use Firefox Bookmark Manager (CTRL + SHIFT + O) to export/import all - your bookmarks (which includes all Session Sync saved data). + Firefox Account + to sync your bookmarks.
+ Currenly you can only use Firefox Bookmark Manager (CTRL + SHIFT + O) to + export/import all your bookmarks (which includes all Session Sync saved data).
Session Sync detects automatically all updated bookmarks after an import.
diff --git a/public/images/material_icons/tab_group.svg b/public/images/material_icons/tab_group.svg new file mode 100644 index 0000000..b94ee3c --- /dev/null +++ b/public/images/material_icons/tab_group.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/background/session-history.js b/src/background/session-history.js index ee2014a..461b5e0 100644 --- a/src/background/session-history.js +++ b/src/background/session-history.js @@ -22,57 +22,71 @@ function WindowSession(id) { this.saveTime = new Date().getTime(); } -BrowserSession.prototype.update = function update() { +BrowserSession.prototype.update = async function update() { if (typeof browser === 'undefined' || !browser.windows) return; // Prepare session information - browser.windows - .getAll({ + try { + const mozWindows = await browser.windows.getAll({ populate: true, windowTypes: ['normal'] - }) - .then( - function (mozWindows) { - var tabCount = 0; + }); - for (var key in this.windows) { - this.windows[key].active = false; - } + let groupMap = new Map(); + if (browser.tabGroups) { + try { + const groups = await browser.tabGroups.query({}); + groups.forEach((g) => groupMap.set(g.id, g)); + } catch (e) { + console.log('Error fetching groups in history update', e); + } + } - for (let mozWindow of mozWindows) { - if (mozWindow.incognito && this.config && this.config.savePrivate == false) { - delete this.windows[mozWindow.id]; - continue; - } + var tabCount = 0; - var windowInfo = new WindowSession(mozWindow.id); - - // Add window tabs - for (var i in mozWindow.tabs) { - var tab = mozWindow.tabs[i]; - var sessionTab = { - title: tab.title, - url: tab.url, - pinned: tab.pinned, - cookieStoreId: tab.cookieStoreId - }; - tabCount++; - windowInfo.tabs[tab.index] = sessionTab; - } + for (var key in this.windows) { + this.windows[key].active = false; + } - this.windows[mozWindow.id] = windowInfo; - } + for (let mozWindow of mozWindows) { + if (mozWindow.incognito && this.config && this.config.savePrivate == false) { + delete this.windows[mozWindow.id]; + continue; + } - // Update internal storage - this.lastSave = new Date().getTime(); - this.tabCount = tabCount; + var windowInfo = new WindowSession(mozWindow.id); + + // Add window tabs + for (var i in mozWindow.tabs) { + var tab = mozWindow.tabs[i]; + var group = groupMap.get(tab.groupId); + var sessionTab = { + title: tab.title, + url: tab.url, + pinned: tab.pinned, + cookieStoreId: tab.cookieStoreId, + groupId: tab.groupId, + groupTitle: group ? group.title : undefined, + groupColor: group ? group.color : undefined + }; + tabCount++; + windowInfo.tabs[tab.index] = sessionTab; + } - // Save new session - if (browser.storage && browser.storage.local) { - browser.storage.local.set({ [storageKey.active]: this }); - } - }.bind(this) - ); + this.windows[mozWindow.id] = windowInfo; + } + + // Update internal storage + this.lastSave = new Date().getTime(); + this.tabCount = tabCount; + + // Save new session + if (browser.storage && browser.storage.local) { + browser.storage.local.set({ [storageKey.active]: this }); + } + } catch (err) { + console.log('Error in BrowserSession.update', err); + } }; BrowserSession.prototype.clear = function clear() { diff --git a/src/background/session-management.js b/src/background/session-management.js index dd5541e..9c2cb9c 100644 --- a/src/background/session-management.js +++ b/src/background/session-management.js @@ -2,7 +2,8 @@ import { FaviconService } from './favicon-service'; import { BookmarkProperties } from '../common/bookmark-properties'; -import { FirefoxContainers } from '../scripts/services/firefox-containers'; +import { FirefoxContainers } from '../common/services/firefox-containers'; +import { BrowserTabGroups } from '../common/services/tab-groups'; export const SessionManagement = (function SessionManagement() { var lazyLoadingKey = 'restore.lazy.loading'; @@ -29,10 +30,15 @@ export const SessionManagement = (function SessionManagement() { } return browser.tabs.create(tabOptions).then( - function (tabInfo) { + async function (tabInfo) { if (info.discard) { browser.tabs.discard(tabInfo.id); } + + if (info.group) { + BrowserTabGroups.setTabsGroup([tabInfo.id], info.group.title, info.group.color); + } + if (onSuccess) onSuccess(tabInfo); }, function (err) { @@ -67,10 +73,12 @@ export const SessionManagement = (function SessionManagement() { }); }; - var openTab = function openTab(options, info) { + var openTab = async function openTab(options, info) { var checkFavicon = options.favicon === true ? FaviconService.checkTabFaviconSaved : () => {}; delete options.favicon; + console.log('openTab', options, info); + switch (info.mode) { case 'newTab': { createTab(options, info, checkFavicon); @@ -88,29 +96,29 @@ export const SessionManagement = (function SessionManagement() { if (options.pinned) { browser.tabs.update(tab.id, { pinned: true }); } + + if (info.group) { + BrowserTabGroups.createGroup([tab.id], info.group.title, info.group.color); + } + checkFavicon(tab); }); break; } case 'activeTab': { - browser.tabs - .query({ active: true, windowId: browser.windows.WINDOW_ID_CURRENT }) - .then((tabs) => browser.tabs.get(tabs[0].id)) - .then((tab) => { - if ( - (options.cookieStoreId == undefined && tab.cookieStoreId == FirefoxContainers.defaultContainer) || - tab.cookieStoreId === options.cookieStoreId - ) { - delete options.cookieStoreId; - browser.tabs.update(tab.id, options).then(checkFavicon); - } else { - browser.tabs.create({ - url: options.url, - cookieStoreId: options.cookieStoreId - }); - } - }); + const tabs = await browser.tabs.query({ active: true, windowId: browser.windows.WINDOW_ID_CURRENT }); + const tab = await browser.tabs.get(tabs[0].id); + + if ( + (options.cookieStoreId == undefined && tab.cookieStoreId == FirefoxContainers.defaultContainer) || + tab.cookieStoreId === options.cookieStoreId + ) { + delete options.cookieStoreId; + browser.tabs.update(tab.id, options).then(checkFavicon); + } else { + createTab(options, info, checkFavicon); + } break; } @@ -125,42 +133,115 @@ export const SessionManagement = (function SessionManagement() { } }; - var createTabsFromBookmarks = function createTabsFromBookmarks(bookmarks, windowId) { + // After all tabs are created, group them according to their saved group metadata. + // Groups are keyed by their title+color so that separate groups with the same + // name are treated as one group within the restored window. + async function assignTabGroups(pendingTabs, windowId) { + if (typeof browser === 'undefined' || !browser.tabGroups) return; + + // Collect unique groups keyed by "title|color" + const groupMap = new Map(); // key -> { title, color, tabIds[] } + + for (const { tabPromise, groupTitle, groupColor } of pendingTabs) { + if (!groupTitle && !groupColor) continue; + const key = (groupTitle || '') + '|' + (groupColor || 'grey'); + if (!groupMap.has(key)) { + groupMap.set(key, { title: groupTitle || '', color: groupColor || 'grey', tabIds: [] }); + } + try { + const tabInfo = await tabPromise; + if (tabInfo) { + groupMap.get(key).tabIds.push(tabInfo.id); + } + } catch (e) { + // Tab creation failed; skip + } + } + + for (const { title, color, tabIds } of groupMap.values()) { + if (tabIds.length === 0) continue; + try { + const groupId = await browser.tabs.group({ tabIds, createProperties: { windowId } }); + await browser.tabGroups.update(groupId, { title, color }); + } catch (e) { + console.log('Error assigning tab group during restore:', e); + } + } + } + + var createTabsFromBookmarks = async function createTabsFromBookmarks(bookmarks, windowId) { checkReverseOrder(bookmarks); + + const pendingTabs = []; + bookmarks.forEach(function (bookmark) { const info = BookmarkProperties.parseTitle(bookmark.title); - createTab( - { - url: bookmark.url, - active: false, - windowId: windowId, - pinned: info.properties.pinned ? true : false - }, - { - discard: lazyLoading, - container: info.properties.container - }, - FaviconService.checkTabFaviconSaved - ); + console.log(info.properties); + + // Wrap createTab in a Promise so we can await the created tab ID + const tabPromise = new Promise((resolve) => { + createTab( + { + url: bookmark.url, + active: false, + windowId: windowId, + pinned: info.properties.pinned ? true : false + }, + { + discard: lazyLoading, + container: info.properties.container + }, + (tabInfo) => { + FaviconService.checkTabFaviconSaved(tabInfo); + resolve(tabInfo); + }, + () => resolve(null) + ); + }); + + if (info.properties.group) { + pendingTabs.push({ + tabPromise, + groupTitle: info.properties.group.title, + groupColor: info.properties.group.color + }); + } }); + + await assignTabGroups(pendingTabs, windowId); }; - var createTabsFromHistoryTabs = function createTabsFromHistoryTabs(tabs, windowId) { + var createTabsFromHistoryTabs = async function createTabsFromHistoryTabs(tabs, windowId) { checkReverseOrder(tabs); + + const pendingTabs = []; + tabs.forEach(function (tab) { - createTab( - { - url: tab.url, - active: false, - windowId: windowId, - pinned: tab.pinned ? true : false - }, - { - discard: lazyLoading, - container: tab.cookieStoreId - } - ); + const tabPromise = new Promise((resolve) => { + createTab( + { + url: tab.url, + active: false, + windowId: windowId, + pinned: tab.pinned ? true : false + }, + { + discard: lazyLoading, + container: tab.cookieStoreId + }, + (tabInfo) => resolve(tabInfo), + () => resolve(null) + ); + }); + + pendingTabs.push({ + tabPromise, + groupTitle: tab.groupTitle, + groupColor: tab.groupColor + }); }); + + await assignTabGroups(pendingTabs, windowId); }; var restoreSession = function restoreSession(folderID, options) { diff --git a/src/common/bookmark-properties.js b/src/common/bookmark-properties.js index 13af44b..09d4d41 100644 --- a/src/common/bookmark-properties.js +++ b/src/common/bookmark-properties.js @@ -11,14 +11,21 @@ export class BookmarkProperties { // ------------------------------------------------------------------------ // Public properties - static encode(bookmark) { + static encode(tab) { var properties = {}; - if (bookmark.cookieStoreId) { - properties.container = bookmark.cookieStoreId; + if (tab.cookieStoreId) { + properties.container = tab.cookieStoreId; } - if (bookmark.pinned) { + if (tab.groupId !== undefined && tab.groupId > -1) { + properties.group = { + title: tab.groupTitle, + color: tab.groupColor + }; + } + + if (tab.pinned) { properties.pinned = 1; } @@ -33,14 +40,6 @@ export class BookmarkProperties { } } - static getTitle(bookmark) { - return splitTitle(bookmark.title).title; - } - - static getBookmarkProperties(bookmark) { - return splitTitle(bookmark.title).properties; - } - static parseTitle(title) { const match = title.match(/\s\[\{.+\}\]$/); diff --git a/src/scripts/services/firefox-containers.js b/src/common/services/firefox-containers.js similarity index 84% rename from src/scripts/services/firefox-containers.js rename to src/common/services/firefox-containers.js index 2e48267..582cdbd 100644 --- a/src/scripts/services/firefox-containers.js +++ b/src/common/services/firefox-containers.js @@ -1,6 +1,6 @@ 'use strict'; -import { DomElem } from '../utils/dom'; +import { DomElem } from '../../scripts/utils/dom'; // ***************************************************************************** // API @@ -17,8 +17,11 @@ export class FirefoxContainers { // ------------------------------------------------------------------------ // Public properties - static loadContainers() { - browser.contextualIdentities.query({}).then(FirefoxContainers.onGot, FirefoxContainers.onError); + static async loadContainers() { + if (typeof browser === 'undefined' || !browser.contextualIdentities) return; + + const identities = await browser.contextualIdentities.query({}); + FirefoxContainers.onGot(identities); } static onGot(contexts) { diff --git a/src/common/services/tab-groups.js b/src/common/services/tab-groups.js new file mode 100644 index 0000000..c7420d0 --- /dev/null +++ b/src/common/services/tab-groups.js @@ -0,0 +1,84 @@ +'use strict'; + +import { DomElem } from '../../scripts/utils/dom'; +import { WindowEvents } from '../../scripts/utils/global-events'; + +// ***************************************************************************** +// API + +export class BrowserTabGroups { + static groups = new Map(); + static domGroups = {}; + + // ------------------------------------------------------------------------ + // Public properties + + static async initTabGroups() { + try { + const groups = await browser.tabGroups.query({}); + BrowserTabGroups.onGot(groups); + + // Add listeners for real-time updates while UI is open + browser.tabGroups.onCreated?.addListener(() => BrowserTabGroups.loadGroups()); + browser.tabGroups.onUpdated?.addListener(() => BrowserTabGroups.loadGroups()); + browser.tabGroups.onRemoved?.addListener(() => BrowserTabGroups.loadGroups()); + } catch (error) { + console.warn('TabGroups API not available or error:', error); + } + } + + static async loadGroups() { + try { + const groups = await browser.tabGroups.query({}); + BrowserTabGroups.onGot(groups); + } catch (error) { + console.warn('TabGroups API not available or error:', error); + } + } + + static onGot(groups) { + console.log('TabGroupsLoaded', groups); + + for (const group of groups) { + BrowserTabGroups.groups.set(group.id, group); + } + + WindowEvents.emitLocal('TabGroupsLoaded'); + } + + static getGroup(groupId) { + return BrowserTabGroups.groups.get(groupId); + } + + static getGroupElement(title, color) { + const tabGroup = DomElem('div', { class: 'tab-group-tag' }); + tabGroup.setAttribute('color', color || 'grey'); + + const name = DomElem('div', { class: 'name' }); + name.textContent = title && title.length > 0 ? title : 'Group'; + + tabGroup.appendChild(name); + + return tabGroup; + } + + static async createGroup(tabs, title, color) { + const groupId = await browser.tabs.group({ tabIds: tabs }); + await browser.tabGroups.update(groupId, { title, color }); + } + + static async setTabsGroup(tabs, title, color) { + const group = await browser.tabGroups.query({ title: title, color: color }); + let groupId = null; + if (group.length > 0) { + await browser.tabs.group({ tabIds: tabs, groupId: group[0].id }); + } else { + groupId = await browser.tabs.group({ tabIds: tabs }); + await browser.tabGroups.update(groupId, { title, color }); + } + + if (groupId) { + browser.tabGroups.update(groupId, { collapsed: false }); + } + } +} diff --git a/src/scripts/app.js b/src/scripts/app.js index 9641673..6d31cfc 100644 --- a/src/scripts/app.js +++ b/src/scripts/app.js @@ -8,7 +8,8 @@ import { WindowEvents, GlobalEvents } from './utils/global-events'; import { SessionSyncUI } from './session-sync-ui'; import { SessionSyncModel } from './components/session-sync-model'; import { BookmarkManager } from './session/bookmarks'; -import { FirefoxContainers } from './services/firefox-containers'; +import { FirefoxContainers } from '../common/services/firefox-containers'; +import { BrowserTabGroups } from '../common/services/tab-groups'; const sessionRoot = 'SessionSync'; @@ -53,6 +54,7 @@ export const init = function init() { AppConfig.init(); FirefoxContainers.loadContainers(); + BrowserTabGroups.initTabGroups(); GlobalEvents.on('update-sessions', listSessions.bind(null, false, false)); }; diff --git a/src/scripts/components/config-panel.js b/src/scripts/components/config-panel.js index 7b92cb3..8d3afbd 100644 --- a/src/scripts/components/config-panel.js +++ b/src/scripts/components/config-panel.js @@ -129,13 +129,45 @@ export function ConfigPanel() { panel.appendChild(section.container); if (AppConfig.isPanel()) { - RangeOptionConfig({ parent: section, name: 'Panel width (px)', key: 'style.panel.width', minValue: 500, maxValue: 800, step: 5 }); - RangeOptionConfig({ parent: section, name: 'Panel height (px)', key: 'style.panel.height', minValue: 400, maxValue: 600, step: 5 }); + RangeOptionConfig({ + parent: section, + name: 'Panel width (px)', + key: 'style.panel.width', + minValue: 500, + maxValue: 800, + step: 5 + }); + RangeOptionConfig({ + parent: section, + name: 'Panel height (px)', + key: 'style.panel.height', + minValue: 400, + maxValue: 600, + step: 5 + }); } - RangeOptionConfig({ parent: section, name: 'Header bar', key: 'style.scale.header', minValue: 10, maxValue: 50 }); + RangeOptionConfig({ + parent: section, + name: 'Header bar', + key: 'style.scale.header', + minValue: 10, + maxValue: 50 + }); RangeOptionConfig({ parent: section, name: 'Toolbar', key: 'style.scale.toolbar', minValue: 10, maxValue: 50 }); - RangeOptionConfig({ parent: section, name: 'Session list', key: 'style.scale.sessions', minValue: 10, maxValue: 50 }); - RangeOptionConfig({ parent: section, name: 'Bookmarks list', key: 'style.scale.bookmarks', minValue: 10, maxValue: 50 }); + RangeOptionConfig({ + parent: section, + name: 'Session list', + key: 'style.scale.sessions', + minValue: 10, + maxValue: 50 + }); + RangeOptionConfig({ + parent: section, + name: 'Bookmarks list', + key: 'style.scale.bookmarks', + minValue: 10, + maxValue: 50 + }); })(); // ------------------------ diff --git a/src/scripts/components/session-bookmark-events.js b/src/scripts/components/session-bookmark-events.js index 51d181c..e9accde 100644 --- a/src/scripts/components/session-bookmark-events.js +++ b/src/scripts/components/session-bookmark-events.js @@ -54,12 +54,12 @@ export function SessionBookmarkEvents(container) { function getTabID(e) { var ID = getAttribute(e.target, 'tabID'); - return ID ? ID | 0 : undefined; + return ID ? ID | 0 : null; } function getWindowID(e) { var ID = getAttribute(e.target, 'windowID'); - return ID ? ID | 0 : undefined; + return ID ? ID | 0 : null; } // -------------------------------------------------------------- @@ -366,7 +366,7 @@ export function SessionBookmarkEvents(container) { let url = getTabURL(e); if (url) { WindowEvents.emit(document, 'HistoryMarksCtxMenu-Open', { - context: url, + context: e, event: e }); } diff --git a/src/scripts/components/session-bookmark.js b/src/scripts/components/session-bookmark.js index 4246dc9..773a272 100644 --- a/src/scripts/components/session-bookmark.js +++ b/src/scripts/components/session-bookmark.js @@ -7,7 +7,8 @@ import { HTMLCreator } from '../utils/dom'; import { AutoScroll } from '../utils/auto-scroll'; import { BookmarkManager } from '../session/bookmarks'; import { BookmarkProperties } from '../../common/bookmark-properties'; -import { FirefoxContainers } from '../services/firefox-containers'; +import { FirefoxContainers } from '../../common/services/firefox-containers'; +import { BrowserTabGroups } from '../../common/services/tab-groups'; // ***************************************************************************** // API @@ -40,10 +41,6 @@ export class SessionBookmark { const box = DomElem('div', { class: 'bookmark' }); - if (bookmark.pinned || info.properties.pinned) { - box.setAttribute('pinned', 'true'); - } - const favicon = DomElem('div', { class: 'favicon' }); favicon.setAttribute('bookmarkID', bookmark.id); favicon.setAttribute('over', ''); @@ -56,6 +53,11 @@ export class SessionBookmark { const propertiesBox = DomElem('div', { class: 'properties' }); propertiesBox.setAttribute('over', ''); + if (bookmark.pinned || info.properties.pinned) { + const pin = DomElem('div', { class: 'pin' }); + propertiesBox.appendChild(pin); + } + if (info.properties.container) { const container = FirefoxContainers.getContainerElement(info.properties.container); if (container) { @@ -63,6 +65,13 @@ export class SessionBookmark { } } + if (info.properties.group) { + const groupTag = BrowserTabGroups.getGroupElement(info.properties.group.title, info.properties.group.color); + if (groupTag) { + propertiesBox.appendChild(groupTag); + } + } + box.appendChild(favicon); box.appendChild(title); box.appendChild(propertiesBox); @@ -146,10 +155,6 @@ export class SessionTab { box.style.top = (tab.index + indexOffset) * bookmarkOffset + 'em'; box.setAttribute('url', tab.url); - if (tab.pinned) { - box.setAttribute('pinned', 'true'); - } - const favicon = DomElem('div', { class: 'favicon' }); favicon.setAttribute('tabID', tab.id); favicon.setAttribute('windowID', tab.windowId); @@ -166,7 +171,19 @@ export class SessionTab { } const propertiesBox = DomElem('div', { class: 'properties' }); - propertiesBox.setAttribute('over', ''); + + if (tab.pinned) { + const pin = DomElem('div', { class: 'pin' }); + propertiesBox.appendChild(pin); + } + + if (tab.groupId !== undefined && tab.groupId > -1) { + const group = BrowserTabGroups.getGroup(tab.groupId); + const groupTag = BrowserTabGroups.getGroupElement(group?.title, group?.color); + if (groupTag) { + propertiesBox.appendChild(groupTag); + } + } if (tab.cookieStoreId) { const container = FirefoxContainers.getContainerElement(tab.cookieStoreId); @@ -228,10 +245,6 @@ export class HistoryTab { box.style.top = (tab.index + indexOffset) * bookmarkOffset + 'em'; box.setAttribute('url', tab.url); - if (tab.pinned) { - box.setAttribute('pinned', 'true'); - } - const favicon = DomElem('div', { class: 'favicon' }); configureHoverItem(favicon, tab.url); @@ -255,6 +268,18 @@ export class HistoryTab { } } + if (tab.groupId > -1) { + const groupTag = BrowserTabGroups.getGroupElement(tab.groupTitle, tab.groupColor); + if (groupTag) { + propertiesBox.appendChild(groupTag); + } + } + + if (tab.pinned) { + const pin = DomElem('div', { class: 'pin' }); + propertiesBox.appendChild(pin); + } + box.appendChild(favicon); box.appendChild(title); box.appendChild(propertiesBox); diff --git a/src/scripts/components/session-container.js b/src/scripts/components/session-container.js index e5a51c6..edfd811 100644 --- a/src/scripts/components/session-container.js +++ b/src/scripts/components/session-container.js @@ -174,6 +174,12 @@ export class SessionContainer { this.updateSessionInfo(); } }); + + WindowEvents.on(document, 'TabGroupsLoaded', () => { + if (this.SyncModel.state.session == 'current') { + this.showCurrentSession(); + } + }); } GlobalEvents.on('UpdateCurrentSession', () => { diff --git a/src/scripts/components/session-folder-events.js b/src/scripts/components/session-folder-events.js index f729336..0d93f05 100644 --- a/src/scripts/components/session-folder-events.js +++ b/src/scripts/components/session-folder-events.js @@ -227,21 +227,13 @@ export function SessionHistoryEvents(container) { } } else { if (typeof document !== 'undefined') { - WindowEvents.emit(document, 'HistoryListCtxMenu-Open', { event: e }); + WindowEvents.emit(document, 'HistoryListCtxMenu-Open', { event: e, context: e }); } } }; - var onClick = function onClick(e) { - if (e.target.className === 'history-node') { - // WindowEvents.emit(document, 'HistoryListCtxMenu-Open', {event: e }); - // this.selectHistorySession(e.target); - } - }; - // ------------------------------------------------------------------------ // Init Code Events - container.addEventListener('click', onClick); container.addEventListener('mousedown', onMouseDown); } diff --git a/src/scripts/session-sync-ui.js b/src/scripts/session-sync-ui.js index ebc9a8a..bb18658 100644 --- a/src/scripts/session-sync-ui.js +++ b/src/scripts/session-sync-ui.js @@ -140,20 +140,35 @@ export const SessionSyncUI = function SessionSyncUI() { SM.addMenuEntry({ value: 'Restore (close all)', event: 'ReplaceSessionAll', icon: 'replace-all' }); SM.addMenuEntry({ value: 'Edit', event: 'EditSession', icon: 'edit' }); SM.addMenuEntry({ value: 'Delete', globalEvent: 'DeleteBookmarkItem', icon: 'delete' }); - SM.addMenuEntry({ value: 'Create new session', callback: SessionManager.createNewSession, icon: 'new-session', separator: 'top' }); + SM.addMenuEntry({ + value: 'Create new session', + callback: SessionManager.createNewSession, + icon: 'new-session', + separator: 'top' + }); SM.addMenuEntry({ value: 'Insert separator', callback: SessionManager.createSeparator, icon: 'new-session' }); content.appendChild(SM.DOMRoot); var SCBM = new ContextMenu({ name: 'SessionContextBookmarkMenu' }); SCBM.addMenuEntry({ value: 'Edit', event: 'EditBookmark', icon: 'edit' }); SCBM.addMenuEntry({ value: 'Delete', globalEvent: 'DeleteBookmarkItem', icon: 'delete' }); - SCBM.addMenuEntry({ value: 'Create new session', callback: SessionManager.createNewSession, icon: 'new-session', separator: 'top' }); + SCBM.addMenuEntry({ + value: 'Create new session', + callback: SessionManager.createNewSession, + icon: 'new-session', + separator: 'top' + }); SCBM.addMenuEntry({ value: 'Insert separator', callback: SessionManager.createSeparator, icon: 'new-session' }); content.appendChild(SCBM.DOMRoot); var SCSM = new ContextMenu({ name: 'SessionContextSeparatorMenu' }); SCSM.addMenuEntry({ value: 'Delete', globalEvent: 'DeleteBookmarkItem', icon: 'delete' }); - SCSM.addMenuEntry({ value: 'Create new session', callback: SessionManager.createNewSession, icon: 'new-session', separator: 'top' }); + SCSM.addMenuEntry({ + value: 'Create new session', + callback: SessionManager.createNewSession, + icon: 'new-session', + separator: 'top' + }); SCSM.addMenuEntry({ value: 'Insert separator', callback: SessionManager.createSeparator, icon: 'new-session' }); content.appendChild(SCSM.DOMRoot); @@ -330,22 +345,16 @@ export const SessionSyncUI = function SessionSyncUI() { WindowEvents.emit(document, 'CopyURL', url); }); - WindowEvents.on(document, 'HistoryMarksCtxMenu-OpenInNewTab', function (url) { - BookmarkManager.openBookmark(url, { - mode: 'newTab' - }); + WindowEvents.on(document, 'HistoryMarksCtxMenu-OpenInNewTab', function (e) { + BookmarkManager.openHistoryTabFromContext(e, 'newTab'); }); - WindowEvents.on(document, 'HistoryMarksCtxMenu-OpenInActiveTab', function (url) { - BookmarkManager.openBookmark(url, { - mode: 'activeTab' - }); + WindowEvents.on(document, 'HistoryMarksCtxMenu-OpenInActiveTab', function (e) { + BookmarkManager.openHistoryTabFromContext(e, 'activeTab'); }); - WindowEvents.on(document, 'HistoryMarksCtxMenu-OpenInNewWindow', function (url) { - BookmarkManager.openBookmark(url, { - mode: 'newWindow' - }); + WindowEvents.on(document, 'HistoryMarksCtxMenu-OpenInNewWindow', function (e) { + BookmarkManager.openHistoryTabFromContext(e, 'newWindow'); }); // HistorySessionCtxMenu events diff --git a/src/scripts/session/bookmarks.js b/src/scripts/session/bookmarks.js index 8978aaa..3f27d3d 100644 --- a/src/scripts/session/bookmarks.js +++ b/src/scripts/session/bookmarks.js @@ -5,6 +5,7 @@ import { AppConfig } from '../config'; import { BookmarkProperties } from '../../common/bookmark-properties'; +import { HistoryManager } from './history'; // ************************************************************************ // API @@ -57,13 +58,24 @@ export const BookmarkManager = (function BookmarkManager() { return state ? 'newTab' : 'activeTab'; }; - var createBookmarkFromTab = function createBookmarkFromTab(tab, parentId) { + var createBookmarkFromTab = async function createBookmarkFromTab(tab, parentId) { var url = tab.url; browser.runtime.sendMessage({ event: 'save-favicon', tab: tab }); var title = tab.title; console.log('tab', tab); - if (tab.cookieStoreId || tab.pinned) { + + if (tab.groupId !== undefined && tab.groupId > -1 && !tab.groupTitle && typeof browser !== 'undefined' && browser.tabGroups) { + try { + const group = await browser.tabGroups.get(tab.groupId); + tab.groupTitle = group.title; + tab.groupColor = group.color; + } catch (e) { + console.log('Error fetching tab group info', e); + } + } + + if (tab.cookieStoreId || tab.pinned || (tab.groupId !== undefined && tab.groupId > -1)) { title += BookmarkProperties.encode(tab); } @@ -85,8 +97,6 @@ export const BookmarkManager = (function BookmarkManager() { }; var openBookmark = function openBookmark(url, info) { - console.log('openBookmark', url, info); - var options = { url: url }; @@ -108,12 +118,35 @@ export const BookmarkManager = (function BookmarkManager() { options.pinned = true; } + if (bookmarkInfo.properties.group) { + info.group = bookmarkInfo.properties.group; + } + delete options.title; } browser.runtime.sendMessage({ event: 'open-tab', options: options, info: info }); }; + function getTabID(e) { + var ID = e.target.getAttribute('tabID'); + return ID ? ID | 0 : null; + } + + function getWindowID(e) { + var ID = e.target.getAttribute('windowID'); + return ID ? ID | 0 : null; + } + + var openHistoryTabFromContext = function openHistoryTabFromContext(e, mode) { + let windowID = getWindowID(e); + let tabID = getTabID(e); + let tab = HistoryManager.getHistoryTab(windowID, tabID); + if (tab) { + openHistoryTab(tab, mode); + } + }; + var openHistoryTab = function openHistoryTab(tab, mode) { console.log('openHistoryTab', tab, mode); @@ -121,6 +154,8 @@ export const BookmarkManager = (function BookmarkManager() { url: tab.url }; + var info = { mode: mode }; + if (AppConfig.isPanel() == false) { options.active = false; if (mode === 'activeTab') { @@ -136,7 +171,14 @@ export const BookmarkManager = (function BookmarkManager() { options.pinned = true; } - browser.runtime.sendMessage({ event: 'open-tab', options: options, info: { mode: mode } }); + if (tab.groupId) { + info.group = { + title: tab.groupTitle, + color: tab.groupColor + }; + } + + browser.runtime.sendMessage({ event: 'open-tab', options: options, info: info }); }; var setTitle = function setTitle(bookmark, title, callback) { @@ -193,6 +235,7 @@ export const BookmarkManager = (function BookmarkManager() { getOpenMode: getOpenMode, openBookmark: openBookmark, openHistoryTab: openHistoryTab, + openHistoryTabFromContext: openHistoryTabFromContext, createBookmark: createBookmark, createBookmarkFromTab: createBookmarkFromTab, searchBookmarks: searchBookmarks, diff --git a/src/scripts/utils/context-menu.js b/src/scripts/utils/context-menu.js index eb04db4..3fe14b4 100644 --- a/src/scripts/utils/context-menu.js +++ b/src/scripts/utils/context-menu.js @@ -72,18 +72,21 @@ export function ContextMenu(options) { hideMenu(); }; } + if (opt.callback) { return function actionEvent(e) { opt.callback(context, e); hideMenu(); }; } + if (opt.globalEvent) { return function action() { GlobalEvents.emit(opt.globalEvent, context); hideMenu(); }; } + return function action() { WindowEvents.emit(document, options.name + '-' + opt.event, context); hideMenu(); diff --git a/src/scripts/utils/global-events.js b/src/scripts/utils/global-events.js index cedca7f..c87d433 100644 --- a/src/scripts/utils/global-events.js +++ b/src/scripts/utils/global-events.js @@ -76,11 +76,15 @@ function NamespaceEvents() { const WindowEvents = new NamespaceEvents(); WindowEvents.onLocal = function onLocal(topic, callback) { - this.on(document, topic, callback); + if (typeof document !== 'undefined') { + this.on(document, topic, callback); + } }; WindowEvents.emitLocal = function emitLocal(topic, object) { - this.emit(document, topic, object); + if (typeof document !== 'undefined') { + this.emit(document, topic, object); + } }; export { GlobalEventSystem as GlobalEvents, WindowEvents }; diff --git a/wxt.config.js b/wxt.config.js index 1cab98a..484ba79 100644 --- a/wxt.config.js +++ b/wxt.config.js @@ -42,7 +42,16 @@ export default defineConfig(() => { } } }, - permissions: ['storage', 'bookmarks', 'tabs', 'clipboardWrite', 'unlimitedStorage', 'cookies', 'contextualIdentities', 'favicon'], + permissions: [ + 'storage', + 'bookmarks', + 'tabs', + 'clipboardWrite', + 'unlimitedStorage', + 'cookies', + 'contextualIdentities', + 'tabGroups' + ], action: { default_icon: { 16: 'images/icon16.png', From 274d52d9cdee90313e2e763647bdde6bc2bc39ea Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Mon, 20 Apr 2026 01:29:21 +0300 Subject: [PATCH 24/32] Moved CSS files and fixed image loading issue --- assets/css/index.css | 19 - {assets => src/assets}/css/_style.css | 294 +++++----- {assets => src/assets}/css/components.css | 0 {assets => src/assets}/css/config-panel.css | 10 +- {assets => src/assets}/css/confirm-box.css | 0 {assets => src/assets}/css/context-menu.css | 194 +++---- {assets => src/assets}/css/field-widget.css | 0 {assets => src/assets}/css/filter-panel.css | 2 +- {assets => src/assets}/css/generic.css | 0 {assets => src/assets}/css/hotkey-manager.css | 0 .../assets}/css/notification-system.css | 2 +- {assets => src/assets}/css/search-bar.css | 286 +++++----- .../assets}/css/session-container.css | 60 +- .../assets}/css/session-header-bar.css | 8 +- .../assets}/css/session-sorting.css | 2 +- .../assets}/css/session-toolbar.css | 540 +++++++++--------- {assets => src/assets}/css/sessions-list.css | 0 {assets => src/assets}/css/tooltip-system.css | 0 {assets => src/assets}/css/trash-can.css | 2 +- {assets => src/assets}/css/url-bar.css | 0 src/assets/session-sync.css | 19 + src/entrypoints/popup/main.js | 5 - src/entrypoints/scripts/session-sync.js | 2 + .../{popup/index.html => session-sync.html} | 2 +- wxt.config.js | 5 +- 25 files changed, 741 insertions(+), 711 deletions(-) delete mode 100644 assets/css/index.css rename {assets => src/assets}/css/_style.css (89%) rename {assets => src/assets}/css/components.css (100%) rename {assets => src/assets}/css/config-panel.css (94%) rename {assets => src/assets}/css/confirm-box.css (100%) rename {assets => src/assets}/css/context-menu.css (67%) rename {assets => src/assets}/css/field-widget.css (100%) rename {assets => src/assets}/css/filter-panel.css (97%) rename {assets => src/assets}/css/generic.css (100%) rename {assets => src/assets}/css/hotkey-manager.css (100%) rename {assets => src/assets}/css/notification-system.css (93%) rename {assets => src/assets}/css/search-bar.css (92%) rename {assets => src/assets}/css/session-container.css (85%) rename {assets => src/assets}/css/session-header-bar.css (94%) rename {assets => src/assets}/css/session-sorting.css (97%) rename {assets => src/assets}/css/session-toolbar.css (80%) rename {assets => src/assets}/css/sessions-list.css (100%) rename {assets => src/assets}/css/tooltip-system.css (100%) rename {assets => src/assets}/css/trash-can.css (96%) rename {assets => src/assets}/css/url-bar.css (100%) create mode 100644 src/assets/session-sync.css delete mode 100644 src/entrypoints/popup/main.js create mode 100644 src/entrypoints/scripts/session-sync.js rename src/entrypoints/{popup/index.html => session-sync.html} (76%) diff --git a/assets/css/index.css b/assets/css/index.css deleted file mode 100644 index 43e6b61..0000000 --- a/assets/css/index.css +++ /dev/null @@ -1,19 +0,0 @@ -@import './_style.css'; -@import './components.css'; -@import './config-panel.css'; -@import './confirm-box.css'; -@import './context-menu.css'; -@import './field-widget.css'; -@import './filter-panel.css'; -@import './generic.css'; -@import './hotkey-manager.css'; -@import './notification-system.css'; -@import './search-bar.css'; -@import './session-container.css'; -@import './session-header-bar.css'; -@import './session-sorting.css'; -@import './session-toolbar.css'; -@import './sessions-list.css'; -@import './tooltip-system.css'; -@import './trash-can.css'; -@import './url-bar.css'; diff --git a/assets/css/_style.css b/src/assets/css/_style.css similarity index 89% rename from assets/css/_style.css rename to src/assets/css/_style.css index 8b96daa..f4144d3 100644 --- a/assets/css/_style.css +++ b/src/assets/css/_style.css @@ -1,146 +1,148 @@ -/** - * Session-Sync - */ -#session-sync { - width: 100%; - height: 100%; - min-height: 400px; - margin: 0; - - font-size: 12px; - font-family: 'Open Sans', 'Segoe UI', Arial, Helvetica, sans-serif; - - color: #000 !important; - overflow: hidden; - - -moz-user-select: none; -} - -#session-sync[panel='false'] { - position: absolute; -} - -#session-sync[panel='false'] .sessions-area .panel-resize-handle { - display: none; -} - -/* - * Only DOM nodes from here - */ - -#session-sync #overlay { - width: 100%; - height: 100%; - - background: rgba(255, 255, 255, 0.01); - - position: absolute; - top: 0; - left: 0; - z-index: 2500; - - display: none; -} - -#session-sync #overlay[state='true'] { - display: block; -} - -/* - * Panel Body - */ - -#session-sync #body { - height: 100%; -} - -#session-sync[config] .body { - background-color: rgba(255, 255, 255, 0.95); -} - -#session-sync .sessions-area { - width: 100%; - height: 100%; - - box-sizing: border-box; - - transition: all 0.2s; - - display: flex; - flex-direction: column; -} - -#session-sync[show='false'] .sessions-area { - display: none; -} - -#session-sync[state='pin'] .sessions-area { - top: 0; - right: 0; - margin: 0; - transform: translate(0); -} - -#session-sync[state='centered'] .sessions-area { - top: calc(50% - 50px); - left: 50%; - transform: translate(-50%, -50%); -} - -#session-sync[config] .sessions-area { - left: 400px; - top: 20%; - transform: translate(0); -} - -#session-sync .sessions-area-body { - width: 100%; - overflow: hidden; - - flex: 1; - display: flex; - flex-direction: row; -} - -/* - * Container resize - */ - -#session-sync[resize] .body { - background: rgba(255, 255, 255, 0.95) !important; - cursor: se-resize; -} - -#session-sync[resize] .sessions-area { - transition: none; -} - -#session-sync .sessions-area .panel-resize-handle { - content: ' '; - width: 12px; - height: 12px; - margin-top: -12px; - background: url('images/resize.png') center no-repeat !important; - position: absolute; - background: #ddd; - bottom: 0; - left: 0px; - cursor: sw-resize; - z-index: 10; - - transform: rotate(90deg); -} - -#session-sync .sessions-area .panel-resize-handle:hover { - background: #1ab61a; -} - -#session-sync .sessions-area .panel-resize-handle[resizing] { - background: #2d92eb; -} - -#clipboard { - position: absolute; - top: -100px; - z-index: -100; -} +/** + * Session-Sync + */ +#session-sync { + width: 100%; + height: 100%; + min-width: 500px; + min-height: 400px; + margin: 0; + + font-size: 12px; + font-family: 'Open Sans', 'Segoe UI', Arial, Helvetica, sans-serif; + + color: #000 !important; + color-scheme: light dark; + overflow: hidden; + + user-select: none; +} + +#session-sync[panel='false'] { + position: absolute; +} + +#session-sync[panel='false'] .sessions-area .panel-resize-handle { + display: none; +} + +/* + * Only DOM nodes from here + */ + +#session-sync #overlay { + width: 100%; + height: 100%; + + background: rgba(255, 255, 255, 0.01); + + position: absolute; + top: 0; + left: 0; + z-index: 2500; + + display: none; +} + +#session-sync #overlay[state='true'] { + display: block; +} + +/* + * Panel Body + */ + +#session-sync #body { + height: 100%; +} + +#session-sync[config] .body { + background-color: rgba(255, 255, 255, 0.95); +} + +#session-sync .sessions-area { + width: 100%; + height: 100%; + + box-sizing: border-box; + + transition: all 0.2s; + + display: flex; + flex-direction: column; +} + +#session-sync[show='false'] .sessions-area { + display: none; +} + +#session-sync[state='pin'] .sessions-area { + top: 0; + right: 0; + margin: 0; + transform: translate(0); +} + +#session-sync[state='centered'] .sessions-area { + top: calc(50% - 50px); + left: 50%; + transform: translate(-50%, -50%); +} + +#session-sync[config] .sessions-area { + left: 400px; + top: 20%; + transform: translate(0); +} + +#session-sync .sessions-area-body { + width: 100%; + overflow: hidden; + + flex: 1; + display: flex; + flex-direction: row; +} + +/* + * Container resize + */ + +#session-sync[resize] .body { + background: rgba(255, 255, 255, 0.95) !important; + cursor: se-resize; +} + +#session-sync[resize] .sessions-area { + transition: none; +} + +#session-sync .sessions-area .panel-resize-handle { + content: ' '; + width: 12px; + height: 12px; + margin-top: -12px; + background: url('../images/resize.png') center no-repeat !important; + position: absolute; + background: #ddd; + bottom: 0; + left: 0px; + cursor: sw-resize; + z-index: 10; + + transform: rotate(90deg); +} + +#session-sync .sessions-area .panel-resize-handle:hover { + background: #1ab61a; +} + +#session-sync .sessions-area .panel-resize-handle[resizing] { + background: #2d92eb; +} + +#clipboard { + position: absolute; + top: -100px; + z-index: -100; +} diff --git a/assets/css/components.css b/src/assets/css/components.css similarity index 100% rename from assets/css/components.css rename to src/assets/css/components.css diff --git a/assets/css/config-panel.css b/src/assets/css/config-panel.css similarity index 94% rename from assets/css/config-panel.css rename to src/assets/css/config-panel.css index 9acdf23..1c4d44e 100644 --- a/assets/css/config-panel.css +++ b/src/assets/css/config-panel.css @@ -60,7 +60,7 @@ margin: 2em auto; padding: 0; border: none; - background: url('images/icon64.png') center top no-repeat; + background: url('../images/icon64.png') center top no-repeat; } #session-sync .config-panel .logo:hover { @@ -107,13 +107,13 @@ width: 1.5em; height: 1.5em; margin: 0 0.5em; - background-image: url('images/arrow-up.png'); + background-image: url('../images/arrow-up.png'); background-size: contain; float: left; } #session-sync .config-panel .section-container[collapsed] .drop-icon { - background-image: url('images/arrow-down.png'); + background-image: url('../images/arrow-down.png'); background-size: contain; } @@ -177,12 +177,12 @@ } #session-sync .config-panel .comp-range-control .button.inc { - background: url('images/arrow-right.png') center top no-repeat; + background: url('../images/arrow-right.png') center top no-repeat; background-size: 100%; } #session-sync .config-panel .comp-range-control .button.dec { - background: url('images/arrow-left.png') center top no-repeat; + background: url('../images/arrow-left.png') center top no-repeat; background-size: 100%; } diff --git a/assets/css/confirm-box.css b/src/assets/css/confirm-box.css similarity index 100% rename from assets/css/confirm-box.css rename to src/assets/css/confirm-box.css diff --git a/assets/css/context-menu.css b/src/assets/css/context-menu.css similarity index 67% rename from assets/css/context-menu.css rename to src/assets/css/context-menu.css index 359ffdb..8faa6d3 100644 --- a/assets/css/context-menu.css +++ b/src/assets/css/context-menu.css @@ -1,97 +1,97 @@ -/** - * Context Menu - */ - -#session-sync .context-menu { - width: 180px; - padding: 5px; - background: #fff; - border: 1px #ccc solid; - - display: none; - position: fixed; - left: 0; - top: 0; - - box-shadow: 0px 0px 5px 0 #ccc; - transform: translateX(5px); - z-index: 3000; - - box-sizing: border-box; -} - -#session-sync .context-menu[data-active='true'] { - display: block; -} - -#session-sync .context-menu .separator { - width: 100%; - height: 1px; - margin: 3px 0; - background: #ccc; -} - -#session-sync .context-menu .button { - width: 100%; - padding: 5px 5px 5px 30px; - - background-image: url('images/icons/heart.png'); - background-size: 20px; - background-repeat: no-repeat; - background-position: 3px 3px; - box-sizing: border-box; -} - -#session-sync .context-menu .button:hover { - background-color: #eee; - cursor: pointer; -} - -#session-sync .context-menu .button[icon='save'] { - background-image: url('images/icons/heart.png'); -} - -#session-sync .context-menu .button[icon='new-tab'] { - background-image: url('images/icons/ribbon.png'); -} - -#session-sync .context-menu .button[icon='same-tab'] { - background-image: url('images/icons/recycle.png'); -} - -#session-sync .context-menu .button[icon='new-window'] { - background-image: url('images/icons/frames.png'); -} - -#session-sync .context-menu .button[icon='replace-current'] { - background-image: url('images/material_icons/repeat_one.svg'); -} - -#session-sync .context-menu .button[icon='replace-all'] { - background-image: url('images/material_icons/repeat.svg'); -} - -#session-sync .context-menu .button[icon='copy'] { - background-image: url('images/icons/clipboard.png'); -} - -#session-sync .context-menu .button[icon='edit'] { - background-image: url('images/icons/document.png'); -} - -#session-sync .context-menu .button[icon='delete'] { - background-image: url('images/icons/denied.png'); -} - -#session-sync .context-menu .button[icon='same-window'] { - background-image: url('images/icons/stack.png'); -} - -#session-sync .context-menu .button[icon='new-session'] { - background-image: url('images/icons/star.png'); -} - -#session-sync .context-menu[icons='no'] .button { - padding: 5px; - background-image: unset; -} +/** + * Context Menu + */ + +#session-sync .context-menu { + width: 180px; + padding: 5px; + background: #fff; + border: 1px #ccc solid; + + display: none; + position: fixed; + left: 0; + top: 0; + + box-shadow: 0px 0px 5px 0 #ccc; + transform: translateX(5px); + z-index: 3000; + + box-sizing: border-box; +} + +#session-sync .context-menu[data-active='true'] { + display: block; +} + +#session-sync .context-menu .separator { + width: 100%; + height: 1px; + margin: 3px 0; + background: #ccc; +} + +#session-sync .context-menu .button { + width: 100%; + padding: 5px 5px 5px 30px; + + background-image: url('../images/icons/heart.png'); + background-size: 20px; + background-repeat: no-repeat; + background-position: 3px 3px; + box-sizing: border-box; +} + +#session-sync .context-menu .button:hover { + background-color: #eee; + cursor: pointer; +} + +#session-sync .context-menu .button[icon='save'] { + background-image: url('../images/icons/heart.png'); +} + +#session-sync .context-menu .button[icon='new-tab'] { + background-image: url('../images/icons/ribbon.png'); +} + +#session-sync .context-menu .button[icon='same-tab'] { + background-image: url('../images/icons/recycle.png'); +} + +#session-sync .context-menu .button[icon='new-window'] { + background-image: url('../images/icons/frames.png'); +} + +#session-sync .context-menu .button[icon='replace-current'] { + background-image: url('../images/material_icons/repeat_one.svg'); +} + +#session-sync .context-menu .button[icon='replace-all'] { + background-image: url('../images/material_icons/repeat.svg'); +} + +#session-sync .context-menu .button[icon='copy'] { + background-image: url('../images/icons/clipboard.png'); +} + +#session-sync .context-menu .button[icon='edit'] { + background-image: url('../images/icons/document.png'); +} + +#session-sync .context-menu .button[icon='delete'] { + background-image: url('../images/icons/denied.png'); +} + +#session-sync .context-menu .button[icon='same-window'] { + background-image: url('../images/icons/stack.png'); +} + +#session-sync .context-menu .button[icon='new-session'] { + background-image: url('../images/icons/star.png'); +} + +#session-sync .context-menu[icons='no'] .button { + padding: 5px; + background-image: unset; +} diff --git a/assets/css/field-widget.css b/src/assets/css/field-widget.css similarity index 100% rename from assets/css/field-widget.css rename to src/assets/css/field-widget.css diff --git a/assets/css/filter-panel.css b/src/assets/css/filter-panel.css similarity index 97% rename from assets/css/filter-panel.css rename to src/assets/css/filter-panel.css index 58d7811..dc2a9d1 100644 --- a/assets/css/filter-panel.css +++ b/src/assets/css/filter-panel.css @@ -36,7 +36,7 @@ margin: 10px 0; height: 64px; width: 100%; - background: url('images/icon64.png') center top no-repeat; + background: url('../images/icon64.png') center top no-repeat; } #session-sync .filter-panel .separator { diff --git a/assets/css/generic.css b/src/assets/css/generic.css similarity index 100% rename from assets/css/generic.css rename to src/assets/css/generic.css diff --git a/assets/css/hotkey-manager.css b/src/assets/css/hotkey-manager.css similarity index 100% rename from assets/css/hotkey-manager.css rename to src/assets/css/hotkey-manager.css diff --git a/assets/css/notification-system.css b/src/assets/css/notification-system.css similarity index 93% rename from assets/css/notification-system.css rename to src/assets/css/notification-system.css index 3958f51..eed05e0 100644 --- a/assets/css/notification-system.css +++ b/src/assets/css/notification-system.css @@ -44,7 +44,7 @@ content: ' '; width: 24px; height: 24px; - background: url('images/logo.png') center no-repeat; + background: url('../images/logo.png') center no-repeat; background-size: contain; top: 50%; left: 20px; diff --git a/assets/css/search-bar.css b/src/assets/css/search-bar.css similarity index 92% rename from assets/css/search-bar.css rename to src/assets/css/search-bar.css index 73dba4e..c360454 100644 --- a/assets/css/search-bar.css +++ b/src/assets/css/search-bar.css @@ -1,143 +1,143 @@ -/** - * Control Panel - */ - -#session-sync .search-bar { - width: 5em; - height: 1.5em; - margin: 0; - padding: 0; - - background: hsla(0, 0%, 100%, 0.5); - border-radius: 1.5em; - - overflow: hidden; - opacity: 1; - - line-height: 1em; - - position: absolute; - top: 0.5em; - left: 0.5em; - z-index: 2; - - transition: all 0.2s ease-out; -} - -#session-sync .search-bar[content='false'] { - width: 5em !important; -} - -#session-sync .search-bar[content='false'] .search { - text-align: center; -} - -#session-sync .search-bar[active='true'] { - width: 25%; - opacity: 1; -} - -#session-sync .search-bar:hover { - opacity: 1; -} - -#session-sync .search-bar .search { - width: 100%; - height: 100%; - padding: 0 0.75em !important; - background: hsla(0, 0%, 100%, 0.95); - border: 1px solid #ccc; - border-radius: 1em; - color: #333; - font-size: 0.75em; - - box-sizing: border-box; - transition: all 0.2s ease-out; -} - -#session-sync .search-bar .search:focus { - border-color: #2d92eb !important; -} - -#session-sync .search-bar[match='true'][content='true'] .search { - color: green; -} - -#session-sync .search-bar .create-session { - width: 1em; - height: 1em; - background-image: url('images/icons/star.png'); - background-size: 100%; - background-repeat: no-repeat; - background-position: center; - text-align: center; - color: #fff; - opacity: 0; - - position: absolute; - top: 0.25em; - right: -2.5em; - z-index: 0; - - filter: saturate(0%); - transition: all 0.2s ease-out; - box-sizing: border-box; -} - -#session-sync .search-bar[match='false'][content='true'] .create-session { - opacity: 0.6; - display: block; - right: 1.6em; -} - -#session-sync .search-bar .create-session:hover { - filter: saturate(100%); - cursor: pointer; - opacity: 1; -} - -#session-sync .search-bar .close { - width: 1.2em; - height: 1.2em; - background: #fff; - border-radius: 2em; - opacity: 1; - - position: absolute; - top: 0.15em; - right: -2em; - z-index: 0; - - box-sizing: border-box; - transition: opacity 0.2s ease-out; -} - -#session-sync .search-bar[active='true'][content='true'] .close { - opacity: 0.75; - right: 0.3em; -} - -#session-sync .search-bar .close:hover { - cursor: pointer; -} - -#session-sync .search-bar .close:hover::after, -#session-sync .search-bar .close:hover::before { - background: #333; -} - -#session-sync .search-bar .close:after, -#session-sync .search-bar .close:before { - content: ''; - width: 60%; - height: 16%; - background: #aaa; - position: absolute; - top: 42%; - left: 20%; - transform: rotate(45deg); -} - -#session-sync .search-bar .close:after { - transform: rotate(-45deg); -} +/** + * Control Panel + */ + +#session-sync .search-bar { + width: 5em; + height: 1.5em; + margin: 0; + padding: 0; + + background: hsla(0, 0%, 100%, 0.5); + border-radius: 1.5em; + + overflow: hidden; + opacity: 1; + + line-height: 1em; + + position: absolute; + top: 0.5em; + left: 0.5em; + z-index: 2; + + transition: all 0.2s ease-out; +} + +#session-sync .search-bar[content='false'] { + width: 5em !important; +} + +#session-sync .search-bar[content='false'] .search { + text-align: center; +} + +#session-sync .search-bar[active='true'] { + width: 25%; + opacity: 1; +} + +#session-sync .search-bar:hover { + opacity: 1; +} + +#session-sync .search-bar .search { + width: 100%; + height: 100%; + padding: 0 0.75em !important; + background: hsla(0, 0%, 100%, 0.95); + border: 1px solid #ccc; + border-radius: 1em; + color: #333; + font-size: 0.75em; + + box-sizing: border-box; + transition: all 0.2s ease-out; +} + +#session-sync .search-bar .search:focus { + border-color: #2d92eb !important; +} + +#session-sync .search-bar[match='true'][content='true'] .search { + color: green; +} + +#session-sync .search-bar .create-session { + width: 1em; + height: 1em; + background-image: url('../images/icons/star.png'); + background-size: 100%; + background-repeat: no-repeat; + background-position: center; + text-align: center; + color: #fff; + opacity: 0; + + position: absolute; + top: 0.25em; + right: -2.5em; + z-index: 0; + + filter: saturate(0%); + transition: all 0.2s ease-out; + box-sizing: border-box; +} + +#session-sync .search-bar[match='false'][content='true'] .create-session { + opacity: 0.6; + display: block; + right: 1.6em; +} + +#session-sync .search-bar .create-session:hover { + filter: saturate(100%); + cursor: pointer; + opacity: 1; +} + +#session-sync .search-bar .close { + width: 1.2em; + height: 1.2em; + background: #fff; + border-radius: 2em; + opacity: 1; + + position: absolute; + top: 0.15em; + right: -2em; + z-index: 0; + + box-sizing: border-box; + transition: opacity 0.2s ease-out; +} + +#session-sync .search-bar[active='true'][content='true'] .close { + opacity: 0.75; + right: 0.3em; +} + +#session-sync .search-bar .close:hover { + cursor: pointer; +} + +#session-sync .search-bar .close:hover::after, +#session-sync .search-bar .close:hover::before { + background: #333; +} + +#session-sync .search-bar .close:after, +#session-sync .search-bar .close:before { + content: ''; + width: 60%; + height: 16%; + background: #aaa; + position: absolute; + top: 42%; + left: 20%; + transform: rotate(45deg); +} + +#session-sync .search-bar .close:after { + transform: rotate(-45deg); +} diff --git a/assets/css/session-container.css b/src/assets/css/session-container.css similarity index 85% rename from assets/css/session-container.css rename to src/assets/css/session-container.css index 3ad3a00..be32c9c 100644 --- a/assets/css/session-container.css +++ b/src/assets/css/session-container.css @@ -31,7 +31,7 @@ height: 3.4em; width: 3.4em; - background: url('images/icons/heart.png') no-repeat; + background: url('../images/icons/heart.png') no-repeat; background-size: 2em; background-position: 1em 0.4em; background-color: #f5f5f5; @@ -177,7 +177,7 @@ #session-sync .session-container .bookmark .favicon { width: 1.33em; height: 100%; - background: url('images/icons/globe.png'); + background: url('../images/icons/globe.png'); background-size: contain; background-repeat: no-repeat; background-position: center center; @@ -208,7 +208,7 @@ #session-sync .session-container .bookmark .pin { width: 1.5em; height: 100%; - background-image: url('images/icons/pin.png'); + background-image: url('../images/icons/pin.png'); background-size: contain; background-repeat: no-repeat; background-position: center center; @@ -253,7 +253,7 @@ padding: 0 0.6em; /* padding: 0 0.6em 0 1.8em; - background-image: url('images/material_icons/tab_group.svg'); + background-image: url('../images/material_icons/tab_group.svg'); background-size: 1em; background-repeat: no-repeat; background-position: 0.4em center; */ @@ -261,44 +261,70 @@ color: #fff; white-space: nowrap; text-overflow: ellipsis; - font-weight: 500; + font-weight: 600; font-size: 0.9em; overflow: hidden; - border-radius: 4px; + border-radius: 6px; border: 1px solid rgba(255, 255, 255, 0.2); box-sizing: border-box; display: flex; } -#session-sync .session-container .bookmark .tab-group-tag[color='grey'] { - background-color: #5f6368; +#session-sync .session-container .bookmark .tab-group-tag[color='grey'], +#session-sync .session-container .bookmark .tab-group-tag[color='gray'] { + background-color: #5f636810; + color: #5f6368; + border: 1px solid #5f6368; } + #session-sync .session-container .bookmark .tab-group-tag[color='blue'] { - background-color: #1a73e8; + background-color: #1a73e810; + color: #1a73e8; + border: 1px solid #1a73e8; } + #session-sync .session-container .bookmark .tab-group-tag[color='red'] { - background-color: #d93025; + background-color: #d9302510; + color: #d93025; + border: 1px solid #d93025; } + #session-sync .session-container .bookmark .tab-group-tag[color='yellow'] { - background-color: #f29900; - color: #202124; + background-color: #c4821010; + color: #c48210; + border: 1px solid #c48210; } + #session-sync .session-container .bookmark .tab-group-tag[color='green'] { - background-color: #1e8e3e; + background-color: #1e8e3e10; + color: #1e8e3e; + border: 1px solid #1e8e3e; } + #session-sync .session-container .bookmark .tab-group-tag[color='pink'] { - background-color: #d01884; + background-color: #d0188410; + color: #d01884; + border: 1px solid #d01884; } + #session-sync .session-container .bookmark .tab-group-tag[color='purple'] { - background-color: #9334e6; + background-color: #9334e610; + color: #9334e6; + border: 1px solid #9334e6; } + #session-sync .session-container .bookmark .tab-group-tag[color='cyan'] { - background-color: #007b83; + background-color: #007b8310; + color: #007b83; + border: 1px solid #007b83; } + #session-sync .session-container .bookmark .tab-group-tag[color='orange'] { - background-color: #fa903e; + background-color: #bb4b0a10; + color: #bb4b0a; + border: 1px solid #bb4b0a; } #session-sync .session-container .bookmark .tab-group-tag .name { diff --git a/assets/css/session-header-bar.css b/src/assets/css/session-header-bar.css similarity index 94% rename from assets/css/session-header-bar.css rename to src/assets/css/session-header-bar.css index 3c32a9e..613d798 100644 --- a/assets/css/session-header-bar.css +++ b/src/assets/css/session-header-bar.css @@ -21,7 +21,7 @@ #session-sync .header-bar .active-session-btn { width: 2.5em; - background: url('images/icon128.png') no-repeat center; + background: url('../images/icon128.png') no-repeat center; background-size: 1em; height: 60%; opacity: 0.5; @@ -65,7 +65,7 @@ #session-sync .header-bar .addon-config { width: 1.8em; height: 1.8em; - background-image: url('images/menu.png'); + background-image: url('../images/menu.png'); background-size: 55%; background-repeat: no-repeat; background-position: center center; @@ -93,7 +93,7 @@ #session-sync .header-bar .header-menu { width: 1.8em; height: 1.8em; - background-image: url('images/icons_v2/menu.png'); + background-image: url('../images/icons_v2/menu.png'); background-size: 75% 80%; background-repeat: no-repeat; background-position: center center; @@ -178,7 +178,7 @@ #session-sync .header-bar .header-menu .menu-button .icon { height: 2em; - background-image: url('images/icons/power.png'); + background-image: url('../images/icons/power.png'); background-size: contain !important; background-repeat: no-repeat; background-position: center; diff --git a/assets/css/session-sorting.css b/src/assets/css/session-sorting.css similarity index 97% rename from assets/css/session-sorting.css rename to src/assets/css/session-sorting.css index 79a74d1..29a6761 100644 --- a/assets/css/session-sorting.css +++ b/src/assets/css/session-sorting.css @@ -8,7 +8,7 @@ padding: 0; display: flex; - background-image: url('images/sort.png'); + background-image: url('../images/sort.png'); background-size: 20px; background-repeat: no-repeat; background-position: center center; diff --git a/assets/css/session-toolbar.css b/src/assets/css/session-toolbar.css similarity index 80% rename from assets/css/session-toolbar.css rename to src/assets/css/session-toolbar.css index ecb1a40..e5a1305 100644 --- a/assets/css/session-toolbar.css +++ b/src/assets/css/session-toolbar.css @@ -1,268 +1,272 @@ -/** - * Control Panel - */ - -#session-sync .session-toolbar { - width: 100%; - height: 3em; - - background: #fff; - color: #000; - display: flex; - font-size: 12px; - - position: relative; - opacity: 100; - border-bottom: 1px solid #ccc; - box-shadow: 0px 0px 3px 0px #ccc; - z-index: 1; - - transition: all 0.15s; -} - -#session-sync .session-toolbar .session-selector { - width: 220px; - min-width: 160px; - max-width: 60%; - height: 100%; - margin: 0; - padding: 0; - border-right: 1px solid #ddd; - - line-height: 3em; - word-break: break-all; - text-align: center; - color: #555; - - display: flex; - box-sizing: border-box; -} - -#session-sync .session-toolbar .session-selector .button { - text-align: center; - overflow: hidden; - display: block; - color: #333; - flex: 1; -} - -#session-sync .session-toolbar .session-selector .button:hover { - cursor: pointer; -} - -#session-sync .session-toolbar .session-selector .active-session { - background-image: url('images/icon128.png'); - background-repeat: no-repeat; - background-position: center center; - background-size: 1.8em; - background-color: #fff; -} - -#session-sync[list='active'] .session-toolbar .session-selector .active-session, -#session-sync[list='sync'] .session-toolbar .session-selector .sync, -#session-sync[list='history'] .session-toolbar .session-selector .history { - display: block; - background-color: #2d92eb; - color: #fff; -} - -#session-sync[history='false'] .session-toolbar .session-selector .history { - display: none; -} - -/* - * Session info - */ - -#session-sync:not([session='restore']) .session-toolbar .session-date { - display: none; -} - -#session-sync .session-toolbar .session-date { - padding: 0.9em 1em; - color: #999; - text-align: right; - display: block; - position: absolute; - top: -1px; - right: 0; - z-index: 10; -} - -/** - * Menu Section - */ - -#session-sync .session-toolbar .menu-bar { - height: 100%; - padding: 0.5em; - - box-sizing: border-box; - flex: 1; -} - -#session-sync .session-toolbar[session-type='separator'] .menu-bar { - display: none; -} - -#session-sync .session-toolbar .menu-bar .button { - width: 2.6em; - height: 100%; - - background: url('images/icons/none.png') no-repeat center; - background-size: contain !important; - - display: none; - float: left; - - /* opacity: 0.75; - filter: grayscale(100%); */ -} - -#session-sync .session-toolbar .menu-bar .button:hover { - cursor: pointer; - opacity: 1; - box-shadow: none; -} - -#session-sync .session-toolbar .menu-bar .separator { - height: 100%; - width: 1px; - margin: 0 0.5em; - background: #ccc; - float: left; - display: none; -} - -#session-sync .session-toolbar .menu-bar .add { - background: url('images/icons/star.png') no-repeat center; -} - -#session-sync .session-toolbar .menu-bar .save { - background: url('images/icons/heart.png') no-repeat center; -} - -#session-sync .session-toolbar .menu-bar .merge-sessions { - background: url('images/icons/merge.png') no-repeat center; -} - -#session-sync .session-toolbar .menu-bar .replace-session { - background: url('images/icons/recycle.png') no-repeat center; -} - -#session-sync .session-toolbar .menu-bar .restore { - background: url('images/icons/stack.png') no-repeat center; -} - -#session-sync .session-toolbar .menu-bar .restore-new-win { - background: url('images/icons/frames.png') no-repeat center; -} - -#session-sync .session-toolbar .menu-bar .repalce-session-all { - background: url('images/material_icons/repeat.svg') no-repeat center; -} - -#session-sync .session-toolbar .menu-bar .repalce-session { - background: url('images/material_icons/repeat_one.svg') no-repeat center; -} - -#session-sync .session-toolbar .menu-bar .fix-session { - background: url('images/icons/tools.png') no-repeat center; -} - -#session-sync .session-toolbar .menu-bar .customize-toolbar { - background: url('images/icons_v2/more.png') no-repeat center; -} - -/* - * Menu state - */ - -#session-sync[session='current'] .session-toolbar .menu-bar .button[state-current='true'] { - display: block; -} - -#session-sync[session='restore'] .session-toolbar .menu-bar .button[state-restore='true'] { - display: block; -} - -#session-sync[session='restore'] .session-toolbar .menu-bar .separator { - display: block; -} - -#session-sync[session='history'] .session-toolbar .menu-bar .button[state-history='true'] { - display: block; -} - -/* - * Save configuration - */ - -#session-sync .session-toolbar .menu-bar .save-config { - height: 100%; - padding: 0 40px 0 0; - display: block; -} - -#session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch { - opacity: 1; - transition: opacity 0.2s ease-out; -} - -#session-sync .session-toolbar .menu-bar:hover .save-config .comp-toggle-switch { - opacity: 1; -} - -#session-sync[session='restore'] .session-toolbar .menu-bar .save-config { - display: none; -} - -#session-sync[session='history'] .session-toolbar .menu-bar .save-config .comp-toggle-switch[windows] { - display: none; -} - -#session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch { - height: 100%; - width: 4em; - margin: 0; - float: right; -} - -#session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch .label { - display: none; -} - -#session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch .switch { - width: 2.6em; - height: 100%; - background-color: #40b740; - background-image: url('images/icons/gear.png'); - background-size: 1.6em; - background-repeat: no-repeat; - background-position: center right 0.2em; - border-radius: 2em; - display: block; - - box-sizing: border-box; -} - -#session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch .switch:after { - display: none; -} - -#session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch .switch[state='false'] { - background-color: #bbb; - background-position: center left 0.2em; -} - -#session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch[pin] .switch { - background-image: url('images/icons/pin.png'); -} - -#session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch[pin-state] .switch { - background-image: url('images/icons/pin2.png'); -} - -#session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch[windows] .switch { - background-image: url('images/icons/stack.png'); -} +/** + * Control Panel + */ + +#session-sync .session-toolbar { + width: 100%; + height: 3em; + + background: #fff; + color: #000; + display: flex; + font-size: 12px; + + position: relative; + opacity: 100; + border-bottom: 1px solid #ccc; + box-shadow: 0px 0px 3px 0px #ccc; + z-index: 1; + + transition: all 0.15s; +} + +#session-sync .session-toolbar .session-selector { + width: 220px; + min-width: 160px; + max-width: 60%; + height: 100%; + margin: 0; + padding: 0; + border-right: 1px solid #ddd; + + line-height: 3em; + word-break: break-all; + text-align: center; + color: #555; + + display: flex; + box-sizing: border-box; +} + +#session-sync .session-toolbar .session-selector .button { + text-align: center; + overflow: hidden; + display: block; + color: #333; + flex: 1; +} + +#session-sync .session-toolbar .session-selector .button:hover { + cursor: pointer; +} + +#session-sync .session-toolbar .session-selector .active-session { + background-image: url('../images/icon128.png'); + background-repeat: no-repeat; + background-position: center center; + background-size: 1.8em; + background-color: #fff; +} + +#session-sync[list='active'] .session-toolbar .session-selector .active-session, +#session-sync[list='sync'] .session-toolbar .session-selector .sync, +#session-sync[list='history'] .session-toolbar .session-selector .history { + display: block; + background-color: #2d92eb; + color: #fff; +} + +#session-sync[history='false'] .session-toolbar .session-selector .history { + display: none; +} + +/* + * Session info + */ + +#session-sync:not([session='restore']) .session-toolbar .session-date { + display: none; +} + +#session-sync .session-toolbar .session-date { + padding: 0.9em 1em; + color: #999; + text-align: right; + display: block; + position: absolute; + top: -1px; + right: 0; + z-index: 10; +} + +/** + * Menu Section + */ + +#session-sync .session-toolbar .menu-bar { + height: 100%; + padding: 0.5em; + + box-sizing: border-box; + flex: 1; +} + +#session-sync .session-toolbar[session-type='separator'] .menu-bar { + display: none; +} + +#session-sync[session='current'] .session-toolbar .menu-bar { + display: block; +} + +#session-sync .session-toolbar .menu-bar .button { + width: 2.6em; + height: 100%; + + background: url('../images/icons/none.png') no-repeat center; + background-size: contain !important; + + display: none; + float: left; + + /* opacity: 0.75; + filter: grayscale(100%); */ +} + +#session-sync .session-toolbar .menu-bar .button:hover { + cursor: pointer; + opacity: 1; + box-shadow: none; +} + +#session-sync .session-toolbar .menu-bar .separator { + height: 100%; + width: 1px; + margin: 0 0.5em; + background: #ccc; + float: left; + display: none; +} + +#session-sync .session-toolbar .menu-bar .add { + background: url('../images/icons/star.png') no-repeat center; +} + +#session-sync .session-toolbar .menu-bar .save { + background: url('../images/icons/heart.png') no-repeat center; +} + +#session-sync .session-toolbar .menu-bar .merge-sessions { + background: url('../images/icons/merge.png') no-repeat center; +} + +#session-sync .session-toolbar .menu-bar .replace-session { + background: url('../images/icons/recycle.png') no-repeat center; +} + +#session-sync .session-toolbar .menu-bar .restore { + background: url('../images/icons/stack.png') no-repeat center; +} + +#session-sync .session-toolbar .menu-bar .restore-new-win { + background: url('../images/icons/frames.png') no-repeat center; +} + +#session-sync .session-toolbar .menu-bar .repalce-session-all { + background: url('../images/material_icons/repeat.svg') no-repeat center; +} + +#session-sync .session-toolbar .menu-bar .repalce-session { + background: url('../images/material_icons/repeat_one.svg') no-repeat center; +} + +#session-sync .session-toolbar .menu-bar .fix-session { + background: url('../images/icons/tools.png') no-repeat center; +} + +#session-sync .session-toolbar .menu-bar .customize-toolbar { + background: url('../images/icons_v2/more.png') no-repeat center; +} + +/* + * Menu state + */ + +#session-sync[session='current'] .session-toolbar .menu-bar .button[state-current='true'] { + display: block; +} + +#session-sync[session='restore'] .session-toolbar .menu-bar .button[state-restore='true'] { + display: block; +} + +#session-sync[session='restore'] .session-toolbar .menu-bar .separator { + display: block; +} + +#session-sync[session='history'] .session-toolbar .menu-bar .button[state-history='true'] { + display: block; +} + +/* + * Save configuration + */ + +#session-sync .session-toolbar .menu-bar .save-config { + height: 100%; + padding: 0 40px 0 0; + display: block; +} + +#session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch { + opacity: 1; + transition: opacity 0.2s ease-out; +} + +#session-sync .session-toolbar .menu-bar:hover .save-config .comp-toggle-switch { + opacity: 1; +} + +#session-sync[session='restore'] .session-toolbar .menu-bar .save-config { + display: none; +} + +#session-sync[session='history'] .session-toolbar .menu-bar .save-config .comp-toggle-switch[windows] { + display: none; +} + +#session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch { + height: 100%; + width: 4em; + margin: 0; + float: right; +} + +#session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch .label { + display: none; +} + +#session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch .switch { + width: 2.6em; + height: 100%; + background-color: #40b740; + background-image: url('../images/icons/gear.png'); + background-size: 1.6em; + background-repeat: no-repeat; + background-position: center right 0.2em; + border-radius: 2em; + display: block; + + box-sizing: border-box; +} + +#session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch .switch:after { + display: none; +} + +#session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch .switch[state='false'] { + background-color: #bbb; + background-position: center left 0.2em; +} + +#session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch[pin] .switch { + background-image: url('../images/icons/pin.png'); +} + +#session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch[pin-state] .switch { + background-image: url('../images/icons/pin2.png'); +} + +#session-sync .session-toolbar .menu-bar .save-config .comp-toggle-switch[windows] .switch { + background-image: url('../images/icons/stack.png'); +} diff --git a/assets/css/sessions-list.css b/src/assets/css/sessions-list.css similarity index 100% rename from assets/css/sessions-list.css rename to src/assets/css/sessions-list.css diff --git a/assets/css/tooltip-system.css b/src/assets/css/tooltip-system.css similarity index 100% rename from assets/css/tooltip-system.css rename to src/assets/css/tooltip-system.css diff --git a/assets/css/trash-can.css b/src/assets/css/trash-can.css similarity index 96% rename from assets/css/trash-can.css rename to src/assets/css/trash-can.css index ec7dfc2..9c7b363 100644 --- a/assets/css/trash-can.css +++ b/src/assets/css/trash-can.css @@ -6,7 +6,7 @@ width: 8em; height: 8em; - background-image: url('images/trash-can.png'); + background-image: url('../images/trash-can.png'); background-position: bottom 1.5em right 1.5em; background-repeat: no-repeat; background-size: 50%; diff --git a/assets/css/url-bar.css b/src/assets/css/url-bar.css similarity index 100% rename from assets/css/url-bar.css rename to src/assets/css/url-bar.css diff --git a/src/assets/session-sync.css b/src/assets/session-sync.css new file mode 100644 index 0000000..80eba37 --- /dev/null +++ b/src/assets/session-sync.css @@ -0,0 +1,19 @@ +@import './css/_style.css'; +@import './css/components.css'; +@import './css/config-panel.css'; +@import './css/confirm-box.css'; +@import './css/context-menu.css'; +@import './css/field-widget.css'; +@import './css/filter-panel.css'; +@import './css/generic.css'; +@import './css/hotkey-manager.css'; +@import './css/notification-system.css'; +@import './css/search-bar.css'; +@import './css/session-container.css'; +@import './css/session-header-bar.css'; +@import './css/session-sorting.css'; +@import './css/session-toolbar.css'; +@import './css/sessions-list.css'; +@import './css/tooltip-system.css'; +@import './css/trash-can.css'; +@import './css/url-bar.css'; diff --git a/src/entrypoints/popup/main.js b/src/entrypoints/popup/main.js deleted file mode 100644 index bccceec..0000000 --- a/src/entrypoints/popup/main.js +++ /dev/null @@ -1,5 +0,0 @@ -import '../../../assets/css/index.css'; - -import { init } from '../../scripts/app.js'; - -init(); diff --git a/src/entrypoints/scripts/session-sync.js b/src/entrypoints/scripts/session-sync.js new file mode 100644 index 0000000..4f93f4a --- /dev/null +++ b/src/entrypoints/scripts/session-sync.js @@ -0,0 +1,2 @@ +import '../../assets/session-sync.css'; +import '../../scripts/app.js'; diff --git a/src/entrypoints/popup/index.html b/src/entrypoints/session-sync.html similarity index 76% rename from src/entrypoints/popup/index.html rename to src/entrypoints/session-sync.html index ca1cea5..2a15b7b 100644 --- a/src/entrypoints/popup/index.html +++ b/src/entrypoints/session-sync.html @@ -8,6 +8,6 @@
- + diff --git a/wxt.config.js b/wxt.config.js index 484ba79..38c0735 100644 --- a/wxt.config.js +++ b/wxt.config.js @@ -15,7 +15,7 @@ export default defineConfig(() => { }, vite: () => ({ optimizeDeps: { - entries: ['src/entrypoints/popup/main.js', 'src/entrypoints/background.js'] + entries: ['src/entrypoints/session-sync.js', 'src/entrypoints/background.js'] } }), browser: 'firefox', @@ -59,7 +59,8 @@ export default defineConfig(() => { 48: 'images/icon48.png', 128: 'images/icon128.png' }, - default_title: 'Session Sync' + default_title: 'Session Sync', + default_popup: 'session-sync.html' }, icons: { 16: 'images/icon16.png', From 91cdb14af58da46a33c72047f79ecb3dfc8fd751 Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Mon, 20 Apr 2026 01:55:11 +0300 Subject: [PATCH 25/32] Refactoring --- src/background/development.js | 25 ----- src/background/init.js | 21 ++++ src/background/main.js | 122 +++++++++------------ src/background/session-management.js | 29 ++++- src/common/services/firefox-containers.js | 2 - src/common/services/tab-groups.js | 4 +- src/entrypoints/background.js | 11 +- src/scripts/app.js | 4 +- src/scripts/components/session-bookmark.js | 28 ++--- wxt.config.js | 17 ++- 10 files changed, 129 insertions(+), 134 deletions(-) delete mode 100644 src/background/development.js create mode 100644 src/background/init.js diff --git a/src/background/development.js b/src/background/development.js deleted file mode 100644 index 5501716..0000000 --- a/src/background/development.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -import { SessionAutoSave } from './session-history'; - -export function initDevelopment() { - // ------------------------------------------------------------------------ - // Init - - try { - if (typeof browser !== 'undefined' && browser.commands && browser.commands.onCommand) { - browser.commands.onCommand.addListener(function (command) { - if (command == 'session-sync-save-history') { - for (var i = 0; i < 10; i++) { - SessionAutoSave.activeSession.update(); - } - } - }); - } - } catch (err) { - console.warn('browser.commands.onCommand.addListener failed in development.js', err); - } - - // ------------------------------------------------------------------------ - // Events -} diff --git a/src/background/init.js b/src/background/init.js new file mode 100644 index 0000000..19b80a3 --- /dev/null +++ b/src/background/init.js @@ -0,0 +1,21 @@ +import { SessionSync } from './main'; +import { SessionManagement } from './session-management'; +import { SessionAutoSave } from './session-history'; + +export function initBackground() { + SessionSync.init(); + SessionManagement.init(); + SessionAutoSave.init(); + + // Delete previous undo events + if (typeof browser !== 'undefined' && browser.storage && browser.storage.local) { + browser.storage.local.set({ 'undo.events': [] }); + browser.storage.local.set({ 'trashcan.hide': true }); + browser.storage.local.set({ 'trashcan.hide-count': 0 }); + browser.storage.local.set({ 'session.active.filter': '' }); + browser.storage.local.set({ 'session.selected': null }); + browser.storage.local.set({ 'session.history.selected': null }); + } + + console.log('Session Sync Background script initialized.'); +} diff --git a/src/background/main.js b/src/background/main.js index 3822e3e..817730d 100644 --- a/src/background/main.js +++ b/src/background/main.js @@ -1,7 +1,6 @@ 'use strict'; export const Commands = { - openPopup: '_execute_browser_action', openTutorial: 'session_sync_tutorial', detachToTab: 'session_sync_detach_tab', detachToWindow: 'session_sync_detach_window', @@ -9,7 +8,7 @@ export const Commands = { openGithub: 'session_sync_open_github' }; -const SessionSync = (function () { +export const SessionSync = (function () { var windowInfoKey = 'style.window.detach'; var activeWindow = null; var defaultWindowProperties = { @@ -17,20 +16,6 @@ const SessionSync = (function () { height: 600 }; - function init() { - try { - if (typeof browser !== 'undefined' && browser.windows && browser.windows.onRemoved) { - browser.windows.onRemoved.addListener(function (windowID) { - if (activeWindow && windowID === activeWindow.id) { - activeWindow = null; - } - }); - } - } catch (err) { - console.warn('browser.windows.onRemoved.addListener failed', err); - } - } - function checkEvent(event) { switch (event) { case Commands.detachToTab: @@ -89,7 +74,7 @@ const SessionSync = (function () { .create({ width: windowInfo.width, height: windowInfo.height, - url: 'popup.html', + url: 'session-sync.html', type: 'panel' }) .then(function (newWindow) { @@ -117,19 +102,65 @@ const SessionSync = (function () { } browser.tabs.create({ - url: 'popup.html' + url: 'session-sync.html' }); }); } + function init() { + try { + if (typeof browser !== 'undefined' && browser.windows && browser.windows.onRemoved) { + browser.windows.onRemoved.addListener(function (windowID) { + if (activeWindow && windowID === activeWindow.id) { + activeWindow = null; + } + }); + } + } catch (err) { + console.warn('browser.windows.onRemoved.addListener failed', err); + } + + try { + if (typeof browser !== 'undefined' && browser.runtime && browser.runtime.onInstalled) { + browser.runtime.onInstalled.addListener(function (startInfo) { + if (startInfo.reason == 'install') { + SessionSync.checkEvent(Commands.openTutorial); + } + }); + } + } catch (err) { + console.warn('browser.runtime.onInstalled.addListener failed', err); + } + + try { + if (typeof browser !== 'undefined' && browser.runtime && browser.runtime.onMessage) { + browser.runtime.onMessage.addListener(function (message) { + if (message && message.event) { + SessionSync.checkEvent(message.event); + } + }); + } + } catch (err) { + console.warn('browser.runtime.onMessage.addListener failed', err); + } + + try { + if (typeof browser !== 'undefined' && browser.commands && browser.commands.onCommand) { + browser.commands.onCommand.addListener(function (command) { + SessionSync.checkEvent(command); + }); + } + } catch (err) { + console.warn('browser.commands.onCommand.addListener failed', err); + } + } + return { checkEvent: checkEvent, init: init }; })(); -export { SessionSync }; - export const WindowUtils = (function () { var windowMargins = { left: -8, @@ -168,54 +199,3 @@ export const WindowUtils = (function () { centerWindow: centerWindow }; })(); - -// ------------------------------------------------------------------------ -// App Init logic (to be called in background entry point) -export function initBackground() { - // Initialize SessionSync internal listeners - SessionSync.init(); - - try { - if (typeof browser !== 'undefined' && browser.runtime && browser.runtime.onInstalled) { - browser.runtime.onInstalled.addListener(function (startInfo) { - if (startInfo.reason == 'install') { - SessionSync.checkEvent(Commands.openTutorial); - } - }); - } - } catch (err) { - console.warn('browser.runtime.onInstalled.addListener failed', err); - } - - try { - if (typeof browser !== 'undefined' && browser.runtime && browser.runtime.onMessage) { - browser.runtime.onMessage.addListener(function (message, sender, sendResponse) { - if (message && message.event) { - SessionSync.checkEvent(message.event); - } - }); - } - } catch (err) { - console.warn('browser.runtime.onMessage.addListener failed', err); - } - - try { - if (typeof browser !== 'undefined' && browser.commands && browser.commands.onCommand) { - browser.commands.onCommand.addListener(function (command) { - SessionSync.checkEvent(command); - }); - } - } catch (err) { - console.warn('browser.commands.onCommand.addListener failed', err); - } - - // Delete previous undo events - if (typeof browser !== 'undefined' && browser.storage && browser.storage.local) { - browser.storage.local.set({ 'undo.events': [] }); - browser.storage.local.set({ 'trashcan.hide': true }); - browser.storage.local.set({ 'trashcan.hide-count': 0 }); - browser.storage.local.set({ 'session.active.filter': '' }); - browser.storage.local.set({ 'session.selected': null }); - browser.storage.local.set({ 'session.history.selected': null }); - } -} diff --git a/src/background/session-management.js b/src/background/session-management.js index 9c2cb9c..5b23f4d 100644 --- a/src/background/session-management.js +++ b/src/background/session-management.js @@ -2,14 +2,13 @@ import { FaviconService } from './favicon-service'; import { BookmarkProperties } from '../common/bookmark-properties'; -import { FirefoxContainers } from '../common/services/firefox-containers'; -import { BrowserTabGroups } from '../common/services/tab-groups'; export const SessionManagement = (function SessionManagement() { var lazyLoadingKey = 'restore.lazy.loading'; var lazyLoading = false; var windowsTabsToKill = []; + const defaultContainer = 'firefox-default'; var reverseOrderKey = 'restore.reverse.order'; var reverseOrder = false; @@ -36,7 +35,7 @@ export const SessionManagement = (function SessionManagement() { } if (info.group) { - BrowserTabGroups.setTabsGroup([tabInfo.id], info.group.title, info.group.color); + setTabsGroup([tabInfo.id], info.group.title, info.group.color); } if (onSuccess) onSuccess(tabInfo); @@ -56,6 +55,26 @@ export const SessionManagement = (function SessionManagement() { tryCreate(info.container != null); } + var createGroup = async function createGroup(tabs, title, color) { + const groupId = await browser.tabs.group({ tabIds: tabs }); + await browser.tabGroups.update(groupId, { title, color }); + }; + + var setTabsGroup = async function setTabsGroup(tabs, title, color) { + const group = await browser.tabGroups.query({ title: title, color: color }); + let groupId = null; + if (group.length > 0) { + await browser.tabs.group({ tabIds: tabs, groupId: group[0].id }); + } else { + groupId = await browser.tabs.group({ tabIds: tabs }); + await browser.tabGroups.update(groupId, { title, color }); + } + + if (groupId) { + browser.tabGroups.update(groupId, { collapsed: false }); + } + }; + var setLazyLoading = function setLazyLoading(state) { if (typeof browser === 'undefined' || !browser.tabs) return; lazyLoading = state; @@ -98,7 +117,7 @@ export const SessionManagement = (function SessionManagement() { } if (info.group) { - BrowserTabGroups.createGroup([tab.id], info.group.title, info.group.color); + createGroup([tab.id], info.group.title, info.group.color); } checkFavicon(tab); @@ -111,7 +130,7 @@ export const SessionManagement = (function SessionManagement() { const tab = await browser.tabs.get(tabs[0].id); if ( - (options.cookieStoreId == undefined && tab.cookieStoreId == FirefoxContainers.defaultContainer) || + (options.cookieStoreId == undefined && tab.cookieStoreId == defaultContainer) || tab.cookieStoreId === options.cookieStoreId ) { delete options.cookieStoreId; diff --git a/src/common/services/firefox-containers.js b/src/common/services/firefox-containers.js index 582cdbd..dd1954c 100644 --- a/src/common/services/firefox-containers.js +++ b/src/common/services/firefox-containers.js @@ -10,7 +10,6 @@ import { DomElem } from '../../scripts/utils/dom'; */ export class FirefoxContainers { - static defaultContainer = 'firefox-default'; static containers = []; static domContainers = {}; @@ -26,7 +25,6 @@ export class FirefoxContainers { static onGot(contexts) { FirefoxContainers.containers = contexts; - console.log('FirefoxContainers.containers', FirefoxContainers.containers); // create dom containers for (const container of contexts) { diff --git a/src/common/services/tab-groups.js b/src/common/services/tab-groups.js index c7420d0..9139d8d 100644 --- a/src/common/services/tab-groups.js +++ b/src/common/services/tab-groups.js @@ -15,6 +15,8 @@ export class BrowserTabGroups { static async initTabGroups() { try { + if (!browser.tabGroups) return; + const groups = await browser.tabGroups.query({}); BrowserTabGroups.onGot(groups); @@ -37,8 +39,6 @@ export class BrowserTabGroups { } static onGot(groups) { - console.log('TabGroupsLoaded', groups); - for (const group of groups) { BrowserTabGroups.groups.set(group.id, group); } diff --git a/src/entrypoints/background.js b/src/entrypoints/background.js index 26bec53..1887a64 100644 --- a/src/entrypoints/background.js +++ b/src/entrypoints/background.js @@ -1,14 +1,5 @@ -import { initBackground } from '../background/main'; -import { initDevelopment } from '../background/development'; -import { SessionManagement } from '../background/session-management'; -import { SessionAutoSave } from '../background/session-history'; +import { initBackground } from '../background/init'; export default defineBackground(() => { - // Initialize the background logic initBackground(); - initDevelopment(); - SessionManagement.init(); - SessionAutoSave.init(); - - console.log('Session Sync Background script initialized.'); }); diff --git a/src/scripts/app.js b/src/scripts/app.js index 6d31cfc..3e946b0 100644 --- a/src/scripts/app.js +++ b/src/scripts/app.js @@ -43,7 +43,7 @@ const listSessions = function listSessions() { }); }; -export const init = function init() { +(function init() { SessionSyncUI(); findSessionSync(function (bookmark) { @@ -57,4 +57,4 @@ export const init = function init() { BrowserTabGroups.initTabGroups(); GlobalEvents.on('update-sessions', listSessions.bind(null, false, false)); -}; +})(); diff --git a/src/scripts/components/session-bookmark.js b/src/scripts/components/session-bookmark.js index 773a272..5cdb693 100644 --- a/src/scripts/components/session-bookmark.js +++ b/src/scripts/components/session-bookmark.js @@ -58,13 +58,6 @@ export class SessionBookmark { propertiesBox.appendChild(pin); } - if (info.properties.container) { - const container = FirefoxContainers.getContainerElement(info.properties.container); - if (container) { - propertiesBox.appendChild(container); - } - } - if (info.properties.group) { const groupTag = BrowserTabGroups.getGroupElement(info.properties.group.title, info.properties.group.color); if (groupTag) { @@ -72,6 +65,13 @@ export class SessionBookmark { } } + if (info.properties.container) { + const container = FirefoxContainers.getContainerElement(info.properties.container); + if (container) { + propertiesBox.appendChild(container); + } + } + box.appendChild(favicon); box.appendChild(title); box.appendChild(propertiesBox); @@ -177,6 +177,13 @@ export class SessionTab { propertiesBox.appendChild(pin); } + if (tab.cookieStoreId) { + const container = FirefoxContainers.getContainerElement(tab.cookieStoreId); + if (container) { + propertiesBox.appendChild(container); + } + } + if (tab.groupId !== undefined && tab.groupId > -1) { const group = BrowserTabGroups.getGroup(tab.groupId); const groupTag = BrowserTabGroups.getGroupElement(group?.title, group?.color); @@ -185,13 +192,6 @@ export class SessionTab { } } - if (tab.cookieStoreId) { - const container = FirefoxContainers.getContainerElement(tab.cookieStoreId); - if (container) { - propertiesBox.appendChild(container); - } - } - box.appendChild(favicon); box.appendChild(title); box.appendChild(propertiesBox); diff --git a/wxt.config.js b/wxt.config.js index 38c0735..54b4278 100644 --- a/wxt.config.js +++ b/wxt.config.js @@ -14,6 +14,18 @@ export default defineConfig(() => { } }, vite: () => ({ + build: { + minify: false, + cssMinify: false, + modulePreload: false, + rollupOptions: { + output: { + entryFileNames: '[name].js', + chunkFileNames: '[name].js', + assetFileNames: 'assets/[name].[ext]' + } + } + }, optimizeDeps: { entries: ['src/entrypoints/session-sync.js', 'src/entrypoints/background.js'] } @@ -28,7 +40,7 @@ export default defineConfig(() => { }, manifest: { name: 'Session Sync', - version: '3.2.0', + version: '3.2.3', author: 'Gabriel Ivanica', description: 'Save sessions as bookmarks and sync them through Firefox Sync (or any other sync engine).\nPowerful session management - organize, edit, change, save, restore, etc\n', @@ -74,8 +86,7 @@ export default defineConfig(() => { _execute_action: { suggested_key: { default: 'Ctrl+Shift+F' - }, - description: 'Toggle Widget' + } }, session_sync_detach_tab: { suggested_key: { From fc39c777f5bd15d044559bcbd0920364f019e74d Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Mon, 20 Apr 2026 01:56:00 +0300 Subject: [PATCH 26/32] Fixed Popup detection - fixes popup open issues --- src/scripts/config.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/scripts/config.js b/src/scripts/config.js index df3cea2..c41e724 100644 --- a/src/scripts/config.js +++ b/src/scripts/config.js @@ -55,7 +55,6 @@ const Config = (function () { var initConfig = true; var localSettings = defaults; var configEvents = new PubSub(); - var isPanelUI = typeof document !== 'undefined' && document.body ? document.body.clientWidth == 0 : false; // ------------------------------------------------------------------------ // API @@ -115,7 +114,10 @@ const Config = (function () { }; var isPanel = function isPanel() { - return isPanelUI; + if (typeof browser === 'undefined' || !browser.extension || !browser.extension.getViews) { + return false; + } + return browser.extension.getViews({ type: 'popup' }).includes(window); }; var devMode = function devMode() { @@ -135,7 +137,6 @@ const Config = (function () { var init = function init() { setupConfig(); - console.log(localSettings); if (isAddonContext()) { browser.commands.getAll().then(function (commands) { From 591ae2ec86e2b465085d46d07936c3889dcf89cb Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Mon, 20 Apr 2026 02:26:33 +0300 Subject: [PATCH 27/32] Refactoring --- .gitignore | 3 ++- .prettierrc | 2 +- README.md | 45 +++++++++++++++++++++++++-------------------- bin/7za.exe | Bin 643584 -> 0 bytes package.json | 4 ++-- 5 files changed, 30 insertions(+), 24 deletions(-) delete mode 100644 bin/7za.exe diff --git a/.gitignore b/.gitignore index ae33555..1e9cfdb 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,8 @@ .wxt .output -/dev/tutorial +/package-lock.json + /dev/experiments /releases /data/session-sync.css diff --git a/.prettierrc b/.prettierrc index cd36e65..d8fc8ea 100644 --- a/.prettierrc +++ b/.prettierrc @@ -2,7 +2,7 @@ "semi": true, "singleQuote": true, "trailingComma": "none", - "printWidth": 160, + "printWidth": 140, "tabWidth": 4, "useTabs": true, "endOfLine": "crlf" diff --git a/README.md b/README.md index a7cb943..01177df 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -SessionSync [![Session Sync](/data/images/icon32.png)](https://addons.mozilla.org/en-US/firefox/addon/session-sync/) -=========== +# SessionSync [![Session Sync](/data/images/icon32.png)](https://addons.mozilla.org/en-US/firefox/addon/session-sync/) ##### Firefox Add-on for storing browser sessions into bookmarks and sync them across multiple devices @@ -10,35 +9,41 @@ SessionSync [![Session Sync](/data/images/icon32.png)](https://addons.mozilla.or Save sessions as bookmarks and sync them using Firefox Sync (or any other sync engine) Session-Sync will automatically detect updates and display updated sessions across multiple machines. Session-Sync provides a rich interface for sessions management - - edit sessions and bookmarks (save, delete, restore, merge, replace, etc) - - order sessions and book using drag-n-drop actions - - use auto-save feature to inspect history sessions - + +- edit sessions and bookmarks (save, delete, restore, merge, replace, etc) +- order sessions and book using drag-n-drop actions +- use auto-save feature to inspect history sessions + [![Session Sync](/data/images/tutorial/session-menu.png)](https://addons.mozilla.org/en-US/firefox/addon/session-sync/) #### Version 3 - WebExtensions - - improved UI support for widget based interaction - - multiple UI/UX improvements - - added optional Tab/Window Detach mode +- improved UI support for widget based interaction +- multiple UI/UX improvements +- added optional Tab/Window Detach mode #### General Features - Advanced UI and Management Features - - improved bookmark management +- improved bookmark management - added support for pinned tabs and other features such as merge, replace, - session auto-saving - history list - session can be saved later as classic bookmark sessions - private windows are ignored by auto-save - - speed improvement and bookmark cache optimizations +- speed improvement and bookmark cache optimizations ##### UI/UX - - resizable UI - - icon set and tooltip system - - advance UI scaling for individual UI areas (session list, bookmark list, session header and toolbar) - - customization panel - - support for touch input - - session sorting by name/position/date - - session filtering - - advanced drag-n-drop support with fluid animations - +- resizable UI +- icon set and tooltip system +- advance UI scaling for individual UI areas (session list, bookmark list, session header and toolbar) +- customization panel +- support for touch input +- session sorting by name/position/date +- session filtering +- advanced drag-n-drop support with fluid animations + +## Development + +```bash +npm run dev:firefox +``` diff --git a/bin/7za.exe b/bin/7za.exe deleted file mode 100644 index 710bda2f9bcfddd37ad2cbcb9e28cc482211ad10..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 643584 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 z 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%0DSGp_hO&^I-kF#Td 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

0rV~gUn*c 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 diff --git a/package.json b/package.json index f3a1b9a..2c5210e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "session-sync", "author": "Gabriel Ivanica", "url": "https://github.com/ReDEnergy/SessionSync", - "version": "3.2.0", + "version": "3.2.3", "description": "Save sessions as bookmarks and sync them through any sync engine.\nManage sessions, edit, save, restore.\nSession-Sync will automatically update sessions across multiple machines", "license": "MPL-2.0", "scripts": { @@ -30,4 +30,4 @@ "prettier": "^3.8.3", "wxt": "^0.20.20" } -} +} \ No newline at end of file From df59619c2ef392cc12f53b40e0a210b83abd9d72 Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Fri, 1 May 2026 02:02:45 +0300 Subject: [PATCH 28/32] Refactoring --- src/background/favicon-service.js | 17 ++++++++--------- src/background/init.js | 4 +++- src/background/session-history.js | 4 ++-- src/background/session-management.js | 14 +++++--------- .../components/session-bookmark-events.js | 8 +++----- src/scripts/components/session-container.js | 2 -- src/scripts/components/session-folder-events.js | 2 +- src/scripts/components/session-sync-model.js | 2 +- src/scripts/config.js | 5 ++--- src/scripts/session-sync-ui.js | 9 +++------ src/scripts/session/bookmarks.js | 11 ++++------- src/scripts/session/history.js | 1 - src/scripts/session/management.js | 2 +- 13 files changed, 33 insertions(+), 48 deletions(-) diff --git a/src/background/favicon-service.js b/src/background/favicon-service.js index cecd92a..84b0ba7 100644 --- a/src/background/favicon-service.js +++ b/src/background/favicon-service.js @@ -76,21 +76,20 @@ const FaviconService = (function FaviconService() { } }; - var initConfig = function initConfig() { + var init = function init() { browser.storage.local.get(serviceStatusKey).then(function (obj) { serviceStatus = obj[serviceStatusKey]; }); - }; - - browser.storage.onChanged.addListener(function onChange(object) { - if (object[serviceStatusKey]) { - serviceStatus = object[serviceStatusKey].newValue; - } - }); - initConfig(); + browser.storage.onChanged.addListener(function onChange(object) { + if (object[serviceStatusKey]) { + serviceStatus = object[serviceStatusKey].newValue; + } + }); + }; return { + init: init, checkTabFaviconSaved: checkTabFaviconSaved }; })(); diff --git a/src/background/init.js b/src/background/init.js index 19b80a3..01e450f 100644 --- a/src/background/init.js +++ b/src/background/init.js @@ -1,8 +1,10 @@ import { SessionSync } from './main'; import { SessionManagement } from './session-management'; import { SessionAutoSave } from './session-history'; +import { FaviconService } from './favicon-service'; export function initBackground() { + FaviconService.init(); SessionSync.init(); SessionManagement.init(); SessionAutoSave.init(); @@ -17,5 +19,5 @@ export function initBackground() { browser.storage.local.set({ 'session.history.selected': null }); } - console.log('Session Sync Background script initialized.'); + console.log('Session Sync -> background script initialized!'); } diff --git a/src/background/session-history.js b/src/background/session-history.js index 461b5e0..96eb9c7 100644 --- a/src/background/session-history.js +++ b/src/background/session-history.js @@ -38,7 +38,7 @@ BrowserSession.prototype.update = async function update() { const groups = await browser.tabGroups.query({}); groups.forEach((g) => groupMap.set(g.id, g)); } catch (e) { - console.log('Error fetching groups in history update', e); + console.error('Error fetching groups in history update', e); } } @@ -85,7 +85,7 @@ BrowserSession.prototype.update = async function update() { browser.storage.local.set({ [storageKey.active]: this }); } } catch (err) { - console.log('Error in BrowserSession.update', err); + console.error('Error in BrowserSession.update', err); } }; diff --git a/src/background/session-management.js b/src/background/session-management.js index 5b23f4d..5e17d0d 100644 --- a/src/background/session-management.js +++ b/src/background/session-management.js @@ -46,7 +46,7 @@ export const SessionManagement = (function SessionManagement() { tryCreate(false); } else { if (onError) onError(err); - else console.log('createTab error:', err); + else console.error('createTab error:', err); } } ); @@ -93,10 +93,7 @@ export const SessionManagement = (function SessionManagement() { }; var openTab = async function openTab(options, info) { - var checkFavicon = options.favicon === true ? FaviconService.checkTabFaviconSaved : () => {}; - delete options.favicon; - - console.log('openTab', options, info); + var checkFavicon = info.favicon === true ? FaviconService.checkTabFaviconSaved : () => {}; switch (info.mode) { case 'newTab': { @@ -183,7 +180,7 @@ export const SessionManagement = (function SessionManagement() { const groupId = await browser.tabs.group({ tabIds, createProperties: { windowId } }); await browser.tabGroups.update(groupId, { title, color }); } catch (e) { - console.log('Error assigning tab group during restore:', e); + console.error('Error assigning tab group during restore:', e); } } } @@ -195,7 +192,6 @@ export const SessionManagement = (function SessionManagement() { bookmarks.forEach(function (bookmark) { const info = BookmarkProperties.parseTitle(bookmark.title); - console.log(info.properties); // Wrap createTab in a Promise so we can await the created tab ID const tabPromise = new Promise((resolve) => { @@ -293,7 +289,7 @@ export const SessionManagement = (function SessionManagement() { } }, function onError() { - console.log('strage error'); + console.error('strage error'); } ); } else { @@ -301,7 +297,7 @@ export const SessionManagement = (function SessionManagement() { } }, function fail() { - console.log('Error getting bookmarks from folder ID: ', folderID); + console.error('Error getting bookmarks from folder ID: ', folderID); } ); }; diff --git a/src/scripts/components/session-bookmark-events.js b/src/scripts/components/session-bookmark-events.js index e9accde..c36d12b 100644 --- a/src/scripts/components/session-bookmark-events.js +++ b/src/scripts/components/session-bookmark-events.js @@ -31,7 +31,7 @@ export function SessionBookmarkEvents(container) { try { return target.getAttribute(attribute); } catch (err) { - console.log(err); + console.error(err); return null; } } @@ -197,8 +197,7 @@ export function SessionBookmarkEvents(container) { var mark = SessionSyncModel.bookmarks[bookmarkID]; BookmarkManager.openBookmark(mark.url, { title: mark.title, - mode: BookmarkManager.getOpenMode(0), - favicon: true + mode: BookmarkManager.getOpenMode(0) }); } } else { @@ -288,8 +287,7 @@ export function SessionBookmarkEvents(container) { var mark = SessionSyncModel.bookmarks[bookmarkID]; BookmarkManager.openBookmark(mark.url, { title: mark.title, - mode: BookmarkManager.getOpenMode(1), - favicon: true + mode: BookmarkManager.getOpenMode(1) }); return; } diff --git a/src/scripts/components/session-container.js b/src/scripts/components/session-container.js index edfd811..d7d50d4 100644 --- a/src/scripts/components/session-container.js +++ b/src/scripts/components/session-container.js @@ -305,8 +305,6 @@ export class SessionContainer { this.selectedHistorySession = sessionInfo; HistoryManager.selectSession(sessionInfo); - console.log('sessionInfo', sessionInfo); - var tabIndex = 0; var windowIndex = 0; diff --git a/src/scripts/components/session-folder-events.js b/src/scripts/components/session-folder-events.js index 0d93f05..91732b7 100644 --- a/src/scripts/components/session-folder-events.js +++ b/src/scripts/components/session-folder-events.js @@ -29,7 +29,7 @@ export function SessionFolderEvents(container) { try { return target.getAttribute(attribute); } catch (err) { - console.log(err); + console.error(err); return null; } } diff --git a/src/scripts/components/session-sync-model.js b/src/scripts/components/session-sync-model.js index f5b1b3b..3f4e422 100644 --- a/src/scripts/components/session-sync-model.js +++ b/src/scripts/components/session-sync-model.js @@ -305,7 +305,7 @@ export const SessionSyncModel = (function SessionSync() { message: 'Success' }); } catch (err) { - console.log(err); + console.error(err); WindowEvents.emitLocal('Notification', { message: 'Error while saving bookmark' }); diff --git a/src/scripts/config.js b/src/scripts/config.js index c41e724..b8bfde3 100644 --- a/src/scripts/config.js +++ b/src/scripts/config.js @@ -92,16 +92,15 @@ const Config = (function () { } }, function onError(error) { - console.log(error); + console.error(error); } ); }; var get = function get(key) { if (localSettings[key] === undefined) { - console.log('[ERROR] Settings key [' + key + '] was not found!'); + console.error('[ERROR] Settings key [' + key + '] was not found!'); } - // console.log(key, localSettings[key]); return localSettings[key]; }; diff --git a/src/scripts/session-sync-ui.js b/src/scripts/session-sync-ui.js index bb18658..3891b39 100644 --- a/src/scripts/session-sync-ui.js +++ b/src/scripts/session-sync-ui.js @@ -316,8 +316,7 @@ export const SessionSyncUI = function SessionSyncUI() { var mark = SessionSyncModel.bookmarks[bookmarkID]; BookmarkManager.openBookmark(mark.url, { title: mark.title, - mode: 'newTab', - favicon: true + mode: 'newTab' }); }); @@ -325,8 +324,7 @@ export const SessionSyncUI = function SessionSyncUI() { var mark = SessionSyncModel.bookmarks[bookmarkID]; BookmarkManager.openBookmark(mark.url, { title: mark.title, - mode: 'activeTab', - favicon: true + mode: 'activeTab' }); }); @@ -334,8 +332,7 @@ export const SessionSyncUI = function SessionSyncUI() { var mark = SessionSyncModel.bookmarks[bookmarkID]; BookmarkManager.openBookmark(mark.url, { title: mark.title, - mode: 'newWindow', - favicon: true + mode: 'newWindow' }); }); diff --git a/src/scripts/session/bookmarks.js b/src/scripts/session/bookmarks.js index 3f27d3d..f843057 100644 --- a/src/scripts/session/bookmarks.js +++ b/src/scripts/session/bookmarks.js @@ -44,7 +44,7 @@ export const BookmarkManager = (function BookmarkManager() { var getFolderBookmarks = function getFolderBookmarks(folderID, callback) { var result = browser.bookmarks.getChildren(folderID); result.then(callback, function fail() { - console.log('Error getting bookmarks from folder ID: ', folderID); + console.error('Error getting bookmarks from folder ID: ', folderID); }); }; @@ -63,7 +63,6 @@ export const BookmarkManager = (function BookmarkManager() { browser.runtime.sendMessage({ event: 'save-favicon', tab: tab }); var title = tab.title; - console.log('tab', tab); if (tab.groupId !== undefined && tab.groupId > -1 && !tab.groupTitle && typeof browser !== 'undefined' && browser.tabGroups) { try { @@ -71,7 +70,7 @@ export const BookmarkManager = (function BookmarkManager() { tab.groupTitle = group.title; tab.groupColor = group.color; } catch (e) { - console.log('Error fetching tab group info', e); + console.error('Error fetching tab group info', e); } } @@ -101,6 +100,8 @@ export const BookmarkManager = (function BookmarkManager() { url: url }; + info.favicon = true; + if (AppConfig.isPanel() == false) { options.active = false; if (info.mode === 'activeTab') { @@ -121,8 +122,6 @@ export const BookmarkManager = (function BookmarkManager() { if (bookmarkInfo.properties.group) { info.group = bookmarkInfo.properties.group; } - - delete options.title; } browser.runtime.sendMessage({ event: 'open-tab', options: options, info: info }); @@ -148,8 +147,6 @@ export const BookmarkManager = (function BookmarkManager() { }; var openHistoryTab = function openHistoryTab(tab, mode) { - console.log('openHistoryTab', tab, mode); - var options = { url: tab.url }; diff --git a/src/scripts/session/history.js b/src/scripts/session/history.js index c67c57a..4fecbbd 100644 --- a/src/scripts/session/history.js +++ b/src/scripts/session/history.js @@ -8,7 +8,6 @@ export const HistoryManager = (function HistoryManager() { var selectSession = function selectSession(sessionInfo) { selectedSessions = sessionInfo; - console.log('selectedSessions', selectedSessions); }; var getSelectedSession = function getSelectedSession() { diff --git a/src/scripts/session/management.js b/src/scripts/session/management.js index 2b0e077..31ae197 100644 --- a/src/scripts/session/management.js +++ b/src/scripts/session/management.js @@ -307,7 +307,7 @@ export const SessionManager = (function SessionManager() { }); }, function onError(msg) { - console.log(msg); + console.error(msg); } ); }; From 98c526c0b80763a7da254be2d481849b32421ea9 Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Fri, 1 May 2026 02:03:27 +0300 Subject: [PATCH 29/32] Fixed bookmark title encoding (empty encoding, removed default container) --- src/background/session-management.js | 3 +-- src/common/bookmark-properties.js | 9 ++++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/background/session-management.js b/src/background/session-management.js index 5e17d0d..1b4e4ba 100644 --- a/src/background/session-management.js +++ b/src/background/session-management.js @@ -8,7 +8,6 @@ export const SessionManagement = (function SessionManagement() { var lazyLoading = false; var windowsTabsToKill = []; - const defaultContainer = 'firefox-default'; var reverseOrderKey = 'restore.reverse.order'; var reverseOrder = false; @@ -127,7 +126,7 @@ export const SessionManagement = (function SessionManagement() { const tab = await browser.tabs.get(tabs[0].id); if ( - (options.cookieStoreId == undefined && tab.cookieStoreId == defaultContainer) || + (options.cookieStoreId == undefined && tab.cookieStoreId == BookmarkProperties.defaultContainer) || tab.cookieStoreId === options.cookieStoreId ) { delete options.cookieStoreId; diff --git a/src/common/bookmark-properties.js b/src/common/bookmark-properties.js index 09d4d41..e59dce4 100644 --- a/src/common/bookmark-properties.js +++ b/src/common/bookmark-properties.js @@ -11,10 +11,12 @@ export class BookmarkProperties { // ------------------------------------------------------------------------ // Public properties + static defaultContainer = 'firefox-default'; + static encode(tab) { var properties = {}; - if (tab.cookieStoreId) { + if (tab.cookieStoreId && tab.cookieStoreId !== BookmarkProperties.defaultContainer) { properties.container = tab.cookieStoreId; } @@ -29,6 +31,11 @@ export class BookmarkProperties { properties.pinned = 1; } + // If properties object is empty, don't add the marker to the title + if (Object.keys(properties).length === 0) { + return ''; + } + return ' [' + JSON.stringify(properties) + ']'; } From 050f0c27414284726fbb42a236554e513db81b48 Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Fri, 1 May 2026 02:04:05 +0300 Subject: [PATCH 30/32] Updated version to 3.2.6 --- package.json | 2 +- wxt.config.js | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 2c5210e..b26918c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "session-sync", "author": "Gabriel Ivanica", "url": "https://github.com/ReDEnergy/SessionSync", - "version": "3.2.3", + "version": "3.2.6", "description": "Save sessions as bookmarks and sync them through any sync engine.\nManage sessions, edit, save, restore.\nSession-Sync will automatically update sessions across multiple machines", "license": "MPL-2.0", "scripts": { diff --git a/wxt.config.js b/wxt.config.js index 54b4278..9981618 100644 --- a/wxt.config.js +++ b/wxt.config.js @@ -40,7 +40,7 @@ export default defineConfig(() => { }, manifest: { name: 'Session Sync', - version: '3.2.3', + version: '3.2.6', author: 'Gabriel Ivanica', description: 'Save sessions as bookmarks and sync them through Firefox Sync (or any other sync engine).\nPowerful session management - organize, edit, change, save, restore, etc\n', @@ -86,7 +86,8 @@ export default defineConfig(() => { _execute_action: { suggested_key: { default: 'Ctrl+Shift+F' - } + }, + description: 'Widget View' }, session_sync_detach_tab: { suggested_key: { @@ -96,7 +97,7 @@ export default defineConfig(() => { }, session_sync_detach_window: { suggested_key: { - default: 'Ctrl+Shift+2' + default: 'Ctrl+Shift+F6' }, description: 'Window View' } From bab24b1b6ddebd6df3621729553a82fb11e19ff0 Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Fri, 5 Jun 2026 00:55:34 +0300 Subject: [PATCH 31/32] Added scroll bar style config option - slim/wide --- src/assets/css/session-container.css | 5 +++-- src/assets/css/sessions-list.css | 6 ++++++ src/scripts/components/config-panel.js | 11 ++++++++--- src/scripts/config.js | 1 + src/scripts/session-sync-ui.js | 8 ++++++++ 5 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/assets/css/session-container.css b/src/assets/css/session-container.css index be32c9c..488041c 100644 --- a/src/assets/css/session-container.css +++ b/src/assets/css/session-container.css @@ -71,8 +71,9 @@ flex: 1; } -#session-sync .session-container .bookmarks:hover { - overflow-y: scroll; +/* When scrollbar is enabled, use standard width to show it */ +#session-sync[scrollbar="wide"] .session-container .bookmarks { + width: 100%; } #session-sync .session-container .window-separator { diff --git a/src/assets/css/sessions-list.css b/src/assets/css/sessions-list.css index 04f6655..d3a015e 100644 --- a/src/assets/css/sessions-list.css +++ b/src/assets/css/sessions-list.css @@ -75,6 +75,12 @@ flex: 0; } +/* When scrollbar is enabled, use standard width to show it */ +#session-sync[scrollbar="wide"] .sessions .history-list, +#session-sync[scrollbar="wide"] .sessions .session-list { + width: 100%; +} + #session-sync .sessions .history-node { width: 100%; height: 2.5em; diff --git a/src/scripts/components/config-panel.js b/src/scripts/components/config-panel.js index 8d3afbd..1b3800c 100644 --- a/src/scripts/components/config-panel.js +++ b/src/scripts/components/config-panel.js @@ -227,13 +227,18 @@ export function ConfigPanel() { offState: 'Same tab' }); + ToggleOptionConfig({ + parent: section, + name: 'Slim scroll bar', + key: 'scroll.bar.slim', + type: 'switch' + }); + ToggleOptionConfig({ parent: section, type: 'switch', name: 'Context menu icons', - key: 'context.menu.icons', - onState: 'Show', - offState: 'Disabled' + key: 'context.menu.icons' }); ToggleOptionConfig({ diff --git a/src/scripts/config.js b/src/scripts/config.js index b8bfde3..0ee47ea 100644 --- a/src/scripts/config.js +++ b/src/scripts/config.js @@ -44,6 +44,7 @@ const Config = (function () { 'restore.reverse.order': false, 'context.menu.icons': true, + 'scroll.bar.slim': true, 'trashcan.hide': true, 'trashcan.hide-count': 0, 'undo.events': [], diff --git a/src/scripts/session-sync-ui.js b/src/scripts/session-sync-ui.js index 3891b39..9f45227 100644 --- a/src/scripts/session-sync-ui.js +++ b/src/scripts/session-sync-ui.js @@ -45,6 +45,7 @@ export const SessionSyncUI = function SessionSyncUI() { var DomElem = HTMLCreator(); document.body.setAttribute('panel', AppConfig.isPanel()); + document.body.setAttribute('scrollbar', AppConfig.get('scroll.bar.slim')); var content = document.getElementById('body'); // ------------------------------------------------------------------------ @@ -420,6 +421,10 @@ export const SessionSyncUI = function SessionSyncUI() { document.body.style.height = height + 'px'; } + function setScrollbarType(value) { + document.body.setAttribute('scrollbar', value ? 'slim' : 'wide'); + } + if (AppConfig.isPanel()) { // Apply immediately from current config (handles Chrome popup where onChange // events from setupConfig fire before these listeners are registered) @@ -452,6 +457,9 @@ export const SessionSyncUI = function SessionSyncUI() { e.stopPropagation(); } + // Handle scrollbar setting + AppConfig.onChange('scroll.bar.slim', setScrollbarType); + document.body.addEventListener('contextmenu', preventDefault); document.body.addEventListener('mousedown', function (e) { // Middle Click From 6a6c032d73839e480fa5c72b09654547c7869f99 Mon Sep 17 00:00:00 2001 From: Gabriel Ivanica Date: Fri, 5 Jun 2026 02:16:59 +0300 Subject: [PATCH 32/32] Updated to version 3.3.0 --- package.json | 2 +- wxt.config.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index b26918c..6167ecc 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "session-sync", "author": "Gabriel Ivanica", "url": "https://github.com/ReDEnergy/SessionSync", - "version": "3.2.6", + "version": "3.3.0", "description": "Save sessions as bookmarks and sync them through any sync engine.\nManage sessions, edit, save, restore.\nSession-Sync will automatically update sessions across multiple machines", "license": "MPL-2.0", "scripts": { diff --git a/wxt.config.js b/wxt.config.js index 9981618..dbc8798 100644 --- a/wxt.config.js +++ b/wxt.config.js @@ -40,7 +40,7 @@ export default defineConfig(() => { }, manifest: { name: 'Session Sync', - version: '3.2.6', + version: '3.3.0', author: 'Gabriel Ivanica', description: 'Save sessions as bookmarks and sync them through Firefox Sync (or any other sync engine).\nPowerful session management - organize, edit, change, save, restore, etc\n',