Compare commits

...

83 Commits

Author SHA1 Message Date
MaysWind e608e85d56 support disable map 2023-06-10 17:14:58 +08:00
MaysWind 58104a9a4d append thousands separator in data management page 2023-06-10 17:04:35 +08:00
MaysWind 8d68dcabb5 list item selection sheet supports large size 2023-06-06 01:01:24 +08:00
MaysWind a5e5389d6c fix some text not changed when user language has been changed 2023-06-06 00:56:39 +08:00
MaysWind 8de862c82f optimize the style when the text is too long 2023-06-06 00:52:35 +08:00
MaysWind 5141303ee1 support turning on dark theme manually 2023-06-05 23:54:01 +08:00
MaysWind 3d95680cbc remove unused code 2023-06-05 09:47:09 +08:00
MaysWind 78663b873c support only match the first locale part when get browser language 2023-06-05 00:56:04 +08:00
MaysWind 0a106026dd user settings supports language and date&time format 2023-06-05 00:54:07 +08:00
MaysWind 999ca6274c remove unused file 2023-06-04 21:08:38 +08:00
MaysWind 36b9177069 fix building issue 2023-06-04 15:43:38 +08:00
MaysWind 8cf7bf859b support map provider and whether use map data proxy settings 2023-06-04 15:06:14 +08:00
MaysWind 2e54b62f60 set cookies in development mode 2023-06-04 14:25:51 +08:00
MaysWind df46069d92 code refactor 2023-06-04 13:15:09 +08:00
MaysWind e31014dde4 code refactor 2023-06-04 13:04:15 +08:00
MaysWind f9a14581e1 fix typo 2023-06-04 01:24:08 +08:00
MaysWind 95ec005894 add icons 2023-06-04 01:22:43 +08:00
MaysWind 73d271b8bc improve ui 2023-06-04 01:22:33 +08:00
MaysWind 0c3b56e44a set the color of input and confirm button to red in password input sheet when clear user data 2023-06-03 20:11:03 +08:00
MaysWind 736f340979 code refactor 2023-06-03 16:49:19 +08:00
MaysWind 49e62d35c3 exchange rate datasource supports Monetary Authority of Singapore 2023-05-29 01:04:16 +08:00
MaysWind 810bce7495 update latest available currencies 2023-05-29 00:27:30 +08:00
MaysWind aff876aa05 only trigger gitea docker-snapshot workflow when push to main branch 2023-05-28 17:59:25 +08:00
MaysWind 9511644ce6 update third party dependencies 2023-05-28 17:59:08 +08:00
MaysWind 21d73e5f69 update golang to 1.20.4, node to 18.16.0, base docker image to alpine 3.17.3 2023-05-28 12:18:40 +08:00
MaysWind d62f3fb936 update github actions 2023-05-21 00:06:11 +08:00
MaysWind 0a011b6075 add gitea actions 2023-05-20 23:49:00 +08:00
MaysWind 4e561dc764 change map marker icon 2023-05-15 23:31:53 +08:00
MaysWind a55256ad82 code refactor 2023-05-15 22:36:21 +08:00
MaysWind ab26ef64a5 code refactor 2023-05-15 22:35:59 +08:00
mayswind 6d1610eee0 fix wrong proxy path 2023-05-15 00:49:02 +08:00
MaysWind 2ba143d6ea support showing geolocation on map 2023-05-15 00:08:03 +08:00
MaysWind bd542ac308 modify geolocation storage type in database 2023-05-14 19:42:57 +08:00
MaysWind e71ffd1a77 support storing geo location in transaction 2023-04-29 13:45:19 +08:00
MaysWind 1ac968f63c improve ui 2023-04-28 17:21:27 +08:00
MaysWind dc4f62a085 modify Simplified Chinese translation 2023-04-28 16:26:52 +08:00
MaysWind ad91d4f1ce add show/hide hidden transaction category/tag menu 2023-04-28 16:24:15 +08:00
MaysWind 8d4c7512ab reduce unit test execute times 2023-04-24 00:54:24 +08:00
MaysWind 12d5837526 modify unit test case fail cause message 2023-04-23 21:49:27 +08:00
MaysWind c82d2abab4 fix problem that cannot switch to other date range sometimes in transaction list page 2023-04-23 01:27:38 +08:00
MaysWind 26cb717a08 build when push to non-main branch 2023-04-23 01:00:27 +08:00
MaysWind 0b1cc0ef5b add lint checking and unit testing in build script 2023-04-23 00:45:56 +08:00
MaysWind 05dc9138b4 fix problem that cannot get transaction statistics when db is set to only_full_group_by 2023-04-23 00:03:26 +08:00
MaysWind a7dcacb26c add log 2023-04-22 23:54:57 +08:00
MaysWind 3ac3f871e4 fix the problem that account list page does not update when modify category of account 2023-04-22 23:12:41 +08:00
MaysWind b180c0bbe6 modify link text 2023-04-22 22:35:36 +08:00
MaysWind e4987f3bde modify vue page name 2023-04-22 22:28:04 +08:00
MaysWind ee2690c2cc fix the problem that cannot change language in transaction preset category page 2023-04-22 21:50:08 +08:00
MaysWind 4cad26f793 change moment api 2023-04-22 21:26:42 +08:00
MaysWind 84f3d5fec5 remove unused code 2023-04-22 21:09:01 +08:00
MaysWind bb3939d570 show none when user does not have visible account or category 2023-04-22 21:03:41 +08:00
MaysWind b303f708f5 improve ui 2023-04-22 20:52:20 +08:00
MaysWind d39550e090 improve ui 2023-04-22 20:47:25 +08:00
MaysWind 22d956f38a improve ui 2023-04-22 20:24:53 +08:00
MaysWind dab7728138 update prompt text 2023-04-22 17:29:30 +08:00
MaysWind 4038b6bc51 improve ui 2023-04-22 17:00:56 +08:00
MaysWind bcaf3a246c show no available category / account in transaction statistics filter page when there are no available category or account 2023-04-22 16:19:10 +08:00
MaysWind 4fb115fcbc remove unused code 2023-04-22 01:37:16 +08:00
MaysWind bfd4b2b6de code refactor 2023-04-22 01:33:38 +08:00
MaysWind c0cd3fc5c2 improve ui 2023-04-22 01:24:49 +08:00
MaysWind f95c4393d2 fix the problem that the day of week in date time / date range picker is wrong 2023-04-22 01:10:22 +08:00
MaysWind e2eb5fabcc improve ui 2023-04-22 00:49:47 +08:00
MaysWind 7275e8ff0d modify infinite distance 2023-04-22 00:31:19 +08:00
MaysWind accfc3df12 improve ui 2023-04-21 23:56:52 +08:00
MaysWind 35392483d9 code refactor 2023-04-21 23:56:44 +08:00
MaysWind 9770851fd4 fix the problem that page jump infinitely 2023-04-21 23:56:02 +08:00
MaysWind e013a6f087 fix the display amount in statistics page when there are no transaction 2023-04-21 23:53:19 +08:00
MaysWind 1ec9ff20b1 remove unused code 2023-04-21 23:41:53 +08:00
MaysWind 9b0dea80c9 don't allow clear the value in datetime picker 2023-04-21 23:14:26 +08:00
MaysWind eea1ea7ed0 always show date range picker in center 2023-04-21 22:36:57 +08:00
MaysWind 85cd46bfc7 fix problem the category separate icon in transaction page does not display 2023-04-21 22:30:07 +08:00
MaysWind a7ca394864 code refactor 2023-04-21 22:20:11 +08:00
MaysWind e178a0795a code refactor 2023-04-21 22:16:35 +08:00
MaysWind e8b0470ceb code refactor 2023-04-21 22:11:10 +08:00
MaysWind c08abfdfdf fix frontend build issue 2023-04-21 08:47:15 +08:00
mayswind b1c765eb51 Upgrade to vue3 (#16)
* upgrade to vue 3.x and framework7 8.x
* change calendar plugin to vue-datepicker
* disable export button when user does not hava any transaction
* implement new pin code input
* append thousands separator in amount in exchange rates page
2023-04-21 01:45:00 +08:00
mayswind 4b0f7d45e8 Merge pull request #15 from vigdail/bugfix/atomic_alignment
Proper fields alignment in `InternalUuidGenerator` struct
2023-04-20 22:49:49 +08:00
vigdail 9e6271b1dc Rearrange fields of InternalUuidGenerator struct to fit atomic alignment requirements 2023-04-20 19:39:38 +06:00
MaysWind a96eb31dc9 redirect to login page when user logout without token 2023-04-16 02:00:59 +08:00
MaysWind 221a7809b6 fix npe error 2023-04-12 00:24:20 +08:00
MaysWind 8c33243c90 code refactor 2023-04-09 01:04:00 +08:00
MaysWind c4b07b98aa update third party dependencies 2023-04-08 14:53:26 +08:00
MaysWind 1fda80a86b bump version to 0.3.0 2023-04-08 14:49:39 +08:00
165 changed files with 15269 additions and 20708 deletions
+13
View File
@@ -0,0 +1,13 @@
module.exports = {
'root': true,
'env': {
'node': true
},
'extends': [
'eslint:recommended',
'plugin:vue/vue3-essential'
],
'rules': {
'vue/no-use-v-if-with-v-for': 'off'
}
}
+50
View File
@@ -0,0 +1,50 @@
name: Docker Release
on:
push:
tags:
- v*
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
${{ secrets.DOCKER_REPO }}/mayswind/ezbookkeeping
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest
- name: Set up the environment
run: |
cat >> docker/custom-backend-pre-setup.sh <<EOF
#!/bin/sh
${{ secrets.CUSTOM_BACKEND_PRE_SETUP }}
EOF
cat >> docker/custom-frontend-pre-setup.sh <<EOF
#!/bin/sh
${{ secrets.CUSTOM_FRONTEND_PRE_SETUP }}
EOF
chmod +x docker/custom-backend-pre-setup.sh
chmod +x docker/custom-frontend-pre-setup.sh
- name: Build and push
uses: docker/build-push-action@v4
env:
ACTIONS_RUNTIME_TOKEN: '' # See https://gitea.com/gitea/act_runner/issues/119
with:
file: Dockerfile
context: .
push: true
build-args: |
RELEASE_BUILD=1
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+49
View File
@@ -0,0 +1,49 @@
name: Docker Snapshot
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
${{ secrets.DOCKER_REPO }}/mayswind/ezbookkeeping
tags: |
type=raw,value=SNAPSHOT-{{date 'YYYYMMDD'}}
type=raw,value=latest-snapshot
type=sha,format=short,prefix=SNAPSHOT-
- name: Set up the environment
run: |
sed -i 's#FROM #FROM ${{ secrets.DOCKER_REPO }}/mirrors/#g' Dockerfile
cat >> docker/custom-backend-pre-setup.sh <<EOF
#!/bin/sh
${{ secrets.CUSTOM_BACKEND_PRE_SETUP }}
EOF
cat >> docker/custom-frontend-pre-setup.sh <<EOF
#!/bin/sh
${{ secrets.CUSTOM_FRONTEND_PRE_SETUP }}
EOF
chmod +x docker/custom-backend-pre-setup.sh
chmod +x docker/custom-frontend-pre-setup.sh
- name: Build and push
uses: docker/build-push-action@v4
env:
ACTIONS_RUNTIME_TOKEN: '' # See https://gitea.com/gitea/act_runner/issues/119
with:
file: Dockerfile
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+32 -23
View File
@@ -9,35 +9,44 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Set up docker tag
id: vars
run: echo ::set-output name=RELEASE_TAG::${GITHUB_REF/refs\/tags\/v/}
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Login to DockerHub
uses: docker/login-action@v1
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
name: Build and push
uses: docker/build-push-action@v2
- name: Build and push
uses: docker/build-push-action@v4
with:
file: Dockerfile
context: .
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6
platforms: |
linux/amd64
linux/arm64/v8
linux/arm/v7
linux/arm/v6
push: true
build-args: |
RELEASE_BUILD=1
tags: |
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping:${{ steps.vars.outputs.RELEASE_TAG }}
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping:latest
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+31 -23
View File
@@ -9,33 +9,41 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Set up docker tag
id: vars
run: echo ::set-output name=BUILD_DATE::$(date '+%Y%m%d')
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Login to DockerHub
uses: docker/login-action@v1
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping
tags: |
type=raw,value=SNAPSHOT-{{date 'YYYYMMDD'}}
type=raw,value=latest-snapshot
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
name: Build and push
uses: docker/build-push-action@v2
- name: Build and push
uses: docker/build-push-action@v4
with:
file: Dockerfile
context: .
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6
platforms: |
linux/amd64
linux/arm64/v8
linux/arm/v7
linux/arm/v6
push: true
tags: |
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping:SNAPSHOT-${{ steps.vars.outputs.BUILD_DATE }}
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping:latest-snapshot
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
@@ -0,0 +1,28 @@
name: Docker Snapshot
on:
push:
branches-ignore:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
name: Build
uses: docker/build-push-action@v2
with:
file: Dockerfile
context: .
platforms: linux/amd64
push: false
+3 -3
View File
@@ -1,5 +1,5 @@
# Build backend binary file
FROM golang:1.20.2-alpine3.17 AS be-builder
FROM golang:1.20.4-alpine3.17 AS be-builder
ARG RELEASE_BUILD
ENV RELEASE_BUILD=$RELEASE_BUILD
WORKDIR /go/src/github.com/mayswind/ezbookkeeping
@@ -9,7 +9,7 @@ RUN apk add git gcc g++ libc-dev
RUN ./build.sh backend
# Build frontend files
FROM node:18.15.0-alpine3.17 AS fe-builder
FROM node:18.16.0-alpine3.17 AS fe-builder
ARG RELEASE_BUILD
ENV RELEASE_BUILD=$RELEASE_BUILD
WORKDIR /go/src/github.com/mayswind/ezbookkeeping
@@ -19,7 +19,7 @@ RUN apk add git
RUN ./build.sh frontend
# Package docker image
FROM alpine:3.17.2
FROM alpine:3.17.3
LABEL maintainer="MaysWind <i@mayswind.net>"
RUN addgroup -S -g 1000 ezbookkeeping && adduser -S -G ezbookkeeping -u 1000 ezbookkeeping
RUN apk --no-cache add tzdata
-5
View File
@@ -1,5 +0,0 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
+47 -8
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env sh
TYPE=""
NO_LINT="0"
NO_TEST="0"
RELEASE=${RELEASE_BUILD:-"0"}
RELEASE_TYPE="unknown"
VERSION=""
@@ -40,6 +42,8 @@ Options:
-r, --release Build release (The script will use environment variable "RELEASE_BUILD" to detect whether this is release building by default)
-o, --output <filename> Package file name (For "package" type only)
-t, --tag Docker tag (For "docker" type only)
--no-lint Do not execute lint check before building
--no-test Do not execute unit testing before building
-h, --help Show help
EOF
}
@@ -63,6 +67,12 @@ parse_args() {
DOCKER_TAG="$2"
shift
;;
--no-lint)
NO_LINT="1"
;;
--no-test)
NO_TEST="1"
;;
--help | -h)
show_help
exit 0
@@ -111,6 +121,27 @@ set_build_parameters() {
}
build_backend() {
if [ "$NO_LINT" = "0" ]; then
echo "Executing backend lint checking..."
go vet -v ./...
if [ "$?" != "0" ]; then
echo_red "Error: Failed to pass lint checking"
exit 1
fi
fi
if [ "$NO_TEST" = "0" ]; then
echo "Executing backend unit testing..."
go clean -cache
go test ./... -v
if [ "$?" != "0" ]; then
echo_red "Error: Failed to pass unit testing"
exit 1
fi
fi
backend_build_extra_arguments="-X main.Version=$VERSION"
backend_build_extra_arguments="$backend_build_extra_arguments -X main.CommitHash=$COMMIT_HASH"
@@ -125,18 +156,26 @@ build_backend() {
}
build_frontend() {
frontend_build_arguments="";
if [ "$RELEASE" = "0" ]; then
frontend_build_arguments="--buildUnixTime=$BUILD_UNIXTIME"
fi
echo "Pulling frontend dependencies..."
npm install
if [ "$NO_LINT" = "0" ]; then
echo "Executing frontend lint checking..."
npm run lint
if [ "$?" != "0" ]; then
echo_red "Error: Failed to pass lint checking"
exit 1
fi
fi
echo "Building frontend files ($RELEASE_TYPE)..."
export NODE_OPTIONS=--openssl-legacy-provider
npm run build -- "$frontend_build_arguments"
if [ "$RELEASE" = "0" ]; then
buildUnixTime=$BUILD_UNIXTIME npm run build
else
npm run build
fi
}
build_package() {
+8 -3
View File
@@ -398,10 +398,15 @@ func printUserInfo(user *models.User) {
fmt.Printf("[Nickname] %s\n", user.Nickname)
fmt.Printf("[Password] %s\n", user.Password)
fmt.Printf("[Salt] %s\n", user.Salt)
fmt.Printf("[DefaultCurrency] %s\n", user.DefaultCurrency)
fmt.Printf("[DefaultAccountId] %d\n", user.DefaultAccountId)
fmt.Printf("[FirstDayOfWeek] %s\n", user.FirstDayOfWeek)
fmt.Printf("[TransactionEditScope] %s\n", user.TransactionEditScope)
fmt.Printf("[TransactionEditScope] %s (%d)\n", user.TransactionEditScope, user.TransactionEditScope)
fmt.Printf("[Language] %s\n", user.Language)
fmt.Printf("[DefaultCurrency] %s\n", user.DefaultCurrency)
fmt.Printf("[FirstDayOfWeek] %s (%d)\n", user.FirstDayOfWeek, user.FirstDayOfWeek)
fmt.Printf("[LongDateFormat] %s (%d)\n", user.LongDateFormat, user.LongDateFormat)
fmt.Printf("[ShortDateFormat] %s (%d)\n", user.ShortDateFormat, user.ShortDateFormat)
fmt.Printf("[LongTimeFormat] %s (%d)\n", user.LongTimeFormat, user.LongTimeFormat)
fmt.Printf("[ShortTimeFormat] %s (%d)\n", user.ShortTimeFormat, user.ShortTimeFormat)
fmt.Printf("[Deleted] %t\n", user.Deleted)
fmt.Printf("[EmailVerified] %t\n", user.EmailVerified)
fmt.Printf("[CreatedAt] %s (%d)\n", utils.FormatUnixTimeToLongDateTimeInServerTimezone(user.CreatedUnixTime), user.CreatedUnixTime)
+33 -1
View File
@@ -110,13 +110,17 @@ func startWebServer(c *cli.Context) error {
router.Static("/mobile/css", filepath.Join(config.StaticRootPath, "css"))
router.Static("/mobile/img", filepath.Join(config.StaticRootPath, "img"))
router.Static("/mobile/fonts", filepath.Join(config.StaticRootPath, "fonts"))
router.Static("/mobile/sw", filepath.Join(config.StaticRootPath, "sw"))
router.StaticFile("/mobile/favicon.ico", filepath.Join(config.StaticRootPath, "favicon.ico"))
router.StaticFile("/mobile/favicon.png", filepath.Join(config.StaticRootPath, "favicon.png"))
router.StaticFile("/mobile/touchicon.png", filepath.Join(config.StaticRootPath, "touchicon.png"))
router.StaticFile("/mobile/manifest.json", filepath.Join(config.StaticRootPath, "manifest.json"))
router.StaticFile("/mobile/sw.js", filepath.Join(config.StaticRootPath, "sw.js"))
workboxFileNames := utils.ListFileNamesWithPrefixAndSuffix(config.StaticRootPath, "workbox-", ".js")
for i := 0; i < len(workboxFileNames); i++ {
router.StaticFile("/mobile/"+workboxFileNames[i], filepath.Join(config.StaticRootPath, workboxFileNames[i]))
}
desktopEntryRoute := router.Group("/desktop")
desktopEntryRoute.Use(bindMiddleware(middlewares.ServerSettingsCookie(config)))
{
@@ -133,6 +137,21 @@ func startWebServer(c *cli.Context) error {
router.GET("/healthz.json", bindApi(api.Healths.HealthStatusHandler))
if config.Mode == settings.MODE_DEVELOPMENT {
devRoute := router.Group("/dev")
devRoute.GET("/cookies", bindMiddleware(middlewares.ServerSettingsCookie(config)))
}
proxyRoute := router.Group("/proxy")
proxyRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString))
{
if config.EnableMapDataFetchProxy {
if config.MapProvider == settings.OpenStreetMapProvider {
proxyRoute.GET("/openstreetmap/tile/:zoomLevel/:coordinateX/:fileName", bindProxy(api.MapImages.OpenStreetMapTileImageProxyHandler))
}
}
}
apiRoute := router.Group("/api")
apiRoute.Use(bindMiddleware(middlewares.RequestId(config)))
@@ -284,3 +303,16 @@ func bindCsv(fn core.DataHandlerFunc) gin.HandlerFunc {
}
}
}
func bindProxy(fn core.ProxyHandlerFunc) gin.HandlerFunc {
return func(ginCtx *gin.Context) {
c := core.WrapContext(ginCtx)
proxy, err := fn(c)
if err != nil {
utils.PrintDataErrorResult(c, "text/text", err)
} else {
proxy.ServeHTTP(c.Writer, c.Request)
}
}
}
+14 -1
View File
@@ -110,8 +110,21 @@ enable_register = true
# Set to true to allow users to export their data
enable_export = true
[map]
# Map provider, supports `openstreetmap`
map_provider = openstreetmap
# Set to true to use the ezbookkeeping server to proxy map data requests, for "openstreetmap"
map_data_fetch_proxy = false
[exchange_rates]
# Exchange rates data source, supports "euro_central_bank", "bank_of_canada", "reserve_bank_of_australia", "czech_national_bank", "national_bank_of_poland" currently
# Exchange rates data source, supports the following types:
# "euro_central_bank"
# "bank_of_canada"
# "reserve_bank_of_australia",
# "czech_national_bank"
# "national_bank_of_poland"
# "monetary_authority_of_singapore"
data_source = euro_central_bank
# Requesting exchange rates data timeout (0 - 4294967295 milliseconds), default is 10000 (10 seconds)
+11 -10
View File
@@ -5,16 +5,16 @@ go 1.20
require (
github.com/gin-contrib/gzip v0.0.6
github.com/gin-gonic/gin v1.9.0
github.com/go-playground/validator/v10 v10.12.0
github.com/go-sql-driver/mysql v1.7.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/lib/pq v1.10.7
github.com/go-playground/validator/v10 v10.14.0
github.com/go-sql-driver/mysql v1.7.1
github.com/golang-jwt/jwt/v5 v5.0.0
github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.16
github.com/pquerna/otp v1.4.0
github.com/sirupsen/logrus v1.9.0
github.com/stretchr/testify v1.8.2
github.com/urfave/cli/v2 v2.25.0
golang.org/x/crypto v0.7.0
github.com/urfave/cli/v2 v2.25.4
golang.org/x/crypto v0.9.0
gopkg.in/ini.v1 v1.67.0
xorm.io/xorm v1.3.2
)
@@ -25,6 +25,7 @@ require (
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
@@ -32,7 +33,7 @@ require (
github.com/golang/snappy v0.0.4 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/leodido/go-urn v1.2.2 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
@@ -44,9 +45,9 @@ require (
github.com/ugorji/go/codec v1.2.9 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
xorm.io/builder v0.3.12 // indirect
+22 -50
View File
@@ -1,6 +1,5 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
gitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@@ -73,6 +72,8 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
@@ -88,7 +89,6 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
@@ -96,12 +96,12 @@ github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI=
github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
@@ -113,8 +113,8 @@ github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFG
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -134,12 +134,10 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
@@ -169,7 +167,6 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
@@ -230,7 +227,6 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
@@ -241,23 +237,21 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxv
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.2.2 h1:7z68G0FCGvDk646jz1AelTYNYWrTNm0bEcFAo147wt4=
github.com/leodido/go-urn v1.2.2/go.mod h1:kUaIbLZWttglzwNuG0pgsh5vuV6u2YcGBYz1hIPjtOQ=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
@@ -307,9 +301,7 @@ github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtb
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
@@ -355,12 +347,10 @@ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
@@ -368,7 +358,6 @@ github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThC
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef/go.mod h1:8AEUvGVi2uQ5b24BIhcr0GCcpd/RNAFWaN2CJFrWIIQ=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
@@ -418,8 +407,8 @@ github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU
github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.25.0 h1:ykdZKuQey2zq0yin/l7JOm9Mh+pg72ngYMeB0ABn6q8=
github.com/urfave/cli/v2 v2.25.0/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
github.com/urfave/cli/v2 v2.25.4 h1:HyYwPrTO3im9rYhUff/ZNs78eolxt0nJ4LN+9yJKSH4=
github.com/urfave/cli/v2 v2.25.4/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
@@ -460,8 +449,8 @@ golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWP
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -471,7 +460,6 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -490,8 +478,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -532,8 +520,8 @@ golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -541,8 +529,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -562,13 +550,11 @@ golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
@@ -595,24 +581,20 @@ google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -622,7 +604,6 @@ honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
@@ -637,7 +618,6 @@ modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g
modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.18 h1:rMZhRcWrba0y3nVmdiQ7kxAgOOSq2m2f2VzjHLgEs6U=
modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60=
modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw=
@@ -673,7 +653,6 @@ modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/E
modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84=
modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ=
modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY=
modernc.org/ccgo/v3 v3.12.82 h1:wudcnJyjLj1aQQCXF3IM9Gz2X6UNjw+afIghzdtn0v8=
modernc.org/ccgo/v3 v3.12.82/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w=
modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
@@ -711,24 +690,17 @@ modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw=
modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0=
modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI=
modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE=
modernc.org/libc v1.11.87 h1:PzIzOqtlzMDDcCzJ5cUP6h/Ku6Fa9iyflP2ccTY64aE=
modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY=
modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8=
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc=
modernc.org/memory v1.0.5 h1:XRch8trV7GgvTec2i7jc33YlUI0RKVDBvZ5eZ5m8y14=
modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM=
modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A=
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.14.2 h1:ohsW2+e+Qe2To1W6GNezzKGwjXwSax6R+CrhRxVaFbE=
modernc.org/sqlite v1.14.2/go.mod h1:yqfn85u8wVOE6ub5UT8VI9JjhrwBUUCNyTACN0h6Sx8=
modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs=
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
modernc.org/tcl v1.8.13/go.mod h1:V+q/Ef0IJaNUSECieLU4o+8IScapxnMyFV6i/7uQlAY=
modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.2.19/go.mod h1:+ZpP0pc4zz97eukOzW3xagV/lS82IpPN9NGG5pNF9vY=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+3401 -12290
View File
File diff suppressed because it is too large Load Diff
+29 -41
View File
@@ -1,6 +1,6 @@
{
"name": "ezbookkeeping",
"version": "0.2.0",
"version": "0.3.0",
"private": true,
"repository": {
"type": "git",
@@ -12,56 +12,44 @@
"url": "https://github.com/mayswind/ezbookkeeping/issues"
},
"scripts": {
"serve": "set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve",
"build": "set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service build",
"lint": "vue-cli-service lint"
"serve": "cross-env NODE_ENV=development vite",
"build": "cross-env NODE_ENV=production vite build",
"serve:dist": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
},
"dependencies": {
"axios": "^1.3.4",
"@vuepic/vue-datepicker": "^5.1.2",
"axios": "^1.4.0",
"cbor-js": "^0.1.0",
"core-js": "^3.29.1",
"clipboard": "^2.0.11",
"crypto-js": "^4.1.1",
"framework7": "^5.7.14",
"dom7": "^4.0.6",
"framework7": "^8.0.5",
"framework7-icons": "^5.0.5",
"framework7-vue": "^5.7.14",
"js-cookie": "^3.0.1",
"framework7-vue": "^8.0.5",
"js-cookie": "^3.0.5",
"leaflet": "^1.9.4",
"line-awesome": "^1.3.0",
"moment": "^2.29.4",
"moment-timezone": "^0.5.41",
"moment-timezone": "^0.5.43",
"register-service-worker": "^1.7.2",
"ua-parser-js": "^1.0.34",
"vue": "^2.7.14",
"vue-clipboard2": "^0.3.3",
"vue-i18n": "^8.28.2",
"vue-pincode-input": "^0.4.0",
"vuex": "^3.6.2"
"skeleton-elements": "^4.0.1",
"swiper": "^9.3.2",
"ua-parser-js": "^1.0.35",
"vue": "^3.3.4",
"vue-i18n": "^9.2.2",
"vuex": "^4.1.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.5.19",
"@vue/cli-plugin-eslint": "^4.5.19",
"@vue/cli-plugin-pwa": "^4.5.19",
"@vue/cli-service": "^4.5.19",
"babel-eslint": "^10.1.0",
"babel-plugin-component": "^1.1.1",
"eslint": "^6.8.0",
"eslint-plugin-vue": "^6.2.2",
"git-revision-webpack-plugin": "^3.0.6",
"moment-locales-webpack-plugin": "^1.2.0",
"vue-template-compiler": "^2.7.14"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
"@vitejs/plugin-vue": "^4.2.0",
"@vue/compiler-sfc": "^3.3.4",
"cross-env": "^7.0.3",
"eslint": "^8.41.0",
"eslint-plugin-vue": "^9.14.1",
"git-rev-sync": "^3.0.2",
"postcss-preset-env": "^8.4.1",
"vite": "^4.3.5",
"vite-plugin-pwa": "^0.15.1"
},
"browserslist": [
"> 1%",
+40
View File
@@ -0,0 +1,40 @@
package api
import (
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
)
const openStreetMapTileImageUrlFormat = "https://tile.openstreetmap.org/%s/%s/%s" // https://tile.openstreetmap.org/{z}/{x}/{y}.png
// MapImageProxy represents map image proxy
type MapImageProxy struct {
}
// Initialize a map image proxy singleton instance
var (
MapImages = &MapImageProxy{}
)
// OpenStreetMapTileImageProxyHandler returns open street map tile image
func (p *MapImageProxy) OpenStreetMapTileImageProxyHandler(c *core.Context) (*httputil.ReverseProxy, *errs.Error) {
director := func(req *http.Request) {
zoomLevel := c.Param("zoomLevel")
coordinateX := c.Param("coordinateX")
fileName := c.Param("fileName")
imageRawUrl := fmt.Sprintf(openStreetMapTileImageUrlFormat, zoomLevel, coordinateX, fileName)
imageUrl, _ := url.Parse(imageRawUrl)
req.URL = imageUrl
req.RequestURI = req.URL.RequestURI()
req.Host = imageUrl.Host
}
return &httputil.ReverseProxy{Director: director}, nil
}
+7 -14
View File
@@ -48,7 +48,7 @@ func (a *TokensApi) TokenListHandler(c *core.Context) (interface{}, *errs.Error)
ExpiredAt: token.ExpiredUnixTime,
}
if utils.Int64ToString(token.Uid) == claims.Id && utils.Int64ToString(token.UserTokenId) == claims.UserTokenId && token.CreatedUnixTime == claims.IssuedAt {
if token.Uid == claims.Uid && utils.Int64ToString(token.UserTokenId) == claims.UserTokenId && token.CreatedUnixTime == claims.IssuedAt {
tokenResp.IsCurrent = true
}
@@ -62,17 +62,10 @@ func (a *TokensApi) TokenListHandler(c *core.Context) (interface{}, *errs.Error)
// TokenRevokeCurrentHandler revokes current token of current user
func (a *TokensApi) TokenRevokeCurrentHandler(c *core.Context) (interface{}, *errs.Error) {
_, claims, err := a.tokens.ParseToken(c)
_, claims, err := a.tokens.ParseTokenByHeader(c)
if err != nil {
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid, err := utils.StringToInt64(claims.Id)
if err != nil {
log.WarnfWithRequestId(c, "[tokens.TokenRevokeCurrentHandler] parse user id failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
return nil, errs.Or(err, errs.NewIncompleteOrIncorrectSubmissionError(err))
}
userTokenId, err := utils.StringToInt64(claims.UserTokenId)
@@ -83,7 +76,7 @@ func (a *TokensApi) TokenRevokeCurrentHandler(c *core.Context) (interface{}, *er
}
tokenRecord := &models.TokenRecord{
Uid: uid,
Uid: claims.Uid,
UserTokenId: userTokenId,
CreatedUnixTime: claims.IssuedAt,
}
@@ -92,11 +85,11 @@ func (a *TokensApi) TokenRevokeCurrentHandler(c *core.Context) (interface{}, *er
err = a.tokens.DeleteToken(tokenRecord)
if err != nil {
log.ErrorfWithRequestId(c, "[token.TokenRevokeCurrentHandler] failed to revoke token \"id:%s\" for user \"uid:%d\", because %s", tokenId, uid, err.Error())
log.ErrorfWithRequestId(c, "[token.TokenRevokeCurrentHandler] failed to revoke token \"id:%s\" for user \"uid:%d\", because %s", tokenId, claims.Uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[token.TokenRevokeCurrentHandler] user \"uid:%d\" has revoked token \"id:%s\"", uid, tokenId)
log.InfofWithRequestId(c, "[token.TokenRevokeCurrentHandler] user \"uid:%d\" has revoked token \"id:%s\"", claims.Uid, tokenId)
return true, nil
}
@@ -154,7 +147,7 @@ func (a *TokensApi) TokenRevokeAllHandler(c *core.Context) (interface{}, *errs.E
for i := 0; i < len(tokens); i++ {
token := tokens[i]
if utils.Int64ToString(token.Uid) == claims.Id && utils.Int64ToString(token.UserTokenId) == claims.UserTokenId && token.CreatedUnixTime == claims.IssuedAt {
if token.Uid == claims.Uid && utils.Int64ToString(token.UserTokenId) == claims.UserTokenId && token.CreatedUnixTime == claims.IssuedAt {
currentTokenIndex = i
break
}
+28
View File
@@ -62,6 +62,11 @@ func (a *TransactionsApi) TransactionCountHandler(c *core.Context) (interface{},
totalCount, err := a.transactions.GetTransactionCount(uid, transactionCountReq.MaxTime, transactionCountReq.MinTime, transactionCountReq.Type, allCategoryIds, allAccountIds, transactionCountReq.Keyword)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionCountHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
countResp := &models.TransactionCountResponse{
TotalCount: totalCount,
}
@@ -229,6 +234,11 @@ func (a *TransactionsApi) TransactionStatisticsHandler(c *core.Context) (interfa
uid := c.GetCurrentUid()
totalAmounts, err := a.transactions.GetAccountsAndCategoriesTotalIncomeAndExpense(uid, statisticReq.StartTime, statisticReq.EndTime)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionStatisticsHandler] failed to get accounts and categories total income and expense for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
statisticResp := &models.TransactionStatisticResponse{
StartTime: statisticReq.StartTime,
EndTime: statisticReq.EndTime,
@@ -394,6 +404,12 @@ func (a *TransactionsApi) TransactionMonthAmountsHandler(c *core.Context) (inter
}
totalAmounts, err := a.transactions.GetAccountsMonthTotalIncomeAndExpense(uid, startTime, endTime, pageCountForLoadTransactionAmounts)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionMonthAmountsHandler] failed to get accounts month total income and expense for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
amountsMap := make(map[string]map[string]*models.TransactionAmountsResponseItemAmountInfo)
for yearMonth, monthAccountsAmounts := range totalAmounts {
@@ -723,6 +739,11 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{}
newTransaction.RelatedAccountAmount = transactionModifyReq.DestinationAmount
}
if transactionModifyReq.GeoLocation != nil {
newTransaction.GeoLongitude = transactionModifyReq.GeoLocation.Longitude
newTransaction.GeoLatitude = transactionModifyReq.GeoLocation.Latitude
}
if newTransaction.CategoryId == transaction.CategoryId &&
utils.GetUnixTimeFromTransactionTime(newTransaction.TransactionTime) == utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime) &&
newTransaction.TimezoneUtcOffset == transaction.TimezoneUtcOffset &&
@@ -732,6 +753,8 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (interface{}
(transaction.Type != models.TRANSACTION_DB_TYPE_TRANSFER_OUT || newTransaction.RelatedAccountAmount == transaction.RelatedAccountAmount) &&
newTransaction.HideAmount == transaction.HideAmount &&
newTransaction.Comment == transaction.Comment &&
newTransaction.GeoLongitude == transaction.GeoLongitude &&
newTransaction.GeoLatitude == transaction.GeoLatitude &&
utils.Int64SliceEquals(tagIds, transactionTagIds) {
return nil, errs.ErrNothingWillBeUpdated
}
@@ -1047,5 +1070,10 @@ func (a *TransactionsApi) createNewTransactionModel(uid int64, transactionCreate
transaction.RelatedAccountAmount = transactionCreateReq.DestinationAmount
}
if transactionCreateReq.GeoLocation != nil {
transaction.GeoLongitude = transactionCreateReq.GeoLocation.Longitude
transaction.GeoLatitude = transactionCreateReq.GeoLocation.Latitude
}
return transaction
}
+57 -15
View File
@@ -57,6 +57,7 @@ func (a *UsersApi) UserRegisterHandler(c *core.Context) (interface{}, *errs.Erro
Email: userRegisterReq.Email,
Nickname: userRegisterReq.Nickname,
Password: userRegisterReq.Password,
Language: userRegisterReq.Language,
DefaultCurrency: userRegisterReq.DefaultCurrency,
FirstDayOfWeek: userRegisterReq.FirstDayOfWeek,
TransactionEditScope: models.TRANSACTION_EDIT_SCOPE_ALL,
@@ -161,12 +162,6 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (interface{}, *errs
anythingUpdate = true
}
if userUpdateReq.DefaultCurrency != "" && userUpdateReq.DefaultCurrency != user.DefaultCurrency {
user.DefaultCurrency = userUpdateReq.DefaultCurrency
userNew.DefaultCurrency = userUpdateReq.DefaultCurrency
anythingUpdate = true
}
if userUpdateReq.DefaultAccountId > 0 && userUpdateReq.DefaultAccountId != user.DefaultAccountId {
accounts, err := a.accounts.GetAccountsByAccountIds(uid, []int64{userUpdateReq.DefaultAccountId})
@@ -179,14 +174,6 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (interface{}, *errs
anythingUpdate = true
}
if userUpdateReq.FirstDayOfWeek != nil && *userUpdateReq.FirstDayOfWeek != user.FirstDayOfWeek {
user.FirstDayOfWeek = *userUpdateReq.FirstDayOfWeek
userNew.FirstDayOfWeek = *userUpdateReq.FirstDayOfWeek
anythingUpdate = true
} else {
userNew.FirstDayOfWeek = models.WEEKDAY_INVALID
}
if userUpdateReq.TransactionEditScope != nil && *userUpdateReq.TransactionEditScope != user.TransactionEditScope {
user.TransactionEditScope = *userUpdateReq.TransactionEditScope
userNew.TransactionEditScope = *userUpdateReq.TransactionEditScope
@@ -195,11 +182,66 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (interface{}, *errs
userNew.TransactionEditScope = models.TRANSACTION_EDIT_SCOPE_INVALID
}
modifyUserLanguage := false
if userUpdateReq.Language != user.Language {
user.Language = userUpdateReq.Language
userNew.Language = userUpdateReq.Language
modifyUserLanguage = true
anythingUpdate = true
}
if userUpdateReq.DefaultCurrency != "" && userUpdateReq.DefaultCurrency != user.DefaultCurrency {
user.DefaultCurrency = userUpdateReq.DefaultCurrency
userNew.DefaultCurrency = userUpdateReq.DefaultCurrency
anythingUpdate = true
}
if userUpdateReq.FirstDayOfWeek != nil && *userUpdateReq.FirstDayOfWeek != user.FirstDayOfWeek {
user.FirstDayOfWeek = *userUpdateReq.FirstDayOfWeek
userNew.FirstDayOfWeek = *userUpdateReq.FirstDayOfWeek
anythingUpdate = true
} else {
userNew.FirstDayOfWeek = models.WEEKDAY_INVALID
}
if userUpdateReq.LongDateFormat != nil && *userUpdateReq.LongDateFormat != user.LongDateFormat {
user.LongDateFormat = *userUpdateReq.LongDateFormat
userNew.LongDateFormat = *userUpdateReq.LongDateFormat
anythingUpdate = true
} else {
userNew.LongDateFormat = models.LONG_DATE_FORMAT_INVALID
}
if userUpdateReq.ShortDateFormat != nil && *userUpdateReq.ShortDateFormat != user.ShortDateFormat {
user.ShortDateFormat = *userUpdateReq.ShortDateFormat
userNew.ShortDateFormat = *userUpdateReq.ShortDateFormat
anythingUpdate = true
} else {
userNew.ShortDateFormat = models.SHORT_DATE_FORMAT_INVALID
}
if userUpdateReq.LongTimeFormat != nil && *userUpdateReq.LongTimeFormat != user.LongTimeFormat {
user.LongTimeFormat = *userUpdateReq.LongTimeFormat
userNew.LongTimeFormat = *userUpdateReq.LongTimeFormat
anythingUpdate = true
} else {
userNew.LongTimeFormat = models.LONG_TIME_FORMAT_INVALID
}
if userUpdateReq.ShortTimeFormat != nil && *userUpdateReq.ShortTimeFormat != user.ShortTimeFormat {
user.ShortTimeFormat = *userUpdateReq.ShortTimeFormat
userNew.ShortTimeFormat = *userUpdateReq.ShortTimeFormat
anythingUpdate = true
} else {
userNew.ShortTimeFormat = models.SHORT_TIME_FORMAT_INVALID
}
if !anythingUpdate {
return nil, errs.ErrNothingWillBeUpdated
}
keyProfileUpdated, err := a.users.UpdateUser(userNew)
keyProfileUpdated, err := a.users.UpdateUser(userNew, modifyUserLanguage)
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error())
+1 -1
View File
@@ -142,7 +142,7 @@ func (l *UserDataCli) ModifyUserPassword(c *cli.Context, username string, passwo
Password: password,
}
_, err = l.users.UpdateUser(userNew)
_, err = l.users.UpdateUser(userNew, false)
if err != nil {
log.BootErrorf("[user_data.ModifyUserPassword] failed to update user \"%s\" password, because %s", user.Username, err.Error())
+1 -7
View File
@@ -61,13 +61,7 @@ func (c *Context) GetCurrentUid() int64 {
return 0
}
uid, err := strconv.ParseInt(claims.Id, 10, 64)
if err != nil {
return 0
}
return uid
return claims.Uid
}
// GetClientTimezoneOffset returns the client timezone offset
+8 -1
View File
@@ -1,6 +1,10 @@
package core
import "github.com/mayswind/ezbookkeeping/pkg/errs"
import (
"net/http/httputil"
"github.com/mayswind/ezbookkeeping/pkg/errs"
)
// MiddlewareHandlerFunc represents the middleware handler function
type MiddlewareHandlerFunc func(*Context)
@@ -10,3 +14,6 @@ type ApiHandlerFunc func(*Context) (interface{}, *errs.Error)
// DataHandlerFunc represents the handler function that returns byte array
type DataHandlerFunc func(*Context) ([]byte, string, *errs.Error)
// ProxyHandlerFunc represents the reverse proxy handler function
type ProxyHandlerFunc func(*Context) (*httputil.ReverseProxy, *errs.Error)
+40 -2
View File
@@ -1,7 +1,9 @@
package core
import (
"github.com/golang-jwt/jwt/v4"
"time"
"github.com/golang-jwt/jwt/v5"
)
// TokenType represents token type
@@ -16,7 +18,43 @@ const (
// UserTokenClaims represents user token
type UserTokenClaims struct {
UserTokenId string `json:"userTokenId"`
Uid int64 `json:"jti,string"`
Username string `json:"username,omitempty"`
Type TokenType `json:"type"`
jwt.StandardClaims
IssuedAt int64 `json:"iat"`
ExpiresAt int64 `json:"exp"`
}
// GetExpirationTime returns the expiration time of this token
func (c *UserTokenClaims) GetExpirationTime() (*jwt.NumericDate, error) {
return &jwt.NumericDate{
Time: time.Unix(c.ExpiresAt, 0),
}, nil
}
// GetIssuedAt returns the issue time of this token
func (c *UserTokenClaims) GetIssuedAt() (*jwt.NumericDate, error) {
return &jwt.NumericDate{
Time: time.Unix(c.IssuedAt, 0),
}, nil
}
// GetNotBefore returns the earliest valid time of this token
func (c *UserTokenClaims) GetNotBefore() (*jwt.NumericDate, error) {
return &jwt.NumericDate{}, nil
}
// GetIssuer returns the issuer of this token
func (c *UserTokenClaims) GetIssuer() (string, error) {
return "", nil
}
// GetSubject returns the subject of this token
func (c *UserTokenClaims) GetSubject() (string, error) {
return "", nil
}
// GetAudience returns the audience of this token
func (c *UserTokenClaims) GetAudience() (jwt.ClaimStrings, error) {
return jwt.ClaimStrings{}, nil
}
+1
View File
@@ -9,4 +9,5 @@ var (
ErrGettingLocalAddress = NewSystemError(SystemSubcategorySetting, 2, http.StatusInternalServerError, "failed to get local address")
ErrInvalidUuidMode = NewSystemError(SystemSubcategorySetting, 3, http.StatusInternalServerError, "invalid uuid mode")
ErrInvalidExchangeRatesDataSource = NewSystemError(SystemSubcategorySetting, 4, http.StatusInternalServerError, "invalid exchange rates data source")
ErrInvalidMapProvider = NewSystemError(SystemSubcategorySetting, 5, http.StatusInternalServerError, "invalid map provider")
)
+1
View File
@@ -18,4 +18,5 @@ var (
ErrInvalidUserTokenId = NewNormalError(NormalSubcategoryToken, 9, http.StatusBadRequest, "user token id is invalid")
ErrTokenRecordNotFound = NewNormalError(NormalSubcategoryToken, 10, http.StatusBadRequest, "token is not found")
ErrTokenExpired = NewNormalError(NormalSubcategoryToken, 11, http.StatusBadRequest, "token is expired")
ErrTokenIsEmpty = NewNormalError(NormalSubcategoryToken, 12, http.StatusBadRequest, "token is empty")
)
@@ -32,6 +32,9 @@ func InitializeExchangeRatesDataSource(config *settings.Config) error {
} else if config.ExchangeRatesDataSource == settings.NationalBankOfPolandDataSource {
Container.Current = &NationalBankOfPolandDataSource{}
return nil
} else if config.ExchangeRatesDataSource == settings.MonetaryAuthorityOfSingaporeDataSource {
Container.Current = &MonetaryAuthorityOfSingaporeDataSource{}
return nil
}
return errs.ErrInvalidExchangeRatesDataSource
@@ -0,0 +1,179 @@
package exchangerates
import (
"encoding/json"
"math"
"strings"
"time"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
"github.com/mayswind/ezbookkeeping/pkg/validators"
)
const monetaryAuthorityOfSingaporeExchangeRateUrl = "https://eservices.mas.gov.sg/api/action/datastore/search.json?resource_id=95932927-c8bc-4e7a-b484-68a66a24edfe&sort=end_of_day+desc&limit=1"
const monetaryAuthorityOfSingaporeExchangeRateReferenceUrl = "https://eservices.mas.gov.sg/Statistics/msb/ExchangeRates.aspx"
const monetaryAuthorityOfSingaporeDataSource = "Monetary Authority of Singapore"
const monetaryAuthorityOfSingaporeBaseCurrency = "SGD"
const monetaryAuthorityOfSingaporeDataUpdateDateFormat = "2006-01-02 15"
const monetaryAuthorityOfSingaporeDataUpdateDateTimezone = "Asia/Singapore"
// MonetaryAuthorityOfSingaporeDataSource defines the structure of exchange rates data source of Monetary Authority of Singapore
type MonetaryAuthorityOfSingaporeDataSource struct {
ExchangeRatesDataSource
}
// MonetaryAuthorityOfSingaporeExchangeRateData represents the whole data from Monetary Authority of Singapore
type MonetaryAuthorityOfSingaporeExchangeRateData struct {
Success bool `json:"success"`
Result *MonetaryAuthorityOfSingaporeResult `json:"result"`
}
// MonetaryAuthorityOfSingaporeResult represents the actual result from Monetary Authority of Singapore
type MonetaryAuthorityOfSingaporeResult struct {
Records []MonetaryAuthorityOfSingaporeRecord `json:"records"`
}
// MonetaryAuthorityOfSingaporeRecord represents the record from Monetary Authority of Singapore
type MonetaryAuthorityOfSingaporeRecord map[string]string
// ToLatestExchangeRateResponse returns a view-object according to original data from Monetary Authority of Singapore
func (e *MonetaryAuthorityOfSingaporeExchangeRateData) ToLatestExchangeRateResponse(c *core.Context) *models.LatestExchangeRateResponse {
if !e.Success {
log.ErrorfWithRequestId(c, "[monetary_authority_of_singapore_datasource.ToLatestExchangeRateResponse] response is not success")
return nil
}
if e.Result == nil {
log.ErrorfWithRequestId(c, "[monetary_authority_of_singapore_datasource.ToLatestExchangeRateResponse] result is null")
return nil
}
if len(e.Result.Records) < 1 {
log.ErrorfWithRequestId(c, "[monetary_authority_of_singapore_datasource.ToLatestExchangeRateResponse] records is empty")
return nil
}
lastDayRecord := e.Result.Records[0]
exchangeRates := make(models.LatestExchangeRateSlice, 0, len(lastDayRecord))
latestUpdateDate := ""
for key, value := range lastDayRecord {
if key == "end_of_day" {
latestUpdateDate = value
continue
}
exchangeRate := e.parseExchangeRateResponse(c, key, value)
if exchangeRate == nil {
continue
}
exchangeRates = append(exchangeRates, exchangeRate)
}
timezone, err := time.LoadLocation(monetaryAuthorityOfSingaporeDataUpdateDateTimezone)
if err != nil {
log.ErrorfWithRequestId(c, "[monetary_authority_of_singapore_datasource.ToLatestExchangeRateResponse] failed to get timezone, timezone name is %s", monetaryAuthorityOfSingaporeDataUpdateDateTimezone)
return nil
}
updateDateTime := latestUpdateDate + " 12" // These rates are the average of buying and selling interbank rates quoted around midday in Singapore
updateTime, err := time.ParseInLocation(monetaryAuthorityOfSingaporeDataUpdateDateFormat, updateDateTime, timezone)
if err != nil {
log.ErrorfWithRequestId(c, "[monetary_authority_of_singapore_datasource.ToLatestExchangeRateResponse] failed to parse update date, datetime is %s", updateDateTime)
return nil
}
latestExchangeRateResp := &models.LatestExchangeRateResponse{
DataSource: monetaryAuthorityOfSingaporeDataSource,
ReferenceUrl: monetaryAuthorityOfSingaporeExchangeRateReferenceUrl,
UpdateTime: updateTime.Unix(),
BaseCurrency: monetaryAuthorityOfSingaporeBaseCurrency,
ExchangeRates: exchangeRates,
}
return latestExchangeRateResp
}
func (e *MonetaryAuthorityOfSingaporeExchangeRateData) parseExchangeRateResponse(c *core.Context, key string, value string) *models.LatestExchangeRate {
if !strings.Contains(key, "_") {
return nil
}
items := strings.Split(key, "_")
if len(items) < 2 {
return nil
}
fromCurrencyCode := strings.ToUpper(items[0])
toCurrencyCode := strings.ToUpper(items[1])
if _, exists := validators.AllCurrencyNames[fromCurrencyCode]; !exists {
return nil
}
if toCurrencyCode != monetaryAuthorityOfSingaporeBaseCurrency {
return nil
}
rate, err := utils.StringToFloat64(value)
if err != nil {
log.WarnfWithRequestId(c, "[monetary_authority_of_singapore_datasource.parseExchangeRateResponse] failed to parse rate, rate is %s", value)
return nil
}
if rate <= 0 {
log.WarnfWithRequestId(c, "[monetary_authority_of_singapore_datasource.parseExchangeRateResponse] rate is invalid, rate is %s", value)
return nil
}
finalRate := 1 / rate
if math.IsInf(finalRate, 0) {
return nil
}
if len(items) == 3 && items[2] == "100" {
finalRate = finalRate * 100
}
return &models.LatestExchangeRate{
Currency: fromCurrencyCode,
Rate: utils.Float64ToString(finalRate),
}
}
// GetRequestUrls returns the Monetary Authority of Singapore data source urls
func (e *MonetaryAuthorityOfSingaporeDataSource) GetRequestUrls() []string {
return []string{monetaryAuthorityOfSingaporeExchangeRateUrl}
}
// Parse returns the common response entity according to the Monetary Authority of Singapore data source raw response
func (e *MonetaryAuthorityOfSingaporeDataSource) Parse(c *core.Context, content []byte) (*models.LatestExchangeRateResponse, error) {
monetaryAuthorityOfSingaporeData := &MonetaryAuthorityOfSingaporeExchangeRateData{}
err := json.Unmarshal(content, monetaryAuthorityOfSingaporeData)
if err != nil {
log.ErrorfWithRequestId(c, "[monetary_authority_of_singapore_datasource.Parse] failed to parse json data, content is %s, because %s", string(content), err.Error())
return nil, errs.ErrFailedToRequestRemoteApi
}
latestExchangeRateResponse := monetaryAuthorityOfSingaporeData.ToLatestExchangeRateResponse(c)
if latestExchangeRateResponse == nil {
log.ErrorfWithRequestId(c, "[monetary_authority_of_singapore_datasource.Parse] failed to parse latest exchange rate data, content is %s", string(content))
return nil, errs.ErrFailedToRequestRemoteApi
}
return latestExchangeRateResponse, nil
}
@@ -0,0 +1,206 @@
package exchangerates
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/gin-gonic/gin"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models"
)
const monetaryAuthorityOfSingaporeMinimumRequiredContent = "{\n" +
" \"success\": true,\n" +
" \"result\": {\n" +
" \"records\": [\n" +
" {\n" +
" \"end_of_day\": \"2023-05-26\",\n" +
" \"usd_sgd\": \"1.3528\",\n" +
" \"cny_sgd_100\": \"19.16\"\n" +
" }\n" +
" ]\n" +
" }\n" +
"}"
func TestMonetaryAuthorityOfSingaporeDataSource_StandardDataExtractBaseCurrency(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte(monetaryAuthorityOfSingaporeMinimumRequiredContent))
assert.Equal(t, nil, err)
assert.Equal(t, "SGD", actualLatestExchangeRateResponse.BaseCurrency)
}
func TestMonetaryAuthorityOfSingaporeDataSource_StandardDataExtractExchangeRates(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte(monetaryAuthorityOfSingaporeMinimumRequiredContent))
assert.Equal(t, nil, err)
assert.Contains(t, actualLatestExchangeRateResponse.ExchangeRates, &models.LatestExchangeRate{
Currency: "USD",
Rate: "0.7392075694855116",
})
assert.Contains(t, actualLatestExchangeRateResponse.ExchangeRates, &models.LatestExchangeRate{
Currency: "CNY",
Rate: "5.219206680584551",
})
}
func TestMonetaryAuthorityOfSingaporeDataSource_BlankContent(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
_, err := dataSource.Parse(context, []byte(""))
assert.NotEqual(t, nil, err)
}
func TestMonetaryAuthorityOfSingaporeDataSource_EmptyJsonObject(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
_, err := dataSource.Parse(context, []byte("{}"))
assert.NotEqual(t, nil, err)
}
func TestMonetaryAuthorityOfSingaporeDataSource_ResponseSuccessIsFalseObject(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
_, err := dataSource.Parse(context, []byte("{\n"+
" \"success\": false,\n"+
" \"result\": {\n"+
" \"records\": [\n"+
" {\n"+
" \"end_of_day\": \"2023-05-26\",\n"+
" \"usd_sgd\": \"1.3528\",\n"+
" \"cny_sgd_100\": \"19.16\"\n"+
" }\n"+
" ]\n"+
" }\n"+
"}"))
assert.NotEqual(t, nil, err)
}
func TestMonetaryAuthorityOfSingaporeDataSource_NoResultContent(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
_, err := dataSource.Parse(context, []byte("{\n"+
" \"success\": true\n"+
"}"))
assert.NotEqual(t, nil, err)
}
func TestMonetaryAuthorityOfSingaporeDataSource_EmptyRecordContent(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
_, err := dataSource.Parse(context, []byte("{\n"+
" \"success\": true,\n"+
" \"result\": {\n"+
" \"records\": [\n"+
" ]\n"+
" }\n"+
"}"))
assert.NotEqual(t, nil, err)
}
func TestMonetaryAuthorityOfSingaporeDataSource_TargetCurrencyIsNotBaseCurrency(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("{\n"+
" \"success\": true,\n"+
" \"result\": {\n"+
" \"records\": [\n"+
" {\n"+
" \"end_of_day\": \"2023-05-26\",\n"+
" \"usd_cny\": \"1\""+
" }\n"+
" ]\n"+
" }\n"+
"}"))
assert.Equal(t, nil, err)
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
}
func TestMonetaryAuthorityOfSingaporeDataSource_InvalidCurrency(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("{\n"+
" \"success\": true,\n"+
" \"result\": {\n"+
" \"records\": [\n"+
" {\n"+
" \"end_of_day\": \"2023-05-26\",\n"+
" \"xxx_sgd\": \"1.3528\""+
" }\n"+
" ]\n"+
" }\n"+
"}"))
assert.Equal(t, nil, err)
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
}
func TestMonetaryAuthorityOfSingaporeDataSource_EmptyRate(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("{\n"+
" \"success\": true,\n"+
" \"result\": {\n"+
" \"records\": [\n"+
" {\n"+
" \"end_of_day\": \"2023-05-26\",\n"+
" \"usd_sgd\": \"\""+
" }\n"+
" ]\n"+
" }\n"+
"}"))
assert.Equal(t, nil, err)
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
}
func TestMonetaryAuthorityOfSingaporeDataSource_InvalidRate(t *testing.T) {
dataSource := &MonetaryAuthorityOfSingaporeDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("{\n"+
" \"success\": true,\n"+
" \"result\": {\n"+
" \"records\": [\n"+
" {\n"+
" \"end_of_day\": \"2023-05-26\",\n"+
" \"usd_sgd\": null"+
" }\n"+
" ]\n"+
" }\n"+
"}"))
assert.Equal(t, nil, err)
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
}
+53 -16
View File
@@ -1,7 +1,7 @@
package middlewares
import (
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
@@ -10,11 +10,20 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// TokenSourceType represents token source
type TokenSourceType byte
// Token source types
const (
TOKEN_SOURCE_TYPE_HEADER TokenSourceType = 1
TOKEN_SOURCE_TYPE_ARGUMENT TokenSourceType = 2
)
const tokenQueryStringParam = "token"
// JWTAuthorization verifies whether current request is valid by jwt token
func JWTAuthorization(c *core.Context) {
claims, err := getTokenClaims(c)
claims, err := getTokenClaims(c, TOKEN_SOURCE_TYPE_HEADER)
if err != nil {
utils.PrintJsonErrorResult(c, err)
@@ -22,13 +31,38 @@ func JWTAuthorization(c *core.Context) {
}
if claims.Type == core.USER_TOKEN_TYPE_REQUIRE_2FA {
log.WarnfWithRequestId(c, "[authorization.JWTAuthorization] user \"uid:%s\" token requires 2fa", claims.Id)
log.WarnfWithRequestId(c, "[authorization.JWTAuthorization] user \"uid:%d\" token requires 2fa", claims.Uid)
utils.PrintJsonErrorResult(c, errs.ErrCurrentTokenRequire2FA)
return
}
if claims.Type != core.USER_TOKEN_TYPE_NORMAL {
log.WarnfWithRequestId(c, "[authorization.JWTAuthorization] user \"uid:%s\" token type is invalid", claims.Id)
log.WarnfWithRequestId(c, "[authorization.JWTAuthorization] user \"uid:%d\" token type is invalid", claims.Uid)
utils.PrintJsonErrorResult(c, errs.ErrCurrentInvalidTokenType)
return
}
c.SetTokenClaims(claims)
c.Next()
}
// JWTAuthorizationByQueryString verifies whether current request is valid by jwt token
func JWTAuthorizationByQueryString(c *core.Context) {
claims, err := getTokenClaims(c, TOKEN_SOURCE_TYPE_ARGUMENT)
if err != nil {
utils.PrintJsonErrorResult(c, err)
return
}
if claims.Type == core.USER_TOKEN_TYPE_REQUIRE_2FA {
log.WarnfWithRequestId(c, "[authorization.JWTAuthorizationByQueryString] user \"uid:%d\" token requires 2fa", claims.Uid)
utils.PrintJsonErrorResult(c, errs.ErrCurrentTokenRequire2FA)
return
}
if claims.Type != core.USER_TOKEN_TYPE_NORMAL {
log.WarnfWithRequestId(c, "[authorization.JWTAuthorizationByQueryString] user \"uid:%d\" token type is invalid", claims.Uid)
utils.PrintJsonErrorResult(c, errs.ErrCurrentInvalidTokenType)
return
}
@@ -39,7 +73,7 @@ func JWTAuthorization(c *core.Context) {
// JWTTwoFactorAuthorization verifies whether current request is valid by 2fa passcode
func JWTTwoFactorAuthorization(c *core.Context) {
claims, err := getTokenClaims(c)
claims, err := getTokenClaims(c, TOKEN_SOURCE_TYPE_HEADER)
if err != nil {
utils.PrintJsonErrorResult(c, err)
@@ -47,7 +81,7 @@ func JWTTwoFactorAuthorization(c *core.Context) {
}
if claims.Type != core.USER_TOKEN_TYPE_REQUIRE_2FA {
log.WarnfWithRequestId(c, "[authorization.JWTTwoFactorAuthorization] user \"uid:%s\" token is not need two factor authorization", claims.Id)
log.WarnfWithRequestId(c, "[authorization.JWTTwoFactorAuthorization] user \"uid:%d\" token is not need two factor authorization", claims.Uid)
utils.PrintJsonErrorResult(c, errs.ErrCurrentTokenNotRequire2FA)
return
}
@@ -56,12 +90,12 @@ func JWTTwoFactorAuthorization(c *core.Context) {
c.Next()
}
func getTokenClaims(c *core.Context) (*core.UserTokenClaims, *errs.Error) {
token, claims, err := services.Tokens.ParseToken(c)
func getTokenClaims(c *core.Context, source TokenSourceType) (*core.UserTokenClaims, *errs.Error) {
token, claims, err := parseToken(c, source)
if err != nil {
log.WarnfWithRequestId(c, "[authorization.getTokenClaims] failed to parse token, because %s", err.Error())
return nil, errs.ErrUnauthorizedAccess
return nil, errs.Or(err, errs.ErrUnauthorizedAccess)
}
if !token.Valid {
@@ -69,15 +103,18 @@ func getTokenClaims(c *core.Context) (*core.UserTokenClaims, *errs.Error) {
return nil, errs.ErrCurrentInvalidToken
}
if !claims.VerifyExpiresAt(time.Now().Unix(), true) {
log.WarnfWithRequestId(c, "[authorization.getTokenClaims] token is expired")
return nil, errs.ErrCurrentTokenExpired
}
if claims.Id == "" {
log.WarnfWithRequestId(c, "[authorization.getTokenClaims] user id in token is empty")
if claims.Uid <= 0 {
log.WarnfWithRequestId(c, "[authorization.getTokenClaims] user id in token is invalid")
return nil, errs.ErrCurrentInvalidToken
}
return claims, nil
}
func parseToken(c *core.Context, source TokenSourceType) (*jwt.Token, *core.UserTokenClaims, error) {
if source == TOKEN_SOURCE_TYPE_ARGUMENT {
return services.Tokens.ParseTokenByArgument(c, tokenQueryStringParam)
}
return services.Tokens.ParseTokenByHeader(c)
}
+2 -1
View File
@@ -5,6 +5,7 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// RequestLog logs the http request log
@@ -28,7 +29,7 @@ func RequestLog(c *core.Context) {
method := c.Request.Method
if claims != nil {
userId = claims.Id
userId = utils.Int64ToString(claims.Uid)
}
if err != nil {
@@ -16,6 +16,8 @@ func ServerSettingsCookie(config *settings.Config) core.MiddlewareHandlerFunc {
settingsArr := []string{
buildBooleanSetting("r", config.EnableUserRegister),
buildBooleanSetting("e", config.EnableDataExport),
buildStringSetting("m", config.MapProvider),
buildBooleanSetting("mp", config.EnableMapDataFetchProxy),
}
bundledSettings := strings.Join(settingsArr, "_")
@@ -25,6 +27,10 @@ func ServerSettingsCookie(config *settings.Config) core.MiddlewareHandlerFunc {
}
}
func buildStringSetting(key string, value string) string {
return fmt.Sprintf("%s.%s", key, strings.Replace(value, ".", "-", -1))
}
func buildBooleanSetting(key string, value bool) string {
if value {
return fmt.Sprintf("%s.1", key)
+162
View File
@@ -0,0 +1,162 @@
package models
import "fmt"
// WeekDay represents week day
type WeekDay byte
// Week days
const (
WEEKDAY_SUNDAY WeekDay = 0
WEEKDAY_MONDAY WeekDay = 1
WEEKDAY_TUESDAY WeekDay = 2
WEEKDAY_WEDNESDAY WeekDay = 3
WEEKDAY_THURSDAY WeekDay = 4
WEEKDAY_FRIDAY WeekDay = 5
WEEKDAY_SATURDAY WeekDay = 6
WEEKDAY_INVALID WeekDay = 255
)
// String returns a textual representation of the week day enum
func (d WeekDay) String() string {
switch d {
case WEEKDAY_SUNDAY:
return "Sunday"
case WEEKDAY_MONDAY:
return "Monday"
case WEEKDAY_TUESDAY:
return "Tuesday"
case WEEKDAY_WEDNESDAY:
return "Wednesday"
case WEEKDAY_THURSDAY:
return "Thursday"
case WEEKDAY_FRIDAY:
return "Friday"
case WEEKDAY_SATURDAY:
return "Saturday"
case WEEKDAY_INVALID:
return "Invalid"
default:
return fmt.Sprintf("Invalid(%d)", int(d))
}
}
// LongDateFormat represents long date format
type LongDateFormat byte
// Long Date Format
const (
LONG_DATE_FORMAT_DEFAULT LongDateFormat = 0
LONG_DATE_FORMAT_YYYY_M_D LongDateFormat = 1
LONG_DATE_FORMAT_M_D_YYYY LongDateFormat = 2
LONG_DATE_FORMAT_D_M_YYYY LongDateFormat = 3
LONG_DATE_FORMAT_INVALID LongDateFormat = 255
)
// String returns a textual representation of the long date format enum
func (f LongDateFormat) String() string {
switch f {
case LONG_DATE_FORMAT_DEFAULT:
return "Default"
case LONG_DATE_FORMAT_YYYY_M_D:
return "YYYY_MM_D"
case LONG_DATE_FORMAT_M_D_YYYY:
return "M_D_YYYY"
case LONG_DATE_FORMAT_D_M_YYYY:
return "D_M_YYYY"
case LONG_DATE_FORMAT_INVALID:
return "Invalid"
default:
return fmt.Sprintf("Invalid(%d)", int(f))
}
}
// ShortDateFormat represents short date format
type ShortDateFormat byte
// Short Date Format
const (
SHORT_DATE_FORMAT_DEFAULT ShortDateFormat = 0
SHORT_DATE_FORMAT_YYYY_M_D ShortDateFormat = 1
SHORT_DATE_FORMAT_M_D_YYYY ShortDateFormat = 2
SHORT_DATE_FORMAT_D_M_YYYY ShortDateFormat = 3
SHORT_DATE_FORMAT_INVALID ShortDateFormat = 255
)
// String returns a textual representation of the short date format enum
func (f ShortDateFormat) String() string {
switch f {
case SHORT_DATE_FORMAT_DEFAULT:
return "Default"
case SHORT_DATE_FORMAT_YYYY_M_D:
return "YYYY_MM_D"
case SHORT_DATE_FORMAT_M_D_YYYY:
return "M_D_YYYY"
case SHORT_DATE_FORMAT_D_M_YYYY:
return "D_M_YYYY"
case SHORT_DATE_FORMAT_INVALID:
return "Invalid"
default:
return fmt.Sprintf("Invalid(%d)", int(f))
}
}
// LongTimeFormat represents long time format
type LongTimeFormat byte
// Long Time Format
const (
LONG_TIME_FORMAT_DEFAULT LongTimeFormat = 0
LONG_TIME_FORMAT_HH_MM_SS LongTimeFormat = 1
LONG_TIME_FORMAT_A_HH_MM_SS LongTimeFormat = 2
LONG_TIME_FORMAT_HH_MM_SS_A LongTimeFormat = 3
LONG_TIME_FORMAT_INVALID LongTimeFormat = 255
)
// String returns a textual representation of the long time format enum
func (f LongTimeFormat) String() string {
switch f {
case LONG_TIME_FORMAT_DEFAULT:
return "Default"
case LONG_TIME_FORMAT_HH_MM_SS:
return "HH_MM_SS"
case LONG_TIME_FORMAT_A_HH_MM_SS:
return "A_HH_MM_SS"
case LONG_TIME_FORMAT_HH_MM_SS_A:
return "HH_MM_SS_A"
case LONG_TIME_FORMAT_INVALID:
return "Invalid"
default:
return fmt.Sprintf("Invalid(%d)", int(f))
}
}
// ShortTimeFormat represents short time format
type ShortTimeFormat byte
// Short Time Format
const (
SHORT_TIME_FORMAT_DEFAULT ShortTimeFormat = 0
SHORT_TIME_FORMAT_HH_MM ShortTimeFormat = 1
SHORT_TIME_FORMAT_A_HH_MM ShortTimeFormat = 2
SHORT_TIME_FORMAT_HH_MM_A ShortTimeFormat = 3
SHORT_TIME_FORMAT_INVALID ShortTimeFormat = 255
)
// String returns a textual representation of the short time format enum
func (f ShortTimeFormat) String() string {
switch f {
case SHORT_TIME_FORMAT_DEFAULT:
return "Default"
case SHORT_TIME_FORMAT_HH_MM:
return "HH_MM"
case SHORT_TIME_FORMAT_A_HH_MM:
return "A_HH_MM"
case SHORT_TIME_FORMAT_HH_MM_A:
return "HH_MM_A"
case SHORT_TIME_FORMAT_INVALID:
return "Invalid"
default:
return fmt.Sprintf("Invalid(%d)", int(f))
}
}
+51 -24
View File
@@ -35,8 +35,8 @@ const (
// Transaction represents transaction data stored in database
type Transaction struct {
TransactionId int64 `xorm:"PK"`
Uid int64 `xorm:"UNIQUE(UQE_transaction_uid_time) INDEX(IDX_transaction_uid_deleted_time) INDEX(IDX_transaction_uid_deleted_type_time) INDEX(IDX_transaction_uid_deleted_category_id_time) INDEX(IDX_transaction_uid_deleted_account_id_time) NOT NULL"`
Deleted bool `xorm:"INDEX(IDX_transaction_uid_deleted_time) INDEX(IDX_transaction_uid_deleted_type_time) INDEX(IDX_transaction_uid_deleted_category_id_time) INDEX(IDX_transaction_uid_deleted_account_id_time) NOT NULL"`
Uid int64 `xorm:"UNIQUE(UQE_transaction_uid_time) INDEX(IDX_transaction_uid_deleted_time) INDEX(IDX_transaction_uid_deleted_type_time) INDEX(IDX_transaction_uid_deleted_category_id_time) INDEX(IDX_transaction_uid_deleted_account_id_time) INDEX(IDX_transaction_uid_deleted_time_longitude_latitude) NOT NULL"`
Deleted bool `xorm:"INDEX(IDX_transaction_uid_deleted_time) INDEX(IDX_transaction_uid_deleted_type_time) INDEX(IDX_transaction_uid_deleted_category_id_time) INDEX(IDX_transaction_uid_deleted_account_id_time) INDEX(IDX_transaction_uid_deleted_time_longitude_latitude) NOT NULL"`
Type TransactionDbType `xorm:"INDEX(IDX_transaction_uid_deleted_type_time) NOT NULL"`
CategoryId int64 `xorm:"INDEX(IDX_transaction_uid_deleted_category_id_time) NOT NULL"`
AccountId int64 `xorm:"INDEX(IDX_transaction_uid_deleted_account_id_time) NOT NULL"`
@@ -48,40 +48,50 @@ type Transaction struct {
RelatedAccountAmount int64 `xorm:"NOT NULL"`
HideAmount bool `xorm:"NOT NULL"`
Comment string `xorm:"VARCHAR(255) NOT NULL"`
GeoLongitude float64 `xorm:"INDEX(IDX_transaction_uid_deleted_time_longitude_latitude)"`
GeoLatitude float64 `xorm:"INDEX(IDX_transaction_uid_deleted_time_longitude_latitude)"`
CreatedIp string `xorm:"VARCHAR(39)"`
CreatedUnixTime int64
UpdatedUnixTime int64
DeletedUnixTime int64
}
// TransactionGeoLocationRequest represents all parameters of transaction geographic location info update request
type TransactionGeoLocationRequest struct {
Latitude float64 `json:"latitude" binding:"required"`
Longitude float64 `json:"longitude" binding:"required"`
}
// TransactionCreateRequest represents all parameters of transaction creation request
type TransactionCreateRequest struct {
Type TransactionType `json:"type" binding:"required"`
CategoryId int64 `json:"categoryId,string"`
Time int64 `json:"time" binding:"required,min=1"`
UtcOffset int16 `json:"utcOffset" binding:"min=-720,max=840"`
SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"`
SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"`
DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"`
HideAmount bool `json:"hideAmount"`
TagIds []string `json:"tagIds"`
Comment string `json:"comment" binding:"max=255"`
Type TransactionType `json:"type" binding:"required"`
CategoryId int64 `json:"categoryId,string"`
Time int64 `json:"time" binding:"required,min=1"`
UtcOffset int16 `json:"utcOffset" binding:"min=-720,max=840"`
SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"`
SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"`
DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"`
HideAmount bool `json:"hideAmount"`
TagIds []string `json:"tagIds"`
Comment string `json:"comment" binding:"max=255"`
GeoLocation *TransactionGeoLocationRequest `json:"geoLocation" binding:"omitempty"`
}
// TransactionModifyRequest represents all parameters of transaction modification request
type TransactionModifyRequest struct {
Id int64 `json:"id,string" binding:"required,min=1"`
CategoryId int64 `json:"categoryId,string"`
Time int64 `json:"time" binding:"required,min=1"`
UtcOffset int16 `json:"utcOffset" binding:"min=-720,max=840"`
SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"`
SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"`
DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"`
HideAmount bool `json:"hideAmount"`
TagIds []string `json:"tagIds"`
Comment string `json:"comment" binding:"max=255"`
Id int64 `json:"id,string" binding:"required,min=1"`
CategoryId int64 `json:"categoryId,string"`
Time int64 `json:"time" binding:"required,min=1"`
UtcOffset int16 `json:"utcOffset" binding:"min=-720,max=840"`
SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"`
SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"`
DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"`
HideAmount bool `json:"hideAmount"`
TagIds []string `json:"tagIds"`
Comment string `json:"comment" binding:"max=255"`
GeoLocation *TransactionGeoLocationRequest `json:"geoLocation" binding:"omitempty"`
}
// TransactionCountRequest represents transaction count request
@@ -170,6 +180,12 @@ type TransactionAccountAmount struct {
TotalExpenseAmount int64
}
// TransactionGeoLocationResponse represents a view-object of transaction geographic location info
type TransactionGeoLocationResponse struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
}
// TransactionInfoResponse represents a view-object of transaction
type TransactionInfoResponse struct {
Id int64 `json:"id,string"`
@@ -189,6 +205,7 @@ type TransactionInfoResponse struct {
TagIds []string `json:"tagIds"`
Tags []*TransactionTagInfoResponse `json:"tags,omitempty"`
Comment string `json:"comment"`
GeoLocation *TransactionGeoLocationResponse `json:"geoLocation,omitempty"`
Editable bool `json:"editable"`
}
@@ -298,6 +315,15 @@ func (t *Transaction) ToTransactionInfoResponse(tagIds []int64, editable bool) *
destinationAmount = t.Amount
}
geoLocation := &TransactionGeoLocationResponse{}
if t.GeoLongitude != 0 || t.GeoLatitude != 0 {
geoLocation.Longitude = t.GeoLongitude
geoLocation.Latitude = t.GeoLatitude
} else {
geoLocation = nil
}
return &TransactionInfoResponse{
Id: t.TransactionId,
TimeSequenceId: t.TransactionTime,
@@ -312,6 +338,7 @@ func (t *Transaction) ToTransactionInfoResponse(tagIds []int64, editable bool) *
HideAmount: t.HideAmount,
TagIds: utils.Int64ArrayToStringArray(tagIds),
Comment: t.Comment,
GeoLocation: geoLocation,
Editable: editable,
}
}
+43 -51
View File
@@ -7,45 +7,6 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// WeekDay represents week day
type WeekDay byte
// Week days
const (
WEEKDAY_SUNDAY WeekDay = 0
WEEKDAY_MONDAY WeekDay = 1
WEEKDAY_TUESDAY WeekDay = 2
WEEKDAY_WEDNESDAY WeekDay = 3
WEEKDAY_THURSDAY WeekDay = 4
WEEKDAY_FRIDAY WeekDay = 5
WEEKDAY_SATURDAY WeekDay = 6
WEEKDAY_INVALID WeekDay = 255
)
// String returns a textual representation of the week day enum
func (d WeekDay) String() string {
switch d {
case WEEKDAY_SUNDAY:
return "Sunday"
case WEEKDAY_MONDAY:
return "Monday"
case WEEKDAY_TUESDAY:
return "Tuesday"
case WEEKDAY_WEDNESDAY:
return "Wednesday"
case WEEKDAY_THURSDAY:
return "Thursday"
case WEEKDAY_FRIDAY:
return "Friday"
case WEEKDAY_SATURDAY:
return "Saturday"
case WEEKDAY_INVALID:
return "Invalid"
default:
return fmt.Sprintf("Invalid(%d)", int(d))
}
}
// TransactionEditScope represents the scope which transaction can be edited
type TransactionEditScope byte
@@ -93,10 +54,15 @@ type User struct {
Nickname string `xorm:"VARCHAR(64) NOT NULL"`
Password string `xorm:"VARCHAR(64) NOT NULL"`
Salt string `xorm:"VARCHAR(10) NOT NULL"`
DefaultCurrency string `xorm:"VARCHAR(3) NOT NULL"`
DefaultAccountId int64
FirstDayOfWeek WeekDay `xorm:"TINYINT NOT NULL"`
TransactionEditScope TransactionEditScope `xorm:"TINYINT NOT NULL"`
Language string `xorm:"VARCHAR(10)"`
DefaultCurrency string `xorm:"VARCHAR(3) NOT NULL"`
FirstDayOfWeek WeekDay `xorm:"TINYINT NOT NULL"`
LongDateFormat LongDateFormat `xorm:"TINYINT"`
ShortDateFormat ShortDateFormat `xorm:"TINYINT"`
LongTimeFormat LongTimeFormat `xorm:"TINYINT"`
ShortTimeFormat ShortTimeFormat `xorm:"TINYINT"`
Deleted bool `xorm:"NOT NULL"`
EmailVerified bool `xorm:"NOT NULL"`
CreatedUnixTime int64
@@ -110,10 +76,15 @@ type UserBasicInfo struct {
Username string `json:"username"`
Email string `json:"email"`
Nickname string `json:"nickname"`
DefaultCurrency string `json:"defaultCurrency"`
DefaultAccountId int64 `json:"defaultAccountId,string"`
FirstDayOfWeek WeekDay `json:"firstDayOfWeek"`
TransactionEditScope TransactionEditScope `json:"transactionEditScope"`
Language string `json:"language"`
DefaultCurrency string `json:"defaultCurrency"`
FirstDayOfWeek WeekDay `json:"firstDayOfWeek"`
LongDateFormat LongDateFormat `json:"longDateFormat"`
ShortDateFormat ShortDateFormat `json:"shortDateFormat"`
LongTimeFormat LongTimeFormat `json:"longTimeFormat"`
ShortTimeFormat ShortTimeFormat `json:"shortTimeFormat"`
}
// UserLoginRequest represents all parameters of user login request
@@ -128,6 +99,7 @@ type UserRegisterRequest struct {
Email string `json:"email" binding:"required,notBlank,max=100,validEmail"`
Nickname string `json:"nickname" binding:"required,notBlank,max=64"`
Password string `json:"password" binding:"required,min=6,max=128"`
Language string `json:"language" binding:"required,min=2,max=16"`
DefaultCurrency string `json:"defaultCurrency" binding:"required,len=3,validCurrency"`
FirstDayOfWeek WeekDay `json:"firstDayOfWeek" binding:"min=0,max=6"`
}
@@ -138,10 +110,15 @@ type UserProfileUpdateRequest struct {
Nickname string `json:"nickname" binding:"omitempty,notBlank,max=64"`
Password string `json:"password" binding:"omitempty,min=6,max=128"`
OldPassword string `json:"oldPassword" binding:"omitempty,min=6,max=128"`
DefaultCurrency string `json:"defaultCurrency" binding:"omitempty,len=3,validCurrency"`
DefaultAccountId int64 `json:"defaultAccountId,string" binding:"omitempty,min=1"`
FirstDayOfWeek *WeekDay `json:"firstDayOfWeek" binding:"omitempty,min=0,max=6"`
TransactionEditScope *TransactionEditScope `json:"transactionEditScope" binding:"omitempty,min=0,max=7"`
Language string `json:"language" binding:"omitempty,min=2,max=16"`
DefaultCurrency string `json:"defaultCurrency" binding:"omitempty,len=3,validCurrency"`
FirstDayOfWeek *WeekDay `json:"firstDayOfWeek" binding:"omitempty,min=0,max=6"`
LongDateFormat *LongDateFormat `json:"longDateFormat" binding:"omitempty,min=0,max=3"`
ShortDateFormat *ShortDateFormat `json:"shortDateFormat" binding:"omitempty,min=0,max=3"`
LongTimeFormat *LongTimeFormat `json:"longTimeFormat" binding:"omitempty,min=0,max=3"`
ShortTimeFormat *ShortTimeFormat `json:"shortTimeFormat" binding:"omitempty,min=0,max=3"`
}
// UserProfileUpdateResponse represents the data returns to frontend after updating profile
@@ -155,10 +132,15 @@ type UserProfileResponse struct {
Username string `json:"username"`
Email string `json:"email"`
Nickname string `json:"nickname"`
DefaultCurrency string `json:"defaultCurrency"`
DefaultAccountId int64 `json:"defaultAccountId,string"`
FirstDayOfWeek WeekDay `json:"firstDayOfWeek"`
TransactionEditScope TransactionEditScope `json:"transactionEditScope"`
Language string `json:"language"`
DefaultCurrency string `json:"defaultCurrency"`
FirstDayOfWeek WeekDay `json:"firstDayOfWeek"`
LongDateFormat LongDateFormat `json:"longDateFormat"`
ShortDateFormat ShortDateFormat `json:"shortDateFormat"`
LongTimeFormat LongTimeFormat `json:"longTimeFormat"`
ShortTimeFormat ShortTimeFormat `json:"shortTimeFormat"`
LastLoginAt int64 `json:"lastLoginAt"`
}
@@ -210,10 +192,15 @@ func (u *User) ToUserBasicInfo() *UserBasicInfo {
Username: u.Username,
Email: u.Email,
Nickname: u.Nickname,
DefaultCurrency: u.DefaultCurrency,
DefaultAccountId: u.DefaultAccountId,
FirstDayOfWeek: u.FirstDayOfWeek,
TransactionEditScope: u.TransactionEditScope,
Language: u.Language,
DefaultCurrency: u.DefaultCurrency,
FirstDayOfWeek: u.FirstDayOfWeek,
LongDateFormat: u.LongDateFormat,
ShortDateFormat: u.ShortDateFormat,
LongTimeFormat: u.LongTimeFormat,
ShortTimeFormat: u.ShortTimeFormat,
}
}
@@ -223,10 +210,15 @@ func (u *User) ToUserProfileResponse() *UserProfileResponse {
Username: u.Username,
Email: u.Email,
Nickname: u.Nickname,
DefaultCurrency: u.DefaultCurrency,
DefaultAccountId: u.DefaultAccountId,
FirstDayOfWeek: u.FirstDayOfWeek,
TransactionEditScope: u.TransactionEditScope,
Language: u.Language,
DefaultCurrency: u.DefaultCurrency,
FirstDayOfWeek: u.FirstDayOfWeek,
LongDateFormat: u.LongDateFormat,
ShortDateFormat: u.ShortDateFormat,
LongTimeFormat: u.LongTimeFormat,
ShortTimeFormat: u.ShortTimeFormat,
LastLoginAt: u.LastLoginUnixTime,
}
}
+73 -54
View File
@@ -6,8 +6,8 @@ import (
"strings"
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/golang-jwt/jwt/v4/request"
"github.com/golang-jwt/jwt/v5"
"github.com/golang-jwt/jwt/v5/request"
"xorm.io/xorm"
"github.com/mayswind/ezbookkeeping/pkg/core"
@@ -63,47 +63,14 @@ func (s *TokenService) GetAllUnexpiredNormalTokensByUid(uid int64) ([]*models.To
return tokenRecords, err
}
// ParseToken returns the token model according to request data
func (s *TokenService) ParseToken(c *core.Context) (*jwt.Token, *core.UserTokenClaims, error) {
claims := &core.UserTokenClaims{}
// ParseTokenByHeader returns the token model according to request data
func (s *TokenService) ParseTokenByHeader(c *core.Context) (*jwt.Token, *core.UserTokenClaims, error) {
return s.parseToken(c, request.BearerExtractor{})
}
token, err := request.ParseFromRequest(c.Request, request.AuthorizationHeaderExtractor,
func(token *jwt.Token) (interface{}, error) {
uid, err := utils.StringToInt64(claims.Id)
now := time.Now().Unix()
if err != nil {
log.WarnfWithRequestId(c, "[tokens.ParseToken] user \"uid:%s\" in token is invalid, because %s", claims.Id, err.Error())
return nil, errs.ErrInvalidToken
}
userTokenId, err := utils.StringToInt64(claims.UserTokenId)
if err != nil {
log.WarnfWithRequestId(c, "[tokens.ParseToken] token \"utid:%s\" in token of user \"uid:%s\" is invalid, because %s", claims.UserTokenId, claims.Id, err.Error())
return nil, errs.ErrInvalidUserTokenId
}
tokenRecord, err := s.getTokenRecord(uid, userTokenId, claims.IssuedAt)
if err != nil {
log.WarnfWithRequestId(c, "[tokens.ParseToken] token \"utid:%s\" of user \"uid:%s\" record not found, because %s", claims.UserTokenId, claims.Id, err.Error())
return nil, errs.ErrTokenRecordNotFound
}
if tokenRecord.ExpiredUnixTime < now {
log.WarnfWithRequestId(c, "[tokens.ParseToken] token \"utid:%s\" of user \"uid:%s\" record is expired", claims.UserTokenId, claims.Id)
return nil, errs.ErrTokenExpired
}
return []byte(tokenRecord.Secret), nil
}, request.WithClaims(claims))
if err != nil {
return nil, nil, err
}
return token, claims, err
// ParseTokenByArgument returns the token model according to request data
func (s *TokenService) ParseTokenByArgument(c *core.Context, tokenParameterName string) (*jwt.Token, *core.UserTokenClaims, error) {
return s.parseToken(c, request.ArgumentExtractor{tokenParameterName})
}
// CreateToken generates a new normal token and saves to database
@@ -163,19 +130,17 @@ func (s *TokenService) DeleteTokens(uid int64, tokenRecords []*models.TokenRecor
// DeleteTokenByClaims deletes given token from database
func (s *TokenService) DeleteTokenByClaims(claims *core.UserTokenClaims) error {
uid, err := utils.StringToInt64(claims.Id)
if err != nil {
return errs.ErrUserIdInvalid
}
userTokenId, err := utils.StringToInt64(claims.UserTokenId)
if err != nil {
return errs.ErrInvalidUserTokenId
}
return s.DeleteToken(&models.TokenRecord{Uid: uid, UserTokenId: userTokenId, CreatedUnixTime: claims.IssuedAt})
return s.DeleteToken(&models.TokenRecord{
Uid: claims.Uid,
UserTokenId: userTokenId,
CreatedUnixTime: claims.IssuedAt,
})
}
// DeleteTokensBeforeTime deletes tokens that is created before specific tim
@@ -230,6 +195,62 @@ func (s *TokenService) GenerateTokenId(tokenRecord *models.TokenRecord) string {
return fmt.Sprintf("%d:%d:%d", tokenRecord.Uid, tokenRecord.CreatedUnixTime, tokenRecord.UserTokenId)
}
func (s *TokenService) parseToken(c *core.Context, extractor request.Extractor) (*jwt.Token, *core.UserTokenClaims, error) {
claims := &core.UserTokenClaims{}
token, err := request.ParseFromRequest(c.Request, extractor,
func(token *jwt.Token) (interface{}, error) {
now := time.Now().Unix()
userTokenId, err := utils.StringToInt64(claims.UserTokenId)
if err != nil {
log.WarnfWithRequestId(c, "[tokens.ParseToken] token \"utid:%s\" in token of user \"uid:%d\" is invalid, because %s", claims.UserTokenId, claims.Uid, err.Error())
return nil, errs.ErrInvalidUserTokenId
}
tokenRecord, err := s.getTokenRecord(claims.Uid, userTokenId, claims.IssuedAt)
if err != nil {
log.WarnfWithRequestId(c, "[tokens.ParseToken] token \"utid:%s\" of user \"uid:%d\" record not found, because %s", claims.UserTokenId, claims.Uid, err.Error())
return nil, errs.ErrTokenRecordNotFound
}
if tokenRecord.ExpiredUnixTime < now {
log.WarnfWithRequestId(c, "[tokens.ParseToken] token \"utid:%s\" of user \"uid:%d\" record is expired", claims.UserTokenId, claims.Uid)
return nil, errs.ErrTokenExpired
}
return []byte(tokenRecord.Secret), nil
},
request.WithClaims(claims),
request.WithParser(jwt.NewParser(jwt.WithIssuedAt())),
)
if err != nil {
if err == request.ErrNoTokenInRequest {
return nil, nil, errs.ErrTokenIsEmpty
}
if err == jwt.ErrTokenMalformed || err == jwt.ErrTokenUnverifiable || err == jwt.ErrTokenSignatureInvalid {
log.WarnfWithRequestId(c, "[tokens.ParseToken] token is invalid, because %s", err.Error())
return nil, nil, errs.ErrCurrentInvalidToken
}
if err == jwt.ErrTokenExpired {
return nil, nil, errs.ErrCurrentTokenExpired
}
if err == jwt.ErrTokenUsedBeforeIssued {
log.WarnfWithRequestId(c, "[tokens.ParseToken] token is invalid, because issue time is later than now")
return nil, nil, errs.ErrCurrentInvalidToken
}
return nil, nil, err
}
return token, claims, err
}
func (s *TokenService) createToken(user *models.User, tokenType core.TokenType, userAgent string, expiryDate time.Duration) (string, *core.UserTokenClaims, error) {
var err error
now := time.Now()
@@ -249,13 +270,11 @@ func (s *TokenService) createToken(user *models.User, tokenType core.TokenType,
claims := &core.UserTokenClaims{
UserTokenId: utils.Int64ToString(tokenRecord.UserTokenId),
Uid: tokenRecord.Uid,
Username: user.Username,
Type: tokenRecord.TokenType,
StandardClaims: jwt.StandardClaims{
Id: utils.Int64ToString(tokenRecord.Uid),
IssuedAt: tokenRecord.CreatedUnixTime,
ExpiresAt: tokenRecord.ExpiredUnixTime,
},
IssuedAt: tokenRecord.CreatedUnixTime,
ExpiresAt: tokenRecord.ExpiredUnixTime,
}
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+11 -1
View File
@@ -542,6 +542,14 @@ func (s *TransactionService) ModifyTransaction(transaction *models.Transaction,
updateCols = append(updateCols, "comment")
}
if transaction.GeoLongitude != oldTransaction.GeoLongitude {
updateCols = append(updateCols, "geo_longitude")
}
if transaction.GeoLatitude != oldTransaction.GeoLatitude {
updateCols = append(updateCols, "geo_latitude")
}
// Get and verify tags
err = s.isTagsValid(sess, transaction, transactionTagIndexs, addTagIds)
@@ -955,6 +963,8 @@ func (s *TransactionService) GetRelatedTransferTransaction(originalTransaction *
RelatedAccountId: originalTransaction.AccountId,
RelatedAccountAmount: originalTransaction.Amount,
Comment: originalTransaction.Comment,
GeoLongitude: originalTransaction.GeoLongitude,
GeoLatitude: originalTransaction.GeoLatitude,
CreatedIp: originalTransaction.CreatedIp,
CreatedUnixTime: originalTransaction.CreatedUnixTime,
UpdatedUnixTime: originalTransaction.UpdatedUnixTime,
@@ -1087,7 +1097,7 @@ func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(uid i
}
var transactionTotalAmounts []*models.Transaction
err := s.UserDataDB(uid).Select("uid, category_id, account_id, SUM(amount) as amount").Where(condition, conditionParams...).GroupBy("category_id, account_id").Find(&transactionTotalAmounts)
err := s.UserDataDB(uid).Select("category_id, account_id, SUM(amount) as amount").Where(condition, conditionParams...).GroupBy("category_id, account_id").Find(&transactionTotalAmounts)
if err != nil {
return nil, err
+27 -7
View File
@@ -150,7 +150,7 @@ func (s *UserService) CreateUser(user *models.User) error {
}
// UpdateUser saves an existed user model to database
func (s *UserService) UpdateUser(user *models.User) (keyProfileUpdated bool, err error) {
func (s *UserService) UpdateUser(user *models.User, modifyUserLanguage bool) (keyProfileUpdated bool, err error) {
if user.Uid <= 0 {
return false, errs.ErrUserIdInvalid
}
@@ -186,20 +186,40 @@ func (s *UserService) UpdateUser(user *models.User) (keyProfileUpdated bool, err
updateCols = append(updateCols, "nickname")
}
if user.DefaultCurrency != "" {
updateCols = append(updateCols, "default_currency")
}
if user.DefaultAccountId > 0 {
updateCols = append(updateCols, "default_account_id")
}
if models.TRANSACTION_EDIT_SCOPE_NONE <= user.TransactionEditScope && user.TransactionEditScope <= models.TRANSACTION_EDIT_SCOPE_THIS_YEAR_OR_LATER {
updateCols = append(updateCols, "transaction_edit_scope")
}
if modifyUserLanguage || user.Language != "" {
updateCols = append(updateCols, "language")
}
if user.DefaultCurrency != "" {
updateCols = append(updateCols, "default_currency")
}
if models.WEEKDAY_SUNDAY <= user.FirstDayOfWeek && user.FirstDayOfWeek <= models.WEEKDAY_SATURDAY {
updateCols = append(updateCols, "first_day_of_week")
}
if models.TRANSACTION_EDIT_SCOPE_NONE <= user.TransactionEditScope && user.TransactionEditScope <= models.TRANSACTION_EDIT_SCOPE_THIS_YEAR_OR_LATER {
updateCols = append(updateCols, "transaction_edit_scope")
if models.LONG_DATE_FORMAT_DEFAULT <= user.LongDateFormat && user.LongDateFormat <= models.LONG_DATE_FORMAT_D_M_YYYY {
updateCols = append(updateCols, "long_date_format")
}
if models.SHORT_DATE_FORMAT_DEFAULT <= user.ShortDateFormat && user.ShortDateFormat <= models.SHORT_DATE_FORMAT_D_M_YYYY {
updateCols = append(updateCols, "short_date_format")
}
if models.LONG_TIME_FORMAT_DEFAULT <= user.LongTimeFormat && user.LongTimeFormat <= models.LONG_TIME_FORMAT_HH_MM_SS_A {
updateCols = append(updateCols, "long_time_format")
}
if models.SHORT_TIME_FORMAT_DEFAULT <= user.ShortTimeFormat && user.ShortTimeFormat <= models.SHORT_TIME_FORMAT_HH_MM_A {
updateCols = append(updateCols, "short_time_format")
}
user.UpdatedUnixTime = now
+36 -5
View File
@@ -62,13 +62,19 @@ const (
InternalUuidGeneratorType string = "internal"
)
// Map provider types
const (
OpenStreetMapProvider string = "openstreetmap"
)
// Exchange rates data source types
const (
EuroCentralBankDataSource string = "euro_central_bank"
BankOfCanadaDataSource string = "bank_of_canada"
ReserveBankOfAustraliaDataSource string = "reserve_bank_of_australia"
CzechNationalBankDataSource string = "czech_national_bank"
NationalBankOfPolandDataSource string = "national_bank_of_poland"
EuroCentralBankDataSource string = "euro_central_bank"
BankOfCanadaDataSource string = "bank_of_canada"
ReserveBankOfAustraliaDataSource string = "reserve_bank_of_australia"
CzechNationalBankDataSource string = "czech_national_bank"
NationalBankOfPolandDataSource string = "national_bank_of_poland"
MonetaryAuthorityOfSingaporeDataSource string = "monetary_authority_of_singapore"
)
const (
@@ -168,6 +174,10 @@ type Config struct {
// Data
EnableDataExport bool
// Map
MapProvider string
EnableMapDataFetchProxy bool
// Exchange Rates
ExchangeRatesDataSource string
ExchangeRatesRequestTimeout uint32
@@ -238,6 +248,12 @@ func LoadConfiguration(configFilePath string) (*Config, error) {
return nil, err
}
err = loadMapConfiguration(config, cfgFile, "map")
if err != nil {
return nil, err
}
err = loadExchangeRatesConfiguration(config, cfgFile, "exchange_rates")
if err != nil {
@@ -415,6 +431,19 @@ func loadDataConfiguration(config *Config, configFile *ini.File, sectionName str
return nil
}
func loadMapConfiguration(config *Config, configFile *ini.File, sectionName string) error {
if getConfigItemStringValue(configFile, sectionName, "map_provider") == "" {
config.MapProvider = ""
} else if getConfigItemStringValue(configFile, sectionName, "map_provider") == OpenStreetMapProvider {
config.MapProvider = OpenStreetMapProvider
} else {
return errs.ErrInvalidMapProvider
}
config.EnableMapDataFetchProxy = getConfigItemBoolValue(configFile, sectionName, "map_data_fetch_proxy", false)
return nil
}
func loadExchangeRatesConfiguration(config *Config, configFile *ini.File, sectionName string) error {
if getConfigItemStringValue(configFile, sectionName, "data_source") == EuroCentralBankDataSource {
config.ExchangeRatesDataSource = EuroCentralBankDataSource
@@ -426,6 +455,8 @@ func loadExchangeRatesConfiguration(config *Config, configFile *ini.File, sectio
config.ExchangeRatesDataSource = CzechNationalBankDataSource
} else if getConfigItemStringValue(configFile, sectionName, "data_source") == NationalBankOfPolandDataSource {
config.ExchangeRatesDataSource = NationalBankOfPolandDataSource
} else if getConfigItemStringValue(configFile, sectionName, "data_source") == MonetaryAuthorityOfSingaporeDataSource {
config.ExchangeRatesDataSource = MonetaryAuthorityOfSingaporeDataSource
} else {
return errs.ErrInvalidExchangeRatesDataSource
}
+30
View File
@@ -3,8 +3,38 @@ package utils
import (
"io"
"os"
"strings"
)
// ListFileNamesWithPrefixAndSuffix returns file name list which has specified prefix and suffix
func ListFileNamesWithPrefixAndSuffix(path string, prefix string, suffix string) []string {
dir, err := os.Open(path)
if err != nil {
return nil
}
fileInfos, err := dir.Readdir(0)
if err != nil {
return nil
}
var fileNames []string
for i := 0; i < len(fileInfos); i++ {
fileInfo := fileInfos[i]
if !fileInfo.IsDir() &&
strings.HasPrefix(fileInfo.Name(), prefix) &&
strings.HasSuffix(fileInfo.Name(), suffix) {
fileNames = append(fileNames, fileInfo.Name())
}
}
return fileNames
}
// IsExists returns whether specified file or directory path exits
func IsExists(path string) (bool, error) {
_, err := os.Stat(path)
+1 -1
View File
@@ -35,8 +35,8 @@ type InternalUuidInfo struct {
// InternalUuidGenerator represents internal bundled uuid generator
type InternalUuidGenerator struct {
uuidServerId uint8
uuidSeqNumbers [1 << internalUuidTypeBits]uint64
uuidServerId uint8
}
// NewInternalUuidGenerator returns a new internal uuid generator
+17 -25
View File
@@ -78,22 +78,18 @@ func TestGenerateUuid_2000TimesIn2Seconds(t *testing.T) {
}
}
func TestGenerateUuid_1000000TimesConcurrent(t *testing.T) {
concurrentCount := 50
func TestGenerateUuid_10000TimesConcurrent(t *testing.T) {
concurrentCount := 10
generator, _ := NewInternalUuidGenerator(&settings.Config{UuidServerId: 3})
var mutex sync.Mutex
var generatedIds sync.Map
var waitGroup sync.WaitGroup
for routineIndex := 0; routineIndex < concurrentCount; routineIndex++ {
go func() {
go func(currentRoutineIndex int) {
waitGroup.Add(1)
for cycle := 0; cycle < 40000; cycle++ {
if cycle%10000 == 0 { // each server can only generate 500,000 (50 * 10000) uuids in one second
time.Sleep(1000 * time.Millisecond)
}
for cycle := 0; cycle < 1000; cycle++ {
expectedUnixTime := time.Now().Unix()
uuid := generator.GenerateUuid(UUID_TYPE_USER)
uuidInfo := generator.ParseUuidInfo(uuid)
@@ -105,18 +101,18 @@ func TestGenerateUuid_1000000TimesConcurrent(t *testing.T) {
}
if uuidInfo.SequentialId == 0 {
if existedUnixTime, exists := generatedIds.Load(uuid); exists {
if existedRoutineIndex, exists := generatedIds.Load(uuid); exists {
mutex.Lock()
assert.Fail(t, fmt.Sprintf("uuid \"%d\" conflicts, seq id is %d, existed unixtime is %d, current unix time is %d", uuid, uuidInfo.SequentialId, existedUnixTime, uuidInfo.UnixTime))
assert.Fail(t, fmt.Sprintf("uuid \"%d\" conflicts, unix time is %d, seq id is %d, existed routine index is %d, current routine index is %d", uuid, uuidInfo.UnixTime, uuidInfo.SequentialId, existedRoutineIndex, currentRoutineIndex))
mutex.Unlock()
}
generatedIds.Store(uuid, uuidInfo.UnixTime)
generatedIds.Store(uuid, currentRoutineIndex)
}
}
waitGroup.Done()
}()
}(routineIndex)
}
waitGroup.Wait()
@@ -199,23 +195,19 @@ func TestGenerateUuids_30TimesIn3Seconds(t *testing.T) {
}
}
func TestGenerateUuids_1000000TimesConcurrent(t *testing.T) {
concurrentCount := 50
expectedUuidCount := uint8(100)
func TestGenerateUuids_20000TimesConcurrent(t *testing.T) {
concurrentCount := 10
expectedUuidCount := uint8(20)
generator, _ := NewInternalUuidGenerator(&settings.Config{UuidServerId: 3})
var mutex sync.Mutex
var generatedIds sync.Map
var waitGroup sync.WaitGroup
for routineIndex := 0; routineIndex < concurrentCount; routineIndex++ {
go func() {
go func(currentRoutineIndex int) {
waitGroup.Add(1)
for cycle := 0; cycle < 400; cycle++ {
if cycle%100 == 0 { // each server can only generate 500,000 (50 * 10000) uuids in one second
time.Sleep(1000 * time.Millisecond)
}
for cycle := 0; cycle < 100; cycle++ {
expectedUnixTime := time.Now().Unix()
uuids := generator.GenerateUuids(UUID_TYPE_USER, expectedUuidCount)
@@ -228,18 +220,18 @@ func TestGenerateUuids_1000000TimesConcurrent(t *testing.T) {
mutex.Unlock()
}
if existedUnixTime, exists := generatedIds.Load(uuids[i]); exists {
if existedRoutineIndex, exists := generatedIds.Load(uuids[i]); exists {
mutex.Lock()
assert.Fail(t, fmt.Sprintf("uuid \"%d\" conflicts, seq id is %d, existed unixtime is %d, current unix time is %d", uuids[i], uuidInfo.SequentialId, existedUnixTime, uuidInfo.UnixTime))
assert.Fail(t, fmt.Sprintf("uuid \"%d\" conflicts, unix time is %d, seq id is %d, existed routine index is %d, current routine index is %d", uuids[i], uuidInfo.UnixTime, uuidInfo.SequentialId, existedRoutineIndex, currentRoutineIndex))
mutex.Unlock()
}
generatedIds.Store(uuids[i], uuidInfo.UnixTime)
generatedIds.Store(uuids[i], currentRoutineIndex)
}
}
waitGroup.Done()
}()
}(routineIndex)
}
waitGroup.Wait()
+4 -1
View File
@@ -132,7 +132,8 @@ var AllCurrencyNames = map[string]bool{
"SEK": true, //Swedish Krona
"SGD": true, //Singapore Dollar
"SHP": true, //Saint Helena Pound
"SLL": true, //Leone
"SLE": true, //Leone (new leone)
"SLL": true, //Leone (old leone)
"SOS": true, //Somali Shilling
"SRD": true, //Surinam Dollar
"SSP": true, //South Sudanese Pound
@@ -154,6 +155,8 @@ var AllCurrencyNames = map[string]bool{
"USD": true, //US Dollar
"UYU": true, //Peso Uruguayo
"UZS": true, //Uzbekistan Sum
"VED": true, //Bolívar Digital
"VEF": true, //Bolívar Fuerte
"VES": true, //Bolívar Soberano
"VND": true, //Dong
"VUV": true, //Vatu
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
plugins: {
'postcss-preset-env': {},
},
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 936 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 B

-346
View File
@@ -1,346 +0,0 @@
<template>
<f7-app :params="f7params">
<f7-view id="main-view" class="safe-areas" main url="/"></f7-view>
</f7-app>
</template>
<script>
import routes from './router/mobile.js';
export default {
data() {
const self = this;
return {
f7params: {
name: 'ezBookkeeping',
id: 'net.mayswind.ezbookkeeping',
theme: 'ios',
autoDarkTheme: self.$settings.isEnableAutoDarkMode(),
routes: routes,
actions: {
animate: self.$settings.isEnableAnimate(),
backdrop: true,
closeOnEscape: true
},
dialog: {
animate: self.$settings.isEnableAnimate(),
backdrop: true
},
popover: {
animate: self.$settings.isEnableAnimate(),
backdrop: true,
closeOnEscape: true
},
popup: {
animate: self.$settings.isEnableAnimate(),
backdrop: true,
closeOnEscape: true,
swipeToClose: true
},
sheet: {
animate: self.$settings.isEnableAnimate(),
backdrop: true,
closeOnEscape: true
},
smartSelect: {
routableModals: false
},
touch: {
tapHold: true,
disableContextMenu: true
},
view: {
animate: self.$settings.isEnableAnimate(),
pushState: !self.isiOSHomeScreenMode(),
pushStateAnimate: false,
iosSwipeBackAnimateShadow: false,
mdSwipeBackAnimateShadow: false
},
calendar: {
locale: 'en',
openIn: 'customModal',
backdrop: true
},
serviceWorker: {
path: self.$settings.isProduction() ? './sw.js' : undefined,
scope: './',
}
}
}
},
created() {
if (this.$user.isUserLogined()) {
if (!this.$settings.isEnableApplicationLock()) {
// refresh token if user is logined
this.$store.dispatch('refreshTokenAndRevokeOldToken');
// auto refresh exchange rates data
if (this.$settings.isAutoUpdateExchangeRatesData()) {
this.$store.dispatch('getLatestExchangeRates', { silent: true, force: false });
}
}
}
},
methods: {
isiOSHomeScreenMode() {
if ((/iphone|ipod|ipad/gi).test(navigator.platform) && (/Safari/i).test(navigator.appVersion) &&
window.matchMedia && window.matchMedia('(display-mode: standalone)').matches
) {
return true;
}
return false;
}
}
}
</script>
<style>
/** Global style **/
html, body {
position: fixed;
}
body {
-ms-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
/** Common class **/
.no-right-border {
border-right: 0;
}
.no-bottom-border {
border-bottom: 0;
}
.work-break-all {
word-break: break-all;
}
.full-line {
width: 100%;
}
.smaller {
font-size: 10px;
}
.readonly {
pointer-events: none !important;
}
.segmented.readonly .button:not(.button-active) > span,
.list.readonly .item-content .item-title.item-label,
.list.readonly .item-content .item-title > .item-header {
opacity: 0.55 !important;
}
/** Replacing the default style of framework7 **/
:root {
--f7-theme-color: #c67e48;
--f7-theme-color-rgb: 198, 126, 72;
--f7-theme-color-shade: #af6a36;
--f7-theme-color-tint: #d09467;
--default-icon-color: var(--f7-text-color);
}
:root .theme-dark {
--default-icon-color: var(--f7-text-color);
}
.ios .theme-dark, .ios.theme-dark {
--f7-list-item-header-text-color: inherit !important;
}
i.icon.la, i.icon.las, i.icon.lab {
font-size: 28px;
}
.chip.chip-placeholder {
border: 0;
}
/** Common class for replacing the default style of framework7 **/
.navbar .navbar-compact-icons.right a + a {
margin-left: 0;
}
.toolbar-item-auto-size .toolbar-inner {
padding-left: 16px;
padding-right: 16px;
}
.toolbar-item-auto-size .toolbar-inner > .link {
width: auto;
}
.tabbar-primary-link,
.tabbar-item-changed {
color: var(--f7-theme-color);
}
.tabbar-text-with-ellipsis > span {
display: block;
width: 100%;
overflow: hidden;
text-align: center;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
}
.list-item-media-valign-middle .item-media {
align-self: normal !important;
}
.list-item-no-item-after .item-after {
display: none;
}
.list-item-with-header-and-title .item-content .item-header {
margin-bottom: 11px;
}
.list-item-with-header-and-title.list-item-title-hide-overflow .item-content .list-item-custom-title {
overflow: hidden;
text-overflow: ellipsis;
}
.list .item-content .input.list-title-input {
margin-top: calc(-1 * var(--f7-list-item-padding-vertical));
margin-bottom: calc(-1 * var(--f7-list-item-padding-vertical));
}
.list .item-content .list-item-valign-middle {
align-self: center;
}
.list .item-content .list-item-showing {
color: rgba(0, 0, 0, 0.2);
font-size: 16px;
font-weight: bold;
}
.theme-dark .list .item-content .list-item-showing {
color: rgba(255, 255, 255, 0.2);
}
.accordion-item.list-item-checked > .item-link > .item-content .item-title {
font-weight: bold;
}
.list .item-content .list-item-checked-icon {
font-size: 20px;
color: var(--f7-radio-active-color, var(--f7-theme-color));
}
.ebk-list-item-error-info div.item-footer {
color: var(--f7-input-error-text-color)
}
.skeleton-text .list-item-toggle .item-after {
height: var(--f7-toggle-height);
}
.skeleton-text .list-item-toggle .item-after > span {
line-height: var(--f7-toggle-height);
font-size: var(--f7-toggle-height);
}
.no-sortable > .sortable-handler {
display: none;
}
.card-header-content {
opacity: 0.6;
}
.card-chevron-icon {
color: var(--f7-list-chevron-icon-color);
font-size: var(--f7-list-chevron-icon-font-size);
font-weight: bolder;
}
.icon-after-text {
margin-left: 6px;
}
.badge.right-bottom-icon {
margin-left: -12px;
margin-top: 14px;
width: 16px;
height: 16px;
}
.badge.right-bottom-icon > .icon {
font-size: 13px;
width: 13px;
height: 13px;
}
/** Nested List item for framework7 **/
.nested-list-item .item-title {
width: 100%;
}
.nested-list-item .item-inner {
padding-right: 0;
}
.nested-list-item.has-child-list-item .nested-list-item-child .item-inner {
padding-bottom: var(--f7-list-item-padding-vertical);
}
.nested-list-item.has-child-list-item .item-link.active-state {
background-color: transparent;
}
.nested-list-item.has-child-list-item .item-link .item-inner {
padding-right: 0;
}
.nested-list-item.has-child-list-item .item-link .item-inner:before {
color: transparent;
}
.nested-list-item.has-child-list-item .item-link.active-state .item-inner .nested-list-item-child .item-link.active-state {
background-color: var(--f7-list-link-pressed-bg-color);
}
.nested-list-item.has-child-list-item .item-link .item-inner .nested-list-item-child .item-link .item-inner {
padding-right: calc(var(--f7-list-chevron-icon-area) + var(--f7-list-item-padding-horizontal) + var(--f7-safe-area-right));
}
.nested-list-item.has-child-list-item .item-link .item-inner .nested-list-item-child .item-link .item-inner:before {
color: var(--f7-list-chevron-icon-color);
}
.nested-list-item .nested-list-item-title {
align-self: center;
margin-left: var(--f7-list-item-media-margin);
margin-right: var(--f7-list-item-media-margin);
overflow: hidden;
text-overflow: ellipsis;
}
.sortable-enabled .nested-list-item .nested-list-item-child .item-inner {
padding-right: var(--f7-safe-area-right) !important;
}
/** Replacing the default style of Vue-pincode-input **/
.vue-pincode-input {
margin: 3px !important;
padding: 5px !important;
box-shadow: 0 0 2px rgba(0,0,0,.5) !important;
}
.theme-dark .vue-pincode-input {
box-shadow: 0 0 2px rgba(255,255,255,.5) !important;
}
</style>
+684
View File
@@ -0,0 +1,684 @@
<template>
<img style="display: none;" :src="devCookiePath" v-if="!isProduction" />
<f7-app v-bind="f7params">
<f7-view id="main-view" class="safe-areas" main url="/"></f7-view>
</f7-app>
</template>
<script>
import { f7ready } from 'framework7-vue';
import routes from './router/mobile.js';
export default {
data() {
const self = this;
let darkMode = 'auto';
if (self.$settings.getTheme() === 'light') {
darkMode = false;
} else if (self.$settings.getTheme() === 'dark') {
darkMode = true;
}
return {
isProduction: self.$settings.isProduction(),
devCookiePath: self.$settings.isProduction() ? '' : '/dev/cookies',
f7params: {
name: 'ezBookkeeping',
theme: 'ios',
colors: {
primary: '#c67e48'
},
routes: routes,
darkMode: darkMode,
touch: {
disableContextMenu: true,
tapHold: true
},
serviceWorker: {
path: self.$settings.isProduction() ? './sw.js' : undefined,
scope: './',
},
actions: {
animate: self.$settings.isEnableAnimate(),
backdrop: true,
closeOnEscape: true
},
dialog: {
animate: self.$settings.isEnableAnimate(),
backdrop: true
},
popover: {
animate: self.$settings.isEnableAnimate(),
backdrop: true,
closeOnEscape: true
},
popup: {
animate: self.$settings.isEnableAnimate(),
backdrop: true,
closeOnEscape: true,
swipeToClose: true
},
sheet: {
animate: self.$settings.isEnableAnimate(),
backdrop: true,
closeOnEscape: true
},
smartSelect: {
routableModals: false
},
view: {
animate: self.$settings.isEnableAnimate(),
browserHistory: !self.isiOSHomeScreenMode(),
browserHistoryInitialMatch: true,
browserHistoryAnimate: false,
iosSwipeBackAnimateShadow: false,
mdSwipeBackAnimateShadow: false
}
},
isDarkMode: undefined,
hasPushPopupBackdrop: undefined,
hasBackdrop: undefined
}
},
created() {
const self = this;
if (self.$user.isUserLogined()) {
if (!self.$settings.isEnableApplicationLock()) {
// refresh token if user is logined
self.$store.dispatch('refreshTokenAndRevokeOldToken').then(response => {
if (response.user && response.user.language) {
self.$locale.setLanguage(response.user.language);
}
});
// auto refresh exchange rates data
if (self.$settings.isAutoUpdateExchangeRatesData()) {
self.$store.dispatch('getLatestExchangeRates', { silent: true, force: false });
}
}
}
},
mounted() {
f7ready((f7) => {
this.isDarkMode = f7.darkMode;
this.setThemeColorMeta(f7.darkMode);
f7.on('actionsOpen', (event) => this.onBackdropChanged(event));
f7.on('actionsClose', (event) => this.onBackdropChanged(event));
f7.on('dialogOpen', (event) => this.onBackdropChanged(event));
f7.on('dialogClose', (event) => this.onBackdropChanged(event));
f7.on('popoverOpen', (event) => this.onBackdropChanged(event));
f7.on('popoverClose', (event) => this.onBackdropChanged(event));
f7.on('popupOpen', (event) => this.onBackdropChanged(event));
f7.on('popupClose', (event) => this.onBackdropChanged(event));
f7.on('sheetOpen', (event) => this.onBackdropChanged(event));
f7.on('sheetClose', (event) => this.onBackdropChanged(event));
f7.on('pageBeforeOut', () => {
if (this.$ui.isModalShowing()) {
f7.actions.close('.actions-modal.modal-in', false);
f7.dialog.close('.dialog.modal-in', false);
f7.popover.close('.popover.modal-in', false);
f7.popup.close('.popup.modal-in', false);
f7.sheet.close('.sheet-modal.modal-in', false);
}
});
f7.on('darkModeChange', (isDarkMode) => {
this.isDarkMode = isDarkMode;
this.setThemeColorMeta(isDarkMode);
});
});
},
methods: {
isiOSHomeScreenMode() {
if ((/iphone|ipod|ipad/gi).test(navigator.platform) && (/Safari/i).test(navigator.appVersion) &&
window.matchMedia && window.matchMedia('(display-mode: standalone)').matches
) {
return true;
}
return false;
},
onBackdropChanged(event) {
if (event.push) {
this.hasPushPopupBackdrop = event.opened;
} else {
this.hasBackdrop = event.opened;
}
this.setThemeColorMeta(this.isDarkMode);
},
setThemeColorMeta(isDarkMode) {
if (this.hasPushPopupBackdrop) {
document.querySelector('meta[name=theme-color]').setAttribute('content', '#000');
return;
}
if (isDarkMode) {
if (this.hasBackdrop) {
document.querySelector('meta[name=theme-color]').setAttribute('content', '#0b0b0b');
} else {
document.querySelector('meta[name=theme-color]').setAttribute('content', '#121212');
}
} else {
if (this.hasBackdrop) {
document.querySelector('meta[name=theme-color]').setAttribute('content', '#949495');
} else {
document.querySelector('meta[name=theme-color]').setAttribute('content', '#f6f6f8');
}
}
}
}
}
</script>
<style>
/** Global style **/
html, body {
position: fixed;
}
body {
-ms-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
/** Common class **/
.no-right-border {
border-right: 0;
}
.no-bottom-border {
border-bottom: 0;
}
.work-break-all {
word-break: break-all;
}
.full-line {
width: 100%;
}
.smaller {
font-size: 10px;
}
.readonly {
pointer-events: none !important;
}
.skeleton-text {
pointer-events: none !important;
}
.segmented.readonly .button:not(.button-active) > span,
.list.readonly .item-content .item-title.item-label,
.list.readonly .item-content .item-title > .item-header {
opacity: 0.55 !important;
}
/** Replacing the default style of framework7 **/
:root {
--f7-popup-push-offset: 5px;
--f7-color-gray: #8e8e93;
--f7-color-gray-rgb: 142, 142, 147;
--f7-color-gray-shade: #79797f;
--f7-color-gray-tint: #a3a3a7;
--default-icon-color: var(--f7-text-color);
}
:root .dark {
--default-icon-color: var(--f7-text-color);
}
.color-gray {
--f7-theme-color: var(--f7-color-gray);
--f7-theme-color-rgb: var(--f7-color-gray-rgb);
--f7-theme-color-shade: var(--f7-color-gray-shade);
--f7-theme-color-tint: var(--f7-color-gray-tint);
}
.ios .dark, .ios.dark {
--f7-list-item-header-text-color: inherit !important;
}
i.icon.la, i.icon.las, i.icon.lab {
font-size: 28px;
}
.chip.chip-placeholder {
border: 0;
}
/** Replacing the default style of @vuepic/vue-datepicker **/
.dp__theme_light {
--dp-primary-color: #c67e48;
}
.dp__theme_dark {
--dp-primary-color: #c67e48;
}
/** Common class for replacing the default style of framework7 **/
.navbar .navbar-compact-icons.right a + a {
margin-left: 0;
}
.toolbar-item-auto-size .toolbar-inner {
padding-left: 16px;
padding-right: 16px;
}
.toolbar-item-auto-size .toolbar-inner > .link {
width: auto;
}
.tabbar-primary-link,
.tabbar-item-changed {
color: var(--f7-theme-color);
}
.tabbar-text-with-ellipsis > span {
display: block;
width: 100%;
overflow: hidden;
text-align: center;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
}
.block-title .accordion-item-toggle .icon {
color: var(--f7-list-chevron-icon-color);
font-size: var(--f7-list-chevron-icon-font-size);
font-weight: bolder;
}
.list-item-media-valign-middle .item-media {
align-self: normal !important;
}
.list-item-no-item-after .item-after {
display: none;
}
.list-item-with-header-and-title .item-content .item-header {
margin-bottom: 11px;
}
.list-item-with-header-and-title.list-item-title-hide-overflow .item-content .list-item-custom-title {
overflow: hidden;
text-overflow: ellipsis;
}
.list .item-content .input.list-title-input {
margin-top: calc(-1 * var(--f7-list-item-padding-vertical));
margin-bottom: calc(-1 * var(--f7-list-item-padding-vertical));
}
.list .item-content .list-item-valign-middle {
align-self: center;
}
.list .item-content .list-item-showing {
color: rgba(0, 0, 0, 0.2);
font-size: 16px;
font-weight: bold;
}
.dark .list .item-content .list-item-showing {
color: rgba(255, 255, 255, 0.2);
}
.accordion-item.list-item-checked > .item-link > .item-content .item-title {
font-weight: bold;
}
.list.list-dividers li.has-child-list-item .item-inner:after {
content: '';
position: absolute;
background-color: var(--f7-list-item-border-color);
display: block !important;
z-index: 15;
top: auto;
right: auto;
bottom: 0;
left: 0;
height: 1px;
width: 100%;
transform-origin: 50% 100%;
transform: scaleY(calc(1 / var(--f7-device-pixel-ratio)));
}
.list.list-dividers li.list-group-title:first-child,
.list.list-dividers li.list-group-title.actual-first-child {
border-radius: var(--f7-list-inset-border-radius) var(--f7-list-inset-border-radius) 0 0;
}
.list.list-dividers li.list-group-title:first-child:before,
.list.list-dividers li.list-group-title.actual-first-child:before {
background-color: transparent;
}
.list.list-dividers li:last-child > .swipeout-content > .item-content > .item-inner:after,
.list.list-dividers li:last-child > .swipeout-content > .item-link > .item-content > .item-inner:after,
.list.list-dividers li.actual-last-child > .swipeout-content > .item-content > .item-inner:after,
.list.list-dividers li.actual-last-child > .swipeout-content > .item-link > .item-content > .item-inner:after {
background-color: transparent;
}
.list.inset li.list-group-title:first-child > a.button {
border-radius: var(--f7-button-border-radius);
}
.list.inset li.swipeout.actual-first-child,
.list.inset li.actual-first-child > a {
border-radius: var(--f7-list-inset-border-radius) var(--f7-list-inset-border-radius) 0 0;
}
.list.inset li.swipeout.actual-last-child,
.list.inset li.actual-last-child > a {
border-radius: 0 0 var(--f7-list-inset-border-radius) var(--f7-list-inset-border-radius);
}
.list.inset li.swipeout.actual-first-child.actual-last-child,
.list.inset li.actual-first-child.actual-last-child > a {
border-radius: var(--f7-list-inset-border-radius);
}
.list.inset.list-has-group-title li.swipeout.actual-first-child,
.list.inset.list-has-group-title li.actual-first-child > a {
border-radius: 0;
}
.list.inset.list-has-group-title li.swipeout:first-child:last-child,
.list.inset.list-has-group-title li:first-child:last-child > a,
.list.inset.list-has-group-title li.swipeout.actual-first-child.actual-last-child,
.list.inset.list-has-group-title li.actual-first-child.actual-last-child > a {
border-radius: 0 0 var(--f7-list-inset-border-radius) var(--f7-list-inset-border-radius);
}
.accordion-item .block-title+.accordion-item-content .list.inset li.swipeout:first-child:not(:last-child),
.accordion-item .block-title+.accordion-item-content .list.inset li:first-child:not(:last-child) > a {
border-radius: 0;
}
.accordion-item .block-title+.accordion-item-content .list.inset li.swipeout:first-child:last-child,
.accordion-item .block-title+.accordion-item-content .list.inset li:first-child:last-child > a,
.accordion-item .block-title+.accordion-item-content .list.inset li.swipeout.actual-last-child:first-child,
.accordion-item .block-title+.accordion-item-content .list.inset li.actual-last-child:first-child > a {
border-radius: 0 0 var(--f7-list-inset-border-radius) var(--f7-list-inset-border-radius);
}
.list .item-content .list-item-checked-icon {
font-size: 20px;
color: var(--f7-radio-active-color, var(--f7-theme-color));
margin-right: calc(var(--f7-list-item-media-margin) + var(--f7-checkbox-extra-margin));
}
.list .item-content > .item-inner > .item-after .list-item-checked-icon {
margin-right: 0;
}
.list li.no-margin .item-content.item-input {
margin: 0;
}
.ebk-list-item-error-info div.item-footer {
color: var(--f7-input-error-text-color)
}
.skeleton-text .list-item-toggle .item-after {
height: var(--f7-toggle-height);
}
.skeleton-text .list-item-toggle .item-after > span {
line-height: var(--f7-toggle-height);
font-size: var(--f7-toggle-height);
}
.no-sortable > .sortable-handler {
display: none;
}
.card-header-content {
opacity: 0.6;
}
.icon-after-text {
margin-left: 6px;
}
.badge.right-bottom-icon {
margin-left: -12px;
margin-top: 14px;
width: 16px;
height: 16px;
}
.badge.right-bottom-icon > .icon {
font-size: 13px;
width: 13px;
height: 13px;
}
/** Swipe handler **/
.swipe-handler {
height: 26px;
position: absolute;
left: 0;
width: 100%;
top: 0;
cursor: pointer;
}
.swipe-handler:after {
content: "";
width: 36px;
height: 6px;
position: absolute;
left: 50%;
top: 50%;
margin-left: -18px;
margin-top: -8px;
border-radius: 3px;
background: #666
}
/** list-item-with-multi-item for framework7 **/
.list-item-with-multi-item .item-content,
.list-item-with-multi-item .item-inner {
padding: 0;
}
.list-item-with-multi-item .item-inner > div {
width: 100%;
}
.list-item-with-multi-item > .item-content > .item-inner:after {
background-color: transparent;
}
.list-item-with-multi-item .list-item-subitem:first-child .item-content {
padding-left: calc(var(--f7-list-item-padding-horizontal) + var(--f7-safe-area-left));
}
.list-item-with-multi-item .list-item-subitem .item-inner {
display: block;
width: 100%;
padding-left: calc(var(--f7-list-item-padding-horizontal) + var(--f7-safe-area-left));
padding-top: var(--f7-list-item-padding-vertical);
padding-bottom: var(--f7-list-item-padding-vertical);
}
.list-item-with-multi-item .list-item-subitem:first-child .item-inner {
padding-left: 0;
}
/** Combination list for framework7 **/
.combination-list-wrapper {
margin: 0;
padding: 0;
}
.combination-list-wrapper .block-title {
margin-top: 0;
margin-bottom: 0;
}
.combination-list-wrapper .list.combination-list-header {
margin: 0;
}
.combination-list-wrapper .list.combination-list-header .item-title {
width: 100%;
display: flex;
}
.combination-list-wrapper .list.combination-list-header > ul {
background-color: var(--f7-list-group-title-bg-color);
}
.combination-list-wrapper .list.combination-list-header.combination-list-opened > ul {
border-radius: var(--f7-list-inset-border-radius) var(--f7-list-inset-border-radius) 0 0;
}
.combination-list-wrapper .list.combination-list-header.combination-list-closed > ul {
border-radius: var(--f7-list-inset-border-radius);
}
.combination-list-wrapper .list.combination-list-header .combination-list-chevron-icon {
margin-left: auto;
}
.combination-list-wrapper .list.combination-list-content.inset > ul {
border-radius: 0 0 var(--f7-list-inset-border-radius) var(--f7-list-inset-border-radius);
}
/** Nested List item for framework7 **/
.nested-list-item .item-title {
width: 100%;
}
.nested-list-item.has-child-list-item > .swipeout-content > .item-content > .item-inner,
.nested-list-item.has-child-list-item > .swipeout-content > .item-link > .item-content > .item-inner {
padding-top: 0;
padding-bottom: 0;
}
.nested-list-item.has-child-list-item .nested-list-item-child .item-inner {
padding-bottom: var(--f7-list-item-padding-vertical);
}
.nested-list-item.has-child-list-item .item-link.active-state {
background-color: transparent;
}
.nested-list-item.has-child-list-item .item-link .item-inner {
padding-right: 0;
}
.nested-list-item.has-child-list-item .item-link .item-inner:before {
color: transparent;
}
.nested-list-item.has-child-list-item .item-link.active-state .item-inner .nested-list-item-child .item-link.active-state {
background-color: var(--f7-list-link-pressed-bg-color);
}
.nested-list-item.has-child-list-item .item-link .item-inner .nested-list-item-child .item-link .item-inner {
padding-right: calc(var(--f7-list-chevron-icon-area) + var(--f7-list-item-padding-horizontal) + var(--f7-safe-area-right));
}
.nested-list-item.has-child-list-item .item-link .item-inner .nested-list-item-child .item-link .item-inner:before {
color: var(--f7-list-chevron-icon-color);
}
.nested-list-item .nested-list-item-title {
align-self: center;
margin-right: var(--f7-list-item-media-margin);
overflow: hidden;
text-overflow: ellipsis;
}
.nested-list-item.has-child-list-item .nested-list-item-title {
margin-left: var(--f7-list-item-media-margin);
}
.nested-list-item.has-child-list-item > .swipeout-content > .item-content > .item-inner:after,
.nested-list-item.has-child-list-item > .swipeout-content > .item-link > .item-content > .item-inner:after {
background-color: transparent;
}
.list.inset li.nested-list-item.has-child-list-item .item-inner li.nested-list-item-child,
.list.inset li.nested-list-item.has-child-list-item .item-inner li.nested-list-item-child > a {
border-radius: 0;
}
.list.list-dividers li.nested-list-item.has-child-list-item .item-inner .nested-list-item-child .item-inner:after {
content: '';
position: absolute;
background-color: var(--f7-list-item-border-color);
display: block !important;
z-index: 15;
top: auto;
right: auto;
bottom: 0;
left: 0;
height: 1px;
width: 100%;
transform-origin: 50% 100%;
transform: scaleY(calc(1 / var(--f7-device-pixel-ratio)));
}
.list.list-dividers li.nested-list-item.has-child-list-item:last-child .item-inner .nested-list-item-child:last-child .item-inner:after,
.list.list-dividers li.nested-list-item.has-child-list-item.actual-last-child .item-inner .nested-list-item-child.actual-last-child .item-inner:after {
background-color: transparent;
}
.sortable-enabled .nested-list-item .nested-list-item-child .item-inner {
padding-right: var(--f7-safe-area-right) !important;
}
/** Fix @vuepic/vue-datepicker style issue **/
.dp__main.dp__flex_display {
flex-direction: column
}
.dp__main .dp__preset_range {
white-space: inherit;
}
.dp__main .dp__menu_inner {
padding-top: 0;
padding-bottom: 0;
}
.dp__main .dp__menu_inner .dp__month_year_row > button {
width: inherit;
}
.dp__main .dp__menu_inner .dp__month_year_row > button.dp__button {
width: 100%;
}
.dp__main .dp__menu_inner .dp__month_year_row .dp__month_year_wrap > button {
line-height: inherit;
}
.dp__main .dp__calendar .dp__calendar_item {
display: flex;
justify-content: center;
flex: 1;
}
.dp__main .dp__calendar .dp__calendar_item > .dp__cell_inner {
width: 100%;
}
</style>
+21 -17
View File
@@ -1,27 +1,27 @@
<template>
<f7-sheet :opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-sheet swipe-to-close swipe-handler=".swipe-handler"
:opened="show"
@sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-toolbar>
<div class="swipe-handler"></div>
<div class="left"></div>
<div class="right">
<f7-link sheet-close :text="$t('Done')"></f7-link>
</div>
</f7-toolbar>
<f7-page-content>
<f7-block class="margin-vertical">
<f7-row class="padding-vertical padding-horizontal-half"
:class="{ 'row-has-selected-item': hasSelectedIcon(row) }"
v-for="(row, idx) in allColorRows" :key="idx">
<f7-col class="text-align-center" v-for="colorInfo in row" :key="colorInfo.color">
<f7-icon f7="app_fill"
:style="colorInfo.color | iconStyle('default', 'var(--default-icon-color)')"
@click.native="onColorClicked(colorInfo)">
<f7-block class="margin-vertical no-padding">
<div class="grid grid-cols-7 padding-vertical-half padding-horizontal-half"
:class="{ 'row-has-selected-item': hasSelectedIcon(row) }"
:key="idx" v-for="(row, idx) in allColorRows">
<div class="text-align-center" :key="colorInfo.color" v-for="colorInfo in row">
<ItemIcon icon-type="fixed-f7" icon-id="app_fill" :color="colorInfo.color" @click="onColorClicked(colorInfo)">
<f7-badge color="default" class="right-bottom-icon" v-if="currentValue && currentValue === colorInfo.color">
<f7-icon f7="checkmark_alt"></f7-icon>
</f7-badge>
</f7-icon>
</f7-col>
<f7-col v-for="idx in (itemPerRow - row.length)" :key="idx"></f7-col>
</f7-row>
</ItemIcon>
</div>
</div>
</f7-block>
</f7-page-content>
</f7-sheet>
@@ -30,16 +30,20 @@
<script>
export default {
props: [
'value',
'modelValue',
'columnCount',
'show',
'allColorInfos'
],
emits: [
'update:modelValue',
'update:show'
],
data() {
const self = this;
return {
currentValue: self.value,
currentValue: self.modelValue,
itemPerRow: self.columnCount || 7
}
},
@@ -64,11 +68,11 @@ export default {
methods: {
onColorClicked(colorInfo) {
this.currentValue = colorInfo.color;
this.$emit('input', this.currentValue);
this.$emit('update:modelValue', this.currentValue);
this.$emit('update:show', false);
},
onSheetOpen(event) {
this.currentValue = this.value;
this.currentValue = this.modelValue;
this.scrollToSelectedItem(event.$el);
},
onSheetClosed() {
@@ -1,56 +1,41 @@
<template>
<f7-sheet style="height:auto" :opened="show"
@sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-sheet swipe-to-close swipe-handler=".swipe-handler" style="height:auto"
:opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<div class="swipe-handler" style="z-index: 10"></div>
<f7-page-content>
<div class="display-flex padding justify-content-space-between align-items-center">
<div style="font-size: 18px" v-if="title"><b>{{ title }}</b></div>
</div>
<div class="padding-horizontal padding-bottom">
<p class="no-margin-top margin-bottom-half" v-if="hint">{{ hint }}</p>
<p class="no-margin-top" v-if="hint">{{ hint }}</p>
<p class="no-margin-top margin-bottom" v-if="beginDateTime && endDateTime">
<span>{{ beginDateTime }}</span>
<span> - </span>
<span>{{ endDateTime }}</span>
</p>
<slot></slot>
<f7-list no-hairlines inline-labels class="no-margin-top margin-bottom">
<f7-list-input
:label="$t('Begin Time')"
type="datepicker"
class="date-range-sheet-time-item"
:calendar-params="{
timePicker: true,
dateFormat: $t('input-format.datetime.long'),
firstDay: defaultFirstDayOfWeek,
toolbarCloseText: $t('Done'),
timePickerPlaceholder: $t('Select Time'),
timePickerFormat: $locale.getInputTimeIntlDateTimeFormatOptions(),
monthNames: $locale.getAllLongMonthNames(),
monthNamesShort: $locale.getAllShortMonthNames(),
dayNames: $locale.getAllLongWeekdayNames(),
dayNamesShort: $locale.getAllShortWeekdayNames()}"
:value="currentMinDate"
@calendar:change="currentMinDate = $event"
>
</f7-list-input>
<f7-list-input
:label="$t('End Time')"
type="datepicker"
class="date-range-sheet-time-item"
:calendar-params="{
timePicker: true,
dateFormat: $t('input-format.datetime.long'),
firstDay: defaultFirstDayOfWeek,
toolbarCloseText: $t('Done'),
timePickerPlaceholder: $t('Select Time'),
timePickerFormat: $locale.getInputTimeIntlDateTimeFormatOptions(),
monthNames: $locale.getAllLongMonthNames(),
monthNamesShort: $locale.getAllShortMonthNames(),
dayNames: $locale.getAllLongWeekdayNames(),
dayNamesShort: $locale.getAllShortWeekdayNames()}"
:value="currentMaxDate"
@calendar:change="currentMaxDate = $event"
>
</f7-list-input>
</f7-list>
<vue-date-picker range inline enable-seconds auto-apply
month-name-format="long"
six-weeks="center"
class="justify-content-center margin-bottom"
:clearable="false"
:dark="isDarkMode"
:week-start="firstDayOfWeek"
:year-range="yearRange"
:day-names="dayNames"
:is24="is24Hour"
:partial-range="false"
:preset-ranges="presetRanges"
v-model="dateRange">
<template #month="{ text }">
{{ $t(`datetime.${text}.short`) }}
</template>
<template #month-overlay-value="{ text }">
{{ $t(`datetime.${text}.short`) }}
</template>
</vue-date-picker>
<f7-button large fill
:class="{ 'disabled': !currentMinDate || !currentMaxDate }"
:class="{ 'disabled': !dateRange[0] || !dateRange[1] }"
:text="$t('Continue')"
@click="confirm">
</f7-button>
@@ -71,6 +56,10 @@ export default {
'hint',
'show'
],
emits: [
'update:show',
'dateRange:change'
],
data() {
const self = this;
let minDate = self.$utilities.getTodayFirstUnixTime();
@@ -84,65 +73,86 @@ export default {
maxDate = self.maxTime;
}
minDate = self.$utilities.getDummyUnixTimeForLocalUsage(minDate, self.$utilities.getTimezoneOffsetMinutes(), self.$utilities.getBrowserTimezoneOffsetMinutes());
maxDate = self.$utilities.getDummyUnixTimeForLocalUsage(maxDate, self.$utilities.getTimezoneOffsetMinutes(), self.$utilities.getBrowserTimezoneOffsetMinutes());
return {
currentMinDate: [self.$utilities.getLocalDatetimeFromUnixTime(minDate)],
currentMaxDate: [self.$utilities.getLocalDatetimeFromUnixTime(maxDate)]
yearRange: [
2000,
this.$utilities.getYear(this.$utilities.getCurrentDateTime()) + 1
],
dateRange: [
this.$utilities.getLocalDatetimeFromUnixTime(this.$utilities.getDummyUnixTimeForLocalUsage(minDate, this.$utilities.getTimezoneOffsetMinutes(), this.$utilities.getBrowserTimezoneOffsetMinutes())),
this.$utilities.getLocalDatetimeFromUnixTime(this.$utilities.getDummyUnixTimeForLocalUsage(maxDate, this.$utilities.getTimezoneOffsetMinutes(), this.$utilities.getBrowserTimezoneOffsetMinutes()))
]
}
},
computed: {
defaultFirstDayOfWeek() {
return this.$store.getters.currentUserFirstDayOfWeek;
}
},
watch: {
'currentMinDate': function (newValue) {
if (!newValue) {
this.currentMinDate = [this.$utilities.getLocalDatetimeFromUnixTime(this.$utilities.getCurrentUnixTime())];
}
isDarkMode() {
return this.$root.isDarkMode;
},
'currentMaxDate': function (newValue) {
if (!newValue) {
this.currentMaxDate = [this.$utilities.getLocalDatetimeFromUnixTime(this.$utilities.getCurrentUnixTime())];
}
firstDayOfWeek() {
return this.$store.getters.currentUserFirstDayOfWeek;
},
dayNames() {
return this.$utilities.arrangeArrayWithNewStartIndex(this.$locale.getAllMinWeekdayNames(), this.firstDayOfWeek);
},
is24Hour() {
return this.$locale.isLongTime24HourFormat();
},
beginDateTime() {
const actualBeginUnixTime = this.$utilities.getActualUnixTimeForStore(this.$utilities.getUnixTime(this.dateRange[0]), this.$utilities.getTimezoneOffsetMinutes(), this.$utilities.getBrowserTimezoneOffsetMinutes());
return this.$utilities.formatUnixTime(actualBeginUnixTime, this.$locale.getLongDateTimeFormat());
},
endDateTime() {
const actualEndUnixTime = this.$utilities.getActualUnixTimeForStore(this.$utilities.getUnixTime(this.dateRange[1]), this.$utilities.getTimezoneOffsetMinutes(), this.$utilities.getBrowserTimezoneOffsetMinutes());
return this.$utilities.formatUnixTime(actualEndUnixTime, this.$locale.getLongDateTimeFormat());
},
presetRanges() {
const presetRanges = [];
[
this.$constants.datetime.allDateRanges.Today,
this.$constants.datetime.allDateRanges.LastSevenDays,
this.$constants.datetime.allDateRanges.LastThirtyDays,
this.$constants.datetime.allDateRanges.ThisWeek,
this.$constants.datetime.allDateRanges.ThisMonth,
this.$constants.datetime.allDateRanges.ThisYear
].forEach(dateRangeType => {
const dateRange = this.$utilities.getDateRangeByDateType(dateRangeType.type, this.firstDayOfWeek);
presetRanges.push({
label: this.$t(dateRangeType.name),
range: [
this.$utilities.getLocalDatetimeFromUnixTime(this.$utilities.getDummyUnixTimeForLocalUsage(dateRange.minTime, this.$utilities.getTimezoneOffsetMinutes(), this.$utilities.getBrowserTimezoneOffsetMinutes())),
this.$utilities.getLocalDatetimeFromUnixTime(this.$utilities.getDummyUnixTimeForLocalUsage(dateRange.maxTime, this.$utilities.getTimezoneOffsetMinutes(), this.$utilities.getBrowserTimezoneOffsetMinutes()))
]
});
});
return presetRanges;
}
},
methods: {
onSheetOpen() {
if (this.minTime) {
const minTime = this.$utilities.getDummyUnixTimeForLocalUsage(this.minTime, this.$utilities.getTimezoneOffsetMinutes(), this.$utilities.getBrowserTimezoneOffsetMinutes());
this.currentMinDate = [this.$utilities.getLocalDatetimeFromUnixTime(minTime)];
this.dateRange[0] = this.$utilities.getLocalDatetimeFromUnixTime(this.$utilities.getDummyUnixTimeForLocalUsage(this.minTime, this.$utilities.getTimezoneOffsetMinutes(), this.$utilities.getBrowserTimezoneOffsetMinutes()));
}
if (this.maxTime) {
const maxTime = this.$utilities.getDummyUnixTimeForLocalUsage(this.maxTime, this.$utilities.getTimezoneOffsetMinutes(), this.$utilities.getBrowserTimezoneOffsetMinutes());
this.currentMaxDate = [this.$utilities.getLocalDatetimeFromUnixTime(maxTime)];
this.dateRange[1] = this.$utilities.getLocalDatetimeFromUnixTime(this.$utilities.getDummyUnixTimeForLocalUsage(this.maxTime, this.$utilities.getTimezoneOffsetMinutes(), this.$utilities.getBrowserTimezoneOffsetMinutes()));
}
},
onSheetClosed() {
this.$emit('update:show', false);
},
confirm() {
if (!this.currentMinDate || !this.currentMaxDate) {
if (!this.dateRange[0] || !this.dateRange[1]) {
return;
}
let currentMinDate = this.currentMinDate;
const currentMinDate = this.dateRange[0];
const currentMaxDate = this.dateRange[1];
if (this.$utilities.isArray(this.currentMinDate)) {
currentMinDate = this.currentMinDate[0];
}
let currentMaxDate = this.currentMaxDate;
if (this.$utilities.isArray(this.currentMaxDate)) {
currentMaxDate = this.currentMaxDate[0];
}
let minUnixTime = this.$utilities.getMinuteFirstUnixTime(currentMinDate);
let maxUnixTime = this.$utilities.getMinuteLastUnixTime(currentMaxDate);
let minUnixTime = this.$utilities.getUnixTime(currentMinDate);
let maxUnixTime = this.$utilities.getUnixTime(currentMaxDate);
if (minUnixTime < 0 || maxUnixTime < 0) {
this.$toast('Date is too early');
@@ -160,9 +170,3 @@ export default {
}
}
</script>
<style>
.list .date-range-sheet-time-item > .item-content {
padding-left: 0;
}
</style>
@@ -0,0 +1,111 @@
<template>
<f7-sheet swipe-to-close swipe-handler=".swipe-handler" class="date-time-selection-sheet" style="height:auto"
:opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-toolbar>
<div class="swipe-handler"></div>
<div class="left">
<f7-link :text="$t('Now')" @click="setCurrentTime"></f7-link>
</div>
<div class="right">
<f7-link :text="$t('Done')" @click="confirm"></f7-link>
</div>
</f7-toolbar>
<f7-page-content>
<vue-date-picker inline enable-seconds auto-apply
month-name-format="long"
six-weeks="center"
class="justify-content-center"
:clearable="false"
:dark="isDarkMode"
:week-start="firstDayOfWeek"
:year-range="yearRange"
:day-names="dayNames"
:is24="is24Hour"
v-model="dateTime">
<template #month="{ text }">
{{ $t(`datetime.${text}.short`) }}
</template>
<template #month-overlay-value="{ text }">
{{ $t(`datetime.${text}.short`) }}
</template>
</vue-date-picker>
</f7-page-content>
</f7-sheet>
</template>
<script>
export default {
props: [
'modelValue',
'show'
],
emits: [
'update:modelValue',
'update:show'
],
data() {
const self = this;
let value = self.$utilities.getCurrentUnixTime();
if (self.modelValue) {
value = self.modelValue;
}
return {
yearRange: [
2000,
this.$utilities.getYear(this.$utilities.getCurrentDateTime()) + 1
],
dateTime: this.$utilities.getLocalDatetimeFromUnixTime(value),
}
},
computed: {
isDarkMode() {
return this.$root.isDarkMode;
},
firstDayOfWeek() {
return this.$store.getters.currentUserFirstDayOfWeek;
},
dayNames() {
return this.$utilities.arrangeArrayWithNewStartIndex(this.$locale.getAllMinWeekdayNames(), this.firstDayOfWeek);
},
is24Hour() {
return this.$locale.isLongTime24HourFormat();
}
},
methods: {
onSheetOpen() {
if (this.modelValue) {
this.dateTime = this.$utilities.getLocalDatetimeFromUnixTime(this.modelValue)
}
},
onSheetClosed() {
this.$emit('update:show', false);
},
setCurrentTime() {
this.dateTime = this.$utilities.getLocalDatetimeFromUnixTime(this.$utilities.getCurrentUnixTime())
},
confirm() {
if (!this.dateTime) {
return;
}
const unixTime = this.$utilities.getUnixTime(this.dateTime);
if (unixTime < 0) {
this.$toast('Date is too early');
return;
}
this.$emit('update:modelValue', unixTime);
this.$emit('update:show', false);
}
}
}
</script>
<style>
.date-time-selection-sheet .dp__menu {
border: 0;
}
</style>
+33 -19
View File
@@ -1,27 +1,27 @@
<template>
<f7-sheet :class="{ 'icon-selection-huge-sheet': hugeIconRows }" :opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-sheet swipe-to-close swipe-handler=".swipe-handler"
:class="heightClass" :opened="show"
@sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-toolbar>
<div class="swipe-handler"></div>
<div class="left"></div>
<div class="right">
<f7-link sheet-close :text="$t('Done')"></f7-link>
</div>
</f7-toolbar>
<f7-page-content>
<f7-block class="margin-vertical">
<f7-row class="padding-vertical-half padding-horizontal-half"
:class="{ 'row-has-selected-item': hasSelectedIcon(row) }"
v-for="(row, idx) in allIconRows" :key="idx">
<f7-col class="text-align-center" v-for="iconInfo in row" :key="iconInfo.id">
<f7-icon :icon="iconInfo.icon"
:style="color | iconStyle('default', 'var(--default-icon-color)')"
@click.native="onIconClicked(iconInfo)">
<f7-block class="margin-vertical no-padding">
<div class="grid grid-cols-7 padding-vertical-half padding-horizontal-half"
:class="{ 'row-has-selected-item': hasSelectedIcon(row) }"
:key="idx" v-for="(row, idx) in allIconRows">
<div class="text-align-center" :key="iconInfo.id" v-for="iconInfo in row">
<ItemIcon icon-type="fixed" :icon-id="iconInfo.icon" :color="color" @click="onIconClicked(iconInfo)">
<f7-badge color="default" class="right-bottom-icon" v-if="currentValue && currentValue === iconInfo.id">
<f7-icon f7="checkmark_alt"></f7-icon>
</f7-badge>
</f7-icon>
</f7-col>
<f7-col v-for="idx in (itemPerRow - row.length)" :key="idx"></f7-col>
</f7-row>
</ItemIcon>
</div>
</div>
</f7-block>
</f7-page-content>
</f7-sheet>
@@ -30,17 +30,21 @@
<script>
export default {
props: [
'value',
'modelValue',
'color',
'columnCount',
'show',
'allIconInfos'
],
emits: [
'update:modelValue',
'update:show'
],
data() {
const self = this;
return {
currentValue: self.value,
currentValue: self.modelValue,
itemPerRow: self.columnCount || 7
}
},
@@ -71,18 +75,24 @@ export default {
return ret;
},
hugeIconRows() {
return this.allIconRows.length > 10;
heightClass() {
if (this.allIconRows.length > 10) {
return 'icon-selection-huge-sheet';
} else if (this.allIconRows.length > 6) {
return 'icon-selection-large-sheet';
} else {
return '';
}
}
},
methods: {
onIconClicked(iconInfo) {
this.currentValue = iconInfo.id;
this.$emit('input', this.currentValue);
this.$emit('update:modelValue', this.currentValue);
this.$emit('update:show', false);
},
onSheetOpen(event) {
this.currentValue = this.value;
this.currentValue = this.modelValue;
this.scrollToSelectedItem(event.$el);
},
onSheetClosed() {
@@ -128,6 +138,10 @@ export default {
<style>
@media (min-height: 630px) {
.icon-selection-large-sheet {
height: 310px;
}
.icon-selection-huge-sheet {
height: 400px;
}
+51 -9
View File
@@ -1,18 +1,21 @@
<template>
<f7-sheet style="height:auto" :opened="show" @sheet:closed="onSheetClosed">
<f7-page-content>
<f7-sheet swipe-to-close swipe-handler=".swipe-handler" style="height:auto"
:opened="show" @sheet:closed="onSheetClosed">
<div class="swipe-handler" style="z-index: 10"></div>
<f7-page-content class="margin-top no-padding-top">
<div class="display-flex padding justify-content-space-between align-items-center">
<div style="font-size: 18px" v-if="title"><b>{{ title }}</b></div>
</div>
<div class="padding-horizontal padding-bottom">
<p class="no-margin-top margin-bottom-half" v-if="hint">
<span>{{ hint }}</span>
<f7-link class="icon-after-text"
<f7-link id="copy-to-clipboard-icon" ref="copyToClipboardIcon"
class="icon-after-text"
icon-only icon-f7="doc_on_doc" icon-size="16px"
v-if="enableCopy"
v-clipboard:copy="information" v-clipboard:success="onCopied"></f7-link>
></f7-link>
</p>
<textarea class="information-content full-line" :rows="rowCount" readonly="readonly" v-model="information"></textarea>
<textarea class="information-content full-line" readonly="readonly" :rows="rowCount" :value="information"></textarea>
<div class="margin-top text-align-center">
<f7-link @click="cancel" :text="$t('Close')"></f7-link>
</div>
@@ -31,14 +34,53 @@ export default {
'enableCopy',
'show'
],
emits: [
'update:show',
'info:copied'
],
data() {
return {
clipboardHolder: null
}
},
mounted() {
this.makeCopyToClipboardClickable();
},
updated() {
this.makeCopyToClipboardClickable();
},
watch: {
'information': function (newValue) {
if (this.clipboardHolder) {
this.$utilities.changeClipboardObjectText(this.clipboardHolder, newValue);
}
}
},
methods: {
onSheetClosed() {
this.$emit('update:show', false);
},
onCopied() {
this.$emit('info:copied');
this.close();
},
cancel() {
this.close();
},
makeCopyToClipboardClickable() {
const self = this;
if (self.clipboardHolder) {
return;
}
if (self.$refs.copyToClipboardIcon) {
self.clipboardHolder = self.$utilities.makeButtonCopyToClipboard({
el: '#copy-to-clipboard-icon',
text: self.information,
successCallback: function () {
self.$emit('info:copied');
}
});
}
},
close() {
this.$emit('update:show', false);
}
}
+127
View File
@@ -0,0 +1,127 @@
<template>
<f7-icon :f7="f7Icon" :icon="icon" :style="style">
<slot></slot>
</f7-icon>
</template>
<script>
export default {
props: [
'iconType',
'iconId',
'color',
'defaultColor',
'additionalColorAttr'
],
computed: {
f7Icon() {
if (this.iconType === 'fixed-f7') {
return this.iconId;
} else {
return '';
}
},
icon() {
if (this.iconType === 'account') {
return this.getAccountIcon(this.iconId);
} else if (this.iconType === 'category') {
return this.getCategoryIcon(this.iconId);
} else if (this.iconType === 'fixed') {
return this.iconId;
} else {
return '';
}
},
style() {
let defaultColor = 'var(--default-icon-color)';
if (this.defaultColor) {
defaultColor = this.defaultColor;
}
if (this.iconType === 'account') {
return this.getAccountIconStyle(this.color, defaultColor, this.additionalColorAttr);
} else if (this.iconType === 'category') {
return this.getCategoryIconStyle(this.color, defaultColor, this.additionalColorAttr);
} else {
return this.getDefaultIconStyle(this.color, defaultColor, this.additionalColorAttr);
}
}
},
methods: {
getAccountIcon(iconId) {
if (this.$utilities.isNumber(iconId)) {
iconId = iconId.toString();
}
if (!this.$constants.icons.allAccountIcons[iconId]) {
return this.$constants.icons.defaultAccountIcon.icon;
}
return this.$constants.icons.allAccountIcons[iconId].icon;
},
getCategoryIcon(iconId) {
if (this.$utilities.isNumber(iconId)) {
iconId = iconId.toString();
}
if (!this.$constants.icons.allCategoryIcons[iconId]) {
return this.$constants.icons.defaultCategoryIcon.icon;
}
return this.$constants.icons.allCategoryIcons[iconId].icon;
},
getAccountIconStyle(color, defaultColor, additionalColorAttr) {
if (color && color !== this.$constants.colors.defaultAccountColor) {
color = '#' + color;
} else {
color = defaultColor;
}
const ret = {
color: color
};
if (additionalColorAttr) {
ret[additionalColorAttr] = color;
}
return ret;
},
getCategoryIconStyle(color, defaultColor, additionalColorAttr) {
if (color && color !== this.$constants.colors.defaultCategoryColor) {
color = '#' + color;
} else {
color = defaultColor;
}
const ret = {
color: color
};
if (additionalColorAttr) {
ret[additionalColorAttr] = color;
}
return ret;
},
getDefaultIconStyle(color, defaultColor, additionalColorAttr) {
if (color && color !== this.$constants.colors.defaultColor) {
color = '#' + color;
} else {
color = defaultColor;
}
const ret = {
color: color
};
if (additionalColorAttr) {
ret[additionalColorAttr] = color;
}
return ret;
}
}
}
</script>
@@ -1,25 +1,29 @@
<template>
<f7-sheet :class="{ 'list-item-selection-huge-sheet': hugeListItemRows }" :opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-sheet swipe-to-close swipe-handler=".swipe-handler"
:class="heightClass" :opened="show"
@sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-toolbar>
<div class="swipe-handler"></div>
<div class="left"></div>
<div class="right">
<f7-link sheet-close :text="$t('Done')"></f7-link>
</div>
</f7-toolbar>
<f7-page-content>
<f7-list no-hairlines class="no-margin-top no-margin-bottom">
<f7-list dividers class="no-margin-vertical">
<f7-list-item link="#" no-chevron
v-for="(item, index) in items"
:key="item | itemKeyValue(index, keyField, valueType)"
:title="$tIf((titleField ? item[titleField] : item), titleI18n)"
:value="getItemValue(item, index, valueField, valueType)"
:class="{ 'list-item-selected': isSelected(item, index) }"
:value="item | itemKeyValue(index, valueField, valueType)"
:title="item | itemFieldContent(titleField, item, titleI18n)"
:key="getItemValue(item, index, keyField, valueType)"
v-for="(item, index) in items"
@click="onItemClicked(item, index)">
<f7-icon slot="media"
:icon="item[iconField] | icon(iconType)"
:style="item[colorField] | iconStyle(iconType, 'var(--default-icon-color)')"
v-if="iconField"></f7-icon>
<f7-icon slot="after" class="list-item-checked-icon" f7="checkmark_alt" v-if="isSelected(item, index)"></f7-icon>
<template #content-start>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" :style="{ 'color': isSelected(item, index) ? '' : 'transparent' }"></f7-icon>
</template>
<template #media v-if="iconField">
<ItemIcon :icon-type="iconType" :icon-id="item[iconField]" :color="item[colorField]"></ItemIcon>
</template>
</f7-list-item>
</f7-list>
</f7-page-content>
@@ -29,7 +33,7 @@
<script>
export default {
props: [
'value',
'modelValue',
'valueType', // item or index
'keyField', // for value type == item
'valueField', // for value type == item
@@ -41,19 +45,38 @@ export default {
'items',
'show'
],
emits: [
'update:modelValue',
'update:show'
],
data() {
const self = this;
return {
currentValue: self.value
currentValue: self.modelValue
}
},
computed: {
hugeListItemRows() {
return this.items.length > 10;
heightClass() {
if (this.items.length > 10) {
return 'list-item-selection-huge-sheet';
} else if (this.items.length > 6) {
return 'list-item-selection-large-sheet';
} else {
return '';
}
}
},
methods: {
getItemValue(item, index, fieldName, valueType) {
if (valueType === 'index') {
return index;
} else if (fieldName) {
return item[fieldName];
} else {
return item;
}
},
onItemClicked(item, index) {
if (this.valueType === 'index') {
this.currentValue = index;
@@ -65,15 +88,15 @@ export default {
}
}
this.$emit('input', this.currentValue);
this.$emit('update:show', false);
this.$emit('update:modelValue', this.currentValue);
this.close();
},
onSheetOpen(event) {
this.currentValue = this.value;
this.currentValue = this.modelValue;
this.scrollToSelectedItem(event.$el);
},
onSheetClosed() {
this.$emit('update:show', false);
this.close();
},
isSelected(item, index) {
if (this.valueType === 'index') {
@@ -106,17 +129,9 @@ export default {
}
container.scrollTop(targetPos);
}
},
filters: {
itemKeyValue(item, index, fieldName, valueType) {
if (valueType === 'index') {
return index;
} else if (fieldName) {
return item[fieldName];
} else {
return item;
}
},
close() {
this.$emit('update:show', false);
}
}
}
@@ -124,6 +139,10 @@ export default {
<style>
@media (min-height: 630px) {
.list-item-selection-large-sheet {
height: 310px;
}
.list-item-selection-huge-sheet {
height: 400px;
}
+153
View File
@@ -0,0 +1,153 @@
<template>
<f7-sheet swipe-to-close swipe-handler=".swipe-handler" style="height:auto"
:opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-toolbar>
<div class="swipe-handler"></div>
<div class="left"></div>
<div class="right">
<f7-link :text="$t('Done')" @click="save"></f7-link>
</div>
</f7-toolbar>
<f7-page-content class="no-margin-vertical no-padding-vertical" v-if="knownMapProvider">
<div ref="map" style="height: 400px; width: 100%"></div>
</f7-page-content>
<f7-page-content class="no-margin-top no-padding-top" v-else-if="!knownMapProvider">
<div class="display-flex padding justify-content-space-between align-items-center">
<div style="font-size: 18px"><b>{{ $t('Unsupported Map Provider') }}</b></div>
</div>
<div class="padding-horizontal padding-bottom">
<p class="no-margin">{{ $t('Please refresh the page and try again. If the error is still displayed, make sure that server map settings are set correctly.') }}</p>
<div class="margin-top text-align-center">
<f7-link @click="close" :text="$t('Close')"></f7-link>
</div>
</div>
</f7-page-content>
</f7-sheet>
</template>
<script>
export default {
props: [
'modelValue',
'show'
],
emits: [
'update:modelValue',
'update:show'
],
data() {
let knownMapProvider = false;
if (this.$settings.getMapProvider() === 'openstreetmap') {
knownMapProvider = true;
}
return {
knownMapProvider: knownMapProvider,
leaflet: null,
tileLayer: null,
zoomControl: null,
attribution: null,
marker: null,
initCenter: [ 0, 0 ],
zoomLevel: 1
}
},
methods: {
save() {
this.$emit('update:show', false);
},
onSheetOpen() {
let isFirstInit = false;
let centerChanged = false;
if (this.modelValue && (this.modelValue.longitude || this.modelValue.latitude)) {
if (this.initCenter[0] !== this.modelValue.latitude || this.initCenter[1] !== this.modelValue.longitude) {
this.initCenter[0] = this.modelValue.latitude;
this.initCenter[1] = this.modelValue.longitude;
this.zoomLevel = 14;
centerChanged = true;
}
} else if (!this.modelValue || (!this.modelValue.longitude && !this.modelValue.latitude)) {
if (this.initCenter[0] || this.initCenter[1]) {
this.initCenter[0] = 0;
this.initCenter[1] = 0;
this.zoomLevel = 1;
centerChanged = true;
}
}
if (this.knownMapProvider && this.$settings.getMapProvider() === 'openstreetmap') {
if (!this.leaflet) {
const mapContainer = this.$refs.map;
this.leaflet = this.$map.leaflet.map(mapContainer, {
attributionControl: false,
zoomControl: false
});
const mapTileImageUrl = this.$map.generateOpenStreetMapTileImageUrl();
this.tileLayer = this.$map.leaflet.tileLayer(mapTileImageUrl.url, {
subdomains: mapTileImageUrl.subDomains,
maxZoom: 19
});
this.tileLayer.addTo(this.leaflet);
this.zoomControl = this.$map.leaflet.control.zoom({
zoomInTitle: this.$t('Zoom in'),
zoomOutTitle: this.$t('Zoom out'),
});
this.zoomControl.addTo(this.leaflet);
this.attribution = this.$map.leaflet.control.attribution({
prefix: false
});
this.attribution.addAttribution('&copy; <a href="http://www.openstreetmap.org/copyright" class="external" target="_blank">OpenStreetMap</a>');
this.attribution.addTo(this.leaflet);
isFirstInit = true;
}
if (isFirstInit || centerChanged) {
this.leaflet.setView(this.initCenter, this.zoomLevel);
}
if (centerChanged && this.zoomLevel > 1) {
if (!this.marker) {
const markerIcon = this.$map.leaflet.icon({
iconUrl: 'img/map-marker-icon.png',
iconRetinaUrl: 'img/map-marker-icon-2x.png',
iconSize: [25, 32],
iconAnchor: [12, 32],
shadowUrl: 'img/map-marker-shadow.png',
shadowSize: [41, 32]
});
this.marker = this.$map.leaflet.marker(this.initCenter, {
icon: markerIcon
});
this.marker.addTo(this.leaflet);
} else {
this.marker.setLatLng(this.initCenter);
}
} else if (centerChanged && this.zoomLevel <= 1) {
if (this.marker) {
this.marker.remove();
this.marker = null;
}
}
} else {
this.knownMapProvider = false;
}
},
onSheetClosed() {
this.close();
},
close() {
this.$emit('update:show', false);
}
}
}
</script>
+27 -18
View File
@@ -1,10 +1,12 @@
<template>
<f7-sheet class="numpad-sheet" :opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-page-content class="no-margin no-padding-top">
<f7-row class="numpad-values">
<f7-sheet swipe-to-close swipe-handler=".swipe-handler" class="numpad-sheet" style="height: auto"
:opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<div class="swipe-handler" style="z-index: 10"></div>
<f7-page-content class="margin-top no-padding-top">
<div class="numpad-values">
<span class="numpad-value" :style="{ fontSize: currentDisplayFontSize + 'px' }">{{ currentDisplay }}</span>
</f7-row>
<f7-row class="numpad-buttons">
</div>
<div class="numpad-buttons">
<f7-button class="numpad-button numpad-button-num" @click="inputNum(7)">
<span class="numpad-button-text numpad-button-text-normal">7</span>
</f7-button>
@@ -47,7 +49,7 @@
<f7-button class="numpad-button numpad-button-num" @click="inputNum(0)">
<span class="numpad-button-text numpad-button-text-normal">0</span>
</f7-button>
<f7-button class="numpad-button numpad-button-num" @click="backspace" @taphold.native="clear()">
<f7-button class="numpad-button numpad-button-num" @click="backspace" @taphold="clear()">
<span class="numpad-button-text numpad-button-text-normal">
<f7-icon f7="delete_left"></f7-icon>
</span>
@@ -55,7 +57,7 @@
<f7-button class="numpad-button numpad-button-confirm no-right-border no-bottom-border" fill @click="confirm()">
<span :class="{ 'numpad-button-text': true, 'numpad-button-text-confirm': !currentSymbol }">{{ confirmText }}</span>
</f7-button>
</f7-row>
</div>
</f7-page-content>
</f7-sheet>
</template>
@@ -63,18 +65,22 @@
<script>
export default {
props: [
'value',
'modelValue',
'minValue',
'maxValue',
'show'
],
emits: [
'update:modelValue',
'update:show'
],
data() {
const self = this;
return {
previousValue: '',
currentSymbol: '',
currentValue: self.getStringValue(self.value)
currentValue: self.getStringValue(self.modelValue)
}
},
computed: {
@@ -103,7 +109,7 @@ export default {
if (this.currentSymbol) {
return '=';
} else {
return this.$i18n.t('OK');
return this.$t('OK');
}
}
},
@@ -125,7 +131,7 @@ export default {
return str;
}
let integer = str.substr(0, dotPos);
let integer = str.substring(0, dotPos);
let decimals = str.substring(dotPos + 1, str.length);
let newDecimals = '';
@@ -232,7 +238,7 @@ export default {
return;
}
this.currentValue = this.currentValue.substr(0, this.currentValue.length - 1);
this.currentValue = this.currentValue.substring(0, this.currentValue.length - 1);
},
clear() {
this.currentValue = '';
@@ -291,17 +297,20 @@ export default {
} else {
const value = this.$utilities.stringCurrencyToNumeric(this.currentValue);
this.$emit('input', value);
this.$emit('update:show', false);
this.$emit('update:modelValue', value);
this.close();
return true;
}
},
close() {
this.$emit('update:show', false);
},
onSheetOpen() {
this.currentValue = this.getStringValue(this.value);
this.currentValue = this.getStringValue(this.modelValue);
},
onSheetClosed() {
this.$emit('update:show', false);
this.close();
}
}
}
@@ -322,7 +331,6 @@ export default {
padding-left: 16px;
line-height: 1;
height: 50px;
justify-content: center;
align-items: center;
box-sizing: border-box;
user-select: none;
@@ -346,6 +354,7 @@ export default {
align-items: center;
box-sizing: border-box;
user-select: none;
touch-action: none;
}
.numpad-button-num {
@@ -371,7 +380,7 @@ export default {
color: var(--f7-color-black);
}
.theme-dark .numpad-button-text-normal {
.dark .numpad-button-text-normal {
color: var(--f7-color-white);
}
+22 -11
View File
@@ -1,23 +1,26 @@
<template>
<f7-sheet style="height:auto" :opened="show"
@sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-page-content>
<f7-sheet swipe-to-close swipe-handler=".swipe-handler" style="height:auto"
:opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<div class="swipe-handler" style="z-index: 10"></div>
<f7-page-content class="margin-top no-padding-top">
<div class="display-flex padding justify-content-space-between align-items-center">
<div style="font-size: 18px" v-if="title"><b>{{ title }}</b></div>
</div>
<div class="padding-horizontal padding-bottom">
<p class="no-margin-top margin-bottom-half" v-if="hint">{{ hint }}</p>
<p class="no-margin" v-if="hint">{{ hint }}</p>
<slot></slot>
<f7-list no-hairlines class="no-margin-top margin-bottom">
<f7-list strong class="no-margin">
<f7-list-input
type="number"
autocomplete="one-time-code"
outline
floating-label
clear-button
class="no-margin no-padding-bottom"
:label="$t('Passcode')"
:placeholder="$t('Passcode')"
:value="currentPasscode"
@input="currentPasscode = $event.target.value"
@keyup.enter.native="confirm()"
v-model:value="currentPasscode"
@keyup.enter="confirm()"
></f7-list-input>
</f7-list>
<f7-button large fill
@@ -36,13 +39,18 @@
<script>
export default {
props: [
'value',
'modelValue',
'title',
'hint',
'confirmDisabled',
'cancelDisabled',
'show'
],
emits: [
'update:modelValue',
'update:show',
'passcode:confirm'
],
data() {
return {
currentPasscode: ''
@@ -53,17 +61,20 @@ export default {
this.currentPasscode = '';
},
onSheetClosed() {
this.$emit('update:show', false);
this.close();
},
confirm() {
if (!this.currentPasscode || this.confirmDisabled) {
return;
}
this.$emit('input', this.currentPasscode);
this.$emit('update:modelValue', this.currentPasscode);
this.$emit('passcode:confirm', this.currentPasscode);
},
cancel() {
this.close();
},
close() {
this.$emit('update:show', false);
}
}
+26 -12
View File
@@ -1,26 +1,31 @@
<template>
<f7-sheet style="height:auto" :opened="show"
@sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-page-content>
<f7-sheet swipe-to-close swipe-handler=".swipe-handler" style="height:auto"
:opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<div class="swipe-handler" style="z-index: 10"></div>
<f7-page-content class="margin-top no-padding-top">
<div class="display-flex padding justify-content-space-between align-items-center">
<div style="font-size: 18px" v-if="title"><b>{{ title }}</b></div>
</div>
<div class="padding-horizontal padding-bottom">
<p class="no-margin-top margin-bottom-half" v-if="hint">{{ hint }}</p>
<f7-list no-hairlines class="no-margin-top margin-bottom">
<p class="no-margin" v-if="hint">{{ hint }}</p>
<f7-list strong class="no-margin">
<f7-list-input
type="password"
autocomplete="current-password"
outline
floating-label
clear-button
:placeholder="$t('Password')"
:value="currentPassword"
@input="currentPassword = $event.target.value"
@keyup.enter.native="confirm()"
class="no-margin no-padding-bottom"
:class="color ? 'color-' + color : ''"
:label="$t('Current Password')"
:placeholder="$t('Current Password')"
v-model:value="currentPassword"
@keyup.enter="confirm()"
></f7-list-input>
</f7-list>
<f7-button large fill
:class="{ 'disabled': !currentPassword || confirmDisabled }"
:color="color || 'primary'"
:text="$t('Continue')"
@click="confirm">
</f7-button>
@@ -35,13 +40,19 @@
<script>
export default {
props: [
'value',
'modelValue',
'title',
'hint',
'color',
'confirmDisabled',
'cancelDisabled',
'show'
],
emits: [
'update:modelValue',
'update:show',
'password:confirm'
],
data() {
return {
currentPassword: ''
@@ -52,17 +63,20 @@ export default {
this.currentPassword = '';
},
onSheetClosed() {
this.$emit('update:show', false);
this.close();
},
confirm() {
if (!this.currentPassword || this.confirmDisabled) {
return;
}
this.$emit('input', this.currentPassword);
this.$emit('update:modelValue', this.currentPassword);
this.$emit('password:confirm', this.currentPassword);
},
cancel() {
this.close();
},
close() {
this.$emit('update:show', false);
}
}
+46 -21
View File
@@ -4,14 +4,15 @@
<circle class="pie-chart-background" cx="0" cy="0" :r="diameter"></circle>
<circle class="pie-chart-item"
v-for="(item, idx) in validItems" :key="idx"
fill="transparent"
cx="0" cy="0"
:r="diameter / 2"
:stroke="item.color | defaultIconColor('var(--default-icon-color)')"
:stroke="getColor(item.color)"
:stroke-width="diameter"
:stroke-dasharray="item | itemStrokeDash(circumference)"
:stroke-dashoffset="item | itemDashOffset(validItems, circumference, itemCommonDashOffset)"
:stroke-dasharray="getItemStrokeDash(item)"
:stroke-dashoffset="getItemDashOffset(item, validItems, itemCommonDashOffset)"
:key="idx"
v-for="(item, idx) in validItems"
@click="switchSelectedIndex(idx)">
</circle>
@@ -48,8 +49,8 @@
<span class="skeleton-text">Percent</span>
</f7-chip>
<f7-chip outline
:text="(selectedItem.percent) | percent(2, '&lt;0.01')"
:style="(selectedItem ? selectedItem.color : '') | iconStyle('default', 'var(--default-icon-color)', '--f7-chip-outline-border-color')"
:text="$utilities.formatPercent(selectedItem.percent, 2, '&lt;0.01')"
:style="getColorStyle(selectedItem ? selectedItem.color : '', '--f7-chip-outline-border-color')"
v-else-if="!skeleton"></f7-chip>
</p>
<p v-else-if="!validItems || !validItems.length">
@@ -59,7 +60,7 @@
<span class="skeleton-text" v-if="skeleton">Name</span>
<span v-else-if="!skeleton && selectedItem.name">{{ selectedItem.name }}</span>
<span class="skeleton-text" v-if="skeleton">Value</span>
<span v-else-if="!skeleton && showValue" :style="(selectedItem ? selectedItem.color : '') | iconStyle('default', 'var(--default-icon-color)')">{{ selectedItem.value | currency(selectedItem.currency || defaultCurrency) }}</span>
<span v-else-if="!skeleton && showValue" :style="getColorStyle(selectedItem ? selectedItem.color : '')">{{ $locale.getDisplayCurrency(selectedItem.value, (selectedItem.currency || defaultCurrency)) }}</span>
<f7-icon class="item-navigate-icon" f7="chevron_right" v-if="enableClickItem"></f7-icon>
</f7-link>
<f7-link :no-link-class="true" v-else-if="!validItems || !validItems.length">
@@ -107,6 +108,9 @@ export default {
'enableClickItem',
'centerTextBackground',
],
emits: [
'click'
],
data: function () {
const diameter = 100;
@@ -195,8 +199,11 @@ export default {
}
},
watch: {
'items': function () {
this.selectedIndex = 0;
'items': {
handler() {
this.selectedIndex = 0;
},
deep: true
}
},
methods: {
@@ -216,14 +223,32 @@ export default {
if (this.enableClickItem) {
this.$emit('click', item.sourceItem);
}
}
},
filters: {
itemStrokeDash(item, circumference) {
const length = item.actualPercent * circumference;
return `${length} ${circumference - length}`;
},
itemDashOffset(item, items, circumference, offset) {
getColor: function (color) {
if (color && color !== this.$constants.colors.defaultColor) {
color = '#' + color;
} else {
color = 'var(--default-icon-color)';
}
return color;
},
getColorStyle: function (color, additionalFieldName) {
const ret = {
color: this.getColor(color)
};
if (additionalFieldName) {
ret[additionalFieldName] = ret.color;
}
return ret;
},
getItemStrokeDash(item) {
const length = item.actualPercent * this.circumference;
return `${length} ${this.circumference - length}`;
},
getItemDashOffset(item, items, offset) {
let allPreviousPercent = 0;
for (let i = 0; i < items.length; i++) {
@@ -237,17 +262,17 @@ export default {
}
if (offset) {
offset += circumference / 4;
offset += this.circumference / 4;
} else {
offset = circumference / 4;
offset = this.circumference / 4;
}
if (allPreviousPercent <= 0) {
return offset;
}
const allPreviousLength = allPreviousPercent * circumference;
return circumference - allPreviousLength + offset;
const allPreviousLength = allPreviousPercent * this.circumference;
return this.circumference - allPreviousLength + offset;
}
}
}
@@ -314,7 +339,7 @@ export default {
fill: #f0f0f0;
}
.theme-dark .pie-chart-background {
.dark .pie-chart-background {
fill: #181818;
}
+248
View File
@@ -0,0 +1,248 @@
<template>
<div class="pin-code-input grid grid-gap" :class="'grid-cols-' + length">
<div class="input input-outline input-with-value"
:key="index" v-for="(code, index) in codes">
<input min="0" maxlength="1" pattern="[0-9]*"
:ref="`pin-code-input-${index}`"
:value="codes[index].value"
:type="codes[index].inputType"
@keydown="onKeydown(index, $event)"
@paste="onPaste(index, $event)"
@change="onInput(index, $event)"
/>
</div>
</div>
</template>
<script>
export default {
props: [
'modelValue',
'secure',
'length'
],
emits: [
'update:modelValue',
'pincode:confirm'
],
data() {
return {
codes: []
}
},
computed: {
finalPinCode() {
let finalPinCode = '';
for (let i = 0; i < this.codes.length; i++) {
if (this.codes[i].value) {
finalPinCode += this.codes[i].value;
} else {
break;
}
}
return finalPinCode;
}
},
watch: {
'length': function (newValue) {
this.init(newValue, this.modelValue);
},
'modelValue': function (newValue) {
if (newValue === this.finalPinCode) {
return;
}
this.init(this.length, newValue);
},
'codes': {
handler() {
this.$emit('update:modelValue', this.finalPinCode);
},
deep: true
}
},
created() {
this.init(this.length, this.modelValue);
},
methods: {
init(length, value) {
this.codes.length = 0;
for (let i = 0; i < length; i++) {
const code = {
value: '',
inputType: 'tel',
inputTimer: null
};
if (value && value[i]) {
code.value = value[i];
if (this.secure) {
code.inputType = 'password';
}
}
this.codes.push(code);
}
},
autoFillText(index, text) {
let lastIndex = index;
for (let i = index, j = 0; i < this.codes.length && j < text.length; i++, j++) {
if (text[j] < '0' || text[j] > '9') {
this.codes[i].value = '';
this.$forceUpdate();
break;
}
this.codes[i].value = text[j];
this.setInputType(i);
lastIndex = i;
}
this.setFocus(lastIndex);
if (this.finalPinCode.length === this.length) {
this.$emit('pincode:confirm', this.finalPinCode);
}
},
setInputType(index) {
const self = this;
if (!self.secure) {
return;
}
if (!self.codes[index].value) {
self.codes[index].inputType = 'tel';
return;
}
if (self.codes[index].inputTimer) {
return;
}
self.codes[index].inputTimer = setTimeout(() => {
if (self.codes[index].value) {
self.codes[index].inputType = 'password';
} else {
self.codes[index].inputType = 'tel';
}
self.codes[index].inputTimer = null;
}, 300);
},
setFocus(index) {
const refId = `pin-code-input-${index}`;
const ref = this.$refs[refId];
if (ref && ref[0]) {
ref[0].focus();
ref[0].select();
}
},
setPreviousFocus(index) {
if (index > 0) {
this.setFocus(index - 1);
}
},
setNextFocus(index) {
if (index < this.length - 1) {
this.setFocus(index + 1);
}
},
onKeydown(index, event) {
if (event.code === 'Enter' && this.finalPinCode.length === this.length) {
this.$emit('pincode:confirm', this.finalPinCode);
event.preventDefault();
return;
}
if (event.code === 'ArrowLeft' || (event.shiftKey && event.code === 'Tab')) {
this.setPreviousFocus(index);
event.preventDefault();
return;
}
if (event.code === 'ArrowRight' || (!event.shiftKey && event.code === 'Tab')) {
this.setNextFocus(index);
event.preventDefault();
return;
}
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyV') {
return;
}
if (event.code === 'Backspace' || event.code === 'Delete' || event.code === 'Del') {
for (let i = index; i < this.codes.length; i++) {
this.codes[i].value = '';
this.setInputType(i);
}
if (event.code === 'Backspace') {
this.setPreviousFocus(index);
}
event.preventDefault();
return;
}
if (event.code.indexOf('Digit') === 0 && event.code.length === 6) {
this.codes[index].value = event.key;
this.setInputType(index);
this.setNextFocus(index);
if (this.finalPinCode.length === this.length) {
this.$emit('pincode:confirm', this.finalPinCode);
}
}
event.preventDefault();
},
onPaste(index, event) {
if (!event.clipboardData) {
event.preventDefault();
return;
}
const text = event.clipboardData.getData('Text');
if (!text) {
event.preventDefault();
return;
}
this.autoFillText(index, text);
event.preventDefault();
},
onInput(index, event) {
if (!event.target.value) {
event.preventDefault();
return;
}
this.autoFillText(index, event.target.value);
event.preventDefault();
}
}
}
</script>
<style>
.pin-code-input input {
text-align: center;
padding-left: 10px;
padding-right: 10px;
height: 60px !important;
}
.pin-code-input.grid.grid-gap {
--f7-grid-gap: 4px;
}
</style>
+14 -8
View File
@@ -1,15 +1,16 @@
<template>
<f7-sheet style="height:auto" :opened="show"
@sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-page-content>
<f7-sheet swipe-to-close swipe-handler=".swipe-handler" style="height:auto"
:opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<div class="swipe-handler" style="z-index: 10"></div>
<f7-page-content class="margin-top no-padding-top">
<div class="display-flex padding justify-content-space-between align-items-center">
<div style="font-size: 18px"><b>{{ title }}</b></div>
</div>
<div class="padding-horizontal padding-bottom">
<p class="no-margin-top margin-bottom-half">{{ hint }}</p>
<f7-list no-hairlines class="no-margin-top margin-bottom">
<p class="no-margin">{{ hint }}</p>
<f7-list class="no-margin">
<f7-list-item class="list-item-pincode-input">
<pincode-input secure :length="6" v-model="currentPinCode" />
<pin-code-input :secure="true" :length="6" v-model="currentPinCode"/>
</f7-list-item>
</f7-list>
<f7-button large fill
@@ -28,13 +29,18 @@
<script>
export default {
props: [
'value',
'modelValue',
'title',
'hint',
'confirmDisabled',
'cancelDisabled',
'show'
],
emits: [
'update:modelValue',
'update:show',
'pincode:confirm'
],
data() {
return {
currentPinCode: ''
@@ -57,7 +63,7 @@ export default {
return;
}
this.$emit('input', this.currentPinCode);
this.$emit('update:modelValue', this.currentPinCode);
this.$emit('pincode:confirm', this.currentPinCode);
},
cancel() {
@@ -1,6 +1,9 @@
<template>
<f7-sheet :class="{ 'tag-selection-huge-sheet': hugeListItemRows }" :opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-sheet swipe-to-close swipe-handler=".swipe-handler"
:opened="show" :class="{ 'tag-selection-huge-sheet': hugeListItemRows }"
@sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-toolbar>
<div class="swipe-handler"></div>
<div class="left">
<f7-link sheet-close :text="$t('Cancel')"></f7-link>
</div>
@@ -9,25 +12,28 @@
</div>
</f7-toolbar>
<f7-page-content>
<f7-list no-hairlines class="no-margin-top no-margin-bottom" v-if="!items || !items.length || noAvailableTag">
<f7-list class="no-margin-top no-margin-bottom" v-if="!items || !items.length || noAvailableTag">
<f7-list-item :title="$t('No available tag')"></f7-list-item>
</f7-list>
<f7-list no-hairlines class="no-margin-top no-margin-bottom" v-else-if="items && items.length && !noAvailableTag">
<f7-list-item checkbox v-for="item in items"
v-show="!item.hidden"
:key="item.id"
:class="item.id | tagItemClass(selectedItemIds)"
<f7-list dividers class="no-margin-top no-margin-bottom" v-else-if="items && items.length && !noAvailableTag">
<f7-list-item checkbox
:class="isChecked(item.id) ? 'list-item-selected' : ''"
:value="item.id"
:checked="item.id | isChecked(selectedItemIds)"
:checked="isChecked(item.id)"
:key="item.id"
v-for="item in items"
v-show="!item.hidden"
@change="changeItemSelection">
<f7-block slot="title" class="no-padding no-margin">
<div class="display-flex">
<f7-icon slot="media" f7="number"></f7-icon>
<div class="tag-selection-list-item list-item-valign-middle padding-left-half">
{{ item.name }}
<template #title>
<f7-block class="no-padding no-margin">
<div class="display-flex">
<f7-icon f7="number"></f7-icon>
<div class="tag-selection-list-item list-item-valign-middle padding-left-half">
{{ item.name }}
</div>
</div>
</div>
</f7-block>
</f7-block>
</template>
</f7-list-item>
</f7-list>
</f7-page-content>
@@ -37,15 +43,19 @@
<script>
export default {
props: [
'value',
'modelValue',
'items',
'show'
],
emits: [
'update:modelValue',
'update:show'
],
data() {
const self = this;
return {
selectedItemIds: self.$utilities.copyArrayTo(self.value, [])
selectedItemIds: self.$utilities.copyArrayTo(self.modelValue, [])
}
},
computed: {
@@ -64,11 +74,11 @@ export default {
},
methods: {
save() {
this.$emit('input', this.selectedItemIds);
this.$emit('update:modelValue', this.selectedItemIds);
this.$emit('update:show', false);
},
onSheetOpen(event) {
this.selectedItemIds = this.$utilities.copyArrayTo(this.value, []);
this.selectedItemIds = this.$utilities.copyArrayTo(this.modelValue, []);
this.scrollToSelectedItem(event.$el);
},
onSheetClosed() {
@@ -95,9 +105,6 @@ export default {
}
},
scrollToSelectedItem(parent) {
const app = this.$f7;
const $$ = app.$;
if (!parent || !parent.length) {
return;
}
@@ -113,8 +120,8 @@ export default {
let lastSelectedItem = selectedItem;
if (selectedItem.length > 0) {
firstSelectedItem = $$(selectedItem[0]);
lastSelectedItem = $$(selectedItem[selectedItem.length - 1]);
firstSelectedItem = this.$ui.elements(selectedItem[0]);
lastSelectedItem = this.$ui.elements(selectedItem[selectedItem.length - 1]);
}
let firstSelectedItemInTop = firstSelectedItem.offset().top - container.offset().top - parseInt(container.css('padding-top'), 10);
@@ -133,26 +140,15 @@ export default {
}
container.scrollTop(targetPos);
}
},
filters: {
isChecked(itemId, selectedItemIds) {
for (let i = 0; i < selectedItemIds.length; i++) {
if (selectedItemIds[i] === itemId) {
},
isChecked(itemId) {
for (let i = 0; i < this.selectedItemIds.length; i++) {
if (this.selectedItemIds[i] === itemId) {
return true;
}
}
return false;
},
tagItemClass(itemId, selectedItemIds) {
for (let i = 0; i < selectedItemIds.length; i++) {
if (selectedItemIds[i] === itemId) {
return 'list-item-selected';
}
}
return '';
}
}
}
@@ -170,4 +166,3 @@ export default {
text-overflow: ellipsis;
}
</style>
self
@@ -1,6 +1,9 @@
<template>
<f7-sheet :class="{ 'tree-view-selection-huge-sheet': hugeTreeViewItems }" :opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-sheet swipe-to-close swipe-handler=".swipe-handler"
:class="{ 'tree-view-selection-huge-sheet': hugeTreeViewItems }"
:opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-toolbar>
<div class="swipe-handler"></div>
<div class="left"></div>
<div class="right">
<f7-link sheet-close :text="$t('Done')"></f7-link>
@@ -8,26 +11,26 @@
</f7-toolbar>
<f7-page-content>
<f7-treeview>
<f7-treeview-item v-for="item in items"
item-toggle
<f7-treeview-item item-toggle
:opened="isPrimaryItemHasSecondaryValue(item)"
:key="item | itemFieldContent(primaryKeyField, item, false)"
:label="item | itemFieldContent(primaryTitleField, item, primaryTitleI18n)">
<f7-icon slot="media"
:icon="item[primaryIconField] | icon(primaryIconType)"
:style="item[primaryColorField] | iconStyle(primaryIconType, 'var(--default-icon-color)')"
v-if="primaryIconField"></f7-icon>
:label="$tIf((primaryTitleField ? item[primaryTitleField] : item), primaryTitleI18n)"
:key="primaryKeyField ? item[primaryKeyField] : item"
v-for="item in items">
<template #media>
<ItemIcon :icon-type="primaryIconType" :icon-id="item[primaryIconField]"
:color="item[primaryColorField]" v-if="primaryIconField"></ItemIcon>
</template>
<f7-treeview-item v-for="subItem in item[primarySubItemsField]"
selectable
<f7-treeview-item selectable
:selected="isSecondarySelected(subItem)"
:key="subItem | itemFieldContent(secondaryKeyField, subItem, false)"
:label="subItem | itemFieldContent(secondaryTitleField, subItem, secondaryTitleI18n)"
:label="$tIf((secondaryTitleField ? subItem[secondaryTitleField] : subItem), secondaryTitleI18n)"
:key="secondaryKeyField ? subItem[secondaryKeyField] : subItem"
v-for="subItem in item[primarySubItemsField]"
@click="onSecondaryItemClicked(subItem)">
<f7-icon slot="media"
:icon="subItem[secondaryIconField] | icon(secondaryIconType)"
:style="subItem[secondaryColorField] | iconStyle(secondaryIconType, 'var(--default-icon-color)')"
v-if="secondaryIconField"></f7-icon>
<template #media>
<ItemIcon :icon-type="secondaryIconType" :icon-id="subItem[secondaryIconField]"
:color="subItem[secondaryColorField]" v-if="secondaryIconField"></ItemIcon>
</template>
</f7-treeview-item>
</f7-treeview-item>
</f7-treeview>
@@ -38,7 +41,7 @@
<script>
export default {
props: [
'value',
'modelValue',
'primaryKeyField',
'primaryValueField',
'primaryTitleField',
@@ -57,11 +60,15 @@ export default {
'items',
'show'
],
emits: [
'update:modelValue',
'update:show'
],
data() {
const self = this;
return {
currentValue: self.value
currentValue: self.modelValue
}
},
computed: {
@@ -85,7 +92,7 @@ export default {
},
methods: {
onSheetOpen(event) {
this.currentValue = this.value;
this.currentValue = this.modelValue;
this.scrollToSelectedItem(event.$el);
},
onSheetClosed() {
@@ -98,7 +105,7 @@ export default {
this.currentValue = subItem;
}
this.$emit('input', this.currentValue);
this.$emit('update:modelValue', this.currentValue);
this.$emit('update:show', false);
},
isPrimaryItemHasSecondaryValue(primaryItem) {
@@ -1,56 +1,60 @@
<template>
<f7-sheet style="height: auto" :opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-sheet swipe-to-close swipe-handler=".swipe-handler"
style="height: auto" :opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-toolbar>
<div class="swipe-handler"></div>
<div class="left"></div>
<div class="right">
<f7-link sheet-close :text="$t('Done')"></f7-link>
</div>
</f7-toolbar>
<f7-page-content>
<f7-row>
<f7-col width="50">
<div class="grid grid-cols-2 grid-gap">
<div>
<div class="primary-list-container">
<f7-list no-hairlines class="primary-list no-margin-top no-margin-bottom">
<f7-list dividers class="primary-list no-margin-vertical">
<f7-list-item link="#" no-chevron
v-for="item in items"
:key="item | itemFieldContent(primaryKeyField, item, false)"
:class="{ 'primary-list-item-selected': item === selectedPrimaryItem }"
:value="item | itemFieldContent(primaryValueField, item, false)"
:title="item | itemFieldContent(primaryTitleField, null, primaryTitleI18n)"
:header="item | itemFieldContent(primaryHeaderField, null, primaryHeaderI18n)"
:footer="item | itemFieldContent(primaryFooterField, null, primaryFooterI18n)"
:value="primaryValueField ? item[primaryValueField] : item"
:title="$tIf(item[primaryTitleField], primaryTitleI18n)"
:header="$tIf(item[primaryHeaderField], primaryHeaderI18n)"
:footer="$tIf(item[primaryFooterField], primaryFooterI18n)"
:key="primaryKeyField ? item[primaryKeyField] : item"
v-for="item in items"
@click="onPrimaryItemClicked(item)">
<f7-icon slot="media"
:icon="item[primaryIconField] | icon(primaryIconType)"
:style="item[primaryColorField] | iconStyle(primaryIconType, 'var(--default-icon-color)')"
v-if="primaryIconField"></f7-icon>
<f7-icon slot="after" class="list-item-showing" f7="chevron_right" v-if="item === selectedPrimaryItem"></f7-icon>
<template #media>
<ItemIcon :icon-type="primaryIconType" :icon-id="item[primaryIconField]" :color="item[primaryColorField]"></ItemIcon>
</template>
<template #after>
<f7-icon class="list-item-showing" f7="chevron_right" v-if="item === selectedPrimaryItem"></f7-icon>
</template>
</f7-list-item>
</f7-list>
</div>
</f7-col>
<f7-col width="50">
</div>
<div>
<div class="secondary-list-container">
<f7-list no-hairlines class="secondary-list no-margin-top no-margin-bottom" v-if="selectedPrimaryItem && primarySubItemsField && selectedPrimaryItem[primarySubItemsField]">
<f7-list dividers class="secondary-list no-margin-vertical" v-if="selectedPrimaryItem && primarySubItemsField && selectedPrimaryItem[primarySubItemsField]">
<f7-list-item link="#" no-chevron
v-for="subItem in selectedPrimaryItem[primarySubItemsField]"
:key="subItem | itemFieldContent(secondaryKeyField, subItem, false)"
:class="{ 'secondary-list-item-selected': isSecondarySelected(subItem) }"
:value="subItem | itemFieldContent(secondaryValueField, subItem, false)"
:title="subItem | itemFieldContent(secondaryTitleField, null, secondaryTitleI18n)"
:header="subItem | itemFieldContent(secondaryHeaderField, null, secondaryHeaderI18n)"
:footer="subItem | itemFieldContent(secondaryFooterField, null, secondaryFooterI18n)"
:value="secondaryValueField ? subItem[secondaryValueField] : subItem"
:title="$tIf(subItem[secondaryTitleField], secondaryTitleI18n)"
:header="$tIf(subItem[secondaryHeaderField], secondaryHeaderI18n)"
:footer="$tIf(subItem[secondaryFooterField], secondaryFooterI18n)"
:key="secondaryKeyField ? subItem[secondaryKeyField] : subItem"
v-for="subItem in selectedPrimaryItem[primarySubItemsField]"
@click="onSecondaryItemClicked(subItem)">
<f7-icon slot="media"
:icon="subItem[secondaryIconField] | icon(secondaryIconType)"
:style="subItem[secondaryColorField] | iconStyle(secondaryIconType, 'var(--default-icon-color)')"
v-if="secondaryIconField"></f7-icon>
<f7-icon slot="after" class="list-item-checked-icon" f7="checkmark_alt" v-if="isSecondarySelected(subItem)"></f7-icon>
<template #media>
<ItemIcon :icon-type="secondaryIconType" :icon-id="subItem[secondaryIconField]" :color="subItem[secondaryColorField]"></ItemIcon>
</template>
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="isSecondarySelected(subItem)"></f7-icon>
</template>
</f7-list-item>
</f7-list>
</div>
</f7-col>
</f7-row>
</div>
</div>
</f7-page-content>
</f7-sheet>
</template>
@@ -58,7 +62,7 @@
<script>
export default {
props: [
'value',
'modelValue',
'primaryKeyField',
'primaryValueField',
'primaryTitleField',
@@ -85,12 +89,16 @@ export default {
'items',
'show'
],
emits: [
'update:modelValue',
'update:show'
],
data() {
const self = this;
return {
currentPrimaryValue: self.getPrimaryValueBySecondaryValue(self.value),
currentSecondaryValue: self.value
currentPrimaryValue: self.getPrimaryValueBySecondaryValue(self.modelValue),
currentSecondaryValue: self.modelValue
}
},
computed: {
@@ -126,13 +134,13 @@ export default {
},
methods: {
onSheetOpen(event) {
this.currentPrimaryValue = this.getPrimaryValueBySecondaryValue(this.value);
this.currentSecondaryValue = this.value;
this.currentPrimaryValue = this.getPrimaryValueBySecondaryValue(this.modelValue);
this.currentSecondaryValue = this.modelValue;
this.scrollToSelectedItem(event.$el, '.primary-list-container', 'li.primary-list-item-selected');
this.scrollToSelectedItem(event.$el, '.secondary-list-container', 'li.secondary-list-item-selected');
},
onSheetClosed() {
this.$emit('update:show', false);
this.close();
},
onPrimaryItemClicked(item) {
if (this.primaryValueField) {
@@ -148,8 +156,8 @@ export default {
this.currentSecondaryValue = subItem;
}
this.$emit('input', this.currentSecondaryValue);
this.$emit('update:show', false);
this.$emit('update:modelValue', this.currentSecondaryValue);
this.close();
},
isSecondarySelected(subItem) {
if (this.secondaryValueField) {
@@ -226,6 +234,9 @@ export default {
}
container.scrollTop(targetPos);
},
close() {
this.$emit('update:show', false);
}
}
}
+4 -2
View File
@@ -1,5 +1,7 @@
const baseUrlPath = '/api';
const baseApiUrlPath = '/api';
const baseProxyUrlPath = '/proxy';
export default {
baseUrlPath: baseUrlPath
baseApiUrlPath: baseApiUrlPath,
baseProxyUrlPath: baseProxyUrlPath
}
+14 -2
View File
@@ -495,7 +495,11 @@ const allCurrencies = {
code: 'SHP',
symbol: '£'
},
'SLL': { // Leone
'SLE': { // Leone (new leone)
code: 'SLE',
symbol: 'Le'
},
'SLL': { // Leone (old leone)
code: 'SLL',
symbol: 'Le'
},
@@ -582,9 +586,17 @@ const allCurrencies = {
'UZS': { // Uzbekistan Sum
code: 'UZS'
},
'VED': { // Bolívar Digital
code: 'VED',
symbol: 'Bs.D'
},
'VEF': { // Bolívar Fuerte
code: 'VEF',
symbol: 'Bs.F'
},
'VES': { // Bolívar Soberano
code: 'VES',
symbol: 'Bs.S.'
symbol: 'Bs.S'
},
'VND': { // Dong
code: 'VND',
+109 -1
View File
@@ -39,6 +39,96 @@ const allWeekDaysArray = [
allWeekDays.Saturday
];
const allLongDateFormat = {
YYYYMMDD: {
type: 1,
key: 'yyyy_mm_dd'
},
MMDDYYYY: {
type: 2,
key: 'mm_dd_yyyy'
},
DDMMYYYY: {
type: 3,
key: 'dd_mm_yyyy'
}
};
const allLongDateFormatArray = [
allLongDateFormat.YYYYMMDD,
allLongDateFormat.MMDDYYYY,
allLongDateFormat.DDMMYYYY
];
const allShortDateFormat = {
YYYYMMDD: {
type: 1,
key: 'yyyy_mm_dd'
},
MMDDYYYY: {
type: 2,
key: 'mm_dd_yyyy'
},
DDMMYYYY: {
type: 3,
key: 'dd_mm_yyyy'
}
};
const allShortDateFormatArray = [
allShortDateFormat.YYYYMMDD,
allShortDateFormat.MMDDYYYY,
allShortDateFormat.DDMMYYYY
];
const allLongTimeFormat = {
HHMMSS: {
type: 1,
key: 'hh_mm_ss',
is24HourFormat: true
},
AHHMMSS: {
type: 2,
key: 'a_hh_mm_ss',
is24HourFormat: false
},
HHMMSSA: {
type: 3,
key: 'hh_mm_ss_a',
is24HourFormat: false
}
};
const allLongTimeFormatArray = [
allLongTimeFormat.HHMMSS,
allLongTimeFormat.AHHMMSS,
allLongTimeFormat.HHMMSSA
];
const allShortTimeFormat = {
HHMM: {
type: 1,
key: 'hh_mm',
is24HourFormat: true
},
AHHMM: {
type: 2,
key: 'a_hh_mm',
is24HourFormat: false
},
HHMMA: {
type: 3,
key: 'hh_mm_a',
is24HourFormat: false
}
};
const allShortTimeFormatArray = [
allShortTimeFormat.HHMM,
allShortTimeFormat.AHHMM,
allShortTimeFormat.HHMMA
];
const allDateRanges = {
All: {
type: 0,
@@ -91,10 +181,28 @@ const allDateRanges = {
};
const defaultFirstDayOfWeek = allWeekDays.Sunday.type;
const defaultLongDateFormat = allLongDateFormat.YYYYMMDD;
const defaultShortDateFormat = allShortDateFormat.YYYYMMDD;
const defaultLongTimeFormat = allLongTimeFormat.HHMMSS;
const defaultShortTimeFormat = allShortTimeFormat.HHMM;
const defaultDateTimeFormatValue = 0;
export default {
allWeekDays: allWeekDays,
allWeekDaysArray: allWeekDaysArray,
allLongDateFormat: allLongDateFormat,
allLongDateFormatArray: allLongDateFormatArray,
allShortDateFormat: allShortDateFormat,
allShortDateFormatArray: allShortDateFormatArray,
allLongTimeFormat: allLongTimeFormat,
allLongTimeFormatArray: allLongTimeFormatArray,
allShortTimeFormat: allShortTimeFormat,
allShortTimeFormatArray: allShortTimeFormatArray,
allDateRanges: allDateRanges,
defaultFirstDayOfWeek: defaultFirstDayOfWeek
defaultFirstDayOfWeek: defaultFirstDayOfWeek,
defaultLongDateFormat: defaultLongDateFormat,
defaultShortDateFormat: defaultShortDateFormat,
defaultLongTimeFormat: defaultLongTimeFormat,
defaultShortTimeFormat: defaultShortTimeFormat,
defaultDateTimeFormatValue: defaultDateTimeFormatValue,
};
+12
View File
@@ -256,6 +256,9 @@ const allCategoryIcons = {
'211': {
icon: 'las la-umbrella'
},
'212': {
icon: 'las la-baby-carriage'
},
'220': {
icon: 'las la-couch'
},
@@ -436,6 +439,9 @@ const allCategoryIcons = {
'517': {
icon: 'las la-snowboarding'
},
'518': {
icon: 'las la-hiking'
},
'520': {
icon: 'las la-futbol'
},
@@ -508,6 +514,9 @@ const allCategoryIcons = {
'570': {
icon: 'las la-id-card-alt'
},
'571': {
icon: 'las la-podcast'
},
'580': {
icon: 'las la-dog'
},
@@ -582,6 +591,9 @@ const allCategoryIcons = {
'720': {
icon: 'las la-birthday-cake'
},
'760': {
icon: 'las la-ribbon'
},
'780': {
icon: 'las la-donate'
},
+2 -1
View File
@@ -593,5 +593,6 @@ const allAvailableTimezones = [
];
export default {
all: allAvailableTimezones
all: allAvailableTimezones,
utcTimezoneName: 'Etc/GMT'
};
+4 -6
View File
@@ -1,8 +1,6 @@
import Vue from 'vue';
import { createApp } from 'vue'
import App from './Desktop.vue';
import App from './DesktopApp.vue';
new Vue({
el: '#app',
render: h => h(App),
})
const app = createApp(App);
app.mount('#app');
+6 -4
View File
@@ -10,9 +10,11 @@
<title>ezBookkeeping</title>
</head>
<body>
<noscript>
<strong>We're sorry but ezBookkeeping doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<noscript>
<strong>We're sorry but ezBookkeeping doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<script type="module" src="./desktop-main.js"></script>
</body>
</html>
+10
View File
@@ -0,0 +1,10 @@
import { autoChangeTextareaSize } from '../../lib/mobile/ui.js';
export default {
mounted(el) {
autoChangeTextareaSize(el);
},
updated(el) {
autoChangeTextareaSize(el);
}
}
-14
View File
@@ -1,14 +0,0 @@
import icons from '../consts/icon.js';
import utils from '../lib/utils.js';
export default function (iconId) {
if (utils.isNumber(iconId)) {
iconId = iconId.toString();
}
if (!icons.allAccountIcons[iconId]) {
return icons.defaultAccountIcon.icon;
}
return icons.allAccountIcons[iconId].icon;
}
-19
View File
@@ -1,19 +0,0 @@
import colorConstants from '../consts/color.js';
export default function (color, defaultColor, additionalFieldName) {
if (color && color !== colorConstants.defaultAccountColor) {
color = '#' + color;
} else {
color = defaultColor;
}
const ret = {
color: color
};
if (additionalFieldName) {
ret[additionalFieldName] = color;
}
return ret;
}
-14
View File
@@ -1,14 +0,0 @@
import icons from '../consts/icon.js';
import utils from '../lib/utils.js';
export default function (iconId) {
if (utils.isNumber(iconId)) {
iconId = iconId.toString();
}
if (!icons.allCategoryIcons[iconId]) {
return icons.defaultCategoryIcon.icon;
}
return icons.allCategoryIcons[iconId].icon;
}
-19
View File
@@ -1,19 +0,0 @@
import colorConstants from '../consts/color.js';
export default function (color, defaultColor, additionalFieldName) {
if (color && color !== colorConstants.defaultCategoryColor) {
color = '#' + color;
} else {
color = defaultColor;
}
const ret = {
color: color
};
if (additionalFieldName) {
ret[additionalFieldName] = color;
}
return ret;
}
-52
View File
@@ -1,52 +0,0 @@
import currency from '../consts/currency.js';
import settings from '../lib/settings.js';
import utils from '../lib/utils.js';
export default function ({i18n}, value, currencyCode, notConvertValue) {
if (!utils.isNumber(value) && !utils.isString(value)) {
return value;
}
if (utils.isNumber(value)) {
value = value.toString();
}
if (!notConvertValue) {
const hasIncompleteFlag = utils.isString(value) && value.charAt(value.length - 1) === '+';
if (hasIncompleteFlag) {
value = value.substr(0, value.length - 1);
}
value = utils.numericCurrencyToString(value);
if (hasIncompleteFlag) {
value = value + '+';
}
}
const currencyDisplayMode = settings.getCurrencyDisplayMode();
if (currencyCode && currencyDisplayMode === currency.allCurrencyDisplayModes.Symbol) {
const currencyInfo = currency.all[currencyCode];
let currencySymbol = currency.defaultCurrencySymbol;
if (currencyInfo && currencyInfo.symbol) {
currencySymbol = currencyInfo.symbol;
} else if (currencyInfo && currencyInfo.code) {
currencySymbol = currencyInfo.code;
}
return i18n.t('format.currency.symbol', {
amount: value,
symbol: currencySymbol
});
} else if (currencyCode && currencyDisplayMode === currency.allCurrencyDisplayModes.Code) {
return `${value} ${currencyCode}`;
} else if (currencyCode && currencyDisplayMode === currency.allCurrencyDisplayModes.Name) {
const currencyName = i18n.t(`currency.${currencyCode}`);
return `${value} ${currencyName}`;
} else {
return value;
}
}
-11
View File
@@ -1,11 +0,0 @@
import colorConstants from '../consts/color.js';
export default function (color, defaultColor) {
if (color && color !== colorConstants.defaultColor) {
color = '#' + color;
} else {
color = defaultColor;
}
return color;
}
-19
View File
@@ -1,19 +0,0 @@
import colorConstants from '../consts/color.js';
export default function (color, defaultColor, additionalFieldName) {
if (color && color !== colorConstants.defaultColor) {
color = '#' + color;
} else {
color = defaultColor;
}
const ret = {
color: color
};
if (additionalFieldName) {
ret[additionalFieldName] = color;
}
return ret;
}
-18
View File
@@ -1,18 +0,0 @@
export default function (rate) {
const rateStr = rate.toString();
if (rateStr.indexOf('.') < 0) {
return rateStr;
} else {
let firstNonZeroPos = 0;
for (let i = 0; i < rateStr.length; i++) {
if (rateStr.charAt(i) !== '.' && rateStr.charAt(i) !== '0') {
firstNonZeroPos = Math.min(i + 4, rateStr.length);
break;
}
}
return rateStr.substr(0, Math.max(6, Math.max(firstNonZeroPos, rateStr.indexOf('.') + 2)));
}
}
-3
View File
@@ -1,3 +0,0 @@
export default function (value, format) {
return format.replaceAll(/#{value}/g, value);
}
-12
View File
@@ -1,12 +0,0 @@
import accountIcon from './accountIcon.js';
import categoryIcon from './categoryIcon.js';
export default function (iconId, iconType) {
if (iconType === 'account') {
return accountIcon(iconId);
} else if (iconType === 'category') {
return categoryIcon(iconId);
} else {
return '';
}
}
-13
View File
@@ -1,13 +0,0 @@
import defaultIconStyle from './defaultIconStyle.js';
import accountIconStyle from './accountIconStyle.js';
import categoryIconStyle from './categoryIconStyle.js';
export default function (color, iconType, defaultColor, additionalFieldName) {
if (iconType === 'account') {
return accountIconStyle(color, defaultColor, additionalFieldName);
} else if (iconType === 'category') {
return categoryIconStyle(color, defaultColor, additionalFieldName);
} else {
return defaultIconStyle(color, defaultColor, additionalFieldName);
}
}
-13
View File
@@ -1,13 +0,0 @@
export default function ({i18n}, value, fieldName, defaultValue, translate) {
let content = defaultValue;
if (fieldName) {
content = value[fieldName];
}
if (translate && content) {
content = i18n.t(content);
}
return content;
}
-11
View File
@@ -1,11 +0,0 @@
import { allLanguages } from '../locales/index.js';
export default function (languageCode) {
const lang = allLanguages[languageCode];
if (!lang) {
return '';
}
return lang.displayName;
}
-3
View File
@@ -1,3 +0,0 @@
export default function ({i18n}, text, options) {
return i18n.t(text, options || {});
}
-20
View File
@@ -1,20 +0,0 @@
import utils from '../lib/utils.js';
export default function (value, format, options) {
if (!utils.isNumber(value)) {
value = utils.getUnixTime(value);
}
let utcOffset = null;
let currentUtcOffset = null;
if (utils.isObject(options) && utils.isNumber(options.utcOffset)) {
utcOffset = options.utcOffset;
}
if (utils.isObject(options) && utils.isNumber(options.currentUtcOffset)) {
currentUtcOffset = options.currentUtcOffset;
}
return utils.formatUnixTime(value, format, utcOffset, currentUtcOffset);
}
-43
View File
@@ -1,43 +0,0 @@
import utils from '../lib/utils.js';
export default function (value, options, keyField, nameField, defaultName) {
if (utils.isArray(options)) {
if (keyField) {
for (let i = 0; i < options.length; i++) {
const option = options[i];
if (option[keyField] === value) {
return option[nameField];
}
}
} else {
if (options[value]) {
const option = options[value];
return option[nameField];
}
}
} else if (utils.isObject(options)) {
if (keyField) {
for (let key in options) {
if (!Object.prototype.hasOwnProperty.call(options, key)) {
continue;
}
const option = options[key];
if (option[keyField] === value) {
return option[nameField];
}
}
} else {
if (options[value]) {
const option = options[value];
return option[nameField];
}
}
}
return defaultName;
}
-11
View File
@@ -1,11 +0,0 @@
export default function (value, precision, lowPrecisionValue) {
const ratio = Math.pow(10, precision);
const normalizedValue = Math.floor(value * ratio);
if (value > 0 && normalizedValue < 1 && lowPrecisionValue) {
return lowPrecisionValue + '%';
}
const result = normalizedValue / ratio;
return result + '%';
}
-19
View File
@@ -1,19 +0,0 @@
export default function (value, maxLength) {
let length = 0;
for (let i = 0; i < value.length; i++) {
const c = value.charCodeAt(i);
if ((c >= 0x0001 && c <= 0x007e) || (0xff60 <= c && c <= 0xff9f)) {
length++;
} else {
length += 2;
}
}
if (length <= maxLength || maxLength <= 3) {
return value;
}
return value.substr(0, maxLength - 3) + '...';
}
-36
View File
@@ -1,36 +0,0 @@
import utils from '../lib/utils.js';
export default function (token) {
const ua = utils.parseUserAgent(token.userAgent);
let result = '';
if (ua.device.model) {
result = ua.device.model;
} else if (ua.os.name) {
result = ua.os.name;
if (ua.os.version) {
result += ' ' + ua.os.version;
}
}
if (ua.browser.name) {
let browserInfo = ua.browser.name;
if (ua.browser.version) {
browserInfo += ' ' + ua.browser.version;
}
if (result) {
result += ' (' + browserInfo + ')';
} else {
result = browserInfo;
}
}
if (!result) {
return 'Unknown Device';
}
return result;
}
-22
View File
@@ -1,22 +0,0 @@
import icons from '../consts/icon.js';
import utils from '../lib/utils.js';
export default function (token) {
const ua = utils.parseUserAgent(token.userAgent);
if (!ua || !ua.device) {
return icons.deviceIcons.desktop.f7Icon;
}
if (ua.device.type === 'mobile') {
return icons.deviceIcons.mobile.f7Icon;
} else if (ua.device.type === 'wearable') {
return icons.deviceIcons.wearable.f7Icon;
} else if (ua.device.type === 'tablet') {
return icons.deviceIcons.tablet.f7Icon;
} else if (ua.device.type === 'smarttv') {
return icons.deviceIcons.tv.f7Icon;
} else {
return icons.deviceIcons.desktop.f7Icon;
}
}
-6
View File
@@ -1,6 +0,0 @@
import utils from '../lib/utils.js';
export default function (utcOffsetMinutes) {
const utcOffset = utils.getUtcOffsetByUtcOffsetMinutes(utcOffsetMinutes);
return `(UTC${utcOffset})`;
}
+6 -4
View File
@@ -52,9 +52,11 @@
<link rel="manifest" href="manifest.json">
</head>
<body>
<noscript>
<strong>We're sorry but ezBookkeeping doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<noscript>
<strong>We're sorry but ezBookkeeping doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<script type="module" src="./index-main.js"></script>
</body>
</html>
+317 -2
View File
@@ -1,4 +1,9 @@
import { defaultLanguage, allLanguages } from '../locales/index.js';
import datetime from "../consts/datetime.js";
import timezone from "../consts/timezone.js";
import currency from "../consts/currency.js";
import settings from "./settings.js";
import utilities from './utilities/index.js';
const apiNotFoundErrorCode = 100001;
const specifiedApiNotFoundErrors = {
@@ -121,11 +126,11 @@ const parameterizedErrors = [
}
];
export function getAllLanguages() {
export function getAllLanguageInfos() {
return allLanguages;
}
export function getLanguage(locale) {
export function getLanguageInfo(locale) {
return allLanguages[locale];
}
@@ -159,6 +164,15 @@ export function getDefaultLanguage() {
browserLocale = locale;
}
}
if (!allLanguages[browserLocale]) {
browserLocale = localeParts[0];
const locale = getLocaleFromLanguageAlias(browserLocale);
if (locale) {
browserLocale = locale;
}
}
}
if (!allLanguages[browserLocale]) {
@@ -168,8 +182,278 @@ export function getDefaultLanguage() {
return browserLocale;
}
export function transateIf(text, isTranslate, translateFn) {
if (isTranslate) {
return translateFn(text);
}
return text;
}
export function getAllLongMonthNames(translateFn) {
return [
translateFn('datetime.January.long'),
translateFn('datetime.February.long'),
translateFn('datetime.March.long'),
translateFn('datetime.April.long'),
translateFn('datetime.May.long'),
translateFn('datetime.June.long'),
translateFn('datetime.July.long'),
translateFn('datetime.August.long'),
translateFn('datetime.September.long'),
translateFn('datetime.October.long'),
translateFn('datetime.November.long'),
translateFn('datetime.December.long')
];
}
export function getAllShortMonthNames(translateFn) {
return [
translateFn('datetime.January.short'),
translateFn('datetime.February.short'),
translateFn('datetime.March.short'),
translateFn('datetime.April.short'),
translateFn('datetime.May.short'),
translateFn('datetime.June.short'),
translateFn('datetime.July.short'),
translateFn('datetime.August.short'),
translateFn('datetime.September.short'),
translateFn('datetime.October.short'),
translateFn('datetime.November.short'),
translateFn('datetime.December.short')
];
}
export function getAllLongWeekdayNames(translateFn) {
return [
translateFn('datetime.Sunday.long'),
translateFn('datetime.Monday.long'),
translateFn('datetime.Tuesday.long'),
translateFn('datetime.Wednesday.long'),
translateFn('datetime.Thursday.long'),
translateFn('datetime.Friday.long'),
translateFn('datetime.Saturday.long')
];
}
export function getAllShortWeekdayNames(translateFn) {
return [
translateFn('datetime.Sunday.short'),
translateFn('datetime.Monday.short'),
translateFn('datetime.Tuesday.short'),
translateFn('datetime.Wednesday.short'),
translateFn('datetime.Thursday.short'),
translateFn('datetime.Friday.short'),
translateFn('datetime.Saturday.short')
];
}
export function getAllMinWeekdayNames(translateFn) {
return [
translateFn('datetime.Sunday.min'),
translateFn('datetime.Monday.min'),
translateFn('datetime.Tuesday.min'),
translateFn('datetime.Wednesday.min'),
translateFn('datetime.Thursday.min'),
translateFn('datetime.Friday.min'),
translateFn('datetime.Saturday.min')
];
}
export function getAllLongDateFormats(translateFn) {
const defaultLongDateFormatTypeName = translateFn('default.longDateFormat');
return getDateTimeFormats(translateFn, datetime.allLongDateFormat, datetime.allLongDateFormatArray, 'format.longDate', defaultLongDateFormatTypeName, datetime.defaultLongDateFormat);
}
export function getAllShortDateFormats(translateFn) {
const defaultShortDateFormatTypeName = translateFn('default.shortDateFormat');
return getDateTimeFormats(translateFn, datetime.allShortDateFormat, datetime.allShortDateFormatArray, 'format.shortDate', defaultShortDateFormatTypeName, datetime.defaultShortDateFormat);
}
export function getAllLongTimeFormats(translateFn) {
const defaultLongTimeFormatTypeName = translateFn('default.longTimeFormat');
return getDateTimeFormats(translateFn, datetime.allLongTimeFormat, datetime.allLongTimeFormatArray, 'format.longTime', defaultLongTimeFormatTypeName, datetime.defaultLongTimeFormat);
}
export function getAllShortTimeFormats(translateFn) {
const defaultShortTimeFormatTypeName = translateFn('default.shortTimeFormat');
return getDateTimeFormats(translateFn, datetime.allShortTimeFormat, datetime.allShortTimeFormatArray, 'format.shortTime', defaultShortTimeFormatTypeName, datetime.defaultShortTimeFormat);
}
export function getI18nLongDateFormat(translateFn, formatTypeValue) {
const defaultLongDateFormatTypeName = translateFn('default.longDateFormat');
return getDateTimeFormat(translateFn, datetime.allLongDateFormat, datetime.allLongDateFormatArray, 'format.longDate', defaultLongDateFormatTypeName, datetime.defaultLongDateFormat, formatTypeValue);
}
export function getI18nShortDateFormat(translateFn, formatTypeValue) {
const defaultShortDateFormatTypeName = translateFn('default.shortDateFormat');
return getDateTimeFormat(translateFn, datetime.allShortDateFormat, datetime.allShortDateFormatArray, 'format.shortDate', defaultShortDateFormatTypeName, datetime.defaultShortDateFormat, formatTypeValue);
}
export function getI18nLongYearFormat(translateFn, formatTypeValue) {
const defaultLongDateFormatTypeName = translateFn('default.longDateFormat');
return getDateTimeFormat(translateFn, datetime.allLongDateFormat, datetime.allLongDateFormatArray, 'format.longYear', defaultLongDateFormatTypeName, datetime.defaultLongDateFormat, formatTypeValue);
}
export function getI18nShortYearFormat(translateFn, formatTypeValue) {
const defaultShortDateFormatTypeName = translateFn('default.shortDateFormat');
return getDateTimeFormat(translateFn, datetime.allShortDateFormat, datetime.allShortDateFormatArray, 'format.shortYear', defaultShortDateFormatTypeName, datetime.defaultShortDateFormat, formatTypeValue);
}
export function getI18nLongYearMonthFormat(translateFn, formatTypeValue) {
const defaultLongDateFormatTypeName = translateFn('default.longDateFormat');
return getDateTimeFormat(translateFn, datetime.allLongDateFormat, datetime.allLongDateFormatArray, 'format.longYearMonth', defaultLongDateFormatTypeName, datetime.defaultLongDateFormat, formatTypeValue);
}
export function getI18nShortYearMonthFormat(translateFn, formatTypeValue) {
const defaultShortDateFormatTypeName = translateFn('default.shortDateFormat');
return getDateTimeFormat(translateFn, datetime.allShortDateFormat, datetime.allShortDateFormatArray, 'format.shortYearMonth', defaultShortDateFormatTypeName, datetime.defaultShortDateFormat, formatTypeValue);
}
export function getI18nLongMonthDayFormat(translateFn, formatTypeValue) {
const defaultLongDateFormatTypeName = translateFn('default.longDateFormat');
return getDateTimeFormat(translateFn, datetime.allLongDateFormat, datetime.allLongDateFormatArray, 'format.longMonthDay', defaultLongDateFormatTypeName, datetime.defaultLongDateFormat, formatTypeValue);
}
export function getI18nShortMonthDayFormat(translateFn, formatTypeValue) {
const defaultShortDateFormatTypeName = translateFn('default.shortDateFormat');
return getDateTimeFormat(translateFn, datetime.allShortDateFormat, datetime.allShortDateFormatArray, 'format.shortMonthDay', defaultShortDateFormatTypeName, datetime.defaultShortDateFormat, formatTypeValue);
}
export function getI18nLongTimeFormat(translateFn, formatTypeValue) {
const defaultLongTimeFormatTypeName = translateFn('default.longTimeFormat');
return getDateTimeFormat(translateFn, datetime.allLongTimeFormat, datetime.allLongTimeFormatArray, 'format.longTime', defaultLongTimeFormatTypeName, datetime.defaultLongTimeFormat, formatTypeValue);
}
export function getI18nShortTimeFormat(translateFn, formatTypeValue) {
const defaultShortTimeFormatTypeName = translateFn('default.shortTimeFormat');
return getDateTimeFormat(translateFn, datetime.allShortTimeFormat, datetime.allShortTimeFormatArray, 'format.shortTime', defaultShortTimeFormatTypeName, datetime.defaultShortTimeFormat, formatTypeValue);
}
export function isLongTime24HourFormat(translateFn, formatTypeValue) {
const defaultLongTimeFormatTypeName = translateFn('default.longTimeFormat');
const type = utilities.getDateTimeFormatType(datetime.allLongTimeFormat, datetime.allLongTimeFormatArray, defaultLongTimeFormatTypeName, datetime.defaultLongTimeFormat, formatTypeValue);
return type.is24HourFormat;
}
export function isShortTime24HourFormat(translateFn, formatTypeValue) {
const defaultShortTimeFormatTypeName = translateFn('default.shortTimeFormat');
const type = utilities.getDateTimeFormatType(datetime.allShortTimeFormat, datetime.allShortTimeFormatArray, defaultShortTimeFormatTypeName, datetime.defaultShortTimeFormat, formatTypeValue);
return type.is24HourFormat;
}
export function getAllTimezones(includeSystemDefault, translateFn) {
const defaultTimezoneOffset = utilities.getTimezoneOffset();
const defaultTimezoneOffsetMinutes = utilities.getTimezoneOffsetMinutes();
const allTimezones = timezone.all;
const allTimezoneInfos = [];
for (let i = 0; i < allTimezones.length; i++) {
allTimezoneInfos.push({
name: allTimezones[i].timezoneName,
utcOffset: (allTimezones[i].timezoneName !== timezone.utcTimezoneName ? utilities.getTimezoneOffset(allTimezones[i].timezoneName) : ''),
utcOffsetMinutes: utilities.getTimezoneOffsetMinutes(allTimezones[i].timezoneName),
displayName: translateFn(`timezone.${allTimezones[i].displayName}`)
});
}
if (includeSystemDefault) {
allTimezoneInfos.push({
name: '',
utcOffset: defaultTimezoneOffset,
utcOffsetMinutes: defaultTimezoneOffsetMinutes,
displayName: translateFn('System Default')
});
}
allTimezoneInfos.sort(function(c1, c2) {
const utcOffset1 = parseInt(c1.utcOffset.replace(':', ''));
const utcOffset2 = parseInt(c2.utcOffset.replace(':', ''));
if (utcOffset1 !== utcOffset2) {
return utcOffset1 - utcOffset2;
}
return c1.displayName.localeCompare(c2.displayName);
})
return allTimezoneInfos;
}
export function getAllCurrencies(translateFn) {
const allCurrencyCodes = currency.all;
const allCurrencies = [];
for (let currencyCode in allCurrencyCodes) {
if (!Object.prototype.hasOwnProperty.call(allCurrencyCodes, currencyCode)) {
return;
}
allCurrencies.push({
code: currencyCode,
displayName: translateFn(`currency.${currencyCode}`)
});
}
allCurrencies.sort(function(c1, c2) {
return c1.displayName.localeCompare(c2.displayName);
})
return allCurrencies;
}
export function getDisplayCurrency(value, currencyCode, notConvertValue, translateFn) {
if (!utilities.isNumber(value) && !utilities.isString(value)) {
return value;
}
if (utilities.isNumber(value)) {
value = value.toString();
}
if (!notConvertValue) {
const hasIncompleteFlag = utilities.isString(value) && value.charAt(value.length - 1) === '+';
if (hasIncompleteFlag) {
value = value.substring(0, value.length - 1);
}
value = utilities.numericCurrencyToString(value);
if (hasIncompleteFlag) {
value = value + '+';
}
}
const currencyDisplayMode = settings.getCurrencyDisplayMode();
if (currencyCode && currencyDisplayMode === currency.allCurrencyDisplayModes.Symbol) {
const currencyInfo = currency.all[currencyCode];
let currencySymbol = currency.defaultCurrencySymbol;
if (currencyInfo && currencyInfo.symbol) {
currencySymbol = currencyInfo.symbol;
} else if (currencyInfo && currencyInfo.code) {
currencySymbol = currencyInfo.code;
}
return translateFn('format.currency.symbol', {
amount: value,
symbol: currencySymbol
});
} else if (currencyCode && currencyDisplayMode === currency.allCurrencyDisplayModes.Code) {
return `${value} ${currencyCode}`;
} else if (currencyCode && currencyDisplayMode === currency.allCurrencyDisplayModes.Name) {
const currencyName = translateFn(`currency.${currencyCode}`);
return `${value} ${currencyName}`;
} else {
return value;
}
}
export function getI18nOptions() {
return {
legacy: true,
locale: defaultLanguage,
fallbackLocale: defaultLanguage,
formatFallbackMessages: true,
@@ -270,3 +554,34 @@ function getLocaleFromLanguageAlias(alias) {
return null;
}
function getDateTimeFormats(translateFn, allFormatMap, allFormatArray, localeFormatPathPrefix, localeDefaultFormatTypeName, systemDefaultFormatType) {
const defaultFormat = getDateTimeFormat(translateFn, allFormatMap, allFormatArray,
localeFormatPathPrefix, localeDefaultFormatTypeName, systemDefaultFormatType, datetime.defaultDateTimeFormatValue);
const ret = [];
ret.push({
type: datetime.defaultDateTimeFormatValue,
format: defaultFormat,
displayName: `${translateFn('Language Default')} (${utilities.formatTime(utilities.getCurrentDateTime(), defaultFormat)})`
});
for (let i = 0; i < allFormatArray.length; i++) {
const formatType = allFormatArray[i];
const format = translateFn(`${localeFormatPathPrefix}.${formatType.key}`);
ret.push({
type: formatType.type,
format: format,
displayName: utilities.formatTime(utilities.getCurrentDateTime(), format)
});
}
return ret;
}
function getDateTimeFormat(translateFn, allFormatMap, allFormatArray, localeFormatPathPrefix, localeDefaultFormatTypeName, systemDefaultFormatType, formatTypeValue) {
const type = utilities.getDateTimeFormatType(allFormatMap, allFormatArray,
localeDefaultFormatTypeName, systemDefaultFormatType, formatTypeValue);
return translateFn(`${localeFormatPathPrefix}.${type.key}`);
}
+2 -2
View File
@@ -1,8 +1,8 @@
export default {
getLicense: () => {
return process.env.LICENSE;
return __EZBOOKKEEPING_LICENSE__; // eslint-disable-line
},
getThirdPartyLicenses: () => {
return process.env.THIRD_PARTY_LICENSES || [];
return __EZBOOKKEEPING_THIRD_PARTY_LICENSES__ || []; // eslint-disable-line
}
};
+130
View File
@@ -0,0 +1,130 @@
import { f7, f7ready } from 'framework7-vue';
import settings from "../settings.js";
import {
getLocalizedError,
getLocalizedErrorParameters
} from "../i18n.js";
export function showAlert(message, confirmCallback, translateFn) {
let parameters = {};
if (message && message.error) {
const localizedError = getLocalizedError(message.error);
message = localizedError.message;
parameters = getLocalizedErrorParameters(localizedError.parameters, s => translateFn(s));
}
f7ready((f7) => {
f7.dialog.create({
title: translateFn('global.app.title'),
text: translateFn(message, parameters),
animate: settings.isEnableAnimate(),
buttons: [
{
text: translateFn('OK'),
onClick: confirmCallback
}
]
}).open();
});
}
export function showConfirm(message, confirmCallback, cancelCallback, translateFn) {
f7ready((f7) => {
f7.dialog.create({
title: translateFn('global.app.title'),
text: translateFn(message),
animate: settings.isEnableAnimate(),
buttons: [
{
text: translateFn('Cancel'),
onClick: cancelCallback
},
{
text: translateFn('OK'),
onClick: confirmCallback
}
]
}).open();
});
}
export function showToast(message, timeout, translateFn) {
let parameters = {};
if (message && message.error) {
const localizedError = getLocalizedError(message.error);
message = localizedError.message;
parameters = getLocalizedErrorParameters(localizedError.parameters, s => translateFn(s));
}
f7ready((f7) => {
f7.toast.create({
text: translateFn(message, parameters),
position: 'center',
closeTimeout: timeout || 1500
}).open();
});
}
export function showLoading(delayConditionFunc, delayMills) {
if (!delayConditionFunc) {
f7ready((f7) => {
return f7.preloader.show();
});
}
f7ready((f7) => {
setTimeout(() => {
if (delayConditionFunc()) {
f7.preloader.show();
}
}, delayMills || 200);
});
}
export function hideLoading() {
f7ready((f7) => {
return f7.preloader.hide();
});
}
export function routeBackOnError(f7router, errorPropertyName) {
const self = this;
const router = f7router;
const unwatch = self.$watch(errorPropertyName, () => {
if (self[errorPropertyName]) {
setTimeout(() => {
if (unwatch) {
unwatch();
}
router.back();
}, 200);
}
}, {
immediate: true
});
}
export function elements(selector) {
return f7.$(selector);
}
export function isModalShowing() {
return f7.$('.modal-in').length;
}
export function onSwipeoutDeleted(domId, callback) {
f7.swipeout.delete(f7.$('#' + domId), callback);
}
export function autoChangeTextareaSize(el) {
f7.$(el).find('textarea').each(el => {
el.scrollTop = 0;
el.style.height = '';
el.style.height = el.scrollHeight + 'px';
});
}

Some files were not shown because too many files have changed in this diff Show More