Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/_locales/dict.browser.json
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,12 @@
"zh_CN": "编辑标题",
"zh_TW": "編輯標題"
},
"KbToggleBindUrl": {
"en": "Bind/unbind tab URL"
},
"KbReturnToBoundUrl": {
"en": "Return to bound URL"
},
"KbOpenNewTabInCurrPanel": {
"en": "Open a new tab in the active panel",
"de": "Öffne neuen Tab im aktiven Panel",
Expand Down
9 changes: 9 additions & 0 deletions src/_locales/dict.common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1933,6 +1933,15 @@ export const commonTranslations: Translations = {
zh_TW: '取消釘選分頁',
ja: 'ピン留め解除',
},
'menu.tab.bind_url': {
en: 'Bind URL',
},
'menu.tab.unbind_url': {
en: 'Unbind URL',
},
'menu.tab.return_to_bound': {
en: 'Return to bound URL',
},
'menu.tab.duplicate': {
en: 'Duplicate',
de: 'Duplizieren',
Expand Down
6 changes: 6 additions & 0 deletions src/_locales/dict.setup-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5622,6 +5622,12 @@ Przykłady: "*", "ctrl+$", "ctrl+alt+g"`,
zh_TW: '編輯標題',
ja: 'タイトルを編集',
},
'settings.tab_action_toggle_bind_url': {
en: 'bind/unbind URL',
},
'settings.tab_action_return_to_bound': {
en: 'return to bound URL',
},
'settings.tab_action_none': {
en: 'none',
de: 'Nichts',
Expand Down
2 changes: 2 additions & 0 deletions src/defaults/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export const TABS_MENU: MenuConf = [
},
'separator-2',
'pin',
'bind_url',
'return_to_bound',
'duplicate',
'discard',
'copyTabsUrls',
Expand Down
5 changes: 4 additions & 1 deletion src/defaults/mocks.tabs.fg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export class MTab implements Tab {
reopenInContainer?: string | undefined
customTitle?: string | undefined
customColor?: string | undefined
boundUrl?: string | undefined
boundTitle?: string | undefined
moving?: boolean | undefined
previewImg?: string | undefined
removing?: boolean | undefined
Expand Down Expand Up @@ -64,7 +66,8 @@ export class MTab implements Tab {
branchColor: null,
customColor: null,
isGroup: false,
preview: false,
bound: false,
boundAway: false,
}
sessionData?: TabSessionData | undefined
titleEl?: HTMLElement | undefined
Expand Down
10 changes: 5 additions & 5 deletions src/defaults/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,13 +310,13 @@ export const SETTINGS_OPTIONS = {
scrollThroughTabs: ['panel', 'global', 'psp', 'psg', 'none'],
discardFoldedDelayUnit: ['sec', 'min'],
tabDoubleClick: ['reload', 'duplicate', 'dup_child', 'pin', 'mute', 'clear_cookies', 'exp',
'new_after', 'new_child', 'close', 'edit_title', 'none'],
'new_after', 'new_child', 'close', 'edit_title', 'toggle_bind_url', 'return_to_bound', 'none'],
tabLongLeftClick: ['reload', 'discard', 'duplicate', 'dup_child', 'pin', 'mute', 'clear_cookies', 'new_after',
'new_child', 'edit_title', 'none'],
'new_child', 'edit_title', 'toggle_bind_url', 'return_to_bound', 'none'],
tabLongRightClick: ['reload', 'discard', 'duplicate', 'dup_child', 'pin', 'mute', 'clear_cookies', 'new_after',
'new_child', 'edit_title', 'none'],
tabMiddleClick: ['close', 'discard', 'discard_or_close', 'duplicate', 'dup_child', 'none'],
tabPinnedMiddleClick: ['close', 'discard', 'discard_or_close', 'duplicate', 'unpin', 'none'],
'new_child', 'edit_title', 'toggle_bind_url', 'return_to_bound', 'none'],
tabMiddleClick: ['close', 'discard', 'discard_or_close', 'duplicate', 'dup_child', 'toggle_bind_url', 'return_to_bound', 'none'],
tabPinnedMiddleClick: ['close', 'discard', 'discard_or_close', 'duplicate', 'unpin', 'toggle_bind_url', 'return_to_bound', 'none'],
tabMiddleClickModifier: ['discard', 'discard_or_close', 'duplicate', 'dup_child', 'edit_title', 'none'],
tabCloseMiddleClick: ['close', 'discard', 'discard_or_close'],
tabsPanelLeftClickAction: ['prev', 'expand', 'parent', 'tab', 'none'],
Expand Down
6 changes: 6 additions & 0 deletions src/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,12 @@
"edit_title": {
"description": "__MSG_KbEditTitle__"
},
"toggle_bind_url": {
"description": "__MSG_KbToggleBindUrl__"
},
"return_to_bound_url": {
"description": "__MSG_KbReturnToBoundUrl__"
},
"sort_tabs_by_title_asc": {
"description": "__MSG_KbSortTabsByTitleAsc__"
},
Expand Down
2 changes: 2 additions & 0 deletions src/page.setup/components/keybindings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@
KeybindingField(:keybinding="Keybindings.reactive.byName.group_tabs_act")
KeybindingField(:keybinding="Keybindings.reactive.byName.flatten_tabs")
KeybindingField(:keybinding="Keybindings.reactive.byName.edit_title")
KeybindingField(:keybinding="Keybindings.reactive.byName.toggle_bind_url")
KeybindingField(:keybinding="Keybindings.reactive.byName.return_to_bound_url")

section
h2 {{translate('settings.kb_tabs_open')}}
Expand Down
1 change: 1 addition & 0 deletions src/page.setup/components/settings.tabs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,7 @@ import CountField from '../../components/count-field.vue'
import ToggleField from '../../components/toggle-field.vue'
import SelectField from '../../components/select-field.vue'
import NumField from '../../components/num-field.vue'
import TextField from 'src/components/text-field.vue'

const el = ref<HTMLElement | null>(null)
const newTabPosEl = ref<HTMLElement | null>(null)
Expand Down
1 change: 1 addition & 0 deletions src/page.setup/setup.html
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
<inject>svg://src/assets/sync.svg#icon_sync</inject>
<inject>svg://src/assets/private-window.svg#icon_priv_win</inject>
<inject>svg://src/assets/undo.svg#icon_undo</inject>
<inject>svg://src/assets/home.svg#icon_home</inject>
</div>
</body>
</html>
14 changes: 14 additions & 0 deletions src/services/keybindings.fg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,8 @@ function onCmd(name: string): void {
else if (name === 'group_tabs_act') onKeyGroupTabs(true)
else if (name === 'flatten_tabs') onKeyFlattenTabs()
else if (name === 'edit_title') onKeyEditTitle()
else if (name === 'toggle_bind_url') onKeyToggleBindUrl()
else if (name === 'return_to_bound_url') onKeyReturnToBoundUrl()
else if (name === 'sel_child_tabs') onKeySelChildTabs()
else if (name === 'sort_tabs_by_title_asc') onKeySortTabs(SortBy.Title, 1)
else if (name === 'sort_tabs_by_title_des') onKeySortTabs(SortBy.Title, -1)
Expand Down Expand Up @@ -1530,6 +1532,18 @@ function onKeyPinTabs() {
else Tabs.pinTabs(ids)
}

function onKeyToggleBindUrl() {
const ids = Selection.isTabs() ? Selection.ids() : [Tabs.activeId]
if (!ids.length) return
Tabs.toggleBindTabs(ids)
}

function onKeyReturnToBoundUrl() {
const tabId = Selection.isTabs() ? Selection.getFirst() : Tabs.activeId
if (tabId === NOID) return
Tabs.returnToBound(tabId)
}

function onKeyHidePanel() {
let panelId = Selection.getFirst()
if (!Sidebar.panelsById[panelId]) panelId = Sidebar.activePanelId
Expand Down
22 changes: 22 additions & 0 deletions src/services/menu.fg.options.tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,28 @@ export const tabsMenuOptions: Record<string, () => MenuOption | MenuOption[] | u
}
},

bind_url: () => {
const selected = Selection.ids()
const firstTab = Tabs.byId[selected[0]]
if (!firstTab) return
return {
label: translate('menu.tab.' + (firstTab.boundUrl ? 'unbind_url' : 'bind_url')),
icon: 'icon_home',
onClick: firstTab.boundUrl ? () => Tabs.unbindTabs(selected) : () => Tabs.bindTabs(selected),
}
},

return_to_bound: () => {
const selected = Selection.ids()
const firstTab = Tabs.byId[selected[0]]
if (!firstTab?.boundUrl || firstTab.url === firstTab.boundUrl) return
return {
label: translate('menu.tab.return_to_bound'),
icon: 'icon_undo',
onClick: () => Tabs.returnToBound(firstTab.id),
}
},

reload: () => {
return {
label: translate('menu.tab.reload'),
Expand Down
5 changes: 5 additions & 0 deletions src/services/tabs.fg.handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,11 @@ function onTabUpdated(tabId: ID, change: browser.tabs.ChangeInfo, nativeTab: Nat
IPPC.onHashMsg(tab, change.url)
}

// Handle bound tab URL change
if (tab.boundUrl) {
tab.reactive.boundAway = change.url !== tab.boundUrl
}

// Reset favicon (to cached)
if (tab.internal || !Utils.sameStart(change.url, tab.url, 16)) {
change.favIconUrl = Favicons.getFavicon(change.url)
Expand Down
52 changes: 52 additions & 0 deletions src/services/tabs.fg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ export function mutateNativeTabToSideberyTab(nativeTab: T.NativeTab): T.Tab {
branchColor: null,
color: null,
isGroup: tab.isGroup,
bound: false,
boundAway: false,
}
}

Expand Down Expand Up @@ -504,6 +506,12 @@ function restoreTab(
tab.reactive.folded = tab.folded = !!props.folded
if (props.customTitle) tab.customTitle = props.customTitle
if (props.customColor) tab.reactive.customColor = tab.customColor = props.customColor
if (props.boundUrl) {
tab.boundUrl = props.boundUrl
tab.boundTitle = props.boundTitle
tab.reactive.bound = true
tab.reactive.boundAway = tab.url !== props.boundUrl
}
} else {
Logs.warn(`Tabs.restoreTab: no props for: "${tab.id} i${tab.index} url${tab.url}"`)
}
Expand Down Expand Up @@ -730,6 +738,8 @@ export function cacheTabsData(delay = 300): void {
if (tab.cookieStoreId !== D.CONTAINER_ID) info.ctx = tab.cookieStoreId
if (tab.customTitle) info.customTitle = tab.customTitle
if (tab.customColor) info.customColor = tab.customColor
if (tab.boundUrl) info.boundUrl = tab.boundUrl
if (tab.boundTitle) info.boundTitle = tab.boundTitle
data.push(info)
}

Expand Down Expand Up @@ -1240,6 +1250,48 @@ export function repinTabs(tabIds: ID[]): void {
}
}

/**
* Bind tabs to their current URL
*/
export function bindTabs(tabIds: ID[]): void {
for (const tabId of tabIds) {
const tab = Tabs.byId[tabId]
if (!tab) continue
tab.boundUrl = tab.url
tab.boundTitle = tab.customTitle ?? tab.title
tab.reactive.bound = true
tab.reactive.boundAway = false
}
Tabs.cacheTabsData()
}

export function unbindTabs(tabIds: ID[]): void {
for (const tabId of tabIds) {
const tab = Tabs.byId[tabId]
if (!tab) continue
tab.boundUrl = undefined
tab.boundTitle = undefined
tab.reactive.bound = false
tab.reactive.boundAway = false
}
Tabs.cacheTabsData()
}

export function toggleBindTabs(tabIds: ID[]): void {
const firstTab = Tabs.byId[tabIds[0]]
if (!firstTab) return
if (firstTab.boundUrl) unbindTabs(tabIds)
else bindTabs(tabIds)
}

export function returnToBound(tabId: ID): void {
const tab = Tabs.byId[tabId]
if (!tab?.boundUrl) return
browser.tabs.update(tabId, { url: tab.boundUrl }).catch(err => {
Logs.err('Tabs.returnToBound: Cannot navigate to bound URL:', err)
})
}

/**
* Duplicate tabs
*/
Expand Down
22 changes: 21 additions & 1 deletion src/sidebar/components/tab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
:data-colorized="!!tabColor"
:data-unread="tab.reactive.unread"
:data-edit="tab.reactive.customTitleEdit"
:data-bound="tab.reactive.bound"
:data-bound-away="tab.reactive.boundAway"
:title="tab.reactive.tooltip"
:draggable="!tab.reactive.customTitleEdit"
@dragstart="onDragStart"
Expand Down Expand Up @@ -47,6 +49,8 @@
.progress-spinner(v-if="Settings.state.animations")
svg.progress-spinner(v-else): use(href="#icon_hourglass")
.child-count(v-if="tab.reactive.folded && tab.reactive.branchLen") {{tab.reactive.branchLen}}
.bind-mark(v-if="tab.reactive.bound")
svg.bind-mark-icon: use(href="#icon_home")
.audio(
v-if="tab.reactive.mediaAudible || tab.reactive.mediaMuted || tab.reactive.mediaPaused"
@mousedown.stop.prevent="onAudioMouseDown($event, tab)"
Expand Down Expand Up @@ -340,6 +344,12 @@ function onMouseDown(e: MouseEvent): void {
} else if (Settings.state.tabPinnedMiddleClick === 'unpin') {
Tabs.unpinTabs([tab.id])
return
} else if (Settings.state.tabPinnedMiddleClick === 'toggle_bind_url') {
Tabs.toggleBindTabs(selectedTabs)
return
} else if (Settings.state.tabPinnedMiddleClick === 'return_to_bound') {
Tabs.returnToBound(tab.id)
return
}
}

Expand All @@ -357,6 +367,10 @@ function onMouseDown(e: MouseEvent): void {
Tabs.duplicateTabs([tab.id])
} else if (Settings.state.tabMiddleClick === 'dup_child') {
Tabs.duplicateTabs([tab.id], true)
} else if (Settings.state.tabMiddleClick === 'toggle_bind_url') {
Tabs.toggleBindTabs(selectedTabs)
} else if (Settings.state.tabMiddleClick === 'return_to_bound') {
Tabs.returnToBound(tab.id)
}
}
}
Expand Down Expand Up @@ -392,7 +406,11 @@ function longClickFeedback(e: MouseEvent) {
else if (action === 'new_after') Tabs.createTabAfter(tab.id)
else if (action === 'new_child' && !tab.pinned) Tabs.createChildTab(tab.id)
else if (action === 'edit_title' && !tab.pinned) Tabs.editTabTitle([tab.id])
else noop = true
else if (action === 'toggle_bind_url') Tabs.toggleBindTabs([tab.id])
else if (action === 'return_to_bound') {
if (tab.boundUrl && tab.url !== tab.boundUrl) Tabs.returnToBound(tab.id)
else noop = true
} else noop = true

if (!noop) Tabs.triggerFlashAnimation(tab)

Expand Down Expand Up @@ -511,6 +529,8 @@ function onDoubleClick(): void {
if (shouldBeConvertedToGroup()) convertToGroup()
else Tabs.removeTabs([tab.id])
} else if (dc === 'edit_title') Tabs.editTabTitle([tab.id])
else if (dc === 'toggle_bind_url') Tabs.toggleBindTabs([tab.id])
else if (dc === 'return_to_bound') Tabs.returnToBound(tab.id)
}

function onDragStart(e: DragEvent): void {
Expand Down
1 change: 1 addition & 0 deletions src/sidebar/sidebar.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<inject>svg://src/assets/firefox.svg#icon_ff</inject>
<inject>svg://src/assets/firefox-min.svg#icon_ffm</inject>
<inject>svg://src/assets/undo.svg#icon_undo</inject>
<inject>svg://src/assets/home.svg#icon_home</inject>
<inject>svg://src/assets/reload.svg#icon_reload</inject>
<inject>svg://src/assets/star.svg#icon_star</inject>
<inject>svg://src/assets/star-add.svg#icon_star_add</inject>
Expand Down
34 changes: 34 additions & 0 deletions src/styles/sidebar/tab.styl
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@
height: 100%
border-radius: var(--tabs-border-radius)
z-index: 1
pointer-events: none

.Tab:hover .body:before
background-color: var(--frame-el-overlay-hover-bg)
Expand Down Expand Up @@ -682,6 +683,39 @@
background-color: var(--tabs-normal-fg)


// ---
// -- Bind URL tabs
// -

// Marker at the bottom-left corner of the favicon. Shows the "home" icon when
// the tab is on its bound URL and the "undo" icon once it has navigated away.
.Tab .bind-mark
position: absolute
left: -3px
bottom: -3px
width: 11px
height: 11px
display: flex
align-items: center
justify-content: center
border-radius: 50%
background-color: var(--frame-bg)
pointer-events: none
z-index: 12
animation: appearing-animation var(--d-norm)

.Tab .bind-mark-icon
width: 9px
height: 9px
fill: var(--tabs-normal-fg)

.Tab[data-active="true"] .bind-mark-icon
fill: var(--tabs-activated-fg)

// Navigated away from the bound URL — highlight the marker with the accent
.Tab[data-bound-away="true"] .bind-mark-icon
fill: var(--accent)

// ---
// -- Keyframes
// -
Expand Down
Loading