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
52 changes: 50 additions & 2 deletions .github/workflows/pr-summary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,55 @@ on:
jobs:
summary:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: Codium-ai/pr-agent@main
- name: Generate PR Summary
uses: actions/github-script@v7
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prNumber = context.payload.pull_request.number;

const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});

const { data: commits } = await github.rest.pulls.listCommits({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});

const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});

const summary = `## 📋 PR Summary

**Title:** ${pr.title}
**Author:** @${pr.user.login}
**State:** ${pr.state.toUpperCase()}
**Draft:** ${pr.draft ? 'Yes' : 'No'}

### Changes
- **Commits:** ${commits.length}
- **Files Changed:** ${files.length}
- **Additions:** +${pr.additions}
- **Deletions:** -${pr.deletions}

### Files Modified
${files.slice(0, 10).map(f => `- \`${f.filename}\` (${f.changes} changes)`).join('\n')}
${files.length > 10 ? `- ... and ${files.length - 10} more files` : ''}

${pr.body ? `### Description\n${pr.body}` : ''}`;

await github.rest.issues.createComment({
issue_number: prNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: summary,
});
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,4 @@ Everything stays **100% local**. No data is sent anywhere except OpenStreetMap/N

---

Built with [Claude Code](https://claude.ai/claude-code) using Claude Opus 4.6 (Anthropic).
Built with [Claude Code](https://claude.ai/claude-code) using Claude Opus 4.6
13 changes: 9 additions & 4 deletions css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@ input,textarea,select{font-family:var(--font)}
#countries-bar .cb-flags span{cursor:default;transition:transform .1s}
#countries-bar .cb-flags span:hover{transform:scale(1.25)}
#countries-bar .cb-flags.collapsed{max-height:2.8em}
#countries-bar .cb-toggle{background:none;border:none;color:var(--muted);font-size:.58rem;cursor:pointer;padding:2px 0 0;transition:color .15s}
#countries-bar .cb-flags{transition:max-height .3s ease}
#countries-bar .cb-toggle{background:none;border:none;color:var(--muted);font-size:1rem;cursor:pointer;padding:0;margin:6px 0 -4px;transition:color .15s, transform .3s ease;display:block;width:100%;text-align:center;line-height:1}
#countries-bar .cb-toggle:hover{color:var(--text)}
#countries-bar .cb-toggle.expanded{transform:rotate(180deg)}
.stat{flex:1;padding:7px 4px;text-align:center}
.stat+.stat{border-left:1px solid var(--border)}
.stat .val{font-size:.9rem;font-weight:600;color:var(--text)}
Expand Down Expand Up @@ -91,7 +93,7 @@ input,textarea,select{font-family:var(--font)}
.badge-nogps{background:rgba(122,130,156,.1);color:var(--muted)}
.badge-date{background:rgba(78,201,138,.12);color:var(--green)}
.no-gps-row .badge-date{background:rgba(122,130,156,.08);color:var(--muted2)}
.place-label{font-size:.62rem;color:var(--muted);margin-top:6px;padding-left:5px}
.place-label{font-size:.62rem;color:var(--muted);margin-top:6px}
.card-actions{display:flex;gap:1px;opacity:0;transition:opacity .15s;flex-shrink:0}
.photo-card:hover .card-actions{opacity:1}
.card-btn{background:none;border:none;color:var(--muted);font-size:.72rem;padding:4px 5px;border-radius:5px;transition:color .15s}
Expand Down Expand Up @@ -145,7 +147,7 @@ input,textarea,select{font-family:var(--font)}
.alb-detail-edit:hover{background:var(--surface2);color:var(--text)}
.alb-detail-body{overflow-y:auto;flex:1;padding:8px 14px 12px}
.alb-detail-desc{font-size:.76rem;color:var(--muted);margin-bottom:10px;line-height:1.5}
.alb-detail-meta{font-size:.66rem;color:var(--muted2);margin-bottom:10px;display:flex;gap:10px;flex-wrap:wrap}
.alb-detail-meta{font-size:.66rem;color:var(--muted2);margin-bottom:10px;display:flex;gap:20px;flex-wrap:wrap}
.alb-detail-meta span{display:flex;align-items:center;gap:3px}
.alb-add-photos-btn{display:flex;align-items:center;justify-content:center;gap:5px;padding:8px;background:var(--surface2);border:1px dashed var(--border2);border-radius:8px;font-size:.72rem;color:var(--accent);margin-bottom:10px;transition:all .15s}
.alb-add-photos-btn:hover{background:var(--accent-dim);border-color:var(--accent)}
Expand Down Expand Up @@ -193,6 +195,9 @@ input,textarea,select{font-family:var(--font)}
.tile-spinner{width:14px;height:14px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .6s linear infinite;opacity:0;transition:opacity .2s;flex-shrink:0}
.tile-spinner.active{opacity:1}
@keyframes spin{to{transform:rotate(360deg)}}
#map-loading{position:absolute;inset:0;z-index:5;display:flex;align-items:center;justify-content:center;background:var(--bg);opacity:1;transition:opacity .4s ease}
#map-loading.done{opacity:0;pointer-events:none}
.map-loading-spinner{width:64px;height:64px;border:4px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .6s linear infinite}

/* MAP STYLE DROPDOWN */
.style-menu{display:none;position:absolute;top:calc(100% + 8px);left:50%;transform:translateX(-50%);background:var(--surface);border:1px solid var(--border);border-radius:8px;box-shadow:var(--shadow);z-index:50;overflow:hidden;min-width:120px}
Expand Down Expand Up @@ -240,7 +245,7 @@ input,textarea,select{font-family:var(--font)}
.gallery-cell{position:relative;aspect-ratio:1;overflow:hidden;background:var(--surface2);cursor:pointer;border-radius:8px;border:2px solid transparent;transition:border-color .15s}
.gallery-cell img{width:100%;height:100%;object-fit:cover;display:block;border-radius:6px}
.gallery-cell:hover{border-color:var(--accent)}
.gallery-rm{position:absolute;top:4px;right:4px;background:rgba(232,119,74,.88);border:none;color:#fff;border-radius:50%;width:17px;height:17px;font-size:.55rem;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .15s;line-height:1;cursor:pointer}
.gallery-rm{position:absolute;top:4px;right:4px;background:var(--accent2);border:none;color:#fff;border-radius:50%;width:17px;height:17px;font-size:.55rem;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .15s;line-height:1;cursor:pointer}
.gallery-cell:hover .gallery-rm{opacity:1}
.gallery-empty-msg{padding:20px 12px;text-align:center;font-size:.74rem;color:var(--muted);line-height:1.6}
.gallery-add-row{display:flex;align-items:center;justify-content:center;gap:6px;margin:7px 10px;padding:7px;background:var(--surface2);border:1px dashed var(--border2);border-radius:8px;font-size:.71rem;color:var(--accent);cursor:pointer;transition:all .15s;flex-shrink:0}
Expand Down
3 changes: 2 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
<div id="countries-bar">
<div class="cb-label">Countries Visited</div>
<div class="cb-flags collapsed" id="countries-flags"></div>
<button class="cb-toggle" id="countries-toggle" style="display:none" onclick="toggleCountriesBar()"></button>
<button class="cb-toggle" id="countries-toggle" style="display:none" onclick="toggleCountriesBar()" aria-label="Toggle countries">▾</button>
<div class="cb-status" id="cb-status">&nbsp;</div>
</div>

Expand Down Expand Up @@ -137,6 +137,7 @@

</div>
<div id="map"></div>
<div id="map-loading"><div class="map-loading-spinner"></div></div>
<div id="esri-attribution">&copy; <a href="https://www.esri.com/" target="_blank">Esri</a></div>
</div>
</div>
Expand Down
68 changes: 58 additions & 10 deletions js/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,31 @@ function updateCountriesBar() {
const isOverflowing = flagsEl.scrollHeight > flagsEl.clientHeight + 2;
toggle.style.display = isOverflowing || !flagsEl.classList.contains('collapsed') ? 'block' : 'none';
if (toggle.style.display === 'block') {
const collapsed = flagsEl.classList.contains('collapsed');
toggle.textContent = collapsed ? `Show all ${sorted.length} countries` : 'Show less';
toggle.classList.toggle('expanded', !flagsEl.classList.contains('collapsed'));
}
});
}

function toggleCountriesBar() {
const flagsEl = document.getElementById('countries-flags');
const toggle = document.getElementById('countries-toggle');
flagsEl.classList.toggle('collapsed');
const collapsed = flagsEl.classList.contains('collapsed');
const count = flagsEl.querySelectorAll('span[data-name]').length;
toggle.textContent = collapsed ? `Show all ${count} countries` : 'Show less';
if (collapsed) {
// Expand: set max-height to actual content height for smooth animation
flagsEl.style.maxHeight = flagsEl.scrollHeight + 'px';
flagsEl.classList.remove('collapsed');
toggle.classList.add('expanded');
// After transition, remove inline max-height so it can grow if flags change
setTimeout(() => { flagsEl.style.maxHeight = ''; }, 300);
} else {
// Collapse: set current height first, then animate to collapsed height
flagsEl.style.maxHeight = flagsEl.scrollHeight + 'px';
requestAnimationFrame(() => {
flagsEl.classList.add('collapsed');
flagsEl.style.maxHeight = '';
toggle.classList.remove('expanded');
});
}
}
// Country flag hover → show name in status bar
(function() {
Expand Down Expand Up @@ -135,13 +147,44 @@ async function _decompressGzip(blob) {
// ═══════════════════════════════════════
// EXPORT DATA
// ═══════════════════════════════════════
async function exportData() {
function exportData() {
document.getElementById('settings-dropdown').classList.remove('open');
if (!photos.length && !albums.length) { showToast('No data to export','error'); return; }
showProg(true);
updProg(5, 'Preparing data...');
// Defer the heavy work so the progress bar renders immediately
setTimeout(() => _doExport(), 50);
}
async function _doExport() {
const payload = { version: 1, exportedAt: Date.now(), photos, albums, geoCodeCache: {..._geoCodeCache}, geoCountryCache: {..._geoCountryCache} };
const json = JSON.stringify(payload);
// Compress with gzip to save disk space
const blob = await _compressGzip(json);
// Compress with gzip in chunks so we can report real progress
const encoder = new TextEncoder();
const totalBytes = json.length;
const chunkSize = 256 * 1024;
const cs = new CompressionStream('gzip');
const writer = cs.writable.getWriter();
const reader = cs.readable.getReader();
const compressed = [];
const readAll = (async () => {
while (true) {
const { done, value } = await reader.read();
if (done) break;
compressed.push(value);
}
})();
let written = 0;
for (let i = 0; i < totalBytes; i += chunkSize) {
await writer.write(encoder.encode(json.slice(i, i + chunkSize)));
written = Math.min(i + chunkSize, totalBytes);
updProg(10 + Math.round(written / totalBytes * 85), `Compressing... ${Math.round(written / totalBytes * 100)}%`);
}
await writer.close();
await readAll;
const blob = new Blob(compressed);
updProg(100, 'Done');
await new Promise(r => setTimeout(r, 2000));
showProg(false);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
const d = new Date();
Expand Down Expand Up @@ -184,6 +227,11 @@ _importInput.addEventListener('change', async e => {
showToast('Invalid backup file — missing photos or albums','error');
return;
}
const totalPhotos = data.photos.length;
const pinnedPhotos = data.photos.filter(p => p.lat !== null).length;
const totalAlbums = data.albums.length;
const summary = `Import summary:\n• ${totalPhotos} photo${totalPhotos !== 1 ? 's' : ''} (${pinnedPhotos} pinned)\n• ${totalAlbums} album${totalAlbums !== 1 ? 's' : ''}\n\nProceed with import?`;
if (!confirm(summary)) return;
await doImport(data);
} catch(err) {
showToast('Failed to read backup file','error');
Expand Down Expand Up @@ -375,7 +423,7 @@ async function checkAutoRestore() {
if (_geoCodeCache[key]) { p.countryCode = _geoCodeCache[key]; dbPut('photos', p); ccFilled++; }
}
}
if (ccFilled) updateCountriesBar();
if (ccFilled) { updateCountriesBar(); buildClusterIndex(); }
// Mark photos as already on disk so auto-save doesn't re-upload them
photos.forEach(p => _savedPhotoDisk.add(p.id));

Expand Down Expand Up @@ -480,7 +528,7 @@ async function init() {
const key = `${p.lat.toFixed(4)}_${p.lng.toFixed(4)}`;
if (_geoCodeCache[key]) { p.countryCode = _geoCodeCache[key]; dbPut('photos', p); filled++; }
}
if (filled) { updateCountriesBar(); scheduleAutoSave(); }
if (filled) { updateCountriesBar(); scheduleAutoSave(); buildClusterIndex(); }
}, 500);
// Proactive tile caching — start after a short delay so it doesn't compete with initial load
setTimeout(() => cacheMapTiles(), 10000);
Expand Down
59 changes: 54 additions & 5 deletions js/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ function _patchStyleWater(styleObj) {
styleObj.layers = styleObj.layers.filter(l => l.id !== 'natural_earth');
if (styleObj.sources) delete styleObj.sources['ne2_shaded'];
}
// Apple Maps Dark palette — neutral dark land, distinctly blue water.
// Colors pre-compensated for canvas filter brightness(1.8) contrast(0.9).
if (_mapStyle === 'dark') {
for (const layer of styleObj.layers) {
if (!layer.paint) layer.paint = {};
if (layer.type === 'fill' && /^water/.test(layer.id)) {
layer.paint['fill-color'] = '#131619';
}
}
}
const color = _mapStyle === 'dark' ? '#6a9fd8' : '#2c5f8a';
let hasPointLabel = false;
for (const layer of styleObj.layers) {
Expand All @@ -108,10 +118,15 @@ function _patchStyleWater(styleObj) {
}
// Hide major city labels at zoom 10+ so they don't mislead when right-clicking returns a sub-area
const cityLayers = ['place_city', 'place_city_large', 'label_city', 'label_city_capital'];
// Show state/province labels only at zoom 2.5+
const stateLayers = ['place_state', 'label_state'];
for (const layer of styleObj.layers) {
if (cityLayers.includes(layer.id)) {
layer.maxzoom = 10;
}
if (stateLayers.includes(layer.id)) {
layer.minzoom = 2.5;
}
}

// Dark style lacks a point-based water label layer — add one for ocean names
Expand Down Expand Up @@ -331,7 +346,10 @@ async function _doStyleSwap(style) {
styleObj = JSON.parse(JSON.stringify(_styleJsonCache[style]));
} else {
try {
const r = await fetch(style);
const ac = new AbortController();
const timer = setTimeout(() => ac.abort(), 5000);
const r = await fetch(style, { signal: ac.signal });
clearTimeout(timer);
const json = await r.json();
_styleJsonCache[style] = json;
styleObj = JSON.parse(JSON.stringify(json));
Expand Down Expand Up @@ -369,6 +387,8 @@ async function _doStyleSwap(style) {
} else {
buildClusterIndex();
}
// Update dark-map CSS class after pin icons are re-added with correct compensation
document.getElementById('map')?.classList.toggle('dark-map', _mapStyle === 'dark');
};
map.once('styledata', () => setTimeout(restore, 100));
setTimeout(restore, 600);
Expand All @@ -382,7 +402,10 @@ async function initMap() {
const styleUrl = _styleUrl();
let initStyle;
try {
const r = await fetch(styleUrl);
const ac = new AbortController();
const timer = setTimeout(() => ac.abort(), 5000);
const r = await fetch(styleUrl, { signal: ac.signal });
clearTimeout(timer);
const json = await r.json();
_styleJsonCache[styleUrl] = json; // seed cache so first style switch is instant
initStyle = JSON.parse(JSON.stringify(json));
Expand All @@ -394,6 +417,24 @@ async function initMap() {
fetch(otherUrl).then(r => r.json()).then(j => { _styleJsonCache[otherUrl] = j; }).catch(() => {});
map = new maplibregl.Map({ container:'map', style: initStyle, center:[0,20], zoom:1.8, attributionControl:false, preserveDrawingBuffer:true });
map.addControl(new maplibregl.NavigationControl({showCompass:false}), 'bottom-right');
// Recover from WebGL context loss (Safari loses context after sleep or memory pressure)
const canvas = map.getCanvas();
canvas.addEventListener('webglcontextlost', (e) => {
e.preventDefault(); // allow context to be restored
console.warn('WebGL context lost — waiting for restore');
});
canvas.addEventListener('webglcontextrestored', () => {
console.log('WebGL context restored — reinitializing map');
map.triggerRepaint();
});
// Safety net: if map is still blank after 8 seconds, show a reload prompt
setTimeout(() => {
const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
if (!gl || gl.isContextLost()) {
console.warn('Map canvas has no WebGL context — prompting reload');
showToast('Map failed to load — please refresh the page', 'error');
}
}, 8000);
// Provide a transparent 1x1 placeholder for any missing sprite images (e.g. POI icons)
map.on('styleimagemissing', (e) => {
if (!map.hasImage(e.id)) {
Expand Down Expand Up @@ -439,13 +480,18 @@ async function initMap() {
// Tile loading spinner
const tileSpinner = document.getElementById('tile-spinner');
map.on('dataloading', () => { tileSpinner?.classList.add('active'); });
map.on('idle', () => { tileSpinner?.classList.remove('active'); });
map.on('idle', () => {
tileSpinner?.classList.remove('active');
const mapLoading = document.getElementById('map-loading');
if (mapLoading) { mapLoading.classList.add('done'); setTimeout(() => mapLoading.remove(), 400); }
});
});
map.on('movestart', () => { _mapBusy = true; });
map.on('moveend', () => { _mapBusy = false; });

// Right-click on map to pin a location
map.on('contextmenu', async (e) => {
try {
e.preventDefault();
// Detect water vs land early — water clicks are allowed at any zoom,
// land clicks require zoom >= 7 for meaningful Nominatim results.
Expand Down Expand Up @@ -513,6 +559,7 @@ async function initMap() {
popup.on('close', () => { if (destMarkerObj) { destMarkerObj.marker.remove(); destMarkerObj = null; } });

destMarkerObj = { marker, popup };
} catch(err) { console.error('[right-click] ERROR in handler:', err); }
});

// Window-level capture handler — fires before anything else can intercept.
Expand Down Expand Up @@ -783,13 +830,15 @@ function toggleStyleMenu(e) {
}

function setMapStyle(mode) {
const wasDark = _mapStyle === 'dark';
_mapStyle = mode;
// Persist style preference (satellite resets to previous on reload)
if (mode !== 'satellite') localStorage.setItem('matrix-theme', mode);

// Update CSS classes
// Defer dark-map CSS class removal until pin icons are re-added with correct
// compensation (otherwise pre-darkened images render without the CSS filter)
const mapEl = document.getElementById('map');
mapEl.classList.toggle('dark-map', _mapStyle === 'dark');
if (_mapStyle === 'dark') mapEl.classList.add('dark-map');
mapEl.classList.toggle('sat-mode', _mapStyle === 'satellite');

// Labels toggle visibility
Expand Down
Loading
Loading