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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
92 changes: 92 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
<script defer src="scripts/google-apps.js"></script>
<script defer src="scripts/voice-search.js"></script>
<script defer src="scripts/backup-restore.js"></script>
<script defer src="scripts/finance.js"></script>

<!-- LANGUAGES SCRIPTS -->
<script src="locales/pt.js"></script>
Expand Down Expand Up @@ -1370,6 +1371,90 @@ <h1>Material You New Tab</h1>
</div>
</div>

<div class="section expanded">
<div class="sectionOuter">
<div class="sectionHeader" role="button">
<span class="sectionTitle" id="financeSectionTitle">Finance Settings</span>
<span class="sectionChevron"></span>
</div>
<div class="sectionInner">
<!-- Visibility Toggle -->
<div class="ttcont">
<div class="texts">
<div class="bigText" id="financeTitleText">Finance Dock</div>
<div class="infoText" id="financeInfoText">Show market trends at the bottom</div>
</div>
<label class="switch">
<input id="financeCheckbox" type="checkbox">
<span class="toggle"></span>
</label>
</div>

<!-- Stack in Rows Toggle -->
<div class="ttcont">
<div class="texts">
<div class="bigText" id="financeStackText">Stack in Rows</div>
<div class="infoText" id="financeStackInfo">Wrap stocks instead of scrolling</div>
</div>
<label class="switch">
<input id="financeWrapToggle" type="checkbox">
<span class="toggle"></span>
</label>
</div>

<!-- Compact Mode Toggle -->
<div class="ttcont">
<div class="texts">
<div class="bigText" id="financeCompactText">Compact Mode</div>
<div class="infoText" id="financeCompactInfo">Reduce pill size to fit more</div>
</div>
<label class="switch">
<input id="financeCompactToggle" type="checkbox">
<span class="toggle"></span>
</label>
</div>

<!-- Display Value Toggle -->
<div class="ttcont">
<div class="texts">
<div class="bigText" id="financeValueText">Display Value</div>
<div class="infoText" id="financeValueInfo">Show price change instead of percentage</div>
</div>
<label class="switch">
<input id="financeValueToggle" type="checkbox">
<span class="toggle"></span>
</label>
</div>

<!-- Add Symbol Input -->
<div class="ttcont unflex">
<div class="texts">
<div class="bigText" id="financeAddText">Add Stocks/Crypto</div>
<div class="infoText" id="financeAddSubtext">Right-click a pill in the dock to remove</div>
</div>
<div class="finance-input-row">
<input type="text" id="stockInput" placeholder="AAPL, BTC-USD..." autocomplete="off">
<button id="addStockBtn" class="savebtn"></button>
</div>
</div>

<!-- Currency Selector -->
<div class="ttcont">
<div class="texts">
<div class="bigText" id="financeCurrencyText">Currency</div>
<div class="infoText" id="financeCurrencySubtext">Force display currency</div>
</div>
<select id="currencySelector" class="languageSelector finance-select">
<option value="original">Default</option>
<option value="USD">USD ($)</option>
<option value="EUR">EUR (€)</option>
<option value="GBP">GBP (Β£)</option>
</select>
</div>
</div>
</div>
</div>

<!-- ---🟑--- -->

<div class="section expanded">
Expand Down Expand Up @@ -1948,6 +2033,13 @@ <h1>Material You New Tab</h1>
<div id="menuButton"></div>
<!-- ending-of-body -->

<!-- FINANCE DOCK -->
<div id="financeDock" class="finance-dock" style="display: none;">
<div id="stockTicker" class="stock-ticker">
<!-- Stocks injected here as horizontal pills -->
</div>
</div>

</body>

</html>
18 changes: 18 additions & 0 deletions locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
181 changes: 181 additions & 0 deletions scripts/finance.js
Original file line number Diff line number Diff line change
@@ -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 = `
<span class="sym">${stock.symbol}</span>
${isCompact ? '' : `<span class="price">${stock.price} <small>${stock.displayCurrency}</small></span>`}
<span class="change ${stock.isUp ? 'up' : 'down'}">${stock.isUp ? 'β–²' : 'β–Ό'} ${trendDisplay}</span>
`;
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();
});
19 changes: 18 additions & 1 deletion scripts/languages.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,19 +225,35 @@ function applyLanguage(lang) {
"resetAISettingsBtn",
"opacityTitle",
"adjustOpacityDesc",
"financeTitleText",
"financeInfoText",
"footerToastTitle",
"footerToastMessage",
"personalizationSectionTitle",
"clockSectionTitle",
"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" },
Expand Down Expand Up @@ -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
Expand Down
Loading