diff --git a/src/constants.js b/src/constants.js index f45e30fb8211..6a40a323ab56 100644 --- a/src/constants.js +++ b/src/constants.js @@ -5,6 +5,10 @@ const IpcChannels = { OPEN_EXTERNAL_LINK: 'open-external-link', GET_SYSTEM_LOCALE: 'get-system-locale', GET_PICTURES_PATH: 'get-pictures-path', + GET_NAV_HISTORY_ENTRY_TITLE_AT_INDEX: 'get-navigation-history-entry-at-index', + GET_NAV_HISTORY_ACTIVE_INDEX: 'get-navigation-history-active-index', + GET_NAV_HISTORY_LENGTH: 'get-navigation-history-length', + GO_TO_NAV_HISTORY_OFFSET: 'go-to-navigation-history-index', SHOW_OPEN_DIALOG: 'show-open-dialog', SHOW_SAVE_DIALOG: 'show-save-dialog', STOP_POWER_SAVE_BLOCKER: 'stop-power-save-blocker', diff --git a/src/main/index.js b/src/main/index.js index 3e671abccaf5..9cccd635de59 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -888,6 +888,26 @@ function runApp() { session.defaultSession.closeAllConnections() }) + // #region navigation history + + ipcMain.on(IpcChannels.GO_TO_NAV_HISTORY_OFFSET, ({ sender }, offset) => { + sender.navigationHistory.goToOffset(offset) + }) + + ipcMain.handle(IpcChannels.GET_NAV_HISTORY_ENTRY_TITLE_AT_INDEX, async ({ sender }, index) => { + return sender.navigationHistory.getEntryAtIndex(index)?.title + }) + + ipcMain.handle(IpcChannels.GET_NAV_HISTORY_ACTIVE_INDEX, async ({ sender }) => { + return sender.navigationHistory.getActiveIndex() + }) + + ipcMain.handle(IpcChannels.GET_NAV_HISTORY_LENGTH, async ({ sender }) => { + return sender.navigationHistory.length() + }) + + // #endregion navigation history + ipcMain.handle(IpcChannels.OPEN_EXTERNAL_LINK, (_, url) => { if (typeof url === 'string') { let parsedURL diff --git a/src/renderer/App.js b/src/renderer/App.js index 6f29f4e2a7c4..71fa8411fc43 100644 --- a/src/renderer/App.js +++ b/src/renderer/App.js @@ -97,6 +97,7 @@ export default defineComponent({ externalPlayer: function () { return this.$store.getters.getExternalPlayer }, + defaultInvidiousInstance: function () { return this.$store.getters.getDefaultInvidiousInstance }, @@ -142,7 +143,11 @@ export default defineComponent({ externalLinkHandling: function () { return this.$store.getters.getExternalLinkHandling - } + }, + + appTitle: function () { + return this.$store.getters.getAppTitle + }, }, watch: { windowTitle: 'setWindowTitle', @@ -154,10 +159,11 @@ export default defineComponent({ secColor: 'checkThemeSettings', locale: 'setLocale', + + appTitle: 'setDocumentTitle' }, created () { this.checkThemeSettings() - this.setWindowTitle() this.setLocale() }, mounted: function () { @@ -203,10 +209,16 @@ export default defineComponent({ if (this.$router.currentRoute.path === '/') { this.$router.replace({ path: this.landingPage }) } + + this.setWindowTitle() }) }) }, methods: { + setDocumentTitle: function(value) { + document.title = value + this.$nextTick(() => this.$refs.topNav?.setActiveNavigationHistoryEntryTitle(value)) + }, checkThemeSettings: function () { const theme = { baseTheme: this.baseTheme || 'dark', @@ -535,7 +547,7 @@ export default defineComponent({ setWindowTitle: function() { if (this.windowTitle !== null) { - document.title = this.windowTitle + this.setAppTitle(this.windowTitle) } }, @@ -558,6 +570,7 @@ export default defineComponent({ 'getExternalPlayerCmdArgumentsData', 'fetchInvidiousInstances', 'fetchInvidiousInstancesFromFile', + 'setAppTitle', 'setRandomCurrentInvidiousInstance', 'setupListenersToSyncWindows', 'updateBaseTheme', diff --git a/src/renderer/components/ft-icon-button/ft-icon-button.js b/src/renderer/components/ft-icon-button/ft-icon-button.js index 6c1209287442..89defc78efe9 100644 --- a/src/renderer/components/ft-icon-button/ft-icon-button.js +++ b/src/renderer/components/ft-icon-button/ft-icon-button.js @@ -2,6 +2,8 @@ import { defineComponent, nextTick } from 'vue' import FtPrompt from '../ft-prompt/ft-prompt.vue' import { sanitizeForHtmlId } from '../../helpers/accessibility' +const LONG_CLICK_BOUNDARY_MS = 500 + export default defineComponent({ name: 'FtIconButton', components: { @@ -55,21 +57,27 @@ export default defineComponent({ dropdownOptions: { // Array of objects with these properties // - type: ('labelValue'|'divider', default to 'labelValue' for less typing) - // - label: String (if type == 'labelValue') - // - value: String (if type == 'labelValue') + // - label: String (if type === 'labelValue') + // - value: String (if type === 'labelValue') + // - (OPTIONAL) active: Number (if type === 'labelValue') type: Array, default: () => { return [] } }, dropdownModalOnMobile: { type: Boolean, default: false + }, + openOnRightOrLongClick: { + type: Boolean, + default: false } }, emits: ['click', 'disabled-click'], data: function () { return { dropdownShown: false, - mouseDownOnIcon: false, + blockLeftClick: false, + longPressTimer: null, useModal: false } }, @@ -91,14 +99,24 @@ export default defineComponent({ this.dropdownShown = false }, - handleIconClick: function () { + handleIconClick: function (e, isRightOrLongClick = false) { if (this.disabled) { this.$emit('disabled-click') return } - if (this.forceDropdown || (this.dropdownOptions.length > 0)) { - this.dropdownShown = !this.dropdownShown + if (this.blockLeftClick) { + return + } + + if (this.longPressTimer != null) { + clearTimeout(this.longPressTimer) + this.longPressTimer = null + } + + if ((!this.openOnRightOrLongClick || (this.openOnRightOrLongClick && isRightOrLongClick)) && + (this.forceDropdown || this.dropdownOptions.length > 0)) { + this.dropdownShown = !this.dropdownShown if (this.dropdownShown && !this.useModal) { // wait until the dropdown is visible // then focus it so we can hide it automatically when it loses focus @@ -111,24 +129,38 @@ export default defineComponent({ } }, - handleIconMouseDown: function () { - if (this.disabled) { return } - if (this.dropdownShown) { - this.mouseDownOnIcon = true + handleIconPointerDown: function (event) { + if (!this.openOnRightOrLongClick) { return } + if (event.button === 2) { // right button click + this.handleIconClick(null, true) + } else if (event.button === 0) { // left button click + this.longPressTimer = setTimeout(() => { + this.handleIconClick(null, true) + + // prevent a long press that ends on the icon button from firing the handleIconClick handler + window.addEventListener('pointerup', this.preventButtonClickAfterLongPress, { once: true }) + window.addEventListener('pointercancel', () => { + window.removeEventListener('pointerup', this.preventButtonClickAfterLongPress) + }, { once: true }) + }, LONG_CLICK_BOUNDARY_MS) } }, + // prevent the handleIconClick handler from firing for an instant + preventButtonClickAfterLongPress: function () { + this.blockLeftClick = true + setTimeout(() => { this.blockLeftClick = false }, 0) + }, + handleDropdownFocusOut: function () { - if (this.mouseDownOnIcon) { - this.mouseDownOnIcon = false - } else if (!this.$refs.dropdown.matches(':focus-within')) { + if (this.dropdownShown && !this.$refs.ftIconButton.matches(':focus-within')) { this.dropdownShown = false } }, handleDropdownEscape: function () { - this.$refs.iconButton.focus() - // handleDropdownFocusOut will hide the dropdown for us + this.dropdownShown = false + this.$refs.ftIconButton.firstElementChild.focus() }, handleDropdownClick: function ({ url, index }) { diff --git a/src/renderer/components/ft-icon-button/ft-icon-button.scss b/src/renderer/components/ft-icon-button/ft-icon-button.scss index c01f70573b49..e1fc416dfbb2 100644 --- a/src/renderer/components/ft-icon-button/ft-icon-button.scss +++ b/src/renderer/components/ft-icon-button/ft-icon-button.scss @@ -4,6 +4,7 @@ flex-flow: row wrap; justify-content: space-evenly; position: relative; + line-height: normal; user-select: none; } @@ -24,12 +25,13 @@ &:not(.disabled) { &:hover, - &:focus-visible { + &:focus-visible, + &.pressed { background-color: var(--side-nav-hover-color); color: var(--side-nav-hover-text-color); } - &:active { + &.active { background-color: var(--side-nav-active-color); color: var(--side-nav-active-text-color); } @@ -38,12 +40,13 @@ &.base-no-default:not(.disabled) { &:hover, - &:focus-visible { + &:focus-visible, + &.pressed { background-color: var(--side-nav-hover-color); color: var(--side-nav-hover-text-color); } - &:active { + &.active { background-color: var(--side-nav-active-color); color: var(--side-nav-active-text-color); } @@ -55,11 +58,12 @@ &:not(.disabled) { &:hover, - &:focus-visible { + &:focus-visible, + &.pressed { background-color: var(--primary-color-hover); } - &:active { + &.active { background-color: var(--primary-color-active); } } @@ -72,11 +76,12 @@ &:not(.disabled) { &:hover, - &:focus-visible { + &:focus-visible, + &.pressed { background-color: var(--accent-color-hover); } - &:active { + &.active { background-color: var(--accent-color-active); } } @@ -88,11 +93,12 @@ &:not(.disabled) { &:hover, - &:focus-visible { + &:focus-visible, + &.pressed { background-color: var(--destructive-hover-color); } - &:active { + &.active { background-color: var(--destructive-active-color); } } @@ -150,6 +156,17 @@ padding: 0; } + :has(.active) { + .checkmarkColumn { + min-inline-size: 12px; + } + + .listItem { + display: flex; + gap: 6px; + } + } + .listItem { cursor: pointer; margin: 0; @@ -159,12 +176,20 @@ white-space: nowrap; &:hover, - &:focus-visible { + &:focus-visible, + &.pressed, + &.active { background-color: var(--side-nav-hover-color); color: var(--side-nav-hover-text-color); transition: background 0.2s ease-in; } + &.active { + font-weight: 600; + display: flex; + gap: 6px; + } + &:active { background-color: var(--side-nav-active-color); color: var(--side-nav-active-text-color); diff --git a/src/renderer/components/ft-icon-button/ft-icon-button.vue b/src/renderer/components/ft-icon-button/ft-icon-button.vue index 4c6af4f0e3d4..f31aa5afdd09 100644 --- a/src/renderer/components/ft-icon-button/ft-icon-button.vue +++ b/src/renderer/components/ft-icon-button/ft-icon-button.vue @@ -1,13 +1,17 @@