├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── bin └── v1 │ └── nomie-plugin.js ├── build-tools └── publish-2bin.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── src ├── assets │ └── meta.png ├── index.html ├── lib │ └── dayjs.ts ├── nui │ ├── background.css │ ├── button-group.css │ ├── button.css │ ├── forms.css │ ├── modal.css │ ├── nui.css │ ├── text.css │ ├── title.css │ └── toolbar.css ├── styles │ ├── index.css │ ├── markdown.css │ └── nui@v1.css └── v1 │ ├── index.html │ ├── index.ts │ ├── plugin-connect.ts │ ├── plugins │ ├── meditate │ │ ├── index.html │ │ └── meditate.js │ ├── memories │ │ ├── index.html │ │ └── memories.js │ ├── my-people │ │ ├── index.html │ │ ├── my-people.js │ │ └── person.class.js │ ├── tester │ │ ├── index.html │ │ ├── template-test.json │ │ └── tester.js │ └── weather │ │ ├── index.html │ │ └── weather.js │ └── wip │ ├── lets-eat │ ├── index.html │ ├── lets-eat.css │ └── lets-eat.js │ └── nui │ └── nui.html ├── tailwind.config.js ├── tsconfig.json ├── webpack-prod.config.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/macos,windows,linux 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows,linux 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | 8 | dist 9 | node_modules 10 | 11 | # temporary files which can be created if a process still has a handle open of a deleted file 12 | .fuse_hidden* 13 | 14 | # KDE directory preferences 15 | .directory 16 | 17 | # Linux trash folder which might appear on any partition or disk 18 | .Trash-* 19 | 20 | # .nfs files are created when an open file is removed but is still being accessed 21 | .nfs* 22 | 23 | ### macOS ### 24 | # General 25 | .DS_Store 26 | .AppleDouble 27 | .LSOverride 28 | 29 | # Icon must end with two \r 30 | Icon 31 | 32 | 33 | # Thumbnails 34 | ._* 35 | 36 | # Files that might appear in the root of a volume 37 | .DocumentRevisions-V100 38 | .fseventsd 39 | .Spotlight-V100 40 | .TemporaryItems 41 | .Trashes 42 | .VolumeIcon.icns 43 | .com.apple.timemachine.donotpresent 44 | 45 | # Directories potentially created on remote AFP share 46 | .AppleDB 47 | .AppleDesktop 48 | Network Trash Folder 49 | Temporary Items 50 | .apdisk 51 | 52 | ### macOS Patch ### 53 | # iCloud generated files 54 | *.icloud 55 | 56 | ### Windows ### 57 | # Windows thumbnail cache files 58 | Thumbs.db 59 | Thumbs.db:encryptable 60 | ehthumbs.db 61 | ehthumbs_vista.db 62 | 63 | # Dump file 64 | *.stackdump 65 | 66 | # Folder config file 67 | [Dd]esktop.ini 68 | 69 | # Recycle Bin used on file shares 70 | $RECYCLE.BIN/ 71 | 72 | # Windows Installer files 73 | *.cab 74 | *.msi 75 | *.msix 76 | *.msm 77 | *.msp 78 | 79 | # Windows shortcuts 80 | *.lnk 81 | 82 | # End of https://www.toptal.com/developers/gitignore/api/macos,windows,linux 83 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "docwriter.style": "Auto-detect", 3 | "liveServer.settings.port": "30001" 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 CodeWithAhsan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nomie6-oss Plugins [WIP] 2 | 3 | Nomie Plugins allows people who are familiar with HTML and Javascript to create entirely new methods of tracking and monitoring data within Nomie6-oss. 4 | 5 | ## Introduction 6 | 7 | Nomie Plugins use iframes to load and communicate with “plugins”. So this really just means, a plugin is a hosted HTML page, that has some extra javascript to request data to and from Nomie. We do this by using postMessage and window.onMessage to securely pass data between Nomie and your Plugin. 8 | 9 | ### Resources 10 | 11 | **Repo** [https://github.com/open-nomie/plugins](https://github.com/open-nomie/plugins/) 12 | 13 | **Nomie6-oss on GitHub Pages**: [https://open-nomie.github.io](https://open-nomie.github.io/) 14 | 15 | # nomie-plugin.js 16 | 17 | This library abstracts the responsibility of posting messages and listening for messages into simple async calls. This document outlines the specific functions of `nomie-plugin.js`. 18 | 19 | ### Including the nomie-plugin.js library 20 | 21 | ```jsx 22 | 11 | 12 | 13 | Continue 14 | 15 | -------------------------------------------------------------------------------- /src/lib/dayjs.ts: -------------------------------------------------------------------------------- 1 | const dayjs = require('dayjs'); 2 | const weekOfYear = require('dayjs/plugin/weekOfYear'); 3 | const advancedFormat = require('dayjs/plugin/advancedFormat'); 4 | const dayOfYear = require('dayjs/plugin/dayOfYear'); 5 | const isoWeek = require('dayjs/plugin/isoWeek'); 6 | const relativeTime = require('dayjs/plugin/relativeTime') 7 | 8 | dayjs.extend(relativeTime) 9 | dayjs.extend(relativeTime) 10 | dayjs.extend(weekOfYear) 11 | dayjs.extend(advancedFormat) 12 | dayjs.extend(dayOfYear) 13 | dayjs.extend(isoWeek) 14 | 15 | export default dayjs; -------------------------------------------------------------------------------- /src/nui/background.css: -------------------------------------------------------------------------------- 1 | .nui-bg-solid { 2 | @apply bg-white dark:bg-black; 3 | } 4 | 5 | .nui-bg-faded { 6 | @apply dark:bg-white bg-black bg-opacity-10 dark:bg-opacity-10; 7 | } 8 | 9 | .nui-bg-primary { 10 | @apply bg-primary-500 dark:bg-primary-400; 11 | } 12 | 13 | .nui-bg-glass { 14 | @apply dark:bg-gray-900 15 | bg-gray-100 16 | bg-opacity-80 17 | dark:bg-opacity-70 18 | backdrop-blur-sm; 19 | } 20 | 21 | .nui-bg-50 { 22 | @apply bg-gray-50 dark:bg-gray-900; 23 | } 24 | 25 | .nui-bg-100 { 26 | @apply bg-gray-100 dark:bg-gray-900; 27 | } 28 | 29 | .nui-bg-200 { 30 | @apply bg-gray-200 dark:bg-gray-800; 31 | } 32 | 33 | .nui-bg-300 { 34 | @apply bg-gray-300 dark:bg-gray-700; 35 | } 36 | .nui-bg-400 { 37 | @apply bg-gray-400 dark:bg-gray-600; 38 | } 39 | 40 | .nui-bg-500 { 41 | @apply bg-gray-500; 42 | } 43 | 44 | .nui-bg-600 { 45 | @apply bg-gray-600 dark:bg-gray-400; 46 | } 47 | 48 | .nui-bg-700 { 49 | @apply bg-gray-700 dark:bg-gray-300; 50 | } 51 | 52 | .nui-bg-800 { 53 | @apply bg-gray-800 dark:bg-gray-200; 54 | } 55 | 56 | .nui-bg-900 { 57 | @apply bg-gray-900 dark:bg-gray-100; 58 | } 59 | -------------------------------------------------------------------------------- /src/nui/button-group.css: -------------------------------------------------------------------------------- 1 | .nui-button-group { 2 | @apply w-full; 3 | @apply flex items-center; 4 | @apply divide-x divide-gray-500 divide-opacity-30; 5 | @apply rounded-lg overflow-hidden; 6 | @apply p-1; 7 | @apply border border-gray-500 border-opacity-20 dark:border-gray-400 dark:border-opacity-40; 8 | @apply nui-bg-solid; 9 | 10 | > button { 11 | @apply w-full; 12 | @apply font-semibold; 13 | } 14 | 15 | .active { 16 | @apply bg-primary-600 text-white; 17 | } 18 | &.compact { 19 | > button { 20 | @apply py-1 px-2; 21 | @apply text-sm; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/nui/button.css: -------------------------------------------------------------------------------- 1 | .nui-button { 2 | @apply rounded-lg; 3 | @apply px-4 py-2; 4 | @apply transition-all duration-100; 5 | @apply transform; 6 | @apply leading-tight; 7 | } 8 | .nui-button:active { 9 | transform: scale(0.98); 10 | } 11 | 12 | .nui-button:focus { 13 | outline: none; 14 | @apply ring ring-current; 15 | @apply ring-opacity-30; 16 | } 17 | .nui-button.solid:active { 18 | @apply shadow-lg; 19 | } 20 | 21 | .nui-button.text { 22 | @apply uppercase; 23 | @apply font-semibold; 24 | } 25 | 26 | .nui-button:disabled { 27 | @apply opacity-50; 28 | } 29 | 30 | .nui-button.sm { 31 | @apply px-3 py-2; 32 | @apply text-sm; 33 | line-height: 110%; 34 | } 35 | 36 | .nui-button.xs { 37 | padding: 4px 6px; 38 | @apply text-xs; 39 | } 40 | .nui-button.primary { 41 | @apply text-primary-600; 42 | @apply dark:text-primary-500; 43 | } 44 | .nui-button.solid { 45 | @apply shadow-md; 46 | } 47 | .nui-button.primary.solid { 48 | @apply bg-primary-600; 49 | @apply text-white; 50 | } 51 | -------------------------------------------------------------------------------- /src/nui/forms.css: -------------------------------------------------------------------------------- 1 | .nui-form { 2 | @apply flex flex-col; 3 | @apply w-full; 4 | @apply space-y-4; 5 | } 6 | 7 | .nui-fieldset { 8 | @apply w-full; 9 | @apply flex flex-col space-y-2; 10 | 11 | label { 12 | @apply text-sm; 13 | @apply text-black dark:text-white text-opacity-80; 14 | @apply font-medium; 15 | } 16 | 17 | &:focus-within label { 18 | @apply text-opacity-100; 19 | } 20 | 21 | &:invalid-within label { 22 | @apply text-red-500; 23 | } 24 | } 25 | 26 | .nui-fieldset.inline { 27 | @apply items-center; 28 | @apply space-y-0; 29 | label { 30 | @apply w-20; 31 | @apply leading-tight; 32 | @apply flex-shrink-0 flex-grow-0; 33 | } 34 | input, 35 | select { 36 | @apply flex-grow flex-shrink; 37 | @apply mt-0; 38 | } 39 | @apply flex flex-row space-x-4 items-center justify-center; 40 | } 41 | 42 | select.nui-input { 43 | @apply relative; 44 | @apply pr-7; 45 | background-repeat: no-repeat; 46 | background-size: 10px 5.8px; 47 | background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjgiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAyOCAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTAgMEwxMy43MTQzIDE2TDI3LjQyODYgMEgwWiIgZmlsbD0iIzdCN0I3QiIgZmlsbC1vcGFjaXR5PSIwLjY4Ii8+Cjwvc3ZnPgo="); 48 | background-position-x: calc(100% - 10px); 49 | background-position-y: calc(100% * 0.5); 50 | } 51 | 52 | select.nui-input.compact { 53 | @apply pr-7; 54 | } 55 | 56 | .nui-description { 57 | @apply text-xs leading-tight; 58 | @apply text-gray-400 dark:text-gray-600; 59 | } 60 | 61 | .nui-input { 62 | @apply appearance-none; 63 | @apply nui-bg-solid; 64 | @apply rounded-full; 65 | @apply border border-gray-500 border-opacity-20 dark:border-gray-400 dark:border-opacity-40; 66 | @apply py-2 px-3; 67 | @apply flex; 68 | @apply dark:text-white; 69 | @apply shadow-sm; 70 | @apply transform-gpu transform transition-all duration-100; 71 | min-height: 36px; 72 | } 73 | .nui-input:active, 74 | .nui-input:focus { 75 | @apply outline-none; 76 | @apply ring ring-primary-500; 77 | @apply shadow-lg; 78 | } 79 | 80 | .nui-input.transparent { 81 | @apply border-transparent; 82 | } 83 | 84 | .nui-input.compact { 85 | @apply py-px px-2; 86 | min-height: 28px; 87 | } 88 | 89 | .nui-input:not(:placeholder-shown):invalid { 90 | @apply text-red-500; 91 | @apply ring ring-red-500; 92 | } 93 | 94 | /* The switch - the box around the slider */ 95 | .nui-switch { 96 | @apply rounded-full; 97 | @apply relative; 98 | @apply inline-block; 99 | @apply w-14; 100 | @apply h-8; 101 | 102 | /* Hide default HTML checkbox */ 103 | input { 104 | @apply opacity-0 h-0 w-0; 105 | @apply rounded-full; 106 | } 107 | 108 | .nui-control { 109 | @apply rounded-full; 110 | @apply absolute; 111 | @apply cursor-pointer; 112 | @apply top-0 left-0 right-0 bottom-0; 113 | @apply transition-all duration-200; 114 | @apply bg-gray-300 dark:bg-gray-700; 115 | } 116 | 117 | .nui-control:before { 118 | @apply absolute; 119 | content: ""; 120 | @apply h-6 w-6; 121 | @apply left-1 top-1; 122 | @apply rounded-full; 123 | @apply bg-white dark:bg-gray-300; 124 | -webkit-transition: 0.4s; 125 | transition: 0.4s; 126 | } 127 | 128 | input:checked + .nui-control { 129 | @apply bg-primary-500; 130 | } 131 | 132 | input:focus + .nui-control { 133 | box-shadow: 0 0 1px #2196f3; 134 | } 135 | 136 | input:checked + .nui-control:before { 137 | -webkit-transform: translateX(24px); 138 | -ms-transform: translateX(24px); 139 | transform: translateX(24px); 140 | } 141 | } 142 | /* // end switch */ 143 | -------------------------------------------------------------------------------- /src/nui/modal.css: -------------------------------------------------------------------------------- 1 | .nui-backdrop { 2 | @apply fixed; 3 | @apply z-50; 4 | @apply w-screen h-screen; 5 | @apply bg-gray-500 backdrop-blur-sm backdrop-filter bg-opacity-70; 6 | @apply flex flex-col items-center justify-center; 7 | } 8 | 9 | .nui-modal { 10 | @apply rounded-2xl; 11 | @apply bg-gray-100 dark:bg-gray-900; 12 | @apply shadow-2xl; 13 | max-height: calc(100vh - 20%); 14 | min-height: calc(50vh); 15 | width: calc(100vw - 10%); 16 | max-width: 500px; 17 | 18 | &.fullscreen { 19 | max-width: calc(100vw - 40px); 20 | max-height: calc(100vh - 40px); 21 | width: calc(100vw - 40px); 22 | height: calc(100vh - 40px); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/nui/nui.css: -------------------------------------------------------------------------------- 1 | /* Title Styles */ 2 | @import "background.css"; 3 | @import "text.css"; 4 | @import "button.css"; 5 | @import "forms.css"; 6 | @import "button-group.css"; 7 | @import "title.css"; 8 | @import "modal.css"; 9 | 10 | /* Text Styles */ 11 | .nui-hbar { 12 | @apply flex w-full items-center; 13 | } 14 | 15 | .nui-vbar { 16 | @apply flex flex-col w-full justify-center; 17 | } 18 | 19 | .nui-toolbar { 20 | @apply flex items-center; 21 | @apply px-4 py-2; 22 | min-height: 48px; 23 | } 24 | 25 | .nui-toolbar.shadow { 26 | @apply shadow-md; 27 | } 28 | 29 | .nui-filler { 30 | @apply min-w-0 31 | max-w-full 32 | min-h-0 33 | max-h-full 34 | flex-grow 35 | flex-shrink; 36 | } 37 | .nui-stiff { 38 | @apply flex-grow-0 flex-shrink-0; 39 | } 40 | 41 | .nui-card { 42 | @apply nui-bg-solid; 43 | @apply shadow-lg; 44 | @apply rounded-lg; 45 | @apply border border-gray-500 border-opacity-20; 46 | } 47 | 48 | .nui-item { 49 | @apply text-left; 50 | @apply w-full; 51 | @apply flex items-center; 52 | @apply px-4 py-2; 53 | @apply space-x-2; 54 | min-height: 40px; 55 | @apply flex-grow; 56 | @apply font-medium; 57 | @apply text-gray-900 dark:text-gray-100; 58 | 59 | &:active { 60 | @apply ring ring-primary-500; 61 | } 62 | 63 | .clickable { 64 | @apply cursor-pointer; 65 | } 66 | } 67 | 68 | .nui-divider, 69 | hr[nui] { 70 | @apply w-full; 71 | @apply relative; 72 | @apply border-t border-gray-500 border-opacity-30 dark:border-opacity-40; 73 | } 74 | 75 | /* Switch */ 76 | 77 | .slide-enter-active, 78 | .slide-leave-active { 79 | @apply transition-all duration-200 ease-in-out; 80 | } 81 | .slide-enter, .slide-leave-to /* .fade-leave-active below version 2.1.8 */ { 82 | @apply translate-y-10; 83 | @apply transform-gpu; 84 | @apply opacity-0; 85 | } 86 | -------------------------------------------------------------------------------- /src/nui/text.css: -------------------------------------------------------------------------------- 1 | .nui-text-lg { 2 | @apply text-lg; 3 | } 4 | 5 | .nui-text-sm { 6 | @apply text-sm; 7 | } 8 | 9 | .nui-text-xl { 10 | @apply text-xl; 11 | } 12 | 13 | .nui-text-2xl { 14 | @apply text-2xl; 15 | } 16 | 17 | .nui-text-solid { 18 | @apply text-white dark:text-black; 19 | } 20 | .nui-text-inverse { 21 | @apply text-black dark:text-white; 22 | } 23 | .nui-text-primary { 24 | @apply text-primary-500; 25 | @apply dark:text-primary-400; 26 | } 27 | 28 | .nui-text-50 { 29 | @apply text-gray-50 dark:text-gray-900; 30 | } 31 | 32 | .nui-text-100 { 33 | @apply text-gray-100 dark:text-gray-900; 34 | } 35 | 36 | .nui-text-200 { 37 | @apply text-gray-200 dark:text-gray-800; 38 | } 39 | 40 | .nui-text-300 { 41 | @apply text-gray-300 dark:text-gray-700; 42 | } 43 | .nui-text-400 { 44 | @apply text-gray-400 dark:text-gray-600; 45 | } 46 | 47 | .nui-text-500 { 48 | @apply text-gray-500; 49 | } 50 | 51 | .nui-text-600 { 52 | @apply text-gray-600 dark:text-gray-400; 53 | } 54 | 55 | .nui-text-700 { 56 | @apply text-gray-700 dark:text-gray-300; 57 | } 58 | 59 | .nui-text-800 { 60 | @apply text-gray-800 dark:text-gray-200; 61 | } 62 | 63 | .nui-text-900 { 64 | @apply text-gray-900 dark:text-gray-100; 65 | } 66 | -------------------------------------------------------------------------------- /src/nui/title.css: -------------------------------------------------------------------------------- 1 | .nui-title { 2 | @apply font-bold; 3 | @apply text-lg; 4 | @apply leading-tight; 5 | @apply text-gray-800 dark:text-gray-200; 6 | } 7 | .nui-title.solid { 8 | @apply text-black dark:text-white; 9 | } 10 | 11 | .nui-title.primary { 12 | @apply text-primary-500 dark:text-primary-400; 13 | } 14 | 15 | .nui-title.md { 16 | @apply text-base; 17 | } 18 | 19 | .nui-title.sm { 20 | @apply text-sm; 21 | } 22 | -------------------------------------------------------------------------------- /src/nui/toolbar.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-nomie/plugins/9643d1ce0533e174fc5f0cfa2ed8876549807f27/src/nui/toolbar.css -------------------------------------------------------------------------------- /src/styles/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | background-color: blueviolet; 7 | } 8 | -------------------------------------------------------------------------------- /src/styles/markdown.css: -------------------------------------------------------------------------------- 1 | .markdown-body { 2 | -ms-text-size-adjust: 100%; 3 | -webkit-text-size-adjust: 100%; 4 | margin: 0; 5 | color: #24292f; 6 | background-color: #ffffff; 7 | font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"; 8 | font-size: 16px; 9 | line-height: 1.5; 10 | word-wrap: break-word; 11 | } 12 | 13 | .markdown-body .octicon { 14 | display: inline-block; 15 | fill: currentColor; 16 | vertical-align: text-bottom; 17 | } 18 | 19 | .markdown-body h1:hover .anchor .octicon-link:before, 20 | .markdown-body h2:hover .anchor .octicon-link:before, 21 | .markdown-body h3:hover .anchor .octicon-link:before, 22 | .markdown-body h4:hover .anchor .octicon-link:before, 23 | .markdown-body h5:hover .anchor .octicon-link:before, 24 | .markdown-body h6:hover .anchor .octicon-link:before { 25 | width: 16px; 26 | height: 16px; 27 | content: ' '; 28 | display: inline-block; 29 | background-color: currentColor; 30 | -webkit-mask-image: url("data:image/svg+xml,"); 31 | mask-image: url("data:image/svg+xml,"); 32 | } 33 | 34 | .markdown-body details, 35 | .markdown-body figcaption, 36 | .markdown-body figure { 37 | display: block; 38 | } 39 | 40 | .markdown-body summary { 41 | display: list-item; 42 | } 43 | 44 | .markdown-body [hidden] { 45 | display: none !important; 46 | } 47 | 48 | .markdown-body a { 49 | background-color: transparent; 50 | color: #0969da; 51 | text-decoration: none; 52 | } 53 | 54 | .markdown-body a:active, 55 | .markdown-body a:hover { 56 | outline-width: 0; 57 | } 58 | 59 | .markdown-body abbr[title] { 60 | border-bottom: none; 61 | text-decoration: underline dotted; 62 | } 63 | 64 | .markdown-body b, 65 | .markdown-body strong { 66 | font-weight: 600; 67 | } 68 | 69 | .markdown-body dfn { 70 | font-style: italic; 71 | } 72 | 73 | .markdown-body h1 { 74 | margin: .67em 0; 75 | font-weight: 600; 76 | padding-bottom: .3em; 77 | font-size: 2em; 78 | border-bottom: 1px solid hsla(210,18%,87%,1); 79 | } 80 | 81 | .markdown-body mark { 82 | background-color: #fff8c5; 83 | color: #24292f; 84 | } 85 | 86 | .markdown-body small { 87 | font-size: 90%; 88 | } 89 | 90 | .markdown-body sub, 91 | .markdown-body sup { 92 | font-size: 75%; 93 | line-height: 0; 94 | position: relative; 95 | vertical-align: baseline; 96 | } 97 | 98 | .markdown-body sub { 99 | bottom: -0.25em; 100 | } 101 | 102 | .markdown-body sup { 103 | top: -0.5em; 104 | } 105 | 106 | .markdown-body img { 107 | border-style: none; 108 | max-width: 100%; 109 | box-sizing: content-box; 110 | background-color: #ffffff; 111 | } 112 | 113 | .markdown-body code, 114 | .markdown-body kbd, 115 | .markdown-body pre, 116 | .markdown-body samp { 117 | font-family: monospace,monospace; 118 | font-size: 1em; 119 | } 120 | 121 | .markdown-body figure { 122 | margin: 1em 40px; 123 | } 124 | 125 | .markdown-body hr { 126 | box-sizing: content-box; 127 | overflow: hidden; 128 | background: transparent; 129 | border-bottom: 1px solid hsla(210,18%,87%,1); 130 | height: .25em; 131 | padding: 0; 132 | margin: 24px 0; 133 | background-color: #d0d7de; 134 | border: 0; 135 | } 136 | 137 | .markdown-body input { 138 | font: inherit; 139 | margin: 0; 140 | overflow: visible; 141 | font-family: inherit; 142 | font-size: inherit; 143 | line-height: inherit; 144 | } 145 | 146 | .markdown-body [type=button], 147 | .markdown-body [type=reset], 148 | .markdown-body [type=submit] { 149 | -webkit-appearance: button; 150 | } 151 | 152 | .markdown-body [type=button]::-moz-focus-inner, 153 | .markdown-body [type=reset]::-moz-focus-inner, 154 | .markdown-body [type=submit]::-moz-focus-inner { 155 | border-style: none; 156 | padding: 0; 157 | } 158 | 159 | .markdown-body [type=button]:-moz-focusring, 160 | .markdown-body [type=reset]:-moz-focusring, 161 | .markdown-body [type=submit]:-moz-focusring { 162 | outline: 1px dotted ButtonText; 163 | } 164 | 165 | .markdown-body [type=checkbox], 166 | .markdown-body [type=radio] { 167 | box-sizing: border-box; 168 | padding: 0; 169 | } 170 | 171 | .markdown-body [type=number]::-webkit-inner-spin-button, 172 | .markdown-body [type=number]::-webkit-outer-spin-button { 173 | height: auto; 174 | } 175 | 176 | .markdown-body [type=search] { 177 | -webkit-appearance: textfield; 178 | outline-offset: -2px; 179 | } 180 | 181 | .markdown-body [type=search]::-webkit-search-cancel-button, 182 | .markdown-body [type=search]::-webkit-search-decoration { 183 | -webkit-appearance: none; 184 | } 185 | 186 | .markdown-body ::-webkit-input-placeholder { 187 | color: inherit; 188 | opacity: .54; 189 | } 190 | 191 | .markdown-body ::-webkit-file-upload-button { 192 | -webkit-appearance: button; 193 | font: inherit; 194 | } 195 | 196 | .markdown-body a:hover { 197 | text-decoration: underline; 198 | } 199 | 200 | .markdown-body hr::before { 201 | display: table; 202 | content: ""; 203 | } 204 | 205 | .markdown-body hr::after { 206 | display: table; 207 | clear: both; 208 | content: ""; 209 | } 210 | 211 | .markdown-body table { 212 | border-spacing: 0; 213 | border-collapse: collapse; 214 | display: block; 215 | width: max-content; 216 | max-width: 100%; 217 | overflow: auto; 218 | } 219 | 220 | .markdown-body td, 221 | .markdown-body th { 222 | padding: 0; 223 | } 224 | 225 | .markdown-body details summary { 226 | cursor: pointer; 227 | } 228 | 229 | .markdown-body details:not([open])>*:not(summary) { 230 | display: none !important; 231 | } 232 | 233 | .markdown-body kbd { 234 | display: inline-block; 235 | padding: 3px 5px; 236 | font: 11px ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; 237 | line-height: 10px; 238 | color: #24292f; 239 | vertical-align: middle; 240 | background-color: #f6f8fa; 241 | border: solid 1px rgba(175,184,193,0.2); 242 | border-bottom-color: rgba(175,184,193,0.2); 243 | border-radius: 6px; 244 | box-shadow: inset 0 -1px 0 rgba(175,184,193,0.2); 245 | } 246 | 247 | .markdown-body h1, 248 | .markdown-body h2, 249 | .markdown-body h3, 250 | .markdown-body h4, 251 | .markdown-body h5, 252 | .markdown-body h6 { 253 | margin-top: 24px; 254 | margin-bottom: 16px; 255 | font-weight: 600; 256 | line-height: 1.25; 257 | } 258 | 259 | .markdown-body h2 { 260 | font-weight: 600; 261 | padding-bottom: .3em; 262 | font-size: 1.5em; 263 | border-bottom: 1px solid hsla(210,18%,87%,1); 264 | } 265 | 266 | .markdown-body h3 { 267 | font-weight: 600; 268 | font-size: 1.25em; 269 | } 270 | 271 | .markdown-body h4 { 272 | font-weight: 600; 273 | font-size: 1em; 274 | } 275 | 276 | .markdown-body h5 { 277 | font-weight: 600; 278 | font-size: .875em; 279 | } 280 | 281 | .markdown-body h6 { 282 | font-weight: 600; 283 | font-size: .85em; 284 | color: #57606a; 285 | } 286 | 287 | .markdown-body p { 288 | margin-top: 0; 289 | margin-bottom: 10px; 290 | } 291 | 292 | .markdown-body blockquote { 293 | margin: 0; 294 | padding: 0 1em; 295 | color: #57606a; 296 | border-left: .25em solid #d0d7de; 297 | } 298 | 299 | .markdown-body ul, 300 | .markdown-body ol { 301 | margin-top: 0; 302 | margin-bottom: 0; 303 | padding-left: 2em; 304 | } 305 | 306 | .markdown-body ol ol, 307 | .markdown-body ul ol { 308 | list-style-type: lower-roman !important; 309 | } 310 | 311 | .markdown-body ul ul ol, 312 | .markdown-body ul ol ol, 313 | .markdown-body ol ul ol, 314 | .markdown-body ol ol ol { 315 | list-style-type: lower-alpha !important; 316 | } 317 | 318 | .markdown-body dd { 319 | margin-left: 0; 320 | } 321 | 322 | .markdown-body tt, 323 | .markdown-body code { 324 | font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; 325 | font-size: 12px; 326 | } 327 | 328 | .markdown-body pre { 329 | margin-top: 0; 330 | margin-bottom: 0; 331 | font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; 332 | font-size: 12px; 333 | word-wrap: normal; 334 | } 335 | 336 | .markdown-body .octicon { 337 | display: inline-block; 338 | overflow: visible !important; 339 | vertical-align: text-bottom; 340 | fill: currentColor; 341 | } 342 | 343 | .markdown-body ::placeholder { 344 | color: #6e7781; 345 | opacity: 1; 346 | } 347 | 348 | .markdown-body input::-webkit-outer-spin-button, 349 | .markdown-body input::-webkit-inner-spin-button { 350 | margin: 0; 351 | -webkit-appearance: none; 352 | appearance: none; 353 | } 354 | 355 | .markdown-body .pl-c { 356 | color: #6e7781; 357 | } 358 | 359 | .markdown-body .pl-c1, 360 | .markdown-body .pl-s .pl-v { 361 | color: #0550ae; 362 | } 363 | 364 | .markdown-body .pl-e, 365 | .markdown-body .pl-en { 366 | color: #8250df; 367 | } 368 | 369 | .markdown-body .pl-smi, 370 | .markdown-body .pl-s .pl-s1 { 371 | color: #24292f; 372 | } 373 | 374 | .markdown-body .pl-ent { 375 | color: #116329; 376 | } 377 | 378 | .markdown-body .pl-k { 379 | color: #cf222e; 380 | } 381 | 382 | .markdown-body .pl-s, 383 | .markdown-body .pl-pds, 384 | .markdown-body .pl-s .pl-pse .pl-s1, 385 | .markdown-body .pl-sr, 386 | .markdown-body .pl-sr .pl-cce, 387 | .markdown-body .pl-sr .pl-sre, 388 | .markdown-body .pl-sr .pl-sra { 389 | color: #0a3069; 390 | } 391 | 392 | .markdown-body .pl-v, 393 | .markdown-body .pl-smw { 394 | color: #953800; 395 | } 396 | 397 | .markdown-body .pl-bu { 398 | color: #82071e; 399 | } 400 | 401 | .markdown-body .pl-ii { 402 | color: #f6f8fa; 403 | background-color: #82071e; 404 | } 405 | 406 | .markdown-body .pl-c2 { 407 | color: #f6f8fa; 408 | background-color: #cf222e; 409 | } 410 | 411 | .markdown-body .pl-sr .pl-cce { 412 | font-weight: bold; 413 | color: #116329; 414 | } 415 | 416 | .markdown-body .pl-ml { 417 | color: #3b2300; 418 | } 419 | 420 | .markdown-body .pl-mh, 421 | .markdown-body .pl-mh .pl-en, 422 | .markdown-body .pl-ms { 423 | font-weight: bold; 424 | color: #0550ae; 425 | } 426 | 427 | .markdown-body .pl-mi { 428 | font-style: italic; 429 | color: #24292f; 430 | } 431 | 432 | .markdown-body .pl-mb { 433 | font-weight: bold; 434 | color: #24292f; 435 | } 436 | 437 | .markdown-body .pl-md { 438 | color: #82071e; 439 | background-color: #FFEBE9; 440 | } 441 | 442 | .markdown-body .pl-mi1 { 443 | color: #116329; 444 | background-color: #dafbe1; 445 | } 446 | 447 | .markdown-body .pl-mc { 448 | color: #953800; 449 | background-color: #ffd8b5; 450 | } 451 | 452 | .markdown-body .pl-mi2 { 453 | color: #eaeef2; 454 | background-color: #0550ae; 455 | } 456 | 457 | .markdown-body .pl-mdr { 458 | font-weight: bold; 459 | color: #8250df; 460 | } 461 | 462 | .markdown-body .pl-ba { 463 | color: #57606a; 464 | } 465 | 466 | .markdown-body .pl-sg { 467 | color: #8c959f; 468 | } 469 | 470 | .markdown-body .pl-corl { 471 | text-decoration: underline; 472 | color: #0a3069; 473 | } 474 | 475 | .markdown-body [data-catalyst] { 476 | display: block; 477 | } 478 | 479 | .markdown-body g-emoji { 480 | font-family: "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; 481 | font-size: 1em; 482 | font-style: normal !important; 483 | font-weight: 400; 484 | line-height: 1; 485 | vertical-align: -0.075em; 486 | } 487 | 488 | .markdown-body g-emoji img { 489 | width: 1em; 490 | height: 1em; 491 | } 492 | 493 | .markdown-body::before { 494 | display: table; 495 | content: ""; 496 | } 497 | 498 | .markdown-body::after { 499 | display: table; 500 | clear: both; 501 | content: ""; 502 | } 503 | 504 | .markdown-body>*:first-child { 505 | margin-top: 0 !important; 506 | } 507 | 508 | .markdown-body>*:last-child { 509 | margin-bottom: 0 !important; 510 | } 511 | 512 | .markdown-body a:not([href]) { 513 | color: inherit; 514 | text-decoration: none; 515 | } 516 | 517 | .markdown-body .absent { 518 | color: #cf222e; 519 | } 520 | 521 | .markdown-body .anchor { 522 | float: left; 523 | padding-right: 4px; 524 | margin-left: -20px; 525 | line-height: 1; 526 | } 527 | 528 | .markdown-body .anchor:focus { 529 | outline: none; 530 | } 531 | 532 | .markdown-body p, 533 | .markdown-body blockquote, 534 | .markdown-body ul, 535 | .markdown-body ol, 536 | .markdown-body dl, 537 | .markdown-body table, 538 | .markdown-body pre, 539 | .markdown-body details { 540 | margin-top: 0; 541 | margin-bottom: 16px; 542 | } 543 | 544 | .markdown-body blockquote>:first-child { 545 | margin-top: 0; 546 | } 547 | 548 | .markdown-body blockquote>:last-child { 549 | margin-bottom: 0; 550 | } 551 | 552 | .markdown-body sup>a::before { 553 | content: "["; 554 | } 555 | 556 | .markdown-body sup>a::after { 557 | content: "]"; 558 | } 559 | 560 | .markdown-body h1 .octicon-link, 561 | .markdown-body h2 .octicon-link, 562 | .markdown-body h3 .octicon-link, 563 | .markdown-body h4 .octicon-link, 564 | .markdown-body h5 .octicon-link, 565 | .markdown-body h6 .octicon-link { 566 | color: #24292f; 567 | vertical-align: middle; 568 | visibility: hidden; 569 | } 570 | 571 | .markdown-body h1:hover .anchor, 572 | .markdown-body h2:hover .anchor, 573 | .markdown-body h3:hover .anchor, 574 | .markdown-body h4:hover .anchor, 575 | .markdown-body h5:hover .anchor, 576 | .markdown-body h6:hover .anchor { 577 | text-decoration: none; 578 | } 579 | 580 | .markdown-body h1:hover .anchor .octicon-link, 581 | .markdown-body h2:hover .anchor .octicon-link, 582 | .markdown-body h3:hover .anchor .octicon-link, 583 | .markdown-body h4:hover .anchor .octicon-link, 584 | .markdown-body h5:hover .anchor .octicon-link, 585 | .markdown-body h6:hover .anchor .octicon-link { 586 | visibility: visible; 587 | } 588 | 589 | .markdown-body h1 tt, 590 | .markdown-body h1 code, 591 | .markdown-body h2 tt, 592 | .markdown-body h2 code, 593 | .markdown-body h3 tt, 594 | .markdown-body h3 code, 595 | .markdown-body h4 tt, 596 | .markdown-body h4 code, 597 | .markdown-body h5 tt, 598 | .markdown-body h5 code, 599 | .markdown-body h6 tt, 600 | .markdown-body h6 code { 601 | padding: 0 .2em; 602 | font-size: inherit; 603 | } 604 | 605 | .markdown-body ul.no-list, 606 | .markdown-body ol.no-list { 607 | padding: 0; 608 | list-style-type: none; 609 | } 610 | 611 | .markdown-body ol[type="1"] { 612 | list-style-type: decimal; 613 | } 614 | 615 | .markdown-body ol[type=a] { 616 | list-style-type: lower-alpha; 617 | } 618 | 619 | .markdown-body ol[type=i] { 620 | list-style-type: lower-roman; 621 | } 622 | 623 | .markdown-body div>ol:not([type]) { 624 | list-style-type: decimal; 625 | } 626 | 627 | .markdown-body ul ul, 628 | .markdown-body ul ol, 629 | .markdown-body ol ol, 630 | .markdown-body ol ul { 631 | margin-top: 0; 632 | margin-bottom: 0; 633 | } 634 | 635 | .markdown-body li>p { 636 | margin-top: 16px; 637 | } 638 | 639 | .markdown-body li+li { 640 | margin-top: .25em; 641 | } 642 | 643 | .markdown-body dl { 644 | padding: 0; 645 | } 646 | 647 | .markdown-body dl dt { 648 | padding: 0; 649 | margin-top: 16px; 650 | font-size: 1em; 651 | font-style: italic; 652 | font-weight: 600; 653 | } 654 | 655 | .markdown-body dl dd { 656 | padding: 0 16px; 657 | margin-bottom: 16px; 658 | } 659 | 660 | .markdown-body table th { 661 | font-weight: 600; 662 | } 663 | 664 | .markdown-body table th, 665 | .markdown-body table td { 666 | padding: 6px 13px; 667 | border: 1px solid #d0d7de; 668 | } 669 | 670 | .markdown-body table tr { 671 | background-color: #ffffff; 672 | border-top: 1px solid hsla(210,18%,87%,1); 673 | } 674 | 675 | .markdown-body table tr:nth-child(2n) { 676 | background-color: #f6f8fa; 677 | } 678 | 679 | .markdown-body table img { 680 | background-color: transparent; 681 | } 682 | 683 | .markdown-body img[align=right] { 684 | padding-left: 20px; 685 | } 686 | 687 | .markdown-body img[align=left] { 688 | padding-right: 20px; 689 | } 690 | 691 | .markdown-body .emoji { 692 | max-width: none; 693 | vertical-align: text-top; 694 | background-color: transparent; 695 | } 696 | 697 | .markdown-body span.frame { 698 | display: block; 699 | overflow: hidden; 700 | } 701 | 702 | .markdown-body span.frame>span { 703 | display: block; 704 | float: left; 705 | width: auto; 706 | padding: 7px; 707 | margin: 13px 0 0; 708 | overflow: hidden; 709 | border: 1px solid #d0d7de; 710 | } 711 | 712 | .markdown-body span.frame span img { 713 | display: block; 714 | float: left; 715 | } 716 | 717 | .markdown-body span.frame span span { 718 | display: block; 719 | padding: 5px 0 0; 720 | clear: both; 721 | color: #24292f; 722 | } 723 | 724 | .markdown-body span.align-center { 725 | display: block; 726 | overflow: hidden; 727 | clear: both; 728 | } 729 | 730 | .markdown-body span.align-center>span { 731 | display: block; 732 | margin: 13px auto 0; 733 | overflow: hidden; 734 | text-align: center; 735 | } 736 | 737 | .markdown-body span.align-center span img { 738 | margin: 0 auto; 739 | text-align: center; 740 | } 741 | 742 | .markdown-body span.align-right { 743 | display: block; 744 | overflow: hidden; 745 | clear: both; 746 | } 747 | 748 | .markdown-body span.align-right>span { 749 | display: block; 750 | margin: 13px 0 0; 751 | overflow: hidden; 752 | text-align: right; 753 | } 754 | 755 | .markdown-body span.align-right span img { 756 | margin: 0; 757 | text-align: right; 758 | } 759 | 760 | .markdown-body span.float-left { 761 | display: block; 762 | float: left; 763 | margin-right: 13px; 764 | overflow: hidden; 765 | } 766 | 767 | .markdown-body span.float-left span { 768 | margin: 13px 0 0; 769 | } 770 | 771 | .markdown-body span.float-right { 772 | display: block; 773 | float: right; 774 | margin-left: 13px; 775 | overflow: hidden; 776 | } 777 | 778 | .markdown-body span.float-right>span { 779 | display: block; 780 | margin: 13px auto 0; 781 | overflow: hidden; 782 | text-align: right; 783 | } 784 | 785 | .markdown-body code, 786 | .markdown-body tt { 787 | padding: .2em .4em; 788 | margin: 0; 789 | font-size: 85%; 790 | background-color: rgba(175,184,193,0.2); 791 | border-radius: 6px; 792 | } 793 | 794 | .markdown-body code br, 795 | .markdown-body tt br { 796 | display: none; 797 | } 798 | 799 | .markdown-body del code { 800 | text-decoration: inherit; 801 | } 802 | 803 | .markdown-body pre code { 804 | font-size: 100%; 805 | } 806 | 807 | .markdown-body pre>code { 808 | padding: 0; 809 | margin: 0; 810 | word-break: normal; 811 | white-space: pre; 812 | background: transparent; 813 | border: 0; 814 | } 815 | 816 | .markdown-body .highlight { 817 | margin-bottom: 16px; 818 | } 819 | 820 | .markdown-body .highlight pre { 821 | margin-bottom: 0; 822 | word-break: normal; 823 | } 824 | 825 | .markdown-body .highlight pre, 826 | .markdown-body pre { 827 | padding: 16px; 828 | overflow: auto; 829 | font-size: 85%; 830 | line-height: 1.45; 831 | background-color: #f6f8fa; 832 | border-radius: 6px; 833 | } 834 | 835 | .markdown-body pre code, 836 | .markdown-body pre tt { 837 | display: inline; 838 | max-width: auto; 839 | padding: 0; 840 | margin: 0; 841 | overflow: visible; 842 | line-height: inherit; 843 | word-wrap: normal; 844 | background-color: transparent; 845 | border: 0; 846 | } 847 | 848 | .markdown-body .csv-data td, 849 | .markdown-body .csv-data th { 850 | padding: 5px; 851 | overflow: hidden; 852 | font-size: 12px; 853 | line-height: 1; 854 | text-align: left; 855 | white-space: nowrap; 856 | } 857 | 858 | .markdown-body .csv-data .blob-num { 859 | padding: 10px 8px 9px; 860 | text-align: right; 861 | background: #ffffff; 862 | border: 0; 863 | } 864 | 865 | .markdown-body .csv-data tr { 866 | border-top: 0; 867 | } 868 | 869 | .markdown-body .csv-data th { 870 | font-weight: 600; 871 | background: #f6f8fa; 872 | border-top: 0; 873 | } 874 | 875 | .markdown-body .footnotes { 876 | font-size: 12px; 877 | color: #57606a; 878 | border-top: 1px solid #d0d7de; 879 | } 880 | 881 | .markdown-body .footnotes ol { 882 | padding-left: 16px; 883 | } 884 | 885 | .markdown-body .footnotes li { 886 | position: relative; 887 | } 888 | 889 | .markdown-body ol li { 890 | list-style-type:decimal !important; 891 | } 892 | .markdown-body ul li { 893 | list-style-type: disc !important; 894 | } 895 | 896 | .markdown-body .footnotes li:target::before { 897 | position: absolute; 898 | top: -8px; 899 | right: -8px; 900 | bottom: -8px; 901 | left: -24px; 902 | pointer-events: none; 903 | content: ""; 904 | border: 2px solid #0969da; 905 | border-radius: 6px; 906 | } 907 | 908 | .markdown-body .footnotes li:target { 909 | color: #24292f; 910 | } 911 | 912 | .markdown-body .footnotes .data-footnote-backref g-emoji { 913 | font-family: monospace; 914 | } 915 | 916 | .markdown-body .task-list-item { 917 | list-style-type: none; 918 | } 919 | 920 | .markdown-body .task-list-item label { 921 | font-weight: 400; 922 | } 923 | 924 | .markdown-body .task-list-item.enabled label { 925 | cursor: pointer; 926 | } 927 | 928 | .markdown-body .task-list-item+.task-list-item { 929 | margin-top: 3px; 930 | } 931 | 932 | .markdown-body .task-list-item .handle { 933 | display: none; 934 | } 935 | 936 | .markdown-body .task-list-item-checkbox { 937 | margin: 0 .2em .25em -1.6em; 938 | vertical-align: middle; 939 | } 940 | 941 | .markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox { 942 | margin: 0 -1.6em .25em .2em; 943 | } 944 | 945 | .markdown-body ::-webkit-calendar-picker-indicator { 946 | filter: invert(50%); 947 | } -------------------------------------------------------------------------------- /src/v1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Nomie-oss Plugins 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | Nomie-oss Plugin Docs 17 |
18 |
19 | Nomie-oss app → 20 |
21 |
22 |
23 |
24 | {content} 25 |
26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/v1/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { NomiePlugin } from "./plugin-connect"; 3 | 4 | //@ts-ignore 5 | window['NomiePlugin'] = NomiePlugin -------------------------------------------------------------------------------- /src/v1/plugin-connect.ts: -------------------------------------------------------------------------------- 1 | import { Dayjs } from "dayjs"; 2 | import dayjs from "../lib/dayjs"; 3 | 4 | export type PluginUseTypes = 5 | | "trackablesSelected" 6 | | "selectTrackables" 7 | | "onUIOpened" 8 | | "openNoteEditor" 9 | | "onWidget" 10 | | "registered" 11 | | "onLaunch" 12 | | "openURL" 13 | | "openPlugin" 14 | | "searchNotes" 15 | | "searchReply" 16 | | "confirmReply" 17 | | "promptReply" 18 | | "onNote" 19 | | "createNote"; 20 | 21 | export type PluginType = { 22 | id: string; 23 | name: string; 24 | description?: string; 25 | emoji?: string; 26 | addToCaptureMenu: boolean; 27 | addToMoreMenu: boolean; 28 | url: string; 29 | version: string; 30 | active: boolean; 31 | uses: Array; 32 | error?: string; 33 | dayjs: Dayjs; 34 | testMode: boolean; 35 | }; 36 | 37 | type getTrackableUsageProps = { 38 | tag: string; 39 | date?: Date; 40 | daysBack?: number; 41 | groupByDay?: boolean; 42 | }; 43 | 44 | type UserPrefs = { 45 | use24Hour?: boolean; 46 | useMetric?: boolean; 47 | useLocation?: boolean; 48 | weekStarts: "monday" | "sunday"; 49 | theme: "dark" | "light" | "system"; 50 | }; 51 | 52 | /* It takes a plugin object as an argument, and 53 | provides a set of methods that can be used to communicate with the Nomie app */ 54 | export class NomiePlugin { 55 | pluginDetails: PluginType; 56 | registered: boolean; 57 | pid: undefined | string; 58 | lid: undefined | string; 59 | listeners: any; 60 | testMode: boolean; 61 | ready: boolean; 62 | storage: any; 63 | prefs?: UserPrefs; 64 | dayjs: Dayjs; 65 | 66 | constructor(starter: PluginType) { 67 | this.pluginDetails = { ...starter }; 68 | this.registered = false; 69 | this.pid = undefined; 70 | this.lid = undefined; 71 | this.listeners = {}; 72 | this.ready = false; 73 | this.testMode = starter.testMode ? true : false; 74 | this.storage = new NomieStorage(this, "prefs"); 75 | this.dayjs = dayjs; 76 | const fireReady = async () => { 77 | if (!this.ready) { 78 | window.addEventListener( 79 | "message", 80 | (evt) => { 81 | this.onMessage(evt); 82 | }, 83 | false 84 | ); 85 | this.ready = true; 86 | this.pid = this.getPid(); 87 | } 88 | }; 89 | 90 | fireReady(); 91 | this.register(); 92 | } 93 | 94 | /** 95 | * It returns a promise that resolves to an array of notes 96 | * @param {string} term - The search term 97 | * @param date - The date to search from. 98 | * @param [daysBack=7] - How many days back to search. 99 | * @returns A promise that resolves to an array of notes. 100 | */ 101 | searchNotes(term: string, date = new Date(), daysBack = 7) { 102 | return new Promise((resolve) => { 103 | let id = this.toId("search"); 104 | this.broadcast("searchNotes", { 105 | term, 106 | date, 107 | daysBack, 108 | id, 109 | lid: this.lid, 110 | }); 111 | this.listeners[id] = (payload: any) => { 112 | resolve(payload.results); 113 | }; 114 | }); 115 | } 116 | 117 | createStorage(fileName: string): NomieStorage { 118 | return new NomieStorage(this, fileName); 119 | } 120 | 121 | openNoteEditor(note: any) { 122 | let openNote: any; 123 | if (typeof note == "string") { 124 | openNote = { note }; 125 | } else if (typeof note == "object") { 126 | openNote = note; 127 | } 128 | if (openNote) { 129 | this.broadcast("openNoteEditor", { 130 | note: openNote, 131 | }); 132 | } 133 | } 134 | 135 | /** 136 | * It returns a promise that resolves to an array of trackables 137 | * @param {any} type - The type of trackable to select. This can be a string or an array of strings. 138 | * @param [multiple=true] - boolean - whether or not to allow multiple trackables to be selected 139 | * @returns A promise that resolves to an array of trackables. 140 | */ 141 | selectTrackables(type: any, multiple = true): Promise> { 142 | return new Promise((resolve) => { 143 | let id = this.toId("select"); 144 | this.broadcast("selectTrackables", { id, type, multiple }); 145 | this.listeners[id] = (payload: any) => { 146 | resolve(payload.selected); 147 | }; 148 | }); 149 | } 150 | 151 | /** 152 | * It returns a promise that resolves to an array of trackables. 153 | * @param {'tracker' | 'context' | 'person'} type - 'tracker' | 'context' | 'person' 154 | * @returns A promise that resolves to an array of trackables. 155 | */ 156 | async selectTrackable(type: "tracker" | "context" | "person") { 157 | let selected = await this.selectTrackables(type, false); 158 | if (selected.length) { 159 | return selected[0]; 160 | } 161 | } 162 | 163 | openTrackableEditor(trackable: any) { 164 | this.broadcast("openTrackableEditor", { trackable }); 165 | } 166 | 167 | /** 168 | * If the prefs object is not null, return the value of the use24Hour property 169 | * @returns The value of the use24Hour property of the prefs object. 170 | */ 171 | get is24Hour() { 172 | return this.prefs?.use24Hour; 173 | } 174 | 175 | /** 176 | * If the prefs object is not null, return the value of the useMetric property 177 | * @returns A boolean value that is the value of the useMetric property of the prefs object. 178 | */ 179 | get isMetric() { 180 | return this.prefs?.useMetric; 181 | } 182 | 183 | /** 184 | * If the log is a string, then broadcast a createNote event with a note property set to the log. 185 | * Otherwise, broadcast a createNote event with the log as the payload 186 | * @param {any} log - any - This is the log that will be sent to the client. 187 | */ 188 | createNote(log: any) { 189 | setTimeout(() => { 190 | if (typeof log === "string") { 191 | this.broadcast("createNote", { note: log }); 192 | } else { 193 | this.broadcast("createNote", log); 194 | } 195 | }, 300); 196 | } 197 | 198 | openURL(url: string, title: string) { 199 | this.broadcast("openURL", { 200 | url, 201 | title, 202 | }); 203 | } 204 | 205 | _fireListeners(key: string, payload: any) { 206 | (this.listeners[key] || []).forEach((func: Function) => { 207 | func(payload); 208 | }); 209 | } 210 | 211 | /** 212 | * It listens for messages from the native app, and then fires the appropriate listener function 213 | * @param {any} event - any 214 | */ 215 | onMessage(event: any) { 216 | const action = event.data.action; 217 | const payload = event.data.data; 218 | switch (action) { 219 | case "registered": 220 | this.registered = true; 221 | this.pid = payload.id; 222 | this.lid = payload.lid; 223 | this.prefs = { ...payload.user }; 224 | this._fireListeners("onRegistered", this); 225 | break; 226 | case "onUIOpened": 227 | this._fireListeners("onUIOpened", this); 228 | break; 229 | case "onWidget": 230 | this._fireListeners("onWidget", this); 231 | break; 232 | case "onNote": 233 | this._fireListeners("onNote", payload); 234 | break; 235 | case "onLaunch": 236 | setTimeout(() => { 237 | this._fireListeners("onLaunch", payload); 238 | }, 400); 239 | break; 240 | case "promptReply": 241 | this.listenerResponse(payload); 242 | break; 243 | case "alertReply": 244 | this.listenerResponse(payload); 245 | break; 246 | case "getTrackableReply": 247 | this.listenerResponse(payload); 248 | break; 249 | case "getTrackableUsageReply": 250 | this.listenerResponse(payload); 251 | break; 252 | case "searchReply": 253 | this.listenerResponse(payload); 254 | break; 255 | case "locationReply": 256 | this.listenerResponse(payload); 257 | break; 258 | case "trackablesSelected": 259 | this.listenerResponse(payload); 260 | break; 261 | case "confirmReply": 262 | this.listenerResponse(payload); 263 | break; 264 | case "setStorageItemReply": 265 | this.listenerResponse(payload); 266 | break; 267 | case "getStorageItemReply": 268 | this.listenerResponse(payload); 269 | break; 270 | } 271 | } 272 | 273 | /** 274 | * It takes a string and returns a string 275 | * @param {string} type - The type of the element. 276 | * @returns A string that is a combination of the type and a random number. 277 | */ 278 | private toId(type: string): string { 279 | return `${type.replace(/[^a-z0-9]/gi, "")}-${Math.random().toString(16)}`; 280 | } 281 | 282 | /** 283 | * It adds a listener to the `listeners` object, which is a property of the `WebSocketService` class 284 | * @param {string} id - The id of the request. 285 | * @param {any} resolver - The function that will be called when the response is received. 286 | */ 287 | private addResponseListener(id: string, resolver: any) { 288 | this.listeners[id] = (pload: any) => { 289 | resolver(pload); 290 | }; 291 | } 292 | 293 | /** 294 | * It returns a promise that resolves to the location data when the location data is available 295 | * @returns A promise that resolves to the location data. 296 | */ 297 | getLocation(): Promise { 298 | return new Promise((resolve, reject) => { 299 | let id = this.toId("location"); 300 | this.broadcast("getLocation", { id }); 301 | this.addResponseListener(id, resolve); 302 | }); 303 | } 304 | 305 | /** 306 | * It returns a promise that resolves to a trackable object. 307 | * @param {string} tag - The tag of the trackable you want to get. 308 | * @returns A promise that resolves to the trackable object. 309 | */ 310 | getTrackable(tag: string) { 311 | return new Promise((resolve) => { 312 | let id = this.toId(`trackable-${tag}`); 313 | const payload = { 314 | id, 315 | tag, 316 | }; 317 | this.broadcast("getTrackable", payload); 318 | this.addResponseListener(id, resolve); 319 | }); 320 | } 321 | 322 | /** 323 | * "Get the value of a trackable input with the given tag." 324 | * 325 | * The first thing we do is create a promise. This is a promise that will be resolved with the value 326 | * of the trackable input 327 | * @param {string} tag - The tag of the trackable input you want to get the value of. 328 | * @returns A promise that resolves to the value of the trackable input. 329 | */ 330 | getTrackableInput(tag: string) { 331 | return new Promise((resolve) => { 332 | let id = this.toId(`trackable-value-${tag}`); 333 | this.broadcast("getTrackableInput", { 334 | id, 335 | tag, 336 | }); 337 | this.addResponseListener(id, resolve); 338 | }); 339 | } 340 | 341 | /** 342 | * `getTrackableUsage` is a function that returns a promise that resolves to an array of objects. 343 | * Each object has a `date` property and a `count` property. The `date` property is a date object. 344 | * The `count` property is a number 345 | * @param {getTrackableUsageProps} props - { 346 | * @returns A promise that resolves to the usage data. 347 | */ 348 | getTrackableUsage(props: getTrackableUsageProps) { 349 | return new Promise((resolve) => { 350 | let id = this.toId(`usage-${props.tag}`); 351 | const payload = { 352 | id, 353 | tag: props.tag, 354 | date: props.date || new Date(), 355 | daysBack: props.daysBack, 356 | groupByDay: props.groupByDay, 357 | }; 358 | this.broadcast("getTrackableUsage", payload); 359 | this.addResponseListener(id, resolve); 360 | }); 361 | } 362 | 363 | /** 364 | * The function returns a promise that resolves to the value of the input field 365 | * @param {string} title - The title of the prompt 366 | * @param {string} [message] - The message to display in the prompt. 367 | * @param {string} [type] - string - The type of prompt. This can be 'confirm' or 'prompt'. 368 | * @returns A promise. 369 | */ 370 | prompt(title: string, message?: string, type?: string) { 371 | return new Promise((resolve, reject) => { 372 | if (this.pluginDetails.testMode) { 373 | let res = window.prompt([title, message].filter((s) => s).join(" - ")); 374 | if (res) { 375 | resolve({ value: res }); 376 | } 377 | } else { 378 | let id = this.toId("prompt"); 379 | this.broadcast("prompt", { 380 | title, 381 | message, 382 | type, 383 | id, 384 | }); 385 | // this.addResponseListener(id, resolve) 386 | this.addResponseListener(id, resolve); 387 | } 388 | }); 389 | } 390 | 391 | alert(title: string, message?: string) { 392 | return new Promise((resolve, reject) => { 393 | let id = this.toId("prompt"); 394 | this.broadcast("alert", { 395 | title, 396 | message, 397 | id, 398 | }); 399 | // this.addResponseListener(id, resolve) 400 | this.addResponseListener(id, resolve); 401 | }); 402 | } 403 | 404 | openTemplateURL(url: string) { 405 | this.broadcast("openTemplateURL", { 406 | url, 407 | }); 408 | } 409 | 410 | /** 411 | * The function returns a promise that resolves when the user clicks the confirm button 412 | * @param {string} title - The title of the modal 413 | * @param {string} [message] - The message to be displayed in the dialog. 414 | * @returns A promise. 415 | */ 416 | confirm(title: string, message?: string) { 417 | return new Promise((resolve, reject) => { 418 | let id = this.toId("confirm"); 419 | this.broadcast("confirm", { 420 | title, 421 | message, 422 | id, 423 | }); 424 | this.addResponseListener(id, resolve); 425 | }); 426 | } 427 | 428 | /** 429 | * If the listener exists, resolve the promise with the payload, and delete the listener 430 | * @param {any} payload - any 431 | */ 432 | listenerResponse(payload: any) { 433 | if (this.listeners && this.listeners[payload.id]) { 434 | const resolve = this.listeners[payload.id]; 435 | if (resolve) { 436 | resolve(payload); 437 | delete this.listeners[payload.id]; 438 | } else { 439 | console.error(`No resolve found for prompts[${payload.id}]`); 440 | } 441 | } 442 | } 443 | 444 | /** 445 | * It broadcasts a message to the main process, and then waits for a response 446 | * @param {string} key - The key to get the value of 447 | * @returns A promise that resolves to the value of the key in the storage. 448 | */ 449 | getStorageItem(key: string): Promise { 450 | return new Promise((resolve, reject) => { 451 | let id = this.toId(`storage-get-${key}`); 452 | this.broadcast("getStorageItem", { 453 | key, 454 | id, 455 | lid: this.lid, 456 | }); 457 | this.addResponseListener(id, resolve); 458 | }); 459 | } 460 | 461 | /** 462 | * It broadcasts a message to the main process, and then waits for a response from the main process 463 | * @param {string} key - The key to store the value under 464 | * @param {any} value - any - The value to set the storage item to. 465 | * @returns A promise that resolves to the value of the key in storage. 466 | */ 467 | setStorageItem(key: string, value: any) { 468 | return new Promise((resolve, reject) => { 469 | let id = this.toId(`storage-set-${key}`); 470 | this.broadcast("setStorageItem", { 471 | key, 472 | value, 473 | id, 474 | }); 475 | this.addResponseListener(id, resolve); 476 | }); 477 | } 478 | 479 | /** 480 | * It takes an action and a payload, and sends a message to the parent window with the action and 481 | * payload 482 | * @param {string} action - The action to be performed. 483 | * @param {any} payload - any - This is the data that you want to send to the parent window. 484 | */ 485 | broadcast(action: string, payload: any) { 486 | const pid = this.getPid(); 487 | if (window.parent) { 488 | const message = { 489 | action, 490 | data: { ...payload, ...{ pid, lid: this.lid } }, 491 | }; 492 | if (!this.lid && action !== "register") { 493 | console.error(`No LID for ${action}`, payload); 494 | } 495 | window.parent.postMessage(message, "*"); 496 | } 497 | } 498 | 499 | /** 500 | * It returns the value of the pid query parameter in the URL, or undefined if it doesn't exist 501 | * @returns The pid of the current page. 502 | */ 503 | getPid(): string | undefined { 504 | return new URL(window.location.href).searchParams.get("pid") || undefined; 505 | } 506 | 507 | /** 508 | * It adds a listener to the event 'onUIOpened' 509 | * @param {Function} func - Function - The function to be called when the event is triggered. 510 | */ 511 | onUIOpened(func: Function) { 512 | this.on("onUIOpened", func); 513 | return this.off("onUIOpened", func); 514 | } 515 | 516 | /** 517 | * A function that takes a function as an argument and returns a function. 518 | * @param {Function} func - Function - The function to be called when the event is triggered. 519 | * @returns A function that removes the event listener. 520 | */ 521 | onWidget(func: Function) { 522 | this.on("onWidget", func); 523 | return this.off("onWidget", func); 524 | } 525 | 526 | /** 527 | * The function takes a function as an argument and calls the on function with the event name and the 528 | * function as arguments 529 | * @param {Function} func - Function - The function to be called when the event is triggered. 530 | */ 531 | onNote(func: Function) { 532 | this.on("onNote", func); 533 | return this.off("onNote", func); 534 | } 535 | 536 | /** 537 | * The function takes a function as an argument and calls the on function with the event name 538 | * onLaunch and the function as the argument 539 | * @param {Function} func - Function 540 | */ 541 | onLaunch(func: Function) { 542 | this.on("onLaunch", func); 543 | return this.off("onLaunch", func); 544 | } 545 | 546 | /** 547 | * The function takes a function as an argument and calls the on function with the event name and the 548 | * function as arguments 549 | * @param {Function} func - Function - The function to be called when the event is triggered. 550 | */ 551 | onRegistered(func: Function) { 552 | this.on("onRegistered", func); 553 | return this.off("onRegistered", func); 554 | } 555 | 556 | /** 557 | * It takes an event name and a function as arguments, and adds the function to the array of 558 | * listeners for that event name 559 | * @param {string} eventName - The name of the event you want to listen for. 560 | * @param {Function} func - The function to be called when the event is triggered. 561 | */ 562 | on(eventName: string, func: Function) { 563 | this.listeners[eventName] = this.listeners[eventName] || []; 564 | if (!this.listeners[eventName].includes(func)) { 565 | this.listeners[eventName].push(func); 566 | } 567 | } 568 | 569 | /** 570 | * It removes a function from the array of functions that are called when an event is triggered 571 | * @param {string} name - The name of the event. 572 | * @param {Function} func - The function to be called when the event is triggered. 573 | */ 574 | off(name: string, func: Function) { 575 | return () => { 576 | if (this.listeners[name]) { 577 | this.listeners[name] = this.listeners[name].filter((d: Function) => { 578 | return d !== func; 579 | }); 580 | } 581 | }; 582 | } 583 | 584 | /** 585 | * If the plugin hasn't been registered, then broadcast the plugin details to the parent window 586 | */ 587 | register() { 588 | if (!this.registered) { 589 | let pluginDetails = { ...this.pluginDetails }; 590 | this.broadcast("register", pluginDetails); 591 | this.registered = true; 592 | } 593 | } 594 | } 595 | 596 | /* It's a wrapper for the storage plugin that makes it easier to work with */ 597 | export class NomieStorage { 598 | plugin: NomiePlugin; 599 | data: any = {}; 600 | filename: string; 601 | 602 | constructor(plugin: NomiePlugin, filename: string = "prefs") { 603 | this.plugin = plugin; 604 | this.filename = filename; 605 | this.data = {}; 606 | } 607 | 608 | /** 609 | * > The `init` function loads the data from the storage plugin and sets the `data` property 610 | * @returns The NomieStorage object 611 | */ 612 | async init(): Promise { 613 | let raw = undefined; 614 | if (this.plugin.testMode) { 615 | raw = localStorage.getItem(`testMode-storage-${this.filename}`) || "{}"; 616 | raw = { value: JSON.parse(raw) }; 617 | } else { 618 | raw = await this.plugin.getStorageItem(this.filename); 619 | } 620 | if (raw && raw.value) this.data = raw.value; 621 | return this; 622 | } 623 | 624 | /** 625 | * It returns the value of the key in the data object 626 | * @param {string} key - The key of the item to get. 627 | * @returns The value of the key in the data object. 628 | */ 629 | getItem(key: string): any { 630 | return this.data[key]; 631 | } 632 | 633 | /** 634 | * It sets the value of the key in the data object, and then saves the data object to the file 635 | * @param {string} key - The key of the item to set. 636 | * @param {any} value - any 637 | * @returns The promise that is returned from the save() method. 638 | */ 639 | async setItem(key: string, value: any) { 640 | this.data[key] = value; 641 | return await this.save(); 642 | } 643 | /** 644 | * It saves the data to the file 645 | * @returns The promise from the plugin. 646 | */ 647 | private async save() { 648 | if (this.plugin.testMode) { 649 | localStorage.setItem( 650 | `testMode-storage-${this.filename}`, 651 | JSON.stringify(this.data) 652 | ); 653 | } else { 654 | return this.plugin.setStorageItem(this.filename, this.data); 655 | } 656 | } 657 | } 658 | -------------------------------------------------------------------------------- /src/v1/plugins/meditate/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Nomie Meditate 8 | 9 | 10 | 11 | 12 | 13 | 16 |
17 |
21 | {{error}} 22 |
23 | 24 | 32 | 43 | 44 | 94 | 95 | 96 |
97 |
98 |

101 | {{completedVideo.name}} Complete 102 |

103 | 108 | 115 | 122 |
123 | or 124 | 130 |
131 |
132 |
133 | 134 | 135 |
139 |
140 |
141 | 147 | 154 | 162 | 169 | 176 |
177 |
178 |
182 |
183 |
184 |
185 | 🎥 Video must play to the end to unlock "Track in Nomie".
186 | 187 | 193 |
194 |
195 | 196 | 197 |
201 | 233 |
234 | 235 |
236 | 243 | 250 |
251 |
252 | 253 | 254 | 259 | 265 | 266 | 267 | -------------------------------------------------------------------------------- /src/v1/plugins/meditate/meditate.js: -------------------------------------------------------------------------------- 1 | /* This is the plugin object. It's a wrapper around the Nomie Plugin API. */ 2 | const plugin = new NomiePlugin({ 3 | name: "Meditate", 4 | addToCaptureMenu: true, 5 | addToMoreMenu: true, 6 | emoji: "🧘🏽‍♀️", 7 | version: "1.1", 8 | description: "Follow Guided Meditations", 9 | uses: ['createNote'], 10 | }); 11 | 12 | let uservideos = [] 13 | let videos = [ 14 | 15 | { 16 | name: '2-Min Morning Meditation', 17 | duration: '00:02:00', 18 | description: 'Two minute meditation to start your day', 19 | note: "#meditation(00:02:00) with @MindfulBreaks", 20 | youtubeId: '41DEKf4KWdU', 21 | }, 22 | { 23 | name: '2-Min Calming Meditation', 24 | duration: '00:02:00', 25 | description: 'Two minute meditation to start your day', 26 | note: "#meditation(00:02:00) calming with @TheSchoolOfLife", 27 | youtubeId: 'Z4rRjGhN-gs', 28 | }, 29 | { 30 | name: '5-Min Morning Meditation', 31 | duration: '00:05:00', 32 | description: 'uplifting and refreshing combination of breathwork and affirmations.', 33 | note: "#meditation(00:05:00) morning with @GreatMeditations", 34 | youtubeId: 'YD5W5eZy90c', 35 | }, 36 | { 37 | name: '5-Min Meditation', 38 | duration: '00:05:00', 39 | description: 'A quick five minute meditation with @Goodful', 40 | note: "Quick #meditation(00:05:00) with @Goodful", 41 | youtubeId: 'inpok4MKVLM', 42 | }, 43 | { 44 | name: '10-Min Energy Cleanse Meditation', 45 | duration: '00:10:00', 46 | description: `Clear Your System of Any Stress & Anxiety with Great Meditations`, 47 | note: "#meditation(00:10:00) cleanse meditation with @GreatMeditation", 48 | youtubeId: 'X4WjbW6amQw', 49 | }, 50 | { 51 | name: '10-Min Positive Energy Meditation', 52 | duration: '00:10:00', 53 | description: `Guided meditation for positive energy, relaxation, and peace`, 54 | note: "#meditation(00:10:00) positive energy meditation with @Lavendaire", 55 | youtubeId: '86m4RC_ADEY', 56 | }, 57 | { 58 | name: '10-Min Letting Go Meditation', 59 | duration: '00:10:00', 60 | description: `Focus on letting go with this guided meditation by Great Meditations`, 61 | note: "#meditation(00:10:00) letting go meditation with @GreatMeditation", 62 | youtubeId: 'KJFB0Re8SMc', 63 | }, 64 | { 65 | name: '10-Min Sleep Meditation', 66 | duration: '00:10:00', 67 | description: `10 minute meditation that's good before you go to bed.`, 68 | note: "#meditation(00:10:00) sleep meditation with @Goodful", 69 | youtubeId: 'aEqlQvczMJQ', 70 | }, 71 | { 72 | name: '15-Min Self Love Meditation', 73 | duration: '00:15:00', 74 | description: `Close your eyes and release all the negative thoughts that you have been holding on to. It's time from some self-love`, 75 | note: "#meditation(00:15:00) self love meditation with @Goodful", 76 | youtubeId: 'itZMM5gCboo', 77 | }, 78 | { 79 | name: '30-Min Vipassana Advanced Sit', 80 | duration: '00:30:00', 81 | description: `Less guidance. More silence. Just the important bits. `, 82 | note: "#meditation(00:30:00) advanced meditation with @AreYouZen", 83 | youtubeId: '8g2iNQIcbLY', 84 | } 85 | 86 | ] 87 | 88 | /** 89 | * Vue 2.0 App 90 | */ 91 | new Vue({ 92 | data: () => ({ 93 | error: undefined, 94 | mode: 'hidden', 95 | loading: true, 96 | activeVideo: undefined, 97 | completedVideo: undefined, 98 | addMeditation: undefined, 99 | videos: videos, 100 | uservideos: uservideos, 101 | status: '', 102 | recording: false, 103 | inNomie: true, 104 | videoState: "", 105 | favorites: [], 106 | trackable: undefined, 107 | new_name: '', 108 | new_duration: '', 109 | new_description: '', 110 | new_note: '', 111 | new_youtubeid: '', 112 | }), 113 | computed: { 114 | 115 | }, 116 | async mounted() { 117 | /** 118 | * on UI Opened 119 | * Gets fired when the user opens the plugin modal 120 | */ 121 | plugin.onUIOpened(async () => { 122 | this.mode = 'modal'; 123 | }); 124 | 125 | plugin.onRegistered(async () => { 126 | this.inNomie = true; 127 | this.loading = false; 128 | await plugin.storage.init() 129 | this.favorites = plugin.storage.getItem('favorites') || []; 130 | this.uservideos = plugin.storage.getItem('uservideos') || [] 131 | this.trackable = await plugin.getTrackable('#meditation'); 132 | this.videos = this.uservideos.concat(videos); 133 | 134 | 135 | }) 136 | 137 | setTimeout(() => { 138 | if (this.loading) { 139 | this.inNomie = false; 140 | } 141 | }, 700); 142 | 143 | }, 144 | computed: { 145 | insideNomie() { 146 | return this.inNomie 147 | } 148 | }, 149 | methods: { 150 | toggleFavorite(videoId, evt) { 151 | if (evt) { 152 | evt.preventDefault(); 153 | evt.stopPropagation(); 154 | } 155 | 156 | if (this.favorites.includes(videoId)) { 157 | this.favorites = this.favorites.filter(f => { 158 | return f !== videoId 159 | }) 160 | } else { 161 | this.favorites.push(videoId); 162 | } 163 | plugin.storage.setItem('favorites', this.favorites); 164 | }, 165 | installTrackable() { 166 | const trackable = { 167 | type: 'tracker', 168 | tag: '#meditation', 169 | tracker: { 170 | id: 'meditation', 171 | type: 'timer', 172 | label: 'Meditation', 173 | emoji: '🧘🏽‍♀️' 174 | } 175 | } 176 | plugin.openTrackableEditor(trackable); 177 | }, 178 | cancelActive() { 179 | this.activeVideo = undefined; 180 | this.videoState = ""; 181 | this.completedVideo = undefined; 182 | }, 183 | async deleteActive(videoId, evt) { 184 | var confirm = await plugin.confirm('Delete this video?', 'Do you really want to delete this video?'); 185 | if (confirm.value){ 186 | let newlist = this.uservideos.filter( el => el.youtubeId !== videoId ); 187 | this.uservideos = newlist; 188 | plugin.storage.setItem("uservideos",this.uservideos); 189 | this.videos = this.uservideos.concat(videos); 190 | this.activeVideo = undefined; 191 | this.videoState = ""; 192 | this.completedVideo = undefined; 193 | }}, 194 | addRecord() { 195 | this.addMeditation = true; 196 | this.new_note = "#meditation(00:10:00)" 197 | }, 198 | cancelAddRecord() { 199 | this.addMeditation = false; 200 | }, 201 | saveAddRecord() { 202 | //validate input 203 | var feedback = ""; 204 | if (this.new_name =="") {feedback = "-Name missing"} 205 | if (this.new_youtubeid =="") {feedback = feedback + "\n-Youtube Id is missing"} 206 | if (this.new_note =="") {feedback = feedback + "\n-Nomie Note is missing"} 207 | var isValid = /^([0-1]?[0-9]|2[0-4]):([0-5][0-9])(:[0-5][0-9])?$/.test(this.new_duration); 208 | if (!isValid) {feedback = feedback + "\n-Duration is not in correct format"} 209 | if (feedback !="") { 210 | plugin.alert('Input Not Valid', feedback); 211 | } 212 | else { 213 | this.uservideos.push( 214 | { 215 | name: this.new_name, 216 | duration: this.new_duration, 217 | description: this.new_description, 218 | note: this.new_note, 219 | youtubeId: this.new_youtubeid, 220 | userDefined: true, 221 | } 222 | ) 223 | plugin.storage.setItem("uservideos",this.uservideos) 224 | this.videos = this.uservideos.concat(videos); 225 | this.addMeditation = false; 226 | } 227 | }, 228 | clear(prompt) { 229 | let proceed = prompt ? confirm('Clear completed video?') : true; 230 | if (proceed) { 231 | this.completedVideo = undefined; 232 | } 233 | }, 234 | async recordCompleted() { 235 | this.recording = true; 236 | if (this.inNomie && this.completedVideo) { 237 | const note = this.completedVideo.note; 238 | plugin.createNote({ 239 | note: `${note} +yt_${this.completedVideo.youtubeId} +care`, 240 | score: 3 241 | }); 242 | }; 243 | const videoId = this.completedVideo.youtubeId; 244 | const updatedVideos = this.videos.map((vid) => { 245 | if (vid.youtubeId == videoId) { 246 | vid.watched = true; 247 | console.log("Adding Watched!!", vid); 248 | } 249 | return vid; 250 | }) 251 | this.videos = updatedVideos; 252 | setTimeout(() => { 253 | this.clear(); 254 | this.recording = false; 255 | }, 1000) 256 | 257 | }, 258 | markAsWatched() { 259 | this.videoState = ""; 260 | // The video is complete 261 | this.completedVideo = { ...this.activeVideo }; 262 | this.activeVideo = undefined; 263 | }, 264 | selectVideo(video) { 265 | this.clear(); 266 | this.activeVideo = video; 267 | setTimeout(() => { 268 | new YT.Player('frame-holder', { 269 | height: `${window.document.body.scrollHeight - 200}px`, 270 | width: '100%', 271 | autoPlay: true, 272 | videoId: video.youtubeId, 273 | playerVars: { 274 | 'playsinline': 1 275 | }, 276 | events: { 277 | 'onReady': (evt) => { 278 | console.log("Video Ready to Play", evt) 279 | }, 280 | 'onStateChange': (evt) => { 281 | if (evt.data == 1) { 282 | this.videoState = "playing"; 283 | } else if (evt.data == 2) { 284 | this.videoState = "paused"; 285 | } else if (evt.data == 0) { 286 | this.markAsWatched() 287 | } 288 | } 289 | } 290 | }); 291 | 292 | 293 | }, 300); 294 | 295 | } 296 | }, 297 | 298 | }).$mount("#content"); 299 | -------------------------------------------------------------------------------- /src/v1/plugins/memories/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Nomie Weather Plugin 8 | 9 | 10 | 11 | 12 | 13 |
14 |
18 | {{error}} 19 |
20 |
24 | Looking for Memories on this day... 25 |
26 | {{status}} 27 |
28 |
29 |
33 | No Memories found on this day. 34 |
35 | 36 |
37 |

40 |
{{yearPayload.date.fromNow()}}
41 |

42 | {{yearPayload.date.format('dddd Do MMM YYYY')}} 43 |

44 |

45 |
46 |
47 |
50 | 51 | 🙂 52 | 🥳 53 | 🙁 54 | 😡 55 | 56 | 57 | 58 | {{plugin.dayjs(entry.end).format(`${plugin.prefs.use24hour ? 59 | 'H:mm' : 'h:mm a'}`)}} 60 | 61 |
62 |
{{entry.location}}
63 |
64 |
67 |

{{entry.note}}

68 |
69 |
70 |
71 |
72 |
73 | 74 | 75 | 80 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/v1/plugins/memories/memories.js: -------------------------------------------------------------------------------- 1 | /* This is the plugin object. It's a wrapper around the Nomie Plugin API. */ 2 | const plugin = new NomiePlugin({ 3 | name: "Memories", 4 | addToCaptureMenu: true, 5 | addToMoreMenu: false, 6 | addToWidgets: false, 7 | emoji: "📆", 8 | version: "1.0", 9 | description: "Multi-year Nomie users can quickly see what happened on this day in your Nomie history", 10 | uses: [ 11 | "searchNotes", 12 | ], 13 | }); 14 | 15 | 16 | /** 17 | * Vue 2.0 App 18 | */ 19 | new Vue({ 20 | data: () => ({ 21 | error: undefined, 22 | years: [], 23 | now: plugin.dayjs(), 24 | mode: 'hidden', 25 | loading: true, 26 | status: '', 27 | inNomie: false 28 | }), 29 | computed: { 30 | yearsArray() { 31 | const arrayMap = Object.keys(this.years).map((year)=>{ 32 | return this.years[year]; 33 | }).sort((a,b)=>{ 34 | return a.year < b.year ? 1 : -1 35 | }) 36 | return arrayMap 37 | } 38 | }, 39 | async mounted() { 40 | /** 41 | * on UI Opened 42 | * Gets fired when the user opens the plugin modal 43 | */ 44 | plugin.onUIOpened(async () => { 45 | this.mode = 'modal'; 46 | this.loadMemories(); 47 | }); 48 | 49 | plugin.onWidget(async () => { 50 | this.mode = 'widget'; 51 | this.loadMemories(); 52 | }); 53 | 54 | plugin.onRegistered(()=>{ 55 | this.inNomie = true; 56 | }) 57 | 58 | // Lets wait 6 seconds and if we're not in nomie, 59 | setTimeout(()=>{ 60 | if(!this.inNomie) this.error = 'Sorry, Memories will only inside of Nomie.app'; 61 | },4000); 62 | 63 | }, 64 | methods: { 65 | edit(note) { 66 | plugin.openNoteEditor(note); 67 | }, 68 | async loadMemories() { 69 | this.loading = true; 70 | this.status = 'Looking...'; 71 | let maxEmpty = 3; 72 | let empties = 0; 73 | let maxYear = 2014; 74 | const endDate = this.now.subtract(1,'year'); 75 | let endYear = parseInt(endDate.format('YYYY')); 76 | let years = {}; 77 | 78 | // Loop over the years 79 | for(let i=0;in.hasNote); 95 | // If we have some add them to year map 96 | if(justNotes.length) { 97 | let year = date.format('YYYY'); 98 | years[year] = years[year] || { 99 | date, 100 | year, 101 | notes: [] 102 | } 103 | years[date.format('YYYY')].notes = notes.filter(n=>n.hasNote); 104 | } 105 | } 106 | } 107 | } 108 | 109 | // Order years by newest first 110 | this.years = Object.keys(years).map((year)=>{ 111 | return years[year]; 112 | }).sort((a,b)=>{ 113 | return a.year < b.year ? 1 : -1 114 | }) 115 | 116 | // Clear Status 117 | this.status = ''; 118 | this.loading = false; 119 | }, 120 | async getNotes(date) { 121 | const notes = await plugin.searchNotes('', date.toDate(), 0); 122 | return notes; 123 | } 124 | }, 125 | 126 | }).$mount("#content"); 127 | -------------------------------------------------------------------------------- /src/v1/plugins/my-people/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | My People 8 | 9 | 10 | 11 | 12 | 13 | 14 | 22 | 23 | 24 |
25 | 26 | 27 |
28 |
32 |
33 |
34 | 39 |
40 |

41 | {{activePerson.name}} 42 |

43 |

47 | {{activePerson.age}} years old 48 |

49 |
50 |
51 |
52 |
53 | 59 | 65 | 71 |
72 |
73 |
74 | 75 | 76 | 77 |
81 |
    82 |
  • 83 |
    Stay In Touch
    84 | 98 |
  • 99 |
    100 |
  • 101 |
    Last Interaction
    102 | 103 |

    104 | {{dayjs(activePerson.latest).fromNow()}} 105 |

    106 |

    Unknown

    107 |
  • 108 |
109 | 110 |
    111 |
  • 112 |
    113 |
    Birthday
    114 |
    115 | 116 |
    117 | {{activePerson.birthdayFormatted}} 118 |
    119 | 120 |
    124 | Today 🎉 125 |
    126 |
    127 | In {{activePerson.birthdayFromNow}} days 128 |
    129 | 136 |
  • 137 |
138 | 139 | 140 |
141 |

About

142 |

143 | {{activePerson.notes}} 144 |

145 |
146 | 147 | 148 | 184 | 185 |
186 | 193 |
194 | 201 |
202 | 209 |
210 |
211 | 212 |
217 |
218 |
219 | 226 |
227 | 228 | 234 |
235 | 241 | 249 | 252 |
253 |
254 |
255 | 256 |
261 |
262 |
266 |

267 | No recent notes 268 |

269 |
270 |
271 | 272 |
276 |

277 | 278 | {{dayjs(note.end).format('ddd Do MMM YYYY')}} 279 | 280 | 281 | {{dayjs(note.end).fromNow()}} 282 | 283 | {{note.location}} 286 |

287 |

{{note.note}}

288 |
289 |
290 | 291 |
294 | 300 |
301 |
302 |
303 |
304 | 305 | 306 | 307 |
308 |
309 |
310 | 315 |

{{editingPerson.name}}

316 |
317 |
318 |
319 | 320 | 326 |
327 | 328 |
329 | 330 | 336 |
337 | 338 |
339 | 340 | 353 |
354 | 355 |
356 | 357 | 362 |
363 | 364 |
365 | 366 | 372 |
373 | 374 |
375 | 376 | 382 |
383 | 384 |
385 | 386 | 392 |
393 | 394 |
395 |
399 | 402 | 406 |
407 |
408 |
409 |
410 | 416 | 422 |
423 |
424 |
425 |
426 | 427 |
428 |
429 |
430 | 🎉 431 |

432 | Today's Birthdays 433 |

434 |
435 | 436 |
437 |
438 | 442 |
443 |
444 | 445 |
449 |
450 |

451 | 👍 You're all caught up! 452 |

453 |

454 | Tap a person and select their "Stay in Touch" frequency. This will 455 | help you remember when to reach out to your friends. 456 |

457 |
458 |
459 | 460 |
464 |
465 |

No people found

466 |

467 | My People will automatically pull in your people over the last 30 468 | days (when you track them in Nomie using the @person tag). You can 469 | also manually import a user by tapping the "+" below. them. 470 |

471 |
472 |
473 | 474 |
475 |
476 | ⚠️ 477 |

480 | Needs Attention 481 |

482 |
483 | 484 |
485 |
486 | 490 |
491 |
492 | 493 |
494 |
495 |

496 | My People 497 |

498 | 499 | Sort 500 | 507 |
508 |

512 | 520 | 524 | 525 | 526 | Loading People... 527 |

528 |
529 |
530 | 534 |
535 |
536 |
537 |
538 |

My People

539 | 545 | 548 |
549 |
550 | 551 | 552 | 557 | 563 | 564 | 565 | -------------------------------------------------------------------------------- /src/v1/plugins/my-people/my-people.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | /* This is the plugin object. It's a wrapper around the Nomie Plugin API. */ 4 | const plugin = new NomiePlugin({ 5 | name: "My People", 6 | addToCaptureMenu: true, 7 | addToMoreMenu: true, 8 | emoji: "👨‍👩‍👧‍👦", 9 | version: "0.5", 10 | description: "Keep up with the people that matter the most.", 11 | uses: ['createNote', 'selectTrackables', 'searchNotes'], 12 | }); 13 | 14 | 15 | const wait = (ms) => { 16 | return new Promise((resolve) => { 17 | setTimeout(() => { 18 | resolve() 19 | }, ms) 20 | }) 21 | } 22 | 23 | const strToTagSafe = (str) => { 24 | return str 25 | .replace(/(\@|\+\#)/gi, '') 26 | .trim() 27 | .replace(/[^A-Z0-9]/gi, '_') 28 | .toLowerCase() 29 | } 30 | 31 | 32 | 33 | const AvatarComponent = Vue.component('person-item', { 34 | props: ['avatar', 'name', 'size'], 35 | methods: { 36 | 37 | }, 38 | template: ` 39 |
41 |
45 | {{(name || "").replace('@','').substring(0,2)}} 46 |
47 |
48 | ` 49 | }) 50 | 51 | /* It's a Vue component that is used to render a person. */ 52 | const PersonItem = Vue.component('person-item', { 53 | props: ['person'], 54 | methods: { 55 | dayjs(date) { 56 | return plugin.dayjs(date); 57 | } 58 | }, 59 | components: { 60 | 'avatar': AvatarComponent 61 | }, 62 | template: ` 63 | 80 | ` 81 | }) 82 | 83 | /** 84 | * Vue 2.0 App 85 | */ 86 | new Vue({ 87 | data: () => ({ 88 | error: undefined, 89 | mode: 'hidden', 90 | loading: true, 91 | activePerson: undefined, 92 | editingPerson: undefined, 93 | activePersonView: 'info', 94 | people: {}, 95 | notes: [], 96 | status: '', 97 | saving: false, 98 | checkInNote: '', 99 | inNomie: true, 100 | sort: localStorage.getItem('mp-sort') || 'attention' 101 | }), 102 | components: { 103 | 'person-item': PersonItem, 104 | 'avatar': AvatarComponent 105 | }, 106 | async mounted() { 107 | document.body.classList.remove('loading'); 108 | /** 109 | * on UI Opened 110 | * Gets fired when the user opens the plugin modal 111 | */ 112 | plugin.onUIOpened(async () => { 113 | this.mode = 'modal'; 114 | }); 115 | 116 | plugin.onRegistered(async () => { 117 | await plugin.storage.init() 118 | this.inNomie = true; 119 | this.loading = false; 120 | let fromStorage = plugin.storage.getItem('people') || {}; 121 | Object.keys(fromStorage).map(username => { 122 | fromStorage[username] = new Person(fromStorage[username]); 123 | }) 124 | this.people = fromStorage; 125 | 126 | const latest = await this.getLatest(); 127 | 128 | }) 129 | 130 | setTimeout(() => { 131 | if (this.loading) { 132 | this.inNomie = false; 133 | } 134 | }, 700); 135 | 136 | }, 137 | computed: { 138 | peopleArray() { 139 | return Object.keys(this.people || {}).map((username) => { 140 | return this.people[username]; 141 | }) 142 | }, 143 | cleanPhone() { 144 | return this.activePerson.phone.replace(/[^a-z0-9]/g, '') 145 | }, 146 | 147 | 148 | everyoneElse() { 149 | const stripName = (n) => { 150 | return `${n || ''}`.replace('@', ''); 151 | } 152 | return this.peopleArray.filter((p) => { 153 | // return !p.isBirthday && p.noContactScore > 2; 154 | return true; 155 | }).filter(p => !p.hidden).sort((a, b) => { 156 | 157 | if (this.sort == 'oldest') { 158 | return a.latest > b.latest ? 1 : -1; 159 | } else if (this.sort == 'newest') { 160 | return a.latest < b.latest ? 1 : -1 161 | } else if (this.sort == 'a-z') { 162 | return stripName(a.name).toLowerCase() > stripName(b.name).toLowerCase() ? 1 : -1 163 | } else if (this.sort == 'z-a') { 164 | return stripName(a.name).toLowerCase() < stripName(b.name).toLowerCase() ? 1 : -1 165 | } else if (this.sort == 'attention') { 166 | 167 | return a.noContactScore > b.noContactScore ? 1 : -1 168 | } 169 | return true; 170 | }) 171 | }, 172 | checkinOptions() { 173 | return [ 174 | { 175 | label: 'Lunch', 176 | note: `Lunch with @${this.activePerson.username} ` 177 | }, 178 | { 179 | label: 'Dinner', 180 | note: `Dinner with @${this.activePerson.username} ` 181 | }, 182 | { 183 | label: 'Phone Call', 184 | note: `Phone Call with @${this.activePerson.username} ` 185 | }, 186 | { 187 | label: 'Virtual Meeting', 188 | note: `Virtual meeting with @${this.activePerson.username} ` 189 | }, 190 | { 191 | label: 'Text/SMS', 192 | note: `Texted with @${this.activePerson.username} ` 193 | }, 194 | { 195 | label: 'Email', 196 | note: `Emailed @${this.activePerson.username} ` 197 | }, 198 | { 199 | label: 'Drinks', 200 | note: `Drinks with @${this.activePerson.username} ` 201 | }, 202 | { 203 | label: 'Meeting', 204 | note: `Meeting with @${this.activePerson.username} ` 205 | }, 206 | { 207 | label: 'Hung-out', 208 | note: `Hung out with @${this.activePerson.username} ` 209 | }, 210 | { 211 | label: 'Other', 212 | note: `With @${this.activePerson.username} ` 213 | }, 214 | ] 215 | }, 216 | birthdayArray() { 217 | return this.peopleArray.filter(p => p.isBirthday); 218 | }, 219 | activePersonNotes() { 220 | return this.notes.filter((note) => { 221 | return note.elements.find(e => { 222 | return e.id == this.activePerson.username; 223 | }) 224 | }) 225 | }, 226 | needsAttention() { 227 | return this.peopleArray.filter((p) => { 228 | return !p.isBirthday && p.noContactScore <= 2 229 | }).sort((a, b) => { 230 | return a.noContactScore > b.noContactScore ? 1 : -1 231 | }) 232 | } 233 | }, 234 | watch: { 235 | "editingPerson.displayName"() { 236 | if (this.editingPerson && this.editingPerson.dirty) { 237 | this.editingPerson.username = strToTagSafe(this.editingPerson.displayName); 238 | } 239 | } 240 | }, 241 | methods: { 242 | dayjs(date) { 243 | return plugin.dayjs(date); 244 | }, 245 | createPerson() { 246 | this.editingPerson = new Person({}); 247 | }, 248 | setNoContactDays(evt) { 249 | const value = evt.target.value; 250 | let person = new Person(this.activePerson); 251 | person.maxNoContactDays = value; 252 | this.upsertPerson(person); 253 | }, 254 | postActionPrompt(type) { 255 | this.activePersonView = 'check-in'; 256 | this.checkInNote = `${type} @${this.activePerson.username} `; 257 | 258 | }, 259 | setSort(evt) { 260 | const value = evt.target.value 261 | localStorage.setItem('mp-sort', value); 262 | }, 263 | async saveNote() { 264 | this.saving = true; 265 | await wait(200); 266 | if (this.activePerson && this.checkInNote) { 267 | this.activePerson.latest = new Date(); 268 | await plugin.createNote(this.checkInNote); 269 | this.clearActivePerson(); 270 | this.saving = false; 271 | await wait(100); 272 | this.getLatest(); 273 | } 274 | }, 275 | clearActivePerson() { 276 | this.checkInNote = ""; 277 | this.activePerson = undefined; 278 | }, 279 | addToNote(str) { 280 | this.checkInNote = [this.checkInNote, str].join(' '); 281 | }, 282 | /** 283 | * Get 30 days of notes 284 | * extract @people to compile the this.people 285 | * @returns 286 | */ 287 | async getLatest() { 288 | let hasChanges = false; 289 | // Search all notes 290 | const allNotes = await plugin.searchNotes('', new Date(), 30); 291 | // Filter out only notes with elements that have a person 292 | const peopleNotes = allNotes.filter(note => { 293 | return note.elements.find(e => e.type == 'person') 294 | }) 295 | // Create a standalone object 296 | const allPeople = { ...this.people }; 297 | // Loop over the people notes 298 | peopleNotes.forEach((note) => { 299 | // Loop over the elements of the notes 300 | // filter out on people 301 | note.elements.filter(e => e.type == 'person').forEach((ele) => { 302 | // If allPeople[username] DOES NOT EXIST 303 | if (!allPeople.hasOwnProperty(ele.id)) { 304 | allPeople[ele.id] = new Person({ 305 | username: ele.id, 306 | displayName: ele.raw, 307 | latest: note.end 308 | }) 309 | hasChanges = true; 310 | } else { 311 | // AllPeople[username] exists 312 | // Get this note date 313 | const noteDate = new Date(note.end); 314 | // Get last note date 315 | const lastUserDate = allPeople[ele.id].latest || '1960-01-01'; 316 | const latest = new Date(lastUserDate); 317 | 318 | // Is latest less than this note date? 319 | if (latest < noteDate) { 320 | hasChanges = true; 321 | // yes, update the latest to this notes date 322 | allPeople[ele.id].latest = note.end; 323 | } 324 | } 325 | }) 326 | }) 327 | // Push allPeople to vue this.people 328 | this.people = allPeople; 329 | this.notes = peopleNotes; 330 | // If has changes = save the 331 | if (hasChanges) this.saveStorage(); 332 | return this.notes; 333 | 334 | }, 335 | async openPerson(person) { 336 | this.activePerson = person; 337 | this.checkInNote = `@` + person.username + ' ' 338 | }, 339 | /** 340 | * Edit a Person - open Modal 341 | * @param {Person} person 342 | */ 343 | async editPerson(_person) { 344 | const person = new Person(_person); 345 | const tag = `@${person.username}`; 346 | if (!person.displayName || !person.avatar) { 347 | const value = await plugin.getTrackable(tag); 348 | const trackable = value.trackable; 349 | 350 | if (trackable) { 351 | person.displayName = trackable.person.displayName; 352 | person.avatar = trackable.person.avatar; 353 | person.notes = person.notes || trackable.person.notes; 354 | } 355 | } 356 | 357 | await wait(100); 358 | this.editingPerson = person 359 | }, 360 | openInNomie(person) { 361 | plugin.openTrackableEditor(person.asTrackable) 362 | }, 363 | /** 364 | * Save a Person to the plugin database 365 | * @param {Person} person 366 | */ 367 | savePerson(person) { 368 | try { 369 | // If new Person (not in Nomie) 370 | if (person.dirty) { 371 | person.dirty = false; 372 | plugin.openTrackableEditor(person.asTrackable) 373 | } 374 | this.upsertPerson(person); 375 | 376 | // If we have the active Person open while editing 377 | if (this.activePerson && this.activePerson.username == this.editingPerson.username) { 378 | this.openPerson(new Person(person)); 379 | } 380 | 381 | // Clear Editing State 382 | this.editingPerson = undefined; 383 | } catch (e) { 384 | alert(e.message); 385 | } 386 | }, 387 | /** 388 | * Add or Update Person to the plugin database 389 | * @param {Person} person 390 | */ 391 | upsertPerson(person) { 392 | if (!person instanceof Person) throw error('Must be a person class to upsert a person'); 393 | let allPeople = { ...this.people }; 394 | let current = allPeople[person.username]; 395 | if (current) { 396 | allPeople[person.username] = new Person({ ...current, ...person }); 397 | } else { 398 | allPeople[person.username] = person; 399 | } 400 | this.people = allPeople; 401 | 402 | plugin.storage.setItem('people', this.people); 403 | 404 | }, 405 | async deletePerson(person) { 406 | const confirmed = await plugin.confirm(`Delete ${person.username} from the My People plugin?`, 'This will not remove them from Nomie'); 407 | if (confirmed.value === true) { 408 | if (person == this.activePerson) this.activePerson = undefined; 409 | 410 | const allPeople = { ...this.people }; 411 | delete allPeople[person.username]; 412 | plugin.storage.setItem('people', allPeople); 413 | this.people = allPeople; 414 | } 415 | }, 416 | saveStorage() { 417 | plugin.storage.setItem('people', this.people); 418 | }, 419 | async selectPersonTrackable() { 420 | let selected = await plugin.selectTrackable('person'); 421 | if (selected) { 422 | let person = new Person(selected.person); 423 | this.upsertPerson(person); 424 | } 425 | } 426 | }, 427 | 428 | }).$mount("#app"); 429 | -------------------------------------------------------------------------------- /src/v1/plugins/my-people/person.class.js: -------------------------------------------------------------------------------- 1 | class Person { 2 | constructor(starter = {}) { 3 | this.username = starter.username; 4 | this.displayName = starter.displayName; 5 | this.birthday = starter.birthday; 6 | this.frequency = starter.frequency 7 | this.notes = starter.notes 8 | this.avatar = starter.avatar 9 | this.latest = starter.latest 10 | this.hidden = starter.hidden ? true : false; 11 | this.email = starter.email; 12 | this.phone = starter.phone; 13 | this.maxNoContactDays = starter.maxNoContactDays; 14 | this.dirty = starter.username ? false : true; 15 | } 16 | 17 | get age() { 18 | if (this.birthday) { 19 | return plugin.dayjs().diff(this.birthdate, 'years'); 20 | } 21 | } 22 | 23 | get name() { 24 | return this.displayName || this.username; 25 | } 26 | 27 | get birthdate() { 28 | if (this.birthday) { 29 | return plugin.dayjs(new Date(`${this.birthday}T12:00:00`)); 30 | } 31 | } 32 | 33 | get noContactScore() { 34 | if (this.maxNoContactDays) { 35 | let last = plugin.dayjs(this.latest || '2001-01-01'); 36 | let daysDiff = plugin.dayjs().diff(last, 'days') 37 | let maxDiff = this.maxNoContactDays - daysDiff; 38 | return maxDiff; 39 | } else { 40 | return 1000; 41 | } 42 | } 43 | 44 | get isBirthday() { 45 | if (this.birthday) { 46 | const now = plugin.dayjs(); 47 | let birthdate = this.birthdate.year(now.year()); 48 | return birthdate.toDate().toDateString() == now.toDate().toDateString(); 49 | } else { 50 | return false; 51 | } 52 | } 53 | 54 | get nextBirthdate() { 55 | let now = plugin.dayjs(); 56 | let birthdate = this.birthdate; 57 | 58 | if (birthdate.year(now.year()).toDate().getTime() < new Date().getTime()) { 59 | birthdate = birthdate.year(now.add(1, 'year').year()); 60 | } else { 61 | birthdate = birthdate.year(now.year()); 62 | } 63 | return birthdate; 64 | } 65 | 66 | get birthdayFromNow() { 67 | if (this.birthday) { 68 | let next = this.nextBirthdate; 69 | return next.diff(plugin.dayjs(), 'days'); 70 | } 71 | } 72 | 73 | get birthdayFormatted() { 74 | if (this.birthday) { 75 | return this.nextBirthdate.format('Do MMM'); 76 | } 77 | } 78 | 79 | get asTrackable() { 80 | return { 81 | type: 'person', 82 | tag: '@' + this.username, 83 | person: { 84 | username: this.username, 85 | displayName: this.displayName, 86 | notes: this.notes, 87 | avatar: "https://shareking.s3.amazonaws.com/Screen-Shot-2022-10-07-09-20-03.45.png" 88 | } 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /src/v1/plugins/tester/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Nomie Weather Plugin 8 | 9 | 10 | 11 | 12 | 13 |
18 |
19 | 25 | 31 | 37 | 41 | 47 | 53 | 59 |
60 | 61 |
    62 |
  • 66 | {{trackable.id}} 67 | {{trackable.value}} 68 |
  • 69 |
70 |
71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/v1/plugins/tester/template-test.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "template", 3 | "id": "11053cbb2c2a8e543eea23dc46933267", 4 | "name": "Tester Template", 5 | "description": "A template for the tester plugin", 6 | "version": "6.0.12", 7 | "trackables": [ 8 | { 9 | "type": "tracker", 10 | "id": "#tester", 11 | "tracker": { 12 | "id": "280b95de-4cd4-4019-a155-2b0b6e85d591", 13 | "tag": "tester", 14 | "label": "Tester", 15 | "type": "range", 16 | "color": "#8e2cf5", 17 | "math": "mean", 18 | "ignore_zeros": false, 19 | "uom": "num", 20 | "emoji": "😉", 21 | "avatar": null, 22 | "default": "3", 23 | "max": "5", 24 | "min": "1", 25 | "minLabel":"Bad", 26 | "maxLabel":"Good", 27 | "step": "1", 28 | "score": 2, 29 | "score_calc": null, 30 | "goal": null, 31 | "one_tap": false, 32 | "include": "", 33 | "note": null, 34 | "hidden": false, 35 | "focus": [ 36 | "mind", 37 | "body" 38 | ] 39 | }, 40 | "value": 1 41 | } 42 | ], 43 | "goals": [ 44 | { 45 | "id": "a2c5d031-81a0-48ec-80fa-7072db3de1de", 46 | "tag": "#mood", 47 | "duration": "day", 48 | "target": 5, 49 | "comparison": "gte" 50 | } 51 | ], 52 | "boards": [ 53 | { 54 | "id": "ee223ff38b", 55 | "label": "Mood", 56 | "elements": [ 57 | "#life_checkup", 58 | "#anxiety", 59 | "#motivation", 60 | "#mood", 61 | "#stress" 62 | ], 63 | "active": true 64 | } 65 | ], 66 | "dashboards": [ 67 | ] 68 | } -------------------------------------------------------------------------------- /src/v1/plugins/tester/tester.js: -------------------------------------------------------------------------------- 1 | /* This is the plugin object. It's a wrapper around the Nomie Plugin API. */ 2 | const plugin = new NomiePlugin({ 3 | name: "Tester Plugin", 4 | addToCaptureMenu: true, 5 | addToMoreMenu: true, 6 | addToWidgets: true, 7 | emoji: "🛠", 8 | version: "1.0", 9 | description: "Tester Plugin", 10 | uses: [ 11 | // NOTE: if you change these, you will need to reinstall this plugin in Nomie 12 | "createNote", // Create a new Note in Nomie 13 | "onLaunch", // 14 | "onNote", 15 | "getTrackableUsage", 16 | "searchNotes", 17 | "selectTrackables", 18 | "getTrackable", 19 | "getLocation", 20 | ], 21 | }); 22 | 23 | const log = (v1, v2, v3, v4) => { 24 | console.log( 25 | `👋👋👋 Tester Plugin:`, 26 | v1 ? v1 : "", 27 | v2 ? v2 : "", 28 | v3 ? v3 : "", 29 | v4 ? v4 : "" 30 | ); 31 | }; 32 | 33 | /** 34 | * Vue 2.0 App 35 | */ 36 | new Vue({ 37 | data: () => ({ 38 | error: undefined, 39 | trackables: [], 40 | }), 41 | async mounted() { 42 | /** 43 | * On Launch 44 | * Gets fired each time the user opens Nomie 45 | */ 46 | plugin.onLaunch(() => { 47 | log("✅ Nomie Launched Fired"); 48 | }); 49 | 50 | /** 51 | * on UI Opened 52 | * Gets fired when the user opens the plugin modal 53 | */ 54 | plugin.onUIOpened(async () => { 55 | log("✅ Nomie UI Opened Fired"); 56 | }); 57 | 58 | plugin.onNote((note) => { 59 | log("✅ Nomie onNote Fired"); 60 | log("Note text", note.note); 61 | log("Note Date", note.end); 62 | log("Note Score", note.score); 63 | log("All Data", note); 64 | }); 65 | 66 | plugin.onRegistered(async () => { 67 | log("✅ Tester Plugin Registered"); 68 | log('User Prefs',plugin.prefs); 69 | plugin.storage.init().then(() => { 70 | log("✅ Plugin Storage initialized"); 71 | }); 72 | }); 73 | }, 74 | methods: { 75 | openURL() { 76 | plugin.openURL("https://nomie.app", "Nomie Website"); 77 | }, 78 | openTemplate() { 79 | plugin.openTemplateURL(`${window.location.origin}/v1/plugins/tester/template-test.json`); 80 | }, 81 | async confirmTest() { 82 | const confirmed = await plugin.confirm("Can you please confirm?","Message should support **markdown**") 83 | if(confirmed.value) { 84 | await plugin.alert('You confirmed it!') 85 | } else { 86 | await plugin.alert('You DID NOT confirm it!') 87 | } 88 | }, 89 | createNote() { 90 | plugin.createNote({ 91 | note: "This is a test note!", 92 | score: 3, 93 | }); 94 | }, 95 | async searchNotes() { 96 | const keyword = await plugin.prompt("Search Term"); 97 | const daysBack = 30; 98 | if (keyword.value) { 99 | console.log({ keyword }); 100 | let notes = await plugin.searchNotes( 101 | keyword.value, 102 | new Date(), 103 | daysBack 104 | ); 105 | plugin.alert(`Found ${notes.length} notes`, 106 | `There are ${notes.length} notes with the term **${keyword.value}** over the last ${daysBack}` 107 | ); 108 | } 109 | }, 110 | async selectTrackables() { 111 | const selected = await plugin.selectTrackables(); 112 | await plugin.alert(`You selected ${selected.length} trackables!`); 113 | }, 114 | async selectTrackableAndValue() { 115 | let trackable = await plugin.selectTrackable('tracker'); 116 | if (trackable) { 117 | console.log({ trackable }); 118 | let res = await plugin.getTrackableInput(trackable.id); 119 | let value = undefined; 120 | if (res && res.value) { 121 | trackable.value = res.value; 122 | } 123 | this.trackables.push(trackable); 124 | } 125 | }, 126 | }, 127 | }).$mount("#content"); 128 | -------------------------------------------------------------------------------- /src/v1/plugins/weather/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | Nomie Weather Plugin 11 | 12 | 13 | 14 | 16 | 17 | 18 | 19 |
21 |
24 | 25 |
28 | Looking for the lastest weather... 29 |
30 |
32 |
34 | ⛔️ {{error}} 35 |
36 |
37 | 38 | 39 |
40 |
43 |

44 | {{currently.dateString}} 45 |

46 |

47 | {{currently.description}} Today 48 |

49 | 50 |
    53 |
  • 54 |
    55 |
    {{currently.temp_low}}°
    56 |
    ↓ Low
    57 |
    58 |
    59 |
    {{currently.temp}}°
    60 |
    ↑ High
    61 |
    62 |
  • 63 |
  • 64 | 🌞 65 | Day Length 66 | {{currently.day_length}}h 67 |
  • 68 |
  • 69 | 🏋️‍♀️ 70 | Pressure 71 | {{currently.pressure}} 72 |
  • 73 |
  • 74 | 💨 75 | Wind Speed 76 | {{currently.wind_speed}} 77 |
  • 78 |
  • 79 | 🌥 80 | Clouds 81 | {{currently.clouds}}% 82 |
  • 83 |
  • 84 | 💧 85 | Humidity 86 | {{currently.humidity}}% 87 |
  • 88 |
  • 89 | 🌧 90 | Precipitation 91 | {{currently.precipitation}} 92 |
  • 93 | 94 |
95 |
96 | 97 |
98 |
102 |
104 | 105 | 109 |
110 |
112 |
113 |
114 | 115 |

116 | Track once a day (if you open 117 | Nomie) 118 |

119 |
120 | 125 |
126 |
128 |
129 |
130 | 131 |

No API Key Found

133 |

•••••••••••••••••••••

135 |
136 | 140 |
141 | 142 | 143 | 155 |
156 |
157 |
158 | 159 | 160 | 161 | 162 | 165 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /src/v1/plugins/weather/weather.js: -------------------------------------------------------------------------------- 1 | let date = new Date().toDateString(); 2 | 3 | /* This is the plugin object. It's a wrapper around the Nomie Plugin API. */ 4 | const plugin = new NomiePlugin({ 5 | name: "Weather", 6 | addToCaptureMenu: true, 7 | addToMoreMenu: true, 8 | addToWidgets: true, 9 | emoji: "⛈", 10 | version: "1.2", 11 | description: `Track the weather with Tomorrow.io. Using your location, this plugin will track the weather one time a day, as long as you open Nomie. **Note** this plugin requires a FREE API key from Tomorrow.io`, 12 | uses: ["createNote", "onLaunch", "getLocation"], 13 | }); 14 | 15 | const debug = false; 16 | const logger = (a, b, c, d) => { 17 | if (debug) { 18 | console.log("%c" + "Weather Plugin", "font-weight:bold;"); 19 | console.log("🌨", a, b ? b : '', c ? c : '', d ? d : ''); 20 | } 21 | } 22 | 23 | 24 | const msToHourFormat = (ms) => { 25 | // 1- Convert to seconds: 26 | let seconds = ms / 1000; 27 | // 2- Extract hours: 28 | const hours = parseInt(seconds / 3600); // 3,600 seconds in 1 hour 29 | seconds = seconds % 3600; // seconds remaining after extracting hours 30 | // 3- Extract minutes: 31 | const minutes = parseInt(seconds / 60); // 60 seconds in 1 minute 32 | // 4- Keep only seconds not extracted to minutes: 33 | seconds = seconds % 60; 34 | return (hours + ":" + minutes + ":" + seconds); 35 | } 36 | 37 | 38 | /** 39 | * It takes a location object, makes a call to the OpenWeatherMap API, and returns a weather object 40 | * @param location - {lat: number, lng: number} 41 | * @returns An object with the following properties: 42 | * temp, name, tempHigh, tempLow, feelsLike, clouds, pressure, wind, latitude, longitude, 43 | * description, latitude, longitude, dateString 44 | */ 45 | const getCurrentConditions = async (location, apikey) => { 46 | logger("getCurrentConditions", { location, apikey }); 47 | // Determine user Units 48 | const units = !plugin.prefs?.useMetric ? "imperial" : "metric"; 49 | // Call Open Weather Map 50 | 51 | const timeSteps = '1d' 52 | 53 | const fields = [ 54 | "sunriseTime", 55 | "sunsetTime", 56 | "windSpeed", 57 | "pressureSurfaceLevel", 58 | "temperatureMin", 59 | "temperatureMax", 60 | "cloudCover", 61 | "precipitationType", 62 | "humidity" 63 | ].map((field) => { 64 | return `fields=${field}`; 65 | }).join('&'); 66 | //&startTime=${start}&endTime=${end} 67 | 68 | 69 | const params = [ 70 | `location=${location.lat},${location.lng}`, 71 | fields, 72 | // `startTime=${start}`, 73 | // `endTime=${end}`, 74 | `units=${units}`, 75 | `timesteps=1d` 76 | ] 77 | 78 | 79 | const urlParams = params.join('&') + `&apikey=${apikey}`; 80 | 81 | logger("Calling Tomorrow API") 82 | const url = `https://api.tomorrow.io/v4/timelines?${urlParams}`; 83 | const call = await fetch(url); 84 | const data = await call.json(); 85 | logger("Tomorrow results", data); 86 | 87 | 88 | if (data && data.data) { 89 | let weather = data.data.timelines[0]; 90 | let day = weather.intervals[0].values; 91 | let dayTimeLight = new Date(day.sunsetTime).getTime() - new Date(day.sunriseTime).getTime(); 92 | 93 | let weatherData = { 94 | 'temp': day.temperatureMax, 95 | 'temp_low': day.temperatureMin, 96 | 'day_length': msToHourFormat(dayTimeLight), 97 | 'pressure': day.pressureSurfaceLevel, 98 | 'wind_speed': day.windSpeed, 99 | 'clouds': day.cloudCover, 100 | 'humidity': day.humidity, 101 | 'precipitation': day.precipitationType, 102 | }; 103 | 104 | let description = ""; 105 | if (day.precipitationType == 0) { 106 | weatherData.weather_sunny = null 107 | description = "Sunny" 108 | } else if (day.precipitationType == 1) { 109 | weatherData.weather_rain = null; 110 | description = "Rain" 111 | } else if (day.precipitationType == 2) { 112 | weatherData.weather_snow = null; 113 | description = "Snow" 114 | } else if (day.precipitationType == 3) { 115 | weatherData.weather_freezing = null; 116 | description = "Freezing Rain" 117 | } else if (day.precipitationType == 4) { 118 | weatherData.weather_ice = null; 119 | description = "Icy conditions" 120 | } 121 | 122 | const note = Object.keys(weatherData).map((tag) => { 123 | let value = weatherData[tag]; 124 | if (!value) { 125 | return `#${tag}`; 126 | } 127 | return `#${tag}(${value})`; 128 | }).join(' '); 129 | 130 | // note = note + ` ` + addOnData.map((str)=>str).join(' '); 131 | weatherData.note = note; 132 | weatherData.latitude = location.lat; 133 | weatherData.longitude = location.lng; 134 | weatherData.description = description; 135 | weatherData.dateString = plugin.dayjs().format('ddd Do MMM YYYY'); 136 | 137 | return weatherData; 138 | 139 | } else { 140 | return undefined; 141 | } 142 | 143 | 144 | 145 | }; 146 | 147 | const API_KEY_NAME = "tomorrow-api-key"; 148 | 149 | /** 150 | * Vue 2.0 App 151 | */ 152 | new Vue({ 153 | data: () => ({ 154 | error: undefined, 155 | currently: {}, 156 | view: "modal", 157 | apikey: undefined, 158 | registered: false, 159 | ignoreFields: [], 160 | autoTrack: false, 161 | errors: [], 162 | location: undefined 163 | }), 164 | async mounted() { 165 | /** 166 | * On Launch 167 | * Gets fired each time the user opens Nomie 168 | */ 169 | plugin.onLaunch(() => { 170 | logger("plugin.onLaunch", this.apikey); 171 | setTimeout(() => { 172 | this.view = 'hidden'; 173 | if (this.apikey) this.loadWeather(); 174 | }, 500); 175 | }); 176 | 177 | /** 178 | * On Widget 179 | * Gets fired each time the widget is displayed 180 | */ 181 | plugin.onWidget(() => { 182 | this.view = "widget"; 183 | this.loadWeather(); 184 | }); 185 | 186 | /** 187 | * on UI Opened 188 | * Gets fired when the user opens the plugin modal 189 | */ 190 | plugin.onUIOpened(async () => { 191 | this.view = "modal"; 192 | this.autoTrack = plugin.storage.getItem('autoTrack') ? true : false; 193 | 194 | if (!this.apikey) { 195 | logger("onUIOpened - no API Key Found") 196 | await this.getAndSetApiKey() 197 | } else { 198 | logger("onUIOpened - API Key exists") 199 | this.loadWeather(); 200 | } 201 | 202 | }); 203 | 204 | /** 205 | * When the Plugin is Registered 206 | * Check if we have an API key for OpenWeatherMap 207 | * if not, prompt the user for the api 208 | */ 209 | plugin.onRegistered(async (payload) => { 210 | // Initialize the Storage 211 | await plugin.storage.init(); 212 | // Get Key from storage 213 | this.ignoreFields = plugin.storage.getItem('ignoreFields') || []; 214 | this.autoTrack = plugin.storage.getItem('autoTrack') === false ? false : true; 215 | this.apikey = plugin.storage.getItem(API_KEY_NAME); 216 | logger("onRegistered - this.apikey", this.apikey.length, this.autoTrack); 217 | // Set LocationId and Plugin Id 218 | this.lid = payload.lid; 219 | this.pid = payload.pid; 220 | // Tag we're registered 221 | this.registered = true; 222 | 223 | }); 224 | 225 | 226 | 227 | 228 | /** 229 | * Wait for 6 seconds then throw an error if we didnt get the temp. 230 | */ 231 | setTimeout(() => { 232 | if (!this.currently.temp) 233 | this.showError("Unable to get your weather."); 234 | if (!this.location) this.showError("Could not find location") 235 | if (!this.registered) this.showError("Plugin was not properly registered, try to exit and refresh plugin or Nomie.") 236 | }, 6000); 237 | }, 238 | watch: { 239 | "autoTrack"() { 240 | plugin.storage.setItem('autoTrack', this.autoTrack); 241 | } 242 | }, 243 | methods: { 244 | clearErrors() { 245 | this.errors = []; 246 | }, 247 | showError(message) { 248 | this.errors.push(message); 249 | }, 250 | /** 251 | * > Prompt the user for their API key, if they provide one, save it and load the weather 252 | * @returns A boolean value. 253 | */ 254 | async getAndSetApiKey() { 255 | logger("Get the Tomrorrow API Key"); 256 | const res = await plugin.prompt( 257 | "Tomorrow.io API Key", 258 | `Tomorrow.io Account & API are required. Get your [FREE API key here](https://app.tomorrow.io/development/keys)` 259 | ); 260 | if (res && res.value && res.value.length > 4) { 261 | this.apikey = res.value; 262 | plugin.storage.setItem(API_KEY_NAME, this.apikey); 263 | logger("getAndSetAPIKey res", this.apikey.length); 264 | this.loadWeather(); 265 | return true; 266 | } else { 267 | alert("Invalid Weather Key provided"); 268 | } 269 | }, 270 | 271 | async getLocation() { 272 | const location = await plugin.getLocation(); 273 | this.location = location; 274 | return location; 275 | }, 276 | 277 | /** 278 | * It gets the user's location, then gets the weather for that location, then if the weather is 279 | * fresh and the user has auto-tracking enabled, it tracks the weather, then if there was an error, 280 | * it sets the error message, otherwise it sets the current weather 281 | */ 282 | async loadWeather() { 283 | logger("loadWeather()"); 284 | const location = await this.getLocation(); 285 | logger("loadWeather Location", location); 286 | if (location) { 287 | try { 288 | let weather = await this.getWeatherCached(location); 289 | if (weather.fresh && this.autoTrack) { 290 | this.trackWeather(); 291 | } 292 | if (weather) this.clearErrors(); 293 | this.currently = weather; 294 | } catch (e) { 295 | this.showError(`${e}`); 296 | } 297 | } else { 298 | this.showError("Unable to find your location"); 299 | } 300 | }, 301 | /** 302 | * > If we have a cached weather object, and it was captured today, return it. Otherwise, get the 303 | * current location, and if we have a lat/long, get the current weather conditions, and return them 304 | * @returns The weather data 305 | */ 306 | async getWeatherCached() { 307 | const LAST_WEATHER_KEY = 'cached-weather'; 308 | logger("getWeatherCached()"); 309 | try { 310 | let fromCache = plugin.storage.getItem(LAST_WEATHER_KEY); 311 | let lookupData = fromCache || {}; 312 | // let lookupData = {}; 313 | let cached = undefined; 314 | // Have lookup data? 315 | if (lookupData && lookupData.captured) { 316 | // Is the last day today? 317 | if (lookupData.captured === new Date().toDateString()) { 318 | cached = lookupData; 319 | cached.fresh = false; 320 | logger("getWeatherCache Exists for today - no need to get fresh") 321 | } 322 | } 323 | 324 | // If it's not cached - lets get it 325 | if (!cached) { 326 | // Get User Location f 327 | const weather = await this.getFreshWeather(); 328 | if (weather.temp) { 329 | // Save to Plugin storage in Nomie 330 | await plugin.storage.setItem(LAST_WEATHER_KEY, weather); 331 | cached = weather; 332 | cached.fresh = true; 333 | } 334 | } 335 | logger("getWeatherCached results", cached); 336 | return cached; 337 | } catch (e) { 338 | this.showError(`${e}`); 339 | } 340 | }, 341 | async getFreshWeather() { 342 | 343 | logger("getFreshWeather()") 344 | let location = await this.getLocation(); 345 | if (location) { 346 | logger("getFreshWeather() location", location); 347 | // Get weather based on location 348 | let weather = await getCurrentConditions(location, this.apikey); 349 | 350 | if (weather) { 351 | // Tag the date 352 | weather.captured = date; 353 | // If we have a temp - we're good to go 354 | return weather; 355 | } else { 356 | throw Error('Unable to get the weather right now... Try later') 357 | } 358 | } 359 | }, 360 | async getWeatherAsNote(fresh) { 361 | let currently; 362 | if (fresh) { 363 | currently = await this.getFreshWeather(); 364 | } else { 365 | currently = await this.getWeatherCached(); 366 | } 367 | if (currently) { 368 | return { 369 | note: currently.note, 370 | lat: currently.latitude, 371 | lng: currently.longitude 372 | }; 373 | } 374 | return undefined; 375 | }, 376 | 377 | /** 378 | * It gets the weather as a note, and if it gets a note, it creates it 379 | */ 380 | async trackWeather() { 381 | const weatherNote = await this.getWeatherAsNote(true); 382 | if (weatherNote) { 383 | plugin.createNote(weatherNote); 384 | } 385 | }, 386 | }, 387 | }).$mount("#content"); 388 | -------------------------------------------------------------------------------- /src/v1/wip/lets-eat/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Nomie Weather Plugin 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 | Search for Food and Drinks 24 |
25 | 26 |
    27 | 43 |
44 | 45 |
46 |
47 |
48 | 51 |
52 |
53 |

Totals

54 |
    55 |
  • 56 | {{nut.name}} {{nut.value}}{{nut.unit}} 57 |
  • 58 |
59 |
60 |
61 | 62 |
63 | 64 | 65 |
66 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/v1/wip/lets-eat/lets-eat.css: -------------------------------------------------------------------------------- 1 | .food-button { 2 | @apply bg-gray-100 dark:bg-gray-900; 3 | @apply py-1 px-3; 4 | } -------------------------------------------------------------------------------- /src/v1/wip/lets-eat/lets-eat.js: -------------------------------------------------------------------------------- 1 | /* This is the plugin object. It's a wrapper around the Nomie Plugin API. */ 2 | const plugin = new NomiePlugin({ 3 | name: "Let's Eat!", 4 | addToCaptureMenu: true, 5 | addToWidgets: true, 6 | emoji: "🍔", 7 | version: "1.0", 8 | description: "Food and Nutrition Tracking", 9 | uses: [ 10 | // NOTE: if you change these, you will need to reinstall this plugin in Nomie 11 | "createNote", 12 | "selectTrackables", 13 | "getTrackable", 14 | "getLocation", 15 | ], 16 | }); 17 | 18 | const log = (v1, v2, v3, v4) => { 19 | console.log( 20 | `👋👋👋 Tester Plugin:`, 21 | v1 ? v1 : "", 22 | v2 ? v2 : "", 23 | v3 ? v3 : "", 24 | v4 ? v4 : "" 25 | ); 26 | }; 27 | 28 | let searchDebounce; 29 | 30 | /** 31 | * Vue 2.0 App 32 | */ 33 | new Vue({ 34 | data: () => ({ 35 | error: undefined, 36 | results: [], 37 | selected: [], 38 | searchTerm: "Burger", 39 | nutrients: undefined, 40 | apikey: undefined, 41 | }), 42 | watchers: { 43 | searchTerm() { 44 | console.log(this.searchTerm); 45 | }, 46 | }, 47 | async mounted() { 48 | plugin.storage.init().then(async () => { 49 | const key = await plugin.storage.getItem("apikey"); 50 | console.log("KEY From Storage", key); 51 | if (!key) { 52 | let newKey = await this.requestKey(); 53 | 54 | if (newKey) { 55 | plugin.storage.setItem("apikey", newKey); 56 | this.apikey = newKey; 57 | } 58 | } else { 59 | this.apikey = key; 60 | this.queryFood(); 61 | } 62 | }); 63 | 64 | /** 65 | * On Launch 66 | * Gets fired each time the user opens Nomie 67 | */ 68 | // plugin.onLaunch(() => { 69 | // log('✅ Nomie Launched Fired') 70 | // }); 71 | 72 | /** 73 | * on UI Opened 74 | * Gets fired when the user opens the plugin modal 75 | */ 76 | plugin.onUIOpened(async () => {}); 77 | 78 | /** 79 | * When the Plugin is Registered 80 | * Check if we have an API key for OpenWeatherMap 81 | * if not, prompt the user for the api 82 | */ 83 | // plugin.onRegistered(async () => { 84 | // await plugin.storage.init(); 85 | // if (!plugin.storage.getItem('apikey')) { 86 | // const key = await plugin.prompt('OpenWeatherMap API Key', 87 | // `OpenWeatherMap API Required. Get your [FREE API key here](https://home.openweathermap.org/api_keys)`) 88 | // if (key && key.value) { 89 | // plugin.storage.setItem('apikey', key.value); 90 | // } 91 | // } 92 | // }) 93 | }, 94 | async unmounted() { 95 | console.log("unmounted weather"); 96 | }, 97 | methods: { 98 | selectFood(food) { 99 | if(!this.selected.includes(food)) { 100 | this.selected.push(food) 101 | } 102 | this.selected = this.selected; 103 | console.log("Select this", food, this.selected); 104 | this.nutrients = this.generateNutrients() 105 | }, 106 | async queryFood() { 107 | if (this.searchTerm) { 108 | let url = `https://api.nal.usda.gov/fdc/v1/foods/search?query=${encodeURIComponent( 109 | this.searchTerm 110 | )}&pageSize=20&api_key=${this.apikey}`; 111 | let call = await fetch(url); 112 | let data = await call.json(); 113 | console.log("food data", data); 114 | if (data.foods) { 115 | this.results = data.foods; 116 | this.results = this.results.map((food)=>{ 117 | let cals = food.foodNutrients.find(n=>n.nutrientName == 'Energy'); 118 | if(cals) { 119 | food.cals = cals.value; 120 | } 121 | return food; 122 | }) 123 | } 124 | } else { 125 | this.results = []; 126 | } 127 | }, 128 | search(evt) { 129 | clearTimeout(searchDebounce); 130 | searchDebounce = setTimeout(() => { 131 | this.queryFood(); 132 | }, 500); 133 | }, 134 | async requestKey() { 135 | const res = await plugin.prompt( 136 | "USDA API Key", 137 | `You need a valid USDA API key to use this plugin. [Get your free API Key here](https://fdc.nal.usda.gov/api-key-signup.html)` 138 | ); 139 | console.log({ res }); 140 | if (res && res.value) { 141 | console.log({ key: res.value }); 142 | return res.value; 143 | } 144 | return undefined; 145 | }, 146 | generateNutrients() { 147 | const foodNutrients = this.selected.map(f=>f.foodNutrients); 148 | console.log({nutrients: foodNutrients.map(ni=>ni.map(n=>n.nutrientName))}); 149 | 150 | // nutrients.map(ni=>ni.map(n=>{ 151 | // map[n.nutrientName] = map[n.nutrientName] || { 152 | // values: [], 153 | // unit: n.unitName 154 | // }; 155 | // if(n.unitName !== 'IU') { 156 | // map[n.nutrientName].values.push(n.value); 157 | // } 158 | // })) 159 | 160 | const findNut = (term) => { 161 | 162 | let nuts = nutrients.find(n=>{ 163 | 164 | }) 165 | 166 | let filtered = Object.keys(map).filter(name=>{ 167 | return name.toLowerCase().search(term.toLowerCase()) > -1 && name.toLowerCase().search('International Units') == -1 168 | }).map(name=>{ 169 | let node = map[name]; 170 | return { 171 | value: node.values.reduce((a, b) => { 172 | let a1 = isNaN(a) ? 0 : a; 173 | let b1 = isNaN(b) ? 0 : b; 174 | return a1 + b1; 175 | }, 0), 176 | unit: map[name].unit 177 | } 178 | }) 179 | console.log(term, {filtered}); 180 | if(filtered.length) { 181 | return { 182 | unit: filtered[0].unit, 183 | value: filtered.length > 1 ? filtered.reduce((a,b)=> { 184 | return a.value + b.value 185 | }, 0) : filtered[0].value 186 | }; 187 | } 188 | } 189 | 190 | const final = []; 191 | let protein = findNut('protein'); 192 | 193 | if(protein) final.push({ name: 'Protein', value: protein.value, unit: protein.unit }) 194 | 195 | let energy = findNut('energy'); 196 | if(energy) final.push({ name: 'Energy', value: energy.value, unit: energy.unit }) 197 | 198 | let fiber = findNut('fiber'); 199 | if(fiber) final.push({ name: 'Fiber', value: fiber.value, unit: fiber.unit }) 200 | 201 | let sugar = findNut('sugar'); 202 | if(sugar) final.push({ name: 'Sugars', value: sugar.value, unit: sugar.unit }) 203 | 204 | console.log({map, final}); 205 | 206 | return final; 207 | } 208 | }, 209 | }).$mount("#app"); 210 | -------------------------------------------------------------------------------- /src/v1/wip/nui/nui.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | NUI Tester 10 | 11 | 12 |
13 |

Welcome to the Tester

14 |
15 |
16 |

Buttons

17 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |
31 |
32 |
33 | 34 | 35 | 36 |
37 |
38 |
39 |

Form Test

40 |
41 | 42 | 48 |

We will use it for something else

49 |
50 |
51 | 52 | 59 |
60 |
61 | 62 | 69 |
70 |
71 | 72 | 79 |
80 |
81 | 82 | 92 |
93 |
94 |
95 | 99 | 100 |
101 |
102 |
103 |
104 |
105 | 106 | 107 | 108 | 109 |
110 |
111 |
112 |
113 | 114 | 115 |
116 |
117 |
118 |
119 |
120 |
121 | 125 |
126 | 130 |
131 | 138 |
139 |
140 |
Hi there
141 |
142 |
143 | 144 | 145 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["./src/**/*.html", "./src/**/*.{js,jsx,ts,tsx}"], 3 | darkMode: "media", 4 | theme: { 5 | extend: { 6 | colors: { 7 | transparent: "transparent", 8 | current: "currentColor", 9 | "light-grey": "#E5EEE5", 10 | purple: "#7652C6", 11 | gray: { 12 | 950: '#090C13', 13 | }, 14 | primary: { 15 | DEFAULT: '#00A4E4', 16 | 50: '#eff9ff', 17 | 100: '#def1ff', 18 | 200: '#b6e5ff', 19 | 300: '#75d3ff', 20 | 400: '#2cbdff', 21 | 500: '#00aaff', 22 | 600: '#0083d4', 23 | 700: '#0068ab', 24 | 800: '#00588d', 25 | 900: '#064974', 26 | }, 27 | }, 28 | }, 29 | }, 30 | variants: { 31 | extend: { 32 | backgroundColor: ['hover', 'responsive', 'focus', 'dark'], 33 | textColor: ['hover', 'responsive', 'focus', 'dark'], 34 | }, 35 | }, 36 | plugins: [], 37 | }; 38 | 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": true, 5 | "noImplicitAny": true, 6 | "module": "es6", 7 | "target": "es5", 8 | "jsx": "react", 9 | "allowJs": true, 10 | "moduleResolution": "node" 11 | } 12 | } -------------------------------------------------------------------------------- /webpack-prod.config.js: -------------------------------------------------------------------------------- 1 | const config = require("./webpack.config"); 2 | 3 | 4 | module.exports = config.map((cnf) => { 5 | cnf.mode = 'production'; 6 | return cnf; 7 | }); 8 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const CopyPlugin = require("copy-webpack-plugin"); 3 | const MarkdownIt = require('markdown-it'), 4 | md = new MarkdownIt(); 5 | const fs = require('fs'); 6 | 7 | module.exports = [ 8 | { 9 | entry: "./src/v1/index.ts", 10 | mode: "development", 11 | devServer: { 12 | watchFiles: ["src/**/*"], 13 | headers: { 14 | "Access-Control-Allow-Origin": "*", 15 | "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS", 16 | "Access-Control-Allow-Headers": "X-Requested-With, content-type, Authorization" 17 | } 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.tsx?$/, 23 | use: "ts-loader", 24 | exclude: /node_modules/, 25 | }, 26 | { 27 | test: /\.css$/i, 28 | include: path.resolve(__dirname, "src"), 29 | use: ["style-loader", "css-loader", "postcss-loader"], 30 | }, 31 | ], 32 | }, 33 | resolve: { 34 | extensions: [".tsx", ".ts", ".js"], 35 | }, 36 | plugins: [ 37 | new CopyPlugin({ 38 | patterns: [ 39 | { 40 | from: "src/v1/index.html", to: "v1/index.html", 41 | transform(content) { 42 | const READMERAW = fs.readFileSync('./README.md', 'utf-8'); 43 | const README = md.render(READMERAW); 44 | return `${content}`.replace('{content}', README) 45 | } 46 | }, 47 | { from: "src/v1/plugins", to: "v1/plugins" }, 48 | { from: "src/v1/wip", to: "v1/wip" }, 49 | { from: "src/assets", to: "assets" }, 50 | { from: "src/styles", to: "styles" }, 51 | { from: "src/index.html", to: "index.html" }, 52 | ], 53 | }), 54 | ], 55 | output: { 56 | filename: "v1/nomie-plugin.js", 57 | path: path.resolve(__dirname, "dist"), 58 | clean: true, 59 | }, 60 | }, 61 | ]; 62 | --------------------------------------------------------------------------------