diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e12bb5f..acedda0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added support for touch-swipe and mouse-wheel gestures on the search engine icon to switch search engines when they are hidden ([@prem-k-r](https://github.com/prem-k-r)) ([#145](https://github.com/prem-k-r/MaterialYouNewTab/pull/145)) +- Added a Finance Dock to display real-time stock and cryptocurrency trends at the bottom of the page ([`@Jmersh`](https://github.com/Jmersh)) ([`#176`](https://github.com/prem-k-r/MaterialYouNewTab/pull/176)) +- Added support for adding custom symbols (e.g., AAPL, BTC-USD) and removing them via right-click ([`@Jmersh`](https://github.com/Jmersh)) ([`#176`](https://github.com/prem-k-r/MaterialYouNewTab/pull/176)) +- Added real-time currency conversion to display stocks in a preferred currency (USD, EUR, GBP) using Yahoo Finance exchange rates ([`@Jmersh`](https://github.com/Jmersh)) ([`#176`](https://github.com/prem-k-r/MaterialYouNewTab/pull/176)) +- Added Finance customization settings: Stack in rows (wrap mode), Compact mode (hide price), and toggle between percentage or numerical price change ([`@Jmersh`](https://github.com/Jmersh)) ([`#176`](https://github.com/prem-k-r/MaterialYouNewTab/pull/176)) ### Improved diff --git a/index.html b/index.html index 2819962b..054bf0a2 100644 --- a/index.html +++ b/index.html @@ -42,6 +42,7 @@ + @@ -1370,6 +1371,90 @@

Material You New Tab

+
+
+
+ Finance Settings + +
+
+ +
+
+
Finance Dock
+
Show market trends at the bottom
+
+ +
+ + +
+
+
Stack in Rows
+
Wrap stocks instead of scrolling
+
+ +
+ + +
+
+
Compact Mode
+
Reduce pill size to fit more
+
+ +
+ + +
+
+
Display Value
+
Show price change instead of percentage
+
+ +
+ + +
+
+
Add Stocks/Crypto
+
Right-click a pill in the dock to remove
+
+
+ + +
+
+ + +
+
+
Currency
+
Force display currency
+
+ +
+
+
+
+
@@ -1948,6 +2033,13 @@

Material You New Tab

+ + + \ No newline at end of file diff --git a/locales/en.js b/locales/en.js index a6e6e61d..dd4a5147 100644 --- a/locales/en.js +++ b/locales/en.js @@ -91,6 +91,24 @@ const en = { "LearnMoreButton": "Learn more", "saveAPI": "Save", +// Finance + "financeSectionTitle": "Finance Settings", + "financeTitleText": "Finance Dock", + "financeInfoText": "Show market trends at the bottom", + "financeStackText": "Stack in Rows", + "financeStackInfo": "Wrap stocks instead of scrolling", + "financeCompactText": "Compact Mode", + "financeCompactInfo": "Reduce pill size to fit more", + "financeValueText": "Display Value", + "financeValueInfo": "Show price change instead of percentage", + "financeAddText": "Add Stocks/Crypto", + "financeAddSubtext": "Right-click a pill in the dock to remove", + "financeInputPlaceholder": "Add Symbol (e.g. AAPL or BTC-USD)", + "financeCurrencyText": "Currency", + "financeCurrencySubtext": "Force display currency", + "financeAddButton": "Add", + + // Body Items // Calendar "days": ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], diff --git a/scripts/finance.js b/scripts/finance.js new file mode 100644 index 00000000..931f98b4 --- /dev/null +++ b/scripts/finance.js @@ -0,0 +1,181 @@ +/* + * Material You NewTab - Finance Widget (with Currency Conversion) + */ + +document.addEventListener("DOMContentLoaded", function () { + const dock = document.getElementById("financeDock"); + const ticker = document.getElementById("stockTicker"); + const input = document.getElementById("stockInput"); + const addBtn = document.getElementById("addStockBtn"); + const currencySelect = document.getElementById("currencySelector"); + + const financeCheckbox = document.getElementById("financeCheckbox"); + const wrapToggle = document.getElementById("financeWrapToggle"); + const compactToggle = document.getElementById("financeCompactToggle"); + const valueToggle = document.getElementById("financeValueToggle"); + + const CACHE_KEY = "financeParsedData"; + const TIME_KEY = "financeLastUpdate"; + const SYMBOLS_KEY = "watchedStocks"; + const RETENTION_MS = 15 * 60 * 1000; + + const proxyurl = localStorage.getItem("proxy") || "https://mynt-proxy.rhythmcorehq.com/proxy?url="; + const exchangeRateCache = {}; + + // --- FIX 1: Safe JSON Parsing --- + function readJson(key, fallback) { + try { + const raw = localStorage.getItem(key); + return raw ? JSON.parse(raw) : fallback; + } catch (e) { + return fallback; + } + } + + let watchedSymbols = readJson(SYMBOLS_KEY, ["AAPL", "BTC-USD", "^GSPC"]); + if (!Array.isArray(watchedSymbols)) watchedSymbols = ["AAPL", "BTC-USD", "^GSPC"]; + + let prefCurrency = localStorage.getItem("financeCurrency") || "original"; + + async function getExchangeRate(from, to) { + if (from === to || to === "original") return 1; + const pair = `${from}${to}=X`; + if (exchangeRateCache[pair]) return exchangeRateCache[pair]; + try { + const url = `https://query1.finance.yahoo.com/v8/finance/chart/${pair}?interval=1d&range=1d`; + const response = await fetch(proxyurl + encodeURIComponent(url)); + const data = await response.json(); + const rate = data.chart.result[0].meta.regularMarketPrice; + exchangeRateCache[pair] = rate; + return rate; + } catch (e) { return 1; } + } + + async function fetchStockData(symbol) { + const url = `https://query1.finance.yahoo.com/v8/finance/chart/${symbol}?interval=1d&range=1d`; + try { + const response = await fetch(proxyurl + encodeURIComponent(url)); + const data = await response.json(); + const meta = data.chart.result[0].meta; + const nativePrice = meta.regularMarketPrice; + const nativePrevClose = meta.chartPreviousClose; + const nativeCurrency = meta.currency; + + const targetCurrency = prefCurrency === "original" ? nativeCurrency : prefCurrency; + const rate = await getExchangeRate(nativeCurrency, targetCurrency); + + const convertedPrice = nativePrice * rate; + const convertedChange = (nativePrice - nativePrevClose) * rate; + const percentChange = ((nativePrice - nativePrevClose) / nativePrevClose) * 100; + + return { + symbol: symbol, + price: convertedPrice.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2}), + changeVal: (convertedChange >= 0 ? "+" : "") + convertedChange.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2}), + percent: (percentChange >= 0 ? "+" : "") + percentChange.toFixed(2) + "%", + isUp: convertedChange >= 0, + displayCurrency: targetCurrency + }; + } catch (e) { return null; } + } + + async function refreshData(force = false) { + if (!financeCheckbox.checked) { + dock.style.display = "none"; + return; + } + + const lastUpdate = localStorage.getItem(TIME_KEY); + const cachedData = localStorage.getItem(CACHE_KEY); + + if (!force && lastUpdate && (Date.now() - lastUpdate < RETENTION_MS) && cachedData) { + const parsed = readJson(CACHE_KEY, []); + if (Array.isArray(parsed) && parsed.length > 0) { + renderStocks(parsed); + return; + } + } + + const results = await Promise.all(watchedSymbols.map(s => fetchStockData(s))); + const validResults = results.filter(r => r !== null); + + localStorage.setItem(CACHE_KEY, JSON.stringify(validResults)); + localStorage.setItem(TIME_KEY, Date.now().toString()); + renderStocks(validResults); + } + + function renderStocks(data) { + ticker.innerHTML = ""; + + // --- FIX 2: Hide dock if empty --- + if (!data.length) { + dock.style.display = "none"; + return; + } + dock.style.display = "flex"; + + if (localStorage.getItem("financeWrap") === "true") ticker.classList.add("wrap-mode"); + else ticker.classList.remove("wrap-mode"); + + const isCompact = localStorage.getItem("financeCompact") === "true"; + const showValue = localStorage.getItem("financeShowValue") === "true"; + + data.forEach(stock => { + const pill = document.createElement("div"); + pill.className = `stock-pill ${isCompact ? 'compact' : ''}`; + const trendDisplay = showValue ? stock.changeVal : stock.percent; + pill.innerHTML = ` + ${stock.symbol} + ${isCompact ? '' : `${stock.price} ${stock.displayCurrency}`} + ${stock.isUp ? '▲' : '▼'} ${trendDisplay} + `; + pill.oncontextmenu = (e) => { + e.preventDefault(); + watchedSymbols = watchedSymbols.filter(s => s !== stock.symbol); + localStorage.setItem(SYMBOLS_KEY, JSON.stringify(watchedSymbols)); + refreshData(true); + }; + ticker.appendChild(pill); + }); + } + + // Logic for Buttons/Toggles remains same... + addBtn.onclick = () => { + const sym = input.value.toUpperCase().trim(); + if (sym && !watchedSymbols.includes(sym)) { + watchedSymbols.push(sym); + localStorage.setItem(SYMBOLS_KEY, JSON.stringify(watchedSymbols)); + input.value = ""; + refreshData(true); + } + }; + input.onkeydown = (e) => { if (e.key === "Enter") addBtn.click(); }; + financeCheckbox.onchange = () => { + localStorage.setItem("financeCheckboxState", financeCheckbox.checked); + refreshData(true); + }; + wrapToggle.onchange = () => { + localStorage.setItem("financeWrap", wrapToggle.checked); + refreshData(); + }; + compactToggle.onchange = () => { + localStorage.setItem("financeCompact", compactToggle.checked); + refreshData(); + }; + valueToggle.onchange = () => { + localStorage.setItem("financeShowValue", valueToggle.checked); + refreshData(); + }; + currencySelect.onchange = () => { + localStorage.setItem("financeCurrency", currencySelect.value); + prefCurrency = currencySelect.value; + refreshData(true); + }; + + financeCheckbox.checked = localStorage.getItem("financeCheckboxState") === "true"; + wrapToggle.checked = localStorage.getItem("financeWrap") === "true"; + compactToggle.checked = localStorage.getItem("financeCompact") === "true"; + valueToggle.checked = localStorage.getItem("financeShowValue") === "true"; + currencySelect.value = prefCurrency; + refreshData(); +}); \ No newline at end of file diff --git a/scripts/languages.js b/scripts/languages.js index 39c3f315..0f900774 100644 --- a/scripts/languages.js +++ b/scripts/languages.js @@ -225,6 +225,8 @@ function applyLanguage(lang) { "resetAISettingsBtn", "opacityTitle", "adjustOpacityDesc", + "financeTitleText", + "financeInfoText", "footerToastTitle", "footerToastMessage", "personalizationSectionTitle", @@ -232,12 +234,26 @@ function applyLanguage(lang) { "searchSectionTitle", "weatherSectionTitle", "appearanceSectionTitle", - "settingsSectionTitle" + "settingsSectionTitle", + "financeSectionTitle", + "financeTitleText", + "financeInfoText", + "financeStackText", + "financeStackInfo", + "financeCompactText", + "financeCompactInfo", + "financeValueText", + "financeValueInfo", + "financeAddText", + "financeAddSubtext", + "financeCurrencyText", + "financeCurrencySubtext" ]; // Specific mapping for placeholders const placeholderMap = [ { id: "userLoc", key: "userLoc" }, + { id: "stockInput", key: "financeInputPlaceholder" }, { id: "userAPI", key: "userAPI" }, { id: "searchQ", key: "searchPlaceholder" }, { id: "todoInput", key: "todoPlaceholder" }, @@ -269,6 +285,7 @@ function applyLanguage(lang) { { id: "editBookmarkNameLabel", key: "editBookmarkName" }, { id: "editBookmarkURLLabel", key: "editBookmarkURL" }, { id: "shortcutsSectionTitle", key: "shortcutsText" }, + { id: "addStockBtn", key: "financeAddButton" } // Uses the existing "Add" style translation ]; // Function to apply translations diff --git a/style.css b/style.css index 07a94cda..2b77464c 100644 --- a/style.css +++ b/style.css @@ -2361,6 +2361,114 @@ html[lang="ur"] .qtRounder2 { right: -1.5px; } +/* ========================================================================== + FINANCE TICKER - MATERIAL YOU (V4 FINAL) + ========================================================================== */ + +.finance-dock { + position: fixed; + bottom: 0; + left: 0; + width: 100%; + min-height: 60px; + display: flex; + align-items: center; + background: color-mix(in srgb, var(--accentLightTint-blue) 40%, transparent); + backdrop-filter: blur(12px); + border-top: 1px solid rgba(255, 255, 255, 0.1); + z-index: 100; + box-sizing: border-box; + padding: 0; /* Padding moved to ticker for better scrolling */ +} + +.stock-ticker { + display: flex; + align-items: center; + gap: 12px; + overflow-x: auto; + scrollbar-width: none; + width: 100%; + height: 100%; + padding: 10px 20px; + /* 120px padding on the right ensures the last stock isn't hidden by the settings button */ + padding-inline-end: 120px; + box-sizing: border-box; +} + +/* Center stocks when there are only a few, but allow scrolling when many */ +.stock-ticker:not(.wrap-mode):after { + content: ''; + flex: 0 0 1px; +} + +/* Ensure wrapped rows are centered */ +.stock-ticker.wrap-mode { + flex-wrap: wrap; + justify-content: center; + padding-inline-end: 20px; /* Reset the extra padding when wrapping */ +} + + + +.stock-ticker::-webkit-scrollbar { display: none; } + +/* Individual Pills */ +.stock-pill { + display: flex; + flex-shrink: 0; + align-items: center; + gap: 8px; + background-color: var(--whitishColor-blue); + padding: 6px 14px; + border-radius: 16px; + white-space: nowrap; + height: 36px; + border: 1px solid rgba(0,0,0,0.05); + transition: transform 0.2s ease; +} + +.stock-pill:hover { transform: translateY(-2px); } +.stock-pill .sym { font-weight: 700; color: var(--textColorDark-blue); font-size: 0.85rem; } +.stock-pill .price { opacity: 0.7; font-size: 0.8rem; color: var(--textColorDark-blue); } + +.stock-pill .change { + font-weight: 800; + font-size: 0.75rem; + padding: 2px 8px; + border-radius: 8px; +} + +.stock-pill.compact { padding: 4px 10px; height: 28px; } +.stock-pill.compact .price { display: none; } + +.stock-pill .change.up { color: #1b5e20; background-color: rgba(76, 175, 80, 0.15); } +.stock-pill .change.down { color: #b71c1c; background-color: rgba(244, 67, 54, 0.15); } + +/* Settings UI Specifics */ +.finance-input-row { display: flex; gap: 10px; margin-top: 12px; } +#stockInput { + flex: 1; + background-color: var(--bg-color-blue) !important; + border-radius: 12px !important; + padding: 8px 12px !important; + height: 36px !important; + border: none !important; +} +#addStockBtn { + background-color: var(--darkColor-blue); + color: white; + border: none; + border-radius: 20px; + padding: 0 18px; + cursor: pointer; +} + +/* Dark Mode Overrides */ +.black-theme .stock-pill { background-color: var(--darkColor-dark); border: none; } +.black-theme .finance-dock { background: rgba(0,0,0,0.4); } +.black-theme .stock-pill .change.up { color: #81c784; } + + /* ----------Shortcuts----------------- */ #shortcuts-section { @@ -2378,6 +2486,16 @@ html[lang="ur"] .qtRounder2 { scrollbar-width: none; margin: auto; } +/* Push shortcuts up if the finance dock is active */ +body:has(#financeDock[style*="display: flex"]) #shortcuts-section { + z-index: 110; /* Higher than the Finance Dock's 100 */ + bottom: 85px; +} + +/* Sync the AI Tools tray to the same height as the shortcuts */ +body:has(#financeDock[style*="display: flex"]) .aiToolsCont { + bottom: calc(85px + var(--gap)); +} #shortcuts-section::-webkit-scrollbar { display: none; @@ -4050,6 +4168,7 @@ option:checked { bottom: var(--gap); width: 50px; height: 50px; + z-index: 101; /* This keeps it above the dock */ background-color: var(--accentLightTint-blue); border-radius: 50%; border: 6px solid var(--accentLightTint-blue);