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
This commit is contained in:
mayswind
2023-04-21 01:45:00 +08:00
committed by GitHub
parent 4b0f7d45e8
commit b1c765eb51
89 changed files with 8353 additions and 16671 deletions
+14
View File
@@ -0,0 +1,14 @@
module.exports = {
'root': true,
'env': {
'node': true
},
'extends': [
'eslint:recommended',
'plugin:vue/vue3-essential'
],
'rules': {
"vue/multi-word-component-names": "off",
"vue/no-use-v-if-with-v-for": "off"
}
}
-5
View File
@@ -1,5 +0,0 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
-1
View File
@@ -135,7 +135,6 @@ build_frontend() {
npm install npm install
echo "Building frontend files ($RELEASE_TYPE)..." echo "Building frontend files ($RELEASE_TYPE)..."
export NODE_OPTIONS=--openssl-legacy-provider
npm run build -- "$frontend_build_arguments" npm run build -- "$frontend_build_arguments"
} }
+5 -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/css", filepath.Join(config.StaticRootPath, "css"))
router.Static("/mobile/img", filepath.Join(config.StaticRootPath, "img")) router.Static("/mobile/img", filepath.Join(config.StaticRootPath, "img"))
router.Static("/mobile/fonts", filepath.Join(config.StaticRootPath, "fonts")) 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.ico", filepath.Join(config.StaticRootPath, "favicon.ico"))
router.StaticFile("/mobile/favicon.png", filepath.Join(config.StaticRootPath, "favicon.png")) 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/touchicon.png", filepath.Join(config.StaticRootPath, "touchicon.png"))
router.StaticFile("/mobile/manifest.json", filepath.Join(config.StaticRootPath, "manifest.json")) router.StaticFile("/mobile/manifest.json", filepath.Join(config.StaticRootPath, "manifest.json"))
router.StaticFile("/mobile/sw.js", filepath.Join(config.StaticRootPath, "sw.js")) 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 := router.Group("/desktop")
desktopEntryRoute.Use(bindMiddleware(middlewares.ServerSettingsCookie(config))) desktopEntryRoute.Use(bindMiddleware(middlewares.ServerSettingsCookie(config)))
{ {
+3217 -12325
View File
File diff suppressed because it is too large Load Diff
+24 -37
View File
@@ -12,56 +12,43 @@
"url": "https://github.com/mayswind/ezbookkeeping/issues" "url": "https://github.com/mayswind/ezbookkeeping/issues"
}, },
"scripts": { "scripts": {
"serve": "set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve", "serve": "cross-env NODE_ENV=development vite",
"build": "set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service build", "build": "cross-env NODE_ENV=production vite build",
"lint": "vue-cli-service lint" "serve:dist": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
}, },
"dependencies": { "dependencies": {
"axios": "^1.3.5", "@vuepic/vue-datepicker": "^4.4.0",
"axios": "^1.3.6",
"cbor-js": "^0.1.0", "cbor-js": "^0.1.0",
"core-js": "^3.30.0", "clipboard": "^2.0.11",
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"framework7": "^5.7.14", "dom7": "^4.0.6",
"framework7": "^8.0.3",
"framework7-icons": "^5.0.5", "framework7-icons": "^5.0.5",
"framework7-vue": "^5.7.14", "framework7-vue": "^8.0.3",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"line-awesome": "^1.3.0", "line-awesome": "^1.3.0",
"moment": "^2.29.4", "moment": "^2.29.4",
"moment-timezone": "^0.5.43", "moment-timezone": "^0.5.43",
"register-service-worker": "^1.7.2", "register-service-worker": "^1.7.2",
"skeleton-elements": "^4.0.1",
"swiper": "^9.2.3",
"ua-parser-js": "^1.0.35", "ua-parser-js": "^1.0.35",
"vue": "^2.7.14", "vue": "^3.2.47",
"vue-clipboard2": "^0.3.3", "vue-i18n": "^9.2.2",
"vue-i18n": "^8.28.2", "vuex": "^4.1.0"
"vue-pincode-input": "^0.4.0",
"vuex": "^3.6.2"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "^4.5.19", "@vitejs/plugin-vue": "^4.1.0",
"@vue/cli-plugin-eslint": "^4.5.19", "@vue/compiler-sfc": "^3.2.47",
"@vue/cli-plugin-pwa": "^4.5.19", "cross-env": "^7.0.3",
"@vue/cli-service": "^4.5.19", "eslint": "^8.38.0",
"babel-eslint": "^10.1.0", "eslint-plugin-vue": "^9.11.0",
"babel-plugin-component": "^1.1.1", "git-rev-sync": "^3.0.2",
"eslint": "^6.8.0", "postcss-preset-env": "^8.3.2",
"eslint-plugin-vue": "^6.2.2", "vite": "^4.2.2",
"git-revision-webpack-plugin": "^3.0.6", "vite-plugin-pwa": "^0.14.7"
"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": {}
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",
+30
View File
@@ -3,8 +3,38 @@ package utils
import ( import (
"io" "io"
"os" "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 // IsExists returns whether specified file or directory path exits
func IsExists(path string) (bool, error) { func IsExists(path string) (bool, error) {
_, err := os.Stat(path) _, err := os.Stat(path)
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
plugins: {
'postcss-preset-env': {},
},
};
+199 -44
View File
@@ -1,10 +1,11 @@
<template> <template>
<f7-app :params="f7params"> <f7-app v-bind="f7params">
<f7-view id="main-view" class="safe-areas" main url="/"></f7-view> <f7-view id="main-view" class="safe-areas" main url="/"></f7-view>
</f7-app> </f7-app>
</template> </template>
<script> <script>
import { f7ready } from 'framework7-vue';
import routes from './router/mobile.js'; import routes from './router/mobile.js';
export default { export default {
@@ -14,10 +15,20 @@ export default {
return { return {
f7params: { f7params: {
name: 'ezBookkeeping', name: 'ezBookkeeping',
id: 'net.mayswind.ezbookkeeping',
theme: 'ios', theme: 'ios',
autoDarkTheme: self.$settings.isEnableAutoDarkMode(), colors: {
primary: '#c67e48'
},
routes: routes, routes: routes,
darkMode: self.$settings.isEnableAutoDarkMode() ? 'auto' : false,
touch: {
disableContextMenu: true,
tapHold: true
},
serviceWorker: {
path: self.$settings.isProduction() ? './sw.js' : undefined,
scope: './',
},
actions: { actions: {
animate: self.$settings.isEnableAnimate(), animate: self.$settings.isEnableAnimate(),
backdrop: true, backdrop: true,
@@ -46,27 +57,16 @@ export default {
smartSelect: { smartSelect: {
routableModals: false routableModals: false
}, },
touch: {
tapHold: true,
disableContextMenu: true
},
view: { view: {
animate: self.$settings.isEnableAnimate(), animate: self.$settings.isEnableAnimate(),
pushState: !self.isiOSHomeScreenMode(), browserHistory: !self.isiOSHomeScreenMode(),
pushStateAnimate: false, browserHistoryInitialMatch: true,
browserHistoryAnimate: false,
iosSwipeBackAnimateShadow: false, iosSwipeBackAnimateShadow: false,
mdSwipeBackAnimateShadow: false mdSwipeBackAnimateShadow: false
},
calendar: {
locale: 'en',
openIn: 'customModal',
backdrop: true
},
serviceWorker: {
path: self.$settings.isProduction() ? './sw.js' : undefined,
scope: './',
} }
} },
isDarkMode: undefined
} }
}, },
created() { created() {
@@ -82,6 +82,27 @@ export default {
} }
} }
}, },
mounted() {
f7ready((f7) => {
this.isDarkMode = f7.darkMode;
this.setThemeColorMeta(f7.darkMode);
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: { methods: {
isiOSHomeScreenMode() { isiOSHomeScreenMode() {
if ((/iphone|ipod|ipad/gi).test(navigator.platform) && (/Safari/i).test(navigator.appVersion) && if ((/iphone|ipod|ipad/gi).test(navigator.platform) && (/Safari/i).test(navigator.appVersion) &&
@@ -91,6 +112,13 @@ export default {
} }
return false; return false;
},
setThemeColorMeta(isDarkMode) {
if (isDarkMode) {
document.querySelector('meta[name=theme-color]').setAttribute('content', '#121212');
} else {
document.querySelector('meta[name=theme-color]').setAttribute('content', '#f6f6f8');
}
} }
} }
} }
@@ -134,6 +162,10 @@ body {
pointer-events: none !important; pointer-events: none !important;
} }
.skeleton-text {
pointer-events: none !important;
}
.segmented.readonly .button:not(.button-active) > span, .segmented.readonly .button:not(.button-active) > span,
.list.readonly .item-content .item-title.item-label, .list.readonly .item-content .item-title.item-label,
.list.readonly .item-content .item-title > .item-header { .list.readonly .item-content .item-title > .item-header {
@@ -142,19 +174,21 @@ body {
/** Replacing the default style of framework7 **/ /** Replacing the default style of framework7 **/
:root { :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); --default-icon-color: var(--f7-text-color);
} }
:root .theme-dark { :root .dark {
--default-icon-color: var(--f7-text-color); --default-icon-color: var(--f7-text-color);
} }
.ios .theme-dark, .ios.theme-dark { .color-gray {
--f7-theme-color: #8e8e93;
--f7-theme-color-rgb: 142, 142, 147;
--f7-theme-color-shade: #79797f;
--f7-theme-color-tint: #a3a3a7;
}
.ios .dark, .ios.dark {
--f7-list-item-header-text-color: inherit !important; --f7-list-item-header-text-color: inherit !important;
} }
@@ -166,6 +200,15 @@ i.icon.la, i.icon.las, i.icon.lab {
border: 0; border: 0;
} }
/** Replacing the default style of 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 **/ /** Common class for replacing the default style of framework7 **/
.navbar .navbar-compact-icons.right a + a { .navbar .navbar-compact-icons.right a + a {
margin-left: 0; margin-left: 0;
@@ -195,6 +238,12 @@ i.icon.la, i.icon.las, i.icon.lab {
white-space: nowrap; 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 { .list-item-media-valign-middle .item-media {
align-self: normal !important; align-self: normal !important;
} }
@@ -227,7 +276,7 @@ i.icon.la, i.icon.las, i.icon.lab {
font-weight: bold; font-weight: bold;
} }
.theme-dark .list .item-content .list-item-showing { .dark .list .item-content .list-item-showing {
color: rgba(255, 255, 255, 0.2); color: rgba(255, 255, 255, 0.2);
} }
@@ -235,9 +284,34 @@ i.icon.la, i.icon.las, i.icon.lab {
font-weight: bold; font-weight: bold;
} }
.list.list-dividers li.list-group-title: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 {
background-color: transparent;
}
.list.list-dividers li: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 .item-content .list-item-checked-icon { .list .item-content .list-item-checked-icon {
font-size: 20px; font-size: 20px;
color: var(--f7-radio-active-color, var(--f7-theme-color)); color: var(--f7-radio-active-color, var(--f7-theme-color));
margin-right: calc(var(--f7-list-item-media-margin) + var(--f7-checkbox-extra-margin));
}
.popover .popover-inner .list .item-content .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 { .ebk-list-item-error-info div.item-footer {
@@ -261,12 +335,6 @@ i.icon.la, i.icon.las, i.icon.lab {
opacity: 0.6; 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 { .icon-after-text {
margin-left: 6px; margin-left: 6px;
} }
@@ -284,6 +352,100 @@ i.icon.la, i.icon.las, i.icon.lab {
height: 13px; height: 13px;
} }
/** Swipe handler **/
.swipe-handler {
height: 16px;
position: absolute;
left: 0;
width: 100%;
top: 0;
cursor: pointer;
z-index: 10
}
.swipe-handler:after {
content: "";
width: 36px;
height: 6px;
position: absolute;
left: 50%;
top: 50%;
margin-left: -18px;
margin-top: -3px;
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 for framework7 **/
.nested-list-item .item-title { .nested-list-item .item-title {
width: 100%; width: 100%;
@@ -329,18 +491,11 @@ i.icon.la, i.icon.las, i.icon.lab {
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.nested-list-item:last-child > .swipeout-content > .item-link > .item-content > .item-inner:after {
background-color: transparent;
}
.sortable-enabled .nested-list-item .nested-list-item-child .item-inner { .sortable-enabled .nested-list-item .nested-list-item-child .item-inner {
padding-right: var(--f7-safe-area-right) !important; 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> </style>
+21 -17
View File
@@ -1,27 +1,27 @@
<template> <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> <f7-toolbar>
<div class="swipe-handler"></div>
<div class="left"></div> <div class="left"></div>
<div class="right"> <div class="right">
<f7-link sheet-close :text="$t('Done')"></f7-link> <f7-link sheet-close :text="$t('Done')"></f7-link>
</div> </div>
</f7-toolbar> </f7-toolbar>
<f7-page-content> <f7-page-content>
<f7-block class="margin-vertical"> <f7-block class="margin-vertical no-padding">
<f7-row class="padding-vertical padding-horizontal-half" <div class="grid grid-cols-7 padding-vertical-half padding-horizontal-half"
:class="{ 'row-has-selected-item': hasSelectedIcon(row) }" :class="{ 'row-has-selected-item': hasSelectedIcon(row) }"
v-for="(row, idx) in allColorRows" :key="idx"> :key="idx" v-for="(row, idx) in allColorRows">
<f7-col class="text-align-center" v-for="colorInfo in row" :key="colorInfo.color"> <div class="text-align-center" :key="colorInfo.color" v-for="colorInfo in row">
<f7-icon f7="app_fill" <ItemIcon icon-type="fixed-f7" icon-id="app_fill" :color="colorInfo.color" @click="onColorClicked(colorInfo)">
:style="colorInfo.color | iconStyle('default', 'var(--default-icon-color)')"
@click.native="onColorClicked(colorInfo)">
<f7-badge color="default" class="right-bottom-icon" v-if="currentValue && currentValue === colorInfo.color"> <f7-badge color="default" class="right-bottom-icon" v-if="currentValue && currentValue === colorInfo.color">
<f7-icon f7="checkmark_alt"></f7-icon> <f7-icon f7="checkmark_alt"></f7-icon>
</f7-badge> </f7-badge>
</f7-icon> </ItemIcon>
</f7-col> </div>
<f7-col v-for="idx in (itemPerRow - row.length)" :key="idx"></f7-col> </div>
</f7-row>
</f7-block> </f7-block>
</f7-page-content> </f7-page-content>
</f7-sheet> </f7-sheet>
@@ -30,16 +30,20 @@
<script> <script>
export default { export default {
props: [ props: [
'value', 'modelValue',
'columnCount', 'columnCount',
'show', 'show',
'allColorInfos' 'allColorInfos'
], ],
emits: [
'update:modelValue',
'update:show'
],
data() { data() {
const self = this; const self = this;
return { return {
currentValue: self.value, currentValue: self.modelValue,
itemPerRow: self.columnCount || 7 itemPerRow: self.columnCount || 7
} }
}, },
@@ -64,11 +68,11 @@ export default {
methods: { methods: {
onColorClicked(colorInfo) { onColorClicked(colorInfo) {
this.currentValue = colorInfo.color; this.currentValue = colorInfo.color;
this.$emit('input', this.currentValue); this.$emit('update:modelValue', this.currentValue);
this.$emit('update:show', false); this.$emit('update:show', false);
}, },
onSheetOpen(event) { onSheetOpen(event) {
this.currentValue = this.value; this.currentValue = this.modelValue;
this.scrollToSelectedItem(event.$el); this.scrollToSelectedItem(event.$el);
}, },
onSheetClosed() { onSheetClosed() {
@@ -1,56 +1,39 @@
<template> <template>
<f7-sheet style="height:auto" :opened="show" <f7-sheet swipe-to-close swipe-handler=".swipe-handler" style="height:auto"
@sheet:open="onSheetOpen" @sheet:closed="onSheetClosed"> :opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<div class="swipe-handler"></div>
<f7-page-content> <f7-page-content>
<div class="display-flex padding justify-content-space-between align-items-center"> <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 style="font-size: 18px" v-if="title"><b>{{ title }}</b></div>
</div> </div>
<div class="padding-horizontal padding-bottom"> <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> <slot></slot>
<f7-list no-hairlines inline-labels class="no-margin-top margin-bottom"> <VueDatePicker range inline enable-seconds six-weeks
<f7-list-input auto-apply month-name-format="long"
:label="$t('Begin Time')" class="margin-bottom"
type="datepicker" :dark="isDarkMode"
class="date-range-sheet-time-item" :week-start="firstDayOfWeek"
:calendar-params="{ :year-range="yearRange"
timePicker: true, :day-names="dayNames"
dateFormat: $t('input-format.datetime.long'), :is24="is24Hour"
firstDay: defaultFirstDayOfWeek, :partial-range="false"
toolbarCloseText: $t('Done'), :preset-ranges="presetRanges"
timePickerPlaceholder: $t('Select Time'), v-model="dateRange">
timePickerFormat: $locale.getInputTimeIntlDateTimeFormatOptions(), <template #month="{ text }">
monthNames: $locale.getAllLongMonthNames(), {{ $t(`datetime.${text}.short`) }}
monthNamesShort: $locale.getAllShortMonthNames(), </template>
dayNames: $locale.getAllLongWeekdayNames(), <template #month-overlay-value="{ text }">
dayNamesShort: $locale.getAllShortWeekdayNames()}" {{ $t(`datetime.${text}.short`) }}
:value="currentMinDate" </template>
@calendar:change="currentMinDate = $event" </VueDatePicker>
>
</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>
<f7-button large fill <f7-button large fill
:class="{ 'disabled': !currentMinDate || !currentMaxDate }" :class="{ 'disabled': !dateRange[0] || !dateRange[1] }"
:text="$t('Continue')" :text="$t('Continue')"
@click="confirm"> @click="confirm">
</f7-button> </f7-button>
@@ -71,6 +54,10 @@ export default {
'hint', 'hint',
'show' 'show'
], ],
emits: [
'update:show',
'dateRange:change'
],
data() { data() {
const self = this; const self = this;
let minDate = self.$utilities.getTodayFirstUnixTime(); let minDate = self.$utilities.getTodayFirstUnixTime();
@@ -84,65 +71,87 @@ export default {
maxDate = self.maxTime; 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 { return {
currentMinDate: [self.$utilities.getLocalDatetimeFromUnixTime(minDate)], yearRange: [
currentMaxDate: [self.$utilities.getLocalDatetimeFromUnixTime(maxDate)] 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: { computed: {
defaultFirstDayOfWeek() { isDarkMode() {
return this.$store.getters.currentUserFirstDayOfWeek; return this.$root.isDarkMode;
}
},
watch: {
'currentMinDate': function (newValue) {
if (!newValue) {
this.currentMinDate = [this.$utilities.getLocalDatetimeFromUnixTime(this.$utilities.getCurrentUnixTime())];
}
}, },
'currentMaxDate': function (newValue) { firstDayOfWeek() {
if (!newValue) { return this.$store.getters.currentUserFirstDayOfWeek;
this.currentMaxDate = [this.$utilities.getLocalDatetimeFromUnixTime(this.$utilities.getCurrentUnixTime())]; },
} dayNames() {
return this.$locale.getAllMinWeekdayNames();
},
is24Hour() {
const datetimeFormat = this.$t('format.datetime.long');
return this.$utilities.is24HourFormat(datetimeFormat);
},
beginDateTime() {
const actualBeginUnixTime = this.$utilities.getActualUnixTimeForStore(this.$utilities.getUnixTime(this.dateRange[0]), this.$utilities.getTimezoneOffsetMinutes(), this.$utilities.getBrowserTimezoneOffsetMinutes());
return this.$utilities.formatUnixTime(actualBeginUnixTime, this.$t('format.datetime.long'));
},
endDateTime() {
const actualEndUnixTime = this.$utilities.getActualUnixTimeForStore(this.$utilities.getUnixTime(this.dateRange[1]), this.$utilities.getTimezoneOffsetMinutes(), this.$utilities.getBrowserTimezoneOffsetMinutes());
return this.$utilities.formatUnixTime(actualEndUnixTime, this.$t('format.datetime.long'));
},
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: { methods: {
onSheetOpen() { onSheetOpen() {
if (this.minTime) { if (this.minTime) {
const minTime = this.$utilities.getDummyUnixTimeForLocalUsage(this.minTime, this.$utilities.getTimezoneOffsetMinutes(), this.$utilities.getBrowserTimezoneOffsetMinutes()); this.dateRange[0] = this.$utilities.getLocalDatetimeFromUnixTime(this.$utilities.getDummyUnixTimeForLocalUsage(this.minTime, this.$utilities.getTimezoneOffsetMinutes(), this.$utilities.getBrowserTimezoneOffsetMinutes()));
this.currentMinDate = [this.$utilities.getLocalDatetimeFromUnixTime(minTime)];
} }
if (this.maxTime) { if (this.maxTime) {
const maxTime = this.$utilities.getDummyUnixTimeForLocalUsage(this.maxTime, this.$utilities.getTimezoneOffsetMinutes(), this.$utilities.getBrowserTimezoneOffsetMinutes()); this.dateRange[1] = this.$utilities.getLocalDatetimeFromUnixTime(this.$utilities.getDummyUnixTimeForLocalUsage(this.maxTime, this.$utilities.getTimezoneOffsetMinutes(), this.$utilities.getBrowserTimezoneOffsetMinutes()));
this.currentMaxDate = [this.$utilities.getLocalDatetimeFromUnixTime(maxTime)];
} }
}, },
onSheetClosed() { onSheetClosed() {
this.$emit('update:show', false); this.$emit('update:show', false);
}, },
confirm() { confirm() {
if (!this.currentMinDate || !this.currentMaxDate) { if (!this.dateRange[0] || !this.dateRange[1]) {
return; return;
} }
let currentMinDate = this.currentMinDate; const currentMinDate = this.dateRange[0];
const currentMaxDate = this.dateRange[1];
if (this.$utilities.isArray(this.currentMinDate)) { let minUnixTime = this.$utilities.getUnixTime(currentMinDate);
currentMinDate = this.currentMinDate[0]; let maxUnixTime = this.$utilities.getUnixTime(currentMaxDate);
}
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);
if (minUnixTime < 0 || maxUnixTime < 0) { if (minUnixTime < 0 || maxUnixTime < 0) {
this.$toast('Date is too early'); this.$toast('Date is too early');
@@ -160,9 +169,3 @@ export default {
} }
} }
</script> </script>
<style>
.list .date-range-sheet-time-item > .item-content {
padding-left: 0;
}
</style>
@@ -0,0 +1,110 @@
<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('Current Time')" @click="setCurrentTime"></f7-link>
</div>
<div class="right">
<f7-link :text="$t('Done')" @click="confirm"></f7-link>
</div>
</f7-toolbar>
<f7-page-content>
<VueDatePicker inline enable-seconds
auto-apply month-name-format="long"
class="justify-content-center"
: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>
</VueDatePicker>
</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.$locale.getAllMinWeekdayNames();
},
is24Hour() {
const datetimeFormat = this.$t('format.datetime.long');
return this.$utilities.is24HourFormat(datetimeFormat);
}
},
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> <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> <f7-toolbar>
<div class="swipe-handler"></div>
<div class="left"></div> <div class="left"></div>
<div class="right"> <div class="right">
<f7-link sheet-close :text="$t('Done')"></f7-link> <f7-link sheet-close :text="$t('Done')"></f7-link>
</div> </div>
</f7-toolbar> </f7-toolbar>
<f7-page-content> <f7-page-content>
<f7-block class="margin-vertical"> <f7-block class="margin-vertical no-padding">
<f7-row class="padding-vertical-half padding-horizontal-half" <div class="grid grid-cols-7 padding-vertical-half padding-horizontal-half"
:class="{ 'row-has-selected-item': hasSelectedIcon(row) }" :class="{ 'row-has-selected-item': hasSelectedIcon(row) }"
v-for="(row, idx) in allIconRows" :key="idx"> :key="idx" v-for="(row, idx) in allIconRows">
<f7-col class="text-align-center" v-for="iconInfo in row" :key="iconInfo.id"> <div class="text-align-center" :key="iconInfo.id" v-for="iconInfo in row">
<f7-icon :icon="iconInfo.icon" <ItemIcon icon-type="fixed" :icon-id="iconInfo.icon" :color="color" @click="onIconClicked(iconInfo)">
:style="color | iconStyle('default', 'var(--default-icon-color)')"
@click.native="onIconClicked(iconInfo)">
<f7-badge color="default" class="right-bottom-icon" v-if="currentValue && currentValue === iconInfo.id"> <f7-badge color="default" class="right-bottom-icon" v-if="currentValue && currentValue === iconInfo.id">
<f7-icon f7="checkmark_alt"></f7-icon> <f7-icon f7="checkmark_alt"></f7-icon>
</f7-badge> </f7-badge>
</f7-icon> </ItemIcon>
</f7-col> </div>
<f7-col v-for="idx in (itemPerRow - row.length)" :key="idx"></f7-col> </div>
</f7-row>
</f7-block> </f7-block>
</f7-page-content> </f7-page-content>
</f7-sheet> </f7-sheet>
@@ -30,17 +30,21 @@
<script> <script>
export default { export default {
props: [ props: [
'value', 'modelValue',
'color', 'color',
'columnCount', 'columnCount',
'show', 'show',
'allIconInfos' 'allIconInfos'
], ],
emits: [
'update:modelValue',
'update:show'
],
data() { data() {
const self = this; const self = this;
return { return {
currentValue: self.value, currentValue: self.modelValue,
itemPerRow: self.columnCount || 7 itemPerRow: self.columnCount || 7
} }
}, },
@@ -71,18 +75,24 @@ export default {
return ret; return ret;
}, },
hugeIconRows() { heightClass() {
return this.allIconRows.length > 10; if (this.allIconRows.length > 10) {
return 'icon-selection-huge-sheet';
} else if (this.allIconRows.length > 6) {
return 'icon-selection-large-sheet';
} else {
return '';
}
} }
}, },
methods: { methods: {
onIconClicked(iconInfo) { onIconClicked(iconInfo) {
this.currentValue = iconInfo.id; this.currentValue = iconInfo.id;
this.$emit('input', this.currentValue); this.$emit('update:modelValue', this.currentValue);
this.$emit('update:show', false); this.$emit('update:show', false);
}, },
onSheetOpen(event) { onSheetOpen(event) {
this.currentValue = this.value; this.currentValue = this.modelValue;
this.scrollToSelectedItem(event.$el); this.scrollToSelectedItem(event.$el);
}, },
onSheetClosed() { onSheetClosed() {
@@ -128,6 +138,10 @@ export default {
<style> <style>
@media (min-height: 630px) { @media (min-height: 630px) {
.icon-selection-large-sheet {
height: 310px;
}
.icon-selection-huge-sheet { .icon-selection-huge-sheet {
height: 400px; height: 400px;
} }
+51 -9
View File
@@ -1,18 +1,21 @@
<template> <template>
<f7-sheet style="height:auto" :opened="show" @sheet:closed="onSheetClosed"> <f7-sheet swipe-to-close swipe-handler=".swipe-handler" style="height:auto"
<f7-page-content> :opened="show" @sheet:closed="onSheetClosed">
<div class="swipe-handler"></div>
<f7-page-content class="margin-top no-padding-top">
<div class="display-flex padding justify-content-space-between align-items-center"> <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 style="font-size: 18px" v-if="title"><b>{{ title }}</b></div>
</div> </div>
<div class="padding-horizontal padding-bottom"> <div class="padding-horizontal padding-bottom">
<p class="no-margin-top margin-bottom-half" v-if="hint"> <p class="no-margin-top margin-bottom-half" v-if="hint">
<span>{{ hint }}</span> <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" icon-only icon-f7="doc_on_doc" icon-size="16px"
v-if="enableCopy" v-if="enableCopy"
v-clipboard:copy="information" v-clipboard:success="onCopied"></f7-link> ></f7-link>
</p> </p>
<textarea class="information-content full-line" :rows="rowCount" readonly="readonly" v-model="information"></textarea> <textarea class="information-content full-line" :rows="rowCount" :value="information"></textarea>
<div class="margin-top text-align-center"> <div class="margin-top text-align-center">
<f7-link @click="cancel" :text="$t('Close')"></f7-link> <f7-link @click="cancel" :text="$t('Close')"></f7-link>
</div> </div>
@@ -31,14 +34,53 @@ export default {
'enableCopy', 'enableCopy',
'show' '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: { methods: {
onSheetClosed() { onSheetClosed() {
this.$emit('update:show', false); this.close();
},
onCopied() {
this.$emit('info:copied');
}, },
cancel() { 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); 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> <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="{ 'list-item-selection-huge-sheet': hugeListItemRows }" :opened="show"
@sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-toolbar> <f7-toolbar>
<div class="swipe-handler"></div>
<div class="left"></div> <div class="left"></div>
<div class="right"> <div class="right">
<f7-link sheet-close :text="$t('Done')"></f7-link> <f7-link sheet-close :text="$t('Done')"></f7-link>
</div> </div>
</f7-toolbar> </f7-toolbar>
<f7-page-content> <f7-page-content>
<f7-list no-hairlines class="no-margin-top no-margin-bottom"> <f7-list dividers no-hairlines class="no-margin-vertical">
<f7-list-item link="#" no-chevron <f7-list-item link="#" no-chevron
v-for="(item, index) in items" :title="$tIf((titleField ? item[titleField] : item), titleI18n)"
:key="item | itemKeyValue(index, keyField, valueType)" :value="getItemValue(item, index, valueField, valueType)"
:class="{ 'list-item-selected': isSelected(item, index) }" :class="{ 'list-item-selected': isSelected(item, index) }"
:value="item | itemKeyValue(index, valueField, valueType)" :key="getItemValue(item, index, keyField, valueType)"
:title="item | itemFieldContent(titleField, item, titleI18n)" v-for="(item, index) in items"
@click="onItemClicked(item, index)"> @click="onItemClicked(item, index)">
<f7-icon slot="media" <template #content-start>
:icon="item[iconField] | icon(iconType)" <f7-icon class="list-item-checked-icon" f7="checkmark_alt" :style="{ 'color': isSelected(item, index) ? '' : 'transparent' }"></f7-icon>
:style="item[colorField] | iconStyle(iconType, 'var(--default-icon-color)')" </template>
v-if="iconField"></f7-icon> <template #media v-if="iconField">
<f7-icon slot="after" class="list-item-checked-icon" f7="checkmark_alt" v-if="isSelected(item, index)"></f7-icon> <ItemIcon :icon-type="iconType" :icon-id="item[iconField]" :color="item[colorField]"></ItemIcon>
</template>
</f7-list-item> </f7-list-item>
</f7-list> </f7-list>
</f7-page-content> </f7-page-content>
@@ -29,7 +33,7 @@
<script> <script>
export default { export default {
props: [ props: [
'value', 'modelValue',
'valueType', // item or index 'valueType', // item or index
'keyField', // for value type == item 'keyField', // for value type == item
'valueField', // for value type == item 'valueField', // for value type == item
@@ -41,11 +45,15 @@ export default {
'items', 'items',
'show' 'show'
], ],
emits: [
'update:modelValue',
'update:show'
],
data() { data() {
const self = this; const self = this;
return { return {
currentValue: self.value currentValue: self.modelValue
} }
}, },
computed: { computed: {
@@ -54,6 +62,15 @@ export default {
} }
}, },
methods: { methods: {
getItemValue(item, index, fieldName, valueType) {
if (valueType === 'index') {
return index;
} else if (fieldName) {
return item[fieldName];
} else {
return item;
}
},
onItemClicked(item, index) { onItemClicked(item, index) {
if (this.valueType === 'index') { if (this.valueType === 'index') {
this.currentValue = index; this.currentValue = index;
@@ -65,15 +82,15 @@ export default {
} }
} }
this.$emit('input', this.currentValue); this.$emit('update:modelValue', this.currentValue);
this.$emit('update:show', false); this.close();
}, },
onSheetOpen(event) { onSheetOpen(event) {
this.currentValue = this.value; this.currentValue = this.modelValue;
this.scrollToSelectedItem(event.$el); this.scrollToSelectedItem(event.$el);
}, },
onSheetClosed() { onSheetClosed() {
this.$emit('update:show', false); this.close();
}, },
isSelected(item, index) { isSelected(item, index) {
if (this.valueType === 'index') { if (this.valueType === 'index') {
@@ -106,17 +123,9 @@ export default {
} }
container.scrollTop(targetPos); container.scrollTop(targetPos);
} },
}, close() {
filters: { this.$emit('update:show', false);
itemKeyValue(item, index, fieldName, valueType) {
if (valueType === 'index') {
return index;
} else if (fieldName) {
return item[fieldName];
} else {
return item;
}
} }
} }
} }
+38 -30
View File
@@ -1,53 +1,55 @@
<template> <template>
<f7-sheet class="numpad-sheet" :opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed"> <f7-sheet swipe-to-close swipe-handler=".swipe-handler" class="numpad-sheet" style="height: auto"
<f7-page-content class="no-margin no-padding-top"> :opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-row class="numpad-values"> <div class="swipe-handler"></div>
<f7-page-content class="margin-top no-padding-top">
<div class="numpad-values">
<span class="numpad-value" :style="{ fontSize: currentDisplayFontSize + 'px' }">{{ currentDisplay }}</span> <span class="numpad-value" :style="{ fontSize: currentDisplayFontSize + 'px' }">{{ currentDisplay }}</span>
</f7-row> </div>
<f7-row class="numpad-buttons"> <div class="numpad-buttons">
<f7-button class="numpad-button numpad-button-num" @click="inputNum(7)"> <f7-button class="numpad-button numpad-button-num" @mousedown="inputNum(7)">
<span class="numpad-button-text numpad-button-text-normal">7</span> <span class="numpad-button-text numpad-button-text-normal">7</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputNum(8)"> <f7-button class="numpad-button numpad-button-num" @mousedown="inputNum(8)">
<span class="numpad-button-text numpad-button-text-normal">8</span> <span class="numpad-button-text numpad-button-text-normal">8</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputNum(9)"> <f7-button class="numpad-button numpad-button-num" @mousedown="inputNum(9)">
<span class="numpad-button-text numpad-button-text-normal">9</span> <span class="numpad-button-text numpad-button-text-normal">9</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-function no-right-border" @click="setSymbol('×')"> <f7-button class="numpad-button numpad-button-function no-right-border" @mousedown="setSymbol('×')">
<span class="numpad-button-text numpad-button-text-normal">&times;</span> <span class="numpad-button-text numpad-button-text-normal">&times;</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputNum(4)"> <f7-button class="numpad-button numpad-button-num" @mousedown="inputNum(4)">
<span class="numpad-button-text numpad-button-text-normal">4</span> <span class="numpad-button-text numpad-button-text-normal">4</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputNum(5)"> <f7-button class="numpad-button numpad-button-num" @mousedown="inputNum(5)">
<span class="numpad-button-text numpad-button-text-normal">5</span> <span class="numpad-button-text numpad-button-text-normal">5</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputNum(6)"> <f7-button class="numpad-button numpad-button-num" @mousedown="inputNum(6)">
<span class="numpad-button-text numpad-button-text-normal">6</span> <span class="numpad-button-text numpad-button-text-normal">6</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-function no-right-border" @click="setSymbol('')"> <f7-button class="numpad-button numpad-button-function no-right-border" @mousedown="setSymbol('')">
<span class="numpad-button-text numpad-button-text-normal">&minus;</span> <span class="numpad-button-text numpad-button-text-normal">&minus;</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputNum(1)"> <f7-button class="numpad-button numpad-button-num" @mousedown="inputNum(1)">
<span class="numpad-button-text numpad-button-text-normal">1</span> <span class="numpad-button-text numpad-button-text-normal">1</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputNum(2)"> <f7-button class="numpad-button numpad-button-num" @mousedown="inputNum(2)">
<span class="numpad-button-text numpad-button-text-normal">2</span> <span class="numpad-button-text numpad-button-text-normal">2</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputNum(3)"> <f7-button class="numpad-button numpad-button-num" @mousedown="inputNum(3)">
<span class="numpad-button-text numpad-button-text-normal">3</span> <span class="numpad-button-text numpad-button-text-normal">3</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-function no-right-border" @click="setSymbol('+')"> <f7-button class="numpad-button numpad-button-function no-right-border" @mousedown="setSymbol('+')">
<span class="numpad-button-text numpad-button-text-normal">&plus;</span> <span class="numpad-button-text numpad-button-text-normal">&plus;</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputDot()"> <f7-button class="numpad-button numpad-button-num" @mousedown="inputDot()">
<span class="numpad-button-text numpad-button-text-normal">.</span> <span class="numpad-button-text numpad-button-text-normal">.</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputNum(0)"> <f7-button class="numpad-button numpad-button-num" @mousedown="inputNum(0)">
<span class="numpad-button-text numpad-button-text-normal">0</span> <span class="numpad-button-text numpad-button-text-normal">0</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-num" @click="backspace" @taphold.native="clear()"> <f7-button class="numpad-button numpad-button-num" @mousedown="backspace" @taphold="clear()">
<span class="numpad-button-text numpad-button-text-normal"> <span class="numpad-button-text numpad-button-text-normal">
<f7-icon f7="delete_left"></f7-icon> <f7-icon f7="delete_left"></f7-icon>
</span> </span>
@@ -55,7 +57,7 @@
<f7-button class="numpad-button numpad-button-confirm no-right-border no-bottom-border" fill @click="confirm()"> <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> <span :class="{ 'numpad-button-text': true, 'numpad-button-text-confirm': !currentSymbol }">{{ confirmText }}</span>
</f7-button> </f7-button>
</f7-row> </div>
</f7-page-content> </f7-page-content>
</f7-sheet> </f7-sheet>
</template> </template>
@@ -63,18 +65,22 @@
<script> <script>
export default { export default {
props: [ props: [
'value', 'modelValue',
'minValue', 'minValue',
'maxValue', 'maxValue',
'show' 'show'
], ],
emits: [
'update:modelValue',
'update:show'
],
data() { data() {
const self = this; const self = this;
return { return {
previousValue: '', previousValue: '',
currentSymbol: '', currentSymbol: '',
currentValue: self.getStringValue(self.value) currentValue: self.getStringValue(self.modelValue)
} }
}, },
computed: { computed: {
@@ -103,7 +109,7 @@ export default {
if (this.currentSymbol) { if (this.currentSymbol) {
return '='; return '=';
} else { } else {
return this.$i18n.t('OK'); return this.$t('OK');
} }
} }
}, },
@@ -291,17 +297,20 @@ export default {
} else { } else {
const value = this.$utilities.stringCurrencyToNumeric(this.currentValue); const value = this.$utilities.stringCurrencyToNumeric(this.currentValue);
this.$emit('input', value); this.$emit('update:modelValue', value);
this.$emit('update:show', false); this.close();
return true; return true;
} }
}, },
close() {
this.$emit('update:show', false);
},
onSheetOpen() { onSheetOpen() {
this.currentValue = this.getStringValue(this.value); this.currentValue = this.getStringValue(this.modelValue);
}, },
onSheetClosed() { onSheetClosed() {
this.$emit('update:show', false); this.close();
} }
} }
} }
@@ -322,7 +331,6 @@ export default {
padding-left: 16px; padding-left: 16px;
line-height: 1; line-height: 1;
height: 50px; height: 50px;
justify-content: center;
align-items: center; align-items: center;
box-sizing: border-box; box-sizing: border-box;
user-select: none; user-select: none;
@@ -371,7 +379,7 @@ export default {
color: var(--f7-color-black); color: var(--f7-color-black);
} }
.theme-dark .numpad-button-text-normal { .dark .numpad-button-text-normal {
color: var(--f7-color-white); color: var(--f7-color-white);
} }
+22 -11
View File
@@ -1,23 +1,26 @@
<template> <template>
<f7-sheet style="height:auto" :opened="show" <f7-sheet swipe-to-close swipe-handler=".swipe-handler" style="height:auto"
@sheet:open="onSheetOpen" @sheet:closed="onSheetClosed"> :opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-page-content> <div class="swipe-handler"></div>
<f7-page-content class="margin-top no-padding-top">
<div class="display-flex padding justify-content-space-between align-items-center"> <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 style="font-size: 18px" v-if="title"><b>{{ title }}</b></div>
</div> </div>
<div class="padding-horizontal padding-bottom"> <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> <slot></slot>
<f7-list no-hairlines class="no-margin-top margin-bottom"> <f7-list no-hairlines strong class="no-margin">
<f7-list-input <f7-list-input
type="number" type="number"
autocomplete="one-time-code" autocomplete="one-time-code"
outline outline
floating-label
clear-button clear-button
class="no-margin no-padding-bottom"
:label="$t('Password')"
:placeholder="$t('Passcode')" :placeholder="$t('Passcode')"
:value="currentPasscode" v-model:value="currentPasscode"
@input="currentPasscode = $event.target.value" @keyup.enter="confirm()"
@keyup.enter.native="confirm()"
></f7-list-input> ></f7-list-input>
</f7-list> </f7-list>
<f7-button large fill <f7-button large fill
@@ -36,13 +39,18 @@
<script> <script>
export default { export default {
props: [ props: [
'value', 'modelValue',
'title', 'title',
'hint', 'hint',
'confirmDisabled', 'confirmDisabled',
'cancelDisabled', 'cancelDisabled',
'show' 'show'
], ],
emits: [
'update:modelValue',
'update:show',
'passcode:confirm'
],
data() { data() {
return { return {
currentPasscode: '' currentPasscode: ''
@@ -53,17 +61,20 @@ export default {
this.currentPasscode = ''; this.currentPasscode = '';
}, },
onSheetClosed() { onSheetClosed() {
this.$emit('update:show', false); this.close();
}, },
confirm() { confirm() {
if (!this.currentPasscode || this.confirmDisabled) { if (!this.currentPasscode || this.confirmDisabled) {
return; return;
} }
this.$emit('input', this.currentPasscode); this.$emit('update:modelValue', this.currentPasscode);
this.$emit('passcode:confirm', this.currentPasscode); this.$emit('passcode:confirm', this.currentPasscode);
}, },
cancel() { cancel() {
this.close();
},
close() {
this.$emit('update:show', false); this.$emit('update:show', false);
} }
} }
+22 -11
View File
@@ -1,22 +1,25 @@
<template> <template>
<f7-sheet style="height:auto" :opened="show" <f7-sheet swipe-to-close swipe-handler=".swipe-handler" style="height:auto"
@sheet:open="onSheetOpen" @sheet:closed="onSheetClosed"> :opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-page-content> <div class="swipe-handler"></div>
<f7-page-content class="margin-top no-padding-top">
<div class="display-flex padding justify-content-space-between align-items-center"> <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 style="font-size: 18px" v-if="title"><b>{{ title }}</b></div>
</div> </div>
<div class="padding-horizontal padding-bottom"> <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>
<f7-list no-hairlines class="no-margin-top margin-bottom"> <f7-list no-hairlines strong class="no-margin">
<f7-list-input <f7-list-input
type="password" type="password"
autocomplete="current-password" autocomplete="current-password"
outline outline
floating-label
clear-button clear-button
class="no-margin no-padding-bottom"
:label="$t('Password')"
:placeholder="$t('Password')" :placeholder="$t('Password')"
:value="currentPassword" v-model:value="currentPassword"
@input="currentPassword = $event.target.value" @keyup.enter="confirm()"
@keyup.enter.native="confirm()"
></f7-list-input> ></f7-list-input>
</f7-list> </f7-list>
<f7-button large fill <f7-button large fill
@@ -35,13 +38,18 @@
<script> <script>
export default { export default {
props: [ props: [
'value', 'modelValue',
'title', 'title',
'hint', 'hint',
'confirmDisabled', 'confirmDisabled',
'cancelDisabled', 'cancelDisabled',
'show' 'show'
], ],
emits: [
'update:modelValue',
'update:show',
'password:confirm'
],
data() { data() {
return { return {
currentPassword: '' currentPassword: ''
@@ -52,17 +60,20 @@ export default {
this.currentPassword = ''; this.currentPassword = '';
}, },
onSheetClosed() { onSheetClosed() {
this.$emit('update:show', false); this.close();
}, },
confirm() { confirm() {
if (!this.currentPassword || this.confirmDisabled) { if (!this.currentPassword || this.confirmDisabled) {
return; return;
} }
this.$emit('input', this.currentPassword); this.$emit('update:modelValue', this.currentPassword);
this.$emit('password:confirm', this.currentPassword); this.$emit('password:confirm', this.currentPassword);
}, },
cancel() { cancel() {
this.close();
},
close() {
this.$emit('update:show', false); 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-background" cx="0" cy="0" :r="diameter"></circle>
<circle class="pie-chart-item" <circle class="pie-chart-item"
v-for="(item, idx) in validItems" :key="idx"
fill="transparent" fill="transparent"
cx="0" cy="0" cx="0" cy="0"
:r="diameter / 2" :r="diameter / 2"
:stroke="item.color | defaultIconColor('var(--default-icon-color)')" :stroke="getColor(item.color)"
:stroke-width="diameter" :stroke-width="diameter"
:stroke-dasharray="item | itemStrokeDash(circumference)" :stroke-dasharray="getItemStrokeDash(item)"
:stroke-dashoffset="item | itemDashOffset(validItems, circumference, itemCommonDashOffset)" :stroke-dashoffset="getItemDashOffset(item, validItems, itemCommonDashOffset)"
:key="idx"
v-for="(item, idx) in validItems"
@click="switchSelectedIndex(idx)"> @click="switchSelectedIndex(idx)">
</circle> </circle>
@@ -48,8 +49,8 @@
<span class="skeleton-text">Percent</span> <span class="skeleton-text">Percent</span>
</f7-chip> </f7-chip>
<f7-chip outline <f7-chip outline
:text="(selectedItem.percent) | percent(2, '&lt;0.01')" :text="$utilities.formatPercent(selectedItem.percent, 2, '&lt;0.01')"
:style="(selectedItem ? selectedItem.color : '') | iconStyle('default', 'var(--default-icon-color)', '--f7-chip-outline-border-color')" :style="getColorStyle(selectedItem ? selectedItem.color : '', '--f7-chip-outline-border-color')"
v-else-if="!skeleton"></f7-chip> v-else-if="!skeleton"></f7-chip>
</p> </p>
<p v-else-if="!validItems || !validItems.length"> <p v-else-if="!validItems || !validItems.length">
@@ -59,7 +60,7 @@
<span class="skeleton-text" v-if="skeleton">Name</span> <span class="skeleton-text" v-if="skeleton">Name</span>
<span v-else-if="!skeleton && selectedItem.name">{{ selectedItem.name }}</span> <span v-else-if="!skeleton && selectedItem.name">{{ selectedItem.name }}</span>
<span class="skeleton-text" v-if="skeleton">Value</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-icon class="item-navigate-icon" f7="chevron_right" v-if="enableClickItem"></f7-icon>
</f7-link> </f7-link>
<f7-link :no-link-class="true" v-else-if="!validItems || !validItems.length"> <f7-link :no-link-class="true" v-else-if="!validItems || !validItems.length">
@@ -107,6 +108,9 @@ export default {
'enableClickItem', 'enableClickItem',
'centerTextBackground', 'centerTextBackground',
], ],
emits: [
'click'
],
data: function () { data: function () {
const diameter = 100; const diameter = 100;
@@ -195,8 +199,11 @@ export default {
} }
}, },
watch: { watch: {
'items': function () { 'items': {
this.selectedIndex = 0; handler() {
this.selectedIndex = 0;
},
deep: true
} }
}, },
methods: { methods: {
@@ -216,14 +223,32 @@ export default {
if (this.enableClickItem) { if (this.enableClickItem) {
this.$emit('click', item.sourceItem); 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; let allPreviousPercent = 0;
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
@@ -237,17 +262,17 @@ export default {
} }
if (offset) { if (offset) {
offset += circumference / 4; offset += this.circumference / 4;
} else { } else {
offset = circumference / 4; offset = this.circumference / 4;
} }
if (allPreviousPercent <= 0) { if (allPreviousPercent <= 0) {
return offset; return offset;
} }
const allPreviousLength = allPreviousPercent * circumference; const allPreviousLength = allPreviousPercent * this.circumference;
return circumference - allPreviousLength + offset; return this.circumference - allPreviousLength + offset;
} }
} }
} }
@@ -314,7 +339,7 @@ export default {
fill: #f0f0f0; fill: #f0f0f0;
} }
.theme-dark .pie-chart-background { .dark .pie-chart-background {
fill: #181818; 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> <template>
<f7-sheet style="height:auto" :opened="show" <f7-sheet swipe-to-close swipe-handler=".swipe-handler" style="height:auto"
@sheet:open="onSheetOpen" @sheet:closed="onSheetClosed"> :opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-page-content> <div class="swipe-handler"></div>
<f7-page-content class="margin-top no-padding-top">
<div class="display-flex padding justify-content-space-between align-items-center"> <div class="display-flex padding justify-content-space-between align-items-center">
<div style="font-size: 18px"><b>{{ title }}</b></div> <div style="font-size: 18px"><b>{{ title }}</b></div>
</div> </div>
<div class="padding-horizontal padding-bottom"> <div class="padding-horizontal padding-bottom">
<p class="no-margin-top margin-bottom-half">{{ hint }}</p> <p class="no-margin">{{ hint }}</p>
<f7-list no-hairlines class="no-margin-top margin-bottom"> <f7-list no-hairlines class="no-margin">
<f7-list-item class="list-item-pincode-input"> <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-item>
</f7-list> </f7-list>
<f7-button large fill <f7-button large fill
@@ -28,13 +29,18 @@
<script> <script>
export default { export default {
props: [ props: [
'value', 'modelValue',
'title', 'title',
'hint', 'hint',
'confirmDisabled', 'confirmDisabled',
'cancelDisabled', 'cancelDisabled',
'show' 'show'
], ],
emits: [
'update:modelValue',
'update:show',
'pincode:confirm'
],
data() { data() {
return { return {
currentPinCode: '' currentPinCode: ''
@@ -57,7 +63,7 @@ export default {
return; return;
} }
this.$emit('input', this.currentPinCode); this.$emit('update:modelValue', this.currentPinCode);
this.$emit('pincode:confirm', this.currentPinCode); this.$emit('pincode:confirm', this.currentPinCode);
}, },
cancel() { cancel() {
@@ -1,6 +1,9 @@
<template> <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> <f7-toolbar>
<div class="swipe-handler"></div>
<div class="left"> <div class="left">
<f7-link sheet-close :text="$t('Cancel')"></f7-link> <f7-link sheet-close :text="$t('Cancel')"></f7-link>
</div> </div>
@@ -12,22 +15,25 @@
<f7-list no-hairlines class="no-margin-top no-margin-bottom" v-if="!items || !items.length || noAvailableTag"> <f7-list no-hairlines 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-item :title="$t('No available tag')"></f7-list-item>
</f7-list> </f7-list>
<f7-list no-hairlines class="no-margin-top no-margin-bottom" v-else-if="items && items.length && !noAvailableTag"> <f7-list dividers 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" <f7-list-item checkbox
v-show="!item.hidden" v-show="!item.hidden"
:key="item.id" :class="isChecked(item.id) ? 'list-item-selected' : ''"
:class="item.id | tagItemClass(selectedItemIds)"
:value="item.id" :value="item.id"
:checked="item.id | isChecked(selectedItemIds)" :checked="isChecked(item.id)"
:key="item.id"
v-for="item in items"
@change="changeItemSelection"> @change="changeItemSelection">
<f7-block slot="title" class="no-padding no-margin"> <template #title>
<div class="display-flex"> <f7-block class="no-padding no-margin">
<f7-icon slot="media" f7="number"></f7-icon> <div class="display-flex">
<div class="tag-selection-list-item list-item-valign-middle padding-left-half"> <f7-icon f7="number"></f7-icon>
{{ item.name }} <div class="tag-selection-list-item list-item-valign-middle padding-left-half">
{{ item.name }}
</div>
</div> </div>
</div> </f7-block>
</f7-block> </template>
</f7-list-item> </f7-list-item>
</f7-list> </f7-list>
</f7-page-content> </f7-page-content>
@@ -37,15 +43,19 @@
<script> <script>
export default { export default {
props: [ props: [
'value', 'modelValue',
'items', 'items',
'show' 'show'
], ],
emits: [
'update:modelValue',
'update:show'
],
data() { data() {
const self = this; const self = this;
return { return {
selectedItemIds: self.$utilities.copyArrayTo(self.value, []) selectedItemIds: self.$utilities.copyArrayTo(self.modelValue, [])
} }
}, },
computed: { computed: {
@@ -64,11 +74,11 @@ export default {
}, },
methods: { methods: {
save() { save() {
this.$emit('input', this.selectedItemIds); this.$emit('update:modelValue', this.selectedItemIds);
this.$emit('update:show', false); this.$emit('update:show', false);
}, },
onSheetOpen(event) { onSheetOpen(event) {
this.selectedItemIds = this.$utilities.copyArrayTo(this.value, []); this.selectedItemIds = this.$utilities.copyArrayTo(this.modelValue, []);
this.scrollToSelectedItem(event.$el); this.scrollToSelectedItem(event.$el);
}, },
onSheetClosed() { onSheetClosed() {
@@ -95,9 +105,6 @@ export default {
} }
}, },
scrollToSelectedItem(parent) { scrollToSelectedItem(parent) {
const app = this.$f7;
const $$ = app.$;
if (!parent || !parent.length) { if (!parent || !parent.length) {
return; return;
} }
@@ -113,8 +120,8 @@ export default {
let lastSelectedItem = selectedItem; let lastSelectedItem = selectedItem;
if (selectedItem.length > 0) { if (selectedItem.length > 0) {
firstSelectedItem = $$(selectedItem[0]); firstSelectedItem = this.$ui.elements(selectedItem[0]);
lastSelectedItem = $$(selectedItem[selectedItem.length - 1]); lastSelectedItem = this.$ui.elements(selectedItem[selectedItem.length - 1]);
} }
let firstSelectedItemInTop = firstSelectedItem.offset().top - container.offset().top - parseInt(container.css('padding-top'), 10); let firstSelectedItemInTop = firstSelectedItem.offset().top - container.offset().top - parseInt(container.css('padding-top'), 10);
@@ -133,26 +140,15 @@ export default {
} }
container.scrollTop(targetPos); container.scrollTop(targetPos);
} },
}, isChecked(itemId) {
filters: { for (let i = 0; i < this.selectedItemIds.length; i++) {
isChecked(itemId, selectedItemIds) { if (this.selectedItemIds[i] === itemId) {
for (let i = 0; i < selectedItemIds.length; i++) {
if (selectedItemIds[i] === itemId) {
return true; return true;
} }
} }
return false; 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; text-overflow: ellipsis;
} }
</style> </style>
self
@@ -1,6 +1,9 @@
<template> <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> <f7-toolbar>
<div class="swipe-handler"></div>
<div class="left"></div> <div class="left"></div>
<div class="right"> <div class="right">
<f7-link sheet-close :text="$t('Done')"></f7-link> <f7-link sheet-close :text="$t('Done')"></f7-link>
@@ -8,26 +11,26 @@
</f7-toolbar> </f7-toolbar>
<f7-page-content> <f7-page-content>
<f7-treeview> <f7-treeview>
<f7-treeview-item v-for="item in items" <f7-treeview-item item-toggle
item-toggle
:opened="isPrimaryItemHasSecondaryValue(item)" :opened="isPrimaryItemHasSecondaryValue(item)"
:key="item | itemFieldContent(primaryKeyField, item, false)" :label="$tIf((primaryTitleField ? item[primaryTitleField] : item), primaryTitleI18n)"
:label="item | itemFieldContent(primaryTitleField, item, primaryTitleI18n)"> :key="primaryKeyField ? item[primaryKeyField] : item"
<f7-icon slot="media" v-for="item in items">
:icon="item[primaryIconField] | icon(primaryIconType)" <template #media>
:style="item[primaryColorField] | iconStyle(primaryIconType, 'var(--default-icon-color)')" <ItemIcon :icon-type="primaryIconType" :icon-id="item[primaryIconField]"
v-if="primaryIconField"></f7-icon> :color="item[primaryColorField]" v-if="primaryIconField"></ItemIcon>
</template>
<f7-treeview-item v-for="subItem in item[primarySubItemsField]" <f7-treeview-item selectable
selectable
:selected="isSecondarySelected(subItem)" :selected="isSecondarySelected(subItem)"
:key="subItem | itemFieldContent(secondaryKeyField, subItem, false)" :label="$tIf((secondaryTitleField ? subItem[secondaryTitleField] : subItem), secondaryTitleI18n)"
:label="subItem | itemFieldContent(secondaryTitleField, subItem, secondaryTitleI18n)" :key="secondaryKeyField ? subItem[secondaryKeyField] : subItem"
v-for="subItem in item[primarySubItemsField]"
@click="onSecondaryItemClicked(subItem)"> @click="onSecondaryItemClicked(subItem)">
<f7-icon slot="media" <template #media>
:icon="subItem[secondaryIconField] | icon(secondaryIconType)" <ItemIcon :icon-type="secondaryIconType" :icon-id="subItem[secondaryIconField]"
:style="subItem[secondaryColorField] | iconStyle(secondaryIconType, 'var(--default-icon-color)')" :color="subItem[secondaryColorField]" v-if="secondaryIconField"></ItemIcon>
v-if="secondaryIconField"></f7-icon> </template>
</f7-treeview-item> </f7-treeview-item>
</f7-treeview-item> </f7-treeview-item>
</f7-treeview> </f7-treeview>
@@ -38,7 +41,7 @@
<script> <script>
export default { export default {
props: [ props: [
'value', 'modelValue',
'primaryKeyField', 'primaryKeyField',
'primaryValueField', 'primaryValueField',
'primaryTitleField', 'primaryTitleField',
@@ -57,11 +60,15 @@ export default {
'items', 'items',
'show' 'show'
], ],
emits: [
'update:modelValue',
'update:show'
],
data() { data() {
const self = this; const self = this;
return { return {
currentValue: self.value currentValue: self.modelValue
} }
}, },
computed: { computed: {
@@ -85,7 +92,7 @@ export default {
}, },
methods: { methods: {
onSheetOpen(event) { onSheetOpen(event) {
this.currentValue = this.value; this.currentValue = this.modelValue;
this.scrollToSelectedItem(event.$el); this.scrollToSelectedItem(event.$el);
}, },
onSheetClosed() { onSheetClosed() {
@@ -98,7 +105,7 @@ export default {
this.currentValue = subItem; this.currentValue = subItem;
} }
this.$emit('input', this.currentValue); this.$emit('update:modelValue', this.currentValue);
this.$emit('update:show', false); this.$emit('update:show', false);
}, },
isPrimaryItemHasSecondaryValue(primaryItem) { isPrimaryItemHasSecondaryValue(primaryItem) {
@@ -1,56 +1,60 @@
<template> <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> <f7-toolbar>
<div class="swipe-handler"></div>
<div class="left"></div> <div class="left"></div>
<div class="right"> <div class="right">
<f7-link sheet-close :text="$t('Done')"></f7-link> <f7-link sheet-close :text="$t('Done')"></f7-link>
</div> </div>
</f7-toolbar> </f7-toolbar>
<f7-page-content> <f7-page-content>
<f7-row> <div class="grid grid-cols-2 grid-gap">
<f7-col width="50"> <div>
<div class="primary-list-container"> <div class="primary-list-container">
<f7-list no-hairlines class="primary-list no-margin-top no-margin-bottom"> <f7-list dividers no-hairlines class="primary-list no-margin-vertical">
<f7-list-item link="#" no-chevron <f7-list-item link="#" no-chevron
v-for="item in items"
:key="item | itemFieldContent(primaryKeyField, item, false)"
:class="{ 'primary-list-item-selected': item === selectedPrimaryItem }" :class="{ 'primary-list-item-selected': item === selectedPrimaryItem }"
:value="item | itemFieldContent(primaryValueField, item, false)" :value="primaryValueField ? item[primaryValueField] : item"
:title="item | itemFieldContent(primaryTitleField, null, primaryTitleI18n)" :title="$tIf(item[primaryTitleField], primaryTitleI18n)"
:header="item | itemFieldContent(primaryHeaderField, null, primaryHeaderI18n)" :header="$tIf(item[primaryHeaderField], primaryHeaderI18n)"
:footer="item | itemFieldContent(primaryFooterField, null, primaryFooterI18n)" :footer="$tIf(item[primaryFooterField], primaryFooterI18n)"
:key="primaryKeyField ? item[primaryKeyField] : item"
v-for="item in items"
@click="onPrimaryItemClicked(item)"> @click="onPrimaryItemClicked(item)">
<f7-icon slot="media" <template #media>
:icon="item[primaryIconField] | icon(primaryIconType)" <ItemIcon :icon-type="primaryIconType" :icon-id="item[primaryIconField]" :color="item[primaryColorField]"></ItemIcon>
:style="item[primaryColorField] | iconStyle(primaryIconType, 'var(--default-icon-color)')" </template>
v-if="primaryIconField"></f7-icon> <template #after>
<f7-icon slot="after" class="list-item-showing" f7="chevron_right" v-if="item === selectedPrimaryItem"></f7-icon> <f7-icon class="list-item-showing" f7="chevron_right" v-if="item === selectedPrimaryItem"></f7-icon>
</template>
</f7-list-item> </f7-list-item>
</f7-list> </f7-list>
</div> </div>
</f7-col> </div>
<f7-col width="50"> <div>
<div class="secondary-list-container"> <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 no-hairlines class="secondary-list no-margin-vertical" v-if="selectedPrimaryItem && primarySubItemsField && selectedPrimaryItem[primarySubItemsField]">
<f7-list-item link="#" no-chevron <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) }" :class="{ 'secondary-list-item-selected': isSecondarySelected(subItem) }"
:value="subItem | itemFieldContent(secondaryValueField, subItem, false)" :value="secondaryValueField ? subItem[secondaryValueField] : subItem"
:title="subItem | itemFieldContent(secondaryTitleField, null, secondaryTitleI18n)" :title="$tIf(subItem[secondaryTitleField], secondaryTitleI18n)"
:header="subItem | itemFieldContent(secondaryHeaderField, null, secondaryHeaderI18n)" :header="$tIf(subItem[secondaryHeaderField], secondaryHeaderI18n)"
:footer="subItem | itemFieldContent(secondaryFooterField, null, secondaryFooterI18n)" :footer="$tIf(subItem[secondaryFooterField], secondaryFooterI18n)"
:key="secondaryKeyField ? subItem[secondaryKeyField] : subItem"
v-for="subItem in selectedPrimaryItem[primarySubItemsField]"
@click="onSecondaryItemClicked(subItem)"> @click="onSecondaryItemClicked(subItem)">
<f7-icon slot="media" <template #media>
:icon="subItem[secondaryIconField] | icon(secondaryIconType)" <ItemIcon :icon-type="secondaryIconType" :icon-id="subItem[secondaryIconField]" :color="subItem[secondaryColorField]"></ItemIcon>
:style="subItem[secondaryColorField] | iconStyle(secondaryIconType, 'var(--default-icon-color)')" </template>
v-if="secondaryIconField"></f7-icon> <template #after>
<f7-icon slot="after" class="list-item-checked-icon" f7="checkmark_alt" v-if="isSecondarySelected(subItem)"></f7-icon> <f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="isSecondarySelected(subItem)"></f7-icon>
</template>
</f7-list-item> </f7-list-item>
</f7-list> </f7-list>
</div> </div>
</f7-col> </div>
</f7-row> </div>
</f7-page-content> </f7-page-content>
</f7-sheet> </f7-sheet>
</template> </template>
@@ -58,7 +62,7 @@
<script> <script>
export default { export default {
props: [ props: [
'value', 'modelValue',
'primaryKeyField', 'primaryKeyField',
'primaryValueField', 'primaryValueField',
'primaryTitleField', 'primaryTitleField',
@@ -85,12 +89,16 @@ export default {
'items', 'items',
'show' 'show'
], ],
emits: [
'update:modelValue',
'update:show'
],
data() { data() {
const self = this; const self = this;
return { return {
currentPrimaryValue: self.getPrimaryValueBySecondaryValue(self.value), currentPrimaryValue: self.getPrimaryValueBySecondaryValue(self.modelValue),
currentSecondaryValue: self.value currentSecondaryValue: self.modelValue
} }
}, },
computed: { computed: {
@@ -126,13 +134,13 @@ export default {
}, },
methods: { methods: {
onSheetOpen(event) { onSheetOpen(event) {
this.currentPrimaryValue = this.getPrimaryValueBySecondaryValue(this.value); this.currentPrimaryValue = this.getPrimaryValueBySecondaryValue(this.modelValue);
this.currentSecondaryValue = this.value; this.currentSecondaryValue = this.modelValue;
this.scrollToSelectedItem(event.$el, '.primary-list-container', 'li.primary-list-item-selected'); this.scrollToSelectedItem(event.$el, '.primary-list-container', 'li.primary-list-item-selected');
this.scrollToSelectedItem(event.$el, '.secondary-list-container', 'li.secondary-list-item-selected'); this.scrollToSelectedItem(event.$el, '.secondary-list-container', 'li.secondary-list-item-selected');
}, },
onSheetClosed() { onSheetClosed() {
this.$emit('update:show', false); this.close();
}, },
onPrimaryItemClicked(item) { onPrimaryItemClicked(item) {
if (this.primaryValueField) { if (this.primaryValueField) {
@@ -148,8 +156,8 @@ export default {
this.currentSecondaryValue = subItem; this.currentSecondaryValue = subItem;
} }
this.$emit('input', this.currentSecondaryValue); this.$emit('update:modelValue', this.currentSecondaryValue);
this.$emit('update:show', false); this.close();
}, },
isSecondarySelected(subItem) { isSecondarySelected(subItem) {
if (this.secondaryValueField) { if (this.secondaryValueField) {
@@ -226,6 +234,9 @@ export default {
} }
container.scrollTop(targetPos); container.scrollTop(targetPos);
},
close() {
this.$emit('update:show', false);
} }
} }
} }
+3 -5
View File
@@ -1,8 +1,6 @@
import Vue from 'vue'; import { createApp } from 'vue'
import App from './Desktop.vue'; import App from './Desktop.vue';
new Vue({ const app = createApp(App);
el: '#app', app.mount('#app');
render: h => h(App),
})
+6 -4
View File
@@ -10,9 +10,11 @@
<title>ezBookkeeping</title> <title>ezBookkeeping</title>
</head> </head>
<body> <body>
<noscript> <noscript>
<strong>We're sorry but ezBookkeeping doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> <strong>We're sorry but ezBookkeeping doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript> </noscript>
<div id="app"></div> <div id="app"></div>
<script type="module" src="./desktop-main.js"></script>
</body> </body>
</html> </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.substring(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.substring(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"> <link rel="manifest" href="manifest.json">
</head> </head>
<body> <body>
<noscript> <noscript>
<strong>We're sorry but ezBookkeeping doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> <strong>We're sorry but ezBookkeeping doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript> </noscript>
<div id="app"></div> <div id="app"></div>
<script type="module" src="./index-main.js"></script>
</body> </body>
</html> </html>
+194 -2
View File
@@ -1,4 +1,8 @@
import { defaultLanguage, allLanguages } from '../locales/index.js'; import { defaultLanguage, allLanguages } from '../locales/index.js';
import timezone from "../consts/timezone.js";
import currency from "../consts/currency.js";
import settings from "./settings";
import utils from './utils.js';
const apiNotFoundErrorCode = 100001; const apiNotFoundErrorCode = 100001;
const specifiedApiNotFoundErrors = { const specifiedApiNotFoundErrors = {
@@ -121,11 +125,11 @@ const parameterizedErrors = [
} }
]; ];
export function getAllLanguages() { export function getAllLanguageInfos() {
return allLanguages; return allLanguages;
} }
export function getLanguage(locale) { export function getLanguageInfo(locale) {
return allLanguages[locale]; return allLanguages[locale];
} }
@@ -168,8 +172,196 @@ export function getDefaultLanguage() {
return browserLocale; 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 getAllTimezones(includeSystemDefault, translateFn) {
const defaultTimezoneOffset = utils.getTimezoneOffset();
const defaultTimezoneOffsetMinutes = utils.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 !== 'Etc/GMT' ? utils.getTimezoneOffset(allTimezones[i].timezoneName) : ''),
utcOffsetMinutes: utils.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 (!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.substring(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 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() { export function getI18nOptions() {
return { return {
legacy: false,
locale: defaultLanguage, locale: defaultLanguage,
fallbackLocale: defaultLanguage, fallbackLocale: defaultLanguage,
formatFallbackMessages: true, formatFallbackMessages: true,
+2 -2
View File
@@ -1,8 +1,8 @@
export default { export default {
getLicense: () => { getLicense: () => {
return process.env.LICENSE; return __EZBOOKKEEPING_LICENSE__; // eslint-disable-line
}, },
getThirdPartyLicenses: () => { 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';
});
}
+164
View File
@@ -1,5 +1,6 @@
import CryptoJS from 'crypto-js'; import CryptoJS from 'crypto-js';
import moment from 'moment'; import moment from 'moment';
import Clipboard from 'clipboard';
import uaParser from 'ua-parser-js'; import uaParser from 'ua-parser-js';
import dateTimeConstants from '../consts/datetime.js'; import dateTimeConstants from '../consts/datetime.js';
@@ -157,6 +158,10 @@ function getCurrentUnixTime() {
return moment().unix(); return moment().unix();
} }
function getCurrentDateTime() {
return moment();
}
function parseDateFromUnixTime(unixTime, utcOffset, currentUtcOffset) { function parseDateFromUnixTime(unixTime, utcOffset, currentUtcOffset) {
if (isNumber(utcOffset)) { if (isNumber(utcOffset)) {
if (!isNumber(currentUtcOffset)) { if (!isNumber(currentUtcOffset)) {
@@ -169,6 +174,16 @@ function parseDateFromUnixTime(unixTime, utcOffset, currentUtcOffset) {
return moment.unix(unixTime); return moment.unix(unixTime);
} }
function is24HourFormat(format) {
if (format.indexOf('HH') >= 0 && format.indexOf('hh') < 0) {
return true;
} else if (format.indexOf('HH') < 0 && format.indexOf('hh') >= 0) {
return false;
}
return true;
}
function formatUnixTime(unixTime, format, utcOffset, currentUtcOffset) { function formatUnixTime(unixTime, format, utcOffset, currentUtcOffset) {
return parseDateFromUnixTime(unixTime, utcOffset, currentUtcOffset).format(format); return parseDateFromUnixTime(unixTime, utcOffset, currentUtcOffset).format(format);
} }
@@ -571,6 +586,38 @@ function getExchangedAmount(amount, fromRate, toRate) {
return amount * exchangeRate; return amount * exchangeRate;
} }
function formatPercent(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 + '%';
}
function limitText(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.substring(0, maxLength - 3) + '...';
}
function base64encode(arrayBuffer) { function base64encode(arrayBuffer) {
if (!arrayBuffer || arrayBuffer.length === 0) { if (!arrayBuffer || arrayBuffer.length === 0) {
return null; return null;
@@ -587,6 +634,48 @@ function stringToArrayBuffer(str){
return Uint8Array.from(str, c => c.charCodeAt(0)).buffer; return Uint8Array.from(str, c => c.charCodeAt(0)).buffer;
} }
function getNameByKeyValue(src, value, keyField, nameField, defaultName) {
if (isArray(src)) {
if (keyField) {
for (let i = 0; i < src.length; i++) {
const option = src[i];
if (option[keyField] === value) {
return option[nameField];
}
}
} else {
if (src[value]) {
const option = src[value];
return option[nameField];
}
}
} else if (isObject(src)) {
if (keyField) {
for (let key in src) {
if (!Object.prototype.hasOwnProperty.call(src, key)) {
continue;
}
const option = src[key];
if (option[keyField] === value) {
return option[nameField];
}
}
} else {
if (src[value]) {
const option = src[value];
return option[nameField];
}
}
}
return defaultName;
}
function generateRandomString() { function generateRandomString() {
const baseString = 'ebk_' + Math.round(new Date().getTime() / 1000) + '_' + Math.random(); const baseString = 'ebk_' + Math.round(new Date().getTime() / 1000) + '_' + Math.random();
return CryptoJS.SHA256(baseString).toString(); return CryptoJS.SHA256(baseString).toString();
@@ -612,6 +701,41 @@ function parseUserAgent(ua) {
}; };
} }
function parseDeviceInfo(ua) {
const uaInfo = parseUserAgent(ua);
let result = '';
if (uaInfo.device.model) {
result = uaInfo.device.model;
} else if (uaInfo.os.name) {
result = uaInfo.os.name;
if (uaInfo.os.version) {
result += ' ' + uaInfo.os.version;
}
}
if (uaInfo.browser.name) {
let browserInfo = uaInfo.browser.name;
if (uaInfo.browser.version) {
browserInfo += ' ' + uaInfo.browser.version;
}
if (result) {
result += ' (' + browserInfo + ')';
} else {
result = browserInfo;
}
}
if (!result) {
return 'Unknown Device';
}
return result;
}
function transactionTypeToCategroyType(transactionType) { function transactionTypeToCategroyType(transactionType) {
if (transactionType === transactionConstants.allTransactionTypes.Income) { if (transactionType === transactionConstants.allTransactionTypes.Income) {
return categoryConstants.allCategoryTypes.Income; return categoryConstants.allCategoryTypes.Income;
@@ -721,6 +845,38 @@ function getAllFilteredAccountsBalance(categorizedAccounts, accountFilter) {
return ret; return ret;
} }
function makeButtonCopyToClipboard({ text, el, successCallback, errorCallback }) {
const clipboard = new Clipboard(el, {
text: function () {
return text;
}
});
clipboard.on('success', (e) => {
if (successCallback) {
successCallback(e);
}
});
clipboard.on('error', (e) => {
if (errorCallback) {
errorCallback(e);
}
});
return clipboard;
}
function changeClipboardObjectText(clipboard, text) {
if (!clipboard) {
return;
}
clipboard.text = function () {
return text;
};
}
export default { export default {
isFunction, isFunction,
isObject, isObject,
@@ -739,7 +895,9 @@ export default {
getActualUnixTimeForStore, getActualUnixTimeForStore,
getDummyUnixTimeForLocalUsage, getDummyUnixTimeForLocalUsage,
getCurrentUnixTime, getCurrentUnixTime,
getCurrentDateTime,
parseDateFromUnixTime, parseDateFromUnixTime,
is24HourFormat,
formatUnixTime, formatUnixTime,
formatTime, formatTime,
getUnixTime, getUnixTime,
@@ -773,14 +931,20 @@ export default {
numericCurrencyToString, numericCurrencyToString,
stringCurrencyToNumeric, stringCurrencyToNumeric,
getExchangedAmount, getExchangedAmount,
formatPercent,
limitText,
base64encode, base64encode,
arrayBufferToString, arrayBufferToString,
stringToArrayBuffer, stringToArrayBuffer,
getNameByKeyValue,
generateRandomString, generateRandomString,
parseUserAgent, parseUserAgent,
parseDeviceInfo,
transactionTypeToCategroyType, transactionTypeToCategroyType,
categroyTypeToTransactionType, categroyTypeToTransactionType,
getCategoryInfo, getCategoryInfo,
getCategorizedAccounts, getCategorizedAccounts,
getAllFilteredAccountsBalance, getAllFilteredAccountsBalance,
makeButtonCopyToClipboard,
changeClipboardObjectText,
}; };
+4 -4
View File
@@ -1,15 +1,15 @@
export default { export default {
getVersion: () => { getVersion: () => {
let version = process.env.VERSION || 'unknown'; let version = __EZBOOKKEEPING_VERSION__ || 'unknown'; // eslint-disable-line
let commitHash = process.env.COMMIT_HASH; let commitHash = __EZBOOKKEEPING_BUILD_COMMIT_HASH__; // eslint-disable-line
if (commitHash) { if (commitHash) {
return `${version} (${commitHash.substr(0, Math.min(7, commitHash.length))})` return `${version} (${commitHash.substring(0, Math.min(7, commitHash.length))})`
} else { } else {
return version; return version;
} }
}, },
getBuildTime: () => { getBuildTime: () => {
return process.env.BUILD_UNIXTIME; return __EZBOOKKEEPING_BUILD_UNIX_TIME__; // eslint-disable-line
} }
}; };
+7 -5
View File
@@ -36,11 +36,6 @@ export default {
'symbol': '{symbol} {amount}' 'symbol': '{symbol} {amount}'
} }
}, },
'input-format': { // The type of date or time format is framework7 format, ref: https://v5.framework7.io/docs/calendar.html#calendar-parameters
'datetime': {
'long': 'm/d/yyyy hh::mm A',
},
},
'dataExport': { 'dataExport': {
'defaultExportFilename': 'ezBookkeeping_export_data', 'defaultExportFilename': 'ezBookkeeping_export_data',
'exportFilename': 'ezBookkeeping_{nickname}_export_data' 'exportFilename': 'ezBookkeeping_{nickname}_export_data'
@@ -658,6 +653,7 @@ export default {
'Update': 'Update', 'Update': 'Update',
'None': 'None', 'None': 'None',
'Not Specified': 'Not Specified', 'Not Specified': 'Not Specified',
'No results': 'No results',
'Done': 'Done', 'Done': 'Done',
'Continue': 'Continue', 'Continue': 'Continue',
'Status': 'Status', 'Status': 'Status',
@@ -690,6 +686,7 @@ export default {
'End Time': 'End Time', 'End Time': 'End Time',
'Select Date': 'Select Date', 'Select Date': 'Select Date',
'Select Time': 'Select Time', 'Select Time': 'Select Time',
'Current Time': 'Current Time',
'Custom': 'Custom', 'Custom': 'Custom',
'Pie Chart': 'Pie Chart', 'Pie Chart': 'Pie Chart',
'Bar Chart': 'Bar Chart', 'Bar Chart': 'Bar Chart',
@@ -865,8 +862,11 @@ export default {
'Account Total Assets': 'Account Total Assets', 'Account Total Assets': 'Account Total Assets',
'Account Total Liabilities': 'Account Total Liabilities', 'Account Total Liabilities': 'Account Total Liabilities',
'Statistics Settings': 'Statistics Settings', 'Statistics Settings': 'Statistics Settings',
'Chart Type': 'Chart Type',
'Default Chart Type': 'Default Chart Type', 'Default Chart Type': 'Default Chart Type',
'Chart Data Type': 'Chart Data Type',
'Default Chart Data Type': 'Default Chart Data Type', 'Default Chart Data Type': 'Default Chart Data Type',
'Date Range': 'Date Range',
'Default Date Range': 'Default Date Range', 'Default Date Range': 'Default Date Range',
'Default Account Filter': 'Default Account Filter', 'Default Account Filter': 'Default Account Filter',
'Default Transaction Category Filter': 'Default Transaction Category Filter', 'Default Transaction Category Filter': 'Default Transaction Category Filter',
@@ -975,6 +975,8 @@ export default {
'You have logged out all other sessions': 'You have logged out all other sessions', 'You have logged out all other sessions': 'You have logged out all other sessions',
'Unable to logout all other sessions': 'Unable to logout all other sessions', 'Unable to logout all other sessions': 'Unable to logout all other sessions',
'Regenerate Backup Codes': 'Regenerate Backup Codes', 'Regenerate Backup Codes': 'Regenerate Backup Codes',
'Enable Two-Factor Authentication': 'Enable Two-Factor Authentication',
'Disable Two-Factor Authentication': 'Disable Two-Factor Authentication',
'Please use two factor authentication app scan the below qrcode and input current passcode': 'Please use two factor authentication app scan the below qrcode and input current passcode', 'Please use two factor authentication app scan the below qrcode and input current passcode': 'Please use two factor authentication app scan the below qrcode and input current passcode',
'Please enter your current password when disable two factor authentication': 'Please enter your current password when disable two factor authentication', 'Please enter your current password when disable two factor authentication': 'Please enter your current password when disable two factor authentication',
'Please enter your current password when regenerate two factor authentication backup codes. If you regenerate backup codes, the old codes will be invalidated.': 'Please enter your current password when regenerate two factor authentication backup codes. If you regenerate backup codes, the old codes will be invalidated.', 'Please enter your current password when regenerate two factor authentication backup codes. If you regenerate backup codes, the old codes will be invalidated.': 'Please enter your current password when regenerate two factor authentication backup codes. If you regenerate backup codes, the old codes will be invalidated.',
+2
View File
@@ -1,3 +1,5 @@
import 'moment/dist/locale/zh-cn.js';
import en from './en.js' import en from './en.js'
import zhHans from './zh_Hans.js' import zhHans from './zh_Hans.js'
+7 -5
View File
@@ -36,11 +36,6 @@ export default {
'symbol': '{symbol} {amount}' 'symbol': '{symbol} {amount}'
} }
}, },
'input-format': {
'datetime': {
'long': 'yyyy年m月d日 HH::mm',
},
},
'dataExport': { 'dataExport': {
'defaultExportFilename': 'ezBookkeeping_导出数据', 'defaultExportFilename': 'ezBookkeeping_导出数据',
'exportFilename': 'ezBookkeeping_{nickname}_导出数据' 'exportFilename': 'ezBookkeeping_{nickname}_导出数据'
@@ -658,6 +653,7 @@ export default {
'Update': '更新', 'Update': '更新',
'None': '无', 'None': '无',
'Not Specified': '未指定', 'Not Specified': '未指定',
'No results': '无结果',
'Done': '完成', 'Done': '完成',
'Continue': '继续', 'Continue': '继续',
'Status': '状态', 'Status': '状态',
@@ -690,6 +686,7 @@ export default {
'End Time': '结束时间', 'End Time': '结束时间',
'Select Date': '选择日期', 'Select Date': '选择日期',
'Select Time': '选择时间', 'Select Time': '选择时间',
'Current Time': '当前时间',
'Custom': '自定义', 'Custom': '自定义',
'Pie Chart': '饼图', 'Pie Chart': '饼图',
'Bar Chart': '条形图', 'Bar Chart': '条形图',
@@ -865,8 +862,11 @@ export default {
'Account Total Assets': '账户总资产', 'Account Total Assets': '账户总资产',
'Account Total Liabilities': '账户总负债', 'Account Total Liabilities': '账户总负债',
'Statistics Settings': '统计设置', 'Statistics Settings': '统计设置',
'Chart Type': '图表类型',
'Default Chart Type': '默认图表类型', 'Default Chart Type': '默认图表类型',
'Chart Data Type': '图表数据类型',
'Default Chart Data Type': '默认图表数据类型', 'Default Chart Data Type': '默认图表数据类型',
'Date Range': '时间范围',
'Default Date Range': '默认时间范围', 'Default Date Range': '默认时间范围',
'Default Account Filter': '默认账号过滤', 'Default Account Filter': '默认账号过滤',
'Default Transaction Category Filter': '默认交易分类过滤', 'Default Transaction Category Filter': '默认交易分类过滤',
@@ -975,6 +975,8 @@ export default {
'You have logged out all other sessions': '您已经退出其他所有会话', 'You have logged out all other sessions': '您已经退出其他所有会话',
'Unable to logout all other sessions': '无法退出其他所有会话', 'Unable to logout all other sessions': '无法退出其他所有会话',
'Regenerate Backup Codes': '重新生成备用码', 'Regenerate Backup Codes': '重新生成备用码',
'Enable Two-Factor Authentication': '启用两步验证',
'Disable Two-Factor Authentication': '禁用两步验证',
'Please use two factor authentication app scan the below qrcode and input current passcode': '请使用两步验证应用扫描下方的二维码并输入当前的验证码', 'Please use two factor authentication app scan the below qrcode and input current passcode': '请使用两步验证应用扫描下方的二维码并输入当前的验证码',
'Please enter your current password when disable two factor authentication': '禁用两步验证时需要输入您的当前密码', 'Please enter your current password when disable two factor authentication': '禁用两步验证时需要输入您的当前密码',
'Please enter your current password when regenerate two factor authentication backup codes. If you regenerate backup codes, the old codes will be invalidated.': '重新生成两步验证备用码时需要输入您的当前密码。如果您重新生成备用码,之前的备用码将失效。', 'Please enter your current password when regenerate two factor authentication backup codes. If you regenerate backup codes, the old codes will be invalidated.': '重新生成两步验证备用码时需要输入您的当前密码。如果您重新生成备用码,之前的备用码将失效。',
+250 -445
View File
@@ -1,54 +1,80 @@
import Vue from 'vue'; import { createApp } from 'vue';
import Vuex from 'vuex'; import { createStore } from 'vuex';
import { createI18n } from 'vue-i18n';
import VueI18n from 'vue-i18n'; import moment from "moment-timezone";
import PincodeInput from 'vue-pincode-input';
import VueClipboard from 'vue-clipboard2';
import moment from 'moment-timezone'; import Framework7 from 'framework7/lite';
import Framework7Dialog from 'framework7/components/dialog';
import Framework7Popup from 'framework7/components/popup';
import Framework7LoginScreen from 'framework7/components/login-screen';
import Framework7Popover from 'framework7/components/popover';
import Framework7Actions from 'framework7/components/actions';
import Framework7Sheet from 'framework7/components/sheet';
import Framework7Toast from 'framework7/components/toast';
import Framework7Preloader from 'framework7/components/preloader';
import Framework7Progressbar from 'framework7/components/progressbar';
import Framework7Sortable from 'framework7/components/sortable';
import Framework7Swipeout from 'framework7/components/swipeout';
import Framework7Accordion from 'framework7/components/accordion';
import Framework7Card from 'framework7/components/card';
import Framework7Chip from 'framework7/components/chip';
import Framework7Form from 'framework7/components/form';
import Framework7Input from 'framework7/components/input';
import Framework7Checkbox from 'framework7/components/checkbox';
import Framework7Radio from 'framework7/components/radio';
import Framework7Toggle from 'framework7/components/toggle';
import Framework7SmartSelect from 'framework7/components/smart-select';
import Framework7Grid from 'framework7/components/grid';
import Framework7Picker from 'framework7/components/picker';
import Framework7InfiniteScroll from 'framework7/components/infinite-scroll';
import Framework7PullToRefresh from 'framework7/components/pull-to-refresh';
import Framework7Searchbar from 'framework7/components/searchbar';
import Framework7Tooltip from 'framework7/components/tooltip';
import Framework7Skeleton from 'framework7/components/skeleton';
import Framework7Treeview from 'framework7/components/treeview';
import Framework7Typography from 'framework7/components/typography';
import Framework7Vue, { registerComponents } from 'framework7-vue/bundle';
import Framework7 from 'framework7/framework7-lite.esm.js'; import 'framework7/css';
import Framework7Dialog from 'framework7/components/dialog/dialog'; import 'framework7/components/dialog/css';
import Framework7Popup from 'framework7/components/popup/popup'; import 'framework7/components/popup/css';
import Framework7LoginScreen from 'framework7/components/login-screen/login-screen'; import 'framework7/components/login-screen/css';
import Framework7Popover from 'framework7/components/popover/popover'; import 'framework7/components/popover/css';
import Framework7Actions from 'framework7/components/actions/actions'; import 'framework7/components/actions/css';
import Framework7Sheet from 'framework7/components/sheet/sheet'; import 'framework7/components/sheet/css';
import Framework7Toast from 'framework7/components/toast/toast'; import 'framework7/components/toast/css';
import Framework7Preloader from 'framework7/components/preloader/preloader'; import 'framework7/components/preloader/css';
import Framework7Progressbar from 'framework7/components/progressbar/progressbar'; import 'framework7/components/progressbar/css';
import Framework7Sortable from 'framework7/components/sortable/sortable'; import 'framework7/components/sortable/css';
import Framework7Swipeout from 'framework7/components/swipeout/swipeout'; import 'framework7/components/swipeout/css';
import Framework7Accordion from 'framework7/components/accordion/accordion'; import 'framework7/components/accordion/css';
import Framework7Card from 'framework7/components/card/card'; import 'framework7/components/card/css';
import Framework7Chip from 'framework7/components/chip/chip'; import 'framework7/components/chip/css';
import Framework7Form from 'framework7/components/form/form'; import 'framework7/components/form/css';
import Framework7Input from 'framework7/components/input/input'; import 'framework7/components/input/css';
import Framework7Checkbox from 'framework7/components/checkbox/checkbox'; import 'framework7/components/checkbox/css';
import Framework7Radio from 'framework7/components/radio/radio'; import 'framework7/components/radio/css';
import Framework7Toggle from 'framework7/components/toggle/toggle'; import 'framework7/components/toggle/css';
import Framework7SmartSelect from 'framework7/components/smart-select/smart-select'; import 'framework7/components/smart-select/css';
import Framework7Grid from 'framework7/components/grid/grid'; import 'framework7/components/grid/css';
import Framework7Calendar from 'framework7/components/calendar/calendar'; import 'framework7/components/picker/css';
import Framework7Picker from 'framework7/components/picker/picker'; import 'framework7/components/infinite-scroll/css';
import Framework7InfiniteScroll from 'framework7/components/infinite-scroll/infinite-scroll'; import 'framework7/components/pull-to-refresh/css';
import Framework7PullToRefresh from 'framework7/components/pull-to-refresh/pull-to-refresh'; import 'framework7/components/searchbar/css';
import Framework7Searchbar from 'framework7/components/searchbar/searchbar'; import 'framework7/components/tooltip/css';
import Framework7Tooltip from 'framework7/components/tooltip/tooltip'; import 'framework7/components/skeleton/css';
import Framework7Skeleton from 'framework7/components/skeleton/skeleton'; import 'framework7/components/treeview/css';
import Framework7Menu from 'framework7/components/menu/menu'; import 'framework7/components/typography/css';
import Framework7Treeview from 'framework7/components/treeview/treeview';
import Framework7Typography from 'framework7/components/typography/typography';
import Framework7Vue from 'framework7-vue/framework7-vue.esm.bundle.js';
import 'framework7/css/framework7.bundle.css';
import 'framework7-icons'; import 'framework7-icons';
import 'line-awesome/dist/line-awesome/css/line-awesome.css'; import 'line-awesome/dist/line-awesome/css/line-awesome.css';
import VueDatePicker from '@vuepic/vue-datepicker';
import '@vuepic/vue-datepicker/dist/main.css';
import api from './consts/api.js'; import api from './consts/api.js';
import datetime from './consts/datetime.js'; import datetime from './consts/datetime.js';
import timezone from './consts/timezone.js';
import currency from './consts/currency.js'; import currency from './consts/currency.js';
import colors from './consts/color.js'; import colors from './consts/color.js';
import icons from './consts/icon.js'; import icons from './consts/icon.js';
@@ -65,35 +91,42 @@ import services from './lib/services.js';
import userstate from './lib/userstate.js'; import userstate from './lib/userstate.js';
import webauthn from './lib/webauthn.js'; import webauthn from './lib/webauthn.js';
import utils from './lib/utils.js'; import utils from './lib/utils.js';
import { getAllLanguages, getLanguage, getDefaultLanguage, getI18nOptions, getLocalizedError, getLocalizedErrorParameters } from './lib/i18n.js'; import {
getAllLanguageInfos,
getLanguageInfo,
getDefaultLanguage,
transateIf,
getAllLongMonthNames,
getAllShortMonthNames,
getAllLongWeekdayNames,
getAllShortWeekdayNames,
getAllMinWeekdayNames,
getAllTimezones,
getAllCurrencies,
getDisplayCurrency,
getI18nOptions,
} from './lib/i18n.js';
import {
showAlert,
showConfirm,
showToast,
showLoading,
hideLoading,
routeBackOnError,
elements,
isModalShowing,
onSwipeoutDeleted
} from './lib/mobile/ui.js';
import stores from './store/index.js'; import stores from './store/index.js';
import localizedFilter from './filters/localized.js'; import ItemIcon from './components/mobile/ItemIcon.vue';
import momentFilter from './filters/moment.js';
import percentFilter from './filters/percent.js';
import formatFilter from './filters/format.js';
import optionNameFilter from './filters/optionName.js';
import itemFieldContentFilter from './filters/itemFieldContent.js';
import languageNameFilter from './filters/languageName.js';
import currencyFilter from './filters/currency.js';
import exchangeRateFilter from './filters/exchangeRate.js';
import utcOffsetFilter from './filters/utcOffset.js';
import textLimitFilter from './filters/textLimit.js';
import iconFilter from './filters/icon.js';
import iconStyleFilter from './filters/iconStyle.js';
import defaultIconColorFilter from './filters/defaultIconColor.js';
import accountIconFilter from './filters/accountIcon.js';
import accountIconStyleFilter from './filters/accountIconStyle.js';
import categoryIconFilter from './filters/categoryIcon.js';
import categoryIconStyleFilter from './filters/categoryIconStyle.js';
import tokenDeviceFilter from './filters/tokenDevice.js';
import tokenIconFilter from './filters/tokenIcon.js';
import PieChart from './components/mobile/PieChart.vue'; import PieChart from './components/mobile/PieChart.vue';
import PinCodeInput from './components/mobile/PinCodeInput.vue';
import PinCodeInputSheet from './components/mobile/PinCodeInputSheet.vue';
import PasswordInputSheet from './components/mobile/PasswordInputSheet.vue'; import PasswordInputSheet from './components/mobile/PasswordInputSheet.vue';
import PasscodeInputSheet from './components/mobile/PasscodeInputSheet.vue'; import PasscodeInputSheet from './components/mobile/PasscodeInputSheet.vue';
import PinCodeInputSheet from './components/mobile/PinCodeInputSheet.vue'; import DateTimeSelectionSheet from './components/mobile/DateTimeSelectionSheet.vue';
import DateRangeSelectionSheet from './components/mobile/DateRangeSelectionSheet.vue'; import DateRangeSelectionSheet from './components/mobile/DateRangeSelectionSheet.vue';
import ListItemSelectionSheet from './components/mobile/ListItemSelectionSheet.vue'; import ListItemSelectionSheet from './components/mobile/ListItemSelectionSheet.vue';
import TwoColumnListItemSelectionSheet from './components/mobile/TwoColumnListItemSelectionSheet.vue'; import TwoColumnListItemSelectionSheet from './components/mobile/TwoColumnListItemSelectionSheet.vue';
@@ -104,93 +137,136 @@ import InformationSheet from './components/mobile/InformationSheet.vue';
import NumberPadSheet from './components/mobile/NumberPadSheet.vue'; import NumberPadSheet from './components/mobile/NumberPadSheet.vue';
import TransactionTagSelectionSheet from './components/mobile/TransactionTagSelectionSheet.vue'; import TransactionTagSelectionSheet from './components/mobile/TransactionTagSelectionSheet.vue';
import TextareaAutoSize from "./directives/mobile/textareaAutoSize.js";
import App from './Mobile.vue'; import App from './Mobile.vue';
Framework7.use(Framework7Dialog); Framework7.use([
Framework7.use(Framework7Popup); Framework7Dialog,
Framework7.use(Framework7LoginScreen); Framework7Popup,
Framework7.use(Framework7Popover); Framework7LoginScreen,
Framework7.use(Framework7Actions); Framework7Popover,
Framework7.use(Framework7Sheet); Framework7Actions,
Framework7.use(Framework7Toast); Framework7Sheet,
Framework7.use(Framework7Preloader); Framework7Toast,
Framework7.use(Framework7Progressbar); Framework7Preloader,
Framework7.use(Framework7Sortable); Framework7Progressbar,
Framework7.use(Framework7Swipeout); Framework7Sortable,
Framework7.use(Framework7Accordion); Framework7Swipeout,
Framework7.use(Framework7Card); Framework7Accordion,
Framework7.use(Framework7Chip); Framework7Card,
Framework7.use(Framework7Form); Framework7Chip,
Framework7.use(Framework7Input); Framework7Form,
Framework7.use(Framework7Checkbox); Framework7Input,
Framework7.use(Framework7Radio); Framework7Checkbox,
Framework7.use(Framework7Toggle); Framework7Radio,
Framework7.use(Framework7SmartSelect); Framework7Toggle,
Framework7.use(Framework7Grid); Framework7SmartSelect,
Framework7.use(Framework7Calendar); Framework7Grid,
Framework7.use(Framework7Picker); Framework7Picker,
Framework7.use(Framework7InfiniteScroll); Framework7InfiniteScroll,
Framework7.use(Framework7PullToRefresh); Framework7PullToRefresh,
Framework7.use(Framework7Searchbar); Framework7Searchbar,
Framework7.use(Framework7Tooltip); Framework7Tooltip,
Framework7.use(Framework7Skeleton); Framework7Skeleton,
Framework7.use(Framework7Menu); Framework7Treeview,
Framework7.use(Framework7Treeview); Framework7Typography,
Framework7.use(Framework7Typography); Framework7Vue
Framework7.use(Framework7Vue); ]);
Vue.use(Vuex); const app = createApp(App);
Vue.use(VueI18n); const store = createStore(stores);
Vue.use(VueClipboard); const i18n = createI18n(getI18nOptions());
registerComponents(app);
app.use(store);
app.use(i18n);
Vue.component('PincodeInput', PincodeInput); function setLanguage(locale) {
Vue.component('PieChart', PieChart); if (settings.getLanguage() !== locale) {
Vue.component('PasswordInputSheet', PasswordInputSheet); settings.setLanguage(locale);
Vue.component('PasscodeInputSheet', PasscodeInputSheet); }
Vue.component('PinCodeInputSheet', PinCodeInputSheet);
Vue.component('DateRangeSelectionSheet', DateRangeSelectionSheet);
Vue.component('ListItemSelectionSheet', ListItemSelectionSheet);
Vue.component('TwoColumnListItemSelectionSheet', TwoColumnListItemSelectionSheet);
Vue.component('TreeViewSelectionSheet', TreeViewSelectionSheet);
Vue.component('IconSelectionSheet', IconSelectionSheet);
Vue.component('ColorSelectionSheet', ColorSelectionSheet);
Vue.component('InformationSheet', InformationSheet);
Vue.component('NumberPadSheet', NumberPadSheet);
Vue.component('TransactionTagSelectionSheet', TransactionTagSelectionSheet);
Vue.filter('localized', (value, options) => localizedFilter({ i18n }, value, options)); i18n.global.locale.value = locale;
Vue.filter('moment', (value, format, options) => momentFilter(value, format, options)); moment.locale(locale, {
Vue.filter('percent', (value, precision, lowPrecisionValue) => percentFilter(value, precision, lowPrecisionValue)); months : app.config.globalProperties.$locale.getAllLongMonthNames(),
Vue.filter('format', (value, format) => formatFilter(value, format)); monthsShort : app.config.globalProperties.$locale.getAllShortMonthNames(),
Vue.filter('optionName', (value, options, keyField, nameField, defaultName) => optionNameFilter(value, options, keyField, nameField, defaultName)); weekdays : app.config.globalProperties.$locale.getAllLongWeekdayNames(),
Vue.filter('itemFieldContent', (value, fieldName, defaultValue, translate) => itemFieldContentFilter({ i18n }, value, fieldName, defaultValue, translate)); weekdaysShort : app.config.globalProperties.$locale.getAllShortWeekdayNames(),
Vue.filter('languageName', (languageCode) => languageNameFilter(languageCode)); weekdaysMin : app.config.globalProperties.$locale.getAllMinWeekdayNames(),
Vue.filter('currency', (value, currencyCode, notConvertValue) => currencyFilter({ i18n }, value, currencyCode, notConvertValue)); });
Vue.filter('exchangeRate', (value, currentCurrency, allExchangeRates) => exchangeRateFilter(value, currentCurrency, allExchangeRates)); services.setLocale(locale);
Vue.filter('utcOffset', (value) => utcOffsetFilter(value)); document.querySelector('html').setAttribute('lang', locale);
Vue.filter('textLimit', (value, maxLength) => textLimitFilter(value, maxLength));
Vue.filter('icon', (value, iconType) => iconFilter(value, iconType));
Vue.filter('iconStyle', (value, iconType, defaultColor, additionalFieldName) => iconStyleFilter(value, iconType, defaultColor, additionalFieldName));
Vue.filter('defaultIconColor', (value, defaultColor) => defaultIconColorFilter(value, defaultColor));
Vue.filter('accountIcon', (value) => accountIconFilter(value));
Vue.filter('accountIconStyle', (value, defaultColor, additionalFieldName) => accountIconStyleFilter(value, defaultColor, additionalFieldName));
Vue.filter('categoryIcon', (value) => categoryIconFilter(value));
Vue.filter('categoryIconStyle', (value, defaultColor, additionalFieldName) => categoryIconStyleFilter(value, defaultColor, additionalFieldName));
Vue.filter('tokenDevice', (value) => tokenDeviceFilter(value));
Vue.filter('tokenIcon', (value) => tokenIconFilter(value));
const store = new Vuex.Store(stores); const defaultCurrency = i18n.global.t('default.currency');
const i18n = new VueI18n(getI18nOptions()); const defaultFirstDayOfWeekName = i18n.global.t('default.firstDayOfWeek');
let defaultFirstDayOfWeek = datetime.defaultFirstDayOfWeek;
Vue.prototype.$version = version.getVersion(); if (datetime.allWeekDays[defaultFirstDayOfWeekName]) {
Vue.prototype.$buildTime = version.getBuildTime(); defaultFirstDayOfWeek = datetime.allWeekDays[defaultFirstDayOfWeekName].type;
}
Vue.prototype.$licenses = { store.dispatch('updateLocalizedDefaultSettings', { defaultCurrency, defaultFirstDayOfWeek });
return locale;
}
function setTimezone(timezone) {
if (timezone) {
settings.setTimezone(timezone);
moment.tz.setDefault(timezone);
} else {
settings.setTimezone('');
moment.tz.setDefault();
}
}
function initLocale() {
if (settings.getLanguage()) {
logger.info(`Current language is ${settings.getLanguage()}`);
setLanguage(settings.getLanguage());
} else {
logger.info(`No language is set, use browser default ${getDefaultLanguage()}`);
setLanguage(getDefaultLanguage());
}
if (settings.getTimezone()) {
logger.info(`Current timezone is ${settings.getTimezone()}`);
setTimezone(settings.getTimezone());
} else {
logger.info(`No timezone is set, use browser default ${utils.getTimezoneOffset()} (maybe ${moment.tz.guess(true)})`);
}
}
app.component('VueDatePicker', VueDatePicker);
app.component('ItemIcon', ItemIcon);
app.component('PieChart', PieChart);
app.component('PinCodeInput', PinCodeInput);
app.component('PinCodeInputSheet', PinCodeInputSheet);
app.component('PasswordInputSheet', PasswordInputSheet);
app.component('PasscodeInputSheet', PasscodeInputSheet);
app.component('DateTimeSelectionSheet', DateTimeSelectionSheet);
app.component('DateRangeSelectionSheet', DateRangeSelectionSheet);
app.component('ListItemSelectionSheet', ListItemSelectionSheet);
app.component('TwoColumnListItemSelectionSheet', TwoColumnListItemSelectionSheet);
app.component('TreeViewSelectionSheet', TreeViewSelectionSheet);
app.component('IconSelectionSheet', IconSelectionSheet);
app.component('ColorSelectionSheet', ColorSelectionSheet);
app.component('InformationSheet', InformationSheet);
app.component('NumberPadSheet', NumberPadSheet);
app.component('TransactionTagSelectionSheet', TransactionTagSelectionSheet);
app.directive('TextareaAutoSize', TextareaAutoSize);
app.config.globalProperties.$version = version.getVersion();
app.config.globalProperties.$buildTime = version.getBuildTime();
app.config.globalProperties.$licenses = {
license: licenses.getLicense(), license: licenses.getLicense(),
thirdPartyLicenses: licenses.getThirdPartyLicenses() thirdPartyLicenses: licenses.getThirdPartyLicenses()
}; };
Vue.prototype.$constants = { app.config.globalProperties.$constants = {
api: api, api: api,
datetime: datetime, datetime: datetime,
currency: currency, currency: currency,
@@ -202,314 +278,43 @@ Vue.prototype.$constants = {
statistics: statistics, statistics: statistics,
}; };
Vue.prototype.$utilities = utils; app.config.globalProperties.$utilities = utils;
Vue.prototype.$logger = logger; app.config.globalProperties.$logger = logger;
Vue.prototype.$webauthn = webauthn; app.config.globalProperties.$webauthn = webauthn;
Vue.prototype.$settings = settings; app.config.globalProperties.$settings = settings;
Vue.prototype.$locale = { app.config.globalProperties.$locale = {
defaultTimezoneOffset: utils.getTimezoneOffset(),
defaultTimezoneOffsetMinutes: utils.getTimezoneOffsetMinutes(),
getDefaultLanguage: getDefaultLanguage, getDefaultLanguage: getDefaultLanguage,
getAllLanguages: getAllLanguages, getAllLanguageInfos: getAllLanguageInfos,
getAllLongMonthNames: function () { getLanguageInfo: getLanguageInfo,
return [ getAllLongMonthNames: () => getAllLongMonthNames(i18n.global.t),
i18n.t('datetime.January.long'), getAllShortMonthNames: () => getAllShortMonthNames(i18n.global.t),
i18n.t('datetime.February.long'), getAllLongWeekdayNames: () => getAllLongWeekdayNames(i18n.global.t),
i18n.t('datetime.March.long'), getAllShortWeekdayNames: () => getAllShortWeekdayNames(i18n.global.t),
i18n.t('datetime.April.long'), getAllMinWeekdayNames: () => getAllMinWeekdayNames(i18n.global.t),
i18n.t('datetime.May.long'), setLanguage: setLanguage,
i18n.t('datetime.June.long'), getTimezone: settings.getTimezone,
i18n.t('datetime.July.long'), setTimezone: setTimezone,
i18n.t('datetime.August.long'), getAllTimezones: (includeSystemDefault) => getAllTimezones(includeSystemDefault, i18n.global.t),
i18n.t('datetime.September.long'), getAllCurrencies: () => getAllCurrencies(i18n.global.t),
i18n.t('datetime.October.long'), getDisplayCurrency: (value, currencyCode, notConvertValue) => getDisplayCurrency(value, currencyCode, notConvertValue, i18n.global.t),
i18n.t('datetime.November.long'), initLocale: initLocale
i18n.t('datetime.December.long') };
]; app.config.globalProperties.$tIf = (text, isTranslate) => transateIf(text, isTranslate, i18n.global.t);
},
getAllShortMonthNames: function () {
return [
i18n.t('datetime.January.short'),
i18n.t('datetime.February.short'),
i18n.t('datetime.March.short'),
i18n.t('datetime.April.short'),
i18n.t('datetime.May.short'),
i18n.t('datetime.June.short'),
i18n.t('datetime.July.short'),
i18n.t('datetime.August.short'),
i18n.t('datetime.September.short'),
i18n.t('datetime.October.short'),
i18n.t('datetime.November.short'),
i18n.t('datetime.December.short')
];
},
getAllLongWeekdayNames: function () {
return [
i18n.t('datetime.Sunday.long'),
i18n.t('datetime.Monday.long'),
i18n.t('datetime.Tuesday.long'),
i18n.t('datetime.Wednesday.long'),
i18n.t('datetime.Thursday.long'),
i18n.t('datetime.Friday.long'),
i18n.t('datetime.Saturday.long')
];
},
getAllShortWeekdayNames: function () {
return [
i18n.t('datetime.Sunday.short'),
i18n.t('datetime.Monday.short'),
i18n.t('datetime.Tuesday.short'),
i18n.t('datetime.Wednesday.short'),
i18n.t('datetime.Thursday.short'),
i18n.t('datetime.Friday.short'),
i18n.t('datetime.Saturday.short')
];
},
getAllMinWeekdayNames: function () {
return [
i18n.t('datetime.Sunday.min'),
i18n.t('datetime.Monday.min'),
i18n.t('datetime.Tuesday.min'),
i18n.t('datetime.Wednesday.min'),
i18n.t('datetime.Thursday.min'),
i18n.t('datetime.Friday.min'),
i18n.t('datetime.Saturday.min')
];
},
getInputTimeIntlDateTimeFormatOptions: function () {
const hourMinuteFormat = i18n.t('input-format.datetime.long');
const is24HourFormat = hourMinuteFormat.indexOf('H') > 0;
const hour2Digits = (hourMinuteFormat.indexOf('HH') > 0) || (hourMinuteFormat.indexOf('hh') > 0);
const minute2Digits = hourMinuteFormat.indexOf(':mm') > 0;
return { app.config.globalProperties.$alert = (message, confirmCallback) => showAlert(message, confirmCallback, i18n.global.t);
hour12: !is24HourFormat, app.config.globalProperties.$confirm = (message, confirmCallback, cancelCallback) => showConfirm(message, confirmCallback, cancelCallback, i18n.global.t);
hour: hour2Digits ? '2-digit' : 'numeric', app.config.globalProperties.$toast = (message, timeout) => showToast(message, timeout, i18n.global.t);
minute: minute2Digits ? '2-digit' : 'numeric' app.config.globalProperties.$showLoading = showLoading;
} app.config.globalProperties.$hideLoading = hideLoading;
}, app.config.globalProperties.$routeBackOnError = routeBackOnError;
getLanguage: getLanguage, app.config.globalProperties.$ui = {
setLanguage: function (locale) { elements: elements,
if (settings.getLanguage() !== locale) { isModalShowing: isModalShowing,
settings.setLanguage(locale); onSwipeoutDeleted: onSwipeoutDeleted
}
i18n.locale = locale;
moment.locale(locale, {
months : this.getAllLongMonthNames(),
monthsShort : this.getAllShortMonthNames(),
weekdays : this.getAllLongWeekdayNames(),
weekdaysShort : this.getAllShortWeekdayNames(),
weekdaysMin : this.getAllMinWeekdayNames(),
});
services.setLocale(locale);
document.querySelector('html').setAttribute('lang', locale);
const defaultCurrency = i18n.t('default.currency');
const defaultFirstDayOfWeekName = i18n.t('default.firstDayOfWeek');
let defaultFirstDayOfWeek = datetime.defaultFirstDayOfWeek;
if (datetime.allWeekDays[defaultFirstDayOfWeekName]) {
defaultFirstDayOfWeek = datetime.allWeekDays[defaultFirstDayOfWeekName].type;
}
store.dispatch('updateLocalizedDefaultSettings', { defaultCurrency, defaultFirstDayOfWeek });
return locale;
},
getTimezone: function () {
return settings.getTimezone();
},
setTimezone: function (timezone) {
if (timezone) {
settings.setTimezone(timezone);
moment.tz.setDefault(timezone);
} else {
settings.setTimezone('');
moment.tz.setDefault();
}
},
getAllTimezones: function (includeSystemDefault) {
const allTimezones = timezone.all;
const allTimezoneInfos = [];
for (let i = 0; i < allTimezones.length; i++) {
allTimezoneInfos.push({
name: allTimezones[i].timezoneName,
utcOffset: (allTimezones[i].timezoneName !== 'Etc/GMT' ? utils.getTimezoneOffset(allTimezones[i].timezoneName) : ''),
utcOffsetMinutes: utils.getTimezoneOffsetMinutes(allTimezones[i].timezoneName),
displayName: i18n.t(`timezone.${allTimezones[i].displayName}`)
});
}
if (includeSystemDefault) {
allTimezoneInfos.push({
name: '',
utcOffset: this.defaultTimezoneOffset,
utcOffsetMinutes: this.defaultTimezoneOffsetMinutes,
displayName: i18n.t('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;
},
getAllCurrencies: function () {
const allCurrencyCodes = currency.all;
const allCurrencies = [];
for (let currencyCode in allCurrencyCodes) {
if (!Object.prototype.hasOwnProperty.call(allCurrencyCodes, currencyCode)) {
return;
}
allCurrencies.push({
code: currencyCode,
displayName: i18n.t(`currency.${currencyCode}`)
});
}
allCurrencies.sort(function(c1, c2){
return c1.displayName.localeCompare(c2.displayName);
})
return allCurrencies;
},
init: function () {
if (settings.getLanguage()) {
logger.info(`Current language is ${settings.getLanguage()}`);
this.setLanguage(settings.getLanguage());
} else {
logger.info(`No language is set, use browser default ${getDefaultLanguage()}`);
this.setLanguage(getDefaultLanguage());
}
if (settings.getTimezone()) {
logger.info(`Current timezone is ${settings.getTimezone()}`);
this.setTimezone(settings.getTimezone());
} else {
logger.info(`No timezone is set, use browser default ${utils.getTimezoneOffset()} (maybe ${moment.tz.guess(true)})`);
}
}
}; };
Vue.prototype.$alert = function (message, confirmCallback) { app.config.globalProperties.$user = userstate;
let parameters = {};
if (message && message.error) { app.config.globalProperties.$locale.initLocale();
const localizedError = getLocalizedError(message.error);
message = localizedError.message;
parameters = getLocalizedErrorParameters(localizedError.parameters, s => i18n.t(s));
}
this.$f7.dialog.create({ app.mount('#app');
title: i18n.t('global.app.title'),
text: i18n.t(message, parameters),
animate: settings.isEnableAnimate(),
buttons: [
{
text: i18n.t('OK'),
onClick: confirmCallback
}
]
}).open();
};
Vue.prototype.$confirm = function (message, confirmCallback, cancelCallback) {
this.$f7.dialog.create({
title: i18n.t('global.app.title'),
text: i18n.t(message),
animate: settings.isEnableAnimate(),
buttons: [
{
text: i18n.t('Cancel'),
onClick: cancelCallback
},
{
text: i18n.t('OK'),
onClick: confirmCallback
}
]
}).open();
};
Vue.prototype.$toast = function (message, timeout) {
let parameters = {};
if (message && message.error) {
const localizedError = getLocalizedError(message.error);
message = localizedError.message;
parameters = getLocalizedErrorParameters(localizedError.parameters, s => i18n.t(s));
}
this.$f7.toast.create({
text: i18n.t(message, parameters),
position: 'center',
closeTimeout: timeout || 1500
}).open();
};
Vue.prototype.$showLoading = function (delayConditionFunc, delayMills) {
if (!delayConditionFunc) {
return this.$f7.preloader.show();
}
setTimeout(() => {
if (delayConditionFunc()) {
this.$f7.preloader.show();
}
}, delayMills || 200);
};
Vue.prototype.$hideLoading = function () {
return this.$f7.preloader.hide();
};
Vue.prototype.$routeBackOnError = function (errorPropertyName) {
const self = this;
const router = self.$f7router;
const unwatch = self.$watch(errorPropertyName, () => {
if (self[errorPropertyName]) {
setTimeout(() => {
if (unwatch) {
unwatch();
}
router.back();
}, 200);
}
}, {
immediate: true
});
};
Vue.prototype.$user = userstate;
Vue.prototype.$locale.init();
new Vue({
el: '#app',
i18n: i18n,
store: store,
render: h => h(App),
mounted: function () {
const app = this.$f7;
const $$ = app.$;
app.on('pageBeforeOut', () => {
if ($$('.modal-in').length) {
app.actions.close('.actions-modal.modal-in', false);
app.dialog.close('.dialog.modal-in', false);
app.popover.close('.popover.modal-in', false);
app.popup.close('.popup.modal-in', false);
app.sheet.close('.sheet-modal.modal-in', false);
}
});
}
});
+7 -5
View File
@@ -9,7 +9,7 @@
<meta name="apple-mobile-web-app-capable" content="yes"/> <meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-title" content="ezBookkeeping"/> <meta name="apple-mobile-web-app-title" content="ezBookkeeping"/>
<meta name="apple-mobile-web-app-status-bar-style" content="default"/> <meta name="apple-mobile-web-app-status-bar-style" content="default"/>
<meta name="theme-color" content="#c67e48"> <meta name="theme-color" content="#f6f6f8">
<meta name="format-detection" content="telephone=no"/> <meta name="format-detection" content="telephone=no"/>
<meta name="description" content="ezBookkeeping is a lightweight personal bookkeeping app hosted by yourself."> <meta name="description" content="ezBookkeeping is a lightweight personal bookkeeping app hosted by yourself.">
<title>ezBookkeeping</title> <title>ezBookkeeping</title>
@@ -52,9 +52,11 @@
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
</head> </head>
<body> <body>
<noscript> <noscript>
<strong>We're sorry but ezBookkeeping doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> <strong>We're sorry but ezBookkeeping doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript> </noscript>
<div id="app"></div> <div id="app"></div>
<script type="module" src="./mobile-main.js"></script>
</body> </body>
</html> </html>
+65 -71
View File
@@ -33,14 +33,12 @@ import CategoryPresetPage from '../views/mobile/categories/Preset.vue';
import TagListPage from '../views/mobile/tags/List.vue'; import TagListPage from '../views/mobile/tags/List.vue';
function checkLogin(to, from, resolve, reject) { function checkLogin({ router, resolve, reject }) {
const router = this;
if (!userState.isUserLogined()) { if (!userState.isUserLogined()) {
reject(); reject();
router.navigate('/login', { router.navigate('/login', {
clearPreviousHistory: true, clearPreviousHistory: true,
pushState: false browserHistory: false
}); });
return; return;
} }
@@ -49,7 +47,7 @@ function checkLogin(to, from, resolve, reject) {
reject(); reject();
router.navigate('/unlock', { router.navigate('/unlock', {
clearPreviousHistory: true, clearPreviousHistory: true,
pushState: false browserHistory: false
}); });
return; return;
} }
@@ -57,14 +55,12 @@ function checkLogin(to, from, resolve, reject) {
resolve(); resolve();
} }
function checkLocked(to, from, resolve, reject) { function checkLocked({ router, resolve, reject }) {
const router = this;
if (!userState.isUserLogined()) { if (!userState.isUserLogined()) {
reject(); reject();
router.navigate('/login', { router.navigate('/login', {
clearPreviousHistory: true, clearPreviousHistory: true,
pushState: false browserHistory: false
}); });
return; return;
} }
@@ -73,7 +69,7 @@ function checkLocked(to, from, resolve, reject) {
reject(); reject();
router.navigate('/', { router.navigate('/', {
clearPreviousHistory: true, clearPreviousHistory: true,
pushState: false browserHistory: false
}); });
return; return;
} }
@@ -81,9 +77,7 @@ function checkLocked(to, from, resolve, reject) {
resolve(); resolve();
} }
function checkNotLogin(to, from, resolve, reject) { function checkNotLogin({ router, resolve, reject }) {
const router = this;
if (userState.isUserLogined() && !userState.isUserUnlocked()) { if (userState.isUserLogined() && !userState.isUserUnlocked()) {
reject(); reject();
router.navigate('/unlock', { router.navigate('/unlock', {
@@ -108,160 +102,160 @@ function checkNotLogin(to, from, resolve, reject) {
const routes = [ const routes = [
{ {
path: '/', path: '/',
component: HomePage, async: ({resolve}) => resolve({component: HomePage}),
beforeEnter: checkLogin, beforeEnter: [checkLogin],
options: { options: {
animate: false, animate: false,
} }
}, },
{ {
path: '/login', path: '/login',
component: LoginPage, async: ({resolve}) => resolve({component: LoginPage}),
beforeEnter: checkNotLogin, beforeEnter: [checkNotLogin],
options: { options: {
animate: false, animate: false,
} }
}, },
{ {
path: '/signup', path: '/signup',
component: SignUpPage, async: ({resolve}) => resolve({component: SignUpPage}),
beforeEnter: checkNotLogin, beforeEnter: [checkNotLogin],
options: { options: {
animate: false, animate: false,
} }
}, },
{ {
path: '/unlock', path: '/unlock',
component: UnlockPage, async: ({resolve}) => resolve({component: UnlockPage}),
beforeEnter: checkLocked, beforeEnter: [checkLocked],
options: { options: {
animate: false, animate: false,
} }
}, },
{ {
path: '/transaction/list', path: '/transaction/list',
component: TransactionListPage, async: ({resolve}) => resolve({component: TransactionListPage}),
beforeEnter: checkLogin beforeEnter: [checkLogin]
}, },
{ {
path: '/transaction/add', path: '/transaction/add',
component: TransactionEditPage, async: ({resolve}) => resolve({component: TransactionEditPage}),
beforeEnter: checkLogin beforeEnter: [checkLogin]
}, },
{ {
path: '/transaction/edit', path: '/transaction/edit',
component: TransactionEditPage, async: ({resolve}) => resolve({component: TransactionEditPage}),
beforeEnter: checkLogin beforeEnter: [checkLogin]
}, },
{ {
path: '/transaction/detail', path: '/transaction/detail',
component: TransactionEditPage, async: ({resolve}) => resolve({component: TransactionEditPage}),
beforeEnter: checkLogin beforeEnter: [checkLogin]
}, },
{ {
path: '/account/list', path: '/account/list',
component: AccountListPage, async: ({resolve}) => resolve({component: AccountListPage}),
beforeEnter: checkLogin beforeEnter: [checkLogin]
}, },
{ {
path: '/account/add', path: '/account/add',
component: AccountEditPage, async: ({resolve}) => resolve({component: AccountEditPage}),
beforeEnter: checkLogin beforeEnter: [checkLogin]
}, },
{ {
path: '/account/edit', path: '/account/edit',
component: AccountEditPage, async: ({resolve}) => resolve({component: AccountEditPage}),
beforeEnter: checkLogin beforeEnter: [checkLogin]
}, },
{ {
path: '/statistic/transaction', path: '/statistic/transaction',
component: StatisticsTransactionPage, async: ({resolve}) => resolve({component: StatisticsTransactionPage}),
beforeEnter: checkLogin beforeEnter: [checkLogin]
}, },
{ {
path: '/statistic/settings', path: '/statistic/settings',
component: StatisticsSettingsPage, async: ({resolve}) => resolve({component: StatisticsSettingsPage}),
beforeEnter: checkLogin beforeEnter: [checkLogin]
}, },
{ {
path: '/statistic/filter/account', path: '/statistic/filter/account',
component: StatisticsAccountFilterSettingsPage, async: ({resolve}) => resolve({component: StatisticsAccountFilterSettingsPage}),
beforeEnter: checkLogin beforeEnter: [checkLogin]
}, },
{ {
path: '/statistic/filter/category', path: '/statistic/filter/category',
component: StatisticsCategoryFilterSettingsPage, async: ({resolve}) => resolve({component: StatisticsCategoryFilterSettingsPage}),
beforeEnter: checkLogin beforeEnter: [checkLogin]
}, },
{ {
path: '/settings', path: '/settings',
component: SettingsPage, async: ({resolve}) => resolve({component: SettingsPage}),
beforeEnter: checkLogin beforeEnter: [checkLogin]
}, },
{ {
path: '/app_lock', path: '/app_lock',
component: ApplicationLockPage, async: ({resolve}) => resolve({component: ApplicationLockPage}),
beforeEnter: checkLogin beforeEnter: [checkLogin]
}, },
{ {
path: '/exchange_rates', path: '/exchange_rates',
component: ExchangeRatesPage, async: ({resolve}) => resolve({component: ExchangeRatesPage}),
beforeEnter: checkLogin beforeEnter: [checkLogin]
}, },
{ {
path: '/about', path: '/about',
component: AboutPage, async: ({resolve}) => resolve({component: AboutPage}),
beforeEnter: checkLogin beforeEnter: [checkLogin]
}, },
{ {
path: '/user/profile', path: '/user/profile',
component: UserProfilePage, async: ({resolve}) => resolve({component: UserProfilePage}),
beforeEnter: checkLogin beforeEnter: [checkLogin]
}, },
{ {
path: '/user/data/management', path: '/user/data/management',
component: DataManagementPage, async: ({resolve}) => resolve({component: DataManagementPage}),
beforeEnter: checkLogin beforeEnter: [checkLogin]
}, },
{ {
path: '/user/2fa', path: '/user/2fa',
component: TwoFactorAuthPage, async: ({resolve}) => resolve({component: TwoFactorAuthPage}),
beforeEnter: checkLogin beforeEnter: [checkLogin]
}, },
{ {
path: '/user/sessions', path: '/user/sessions',
component: SessionListPage, async: ({resolve}) => resolve({component: SessionListPage}),
beforeEnter: checkLogin beforeEnter: [checkLogin]
}, },
{ {
path: '/category/all', path: '/category/all',
component: CategoryAllPage, async: ({resolve}) => resolve({component: CategoryAllPage}),
beforeEnter: checkLogin beforeEnter: [checkLogin]
}, },
{ {
path: '/category/list', path: '/category/list',
component: CategoryListPage, async: ({resolve}) => resolve({component: CategoryListPage}),
beforeEnter: checkLogin beforeEnter: [checkLogin]
}, },
{ {
path: '/category/add', path: '/category/add',
component: CategoryEditPage, async: ({resolve}) => resolve({component: CategoryEditPage}),
beforeEnter: checkLogin beforeEnter: [checkLogin]
}, },
{ {
path: '/category/edit', path: '/category/edit',
component: CategoryEditPage, async: ({resolve}) => resolve({component: CategoryEditPage}),
beforeEnter: checkLogin beforeEnter: [checkLogin]
}, },
{ {
path: '/category/preset', path: '/category/preset',
component: CategoryPresetPage, async: ({resolve}) => resolve({component: CategoryPresetPage}),
beforeEnter: checkLogin beforeEnter: [checkLogin]
}, },
{ {
path: '/tag/list', path: '/tag/list',
component: TagListPage, async: ({resolve}) => resolve({component: TagListPage}),
beforeEnter: checkLogin beforeEnter: [checkLogin]
}, },
{ {
path: '(.*)', path: '(.*)',
+31 -19
View File
@@ -2,28 +2,22 @@
<f7-page> <f7-page>
<f7-navbar :title="$t('About')" :back-link="$t('Back')"></f7-navbar> <f7-navbar :title="$t('About')" :back-link="$t('Back')"></f7-navbar>
<f7-card> <f7-list strong inset dividers class="margin-top">
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-item :title="$t('Version')" :after="version"></f7-list-item>
<f7-list> <f7-list-item :title="$t('Build Time')" :after="buildTime" v-if="buildTime"></f7-list-item>
<f7-list-item :title="$t('Version')" :after="version"></f7-list-item> <f7-list-item external :title="$t('Official Website')" link="https://github.com/mayswind/ezbookkeeping" target="_blank"></f7-list-item>
<f7-list-item :title="$t('Build Time')" :after="buildTime | moment($t('format.datetime.long'))" v-if="buildTime"></f7-list-item> <f7-list-item :title="$t('License')" link="#" popup-open=".license-popup"></f7-list-item>
<f7-list-item external :title="$t('Official Website')" link="https://github.com/mayswind/ezbookkeeping" target="_blank"></f7-list-item> </f7-list>
<f7-list-item :title="$t('License')" link="#" popup-open=".license-popup"></f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-popup class="license-popup"> <f7-popup push with-subnavbar swipe-to-close swipe-handler=".swipe-handler" class="license-popup">
<f7-page> <f7-page>
<f7-navbar> <f7-navbar>
<f7-nav-title :title="$t('License')"></f7-nav-title> <div class="swipe-handler"></div>
<f7-nav-right> <f7-subnavbar :title="$t('License') "></f7-subnavbar>
<f7-link popup-close :text="$t('Done')"></f7-link>
</f7-nav-right>
</f7-navbar> </f7-navbar>
<f7-block> <f7-block strong outline>
<p> <p>
<span v-for="(line, num) in licenseLines" :key="num" <span :key="num" v-for="(line, num) in licenseLines"
:style="{ 'display': line ? 'initial' : 'block', 'padding' : line ? '0' : '0 0 1em 0' }"> :style="{ 'display': line ? 'initial' : 'block', 'padding' : line ? '0' : '0 0 1em 0' }">
{{ line }} {{ line }}
</span> </span>
@@ -34,7 +28,7 @@
<span>All the third party software included or linked is redistributed under the terms and conditions of their original licenses.</span> <span>All the third party software included or linked is redistributed under the terms and conditions of their original licenses.</span>
</p> </p>
<p></p> <p></p>
<p v-for="license in thirdPartyLicenses" :key="license.name"> <p :key="license.name" v-for="license in thirdPartyLicenses">
<strong>{{ license.name }}</strong> <strong>{{ license.name }}</strong>
<br v-if="license.copyright"/><span v-if="license.copyright">{{ license.copyright }}</span> <br v-if="license.copyright"/><span v-if="license.copyright">{{ license.copyright }}</span>
<br v-if="license.url"/><span class="work-break-all" v-if="license.url">{{ license.url }}</span> <br v-if="license.url"/><span class="work-break-all" v-if="license.url">{{ license.url }}</span>
@@ -53,7 +47,11 @@ export default {
return 'v' + this.$version; return 'v' + this.$version;
}, },
buildTime() { buildTime() {
return this.$buildTime; if (!this.$buildTime) {
return this.$buildTime;
}
return this.$utilities.formatUnixTime(this.$buildTime, this.$t('format.datetime.long'));
}, },
licenseLines() { licenseLines() {
return this.$licenses.license.replaceAll(/\r/g, '').split('\n'); return this.$licenses.license.replaceAll(/\r/g, '').split('\n');
@@ -64,3 +62,17 @@ export default {
} }
} }
</script> </script>
<style>
.license-popup .navbar-bg {
background-color: rgb(var(--f7-navbar-bg-color-rgb, var(--f7-bars-bg-color-rgb)));
}
.license-popup .subnavbar {
background-color: rgb(var(--f7-subnavbar-bg-color-rgb, var(--f7-bars-bg-color-rgb)));
}
.license-popup .subnavbar-title {
--f7-subnavbar-title-font-size: 30px;
}
</style>
+11 -23
View File
@@ -5,38 +5,26 @@
<f7-nav-title :title="$t('Application Lock')"></f7-nav-title> <f7-nav-title :title="$t('Application Lock')"></f7-nav-title>
</f7-navbar> </f7-navbar>
<f7-card v-if="isEnableApplicationLock"> <f7-list strong inset dividers class="margin-top">
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-item :title="$t('Status')" :after="$t(isEnableApplicationLock ? 'Enabled' : 'Disabled')"></f7-list-item>
<f7-list> <f7-list-item v-if="isEnableApplicationLock && isSupportedWebAuthn">
<f7-list-item :title="$t('Status')" :after="$t('Enabled')"></f7-list-item> <span>{{ $t('Face ID / Touch ID') }}</span>
<f7-list-item v-if="isSupportedWebAuthn"> <f7-toggle :checked="isEnableApplicationLockWebAuthn" @toggle:change="isEnableApplicationLockWebAuthn = $event"></f7-toggle>
<span>{{ $t('Face ID / Touch ID') }}</span> </f7-list-item>
<f7-toggle :checked="isEnableApplicationLockWebAuthn" @toggle:change="isEnableApplicationLockWebAuthn = $event"></f7-toggle> <f7-list-button v-if="isEnableApplicationLock" @click="disable(null)">{{ $t('Disable') }}</f7-list-button>
</f7-list-item> <f7-list-button v-if="!isEnableApplicationLock" @click="enable(null)">{{ $t('Enable') }}</f7-list-button>
<f7-list-button @click="disable(null)">{{ $t('Disable') }}</f7-list-button> </f7-list>
</f7-list>
</f7-card-content>
</f7-card>
<f7-card v-else-if="!isEnableApplicationLock">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item :title="$t('Status')" :after="$t('Disabled')"></f7-list-item>
<f7-list-button @click="enable(null)">{{ $t('Enable') }}</f7-list-button>
</f7-list>
</f7-card-content>
</f7-card>
<pin-code-input-sheet :title="$t('PIN Code')" <pin-code-input-sheet :title="$t('PIN Code')"
:hint="$t('Please input a new PIN code. PIN code would encrypt your local data, so you need input this PIN code when you launch this app. If this PIN code is lost, you should re-login.')" :hint="$t('Please input a new PIN code. PIN code would encrypt your local data, so you need input this PIN code when you launch this app. If this PIN code is lost, you should re-login.')"
:show.sync="showInputPinCodeSheetForEnable" v-model:show="showInputPinCodeSheetForEnable"
v-model="currentPinCodeForEnable" v-model="currentPinCodeForEnable"
@pincode:confirm="enable"> @pincode:confirm="enable">
</pin-code-input-sheet> </pin-code-input-sheet>
<pin-code-input-sheet :title="$t('PIN Code')" <pin-code-input-sheet :title="$t('PIN Code')"
:hint="$t('Please enter your current PIN code when disable application lock')" :hint="$t('Please enter your current PIN code when disable application lock')"
:show.sync="showInputPinCodeSheetForDisable" v-model:show="showInputPinCodeSheetForDisable"
v-model="currentPinCodeForDisable" v-model="currentPinCodeForDisable"
@pincode:confirm="disable"> @pincode:confirm="disable">
</pin-code-input-sheet> </pin-code-input-sheet>
+95 -66
View File
@@ -8,73 +8,73 @@
</f7-nav-right> </f7-nav-right>
</f7-navbar> </f7-navbar>
<f7-card> <f7-list strong inset dividers class="margin-vertical" v-if="exchangeRatesData && exchangeRatesData.exchangeRates && exchangeRatesData.exchangeRates.length">
<f7-card-content class="no-safe-areas" :padding="false" v-if="exchangeRatesData && exchangeRatesData.exchangeRates && exchangeRatesData.exchangeRates.length"> <f7-list-item
<f7-list> class="list-item-with-header-and-title list-item-no-item-after"
<f7-list-item :header="$t('Base Currency')"
class="list-item-with-header-and-title list-item-no-item-after" smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Currency Name'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), pageTitle: $t('Base Currency'), popupCloseLinkText: $t('Done') }"
:header="$t('Base Currency')" >
smart-select :smart-select-params="{ openIn: 'popup', pageTitle: $t('Base Currency'), searchbar: true, searchbarPlaceholder: $t('Currency Name'), searchbarDisableText: $t('Cancel'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }" <template #title>
> <div class="no-padding no-margin">
<f7-block slot="title" class="no-padding no-margin"> <span>{{ $t(`currency.${baseCurrency}`) }}&nbsp;</span>
<span>{{ $t(`currency.${baseCurrency}`) }}&nbsp;</span> <small class="smaller">{{ baseCurrency }}</small>
<small class="smaller">{{ baseCurrency }}</small> </div>
</f7-block> </template>
<select v-model="baseCurrency"> <select v-model="baseCurrency">
<option v-for="exchangeRate in availableExchangeRates" <option :value="exchangeRate.currencyCode"
:key="exchangeRate.currencyCode" :key="exchangeRate.currencyCode"
:value="exchangeRate.currencyCode">{{ exchangeRate.currencyDisplayName }}</option> v-for="exchangeRate in availableExchangeRates">{{ exchangeRate.currencyDisplayName }}</option>
</select> </select>
</f7-list-item> </f7-list-item>
<f7-list-item <f7-list-item
class="currency-base-amount" class="currency-base-amount"
link="#" no-chevron link="#" no-chevron
:style="{ fontSize: baseAmountFontSize + 'px' }" :style="{ fontSize: baseAmountFontSize + 'px' }"
:header="$t('Base Amount')" :header="$t('Base Amount')"
:title="baseAmount | currency" :title="displayBaseAmount"
@click="showBaseAmountSheet = true" @click="showBaseAmountSheet = true"
> >
<number-pad-sheet :min-value="$constants.transaction.minAmount" <number-pad-sheet :min-value="$constants.transaction.minAmount"
:max-value="$constants.transaction.maxAmount" :max-value="$constants.transaction.maxAmount"
:show.sync="showBaseAmountSheet" v-model:show="showBaseAmountSheet"
v-model="baseAmount" v-model="baseAmount"
></number-pad-sheet> ></number-pad-sheet>
</f7-list-item> </f7-list-item>
</f7-list> </f7-list>
</f7-card-content>
</f7-card>
<f7-card> <f7-list strong inset dividers class="margin-vertical" v-if="!exchangeRatesData || !exchangeRatesData.exchangeRates || !exchangeRatesData.exchangeRates.length">
<f7-card-content class="no-safe-areas" :padding="false" v-if="!exchangeRatesData || !exchangeRatesData.exchangeRates || !exchangeRatesData.exchangeRates.length"> <f7-list-item :title="$t('No exchange rates data')"></f7-list-item>
<f7-list> </f7-list>
<f7-list-item :title="$t('No exchange rates data')"></f7-list-item>
</f7-list> <f7-list strong inset dividers class="margin-vertical" v-if="exchangeRatesData && exchangeRatesData.exchangeRates && exchangeRatesData.exchangeRates.length">
</f7-card-content> <f7-list-item swipeout
<f7-card-content class="no-safe-areas" :padding="false" v-if="exchangeRatesData && exchangeRatesData.exchangeRates && exchangeRatesData.exchangeRates.length"> :after="getDisplayConvertedAmount(exchangeRate)"
<f7-list> :key="exchangeRate.currencyCode" v-for="exchangeRate in availableExchangeRates">
<f7-list-item v-for="exchangeRate in availableExchangeRates" :key="exchangeRate.currencyCode" <template #title>
:after="getConvertedAmount(exchangeRate) | exchangeRate" <div class="no-padding no-margin">
swipeout> <span style="margin-right: 5px">{{ exchangeRate.currencyDisplayName }}</span>
<f7-block slot="title" class="no-padding no-margin"> <small class="smaller">{{ exchangeRate.currencyCode }}</small>
<span style="margin-right: 5px">{{ exchangeRate.currencyDisplayName }}</span> </div>
<small class="smaller">{{ exchangeRate.currencyCode }}</small> </template>
</f7-block> <f7-swipeout-actions right>
<f7-swipeout-actions right> <f7-swipeout-button color="primary" close :text="$t('Set As Baseline')" @click="setAsBaseline(exchangeRate.currencyCode, getConvertedAmount(exchangeRate))"></f7-swipeout-button>
<f7-swipeout-button color="primary" close :text="$t('Set As Baseline')" @click="setAsBaseline(exchangeRate.currencyCode, getConvertedAmount(exchangeRate))"></f7-swipeout-button> </f7-swipeout-actions>
</f7-swipeout-actions> </f7-list-item>
</f7-list-item> </f7-list>
</f7-list>
</f7-card-content> <f7-list strong inset dividers class="margin-vertical" v-if="exchangeRatesData && exchangeRatesData.exchangeRates && exchangeRatesData.exchangeRates.length">
<f7-card-footer v-if="exchangeRatesData.exchangeRates && exchangeRatesData.exchangeRates.length"> <f7-list-item v-if="exchangeRatesDataUpdateTime">
<span>{{ $t('Last Updated') }}</span> <small>{{ $t('Last Updated') }}</small>
<span>{{ exchangeRatesData.updateTime | moment($t('format.date.long')) }}</span> <small>{{ exchangeRatesDataUpdateTime }}</small>
</f7-card-footer> </f7-list-item>
<f7-card-footer v-if="exchangeRatesData.exchangeRates && exchangeRatesData.exchangeRates.length"> <f7-list-item>
<span>{{ $t('Data source') }}</span> <small>{{ $t('Data source') }}</small>
<f7-link external target="_blank" :href="exchangeRatesData.referenceUrl" v-if="exchangeRatesData.referenceUrl">{{ exchangeRatesData.dataSource }}</f7-link> <small>
<span v-else-if="!exchangeRatesData.referenceUrl">{{ exchangeRatesData.dataSource }}</span> <f7-link external target="_blank" :href="exchangeRatesData.referenceUrl" v-if="exchangeRatesData.referenceUrl">{{ exchangeRatesData.dataSource }}</f7-link>
</f7-card-footer> <span v-else-if="!exchangeRatesData.referenceUrl">{{ exchangeRatesData.dataSource }}</span>
</f7-card> </small>
</f7-list-item>
</f7-list>
<f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false"> <f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false">
<f7-actions-group> <f7-actions-group>
@@ -106,6 +106,13 @@ export default {
exchangeRatesData() { exchangeRatesData() {
return this.$store.state.latestExchangeRates.data; return this.$store.state.latestExchangeRates.data;
}, },
exchangeRatesDataUpdateTime() {
if (!this.exchangeRatesData) {
return '';
}
return this.$utilities.formatUnixTime(this.exchangeRatesData.updateTime, this.$t('format.date.long'));
},
exchangeRateMap() { exchangeRateMap() {
const exchangeRateMap = {}; const exchangeRateMap = {};
@@ -143,6 +150,9 @@ export default {
return availableExchangeRates; return availableExchangeRates;
}, },
displayBaseAmount() {
return this.$locale.getDisplayCurrency(this.baseAmount);
},
baseAmountFontSize() { baseAmountFontSize() {
return this.getFontSizeByAmount(this.baseAmount); return this.getFontSizeByAmount(this.baseAmount);
} }
@@ -213,6 +223,25 @@ export default {
return this.$utilities.getExchangedAmount(this.baseAmount / 100, fromExchangeRate.rate, toExchangeRate.rate); return this.$utilities.getExchangedAmount(this.baseAmount / 100, fromExchangeRate.rate, toExchangeRate.rate);
}, },
getDisplayConvertedAmount(toExchangeRate) {
const rateStr = this.getConvertedAmount(toExchangeRate).toString();
if (rateStr.indexOf('.') < 0) {
return this.$utilities.appendThousandsSeparator(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;
}
}
const trimmedRateStr = rateStr.substring(0, Math.max(6, Math.max(firstNonZeroPos, rateStr.indexOf('.') + 2)));
return this.$utilities.appendThousandsSeparator(trimmedRateStr);
}
},
setAsBaseline(currency, amount) { setAsBaseline(currency, amount) {
if (!this.$utilities.isNumber(amount)) { if (!this.$utilities.isNumber(amount)) {
amount = ''; amount = '';
+186 -124
View File
@@ -13,14 +13,14 @@
<small>Expense</small> <small>Expense</small>
</span> </span>
<span class="card-header-content" v-else-if="!loading"> <span class="card-header-content" v-else-if="!loading">
<span class="home-summary-month">{{ dateRange.thisMonth.startTime | moment('MMMM') }}</span> <span class="home-summary-month">{{ displayDateRange.thisMonth.displayTime }}</span>
<span>·</span> <span>·</span>
<small>{{ $t('Expense') }}</small> <small>{{ $t('Expense') }}</small>
</span> </span>
</p> </p>
<p class="no-margin"> <p class="no-margin">
<span class="month-expense" v-if="loading">0.00 USD</span> <span class="month-expense" v-if="loading">0.00 USD</span>
<span class="month-expense" v-else-if="!loading">{{ thisMonthAmount.expenseAmount | amount(thisMonthAmount.incompleteExpenseAmount, showAmountInHomePage) | currency(defaultCurrency) }}</span> <span class="month-expense" v-else-if="!loading">{{ transactionOverview.thisMonth.expenseAmount }}</span>
<f7-link class="margin-left-half" @click="toggleShowAmountInHomePage()"> <f7-link class="margin-left-half" @click="toggleShowAmountInHomePage()">
<f7-icon :f7="showAmountInHomePage ? 'eye_slash_fill' : 'eye_fill'" size="18px"></f7-icon> <f7-icon :f7="showAmountInHomePage ? 'eye_slash_fill' : 'eye_fill'" size="18px"></f7-icon>
</f7-link> </f7-link>
@@ -29,137 +29,157 @@
<small class="home-summary-misc" v-if="loading">Monthly income 0.00 USD</small> <small class="home-summary-misc" v-if="loading">Monthly income 0.00 USD</small>
<small class="home-summary-misc" v-else-if="!loading"> <small class="home-summary-misc" v-else-if="!loading">
<span>{{ $t('Monthly income') }}</span> <span>{{ $t('Monthly income') }}</span>
<span>{{ thisMonthAmount.incomeAmount | amount(thisMonthAmount.incompleteIncomeAmount, showAmountInHomePage) | currency(defaultCurrency) }}</span> <span>{{ transactionOverview.thisMonth.incomeAmount }}</span>
</small> </small>
</p> </p>
</f7-card-header> </f7-card-header>
</f7-card> </f7-card>
<f7-card :class="{ 'skeleton-text': loading }"> <f7-list strong inset dividers class="margin-top" :class="{ 'skeleton-text': loading }">
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-item :link="'/transaction/list?dateType=' + $constants.datetime.allDateRanges.Today.type" chevron-center>
<f7-list> <template #media>
<f7-list-item :link="'/transaction/list?dateType=' + $constants.datetime.allDateRanges.Today.type" chevron-center> <f7-icon f7="calendar_today"></f7-icon>
<div slot="media"> </template>
<f7-icon f7="calendar_today"></f7-icon> <template #title>
<div class="padding-top-half">
<span v-if="loading">Today</span>
<span v-else-if="!loading">{{ $t('Today') }}</span>
</div>
</template>
<template #footer>
<div class="overview-transaction-footer padding-bottom-half">
<span v-if="loading">MM/DD/YYYY</span>
<span v-else-if="!loading">{{ displayDateRange.today.displayTime }}</span>
</div>
</template>
<template #after>
<div>
<div class="text-color-red">
<small v-if="loading">0.00 USD</small>
<small v-else-if="!loading && transactionOverview.today && transactionOverview.today.valid">{{ transactionOverview.today.incomeAmount }}</small>
</div> </div>
<div slot="title" class="padding-top-half"> <div class="text-color-teal">
<span v-if="loading">Today</span> <small v-if="loading">0.00 USD</small>
<span v-else-if="!loading">{{ $t('Today') }}</span> <small v-else-if="!loading && transactionOverview.today && transactionOverview.today.valid">{{ transactionOverview.today.expenseAmount }}</small>
</div> </div>
<div slot="footer" class="overview-transaction-footer padding-bottom-half"> </div>
<span v-if="loading">MM/DD/YYYY</span> </template>
<span v-else-if="!loading">{{ dateRange.today.startTime | moment($t('format.date.long')) }}</span> </f7-list-item>
</div>
<div slot="after">
<div class="text-color-red">
<small v-if="loading">0.00 USD</small>
<small v-else-if="!loading && transactionOverview.today">{{ transactionOverview.today.incomeAmount | amount(transactionOverview.today.incompleteIncomeAmount, showAmountInHomePage) | currency(defaultCurrency) }}</small>
</div>
<div class="text-color-teal">
<small v-if="loading">0.00 USD</small>
<small v-else-if="!loading && transactionOverview.today">{{ transactionOverview.today.expenseAmount | amount(transactionOverview.today.incompleteExpenseAmount, showAmountInHomePage) | currency(defaultCurrency) }}</small>
</div>
</div>
</f7-list-item>
<f7-list-item :link="'/transaction/list?dateType=' + $constants.datetime.allDateRanges.ThisWeek.type" chevron-center> <f7-list-item :link="'/transaction/list?dateType=' + $constants.datetime.allDateRanges.ThisWeek.type" chevron-center>
<div slot="media"> <template #media>
<f7-icon f7="calendar"></f7-icon> <f7-icon f7="calendar"></f7-icon>
</template>
<template #title>
<div class="padding-top-half">
<span v-if="loading">This Week</span>
<span v-else-if="!loading">{{ $t('This Week') }}</span>
</div>
</template>
<template #footer>
<div class="overview-transaction-footer padding-bottom-half">
<span v-if="loading">MM/DD</span>
<span v-else-if="!loading">{{ displayDateRange.thisWeek.startTime }}</span>
<span>-</span>
<span v-if="loading">MM/DD</span>
<span v-else-if="!loading">{{ displayDateRange.thisWeek.endTime }}</span>
</div>
</template>
<template #after>
<div>
<div class="text-color-red">
<small v-if="loading">0.00 USD</small>
<small v-else-if="!loading && transactionOverview.thisWeek && transactionOverview.thisWeek.valid">{{ transactionOverview.thisWeek.incomeAmount }}</small>
</div> </div>
<div slot="title" class="padding-top-half"> <div class="text-color-teal">
<span v-if="loading">This Week</span> <small v-if="loading">0.00 USD</small>
<span v-else-if="!loading">{{ $t('This Week') }}</span> <small v-else-if="!loading && transactionOverview.thisWeek && transactionOverview.thisWeek.valid">{{ transactionOverview.thisWeek.expenseAmount }}</small>
</div> </div>
<div slot="footer" class="overview-transaction-footer padding-bottom-half"> </div>
<span v-if="loading">MM/DD</span> </template>
<span v-else-if="!loading">{{ dateRange.thisWeek.startTime | moment($t('format.monthDay.long')) }}</span> </f7-list-item>
<span>-</span>
<span v-if="loading">MM/DD</span>
<span v-else-if="!loading">{{ dateRange.thisWeek.endTime | moment($t('format.monthDay.long')) }}</span>
</div>
<div slot="after">
<div class="text-color-red">
<small v-if="loading">0.00 USD</small>
<small v-else-if="!loading && transactionOverview.thisWeek">{{ transactionOverview.thisWeek.incomeAmount | amount(transactionOverview.thisWeek.incompleteIncomeAmount, showAmountInHomePage) | currency(defaultCurrency) }}</small>
</div>
<div class="text-color-teal">
<small v-if="loading">0.00 USD</small>
<small v-else-if="!loading && transactionOverview.thisWeek">{{ transactionOverview.thisWeek.expenseAmount | amount(transactionOverview.thisWeek.incompleteExpenseAmount, showAmountInHomePage) | currency(defaultCurrency) }}</small>
</div>
</div>
</f7-list-item>
<f7-list-item :link="'/transaction/list?dateType=' + $constants.datetime.allDateRanges.ThisMonth.type" chevron-center> <f7-list-item :link="'/transaction/list?dateType=' + $constants.datetime.allDateRanges.ThisMonth.type" chevron-center>
<div slot="media"> <template #media>
<f7-icon f7="calendar"></f7-icon> <f7-icon f7="calendar"></f7-icon>
</template>
<template #title>
<div class="padding-top-half">
<span v-if="loading">This Month</span>
<span v-else-if="!loading">{{ $t('This Month') }}</span>
</div>
</template>
<template #footer>
<div class="overview-transaction-footer padding-bottom-half">
<span v-if="loading">MM/DD</span>
<span v-else-if="!loading">{{ displayDateRange.thisMonth.startTime }}</span>
<span>-</span>
<span v-if="loading">MM/DD</span>
<span v-else-if="!loading">{{ displayDateRange.thisMonth.endTime }}</span>
</div>
</template>
<template #after>
<div>
<div class="text-color-red">
<small v-if="loading">0.00 USD</small>
<small v-else-if="!loading && transactionOverview.thisMonth && transactionOverview.thisMonth.valid">{{ transactionOverview.thisMonth.incomeAmount }}</small>
</div> </div>
<div slot="title" class="padding-top-half"> <div class="text-color-teal">
<span v-if="loading">This Month</span> <small v-if="loading">0.00 USD</small>
<span v-else-if="!loading">{{ $t('This Month') }}</span> <small v-else-if="!loading && transactionOverview.thisMonth && transactionOverview.thisMonth.valid">{{ transactionOverview.thisMonth.expenseAmount }}</small>
</div> </div>
<div slot="footer" class="overview-transaction-footer padding-bottom-half"> </div>
<span v-if="loading">MM/DD</span> </template>
<span v-else-if="!loading">{{ dateRange.thisMonth.startTime | moment($t('format.monthDay.long')) }}</span> </f7-list-item>
<span>-</span>
<span v-if="loading">MM/DD</span>
<span v-else-if="!loading">{{ dateRange.thisMonth.endTime | moment($t('format.monthDay.long')) }}</span>
</div>
<div slot="after">
<div class="text-color-red">
<small v-if="loading">0.00 USD</small>
<small v-else-if="!loading && transactionOverview.thisMonth">{{ transactionOverview.thisMonth.incomeAmount | amount(transactionOverview.thisMonth.incompleteIncomeAmount, showAmountInHomePage) | currency(defaultCurrency) }}</small>
</div>
<div class="text-color-teal">
<small v-if="loading">0.00 USD</small>
<small v-else-if="!loading && transactionOverview.thisMonth">{{ transactionOverview.thisMonth.expenseAmount | amount(transactionOverview.thisMonth.incompleteExpenseAmount, showAmountInHomePage) | currency(defaultCurrency) }}</small>
</div>
</div>
</f7-list-item>
<f7-list-item :link="'/transaction/list?dateType=' + $constants.datetime.allDateRanges.ThisYear.type" chevron-center> <f7-list-item :link="'/transaction/list?dateType=' + $constants.datetime.allDateRanges.ThisYear.type" chevron-center>
<div slot="media"> <template #media>
<f7-icon f7="square_stack_3d_up"></f7-icon> <f7-icon f7="square_stack_3d_up"></f7-icon>
</template>
<template #title>
<div class="padding-top-half">
<span v-if="loading">This Year</span>
<span v-else-if="!loading">{{ $t('This Year') }}</span>
</div>
</template>
<template #footer>
<div class="overview-transaction-footer padding-bottom-half">
<span v-if="loading">YYYY</span>
<span v-else-if="!loading">{{ displayDateRange.thisYear.displayTime }}</span>
</div>
</template>
<template #after>
<div>
<div class="text-color-red">
<small v-if="loading">0.00 USD</small>
<small v-else-if="!loading && transactionOverview.thisYear && transactionOverview.thisYear.valid">{{ transactionOverview.thisYear.incomeAmount }}</small>
</div> </div>
<div slot="title" class="padding-top-half"> <div class="text-color-teal">
<span v-if="loading">This Year</span> <small v-if="loading">0.00 USD</small>
<span v-else-if="!loading">{{ $t('This Year') }}</span> <small v-else-if="!loading && transactionOverview.thisYear && transactionOverview.thisYear.valid">{{ transactionOverview.thisYear.expenseAmount }}</small>
</div> </div>
<div slot="footer" class="overview-transaction-footer padding-bottom-half"> </div>
<span v-if="loading">YYYY</span> </template>
<span v-else-if="!loading">{{ dateRange.thisYear.startTime | moment($t('format.year.long')) }}</span> </f7-list-item>
</div> </f7-list>
<div slot="after">
<div class="text-color-red">
<small v-if="loading">0.00 USD</small>
<small v-else-if="!loading && transactionOverview.thisYear">{{ transactionOverview.thisYear.incomeAmount | amount(transactionOverview.thisYear.incompleteIncomeAmount, showAmountInHomePage) | currency(defaultCurrency) }}</small>
</div>
<div class="text-color-teal">
<small v-if="loading">0.00 USD</small>
<small v-else-if="!loading && transactionOverview.thisYear">{{ transactionOverview.thisYear.expenseAmount | amount(transactionOverview.thisYear.incompleteExpenseAmount, showAmountInHomePage) | currency(defaultCurrency) }}</small>
</div>
</div>
</f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-toolbar tabbar labels bottom> <f7-toolbar tabbar icons bottom class="main-tabbar">
<f7-link href="/transaction/list"> <f7-link class="link" href="/transaction/list">
<f7-icon f7="square_list"></f7-icon> <f7-icon f7="square_list"></f7-icon>
<span class="tabbar-label">{{ $t('Details') }}</span> <span class="tabbar-label">{{ $t('Details') }}</span>
</f7-link> </f7-link>
<f7-link href="/account/list"> <f7-link class="link" href="/account/list">
<f7-icon f7="creditcard"></f7-icon> <f7-icon f7="creditcard"></f7-icon>
<span class="tabbar-label">{{ $t('Accounts') }}</span> <span class="tabbar-label">{{ $t('Accounts') }}</span>
</f7-link> </f7-link>
<f7-link href="/transaction/add"> <f7-link class="link" href="/transaction/add">
<f7-icon f7="plus_square" class="ebk-tarbar-big-icon"></f7-icon> <f7-icon f7="plus_square" class="ebk-tarbar-big-icon"></f7-icon>
</f7-link> </f7-link>
<f7-link href="/statistic/transaction"> <f7-link class="link" href="/statistic/transaction">
<f7-icon f7="chart_pie"></f7-icon> <f7-icon f7="chart_pie"></f7-icon>
<span class="tabbar-label">{{ $t('Statistics') }}</span> <span class="tabbar-label">{{ $t('Statistics') }}</span>
</f7-link> </f7-link>
<f7-link href="/settings"> <f7-link class="link" href="/settings">
<f7-icon f7="gear_alt"></f7-icon> <f7-icon f7="gear_alt"></f7-icon>
<span class="tabbar-label">{{ $t('Settings') }}</span> <span class="tabbar-label">{{ $t('Settings') }}</span>
</f7-link> </f7-link>
@@ -182,9 +202,6 @@ export default {
}; };
}, },
computed: { computed: {
transactionOverview() {
return this.$store.state.transactionOverview;
},
defaultCurrency() { defaultCurrency() {
return this.$store.getters.currentUserDefaultCurrency; return this.$store.getters.currentUserDefaultCurrency;
}, },
@@ -213,17 +230,60 @@ export default {
} }
}; };
}, },
thisMonthAmount() { displayDateRange() {
const self = this;
return {
today: {
displayTime: self.$utilities.formatUnixTime(self.dateRange.today.startTime, self.$t('format.date.long')),
},
thisWeek: {
startTime: self.$utilities.formatUnixTime(self.dateRange.thisWeek.startTime, self.$t('format.monthDay.long')),
endTime: self.$utilities.formatUnixTime(self.dateRange.thisWeek.endTime, self.$t('format.monthDay.long'))
},
thisMonth: {
displayTime: self.$utilities.formatUnixTime(self.dateRange.thisMonth.startTime, 'MMMM'),
startTime: self.$utilities.formatUnixTime(self.dateRange.thisMonth.startTime, self.$t('format.monthDay.long')),
endTime: self.$utilities.formatUnixTime(self.dateRange.thisMonth.endTime, self.$t('format.monthDay.long'))
},
thisYear: {
displayTime: self.$utilities.formatUnixTime(self.dateRange.thisYear.startTime, self.$t('format.year.long'))
}
};
},
transactionOverview() {
// make sure this computed property refers these property, so these property can trigger this computed property to update
const isEnableThousandsSeparator = this.isEnableThousandsSeparator; // eslint-disable-line
const currencyDisplayMode = this.currencyDisplayMode; // eslint-disable-line
if (!this.$store.state.transactionOverview || !this.$store.state.transactionOverview.thisMonth) { if (!this.$store.state.transactionOverview || !this.$store.state.transactionOverview.thisMonth) {
return { return {
incomeAmount: 0, thisMonth: {
expenseAmount: 0, valid: false,
incompleteIncomeAmount: false, incomeAmount: this.getDisplayAmount(0, false),
incompleteExpenseAmount: false expenseAmount: this.getDisplayAmount(0, false)
}
}; };
} }
return this.$store.state.transactionOverview.thisMonth; const originalOverview = this.$store.state.transactionOverview;
const displayOverview = {};
[ 'today', 'thisWeek', 'thisMonth', 'thisYear' ].forEach(key => {
if (!originalOverview[key]) {
return;
}
const item = originalOverview[key];
displayOverview[key] = {
valid: true,
incomeAmount: this.getDisplayAmount(item.incomeAmount, item.incompleteIncomeAmount),
expenseAmount: this.getDisplayAmount(item.expenseAmount, item.incompleteExpenseAmount)
};
});
return displayOverview;
} }
}, },
created() { created() {
@@ -292,15 +352,13 @@ export default {
toggleShowAmountInHomePage() { toggleShowAmountInHomePage() {
this.showAmountInHomePage = !this.showAmountInHomePage; this.showAmountInHomePage = !this.showAmountInHomePage;
this.$settings.setShowAmountInHomePage(this.showAmountInHomePage); this.$settings.setShowAmountInHomePage(this.showAmountInHomePage);
} },
}, getDisplayAmount(amount, incomplete) {
filters: { if (!this.showAmountInHomePage) {
amount(amount, incomplete, showAmount) { return this.$locale.getDisplayCurrency('***', this.defaultCurrency);
if (!showAmount) {
return '***';
} }
return amount + (incomplete ? '+' : ''); return this.$locale.getDisplayCurrency(amount, this.defaultCurrency) + (incomplete ? '+' : '');
} }
} }
} }
@@ -331,11 +389,11 @@ export default {
margin-right: 0; margin-right: 0;
} }
.theme-dark .home-summary-card { .dark .home-summary-card {
background-color: var(--f7-theme-color); background-color: var(--f7-theme-color);
} }
.theme-dark .home-summary-card a { .dark .home-summary-card a {
color: var(--f7-text-color); color: var(--f7-text-color);
opacity: 0.6; opacity: 0.6;
} }
@@ -349,7 +407,11 @@ export default {
margin-right: 4px; margin-right: 4px;
} }
.tabbar-labels i.ebk-tarbar-big-icon { .tabbar.main-tabbar .link i + span.tabbar-label {
margin-top: 2px;
}
.tabbar.main-tabbar .link i.ebk-tarbar-big-icon {
font-size: 42px; font-size: 42px;
width: 42px; width: 42px;
height: 42px; height: 42px;
+31 -27
View File
@@ -1,19 +1,19 @@
<template> <template>
<f7-page no-toolbar no-navbar no-swipeback login-screen> <f7-page no-toolbar no-navbar no-swipeback login-screen>
<f7-login-screen-title> <f7-login-screen-title>
<img class="login-page-logo" src="img/ezbookkeeping-192.png" /> <img alt="logo" class="login-page-logo" src="/img/ezbookkeeping-192.png" />
<f7-block class="margin-vertical-half">{{ $t('global.app.title') }}</f7-block> <f7-block class="margin-vertical-half">{{ $t('global.app.title') }}</f7-block>
</f7-login-screen-title> </f7-login-screen-title>
<f7-list form> <f7-list form dividers>
<f7-list-input <f7-list-input
type="text" type="text"
autocomplete="username" autocomplete="username"
clear-button clear-button
:label="$t('Username')" :label="$t('Username')"
:placeholder="$t('Your username or email')" :placeholder="$t('Your username or email')"
:value="username" v-model:value="username"
@input="username = $event.target.value; tempToken = ''" @input="tempToken = ''"
></f7-list-input> ></f7-list-input>
<f7-list-input <f7-list-input
type="password" type="password"
@@ -21,9 +21,9 @@
clear-button clear-button
:label="$t('Password')" :label="$t('Password')"
:placeholder="$t('Your password')" :placeholder="$t('Your password')"
:value="password" v-model:value="password"
@input="password = $event.target.value; tempToken = ''" @input="tempToken = ''"
@keyup.enter.native="loginByPressEnter" @keyup.enter="loginByPressEnter"
></f7-list-input> ></f7-list-input>
</f7-list> </f7-list>
@@ -50,15 +50,17 @@
</f7-list> </f7-list>
<f7-popover class="lang-popover-menu"> <f7-popover class="lang-popover-menu">
<f7-list> <f7-list dividers>
<f7-list-item <f7-list-item
link="#" no-chevron popover-close link="#" no-chevron popover-close
v-for="(lang, locale) in allLanguages"
:key="locale"
:title="lang.displayName" :title="lang.displayName"
:key="locale"
v-for="(lang, locale) in allLanguages"
@click="changeLanguage(locale)" @click="changeLanguage(locale)"
> >
<f7-icon slot="after" class="list-item-checked-icon" f7="checkmark_alt" v-if="$i18n.locale === locale"></f7-icon> <template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="$i18n.locale === locale"></f7-icon>
</template>
</f7-list-item> </f7-list-item>
</f7-list> </f7-list>
</f7-popover> </f7-popover>
@@ -72,26 +74,28 @@
<div style="font-size: 18px"><b>{{ $t('Two-Factor Authentication') }}</b></div> <div style="font-size: 18px"><b>{{ $t('Two-Factor Authentication') }}</b></div>
</div> </div>
<div class="padding-horizontal padding-bottom"> <div class="padding-horizontal padding-bottom">
<f7-list no-hairlines class="no-margin-top margin-bottom"> <f7-list no-hairlines strong class="no-margin">
<f7-list-input <f7-list-input
type="number" type="number"
autocomplete="one-time-code" autocomplete="one-time-code"
outline outline
floating-label
clear-button clear-button
v-if="twoFAVerifyType === 'passcode'" v-if="twoFAVerifyType === 'passcode'"
:label="$t('Passcode')"
:placeholder="$t('Passcode')" :placeholder="$t('Passcode')"
:value="passcode" v-model:value="passcode"
@input="passcode = $event.target.value" @keyup.enter="verify"
@keyup.enter.native="verify"
></f7-list-input> ></f7-list-input>
<f7-list-input <f7-list-input
outline outline
floating-label
clear-button clear-button
v-if="twoFAVerifyType === 'backupcode'" v-if="twoFAVerifyType === 'backupcode'"
:label="$t('Backup Code')"
:placeholder="$t('Backup Code')" :placeholder="$t('Backup Code')"
:value="backupCode" v-model:value="backupCode"
@input="backupCode = $event.target.value" @keyup.enter="verify"
@keyup.enter.native="verify"
></f7-list-input> ></f7-list-input>
</f7-list> </f7-list>
<f7-button large fill :class="{ 'disabled': twoFAInputIsEmpty || verifying }" :text="$t('Verify')" @click="verify"></f7-button> <f7-button large fill :class="{ 'disabled': twoFAInputIsEmpty || verifying }" :text="$t('Verify')" @click="verify"></f7-button>
@@ -106,6 +110,9 @@
<script> <script>
export default { export default {
props: [
'f7router'
],
data() { data() {
return { return {
username: '', username: '',
@@ -124,7 +131,7 @@ export default {
return 'v' + this.$version; return 'v' + this.$version;
}, },
allLanguages() { allLanguages() {
return this.$locale.getAllLanguages(); return this.$locale.getAllLanguageInfos();
}, },
isUserRegistrationEnabled() { isUserRegistrationEnabled() {
return this.$settings.isUserRegistrationEnabled(); return this.$settings.isUserRegistrationEnabled();
@@ -148,10 +155,10 @@ export default {
}, },
currentLanguageName() { currentLanguageName() {
const currentLocale = this.$i18n.locale; const currentLocale = this.$i18n.locale;
let lang = this.$locale.getLanguage(currentLocale); let lang = this.$locale.getLanguageInfo(currentLocale);
if (!lang) { if (!lang) {
lang = this.$locale.getLanguage(this.$locale.getDefaultLanguage()); lang = this.$locale.getLanguageInfo(this.$locale.getDefaultLanguage());
} }
return lang.displayName; return lang.displayName;
@@ -160,7 +167,7 @@ export default {
methods: { methods: {
login() { login() {
const self = this; const self = this;
const router = self.$f7router; const router = self.f7router;
if (!this.username) { if (!this.username) {
self.$alert('Username cannot be empty'); self.$alert('Username cannot be empty');
@@ -208,10 +215,7 @@ export default {
}); });
}, },
loginByPressEnter() { loginByPressEnter() {
const app = this.$f7; if (this.$ui.isModalShowing()) {
const $$ = app.$;
if ($$('.modal-in').length) {
return; return;
} }
@@ -219,7 +223,7 @@ export default {
}, },
verify() { verify() {
const self = this; const self = this;
const router = self.$f7router; const router = self.f7router;
if (self.twoFAInputIsEmpty || self.verifying) { if (self.twoFAInputIsEmpty || self.verifying) {
return; return;
+80 -85
View File
@@ -3,108 +3,103 @@
<f7-navbar :title="$t('Settings')" :back-link="$t('Back')"></f7-navbar> <f7-navbar :title="$t('Settings')" :back-link="$t('Back')"></f7-navbar>
<f7-block-title class="margin-top">{{ currentNickName }}</f7-block-title> <f7-block-title class="margin-top">{{ currentNickName }}</f7-block-title>
<f7-card> <f7-list strong inset dividers>
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-item :title="$t('User Profile')" link="/user/profile"></f7-list-item>
<f7-list> <f7-list-item :title="$t('Transaction Categories')" link="/category/all"></f7-list-item>
<f7-list-item :title="$t('User Profile')" link="/user/profile"></f7-list-item> <f7-list-item :title="$t('Transaction Tags')" link="/tag/list"></f7-list-item>
<f7-list-item :title="$t('Transaction Categories')" link="/category/all"></f7-list-item> <f7-list-item :title="$t('Data Management')" link="/user/data/management"></f7-list-item>
<f7-list-item :title="$t('Transaction Tags')" link="/tag/list"></f7-list-item> <f7-list-item :title="$t('Two-Factor Authentication')" link="/user/2fa"></f7-list-item>
<f7-list-item :title="$t('Data Management')" link="/user/data/management"></f7-list-item> <f7-list-item :title="$t('Device & Sessions')" link="/user/sessions"></f7-list-item>
<f7-list-item :title="$t('Two-Factor Authentication')" link="/user/2fa"></f7-list-item> <f7-list-button :class="{ 'disabled': logouting }" @click="logout">{{ $t('Log Out') }}</f7-list-button>
<f7-list-item :title="$t('Device & Sessions')" link="/user/sessions"></f7-list-item> </f7-list>
<f7-list-button :class="{ 'disabled': logouting }" @click="logout">{{ $t('Log Out') }}</f7-list-button>
</f7-list>
</f7-card-content>
</f7-card>
<f7-block-title>{{ $t('Application') }}</f7-block-title> <f7-block-title>{{ $t('Application') }}</f7-block-title>
<f7-card> <f7-list strong inset dividers>
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-item
<f7-list> :key="currentLocale + '_lang'"
<f7-list-item :title="$t('Language')"
:key="currentLocale + '_lang'" smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Language'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), popupCloseLinkText: $t('Done') }">
:title="$t('Language')" <select v-model="currentLocale">
smart-select :smart-select-params="{ openIn: 'popup', searchbar: true, searchbarPlaceholder: $t('Language'), searchbarDisableText: $t('Cancel'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }"> <option :value="locale"
<select v-model="currentLocale"> :key="locale"
<option v-for="(lang, locale) in allLanguages" v-for="(lang, locale) in allLanguages">{{ lang.displayName }}</option>
:key="locale" </select>
:value="locale">{{ lang.displayName }}</option> </f7-list-item>
</select>
</f7-list-item>
<f7-list-item <f7-list-item
:key="currentLocale + '_timezone'" :key="currentLocale + '_timezone'"
:title="$t('Timezone')" :title="$t('Timezone')"
smart-select :smart-select-params="{ openIn: 'popup', searchbar: true, searchbarPlaceholder: $t('Timezone'), searchbarDisableText: $t('Cancel'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }"> smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Timezone'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), popupCloseLinkText: $t('Done') }">
<select v-model="currentTimezone"> <select v-model="currentTimezone">
<option v-for="timezone in allTimezones" <option :value="timezone.name"
:key="timezone.name" :key="timezone.name"
:value="timezone.name">{{ `(UTC${timezone.utcOffset}) ${timezone.displayName}` }}</option> v-for="timezone in allTimezones">{{ `(UTC${timezone.utcOffset}) ${timezone.displayName}` }}</option>
</select> </select>
</f7-list-item> </f7-list-item>
<f7-list-item :title="$t('Application Lock')" :after="isEnableApplicationLock ? $t('Enabled') : $t('Disabled')" link="/app_lock"></f7-list-item> <f7-list-item :title="$t('Application Lock')" :after="isEnableApplicationLock ? $t('Enabled') : $t('Disabled')" link="/app_lock"></f7-list-item>
<f7-list-item :title="$t('Exchange Rates Data')" :after="exchangeRatesLastUpdateDate" link="/exchange_rates"></f7-list-item> <f7-list-item :title="$t('Exchange Rates Data')" :after="exchangeRatesLastUpdateDate" link="/exchange_rates"></f7-list-item>
<f7-list-item> <f7-list-item>
<span>{{ $t('Auto Update Exchange Rates Data') }}</span> <span>{{ $t('Auto Update Exchange Rates Data') }}</span>
<f7-toggle :checked="isAutoUpdateExchangeRatesData" @toggle:change="isAutoUpdateExchangeRatesData = $event"></f7-toggle> <f7-toggle :checked="isAutoUpdateExchangeRatesData" @toggle:change="isAutoUpdateExchangeRatesData = $event"></f7-toggle>
</f7-list-item> </f7-list-item>
<f7-list-item> <f7-list-item>
<span>{{ $t('Enable Thousands Separator') }}</span> <span>{{ $t('Enable Thousands Separator') }}</span>
<f7-toggle :checked="isEnableThousandsSeparator" @toggle:change="isEnableThousandsSeparator = $event"></f7-toggle> <f7-toggle :checked="isEnableThousandsSeparator" @toggle:change="isEnableThousandsSeparator = $event"></f7-toggle>
</f7-list-item> </f7-list-item>
<f7-list-item <f7-list-item
:key="currentLocale + '_currency_display'" :key="currentLocale + '_currency_display'"
:title="$t('Currency Display Mode')" :title="$t('Currency Display Mode')"
smart-select :smart-select-params="{ openIn: 'popup', searchbar: true, searchbarPlaceholder: $t('Currency Display Mode'), searchbarDisableText: $t('Cancel'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }"> smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Currency Display Mode'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), popupCloseLinkText: $t('Done') }">
<select v-model="currencyDisplayMode"> <select v-model="currencyDisplayMode">
<option :value="$constants.currency.allCurrencyDisplayModes.None">{{ $t('None') }}</option> <option :value="$constants.currency.allCurrencyDisplayModes.None">{{ $t('None') }}</option>
<option :value="$constants.currency.allCurrencyDisplayModes.Symbol">{{ $t('Currency Symbol') }}</option> <option :value="$constants.currency.allCurrencyDisplayModes.Symbol">{{ $t('Currency Symbol') }}</option>
<option :value="$constants.currency.allCurrencyDisplayModes.Code">{{ $t('Currency Code') }}</option> <option :value="$constants.currency.allCurrencyDisplayModes.Code">{{ $t('Currency Code') }}</option>
<option :value="$constants.currency.allCurrencyDisplayModes.Name">{{ $t('Currency Name') }}</option> <option :value="$constants.currency.allCurrencyDisplayModes.Name">{{ $t('Currency Name') }}</option>
</select> </select>
</f7-list-item> </f7-list-item>
<f7-list-item> <f7-list-item>
<span>{{ $t('Show Amount In Home Page') }}</span> <span>{{ $t('Show Amount In Home Page') }}</span>
<f7-toggle :checked="showAmountInHomePage" @toggle:change="showAmountInHomePage = $event"></f7-toggle> <f7-toggle :checked="showAmountInHomePage" @toggle:change="showAmountInHomePage = $event"></f7-toggle>
</f7-list-item> </f7-list-item>
<f7-list-item> <f7-list-item>
<span>{{ $t('Show Account Balance') }}</span> <span>{{ $t('Show Account Balance') }}</span>
<f7-toggle :checked="showAccountBalance" @toggle:change="showAccountBalance = $event"></f7-toggle> <f7-toggle :checked="showAccountBalance" @toggle:change="showAccountBalance = $event"></f7-toggle>
</f7-list-item> </f7-list-item>
<f7-list-item> <f7-list-item>
<span>{{ $t('Show Total Amount In Transaction List Page') }}</span> <span>{{ $t('Show Total Amount In Transaction List Page') }}</span>
<f7-toggle :checked="showTotalAmountInTransactionListPage" @toggle:change="showTotalAmountInTransactionListPage = $event"></f7-toggle> <f7-toggle :checked="showTotalAmountInTransactionListPage" @toggle:change="showTotalAmountInTransactionListPage = $event"></f7-toggle>
</f7-list-item> </f7-list-item>
<f7-list-item :title="$t('Statistics Settings')" link="/statistic/settings"></f7-list-item> <f7-list-item :title="$t('Statistics Settings')" link="/statistic/settings"></f7-list-item>
<f7-list-item> <f7-list-item>
<span>{{ $t('Enable Animate') }}</span> <span>{{ $t('Enable Animate') }}</span>
<f7-toggle :checked="isEnableAnimate" @toggle:change="isEnableAnimate = $event"></f7-toggle> <f7-toggle :checked="isEnableAnimate" @toggle:change="isEnableAnimate = $event"></f7-toggle>
</f7-list-item> </f7-list-item>
<f7-list-item> <f7-list-item>
<span>{{ $t('Enable Auto Dark Mode') }}</span> <span>{{ $t('Enable Auto Dark Mode') }}</span>
<f7-toggle :checked="isEnableAutoDarkMode" @toggle:change="isEnableAutoDarkMode = $event"></f7-toggle> <f7-toggle :checked="isEnableAutoDarkMode" @toggle:change="isEnableAutoDarkMode = $event"></f7-toggle>
</f7-list-item> </f7-list-item>
<f7-list-item :title="$t('About')" link="/about" :after="version"></f7-list-item> <f7-list-item :title="$t('About')" link="/about" :after="version"></f7-list-item>
</f7-list> </f7-list>
</f7-card-content>
</f7-card>
</f7-page> </f7-page>
</template> </template>
<script> <script>
export default { export default {
props: [
'f7router'
],
data() { data() {
const self = this; const self = this;
@@ -118,7 +113,7 @@ export default {
return 'v' + this.$version; return 'v' + this.$version;
}, },
allLanguages() { allLanguages() {
return this.$locale.getAllLanguages(); return this.$locale.getAllLanguageInfos();
}, },
allTimezones() { allTimezones() {
return this.$locale.getAllTimezones(true); return this.$locale.getAllTimezones(true);
@@ -223,7 +218,7 @@ export default {
}, },
logout() { logout() {
const self = this; const self = this;
const router = self.$f7router; const router = self.f7router;
self.$confirm('Are you sure you want to log out?', () => { self.$confirm('Are you sure you want to log out?', () => {
self.logouting = true; self.logouting = true;
@@ -234,7 +229,7 @@ export default {
self.$hideLoading(); self.$hideLoading();
self.$settings.clearSettings(); self.$settings.clearSettings();
self.$locale.init(); self.$locale.initLocale();
router.navigate('/'); router.navigate('/');
}).catch(error => { }).catch(error => {
+151 -156
View File
@@ -8,126 +8,112 @@
</f7-nav-right> </f7-nav-right>
</f7-navbar> </f7-navbar>
<f7-card> <f7-list form strong inset dividers class="margin-top">
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-input
<f7-list form> type="text"
<f7-list-input autocomplete="username"
type="text" clear-button
autocomplete="username" :label="$t('Username')"
clear-button :placeholder="$t('Your username')"
:label="$t('Username')" v-model:value="user.username"
:placeholder="$t('Your username')" ></f7-list-input>
:value="user.username"
@input="user.username = $event.target.value"
></f7-list-input>
<f7-list-input <f7-list-input
type="password" type="password"
autocomplete="new-password" autocomplete="new-password"
clear-button clear-button
:label="$t('Password')" :label="$t('Password')"
:placeholder="$t('Your password, at least 6 characters')" :placeholder="$t('Your password, at least 6 characters')"
:value="user.password" v-model:value="user.password"
@input="user.password = $event.target.value" ></f7-list-input>
></f7-list-input>
<f7-list-input <f7-list-input
type="password" type="password"
autocomplete="new-password" autocomplete="new-password"
clear-button clear-button
:label="$t('Confirmation Password')" :label="$t('Confirmation Password')"
:placeholder="$t('Re-enter the password')" :placeholder="$t('Re-enter the password')"
:value="user.confirmPassword" v-model:value="user.confirmPassword"
@input="user.confirmPassword = $event.target.value" ></f7-list-input>
></f7-list-input>
<f7-list-input <f7-list-input
type="email" type="email"
autocomplete="email" autocomplete="email"
clear-button clear-button
:label="$t('E-mail')" :label="$t('E-mail')"
:placeholder="$t('Your email address')" :placeholder="$t('Your email address')"
:value="user.email" v-model:value="user.email"
@input="user.email = $event.target.value" ></f7-list-input>
></f7-list-input>
<f7-list-input <f7-list-input
type="text" type="text"
autocomplete="nickname" autocomplete="nickname"
clear-button clear-button
:label="$t('Nickname')" :label="$t('Nickname')"
:placeholder="$t('Your nickname')" :placeholder="$t('Your nickname')"
:value="user.nickname" v-model:value="user.nickname"
@input="user.nickname = $event.target.value" ></f7-list-input>
></f7-list-input>
<f7-list-item class="ebk-list-item-error-info" v-if="inputIsInvalid" :footer="$t(inputInvalidProblemMessage)"></f7-list-item> <f7-list-item class="ebk-list-item-error-info" v-if="inputIsInvalid" :footer="$t(inputInvalidProblemMessage)"></f7-list-item>
</f7-list> </f7-list>
</f7-card-content>
</f7-card>
<f7-card> <f7-list strong inset dividers>
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-item
<f7-list form> class="list-item-with-header-and-title list-item-no-item-after"
<f7-list-item :key="currentLocale + '_lang'"
class="list-item-with-header-and-title list-item-no-item-after" :header="$t('Language')"
:key="currentLocale + '_lang'" :title="currentLanguageName"
:header="$t('Language')" smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Language'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), pageTitle: $t('Language'), popupCloseLinkText: $t('Done') }"
:title="currentLocale | languageName" >
smart-select :smart-select-params="{ openIn: 'popup', pageTitle: $t('Language'), searchbar: true, searchbarPlaceholder: $t('Language'), searchbarDisableText: $t('Cancel'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }" <select v-model="currentLocale">
> <option :value="locale"
<select v-model="currentLocale"> :key="locale"
<option v-for="(lang, locale) in allLanguages" v-for="(lang, locale) in allLanguages">{{ lang.displayName }}</option>
:key="locale" </select>
:value="locale">{{ lang.displayName }}</option> </f7-list-item>
</select>
</f7-list-item>
<f7-list-item <f7-list-item
class="list-item-with-header-and-title list-item-no-item-after" class="list-item-with-header-and-title list-item-no-item-after"
:key="currentLocale + '_currency'" :key="currentLocale + '_currency'"
:header="$t('Default Currency')" :header="$t('Default Currency')"
smart-select :smart-select-params="{ openIn: 'popup', pageTitle: $t('Default Currency'), searchbar: true, searchbarPlaceholder: $t('Currency Name'), searchbarDisableText: $t('Cancel'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }" smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Currency Name'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), pageTitle: $t('Default Currency'), popupCloseLinkText: $t('Done') }"
> >
<f7-block slot="title" class="no-padding no-margin"> <template #title>
<span>{{ $t(`currency.${user.defaultCurrency}`) }}&nbsp;</span> <f7-block class="no-padding no-margin">
<small class="smaller">{{ user.defaultCurrency }}</small> <span>{{ $t(`currency.${user.defaultCurrency}`) }}&nbsp;</span>
</f7-block> <small class="smaller">{{ user.defaultCurrency }}</small>
<select autocomplete="transaction-currency" v-model="user.defaultCurrency"> </f7-block>
<option v-for="currency in allCurrencies" </template>
:key="currency.code" <select autocomplete="transaction-currency" v-model="user.defaultCurrency">
:value="currency.code">{{ currency.displayName }}</option> <option :value="currency.code"
</select> :key="currency.code"
</f7-list-item> v-for="currency in allCurrencies">{{ currency.displayName }}</option>
</select>
</f7-list-item>
<f7-list-item <f7-list-item
class="list-item-with-header-and-title list-item-no-item-after" class="list-item-with-header-and-title list-item-no-item-after"
:key="currentLocale + '_firstDayOfWeek'" :key="currentLocale + '_firstDayOfWeek'"
:header="$t('First Day of Week')" :header="$t('First Day of Week')"
:title="user.firstDayOfWeek | optionName(allWeekDays, 'type', 'name') | format('datetime.#{value}.long') | localized" :title="getDayOfWeekName(user.firstDayOfWeek)"
smart-select :smart-select-params="{ openIn: 'popup', pageTitle: $t('First Day of Week'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }" smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Date'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), pageTitle: $t('First Day of Week'), popupCloseLinkText: $t('Done') }"
> >
<select v-model="user.firstDayOfWeek"> <select v-model="user.firstDayOfWeek">
<option v-for="weekDay in allWeekDays" <option :value="weekDay.type"
:key="weekDay.type" :key="weekDay.type"
:value="weekDay.type">{{ $t(`datetime.${weekDay.name}.long`) }}</option> v-for="weekDay in allWeekDays">{{ $t(`datetime.${weekDay.name}.long`) }}</option>
</select> </select>
</f7-list-item> </f7-list-item>
</f7-list> </f7-list>
</f7-card-content>
</f7-card>
<f7-card> <f7-list strong inset dividers>
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-item :title="$t('Use preset transaction categories')" link="#" @click="showPresetCategories = true">
<f7-list form> <f7-toggle :checked="usePresetCategories" @toggle:change="usePresetCategories = $event"></f7-toggle>
<f7-list-item :title="$t('Use preset transaction categories')" link="#" @click="showPresetCategories = true"> </f7-list-item>
<f7-toggle :checked="usePresetCategories" @toggle:change="usePresetCategories = $event"></f7-toggle> </f7-list>
</f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-popup :opened="showPresetCategories" @popup:closed="showPresetCategories = false"> <f7-popup push :close-on-escape="false" :opened="showPresetCategories"
@popup:closed="showPresetCategories = false">
<f7-page> <f7-page>
<f7-navbar> <f7-navbar>
<f7-nav-left> <f7-nav-left>
@@ -140,39 +126,32 @@
<f7-link close @click="usePresetCategories = false; showPresetCategories = false" v-if="usePresetCategories">{{ $t('Disable') }}</f7-link> <f7-link close @click="usePresetCategories = false; showPresetCategories = false" v-if="usePresetCategories">{{ $t('Disable') }}</f7-link>
</f7-nav-right> </f7-nav-right>
</f7-navbar> </f7-navbar>
<f7-card v-for="(categories, categoryType) in presetCategories" :key="categoryType"> <f7-block class="no-padding no-margin"
<f7-card-header> :key="categoryType" v-for="(categories, categoryType) in presetCategories">
<small class="card-header-content"> <f7-block-title class="margin-top margin-horizontal">{{ getCategoryTypeName(categoryType) }}</f7-block-title>
<span>{{ categoryType | categoryTypeName($constants.category.allCategoryTypes) | localized }}</span> <f7-list strong inset dividers v-if="showPresetCategories">
</small> <f7-list-item :title="$t('category.' + category.name, currentLocale)"
</f7-card-header> :accordion-item="!!category.subCategories.length"
<f7-card-content class="no-safe-areas" :padding="false"> :key="idx"
<f7-list v-if="showPresetCategories"> v-for="(category, idx) in categories">
<f7-list-item v-for="(category, idx) in categories" <template #media>
:key="idx" <ItemIcon icon-type="category" :icon-id="category.categoryIconId" :color="category.color"></ItemIcon>
:accordion-item="!!category.subCategories.length" </template>
:title="$t('category.' + category.name, currentLocale)">
<f7-icon slot="media"
:icon="category.categoryIconId | categoryIcon"
:style="category.color | categoryIconStyle('var(--default-icon-color)')">
</f7-icon>
<f7-accordion-content v-if="category.subCategories.length" class="padding-left"> <f7-accordion-content v-if="category.subCategories.length" class="padding-left">
<f7-list> <f7-list>
<f7-list-item v-for="(subCategory, subIdx) in category.subCategories" <f7-list-item :title="$t('category.' + subCategory.name, currentLocale)"
:key="subIdx" :key="subIdx"
:title="$t('category.' + subCategory.name, currentLocale)"> v-for="(subCategory, subIdx) in category.subCategories">
<f7-icon slot="media" <template #media>
:icon="subCategory.categoryIconId | categoryIcon" <ItemIcon icon-type="category" :icon-id="subCategory.categoryIconId" :color="subCategory.color"></ItemIcon>
:style="subCategory.color | categoryIconStyle('var(--default-icon-color)')"> </template>
</f7-icon> </f7-list-item>
</f7-list-item> </f7-list>
</f7-list> </f7-accordion-content>
</f7-accordion-content> </f7-list-item>
</f7-list-item> </f7-list>
</f7-list> </f7-block>
</f7-card-content>
</f7-card>
</f7-page> </f7-page>
<f7-actions close-by-outside-click close-on-escape :opened="showPresetCategoriesMoreActionSheet" @actions:closed="showPresetCategoriesMoreActionSheet = false"> <f7-actions close-by-outside-click close-on-escape :opened="showPresetCategoriesMoreActionSheet" @actions:closed="showPresetCategoriesMoreActionSheet = false">
@@ -187,7 +166,7 @@
<list-item-selection-sheet value-type="index" <list-item-selection-sheet value-type="index"
title-field="displayName" title-field="displayName"
:items="allLanguages" :items="allLanguages"
:show.sync="showPresetCategoriesChangeLocaleSheet" v-model:show="showPresetCategoriesChangeLocaleSheet"
v-model="currentLocale"> v-model="currentLocale">
</list-item-selection-sheet> </list-item-selection-sheet>
</f7-popup> </f7-popup>
@@ -196,6 +175,9 @@
<script> <script>
export default { export default {
props: [
'f7router'
],
data() { data() {
const self = this; const self = this;
@@ -223,7 +205,7 @@ export default {
}, },
computed: { computed: {
allLanguages() { allLanguages() {
return this.$locale.getAllLanguages(); return this.$locale.getAllLanguageInfos();
}, },
allCurrencies() { allCurrencies() {
return this.$locale.getAllCurrencies(); return this.$locale.getAllCurrencies();
@@ -250,6 +232,15 @@ export default {
} }
} }
}, },
currentLanguageName() {
const languageInfo = this.$locale.getLanguageInfo(this.currentLocale);
if (!languageInfo) {
return '';
}
return languageInfo.displayName;
},
inputIsEmpty() { inputIsEmpty() {
return !!this.inputEmptyProblemMessage; return !!this.inputEmptyProblemMessage;
}, },
@@ -284,7 +275,7 @@ export default {
methods: { methods: {
submit() { submit() {
const self = this; const self = this;
const router = self.$f7router; const router = self.f7router;
let problemMessage = self.inputEmptyProblemMessage || self.inputInvalidProblemMessage; let problemMessage = self.inputEmptyProblemMessage || self.inputInvalidProblemMessage;
@@ -384,21 +375,25 @@ export default {
self.$toast(error.message || error); self.$toast(error.message || error);
} }
}); });
} },
}, getDayOfWeekName(dayOfWeek) {
filters: { const weekName = this.$utilities.getNameByKeyValue(this.$constants.datetime.allWeekDays, dayOfWeek, 'type', 'name');
categoryTypeName(categoryType, allCategoryTypes) { const i18nWeekNameKey = `datetime.${weekName}.long`;
return this.$t(i18nWeekNameKey);
},
getCategoryTypeName(categoryType) {
switch (categoryType) { switch (categoryType) {
case allCategoryTypes.Income.toString(): case this.$constants.category.allCategoryTypes.Income.toString():
return 'Income Categories'; return this.$t('Income Categories');
case allCategoryTypes.Expense.toString(): case this.$constants.category.allCategoryTypes.Expense.toString():
return 'Expense Categories'; return this.$t('Expense Categories');
case allCategoryTypes.Transfer.toString(): case this.$constants.category.allCategoryTypes.Transfer.toString():
return 'Transfer Categories'; return this.$t('Transfer Categories');
default: default:
return 'Transaction Categories'; return this.$t('Transaction Categories');
} }
} }
} }
}; };
</script> </script>
+32 -28
View File
@@ -1,21 +1,23 @@
<template> <template>
<f7-page no-toolbar no-navbar no-swipeback login-screen> <f7-page no-toolbar no-navbar no-swipeback login-screen>
<f7-login-screen-title> <f7-login-screen-title>
<img class="login-page-logo" src="img/ezbookkeeping-192.png" /> <img alt="logo" class="login-page-logo" src="/img/ezbookkeeping-192.png" />
<f7-block class="margin-vertical-half">{{ $t('global.app.title') }}</f7-block> <f7-block class="margin-vertical-half">{{ $t('global.app.title') }}</f7-block>
</f7-login-screen-title> </f7-login-screen-title>
<f7-list form> <f7-list form>
<f7-list-item-row class="justify-content-center padding-vertical-half"> <f7-list-item class="no-padding no-margin">
{{ $t('Unlock Application') }} <template #inner>
</f7-list-item-row> <div class="display-flex justify-content-center full-line">{{ $t('Unlock Application') }}</div>
<f7-list-item class="list-item-pincode-input"> </template>
<pincode-input secure :length="6" v-model="pinCode" @keyup.native="unlockByPin" /> </f7-list-item>
<f7-list-item class="list-item-pincode-input padding-horizontal margin-horizontal">
<pin-code-input :secure="true" :length="6" v-model="pinCode" @pincode:confirm="unlockByPin" />
</f7-list-item> </f7-list-item>
</f7-list> </f7-list>
<f7-list> <f7-list>
<f7-list-button :class="{ 'disabled': !pinCodeValid }" :text="$t('Unlock By PIN Code')" @click="unlockByPin"></f7-list-button> <f7-list-button :class="{ 'disabled': !isPinCodeValid(pinCode) }" :text="$t('Unlock By PIN Code')" @click="unlockByPin"></f7-list-button>
<f7-list-button v-if="isWebAuthnAvailable" :text="$t('Unlock By Face ID/Touch ID')" @click="unlockByWebAuthn"></f7-list-button> <f7-list-button v-if="isWebAuthnAvailable" :text="$t('Unlock By Face ID/Touch ID')" @click="unlockByWebAuthn"></f7-list-button>
<f7-block-footer> <f7-block-footer>
<f7-link :text="$t('Re-login')" @click="relogin"></f7-link> <f7-link :text="$t('Re-login')" @click="relogin"></f7-link>
@@ -37,15 +39,17 @@
</f7-list> </f7-list>
<f7-popover class="lang-popover-menu"> <f7-popover class="lang-popover-menu">
<f7-list> <f7-list dividers>
<f7-list-item <f7-list-item
link="#" no-chevron popover-close link="#" no-chevron popover-close
v-for="(lang, locale) in allLanguages"
:key="locale"
:title="lang.displayName" :title="lang.displayName"
:key="locale"
v-for="(lang, locale) in allLanguages"
@click="changeLanguage(locale)" @click="changeLanguage(locale)"
> >
<f7-icon slot="after" class="list-item-checked-icon" f7="checkmark_alt" v-if="$i18n.locale === locale"></f7-icon> <template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="$i18n.locale === locale"></f7-icon>
</template>
</f7-list-item> </f7-list-item>
</f7-list> </f7-list>
</f7-popover> </f7-popover>
@@ -54,6 +58,9 @@
<script> <script>
export default { export default {
props: [
'f7router'
],
data() { data() {
return { return {
pinCode: '' pinCode: ''
@@ -64,22 +71,19 @@ export default {
return 'v' + this.$version; return 'v' + this.$version;
}, },
allLanguages() { allLanguages() {
return this.$locale.getAllLanguages(); return this.$locale.getAllLanguageInfos();
}, },
isWebAuthnAvailable() { isWebAuthnAvailable() {
return this.$settings.isEnableApplicationLockWebAuthn() return this.$settings.isEnableApplicationLockWebAuthn()
&& this.$user.getWebAuthnCredentialId() && this.$user.getWebAuthnCredentialId()
&& this.$webauthn.isSupported(); && this.$webauthn.isSupported();
}, },
pinCodeValid() {
return this.pinCode && this.pinCode.length === 6;
},
currentLanguageName() { currentLanguageName() {
const currentLocale = this.$i18n.locale; const currentLocale = this.$i18n.locale;
let lang = this.$locale.getLanguage(currentLocale); let lang = this.$locale.getLanguageInfo(currentLocale);
if (!lang) { if (!lang) {
lang = this.$locale.getLanguage(this.$locale.getDefaultLanguage()); lang = this.$locale.getLanguageInfo(this.$locale.getDefaultLanguage());
} }
return lang.displayName; return lang.displayName;
@@ -88,7 +92,7 @@ export default {
methods: { methods: {
unlockByWebAuthn() { unlockByWebAuthn() {
const self = this; const self = this;
const router = self.$f7router; const router = self.f7router;
if (!self.$settings.isEnableApplicationLockWebAuthn() || !self.$user.getWebAuthnCredentialId()) { if (!self.$settings.isEnableApplicationLockWebAuthn() || !self.$user.getWebAuthnCredentialId()) {
self.$toast('Face ID/Touch ID authentication is not enabled'); self.$toast('Face ID/Touch ID authentication is not enabled');
@@ -131,19 +135,16 @@ export default {
} }
}); });
}, },
unlockByPin() { unlockByPin(pinCode) {
const app = this.$f7; if (!this.isPinCodeValid(pinCode)) {
const $$ = app.$;
if (!this.pinCodeValid) {
return; return;
} }
if ($$('.modal-in').length) { if (this.$ui.isModalShowing()) {
return; return;
} }
const router = this.$f7router; const router = this.f7router;
const user = this.$store.state.currentUserInfo; const user = this.$store.state.currentUserInfo;
if (!user || !user.username) { if (!user || !user.username) {
@@ -152,7 +153,7 @@ export default {
} }
try { try {
this.$user.unlockTokenByPinCode(user.username, this.pinCode); this.$user.unlockTokenByPinCode(user.username, pinCode);
this.$store.dispatch('refreshTokenAndRevokeOldToken'); this.$store.dispatch('refreshTokenAndRevokeOldToken');
if (this.$settings.isAutoUpdateExchangeRatesData()) { if (this.$settings.isAutoUpdateExchangeRatesData()) {
@@ -167,7 +168,7 @@ export default {
}, },
relogin() { relogin() {
const self = this; const self = this;
const router = self.$f7router; const router = self.f7router;
self.$confirm('Are you sure you want to re-login?', () => { self.$confirm('Are you sure you want to re-login?', () => {
self.$user.clearTokenAndUserInfo(true); self.$user.clearTokenAndUserInfo(true);
@@ -175,13 +176,16 @@ export default {
self.$store.dispatch('clearUserInfoState'); self.$store.dispatch('clearUserInfoState');
self.$store.dispatch('resetState'); self.$store.dispatch('resetState');
self.$settings.clearSettings(); self.$settings.clearSettings();
self.$locale.init(); self.$locale.initLocale();
router.navigate('/login', { router.navigate('/login', {
clearPreviousHistory: true clearPreviousHistory: true
}); });
}); });
}, },
isPinCodeValid(pinCode) {
return pinCode && pinCode.length === 6;
},
changeLanguage(locale) { changeLanguage(locale) {
this.$locale.setLanguage(locale); this.$locale.setLanguage(locale);
} }
+380 -304
View File
@@ -9,320 +9,393 @@
</f7-nav-right> </f7-nav-right>
</f7-navbar> </f7-navbar>
<f7-card class="skeleton-text" v-if="loading"> <f7-list strong inset dividers class="margin-vertical skeleton-text" v-if="loading">
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-item class="list-item-with-header-and-title" header="Account Category" title="Category"></f7-list-item>
<f7-list> <f7-list-item class="list-item-with-header-and-title" header="Account Type" title="Account Type"></f7-list-item>
<f7-list-item class="list-item-with-header-and-title" link="#" header="Account Category" title="Category"></f7-list-item> </f7-list>
<f7-list-item class="list-item-with-header-and-title" link="#" header="Account Type" title="Account Type"></f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-card v-else-if="!loading"> <f7-list form strong inset dividers class="margin-vertical" v-else-if="!loading">
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-item
<f7-list form> link="#" no-chevron
<f7-list-item class="list-item-with-header-and-title"
class="list-item-with-header-and-title" :header="$t('Account Category')"
link="#" :title="getAccountCategoryName(account.category)"
:header="$t('Account Category')" @click="showAccountCategorySheet = true"
:title="account.category | optionName(allAccountCategories, 'id', 'name') | localized" >
@click="showAccountCategorySheet = true" <list-item-selection-sheet value-type="item"
> key-field="id" value-field="id" title-field="name"
<list-item-selection-sheet value-type="item" icon-field="defaultAccountIconId" icon-type="account"
key-field="id" value-field="id" title-field="name" :title-i18n="true"
icon-field="defaultAccountIconId" icon-type="account" :items="allAccountCategories"
:title-i18n="true" v-model:show="showAccountCategorySheet"
:items="allAccountCategories" v-model="account.category">
:show.sync="showAccountCategorySheet" </list-item-selection-sheet>
v-model="account.category"> </f7-list-item>
</list-item-selection-sheet>
</f7-list-item>
<f7-list-item <f7-list-item
class="list-item-with-header-and-title" link="#" no-chevron
link="#" class="list-item-with-header-and-title"
:class="{ 'disabled': editAccountId }" :class="{ 'disabled': editAccountId }"
:header="$t('Account Type')" :header="$t('Account Type')"
:title="account.type | optionName(allAccountTypes, 'id', 'name') | localized" :title="this.getAccountTypeName(account.type)"
:no-chevron="!!editAccountId" @click="showAccountTypeSheet = true"
@click="showAccountTypeSheet = true" >
> <list-item-selection-sheet value-type="item"
<list-item-selection-sheet value-type="item" key-field="id" value-field="id" title-field="name"
key-field="id" value-field="id" title-field="name" :items="allAccountTypes"
:items="allAccountTypes" :title-i18n="true"
:title-i18n="true" v-model:show="showAccountTypeSheet"
:show.sync="showAccountTypeSheet" v-model="account.type">
v-model="account.type"> </list-item-selection-sheet>
</list-item-selection-sheet> </f7-list-item>
</f7-list-item> </f7-list>
</f7-list>
</f7-card-content>
</f7-card>
<f7-card class="skeleton-text" v-if="loading"> <f7-list strong inset dividers class="margin-vertical skeleton-text" v-if="loading">
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-input label="Account Name" placeholder="Your account name"></f7-list-input>
<f7-list> <f7-list-item class="list-item-with-header-and-title list-item-with-multi-item">
<f7-list-input label="Account Name" placeholder="Your account name"></f7-list-input> <template #default>
<f7-list-item class="list-item-with-header-and-title" header="Account Icon" link="#"> <div class="grid grid-cols-2">
<f7-block slot="title" class="list-item-custom-title no-padding"> <div class="list-item-subitem no-chevron">
<f7-icon f7="app_fill"></f7-icon> <a class="item-link" href="#">
</f7-block> <div class="item-content">
</f7-list-item> <div class="item-inner">
<f7-list-item class="list-item-with-header-and-title" header="Account Color" link="#"> <div class="item-header">
<f7-block slot="title" class="list-item-custom-title no-padding"> <span>Account Icon</span>
<f7-icon f7="app_fill"></f7-icon> </div>
</f7-block> <div class="item-title">
</f7-list-item> <div class="list-item-custom-title no-padding">
<f7-list-item class="list-item-with-header-and-title list-item-no-item-after" header="Currency" title="Currency" link="#"></f7-list-item> <f7-icon f7="app_fill"></f7-icon>
<f7-list-item class="list-item-with-header-and-title" header="Account Balance" title="Balance" link="#"></f7-list-item> </div>
<f7-list-item class="list-item-toggle" header="Visible" after="True"></f7-list-item> </div>
<f7-list-input label="Description" type="textarea" placeholder="Your account description (optional)"></f7-list-input> </div>
</f7-list> </div>
</f7-card-content> </a>
</f7-card> </div>
<div class="list-item-subitem no-chevron">
<a class="item-link" href="#">
<div class="item-content">
<div class="item-inner">
<div class="item-header">
<span>Account Color</span>
</div>
<div class="item-title">
<div class="list-item-custom-title no-padding">
<f7-icon f7="app_fill"></f7-icon>
</div>
</div>
</div>
</div>
</a>
</div>
</div>
</template>
</f7-list-item>
<f7-list-item class="list-item-with-header-and-title list-item-no-item-after" header="Currency" title="Currency" :link="editAccountId ? null : '#'"></f7-list-item>
<f7-list-item class="list-item-with-header-and-title" header="Account Balance" title="Balance"></f7-list-item>
<f7-list-item class="list-item-toggle" header="Visible" after="True"></f7-list-item>
<f7-list-input label="Description" type="textarea" placeholder="Your account description (optional)"></f7-list-input>
</f7-list>
<f7-card v-else-if="!loading && account.type === $constants.account.allAccountTypes.SingleAccount"> <f7-list form strong inset dividers class="margin-vertical" v-else-if="!loading && account.type === $constants.account.allAccountTypes.SingleAccount">
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-input
<f7-list form> type="text"
<f7-list-input clear-button
type="text" :label="$t('Account Name')"
clear-button :placeholder="$t('Your account name')"
:label="$t('Account Name')" v-model:value="account.name"
:placeholder="$t('Your account name')" ></f7-list-input>
:value="account.name"
@input="account.name = $event.target.value"
></f7-list-input>
<f7-list-item class="list-item-with-header-and-title" <f7-list-item class="list-item-with-header-and-title list-item-with-multi-item">
:header="$t('Account Icon')" link="#" <template #default>
@click="account.showIconSelectionSheet = true"> <div class="grid grid-cols-2">
<f7-block slot="title" class="list-item-custom-title no-padding"> <div class="list-item-subitem no-chevron">
<f7-icon :icon="account.icon | accountIcon" <a class="item-link" href="#" @click="account.showIconSelectionSheet = true">
:style="account.color | accountIconStyle('var(--default-icon-color)')"></f7-icon> <div class="item-content">
</f7-block> <div class="item-inner">
<icon-selection-sheet :all-icon-infos="allAccountIcons" <div class="item-header">
:show.sync="account.showIconSelectionSheet" <span>{{ $t('Account Icon') }}</span>
:color="account.color" </div>
v-model="account.icon" <div class="item-title">
></icon-selection-sheet> <div class="list-item-custom-title no-padding">
</f7-list-item> <ItemIcon icon-type="account" :icon-id="account.icon" :color="account.color"></ItemIcon>
</div>
</div>
</div>
</div>
</a>
<f7-list-item class="list-item-with-header-and-title" <icon-selection-sheet :all-icon-infos="allAccountIcons"
:header="$t('Account Color')" link="#" :color="account.color"
@click="account.showColorSelectionSheet = true"> v-model:show="account.showIconSelectionSheet"
<f7-block slot="title" class="list-item-custom-title no-padding"> v-model="account.icon"
<f7-icon f7="app_fill" ></icon-selection-sheet>
:style="account.color | accountIconStyle('var(--default-icon-color)')"></f7-icon> </div>
</f7-block> <div class="list-item-subitem no-chevron">
<color-selection-sheet :all-color-infos="allAccountColors" <a class="item-link" href="#" @click="account.showColorSelectionSheet = true">
:show.sync="account.showColorSelectionSheet" <div class="item-content">
v-model="account.color" <div class="item-inner">
></color-selection-sheet> <div class="item-header">
</f7-list-item> <span>{{ $t('Account Color') }}</span>
</div>
<div class="item-title">
<div class="list-item-custom-title no-padding">
<ItemIcon icon-type="fixed-f7" icon-id="app_fill" :color="account.color"></ItemIcon>
</div>
</div>
</div>
</div>
</a>
<f7-list-item <color-selection-sheet :all-color-infos="allAccountColors"
class="list-item-with-header-and-title list-item-no-item-after" v-model:show="account.showColorSelectionSheet"
:class="{ 'disabled': editAccountId }" v-model="account.color"
:header="$t('Currency')" ></color-selection-sheet>
:no-chevron="!!editAccountId" </div>
smart-select :smart-select-params="{ openIn: 'popup', searchbar: true, searchbarPlaceholder: $t('Currency Name'), searchbarDisableText: $t('Cancel'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }" </div>
> </template>
<f7-block slot="title" class="no-padding no-margin"> </f7-list-item>
<span>{{ $t(`currency.${account.currency}`) }}&nbsp;</span>
<small class="smaller">{{ account.currency }}</small>
</f7-block>
<select autocomplete="transaction-currency" v-model="account.currency">
<option v-for="currency in allCurrencies"
:key="currency.code"
:value="currency.code">{{ currency.displayName }}</option>
</select>
</f7-list-item>
<f7-list-item <f7-list-item
class="list-item-with-header-and-title" class="list-item-with-header-and-title list-item-no-item-after"
:link="editAccountId ? null : '#'" :class="{ 'disabled': editAccountId }"
:class="{ 'disabled': editAccountId }" :header="$t('Currency')"
:header="$t('Account Balance')" :no-chevron="!!editAccountId"
:title="account.balance | currency(account.currency)" smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Currency Name'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), pageTitle: $t('Currency Name'), popupCloseLinkText: $t('Done') }"
@click="account.showBalanceSheet = true" >
> <template #title>
<number-pad-sheet :min-value="$constants.transaction.minAmount" <div class="no-padding no-margin">
:max-value="$constants.transaction.maxAmount" <span>{{ $t(`currency.${account.currency}`) }}&nbsp;</span>
:show.sync="account.showBalanceSheet" <small class="smaller">{{ account.currency }}</small>
v-model="account.balance" </div>
></number-pad-sheet> </template>
</f7-list-item> <select autocomplete="transaction-currency" v-model="account.currency">
<option :value="currency.code"
:key="currency.code"
v-for="currency in allCurrencies">{{ currency.displayName }}</option>
</select>
</f7-list-item>
<f7-list-item :header="$t('Visible')" v-if="editAccountId"> <f7-list-item
<f7-toggle :checked="account.visible" @toggle:change="account.visible = $event"></f7-toggle> link="#" no-chevron
</f7-list-item> class="list-item-with-header-and-title"
:class="{ 'disabled': editAccountId }"
:header="$t('Account Balance')"
:title="$locale.getDisplayCurrency(account.balance, account.currency)"
@click="account.showBalanceSheet = true"
>
<number-pad-sheet :min-value="$constants.transaction.minAmount"
:max-value="$constants.transaction.maxAmount"
v-model:show="account.showBalanceSheet"
v-model="account.balance"
></number-pad-sheet>
</f7-list-item>
<f7-list-input <f7-list-item :header="$t('Visible')" v-if="editAccountId">
type="textarea" <f7-toggle :checked="account.visible" @toggle:change="account.visible = $event"></f7-toggle>
class="textarea-auto-size" </f7-list-item>
style="height: auto"
:label="$t('Description')"
:placeholder="$t('Your account description (optional)')"
:value="account.comment"
@input="account.comment = $event.target.value"
></f7-list-input>
</f7-list>
</f7-card-content>
</f7-card>
<f7-card v-else-if="!loading && account.type === $constants.account.allAccountTypes.MultiSubAccounts"> <f7-list-input
<f7-card-content class="no-safe-areas" :padding="false"> type="textarea"
<f7-list form> style="height: auto"
<f7-list-input :label="$t('Description')"
type="text" :placeholder="$t('Your account description (optional)')"
clear-button v-textarea-auto-size
:label="$t('Account Name')" v-model:value="account.comment"
:placeholder="$t('Your account name')" ></f7-list-input>
:value="account.name" </f7-list>
@input="account.name = $event.target.value"
></f7-list-input>
<f7-list-item class="list-item-with-header-and-title" <f7-list form strong inset dividers class="margin-vertical" v-else-if="!loading && account.type === $constants.account.allAccountTypes.MultiSubAccounts">
:header="$t('Account Icon')" link="#" <f7-list-input
@click="account.showIconSelectionSheet = true"> type="text"
<f7-block slot="title" class="list-item-custom-title no-padding"> clear-button
<f7-icon :icon="account.icon | accountIcon" :label="$t('Account Name')"
:style="account.color | accountIconStyle('var(--default-icon-color)')"></f7-icon> :placeholder="$t('Your account name')"
</f7-block> v-model:value="account.name"
<icon-selection-sheet :all-icon-infos="allAccountIcons" ></f7-list-input>
:show.sync="account.showIconSelectionSheet"
:color="account.color"
v-model="account.icon"
></icon-selection-sheet>
</f7-list-item>
<f7-list-item class="list-item-with-header-and-title" <f7-list-item class="list-item-with-header-and-title list-item-with-multi-item">
:header="$t('Account Color')" link="#" <template #default>
@click="account.showColorSelectionSheet = true"> <div class="grid grid-cols-2">
<f7-block slot="title" class="list-item-custom-title no-padding"> <div class="list-item-subitem no-chevron">
<f7-icon f7="app_fill" <a class="item-link" href="#" @click="account.showIconSelectionSheet = true">
:style="account.color | accountIconStyle('var(--default-icon-color)')"></f7-icon> <div class="item-content">
</f7-block> <div class="item-inner">
<color-selection-sheet :all-color-infos="allAccountColors" <div class="item-header">
:show.sync="account.showColorSelectionSheet" <span>{{ $t('Account Icon') }}</span>
v-model="account.color" </div>
></color-selection-sheet> <div class="item-title">
</f7-list-item> <div class="list-item-custom-title no-padding">
<ItemIcon icon-type="account" :icon-id="account.icon" :color="account.color"></ItemIcon>
</div>
</div>
</div>
</div>
</a>
<f7-list-item :header="$t('Visible')" v-if="editAccountId"> <icon-selection-sheet :all-icon-infos="allAccountIcons"
<f7-toggle :checked="account.visible" @toggle:change="account.visible = $event"></f7-toggle> :color="account.color"
</f7-list-item> v-model:show="account.showIconSelectionSheet"
v-model="account.icon"
></icon-selection-sheet>
</div>
<div class="list-item-subitem no-chevron">
<a class="item-link" href="#" @click="account.showColorSelectionSheet = true">
<div class="item-content">
<div class="item-inner">
<div class="item-header">
<span>{{ $t('Account Color') }}</span>
</div>
<div class="item-title">
<div class="list-item-custom-title no-padding">
<ItemIcon icon-type="fixed-f7" icon-id="app_fill" :color="account.color"></ItemIcon>
</div>
</div>
</div>
</div>
</a>
<f7-list-input <color-selection-sheet :all-color-infos="allAccountColors"
type="textarea" v-model:show="account.showColorSelectionSheet"
class="textarea-auto-size" v-model="account.color"
style="height: auto" ></color-selection-sheet>
:label="$t('Description')" </div>
:placeholder="$t('Your account description (optional)')" </div>
:value="account.comment" </template>
@input="account.comment = $event.target.value" </f7-list-item>
></f7-list-input>
</f7-list> <f7-list-item :header="$t('Visible')" v-if="editAccountId">
</f7-card-content> <f7-toggle :checked="account.visible" @toggle:change="account.visible = $event"></f7-toggle>
</f7-card> </f7-list-item>
<f7-list-input
type="textarea"
style="height: auto"
:label="$t('Description')"
:placeholder="$t('Your account description (optional)')"
v-textarea-auto-size
v-model:value="account.comment"
></f7-list-input>
</f7-list>
<f7-block class="no-padding no-margin" v-if="!loading && account.type === $constants.account.allAccountTypes.MultiSubAccounts"> <f7-block class="no-padding no-margin" v-if="!loading && account.type === $constants.account.allAccountTypes.MultiSubAccounts">
<f7-card v-for="(subAccount, idx) in subAccounts" :key="idx"> <f7-list strong inset dividers class="subaccount-edit-list margin-vertical"
<f7-card-header> :key="idx"
<small class="subaccount-header-content">{{ $t('Sub Account') + ' #' + (idx + 1) }}</small> v-for="(subAccount, idx) in subAccounts">
<f7-button rasied fill color="red" icon-f7="trash" icon-size="16px" <f7-list-item group-title>
<small>{{ $t('Sub Account') + ' #' + (idx + 1) }}</small>
<f7-button rasied fill class="subaccount-delete-button" color="red" icon-f7="trash" icon-size="16px"
:tooltip="$t('Remove Sub Account')" :tooltip="$t('Remove Sub Account')"
v-if="!editAccountId" v-if="!editAccountId"
@click="removeSubAccount(subAccount, false)"> @click="removeSubAccount(subAccount, false)">
</f7-button> </f7-button>
</f7-card-header> </f7-list-item>
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-input
type="text"
clear-button
:label="$t('Sub Account Name')"
:placeholder="$t('Your sub account name')"
:value="subAccount.name"
@input="subAccount.name = $event.target.value"
></f7-list-input>
<f7-list-item class="list-item-with-header-and-title" <f7-list-input
:header="$t('Sub Account Icon')" link="#" type="text"
@click="subAccount.showIconSelectionSheet = true"> clear-button
<f7-block slot="title" class="list-item-custom-title no-padding"> :label="$t('Sub Account Name')"
<f7-icon :icon="subAccount.icon | accountIcon" :placeholder="$t('Your sub account name')"
:style="subAccount.color | accountIconStyle('var(--default-icon-color)')"></f7-icon> v-model:value="subAccount.name"
</f7-block> ></f7-list-input>
<icon-selection-sheet :all-icon-infos="allAccountIcons"
:show.sync="subAccount.showIconSelectionSheet"
:color="subAccount.color"
v-model="subAccount.icon"
></icon-selection-sheet>
</f7-list-item>
<f7-list-item class="list-item-with-header-and-title" <f7-list-item class="list-item-with-header-and-title list-item-with-multi-item">
:header="$t('Sub Account Color')" link="#" <template #default>
@click="subAccount.showColorSelectionSheet = true"> <div class="grid grid-cols-2">
<f7-block slot="title" class="list-item-custom-title no-padding"> <div class="list-item-subitem no-chevron">
<f7-icon f7="app_fill" <a class="item-link" href="#" @click="subAccount.showIconSelectionSheet = true">
:style="subAccount.color | accountIconStyle('var(--default-icon-color)')"></f7-icon> <div class="item-content">
</f7-block> <div class="item-inner">
<color-selection-sheet :all-color-infos="allAccountColors" <div class="item-header">
:show.sync="subAccount.showColorSelectionSheet" <span>{{ $t('Sub Account Icon') }}</span>
v-model="subAccount.color" </div>
></color-selection-sheet> <div class="item-title">
</f7-list-item> <div class="list-item-custom-title no-padding">
<ItemIcon icon-type="account" :icon-id="subAccount.icon" :color="subAccount.color"></ItemIcon>
</div>
</div>
</div>
</div>
</a>
<f7-list-item <icon-selection-sheet :all-icon-infos="allAccountIcons"
class="list-item-with-header-and-title list-item-no-item-after" :color="subAccount.color"
:class="{ 'disabled': editAccountId }" v-model:show="subAccount.showIconSelectionSheet"
:header="$t('Currency')" v-model="subAccount.icon"
:no-chevron="!!editAccountId" ></icon-selection-sheet>
smart-select :smart-select-params="{ openIn: 'popup', searchbar: true, searchbarPlaceholder: $t('Currency Name'), searchbarDisableText: $t('Cancel'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }" </div>
> <div class="list-item-subitem no-chevron">
<f7-block slot="title" class="no-padding no-margin"> <a class="item-link" href="#" @click="subAccount.showColorSelectionSheet = true">
<span>{{ $t(`currency.${subAccount.currency}`) }}&nbsp;</span> <div class="item-content">
<small class="smaller">{{ subAccount.currency }}</small> <div class="item-inner">
</f7-block> <div class="item-header">
<select autocomplete="transaction-currency" v-model="subAccount.currency"> <span>{{ $t('Sub Account Color') }}</span>
<option v-for="currency in allCurrencies" </div>
:key="currency.code" <div class="item-title">
:value="currency.code">{{ currency.displayName }}</option> <div class="list-item-custom-title no-padding">
</select> <ItemIcon icon-type="fixed-f7" icon-id="app_fill" :color="subAccount.color"></ItemIcon>
</f7-list-item> </div>
</div>
</div>
</div>
</a>
<f7-list-item <color-selection-sheet :all-color-infos="allAccountColors"
class="list-item-with-header-and-title" v-model:show="subAccount.showColorSelectionSheet"
:link="editAccountId ? null : '#'" v-model="subAccount.color"
:class="{ 'disabled': editAccountId }" ></color-selection-sheet>
:header="$t('Sub Account Balance')" </div>
:title="subAccount.balance | currency(subAccount.currency)" </div>
@click="subAccount.showBalanceSheet = true" </template>
> </f7-list-item>
<number-pad-sheet :min-value="$constants.transaction.minAmount"
:max-value="$constants.transaction.maxAmount"
:show.sync="subAccount.showBalanceSheet"
v-model="subAccount.balance"
></number-pad-sheet>
</f7-list-item>
<f7-list-item :header="$t('Visible')" v-if="editAccountId"> <f7-list-item
<f7-toggle :checked="subAccount.visible" @toggle:change="subAccount.visible = $event"></f7-toggle> class="list-item-with-header-and-title list-item-no-item-after"
</f7-list-item> :class="{ 'disabled': editAccountId }"
:header="$t('Currency')"
:no-chevron="!!editAccountId"
smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Currency Name'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), pageTitle: $t('Currency Name'), popupCloseLinkText: $t('Done') }"
>
<template #title>
<div class="no-padding no-margin">
<span>{{ $t(`currency.${subAccount.currency}`) }}&nbsp;</span>
<small class="smaller">{{ subAccount.currency }}</small>
</div>
</template>
<select autocomplete="transaction-currency" v-model="subAccount.currency">
<option :value="currency.code"
:key="currency.code"
v-for="currency in allCurrencies">{{ currency.displayName }}</option>
</select>
</f7-list-item>
<f7-list-input <f7-list-item
type="textarea" link="#" no-chevron
class="textarea-auto-size" class="list-item-with-header-and-title"
style="height: auto" :class="{ 'disabled': editAccountId }"
:label="$t('Description')" :header="$t('Sub Account Balance')"
:placeholder="$t('Your sub account description (optional)')" :title="$locale.getDisplayCurrency(subAccount.balance, subAccount.currency)"
:value="subAccount.comment" @click="subAccount.showBalanceSheet = true"
@input="subAccount.comment = $event.target.value" >
></f7-list-input> <number-pad-sheet :min-value="$constants.transaction.minAmount"
</f7-list> :max-value="$constants.transaction.maxAmount"
</f7-card-content> v-model:show="subAccount.showBalanceSheet"
</f7-card> v-model="subAccount.balance"
></number-pad-sheet>
</f7-list-item>
<f7-list-item :header="$t('Visible')" v-if="editAccountId">
<f7-toggle :checked="subAccount.visible" @toggle:change="subAccount.visible = $event"></f7-toggle>
</f7-list-item>
<f7-list-input
type="textarea"
style="height: auto"
:label="$t('Description')"
:placeholder="$t('Your sub account description (optional)')"
v-textarea-auto-size
v-model:value="subAccount.comment"
></f7-list-input>
</f7-list>
</f7-block> </f7-block>
<f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false"> <f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false">
@@ -348,6 +421,10 @@
<script> <script>
export default { export default {
props: [
'f7route',
'f7router'
],
data() { data() {
const self = this; const self = this;
@@ -427,7 +504,7 @@ export default {
}, },
created() { created() {
const self = this; const self = this;
const query = self.$f7route.query; const query = self.f7route.query;
if (query.id) { if (query.id) {
self.loading = true; self.loading = true;
@@ -484,12 +561,9 @@ export default {
self.loading = false; self.loading = false;
} }
}, },
updated: function () {
this.autoChangeCommentTextareaSize();
},
methods: { methods: {
onPageAfterIn() { onPageAfterIn() {
this.$routeBackOnError('loadingError'); this.$routeBackOnError(this.f7router, 'loadingError');
}, },
addSubAccount() { addSubAccount() {
const self = this; const self = this;
@@ -536,7 +610,7 @@ export default {
}, },
save() { save() {
const self = this; const self = this;
const router = self.$f7router; const router = self.f7router;
let problemMessage = self.getInputEmptyProblemMessage(self.account, false); let problemMessage = self.getInputEmptyProblemMessage(self.account, false);
@@ -622,15 +696,13 @@ export default {
} }
}); });
}, },
autoChangeCommentTextareaSize() { getAccountTypeName(accountType) {
const app = this.$f7; const typeName = this.$utilities.getNameByKeyValue(this.allAccountTypes, accountType, 'id', 'name');
const $$ = app.$; return this.$t(typeName);
},
$$('.textarea-auto-size textarea').each((idx, el) => { getAccountCategoryName(accountCategory) {
el.scrollTop = 0; const categoryName = this.$utilities.getNameByKeyValue(this.allAccountCategories, accountCategory, 'id', 'name');
el.style.height = ''; return this.$t(categoryName);
el.style.height = el.scrollHeight + 'px';
});
}, },
chooseSuitableIcon(oldCategory, newCategory) { chooseSuitableIcon(oldCategory, newCategory) {
const allCategories = this.$constants.account.allCategories; const allCategories = this.$constants.account.allCategories;
@@ -688,7 +760,11 @@ export default {
</script> </script>
<style> <style>
.subaccount-header-content { .subaccount-edit-list {
opacity: 0.6; --f7-list-group-title-height: 40px;
}
.subaccount-delete-button {
margin-left: auto;
} }
</style> </style>
+115 -154
View File
@@ -18,7 +18,7 @@
</p> </p>
<p class="no-margin"> <p class="no-margin">
<span class="net-assets" v-if="loading">0.00 USD</span> <span class="net-assets" v-if="loading">0.00 USD</span>
<span class="net-assets" v-else-if="!loading">{{ netAssets | currency(defaultCurrency) }}</span> <span class="net-assets" v-else-if="!loading">{{ $locale.getDisplayCurrency(netAssets, defaultCurrency) }}</span>
<f7-link class="margin-left-half" @click="toggleShowAccountBalance()"> <f7-link class="margin-left-half" @click="toggleShowAccountBalance()">
<f7-icon :f7="showAccountBalance ? 'eye_slash_fill' : 'eye_fill'" size="18px"></f7-icon> <f7-icon :f7="showAccountBalance ? 'eye_slash_fill' : 'eye_fill'" size="18px"></f7-icon>
</f7-link> </f7-link>
@@ -29,155 +29,106 @@
</small> </small>
<small class="account-overview-info" v-else-if="!loading"> <small class="account-overview-info" v-else-if="!loading">
<span>{{ $t('Total assets') }}</span> <span>{{ $t('Total assets') }}</span>
<span>{{ totalAssets | currency(defaultCurrency) }}</span> <span>{{ $locale.getDisplayCurrency(totalAssets, defaultCurrency) }}</span>
<span>|</span> <span>|</span>
<span>{{ $t('Total liabilities') }}</span> <span>{{ $t('Total liabilities') }}</span>
<span>{{ totalLiabilities | currency(defaultCurrency) }}</span> <span>{{ $locale.getDisplayCurrency(totalLiabilities, defaultCurrency) }}</span>
</small> </small>
</p> </p>
</f7-card-header> </f7-card-header>
</f7-card> </f7-card>
<f7-card class="skeleton-text" v-if="loading"> <f7-list strong inset dividers class="account-list margin-vertical skeleton-text"
<f7-card-header> :key="listIdx" v-for="listIdx in [ 1, 2, 3 ]" v-if="loading">
<small class="card-header-content">Account Category</small> <f7-list-item group-title>
</f7-card-header> <small>Account Category</small>
<f7-card-content class="no-safe-areas" :padding="false"> </f7-list-item>
<f7-list> <f7-list-item class="nested-list-item" after="0.00 USD" link="#"
<f7-list-item class="nested-list-item" after="0.00 USD" link="#"> :key="itemIdx" v-for="itemIdx in (listIdx === 1 ? [ 1 ] : [ 1, 2 ])">
<f7-block slot="title" class="no-padding"> <template #title>
<div class="display-flex padding-top-half padding-bottom-half"> <div class="display-flex padding-top-half padding-bottom-half">
<f7-icon slot="media" f7="app_fill"></f7-icon> <f7-icon f7="app_fill"></f7-icon>
<div class="nested-list-item-title">Account Name</div> <div class="nested-list-item-title">Account Name</div>
</div> </div>
</f7-block> </template>
</f7-list-item> </f7-list-item>
</f7-list> </f7-list>
</f7-card-content>
</f7-card>
<f7-card class="skeleton-text" v-if="loading"> <f7-list strong inset dividers class="margin-vertical" v-if="!loading && noAvailableAccount">
<f7-card-header> <f7-list-item :title="$t('No available account')"></f7-list-item>
<small class="card-header-content">Account Category 2</small> </f7-list>
</f7-card-header>
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item class="nested-list-item" after="0.00 USD" link="#">
<f7-block slot="title" class="no-padding">
<div class="display-flex padding-top-half padding-bottom-half">
<f7-icon slot="media" f7="app_fill"></f7-icon>
<div class="nested-list-item-title">Account Name</div>
</div>
</f7-block>
</f7-list-item>
<f7-list-item class="nested-list-item" after="0.00 USD" link="#">
<f7-block slot="title" class="no-padding">
<div class="display-flex padding-top-half padding-bottom-half">
<f7-icon slot="media" f7="app_fill"></f7-icon>
<div class="nested-list-item-title">Account Name 2</div>
</div>
</f7-block>
</f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-card class="skeleton-text" v-if="loading"> <div :key="accountCategory.id"
<f7-card-header> v-for="accountCategory in allAccountCategories"
<small class="card-header-content">Account Category 3</small> v-show="(showHidden && hasAccount(accountCategory, false)) || hasAccount(accountCategory, true)">
</f7-card-header> <f7-list strong inset dividers sortable class="account-list margin-vertical"
<f7-card-content class="no-safe-areas" :padding="false"> :sortable-enabled="sortable"
<f7-list> v-if="categorizedAccounts[accountCategory.id]"
<f7-list-item class="nested-list-item" after="0.00 USD" link="#"> @sortable:sort="onSort">
<f7-block slot="title" class="no-padding"> <f7-list-item group-title :sortable="false">
<div class="display-flex padding-top-half padding-bottom-half"> <small>
<f7-icon slot="media" f7="app_fill"></f7-icon> <span>{{ $t(accountCategory.name) }}</span>
<div class="nested-list-item-title">Account Name</div> <span style="margin-left: 10px">{{ $locale.getDisplayCurrency(accountCategoryTotalBalance(accountCategory), defaultCurrency) }}</span>
</small>
</f7-list-item>
<f7-list-item swipeout
:id="getAccountDomId(account)"
:class="{ 'nested-list-item': true, 'has-child-list-item': account.type === $constants.account.allAccountTypes.MultiSubAccounts }"
:after="$locale.getDisplayCurrency(accountBalance(account), account.currency)"
:link="!sortable ? '/transaction/list?accountId=' + account.id : null"
:key="account.id"
v-for="account in categorizedAccounts[accountCategory.id].accounts"
v-show="showHidden || !account.hidden"
@taphold="setSortable()"
>
<template #title>
<div class="display-flex padding-top-half padding-bottom-half">
<ItemIcon icon-type="account" :icon-id="account.icon" :color="account.color">
<f7-badge color="gray" class="right-bottom-icon" v-if="account.hidden">
<f7-icon f7="eye_slash_fill"></f7-icon>
</f7-badge>
</ItemIcon>
<div class="nested-list-item-title">
<span>{{ account.name }}</span>
<div class="item-footer" v-if="account.comment">{{ account.comment }}</div>
</div> </div>
</f7-block> </div>
</f7-list-item> <li v-if="account.type === $constants.account.allAccountTypes.MultiSubAccounts">
<f7-list-item class="nested-list-item" after="0.00 USD" link="#"> <ul class="no-padding">
<f7-block slot="title" class="no-padding"> <f7-list-item class="no-sortable nested-list-item-child"
<div class="display-flex padding-top-half padding-bottom-half"> :id="getAccountDomId(subAccount)"
<f7-icon slot="media" f7="app_fill"></f7-icon> :title="subAccount.name" :footer="subAccount.comment" :after="$locale.getDisplayCurrency(accountBalance(subAccount), subAccount.currency)"
<div class="nested-list-item-title">Account Name 2</div> :link="!sortable ? '/transaction/list?accountId=' + subAccount.id : null"
</div> :key="subAccount.id"
</f7-block> v-for="subAccount in account.subAccounts"
</f7-list-item> v-show="showHidden || !subAccount.hidden"
</f7-list> >
</f7-card-content> <template #media>
</f7-card> <ItemIcon icon-type="account" :icon-id="subAccount.icon" :color="subAccount.color">
<f7-card v-if="!loading && noAvailableAccount">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item :title="$t('No available account')"></f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-card v-for="accountCategory in allAccountCategories" :key="accountCategory.id" v-show="(showHidden && hasAccount(accountCategory, false)) || hasAccount(accountCategory, true)">
<f7-card-header>
<small class="card-header-content">
<span>{{ $t(accountCategory.name) }}</span>
<span style="margin-left: 10px">{{ accountCategoryTotalBalance(accountCategory) | currency(defaultCurrency) }}</span>
</small>
</f7-card-header>
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list class="account-list" sortable :sortable-enabled="sortable" @sortable:sort="onSort" v-if="categorizedAccounts[accountCategory.id]">
<f7-list-item v-for="account in categorizedAccounts[accountCategory.id].accounts" v-show="showHidden || !account.hidden"
:key="account.id" :id="account | accountDomId"
:class="{ 'nested-list-item': true, 'has-child-list-item': account.type === $constants.account.allAccountTypes.MultiSubAccounts }"
:after="accountBalance(account) | currency(account.currency)"
:link="!sortable ? '/transaction/list?accountId=' + account.id : null"
swipeout @taphold.native="setSortable()"
>
<f7-block slot="title" class="no-padding">
<div class="display-flex padding-top-half padding-bottom-half">
<f7-icon class="align-self-center" slot="media" :icon="account.icon | accountIcon"
:style="account.color | accountIconStyle('var(--default-icon-color)')">
<f7-badge color="gray" class="right-bottom-icon" v-if="account.hidden">
<f7-icon f7="eye_slash_fill"></f7-icon>
</f7-badge>
</f7-icon>
<div class="nested-list-item-title">
<span>{{ account.name }}</span>
<div class="item-footer" slot="footer" v-if="account.comment">{{ account.comment }}</div>
</div>
</div>
<li v-if="account.type === $constants.account.allAccountTypes.MultiSubAccounts">
<ul class="no-padding">
<f7-list-item class="no-sortable nested-list-item-child" v-for="subAccount in account.subAccounts" v-show="showHidden || !subAccount.hidden"
:key="subAccount.id" :id="subAccount | accountDomId"
:title="subAccount.name" :footer="subAccount.comment" :after="accountBalance(subAccount) | currency(subAccount.currency)"
:link="!sortable ? '/transaction/list?accountId=' + subAccount.id : null"
>
<f7-icon slot="media" :icon="subAccount.icon | accountIcon"
:style="subAccount.color | accountIconStyle('var(--default-icon-color)')">
<f7-badge color="gray" class="right-bottom-icon" v-if="subAccount.hidden"> <f7-badge color="gray" class="right-bottom-icon" v-if="subAccount.hidden">
<f7-icon f7="eye_slash_fill"></f7-icon> <f7-icon f7="eye_slash_fill"></f7-icon>
</f7-badge> </f7-badge>
</f7-icon> </ItemIcon>
</f7-list-item> </template>
</ul> </f7-list-item>
</li> </ul>
</f7-block> </li>
<f7-swipeout-actions left v-if="sortable"> </template>
<f7-swipeout-button :color="account.hidden ? 'blue' : 'gray'" class="padding-left padding-right" <f7-swipeout-actions left v-if="sortable">
overswipe close @click="hide(account, !account.hidden)"> <f7-swipeout-button :color="account.hidden ? 'blue' : 'gray'" class="padding-left padding-right"
<f7-icon :f7="account.hidden ? 'eye' : 'eye_slash'"></f7-icon> overswipe close @click="hide(account, !account.hidden)">
</f7-swipeout-button> <f7-icon :f7="account.hidden ? 'eye' : 'eye_slash'"></f7-icon>
</f7-swipeout-actions> </f7-swipeout-button>
<f7-swipeout-actions right v-if="!sortable"> </f7-swipeout-actions>
<f7-swipeout-button color="orange" close :text="$t('Edit')" @click="edit(account)"></f7-swipeout-button> <f7-swipeout-actions right v-if="!sortable">
<f7-swipeout-button color="red" class="padding-left padding-right" @click="remove(account, false)"> <f7-swipeout-button color="orange" close :text="$t('Edit')" @click="edit(account)"></f7-swipeout-button>
<f7-icon f7="trash"></f7-icon> <f7-swipeout-button color="red" class="padding-left padding-right" @click="remove(account, false)">
</f7-swipeout-button> <f7-icon f7="trash"></f7-icon>
</f7-swipeout-actions> </f7-swipeout-button>
</f7-list-item> </f7-swipeout-actions>
</f7-list> </f7-list-item>
</f7-card-content> </f7-list>
</f7-card> </div>
<f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false"> <f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false">
<f7-actions-group> <f7-actions-group>
@@ -204,6 +155,9 @@
<script> <script>
export default { export default {
props: [
'f7router'
],
data() { data() {
return { return {
loading: true, loading: true,
@@ -353,7 +307,7 @@ export default {
this.reload(null); this.reload(null);
} }
this.$routeBackOnError('loadingError'); this.$routeBackOnError(this.f7router, 'loadingError');
}, },
reload(done) { reload(done) {
if (this.sortable) { if (this.sortable) {
@@ -473,17 +427,22 @@ export default {
onSort(event) { onSort(event) {
const self = this; const self = this;
if (!event || !event.el || !event.el.id || event.el.id.indexOf('account_') !== 0) { if (!event || !event.el || !event.el.id) {
self.$toast('Unable to move account'); self.$toast('Unable to move account');
return; return;
} }
const id = event.el.id.substring(8); // account_ const id = self.parseAccountIdFromDomId(event.el.id);
if (!id) {
self.$toast('Unable to move account');
return;
}
self.$store.dispatch('changeAccountDisplayOrder', { self.$store.dispatch('changeAccountDisplayOrder', {
accountId: id, accountId: id,
from: event.from, from: event.from - 1, // first item in the list is title, so the index need minus one
to: event.to to: event.to - 1
}).then(() => { }).then(() => {
self.displayOrderModified = true; self.displayOrderModified = true;
}).catch(error => { }).catch(error => {
@@ -519,7 +478,7 @@ export default {
}); });
}, },
edit(account) { edit(account) {
this.$f7router.navigate('/account/edit?id=' + account.id); this.f7router.navigate('/account/edit?id=' + account.id);
}, },
hide(account, hidden) { hide(account, hidden) {
const self = this; const self = this;
@@ -541,8 +500,6 @@ export default {
}, },
remove(account, confirm) { remove(account, confirm) {
const self = this; const self = this;
const app = self.$f7;
const $$ = app.$;
if (!account) { if (!account) {
self.$alert('An error has occurred'); self.$alert('An error has occurred');
@@ -562,9 +519,7 @@ export default {
self.$store.dispatch('deleteAccount', { self.$store.dispatch('deleteAccount', {
account: account, account: account,
beforeResolve: (done) => { beforeResolve: (done) => {
app.swipeout.delete($$(`#${self.$options.filters.accountDomId(account)}`), () => { self.$ui.onSwipeoutDeleted(self.getAccountDomId(account), done);
done();
});
} }
}).then(() => { }).then(() => {
self.$hideLoading(); self.$hideLoading();
@@ -575,11 +530,16 @@ export default {
self.$toast(error.message || error); self.$toast(error.message || error);
} }
}); });
} },
}, getAccountDomId(account) {
filters: {
accountDomId(account) {
return 'account_' + account.id; return 'account_' + account.id;
},
parseAccountIdFromDomId(domId) {
if (!domId || domId.indexOf('account_') !== 0) {
return null;
}
return domId.substring(8); // account_
} }
} }
}; };
@@ -590,11 +550,11 @@ export default {
background-color: var(--f7-color-yellow); background-color: var(--f7-color-yellow);
} }
.theme-dark .account-overview-card { .dark .account-overview-card {
background-color: var(--f7-theme-color); background-color: var(--f7-theme-color);
} }
.theme-dark .account-overview-card a { .dark .account-overview-card a {
color: var(--f7-text-color); color: var(--f7-text-color);
opacity: 0.6; opacity: 0.6;
} }
@@ -616,6 +576,7 @@ export default {
} }
.account-list { .account-list {
--f7-list-group-title-height: 36px;
--f7-list-item-footer-font-size: 13px; --f7-list-item-footer-font-size: 13px;
} }
+14 -19
View File
@@ -2,31 +2,26 @@
<f7-page ptr @ptr:refresh="reload" @page:afterin="onPageAfterIn"> <f7-page ptr @ptr:refresh="reload" @page:afterin="onPageAfterIn">
<f7-navbar :title="$t('Transaction Categories')" :back-link="$t('Back')"></f7-navbar> <f7-navbar :title="$t('Transaction Categories')" :back-link="$t('Back')"></f7-navbar>
<f7-card class="skeleton-text" v-if="loading"> <f7-list strong inset dividers class="margin-top skeleton-text" v-if="loading">
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-item title="Expense" link="#"></f7-list-item>
<f7-list> <f7-list-item title="Income" link="#"></f7-list-item>
<f7-list-item title="Expense" link="#"></f7-list-item> <f7-list-item title="Transfer" link="#"></f7-list-item>
<f7-list-item title="Income" link="#"></f7-list-item> </f7-list>
<f7-list-item title="Transfer" link="#"></f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-card v-else-if="!loading"> <f7-list strong inset dividers class="margin-top" v-else-if="!loading">
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-item :title="$t('Expense')" link="/category/list?type=2"></f7-list-item>
<f7-list> <f7-list-item :title="$t('Income')" link="/category/list?type=1"></f7-list-item>
<f7-list-item :title="$t('Expense')" link="/category/list?type=2"></f7-list-item> <f7-list-item :title="$t('Transfer')" link="/category/list?type=3"></f7-list-item>
<f7-list-item :title="$t('Income')" link="/category/list?type=1"></f7-list-item> </f7-list>
<f7-list-item :title="$t('Transfer')" link="/category/list?type=3"></f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
</f7-page> </f7-page>
</template> </template>
<script> <script>
export default { export default {
props: [
'f7router'
],
data() { data() {
return { return {
loading: true, loading: true,
@@ -53,7 +48,7 @@ export default {
}, },
methods: { methods: {
onPageAfterIn() { onPageAfterIn() {
this.$routeBackOnError('loadingError'); this.$routeBackOnError(this.f7router, 'loadingError');
}, },
reload(done) { reload(done) {
const self = this; const self = this;
+119 -89
View File
@@ -8,91 +8,134 @@
</f7-nav-right> </f7-nav-right>
</f7-navbar> </f7-navbar>
<f7-card class="skeleton-text" v-if="loading"> <f7-list strong inset dividers class="margin-top skeleton-text" v-if="loading">
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-input label="Category Name" placeholder="Your category name"></f7-list-input>
<f7-list> <f7-list-item class="list-item-with-header-and-title list-item-with-multi-item">
<f7-list-input label="Category Name" placeholder="Your category name"></f7-list-input> <template #default>
<f7-list-item class="list-item-with-header-and-title" header="Category Icon" link="#"> <div class="grid grid-cols-2">
<f7-block slot="title" class="list-item-custom-title no-padding"> <div class="list-item-subitem no-chevron">
<f7-icon f7="app_fill"></f7-icon> <a class="item-link" href="#">
</f7-block> <div class="item-content">
</f7-list-item> <div class="item-inner">
<f7-list-item class="list-item-with-header-and-title" header="Category Color" link="#"> <div class="item-header">
<f7-block slot="title" class="list-item-custom-title no-padding"> <span>Category Icon</span>
<f7-icon f7="app_fill"></f7-icon> </div>
</f7-block> <div class="item-title">
</f7-list-item> <div class="list-item-custom-title no-padding">
<f7-list-item class="list-item-toggle" header="Visible" after="True"></f7-list-item> <f7-icon f7="app_fill"></f7-icon>
<f7-list-input label="Description" type="textarea" placeholder="Your category description (optional)"></f7-list-input> </div>
</f7-list> </div>
</f7-card-content> </div>
</f7-card> </div>
</a>
</div>
<div class="list-item-subitem no-chevron">
<a class="item-link" href="#">
<div class="item-content">
<div class="item-inner">
<div class="item-header">
<span>Category Color</span>
</div>
<div class="item-title">
<div class="list-item-custom-title no-padding">
<f7-icon f7="app_fill"></f7-icon>
</div>
</div>
</div>
</div>
</a>
</div>
</div>
</template>
</f7-list-item>
<f7-list-item class="list-item-toggle" header="Visible" after="True"></f7-list-item>
<f7-list-input label="Description" type="textarea" placeholder="Your category description (optional)"></f7-list-input>
</f7-list>
<f7-card v-else-if="!loading"> <f7-list form strong inset dividers class="margin-top" v-else-if="!loading">
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-input
<f7-list form> type="text"
<f7-list-input clear-button
type="text" :label="$t('Category Name')"
clear-button :placeholder="$t('Your category name')"
:label="$t('Category Name')" v-model:value="category.name"
:placeholder="$t('Your category name')" ></f7-list-input>
:value="category.name"
@input="category.name = $event.target.value"
></f7-list-input>
<f7-list-item class="list-item-with-header-and-title" <f7-list-item class="list-item-with-header-and-title list-item-with-multi-item">
key="singleTypeCategoryIconSelection" link="#" <template #default>
:header="$t('Category Icon')" <div class="grid grid-cols-2">
@click="category.showIconSelectionSheet = true"> <div class="list-item-subitem no-chevron">
<f7-block slot="title" class="list-item-custom-title no-padding"> <a class="item-link" href="#" @click="category.showIconSelectionSheet = true">
<f7-icon :icon="category.icon | categoryIcon" <div class="item-content">
:style="category.color | categoryIconStyle('var(--default-icon-color)')"></f7-icon> <div class="item-inner">
</f7-block> <div class="item-header">
<icon-selection-sheet :all-icon-infos="allCategoryIcons" <span>{{ $t('Category Icon') }}</span>
:show.sync="category.showIconSelectionSheet" </div>
:color="category.color" <div class="item-title">
v-model="category.icon" <div class="list-item-custom-title no-padding">
></icon-selection-sheet> <ItemIcon icon-type="category" :icon-id="category.icon" :color="category.color"></ItemIcon>
</f7-list-item> </div>
</div>
</div>
</div>
</a>
<f7-list-item class="list-item-with-header-and-title" <icon-selection-sheet :all-icon-infos="allCategoryIcons"
key="singleTypeCategoryColorSelection" link="#" :color="category.color"
:header="$t('Category Color')" v-model:show="category.showIconSelectionSheet"
@click="category.showColorSelectionSheet = true"> v-model="category.icon"
<f7-block slot="title" class="list-item-custom-title no-padding"> ></icon-selection-sheet>
<f7-icon f7="app_fill" </div>
:style="category.color | categoryIconStyle('var(--default-icon-color)')"></f7-icon> <div class="list-item-subitem no-chevron">
</f7-block> <a class="item-link" href="#" @click="category.showColorSelectionSheet = true">
<color-selection-sheet :all-color-infos="allCategoryColors" <div class="item-content">
:show.sync="category.showColorSelectionSheet" <div class="item-inner">
v-model="category.color" <div class="item-header">
></color-selection-sheet> <span>{{ $t('Category Color') }}</span>
</f7-list-item> </div>
<div class="item-title">
<div class="list-item-custom-title no-padding">
<ItemIcon icon-type="fixed-f7" icon-id="app_fill" :color="category.color"></ItemIcon>
</div>
</div>
</div>
</div>
</a>
<f7-list-item :header="$t('Visible')" v-if="editCategoryId"> <color-selection-sheet :all-color-infos="allCategoryColors"
<f7-toggle :checked="category.visible" @toggle:change="category.visible = $event"></f7-toggle> v-model:show="category.showColorSelectionSheet"
</f7-list-item> v-model="category.color"
></color-selection-sheet>
</div>
</div>
</template>
</f7-list-item>
<f7-list-input <f7-list-item :header="$t('Visible')" v-if="editCategoryId">
type="textarea" <f7-toggle :checked="category.visible" @toggle:change="category.visible = $event"></f7-toggle>
class="textarea-auto-size" </f7-list-item>
style="height: auto"
:label="$t('Description')" <f7-list-input
:placeholder="$t('Your category description (optional)')" type="textarea"
:value="category.comment" style="height: auto"
@input="category.comment = $event.target.value" :label="$t('Description')"
></f7-list-input> :placeholder="$t('Your category description (optional)')"
</f7-list> v-textarea-auto-size
</f7-card-content> v-model:value="category.comment"
</f7-card> ></f7-list-input>
</f7-list>
</f7-page> </f7-page>
</template> </template>
<script> <script>
export default { export default {
props: [
'f7route',
'f7router'
],
data() { data() {
const self = this; const self = this;
const query = self.$f7route.query; const query = self.f7route.query;
return { return {
editCategoryId: null, editCategoryId: null,
@@ -150,7 +193,7 @@ export default {
}, },
created() { created() {
const self = this; const self = this;
const query = self.$f7route.query; const query = self.f7route.query;
if (!query.id && !query.parentId) { if (!query.id && !query.parentId) {
self.$toast('Parameter Invalid'); self.$toast('Parameter Invalid');
@@ -197,16 +240,13 @@ export default {
self.loading = false; self.loading = false;
} }
}, },
updated: function () {
this.autoChangeCommentTextareaSize();
},
methods: { methods: {
onPageAfterIn() { onPageAfterIn() {
this.$routeBackOnError('loadingError'); this.$routeBackOnError(this.f7router, 'loadingError');
}, },
save() { save() {
const self = this; const self = this;
const router = self.$f7router; const router = self.f7router;
const problemMessage = self.inputEmptyProblemMessage; const problemMessage = self.inputEmptyProblemMessage;
@@ -253,16 +293,6 @@ export default {
self.$toast(error.message || error); self.$toast(error.message || error);
} }
}); });
},
autoChangeCommentTextareaSize() {
const app = this.$f7;
const $$ = app.$;
$$('.textarea-auto-size textarea').each((idx, el) => {
el.scrollTop = 0;
el.style.height = '';
el.style.height = el.scrollHeight + 'px';
});
} }
} }
} }
+74 -70
View File
@@ -10,63 +10,57 @@
</f7-nav-right> </f7-nav-right>
</f7-navbar> </f7-navbar>
<f7-card class="skeleton-text" v-if="loading"> <f7-list strong inset dividers class="margin-top skeleton-text" v-if="loading">
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-item title="Category Name"
<f7-list> :link="hasSubCategories ? '#' : null"
<f7-list-item title="Category Name"> :key="itemIdx" v-for="itemIdx in [ 1, 2, 3 ]">
<f7-icon slot="media" f7="app_fill"></f7-icon> <template #media>
</f7-list-item> <f7-icon f7="app_fill"></f7-icon>
<f7-list-item title="Category Name 2"> </template>
<f7-icon slot="media" f7="app_fill"></f7-icon> </f7-list-item>
</f7-list-item> </f7-list>
<f7-list-item title="Category Name 3">
<f7-icon slot="media" f7="app_fill"></f7-icon>
</f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-card v-else-if="!loading"> <f7-list strong inset dividers class="margin-top" v-if="!loading && noAvailableCategory">
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-item :title="$t('No available category')"></f7-list-item>
<f7-list v-if="noAvailableCategory"> <f7-list-button v-if="hasSubCategories"
<f7-list-item :title="$t('No available category')"></f7-list-item> :title="$t('Add Default Categories')"
<f7-list-button v-if="hasSubCategories" :href="'/category/preset?type=' + categoryType"></f7-list-button>
:title="$t('Add Default Categories')" </f7-list>
:href="'/category/preset?type=' + categoryType"></f7-list-button>
</f7-list>
<f7-list class="category-list" sortable :sortable-enabled="sortable" @sortable:sort="onSort"> <f7-list strong inset dividers sortable class="margin-top category-list"
<f7-list-item v-for="category in categories" :sortable-enabled="sortable"
:key="category.id" v-if="!loading"
:id="category | categoryDomId" @sortable:sort="onSort">
:title="category.name" <f7-list-item swipeout
:footer="category.comment" :id="getCategoryDomId(category)"
:link="hasSubCategories ? '/category/list?type=' + categoryType + '&id=' + category.id : null" :title="category.name"
v-show="showHidden || !category.hidden" :footer="category.comment"
swipeout @taphold.native="setSortable()"> :link="hasSubCategories ? '/category/list?type=' + categoryType + '&id=' + category.id : null"
<f7-icon slot="media" :key="category.id"
:icon="category.icon | categoryIcon" v-for="category in categories"
:style="category.color | categoryIconStyle('var(--default-icon-color)')"> v-show="showHidden || !category.hidden"
<f7-badge color="gray" class="right-bottom-icon" v-if="category.hidden"> @taphold="setSortable()">
<f7-icon f7="eye_slash_fill"></f7-icon> <template #media>
</f7-badge> <ItemIcon icon-type="category" :icon-id="category.icon" :color="category.color">
</f7-icon> <f7-badge color="gray" class="right-bottom-icon" v-if="category.hidden">
<f7-swipeout-actions left v-if="sortable"> <f7-icon f7="eye_slash_fill"></f7-icon>
<f7-swipeout-button :color="category.hidden ? 'blue' : 'gray'" class="padding-left padding-right" </f7-badge>
overswipe close @click="hide(category, !category.hidden)"> </ItemIcon>
<f7-icon :f7="category.hidden ? 'eye' : 'eye_slash'"></f7-icon> </template>
</f7-swipeout-button> <f7-swipeout-actions left v-if="sortable">
</f7-swipeout-actions> <f7-swipeout-button :color="category.hidden ? 'blue' : 'gray'" class="padding-left padding-right"
<f7-swipeout-actions right v-if="!sortable"> overswipe close @click="hide(category, !category.hidden)">
<f7-swipeout-button color="orange" close :text="$t('Edit')" @click="edit(category)"></f7-swipeout-button> <f7-icon :f7="category.hidden ? 'eye' : 'eye_slash'"></f7-icon>
<f7-swipeout-button color="red" class="padding-left padding-right" @click="remove(category, false)"> </f7-swipeout-button>
<f7-icon f7="trash"></f7-icon> </f7-swipeout-actions>
</f7-swipeout-button> <f7-swipeout-actions right v-if="!sortable">
</f7-swipeout-actions> <f7-swipeout-button color="orange" close :text="$t('Edit')" @click="edit(category)"></f7-swipeout-button>
</f7-list-item> <f7-swipeout-button color="red" class="padding-left padding-right" @click="remove(category, false)">
</f7-list> <f7-icon f7="trash"></f7-icon>
</f7-card-content> </f7-swipeout-button>
</f7-card> </f7-swipeout-actions>
</f7-list-item>
</f7-list>
<f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false"> <f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false">
<f7-actions-group> <f7-actions-group>
@@ -91,6 +85,10 @@
<script> <script>
export default { export default {
props: [
'f7route',
'f7router'
],
data() { data() {
return { return {
hasSubCategories: false, hasSubCategories: false,
@@ -166,7 +164,7 @@ export default {
}, },
created() { created() {
const self = this; const self = this;
const query = self.$f7route.query; const query = self.f7route.query;
self.categoryType = parseInt(query.type); self.categoryType = parseInt(query.type);
@@ -207,7 +205,7 @@ export default {
this.reload(null); this.reload(null);
} }
this.$routeBackOnError('loadingError'); this.$routeBackOnError(this.f7router, 'loadingError');
}, },
reload(done) { reload(done) {
if (this.sortable) { if (this.sortable) {
@@ -245,12 +243,17 @@ export default {
onSort(event) { onSort(event) {
const self = this; const self = this;
if (!event || !event.el || !event.el.id || event.el.id.indexOf('category_') !== 0) { if (!event || !event.el || !event.el.id) {
this.$toast('Unable to move category'); self.$toast('Unable to move category');
return; return;
} }
const id = event.el.id.substring(9); // category_ const id = self.parseCategoryIdFromDomId(event.el.id);
if (!id) {
self.$toast('Unable to move category');
return;
}
self.$store.dispatch('changeCategoryDisplayOrder', { self.$store.dispatch('changeCategoryDisplayOrder', {
categoryId: id, categoryId: id,
@@ -294,7 +297,7 @@ export default {
}); });
}, },
edit(category) { edit(category) {
this.$f7router.navigate('/category/edit?id=' + category.id); this.f7router.navigate('/category/edit?id=' + category.id);
}, },
hide(category, hidden) { hide(category, hidden) {
const self = this; const self = this;
@@ -316,8 +319,6 @@ export default {
}, },
remove(category, confirm) { remove(category, confirm) {
const self = this; const self = this;
const app = self.$f7;
const $$ = app.$;
if (!category) { if (!category) {
self.$alert('An error has occurred'); self.$alert('An error has occurred');
@@ -337,9 +338,7 @@ export default {
self.$store.dispatch('deleteCategory', { self.$store.dispatch('deleteCategory', {
category: category, category: category,
beforeResolve: (done) => { beforeResolve: (done) => {
app.swipeout.delete($$(`#${self.$options.filters.categoryDomId(category)}`), () => { self.$ui.onSwipeoutDeleted(self.getCategoryDomId(category), done);
done();
});
} }
}).then(() => { }).then(() => {
self.$hideLoading(); self.$hideLoading();
@@ -350,11 +349,16 @@ export default {
self.$toast(error.message || error); self.$toast(error.message || error);
} }
}); });
} },
}, getCategoryDomId(category) {
filters: {
categoryDomId(category) {
return 'category_' + category.id; return 'category_' + category.id;
},
parseCategoryIdFromDomId(domId) {
if (!domId || domId.indexOf('category_') !== 0) {
return null;
}
return domId.substring(9); // category_
} }
} }
}; };
+45 -50
View File
@@ -4,44 +4,37 @@
<f7-nav-left :back-link="$t('Back')"></f7-nav-left> <f7-nav-left :back-link="$t('Back')"></f7-nav-left>
<f7-nav-title :title="$t('Default Categories')"></f7-nav-title> <f7-nav-title :title="$t('Default Categories')"></f7-nav-title>
<f7-nav-right> <f7-nav-right>
<f7-link icon-f7="ellipsis" @click="showMoreActionSheet = true"></f7-link> <f7-link icon-f7="ellipsis" v-if="allCategories && allCategories.length" @click="showMoreActionSheet = true"></f7-link>
<f7-link :text="$t('Save')" :class="{ 'disabled': submitting }" @click="save"></f7-link> <f7-link :text="$t('Save')" :class="{ 'disabled': submitting }" v-if="allCategories && allCategories.length" @click="save"></f7-link>
</f7-nav-right> </f7-nav-right>
</f7-navbar> </f7-navbar>
<f7-card v-for="categoryInfo in allCategories" :key="categoryInfo.type"> <f7-block class="no-padding no-margin" :key="categoryInfo.type" v-for="categoryInfo in allCategories">
<f7-card-header> <f7-block-title class="margin-top margin-horizontal">{{ getCategoryTypeName(categoryInfo.type) }}</f7-block-title>
<small class="card-header-content">
<span>{{ categoryInfo.type | categoryTypeName($constants.category.allCategoryTypes) | localized }}</span>
</small>
</f7-card-header>
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item v-for="(category, idx) in categoryInfo.categories"
:key="idx"
:accordion-item="!!category.subCategories.length"
:title="$t('category.' + category.name, currentLocale)">
<f7-icon slot="media"
:icon="category.categoryIconId | categoryIcon"
:style="category.color | categoryIconStyle('var(--default-icon-color)')">
</f7-icon>
<f7-accordion-content v-if="category.subCategories.length" class="padding-left"> <f7-list strong inset dividers class="margin-top">
<f7-list> <f7-list-item :title="$t('category.' + category.name, currentLocale)"
<f7-list-item v-for="(subCategory, subIdx) in category.subCategories" :accordion-item="!!category.subCategories.length"
:key="subIdx" :key="idx"
:title="$t('category.' + subCategory.name, currentLocale)"> v-for="(category, idx) in categoryInfo.categories">
<f7-icon slot="media" <template #media>
:icon="subCategory.categoryIconId | categoryIcon" <ItemIcon icon-type="category" :icon-id="category.categoryIconId" :color="category.color"></ItemIcon>
:style="subCategory.color | categoryIconStyle('var(--default-icon-color)')"> </template>
</f7-icon>
</f7-list-item> <f7-accordion-content v-if="category.subCategories.length" class="padding-left">
</f7-list> <f7-list>
</f7-accordion-content> <f7-list-item :title="$t('category.' + subCategory.name, currentLocale)"
</f7-list-item> :key="subIdx"
</f7-list> v-for="(subCategory, subIdx) in category.subCategories">
</f7-card-content> <template #media>
</f7-card> <ItemIcon icon-type="category" :icon-id="subCategory.categoryIconId" :color="subCategory.color"></ItemIcon>
</template>
</f7-list-item>
</f7-list>
</f7-accordion-content>
</f7-list-item>
</f7-list>
</f7-block>
<f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false"> <f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false">
<f7-actions-group> <f7-actions-group>
@@ -55,7 +48,7 @@
<list-item-selection-sheet value-type="index" <list-item-selection-sheet value-type="index"
title-field="displayName" title-field="displayName"
:items="allLanguages" :items="allLanguages"
:show.sync="showChangeLocaleSheet" v-model:show="showChangeLocaleSheet"
v-model="currentLocale"> v-model="currentLocale">
</list-item-selection-sheet> </list-item-selection-sheet>
</f7-page> </f7-page>
@@ -63,6 +56,10 @@
<script> <script>
export default { export default {
props: [
'f7route',
'f7router'
],
data() { data() {
const self = this; const self = this;
@@ -78,12 +75,12 @@ export default {
}, },
computed: { computed: {
allLanguages() { allLanguages() {
return this.$locale.getAllLanguages(); return this.$locale.getAllLanguageInfos();
} }
}, },
created() { created() {
const self = this; const self = this;
const query = self.$f7route.query; const query = self.f7route.query;
self.categoryType = parseInt(query.type); self.categoryType = parseInt(query.type);
@@ -112,7 +109,7 @@ export default {
}, },
methods: { methods: {
onPageAfterIn() { onPageAfterIn() {
this.$routeBackOnError('loadingError'); this.$routeBackOnError(this.f7router, 'loadingError');
}, },
getDefaultCategories(categoryType) { getDefaultCategories(categoryType) {
switch (categoryType) { switch (categoryType) {
@@ -128,7 +125,7 @@ export default {
}, },
save() { save() {
const self = this; const self = this;
const router = self.$f7router; const router = self.f7router;
self.submitting = true; self.submitting = true;
self.$showLoading(() => self.submitting); self.$showLoading(() => self.submitting);
@@ -178,19 +175,17 @@ export default {
self.$toast(error.message || error); self.$toast(error.message || error);
} }
}); });
} },
}, getCategoryTypeName(categoryType) {
filters: {
categoryTypeName(categoryType, allCategoryTypes) {
switch (categoryType) { switch (categoryType) {
case allCategoryTypes.Income: case this.$constants.category.allCategoryTypes.Income:
return 'Income Categories'; return this.$t('Income Categories');
case allCategoryTypes.Expense: case this.$constants.category.allCategoryTypes.Expense:
return 'Expense Categories'; return this.$t('Expense Categories');
case allCategoryTypes.Transfer: case this.$constants.category.allCategoryTypes.Transfer:
return 'Transfer Categories'; return this.$t('Transfer Categories');
default: default:
return 'Transaction Categories'; return this.$t('Transaction Categories');
} }
} }
} }
@@ -9,96 +9,93 @@
</f7-nav-right> </f7-nav-right>
</f7-navbar> </f7-navbar>
<f7-card class="skeleton-text" v-if="loading"> <f7-block class="combination-list-wrapper margin-vertical skeleton-text"
<f7-card-header> :key="blockIdx" v-for="blockIdx in [ 1, 2, 3 ]" v-if="loading">
<f7-accordion-toggle class="full-line"> <f7-accordion-item>
<small class="card-header-content"> <f7-block-title>
<span>Account Category</span> <f7-accordion-toggle>
</small> <f7-list strong inset dividers media-list
<f7-icon class="card-chevron-icon float-right" f7="chevron_up"></f7-icon> class="combination-list-header combination-list-opened">
</f7-accordion-toggle> <f7-list-item>
</f7-card-header> <template #title>
<span>Account Category</span>
<f7-icon class="combination-list-chevron-icon" f7="chevron_up"></f7-icon>
</template>
</f7-list-item>
</f7-list>
</f7-accordion-toggle>
</f7-block-title>
<f7-accordion-content style="height: auto">
<f7-list strong inset dividers accordion-list class="combination-list-content">
<f7-list-item checkbox class="disabled" title="Account Name"
:key="itemIdx" v-for="itemIdx in (blockIdx === 1 ? [ 1 ] : [ 1, 2 ])">
<template #media>
<f7-icon f7="app_fill"></f7-icon>
</template>
</f7-list-item>
</f7-list>
</f7-accordion-content>
</f7-accordion-item>
</f7-block>
<f7-card-content class="no-safe-areas" :padding="false"> <f7-block class="combination-list-wrapper margin-vertical"
<f7-list> :key="accountCategory.id"
<f7-list-item checkbox class="disabled" title="Account Name"> v-for="accountCategory in allAccountCategories"
<f7-icon slot="media" f7="app_fill"></f7-icon> v-else-if="!loading">
</f7-list-item> <f7-accordion-item :opened="collapseStates[accountCategory.id].opened"
</f7-list> v-show="hasShownAccount(accountCategory)"
</f7-card-content> @accordion:open="collapseStates[accountCategory.id].opened = true"
</f7-card> @accordion:close="collapseStates[accountCategory.id].opened = false">
<f7-block-title>
<f7-accordion-toggle>
<f7-list strong inset dividers media-list
class="combination-list-header"
:class="collapseStates[accountCategory.id].opened ? 'combination-list-opened' : 'combination-list-closed'">
<f7-list-item>
<template #title>
<span>{{ $t(accountCategory.name) }}</span>
<f7-icon class="combination-list-chevron-icon" :f7="collapseStates[accountCategory.id].opened ? 'chevron_up' : 'chevron_down'"></f7-icon>
</template>
</f7-list-item>
</f7-list>
</f7-accordion-toggle>
</f7-block-title>
<f7-accordion-content :style="{ height: collapseStates[accountCategory.id].opened ? 'auto' : '' }">
<f7-list strong inset dividers accordion-list class="combination-list-content"
v-if="categorizedAccounts[accountCategory.id]">
<f7-list-item checkbox
:title="account.name"
:value="account.id"
:checked="isAccountOrSubAccountsAllChecked(account, filterAccountIds)"
:indeterminate="isAccountOrSubAccountsHasButNotAllChecked(account, filterAccountIds)"
:key="account.id"
v-for="account in categorizedAccounts[accountCategory.id].accounts"
v-show="!account.hidden"
@change="selectAccountOrSubAccounts">
<template #media>
<ItemIcon icon-type="account" :icon-id="account.icon" :color="account.color"></ItemIcon>
</template>
<f7-card class="skeleton-text" v-if="loading"> <template #root>
<f7-card-header> <ul v-if="account.type === $constants.account.allAccountTypes.MultiSubAccounts" class="padding-left">
<f7-accordion-toggle class="full-line"> <f7-list-item checkbox
<small class="card-header-content"> :title="subAccount.name"
<span>Account Category 2</span> :value="subAccount.id"
</small> :checked="isAccountChecked(subAccount, filterAccountIds)"
<f7-icon class="card-chevron-icon float-right" f7="chevron_up"></f7-icon> :key="subAccount.id"
</f7-accordion-toggle> v-for="subAccount in account.subAccounts"
</f7-card-header> v-show="!subAccount.hidden"
@change="selectAccount">
<f7-card-content class="no-safe-areas" :padding="false"> <template #media>
<f7-list> <ItemIcon icon-type="account" :icon-id="subAccount.icon" :color="subAccount.color"></ItemIcon>
<f7-list-item checkbox class="disabled" title="Account Name"> </template>
<f7-icon slot="media" f7="app_fill"></f7-icon> </f7-list-item>
</f7-list-item> </ul>
<f7-list-item checkbox class="disabled" title="Account Name 2"> </template>
<f7-icon slot="media" f7="app_fill"></f7-icon> </f7-list-item>
</f7-list-item> </f7-list>
</f7-list> </f7-accordion-content>
</f7-card-content> </f7-accordion-item>
</f7-card>
<f7-block class="no-padding no-margin" v-if="!loading">
<f7-card v-for="accountCategory in allAccountCategories" :key="accountCategory.id" v-show="hasShownAccount(accountCategory)">
<f7-accordion-item :opened="collapseStates[accountCategory.id].opened"
@accordion:open="collapseStates[accountCategory.id].opened = true"
@accordion:close="collapseStates[accountCategory.id].opened = false">
<f7-card-header>
<f7-accordion-toggle class="full-line">
<small class="card-header-content">
<span>{{ $t(accountCategory.name) }}</span>
</small>
<f7-icon class="card-chevron-icon float-right" :f7="collapseStates[accountCategory.id].opened ? 'chevron_up' : 'chevron_down'"></f7-icon>
</f7-accordion-toggle>
</f7-card-header>
<f7-card-content class="no-safe-areas" :padding="false" accordion-list>
<f7-accordion-content :style="{ height: collapseStates[accountCategory.id].opened ? 'auto' : '' }">
<f7-list v-if="categorizedAccounts[accountCategory.id]">
<f7-list-item checkbox v-for="account in categorizedAccounts[accountCategory.id].accounts"
v-show="!account.hidden"
:key="account.id"
:title="account.name"
:value="account.id"
:checked="account | accountOrSubAccountsAllChecked(filterAccountIds)"
:indeterminate="account | accountOrSubAccountsHasButNotAllChecked(filterAccountIds)"
@change="selectAccountOrSubAccounts">
<f7-icon slot="media"
:icon="account.icon | accountIcon"
:style="account.color | accountIconStyle('var(--default-icon-color)')">
</f7-icon>
<ul slot="root" v-if="account.type === $constants.account.allAccountTypes.MultiSubAccounts" class="padding-left">
<f7-list-item checkbox v-for="subAccount in account.subAccounts"
v-show="!subAccount.hidden"
:key="subAccount.id"
:title="subAccount.name"
:value="subAccount.id"
:checked="subAccount | accountChecked(filterAccountIds) "
@change="selectAccount">
<f7-icon slot="media"
:icon="subAccount.icon | accountIcon"
:style="subAccount.color | accountIconStyle('var(--default-icon-color)')">
</f7-icon>
</f7-list-item>
</ul>
</f7-list-item>
</f7-list>
</f7-accordion-content>
</f7-card-content>
</f7-accordion-item>
</f7-card>
</f7-block> </f7-block>
<f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false"> <f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false">
@@ -116,6 +113,10 @@
<script> <script>
export default { export default {
props: [
'f7route',
'f7router'
],
data: function () { data: function () {
const self = this; const self = this;
@@ -152,7 +153,7 @@ export default {
}, },
created() { created() {
const self = this; const self = this;
const query = self.$f7route.query; const query = self.f7route.query;
self.modifyDefault = !!query.modifyDefault; self.modifyDefault = !!query.modifyDefault;
@@ -188,11 +189,11 @@ export default {
}, },
methods: { methods: {
onPageAfterIn() { onPageAfterIn() {
this.$routeBackOnError('loadingError'); this.$routeBackOnError(this.f7router, 'loadingError');
}, },
save() { save() {
const self = this; const self = this;
const router = self.$f7router; const router = self.f7router;
const filteredAccountIds = {}; const filteredAccountIds = {};
@@ -286,6 +287,39 @@ export default {
} }
} }
}, },
isAccountChecked(account, filterAccountIds) {
return !filterAccountIds[account.id];
},
isAccountOrSubAccountsAllChecked(account, filterAccountIds) {
if (!account.subAccounts) {
return !filterAccountIds[account.id];
}
for (let i = 0; i < account.subAccounts.length; i++) {
const subAccount = account.subAccounts[i];
if (filterAccountIds[subAccount.id]) {
return false;
}
}
return true;
},
isAccountOrSubAccountsHasButNotAllChecked(account, filterAccountIds) {
if (!account.subAccounts) {
return false;
}
let checkedCount = 0;
for (let i = 0; i < account.subAccounts.length; i++) {
const subAccount = account.subAccounts[i];
if (!filterAccountIds[subAccount.id]) {
checkedCount++;
}
}
return checkedCount > 0 && checkedCount < account.subAccounts.length;
},
hasShownAccount(accountCategory) { hasShownAccount(accountCategory) {
if (!this.categorizedAccounts[accountCategory.id] || if (!this.categorizedAccounts[accountCategory.id] ||
!this.categorizedAccounts[accountCategory.id].accounts || !this.categorizedAccounts[accountCategory.id].accounts ||
@@ -320,41 +354,6 @@ export default {
return collapseStates; return collapseStates;
} }
},
filters: {
accountChecked(account, filterAccountIds) {
return !filterAccountIds[account.id];
},
accountOrSubAccountsAllChecked(account, filterAccountIds) {
if (!account.subAccounts) {
return !filterAccountIds[account.id];
}
for (let i = 0; i < account.subAccounts.length; i++) {
const subAccount = account.subAccounts[i];
if (filterAccountIds[subAccount.id]) {
return false;
}
}
return true;
},
accountOrSubAccountsHasButNotAllChecked(account, filterAccountIds) {
if (!account.subAccounts) {
return false;
}
let checkedCount = 0;
for (let i = 0; i < account.subAccounts.length; i++) {
const subAccount = account.subAccounts[i];
if (!filterAccountIds[subAccount.id]) {
checkedCount++;
}
}
return checkedCount > 0 && checkedCount < account.subAccounts.length;
}
} }
} }
</script> </script>
@@ -9,98 +9,101 @@
</f7-nav-right> </f7-nav-right>
</f7-navbar> </f7-navbar>
<f7-card class="skeleton-text" v-if="loading"> <f7-block class="combination-list-wrapper margin-vertical skeleton-text"
<f7-card-header> :key="blockIdx" v-for="blockIdx in [ 1, 2 ]" v-if="loading">
<f7-accordion-toggle class="full-line"> <f7-accordion-item>
<small class="card-header-content"> <f7-block-title>
<span>Transaction Category</span> <f7-accordion-toggle>
</small> <f7-list strong inset dividers media-list
<f7-icon class="card-chevron-icon float-right" f7="chevron_up"></f7-icon> class="combination-list-header combination-list-opened">
</f7-accordion-toggle> <f7-list-item>
</f7-card-header> <template #title>
<f7-card-content class="no-safe-areas" :padding="false"> <span>Transaction Category</span>
<f7-list> <f7-icon class="combination-list-chevron-icon" f7="chevron_up"></f7-icon>
<f7-list-item checkbox class="disabled" title="Category Name"> </template>
<f7-icon slot="media" f7="app_fill"></f7-icon>
<ul slot="root" class="padding-left">
<f7-list-item checkbox class="disabled" title="Sub Category Name">
<f7-icon slot="media" f7="app_fill"></f7-icon>
</f7-list-item> </f7-list-item>
<f7-list-item checkbox class="disabled" title="Sub Category Name 2"> </f7-list>
<f7-icon slot="media" f7="app_fill"></f7-icon> </f7-accordion-toggle>
</f7-list-item> </f7-block-title>
<f7-list-item checkbox class="disabled" title="Sub Category Name 3"> <f7-accordion-content style="height: auto">
<f7-icon slot="media" f7="app_fill"></f7-icon> <f7-list strong inset dividers accordion-list class="combination-list-content">
</f7-list-item> <f7-list-item checkbox class="disabled" title="Category Name"
</ul> :key="itemIdx" v-for="itemIdx in [ 1, 2 ]">
</f7-list-item> <template #media>
<f7-list-item checkbox class="disabled" title="Category Name 2"> <f7-icon f7="app_fill"></f7-icon>
<f7-icon slot="media" f7="app_fill"></f7-icon> </template>
<ul slot="root" class="padding-left"> <template #root>
<f7-list-item checkbox class="disabled" title="Sub Category Name"> <ul class="padding-left">
<f7-icon slot="media" f7="app_fill"></f7-icon> <f7-list-item checkbox class="disabled" title="Sub Category Name"
</f7-list-item> :key="subItemIdx" v-for="subItemIdx in [ 1, 2, 3 ]">
<f7-list-item checkbox class="disabled" title="Sub Category Name 2"> <template #media>
<f7-icon slot="media" f7="app_fill"></f7-icon> <f7-icon f7="app_fill"></f7-icon>
</f7-list-item> </template>
<f7-list-item checkbox class="disabled" title="Sub Category Name 3"> </f7-list-item>
<f7-icon slot="media" f7="app_fill"></f7-icon> </ul>
</f7-list-item> </template>
</ul> </f7-list-item>
</f7-list-item> </f7-list>
</f7-list> </f7-accordion-content>
</f7-card-content> </f7-accordion-item>
</f7-card> </f7-block>
<f7-block class="no-padding no-margin" v-if="!loading"> <f7-block class="combination-list-wrapper margin-vertical"
<f7-card v-for="(categories, categoryType) in allTransactionCategories" :key="categoryType"> :key="categoryType"
<f7-accordion-item :opened="collapseStates[categoryType].opened" v-for="(categories, categoryType) in allTransactionCategories"
@accordion:open="collapseStates[categoryType].opened = true" v-else-if="!loading">
@accordion:close="collapseStates[categoryType].opened = false"> <f7-accordion-item :opened="collapseStates[categoryType].opened"
<f7-card-header> @accordion:open="collapseStates[categoryType].opened = true"
<f7-accordion-toggle class="full-line"> @accordion:close="collapseStates[categoryType].opened = false">
<small class="card-header-content"> <f7-block-title>
<span>{{ categoryType | categoryTypeName($constants.category.allCategoryTypes) | localized }}</span> <f7-accordion-toggle>
</small> <f7-list strong inset dividers media-list
<f7-icon class="card-chevron-icon float-right" :f7="collapseStates[categoryType].opened ? 'chevron_up' : 'chevron_down'"></f7-icon> class="combination-list-header"
</f7-accordion-toggle> :class="collapseStates[categoryType].opened ? 'combination-list-opened' : 'combination-list-closed'">
</f7-card-header> <f7-list-item>
<f7-card-content class="no-safe-areas" :padding="false" accordion-list> <template #title>
<f7-accordion-content :style="{ height: collapseStates[categoryType].opened ? 'auto' : '' }"> <span>{{ getCategoryTypeName(categoryType) }}</span>
<f7-list> <f7-icon class="combination-list-chevron-icon" :f7="collapseStates[categoryType].opened ? 'chevron_up' : 'chevron_down'"></f7-icon>
<f7-list-item checkbox v-for="category in categories" </template>
v-show="!category.hidden" </f7-list-item>
:key="category.id" </f7-list>
:title="category.name" </f7-accordion-toggle>
:value="category.id" </f7-block-title>
:checked="category | subCategoriesAllChecked(filterCategoryIds)" <f7-accordion-content :style="{ height: collapseStates[categoryType].opened ? 'auto' : '' }">
:indeterminate="category | subCategoriesHasButNotAllChecked(filterCategoryIds)" <f7-list strong inset dividers accordion-list class="combination-list-content">
@change="selectSubCategories"> <f7-list-item checkbox
<f7-icon slot="media" :title="category.name"
:icon="category.icon | categoryIcon" :value="category.id"
:style="category.color | categoryIconStyle('var(--default-icon-color)')"> :checked="isSubCategoriesAllChecked(category, filterCategoryIds)"
</f7-icon> :indeterminate="isSubCategoriesHasButNotAllChecked(category, filterCategoryIds)"
:key="category.id"
v-for="category in categories"
v-show="!category.hidden"
@change="selectSubCategories">
<template #media>
<ItemIcon icon-type="category" :icon-id="category.icon" :color="category.color"></ItemIcon>
</template>
<ul slot="root" v-if="category.subCategories.length" class="padding-left"> <template #root>
<f7-list-item checkbox v-for="subCategory in category.subCategories" <ul v-if="category.subCategories.length" class="padding-left">
v-show="!subCategory.hidden" <f7-list-item checkbox
:key="subCategory.id" :title="subCategory.name"
:title="subCategory.name" :value="subCategory.id"
:value="subCategory.id" :checked="isCategoryChecked(subCategory, filterCategoryIds)"
:checked="subCategory | categoryChecked(filterCategoryIds) " :key="subCategory.id"
@change="selectCategory"> v-for="subCategory in category.subCategories"
<f7-icon slot="media" v-show="!subCategory.hidden"
:icon="subCategory.icon | categoryIcon" @change="selectCategory">
:style="subCategory.color | categoryIconStyle('var(--default-icon-color)')"> <template #media>
</f7-icon> <ItemIcon icon-type="category" :icon-id="subCategory.icon" :color="subCategory.color"></ItemIcon>
</f7-list-item> </template>
</ul> </f7-list-item>
</f7-list-item> </ul>
</f7-list> </template>
</f7-accordion-content> </f7-list-item>
</f7-card-content> </f7-list>
</f7-accordion-item> </f7-accordion-content>
</f7-card> </f7-accordion-item>
</f7-block> </f7-block>
<f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false"> <f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false">
@@ -118,6 +121,10 @@
<script> <script>
export default { export default {
props: [
'f7route',
'f7router'
],
data: function () { data: function () {
const self = this; const self = this;
@@ -151,7 +158,7 @@ export default {
}, },
created() { created() {
const self = this; const self = this;
const query = self.$f7route.query; const query = self.f7route.query;
self.modifyDefault = !!query.modifyDefault; self.modifyDefault = !!query.modifyDefault;
@@ -187,11 +194,11 @@ export default {
}, },
methods: { methods: {
onPageAfterIn() { onPageAfterIn() {
this.$routeBackOnError('loadingError'); this.$routeBackOnError(this.f7router, 'loadingError');
}, },
save() { save() {
const self = this; const self = this;
const router = self.$f7router; const router = self.f7router;
const filteredCategoryIds = {}; const filteredCategoryIds = {};
@@ -277,6 +284,43 @@ export default {
} }
} }
}, },
getCategoryTypeName(categoryType) {
switch (categoryType) {
case this.$constants.category.allCategoryTypes.Income.toString():
return this.$t('Income Categories');
case this.$constants.category.allCategoryTypes.Expense.toString():
return this.$t('Expense Categories');
case this.$constants.category.allCategoryTypes.Transfer.toString():
return this.$t('Transfer Categories');
default:
return this.$t('Transaction Categories');
}
},
isCategoryChecked(category, filterCategoryIds) {
return !filterCategoryIds[category.id];
},
isSubCategoriesAllChecked(category, filterCategoryIds) {
for (let i = 0; i < category.subCategories.length; i++) {
const subCategory = category.subCategories[i];
if (filterCategoryIds[subCategory.id]) {
return false;
}
}
return true;
},
isSubCategoriesHasButNotAllChecked(category, filterCategoryIds) {
let checkedCount = 0;
for (let i = 0; i < category.subCategories.length; i++) {
const subCategory = category.subCategories[i];
if (!filterCategoryIds[subCategory.id]) {
checkedCount++;
}
}
return checkedCount > 0 && checkedCount < category.subCategories.length;
},
getCollapseStates() { getCollapseStates() {
const collapseStates = {}; const collapseStates = {};
@@ -294,45 +338,6 @@ export default {
return collapseStates; return collapseStates;
} }
},
filters: {
categoryTypeName(categoryType, allCategoryTypes) {
switch (categoryType) {
case allCategoryTypes.Income.toString():
return 'Income Categories';
case allCategoryTypes.Expense.toString():
return 'Expense Categories';
case allCategoryTypes.Transfer.toString():
return 'Transfer Categories';
default:
return 'Transaction Categories';
}
},
categoryChecked(category, filterCategoryIds) {
return !filterCategoryIds[category.id];
},
subCategoriesAllChecked(category, filterCategoryIds) {
for (let i = 0; i < category.subCategories.length; i++) {
const subCategory = category.subCategories[i];
if (filterCategoryIds[subCategory.id]) {
return false;
}
}
return true;
},
subCategoriesHasButNotAllChecked(category, filterCategoryIds) {
let checkedCount = 0;
for (let i = 0; i < category.subCategories.length; i++) {
const subCategory = category.subCategories[i];
if (!filterCategoryIds[subCategory.id]) {
checkedCount++;
}
}
return checkedCount > 0 && checkedCount < category.subCategories.length;
}
} }
} }
</script> </script>
+39 -43
View File
@@ -2,54 +2,50 @@
<f7-page> <f7-page>
<f7-navbar :title="$t('Statistics Settings')" :back-link="$t('Back')"></f7-navbar> <f7-navbar :title="$t('Statistics Settings')" :back-link="$t('Back')"></f7-navbar>
<f7-card> <f7-list strong inset dividers class="margin-top">
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-item
<f7-list> :title="$t('Default Chart Type')"
<f7-list-item smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Chart Type'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), popupCloseLinkText: $t('Done') }">
:title="$t('Default Chart Type')" <select v-model="defaultChartType">
smart-select :smart-select-params="{ openIn: 'popup', searchbar: true, searchbarPlaceholder: $t('Default Chart Type'), searchbarDisableText: $t('Cancel'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }"> <option :value="$constants.statistics.allChartTypes.Pie">{{ $t('Pie Chart') }}</option>
<select v-model="defaultChartType"> <option :value="$constants.statistics.allChartTypes.Bar">{{ $t('Bar Chart') }}</option>
<option :value="$constants.statistics.allChartTypes.Pie">{{ $t('Pie Chart') }}</option> </select>
<option :value="$constants.statistics.allChartTypes.Bar">{{ $t('Bar Chart') }}</option> </f7-list-item>
</select>
</f7-list-item>
<f7-list-item <f7-list-item
:title="$t('Default Chart Data Type')" :title="$t('Default Chart Data Type')"
smart-select :smart-select-params="{ openIn: 'popup', searchbar: true, searchbarPlaceholder: $t('Default Chart Data Type'), searchbarDisableText: $t('Cancel'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }"> smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Chart Data Type'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), popupCloseLinkText: $t('Done') }">
<select v-model="defaultChartDataType"> <select v-model="defaultChartDataType">
<option v-for="chartDataType in allChartDataTypes" <option :value="chartDataType.type"
:key="chartDataType.type" :key="chartDataType.type"
:value="chartDataType.type">{{ $t(chartDataType.name) }}</option> v-for="chartDataType in allChartDataTypes">{{ $t(chartDataType.name) }}</option>
</select> </select>
</f7-list-item> </f7-list-item>
<f7-list-item <f7-list-item
:title="$t('Default Date Range')" :title="$t('Default Date Range')"
smart-select :smart-select-params="{ openIn: 'popup', searchbar: true, searchbarPlaceholder: $t('Default Date Range'), searchbarDisableText: $t('Cancel'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }"> smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Date Range'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), popupCloseLinkText: $t('Done') }">
<select v-model="defaultDateRange"> <select v-model="defaultDateRange">
<option v-for="dateRange in allDateRanges" <option :value="dateRange.type"
:key="dateRange.type" :key="dateRange.type"
:value="dateRange.type">{{ $t(dateRange.name) }}</option> v-for="dateRange in allDateRanges">{{ $t(dateRange.name) }}</option>
</select> </select>
</f7-list-item> </f7-list-item>
<f7-list-item :title="$t('Default Account Filter')" link="/statistic/filter/account?modifyDefault=1"></f7-list-item> <f7-list-item :title="$t('Default Account Filter')" link="/statistic/filter/account?modifyDefault=1"></f7-list-item>
<f7-list-item :title="$t('Default Transaction Category Filter')" link="/statistic/filter/category?modifyDefault=1"></f7-list-item> <f7-list-item :title="$t('Default Transaction Category Filter')" link="/statistic/filter/category?modifyDefault=1"></f7-list-item>
<f7-list-item <f7-list-item
:title="$t('Default Sort By')" :title="$t('Default Sort By')"
smart-select :smart-select-params="{ openIn: 'popup', searchbar: true, searchbarPlaceholder: $t('Default Sort By'), searchbarDisableText: $t('Cancel'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }"> smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Sort By'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), popupCloseLinkText: $t('Done') }">
<select v-model="defaultSortingType"> <select v-model="defaultSortingType">
<option v-for="sortingType in allSortingTypes" <option :value="sortingType.type"
:key="sortingType.type" :key="sortingType.type"
:value="sortingType.type">{{ $t(sortingType.name) }}</option> v-for="sortingType in allSortingTypes">{{ $t(sortingType.name) }}</option>
</select> </select>
</f7-list-item> </f7-list-item>
</f7-list> </f7-list>
</f7-card-content>
</f7-card>
</f7-page> </f7-page>
</template> </template>
+158 -154
View File
@@ -4,7 +4,7 @@
<f7-nav-left :back-link="$t('Back')"></f7-nav-left> <f7-nav-left :back-link="$t('Back')"></f7-nav-left>
<f7-nav-title> <f7-nav-title>
<f7-link popover-open=".chart-data-type-popover-menu"> <f7-link popover-open=".chart-data-type-popover-menu">
<span>{{ query.chartDataType | optionName(allChartDataTypes, 'type', 'name', 'Statistics') | localized }}</span> <span>{{ queryChartDataTypeName }}</span>
<f7-icon size="14px" :f7="showChartDataTypePopover ? 'arrowtriangle_up_fill' : 'arrowtriangle_down_fill'"></f7-icon> <f7-icon size="14px" :f7="showChartDataTypePopover ? 'arrowtriangle_up_fill' : 'arrowtriangle_down_fill'"></f7-icon>
</f7-link> </f7-link>
</f7-nav-title> </f7-nav-title>
@@ -15,11 +15,14 @@
<f7-popover class="chart-data-type-popover-menu" :opened="showChartDataTypePopover" <f7-popover class="chart-data-type-popover-menu" :opened="showChartDataTypePopover"
@popover:open="showChartDataTypePopover = true" @popover:close="showChartDataTypePopover = false"> @popover:open="showChartDataTypePopover = true" @popover:close="showChartDataTypePopover = false">
<f7-list> <f7-list dividers>
<f7-list-item <f7-list-item :title="$t(dataType.name)"
v-for="dataType in allChartDataTypes" :key="dataType.type" :key="dataType.type"
:title="$t(dataType.name)" @click="setChartDataType(dataType.type)"> v-for="dataType in allChartDataTypes"
<f7-icon slot="after" class="list-item-checked-icon" f7="checkmark_alt" v-if="query.chartDataType === dataType.type"></f7-icon> @click="setChartDataType(dataType.type)">
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.chartDataType === dataType.type"></f7-icon>
</template>
</f7-list-item> </f7-list-item>
</f7-list> </f7-list>
</f7-popover> </f7-popover>
@@ -28,7 +31,7 @@
<f7-card-header class="no-border display-block"> <f7-card-header class="no-border display-block">
<div class="statistics-chart-header full-line text-align-right"> <div class="statistics-chart-header full-line text-align-right">
<span style="margin-right: 4px;">{{ $t('Sort By') }}</span> <span style="margin-right: 4px;">{{ $t('Sort By') }}</span>
<f7-link href="#" popover-open=".sorting-type-popover-menu">{{ query.sortingType | optionName(allSortingTypes, 'type', 'name', 'System Default') | localized }}</f7-link> <f7-link href="#" popover-open=".sorting-type-popover-menu">{{ querySortingTypeName }}</f7-link>
</div> </div>
</f7-card-header> </f7-card-header>
<f7-card-content class="pie-chart-container" style="margin-top: -6px" :padding="false"> <f7-card-content class="pie-chart-container" style="margin-top: -6px" :padding="false">
@@ -61,10 +64,10 @@
@click="clickPieChartItem" @click="clickPieChartItem"
> >
<text class="statistics-pie-chart-total-amount-title" v-if="statisticsData.items && statisticsData.items.length"> <text class="statistics-pie-chart-total-amount-title" v-if="statisticsData.items && statisticsData.items.length">
{{ query.chartDataType | totalAmountName(allChartDataTypes) | localized }} {{ totalAmountName }}
</text> </text>
<text class="statistics-pie-chart-total-amount-value" v-if="statisticsData.items && statisticsData.items.length"> <text class="statistics-pie-chart-total-amount-value" v-if="statisticsData.items && statisticsData.items.length">
{{ statisticsData.totalAmount | currency(defaultCurrency) | finalAmount(showAccountBalance, query.chartDataType, allChartDataTypes) | textLimit(16) }} {{ getDisplayAmount(statisticsData.totalAmount, defaultCurrency, 16) }}
</text> </text>
<text class="statistics-pie-chart-total-no-data" cy="50%" v-if="!statisticsData.items || !statisticsData.items.length"> <text class="statistics-pie-chart-total-no-data" cy="50%" v-if="!statisticsData.items || !statisticsData.items.length">
{{ $t('No data') }} {{ $t('No data') }}
@@ -77,128 +80,91 @@
<f7-card-header class="no-border display-block"> <f7-card-header class="no-border display-block">
<div class="statistics-chart-header display-flex full-line justify-content-space-between"> <div class="statistics-chart-header display-flex full-line justify-content-space-between">
<div> <div>
{{ query.chartDataType | totalAmountName(allChartDataTypes) | localized }} {{ totalAmountName }}
</div> </div>
<div class="align-self-flex-end"> <div class="align-self-flex-end">
<span style="margin-right: 4px;">{{ $t('Sort By') }}</span> <span style="margin-right: 4px;">{{ $t('Sort By') }}</span>
<f7-link href="#" popover-open=".sorting-type-popover-menu">{{ query.sortingType | optionName(allSortingTypes, 'type', 'name', 'System Default') | localized }}</f7-link> <f7-link href="#" popover-open=".sorting-type-popover-menu">{{ querySortingTypeName }}</f7-link>
</div> </div>
</div> </div>
<div class="display-flex full-line"> <div class="display-flex full-line">
<div :class="{ 'statistics-list-item-overview-amount': true, 'text-color-teal': query.chartDataType === allChartDataTypes.ExpenseByAccount.type || query.chartDataType === allChartDataTypes.ExpenseByPrimaryCategory.type || query.chartDataType === allChartDataTypes.ExpenseBySecondaryCategory.type, 'text-color-red': query.chartDataType === allChartDataTypes.IncomeByAccount.type || query.chartDataType === allChartDataTypes.IncomeByPrimaryCategory.type || query.chartDataType === allChartDataTypes.IncomeBySecondaryCategory.type }"> <div :class="{ 'statistics-list-item-overview-amount': true, 'text-color-teal': query.chartDataType === allChartDataTypes.ExpenseByAccount.type || query.chartDataType === allChartDataTypes.ExpenseByPrimaryCategory.type || query.chartDataType === allChartDataTypes.ExpenseBySecondaryCategory.type, 'text-color-red': query.chartDataType === allChartDataTypes.IncomeByAccount.type || query.chartDataType === allChartDataTypes.IncomeByPrimaryCategory.type || query.chartDataType === allChartDataTypes.IncomeBySecondaryCategory.type }">
<span v-if="!loading && statisticsData && statisticsData.items && statisticsData.items.length"> <span v-if="!loading && statisticsData && statisticsData.items && statisticsData.items.length">
{{ statisticsData.totalAmount | currency(defaultCurrency) | finalAmount(showAccountBalance, query.chartDataType, allChartDataTypes) }} {{ getDisplayAmount(statisticsData.totalAmount, defaultCurrency) }}
</span> </span>
<span v-else-if="loading || !statisticsData || !statisticsData.items || !statisticsData.items.length"> <span :class="{ 'skeleton-text': loading }" v-else-if="loading || !statisticsData || !statisticsData.items || !statisticsData.items.length">
{{ '---' | currency(defaultCurrency, true) }} ***.**
</span> </span>
</div> </div>
</div> </div>
</f7-card-header> </f7-card-header>
<f7-card-content class="no-safe-areas" style="margin-top: -14px" :padding="false"> <f7-card-content style="margin-top: -14px" :padding="false">
<f7-list class="statistics-list-item skeleton-text" v-if="loading"> <f7-list class="statistics-list-item skeleton-text" v-if="loading">
<f7-list-item link="#"> <f7-list-item link="#" :key="itemIdx" v-for="itemIdx in [ 1, 2, 3 ]">
<div slot="media" class="display-flex no-padding-horizontal"> <template #media>
<div class="display-flex align-items-center statistics-icon"> <div class="display-flex no-padding-horizontal">
<f7-icon slot="media" f7="app_fill"></f7-icon> <div class="display-flex align-items-center statistics-icon">
<f7-icon f7="app_fill"></f7-icon>
</div>
</div> </div>
</div> </template>
<template #title>
<div slot="title" class="statistics-list-item-text"> <div class="statistics-list-item-text">
<span>Category Name 1</span> <span>Category Name</span>
<small class="statistics-percent">33.33</small> <small class="statistics-percent">33.33</small>
</div> </div>
</template>
<div slot="after"> <template #after>
<span>0.00 USD</span> <span>0.00 USD</span>
</div> </template>
<template #inner-end>
<div slot="inner-end" class="statistics-item-end"> <div class="statistics-item-end">
<div class="statistics-percent-line"> <div class="statistics-percent-line">
<f7-progressbar></f7-progressbar> <f7-progressbar></f7-progressbar>
</div>
</div> </div>
</div> </template>
</f7-list-item>
<f7-list-item link="#">
<div slot="media" class="display-flex no-padding-horizontal">
<div class="display-flex align-items-center statistics-icon">
<f7-icon slot="media" f7="app_fill"></f7-icon>
</div>
</div>
<div slot="title" class="statistics-list-item-text">
<span>Category Name 2</span>
<small class="statistics-percent">33.33</small>
</div>
<div slot="after">
<span>0.00 USD</span>
</div>
<div slot="inner-end" class="statistics-item-end">
<div class="statistics-percent-line">
<f7-progressbar></f7-progressbar>
</div>
</div>
</f7-list-item>
<f7-list-item link="#">
<div slot="media" class="display-flex no-padding-horizontal">
<div class="display-flex align-items-center statistics-icon">
<f7-icon slot="media" f7="app_fill"></f7-icon>
</div>
</div>
<div slot="title" class="statistics-list-item-text">
<span>Category Name 3</span>
<small class="statistics-percent">33.33</small>
</div>
<div slot="after">
<span>0.00 USD</span>
</div>
<div slot="inner-end" class="statistics-item-end">
<div class="statistics-percent-line">
<f7-progressbar></f7-progressbar>
</div>
</div>
</f7-list-item> </f7-list-item>
</f7-list> </f7-list>
<f7-list v-else-if="!loading && (!statisticsData || !statisticsData.items || !statisticsData.items.length)"> <f7-list v-else-if="!loading && (!statisticsData || !statisticsData.items || !statisticsData.items.length)">
<f7-list-item :title="$t('No transaction data')"></f7-list-item> <f7-list-item :title="$t('No transaction data')"></f7-list-item>
</f7-list> </f7-list>
<f7-list v-else-if="!loading && statisticsData && statisticsData.items && statisticsData.items.length"> <f7-list v-else-if="!loading && statisticsData && statisticsData.items && statisticsData.items.length">
<f7-list-item v-for="(item, idx) in statisticsData.items" :key="idx" <f7-list-item class="statistics-list-item"
class="statistics-list-item" :link="getItemLinkUrl(item)"
:link="item | itemLinkUrl(query, allChartDataTypes)" :key="idx"
v-for="(item, idx) in statisticsData.items"
v-show="!item.hidden" v-show="!item.hidden"
> >
<div slot="media" class="display-flex no-padding-horizontal"> <template #media>
<div class="display-flex align-items-center statistics-icon"> <div class="display-flex no-padding-horizontal">
<f7-icon v-if="item.icon" <div class="display-flex align-items-center statistics-icon">
:icon="item.icon | icon(item.type)" <ItemIcon icon-type="category" :icon-id="item.icon" :color="item.color" v-if="item.icon"></ItemIcon>
:style="item.color | iconStyle(item.type, 'var(--category-icon-color)')"> <f7-icon f7="pencil_ellipsis_rectangle" v-else-if="!item.icon"></f7-icon>
</f7-icon> </div>
<f7-icon v-else-if="!item.icon"
f7="pencil_ellipsis_rectangle">
</f7-icon>
</div> </div>
</div> </template>
<div slot="title" class="statistics-list-item-text"> <template #title>
<span>{{ item.name }}</span> <div class="statistics-list-item-text">
<small class="statistics-percent" v-if="item.percent >= 0">{{ item.percent | percent(2, '&lt;0.01') }}</small> <span>{{ item.name }}</span>
</div> <small class="statistics-percent" v-if="item.percent >= 0">{{ $utilities.formatPercent(item.percent, 2, '&lt;0.01') }}</small>
<div slot="after">
<span>{{ item.totalAmount | currency(item.currency || defaultCurrency) | finalAmount(showAccountBalance, query.chartDataType, allChartDataTypes) }}</span>
</div>
<div slot="inner-end" class="statistics-item-end">
<div class="statistics-percent-line">
<f7-progressbar :progress="item.percent >= 0 ? item.percent : 0" :style="{ '--f7-progressbar-progress-color': (item.color ? '#' + item.color : '') } "></f7-progressbar>
</div> </div>
</div> </template>
<template #after>
<span>{{ getDisplayAmount(item.totalAmount, (item.currency || defaultCurrency)) }}</span>
</template>
<template #inner-end>
<div class="statistics-item-end">
<div class="statistics-percent-line">
<f7-progressbar :progress="item.percent >= 0 ? item.percent : 0" :style="{ '--f7-progressbar-progress-color': (item.color ? '#' + item.color : '') } "></f7-progressbar>
</div>
</div>
</template>
</f7-list-item> </f7-list-item>
</f7-list> </f7-list>
</f7-card-content> </f7-card-content>
@@ -207,13 +173,15 @@
<f7-popover class="sorting-type-popover-menu" :opened="showSortingTypePopover" <f7-popover class="sorting-type-popover-menu" :opened="showSortingTypePopover"
@popover:open="scrollPopoverToSelectedItem" @popover:open="scrollPopoverToSelectedItem"
@popover:opened="showSortingTypePopover = true" @popover:closed="showSortingTypePopover = false"> @popover:opened="showSortingTypePopover = true" @popover:closed="showSortingTypePopover = false">
<f7-list> <f7-list dividers>
<f7-list-item v-for="sortingType in allSortingTypes" <f7-list-item :title="$t(sortingType.name)"
:key="sortingType.type"
:class="{ 'list-item-selected': query.sortingType === sortingType.type }" :class="{ 'list-item-selected': query.sortingType === sortingType.type }"
:title="$t(sortingType.name)" :key="sortingType.type"
v-for="sortingType in allSortingTypes"
@click="setSortingType(sortingType.type)"> @click="setSortingType(sortingType.type)">
<f7-icon slot="after" class="list-item-checked-icon" f7="checkmark_alt" v-if="query.sortingType === sortingType.type"></f7-icon> <template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.sortingType === sortingType.type"></f7-icon>
</template>
</f7-list-item> </f7-list-item>
</f7-list> </f7-list>
</f7-popover> </f7-popover>
@@ -239,28 +207,31 @@
<f7-popover class="date-popover-menu" :opened="showDatePopover" <f7-popover class="date-popover-menu" :opened="showDatePopover"
@popover:open="scrollPopoverToSelectedItem" @popover:open="scrollPopoverToSelectedItem"
@popover:opened="showDatePopover = true" @popover:closed="showDatePopover = false"> @popover:opened="showDatePopover = true" @popover:closed="showDatePopover = false">
<f7-list> <f7-list dividers>
<f7-list-item v-for="dateRange in allDateRanges" <f7-list-item :title="$t(dateRange.name)"
:key="dateRange.type"
:class="{ 'list-item-selected': query.dateType === dateRange.type }" :class="{ 'list-item-selected': query.dateType === dateRange.type }"
:title="$t(dateRange.name)" :key="dateRange.type"
v-for="dateRange in allDateRanges"
@click="setDateFilter(dateRange.type)"> @click="setDateFilter(dateRange.type)">
<f7-icon slot="after" class="list-item-checked-icon" f7="checkmark_alt" v-if="query.dateType === dateRange.type"></f7-icon> <template #after>
<div slot="footer" <f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.dateType === dateRange.type"></f7-icon>
v-if="dateRange.type === allDateRanges.Custom.type && query.dateType === allDateRanges.Custom.type && query.startTime && query.endTime"> </template>
<span>{{ query.startTime | moment($t('format.datetime.long-without-second')) }}</span> <template #footer>
<span>&nbsp;-&nbsp;</span> <div v-if="dateRange.type === allDateRanges.Custom.type && query.dateType === allDateRanges.Custom.type && query.startTime && query.endTime">
<br/> <span>{{ $utilities.formatUnixTime(query.startTime, $t('format.datetime.long-without-second')) }}</span>
<span>{{ query.endTime | moment($t('format.datetime.long-without-second')) }}</span> <span>&nbsp;-&nbsp;</span>
</div> <br/>
<span>{{ $utilities.formatUnixTime(query.endTime, $t('format.datetime.long-without-second')) }}</span>
</div>
</template>
</f7-list-item> </f7-list-item>
</f7-list> </f7-list>
</f7-popover> </f7-popover>
<date-range-selection-sheet :title="$t('Custom Date Range')" <date-range-selection-sheet :title="$t('Custom Date Range')"
:show.sync="showCustomDateRangeSheet"
:min-time="query.startTime" :min-time="query.startTime"
:max-time="query.endTime" :max-time="query.endTime"
v-model:show="showCustomDateRangeSheet"
@dateRange:change="setCustomDateFilter"> @dateRange:change="setCustomDateFilter">
</date-range-selection-sheet> </date-range-selection-sheet>
@@ -281,6 +252,9 @@
<script> <script>
export default { export default {
props: [
'f7router'
],
data() { data() {
const self = this; const self = this;
@@ -305,6 +279,14 @@ export default {
query() { query() {
return this.$store.state.transactionStatisticsFilter; return this.$store.state.transactionStatisticsFilter;
}, },
queryChartDataTypeName() {
const queryChartDataTypeName = this.$utilities.getNameByKeyValue(this.allChartDataTypes, this.query.chartDataType, 'type', 'name', 'Statistics');
return this.$t(queryChartDataTypeName);
},
querySortingTypeName() {
const querySortingTypeName = this.$utilities.getNameByKeyValue(this.allSortingTypes, this.query.sortingType, 'type', 'name', 'System Default');
return this.$t(querySortingTypeName);
},
allChartDataTypes() { allChartDataTypes() {
return this.$constants.statistics.allChartDataTypes; return this.$constants.statistics.allChartDataTypes;
}, },
@@ -314,6 +296,23 @@ export default {
allDateRanges() { allDateRanges() {
return this.$constants.datetime.allDateRanges; return this.$constants.datetime.allDateRanges;
}, },
totalAmountName() {
if (this.query.chartDataType === this.allChartDataTypes.IncomeByAccount.type
|| this.query.chartDataType === this.allChartDataTypes.IncomeByPrimaryCategory.type
|| this.query.chartDataType === this.allChartDataTypes.IncomeBySecondaryCategory.type) {
return this.$t('Total Income');
} else if (this.query.chartDataType === this.allChartDataTypes.ExpenseByAccount.type
|| this.query.chartDataType === this.allChartDataTypes.ExpenseByPrimaryCategory.type
|| this.query.chartDataType === this.allChartDataTypes.ExpenseBySecondaryCategory.type) {
return this.$t('Total Expense');
} else if (this.query.chartDataType === this.allChartDataTypes.AccountTotalAssets.type) {
return this.$t('Total Assets');
} else if (this.query.chartDataType === this.allChartDataTypes.AccountTotalLiabilities.type) {
return this.$t('Total Liabilities');
}
return this.$t('Total Amount');
},
statisticsData() { statisticsData() {
const self = this; const self = this;
let combinedData = { let combinedData = {
@@ -466,7 +465,7 @@ export default {
this.reload(null); this.reload(null);
} }
this.$routeBackOnError('loadingError'); this.$routeBackOnError(this.f7router, 'loadingError');
}, },
reload(done) { reload(done) {
const self = this; const self = this;
@@ -649,16 +648,16 @@ export default {
return `${displayStartTime} ~ ${displayEndTime}`; return `${displayStartTime} ~ ${displayEndTime}`;
}, },
clickPieChartItem(item) { clickPieChartItem(item) {
this.$f7router.navigate(this.$options.filters.itemLinkUrl(item, this.query, this.allChartDataTypes)); this.f7router.navigate(this.getItemLinkUrl(item));
}, },
filterAccounts() { filterAccounts() {
this.$f7router.navigate('/statistic/filter/account'); this.f7router.navigate('/statistic/filter/account');
}, },
filterCategories() { filterCategories() {
this.$f7router.navigate('/statistic/filter/category'); this.f7router.navigate('/statistic/filter/category');
}, },
settings() { settings() {
this.$f7router.navigate('/statistic/settings'); this.f7router.navigate('/statistic/settings');
}, },
scrollPopoverToSelectedItem(event) { scrollPopoverToSelectedItem(event) {
if (!event || !event.$el || !event.$el.length) { if (!event || !event.$el || !event.$el.length) {
@@ -680,48 +679,53 @@ export default {
} }
container.scrollTop(targetPos); container.scrollTop(targetPos);
} },
}, getDisplayAmount(amount, currency, textLimit) {
filters: { amount = this.$locale.getDisplayCurrency(amount, currency);
finalAmount(amount, isShowAccountBalance, dataType, allChartDataTypes) {
if (!isShowAccountBalance && (dataType === allChartDataTypes.AccountTotalAssets.type || dataType === allChartDataTypes.AccountTotalLiabilities.type)) { if (!this.showAccountBalance
&& (this.query.chartDataType === this.allChartDataTypes.AccountTotalAssets.type
|| this.query.chartDataType === this.allChartDataTypes.AccountTotalLiabilities.type)
) {
return '***'; return '***';
} }
if (textLimit) {
this.$utilities.limitText(amount, textLimit);
}
return amount; return amount;
}, },
totalAmountName(dataType, allChartDataTypes) { getItemLinkUrl(item) {
if (dataType === allChartDataTypes.IncomeByAccount.type || dataType === allChartDataTypes.IncomeByPrimaryCategory.type || dataType === allChartDataTypes.IncomeBySecondaryCategory.type) {
return 'Total Income';
} else if (dataType === allChartDataTypes.ExpenseByAccount.type || dataType === allChartDataTypes.ExpenseByPrimaryCategory.type || dataType === allChartDataTypes.ExpenseBySecondaryCategory.type) {
return 'Total Expense';
} else if (dataType === allChartDataTypes.AccountTotalAssets.type) {
return 'Total Assets';
} else if (dataType === allChartDataTypes.AccountTotalLiabilities.type) {
return 'Total Liabilities';
}
return 'Total Amount';
},
itemLinkUrl(item, query, allChartDataTypes) {
const querys = []; const querys = [];
if (query.chartDataType === allChartDataTypes.IncomeByAccount.type || query.chartDataType === allChartDataTypes.IncomeByPrimaryCategory.type || query.chartDataType === allChartDataTypes.IncomeBySecondaryCategory.type) { if (this.query.chartDataType === this.allChartDataTypes.IncomeByAccount.type
|| this.query.chartDataType === this.allChartDataTypes.IncomeByPrimaryCategory.type
|| this.query.chartDataType === this.allChartDataTypes.IncomeBySecondaryCategory.type) {
querys.push('type=2'); querys.push('type=2');
} else if (query.chartDataType === allChartDataTypes.ExpenseByAccount.type || query.chartDataType === allChartDataTypes.ExpenseByPrimaryCategory.type || query.chartDataType === allChartDataTypes.ExpenseBySecondaryCategory.type) { } else if (this.query.chartDataType === this.allChartDataTypes.ExpenseByAccount.type
|| this.query.chartDataType === this.allChartDataTypes.ExpenseByPrimaryCategory.type
|| this.query.chartDataType === this.allChartDataTypes.ExpenseBySecondaryCategory.type) {
querys.push('type=3'); querys.push('type=3');
} }
if (query.chartDataType === allChartDataTypes.IncomeByAccount.type || query.chartDataType === allChartDataTypes.ExpenseByAccount.type || query.chartDataType === allChartDataTypes.AccountTotalAssets.type || query.chartDataType === allChartDataTypes.AccountTotalLiabilities.type) { if (this.query.chartDataType === this.allChartDataTypes.IncomeByAccount.type
|| this.query.chartDataType === this.allChartDataTypes.ExpenseByAccount.type
|| this.query.chartDataType === this.allChartDataTypes.AccountTotalAssets.type
|| this.query.chartDataType === this.allChartDataTypes.AccountTotalLiabilities.type) {
querys.push('accountId=' + item.id); querys.push('accountId=' + item.id);
} else if (query.chartDataType === allChartDataTypes.IncomeByPrimaryCategory.type || query.chartDataType === allChartDataTypes.IncomeBySecondaryCategory.type || query.chartDataType === allChartDataTypes.ExpenseByPrimaryCategory.type || query.chartDataType === allChartDataTypes.ExpenseBySecondaryCategory.type) { } else if (this.query.chartDataType === this.allChartDataTypes.IncomeByPrimaryCategory.type
|| this.query.chartDataType === this.allChartDataTypes.IncomeBySecondaryCategory.type
|| this.query.chartDataType === this.allChartDataTypes.ExpenseByPrimaryCategory.type
|| this.query.chartDataType === this.allChartDataTypes.ExpenseBySecondaryCategory.type) {
querys.push('categoryId=' + item.id); querys.push('categoryId=' + item.id);
} }
if (query.chartDataType !== allChartDataTypes.AccountTotalAssets.type && query.chartDataType !== allChartDataTypes.AccountTotalLiabilities.type) { if (this.query.chartDataType !== this.allChartDataTypes.AccountTotalAssets.type
querys.push('dateType=' + query.dateType); && this.query.chartDataType !== this.allChartDataTypes.AccountTotalLiabilities.type) {
querys.push('minTime=' + query.startTime); querys.push('dateType=' + this.query.dateType);
querys.push('maxTime=' + query.endTime); querys.push('minTime=' + this.query.startTime);
querys.push('maxTime=' + this.query.endTime);
} }
return '/transaction/list?' + querys.join('&'); return '/transaction/list?' + querys.join('&');
@@ -808,7 +812,7 @@ export default {
--f7-progressbar-bg-color: #f8f8f8; --f7-progressbar-bg-color: #f8f8f8;
} }
.theme-dark .statistics-percent-line .progressbar { .dark .statistics-percent-line .progressbar {
--f7-progressbar-bg-color: #161616; --f7-progressbar-bg-color: #161616;
} }
</style> </style>
+130 -134
View File
@@ -10,132 +10,115 @@
</f7-nav-right> </f7-nav-right>
</f7-navbar> </f7-navbar>
<f7-card class="skeleton-text" v-if="loading"> <f7-list strong inset dividers class="tag-item-list margin-top skeleton-text" v-if="loading">
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-item :key="itemIdx" v-for="itemIdx in [ 1, 2, 3 ]">
<f7-list> <template #media>
<f7-list-item> <f7-icon f7="number"></f7-icon>
<f7-block slot="title" class="no-padding"> </template>
<div class="display-flex"> <template #title>
<f7-icon slot="media" f7="number"></f7-icon> <div class="display-flex">
<div class="transaction-tag-list-item-content list-item-valign-middle padding-left-half">Tag Name</div> <div class="transaction-tag-list-item-content list-item-valign-middle padding-left-half">Tag Name</div>
</div> </div>
</f7-block> </template>
</f7-list-item> </f7-list-item>
<f7-list-item> </f7-list>
<f7-block slot="title" class="no-padding">
<div class="display-flex">
<f7-icon slot="media" f7="number"></f7-icon>
<div class="transaction-tag-list-item-content list-item-valign-middle padding-left-half">Tag Name 2</div>
</div>
</f7-block>
</f7-list-item>
<f7-list-item>
<f7-block slot="title" class="no-padding">
<div class="display-flex">
<f7-icon slot="media" f7="number"></f7-icon>
<div class="transaction-tag-list-item-content list-item-valign-middle padding-left-half">Tag Name 3</div>
</div>
</f7-block>
</f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-card v-else-if="!loading"> <f7-list strong inset dividers class="tag-item-list margin-top" v-if="!loading && noAvailableTag && !newTag">
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-item :title="$t('No available tag')"></f7-list-item>
<f7-list v-if="noAvailableTag && !newTag"> </f7-list>
<f7-list-item :title="$t('No available tag')"></f7-list-item>
</f7-list>
<f7-list sortable :sortable-enabled="sortable" @sortable:sort="onSort"> <f7-list strong inset dividers sortable class="tag-item-list margin-top"
<f7-list-item v-for="tag in tags" :sortable-enabled="sortable" @sortable:sort="onSort"
:key="tag.id" v-if="!loading">
:id="tag | tagDomId"
v-show="showHidden || !tag.hidden"
swipeout @taphold.native="setSortable()">
<f7-block slot="title" class="no-padding">
<div class="display-flex">
<f7-icon slot="media" f7="number">
<f7-badge color="gray" class="right-bottom-icon" v-if="tag.hidden">
<f7-icon f7="eye_slash_fill"></f7-icon>
</f7-badge>
</f7-icon>
<div class="transaction-tag-list-item-content list-item-valign-middle padding-left-half" <f7-list-item swipeout
v-if="editingTag.id !== tag.id"> :id="getTagDomId(tag)"
{{ tag.name }} :key="tag.id"
</div> v-for="tag in tags"
<f7-input class="list-title-input padding-left-half" v-show="showHidden || !tag.hidden"
type="text" @taphold="setSortable()">
:placeholder="$t('Tag Title')" <template #media>
:value="editingTag.name" <f7-icon f7="number">
v-else-if="editingTag.id === tag.id" <f7-badge color="gray" class="right-bottom-icon" v-if="tag.hidden">
@input="editingTag.name = $event.target.value" <f7-icon f7="eye_slash_fill"></f7-icon>
@keyup.enter.native="save(tag)"> </f7-badge>
</f7-input> </f7-icon>
</div> </template>
</f7-block> <template #title>
<f7-button slot="after" <div class="display-flex">
:class="{ 'no-padding': true, 'disabled': !isTagModified(tag) }" <div class="transaction-tag-list-item-content list-item-valign-middle padding-left-half"
raised fill v-if="editingTag.id !== tag.id">
icon-f7="checkmark_alt" {{ tag.name }}
color="blue" </div>
v-if="editingTag.id === tag.id" <f7-input class="list-title-input padding-left-half"
@click="save(editingTag)"> type="text"
</f7-button> :placeholder="$t('Tag Title')"
<f7-button slot="after" v-else-if="editingTag.id === tag.id"
class="no-padding margin-left-half" v-model:value="editingTag.name"
raised fill @keyup.enter="save(tag)">
icon-f7="xmark" </f7-input>
color="gray" </div>
v-if="editingTag.id === tag.id" </template>
@click="cancelSave(editingTag)"> <template #after>
</f7-button> <f7-button :class="{ 'no-padding': true, 'disabled': !isTagModified(tag) }"
<f7-swipeout-actions left v-if="sortable && editingTag.id !== tag.id"> raised fill
<f7-swipeout-button :color="tag.hidden ? 'blue' : 'gray'" class="padding-left padding-right" icon-f7="checkmark_alt"
overswipe close @click="hide(tag, !tag.hidden)"> color="blue"
<f7-icon :f7="tag.hidden ? 'eye' : 'eye_slash'"></f7-icon> v-if="editingTag.id === tag.id"
</f7-swipeout-button> @click="save(editingTag)">
</f7-swipeout-actions> </f7-button>
<f7-swipeout-actions right v-if="!sortable && editingTag.id !== tag.id"> <f7-button class="no-padding margin-left-half"
<f7-swipeout-button color="orange" close :text="$t('Edit')" @click="edit(tag)"></f7-swipeout-button> raised fill
<f7-swipeout-button color="red" class="padding-left padding-right" @click="remove(tag, false)"> icon-f7="xmark"
<f7-icon f7="trash"></f7-icon> color="gray"
</f7-swipeout-button> v-if="editingTag.id === tag.id"
</f7-swipeout-actions> @click="cancelSave(editingTag)">
</f7-list-item> </f7-button>
</template>
<f7-swipeout-actions left v-if="sortable && editingTag.id !== tag.id">
<f7-swipeout-button :color="tag.hidden ? 'blue' : 'gray'" class="padding-left padding-right"
overswipe close @click="hide(tag, !tag.hidden)">
<f7-icon :f7="tag.hidden ? 'eye' : 'eye_slash'"></f7-icon>
</f7-swipeout-button>
</f7-swipeout-actions>
<f7-swipeout-actions right v-if="!sortable && editingTag.id !== tag.id">
<f7-swipeout-button color="orange" close :text="$t('Edit')" @click="edit(tag)"></f7-swipeout-button>
<f7-swipeout-button color="red" class="padding-left padding-right" @click="remove(tag, false)">
<f7-icon f7="trash"></f7-icon>
</f7-swipeout-button>
</f7-swipeout-actions>
</f7-list-item>
<f7-list-item v-if="newTag"> <f7-list-item v-if="newTag">
<f7-block slot="title" class="no-padding"> <template #media>
<div class="display-flex"> <f7-icon f7="number"></f7-icon>
<f7-icon slot="media" f7="number"></f7-icon> </template>
<f7-input class="list-title-input padding-left-half" <template #title>
type="text" <div class="display-flex">
:placeholder="$t('Tag Title')" <f7-input class="list-title-input padding-left-half"
:value="newTag.name" type="text"
@input="newTag.name = $event.target.value" :placeholder="$t('Tag Title')"
@keyup.enter.native="save(newTag)"> v-model:value="newTag.name"
</f7-input> @keyup.enter="save(newTag)">
</div> </f7-input>
</f7-block> </div>
<f7-button slot="after" </template>
:class="{ 'no-padding': true, 'disabled': !isTagModified(newTag) }" <template #after>
raised fill <f7-button :class="{ 'no-padding': true, 'disabled': !isTagModified(newTag) }"
icon-f7="checkmark_alt" raised fill
color="blue" icon-f7="checkmark_alt"
@click="save(newTag)"> color="blue"
</f7-button> @click="save(newTag)">
<f7-button slot="after" </f7-button>
class="no-padding margin-left-half" <f7-button class="no-padding margin-left-half"
raised fill raised fill
icon-f7="xmark" icon-f7="xmark"
color="gray" color="gray"
@click="cancelSave(newTag)"> @click="cancelSave(newTag)">
</f7-button> </f7-button>
</f7-list-item> </template>
</f7-list> </f7-list-item>
</f7-card-content> </f7-list>
</f7-card>
<f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false"> <f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false">
<f7-actions-group> <f7-actions-group>
@@ -160,6 +143,9 @@
<script> <script>
export default { export default {
props: [
'f7router'
],
data() { data() {
return { return {
newTag: null, newTag: null,
@@ -216,7 +202,7 @@ export default {
}, },
methods: { methods: {
onPageAfterIn() { onPageAfterIn() {
this.$routeBackOnError('loadingError'); this.$routeBackOnError(this.f7router, 'loadingError');
}, },
reload(done) { reload(done) {
if (this.sortable || this.hasEditingTag) { if (this.sortable || this.hasEditingTag) {
@@ -254,12 +240,17 @@ export default {
onSort(event) { onSort(event) {
const self = this; const self = this;
if (!event || !event.el || !event.el.id || event.el.id.indexOf('tag_') !== 0) { if (!event || !event.el || !event.el.id) {
self.$toast('Unable to move tag'); self.$toast('Unable to move tag');
return; return;
} }
const id = event.el.id.substring(4); // tag_ const id = self.parseTagIdFromDomId(event.el.id);
if (!id) {
self.$toast('Unable to move tag');
return;
}
self.$store.dispatch('changeTagDisplayOrder', { self.$store.dispatch('changeTagDisplayOrder', {
tagId: id, tagId: id,
@@ -367,8 +358,6 @@ export default {
}, },
remove(tag, confirm) { remove(tag, confirm) {
const self = this; const self = this;
const app = self.$f7;
const $$ = app.$;
if (!tag) { if (!tag) {
self.$alert('An error has occurred'); self.$alert('An error has occurred');
@@ -388,9 +377,7 @@ export default {
self.$store.dispatch('deleteTag', { self.$store.dispatch('deleteTag', {
tag: tag, tag: tag,
beforeResolve: (done) => { beforeResolve: (done) => {
app.swipeout.delete($$(`#${self.$options.filters.tagDomId(tag)}`), () => { self.$ui.onSwipeoutDeleted(self.getTagDomId(tag), done);
done();
});
} }
}).then(() => { }).then(() => {
self.$hideLoading(); self.$hideLoading();
@@ -401,17 +388,26 @@ export default {
self.$toast(error.message || error); self.$toast(error.message || error);
} }
}); });
} },
}, getTagDomId(category) {
filters: {
tagDomId(category) {
return 'tag_' + category.id; return 'tag_' + category.id;
},
parseTagIdFromDomId(domId) {
if (!domId || domId.indexOf('tag_') !== 0) {
return null;
}
return domId.substring(4); // tag_
} }
} }
} }
</script> </script>
<style> <style>
.tag-item-list.list .item-media + .item-inner {
margin-left: 5px;
}
.transaction-tag-list-item-content { .transaction-tag-list-item-content {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
+261 -287
View File
@@ -1,5 +1,5 @@
<template> <template>
<f7-page @page:afterin="onPageAfterIn"> <f7-page with-subnavbar @page:afterin="onPageAfterIn">
<f7-navbar> <f7-navbar>
<f7-nav-left :back-link="$t('Back')"></f7-nav-left> <f7-nav-left :back-link="$t('Back')"></f7-nav-left>
<f7-nav-title :title="$t(title)"></f7-nav-title> <f7-nav-title :title="$t(title)"></f7-nav-title>
@@ -17,255 +17,255 @@
</f7-subnavbar> </f7-subnavbar>
</f7-navbar> </f7-navbar>
<f7-card class="skeleton-text" v-if="loading"> <f7-list strong inset dividers class="margin-vertical skeleton-text" v-if="loading">
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-item
<f7-list> class="transaction-edit-amount"
<f7-list-item style="font-size: 40px;"
class="transaction-edit-amount" header="Expense Amount" title="0.00">
style="font-size: 40px;" </f7-list-item>
header="Expense Amount" title="0.00"> <f7-list-item class="list-item-with-header-and-title list-item-title-hide-overflow" header="Category" title="Category Names"></f7-list-item>
</f7-list-item> <f7-list-item class="list-item-with-header-and-title" header="Account" title="Account Name"></f7-list-item>
<f7-list-item :no-chevron="mode === 'view'" class="list-item-with-header-and-title list-item-title-hide-overflow" header="Category" title="Category Names" link="#"></f7-list-item> <f7-list-input label="Transaction Time" placeholder="YYYY/MM/DD HH:mm"></f7-list-input>
<f7-list-item :no-chevron="mode === 'view'" class="list-item-with-header-and-title" header="Account" title="Account Name" link="#"></f7-list-item> <f7-list-item :no-chevron="mode === 'view'" class="list-item-with-header-and-title list-item-title-hide-overflow" header="Transaction Time Zone" title="(UTC XX:XX) System Default" link="#"></f7-list-item>
<f7-list-input label="Transaction Time" placeholder="YYYY/MM/DD HH:mm"></f7-list-input> <f7-list-item header="Tags">
<f7-list-item :no-chevron="mode === 'view'" class="list-item-with-header-and-title list-item-title-hide-overflow" header="Transaction Time Zone" title="(UTC XX:XX) System Default" link="#"></f7-list-item> <template #footer>
<f7-list-item :no-chevron="mode === 'view'" header="Tags" link="#"> <f7-block class="margin-top-half no-padding no-margin">
<f7-block class="margin-top-half no-padding" slot="footer"> <f7-chip class="transaction-edit-tag" text="None"></f7-chip>
<f7-chip class="transaction-edit-tag" text="None"></f7-chip> </f7-block>
</f7-block> </template>
</f7-list-item> </f7-list-item>
<f7-list-input type="textarea" label="Description" placeholder="Your transaction description (optional)"></f7-list-input> <f7-list-input type="textarea" label="Description" placeholder="Your transaction description (optional)"></f7-list-input>
</f7-list> </f7-list>
</f7-card-content>
</f7-card>
<f7-card v-else-if="!loading"> <f7-list form strong inset dividers
<f7-card-content class="no-safe-areas" :padding="false"> class="margin-vertical" :class="{ 'readonly': mode === 'view' }"
<f7-list form :class="{ 'readonly': mode === 'view' }"> v-else-if="!loading">
<f7-list-item <f7-list-item
class="transaction-edit-amount" class="transaction-edit-amount"
link="#" no-chevron link="#" no-chevron
:class="{ 'color-theme-teal': transaction.type === $constants.transaction.allTransactionTypes.Expense, 'color-theme-red': transaction.type === $constants.transaction.allTransactionTypes.Income }" :class="{ 'color-teal': transaction.type === $constants.transaction.allTransactionTypes.Expense, 'color-red': transaction.type === $constants.transaction.allTransactionTypes.Income }"
:style="{ fontSize: sourceAmountFontSize + 'px' }" :style="{ fontSize: sourceAmountFontSize + 'px' }"
:header="$t(sourceAmountName)" :header="$t(sourceAmountName)"
:title="transaction.sourceAmount | finalAmount(transaction.hideAmount) | currency" :title="getDisplayAmount(transaction.sourceAmount, transaction.hideAmount)"
@click="showSourceAmountSheet = true" @click="showSourceAmountSheet = true"
> >
<number-pad-sheet :min-value="$constants.transaction.minAmount" <number-pad-sheet :min-value="$constants.transaction.minAmount"
:max-value="$constants.transaction.maxAmount" :max-value="$constants.transaction.maxAmount"
:show.sync="showSourceAmountSheet" v-model:show="showSourceAmountSheet"
v-model="transaction.sourceAmount" v-model="transaction.sourceAmount"
></number-pad-sheet> ></number-pad-sheet>
</f7-list-item> </f7-list-item>
<f7-list-item <f7-list-item
class="transaction-edit-amount" class="transaction-edit-amount"
link="#" no-chevron link="#" no-chevron
:style="{ fontSize: destinationAmountFontSize + 'px' }" :style="{ fontSize: destinationAmountFontSize + 'px' }"
:header="$t('Transfer In Amount')" :header="$t('Transfer In Amount')"
:title="transaction.destinationAmount | finalAmount(transaction.hideAmount) | currency" :title="getDisplayAmount(transaction.destinationAmount, transaction.hideAmount)"
@click="showDestinationAmountSheet = true" @click="showDestinationAmountSheet = true"
v-if="transaction.type === $constants.transaction.allTransactionTypes.Transfer" v-if="transaction.type === $constants.transaction.allTransactionTypes.Transfer"
> >
<number-pad-sheet :min-value="$constants.transaction.minAmount" <number-pad-sheet :min-value="$constants.transaction.minAmount"
:max-value="$constants.transaction.maxAmount" :max-value="$constants.transaction.maxAmount"
:show.sync="showDestinationAmountSheet" v-model:show="showDestinationAmountSheet"
v-model="transaction.destinationAmount" v-model="transaction.destinationAmount"
></number-pad-sheet> ></number-pad-sheet>
</f7-list-item> </f7-list-item>
<f7-list-item <f7-list-item
class="list-item-with-header-and-title list-item-title-hide-overflow" class="list-item-with-header-and-title list-item-title-hide-overflow"
key="expenseCategorySelection" key="expenseCategorySelection"
link="#" :no-chevron="mode === 'view'" link="#" no-chevron
:class="{ 'disabled': !hasAvailableExpenseCategories }" :class="{ 'disabled': !hasAvailableExpenseCategories }"
:header="$t('Category')" :header="$t('Category')"
@click="showCategorySheet = true" @click="showCategorySheet = true"
v-if="transaction.type === $constants.transaction.allTransactionTypes.Expense" v-if="transaction.type === $constants.transaction.allTransactionTypes.Expense"
> >
<div slot="title" class="list-item-custom-title"> <template #title>
<span>{{ transaction.expenseCategory | primaryCategoryName(allCategories[$constants.category.allCategoryTypes.Expense]) }}</span> <div class="list-item-custom-title">
<f7-icon class="category-separate-icon" f7="chevron_right"></f7-icon> <span>{{ getPrimaryCategoryName(transaction.expenseCategory, allCategories[$constants.category.allCategoryTypes.Expense]) }}</span>
<span>{{ transaction.expenseCategory | secondaryCategoryName(allCategories[$constants.category.allCategoryTypes.Expense]) }}</span> <f7-icon class="category-separate-icon" f7="chevron_right"></f7-icon>
</div> <span>{{ getSecondaryCategoryName(transaction.expenseCategory, allCategories[$constants.category.allCategoryTypes.Expense]) }}</span>
<tree-view-selection-sheet primary-key-field="id" primary-value-field="id" primary-title-field="name" </div>
primary-icon-field="icon" primary-icon-type="category" primary-color-field="color" </template>
primary-sub-items-field="subCategories" <tree-view-selection-sheet primary-key-field="id" primary-value-field="id" primary-title-field="name"
secondary-key-field="id" secondary-value-field="id" secondary-title-field="name" primary-icon-field="icon" primary-icon-type="category" primary-color-field="color"
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color" primary-sub-items-field="subCategories"
:items="allCategories[$constants.category.allCategoryTypes.Expense]" secondary-key-field="id" secondary-value-field="id" secondary-title-field="name"
:show.sync="showCategorySheet" secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
v-model="transaction.expenseCategory"> :items="allCategories[$constants.category.allCategoryTypes.Expense]"
</tree-view-selection-sheet> v-model:show="showCategorySheet"
</f7-list-item> v-model="transaction.expenseCategory">
</tree-view-selection-sheet>
</f7-list-item>
<f7-list-item <f7-list-item
class="list-item-with-header-and-title list-item-title-hide-overflow" class="list-item-with-header-and-title list-item-title-hide-overflow"
key="incomeCategorySelection" key="incomeCategorySelection"
link="#" :no-chevron="mode === 'view'" link="#" no-chevron
:class="{ 'disabled': !hasAvailableIncomeCategories }" :class="{ 'disabled': !hasAvailableIncomeCategories }"
:header="$t('Category')" :header="$t('Category')"
@click="showCategorySheet = true" @click="showCategorySheet = true"
v-if="transaction.type === $constants.transaction.allTransactionTypes.Income" v-if="transaction.type === $constants.transaction.allTransactionTypes.Income"
> >
<div slot="title" class="list-item-custom-title"> <template #title>
<span>{{ transaction.incomeCategory | primaryCategoryName(allCategories[$constants.category.allCategoryTypes.Income]) }}</span> <div class="list-item-custom-title">
<f7-icon class="category-separate-icon" f7="chevron_right"></f7-icon> <span>{{ getPrimaryCategoryName(transaction.incomeCategory, allCategories[$constants.category.allCategoryTypes.Income]) }}</span>
<span>{{ transaction.incomeCategory | secondaryCategoryName(allCategories[$constants.category.allCategoryTypes.Income]) }}</span> <f7-icon class="category-separate-icon" f7="chevron_right"></f7-icon>
</div> <span>{{ getSecondaryCategoryName(transaction.incomeCategory, allCategories[$constants.category.allCategoryTypes.Income]) }}</span>
<tree-view-selection-sheet primary-key-field="id" primary-value-field="id" primary-title-field="name" </div>
primary-icon-field="icon" primary-icon-type="category" primary-color-field="color" </template>
primary-sub-items-field="subCategories" <tree-view-selection-sheet primary-key-field="id" primary-value-field="id" primary-title-field="name"
secondary-key-field="id" secondary-value-field="id" secondary-title-field="name" primary-icon-field="icon" primary-icon-type="category" primary-color-field="color"
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color" primary-sub-items-field="subCategories"
:items="allCategories[$constants.category.allCategoryTypes.Income]" secondary-key-field="id" secondary-value-field="id" secondary-title-field="name"
:show.sync="showCategorySheet" secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
v-model="transaction.incomeCategory"> :items="allCategories[$constants.category.allCategoryTypes.Income]"
</tree-view-selection-sheet> v-model:show="showCategorySheet"
</f7-list-item> v-model="transaction.incomeCategory">
</tree-view-selection-sheet>
</f7-list-item>
<f7-list-item <f7-list-item
class="list-item-with-header-and-title list-item-title-hide-overflow" class="list-item-with-header-and-title list-item-title-hide-overflow"
key="transferCategorySelection" key="transferCategorySelection"
link="#" :no-chevron="mode === 'view'" link="#" no-chevron
:class="{ 'disabled': !hasAvailableTransferCategories }" :class="{ 'disabled': !hasAvailableTransferCategories }"
:header="$t('Category')" :header="$t('Category')"
@click="showCategorySheet = true" @click="showCategorySheet = true"
v-if="transaction.type === $constants.transaction.allTransactionTypes.Transfer" v-if="transaction.type === $constants.transaction.allTransactionTypes.Transfer"
> >
<div slot="title" class="list-item-custom-title"> <template #title>
<span>{{ transaction.transferCategory | primaryCategoryName(allCategories[$constants.category.allCategoryTypes.Transfer]) }}</span> <div class="list-item-custom-title">
<f7-icon class="category-separate-icon" f7="chevron_right"></f7-icon> <span>{{ getPrimaryCategoryName(transaction.transferCategory, allCategories[$constants.category.allCategoryTypes.Transfer]) }}</span>
<span>{{ transaction.transferCategory | secondaryCategoryName(allCategories[$constants.category.allCategoryTypes.Transfer]) }}</span> <f7-icon class="category-separate-icon" f7="chevron_right"></f7-icon>
</div> <span>{{ getSecondaryCategoryName(transaction.transferCategory, allCategories[$constants.category.allCategoryTypes.Transfer]) }}</span>
<tree-view-selection-sheet primary-key-field="id" primary-value-field="id" primary-title-field="name" </div>
primary-icon-field="icon" primary-icon-type="category" primary-color-field="color" </template>
primary-sub-items-field="subCategories" <tree-view-selection-sheet primary-key-field="id" primary-value-field="id" primary-title-field="name"
secondary-key-field="id" secondary-value-field="id" secondary-title-field="name" primary-icon-field="icon" primary-icon-type="category" primary-color-field="color"
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color" primary-sub-items-field="subCategories"
:items="allCategories[$constants.category.allCategoryTypes.Transfer]" secondary-key-field="id" secondary-value-field="id" secondary-title-field="name"
:show.sync="showCategorySheet" secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
v-model="transaction.transferCategory"> :items="allCategories[$constants.category.allCategoryTypes.Transfer]"
</tree-view-selection-sheet> v-model:show="showCategorySheet"
</f7-list-item> v-model="transaction.transferCategory">
</tree-view-selection-sheet>
</f7-list-item>
<f7-list-item <f7-list-item
class="list-item-with-header-and-title" class="list-item-with-header-and-title"
link="#" :no-chevron="mode === 'view'" link="#" no-chevron
:class="{ 'disabled': !allVisibleAccounts.length }" :class="{ 'disabled': !allVisibleAccounts.length }"
:header="$t(sourceAccountName)" :header="$t(sourceAccountName)"
:title="transaction.sourceAccountId | optionName(allAccounts, 'id', 'name')" :title="$utilities.getNameByKeyValue(allAccounts, transaction.sourceAccountId, 'id', 'name')"
@click="showSourceAccountSheet = true" @click="showSourceAccountSheet = true"
> >
<two-column-list-item-selection-sheet primary-key-field="id" primary-value-field="category" <two-column-list-item-selection-sheet primary-key-field="id" primary-value-field="category"
primary-title-field="name" primary-footer-field="displayBalance" primary-title-field="name" primary-footer-field="displayBalance"
primary-icon-field="icon" primary-icon-type="account" primary-icon-field="icon" primary-icon-type="account"
primary-sub-items-field="accounts" primary-sub-items-field="accounts"
:primary-title-i18n="true" :primary-title-i18n="true"
secondary-key-field="id" secondary-value-field="id" secondary-key-field="id" secondary-value-field="id"
secondary-title-field="name" secondary-footer-field="displayBalance" secondary-title-field="name" secondary-footer-field="displayBalance"
secondary-icon-field="icon" secondary-icon-type="account" secondary-color-field="color" secondary-icon-field="icon" secondary-icon-type="account" secondary-color-field="color"
:items="categorizedAccounts" :items="categorizedAccounts"
:show.sync="showSourceAccountSheet" v-model:show="showSourceAccountSheet"
v-model="transaction.sourceAccountId"> v-model="transaction.sourceAccountId">
</two-column-list-item-selection-sheet> </two-column-list-item-selection-sheet>
</f7-list-item> </f7-list-item>
<f7-list-item <f7-list-item
class="list-item-with-header-and-title" class="list-item-with-header-and-title"
link="#" :no-chevron="mode === 'view'" link="#" no-chevron
:class="{ 'disabled': !allVisibleAccounts.length }" :class="{ 'disabled': !allVisibleAccounts.length }"
:header="$t('Destination Account')" :header="$t('Destination Account')"
:title="transaction.destinationAccountId | optionName(allAccounts, 'id', 'name')" :title="$utilities.getNameByKeyValue(allAccounts, transaction.destinationAccountId, 'id', 'name')"
v-if="transaction.type === $constants.transaction.allTransactionTypes.Transfer" v-if="transaction.type === $constants.transaction.allTransactionTypes.Transfer"
@click="showDestinationAccountSheet = true" @click="showDestinationAccountSheet = true"
> >
<two-column-list-item-selection-sheet primary-key-field="id" primary-value-field="category" <two-column-list-item-selection-sheet primary-key-field="id" primary-value-field="category"
primary-title-field="name" primary-footer-field="displayBalance" primary-title-field="name" primary-footer-field="displayBalance"
primary-icon-field="icon" primary-icon-type="account" primary-icon-field="icon" primary-icon-type="account"
primary-sub-items-field="accounts" primary-sub-items-field="accounts"
:primary-title-i18n="true" :primary-title-i18n="true"
secondary-key-field="id" secondary-value-field="id" secondary-key-field="id" secondary-value-field="id"
secondary-title-field="name" secondary-footer-field="displayBalance" secondary-title-field="name" secondary-footer-field="displayBalance"
secondary-icon-field="icon" secondary-icon-type="account" secondary-color-field="color" secondary-icon-field="icon" secondary-icon-type="account" secondary-color-field="color"
:items="categorizedAccounts" :items="categorizedAccounts"
:show.sync="showDestinationAccountSheet" v-model:show="showDestinationAccountSheet"
v-model="transaction.destinationAccountId"> v-model="transaction.destinationAccountId">
</two-column-list-item-selection-sheet> </two-column-list-item-selection-sheet>
</f7-list-item> </f7-list-item>
<f7-list-input <f7-list-item
:label="$t('Transaction Time')" class="list-item-with-header-and-title"
type="datepicker" link="#" no-chevron
class="transaction-edit-time" :header="$t('Transaction Time')"
:calendar-params="{ :title="$utilities.formatUnixTime($utilities.getActualUnixTimeForStore(transaction.time, $utilities.getTimezoneOffsetMinutes(), $utilities.getBrowserTimezoneOffsetMinutes()), this.$t('format.datetime.long'))"
timePicker: true, @click="showTransactionDateTimeSheet = true"
dateFormat: $t('input-format.datetime.long'), >
firstDay: defaultFirstDayOfWeek, <date-time-selection-sheet v-model:show="showTransactionDateTimeSheet"
toolbarCloseText: $t('Done'), v-model="transaction.time">
timePickerPlaceholder: $t('Select Time'), </date-time-selection-sheet>
timePickerFormat: $locale.getInputTimeIntlDateTimeFormatOptions(), </f7-list-item>
monthNames: $locale.getAllLongMonthNames(),
monthNamesShort: $locale.getAllShortMonthNames(),
dayNames: $locale.getAllLongWeekdayNames(),
dayNamesShort: $locale.getAllShortWeekdayNames()}"
:value="transaction.time"
@calendar:change="transaction.time = $event"
>
</f7-list-input>
<f7-list-item <f7-list-item
:no-chevron="mode === 'view'" :no-chevron="mode === 'view'"
class="list-item-with-header-and-title list-item-title-hide-overflow list-item-no-item-after" class="list-item-with-header-and-title list-item-title-hide-overflow list-item-no-item-after"
:header="$t('Transaction Time Zone')" :header="$t('Transaction Time Zone')"
smart-select :smart-select-params="{ openIn: 'popup', pageTitle: $t('Transaction Time Zone'), searchbar: true, searchbarPlaceholder: $t('Timezone'), searchbarDisableText: $t('Cancel'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }"> smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Timezone'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), pageTitle: $t('Transaction Time Zone'), popupCloseLinkText: $t('Done') }">
<select v-model="transaction.timeZone"> <select v-model="transaction.timeZone">
<option v-for="timezone in allTimezones" <option :value="timezone.name"
:key="timezone.name" :key="timezone.name"
:value="timezone.name">{{ `(UTC${timezone.utcOffset}) ${timezone.displayName}` }}</option> v-for="timezone in allTimezones">{{ `(UTC${timezone.utcOffset}) ${timezone.displayName}` }}</option>
</select> </select>
<f7-block slot="title" class="list-item-custom-title no-padding"> <template #title>
<span>{{ transaction.utcOffset | utcOffset }}</span> <f7-block class="list-item-custom-title no-padding no-margin">
<span class="transaction-edit-timezone-name" v-if="transaction.timeZone || transaction.timeZone === ''">{{ transaction.timeZone | optionName(allTimezones, 'name', 'displayName') }}</span> <span>{{ `(UTC${$utilities.getUtcOffsetByUtcOffsetMinutes(transaction.utcOffset)})` }}</span>
</f7-block> <span class="transaction-edit-timezone-name" v-if="transaction.timeZone || transaction.timeZone === ''">{{ $utilities.getNameByKeyValue(allTimezones, transaction.timeZone, 'name', 'displayName') }}</span>
</f7-list-item> </f7-block>
</template>
</f7-list-item>
<f7-list-item <f7-list-item
link="#" :no-chevron="mode === 'view'" link="#" no-chevron
:header="$t('Tags')" :header="$t('Tags')"
@click="showTransactionTagSheet = true" @click="showTransactionTagSheet = true"
> >
<transaction-tag-selection-sheet :items="allTags" <transaction-tag-selection-sheet :items="allTags"
:show.sync="showTransactionTagSheet" v-model:show="showTransactionTagSheet"
v-model="transaction.tagIds"> v-model="transaction.tagIds">
</transaction-tag-selection-sheet> </transaction-tag-selection-sheet>
<f7-block class="margin-top-half no-padding" slot="footer" v-if="transaction.tagIds && transaction.tagIds.length"> <template #footer>
<f7-chip class="transaction-edit-tag" media-bg-color="black" <f7-block class="margin-top-half no-padding no-margin" v-if="transaction.tagIds && transaction.tagIds.length">
v-for="tagId in transaction.tagIds" <f7-chip media-bg-color="black" class="transaction-edit-tag"
:key="tagId" :text="$utilities.getNameByKeyValue(allTags, tagId, 'id', 'name')"
:text="tagId | optionName(allTags, 'id', 'name')"> :key="tagId"
<f7-icon slot="media" f7="number"></f7-icon> v-for="tagId in transaction.tagIds">
</f7-chip> <template #media>
</f7-block> <f7-icon f7="number"></f7-icon>
<f7-block class="margin-top-half no-padding" slot="footer" v-else-if="!transaction.tagIds || !transaction.tagIds.length"> </template>
<f7-chip class="transaction-edit-tag" :text="$t('None')"> </f7-chip>
</f7-chip> </f7-block>
</f7-block> <f7-block class="margin-top-half no-padding no-margin" v-else-if="!transaction.tagIds || !transaction.tagIds.length">
</f7-list-item> <f7-chip class="transaction-edit-tag" :text="$t('None')">
</f7-chip>
</f7-block>
</template>
</f7-list-item>
<f7-list-input <f7-list-input
type="textarea" type="textarea"
class="transaction-edit-comment textarea-auto-size" class="transaction-edit-comment"
style="height: auto" style="height: auto"
:label="$t('Description')" :label="$t('Description')"
:placeholder="mode !== 'view' ? $t('Your transaction description (optional)') : ''" :placeholder="mode !== 'view' ? $t('Your transaction description (optional)') : ''"
:value="transaction.comment" v-textarea-auto-size
@input="transaction.comment = $event.target.value" v-model:value="transaction.comment"
></f7-list-input> ></f7-list-input>
</f7-list> </f7-list>
</f7-card-content>
</f7-card>
<f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false"> <f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false">
<f7-actions-group> <f7-actions-group>
@@ -287,9 +287,13 @@
<script> <script>
export default { export default {
props: [
'f7route',
'f7router'
],
data() { data() {
const self = this; const self = this;
const query = self.$f7route.query; const query = self.f7route.query;
const now = self.$utilities.getCurrentUnixTime(); const now = self.$utilities.getCurrentUnixTime();
const currentTimezone = self.$locale.getTimezone(); const currentTimezone = self.$locale.getTimezone();
@@ -306,8 +310,7 @@ export default {
editTransactionId: null, editTransactionId: null,
transaction: { transaction: {
type: defaultType, type: defaultType,
unixTime: now, time: now,
time: self.$utilities.getLocalDatetimeFromUnixTime(now),
timeZone: currentTimezone, timeZone: currentTimezone,
utcOffset: self.$utilities.getTimezoneOffsetMinutes(currentTimezone), utcOffset: self.$utilities.getTimezoneOffsetMinutes(currentTimezone),
expenseCategory: '', expenseCategory: '',
@@ -331,6 +334,7 @@ export default {
showCategorySheet: false, showCategorySheet: false,
showSourceAccountSheet: false, showSourceAccountSheet: false,
showDestinationAccountSheet: false, showDestinationAccountSheet: false,
showTransactionDateTimeSheet: false,
showTransactionTagSheet: false showTransactionTagSheet: false
}; };
}, },
@@ -407,9 +411,9 @@ export default {
const account = accountCategory.accounts[i]; const account = accountCategory.accounts[i];
if (this.showAccountBalance && account.isAsset) { if (this.showAccountBalance && account.isAsset) {
account.displayBalance = this.$options.filters.currency(account.balance, account.currency); account.displayBalance = this.$locale.getDisplayCurrency(account.balance, account.currency);
} else if (this.showAccountBalance && account.isLiability) { } else if (this.showAccountBalance && account.isLiability) {
account.displayBalance = this.$options.filters.currency(-account.balance, account.currency); account.displayBalance = this.$locale.getDisplayCurrency(-account.balance, account.currency);
} else { } else {
account.displayBalance = '***'; account.displayBalance = '***';
} }
@@ -448,7 +452,7 @@ export default {
totalBalance = totalBalance + '+'; totalBalance = totalBalance + '+';
} }
accountCategory.displayBalance = this.$options.filters.currency(totalBalance, this.defaultCurrency); accountCategory.displayBalance = this.$locale.getDisplayCurrency(totalBalance, this.defaultCurrency);
} else { } else {
accountCategory.displayBalance = '***'; accountCategory.displayBalance = '***';
} }
@@ -543,20 +547,6 @@ export default {
this.transaction.sourceAmount = newValue; this.transaction.sourceAmount = newValue;
} }
}, },
'transaction.time': function (newValue) {
if (this.$utilities.isArray(newValue)) {
newValue = newValue[0];
}
if (!newValue) {
newValue = this.$utilities.getLocalDatetimeFromUnixTime(this.$utilities.getCurrentUnixTime());
this.transaction.time = [newValue];
}
if (this.$utilities.getUnixTimeFromLocalDatetime(newValue) !== this.transaction.unixTime) {
this.transaction.unixTime = this.$utilities.getUnixTimeFromLocalDatetime(newValue);
}
},
'transaction.timeZone': function (newValue) { 'transaction.timeZone': function (newValue) {
for (let i = 0; i < this.allTimezones.length; i++) { for (let i = 0; i < this.allTimezones.length; i++) {
if (this.allTimezones[i].name === newValue) { if (this.allTimezones[i].name === newValue) {
@@ -568,11 +558,11 @@ export default {
}, },
created() { created() {
const self = this; const self = this;
const query = self.$f7route.query; const query = self.f7route.query;
if (self.$f7route.path === '/transaction/edit') { if (self.f7route.path === '/transaction/edit') {
self.mode = 'edit'; self.mode = 'edit';
} else if (self.$f7route.path === '/transaction/detail') { } else if (self.f7route.path === '/transaction/detail') {
self.mode = 'view'; self.mode = 'view';
} }
@@ -695,8 +685,7 @@ export default {
if (self.mode === 'edit' || self.mode === 'view') { if (self.mode === 'edit' || self.mode === 'view') {
self.transaction.utcOffset = transaction.utcOffset; self.transaction.utcOffset = transaction.utcOffset;
self.transaction.timeZone = null; self.transaction.timeZone = null;
self.transaction.unixTime = self.$utilities.getDummyUnixTimeForLocalUsage(transaction.time, self.transaction.utcOffset, self.$utilities.getBrowserTimezoneOffsetMinutes()); self.transaction.time = self.$utilities.getDummyUnixTimeForLocalUsage(transaction.time, self.transaction.utcOffset, self.$utilities.getBrowserTimezoneOffsetMinutes());
self.transaction.time = [self.$utilities.getLocalDatetimeFromUnixTime(self.transaction.unixTime)];
} }
self.transaction.sourceAccountId = transaction.sourceAccountId; self.transaction.sourceAccountId = transaction.sourceAccountId;
@@ -728,16 +717,13 @@ export default {
} }
}); });
}, },
updated: function () {
this.autoChangeCommentTextareaSize();
},
methods: { methods: {
onPageAfterIn() { onPageAfterIn() {
this.$routeBackOnError('loadingError'); this.$routeBackOnError(this.f7router, 'loadingError');
}, },
save() { save() {
const self = this; const self = this;
const router = self.$f7router; const router = self.f7router;
if (self.mode === 'view') { if (self.mode === 'view') {
return; return;
@@ -745,7 +731,7 @@ export default {
const submitTransaction = { const submitTransaction = {
type: self.transaction.type, type: self.transaction.type,
time: self.$utilities.getActualUnixTimeForStore(self.transaction.unixTime, self.transaction.utcOffset, self.$utilities.getBrowserTimezoneOffsetMinutes()), time: self.$utilities.getActualUnixTimeForStore(self.transaction.time, self.transaction.utcOffset, self.$utilities.getBrowserTimezoneOffsetMinutes()),
sourceAccountId: self.transaction.sourceAccountId, sourceAccountId: self.transaction.sourceAccountId,
sourceAmount: self.transaction.sourceAmount, sourceAmount: self.transaction.sourceAmount,
destinationAccountId: '0', destinationAccountId: '0',
@@ -809,16 +795,6 @@ export default {
doSubmit(); doSubmit();
} }
}, },
autoChangeCommentTextareaSize() {
const app = this.$f7;
const $$ = app.$;
$$('.textarea-auto-size textarea').each((idx, el) => {
el.scrollTop = 0;
el.style.height = '';
el.style.height = el.scrollHeight + 'px';
});
},
isCategoryIdAvailable(categories, categoryId) { isCategoryIdAvailable(categories, categoryId) {
if (!categories || !categories.length) { if (!categories || !categories.length) {
return false; return false;
@@ -853,17 +829,15 @@ export default {
} else { } else {
return 40; return 40;
} }
} },
}, getDisplayAmount(amount, hideAmount) {
filters: {
finalAmount(amount, hideAmount) {
if (hideAmount) { if (hideAmount) {
return '***'; return this.$locale.getDisplayCurrency('***');
} }
return amount; return this.$locale.getDisplayCurrency(amount);
}, },
primaryCategoryName(categoryId, allCategories) { getPrimaryCategoryName(categoryId, allCategories) {
for (let i = 0; i < allCategories.length; i++) { for (let i = 0; i < allCategories.length; i++) {
for (let j = 0; j < allCategories[i].subCategories.length; j++) { for (let j = 0; j < allCategories[i].subCategories.length; j++) {
const subCategory = allCategories[i].subCategories[j]; const subCategory = allCategories[i].subCategories[j];
@@ -875,7 +849,7 @@ export default {
return ''; return '';
}, },
secondaryCategoryName(categoryId, allCategories) { getSecondaryCategoryName(categoryId, allCategories) {
for (let i = 0; i < allCategories.length; i++) { for (let i = 0; i < allCategories.length; i++) {
for (let j = 0; j < allCategories[i].subCategories.length; j++) { for (let j = 0; j < allCategories[i].subCategories.length; j++) {
const subCategory = allCategories[i].subCategories[j]; const subCategory = allCategories[i].subCategories[j];
File diff suppressed because it is too large Load Diff
+43 -35
View File
@@ -2,39 +2,33 @@
<f7-page @page:afterin="onPageAfterIn"> <f7-page @page:afterin="onPageAfterIn">
<f7-navbar :title="$t('Data Management')" :back-link="$t('Back')"></f7-navbar> <f7-navbar :title="$t('Data Management')" :back-link="$t('Back')"></f7-navbar>
<f7-card class="skeleton-text" v-if="loading"> <f7-list strong inset dividers class="margin-vertical skeleton-text" v-if="loading">
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-item title="Accounts" after="Count"></f7-list-item>
<f7-list> <f7-list-item title="Transaction Categories" after="Count"></f7-list-item>
<f7-list-item title="Accounts" after="Count"></f7-list-item> <f7-list-item title="Transaction Tags" after="Count"></f7-list-item>
<f7-list-item title="Transaction Categories" after="Count"></f7-list-item> <f7-list-item title="Transactions" after="Count"></f7-list-item>
<f7-list-item title="Transaction Tags" after="Count"></f7-list-item> </f7-list>
<f7-list-item title="Transactions" after="Count"></f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-card v-else-if="!loading"> <f7-list strong inset dividers class="margin-vertical" v-else-if="!loading">
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-item :title="$t('Accounts')" :after="dataStatistics.totalAccountCount"></f7-list-item>
<f7-list> <f7-list-item :title="$t('Transaction Categories')" :after="dataStatistics.totalTransactionCategoryCount"></f7-list-item>
<f7-list-item :title="$t('Accounts')" :after="dataStatistics.totalAccountCount"></f7-list-item> <f7-list-item :title="$t('Transaction Tags')" :after="dataStatistics.totalTransactionTagCount"></f7-list-item>
<f7-list-item :title="$t('Transaction Categories')" :after="dataStatistics.totalTransactionCategoryCount"></f7-list-item> <f7-list-item :title="$t('Transactions')" :after="dataStatistics.totalTransactionCount"></f7-list-item>
<f7-list-item :title="$t('Transaction Tags')" :after="dataStatistics.totalTransactionTagCount"></f7-list-item> </f7-list>
<f7-list-item :title="$t('Transactions')" :after="dataStatistics.totalTransactionCount"></f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-card> <f7-list strong inset dividers class="margin-vertical" :class="{ 'disabled': loading }">
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-button :class="{ 'disabled': !dataStatistics || !dataStatistics.totalTransactionCount || dataStatistics.totalTransactionCount === '0' }"
<f7-list> v-if="isDataExportingEnabled"
<f7-list-button @click="exportedData = null; showExportDataSheet = true" v-if="isDataExportingEnabled">{{ $t('Export Data') }}</f7-list-button> @click="exportedData = null; showExportDataSheet = true">{{ $t('Export Data') }}</f7-list-button>
<f7-list-button color="red" @click="clearData(null)">{{ $t('Clear User Data') }}</f7-list-button> <f7-list-button color="red" @click="clearData(null)">{{ $t('Clear User Data') }}</f7-list-button>
</f7-list> </f7-list>
</f7-card-content>
</f7-card>
<f7-sheet style="height:auto" :opened="showExportDataSheet" @sheet:closed="showExportDataSheet = false; exportedData = null;"> <f7-sheet swipe-handler=".swipe-handler" style="height:auto"
<f7-page-content> :swipe-to-close="!exportingData" :close-on-escape="!exportingData"
:close-by-backdrop-click="!exportingData" :close-by-outside-click="!exportingData"
:opened="showExportDataSheet" @sheet:closed="showExportDataSheet = false; exportedData = null;">
<div class="swipe-handler"></div>
<f7-page-content class="margin-top no-padding-top">
<div class="display-flex padding justify-content-space-between align-items-center"> <div class="display-flex padding justify-content-space-between align-items-center">
<div style="font-size: 18px"><b>{{ $t('Are you sure you want to export all data to csv file?') }}</b></div> <div style="font-size: 18px"><b>{{ $t('Are you sure you want to export all data to csv file?') }}</b></div>
</div> </div>
@@ -51,9 +45,9 @@
<password-input-sheet :title="$t('Are you sure you want to clear all data?')" <password-input-sheet :title="$t('Are you sure you want to clear all data?')"
:hint="$t('You CANNOT undo this action. This will clear your accounts, categories, tags and transactions data. Please input your current password to confirm.')" :hint="$t('You CANNOT undo this action. This will clear your accounts, categories, tags and transactions data. Please input your current password to confirm.')"
:show.sync="showInputPasswordSheetForClearData"
:confirm-disabled="clearingData" :confirm-disabled="clearingData"
:cancel-disabled="clearingData" :cancel-disabled="clearingData"
v-model:show="showInputPasswordSheetForClearData"
v-model="currentPasswordForClearData" v-model="currentPasswordForClearData"
@password:confirm="clearData"> @password:confirm="clearData">
</password-input-sheet> </password-input-sheet>
@@ -62,6 +56,9 @@
<script> <script>
export default { export default {
props: [
'f7router'
],
data() { data() {
return { return {
loading: true, loading: true,
@@ -76,9 +73,6 @@ export default {
}; };
}, },
computed: { computed: {
currentTimezoneOffsetMinutes() {
return this.$utilities.getTimezoneOffsetMinutes();
},
isDataExportingEnabled() { isDataExportingEnabled() {
return this.$settings.isDataExportingEnabled(); return this.$settings.isDataExportingEnabled();
}, },
@@ -113,7 +107,7 @@ export default {
}, },
methods: { methods: {
onPageAfterIn() { onPageAfterIn() {
this.$routeBackOnError('loadingError'); this.$routeBackOnError(this.f7router, 'loadingError');
}, },
exportData() { exportData() {
const self = this; const self = this;
@@ -155,6 +149,20 @@ export default {
self.showInputPasswordSheetForClearData = false; self.showInputPasswordSheetForClearData = false;
self.$toast('All user data has been cleared'); self.$toast('All user data has been cleared');
self.loading = true;
self.$store.dispatch('getUserDataStatistics').then(dataStatistics => {
self.dataStatistics = dataStatistics;
self.loading = false;
}).catch(error => {
if (error.processed) {
self.loading = false;
} else {
self.loadingError = error;
self.$toast(error.message || error);
}
});
}).catch(error => { }).catch(error => {
self.clearingData = false; self.clearingData = false;
self.$hideLoading(); self.$hideLoading();
+83 -47
View File
@@ -4,46 +4,49 @@
<f7-nav-left :back-link="$t('Back')"></f7-nav-left> <f7-nav-left :back-link="$t('Back')"></f7-nav-left>
<f7-nav-title :title="$t('Device & Sessions')"></f7-nav-title> <f7-nav-title :title="$t('Device & Sessions')"></f7-nav-title>
<f7-nav-right> <f7-nav-right>
<f7-link :class="{ 'disabled': tokens.length < 2 }" :text="$t('Logout All')" @click="revokeAll"></f7-link> <f7-link :class="{ 'disabled': sessions.length < 2 }" :text="$t('Logout All')" @click="revokeAll"></f7-link>
</f7-nav-right> </f7-nav-right>
</f7-navbar> </f7-navbar>
<f7-card class="skeleton-text" v-if="loading"> <f7-list media-list strong inset dividers class="margin-top skeleton-text" v-if="loading">
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-item class="list-item-media-valign-middle"
<f7-list media-list> title="Current"
<f7-list-item class="list-item-media-valign-middle" text="Device Name (Browser xx.x.xxxx.xx)">
title="Current" <template #media>
text="Device Name (Browser xx.x.xxxx.xx)"> <f7-icon f7="device_phone_portrait"></f7-icon>
<f7-icon slot="media" f7="device_phone_portrait"></f7-icon> </template>
<small slot="after">MM/DD/YYYY HH:mm:ss</small> <template #after>
</f7-list-item> <small>MM/DD/YYYY HH:mm:ss</small>
</f7-list> </template>
</f7-card-content> </f7-list-item>
</f7-card> </f7-list>
<f7-card v-else-if="!loading"> <f7-list media-list strong inset dividers class="margin-top" v-else-if="!loading">
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-item class="list-item-media-valign-middle" swipeout
<f7-list media-list> :id="session.domId"
<f7-list-item class="list-item-media-valign-middle" swipeout :title="session.deviceType"
v-for="token in tokens" :text="session.deviceInfo"
:key="token.tokenId" :key="session.tokenId"
:id="token | tokenDomId" v-for="session in sessions">
:title="token | tokenTitle | localized" <template #media>
:text="token | tokenDevice | localized"> <f7-icon :f7="session.icon"></f7-icon>
<f7-icon slot="media" :f7="token | tokenIcon"></f7-icon> </template>
<small slot="after">{{ token.createdAt | moment($t('format.datetime.long')) }}</small> <template #after>
<f7-swipeout-actions right v-if="!token.isCurrent"> <small>{{ session.createdAt }}</small>
<f7-swipeout-button color="red" :text="$t('Log Out')" @click="revoke(token)"></f7-swipeout-button> </template>
</f7-swipeout-actions> <f7-swipeout-actions right v-if="!session.isCurrent">
</f7-list-item> <f7-swipeout-button color="red" :text="$t('Log Out')" @click="revoke(session)"></f7-swipeout-button>
</f7-list> </f7-swipeout-actions>
</f7-card-content> </f7-list-item>
</f7-card> </f7-list>
</f7-page> </f7-page>
</template> </template>
<script> <script>
export default { export default {
props: [
'f7router'
],
data() { data() {
return { return {
tokens: [], tokens: [],
@@ -51,6 +54,31 @@ export default {
loadingError: null loadingError: null
}; };
}, },
computed: {
sessions() {
if (!this.tokens) {
return this.tokens;
}
const sessions = [];
for (let i = 0; i < this.tokens.length; i++) {
const token = this.tokens[i];
sessions.push({
tokenId: token.tokenId,
domId: this.getTokenDomId(token.tokenId),
isCurrent: token.isCurrent,
deviceType: this.$t(token.isCurrent ? 'Current' : 'Other Device'),
deviceInfo: this.$utilities.parseDeviceInfo(token.userAgent),
icon: this.getTokenIcon(token),
createdAt: this.$utilities.formatUnixTime(token.createdAt, this.$t('format.datetime.long'))
});
}
return sessions;
}
},
created() { created() {
const self = this; const self = this;
@@ -70,7 +98,7 @@ export default {
}, },
methods: { methods: {
onPageAfterIn() { onPageAfterIn() {
this.$routeBackOnError('loadingError'); this.$routeBackOnError(this.f7router, 'loadingError');
}, },
reload(done) { reload(done) {
const self = this; const self = this;
@@ -91,22 +119,20 @@ export default {
} }
}); });
}, },
revoke(token) { revoke(session) {
const self = this; const self = this;
const app = self.$f7;
const $$ = app.$;
self.$confirm('Are you sure you want to logout from this session?', () => { self.$confirm('Are you sure you want to logout from this session?', () => {
self.$showLoading(); self.$showLoading();
self.$store.dispatch('revokeToken', { self.$store.dispatch('revokeToken', {
tokenId: token.tokenId tokenId: session.tokenId
}).then(() => { }).then(() => {
self.$hideLoading(); self.$hideLoading();
app.swipeout.delete($$(`#${self.$options.filters.tokenDomId(token)}`), () => { self.$ui.onSwipeoutDeleted(self.getTokenDomId(session.tokenId), () => {
for (let i = 0; i < self.tokens.length; i++) { for (let i = 0; i < self.tokens.length; i++) {
if (self.tokens[i].tokenId === token.tokenId) { if (self.tokens[i].tokenId === session.tokenId) {
self.tokens.splice(i, 1); self.tokens.splice(i, 1);
} }
} }
@@ -148,18 +174,28 @@ export default {
} }
}); });
}); });
} },
}, getTokenIcon(token) {
filters: { const ua = this.$utilities.parseUserAgent(token.userAgent);
tokenTitle(token) {
if (token.isCurrent) { if (!ua || !ua.device) {
return 'Current'; return this.$constants.icons.deviceIcons.desktop.f7Icon;
} }
return 'Other Device'; if (ua.device.type === 'mobile') {
return this.$constants.icons.deviceIcons.mobile.f7Icon;
} else if (ua.device.type === 'wearable') {
return this.$constants.icons.deviceIcons.wearable.f7Icon;
} else if (ua.device.type === 'tablet') {
return this.$constants.icons.deviceIcons.tablet.f7Icon;
} else if (ua.device.type === 'smarttv') {
return this.$constants.icons.deviceIcons.tv.f7Icon;
} else {
return this.$constants.icons.deviceIcons.desktop.f7Icon;
}
}, },
tokenDomId(token) { getTokenDomId(tokenId) {
return 'token_' + token.tokenId.replace(/:/g, '_'); return 'token_' + tokenId.replace(/:/g, '_');
} }
} }
}; };
+28 -38
View File
@@ -2,62 +2,44 @@
<f7-page @page:afterin="onPageAfterIn"> <f7-page @page:afterin="onPageAfterIn">
<f7-navbar :title="$t('Two-Factor Authentication')" :back-link="$t('Back')"></f7-navbar> <f7-navbar :title="$t('Two-Factor Authentication')" :back-link="$t('Back')"></f7-navbar>
<f7-card class="skeleton-text" v-if="loading"> <f7-list strong inset dividers class="margin-top skeleton-text" v-if="loading">
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-item title="Status" after="Unknown"></f7-list-item>
<f7-list> <f7-list-button class="disabled">Operate</f7-list-button>
<f7-list-item title="Status" after="Unknown"></f7-list-item> </f7-list>
<f7-list-button class="disabled">Operate</f7-list-button>
</f7-list>
</f7-card-content>
</f7-card>
<f7-card v-else-if="!loading && status === true"> <f7-list strong inset dividers class="margin-top" v-else-if="!loading">
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-item :title="$t('Status')" :after="$t(status ? 'Enabled' : 'Disabled')"></f7-list-item>
<f7-list> <f7-list-button :class="{ 'disabled': regenerating }" v-if="status === true" @click="regenerateBackupCode(null)">{{ $t('Regenerate Backup Codes') }}</f7-list-button>
<f7-list-item :title="$t('Status')" :after="$t('Enabled')"></f7-list-item> <f7-list-button :class="{ 'disabled': disabling }" v-if="status === true" @click="disable(null)">{{ $t('Disable') }}</f7-list-button>
<f7-list-button :class="{ 'disabled': regenerating }" @click="regenerateBackupCode(null)">{{ $t('Regenerate Backup Codes') }}</f7-list-button> <f7-list-button :class="{ 'disabled': enabling }" v-if="status === false" @click="enable">{{ $t('Enable') }}</f7-list-button>
<f7-list-button :class="{ 'disabled': disabling }" @click="disable(null)">{{ $t('Disable') }}</f7-list-button> </f7-list>
</f7-list>
</f7-card-content>
</f7-card>
<f7-card v-else-if="!loading && status === false"> <passcode-input-sheet :title="$t('Enable Two-Factor Authentication')"
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item :title="$t('Status')" :after="$t('Disabled')"></f7-list-item>
<f7-list-button :class="{ 'disabled': enabling }" @click="enable">{{ $t('Enable') }}</f7-list-button>
</f7-list>
</f7-card-content>
</f7-card>
<passcode-input-sheet :title="$t('Passcode')"
:hint="$t('Please use two factor authentication app scan the below qrcode and input current passcode')" :hint="$t('Please use two factor authentication app scan the below qrcode and input current passcode')"
:show.sync="showInputPasscodeSheetForEnable"
:confirm-disabled="enableConfirming" :confirm-disabled="enableConfirming"
:cancel-disabled="enableConfirming" :cancel-disabled="enableConfirming"
v-model:show="showInputPasscodeSheetForEnable"
v-model="currentPasscodeForEnable" v-model="currentPasscodeForEnable"
@passcode:confirm="enableConfirm"> @passcode:confirm="enableConfirm">
<div class="row"> <div class="col-100 text-align-center">
<div class="col-100 text-align-center"> <img alt="qrcode" class="img-qrcode" :src="new2FAQRCode" />
<img alt="qrcode" width="240px" height="240px" :src="new2FAQRCode" />
</div>
</div> </div>
</passcode-input-sheet> </passcode-input-sheet>
<password-input-sheet :title="$t('Current Password')" <password-input-sheet :title="$t('Disable Two-Factor Authentication')"
:hint="$t('Please enter your current password when disable two factor authentication')" :hint="$t('Please enter your current password when disable two factor authentication')"
:show.sync="showInputPasswordSheetForDisable"
:confirm-disabled="disabling" :confirm-disabled="disabling"
:cancel-disabled="disabling" :cancel-disabled="disabling"
v-model:show="showInputPasswordSheetForDisable"
v-model="currentPasswordForDisable" v-model="currentPasswordForDisable"
@password:confirm="disable"> @password:confirm="disable">
</password-input-sheet> </password-input-sheet>
<password-input-sheet :title="$t('Current Password')" <password-input-sheet :title="$t('Regenerate Backup Codes')"
:hint="$t('Please enter your current password when regenerate two factor authentication backup codes. If you regenerate backup codes, the old codes will be invalidated.')" :hint="$t('Please enter your current password when regenerate two factor authentication backup codes. If you regenerate backup codes, the old codes will be invalidated.')"
:show.sync="showInputPasswordSheetForRegenerate"
:confirm-disabled="regenerating" :confirm-disabled="regenerating"
:cancel-disabled="regenerating" :cancel-disabled="regenerating"
v-model:show="showInputPasswordSheetForRegenerate"
v-model="currentPasswordForRegenerate" v-model="currentPasswordForRegenerate"
@password:confirm="regenerateBackupCode"> @password:confirm="regenerateBackupCode">
</password-input-sheet> </password-input-sheet>
@@ -68,7 +50,7 @@
:information="currentBackupCode" :information="currentBackupCode"
:row-count="10" :row-count="10"
:enable-copy="true" :enable-copy="true"
:show.sync="showBackupCodeSheet" v-model:show="showBackupCodeSheet"
@info:copied="onBackupCodeCopied"> @info:copied="onBackupCodeCopied">
</information-sheet> </information-sheet>
</f7-page> </f7-page>
@@ -76,6 +58,9 @@
<script> <script>
export default { export default {
props: [
'f7router'
],
data() { data() {
return { return {
status: null, status: null,
@@ -116,7 +101,7 @@ export default {
}, },
methods: { methods: {
onPageAfterIn() { onPageAfterIn() {
this.$routeBackOnError('loadingError'); this.$routeBackOnError(this.f7router, 'loadingError');
}, },
enable() { enable() {
const self = this; const self = this;
@@ -245,6 +230,11 @@ export default {
</script> </script>
<style> <style>
.img-qrcode {
width: 240px;
height: 240px
}
.backup-code-sheet .information-content { .backup-code-sheet .information-content {
font-family: monospace; font-family: monospace;
} }
+123 -132
View File
@@ -8,152 +8,135 @@
</f7-nav-right> </f7-nav-right>
</f7-navbar> </f7-navbar>
<f7-card class="skeleton-text" v-if="loading"> <f7-list strong inset dividers class="margin-vertical skeleton-text" v-if="loading">
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-input label="Password" placeholder="Your password"></f7-list-input>
<f7-list> <f7-list-input label="Confirmation Password" placeholder="Re-enter the password"></f7-list-input>
<f7-list-input label="Password" placeholder="Your password"></f7-list-input> <f7-list-input label="E-mail" placeholder="Your email address"></f7-list-input>
<f7-list-input label="Confirmation Password" placeholder="Re-enter the password"></f7-list-input> <f7-list-input label="Nickname" placeholder="Your nickname"></f7-list-input>
<f7-list-input label="E-mail" placeholder="Your email address"></f7-list-input> </f7-list>
<f7-list-input label="Nickname" placeholder="Your nickname"></f7-list-input>
</f7-list>
</f7-card-content>
</f7-card>
<f7-card class="skeleton-text" v-if="loading"> <f7-list strong inset dividers class="margin-vertical skeleton-text" v-if="loading">
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-item class="list-item-with-header-and-title list-item-no-item-after" header="Default Currency" title="Currency" link="#"></f7-list-item>
<f7-list> <f7-list-item class="list-item-with-header-and-title list-item-no-item-after" header="Default Account" title="Not Specified"></f7-list-item>
<f7-list-item class="list-item-with-header-and-title list-item-no-item-after" header="Default Currency" title="Currency"></f7-list-item> <f7-list-item class="list-item-with-header-and-title list-item-no-item-after" header="First Day of Week" title="Week Day" link="#"></f7-list-item>
<f7-list-item class="list-item-with-header-and-title list-item-no-item-after" header="First Day of Week" title="Week Day"></f7-list-item> <f7-list-item class="list-item-with-header-and-title list-item-no-item-after" header="Editable Transaction Scope" title="All" link="#"></f7-list-item>
<f7-list-item class="list-item-with-header-and-title list-item-no-item-after" header="Editable Transaction Scope" title="All"></f7-list-item> </f7-list>
</f7-list>
</f7-card-content>
</f7-card>
<f7-card v-if="!loading"> <f7-list form strong inset dividers class="margin-vertical" v-if="!loading">
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-input
<f7-list form> type="password"
<f7-list-input autocomplete="new-password"
type="password" clear-button
autocomplete="new-password" :label="$t('Password')"
clear-button :placeholder="$t('Your password')"
:label="$t('Password')" v-model:value="newProfile.password"
:placeholder="$t('Your password')" ></f7-list-input>
:value="newProfile.password"
@input="newProfile.password = $event.target.value"
></f7-list-input>
<f7-list-input <f7-list-input
type="password" type="password"
autocomplete="new-password" autocomplete="new-password"
clear-button clear-button
:label="$t('Confirmation Password')" :label="$t('Confirmation Password')"
:placeholder="$t('Re-enter the password')" :placeholder="$t('Re-enter the password')"
:value="newProfile.confirmPassword" v-model:value="newProfile.confirmPassword"
@input="newProfile.confirmPassword = $event.target.value" ></f7-list-input>
></f7-list-input>
<f7-list-input <f7-list-input
type="email" type="email"
autocomplete="email" autocomplete="email"
clear-button clear-button
:label="$t('E-mail')" :label="$t('E-mail')"
:placeholder="$t('Your email address')" :placeholder="$t('Your email address')"
:value="newProfile.email" v-model:value="newProfile.email"
@input="newProfile.email = $event.target.value" ></f7-list-input>
></f7-list-input>
<f7-list-input <f7-list-input
type="text" type="text"
autocomplete="nickname" autocomplete="nickname"
clear-button clear-button
:label="$t('Nickname')" :label="$t('Nickname')"
:placeholder="$t('Your nickname')" :placeholder="$t('Your nickname')"
:value="newProfile.nickname" v-model:value="newProfile.nickname"
@input="newProfile.nickname = $event.target.value" ></f7-list-input>
></f7-list-input>
<f7-list-item class="ebk-list-item-error-info" v-if="inputIsInvalid" :footer="$t(inputInvalidProblemMessage)"></f7-list-item> <f7-list-item class="ebk-list-item-error-info" v-if="inputIsInvalid" :footer="$t(inputInvalidProblemMessage)"></f7-list-item>
</f7-list> </f7-list>
</f7-card-content>
</f7-card>
<f7-card v-if="!loading"> <f7-list form strong inset dividers class="margin-vertical" v-if="!loading">
<f7-card-content class="no-safe-areas" :padding="false"> <f7-list-item
<f7-list form> class="list-item-with-header-and-title list-item-no-item-after"
<f7-list-item :header="$t('Default Currency')"
class="list-item-with-header-and-title list-item-no-item-after" smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Currency Name'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), pageTitle: $t('Default Currency'), popupCloseLinkText: $t('Done') }"
:header="$t('Default Currency')" >
smart-select :smart-select-params="{ openIn: 'popup', pageTitle: $t('Default Currency'), searchbar: true, searchbarPlaceholder: $t('Currency Name'), searchbarDisableText: $t('Cancel'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }" <template #title>
> <f7-block class="no-padding no-margin">
<f7-block slot="title" class="no-padding no-margin"> <span>{{ $t(`currency.${newProfile.defaultCurrency}`) }}&nbsp;</span>
<span>{{ $t(`currency.${newProfile.defaultCurrency}`) }}&nbsp;</span> <small class="smaller">{{ newProfile.defaultCurrency }}</small>
<small class="smaller">{{ newProfile.defaultCurrency }}</small> </f7-block>
</f7-block> </template>
<select autocomplete="transaction-currency" v-model="newProfile.defaultCurrency"> <select autocomplete="transaction-currency" v-model="newProfile.defaultCurrency">
<option v-for="currency in allCurrencies" <option :value="currency.code"
:key="currency.code" :key="currency.code"
:value="currency.code">{{ currency.displayName }}</option> v-for="currency in allCurrencies">{{ currency.displayName }}</option>
</select> </select>
</f7-list-item> </f7-list-item>
<f7-list-item <f7-list-item
class="list-item-with-header-and-title" class="list-item-with-header-and-title"
link="#" link="#" no-chevron
:class="{ 'disabled': !allVisibleAccounts.length }" :class="{ 'disabled': !allVisibleAccounts.length }"
:header="$t('Default Account')" :header="$t('Default Account')"
:title="newProfile.defaultAccountId | optionName(allAccounts, 'id', 'name', $t('Not Specified'))" :title="$utilities.getNameByKeyValue(allAccounts, newProfile.defaultAccountId, 'id', 'name', $t('Not Specified'))"
@click="showAccountSheet = true" @click="showAccountSheet = true"
> >
<two-column-list-item-selection-sheet primary-key-field="id" primary-value-field="category" <two-column-list-item-selection-sheet primary-key-field="id" primary-value-field="category"
primary-title-field="name" primary-title-field="name"
primary-icon-field="icon" primary-icon-type="account" primary-icon-field="icon" primary-icon-type="account"
primary-sub-items-field="accounts" primary-sub-items-field="accounts"
:primary-title-i18n="true" :primary-title-i18n="true"
secondary-key-field="id" secondary-value-field="id" secondary-key-field="id" secondary-value-field="id"
secondary-title-field="name" secondary-title-field="name"
secondary-icon-field="icon" secondary-icon-type="account" secondary-color-field="color" secondary-icon-field="icon" secondary-icon-type="account" secondary-color-field="color"
:items="allCategorizedAccounts" :items="allCategorizedAccounts"
:show.sync="showAccountSheet" v-model:show="showAccountSheet"
v-model="newProfile.defaultAccountId"> v-model="newProfile.defaultAccountId">
</two-column-list-item-selection-sheet> </two-column-list-item-selection-sheet>
</f7-list-item> </f7-list-item>
<f7-list-item <f7-list-item
class="list-item-with-header-and-title list-item-no-item-after" class="list-item-with-header-and-title list-item-no-item-after"
:header="$t('First Day of Week')" :header="$t('First Day of Week')"
:title="newProfile.firstDayOfWeek | optionName(allWeekDays, 'type', 'name') | format('datetime.#{value}.long') | localized" :title="getDayOfWeekName(newProfile.firstDayOfWeek)"
smart-select :smart-select-params="{ openIn: 'popup', pageTitle: $t('First Day of Week'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }" smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Date'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), pageTitle: $t('First Day of Week'), popupCloseLinkText: $t('Done') }"
> >
<select v-model="newProfile.firstDayOfWeek"> <select v-model="newProfile.firstDayOfWeek">
<option v-for="weekDay in allWeekDays" <option :value="weekDay.type"
:key="weekDay.type" :key="weekDay.type"
:value="weekDay.type">{{ $t(`datetime.${weekDay.name}.long`) }}</option> v-for="weekDay in allWeekDays">{{ $t(`datetime.${weekDay.name}.long`) }}</option>
</select> </select>
</f7-list-item> </f7-list-item>
<f7-list-item <f7-list-item
class="list-item-with-header-and-title list-item-no-item-after" class="list-item-with-header-and-title list-item-no-item-after"
:header="$t('Editable Transaction Scope')" :header="$t('Editable Transaction Scope')"
:title="newProfile.transactionEditScope | optionName(allTransactionEditScopeTypes, 'value', 'name') | localized" :title="$t($utilities.getNameByKeyValue(allTransactionEditScopeTypes, newProfile.transactionEditScope, 'value', 'name'))"
smart-select :smart-select-params="{ openIn: 'popup', pageTitle: $t('Editable Transaction Scope'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }" smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Date Range'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), pageTitle: $t('Editable Transaction Scope'), popupCloseLinkText: $t('Done') }"
> >
<select v-model="newProfile.transactionEditScope"> <select v-model="newProfile.transactionEditScope">
<option v-for="option in allTransactionEditScopeTypes" <option :value="option.value"
:key="option.value" :key="option.value"
:value="option.value">{{ $t(option.name) }}</option> v-for="option in allTransactionEditScopeTypes">{{ $t(option.name) }}</option>
</select> </select>
</f7-list-item> </f7-list-item>
<f7-list-item class="ebk-list-item-error-info" v-if="extendInputIsInvalid" :footer="$t(extendInputInvalidProblemMessage)"></f7-list-item> <f7-list-item class="ebk-list-item-error-info" v-if="extendInputIsInvalid" :footer="$t(extendInputInvalidProblemMessage)"></f7-list-item>
</f7-list> </f7-list>
</f7-card-content>
</f7-card>
<password-input-sheet :title="$t('Current Password')" <password-input-sheet :title="$t('Current Password')"
:hint="$t('Please enter your current password when modifying your password')" :hint="$t('Please enter your current password when modifying your password')"
:show.sync="showInputPasswordSheet"
:confirm-disabled="saving" :confirm-disabled="saving"
:cancel-disabled="saving" :cancel-disabled="saving"
v-model:show="showInputPasswordSheet"
v-model="currentPassword" v-model="currentPassword"
@password:confirm="save()"> @password:confirm="save()">
</password-input-sheet> </password-input-sheet>
@@ -162,6 +145,9 @@
<script> <script>
export default { export default {
props: [
'f7router'
],
data() { data() {
return { return {
newProfile: { newProfile: {
@@ -318,11 +304,11 @@ export default {
}, },
methods: { methods: {
onPageAfterIn() { onPageAfterIn() {
this.$routeBackOnError('loadingError'); this.$routeBackOnError(this.f7router, 'loadingError');
}, },
save() { save() {
const self = this; const self = this;
const router = self.$f7router; const router = self.f7router;
self.showInputPasswordSheet = false; self.showInputPasswordSheet = false;
@@ -360,6 +346,11 @@ export default {
self.$toast(error.message || error); self.$toast(error.message || error);
} }
}); });
},
getDayOfWeekName(dayOfWeek) {
const weekName = this.$utilities.getNameByKeyValue(this.$constants.datetime.allWeekDays, dayOfWeek, 'type', 'name');
const i18nWeekNameKey = `datetime.${weekName}.long`;
return this.$t(i18nWeekNameKey);
} }
} }
}; };
+41 -29
View File
@@ -82,34 +82,22 @@
"licenseUrl": "https://github.com/stretchr/testify/blob/v1.8.2/LICENSE" "licenseUrl": "https://github.com/stretchr/testify/blob/v1.8.2/LICENSE"
}, },
{ {
"name": "vue", "name": "vuejs/core",
"copyright": "Copyright (c) 2013-present, Yuxi (Evan) You", "copyright": "Copyright (c) 2018-present, Yuxi (Evan) You",
"url": "https://github.com/vuejs/vue", "url": "https://github.com/vuejs/core",
"licenseUrl": "https://github.com/vuejs/vue/blob/v2.7.14/LICENSE" "licenseUrl": "https://github.com/vuejs/core/blob/v3.2.47/LICENSE"
}, },
{ {
"name": "vuex", "name": "vuex",
"copyright": "Copyright (c) 2015-present Evan You", "copyright": "Copyright (c) 2015-present Evan You",
"url": "https://github.com/vuejs/vuex", "url": "https://github.com/vuejs/vuex",
"licenseUrl": "https://github.com/vuejs/vuex/blob/v3.6.2/LICENSE" "licenseUrl": "https://github.com/vuejs/vuex/blob/v4.1.0/LICENSE"
}, },
{ {
"name": "vue-i18n", "name": "vue-i18n",
"copyright": "Copyright (c) 2016 kazuya kawaguchi", "copyright": "Copyright (c) 2016 kazuya kawaguchi",
"url": "https://github.com/kazupon/vue-i18n", "url": "https://github.com/intlify/vue-i18n-next",
"licenseUrl": "https://github.com/kazupon/vue-i18n/blob/v8.28.2/LICENSE" "licenseUrl": "https://github.com/intlify/vue-i18n-next/blob/v9.2.2/LICENSE"
},
{
"name": "vue-clipboard2",
"copyright": "Copyright (c) 2017 Inndy <inndy \\dot tw \\at gmail \\dot com>",
"url": "https://github.com/Inndy/vue-clipboard2",
"licenseUrl": "https://github.com/Inndy/vue-clipboard2/blob/v0.3.3/LICENSE"
},
{
"name": "vue-pincode-input",
"copyright": "Copyright (c) 2019 Maxim Noverin",
"url": "https://github.com/Seokky/vue-pincode-input",
"licenseUrl": "https://github.com/Seokky/vue-pincode-input/blob/master/LICENSE"
}, },
{ {
"name": "register-service-worker", "name": "register-service-worker",
@@ -117,23 +105,17 @@
"url": "https://github.com/yyx990803/register-service-worker", "url": "https://github.com/yyx990803/register-service-worker",
"licenseUrl": "https://github.com/yyx990803/register-service-worker/blob/v1.7.2/LICENSE" "licenseUrl": "https://github.com/yyx990803/register-service-worker/blob/v1.7.2/LICENSE"
}, },
{
"name": "core-js",
"copyright": "Copyright (c) 2014-2023 Denis Pushkarev",
"url": "https://github.com/zloirock/core-js",
"licenseUrl": "https://github.com/zloirock/core-js/blob/v3.30.0/LICENSE"
},
{ {
"name": "Framework7", "name": "Framework7",
"copyright": "Copyright (c) 2014 Vladimir Kharlampidi", "copyright": "Copyright (c) 2014 Vladimir Kharlampidi",
"url": "https://framework7.io/", "url": "https://framework7.io/",
"licenseUrl": "https://github.com/framework7io/framework7/blob/v5.7.14/LICENSE" "licenseUrl": "https://github.com/framework7io/framework7/blob/v8.0.3/LICENSE"
}, },
{ {
"name": "Framework7-vue", "name": "Framework7-vue",
"copyright": "Copyright (c) 2014 Vladimir Kharlampidi", "copyright": "Copyright (c) 2014 Vladimir Kharlampidi",
"url": "https://framework7.io/vue/", "url": "https://framework7.io/vue/",
"licenseUrl": "https://github.com/framework7io/framework7/blob/v5.7.14/LICENSE" "licenseUrl": "https://github.com/framework7io/framework7/blob/v8.0.3/LICENSE"
}, },
{ {
"name": "Framework7-icons", "name": "Framework7-icons",
@@ -141,11 +123,35 @@
"url": "https://framework7.io/icons/", "url": "https://framework7.io/icons/",
"licenseUrl": "https://github.com/framework7io/framework7-icons/blob/v5.0.5/LICENSE" "licenseUrl": "https://github.com/framework7io/framework7-icons/blob/v5.0.5/LICENSE"
}, },
{
"name": "Dom7",
"copyright": "Copyright (c) 2017 Vladimir Kharlampidi",
"url": "https://framework7.io/docs/dom7.html",
"licenseUrl": "https://github.com/nolimits4web/dom7/blob/v4.0.6/LICENSE"
},
{
"name": "Swiper",
"copyright": "Copyright (c) 2019 Vladimir Kharlampidi",
"url": "https://swiperjs.com",
"licenseUrl": "https://github.com/nolimits4web/swiper/blob/v9.2.3/LICENSE"
},
{
"name": "Skeleton Elements",
"copyright": "Copyright (c) 2020 Vladimir Kharlampidi",
"url": "https://github.com/nolimits4web/skeleton-elements",
"licenseUrl": "https://github.com/nolimits4web/skeleton-elements/blob/v4.0.1/LICENSE"
},
{
"name": "@vuepic/vue-datepicker",
"copyright": "Copyright (c) 2021-present Vuepic",
"url": "https://vue3datepicker.com/",
"licenseUrl": "https://github.com/Vuepic/vue-datepicker/blob/v4.4.0/LICENSE"
},
{ {
"name": "axios", "name": "axios",
"copyright": "Copyright (c) 2014-present Matt Zabriskie", "copyright": "Copyright (c) 2014-present Matt Zabriskie",
"url": "https://github.com/axios/axios", "url": "https://axios-http.com",
"licenseUrl": "https://github.com/axios/axios/blob/v1.3.5/LICENSE" "licenseUrl": "https://github.com/axios/axios/blob/v1.3.6/LICENSE"
}, },
{ {
"name": "Moment.js", "name": "Moment.js",
@@ -171,6 +177,12 @@
"url": "https://github.com/paroga/cbor-js", "url": "https://github.com/paroga/cbor-js",
"licenseUrl": "https://github.com/paroga/cbor-js/blob/v0.1.0/LICENSE" "licenseUrl": "https://github.com/paroga/cbor-js/blob/v0.1.0/LICENSE"
}, },
{
"name": "clipboard.js",
"copyright": "Copyright (c) Zeno Rocha",
"url": "https://clipboardjs.com",
"licenseUrl": "https://github.com/zenorocha/clipboard.js/blob/v2.0.11/LICENSE"
},
{ {
"name": "js-cookie", "name": "js-cookie",
"copyright": "Copyright (c) 2018 Copyright 2018 Klaus Hartl, Fagner Brack, GitHub Contributors", "copyright": "Copyright (c) 2018 Copyright 2018 Klaus Hartl, Fagner Brack, GitHub Contributors",
+148
View File
@@ -0,0 +1,148 @@
import fs from 'fs';
import { resolve } from 'path';
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue';
import { VitePWA } from 'vite-plugin-pwa';
import git from 'git-rev-sync';
import packageFile from './package.json';
import thirdPartyLicenseFile from './third-patry-dependencies.json';
const SRC_DIR = resolve(__dirname, './src');
const PUBLIC_DIR = resolve(__dirname, './public');
const BUILD_DIR = resolve(__dirname, './dist',);
export default defineConfig(async () => {
const licenseContent = fs.readFileSync('./LICENSE', 'UTF-8');
let buildUnixTime = '';
for (let i = 0; i < process.argv.length; i++) {
if (process.argv[i].indexOf('--') !== 0) {
continue;
}
const pairs = process.argv[i].split('=');
if (pairs[0] === '--buildUnixTime') {
buildUnixTime = pairs[1];
}
}
return {
root: SRC_DIR,
publicDir: PUBLIC_DIR,
base: './',
define: {
__EZBOOKKEEPING_VERSION__: JSON.stringify(packageFile.version),
__EZBOOKKEEPING_BUILD_UNIX_TIME__: JSON.stringify(buildUnixTime),
__EZBOOKKEEPING_BUILD_COMMIT_HASH__: JSON.stringify(git.short()),
__EZBOOKKEEPING_LICENSE__: JSON.stringify(licenseContent),
__EZBOOKKEEPING_THIRD_PARTY_LICENSES__: JSON.stringify(thirdPartyLicenseFile)
},
plugins: [
vue({
template: {
compilerOptions: {
isCustomElement: (tag) => tag.includes('swiper-')
}
}
}),
VitePWA({
filename: 'sw.js',
manifestFilename: 'manifest.json',
strategies: 'generateSW',
injectRegister: 'null',
manifest: {
name: 'ezBookkeeping',
short_name: 'ezBookkeeping',
description: 'A lightweight personal bookkeeping app hosted by yourself.',
theme_color: '#C67E48',
background_color: '#F6F7F8',
start_url: './',
scope: './',
display: 'standalone',
related_applications: [],
prefer_related_applications: false,
icons: [
{
src: 'img/ezbookkeeping-192.png',
sizes: '192x192',
type: 'image/png'
},
{
src: 'img/ezbookkeeping-512.png',
sizes: '512x512',
type: 'image/png'
}
]
},
workbox: {
globDirectory: 'dist/',
globPatterns: ['**/*.{js,css,html,ico,png,jpg,jpeg,gif,tiff,bmp,ttf,woff,woff2,svg,eot}'],
globIgnores: [
'index.html',
'mobile.html',
'desktop.html',
'robots.txt',
'css/desktop-*.js',
'js/desktop-*.js'
],
navigateFallback: '',
skipWaiting: true,
clientsClaim: true
}
})
],
build: {
outDir: BUILD_DIR,
sourcemap: false,
assetsInlineLimit: 0,
emptyOutDir: true,
rollupOptions: {
input: {
index: resolve(SRC_DIR, 'index.html'),
desktop: resolve(SRC_DIR, 'desktop.html'),
mobile: resolve(SRC_DIR, 'mobile.html')
},
output: {
assetFileNames: (assetInfo) => {
const fileExt = assetInfo.name.split('.').at(1);
let assetType = fileExt;
if (/png|jpe?g|gif|tiff|bmp|ico/i.test(fileExt)) {
assetType = 'img';
} else if (/ttf|woff|woff2|svg|eot/i.test(fileExt)) {
assetType = 'fonts';
}
return `${assetType}/[name]-[hash][extname]`;
},
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'js/[name]-[hash].js',
manualChunks: function (id) {
if (/[\\/]node_modules[\\/]/i.test(id)) {
return 'vendor';
}
}
},
treeshake: false
},
},
resolve: {
alias: {
'@': SRC_DIR,
},
},
server: {
host: '0.0.0.0',
port: 8081,
strictPort: true,
proxy: {
'/api': {
target: 'http://127.0.0.1:8080/',
changeOrigin: true
}
}
},
};
})
-161
View File
@@ -1,161 +0,0 @@
const fs = require('fs');
const GitRevisionPlugin = require('git-revision-webpack-plugin');
const MomentLocalesPlugin = require('moment-locales-webpack-plugin');
const pkgFile = require('./package.json');
const thirdPartyLicenseFile = require('./third-patry-dependencies.json');
const licenseFile = fs.readFileSync('./LICENSE', 'UTF-8');
module.exports = {
pages: {
index: {
entry: 'src/index-main.js',
template: 'src/public/index.html',
filename: 'index.html',
chunks: ['vendors-common-bundle', 'vendors-index-bundle', 'common-bundle', 'index']
},
desktop: {
entry: 'src/desktop-main.js',
template: 'src/public/desktop.html',
filename: 'desktop.html',
chunks: ['vendors-common-bundle', 'vendors-desktop-bundle', 'common-bundle', 'desktop']
},
mobile: {
entry: 'src/mobile-main.js',
template: 'src/public/mobile.html',
filename: 'mobile.html',
chunks: ['vendors-common-bundle', 'vendors-mobile-bundle', 'common-bundle', 'mobile']
}
},
publicPath: '',
productionSourceMap: false,
configureWebpack: {
plugins: [
new MomentLocalesPlugin()
]
},
chainWebpack: config => {
config.optimization.splitChunks({
cacheGroups: {
'vendors-common-bundle': {
name: 'vendors-common-bundle',
test: /[\\/]node_modules[\\/]/,
chunks: 'initial',
priority: 10,
minChunks: 2
},
'vendors-bundle': {
name: (module, chunks) => {
const allChunksNames = chunks.map((item) => item.name).join('-');
return `vendors-${allChunksNames}-bundle`;
},
test: /[\\/]node_modules[\\/]/,
chunks: 'initial',
priority: 5,
minChunks: 1
},
'common-bundle': {
name: 'common-bundle',
chunks: 'initial',
priority: 1,
minChunks: 2
}
}
});
config.plugin('define').tap(definitions => {
let buildUnixTime = '';
for (let i = 0; i < process.argv.length; i++) {
if (process.argv[i].indexOf('--') !== 0) {
continue;
}
const pairs = process.argv[i].split('=');
if (pairs[0] === '--buildUnixTime') {
buildUnixTime = pairs[1];
}
}
const gitRevisionPlugin = new GitRevisionPlugin();
definitions[0]['process.env']['VERSION'] = JSON.stringify(pkgFile.version);
definitions[0]['process.env']['COMMIT_HASH'] = JSON.stringify(gitRevisionPlugin.commithash());
definitions[0]['process.env']['BUILD_UNIXTIME'] = buildUnixTime;
definitions[0]['process.env']['LICENSE'] = JSON.stringify(licenseFile.trim());
definitions[0]['process.env']['THIRD_PARTY_LICENSES'] = JSON.stringify(thirdPartyLicenseFile);
return definitions;
});
},
pwa: {
name: 'ezBookkeeping',
themeColor: '#C67E48',
appleMobileWebAppCapable: 'yes',
appleMobileWebAppStatusBarStyle: 'default',
workboxPluginMode: 'GenerateSW',
manifestPath: 'manifest.json',
manifestOptions: {
short_name: 'ezBookkeeping',
icons: [
{
src: "img/ezbookkeeping-192.png",
sizes: "192x192",
type: "image/png"
},
{
src: "img/ezbookkeeping-512.png",
sizes: "512x512",
type: "image/png"
}
],
start_url: './',
scope: "./",
display: 'standalone',
background_color: "#F6F7F8",
related_applications: [],
prefer_related_applications: false
},
iconPaths: {
favicon32: 'favicon.png',
favicon16: 'favicon.ico',
appleTouchIcon: 'touchicon.png',
},
workboxOptions: {
importWorkboxFrom: 'local',
importsDirectory: 'sw',
precacheManifestFilename: 'precache-manifest.[manifestHash].js',
directoryIndex: '/',
globDirectory: '.',
templatedURLs: {
'/': [
'src/public/mobile.html'
]
},
exclude: [
/^index\.html$/,
/^mobile\.html$/,
/^desktop\.html$/,
/^robots\.txt$/,
/^(css|js)\/desktop\.[a-z0-9]+\.js$/,
/^(css|js)\/vendors-desktop-bundle\.[a-z0-9]+\.js$/,
],
skipWaiting: true,
clientsClaim: true,
swDest: 'sw.js'
}
},
devServer: {
host: '0.0.0.0',
port: 8081,
disableHostCheck: true,
proxy: {
'/api': {
target: 'http://127.0.0.1:8080/',
changeOrigin: true
}
}
}
}