Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
632efeeab5
|
|||
|
deeea9961f
|
|||
|
7a109c9ba5
|
|||
|
10aae3c9b3
|
|||
|
b492e2aa89
|
|||
|
4c4a53ae42
|
|||
|
a240a5d199
|
|||
|
0332cf4c3c
|
|||
|
59c447fe1f
|
|||
|
1140ecfe41
|
|||
|
60936ed2f5
|
|||
|
ca82a029bc
|
|||
|
0630aed73d
|
@@ -15,6 +15,7 @@ export default class AppHeaderComponent extends Component {
|
||||
@service settings;
|
||||
@service nostrAuth;
|
||||
@service nostrData;
|
||||
@service mapUi;
|
||||
@tracked isUserMenuOpen = false;
|
||||
@tracked searchQuery = '';
|
||||
|
||||
@@ -22,6 +23,11 @@ export default class AppHeaderComponent extends Component {
|
||||
return !!this.searchQuery;
|
||||
}
|
||||
|
||||
get showQuickSearch() {
|
||||
const zoom = this.mapUi.currentZoom ?? 13;
|
||||
return this.settings.showQuickSearchButtons && zoom >= 12;
|
||||
}
|
||||
|
||||
@action
|
||||
toggleUserMenu() {
|
||||
this.isUserMenuOpen = !this.isUserMenuOpen;
|
||||
@@ -54,7 +60,7 @@ export default class AppHeaderComponent extends Component {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{{#if this.settings.showQuickSearchButtons}}
|
||||
{{#if this.showQuickSearch}}
|
||||
<div class="header-center {{if this.hasQuery 'searching'}}">
|
||||
<CategoryChips @onSelect={{this.handleChipSelect}} />
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ export default class AppMenuSettingsApis extends Component {
|
||||
<Icon @name="server" @size={{20}} />
|
||||
<span>API Providers</span>
|
||||
</summary>
|
||||
<div class="details-content">
|
||||
<div class="details-content form-layout">
|
||||
<div class="form-group">
|
||||
<label for="overpass-api">Overpass API Provider</label>
|
||||
<select
|
||||
|
||||
@@ -14,7 +14,7 @@ export default class AppMenuSettingsMapUi extends Component {
|
||||
<Icon @name="map" @size={{20}} />
|
||||
<span>Map & UI</span>
|
||||
</summary>
|
||||
<div class="details-content">
|
||||
<div class="details-content form-layout">
|
||||
<div class="form-group">
|
||||
<label for="show-quick-search">Quick search buttons visible</label>
|
||||
<select
|
||||
|
||||
@@ -108,7 +108,7 @@ export default class AppMenuSettingsNostr extends Component {
|
||||
<Icon @name="zap" @size={{20}} />
|
||||
<span>Nostr</span>
|
||||
</summary>
|
||||
<div class="details-content">
|
||||
<div class="details-content form-layout">
|
||||
<div class="form-group">
|
||||
<label for="nostr-photo-fallback-uploads">Upload photos to fallback
|
||||
servers</label>
|
||||
|
||||
53
app/components/dropdown-menu.gjs
Normal file
53
app/components/dropdown-menu.gjs
Normal file
@@ -0,0 +1,53 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
import { on } from '@ember/modifier';
|
||||
import Icon from '#components/icon';
|
||||
|
||||
export default class DropdownMenu extends Component {
|
||||
@tracked isOpen = false;
|
||||
|
||||
@action
|
||||
toggleMenu(e) {
|
||||
e?.stopPropagation();
|
||||
this.isOpen = !this.isOpen;
|
||||
}
|
||||
|
||||
@action
|
||||
closeMenu(e) {
|
||||
e?.stopPropagation();
|
||||
this.isOpen = false;
|
||||
}
|
||||
|
||||
get triggerIcon() {
|
||||
return this.args.triggerIcon || 'more-vertical';
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="dropdown-menu-container">
|
||||
<button
|
||||
class="dropdown-trigger-btn btn-press"
|
||||
type="button"
|
||||
title={{@triggerTitle}}
|
||||
{{on "click" this.toggleMenu}}
|
||||
>
|
||||
<Icon
|
||||
@name={{this.triggerIcon}}
|
||||
@size={{@iconSize}}
|
||||
@color={{@iconColor}}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{{#if this.isOpen}}
|
||||
<div class="dropdown-popover {{@popoverClass}}">
|
||||
{{yield this.closeMenu}}
|
||||
</div>
|
||||
<div
|
||||
class="menu-backdrop"
|
||||
{{on "click" this.closeMenu}}
|
||||
role="button"
|
||||
></div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
@@ -284,6 +284,7 @@ export default class MapComponent extends Component {
|
||||
// Initialize the UI service with the map center
|
||||
const initialCenter = toLonLat(view.getCenter());
|
||||
this.mapUi.updateCenter(initialCenter[1], initialCenter[0]);
|
||||
this.mapUi.updateZoom(view.getZoom());
|
||||
|
||||
apply(this.mapInstance, 'https://tiles.openfreemap.org/styles/liberty', {
|
||||
webfonts: 'data:text/css,',
|
||||
@@ -1046,6 +1047,7 @@ export default class MapComponent extends Component {
|
||||
const view = this.mapInstance.getView();
|
||||
const center = toLonLat(view.getCenter());
|
||||
this.mapUi.updateCenter(center[1], center[0]);
|
||||
this.mapUi.updateZoom(view.getZoom());
|
||||
|
||||
// If in creation mode, update the coordinates in the service AND the URL
|
||||
if (this.mapUi.isCreating) {
|
||||
|
||||
@@ -41,6 +41,40 @@ export default class NostrConnectComponent extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async copyConnectUri() {
|
||||
const text = this.nostrAuth.connectUri;
|
||||
|
||||
try {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} else {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.top = '0';
|
||||
textArea.style.left = '0';
|
||||
textArea.style.opacity = '0';
|
||||
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
const successful = document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
|
||||
if (!successful) {
|
||||
throw new Error('Fallback copy failed');
|
||||
}
|
||||
}
|
||||
this.toast.show('Connection link copied to clipboard');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy text: ', err);
|
||||
alert('Failed to copy link');
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="nostr-connect-modal">
|
||||
<h2>Connect with Nostr</h2>
|
||||
@@ -59,7 +93,7 @@ export default class NostrConnectComponent extends Component {
|
||||
class="btn btn-outline"
|
||||
type="button"
|
||||
disabled
|
||||
title="No Nostr extension found in your browser."
|
||||
title="No Nostr extension found in your browser"
|
||||
>
|
||||
Browser Extension (Not Found)
|
||||
</button>
|
||||
@@ -79,9 +113,20 @@ export default class NostrConnectComponent extends Component {
|
||||
{{#if this.nostrAuth.isMobile}}
|
||||
<p>Waiting for you to approve the connection in your mobile signer
|
||||
app...</p>
|
||||
<div class="mobile-connect-actions">
|
||||
<a href={{this.nostrAuth.connectUri}} class="btn btn-primary">
|
||||
Open Signer App
|
||||
</a>
|
||||
<button
|
||||
class="btn btn-outline"
|
||||
type="button"
|
||||
{{on "click" this.copyConnectUri}}
|
||||
>
|
||||
Copy Connection Link
|
||||
</button>
|
||||
</div>
|
||||
{{else}}
|
||||
<p>Scan this QR code with a compatible Nostr signer app (like
|
||||
Amber):</p>
|
||||
<p>Scan this QR code with a Nostr signer app (like Amber):</p>
|
||||
<div class="qr-code-container">
|
||||
<canvas {{qrCode this.nostrAuth.connectUri}}></canvas>
|
||||
</div>
|
||||
|
||||
@@ -33,6 +33,14 @@ export default class PhotoCarousel extends Component {
|
||||
return !this.canScrollRight;
|
||||
}
|
||||
|
||||
get isGalleryMain() {
|
||||
return this.args.variant === 'gallery-main';
|
||||
}
|
||||
|
||||
get isGalleryThumbnails() {
|
||||
return this.args.variant === 'gallery-thumbnails';
|
||||
}
|
||||
|
||||
get variantClass() {
|
||||
return this.args.variant || 'inline';
|
||||
}
|
||||
@@ -205,29 +213,47 @@ export default class PhotoCarousel extends Component {
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if photo.isLandscape}}
|
||||
<picture>
|
||||
{{#if photo.thumbUrl}}
|
||||
<source
|
||||
media="(max-width: 768px)"
|
||||
data-srcset={{photo.thumbUrl}}
|
||||
/>
|
||||
{{/if}}
|
||||
<img
|
||||
data-src={{photo.url}}
|
||||
class="place-header-photo landscape"
|
||||
alt={{@name}}
|
||||
{{fadeInImage photo.url}}
|
||||
/>
|
||||
</picture>
|
||||
{{else}}
|
||||
{{! Portrait uses thumb everywhere if available }}
|
||||
{{#if this.isGalleryMain}}
|
||||
<img
|
||||
data-src={{photo.url}}
|
||||
class="place-header-photo
|
||||
{{if photo.isLandscape 'landscape' 'portrait'}}"
|
||||
alt={{@name}}
|
||||
{{fadeInImage photo.url}}
|
||||
/>
|
||||
{{else if this.isGalleryThumbnails}}
|
||||
<img
|
||||
data-src={{if photo.thumbUrl photo.thumbUrl photo.url}}
|
||||
class="place-header-photo portrait"
|
||||
class="place-header-photo
|
||||
{{if photo.isLandscape 'landscape' 'portrait'}}"
|
||||
alt={{@name}}
|
||||
{{fadeInImage (if photo.thumbUrl photo.thumbUrl photo.url)}}
|
||||
/>
|
||||
{{else}}
|
||||
{{#if photo.isLandscape}}
|
||||
<picture>
|
||||
{{#if photo.thumbUrl}}
|
||||
<source
|
||||
media="(max-width: 768px)"
|
||||
data-srcset={{photo.thumbUrl}}
|
||||
/>
|
||||
{{/if}}
|
||||
<img
|
||||
data-src={{photo.url}}
|
||||
class="place-header-photo landscape"
|
||||
alt={{@name}}
|
||||
{{fadeInImage photo.url}}
|
||||
/>
|
||||
</picture>
|
||||
{{else}}
|
||||
{{! Portrait uses thumb everywhere if available }}
|
||||
<img
|
||||
data-src={{if photo.thumbUrl photo.thumbUrl photo.url}}
|
||||
class="place-header-photo portrait"
|
||||
alt={{@name}}
|
||||
{{fadeInImage (if photo.thumbUrl photo.thumbUrl photo.url)}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { on } from '@ember/modifier';
|
||||
import Icon from './icon';
|
||||
import { fn } from '@ember/helper';
|
||||
import Icon from '#components/icon';
|
||||
import PhotoCarousel from './photo-carousel';
|
||||
import DropdownMenu from '#components/dropdown-menu';
|
||||
|
||||
export default class PhotoGallery extends Component {
|
||||
@service toast;
|
||||
@tracked currentPhoto = this.args.selectedPhoto || this.args.photos?.[0];
|
||||
|
||||
@action
|
||||
@@ -21,7 +25,8 @@ export default class PhotoGallery extends Component {
|
||||
if (
|
||||
e.target.closest('.thumbnail-strip-container') ||
|
||||
e.target.closest('.carousel-nav-btn') ||
|
||||
e.target.closest('.close-btn')
|
||||
e.target.closest('.close-btn') ||
|
||||
e.target.closest('.actions-btn-container')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -41,6 +46,20 @@ export default class PhotoGallery extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async copyEventId(closeMenu) {
|
||||
if (this.currentPhoto?.eventId) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.currentPhoto.eventId);
|
||||
this.toast.show('Event ID copied to clipboard');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy event ID:', err);
|
||||
this.toast.show('Failed to copy event ID');
|
||||
}
|
||||
}
|
||||
closeMenu();
|
||||
}
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="photo-gallery-overlay"
|
||||
@@ -50,6 +69,26 @@ export default class PhotoGallery extends Component {
|
||||
>
|
||||
{{! template-lint-disable no-invalid-interactive }}
|
||||
<div class="photo-gallery-content">
|
||||
<div class="actions-btn-container">
|
||||
<DropdownMenu
|
||||
@iconSize={{24}}
|
||||
@triggerIcon="more-horizontal"
|
||||
@iconColor="white"
|
||||
as |closeMenu|
|
||||
>
|
||||
<button
|
||||
class="dropdown-item"
|
||||
type="button"
|
||||
{{on "click" (fn this.copyEventId closeMenu)}}
|
||||
>Copy Photo Event ID</button>
|
||||
<button
|
||||
class="dropdown-item"
|
||||
type="button"
|
||||
{{on "click" closeMenu}}
|
||||
>Report Photo</button>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="close-btn btn-text"
|
||||
|
||||
@@ -29,6 +29,14 @@ export default class PlaceDetails extends Component {
|
||||
@tracked isConnectingNostr = false;
|
||||
@tracked isGalleryOpen = false;
|
||||
@tracked selectedGalleryPhoto = null;
|
||||
@tracked isPhotoUploadModalOpen = false;
|
||||
@tracked isNostrConnectModalOpen = false;
|
||||
@tracked newlyUploadedPhotoId = null;
|
||||
|
||||
@action
|
||||
handleUploadStateChange(isActive) {
|
||||
this.isPhotoUploadActive = isActive;
|
||||
}
|
||||
|
||||
@action
|
||||
openPhotoUploadModal(e) {
|
||||
|
||||
@@ -11,6 +11,7 @@ export default class MapUiService extends Service {
|
||||
@tracked returnToSearch = false;
|
||||
@tracked currentCenter = null;
|
||||
@tracked currentBounds = null;
|
||||
@tracked currentZoom = null;
|
||||
@tracked searchBoxHasFocus = false;
|
||||
@tracked selectionOptions = {};
|
||||
@tracked preventNextZoom = false;
|
||||
@@ -81,6 +82,10 @@ export default class MapUiService extends Service {
|
||||
this.currentCenter = { lat, lon };
|
||||
}
|
||||
|
||||
updateZoom(zoom) {
|
||||
this.currentZoom = zoom;
|
||||
}
|
||||
|
||||
updateBounds(bounds) {
|
||||
this.currentBounds = bounds;
|
||||
}
|
||||
|
||||
@@ -594,6 +594,9 @@ body {
|
||||
padding: 0 1.4rem 1rem;
|
||||
animation: details-slide-down 0.2s ease-out;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.sidebar-content details .details-content.form-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
@@ -1081,6 +1084,7 @@ abbr[title] {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
@@ -1777,6 +1781,13 @@ button.create-place {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.mobile-connect-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.nostr-connect-status {
|
||||
margin-top: 1.5rem;
|
||||
text-align: center;
|
||||
@@ -2016,3 +2027,63 @@ button.create-place {
|
||||
.photo-carousel.gallery-thumbnails .carousel-nav-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Dropdown Menu Component */
|
||||
.dropdown-menu-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdown-trigger-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdown-popover {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
margin-top: 5px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
|
||||
padding: 0.5rem 0;
|
||||
z-index: 3001;
|
||||
min-width: 150px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
/* Actions button in photo gallery */
|
||||
.photo-gallery-overlay .actions-btn-container {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
left: 0.5rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ import mail from 'feather-icons/dist/icons/mail.svg?raw';
|
||||
import map from 'feather-icons/dist/icons/map.svg?raw';
|
||||
import mapPin from 'feather-icons/dist/icons/map-pin.svg?raw';
|
||||
import menu from 'feather-icons/dist/icons/menu.svg?raw';
|
||||
import moreHorizontal from 'feather-icons/dist/icons/more-horizontal.svg?raw';
|
||||
import moreVertical from 'feather-icons/dist/icons/more-vertical.svg?raw';
|
||||
import navigation from 'feather-icons/dist/icons/navigation.svg?raw';
|
||||
import phone from 'feather-icons/dist/icons/phone.svg?raw';
|
||||
import plus from 'feather-icons/dist/icons/plus.svg?raw';
|
||||
@@ -81,10 +83,6 @@ import iceCreamOnCone from '@waysidemapping/pinhead/dist/icons/ice_cream_on_cone
|
||||
import industrialBuilding from '@waysidemapping/pinhead/dist/icons/industrial_building.svg?raw';
|
||||
import jewel from '@waysidemapping/pinhead/dist/icons/jewel.svg?raw';
|
||||
import lowriseBuilding from '@waysidemapping/pinhead/dist/icons/lowrise_building.svg?raw';
|
||||
import marketStall from '@waysidemapping/pinhead/dist/icons/market_stall.svg?raw';
|
||||
import memorialStoneWithInscription from '@waysidemapping/pinhead/dist/icons/memorial_stone_with_inscription.svg?raw';
|
||||
import mobilePhoneWithKeypadAndAntenna from '@waysidemapping/pinhead/dist/icons/mobile_phone_with_keypad_and_antenna.svg?raw';
|
||||
import molarTooth from '@waysidemapping/pinhead/dist/icons/molar_tooth.svg?raw';
|
||||
import needleAndSpoolOfThread from '@waysidemapping/pinhead/dist/icons/needle_and_spool_of_thread.svg?raw';
|
||||
import openBook from '@waysidemapping/pinhead/dist/icons/open_book.svg?raw';
|
||||
import palace from '@waysidemapping/pinhead/dist/icons/palace.svg?raw';
|
||||
@@ -193,11 +191,9 @@ const ICONS = {
|
||||
mail,
|
||||
map,
|
||||
'map-pin': mapPin,
|
||||
'market-stall': marketStall,
|
||||
'memorial-stone-with-inscription': memorialStoneWithInscription,
|
||||
menu,
|
||||
'mobile-phone-with-keypad-and-antenna': mobilePhoneWithKeypadAndAntenna,
|
||||
'molar-tooth': molarTooth,
|
||||
'more-horizontal': moreHorizontal,
|
||||
'more-vertical': moreVertical,
|
||||
navigation,
|
||||
'needle-and-spool-of-thread': needleAndSpoolOfThread,
|
||||
nostrich,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "marco",
|
||||
"version": "1.21.0",
|
||||
"version": "1.21.3",
|
||||
"private": true,
|
||||
"description": "Unhosted maps app",
|
||||
"repository": {
|
||||
|
||||
File diff suppressed because one or more lines are too long
1
release/assets/main-BmLeTC2Y.css
Normal file
1
release/assets/main-BmLeTC2Y.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -39,8 +39,8 @@
|
||||
<meta name="msapplication-TileColor" content="#F6E9A6">
|
||||
<meta name="msapplication-TileImage" content="/icons/icon-144.png">
|
||||
|
||||
<script type="module" crossorigin src="/assets/main-B30qTale.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-48yHGHPo.css">
|
||||
<script type="module" crossorigin src="/assets/main-C_1D7C3-.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-BmLeTC2Y.css">
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { babel } from '@rollup/plugin-babel';
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
host: '127.0.0.1',
|
||||
host: '0.0.0.0',
|
||||
},
|
||||
plugins: [
|
||||
ember(),
|
||||
|
||||
Reference in New Issue
Block a user