first commit
15
.editorconfig
Normal file
@ -0,0 +1,15 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
end_of_line = lf
|
||||
# editorconfig-tools is unable to ignore longs strings or urls
|
||||
max_line_length = off
|
||||
|
||||
[*.md]
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = false
|
22
.eslintrc
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"airbnb-base"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"import/extensions": "off",
|
||||
"no-console": "off",
|
||||
"max-len": "off"
|
||||
},
|
||||
"settings": {
|
||||
"import/core-modules": ["electron"]
|
||||
}
|
||||
}
|
289
.gitignore
vendored
Normal file
@ -0,0 +1,289 @@
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/node,macos,windows,intellij
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=node,macos,windows,intellij
|
||||
|
||||
### Intellij ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### Intellij Patch ###
|
||||
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
|
||||
|
||||
# *.iml
|
||||
# modules.xml
|
||||
# .idea/misc.xml
|
||||
# *.ipr
|
||||
|
||||
# Sonarlint plugin
|
||||
# https://plugins.jetbrains.com/plugin/7973-sonarlint
|
||||
.idea/**/sonarlint/
|
||||
|
||||
# SonarQube Plugin
|
||||
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
|
||||
.idea/**/sonarIssues.xml
|
||||
|
||||
# Markdown Navigator plugin
|
||||
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
|
||||
.idea/**/markdown-navigator.xml
|
||||
.idea/**/markdown-navigator-enh.xml
|
||||
.idea/**/markdown-navigator/
|
||||
|
||||
# Cache file creation bug
|
||||
# See https://youtrack.jetbrains.com/issue/JBR-2257
|
||||
.idea/$CACHE_FILE$
|
||||
|
||||
# CodeStream plugin
|
||||
# https://plugins.jetbrains.com/plugin/12206-codestream
|
||||
.idea/codestream.xml
|
||||
|
||||
### macOS ###
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
.env*.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Storybook build outputs
|
||||
.out
|
||||
.storybook-out
|
||||
storybook-static
|
||||
|
||||
# rollup.js default build output
|
||||
dist/
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# Temporary folders
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
### Windows ###
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
Thumbs.db:encryptable
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/node,macos,windows,intellij
|
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
6
.idea/jsLibraryMappings.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptLibraryMappings">
|
||||
<includedPredefinedLibrary name="Node.js Core" />
|
||||
</component>
|
||||
</project>
|
6
.idea/jsLinters/eslint.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="EslintConfiguration">
|
||||
<option name="fix-on-save" value="true" />
|
||||
</component>
|
||||
</project>
|
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/timestamp.iml" filepath="$PROJECT_DIR$/.idea/timestamp.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
10
.idea/timestamp.iml
generated
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/dist" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
15
CHANGELOG.md
Normal file
@ -0,0 +1,15 @@
|
||||
# [1.0.1]
|
||||
###### 2016-09-09
|
||||
|
||||
A tiny patch release which eliminates two bugs. 🐞
|
||||
|
||||
###### Fixed
|
||||
- Calendar window position being offset on multiple monitors
|
||||
- Preferences and about window being opened multiple times
|
||||
|
||||
# 1.0.0
|
||||
###### 2016-09-04
|
||||
|
||||
First public release! 🎉
|
||||
|
||||
[1.0.1]: https://github.com/mzdr/timestamp/compare/1.0.0...1.0.1
|
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021 Sebastian Prein
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
45
README.md
Normal file
@ -0,0 +1,45 @@
|
||||
<h1 align="center">
|
||||
<img src="https://mzdr.github.io/timestamp/icon.svg" width="128" alt="Logo of Timestamp">
|
||||
<p>Timestamp</p>
|
||||
<a href="https://github.com/mzdr/timestamp/releases/latest"><img src="https://img.shields.io/github/release/mzdr/timestamp.svg?maxAge=3600" alt="Latest Timestamp release"></a>
|
||||
</h1>
|
||||
|
||||
A better macOS menu bar clock with a customizable date/time display and a calendar. Inspired by [Day-O].
|
||||
|
||||
Built with [Electron] and [date-fns].
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||
## Install
|
||||
|
||||
### Manual
|
||||
**[Download]**, unzip, and move `Timestamp.app` to the `/Applications` directory.
|
||||
|
||||
### Homebrew
|
||||
Simply run `brew install --cask timestamp` in your terminal.
|
||||
|
||||
## Support
|
||||
|
||||
**Bugs and requests**: Please use the project's [issue tracker].
|
||||
[](https://github.com/mzdr/timestamp/issues)
|
||||
|
||||
**Want to contribute?** Please fork this repository and open a pull request with your new shiny stuff. 🌟
|
||||
[](https://github.com/mzdr/timestamp/pulls)
|
||||
|
||||
**Do you like it?** Support the project by starring the repository or [tweet] about it.
|
||||
|
||||
## Thanks
|
||||
|
||||
**Timestamp** © 2021, Sebastian Prein. Released under the [MIT License].
|
||||
|
||||
[Day-O]: http://shauninman.com/archive/2011/10/20/day_o_mac_menu_bar_clock
|
||||
[Electron]: http://electron.atom.io/
|
||||
[date-fns]: https://date-fns.org/
|
||||
[MIT License]: https://mit-license.org/
|
||||
[issue tracker]: https://github.com/mzdr/timestamp/issues/new
|
||||
[tweet]: https://twitter.com/intent/tweet?url=https://github.com/mzdr/timestamp&text=Timestamp,%20a%20better%20macOS%20menu%20bar%20clock%20with%20a%20customizable%20date/time%20display%20and%20a%20calendar.%20%E2%80%94
|
||||
[customizable]: https://date-fns.org/docs/format
|
||||
[Download]: https://github.com/mzdr/timestamp/releases/latest
|
||||
[support]: #support
|
1
app/assets/backgrounds/empty.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg"></svg>
|
After Width: | Height: | Size: 47 B |
55
app/assets/backgrounds/gradient.svg
Normal file
@ -0,0 +1,55 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 196" preserveAspectRatio="xMidYMax meet">
|
||||
<style>
|
||||
/*
|
||||
|
||||
Thanks to Claudio Guglieri and Peter Bork.
|
||||
|
||||
https://codepen.io/bork/details/wJhEm
|
||||
https://dribbble.com/shots/2644219-Colorscape-Color-study
|
||||
|
||||
*/
|
||||
|
||||
.wave { fill: var(--color-background-primary, #fff); }
|
||||
.wave-start { stop-color: var(--color-background-primary, #fff); }
|
||||
.wave-stop { stop-color: var(--color-background-primary, #fff); }
|
||||
|
||||
:host {
|
||||
background-image: linear-gradient(var(--deg), var(--start), var(--stop));
|
||||
color: var(--palette-white, #fff);
|
||||
}
|
||||
|
||||
:host([data-hour="0"]) { --deg: 180deg; --start: #001322; --stop: #012459; }
|
||||
:host([data-hour="1"]) { --deg: 195deg; --start: #001322; --stop: #012459; }
|
||||
:host([data-hour="2"]) { --deg: 210deg; --start: #001322; --stop: #012459; }
|
||||
:host([data-hour="3"]) { --deg: 225deg; --start: #001322; --stop: #012459; }
|
||||
:host([data-hour="4"]) { --deg: 240deg; --start: #001322; --stop: #003972; }
|
||||
:host([data-hour="5"]) { --deg: 255deg; --start: #00182b; --stop: #016792; }
|
||||
:host([data-hour="6"]) { --deg: 270deg; --start: #042c47; --stop: #07729f; }
|
||||
:host([data-hour="7"]) { --deg: 285deg; --start: #07506e; --stop: #12a1c0; }
|
||||
:host([data-hour="8"]) { --deg: 300deg; --start: #1386a6; --stop: #74d4cc; }
|
||||
:host([data-hour="9"]) { --deg: 315deg; --start: #efeebc; --stop: #61d0cf; color: var(--palette-black); }
|
||||
:host([data-hour="10"]) { --deg: 330deg; --start: #fee154; --stop: #a3dec6; color: var(--palette-black); }
|
||||
:host([data-hour="11"]) { --deg: 345deg; --start: #fdc352; --stop: #e8ed92; color: var(--palette-black); }
|
||||
:host([data-hour="12"]) { --deg: 0deg; --start: #ff8d27; --stop: #ffe577; }
|
||||
:host([data-hour="13"]) { --deg: 15deg; --start: #ff7e20; --stop: #ffda35; }
|
||||
:host([data-hour="14"]) { --deg: 30deg; --start: #ff8d27; --stop: #ffe577; }
|
||||
:host([data-hour="15"]) { --deg: 45deg; --start: #f18448; --stop: #ffd364; }
|
||||
:host([data-hour="16"]) { --deg: 60deg; --start: #f06b7e; --stop: #f9a856; }
|
||||
:host([data-hour="17"]) { --deg: 75deg; --start: #ca5a92; --stop: #f4896b; }
|
||||
:host([data-hour="18"]) { --deg: 90deg; --start: #5b2c83; --stop: #d1628b; }
|
||||
:host([data-hour="19"]) { --deg: 105deg; --start: #371a79; --stop: #713684; }
|
||||
:host([data-hour="20"]) { --deg: 120deg; --start: #28166b; --stop: #45217c; }
|
||||
:host([data-hour="21"]) { --deg: 135deg; --start: #192861; --stop: #372074; }
|
||||
:host([data-hour="22"]) { --deg: 150deg; --start: #040b3c; --stop: #233072; }
|
||||
:host([data-hour="23"]) { --deg: 165deg; --start: #040b3c; --stop: #012459; }
|
||||
</style>
|
||||
<defs>
|
||||
<linearGradient id="wave" gradientTransform="rotate(90)">
|
||||
<stop class="wave-start" />
|
||||
<stop class="wave-stop" stop-opacity="0.3" offset="1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path fill="url(#gradient)" d="M0 0h256v196H0z" />
|
||||
<path fill="url(#wave)" d="M0 168.5c66.5-9 174.582 28.729 256 11.907V196H0v-27.5z" />
|
||||
<path class="wave" d="M0 183.5c67.5 18.5 153-43 256 8.5v4H0v-12.5z" />
|
||||
</svg>
|
After Width: | Height: | Size: 3.0 KiB |
1
app/assets/icons/bell.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg>
|
After Width: | Height: | Size: 306 B |
1
app/assets/icons/calendar.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg>
|
After Width: | Height: | Size: 391 B |
1
app/assets/icons/command.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path d="M18 3a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3H6a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3V6a3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3h12a3 3 0 0 0 3-3 3 3 0 0 0-3-3z"></path></svg>
|
After Width: | Height: | Size: 403 B |
1
app/assets/icons/settings.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
|
After Width: | Height: | Size: 992 B |
43
app/assets/logo.svg
Normal file
@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<linearGradient x1="22.447823%" y1="0%" x2="22.447823%" y2="100%" id="linearGradient-1">
|
||||
<stop stop-color="#000000" stop-opacity="0" offset="0%"></stop>
|
||||
<stop stop-color="#000000" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="0%" y1="50%" x2="75.4839487%" y2="50%" id="linearGradient-2">
|
||||
<stop stop-color="#000000" offset="0%"></stop>
|
||||
<stop stop-color="#008BED" stop-opacity="0" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<polygon id="path-3" points="358.999994 895.999994 179 895.999994 179 716 538.999963 715.999994"></polygon>
|
||||
<linearGradient x1="0%" y1="50%" x2="74.7274382%" y2="50%" id="linearGradient-4">
|
||||
<stop stop-color="#000000" offset="0%"></stop>
|
||||
<stop stop-color="#008BED" stop-opacity="0" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<polygon id="path-5" points="359.199994 538.399994 179.2 538.399994 179.2 358.4 539.199963 358.399994"></polygon>
|
||||
<linearGradient x1="0%" y1="0%" x2="100%" y2="100%" id="linearGradient-6">
|
||||
<stop stop-color="#FFFFFF" offset="0%"></stop>
|
||||
<stop stop-color="#2195F2" stop-opacity="0" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<polygon id="path-7" points="179.2 2.67351757e-14 179.200012 896 0 716.799988 0 0"></polygon>
|
||||
</defs>
|
||||
<g id="Timestamp" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="#3">
|
||||
<polygon id="shadow" fill-opacity="0.300000012" fill="url(#linearGradient-1)" points="932.977759 845 602.264373 959.791757 422.200012 960 447.833499 845.178208"></polygon>
|
||||
<g id="t" transform="translate(243.000000, 64.000000)">
|
||||
<g id="bottom-line">
|
||||
<use fill="#2195F2" xlink:href="#path-3"></use>
|
||||
<use fill="url(#linearGradient-2)" style="mix-blend-mode: soft-light;" xlink:href="#path-3"></use>
|
||||
</g>
|
||||
<g id="upper-line">
|
||||
<use fill="#2195F2" xlink:href="#path-5"></use>
|
||||
<use fill="url(#linearGradient-4)" style="mix-blend-mode: soft-light;" xlink:href="#path-5"></use>
|
||||
</g>
|
||||
<g id="body">
|
||||
<use fill="#2195F2" xlink:href="#path-7"></use>
|
||||
<use fill="url(#linearGradient-6)" style="mix-blend-mode: soft-light;" xlink:href="#path-7"></use>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
154
app/components/Calendar.js
Normal file
@ -0,0 +1,154 @@
|
||||
const { ipcMain } = require('electron');
|
||||
const { resolve } = require('path');
|
||||
const datefns = require('date-fns');
|
||||
|
||||
const Window = require('./Window');
|
||||
|
||||
const {
|
||||
CALENDAR_GET_CALENDAR,
|
||||
CALENDAR_GET_DATE,
|
||||
CALENDAR_GET_MONTHS,
|
||||
CALENDAR_HIDE,
|
||||
CALENDAR_IS_SAME_HOUR,
|
||||
CALENDAR_SHOW,
|
||||
} = require('../views/calendar/ipc');
|
||||
|
||||
class Calendar {
|
||||
constructor({ locale, logger }) {
|
||||
this.logger = logger;
|
||||
this.locale = locale.getObject();
|
||||
|
||||
ipcMain.handle(CALENDAR_GET_CALENDAR, this.getCalendar.bind(this));
|
||||
ipcMain.handle(CALENDAR_GET_DATE, this.getDate.bind(this));
|
||||
ipcMain.handle(CALENDAR_GET_MONTHS, this.getMonths.bind(this));
|
||||
ipcMain.handle(CALENDAR_IS_SAME_HOUR, (event, ...dates) => datefns.isSameHour(...dates));
|
||||
|
||||
ipcMain.on(CALENDAR_HIDE, () => this.window.hide());
|
||||
ipcMain.on(CALENDAR_SHOW, () => this.window.show());
|
||||
|
||||
this.window = new Window({
|
||||
name: 'calendar',
|
||||
sourceFile: resolve(__dirname, '../views/calendar/calendar.html'),
|
||||
webPreferences: {
|
||||
preload: resolve(__dirname, '../views/calendar/preload.js'),
|
||||
backgroundThrottling: false,
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.debug('Calendar module created.');
|
||||
}
|
||||
|
||||
getDate(event, payload = {}) {
|
||||
const { locale } = this;
|
||||
|
||||
const {
|
||||
date, format, set, diff,
|
||||
} = payload;
|
||||
|
||||
let final = date || new Date();
|
||||
|
||||
if (diff) {
|
||||
final = datefns.add(final, diff); // date-fns.add() supports negative numbers as well
|
||||
}
|
||||
|
||||
if (set) {
|
||||
final = datefns.set(final, set);
|
||||
}
|
||||
|
||||
if (format) {
|
||||
try {
|
||||
return datefns.format(final, format, { locale });
|
||||
} catch (e) {
|
||||
return '#invalid format#';
|
||||
}
|
||||
}
|
||||
|
||||
return final;
|
||||
}
|
||||
|
||||
getCalendar(event, { date }) {
|
||||
const { locale } = this;
|
||||
const weekdays = [];
|
||||
const weeks = [];
|
||||
const days = [];
|
||||
|
||||
const totalDays = datefns.getDaysInMonth(date);
|
||||
const lastDayOfMonth = datefns.lastDayOfMonth(date);
|
||||
const startOfMonth = datefns.startOfMonth(date);
|
||||
const firstWeek = datefns.startOfWeek(startOfMonth, { locale });
|
||||
const lastWeek = datefns.endOfWeek(lastDayOfMonth, { locale });
|
||||
const previousMonthDays = datefns.differenceInCalendarDays(startOfMonth, firstWeek);
|
||||
const nextMonthDays = datefns.differenceInCalendarDays(lastWeek, lastDayOfMonth);
|
||||
|
||||
for (let i = 0; i < previousMonthDays; i += 1) {
|
||||
const day = datefns.addDays(firstWeek, i);
|
||||
const isToday = datefns.isToday(day);
|
||||
const isThisWeek = datefns.isThisWeek(day, { locale });
|
||||
const previousMonth = true;
|
||||
|
||||
days.push({
|
||||
day, isToday, isThisWeek, previousMonth,
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < totalDays; i += 1) {
|
||||
const day = datefns.addDays(startOfMonth, i);
|
||||
const isToday = datefns.isToday(day);
|
||||
const isThisWeek = datefns.isThisWeek(day, { locale });
|
||||
|
||||
days.push({ day, isToday, isThisWeek });
|
||||
}
|
||||
|
||||
for (let i = 1; i <= nextMonthDays; i += 1) {
|
||||
const day = datefns.addDays(lastDayOfMonth, i);
|
||||
const isToday = datefns.isToday(day);
|
||||
const isThisWeek = datefns.isThisWeek(day, { locale });
|
||||
const nextMonth = true;
|
||||
|
||||
days.push({
|
||||
day, isToday, isThisWeek, nextMonth,
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < 7; i += 1) {
|
||||
weekdays.push(datefns.format(days[i].day, 'EEE', { locale }));
|
||||
}
|
||||
|
||||
for (let i = 0; i < days.length; i += 7) {
|
||||
weeks.push(datefns.getWeek(days[i].day, { locale }));
|
||||
}
|
||||
|
||||
return {
|
||||
totalDays,
|
||||
lastDayOfMonth,
|
||||
startOfMonth,
|
||||
firstWeek,
|
||||
lastWeek,
|
||||
previousMonthDays,
|
||||
nextMonthDays,
|
||||
weekdays,
|
||||
weeks,
|
||||
days,
|
||||
};
|
||||
}
|
||||
|
||||
getMonths() {
|
||||
const { locale } = this;
|
||||
const start = datefns.startOfYear(new Date());
|
||||
const months = [];
|
||||
|
||||
for (let i = 0; i < 12; i += 1) {
|
||||
months.push(
|
||||
datefns.format(
|
||||
datefns.addMonths(start, i),
|
||||
'MMM',
|
||||
{ locale },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return months;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Calendar;
|
39
app/components/Clock.js
Normal file
@ -0,0 +1,39 @@
|
||||
const datefns = require('date-fns');
|
||||
|
||||
class Clock {
|
||||
constructor(options = {}) {
|
||||
const { onTick, locale, format } = options;
|
||||
|
||||
this.locale = locale.getObject();
|
||||
this.setFormat(format);
|
||||
|
||||
if (typeof onTick === 'function') {
|
||||
setInterval(() => onTick(this.toString()), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
getFormat() {
|
||||
return this.format;
|
||||
}
|
||||
|
||||
setFormat(value) {
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error(`Clock.format is supposed to be a string, ${typeof value} given.`);
|
||||
}
|
||||
|
||||
// @see https://date-fns.org/docs/format
|
||||
this.format = value;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
toString() {
|
||||
try {
|
||||
return datefns.format(new Date(), this.getFormat(), { locale: this.locale });
|
||||
} catch (e) {
|
||||
return '#invalid format#';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Clock;
|
40
app/components/Locale.js
Normal file
@ -0,0 +1,40 @@
|
||||
/* eslint-disable global-require */
|
||||
const locales = {
|
||||
date: require('date-fns/locale'),
|
||||
app: require('../locales'),
|
||||
};
|
||||
|
||||
class Locale {
|
||||
constructor(options = {}) {
|
||||
const { preferred, logger } = options;
|
||||
const [language, extension] = String(preferred).split('-');
|
||||
|
||||
logger.debug(`Preferred locale is “${preferred}”.`);
|
||||
|
||||
const fullSupport = `${language}${extension}`;
|
||||
const partialSupport = language;
|
||||
const fallback = 'en-US';
|
||||
|
||||
this.locale = [fullSupport, partialSupport, fallback].find((k) => locales.date[k]);
|
||||
this.localeObject = locales.date[this.locale];
|
||||
this.translations = locales.app[partialSupport] || locales.app.en;
|
||||
|
||||
logger.debug(`Using “${this.translations.locale}” as application locale and “${this.locale}” as clock/calendar locale.`);
|
||||
}
|
||||
|
||||
get() {
|
||||
return this.locale;
|
||||
}
|
||||
|
||||
getObject() {
|
||||
return this.localeObject;
|
||||
}
|
||||
|
||||
translate(key) {
|
||||
return key
|
||||
.split('.')
|
||||
.reduce((o, i) => (o || {})[i], this.translations) || key;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Locale;
|
63
app/components/Logger.js
Normal file
@ -0,0 +1,63 @@
|
||||
const { writeFile } = require('fs').promises;
|
||||
const { logFile } = require('../paths');
|
||||
|
||||
class Logger {
|
||||
constructor() {
|
||||
this.filePath = logFile;
|
||||
this.levels = {
|
||||
emergency: 0,
|
||||
alert: 1,
|
||||
critical: 2,
|
||||
error: 3,
|
||||
warning: 4,
|
||||
notice: 5,
|
||||
informational: 6,
|
||||
debug: 7,
|
||||
};
|
||||
|
||||
this.cleanUp = writeFile(this.filePath, '\n');
|
||||
}
|
||||
|
||||
async log(level, message) {
|
||||
const severity = Object.keys(this.levels).find((key) => this.levels[key] === level);
|
||||
const entry = `[${severity}]: ${message}`;
|
||||
|
||||
await this.cleanUp;
|
||||
|
||||
writeFile(this.filePath, `${entry}\n`, { flag: 'a' });
|
||||
}
|
||||
|
||||
emergency(message) {
|
||||
return this.log(this.levels.emergency, message);
|
||||
}
|
||||
|
||||
alert(message) {
|
||||
return this.log(this.levels.alert, message);
|
||||
}
|
||||
|
||||
critical(message) {
|
||||
return this.log(this.levels.critical, message);
|
||||
}
|
||||
|
||||
error(message) {
|
||||
return this.log(this.levels.error, message);
|
||||
}
|
||||
|
||||
warning(message) {
|
||||
return this.log(this.levels.warning, message);
|
||||
}
|
||||
|
||||
notice(message) {
|
||||
return this.log(this.levels.notice, message);
|
||||
}
|
||||
|
||||
informational(message) {
|
||||
return this.log(this.levels.informational, message);
|
||||
}
|
||||
|
||||
debug(message) {
|
||||
return this.log(this.levels.debug, message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Logger;
|
151
app/components/Preferences.js
Normal file
@ -0,0 +1,151 @@
|
||||
const { ipcMain } = require('electron');
|
||||
const { resolve } = require('path');
|
||||
|
||||
const {
|
||||
readFile,
|
||||
writeFile,
|
||||
readdir,
|
||||
mkdir,
|
||||
} = require('fs').promises;
|
||||
|
||||
const {
|
||||
preferencesFile,
|
||||
customBackgroundsDirectory,
|
||||
integratedBackgroundsDirectory,
|
||||
} = require('../paths');
|
||||
|
||||
const Window = require('./Window');
|
||||
|
||||
const {
|
||||
PREFERENCES_GET,
|
||||
PREFERENCES_GET_ALL,
|
||||
PREFERENCES_GET_BACKGROUND_FILE_CONTENTS,
|
||||
PREFERENCES_GET_BACKGROUNDS,
|
||||
PREFERENCES_HIDE,
|
||||
PREFERENCES_SET,
|
||||
PREFERENCES_SHOW,
|
||||
|
||||
} = require('../views/preferences/ipc');
|
||||
|
||||
class Preferences {
|
||||
constructor(options = {}) {
|
||||
const {
|
||||
onChange,
|
||||
defaults,
|
||||
logger,
|
||||
} = options;
|
||||
|
||||
this.logger = logger;
|
||||
this.filePath = preferencesFile;
|
||||
this.onChange = onChange || (() => {});
|
||||
this.data = new Map(Object.entries(defaults));
|
||||
|
||||
ipcMain.handle(PREFERENCES_GET, (event, key) => this.get(key));
|
||||
ipcMain.handle(PREFERENCES_GET_ALL, () => this.getAll());
|
||||
ipcMain.handle(PREFERENCES_GET_BACKGROUND_FILE_CONTENTS, this.getBackgroundFileContents.bind(this));
|
||||
ipcMain.handle(PREFERENCES_GET_BACKGROUNDS, this.getBackgrounds.bind(this));
|
||||
|
||||
ipcMain.on(PREFERENCES_SET, (event, key, value) => this.set(key, value));
|
||||
ipcMain.on(PREFERENCES_HIDE, () => this.window.hide());
|
||||
ipcMain.on(PREFERENCES_SHOW, () => this.window.show());
|
||||
|
||||
this.window = new Window({
|
||||
name: 'preferences',
|
||||
titleBarStyle: 'hidden',
|
||||
transparent: true,
|
||||
vibrancy: 'sidebar',
|
||||
trafficLightPosition: { x: 20, y: 20 },
|
||||
sourceFile: resolve(__dirname, '../views/preferences/preferences.html'),
|
||||
webPreferences: {
|
||||
preload: resolve(__dirname, '../views/preferences/preload.js'),
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.debug('Preferences module created.');
|
||||
this.load();
|
||||
}
|
||||
|
||||
async load() {
|
||||
try {
|
||||
this.logger.debug(`Trying to load user preferences from “${this.filePath}”.`);
|
||||
|
||||
Object
|
||||
.entries(JSON.parse(await readFile(this.filePath, 'utf8')))
|
||||
.forEach((item) => this.set(...item, false));
|
||||
|
||||
await mkdir(customBackgroundsDirectory);
|
||||
} catch ({ message }) {
|
||||
if (/enoent/i.test(message)) {
|
||||
this.logger.debug('Looks like it’s the first time starting Timestamp. No user preferences found.');
|
||||
} else if (/eexist/i.test(message)) {
|
||||
this.logger.debug('Directory for custom backgrounds has already been created.');
|
||||
} else {
|
||||
this.logger.error(message);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
async save() {
|
||||
await writeFile(this.filePath, JSON.stringify(Object.fromEntries(this.data)));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
getAll() {
|
||||
return new Map(this.data);
|
||||
}
|
||||
|
||||
async getBackgroundFileContents(event, filePath) {
|
||||
try {
|
||||
return await readFile(filePath, { encoding: 'utf-8' });
|
||||
} catch ({ message }) {
|
||||
if (/enoent/i.test(message)) {
|
||||
this.logger.warning(`Couldn’t find background file “${filePath}”.`);
|
||||
} else {
|
||||
this.logger.error(message);
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
async getBackgrounds() {
|
||||
const backgrounds = [];
|
||||
const directories = [integratedBackgroundsDirectory, customBackgroundsDirectory];
|
||||
|
||||
await Promise.all(
|
||||
directories.map(async (directory) => {
|
||||
try {
|
||||
(await readdir(directory)).forEach(
|
||||
(background) => backgrounds.push(resolve(directory, background)),
|
||||
);
|
||||
} catch ({ message }) {
|
||||
this.logger.warn(message);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return backgrounds;
|
||||
}
|
||||
|
||||
get(key) {
|
||||
return this.data.get(key);
|
||||
}
|
||||
|
||||
set(key, value, persist = true) {
|
||||
this.logger.debug(`Setting value for preference with key ”${key}” to “${value}”.`);
|
||||
|
||||
this.data.set(key, value);
|
||||
this.onChange(key, value);
|
||||
|
||||
if (persist) {
|
||||
this.save();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Preferences;
|
55
app/components/SystemTray.js
Normal file
@ -0,0 +1,55 @@
|
||||
const { Tray, nativeImage } = require('electron');
|
||||
|
||||
class SystemTray {
|
||||
constructor(options = {}) {
|
||||
const { onClick, logger } = options;
|
||||
|
||||
this.logger = logger;
|
||||
this.prefix = '';
|
||||
this.tray = new Tray(
|
||||
nativeImage.createEmpty(),
|
||||
);
|
||||
|
||||
this.logger.debug('System tray created.');
|
||||
|
||||
if (typeof onClick === 'function') {
|
||||
this.tray.on('click', onClick);
|
||||
}
|
||||
}
|
||||
|
||||
getBounds() {
|
||||
return this.tray.getBounds();
|
||||
}
|
||||
|
||||
getPrefix() {
|
||||
return this.prefix;
|
||||
}
|
||||
|
||||
setPrefix(value) {
|
||||
this.prefix = value;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
getLabel() {
|
||||
return this.tray.getTitle();
|
||||
}
|
||||
|
||||
setLabel(label) {
|
||||
const { tray } = this;
|
||||
|
||||
if (tray.isDestroyed()) {
|
||||
this.logger.error('Unable to set label since tray is destroyed.');
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
tray.setTitle(`${this.getPrefix()}${this.label = label}`, {
|
||||
fontType: 'monospacedDigit',
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SystemTray;
|
83
app/components/Updater.js
Normal file
@ -0,0 +1,83 @@
|
||||
const { lt } = require('semver');
|
||||
const { get } = require('https');
|
||||
const { autoUpdater } = require('electron');
|
||||
|
||||
class Updater {
|
||||
constructor(options = {}) {
|
||||
const {
|
||||
checkEvery = 1000 * 60 * 60,
|
||||
currentVersion,
|
||||
feedUrl,
|
||||
logger,
|
||||
onUpdateDownloaded,
|
||||
} = options;
|
||||
|
||||
this.feedUrl = feedUrl;
|
||||
this.logger = logger;
|
||||
|
||||
autoUpdater.on('error', this.onError.bind(this));
|
||||
autoUpdater.on('update-downloaded', onUpdateDownloaded);
|
||||
|
||||
setInterval(this.onTick.bind(this, currentVersion), checkEvery);
|
||||
|
||||
this.logger.debug('Updater module created.');
|
||||
this.logger.debug(`Checking “${feedUrl}” every ${checkEvery / 1000} seconds for updates.`);
|
||||
|
||||
this.onTick(currentVersion);
|
||||
}
|
||||
|
||||
async fetchJson() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = get(this.feedUrl, (response) => {
|
||||
const { statusCode } = response;
|
||||
const json = [];
|
||||
|
||||
if (statusCode !== 200) {
|
||||
reject(new Error(`Feed url is not reachable. Response status code is ${statusCode}.`));
|
||||
} else {
|
||||
response.on('data', json.push.bind(json));
|
||||
response.on('end', () => {
|
||||
try {
|
||||
resolve(JSON.parse(json.join()));
|
||||
} catch (e) {
|
||||
this.logger.error('Couldn’t parse feed response.');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
request.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
quitAndInstall() {
|
||||
autoUpdater.quitAndInstall();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
async onTick(currentVersion) {
|
||||
try {
|
||||
const { version } = await this.fetchJson();
|
||||
|
||||
if (lt(currentVersion, version) === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
autoUpdater.setFeedURL(this.feedUrl);
|
||||
autoUpdater.checkForUpdates();
|
||||
|
||||
this.logger.debug(`Update available. (${currentVersion} -> ${version})`);
|
||||
} catch ({ message }) {
|
||||
this.logger.warning(message);
|
||||
}
|
||||
}
|
||||
|
||||
onError({ message }) {
|
||||
this.logger.warning(message);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Updater;
|
120
app/components/Window.js
Normal file
@ -0,0 +1,120 @@
|
||||
const { BrowserWindow, shell } = require('electron');
|
||||
|
||||
class Window {
|
||||
constructor(options = {}) {
|
||||
const defaults = {
|
||||
alwaysOnTop: true,
|
||||
frame: false,
|
||||
minimizable: false,
|
||||
resizable: false,
|
||||
show: false,
|
||||
};
|
||||
|
||||
const {
|
||||
sourceFile,
|
||||
name,
|
||||
onReady,
|
||||
...rest
|
||||
} = options;
|
||||
|
||||
this.name = name;
|
||||
this.browserWindow = new BrowserWindow({ ...defaults, ...rest });
|
||||
|
||||
if (typeof onReady === 'function') {
|
||||
this.browserWindow.on('ready-to-show', onReady);
|
||||
}
|
||||
|
||||
// @see https://www.electronjs.org/docs/tutorial/security#12-disable-or-limit-navigation
|
||||
this.browserWindow.webContents.on('will-navigate', (event, navigationUrl) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (/^https?:\/\//.test(navigationUrl)) {
|
||||
shell.openExternal(navigationUrl);
|
||||
}
|
||||
});
|
||||
|
||||
// @see https://www.electronjs.org/docs/tutorial/security#13-disable-or-limit-creation-of-new-windows
|
||||
this.browserWindow.webContents.on('new-window', (event) => event.preventDefault());
|
||||
|
||||
this
|
||||
.browserWindow
|
||||
.on('close', this.onClose.bind(this))
|
||||
.loadFile(sourceFile);
|
||||
}
|
||||
|
||||
isSame(window) {
|
||||
return window === this.browserWindow;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.browserWindow.destroy();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
show() {
|
||||
this.browserWindow.webContents.send(`${this.name}.show`);
|
||||
this.browserWindow.show();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.browserWindow.webContents.send(`${this.name}.hide`);
|
||||
this.browserWindow.hide();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
toggleVisibility() {
|
||||
return this.isVisible() ? this.hide() : this.show();
|
||||
}
|
||||
|
||||
isVisible() {
|
||||
return this.browserWindow.isVisible();
|
||||
}
|
||||
|
||||
onClose(event) {
|
||||
this.hide();
|
||||
|
||||
// By default all windows in Timestamp are hidden and not closed
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
getBrowserWindow() {
|
||||
return this.browserWindow;
|
||||
}
|
||||
|
||||
getWebContents() {
|
||||
return this.browserWindow.webContents;
|
||||
}
|
||||
|
||||
getContentSize() {
|
||||
return this.browserWindow.getContentSize();
|
||||
}
|
||||
|
||||
setContentSize(width, height) {
|
||||
if (typeof width !== 'number' || typeof height !== 'number') {
|
||||
throw new Error('Window.setContentSize has been call with non-numeric arguments.');
|
||||
}
|
||||
|
||||
this.browserWindow.setContentSize(width, height, true);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
getPosition() {
|
||||
return this.browserWindow.getPosition();
|
||||
}
|
||||
|
||||
setPosition(x, y, centerToX = true) {
|
||||
this.browserWindow.setPosition(
|
||||
centerToX ? Math.round(x - (this.browserWindow.getSize()[0] / 2)) : x,
|
||||
y,
|
||||
);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Window;
|
156
app/index.js
Normal file
@ -0,0 +1,156 @@
|
||||
const {
|
||||
app, screen, ipcMain, BrowserWindow,
|
||||
} = require('electron');
|
||||
|
||||
const { arch, platform, release } = require('os');
|
||||
const { resolve } = require('path');
|
||||
const { parseInline } = require('marked');
|
||||
|
||||
const Calendar = require('./components/Calendar');
|
||||
const Clock = require('./components/Clock');
|
||||
const Locale = require('./components/Locale');
|
||||
const Logger = require('./components/Logger');
|
||||
const Preferences = require('./components/Preferences');
|
||||
const SystemTray = require('./components/SystemTray');
|
||||
const Updater = require('./components/Updater');
|
||||
const { PREFERENCES_CHANGED } = require('./views/preferences/ipc');
|
||||
const { integratedBackgroundsDirectory } = require('./paths');
|
||||
|
||||
const {
|
||||
APP_QUIT,
|
||||
APP_RESIZE_WINDOW,
|
||||
APP_RESTART,
|
||||
APP_TRANSLATE,
|
||||
APP_UPDATE_DOWNLOADED,
|
||||
} = require('./ipc');
|
||||
|
||||
const defaultPreferences = {
|
||||
calendarBackground: resolve(integratedBackgroundsDirectory, 'empty.svg'),
|
||||
calendarLegendFormat: 'MMMM y',
|
||||
calendarTodayFormat: 'EEEE,\ndo MMMM',
|
||||
clockFormat: 'PPPP',
|
||||
openAtLogin: false,
|
||||
};
|
||||
|
||||
(async () => {
|
||||
await app.whenReady();
|
||||
|
||||
app.dock.hide();
|
||||
|
||||
return new class {
|
||||
constructor() {
|
||||
const currentVersion = app.getVersion();
|
||||
|
||||
this.logger = new Logger();
|
||||
|
||||
this.logger.debug(`Starting Timestamp v${currentVersion} on “${platform()}-${arch()} v${release()}”.`);
|
||||
this.logger.debug(`Running in ${app.isPackaged ? 'production' : 'development'} mode.`);
|
||||
|
||||
if (app.isPackaged) {
|
||||
this.updater = new Updater({
|
||||
currentVersion,
|
||||
feedUrl: 'https://mzdr.github.io/timestamp/update.json',
|
||||
logger: this.logger,
|
||||
onUpdateDownloaded: this.onUpdateDownloaded.bind(this),
|
||||
});
|
||||
}
|
||||
|
||||
this.locale = new Locale({
|
||||
logger: this.logger,
|
||||
preferred: app.getLocale(),
|
||||
});
|
||||
|
||||
this.tray = new SystemTray({
|
||||
logger: this.logger,
|
||||
onClick: this.onTrayClicked.bind(this),
|
||||
});
|
||||
|
||||
this.clock = new Clock({
|
||||
format: defaultPreferences.clockFormat,
|
||||
locale: this.locale,
|
||||
onTick: this.tray.setLabel.bind(this.tray),
|
||||
});
|
||||
|
||||
this.calendar = new Calendar({
|
||||
locale: this.locale,
|
||||
logger: this.logger,
|
||||
});
|
||||
|
||||
this.preferences = new Preferences({
|
||||
defaults: defaultPreferences,
|
||||
logger: this.logger,
|
||||
onChange: this.onPreferencesChanged.bind(this),
|
||||
});
|
||||
|
||||
ipcMain.on(APP_QUIT, () => app.exit());
|
||||
ipcMain.on(APP_RESTART, this.onRestart.bind(this));
|
||||
ipcMain.on(APP_RESIZE_WINDOW, this.onResizeWindow.bind(this));
|
||||
ipcMain.handle(APP_TRANSLATE, this.onTranslate.bind(this));
|
||||
}
|
||||
|
||||
onRestart() {
|
||||
if (this.updater === undefined) {
|
||||
return this;
|
||||
}
|
||||
|
||||
this.calendar.window.destroy();
|
||||
this.preferences.window.destroy();
|
||||
this.updater.quitAndInstall();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
onResizeWindow({ sender }, { width, height }) {
|
||||
const { calendar, preferences } = this;
|
||||
const window = BrowserWindow.fromWebContents(sender);
|
||||
|
||||
[calendar, preferences]
|
||||
.find((view) => view.window.isSame(window))
|
||||
.window
|
||||
.setContentSize(width, height);
|
||||
}
|
||||
|
||||
onPreferencesChanged(key, value) {
|
||||
if (key === 'openAtLogin') {
|
||||
app.setLoginItemSettings({ openAtLogin: value });
|
||||
} else if (key === 'clockFormat') {
|
||||
this.clock.setFormat(value);
|
||||
} else if (/^calendar/.test(key)) {
|
||||
this.calendar.window.getWebContents().send(PREFERENCES_CHANGED, key, value);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
onTranslate(event, key, options = {}) {
|
||||
const { markdown = false } = options;
|
||||
const translation = this.locale.translate(key);
|
||||
|
||||
if (markdown) {
|
||||
return parseInline(translation);
|
||||
}
|
||||
|
||||
return translation;
|
||||
}
|
||||
|
||||
onTrayClicked() {
|
||||
const { calendar, tray } = this;
|
||||
const bounds = tray.getBounds();
|
||||
const currentMousePosition = screen.getCursorScreenPoint();
|
||||
const currentDisplay = screen.getDisplayNearestPoint(currentMousePosition);
|
||||
const yOffset = 6;
|
||||
|
||||
// Always center calendar window relative to tray icon
|
||||
calendar
|
||||
.window
|
||||
.setPosition(bounds.x + (bounds.width / 2), currentDisplay.workArea.y + yOffset)
|
||||
.toggleVisibility();
|
||||
}
|
||||
|
||||
onUpdateDownloaded() {
|
||||
this.tray.setPrefix('→ ');
|
||||
this.preferences.window.getWebContents().send(APP_UPDATE_DOWNLOADED);
|
||||
this.calendar.window.getWebContents().send(APP_UPDATE_DOWNLOADED);
|
||||
}
|
||||
}();
|
||||
})();
|
28
app/ipc.js
Normal file
@ -0,0 +1,28 @@
|
||||
const { ipcRenderer } = require('electron');
|
||||
const { productName, version, copyright } = require('../package.json');
|
||||
|
||||
const APP_QUIT = 'app.quit';
|
||||
const APP_RESIZE_WINDOW = 'app.resize-window';
|
||||
const APP_RESTART = 'app.restart';
|
||||
const APP_TRANSLATE = 'app.translate';
|
||||
const APP_UPDATE_DOWNLOADED = 'app.update-downloaded';
|
||||
|
||||
module.exports = {
|
||||
APP_QUIT,
|
||||
APP_RESIZE_WINDOW,
|
||||
APP_RESTART,
|
||||
APP_TRANSLATE,
|
||||
APP_UPDATE_DOWNLOADED,
|
||||
|
||||
api: {
|
||||
productName,
|
||||
version,
|
||||
copyright,
|
||||
|
||||
on: (channel, fn) => ipcRenderer.on(`app.${channel}`, fn),
|
||||
quit: () => ipcRenderer.send(APP_QUIT),
|
||||
resizeWindow: (payload) => ipcRenderer.send(APP_RESIZE_WINDOW, payload),
|
||||
restart: () => ipcRenderer.send(APP_RESTART),
|
||||
translate: (key, options) => ipcRenderer.invoke(APP_TRANSLATE, key, options),
|
||||
},
|
||||
};
|
50
app/locales/de.js
Normal file
@ -0,0 +1,50 @@
|
||||
module.exports = {
|
||||
name: 'Deutsch',
|
||||
locale: 'de',
|
||||
app: {
|
||||
restart: 'Neustarten',
|
||||
updateDownloaded: 'Eine neue Version von Timestamp wurde heruntergeladen. Bitte starten Sie die App neu, um sie zu aktualisieren.',
|
||||
},
|
||||
preferences: {
|
||||
category: {
|
||||
general: 'Generell',
|
||||
tray: 'System Tray',
|
||||
calendar: 'Kalender',
|
||||
shortcuts: 'Tastaturkürzel',
|
||||
},
|
||||
openAtLogin: {
|
||||
label: 'Autostart',
|
||||
description: 'Aktivieren Sie diese Option, wenn Sie möchten, dass Timestamp automatisch beim Starten des Computers gestartet werden soll.',
|
||||
},
|
||||
clockFormat: {
|
||||
label: 'Format der Uhr',
|
||||
description: 'Das [Format](https://date-fns.org/docs/format) der Uhrzeitanzeige im System-Tray.',
|
||||
},
|
||||
calendarBackground: {
|
||||
label: 'Hintergrund',
|
||||
description: 'Wählen Sie einen Kalenderhintergrund aus der Ihrem Geschmack entspricht.',
|
||||
},
|
||||
calendarLegendFormat: {
|
||||
label: 'Format der Legende',
|
||||
description: 'Das [Format](https://date-fns.org/docs/format) der Legende über dem Monat.',
|
||||
},
|
||||
calendarTodayFormat: {
|
||||
label: 'Format des aktuellen Tages',
|
||||
description: 'Das [Format](https://date-fns.org/docs/format) des aktuellen Tages welches im Kalenderkopf angezeigt wird.',
|
||||
},
|
||||
shortcuts: {
|
||||
description: 'Im Folgenden finden Sie eine vollständige Liste der Tastenkombinationen, die Sie im Kalenderfenster verwenden können.',
|
||||
keys: [
|
||||
['W', 'Wochennummern anzeigen'],
|
||||
['↑', 'Nächstes Jahr'],
|
||||
['↓', 'Vorheriges Jahr'],
|
||||
['→', 'Nächster Monat'],
|
||||
['←', 'Vorheriger Monat'],
|
||||
['⌘+,', 'Einstellungen anzeigen'],
|
||||
['⌘+Q', 'Timestamp beenden'],
|
||||
['Esc', 'Fenster schließen'],
|
||||
['Leertaste', 'Aktuellen Tag anzeigen'],
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
50
app/locales/en.js
Normal file
@ -0,0 +1,50 @@
|
||||
module.exports = {
|
||||
name: 'English',
|
||||
locale: 'en',
|
||||
app: {
|
||||
restart: 'Restart',
|
||||
updateDownloaded: 'A new version of Timestamp has been downloaded. Please restart the app in order to update it.',
|
||||
},
|
||||
preferences: {
|
||||
category: {
|
||||
general: 'General',
|
||||
tray: 'System tray',
|
||||
calendar: 'Calendar',
|
||||
shortcuts: 'Shortcuts',
|
||||
},
|
||||
openAtLogin: {
|
||||
label: 'Open at login',
|
||||
description: 'Enable this option if you want Timestamp to start automatically when you start your computer.',
|
||||
},
|
||||
clockFormat: {
|
||||
label: 'Clock format',
|
||||
description: 'The format [pattern](https://date-fns.org/docs/format) of the system tray clock.',
|
||||
},
|
||||
calendarBackground: {
|
||||
label: 'Background',
|
||||
description: 'Choose a calendar background that suits your personal liking.',
|
||||
},
|
||||
calendarLegendFormat: {
|
||||
label: 'Legend format',
|
||||
description: 'The format [pattern](https://date-fns.org/docs/format) of the legend above the month.',
|
||||
},
|
||||
calendarTodayFormat: {
|
||||
label: 'Today format',
|
||||
description: 'The format [pattern](https://date-fns.org/docs/format) of the today display in the calendar head.',
|
||||
},
|
||||
shortcuts: {
|
||||
description: 'See below for a complete list of keyboard shortcuts that you can use in the calendar window.',
|
||||
keys: [
|
||||
['W', 'Toggle week numbers'],
|
||||
['↑', 'Next year'],
|
||||
['↓', 'Previous year'],
|
||||
['→', 'Next month'],
|
||||
['←', 'Previous month'],
|
||||
['⌘+,', 'Show preferences'],
|
||||
['⌘+Q', 'Quit timestamp'],
|
||||
['Esc', 'Close window'],
|
||||
['Space', 'Go to today'],
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
7
app/locales/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
const de = require('./de');
|
||||
const en = require('./en');
|
||||
|
||||
module.exports = {
|
||||
de,
|
||||
en,
|
||||
};
|
11
app/paths.js
Normal file
@ -0,0 +1,11 @@
|
||||
const { app } = require('electron');
|
||||
const { resolve } = require('path');
|
||||
|
||||
const storagePath = app.getPath('userData');
|
||||
|
||||
module.exports = {
|
||||
integratedBackgroundsDirectory: resolve(__dirname, 'assets/backgrounds'),
|
||||
customBackgroundsDirectory: resolve(storagePath, 'Backgrounds'),
|
||||
logFile: resolve(storagePath, 'Output.log'),
|
||||
preferencesFile: resolve(storagePath, 'UserPreferences.json'),
|
||||
};
|
13
app/styles/components/button-primary.css
Normal file
@ -0,0 +1,13 @@
|
||||
.button-primary {
|
||||
background-blend-mode: color-burn;
|
||||
background-color: var(--color-brand);
|
||||
background-image: linear-gradient(180deg, #fff, #ccc);
|
||||
border-radius: 3px;
|
||||
border: 0;
|
||||
color: var(--palette-white);
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
outline: 0;
|
||||
padding: 6px 12px;
|
||||
text-shadow: -1px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
18
app/styles/components/container-alert.css
Normal file
@ -0,0 +1,18 @@
|
||||
.container-alert {
|
||||
align-items: center;
|
||||
display: grid;
|
||||
grid-gap: var(--grid-gap);
|
||||
grid-template-areas: "icon message actions";
|
||||
overflow: hidden;
|
||||
padding: var(--grid-gap);
|
||||
}
|
||||
|
||||
.container-alert > .icon {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: calc(var(--type-size) * 2);
|
||||
grid-area: icon;
|
||||
justify-content: center;
|
||||
}
|
||||
.container-alert > .message { grid-area: message; }
|
||||
.container-alert > .actions { grid-area: actions; }
|
43
app/styles/components/form-group.css
Normal file
@ -0,0 +1,43 @@
|
||||
.form-group {
|
||||
display: grid;
|
||||
grid-gap: calc(var(--grid-gap) / 3) calc(var(--grid-gap) * 3);
|
||||
grid-template-areas: "label action" "description action";
|
||||
grid-template-columns: 1fr min-content;
|
||||
}
|
||||
|
||||
.form-group > .label {
|
||||
align-self: center;
|
||||
grid-area: label;
|
||||
}
|
||||
|
||||
.form-group > .description {
|
||||
color: var(--color-text-shy);
|
||||
font-size: var(--shy-size);
|
||||
grid-area: description;
|
||||
}
|
||||
|
||||
.form-group > .action {
|
||||
align-self: flex-start;
|
||||
grid-area: action;
|
||||
margin: 0;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.form-group > .action.-text {
|
||||
color: var(--palette-black);
|
||||
font: inherit;
|
||||
outline: 0;
|
||||
padding: 2px 4px;
|
||||
resize: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-group > .action.-toggle {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.form-group > .action.-select {
|
||||
padding: 5px;
|
||||
text-transform: capitalize;
|
||||
}
|
21
app/styles/components/is-native.css
Normal file
@ -0,0 +1,21 @@
|
||||
.is-native {
|
||||
--palette-black: #272727;
|
||||
--palette-white: #f5f2f2;
|
||||
|
||||
--color-line: #dcd9da;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.is-native {
|
||||
--palette-black: #322d2c;
|
||||
--palette-white: #e0e0df;
|
||||
|
||||
--color-background-primary: var(--palette-black);
|
||||
--color-text-primary: var(--palette-white);
|
||||
--color-line: #434041;
|
||||
}
|
||||
}
|
||||
|
||||
.is-native.-transparent {
|
||||
background-color: transparent;
|
||||
}
|
23
app/styles/components/list-shortcuts.css
Normal file
@ -0,0 +1,23 @@
|
||||
.list-shortcuts {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
grid-gap: calc(var(--grid-gap) / 2) var(--grid-gap);
|
||||
}
|
||||
|
||||
.list-shortcuts > .keys {
|
||||
align-items: center;
|
||||
display: grid;
|
||||
font-size: var(--shy-size);
|
||||
grid-auto-flow: column;
|
||||
grid-column-gap: calc(var(--grid-gap) / 2);
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.list-shortcuts > .keys > .key {
|
||||
background-color: var(--palette-white);
|
||||
background-image: linear-gradient(to top, #e6e6e6, transparent);
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||||
color: var(--palette-black);
|
||||
padding: 6px 12px;
|
||||
}
|
27
app/styles/meta/colors.css
Normal file
@ -0,0 +1,27 @@
|
||||
:root {
|
||||
--palette-blue: #008bed;
|
||||
--palette-green: #0cbd00;
|
||||
--palette-black: #111;
|
||||
--palette-gray-20: #333;
|
||||
--palette-gray-40: #666;
|
||||
--palette-gray-60: #999;
|
||||
--palette-gray-80: #ccc;
|
||||
--palette-white: #fff;
|
||||
|
||||
--color-brand: var(--palette-blue);
|
||||
--color-background-primary: var(--palette-white);
|
||||
--color-line: var(--palette-gray-80);
|
||||
--color-text-primary: var(--palette-black);
|
||||
--color-text-shy: var(--palette-gray-60);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--palette-white: #eee;
|
||||
|
||||
--color-background-primary: var(--palette-black);
|
||||
--color-text-primary: var(--palette-white);
|
||||
--color-text-shy: var(--palette-gray-40);
|
||||
--color-line: var(--palette-gray-20);
|
||||
}
|
||||
}
|
4
app/styles/meta/fx.css
Normal file
@ -0,0 +1,4 @@
|
||||
:root {
|
||||
--fx-duration: 300ms;
|
||||
--fx-radius: 15px;
|
||||
}
|
3
app/styles/meta/grid.css
Normal file
@ -0,0 +1,3 @@
|
||||
:root {
|
||||
--grid-gap: 22px;
|
||||
}
|
9
app/styles/meta/typography.css
Normal file
@ -0,0 +1,9 @@
|
||||
:root {
|
||||
--type-family: system-ui;
|
||||
--type-rendering: optimizeLegibility;
|
||||
--type-rhythm: 1.5;
|
||||
--type-sentence: 60ch;
|
||||
--type-size: 14px;
|
||||
|
||||
--shy-size: 12px;
|
||||
}
|
16
app/styles/shared/base.css
Normal file
@ -0,0 +1,16 @@
|
||||
:root {
|
||||
background-color: var(--color-background-primary);
|
||||
box-sizing: border-box;
|
||||
color: var(--color-text-primary);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
6
app/styles/shared/typography.css
Normal file
@ -0,0 +1,6 @@
|
||||
:root {
|
||||
font-family: var(--type-family);
|
||||
font-size: var(--type-size);
|
||||
line-height: var(--type-rhythm);
|
||||
text-rendering: var(--type-rendering);
|
||||
}
|
7
app/styles/styles.css
Normal file
@ -0,0 +1,7 @@
|
||||
@import "meta/colors.css";
|
||||
@import "meta/fx.css";
|
||||
@import "meta/grid.css";
|
||||
@import "meta/typography.css";
|
||||
@import "shared/base.css";
|
||||
@import "shared/typography.css";
|
||||
@import "components/is-native.css";
|
@ -0,0 +1,22 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.calendar-background {
|
||||
display: grid;
|
||||
grid-template-areas: "main";
|
||||
}
|
||||
|
||||
.calendar-background > .image,
|
||||
.calendar-background > .slot {
|
||||
grid-area: main;
|
||||
}
|
||||
|
||||
.calendar-background > .image {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.calendar-background > .slot {
|
||||
display: block;
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
import { dispatch, upgrade } from '../../../../node_modules/@browserkids/dom/index.js';
|
||||
|
||||
export default class CalendarBackground extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
upgrade(this, `
|
||||
<template>
|
||||
<link rel="stylesheet" href="calendar-background/calendar-background.css">
|
||||
<div @postupdate.window="onPostUpdate" class="calendar-background">
|
||||
<figure class="image" #$content></figure>
|
||||
<slot class="slot"></slot>
|
||||
</div>
|
||||
</template>
|
||||
`);
|
||||
|
||||
const { preferences } = window;
|
||||
|
||||
preferences.on('changed', this.onPreferencesChanged.bind(this));
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
onPreferencesChanged(event, key, value) {
|
||||
if (key !== 'calendarBackground') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.render(value);
|
||||
}
|
||||
|
||||
async render(background) {
|
||||
const { preferences } = window;
|
||||
const { $content } = this.$refs;
|
||||
|
||||
$content.innerHTML = await preferences.getBackgroundFileContents(
|
||||
background || await preferences.get('calendarBackground'),
|
||||
);
|
||||
|
||||
dispatch(this, 'postrender');
|
||||
}
|
||||
|
||||
async onPostUpdate() {
|
||||
const { calendar } = window;
|
||||
const now = await calendar.getDate();
|
||||
|
||||
Object.assign(this.dataset, {
|
||||
hour: now.getHours(),
|
||||
month: now.getMonth(),
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
28
app/views/calendar/calendar-legend/calendar-legend.css
Normal file
@ -0,0 +1,28 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.calendar-legend {
|
||||
align-items: center;
|
||||
color: var(--color-text-shy);
|
||||
display: grid;
|
||||
grid-column-gap: 20px;
|
||||
grid-template-columns: 50px minmax(60px, max-content) 50px;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.calendar-legend::before,
|
||||
.calendar-legend::after {
|
||||
content: "";
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.calendar-legend::before {
|
||||
background-image: linear-gradient(to left, var(--color-line), transparent);
|
||||
}
|
||||
|
||||
.calendar-legend::after {
|
||||
background-image: linear-gradient(to right, var(--color-line), transparent);
|
||||
}
|
27
app/views/calendar/calendar-legend/calendar-legend.js
Normal file
@ -0,0 +1,27 @@
|
||||
import { dispatch, upgrade } from '../../../../node_modules/@browserkids/dom/index.js';
|
||||
|
||||
export default class CalendarLegend extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
upgrade(this, `
|
||||
<template>
|
||||
<link rel="stylesheet" href="calendar-legend/calendar-legend.css">
|
||||
<span class="calendar-legend" #$content @postupdate.window="onPostUpdate"> </span>
|
||||
</template>
|
||||
`);
|
||||
}
|
||||
|
||||
async onPostUpdate({ detail }) {
|
||||
const { selectedMonth } = detail;
|
||||
const { calendar, preferences } = window;
|
||||
const { $content } = this.$refs;
|
||||
|
||||
$content.textContent = await calendar.getDate({
|
||||
date: selectedMonth,
|
||||
format: await preferences.get('calendarLegendFormat'),
|
||||
});
|
||||
|
||||
dispatch(this, 'postrender');
|
||||
}
|
||||
}
|
72
app/views/calendar/calendar-month/calendar-month.css
Normal file
@ -0,0 +1,72 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
:host(.show-weeks) > .calendar-month {
|
||||
grid-template-areas: "no weekdays" "weeks days";
|
||||
grid-template-columns: 1fr 7fr;
|
||||
}
|
||||
|
||||
:host(:not(.show-weeks)) > .calendar-month > .no,
|
||||
:host(:not(.show-weeks)) > .calendar-month > .weeks {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.calendar-month {
|
||||
display: grid;
|
||||
grid-row-gap: 10px;
|
||||
grid-template-areas: "weekdays" "days";
|
||||
grid-template-columns: 1fr;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.calendar-month > .weekdays {
|
||||
display: grid;
|
||||
grid-area: weekdays;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.calendar-month > .no {
|
||||
color: var(--color-line);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.calendar-month > .weeks {
|
||||
color: var(--color-text-shy);
|
||||
grid-area: weeks;
|
||||
}
|
||||
|
||||
.calendar-month > .weeks,
|
||||
.calendar-month > .days {
|
||||
display: grid;
|
||||
grid-row-gap: 6px;
|
||||
}
|
||||
|
||||
.calendar-month > .weeks > .week,
|
||||
.calendar-month > .days > .day {
|
||||
align-items: center;
|
||||
aspect-ratio: 1 / 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.calendar-month > .days {
|
||||
grid-area: days;
|
||||
grid-auto-flow: row;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.calendar-month > .days > .day.-previous,
|
||||
.calendar-month > .days > .day.-next {
|
||||
color: var(--color-text-shy);
|
||||
}
|
||||
|
||||
.calendar-month > .days > .day.-today {
|
||||
background-color: var(--color-brand);
|
||||
background-image: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.1));
|
||||
border-radius: 50%;
|
||||
color: var(--palette-white);
|
||||
}
|
49
app/views/calendar/calendar-month/calendar-month.js
Normal file
@ -0,0 +1,49 @@
|
||||
import { dispatch, upgrade } from '../../../../node_modules/@browserkids/dom/index.js';
|
||||
|
||||
export default class CalendarMonth extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
upgrade(this, `
|
||||
<template>
|
||||
<link rel="stylesheet" href="calendar-month/calendar-month.css">
|
||||
<div class="calendar-month" #$content @postupdate.window="onPostUpdate"></div>
|
||||
</template>
|
||||
`);
|
||||
}
|
||||
|
||||
async onPostUpdate({ detail }) {
|
||||
const { selectedMonth } = detail;
|
||||
const { calendar } = window;
|
||||
const { $content } = this.$refs;
|
||||
const { weekdays, weeks, days } = await calendar.getCalendar({ date: selectedMonth });
|
||||
|
||||
const $weekdays = weekdays.map((weekday) => `<span class="weekday">${weekday}</span>`).join('');
|
||||
const $weeks = weeks.map((week) => `<span class="week">${week}</span>`).join('');
|
||||
|
||||
const $days = days.map(({
|
||||
day, isToday, isThisWeek, previousMonth, nextMonth,
|
||||
}) => {
|
||||
const cssClass = [
|
||||
'day',
|
||||
isToday ? '-today' : null,
|
||||
isThisWeek ? '-week' : null,
|
||||
previousMonth ? '-previous' : null,
|
||||
nextMonth ? '-next' : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
return `<span class="${cssClass}">${day.getDate()}</span>`;
|
||||
}).join('');
|
||||
|
||||
$content.innerHTML = `
|
||||
<div class="no">#</div>
|
||||
<div class="weekdays">${$weekdays}</div>
|
||||
<div class="weeks">${$weeks}</div>
|
||||
<div class="days">${$days}</div>
|
||||
`;
|
||||
|
||||
dispatch(this, 'postrender');
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
:host {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.calendar-navigation {
|
||||
align-content: center;
|
||||
backdrop-filter: blur(20px);
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
display: grid;
|
||||
grid-row-gap: calc(var(--grid-gap) * 3);
|
||||
padding: var(--grid-gap);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.calendar-navigation > .year,
|
||||
.calendar-navigation > .month {
|
||||
display: grid;
|
||||
grid-row-gap: var(--grid-gap);
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.go-to {
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
border-radius: 1em;
|
||||
border: 0;
|
||||
color: currentColor;
|
||||
display: flex;
|
||||
font-family: var(--type-family);
|
||||
font-size: calc(var(--type-size) * 1.5);
|
||||
font-weight: 200;
|
||||
justify-content: center;
|
||||
line-height: calc(var(--type-rhythm) * 1.5);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.go-to.-current,
|
||||
.go-to.-year {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.go-to.-current {
|
||||
box-shadow: inset 0 0 0 1px currentColor;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.calendar-navigation {
|
||||
background-color: rgba(17, 17, 17, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.icon-chevron {
|
||||
opacity: 0.6;
|
||||
width: 16px;
|
||||
}
|
127
app/views/calendar/calendar-navigation/calendar-navigation.js
Normal file
@ -0,0 +1,127 @@
|
||||
import { dispatch, upgrade } from '../../../../node_modules/@browserkids/dom/index.js';
|
||||
|
||||
export default class CalendarNavigation extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
upgrade(this, `
|
||||
<template>
|
||||
<link rel="stylesheet" href="calendar-navigation/calendar-navigation.css">
|
||||
|
||||
<div
|
||||
class="calendar-navigation"
|
||||
@keydown.window="onKeyDown"
|
||||
@postupdate.window="onPostUpdate"
|
||||
>
|
||||
<div class="year">
|
||||
<button class="go-to" @click="goPreviousYear">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16" class="icon-chevron">
|
||||
<path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 010 .708L5.707 8l5.647 5.646a.5.5 0 01-.708.708l-6-6a.5.5 0 010-.708l6-6a.5.5 0 01.708 0z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<strong class="go-to -year" #$year></strong>
|
||||
|
||||
<button class="go-to" @click="goNextYear">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16" class="icon-chevron">
|
||||
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 01.708 0l6 6a.5.5 0 010 .708l-6 6a.5.5 0 01-.708-.708L10.293 8 4.646 2.354a.5.5 0 010-.708z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="month">
|
||||
<button #$month class="go-to" @click="goMonth"></button>
|
||||
<button #$month class="go-to" @click="goMonth"></button>
|
||||
<button #$month class="go-to" @click="goMonth"></button>
|
||||
<button #$month class="go-to" @click="goMonth"></button>
|
||||
<button #$month class="go-to" @click="goMonth"></button>
|
||||
<button #$month class="go-to" @click="goMonth"></button>
|
||||
<button #$month class="go-to" @click="goMonth"></button>
|
||||
<button #$month class="go-to" @click="goMonth"></button>
|
||||
<button #$month class="go-to" @click="goMonth"></button>
|
||||
<button #$month class="go-to" @click="goMonth"></button>
|
||||
<button #$month class="go-to" @click="goMonth"></button>
|
||||
<button #$month class="go-to" @click="goMonth"></button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
`);
|
||||
}
|
||||
|
||||
onKeyDown(e) {
|
||||
const { key } = e;
|
||||
|
||||
if (key === 'ArrowRight') {
|
||||
this.goNextMonth();
|
||||
} else if (key === 'ArrowLeft') {
|
||||
this.goPreviousMonth();
|
||||
} else if (key === 'ArrowUp') {
|
||||
this.goNextYear();
|
||||
} else if (key === 'ArrowDown') {
|
||||
this.goPreviousYear();
|
||||
} else if (key === ' ') {
|
||||
this.goToday();
|
||||
|
||||
// Hitting space might as well trigger a focused button (month),
|
||||
// avoid that behaviour.
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
async onPostUpdate({ detail }) {
|
||||
const { calendar } = window;
|
||||
const { selectedMonth } = detail;
|
||||
const { $year, $month } = this.$refs;
|
||||
const months = await calendar.getMonths();
|
||||
|
||||
$year.textContent = selectedMonth.getFullYear();
|
||||
|
||||
$month.forEach(($el, month) => {
|
||||
$el.classList.toggle(
|
||||
'-current',
|
||||
month === selectedMonth.getMonth(),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
$el.textContent = months[month];
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
goToday() {
|
||||
dispatch(this, 'change');
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
goMonth({ currentTarget }) {
|
||||
dispatch(this, 'change', { set: { month: this.$refs.$month.indexOf(currentTarget) } });
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
goPreviousYear() {
|
||||
dispatch(this, 'change', { diff: { years: -1 } });
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
goNextYear() {
|
||||
dispatch(this, 'change', { diff: { years: 1 } });
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
goPreviousMonth() {
|
||||
dispatch(this, 'change', { diff: { months: -1 } });
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
goNextMonth() {
|
||||
dispatch(this, 'change', { diff: { months: 1 } });
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
@keyframes bounce {
|
||||
0%, 20%, 53%, 80%, 100% {
|
||||
animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
40%, 43% {
|
||||
animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
|
||||
transform: translate3d(0, -10px, 0);
|
||||
}
|
||||
|
||||
70% {
|
||||
animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
|
||||
transform: translate3d(0, -6px, 0);
|
||||
}
|
||||
|
||||
90% {
|
||||
transform: translate3d(0, -2px, 0);
|
||||
}
|
||||
}
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.icon-dots {
|
||||
display: block;
|
||||
height: 6px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.icon-dots > .dot {
|
||||
fill: currentColor;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.icon-dots > .dot:nth-of-type(2) {
|
||||
animation-duration: 1.25s;
|
||||
animation-fill-mode: both;
|
||||
animation-iteration-count: infinite;
|
||||
animation-name: bounce;
|
||||
animation-play-state: paused;
|
||||
transform-origin: center bottom;
|
||||
}
|
||||
|
||||
:host(.update-downloaded) .icon-dots > .dot:nth-of-type(2) {
|
||||
animation-play-state: running;
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { createShadowRoot } from '../../../../node_modules/@browserkids/dom/index.js';
|
||||
|
||||
export default class CalendarShowPreferences extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
createShadowRoot(this, `
|
||||
<template>
|
||||
<link rel="stylesheet" href="calendar-show-preferences/calendar-show-preferences.css">
|
||||
<svg class="icon-dots" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 6">
|
||||
<circle class="dot" cx="21" cy="3" r="3" />
|
||||
<circle class="dot" cx="12" cy="3" r="3" />
|
||||
<circle class="dot" cx="3" cy="3" r="3" />
|
||||
</svg>
|
||||
</template>
|
||||
`);
|
||||
|
||||
const { app } = window;
|
||||
|
||||
app.on('update-downloaded', this.onUpdateDownloaded.bind(this));
|
||||
}
|
||||
|
||||
onUpdateDownloaded() {
|
||||
this.classList.add('update-downloaded');
|
||||
}
|
||||
}
|
12
app/views/calendar/calendar-today/calendar-today.css
Normal file
@ -0,0 +1,12 @@
|
||||
:host {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.calendar-today {
|
||||
display: block;
|
||||
font-size: calc(var(--type-size) * 2.5);
|
||||
font-weight: 100;
|
||||
padding: calc(var(--grid-gap) * 2.5) var(--grid-gap);
|
||||
white-space: pre;
|
||||
}
|
38
app/views/calendar/calendar-today/calendar-today.js
Normal file
@ -0,0 +1,38 @@
|
||||
import { dispatch, upgrade } from '../../../../node_modules/@browserkids/dom/index.js';
|
||||
|
||||
export default class CalendarToday extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
upgrade(this, `
|
||||
<template>
|
||||
<link rel="stylesheet" href="calendar-today/calendar-today.css">
|
||||
<slot class="calendar-today"> </slot>
|
||||
</template>
|
||||
`);
|
||||
|
||||
const { preferences } = window;
|
||||
|
||||
preferences.on('changed', this.onPreferencesChanged.bind(this));
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
onPreferencesChanged(event, key, value) {
|
||||
if (key !== 'calendarTodayFormat') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.render(value);
|
||||
}
|
||||
|
||||
async render(format) {
|
||||
const { calendar, preferences } = window;
|
||||
|
||||
this.innerHTML = await calendar.getDate({
|
||||
format: (format || await preferences.get('calendarTodayFormat')).replace(/\n/g, '\'<br>\''),
|
||||
});
|
||||
|
||||
dispatch(this, 'postrender');
|
||||
}
|
||||
}
|
47
app/views/calendar/calendar-view/calendar-view.css
Normal file
@ -0,0 +1,47 @@
|
||||
:host {
|
||||
display: grid;
|
||||
grid-template-areas: "background" "legend" "month";
|
||||
padding-bottom: var(--grid-gap);
|
||||
position: relative;
|
||||
user-select: none;
|
||||
width: 340px;
|
||||
}
|
||||
|
||||
calendar-background {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
calendar-legend {
|
||||
padding: 14px var(--grid-gap) 22px;
|
||||
}
|
||||
|
||||
calendar-month {
|
||||
padding: 0 var(--grid-gap);
|
||||
}
|
||||
|
||||
calendar-background { grid-area: background; }
|
||||
calendar-legend { grid-area: legend; }
|
||||
calendar-month { grid-area: month; }
|
||||
|
||||
calendar-show-preferences {
|
||||
padding: 18px 12px;
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 6px;
|
||||
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
calendar-navigation {
|
||||
inset: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
calendar-navigation[hidden] {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
calendar-navigation:not([hidden]) {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
118
app/views/calendar/calendar-view/calendar-view.js
Normal file
@ -0,0 +1,118 @@
|
||||
import { dispatch, upgrade } from '../../../../node_modules/@browserkids/dom/index.js';
|
||||
|
||||
export default class CalendarView extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
upgrade(this, `
|
||||
<template>
|
||||
<link rel="stylesheet" href="calendar-view/calendar-view.css" @keydown.window="onKeyDown" />
|
||||
<calendar-background @postrender="onPostRender">
|
||||
<calendar-today @postrender="onPostRender" @click="onTodayClicked"></calendar-today>
|
||||
<calendar-show-preferences @click="onShowPreferences"></calendar-show-preferences>
|
||||
</calendar-background>
|
||||
<calendar-legend @click="onLegendClicked" @postrender="onPostRender"></calendar-legend>
|
||||
<calendar-month #$month @postrender="onPostRender"></calendar-month>
|
||||
<calendar-navigation #$navigation @change="onChange" hidden></calendar-navigation>
|
||||
</template>
|
||||
`);
|
||||
|
||||
const { calendar, preferences } = window;
|
||||
|
||||
calendar.on('hide', this.onTodayClicked.bind(this));
|
||||
preferences.on('changed', this.onPreferencesChanged.bind(this));
|
||||
|
||||
setInterval(this.update.bind(this), 1000);
|
||||
|
||||
this.selectedMonth = null;
|
||||
this.lastUpdate = null;
|
||||
|
||||
this.onTodayClicked();
|
||||
}
|
||||
|
||||
async update(force = false) {
|
||||
const { selectedMonth } = this;
|
||||
const { calendar } = window;
|
||||
const now = await calendar.getDate();
|
||||
const isFirstTime = this.lastUpdate === null;
|
||||
const isSameHour = await calendar.isSameHour(this.lastUpdate || now, now);
|
||||
|
||||
if (force === false && isFirstTime === false && isSameHour) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastUpdate = now;
|
||||
|
||||
dispatch(window, 'postupdate', { selectedMonth });
|
||||
}
|
||||
|
||||
onKeyDown(e) {
|
||||
const { key, metaKey } = e;
|
||||
const { app, calendar, preferences } = window;
|
||||
const { $navigation } = this.$refs;
|
||||
|
||||
if (key === 'Escape') {
|
||||
if ($navigation.hidden) {
|
||||
calendar.hide();
|
||||
} else {
|
||||
$navigation.hidden = true;
|
||||
}
|
||||
} else if (key === 'w' && metaKey === false) {
|
||||
this.onToggle({ weeks: true });
|
||||
} else if (key === ',' && metaKey) {
|
||||
preferences.show();
|
||||
} else if (key === 'q' && metaKey) {
|
||||
app.quit();
|
||||
}
|
||||
}
|
||||
|
||||
onPreferencesChanged() {
|
||||
this.update(true);
|
||||
}
|
||||
|
||||
onToggle({ weeks }) {
|
||||
const { $month } = this.$refs;
|
||||
|
||||
if (weeks) {
|
||||
$month.classList.toggle('show-weeks');
|
||||
}
|
||||
|
||||
this.onPostRender();
|
||||
}
|
||||
|
||||
onPostRender() {
|
||||
const { app } = window;
|
||||
|
||||
app.resizeWindow({
|
||||
width: this.offsetWidth,
|
||||
height: this.offsetHeight,
|
||||
});
|
||||
}
|
||||
|
||||
onShowPreferences() {
|
||||
const { preferences } = window;
|
||||
|
||||
preferences.show();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
onTodayClicked() {
|
||||
this.onChange();
|
||||
}
|
||||
|
||||
onLegendClicked() {
|
||||
this.$refs.$navigation.hidden = false;
|
||||
}
|
||||
|
||||
async onChange({ detail } = {}) {
|
||||
const { calendar } = window;
|
||||
|
||||
this.selectedMonth = await calendar.getDate(detail ? { date: this.selectedMonth, ...detail } : {});
|
||||
this.update(true);
|
||||
|
||||
if (detail?.diff?.years === undefined) {
|
||||
this.$refs.$navigation.hidden = true;
|
||||
}
|
||||
}
|
||||
}
|
12
app/views/calendar/calendar.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'">
|
||||
<link rel="stylesheet" href="../../styles/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<calendar-view></calendar-view>
|
||||
<script src="./renderer.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
27
app/views/calendar/ipc.js
Normal file
@ -0,0 +1,27 @@
|
||||
const { ipcRenderer } = require('electron');
|
||||
|
||||
const CALENDAR_GET_CALENDAR = 'calendar.get-calendar';
|
||||
const CALENDAR_GET_DATE = 'calendar.get-date';
|
||||
const CALENDAR_GET_MONTHS = 'calendar.get-months';
|
||||
const CALENDAR_HIDE = 'calendar.hide';
|
||||
const CALENDAR_IS_SAME_HOUR = 'calendar.is-same-hour';
|
||||
const CALENDAR_SHOW = 'calendar.show';
|
||||
|
||||
module.exports = {
|
||||
CALENDAR_GET_CALENDAR,
|
||||
CALENDAR_GET_DATE,
|
||||
CALENDAR_GET_MONTHS,
|
||||
CALENDAR_HIDE,
|
||||
CALENDAR_IS_SAME_HOUR,
|
||||
CALENDAR_SHOW,
|
||||
|
||||
api: {
|
||||
getCalendar: (payload) => ipcRenderer.invoke(CALENDAR_GET_CALENDAR, payload),
|
||||
getDate: (payload) => ipcRenderer.invoke(CALENDAR_GET_DATE, payload),
|
||||
getMonths: () => ipcRenderer.invoke(CALENDAR_GET_MONTHS),
|
||||
hide: () => ipcRenderer.send(CALENDAR_HIDE),
|
||||
isSameHour: (...payload) => ipcRenderer.invoke(CALENDAR_IS_SAME_HOUR, ...payload),
|
||||
on: (channel, fn) => ipcRenderer.on(`calendar.${channel}`, fn),
|
||||
show: () => ipcRenderer.send(CALENDAR_SHOW),
|
||||
},
|
||||
};
|
9
app/views/calendar/preload.js
Normal file
@ -0,0 +1,9 @@
|
||||
const { contextBridge } = require('electron');
|
||||
|
||||
const { api: app } = require('../../ipc');
|
||||
const { api: preferences } = require('../preferences/ipc');
|
||||
const { api: calendar } = require('./ipc');
|
||||
|
||||
contextBridge.exposeInMainWorld('app', app);
|
||||
contextBridge.exposeInMainWorld('calendar', calendar);
|
||||
contextBridge.exposeInMainWorld('preferences', preferences);
|
15
app/views/calendar/renderer.js
Normal file
@ -0,0 +1,15 @@
|
||||
import CalendarBackground from './calendar-background/calendar-background.js';
|
||||
import CalendarLegend from './calendar-legend/calendar-legend.js';
|
||||
import CalendarMonth from './calendar-month/calendar-month.js';
|
||||
import CalendarNavigation from './calendar-navigation/calendar-navigation.js';
|
||||
import CalendarShowPreferences from './calendar-show-preferences/calendar-show-preferences.js';
|
||||
import CalendarToday from './calendar-today/calendar-today.js';
|
||||
import CalendarView from './calendar-view/calendar-view.js';
|
||||
|
||||
customElements.define('calendar-background', CalendarBackground);
|
||||
customElements.define('calendar-legend', CalendarLegend);
|
||||
customElements.define('calendar-month', CalendarMonth);
|
||||
customElements.define('calendar-navigation', CalendarNavigation);
|
||||
customElements.define('calendar-show-preferences', CalendarShowPreferences);
|
||||
customElements.define('calendar-today', CalendarToday);
|
||||
customElements.define('calendar-view', CalendarView);
|
24
app/views/common/translation-key/translation-key.js
Normal file
@ -0,0 +1,24 @@
|
||||
import { dispatch } from '../../../../node_modules/@browserkids/dom/index.js';
|
||||
|
||||
export default class TranslationKey extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.originalKey = this.textContent;
|
||||
this.onPostUpdate();
|
||||
|
||||
window.addEventListener('postupdate', this.onPostUpdate.bind(this));
|
||||
}
|
||||
|
||||
async onPostUpdate() {
|
||||
const { app } = window;
|
||||
|
||||
if (this.hasAttribute('markdown')) {
|
||||
this.innerHTML = await app.translate(this.originalKey, { markdown: true });
|
||||
} else {
|
||||
this.textContent = await app.translate(this.originalKey);
|
||||
}
|
||||
|
||||
dispatch(this, 'postrender');
|
||||
}
|
||||
}
|
32
app/views/preferences/ipc.js
Normal file
@ -0,0 +1,32 @@
|
||||
const { ipcRenderer } = require('electron');
|
||||
|
||||
const PREFERENCES_CHANGED = 'preferences.changed';
|
||||
const PREFERENCES_GET = 'preferences.get';
|
||||
const PREFERENCES_GET_ALL = 'preferences.getAll';
|
||||
const PREFERENCES_GET_BACKGROUND_FILE_CONTENTS = 'preferences.get-background-file-contents';
|
||||
const PREFERENCES_GET_BACKGROUNDS = 'preferences.get-backgrounds';
|
||||
const PREFERENCES_HIDE = 'preferences.hide';
|
||||
const PREFERENCES_SET = 'preferences.set';
|
||||
const PREFERENCES_SHOW = 'preferences.show';
|
||||
|
||||
module.exports = {
|
||||
PREFERENCES_CHANGED,
|
||||
PREFERENCES_GET,
|
||||
PREFERENCES_GET_ALL,
|
||||
PREFERENCES_GET_BACKGROUND_FILE_CONTENTS,
|
||||
PREFERENCES_GET_BACKGROUNDS,
|
||||
PREFERENCES_HIDE,
|
||||
PREFERENCES_SET,
|
||||
PREFERENCES_SHOW,
|
||||
|
||||
api: {
|
||||
get: (key) => ipcRenderer.invoke(PREFERENCES_GET, key),
|
||||
getAll: () => ipcRenderer.invoke(PREFERENCES_GET_ALL),
|
||||
getBackgroundFileContents: (payload) => ipcRenderer.invoke(PREFERENCES_GET_BACKGROUND_FILE_CONTENTS, payload),
|
||||
getBackgrounds: () => ipcRenderer.invoke(PREFERENCES_GET_BACKGROUNDS),
|
||||
hide: () => ipcRenderer.send(PREFERENCES_HIDE),
|
||||
on: (channel, fn) => ipcRenderer.on(`preferences.${channel}`, fn),
|
||||
set: (key, value) => ipcRenderer.send(PREFERENCES_SET, key, value),
|
||||
show: () => ipcRenderer.send(PREFERENCES_SHOW),
|
||||
},
|
||||
};
|
126
app/views/preferences/preferences-view/preferences-view.css
Normal file
@ -0,0 +1,126 @@
|
||||
:host {
|
||||
display: block;
|
||||
user-select: none;
|
||||
width: 720px;
|
||||
}
|
||||
|
||||
translation-key a {
|
||||
color: var(--color-brand);
|
||||
}
|
||||
|
||||
.preferences-view {
|
||||
display: grid;
|
||||
grid-template-areas: "side alert" "side contents";
|
||||
grid-template-columns: 192px 1fr;
|
||||
grid-template-rows: min-content 1fr;
|
||||
}
|
||||
|
||||
.preferences-view > .side { grid-area: side; }
|
||||
.preferences-view > .alert { grid-area: alert; }
|
||||
.preferences-view > .contents { grid-area: contents; }
|
||||
|
||||
.preferences-view > .alert,
|
||||
.preferences-view > .contents {
|
||||
background-color: var(--color-background-primary);
|
||||
}
|
||||
|
||||
.preferences-view > .alert {
|
||||
border-bottom: 1px solid var(--color-line);
|
||||
}
|
||||
|
||||
.preferences-view > .alert.-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.preferences-side {
|
||||
box-shadow: inset -1px 0 0 var(--color-line);
|
||||
display: grid;
|
||||
grid-template-rows: repeat(3, min-content);
|
||||
padding: var(--grid-gap);
|
||||
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.preferences-side > .logo {
|
||||
margin: calc(var(--grid-gap) * 2) auto var(--grid-gap);
|
||||
width: 128px;
|
||||
}
|
||||
|
||||
.preferences-side > .name {
|
||||
font-size: calc(var(--type-size) * 1.5);
|
||||
font-weight: 300;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.preferences-side > .navigation {
|
||||
margin: calc(var(--grid-gap) * 2) 0;
|
||||
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.preferences-side > .about {
|
||||
align-self: flex-end;
|
||||
color: var(--color-text-shy);
|
||||
font-size: var(--shy-size);
|
||||
}
|
||||
|
||||
.preferences-navigation {
|
||||
display: grid;
|
||||
grid-row-gap: calc(var(--grid-gap) / 3);
|
||||
}
|
||||
|
||||
.preferences-navigation > .item {
|
||||
align-items: center;
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: currentColor;
|
||||
display: grid;
|
||||
font: inherit;
|
||||
grid-column-gap: calc(var(--grid-gap) / 3);
|
||||
grid-template-columns: min-content 1fr;
|
||||
outline: 0;
|
||||
padding: 4px 8px;
|
||||
position: relative;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.preferences-navigation > .item.-active::before {
|
||||
background-color: var(--color-text-primary);
|
||||
border-radius: 5px;
|
||||
content: "";
|
||||
height: 100%;
|
||||
left: 0;
|
||||
opacity: 0.14;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.preferences-navigation > .item > .icon {
|
||||
height: 14px;
|
||||
stroke-width: 1.5;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.preferences-contents {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.preferences-contents > .content {
|
||||
align-content: flex-start;
|
||||
display: grid;
|
||||
grid-area: 1 / 1 / -1 / -1;
|
||||
grid-row-gap: var(--grid-gap);
|
||||
padding: calc(var(--grid-gap) * 2);
|
||||
}
|
||||
|
||||
.preferences-contents > .content:not(.-active) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.preferences-contents > .content > .description {
|
||||
color: var(--color-text-shy);
|
||||
}
|
196
app/views/preferences/preferences-view/preferences-view.js
Normal file
@ -0,0 +1,196 @@
|
||||
import { upgrade } from '../../../../node_modules/@browserkids/dom/index.js';
|
||||
|
||||
export default class PreferencesView extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const { app } = window;
|
||||
|
||||
upgrade(this, `
|
||||
<template>
|
||||
<link rel="stylesheet" href="../../styles/components/button-primary.css">
|
||||
<link rel="stylesheet" href="../../styles/components/container-alert.css">
|
||||
<link rel="stylesheet" href="../../styles/components/form-group.css">
|
||||
<link rel="stylesheet" href="../../styles/components/list-shortcuts.css">
|
||||
<link rel="stylesheet" href="preferences-view/preferences-view.css">
|
||||
|
||||
<div class="preferences-view">
|
||||
<aside class="preferences-side side">
|
||||
<img class="logo" src="../../assets/logo.svg" alt="">
|
||||
<h1 class="name">${app.productName}</h1>
|
||||
|
||||
<nav class="navigation preferences-navigation">
|
||||
<button #$tab class="item" @click="onCategoryClicked">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
|
||||
<translation-key class="title">preferences.category.general</translation-key>
|
||||
</button>
|
||||
<button #$tab class="item" @click="onCategoryClicked">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg>
|
||||
<translation-key class="title">preferences.category.tray</translation-key>
|
||||
</button>
|
||||
<button #$tab class="item" @click="onCategoryClicked">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg>
|
||||
<translation-key class="title">preferences.category.calendar</translation-key>
|
||||
</button>
|
||||
<button #$tab class="item" @click="onCategoryClicked">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path d="M18 3a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3H6a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3V6a3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3h12a3 3 0 0 0 3-3 3 3 0 0 0-3-3z"></path></svg>
|
||||
<translation-key class="title">preferences.category.shortcuts</translation-key>
|
||||
</button>
|
||||
</nav>
|
||||
<section class="about preferences-about">v${app.version}</section>
|
||||
</aside>
|
||||
|
||||
<section #$alert class="alert -hidden container-alert">
|
||||
<span class="icon">🎉</span>
|
||||
<translation-key>app.updateDownloaded</translation-key>
|
||||
<div class="actions">
|
||||
<button @click="onRestartClicked" class="button-primary">
|
||||
<translation-key>app.restart</translation-key>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<form
|
||||
#$form
|
||||
@input="onInput"
|
||||
@postrender="onPostRender"
|
||||
@keydown.window="onKeyDown"
|
||||
class="contents preferences-contents"
|
||||
>
|
||||
<section class="content" #$content>
|
||||
<label class="form-group">
|
||||
<input type="checkbox" name="openAtLogin" class="action -toggle">
|
||||
<translation-key class="label">preferences.openAtLogin.label</translation-key>
|
||||
<translation-key class="description">preferences.openAtLogin.description</translation-key>
|
||||
</label>
|
||||
</section>
|
||||
<section class="content" #$content>
|
||||
<label class="form-group">
|
||||
<input type="text" name="clockFormat" class="action -text" size="12">
|
||||
<translation-key class="label">preferences.clockFormat.label</translation-key>
|
||||
<translation-key class="description" markdown>preferences.clockFormat.description</translation-key>
|
||||
</label>
|
||||
</section>
|
||||
<section class="content" #$content>
|
||||
<label class="form-group">
|
||||
<select #$backgrounds name="calendarBackground" class="action -select"></select>
|
||||
<translation-key class="label">preferences.calendarBackground.label</translation-key>
|
||||
<translation-key class="description">preferences.calendarBackground.description</translation-key>
|
||||
</label>
|
||||
<label class="form-group">
|
||||
<input type="text" name="calendarLegendFormat" class="action -text" size="8">
|
||||
<translation-key class="label">preferences.calendarLegendFormat.label</translation-key>
|
||||
<translation-key class="description" markdown>preferences.calendarLegendFormat.description</translation-key>
|
||||
</label>
|
||||
<label class="form-group">
|
||||
<textarea name="calendarTodayFormat" class="action -text" cols="10"></textarea>
|
||||
<translation-key class="label">preferences.calendarTodayFormat.label</translation-key>
|
||||
<translation-key class="description" markdown>preferences.calendarTodayFormat.description</translation-key>
|
||||
</label>
|
||||
</section>
|
||||
<section class="content" #$content>
|
||||
<translation-key class="description">preferences.shortcuts.description</translation-key>
|
||||
<dl class="list-shortcuts" #$keys></dl>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
`);
|
||||
|
||||
this.render();
|
||||
|
||||
app.on('update-downloaded', this.onUpdateDownloaded.bind(this));
|
||||
}
|
||||
|
||||
async render() {
|
||||
const { app, preferences } = window;
|
||||
|
||||
const {
|
||||
$form,
|
||||
$backgrounds,
|
||||
$keys,
|
||||
$tab,
|
||||
} = this.$refs;
|
||||
|
||||
const all = await preferences.getAll();
|
||||
const backgrounds = await preferences.getBackgrounds();
|
||||
const shortcuts = await app.translate('preferences.shortcuts.keys');
|
||||
|
||||
backgrounds
|
||||
.map((background) => ([background, background.split('/').pop().split('.').shift()]))
|
||||
.map(([value, name]) => `<option value="${value}">${name}</option>`)
|
||||
.forEach((background) => $backgrounds.insertAdjacentHTML('beforeend', background));
|
||||
|
||||
shortcuts
|
||||
.map(([keys, label]) => ([keys.split('+').map((key) => `<span class="key">${key}</span>`).join('+'), label]))
|
||||
.map(([keys, label]) => `<dt class="keys">${keys}</dt><dd class="label">${label}</dd>`)
|
||||
.forEach((shortcut) => $keys.insertAdjacentHTML('beforeend', shortcut));
|
||||
|
||||
Array
|
||||
.from(all)
|
||||
.filter(([key]) => $form[key])
|
||||
.forEach(([key, value]) => {
|
||||
$form[key][typeof value === 'boolean' ? 'checked' : 'value'] = value;
|
||||
});
|
||||
|
||||
this.onCategoryClicked({ currentTarget: $tab[0] });
|
||||
}
|
||||
|
||||
onInput({ target }) {
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
type,
|
||||
checked,
|
||||
} = target;
|
||||
|
||||
const { preferences } = window;
|
||||
const isBoolean = ['on', 'off'].indexOf(value) >= 0 && ['checkbox', 'radio'].indexOf(type) >= 0;
|
||||
|
||||
preferences.set(name, isBoolean ? checked : value);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
onCategoryClicked({ currentTarget }) {
|
||||
const { $content, $tab } = this.$refs;
|
||||
const index = $tab.indexOf(currentTarget);
|
||||
const toggleActive = ($el, position) => $el.classList.toggle('-active', index === position);
|
||||
|
||||
[$tab, $content].forEach(($el) => $el.forEach(toggleActive));
|
||||
|
||||
this.onPostRender();
|
||||
}
|
||||
|
||||
onKeyDown({ key }) {
|
||||
const { preferences } = window;
|
||||
|
||||
if (key === 'Escape') {
|
||||
preferences.hide();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
onRestartClicked() {
|
||||
const { app } = window;
|
||||
|
||||
app.restart();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
onUpdateDownloaded() {
|
||||
this.$refs.$alert.classList.remove('-hidden');
|
||||
this.onPostRender();
|
||||
}
|
||||
|
||||
onPostRender() {
|
||||
const { app } = window;
|
||||
|
||||
app.resizeWindow({
|
||||
width: this.offsetWidth,
|
||||
height: this.offsetHeight,
|
||||
});
|
||||
}
|
||||
}
|
12
app/views/preferences/preferences.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html class="is-native -transparent">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
|
||||
<link rel="stylesheet" href="../../styles/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<preferences-view></preferences-view>
|
||||
<script src="./renderer.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
7
app/views/preferences/preload.js
Normal file
@ -0,0 +1,7 @@
|
||||
const { contextBridge } = require('electron');
|
||||
|
||||
const { api: app } = require('../../ipc');
|
||||
const { api: preferences } = require('./ipc');
|
||||
|
||||
contextBridge.exposeInMainWorld('app', app);
|
||||
contextBridge.exposeInMainWorld('preferences', preferences);
|
5
app/views/preferences/renderer.js
Normal file
@ -0,0 +1,5 @@
|
||||
import PreferencesView from './preferences-view/preferences-view.js';
|
||||
import TranslationKey from '../common/translation-key/translation-key.js';
|
||||
|
||||
customElements.define('preferences-view', PreferencesView);
|
||||
customElements.define('translation-key', TranslationKey);
|
BIN
build/_icon.icns
Normal file
10
build/entitlements.mac.plist
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
BIN
build/icon.icns
Normal file
4155
package-lock.json
generated
Normal file
65
package.json
Normal file
@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "timestamp",
|
||||
"productName": "Timestamp",
|
||||
"description": "A better macOS menu bar clock with a customizable date/time display and a calendar.",
|
||||
"version": "1.1.0",
|
||||
"author": "Sebastian Prein <hi@sebastianprein.com>",
|
||||
"copyright": "© 2021 Sebastian Prein",
|
||||
"homepage": "https://github.com/mzdr/timestamp",
|
||||
"license": "MIT",
|
||||
"main": "app/index.js",
|
||||
"keywords": [
|
||||
"calendar",
|
||||
"clock",
|
||||
"customizable",
|
||||
"date",
|
||||
"electron",
|
||||
"macos",
|
||||
"menubar",
|
||||
"time",
|
||||
"timestamp"
|
||||
],
|
||||
"build": {
|
||||
"appId": "com.mzdr.timestamp",
|
||||
"files": [
|
||||
"app/**/*",
|
||||
"node_modules/**/*",
|
||||
"package.json"
|
||||
],
|
||||
"mac": {
|
||||
"category": "public.app-category.utilities",
|
||||
"hardenedRuntime": true,
|
||||
"gatekeeperAssess": false,
|
||||
"entitlements": "build/entitlements.mac.plist",
|
||||
"entitlementsInherit": "build/entitlements.mac.plist",
|
||||
"darkModeSupport": true
|
||||
},
|
||||
"afterSign": "scripts/notarize.js"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "electron-builder",
|
||||
"clean": "rimraf dist",
|
||||
"lint": "eslint ./app",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"prebuild": "npm run lint && npm run clean",
|
||||
"start": "electron ./app --enable-logging",
|
||||
"test": "npm run lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@browserkids/dom": "^0.5.0",
|
||||
"date-fns": "^2.23.0",
|
||||
"marked": "^2.1.3",
|
||||
"semver": "^7.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dotenv": "^10.0.0",
|
||||
"electron": "^13.1.7",
|
||||
"electron-builder": "^22.11.7",
|
||||
"electron-notarize": "^1.0.1",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb-base": "^14.2.1",
|
||||
"eslint-plugin-import": "^2.23.4",
|
||||
"rimraf": "^3.0.2"
|
||||
}
|
||||
}
|
28
scripts/notarize.js
Normal file
@ -0,0 +1,28 @@
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
require('dotenv').config();
|
||||
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const { notarize } = require('electron-notarize');
|
||||
const { build } = require('../package.json');
|
||||
|
||||
exports.default = async function notarizing(context) {
|
||||
const { electronPlatformName, appOutDir, packager } = context;
|
||||
const isUnpacked = process.argv.includes('--dir');
|
||||
const isMacOs = electronPlatformName === 'darwin';
|
||||
|
||||
if (isUnpacked || isMacOs === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const appBundleId = build.appId;
|
||||
const appPath = `${appOutDir}/${packager.appInfo.productFilename}.app`;
|
||||
const appleId = process.env.APPLE_ID;
|
||||
const appleIdPassword = process.env.APPLE_ID_PASSWORD;
|
||||
|
||||
await notarize({
|
||||
appBundleId,
|
||||
appPath,
|
||||
appleId,
|
||||
appleIdPassword,
|
||||
});
|
||||
};
|