Map errors in every language, and print buttons that show they are busy (UPG Neighbor, All People Groups, Prayer Walk Planner, Wall)
This commit is contained in:
parent
01b02e3781
commit
786a834610
6 changed files with 37 additions and 9 deletions
|
|
@ -31,6 +31,7 @@
|
||||||
*/
|
*/
|
||||||
import { inject, ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
import { inject, ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { useOptionalI18n } from '@map/composables/useOptionalT.js'
|
import { useOptionalI18n } from '@map/composables/useOptionalT.js'
|
||||||
|
import { tStatic } from '@map/i18n/index.js'
|
||||||
import { useMapInstance, engineGlobal } from '@map/composables/useMapInstance.js'
|
import { useMapInstance, engineGlobal } from '@map/composables/useMapInstance.js'
|
||||||
import { resolveMapProvider, setProjectionOn } from '@map/composables/useMapProvider.js'
|
import { resolveMapProvider, setProjectionOn } from '@map/composables/useMapProvider.js'
|
||||||
import { useMapData } from '@map/composables/useMapData.js'
|
import { useMapData } from '@map/composables/useMapData.js'
|
||||||
|
|
@ -354,7 +355,8 @@ onMounted(async () => {
|
||||||
}
|
}
|
||||||
// No map: if the pane is still here that is a real failure (token, container) and says so; if it
|
// No map: if the pane is still here that is a real failure (token, container) and says so; if it
|
||||||
// was unmounted while initialising, useMapInstance returned quietly and so do we.
|
// was unmounted while initialising, useMapInstance returned quietly and so do we.
|
||||||
if (!map.value) { if (mapContainer.value) mapError.value = 'Map failed to initialize — check token and container'; return }
|
// Shown after tr('loadError'); this detail used to be English in every locale.
|
||||||
|
if (!map.value) { if (mapContainer.value) mapError.value = tStatic('messages.mapUnavailable'); return }
|
||||||
map.value.once('style.load', async () => {
|
map.value.once('style.load', async () => {
|
||||||
try {
|
try {
|
||||||
const rows = await rowsPromise
|
const rows = await rowsPromise
|
||||||
|
|
|
||||||
|
|
@ -574,7 +574,14 @@ function onDeleteSelected() {
|
||||||
}
|
}
|
||||||
function onSelectAll() { polygons.forEach(p => { p.selected = true }); refreshMapSources() }
|
function onSelectAll() { polygons.forEach(p => { p.selected = true }); refreshMapSources() }
|
||||||
function onClearSelection() { clearSelectionInternal() }
|
function onClearSelection() { clearSelectionInternal() }
|
||||||
function onPrintClick() { printInternal(null) }
|
// Printing renders one off-screen map per selected zone and waits for its tiles: seconds, not a
|
||||||
|
// click. The button shows it is busy and cannot start a second print meanwhile.
|
||||||
|
const printBusy = ref(false)
|
||||||
|
async function onPrintClick() {
|
||||||
|
if (printBusy.value) return
|
||||||
|
printBusy.value = true
|
||||||
|
try { await printInternal(null) } finally { printBusy.value = false }
|
||||||
|
}
|
||||||
function startRename(id) { editingTitleId.value = id }
|
function startRename(id) { editingTitleId.value = id }
|
||||||
function commitRename(id, value) {
|
function commitRename(id, value) {
|
||||||
const p = polygons.find(p => p.id === id)
|
const p = polygons.find(p => p.id === id)
|
||||||
|
|
@ -620,6 +627,10 @@ useShadowStyles(
|
||||||
'.pwp-btn.is-active{background:#92b195;border-color:#92b195;color:#fff}' +
|
'.pwp-btn.is-active{background:#92b195;border-color:#92b195;color:#fff}' +
|
||||||
'.pwp-btn.is-accent{background:#92b195;border-color:#92b195;color:#fff}' +
|
'.pwp-btn.is-accent{background:#92b195;border-color:#92b195;color:#fff}' +
|
||||||
'.pwp-btn.is-accent:hover{opacity:.9}' +
|
'.pwp-btn.is-accent:hover{opacity:.9}' +
|
||||||
|
'.pwp-btn.busy{cursor:progress;opacity:.75}' +
|
||||||
|
".pwp-btn.busy::after{content:'';display:inline-block;width:.8em;height:.8em;margin-inline-start:.5em;vertical-align:-.1em;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:pwp-spin .8s linear infinite}" +
|
||||||
|
'@keyframes pwp-spin{to{transform:rotate(360deg)}}' +
|
||||||
|
'@media (prefers-reduced-motion:reduce){.pwp-btn.busy::after{animation:none}}' +
|
||||||
'.pwp-btn:disabled{opacity:.45;cursor:not-allowed}' +
|
'.pwp-btn:disabled{opacity:.45;cursor:not-allowed}' +
|
||||||
'.pwp-btn.is-danger{color:#8a3b3b;border-color:#e0c4c4}' +
|
'.pwp-btn.is-danger{color:#8a3b3b;border-color:#e0c4c4}' +
|
||||||
'.pwp-spacer{flex:1}' +
|
'.pwp-spacer{flex:1}' +
|
||||||
|
|
@ -692,7 +703,8 @@ useShadowStyles(
|
||||||
</div>
|
</div>
|
||||||
<div class="pwp-spacer"></div>
|
<div class="pwp-spacer"></div>
|
||||||
<span class="pwp-count">{{ tr('countSelected', { n: selectedCount, total: polygons.length }) }}</span>
|
<span class="pwp-count">{{ tr('countSelected', { n: selectedCount, total: polygons.length }) }}</span>
|
||||||
<button type="button" class="pwp-btn is-accent pwp-print" :disabled="selectedCount === 0"
|
<button type="button" class="pwp-btn is-accent pwp-print" :class="{ busy: printBusy }"
|
||||||
|
:disabled="selectedCount === 0 || printBusy" :aria-busy="printBusy ? 'true' : 'false'"
|
||||||
@click="onPrintClick" :aria-label="tr('printAria')">
|
@click="onPrintClick" :aria-label="tr('printAria')">
|
||||||
{{ tr('printSelected') }}
|
{{ tr('printSelected') }}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
||||||
import { setProjectionOn } from '@map/composables/useMapProvider.js'
|
import { setProjectionOn } from '@map/composables/useMapProvider.js'
|
||||||
import { whenStyleReady } from './whenStyleReady.js'
|
import { whenStyleReady } from './whenStyleReady.js'
|
||||||
import { useNeighborT } from './composables/useNeighborT.js'
|
import { useNeighborT } from './composables/useNeighborT.js'
|
||||||
|
import { tStatic } from '@map/i18n/index.js'
|
||||||
import { loadNearData, E_COLORS, UNREACHED, NAVY, fold, fmtNum, downloadCsv } from './nearData.js'
|
import { loadNearData, E_COLORS, UNREACHED, NAVY, fold, fmtNum, downloadCsv } from './nearData.js'
|
||||||
|
|
||||||
const nt = useNeighborT()
|
const nt = useNeighborT()
|
||||||
|
|
@ -124,7 +125,8 @@ onMounted(async () => {
|
||||||
.catch((e) => { loadError.value = nt('neighbor.mob.loadError', { message: String(e.message || e) }) })
|
.catch((e) => { loadError.value = nt('neighbor.mob.loadError', { message: String(e.message || e) }) })
|
||||||
await initializeMap()
|
await initializeMap()
|
||||||
const m = map.value
|
const m = map.value
|
||||||
if (!m) { mapError.value = nt('messages.mapUnavailable'); return }
|
// A shared key, so tStatic: nt() would prefix it with neighbor. and never resolve.
|
||||||
|
if (!m) { mapError.value = tStatic('messages.mapUnavailable'); return }
|
||||||
whenStyleReady(m, () => {
|
whenStyleReady(m, () => {
|
||||||
setProjectionOn(m, 'mercator'); repaint(); frame()
|
setProjectionOn(m, 'mercator'); repaint(); frame()
|
||||||
m.on('click', 'cp-u', (ev) => {
|
m.on('click', 'cp-u', (ev) => {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
||||||
import { setProjectionOn } from '@map/composables/useMapProvider.js'
|
import { setProjectionOn } from '@map/composables/useMapProvider.js'
|
||||||
import { whenStyleReady } from './whenStyleReady.js'
|
import { whenStyleReady } from './whenStyleReady.js'
|
||||||
import { useNeighborT } from './composables/useNeighborT.js'
|
import { useNeighborT } from './composables/useNeighborT.js'
|
||||||
|
import { tStatic } from '@map/i18n/index.js'
|
||||||
import { loadNearData, E_COLORS, UNREACHED, REACHED, SELECTED, NAVY, fold, fmtNum, downloadCsv } from './nearData.js'
|
import { loadNearData, E_COLORS, UNREACHED, REACHED, SELECTED, NAVY, fold, fmtNum, downloadCsv } from './nearData.js'
|
||||||
|
|
||||||
const nt = useNeighborT()
|
const nt = useNeighborT()
|
||||||
|
|
@ -216,7 +217,8 @@ onMounted(async () => {
|
||||||
.catch((e) => { loadError.value = nt('neighbor.mob.loadError', { message: String(e.message || e) }) })
|
.catch((e) => { loadError.value = nt('neighbor.mob.loadError', { message: String(e.message || e) }) })
|
||||||
await initializeMap()
|
await initializeMap()
|
||||||
const m = map.value
|
const m = map.value
|
||||||
if (!m) { mapError.value = nt('messages.mapUnavailable'); return }
|
// A shared key, so tStatic: nt() would prefix it with neighbor. and never resolve.
|
||||||
|
if (!m) { mapError.value = tStatic('messages.mapUnavailable'); return }
|
||||||
whenStyleReady(m, () => {
|
whenStyleReady(m, () => {
|
||||||
setProjectionOn(m, 'mercator')
|
setProjectionOn(m, 'mercator')
|
||||||
repaint(); frame()
|
repaint(); frame()
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,9 @@
|
||||||
<span class="btn-label">{{ isLandscape ? $t('wall.toPortrait') : $t('wall.toLandscape') }}</span>
|
<span class="btn-label">{{ isLandscape ? $t('wall.toPortrait') : $t('wall.toLandscape') }}</span>
|
||||||
<span class="current-mode">{{ isLandscape ? $t('wall.currentLandscape') : $t('wall.currentPortrait') }}</span>
|
<span class="current-mode">{{ isLandscape ? $t('wall.currentLandscape') : $t('wall.currentPortrait') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button @click="printWall" class="control-btn">{{ $t('wall.printPoster') }}</button>
|
<!-- Loading every card's HD photo can take a while: the button shows it is busy and cannot
|
||||||
|
start a second load while the first is still running. -->
|
||||||
|
<button @click="printWall" class="control-btn" :class="{ busy: printing }" :disabled="printing" :aria-busy="printing ? 'true' : 'false'">{{ $t('wall.printPoster') }}</button>
|
||||||
<button @click="resetZoom" class="control-btn">{{ $t('wall.resetZoom') }}</button>
|
<button @click="resetZoom" class="control-btn">{{ $t('wall.resetZoom') }}</button>
|
||||||
<span class="group-count">{{ $t('wall.groupsLoaded', { n: totalGroups }) }}</span>
|
<span class="group-count">{{ $t('wall.groupsLoaded', { n: totalGroups }) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -42,6 +44,7 @@ export default {
|
||||||
inject: { profileConfig: { default: null } },
|
inject: { profileConfig: { default: null } },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
printing: false, // the HD poster print is loading photos (drives the button's busy state)
|
||||||
svg: null,
|
svg: null,
|
||||||
zoom: null,
|
zoom: null,
|
||||||
cardWidth: 50,
|
cardWidth: 50,
|
||||||
|
|
@ -504,7 +507,7 @@ export default {
|
||||||
|
|
||||||
// Reconcile which cards hold their HD photo with what the view needs now (wallLod.js).
|
// Reconcile which cards hold their HD photo with what the view needs now (wallLod.js).
|
||||||
updateLod() {
|
updateLod() {
|
||||||
if (!this._lodGrid || !this.svg || !this._imgNodes || this._printing) return;
|
if (!this._lodGrid || !this.svg || !this._imgNodes || this.printing) return;
|
||||||
const el = this.svg.node();
|
const el = this.svg.node();
|
||||||
const t = this._lodT || wallD3().zoomTransform(el);
|
const t = this._lodT || wallD3().zoomTransform(el);
|
||||||
const r = el.getBoundingClientRect();
|
const r = el.getBoundingClientRect();
|
||||||
|
|
@ -524,7 +527,8 @@ export default {
|
||||||
// THE HD POSTER PATH: load every photo, wait for them, print, then go back to level of detail.
|
// THE HD POSTER PATH: load every photo, wait for them, print, then go back to level of detail.
|
||||||
async printWall() {
|
async printWall() {
|
||||||
const nodes = (this._imgNodes || []).filter(Boolean);
|
const nodes = (this._imgNodes || []).filter(Boolean);
|
||||||
this._printing = true;
|
if (this.printing) return;
|
||||||
|
this.printing = true;
|
||||||
await Promise.all(nodes.map((n) => new Promise((res) => {
|
await Promise.all(nodes.map((n) => new Promise((res) => {
|
||||||
if (n.getAttribute('href')) return res();
|
if (n.getAttribute('href')) return res();
|
||||||
n.addEventListener('load', res, { once: true });
|
n.addEventListener('load', res, { once: true });
|
||||||
|
|
@ -533,7 +537,7 @@ export default {
|
||||||
n.setAttribute('href', n.getAttribute('data-hd'));
|
n.setAttribute('href', n.getAttribute('data-hd'));
|
||||||
})));
|
})));
|
||||||
try { window.print(); } finally {
|
try { window.print(); } finally {
|
||||||
this._printing = false;
|
this.printing = false;
|
||||||
for (const n of nodes) n.removeAttribute('href');
|
for (const n of nodes) n.removeAttribute('href');
|
||||||
this._hd = new Set();
|
this._hd = new Set();
|
||||||
this.updateLod();
|
this.updateLod();
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,12 @@ body {
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.control-btn.busy { opacity: .7; cursor: progress; }
|
||||||
|
.control-btn.busy::after { content: ''; display: inline-block; width: .8em; height: .8em; margin-inline-start: .5em;
|
||||||
|
vertical-align: -.1em; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%;
|
||||||
|
animation: wall-spin .8s linear infinite; }
|
||||||
|
@keyframes wall-spin { to { transform: rotate(360deg); } }
|
||||||
|
@media (prefers-reduced-motion: reduce) { .control-btn.busy::after { animation: none; } }
|
||||||
.control-btn:hover {
|
.control-btn:hover {
|
||||||
background: #0081d6;
|
background: #0081d6;
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue