From 22a7b11973eb9a8a2ffd7ba0d81add834db8bf80 Mon Sep 17 00:00:00 2001
From: Lucas McMurtrie <131608268+Chickaboo@users.noreply.github.com>
Date: Sun, 19 Oct 2025 21:30:45 -0500
Subject: [PATCH 1/6] Add customizable homepage
Introduces a new customizable new tab page with clock, greeting, and search bar. Adds settings for enabling/disabling the homepage, choosing between default or custom URL, toggling clock and greeting, and selecting time format. Implements new internal protocols for 'min://newtab' and 'min://blank', updates localization, and refines related CSS and JS for the new tab and settings pages.
---
css/newTabPage.css | 25 +++-
js/webviews.js | 23 ++-
localization/languages/en-US.json | 10 ++
main/minInternalProtocol.js | 14 +-
pages/blank/index.html | 26 ++++
pages/newtab/index.html | 36 ++++-
pages/newtab/newtab.css | 217 +++++++++++++++++++++++++++
pages/newtab/newtab.js | 241 ++++++++++++++++++++++++++++++
pages/settings/index.html | 52 +++++++
pages/settings/settings.js | 103 +++++++++++++
10 files changed, 740 insertions(+), 7 deletions(-)
create mode 100644 pages/blank/index.html
create mode 100644 pages/newtab/newtab.css
create mode 100644 pages/newtab/newtab.js
diff --git a/css/newTabPage.css b/css/newTabPage.css
index 26937cede..e8b449698 100644
--- a/css/newTabPage.css
+++ b/css/newTabPage.css
@@ -1,3 +1,5 @@
+/* New Tab Page Styles */
+
#ntp-content {
width: 100%;
height: 100%;
@@ -11,18 +13,31 @@ body:not(.is-ntp) #ntp-content {
opacity: 0;
}
+#ntp-content iframe {
+ width: 100%;
+ height: 100%;
+ border: none;
+}
+
+/* Fallback styles for non-iframe version */
#ntp-background {
- /*max-height: max(100%, 72vw);*/
+ position: absolute;
+ top: 0;
+ left: 0;
width: 100%;
height: 100%;
margin: auto;
object-fit: cover;
+ z-index: 0;
}
#ntp-background-controls {
position: absolute;
bottom: 1em;
right: 1.25em;
+ z-index: 10;
+ display: flex;
+ gap: 0.5em;
}
#ntp-background-controls button {
@@ -36,6 +51,10 @@ body:not(.is-ntp) #ntp-content {
border-radius: 50%;
opacity: 0.7;
transition: 0.15s;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
.dark-mode #ntp-background-controls button {
@@ -49,6 +68,10 @@ body:not(.is-ntp) #ntp-content {
box-shadow: 0px 3px 6px rgb(0, 0, 0, 0.25);
}
+#ntp-background-controls button:hover {
+ transform: translateY(-2px);
+}
+
#ntp-background-controls:hover #ntp-image-remove {
color: rgb(248, 73, 73) !important;
}
diff --git a/js/webviews.js b/js/webviews.js
index df535a2ab..81d01f899 100644
--- a/js/webviews.js
+++ b/js/webviews.js
@@ -207,9 +207,26 @@ const webviews = {
if (!existingViewId) {
if (tabData.url) {
ipc.send('loadURLInView', { id: tabData.id, url: urlParser.parse(tabData.url) })
- } else if (tabData.private) {
- // workaround for https://github.com/minbrowser/min/issues/872
- ipc.send('loadURLInView', { id: tabData.id, url: urlParser.parse('min://newtab') })
+ } else {
+ // Load new tab page only if homepage is enabled in settings (default: enabled)
+ var homepageEnabled = settings.get('homepageEnabled')
+ if (homepageEnabled === false) {
+ // load a blank page when homepage is disabled
+ ipc.send('loadURLInView', { id: tabData.id, url: urlParser.parse('min://blank') })
+ } else {
+ // Determine which homepage to load (default or custom)
+ var homepageType = settings.get('homepageType') || 'default'
+ var homepageURL = 'min://newtab'
+
+ if (homepageType === 'custom') {
+ var customURL = settings.get('customHomepageURL')
+ if (customURL) {
+ homepageURL = customURL
+ }
+ }
+
+ ipc.send('loadURLInView', { id: tabData.id, url: urlParser.parse(homepageURL) })
+ }
}
}
diff --git a/localization/languages/en-US.json b/localization/languages/en-US.json
index 6fc6a6c53..0d0f5d19a 100644
--- a/localization/languages/en-US.json
+++ b/localization/languages/en-US.json
@@ -159,6 +159,16 @@
"settingsDarkModeAlways": "Always",
"settingsDarkModeSystem": "Follow system theme",
"settingsSiteThemeToggle": "Enable site theme",
+ "settingsHomepageHeading": "Homepage",
+ "settingsHomepage": "Homepage",
+ "settingsHomepageType": "Homepage type:",
+ "settingsHomepageTypeDefault": "Default",
+ "settingsHomepageTypeCustom": "Custom URL",
+ "settingsCustomHomepageURL": "Homepage URL:",
+ "settingsHomepageShowClock": "Show clock",
+ "settingsHomepageShowGreeting": "Show greeting",
+ "settingsHomepageTimeFormat12h": "Standard Time (12-hour)",
+ "settingsHomepageTimeFormat24h": "Military Time (24-hour)",
"settingsAdditionalFeaturesHeading": "Additional Features",
"settingsUserscriptsToggle": "Enable user scripts",
"settingsShowDividerToggle": "Show divider between tabs",
diff --git a/main/minInternalProtocol.js b/main/minInternalProtocol.js
index 13aa3fd99..b02a44942 100644
--- a/main/minInternalProtocol.js
+++ b/main/minInternalProtocol.js
@@ -19,13 +19,25 @@ function registerBundleProtocol (ses) {
pathname = pathname.substring(1)
}
- if (host !== 'app') {
+ if (host !== 'app' && host !== 'newtab' && host !== 'blank') {
return new Response('bad', {
status: 400,
headers: { 'content-type': 'text/html' }
})
}
+ // Handle newtab:// protocol
+ if (host === 'newtab') {
+ const newtabPath = path.resolve(__dirname, '../pages/newtab/index.html')
+ return net.fetch(pathToFileURL(newtabPath).toString())
+ }
+
+ // Handle blank:// protocol
+ if (host === 'blank') {
+ const blankPath = path.resolve(__dirname, '../pages/blank/index.html')
+ return net.fetch(pathToFileURL(blankPath).toString())
+ }
+
// NB, this checks for paths that escape the bundle, e.g.
// app://bundle/../../secret_file.txt
const pathToServe = path.resolve(__dirname, pathname)
diff --git a/pages/blank/index.html b/pages/blank/index.html
new file mode 100644
index 000000000..6bc5f6f2f
--- /dev/null
+++ b/pages/blank/index.html
@@ -0,0 +1,26 @@
+
+
+
+
+ New Tab
+
+
+
+
+
+
diff --git a/pages/newtab/index.html b/pages/newtab/index.html
index 1149581c0..373221144 100644
--- a/pages/newtab/index.html
+++ b/pages/newtab/index.html
@@ -1,5 +1,37 @@
-
-
+
+
+ New Tab
+
+
+
+
+
+
+
+
+
diff --git a/pages/newtab/newtab.css b/pages/newtab/newtab.css
new file mode 100644
index 000000000..b96a436dd
--- /dev/null
+++ b/pages/newtab/newtab.css
@@ -0,0 +1,217 @@
+/* New Tab Page - Minimal & Refined Design */
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+:root {
+ --text-primary: #2c3e50;
+ --text-secondary: #5f6c7b;
+ --search-bg: #ffffff;
+ --search-border: #e1e4e8;
+ --shadow: rgba(0, 0, 0, 0.08);
+ --accent-color: #4285f4;
+}
+
+body,
+html {
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
+ "Helvetica Neue", Arial, "Noto Sans", sans-serif;
+ background-color: #eee;
+ color: var(--text-primary);
+ transition: background-color 0.3s ease, color 0.3s ease;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+body.dark-mode {
+ background-color: rgb(33, 37, 43);
+ color: #e8eaed;
+}
+
+/* Container */
+.ntp-container {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* Main Content */
+.ntp-main-content {
+ position: relative;
+ z-index: 2;
+ width: 90%;
+ max-width: 680px;
+ padding: 2rem;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 3rem;
+ animation: fadeInContent 0.5s ease;
+}
+
+@keyframes fadeInContent {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+/* Header - Time & Greeting */
+.ntp-header {
+ text-align: center;
+ transition: opacity 0.3s ease;
+}
+
+.ntp-time {
+ font-size: 5.5rem;
+ font-weight: 200;
+ color: var(--text-primary);
+ letter-spacing: -0.03em;
+ line-height: 1;
+ margin-bottom: 0.75rem;
+ font-variant-numeric: tabular-nums;
+ transition: color 0.3s ease;
+}
+
+.ntp-greeting {
+ font-size: 1.75rem;
+ font-weight: 300;
+ color: var(--text-secondary);
+ letter-spacing: 0.01em;
+ transition: color 0.3s ease;
+}
+
+body.dark-mode .ntp-time {
+ color: #e8eaed;
+}
+
+body.dark-mode .ntp-greeting {
+ color: #9aa0a6;
+}
+
+/* Search Container */
+.ntp-search-container {
+ width: 100%;
+ max-width: 600px;
+}
+
+.ntp-search-wrapper {
+ position: relative;
+ display: flex;
+ align-items: center;
+ background: var(--search-bg);
+ border: 1px solid var(--search-border);
+ border-radius: 24px;
+ padding: 0.875rem 1.5rem;
+ box-shadow: 0 2px 8px var(--shadow);
+ transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.ntp-search-wrapper:hover {
+ box-shadow: 0 4px 16px var(--shadow);
+ border-color: var(--accent-color);
+}
+
+.ntp-search-wrapper:focus-within {
+ box-shadow: 0 4px 16px var(--shadow);
+ border-color: var(--accent-color);
+ outline: 2px solid rgba(66, 133, 244, 0.2);
+ outline-offset: 2px;
+}
+
+body.dark-mode .ntp-search-wrapper {
+ background: #292d33;
+ border-color: #3c4149;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
+}
+
+body.dark-mode .ntp-search-wrapper:hover,
+body.dark-mode .ntp-search-wrapper:focus-within {
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
+ border-color: var(--accent-color);
+}
+
+.ntp-search-input {
+ flex: 1;
+ border: none;
+ background: transparent;
+ font-size: 1rem;
+ color: var(--text-primary);
+ outline: none;
+ font-weight: 400;
+ letter-spacing: 0.01em;
+}
+
+.ntp-search-input::placeholder {
+ color: var(--text-secondary);
+ opacity: 0.6;
+}
+
+body.dark-mode .ntp-search-input {
+ color: #e8eaed;
+}
+
+body.dark-mode .ntp-search-input::placeholder {
+ color: #9aa0a6;
+ opacity: 0.6;
+}
+
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ .ntp-time {
+ font-size: 4rem;
+ }
+
+ .ntp-greeting {
+ font-size: 1.5rem;
+ }
+
+ .ntp-main-content {
+ gap: 2.5rem;
+ }
+
+ .ntp-search-wrapper {
+ padding: 0.75rem 1.25rem;
+ }
+
+ .ntp-search-input {
+ font-size: 0.9375rem;
+ }
+}
+
+@media (max-width: 480px) {
+ .ntp-time {
+ font-size: 3rem;
+ }
+
+ .ntp-greeting {
+ font-size: 1.25rem;
+ }
+
+ .ntp-main-content {
+ gap: 2rem;
+ padding: 1.5rem;
+ }
+
+ .ntp-search-wrapper {
+ padding: 0.625rem 1rem;
+ }
+
+ .ntp-search-input {
+ font-size: 0.875rem;
+ }
+}
diff --git a/pages/newtab/newtab.js b/pages/newtab/newtab.js
new file mode 100644
index 000000000..fa5da86ed
--- /dev/null
+++ b/pages/newtab/newtab.js
@@ -0,0 +1,241 @@
+/**
+ * New Tab Page
+ * Displays time, greeting, and search functionality
+ */
+
+;(function () {
+ 'use strict'
+
+ // DOM element references
+ const elements = {
+ time: null,
+ greeting: null,
+ searchInput: null
+ }
+
+ // Time update interval reference
+ let updateInterval = null
+
+ /**
+ * Initialize the new tab page
+ */
+ function initialize () {
+ // Cache DOM elements
+ elements.time = document.getElementById('ntp-time')
+ elements.greeting = document.getElementById('ntp-greeting')
+ elements.searchInput = document.getElementById('ntp-search-input')
+
+ // Initialize components
+ initializeSettings()
+ initializeTime()
+ initializeSearch()
+
+ // Start time updates
+ updateTime()
+ updateInterval = setInterval(updateTime, 1000) // Update every second for accuracy
+ }
+
+ /**
+ * Initialize settings for clock and greeting visibility
+ */
+ function initializeSettings () {
+ if (typeof settings === 'undefined') {
+ // If settings aren't available, show both by default
+ if (elements.time) elements.time.style.display = 'block'
+ if (elements.greeting) elements.greeting.style.display = 'block'
+ return
+ }
+
+ // Clock visibility
+ settings.get('homepageShowClock', function (value) {
+ if (elements.time) {
+ elements.time.style.display = (value !== false) ? 'block' : 'none'
+ }
+ })
+
+ settings.listen('homepageShowClock', function (value) {
+ if (elements.time) {
+ elements.time.style.display = value ? 'block' : 'none'
+ }
+ })
+
+ // Greeting visibility
+ settings.get('homepageShowGreeting', function (value) {
+ if (elements.greeting) {
+ elements.greeting.style.display = (value !== false) ? 'block' : 'none'
+ }
+ })
+
+ settings.listen('homepageShowGreeting', function (value) {
+ if (elements.greeting) {
+ elements.greeting.style.display = value ? 'block' : 'none'
+ }
+ })
+
+ // Time format preference
+ settings.listen('homepageTimeFormat', function (value) {
+ updateTime()
+ })
+ }
+
+ /**
+ * Initialize time display
+ */
+ function initializeTime () {
+ if (!elements.time || !elements.greeting) {
+ return
+ }
+ }
+
+ /**
+ * Update the time and greeting display
+ */
+ function updateTime () {
+ if (!elements.time || !elements.greeting) {
+ return
+ }
+
+ const now = new Date()
+ const originalHours = now.getHours()
+ const minutes = now.getMinutes()
+
+ // Get time format setting (default to 24h if not set)
+ let timeFormat = '24h'
+ if (typeof settings !== 'undefined') {
+ settings.get('homepageTimeFormat', function (value) {
+ timeFormat = value || '24h'
+ })
+ }
+
+ // Format time based on preference
+ let formattedTime
+ if (timeFormat === '12h') {
+ // Convert to 12-hour format with AM/PM, do not pad leading hour
+ const ampm = originalHours >= 12 ? 'PM' : 'AM'
+ let displayHour = originalHours % 12
+ displayHour = displayHour ? displayHour : 12 // 0 -> 12
+ formattedTime = String(displayHour) + ':' + String(minutes).padStart(2, '0') + ' ' + ampm
+ } else {
+ // 24-hour format: pad hours to two digits
+ formattedTime = String(originalHours).padStart(2, '0') + ':' + String(minutes).padStart(2, '0')
+ }
+
+ elements.time.textContent = formattedTime
+
+ // Update greeting based on original 24-hour time
+ let greeting = 'Hello'
+ if (originalHours < 12) {
+ greeting = 'Good Morning'
+ } else if (originalHours < 18) {
+ greeting = 'Good Afternoon'
+ } else {
+ greeting = 'Good Evening'
+ }
+ elements.greeting.textContent = greeting
+ }
+
+ /**
+ * Initialize search functionality
+ */
+ function initializeSearch () {
+ if (!elements.searchInput) {
+ return
+ }
+
+ // Handle search submission
+ elements.searchInput.addEventListener('keydown', function (e) {
+ if (e.key === 'Enter') {
+ e.preventDefault()
+ handleSearch()
+ }
+ })
+
+ // Auto-focus search input after a brief delay
+ setTimeout(function () {
+ if (elements.searchInput && document.activeElement !== elements.searchInput) {
+ elements.searchInput.focus()
+ }
+ }, 100)
+ }
+
+ /**
+ * Handle search query submission
+ */
+ function handleSearch () {
+ const query = elements.searchInput.value.trim()
+
+ if (!query) {
+ return
+ }
+
+ // Determine if input is a URL or search query
+ if (isURL(query)) {
+ navigateToURL(query)
+ } else {
+ performSearch(query)
+ }
+ }
+
+ /**
+ * Check if the input appears to be a URL
+ * @param {string} input - The user input to check
+ * @returns {boolean} True if the input looks like a URL
+ */
+ function isURL (input) {
+ // Check for common URL patterns
+ if (input.startsWith('http://') || input.startsWith('https://')) {
+ return true
+ }
+
+ // Check for domain-like patterns (contains dot and no spaces)
+ if (input.includes('.') && !input.includes(' ')) {
+ // Verify it has a valid TLD-like pattern
+ const parts = input.split('.')
+ return parts.length >= 2 && parts[parts.length - 1].length >= 2
+ }
+
+ return false
+ }
+
+ /**
+ * Navigate to a URL
+ * @param {string} url - The URL to navigate to
+ */
+ function navigateToURL (url) {
+ // Add protocol if missing
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
+ url = 'https://' + url
+ }
+
+ window.location.href = url
+ }
+
+ /**
+ * Perform a search using the default search engine
+ * @param {string} query - The search query
+ */
+ function performSearch (query) {
+ const searchURL = 'https://duckduckgo.com/?q=' + encodeURIComponent(query)
+ window.location.href = searchURL
+ }
+
+ /**
+ * Cleanup on page unload
+ */
+ function cleanup () {
+ if (updateInterval) {
+ clearInterval(updateInterval)
+ updateInterval = null
+ }
+ }
+
+ // Initialize when DOM is ready
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', initialize)
+ } else {
+ initialize()
+ }
+
+ // Cleanup on page unload
+ window.addEventListener('beforeunload', cleanup)
+})()
diff --git a/pages/settings/index.html b/pages/settings/index.html
index d8e6d629c..80fefb199 100644
--- a/pages/settings/index.html
+++ b/pages/settings/index.html
@@ -137,6 +137,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/settings/settings.js b/pages/settings/settings.js
index 64005305f..6f924cca4 100644
--- a/pages/settings/settings.js
+++ b/pages/settings/settings.js
@@ -298,6 +298,109 @@ for (var language in languages) { //from localization.build.js
languagePicker.value = getCurrentLanguage()
+/* homepage settings */
+
+var enableHomepageCheckbox = document.getElementById('checkbox-enable-homepage')
+var homepageOptions = document.getElementById('homepage-options')
+var homepageTypeSelect = document.getElementById('homepage-type')
+var customHomepageURLSection = document.getElementById('custom-homepage-url-section')
+var customHomepageURLInput = document.getElementById('custom-homepage-url')
+var homepageShowClockCheckbox = document.getElementById('checkbox-homepage-show-clock')
+var homepageShowGreetingCheckbox = document.getElementById('checkbox-homepage-show-greeting')
+var homepageTimeFormatSelect = document.getElementById('homepage-time-format')
+
+function updateHomepageOptionsVisibility (enabled) {
+ if (enabled) {
+ homepageOptions.style.opacity = '1'
+ homepageOptions.style.pointerEvents = 'auto'
+ } else {
+ homepageOptions.style.opacity = '0.5'
+ homepageOptions.style.pointerEvents = 'none'
+ }
+}
+
+settings.get('homepageEnabled', function (value) {
+ if (value === true || value === undefined) {
+ enableHomepageCheckbox.checked = true
+ updateHomepageOptionsVisibility(true)
+ } else {
+ enableHomepageCheckbox.checked = false
+ updateHomepageOptionsVisibility(false)
+ }
+})
+
+enableHomepageCheckbox.addEventListener('change', function (e) {
+ settings.set('homepageEnabled', this.checked)
+ updateHomepageOptionsVisibility(this.checked)
+})
+
+settings.get('homepageShowClock', function (value) {
+ if (value === false) {
+ homepageShowClockCheckbox.checked = false
+ } else {
+ homepageShowClockCheckbox.checked = true
+ }
+})
+
+homepageShowClockCheckbox.addEventListener('change', function (e) {
+ settings.set('homepageShowClock', this.checked)
+})
+
+settings.get('homepageShowGreeting', function (value) {
+ if (value === false) {
+ homepageShowGreetingCheckbox.checked = false
+ } else {
+ homepageShowGreetingCheckbox.checked = true
+ }
+})
+
+homepageShowGreetingCheckbox.addEventListener('change', function (e) {
+ settings.set('homepageShowGreeting', this.checked)
+})
+
+settings.get('homepageTimeFormat', function (value) {
+ if (value) {
+ homepageTimeFormatSelect.value = value
+ }
+})
+
+homepageTimeFormatSelect.addEventListener('change', function (e) {
+ settings.set('homepageTimeFormat', this.value)
+})
+
+settings.get('homepageType', function (value) {
+ if (value === 'custom') {
+ homepageTypeSelect.value = 'custom'
+ customHomepageURLSection.style.display = 'block'
+ } else {
+ homepageTypeSelect.value = 'default'
+ customHomepageURLSection.style.display = 'none'
+ }
+})
+
+homepageTypeSelect.addEventListener('change', function (e) {
+ settings.set('homepageType', this.value)
+ if (this.value === 'custom') {
+ customHomepageURLSection.style.display = 'block'
+ customHomepageURLInput.focus()
+ } else {
+ customHomepageURLSection.style.display = 'none'
+ }
+})
+
+settings.get('customHomepageURL', function (value) {
+ if (value) {
+ customHomepageURLInput.value = value
+ }
+})
+
+customHomepageURLInput.addEventListener('change', function (e) {
+ const url = this.value.trim()
+ if (url) {
+ settings.set('customHomepageURL', url)
+ }
+})
+
languagePicker.addEventListener('change', function () {
settings.set('userSelectedLanguage', this.value)
showRestartRequiredBanner()
From 8021bfb4e4412cf1e7d2e4775f671e652cb70528 Mon Sep 17 00:00:00 2001
From: Lucas McMurtrie <131608268+Chickaboo@users.noreply.github.com>
Date: Sun, 19 Oct 2025 21:37:01 -0500
Subject: [PATCH 2/6] Use selected search engine for new tab searches
Updated the new tab page to use the currently selected search engine instead of a hardcoded DuckDuckGo URL. Added the searchEngine utility script to index.html and modified performSearch to construct the search URL dynamically.
---
pages/newtab/index.html | 1 +
pages/newtab/newtab.js | 6 ++++--
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/pages/newtab/index.html b/pages/newtab/index.html
index 373221144..2a3f23a13 100644
--- a/pages/newtab/index.html
+++ b/pages/newtab/index.html
@@ -31,6 +31,7 @@
+