Merge branch 'main' into i10n_main

This commit is contained in:
Christian Beutel
2025-09-13 16:40:14 +02:00
55 changed files with 833 additions and 295 deletions

View File

@@ -18,11 +18,11 @@ jobs:
steps:
# 1. Checkout the repository
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v5
# 2. Setup node & npm
- name: Setup Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: '22'
@@ -53,12 +53,12 @@ jobs:
steps:
# 1. Checkout the repository
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v5
with:
ref: ${{ github.ref }}
- name: Setup Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: '1.22'
@@ -106,7 +106,7 @@ jobs:
steps:
# 1. Checkout the repository
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v5
with:
ref: ${{ github.ref }}
# 2. Extract release notes from CHANGELOG.md

2
.gitignore vendored
View File

@@ -9,6 +9,6 @@ search/dumps
run.sh
build*.sh
start.*
start*.*
data*/

View File

@@ -1,3 +1,17 @@
##v0.18.2
## Features
- Adds `dedup` command to pocketbase. This command allows an admin to quickly identify duplicate trails and delete them. Use the `--dry-run` flag to only log duplicate trails without deleting them. To execute the command run `docker exec -it wanderer-db ./pocketbase dedup --dry-run`.
- Adds option to only sync strava activities after a certain date
- Singificant performance improvements for instances with larger userbases
- Greatly improved initial indexing speed when starting wanderer
## Bug fixes
- Fixes permission issues for public trails
- Fixes bug that caused trails to be duplicated multiple times (to clean up see the `dedup` command above)
- Fixes link to "New Trail" from empty profiles
- Fixes link when opening a trail from the map searchbar
- Sorting by difficulty no longer sorts by difficulty alphabetically
- Fixes strava integration stopping after only one page
# v0.18.1
## Bug fixes
- Fixes permission issues that prevented federation from working properly

121
db/commands/dedup.go Normal file
View File

@@ -0,0 +1,121 @@
package commands
import (
"crypto/sha1"
"fmt"
"log"
"sort"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/core"
"github.com/spf13/cobra"
)
func Dedup(app *pocketbase.PocketBase) *cobra.Command {
var dryRun bool
cmd := &cobra.Command{
Use: "dedup",
Short: "Deduplicate trails by all matching fields",
Run: func(cmd *cobra.Command, args []string) {
records, err := app.FindAllRecords("trails")
if err != nil {
log.Fatalf("failed to fetch trails: %v", err)
}
// group by composite key
trailsByKey := make(map[string][]*core.Record)
for _, r := range records {
key := makeKey(r)
trailsByKey[key] = append(trailsByKey[key], r)
}
var duplicates []*core.Record
for _, recs := range trailsByKey {
if len(recs) <= 1 {
continue
}
// sort by created date ascending
sort.Slice(recs, func(i, j int) bool {
return recs[i].GetDateTime("created").Time().Before(recs[j].GetDateTime("created").Time())
})
original := recs[0]
dupes := recs[1:]
// print header row for original
// print original as header
fmt.Printf("\nOriginal: id=%s, name=%s, distance=%.2f, elevation_gain=%.2f, elevation_loss=%.2f, lat=%.5f, lon=%.5f, duration=%.2f, location=%s, category=%s, author=%s, created=%s\n",
original.Id,
original.GetString("name"),
original.GetFloat("distance"),
original.GetFloat("elevation_gain"),
original.GetFloat("elevation_loss"),
original.GetFloat("lat"),
original.GetFloat("lon"),
original.GetFloat("duration"),
original.GetString("location"),
original.GetString("category"),
original.GetString("author"),
original.GetDateTime("created"),
)
// print duplicates indented
for _, d := range dupes {
fmt.Printf(" Duplicate: id=%s, name=%s, distance=%.2f, elevation_gain=%.2f, elevation_loss=%.2f, lat=%.5f, lon=%.5f, duration=%.2f, location=%s, category=%s, author=%s, created=%s\n",
d.Id,
d.GetString("name"),
d.GetFloat("distance"),
d.GetFloat("elevation_gain"),
d.GetFloat("elevation_loss"),
d.GetFloat("lat"),
d.GetFloat("lon"),
d.GetFloat("duration"),
d.GetString("location"),
d.GetString("category"),
d.GetString("author"),
d.GetDateTime("created"),
)
duplicates = append(duplicates, d)
}
}
if dryRun {
fmt.Printf("\n[Dry Run] Found %d duplicates (no deletions performed)\n", len(duplicates))
return
}
// delete duplicates
for _, d := range duplicates {
if err := app.Delete(d); err != nil {
fmt.Printf("Failed to delete duplicate %s: %v\n", d.Id, err)
} else {
fmt.Printf("Deleted duplicate %s\n", d.Id)
}
}
},
}
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Show duplicates without deleting them")
return cmd
}
// makeKey creates a composite key string for duplicate detection
func makeKey(r *core.Record) string {
data := fmt.Sprintf("%s|%f|%f|%f|%f|%f|%f|%s|%s|%s",
r.GetString("name"),
r.GetFloat("distance"),
r.GetFloat("elevation_gain"),
r.GetFloat("elevation_loss"),
r.GetFloat("lat"),
r.GetFloat("lon"),
r.GetFloat("duration"),
r.GetString("location"),
r.GetString("category"),
r.GetString("author"),
)
h := sha1.Sum([]byte(data))
return fmt.Sprintf("%x", h)
}

View File

@@ -205,27 +205,27 @@ func syncTrailWithTours(app core.App, k *KomootApi, i KomootIntegration, user st
app.Logger().Warn(fmt.Sprintf("Unable to generate GPX for tour '%s': %v", tour.Name, err))
continue
}
wpIds, err := createWaypointsFromTour(app, detailedTour, user)
if err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to create waypoints for tour '%s': %v", tour.Name, err))
continue
}
err = createTrailFromTour(app, k, detailedTour, gpx, actor, wpIds)
trailid, err := createTrailFromTour(app, k, detailedTour, gpx, actor)
if err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to create trail for tour '%s': %v", tour.Name, err))
continue
}
err = createWaypointsFromTour(app, detailedTour, user, trailid)
if err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to create waypoints for tour '%s': %v", tour.Name, err))
continue
}
}
return hasNewTours, nil
}
func createTrailFromTour(app core.App, k *KomootApi, detailedTour *DetailedKomootTour, gpx *filesystem.File, actor string, wpIds []string) error {
func createTrailFromTour(app core.App, k *KomootApi, detailedTour *DetailedKomootTour, gpx *filesystem.File, actor string) (string, error) {
trailid := security.RandomStringWithAlphabet(core.DefaultIdLength, core.DefaultIdAlphabet)
collection, err := app.FindCollectionByNameOrId("trails")
if err != nil {
return err
return "", err
}
record := core.NewRecord(collection)
@@ -251,12 +251,12 @@ func createTrailFromTour(app core.App, k *KomootApi, detailedTour *DetailedKomoo
if len(detailedTour.Embedded.CoverImages.Embedded.Items) > 0 {
photos, err = fetchRoutePhotos(k, detailedTour)
if err != nil {
return err
return "", err
}
} else {
photo, err := fetchPhoto(detailedTour.MapImage.Src, "", "")
if err != nil {
return err
return "", err
}
photos = append(photos, photo)
}
@@ -281,7 +281,6 @@ func createTrailFromTour(app core.App, k *KomootApi, detailedTour *DetailedKomoo
"lon": detailedTour.StartPoint.Lng,
"difficulty": diffculty,
"category": categoryId,
"waypoints": wpIds,
"author": actor,
})
@@ -293,13 +292,13 @@ func createTrailFromTour(app core.App, k *KomootApi, detailedTour *DetailedKomoo
}
if err := app.Save(record); err != nil {
return err
return "", err
}
if detailedTour.Type == "tour_recorded" {
collection, err := app.FindCollectionByNameOrId("summit_logs")
if err != nil {
return err
return "", err
}
summitLogRecord := core.NewRecord(collection)
@@ -313,25 +312,23 @@ func createTrailFromTour(app core.App, k *KomootApi, detailedTour *DetailedKomoo
"trail": trailid,
})
if err := app.Save(summitLogRecord); err != nil {
return err
return "", err
}
}
return nil
return trailid, nil
}
func createWaypointsFromTour(app core.App, tour *DetailedKomootTour, user string) ([]string, error) {
func createWaypointsFromTour(app core.App, tour *DetailedKomootTour, user string, trailid string) error {
collection, err := app.FindCollectionByNameOrId("waypoints")
if err != nil {
return nil, err
return err
}
wpIds := make([]string, len(tour.Embedded.Timeline.Embedded.Items))
for i, wp := range tour.Embedded.Timeline.Embedded.Items {
for _, wp := range tour.Embedded.Timeline.Embedded.Items {
photos, err := fetchWaypointPhotos(wp)
if err != nil {
return nil, err
return err
}
record := core.NewRecord(collection)
@@ -358,6 +355,7 @@ func createWaypointsFromTour(app core.App, tour *DetailedKomootTour, user string
"icon": "circle",
"author": user,
"distance_from_start": 0,
"trail": trailid,
})
if photos != nil {
@@ -365,13 +363,11 @@ func createWaypointsFromTour(app core.App, tour *DetailedKomootTour, user string
}
if err := app.Save(record); err != nil {
return nil, err
return err
}
}
wpIds[i] = record.Id
}
return wpIds, nil
return nil
}
func fetchRoutePhotos(k *KomootApi, tour *DetailedKomootTour) ([]*filesystem.File, error) {

View File

@@ -29,6 +29,7 @@ type StravaIntegration struct {
AccessToken string `json:"accessToken,omitempty"`
RefreshToken string `json:"refreshToken,omitempty"`
ExpiresAt int64 `json:"expiresAt,omitempty"`
After string `json:"after,omitempty"`
}
type StravaRoute struct {
Athlete Athlete `json:"athlete"`

View File

@@ -89,22 +89,12 @@ func SyncStrava(app core.App) error {
stravaIntegration.ExpiresAt = r.ExpiresAt
}
b, err := json.Marshal(stravaIntegration)
if err != nil {
return err
}
i.Set("strava", string(b))
err = app.Save(i)
if err != nil {
return err
}
if stravaIntegration.Routes {
page := 1
hasNewRoutes := true
for hasNewRoutes {
hasMore := true
for hasMore {
routes, err := fetchStravaRoutes(r.AccessToken, page)
hasMore = len(routes) > 0
page += 1
if err != nil {
warning := fmt.Sprintf("error fetching routes from strava: %v\n", err)
@@ -112,7 +102,7 @@ func SyncStrava(app core.App) error {
app.Logger().Warn(warning)
break
}
hasNewRoutes, err = syncTrailsWithRoutes(app, r.AccessToken, userId, actorId, routes)
err = syncTrailsWithRoutes(app, r.AccessToken, userId, actorId, routes)
if err != nil {
warning := fmt.Sprintf("error syncing strava routes with trails: %v\n", err)
fmt.Print(warning)
@@ -123,9 +113,20 @@ func SyncStrava(app core.App) error {
}
if stravaIntegration.Activities {
page := 1
hasNewActivities := true
for hasNewActivities {
activities, err := fetchStravaActivities(r.AccessToken, page)
hasMore := true
for hasMore {
var after int64 = 0
if stravaIntegration.After != "" {
t, err := time.Parse("2006-01-02", stravaIntegration.After)
if err != nil {
return err
}
t = t.UTC()
after = t.Unix()
}
activities, err := fetchStravaActivities(r.AccessToken, page, after)
hasMore = len(activities) > 0
page += 1
if err != nil {
warning := fmt.Sprintf("error fetching activities from strava: %v", err)
@@ -133,7 +134,8 @@ func SyncStrava(app core.App) error {
app.Logger().Warn(warning)
break
}
hasNewActivities, err = syncTrailsWithActivities(app, r.AccessToken, userId, actorId, activities)
err = syncTrailsWithActivities(app, r.AccessToken, actorId, activities)
if err != nil {
warning := fmt.Sprintf("error syncing strava activities with trails: %v", err)
fmt.Print(warning)
@@ -141,6 +143,17 @@ func SyncStrava(app core.App) error {
continue
}
}
}
b, err := json.Marshal(stravaIntegration)
if err != nil {
return err
}
i.Set("strava", string(b))
err = app.Save(i)
if err != nil {
return err
}
}
@@ -208,8 +221,8 @@ func fetchStravaRoutes(accessToken string, page int) ([]StravaRoute, error) {
return routes, nil
}
func fetchStravaActivities(accessToken string, page int) ([]StravaActivity, error) {
stravaRoutesURL := fmt.Sprintf("https://www.strava.com/api/v3/athlete/activities?page=%d", page)
func fetchStravaActivities(accessToken string, page int, after int64) ([]StravaActivity, error) {
stravaRoutesURL := fmt.Sprintf("https://www.strava.com/api/v3/athlete/activities?page=%d&after=%d", page, after)
req, err := http.NewRequest("GET", stravaRoutesURL, nil)
if err != nil {
return nil, err
@@ -235,36 +248,33 @@ func fetchStravaActivities(accessToken string, page int) ([]StravaActivity, erro
return activities, nil
}
func syncTrailsWithRoutes(app core.App, accessToken string, user string, actor string, routes []StravaRoute) (bool, error) {
hasNewRoutes := false
func syncTrailsWithRoutes(app core.App, accessToken string, user string, actor string, routes []StravaRoute) error {
for _, route := range routes {
trails, err := app.FindRecordsByFilter("trails", "external_id = {:id}", "", 1, 0, dbx.Params{"id": route.IDStr})
if err != nil {
return hasNewRoutes, err
return err
}
if len(trails) != 0 {
continue
}
hasNewRoutes = true
gpx, err := fetchRouteGPX(route, accessToken)
if err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to fetch GPX for route '%s': %v", route.Name, err))
continue
}
wpIds, err := createWaypointsFromRoute(app, route, user)
if err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to create waypoints for route '%s': %v", route.Name, err))
continue
}
err = createTrailFromRoute(app, route, gpx, actor, wpIds)
trailid, err := createTrailFromRoute(app, route, gpx, actor)
if err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to create trail for route '%s': %v", route.Name, err))
continue
}
err = createWaypointsFromRoute(app, route, user, trailid)
if err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to create waypoints for route '%s': %v", route.Name, err))
continue
}
}
return hasNewRoutes, nil
return nil
}
func fetchRouteGPX(route StravaRoute, accessToken string) (*filesystem.File, error) {
@@ -305,10 +315,12 @@ func fetchRouteGPX(route StravaRoute, accessToken string) (*filesystem.File, err
return gpxFile, nil
}
func createTrailFromRoute(app core.App, route StravaRoute, gpx *filesystem.File, actor string, wpIds []string) error {
func createTrailFromRoute(app core.App, route StravaRoute, gpx *filesystem.File, actor string) (string, error) {
trailid := security.RandomStringWithAlphabet(core.DefaultIdLength, core.DefaultIdAlphabet)
collection, err := app.FindCollectionByNameOrId("trails")
if err != nil {
return err
return "", err
}
record := core.NewRecord(collection)
@@ -337,6 +349,7 @@ func createTrailFromRoute(app core.App, route StravaRoute, gpx *filesystem.File,
}
record.Load(map[string]any{
"id": trailid,
"name": route.Name,
"description": route.Description,
"public": !route.Private,
@@ -348,7 +361,6 @@ func createTrailFromRoute(app core.App, route StravaRoute, gpx *filesystem.File,
"external_id": route.IDStr,
"lat": lat,
"lon": lon,
"waypoints": wpIds,
"difficulty": "easy",
"category": category,
"author": actor,
@@ -359,20 +371,18 @@ func createTrailFromRoute(app core.App, route StravaRoute, gpx *filesystem.File,
}
if err := app.Save(record); err != nil {
return err
return "", err
}
return nil
return trailid, err
}
func createWaypointsFromRoute(app core.App, route StravaRoute, user string) ([]string, error) {
func createWaypointsFromRoute(app core.App, route StravaRoute, user string, trailid string) error {
collection, err := app.FindCollectionByNameOrId("waypoints")
if err != nil {
return nil, err
return err
}
wpIds := make([]string, len(route.Waypoints))
for i, wp := range route.Waypoints {
record := core.NewRecord(collection)
@@ -383,26 +393,26 @@ func createWaypointsFromRoute(app core.App, route StravaRoute, user string) ([]s
record.Set("icon", "circle")
record.Set("author", user)
record.Set("distance_from_start", wp.DistanceIntoRoute)
record.Set("trail", trailid)
app.Save(record)
wpIds[i] = record.Id
if err := app.Save(record); err != nil {
return err
}
return wpIds, nil
}
return nil
}
func syncTrailsWithActivities(app core.App, accessToken string, user string, actor string, activities []StravaActivity) (bool, error) {
hasNewActivites := false
func syncTrailsWithActivities(app core.App, accessToken string, actor string, activities []StravaActivity) error {
for _, activity := range activities {
trails, err := app.FindRecordsByFilter("trails", "external_id = {:id}", "", 1, 0, dbx.Params{"id": strconv.Itoa(int(activity.ID))})
if err != nil {
return hasNewActivites, err
return err
}
if len(trails) != 0 {
continue
}
hasNewActivites = true
detailedActivity, err := fetchDetailedActivity(activity, accessToken)
if err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to fetch detailed activity '%s': %v", activity.Name, err))
@@ -418,10 +428,9 @@ func syncTrailsWithActivities(app core.App, accessToken string, user string, act
app.Logger().Warn(fmt.Sprintf("Unable to create trail from activity '%s': %v", activity.Name, err))
continue
}
}
return hasNewActivites, nil
return nil
}
func fetchDetailedActivity(activity StravaActivity, accessToken string) (*DetailedStravaActivity, error) {

View File

@@ -21,6 +21,7 @@ import (
"github.com/pocketbase/pocketbase/tools/security"
"github.com/spf13/cast"
"pocketbase/commands"
"pocketbase/federation"
"pocketbase/integrations/komoot"
"pocketbase/integrations/strava"
@@ -73,6 +74,8 @@ func main() {
registerMigrations(app)
setupEventHandlers(app, client)
setupCommands(app)
if err := app.Start(); err != nil {
log.Fatal(err)
}
@@ -140,6 +143,10 @@ func setupEventHandlers(app *pocketbase.PocketBase, client meilisearch.ServiceMa
app.OnBootstrap().BindFunc(onBootstrapHandler())
}
func setupCommands(app *pocketbase.PocketBase) {
app.RootCmd.AddCommand(commands.Dedup(app))
}
func sanitizeHTML() func(e *core.RecordRequestEvent) error {
return func(e *core.RecordRequestEvent) error {
fieldsToSanitize := map[string][]string{
@@ -233,7 +240,7 @@ func createTrailHandler(client meilisearch.ServiceManager) func(e *core.RecordEv
if err != nil {
return err
}
if err := util.IndexTrail(e.App, record, author, client); err != nil {
if err := util.IndexTrails(e.App, []*core.Record{record}, client); err != nil {
return err
}
if !author.GetBool("isLocal") {
@@ -340,12 +347,7 @@ func createSummitLogHandler(client meilisearch.ServiceManager) func(e *core.Reco
return err
}
trailAuthor, err := e.App.FindFirstRecordByData("activitypub_actors", "id", trail.GetString("author"))
if err != nil {
return err
}
if err := util.IndexTrail(e.App, trail, trailAuthor, client); err != nil {
if err := util.IndexTrails(e.App, []*core.Record{trail}, client); err != nil {
return err
}
@@ -391,12 +393,7 @@ func deleteSummitLogHandler(client meilisearch.ServiceManager) func(e *core.Reco
return err
}
trailAuthor, err := e.App.FindFirstRecordByData("activitypub_actors", "id", trail.GetString("author"))
if err != nil {
return err
}
if err := util.IndexTrail(e.App, trail, trailAuthor, client); err != nil {
if err := util.IndexTrails(e.App, []*core.Record{trail}, client); err != nil {
return err
}
@@ -616,7 +613,7 @@ func createListHandler(client meilisearch.ServiceManager) func(e *core.RecordEve
return err
}
if err := util.IndexList(e.App, record, author, client); err != nil {
if err := util.IndexLists(e.App, []*core.Record{record}, client); err != nil {
return err
}
@@ -1308,94 +1305,62 @@ func bootstrapCategories(app core.App) error {
}
func bootstrapMeilisearchDocuments(app core.App, client meilisearch.ServiceManager) error {
query := app.RecordQuery("trails")
// --- Trails ---
const pageSize int64 = 100
var page int64 = 0
// Clear index before re-indexing
if _, err := client.Index("trails").DeleteAllDocuments(); err != nil {
return err
}
for {
trails := []*core.Record{}
if err := query.All(&trails); err != nil {
return err
}
_, err := client.Index("trails").DeleteAllDocuments()
err := app.RecordQuery("trails").
Limit(pageSize).
Offset(page * pageSize).
All(&trails)
if err != nil {
return err
}
for _, trail := range trails {
author, err := app.FindRecordById("activitypub_actors", trail.GetString(("author")))
if err != nil {
return err
if len(trails) == 0 {
break
}
if err := util.IndexTrail(app, trail, author, client); err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to index trail '%s': %v", trail.GetString("name"), err))
if err := util.IndexTrails(app, trails, client); err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to index trails page %d: %v", page, err))
continue
}
shares, err := app.FindAllRecords("trail_share",
dbx.NewExp("trail = {:trailId}", dbx.Params{"trailId": trail.Id}),
)
if err != nil {
return err
}
actorIds := make([]string, len(shares))
for i, r := range shares {
actorIds[i] = r.GetString("actor")
}
err = util.UpdateTrailShares(trail.Id, actorIds, client)
if err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to update trail shares '%s': %v", trail.GetString("name"), err))
continue
}
likes, err := app.FindAllRecords("trail_like",
dbx.NewExp("trail = {:trailId}", dbx.Params{"trailId": trail.Id}),
)
if err != nil {
return err
}
actorIds = make([]string, len(likes))
for i, r := range likes {
actorIds[i] = r.GetString("actor")
}
err = util.UpdateTrailLikes(trail.Id, actorIds, client)
if err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to update trail likes '%s': %v", trail.GetString("name"), err))
continue
}
page++
}
lists, err := app.FindAllRecords("lists")
if err != nil {
return err
}
_, err = client.Index("lists").DeleteAllDocuments()
if err != nil {
// --- Lists ---
if _, err := client.Index("lists").DeleteAllDocuments(); err != nil {
return err
}
for _, list := range lists {
author, err := app.FindRecordById("activitypub_actors", list.GetString(("author")))
page = 0
for {
lists := []*core.Record{}
err := app.RecordQuery("lists").
Limit(pageSize).
Offset(page * pageSize).
All(&lists)
if err != nil {
return err
}
if err := util.IndexList(app, list, author, client); err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to index list '%s': %v", list.GetString("name"), err))
if len(lists) == 0 {
break
}
if err := util.IndexLists(app, lists, client); err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to index list page %d: %v", page, err))
continue
}
shares, err := app.FindAllRecords("list_share",
dbx.NewExp("list = {:listId}", dbx.Params{"listId": list.Id}),
)
if err != nil {
return err
page++
}
actorIds := make([]string, len(shares))
for i, r := range shares {
actorIds[i] = r.GetString("actor")
}
err = util.UpdateListShares(list.Id, actorIds, client)
if err != nil {
app.Logger().Warn(fmt.Sprintf("Unable to update list shares '%s': %v", list.GetString("name"), err))
continue
}
}
return nil
}

View File

@@ -0,0 +1,42 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": "author.user = @request.auth.id || public = true || (@request.auth.id != \"\" && trail_share_via_trail.actor.user ?= @request.auth.id) || (trail_link_share_via_trail.token != \"\" && trail_link_share_via_trail.token = @request.query.share)",
"viewRule": "author.user = @request.auth.id || public = true || (@request.auth.id != \"\" && trail_share_via_trail.actor.user ?= @request.auth.id) || (trail_link_share_via_trail.token != \"\" && trail_link_share_via_trail.token = @request.query.share)"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": "author.user = @request.auth.id || public = true || (@request.auth.id != \"\" && trail_share_via_trail.actor.user ?= @request.auth.id) || trail_link_share_via_trail.token = @request.query.share",
"viewRule": "author.user = @request.auth.id || public = true || (@request.auth.id != \"\" && trail_share_via_trail.actor.user ?= @request.auth.id) || trail_link_share_via_trail.token = @request.query.share "
}`), &collection); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,42 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)\n|| \n(@collection.trail_link_share.token != \"\" && @collection.trail_link_share.trail.waypoints.id ?= id)",
"viewRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)\n|| \n(@collection.trail_link_share.token != \"\" && @collection.trail_link_share.trail.waypoints.id ?= id)"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)\n|| \n(@collection.trail_link_share.trail.id ?= trails_via_waypoints.id && @collection.trail_link_share.token = @request.query.share)",
"viewRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)\n|| \n(@collection.trail_link_share.trail.id ?= trails_via_waypoints.id && @collection.trail_link_share.token = @request.query.share)"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,62 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": "author = @request.auth.id || trail.author.user ?= @request.auth.id || trail.public ?= true || trail.trail_share_via_trail.actor.user ?= @request.auth.id\n|| \n(trail.trail_link_share_via_trail.token != \"\" && trail.trail_link_share_via_trail.token = @request.query.share)",
"viewRule": "author = @request.auth.id || trail.author.user ?= @request.auth.id || trail.public ?= true || trail.trail_share_via_trail.actor.user ?= @request.auth.id\n|| \n(trail.trail_link_share_via_trail.token != \"\" && trail.trail_link_share_via_trail.token = @request.query.share)"
}`), &collection); err != nil {
return err
}
// add field
if err := collection.Fields.AddMarshaledJSONAt(9, []byte(`{
"cascadeDelete": true,
"collectionId": "e864strfxo14pm4",
"hidden": false,
"id": "relation2993194383",
"maxSelect": 1,
"minSelect": 0,
"name": "trail",
"presentable": false,
"required": true,
"system": false,
"type": "relation"
}`)); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)\n|| \n(@collection.trail_link_share.token != \"\" && @collection.trail_link_share.trail.waypoints.id ?= id)",
"viewRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)\n|| \n(@collection.trail_link_share.token != \"\" && @collection.trail_link_share.trail.waypoints.id ?= id)"
}`), &collection); err != nil {
return err
}
// remove field
collection.Fields.RemoveById("relation2993194383")
return app.Save(collection)
})
}

View File

@@ -0,0 +1,34 @@
package migrations
import (
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
wps, err := app.FindAllRecords("waypoints")
if err != nil {
return err
}
for _, wp := range wps {
trail, err := app.FindFirstRecordByFilter("trails", "waypoints ?~ {:id}", dbx.Params{"id": wp.Id})
if err != nil {
continue
}
wp.Set("trail", trail.Id)
err = app.UnsafeWithoutHooks().Save(wp)
if err != nil {
return err
}
}
return nil
}, func(app core.App) error {
// add down queries...
return nil
})
}

View File

@@ -0,0 +1,44 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
if err != nil {
return err
}
// remove field
collection.Fields.RemoveById("ppq2sist")
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
if err != nil {
return err
}
// add field
if err := collection.Fields.AddMarshaledJSONAt(16, []byte(`{
"cascadeDelete": false,
"collectionId": "goeo2ubp103rzp9",
"hidden": false,
"id": "ppq2sist",
"maxSelect": 2147483647,
"minSelect": 0,
"name": "waypoints",
"presentable": false,
"required": false,
"system": false,
"type": "relation"
}`)); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,46 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_1295301207")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"indexes": [
"CREATE UNIQUE INDEX `+"`"+`idx_rpT7QJwWTm`+"`"+` ON `+"`"+`activitypub_actors`+"`"+` (`+"`"+`iri`+"`"+`)",
"CREATE INDEX idx_actors_username_domain\nON activitypub_actors(preferred_username, domain);",
"CREATE INDEX idx_activitypub_actors_user ON activitypub_actors(user);"
]
}`), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_1295301207")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"indexes": [
"CREATE UNIQUE INDEX `+"`"+`idx_rpT7QJwWTm`+"`"+` ON `+"`"+`activitypub_actors`+"`"+` (`+"`"+`iri`+"`"+`)"
]
}`), &collection); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -13,6 +13,7 @@ import (
"net/http"
"net/url"
"os"
"path"
"strconv"
"strings"
"time"
@@ -162,7 +163,19 @@ func TrailFromActivity(activity pub.Activity, app core.App, actor *core.Record)
return nil, err
}
record, err := app.FindFirstRecordByData("trails", "iri", t.ID.String())
iri := t.ID.String()
var record *core.Record
if actor.GetBool(("isLocal")) {
trailUrl, parseErr := url.Parse(iri)
if parseErr != nil {
return nil, parseErr
}
trailId := path.Base(trailUrl.Path)
record, err = app.FindRecordById("trails", trailId)
} else {
record, err = app.FindFirstRecordByData("trails", "iri", iri)
}
if err != nil {
if err == sql.ErrNoRows {
collection, err := app.FindCollectionByNameOrId("trails")
@@ -279,7 +292,7 @@ func TrailFromActivity(activity pub.Activity, app core.App, actor *core.Record)
}
if len(photoURLs) > 0 {
photos := make([]*filesystem.File, len(photoURLs))
photos := []*filesystem.File{}
for i, purl := range photoURLs {
photo, err := filesystem.NewFileFromURL(context.Background(), purl)
if err != nil {

View File

@@ -69,7 +69,7 @@ func documentFromTrailRecord(app core.App, r *core.Record, author *core.Record,
"elevation_gain": r.GetFloat("elevation_gain"),
"elevation_loss": r.GetFloat("elevation_loss"),
"duration": r.GetFloat("duration"),
"difficulty": r.Get("difficulty"),
"difficulty": difficultyToNumber(r.GetString("difficulty")),
"category": category,
"completed": logCount > 0,
"date": r.GetDateTime("date").Time().Unix(),
@@ -88,15 +88,52 @@ func documentFromTrailRecord(app core.App, r *core.Record, author *core.Record,
}
if includeShares {
trailShares := r.ExpandedAll("trail_share_via_trail")
if trailShares != nil {
sharedIDs := make([]string, len(trailShares))
for i, v := range trailShares {
sharedIDs[i] = v.GetString("actor")
}
document["shares"] = sharedIDs
} else {
document["shares"] = []string{}
}
trailLikes := r.ExpandedAll("trail_like_via_trail")
if trailLikes != nil {
likeIDs := make([]string, len(trailLikes))
for i, v := range trailLikes {
likeIDs[i] = v.GetString("actor")
}
document["likes"] = likeIDs
document["like_count"] = len(trailLikes)
} else {
document["likes"] = []string{}
document["like_count"] = 0
}
}
return document, nil
}
func difficultyToNumber(difficulty string) int32 {
switch difficulty {
case "easy":
return 0
case "moderate":
return 1
case "difficult":
return 2
}
return 0
}
func getPolyline(app core.App, r *core.Record) (string, error) {
gpxPath := r.GetString("gpx")
if len(gpxPath) == 0 {
@@ -173,7 +210,7 @@ func documentFromListRecord(r *core.Record, author *core.Record, includeShares b
domain = author.GetString("domain")
}
document := map[string]interface{}{
document := map[string]any{
"id": r.Id,
"author": author.Id,
"author_name": author.GetString("preferred_username"),
@@ -193,8 +230,19 @@ func documentFromListRecord(r *core.Record, author *core.Record, includeShares b
}
if includeShares {
listShares := r.ExpandedAll("list_share_via_list")
if listShares != nil {
sharedIDs := make([]string, len(listShares))
for i, v := range listShares {
sharedIDs[i] = v.GetString("actor")
}
document["shares"] = sharedIDs
} else {
document["shares"] = []string{}
}
}
return document, nil
}
@@ -253,7 +301,10 @@ func documentFromRemoteRecord(r *core.Record, index string) (map[string]interfac
return document, nil
}
func IndexTrail(app core.App, r *core.Record, author *core.Record, client meilisearch.ServiceManager) error {
func IndexTrails(app core.App, trails []*core.Record, client meilisearch.ServiceManager) error {
documents := make([]map[string]any, len(trails))
for i, r := range trails {
errs := app.ExpandRecord(r, []string{"tags"}, nil)
if len(errs) > 0 {
return fmt.Errorf("failed to expand tags: %v", errs)
@@ -262,11 +313,28 @@ func IndexTrail(app core.App, r *core.Record, author *core.Record, client meilis
if len(errs) > 0 {
return fmt.Errorf("failed to expand category: %v", errs)
}
errs = app.ExpandRecord(r, []string{"trail_share_via_trail"}, nil)
if len(errs) > 0 {
return fmt.Errorf("failed to expand trail_share_via_trail: %v", errs)
}
errs = app.ExpandRecord(r, []string{"trail_like_via_trail"}, nil)
if len(errs) > 0 {
return fmt.Errorf("failed to expand trail_like_via_trail: %v", errs)
}
errs = app.ExpandRecord(r, []string{"author"}, nil)
if len(errs) > 0 {
return fmt.Errorf("failed to expand author: %v", errs)
}
author := r.ExpandedOne("author")
doc, err := documentFromTrailRecord(app, r, author, true)
if err != nil {
return err
}
documents := []map[string]interface{}{doc}
documents[i] = doc
}
if _, err := client.Index("trails").AddDocuments(documents); err != nil {
return err
@@ -325,17 +393,32 @@ func UpdateTrailLikes(trailId string, likes []string, client meilisearch.Service
return nil
}
func IndexList(app core.App, r *core.Record, author *core.Record, client meilisearch.ServiceManager) error {
func IndexLists(app core.App, lists []*core.Record, client meilisearch.ServiceManager) error {
documents := make([]map[string]any, len(lists))
for i, r := range lists {
errs := app.ExpandRecord(r, []string{"trails"}, nil)
if len(errs) > 0 {
return fmt.Errorf("failed to expand trails: %v", errs)
}
errs = app.ExpandRecord(r, []string{"list_share_via_list"}, nil)
if len(errs) > 0 {
return fmt.Errorf("failed to expand list_share_via_list: %v", errs)
}
errs = app.ExpandRecord(r, []string{"author"}, nil)
if len(errs) > 0 {
return fmt.Errorf("failed to expand author: %v", errs)
}
documents, err := documentFromListRecord(r, author, true)
author := r.ExpandedOne("author")
doc, err := documentFromListRecord(r, author, true)
if err != nil {
return err
}
if _, err = client.Index("lists").AddDocuments(documents); err != nil {
documents[i] = doc
}
if _, err := client.Index("lists").AddDocuments(documents); err != nil {
return err
}

View File

@@ -2,6 +2,19 @@
title: Changelog
description: What changed in the last patch?
---
## v0.18.2
### Features
- Adds `dedup` command to pocketbase. This command allows an admin to quickly identify duplicate trails and delete them. Use the `--dry-run` flag to only log duplicate trails without deleting them. To execute the command run `docker exec -it wanderer-db ./pocketbase dedup --dry-run`.
- Adds option to only sync strava activities after a certain date
- Singificant performance improvements for instances with larger userbases
- Greatly improved initial indexing speed when starting wanderer
### Bug fixes
- Fixes permission issues for public trails
- Fixes bug that caused trails to be duplicated multiple times (to clean up see the `dedup` command above)
- Fixes link to "New Trail" from empty profiles
- Fixes link when opening a trail from the map searchbar
- Sorting by difficulty no longer sorts by difficulty alphabetically
- Fixes strava integration stopping after only one page
## v0.18.1
### Bug fixes
- Fixes permission issues that prevented federation from working properly

View File

@@ -48,6 +48,7 @@ function isFormContentType(request: Request) {
);
}
let publicMeilisearchKey: string | undefined = undefined;
const auth: Handle = async ({ event, resolve }) => {
const pb = new PocketBase(envPub.PUBLIC_POCKETBASE_URL)
@@ -82,10 +83,14 @@ const auth: Handle = async ({ event, resolve }) => {
if (pb.authStore.record) {
meiliApiKey = pb.authStore.record.token
settings = await pb.collection('settings').getFirstListItem<Settings>(`user="${pb.authStore.record.id}"`, { requestKey: null })
actor = await pb.collection("activitypub_actors").getFirstListItem(`user='${pb.authStore.record.id}'`)
actor = await pb.collection("activitypub_actors").getFirstListItem(`isLocal=1&&user='${pb.authStore.record.id}'`)
} else {
if (!publicMeilisearchKey) {
const response = await pb.send("/public/search/token", { method: "GET", fetch: event.fetch });
meiliApiKey = response.token;
publicMeilisearchKey = response.token;
}
meiliApiKey = publicMeilisearchKey!;
}
const ms = new MeiliSearch({ host: env.MEILI_URL, apiKey: meiliApiKey });

View File

@@ -1,4 +1,5 @@
<script lang="ts">
import Datepicker from "$lib/components/base/datepicker.svelte";
import Modal from "$lib/components/base/modal.svelte";
import TextField from "$lib/components/base/text_field.svelte";
import Toggle from "$lib/components/base/toggle.svelte";
@@ -25,23 +26,32 @@
modal.openModal();
}
const { form, errors } = createForm({
const {
form,
errors,
data: formData,
} = createForm({
initialValues: {
clientId: integration?.strava?.clientId ?? "",
clientSecret: integration?.strava?.clientSecret ?? "",
routes: integration?.strava?.routes ?? true,
activities: integration?.strava?.activities ?? true,
active: integration?.strava?.active ?? false,
after: integration?.strava?.after,
},
extend: validator({
schema: StravaSchema,
}),
onSubmit: async (form) => {
form.active = integration?.strava?.active ?? form.active
form.active = integration?.strava?.active ?? form.active;
onsave?.(form);
modal.closeModal();
},
});
function clearAfterDate() {
($formData as any).after = undefined;
}
</script>
<Modal
@@ -60,7 +70,9 @@
></TextField>
<TextField
label="Client Secret"
placeholder={integration?.strava ? `(${$_("unchanged")})` : "de8b3789bd7116d..."}
placeholder={integration?.strava
? `(${$_("unchanged")})`
: "de8b3789bd7116d..."}
name="clientSecret"
type="password"
error={$errors.clientSecret}
@@ -73,6 +85,25 @@
label={$_("activity", { values: { n: 2 } })}
></Toggle>
</div>
<p
class="text-xs text-gray-500 max-w-lg pt-4 pb-1 border-t border-input-border"
>
{$_("strava-integration-after-date-hint")}
</p>
<div class="flex items-end relative gap-x-2">
<Datepicker
error={$errors.after}
label={$_("after")}
bind:value={$formData.after}
></Datepicker>
<button
class="btn-icon mb-[10px]"
type="button"
onclick={clearAfterDate}
aria-label="Clear 'after' date"
><i class="fa fa-close"></i></button
>
</div>
</form>
{/snippet}
{#snippet footer()}

View File

@@ -51,9 +51,9 @@
];
const difficultyItems: SelectItem[] = [
{ text: $_("easy"), value: "easy" },
{ text: $_("moderate"), value: "moderate" },
{ text: $_("difficult"), value: "difficult" },
{ text: $_("easy"), value: 0 },
{ text: $_("moderate"), value: 1 },
{ text: $_("difficult"), value: 2 },
];
let searchDropdownItems: SearchItem[] = $state([]);

View File

@@ -658,7 +658,7 @@
<MapWithElevationMaplibre
trails={[trail]}
activeTrail={0}
waypoints={trail.expand?.waypoints}
waypoints={trail.expand?.waypoints_via_trail}
showElevation={true}
elevationProfileContainer={"epc-container"}
showStyleSwitcher={false}

View File

@@ -25,7 +25,7 @@
<div class="">
<p class="font-semibold">{$_("start")}</p>
</div>
{#each trail.expand?.waypoints ?? [] as wp, i}
{#each trail.expand?.waypoints_via_trail ?? [] as wp, i}
<div
class="bg-background cursor-pointer"
role="presentation"

View File

@@ -32,6 +32,7 @@
"bakery": "Bäckerei",
"barrier": "Barriere",
"basic-info": "Basisinformation",
"basque": "Baskisch",
"before": "Vor",
"bicycle-parking": "Fahrrad Parkplatz",
"bicycle-rental": "Fahrradverleih",
@@ -56,7 +57,6 @@
"car": "Auto",
"car-motorcycle": "Auto/Motorrad",
"card": "{n, plural, =1 {Karte} other {Karten}}",
"basque": "Baskisch",
"categories": "Kategorien",
"category": "Kategorie",
"change": "Ändern",
@@ -390,6 +390,7 @@
"statistics": "Statistiken",
"stop-drawing": "Zeichnen beenden",
"stop-editing": "Bearbeiten beenden",
"strava-integration-after-date-hint": "",
"subway-stop": "U-Bahn Eingang",
"summit": "Gipfel",
"summit-book": "Gipfelbuch",

View File

@@ -32,6 +32,7 @@
"bakery": "Bakery",
"barrier": "Barrier",
"basic-info": "Basic Info",
"basque": "Basque",
"before": "Before",
"bicycle-parking": "Bicycle Parking",
"bicycle-rental": "Bicycle Rental",
@@ -56,7 +57,6 @@
"car": "Car",
"car-motorcycle": "Car/Motorcycle",
"card": "{n, plural, =1 {Card} other {Cards}}",
"basque": "Basque",
"categories": "Categories",
"category": "Category",
"change": "Change",
@@ -390,6 +390,7 @@
"statistics": "Statistics",
"stop-drawing": "Stop drawing",
"stop-editing": "Stop editing",
"strava-integration-after-date-hint": "If your account has a large amount of acitivities you may run into Strava's API rate limit preventing you from syncing all activities at once. To mitigate this issue you can set an \"After\" date below so that only activities that were recorded after this date are synced.",
"subway-stop": "Subway entrance",
"summit": "Summit",
"summit-book": "Summit Book",

View File

@@ -32,6 +32,7 @@
"bakery": "Bakery",
"barrier": "Barrier",
"basic-info": "Información básica",
"basque": "Basque",
"before": "Antes",
"bicycle-parking": "Bicycle Parking",
"bicycle-rental": "Bicycle Rental",
@@ -56,7 +57,6 @@
"car": "Coche",
"car-motorcycle": "Car/Motorcycle",
"card": "{n, plural, one {}=1 {Ficha} other {Fichas}}",
"basque": "Basque",
"categories": "Categorías",
"category": "Categoría",
"change": "Modificar",
@@ -390,6 +390,7 @@
"statistics": "Estadísticas",
"stop-drawing": "Parar de diseñar",
"stop-editing": "Parar de editar",
"strava-integration-after-date-hint": "",
"subway-stop": "Subway entrance",
"summit": "Summit",
"summit-book": "Libro de ascensos",

View File

@@ -32,6 +32,7 @@
"bakery": "Okindegia",
"barrier": "Oztopoa",
"basic-info": "Oinarrizko informazioa",
"basque": "Basque",
"before": "Aurretik",
"bicycle-parking": "Bizikleta-parkina",
"bicycle-rental": "Bizikleta-alokairua",
@@ -56,7 +57,6 @@
"car": "Kotxea",
"car-motorcycle": "Kotxea/Motozikleta",
"card": "{n, plural, one {}=1 {Txartea} other {Txartelak}}",
"basque": "Basque",
"categories": "Kategoriak",
"category": "Kategoria",
"change": "Aldatu",
@@ -390,6 +390,7 @@
"statistics": "Estatistikak",
"stop-drawing": "Utzi marrazteari",
"stop-editing": "Utzi editatzeari",
"strava-integration-after-date-hint": "",
"subway-stop": "Metro sarbidea",
"summit": "Gailurra",
"summit-book": "Igoeren liburua",

View File

@@ -32,6 +32,7 @@
"bakery": "Bakery",
"barrier": "Barrier",
"basic-info": "Informations de base",
"basque": "Basque",
"before": "Avant le",
"bicycle-parking": "Bicycle Parking",
"bicycle-rental": "Bicycle Rental",
@@ -56,7 +57,6 @@
"car": "Voiture",
"car-motorcycle": "Car/Motorcycle",
"card": "{n, plural, =1 {Tuile} other {Tuiles}}",
"basque": "Basque",
"categories": "Catégories",
"category": "Catégorie",
"change": "Changement",
@@ -390,6 +390,7 @@
"statistics": "Statistiques",
"stop-drawing": "Arrêter de tracer",
"stop-editing": "Arrêter la modification",
"strava-integration-after-date-hint": "",
"subway-stop": "Subway entrance",
"summit": "Summit",
"summit-book": "Liste des ascensions",

View File

@@ -32,6 +32,7 @@
"bakery": "Bakery",
"barrier": "Barrier",
"basic-info": "Alap információk",
"basque": "Basque",
"before": "Before",
"bicycle-parking": "Bicycle Parking",
"bicycle-rental": "Bicycle Rental",
@@ -56,7 +57,6 @@
"car": "Car",
"car-motorcycle": "Car/Motorcycle",
"card": "{n, plural, =1 {Kártya} other {Kártyák}}",
"basque": "Basque",
"categories": "Kategóriák",
"category": "Kategória",
"change": "Változás",
@@ -390,6 +390,7 @@
"statistics": "Statistics",
"stop-drawing": "Stop drawing",
"stop-editing": "Stop editing",
"strava-integration-after-date-hint": "",
"subway-stop": "Subway entrance",
"summit": "Summit",
"summit-book": "Csúcspont könyv",

View File

@@ -32,6 +32,7 @@
"bakery": "Bakery",
"barrier": "Barrier",
"basic-info": "Informazioni di base",
"basque": "Basque",
"before": "Prima",
"bicycle-parking": "Bicycle Parking",
"bicycle-rental": "Bicycle Rental",
@@ -56,7 +57,6 @@
"car": "Car",
"car-motorcycle": "Car/Motorcycle",
"card": "{n, plural, =1 {Carta} other {Carte}}",
"basque": "Basque",
"categories": "Categorie",
"category": "Categoria",
"change": "Modifica",
@@ -390,6 +390,7 @@
"statistics": "Statistiche",
"stop-drawing": "Smettere di disegnare",
"stop-editing": "Stop editing",
"strava-integration-after-date-hint": "",
"subway-stop": "Subway entrance",
"summit": "Summit",
"summit-book": "Libro di vetta",

View File

@@ -32,6 +32,7 @@
"bakery": "Bakery",
"barrier": "Barrier",
"basic-info": "Algemene informatie",
"basque": "Basque",
"before": "Voor",
"bicycle-parking": "Bicycle Parking",
"bicycle-rental": "Bicycle Rental",
@@ -56,7 +57,6 @@
"car": "Wagen",
"car-motorcycle": "Car/Motorcycle",
"card": "{n, plural, =1 {Kaart} other {Kaarten}}",
"basque": "Basque",
"categories": "Categorieën",
"category": "Categorie",
"change": "Wijzigen",
@@ -390,6 +390,7 @@
"statistics": "Statistieken",
"stop-drawing": "Stop met tekenen",
"stop-editing": "Stop met bewerken",
"strava-integration-after-date-hint": "",
"subway-stop": "Subway entrance",
"summit": "Summit",
"summit-book": "Bergtopboek",

View File

@@ -32,6 +32,7 @@
"bakery": "Bakery",
"barrier": "Barrier",
"basic-info": "Podstawowe informacje",
"basque": "Basque",
"before": "Przed",
"bicycle-parking": "Bicycle Parking",
"bicycle-rental": "Bicycle Rental",
@@ -56,7 +57,6 @@
"car": "Samochód",
"car-motorcycle": "Car/Motorcycle",
"card": "{n, plural, =1 {Karta} other {Karty}}",
"basque": "Basque",
"categories": "Kategorie",
"category": "Kategoria",
"change": "Zmień",
@@ -390,6 +390,7 @@
"statistics": "Statystyki",
"stop-drawing": "Przestań rysować",
"stop-editing": "Zakończ edycję",
"strava-integration-after-date-hint": "",
"subway-stop": "Subway entrance",
"summit": "Summit",
"summit-book": "Logbook",

View File

@@ -32,6 +32,7 @@
"bakery": "Bakery",
"barrier": "Barrier",
"basic-info": "Informações básicas",
"basque": "Basque",
"before": "Antes",
"bicycle-parking": "Bicycle Parking",
"bicycle-rental": "Bicycle Rental",
@@ -56,7 +57,6 @@
"car": "Car",
"car-motorcycle": "Car/Motorcycle",
"card": "{n, plural, =1 {Cartão} other {Cartões}}",
"basque": "Basque",
"categories": "Categorias",
"category": "Categoria",
"change": "Alterar",
@@ -390,6 +390,7 @@
"statistics": "Statistics",
"stop-drawing": "Parar desenho",
"stop-editing": "Stop editing",
"strava-integration-after-date-hint": "",
"subway-stop": "Subway entrance",
"summit": "Summit",
"summit-book": "Livro da cimeira",

View File

@@ -32,6 +32,7 @@
"bakery": "Bakery",
"barrier": "Barrier",
"basic-info": "Основная информация",
"basque": "Basque",
"before": "До",
"bicycle-parking": "Bicycle Parking",
"bicycle-rental": "Bicycle Rental",
@@ -56,7 +57,6 @@
"car": "Автомобиль:",
"car-motorcycle": "Car/Motorcycle",
"card": "{n, plural, =1 {Карточка} other {Карточки}}",
"basque": "Basque",
"categories": "Категории",
"category": "Категория",
"change": "Изменить",
@@ -390,6 +390,7 @@
"statistics": "Статистика",
"stop-drawing": "Закончить рисование",
"stop-editing": "Закончить редактирование",
"strava-integration-after-date-hint": "",
"subway-stop": "Subway entrance",
"summit": "Summit",
"summit-book": "История поездок",

View File

@@ -32,6 +32,7 @@
"bakery": "面包店",
"barrier": "障碍",
"basic-info": "基本信息",
"basque": "Basque",
"before": "之前",
"bicycle-parking": "自行车停车场",
"bicycle-rental": "自行车租车",
@@ -56,7 +57,6 @@
"car": "汽车",
"car-motorcycle": "汽车/摩托车",
"card": "{n, plural, =1 {卡片} other {卡片}}",
"basque": "Basque",
"categories": "分类",
"category": "分类",
"change": "更换",
@@ -390,6 +390,7 @@
"statistics": "统计",
"stop-drawing": "停止绘制",
"stop-editing": "停止编辑",
"strava-integration-after-date-hint": "",
"subway-stop": "地铁入口",
"summit": "山峰",
"summit-book": "详细日程",

View File

@@ -6,7 +6,8 @@ const StravaSchema = z.object({
clientSecret: z.string().length(40).optional().or(z.literal('')),
routes: z.boolean(),
activities: z.boolean(),
active: z.boolean()
active: z.boolean(),
after: z.string().date().optional(),
})
const KomootSchema = z.object({

View File

@@ -18,7 +18,6 @@ const TrailCreateSchema = z.object({
duration: z.number({ coerce: true }).nonnegative().optional(),
photos: z.array(z.string()).default([]),
thumbnail: z.number().int().nonnegative().optional(),
waypoints: z.array(z.string()).default([]),
like_count: z.number().int().min(0).optional().default(0),
category: z.string().length(15).optional().or(z.literal('')),
tags: z.array(z.string()).default([]),
@@ -44,7 +43,6 @@ const TrailUpdateSchema = z.object({
"photos-": z.string().optional(),
"photos+": z.string().optional(),
thumbnail: z.number().int().nonnegative().optional(),
waypoints: z.array(z.string()).optional(),
like_count: z.number().int().min(0).optional().default(0),
category: z.string().optional(),
tags: z.array(z.string()).optional(),

View File

@@ -6,20 +6,21 @@ const WaypointCreateSchema = z.object({
id: z.string().length(15).optional(),
name: z.string().optional(),
description: z.string().optional(),
lat: z.number({coerce: true}).min(-90).max(90),
lon: z.number({coerce: true}).min(-180).max(180),
distance_from_start: z.number({coerce: true}).min(0).optional(),
lat: z.number({ coerce: true }).min(-90).max(90),
lon: z.number({ coerce: true }).min(-180).max(180),
distance_from_start: z.number({ coerce: true }).min(0).optional(),
icon: z.enum(icons).optional(),
author: z.string().length(15),
photos: z.array(z.string()).default([]),
trail: z.string().length(15).optional()
}) satisfies ZodType<Partial<Waypoint>>
const WaypointUpdateSchema = z.object({
name: z.string().optional(),
description: z.string().optional(),
lat: z.number({coerce: true}).min(-90).max(90).optional(),
lon: z.number({coerce: true}).min(-180).max(180).optional(),
distance_from_start: z.number({coerce: true}).min(0).optional(),
lat: z.number({ coerce: true }).min(-90).max(90).optional(),
lon: z.number({ coerce: true }).min(-180).max(180).optional(),
distance_from_start: z.number({ coerce: true }).min(0).optional(),
icon: z.enum(icons).default("circle").optional(),
photos: z.array(z.string()).optional(),
"photos-": z.string().optional(),

View File

@@ -11,6 +11,7 @@ export interface StravaIntegration extends BaseIntegration {
accessToken?: string;
refreshToken?: string;
expiresAt?: number;
after?: string
}
export interface KomootIntegration extends BaseIntegration {

View File

@@ -28,7 +28,6 @@ class Trail {
updated?: string;
category?: string;
tags: string[];
waypoints: string[];
polyline?: string;
domain?: string;
iri?: string;
@@ -36,7 +35,7 @@ class Trail {
expand?: {
tags?: Tag[]
category?: Category;
waypoints?: Waypoint[]
waypoints_via_trail?: Waypoint[]
summit_logs_via_trail?: SummitLog[]
author?: Actor
comments_via_trail?: Comment[]
@@ -90,13 +89,12 @@ class Trail {
this.lon = params?.lon;
this.thumbnail = params?.thumbnail ?? 0;
this.photos = params?.photos ?? [];
this.waypoints = [];
this.tags = []
this.gpx = params?.gpx;
this.like_count = 0
this.expand = {
category: params?.category,
waypoints: params?.waypoints ?? [],
waypoints_via_trail: params?.waypoints ?? [],
summit_logs_via_trail: params?.summit_logs ?? [],
comments_via_trail: params?.comments ?? [],
trail_share_via_trail: params?.shares ?? []
@@ -111,7 +109,7 @@ interface TrailFilter {
q: string,
category: string[],
tags: string[],
difficulty: ("easy" | "moderate" | "difficult")[]
difficulty: (0 | 1 | 2)[]
author?: string;
public?: boolean;
shared?: boolean;
@@ -169,7 +167,7 @@ interface TrailSearchResult {
elevation_gain: number;
elevation_loss: number;
duration: number;
difficulty: "easy" | "moderate" | "difficult";
difficulty: 0 | 1 | 2;
category: string;
completed: boolean;
date: number;

View File

@@ -13,10 +13,12 @@ class Waypoint {
photos: string[];
_photos?: File[];
author: string;
trail?: string;
constructor(lat: number, lon: number, params?: {
id?: string, name?: string, description?: string, icon?: typeof icons[number], marker?: M.Marker, photos?: string[];
id?: string, name?: string, description?: string, icon?: typeof icons[number], marker?: M.Marker, photos?: string[], trail?: string
}) {
this.trail = params?.trail;
this.id = params?.id;
this.name = params?.name ?? "";
this.description = params?.description ?? "";

View File

@@ -22,7 +22,7 @@ export const editTrail: Writable<Trail> = writable(new Trail(""));
export async function trails_index(perPage: number = 21, random: boolean = false, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
const r = await f('/api/v1/trail?' + new URLSearchParams({
"perPage": perPage.toString(),
expand: "category,waypoints,summit_logs_via_trail,tags",
expand: "category,waypoints_via_trail,summit_logs_via_trail,tags",
sort: random ? "@random" : "",
}), {
method: 'GET',
@@ -136,7 +136,7 @@ export async function trails_search_bounding_box(northEast: M.LngLat, southWest:
export async function trails_show(id: string, handle?: string, share?: string, loadGPX?: boolean, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
const r = await f(`/api/v1/trail/${id}?` + new URLSearchParams({
expand: "category,waypoints,summit_logs_via_trail,summit_logs_via_trail.author,trail_share_via_trail.actor,trail_like_via_trail,tags,author",
expand: "category,waypoints_via_trail,summit_logs_via_trail,summit_logs_via_trail.author,trail_share_via_trail.actor,trail_like_via_trail,tags,author",
...(handle ? { handle } : {}),
...(share ? { share } : {})
}), {
@@ -148,7 +148,7 @@ export async function trails_show(id: string, handle?: string, share?: string, l
throw new APIError(r.status, response.message, response.detail)
}
const response = await r.json()
const response: Trail = await r.json()
if (loadGPX) {
if (!response.expand) {
@@ -171,8 +171,8 @@ export async function trails_show(id: string, handle?: string, share?: string, l
}
}
response.expand.waypoints = response.expand.waypoints || [];
response.expand.summit_logs = response.expand.summit_logs?.sort((a: SummitLog, b: SummitLog) => Date.parse(a.date) - Date.parse(b.date)) || [];
response.expand!.waypoints_via_trail = response.expand!.waypoints_via_trail || [];
response.expand!.summit_logs_via_trail = response.expand!.summit_logs_via_trail?.sort((a: SummitLog, b: SummitLog) => Date.parse(a.date) - Date.parse(b.date)) || [];
trail.set(response);
@@ -185,13 +185,6 @@ export async function trails_create(trail: Trail, photos: File[], gpx: File | Bl
throw Error("Unauthenticated")
}
for (const waypoint of trail.expand?.waypoints ?? []) {
const model = await waypoints_create({
...waypoint,
marker: undefined,
}, f, user);
trail.waypoints.push(model.id!);
}
for (const tag of trail.expand?.tags ?? []) {
if (!tag.id) {
const model = await tags_create(tag)
@@ -214,7 +207,7 @@ export async function trails_create(trail: Trail, photos: File[], gpx: File | Bl
}
let r = await f(`/api/v1/trail/form?` + new URLSearchParams({
expand: "category,waypoints,summit_logs_via_trail,trail_share_via_trail,tags",
expand: "category,waypoints_via_trail,summit_logs_via_trail,trail_share_via_trail,tags",
}), {
method: 'PUT',
body: formData,
@@ -232,6 +225,14 @@ export async function trails_create(trail: Trail, photos: File[], gpx: File | Bl
await summit_logs_create(summitLog, f);
}
for (const wp of trail.expand?.waypoints_via_trail ?? []) {
wp.trail = model.id!;
await waypoints_create({
...wp,
marker: undefined,
}, f, user);
}
return model;
}
@@ -239,18 +240,17 @@ export async function trails_create(trail: Trail, photos: File[], gpx: File | Bl
export async function trails_update(oldTrail: Trail, newTrail: Trail, photos?: File[], gpx?: File | Blob | null) {
newTrail.author = oldTrail.author
const waypointUpdates = compareObjectArrays<Waypoint>(oldTrail.expand?.waypoints ?? [], newTrail.expand?.waypoints ?? []);
const waypointUpdates = compareObjectArrays<Waypoint>(oldTrail.expand?.waypoints_via_trail ?? [], newTrail.expand?.waypoints_via_trail ?? []);
for (const addedWaypoint of waypointUpdates.added) {
const model = await waypoints_create({
...addedWaypoint,
marker: undefined,
},);
newTrail.waypoints.push(model.id!);
}
for (const updatedWaypoint of waypointUpdates.updated) {
const oldWaypoint = oldTrail.expand?.waypoints?.find(w => w.id == updatedWaypoint.id);
const oldWaypoint = oldTrail.expand?.waypoints_via_trail?.find(w => w.id == updatedWaypoint.id);
const model = await waypoints_update(oldWaypoint!, {
...updatedWaypoint,
marker: undefined,
@@ -313,7 +313,7 @@ export async function trails_update(oldTrail: Trail, newTrail: Trail, photos?: F
let r = await fetch(`/api/v1/trail/form/${newTrail.id}?` + new URLSearchParams({
expand: "category,waypoints,summit_logs_via_trail,trail_share_via_trail,tags",
expand: "category,waypoints_via_trail,summit_logs_via_trail,trail_share_via_trail,tags",
}), {
method: 'POST',
body: formData,
@@ -340,17 +340,6 @@ export async function trails_update(oldTrail: Trail, newTrail: Trail, photos?: F
export async function trails_delete(trail: Trail) {
if (trail.expand?.waypoints) {
for (const waypoint of trail.expand.waypoints) {
await waypoints_delete(waypoint);
}
}
if (trail.expand?.summit_logs_via_trail) {
for (const summit_log of trail.expand.summit_logs_via_trail) {
await summit_logs_delete(summit_log);
}
}
const r = await fetch('/api/v1/trail/' + trail.id, {
method: 'DELETE',
})
@@ -457,7 +446,7 @@ export async function searchResultToTrailList(hits: Hits<TrailSearchResult>): Pr
created: new Date(h.created * 1000).toISOString(),
date: new Date(h.date * 1000).toISOString(),
description: h.description,
difficulty: h.difficulty,
difficulty: h.difficulty == 0 ? "easy" : h.difficulty == 1 ? "moderate" : "difficult",
distance: h.distance,
duration: h.duration,
elevation_gain: h.elevation_gain,

View File

@@ -45,7 +45,7 @@ export async function gpx2trail(gpxString: string, fallbackName?: string, correc
wp.id = cryptoRandomString({ length: 15 });
wp.name = wpt.name ?? ""
wp.description = wpt.desc;
trail.expand!.waypoints?.push(wp);
trail.expand!.waypoints_via_trail?.push(wp);
}
const totals = gpx.features
@@ -108,7 +108,7 @@ export async function trail2gpx(trail: Trail, user?: AuthRecord) {
gpx.wpt = [];
}
for (const wp of gpxTrail.expand!.waypoints ?? []) {
for (const wp of gpxTrail.expand!.waypoints_via_trail ?? []) {
const gpxWpt = gpx.wpt.find((w) => w.$.lat == wp.lat && w.$.lon == wp.lon)
if (!gpxWpt) {
gpx.wpt.push(new GPXWaypoint({

View File

@@ -20,7 +20,7 @@ export async function GET(event: RequestEvent) {
const [username, domain] = splitUsername(fullUsername, env.ORIGIN)
const actor: Actor = await event.locals.pb.collection("activitypub_actors").getFirstListItem(`preferred_username:lower='${username?.toLowerCase()}'&&isLocal=true`)
const actor: Actor = await event.locals.pb.collection("activitypub_actors").getFirstListItem(`preferred_username:lower='${username?.toLowerCase()}'&&isLocal=1`)
const user: UserAnonymous = await event.locals.pb.collection("users_anonymous").getOne(actor.user!)

View File

@@ -15,7 +15,7 @@ export async function GET(event: RequestEvent) {
t.expand = {} as any
}
t.expand?.waypoints?.sort((a, b) => (a.distance_from_start ?? 0) - (b.distance_from_start ?? 0))
t.expand?.waypoints_via_trail?.sort((a, b) => (a.distance_from_start ?? 0) - (b.distance_from_start ?? 0))
}
return json(r)
} catch (e: any) {

View File

@@ -84,7 +84,7 @@ export async function GET(event: RequestEvent) {
l.expand.author.isLocal = false
}
})
t.expand?.waypoints?.forEach(w => {
t.expand?.waypoints_via_trail?.forEach(w => {
w.photos = w.photos.map(p =>
`${origin}/api/v1/files/waypoints/${w.id}/${p}`
@@ -131,7 +131,7 @@ export async function GET(event: RequestEvent) {
await enrichRecord(event.locals.pb, t);
// sort waypoints by distance
t.expand?.waypoints?.sort((a, b) => (a.distance_from_start ?? 0) - (b.distance_from_start ?? 0))
t.expand?.waypoints_via_trail?.sort((a, b) => (a.distance_from_start ?? 0) - (b.distance_from_start ?? 0))
return json(t)
} catch (e: any) {
return handleError(e)

View File

@@ -71,7 +71,7 @@
let selectedTrailIndex = $derived(selectedTrail ? 0 : null);
let selectedTrailWaypoints = $derived(
(selectedTrail as Trail | null)?.expand?.waypoints,
(selectedTrail as Trail | null)?.expand?.waypoints_via_trail,
);
onMount(() => {

View File

@@ -101,7 +101,7 @@
const trailItems = r[0].hits.map((t: TrailSearchResult) => ({
text: t.name,
description: `Trail ${t.location.length ? ", " + t.location : ""}`,
value: `@${t.author}${t.domain ? `@${t.domain}` : ""}/${t.id}`,
value: `@${t.author_name}${t.domain ? `@${t.domain}` : ""}/${t.id}`,
icon: "route",
}));
const listItems = r[1].hits.map((t: ListSearchResult) => ({

View File

@@ -11,7 +11,7 @@ export const load: ServerLoad = async ({ params, locals, fetch }) => {
q: "",
category: [],
tags: [],
difficulty: ["easy", "moderate", "difficult"],
difficulty: [0, 1, 2],
author: "",
public: true,
shared: true,

View File

@@ -45,7 +45,7 @@
<div id="trail-details">
<MapWithElevationMaplibre
trails={[trail]}
waypoints={trail.expand?.waypoints}
waypoints={trail.expand?.waypoints_via_trail}
activeTrail={0}
bind:markers
showTerrain={true}

View File

@@ -390,7 +390,7 @@
currentHeight += getTextHeight($trail.description, doc, width - 32) + 8;
}
if (includeWaypoints && $trail.expand?.waypoints) {
if (includeWaypoints && $trail.expand?.waypoints_via_trail) {
const header = $_("waypoints", { values: { n: 2 } })
let textHeight = getTextHeight(header, doc, width - 32)
if (currentHeight + textHeight + 8 > height) {
@@ -401,7 +401,7 @@
currentHeight += textHeight + 8;
doc.setFont("IBMPlexSans-Regular", "normal");
($trail.expand.waypoints || []).forEach(waypoint => {
($trail.expand.waypoints_via_trail || []).forEach(waypoint => {
let description = waypoint.description || "";
let name = waypoint.name || "";
@@ -616,7 +616,7 @@
<div class="basis-full">
<MapWithElevationMaplibre
trails={[$trail]}
waypoints={$trail.expand?.waypoints}
waypoints={$trail.expand?.waypoints_via_trail}
activeTrail={0}
onzoom={updateScale}
bind:map

View File

@@ -136,7 +136,7 @@
<div class="space-y-4">
<h4 class="text-xl font-semibold">Timeline</h4>
{#if !feed.items?.length && data.isOwnProfile}
<a class="btn-primary inline-block" href="/trails/edit/new"
<a class="btn-primary inline-block" href="/trail/edit/new"
>+ {$_("new-trail")}</a
>
{:else if !feed.items?.length}

View File

@@ -12,7 +12,7 @@ export const load: Load = async ({ params, fetch, parent }) => {
q: "",
category: [],
tags: [],
difficulty: ["easy", "moderate", "difficult"],
difficulty: [0, 1, 2],
author: actor.id,
public: true,
shared: true,

View File

@@ -139,7 +139,7 @@
summit_logs_via_trail: z
.array(SummitLogCreateSchema)
.optional(),
waypoints: z
waypoints_via_trail: z
.array(
WaypointCreateSchema.extend({
marker: z.any().optional(),
@@ -384,11 +384,10 @@
}
function clearWaypoints() {
for (const waypoint of $formData.expand!.waypoints ?? []) {
for (const waypoint of $formData.expand!.waypoints_via_trail ?? []) {
waypoint.marker?.remove();
}
$formData.expand!.waypoints = [];
$formData.waypoints = [];
$formData.expand!.waypoints_via_trail = [];
}
function initRouteAnchors(gpx: GPX, addToMap: boolean = false) {
@@ -445,29 +444,28 @@
}
function deleteWaypoint(index: number) {
const wp = $formData.expand!.waypoints?.splice(index, 1);
$formData.waypoints.splice(index, 1);
const wp = $formData.expand!.waypoints_via_trail?.splice(index, 1);
if (!$formData.expand!.waypoints?.length) {
$formData.expand!.waypoints = [];
if (!$formData.expand!.waypoints_via_trail?.length) {
$formData.expand!.waypoints_via_trail = [];
}
$formData.expand!.waypoints = $formData.expand!.waypoints;
$formData.expand!.waypoints_via_trail = $formData.expand!.waypoints_via_trail;
// updateTrailOnMap();
}
function saveWaypoint(savedWaypoint: Waypoint) {
let editedWaypointIndex =
$formData.expand!.waypoints?.findIndex(
$formData.expand!.waypoints_via_trail?.findIndex(
(s) => s.id == savedWaypoint.id,
) ?? -1;
if (editedWaypointIndex >= 0) {
$formData.expand!.waypoints![editedWaypointIndex] = savedWaypoint;
$formData.expand!.waypoints_via_trail![editedWaypointIndex] = savedWaypoint;
} else {
savedWaypoint.id = cryptoRandomString({ length: 15 });
$formData.expand!.waypoints = [
...($formData.expand!.waypoints ?? []),
$formData.expand!.waypoints_via_trail = [
...($formData.expand!.waypoints_via_trail ?? []),
savedWaypoint,
];
@@ -478,15 +476,15 @@
function moveMarker(marker: M.Marker, wpId?: string) {
const position = marker.getLngLat();
const editableWaypointIndex =
$formData.expand!.waypoints?.findIndex((w) => w.id == wpId) ?? -1;
$formData.expand!.waypoints_via_trail?.findIndex((w) => w.id == wpId) ?? -1;
const editableWaypoint =
$formData.expand!.waypoints![editableWaypointIndex];
$formData.expand!.waypoints_via_trail![editableWaypointIndex];
if (!editableWaypoint) {
return;
}
editableWaypoint.lat = position.lat;
editableWaypoint.lon = position.lng;
$formData.expand!.waypoints = [...($formData.expand!.waypoints ?? [])];
$formData.expand!.waypoints_via_trail = [...($formData.expand!.waypoints_via_trail ?? [])];
// updateTrailOnMap();
}
@@ -1369,7 +1367,7 @@
{$_("waypoints", { values: { n: 2 } })}
</h3>
<ul>
{#each $formData.expand?.waypoints ?? [] as waypoint, i}
{#each $formData.expand?.waypoints_via_trail ?? [] as waypoint, i}
<li
onmouseenter={() => openMarkerPopup(waypoint)}
onmouseleave={() => openMarkerPopup(waypoint)}
@@ -1501,7 +1499,7 @@
<div id="trail-map">
<MapWithElevationMaplibre
trails={mapTrail}
waypoints={$formData.expand?.waypoints}
waypoints={$formData.expand?.waypoints_via_trail}
drawing={drawingActive}
showTerrain={true}
onmarkerdragend={moveMarker}

View File

@@ -25,7 +25,24 @@
export const snapshot: Snapshot<TrailFilter> = {
capture: () => filter,
restore: (value) => {
filter = value;
const difficultyMap: Record<string, 0 | 1 | 2> = {
easy: 0,
moderate: 1,
difficult: 2,
};
// defensive copy
const migrated = { ...value };
if (Array.isArray(migrated.difficulty)) {
migrated.difficulty = migrated.difficulty.map((d: any) => {
if (typeof d === "string" && d in difficultyMap) {
return difficultyMap[d];
}
return d;
});
}
filter = migrated;
handleFilterUpdate();
},
};

View File

@@ -10,7 +10,7 @@ export const load: ServerLoad = async ({ params, locals, url, fetch }) => {
q: "",
category: [],
tags: [],
difficulty: ["easy", "moderate", "difficult"],
difficulty: [0, 1, 2],
author: "",
public: true,
shared: true,