Skip to content
Merged
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
324 changes: 324 additions & 0 deletions diff.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,324 @@
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 @@
<!-- Sub-Tab 2: Favorites & Setup -->
<div class="sub-tab-panel" id="subtab-fb-favorites" role="tabpanel" style="display: none;">
<div class="section">
- <h2 class="section-title">⭐ Favorite Teams</h2>
+ <h2 class="section-title">📌 Pinned Fixtures</h2>
+ <div id="pinnedFixturesList" class="pinned-list">
+ <div class="fb-empty">No pinned fixtures yet. Star a match in the 📅 Matches tab!</div>
+ </div>
+ </div>
+
+ <div class="section" style="margin-top: 16px;">
+ <h2 class="section-title">⭐ Favorite Teams Matches</h2>
+ <div id="favoriteTeamsMatchesList" class="fixtures-list">
+ <div class="fb-empty">Add a favorite team below to see their matches here.</div>
+ </div>
+ </div>
+
+ <div class="section" style="margin-top: 16px;">
+ <h2 class="section-title">❤️ Choose Favorite Teams</h2>
<div class="league-chips" id="favLeagueChips"></div>
<div class="input-with-btn" style="margin-top: 8px;">
<input type="text" id="teamSearchInput" placeholder="Search a team to favorite..." autocomplete="off">
@@ -275,14 +289,7 @@
<div id="favoriteTeamsList" class="favorite-chips" style="margin-top: 10px;"></div>
</div>

- <div class="section" style="margin-top: 16px;">
- <h2 class="section-title">📌 Pinned Fixtures</h2>
- <div id="pinnedFixturesList" class="pinned-list">
- <div class="fb-empty">No pinned fixtures yet. Star a match in the 📅 Matches tab!</div>
- </div>
- </div>
-
- <div class="section" style="margin-top: 16px;">
+ <div class="section" style="margin-top: 16px; display: none;">
<h2 class="section-title">📡 Data Source</h2>
<span class="help-text">Football fixtures and live scores come from ESPN automatically — no API key needed. Team search uses the built-in top-clubs list.</span>
</div>
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
- ? `<span class="fixture-score">${f.score.home ?? '-'} : ${f.score.away ?? '-'}</span>`
+ ? `<span class="fixture-kickoff">FT</span>`
: live
? `<span class="fixture-score">${escapeHtml(f.minute || 'LIVE')}</span>`
: `<span class="fixture-kickoff">${new Date(f.kickoffUtc).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>`;
return `
<div class="fixture-row ${live ? 'is-live' : ''}" data-fixture-id="${escapeHtml(f.id)}">
+ <div class="fixture-meta-left">
+ ${(LEAGUE_LABELS[f.competition.code] || f.competition.code) ? `<span class="comp-chip">${escapeHtml(LEAGUE_LABELS[f.competition.code] || f.competition.code)}</span>` : ''}
+ </div>
<div class="fixture-teams">
<span class="fixture-team home">${homeHtml}</span>
${center}
<span class="fixture-team away">${awayHtml}</span>
</div>
- ${f.bigMatch ? SVG_ICONS.flame : ''}
- ${(LEAGUE_LABELS[f.competition.code] || f.competition.code) ? `<span class="comp-chip">${escapeHtml(LEAGUE_LABELS[f.competition.code] || f.competition.code)}</span>` : ''}
- <button type="button" class="star-btn ${isPinned(f) ? 'pinned' : ''}" data-pin-id="${escapeHtml(f.id)}" title="Pin fixture">
- ${isPinned(f) ? SVG_ICONS.starFilled : SVG_ICONS.star}
- </button>
+ <div class="fixture-meta-right">
+ ${f.bigMatch ? SVG_ICONS.flame : ''}
+ <button type="button" class="star-btn ${isPinned(f) ? 'pinned' : ''}" data-pin-id="${escapeHtml(f.id)}" title="Pin fixture">
+ ${isPinned(f) ? SVG_ICONS.starFilled : SVG_ICONS.star}
+ </button>
+ </div>
</div>`;
}).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 = '<div class="fb-empty">Add a favorite team below to see their matches here.</div>';
+ 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 = '<div class="fb-empty">No upcoming or recent matches found for your favorite teams.</div>';
+ 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)} <strong class="score-val">${f.score.home ?? 0}</strong>`
+ : `${escapeHtml(f.homeTeam.name)} ${crestHtml(f.homeTeam)}`;
+ const awayHtml = (live || finished)
+ ? `<strong class="score-val">${f.score.away ?? 0}</strong> ${crestHtml(f.awayTeam)} ${escapeHtml(f.awayTeam.name)}`
+ : `${crestHtml(f.awayTeam)} ${escapeHtml(f.awayTeam.name)}`;
+
+ const center = finished
+ ? `<span class="fixture-kickoff">FT</span>`
+ : live
+ ? `<span class="fixture-score">${escapeHtml(f.minute || 'LIVE')}</span>`
+ : `<span class="fixture-kickoff">${f.kickoffUtc ? new Date(f.kickoffUtc).toLocaleString([], { weekday: 'short', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : 'TBD'}</span>`;
+
+ return `
+ <div class="fixture-row ${live ? 'is-live' : ''}" data-fixture-id="${escapeHtml(f.id)}">
+ <div class="fixture-meta-left">
+ ${(LEAGUE_LABELS[f.competition.code] || f.competition.code) ? `<span class="comp-chip">${escapeHtml(LEAGUE_LABELS[f.competition.code] || f.competition.code)}</span>` : ''}
+ </div>
+ <div class="fixture-teams">
+ <span class="fixture-team home">${homeHtml}</span>
+ ${center}
+ <span class="fixture-team away">${awayHtml}</span>
+ </div>
+ <div class="fixture-meta-right">
+ ${f.bigMatch ? SVG_ICONS.flame : ''}
+ <button type="button" class="star-btn ${isPinned(f) ? 'pinned' : ''}" data-pin-id="${escapeHtml(f.id)}" title="Pin fixture">
+ ${isPinned(f) ? SVG_ICONS.starFilled : SVG_ICONS.star}
+ </button>
+ </div>
+ </div>`;
+ }).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 = '<span class="fb-empty" style="padding:6px;">No favorite teams yet — search above!</span>';
@@ -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 = `<div class="fb-empty">⭐ ${escapeHtml(el.dataset.teamName)} favorited!</div>`;
}
@@ -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); }

22 changes: 7 additions & 15 deletions src/main/footballMonitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
25 changes: 16 additions & 9 deletions src/renderer/settings/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,21 @@ <h2 class="section-title" style="margin-bottom: 0;">📅 Fixture Schedule (Next
<!-- Sub-Tab 2: Favorites & Setup -->
<div class="sub-tab-panel" id="subtab-fb-favorites" role="tabpanel" style="display: none;">
<div class="section">
<h2 class="section-title">⭐ Favorite Teams</h2>
<h2 class="section-title">📌 Pinned Fixtures</h2>
<div id="pinnedFixturesList" class="pinned-list">
<div class="fb-empty">No pinned fixtures yet. Star a match in the 📅 Matches tab!</div>
</div>
</div>

<div class="section" style="margin-top: 16px;">
<h2 class="section-title">⭐ Favorite Teams Matches</h2>
<div id="favoriteTeamsMatchesList" class="fixtures-list">
<div class="fb-empty">Add a favorite team below to see their matches here.</div>
</div>
</div>

<div class="section" style="margin-top: 16px;">
<h2 class="section-title">❤️ Choose Favorite Teams</h2>
<div class="league-chips" id="favLeagueChips"></div>
<div class="input-with-btn" style="margin-top: 8px;">
<input type="text" id="teamSearchInput" placeholder="Search a team to favorite..." autocomplete="off">
Expand All @@ -275,14 +289,7 @@ <h2 class="section-title">⭐ Favorite Teams</h2>
<div id="favoriteTeamsList" class="favorite-chips" style="margin-top: 10px;"></div>
</div>

<div class="section" style="margin-top: 16px;">
<h2 class="section-title">📌 Pinned Fixtures</h2>
<div id="pinnedFixturesList" class="pinned-list">
<div class="fb-empty">No pinned fixtures yet. Star a match in the 📅 Matches tab!</div>
</div>
</div>

<div class="section" style="margin-top: 16px;">
<div class="section" style="margin-top: 16px; display: none;">
<h2 class="section-title">📡 Data Source</h2>
<span class="help-text">Football fixtures and live scores come from ESPN automatically — no API key needed. Team search uses the built-in top-clubs list.</span>
</div>
Expand Down
Loading
Loading