diff --git a/build/scripts.js b/build/scripts.js index fbdcc47d1..f57af5f44 100644 --- a/build/scripts.js +++ b/build/scripts.js @@ -14,6 +14,7 @@ const TS_CONFIG = getTSConfig() const BUNDLES = { 'src/injections/group.ts': true, 'src/injections/url.ts': true, + 'src/injections/tab-preview.ts': true, 'src/popup.tab-preview/tab-preview.ts': true, } const IMPORT_RE = /(^|\n|\r\n|;)(im|ex)port\s?((?:\n|.)*?)\sfrom\s"(\.\.?|src|vue)(\/.+?)?"/g @@ -307,6 +308,7 @@ async function main() { 'src/injections/pauseMedia.ts', 'src/injections/group.ts', 'src/injections/url.ts', + 'src/injections/tab-preview.ts', ], tsconfig: 'tsconfig.json', charset: 'utf8', diff --git a/src/_locales/dict.setup-page.ts b/src/_locales/dict.setup-page.ts index b47742367..a59f3d563 100644 --- a/src/_locales/dict.setup-page.ts +++ b/src/_locales/dict.setup-page.ts @@ -2398,11 +2398,20 @@ Beispiele: "*", "ctrl+$", "ctrl+alt+g"`, 'settings.tabs.preview_mode': { en: 'Preview mode', }, - 'settings.tabs.preview_mode_in': { + 'settings.tabs.preview_mode_i': { en: 'in sidebar after the tab', }, - 'settings.tabs.preview_mode_piw': { - en: 'popup in window (experimental)', + 'settings.tabs.preview_mode_p': { + en: 'popup in page (experimental)', + }, + 'settings.tabs.preview_mode_w': { + en: 'window (experimental)', + }, + 'settings.tabs.preview_mode_n': { + en: "don't show preview", + }, + 'settings.tabs.preview_page_mode_fallback': { + en: "Fallback mode (if it's impossible to use the main mode)", }, 'settings.tabs.preview_inline_height': { en: 'Preview height (px)', @@ -2425,11 +2434,14 @@ Beispiele: "*", "ctrl+$", "ctrl+alt+g"`, 'settings.tabs.preview_follow_mouse': { en: 'Follow the mouse cursor', }, - 'settings.tabs.preview_offset_y': { - en: 'Popup vertical offset (px)', + 'settings.tabs.preview_win_offset_y': { + en: 'Window vertical offset (px)', + }, + 'settings.tabs.preview_win_offset_x': { + en: 'Window horizontal offset (px)', }, - 'settings.tabs.preview_offset_x': { - en: 'Popup horizontal offset (px)', + 'settings.tabs.preview_in_page_offset_y': { + en: 'Popup vertical offset in page (px)', }, // - Native tabs diff --git a/src/defaults/settings.ts b/src/defaults/settings.ts index 87d941dbb..60194af22 100644 --- a/src/defaults/settings.ts +++ b/src/defaults/settings.ts @@ -129,14 +129,16 @@ export const DEFAULT_SETTINGS: SettingsState = { // Tabs preview previewTabs: false, - previewTabsMode: 'in', + previewTabsMode: 'i', + previewTabsPageModeFallback: 'w', previewTabsInlineHeight: 70, previewTabsPopupWidth: 280, previewTabsSide: 'right', previewTabsDelay: 500, previewTabsFollowMouse: true, - previewTabsOffsetY: 36, - previewTabsOffsetX: 6, + previewTabsWinOffsetY: 36, + previewTabsWinOffsetX: 6, + previewTabsInPageOffsetY: 0, // Native tabs hideInact: false, @@ -279,7 +281,8 @@ export const SETTINGS_OPTIONS = { tabsUpdateMark: ['all', 'pin', 'norm', 'none'], pinnedTabsPosition: ['panel', 'top', 'left', 'right'], tabsTreeLimit: [1, 2, 3, 4, 5, 'none'], - previewTabsMode: ['in', 'piw'], + previewTabsMode: ['i', 'p', 'w'], + previewTabsPageModeFallback: ['i', 'w', 'n'], previewTabsSide: ['right', 'left'], hideFoldedParent: ['any', 'group', 'none'], rmChildTabs: ['all', 'folded', 'none'], diff --git a/src/injections/tab-preview.ts b/src/injections/tab-preview.ts new file mode 100644 index 000000000..fc5a75c00 --- /dev/null +++ b/src/injections/tab-preview.ts @@ -0,0 +1,348 @@ +import { InstanceType } from 'src/types' +import { NOID } from 'src/defaults' +import * as IPC from 'src/services/ipc' +import * as Logs from 'src/services/logs' + +export interface TabPreviewInitData { + bg: string | undefined | null + fg: string | undefined | null + hbg: string | undefined | null + hfg: string | undefined | null + tabId: ID + winId: ID + title: string + url: string + y: number + dpr: number + popupWidth: number + offsetY: number + atTheLeft: boolean +} + +const MARGIN = 2 +const state = { + tabId: NOID, + winId: NOID, + unloaded: false, + + rootEl: null as HTMLElement | null, + popupEl: null as HTMLElement | null, + titleEl: null as HTMLElement | null, + urlEl: null as HTMLElement | null, + previewEl1: null as HTMLElement | null, + previewEl2: null as HTMLElement | null, + + referenceDevicePixelRatio: 1, + previewWidth: 280, + previewHeight: 250, + popupHeight: 250, + offsetY: 0, + pageWidth: window.innerWidth, + pageHeight: window.innerHeight, + compScale: 1, + minY: MARGIN, + maxY: window.innerHeight - 250, + hidden: false, +} +const previewConf = { + format: 'jpeg' as const, + quality: 90, + scale: window.devicePixelRatio / 2, +} + +function waitInitData(): Promise { + return new Promise((ok, err) => { + if (window.sideberyInitData) return ok() + window.onSideberyInitDataReady = ok + setTimeout(() => err('GroupPage: No initial data (sideberyInitData)'), 2000) + }) +} + +async function updatePreview(tabId: ID, title: string, url: string, unloaded: boolean) { + if (state.titleEl) state.titleEl.innerText = title + if (state.urlEl) state.urlEl.innerText = url + + state.tabId = tabId + state.unloaded = unloaded + + if (!state.unloaded) { + const preview = (await IPC.bg('tabsApiProxy', 'captureTab', tabId, previewConf).catch( + () => '' + )) as string + if (state.tabId === tabId) setPreview(preview) + } else { + setPreview('') + } +} + +let previewElN = 0 +function setPreview(preview: string) { + if (!state.previewEl1 || !state.previewEl2) return + + if (previewElN) { + previewElN = 0 + state.previewEl1.style.setProperty('opacity', '1') + state.previewEl1.style.setProperty('background-image', preview ? `url("${preview}")` : 'none') + state.previewEl2.style.setProperty('opacity', '0') + } else { + previewElN++ + state.previewEl2.style.setProperty('opacity', '1') + state.previewEl2.style.setProperty('background-image', preview ? `url("${preview}")` : 'none') + state.previewEl1.style.setProperty('opacity', '0') + } +} + +function setPopupPosition(y: number) { + if (!state.popupEl) return + let newY = y + state.offsetY + if (newY > state.maxY) newY = state.maxY + else if (newY < state.minY) newY = state.minY + state.popupEl.style.transform = `translateY(${newY}px)` +} + +function show() { + if (!state.rootEl || state.hidden) return + state.rootEl.style.opacity = '1' +} + +function hide() { + if (!state.rootEl) return + state.rootEl.style.opacity = '0' + state.hidden = true + + IPC.disconnectFrom(InstanceType.bg) + IPC.disconnectFrom(InstanceType.sidebar, state.winId) +} + +function compensateZoom() { + if (!state.rootEl) return + if (state.referenceDevicePixelRatio === window.devicePixelRatio) return + state.compScale = state.referenceDevicePixelRatio / window.devicePixelRatio + state.rootEl.style.transform = `scale(${state.compScale})` +} + +function calcPreviewHeight(popupWidth: number) { + const pageWidth = state.pageWidth + const pageHeight = state.pageHeight + + let popupHeight = Math.round((pageHeight / pageWidth) * popupWidth) + if (popupHeight > popupWidth) popupHeight = popupWidth + + return popupHeight +} + +function calcScale(previewWidth: number, previewHeight: number, devicePixelRatio: number) { + const pageWidth = state.pageWidth + const pageHeight = state.pageHeight + + const w = pageWidth / previewWidth + const h = pageHeight / previewHeight + let scale = (devicePixelRatio / Math.min(w, h)) * 1.5 + if (scale > devicePixelRatio) scale = devicePixelRatio + + return scale +} + +function getPopupHeight() { + if (!state.popupEl) return state.previewHeight + return state.popupEl.offsetHeight +} + +function calcPositionRestraints() { + state.minY = MARGIN / state.compScale + state.maxY = (state.pageHeight - MARGIN) / state.compScale - state.popupHeight +} + +async function main() { + // Remove previous container + state.rootEl = document.getElementById('sdbr_preview_root') + if (state.rootEl) state.rootEl.remove() + + // Create new container + state.rootEl = document.createElement('div') + state.rootEl.setAttribute('id', 'sdbr_preview_root') + document.body.appendChild(state.rootEl) + + if (!state.rootEl) return + + await waitInitData() + + const initData = window.sideberyInitData as TabPreviewInitData + + window.sideberyInitData = undefined + window.onSideberyInitDataReady = undefined + + state.winId = initData.winId + state.referenceDevicePixelRatio = initData.dpr + state.previewWidth = initData.popupWidth + state.previewHeight = calcPreviewHeight(initData.popupWidth) + state.offsetY = initData.offsetY + + previewConf.scale = calcScale( + state.previewWidth, + state.previewHeight, + state.referenceDevicePixelRatio + ) + + // Setup IPC + IPC.setInstanceType(InstanceType.preview) + IPC.connectTo(InstanceType.bg) + IPC.connectTo(InstanceType.sidebar, initData.winId) + IPC.registerActions({ updatePreview, setY: setPopupPosition, close: hide }) + + // Create shadow DOM + const shadow = state.rootEl.attachShadow({ mode: 'closed' }) + + // Setup styles for shadow + // meh... Can't use `adoptedStyleSheets` b/c it throws + // an error: "Accessing from Xray wrapper is not supported." + // https://bugzilla.mozilla.org/show_bug.cgi?id=1817675 + // https://bugzilla.mozilla.org/show_bug.cgi?id=1751346 + // const shadowStyles = new CSSStyleSheet() + // shadowStyles.replaceSync(popupStyles) + // shadow.adoptedStyleSheets = [shadowStyles] + + // Set container styles + state.rootEl.style.cssText = ` + --bg: ${initData.bg}; + --fg: ${initData.fg}; + --hbg: ${initData.hbg}; + --hfg: ${initData.hfg}; + position: fixed; + z-index: 999999; + top: 0; + ${initData.atTheLeft ? 'left: 0;' : 'right: 0;'} + height: 100vh; + width: 0; + padding: 0; + margin: 0; + border: none; + pointer-events: none; + opacity: 0; + transition: opacity .1s; + transform-origin: 50% 0%; +` + + // Create popup element + state.popupEl = document.createElement('div') + state.popupEl.classList.add('popup') + shadow.appendChild(state.popupEl) + state.popupEl.style.cssText = ` + position: absolute; + width: ${state.previewWidth}px; + ${initData.atTheLeft ? 'left' : 'right'}: ${MARGIN}px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + border-radius: 8px; + box-shadow: 0 1px 12px 0 #0005; + background-color: var(--bg); + overflow: hidden; + color: var(--fg); + font-family: sans-serif; + transition: background 1s; +` + + // Create header element + const headerEl = document.createElement('div') + headerEl.classList.add('header') + state.popupEl.appendChild(headerEl) + headerEl.style.cssText = ` + position: relative; + flex-shrink: 0; + width: 100%; + background-color: var(--hbg); + color: var(--hfg); + overflow: hidden; +` + + // Create title element + state.titleEl = document.createElement('div') + state.titleEl.classList.add('title') + headerEl.appendChild(state.titleEl) + state.titleEl.style.cssText = ` + position: relative; + margin: 6px 8px; + padding: 0; + font-size: .875rem; + font-weight: 700; + /* line-height: 1.1rem; + max-height: 2.2rem; + overflow: clip; */ + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +` + + // Create url element + state.urlEl = document.createElement('div') + state.urlEl.classList.add('url') + headerEl.appendChild(state.urlEl) + state.urlEl.style.cssText = ` + position: relative; + margin: 4px 8px 8px; + padding: 0; + font-size: .75rem; + font-weight: 400; + /* line-height: 1rem; + max-height: 2rem; + overflow: clip; */ + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + opacity: .75; +` + + // Create preview box element + const previewBoxEl = document.createElement('div') + state.popupEl.appendChild(previewBoxEl) + previewBoxEl.style.cssText = ` + position: relative; + width: 100%; + height: ${state.previewHeight}px; +` + + // Create preview 1 element + state.previewEl1 = document.createElement('div') + previewBoxEl.appendChild(state.previewEl1) + state.previewEl1.style.cssText = ` + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + background-image: var(--preview); + background-repeat: no-repeat; + background-position: 50% 0%; + background-size: cover; + opacity: 0; + transition: opacity .2s; +` + + // Create preview 2 element + state.previewEl2 = document.createElement('div') + previewBoxEl.appendChild(state.previewEl2) + state.previewEl2.style.cssText = ` + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + background-image: var(--preview); + background-repeat: no-repeat; + background-position: 50% 0%; + background-size: cover; + opacity: 0; + transition: opacity .2s; +` + + compensateZoom() + updatePreview(initData.tabId, initData.title, initData.url, false) + state.popupHeight = getPopupHeight() + calcPositionRestraints() + setPopupPosition(initData.y) + setTimeout(() => show(), 50) +} + +main() diff --git a/src/page.setup/components/settings.tabs.vue b/src/page.setup/components/settings.tabs.vue index 92a885f80..e79945258 100644 --- a/src/page.setup/components/settings.tabs.vue +++ b/src/page.setup/components/settings.tabs.vue @@ -321,6 +321,15 @@ section(ref="el") :opts="Settings.getOpts('previewTabsMode')" :folded="true" @update:value="Settings.saveDebounced(150)") + .sub-fields + SelectField( + label="settings.tabs.preview_page_mode_fallback" + optLabel="settings.tabs.preview_mode_" + v-model:value="Settings.state.previewTabsPageModeFallback" + :inactive="!Settings.state.previewTabs || Settings.state.previewTabsMode !== 'p'" + :opts="Settings.getOpts('previewTabsPageModeFallback')" + :folded="true" + @update:value="Settings.saveDebounced(150)") NumField.-inline( label="settings.tabs.preview_delay" v-model:value="Settings.state.previewTabsDelay" @@ -331,37 +340,46 @@ section(ref="el") label="settings.tabs.preview_inline_height" v-model:value="Settings.state.previewTabsInlineHeight" :or="0" - :inactive="!Settings.state.previewTabs || Settings.state.previewTabsMode !== 'in'" + :inactive="!Settings.state.previewTabs || (Settings.state.previewTabsMode !== 'i' && Settings.state.previewTabsPageModeFallback !== 'i')" @update:value="Settings.saveDebounced(500)") NumField.-inline( label="settings.tabs.preview_popup_width" v-model:value="Settings.state.previewTabsPopupWidth" :or="0" - :inactive="!Settings.state.previewTabs || Settings.state.previewTabsMode === 'in'" + :inactive="!Settings.state.previewTabs || Settings.state.previewTabsMode === 'i'" @update:value="Settings.saveDebounced(500)") SelectField( label="settings.tabs.preview_side" optLabel="settings.tabs.preview_side_" v-model:value="Settings.state.previewTabsSide" - :inactive="!Settings.state.previewTabs || Settings.state.previewTabsMode === 'in'" + :inactive="!Settings.state.previewTabs || Settings.state.previewTabsMode === 'i'" :opts="Settings.getOpts('previewTabsSide')" @update:value="Settings.saveDebounced(150)") ToggleField( label="settings.tabs.preview_follow_mouse" v-model:value="Settings.state.previewTabsFollowMouse" - :inactive="!Settings.state.previewTabs || Settings.state.previewTabsMode === 'in'" + :inactive="!Settings.state.previewTabs || Settings.state.previewTabsMode === 'i'" @update:value="Settings.saveDebounced(150)") NumField.-inline( - label="settings.tabs.preview_offset_y" - v-model:value="Settings.state.previewTabsOffsetY" + label="settings.tabs.preview_win_offset_y" + v-model:value="Settings.state.previewTabsWinOffsetY" + :allowNegative="true" + :or="0" + :inactive="!Settings.state.previewTabs || (Settings.state.previewTabsMode !== 'w' && Settings.state.previewTabsPageModeFallback !== 'w')" + @update:value="Settings.saveDebounced(500)") + NumField.-inline( + label="settings.tabs.preview_win_offset_x" + v-model:value="Settings.state.previewTabsWinOffsetX" + :allowNegative="true" :or="0" - :inactive="!Settings.state.previewTabs || Settings.state.previewTabsMode === 'in'" + :inactive="!Settings.state.previewTabs || (Settings.state.previewTabsMode !== 'w' && Settings.state.previewTabsPageModeFallback !== 'w')" @update:value="Settings.saveDebounced(500)") NumField.-inline( - label="settings.tabs.preview_offset_x" - v-model:value="Settings.state.previewTabsOffsetX" + label="settings.tabs.preview_in_page_offset_y" + v-model:value="Settings.state.previewTabsInPageOffsetY" + :allowNegative="true" :or="0" - :inactive="!Settings.state.previewTabs || Settings.state.previewTabsMode === 'in'" + :inactive="!Settings.state.previewTabs || Settings.state.previewTabsMode !== 'p'" @update:value="Settings.saveDebounced(500)") .wrapper diff --git a/src/popup.tab-preview/tab-preview.ts b/src/popup.tab-preview/tab-preview.ts index 2a3a1412f..f4e5be4e2 100644 --- a/src/popup.tab-preview/tab-preview.ts +++ b/src/popup.tab-preview/tab-preview.ts @@ -56,7 +56,7 @@ async function main() { IPC.setInstanceType(InstanceType.preview) IPC.connectTo(InstanceType.sidebar, state.winId) - IPC.registerActions({ updatePreview }) + IPC.registerActions({ updatePreview, setY: () => {}, close: () => {} }) } let previewElN = 0 diff --git a/src/services/info.actions.ts b/src/services/info.actions.ts index 13b793032..cb50d1de7 100644 --- a/src/services/info.actions.ts +++ b/src/services/info.actions.ts @@ -38,6 +38,7 @@ export function setInstanceType(t: InstanceType): void { else if (t === InstanceType.url) Info.isUrl = true else if (t === InstanceType.proxy) Info.isProxy = true else if (t === InstanceType.search) Info.isSearch = true + else if (t === InstanceType.preview) Info.isPreview = true } export function getInstanceName(instance?: InstanceType): string { @@ -48,6 +49,7 @@ export function getInstanceName(instance?: InstanceType): string { else if (instance === InstanceType.proxy) return 'proxy' else if (instance === InstanceType.url) return 'url' else if (instance === InstanceType.search) return 'search' + else if (instance === InstanceType.preview) return 'preview' return 'unknown' } diff --git a/src/services/info.ts b/src/services/info.ts index 06519bdf0..0ec09ed9a 100644 --- a/src/services/info.ts +++ b/src/services/info.ts @@ -21,6 +21,7 @@ export const Info = { isUrl: false, isBg: false, isSearch: false, + isPreview: false, majorVersion: undefined as number | undefined, prevMajorVersion: undefined as number | undefined, diff --git a/src/services/menu.actions.ts b/src/services/menu.actions.ts index 82318a897..c3855864d 100644 --- a/src/services/menu.actions.ts +++ b/src/services/menu.actions.ts @@ -171,7 +171,7 @@ export function open(type: MenuType, x?: number, y?: number, customForced?: bool if (type === MenuType.Tabs) { nodeType = 'tab' blocks = createMenuBlocks(Menu.tabsConf, customForced) - if (Settings.state.previewTabs) Preview.close() + if (Settings.state.previewTabs) Preview.closePreview() } else if (type === MenuType.Bookmarks) { nodeType = 'bookmark' blocks = createMenuBlocks(Menu.bookmarksConf, customForced) diff --git a/src/services/mouse.actions.ts b/src/services/mouse.actions.ts index 901354f18..a32330b7b 100644 --- a/src/services/mouse.actions.ts +++ b/src/services/mouse.actions.ts @@ -85,7 +85,7 @@ export function onMouseMove(e: MouseEvent): void { // Update position of external preview popup if ( Preview.state.status === Preview.Status.Open && - Settings.state.previewTabsMode !== 'in' && + Preview.state.mode !== Preview.Mode.Inline && Settings.state.previewTabsFollowMouse ) { Preview.setPreviewPopupPosition(e.clientY) @@ -117,7 +117,7 @@ export function onMouseMove(e: MouseEvent): void { startY = multiSelectionStartY - activePanel.topOffset + scroll // Close tab preview - if (Settings.state.previewTabs) Preview.close() + if (Settings.state.previewTabs) Preview.closePreview() return } @@ -178,7 +178,7 @@ export function startLongClick( clearTimeout(longClickTimeout) longClickTimeout = setTimeout(() => { // Close tab preview - if (Settings.state.previewTabs) Preview.close() + if (Settings.state.previewTabs) Preview.closePreview() if (DnD.reactive.isStarted) return Mouse.longClickApplied = true diff --git a/src/services/settings.actions.ts b/src/services/settings.actions.ts index cb304cb79..70c2c2af7 100644 --- a/src/services/settings.actions.ts +++ b/src/services/settings.actions.ts @@ -12,6 +12,7 @@ import { Menu } from 'src/services/menu' import { Tabs } from 'src/services/tabs.fg' import { Snapshots } from 'src/services/snapshots' import * as IPC from './ipc' +import * as Preview from 'src/services/tabs.preview' import { updateWebReqHandlers } from './web-req.fg' import { Search } from './search' @@ -137,6 +138,7 @@ export function updateSettingsFg(settings?: SettingsState | null): void { const tabsUrlInTooltip = prev.tabsUrlInTooltip !== next.tabsUrlInTooltip const newTabCtxReopen = prev.newTabCtxReopen !== next.newTabCtxReopen const previewTabs = prev.previewTabs !== next.previewTabs + const previewTabsMode = prev.previewTabsMode !== next.previewTabsMode // Update settings of this instance Utils.updateObject(Settings.state, settings, Settings.state) @@ -147,6 +149,10 @@ export function updateSettingsFg(settings?: SettingsState | null): void { Tabs.list.forEach(t => Tabs.updateTooltip(t.id)) } + if (previewTabsMode) { + Preview.resetMode() + } + if (navTabsPanelMidClickAction || navBookmarksPanelMidClickAction) { Sidebar.updatePanelsTooltips() } diff --git a/src/services/tabs.fg.handlers.ts b/src/services/tabs.fg.handlers.ts index 26757583f..5648dcf46 100644 --- a/src/services/tabs.fg.handlers.ts +++ b/src/services/tabs.fg.handlers.ts @@ -13,6 +13,7 @@ import * as Favicons from 'src/services/favicons.fg' import { DnD } from 'src/services/drag-and-drop' import { Tabs } from './tabs.fg' import * as IPC from './ipc' +import * as Preview from 'src/services/tabs.preview' import { Search } from './search' import { Containers } from './containers' import { Mouse } from './mouse' @@ -1419,4 +1420,9 @@ function onTabActivated(info: browser.tabs.ActiveInfo): void { // Update succession Tabs.updateSuccessionDebounced(10) + + // Reset fallback preview mode + if (Settings.state.previewTabs && Preview.state.modeFallback) { + Preview.resetMode() + } } diff --git a/src/services/tabs.preview.ts b/src/services/tabs.preview.ts index 75e6ae0b5..32aa41663 100644 --- a/src/services/tabs.preview.ts +++ b/src/services/tabs.preview.ts @@ -10,6 +10,7 @@ import * as IPC from './ipc' import { Menu } from './menu' import { Mouse } from './mouse' import { Selection } from './selection' +import { TabPreviewInitData } from 'src/injections/tab-preview' export const enum Status { Closing = -1, @@ -18,8 +19,17 @@ export const enum Status { Open = 2, } +export const enum Mode { + Nope = 0, + Inline = 1, + Window = 2, + InPage = 3, +} + export const state = { status: Status.Closed, + mode: Mode.Nope, + modeFallback: false, popupWinId: NOID, targetTabId: NOID, @@ -45,6 +55,18 @@ let currentWinOffsetX = 0 let deadOnArrival = false let listening = false +function dbgStr() { + let m = state.mode === Mode.Nope ? 'Nope' : 'Inline' + if (state.mode === Mode.Window) m = 'Window' + else if (state.mode === Mode.InPage) m = 'InPage' + + let s = state.status === Status.Closed ? 'Closed' : 'Closing' + if (state.status === Status.Open) s = 'Open' + else if (state.status === Status.Opening) s = 'Opening' + + return `mode: ${m}, status: ${s}` +} + export function setTargetTab(tabId: ID, y: number) { clearTimeout(state.mouseEnterTimeout) if (Settings.state.previewTabsFollowMouse) { @@ -56,7 +78,7 @@ export function setTargetTab(tabId: ID, y: number) { // Start timeout to... if (!Menu.isOpen && !Mouse.multiSelectionMode && !Selection.selected.length) { // Show preview in inline mode - if (Settings.state.previewTabsMode === 'in') { + if (state.mode === Mode.Inline) { state.mouseEnterTimeout = setTimeout(() => { state.mouseEnterTimeout = undefined showPreviewInline(state.targetTabId) @@ -73,11 +95,11 @@ export function setTargetTab(tabId: ID, y: number) { return } - // Show preview in popup mode - if (Windows.focused && state.status === Status.Closed) { + // Show preview + if (state.status === Status.Closed) { state.mouseEnterTimeout = setTimeout(() => { state.mouseEnterTimeout = undefined - showPreviewPopup(state.targetTabId, y) + showPreview(state.targetTabId, y) }, Settings.state.previewTabsDelay) return } @@ -94,22 +116,130 @@ export function resetTargetTab(tabId: ID) { state.mouseEnterTimeout = undefined - if (Settings.state.previewTabsMode !== 'in') { + if (state.mode !== Mode.Inline) { state.mouseLeaveTimeout = setTimeout(() => { closePreviewPopup() }, 36) } } -export function close() { +async function injectTabPreview(tabId: ID, y?: number) { + const activeTab = Tabs.byId[Tabs.activeId] + if (!activeTab) return + + const initData = getTabPreviewInitData(tabId, y) + const initDataJson = JSON.stringify(initData) + const injectingData = browser.tabs + .executeScript(activeTab.id, { + code: `window.sideberyInitData=${initDataJson};window.onSideberyInitDataReady?.();true;`, + runAt: 'document_start', + matchAboutBlank: true, + }) + .catch(() => { + // Cannot inject init data + }) + const injectingScript = browser.tabs + .executeScript(activeTab.id, { + file: '../injections/tab-preview.js', + runAt: 'document_start', + allFrames: false, + matchAboutBlank: true, + }) + .catch(() => { + // Cannot exec script + }) + const [result, _] = await Promise.all([injectingData, injectingScript]) + return result +} + +function getTabPreviewInitData(tabId: ID, y?: number): TabPreviewInitData { + const tab = Tabs.byId[tabId] + + return { + bg: Styles.parsedTheme?.vars.frame_bg, + fg: Styles.parsedTheme?.vars.frame_fg, + hbg: Styles.parsedTheme?.vars.toolbar_bg, + hfg: Styles.parsedTheme?.vars.toolbar_fg, + tabId: tabId, + winId: Windows.id, + title: tab?.title ?? '---', + url: tab?.url ?? '---', + y: y ?? 0, + dpr: window.devicePixelRatio, + popupWidth: Settings.state.previewTabsPopupWidth, + offsetY: Settings.state.previewTabsInPageOffsetY, + atTheLeft: Settings.state.previewTabsSide === 'right', + } +} + +async function showPreview(tabId: ID, y?: number) { + const tab = Tabs.byId[tabId] + if (!tab || tab.invisible || tab.discarded) return + + // Inline + if (state.mode === Mode.Inline) { + return showPreviewInline(tabId) + } + + // In page popup + else if (state.mode === Mode.InPage) { + state.status = Status.Opening + const result = await injectTabPreview(tabId, y) + if (result?.[0]) { + state.status = Status.Open + + if (deadOnArrival || tab.invisible) { + deadOnArrival = false + closePreviewPopup() + } + + return + } + + state.status = Status.Closed + state.modeFallback = true + + if (Settings.state.previewTabsPageModeFallback === 'n') { + state.mode = Mode.Nope + return + } + + if (Settings.state.previewTabsPageModeFallback === 'i') { + state.mode = Mode.Inline + return showPreviewInline(tabId) + } + + if (Windows.focused && Settings.state.previewTabsPageModeFallback === 'w') { + state.mode = Mode.Window + return showPreveiwPopupWindow(tabId, y) + } + } + + // Popup window + else if (Windows.focused && state.mode === Mode.Window) { + return showPreveiwPopupWindow(tabId, y) + } +} + +export function closePreview() { clearTimeout(state.mouseEnterTimeout) clearTimeout(state.mouseLeaveTimeout) - if (Settings.state.previewTabsMode === 'in') closePreviewInline() - else closePreviewPopup() + if (state.mode === Mode.Inline) return closePreviewInline() + else if (state.mode === Mode.InPage || state.mode === Mode.Window) return closePreviewPopup() } -export async function showPreviewPopup(tabId: ID, y?: number) { +export function resetMode() { + if (state.status !== Status.Closed) closePreview() + + if (Settings.state.previewTabsMode === 'i') state.mode = Mode.Inline + else if (Settings.state.previewTabsMode === 'p') state.mode = Mode.InPage + else state.mode = Mode.Window + + state.modeFallback = false +} + +export async function showPreveiwPopupWindow(tabId: ID, y?: number) { const tab = Tabs.byId[tabId] if (!tab || tab.invisible || tab.discarded) return @@ -163,7 +293,7 @@ export async function showPreviewPopup(tabId: ID, y?: number) { if (tab.discarded) previewData.off = 'y' const params = new URLSearchParams(previewData).toString() - const top = (currentWinY ?? 0) + (y ?? 0) + Settings.state.previewTabsOffsetY + const top = (currentWinY ?? 0) + (y ?? 0) + Settings.state.previewTabsWinOffsetY const left = getPopupX() const previewWindow = await browser.windows.create({ allowScriptsToClose: true, @@ -232,9 +362,11 @@ export async function showPreviewPopup(tabId: ID, y?: number) { export function setPreviewPopupPosition(y: number) { if (state.popupWinId !== NOID) { browser.windows.update(state.popupWinId, { - top: currentWinY + y + Settings.state.previewTabsOffsetY + currentWinOffsetY, + top: currentWinY + y + Settings.state.previewTabsWinOffsetY + currentWinOffsetY, left: getPopupX(), }) + } else if (IPC.state.previewConnection) { + IPC.sendToPreview('setY', y) } } @@ -257,9 +389,12 @@ export async function closePreviewPopup() { return } - if (state.popupWinId !== NOID) { + if (state.status === Status.Open) { state.status = Status.Closing - await browser.windows.remove(state.popupWinId) + if (state.popupWinId !== NOID) await browser.windows.remove(state.popupWinId) + else if (Settings.state.previewTabsMode === 'p' && IPC.state.previewConnection) { + IPC.sendToPreview('close') + } state.popupWinId = NOID state.status = Status.Closed } @@ -267,14 +402,14 @@ export async function closePreviewPopup() { function getPopupX() { if (Settings.state.previewTabsSide === 'right') { - return currentWinX + Sidebar.width + Settings.state.previewTabsOffsetX + currentWinOffsetX + return currentWinX + Sidebar.width + Settings.state.previewTabsWinOffsetX + currentWinOffsetX } else { return ( currentWinX + currentWinWidth - Sidebar.width - Settings.state.previewTabsPopupWidth - - Settings.state.previewTabsOffsetX + + Settings.state.previewTabsWinOffsetX + currentWinOffsetX ) } diff --git a/src/sidebar/components/panel.tabs.vue b/src/sidebar/components/panel.tabs.vue index 379814226..727273445 100644 --- a/src/sidebar/components/panel.tabs.vue +++ b/src/sidebar/components/panel.tabs.vue @@ -17,7 +17,7 @@ @mouseenter="onPreviewMouseEnter" @mouseleave="onPreviewMouseLeave") template( - v-if="Settings.state.previewTabs && Settings.state.previewTabsMode === 'in'" + v-if="Settings.state.previewTabs && (Settings.state.previewTabsMode === 'i' || Settings.state.previewTabsPageModeFallback === 'i')" v-for="id in panel.reactive.visibleTabIds" :key="id") TabComponent(:tabId="id") diff --git a/src/sidebar/components/tab.vue b/src/sidebar/components/tab.vue index 880721fce..e89da6445 100644 --- a/src/sidebar/components/tab.vue +++ b/src/sidebar/components/tab.vue @@ -165,8 +165,9 @@ function onMouseDown(e: MouseEvent): void { clearTimeout(Preview.state.mouseEnterTimeout) if ( - Settings.state.previewTabsMode !== 'in' && - Preview.state.status === Preview.Status.Opening + Preview.state.mode === Preview.Mode.InPage || + (Preview.state.mode !== Preview.Mode.Inline && + Preview.state.status === Preview.Status.Opening) ) { Preview.closePreviewPopup() } @@ -392,7 +393,7 @@ function onDragStart(e: DragEvent): void { clearTimeout(Preview.state.mouseEnterTimeout) clearTimeout(Preview.state.mouseLeaveTimeout) - if (Settings.state.previewTabsMode === 'in') Preview.closePreviewInline() + if (Preview.state.mode === Preview.Mode.Inline) Preview.closePreviewInline() else { if (Preview.state.status === Preview.Status.Opening) { e.stopPropagation() @@ -542,7 +543,7 @@ function onExpandMouseDown(): void { clearTimeout(Preview.state.mouseEnterTimeout) if ( - Settings.state.previewTabsMode !== 'in' && + Preview.state.mode !== Preview.Mode.Inline && Preview.state.status === Preview.Status.Opening ) { Preview.closePreviewPopup() diff --git a/src/sidebar/sidebar.ts b/src/sidebar/sidebar.ts index a613d564e..0aa73db07 100644 --- a/src/sidebar/sidebar.ts +++ b/src/sidebar/sidebar.ts @@ -5,6 +5,7 @@ import * as IPC from 'src/services/ipc' import * as Logs from 'src/services/logs' import * as Popups from 'src/services/popups' import * as Favicons from 'src/services/favicons.fg' +import * as Preview from 'src/services/tabs.preview' import { Settings } from 'src/services/settings' import { Sidebar } from 'src/services/sidebar' import { Windows } from 'src/services/windows' @@ -155,6 +156,8 @@ async function main(): Promise { } } + if (Settings.state.previewTabs) Preview.resetMode() + Logs.info(`Init end: ${performance.now() - ts}ms`) } main() diff --git a/src/sidebar/sidebar.vue b/src/sidebar/sidebar.vue index dd592c2f6..198a4a830 100644 --- a/src/sidebar/sidebar.vue +++ b/src/sidebar/sidebar.vue @@ -336,13 +336,7 @@ function onMouseLeave(): void { Preview.state.status === Preview.Status.Open || Preview.state.status === Preview.Status.Opening ) { - if (Settings.state.previewTabsMode === 'in') { - Preview.closePreviewInline() - } else { - clearTimeout(Preview.state.mouseEnterTimeout) - clearTimeout(Preview.state.mouseLeaveTimeout) - Preview.closePreviewPopup() - } + Preview.closePreview() } if (DnD.dragEndedRecently) return diff --git a/src/types/ipc.ts b/src/types/ipc.ts index 1d07d7219..85f215d51 100644 --- a/src/types/ipc.ts +++ b/src/types/ipc.ts @@ -109,6 +109,8 @@ export type SearchPopupActions = { export type PreviewAction = { updatePreview: (tabId: ID, title: string, url: string, unloaded: boolean) => void + setY: (y: number) => void + close: () => void } export type Actions = diff --git a/src/types/settings.ts b/src/types/settings.ts index 017f9391e..0c2d0f25e 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -129,13 +129,15 @@ export interface SettingsState { // Tabs preview previewTabs: boolean previewTabsMode: (typeof SETTINGS_OPTIONS.previewTabsMode)[number] + previewTabsPageModeFallback: (typeof SETTINGS_OPTIONS.previewTabsPageModeFallback)[number] previewTabsInlineHeight: number previewTabsPopupWidth: number previewTabsSide: (typeof SETTINGS_OPTIONS.previewTabsSide)[number] previewTabsDelay: number previewTabsFollowMouse: boolean - previewTabsOffsetY: number - previewTabsOffsetX: number + previewTabsWinOffsetY: number + previewTabsWinOffsetX: number + previewTabsInPageOffsetY: number // Native tabs hideInact: boolean