init
This commit is contained in:
commit
c3ee96429c
46 changed files with 6006 additions and 0 deletions
9
.editorconfig
Normal file
9
.editorconfig
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
|
||||||
|
charset = utf-8
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
end_of_line = lf
|
||||||
|
max_line_length = 100
|
1
.envrc
Normal file
1
.envrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
use flake
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
* text=auto eol=lf
|
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
BIN
bin/neutralino-linux_arm64
Executable file
BIN
bin/neutralino-linux_arm64
Executable file
Binary file not shown.
BIN
bin/neutralino-linux_armhf
Executable file
BIN
bin/neutralino-linux_armhf
Executable file
Binary file not shown.
BIN
bin/neutralino-linux_x64
Executable file
BIN
bin/neutralino-linux_x64
Executable file
Binary file not shown.
BIN
bin/neutralino-mac_arm64
Executable file
BIN
bin/neutralino-mac_arm64
Executable file
Binary file not shown.
BIN
bin/neutralino-mac_universal
Executable file
BIN
bin/neutralino-mac_universal
Executable file
Binary file not shown.
BIN
bin/neutralino-mac_x64
Executable file
BIN
bin/neutralino-mac_x64
Executable file
Binary file not shown.
BIN
bin/neutralino-win_x64.exe
Normal file
BIN
bin/neutralino-win_x64.exe
Normal file
Binary file not shown.
1
build-scripts
Submodule
1
build-scripts
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit ececd00d5fcbc78b83947db8fbab4a4b628ffd13
|
27
flake.lock
generated
Normal file
27
flake.lock
generated
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1747426788,
|
||||||
|
"narHash": "sha256-N4cp0asTsJCnRMFZ/k19V9akkxb7J/opG+K+jU57JGc=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "12a55407652e04dcf2309436eb06fef0d3713ef3",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
56
flake.nix
Normal file
56
flake.nix
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
{
|
||||||
|
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
|
||||||
|
|
||||||
|
outputs = { nixpkgs, ... }:
|
||||||
|
let pkgs = nixpkgs.legacyPackages."x86_64-linux";
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
gtk3
|
||||||
|
glib
|
||||||
|
cairo
|
||||||
|
pango
|
||||||
|
gdk-pixbuf
|
||||||
|
atk
|
||||||
|
at-spi2-atk
|
||||||
|
at-spi2-core
|
||||||
|
dbus
|
||||||
|
libappindicator-gtk3
|
||||||
|
webkitgtk_4_1
|
||||||
|
xorg.libX11
|
||||||
|
xorg.libXext
|
||||||
|
xorg.libXrender
|
||||||
|
xorg.libXtst
|
||||||
|
xorg.libxcb
|
||||||
|
xorg.libXcomposite
|
||||||
|
xorg.libXdamage
|
||||||
|
xorg.libXfixes
|
||||||
|
xorg.libXrandr
|
||||||
|
xorg.libXi
|
||||||
|
xorg.libXcursor
|
||||||
|
xorg.libXinerama
|
||||||
|
libpng
|
||||||
|
libjpeg
|
||||||
|
libGL
|
||||||
|
libGLU
|
||||||
|
mesa
|
||||||
|
fontconfig
|
||||||
|
freetype
|
||||||
|
# Not needed
|
||||||
|
# stdenv.cc.cc.lib
|
||||||
|
# glibc
|
||||||
|
gst_all_1.gstreamer
|
||||||
|
gst_all_1.gst-plugins-base
|
||||||
|
gst_all_1.gst-plugins-good
|
||||||
|
gst_all_1.gst-plugins-bad
|
||||||
|
gst_all_1.gst-plugins-ugly
|
||||||
|
];
|
||||||
|
in {
|
||||||
|
devShells."x86_64-linux".default = pkgs.mkShell {
|
||||||
|
nativeBuildInputs = [ pkgs.nodejs_24 pkgs.pnpm pkgs.git ];
|
||||||
|
inherit buildInputs;
|
||||||
|
shellHook = ''
|
||||||
|
export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath buildInputs}:$LD_LIBRARY_PATH
|
||||||
|
export GST_PLUGIN_PATH=${pkgs.gst_all_1.gstreamer}/lib/gstreamer-1.0:${pkgs.gst_all_1.gst-plugins-base}/lib/gstreamer-1.0:${pkgs.gst_all_1.gst-plugins-good}/lib/gstreamer-1.0:${pkgs.gst_all_1.gst-plugins-bad}/lib/gstreamer-1.0:${pkgs.gst_all_1.gst-plugins-ugly}/lib/gstreamer-1.0
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
38
neutralino.config.json
Normal file
38
neutralino.config.json
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"applicationId": "js.neutralino.zero",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"defaultMode": "window",
|
||||||
|
"documentRoot": "/www/dist/",
|
||||||
|
"url": "/",
|
||||||
|
"enableServer": true,
|
||||||
|
"enableNativeAPI": true,
|
||||||
|
"nativeAllowList": ["app.*"],
|
||||||
|
"modes": {
|
||||||
|
"window": {
|
||||||
|
"title": "myapp",
|
||||||
|
"width": 800,
|
||||||
|
"height": 500,
|
||||||
|
"minWidth": 400,
|
||||||
|
"minHeight": 200,
|
||||||
|
"icon": "/www/dist/favicon.ico"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cli": {
|
||||||
|
"binaryName": "myapp",
|
||||||
|
"resourcesPath": "/www/dist/",
|
||||||
|
"extensionsPath": "/extensions/",
|
||||||
|
"clientLibrary": "/www/public/neutralino.js",
|
||||||
|
"binaryVersion": "6.1.0",
|
||||||
|
"clientVersion": "6.1.0"
|
||||||
|
},
|
||||||
|
"buildScript": {
|
||||||
|
"mac": {
|
||||||
|
"architecture": ["x64", "arm64", "universal"],
|
||||||
|
"minimumOS": "10.13.0",
|
||||||
|
"appName": "myapp",
|
||||||
|
"appBundleName": "myapp",
|
||||||
|
"appIdentifier": "com.marketmix.ext.bun.demo",
|
||||||
|
"appIcon": "/packaging/pucoti.icns"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
packaging/pucoti.icns
Normal file
BIN
packaging/pucoti.icns
Normal file
Binary file not shown.
6
www/.prettierrc.json
Normal file
6
www/.prettierrc.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/prettierrc",
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
39
www/README.md
Normal file
39
www/README.md
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# pucoti
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||||
|
|
||||||
|
## Type Support for `.vue` Imports in TS
|
||||||
|
|
||||||
|
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
|
||||||
|
|
||||||
|
## Customize configuration
|
||||||
|
|
||||||
|
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Hot-Reload for Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type-Check, Compile and Minify for Production
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lint with [ESLint](https://eslint.org/)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm lint
|
||||||
|
```
|
1
www/env.d.ts
vendored
Normal file
1
www/env.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="vite/client" />
|
24
www/eslint.config.ts
Normal file
24
www/eslint.config.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { globalIgnores } from 'eslint/config'
|
||||||
|
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
|
||||||
|
import pluginVue from 'eslint-plugin-vue'
|
||||||
|
import pluginOxlint from 'eslint-plugin-oxlint'
|
||||||
|
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
|
||||||
|
|
||||||
|
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
|
||||||
|
// import { configureVueProject } from '@vue/eslint-config-typescript'
|
||||||
|
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
|
||||||
|
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
|
||||||
|
|
||||||
|
export default defineConfigWithVueTs(
|
||||||
|
{
|
||||||
|
name: 'app/files-to-lint',
|
||||||
|
files: ['**/*.{ts,mts,tsx,vue}'],
|
||||||
|
},
|
||||||
|
|
||||||
|
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
|
||||||
|
|
||||||
|
pluginVue.configs['flat/essential'],
|
||||||
|
vueTsConfigs.recommended,
|
||||||
|
...pluginOxlint.configs['flat/recommended'],
|
||||||
|
skipFormatting,
|
||||||
|
)
|
BIN
www/favicon.ico
Normal file
BIN
www/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
20
www/index.html
Normal file
20
www/index.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Pucoti</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Bevan:ital@0;1&family=Inter:ital,wght@0,400..700;1,400..700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
44
www/package.json
Normal file
44
www/package.json
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"name": "pucoti",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "run-p type-check \"build-only {@}\" --",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"build-only": "vite build",
|
||||||
|
"type-check": "vue-tsc --build",
|
||||||
|
"lint:oxlint": "oxlint . --fix -D correctness --ignore-path .gitignore",
|
||||||
|
"lint:eslint": "eslint . --fix",
|
||||||
|
"lint": "run-s lint:*",
|
||||||
|
"format": "prettier --write src/"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.1.7",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
|
"pinia": "^3.0.1",
|
||||||
|
"tailwindcss": "^4.1.7",
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"vue-router": "^4.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tsconfig/node22": "^22.0.1",
|
||||||
|
"@types/node": "^22.14.0",
|
||||||
|
"@vitejs/plugin-vue": "^5.2.3",
|
||||||
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
|
"@vue/eslint-config-typescript": "^14.5.0",
|
||||||
|
"@vue/tsconfig": "^0.7.0",
|
||||||
|
"eslint": "^9.22.0",
|
||||||
|
"eslint-plugin-oxlint": "^0.16.0",
|
||||||
|
"eslint-plugin-vue": "~10.0.0",
|
||||||
|
"jiti": "^2.4.2",
|
||||||
|
"npm-run-all2": "^7.0.2",
|
||||||
|
"oxlint": "^0.16.0",
|
||||||
|
"prettier": "3.5.3",
|
||||||
|
"typescript": "~5.8.0",
|
||||||
|
"vite": "^6.2.4",
|
||||||
|
"vite-plugin-vue-devtools": "^7.7.2",
|
||||||
|
"vue-tsc": "^2.2.8"
|
||||||
|
}
|
||||||
|
}
|
3614
www/pnpm-lock.yaml
generated
Normal file
3614
www/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
BIN
www/public/bell.mp3
Normal file
BIN
www/public/bell.mp3
Normal file
Binary file not shown.
BIN
www/public/favicon.ico
Normal file
BIN
www/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
1
www/public/neutralino.js
Normal file
1
www/public/neutralino.js
Normal file
File diff suppressed because one or more lines are too long
42
www/src/App.vue
Normal file
42
www/src/App.vue
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { RouterLink, RouterView } from 'vue-router'
|
||||||
|
import { usePucotiStore } from './stores/counter'
|
||||||
|
import { humanTimeToMs } from './utils'
|
||||||
|
import { tickClock, useIntervalFn } from './lib'
|
||||||
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
const audio = new Audio('/bell.mp3')
|
||||||
|
|
||||||
|
const store = usePucotiStore()
|
||||||
|
store.setIntention('')
|
||||||
|
|
||||||
|
function checkTime() {
|
||||||
|
const now = Date.now()
|
||||||
|
const timeOnCountdown = store.timers.main.zeroAt - now
|
||||||
|
if (timeOnCountdown <= 0) {
|
||||||
|
if (now - store.lastRung > humanTimeToMs(store.ringEvery)) {
|
||||||
|
store.lastRung = now
|
||||||
|
audio.currentTime = 0
|
||||||
|
audio.play()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
store.lastRung = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useIntervalFn(tickClock, 1000)
|
||||||
|
useIntervalFn(checkTime, 500)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!--
|
||||||
|
<header>
|
||||||
|
<nav>
|
||||||
|
<RouterLink to="/">Home</RouterLink>
|
||||||
|
<RouterLink to="/about">About</RouterLink>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<RouterView />
|
||||||
|
</template>
|
32
www/src/assets/main.css
Normal file
32
www/src/assets/main.css
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
@import 'tailwindcss/preflight.css' layer(base);
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--font-sans: 'Inter', sans-serif;
|
||||||
|
--font-display: 'Bevan', sans-serif;
|
||||||
|
--color-light: oklch(0.9 0.1 88);
|
||||||
|
|
||||||
|
--color-dark: oklch(0.15 0.025 45);
|
||||||
|
/* Darker is actually less dark. You're welcome. */
|
||||||
|
--color-darker: oklch(0.2 0.025 45);
|
||||||
|
--color-acid: rgb(183, 255, 183);
|
||||||
|
|
||||||
|
--timer-negative: #f00;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-size: clamp(1rem, 1.5vw, 1.5rem);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
background-color: var(--color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
16
www/src/components/AppLogo.vue
Normal file
16
www/src/components/AppLogo.vue
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span>Pucoti</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
span {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
color: var(--color-light);
|
||||||
|
background-color: var(--color-dark);
|
||||||
|
padding: 0.25em 0.75em;
|
||||||
|
font-style: italic;
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
</style>
|
51
www/src/components/Button.vue
Normal file
51
www/src/components/Button.vue
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
const props = defineProps<{
|
||||||
|
shortcut?: string
|
||||||
|
label: string
|
||||||
|
outline?: boolean
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<button v-bind="$attrs" :class="{ outline: outline }">
|
||||||
|
<span>{{ label }}</span>
|
||||||
|
<kbd v-if="shortcut">{{ shortcut }}</kbd>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
button {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0 1vw;
|
||||||
|
|
||||||
|
padding: 0.5vw 0.8vw;
|
||||||
|
|
||||||
|
color: var(--color-light);
|
||||||
|
border: 1px solid color-mix(in oklab, var(--color-light) 20%, transparent);
|
||||||
|
background-image: linear-gradient(to top,
|
||||||
|
transparent 0%,
|
||||||
|
color-mix(in oklab, var(--color-light) 15%, var(--color-dark)) 90%);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.outline {
|
||||||
|
border: none;
|
||||||
|
background: var(--color-darker);
|
||||||
|
padding: 0.3em 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-family: monospace;
|
||||||
|
width: clamp(1.2em, 2vw, 5em);
|
||||||
|
height: clamp(1.2em, 2vw, 5em);
|
||||||
|
background-color: var(--color-dark);
|
||||||
|
border: 1px solid color-mix(in oklab, var(--color-light) 10%, transparent);
|
||||||
|
}
|
||||||
|
</style>
|
240
www/src/components/IntentionHistory.vue
Normal file
240
www/src/components/IntentionHistory.vue
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, nextTick } from 'vue'
|
||||||
|
import { usePucotiStore } from '@/stores/counter'
|
||||||
|
import { fmtSeconds } from '@/utils'
|
||||||
|
import Button from './Button.vue'
|
||||||
|
import type { IntentionHistoryItem } from '@/types'
|
||||||
|
|
||||||
|
const store = usePucotiStore()
|
||||||
|
|
||||||
|
// Reactive state for editing
|
||||||
|
const editingIndex = ref<number | null>(null)
|
||||||
|
const editingValue = ref<string>('')
|
||||||
|
|
||||||
|
// Computed property for completed intentions with duration
|
||||||
|
const completedIntentions = computed(() => {
|
||||||
|
let history = store.intentionHistory.filter(
|
||||||
|
(item) => item.intention != '' && item.end !== undefined,
|
||||||
|
)
|
||||||
|
return history
|
||||||
|
.map((item: IntentionHistoryItem, index: number) => ({
|
||||||
|
...item,
|
||||||
|
index,
|
||||||
|
duration: item.end ? item.end - item.start : 0,
|
||||||
|
isCompleted: !!item.end,
|
||||||
|
}))
|
||||||
|
.reverse() // Show most recent first
|
||||||
|
})
|
||||||
|
|
||||||
|
// Edit functionality
|
||||||
|
const startEditing = (index: number, currentText: string) => {
|
||||||
|
editingIndex.value = index
|
||||||
|
editingValue.value = currentText
|
||||||
|
nextTick(() => {
|
||||||
|
const input = document.querySelector(`input[data-edit-index="${index}"]`) as HTMLInputElement
|
||||||
|
if (input) {
|
||||||
|
input.focus()
|
||||||
|
input.select()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveEdit = (originalIndex: number) => {
|
||||||
|
if (editingValue.value.trim()) {
|
||||||
|
store.updateHistoricalIntention(originalIndex, editingValue.value.trim())
|
||||||
|
}
|
||||||
|
cancelEdit()
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelEdit = () => {
|
||||||
|
editingIndex.value = null
|
||||||
|
editingValue.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEditKeydown = (event: KeyboardEvent, originalIndex: number) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault()
|
||||||
|
saveEdit(originalIndex)
|
||||||
|
} else if (event.key === 'Escape') {
|
||||||
|
event.preventDefault()
|
||||||
|
cancelEdit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue functionality
|
||||||
|
const continueFromHistory = (intention: string) => {
|
||||||
|
store.continueFromHistory(intention)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format duration helper
|
||||||
|
const formatDuration = (milliseconds: number) => {
|
||||||
|
const parts = fmtSeconds(milliseconds)
|
||||||
|
return parts.join(':')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="history-section">
|
||||||
|
<h2 class="history-title">History</h2>
|
||||||
|
|
||||||
|
<div v-if="completedIntentions.length === 0" class="empty-state">
|
||||||
|
No intentions completed yet. Start working on something to see your history here.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="history-list">
|
||||||
|
<div
|
||||||
|
v-for="item in completedIntentions"
|
||||||
|
:key="`${item.index}-${item.start}`"
|
||||||
|
class="history-item"
|
||||||
|
>
|
||||||
|
<div class="history-item-content">
|
||||||
|
<div class="intention-text">
|
||||||
|
<input
|
||||||
|
v-if="editingIndex === item.index"
|
||||||
|
:data-edit-index="item.index"
|
||||||
|
v-model="editingValue"
|
||||||
|
class="intention-edit-input"
|
||||||
|
@keydown="handleEditKeydown($event, item.index)"
|
||||||
|
@blur="cancelEdit"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
<span v-else class="intention-display">{{ item.intention }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="history-actions">
|
||||||
|
<button
|
||||||
|
v-if="editingIndex !== item.index"
|
||||||
|
class="edit-button"
|
||||||
|
title="Edit intention"
|
||||||
|
@click="startEditing(item.index, item.intention)"
|
||||||
|
>
|
||||||
|
✏️
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="duration">{{ formatDuration(item.duration) }}</div>
|
||||||
|
|
||||||
|
<Button label="Continue" @click="continueFromHistory(item.intention)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.history-section {
|
||||||
|
margin-top: clamp(2em, 3vw, 100vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-title {
|
||||||
|
color: var(--color-acid);
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: clamp(1.5em, 2.5vw, 100vw);
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
color: color-mix(in oklab, var(--color-light) 80%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-list {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item {
|
||||||
|
background-color: color-mix(in oklab, var(--color-light) 5%, transparent);
|
||||||
|
border: 1px solid color-mix(in oklab, var(--color-light) 15%, transparent);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
padding: 1em;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item:hover {
|
||||||
|
background-color: color-mix(in oklab, var(--color-light) 8%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intention-text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0; /* Allow text to shrink */
|
||||||
|
}
|
||||||
|
|
||||||
|
.intention-display {
|
||||||
|
color: var(--color-light);
|
||||||
|
word-break: break-word;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intention-edit-input {
|
||||||
|
width: 100%;
|
||||||
|
background: color-mix(in oklab, var(--color-light) 10%, transparent);
|
||||||
|
border: 1px solid var(--color-acid);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--color-light);
|
||||||
|
padding: 0.25em 0.5em;
|
||||||
|
font-size: inherit;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intention-edit-input:focus {
|
||||||
|
border-color: var(--color-acid);
|
||||||
|
box-shadow: 0 0 0 2px color-mix(in oklab, var(--color-acid) 20%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75em;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.25em;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-button:hover {
|
||||||
|
background-color: color-mix(in oklab, var(--color-light) 15%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration {
|
||||||
|
color: var(--color-acid);
|
||||||
|
font-family: var(--font-mono, monospace);
|
||||||
|
font-size: 0.9em;
|
||||||
|
min-width: 4em;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar styling for webkit browsers */
|
||||||
|
.history-list::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-list::-webkit-scrollbar-track {
|
||||||
|
background: color-mix(in oklab, var(--color-light) 5%, transparent);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-list::-webkit-scrollbar-thumb {
|
||||||
|
background: color-mix(in oklab, var(--color-light) 20%, transparent);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-list::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: color-mix(in oklab, var(--color-light) 30%, transparent);
|
||||||
|
}
|
||||||
|
</style>
|
33
www/src/components/Timer.vue
Normal file
33
www/src/components/Timer.vue
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Timer } from '@/types'
|
||||||
|
import { useListenerFn } from '@/lib'
|
||||||
|
import { timerToMs, timerToString } from '@/utils'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
timer: Timer
|
||||||
|
color: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const displayTime = ref('00:00')
|
||||||
|
const color = ref('')
|
||||||
|
|
||||||
|
function handleClockTick() {
|
||||||
|
displayTime.value = timerToString(props.timer)
|
||||||
|
color.value = timerToMs(props.timer) < 0 ? 'var(--timer-negative)' : props.color
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(handleClockTick)
|
||||||
|
useListenerFn('clock-tick', handleClockTick)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span :style="{ color: color }">{{ displayTime }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
span {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: clamp(2em, 12vw, 100em);
|
||||||
|
}
|
||||||
|
</style>
|
80
www/src/lib.ts
Normal file
80
www/src/lib.ts
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
export function tickClock() {
|
||||||
|
document.dispatchEvent(new Event('clock-tick'))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the size metrics of a simple text element.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} el The element to get the text metrics for
|
||||||
|
* @param {string} fontSizeOverride The font size to use, in CSS units (e.g. '16px')
|
||||||
|
*
|
||||||
|
* @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
|
||||||
|
*/
|
||||||
|
export function getTextMetrics(el: HTMLElement, fontSizeOverride: string) {
|
||||||
|
// re-use canvas object for better performance
|
||||||
|
const canvas = getTextMetrics.canvas || (getTextMetrics.canvas = document.createElement('canvas'))
|
||||||
|
const context = canvas.getContext('2d')
|
||||||
|
|
||||||
|
const fontWeight = getCssStyle(el, 'font-weight') || 'normal'
|
||||||
|
const fontSize = fontSizeOverride || getCssStyle(el, 'font-size')
|
||||||
|
const fontFamily = getCssStyle(el, 'font-family') || 'Times New Roman'
|
||||||
|
context.font = `${fontWeight} ${fontSize} ${fontFamily}`
|
||||||
|
|
||||||
|
const text = el.innerText || el.value || el.getAttribute('placeholder')
|
||||||
|
|
||||||
|
const metrics = context.measureText(text)
|
||||||
|
metrics.actualHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent
|
||||||
|
return metrics
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCssStyle(element: HTMLElement, prop: string) {
|
||||||
|
return window.getComputedStyle(element, null).getPropertyValue(prop)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scale the font size of the element to the maximum that fits inside the element.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} element
|
||||||
|
*/
|
||||||
|
export function scaleMaxSize(element: HTMLElement) {
|
||||||
|
let fontSizeLower = 0
|
||||||
|
let fontSizeUpper = 2000
|
||||||
|
|
||||||
|
// Actual available, without the padding.lib.
|
||||||
|
const width =
|
||||||
|
element.clientWidth -
|
||||||
|
parseFloat(getCssStyle(element, 'padding-left')) -
|
||||||
|
parseFloat(getCssStyle(element, 'padding-right'))
|
||||||
|
const height =
|
||||||
|
element.clientHeight -
|
||||||
|
parseFloat(getCssStyle(element, 'padding-top')) -
|
||||||
|
parseFloat(getCssStyle(element, 'padding-bottom'))
|
||||||
|
|
||||||
|
// Simple binary search to find the largest font size that fits
|
||||||
|
while (fontSizeUpper > fontSizeLower + 1) {
|
||||||
|
const fontSize = Math.floor((fontSizeUpper + fontSizeLower) / 2)
|
||||||
|
const metrics = getTextMetrics(element, fontSize + 'px')
|
||||||
|
if (metrics.width > width || metrics.actualHeight > height) {
|
||||||
|
fontSizeUpper = fontSize
|
||||||
|
} else {
|
||||||
|
fontSizeLower = fontSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log(getTextMetrics(element, fontSizeLower + 'px'), element, "width", width, "height", height);
|
||||||
|
|
||||||
|
element.style.fontSize = fontSizeLower + 'px'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useIntervalFn(cb: () => void, ms: number) {
|
||||||
|
let intervalId: number = 0
|
||||||
|
onMounted(() => (intervalId = setInterval(cb, ms)))
|
||||||
|
onUnmounted(() => clearInterval(intervalId))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useListenerFn(event: string, cb: () => void) {
|
||||||
|
onMounted(() => document.addEventListener(event, cb))
|
||||||
|
onUnmounted(() => document.removeEventListener(event, cb))
|
||||||
|
}
|
14
www/src/main.ts
Normal file
14
www/src/main.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import './assets/main.css'
|
||||||
|
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(createPinia())
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
app.mount('#app')
|
897
www/src/neutralino.d.ts
vendored
Normal file
897
www/src/neutralino.d.ts
vendored
Normal file
|
@ -0,0 +1,897 @@
|
||||||
|
// Type definitions for Neutralino 6.1.0
|
||||||
|
// Project: https://github.com/neutralinojs
|
||||||
|
// Definitions project: https://github.com/neutralinojs/neutralino.js
|
||||||
|
|
||||||
|
declare namespace Neutralino {
|
||||||
|
|
||||||
|
namespace filesystem {
|
||||||
|
interface DirectoryEntry {
|
||||||
|
entry: string;
|
||||||
|
path: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
interface FileReaderOptions {
|
||||||
|
pos: number;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
interface DirectoryReaderOptions {
|
||||||
|
recursive: boolean;
|
||||||
|
}
|
||||||
|
interface OpenedFile {
|
||||||
|
id: number;
|
||||||
|
eof: boolean;
|
||||||
|
pos: number;
|
||||||
|
lastRead: number;
|
||||||
|
}
|
||||||
|
interface Stats {
|
||||||
|
size: number;
|
||||||
|
isFile: boolean;
|
||||||
|
isDirectory: boolean;
|
||||||
|
createdAt: number;
|
||||||
|
modifiedAt: number;
|
||||||
|
}
|
||||||
|
interface Watcher {
|
||||||
|
id: number;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
interface CopyOptions {
|
||||||
|
recursive: boolean;
|
||||||
|
overwrite: boolean;
|
||||||
|
skip: boolean;
|
||||||
|
}
|
||||||
|
interface PathParts {
|
||||||
|
rootName: string;
|
||||||
|
rootDirectory: string;
|
||||||
|
rootPath: string;
|
||||||
|
relativePath: string;
|
||||||
|
parentPath: string;
|
||||||
|
filename: string;
|
||||||
|
stem: string;
|
||||||
|
extension: string;
|
||||||
|
}
|
||||||
|
interface Permissions {
|
||||||
|
all: boolean;
|
||||||
|
ownerAll: boolean;
|
||||||
|
ownerRead: boolean;
|
||||||
|
ownerWrite: boolean;
|
||||||
|
ownerExec: boolean;
|
||||||
|
groupAll: boolean;
|
||||||
|
groupRead: boolean;
|
||||||
|
groupWrite: boolean;
|
||||||
|
groupExec: boolean;
|
||||||
|
othersAll: boolean;
|
||||||
|
othersRead: boolean;
|
||||||
|
othersWrite: boolean;
|
||||||
|
othersExec: boolean;
|
||||||
|
}
|
||||||
|
type PermissionsMode = "ADD" | "REPLACE" | "REMOVE";
|
||||||
|
function createDirectory(path: string): Promise<void>;
|
||||||
|
function remove(path: string): Promise<void>;
|
||||||
|
function writeFile(path: string, data: string): Promise<void>;
|
||||||
|
function appendFile(path: string, data: string): Promise<void>;
|
||||||
|
function writeBinaryFile(path: string, data: ArrayBuffer): Promise<void>;
|
||||||
|
function appendBinaryFile(path: string, data: ArrayBuffer): Promise<void>;
|
||||||
|
function readFile(path: string, options?: FileReaderOptions): Promise<string>;
|
||||||
|
function readBinaryFile(path: string, options?: FileReaderOptions): Promise<ArrayBuffer>;
|
||||||
|
function openFile(path: string): Promise<number>;
|
||||||
|
function createWatcher(path: string): Promise<number>;
|
||||||
|
function removeWatcher(id: number): Promise<number>;
|
||||||
|
function getWatchers(): Promise<Watcher[]>;
|
||||||
|
function updateOpenedFile(id: number, event: string, data?: any): Promise<void>;
|
||||||
|
function getOpenedFileInfo(id: number): Promise<OpenedFile>;
|
||||||
|
function readDirectory(path: string, options?: DirectoryReaderOptions): Promise<DirectoryEntry[]>;
|
||||||
|
function copy(source: string, destination: string, options?: CopyOptions): Promise<void>;
|
||||||
|
function move(source: string, destination: string): Promise<void>;
|
||||||
|
function getStats(path: string): Promise<Stats>;
|
||||||
|
function getAbsolutePath(path: string): Promise<string>;
|
||||||
|
function getRelativePath(path: string, base?: string): Promise<string>;
|
||||||
|
function getPathParts(path: string): Promise<PathParts>;
|
||||||
|
function getPermissions(path: string): Promise<Permissions>;
|
||||||
|
function setPermissions(path: string, permissions: Permissions, mode: PermissionsMode): Promise<void>;
|
||||||
|
}
|
||||||
|
namespace os {
|
||||||
|
// debug
|
||||||
|
enum LoggerType {
|
||||||
|
WARNING = "WARNING",
|
||||||
|
ERROR = "ERROR",
|
||||||
|
INFO = "INFO"
|
||||||
|
}
|
||||||
|
// os
|
||||||
|
enum Icon {
|
||||||
|
WARNING = "WARNING",
|
||||||
|
ERROR = "ERROR",
|
||||||
|
INFO = "INFO",
|
||||||
|
QUESTION = "QUESTION"
|
||||||
|
}
|
||||||
|
enum MessageBoxChoice {
|
||||||
|
OK = "OK",
|
||||||
|
OK_CANCEL = "OK_CANCEL",
|
||||||
|
YES_NO = "YES_NO",
|
||||||
|
YES_NO_CANCEL = "YES_NO_CANCEL",
|
||||||
|
RETRY_CANCEL = "RETRY_CANCEL",
|
||||||
|
ABORT_RETRY_IGNORE = "ABORT_RETRY_IGNORE"
|
||||||
|
}
|
||||||
|
//clipboard
|
||||||
|
enum ClipboardFormat {
|
||||||
|
unknown = "unknown",
|
||||||
|
text = "text",
|
||||||
|
image = "image"
|
||||||
|
}
|
||||||
|
// NL_GLOBALS
|
||||||
|
enum Mode {
|
||||||
|
window = "window",
|
||||||
|
browser = "browser",
|
||||||
|
cloud = "cloud",
|
||||||
|
chrome = "chrome"
|
||||||
|
}
|
||||||
|
enum OperatingSystem {
|
||||||
|
Linux = "Linux",
|
||||||
|
Windows = "Windows",
|
||||||
|
Darwin = "Darwin",
|
||||||
|
FreeBSD = "FreeBSD",
|
||||||
|
Unknown = "Unknown"
|
||||||
|
}
|
||||||
|
enum Architecture {
|
||||||
|
x64 = "x64",
|
||||||
|
arm = "arm",
|
||||||
|
itanium = "itanium",
|
||||||
|
ia32 = "ia32",
|
||||||
|
unknown = "unknown"
|
||||||
|
}
|
||||||
|
interface ExecCommandOptions {
|
||||||
|
stdIn?: string;
|
||||||
|
background?: boolean;
|
||||||
|
cwd?: string;
|
||||||
|
}
|
||||||
|
interface ExecCommandResult {
|
||||||
|
pid: number;
|
||||||
|
stdOut: string;
|
||||||
|
stdErr: string;
|
||||||
|
exitCode: number;
|
||||||
|
}
|
||||||
|
interface SpawnedProcess {
|
||||||
|
id: number;
|
||||||
|
pid: number;
|
||||||
|
}
|
||||||
|
interface SpawnedProcessOptions {
|
||||||
|
cwd?: string;
|
||||||
|
envs?: Record<string, string>;
|
||||||
|
}
|
||||||
|
interface Envs {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
interface OpenDialogOptions {
|
||||||
|
multiSelections?: boolean;
|
||||||
|
filters?: Filter[];
|
||||||
|
defaultPath?: string;
|
||||||
|
}
|
||||||
|
interface FolderDialogOptions {
|
||||||
|
defaultPath?: string;
|
||||||
|
}
|
||||||
|
interface SaveDialogOptions {
|
||||||
|
forceOverwrite?: boolean;
|
||||||
|
filters?: Filter[];
|
||||||
|
defaultPath?: string;
|
||||||
|
}
|
||||||
|
interface Filter {
|
||||||
|
name: string;
|
||||||
|
extensions: string[];
|
||||||
|
}
|
||||||
|
interface TrayOptions {
|
||||||
|
icon: string;
|
||||||
|
menuItems: TrayMenuItem[];
|
||||||
|
}
|
||||||
|
interface TrayMenuItem {
|
||||||
|
id?: string;
|
||||||
|
text: string;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
isChecked?: boolean;
|
||||||
|
}
|
||||||
|
type KnownPath = "config" | "data" | "cache" | "documents" | "pictures" | "music" | "video" | "downloads" | "savedGames1" | "savedGames2" | "temp";
|
||||||
|
function execCommand(command: string, options?: ExecCommandOptions): Promise<ExecCommandResult>;
|
||||||
|
function spawnProcess(command: string, options?: SpawnedProcessOptions): Promise<SpawnedProcess>;
|
||||||
|
function updateSpawnedProcess(id: number, event: string, data?: any): Promise<void>;
|
||||||
|
function getSpawnedProcesses(): Promise<SpawnedProcess[]>;
|
||||||
|
function getEnv(key: string): Promise<string>;
|
||||||
|
function getEnvs(): Promise<Envs>;
|
||||||
|
function showOpenDialog(title?: string, options?: OpenDialogOptions): Promise<string[]>;
|
||||||
|
function showFolderDialog(title?: string, options?: FolderDialogOptions): Promise<string>;
|
||||||
|
function showSaveDialog(title?: string, options?: SaveDialogOptions): Promise<string>;
|
||||||
|
function showNotification(title: string, content: string, icon?: Icon): Promise<void>;
|
||||||
|
function showMessageBox(title: string, content: string, choice?: MessageBoxChoice, icon?: Icon): Promise<string>;
|
||||||
|
function setTray(options: TrayOptions): Promise<void>;
|
||||||
|
function open(url: string): Promise<void>;
|
||||||
|
function getPath(name: KnownPath): Promise<string>;
|
||||||
|
}
|
||||||
|
namespace computer {
|
||||||
|
interface MemoryInfo {
|
||||||
|
physical: {
|
||||||
|
total: number;
|
||||||
|
available: number;
|
||||||
|
};
|
||||||
|
virtual: {
|
||||||
|
total: number;
|
||||||
|
available: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
interface KernelInfo {
|
||||||
|
variant: string;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
interface OSInfo {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
interface CPUInfo {
|
||||||
|
vendor: string;
|
||||||
|
model: string;
|
||||||
|
frequency: number;
|
||||||
|
architecture: string;
|
||||||
|
logicalThreads: number;
|
||||||
|
physicalCores: number;
|
||||||
|
physicalUnits: number;
|
||||||
|
}
|
||||||
|
interface Display {
|
||||||
|
id: number;
|
||||||
|
resolution: Resolution;
|
||||||
|
dpi: number;
|
||||||
|
bpp: number;
|
||||||
|
refreshRate: number;
|
||||||
|
}
|
||||||
|
interface Resolution {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
interface MousePosition {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
function getMemoryInfo(): Promise<MemoryInfo>;
|
||||||
|
function getArch(): Promise<string>;
|
||||||
|
function getKernelInfo(): Promise<KernelInfo>;
|
||||||
|
function getOSInfo(): Promise<OSInfo>;
|
||||||
|
function getCPUInfo(): Promise<CPUInfo>;
|
||||||
|
function getDisplays(): Promise<Display[]>;
|
||||||
|
function getMousePosition(): Promise<MousePosition>;
|
||||||
|
}
|
||||||
|
namespace storage {
|
||||||
|
function setData(key: string, data: string): Promise<void>;
|
||||||
|
function getData(key: string): Promise<string>;
|
||||||
|
function getKeys(): Promise<string[]>;
|
||||||
|
}
|
||||||
|
namespace debug {
|
||||||
|
// debug
|
||||||
|
enum LoggerType {
|
||||||
|
WARNING = "WARNING",
|
||||||
|
ERROR = "ERROR",
|
||||||
|
INFO = "INFO"
|
||||||
|
}
|
||||||
|
// os
|
||||||
|
enum Icon {
|
||||||
|
WARNING = "WARNING",
|
||||||
|
ERROR = "ERROR",
|
||||||
|
INFO = "INFO",
|
||||||
|
QUESTION = "QUESTION"
|
||||||
|
}
|
||||||
|
enum MessageBoxChoice {
|
||||||
|
OK = "OK",
|
||||||
|
OK_CANCEL = "OK_CANCEL",
|
||||||
|
YES_NO = "YES_NO",
|
||||||
|
YES_NO_CANCEL = "YES_NO_CANCEL",
|
||||||
|
RETRY_CANCEL = "RETRY_CANCEL",
|
||||||
|
ABORT_RETRY_IGNORE = "ABORT_RETRY_IGNORE"
|
||||||
|
}
|
||||||
|
//clipboard
|
||||||
|
enum ClipboardFormat {
|
||||||
|
unknown = "unknown",
|
||||||
|
text = "text",
|
||||||
|
image = "image"
|
||||||
|
}
|
||||||
|
// NL_GLOBALS
|
||||||
|
enum Mode {
|
||||||
|
window = "window",
|
||||||
|
browser = "browser",
|
||||||
|
cloud = "cloud",
|
||||||
|
chrome = "chrome"
|
||||||
|
}
|
||||||
|
enum OperatingSystem {
|
||||||
|
Linux = "Linux",
|
||||||
|
Windows = "Windows",
|
||||||
|
Darwin = "Darwin",
|
||||||
|
FreeBSD = "FreeBSD",
|
||||||
|
Unknown = "Unknown"
|
||||||
|
}
|
||||||
|
enum Architecture {
|
||||||
|
x64 = "x64",
|
||||||
|
arm = "arm",
|
||||||
|
itanium = "itanium",
|
||||||
|
ia32 = "ia32",
|
||||||
|
unknown = "unknown"
|
||||||
|
}
|
||||||
|
function log(message: string, type?: LoggerType): Promise<void>;
|
||||||
|
}
|
||||||
|
namespace app {
|
||||||
|
interface OpenActionOptions {
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
interface RestartOptions {
|
||||||
|
args: string;
|
||||||
|
}
|
||||||
|
function exit(code?: number): Promise<void>;
|
||||||
|
function killProcess(): Promise<void>;
|
||||||
|
function restartProcess(options?: RestartOptions): Promise<void>;
|
||||||
|
function getConfig(): Promise<any>;
|
||||||
|
function broadcast(event: string, data?: any): Promise<void>;
|
||||||
|
function readProcessInput(readAll?: boolean): Promise<string>;
|
||||||
|
function writeProcessOutput(data: string): Promise<void>;
|
||||||
|
function writeProcessError(data: string): Promise<void>;
|
||||||
|
}
|
||||||
|
namespace window {
|
||||||
|
interface WindowOptions extends WindowSizeOptions, WindowPosOptions {
|
||||||
|
title?: string;
|
||||||
|
icon?: string;
|
||||||
|
fullScreen?: boolean;
|
||||||
|
alwaysOnTop?: boolean;
|
||||||
|
enableInspector?: boolean;
|
||||||
|
borderless?: boolean;
|
||||||
|
maximize?: boolean;
|
||||||
|
hidden?: boolean;
|
||||||
|
maximizable?: boolean;
|
||||||
|
useSavedState?: boolean;
|
||||||
|
exitProcessOnClose?: boolean;
|
||||||
|
extendUserAgentWith?: string;
|
||||||
|
injectGlobals?: boolean;
|
||||||
|
injectClientLibrary?: boolean;
|
||||||
|
injectScript?: string;
|
||||||
|
processArgs?: string;
|
||||||
|
}
|
||||||
|
interface WindowSizeOptions {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
minWidth?: number;
|
||||||
|
minHeight?: number;
|
||||||
|
maxWidth?: number;
|
||||||
|
maxHeight?: number;
|
||||||
|
resizable?: boolean;
|
||||||
|
}
|
||||||
|
interface WindowPosOptions {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
interface WindowMenu extends Array<WindowMenuItem> {
|
||||||
|
}
|
||||||
|
interface WindowMenuItem {
|
||||||
|
id?: string;
|
||||||
|
text: string;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
isChecked?: boolean;
|
||||||
|
menuItems?: WindowMenuItem[];
|
||||||
|
}
|
||||||
|
function setTitle(title: string): Promise<void>;
|
||||||
|
function getTitle(): Promise<string>;
|
||||||
|
function maximize(): Promise<void>;
|
||||||
|
function unmaximize(): Promise<void>;
|
||||||
|
function isMaximized(): Promise<boolean>;
|
||||||
|
function minimize(): Promise<void>;
|
||||||
|
function unminimize(): Promise<void>;
|
||||||
|
function isMinimized(): Promise<boolean>;
|
||||||
|
function setFullScreen(): Promise<void>;
|
||||||
|
function exitFullScreen(): Promise<void>;
|
||||||
|
function isFullScreen(): Promise<boolean>;
|
||||||
|
function show(): Promise<void>;
|
||||||
|
function hide(): Promise<void>;
|
||||||
|
function isVisible(): Promise<boolean>;
|
||||||
|
function focus(): Promise<void>;
|
||||||
|
function setIcon(icon: string): Promise<void>;
|
||||||
|
function move(x: number, y: number): Promise<void>;
|
||||||
|
function center(): Promise<void>;
|
||||||
|
type DraggableRegionOptions = {
|
||||||
|
/**
|
||||||
|
* If set to `true`, the region will always capture the pointer,
|
||||||
|
* ensuring dragging doesn't break on fast pointer movement.
|
||||||
|
* Note that it prevents child elements from receiving any pointer events.
|
||||||
|
* Defaults to `false`.
|
||||||
|
*/
|
||||||
|
alwaysCapture?: boolean;
|
||||||
|
/**
|
||||||
|
* Minimum distance between cursor's starting and current position
|
||||||
|
* after which dragging is started. This helps prevent accidental dragging
|
||||||
|
* while interacting with child elements.
|
||||||
|
* Defaults to `10`. (In pixels.)
|
||||||
|
*/
|
||||||
|
dragMinDistance?: number;
|
||||||
|
};
|
||||||
|
function setDraggableRegion(domElementOrId: string | HTMLElement, options?: DraggableRegionOptions): Promise<{
|
||||||
|
success: true;
|
||||||
|
message: string;
|
||||||
|
}>;
|
||||||
|
function unsetDraggableRegion(domElementOrId: string | HTMLElement): Promise<{
|
||||||
|
success: true;
|
||||||
|
message: string;
|
||||||
|
}>;
|
||||||
|
function setSize(options: WindowSizeOptions): Promise<void>;
|
||||||
|
function getSize(): Promise<WindowSizeOptions>;
|
||||||
|
function getPosition(): Promise<WindowPosOptions>;
|
||||||
|
function setAlwaysOnTop(onTop: boolean): Promise<void>;
|
||||||
|
function create(url: string, options?: WindowOptions): Promise<void>;
|
||||||
|
function snapshot(path: string): Promise<void>;
|
||||||
|
function setMainMenu(options: WindowMenu): Promise<void>;
|
||||||
|
}
|
||||||
|
namespace events {
|
||||||
|
interface Response {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
type Builtin = "ready" | "trayMenuItemClicked" | "windowClose" | "serverOffline" | "clientConnect" | "clientDisconnect" | "appClientConnect" | "appClientDisconnect" | "extClientConnect" | "extClientDisconnect" | "extensionReady" | "neuDev_reloadApp";
|
||||||
|
function on(event: string, handler: (ev: CustomEvent) => void): Promise<Response>;
|
||||||
|
function off(event: string, handler: (ev: CustomEvent) => void): Promise<Response>;
|
||||||
|
function dispatch(event: string, data?: any): Promise<Response>;
|
||||||
|
function broadcast(event: string, data?: any): Promise<void>;
|
||||||
|
}
|
||||||
|
namespace extensions {
|
||||||
|
interface ExtensionStats {
|
||||||
|
loaded: string[];
|
||||||
|
connected: string[];
|
||||||
|
}
|
||||||
|
function dispatch(extensionId: string, event: string, data?: any): Promise<void>;
|
||||||
|
function broadcast(event: string, data?: any): Promise<void>;
|
||||||
|
function getStats(): Promise<ExtensionStats>;
|
||||||
|
}
|
||||||
|
namespace updater {
|
||||||
|
interface Manifest {
|
||||||
|
applicationId: string;
|
||||||
|
version: string;
|
||||||
|
resourcesURL: string;
|
||||||
|
}
|
||||||
|
function checkForUpdates(url: string): Promise<Manifest>;
|
||||||
|
function install(): Promise<void>;
|
||||||
|
}
|
||||||
|
namespace clipboard {
|
||||||
|
interface ClipboardImage {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
bpp: number;
|
||||||
|
bpr: number;
|
||||||
|
redMask: number;
|
||||||
|
greenMask: number;
|
||||||
|
blueMask: number;
|
||||||
|
redShift: number;
|
||||||
|
greenShift: number;
|
||||||
|
blueShift: number;
|
||||||
|
data: ArrayBuffer;
|
||||||
|
}
|
||||||
|
// debug
|
||||||
|
enum LoggerType {
|
||||||
|
WARNING = "WARNING",
|
||||||
|
ERROR = "ERROR",
|
||||||
|
INFO = "INFO"
|
||||||
|
}
|
||||||
|
// os
|
||||||
|
enum Icon {
|
||||||
|
WARNING = "WARNING",
|
||||||
|
ERROR = "ERROR",
|
||||||
|
INFO = "INFO",
|
||||||
|
QUESTION = "QUESTION"
|
||||||
|
}
|
||||||
|
enum MessageBoxChoice {
|
||||||
|
OK = "OK",
|
||||||
|
OK_CANCEL = "OK_CANCEL",
|
||||||
|
YES_NO = "YES_NO",
|
||||||
|
YES_NO_CANCEL = "YES_NO_CANCEL",
|
||||||
|
RETRY_CANCEL = "RETRY_CANCEL",
|
||||||
|
ABORT_RETRY_IGNORE = "ABORT_RETRY_IGNORE"
|
||||||
|
}
|
||||||
|
//clipboard
|
||||||
|
enum ClipboardFormat {
|
||||||
|
unknown = "unknown",
|
||||||
|
text = "text",
|
||||||
|
image = "image"
|
||||||
|
}
|
||||||
|
// NL_GLOBALS
|
||||||
|
enum Mode {
|
||||||
|
window = "window",
|
||||||
|
browser = "browser",
|
||||||
|
cloud = "cloud",
|
||||||
|
chrome = "chrome"
|
||||||
|
}
|
||||||
|
enum OperatingSystem {
|
||||||
|
Linux = "Linux",
|
||||||
|
Windows = "Windows",
|
||||||
|
Darwin = "Darwin",
|
||||||
|
FreeBSD = "FreeBSD",
|
||||||
|
Unknown = "Unknown"
|
||||||
|
}
|
||||||
|
enum Architecture {
|
||||||
|
x64 = "x64",
|
||||||
|
arm = "arm",
|
||||||
|
itanium = "itanium",
|
||||||
|
ia32 = "ia32",
|
||||||
|
unknown = "unknown"
|
||||||
|
}
|
||||||
|
function getFormat(): Promise<ClipboardFormat>;
|
||||||
|
function readText(): Promise<string>;
|
||||||
|
function readImage(format?: string): Promise<ClipboardImage | null>;
|
||||||
|
function writeText(data: string): Promise<void>;
|
||||||
|
function writeImage(image: ClipboardImage): Promise<void>;
|
||||||
|
function readHTML(): Promise<string>;
|
||||||
|
function writeHTML(data: string): Promise<void>;
|
||||||
|
function clear(): Promise<void>;
|
||||||
|
}
|
||||||
|
namespace resources {
|
||||||
|
interface Stats {
|
||||||
|
size: number;
|
||||||
|
isFile: boolean;
|
||||||
|
isDirectory: boolean;
|
||||||
|
}
|
||||||
|
function getFiles(): Promise<string[]>;
|
||||||
|
function getStats(path: string): Promise<Stats>;
|
||||||
|
function extractFile(path: string, destination: string): Promise<void>;
|
||||||
|
function extractDirectory(path: string, destination: string): Promise<void>;
|
||||||
|
function readFile(path: string): Promise<string>;
|
||||||
|
function readBinaryFile(path: string): Promise<ArrayBuffer>;
|
||||||
|
}
|
||||||
|
namespace server {
|
||||||
|
function mount(path: string, target: string): Promise<void>;
|
||||||
|
function unmount(path: string): Promise<void>;
|
||||||
|
function getMounts(): Promise<void>;
|
||||||
|
}
|
||||||
|
namespace custom {
|
||||||
|
function getMethods(): Promise<string[]>;
|
||||||
|
}
|
||||||
|
interface InitOptions {
|
||||||
|
exportCustomMethods?: boolean;
|
||||||
|
}
|
||||||
|
function init(options?: InitOptions): void;
|
||||||
|
type ErrorCode = "NE_FS_DIRCRER" | "NE_FS_RMDIRER" | "NE_FS_FILRDER" | "NE_FS_FILWRER" | "NE_FS_FILRMER" | "NE_FS_NOPATHE" | "NE_FS_COPYFER" | "NE_FS_MOVEFER" | "NE_OS_INVMSGA" | "NE_OS_INVKNPT" | "NE_ST_INVSTKY" | "NE_ST_STKEYWE" | "NE_RT_INVTOKN" | "NE_RT_NATPRME" | "NE_RT_APIPRME" | "NE_RT_NATRTER" | "NE_RT_NATNTIM" | "NE_CL_NSEROFF" | "NE_EX_EXTNOTC" | "NE_UP_CUPDMER" | "NE_UP_CUPDERR" | "NE_UP_UPDNOUF" | "NE_UP_UPDINER";
|
||||||
|
interface Error {
|
||||||
|
code: ErrorCode;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
interface OpenActionOptions {
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
interface RestartOptions {
|
||||||
|
args: string;
|
||||||
|
}
|
||||||
|
interface MemoryInfo {
|
||||||
|
physical: {
|
||||||
|
total: number;
|
||||||
|
available: number;
|
||||||
|
};
|
||||||
|
virtual: {
|
||||||
|
total: number;
|
||||||
|
available: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
interface KernelInfo {
|
||||||
|
variant: string;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
interface OSInfo {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
interface CPUInfo {
|
||||||
|
vendor: string;
|
||||||
|
model: string;
|
||||||
|
frequency: number;
|
||||||
|
architecture: string;
|
||||||
|
logicalThreads: number;
|
||||||
|
physicalCores: number;
|
||||||
|
physicalUnits: number;
|
||||||
|
}
|
||||||
|
interface Display {
|
||||||
|
id: number;
|
||||||
|
resolution: Resolution;
|
||||||
|
dpi: number;
|
||||||
|
bpp: number;
|
||||||
|
refreshRate: number;
|
||||||
|
}
|
||||||
|
interface Resolution {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
interface MousePosition {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
interface ClipboardImage {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
bpp: number;
|
||||||
|
bpr: number;
|
||||||
|
redMask: number;
|
||||||
|
greenMask: number;
|
||||||
|
blueMask: number;
|
||||||
|
redShift: number;
|
||||||
|
greenShift: number;
|
||||||
|
blueShift: number;
|
||||||
|
data: ArrayBuffer;
|
||||||
|
}
|
||||||
|
interface ExtensionStats {
|
||||||
|
loaded: string[];
|
||||||
|
connected: string[];
|
||||||
|
}
|
||||||
|
interface DirectoryEntry {
|
||||||
|
entry: string;
|
||||||
|
path: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
interface FileReaderOptions {
|
||||||
|
pos: number;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
interface DirectoryReaderOptions {
|
||||||
|
recursive: boolean;
|
||||||
|
}
|
||||||
|
interface OpenedFile {
|
||||||
|
id: number;
|
||||||
|
eof: boolean;
|
||||||
|
pos: number;
|
||||||
|
lastRead: number;
|
||||||
|
}
|
||||||
|
interface Stats {
|
||||||
|
size: number;
|
||||||
|
isFile: boolean;
|
||||||
|
isDirectory: boolean;
|
||||||
|
createdAt: number;
|
||||||
|
modifiedAt: number;
|
||||||
|
}
|
||||||
|
interface Watcher {
|
||||||
|
id: number;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
interface CopyOptions {
|
||||||
|
recursive: boolean;
|
||||||
|
overwrite: boolean;
|
||||||
|
skip: boolean;
|
||||||
|
}
|
||||||
|
interface PathParts {
|
||||||
|
rootName: string;
|
||||||
|
rootDirectory: string;
|
||||||
|
rootPath: string;
|
||||||
|
relativePath: string;
|
||||||
|
parentPath: string;
|
||||||
|
filename: string;
|
||||||
|
stem: string;
|
||||||
|
extension: string;
|
||||||
|
}
|
||||||
|
interface Permissions {
|
||||||
|
all: boolean;
|
||||||
|
ownerAll: boolean;
|
||||||
|
ownerRead: boolean;
|
||||||
|
ownerWrite: boolean;
|
||||||
|
ownerExec: boolean;
|
||||||
|
groupAll: boolean;
|
||||||
|
groupRead: boolean;
|
||||||
|
groupWrite: boolean;
|
||||||
|
groupExec: boolean;
|
||||||
|
othersAll: boolean;
|
||||||
|
othersRead: boolean;
|
||||||
|
othersWrite: boolean;
|
||||||
|
othersExec: boolean;
|
||||||
|
}
|
||||||
|
type PermissionsMode = "ADD" | "REPLACE" | "REMOVE";
|
||||||
|
interface ExecCommandOptions {
|
||||||
|
stdIn?: string;
|
||||||
|
background?: boolean;
|
||||||
|
cwd?: string;
|
||||||
|
}
|
||||||
|
interface ExecCommandResult {
|
||||||
|
pid: number;
|
||||||
|
stdOut: string;
|
||||||
|
stdErr: string;
|
||||||
|
exitCode: number;
|
||||||
|
}
|
||||||
|
interface SpawnedProcess {
|
||||||
|
id: number;
|
||||||
|
pid: number;
|
||||||
|
}
|
||||||
|
interface SpawnedProcessOptions {
|
||||||
|
cwd?: string;
|
||||||
|
envs?: Record<string, string>;
|
||||||
|
}
|
||||||
|
interface Envs {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
interface OpenDialogOptions {
|
||||||
|
multiSelections?: boolean;
|
||||||
|
filters?: Filter[];
|
||||||
|
defaultPath?: string;
|
||||||
|
}
|
||||||
|
interface FolderDialogOptions {
|
||||||
|
defaultPath?: string;
|
||||||
|
}
|
||||||
|
interface SaveDialogOptions {
|
||||||
|
forceOverwrite?: boolean;
|
||||||
|
filters?: Filter[];
|
||||||
|
defaultPath?: string;
|
||||||
|
}
|
||||||
|
interface Filter {
|
||||||
|
name: string;
|
||||||
|
extensions: string[];
|
||||||
|
}
|
||||||
|
interface TrayOptions {
|
||||||
|
icon: string;
|
||||||
|
menuItems: TrayMenuItem[];
|
||||||
|
}
|
||||||
|
interface TrayMenuItem {
|
||||||
|
id?: string;
|
||||||
|
text: string;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
isChecked?: boolean;
|
||||||
|
}
|
||||||
|
type KnownPath = "config" | "data" | "cache" | "documents" | "pictures" | "music" | "video" | "downloads" | "savedGames1" | "savedGames2" | "temp";
|
||||||
|
interface Manifest {
|
||||||
|
applicationId: string;
|
||||||
|
version: string;
|
||||||
|
resourcesURL: string;
|
||||||
|
}
|
||||||
|
interface WindowOptions extends WindowSizeOptions, WindowPosOptions {
|
||||||
|
title?: string;
|
||||||
|
icon?: string;
|
||||||
|
fullScreen?: boolean;
|
||||||
|
alwaysOnTop?: boolean;
|
||||||
|
enableInspector?: boolean;
|
||||||
|
borderless?: boolean;
|
||||||
|
maximize?: boolean;
|
||||||
|
hidden?: boolean;
|
||||||
|
maximizable?: boolean;
|
||||||
|
useSavedState?: boolean;
|
||||||
|
exitProcessOnClose?: boolean;
|
||||||
|
extendUserAgentWith?: string;
|
||||||
|
injectGlobals?: boolean;
|
||||||
|
injectClientLibrary?: boolean;
|
||||||
|
injectScript?: string;
|
||||||
|
processArgs?: string;
|
||||||
|
}
|
||||||
|
interface WindowSizeOptions {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
minWidth?: number;
|
||||||
|
minHeight?: number;
|
||||||
|
maxWidth?: number;
|
||||||
|
maxHeight?: number;
|
||||||
|
resizable?: boolean;
|
||||||
|
}
|
||||||
|
interface WindowPosOptions {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
interface WindowMenu extends Array<WindowMenuItem> {
|
||||||
|
}
|
||||||
|
interface WindowMenuItem {
|
||||||
|
id?: string;
|
||||||
|
text: string;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
isChecked?: boolean;
|
||||||
|
menuItems?: WindowMenuItem[];
|
||||||
|
}
|
||||||
|
interface Response {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
type Builtin = "ready" | "trayMenuItemClicked" | "windowClose" | "serverOffline" | "clientConnect" | "clientDisconnect" | "appClientConnect" | "appClientDisconnect" | "extClientConnect" | "extClientDisconnect" | "extensionReady" | "neuDev_reloadApp";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// debug
|
||||||
|
enum LoggerType {
|
||||||
|
WARNING = 'WARNING',
|
||||||
|
ERROR = 'ERROR',
|
||||||
|
INFO = 'INFO'
|
||||||
|
}
|
||||||
|
|
||||||
|
// os
|
||||||
|
enum Icon {
|
||||||
|
WARNING = 'WARNING',
|
||||||
|
ERROR = 'ERROR',
|
||||||
|
INFO = 'INFO',
|
||||||
|
QUESTION = 'QUESTION'
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MessageBoxChoice {
|
||||||
|
OK = 'OK',
|
||||||
|
OK_CANCEL = 'OK_CANCEL',
|
||||||
|
YES_NO = 'YES_NO',
|
||||||
|
YES_NO_CANCEL = 'YES_NO_CANCEL',
|
||||||
|
RETRY_CANCEL = 'RETRY_CANCEL',
|
||||||
|
ABORT_RETRY_IGNORE = 'ABORT_RETRY_IGNORE'
|
||||||
|
}
|
||||||
|
|
||||||
|
//clipboard
|
||||||
|
enum ClipboardFormat {
|
||||||
|
unknown = 'unknown',
|
||||||
|
text = 'text',
|
||||||
|
image = 'image'
|
||||||
|
}
|
||||||
|
|
||||||
|
// NL_GLOBALS
|
||||||
|
enum Mode {
|
||||||
|
window = 'window',
|
||||||
|
browser = 'browser',
|
||||||
|
cloud = 'cloud',
|
||||||
|
chrome = 'chrome'
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OperatingSystem {
|
||||||
|
Linux = 'Linux',
|
||||||
|
Windows = 'Windows',
|
||||||
|
Darwin = 'Darwin',
|
||||||
|
FreeBSD = 'FreeBSD',
|
||||||
|
Unknown = 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Architecture {
|
||||||
|
x64 = 'x64',
|
||||||
|
arm = 'arm',
|
||||||
|
itanium = 'itanium',
|
||||||
|
ia32 = 'ia32',
|
||||||
|
unknown = 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface Response {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Builtin =
|
||||||
|
'ready' |
|
||||||
|
'trayMenuItemClicked' |
|
||||||
|
'windowClose' |
|
||||||
|
'serverOffline' |
|
||||||
|
'clientConnect' |
|
||||||
|
'clientDisconnect' |
|
||||||
|
'appClientConnect' |
|
||||||
|
'appClientDisconnect' |
|
||||||
|
'extClientConnect' |
|
||||||
|
'extClientDisconnect' |
|
||||||
|
'extensionReady' |
|
||||||
|
'neuDev_reloadApp'
|
||||||
|
|
||||||
|
|
||||||
|
// --- globals ---
|
||||||
|
/** Mode of the application: window, browser, cloud, or chrome */
|
||||||
|
declare const NL_MODE: Mode;
|
||||||
|
/** Application port */
|
||||||
|
declare const NL_PORT: number;
|
||||||
|
/** Command-line arguments */
|
||||||
|
declare const NL_ARGS: string[];
|
||||||
|
/** Basic authentication token */
|
||||||
|
declare const NL_TOKEN: string;
|
||||||
|
/** Neutralinojs client version */
|
||||||
|
declare const NL_CVERSION: string;
|
||||||
|
/** Application identifier */
|
||||||
|
declare const NL_APPID: string;
|
||||||
|
/** Application version */
|
||||||
|
declare const NL_APPVERSION: string;
|
||||||
|
/** Application path */
|
||||||
|
declare const NL_PATH: string;
|
||||||
|
/** Application data path */
|
||||||
|
declare const NL_DATAPATH: string;
|
||||||
|
/** Returns true if extensions are enabled */
|
||||||
|
declare const NL_EXTENABLED: boolean;
|
||||||
|
/** Returns true if the client library is injected */
|
||||||
|
declare const NL_GINJECTED: boolean;
|
||||||
|
/** Returns true if globals are injected */
|
||||||
|
declare const NL_CINJECTED: boolean;
|
||||||
|
/** Operating system name: Linux, Windows, Darwin, FreeBSD, or Uknown */
|
||||||
|
declare const NL_OS: OperatingSystem;
|
||||||
|
/** CPU architecture: x64, arm, itanium, ia32, or unknown */
|
||||||
|
declare const NL_ARCH: Architecture;
|
||||||
|
/** Neutralinojs server version */
|
||||||
|
declare const NL_VERSION: string;
|
||||||
|
/** Current working directory */
|
||||||
|
declare const NL_CWD: string;
|
||||||
|
/** Identifier of the current process */
|
||||||
|
declare const NL_PID: string;
|
||||||
|
/** Source of application resources: bundle or directory */
|
||||||
|
declare const NL_RESMODE: string;
|
||||||
|
/** Release commit of the client library */
|
||||||
|
declare const NL_CCOMMIT: string;
|
||||||
|
/** An array of custom methods */
|
||||||
|
declare const NL_CMETHODS: string[];
|
||||||
|
|
20
www/src/router/index.ts
Normal file
20
www/src/router/index.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import HomeView from '../views/HomeView.vue'
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'home',
|
||||||
|
component: HomeView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/settings',
|
||||||
|
name: 'settings',
|
||||||
|
component: () => import('../views/SettingsView.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
97
www/src/stores/counter.ts
Normal file
97
www/src/stores/counter.ts
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import type { IntentionHistoryItem, Timer } from '@/types'
|
||||||
|
import { tickClock } from '@/lib'
|
||||||
|
|
||||||
|
export const usePucotiStore = defineStore('pucoti', {
|
||||||
|
state: () => {
|
||||||
|
const countdownDuration = 10 * 60 * 1000
|
||||||
|
const now = new Date().getTime()
|
||||||
|
const ringTime = now + countdownDuration
|
||||||
|
|
||||||
|
return {
|
||||||
|
countdownDuration,
|
||||||
|
intentionHistory: [] as IntentionHistoryItem[],
|
||||||
|
pucotiStart: now,
|
||||||
|
lastRung: 0,
|
||||||
|
ringEvery: '20s', // seconds
|
||||||
|
secondaryTimers: ['total', 'onIntention'],
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
at: '0m',
|
||||||
|
every: '1m',
|
||||||
|
cmd: "notify-send 'Pucoti' 'Time was up 1+ minute ago !!'",
|
||||||
|
lastRan: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timers: {
|
||||||
|
main: {
|
||||||
|
zeroAt: ringTime,
|
||||||
|
increasing: false,
|
||||||
|
name: 'Main timer',
|
||||||
|
color: 'var(--color-light)',
|
||||||
|
},
|
||||||
|
onIntention: {
|
||||||
|
zeroAt: now,
|
||||||
|
increasing: true,
|
||||||
|
name: 'On intention',
|
||||||
|
color: 'var(--color-acid)',
|
||||||
|
},
|
||||||
|
} as Record<string, Timer>,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
intentionStart(): number {
|
||||||
|
if (this.intentionHistory.length > 0) {
|
||||||
|
return this.intentionHistory[this.intentionHistory.length - 1].start
|
||||||
|
}
|
||||||
|
return Date.now()
|
||||||
|
},
|
||||||
|
intention(): string {
|
||||||
|
if (this.intentionHistory.length > 0) {
|
||||||
|
return this.intentionHistory[this.intentionHistory.length - 1].intention
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
setIntention(intention: string) {
|
||||||
|
const now = Date.now()
|
||||||
|
if (this.intentionHistory.length > 0) {
|
||||||
|
this.intentionHistory[this.intentionHistory.length - 1].end = now
|
||||||
|
}
|
||||||
|
|
||||||
|
this.intentionHistory.push({
|
||||||
|
intention,
|
||||||
|
start: now,
|
||||||
|
})
|
||||||
|
this.timers.onIntention.zeroAt = now
|
||||||
|
},
|
||||||
|
addTime(ms: number) {
|
||||||
|
this.timers.main.zeroAt += ms
|
||||||
|
tickClock()
|
||||||
|
},
|
||||||
|
setRingIn(ms: number) {
|
||||||
|
this.timers.main.zeroAt = Date.now() + ms
|
||||||
|
tickClock()
|
||||||
|
},
|
||||||
|
updateHistoricalIntention(index: number, newIntention: string) {
|
||||||
|
if (index >= 0 && index < this.intentionHistory.length) {
|
||||||
|
this.intentionHistory[index].intention = newIntention
|
||||||
|
}
|
||||||
|
},
|
||||||
|
continueFromHistory(intention: string) {
|
||||||
|
// Set the current intention without resetting the timer
|
||||||
|
const now = Date.now()
|
||||||
|
if (this.intentionHistory.length > 0) {
|
||||||
|
this.intentionHistory[this.intentionHistory.length - 1].end = now
|
||||||
|
}
|
||||||
|
|
||||||
|
this.intentionHistory.push({
|
||||||
|
intention,
|
||||||
|
start: now,
|
||||||
|
})
|
||||||
|
this.timers.onIntention.zeroAt = now
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
12
www/src/types/index.d.ts
vendored
Normal file
12
www/src/types/index.d.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
export type IntentionHistoryItem = {
|
||||||
|
intention: string
|
||||||
|
start: number
|
||||||
|
end?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Timer = {
|
||||||
|
zeroAt: number
|
||||||
|
increasing: boolean
|
||||||
|
color: string
|
||||||
|
name: string
|
||||||
|
}
|
154
www/src/utils.ts
Normal file
154
www/src/utils.ts
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
import { format } from 'date-fns'
|
||||||
|
import type { Timer } from './types'
|
||||||
|
|
||||||
|
export const MINUTE = 60 * 1000
|
||||||
|
|
||||||
|
export function timerToMs(timer: Timer): number {
|
||||||
|
return timer.increasing ? Date.now() - timer.zeroAt : timer.zeroAt - Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function timerToString(timer: Timer): string {
|
||||||
|
let time = timerToMs(timer)
|
||||||
|
|
||||||
|
if (time < 0) {
|
||||||
|
time = -time
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = fmtSeconds(time)
|
||||||
|
return parts.join(':')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a time in seconds to the parts of a string.
|
||||||
|
*
|
||||||
|
* Example: 3661 -> ["1", "01", "01"]
|
||||||
|
* Example: 61 -> ["01", "01"]
|
||||||
|
*
|
||||||
|
* @param {number} time Duration in seconds
|
||||||
|
* @returns {string[]} Array of strings representing the time in the format [hours, minutes, seconds]. Hours is there only if it's not 0
|
||||||
|
*/
|
||||||
|
export function fmtSeconds(time: number) {
|
||||||
|
const seconds = Math.floor(Math.abs(time) / 1000)
|
||||||
|
const minutes = Math.floor(seconds / 60)
|
||||||
|
const hours = Math.floor(minutes / 60)
|
||||||
|
const parts = []
|
||||||
|
if (hours > 0) {
|
||||||
|
parts.push(hours)
|
||||||
|
}
|
||||||
|
parts.push((minutes % 60).toString().padStart(2, '0'))
|
||||||
|
parts.push((seconds % 60).toString().padStart(2, '0'))
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a human duration such as "1h 30m" to seconds.
|
||||||
|
* @param {string} duration - The duration string to convert
|
||||||
|
* @returns {number} The duration in miliseconds
|
||||||
|
*/
|
||||||
|
export function humanTimeToMs(duration: string): number {
|
||||||
|
if (duration.startsWith('-')) {
|
||||||
|
return -humanTimeToMs(duration.slice(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the duration.
|
||||||
|
let total = 0
|
||||||
|
const multiplier = { s: 1, m: 60, h: 3600, d: 86400 }
|
||||||
|
|
||||||
|
const parts = duration.split(' ')
|
||||||
|
for (const part of parts) {
|
||||||
|
try {
|
||||||
|
const value = parseInt(part.slice(0, -1))
|
||||||
|
const unit = part.slice(-1)
|
||||||
|
|
||||||
|
if (isNaN(value) || !(unit in multiplier)) {
|
||||||
|
throw new Error(`Invalid duration part: ${part}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
total += value * multiplier[unit]
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Invalid duration part: ${part}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return total * 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fmtTimeRelative(seconds: Date): string {
|
||||||
|
/**
|
||||||
|
* Get a datetime object or a number Epoch timestamp and return a
|
||||||
|
* pretty string like 'an hour ago', 'Yesterday', '3 months ago',
|
||||||
|
* 'just now', etc.
|
||||||
|
*
|
||||||
|
* The time must be in the past.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const diff = Date.now() - seconds.getTime()
|
||||||
|
const secondDiff = Math.floor(diff / 1000)
|
||||||
|
const dayDiff = Math.floor(secondDiff / 86400)
|
||||||
|
|
||||||
|
if (dayDiff < 0) {
|
||||||
|
throw new Error('Invalid date difference')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dayDiff === 0) {
|
||||||
|
if (secondDiff < 10) return 'just now'
|
||||||
|
if (secondDiff < 60) return `${secondDiff}s ago`
|
||||||
|
if (secondDiff < 120) return `1m ${secondDiff % 60}s ago`
|
||||||
|
if (secondDiff < 3600) return `${Math.floor(secondDiff / 60)}m ago`
|
||||||
|
if (secondDiff < 7200) return `1h ${Math.floor((secondDiff % 3600) / 60)}m ago`
|
||||||
|
if (secondDiff < 86400) return `${Math.floor(secondDiff / 3600)}h ago`
|
||||||
|
}
|
||||||
|
if (dayDiff <= 1) return 'Yesterday'
|
||||||
|
if (dayDiff < 7) return `${dayDiff} days ago`
|
||||||
|
if (dayDiff < 31) return `${Math.floor(dayDiff / 7)} weeks ago`
|
||||||
|
if (dayDiff < 365) return `${Math.floor(dayDiff / 30)} months ago`
|
||||||
|
|
||||||
|
return `${Math.floor(dayDiff / 365)} years ago`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Human readable time format for dates in the past.
|
||||||
|
* E.g. "at 12:34", "Yest at 12:34", "Mon at 12:34", "Mon 12 at 12:34", "Mon 12 Jan at 12:34"
|
||||||
|
*/
|
||||||
|
export function fmtTimeAbsolute(seconds: Date): string {
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
const diff = now.getTime() - seconds.getTime()
|
||||||
|
const dayDiff = Math.floor(diff / (1000 * 60 * 60 * 24))
|
||||||
|
|
||||||
|
if (dayDiff < 0) {
|
||||||
|
throw new Error('Invalid date difference')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dayDiff === 0) {
|
||||||
|
// return `at ${format(seconds, 'HH:mm')}`;
|
||||||
|
return format(seconds, "'at' HH:mm")
|
||||||
|
}
|
||||||
|
if (dayDiff === 1) {
|
||||||
|
// return `Yest at ${format(seconds, 'HH:mm')}`;
|
||||||
|
return format(seconds, "'Yest' HH:mm")
|
||||||
|
}
|
||||||
|
if (dayDiff < 7) {
|
||||||
|
// return `${format(seconds, 'EEE')} at ${format(seconds, 'HH:mm')}`;
|
||||||
|
return format(seconds, "EEE 'at' HH:mm")
|
||||||
|
}
|
||||||
|
if (dayDiff < 31 && now.getMonth() === seconds.getMonth()) {
|
||||||
|
// return `${format(seconds, 'EEE dd')} at ${format(seconds, 'HH:mm')}`;
|
||||||
|
return format(seconds, "EEE dd 'at' HH:mm")
|
||||||
|
}
|
||||||
|
if (now.getFullYear() === seconds.getFullYear()) {
|
||||||
|
// return `${format(seconds, 'EEE dd MMM')} at ${format(seconds, 'HH:mm')}`;
|
||||||
|
return format(seconds, "EEE dd MMM 'at' HH:mm")
|
||||||
|
}
|
||||||
|
// return `${format(seconds, 'EEE dd MMM yyyy')} at ${format(seconds, 'HH:mm')}`;
|
||||||
|
return format(seconds, "EEE dd MMM yyyy 'at' HH:mm")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fmtTime(seconds: Date, relative: boolean = true): string {
|
||||||
|
return relative ? fmtTimeRelative(seconds) : fmtTimeAbsolute(seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeTimerEnd(timer: number, start: number): number {
|
||||||
|
// +0.5 to show visually round time -> more satisfying
|
||||||
|
return timer + (Math.round(Date.now() / 1000 + 0.5) - start)
|
||||||
|
}
|
345
www/src/views/HomeView.vue
Normal file
345
www/src/views/HomeView.vue
Normal file
|
@ -0,0 +1,345 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, nextTick, useTemplateRef } from 'vue'
|
||||||
|
import Button from '@/components/Button.vue'
|
||||||
|
import AppLogo from '@/components/AppLogo.vue'
|
||||||
|
import Timer from '@/components/Timer.vue'
|
||||||
|
import IntentionHistory from '@/components/IntentionHistory.vue'
|
||||||
|
import { usePucotiStore } from '@/stores/counter'
|
||||||
|
import { RouterLink } from 'vue-router'
|
||||||
|
import router from '@/router'
|
||||||
|
import { humanTimeToMs, MINUTE } from '@/utils'
|
||||||
|
import { useListenerFn } from '@/lib'
|
||||||
|
|
||||||
|
const store = usePucotiStore()
|
||||||
|
|
||||||
|
const isEditingTime = ref<boolean>(false)
|
||||||
|
const newTimeValue = ref<string>('')
|
||||||
|
const timeInputRef = ref<HTMLInputElement | null>(null)
|
||||||
|
const startEditingTime = () => {
|
||||||
|
isEditingTime.value = true
|
||||||
|
nextTick(() => {
|
||||||
|
if (timeInputRef.value) {
|
||||||
|
timeInputRef.value.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const updateTimer = (e) => {
|
||||||
|
try {
|
||||||
|
let newTime = humanTimeToMs(e.target.value)
|
||||||
|
store.setRingIn(newTime)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
stopEditingTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopEditingTime = () => {
|
||||||
|
isEditingTime.value = false
|
||||||
|
newTimeValue.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEditingIntention = ref<boolean>(false)
|
||||||
|
const $intention = useTemplateRef<HTMLInputElement>('intention')
|
||||||
|
|
||||||
|
const startEditingIntention = () => {
|
||||||
|
isEditingIntention.value = true
|
||||||
|
nextTick(() => {
|
||||||
|
if ($intention.value) {
|
||||||
|
$intention.value.focus()
|
||||||
|
$intention.value.select()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopEditingIntention = () => {
|
||||||
|
$intention.value = store.intention // Revert to the current store value
|
||||||
|
$intention.value.blur() // blur to take focus away from the input
|
||||||
|
isEditingIntention.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function onIntentionInput(e: KeyboardEvent) {
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
const target = e.target as HTMLInputElement
|
||||||
|
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
store.setIntention(target.value)
|
||||||
|
stopEditingIntention()
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
target.value = store.intention
|
||||||
|
stopEditingIntention()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useListenerFn('keydown', (e: KeyboardEvent) => {
|
||||||
|
// If we are editing either intention or time, global shortcuts are disabled
|
||||||
|
if (isEditingIntention.value || isEditingTime.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (e.key) {
|
||||||
|
case 'j':
|
||||||
|
store.addTime(-MINUTE)
|
||||||
|
break
|
||||||
|
case 'J':
|
||||||
|
store.addTime(-5 * MINUTE)
|
||||||
|
break
|
||||||
|
case 'k':
|
||||||
|
store.addTime(MINUTE)
|
||||||
|
break
|
||||||
|
case 'K':
|
||||||
|
store.addTime(5 * MINUTE)
|
||||||
|
break
|
||||||
|
case 'r':
|
||||||
|
store.setRingIn(store.countdownDuration)
|
||||||
|
break
|
||||||
|
case 'h':
|
||||||
|
router.push('/help')
|
||||||
|
break
|
||||||
|
case 's':
|
||||||
|
router.push('/settings')
|
||||||
|
break
|
||||||
|
case 'p':
|
||||||
|
router.push('/intentionhistory')
|
||||||
|
break
|
||||||
|
case 'Enter':
|
||||||
|
startEditingIntention()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
// Key corresponding to 0-9 -> set to that minute
|
||||||
|
// + if shift is pressed, set to 10*key
|
||||||
|
if (e.code.startsWith('Digit')) {
|
||||||
|
let digit = parseInt(e.code[5])
|
||||||
|
if (e.shiftKey) {
|
||||||
|
digit *= 10
|
||||||
|
}
|
||||||
|
store.setRingIn(digit * MINUTE)
|
||||||
|
store.countdownDuration = digit * MINUTE
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<header>
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<RouterLink to="/">
|
||||||
|
<AppLogo />
|
||||||
|
</RouterLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://pucoti.com" class="">Download</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<RouterLink to="/settings" class="">Settings</RouterLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<div class="intention-area">
|
||||||
|
<div v-if="!isEditingIntention" class="intention-placeholder" @click="startEditingIntention">
|
||||||
|
{{ store.intention || 'Enter your intention' }}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
id="intention"
|
||||||
|
type="text"
|
||||||
|
ref="intention"
|
||||||
|
:value="store.intention"
|
||||||
|
placeholder="Enter your intention"
|
||||||
|
@keydown.stop="onIntentionInput"
|
||||||
|
@blur="stopEditingIntention"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="main-layout">
|
||||||
|
<div class="timer-layout">
|
||||||
|
<div class="action-item-list">
|
||||||
|
<Button label="-1 min" shortcut="j" @click="store.addTime(-MINUTE)" />
|
||||||
|
<Button label="-5 min" shortcut="J" @click="store.addTime(-5 * MINUTE)" />
|
||||||
|
</div>
|
||||||
|
<div v-if="!isEditingTime" class="main-timer-container" @click="startEditingTime">
|
||||||
|
<Timer :timer="store.timers.main" color="var(--color-light)" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
ref="timeInputRef"
|
||||||
|
v-model="newTimeValue"
|
||||||
|
placeholder="1h 10m"
|
||||||
|
class="timer-input"
|
||||||
|
@keyup.escape="stopEditingTime"
|
||||||
|
@keyup.enter="updateTimer"
|
||||||
|
@blur="stopEditingTime"
|
||||||
|
/>
|
||||||
|
<div class="action-item-list">
|
||||||
|
<Button label="+1 min" shortcut="k" @click="store.addTime(MINUTE)" />
|
||||||
|
<Button label="+5 min" shortcut="K" @click="store.addTime(5 * MINUTE)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer-action-items">
|
||||||
|
<Button label="Create room" shortcut="C" outline />
|
||||||
|
<Button label="Enter room" shortcut="E" outline />
|
||||||
|
</div>
|
||||||
|
<div class="below">
|
||||||
|
<IntentionHistory />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
header {
|
||||||
|
padding: 1vw 1vw 0 1vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav > ul {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav > ul > li:first-child {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav > ul > li + li {
|
||||||
|
margin-left: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
background: var(--color-dark);
|
||||||
|
margin: 1vw 1vw 0 1vw;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purpose {
|
||||||
|
margin-top: clamp(1em, 1.5vw, 100vw);
|
||||||
|
color: var(--color-acid);
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: clamp(1em, 2vw, 100vw);
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-timer-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: color-mix(in oklab, var(--color-light) 10%, transparent);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-layout {
|
||||||
|
margin-top: clamp(1em, 1.5vw, 100vw);
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas:
|
||||||
|
'action-left timer action-right'
|
||||||
|
'. timer .'
|
||||||
|
'. footer .'
|
||||||
|
'. below .';
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
column-gap: 1vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-layout {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-item-list:first-of-type {
|
||||||
|
grid-area: action-left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-timer-container {
|
||||||
|
grid-area: timer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.below {
|
||||||
|
grid-area: below;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-item-list:last-of-type {
|
||||||
|
grid-area: action-right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-item-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-item-list > * + * {
|
||||||
|
margin-top: 1vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-action-items {
|
||||||
|
grid-area: footer;
|
||||||
|
display: flex;
|
||||||
|
margin-top: clamp(1em, 1.5vw, 100vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-action-items > * + * {
|
||||||
|
margin-left: 1vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-input {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: clamp(4em, 12vw, 100vw);
|
||||||
|
color: var(--color-light);
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-input::placeholder {
|
||||||
|
color: color-mix(in srgb, var(--color-light) 50%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.intention-area {
|
||||||
|
margin-top: clamp(1em, 1.5vw, 100vw);
|
||||||
|
text-align: center;
|
||||||
|
/* Center the content within the div */
|
||||||
|
}
|
||||||
|
|
||||||
|
.intention-placeholder {
|
||||||
|
display: inline-block;
|
||||||
|
/* Allows padding and margin */
|
||||||
|
color: var(--color-acid);
|
||||||
|
background-color: color-mix(in oklab, var(--color-light) 10%, transparent);
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 3em;
|
||||||
|
/* Match input font size */
|
||||||
|
padding: 0.1em 0.5em;
|
||||||
|
/* Add some padding to make it look like an input */
|
||||||
|
cursor: text;
|
||||||
|
/* Indicate it's clickable/editable */
|
||||||
|
min-width: 300px;
|
||||||
|
/* Ensure a minimum width */
|
||||||
|
}
|
||||||
|
|
||||||
|
#intention {
|
||||||
|
display: inline-block;
|
||||||
|
/* Use inline-block for consistent centering */
|
||||||
|
color: var(--color-acid);
|
||||||
|
background-color: color-mix(in oklab, var(--color-light) 10%, transparent);
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 3em;
|
||||||
|
text-align: center;
|
||||||
|
border: none;
|
||||||
|
/* Remove default input border */
|
||||||
|
outline: none;
|
||||||
|
/* Remove default input outline */
|
||||||
|
padding: 0.1em 0.5em;
|
||||||
|
/* Match placeholder padding */
|
||||||
|
min-width: 300px;
|
||||||
|
/* Match placeholder width */
|
||||||
|
}
|
||||||
|
|
||||||
|
#intention::placeholder {
|
||||||
|
color: color-mix(in oklab, var(--color-acid) 40%, transparent);
|
||||||
|
}
|
||||||
|
</style>
|
3
www/src/views/SettingsView.vue
Normal file
3
www/src/views/SettingsView.vue
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<h1>Settings</h1>
|
||||||
|
</template>
|
12
www/tsconfig.app.json
Normal file
12
www/tsconfig.app.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||||
|
"exclude": ["src/**/__tests__/*"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
www/tsconfig.json
Normal file
11
www/tsconfig.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
19
www/tsconfig.node.json
Normal file
19
www/tsconfig.node.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"extends": "@tsconfig/node22/tsconfig.json",
|
||||||
|
"include": [
|
||||||
|
"vite.config.*",
|
||||||
|
"vitest.config.*",
|
||||||
|
"cypress.config.*",
|
||||||
|
"nightwatch.conf.*",
|
||||||
|
"playwright.config.*",
|
||||||
|
"eslint.config.*"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"types": ["node"]
|
||||||
|
}
|
||||||
|
}
|
16
www/vite.config.ts
Normal file
16
www/vite.config.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue(), vueDevTools(), tailwindcss()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
Loading…
Add table
Add a link
Reference in a new issue