mirror of
https://github.com/fikte/calendar-period-week-notes.git
synced 2026-07-22 06:44:03 +00:00
Major new feature: set goals
This commit is contained in:
parent
be3b46c578
commit
95dbfbe6f1
20 changed files with 2954 additions and 479 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -17,7 +17,7 @@ Thumbs.db
|
|||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
calendar-period-week-notes.code-workspace
|
||||
calendar-period-week-notes.code-workspace
|
||||
|
||||
#Local Scripts
|
||||
deploy-to-obsidian.sh
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "calendar-period-week-notes",
|
||||
"name": "Calendar Period Week Notes",
|
||||
"version": "1.4.2",
|
||||
"version": "1.5.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "A calendar grid plugin with a scratchpad area, notes list, pinned notes, tasks and asset views.",
|
||||
"author": "Tim Williams",
|
||||
|
|
|
|||
353
package-lock.json
generated
353
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "calendar-period-week-notes",
|
||||
"version": "1.4.2",
|
||||
"version": "1.5.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "calendar-period-week-notes",
|
||||
"version": "1.4.2",
|
||||
"version": "1.5.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ical.js": "^2.2.1",
|
||||
|
|
@ -21,15 +21,11 @@
|
|||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/plugin-commonjs": {
|
||||
"version": "28.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.9.tgz",
|
||||
"integrity": "sha512-PIR4/OHZ79romx0BVVll/PkwWpJ7e5lsqFa3gFfcrFPWwLXLV39JVUzQV9RKjWerE7B845Hqjj9VYlQeieZ2dA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -55,8 +51,6 @@
|
|||
},
|
||||
"node_modules/@rollup/plugin-json": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz",
|
||||
"integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -76,8 +70,6 @@
|
|||
},
|
||||
"node_modules/@rollup/plugin-node-resolve": {
|
||||
"version": "16.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz",
|
||||
"integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -101,8 +93,6 @@
|
|||
},
|
||||
"node_modules/@rollup/pluginutils": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
|
||||
"integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -122,38 +112,8 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.53.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz",
|
||||
"integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.53.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz",
|
||||
"integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.53.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz",
|
||||
"integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -164,297 +124,23 @@
|
|||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.53.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz",
|
||||
"integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.53.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz",
|
||||
"integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.53.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz",
|
||||
"integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.53.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz",
|
||||
"integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.53.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz",
|
||||
"integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.53.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz",
|
||||
"integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.53.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz",
|
||||
"integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.53.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz",
|
||||
"integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.53.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz",
|
||||
"integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.53.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz",
|
||||
"integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.53.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz",
|
||||
"integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.53.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz",
|
||||
"integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.53.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz",
|
||||
"integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.53.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz",
|
||||
"integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.53.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz",
|
||||
"integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.53.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz",
|
||||
"integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.53.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz",
|
||||
"integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.53.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz",
|
||||
"integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.53.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz",
|
||||
"integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/commondir": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
|
||||
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
@ -463,15 +149,11 @@
|
|||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
@ -488,10 +170,7 @@
|
|||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -503,8 +182,6 @@
|
|||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
|
|
@ -513,8 +190,6 @@
|
|||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -526,14 +201,10 @@
|
|||
},
|
||||
"node_modules/ical.js": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ical.js/-/ical.js-2.2.1.tgz",
|
||||
"integrity": "sha512-yK/UlPbEs316igb/tjRgbFA8ZV75rCsBJp/hWOatpyaPNlgw0dGDmU+FoicOcwX4xXkeXOkYiOmCqNPFpNPkQg==",
|
||||
"license": "MPL-2.0"
|
||||
},
|
||||
"node_modules/is-core-module": {
|
||||
"version": "2.16.1",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
||||
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -548,15 +219,11 @@
|
|||
},
|
||||
"node_modules/is-module": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
|
||||
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-reference": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
|
||||
"integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -565,8 +232,6 @@
|
|||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -575,8 +240,6 @@
|
|||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
|
|
@ -584,18 +247,13 @@
|
|||
},
|
||||
"node_modules/path-parse": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
@ -605,8 +263,6 @@
|
|||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.11",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -626,11 +282,8 @@
|
|||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.53.3",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
|
||||
"integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
|
|
@ -669,8 +322,6 @@
|
|||
},
|
||||
"node_modules/supports-preserve-symlinks-flag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "calendar-period-week-notes",
|
||||
"version": "1.4.2",
|
||||
"version": "1.5.0",
|
||||
"description": "A calendar based Obsidian plugin.",
|
||||
"main": "main.js",
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -3,13 +3,17 @@ export const VIEW_TYPE_PERIOD = "calendar-period-week";
|
|||
|
||||
export const DASHBOARDWIDGETS = {
|
||||
tasks: {
|
||||
weeklyGoalPoints: { name: 'Weekly points score (Full)' },
|
||||
goalStatusList: { name: 'Daily goals (Full)' },
|
||||
goalStatusListCondensed: { name: 'Daily goals (Half-width)', default: false },
|
||||
weeklyGoalPointsCondensed: { name: 'Weekly momentum (Half-width)', default: false },
|
||||
today: { name: 'Today' },
|
||||
tomorrow: { name: 'Tomorrow' },
|
||||
next7days: { name: 'Next 7 days' },
|
||||
futureNoDue: { name: 'Future / No due' },
|
||||
upcomingoverdue: { name: 'Upcoming & overdue tasks' },
|
||||
taskstatusoverview: { name: 'Task status overview' },
|
||||
taskcompletionheatmap: { name: 'Task completion activity' }
|
||||
taskcompletionheatmap: { name: 'Task completion activity' },
|
||||
},
|
||||
creation: {
|
||||
allfilesheatmap: { name: 'All files' },
|
||||
|
|
@ -253,7 +257,8 @@ export const DEFAULT_SETTINGS = {
|
|||
regularnotesheatmap: true,
|
||||
assetsheatmap: true,
|
||||
},
|
||||
tasksDashboardOrder: ['today', 'tomorrow', 'next7days', 'futureNoDue', 'upcomingoverdue', 'taskstatusoverview', 'taskcompletionheatmap'],
|
||||
|
||||
tasksDashboardOrder: ['Today\'s goals', 'Weekly points score', 'today', 'tomorrow', 'next7days', 'futureNoDue', 'upcomingoverdue', 'taskstatusoverview', 'taskcompletionheatmap'],
|
||||
tasksDashboardWidgets: {
|
||||
today: true,
|
||||
tomorrow: true,
|
||||
|
|
@ -269,9 +274,38 @@ export const DEFAULT_SETTINGS = {
|
|||
isHeatmapGuideCollapsed: true,
|
||||
heatmapLinkConfig: {},
|
||||
heatmapTotalClickOpenAction: 'new-tab',
|
||||
customHeatmaps: [],
|
||||
goals: [
|
||||
{
|
||||
id: 'core-daily-note',
|
||||
name: 'Daily note created',
|
||||
type: 'note-created',
|
||||
target: 1,
|
||||
points: 60,
|
||||
core: true,
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'core-words-written',
|
||||
name: 'Words written',
|
||||
type: 'word-count',
|
||||
target: 200,
|
||||
points: 24,
|
||||
core: true,
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'core-tasks-completed',
|
||||
name: 'Tasks completed',
|
||||
type: 'task-count',
|
||||
target: 1,
|
||||
points: 30,
|
||||
core: true,
|
||||
enabled: true
|
||||
}
|
||||
],
|
||||
goalScoreHistory: {},
|
||||
vacationHistory: {},
|
||||
|
||||
// In your DEFAULT_SETTINGS
|
||||
tasksStatsWidgets: [
|
||||
{
|
||||
"widgetKey": "custom-completed-line-30d",
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ export const BUNDLED_TEMPLATES = [
|
|||
line,
|
||||
pie,
|
||||
radar,
|
||||
// Add other imported templates to this array
|
||||
];
|
||||
|
||||
|
||||
|
|
|
|||
393
src/logic/GoalTracker.js
Normal file
393
src/logic/GoalTracker.js
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
import { moment, TFile } from 'obsidian';
|
||||
|
||||
export class GoalTracker {
|
||||
constructor(app, settings) {
|
||||
this.app = app;
|
||||
this.settings = settings;
|
||||
|
||||
if (!this.settings.goalScoreHistory) {
|
||||
this.settings.goalScoreHistory = {};
|
||||
}
|
||||
if (!this.settings.vacationHistory) {
|
||||
this.settings.vacationHistory = {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a single goal and returns its status, including current progress.
|
||||
* @param {object} goal - The goal configuration object.
|
||||
* @param {moment.Moment} date - The date to check against.
|
||||
* @param {Array} allTasks - The complete list of tasks.
|
||||
* @returns {Promise<{isMet: boolean, current: number}>}
|
||||
*/
|
||||
async checkGoal(goal, date, allTasks) {
|
||||
let result = { isMet: false, current: 0 };
|
||||
|
||||
switch (goal.type) {
|
||||
case 'note-created':
|
||||
result = await this.checkNoteCreated(date, goal.folder);
|
||||
break;
|
||||
case 'word-count':
|
||||
result = await this.checkWordCount(date, goal.folder, goal.target);
|
||||
break;
|
||||
case 'task-count':
|
||||
result = this.checkTaskCount(date, allTasks, goal.query, goal.target);
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the total score for a day with bonuses, active-day rules,
|
||||
* vacation mode, and persistent caching.
|
||||
* @param {moment.Moment} date - The date to score.
|
||||
* @param {Array} allTasks - The complete list of tasks.
|
||||
* @returns {Promise<{date: string, totalPoints: number, results: Array<object>, allMet: boolean, level: number, levelTitle: string, levelIcon: string}>}
|
||||
*/
|
||||
async calculateDailyScore(date, allTasks = []) {
|
||||
|
||||
if (this.settings.vacationModeGlobal) {
|
||||
// Vacation applies to all goals, skip scoring
|
||||
return {
|
||||
date: date.format('YYYY-MM-DD'),
|
||||
totalPoints: 0,
|
||||
results: [],
|
||||
allMet: false,
|
||||
level: 0,
|
||||
levelTitle: '',
|
||||
levelIcon: ''
|
||||
};
|
||||
}
|
||||
|
||||
const isoKey = date.format('YYYY-MM-DD');
|
||||
|
||||
const todayStart = moment().startOf('day');
|
||||
const dateStart = date.clone().startOf('day');
|
||||
const canUseCache = dateStart.isBefore(todayStart, 'day');
|
||||
|
||||
// 0. Return cached snapshot if available
|
||||
if (canUseCache &&
|
||||
this.settings.goalScoreHistory &&
|
||||
this.settings.goalScoreHistory[isoKey]) {
|
||||
return this.settings.goalScoreHistory[isoKey];
|
||||
}
|
||||
|
||||
// Ensure containers exist
|
||||
if (!this.settings.goalScoreHistory) this.settings.goalScoreHistory = {};
|
||||
if (!this.settings.vacationHistory) this.settings.vacationHistory = {};
|
||||
|
||||
const vacationForDay = this.settings.vacationHistory[isoKey] || {};
|
||||
|
||||
let totalPoints = 0;
|
||||
const results = [];
|
||||
|
||||
const goals = this.settings.goals || [];
|
||||
const weekdayIndex = date.isoWeekday() - 1; // 0=Mon .. 6=Sun
|
||||
|
||||
let activeGoalsCount = 0;
|
||||
let metGoalsCount = 0;
|
||||
|
||||
const activeGoals = (this.settings.goals || []).filter(g => !!g.enabled);
|
||||
|
||||
for (const goal of activeGoals) {
|
||||
const activeDays = Array.isArray(goal.activeDays) && goal.activeDays.length > 0
|
||||
? goal.activeDays
|
||||
: [0, 1, 2, 3, 4, 5, 6];
|
||||
|
||||
const isActiveToday = activeDays.includes(weekdayIndex);
|
||||
|
||||
// Historical vacation: if this goal was ever on vacation for this date,
|
||||
// keep that regardless of the current toggle state.
|
||||
const isVacationToday =
|
||||
!!vacationForDay[goal.id] || !!goal.vacationMode === true;
|
||||
|
||||
// If the goal is in vacationMode *now* and the day is being scored,
|
||||
// persist that into vacationHistory so future recalcs remember it.
|
||||
if (isActiveToday && goal.vacationMode) {
|
||||
if (!this.settings.vacationHistory[isoKey]) {
|
||||
this.settings.vacationHistory[isoKey] = {};
|
||||
}
|
||||
this.settings.vacationHistory[isoKey][goal.id] = true;
|
||||
}
|
||||
|
||||
let result = { isMet: false, current: 0 };
|
||||
if (isActiveToday && !isVacationToday) {
|
||||
result = await this.checkGoal(goal, date, allTasks);
|
||||
activeGoalsCount++;
|
||||
} else if (isActiveToday && isVacationToday) {
|
||||
// still "active" for display purposes but we won't penalise
|
||||
result = await this.checkGoal(goal, date, allTasks);
|
||||
activeGoalsCount++;
|
||||
}
|
||||
|
||||
let goalPoints = 0;
|
||||
|
||||
if (isActiveToday && result.isMet && !isVacationToday) {
|
||||
metGoalsCount++;
|
||||
|
||||
const baseValue = parseInt(goal.points || 0);
|
||||
goalPoints = baseValue;
|
||||
|
||||
if (goal.type === 'task-count') {
|
||||
const extra = Math.max(0, result.current - goal.target);
|
||||
if (extra > 0) {
|
||||
goalPoints += extra * 2;
|
||||
}
|
||||
}
|
||||
} else if (isActiveToday && !result.isMet && !isVacationToday) {
|
||||
// PENALTY for missing a goal, unless vacation is recorded
|
||||
goalPoints = -10;
|
||||
} else {
|
||||
// Not active today or vacation → neutral
|
||||
goalPoints = 0;
|
||||
}
|
||||
|
||||
totalPoints += goalPoints;
|
||||
results.push({
|
||||
id: goal.id,
|
||||
name: goal.name,
|
||||
isMet: isActiveToday ? result.isMet : false,
|
||||
isActiveToday,
|
||||
isVacationToday,
|
||||
points: goalPoints,
|
||||
target: goal.target,
|
||||
current: result.current,
|
||||
type: goal.type
|
||||
});
|
||||
}
|
||||
|
||||
// 2. "All Goals Met" Daily Bonus (only active goals count)
|
||||
const allGoalsMet = activeGoalsCount > 0 && metGoalsCount === activeGoalsCount;
|
||||
if (allGoalsMet) {
|
||||
totalPoints += 100;
|
||||
}
|
||||
|
||||
// 3. Small Goal Count Multiplier (1–2 active goals)
|
||||
if (activeGoalsCount > 0 && activeGoalsCount < 3) {
|
||||
totalPoints = Math.floor(totalPoints * 1.5);
|
||||
}
|
||||
|
||||
// 4. Streak Bonus (Check Yesterday Only, using active goals)
|
||||
if (allGoalsMet) {
|
||||
const yesterday = date.clone().subtract(1, 'days');
|
||||
const yesterdayResults = await this.calculateDailyScoreSimple(yesterday, allTasks);
|
||||
if (yesterdayResults.allMet) {
|
||||
totalPoints += 50;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Get Achievement Level for this day's score
|
||||
const achievement = this.getAchievementLevel(totalPoints);
|
||||
|
||||
const resultObj = {
|
||||
date: isoKey,
|
||||
totalPoints,
|
||||
results,
|
||||
allMet: allGoalsMet,
|
||||
level: achievement.level,
|
||||
levelTitle: achievement.title,
|
||||
levelIcon: achievement.icon
|
||||
};
|
||||
|
||||
// 6. Persist snapshot in cache for past days
|
||||
if (canUseCache) {
|
||||
if (!this.settings.goalScoreHistory) {
|
||||
this.settings.goalScoreHistory = {};
|
||||
}
|
||||
this.settings.goalScoreHistory[isoKey] = {
|
||||
...resultObj,
|
||||
cachedAt: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
return resultObj;
|
||||
}
|
||||
|
||||
|
||||
// Simplified calculation for "Yesterday" to avoid infinite loops/deep recursion
|
||||
async calculateDailyScoreSimple(date, allTasks) {
|
||||
const activeGoals = (this.settings.goals || []).filter(g => !!g.enabled);
|
||||
|
||||
if (activeGoals.length === 0) return { allMet: false };
|
||||
|
||||
let met = 0;
|
||||
for (const goal of activeGoals) {
|
||||
const res = await this.checkGoal(goal, date, allTasks);
|
||||
if (res.isMet) met++;
|
||||
}
|
||||
return { allMet: met === activeGoals.length };
|
||||
}
|
||||
|
||||
// --- HELPER TO GET DAILY NOTE PATH ---
|
||||
getDailyNotePath(date, overrideFolder) {
|
||||
let folder = overrideFolder || '';
|
||||
let format = 'YYYY-MM-DD';
|
||||
|
||||
// 1. Try Periodic Notes (Community Plugin)
|
||||
// Attempt to detect Periodic Notes plugin (community plugin)
|
||||
const periodicNotes = this.app.plugins.plugins['periodic-notes'];
|
||||
if (periodicNotes && periodicNotes.settings?.daily?.enabled) {
|
||||
const daily = periodicNotes.settings.daily;
|
||||
if (!folder) folder = daily.folder || '';
|
||||
format = daily.format || 'YYYY-MM-DD';
|
||||
}
|
||||
else {
|
||||
// 2. Try Core Daily Notes Plugin
|
||||
const dailyNotes = this.app.internalPlugins.getPluginById('daily-notes');
|
||||
if (dailyNotes && dailyNotes.instance && dailyNotes.instance.options) {
|
||||
const options = dailyNotes.instance.options;
|
||||
if (!folder) folder = options.folder || '';
|
||||
format = options.format || 'YYYY-MM-DD';
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fallback
|
||||
if (!format) format = 'YYYY-MM-DD';
|
||||
|
||||
// 4. Construct Path
|
||||
const filename = date.format(format);
|
||||
const normalizedFolder = folder ? folder.trim().replace(/^\/|\/$/g, '') : '';
|
||||
const fullPath = normalizedFolder ? `${normalizedFolder}/${filename}.md` : `${filename}.md`;
|
||||
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a daily note exists.
|
||||
*/
|
||||
async checkNoteCreated(date, folderPath) {
|
||||
const path = this.getDailyNotePath(date, folderPath);
|
||||
const file = this.app.vault.getAbstractFileByPath(path);
|
||||
const exists = file instanceof TFile;
|
||||
return { isMet: exists, current: exists ? 1 : 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks word count in a daily note.
|
||||
*/
|
||||
async checkWordCount(date, folderPath, target) {
|
||||
const path = this.getDailyNotePath(date, folderPath);
|
||||
const file = this.app.vault.getAbstractFileByPath(path);
|
||||
|
||||
if (!file || !(file instanceof TFile)) {
|
||||
return { isMet: false, current: 0 };
|
||||
}
|
||||
|
||||
const content = await this.app.vault.read(file);
|
||||
const contentBody = content.replace(/^---[\s\S]+?---/, ''); // Strip frontmatter
|
||||
const words = contentBody.trim().split(/\s+/).filter(w => w.length > 0).length;
|
||||
|
||||
return { isMet: words >= target, current: words };
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks completed task count.
|
||||
*/
|
||||
checkTaskCount(date, allTasks, tagQuery, target) {
|
||||
if (!allTasks) return { isMet: false, current: 0 };
|
||||
|
||||
const targetDateStart = date.clone().startOf('day');
|
||||
const targetDateEnd = date.clone().endOf('day');
|
||||
|
||||
const completed = allTasks.filter(t => {
|
||||
const isDone = t.status === 'x' || t.status?.type === 'DONE';
|
||||
if (!isDone) return false;
|
||||
|
||||
const completionDate = t.done?.moment || (t.completionDate ? moment(t.completionDate) : null);
|
||||
if (!completionDate) return false;
|
||||
|
||||
const isSameDay = completionDate.isBetween(targetDateStart, targetDateEnd, null, '[]');
|
||||
|
||||
const matchesTag = tagQuery
|
||||
? (t.tags && t.tags.some(tag => tag.includes(tagQuery.replace('#', ''))))
|
||||
: true;
|
||||
|
||||
return isSameDay && matchesTag;
|
||||
});
|
||||
|
||||
return { isMet: completed.length >= target, current: completed.length };
|
||||
}
|
||||
|
||||
getAchievementLevel(points) {
|
||||
const LEVEL_BANDS = [
|
||||
{ frac: 1.00, title: "Grandmaster of Flow", icon: "infinity" },
|
||||
{ frac: 0.90, title: "Legendary Leader", icon: "crown" },
|
||||
{ frac: 0.80, title: "Relentless Result", icon: "trophy" },
|
||||
{ frac: 0.70, title: "Task Titan", icon: "medal" },
|
||||
{ frac: 0.60, title: "Output Overlord", icon: "award" },
|
||||
{ frac: 0.50, title: "High Performer", icon: "star" },
|
||||
{ frac: 0.40, title: "Productivity Pro", icon: "zap" },
|
||||
{ frac: 0.30, title: "Goal Getter", icon: "target" },
|
||||
{ frac: 0.22, title: "Focused Ninja", icon: "crosshair" },
|
||||
{ frac: 0.16, title: "Efficiency Enthusiast", icon: "gauge" },
|
||||
{ frac: 0.10, title: "Daily Driver", icon: "calendar-check" },
|
||||
{ frac: 0.06, title: "Momentum Maker", icon: "trending-up" },
|
||||
{ frac: 0.03, title: "Task Tinkerer", icon: "check-circle-2" },
|
||||
{ frac: 0.01, title: "Routine Rookie", icon: "clipboard-list" },
|
||||
{ frac: 0.00, title: "Starter Spark", icon: "sprout" }
|
||||
];
|
||||
|
||||
//const target = this.getLifetimeTargetPoints();
|
||||
const target = this.getLifetimeTargetPoints() || 300000;
|
||||
if (!target) {
|
||||
// Fallback: no goals yet
|
||||
return { min: 0, title: "Starter Spark", level: 1, icon: "sprout" };
|
||||
}
|
||||
|
||||
const levels = LEVEL_BANDS
|
||||
.map((band, idx) => ({
|
||||
min: Math.round(band.frac * target),
|
||||
title: band.title,
|
||||
level: LEVEL_BANDS.length - idx,
|
||||
icon: band.icon
|
||||
}))
|
||||
// IMPORTANT: sort from highest min to lowest
|
||||
.sort((a, b) => b.min - a.min);
|
||||
|
||||
// Find the highest band whose min you meet
|
||||
const match = levels.find(l => points >= l.min);
|
||||
return match || levels[levels.length - 1];
|
||||
|
||||
}
|
||||
|
||||
static async getLifetimePoints(app, settings, allTasks) {
|
||||
const history = settings.goalScoreHistory || {};
|
||||
let total = 0;
|
||||
for (const dateKey in history) {
|
||||
total += history[dateKey].totalPoints || 0;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
getLifetimeTargetPoints() {
|
||||
const idealDaily = this.getIdealDailyMax();
|
||||
if (!idealDaily) return 0;
|
||||
|
||||
// Assume a typical user hits ~80% of ideal on average
|
||||
const typicalDaily = idealDaily * 0.8;
|
||||
|
||||
const daysForMaxLevel = 3 * 365; // 3 years
|
||||
return Math.round(typicalDaily * daysForMaxLevel);
|
||||
}
|
||||
|
||||
getIdealDailyMax() {
|
||||
const goals = this.settings.goals || [];
|
||||
if (goals.length === 0) return 0;
|
||||
|
||||
// Sum configured points for all goals
|
||||
let base = goals.reduce((acc, g) => acc + (parseInt(g.points || 0) || 0), 0);
|
||||
|
||||
// Assume "all goals met" bonus
|
||||
base += 100;
|
||||
|
||||
// If user has only 1–2 goals, they still get the small multiplier
|
||||
if (goals.length < 3) {
|
||||
base = Math.floor(base * 1.5);
|
||||
}
|
||||
|
||||
// Assume they're in a streak on a good day
|
||||
base += 50;
|
||||
|
||||
return base; // this is "max-ish" points for a really good day
|
||||
}
|
||||
}
|
||||
61
src/main.js
61
src/main.js
|
|
@ -1,7 +1,4 @@
|
|||
/* --cpwn-- Obsidian Plugin: Periodic Notes Calendar View --cpwn--
|
||||
Developer: Tim Williams
|
||||
This plugin uses library: https://app.unpkg.com/ical.js@2.2.1/files/dist/ical.es5.min.cjs (c) 2024 Chris Lawley, MIT License
|
||||
*/
|
||||
/* -- Obsidian Plugin: Calendar Periodic Week Notes -- */
|
||||
|
||||
import { Plugin, debounce } from 'obsidian';
|
||||
import { PeriodMonthView, VIEW_TYPE_PERIOD } from './views/PeriodMonthView.js';
|
||||
|
|
@ -21,6 +18,50 @@ export default class PeriodMonthPlugin extends Plugin {
|
|||
async onload() {
|
||||
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
|
||||
const CORE_GOALS = [
|
||||
{
|
||||
id: 'core-daily-note',
|
||||
name: 'Daily note created',
|
||||
type: 'note-created',
|
||||
target: 1,
|
||||
points: 60,
|
||||
core: true,
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'core-words-written',
|
||||
name: 'Words written',
|
||||
type: 'word-count',
|
||||
target: 200,
|
||||
points: 24,
|
||||
core: true,
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'core-tasks-completed',
|
||||
name: 'Tasks completed',
|
||||
type: 'task-count',
|
||||
target: 2,
|
||||
points: 60,
|
||||
core: true,
|
||||
enabled: true
|
||||
}
|
||||
];
|
||||
|
||||
// Make sure the goals property exists
|
||||
if (!this.settings.goals || this.settings.goals.length === 0) {
|
||||
this.settings.goals = [...CORE_GOALS];
|
||||
}
|
||||
|
||||
// Always check and inject missing core goals before showing UI
|
||||
CORE_GOALS.forEach(coreGoal => {
|
||||
const found = this.settings.goals.some(g => g.id === coreGoal.id && g.core);
|
||||
if (!found) {
|
||||
this.settings.goals.unshift({ ...coreGoal });
|
||||
}
|
||||
});
|
||||
|
||||
this.updateStyles();
|
||||
this.registerView(VIEW_TYPE_PERIOD, leaf => new PeriodMonthView(leaf, this));
|
||||
this.addRibbonIcon("calendar-check", "Open Calendar Period Week Notes", () => this.activateView());
|
||||
|
|
@ -57,16 +98,6 @@ export default class PeriodMonthPlugin extends Plugin {
|
|||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the plugin's CSS into the document head.
|
||||
*/
|
||||
addStyle() {
|
||||
const styleEl = document.createElement('style');
|
||||
styleEl.id = 'cpwn-period-month-plugin-styles';
|
||||
styleEl.textContent = PLUGIN_STYLES;
|
||||
document.head.appendChild(styleEl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates CSS custom properties (variables) based on the current plugin settings.
|
||||
*/
|
||||
|
|
@ -192,7 +223,7 @@ export default class PeriodMonthPlugin extends Plugin {
|
|||
}
|
||||
|
||||
async loadTemplates() {
|
||||
|
||||
|
||||
return Promise.resolve(BUNDLED_TEMPLATES);
|
||||
|
||||
}
|
||||
|
|
|
|||
252
src/modals/GoalEditModal.js
Normal file
252
src/modals/GoalEditModal.js
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
import { Modal, Setting, Notice, AbstractInputSuggest, TFolder } from 'obsidian';
|
||||
|
||||
// --- 1. Define FolderSuggest Helper Class ---
|
||||
class FolderSuggest extends AbstractInputSuggest {
|
||||
constructor(app, textInputEl) {
|
||||
super(app, textInputEl);
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
getSuggestions(inputStr) {
|
||||
const abstractFiles = this.app.vault.getAllLoadedFiles();
|
||||
const folders = [];
|
||||
const lowerCaseInputStr = inputStr.toLowerCase();
|
||||
|
||||
for (const file of abstractFiles) {
|
||||
if (file instanceof TFolder) {
|
||||
if (file.path.toLowerCase().includes(lowerCaseInputStr)) {
|
||||
folders.push(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
return folders;
|
||||
}
|
||||
|
||||
renderSuggestion(file, el) {
|
||||
el.setText(file.path);
|
||||
}
|
||||
|
||||
selectSuggestion(file) {
|
||||
this.inputEl.value = file.path;
|
||||
this.inputEl.trigger("input");
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
// --- 2. Your GoalEditModal Class ---
|
||||
export class GoalEditModal extends Modal {
|
||||
constructor(app, existingGoal, onSave) {
|
||||
super(app);
|
||||
this.goal = existingGoal || {
|
||||
id: crypto.randomUUID(),
|
||||
name: '',
|
||||
type: 'task-count',
|
||||
target: 1,
|
||||
points: 10,
|
||||
query: '',
|
||||
folder: '',
|
||||
activeDays: [0, 1, 2, 3, 4, 5, 6],
|
||||
vacationMode: false
|
||||
};
|
||||
|
||||
this.tempGoal = { ...this.goal };
|
||||
|
||||
// Ensure new fields exist on older goals
|
||||
if (!Array.isArray(this.tempGoal.activeDays) || this.tempGoal.activeDays.length === 0) {
|
||||
this.tempGoal.activeDays = [0, 1, 2, 3, 4, 5, 6];
|
||||
}
|
||||
if (typeof this.tempGoal.vacationMode !== 'boolean') {
|
||||
this.tempGoal.vacationMode = false;
|
||||
}
|
||||
|
||||
this.onSave = onSave;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.createEl('h2', { text: this.goal.name ? 'Edit goal' : 'Create new goal' });
|
||||
|
||||
new Setting(contentEl)
|
||||
.setDesc('If you change a folder location, #tags or goal values to achieve it\'s recommended to perform a \'Reset cache\' to recalculate your progress.')
|
||||
|
||||
// 1. Goal Name
|
||||
new Setting(contentEl)
|
||||
.setName('Goal name')
|
||||
.setDesc('e.g., "Morning writing" or "Daily tasks"')
|
||||
.addText(text => text
|
||||
.setValue(this.tempGoal.name)
|
||||
.onChange((value) => {
|
||||
this.tempGoal.name = value;
|
||||
}));
|
||||
|
||||
// 2. Goal Type Dropdown
|
||||
new Setting(contentEl)
|
||||
.setName('Goal type')
|
||||
.addDropdown(drop => drop
|
||||
.addOption('task-count', 'Daily tasks')
|
||||
.addOption('note-created', 'Daily note created')
|
||||
.addOption('word-count', 'Daily word count')
|
||||
.setValue(this.tempGoal.type)
|
||||
.onChange(value => {
|
||||
this.tempGoal.type = value;
|
||||
|
||||
// Set sensible defaults
|
||||
if (value === 'note-created') {
|
||||
this.tempGoal.target = 1;
|
||||
} else if (value === 'word-count' && this.tempGoal.target < 100) {
|
||||
this.tempGoal.target = 500;
|
||||
} else if (value === 'task-count' && this.tempGoal.target > 50) {
|
||||
this.tempGoal.target = 3;
|
||||
}
|
||||
|
||||
this.updatePointsPreview();
|
||||
this.display();
|
||||
}));
|
||||
|
||||
// 3. Conditional Input: Tag Query
|
||||
if (this.tempGoal.type === 'task-count') {
|
||||
new Setting(contentEl)
|
||||
.setName('Filter by tag (optional)')
|
||||
.setDesc('Only count tasks with this tag (e.g., #gym). Leave blank for all tasks.')
|
||||
.addText(text => text
|
||||
.setPlaceholder('#tag')
|
||||
.setValue(this.tempGoal.query)
|
||||
.onChange(value => this.tempGoal.query = value));
|
||||
}
|
||||
|
||||
// 4. Conditional Input: Folder Picker WITH DROPDOWN
|
||||
if (this.tempGoal.type === 'word-count' || this.tempGoal.type === 'note-created') {
|
||||
new Setting(contentEl)
|
||||
.setName("Folder location (optional)")
|
||||
.setDesc("Leave blank to use your default Daily Notes folder, or specify a folder.")
|
||||
.addText(text => {
|
||||
text.setPlaceholder("Example: Journals")
|
||||
.setValue(this.tempGoal.folder || "")
|
||||
.onChange(value => {
|
||||
this.tempGoal.folder = value;
|
||||
});
|
||||
|
||||
// --- ATTACH FOLDER SUGGESTER HERE ---
|
||||
new FolderSuggest(this.app, text.inputEl);
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Conditional Input: Target Value
|
||||
if (this.tempGoal.type !== 'note-created') {
|
||||
new Setting(contentEl)
|
||||
.setName("Target value")
|
||||
.setDesc(this.tempGoal.type === 'word-count' ? "How many words?" : "How many tasks?")
|
||||
.addText(text => text
|
||||
.setValue(String(this.tempGoal.target))
|
||||
.onChange((value) => {
|
||||
this.tempGoal.target = Number(value) || 0;
|
||||
this.updatePointsPreview();
|
||||
}));
|
||||
}
|
||||
|
||||
// 5.5 Active days picker
|
||||
const daysSetting = new Setting(contentEl)
|
||||
.setName("Active days")
|
||||
.setDesc("Points and penalties only apply on these days.");
|
||||
|
||||
const daysContainer = daysSetting.controlEl.createDiv({
|
||||
cls: 'cpwn-goal-days-picker'
|
||||
});
|
||||
|
||||
const dayLabels = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
|
||||
|
||||
dayLabels.forEach((label, idx) => {
|
||||
const dayEl = daysContainer.createSpan({
|
||||
text: label,
|
||||
cls: 'cpwn-goal-day'
|
||||
});
|
||||
|
||||
if (this.tempGoal.activeDays.includes(idx)) {
|
||||
dayEl.addClass('is-active');
|
||||
}
|
||||
|
||||
dayEl.addEventListener('click', () => {
|
||||
const active = this.tempGoal.activeDays;
|
||||
const pos = active.indexOf(idx);
|
||||
|
||||
if (pos === -1) {
|
||||
active.push(idx);
|
||||
dayEl.addClass('is-active');
|
||||
} else {
|
||||
if (active.length > 1) {
|
||||
active.splice(pos, 1);
|
||||
dayEl.removeClass('is-active');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 5.6 Vacation mode toggle
|
||||
new Setting(contentEl)
|
||||
.setName("Vacation mode")
|
||||
.setDesc("When enabled, missed days for this goal give no penalties.")
|
||||
.addToggle(toggle => {
|
||||
toggle
|
||||
.setValue(!!this.tempGoal.vacationMode)
|
||||
.onChange((value) => {
|
||||
this.tempGoal.vacationMode = value;
|
||||
});
|
||||
});
|
||||
|
||||
// 6. Points Display
|
||||
const pointsSetting = new Setting(contentEl)
|
||||
.setName("Points reward details")
|
||||
.setDesc("Points are calculated automatically based on your target.");
|
||||
|
||||
this.pointsInfoEl = pointsSetting.controlEl.createDiv({ cls: 'cpwn-points-info' });
|
||||
this.pointsInfoEl.style.textAlign = 'right';
|
||||
this.pointsInfoEl.style.fontSize = '0.9em';
|
||||
this.pointsInfoEl.style.color = 'var(--text-accent)';
|
||||
|
||||
this.updatePointsPreview();
|
||||
|
||||
// 7. Save Button
|
||||
new Setting(contentEl)
|
||||
.addButton(btn => btn
|
||||
.setButtonText("Save goal")
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
if (!this.tempGoal.name) {
|
||||
new Notice('Please enter a goal name.');
|
||||
return;
|
||||
}
|
||||
this.onSave(this.tempGoal);
|
||||
this.close();
|
||||
}));
|
||||
}
|
||||
|
||||
updatePointsPreview() {
|
||||
let points = 0;
|
||||
let explanation = "";
|
||||
|
||||
if (this.tempGoal.type === 'note-created') {
|
||||
points = 60;
|
||||
explanation = "Reward: 60 pts\n(Penalty: -10 pts if missed)";
|
||||
} else if (this.tempGoal.type === 'task-count') {
|
||||
const count = this.tempGoal.target || 0;
|
||||
points = count * 30;
|
||||
explanation = `${count} tasks × 30 pts = ${points} pts\n(+2 pts per extra task)\n(Penalty: -10 pts if target not met)`;
|
||||
} else if (this.tempGoal.type === 'word-count') {
|
||||
const count = this.tempGoal.target || 0;
|
||||
points = Math.floor((count / 250) * 30);
|
||||
explanation = `${count} words ≈ ${points} pts\n(30 pts per 250 words)\n(Penalty: -10 pts if target not met)`;
|
||||
}
|
||||
|
||||
if (this.pointsInfoEl) {
|
||||
this.pointsInfoEl.innerText = explanation;
|
||||
this.pointsInfoEl.style.whiteSpace = 'pre-line';
|
||||
}
|
||||
|
||||
this.tempGoal.points = points;
|
||||
}
|
||||
|
||||
display() {
|
||||
this.onOpen();
|
||||
}
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@ export class SectionHeaderModal extends Modal {
|
|||
: defaultConfig;
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.createEl('h2', { text: 'Create section header' });
|
||||
|
|
@ -44,7 +44,7 @@ export class SectionHeaderModal extends Modal {
|
|||
|
||||
new Setting(contentEl)
|
||||
.setName('Visible')
|
||||
.setDesc('Toggle whether this section header is visible on the dashboard. Having the section header enabled but hidden allows you to use it for grouping widgets without affecting if a section header that\s above is being toggled to show and hide over time, i.e. this section of grouped widgets will always be visible.')
|
||||
.setDesc('Toggle whether this section header is visible on the dashboard. Having the section header enabled but hidden allows you to use it for grouping widgets without affecting if a section header that\'s above is being toggled to show and hide over time, i.e. this section of grouped widgets will always be visible.')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.config.isVisible ?? true) // Default to true if not set
|
||||
.onChange(value => {
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ export class TemplatePickerModal extends Modal {
|
|||
ctx.clearRect(0, 0, width, height); // Clear canvas before drawing
|
||||
|
||||
ctx.font = "48px sans-serif"; // Font size and family
|
||||
ctx.fillStyle = template.previewData.datasets[0].color || "#4caf50";; // Text color (KPI color if you wish)
|
||||
ctx.fillStyle = template.previewData.datasets[0].color || "#4caf50";
|
||||
ctx.textAlign = "center"; // Center horizontally
|
||||
ctx.textBaseline = "middle"; // Center vertically
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export class WidgetBuilderModal extends Modal {
|
|||
}
|
||||
|
||||
// Clear and add the correct options
|
||||
legendPositionDropdown.selectEl.innerHTML = '';
|
||||
llegendPositionDropdown.selectEl.empty();
|
||||
legendPositionDropdown.addOptions(newOptions);
|
||||
|
||||
// Reset position if the old one is invalid
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import { PeriodMonthView } from '../views/PeriodMonthView.js';
|
|||
import { WidgetBuilderModal } from '../modals/WidgetBuilderModal.js';
|
||||
import { SectionHeaderModal } from '../modals/SectionHeaderModal.js';
|
||||
import { TemplatePickerModal } from '../modals/TemplatePickerModal.js';
|
||||
import { GoalEditModal } from '../modals/GoalEditModal.js';
|
||||
import { GoalTracker } from '../logic/GoalTracker';
|
||||
|
||||
/**
|
||||
* The settings tab class for the plugin, responsible for building the settings UI.
|
||||
|
|
@ -1103,6 +1105,7 @@ export class PeriodSettingsTab extends PluginSettingTab {
|
|||
assets: "Assets tab",
|
||||
tasks: "Tasks tab",
|
||||
dashboard: 'Dashboard tab',
|
||||
goals: 'Goal configuration',
|
||||
themes: "Themes",
|
||||
importExport: "Import / Export",
|
||||
startHere: "Help",
|
||||
|
|
@ -1252,6 +1255,9 @@ export class PeriodSettingsTab extends PluginSettingTab {
|
|||
case 'dashboard':
|
||||
this.renderDashboardSettings();
|
||||
break;
|
||||
case 'goals':
|
||||
this.renderGoalsSettings();
|
||||
break;
|
||||
case 'themes':
|
||||
this.renderThemesSettings();
|
||||
break;
|
||||
|
|
@ -2404,6 +2410,296 @@ export class PeriodSettingsTab extends PluginSettingTab {
|
|||
tasksBtn.click();
|
||||
}
|
||||
|
||||
renderGoalsSettings() {
|
||||
const containerEl = this.contentEl;
|
||||
containerEl.empty();
|
||||
containerEl.createEl("h1", { text: "Goal configuration" });
|
||||
containerEl.createEl("p", { text: "Define goals to track your progress and earn points. Drag to reorder." });
|
||||
|
||||
const goalsList = containerEl.createDiv({ cls: 'cpwn-goals-list' });
|
||||
this.enableGoalSorting(goalsList);
|
||||
|
||||
const allGoals = this.plugin.settings.goals || [];
|
||||
const coreGoals = allGoals.filter(g => g.core);
|
||||
const customGoals = allGoals.filter(g => !g.core);
|
||||
|
||||
// --- Render core goals first ---
|
||||
coreGoals.forEach((goal, index) =>
|
||||
this.renderGoalItem(goalsList, goal, allGoals.indexOf(goal), true)
|
||||
);
|
||||
|
||||
// --- Render Custom Goals ---
|
||||
customGoals.forEach((goal, index) => {
|
||||
this.renderGoalItem(goalsList, goal, allGoals.indexOf(goal), false);
|
||||
});
|
||||
|
||||
// --- Add New Goal Button ---
|
||||
new Setting(containerEl)
|
||||
.addButton(btn => {
|
||||
btn.setButtonText("Add new goal")
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
new GoalEditModal(this.app, null, async (newGoal) => {
|
||||
if (!this.plugin.settings.goals) this.plugin.settings.goals = [];
|
||||
this.plugin.settings.goals.push(newGoal);
|
||||
await this.saveAndUpdate();
|
||||
this.renderGoalsSettings();
|
||||
}).open();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// --- Global Vacation Toggle ---
|
||||
new Setting(containerEl)
|
||||
.setName("Global Vacation Mode")
|
||||
.setDesc("When enabled, all goals are paused — no points or penalties are tracked. Note: you can also pause goals individually. To do this enter the goal settings to toggle on/off.")
|
||||
.addToggle(toggle => {
|
||||
toggle.setValue(!!this.plugin.settings.vacationModeGlobal)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.vacationModeGlobal = value;
|
||||
await this.saveAndUpdate();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
containerEl.createEl('h3', { text: 'Level ladder' });
|
||||
|
||||
const tracker = new GoalTracker(this.app, this.plugin.settings);
|
||||
const lifetimeTargetRaw =
|
||||
tracker.getLifetimeTargetPoints && tracker.getLifetimeTargetPoints();
|
||||
const effectiveTarget =
|
||||
lifetimeTargetRaw && lifetimeTargetRaw > 0 ? lifetimeTargetRaw : 300000;
|
||||
|
||||
// Same bands you use in the widget / GoalTracker
|
||||
const LEVEL_BANDS = [
|
||||
{ frac: 1.0, title: 'Grandmaster of Flow', icon: 'infinity' },
|
||||
{ frac: 0.9, title: 'Legendary Leader', icon: 'crown' },
|
||||
{ frac: 0.8, title: 'Relentless Result', icon: 'trophy' },
|
||||
{ frac: 0.7, title: 'Task Titan', icon: 'medal' },
|
||||
{ frac: 0.6, title: 'Output Overlord', icon: 'award' },
|
||||
{ frac: 0.5, title: 'High Performer', icon: 'star' },
|
||||
{ frac: 0.4, title: 'Productivity Pro', icon: 'zap' },
|
||||
{ frac: 0.3, title: 'Goal Getter', icon: 'target' },
|
||||
{ frac: 0.22, title: 'Focused Ninja', icon: 'crosshair' },
|
||||
{ frac: 0.16, title: 'Efficiency Enthusiast', icon: 'gauge' },
|
||||
{ frac: 0.10, title: 'Daily Driver', icon: 'calendar-check' },
|
||||
{ frac: 0.06, title: 'Momentum Maker', icon: 'trending-up' },
|
||||
{ frac: 0.03, title: 'Task Tinkerer', icon: 'check-circle-2' },
|
||||
{ frac: 0.01, title: 'Routine Rookie', icon: 'clipboard-list' },
|
||||
{ frac: 0.0, title: 'Starter Spark', icon: 'sprout' }
|
||||
];
|
||||
|
||||
const levels = LEVEL_BANDS
|
||||
.map((band, idx) => ({
|
||||
min: Math.round(band.frac * effectiveTarget),
|
||||
title: band.title,
|
||||
icon: band.icon,
|
||||
level: LEVEL_BANDS.length - idx
|
||||
}))
|
||||
// for display, show from lowest -> highest points
|
||||
.sort((a, b) => a.min - b.min);
|
||||
|
||||
const table = containerEl.createEl('table', { cls: 'cpwn-level-table' });
|
||||
|
||||
const headerRow = table.createEl('tr');
|
||||
headerRow.createEl('th', { text: 'Level' });
|
||||
headerRow.createEl('th', { text: 'Name' });
|
||||
headerRow.createEl('th', { text: 'Icon' });
|
||||
headerRow.createEl('th', { text: 'Points needed' });
|
||||
|
||||
for (const lvl of levels) {
|
||||
const row = table.createEl('tr');
|
||||
|
||||
row.createEl('td', { text: String(lvl.level) });
|
||||
row.createEl('td', { text: lvl.title });
|
||||
|
||||
const iconCell = row.createEl('td');
|
||||
const iconDiv = iconCell.createDiv({ cls: 'cpwn-level-icon' });
|
||||
setIcon(iconDiv, lvl.icon);
|
||||
|
||||
row.createEl('td', { text: lvl.min.toLocaleString() });
|
||||
}
|
||||
|
||||
containerEl.createEl('div', { cls: 'cpwn-setting-spacer' });
|
||||
|
||||
// --- Reset Scoring Cache ---
|
||||
new Setting(containerEl)
|
||||
.setName("Reset scoring cache")
|
||||
.setDesc("Clears cached daily scores and recomputes them from your tasks and notes. Vacation history is kept, but past totals may change.")
|
||||
.addButton(btn => btn
|
||||
.setButtonText("Reset cache")
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
if (!confirm("Are you sure you want to reset the scoring cache? This will delete all cached daily scores and rebuild them from your tasks and notes.")) {
|
||||
return;
|
||||
}
|
||||
this.plugin.settings.goalScoreHistory = {};
|
||||
await this.plugin.saveData(this.plugin.settings);
|
||||
new Notice("Goal scoring cache cleared. It will be rebuilt as you view dashboards.", 4000);
|
||||
this.triggerDashboardRefresh();
|
||||
}));
|
||||
|
||||
// --- Reset Vacation History ---
|
||||
new Setting(containerEl)
|
||||
.setName("Reset vacation history")
|
||||
.setDesc("Clears stored vacation days for all goals. After resetting the scoring cache, past days will be recomputed without vacation applied.")
|
||||
.addButton(btn => btn
|
||||
.setButtonText("Reset vacation days")
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
if (!confirm("Are you sure you want to reset all vacation history? All days will be treated as normal working days on the next rebuild.")) {
|
||||
return;
|
||||
}
|
||||
this.plugin.settings.vacationHistory = {};
|
||||
await this.plugin.saveData(this.plugin.settings);
|
||||
new Notice("Vacation history cleared. Reset the scoring cache if you want past days recomputed without vacation.", 5000);
|
||||
}));
|
||||
}
|
||||
|
||||
getGoalDisplayName(goal) {
|
||||
// If the user named it "Tasks completed" (default), rename it to "Core: Tasks"
|
||||
// Or check by type if you want to force it
|
||||
if (goal.type === 'task-count') return "Task count";
|
||||
if (goal.type === 'word-count') return "Words count";
|
||||
if (goal.type === 'note-created') return "Create note";
|
||||
|
||||
// Fallback for custom names
|
||||
return goal.type;
|
||||
}
|
||||
|
||||
renderGoalItem(goalsList, goal, index, isCore) {
|
||||
const goalEl = goalsList.createDiv({ cls: 'cpwn-goal-item setting-item' });
|
||||
goalEl.setAttribute('draggable', isCore ? 'false' : 'true');
|
||||
goalEl.dataset.index = index;
|
||||
|
||||
// Drag handle (disabled for core)
|
||||
const handle = goalEl.createDiv({ cls: 'cpwn-pm-drag-handle' });
|
||||
setIcon(handle, 'grip-vertical');
|
||||
handle.style.marginRight = '10px';
|
||||
handle.style.cursor = isCore ? 'not-allowed' : 'grab';
|
||||
if (isCore) handle.style.opacity = '0.5';
|
||||
|
||||
const info = goalEl.createDiv({ cls: 'setting-item-info' });
|
||||
|
||||
|
||||
|
||||
info.createEl('div', { text: goal.name, cls: 'setting-item-name' });
|
||||
info.createEl('div', {
|
||||
text: `${this.getGoalDisplayName(goal)} | Target: ${goal.target} | ${goal.points} pts`,
|
||||
cls: 'setting-item-description'
|
||||
});
|
||||
|
||||
const controls = goalEl.createDiv({ cls: 'setting-item-control' });
|
||||
|
||||
// EDIT (always)
|
||||
new Setting(controls)
|
||||
.addButton(btn => btn
|
||||
.setIcon('pencil')
|
||||
.onClick(() => {
|
||||
new GoalEditModal(this.app, goal, async (updatedGoal) => {
|
||||
this.plugin.settings.goals[index] = updatedGoal;
|
||||
await this.saveAndUpdate();
|
||||
this.renderGoalsSettings();
|
||||
}).open();
|
||||
}));
|
||||
|
||||
// TOGGLE (always)
|
||||
new Setting(controls)
|
||||
.addToggle(toggle => {
|
||||
toggle.setValue(!!goal.enabled)
|
||||
.onChange(async value => {
|
||||
goal.enabled = value;
|
||||
await this.saveAndUpdate();
|
||||
this.renderGoalsSettings();
|
||||
});
|
||||
});
|
||||
|
||||
// DELETE (only for custom)
|
||||
if (!isCore) {
|
||||
new Setting(controls)
|
||||
.addButton(btn => btn
|
||||
.setIcon('trash')
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
if (confirm(`Delete goal "${goal.name}"?`)) {
|
||||
this.plugin.settings.goals.splice(index, 1);
|
||||
await this.saveAndUpdate();
|
||||
this.renderGoalsSettings();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Helper method to handle Drag & Drop logic
|
||||
enableGoalSorting(container) {
|
||||
let draggedItem = null;
|
||||
|
||||
container.addEventListener('dragstart', (e) => {
|
||||
draggedItem = e.target.closest('.cpwn-goal-item');
|
||||
if (draggedItem) {
|
||||
draggedItem.classList.add('dragging');
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
// Firefox requires data to be set
|
||||
e.dataTransfer.setData('text/plain', draggedItem.dataset.index);
|
||||
}
|
||||
});
|
||||
|
||||
container.addEventListener('dragend', async (e) => {
|
||||
if (draggedItem) {
|
||||
draggedItem.classList.remove('dragging');
|
||||
|
||||
// Update settings based on DOM order
|
||||
const newOrder = [];
|
||||
const children = Array.from(container.children);
|
||||
|
||||
children.forEach(child => {
|
||||
const oldIndex = parseInt(child.dataset.index);
|
||||
if (!isNaN(oldIndex) && this.plugin.settings.goals[oldIndex]) {
|
||||
newOrder.push(this.plugin.settings.goals[oldIndex]);
|
||||
}
|
||||
});
|
||||
|
||||
this.plugin.settings.goals = newOrder;
|
||||
await this.saveAndUpdate();
|
||||
|
||||
// Re-render to update indices
|
||||
this.renderGoalsSettings();
|
||||
}
|
||||
draggedItem = null;
|
||||
});
|
||||
|
||||
container.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
const afterElement = this.getDragAfterElement(container, e.clientY);
|
||||
if (draggedItem) {
|
||||
if (afterElement == null) {
|
||||
container.appendChild(draggedItem);
|
||||
} else {
|
||||
container.insertBefore(draggedItem, afterElement);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Reusing your existing helper, or defining it here if it's private
|
||||
getDragAfterElement(container, y) {
|
||||
const draggableElements = [...container.querySelectorAll('.cpwn-goal-item:not(.dragging)')];
|
||||
|
||||
return draggableElements.reduce((closest, child) => {
|
||||
const box = child.getBoundingClientRect();
|
||||
const offset = y - box.top - box.height / 2;
|
||||
if (offset < 0 && offset > closest.offset) {
|
||||
return { offset: offset, element: child };
|
||||
} else {
|
||||
return closest;
|
||||
}
|
||||
}, { offset: Number.NEGATIVE_INFINITY }).element;
|
||||
}
|
||||
|
||||
|
||||
|
||||
renderGeneralDashboardSettings(container) {
|
||||
container.empty();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// src/utils/dateUtils.js
|
||||
import moment from 'moment';
|
||||
import { moment } from 'obsidian';
|
||||
|
||||
/**
|
||||
* Calculates the custom period and week number based on a given start date.
|
||||
|
|
|
|||
|
|
@ -10,10 +10,13 @@ export class TagSuggest extends AbstractInputSuggest {
|
|||
this.inputEl = inputEl;
|
||||
this.view = view;
|
||||
this.allTags = new Set();
|
||||
|
||||
this.refreshAllTags();
|
||||
|
||||
}
|
||||
|
||||
getSuggestions(query) {
|
||||
this.refreshAllTags();
|
||||
//this.refreshAllTags();
|
||||
|
||||
// If the query is empty, show all tags, sorted alphabetically.
|
||||
if (!query) {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -219,7 +219,7 @@ export class CssChartRenderer {
|
|||
// Add the correct class to the main container. This is what the CSS will use.
|
||||
if (showLegend) {
|
||||
this.container.addClass(`cpwn-legend-${legendPosition}`);
|
||||
console.log(legendPosition);
|
||||
console.log(legendPosition);
|
||||
} else {
|
||||
// This class is for when the legend is off (e.g., mini/small widgets)
|
||||
this.container.addClass('is-pie-only');
|
||||
|
|
@ -900,4 +900,212 @@ export class CssChartRenderer {
|
|||
this.renderLegend(chartContainer, datasets);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a bespoke cumulative area chart for Goal Momentum.
|
||||
* Strictly follows Obsidian UI guidelines (CSS classes, textContent).
|
||||
*/
|
||||
renderWeeklyGoalMomentum(chartData, options = {}) {
|
||||
this.cleanup();
|
||||
const chartContainer = this.container.createDiv({ cls: 'cpwn-pm-css-chart-container' });
|
||||
|
||||
const svgNs = "http://www.w3.org/2000/svg";
|
||||
const svg = document.createElementNS(svgNs, "svg");
|
||||
|
||||
svg.setAttribute('viewBox', '0 0 300 135');
|
||||
svg.setAttribute('preserveAspectRatio', 'none');
|
||||
|
||||
svg.setAttribute('preserveAspectRatio', 'none');
|
||||
svg.setAttribute('width', '100%');
|
||||
svg.setAttribute('height', '100%');
|
||||
|
||||
|
||||
chartContainer.appendChild(svg);
|
||||
|
||||
const dataset = chartData.datasets[0];
|
||||
if (!dataset || dataset.data.length < 2) {
|
||||
chartContainer.createDiv({ text: "Not enough data", cls: "cpwn-muted-text" });
|
||||
return;
|
||||
}
|
||||
|
||||
const rawData = dataset.data;
|
||||
const minVal = Math.min(...rawData);
|
||||
const maxVal = Math.max(...rawData);
|
||||
const range = (maxVal - minVal) === 0 ? 10 : (maxVal - minVal);
|
||||
|
||||
const padding = { top: 15, right: 10, bottom: 10, left: 10 };
|
||||
const width = 300 - padding.left - padding.right;
|
||||
const height = 120 - padding.top - padding.bottom;
|
||||
|
||||
// Map values to coordinates
|
||||
const points = rawData.map((val, i) => {
|
||||
const x = padding.left + (i / (rawData.length - 1)) * width;
|
||||
// Normalize Y: Lowest value sits at bottom (y = height + top)
|
||||
const normalizedVal = val - minVal;
|
||||
const y = padding.top + height - (normalizedVal / range) * height;
|
||||
return { x, y, val };
|
||||
});
|
||||
|
||||
// 1. Grid Lines (Stop at the dot)
|
||||
points.forEach((p) => {
|
||||
const gridLine = document.createElementNS(svgNs, 'line');
|
||||
gridLine.classList.add('cpwn-chart-grid-line');
|
||||
gridLine.setAttribute('x1', p.x);
|
||||
gridLine.setAttribute('y1', p.y); // Start EXACTLY at the dot
|
||||
gridLine.setAttribute('x2', p.x);
|
||||
gridLine.setAttribute('y2', padding.top + height); // Go down to bottom axis
|
||||
gridLine.setAttribute('stroke-opacity', '0.3'); // Subtle opacity via attribute
|
||||
svg.appendChild(gridLine);
|
||||
});
|
||||
|
||||
// 2. Gradient Definition
|
||||
const defs = document.createElementNS(svgNs, 'defs');
|
||||
const gradientId = 'goalMomentumGradient-' + Math.random().toString(36).substr(2, 9);
|
||||
const gradient = document.createElementNS(svgNs, 'linearGradient');
|
||||
gradient.setAttribute('id', gradientId);
|
||||
gradient.setAttribute('x1', '0%');
|
||||
gradient.setAttribute('y1', '0%');
|
||||
gradient.setAttribute('x2', '0%');
|
||||
gradient.setAttribute('y2', '100%');
|
||||
|
||||
const stop1 = document.createElementNS(svgNs, 'stop');
|
||||
stop1.setAttribute('offset', '0%');
|
||||
stop1.setAttribute('stop-color', 'var(--interactive-accent)');
|
||||
stop1.setAttribute('stop-opacity', '0.5');
|
||||
|
||||
const stop2 = document.createElementNS(svgNs, 'stop');
|
||||
stop2.setAttribute('offset', '100%');
|
||||
stop2.setAttribute('stop-color', 'var(--interactive-accent)');
|
||||
stop2.setAttribute('stop-opacity', '0.05');
|
||||
|
||||
gradient.appendChild(stop1);
|
||||
gradient.appendChild(stop2);
|
||||
defs.appendChild(gradient);
|
||||
svg.appendChild(defs);
|
||||
|
||||
// 3. Draw Paths
|
||||
const lineD = points.map((p, i) => (i === 0 ? `M ${p.x},${p.y}` : `L ${p.x},${p.y}`)).join(' ');
|
||||
const areaD = `${lineD} L ${points[points.length - 1].x},${padding.top + height} L ${points[0].x},${padding.top + height} Z`;
|
||||
|
||||
const areaPath = document.createElementNS(svgNs, 'path');
|
||||
areaPath.classList.add('cpwn-chart-area-fill');
|
||||
areaPath.setAttribute('d', areaD);
|
||||
areaPath.setAttribute('fill', `url(#${gradientId})`);
|
||||
svg.appendChild(areaPath);
|
||||
|
||||
const linePath = document.createElementNS(svgNs, 'path');
|
||||
linePath.classList.add('cpwn-chart-line-stroke');
|
||||
linePath.setAttribute('d', lineD);
|
||||
svg.appendChild(linePath);
|
||||
|
||||
// 4. Dots & Hit Targets
|
||||
points.forEach((p, i) => {
|
||||
const circle = document.createElementNS(svgNs, 'circle');
|
||||
circle.classList.add('cpwn-chart-dot');
|
||||
circle.setAttribute('cx', p.x);
|
||||
circle.setAttribute('cy', p.y);
|
||||
circle.setAttribute('r', '3.5');
|
||||
svg.appendChild(circle);
|
||||
|
||||
// Invisible Hit Target
|
||||
const hitTarget = document.createElementNS(svgNs, 'rect');
|
||||
hitTarget.classList.add('cpwn-chart-hit-target');
|
||||
hitTarget.setAttribute('x', p.x - 10);
|
||||
hitTarget.setAttribute('y', padding.top);
|
||||
hitTarget.setAttribute('width', 20);
|
||||
hitTarget.setAttribute('height', height);
|
||||
|
||||
// Tooltip Events (Using Arrow Functions to fix 'this' context)
|
||||
hitTarget.addEventListener('mouseenter', (e) => {
|
||||
const label = chartData.labels[i];
|
||||
this.showTooltip(e, `${label}: ${p.val} pts`);
|
||||
circle.setAttribute('r', '5.5'); // Highlight effect
|
||||
});
|
||||
|
||||
hitTarget.addEventListener('mousemove', (e) => {
|
||||
this.moveTooltip(e);
|
||||
});
|
||||
|
||||
hitTarget.addEventListener('mouseleave', () => {
|
||||
this.hideTooltip();
|
||||
circle.setAttribute('r', '3.5'); // Reset
|
||||
});
|
||||
|
||||
svg.appendChild(hitTarget);
|
||||
});
|
||||
|
||||
const labelClass = options.labelClass || 'cpwn-axis-label';
|
||||
|
||||
// 5. X-Axis Labels
|
||||
chartData.labels.forEach((label, i) => {
|
||||
const text = document.createElementNS(svgNs, 'text');
|
||||
text.classList.add(labelClass);
|
||||
text.setAttribute('text-anchor', 'middle');
|
||||
text.setAttribute('x', points[i].x);
|
||||
text.setAttribute('y', 130);
|
||||
text.textContent = label; // Use textContent
|
||||
svg.appendChild(text);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
showTooltip(e, text) {
|
||||
// 1. Create tooltip element if it doesn't exist yet
|
||||
if (!this.tooltipEl) {
|
||||
this.tooltipEl = document.body.createDiv({ cls: 'cpwn-chart-tooltip' });
|
||||
// Safety: Ensure it doesn't block mouse events for the chart
|
||||
this.tooltipEl.style.pointerEvents = 'none';
|
||||
this.tooltipEl.style.position = 'absolute';
|
||||
this.tooltipEl.style.zIndex = '9999'; // Ensure it sits on top
|
||||
}
|
||||
|
||||
// 2. Set the text content
|
||||
this.tooltipEl.setText(text);
|
||||
|
||||
// 3. Position and show it
|
||||
this.moveTooltip(e);
|
||||
this.tooltipEl.style.display = 'block';
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the tooltip position to follow the mouse.
|
||||
* Adds smart positioning to prevent overflow on the right edge.
|
||||
* @param {MouseEvent} e - The mouse movement event.
|
||||
*/
|
||||
moveTooltip(e) {
|
||||
if (this.tooltipEl) {
|
||||
const tooltipRect = this.tooltipEl.getBoundingClientRect();
|
||||
const viewportWidth = window.innerWidth;
|
||||
const offset = 15; // Base offset from cursor
|
||||
|
||||
let left = e.pageX + offset;
|
||||
let top = e.pageY + offset;
|
||||
|
||||
// Check for right overflow
|
||||
if (left + tooltipRect.width > viewportWidth - 20) { // 20px buffer
|
||||
// Flip to left side of cursor
|
||||
left = e.pageX - tooltipRect.width - offset;
|
||||
}
|
||||
|
||||
// Optional: Check for bottom overflow (flip up)
|
||||
const viewportHeight = window.innerHeight;
|
||||
if (top + tooltipRect.height > viewportHeight - 20) {
|
||||
top = e.pageY - tooltipRect.height - offset;
|
||||
}
|
||||
|
||||
this.tooltipEl.style.left = `${left}px`;
|
||||
this.tooltipEl.style.top = `${top}px`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Hides the tooltip from view.
|
||||
*/
|
||||
hideTooltip() {
|
||||
if (this.tooltipEl) {
|
||||
this.tooltipEl.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
48
src/widgets/GoalDataAggregator.js
Normal file
48
src/widgets/GoalDataAggregator.js
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { GoalTracker } from '../logic/GoalTracker';
|
||||
import { moment } from 'obsidian';
|
||||
|
||||
export class GoalDataAggregator {
|
||||
|
||||
static async getTodaysGoals(app, settings, allTasks) {
|
||||
const tracker = new GoalTracker(app, settings);
|
||||
const score = await tracker.calculateDailyScore(moment(), allTasks);
|
||||
return score.results; // [{ name: 'Journal', isMet: true, ... }]
|
||||
}
|
||||
|
||||
static async getPointsHistory(app, settings, allTasks) {
|
||||
const tracker = new GoalTracker(app, settings);
|
||||
const labels = [];
|
||||
const data = []; // This will hold the ACTUAL cumulative totals
|
||||
|
||||
// 1. Calculate "Baseline" Total (Score from deep past up to 7 days ago)
|
||||
let runningTotal = 0;
|
||||
const startOfHistory = moment().subtract(30, 'days');
|
||||
const startOfChart = moment().subtract(6, 'days');
|
||||
|
||||
// Calculate baseline (simplified loop for performance)
|
||||
for (let d = startOfHistory.clone(); d.isBefore(startOfChart, 'day'); d.add(1, 'day')) {
|
||||
const score = await tracker.calculateDailyScore(d, allTasks);
|
||||
runningTotal += score.totalPoints;
|
||||
}
|
||||
|
||||
// 2. Calculate Visible 7 Days
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const date = moment().subtract(i, 'days');
|
||||
labels.push(date.format('ddd'));
|
||||
|
||||
const score = await tracker.calculateDailyScore(date, allTasks);
|
||||
runningTotal += score.totalPoints;
|
||||
data.push(runningTotal);
|
||||
}
|
||||
|
||||
return {
|
||||
labels,
|
||||
datasets: [{
|
||||
label: 'Total Points',
|
||||
data: data // [150, 160, 165...]
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
// src/widgets/TaskDataAggregator.js
|
||||
import { moment } from 'obsidian';
|
||||
|
||||
export class TaskDataAggregator {
|
||||
/**
|
||||
* Filters and groups tasks based on a widget configuration.
|
||||
|
|
|
|||
523
styles.css
523
styles.css
|
|
@ -3109,6 +3109,11 @@ body.is-mobile .cpwn-tabs-content-wrapper {
|
|||
fill: var(--text-muted);
|
||||
}
|
||||
|
||||
.cpwn-pm-line-chart .cpwn-small-axis-label {
|
||||
font-size: 14px;
|
||||
fill: var(--text-muted);
|
||||
}
|
||||
|
||||
.cpwn-pm-line-chart .line-path {
|
||||
fill: none;
|
||||
stroke-width: 3.5; /* Slightly thicker line */
|
||||
|
|
@ -3485,21 +3490,21 @@ body.is-mobile .cpwn-tabs-content-wrapper {
|
|||
|
||||
/* Pie Chart Size Differentiation */
|
||||
.cpwn-pm-widget-medium .cpwn-pm-widget-pie {
|
||||
min-height: 175px; /* Default for medium */
|
||||
max-height: 150px; /* Default for medium */
|
||||
}
|
||||
.cpwn-pm-widget-medium .cpwn-pm-widget-pie .cpwn-pm-pie-chart {
|
||||
min-height: 150px;
|
||||
/*min-height: 150px;*/
|
||||
max-height: 175px;
|
||||
}
|
||||
|
||||
.cpwn-pm-widget-large .cpwn-pm-widget-pie {
|
||||
min-height: 260px; /* Taller for large */
|
||||
max-height: 260px; /* Taller for large */
|
||||
}
|
||||
|
||||
/* Make sure the pie itself scales appropriately */
|
||||
.cpwn-pm-widget-large .cpwn-pm-widget-pie .cpwn-pm-pie-chart {
|
||||
min-height: 220px;
|
||||
max-height: 220px;
|
||||
/* min-height: 220px;*/
|
||||
max-height: 260px;
|
||||
}
|
||||
|
||||
/* -- LAYOUTS -- */
|
||||
|
|
@ -4028,4 +4033,510 @@ body.is-mobile .cpwn-tabs-content-wrapper {
|
|||
|
||||
.cpwn-fade-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Chart Container */
|
||||
.cpwn-pm-css-chart-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cpwn-pm-css-chart-container svg {
|
||||
overflow: visible;
|
||||
width: 100%;
|
||||
font-family: var(--font-ui);
|
||||
}
|
||||
|
||||
/* Grid Lines */
|
||||
.cpwn-chart-grid-line {
|
||||
stroke: var(--background-modifier-border);
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
|
||||
.cpwn-chart-line-stroke {
|
||||
fill: none;
|
||||
stroke: var(--interactive-accent);
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
/* Data Points */
|
||||
.cpwn-chart-dot {
|
||||
fill: var(--interactive-accent);
|
||||
stroke: var(--background-primary);
|
||||
stroke-width: 1px;
|
||||
transition: r 0.2s ease;
|
||||
}
|
||||
|
||||
/* Hit Targets (Invisible) */
|
||||
.cpwn-chart-hit-target {
|
||||
fill: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Axis Labels */
|
||||
.cpwn-axis-label {
|
||||
font-size: 10px;
|
||||
fill: var(--text-muted);
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
|
||||
/* Tooltip */
|
||||
.cpwn-chart-tooltip {
|
||||
position: absolute;
|
||||
z-index: var(--layer-tooltip);
|
||||
pointer-events: none;
|
||||
padding: 4px 8px;
|
||||
background-color: var(--background-modifier-message);
|
||||
color: var(--text-normal);
|
||||
border-radius: var(--radius-s);
|
||||
font-size: var(--font-ui-smaller);
|
||||
box-shadow: var(--shadow-s);
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Goal Row Layout */
|
||||
.cpwn-goal-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
/* Left Section (Icon + Text) */
|
||||
.cpwn-goal-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
min-width: 0; /* Allows text truncation if needed */
|
||||
}
|
||||
|
||||
/* Icon Styling */
|
||||
.cpwn-goal-icon {
|
||||
margin-right: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cpwn-goal-icon.is-completed {
|
||||
color: var(--text-success);
|
||||
}
|
||||
|
||||
.cpwn-goal-icon.is-incomplete {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Text Container */
|
||||
.cpwn-goal-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.cpwn-goal-title {
|
||||
font-weight: var(--font-ui-small);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.cpwn-goal-progress {
|
||||
font-size: var(--font-ui-smaller);
|
||||
/* cpwn-muted-text handles color, but we ensure size here */
|
||||
}
|
||||
|
||||
/* Right Section (Points) */
|
||||
.cpwn-goal-right {
|
||||
white-space: nowrap;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.cpwn-goal-points {
|
||||
font-weight: bold;
|
||||
font-size: var(--font-ui-small);
|
||||
color: var(--text-accent);
|
||||
}
|
||||
|
||||
.cpwn-goal-list-left .cpwn-goal-row:first-child {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* --- Daily Goals Widget Split Layout --- */
|
||||
.cpwn-goal-split-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 80px; /* List takes available space, Level takes fixed 80px */
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Left List Styling */
|
||||
.cpwn-goal-list-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/*gap: 6px;*/
|
||||
}
|
||||
|
||||
.cpwn-goal-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.cpwn-goal-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
overflow: hidden; /* Prevent text overflow */
|
||||
}
|
||||
|
||||
.cpwn-goal-title-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.cpwn-goal-title {
|
||||
font-size: 0.95em;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.cpwn-goal-count-inline {
|
||||
font-size: 0.85em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.cpwn-goal-right {
|
||||
margin-left: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cpwn-goal-points {
|
||||
font-weight: bold;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.cpwn-points-pos { color: var(--text-accent); }
|
||||
.cpwn-points-neg { color: var(--text-error); }
|
||||
|
||||
/* Right Level Styling */
|
||||
.cpwn-goal-level-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cpwn-level-circle-wrapper {
|
||||
position: relative;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cpwn-level-icon-inner {
|
||||
position: absolute;
|
||||
color: var(--text-normal);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.cpwn-level-icon-inner svg {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.cpwn-level-title-text {
|
||||
font-size: 0.75em;
|
||||
color: var(--text-muted);
|
||||
margin-top: 6px;
|
||||
line-height: 1.1;
|
||||
max-width: 80px;
|
||||
}
|
||||
|
||||
.cpwn-level-circle-wrapper svg circle:nth-child(2) {
|
||||
transition: stroke-dashoffset 0.5s ease;
|
||||
/* Ensure the stroke color is visible */
|
||||
stroke: var(--text-accent);
|
||||
/* Ensure it's on top */
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* Goal progress tooltip, styled like a mini popup window */
|
||||
.cpwn-goal-tooltip {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
max-width: 360px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
|
||||
color: var(--text-normal);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease-out;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Optional: reuse popup font/spacing feel */
|
||||
.cpwn-goal-tooltip-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* Wrapper for the mini ring */
|
||||
.cpwn-goal-tooltip-ring-wrapper {
|
||||
position: relative;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cpwn-goal-tooltip-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Performance state icon (color set inline from JS) */
|
||||
.cpwn-goal-tooltip-icon-perf {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Level icon uses the progress accent colour */
|
||||
.cpwn-goal-tooltip-icon-level {
|
||||
/* Bigger icon in the popup */
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
/* Progress value (0–100) comes from JS; default to 0 */
|
||||
--cpwn-goal-progress: 0;
|
||||
|
||||
/* Outer ring: accent for [0, progress], dull border after that */
|
||||
background:
|
||||
conic-gradient(
|
||||
var(--text-accent)
|
||||
0deg
|
||||
calc(var(--cpwn-goal-progress) * 3.6deg),
|
||||
var(--background-modifier-border)
|
||||
calc(var(--cpwn-goal-progress) * 3.6deg)
|
||||
360deg
|
||||
),
|
||||
radial-gradient(
|
||||
farthest-side,
|
||||
var(--background-primary) 70%,
|
||||
transparent 71%
|
||||
);
|
||||
|
||||
/* Make sure the inner circle stays the background color, ring outside */
|
||||
background-origin: border-box;
|
||||
background-clip: border-box, padding-box;
|
||||
}
|
||||
|
||||
|
||||
/* Center the Lucide icon over the SVG ring */
|
||||
.cpwn-goal-tooltip-ring-icon {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Make the Lucide glyph itself clearly visible inside the ring */
|
||||
.cpwn-goal-tooltip-ring-icon svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
stroke: var(--text-normal);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.cpwn-goal-tooltip-title {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cpwn-goal-tooltip-body {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.cpwn-goal-tooltip-line-strong {
|
||||
font-weight: 500;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.cpwn-goal-tooltip-line {
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.cpwn-goal-tooltip-spacer {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.cpwn-goal-tooltip-spacer-large {
|
||||
height: 8px; /* gap between “Nice and steady…” and “At this pace…” */
|
||||
}
|
||||
|
||||
/* Weekday pill selector in GoalEditModal */
|
||||
.cpwn-goal-days-picker {
|
||||
display: inline-flex;
|
||||
border-radius: 999px;
|
||||
overflow: hidden; /* clip inner corners */
|
||||
background-color: var(--background-modifier-border);
|
||||
}
|
||||
|
||||
/* Each day cell: larger tap target */
|
||||
.cpwn-goal-day {
|
||||
padding: 6px 16px; /* bigger horizontally and vertically */
|
||||
min-width: 28px; /* wider cells */
|
||||
text-align: center;
|
||||
font-size: 12px; /* larger letter */
|
||||
letter-spacing: 0.06em;
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
background-color: transparent;
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
/* Make the very first cell visually round on the left */
|
||||
.cpwn-goal-day:first-child {
|
||||
border-top-left-radius: 999px;
|
||||
border-bottom-left-radius: 999px;
|
||||
}
|
||||
|
||||
/* Make the very last cell visually round on the right */
|
||||
.cpwn-goal-day:last-child {
|
||||
border-top-right-radius: 999px;
|
||||
border-bottom-right-radius: 999px;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* Active days: Obsidian accent, but keep corner rounding from above */
|
||||
.cpwn-goal-day.is-active {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent, #fff);
|
||||
}
|
||||
|
||||
.cpwn-level-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 6px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.cpwn-level-table th,
|
||||
.cpwn-level-table td {
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.cpwn-level-table tr:nth-child(even) {
|
||||
background-color: var(--background-secondary-alt);
|
||||
}
|
||||
|
||||
.cpwn-level-icon svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.cpwn-goal-item .setting-item {
|
||||
border-top: none;
|
||||
border-bottom: none;
|
||||
padding: 0; /* Remove default padding if it's too large */
|
||||
}
|
||||
|
||||
/* Dim the entire row */
|
||||
.cpwn-goal-item.cpwn-goal-inactive {
|
||||
opacity: 0.5;
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
/* Style the "Rest" icon */
|
||||
.cpwn-goal-item.cpwn-goal-inactive .cpwn-goal-icon {
|
||||
color: var(--text-muted);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Style the "OFF" badge */
|
||||
.cpwn-goal-rest-badge {
|
||||
font-size: 0.7em;
|
||||
font-weight: bold;
|
||||
color: var(--text-muted);
|
||||
border: 1px solid var(--text-muted);
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
margin-left: auto; /* Push to the right */
|
||||
}
|
||||
|
||||
/* Condensed Daily Goal Widget Layout */
|
||||
.cpwn-goal-split-container.condensed {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.cpwn-goal-summary-large {
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.cpwn-goal-summary-sub {
|
||||
font-size: 0.8em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* The larger font size for the condensed chart */
|
||||
.cpwn-axis-label-large {
|
||||
font-size: 16px; /* Larger font */
|
||||
fill: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Vacation Text */
|
||||
.cpwn-summary-vacation {
|
||||
font-size: 0.9em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Large Numbers (e.g. "1/3") */
|
||||
.cpwn-summary-large {
|
||||
font-size: 2.2em;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.cpwn-summary-icon-rest {
|
||||
display: flex;
|
||||
justify-content: center; /* Center horizontally */
|
||||
margin-top: 10px;
|
||||
margin-bottom: 2px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.cpwn-summary-icon-rest svg {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
/* Subtext (e.g. "goals met" or "2 off today") */
|
||||
.cpwn-summary-sub {
|
||||
padding-top: 10px;
|
||||
font-size: 0.6em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue