Merge branch 'main' into i10n_main
This commit is contained in:
10
.github/workflows/release.yaml
vendored
10
.github/workflows/release.yaml
vendored
@@ -18,11 +18,11 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# 1. Checkout the repository
|
# 1. Checkout the repository
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
# 2. Setup node & npm
|
# 2. Setup node & npm
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
|
|
||||||
@@ -53,12 +53,12 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# 1. Checkout the repository
|
# 1. Checkout the repository
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.ref }}
|
ref: ${{ github.ref }}
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.22'
|
go-version: '1.22'
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# 1. Checkout the repository
|
# 1. Checkout the repository
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.ref }}
|
ref: ${{ github.ref }}
|
||||||
# 2. Extract release notes from CHANGELOG.md
|
# 2. Extract release notes from CHANGELOG.md
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -9,6 +9,6 @@ search/dumps
|
|||||||
|
|
||||||
run.sh
|
run.sh
|
||||||
build*.sh
|
build*.sh
|
||||||
start.*
|
start*.*
|
||||||
|
|
||||||
data*/
|
data*/
|
||||||
|
|||||||
14
CHANGELOG.md
14
CHANGELOG.md
@@ -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
|
# v0.18.1
|
||||||
## Bug fixes
|
## Bug fixes
|
||||||
- Fixes permission issues that prevented federation from working properly
|
- Fixes permission issues that prevented federation from working properly
|
||||||
|
|||||||
121
db/commands/dedup.go
Normal file
121
db/commands/dedup.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -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))
|
app.Logger().Warn(fmt.Sprintf("Unable to generate GPX for tour '%s': %v", tour.Name, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
wpIds, err := createWaypointsFromTour(app, detailedTour, user)
|
trailid, err := createTrailFromTour(app, k, detailedTour, gpx, actor)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.Logger().Warn(fmt.Sprintf("Unable to create trail for tour '%s': %v", tour.Name, err))
|
app.Logger().Warn(fmt.Sprintf("Unable to create trail for tour '%s': %v", tour.Name, err))
|
||||||
continue
|
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
|
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)
|
trailid := security.RandomStringWithAlphabet(core.DefaultIdLength, core.DefaultIdAlphabet)
|
||||||
|
|
||||||
collection, err := app.FindCollectionByNameOrId("trails")
|
collection, err := app.FindCollectionByNameOrId("trails")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
record := core.NewRecord(collection)
|
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 {
|
if len(detailedTour.Embedded.CoverImages.Embedded.Items) > 0 {
|
||||||
photos, err = fetchRoutePhotos(k, detailedTour)
|
photos, err = fetchRoutePhotos(k, detailedTour)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
photo, err := fetchPhoto(detailedTour.MapImage.Src, "", "")
|
photo, err := fetchPhoto(detailedTour.MapImage.Src, "", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
photos = append(photos, photo)
|
photos = append(photos, photo)
|
||||||
}
|
}
|
||||||
@@ -281,7 +281,6 @@ func createTrailFromTour(app core.App, k *KomootApi, detailedTour *DetailedKomoo
|
|||||||
"lon": detailedTour.StartPoint.Lng,
|
"lon": detailedTour.StartPoint.Lng,
|
||||||
"difficulty": diffculty,
|
"difficulty": diffculty,
|
||||||
"category": categoryId,
|
"category": categoryId,
|
||||||
"waypoints": wpIds,
|
|
||||||
"author": actor,
|
"author": actor,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -293,13 +292,13 @@ func createTrailFromTour(app core.App, k *KomootApi, detailedTour *DetailedKomoo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := app.Save(record); err != nil {
|
if err := app.Save(record); err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if detailedTour.Type == "tour_recorded" {
|
if detailedTour.Type == "tour_recorded" {
|
||||||
collection, err := app.FindCollectionByNameOrId("summit_logs")
|
collection, err := app.FindCollectionByNameOrId("summit_logs")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
summitLogRecord := core.NewRecord(collection)
|
summitLogRecord := core.NewRecord(collection)
|
||||||
@@ -313,25 +312,23 @@ func createTrailFromTour(app core.App, k *KomootApi, detailedTour *DetailedKomoo
|
|||||||
"trail": trailid,
|
"trail": trailid,
|
||||||
})
|
})
|
||||||
if err := app.Save(summitLogRecord); err != nil {
|
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")
|
collection, err := app.FindCollectionByNameOrId("waypoints")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
wpIds := make([]string, len(tour.Embedded.Timeline.Embedded.Items))
|
for _, wp := range tour.Embedded.Timeline.Embedded.Items {
|
||||||
|
|
||||||
for i, wp := range tour.Embedded.Timeline.Embedded.Items {
|
|
||||||
photos, err := fetchWaypointPhotos(wp)
|
photos, err := fetchWaypointPhotos(wp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
record := core.NewRecord(collection)
|
record := core.NewRecord(collection)
|
||||||
|
|
||||||
@@ -358,6 +355,7 @@ func createWaypointsFromTour(app core.App, tour *DetailedKomootTour, user string
|
|||||||
"icon": "circle",
|
"icon": "circle",
|
||||||
"author": user,
|
"author": user,
|
||||||
"distance_from_start": 0,
|
"distance_from_start": 0,
|
||||||
|
"trail": trailid,
|
||||||
})
|
})
|
||||||
|
|
||||||
if photos != nil {
|
if photos != nil {
|
||||||
@@ -365,13 +363,11 @@ func createWaypointsFromTour(app core.App, tour *DetailedKomootTour, user string
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := app.Save(record); err != nil {
|
if err := app.Save(record); err != nil {
|
||||||
return nil, err
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wpIds[i] = record.Id
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
return wpIds, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchRoutePhotos(k *KomootApi, tour *DetailedKomootTour) ([]*filesystem.File, error) {
|
func fetchRoutePhotos(k *KomootApi, tour *DetailedKomootTour) ([]*filesystem.File, error) {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type StravaIntegration struct {
|
|||||||
AccessToken string `json:"accessToken,omitempty"`
|
AccessToken string `json:"accessToken,omitempty"`
|
||||||
RefreshToken string `json:"refreshToken,omitempty"`
|
RefreshToken string `json:"refreshToken,omitempty"`
|
||||||
ExpiresAt int64 `json:"expiresAt,omitempty"`
|
ExpiresAt int64 `json:"expiresAt,omitempty"`
|
||||||
|
After string `json:"after,omitempty"`
|
||||||
}
|
}
|
||||||
type StravaRoute struct {
|
type StravaRoute struct {
|
||||||
Athlete Athlete `json:"athlete"`
|
Athlete Athlete `json:"athlete"`
|
||||||
|
|||||||
@@ -89,22 +89,12 @@ func SyncStrava(app core.App) error {
|
|||||||
stravaIntegration.ExpiresAt = r.ExpiresAt
|
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 {
|
if stravaIntegration.Routes {
|
||||||
page := 1
|
page := 1
|
||||||
hasNewRoutes := true
|
hasMore := true
|
||||||
for hasNewRoutes {
|
for hasMore {
|
||||||
|
|
||||||
routes, err := fetchStravaRoutes(r.AccessToken, page)
|
routes, err := fetchStravaRoutes(r.AccessToken, page)
|
||||||
|
hasMore = len(routes) > 0
|
||||||
page += 1
|
page += 1
|
||||||
if err != nil {
|
if err != nil {
|
||||||
warning := fmt.Sprintf("error fetching routes from strava: %v\n", err)
|
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)
|
app.Logger().Warn(warning)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
hasNewRoutes, err = syncTrailsWithRoutes(app, r.AccessToken, userId, actorId, routes)
|
err = syncTrailsWithRoutes(app, r.AccessToken, userId, actorId, routes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
warning := fmt.Sprintf("error syncing strava routes with trails: %v\n", err)
|
warning := fmt.Sprintf("error syncing strava routes with trails: %v\n", err)
|
||||||
fmt.Print(warning)
|
fmt.Print(warning)
|
||||||
@@ -123,9 +113,20 @@ func SyncStrava(app core.App) error {
|
|||||||
}
|
}
|
||||||
if stravaIntegration.Activities {
|
if stravaIntegration.Activities {
|
||||||
page := 1
|
page := 1
|
||||||
hasNewActivities := true
|
hasMore := true
|
||||||
for hasNewActivities {
|
for hasMore {
|
||||||
activities, err := fetchStravaActivities(r.AccessToken, page)
|
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
|
page += 1
|
||||||
if err != nil {
|
if err != nil {
|
||||||
warning := fmt.Sprintf("error fetching activities from strava: %v", err)
|
warning := fmt.Sprintf("error fetching activities from strava: %v", err)
|
||||||
@@ -133,7 +134,8 @@ func SyncStrava(app core.App) error {
|
|||||||
app.Logger().Warn(warning)
|
app.Logger().Warn(warning)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
hasNewActivities, err = syncTrailsWithActivities(app, r.AccessToken, userId, actorId, activities)
|
err = syncTrailsWithActivities(app, r.AccessToken, actorId, activities)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
warning := fmt.Sprintf("error syncing strava activities with trails: %v", err)
|
warning := fmt.Sprintf("error syncing strava activities with trails: %v", err)
|
||||||
fmt.Print(warning)
|
fmt.Print(warning)
|
||||||
@@ -141,6 +143,17 @@ func SyncStrava(app core.App) error {
|
|||||||
continue
|
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
|
return routes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchStravaActivities(accessToken string, page int) ([]StravaActivity, error) {
|
func fetchStravaActivities(accessToken string, page int, after int64) ([]StravaActivity, error) {
|
||||||
stravaRoutesURL := fmt.Sprintf("https://www.strava.com/api/v3/athlete/activities?page=%d", page)
|
stravaRoutesURL := fmt.Sprintf("https://www.strava.com/api/v3/athlete/activities?page=%d&after=%d", page, after)
|
||||||
req, err := http.NewRequest("GET", stravaRoutesURL, nil)
|
req, err := http.NewRequest("GET", stravaRoutesURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -235,36 +248,33 @@ func fetchStravaActivities(accessToken string, page int) ([]StravaActivity, erro
|
|||||||
return activities, nil
|
return activities, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncTrailsWithRoutes(app core.App, accessToken string, user string, actor string, routes []StravaRoute) (bool, error) {
|
func syncTrailsWithRoutes(app core.App, accessToken string, user string, actor string, routes []StravaRoute) error {
|
||||||
hasNewRoutes := false
|
|
||||||
for _, route := range routes {
|
for _, route := range routes {
|
||||||
trails, err := app.FindRecordsByFilter("trails", "external_id = {:id}", "", 1, 0, dbx.Params{"id": route.IDStr})
|
trails, err := app.FindRecordsByFilter("trails", "external_id = {:id}", "", 1, 0, dbx.Params{"id": route.IDStr})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return hasNewRoutes, err
|
return err
|
||||||
}
|
}
|
||||||
if len(trails) != 0 {
|
if len(trails) != 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
hasNewRoutes = true
|
|
||||||
gpx, err := fetchRouteGPX(route, accessToken)
|
gpx, err := fetchRouteGPX(route, accessToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.Logger().Warn(fmt.Sprintf("Unable to fetch GPX for route '%s': %v", route.Name, err))
|
app.Logger().Warn(fmt.Sprintf("Unable to fetch GPX for route '%s': %v", route.Name, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
wpIds, err := createWaypointsFromRoute(app, route, user)
|
trailid, err := createTrailFromRoute(app, route, gpx, actor)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.Logger().Warn(fmt.Sprintf("Unable to create trail for route '%s': %v", route.Name, err))
|
app.Logger().Warn(fmt.Sprintf("Unable to create trail for route '%s': %v", route.Name, err))
|
||||||
continue
|
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) {
|
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
|
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")
|
collection, err := app.FindCollectionByNameOrId("trails")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
record := core.NewRecord(collection)
|
record := core.NewRecord(collection)
|
||||||
@@ -337,6 +349,7 @@ func createTrailFromRoute(app core.App, route StravaRoute, gpx *filesystem.File,
|
|||||||
}
|
}
|
||||||
|
|
||||||
record.Load(map[string]any{
|
record.Load(map[string]any{
|
||||||
|
"id": trailid,
|
||||||
"name": route.Name,
|
"name": route.Name,
|
||||||
"description": route.Description,
|
"description": route.Description,
|
||||||
"public": !route.Private,
|
"public": !route.Private,
|
||||||
@@ -348,7 +361,6 @@ func createTrailFromRoute(app core.App, route StravaRoute, gpx *filesystem.File,
|
|||||||
"external_id": route.IDStr,
|
"external_id": route.IDStr,
|
||||||
"lat": lat,
|
"lat": lat,
|
||||||
"lon": lon,
|
"lon": lon,
|
||||||
"waypoints": wpIds,
|
|
||||||
"difficulty": "easy",
|
"difficulty": "easy",
|
||||||
"category": category,
|
"category": category,
|
||||||
"author": actor,
|
"author": actor,
|
||||||
@@ -359,20 +371,18 @@ func createTrailFromRoute(app core.App, route StravaRoute, gpx *filesystem.File,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := app.Save(record); err != nil {
|
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")
|
collection, err := app.FindCollectionByNameOrId("waypoints")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
wpIds := make([]string, len(route.Waypoints))
|
|
||||||
|
|
||||||
for i, wp := range route.Waypoints {
|
for i, wp := range route.Waypoints {
|
||||||
record := core.NewRecord(collection)
|
record := core.NewRecord(collection)
|
||||||
|
|
||||||
@@ -383,26 +393,26 @@ func createWaypointsFromRoute(app core.App, route StravaRoute, user string) ([]s
|
|||||||
record.Set("icon", "circle")
|
record.Set("icon", "circle")
|
||||||
record.Set("author", user)
|
record.Set("author", user)
|
||||||
record.Set("distance_from_start", wp.DistanceIntoRoute)
|
record.Set("distance_from_start", wp.DistanceIntoRoute)
|
||||||
|
record.Set("trail", trailid)
|
||||||
|
|
||||||
app.Save(record)
|
if err := app.Save(record); err != nil {
|
||||||
|
return err
|
||||||
wpIds[i] = record.Id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return wpIds, nil
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncTrailsWithActivities(app core.App, accessToken string, user string, actor string, activities []StravaActivity) (bool, error) {
|
func syncTrailsWithActivities(app core.App, accessToken string, actor string, activities []StravaActivity) error {
|
||||||
hasNewActivites := false
|
|
||||||
for _, activity := range activities {
|
for _, activity := range activities {
|
||||||
trails, err := app.FindRecordsByFilter("trails", "external_id = {:id}", "", 1, 0, dbx.Params{"id": strconv.Itoa(int(activity.ID))})
|
trails, err := app.FindRecordsByFilter("trails", "external_id = {:id}", "", 1, 0, dbx.Params{"id": strconv.Itoa(int(activity.ID))})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return hasNewActivites, err
|
return err
|
||||||
}
|
}
|
||||||
if len(trails) != 0 {
|
if len(trails) != 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
hasNewActivites = true
|
|
||||||
detailedActivity, err := fetchDetailedActivity(activity, accessToken)
|
detailedActivity, err := fetchDetailedActivity(activity, accessToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.Logger().Warn(fmt.Sprintf("Unable to fetch detailed activity '%s': %v", activity.Name, err))
|
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))
|
app.Logger().Warn(fmt.Sprintf("Unable to create trail from activity '%s': %v", activity.Name, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return hasNewActivites, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchDetailedActivity(activity StravaActivity, accessToken string) (*DetailedStravaActivity, error) {
|
func fetchDetailedActivity(activity StravaActivity, accessToken string) (*DetailedStravaActivity, error) {
|
||||||
|
|||||||
129
db/main.go
129
db/main.go
@@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/pocketbase/pocketbase/tools/security"
|
"github.com/pocketbase/pocketbase/tools/security"
|
||||||
"github.com/spf13/cast"
|
"github.com/spf13/cast"
|
||||||
|
|
||||||
|
"pocketbase/commands"
|
||||||
"pocketbase/federation"
|
"pocketbase/federation"
|
||||||
"pocketbase/integrations/komoot"
|
"pocketbase/integrations/komoot"
|
||||||
"pocketbase/integrations/strava"
|
"pocketbase/integrations/strava"
|
||||||
@@ -73,6 +74,8 @@ func main() {
|
|||||||
registerMigrations(app)
|
registerMigrations(app)
|
||||||
setupEventHandlers(app, client)
|
setupEventHandlers(app, client)
|
||||||
|
|
||||||
|
setupCommands(app)
|
||||||
|
|
||||||
if err := app.Start(); err != nil {
|
if err := app.Start(); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -140,6 +143,10 @@ func setupEventHandlers(app *pocketbase.PocketBase, client meilisearch.ServiceMa
|
|||||||
app.OnBootstrap().BindFunc(onBootstrapHandler())
|
app.OnBootstrap().BindFunc(onBootstrapHandler())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setupCommands(app *pocketbase.PocketBase) {
|
||||||
|
app.RootCmd.AddCommand(commands.Dedup(app))
|
||||||
|
}
|
||||||
|
|
||||||
func sanitizeHTML() func(e *core.RecordRequestEvent) error {
|
func sanitizeHTML() func(e *core.RecordRequestEvent) error {
|
||||||
return func(e *core.RecordRequestEvent) error {
|
return func(e *core.RecordRequestEvent) error {
|
||||||
fieldsToSanitize := map[string][]string{
|
fieldsToSanitize := map[string][]string{
|
||||||
@@ -233,7 +240,7 @@ func createTrailHandler(client meilisearch.ServiceManager) func(e *core.RecordEv
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
return err
|
||||||
}
|
}
|
||||||
if !author.GetBool("isLocal") {
|
if !author.GetBool("isLocal") {
|
||||||
@@ -340,12 +347,7 @@ func createSummitLogHandler(client meilisearch.ServiceManager) func(e *core.Reco
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
trailAuthor, err := e.App.FindFirstRecordByData("activitypub_actors", "id", trail.GetString("author"))
|
if err := util.IndexTrails(e.App, []*core.Record{trail}, client); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := util.IndexTrail(e.App, trail, trailAuthor, client); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,12 +393,7 @@ func deleteSummitLogHandler(client meilisearch.ServiceManager) func(e *core.Reco
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
trailAuthor, err := e.App.FindFirstRecordByData("activitypub_actors", "id", trail.GetString("author"))
|
if err := util.IndexTrails(e.App, []*core.Record{trail}, client); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := util.IndexTrail(e.App, trail, trailAuthor, client); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,7 +613,7 @@ func createListHandler(client meilisearch.ServiceManager) func(e *core.RecordEve
|
|||||||
return err
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1308,94 +1305,62 @@ func bootstrapCategories(app core.App) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func bootstrapMeilisearchDocuments(app core.App, client meilisearch.ServiceManager) 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{}
|
trails := []*core.Record{}
|
||||||
|
err := app.RecordQuery("trails").
|
||||||
if err := query.All(&trails); err != nil {
|
Limit(pageSize).
|
||||||
return err
|
Offset(page * pageSize).
|
||||||
}
|
All(&trails)
|
||||||
|
|
||||||
_, err := client.Index("trails").DeleteAllDocuments()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, trail := range trails {
|
if len(trails) == 0 {
|
||||||
author, err := app.FindRecordById("activitypub_actors", trail.GetString(("author")))
|
break
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
shares, err := app.FindAllRecords("trail_share",
|
page++
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lists, err := app.FindAllRecords("lists")
|
// --- Lists ---
|
||||||
if err != nil {
|
if _, err := client.Index("lists").DeleteAllDocuments(); err != nil {
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = client.Index("lists").DeleteAllDocuments()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, list := range lists {
|
page = 0
|
||||||
author, err := app.FindRecordById("activitypub_actors", list.GetString(("author")))
|
for {
|
||||||
|
lists := []*core.Record{}
|
||||||
|
err := app.RecordQuery("lists").
|
||||||
|
Limit(pageSize).
|
||||||
|
Offset(page * pageSize).
|
||||||
|
All(&lists)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := util.IndexList(app, list, author, client); err != nil {
|
if len(lists) == 0 {
|
||||||
app.Logger().Warn(fmt.Sprintf("Unable to index list '%s': %v", list.GetString("name"), err))
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
shares, err := app.FindAllRecords("list_share",
|
page++
|
||||||
dbx.NewExp("list = {:listId}", dbx.Params{"listId": list.Id}),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
42
db/migrations/1756471290_updated_trails.go
Normal file
42
db/migrations/1756471290_updated_trails.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
42
db/migrations/1756473199_updated_waypoints.go
Normal file
42
db/migrations/1756473199_updated_waypoints.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
62
db/migrations/1757689107_updated_waypoints.go
Normal file
62
db/migrations/1757689107_updated_waypoints.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
34
db/migrations/1757689185_migrate_waypoint_trails.go
Normal file
34
db/migrations/1757689185_migrate_waypoint_trails.go
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
44
db/migrations/1757690946_updated_trails.go
Normal file
44
db/migrations/1757690946_updated_trails.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
46
db/migrations/1757759342_updated_activitypub_actors.go
Normal file
46
db/migrations/1757759342_updated_activitypub_actors.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -162,7 +163,19 @@ func TrailFromActivity(activity pub.Activity, app core.App, actor *core.Record)
|
|||||||
return nil, err
|
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 != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
collection, err := app.FindCollectionByNameOrId("trails")
|
collection, err := app.FindCollectionByNameOrId("trails")
|
||||||
@@ -279,7 +292,7 @@ func TrailFromActivity(activity pub.Activity, app core.App, actor *core.Record)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(photoURLs) > 0 {
|
if len(photoURLs) > 0 {
|
||||||
photos := make([]*filesystem.File, len(photoURLs))
|
photos := []*filesystem.File{}
|
||||||
for i, purl := range photoURLs {
|
for i, purl := range photoURLs {
|
||||||
photo, err := filesystem.NewFileFromURL(context.Background(), purl)
|
photo, err := filesystem.NewFileFromURL(context.Background(), purl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ func documentFromTrailRecord(app core.App, r *core.Record, author *core.Record,
|
|||||||
"elevation_gain": r.GetFloat("elevation_gain"),
|
"elevation_gain": r.GetFloat("elevation_gain"),
|
||||||
"elevation_loss": r.GetFloat("elevation_loss"),
|
"elevation_loss": r.GetFloat("elevation_loss"),
|
||||||
"duration": r.GetFloat("duration"),
|
"duration": r.GetFloat("duration"),
|
||||||
"difficulty": r.Get("difficulty"),
|
"difficulty": difficultyToNumber(r.GetString("difficulty")),
|
||||||
"category": category,
|
"category": category,
|
||||||
"completed": logCount > 0,
|
"completed": logCount > 0,
|
||||||
"date": r.GetDateTime("date").Time().Unix(),
|
"date": r.GetDateTime("date").Time().Unix(),
|
||||||
@@ -88,15 +88,52 @@ func documentFromTrailRecord(app core.App, r *core.Record, author *core.Record,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if includeShares {
|
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{}
|
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["likes"] = []string{}
|
||||||
document["like_count"] = 0
|
document["like_count"] = 0
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return document, nil
|
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) {
|
func getPolyline(app core.App, r *core.Record) (string, error) {
|
||||||
gpxPath := r.GetString("gpx")
|
gpxPath := r.GetString("gpx")
|
||||||
if len(gpxPath) == 0 {
|
if len(gpxPath) == 0 {
|
||||||
@@ -173,7 +210,7 @@ func documentFromListRecord(r *core.Record, author *core.Record, includeShares b
|
|||||||
domain = author.GetString("domain")
|
domain = author.GetString("domain")
|
||||||
}
|
}
|
||||||
|
|
||||||
document := map[string]interface{}{
|
document := map[string]any{
|
||||||
"id": r.Id,
|
"id": r.Id,
|
||||||
"author": author.Id,
|
"author": author.Id,
|
||||||
"author_name": author.GetString("preferred_username"),
|
"author_name": author.GetString("preferred_username"),
|
||||||
@@ -193,8 +230,19 @@ func documentFromListRecord(r *core.Record, author *core.Record, includeShares b
|
|||||||
}
|
}
|
||||||
|
|
||||||
if includeShares {
|
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{}
|
document["shares"] = []string{}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return document, nil
|
return document, nil
|
||||||
}
|
}
|
||||||
@@ -253,7 +301,10 @@ func documentFromRemoteRecord(r *core.Record, index string) (map[string]interfac
|
|||||||
return document, nil
|
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)
|
errs := app.ExpandRecord(r, []string{"tags"}, nil)
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
return fmt.Errorf("failed to expand tags: %v", errs)
|
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 {
|
if len(errs) > 0 {
|
||||||
return fmt.Errorf("failed to expand category: %v", errs)
|
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)
|
doc, err := documentFromTrailRecord(app, r, author, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
documents := []map[string]interface{}{doc}
|
|
||||||
|
documents[i] = doc
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := client.Index("trails").AddDocuments(documents); err != nil {
|
if _, err := client.Index("trails").AddDocuments(documents); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -325,17 +393,32 @@ func UpdateTrailLikes(trailId string, likes []string, client meilisearch.Service
|
|||||||
return nil
|
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)
|
errs := app.ExpandRecord(r, []string{"trails"}, nil)
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
return fmt.Errorf("failed to expand trails: %v", errs)
|
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 {
|
if err != nil {
|
||||||
return err
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,19 @@
|
|||||||
title: Changelog
|
title: Changelog
|
||||||
description: What changed in the last patch?
|
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
|
## v0.18.1
|
||||||
### Bug fixes
|
### Bug fixes
|
||||||
- Fixes permission issues that prevented federation from working properly
|
- Fixes permission issues that prevented federation from working properly
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ function isFormContentType(request: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let publicMeilisearchKey: string | undefined = undefined;
|
||||||
|
|
||||||
const auth: Handle = async ({ event, resolve }) => {
|
const auth: Handle = async ({ event, resolve }) => {
|
||||||
const pb = new PocketBase(envPub.PUBLIC_POCKETBASE_URL)
|
const pb = new PocketBase(envPub.PUBLIC_POCKETBASE_URL)
|
||||||
@@ -82,10 +83,14 @@ const auth: Handle = async ({ event, resolve }) => {
|
|||||||
if (pb.authStore.record) {
|
if (pb.authStore.record) {
|
||||||
meiliApiKey = pb.authStore.record.token
|
meiliApiKey = pb.authStore.record.token
|
||||||
settings = await pb.collection('settings').getFirstListItem<Settings>(`user="${pb.authStore.record.id}"`, { requestKey: null })
|
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 {
|
} else {
|
||||||
|
if (!publicMeilisearchKey) {
|
||||||
const response = await pb.send("/public/search/token", { method: "GET", fetch: event.fetch });
|
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 });
|
const ms = new MeiliSearch({ host: env.MEILI_URL, apiKey: meiliApiKey });
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import Datepicker from "$lib/components/base/datepicker.svelte";
|
||||||
import Modal from "$lib/components/base/modal.svelte";
|
import Modal from "$lib/components/base/modal.svelte";
|
||||||
import TextField from "$lib/components/base/text_field.svelte";
|
import TextField from "$lib/components/base/text_field.svelte";
|
||||||
import Toggle from "$lib/components/base/toggle.svelte";
|
import Toggle from "$lib/components/base/toggle.svelte";
|
||||||
@@ -25,23 +26,32 @@
|
|||||||
modal.openModal();
|
modal.openModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { form, errors } = createForm({
|
const {
|
||||||
|
form,
|
||||||
|
errors,
|
||||||
|
data: formData,
|
||||||
|
} = createForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
clientId: integration?.strava?.clientId ?? "",
|
clientId: integration?.strava?.clientId ?? "",
|
||||||
clientSecret: integration?.strava?.clientSecret ?? "",
|
clientSecret: integration?.strava?.clientSecret ?? "",
|
||||||
routes: integration?.strava?.routes ?? true,
|
routes: integration?.strava?.routes ?? true,
|
||||||
activities: integration?.strava?.activities ?? true,
|
activities: integration?.strava?.activities ?? true,
|
||||||
active: integration?.strava?.active ?? false,
|
active: integration?.strava?.active ?? false,
|
||||||
|
after: integration?.strava?.after,
|
||||||
},
|
},
|
||||||
extend: validator({
|
extend: validator({
|
||||||
schema: StravaSchema,
|
schema: StravaSchema,
|
||||||
}),
|
}),
|
||||||
onSubmit: async (form) => {
|
onSubmit: async (form) => {
|
||||||
form.active = integration?.strava?.active ?? form.active
|
form.active = integration?.strava?.active ?? form.active;
|
||||||
onsave?.(form);
|
onsave?.(form);
|
||||||
modal.closeModal();
|
modal.closeModal();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function clearAfterDate() {
|
||||||
|
($formData as any).after = undefined;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
@@ -60,7 +70,9 @@
|
|||||||
></TextField>
|
></TextField>
|
||||||
<TextField
|
<TextField
|
||||||
label="Client Secret"
|
label="Client Secret"
|
||||||
placeholder={integration?.strava ? `(${$_("unchanged")})` : "de8b3789bd7116d..."}
|
placeholder={integration?.strava
|
||||||
|
? `(${$_("unchanged")})`
|
||||||
|
: "de8b3789bd7116d..."}
|
||||||
name="clientSecret"
|
name="clientSecret"
|
||||||
type="password"
|
type="password"
|
||||||
error={$errors.clientSecret}
|
error={$errors.clientSecret}
|
||||||
@@ -73,6 +85,25 @@
|
|||||||
label={$_("activity", { values: { n: 2 } })}
|
label={$_("activity", { values: { n: 2 } })}
|
||||||
></Toggle>
|
></Toggle>
|
||||||
</div>
|
</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>
|
</form>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet footer()}
|
{#snippet footer()}
|
||||||
|
|||||||
@@ -51,9 +51,9 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
const difficultyItems: SelectItem[] = [
|
const difficultyItems: SelectItem[] = [
|
||||||
{ text: $_("easy"), value: "easy" },
|
{ text: $_("easy"), value: 0 },
|
||||||
{ text: $_("moderate"), value: "moderate" },
|
{ text: $_("moderate"), value: 1 },
|
||||||
{ text: $_("difficult"), value: "difficult" },
|
{ text: $_("difficult"), value: 2 },
|
||||||
];
|
];
|
||||||
|
|
||||||
let searchDropdownItems: SearchItem[] = $state([]);
|
let searchDropdownItems: SearchItem[] = $state([]);
|
||||||
|
|||||||
@@ -658,7 +658,7 @@
|
|||||||
<MapWithElevationMaplibre
|
<MapWithElevationMaplibre
|
||||||
trails={[trail]}
|
trails={[trail]}
|
||||||
activeTrail={0}
|
activeTrail={0}
|
||||||
waypoints={trail.expand?.waypoints}
|
waypoints={trail.expand?.waypoints_via_trail}
|
||||||
showElevation={true}
|
showElevation={true}
|
||||||
elevationProfileContainer={"epc-container"}
|
elevationProfileContainer={"epc-container"}
|
||||||
showStyleSwitcher={false}
|
showStyleSwitcher={false}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
<div class="">
|
<div class="">
|
||||||
<p class="font-semibold">{$_("start")}</p>
|
<p class="font-semibold">{$_("start")}</p>
|
||||||
</div>
|
</div>
|
||||||
{#each trail.expand?.waypoints ?? [] as wp, i}
|
{#each trail.expand?.waypoints_via_trail ?? [] as wp, i}
|
||||||
<div
|
<div
|
||||||
class="bg-background cursor-pointer"
|
class="bg-background cursor-pointer"
|
||||||
role="presentation"
|
role="presentation"
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"bakery": "Bäckerei",
|
"bakery": "Bäckerei",
|
||||||
"barrier": "Barriere",
|
"barrier": "Barriere",
|
||||||
"basic-info": "Basisinformation",
|
"basic-info": "Basisinformation",
|
||||||
|
"basque": "Baskisch",
|
||||||
"before": "Vor",
|
"before": "Vor",
|
||||||
"bicycle-parking": "Fahrrad Parkplatz",
|
"bicycle-parking": "Fahrrad Parkplatz",
|
||||||
"bicycle-rental": "Fahrradverleih",
|
"bicycle-rental": "Fahrradverleih",
|
||||||
@@ -56,7 +57,6 @@
|
|||||||
"car": "Auto",
|
"car": "Auto",
|
||||||
"car-motorcycle": "Auto/Motorrad",
|
"car-motorcycle": "Auto/Motorrad",
|
||||||
"card": "{n, plural, =1 {Karte} other {Karten}}",
|
"card": "{n, plural, =1 {Karte} other {Karten}}",
|
||||||
"basque": "Baskisch",
|
|
||||||
"categories": "Kategorien",
|
"categories": "Kategorien",
|
||||||
"category": "Kategorie",
|
"category": "Kategorie",
|
||||||
"change": "Ändern",
|
"change": "Ändern",
|
||||||
@@ -390,6 +390,7 @@
|
|||||||
"statistics": "Statistiken",
|
"statistics": "Statistiken",
|
||||||
"stop-drawing": "Zeichnen beenden",
|
"stop-drawing": "Zeichnen beenden",
|
||||||
"stop-editing": "Bearbeiten beenden",
|
"stop-editing": "Bearbeiten beenden",
|
||||||
|
"strava-integration-after-date-hint": "",
|
||||||
"subway-stop": "U-Bahn Eingang",
|
"subway-stop": "U-Bahn Eingang",
|
||||||
"summit": "Gipfel",
|
"summit": "Gipfel",
|
||||||
"summit-book": "Gipfelbuch",
|
"summit-book": "Gipfelbuch",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"bakery": "Bakery",
|
"bakery": "Bakery",
|
||||||
"barrier": "Barrier",
|
"barrier": "Barrier",
|
||||||
"basic-info": "Basic Info",
|
"basic-info": "Basic Info",
|
||||||
|
"basque": "Basque",
|
||||||
"before": "Before",
|
"before": "Before",
|
||||||
"bicycle-parking": "Bicycle Parking",
|
"bicycle-parking": "Bicycle Parking",
|
||||||
"bicycle-rental": "Bicycle Rental",
|
"bicycle-rental": "Bicycle Rental",
|
||||||
@@ -56,7 +57,6 @@
|
|||||||
"car": "Car",
|
"car": "Car",
|
||||||
"car-motorcycle": "Car/Motorcycle",
|
"car-motorcycle": "Car/Motorcycle",
|
||||||
"card": "{n, plural, =1 {Card} other {Cards}}",
|
"card": "{n, plural, =1 {Card} other {Cards}}",
|
||||||
"basque": "Basque",
|
|
||||||
"categories": "Categories",
|
"categories": "Categories",
|
||||||
"category": "Category",
|
"category": "Category",
|
||||||
"change": "Change",
|
"change": "Change",
|
||||||
@@ -390,6 +390,7 @@
|
|||||||
"statistics": "Statistics",
|
"statistics": "Statistics",
|
||||||
"stop-drawing": "Stop drawing",
|
"stop-drawing": "Stop drawing",
|
||||||
"stop-editing": "Stop editing",
|
"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",
|
"subway-stop": "Subway entrance",
|
||||||
"summit": "Summit",
|
"summit": "Summit",
|
||||||
"summit-book": "Summit Book",
|
"summit-book": "Summit Book",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"bakery": "Bakery",
|
"bakery": "Bakery",
|
||||||
"barrier": "Barrier",
|
"barrier": "Barrier",
|
||||||
"basic-info": "Información básica",
|
"basic-info": "Información básica",
|
||||||
|
"basque": "Basque",
|
||||||
"before": "Antes",
|
"before": "Antes",
|
||||||
"bicycle-parking": "Bicycle Parking",
|
"bicycle-parking": "Bicycle Parking",
|
||||||
"bicycle-rental": "Bicycle Rental",
|
"bicycle-rental": "Bicycle Rental",
|
||||||
@@ -56,7 +57,6 @@
|
|||||||
"car": "Coche",
|
"car": "Coche",
|
||||||
"car-motorcycle": "Car/Motorcycle",
|
"car-motorcycle": "Car/Motorcycle",
|
||||||
"card": "{n, plural, one {}=1 {Ficha} other {Fichas}}",
|
"card": "{n, plural, one {}=1 {Ficha} other {Fichas}}",
|
||||||
"basque": "Basque",
|
|
||||||
"categories": "Categorías",
|
"categories": "Categorías",
|
||||||
"category": "Categoría",
|
"category": "Categoría",
|
||||||
"change": "Modificar",
|
"change": "Modificar",
|
||||||
@@ -390,6 +390,7 @@
|
|||||||
"statistics": "Estadísticas",
|
"statistics": "Estadísticas",
|
||||||
"stop-drawing": "Parar de diseñar",
|
"stop-drawing": "Parar de diseñar",
|
||||||
"stop-editing": "Parar de editar",
|
"stop-editing": "Parar de editar",
|
||||||
|
"strava-integration-after-date-hint": "",
|
||||||
"subway-stop": "Subway entrance",
|
"subway-stop": "Subway entrance",
|
||||||
"summit": "Summit",
|
"summit": "Summit",
|
||||||
"summit-book": "Libro de ascensos",
|
"summit-book": "Libro de ascensos",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"bakery": "Okindegia",
|
"bakery": "Okindegia",
|
||||||
"barrier": "Oztopoa",
|
"barrier": "Oztopoa",
|
||||||
"basic-info": "Oinarrizko informazioa",
|
"basic-info": "Oinarrizko informazioa",
|
||||||
|
"basque": "Basque",
|
||||||
"before": "Aurretik",
|
"before": "Aurretik",
|
||||||
"bicycle-parking": "Bizikleta-parkina",
|
"bicycle-parking": "Bizikleta-parkina",
|
||||||
"bicycle-rental": "Bizikleta-alokairua",
|
"bicycle-rental": "Bizikleta-alokairua",
|
||||||
@@ -56,7 +57,6 @@
|
|||||||
"car": "Kotxea",
|
"car": "Kotxea",
|
||||||
"car-motorcycle": "Kotxea/Motozikleta",
|
"car-motorcycle": "Kotxea/Motozikleta",
|
||||||
"card": "{n, plural, one {}=1 {Txartea} other {Txartelak}}",
|
"card": "{n, plural, one {}=1 {Txartea} other {Txartelak}}",
|
||||||
"basque": "Basque",
|
|
||||||
"categories": "Kategoriak",
|
"categories": "Kategoriak",
|
||||||
"category": "Kategoria",
|
"category": "Kategoria",
|
||||||
"change": "Aldatu",
|
"change": "Aldatu",
|
||||||
@@ -390,6 +390,7 @@
|
|||||||
"statistics": "Estatistikak",
|
"statistics": "Estatistikak",
|
||||||
"stop-drawing": "Utzi marrazteari",
|
"stop-drawing": "Utzi marrazteari",
|
||||||
"stop-editing": "Utzi editatzeari",
|
"stop-editing": "Utzi editatzeari",
|
||||||
|
"strava-integration-after-date-hint": "",
|
||||||
"subway-stop": "Metro sarbidea",
|
"subway-stop": "Metro sarbidea",
|
||||||
"summit": "Gailurra",
|
"summit": "Gailurra",
|
||||||
"summit-book": "Igoeren liburua",
|
"summit-book": "Igoeren liburua",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"bakery": "Bakery",
|
"bakery": "Bakery",
|
||||||
"barrier": "Barrier",
|
"barrier": "Barrier",
|
||||||
"basic-info": "Informations de base",
|
"basic-info": "Informations de base",
|
||||||
|
"basque": "Basque",
|
||||||
"before": "Avant le",
|
"before": "Avant le",
|
||||||
"bicycle-parking": "Bicycle Parking",
|
"bicycle-parking": "Bicycle Parking",
|
||||||
"bicycle-rental": "Bicycle Rental",
|
"bicycle-rental": "Bicycle Rental",
|
||||||
@@ -56,7 +57,6 @@
|
|||||||
"car": "Voiture",
|
"car": "Voiture",
|
||||||
"car-motorcycle": "Car/Motorcycle",
|
"car-motorcycle": "Car/Motorcycle",
|
||||||
"card": "{n, plural, =1 {Tuile} other {Tuiles}}",
|
"card": "{n, plural, =1 {Tuile} other {Tuiles}}",
|
||||||
"basque": "Basque",
|
|
||||||
"categories": "Catégories",
|
"categories": "Catégories",
|
||||||
"category": "Catégorie",
|
"category": "Catégorie",
|
||||||
"change": "Changement",
|
"change": "Changement",
|
||||||
@@ -390,6 +390,7 @@
|
|||||||
"statistics": "Statistiques",
|
"statistics": "Statistiques",
|
||||||
"stop-drawing": "Arrêter de tracer",
|
"stop-drawing": "Arrêter de tracer",
|
||||||
"stop-editing": "Arrêter la modification",
|
"stop-editing": "Arrêter la modification",
|
||||||
|
"strava-integration-after-date-hint": "",
|
||||||
"subway-stop": "Subway entrance",
|
"subway-stop": "Subway entrance",
|
||||||
"summit": "Summit",
|
"summit": "Summit",
|
||||||
"summit-book": "Liste des ascensions",
|
"summit-book": "Liste des ascensions",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"bakery": "Bakery",
|
"bakery": "Bakery",
|
||||||
"barrier": "Barrier",
|
"barrier": "Barrier",
|
||||||
"basic-info": "Alap információk",
|
"basic-info": "Alap információk",
|
||||||
|
"basque": "Basque",
|
||||||
"before": "Before",
|
"before": "Before",
|
||||||
"bicycle-parking": "Bicycle Parking",
|
"bicycle-parking": "Bicycle Parking",
|
||||||
"bicycle-rental": "Bicycle Rental",
|
"bicycle-rental": "Bicycle Rental",
|
||||||
@@ -56,7 +57,6 @@
|
|||||||
"car": "Car",
|
"car": "Car",
|
||||||
"car-motorcycle": "Car/Motorcycle",
|
"car-motorcycle": "Car/Motorcycle",
|
||||||
"card": "{n, plural, =1 {Kártya} other {Kártyák}}",
|
"card": "{n, plural, =1 {Kártya} other {Kártyák}}",
|
||||||
"basque": "Basque",
|
|
||||||
"categories": "Kategóriák",
|
"categories": "Kategóriák",
|
||||||
"category": "Kategória",
|
"category": "Kategória",
|
||||||
"change": "Változás",
|
"change": "Változás",
|
||||||
@@ -390,6 +390,7 @@
|
|||||||
"statistics": "Statistics",
|
"statistics": "Statistics",
|
||||||
"stop-drawing": "Stop drawing",
|
"stop-drawing": "Stop drawing",
|
||||||
"stop-editing": "Stop editing",
|
"stop-editing": "Stop editing",
|
||||||
|
"strava-integration-after-date-hint": "",
|
||||||
"subway-stop": "Subway entrance",
|
"subway-stop": "Subway entrance",
|
||||||
"summit": "Summit",
|
"summit": "Summit",
|
||||||
"summit-book": "Csúcspont könyv",
|
"summit-book": "Csúcspont könyv",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"bakery": "Bakery",
|
"bakery": "Bakery",
|
||||||
"barrier": "Barrier",
|
"barrier": "Barrier",
|
||||||
"basic-info": "Informazioni di base",
|
"basic-info": "Informazioni di base",
|
||||||
|
"basque": "Basque",
|
||||||
"before": "Prima",
|
"before": "Prima",
|
||||||
"bicycle-parking": "Bicycle Parking",
|
"bicycle-parking": "Bicycle Parking",
|
||||||
"bicycle-rental": "Bicycle Rental",
|
"bicycle-rental": "Bicycle Rental",
|
||||||
@@ -56,7 +57,6 @@
|
|||||||
"car": "Car",
|
"car": "Car",
|
||||||
"car-motorcycle": "Car/Motorcycle",
|
"car-motorcycle": "Car/Motorcycle",
|
||||||
"card": "{n, plural, =1 {Carta} other {Carte}}",
|
"card": "{n, plural, =1 {Carta} other {Carte}}",
|
||||||
"basque": "Basque",
|
|
||||||
"categories": "Categorie",
|
"categories": "Categorie",
|
||||||
"category": "Categoria",
|
"category": "Categoria",
|
||||||
"change": "Modifica",
|
"change": "Modifica",
|
||||||
@@ -390,6 +390,7 @@
|
|||||||
"statistics": "Statistiche",
|
"statistics": "Statistiche",
|
||||||
"stop-drawing": "Smettere di disegnare",
|
"stop-drawing": "Smettere di disegnare",
|
||||||
"stop-editing": "Stop editing",
|
"stop-editing": "Stop editing",
|
||||||
|
"strava-integration-after-date-hint": "",
|
||||||
"subway-stop": "Subway entrance",
|
"subway-stop": "Subway entrance",
|
||||||
"summit": "Summit",
|
"summit": "Summit",
|
||||||
"summit-book": "Libro di vetta",
|
"summit-book": "Libro di vetta",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"bakery": "Bakery",
|
"bakery": "Bakery",
|
||||||
"barrier": "Barrier",
|
"barrier": "Barrier",
|
||||||
"basic-info": "Algemene informatie",
|
"basic-info": "Algemene informatie",
|
||||||
|
"basque": "Basque",
|
||||||
"before": "Voor",
|
"before": "Voor",
|
||||||
"bicycle-parking": "Bicycle Parking",
|
"bicycle-parking": "Bicycle Parking",
|
||||||
"bicycle-rental": "Bicycle Rental",
|
"bicycle-rental": "Bicycle Rental",
|
||||||
@@ -56,7 +57,6 @@
|
|||||||
"car": "Wagen",
|
"car": "Wagen",
|
||||||
"car-motorcycle": "Car/Motorcycle",
|
"car-motorcycle": "Car/Motorcycle",
|
||||||
"card": "{n, plural, =1 {Kaart} other {Kaarten}}",
|
"card": "{n, plural, =1 {Kaart} other {Kaarten}}",
|
||||||
"basque": "Basque",
|
|
||||||
"categories": "Categorieën",
|
"categories": "Categorieën",
|
||||||
"category": "Categorie",
|
"category": "Categorie",
|
||||||
"change": "Wijzigen",
|
"change": "Wijzigen",
|
||||||
@@ -390,6 +390,7 @@
|
|||||||
"statistics": "Statistieken",
|
"statistics": "Statistieken",
|
||||||
"stop-drawing": "Stop met tekenen",
|
"stop-drawing": "Stop met tekenen",
|
||||||
"stop-editing": "Stop met bewerken",
|
"stop-editing": "Stop met bewerken",
|
||||||
|
"strava-integration-after-date-hint": "",
|
||||||
"subway-stop": "Subway entrance",
|
"subway-stop": "Subway entrance",
|
||||||
"summit": "Summit",
|
"summit": "Summit",
|
||||||
"summit-book": "Bergtopboek",
|
"summit-book": "Bergtopboek",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"bakery": "Bakery",
|
"bakery": "Bakery",
|
||||||
"barrier": "Barrier",
|
"barrier": "Barrier",
|
||||||
"basic-info": "Podstawowe informacje",
|
"basic-info": "Podstawowe informacje",
|
||||||
|
"basque": "Basque",
|
||||||
"before": "Przed",
|
"before": "Przed",
|
||||||
"bicycle-parking": "Bicycle Parking",
|
"bicycle-parking": "Bicycle Parking",
|
||||||
"bicycle-rental": "Bicycle Rental",
|
"bicycle-rental": "Bicycle Rental",
|
||||||
@@ -56,7 +57,6 @@
|
|||||||
"car": "Samochód",
|
"car": "Samochód",
|
||||||
"car-motorcycle": "Car/Motorcycle",
|
"car-motorcycle": "Car/Motorcycle",
|
||||||
"card": "{n, plural, =1 {Karta} other {Karty}}",
|
"card": "{n, plural, =1 {Karta} other {Karty}}",
|
||||||
"basque": "Basque",
|
|
||||||
"categories": "Kategorie",
|
"categories": "Kategorie",
|
||||||
"category": "Kategoria",
|
"category": "Kategoria",
|
||||||
"change": "Zmień",
|
"change": "Zmień",
|
||||||
@@ -390,6 +390,7 @@
|
|||||||
"statistics": "Statystyki",
|
"statistics": "Statystyki",
|
||||||
"stop-drawing": "Przestań rysować",
|
"stop-drawing": "Przestań rysować",
|
||||||
"stop-editing": "Zakończ edycję",
|
"stop-editing": "Zakończ edycję",
|
||||||
|
"strava-integration-after-date-hint": "",
|
||||||
"subway-stop": "Subway entrance",
|
"subway-stop": "Subway entrance",
|
||||||
"summit": "Summit",
|
"summit": "Summit",
|
||||||
"summit-book": "Logbook",
|
"summit-book": "Logbook",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"bakery": "Bakery",
|
"bakery": "Bakery",
|
||||||
"barrier": "Barrier",
|
"barrier": "Barrier",
|
||||||
"basic-info": "Informações básicas",
|
"basic-info": "Informações básicas",
|
||||||
|
"basque": "Basque",
|
||||||
"before": "Antes",
|
"before": "Antes",
|
||||||
"bicycle-parking": "Bicycle Parking",
|
"bicycle-parking": "Bicycle Parking",
|
||||||
"bicycle-rental": "Bicycle Rental",
|
"bicycle-rental": "Bicycle Rental",
|
||||||
@@ -56,7 +57,6 @@
|
|||||||
"car": "Car",
|
"car": "Car",
|
||||||
"car-motorcycle": "Car/Motorcycle",
|
"car-motorcycle": "Car/Motorcycle",
|
||||||
"card": "{n, plural, =1 {Cartão} other {Cartões}}",
|
"card": "{n, plural, =1 {Cartão} other {Cartões}}",
|
||||||
"basque": "Basque",
|
|
||||||
"categories": "Categorias",
|
"categories": "Categorias",
|
||||||
"category": "Categoria",
|
"category": "Categoria",
|
||||||
"change": "Alterar",
|
"change": "Alterar",
|
||||||
@@ -390,6 +390,7 @@
|
|||||||
"statistics": "Statistics",
|
"statistics": "Statistics",
|
||||||
"stop-drawing": "Parar desenho",
|
"stop-drawing": "Parar desenho",
|
||||||
"stop-editing": "Stop editing",
|
"stop-editing": "Stop editing",
|
||||||
|
"strava-integration-after-date-hint": "",
|
||||||
"subway-stop": "Subway entrance",
|
"subway-stop": "Subway entrance",
|
||||||
"summit": "Summit",
|
"summit": "Summit",
|
||||||
"summit-book": "Livro da cimeira",
|
"summit-book": "Livro da cimeira",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"bakery": "Bakery",
|
"bakery": "Bakery",
|
||||||
"barrier": "Barrier",
|
"barrier": "Barrier",
|
||||||
"basic-info": "Основная информация",
|
"basic-info": "Основная информация",
|
||||||
|
"basque": "Basque",
|
||||||
"before": "До",
|
"before": "До",
|
||||||
"bicycle-parking": "Bicycle Parking",
|
"bicycle-parking": "Bicycle Parking",
|
||||||
"bicycle-rental": "Bicycle Rental",
|
"bicycle-rental": "Bicycle Rental",
|
||||||
@@ -56,7 +57,6 @@
|
|||||||
"car": "Автомобиль:",
|
"car": "Автомобиль:",
|
||||||
"car-motorcycle": "Car/Motorcycle",
|
"car-motorcycle": "Car/Motorcycle",
|
||||||
"card": "{n, plural, =1 {Карточка} other {Карточки}}",
|
"card": "{n, plural, =1 {Карточка} other {Карточки}}",
|
||||||
"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": "",
|
||||||
"subway-stop": "Subway entrance",
|
"subway-stop": "Subway entrance",
|
||||||
"summit": "Summit",
|
"summit": "Summit",
|
||||||
"summit-book": "История поездок",
|
"summit-book": "История поездок",
|
||||||
|
|||||||
@@ -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 {卡片} other {卡片}}",
|
"card": "{n, plural, =1 {卡片} other {卡片}}",
|
||||||
"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": "",
|
||||||
"subway-stop": "地铁入口",
|
"subway-stop": "地铁入口",
|
||||||
"summit": "山峰",
|
"summit": "山峰",
|
||||||
"summit-book": "详细日程",
|
"summit-book": "详细日程",
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ const StravaSchema = z.object({
|
|||||||
clientSecret: z.string().length(40).optional().or(z.literal('')),
|
clientSecret: z.string().length(40).optional().or(z.literal('')),
|
||||||
routes: z.boolean(),
|
routes: z.boolean(),
|
||||||
activities: z.boolean(),
|
activities: z.boolean(),
|
||||||
active: z.boolean()
|
active: z.boolean(),
|
||||||
|
after: z.string().date().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const KomootSchema = z.object({
|
const KomootSchema = z.object({
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ const TrailCreateSchema = z.object({
|
|||||||
duration: z.number({ coerce: true }).nonnegative().optional(),
|
duration: z.number({ coerce: true }).nonnegative().optional(),
|
||||||
photos: z.array(z.string()).default([]),
|
photos: z.array(z.string()).default([]),
|
||||||
thumbnail: z.number().int().nonnegative().optional(),
|
thumbnail: z.number().int().nonnegative().optional(),
|
||||||
waypoints: z.array(z.string()).default([]),
|
|
||||||
like_count: z.number().int().min(0).optional().default(0),
|
like_count: z.number().int().min(0).optional().default(0),
|
||||||
category: z.string().length(15).optional().or(z.literal('')),
|
category: z.string().length(15).optional().or(z.literal('')),
|
||||||
tags: z.array(z.string()).default([]),
|
tags: z.array(z.string()).default([]),
|
||||||
@@ -44,7 +43,6 @@ const TrailUpdateSchema = z.object({
|
|||||||
"photos-": z.string().optional(),
|
"photos-": z.string().optional(),
|
||||||
"photos+": z.string().optional(),
|
"photos+": z.string().optional(),
|
||||||
thumbnail: z.number().int().nonnegative().optional(),
|
thumbnail: z.number().int().nonnegative().optional(),
|
||||||
waypoints: z.array(z.string()).optional(),
|
|
||||||
like_count: z.number().int().min(0).optional().default(0),
|
like_count: z.number().int().min(0).optional().default(0),
|
||||||
category: z.string().optional(),
|
category: z.string().optional(),
|
||||||
tags: z.array(z.string()).optional(),
|
tags: z.array(z.string()).optional(),
|
||||||
|
|||||||
@@ -6,20 +6,21 @@ const WaypointCreateSchema = z.object({
|
|||||||
id: z.string().length(15).optional(),
|
id: z.string().length(15).optional(),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
lat: z.number({coerce: true}).min(-90).max(90),
|
lat: z.number({ coerce: true }).min(-90).max(90),
|
||||||
lon: z.number({coerce: true}).min(-180).max(180),
|
lon: z.number({ coerce: true }).min(-180).max(180),
|
||||||
distance_from_start: z.number({coerce: true}).min(0).optional(),
|
distance_from_start: z.number({ coerce: true }).min(0).optional(),
|
||||||
icon: z.enum(icons).optional(),
|
icon: z.enum(icons).optional(),
|
||||||
author: z.string().length(15),
|
author: z.string().length(15),
|
||||||
photos: z.array(z.string()).default([]),
|
photos: z.array(z.string()).default([]),
|
||||||
|
trail: z.string().length(15).optional()
|
||||||
}) satisfies ZodType<Partial<Waypoint>>
|
}) satisfies ZodType<Partial<Waypoint>>
|
||||||
|
|
||||||
const WaypointUpdateSchema = z.object({
|
const WaypointUpdateSchema = z.object({
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
lat: z.number({coerce: true}).min(-90).max(90).optional(),
|
lat: z.number({ coerce: true }).min(-90).max(90).optional(),
|
||||||
lon: z.number({coerce: true}).min(-180).max(180).optional(),
|
lon: z.number({ coerce: true }).min(-180).max(180).optional(),
|
||||||
distance_from_start: z.number({coerce: true}).min(0).optional(),
|
distance_from_start: z.number({ coerce: true }).min(0).optional(),
|
||||||
icon: z.enum(icons).default("circle").optional(),
|
icon: z.enum(icons).default("circle").optional(),
|
||||||
photos: z.array(z.string()).optional(),
|
photos: z.array(z.string()).optional(),
|
||||||
"photos-": z.string().optional(),
|
"photos-": z.string().optional(),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export interface StravaIntegration extends BaseIntegration {
|
|||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
refreshToken?: string;
|
refreshToken?: string;
|
||||||
expiresAt?: number;
|
expiresAt?: number;
|
||||||
|
after?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KomootIntegration extends BaseIntegration {
|
export interface KomootIntegration extends BaseIntegration {
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ class Trail {
|
|||||||
updated?: string;
|
updated?: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
waypoints: string[];
|
|
||||||
polyline?: string;
|
polyline?: string;
|
||||||
domain?: string;
|
domain?: string;
|
||||||
iri?: string;
|
iri?: string;
|
||||||
@@ -36,7 +35,7 @@ class Trail {
|
|||||||
expand?: {
|
expand?: {
|
||||||
tags?: Tag[]
|
tags?: Tag[]
|
||||||
category?: Category;
|
category?: Category;
|
||||||
waypoints?: Waypoint[]
|
waypoints_via_trail?: Waypoint[]
|
||||||
summit_logs_via_trail?: SummitLog[]
|
summit_logs_via_trail?: SummitLog[]
|
||||||
author?: Actor
|
author?: Actor
|
||||||
comments_via_trail?: Comment[]
|
comments_via_trail?: Comment[]
|
||||||
@@ -90,13 +89,12 @@ class Trail {
|
|||||||
this.lon = params?.lon;
|
this.lon = params?.lon;
|
||||||
this.thumbnail = params?.thumbnail ?? 0;
|
this.thumbnail = params?.thumbnail ?? 0;
|
||||||
this.photos = params?.photos ?? [];
|
this.photos = params?.photos ?? [];
|
||||||
this.waypoints = [];
|
|
||||||
this.tags = []
|
this.tags = []
|
||||||
this.gpx = params?.gpx;
|
this.gpx = params?.gpx;
|
||||||
this.like_count = 0
|
this.like_count = 0
|
||||||
this.expand = {
|
this.expand = {
|
||||||
category: params?.category,
|
category: params?.category,
|
||||||
waypoints: params?.waypoints ?? [],
|
waypoints_via_trail: params?.waypoints ?? [],
|
||||||
summit_logs_via_trail: params?.summit_logs ?? [],
|
summit_logs_via_trail: params?.summit_logs ?? [],
|
||||||
comments_via_trail: params?.comments ?? [],
|
comments_via_trail: params?.comments ?? [],
|
||||||
trail_share_via_trail: params?.shares ?? []
|
trail_share_via_trail: params?.shares ?? []
|
||||||
@@ -111,7 +109,7 @@ interface TrailFilter {
|
|||||||
q: string,
|
q: string,
|
||||||
category: string[],
|
category: string[],
|
||||||
tags: string[],
|
tags: string[],
|
||||||
difficulty: ("easy" | "moderate" | "difficult")[]
|
difficulty: (0 | 1 | 2)[]
|
||||||
author?: string;
|
author?: string;
|
||||||
public?: boolean;
|
public?: boolean;
|
||||||
shared?: boolean;
|
shared?: boolean;
|
||||||
@@ -169,7 +167,7 @@ interface TrailSearchResult {
|
|||||||
elevation_gain: number;
|
elevation_gain: number;
|
||||||
elevation_loss: number;
|
elevation_loss: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
difficulty: "easy" | "moderate" | "difficult";
|
difficulty: 0 | 1 | 2;
|
||||||
category: string;
|
category: string;
|
||||||
completed: boolean;
|
completed: boolean;
|
||||||
date: number;
|
date: number;
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ class Waypoint {
|
|||||||
photos: string[];
|
photos: string[];
|
||||||
_photos?: File[];
|
_photos?: File[];
|
||||||
author: string;
|
author: string;
|
||||||
|
trail?: string;
|
||||||
|
|
||||||
constructor(lat: number, lon: number, params?: {
|
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.id = params?.id;
|
||||||
this.name = params?.name ?? "";
|
this.name = params?.name ?? "";
|
||||||
this.description = params?.description ?? "";
|
this.description = params?.description ?? "";
|
||||||
|
|||||||
@@ -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) {
|
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({
|
const r = await f('/api/v1/trail?' + new URLSearchParams({
|
||||||
"perPage": perPage.toString(),
|
"perPage": perPage.toString(),
|
||||||
expand: "category,waypoints,summit_logs_via_trail,tags",
|
expand: "category,waypoints_via_trail,summit_logs_via_trail,tags",
|
||||||
sort: random ? "@random" : "",
|
sort: random ? "@random" : "",
|
||||||
}), {
|
}), {
|
||||||
method: 'GET',
|
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) {
|
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({
|
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 } : {}),
|
...(handle ? { handle } : {}),
|
||||||
...(share ? { share } : {})
|
...(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)
|
throw new APIError(r.status, response.message, response.detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await r.json()
|
const response: Trail = await r.json()
|
||||||
|
|
||||||
if (loadGPX) {
|
if (loadGPX) {
|
||||||
if (!response.expand) {
|
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!.waypoints_via_trail = response.expand!.waypoints_via_trail || [];
|
||||||
response.expand.summit_logs = response.expand.summit_logs?.sort((a: SummitLog, b: SummitLog) => Date.parse(a.date) - Date.parse(b.date)) || [];
|
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);
|
trail.set(response);
|
||||||
|
|
||||||
@@ -185,13 +185,6 @@ export async function trails_create(trail: Trail, photos: File[], gpx: File | Bl
|
|||||||
throw Error("Unauthenticated")
|
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 ?? []) {
|
for (const tag of trail.expand?.tags ?? []) {
|
||||||
if (!tag.id) {
|
if (!tag.id) {
|
||||||
const model = await tags_create(tag)
|
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({
|
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',
|
method: 'PUT',
|
||||||
body: formData,
|
body: formData,
|
||||||
@@ -232,6 +225,14 @@ export async function trails_create(trail: Trail, photos: File[], gpx: File | Bl
|
|||||||
await summit_logs_create(summitLog, f);
|
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;
|
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) {
|
export async function trails_update(oldTrail: Trail, newTrail: Trail, photos?: File[], gpx?: File | Blob | null) {
|
||||||
newTrail.author = oldTrail.author
|
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) {
|
for (const addedWaypoint of waypointUpdates.added) {
|
||||||
const model = await waypoints_create({
|
const model = await waypoints_create({
|
||||||
...addedWaypoint,
|
...addedWaypoint,
|
||||||
marker: undefined,
|
marker: undefined,
|
||||||
},);
|
},);
|
||||||
newTrail.waypoints.push(model.id!);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const updatedWaypoint of waypointUpdates.updated) {
|
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!, {
|
const model = await waypoints_update(oldWaypoint!, {
|
||||||
...updatedWaypoint,
|
...updatedWaypoint,
|
||||||
marker: undefined,
|
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({
|
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',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
@@ -340,17 +340,6 @@ export async function trails_update(oldTrail: Trail, newTrail: Trail, photos?: F
|
|||||||
|
|
||||||
|
|
||||||
export async function trails_delete(trail: Trail) {
|
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, {
|
const r = await fetch('/api/v1/trail/' + trail.id, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
})
|
})
|
||||||
@@ -457,7 +446,7 @@ export async function searchResultToTrailList(hits: Hits<TrailSearchResult>): Pr
|
|||||||
created: new Date(h.created * 1000).toISOString(),
|
created: new Date(h.created * 1000).toISOString(),
|
||||||
date: new Date(h.date * 1000).toISOString(),
|
date: new Date(h.date * 1000).toISOString(),
|
||||||
description: h.description,
|
description: h.description,
|
||||||
difficulty: h.difficulty,
|
difficulty: h.difficulty == 0 ? "easy" : h.difficulty == 1 ? "moderate" : "difficult",
|
||||||
distance: h.distance,
|
distance: h.distance,
|
||||||
duration: h.duration,
|
duration: h.duration,
|
||||||
elevation_gain: h.elevation_gain,
|
elevation_gain: h.elevation_gain,
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export async function gpx2trail(gpxString: string, fallbackName?: string, correc
|
|||||||
wp.id = cryptoRandomString({ length: 15 });
|
wp.id = cryptoRandomString({ length: 15 });
|
||||||
wp.name = wpt.name ?? ""
|
wp.name = wpt.name ?? ""
|
||||||
wp.description = wpt.desc;
|
wp.description = wpt.desc;
|
||||||
trail.expand!.waypoints?.push(wp);
|
trail.expand!.waypoints_via_trail?.push(wp);
|
||||||
}
|
}
|
||||||
|
|
||||||
const totals = gpx.features
|
const totals = gpx.features
|
||||||
@@ -108,7 +108,7 @@ export async function trail2gpx(trail: Trail, user?: AuthRecord) {
|
|||||||
gpx.wpt = [];
|
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)
|
const gpxWpt = gpx.wpt.find((w) => w.$.lat == wp.lat && w.$.lon == wp.lon)
|
||||||
if (!gpxWpt) {
|
if (!gpxWpt) {
|
||||||
gpx.wpt.push(new GPXWaypoint({
|
gpx.wpt.push(new GPXWaypoint({
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export async function GET(event: RequestEvent) {
|
|||||||
|
|
||||||
const [username, domain] = splitUsername(fullUsername, env.ORIGIN)
|
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!)
|
const user: UserAnonymous = await event.locals.pb.collection("users_anonymous").getOne(actor.user!)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export async function GET(event: RequestEvent) {
|
|||||||
t.expand = {} as any
|
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)
|
return json(r)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export async function GET(event: RequestEvent) {
|
|||||||
l.expand.author.isLocal = false
|
l.expand.author.isLocal = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.expand?.waypoints?.forEach(w => {
|
t.expand?.waypoints_via_trail?.forEach(w => {
|
||||||
|
|
||||||
w.photos = w.photos.map(p =>
|
w.photos = w.photos.map(p =>
|
||||||
`${origin}/api/v1/files/waypoints/${w.id}/${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);
|
await enrichRecord(event.locals.pb, t);
|
||||||
|
|
||||||
// sort waypoints by distance
|
// 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)
|
return json(t)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
return handleError(e)
|
return handleError(e)
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
let selectedTrailIndex = $derived(selectedTrail ? 0 : null);
|
let selectedTrailIndex = $derived(selectedTrail ? 0 : null);
|
||||||
|
|
||||||
let selectedTrailWaypoints = $derived(
|
let selectedTrailWaypoints = $derived(
|
||||||
(selectedTrail as Trail | null)?.expand?.waypoints,
|
(selectedTrail as Trail | null)?.expand?.waypoints_via_trail,
|
||||||
);
|
);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
|||||||
@@ -101,7 +101,7 @@
|
|||||||
const trailItems = r[0].hits.map((t: TrailSearchResult) => ({
|
const trailItems = r[0].hits.map((t: TrailSearchResult) => ({
|
||||||
text: t.name,
|
text: t.name,
|
||||||
description: `Trail ${t.location.length ? ", " + t.location : ""}`,
|
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",
|
icon: "route",
|
||||||
}));
|
}));
|
||||||
const listItems = r[1].hits.map((t: ListSearchResult) => ({
|
const listItems = r[1].hits.map((t: ListSearchResult) => ({
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const load: ServerLoad = async ({ params, locals, fetch }) => {
|
|||||||
q: "",
|
q: "",
|
||||||
category: [],
|
category: [],
|
||||||
tags: [],
|
tags: [],
|
||||||
difficulty: ["easy", "moderate", "difficult"],
|
difficulty: [0, 1, 2],
|
||||||
author: "",
|
author: "",
|
||||||
public: true,
|
public: true,
|
||||||
shared: true,
|
shared: true,
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
<div id="trail-details">
|
<div id="trail-details">
|
||||||
<MapWithElevationMaplibre
|
<MapWithElevationMaplibre
|
||||||
trails={[trail]}
|
trails={[trail]}
|
||||||
waypoints={trail.expand?.waypoints}
|
waypoints={trail.expand?.waypoints_via_trail}
|
||||||
activeTrail={0}
|
activeTrail={0}
|
||||||
bind:markers
|
bind:markers
|
||||||
showTerrain={true}
|
showTerrain={true}
|
||||||
|
|||||||
@@ -390,7 +390,7 @@
|
|||||||
currentHeight += getTextHeight($trail.description, doc, width - 32) + 8;
|
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 } })
|
const header = $_("waypoints", { values: { n: 2 } })
|
||||||
let textHeight = getTextHeight(header, doc, width - 32)
|
let textHeight = getTextHeight(header, doc, width - 32)
|
||||||
if (currentHeight + textHeight + 8 > height) {
|
if (currentHeight + textHeight + 8 > height) {
|
||||||
@@ -401,7 +401,7 @@
|
|||||||
currentHeight += textHeight + 8;
|
currentHeight += textHeight + 8;
|
||||||
doc.setFont("IBMPlexSans-Regular", "normal");
|
doc.setFont("IBMPlexSans-Regular", "normal");
|
||||||
|
|
||||||
($trail.expand.waypoints || []).forEach(waypoint => {
|
($trail.expand.waypoints_via_trail || []).forEach(waypoint => {
|
||||||
let description = waypoint.description || "";
|
let description = waypoint.description || "";
|
||||||
let name = waypoint.name || "";
|
let name = waypoint.name || "";
|
||||||
|
|
||||||
@@ -616,7 +616,7 @@
|
|||||||
<div class="basis-full">
|
<div class="basis-full">
|
||||||
<MapWithElevationMaplibre
|
<MapWithElevationMaplibre
|
||||||
trails={[$trail]}
|
trails={[$trail]}
|
||||||
waypoints={$trail.expand?.waypoints}
|
waypoints={$trail.expand?.waypoints_via_trail}
|
||||||
activeTrail={0}
|
activeTrail={0}
|
||||||
onzoom={updateScale}
|
onzoom={updateScale}
|
||||||
bind:map
|
bind:map
|
||||||
|
|||||||
@@ -136,7 +136,7 @@
|
|||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<h4 class="text-xl font-semibold">Timeline</h4>
|
<h4 class="text-xl font-semibold">Timeline</h4>
|
||||||
{#if !feed.items?.length && data.isOwnProfile}
|
{#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
|
>+ {$_("new-trail")}</a
|
||||||
>
|
>
|
||||||
{:else if !feed.items?.length}
|
{:else if !feed.items?.length}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const load: Load = async ({ params, fetch, parent }) => {
|
|||||||
q: "",
|
q: "",
|
||||||
category: [],
|
category: [],
|
||||||
tags: [],
|
tags: [],
|
||||||
difficulty: ["easy", "moderate", "difficult"],
|
difficulty: [0, 1, 2],
|
||||||
author: actor.id,
|
author: actor.id,
|
||||||
public: true,
|
public: true,
|
||||||
shared: true,
|
shared: true,
|
||||||
|
|||||||
@@ -139,7 +139,7 @@
|
|||||||
summit_logs_via_trail: z
|
summit_logs_via_trail: z
|
||||||
.array(SummitLogCreateSchema)
|
.array(SummitLogCreateSchema)
|
||||||
.optional(),
|
.optional(),
|
||||||
waypoints: z
|
waypoints_via_trail: z
|
||||||
.array(
|
.array(
|
||||||
WaypointCreateSchema.extend({
|
WaypointCreateSchema.extend({
|
||||||
marker: z.any().optional(),
|
marker: z.any().optional(),
|
||||||
@@ -384,11 +384,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearWaypoints() {
|
function clearWaypoints() {
|
||||||
for (const waypoint of $formData.expand!.waypoints ?? []) {
|
for (const waypoint of $formData.expand!.waypoints_via_trail ?? []) {
|
||||||
waypoint.marker?.remove();
|
waypoint.marker?.remove();
|
||||||
}
|
}
|
||||||
$formData.expand!.waypoints = [];
|
$formData.expand!.waypoints_via_trail = [];
|
||||||
$formData.waypoints = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function initRouteAnchors(gpx: GPX, addToMap: boolean = false) {
|
function initRouteAnchors(gpx: GPX, addToMap: boolean = false) {
|
||||||
@@ -445,29 +444,28 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function deleteWaypoint(index: number) {
|
function deleteWaypoint(index: number) {
|
||||||
const wp = $formData.expand!.waypoints?.splice(index, 1);
|
const wp = $formData.expand!.waypoints_via_trail?.splice(index, 1);
|
||||||
$formData.waypoints.splice(index, 1);
|
|
||||||
|
|
||||||
if (!$formData.expand!.waypoints?.length) {
|
if (!$formData.expand!.waypoints_via_trail?.length) {
|
||||||
$formData.expand!.waypoints = [];
|
$formData.expand!.waypoints_via_trail = [];
|
||||||
}
|
}
|
||||||
$formData.expand!.waypoints = $formData.expand!.waypoints;
|
$formData.expand!.waypoints_via_trail = $formData.expand!.waypoints_via_trail;
|
||||||
|
|
||||||
// updateTrailOnMap();
|
// updateTrailOnMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveWaypoint(savedWaypoint: Waypoint) {
|
function saveWaypoint(savedWaypoint: Waypoint) {
|
||||||
let editedWaypointIndex =
|
let editedWaypointIndex =
|
||||||
$formData.expand!.waypoints?.findIndex(
|
$formData.expand!.waypoints_via_trail?.findIndex(
|
||||||
(s) => s.id == savedWaypoint.id,
|
(s) => s.id == savedWaypoint.id,
|
||||||
) ?? -1;
|
) ?? -1;
|
||||||
|
|
||||||
if (editedWaypointIndex >= 0) {
|
if (editedWaypointIndex >= 0) {
|
||||||
$formData.expand!.waypoints![editedWaypointIndex] = savedWaypoint;
|
$formData.expand!.waypoints_via_trail![editedWaypointIndex] = savedWaypoint;
|
||||||
} else {
|
} else {
|
||||||
savedWaypoint.id = cryptoRandomString({ length: 15 });
|
savedWaypoint.id = cryptoRandomString({ length: 15 });
|
||||||
$formData.expand!.waypoints = [
|
$formData.expand!.waypoints_via_trail = [
|
||||||
...($formData.expand!.waypoints ?? []),
|
...($formData.expand!.waypoints_via_trail ?? []),
|
||||||
savedWaypoint,
|
savedWaypoint,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -478,15 +476,15 @@
|
|||||||
function moveMarker(marker: M.Marker, wpId?: string) {
|
function moveMarker(marker: M.Marker, wpId?: string) {
|
||||||
const position = marker.getLngLat();
|
const position = marker.getLngLat();
|
||||||
const editableWaypointIndex =
|
const editableWaypointIndex =
|
||||||
$formData.expand!.waypoints?.findIndex((w) => w.id == wpId) ?? -1;
|
$formData.expand!.waypoints_via_trail?.findIndex((w) => w.id == wpId) ?? -1;
|
||||||
const editableWaypoint =
|
const editableWaypoint =
|
||||||
$formData.expand!.waypoints![editableWaypointIndex];
|
$formData.expand!.waypoints_via_trail![editableWaypointIndex];
|
||||||
if (!editableWaypoint) {
|
if (!editableWaypoint) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
editableWaypoint.lat = position.lat;
|
editableWaypoint.lat = position.lat;
|
||||||
editableWaypoint.lon = position.lng;
|
editableWaypoint.lon = position.lng;
|
||||||
$formData.expand!.waypoints = [...($formData.expand!.waypoints ?? [])];
|
$formData.expand!.waypoints_via_trail = [...($formData.expand!.waypoints_via_trail ?? [])];
|
||||||
// updateTrailOnMap();
|
// updateTrailOnMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1369,7 +1367,7 @@
|
|||||||
{$_("waypoints", { values: { n: 2 } })}
|
{$_("waypoints", { values: { n: 2 } })}
|
||||||
</h3>
|
</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{#each $formData.expand?.waypoints ?? [] as waypoint, i}
|
{#each $formData.expand?.waypoints_via_trail ?? [] as waypoint, i}
|
||||||
<li
|
<li
|
||||||
onmouseenter={() => openMarkerPopup(waypoint)}
|
onmouseenter={() => openMarkerPopup(waypoint)}
|
||||||
onmouseleave={() => openMarkerPopup(waypoint)}
|
onmouseleave={() => openMarkerPopup(waypoint)}
|
||||||
@@ -1501,7 +1499,7 @@
|
|||||||
<div id="trail-map">
|
<div id="trail-map">
|
||||||
<MapWithElevationMaplibre
|
<MapWithElevationMaplibre
|
||||||
trails={mapTrail}
|
trails={mapTrail}
|
||||||
waypoints={$formData.expand?.waypoints}
|
waypoints={$formData.expand?.waypoints_via_trail}
|
||||||
drawing={drawingActive}
|
drawing={drawingActive}
|
||||||
showTerrain={true}
|
showTerrain={true}
|
||||||
onmarkerdragend={moveMarker}
|
onmarkerdragend={moveMarker}
|
||||||
|
|||||||
@@ -25,7 +25,24 @@
|
|||||||
export const snapshot: Snapshot<TrailFilter> = {
|
export const snapshot: Snapshot<TrailFilter> = {
|
||||||
capture: () => filter,
|
capture: () => filter,
|
||||||
restore: (value) => {
|
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();
|
handleFilterUpdate();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const load: ServerLoad = async ({ params, locals, url, fetch }) => {
|
|||||||
q: "",
|
q: "",
|
||||||
category: [],
|
category: [],
|
||||||
tags: [],
|
tags: [],
|
||||||
difficulty: ["easy", "moderate", "difficult"],
|
difficulty: [0, 1, 2],
|
||||||
author: "",
|
author: "",
|
||||||
public: true,
|
public: true,
|
||||||
shared: true,
|
shared: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user