++
+
๐ก Data Source
+ Football fixtures and live scores come from ESPN automatically โ no API key needed. Team search uses the built-in top-clubs list.
+
+diff --git a/src/renderer/settings/renderer.js b/src/renderer/settings/renderer.js
+index 8bfc9c2..d75f68a 100644
+--- a/src/renderer/settings/renderer.js
++++ b/src/renderer/settings/renderer.js
+@@ -1121,7 +1121,9 @@ async function loadFootballTab() {
+ renderLeagueChips(leagueChipsEl, footballState.leagues, footballState.cupsEnabled, footballState.allMode);
+ renderFavLeagueChips(favLeagueChipsEl, favSearchLeague);
+ renderFavoriteTeams();
++ renderFavoriteTeamsMatches();
+ renderPinnedFixtures();
++ renderFavoriteTeamsMatches();
+ renderFixtures();
+ updateFootballBanner();
+ } catch (error) {
+@@ -1178,13 +1180,15 @@ async function toggleCups() {
+ }
+
+ async function toggleLeague(code) {
+- // Clicking a league chip exits All mode and selects that chip; clicking a
+- // selected chip deselects it (multi-select across clicks).
+- footballState.allMode = false;
+- if (footballState.leagues.includes(code)) {
+- footballState.leagues = footballState.leagues.filter(c => c !== code);
++ if (footballState.allMode) {
++ footballState.allMode = false;
++ footballState.leagues = [code];
+ } else {
+- footballState.leagues = [...footballState.leagues, code];
++ if (footballState.leagues.includes(code)) {
++ footballState.leagues = footballState.leagues.filter(c => c !== code);
++ } else {
++ footballState.leagues = [...footballState.leagues, code];
++ }
+ }
+ await saveLeagues();
+ }
+@@ -1328,22 +1332,26 @@ function renderFixturesInner() {
+ : `${crestHtml(f.awayTeam)} ${escapeHtml(f.awayTeam.name)}`;
+
+ const center = finished
+- ? `
${f.score.home ?? '-'} : ${f.score.away ?? '-'}`
++ ? `
FT`
+ : live
+ ? `
${escapeHtml(f.minute || 'LIVE')}`
+ : `
${new Date(f.kickoffUtc).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
+ return `
+
++
++ ${(LEAGUE_LABELS[f.competition.code] || f.competition.code) ? `${escapeHtml(LEAGUE_LABELS[f.competition.code] || f.competition.code)}` : ''}
++
+
+ ${homeHtml}
+ ${center}
+ ${awayHtml}
+
+- ${f.bigMatch ? SVG_ICONS.flame : ''}
+- ${(LEAGUE_LABELS[f.competition.code] || f.competition.code) ? `
${escapeHtml(LEAGUE_LABELS[f.competition.code] || f.competition.code)}` : ''}
+-
++
++ ${f.bigMatch ? SVG_ICONS.flame : ''}
++
++
+
`;
+ }).join('');
+
+@@ -1361,6 +1369,7 @@ function renderFixturesInner() {
+ footballState.pinnedFixtures = res.pinnedFixtures;
+ renderFixtures();
+ renderPinnedFixtures();
++ renderFavoriteTeamsMatches();
+ }
+ } catch (error) {
+ console.error('Failed to toggle pin:', error);
+@@ -1421,6 +1430,7 @@ function renderPinnedFixtures() {
+ if (res.success) {
+ footballState.pinnedFixtures = res.pinnedFixtures;
+ renderPinnedFixtures();
++ renderFavoriteTeamsMatches();
+ renderFixtures();
+ }
+ } catch (error) {
+@@ -1430,6 +1440,103 @@ function renderPinnedFixtures() {
+ });
+ }
+
++function renderFavoriteTeamsMatches() {
++ const container = document.getElementById('favoriteTeamsMatchesList');
++ if (!container) return;
++
++ if (footballState.favoriteTeams.length === 0) {
++ container.innerHTML = '
Add a favorite team below to see their matches here.
';
++ return;
++ }
++
++ const now = new Date();
++ const next7Days = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
++ const startDay = new Date(now.getTime() - 24 * 60 * 60 * 1000);
++
++ let rawFavMatches = fixturesCache.filter(f => {
++ if (!isFavoriteSide(f)) return false;
++ if (!f.kickoffUtc) return true;
++ const date = new Date(f.kickoffUtc);
++ return date >= startDay && date <= next7Days;
++ });
++
++ const uniqueFavMatches = [];
++ const seen = new Set();
++
++ for (const f of rawFavMatches) {
++ const dateStr = f.kickoffUtc ? new Date(f.kickoffUtc).toDateString() : 'tbd';
++ const key = `${(f.homeTeam && f.homeTeam.name ? f.homeTeam.name : '').toLowerCase()}-${(f.awayTeam && f.awayTeam.name ? f.awayTeam.name : '').toLowerCase()}-${dateStr}`;
++ if (!seen.has(key)) {
++ seen.add(key);
++ uniqueFavMatches.push(f);
++ }
++ }
++
++ const favMatches = uniqueFavMatches.sort((a, b) => Date.parse(a.kickoffUtc || 0) - Date.parse(b.kickoffUtc || 0));
++
++ if (favMatches.length === 0) {
++ container.innerHTML = '
No upcoming or recent matches found for your favorite teams.
';
++ return;
++ }
++
++ container.innerHTML = favMatches.map(f => {
++ const live = f.status === 'live';
++ const finished = f.status === 'finished';
++
++ const homeHtml = (live || finished)
++ ? `${escapeHtml(f.homeTeam.name)} ${crestHtml(f.homeTeam)}
${f.score.home ?? 0}`
++ : `${escapeHtml(f.homeTeam.name)} ${crestHtml(f.homeTeam)}`;
++ const awayHtml = (live || finished)
++ ? `
${f.score.away ?? 0} ${crestHtml(f.awayTeam)} ${escapeHtml(f.awayTeam.name)}`
++ : `${crestHtml(f.awayTeam)} ${escapeHtml(f.awayTeam.name)}`;
++
++ const center = finished
++ ? `
FT`
++ : live
++ ? `
${escapeHtml(f.minute || 'LIVE')}`
++ : `
${f.kickoffUtc ? new Date(f.kickoffUtc).toLocaleString([], { weekday: 'short', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : 'TBD'}`;
++
++ return `
++
++
++ ${(LEAGUE_LABELS[f.competition.code] || f.competition.code) ? `${escapeHtml(LEAGUE_LABELS[f.competition.code] || f.competition.code)}` : ''}
++
++
++ ${homeHtml}
++ ${center}
++ ${awayHtml}
++
++
++ ${f.bigMatch ? SVG_ICONS.flame : ''}
++
++
++
`;
++ }).join('');
++
++ wireCrestFallbacks(container);
++
++ container.querySelectorAll('.star-btn').forEach(btn => {
++ btn.addEventListener('click', async (e) => {
++ e.stopPropagation();
++ const fixture = fixturesCache.find(f => f.id === btn.dataset.pinId);
++ if (!fixture) return;
++ try {
++ const res = await window.api.togglePinFixture(fixture);
++ if (res.success) {
++ footballState.pinnedFixtures = res.pinnedFixtures;
++ renderFixtures();
++ renderPinnedFixtures();
++ renderFavoriteTeamsMatches();
++ }
++ } catch (error) {
++ console.error('Failed to toggle pin:', error);
++ }
++ });
++ });
++}
++
+ function renderFavoriteTeams() {
+ if (footballState.favoriteTeams.length === 0) {
+ favoriteTeamsListEl.innerHTML = '
No favorite teams yet โ search above!';
+@@ -1448,6 +1555,7 @@ function renderFavoriteTeams() {
+ if (res.success) {
+ footballState.favoriteTeams = res.favoriteTeams;
+ renderFavoriteTeams();
++ renderFavoriteTeamsMatches();
+ renderFixtures();
+ }
+ } catch (error) {
+@@ -1486,6 +1594,7 @@ async function runTeamSearch() {
+ if (res2.success) {
+ footballState.favoriteTeams = res2.favoriteTeams;
+ renderFavoriteTeams();
++ renderFavoriteTeamsMatches();
+ renderFixtures();
+ teamSearchResultsEl.innerHTML = `
โญ ${escapeHtml(el.dataset.teamName)} favorited!
`;
+ }
+@@ -1562,6 +1671,7 @@ if (typeof window.api.onFootballStatusUpdated === 'function') {
+ fixturesCache = (sched && sched.fixtures) || [];
+ renderFixtures();
+ renderPinnedFixtures();
++ renderFavoriteTeamsMatches();
+ }).catch(err => console.error('Football schedule refresh failed:', err));
+ });
+ }
+@@ -1619,4 +1729,5 @@ if (window.api && typeof window.api.onSelectTab === 'function') {
+ });
+ }
+
+-loadSettings();
+\ No newline at end of file
++loadSettings();
++
+diff --git a/src/renderer/settings/styles.css b/src/renderer/settings/styles.css
+index a7b6a46..0e3e98b 100644
+--- a/src/renderer/settings/styles.css
++++ b/src/renderer/settings/styles.css
+@@ -2040,6 +2040,22 @@ body {
+ white-space: nowrap;
+ }
+
++.fixture-meta-left {
++ display: flex;
++ align-items: center;
++ width: 100px;
++ flex-shrink: 0;
++}
++
++.fixture-meta-right {
++ display: flex;
++ align-items: center;
++ justify-content: flex-end;
++ gap: 8px;
++ width: 50px;
++ flex-shrink: 0;
++}
++
+ .fixture-row.is-live .fixture-score,
+ .fixture-row.is-live .fb-live-dot { color: var(--fb-live-accent); }
+
diff --git a/src/main/footballMonitor.js b/src/main/footballMonitor.js
index 4bfda39..98bdec6 100644
--- a/src/main/footballMonitor.js
+++ b/src/main/footballMonitor.js
@@ -176,21 +176,13 @@ function createFootballMonitor({
const windowEndMs = 2 * 60 * 60 * 1000 + 45 * 60 * 1000; // 2h45m after kickoff
const cached = store.get(FIXTURES_KEY, []);
- const watchedCached = cached.filter(f =>
- (
- (isWatchedFixture(f, favoriteTeams, pinnedFixtures) ||
- pinnedFixtures.some(p => p.id === f.id)) &&
- (() => {
- // Watcher only polls inside active match windows (plan ยง2.3)โฆ
- if (!f.kickoffUtc) return true;
- const k = Date.parse(f.kickoffUtc);
- return now >= k - windowStartMs && now <= k + windowEndMs;
- })()
- ) ||
- // โฆbut a fixture the cache believes is LIVE stays watched past the
- // window so the dead-man switch can retire it.
- f.status === 'live'
- );
+ const watchedCached = cached.filter(f => {
+ const isWatched = isWatchedFixture(f, favoriteTeams, pinnedFixtures) || pinnedFixtures.some(p => p.id === f.id);
+ if (!isWatched) return false;
+ const k = f.kickoffUtc ? Date.parse(f.kickoffUtc) : null;
+ const inWindow = !k || (now >= k - windowStartMs && now <= k + windowEndMs);
+ return inWindow || f.status === 'live';
+ });
const targets = new Map();
for (const f of watchedCached) {
diff --git a/src/renderer/settings/index.html b/src/renderer/settings/index.html
index 2a71ea0..434e6a8 100644
--- a/src/renderer/settings/index.html
+++ b/src/renderer/settings/index.html
@@ -264,7 +264,21 @@
๐
Fixture Schedule (Next
-
โญ Favorite Teams
+
๐ Pinned Fixtures
+
+
No pinned fixtures yet. Star a match in the ๐
Matches tab!
+
+
+
+
+
โญ Favorite Teams Matches
+
+
Add a favorite team below to see their matches here.
+
+
+
+
+
โค๏ธ Choose Favorite Teams
-
-
๐ Pinned Fixtures
-
-
No pinned fixtures yet. Star a match in the ๐
Matches tab!
-
-
-
-
+
๐ก Data Source
Football fixtures and live scores come from ESPN automatically โ no API key needed. Team search uses the built-in top-clubs list.
diff --git a/src/renderer/settings/renderer.js b/src/renderer/settings/renderer.js
index 8bfc9c2..d75f68a 100644
--- a/src/renderer/settings/renderer.js
+++ b/src/renderer/settings/renderer.js
@@ -1121,7 +1121,9 @@ async function loadFootballTab() {
renderLeagueChips(leagueChipsEl, footballState.leagues, footballState.cupsEnabled, footballState.allMode);
renderFavLeagueChips(favLeagueChipsEl, favSearchLeague);
renderFavoriteTeams();
+ renderFavoriteTeamsMatches();
renderPinnedFixtures();
+ renderFavoriteTeamsMatches();
renderFixtures();
updateFootballBanner();
} catch (error) {
@@ -1178,13 +1180,15 @@ async function toggleCups() {
}
async function toggleLeague(code) {
- // Clicking a league chip exits All mode and selects that chip; clicking a
- // selected chip deselects it (multi-select across clicks).
- footballState.allMode = false;
- if (footballState.leagues.includes(code)) {
- footballState.leagues = footballState.leagues.filter(c => c !== code);
+ if (footballState.allMode) {
+ footballState.allMode = false;
+ footballState.leagues = [code];
} else {
- footballState.leagues = [...footballState.leagues, code];
+ if (footballState.leagues.includes(code)) {
+ footballState.leagues = footballState.leagues.filter(c => c !== code);
+ } else {
+ footballState.leagues = [...footballState.leagues, code];
+ }
}
await saveLeagues();
}
@@ -1328,22 +1332,26 @@ function renderFixturesInner() {
: `${crestHtml(f.awayTeam)} ${escapeHtml(f.awayTeam.name)}`;
const center = finished
- ? `
${f.score.home ?? '-'} : ${f.score.away ?? '-'}`
+ ? `
FT`
: live
? `
${escapeHtml(f.minute || 'LIVE')}`
: `
${new Date(f.kickoffUtc).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
return `
+
+ ${(LEAGUE_LABELS[f.competition.code] || f.competition.code) ? `${escapeHtml(LEAGUE_LABELS[f.competition.code] || f.competition.code)}` : ''}
+
${homeHtml}
${center}
${awayHtml}
- ${f.bigMatch ? SVG_ICONS.flame : ''}
- ${(LEAGUE_LABELS[f.competition.code] || f.competition.code) ? `
${escapeHtml(LEAGUE_LABELS[f.competition.code] || f.competition.code)}` : ''}
-
+
+ ${f.bigMatch ? SVG_ICONS.flame : ''}
+
+
`;
}).join('');
@@ -1361,6 +1369,7 @@ function renderFixturesInner() {
footballState.pinnedFixtures = res.pinnedFixtures;
renderFixtures();
renderPinnedFixtures();
+ renderFavoriteTeamsMatches();
}
} catch (error) {
console.error('Failed to toggle pin:', error);
@@ -1421,6 +1430,7 @@ function renderPinnedFixtures() {
if (res.success) {
footballState.pinnedFixtures = res.pinnedFixtures;
renderPinnedFixtures();
+ renderFavoriteTeamsMatches();
renderFixtures();
}
} catch (error) {
@@ -1430,6 +1440,103 @@ function renderPinnedFixtures() {
});
}
+function renderFavoriteTeamsMatches() {
+ const container = document.getElementById('favoriteTeamsMatchesList');
+ if (!container) return;
+
+ if (footballState.favoriteTeams.length === 0) {
+ container.innerHTML = '
Add a favorite team below to see their matches here.
';
+ return;
+ }
+
+ const now = new Date();
+ const next7Days = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
+ const startDay = new Date(now.getTime() - 24 * 60 * 60 * 1000);
+
+ let rawFavMatches = fixturesCache.filter(f => {
+ if (!isFavoriteSide(f)) return false;
+ if (!f.kickoffUtc) return true;
+ const date = new Date(f.kickoffUtc);
+ return date >= startDay && date <= next7Days;
+ });
+
+ const uniqueFavMatches = [];
+ const seen = new Set();
+
+ for (const f of rawFavMatches) {
+ const dateStr = f.kickoffUtc ? new Date(f.kickoffUtc).toDateString() : 'tbd';
+ const key = `${(f.homeTeam && f.homeTeam.name ? f.homeTeam.name : '').toLowerCase()}-${(f.awayTeam && f.awayTeam.name ? f.awayTeam.name : '').toLowerCase()}-${dateStr}`;
+ if (!seen.has(key)) {
+ seen.add(key);
+ uniqueFavMatches.push(f);
+ }
+ }
+
+ const favMatches = uniqueFavMatches.sort((a, b) => Date.parse(a.kickoffUtc || 0) - Date.parse(b.kickoffUtc || 0));
+
+ if (favMatches.length === 0) {
+ container.innerHTML = '
No upcoming or recent matches found for your favorite teams.
';
+ return;
+ }
+
+ container.innerHTML = favMatches.map(f => {
+ const live = f.status === 'live';
+ const finished = f.status === 'finished';
+
+ const homeHtml = (live || finished)
+ ? `${escapeHtml(f.homeTeam.name)} ${crestHtml(f.homeTeam)}
${f.score.home ?? 0}`
+ : `${escapeHtml(f.homeTeam.name)} ${crestHtml(f.homeTeam)}`;
+ const awayHtml = (live || finished)
+ ? `
${f.score.away ?? 0} ${crestHtml(f.awayTeam)} ${escapeHtml(f.awayTeam.name)}`
+ : `${crestHtml(f.awayTeam)} ${escapeHtml(f.awayTeam.name)}`;
+
+ const center = finished
+ ? `
FT`
+ : live
+ ? `
${escapeHtml(f.minute || 'LIVE')}`
+ : `
${f.kickoffUtc ? new Date(f.kickoffUtc).toLocaleString([], { weekday: 'short', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : 'TBD'}`;
+
+ return `
+
+
+ ${(LEAGUE_LABELS[f.competition.code] || f.competition.code) ? `${escapeHtml(LEAGUE_LABELS[f.competition.code] || f.competition.code)}` : ''}
+
+
+ ${homeHtml}
+ ${center}
+ ${awayHtml}
+
+
+ ${f.bigMatch ? SVG_ICONS.flame : ''}
+
+
+
`;
+ }).join('');
+
+ wireCrestFallbacks(container);
+
+ container.querySelectorAll('.star-btn').forEach(btn => {
+ btn.addEventListener('click', async (e) => {
+ e.stopPropagation();
+ const fixture = fixturesCache.find(f => f.id === btn.dataset.pinId);
+ if (!fixture) return;
+ try {
+ const res = await window.api.togglePinFixture(fixture);
+ if (res.success) {
+ footballState.pinnedFixtures = res.pinnedFixtures;
+ renderFixtures();
+ renderPinnedFixtures();
+ renderFavoriteTeamsMatches();
+ }
+ } catch (error) {
+ console.error('Failed to toggle pin:', error);
+ }
+ });
+ });
+}
+
function renderFavoriteTeams() {
if (footballState.favoriteTeams.length === 0) {
favoriteTeamsListEl.innerHTML = '
No favorite teams yet โ search above!';
@@ -1448,6 +1555,7 @@ function renderFavoriteTeams() {
if (res.success) {
footballState.favoriteTeams = res.favoriteTeams;
renderFavoriteTeams();
+ renderFavoriteTeamsMatches();
renderFixtures();
}
} catch (error) {
@@ -1486,6 +1594,7 @@ async function runTeamSearch() {
if (res2.success) {
footballState.favoriteTeams = res2.favoriteTeams;
renderFavoriteTeams();
+ renderFavoriteTeamsMatches();
renderFixtures();
teamSearchResultsEl.innerHTML = `
โญ ${escapeHtml(el.dataset.teamName)} favorited!
`;
}
@@ -1562,6 +1671,7 @@ if (typeof window.api.onFootballStatusUpdated === 'function') {
fixturesCache = (sched && sched.fixtures) || [];
renderFixtures();
renderPinnedFixtures();
+ renderFavoriteTeamsMatches();
}).catch(err => console.error('Football schedule refresh failed:', err));
});
}
@@ -1619,4 +1729,5 @@ if (window.api && typeof window.api.onSelectTab === 'function') {
});
}
-loadSettings();
\ No newline at end of file
+loadSettings();
+
diff --git a/src/renderer/settings/styles.css b/src/renderer/settings/styles.css
index a7b6a46..0e3e98b 100644
--- a/src/renderer/settings/styles.css
+++ b/src/renderer/settings/styles.css
@@ -2040,6 +2040,22 @@ body {
white-space: nowrap;
}
+.fixture-meta-left {
+ display: flex;
+ align-items: center;
+ width: 100px;
+ flex-shrink: 0;
+}
+
+.fixture-meta-right {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 8px;
+ width: 50px;
+ flex-shrink: 0;
+}
+
.fixture-row.is-live .fixture-score,
.fixture-row.is-live .fb-live-dot { color: var(--fb-live-accent); }