full settings overhaul
This commit is contained in:
15
db/main.go
15
db/main.go
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/pocketbase/pocketbase/models"
|
"github.com/pocketbase/pocketbase/models"
|
||||||
"github.com/pocketbase/pocketbase/plugins/migratecmd"
|
"github.com/pocketbase/pocketbase/plugins/migratecmd"
|
||||||
"github.com/pocketbase/pocketbase/tools/filesystem"
|
"github.com/pocketbase/pocketbase/tools/filesystem"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/hook"
|
||||||
|
|
||||||
_ "pocketbase/migrations"
|
_ "pocketbase/migrations"
|
||||||
"pocketbase/util"
|
"pocketbase/util"
|
||||||
@@ -122,6 +123,20 @@ func main() {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.OnRecordBeforeRequestEmailChangeRequest("users").Add(func(e *core.RecordRequestEmailChangeEvent) error {
|
||||||
|
form := forms.NewRecordEmailChangeRequest(app, e.Record)
|
||||||
|
if err := e.HttpContext.Bind(form); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.Record.Set("email", form.NewEmail)
|
||||||
|
|
||||||
|
if err := app.Dao().SaveRecord(e.Record); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return hook.StopPropagation
|
||||||
|
})
|
||||||
|
|
||||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||||
e.Router.GET("/public/search/token", func(c echo.Context) error {
|
e.Router.GET("/public/search/token", func(c echo.Context) error {
|
||||||
searchRules := map[string]interface{}{
|
searchRules := map[string]interface{}{
|
||||||
|
|||||||
57
db/migrations/1720090768_updated_settings.go
Normal file
57
db/migrations/1720090768_updated_settings.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/pocketbase/dbx"
|
||||||
|
"github.com/pocketbase/pocketbase/daos"
|
||||||
|
m "github.com/pocketbase/pocketbase/migrations"
|
||||||
|
"github.com/pocketbase/pocketbase/models/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
m.Register(func(db dbx.Builder) error {
|
||||||
|
dao := daos.New(db);
|
||||||
|
|
||||||
|
collection, err := dao.FindCollectionByNameOrId("uavt73rsqcn1n13")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// add
|
||||||
|
new_category := &schema.SchemaField{}
|
||||||
|
if err := json.Unmarshal([]byte(`{
|
||||||
|
"system": false,
|
||||||
|
"id": "owlyzl1x",
|
||||||
|
"name": "category",
|
||||||
|
"type": "relation",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"collectionId": "kjxvi8asj2igqwf",
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"minSelect": null,
|
||||||
|
"maxSelect": 1,
|
||||||
|
"displayFields": null
|
||||||
|
}
|
||||||
|
}`), new_category); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
collection.Schema.AddField(new_category)
|
||||||
|
|
||||||
|
return dao.SaveCollection(collection)
|
||||||
|
}, func(db dbx.Builder) error {
|
||||||
|
dao := daos.New(db);
|
||||||
|
|
||||||
|
collection, err := dao.FindCollectionByNameOrId("uavt73rsqcn1n13")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove
|
||||||
|
collection.Schema.RemoveField("owlyzl1x")
|
||||||
|
|
||||||
|
return dao.SaveCollection(collection)
|
||||||
|
})
|
||||||
|
}
|
||||||
53
db/migrations/1720094290_updated_settings.go
Normal file
53
db/migrations/1720094290_updated_settings.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/pocketbase/dbx"
|
||||||
|
"github.com/pocketbase/pocketbase/daos"
|
||||||
|
m "github.com/pocketbase/pocketbase/migrations"
|
||||||
|
"github.com/pocketbase/pocketbase/models/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
m.Register(func(db dbx.Builder) error {
|
||||||
|
dao := daos.New(db);
|
||||||
|
|
||||||
|
collection, err := dao.FindCollectionByNameOrId("uavt73rsqcn1n13")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// add
|
||||||
|
new_tilesets := &schema.SchemaField{}
|
||||||
|
if err := json.Unmarshal([]byte(`{
|
||||||
|
"system": false,
|
||||||
|
"id": "xdbayoqg",
|
||||||
|
"name": "tilesets",
|
||||||
|
"type": "json",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"maxSize": 2000000
|
||||||
|
}
|
||||||
|
}`), new_tilesets); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
collection.Schema.AddField(new_tilesets)
|
||||||
|
|
||||||
|
return dao.SaveCollection(collection)
|
||||||
|
}, func(db dbx.Builder) error {
|
||||||
|
dao := daos.New(db);
|
||||||
|
|
||||||
|
collection, err := dao.FindCollectionByNameOrId("uavt73rsqcn1n13")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove
|
||||||
|
collection.Schema.RemoveField("xdbayoqg")
|
||||||
|
|
||||||
|
return dao.SaveCollection(collection)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ A route is the GPS data of a trail. There are two main ways that a user can prov
|
|||||||
|
|
||||||
#### Upload a file
|
#### Upload a file
|
||||||
|
|
||||||
Click the `Upload file` button and choose a file. The file must be either in GPX, TCX, or KML format. Once you have selected a file you will see a couple of things happen:
|
Click the `Upload file` button and choose a file. The file must be either in GPX, FIT, TCX, or KML format. Once you have selected a file you will see a couple of things happen:
|
||||||
- the map will display and focus on your track.
|
- the map will display and focus on your track.
|
||||||
- the elevation profile and speed charts (if your file contains that information) will be updated accordingly.
|
- the elevation profile and speed charts (if your file contains that information) will be updated accordingly.
|
||||||
- in the left-hand panel information like the trail name, location, and distance will be displayed.
|
- in the left-hand panel information like the trail name, location, and distance will be displayed.
|
||||||
|
|||||||
65
web/package-lock.json
generated
65
web/package-lock.json
generated
@@ -14,9 +14,9 @@
|
|||||||
"@threlte/extras": "^8.7.5",
|
"@threlte/extras": "^8.7.5",
|
||||||
"@types/leaflet.awesome-markers": "^2.0.28",
|
"@types/leaflet.awesome-markers": "^2.0.28",
|
||||||
"@types/three": "^0.161.2",
|
"@types/three": "^0.161.2",
|
||||||
|
"@types/xmldom": "^0.1.34",
|
||||||
"canvg": "^4.0.1",
|
"canvg": "^4.0.1",
|
||||||
"crypto-random-string": "^5.0.0",
|
"crypto-random-string": "^5.0.0",
|
||||||
"easy-fit": "^0.0.8",
|
|
||||||
"instead": "^1.0.3",
|
"instead": "^1.0.3",
|
||||||
"isomorphic-xml2js": "^0.1.3",
|
"isomorphic-xml2js": "^0.1.3",
|
||||||
"jspdf": "^2.5.1",
|
"jspdf": "^2.5.1",
|
||||||
@@ -32,6 +32,7 @@
|
|||||||
"qrcode": "^1.4.4",
|
"qrcode": "^1.4.4",
|
||||||
"svelte-i18n": "^4.0.0",
|
"svelte-i18n": "^4.0.0",
|
||||||
"three": "^0.161.0",
|
"three": "^0.161.0",
|
||||||
|
"xmldom": "^0.6.0",
|
||||||
"yup": "^1.3.3"
|
"yup": "^1.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -1158,6 +1159,11 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/xmldom": {
|
||||||
|
"version": "0.1.34",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/xmldom/-/xmldom-0.1.34.tgz",
|
||||||
|
"integrity": "sha512-7eZFfxI9XHYjJJuugddV6N5YNeXgQE1lArWOcd1eCOKWb/FGs5SIjacSYuEJuwhsGS3gy4RuZ5EUIcqYscuPDA=="
|
||||||
|
},
|
||||||
"node_modules/@vitest/expect": {
|
"node_modules/@vitest/expect": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.2.1.tgz",
|
||||||
@@ -2235,11 +2241,6 @@
|
|||||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/easy-fit": {
|
|
||||||
"version": "0.0.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/easy-fit/-/easy-fit-0.0.8.tgz",
|
|
||||||
"integrity": "sha512-59BXYGsiteuFG7qfkOccPz2ioXH9RTWcO4ffY/m3niYdAyzYSsSkgqOB6vKncLcbIBZU9cC8V12dCbeAlPBeew=="
|
|
||||||
},
|
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.4.647",
|
"version": "1.4.647",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.647.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.647.tgz",
|
||||||
@@ -3334,9 +3335,9 @@
|
|||||||
"integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ=="
|
"integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ=="
|
||||||
},
|
},
|
||||||
"node_modules/jsdom": {
|
"node_modules/jsdom": {
|
||||||
"version": "24.0.0",
|
"version": "24.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.0.tgz",
|
||||||
"integrity": "sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A==",
|
"integrity": "sha512-6gpM7pRXCwIOKxX47cgOyvyQDN/Eh0f1MeKySBV2xGdKtqJBLj8P25eY3EVCWo2mglDDzozR2r2MW4T+JiNUZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
"peer": true,
|
||||||
@@ -3346,21 +3347,21 @@
|
|||||||
"decimal.js": "^10.4.3",
|
"decimal.js": "^10.4.3",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
"html-encoding-sniffer": "^4.0.0",
|
"html-encoding-sniffer": "^4.0.0",
|
||||||
"http-proxy-agent": "^7.0.0",
|
"http-proxy-agent": "^7.0.2",
|
||||||
"https-proxy-agent": "^7.0.2",
|
"https-proxy-agent": "^7.0.4",
|
||||||
"is-potential-custom-element-name": "^1.0.1",
|
"is-potential-custom-element-name": "^1.0.1",
|
||||||
"nwsapi": "^2.2.7",
|
"nwsapi": "^2.2.10",
|
||||||
"parse5": "^7.1.2",
|
"parse5": "^7.1.2",
|
||||||
"rrweb-cssom": "^0.6.0",
|
"rrweb-cssom": "^0.7.0",
|
||||||
"saxes": "^6.0.0",
|
"saxes": "^6.0.0",
|
||||||
"symbol-tree": "^3.2.4",
|
"symbol-tree": "^3.2.4",
|
||||||
"tough-cookie": "^4.1.3",
|
"tough-cookie": "^4.1.4",
|
||||||
"w3c-xmlserializer": "^5.0.0",
|
"w3c-xmlserializer": "^5.0.0",
|
||||||
"webidl-conversions": "^7.0.0",
|
"webidl-conversions": "^7.0.0",
|
||||||
"whatwg-encoding": "^3.1.1",
|
"whatwg-encoding": "^3.1.1",
|
||||||
"whatwg-mimetype": "^4.0.0",
|
"whatwg-mimetype": "^4.0.0",
|
||||||
"whatwg-url": "^14.0.0",
|
"whatwg-url": "^14.0.0",
|
||||||
"ws": "^8.16.0",
|
"ws": "^8.17.0",
|
||||||
"xml-name-validator": "^5.0.0"
|
"xml-name-validator": "^5.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -3375,6 +3376,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jsdom/node_modules/rrweb-cssom": {
|
||||||
|
"version": "0.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
|
||||||
|
"integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/jsdom/node_modules/tr46": {
|
"node_modules/jsdom/node_modules/tr46": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz",
|
||||||
@@ -3912,9 +3921,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nwsapi": {
|
"node_modules/nwsapi": {
|
||||||
"version": "2.2.7",
|
"version": "2.2.10",
|
||||||
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.10.tgz",
|
||||||
"integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==",
|
"integrity": "sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true
|
"peer": true
|
||||||
@@ -5521,9 +5530,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tough-cookie": {
|
"node_modules/tough-cookie": {
|
||||||
"version": "4.1.3",
|
"version": "4.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
|
||||||
"integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
|
"integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
"peer": true,
|
||||||
@@ -6135,9 +6144,9 @@
|
|||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.16.0",
|
"version": "8.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||||
"integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
|
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
"peer": true,
|
||||||
@@ -6196,6 +6205,14 @@
|
|||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/xmldom": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/y18n": {
|
"node_modules/y18n": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||||
|
|||||||
@@ -38,9 +38,9 @@
|
|||||||
"@threlte/extras": "^8.7.5",
|
"@threlte/extras": "^8.7.5",
|
||||||
"@types/leaflet.awesome-markers": "^2.0.28",
|
"@types/leaflet.awesome-markers": "^2.0.28",
|
||||||
"@types/three": "^0.161.2",
|
"@types/three": "^0.161.2",
|
||||||
|
"@types/xmldom": "^0.1.34",
|
||||||
"canvg": "^4.0.1",
|
"canvg": "^4.0.1",
|
||||||
"crypto-random-string": "^5.0.0",
|
"crypto-random-string": "^5.0.0",
|
||||||
"easy-fit": "^0.0.8",
|
|
||||||
"instead": "^1.0.3",
|
"instead": "^1.0.3",
|
||||||
"isomorphic-xml2js": "^0.1.3",
|
"isomorphic-xml2js": "^0.1.3",
|
||||||
"jspdf": "^2.5.1",
|
"jspdf": "^2.5.1",
|
||||||
@@ -56,6 +56,7 @@
|
|||||||
"qrcode": "^1.4.4",
|
"qrcode": "^1.4.4",
|
||||||
"svelte-i18n": "^4.0.0",
|
"svelte-i18n": "^4.0.0",
|
||||||
"three": "^0.161.0",
|
"three": "^0.161.0",
|
||||||
|
"xmldom": "^0.6.0",
|
||||||
"yup": "^1.3.3"
|
"yup": "^1.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
export let name: string = "";
|
export let name: string = "";
|
||||||
export let value: string | number = "";
|
export let value: string | number = "";
|
||||||
export let placeholder: string = "";
|
export let placeholder: string = "";
|
||||||
|
export let disabled: boolean = false;
|
||||||
export let label: string = "";
|
export let label: string = "";
|
||||||
export let error: string = "";
|
export let error: string = "";
|
||||||
export let icon: string = "";
|
export let icon: string = "";
|
||||||
@@ -28,6 +29,8 @@
|
|||||||
class="bg-input-background border border-input-border rounded-md p-3 transition-colors focus:border-input-border-focus focus:outline-none focus:ring-0 w-full {extraClasses}"
|
class="bg-input-background border border-input-border rounded-md p-3 transition-colors focus:border-input-border-focus focus:outline-none focus:ring-0 w-full {extraClasses}"
|
||||||
class:border-red-400={error.length > 0}
|
class:border-red-400={error.length > 0}
|
||||||
class:bg-input-background-error={error.length > 0}
|
class:bg-input-background-error={error.length > 0}
|
||||||
|
class:text-gray-500={disabled}
|
||||||
|
{disabled}
|
||||||
{autocomplete}
|
{autocomplete}
|
||||||
use:typeAction
|
use:typeAction
|
||||||
bind:value
|
bind:value
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
const dropdownItems = [
|
const dropdownItems = [
|
||||||
{ text: $_("profile"), value: "profile", icon: "user" },
|
{ text: $_("settings"), value: "settings", icon: "cog" },
|
||||||
{ text: $_("logout"), value: "logout", icon: "right-from-bracket" },
|
{ text: $_("logout"), value: "logout", icon: "right-from-bracket" },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -79,8 +79,8 @@
|
|||||||
if (item.value == "logout") {
|
if (item.value == "logout") {
|
||||||
logout();
|
logout();
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
} else if (item.value == "profile") {
|
} else if (item.value == "settings") {
|
||||||
goto("/profile");
|
goto("/settings/account");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -203,11 +203,11 @@
|
|||||||
let:toggleMenu={openDropdown}
|
let:toggleMenu={openDropdown}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="rounded-full bg-white text-black hover:bg-gray-200 focus:ring-4 ring-gray-100/50 transition-colors h-10 w-10"
|
class="rounded-full bg-white text-black hover:bg-gray-200 focus:ring-4 ring-gray-100/50 transition-colors max-h-11 aspect-square"
|
||||||
on:click={openDropdown}
|
on:click={openDropdown}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
class="rounded-full"
|
class="rounded-full w-full h-full"
|
||||||
src={getFileURL($currentUser, $currentUser.avatar) ||
|
src={getFileURL($currentUser, $currentUser.avatar) ||
|
||||||
`https://api.dicebear.com/7.x/initials/svg?seed=${$currentUser.username}&backgroundType=gradientLinear`}
|
`https://api.dicebear.com/7.x/initials/svg?seed=${$currentUser.username}&backgroundType=gradientLinear`}
|
||||||
alt="avatar"
|
alt="avatar"
|
||||||
|
|||||||
60
web/src/lib/components/profile/email_modal.svelte
Normal file
60
web/src/lib/components/profile/email_modal.svelte
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Modal from "$lib/components/base/modal.svelte";
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
import { _ } from "svelte-i18n";
|
||||||
|
import TextField from "../base/text_field.svelte";
|
||||||
|
import { createForm } from "$lib/vendor/svelte-form-lib";
|
||||||
|
import { object, string } from "yup";
|
||||||
|
|
||||||
|
let _openModal: (() => void) | undefined = undefined;
|
||||||
|
export let closeModal: (() => void) | undefined = undefined;
|
||||||
|
export function openModal() {
|
||||||
|
$form.email = email
|
||||||
|
$errors.email = "";
|
||||||
|
_openModal!();
|
||||||
|
}
|
||||||
|
|
||||||
|
export let email = "";
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
const { form, errors, handleChange, handleSubmit } = createForm<{
|
||||||
|
email: string;
|
||||||
|
}>({
|
||||||
|
initialValues: { email: email },
|
||||||
|
validationSchema: object({
|
||||||
|
email: string()
|
||||||
|
.email($_("not-a-valid-email-address"))
|
||||||
|
.required($_("required")),
|
||||||
|
}),
|
||||||
|
onSubmit: async (_) => {
|
||||||
|
dispatch("save", $form.email);
|
||||||
|
closeModal!();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
id="email-modal"
|
||||||
|
size="max-w-xl"
|
||||||
|
title={$_("change-email")}
|
||||||
|
bind:openModal={_openModal}
|
||||||
|
bind:closeModal
|
||||||
|
>
|
||||||
|
<form id="email-form" slot="content" on:submit={handleSubmit}>
|
||||||
|
<TextField
|
||||||
|
name="email"
|
||||||
|
bind:value={$form.email}
|
||||||
|
error={$errors.email}
|
||||||
|
on:change={handleChange}
|
||||||
|
></TextField>
|
||||||
|
</form>
|
||||||
|
<div slot="footer" class="flex items-center gap-4">
|
||||||
|
<button class="btn-secondary" on:click={closeModal}
|
||||||
|
>{$_("cancel")}</button
|
||||||
|
>
|
||||||
|
<button class="btn-primary" type="submit" form="email-form" name="save"
|
||||||
|
>{$_("save")}</button
|
||||||
|
>
|
||||||
|
</div></Modal
|
||||||
|
>
|
||||||
84
web/src/lib/components/profile/password_modal.svelte
Normal file
84
web/src/lib/components/profile/password_modal.svelte
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Modal from "$lib/components/base/modal.svelte";
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
import { _ } from "svelte-i18n";
|
||||||
|
import TextField from "../base/text_field.svelte";
|
||||||
|
import { createForm } from "$lib/vendor/svelte-form-lib";
|
||||||
|
import { object, string } from "yup";
|
||||||
|
|
||||||
|
let _openModal: (() => void) | undefined = undefined;
|
||||||
|
export let closeModal: (() => void) | undefined = undefined;
|
||||||
|
export function openModal() {
|
||||||
|
$form.password = ""
|
||||||
|
$errors.password = "";
|
||||||
|
$form.oldPassword = ""
|
||||||
|
$errors.oldPassword = "";
|
||||||
|
_openModal!();
|
||||||
|
}
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
const { form, errors, handleChange, handleSubmit } = createForm<{
|
||||||
|
oldPassword: string;
|
||||||
|
password: string;
|
||||||
|
}>({
|
||||||
|
initialValues: { oldPassword: "", password: "" },
|
||||||
|
validationSchema: object({
|
||||||
|
oldPassword: string()
|
||||||
|
.min(
|
||||||
|
8,
|
||||||
|
$_("must-be-at-least-n-characters-long", {
|
||||||
|
values: { n: 8 },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.required($_("required")),
|
||||||
|
password: string()
|
||||||
|
.min(
|
||||||
|
8,
|
||||||
|
$_("must-be-at-least-n-characters-long", {
|
||||||
|
values: { n: 8 },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.required($_("required")),
|
||||||
|
}),
|
||||||
|
onSubmit: async (_) => {
|
||||||
|
dispatch("save", {oldPassword: $form.oldPassword, password: $form.password, passwordConfirm: $form.password});
|
||||||
|
closeModal!();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
id="password-modal"
|
||||||
|
size="max-w-xl"
|
||||||
|
title={$_("change-password")}
|
||||||
|
bind:openModal={_openModal}
|
||||||
|
bind:closeModal
|
||||||
|
>
|
||||||
|
<form id="password-form" slot="content" on:submit={handleSubmit}>
|
||||||
|
<TextField
|
||||||
|
name="oldPassword"
|
||||||
|
label={$_("current-password")}
|
||||||
|
type="password"
|
||||||
|
bind:value={$form.oldPassword}
|
||||||
|
error={$errors.oldPassword}
|
||||||
|
on:change={handleChange}
|
||||||
|
></TextField>
|
||||||
|
<TextField
|
||||||
|
name="password"
|
||||||
|
label={$_("new-password")}
|
||||||
|
type="password"
|
||||||
|
bind:value={$form.password}
|
||||||
|
error={$errors.password}
|
||||||
|
on:change={handleChange}
|
||||||
|
></TextField>
|
||||||
|
</form>
|
||||||
|
<div slot="footer" class="flex items-center gap-4">
|
||||||
|
<button class="btn-secondary" on:click={closeModal}
|
||||||
|
>{$_("cancel")}</button
|
||||||
|
>
|
||||||
|
<button class="btn-primary" type="submit" form="password-form" name="save"
|
||||||
|
>{$_("save")}</button
|
||||||
|
>
|
||||||
|
</div></Modal
|
||||||
|
>
|
||||||
41
web/src/lib/components/profile/settings.svelte
Normal file
41
web/src/lib/components/profile/settings.svelte
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
|
import type { DropdownItem } from "../base/dropdown.svelte";
|
||||||
|
|
||||||
|
export let selected: number;
|
||||||
|
|
||||||
|
const items: DropdownItem[] = [
|
||||||
|
{ text: "My Account", value: "/settings/account" },
|
||||||
|
{ text: "Display", value: "/settings/display" },
|
||||||
|
{ text: "Import/Export", value: "/settings/export" },
|
||||||
|
{ text: "Help", value: "https://wanderer.to" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function handleItemClick(item: DropdownItem) {
|
||||||
|
if(item.text != "Help") {
|
||||||
|
goto(item.value);
|
||||||
|
}else {
|
||||||
|
window.open(item.value, '_blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-1 md:grid-cols-[256px_1fr] max-w-4xl mx-4 md:mx-auto gap-x-12 items-start"
|
||||||
|
>
|
||||||
|
<menu class="p-4 border border-input-border rounded-xl mb-4">
|
||||||
|
{#each items as item, i}
|
||||||
|
<li
|
||||||
|
class="menu-item flex items-center px-4 py-3 my-1 cursor-pointer hover:bg-menu-item-background-hover focus:bg-menu-item-background-focus transition-colors rounded-md"
|
||||||
|
class:bg-menu-item-background-hover={i == selected}
|
||||||
|
role="presentation"
|
||||||
|
on:mousedown|stopPropagation={() => handleItemClick(item)}
|
||||||
|
>
|
||||||
|
{item.text}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</menu>
|
||||||
|
<div class="settings-content">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from "$app/stores";
|
import { page } from "$app/stores";
|
||||||
|
import { Settings } from "$lib/models/settings";
|
||||||
import type { Trail } from "$lib/models/trail";
|
import type { Trail } from "$lib/models/trail";
|
||||||
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
|
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
|
||||||
import "$lib/vendor/leaflet-elevation/src/index.css";
|
import "$lib/vendor/leaflet-elevation/src/index.css";
|
||||||
@@ -99,27 +100,33 @@
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const baseMaps = {
|
const baseMaps: Record<string, L.TileLayer> = {
|
||||||
OpenStreetMaps: baseLayer,
|
OpenStreetMaps: baseLayer,
|
||||||
OpenTopoMaps: topoLayer,
|
OpenTopoMaps: topoLayer,
|
||||||
|
...($page.data.settings as Settings).tilesets?.reduce<
|
||||||
|
Record<string, string>
|
||||||
|
>((t, current) => {
|
||||||
|
t[current.name] = L.tileLayer(current.url);
|
||||||
|
return t;
|
||||||
|
}, {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
L.control.layers(baseMaps).addTo(map);
|
L.control.layers(baseMaps).addTo(map);
|
||||||
|
|
||||||
switch (localStorage.getItem("layer")) {
|
const layerPreference = localStorage.getItem("layer")
|
||||||
case "OpenTopoMaps":
|
|
||||||
topoLayer.addTo(map);
|
if (layerPreference && Object.keys(baseMaps).includes(layerPreference)) {
|
||||||
break;
|
baseMaps[layerPreference].addTo(map!)
|
||||||
default:
|
}else {
|
||||||
baseLayer.addTo(map);
|
baseLayer.addTo(map)
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
map!.on("baselayerchange", function (e) {
|
map!.on("baselayerchange", function (e) {
|
||||||
localStorage.setItem("layer", e.name);
|
localStorage.setItem("layer", e.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
const localMetric = localStorage.getItem("gradient") as any ?? "altitude";
|
const localMetric =
|
||||||
|
(localStorage.getItem("gradient") as any) ?? "altitude";
|
||||||
selectedMetric = localMetric === "false" ? false : localMetric;
|
selectedMetric = localMetric === "false" ? false : localMetric;
|
||||||
|
|
||||||
const default_elevation_options = {
|
const default_elevation_options = {
|
||||||
|
|||||||
@@ -161,7 +161,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="trail-info-panel mx-auto border border-input-border rounded-3xl h-full" style="max-width: min(100%, 64rem)"
|
class="trail-info-panel mx-auto border border-input-border rounded-3xl h-full" style="max-width: min(100%, 64rem);"
|
||||||
>
|
>
|
||||||
<div class="trail-info-panel-header">
|
<div class="trail-info-panel-header">
|
||||||
<section class="relative h-80">
|
<section class="relative h-80">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"Biking": "Radfahren",
|
"Biking": "Radfahren",
|
||||||
"Canoeing": "Kanufahren",
|
"Canoeing": "Kanufahren",
|
||||||
"Climbing": "Klettern",
|
"Climbing": "Klettern",
|
||||||
|
"export-all-trails": "",
|
||||||
"Hiking": "Wandern",
|
"Hiking": "Wandern",
|
||||||
"Skiing": "Skifahren",
|
"Skiing": "Skifahren",
|
||||||
"Walking": "Laufen",
|
"Walking": "Laufen",
|
||||||
@@ -23,6 +24,8 @@
|
|||||||
"categories": "Kategorien",
|
"categories": "Kategorien",
|
||||||
"category": "Kategorie",
|
"category": "Kategorie",
|
||||||
"change": "Ändern",
|
"change": "Ändern",
|
||||||
|
"change-email": "",
|
||||||
|
"change-password": "",
|
||||||
"changelog": "Changelog",
|
"changelog": "Changelog",
|
||||||
"chinese": "Chinesisch (vereinfacht)",
|
"chinese": "Chinesisch (vereinfacht)",
|
||||||
"close": "",
|
"close": "",
|
||||||
@@ -33,9 +36,11 @@
|
|||||||
"copy-link": "Link kopieren",
|
"copy-link": "Link kopieren",
|
||||||
"create-new-list": "Neue Liste erstellen",
|
"create-new-list": "Neue Liste erstellen",
|
||||||
"creation-date": "Erstellungsdatum",
|
"creation-date": "Erstellungsdatum",
|
||||||
|
"current-password": "",
|
||||||
"cycling": "Radfahren",
|
"cycling": "Radfahren",
|
||||||
"danger-zone": "Gefahrenzone",
|
"danger-zone": "Gefahrenzone",
|
||||||
"date": "Datum",
|
"date": "Datum",
|
||||||
|
"default-category": "",
|
||||||
"default-location": "Standort",
|
"default-location": "Standort",
|
||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
"delete-account": "Konto löschen",
|
"delete-account": "Konto löschen",
|
||||||
@@ -46,6 +51,7 @@
|
|||||||
"difficult": "Schwer",
|
"difficult": "Schwer",
|
||||||
"difficulty": "Schwierigkeit",
|
"difficulty": "Schwierigkeit",
|
||||||
"directions": "Wegbeschreibung",
|
"directions": "Wegbeschreibung",
|
||||||
|
"display": "",
|
||||||
"display-as": "Anzeigen als",
|
"display-as": "Anzeigen als",
|
||||||
"distance": "Distanz",
|
"distance": "Distanz",
|
||||||
"documentation": "Dokumentation",
|
"documentation": "Dokumentation",
|
||||||
@@ -66,12 +72,14 @@
|
|||||||
"error-exporting-trail": "Fehler beim Exportieren des Trails",
|
"error-exporting-trail": "Fehler beim Exportieren des Trails",
|
||||||
"error-printing-map": "Fehler beim Drucken der Karte",
|
"error-printing-map": "Fehler beim Drucken der Karte",
|
||||||
"error-reading-file": "Fehler beim Lesen der Datei",
|
"error-reading-file": "Fehler beim Lesen der Datei",
|
||||||
|
"error-updating-password": "",
|
||||||
"est-duration": "Gesch. Dauer",
|
"est-duration": "Gesch. Dauer",
|
||||||
"explore": "Erkunden",
|
"explore": "Erkunden",
|
||||||
"explore-some-trails": "Erkunden Sie einige Routen",
|
"explore-some-trails": "Erkunden Sie einige Routen",
|
||||||
"export": "Exportieren",
|
"export": "Exportieren",
|
||||||
"features": "Features",
|
"features": "Features",
|
||||||
"file-format": "Dateiformat",
|
"file-format": "Dateiformat",
|
||||||
|
"focus-map-on": "",
|
||||||
"french": "Französisch",
|
"french": "Französisch",
|
||||||
"german": "Deutsch",
|
"german": "Deutsch",
|
||||||
"get-position-from-exif": "Koordinaten aus EXIF Daten",
|
"get-position-from-exif": "Koordinaten aus EXIF Daten",
|
||||||
@@ -85,6 +93,7 @@
|
|||||||
"hungarian": "Ungarisch",
|
"hungarian": "Ungarisch",
|
||||||
"icon": "Icon",
|
"icon": "Icon",
|
||||||
"imperial": "Amerikanisch",
|
"imperial": "Amerikanisch",
|
||||||
|
"import": "",
|
||||||
"include-description": "Beschreibung übernehmen",
|
"include-description": "Beschreibung übernehmen",
|
||||||
"invalid-date": "Ungültiges Datum",
|
"invalid-date": "Ungültiges Datum",
|
||||||
"invalid-username": "Ungültiger Nutzername",
|
"invalid-username": "Ungültiger Nutzername",
|
||||||
@@ -96,6 +105,7 @@
|
|||||||
"list": "{n, plural, =1 {Liste} other {Listen}}",
|
"list": "{n, plural, =1 {Liste} other {Listen}}",
|
||||||
"location": "Standort",
|
"location": "Standort",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
|
"login-details": "",
|
||||||
"logout": "Logout",
|
"logout": "Logout",
|
||||||
"longitude": "Längengrad",
|
"longitude": "Längengrad",
|
||||||
"make-one": "Neues erstellen!",
|
"make-one": "Neues erstellen!",
|
||||||
@@ -113,6 +123,7 @@
|
|||||||
"name": "Name",
|
"name": "Name",
|
||||||
"near": "Nahe",
|
"near": "Nahe",
|
||||||
"new-list": "Neue Liste",
|
"new-list": "Neue Liste",
|
||||||
|
"new-password": "",
|
||||||
"new-trail": "Neue Route",
|
"new-trail": "Neue Route",
|
||||||
"no-account": "Du hast noch kein Konto?",
|
"no-account": "Du hast noch kein Konto?",
|
||||||
"no-preference": "Keine Präferenz",
|
"no-preference": "Keine Präferenz",
|
||||||
@@ -124,6 +135,7 @@
|
|||||||
"orientation": "Ausrichtung",
|
"orientation": "Ausrichtung",
|
||||||
"paper-size": "Papierformat",
|
"paper-size": "Papierformat",
|
||||||
"password": "Passwort",
|
"password": "Passwort",
|
||||||
|
"password-updated": "",
|
||||||
"photos": "Fotos",
|
"photos": "Fotos",
|
||||||
"pick-a-trail": "Route auswählen",
|
"pick-a-trail": "Route auswählen",
|
||||||
"polish": "Polnisch",
|
"polish": "Polnisch",
|
||||||
@@ -156,12 +168,14 @@
|
|||||||
"stop-drawing": "Zeichnen beenden",
|
"stop-drawing": "Zeichnen beenden",
|
||||||
"summit-book": "Gipfelbuch",
|
"summit-book": "Gipfelbuch",
|
||||||
"text": "Text",
|
"text": "Text",
|
||||||
|
"tilesets": "",
|
||||||
"trail": "{n, plural, =1 {Route} other {Routen}}",
|
"trail": "{n, plural, =1 {Route} other {Routen}}",
|
||||||
"trail-not-shared": "Route mit niemandem geteilt",
|
"trail-not-shared": "Route mit niemandem geteilt",
|
||||||
"trails-for-you": "Routen für dich",
|
"trails-for-you": "Routen für dich",
|
||||||
"units": "Einheiten",
|
"units": "Einheiten",
|
||||||
"upload-file": "Datei hochladen",
|
"upload-file": "Datei hochladen",
|
||||||
"upload-gpx": "GPX hochladen",
|
"upload-gpx": "GPX hochladen",
|
||||||
|
"uploaded": "",
|
||||||
"username": "Nutzername",
|
"username": "Nutzername",
|
||||||
"view": "Ansehen",
|
"view": "Ansehen",
|
||||||
"waypoints": "{n, plural, =1 {Wegpunkt} other {Wegpunkte}}",
|
"waypoints": "{n, plural, =1 {Wegpunkt} other {Wegpunkte}}",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"Biking": "Biking",
|
"Biking": "Biking",
|
||||||
"Canoeing": "Canoeing",
|
"Canoeing": "Canoeing",
|
||||||
"Climbing": "Climbing",
|
"Climbing": "Climbing",
|
||||||
|
"export-all-trails": "Export all trails",
|
||||||
"Hiking": "Hiking",
|
"Hiking": "Hiking",
|
||||||
"Skiing": "Skiing",
|
"Skiing": "Skiing",
|
||||||
"Walking": "Walking",
|
"Walking": "Walking",
|
||||||
@@ -23,6 +24,8 @@
|
|||||||
"categories": "Categories",
|
"categories": "Categories",
|
||||||
"category": "Category",
|
"category": "Category",
|
||||||
"change": "Change",
|
"change": "Change",
|
||||||
|
"change-email": "Change email",
|
||||||
|
"change-password": "Change password",
|
||||||
"changelog": "Changelog",
|
"changelog": "Changelog",
|
||||||
"chinese": "Chinese (simplified)",
|
"chinese": "Chinese (simplified)",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
@@ -33,9 +36,11 @@
|
|||||||
"copy-link": "Copy Link",
|
"copy-link": "Copy Link",
|
||||||
"create-new-list": "Create new list",
|
"create-new-list": "Create new list",
|
||||||
"creation-date": "Creation date",
|
"creation-date": "Creation date",
|
||||||
|
"current-password": "Current password",
|
||||||
"cycling": "Cycling",
|
"cycling": "Cycling",
|
||||||
"danger-zone": "Danger zone",
|
"danger-zone": "Danger zone",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
|
"default-category": "Default category",
|
||||||
"default-location": "Default Location",
|
"default-location": "Default Location",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"delete-account": "Delete Account",
|
"delete-account": "Delete Account",
|
||||||
@@ -46,6 +51,7 @@
|
|||||||
"difficult": "Difficult",
|
"difficult": "Difficult",
|
||||||
"difficulty": "Difficulty",
|
"difficulty": "Difficulty",
|
||||||
"directions": "Directions",
|
"directions": "Directions",
|
||||||
|
"display": "Display",
|
||||||
"display-as": "Display as",
|
"display-as": "Display as",
|
||||||
"distance": "Distance",
|
"distance": "Distance",
|
||||||
"documentation": "Documentation",
|
"documentation": "Documentation",
|
||||||
@@ -66,12 +72,14 @@
|
|||||||
"error-exporting-trail": "Error exporting trail",
|
"error-exporting-trail": "Error exporting trail",
|
||||||
"error-printing-map": "Error printing map",
|
"error-printing-map": "Error printing map",
|
||||||
"error-reading-file": "Error reading file",
|
"error-reading-file": "Error reading file",
|
||||||
|
"error-updating-password": "Error updating password",
|
||||||
"est-duration": "Est. duration",
|
"est-duration": "Est. duration",
|
||||||
"explore": "Explore",
|
"explore": "Explore",
|
||||||
"explore-some-trails": "Explore some trails",
|
"explore-some-trails": "Explore some trails",
|
||||||
"export": "Export",
|
"export": "Export",
|
||||||
"features": "Features",
|
"features": "Features",
|
||||||
"file-format": "File format",
|
"file-format": "File format",
|
||||||
|
"focus-map-on": "Focus map on",
|
||||||
"french": "French",
|
"french": "French",
|
||||||
"german": "German",
|
"german": "German",
|
||||||
"get-position-from-exif": "Get coordinates from EXIF data",
|
"get-position-from-exif": "Get coordinates from EXIF data",
|
||||||
@@ -85,6 +93,7 @@
|
|||||||
"hungarian": "Hungarian",
|
"hungarian": "Hungarian",
|
||||||
"icon": "Icon",
|
"icon": "Icon",
|
||||||
"imperial": "Imperial",
|
"imperial": "Imperial",
|
||||||
|
"import": "Import",
|
||||||
"include-description": "Include description",
|
"include-description": "Include description",
|
||||||
"invalid-date": "Invalid Date",
|
"invalid-date": "Invalid Date",
|
||||||
"invalid-username": "Invalid username",
|
"invalid-username": "Invalid username",
|
||||||
@@ -96,6 +105,7 @@
|
|||||||
"list": "{n, plural, =1 {List} other {Lists}}",
|
"list": "{n, plural, =1 {List} other {Lists}}",
|
||||||
"location": "Location",
|
"location": "Location",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
|
"login-details": "Login details",
|
||||||
"logout": "Logout",
|
"logout": "Logout",
|
||||||
"longitude": "Longitude",
|
"longitude": "Longitude",
|
||||||
"make-one": "Make one!",
|
"make-one": "Make one!",
|
||||||
@@ -113,6 +123,7 @@
|
|||||||
"name": "Name",
|
"name": "Name",
|
||||||
"near": "Near",
|
"near": "Near",
|
||||||
"new-list": "New List",
|
"new-list": "New List",
|
||||||
|
"new-password": "New password",
|
||||||
"new-trail": "New Trail",
|
"new-trail": "New Trail",
|
||||||
"no-account": "Don't have an account?",
|
"no-account": "Don't have an account?",
|
||||||
"no-preference": "No preference",
|
"no-preference": "No preference",
|
||||||
@@ -124,6 +135,7 @@
|
|||||||
"orientation": "Orientation",
|
"orientation": "Orientation",
|
||||||
"paper-size": "Paper size",
|
"paper-size": "Paper size",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
|
"password-updated": "Password updated",
|
||||||
"photos": "Photos",
|
"photos": "Photos",
|
||||||
"pick-a-trail": "Pick a trail",
|
"pick-a-trail": "Pick a trail",
|
||||||
"polish": "Polish",
|
"polish": "Polish",
|
||||||
@@ -156,12 +168,14 @@
|
|||||||
"stop-drawing": "Stop drawing",
|
"stop-drawing": "Stop drawing",
|
||||||
"summit-book": "Summit Book",
|
"summit-book": "Summit Book",
|
||||||
"text": "Text",
|
"text": "Text",
|
||||||
|
"tilesets": "Custom tilesets",
|
||||||
"trail": "{n, plural, =1 {Trail} other {Trails}}",
|
"trail": "{n, plural, =1 {Trail} other {Trails}}",
|
||||||
"trail-not-shared": "Not shared with anyone",
|
"trail-not-shared": "Not shared with anyone",
|
||||||
"trails-for-you": "Trails for you",
|
"trails-for-you": "Trails for you",
|
||||||
"units": "Units",
|
"units": "Units",
|
||||||
"upload-file": "Upload file",
|
"upload-file": "Upload file",
|
||||||
"upload-gpx": "Upload GPX",
|
"upload-gpx": "Upload GPX",
|
||||||
|
"uploaded": "uploaded",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
"view": "View",
|
"view": "View",
|
||||||
"waypoints": "{n, plural, =1 {Waypoint} other {Waypoints}}",
|
"waypoints": "{n, plural, =1 {Waypoint} other {Waypoints}}",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"Biking": "",
|
"Biking": "",
|
||||||
"Canoeing": "",
|
"Canoeing": "",
|
||||||
"Climbing": "",
|
"Climbing": "",
|
||||||
|
"export-all-trails": "",
|
||||||
"Hiking": "",
|
"Hiking": "",
|
||||||
"Skiing": "",
|
"Skiing": "",
|
||||||
"Walking": "",
|
"Walking": "",
|
||||||
@@ -23,6 +24,8 @@
|
|||||||
"categories": "Catégories",
|
"categories": "Catégories",
|
||||||
"category": "Catégorie",
|
"category": "Catégorie",
|
||||||
"change": "Changement",
|
"change": "Changement",
|
||||||
|
"change-email": "",
|
||||||
|
"change-password": "",
|
||||||
"changelog": "Journal des modifications",
|
"changelog": "Journal des modifications",
|
||||||
"chinese": "Chinois (simplifié)",
|
"chinese": "Chinois (simplifié)",
|
||||||
"close": "",
|
"close": "",
|
||||||
@@ -33,9 +36,11 @@
|
|||||||
"copy-link": "",
|
"copy-link": "",
|
||||||
"create-new-list": "Créer une nouvelle liste",
|
"create-new-list": "Créer une nouvelle liste",
|
||||||
"creation-date": "Date de création",
|
"creation-date": "Date de création",
|
||||||
|
"current-password": "",
|
||||||
"cycling": "",
|
"cycling": "",
|
||||||
"danger-zone": "Zone de danger",
|
"danger-zone": "Zone de danger",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
|
"default-category": "",
|
||||||
"default-location": "Endroit pas défaut",
|
"default-location": "Endroit pas défaut",
|
||||||
"delete": "Supprimer",
|
"delete": "Supprimer",
|
||||||
"delete-account": "Supprimer le compte",
|
"delete-account": "Supprimer le compte",
|
||||||
@@ -46,6 +51,7 @@
|
|||||||
"difficult": "Difficile",
|
"difficult": "Difficile",
|
||||||
"difficulty": "Difficulté",
|
"difficulty": "Difficulté",
|
||||||
"directions": "Directions",
|
"directions": "Directions",
|
||||||
|
"display": "",
|
||||||
"display-as": "Afficher en tant que",
|
"display-as": "Afficher en tant que",
|
||||||
"distance": "Distance",
|
"distance": "Distance",
|
||||||
"documentation": "Documentation",
|
"documentation": "Documentation",
|
||||||
@@ -66,12 +72,14 @@
|
|||||||
"error-exporting-trail": "",
|
"error-exporting-trail": "",
|
||||||
"error-printing-map": "Erreur d'impression de la carte",
|
"error-printing-map": "Erreur d'impression de la carte",
|
||||||
"error-reading-file": "Erreur de lecture du fichier",
|
"error-reading-file": "Erreur de lecture du fichier",
|
||||||
|
"error-updating-password": "",
|
||||||
"est-duration": "Temps estimé",
|
"est-duration": "Temps estimé",
|
||||||
"explore": "Explorer",
|
"explore": "Explorer",
|
||||||
"explore-some-trails": "Explorer certains itinéraires",
|
"explore-some-trails": "Explorer certains itinéraires",
|
||||||
"export": "",
|
"export": "",
|
||||||
"features": "Fonctionnalités",
|
"features": "Fonctionnalités",
|
||||||
"file-format": "",
|
"file-format": "",
|
||||||
|
"focus-map-on": "",
|
||||||
"french": "Français",
|
"french": "Français",
|
||||||
"german": "Allemand",
|
"german": "Allemand",
|
||||||
"get-position-from-exif": "",
|
"get-position-from-exif": "",
|
||||||
@@ -85,6 +93,7 @@
|
|||||||
"hungarian": "Hongrois",
|
"hungarian": "Hongrois",
|
||||||
"icon": "Icône",
|
"icon": "Icône",
|
||||||
"imperial": "Impérial",
|
"imperial": "Impérial",
|
||||||
|
"import": "",
|
||||||
"include-description": "Inclure la description",
|
"include-description": "Inclure la description",
|
||||||
"invalid-date": "Date invalide",
|
"invalid-date": "Date invalide",
|
||||||
"invalid-username": "Nom d'utilisateur invalide",
|
"invalid-username": "Nom d'utilisateur invalide",
|
||||||
@@ -96,6 +105,7 @@
|
|||||||
"list": "{n, plural, =1 {Liste} other {Listes}}",
|
"list": "{n, plural, =1 {Liste} other {Listes}}",
|
||||||
"location": "Localisation",
|
"location": "Localisation",
|
||||||
"login": "Connexion",
|
"login": "Connexion",
|
||||||
|
"login-details": "",
|
||||||
"logout": "Déconnexion",
|
"logout": "Déconnexion",
|
||||||
"longitude": "Longitude",
|
"longitude": "Longitude",
|
||||||
"make-one": "Faites-en un !",
|
"make-one": "Faites-en un !",
|
||||||
@@ -113,6 +123,7 @@
|
|||||||
"name": "Nom",
|
"name": "Nom",
|
||||||
"near": "Proche",
|
"near": "Proche",
|
||||||
"new-list": "Nouvelle liste",
|
"new-list": "Nouvelle liste",
|
||||||
|
"new-password": "",
|
||||||
"new-trail": "Nouvel itinéraire",
|
"new-trail": "Nouvel itinéraire",
|
||||||
"no-account": "Pas encore de compte ?",
|
"no-account": "Pas encore de compte ?",
|
||||||
"no-preference": "Pas de préférence",
|
"no-preference": "Pas de préférence",
|
||||||
@@ -124,6 +135,7 @@
|
|||||||
"orientation": "Orientation",
|
"orientation": "Orientation",
|
||||||
"paper-size": "Taille de papier",
|
"paper-size": "Taille de papier",
|
||||||
"password": "Mot de passe",
|
"password": "Mot de passe",
|
||||||
|
"password-updated": "",
|
||||||
"photos": "Photos",
|
"photos": "Photos",
|
||||||
"pick-a-trail": "Choisir un itinéraire",
|
"pick-a-trail": "Choisir un itinéraire",
|
||||||
"polish": "polonais",
|
"polish": "polonais",
|
||||||
@@ -156,12 +168,14 @@
|
|||||||
"stop-drawing": "",
|
"stop-drawing": "",
|
||||||
"summit-book": "Livre du sommet",
|
"summit-book": "Livre du sommet",
|
||||||
"text": "Texte",
|
"text": "Texte",
|
||||||
|
"tilesets": "",
|
||||||
"trail": "{n, plural, =1 {Itinéraire} other {Itinéraires}}",
|
"trail": "{n, plural, =1 {Itinéraire} other {Itinéraires}}",
|
||||||
"trail-not-shared": "",
|
"trail-not-shared": "",
|
||||||
"trails-for-you": "Itinéraire pour vous",
|
"trails-for-you": "Itinéraire pour vous",
|
||||||
"units": "Unités",
|
"units": "Unités",
|
||||||
"upload-file": "Téléverser un fichier",
|
"upload-file": "Téléverser un fichier",
|
||||||
"upload-gpx": "Envoyer un GPX",
|
"upload-gpx": "Envoyer un GPX",
|
||||||
|
"uploaded": "",
|
||||||
"username": "Nom d'utilisteur",
|
"username": "Nom d'utilisteur",
|
||||||
"view": "",
|
"view": "",
|
||||||
"waypoints": "Point de repère",
|
"waypoints": "Point de repère",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"Biking": "",
|
"Biking": "",
|
||||||
"Canoeing": "",
|
"Canoeing": "",
|
||||||
"Climbing": "",
|
"Climbing": "",
|
||||||
|
"export-all-trails": "",
|
||||||
"Hiking": "",
|
"Hiking": "",
|
||||||
"Skiing": "",
|
"Skiing": "",
|
||||||
"Walking": "",
|
"Walking": "",
|
||||||
@@ -23,6 +24,8 @@
|
|||||||
"categories": "Kategóriák",
|
"categories": "Kategóriák",
|
||||||
"category": "Kategória",
|
"category": "Kategória",
|
||||||
"change": "Változás",
|
"change": "Változás",
|
||||||
|
"change-email": "",
|
||||||
|
"change-password": "",
|
||||||
"changelog": "Változás napló",
|
"changelog": "Változás napló",
|
||||||
"chinese": "Kínai (egyszerűsítve)",
|
"chinese": "Kínai (egyszerűsítve)",
|
||||||
"close": "",
|
"close": "",
|
||||||
@@ -33,9 +36,11 @@
|
|||||||
"copy-link": "",
|
"copy-link": "",
|
||||||
"create-new-list": "Új lista létrehozása",
|
"create-new-list": "Új lista létrehozása",
|
||||||
"creation-date": "létrehozás dátuma",
|
"creation-date": "létrehozás dátuma",
|
||||||
|
"current-password": "",
|
||||||
"cycling": "",
|
"cycling": "",
|
||||||
"danger-zone": "Veszélyes zóna",
|
"danger-zone": "Veszélyes zóna",
|
||||||
"date": "Dátum",
|
"date": "Dátum",
|
||||||
|
"default-category": "",
|
||||||
"default-location": "Alapértelmezett hely",
|
"default-location": "Alapértelmezett hely",
|
||||||
"delete": "Törlés",
|
"delete": "Törlés",
|
||||||
"delete-account": "Fiók törlése",
|
"delete-account": "Fiók törlése",
|
||||||
@@ -46,6 +51,7 @@
|
|||||||
"difficult": "Nehéz",
|
"difficult": "Nehéz",
|
||||||
"difficulty": "Nehézség",
|
"difficulty": "Nehézség",
|
||||||
"directions": "Irányok",
|
"directions": "Irányok",
|
||||||
|
"display": "",
|
||||||
"display-as": "Megjelenítés mint",
|
"display-as": "Megjelenítés mint",
|
||||||
"distance": "Távolság",
|
"distance": "Távolság",
|
||||||
"documentation": "Dokumentáció",
|
"documentation": "Dokumentáció",
|
||||||
@@ -66,12 +72,14 @@
|
|||||||
"error-exporting-trail": "",
|
"error-exporting-trail": "",
|
||||||
"error-printing-map": "",
|
"error-printing-map": "",
|
||||||
"error-reading-file": "Hiba a fájl olvasása közben",
|
"error-reading-file": "Hiba a fájl olvasása közben",
|
||||||
|
"error-updating-password": "",
|
||||||
"est-duration": "Becsült időtartam",
|
"est-duration": "Becsült időtartam",
|
||||||
"explore": "Felfedezés",
|
"explore": "Felfedezés",
|
||||||
"explore-some-trails": "Fedezzen fel néhány ösvényt",
|
"explore-some-trails": "Fedezzen fel néhány ösvényt",
|
||||||
"export": "",
|
"export": "",
|
||||||
"features": "Jellemzők",
|
"features": "Jellemzők",
|
||||||
"file-format": "",
|
"file-format": "",
|
||||||
|
"focus-map-on": "",
|
||||||
"french": "Francia",
|
"french": "Francia",
|
||||||
"german": "Német",
|
"german": "Német",
|
||||||
"get-position-from-exif": "",
|
"get-position-from-exif": "",
|
||||||
@@ -85,6 +93,7 @@
|
|||||||
"hungarian": "Magyar",
|
"hungarian": "Magyar",
|
||||||
"icon": "Ikon",
|
"icon": "Ikon",
|
||||||
"imperial": "Angolszász",
|
"imperial": "Angolszász",
|
||||||
|
"import": "",
|
||||||
"include-description": "",
|
"include-description": "",
|
||||||
"invalid-date": "Érvénytelen dátum",
|
"invalid-date": "Érvénytelen dátum",
|
||||||
"invalid-username": "Érvénytelen felhasználó",
|
"invalid-username": "Érvénytelen felhasználó",
|
||||||
@@ -96,6 +105,7 @@
|
|||||||
"list": "{n, plural, =1 {Lista} other {Listák}}",
|
"list": "{n, plural, =1 {Lista} other {Listák}}",
|
||||||
"location": "Helyszín",
|
"location": "Helyszín",
|
||||||
"login": "Bejelentkezés",
|
"login": "Bejelentkezés",
|
||||||
|
"login-details": "",
|
||||||
"logout": "Kijelentkezés",
|
"logout": "Kijelentkezés",
|
||||||
"longitude": "Hosszúság",
|
"longitude": "Hosszúság",
|
||||||
"make-one": "Készítsen egyet!",
|
"make-one": "Készítsen egyet!",
|
||||||
@@ -113,6 +123,7 @@
|
|||||||
"name": "Név",
|
"name": "Név",
|
||||||
"near": "Közelben",
|
"near": "Közelben",
|
||||||
"new-list": "Új lista",
|
"new-list": "Új lista",
|
||||||
|
"new-password": "",
|
||||||
"new-trail": "Új útvonal",
|
"new-trail": "Új útvonal",
|
||||||
"no-account": "Nincs még fiókja?",
|
"no-account": "Nincs még fiókja?",
|
||||||
"no-preference": "Nincs preferált",
|
"no-preference": "Nincs preferált",
|
||||||
@@ -124,6 +135,7 @@
|
|||||||
"orientation": "",
|
"orientation": "",
|
||||||
"paper-size": "",
|
"paper-size": "",
|
||||||
"password": "Jelszó",
|
"password": "Jelszó",
|
||||||
|
"password-updated": "",
|
||||||
"photos": "Képek",
|
"photos": "Képek",
|
||||||
"pick-a-trail": "Válasszon útvonalat",
|
"pick-a-trail": "Válasszon útvonalat",
|
||||||
"polish": "Lengyel",
|
"polish": "Lengyel",
|
||||||
@@ -156,12 +168,14 @@
|
|||||||
"stop-drawing": "",
|
"stop-drawing": "",
|
||||||
"summit-book": "Csúcspont könyv",
|
"summit-book": "Csúcspont könyv",
|
||||||
"text": "Szöveg",
|
"text": "Szöveg",
|
||||||
|
"tilesets": "",
|
||||||
"trail": "{n, plural, =1 {Útvonal} other {Útvonalak}}",
|
"trail": "{n, plural, =1 {Útvonal} other {Útvonalak}}",
|
||||||
"trail-not-shared": "",
|
"trail-not-shared": "",
|
||||||
"trails-for-you": "Útvonalak önnek",
|
"trails-for-you": "Útvonalak önnek",
|
||||||
"units": "Mértékegységek",
|
"units": "Mértékegységek",
|
||||||
"upload-file": "Fájl feltöltése",
|
"upload-file": "Fájl feltöltése",
|
||||||
"upload-gpx": "GPX feltöltése",
|
"upload-gpx": "GPX feltöltése",
|
||||||
|
"uploaded": "",
|
||||||
"username": "Felhasználónév",
|
"username": "Felhasználónév",
|
||||||
"view": "",
|
"view": "",
|
||||||
"waypoints": "Útvonalpontok",
|
"waypoints": "Útvonalpontok",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"Biking": "Ciclismo",
|
"Biking": "Ciclismo",
|
||||||
"Canoeing": "Canoa",
|
"Canoeing": "Canoa",
|
||||||
"Climbing": "Arrampicata",
|
"Climbing": "Arrampicata",
|
||||||
|
"export-all-trails": "",
|
||||||
"Hiking": "Escursionismo",
|
"Hiking": "Escursionismo",
|
||||||
"Skiing": "Sciare",
|
"Skiing": "Sciare",
|
||||||
"Walking": "Camminare",
|
"Walking": "Camminare",
|
||||||
@@ -23,6 +24,8 @@
|
|||||||
"categories": "Categorie",
|
"categories": "Categorie",
|
||||||
"category": "Categoria",
|
"category": "Categoria",
|
||||||
"change": "Modifica",
|
"change": "Modifica",
|
||||||
|
"change-email": "",
|
||||||
|
"change-password": "",
|
||||||
"changelog": "Changelog",
|
"changelog": "Changelog",
|
||||||
"chinese": "Cinese (semplificato)",
|
"chinese": "Cinese (semplificato)",
|
||||||
"close": "Chiudi",
|
"close": "Chiudi",
|
||||||
@@ -33,9 +36,11 @@
|
|||||||
"copy-link": "Copia link",
|
"copy-link": "Copia link",
|
||||||
"create-new-list": "Crea nuova lista",
|
"create-new-list": "Crea nuova lista",
|
||||||
"creation-date": "Data di creazione",
|
"creation-date": "Data di creazione",
|
||||||
|
"current-password": "",
|
||||||
"cycling": "Ciclismo",
|
"cycling": "Ciclismo",
|
||||||
"danger-zone": "Zona di pericolo",
|
"danger-zone": "Zona di pericolo",
|
||||||
"date": "Data",
|
"date": "Data",
|
||||||
|
"default-category": "",
|
||||||
"default-location": "Posizione predefinita",
|
"default-location": "Posizione predefinita",
|
||||||
"delete": "Elimina",
|
"delete": "Elimina",
|
||||||
"delete-account": "Elimina account",
|
"delete-account": "Elimina account",
|
||||||
@@ -46,6 +51,7 @@
|
|||||||
"difficult": "Difficile",
|
"difficult": "Difficile",
|
||||||
"difficulty": "Difficoltà",
|
"difficulty": "Difficoltà",
|
||||||
"directions": "Indicazioni",
|
"directions": "Indicazioni",
|
||||||
|
"display": "",
|
||||||
"display-as": "Visualizza come",
|
"display-as": "Visualizza come",
|
||||||
"distance": "Distanza",
|
"distance": "Distanza",
|
||||||
"documentation": "Documentazione",
|
"documentation": "Documentazione",
|
||||||
@@ -66,12 +72,14 @@
|
|||||||
"error-exporting-trail": "Errore durante l'esportazione del percorso",
|
"error-exporting-trail": "Errore durante l'esportazione del percorso",
|
||||||
"error-printing-map": "Errore durante la stampa della mappa",
|
"error-printing-map": "Errore durante la stampa della mappa",
|
||||||
"error-reading-file": "Errore durante la lettura del file",
|
"error-reading-file": "Errore durante la lettura del file",
|
||||||
|
"error-updating-password": "",
|
||||||
"est-duration": "Durata stimata",
|
"est-duration": "Durata stimata",
|
||||||
"explore": "Esplora",
|
"explore": "Esplora",
|
||||||
"explore-some-trails": "Esplora alcuni percorsi",
|
"explore-some-trails": "Esplora alcuni percorsi",
|
||||||
"export": "Esporta",
|
"export": "Esporta",
|
||||||
"features": "Caratteristiche",
|
"features": "Caratteristiche",
|
||||||
"file-format": "Formato del file",
|
"file-format": "Formato del file",
|
||||||
|
"focus-map-on": "",
|
||||||
"french": "Francese",
|
"french": "Francese",
|
||||||
"german": "Tedesco",
|
"german": "Tedesco",
|
||||||
"get-position-from-exif": "Ottieni posizione da dati EXIF",
|
"get-position-from-exif": "Ottieni posizione da dati EXIF",
|
||||||
@@ -85,9 +93,11 @@
|
|||||||
"hungarian": "Ungherese",
|
"hungarian": "Ungherese",
|
||||||
"icon": "Icona",
|
"icon": "Icona",
|
||||||
"imperial": "Imperiale",
|
"imperial": "Imperiale",
|
||||||
|
"import": "",
|
||||||
"include-description": "Adotta descrizione",
|
"include-description": "Adotta descrizione",
|
||||||
"invalid-date": "Data non valida",
|
"invalid-date": "Data non valida",
|
||||||
"invalid-username": "Nome utente non valido",
|
"invalid-username": "Nome utente non valido",
|
||||||
|
"italian": "",
|
||||||
"language": "Lingua",
|
"language": "Lingua",
|
||||||
"latitude": "Latitudine",
|
"latitude": "Latitudine",
|
||||||
"license": "Licenza",
|
"license": "Licenza",
|
||||||
@@ -95,6 +105,7 @@
|
|||||||
"list": "{n, plural, =1 {Lista} other {Liste}}",
|
"list": "{n, plural, =1 {Lista} other {Liste}}",
|
||||||
"location": "Posizione",
|
"location": "Posizione",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
|
"login-details": "",
|
||||||
"logout": "Logout",
|
"logout": "Logout",
|
||||||
"longitude": "Longitudine",
|
"longitude": "Longitudine",
|
||||||
"make-one": "Creane uno!",
|
"make-one": "Creane uno!",
|
||||||
@@ -112,6 +123,7 @@
|
|||||||
"name": "Nome",
|
"name": "Nome",
|
||||||
"near": "Vicino",
|
"near": "Vicino",
|
||||||
"new-list": "Nuova lista",
|
"new-list": "Nuova lista",
|
||||||
|
"new-password": "",
|
||||||
"new-trail": "Nuovo percorso",
|
"new-trail": "Nuovo percorso",
|
||||||
"no-account": "Non hai ancora un account?",
|
"no-account": "Non hai ancora un account?",
|
||||||
"no-preference": "Nessuna preferenza",
|
"no-preference": "Nessuna preferenza",
|
||||||
@@ -123,6 +135,7 @@
|
|||||||
"orientation": "Orientamento",
|
"orientation": "Orientamento",
|
||||||
"paper-size": "Formato carta",
|
"paper-size": "Formato carta",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
|
"password-updated": "",
|
||||||
"photos": "Foto",
|
"photos": "Foto",
|
||||||
"pick-a-trail": "Scegli un percorso",
|
"pick-a-trail": "Scegli un percorso",
|
||||||
"polish": "Polacco",
|
"polish": "Polacco",
|
||||||
@@ -155,12 +168,14 @@
|
|||||||
"stop-drawing": "Smettere di disegnare",
|
"stop-drawing": "Smettere di disegnare",
|
||||||
"summit-book": "Libro di vetta",
|
"summit-book": "Libro di vetta",
|
||||||
"text": "Testo",
|
"text": "Testo",
|
||||||
|
"tilesets": "",
|
||||||
"trail": "{n, plural, =1 {Percorso} other {Percorsi}}",
|
"trail": "{n, plural, =1 {Percorso} other {Percorsi}}",
|
||||||
"trail-not-shared": "Percorso non condiviso con nessuno",
|
"trail-not-shared": "Percorso non condiviso con nessuno",
|
||||||
"trails-for-you": "Percorsi per te",
|
"trails-for-you": "Percorsi per te",
|
||||||
"units": "Unità",
|
"units": "Unità",
|
||||||
"upload-file": "Carica file",
|
"upload-file": "Carica file",
|
||||||
"upload-gpx": "Carica file GPX",
|
"upload-gpx": "Carica file GPX",
|
||||||
|
"uploaded": "",
|
||||||
"username": "Nome utente",
|
"username": "Nome utente",
|
||||||
"view": "Visualizza",
|
"view": "Visualizza",
|
||||||
"waypoints": "{n, plural, =1 {Waypoint} other {Waypoint}}",
|
"waypoints": "{n, plural, =1 {Waypoint} other {Waypoint}}",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"Biking": "",
|
"Biking": "",
|
||||||
"Canoeing": "",
|
"Canoeing": "",
|
||||||
"Climbing": "",
|
"Climbing": "",
|
||||||
|
"export-all-trails": "",
|
||||||
"Hiking": "",
|
"Hiking": "",
|
||||||
"Skiing": "",
|
"Skiing": "",
|
||||||
"Walking": "",
|
"Walking": "",
|
||||||
@@ -23,6 +24,8 @@
|
|||||||
"categories": "Categorieën",
|
"categories": "Categorieën",
|
||||||
"category": "Categorie",
|
"category": "Categorie",
|
||||||
"change": "Wijzigen",
|
"change": "Wijzigen",
|
||||||
|
"change-email": "",
|
||||||
|
"change-password": "",
|
||||||
"changelog": "Wijzigingslog",
|
"changelog": "Wijzigingslog",
|
||||||
"chinese": "Versimpeld Chinees",
|
"chinese": "Versimpeld Chinees",
|
||||||
"close": "",
|
"close": "",
|
||||||
@@ -33,9 +36,11 @@
|
|||||||
"copy-link": "",
|
"copy-link": "",
|
||||||
"create-new-list": "Nieuwe lijst",
|
"create-new-list": "Nieuwe lijst",
|
||||||
"creation-date": "Aanmaakdatum",
|
"creation-date": "Aanmaakdatum",
|
||||||
|
"current-password": "",
|
||||||
"cycling": "",
|
"cycling": "",
|
||||||
"danger-zone": "Gevarenzone",
|
"danger-zone": "Gevarenzone",
|
||||||
"date": "Datum",
|
"date": "Datum",
|
||||||
|
"default-category": "",
|
||||||
"default-location": "Standaardlocatie",
|
"default-location": "Standaardlocatie",
|
||||||
"delete": "Verwijderen",
|
"delete": "Verwijderen",
|
||||||
"delete-account": "Account verwijderen",
|
"delete-account": "Account verwijderen",
|
||||||
@@ -46,6 +51,7 @@
|
|||||||
"difficult": "Moeilijk",
|
"difficult": "Moeilijk",
|
||||||
"difficulty": "Moeilijkheidsgraad",
|
"difficulty": "Moeilijkheidsgraad",
|
||||||
"directions": "Routebeschrijving",
|
"directions": "Routebeschrijving",
|
||||||
|
"display": "",
|
||||||
"display-as": "Tonen als",
|
"display-as": "Tonen als",
|
||||||
"distance": "Afstand",
|
"distance": "Afstand",
|
||||||
"documentation": "Documentatie",
|
"documentation": "Documentatie",
|
||||||
@@ -66,12 +72,14 @@
|
|||||||
"error-exporting-trail": "",
|
"error-exporting-trail": "",
|
||||||
"error-printing-map": "Fout bij afdrukken van kaart",
|
"error-printing-map": "Fout bij afdrukken van kaart",
|
||||||
"error-reading-file": "Het bestand kan niet worden ingelezen",
|
"error-reading-file": "Het bestand kan niet worden ingelezen",
|
||||||
|
"error-updating-password": "",
|
||||||
"est-duration": "Geschatte duur",
|
"est-duration": "Geschatte duur",
|
||||||
"explore": "Verkennen",
|
"explore": "Verkennen",
|
||||||
"explore-some-trails": "Verken enkele wandelroutes",
|
"explore-some-trails": "Verken enkele wandelroutes",
|
||||||
"export": "",
|
"export": "",
|
||||||
"features": "Kenmerken",
|
"features": "Kenmerken",
|
||||||
"file-format": "",
|
"file-format": "",
|
||||||
|
"focus-map-on": "",
|
||||||
"french": "Frans",
|
"french": "Frans",
|
||||||
"german": "Duits",
|
"german": "Duits",
|
||||||
"get-position-from-exif": "",
|
"get-position-from-exif": "",
|
||||||
@@ -85,6 +93,7 @@
|
|||||||
"hungarian": "Hongaars",
|
"hungarian": "Hongaars",
|
||||||
"icon": "Pictogram",
|
"icon": "Pictogram",
|
||||||
"imperial": "Imperiaal",
|
"imperial": "Imperiaal",
|
||||||
|
"import": "",
|
||||||
"include-description": "Inclusief beschrijving",
|
"include-description": "Inclusief beschrijving",
|
||||||
"invalid-date": "Ongeldige datum",
|
"invalid-date": "Ongeldige datum",
|
||||||
"invalid-username": "Ongeldige gebruikersnaam",
|
"invalid-username": "Ongeldige gebruikersnaam",
|
||||||
@@ -96,6 +105,7 @@
|
|||||||
"list": "{n, plural, =1 {Lijst} other {Lijsten}}",
|
"list": "{n, plural, =1 {Lijst} other {Lijsten}}",
|
||||||
"location": "Locatie",
|
"location": "Locatie",
|
||||||
"login": "Inloggen",
|
"login": "Inloggen",
|
||||||
|
"login-details": "",
|
||||||
"logout": "Uitloggen",
|
"logout": "Uitloggen",
|
||||||
"longitude": "Lengtegraad",
|
"longitude": "Lengtegraad",
|
||||||
"make-one": "Maak er een aan!",
|
"make-one": "Maak er een aan!",
|
||||||
@@ -113,6 +123,7 @@
|
|||||||
"name": "Naam",
|
"name": "Naam",
|
||||||
"near": "Nabij",
|
"near": "Nabij",
|
||||||
"new-list": "Nieuwe lijst",
|
"new-list": "Nieuwe lijst",
|
||||||
|
"new-password": "",
|
||||||
"new-trail": "Nieuwe wandelroute",
|
"new-trail": "Nieuwe wandelroute",
|
||||||
"no-account": "Heb je nog geen account?",
|
"no-account": "Heb je nog geen account?",
|
||||||
"no-preference": "Geen voorkeur",
|
"no-preference": "Geen voorkeur",
|
||||||
@@ -124,6 +135,7 @@
|
|||||||
"orientation": "Oriëntatie",
|
"orientation": "Oriëntatie",
|
||||||
"paper-size": "Papierformaat",
|
"paper-size": "Papierformaat",
|
||||||
"password": "Wachtwoord",
|
"password": "Wachtwoord",
|
||||||
|
"password-updated": "",
|
||||||
"photos": "Foto's",
|
"photos": "Foto's",
|
||||||
"pick-a-trail": "Kies een wandelroute",
|
"pick-a-trail": "Kies een wandelroute",
|
||||||
"polish": "Pools",
|
"polish": "Pools",
|
||||||
@@ -156,12 +168,14 @@
|
|||||||
"stop-drawing": "",
|
"stop-drawing": "",
|
||||||
"summit-book": "Bergtopboek",
|
"summit-book": "Bergtopboek",
|
||||||
"text": "Tekst",
|
"text": "Tekst",
|
||||||
|
"tilesets": "",
|
||||||
"trail": "{n, plural, =1 {Wandelroute} other {Wandelroutes}}",
|
"trail": "{n, plural, =1 {Wandelroute} other {Wandelroutes}}",
|
||||||
"trail-not-shared": "",
|
"trail-not-shared": "",
|
||||||
"trails-for-you": "Wandelroutes voor jou",
|
"trails-for-you": "Wandelroutes voor jou",
|
||||||
"units": "Eenheden",
|
"units": "Eenheden",
|
||||||
"upload-file": "Bestand uploaden",
|
"upload-file": "Bestand uploaden",
|
||||||
"upload-gpx": "GPX-bestand uploaden",
|
"upload-gpx": "GPX-bestand uploaden",
|
||||||
|
"uploaded": "",
|
||||||
"username": "Gebruikersnaam",
|
"username": "Gebruikersnaam",
|
||||||
"view": "",
|
"view": "",
|
||||||
"waypoints": "Routepunten",
|
"waypoints": "Routepunten",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"Biking": "",
|
"Biking": "",
|
||||||
"Canoeing": "",
|
"Canoeing": "",
|
||||||
"Climbing": "",
|
"Climbing": "",
|
||||||
|
"export-all-trails": "",
|
||||||
"Hiking": "",
|
"Hiking": "",
|
||||||
"Skiing": "",
|
"Skiing": "",
|
||||||
"Walking": "",
|
"Walking": "",
|
||||||
@@ -23,6 +24,8 @@
|
|||||||
"categories": "Kategorie",
|
"categories": "Kategorie",
|
||||||
"category": "Kategoria",
|
"category": "Kategoria",
|
||||||
"change": "Zmień",
|
"change": "Zmień",
|
||||||
|
"change-email": "",
|
||||||
|
"change-password": "",
|
||||||
"changelog": "Dziennik zmian",
|
"changelog": "Dziennik zmian",
|
||||||
"chinese": "Uproszczony chiński",
|
"chinese": "Uproszczony chiński",
|
||||||
"close": "",
|
"close": "",
|
||||||
@@ -33,9 +36,11 @@
|
|||||||
"copy-link": "",
|
"copy-link": "",
|
||||||
"create-new-list": "Stwórz nową listę",
|
"create-new-list": "Stwórz nową listę",
|
||||||
"creation-date": "Data dodania",
|
"creation-date": "Data dodania",
|
||||||
|
"current-password": "",
|
||||||
"cycling": "",
|
"cycling": "",
|
||||||
"danger-zone": "Strefa niebezpieczna",
|
"danger-zone": "Strefa niebezpieczna",
|
||||||
"date": "Data",
|
"date": "Data",
|
||||||
|
"default-category": "",
|
||||||
"default-location": "Domyślna Lokalizacja",
|
"default-location": "Domyślna Lokalizacja",
|
||||||
"delete": "Usuń",
|
"delete": "Usuń",
|
||||||
"delete-account": "Usuń Konto",
|
"delete-account": "Usuń Konto",
|
||||||
@@ -46,6 +51,7 @@
|
|||||||
"difficult": "Trudna",
|
"difficult": "Trudna",
|
||||||
"difficulty": "Trudność",
|
"difficulty": "Trudność",
|
||||||
"directions": "Kierunki",
|
"directions": "Kierunki",
|
||||||
|
"display": "",
|
||||||
"display-as": "Wyświetl jako",
|
"display-as": "Wyświetl jako",
|
||||||
"distance": "Dystans",
|
"distance": "Dystans",
|
||||||
"documentation": "Dokumentacja",
|
"documentation": "Dokumentacja",
|
||||||
@@ -66,12 +72,14 @@
|
|||||||
"error-exporting-trail": "",
|
"error-exporting-trail": "",
|
||||||
"error-printing-map": "",
|
"error-printing-map": "",
|
||||||
"error-reading-file": "Błąd wczytywania pliku",
|
"error-reading-file": "Błąd wczytywania pliku",
|
||||||
|
"error-updating-password": "",
|
||||||
"est-duration": "Szacowany czas",
|
"est-duration": "Szacowany czas",
|
||||||
"explore": "Eksploruj",
|
"explore": "Eksploruj",
|
||||||
"explore-some-trails": "Eksploruj różne ścieżki",
|
"explore-some-trails": "Eksploruj różne ścieżki",
|
||||||
"export": "",
|
"export": "",
|
||||||
"features": "Funkcje",
|
"features": "Funkcje",
|
||||||
"file-format": "",
|
"file-format": "",
|
||||||
|
"focus-map-on": "",
|
||||||
"french": "Francuski",
|
"french": "Francuski",
|
||||||
"german": "Niemiecki",
|
"german": "Niemiecki",
|
||||||
"get-position-from-exif": "",
|
"get-position-from-exif": "",
|
||||||
@@ -85,6 +93,7 @@
|
|||||||
"hungarian": "Język węgierski",
|
"hungarian": "Język węgierski",
|
||||||
"icon": "Ikona",
|
"icon": "Ikona",
|
||||||
"imperial": "Anglosaskie",
|
"imperial": "Anglosaskie",
|
||||||
|
"import": "",
|
||||||
"include-description": "",
|
"include-description": "",
|
||||||
"invalid-date": "Nieprawidłowa data",
|
"invalid-date": "Nieprawidłowa data",
|
||||||
"invalid-username": "Błędna nazwa użytkownika",
|
"invalid-username": "Błędna nazwa użytkownika",
|
||||||
@@ -96,6 +105,7 @@
|
|||||||
"list": "{n, plural, =1 {Lista} other {Listy}}",
|
"list": "{n, plural, =1 {Lista} other {Listy}}",
|
||||||
"location": "Lokalizacja",
|
"location": "Lokalizacja",
|
||||||
"login": "Zaloguj się",
|
"login": "Zaloguj się",
|
||||||
|
"login-details": "",
|
||||||
"logout": "Wyloguj",
|
"logout": "Wyloguj",
|
||||||
"longitude": "Wysokość",
|
"longitude": "Wysokość",
|
||||||
"make-one": "Stwórz ją!",
|
"make-one": "Stwórz ją!",
|
||||||
@@ -113,6 +123,7 @@
|
|||||||
"name": "Nazwa",
|
"name": "Nazwa",
|
||||||
"near": "Blisko",
|
"near": "Blisko",
|
||||||
"new-list": "Nowa Lista",
|
"new-list": "Nowa Lista",
|
||||||
|
"new-password": "",
|
||||||
"new-trail": "Nowa ścieżka",
|
"new-trail": "Nowa ścieżka",
|
||||||
"no-account": "Nie masz konta?",
|
"no-account": "Nie masz konta?",
|
||||||
"no-preference": "Brak preferencji",
|
"no-preference": "Brak preferencji",
|
||||||
@@ -124,6 +135,7 @@
|
|||||||
"orientation": "",
|
"orientation": "",
|
||||||
"paper-size": "",
|
"paper-size": "",
|
||||||
"password": "Hasło",
|
"password": "Hasło",
|
||||||
|
"password-updated": "",
|
||||||
"photos": "Zdjęcia",
|
"photos": "Zdjęcia",
|
||||||
"pick-a-trail": "Wybierz ścieżkę",
|
"pick-a-trail": "Wybierz ścieżkę",
|
||||||
"polish": "Polski",
|
"polish": "Polski",
|
||||||
@@ -156,12 +168,14 @@
|
|||||||
"stop-drawing": "",
|
"stop-drawing": "",
|
||||||
"summit-book": "Logbook",
|
"summit-book": "Logbook",
|
||||||
"text": "Tekst",
|
"text": "Tekst",
|
||||||
|
"tilesets": "",
|
||||||
"trail": "{n, plural, =1 {Szlak} other {Szlaki}}",
|
"trail": "{n, plural, =1 {Szlak} other {Szlaki}}",
|
||||||
"trail-not-shared": "",
|
"trail-not-shared": "",
|
||||||
"trails-for-you": "Ścieżki dla ciebie",
|
"trails-for-you": "Ścieżki dla ciebie",
|
||||||
"units": "Jednostki",
|
"units": "Jednostki",
|
||||||
"upload-file": "Przesyłanie pliku",
|
"upload-file": "Przesyłanie pliku",
|
||||||
"upload-gpx": "Importuj GPX",
|
"upload-gpx": "Importuj GPX",
|
||||||
|
"uploaded": "",
|
||||||
"username": "Nazwa Użytkownika",
|
"username": "Nazwa Użytkownika",
|
||||||
"view": "",
|
"view": "",
|
||||||
"waypoints": "Punkty",
|
"waypoints": "Punkty",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"Biking": "",
|
"Biking": "",
|
||||||
"Canoeing": "",
|
"Canoeing": "",
|
||||||
"Climbing": "",
|
"Climbing": "",
|
||||||
|
"export-all-trails": "",
|
||||||
"Hiking": "",
|
"Hiking": "",
|
||||||
"Skiing": "",
|
"Skiing": "",
|
||||||
"Walking": "",
|
"Walking": "",
|
||||||
@@ -23,6 +24,8 @@
|
|||||||
"categories": "Categorias",
|
"categories": "Categorias",
|
||||||
"category": "Categoria",
|
"category": "Categoria",
|
||||||
"change": "Alterar",
|
"change": "Alterar",
|
||||||
|
"change-email": "",
|
||||||
|
"change-password": "",
|
||||||
"changelog": "Registo de alterações",
|
"changelog": "Registo de alterações",
|
||||||
"chinese": "Chinês (simplificado)",
|
"chinese": "Chinês (simplificado)",
|
||||||
"close": "",
|
"close": "",
|
||||||
@@ -33,9 +36,11 @@
|
|||||||
"copy-link": "",
|
"copy-link": "",
|
||||||
"create-new-list": "Criar nova lista",
|
"create-new-list": "Criar nova lista",
|
||||||
"creation-date": "Data de criação",
|
"creation-date": "Data de criação",
|
||||||
|
"current-password": "",
|
||||||
"cycling": "",
|
"cycling": "",
|
||||||
"danger-zone": "Zona de perigo",
|
"danger-zone": "Zona de perigo",
|
||||||
"date": "Data",
|
"date": "Data",
|
||||||
|
"default-category": "",
|
||||||
"default-location": "Localização padrão",
|
"default-location": "Localização padrão",
|
||||||
"delete": "Excluir",
|
"delete": "Excluir",
|
||||||
"delete-account": "Excluir conta",
|
"delete-account": "Excluir conta",
|
||||||
@@ -46,6 +51,7 @@
|
|||||||
"difficult": "Difícil",
|
"difficult": "Difícil",
|
||||||
"difficulty": "Dificuldade",
|
"difficulty": "Dificuldade",
|
||||||
"directions": "Instruções",
|
"directions": "Instruções",
|
||||||
|
"display": "",
|
||||||
"display-as": "Exibir como",
|
"display-as": "Exibir como",
|
||||||
"distance": "Distância",
|
"distance": "Distância",
|
||||||
"documentation": "Documentação",
|
"documentation": "Documentação",
|
||||||
@@ -66,12 +72,14 @@
|
|||||||
"error-exporting-trail": "",
|
"error-exporting-trail": "",
|
||||||
"error-printing-map": "",
|
"error-printing-map": "",
|
||||||
"error-reading-file": "Erro ao ler o arquivo",
|
"error-reading-file": "Erro ao ler o arquivo",
|
||||||
|
"error-updating-password": "",
|
||||||
"est-duration": "Duração prevista",
|
"est-duration": "Duração prevista",
|
||||||
"explore": "Explorar",
|
"explore": "Explorar",
|
||||||
"explore-some-trails": "Explore algumas trilhas",
|
"explore-some-trails": "Explore algumas trilhas",
|
||||||
"export": "",
|
"export": "",
|
||||||
"features": "Características",
|
"features": "Características",
|
||||||
"file-format": "",
|
"file-format": "",
|
||||||
|
"focus-map-on": "",
|
||||||
"french": "Francês",
|
"french": "Francês",
|
||||||
"german": "Alemão",
|
"german": "Alemão",
|
||||||
"get-position-from-exif": "",
|
"get-position-from-exif": "",
|
||||||
@@ -85,6 +93,7 @@
|
|||||||
"hungarian": "Húngaro",
|
"hungarian": "Húngaro",
|
||||||
"icon": "Ícone",
|
"icon": "Ícone",
|
||||||
"imperial": "Imperial",
|
"imperial": "Imperial",
|
||||||
|
"import": "",
|
||||||
"include-description": "",
|
"include-description": "",
|
||||||
"invalid-date": "Data inválida",
|
"invalid-date": "Data inválida",
|
||||||
"invalid-username": "Nome de usuário inválido",
|
"invalid-username": "Nome de usuário inválido",
|
||||||
@@ -96,6 +105,7 @@
|
|||||||
"list": "{n, plural, =1 {Lista} other {Listas}}",
|
"list": "{n, plural, =1 {Lista} other {Listas}}",
|
||||||
"location": "Localização",
|
"location": "Localização",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
|
"login-details": "",
|
||||||
"logout": "Sair",
|
"logout": "Sair",
|
||||||
"longitude": "Longitude",
|
"longitude": "Longitude",
|
||||||
"make-one": "Faz um!",
|
"make-one": "Faz um!",
|
||||||
@@ -113,6 +123,7 @@
|
|||||||
"name": "Nome",
|
"name": "Nome",
|
||||||
"near": "Próximo",
|
"near": "Próximo",
|
||||||
"new-list": "Nova lista",
|
"new-list": "Nova lista",
|
||||||
|
"new-password": "",
|
||||||
"new-trail": "Nova trilha",
|
"new-trail": "Nova trilha",
|
||||||
"no-account": "Não tem uma conta?",
|
"no-account": "Não tem uma conta?",
|
||||||
"no-preference": "Nenhuma preferência",
|
"no-preference": "Nenhuma preferência",
|
||||||
@@ -124,6 +135,7 @@
|
|||||||
"orientation": "",
|
"orientation": "",
|
||||||
"paper-size": "",
|
"paper-size": "",
|
||||||
"password": "Senha",
|
"password": "Senha",
|
||||||
|
"password-updated": "",
|
||||||
"photos": "Fotos",
|
"photos": "Fotos",
|
||||||
"pick-a-trail": "Escolha uma trilha",
|
"pick-a-trail": "Escolha uma trilha",
|
||||||
"polish": "Polonês",
|
"polish": "Polonês",
|
||||||
@@ -156,12 +168,14 @@
|
|||||||
"stop-drawing": "",
|
"stop-drawing": "",
|
||||||
"summit-book": "Livro da cimeira",
|
"summit-book": "Livro da cimeira",
|
||||||
"text": "Texto",
|
"text": "Texto",
|
||||||
|
"tilesets": "",
|
||||||
"trail": "{n, plural, =1 {Percurso} other {Percursos}}",
|
"trail": "{n, plural, =1 {Percurso} other {Percursos}}",
|
||||||
"trail-not-shared": "",
|
"trail-not-shared": "",
|
||||||
"trails-for-you": "Trilhos para si",
|
"trails-for-you": "Trilhos para si",
|
||||||
"units": "Unidades",
|
"units": "Unidades",
|
||||||
"upload-file": "Subir arquivo",
|
"upload-file": "Subir arquivo",
|
||||||
"upload-gpx": "Carregar GPX",
|
"upload-gpx": "Carregar GPX",
|
||||||
|
"uploaded": "",
|
||||||
"username": "Nome de utilizador",
|
"username": "Nome de utilizador",
|
||||||
"view": "",
|
"view": "",
|
||||||
"waypoints": "Pontos de passagem",
|
"waypoints": "Pontos de passagem",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"Biking": "",
|
"Biking": "",
|
||||||
"Canoeing": "",
|
"Canoeing": "",
|
||||||
"Climbing": "",
|
"Climbing": "",
|
||||||
|
"export-all-trails": "",
|
||||||
"Hiking": "",
|
"Hiking": "",
|
||||||
"Skiing": "",
|
"Skiing": "",
|
||||||
"Walking": "",
|
"Walking": "",
|
||||||
@@ -23,6 +24,8 @@
|
|||||||
"categories": "分类",
|
"categories": "分类",
|
||||||
"category": "分类",
|
"category": "分类",
|
||||||
"change": "更换",
|
"change": "更换",
|
||||||
|
"change-email": "",
|
||||||
|
"change-password": "",
|
||||||
"changelog": "变更日志",
|
"changelog": "变更日志",
|
||||||
"chinese": "华人",
|
"chinese": "华人",
|
||||||
"close": "",
|
"close": "",
|
||||||
@@ -33,9 +36,11 @@
|
|||||||
"copy-link": "",
|
"copy-link": "",
|
||||||
"create-new-list": "创建新列表",
|
"create-new-list": "创建新列表",
|
||||||
"creation-date": "创建日期",
|
"creation-date": "创建日期",
|
||||||
|
"current-password": "",
|
||||||
"cycling": "",
|
"cycling": "",
|
||||||
"danger-zone": "危险区域",
|
"danger-zone": "危险区域",
|
||||||
"date": "日期",
|
"date": "日期",
|
||||||
|
"default-category": "",
|
||||||
"default-location": "默认地点",
|
"default-location": "默认地点",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
"delete-account": "注销账户",
|
"delete-account": "注销账户",
|
||||||
@@ -46,6 +51,7 @@
|
|||||||
"difficult": "困难",
|
"difficult": "困难",
|
||||||
"difficulty": "难度",
|
"difficulty": "难度",
|
||||||
"directions": "导航",
|
"directions": "导航",
|
||||||
|
"display": "",
|
||||||
"display-as": "显示方式",
|
"display-as": "显示方式",
|
||||||
"distance": "距离",
|
"distance": "距离",
|
||||||
"documentation": "文档",
|
"documentation": "文档",
|
||||||
@@ -66,12 +72,14 @@
|
|||||||
"error-exporting-trail": "",
|
"error-exporting-trail": "",
|
||||||
"error-printing-map": "",
|
"error-printing-map": "",
|
||||||
"error-reading-file": "读取文件错误",
|
"error-reading-file": "读取文件错误",
|
||||||
|
"error-updating-password": "",
|
||||||
"est-duration": "预计时长",
|
"est-duration": "预计时长",
|
||||||
"explore": "探索",
|
"explore": "探索",
|
||||||
"explore-some-trails": "探索行程",
|
"explore-some-trails": "探索行程",
|
||||||
"export": "",
|
"export": "",
|
||||||
"features": "特性",
|
"features": "特性",
|
||||||
"file-format": "",
|
"file-format": "",
|
||||||
|
"focus-map-on": "",
|
||||||
"french": "法语",
|
"french": "法语",
|
||||||
"german": "德语",
|
"german": "德语",
|
||||||
"get-position-from-exif": "",
|
"get-position-from-exif": "",
|
||||||
@@ -85,6 +93,7 @@
|
|||||||
"hungarian": "匈牙利语",
|
"hungarian": "匈牙利语",
|
||||||
"icon": "图标",
|
"icon": "图标",
|
||||||
"imperial": "英制",
|
"imperial": "英制",
|
||||||
|
"import": "",
|
||||||
"include-description": "",
|
"include-description": "",
|
||||||
"invalid-date": "无效日期",
|
"invalid-date": "无效日期",
|
||||||
"invalid-username": "无效用户名",
|
"invalid-username": "无效用户名",
|
||||||
@@ -96,6 +105,7 @@
|
|||||||
"list": "{n, plural, =1 {列表} other {列表}}",
|
"list": "{n, plural, =1 {列表} other {列表}}",
|
||||||
"location": "地点",
|
"location": "地点",
|
||||||
"login": "登录",
|
"login": "登录",
|
||||||
|
"login-details": "",
|
||||||
"logout": "登出",
|
"logout": "登出",
|
||||||
"longitude": "经度",
|
"longitude": "经度",
|
||||||
"make-one": "立刻注册!",
|
"make-one": "立刻注册!",
|
||||||
@@ -113,6 +123,7 @@
|
|||||||
"name": "名称",
|
"name": "名称",
|
||||||
"near": "附近",
|
"near": "附近",
|
||||||
"new-list": "新列表",
|
"new-list": "新列表",
|
||||||
|
"new-password": "",
|
||||||
"new-trail": "创建新路线",
|
"new-trail": "创建新路线",
|
||||||
"no-account": "还未注册?",
|
"no-account": "还未注册?",
|
||||||
"no-preference": "尚未规划",
|
"no-preference": "尚未规划",
|
||||||
@@ -124,6 +135,7 @@
|
|||||||
"orientation": "",
|
"orientation": "",
|
||||||
"paper-size": "",
|
"paper-size": "",
|
||||||
"password": "密码",
|
"password": "密码",
|
||||||
|
"password-updated": "",
|
||||||
"photos": "图片",
|
"photos": "图片",
|
||||||
"pick-a-trail": "选择一个路线",
|
"pick-a-trail": "选择一个路线",
|
||||||
"polish": "波兰语",
|
"polish": "波兰语",
|
||||||
@@ -156,12 +168,14 @@
|
|||||||
"stop-drawing": "",
|
"stop-drawing": "",
|
||||||
"summit-book": "详细日程",
|
"summit-book": "详细日程",
|
||||||
"text": "文本",
|
"text": "文本",
|
||||||
|
"tilesets": "",
|
||||||
"trail": "{n, plural, =1 {路线} other {路线}}",
|
"trail": "{n, plural, =1 {路线} other {路线}}",
|
||||||
"trail-not-shared": "",
|
"trail-not-shared": "",
|
||||||
"trails-for-you": "推荐路线",
|
"trails-for-you": "推荐路线",
|
||||||
"units": "单位",
|
"units": "单位",
|
||||||
"upload-file": "上传文件",
|
"upload-file": "上传文件",
|
||||||
"upload-gpx": "上传 GPX",
|
"upload-gpx": "上传 GPX",
|
||||||
|
"uploaded": "",
|
||||||
"username": "用户名",
|
"username": "用户名",
|
||||||
"view": "",
|
"view": "",
|
||||||
"waypoints": "坐标",
|
"waypoints": "坐标",
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ class Settings {
|
|||||||
language?: "en" | "de" | "fr" | "hu"| "it" | "nl" | "pl" | "pt" | "zh";
|
language?: "en" | "de" | "fr" | "hu"| "it" | "nl" | "pl" | "pt" | "zh";
|
||||||
mapFocus?: "trails" | "location";
|
mapFocus?: "trails" | "location";
|
||||||
location?: { name: string, lat: number, lon: number };
|
location?: { name: string, lat: number, lon: number };
|
||||||
|
category?: string;
|
||||||
|
tilesets?: {name: string, url: string}[]
|
||||||
user?: string;
|
user?: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -13,7 +15,9 @@ class Settings {
|
|||||||
mapFocus: "trails" | "location",
|
mapFocus: "trails" | "location",
|
||||||
user: string,
|
user: string,
|
||||||
params?: {
|
params?: {
|
||||||
location: { name: string, lat: number, lon: number }
|
location?: { name: string, lat: number, lon: number }
|
||||||
|
category?: string
|
||||||
|
tilesets?: {name: string, url: string}[]
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
this.unit = unit;
|
this.unit = unit;
|
||||||
@@ -21,6 +25,8 @@ class Settings {
|
|||||||
this.mapFocus = mapFocus;
|
this.mapFocus = mapFocus;
|
||||||
this.user = user;
|
this.user = user;
|
||||||
this.location = params?.location;
|
this.location = params?.location;
|
||||||
|
this.category = params?.category;
|
||||||
|
this.tilesets = params?.tilesets ?? [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ export const trail: Writable<Trail> = writable(new Trail(""));
|
|||||||
|
|
||||||
export const editTrail: Writable<Trail> = writable(new Trail(""));
|
export const editTrail: Writable<Trail> = writable(new Trail(""));
|
||||||
|
|
||||||
export async function trails_index(data: { perPage: number, random?: boolean, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> } = { perPage: 30, random: false, f: fetch }) {
|
export async function trails_index(perPage: number = 21, random: boolean = false, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||||
const r = await data.f('/api/v1/trail?' + new URLSearchParams({
|
const r = await f('/api/v1/trail?' + new URLSearchParams({
|
||||||
"per-page": data.perPage.toString(),
|
"per-page": perPage.toString(),
|
||||||
expand: "category,waypoints,summit_logs",
|
expand: "category,waypoints,summit_logs",
|
||||||
sort: data.random ? "@random" : ""
|
sort: random ? "@random" : ""
|
||||||
}), {
|
}), {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
})
|
})
|
||||||
@@ -343,6 +343,22 @@ export async function trails_get_bounding_box(f: (url: RequestInfo | URL, config
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function trails_upload(file: File, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch): Promise<TrailFilterValues> {
|
||||||
|
const r = await f('/api/v1/trail/upload', {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data"
|
||||||
|
},
|
||||||
|
method: 'PUT',
|
||||||
|
body: file
|
||||||
|
})
|
||||||
|
|
||||||
|
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) {
|
export async function fetchGPX(trail: Trail, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||||
if (!trail.gpx) {
|
if (!trail.gpx) {
|
||||||
return "";
|
return "";
|
||||||
|
|||||||
@@ -107,12 +107,11 @@ export async function users_update(user: User | { [K in keyof User]?: User[K] },
|
|||||||
throw new ClientResponseError(await r.json())
|
throw new ClientResponseError(await r.json())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (avatar) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
if (avatar) {
|
|
||||||
formData.append("avatar", avatar);
|
formData.append("avatar", avatar);
|
||||||
}
|
|
||||||
|
|
||||||
r = await fetch(`/api/v1/user/${user.id!}/file`, {
|
r = await fetch(`/api/v1/user/${user.id!}/file`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -122,6 +121,7 @@ export async function users_update(user: User | { [K in keyof User]?: User[K] },
|
|||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
throw new ClientResponseError(await r.json())
|
throw new ClientResponseError(await r.json())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const model: User = await r.json();
|
const model: User = await r.json();
|
||||||
currentUser.set(model);
|
currentUser.set(model);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const privateRoutes = [
|
const privateRoutes = [
|
||||||
"/profile",
|
"/settings",
|
||||||
"/lists",
|
"/lists",
|
||||||
"/trail/edit/new",
|
"/trail/edit/new",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import GPX from "$lib/models/gpx/gpx";
|
import GPX from "$lib/models/gpx/gpx";
|
||||||
import { Trail } from "$lib/models/trail";
|
import { Trail } from "$lib/models/trail";
|
||||||
import { Waypoint, waypointSchema } from "$lib/models/waypoint";
|
import { Waypoint } from "$lib/models/waypoint";
|
||||||
import { currentUser } from "$lib/stores/user_store";
|
import { currentUser } from "$lib/stores/user_store";
|
||||||
import GeoJsonToGpx from "$lib/vendor/geoJSONToGPX";
|
|
||||||
import { kml, tcx } from "$lib/vendor/toGeoJSON/toGeoJSON";
|
import { kml, tcx } from "$lib/vendor/toGeoJSON/toGeoJSON";
|
||||||
import cryptoRandomString from "crypto-random-string";
|
import cryptoRandomString from "crypto-random-string";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
import EasyFit from "easy-fit"
|
import EasyFit from "$lib/vendor/easy-fit/easy-fit"
|
||||||
import Track from "$lib/models/gpx/track";
|
import Track from "$lib/models/gpx/track";
|
||||||
import TrackSegment from "$lib/models/gpx/track-segment";
|
import TrackSegment from "$lib/models/gpx/track-segment";
|
||||||
import GPXWaypoint from "$lib/models/gpx/waypoint";
|
import GPXWaypoint from "$lib/models/gpx/waypoint";
|
||||||
|
import { browser } from "$app/environment";
|
||||||
|
import * as xmldom from 'xmldom'
|
||||||
|
import type { Feature, FeatureCollection, GeoJsonProperties, Position } from 'geojson';
|
||||||
|
|
||||||
|
|
||||||
export async function gpx2trail(gpxString: string) {
|
export async function gpx2trail(gpxString: string) {
|
||||||
const gpx = await GPX.parse(gpxString);
|
const gpx = await GPX.parse(gpxString);
|
||||||
@@ -60,7 +63,7 @@ export async function gpx2trail(gpxString: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function trail2gpx(trail: Trail) {
|
export async function trail2gpx(trail: Trail) {
|
||||||
if (!trail.expand.gpx_data) {
|
if (!trail.expand?.gpx_data) {
|
||||||
throw Error("Trail has no GPX data")
|
throw Error("Trail has no GPX data")
|
||||||
}
|
}
|
||||||
const gpx = await GPX.parse(trail.expand.gpx_data) as GPX;
|
const gpx = await GPX.parse(trail.expand.gpx_data) as GPX;
|
||||||
@@ -81,7 +84,7 @@ export async function trail2gpx(trail: Trail) {
|
|||||||
gpx.wpt = [];
|
gpx.wpt = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const wp of trail.expand.waypoints) {
|
for (const wp of trail.expand.waypoints ?? []) {
|
||||||
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({
|
gpx.wpt.push({
|
||||||
@@ -97,50 +100,33 @@ export async function trail2gpx(trail: Trail) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function fromKML(kmlData: string) {
|
export function fromKML(kmlData: string) {
|
||||||
const geojson = kml(new DOMParser().parseFromString(kmlData, "text/xml"))
|
const parser = browser ? new DOMParser() : new xmldom.DOMParser();
|
||||||
const options = {
|
const nodes = parser.parseFromString(kmlData, "text/xml")
|
||||||
metadata: {
|
const geojson = kml(nodes) as Feature | FeatureCollection
|
||||||
name: '',
|
|
||||||
author: {
|
|
||||||
name: '',
|
|
||||||
link: {
|
|
||||||
href: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const gpx = GeoJsonToGpx(geojson as any, options)
|
|
||||||
|
|
||||||
return new XMLSerializer().serializeToString(gpx)
|
const gpx = fromGeoJSON(geojson);
|
||||||
|
|
||||||
|
return gpx
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fromTCX(tcxData: string) {
|
export function fromTCX(tcxData: string) {
|
||||||
const geojson = tcx(new DOMParser().parseFromString(tcxData, "text/xml"))
|
const parser = browser ? new DOMParser() : new xmldom.DOMParser();
|
||||||
const options = {
|
const nodes = parser.parseFromString(tcxData, "text/xml")
|
||||||
metadata: {
|
const geojson = tcx(nodes) as Feature | FeatureCollection
|
||||||
name: '',
|
|
||||||
author: {
|
const gpx = fromGeoJSON(geojson);
|
||||||
name: '',
|
|
||||||
link: {
|
return gpx
|
||||||
href: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const gpx = GeoJsonToGpx(geojson as any, options)
|
|
||||||
return new XMLSerializer().serializeToString(gpx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fromFIT(fitData: ArrayBuffer) {
|
export async function fromFIT(fitData: ArrayBuffer) {
|
||||||
const easyFit = new EasyFit({
|
const easyFit = new EasyFit();
|
||||||
});
|
return new Promise<string>((resolve, reject) => easyFit.parse(fitData, function (error, data) {
|
||||||
return new Promise<string>((resolve, reject) => easyFit.parse(fitData, function (error: string, data: any) {
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
reject(error)
|
reject(error)
|
||||||
}
|
}
|
||||||
console.log(data);
|
|
||||||
|
|
||||||
const gpx = new GPX({
|
const gpx = new GPX({
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -152,7 +138,7 @@ export async function fromFIT(fitData: ArrayBuffer) {
|
|||||||
trk: [
|
trk: [
|
||||||
new Track({
|
new Track({
|
||||||
trkseg: new TrackSegment({
|
trkseg: new TrackSegment({
|
||||||
trkpt: data.records.flatMap((d => {
|
trkpt: data.records.flatMap(((d: { position_lat: any; position_long: any; timestamp: any; altitude: any; }) => {
|
||||||
return (d.position_lat && d.position_long) ? new GPXWaypoint({
|
return (d.position_lat && d.position_long) ? new GPXWaypoint({
|
||||||
$: { lat: d.position_lat, lon: d.position_long },
|
$: { lat: d.position_lat, lon: d.position_long },
|
||||||
time: d.timestamp,
|
time: d.timestamp,
|
||||||
@@ -164,8 +150,133 @@ export async function fromFIT(fitData: ArrayBuffer) {
|
|||||||
})
|
})
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
console.log(gpx);
|
|
||||||
|
|
||||||
resolve(gpx.toString());
|
resolve(gpx.toString());
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fromGeoJSON(geoJson: Feature | FeatureCollection) {
|
||||||
|
const gpx = new GPX({
|
||||||
|
metadata: {
|
||||||
|
name: "",
|
||||||
|
desc: "",
|
||||||
|
time: new Date(),
|
||||||
|
keywords: "wanderer"
|
||||||
|
},
|
||||||
|
trk: [],
|
||||||
|
wpt: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const type = geoJson.type;
|
||||||
|
|
||||||
|
function addSupportedPropertiesFromObject(el: any, supports: string[], properties: GeoJsonProperties) {
|
||||||
|
if (properties && typeof properties === 'object') {
|
||||||
|
supports.forEach(function (key: any) {
|
||||||
|
var value = properties[key];
|
||||||
|
if (value && typeof value === 'string' && supports.includes(key)) {
|
||||||
|
el[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function createTrk(properties: GeoJsonProperties) {
|
||||||
|
var trk = new Track({ trkseg: [] });
|
||||||
|
var supports = ['name', 'desc', 'src', 'type'];
|
||||||
|
addSupportedPropertiesFromObject(trk, supports, properties);
|
||||||
|
return trk;
|
||||||
|
}
|
||||||
|
function createPt(position: Position, properties?: GeoJsonProperties) {
|
||||||
|
const pt = new GPXWaypoint({ $: { lat: position[1], lon: position[0] }, ele: position[2], time: position[3] as any });
|
||||||
|
const supports = ['name', 'desc', 'src', 'type'];
|
||||||
|
if (properties) {
|
||||||
|
addSupportedPropertiesFromObject(pt, supports, properties);
|
||||||
|
}
|
||||||
|
return pt;
|
||||||
|
}
|
||||||
|
function createTrkSeg(coordinates: Position[]) {
|
||||||
|
var trkSeg = new TrackSegment({ trkpt: [] });
|
||||||
|
coordinates.forEach(function (point) {
|
||||||
|
trkSeg.trkpt!.push(createPt(point));
|
||||||
|
});
|
||||||
|
return trkSeg;
|
||||||
|
}
|
||||||
|
function interpretFeature(feature: Feature) {
|
||||||
|
const geometry = feature.geometry
|
||||||
|
if (!geometry) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const properties = feature.properties;
|
||||||
|
const type = geometry.type;
|
||||||
|
switch (type) {
|
||||||
|
case 'Polygon':
|
||||||
|
break;
|
||||||
|
case 'Point': {
|
||||||
|
gpx.wpt!.push(createPt(geometry.coordinates, properties));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'MultiPoint': {
|
||||||
|
geometry.coordinates.forEach(function (coord) {
|
||||||
|
gpx.wpt!.push(createPt(coord, properties));
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'LineString': {
|
||||||
|
var lineTrk = createTrk(properties);
|
||||||
|
var trkseg = createTrkSeg(geometry.coordinates);
|
||||||
|
lineTrk.trkseg!.push(trkseg);
|
||||||
|
gpx.trk!.push(lineTrk);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'MultiLineString': {
|
||||||
|
var trk_1 = createTrk(properties);
|
||||||
|
geometry.coordinates.forEach(function (pos) {
|
||||||
|
var trkseg = createTrkSeg(pos);
|
||||||
|
trk_1.trkseg!.push(trkseg);
|
||||||
|
});
|
||||||
|
gpx.trk!.push(trk_1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (type) {
|
||||||
|
case 'Feature': {
|
||||||
|
interpretFeature(geoJson);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'FeatureCollection': {
|
||||||
|
var features = geoJson.features;
|
||||||
|
features.forEach(function (feature) {
|
||||||
|
interpretFeature(feature);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return gpx.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFITFile(buffer: ArrayBuffer) {
|
||||||
|
var blob = new Uint8Array(buffer);
|
||||||
|
|
||||||
|
if (blob.length < 12) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var headerLength = blob[0];
|
||||||
|
if (headerLength !== 14 && headerLength !== 12) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileTypeString = '';
|
||||||
|
for (var i = 8; i < 12; i++) {
|
||||||
|
fileTypeString += String.fromCharCode(blob[i]);
|
||||||
|
}
|
||||||
|
if (fileTypeString !== '.FIT') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
266
web/src/lib/vendor/easy-fit/binary.ts
vendored
Normal file
266
web/src/lib/vendor/easy-fit/binary.ts
vendored
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
import { FIT } from "./fit";
|
||||||
|
import { getFitMessage, getFitMessageBaseType } from "./messages";
|
||||||
|
|
||||||
|
export { addEndian, readRecord, getArrayBuffer, calculateCRC };
|
||||||
|
|
||||||
|
|
||||||
|
function addEndian(littleEndian: boolean, bytes: any[]) {
|
||||||
|
var result = 0;
|
||||||
|
if (!littleEndian) bytes.reverse();
|
||||||
|
for (var i = 0; i < bytes.length; i++) {
|
||||||
|
result += bytes[i] << (i << 3) >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readData(blob: Uint8Array, fDef: { endianAbility: boolean; size: number; littleEndian: boolean; type: string; }, startIndex: number) {
|
||||||
|
if (fDef.endianAbility === true) {
|
||||||
|
var temp = [];
|
||||||
|
for (var i = 0; i < fDef.size; i++) {
|
||||||
|
temp.push(blob[startIndex + i]);
|
||||||
|
}
|
||||||
|
var uint32Rep = addEndian(fDef.littleEndian, temp);
|
||||||
|
|
||||||
|
if (fDef.type === 'sint32') {
|
||||||
|
return uint32Rep >> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return uint32Rep;
|
||||||
|
}
|
||||||
|
return blob[startIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatByType(data: number, type: string | number, scale: number, offset: number) {
|
||||||
|
switch (type) {
|
||||||
|
case 'date_time':
|
||||||
|
return new Date(data * 1000 + 631065600000);
|
||||||
|
case 'sint32':
|
||||||
|
case 'sint16':
|
||||||
|
return data * FIT.scConst;
|
||||||
|
case 'uint32':
|
||||||
|
case 'uint16':
|
||||||
|
return scale ? data / scale + offset : data;
|
||||||
|
default:
|
||||||
|
if (FIT.types[type]) {
|
||||||
|
return FIT.types[type][data];
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInvalidValue(data: number, type: string) {
|
||||||
|
switch (type) {
|
||||||
|
case 'enum':
|
||||||
|
return data === 0xFF;
|
||||||
|
case 'sint8':
|
||||||
|
return data === 0x7F;
|
||||||
|
case 'uint8':
|
||||||
|
return data === 0xFF;
|
||||||
|
case 'sint16':
|
||||||
|
return data === 0x7FFF;
|
||||||
|
case 'unit16':
|
||||||
|
return data === 0xFFFF;
|
||||||
|
case 'sint32':
|
||||||
|
return data === 0x7FFFFFFF;
|
||||||
|
case 'uint32':
|
||||||
|
return data === 0xFFFFFFFF;
|
||||||
|
case 'string':
|
||||||
|
return data === 0x00;
|
||||||
|
case 'float32':
|
||||||
|
return data === 0xFFFFFFFF;
|
||||||
|
case 'float64':
|
||||||
|
return data === 0xFFFFFFFFFFFFFFFF;
|
||||||
|
case 'uint8z':
|
||||||
|
return data === 0x00;
|
||||||
|
case 'uint16z':
|
||||||
|
return data === 0x0000;
|
||||||
|
case 'uint32z':
|
||||||
|
return data === 0x000000;
|
||||||
|
case 'byte':
|
||||||
|
return data === 0xFF;
|
||||||
|
case 'sint64':
|
||||||
|
return data === 0x7FFFFFFFFFFFFFFF;
|
||||||
|
case 'uint64':
|
||||||
|
return data === 0xFFFFFFFFFFFFFFFF;
|
||||||
|
case 'uint64z':
|
||||||
|
return data === 0x0000000000000000;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertTo(data: number, unitsList: string, speedUnit: string | number) {
|
||||||
|
var unitObj = FIT.options[unitsList][speedUnit];
|
||||||
|
return unitObj ? data * unitObj.multiplier + unitObj.offset : data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyOptions(data: number, field: any, options: { speedUnit?: string | number; lengthUnit?: string | number; temperatureUnit?: string | number; }) {
|
||||||
|
switch (field) {
|
||||||
|
case 'speed':
|
||||||
|
case 'enhanced_speed':
|
||||||
|
case 'vertical_speed':
|
||||||
|
case 'avg_speed':
|
||||||
|
case 'max_speed':
|
||||||
|
case 'speed_1s':
|
||||||
|
case 'ball_speed':
|
||||||
|
case 'enhanced_avg_speed':
|
||||||
|
case 'enhanced_max_speed':
|
||||||
|
case 'avg_pos_vertical_speed':
|
||||||
|
case 'max_pos_vertical_speed':
|
||||||
|
case 'avg_neg_vertical_speed':
|
||||||
|
case 'max_neg_vertical_speed':
|
||||||
|
return convertTo(data, 'speedUnits', options.speedUnit ?? 0);
|
||||||
|
case 'distance':
|
||||||
|
case 'total_distance':
|
||||||
|
case 'enhanced_avg_altitude':
|
||||||
|
case 'enhanced_min_altitude':
|
||||||
|
case 'enhanced_max_altitude':
|
||||||
|
case 'enhanced_altitude':
|
||||||
|
case 'height':
|
||||||
|
case 'odometer':
|
||||||
|
case 'avg_stroke_distance':
|
||||||
|
case 'min_altitude':
|
||||||
|
case 'avg_altitude':
|
||||||
|
case 'max_altitude':
|
||||||
|
case 'total_ascent':
|
||||||
|
case 'total_descent':
|
||||||
|
case 'altitude':
|
||||||
|
case 'cycle_length':
|
||||||
|
case 'auto_wheelsize':
|
||||||
|
case 'custom_wheelsize':
|
||||||
|
case 'gps_accuracy':
|
||||||
|
return convertTo(data, 'lengthUnits', options.lengthUnit ?? 0);
|
||||||
|
case 'temperature':
|
||||||
|
case 'avg_temperature':
|
||||||
|
case 'max_temperature':
|
||||||
|
return convertTo(data, 'temperatureUnits', options.temperatureUnit ?? 0);
|
||||||
|
default:
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readRecord(blob: Uint8Array, messageTypes: Record<number, any>, startIndex: number, options: { elapsedRecordField?: any; speedUnit?: string | number; lengthUnit?: string | number; temperatureUnit?: string | number; }, startDate: number | undefined) {
|
||||||
|
var recordHeader = blob[startIndex];
|
||||||
|
var localMessageType = recordHeader & 15;
|
||||||
|
|
||||||
|
if ((recordHeader & 64) === 64) {
|
||||||
|
// is definition message
|
||||||
|
// startIndex + 1 is reserved
|
||||||
|
|
||||||
|
var lEnd = blob[startIndex + 2] === 0;
|
||||||
|
var mTypeDef: { littleEndian: boolean, globalMessageNumber: number, numberOfFields: number, fieldDefs: any[] } = {
|
||||||
|
littleEndian: lEnd,
|
||||||
|
globalMessageNumber: addEndian(lEnd, [blob[startIndex + 3], blob[startIndex + 4]]),
|
||||||
|
numberOfFields: blob[startIndex + 5],
|
||||||
|
fieldDefs: []
|
||||||
|
};
|
||||||
|
|
||||||
|
var _message = getFitMessage(mTypeDef.globalMessageNumber);
|
||||||
|
|
||||||
|
for (var i = 0; i < mTypeDef.numberOfFields; i++) {
|
||||||
|
var fDefIndex = startIndex + 6 + i * 3;
|
||||||
|
var baseType = blob[fDefIndex + 2];
|
||||||
|
|
||||||
|
var _message$getAttribute = _message.getAttributes(blob[fDefIndex]),
|
||||||
|
field = _message$getAttribute.field,
|
||||||
|
type = _message$getAttribute.type;
|
||||||
|
|
||||||
|
var fDef = {
|
||||||
|
type: type,
|
||||||
|
fDefNo: blob[fDefIndex],
|
||||||
|
size: blob[fDefIndex + 1],
|
||||||
|
endianAbility: (baseType & 128) === 128,
|
||||||
|
littleEndian: lEnd,
|
||||||
|
baseTypeNo: baseType & 15,
|
||||||
|
name: field,
|
||||||
|
dataType: getFitMessageBaseType(baseType & 15)
|
||||||
|
};
|
||||||
|
|
||||||
|
mTypeDef.fieldDefs.push(fDef);
|
||||||
|
}
|
||||||
|
messageTypes[localMessageType] = mTypeDef;
|
||||||
|
|
||||||
|
return {
|
||||||
|
messageType: 'fieldDescription',
|
||||||
|
nextIndex: startIndex + 6 + mTypeDef.numberOfFields * 3
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var messageType: { littleEndian: boolean, globalMessageNumber: number, numberOfFields: number, fieldDefs: any[] } | undefined = void 0;
|
||||||
|
|
||||||
|
if (messageTypes[localMessageType]) {
|
||||||
|
messageType = messageTypes[localMessageType];
|
||||||
|
} else {
|
||||||
|
messageType = messageTypes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: handle compressed header ((recordHeader & 128) == 128)
|
||||||
|
|
||||||
|
// uncompressed header
|
||||||
|
var messageSize = 0;
|
||||||
|
var readDataFromIndex = startIndex + 1;
|
||||||
|
var fields: any = {};
|
||||||
|
var message = getFitMessage(messageType?.globalMessageNumber);
|
||||||
|
|
||||||
|
for (var _i = 0; _i < (messageType?.fieldDefs.length ?? 0); _i++) {
|
||||||
|
var _fDef = messageType?.fieldDefs[_i];
|
||||||
|
var data = readData(blob, _fDef, readDataFromIndex);
|
||||||
|
|
||||||
|
if (!isInvalidValue(data, _fDef.type)) {
|
||||||
|
var _message$getAttribute2 = message.getAttributes(_fDef.fDefNo),
|
||||||
|
field = _message$getAttribute2.field,
|
||||||
|
type = _message$getAttribute2.type,
|
||||||
|
scale = _message$getAttribute2.scale,
|
||||||
|
offset = _message$getAttribute2.offset;
|
||||||
|
|
||||||
|
if (field !== 'unknown' && field !== '' && field !== undefined) {
|
||||||
|
fields[field] = applyOptions(formatByType(data, type, scale, offset), field, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.name === 'record' && options.elapsedRecordField) {
|
||||||
|
fields.elapsed_time = (fields.timestamp - (startDate ?? 0)) / 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readDataFromIndex += _fDef.size;
|
||||||
|
messageSize += _fDef.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = {
|
||||||
|
messageType: message.name,
|
||||||
|
nextIndex: startIndex + messageSize + 1,
|
||||||
|
message: fields
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getArrayBuffer(buffer: ArrayBuffer | number[]) {
|
||||||
|
if (buffer instanceof ArrayBuffer) {
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
var ab = new ArrayBuffer(buffer.length);
|
||||||
|
var view = new Uint8Array(ab);
|
||||||
|
for (var i = 0; i < buffer.length; ++i) {
|
||||||
|
view[i] = buffer[i];
|
||||||
|
}
|
||||||
|
return ab;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateCRC(blob: Uint8Array, start: number, end: number) {
|
||||||
|
var crcTable = [0x0000, 0xCC01, 0xD801, 0x1400, 0xF001, 0x3C00, 0x2800, 0xE401, 0xA001, 0x6C00, 0x7800, 0xB401, 0x5000, 0x9C01, 0x8801, 0x4400];
|
||||||
|
|
||||||
|
var crc = 0;
|
||||||
|
for (var i = start; i < end; i++) {
|
||||||
|
var byte = blob[i];
|
||||||
|
var tmp = crcTable[crc & 0xF];
|
||||||
|
crc = crc >> 4 & 0x0FFF;
|
||||||
|
crc = crc ^ tmp ^ crcTable[byte & 0xF];
|
||||||
|
tmp = crcTable[crc & 0xF];
|
||||||
|
crc = crc >> 4 & 0x0FFF;
|
||||||
|
crc = crc ^ tmp ^ crcTable[byte >> 4 & 0xF];
|
||||||
|
}
|
||||||
|
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
156
web/src/lib/vendor/easy-fit/easy-fit.ts
vendored
Normal file
156
web/src/lib/vendor/easy-fit/easy-fit.ts
vendored
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import { calculateCRC, getArrayBuffer, readRecord } from "./binary";
|
||||||
|
|
||||||
|
type Options = {
|
||||||
|
force: boolean,
|
||||||
|
speedUnit: 'm/s' | 'km/h' | 'mph',
|
||||||
|
lengthUnit: 'm' | 'km' | 'mi',
|
||||||
|
temperatureUnit: 'celsius' | 'fahrenheit',
|
||||||
|
elapsedRecordField: boolean,
|
||||||
|
mode: 'cascade' | 'list' | 'both'
|
||||||
|
}
|
||||||
|
|
||||||
|
class EasyFit {
|
||||||
|
options: Options;
|
||||||
|
constructor(
|
||||||
|
options?: Options
|
||||||
|
) {
|
||||||
|
this.options = options || {
|
||||||
|
force: true,
|
||||||
|
speedUnit: 'm/s',
|
||||||
|
lengthUnit: 'm',
|
||||||
|
temperatureUnit: 'celsius',
|
||||||
|
elapsedRecordField: false,
|
||||||
|
mode: 'list'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
parse(content: ArrayBuffer, callback: { (error: string, data: any): void; (arg0: string | null, arg1: {}): void; }) {
|
||||||
|
var blob = new Uint8Array(getArrayBuffer(content));
|
||||||
|
|
||||||
|
if (blob.length < 12) {
|
||||||
|
callback('File to small to be a FIT file', {});
|
||||||
|
if (!this.options.force) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var headerLength = blob[0];
|
||||||
|
if (headerLength !== 14 && headerLength !== 12) {
|
||||||
|
callback('Incorrect header size', {});
|
||||||
|
if (!this.options.force) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileTypeString = '';
|
||||||
|
for (var i = 8; i < 12; i++) {
|
||||||
|
fileTypeString += String.fromCharCode(blob[i]);
|
||||||
|
}
|
||||||
|
if (fileTypeString !== '.FIT') {
|
||||||
|
callback('Missing \'.FIT\' in header', {});
|
||||||
|
if (!this.options.force) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headerLength === 14) {
|
||||||
|
var crcHeader = blob[12] + (blob[13] << 8);
|
||||||
|
var crcHeaderCalc = calculateCRC(blob, 0, 12);
|
||||||
|
if (crcHeader !== crcHeaderCalc) {
|
||||||
|
// callback('Header CRC mismatch', {});
|
||||||
|
// TODO: fix Header CRC check
|
||||||
|
if (!this.options.force) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var dataLength = blob[4] + (blob[5] << 8) + (blob[6] << 16) + (blob[7] << 24);
|
||||||
|
var crcStart = dataLength + headerLength;
|
||||||
|
var crcFile = blob[crcStart] + (blob[crcStart + 1] << 8);
|
||||||
|
var crcFileCalc = calculateCRC(blob, headerLength === 12 ? 0 : headerLength, crcStart);
|
||||||
|
|
||||||
|
if (crcFile !== crcFileCalc) {
|
||||||
|
// callback('File CRC mismatch', {});
|
||||||
|
// TODO: fix File CRC check
|
||||||
|
if (!this.options.force) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var fitObj = {};
|
||||||
|
var sessions = [];
|
||||||
|
var laps = [];
|
||||||
|
var records = [];
|
||||||
|
var events = [];
|
||||||
|
|
||||||
|
var tempLaps = [];
|
||||||
|
var tempRecords = [];
|
||||||
|
|
||||||
|
var loopIndex = headerLength;
|
||||||
|
var messageTypes: Record<number, any> = [];
|
||||||
|
|
||||||
|
var isModeCascade = this.options.mode === 'cascade';
|
||||||
|
var isCascadeNeeded = isModeCascade || this.options.mode === 'both';
|
||||||
|
|
||||||
|
var startDate = void 0;
|
||||||
|
|
||||||
|
while (loopIndex < crcStart) {
|
||||||
|
var _readRecord = readRecord(blob, messageTypes, loopIndex, this.options, startDate),
|
||||||
|
nextIndex = _readRecord.nextIndex,
|
||||||
|
messageType = _readRecord.messageType,
|
||||||
|
message = _readRecord.message;
|
||||||
|
|
||||||
|
loopIndex = nextIndex;
|
||||||
|
switch (messageType) {
|
||||||
|
case 'lap':
|
||||||
|
if (isCascadeNeeded) {
|
||||||
|
message.records = tempRecords;
|
||||||
|
tempRecords = [];
|
||||||
|
tempLaps.push(message);
|
||||||
|
}
|
||||||
|
laps.push(message);
|
||||||
|
break;
|
||||||
|
case 'session':
|
||||||
|
if (isCascadeNeeded) {
|
||||||
|
message.laps = tempLaps;
|
||||||
|
tempLaps = [];
|
||||||
|
}
|
||||||
|
sessions.push(message);
|
||||||
|
break;
|
||||||
|
case 'event':
|
||||||
|
events.push(message);
|
||||||
|
break;
|
||||||
|
case 'record':
|
||||||
|
if (!startDate) {
|
||||||
|
startDate = message.timestamp;
|
||||||
|
message.elapsed_time = 0;
|
||||||
|
}
|
||||||
|
records.push(message);
|
||||||
|
if (isCascadeNeeded) {
|
||||||
|
tempRecords.push(message);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (messageType !== '') {
|
||||||
|
fitObj[messageType] = message;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCascadeNeeded) {
|
||||||
|
fitObj.activity.sessions = sessions;
|
||||||
|
fitObj.activity.events = events;
|
||||||
|
}
|
||||||
|
if (!isModeCascade) {
|
||||||
|
fitObj.sessions = sessions;
|
||||||
|
fitObj.laps = laps;
|
||||||
|
fitObj.records = records;
|
||||||
|
fitObj.events = events;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(null, fitObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EasyFit
|
||||||
2437
web/src/lib/vendor/easy-fit/fit.ts
vendored
Normal file
2437
web/src/lib/vendor/easy-fit/fit.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
16
web/src/lib/vendor/easy-fit/messages.ts
vendored
Normal file
16
web/src/lib/vendor/easy-fit/messages.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export { getFitMessage, getFitMessageBaseType }
|
||||||
|
|
||||||
|
import { getFieldObject, getMessageName } from "./fit"
|
||||||
|
|
||||||
|
function getFitMessage(messageNum: any) {
|
||||||
|
return {
|
||||||
|
name: getMessageName(messageNum),
|
||||||
|
getAttributes: function getAttributes(fieldNum: any) {
|
||||||
|
return getFieldObject(fieldNum, messageNum);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFitMessageBaseType(foo: any) {
|
||||||
|
return foo;
|
||||||
|
}
|
||||||
39
web/src/lib/vendor/geoJSONToGPX/index.d.ts
vendored
39
web/src/lib/vendor/geoJSONToGPX/index.d.ts
vendored
@@ -1,39 +0,0 @@
|
|||||||
import { Feature, FeatureCollection } from 'geojson';
|
|
||||||
declare interface Bounds {
|
|
||||||
minlat: string;
|
|
||||||
minlon: string;
|
|
||||||
maxlat: string;
|
|
||||||
maxlon: string;
|
|
||||||
}
|
|
||||||
declare interface Copyright {
|
|
||||||
author?: string;
|
|
||||||
year?: string;
|
|
||||||
license?: string;
|
|
||||||
}
|
|
||||||
declare interface Link {
|
|
||||||
href: string;
|
|
||||||
text?: string;
|
|
||||||
type?: string;
|
|
||||||
}
|
|
||||||
declare interface Person {
|
|
||||||
name?: string;
|
|
||||||
email?: string;
|
|
||||||
link?: Link;
|
|
||||||
}
|
|
||||||
declare interface MetaData {
|
|
||||||
name?: string;
|
|
||||||
desc?: string;
|
|
||||||
author?: Person;
|
|
||||||
copyright?: Copyright;
|
|
||||||
link?: Link;
|
|
||||||
time?: string;
|
|
||||||
keywords?: string;
|
|
||||||
bounds?: Bounds;
|
|
||||||
}
|
|
||||||
export declare interface Options {
|
|
||||||
creator?: string;
|
|
||||||
version?: string;
|
|
||||||
metadata?: MetaData;
|
|
||||||
}
|
|
||||||
export default function GeoJsonToGpx(geoJson: Feature | FeatureCollection, options?: Options, implementation?: DOMImplementation): XMLDocument;
|
|
||||||
export {};
|
|
||||||
155
web/src/lib/vendor/geoJSONToGPX/index.js
vendored
155
web/src/lib/vendor/geoJSONToGPX/index.js
vendored
@@ -1,155 +0,0 @@
|
|||||||
export default function GeoJsonToGpx(geoJson, options, implementation) {
|
|
||||||
if (implementation === void 0) { implementation = document.implementation; }
|
|
||||||
var doc = implementation.createDocument('http://www.topografix.com/GPX/1/1', '');
|
|
||||||
var instruct = doc.createProcessingInstruction('xml', 'version="1.0" encoding="UTF-8"');
|
|
||||||
doc.appendChild(instruct);
|
|
||||||
var defaultPackageName = '@dwayneparton/geojson-to-gpx';
|
|
||||||
var creator = (options === null || options === void 0 ? void 0 : options.creator) || defaultPackageName;
|
|
||||||
var createElementWithNS = function (tagName) {
|
|
||||||
return doc.createElementNS('http://www.topografix.com/GPX/1/1', tagName);
|
|
||||||
};
|
|
||||||
var gpx = createElementWithNS('gpx');
|
|
||||||
gpx.setAttribute('version', '1.1');
|
|
||||||
gpx.setAttribute('creator', creator);
|
|
||||||
var wpts = [];
|
|
||||||
var trks = [];
|
|
||||||
function createTagInParentElement(parent, tagName, content) {
|
|
||||||
if (content === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var element = createElementWithNS(tagName);
|
|
||||||
var contentEl = doc.createTextNode(String(content));
|
|
||||||
element.appendChild(contentEl);
|
|
||||||
parent.appendChild(element);
|
|
||||||
}
|
|
||||||
function addSupportedPropertiesFromObject(el, supports, properties) {
|
|
||||||
if (properties && typeof properties === 'object') {
|
|
||||||
supports.forEach(function (key) {
|
|
||||||
var value = properties[key];
|
|
||||||
if (value && typeof value === 'string' && supports.includes(key)) {
|
|
||||||
createTagInParentElement(el, key, value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function createLinkInParentElement(parent, props) {
|
|
||||||
var href = props.href;
|
|
||||||
if (!href) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var el = createElementWithNS('link');
|
|
||||||
el.setAttribute('href', href);
|
|
||||||
addSupportedPropertiesFromObject(el, ['text', 'type'], props);
|
|
||||||
parent.appendChild(el);
|
|
||||||
}
|
|
||||||
function createTrk(properties) {
|
|
||||||
var el = createElementWithNS('trk');
|
|
||||||
var supports = ['name', 'desc', 'src', 'type'];
|
|
||||||
addSupportedPropertiesFromObject(el, supports, properties);
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
function createPt(type, position, properties) {
|
|
||||||
var lon = position[0], lat = position[1], ele = position[2], time = position[3];
|
|
||||||
var el = createElementWithNS(type);
|
|
||||||
el.setAttribute('lat', String(lat));
|
|
||||||
el.setAttribute('lon', String(lon));
|
|
||||||
createTagInParentElement(el, 'ele', ele);
|
|
||||||
createTagInParentElement(el, 'time', time);
|
|
||||||
var supports = ['name', 'desc', 'src', 'type'];
|
|
||||||
addSupportedPropertiesFromObject(el, supports, properties);
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
function createTrkSeg(coordinates) {
|
|
||||||
var el = createElementWithNS('trkseg');
|
|
||||||
coordinates.forEach(function (point) {
|
|
||||||
el.appendChild(createPt('trkpt', point));
|
|
||||||
});
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
function interpretFeature(feature) {
|
|
||||||
var geometry = feature.geometry, properties = feature.properties;
|
|
||||||
var type = geometry.type;
|
|
||||||
switch (type) {
|
|
||||||
case 'Polygon':
|
|
||||||
break;
|
|
||||||
case 'Point': {
|
|
||||||
wpts.push(createPt('wpt', geometry.coordinates, properties));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'MultiPoint': {
|
|
||||||
geometry.coordinates.forEach(function (coord) {
|
|
||||||
wpts.push(createPt('wpt', coord, properties));
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'LineString': {
|
|
||||||
var lineTrk = createTrk(properties);
|
|
||||||
var trkseg = createTrkSeg(geometry.coordinates);
|
|
||||||
lineTrk.appendChild(trkseg);
|
|
||||||
trks.push(lineTrk);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'MultiLineString': {
|
|
||||||
var trk_1 = createTrk(properties);
|
|
||||||
geometry.coordinates.forEach(function (pos) {
|
|
||||||
var trkseg = createTrkSeg(pos);
|
|
||||||
trk_1.appendChild(trkseg);
|
|
||||||
});
|
|
||||||
trks.push(trk_1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (options && typeof options.metadata === 'object') {
|
|
||||||
var meta = options.metadata;
|
|
||||||
var metadata = createElementWithNS('metadata');
|
|
||||||
createTagInParentElement(metadata, 'name', meta.name);
|
|
||||||
createTagInParentElement(metadata, 'desc', meta.desc);
|
|
||||||
if (meta.author && typeof meta.author === 'object') {
|
|
||||||
var author = createElementWithNS('author');
|
|
||||||
createTagInParentElement(author, 'name', meta.author.name);
|
|
||||||
createTagInParentElement(author, 'email', meta.author.email);
|
|
||||||
if (meta.author.link && typeof meta.author.link === 'object') {
|
|
||||||
createLinkInParentElement(author, meta.author.link);
|
|
||||||
}
|
|
||||||
metadata.appendChild(author);
|
|
||||||
}
|
|
||||||
if (typeof meta.copyright === 'object') {
|
|
||||||
var copyright = createElementWithNS('copyright');
|
|
||||||
if (meta.copyright.author) {
|
|
||||||
copyright.setAttribute('author', meta.copyright.author);
|
|
||||||
}
|
|
||||||
createTagInParentElement(copyright, 'year', meta.copyright.year);
|
|
||||||
createTagInParentElement(copyright, 'license', meta.copyright.license);
|
|
||||||
metadata.appendChild(copyright);
|
|
||||||
}
|
|
||||||
if (typeof meta.link === 'object') {
|
|
||||||
createLinkInParentElement(metadata, meta.link);
|
|
||||||
}
|
|
||||||
createTagInParentElement(metadata, 'time', meta.time);
|
|
||||||
createTagInParentElement(metadata, 'keywords', meta.keywords);
|
|
||||||
gpx.appendChild(metadata);
|
|
||||||
}
|
|
||||||
var type = geoJson.type;
|
|
||||||
switch (type) {
|
|
||||||
case 'Feature': {
|
|
||||||
interpretFeature(geoJson);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'FeatureCollection': {
|
|
||||||
var features = geoJson.features;
|
|
||||||
features.forEach(function (feature) {
|
|
||||||
interpretFeature(feature);
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
wpts.forEach(function (wpt) { return gpx.appendChild(wpt); });
|
|
||||||
trks.forEach(function (trk) { return gpx.appendChild(trk); });
|
|
||||||
doc.appendChild(gpx);
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
@@ -134,7 +134,9 @@ export const createForm = (config) => {
|
|||||||
return clearErrorsAndSubmit(values);
|
return clearErrorsAndSubmit(values);
|
||||||
} else {
|
} else {
|
||||||
errors.set(error);
|
errors.set(error);
|
||||||
|
if(onError) {
|
||||||
onError(error)
|
onError(error)
|
||||||
|
}
|
||||||
isSubmitting.set(false);
|
isSubmitting.set(false);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -156,7 +158,9 @@ export const createForm = (config) => {
|
|||||||
yupErrors.inner.map((error) =>
|
yupErrors.inner.map((error) =>
|
||||||
util.set(updatedErrors, error.path, error.message),
|
util.set(updatedErrors, error.path, error.message),
|
||||||
);
|
);
|
||||||
|
if(onError) {
|
||||||
onError(updatedErrors)
|
onError(updatedErrors)
|
||||||
|
}
|
||||||
errors.set(updatedErrors);
|
errors.set(updatedErrors);
|
||||||
}
|
}
|
||||||
isSubmitting.set(false);
|
isSubmitting.set(false);
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ import { trails_index } from "$lib/stores/trail_store";
|
|||||||
import type { ServerLoad } from "@sveltejs/kit";
|
import type { ServerLoad } from "@sveltejs/kit";
|
||||||
|
|
||||||
export const load: ServerLoad = async ({ params, locals, fetch }) => {
|
export const load: ServerLoad = async ({ params, locals, fetch }) => {
|
||||||
await trails_index({perPage: 20, random: true, f: fetch})
|
await trails_index(20, true, fetch)
|
||||||
await categories_index(fetch)
|
await categories_index(fetch)
|
||||||
};
|
};
|
||||||
@@ -10,8 +10,17 @@ export async function GET(event: RequestEvent) {
|
|||||||
const filter = event.url.searchParams.get("filter") ?? "";
|
const filter = event.url.searchParams.get("filter") ?? "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const r = await pb.collection('trails')
|
let r;
|
||||||
|
if (parseInt(perPage) < 0) {
|
||||||
|
r = {
|
||||||
|
items: await pb.collection('trails')
|
||||||
|
.getFullList<Trail>({ expand: expand, sort: sort, filter: filter })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
r = await pb.collection('trails')
|
||||||
.getList<Trail>(parseInt(page), parseInt(perPage), { expand: expand ?? "", sort: sort ?? "", filter: filter ?? "" })
|
.getList<Trail>(parseInt(page), parseInt(perPage), { expand: expand ?? "", sort: sort ?? "", filter: filter ?? "" })
|
||||||
|
}
|
||||||
|
|
||||||
return json(r)
|
return json(r)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
throw error(e.status, e);
|
throw error(e.status, e);
|
||||||
|
|||||||
@@ -1,25 +1,48 @@
|
|||||||
import type { Trail } from "$lib/models/trail";
|
import type { Trail } from "$lib/models/trail";
|
||||||
import { trails_create } from "$lib/stores/trail_store";
|
import { trails_create } from "$lib/stores/trail_store";
|
||||||
import { gpx2trail } from "$lib/util/gpx_util";
|
import { fromFIT, fromKML, fromTCX, gpx2trail, isFITFile } from "$lib/util/gpx_util";
|
||||||
import { error, json, type RequestEvent } from "@sveltejs/kit";
|
import { error, json, type RequestEvent } from "@sveltejs/kit";
|
||||||
import { ClientResponseError } from "pocketbase";
|
import { ClientResponseError } from "pocketbase";
|
||||||
|
|
||||||
export async function PUT(event: RequestEvent) {
|
export async function PUT(event: RequestEvent) {
|
||||||
try {
|
try {
|
||||||
const data = await event.request.blob();
|
const data = await event.request.blob();
|
||||||
const gpxString = await data.text();
|
const fileBuffer = await data.arrayBuffer();
|
||||||
if (!gpxString.length) {
|
const fileContent = await data.text();
|
||||||
|
let gpxData = ""
|
||||||
|
let gpxFile: Blob;
|
||||||
|
if (isFITFile(fileBuffer)) {
|
||||||
|
gpxData = await fromFIT(fileBuffer);
|
||||||
|
gpxFile = new Blob([gpxData], {
|
||||||
|
type: "application/gpx+xml",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (fileContent.includes("http://www.opengis.net/kml")) {
|
||||||
|
gpxData = fromKML(fileContent);
|
||||||
|
gpxFile = new Blob([gpxData], {
|
||||||
|
type: "application/gpx+xml",
|
||||||
|
});
|
||||||
|
} else if (fileContent.includes("TrainingCenterDatabase")) {
|
||||||
|
gpxData = fromTCX(fileContent);
|
||||||
|
gpxFile = new Blob([gpxData], {
|
||||||
|
type: "application/gpx+xml",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
gpxData = fileContent;
|
||||||
|
gpxFile = data
|
||||||
|
}
|
||||||
|
if (!gpxData.length) {
|
||||||
throw new ClientResponseError({ status: 400, response: { message: "Empty file" } })
|
throw new ClientResponseError({ status: 400, response: { message: "Empty file" } })
|
||||||
}
|
}
|
||||||
let trail: Trail;
|
let trail: Trail;
|
||||||
try {
|
try {
|
||||||
trail = (await gpx2trail(gpxString)).trail;
|
trail = (await gpx2trail(gpxData)).trail;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
throw new ClientResponseError({ status: 400, response: { message: "Invalid GPX file" } })
|
throw new ClientResponseError({ status: 400, response: { message: "Invalid file" } })
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
trail = await trails_create(trail, [], data, event.fetch);
|
trail = await trails_create(trail, [], gpxFile, event.fetch);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
|
|
||||||
@@ -28,6 +51,8 @@ export async function PUT(event: RequestEvent) {
|
|||||||
return json(trail);
|
return json(trail);
|
||||||
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
console.log(e);
|
||||||
|
|
||||||
throw error(e.status, e)
|
throw error(e.status, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,8 +15,17 @@ export async function GET(event: RequestEvent) {
|
|||||||
export async function POST(event: RequestEvent) {
|
export async function POST(event: RequestEvent) {
|
||||||
const data = await event.request.json()
|
const data = await event.request.json()
|
||||||
try {
|
try {
|
||||||
|
if (data.email != pb.authStore.model!.email) {
|
||||||
|
const r = await pb.collection('users').requestEmailChange(data.email);
|
||||||
|
pb.authStore.model!.email = data.email;
|
||||||
|
}
|
||||||
const r = await pb.collection('users').update<User>(event.params.id as string, data)
|
const r = await pb.collection('users').update<User>(event.params.id as string, data)
|
||||||
|
if (data.password) {
|
||||||
|
const r = await pb.collection('users').authWithPassword(data.email ?? data.username!, data.password);
|
||||||
|
return json(r.record)
|
||||||
|
} else {
|
||||||
return json(r);
|
return json(r);
|
||||||
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
throw error(e.status, e)
|
throw error(e.status, e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
<main class="grid grid-cols-1 md:grid-cols-[458px_1fr] gap-x-1 gap-y-4">
|
<main class="grid grid-cols-1 md:grid-cols-[458px_1fr] gap-x-1 gap-y-4">
|
||||||
<TrailInfoPanel trail={$trail} {markers}></TrailInfoPanel>
|
<TrailInfoPanel trail={$trail} {markers}></TrailInfoPanel>
|
||||||
<div id="trail-details" class=" sticky top-0">
|
<div id="trail-details" class=" sticky top-0 min-h-[600px]">
|
||||||
<MapWithElevation trail={$trail} bind:markers></MapWithElevation>
|
<MapWithElevation trail={$trail} bind:markers></MapWithElevation>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,229 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { goto } from "$app/navigation";
|
|
||||||
import { page } from "$app/stores";
|
|
||||||
import RadioGroup, {
|
|
||||||
type RadioItem,
|
|
||||||
} from "$lib/components/base/radio_group.svelte";
|
|
||||||
import Search, {
|
|
||||||
type SearchItem,
|
|
||||||
} from "$lib/components/base/search.svelte";
|
|
||||||
import Select, {
|
|
||||||
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,
|
|
||||||
users_delete,
|
|
||||||
users_update,
|
|
||||||
} from "$lib/stores/user_store";
|
|
||||||
import { country_codes } from "$lib/util/country_code_util";
|
|
||||||
import { getFileURL } from "$lib/util/file_util";
|
|
||||||
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" },
|
|
||||||
{ text: $_("english"), value: "en" },
|
|
||||||
{ text: $_("french"), value: "fr" },
|
|
||||||
{ text: $_("hungarian"), value: "hu" },
|
|
||||||
{ text: $_("italian"), value: "it" },
|
|
||||||
{ text: $_("dutch"), value: "nl" },
|
|
||||||
{ text: $_("polish"), value: "pl" },
|
|
||||||
{ 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 = "";
|
|
||||||
|
|
||||||
let openConfirmModal: () => void;
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
citySearchQuery = settings?.location?.name ?? "";
|
|
||||||
selectedLanguage = settings?.language || "en";
|
|
||||||
selectedMapFocus = settings?.mapFocus ?? "trails";
|
|
||||||
});
|
|
||||||
|
|
||||||
async function searchCities(q: string) {
|
|
||||||
const r = await fetch("/api/v1/search/cities500", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ q: q, options: { limit: 5 } }),
|
|
||||||
});
|
|
||||||
const result = await r.json();
|
|
||||||
searchDropdownItems = result.hits.map((h: Record<string, any>) => ({
|
|
||||||
text: h.name,
|
|
||||||
description: `${h.division ? `${h.division} | ` : ""}${
|
|
||||||
country_codes[h["country code"] as keyof typeof country_codes]
|
|
||||||
}`,
|
|
||||||
value: h,
|
|
||||||
icon: "city",
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSearchClick(item: SearchItem) {
|
|
||||||
citySearchQuery = item.text;
|
|
||||||
await settings_update({
|
|
||||||
id: settings?.id!,
|
|
||||||
location: {
|
|
||||||
name: item.value.name,
|
|
||||||
lat: item.value.lat,
|
|
||||||
lon: item.value.lon,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleAvatarSelection() {
|
|
||||||
const files = (
|
|
||||||
document.getElementById("avatarInput") as HTMLInputElement
|
|
||||||
).files;
|
|
||||||
|
|
||||||
if (!files || files.length == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await users_update($currentUser!, files[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleUnitSelection(e: RadioItem) {
|
|
||||||
await settings_update({
|
|
||||||
id: settings!.id,
|
|
||||||
unit: e.value as "imperial" | "metric",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleLanguageSelection(
|
|
||||||
value: "en" | "de" | "fr" | "hu" | "nl" | "pl" | "pt",
|
|
||||||
) {
|
|
||||||
locale.set(value);
|
|
||||||
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();
|
|
||||||
goto("/");
|
|
||||||
}
|
|
||||||
|
|
||||||
function openFileBrowser() {
|
|
||||||
document.getElementById("avatarInput")!.click();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:head>
|
|
||||||
<title>{$_("profile")} | wanderer</title>
|
|
||||||
</svelte:head>
|
|
||||||
<main
|
|
||||||
class="grid grid-cols-1 md:grid-cols-[356px_1fr] max-w-4xl mx-4 md:mx-auto gap-x-8 items-start"
|
|
||||||
>
|
|
||||||
{#if $currentUser}
|
|
||||||
<div
|
|
||||||
class="flex flex-col items-center rounded-xl border border-input-border p-4"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="rounded-full w-32 aspect-square overflow-hidden relative group"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={getFileURL($currentUser, $currentUser.avatar) ||
|
|
||||||
`https://api.dicebear.com/7.x/initials/svg?seed=${$currentUser.username}&backgroundType=gradientLinear`}
|
|
||||||
alt="avatar"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="absolute top-0 w-32 aspect-square opacity-0 group-hover:opacity-100 flex justify-center items-center bg-black/50 focus:bg-black/60 text-white cursor-pointer transition-opacity"
|
|
||||||
on:click={openFileBrowser}
|
|
||||||
>
|
|
||||||
<i class="fa fa-pen"></i>
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
name="avatar"
|
|
||||||
id="avatarInput"
|
|
||||||
accept="image/*"
|
|
||||||
style="display: none;"
|
|
||||||
on:change={handleAvatarSelection}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="text-2xl mt-4 font-semibold">{$currentUser.username}</h3>
|
|
||||||
<h5 class="font-medium">{$currentUser.email}</h5>
|
|
||||||
</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">{$_("language")}</h5>
|
|
||||||
<Select
|
|
||||||
items={languages}
|
|
||||||
bind:value={selectedLanguage}
|
|
||||||
on:change={(e) => handleLanguageSelection(e.detail)}
|
|
||||||
></Select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h5 class="font-medium mb-1">{$_("units")}</h5>
|
|
||||||
<RadioGroup
|
|
||||||
name="unit"
|
|
||||||
items={units}
|
|
||||||
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>
|
|
||||||
<button class="btn-danger" on:click={openConfirmModal}
|
|
||||||
>{$_("delete-account")}</button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</main>
|
|
||||||
<ConfirmModal
|
|
||||||
text={$_("account-delete-confirm")}
|
|
||||||
bind:openModal={openConfirmModal}
|
|
||||||
on:confirm={deleteAccount}
|
|
||||||
></ConfirmModal>
|
|
||||||
@@ -4,9 +4,10 @@
|
|||||||
import TextField from "$lib/components/base/text_field.svelte";
|
import TextField from "$lib/components/base/text_field.svelte";
|
||||||
import LogoTextTwoLineDark from "$lib/components/logo/logo_text_two_line_dark.svelte";
|
import LogoTextTwoLineDark from "$lib/components/logo/logo_text_two_line_dark.svelte";
|
||||||
import LogoTextTwoLineLight from "$lib/components/logo/logo_text_two_line_light.svelte";
|
import LogoTextTwoLineLight from "$lib/components/logo/logo_text_two_line_light.svelte";
|
||||||
|
import type { User } from "$lib/models/user";
|
||||||
import { theme } from "$lib/stores/theme_store";
|
import { theme } from "$lib/stores/theme_store";
|
||||||
import { show_toast } from "$lib/stores/toast_store";
|
import { show_toast } from "$lib/stores/toast_store";
|
||||||
import { login, users_create, type User } from "$lib/stores/user_store";
|
import { login, users_create } from "$lib/stores/user_store";
|
||||||
import { createForm } from "$lib/vendor/svelte-form-lib";
|
import { createForm } from "$lib/vendor/svelte-form-lib";
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
import { object, string } from "yup";
|
import { object, string } from "yup";
|
||||||
|
|||||||
285
web/src/routes/settings/account/+page.svelte
Normal file
285
web/src/routes/settings/account/+page.svelte
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
|
import { page } from "$app/stores";
|
||||||
|
import { type RadioItem } from "$lib/components/base/radio_group.svelte";
|
||||||
|
import { type SearchItem } from "$lib/components/base/search.svelte";
|
||||||
|
import Select, {
|
||||||
|
type SelectItem,
|
||||||
|
} from "$lib/components/base/select.svelte";
|
||||||
|
import ConfirmModal from "$lib/components/confirm_modal.svelte";
|
||||||
|
import EmailModal from "$lib/components/profile/email_modal.svelte";
|
||||||
|
import PasswordModal from "$lib/components/profile/password_modal.svelte";
|
||||||
|
import Settings from "$lib/components/profile/settings.svelte";
|
||||||
|
import { settings_update } from "$lib/stores/settings_store";
|
||||||
|
import { show_toast } from "$lib/stores/toast_store";
|
||||||
|
import {
|
||||||
|
currentUser,
|
||||||
|
logout,
|
||||||
|
users_delete,
|
||||||
|
users_update,
|
||||||
|
} from "$lib/stores/user_store";
|
||||||
|
import { country_codes } from "$lib/util/country_code_util";
|
||||||
|
import { getFileURL } from "$lib/util/file_util";
|
||||||
|
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" },
|
||||||
|
{ text: $_("english"), value: "en" },
|
||||||
|
{ text: $_("french"), value: "fr" },
|
||||||
|
{ text: $_("hungarian"), value: "hu" },
|
||||||
|
{ text: $_("italian"), value: "it" },
|
||||||
|
{ text: $_("dutch"), value: "nl" },
|
||||||
|
{ text: $_("polish"), value: "pl" },
|
||||||
|
{ 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 = "";
|
||||||
|
|
||||||
|
let openConfirmModal: () => void;
|
||||||
|
let openEmailModal: () => void;
|
||||||
|
let openPasswordModal: () => void;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
citySearchQuery = settings?.location?.name ?? "";
|
||||||
|
selectedLanguage = settings?.language || "en";
|
||||||
|
selectedMapFocus = settings?.mapFocus ?? "trails";
|
||||||
|
});
|
||||||
|
|
||||||
|
async function searchCities(q: string) {
|
||||||
|
const r = await fetch("/api/v1/search/cities500", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ q: q, options: { limit: 5 } }),
|
||||||
|
});
|
||||||
|
const result = await r.json();
|
||||||
|
searchDropdownItems = result.hits.map((h: Record<string, any>) => ({
|
||||||
|
text: h.name,
|
||||||
|
description: `${h.division ? `${h.division} | ` : ""}${
|
||||||
|
country_codes[h["country code"] as keyof typeof country_codes]
|
||||||
|
}`,
|
||||||
|
value: h,
|
||||||
|
icon: "city",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSearchClick(item: SearchItem) {
|
||||||
|
citySearchQuery = item.text;
|
||||||
|
await settings_update({
|
||||||
|
id: settings?.id!,
|
||||||
|
location: {
|
||||||
|
name: item.value.name,
|
||||||
|
lat: item.value.lat,
|
||||||
|
lon: item.value.lon,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAvatarSelection() {
|
||||||
|
const files = (
|
||||||
|
document.getElementById("avatarInput") as HTMLInputElement
|
||||||
|
).files;
|
||||||
|
|
||||||
|
if (!files || files.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await users_update($currentUser!, files[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUnitSelection(e: RadioItem) {
|
||||||
|
await settings_update({
|
||||||
|
id: settings!.id,
|
||||||
|
unit: e.value as "imperial" | "metric",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLanguageSelection(
|
||||||
|
value: "en" | "de" | "fr" | "hu" | "nl" | "pl" | "pt",
|
||||||
|
) {
|
||||||
|
locale.set(value);
|
||||||
|
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();
|
||||||
|
goto("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateEmail(email: string) {
|
||||||
|
try {
|
||||||
|
await users_update({ ...$currentUser!, email: email });
|
||||||
|
} catch (e) {
|
||||||
|
show_toast({
|
||||||
|
text: "Error updating email",
|
||||||
|
icon: "close",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updatePassword(data: {
|
||||||
|
oldPassword: string;
|
||||||
|
password: string;
|
||||||
|
passwordConfirm: string;
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
await users_update({ ...$currentUser!, ...data });
|
||||||
|
show_toast({
|
||||||
|
text: $_("password-updated"),
|
||||||
|
icon: "check",
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
show_toast({
|
||||||
|
text: $_("error-updating-password"),
|
||||||
|
icon: "close",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openFileBrowser() {
|
||||||
|
document.getElementById("avatarInput")!.click();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{$_("settings")} | wanderer</title>
|
||||||
|
</svelte:head>
|
||||||
|
<main>
|
||||||
|
{#if $currentUser}
|
||||||
|
<Settings selected={0}>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<h3 class="text-2xl font-semibold">{$_("profile")}</h3>
|
||||||
|
|
||||||
|
<div class="flex gap-6 items-center">
|
||||||
|
<div
|
||||||
|
class="rounded-full w-24 aspect-square overflow-hidden relative group"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={getFileURL(
|
||||||
|
$currentUser,
|
||||||
|
$currentUser.avatar,
|
||||||
|
) ||
|
||||||
|
`https://api.dicebear.com/7.x/initials/svg?seed=${$currentUser.username}&backgroundType=gradientLinear`}
|
||||||
|
alt="avatar"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="absolute top-0 w-24 aspect-square opacity-0 group-hover:opacity-100 flex justify-center items-center bg-black/50 focus:bg-black/60 text-white cursor-pointer transition-opacity"
|
||||||
|
on:click={openFileBrowser}
|
||||||
|
>
|
||||||
|
<i class="fa fa-pen"></i>
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
name="avatar"
|
||||||
|
id="avatarInput"
|
||||||
|
accept="image/*"
|
||||||
|
style="display: none;"
|
||||||
|
on:change={handleAvatarSelection}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="text-2xl">{$currentUser.username}</h4>
|
||||||
|
<h5 class="font-medium">{$currentUser.email}</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="text-2xl font-semibold">{$_("login-details")}</h3>
|
||||||
|
<button class="btn-secondary block" on:click={openEmailModal}
|
||||||
|
>{$_("change-email")}</button
|
||||||
|
>
|
||||||
|
<button class="btn-secondary" on:click={openPasswordModal}
|
||||||
|
>{$_("change-password")}</button
|
||||||
|
>
|
||||||
|
|
||||||
|
<h3 class="text-2xl font-semibold">{$_("language")}</h3>
|
||||||
|
<div class="block">
|
||||||
|
<Select
|
||||||
|
items={languages}
|
||||||
|
bind:value={selectedLanguage}
|
||||||
|
on:change={(e) => handleLanguageSelection(e.detail)}
|
||||||
|
></Select>
|
||||||
|
</div>
|
||||||
|
<!--<div>
|
||||||
|
<h5 class="font-medium mb-1">{$_("units")}</h5>
|
||||||
|
<RadioGroup
|
||||||
|
name="unit"
|
||||||
|
items={units}
|
||||||
|
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>
|
||||||
|
<button class="btn-danger" on:click={openConfirmModal}
|
||||||
|
>{$_("delete-account")}</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<EmailModal
|
||||||
|
email={$currentUser.email}
|
||||||
|
on:save={(e) => updateEmail(e.detail)}
|
||||||
|
bind:openModal={openEmailModal}
|
||||||
|
></EmailModal>
|
||||||
|
<PasswordModal
|
||||||
|
on:save={(e) => updatePassword(e.detail)}
|
||||||
|
bind:openModal={openPasswordModal}
|
||||||
|
></PasswordModal>
|
||||||
|
</Settings>
|
||||||
|
{/if}
|
||||||
|
</main>
|
||||||
|
<ConfirmModal
|
||||||
|
text={$_("account-delete-confirm")}
|
||||||
|
bind:openModal={openConfirmModal}
|
||||||
|
on:confirm={deleteAccount}
|
||||||
|
></ConfirmModal>
|
||||||
212
web/src/routes/settings/display/+page.svelte
Normal file
212
web/src/routes/settings/display/+page.svelte
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { page } from "$app/stores";
|
||||||
|
import RadioGroup, {
|
||||||
|
type RadioItem,
|
||||||
|
} from "$lib/components/base/radio_group.svelte";
|
||||||
|
import Search, {
|
||||||
|
type SearchItem,
|
||||||
|
} from "$lib/components/base/search.svelte";
|
||||||
|
import Select, {
|
||||||
|
type SelectItem,
|
||||||
|
} from "$lib/components/base/select.svelte";
|
||||||
|
|
||||||
|
import TextField from "$lib/components/base/text_field.svelte";
|
||||||
|
import Settings from "$lib/components/profile/settings.svelte";
|
||||||
|
import type { Category } from "$lib/models/category.js";
|
||||||
|
import { settings_update } from "$lib/stores/settings_store";
|
||||||
|
import { currentUser } from "$lib/stores/user_store";
|
||||||
|
import { country_codes } from "$lib/util/country_code_util";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { _ } from "svelte-i18n";
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
|
||||||
|
$: settings = $page.data.settings;
|
||||||
|
|
||||||
|
const mapFocus: SelectItem[] = [
|
||||||
|
{ text: $_("trail", { values: { n: 2 } }), value: "trails" },
|
||||||
|
{ text: $_("location"), value: "location" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const categoryItems: SelectItem[] = data.categories.map((c: Category) => ({
|
||||||
|
text: $_(c.name),
|
||||||
|
value: c.id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const units: RadioItem[] = [
|
||||||
|
{ text: $_("metric"), value: "metric" },
|
||||||
|
{ text: $_("imperial"), value: "imperial" },
|
||||||
|
];
|
||||||
|
|
||||||
|
let selectedLanguage = "en";
|
||||||
|
let selectedMapFocus = "trails";
|
||||||
|
|
||||||
|
let selectedCategory =
|
||||||
|
$page.data.settings?.category || data.categories[0].id;
|
||||||
|
|
||||||
|
let searchDropdownItems: SearchItem[] = [];
|
||||||
|
let citySearchQuery: string = "";
|
||||||
|
|
||||||
|
let customTilesetName: string = "";
|
||||||
|
let customTilesetURL: string = "";
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
citySearchQuery = settings?.location?.name ?? "";
|
||||||
|
selectedLanguage = settings?.language || "en";
|
||||||
|
selectedMapFocus = settings?.mapFocus ?? "trails";
|
||||||
|
});
|
||||||
|
|
||||||
|
async function searchCities(q: string) {
|
||||||
|
const r = await fetch("/api/v1/search/cities500", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ q: q, options: { limit: 5 } }),
|
||||||
|
});
|
||||||
|
const result = await r.json();
|
||||||
|
searchDropdownItems = result.hits.map((h: Record<string, any>) => ({
|
||||||
|
text: h.name,
|
||||||
|
description: `${h.division ? `${h.division} | ` : ""}${
|
||||||
|
country_codes[h["country code"] as keyof typeof country_codes]
|
||||||
|
}`,
|
||||||
|
value: h,
|
||||||
|
icon: "city",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSearchClick(item: SearchItem) {
|
||||||
|
citySearchQuery = item.text;
|
||||||
|
await settings_update({
|
||||||
|
id: settings?.id!,
|
||||||
|
location: {
|
||||||
|
name: item.value.name,
|
||||||
|
lat: item.value.lat,
|
||||||
|
lon: item.value.lon,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUnitSelection(e: RadioItem) {
|
||||||
|
await settings_update({
|
||||||
|
id: settings!.id,
|
||||||
|
unit: e.value as "imperial" | "metric",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleMapFocusSelection(value: "trails" | "location") {
|
||||||
|
await settings_update({
|
||||||
|
id: settings!.id,
|
||||||
|
mapFocus: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCategorySelection(value: string) {
|
||||||
|
await settings_update({
|
||||||
|
id: settings!.id,
|
||||||
|
category: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleTilesetAdd() {
|
||||||
|
if (!customTilesetName || !customTilesetURL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await settings_update({
|
||||||
|
id: settings!.id,
|
||||||
|
tilesets: [
|
||||||
|
...(settings.tilesets ?? []),
|
||||||
|
{ name: customTilesetName, url: customTilesetURL },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
customTilesetName = "";
|
||||||
|
customTilesetURL = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleTilesetDelete(index: number) {
|
||||||
|
settings.tilesets.splice(index, 1);
|
||||||
|
await settings_update({
|
||||||
|
id: settings!.id,
|
||||||
|
tilesets: settings.tilesets,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{$_("settings")} | wanderer</title>
|
||||||
|
</svelte:head>
|
||||||
|
<main>
|
||||||
|
{#if $currentUser}
|
||||||
|
<Settings selected={1}>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<h3 class="text-2xl font-semibold">{$_("units")}</h3>
|
||||||
|
<RadioGroup
|
||||||
|
name="unit"
|
||||||
|
items={units}
|
||||||
|
selected={settings?.unit == "metric" ? 0 : 1}
|
||||||
|
on:change={(e) => handleUnitSelection(e.detail)}
|
||||||
|
></RadioGroup>
|
||||||
|
<h3 class="text-2xl font-semibold">{$_("focus-map-on")}</h3>
|
||||||
|
|
||||||
|
<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}
|
||||||
|
<h3 class="text-2xl font-semibold">{$_("default-category")}</h3>
|
||||||
|
<Select
|
||||||
|
items={categoryItems}
|
||||||
|
bind:value={selectedCategory}
|
||||||
|
on:change={(e) => handleCategorySelection(e.detail)}
|
||||||
|
></Select>
|
||||||
|
<h3 class="text-2xl font-semibold">{$_("tilesets")}</h3>
|
||||||
|
|
||||||
|
{#each settings.tilesets ?? [] as tileset, i}
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between px-4 py-2 border border-input-border rounded-xl"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p class="font-medium">{tileset.name}</p>
|
||||||
|
<p class=" text-sm">{tileset.url}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="btn-icon"
|
||||||
|
on:click={() => handleTilesetDelete(i)}
|
||||||
|
><i class="fa fa-trash text-red-500"></i></button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<div class="flex gap-4 items-center">
|
||||||
|
<TextField
|
||||||
|
label={$_("name")}
|
||||||
|
bind:value={customTilesetName}
|
||||||
|
placeholder="Open Street Maps"
|
||||||
|
></TextField>
|
||||||
|
<div class="flex items-center basis-full gap-2">
|
||||||
|
<div class="flex-grow">
|
||||||
|
<TextField
|
||||||
|
label="URL"
|
||||||
|
bind:value={customTilesetURL}
|
||||||
|
placeholder="https://{'{'}s{'}'}.tile.openstreetmap.org/{'{'}z{'}'}/{'{'}x{'}'}/{'{'}y{'}'}.png"
|
||||||
|
></TextField>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="btn-icon mt-6"
|
||||||
|
on:click={handleTilesetAdd}
|
||||||
|
><i class="fa fa-plus"></i></button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Settings>
|
||||||
|
{/if}
|
||||||
|
</main>
|
||||||
8
web/src/routes/settings/display/+page.ts
Normal file
8
web/src/routes/settings/display/+page.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { categories_index } from "$lib/stores/category_store";
|
||||||
|
import { type Load } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
export const load: Load = async ({ params, fetch }) => {
|
||||||
|
const categories = await categories_index(fetch)
|
||||||
|
|
||||||
|
return { categories: categories }
|
||||||
|
};
|
||||||
194
web/src/routes/settings/export/+page.svelte
Normal file
194
web/src/routes/settings/export/+page.svelte
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Button from "$lib/components/base/button.svelte";
|
||||||
|
import Settings from "$lib/components/profile/settings.svelte";
|
||||||
|
import TrailExportModal from "$lib/components/trail/trail_export_modal.svelte";
|
||||||
|
import { show_toast } from "$lib/stores/toast_store";
|
||||||
|
import { fetchGPX, trails_index, trails_upload } from "$lib/stores/trail_store";
|
||||||
|
import { getFileURL, saveAs } from "$lib/util/file_util";
|
||||||
|
import { trail2gpx } from "$lib/util/gpx_util";
|
||||||
|
import { gpx } from "$lib/vendor/toGeoJSON/toGeoJSON";
|
||||||
|
import JSZip from "jszip";
|
||||||
|
import { _ } from "svelte-i18n";
|
||||||
|
import { linear } from "svelte/easing";
|
||||||
|
import { tweened } from "svelte/motion";
|
||||||
|
|
||||||
|
const uploadProgress = tweened(0, {
|
||||||
|
duration: 300,
|
||||||
|
easing: linear,
|
||||||
|
});
|
||||||
|
|
||||||
|
let openExportModal: () => void;
|
||||||
|
|
||||||
|
function openFileBrowser() {
|
||||||
|
document.getElementById("file-input")!.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragOver(e: DragEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDrop(e: DragEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleFileSelection(e.dataTransfer?.files);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFileSelection(files?: FileList | null) {
|
||||||
|
if (!files) {
|
||||||
|
files = (document.getElementById("file-input") as HTMLInputElement)
|
||||||
|
.files;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!files) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let errorsThrown = 0;
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
const file = files[i];
|
||||||
|
try {
|
||||||
|
await trails_upload(file);
|
||||||
|
} catch (e) {
|
||||||
|
errorsThrown += 1;
|
||||||
|
show_toast({
|
||||||
|
type: "error",
|
||||||
|
icon: "close",
|
||||||
|
text: `Error uploading file: ${file.name}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
$uploadProgress += (files.length / i) * 100;
|
||||||
|
setTimeout(() => {}, 3000);
|
||||||
|
}
|
||||||
|
if (errorsThrown == 0) {
|
||||||
|
show_toast({
|
||||||
|
type: "success",
|
||||||
|
icon: "check",
|
||||||
|
text: `${files.length} ${$_("trail", { values: { n: files.length } })} ${$_("uploaded")}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportTrails(exportSettings: {
|
||||||
|
fileFormat: "gpx" | "json";
|
||||||
|
photos: boolean;
|
||||||
|
summitLog: boolean;
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
const trails = await trails_index(-1);
|
||||||
|
|
||||||
|
const zip = new JSZip();
|
||||||
|
|
||||||
|
for (const trail of trails) {
|
||||||
|
const gpxData: string = await fetchGPX(trail);
|
||||||
|
if (!trail.expand) {
|
||||||
|
trail.expand = {};
|
||||||
|
}
|
||||||
|
trail.expand.gpx_data = gpxData;
|
||||||
|
const trailFolder = zip.folder(`${trail.name}`);
|
||||||
|
let fileData: string = await trail2gpx(trail);
|
||||||
|
if (exportSettings.fileFormat == "json") {
|
||||||
|
fileData = JSON.stringify(
|
||||||
|
gpx(
|
||||||
|
new DOMParser().parseFromString(
|
||||||
|
fileData,
|
||||||
|
"text/xml",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
trailFolder?.file(
|
||||||
|
`${trail.name}.${exportSettings.fileFormat}`,
|
||||||
|
fileData,
|
||||||
|
);
|
||||||
|
if (exportSettings.photos) {
|
||||||
|
const photoFolder = trailFolder?.folder($_("photos"));
|
||||||
|
for (const photo of trail.photos ?? []) {
|
||||||
|
const photoURL = getFileURL(trail, photo);
|
||||||
|
const photoBlob = await fetch(photoURL).then(
|
||||||
|
(response) => response.blob(),
|
||||||
|
);
|
||||||
|
const photoData = new File([photoBlob], photo);
|
||||||
|
photoFolder?.file(photo, photoData, { base64: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (exportSettings.summitLog) {
|
||||||
|
let summitLogString = "";
|
||||||
|
for (const summitLog of trail.expand?.summit_logs ?? []) {
|
||||||
|
summitLogString += `${summitLog.date},${summitLog.text}\n`;
|
||||||
|
}
|
||||||
|
trailFolder?.file(
|
||||||
|
`${trail.name} - ${$_("summit-book")}.csv`,
|
||||||
|
summitLogString,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await zip.generateAsync({ type: "blob" });
|
||||||
|
saveAs(blob, `wanderer-export-${Date.now()}.zip`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
show_toast({
|
||||||
|
type: "error",
|
||||||
|
icon: "close",
|
||||||
|
text: $_("error-exporting-trail"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{$_("settings")} | wanderer</title>
|
||||||
|
</svelte:head>
|
||||||
|
<main>
|
||||||
|
<Settings selected={2}>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<h3 class="text-2xl font-semibold">{$_("import")}</h3>
|
||||||
|
<button
|
||||||
|
class="drop-area relative h-64 w-full border border-content border-dashed rounded-xl flex items-center justify-center text-gray-500 bg-background cursor-pointer hover:bg-menu-item-background-hover focus:bg-menu-item-background-focus transition-colors"
|
||||||
|
style="--progress: {$uploadProgress}%"
|
||||||
|
on:click={openFileBrowser}
|
||||||
|
on:dragover={handleDragOver}
|
||||||
|
on:drop={handleDrop}
|
||||||
|
>
|
||||||
|
Select or drag GPX, FIT, KML or TCX files here...
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="file-input"
|
||||||
|
accept=".gpx,.GPX,.tcx,.TCX,.kml,.KML,.fit,.FIT"
|
||||||
|
multiple={true}
|
||||||
|
style="display: none;"
|
||||||
|
on:change={() => handleFileSelection()}
|
||||||
|
/>
|
||||||
|
<h3 class="text-2xl font-semibold">{$_("export")}</h3>
|
||||||
|
<Button secondary={true} on:click={() => openExportModal()}
|
||||||
|
>{$_("export-all-trails")}</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</Settings>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<TrailExportModal
|
||||||
|
bind:openModal={openExportModal}
|
||||||
|
on:export={(e) => exportTrails(e.detail)}
|
||||||
|
></TrailExportModal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.drop-area::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: -4px;
|
||||||
|
top: -4px;
|
||||||
|
right: -4px;
|
||||||
|
bottom: -4px;
|
||||||
|
background-color: transparent;
|
||||||
|
z-index: -100;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background-image: conic-gradient(
|
||||||
|
rgba(var(--content)),
|
||||||
|
rgba(var(--content)) var(--progress),
|
||||||
|
transparent var(--progress)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { page } from "$app/stores";
|
||||||
import { PUBLIC_VALHALLA_URL } from "$env/static/public";
|
import { PUBLIC_VALHALLA_URL } from "$env/static/public";
|
||||||
import Button from "$lib/components/base/button.svelte";
|
import Button from "$lib/components/base/button.svelte";
|
||||||
import Datepicker from "$lib/components/base/datepicker.svelte";
|
import Datepicker from "$lib/components/base/datepicker.svelte";
|
||||||
@@ -50,18 +51,18 @@
|
|||||||
formatElevation,
|
formatElevation,
|
||||||
formatTimeHHMM,
|
formatTimeHHMM,
|
||||||
} from "$lib/util/format_util";
|
} from "$lib/util/format_util";
|
||||||
import { fromFIT, fromKML, fromTCX, gpx2trail } from "$lib/util/gpx_util";
|
import { fromFIT, fromKML, fromTCX, gpx2trail, isFITFile } from "$lib/util/gpx_util";
|
||||||
import {
|
import {
|
||||||
createAnchorMarker,
|
createAnchorMarker,
|
||||||
createMarkerFromWaypoint,
|
createMarkerFromWaypoint,
|
||||||
} from "$lib/util/leaflet_util";
|
} from "$lib/util/leaflet_util";
|
||||||
import { createForm } from "$lib/vendor/svelte-form-lib";
|
import { createForm } from "$lib/vendor/svelte-form-lib";
|
||||||
import cryptoRandomString from "crypto-random-string";
|
import cryptoRandomString from "crypto-random-string";
|
||||||
import type { DivIcon, LatLng, LeafletMouseEvent, Map } from "leaflet";
|
import type { DivIcon, LeafletMouseEvent, Map } from "leaflet";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
import { backInOut } from "svelte/easing";
|
import { backInOut } from "svelte/easing";
|
||||||
import { fade, scale } from "svelte/transition";
|
import { scale } from "svelte/transition";
|
||||||
import { array, number, object, string } from "yup";
|
import { array, number, object, string } from "yup";
|
||||||
|
|
||||||
export let data: { trail: Trail };
|
export let data: { trail: Trail };
|
||||||
@@ -110,7 +111,10 @@
|
|||||||
let autoRouting = true;
|
let autoRouting = true;
|
||||||
|
|
||||||
const { form, errors, handleChange, handleSubmit } = createForm<Trail>({
|
const { form, errors, handleChange, handleSubmit } = createForm<Trail>({
|
||||||
initialValues: data.trail,
|
initialValues: {
|
||||||
|
...data.trail,
|
||||||
|
category: $page.data.settings?.category || $categories[0].id,
|
||||||
|
},
|
||||||
validationSchema: trailSchema,
|
validationSchema: trailSchema,
|
||||||
onError: (errors) => {
|
onError: (errors) => {
|
||||||
if (errors.name) {
|
if (errors.name) {
|
||||||
@@ -225,9 +229,10 @@
|
|||||||
const fileExtension = file.name.split(".").pop()?.toLowerCase();
|
const fileExtension = file.name.split(".").pop()?.toLowerCase();
|
||||||
|
|
||||||
let gpxData = "";
|
let gpxData = "";
|
||||||
|
|
||||||
if (fileExtension !== "fit") {
|
|
||||||
const fileContent = await file.text();
|
const fileContent = await file.text();
|
||||||
|
const fileBuffer = await file.arrayBuffer();
|
||||||
|
|
||||||
|
if (!isFITFile(fileBuffer)) {
|
||||||
if (fileContent.includes("http://www.opengis.net/kml")) {
|
if (fileContent.includes("http://www.opengis.net/kml")) {
|
||||||
gpxData = fromKML(fileContent);
|
gpxData = fromKML(fileContent);
|
||||||
gpxFile = new Blob([gpxData], {
|
gpxFile = new Blob([gpxData], {
|
||||||
@@ -244,11 +249,13 @@
|
|||||||
gpxFile = file;
|
gpxFile = file;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const fileContent = await file.arrayBuffer();
|
gpxData = await fromFIT(fileBuffer);
|
||||||
gpxData = await fromFIT(fileContent);
|
gpxFile = new Blob([gpxData], {
|
||||||
|
type: "application/gpx+xml",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return gpxData
|
return gpxData;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleFileSelection() {
|
async function handleFileSelection() {
|
||||||
@@ -272,7 +279,7 @@
|
|||||||
const parseResult = await gpx2trail(gpxData);
|
const parseResult = await gpx2trail(gpxData);
|
||||||
$form = parseResult.trail;
|
$form = parseResult.trail;
|
||||||
$form.expand.gpx_data = gpxData;
|
$form.expand.gpx_data = gpxData;
|
||||||
$form.category = $categories[0].id;
|
$form.category = $page.data.settings.category || $categories[0].id;
|
||||||
|
|
||||||
setRoute(parseResult.gpx);
|
setRoute(parseResult.gpx);
|
||||||
initRouteAnchors(parseResult.gpx);
|
initRouteAnchors(parseResult.gpx);
|
||||||
@@ -399,7 +406,7 @@
|
|||||||
savedWaypoint.marker = marker;
|
savedWaypoint.marker = marker;
|
||||||
}
|
}
|
||||||
|
|
||||||
function beforSummitLogModalOpen() {
|
function beforeSummitLogModalOpen() {
|
||||||
summitLog.set(new SummitLog(new Date().toISOString().split("T")[0]));
|
summitLog.set(new SummitLog(new Date().toISOString().split("T")[0]));
|
||||||
openSummitLogModal();
|
openSummitLogModal();
|
||||||
}
|
}
|
||||||
@@ -810,7 +817,7 @@
|
|||||||
<button
|
<button
|
||||||
class="btn-secondary"
|
class="btn-secondary"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={beforSummitLogModalOpen}
|
on:click={beforeSummitLogModalOpen}
|
||||||
><i class="fa fa-plus mr-2"></i>{$_("add-entry")}</button
|
><i class="fa fa-plus mr-2"></i>{$_("add-entry")}</button
|
||||||
>
|
>
|
||||||
{#if $lists.length}
|
{#if $lists.length}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { lists_index } from "$lib/stores/list_store";
|
|||||||
import { trails_show } from "$lib/stores/trail_store";
|
import { trails_show } from "$lib/stores/trail_store";
|
||||||
import { error, type Load } from "@sveltejs/kit";
|
import { error, type Load } from "@sveltejs/kit";
|
||||||
|
|
||||||
export const load: Load = async ({ params, fetch }) => {
|
export const load: Load = async ({ params, fetch, data }) => {
|
||||||
if (!params.id) {
|
if (!params.id) {
|
||||||
return error(400, "Bad Request")
|
return error(400, "Bad Request")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user