adds settings collection
This commit is contained in:
130
db/migrations/1713016468_created_settings.go
Normal file
130
db/migrations/1713016468_created_settings.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(db dbx.Builder) error {
|
||||
jsonData := `{
|
||||
"id": "uavt73rsqcn1n13",
|
||||
"created": "2024-04-13 13:54:26.023Z",
|
||||
"updated": "2024-04-13 13:54:26.023Z",
|
||||
"name": "settings",
|
||||
"type": "base",
|
||||
"system": false,
|
||||
"schema": [
|
||||
{
|
||||
"system": false,
|
||||
"id": "0sepzvkh",
|
||||
"name": "language",
|
||||
"type": "select",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSelect": 1,
|
||||
"values": [
|
||||
"en",
|
||||
"de",
|
||||
"fr",
|
||||
"hu",
|
||||
"nl",
|
||||
"pl",
|
||||
"pt",
|
||||
"zh"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "zwg1jl0d",
|
||||
"name": "unit",
|
||||
"type": "select",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSelect": 1,
|
||||
"values": [
|
||||
"metric",
|
||||
"imperial"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "jo1zcsbu",
|
||||
"name": "mapFocus",
|
||||
"type": "select",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSelect": 1,
|
||||
"values": [
|
||||
"trails",
|
||||
"location"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "ufhepjxo",
|
||||
"name": "location",
|
||||
"type": "json",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSize": 2000000
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "5uip7a4p",
|
||||
"name": "user",
|
||||
"type": "relation",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"collectionId": "_pb_users_auth_",
|
||||
"cascadeDelete": true,
|
||||
"minSelect": null,
|
||||
"maxSelect": 1,
|
||||
"displayFields": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [],
|
||||
"listRule": null,
|
||||
"viewRule": null,
|
||||
"createRule": null,
|
||||
"updateRule": null,
|
||||
"deleteRule": null,
|
||||
"options": {}
|
||||
}`
|
||||
|
||||
collection := &models.Collection{}
|
||||
if err := json.Unmarshal([]byte(jsonData), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return daos.New(db).SaveCollection(collection)
|
||||
}, func(db dbx.Builder) error {
|
||||
dao := daos.New(db)
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId("uavt73rsqcn1n13")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dao.DeleteCollection(collection)
|
||||
})
|
||||
}
|
||||
162
db/migrations/1713018113_updated_users.go
Normal file
162
db/migrations/1713018113_updated_users.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/models/schema"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(db dbx.Builder) error {
|
||||
dao := daos.New(db)
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId("_pb_users_auth_")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove
|
||||
collection.Schema.RemoveField("wjofulpg")
|
||||
|
||||
// remove
|
||||
collection.Schema.RemoveField("t1wlsqyp")
|
||||
|
||||
// remove
|
||||
collection.Schema.RemoveField("fhxhln9g")
|
||||
|
||||
// remove
|
||||
collection.Schema.RemoveField("wosrk4ue")
|
||||
|
||||
query := dao.RecordQuery("_pb_users_auth_")
|
||||
|
||||
users := []*models.Record{}
|
||||
if err := query.All(&users); err != nil {
|
||||
return err
|
||||
}
|
||||
settingsCollection, err := dao.FindCollectionByNameOrId("settings")
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
settings := models.NewRecord(settingsCollection)
|
||||
language := user.Get("language")
|
||||
unit := user.Get("unit")
|
||||
location := user.Get("location")
|
||||
settings.Set("language", language)
|
||||
settings.Set("unit", unit)
|
||||
settings.Set("location", location)
|
||||
settings.Set("user", user.Id)
|
||||
settings.Set("mapFocus", "trails")
|
||||
if err := dao.SaveRecord(settings); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return dao.SaveCollection(collection)
|
||||
}, func(db dbx.Builder) error {
|
||||
dao := daos.New(db)
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId("_pb_users_auth_")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add
|
||||
del_unit := &schema.SchemaField{}
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"system": false,
|
||||
"id": "wjofulpg",
|
||||
"name": "unit",
|
||||
"type": "select",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSelect": 1,
|
||||
"values": [
|
||||
"metric",
|
||||
"imperial"
|
||||
]
|
||||
}
|
||||
}`), del_unit); err != nil {
|
||||
return err
|
||||
}
|
||||
collection.Schema.AddField(del_unit)
|
||||
|
||||
// add
|
||||
del_language := &schema.SchemaField{}
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"system": false,
|
||||
"id": "t1wlsqyp",
|
||||
"name": "language",
|
||||
"type": "select",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSelect": 1,
|
||||
"values": [
|
||||
"en",
|
||||
"de",
|
||||
"fr",
|
||||
"hu",
|
||||
"nl",
|
||||
"pl",
|
||||
"pt",
|
||||
"zh"
|
||||
]
|
||||
}
|
||||
}`), del_language); err != nil {
|
||||
return err
|
||||
}
|
||||
collection.Schema.AddField(del_language)
|
||||
|
||||
// add
|
||||
del_mapView := &schema.SchemaField{}
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"system": false,
|
||||
"id": "fhxhln9g",
|
||||
"name": "mapView",
|
||||
"type": "select",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSelect": 1,
|
||||
"values": [
|
||||
"location",
|
||||
"trails"
|
||||
]
|
||||
}
|
||||
}`), del_mapView); err != nil {
|
||||
return err
|
||||
}
|
||||
collection.Schema.AddField(del_mapView)
|
||||
|
||||
// add
|
||||
del_location := &schema.SchemaField{}
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"system": false,
|
||||
"id": "wosrk4ue",
|
||||
"name": "location",
|
||||
"type": "json",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSize": 2000000
|
||||
}
|
||||
}`), del_location); err != nil {
|
||||
return err
|
||||
}
|
||||
collection.Schema.AddField(del_location)
|
||||
|
||||
return dao.SaveCollection(collection)
|
||||
})
|
||||
}
|
||||
46
db/migrations/1713023959_updated_settings.go
Normal file
46
db/migrations/1713023959_updated_settings.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(db dbx.Builder) error {
|
||||
dao := daos.New(db);
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId("uavt73rsqcn1n13")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collection.ListRule = types.Pointer("user = @request.auth.id")
|
||||
|
||||
collection.ViewRule = types.Pointer("user = @request.auth.id")
|
||||
|
||||
collection.CreateRule = types.Pointer("")
|
||||
|
||||
collection.UpdateRule = types.Pointer("user = @request.auth.id")
|
||||
|
||||
return dao.SaveCollection(collection)
|
||||
}, func(db dbx.Builder) error {
|
||||
dao := daos.New(db);
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId("uavt73rsqcn1n13")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collection.ListRule = nil
|
||||
|
||||
collection.ViewRule = nil
|
||||
|
||||
collection.CreateRule = nil
|
||||
|
||||
collection.UpdateRule = nil
|
||||
|
||||
return dao.SaveCollection(collection)
|
||||
})
|
||||
}
|
||||
98
db/migrations/1713027641_created_trails_bounding_box.go
Normal file
98
db/migrations/1713027641_created_trails_bounding_box.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(db dbx.Builder) error {
|
||||
jsonData := `{
|
||||
"id": "urytyc428mwlbqq",
|
||||
"created": "2024-04-13 17:00:41.541Z",
|
||||
"updated": "2024-04-13 17:00:41.541Z",
|
||||
"name": "trails_bounding_box",
|
||||
"type": "view",
|
||||
"system": false,
|
||||
"schema": [
|
||||
{
|
||||
"system": false,
|
||||
"id": "iyhsoisl",
|
||||
"name": "max_lat",
|
||||
"type": "json",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSize": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "kx2qfztr",
|
||||
"name": "max_lon",
|
||||
"type": "json",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSize": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "z4qsnjeb",
|
||||
"name": "min_lat",
|
||||
"type": "json",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSize": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "p66xomdb",
|
||||
"name": "min_lon",
|
||||
"type": "json",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSize": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [],
|
||||
"listRule": null,
|
||||
"viewRule": null,
|
||||
"createRule": null,
|
||||
"updateRule": null,
|
||||
"deleteRule": null,
|
||||
"options": {
|
||||
"query": "SELECT users.id, COALESCE(MAX(trails.lat), 0) AS max_lat, COALESCE(MAX(trails.lon), 0) AS max_lon, COALESCE(MIN(trails.lat), 0) AS min_lat, COALESCE(MIN(trails.lon), 0) AS min_lon FROM users LEFT JOIN trails ON users.id = trails.author GROUP BY users.id;"
|
||||
}
|
||||
}`
|
||||
|
||||
collection := &models.Collection{}
|
||||
if err := json.Unmarshal([]byte(jsonData), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return daos.New(db).SaveCollection(collection)
|
||||
}, func(db dbx.Builder) error {
|
||||
dao := daos.New(db);
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId("urytyc428mwlbqq")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dao.DeleteCollection(collection)
|
||||
})
|
||||
}
|
||||
205
db/migrations/1713028155_updated_trails_bounding_box.go
Normal file
205
db/migrations/1713028155_updated_trails_bounding_box.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
"github.com/pocketbase/pocketbase/models/schema"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(db dbx.Builder) error {
|
||||
dao := daos.New(db);
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId("urytyc428mwlbqq")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collection.ViewRule = types.Pointer("@request.auth.id = id")
|
||||
|
||||
// remove
|
||||
collection.Schema.RemoveField("iyhsoisl")
|
||||
|
||||
// remove
|
||||
collection.Schema.RemoveField("kx2qfztr")
|
||||
|
||||
// remove
|
||||
collection.Schema.RemoveField("z4qsnjeb")
|
||||
|
||||
// remove
|
||||
collection.Schema.RemoveField("p66xomdb")
|
||||
|
||||
// add
|
||||
new_max_lat := &schema.SchemaField{}
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"system": false,
|
||||
"id": "osfey1yx",
|
||||
"name": "max_lat",
|
||||
"type": "json",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSize": 1
|
||||
}
|
||||
}`), new_max_lat); err != nil {
|
||||
return err
|
||||
}
|
||||
collection.Schema.AddField(new_max_lat)
|
||||
|
||||
// add
|
||||
new_max_lon := &schema.SchemaField{}
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"system": false,
|
||||
"id": "eohslzky",
|
||||
"name": "max_lon",
|
||||
"type": "json",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSize": 1
|
||||
}
|
||||
}`), new_max_lon); err != nil {
|
||||
return err
|
||||
}
|
||||
collection.Schema.AddField(new_max_lon)
|
||||
|
||||
// add
|
||||
new_min_lat := &schema.SchemaField{}
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"system": false,
|
||||
"id": "kigvankd",
|
||||
"name": "min_lat",
|
||||
"type": "json",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSize": 1
|
||||
}
|
||||
}`), new_min_lat); err != nil {
|
||||
return err
|
||||
}
|
||||
collection.Schema.AddField(new_min_lat)
|
||||
|
||||
// add
|
||||
new_min_lon := &schema.SchemaField{}
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"system": false,
|
||||
"id": "ifnks9mg",
|
||||
"name": "min_lon",
|
||||
"type": "json",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSize": 1
|
||||
}
|
||||
}`), new_min_lon); err != nil {
|
||||
return err
|
||||
}
|
||||
collection.Schema.AddField(new_min_lon)
|
||||
|
||||
return dao.SaveCollection(collection)
|
||||
}, func(db dbx.Builder) error {
|
||||
dao := daos.New(db);
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId("urytyc428mwlbqq")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collection.ViewRule = nil
|
||||
|
||||
// add
|
||||
del_max_lat := &schema.SchemaField{}
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"system": false,
|
||||
"id": "iyhsoisl",
|
||||
"name": "max_lat",
|
||||
"type": "json",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSize": 1
|
||||
}
|
||||
}`), del_max_lat); err != nil {
|
||||
return err
|
||||
}
|
||||
collection.Schema.AddField(del_max_lat)
|
||||
|
||||
// add
|
||||
del_max_lon := &schema.SchemaField{}
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"system": false,
|
||||
"id": "kx2qfztr",
|
||||
"name": "max_lon",
|
||||
"type": "json",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSize": 1
|
||||
}
|
||||
}`), del_max_lon); err != nil {
|
||||
return err
|
||||
}
|
||||
collection.Schema.AddField(del_max_lon)
|
||||
|
||||
// add
|
||||
del_min_lat := &schema.SchemaField{}
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"system": false,
|
||||
"id": "z4qsnjeb",
|
||||
"name": "min_lat",
|
||||
"type": "json",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSize": 1
|
||||
}
|
||||
}`), del_min_lat); err != nil {
|
||||
return err
|
||||
}
|
||||
collection.Schema.AddField(del_min_lat)
|
||||
|
||||
// add
|
||||
del_min_lon := &schema.SchemaField{}
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"system": false,
|
||||
"id": "p66xomdb",
|
||||
"name": "min_lon",
|
||||
"type": "json",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSize": 1
|
||||
}
|
||||
}`), del_min_lon); err != nil {
|
||||
return err
|
||||
}
|
||||
collection.Schema.AddField(del_min_lon)
|
||||
|
||||
// remove
|
||||
collection.Schema.RemoveField("osfey1yx")
|
||||
|
||||
// remove
|
||||
collection.Schema.RemoveField("eohslzky")
|
||||
|
||||
// remove
|
||||
collection.Schema.RemoveField("kigvankd")
|
||||
|
||||
// remove
|
||||
collection.Schema.RemoveField("ifnks9mg")
|
||||
|
||||
return dao.SaveCollection(collection)
|
||||
})
|
||||
}
|
||||
10
web/package-lock.json
generated
10
web/package-lock.json
generated
@@ -26,7 +26,7 @@
|
||||
"pdfkit": "^0.15.0",
|
||||
"photoswipe": "^5.4.3",
|
||||
"pocketbase": "^0.21.0",
|
||||
"qrcode-with-logos": "^1.0.5",
|
||||
"qrcode": "^1.4.4",
|
||||
"svelte-i18n": "^4.0.0",
|
||||
"three": "^0.161.0",
|
||||
"yup": "^1.3.3"
|
||||
@@ -4356,14 +4356,6 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode-with-logos": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/qrcode-with-logos/-/qrcode-with-logos-1.0.5.tgz",
|
||||
"integrity": "sha512-ZHynBWdidxv57T5Lw8KBGpM6c/bwrL9buITPOrtX0PaoV0AgKkqHifdO+rKV7jMSiR/EcQWpvHI9ZarZonDq6g==",
|
||||
"dependencies": {
|
||||
"qrcode": "^1.4.4"
|
||||
}
|
||||
},
|
||||
"node_modules/querystringify": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"pdfkit": "^0.15.0",
|
||||
"photoswipe": "^5.4.3",
|
||||
"pocketbase": "^0.21.0",
|
||||
"qrcode-with-logos": "^1.0.5",
|
||||
"qrcode": "^1.4.4",
|
||||
"svelte-i18n": "^4.0.0",
|
||||
"three": "^0.161.0",
|
||||
"yup": "^1.3.3"
|
||||
|
||||
4
web/src/app.d.ts
vendored
4
web/src/app.d.ts
vendored
@@ -14,7 +14,9 @@ declare global {
|
||||
interface Locals {
|
||||
pb: PocketBase
|
||||
ms: MeiliSearch
|
||||
user: AuthModel | null
|
||||
user: AuthModel | null,
|
||||
settings: Settings | null
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { User } from '$lib/models/user'
|
||||
import { pb } from '$lib/pocketbase'
|
||||
import { currentUser, type User } from '$lib/stores/user_store'
|
||||
import { settings_show } from '$lib/stores/settings_store';
|
||||
import { currentUser } from '$lib/stores/user_store'
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
pb.authStore.loadFromCookie(document.cookie)
|
||||
pb.authStore.onChange(() => {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { env } from '$env/dynamic/private'
|
||||
import { env as envPub } from '$env/dynamic/public'
|
||||
import type { Settings } from '$lib/models/settings'
|
||||
|
||||
import { pb } from '$lib/pocketbase'
|
||||
import { isRouteProtected } from '$lib/util/authorization_util'
|
||||
import { redirect, type Handle } from '@sveltejs/kit'
|
||||
import { MeiliSearch } from 'meilisearch'
|
||||
import { locale } from 'svelte-i18n'
|
||||
import { get } from 'svelte/store'
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
// load the store data from the request cookie string
|
||||
@@ -34,8 +36,10 @@ export const handle: Handle = async ({ event, resolve }) => {
|
||||
}
|
||||
|
||||
let meiliApiKey: string = "";
|
||||
let settings: Settings | undefined;
|
||||
if (pb.authStore.model) {
|
||||
meiliApiKey = pb.authStore.model.token
|
||||
settings = await pb.collection('settings').getFirstListItem<Settings>(`user="${pb.authStore.model.id}"`)
|
||||
} else {
|
||||
const r = await event.fetch(pb.buildUrl("/public/search/token"));
|
||||
const response = await r.json();
|
||||
@@ -46,11 +50,15 @@ export const handle: Handle = async ({ event, resolve }) => {
|
||||
event.locals.ms = ms
|
||||
event.locals.pb = pb
|
||||
event.locals.user = pb.authStore.model
|
||||
event.locals.settings = settings
|
||||
|
||||
const lang = pb.authStore.model?.language ?? event.request.headers.get('accept-language')?.split(',')[0]
|
||||
const lang = settings?.language ?? event.request.headers.get('accept-language')?.split(',')[0]
|
||||
|
||||
if (lang) {
|
||||
locale.set(lang)
|
||||
if (pb.authStore.model) {
|
||||
pb.authStore.model!.language = lang;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await resolve(event)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { page } from "$app/stores";
|
||||
import type { Trail } from "$lib/models/trail";
|
||||
import { currentUser } from "$lib/stores/user_store";
|
||||
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
|
||||
import type AutoGraticule from "$lib/vendor/leaflet-graticule/leaflet-auto-graticule";
|
||||
import type { Map, Marker } from "leaflet";
|
||||
@@ -52,7 +52,7 @@
|
||||
closeBtn: false,
|
||||
followMarker: true,
|
||||
autofitBounds: true,
|
||||
imperial: $currentUser?.unit == "imperial" ?? false,
|
||||
imperial: $page.data.settings?.unit == "imperial" ?? false,
|
||||
reverseCoords: false,
|
||||
acceleration: false,
|
||||
slope: true,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { page } from "$app/stores";
|
||||
import type { Trail } from "$lib/models/trail";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
import {
|
||||
@@ -64,12 +65,12 @@
|
||||
<div class="flex mt-1 gap-4 text-sm text-gray-500 whitespace-nowrap">
|
||||
<span
|
||||
><i class="fa fa-left-right mr-2"></i>{formatDistance(
|
||||
trail.distance,
|
||||
trail.distance
|
||||
)}</span
|
||||
>
|
||||
<span
|
||||
><i class="fa fa-up-down mr-2"></i>{formatElevation(
|
||||
trail.elevation_gain,
|
||||
trail.elevation_gain
|
||||
)}</span
|
||||
>
|
||||
<span
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import Search, { type SearchItem } from "../base/search.svelte";
|
||||
import Slider from "../base/slider.svelte";
|
||||
import type { SelectItem } from "../base/select.svelte";
|
||||
import { page } from "$app/stores";
|
||||
|
||||
export let categories: Category[];
|
||||
export let filterExpanded: boolean = true;
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
import Textarea from "../base/textarea.svelte";
|
||||
import CommentCard from "../comment/comment_card.svelte";
|
||||
import PhotoGallery from "../photo_gallery.svelte";
|
||||
import { page } from "$app/stores";
|
||||
|
||||
export let trail: Trail;
|
||||
export let mode: "overview" | "map" = "map";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { page } from "$app/stores";
|
||||
import type { Trail } from "$lib/models/trail";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
import {
|
||||
@@ -53,12 +54,12 @@
|
||||
<div class="flex mt-1 gap-4 text-sm text-gray-500">
|
||||
<span
|
||||
><i class="fa fa-left-right mr-2"></i>{formatDistance(
|
||||
trail.distance,
|
||||
trail.distance
|
||||
)}</span
|
||||
>
|
||||
<span
|
||||
><i class="fa fa-up-down mr-2"></i>{formatElevation(
|
||||
trail.elevation_gain,
|
||||
trail.elevation_gain
|
||||
)}</span
|
||||
>
|
||||
<span
|
||||
|
||||
28
web/src/lib/models/settings.ts
Normal file
28
web/src/lib/models/settings.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
class Settings {
|
||||
id?: string;
|
||||
unit?: "metric" | "imperial";
|
||||
language?: "en" | "de" | "fr" | "hu" | "nl" | "pl" | "pt" | "zh";
|
||||
mapFocus?: "trails" | "location";
|
||||
location?: { name: string, lat: number, lon: number };
|
||||
user?: string;
|
||||
|
||||
constructor(
|
||||
unit: "metric" | "imperial",
|
||||
language: "en" | "de" | "fr" | "hu" | "nl" | "pl" | "pt" | "zh",
|
||||
mapFocus: "trails" | "location",
|
||||
user: string,
|
||||
params?: {
|
||||
location: { name: string, lat: number, lon: number }
|
||||
}
|
||||
) {
|
||||
this.unit = unit;
|
||||
this.language = language;
|
||||
this.mapFocus = mapFocus;
|
||||
this.user = user;
|
||||
this.location = params?.location;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export { Settings };
|
||||
@@ -115,6 +115,13 @@ interface TrailFilterValues {
|
||||
max_duration: number,
|
||||
}
|
||||
|
||||
interface TrailBoundingBox {
|
||||
max_lat: number,
|
||||
min_lat: number,
|
||||
max_lon: number,
|
||||
min_lon: number,
|
||||
}
|
||||
|
||||
export { Trail };
|
||||
|
||||
export type { TrailFilter, TrailFilterValues };
|
||||
export type { TrailFilter, TrailFilterValues, TrailBoundingBox };
|
||||
10
web/src/lib/models/user.ts
Normal file
10
web/src/lib/models/user.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { Settings } from "./settings";
|
||||
|
||||
export type User = {
|
||||
id: string,
|
||||
username?: string,
|
||||
email?: string,
|
||||
password: string,
|
||||
avatar?: string;
|
||||
language?: string;
|
||||
}
|
||||
55
web/src/lib/stores/settings_store.ts
Normal file
55
web/src/lib/stores/settings_store.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { invalidateAll } from "$app/navigation";
|
||||
import { Settings } from "$lib/models/settings";
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
|
||||
export async function settings_show(userId: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
const r = await f('/api/v1/settings/' + userId, {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
const response = await r.json();
|
||||
if (r.ok) {
|
||||
return response;
|
||||
} else {
|
||||
throw new ClientResponseError(response)
|
||||
}
|
||||
}
|
||||
|
||||
export async function settings_create(settings: Settings) {
|
||||
const r = await fetch('/api/v1/settings', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(settings),
|
||||
})
|
||||
|
||||
if (r.ok) {
|
||||
return await r.json();
|
||||
} else {
|
||||
throw new ClientResponseError(await r.json())
|
||||
}
|
||||
}
|
||||
|
||||
export async function settings_update(settings: Settings) {
|
||||
const r = await fetch('/api/v1/settings/' + settings.id, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(settings),
|
||||
})
|
||||
|
||||
if (r.ok) {
|
||||
await invalidateAll()
|
||||
return await r.json();
|
||||
} else {
|
||||
throw new ClientResponseError(await r.json())
|
||||
}
|
||||
}
|
||||
|
||||
export async function settings_delete(settings: Settings) {
|
||||
const r = await fetch('/api/v1/settings/' + settings.id, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
if (r.ok) {
|
||||
return await r.json();
|
||||
} else {
|
||||
throw new ClientResponseError(await r.json())
|
||||
}
|
||||
}
|
||||
@@ -370,6 +370,18 @@ export async function trails_get_filter_values(f: (url: RequestInfo | URL, confi
|
||||
}
|
||||
}
|
||||
|
||||
export async function trails_get_bounding_box(f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch): Promise<TrailFilterValues> {
|
||||
const r = await f('/api/v1/trail/bounding-box', {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
if (r.ok) {
|
||||
return await r.json();
|
||||
} else {
|
||||
throw new ClientResponseError(await r.json())
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchGPX(trail: Trail, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
if (!trail.gpx) {
|
||||
return "";
|
||||
|
||||
@@ -1,33 +1,26 @@
|
||||
import { Settings } from "$lib/models/settings";
|
||||
import type { User } from "$lib/models/user";
|
||||
import { pb } from "$lib/pocketbase";
|
||||
import { ClientResponseError, type AuthMethodsList } from "pocketbase";
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
|
||||
export type User = {
|
||||
id: string,
|
||||
username?: string,
|
||||
email?: string,
|
||||
password: string,
|
||||
avatar?: string;
|
||||
unit?: "metric" | "imperial";
|
||||
language?: "en" | "de" | "fr" | "hu" | "nl" | "pl" | "pt" | "zh" ;
|
||||
location?: { name: string, lat: number, lon: number }
|
||||
}
|
||||
import { settings_create } from "./settings_store";
|
||||
|
||||
export const currentUser: Writable<User | null> = writable<User | null>()
|
||||
|
||||
export async function users_create(user: User) {
|
||||
user.unit = "metric";
|
||||
const r = await fetch('/api/v1/user', {
|
||||
let r = await fetch('/api/v1/user', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ ...user, passwordConfirm: user.password })
|
||||
})
|
||||
|
||||
if (r.ok) {
|
||||
return await r.json();
|
||||
} else {
|
||||
if (!r.ok) {
|
||||
throw new ClientResponseError(await r.json())
|
||||
}
|
||||
|
||||
const createdUser: User = await r.json();
|
||||
|
||||
const settings = new Settings("metric", "en", "trails", createdUser.id!)
|
||||
await settings_create(settings);
|
||||
}
|
||||
|
||||
export async function users_auth_methods(f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch): Promise<AuthMethodsList> {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { page } from "$app/stores";
|
||||
import { currentUser } from "$lib/stores/user_store";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
@@ -17,7 +18,7 @@ export function formatDistance(meters?: number) {
|
||||
return "-";
|
||||
}
|
||||
|
||||
const unit = get(currentUser)?.unit ?? "metric";
|
||||
const unit = get(page).data.settings?.unit ?? "metric";
|
||||
|
||||
if (unit == "metric") {
|
||||
if (meters >= 1000) {
|
||||
@@ -38,7 +39,7 @@ export function formatElevation(meters?: number) {
|
||||
return "-";
|
||||
}
|
||||
|
||||
const unit = get(currentUser)?.unit ?? "metric";
|
||||
const unit = get(page).data.settings?.unit ?? "metric";
|
||||
|
||||
if (unit == "metric") {
|
||||
return `${Math.round(meters)} m`
|
||||
|
||||
366
web/src/lib/vendor/qr-code-with-logos/index.js
vendored
Normal file
366
web/src/lib/vendor/qr-code-with-logos/index.js
vendored
Normal file
@@ -0,0 +1,366 @@
|
||||
import QRCode from "qrcode";
|
||||
|
||||
'use strict';
|
||||
|
||||
/*! *****************************************************************************
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
***************************************************************************** */
|
||||
|
||||
function __awaiter(thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
}
|
||||
|
||||
function __generator(thisArg, body) {
|
||||
var _ = { label: 0, sent: function () { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function () { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (_) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* @Author: super
|
||||
* @Date: 2019-06-27 16:29:43
|
||||
* @Last Modified by: super
|
||||
* @Last Modified time: 2019-06-27 17:46:21
|
||||
*/
|
||||
/**
|
||||
* promisify promise化,使得promisify(func).then()更加方便,不用每次都構造 promise
|
||||
* Making Promise more convenient, without having to construct a promise every time
|
||||
* @param f {function} 異步函數
|
||||
*/
|
||||
var promisify = function (f) {
|
||||
return function () {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
return new Promise(function (resolve, reject) {
|
||||
args.push(function (err, result) {
|
||||
if (err)
|
||||
reject(err);
|
||||
else
|
||||
resolve(result);
|
||||
});
|
||||
f.apply(null, args);
|
||||
});
|
||||
};
|
||||
};
|
||||
/**
|
||||
* 判斷是否是函數
|
||||
* Determine if it is a function
|
||||
* @param o {function} 函數
|
||||
*/
|
||||
function isFunction(o) {
|
||||
return typeof o === "function";
|
||||
}
|
||||
/**
|
||||
* 判斷是不是字符串
|
||||
* Determine if it is a string
|
||||
* @param o {string} 字符串
|
||||
*/
|
||||
function isString(o) {
|
||||
return typeof o === "string";
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
// import QRCode from "qrcode"
|
||||
var toCanvas$1 = promisify(QRCode.toCanvas);
|
||||
var renderQrCode = function (_a) {
|
||||
var canvas = _a.canvas, content = _a.content, _b = _a.width, width = _b === void 0 ? 0 : _b, _c = _a.nodeQrCodeOptions, nodeQrCodeOptions = _c === void 0 ? {} : _c;
|
||||
// 容错率,默认对内容少的二维码采用高容错率,内容多的二维码采用低容错率
|
||||
// according to the content length to choose different errorCorrectionLevel
|
||||
nodeQrCodeOptions.errorCorrectionLevel =
|
||||
nodeQrCodeOptions.errorCorrectionLevel || getErrorCorrectionLevel(content);
|
||||
return getOriginWidth(content, nodeQrCodeOptions).then(function (_width) {
|
||||
// 得到原始比例后还原至设定值,再放大4倍以获取高清图
|
||||
// Restore to the set value according to the original ratio, and then zoom in 4 times to get the HD image.
|
||||
nodeQrCodeOptions.scale = width === 0 ? undefined : (width / _width) * 4;
|
||||
// @ts-ignore
|
||||
return toCanvas$1(canvas, content, nodeQrCodeOptions);
|
||||
});
|
||||
};
|
||||
// 得到原QrCode的大小,以便缩放得到正确的QrCode大小
|
||||
// Get the size of the original QrCode
|
||||
var getOriginWidth = function (content, nodeQrCodeOption) {
|
||||
var _canvas = document.createElement("canvas");
|
||||
// @ts-ignore
|
||||
return toCanvas$1(_canvas, content, nodeQrCodeOption).then(function () { return _canvas.width; });
|
||||
};
|
||||
// 对于内容少的QrCode,增大容错率
|
||||
// Increase the fault tolerance for QrCode with less content
|
||||
var getErrorCorrectionLevel = function (content) {
|
||||
if (content.length > 36) {
|
||||
return "M";
|
||||
}
|
||||
else if (content.length > 16) {
|
||||
return "Q";
|
||||
}
|
||||
else {
|
||||
return "H";
|
||||
}
|
||||
};
|
||||
|
||||
var drawLogo = function (_a) {
|
||||
var canvas = _a.canvas, logo = _a.logo;
|
||||
if (!logo)
|
||||
return Promise.resolve();
|
||||
if (logo === '')
|
||||
return Promise.resolve();
|
||||
var canvasWidth = canvas.width;
|
||||
if (isString(logo)) {
|
||||
logo = { src: logo };
|
||||
}
|
||||
var _b = logo, _c = _b.logoSize, logoSize = _c === void 0 ? 0.15 : _c, _d = _b.borderColor, borderColor = _d === void 0 ? "#ffffff" : _d, _e = _b.bgColor, bgColor = _e === void 0 ? borderColor || "#ffffff" : _e, _f = _b.borderSize, borderSize = _f === void 0 ? 0.05 : _f, crossOrigin = _b.crossOrigin, _g = _b.borderRadius, borderRadius = _g === void 0 ? 8 : _g, _h = _b.logoRadius, logoRadius = _h === void 0 ? 0 : _h;
|
||||
var logoSrc = typeof logo === "string" ? logo : logo.src;
|
||||
var logoWidth = canvasWidth * logoSize;
|
||||
var logoXY = (canvasWidth * (1 - logoSize)) / 2;
|
||||
var logoBgWidth = canvasWidth * (logoSize + borderSize);
|
||||
var logoBgXY = (canvasWidth * (1 - logoSize - borderSize)) / 2;
|
||||
var ctx = canvas.getContext("2d");
|
||||
// logo 底色, draw logo background color
|
||||
canvasRoundRect(ctx)(logoBgXY, logoBgXY, logoBgWidth, logoBgWidth, borderRadius);
|
||||
ctx.fillStyle = bgColor;
|
||||
ctx.fill();
|
||||
// logo
|
||||
var image = new Image();
|
||||
image.setAttribute("crossOrigin", crossOrigin || "anonymous");
|
||||
image.src = logoSrc;
|
||||
// 使用image绘制可以避免某些跨域情况
|
||||
// Use image drawing to avoid some cross-domain situations
|
||||
var drawLogoWithImage = function (image) {
|
||||
ctx.drawImage(image, logoXY, logoXY, logoWidth, logoWidth);
|
||||
};
|
||||
// 使用canvas绘制以获得更多的功能
|
||||
// Use canvas to draw more features, such as borderRadius
|
||||
var drawLogoWithCanvas = function (image) {
|
||||
var canvasImage = document.createElement("canvas");
|
||||
canvasImage.width = logoXY + logoWidth;
|
||||
canvasImage.height = logoXY + logoWidth;
|
||||
canvasImage
|
||||
.getContext("2d")
|
||||
.drawImage(image, logoXY, logoXY, logoWidth, logoWidth);
|
||||
canvasRoundRect(ctx)(logoXY, logoXY, logoWidth, logoWidth, logoRadius);
|
||||
// @ts-ignore
|
||||
ctx.fillStyle = ctx.createPattern(canvasImage, "no-repeat");
|
||||
ctx.fill();
|
||||
};
|
||||
// 将 logo绘制到 canvas上
|
||||
// Draw the logo on the canvas
|
||||
return new Promise(function (resolve, reject) {
|
||||
image.onload = function () {
|
||||
logoRadius ? drawLogoWithCanvas(image) : drawLogoWithImage(image);
|
||||
resolve();
|
||||
};
|
||||
image.onerror = function () {
|
||||
reject('logo load fail!');
|
||||
};
|
||||
});
|
||||
};
|
||||
// draw radius
|
||||
var canvasRoundRect = function (ctx) {
|
||||
return function (x, y, w, h, r) {
|
||||
var minSize = Math.min(w, h);
|
||||
if (r > minSize / 2) {
|
||||
r = minSize / 2;
|
||||
}
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + r, y);
|
||||
ctx.arcTo(x + w, y, x + w, y + h, r);
|
||||
ctx.arcTo(x + w, y + h, x, y + h, r);
|
||||
ctx.arcTo(x, y + h, x, y, r);
|
||||
ctx.arcTo(x, y, x + w, y, r);
|
||||
ctx.closePath();
|
||||
return ctx;
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
* @Author: super
|
||||
* @Date: 2019-06-27 16:29:34
|
||||
* @Last Modified by: super
|
||||
* @Last Modified time: 2019-06-27 16:47:22
|
||||
*/
|
||||
var toCanvas = function (options) {
|
||||
return renderQrCode(options).then(function () { return drawLogo(options); });
|
||||
};
|
||||
|
||||
var toImage = function (options, instance) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var canvas, image, _a, downloadName, download, startDownload;
|
||||
return __generator(this, function (_b) {
|
||||
switch (_b.label) {
|
||||
case 0:
|
||||
canvas = options.canvas;
|
||||
if (options.logo) {
|
||||
if (isString(options.logo)) {
|
||||
options.logo = { src: options.logo };
|
||||
}
|
||||
options.logo.crossOrigin = 'Anonymous';
|
||||
}
|
||||
if (!!instance.ifCanvasDrawed) return [3 /*break*/, 2];
|
||||
return [4 /*yield*/, toCanvas(options)];
|
||||
case 1:
|
||||
_b.sent();
|
||||
_b.label = 2;
|
||||
case 2:
|
||||
image = options.image, _a = options.downloadName, downloadName = _a === void 0 ? 'qr-code' : _a;
|
||||
download = options.download;
|
||||
if (canvas.toDataURL()) {
|
||||
image.src = canvas.toDataURL();
|
||||
}
|
||||
else {
|
||||
throw new Error('Can not get the canvas DataURL');
|
||||
}
|
||||
instance.ifImageCreated = true;
|
||||
if (download !== true && !isFunction(download)) {
|
||||
return [2 /*return*/];
|
||||
}
|
||||
download = download === true ? function (start) { return start(); } : download;
|
||||
startDownload = function () {
|
||||
return saveImage(image, downloadName);
|
||||
};
|
||||
if (download) {
|
||||
return [2 /*return*/, download(startDownload)];
|
||||
}
|
||||
return [2 /*return*/, Promise.resolve()];
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
/**save image */
|
||||
var saveImage = function (image, name) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
try {
|
||||
var dataURL = image.src;
|
||||
var link = document.createElement('a');
|
||||
link.download = name;
|
||||
link.href = dataURL;
|
||||
link.dispatchEvent(new MouseEvent('click'));
|
||||
resolve(true);
|
||||
}
|
||||
catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var version = "1.0.5";
|
||||
|
||||
/*
|
||||
* @Author: super
|
||||
* @Date: 2019-06-27 16:29:31
|
||||
* @Last Modified by: suporka
|
||||
* @Last Modified time: 2020-03-04 12:24:50
|
||||
*/
|
||||
var QrCodeWithLogo = /** @class */ (function () {
|
||||
function QrCodeWithLogo(option) {
|
||||
this.ifCanvasDrawed = false;
|
||||
this.ifImageCreated = false;
|
||||
this.defaultOption = {
|
||||
canvas: undefined,
|
||||
image: undefined,
|
||||
content: ''
|
||||
};
|
||||
this.option = Object.assign(this.defaultOption, option);
|
||||
if (!this.option.canvas)
|
||||
this.option.canvas = document.createElement("canvas");
|
||||
if (!this.option.image)
|
||||
this.option.image = document.createElement("img");
|
||||
this.toCanvas().then(this.toImage.bind(this));
|
||||
}
|
||||
QrCodeWithLogo.prototype.toCanvas = function () {
|
||||
var _this = this;
|
||||
return toCanvas.call(this, this.option).then(function () {
|
||||
_this.ifCanvasDrawed = true;
|
||||
return Promise.resolve();
|
||||
});
|
||||
};
|
||||
QrCodeWithLogo.prototype.toImage = function () {
|
||||
return toImage(this.option, this);
|
||||
};
|
||||
QrCodeWithLogo.prototype.downloadImage = function (name) {
|
||||
if (name === void 0) { name = 'qrcode.png'; }
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
if (!!this.ifImageCreated) return [3 /*break*/, 2];
|
||||
return [4 /*yield*/, this.toImage()];
|
||||
case 1:
|
||||
_a.sent();
|
||||
_a.label = 2;
|
||||
case 2: return [2 /*return*/, saveImage(this.option.image, name)];
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
QrCodeWithLogo.prototype.getImage = function () {
|
||||
return __awaiter(this, void 0, Promise, function () {
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
if (!!this.ifImageCreated) return [3 /*break*/, 2];
|
||||
return [4 /*yield*/, this.toImage()];
|
||||
case 1:
|
||||
_a.sent();
|
||||
_a.label = 2;
|
||||
case 2: return [2 /*return*/, this.option.image];
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
QrCodeWithLogo.prototype.getCanvas = function () {
|
||||
return __awaiter(this, void 0, Promise, function () {
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
if (!!this.ifCanvasDrawed) return [3 /*break*/, 2];
|
||||
return [4 /*yield*/, this.toCanvas()];
|
||||
case 1:
|
||||
_a.sent();
|
||||
_a.label = 2;
|
||||
case 2: return [2 /*return*/, this.option.canvas];
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
QrCodeWithLogo.version = version;
|
||||
return QrCodeWithLogo;
|
||||
}());
|
||||
|
||||
export default QrCodeWithLogo;
|
||||
7
web/src/routes/+layout.server.ts
Normal file
7
web/src/routes/+layout.server.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// +layout.ts
|
||||
import '$lib/i18n';
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
|
||||
export const load: LayoutServerLoad = async ({ locals }) => {
|
||||
return { settings: locals.settings }
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { beforeNavigate, goto } from "$app/navigation";
|
||||
import Toast from "$lib/components/base/toast.svelte";
|
||||
import Footer from "$lib/components/footer.svelte";
|
||||
|
||||
14
web/src/routes/api/v1/settings/+server.ts
Normal file
14
web/src/routes/api/v1/settings/+server.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { Settings } from '$lib/models/settings';
|
||||
import { pb } from '$lib/pocketbase';
|
||||
import { error, json, type RequestEvent } from '@sveltejs/kit';
|
||||
|
||||
export async function PUT(event: RequestEvent) {
|
||||
const data = await event.request.json();
|
||||
|
||||
try {
|
||||
const r = await pb.collection('settings').create<Settings>(data)
|
||||
return json(r);
|
||||
} catch (e: any) {
|
||||
throw error(e.status, e)
|
||||
}
|
||||
}
|
||||
32
web/src/routes/api/v1/settings/[id]/+server.ts
Normal file
32
web/src/routes/api/v1/settings/[id]/+server.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { Settings } from "$lib/models/settings";
|
||||
import { pb } from "$lib/pocketbase";
|
||||
import { error, json, type RequestEvent } from "@sveltejs/kit";
|
||||
|
||||
|
||||
export async function GET(event: RequestEvent) {
|
||||
try {
|
||||
const r = await pb.collection('settings').getFirstListItem<Settings>(`user="${event.params.id}"`)
|
||||
return json(r)
|
||||
} catch (e: any) {
|
||||
throw error(e.status, e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(event: RequestEvent) {
|
||||
const data = await event.request.json()
|
||||
try {
|
||||
const r = await await pb.collection("settings").update<Settings>(event.params.id as string, data);
|
||||
return json(r);
|
||||
} catch (e: any) {
|
||||
throw error(e.status, e)
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(event: RequestEvent) {
|
||||
try {
|
||||
const r = await pb.collection('settings').delete(event.params.id as string)
|
||||
return json({ 'acknowledged': r });
|
||||
} catch (e: any) {
|
||||
throw error(e.status, e)
|
||||
}
|
||||
}
|
||||
20
web/src/routes/api/v1/trail/bounding-box/+server.ts
Normal file
20
web/src/routes/api/v1/trail/bounding-box/+server.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { type TrailBoundingBox, type TrailFilterValues } from '$lib/models/trail';
|
||||
import { pb } from '$lib/pocketbase';
|
||||
import { error, json, type RequestEvent } from '@sveltejs/kit';
|
||||
|
||||
export async function GET(event: RequestEvent) {
|
||||
if (!pb.authStore.model) {
|
||||
return json({
|
||||
max_lat: 0,
|
||||
min_lat: 0,
|
||||
max_lon: 0,
|
||||
min_lon: 0
|
||||
});
|
||||
}
|
||||
try {
|
||||
const r = await pb.collection('trails_bounding_box').getOne<TrailBoundingBox>(pb.authStore.model!.id)
|
||||
return json(r)
|
||||
} catch (e: any) {
|
||||
throw error(e.status || 500, e);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,12 @@
|
||||
import EmptyStateSearch from "$lib/components/empty_states/empty_state_search.svelte";
|
||||
import TrailCard from "$lib/components/trail/trail_card.svelte";
|
||||
import TrailFilterPanel from "$lib/components/trail/trail_filter_panel.svelte";
|
||||
import type { Trail, TrailFilter } from "$lib/models/trail";
|
||||
import type { Settings } from "$lib/models/settings";
|
||||
import type {
|
||||
Trail,
|
||||
TrailBoundingBox,
|
||||
TrailFilter,
|
||||
} from "$lib/models/trail";
|
||||
import { categories } from "$lib/stores/category_store";
|
||||
import {
|
||||
trails,
|
||||
@@ -48,6 +53,8 @@
|
||||
let showMap: boolean = true;
|
||||
|
||||
const filter: TrailFilter = $page.data.filter;
|
||||
const maxBoundingBox: TrailBoundingBox = $page.data.boundingBox;
|
||||
const settings: Settings = $page.data.settings;
|
||||
|
||||
onMount(async () => {
|
||||
L = (await import("leaflet")).default;
|
||||
@@ -90,11 +97,18 @@
|
||||
];
|
||||
|
||||
map.fitBounds(boundingBox);
|
||||
} else if ($currentUser && $currentUser.location) {
|
||||
map.setView(
|
||||
[$currentUser.location.lat, $currentUser.location.lon],
|
||||
12,
|
||||
);
|
||||
} else if (settings && settings.mapFocus == "trails") {
|
||||
const boundingBox: LatLngBoundsExpression = [
|
||||
[maxBoundingBox.max_lat, maxBoundingBox.min_lon],
|
||||
[maxBoundingBox.min_lat, maxBoundingBox.max_lon],
|
||||
];
|
||||
map.fitBounds(boundingBox);
|
||||
} else if (
|
||||
settings &&
|
||||
settings.mapFocus == "location" &&
|
||||
settings.location
|
||||
) {
|
||||
map.setView([settings.location.lat, settings.location.lon], 12);
|
||||
} else {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { TrailFilter } from "$lib/models/trail";
|
||||
import { categories_index } from "$lib/stores/category_store";
|
||||
import { trails, trails_get_filter_values } from "$lib/stores/trail_store";
|
||||
import { trails, trails_get_bounding_box, trails_get_filter_values } from "$lib/stores/trail_store";
|
||||
import type { ServerLoad } from "@sveltejs/kit";
|
||||
|
||||
export const load: ServerLoad = async ({ params, locals, fetch }) => {
|
||||
const boundingBox = await trails_get_bounding_box(fetch);
|
||||
const filterValues = await trails_get_filter_values(fetch);
|
||||
|
||||
const filter: TrailFilter = {
|
||||
@@ -27,5 +28,5 @@ export const load: ServerLoad = async ({ params, locals, fetch }) => {
|
||||
|
||||
trails.set([])
|
||||
|
||||
return { filter: filter }
|
||||
return { filter: filter, boundingBox: boundingBox }
|
||||
};
|
||||
@@ -4,13 +4,12 @@
|
||||
import "$lib/assets/fonts/IBMPlexSans-SemiBold-bold";
|
||||
import "$lib/assets/fonts/fa-solid-900-normal";
|
||||
import Button from "$lib/components/base/button.svelte";
|
||||
import Select, {
|
||||
type SelectItem,
|
||||
} from "$lib/components/base/select.svelte";
|
||||
import Select from "$lib/components/base/select.svelte";
|
||||
import LogoText from "$lib/components/logo/logo_text.svelte";
|
||||
import MapWithElevation from "$lib/components/trail/map_with_elevation.svelte";
|
||||
import type { Settings } from "$lib/models/settings";
|
||||
import { show_toast } from "$lib/stores/toast_store";
|
||||
import { trail } from "$lib/stores/trail_store";
|
||||
import { currentUser } from "$lib/stores/user_store";
|
||||
import {
|
||||
formatDistance,
|
||||
formatElevation,
|
||||
@@ -22,21 +21,22 @@
|
||||
} from "$lib/util/leaflet_util";
|
||||
import { createRect, createText } from "$lib/util/svg_util";
|
||||
import "$lib/vendor/leaflet-elevation/src/index.css";
|
||||
import type AutoGraticule from "$lib/vendor/leaflet-graticule/leaflet-auto-graticule";
|
||||
import leafletImage from "$lib/vendor/leaflet-image/leaflet-image.js";
|
||||
import { Canvg } from "canvg";
|
||||
import { jsPDF } from "jspdf";
|
||||
import type { Map } from "leaflet";
|
||||
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import QrCodeWithLogo from "qrcode-with-logos";
|
||||
import QrCodeWithLogo from "$lib/vendor/qr-code-with-logos/index";
|
||||
import { onMount, tick } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { Canvg } from "canvg";
|
||||
import type AutoGraticule from "$lib/vendor/leaflet-graticule/leaflet-auto-graticule";
|
||||
import { show_toast } from "$lib/stores/toast_store";
|
||||
|
||||
let map: Map;
|
||||
let graticule: AutoGraticule;
|
||||
|
||||
const settings: Settings = $page.data.settings;
|
||||
|
||||
const paperSizes: { text: string; value: keyof typeof paperDimensions }[] =
|
||||
[
|
||||
{ text: "A4", value: "a4" },
|
||||
@@ -375,7 +375,7 @@
|
||||
|
||||
let oneUnitInPixels = calculatePixelPerMeter(
|
||||
map,
|
||||
$currentUser && $currentUser.unit == "imperial" ? 1609.34 : 1000,
|
||||
settings && settings.unit == "imperial" ? 1609.34 : 1000,
|
||||
);
|
||||
let unitsInRuler = width / oneUnitInPixels;
|
||||
|
||||
@@ -430,7 +430,7 @@
|
||||
|
||||
svg.appendChild(
|
||||
createText(
|
||||
$currentUser && $currentUser.unit == "imperial" ? "MI" : "KM",
|
||||
settings && settings.unit == "imperial" ? "MI" : "KM",
|
||||
segmentCount * multiplier * oneUnitInPixels - 20,
|
||||
height / 2 - 7,
|
||||
),
|
||||
@@ -535,7 +535,9 @@
|
||||
>
|
||||
<span
|
||||
><i class="fa fa-left-right mr-2"
|
||||
></i>{formatDistance($trail.distance)}</span
|
||||
></i>{formatDistance(
|
||||
$trail.distance,
|
||||
)}</span
|
||||
>
|
||||
<span
|
||||
><i class="fa fa-up-down mr-2"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/stores";
|
||||
import RadioGroup, {
|
||||
type RadioItem,
|
||||
} from "$lib/components/base/radio_group.svelte";
|
||||
@@ -10,6 +11,7 @@
|
||||
type SelectItem,
|
||||
} from "$lib/components/base/select.svelte";
|
||||
import ConfirmModal from "$lib/components/confirm_modal.svelte";
|
||||
import { settings_update } from "$lib/stores/settings_store";
|
||||
import {
|
||||
currentUser,
|
||||
logout,
|
||||
@@ -21,6 +23,8 @@
|
||||
import { onMount } from "svelte";
|
||||
import { _, locale } from "svelte-i18n";
|
||||
|
||||
const settings = $page.data.settings;
|
||||
|
||||
const languages: SelectItem[] = [
|
||||
{ text: $_("chinese"), value: "zh" },
|
||||
{ text: $_("german"), value: "de" },
|
||||
@@ -32,12 +36,18 @@
|
||||
{ text: $_("portuguese"), value: "pt" },
|
||||
];
|
||||
|
||||
const mapFocus: SelectItem[] = [
|
||||
{ text: $_("trail", { values: { n: 2 } }), value: "trails" },
|
||||
{ text: $_("location"), value: "location" },
|
||||
];
|
||||
|
||||
const units: RadioItem[] = [
|
||||
{ text: $_("metric"), value: "metric" },
|
||||
{ text: $_("imperial"), value: "imperial" },
|
||||
];
|
||||
|
||||
let selectedLanguage = "en";
|
||||
let selectedMapFocus = "trails";
|
||||
|
||||
let searchDropdownItems: SearchItem[] = [];
|
||||
let citySearchQuery: string = "";
|
||||
@@ -45,8 +55,9 @@
|
||||
let openConfirmModal: () => void;
|
||||
|
||||
onMount(() => {
|
||||
citySearchQuery = $currentUser?.location?.name ?? "";
|
||||
selectedLanguage = $currentUser?.language || "en";
|
||||
citySearchQuery = settings?.location?.name ?? "";
|
||||
selectedLanguage = settings?.language || "en";
|
||||
selectedMapFocus = settings?.mapFocus ?? "trails";
|
||||
});
|
||||
|
||||
async function searchCities(q: string) {
|
||||
@@ -67,8 +78,8 @@
|
||||
|
||||
async function handleSearchClick(item: SearchItem) {
|
||||
citySearchQuery = item.text;
|
||||
await users_update({
|
||||
id: $currentUser!.id,
|
||||
await settings_update({
|
||||
id: settings?.id!,
|
||||
location: {
|
||||
name: item.value.name,
|
||||
lat: item.value.lat,
|
||||
@@ -90,8 +101,8 @@
|
||||
}
|
||||
|
||||
async function handleUnitSelection(e: RadioItem) {
|
||||
await users_update({
|
||||
id: $currentUser!.id,
|
||||
await settings_update({
|
||||
id: settings!.id,
|
||||
unit: e.value as "imperial" | "metric",
|
||||
});
|
||||
}
|
||||
@@ -100,12 +111,19 @@
|
||||
value: "en" | "de" | "fr" | "hu" | "nl" | "pl" | "pt",
|
||||
) {
|
||||
locale.set(value);
|
||||
await users_update({
|
||||
id: $currentUser!.id,
|
||||
await settings_update({
|
||||
id: settings!.id,
|
||||
language: value,
|
||||
});
|
||||
}
|
||||
|
||||
async function handleMapFocusSelection(value: "trails" | "location") {
|
||||
await settings_update({
|
||||
id: settings!.id,
|
||||
mapFocus: value,
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteAccount() {
|
||||
await users_delete($currentUser!);
|
||||
logout();
|
||||
@@ -156,17 +174,6 @@
|
||||
</div>
|
||||
<div class="space-y-6 rounded-xl p-4">
|
||||
<h3 class="text-2xl font-semibold">{$_("settings")}</h3>
|
||||
|
||||
<div>
|
||||
<h5 class="font-medium mb-1">{$_("default-location")}</h5>
|
||||
<Search
|
||||
items={searchDropdownItems}
|
||||
placeholder="{$_('search-cities')}..."
|
||||
bind:value={citySearchQuery}
|
||||
on:update={(e) => searchCities(e.detail)}
|
||||
on:click={(e) => handleSearchClick(e.detail)}
|
||||
></Search>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="font-medium mb-1">{$_("language")}</h5>
|
||||
<Select
|
||||
@@ -180,10 +187,30 @@
|
||||
<RadioGroup
|
||||
name="unit"
|
||||
items={units}
|
||||
selected={$currentUser.unit == "metric" ? 0 : 1}
|
||||
selected={settings?.unit == "metric" ? 0 : 1}
|
||||
on:change={(e) => handleUnitSelection(e.detail)}
|
||||
></RadioGroup>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="font-medium mb-1">Focus map on</h5>
|
||||
|
||||
<Select
|
||||
items={mapFocus}
|
||||
bind:value={selectedMapFocus}
|
||||
on:change={(e) => handleMapFocusSelection(e.detail)}
|
||||
></Select>
|
||||
{#if selectedMapFocus == "location"}
|
||||
<div class="mt-3">
|
||||
<Search
|
||||
items={searchDropdownItems}
|
||||
placeholder="{$_('search-cities')}..."
|
||||
bind:value={citySearchQuery}
|
||||
on:update={(e) => searchCities(e.detail)}
|
||||
on:click={(e) => handleSearchClick(e.detail)}
|
||||
></Search>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<hr class="border-input-border" />
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-xl text-red-400">{$_("danger-zone")}</h4>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { page } from "$app/stores";
|
||||
import Button from "$lib/components/base/button.svelte";
|
||||
import Datepicker from "$lib/components/base/datepicker.svelte";
|
||||
import Select from "$lib/components/base/select.svelte";
|
||||
|
||||
Reference in New Issue
Block a user