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 css/newTabPage.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ body:not(.is-ntp) #ntp-content {
height: 100%;
margin: auto;
object-fit: cover;
transition: 0.2s filter, 0.2s transform;
transform: scale(1.03);
}

.ntp-blur #ntp-background {
filter: blur(12px);
}

#ntp-background-controls {
Expand Down
9 changes: 9 additions & 0 deletions css/searchbar.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@
-webkit-user-select: none;
max-height: calc(100% - 125px);
overflow-y: auto;
transition: 0.2s opacity, 0.2s visibility;
opacity: 1;
visibility: visible;
}

#searchbar.searchbar-hidden {
opacity: 0;
visibility: hidden;
pointer-events: none;
}

body:not(.is-ntp) #searchbar,
Expand Down
16 changes: 15 additions & 1 deletion js/browserUI.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,28 @@ function addTab (tabId = tabs.add(), options = {}) {
destroyTab(tabs.getSelected())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there should be a case added to this if statement to destroy the current tab if it equals the NTP URL. Currently, if I close all my tabs, I get a single tab with the NTP; then if I create a new private tab, I get two tabs: one with the NTP, and a second with the private tab. I'd expect that the NTP would close when I create the new tab.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems the desired functionality is already achieved for the New Tab Picture. Are you asking for this behavior to be applied for the custom URL as well?

2026-01-04.20-04-10.mp4

}

// if the tab is a new, blank tab, replace it with the custom new tab page
let isCustomUrl = false
if (!tabs.get(tabId).url && !tabs.get(tabId).private) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might be possible for a site to create a popup with no URL (I'm not sure exactly), in which case we shouldn't show the NTP. Perhaps we could solve that by also checking enterEditMode here, and only showing the NTP if it's true.

const newTabUrl = settings.get('newTabUrl')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this setting isn't a URL, could we rename it to something like newTabType?

if (newTabUrl === 'customUrl') {
const customUrl = settings.get('newTabCustomUrl')
if (customUrl) {
tabs.update(tabId, { url: customUrl })
isCustomUrl = true
}
}
}

tabBar.addTab(tabId)
webviews.add(tabId)

if (!options.openInBackground) {
switchToTab(tabId, {
focusWebview: options.enterEditMode === false
})
if (options.enterEditMode !== false) {
// Don't enter edit mode for custom URLs - just show the website
if (options.enterEditMode !== false && !isCustomUrl) {
tabEditor.show(tabId)
}
} else {
Expand Down
19 changes: 19 additions & 0 deletions js/navbar/tabEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ var urlParser = require('util/urlParser.js')
var keyboardNavigationHelper = require('util/keyboardNavigationHelper.js')
var bookmarkStar = require('navbar/bookmarkStar.js')
var contentBlockingToggle = require('navbar/contentBlockingToggle.js')
var newTabPage = require('newTabPage.js')
var settings = require('util/settings/settings.js')

const tabEditor = {
container: document.getElementById('tab-editor'),
Expand Down Expand Up @@ -45,8 +47,18 @@ const tabEditor = {
if (showSearchbar !== false) {
if (editingValue) {
searchbar.showResults(editingValue, null)
searchbar.el.classList.remove('searchbar-hidden')
} else {
searchbar.showResults('', null)
const newTabUrlMode = settings.get('newTabUrl')
const isNTP = (newTabUrlMode === 'backgroundImage' && currentURL === '') ||
(newTabUrlMode === 'customUrl' && currentURL === settings.get('newTabCustomUrl'))

Comment on lines +55 to +56
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The NTP (New Tab Page) detection logic is incomplete. It only checks for 'backgroundImage' and 'customUrl' modes, but doesn't account for the 'blank' mode. When newTabUrl is set to 'blank' and currentURL is empty, this should also be considered an NTP scenario. The current logic may show the searchbar when it should be hidden in blank mode.

Suggested change
(newTabUrlMode === 'customUrl' && currentURL === settings.get('newTabCustomUrl'))
(newTabUrlMode === 'customUrl' && currentURL === settings.get('newTabCustomUrl')) ||
(newTabUrlMode === 'blank' && currentURL === '')

Copilot uses AI. Check for mistakes.
if (isNTP) {
searchbar.el.classList.add('searchbar-hidden')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could just skip calling searchbar.showResults in this case (perhaps calling searchbar.hide instead if that's necessary), and then we no longer need the hidden class.

} else {
searchbar.el.classList.remove('searchbar-hidden')
}
}
}

Expand Down Expand Up @@ -79,6 +91,8 @@ const tabEditor = {
searchbar.hide()

document.body.classList.remove('is-edit-mode')
document.body.classList.remove('ntp-blur')
searchbar.el.classList.remove('searchbar-hidden')

webviews.hidePlaceholder('editMode')
},
Expand All @@ -94,6 +108,11 @@ const tabEditor = {
keyboardNavigationHelper.addToGroup('searchbar', tabEditor.container)

tabEditor.input.addEventListener('input', function (e) {
if (newTabPage.hasBackground) {
document.body.classList.add('ntp-blur')
}
searchbar.el.classList.remove('searchbar-hidden')

// handles all inputs except for the case where the selection is moved (since we call preventDefault() there)
searchbar.showResults(this.value, {
isDeletion: e.inputType.includes('delete')
Expand Down
59 changes: 45 additions & 14 deletions js/newTabPage.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const path = require('path')
const statistics = require('js/statistics.js')
const settings = require('util/settings/settings.js')
const webviews = require('webviews.js')

const newTabPage = {
background: document.getElementById('ntp-background'),
Expand All @@ -9,6 +11,15 @@ const newTabPage = {
imagePath: path.join(window.globalArgs['user-data-path'], 'newTabBackground'),
blobInstance: null,
reloadBackground: function () {
const newTabUrl = settings.get('newTabUrl')
if (newTabUrl === 'blank') {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the setting has never been modified, settings.get will return undefined, which this also needs to check for (ie since a blank page is the default value, undefined and "blank" should be treated the same way)

newTabPage.background.hidden = true
newTabPage.hasBackground = false
document.body.classList.remove('ntp-has-background')
newTabPage.deleteBackground.hidden = true
return
}

fs.readFile(newTabPage.imagePath, function (err, data) {
if (newTabPage.blobInstance) {
URL.revokeObjectURL(newTabPage.blobInstance)
Expand All @@ -32,32 +43,52 @@ const newTabPage = {
}
})
},
pickImage: async function () {
var filePath = await ipc.invoke('showOpenDialog', {
filters: [
{ name: 'Image files', extensions: ['jpg', 'jpeg', 'png', 'gif', 'webp'] }
]
})

if (!filePath || filePath.length === 0) {
// User cancelled the file picker - reset to blank if no image was previously set
if (!newTabPage.hasBackground) {
settings.set('newTabUrl', 'blank')
}
return
}

await fs.promises.copyFile(filePath[0], newTabPage.imagePath)
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling for the file copy operation. If copying the file fails (e.g., due to permissions, disk space, or invalid file path), the error will be unhandled and may cause unexpected behavior. Consider wrapping this in a try-catch block and handling errors appropriately.

Suggested change
await fs.promises.copyFile(filePath[0], newTabPage.imagePath)
try {
await fs.promises.copyFile(filePath[0], newTabPage.imagePath)
} catch (err) {
console.error('Failed to copy new tab background image:', err)
return
}

Copilot uses AI. Check for mistakes.
settings.set('newTabUrl', 'backgroundImage')
newTabPage.reloadBackground()
},
removeImage: async function () {
await fs.promises.unlink(newTabPage.imagePath)
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling for the file unlink operation. If removing the file fails (e.g., file doesn't exist, permissions issue), the error will be unhandled. Consider wrapping this in a try-catch block to handle errors gracefully.

Suggested change
await fs.promises.unlink(newTabPage.imagePath)
try {
await fs.promises.unlink(newTabPage.imagePath)
} catch (err) {
// Ignore if the file does not exist; log and abort for other errors.
if (err && err.code === 'ENOENT') {
// File is already gone; continue to update settings/UI.
} else {
console.error('Failed to remove new tab background image:', err)
return
}
}

Copilot uses AI. Check for mistakes.
settings.set('newTabUrl', 'blank')
newTabPage.reloadBackground()
},
initialize: function () {
newTabPage.reloadBackground()

newTabPage.picker.addEventListener('click', async function () {
var filePath = await ipc.invoke('showOpenDialog', {
filters: [
{ name: 'Image files', extensions: ['jpg', 'jpeg', 'png', 'gif', 'webp'] }
]
})
newTabPage.picker.addEventListener('click', newTabPage.pickImage)

if (!filePath) {
return
}
newTabPage.deleteBackground.addEventListener('click', newTabPage.removeImage)

await fs.promises.copyFile(filePath[0], newTabPage.imagePath)
newTabPage.reloadBackground()
webviews.bindIPC('uploadNewTabBackground', function () {
newTabPage.pickImage()
})

newTabPage.deleteBackground.addEventListener('click', async function () {
await fs.promises.unlink(newTabPage.imagePath)
newTabPage.reloadBackground()
webviews.bindIPC('removeNewTabBackground', function () {
newTabPage.removeImage()
})

statistics.registerGetter('ntpHasBackground', function () {
return newTabPage.hasBackground
})

settings.listen('newTabUrl', function (value) {
newTabPage.reloadBackground()
})
}
}

Expand Down
8 changes: 8 additions & 0 deletions js/preload/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,12 @@ window.addEventListener('message', function (e) {
if (e.data?.message === 'downloadFile') {
ipc.send('downloadFile', e.data.url)
}

if (e.data?.message === 'uploadNewTabBackground') {
ipc.send('uploadNewTabBackground')
}

if (e.data?.message === 'removeNewTabBackground') {
ipc.send('removeNewTabBackground')
}
})
6 changes: 6 additions & 0 deletions localization/languages/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,12 @@
"settingsStartupCreateTask": "Re-open previous tabs",
"settingsStartupNewTaskandBackground": "Move previous tabs to a background task",
"settingsStartupNewTaskOnly": "Open a new blank task",
"settingsNewTabUrl": "New Tab Page:",
"settingsNewTabUrlBlank": "Blank Page",
"settingsNewTabUrlCustomUrl": "Custom URL",
"settingsNewTabUrlBackgroundImage": "Background Image",
"settingsNewTabUrlUploadImage": "Upload Image",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be useful to also add these as the title attributes of the camera and delete buttons on the new tab page.

"settingsNewTabUrlRemoveImage": "Remove Image",
"settingsNewWindowOptions": "In new windows: ",
"settingsNewWindowCreateTask": "Create a new task",
"settingsNewWindowPickTask": "Show task list",
Expand Down
18 changes: 0 additions & 18 deletions main/menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -257,24 +257,6 @@ function buildAppMenu (options = {}) {
sendIPCToWindow(window, 'zoomOut')
}
},
// Hidden item to enable shortcut on numpad
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like an accidental change, can you revert it?

{
label: l('appMenuZoomIn'),
accelerator: 'CmdOrCtrl+numadd',
click: function (item, window) {
sendIPCToWindow(window, 'zoomIn')
},
visible: false
},
// Hidden item to enable shortcut on numpad
{
label: l('appMenuZoomOut'),
accelerator: 'CmdOrCtrl+numsub',
click: function (item, window) {
sendIPCToWindow(window, 'zoomOut')
},
visible: false
},
{
label: l('appMenuActualSize'),
accelerator: 'CmdOrCtrl+0',
Expand Down
15 changes: 15 additions & 0 deletions pages/settings/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,21 @@ <h3 data-string="settingsAdditionalFeaturesHeading"></h3>
</select>
</div>

<div class="setting-section">
<label for="newTabUrl" data-string="settingsNewTabUrl"></label>
<select id="new-tab-url-dropdown" name="newTabUrl">
<option value="blank" data-string="settingsNewTabUrlBlank"></option>
<option value="customUrl" data-string="settingsNewTabUrlCustomUrl"></option>
<option value="backgroundImage" data-string="settingsNewTabUrlBackgroundImage"></option>
</select>
<input
type="text"
id="new-tab-custom-url-input"
style="margin-left: 0.5em; min-width: 325px"
hidden
Comment on lines +162 to +165
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The custom URL input field lacks input validation. Users can enter any text, including invalid URLs, which may cause issues when the browser tries to navigate to them. Consider adding validation to ensure the input is a valid URL, or at least provide user feedback when an invalid URL is entered.

Suggested change
type="text"
id="new-tab-custom-url-input"
style="margin-left: 0.5em; min-width: 325px"
hidden
type="url"
id="new-tab-custom-url-input"
style="margin-left: 0.5em; min-width: 325px"
hidden
placeholder="https://example.com"

Copilot uses AI. Check for mistakes.
/>
</div>

<div class="setting-section">
<label for="newWindowOptions" data-string="settingsNewWindowOptions"></label>
<select id="new-window-options" name="newWindowOptions">
Expand Down
37 changes: 37 additions & 0 deletions pages/settings/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,43 @@ startupSettingInput.addEventListener('change', function() {
settings.set('startupTabOption', parseInt(this.value))
})

/* new tab settings */

var newTabUrlDropdown = document.getElementById('new-tab-url-dropdown')
var newTabCustomUrlInput = document.getElementById('new-tab-custom-url-input')

settings.get('newTabUrl', function (value = 'blank') {
newTabUrlDropdown.value = value
if (value === 'customUrl') {
newTabCustomUrlInput.hidden = false
}
})

settings.listen('newTabUrl', function (value) {
newTabUrlDropdown.value = value
newTabCustomUrlInput.hidden = value !== 'customUrl'
})

settings.get('newTabCustomUrl', function (value = '') {
newTabCustomUrlInput.value = value
})

newTabUrlDropdown.addEventListener('change', function () {
settings.set('newTabUrl', this.value)
newTabCustomUrlInput.hidden = true
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The statement newTabCustomUrlInput.hidden = true is unnecessary here. When the dropdown value changes, the code already handles the visibility of the custom URL input field in the subsequent conditional (lines 271-276). This line sets it to hidden unconditionally, only to potentially set it to false again immediately after, which is redundant.

Suggested change
newTabCustomUrlInput.hidden = true

Copilot uses AI. Check for mistakes.

if (this.value === 'customUrl') {
newTabCustomUrlInput.hidden = false
} else if (this.value === 'backgroundImage') {
// Auto-prompt to pick an image when backgroundImage is selected
postMessage({ message: 'uploadNewTabBackground' })
}
})

newTabCustomUrlInput.addEventListener('input', function () {
settings.set('newTabCustomUrl', this.value)
})

/* new window settings */

var newWindowSettingInput = document.getElementById('new-window-options')
Expand Down