From 94af80a5055300960dd2e4417dcbad9fc1720505 Mon Sep 17 00:00:00 2001 From: brandon wright <33914549+bwrightkc@users.noreply.github.com> Date: Fri, 2 Oct 2026 10:49:53 -0500 Subject: [PATCH] adds recently closed tab to context menu shows 15 most recently closed tabs --- src/_locales/dict.common.ts | 3 + src/defaults/menu.ts | 1 + src/page.setup/components/menu-editor.vue | 1 + src/services/menu.fg.options.tabs.ts | 19 ++++++ src/services/menu.fg.ts | 30 ++++---- src/services/tabs.fg.rm.ts | 59 +++++++++++++++- .../components/sub-panel.closed-tabs.vue | 68 +------------------ 7 files changed, 100 insertions(+), 81 deletions(-) diff --git a/src/_locales/dict.common.ts b/src/_locales/dict.common.ts index 82c4e8676..a7f88d38f 100644 --- a/src/_locales/dict.common.ts +++ b/src/_locales/dict.common.ts @@ -1472,6 +1472,9 @@ export const commonTranslations: Translations = { ja: 'パネルを削除', }, // - Tab + 'menu.tab.recently_closed': { + en: 'Recently closed tabs', + }, 'menu.tab.undo': { en: 'Undo close tab', de: 'Geschlossenen Tab wiederherstellen', diff --git a/src/defaults/menu.ts b/src/defaults/menu.ts index a7aa7915e..fd06a25fa 100644 --- a/src/defaults/menu.ts +++ b/src/defaults/menu.ts @@ -3,6 +3,7 @@ import { MenuConf } from 'src/types' export const TABS_MENU: MenuConf = [ { opts: ['undoRmTab', 'mute', 'reload', 'bookmark'] }, 'separator-1', + 'recentlyClosedTabs', { name: '%menu.tab.move_to_sub_menu_name', opts: ['moveToNewWin', 'moveToWin', 'separator-5', 'moveToPanel', 'moveToNewPanel'], diff --git a/src/page.setup/components/menu-editor.vue b/src/page.setup/components/menu-editor.vue index c78b0a32e..abd904fbf 100644 --- a/src/page.setup/components/menu-editor.vue +++ b/src/page.setup/components/menu-editor.vue @@ -159,6 +159,7 @@ interface MenuEditorGroup { const TABS_MENU_OPTS: Record = { undoRmTab: 'menu.tab.undo', + recentlyClosedTabs: 'menu.tab.recently_closed', pin: 'menu.tab.pin', reload: 'menu.tab.reload', bookmark: 'menu.tab.bookmark', diff --git a/src/services/menu.fg.options.tabs.ts b/src/services/menu.fg.options.tabs.ts index f39b44858..7a4580696 100644 --- a/src/services/menu.fg.options.tabs.ts +++ b/src/services/menu.fg.options.tabs.ts @@ -22,6 +22,25 @@ export const tabsMenuOptions: Record MenuOption | MenuOption[] | u onClick: () => Tabs.undoRmTab(), }), + recentlyClosedTabs: () => { + const sub: MenuOption[] = Tabs.recentlyRemoved.slice(0, 15).map(rmt => ({ + label: rmt.title || rmt.url, + tooltip: rmt.url, + img: rmt.favIconUrl, + icon: rmt.favIconUrl ? undefined : rmt.favPlaceholder?.replace(/^#/, ''), + onClick: () => Tabs.reopenRecentlyRemoved(rmt, false, false), + onAltClick: () => Tabs.reopenRecentlyRemoved(rmt, true, true), + })) + const option: MenuOption = { + label: translate('menu.tab.recently_closed'), + icon: 'icon_undo', + sub, + inactive: sub.length === 0, + } + if (!Settings.state.ctxMenuRenderInact && option.inactive) return + return option + }, + moveToNewWin: () => { return { label: translate(`menu.tab.move_to_new_${Windows.incognito ? 'priv_window' : 'window'}`), diff --git a/src/services/menu.fg.ts b/src/services/menu.fg.ts index bf64d6ff0..3cce79ae5 100644 --- a/src/services/menu.fg.ts +++ b/src/services/menu.fg.ts @@ -163,7 +163,7 @@ export function open(type: MenuType, x?: number, y?: number, customForced?: bool for (const block of blocks) { for (const opt of block.opts) { if (opt.sub && opt.sub.length && opt.label) { - const parentId = createNativeSubMenuOption(opt.label, nodeType) + const parentId = createNativeSubMenuOption(opt, nodeType) for (const subOpt of opt.sub) { createNativeOption(nodeType, subOpt, parentId) } @@ -307,6 +307,16 @@ function getBase64SVGIcon(icon: string, rgbColor: string): string | undefined { } } +function getNativeOptionIcon(option: T.MenuOption): string | undefined { + if (!Settings.state.ctxMenuRenderIcons) return + if (option.img) return option.img + if (option.icon) { + const alpha = option.inactive ? '64' : 'ff' + const rgbColor = option.color ? D.RGB_COLORS[option.color] : '#686868' + alpha + return getBase64SVGIcon(option.icon, rgbColor) + } +} + function createNativeOption( ctx: browser.menus.ContextType, option: T.MenuOption, @@ -318,17 +328,7 @@ function createNativeOption( return } - let icon - if (Settings.state.ctxMenuRenderIcons) { - if (option.img) { - icon = option.img - } else if (option.icon) { - const alpha = option.inactive ? '64' : 'ff' - const rgbColor = option.color ? D.RGB_COLORS[option.color] : '#686868' + alpha - - icon = getBase64SVGIcon(option.icon, rgbColor) - } - } + const icon = getNativeOptionIcon(option) const optProps: browser.menus.CreateProperties = { type: 'normal', @@ -350,14 +350,16 @@ function createNativeOption( browser.menus.create(optProps) } -function createNativeSubMenuOption(title: string, ctx?: browser.menus.ContextType): string { +function createNativeSubMenuOption(option: T.MenuOption, ctx?: browser.menus.ContextType): string { if (!ctx) ctx = 'all' const optProps: browser.menus.CreateProperties = { type: 'normal', contexts: [ctx], viewTypes: ['sidebar'], - title: title, + title: option.label, } + const icon = getNativeOptionIcon(option) + if (icon) optProps.icons = { '16': icon } return browser.menus.create(optProps) } diff --git a/src/services/tabs.fg.rm.ts b/src/services/tabs.fg.rm.ts index ee9439f29..2f245e2ec 100644 --- a/src/services/tabs.fg.rm.ts +++ b/src/services/tabs.fg.rm.ts @@ -1,4 +1,4 @@ -import { ItemInfo, RecentlyClosedTabInfo, Tab } from 'src/types' +import { DstPlaceInfo, ItemInfo, RecentlyClosedTabInfo, Tab } from 'src/types' import { ConfirmationType } from 'src/enums' import { NOID } from 'src/defaults' import { translate } from 'src/dict' @@ -516,3 +516,60 @@ export async function undoRmTab(): Promise { if (session && session.tab?.sessionId) await browser.sessions.restore(session.tab.sessionId) } } + +export function getRecentlyRemovedBranch(rootTab: RecentlyClosedTabInfo): RecentlyClosedTabInfo[] { + const branch: RecentlyClosedTabInfo[] = [rootTab] + + const startIndex = Tabs.recentlyRemoved.findIndex(t => t.id === rootTab.id) + if (startIndex === -1) return branch + + for (let i = startIndex + 1; i < Tabs.recentlyRemoved.length; i++) { + const tab = Tabs.recentlyRemoved[i] + if (!tab) break + if (rootTab.lvl >= tab.lvl) break + branch.push(tab) + } + + return branch +} + +export async function reopenRecentlyRemoved( + targetTab: RecentlyClosedTabInfo, + inactive: boolean, + branch: boolean +): Promise { + if (!targetTab.isParent) branch = false + if (branch) inactive = true + + const tabsBranch = getRecentlyRemovedBranch(targetTab) + const rcTabs = branch ? tabsBranch : [targetTab] + const panelId = Sidebar.activePanelId + const panel = Sidebar.panelsById[panelId] + if (!Utils.isTabsPanel(panel)) return + + const dst: DstPlaceInfo = { + panelId, + discarded: inactive, + index: Tabs.getIndexForNewTab(panel), + parentId: Tabs.getParentForNewTab(panel), + } + + const tabsToOpen: ItemInfo[] = rcTabs.map(rct => ({ + id: rct.id, + title: rct.title, + url: rct.url, + container: rct.containerId, + parentId: rct.parentId, + })) + if (!inactive && tabsToOpen.length) tabsToOpen[0].active = true + + await Tabs.open(tabsToOpen, dst) + + if (rcTabs.length === 1) tabsBranch.forEach(t => t.lvl--) + for (const tab of rcTabs) { + const index = Tabs.recentlyRemoved.findIndex(t => t.id === tab.id) + if (index !== -1) Tabs.recentlyRemoved.splice(index, 1) + } + + Tabs.reactive.recentlyRemovedLen = Tabs.recentlyRemoved.length +} diff --git a/src/sidebar/components/sub-panel.closed-tabs.vue b/src/sidebar/components/sub-panel.closed-tabs.vue index 9a65ffa6f..7611e764e 100644 --- a/src/sidebar/components/sub-panel.closed-tabs.vue +++ b/src/sidebar/components/sub-panel.closed-tabs.vue @@ -177,74 +177,10 @@ function onTabDragStart(e: DragEvent, tab: T.RecentlyClosedTabInfo) { Sidebar.closeSubPanel() } -function getBranch(rootTab: T.RecentlyClosedTabInfo): T.RecentlyClosedTabInfo[] { - const branch: T.RecentlyClosedTabInfo[] = [rootTab] - - const startIndex = Tabs.recentlyRemoved.findIndex(t => t.id === rootTab.id) - if (startIndex === -1) return branch - - for (let i = startIndex + 1; i < Tabs.recentlyRemoved.length; i++) { - const tab = Tabs.recentlyRemoved[i] - if (!tab) break - if (rootTab.lvl >= tab.lvl) break - branch.push(tab) - } - - return branch -} +const getBranch = Tabs.getRecentlyRemovedBranch async function openTabs(targetTab: T.RecentlyClosedTabInfo, inactive: boolean, branch: boolean) { - if (!targetTab.isParent) branch = false - if (branch) inactive = true - - const tabsBranch = getBranch(targetTab) - const rcTabs = branch ? tabsBranch : [targetTab] - const panelId = Sidebar.activePanelId - const panel = Sidebar.panelsById[panelId] - if (!Utils.isTabsPanel(panel)) return - - const dst: T.DstPlaceInfo = { - panelId, - discarded: inactive, - index: Tabs.getIndexForNewTab(panel), - parentId: Tabs.getParentForNewTab(panel), - } - - const tabsToOpen: T.ItemInfo[] = [] - for (const rct of rcTabs) { - tabsToOpen.push({ - id: rct.id, - title: rct.title, - url: rct.url, - container: rct.containerId, - parentId: rct.parentId, - }) - } - if (!inactive && tabsToOpen.length) tabsToOpen[0].active = true - - await Tabs.open(tabsToOpen, dst) - - // Trigger flash animation - // const els = [] - // for (const tab of tabs) { - // const id = `rmt${tab.id}` - // const tabEl = document.getElementById(id) - // if (!tabEl) continue - // tabEl.setAttribute('data-flash', 'true') - // els.push(tabEl) - // } - // await Utils.sleep(500) - // els.forEach(el => el.removeAttribute('data-flash')) - - // Or remove from list - if (rcTabs.length === 1) tabsBranch.forEach(t => t.lvl--) - for (const tab of rcTabs) { - const index = Tabs.recentlyRemoved.findIndex(t => t.id === tab.id) - if (index !== -1) Tabs.recentlyRemoved.splice(index, 1) - } - + await Tabs.reopenRecentlyRemoved(targetTab, inactive, branch) if (!Tabs.recentlyRemoved.length) Sidebar.closeSubPanel() - - Tabs.reactive.recentlyRemovedLen = Tabs.recentlyRemoved.length }