89
db/migrations/1725619780_updated_trails.go
Normal file
89
db/migrations/1725619780_updated_trails.go
Normal 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
6
web/package-lock.json
generated
@@ -17,6 +17,7 @@
|
||||
"@types/xmldom": "^0.1.34",
|
||||
"canvg": "^4.0.1",
|
||||
"crypto-random-string": "^5.0.0",
|
||||
"heic2any": "^0.0.4",
|
||||
"instead": "^1.0.3",
|
||||
"isomorphic-xml2js": "^0.1.3",
|
||||
"jspdf": "^2.5.1",
|
||||
@@ -2813,6 +2814,11 @@
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"@types/xmldom": "^0.1.34",
|
||||
"canvg": "^4.0.1",
|
||||
"crypto-random-string": "^5.0.0",
|
||||
"heic2any": "^0.0.4",
|
||||
"instead": "^1.0.3",
|
||||
"isomorphic-xml2js": "^0.1.3",
|
||||
"jspdf": "^2.5.1",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { getFileURL, readAsDataURLAsync } from "$lib/util/file_util";
|
||||
import { onMount } from "svelte";
|
||||
import PhotoCard from "../photo_card.svelte";
|
||||
|
||||
export let id: string;
|
||||
@@ -42,7 +43,7 @@
|
||||
document.getElementById(`${id}-photo-input`)!.click();
|
||||
}
|
||||
|
||||
function handlePhotoSelection(files?: FileList | null) {
|
||||
async function handlePhotoSelection(files?: FileList | null) {
|
||||
if (!files) {
|
||||
files = (
|
||||
document.getElementById(`${id}-photo-input`) as HTMLInputElement
|
||||
@@ -54,13 +55,25 @@
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
let photoFile = file;
|
||||
if (!file.type.startsWith("image")) {
|
||||
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) {
|
||||
photoFiles = [];
|
||||
}
|
||||
photoFiles = [...photoFiles, file];
|
||||
photoFiles = [...photoFiles, photoFile];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
$: thumbnail = trail.photos.length
|
||||
? getFileURL(trail, trail.photos[trail.thumbnail])
|
||||
: "/imgs/default_thumbnail.webp";
|
||||
|
||||
$: trailIsShared = (trail.expand?.trail_share_via_trail?.length ?? 0) > 0;
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -27,12 +29,18 @@
|
||||
>
|
||||
<img src={thumbnail} alt="" />
|
||||
</div>
|
||||
{#if trail.public || trail.expand?.trail_share_via_trail?.length}
|
||||
{#if trail.public || trailIsShared}
|
||||
<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}
|
||||
<span class="tooltip" data-title={$_("public")}>
|
||||
<span
|
||||
class="tooltip"
|
||||
class:mr-2={trail.public && trailIsShared}
|
||||
data-title={$_("public")}
|
||||
>
|
||||
<i class="fa fa-globe"></i>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
@@ -109,14 +109,23 @@
|
||||
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();
|
||||
zip.file(`${trail.name}.${exportSettings.fileFormat}`, fileData);
|
||||
zip.file(
|
||||
`${trail.name}.${exportSettings.fileFormat}`,
|
||||
fileData,
|
||||
);
|
||||
if (exportSettings.photos) {
|
||||
const photoFolder = zip.folder($_("photos"));
|
||||
for (const photo of trail.photos) {
|
||||
const photoURL = getFileURL(trail, photo);
|
||||
const photoBlob = await fetch(photoURL).then((response) =>
|
||||
response.blob(),
|
||||
const photoBlob = await fetch(photoURL).then(
|
||||
(response) => response.blob(),
|
||||
);
|
||||
const photoData = new File([photoBlob], photo);
|
||||
photoFolder?.file(photo, photoData, { base64: true });
|
||||
@@ -134,6 +143,7 @@
|
||||
}
|
||||
const blob = await zip.generateAsync({ type: "blob" });
|
||||
saveAs(blob, `${trail.name}.zip`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
show_toast({
|
||||
|
||||
@@ -47,6 +47,9 @@
|
||||
...($currentUser ? [$_("comment", { values: { n: 2 } })] : []),
|
||||
];
|
||||
|
||||
const trailIsShared =
|
||||
(trail.expand?.trail_share_via_trail?.length ?? 0) > 0;
|
||||
|
||||
let map: Map;
|
||||
|
||||
let activeTab = 0;
|
||||
@@ -161,7 +164,8 @@
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<section class="relative h-80">
|
||||
@@ -169,19 +173,23 @@
|
||||
<div
|
||||
class="absolute bottom-0 w-full h-1/2 bg-gradient-to-b from-transparent to-black opacity-50"
|
||||
></div>
|
||||
{#if trail.public || trail.expand?.trail_share_via_trail?.length}
|
||||
{#if trail.public || trailIsShared}
|
||||
<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}
|
||||
<span
|
||||
class="tooltip text-2xl"
|
||||
class:mr-3={trail.public && trailIsShared}
|
||||
data-title={$_("public")}
|
||||
>
|
||||
<i class="fa fa-globe"></i>
|
||||
</span>
|
||||
{/if}
|
||||
{#if trail.expand?.trail_share_via_trail?.length}
|
||||
{#if trailIsShared}
|
||||
<TrailShareInfo {trail} large={true}></TrailShareInfo>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -266,9 +274,7 @@
|
||||
<hr class="border-separator" />
|
||||
{/if}
|
||||
</div>
|
||||
<section
|
||||
class="trail-info-panel-tabs px-4 py-2 bg-background sticky top-0"
|
||||
>
|
||||
<section class="trail-info-panel-tabs px-4 py-2 bg-background sticky top-0">
|
||||
<Tabs {tabs} bind:activeTab></Tabs>
|
||||
</section>
|
||||
<section class="trail-info-panel-content px-8">
|
||||
|
||||
@@ -35,19 +35,19 @@
|
||||
|
||||
onMount(() => {
|
||||
const storedDisplayOption = localStorage.getItem("displayOption");
|
||||
const storedSort= localStorage.getItem("sort");
|
||||
const storedSortOrder= localStorage.getItem("sort_order");
|
||||
const storedSort = localStorage.getItem("sort");
|
||||
const storedSortOrder = localStorage.getItem("sort_order");
|
||||
|
||||
if (storedDisplayOption) {
|
||||
selectedDisplayOption = storedDisplayOption;
|
||||
}
|
||||
if(storedSort) {
|
||||
filter.sort = storedSort as typeof filter.sort
|
||||
if (storedSort) {
|
||||
filter.sort = storedSort as typeof filter.sort;
|
||||
}
|
||||
if(storedSortOrder) {
|
||||
filter.sortOrder = storedSortOrder as typeof filter.sortOrder
|
||||
if (storedSortOrder) {
|
||||
filter.sortOrder = storedSortOrder as typeof filter.sortOrder;
|
||||
}
|
||||
if(storedSort || storedSortOrder) {
|
||||
if (storedSort || storedSortOrder) {
|
||||
dispatch("update", filter);
|
||||
}
|
||||
});
|
||||
@@ -57,7 +57,7 @@
|
||||
}
|
||||
|
||||
function setSort() {
|
||||
localStorage.setItem("sort", filter.sort)
|
||||
localStorage.setItem("sort", filter.sort);
|
||||
dispatch("update", filter);
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
} else {
|
||||
filter.sortOrder = "+";
|
||||
}
|
||||
localStorage.setItem("sort_order", filter.sortOrder)
|
||||
localStorage.setItem("sort_order", filter.sortOrder);
|
||||
dispatch("update", filter);
|
||||
}
|
||||
</script>
|
||||
@@ -130,6 +130,11 @@
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
<Pagination
|
||||
page={pagination.page}
|
||||
totalPages={pagination.totalPages}
|
||||
on:pagination
|
||||
></Pagination>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</svelte:head>
|
||||
<main class="grid grid-cols-1 md:grid-cols-[458px_1fr] gap-x-1 gap-y-4">
|
||||
<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>
|
||||
</div>
|
||||
</main>
|
||||
@@ -25,7 +25,7 @@
|
||||
<style>
|
||||
@media only screen and (min-width: 768px) {
|
||||
#trail-details {
|
||||
max-height: calc(100vh - 124px);
|
||||
height: calc(100vh - 124px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -26,7 +26,9 @@ export const load: ServerLoad = async ({ params, locals, url, fetch }) => {
|
||||
if (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)
|
||||
|
||||
return {
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/stores";
|
||||
import TrailFilterPanel from "$lib/components/trail/trail_filter_panel.svelte";
|
||||
import TrailList from "$lib/components/trail/trail_list.svelte";
|
||||
@@ -21,7 +22,7 @@
|
||||
});
|
||||
|
||||
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.totalPages = response.totalPages;
|
||||
}
|
||||
@@ -29,6 +30,8 @@
|
||||
async function paginate(page: number) {
|
||||
pagination.page = page;
|
||||
const response = await trails_search_filter(filter, page);
|
||||
$page.url.searchParams.set("page", page.toString());
|
||||
goto(`?${$page.url.searchParams.toString()}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -37,7 +40,7 @@
|
||||
</svelte:head>
|
||||
|
||||
<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
|
||||
categories={$categories}
|
||||
|
||||
Reference in New Issue
Block a user