Offline-capable geomap, meant for storing location bookmarks
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: bookmark cleanup

+77 -6
+1 -1
www/models/app.ts
··· 67 67 68 68 async updateBookmark( 69 69 id: string, 70 - updates: Partial<Pick<Bookmark, 'name' | 'address' | 'folderId'>>, 70 + updates: Partial<Pick<Bookmark, 'name' | 'address' | 'notes' | 'folderId'>>, 71 71 ): Promise<void> { 72 72 await this.store.updateBookmark(id, updates) 73 73 this.notify()
+1
www/models/schema/v0.ts
··· 10 10 id: z.string(), 11 11 name: z.string(), 12 12 address: z.optional(z.string()), 13 + notes: z.optional(z.string()), 13 14 lat: z.number(), 14 15 lng: z.number(), 15 16 zoom: z._default(z.number(), 12),
+1 -1
www/models/store.ts
··· 138 138 139 139 async updateBookmark( 140 140 id: string, 141 - updates: Partial<Pick<Bookmark, 'name' | 'address' | 'folderId'>>, 141 + updates: Partial<Pick<Bookmark, 'name' | 'address' | 'notes' | 'folderId'>>, 142 142 ): Promise<void> { 143 143 const data = await this.#sync.get() 144 144 const idx = (data.bookmarks ?? []).findIndex((b) => b.id === id)
+45 -4
www/routes/bookmarks.ts
··· 3 3 import type { Bookmark, BookmarkFolder } from '../models/schema.ts' 4 4 import { 5 5 type ImportResult, 6 + parseGeoJson, 6 7 parseGpx, 7 8 parseKml, 8 9 parseKmz, ··· 18 19 private editingBookmark: Bookmark | null = null 19 20 private pendingImport: ImportResult | null = null 20 21 private importError: string | null = null 22 + private importTargetFolderId: string | null = null 21 23 22 24 protected override createRenderRoot() { 23 25 return this ··· 73 75 const fd = new FormData(form) 74 76 const name = (fd.get('name') as string).trim() 75 77 const address = (fd.get('address') as string).trim() || undefined 78 + const notes = (fd.get('notes') as string).trim() || undefined 76 79 const folderId = (fd.get('folderId') as string) || null 77 80 if (!name) return 78 81 await app.updateBookmark(this.editingBookmark.id, { 79 82 name, 80 83 address, 84 + notes, 81 85 folderId, 82 86 }) 83 87 this.editingBookmark = null ··· 97 101 if (!file) return 98 102 input.value = '' 99 103 this.importError = null 104 + this.importTargetFolderId = null 100 105 try { 101 106 if (file.name.endsWith('.kmz')) { 102 107 this.pendingImport = await parseKmz(await file.arrayBuffer()) 103 108 } else if (file.name.endsWith('.gpx')) { 104 109 this.pendingImport = parseGpx(await file.text()) 110 + } else if (file.name.endsWith('.json')) { 111 + this.pendingImport = parseGeoJson(await file.text()) 105 112 } else { 106 113 this.pendingImport = parseKml(await file.text()) 107 114 } ··· 124 131 async #confirmImport() { 125 132 if (!this.pendingImport) return 126 133 const { folders, bookmarks } = this.pendingImport 134 + const targetFolderId = this.importTargetFolderId 127 135 this.pendingImport = null 136 + this.importTargetFolderId = null 128 137 this.requestUpdate() 129 138 const folderIdMap = new Map<string, string>() 130 139 for (const folder of folders) { ··· 134 143 } 135 144 for (const bm of bookmarks) { 136 145 if (bm.isDuplicate) continue 137 - const folderId = bm.folderTempId 138 - ? (folderIdMap.get(bm.folderTempId) ?? null) 139 - : null 146 + const folderId = targetFolderId ?? 147 + (bm.folderTempId ? (folderIdMap.get(bm.folderTempId) ?? null) : null) 140 148 await app.addBookmark(bm.name, bm.lat, bm.lng, 12, folderId, bm.address) 141 149 } 142 150 } ··· 208 216 <input 209 217 class="bm-file-input" 210 218 type="file" 211 - accept=".kml,.kmz,.gpx" 219 + accept=".kml,.kmz,.gpx,.json" 212 220 style="display:none" 213 221 @change="${this.#handleImport}" 214 222 > ··· 352 360 ?open="${this.pendingImport !== null}" 353 361 @dismiss="${() => { 354 362 this.pendingImport = null 363 + this.importTargetFolderId = null 355 364 this.requestUpdate() 356 365 }}" 357 366 > ··· 453 462 ` 454 463 : ''} 455 464 </div> 465 + ${this.pendingImport.folders.length === 0 && 466 + this.folders.length > 0 467 + ? html` 468 + <select 469 + .value="${this.importTargetFolderId ?? ''}" 470 + @change="${(e: Event) => { 471 + this.importTargetFolderId = 472 + (e.target as HTMLSelectElement).value || null 473 + }}" 474 + > 475 + <option value="">No Category</option> 476 + ${this.folders.map( 477 + (f) => html` 478 + <option 479 + value="${f.id}" 480 + ?selected="${this.importTargetFolderId === f.id}" 481 + > 482 + ${f.name} 483 + </option> 484 + `, 485 + )} 486 + </select> 487 + ` 488 + : ''} 456 489 <div class="bm-form-actions"> 457 490 <button @click="${this.#confirmImport}">Import</button> 458 491 <button 459 492 type="button" 460 493 @click="${() => { 461 494 this.pendingImport = null 495 + this.importTargetFolderId = null 462 496 this.requestUpdate() 463 497 }}" 464 498 > ··· 499 533 rows="3" 500 534 autocomplete="off" 501 535 .value="${this.editingBookmark.address ?? ''}" 536 + ></textarea> 537 + <textarea 538 + name="notes" 539 + placeholder="Notes" 540 + rows="4" 541 + autocomplete="off" 542 + .value="${this.editingBookmark.notes ?? ''}" 502 543 ></textarea> 503 544 <p class="bm-dialog-coords">${this.editingBookmark.lat 504 545 .toFixed(5)}, ${this.editingBookmark.lng.toFixed(
+29
www/utils/import_bookmarks.ts
··· 61 61 return { folders: [], bookmarks } 62 62 } 63 63 64 + export function parseGeoJson(text: string): ImportResult { 65 + const data = JSON.parse(text) 66 + const bookmarks: ParsedBookmark[] = [] 67 + 68 + const features = 69 + data.type === 'FeatureCollection' 70 + ? data.features 71 + : data.type === 'Feature' 72 + ? [data] 73 + : [] 74 + 75 + for (const feature of features) { 76 + if (feature?.geometry?.type !== 'Point') continue 77 + const [lng, lat] = feature.geometry.coordinates as [number, number] 78 + if (isNaN(lat) || isNaN(lng)) continue 79 + const props = feature.properties ?? {} 80 + const loc = props.location as Record<string, string> | undefined 81 + const name = ( 82 + (props.name ?? props.Name ?? loc?.name) as string | undefined 83 + )?.trim() || 'Unnamed' 84 + const address = ( 85 + (props.address ?? loc?.address ?? props.description) as string | undefined 86 + )?.trim() || undefined 87 + bookmarks.push({ name, lat, lng, folderTempId: null, address }) 88 + } 89 + 90 + return { folders: [], bookmarks } 91 + } 92 + 64 93 export async function parseKmz(buffer: ArrayBuffer): Promise<ImportResult> { 65 94 const files = unzipSync(new Uint8Array(buffer)) 66 95 const entry = Object.keys(files).find((k) => k === 'doc.kml') ??