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 { useOptionalI18n } from '@map/composables/useOptionalT.js'
|
||||
import { tStatic } from '@map/i18n/index.js'
|
||||
import { useMapInstance, engineGlobal } from '@map/composables/useMapInstance.js'
|
||||
import { resolveMapProvider, setProjectionOn } from '@map/composables/useMapProvider.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
|
||||
// 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 () => {
|
||||
try {
|
||||
const rows = await rowsPromise
|
||||
|
|
|
|||
|
|
@ -574,7 +574,14 @@ function onDeleteSelected() {
|
|||
}
|
||||
function onSelectAll() { polygons.forEach(p => { p.selected = true }); refreshMapSources() }
|
||||
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 commitRename(id, value) {
|
||||
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-accent{background:#92b195;border-color:#92b195;color:#fff}' +
|
||||
'.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.is-danger{color:#8a3b3b;border-color:#e0c4c4}' +
|
||||
'.pwp-spacer{flex:1}' +
|
||||
|
|
@ -692,7 +703,8 @@ useShadowStyles(
|
|||
</div>
|
||||
<div class="pwp-spacer"></div>
|
||||
<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')">
|
||||
{{ tr('printSelected') }}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
|||
import { setProjectionOn } from '@map/composables/useMapProvider.js'
|
||||
import { whenStyleReady } from './whenStyleReady.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'
|
||||
|
||||
const nt = useNeighborT()
|
||||
|
|
@ -124,7 +125,8 @@ onMounted(async () => {
|
|||
.catch((e) => { loadError.value = nt('neighbor.mob.loadError', { message: String(e.message || e) }) })
|
||||
await initializeMap()
|
||||
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, () => {
|
||||
setProjectionOn(m, 'mercator'); repaint(); frame()
|
||||
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 { whenStyleReady } from './whenStyleReady.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'
|
||||
|
||||
const nt = useNeighborT()
|
||||
|
|
@ -216,7 +217,8 @@ onMounted(async () => {
|
|||
.catch((e) => { loadError.value = nt('neighbor.mob.loadError', { message: String(e.message || e) }) })
|
||||
await initializeMap()
|
||||
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, () => {
|
||||
setProjectionOn(m, 'mercator')
|
||||
repaint(); frame()
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@
|
|||
<span class="btn-label">{{ isLandscape ? $t('wall.toPortrait') : $t('wall.toLandscape') }}</span>
|
||||
<span class="current-mode">{{ isLandscape ? $t('wall.currentLandscape') : $t('wall.currentPortrait') }}</span>
|
||||
</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>
|
||||
<span class="group-count">{{ $t('wall.groupsLoaded', { n: totalGroups }) }}</span>
|
||||
</div>
|
||||
|
|
@ -42,6 +44,7 @@ export default {
|
|||
inject: { profileConfig: { default: null } },
|
||||
data() {
|
||||
return {
|
||||
printing: false, // the HD poster print is loading photos (drives the button's busy state)
|
||||
svg: null,
|
||||
zoom: null,
|
||||
cardWidth: 50,
|
||||
|
|
@ -504,7 +507,7 @@ export default {
|
|||
|
||||
// Reconcile which cards hold their HD photo with what the view needs now (wallLod.js).
|
||||
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 t = this._lodT || wallD3().zoomTransform(el);
|
||||
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.
|
||||
async printWall() {
|
||||
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) => {
|
||||
if (n.getAttribute('href')) return res();
|
||||
n.addEventListener('load', res, { once: true });
|
||||
|
|
@ -533,7 +537,7 @@ export default {
|
|||
n.setAttribute('href', n.getAttribute('data-hd'));
|
||||
})));
|
||||
try { window.print(); } finally {
|
||||
this._printing = false;
|
||||
this.printing = false;
|
||||
for (const n of nodes) n.removeAttribute('href');
|
||||
this._hd = new Set();
|
||||
this.updateLod();
|
||||
|
|
|
|||
|
|
@ -79,6 +79,12 @@ body {
|
|||
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 {
|
||||
background: #0081d6;
|
||||
transform: translateY(-1px);
|
||||
|
|
|
|||
Loading…
Reference in a new issue