This commit is contained in:
Christian Beutel
2024-09-06 13:25:08 +02:00
parent b29696f326
commit 3fefab45a2
12 changed files with 193 additions and 50 deletions

View File

@@ -0,0 +1,89 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
m "github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/models/schema"
)
func init() {
m.Register(func(db dbx.Builder) error {
dao := daos.New(db);
collection, err := dao.FindCollectionByNameOrId("e864strfxo14pm4")
if err != nil {
return err
}
// update
edit_photos := &schema.SchemaField{}
if err := json.Unmarshal([]byte(`{
"system": false,
"id": "aqbpyawe",
"name": "photos",
"type": "file",
"required": false,
"presentable": false,
"unique": false,
"options": {
"mimeTypes": [
"image/jpeg",
"image/vnd.mozilla.apng",
"image/png",
"image/webp",
"image/svg+xml",
"image/heic"
],
"thumbs": [],
"maxSelect": 99,
"maxSize": 5242880,
"protected": false
}
}`), edit_photos); err != nil {
return err
}
collection.Schema.AddField(edit_photos)
return dao.SaveCollection(collection)
}, func(db dbx.Builder) error {
dao := daos.New(db);
collection, err := dao.FindCollectionByNameOrId("e864strfxo14pm4")
if err != nil {
return err
}
// update
edit_photos := &schema.SchemaField{}
if err := json.Unmarshal([]byte(`{
"system": false,
"id": "aqbpyawe",
"name": "photos",
"type": "file",
"required": false,
"presentable": false,
"unique": false,
"options": {
"mimeTypes": [
"image/jpeg",
"image/vnd.mozilla.apng",
"image/png",
"image/webp",
"image/svg+xml"
],
"thumbs": [],
"maxSelect": 99,
"maxSize": 5242880,
"protected": false
}
}`), edit_photos); err != nil {
return err
}
collection.Schema.AddField(edit_photos)
return dao.SaveCollection(collection)
})
}

6
web/package-lock.json generated
View File

@@ -17,6 +17,7 @@
"@types/xmldom": "^0.1.34", "@types/xmldom": "^0.1.34",
"canvg": "^4.0.1", "canvg": "^4.0.1",
"crypto-random-string": "^5.0.0", "crypto-random-string": "^5.0.0",
"heic2any": "^0.0.4",
"instead": "^1.0.3", "instead": "^1.0.3",
"isomorphic-xml2js": "^0.1.3", "isomorphic-xml2js": "^0.1.3",
"jspdf": "^2.5.1", "jspdf": "^2.5.1",
@@ -2813,6 +2814,11 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/heic2any": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/heic2any/-/heic2any-0.0.4.tgz",
"integrity": "sha512-3lLnZiDELfabVH87htnRolZ2iehX9zwpRyGNz22GKXIu0fznlblf0/ftppXKNqS26dqFSeqfIBhAmAj/uSp0cA=="
},
"node_modules/html-encoding-sniffer": { "node_modules/html-encoding-sniffer": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",

View File

@@ -41,6 +41,7 @@
"@types/xmldom": "^0.1.34", "@types/xmldom": "^0.1.34",
"canvg": "^4.0.1", "canvg": "^4.0.1",
"crypto-random-string": "^5.0.0", "crypto-random-string": "^5.0.0",
"heic2any": "^0.0.4",
"instead": "^1.0.3", "instead": "^1.0.3",
"isomorphic-xml2js": "^0.1.3", "isomorphic-xml2js": "^0.1.3",
"jspdf": "^2.5.1", "jspdf": "^2.5.1",

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { getFileURL, readAsDataURLAsync } from "$lib/util/file_util"; import { getFileURL, readAsDataURLAsync } from "$lib/util/file_util";
import { onMount } from "svelte";
import PhotoCard from "../photo_card.svelte"; import PhotoCard from "../photo_card.svelte";
export let id: string; export let id: string;
@@ -42,7 +43,7 @@
document.getElementById(`${id}-photo-input`)!.click(); document.getElementById(`${id}-photo-input`)!.click();
} }
function handlePhotoSelection(files?: FileList | null) { async function handlePhotoSelection(files?: FileList | null) {
if (!files) { if (!files) {
files = ( files = (
document.getElementById(`${id}-photo-input`) as HTMLInputElement document.getElementById(`${id}-photo-input`) as HTMLInputElement
@@ -54,13 +55,25 @@
} }
for (const file of files) { for (const file of files) {
let photoFile = file;
if (!file.type.startsWith("image")) { if (!file.type.startsWith("image")) {
continue; continue;
} else if (file.type === "image/heic") {
const heic2any = (await import("heic2any")).default
photoFile = new File(
[
(await heic2any({
blob: file,
toType: "image/jpeg",
})) as Blob,
],
file.name,
);
} }
if (!photoFiles) { if (!photoFiles) {
photoFiles = []; photoFiles = [];
} }
photoFiles = [...photoFiles, file]; photoFiles = [...photoFiles, photoFile];
} }
} }

View File

@@ -14,6 +14,8 @@
$: thumbnail = trail.photos.length $: thumbnail = trail.photos.length
? getFileURL(trail, trail.photos[trail.thumbnail]) ? getFileURL(trail, trail.photos[trail.thumbnail])
: "/imgs/default_thumbnail.webp"; : "/imgs/default_thumbnail.webp";
$: trailIsShared = (trail.expand?.trail_share_via_trail?.length ?? 0) > 0;
</script> </script>
<div <div
@@ -27,12 +29,18 @@
> >
<img src={thumbnail} alt="" /> <img src={thumbnail} alt="" />
</div> </div>
{#if trail.public || trail.expand?.trail_share_via_trail?.length} {#if trail.public || trailIsShared}
<div <div
class="flex absolute top-4 right-4 w-8 h-8 rounded-full items-center justify-center bg-background text-content" class="flex absolute top-4 right-4 {trail.public && trailIsShared
? 'w-14'
: 'w-8'} h-8 rounded-full items-center justify-center bg-background text-content"
> >
{#if trail.public} {#if trail.public}
<span class="tooltip" data-title={$_("public")}> <span
class="tooltip"
class:mr-2={trail.public && trailIsShared}
data-title={$_("public")}
>
<i class="fa fa-globe"></i> <i class="fa fa-globe"></i>
</span> </span>
{/if} {/if}

View File

@@ -109,14 +109,23 @@
gpx(new DOMParser().parseFromString(fileData, "text/xml")), gpx(new DOMParser().parseFromString(fileData, "text/xml")),
); );
} }
if (!exportSettings.photos && !exportSettings.summitLog) {
const blob = new Blob([fileData], {
type: "text/plain",
});
saveAs(blob, `${trail.name}.${exportSettings.fileFormat}`);
} else {
const zip = new JSZip(); const zip = new JSZip();
zip.file(`${trail.name}.${exportSettings.fileFormat}`, fileData); zip.file(
`${trail.name}.${exportSettings.fileFormat}`,
fileData,
);
if (exportSettings.photos) { if (exportSettings.photos) {
const photoFolder = zip.folder($_("photos")); const photoFolder = zip.folder($_("photos"));
for (const photo of trail.photos) { for (const photo of trail.photos) {
const photoURL = getFileURL(trail, photo); const photoURL = getFileURL(trail, photo);
const photoBlob = await fetch(photoURL).then((response) => const photoBlob = await fetch(photoURL).then(
response.blob(), (response) => response.blob(),
); );
const photoData = new File([photoBlob], photo); const photoData = new File([photoBlob], photo);
photoFolder?.file(photo, photoData, { base64: true }); photoFolder?.file(photo, photoData, { base64: true });
@@ -134,6 +143,7 @@
} }
const blob = await zip.generateAsync({ type: "blob" }); const blob = await zip.generateAsync({ type: "blob" });
saveAs(blob, `${trail.name}.zip`); saveAs(blob, `${trail.name}.zip`);
}
} catch (e) { } catch (e) {
console.error(e); console.error(e);
show_toast({ show_toast({

View File

@@ -47,6 +47,9 @@
...($currentUser ? [$_("comment", { values: { n: 2 } })] : []), ...($currentUser ? [$_("comment", { values: { n: 2 } })] : []),
]; ];
const trailIsShared =
(trail.expand?.trail_share_via_trail?.length ?? 0) > 0;
let map: Map; let map: Map;
let activeTab = 0; let activeTab = 0;
@@ -161,7 +164,8 @@
</script> </script>
<div <div
class="trail-info-panel mx-auto border border-input-border rounded-3xl h-full" style="max-width: min(100%, 64rem);" class="trail-info-panel mx-auto border border-input-border rounded-3xl h-full"
style="max-width: min(100%, 64rem);"
> >
<div class="trail-info-panel-header"> <div class="trail-info-panel-header">
<section class="relative h-80"> <section class="relative h-80">
@@ -169,19 +173,23 @@
<div <div
class="absolute bottom-0 w-full h-1/2 bg-gradient-to-b from-transparent to-black opacity-50" class="absolute bottom-0 w-full h-1/2 bg-gradient-to-b from-transparent to-black opacity-50"
></div> ></div>
{#if trail.public || trail.expand?.trail_share_via_trail?.length} {#if trail.public || trailIsShared}
<div <div
class="flex absolute top-8 right-10 w-8 h-8 rounded-full items-center justify-center bg-white text-primary" class="flex absolute top-8 right-6 {trail.public &&
trailIsShared
? 'w-16'
: 'w-8'} h-8 rounded-full items-center justify-center bg-white text-primary"
> >
{#if trail.public} {#if trail.public}
<span <span
class="tooltip text-2xl" class="tooltip text-2xl"
class:mr-3={trail.public && trailIsShared}
data-title={$_("public")} data-title={$_("public")}
> >
<i class="fa fa-globe"></i> <i class="fa fa-globe"></i>
</span> </span>
{/if} {/if}
{#if trail.expand?.trail_share_via_trail?.length} {#if trailIsShared}
<TrailShareInfo {trail} large={true}></TrailShareInfo> <TrailShareInfo {trail} large={true}></TrailShareInfo>
{/if} {/if}
</div> </div>
@@ -266,9 +274,7 @@
<hr class="border-separator" /> <hr class="border-separator" />
{/if} {/if}
</div> </div>
<section <section class="trail-info-panel-tabs px-4 py-2 bg-background sticky top-0">
class="trail-info-panel-tabs px-4 py-2 bg-background sticky top-0"
>
<Tabs {tabs} bind:activeTab></Tabs> <Tabs {tabs} bind:activeTab></Tabs>
</section> </section>
<section class="trail-info-panel-content px-8"> <section class="trail-info-panel-content px-8">

View File

@@ -42,10 +42,10 @@
selectedDisplayOption = storedDisplayOption; selectedDisplayOption = storedDisplayOption;
} }
if (storedSort) { if (storedSort) {
filter.sort = storedSort as typeof filter.sort filter.sort = storedSort as typeof filter.sort;
} }
if (storedSortOrder) { if (storedSortOrder) {
filter.sortOrder = storedSortOrder as typeof filter.sortOrder filter.sortOrder = storedSortOrder as typeof filter.sortOrder;
} }
if (storedSort || storedSortOrder) { if (storedSort || storedSortOrder) {
dispatch("update", filter); dispatch("update", filter);
@@ -57,7 +57,7 @@
} }
function setSort() { function setSort() {
localStorage.setItem("sort", filter.sort) localStorage.setItem("sort", filter.sort);
dispatch("update", filter); dispatch("update", filter);
} }
@@ -67,7 +67,7 @@
} else { } else {
filter.sortOrder = "+"; filter.sortOrder = "+";
} }
localStorage.setItem("sort_order", filter.sortOrder) localStorage.setItem("sort_order", filter.sortOrder);
dispatch("update", filter); dispatch("update", filter);
} }
</script> </script>
@@ -130,6 +130,11 @@
</a> </a>
{/each} {/each}
</div> </div>
<Pagination
page={pagination.page}
totalPages={pagination.totalPages}
on:pagination
></Pagination>
</div> </div>
<style> <style>

View File

@@ -17,7 +17,7 @@
</svelte:head> </svelte:head>
<main class="grid grid-cols-1 md:grid-cols-[458px_1fr] gap-x-1 gap-y-4"> <main class="grid grid-cols-1 md:grid-cols-[458px_1fr] gap-x-1 gap-y-4">
<TrailInfoPanel trail={$trail} {markers}></TrailInfoPanel> <TrailInfoPanel trail={$trail} {markers}></TrailInfoPanel>
<div id="trail-details" class=" sticky top-0 min-h-[600px]"> <div id="trail-details" class=" sticky top-[62px]">
<MapWithElevation trail={$trail} bind:markers></MapWithElevation> <MapWithElevation trail={$trail} bind:markers></MapWithElevation>
</div> </div>
</main> </main>
@@ -25,7 +25,7 @@
<style> <style>
@media only screen and (min-width: 768px) { @media only screen and (min-width: 768px) {
#trail-details { #trail-details {
max-height: calc(100vh - 124px); height: calc(100vh - 124px);
} }
} }
</style> </style>

View File

@@ -26,7 +26,9 @@ export const load: ServerLoad = async ({ params, locals, url, fetch }) => {
if (paramCategory) { if (paramCategory) {
filter.category.push(paramCategory); filter.category.push(paramCategory);
} }
const response = await trails_search_filter(filter, 1, fetch); const page = url.searchParams.get("page") ?? "1";
const response = await trails_search_filter(filter, parseInt(page), fetch);
await categories_index(fetch) await categories_index(fetch)
return { return {

View File

@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation";
import { page } from "$app/stores"; import { page } from "$app/stores";
import TrailFilterPanel from "$lib/components/trail/trail_filter_panel.svelte"; import TrailFilterPanel from "$lib/components/trail/trail_filter_panel.svelte";
import TrailList from "$lib/components/trail/trail_list.svelte"; import TrailList from "$lib/components/trail/trail_list.svelte";
@@ -21,7 +22,7 @@
}); });
async function handleFilterUpdate() { async function handleFilterUpdate() {
const response = await trails_search_filter(filter, 1); const response = await trails_search_filter(filter, pagination.page);
pagination.page = response.page; pagination.page = response.page;
pagination.totalPages = response.totalPages; pagination.totalPages = response.totalPages;
} }
@@ -29,6 +30,8 @@
async function paginate(page: number) { async function paginate(page: number) {
pagination.page = page; pagination.page = page;
const response = await trails_search_filter(filter, page); const response = await trails_search_filter(filter, page);
$page.url.searchParams.set("page", page.toString());
goto(`?${$page.url.searchParams.toString()}`);
} }
</script> </script>
@@ -37,7 +40,7 @@
</svelte:head> </svelte:head>
<main <main
class="grid grid-cols-1 md:grid-cols-[300px_1fr] gap-8 max-w-7xl mx-6 md:mx-auto" class="grid grid-cols-1 md:grid-cols-[300px_1fr] items-start gap-8 max-w-7xl mx-6 md:mx-auto"
> >
<TrailFilterPanel <TrailFilterPanel
categories={$categories} categories={$categories}