Repository: muffinista/before-dawn
Branch: main
Commit: f873649102d4
Files: 139
Total size: 305.3 KB
Directory structure:
gitextract_ufwarflo/
├── .babelrc
├── .browserslistrc
├── .dockerignore
├── .github/
│ └── workflows/
│ ├── ci.yml
│ ├── codeql-analysis.yml
│ ├── eslint.yml
│ └── release.yml
├── .gitignore
├── .nvmrc
├── CHANGELOG.md
├── LICENSE.txt
├── README.md
├── assets/
│ ├── icon-paused.xcf
│ ├── icon.xcf
│ └── monitor-overlay.psd
├── bin/
│ ├── build-icon.js
│ ├── build-on-ci.js
│ ├── capture-screens.js
│ ├── dev-runner.js
│ ├── download-screensavers.js
│ ├── generate-release.js
│ └── get-release-name
├── build/
│ └── icon.icns
├── code_of_conduct.md
├── docs/
│ ├── .gitignore
│ ├── .ruby-version
│ ├── Gemfile
│ ├── _config.yml
│ ├── _includes/
│ │ ├── footer.html
│ │ ├── head.html
│ │ └── header.html
│ ├── _layouts/
│ │ ├── default.html
│ │ ├── page.html
│ │ └── post.html
│ ├── _posts/
│ │ └── 2016-12-02-welcome-to-jekyll.markdown
│ ├── _sass/
│ │ ├── _base.scss
│ │ ├── _layout.scss
│ │ └── _syntax-highlighting.scss
│ ├── about.md
│ ├── contributing.md
│ ├── creating.md
│ ├── css/
│ │ └── main.scss
│ ├── index.html
│ ├── installing.md
│ └── preferences.md
├── eslint.config.mjs
├── lefthook.yml
├── mise.toml
├── package.json
├── src/
│ ├── css/
│ │ └── styles.scss
│ ├── index.ejs
│ ├── lib/
│ │ ├── package.js
│ │ ├── prefs-schema.json
│ │ ├── prefs.js
│ │ ├── saver-factory.js
│ │ ├── saver-list.js
│ │ └── saver.js
│ ├── main/
│ │ ├── assets/
│ │ │ ├── global.css
│ │ │ ├── grabber.html
│ │ │ ├── grabber.mjs
│ │ │ ├── preload.mjs
│ │ │ ├── shim.html
│ │ │ └── shim.js
│ │ ├── autostarter.js
│ │ ├── bootstrap.js
│ │ ├── dock.js
│ │ ├── index.dev.js
│ │ ├── index.js
│ │ ├── menus.js
│ │ ├── power.js
│ │ ├── release_check.js
│ │ ├── screen.js
│ │ ├── state_manager.js
│ │ ├── system-savers/
│ │ │ ├── __template/
│ │ │ │ ├── index.html
│ │ │ │ └── saver.json
│ │ │ ├── blank/
│ │ │ │ ├── index.html
│ │ │ │ └── saver.json
│ │ │ ├── dimmer/
│ │ │ │ ├── index.html
│ │ │ │ └── saver.json
│ │ │ └── random/
│ │ │ ├── index.html
│ │ │ └── saver.json
│ │ └── windows.js
│ └── renderer/
│ ├── AboutScreen.svelte
│ ├── EditorScreen.svelte
│ ├── NewScreensaverScreen.svelte
│ ├── PrefsScreen.svelte
│ ├── SettingsScreen.svelte
│ ├── components/
│ │ ├── FolderChooser.svelte
│ │ ├── Notarize.js
│ │ ├── SaverForm.svelte
│ │ ├── SaverList.svelte
│ │ ├── SaverOptionInput.svelte
│ │ ├── SaverOptions.svelte
│ │ ├── SaverSummary.svelte
│ │ ├── Spinner.svelte
│ │ └── icons/
│ │ ├── BugIcon.svelte
│ │ ├── FolderIcon.svelte
│ │ ├── ReloadIcon.svelte
│ │ └── SaveIcon.svelte
│ └── main.js
├── test/
│ ├── fixtures/
│ │ ├── bad-config.json
│ │ ├── config-2.json
│ │ ├── config-with-options.json
│ │ ├── config.json
│ │ ├── default-repo.json
│ │ ├── index.html
│ │ ├── invalid.json
│ │ ├── no-requirements.json
│ │ ├── old-config.json
│ │ ├── power/
│ │ │ ├── linux-charged.txt
│ │ │ ├── linux-charging.txt
│ │ │ └── linux-discharging.txt
│ │ ├── release-no-update.json
│ │ ├── release.json
│ │ ├── releases/
│ │ │ └── updates.json
│ │ ├── saver.json
│ │ ├── saver2.json
│ │ └── test-savers.json
│ ├── helpers.js
│ ├── lib/
│ │ ├── package.js
│ │ ├── prefs.js
│ │ ├── saver-factory.js
│ │ ├── saver-list.js
│ │ └── saver.js
│ ├── main/
│ │ ├── power.js
│ │ ├── release_check.js
│ │ └── state_manager.js
│ └── ui/
│ ├── about.js
│ ├── bootstrap.js
│ ├── editor.js
│ ├── new.js
│ ├── prefs.js
│ ├── settings.js
│ └── tray.js
├── tools/
│ ├── build-packages.sh
│ └── update-build-version.js
├── webpack.config.js
├── webpack.main.config.js
└── webpack.renderer.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"presets": ["@babel/preset-env"]
}
================================================
FILE: .browserslistrc
================================================
last 1 electron version
================================================
FILE: .dockerignore
================================================
build
dist
output
tools
docs
assets
coverage
node_modules
.git
.gitignore
.electron-symbols
.DS_Store
.env
.nyc_output
================================================
FILE: .github/workflows/ci.yml
================================================
name: Run tests
on: push
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
steps:
- name: Install ubuntu requirements
if: ${{ matrix.os == 'ubuntu-latest' }}
run: |
sudo apt-get -qq update
sudo apt-get install -y libx11-dev libxss-dev icnsutils graphicsmagick libxtst-dev
- uses: actions/checkout@v5
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Specify MSVS version
if: ${{ matrix.os == 'windows-latest' }}
shell: powershell
run: |
echo "GYP_MSVS_VERSION=2022" >> $env:GITHUB_ENV
- run: npm ci
- run: npm rebuild
- run: npm run test-lib
- name: Run integration tests
if: ${{ matrix.os != 'ubuntu-latest' }}
run: npm run test-ui
- uses: actions/upload-artifact@v4
if: always()
with:
name: logs
#/Users/runner/Library/Logs/mocha/
# ~/.config/Before Dawn/logs/
path: |
C:\Users\runneradmin\AppData\Roaming\mocha\logs\
================================================
FILE: .github/workflows/codeql-analysis.yml
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '42 16 * * 5'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
steps:
- name: Checkout code
uses: actions/checkout@v5
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v4
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
================================================
FILE: .github/workflows/eslint.yml
================================================
name: eslint
# Run this workflow every time a new commit pushed to your repository
on: push
jobs:
# Set the job key. The key is displayed as the job name
# when a job name is not provided
eslint:
# Name the Job
name: Run eslint
# Set the type of machine to run on
runs-on: ubuntu-latest
steps:
- name: Install ubuntu requirements
run: |
sudo apt-get -qq update
sudo apt-get install -y libx11-dev libxss-dev icnsutils graphicsmagick libxtst-dev
# Checks out a copy of your repository on the ubuntu-latest machine
- name: Checkout code
uses: actions/checkout@v5
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Setup code
run: npm ci
- name: Run eslint
run: npm run eslint-all
================================================
FILE: .github/workflows/release.yml
================================================
name: Build release
on:
push:
branches:
- main
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
env:
# https://stackoverflow.com/questions/77251296/distutils-not-found-when-running-npm-install
PYTHON: 3.11
steps:
- name: Install ubuntu requirements
if: ${{ matrix.os == 'ubuntu-latest' }}
run: |
sudo apt-get -qq update
sudo apt-get install -y libx11-dev libxss-dev icnsutils graphicsmagick libxtst-dev
- name: Checkout code
uses: actions/checkout@v5
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Specify MSVS version
if: ${{ matrix.os == 'windows-latest' }}
shell: powershell
run: |
echo "GYP_MSVS_VERSION=2022" >> $env:GITHUB_ENV
- run: npm ci
- run: npm rebuild
- name: Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: npm run release
================================================
FILE: .gitignore
================================================
.DS_Store
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
node_modules
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
bower_components
*.map
.sass-cache
.nyc_output
output/*
dist/*
.electron-symbols
.env
data/savers
data/*.*
.vscode/*
TODO
notes.txt
bin/sentry.properties
# ignore some assets i don't want in git
assets/noun*
# ignore VS code file
*.code-workspace
# Sentry Config File
.env.sentry-build-plugin
================================================
FILE: .nvmrc
================================================
22.21.1
================================================
FILE: CHANGELOG.md
================================================
# Change Log
## [v0.9.25](https://github.com/muffinista/before-dawn/tree/v0.9.24) (2018-04-05)
- Fix some issues with the preview tool, which wasn't properly passing
params to the screensaver preview
## [v0.9.24](https://github.com/muffinista/before-dawn/tree/v0.9.24) (2018-03-15)
- Save settings when 'create screensaver' button is clicked
- Fix issue where screensaver data wasn't loading properly
- Remove lodash from codebase
- Add environment variable toggle for running in development mode without hot reload
- Fix issue with updating timestamp of package update checks
- Remove command-line argument parsing
- Switch a lot of callback-based code to use promises
- Attempt to deal with failed download issues
## [v0.9.23](https://github.com/muffinista/before-dawn/tree/v0.9.23) (2018-02-23)
- Fix logging issue
## [v0.9.22](https://github.com/muffinista/before-dawn/tree/v0.9.22) (2018-02-22)
- Fix issue where screensaver package would be downloaded twice at the
same time, which is definitely a bad thing.
## [v0.9.21](https://github.com/muffinista/before-dawn/tree/v0.9.21) (2018-02-19)
- Add preferences option to run on a single display -- this helps keep
CPU load down
- Add a 'yes/no' boolean option for screensavers -- this can be used
to add boolean fields for screensavers.
- Minor updates to styles -- I'm working on cleaning up the app styles
and CSS
- Keep app running if user quits the main window
- Activate asar for builds -- this should make downloads smaller and
maybe help a bit with performance.
- Cleanup logging output and the code used to setup auto launching
- Update electron version
## [v0.9.20](https://github.com/muffinista/before-dawn/tree/v0.9.20) (2018-01-07)
- Fix some issues with proper release checking -- it was broken before
## [v0.9.19](https://github.com/muffinista/before-dawn/tree/v0.9.19) (2018-01-06)
- Fix issue with missing version data
## [v0.9.18](https://github.com/muffinista/before-dawn/tree/v0.9.18) (2018-01-04)
- Make sure delay/sleep values are integers
## [v0.9.17](https://github.com/muffinista/before-dawn/tree/v0.9.17) (2018-01-03)
- Update method of displaying screensaver windows since things seem to
have broken in High Sierra
- Tweak fullscreen.js to handle OSX issues
- Add some logging code to savers library
- Handle screensaver load/parse errors rather than entirely failing
- Update Electron version
## [v0.9.16](https://github.com/muffinista/before-dawn/tree/v0.9.16) (2017-12-14)
- Notify main process when user preferences have been updated
- Disable 'Save' button when creating new screensaver, but local
directory isn't setup.
- Remove crash reporter, since it's basically unused
## [v0.9.15](https://github.com/muffinista/before-dawn/tree/v0.9.15) (2017-12-14)
- Update raven/sentry setup, fix some issues with paths in the app
## [v0.9.14](https://github.com/muffinista/before-dawn/tree/v0.9.14) (2017-12-11)
- Fix some issues with setting local directory properly
## [v0.9.13](https://github.com/muffinista/before-dawn/tree/v0.9.13) (2017-12-08)
- Replaced React with Vue.js, and did a lot of refactoring and cleanup
of javascript in general. Most of the UI code is in Vue components
now. Previously, it was a mix of React, vanilla JS, and some jQuery.
- Moved code/asset compilation into webpack. This makes development a
little easier to manage.
- Updated how data and objects get passed between the UI and the main
process, which should help make the app more performant.
- Added some tweaks to hopefully take care of some annoying OSX
security issues.
## [v0.9.12](https://github.com/muffinista/before-dawn/tree/v0.9.12) (2017-11-17)
- Tweak some temp directory usage to try and fix some OSX issues
## [v0.9.11](https://github.com/muffinista/before-dawn/tree/v0.9.11) (2017-11-16)
- Add some safety checks to config reading -- if it's corrupted
somehow, just restart with a clean config
- Pass the savers module to windows when opening them -- I think this
is faster than passing data back and forth
## [v0.9.10](https://github.com/muffinista/before-dawn/tree/v0.9.10) (2017-11-13)
- Add a random screensaver picker, as well as basic 'system
screensaver' support -- ie, screensavers that are integral to the
application and not installed as a separate package.
- If 'Run Now' chosen in menu, don't check power state
- Improve dock display -- show icon for more windows and hide only
when all windows are closed
- Tweak layout of prefs window and the preview tool
- Update main process to listen for events from windows and pass data
around. The main process has responsibilty for opening windows,
saving new screensavers, etc.
- Reorganize code for app, switch to a single package.json
- Make a bunch of calls asychronous
- Use async/await in a few places
- Add some data caching to help performance
- When launching screensavers, don't take screengrab unless
requested - this greatly speeds up launch time
- Switch to yarn, cleanup build process
- I'd prefer to not have yarn as a dependency, but it does a better
job of handling installations across multiple platforms -- ie,
windows and OSX
- Add webpack and use it to build UI assets
- I also might get rid of this at some point, and also React for
that matter
- Update bootstrap version and assorted styling
- Update electron version
- Update React version and a bunch of assorted components
- Add mocha tests
- Add appveyor and Travis builds
- Update some stale packages, and remove some dead ones
## [v0.9.9](https://github.com/muffinista/before-dawn/tree/v0.9.9) (2017-11-13)
- This version was yanked before it had a chance to truly shine. RIP
## [v0.9.8](https://github.com/muffinista/before-dawn/tree/v0.9.8) (2017-08-03)
- Minor bug fixes
## [v0.9.7](https://github.com/muffinista/before-dawn/tree/v0.9.7) (2017-07-28)
- Scroll to the currently selected screensaver when rendering prefs
panel
- Handle missing screensaver object in watcher window
## [v0.9.6](https://github.com/muffinista/before-dawn/tree/v0.9.6) (2017-07-11)
- Fix some issues with loading screensavers from folders with spaces in their name
- Add some handlers for power on/off events
- Close running screensavers when the display count changes (the user
has plugged/unplugged a monitor)
## [v0.9.5](https://github.com/muffinista/before-dawn/tree/v0.9.5) (2017-06-27)
- Fix some bugs that can occur when setting a custom screensaver
source directory that either doesn't exist or is empty.
## [v0.9.4](https://github.com/muffinista/before-dawn/tree/v0.9.4) (2017-06-09)
- Disable ASAR packages. I think there's a few things that are broken
when they are being used, and I want to have the whole app running
more smoothly before switching back to them.
- Add link to issues URL so users can report bugs.
- Fix bug where we tried to render preview when a screensaver hadn't
been selected yet.
## [v0.9.3](https://github.com/muffinista/before-dawn/tree/v0.9.3) (2017-06-01)
- Tweak state machine to rely on idle time checks and not much else
- Fix bug with (I think) newer versions of Electron where opening a
BrowserWindow would trigger a reset of idle time on Windows.
- Hide mouse by using robotjs to move mouse off screen when showing screensaver
- Build ASAR packages
- Implement crash reporting, and some sentry.io error reporting
- Add a background color to boot process to look a little nicer
- Optimize screen grabber code, fix some CPU spikes
- Assorted library/code updates
## [v0.9.2](https://github.com/muffinista/before-dawn/tree/v0.9.2) (2017-03-01)
- Fix bug where "don't run on battery" would always be true
- Tweak fullscreen detection code a bit, move into its own module
- Move some platform-specific deps into 'optionalDependencies'
## [v0.9.0](https://github.com/muffinista/before-dawn/tree/v0.9.0) (2017-03-01)
- Check for fullscreen apps and don't activate the screensaver if one is running
- Sort screensavers alphabetically regardless of capitalization
- Add right-click action to system tray
- If disabled, display a 'paused' icon in system tray
- Tweaked tray icon to be a little bolder
- Updated preferences display
- Fixed a case where I think the app would stop checking idle time, so it wouldn't load a screensaver
## [v0.8.3](https://github.com/muffinista/before-dawn/tree/v0.8.3) (2017-01-27)
[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.8.1...v0.8.3)
## [v0.8.1](https://github.com/muffinista/before-dawn/tree/v0.8.1) (2017-01-17)
[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.8.0...v0.8.1)
## [v0.8.0](https://github.com/muffinista/before-dawn/tree/v0.8.0) (2017-01-14)
[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.7.0...v0.8.0)
## [v0.7.0](https://github.com/muffinista/before-dawn/tree/v0.7.0) (2016-07-15)
[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.6.3...v0.7.0)
## [v0.6.3](https://github.com/muffinista/before-dawn/tree/v0.6.3) (2016-03-09)
[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.6.1...v0.6.3)
## [v0.6.1](https://github.com/muffinista/before-dawn/tree/v0.6.1) (2016-03-05)
[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.6.0...v0.6.1)
## [v0.6.0](https://github.com/muffinista/before-dawn/tree/v0.6.0) (2016-03-04)
[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.5.0...v0.6.0)
## [v0.5.0](https://github.com/muffinista/before-dawn/tree/v0.5.0) (2016-02-23)
[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.4.0...v0.5.0)
## [v0.4.0](https://github.com/muffinista/before-dawn/tree/v0.4.0) (2016-02-21)
[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.3...v0.4.0)
## [v0.3](https://github.com/muffinista/before-dawn/tree/v0.3) (2016-02-20)
[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.2...v0.3)
## [v0.2](https://github.com/muffinista/before-dawn/tree/v0.2) (2016-02-10)
[Full Changelog](https://github.com/muffinista/before-dawn/compare/v0.1...v0.2)
## [v0.1](https://github.com/muffinista/before-dawn/tree/v0.1) (2016-02-04)
================================================
FILE: LICENSE.txt
================================================
The MIT License
Copyright (c) 2016 Colin Mitchell http://muffinlabs.com/
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.
================================================
FILE: README.md
================================================
# Before Dawn
Before Dawn is a an open-source, cross-platform screensaver application using
web-based technologies. You can generate screensavers with it using HTML/CSS,
javascript, canvas, and any tools that rely on those technologies. In theory,
generating a Before Dawn screensaver is as simple as writing an HTML page.
The project developed out of a personal project to explore the history of early
screensavers. I decided that I wanted to write a framework that I could use to
actually run screensavers on my computer. I wanted it to be cross-platform and
easily accessible to artists and developers.
Before Dawn is definitely a bit of a experiment -- to actually use it, you need
to run it as a separate application on your computer and disable whatever
screensaver you have running in your OS, but it is fun and definitely works.
The core of the app is built on [Electron](https://www.electronjs.org/), a system
that allows you to build desktop applications that run on
[node.js](https://nodejs.org/) and are rendered via Chrome.
There's a bunch of screensavers to choose from. Here's the Flying Emoji screensaver:

You can get a quick preview of the other screensavers via this [preview
page](http://muffinista.github.io/before-dawn-screensavers/).
The first time you open the application, the preferences window will open. You
can preview and pick screensavers there:

There's an 'Advanced Settings' section where you can specify certain options for
how the application should run.

## Downloads
Installers are available from the
[releases](https://github.com/muffinista/before-dawn/releases) page.
## Status
Right now the application itself is pretty stable. This repo includes the main
code for running the actual screensaver, a simple app for picking your
screensaver and setting some options, and a bunch of modules to pull it all
together.
The actual code for the screensavers is in [a separate
repo](https://github.com/muffinista/before-dawn-screensavers). If you want to
write a screensaver, please add it to the project via a PR!
## Running It
The easiest way to use the tool is to install it to your computer. You can grab
an installer from the
[releases](https://github.com/muffinista/before-dawn/releases) page. Binaries
are available for OSX and Windows, and there's an experimental release for
Ubuntu/Debian.
Once it's running, there will be a sunrise icon in your system tray, with a few
different options. If you click 'Preferences,' you can preview the different
screensavers, set how much idle time is required before the screensaver starts
to run, specify custom paths, etc.
Once you've set all of that up, Before Dawn will happily run in the background,
and when it detects that you have been idle, it will engage your screensaver.
That's all there is to it!
### A note for Linux/Wayland users
When using Wayland, seemingly every time it's started the app will request
permission to capture the screen. I get around this by running with
`XDG_SESSION_TYPE=x11` set.
## Building It
Steps for generating your own build Before Dawn are listed in [the
wiki](https://github.com/muffinista/before-dawn/wiki/Building-Before-Dawn)
## Hacking It
If you would like to hack on Before Dawn, there's some instructions on the
[Development page](https://github.com/muffinista/before-dawn/wiki/Development)
in the wiki. It's pretty straightforward once you have a basic setup in place.
## How to Write a Screensaver
A Before Dawn screensaver is basically just a web page running in fullscreen
mode. That said, there's a few twists to make it run as smoothly as possible.
There's a bunch of specific implementation details in [the
wiki](https://github.com/muffinista/before-dawn/wiki/Writing-A-Screensaver).
There's also a very basic editor mode built into Before Dawn, which will
generate some basic code for you to work from, and will make it easier to add
some configurable options to your work.
The editor has a simple preview, a form to describe the screensaver, and a
section where you can add custom options for your screensaver:

## Contributing
Contributions and suggestions are eagerly accepted. Please check out the [code
of
conduct](https://github.com/muffinista/before-dawn/blob/main/code_of_conduct.md)
before contributing.
If you find a bug or have a suggestion, you can open an issue or a pull request
here.
If you would like to add a screensaver to the program, you can submit a PR to
the
[before-dawn-screensavers](https://github.com/muffinista/before-dawn-screensavers)
repo.
I will accept pretty much any pull request to the repository given that the
content you are posting is legal and appropriate. If you need help or have a
suggestion, please feel free to open an issue here.
## Copyright/License
Unless otherwise stated, Copyright (c) 2024 [Colin
Mitchell](http://muffinlabs.com).
Before Dawn is is distributed under the MIT licence -- Please see LICENSE.txt
for further details.
================================================
FILE: bin/build-icon.js
================================================
#!/usr/bin/env node
import "dotenv/config";
import * as path from "path";
import * as tmp from "tmp";
import * as fs from "fs";
import pngToIco from "png-to-ico";
import { Jimp } from "jimp";
const sizes = [256, 128, 48, 32, 16];
async function main() {
let outputs = [];
let pauseOutputs = [];
const image = await Jimp.read("assets/icon.png");
const pauseImage = await Jimp.read("assets/icon-paused.png");
const tmpDir = tmp.dirSync().name;
for ( let index in sizes ) {
const size = sizes[index];
console.log(size);
const name = path.join(tmpDir, `icon-${size}.png`);
await image.resize({w: size, h: size});
await image.write(name);
outputs.push(name);
const pausedName = path.join(tmpDir, `icon-paused-${size}.png`);
await pauseImage.resize({w: size, h: size});
await pauseImage.write(pausedName);
pauseOutputs.push(pausedName);
}
console.log(outputs);
const buf = await pngToIco(outputs);
fs.writeFileSync(path.join("src", "main", "assets", "icon.ico"), buf);
console.log(pauseOutputs);
const buf2 = await pngToIco(pauseOutputs);
fs.writeFileSync(path.join("src", "main", "assets", "icon-paused.ico"), buf2);
}
main().catch(e => console.error(e));
================================================
FILE: bin/build-on-ci.js
================================================
#!/usr/bin/env node
/* eslint-disable no-console */
require("dotenv").config();
const apiUrl = "https://ci.appveyor.com/api/account/muffinista/builds";
const travisApiUrl = "https://api.travis-ci.org/repo/muffinista%2Fbefore-dawn/requests";
const body = {
"accountName": "muffinista",
"projectSlug": "before-dawn",
"branch": "main"
};
const appveyorOpts = {
method: "post",
body: JSON.stringify(body),
headers: {
"Content-type": "application/json",
"Authorization": `Bearer ${process.env.APPVEYOR_TOKEN}`
},
};
const travisBody = {
"request": {
"branch": "main"
}
};
const travisOpts = {
method: "post",
body: JSON.stringify(travisBody),
headers: {
"Content-type": "application/json",
"Accept": "application/json",
"Authorization": `token ${process.env.TRAVIS_TOKEN}`,
"Travis-API-Version": "3"
}
};
fetch(apiUrl, appveyorOpts)
.then(res => res.json())
.then(json => console.log(json))
.then(() => {
fetch(travisApiUrl, travisOpts)
.then(res => res.json())
.then(json => console.log(json));
});
================================================
FILE: bin/capture-screens.js
================================================
#!/usr/bin/env node
"use strict";
const path = require("path");
const { _electron: electron } = require("playwright");
const appPath = require("electron");
const helpers = require("../test/helpers.js");
let shim;
const SCREENSAVER = "Screen Glitcher";
const callIpc = async(method, opts={}) => {
await shim.fill("#ipc", method);
await shim.fill("#ipcopts", JSON.stringify(opts));
await shim.click("text=go");
};
async function main() {
let env = {
CONFIG_DIR: "/Users/colin/Library/Application Support/Before Dawn",
BEFORE_DAWN_DIR: "/Users/colin/Library/Application Support/Before Dawn",
TEST_MODE: true,
QUIET_MODE: true
};
let app = await electron.launch({
path: appPath,
args: [path.join(__dirname, "..", "output/main.js")],
env: env
});
// wait for the first window (our test shim) to open, and
// hang onto it for later use
shim = await app.firstWindow();
await callIpc("open-window prefs");
let window = await helpers.waitFor(app, "prefs");
await window.click(`text=${SCREENSAVER}`);
await helpers.sleep(1000);
await window.screenshot({ path: path.join(__dirname, "..", "assets", "prefs.png") });
await window.click("button.settings");
let settings = await helpers.waitFor(app, "settings");
await helpers.sleep(1000);
await settings.screenshot({ path: path.join(__dirname, "..", "assets", "settings.png") });
await callIpc("close-window settings");
await window.click("button.create");
let create = await helpers.waitFor(app, "new");
await helpers.sleep(1000);
await create.screenshot({ path: path.join(__dirname, "..", "assets", "create-screensaver.png") });
await create.click("button.cancel");
var saversDir = helpers.getTempDir();
const saverJSON = helpers.addSaver(saversDir, "saver-one", "saver.json");
await callIpc("open-window editor", {
screenshot: "file://" + path.join(__dirname, "../fixtures/screenshot.png"),
src: saverJSON
});
let editor = await helpers.waitFor(app, "editor");
await helpers.sleep(1000);
await editor.screenshot({ path: path.join(__dirname, "..", "assets", "editor.png"), fullPage: true });
app.close();
}
main().catch(e => console.error(e));
================================================
FILE: bin/dev-runner.js
================================================
"use strict";
import electron from "electron";
import * as path from "path";
import { spawn } from "child_process";
import webpack from "webpack";
import WebpackDevServer from "webpack-dev-server";
import { readFile } from 'fs/promises';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
let devPort;
import mainConfig from "../webpack.main.config.js";
import rendererConfig from "../webpack.renderer.config.js";
try {
const packageJSON = JSON.parse(
await readFile(
new URL('../package.json', import.meta.url)
)
);
devPort = packageJSON.devport;
}
catch(e) {
console.log(`Can't read package.json, defaulting dev port to 9080: ${e}`);
devPort = 9080;
}
let electronProcess = null;
let manualRestart = false;
let skipMainRestart = true;
/**
* Setup webpack compiler and server for the renderer process
*
* @returns Promise
*/
function startRenderer () {
return new Promise((resolve) => {
const compiler = webpack(rendererConfig);
const serverOptions = {
host: "localhost",
port: devPort,
hot: true,
onListening: function (devServer) {
const port = devServer.server.address().port;
console.log("Listening on port:", port);
},
};
const devServer = new WebpackDevServer(
serverOptions, compiler
);
devServer.startCallback(() => {
console.log("startRenderer finished");
resolve();
})
});
}
/**
* Setup webpack compiler and watcher for the main process
*
* @returns Promise
*/
function startMain () {
return new Promise((resolve) => {
const mainCompiler = webpack(mainConfig);
mainCompiler.run(() => {
mainCompiler.watch({}, (err) => {
if (err) {
console.log(err);
return;
}
// skip the first watch event
if ( skipMainRestart ) {
skipMainRestart = false;
return;
}
// kill and restart the main process
if (electronProcess && electronProcess.kill) {
manualRestart = true;
process.kill(electronProcess.pid);
electronProcess = null;
startElectron();
setTimeout(() => {
manualRestart = false;
}, 5000);
}
});
resolve();
});
});
}
function startElectron () {
// @todo set environment here
electronProcess = spawn(electron, ["--no-sandbox", "--inspect=5858", path.join(__dirname, "../src/main/index.js")]);
electronProcess.stdout.on("data", data => {
process.stdout.write(data.toString());
});
electronProcess.stderr.on("data", data => {
process.stdout.write(data.toString());
});
electronProcess.once("close", () => {
if (!manualRestart) {process.exit();}
});
}
function init () {
Promise.all([startRenderer(), startMain()])
.then(startElectron)
.catch(console.error);
}
init();
================================================
FILE: bin/download-screensavers.js
================================================
#!/usr/bin/env node
/* eslint-disable no-console */
import "dotenv/config";
import * as path from "path";
import * as fs from "fs";
import { rimraf } from 'rimraf'
import * as mkdirp from "mkdirp";
import { Octokit } from "octokit";
import Package from "../src/lib/package.js";
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const srcRoot = path.join(__dirname, "..");
const workingDir = path.join(srcRoot, "data");
let opts = {};
const octokit = new Octokit(opts);
let owner = "muffinista";
let repo = "before-dawn-screensavers";
console.log("cleaning up working dir", workingDir);
// ensure directory exists, clean it out, then recreate just to be sure
mkdirp.sync(workingDir);
rimraf.sync(workingDir);
mkdirp.sync(workingDir);
async function main() {
let result = await octokit.rest.repos.getLatestRelease({owner, repo});
const tag_name = result.data.tag_name;
const jsonFile = `${repo}-${tag_name}.json`;
const jsonDest = path.join(srcRoot, "data", jsonFile);
fs.writeFileSync(jsonDest, JSON.stringify(result.data));
const url = result.data.zipball_url;
const dest = path.join(srcRoot, "data", "savers");
const p = new Package({
repo: `${owner}/${repo}`,
dest: dest
});
await p.downloadRelease(url, dest);
}
main().catch(e => console.error(e));
/* eslint-enable no-console */
================================================
FILE: bin/generate-release.js
================================================
#!/usr/bin/env node
import "dotenv/config";
import * as path from "path";
import { Octokit } from "octokit";
import { readFile } from 'fs/promises';
import { fileURLToPath } from 'url';
import SentryCli from '@sentry/cli';
const pjson = JSON.parse(
await readFile(
new URL('../package.json', import.meta.url)
)
);
const __dirname = path.dirname(fileURLToPath(import.meta.url));
let opts = {
auth: `token ${process.env.GITHUB_AUTH_TOKEN}`
};
const octokit = new Octokit(opts);
let owner = "muffinista";
let repo = "before-dawn";
let tag_name = `v${pjson.version}`;
let draft = true;
let releaseName = `${pjson.productName} ${pjson.version}`;
const sentryCli = new SentryCli(path.join(__dirname, "sentry.properties"));
async function main() {
let release = {
owner: owner,
repo: repo,
tag_name: tag_name,
target_commitish: "main",
name: tag_name,
body: "description",
draft: draft
};
console.log(`checking ${owner}/${repo} for ${tag_name}`);
let result = await octokit.rest.repos.getLatestRelease({owner, repo});
if ( result.data.tag_name === tag_name ) {
console.log("release already created!");
}
else {
console.log(release);
// Create a release
result = await octokit.rest.repos.createRelease(release);
console.log(result);
}
console.log("Create new release on sentry");
await sentryCli.execute(["releases", "new", releaseName], true);
console.log("Add commits to release");
await sentryCli.execute(["releases", "set-commits", "--auto", releaseName], true);
console.log("Upload sourcemaps");
await sentryCli.execute(["releases", "files", releaseName, "upload-sourcemaps", "output"], true);
console.log("Finalize release");
await sentryCli.execute(["releases", "finalize", releaseName], true);
console.log("Set new deploy");
await sentryCli.execute(["releases", "deploys", releaseName, "new", "--env", "production"], true);
}
main().catch(e => console.error(e));
/* eslint-enable no-console */
================================================
FILE: bin/get-release-name
================================================
#!/usr/bin/env node
const path = require("path");
var pjson = require(path.join(__dirname, "..", "package.json"));
console.log(`${pjson.productName} ${pjson.version}`);
================================================
FILE: code_of_conduct.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at colin@muffinlabs.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
================================================
FILE: docs/.gitignore
================================================
_site
.sass-cache
================================================
FILE: docs/.ruby-version
================================================
3.3
================================================
FILE: docs/Gemfile
================================================
source 'https://rubygems.org'
gem 'github-pages', group: :jekyll_plugins
================================================
FILE: docs/_config.yml
================================================
# Site settings
title: Before Dawn -- Screensavers for the Modern Era
email: colin at muffinlabs.com
description: > # this means to ignore newlines until "baseurl:"
This is the help website for Before Dawn, an open source screensaver
app. Enjoy!
baseurl: "" # the subpath of your site, e.g. /blog/
url: "https://muffinista.github.io/before-dawn/" # the base hostname & protocol for your site
twitter_username: muffinista
github_username: muffinista
# Build settings
markdown: kramdown
================================================
FILE: docs/_includes/footer.html
================================================
================================================
FILE: docs/_includes/head.html
================================================
{% if page.title %}{{ page.title }} :: {% endif %}{{ site.title }}
================================================
FILE: docs/_includes/header.html
================================================
================================================
FILE: docs/_layouts/default.html
================================================
{% include head.html %}
{% include header.html %}
{% include footer.html %}
================================================
FILE: docs/_layouts/page.html
================================================
---
layout: default
---
================================================
FILE: docs/_layouts/post.html
================================================
---
layout: default
---
================================================
FILE: docs/_posts/2016-12-02-welcome-to-jekyll.markdown
================================================
---
layout: post
title: "Welcome to Jekyll!"
date: 2016-12-02 13:11:20
categories: jekyll update
---
You’ll find this post in your `_posts` directory. Go ahead and edit it and re-build the site to see your changes. You can rebuild the site in many different ways, but the most common way is to run `jekyll serve --watch`, which launches a web server and auto-regenerates your site when a file is updated.
To add new posts, simply add a file in the `_posts` directory that follows the convention `YYYY-MM-DD-name-of-post.ext` and includes the necessary front matter. Take a look at the source for this post to get an idea about how it works.
Jekyll also offers powerful support for code snippets:
{% highlight ruby %}
def print_hi(name)
puts "Hi, #{name}"
end
print_hi('Tom')
#=> prints 'Hi, Tom' to STDOUT.
{% endhighlight %}
Check out the [Jekyll docs][jekyll] for more info on how to get the most out of Jekyll. File all bugs/feature requests at [Jekyll’s GitHub repo][jekyll-gh]. If you have questions, you can ask them on [Jekyll’s dedicated Help repository][jekyll-help].
[jekyll]: http://jekyllrb.com
[jekyll-gh]: https://github.com/jekyll/jekyll
[jekyll-help]: https://github.com/jekyll/jekyll-help
================================================
FILE: docs/_sass/_base.scss
================================================
/**
* Reset some basic elements
*/
body, h1, h2, h3, h4, h5, h6,
p, blockquote, pre, hr,
dl, dd, ol, ul, figure {
margin: 0;
padding: 0;
}
/**
* Basic styling
*/
body {
font-family: $base-font-family;
font-size: $base-font-size;
line-height: $base-line-height;
font-weight: 300;
color: $text-color;
background-color: $background-color;
-webkit-text-size-adjust: 100%;
}
/**
* Set `margin-bottom` to maintain vertical rhythm
*/
h1, h2, h3, h4, h5, h6,
p, blockquote, pre,
ul, ol, dl, figure,
%vertical-rhythm {
margin-bottom: $spacing-unit / 2;
}
/**
* Images
*/
img {
max-width: 100%;
vertical-align: middle;
}
/**
* Figures
*/
figure > img {
display: block;
}
figcaption {
font-size: $small-font-size;
}
/**
* Lists
*/
ul, ol {
margin-left: $spacing-unit;
}
li {
> ul,
> ol {
margin-bottom: 0;
}
}
/**
* Headings
*/
h1, h2, h3, h4, h5, h6 {
font-weight: 300;
}
/**
* Links
*/
a {
color: $brand-color;
text-decoration: none;
&:visited {
color: darken($brand-color, 15%);
}
&:hover {
color: $text-color;
text-decoration: underline;
}
}
/**
* Blockquotes
*/
blockquote {
color: $grey-color;
border-left: 4px solid $grey-color-light;
padding-left: $spacing-unit / 2;
font-size: 18px;
letter-spacing: -1px;
font-style: italic;
> :last-child {
margin-bottom: 0;
}
}
/**
* Code formatting
*/
pre,
code {
font-size: 15px;
border: 1px solid $grey-color-light;
border-radius: 3px;
background-color: #eef;
}
code {
padding: 1px 5px;
}
pre {
padding: 8px 12px;
overflow-x: scroll;
> code {
border: 0;
padding-right: 0;
padding-left: 0;
}
}
/**
* Wrapper
*/
.wrapper {
max-width: -webkit-calc(800px - (#{$spacing-unit} * 2));
max-width: calc(800px - (#{$spacing-unit} * 2));
margin-right: auto;
margin-left: auto;
padding-right: $spacing-unit;
padding-left: $spacing-unit;
@extend %clearfix;
@include media-query($on-laptop) {
max-width: -webkit-calc(800px - (#{$spacing-unit}));
max-width: calc(800px - (#{$spacing-unit}));
padding-right: $spacing-unit / 2;
padding-left: $spacing-unit / 2;
}
}
/**
* Clearfix
*/
%clearfix {
&:after {
content: "";
display: table;
clear: both;
}
}
/**
* Icons
*/
.icon {
> svg {
display: inline-block;
width: 16px;
height: 16px;
vertical-align: middle;
path {
fill: $grey-color;
}
}
}
================================================
FILE: docs/_sass/_layout.scss
================================================
/**
* Site header
*/
.site-header {
border-top: 5px solid $grey-color-dark;
border-bottom: 1px solid $grey-color-light;
min-height: 56px;
// Positioning context for the mobile navigation icon
position: relative;
}
.site-title {
font-size: 26px;
line-height: 56px;
letter-spacing: -1px;
margin-bottom: 0;
float: left;
&,
&:visited {
color: $grey-color-dark;
}
}
.site-nav {
float: right;
line-height: 56px;
.menu-icon {
display: none;
}
.page-link {
color: $text-color;
line-height: $base-line-height;
// Gaps between nav items, but not on the first one
&:not(:first-child) {
margin-left: 20px;
}
}
@include media-query($on-palm) {
position: absolute;
top: 9px;
right: 30px;
background-color: $background-color;
border: 1px solid $grey-color-light;
border-radius: 5px;
text-align: right;
.menu-icon {
display: block;
float: right;
width: 36px;
height: 26px;
line-height: 0;
padding-top: 10px;
text-align: center;
> svg {
width: 18px;
height: 15px;
path {
fill: $grey-color-dark;
}
}
}
.trigger {
clear: both;
display: none;
}
&:hover .trigger {
display: block;
padding-bottom: 5px;
}
.page-link {
display: block;
padding: 5px 10px;
}
}
}
/**
* Site footer
*/
.site-footer {
border-top: 1px solid $grey-color-light;
padding: $spacing-unit 0;
}
.footer-heading {
font-size: 18px;
margin-bottom: $spacing-unit / 2;
}
.contact-list,
.social-media-list {
list-style: none;
margin-left: 0;
}
.footer-col-wrapper {
font-size: 15px;
color: $grey-color;
margin-left: -$spacing-unit / 2;
@extend %clearfix;
}
.footer-col {
float: left;
margin-bottom: $spacing-unit / 2;
padding-left: $spacing-unit / 2;
}
.footer-col-1 {
width: -webkit-calc(35% - (#{$spacing-unit} / 2));
width: calc(35% - (#{$spacing-unit} / 2));
}
.footer-col-2 {
width: -webkit-calc(20% - (#{$spacing-unit} / 2));
width: calc(20% - (#{$spacing-unit} / 2));
}
.footer-col-3 {
width: -webkit-calc(45% - (#{$spacing-unit} / 2));
width: calc(45% - (#{$spacing-unit} / 2));
}
@include media-query($on-laptop) {
.footer-col-1,
.footer-col-2 {
width: -webkit-calc(50% - (#{$spacing-unit} / 2));
width: calc(50% - (#{$spacing-unit} / 2));
}
.footer-col-3 {
width: -webkit-calc(100% - (#{$spacing-unit} / 2));
width: calc(100% - (#{$spacing-unit} / 2));
}
}
@include media-query($on-palm) {
.footer-col {
float: none;
width: -webkit-calc(100% - (#{$spacing-unit} / 2));
width: calc(100% - (#{$spacing-unit} / 2));
}
}
/**
* Page content
*/
.page-content {
padding: $spacing-unit 0;
}
.page-heading {
font-size: 20px;
}
.post-list {
margin-left: 0;
list-style: none;
> li {
margin-bottom: $spacing-unit;
}
}
.post-meta {
font-size: $small-font-size;
color: $grey-color;
}
.post-link {
display: block;
font-size: 24px;
}
/**
* Posts
*/
.post-header {
margin-bottom: $spacing-unit;
}
.post-title {
font-size: 42px;
letter-spacing: -1px;
line-height: 1;
@include media-query($on-laptop) {
font-size: 36px;
}
}
.post-content {
margin-bottom: $spacing-unit;
h2 {
font-size: 32px;
@include media-query($on-laptop) {
font-size: 28px;
}
}
h3 {
font-size: 26px;
@include media-query($on-laptop) {
font-size: 22px;
}
}
h4 {
font-size: 20px;
@include media-query($on-laptop) {
font-size: 18px;
}
}
}
================================================
FILE: docs/_sass/_syntax-highlighting.scss
================================================
/**
* Syntax highlighting styles
*/
.highlight {
background: #fff;
@extend %vertical-rhythm;
.c { color: #998; font-style: italic } // Comment
.err { color: #a61717; background-color: #e3d2d2 } // Error
.k { font-weight: bold } // Keyword
.o { font-weight: bold } // Operator
.cm { color: #998; font-style: italic } // Comment.Multiline
.cp { color: #999; font-weight: bold } // Comment.Preproc
.c1 { color: #998; font-style: italic } // Comment.Single
.cs { color: #999; font-weight: bold; font-style: italic } // Comment.Special
.gd { color: #000; background-color: #fdd } // Generic.Deleted
.gd .x { color: #000; background-color: #faa } // Generic.Deleted.Specific
.ge { font-style: italic } // Generic.Emph
.gr { color: #a00 } // Generic.Error
.gh { color: #999 } // Generic.Heading
.gi { color: #000; background-color: #dfd } // Generic.Inserted
.gi .x { color: #000; background-color: #afa } // Generic.Inserted.Specific
.go { color: #888 } // Generic.Output
.gp { color: #555 } // Generic.Prompt
.gs { font-weight: bold } // Generic.Strong
.gu { color: #aaa } // Generic.Subheading
.gt { color: #a00 } // Generic.Traceback
.kc { font-weight: bold } // Keyword.Constant
.kd { font-weight: bold } // Keyword.Declaration
.kp { font-weight: bold } // Keyword.Pseudo
.kr { font-weight: bold } // Keyword.Reserved
.kt { color: #458; font-weight: bold } // Keyword.Type
.m { color: #099 } // Literal.Number
.s { color: #d14 } // Literal.String
.na { color: #008080 } // Name.Attribute
.nb { color: #0086B3 } // Name.Builtin
.nc { color: #458; font-weight: bold } // Name.Class
.no { color: #008080 } // Name.Constant
.ni { color: #800080 } // Name.Entity
.ne { color: #900; font-weight: bold } // Name.Exception
.nf { color: #900; font-weight: bold } // Name.Function
.nn { color: #555 } // Name.Namespace
.nt { color: #000080 } // Name.Tag
.nv { color: #008080 } // Name.Variable
.ow { font-weight: bold } // Operator.Word
.w { color: #bbb } // Text.Whitespace
.mf { color: #099 } // Literal.Number.Float
.mh { color: #099 } // Literal.Number.Hex
.mi { color: #099 } // Literal.Number.Integer
.mo { color: #099 } // Literal.Number.Oct
.sb { color: #d14 } // Literal.String.Backtick
.sc { color: #d14 } // Literal.String.Char
.sd { color: #d14 } // Literal.String.Doc
.s2 { color: #d14 } // Literal.String.Double
.se { color: #d14 } // Literal.String.Escape
.sh { color: #d14 } // Literal.String.Heredoc
.si { color: #d14 } // Literal.String.Interpol
.sx { color: #d14 } // Literal.String.Other
.sr { color: #009926 } // Literal.String.Regex
.s1 { color: #d14 } // Literal.String.Single
.ss { color: #990073 } // Literal.String.Symbol
.bp { color: #999 } // Name.Builtin.Pseudo
.vc { color: #008080 } // Name.Variable.Class
.vg { color: #008080 } // Name.Variable.Global
.vi { color: #008080 } // Name.Variable.Instance
.il { color: #099 } // Literal.Number.Integer.Long
}
================================================
FILE: docs/about.md
================================================
---
layout: page
title: About
permalink: /about.html
---
Before Dawn is a an open-source, cross-platform screensaver
application using web-based technologies. You can generate
screensavers with it using HTML/CSS, javascript, canvas, and any tools
that rely on those technologies. In theory, generating a Before Dawn
screensaver is as simple as writing an HTML page.
The project developed out of a personal project to explore the history
of early screensavers. I decided that I wanted to write a framework
that I could use to actually run screensavers on my computer. I wanted
it to be cross-platform and easily accessible to artists and
developers.
Before Dawn is definitely a bit of a experiment -- to actually use it,
you need to run it as a separate application on your computer and
disable whatever screensaver you have running in your OS, but it is
fun and definitely works.
================================================
FILE: docs/contributing.md
================================================
---
layout: page
title: Contributing
permalink: /contributing.html
---
Contributions and suggestions are eagerly accepted. Please read the
[code of conduct](https://github.com/muffinista/before-dawn/blob/main/code_of_conduct.md)
before contributing.
If you find a bug or have a suggestion, you can
[open an issue](https://github.com/muffinista/before-dawn/issues) or a
pull request on the main repository.
The screensavers for Before Dawn are in their own repository. If you
would like to add a screensaver to the program, you can submit a PR to
the
[before-dawn-screensavers](https://github.com/muffinista/before-dawn-screensavers)
repo.
I will accept pretty much any pull request to the repository given
that the content you are posting is legal and appropriate. If you need
help or have a suggestion, please feel free to open an issue.
================================================
FILE: docs/creating.md
================================================
---
layout: page
title: Adding Your Own Screensaver
permalink: /creating.html
---
A Before Dawn screensaver is an HTML page that runs in full screen
mode. You can extend them with CSS and Javascript, and they can get
very complicated, but the basics are really very simple.
Before you add your own screensaver, you'll need to specify a local
directory for your work in the Prefrences window. Once you've done
that, click the 'Create Screensaver' button and a little form will
open up where you can specify some basic information about your
screensaver:
- *Name:* The name of your screensaver
- *Description:* A brief description of your screensaver.
- *About URL:* An optional URL with more details about your work.
- *Author:* The author of this screensaver.
After you enter in your values and click save, Before Dawn creates a
folder for you which contains an HTML file which serves as the basis
of your new screensaver, and a `saver.json` file which contains the
information about your screensaver. Before Dawn should display the
folder for you so you can get to work building your awesome
screensaver!
## Testing, Adding Options ##
Any screensaver that is in your local sources directory can be edited.
In the preview list, there will be an 'edit' link. Clicking that link
opens a window which will allow you to update the basic information
for the screensaver, add configurable options to your screensaver,
view a working preview of the screensaver, debug it, etc.
There are two tabs and a couple of buttons in the edit window. The
Preview tab will run your screensaver. The window should auto-reload
whenever you save changes to your screensaver. The Settings tab is
where you can update information about your screensaver, or add
configurable options (see below). Then there are four buttons:
- the folder button will open the working folder for your screensaver
- the save button will save any changes you've made in the settings
form
- the reload button will reload the preview
- the bug button will open the developer tools console in case you
need to debug something.
## Configurable Options ##
Before Dawn has a very simple interface for adding configurable
options to your screensaver. There are two kinds of inputs right now:
text and sliders. You could use a text input to allow users to specify
a URL or some text that will be used in your screensaver. A slider can
be used to allow the user to input a number, etc.
Screensavers are loaded as URLs, and any options will be passed as
values to the URL. The template contains some code to parse incoming
values.
================================================
FILE: docs/css/main.scss
================================================
---
# Only the main Sass file needs front matter (the dashes are enough)
---
@charset "utf-8";
// Our variables
$base-font-family: 'ChicagoFLFRegular', Helvetica, Arial, sans-serif;
$base-font-size: 16px;
$small-font-size: $base-font-size * 0.875;
$base-line-height: 1.5;
$spacing-unit: 30px;
$text-color: #111;
$background-color: #fdfdfd;
$brand-color: #2a7ae2;
$grey-color: #828282;
$grey-color-light: lighten($grey-color, 40%);
$grey-color-dark: darken($grey-color, 25%);
$on-palm: 600px;
$on-laptop: 800px;
// Using media queries with like this:
// @include media-query($palm) {
// .wrapper {
// padding-right: $spacing-unit / 2;
// padding-left: $spacing-unit / 2;
// }
// }
@mixin media-query($device) {
@media screen and (max-width: $device) {
@content;
}
}
// Import partials from `sass_dir` (defaults to `_sass`)
@import "base", "layout";
html {
height: 100%;
}
body {
display: flex;
flex-direction: column;
width: 100%;
min-height: 100%; /* this helps with the sticky footer */
}
.page-content {
flex-grow: 1;
flex-shrink: 0;
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAANklEQVQoU2NkIAIwEqFGipAiKQYGhmeEFIEtwqUIbALMKdgUoSjAZxKKf5BNwjAB3TqcCkAKAW9cBSRvYfskAAAAAElFTkSuQmCC) repeat;
}
footer {
flex-shrink: 0;
}
.home {
display: flex;
p {
padding-right: 10px;
}
.main, .preview {
flex: 0 0 50%;
}
.preview {
height: 500px;
background-image: url(../background3.svg);
background-size: contain;
background-repeat: no-repeat;
}
img.saver {
margin-top: 50px;
margin-left: 32px;
width:310px;
height: 245px;
}
}
@media screen and (max-width: 800px) {
.home {
display: block;
.preview {
background-image: none;
img.saver {
width: 100%;
height: auto;
margin: 0px;
}
}
}
}
@include media-query($on-palm) {
.site-title {
max-width: $on-palm - 180px;
}
}
================================================
FILE: docs/index.html
================================================
---
layout: page
title: Welcome!
---
Welcome to the help website for Before Dawn, an open-source
screensaver application. Before Dawn runs on Windows, OSX and Linux,
and is powered by javascript.
Screensavers can be written in HTML
and Javascript. If it works in a browser, it can be a screensaver!
You can
learn more about
Before Dawn, find out how to install it, and learn about
writing your own screensaver.
================================================
FILE: docs/installing.md
================================================
---
layout: page
title: Installing Before Dawn
permalink: /installing.html
---
You can find installers for Before Dawn on github in the
[Releases section](https://github.com/muffinista/before-dawn/releases)
of the project. Installing on OSX or Windows should be as simple as
downloading the appropriate file, and running it locally. Installing
on Linux is a little more involved, and I will add documentation for
that as soon as I can.
The first time you install it, you will need to run Before Dawn
manually. The preferences window will open, and you can pick a
screensaver and set whether you want Before Dawn to automatically load
when your system boots, and you can tweak some other settings as well.
================================================
FILE: docs/preferences.md
================================================
---
layout: page
title: Preferences
permalink: /preferences.html
---
Before Dawn has a Preferences window where you can see the list of
screensavers, and tweak your settings.
## Picking a Screensaver ##
The main tab of the preferences window lists all of the screensavers
available to you. As you click on them, you'll see a preview in the
right side of the window.
### Screensaver Options ###
Some screensavers have configurable options to control how they run --
for example, the Emoji Starfield screensaver has a control to set how
many emoji are on your screen at once. You can fiddle with these
settings and get an idea of how it looks in the preview area.
## Options ##
The Options tab of the Preferences window has a number of settings to
control how Before Dawn runs:
- *Activate after* -- you can set how long your computer is idle
before your screensaver starts
- *Sleep after* - you can set a time that Before Dawn will
stop running and blank the screens. It's possible that your OS will do
a better job of this, but since Before Dawn is a bit of an experiment,
having this setting might save your CPU from running when your
computer is idle for a long time.
- *Lock screen after running?* -- If you want your computer to lock
once the screensaver starts running, you can use this option.
- *Disable when on battery?* -- You can toggle this so that Before Dawn
won't run if your computer is running on battery power.
- *Auto start on login?* -- should Before Dawn start automatically
when your computer starts?
### Advanced Options ###
- *Local Source* -- If you want to develop your own screensavers, or
run a set of screensavers that aren't in the main package, you can put a path
to a directory here. Before Dawn will check this directory for any
local screensavers and add them to the list.
### Create Screensaver ###
Check out the [creating](../creating) section to learn about adding your
own screensaver.
================================================
FILE: eslint.config.mjs
================================================
import mocha from "eslint-plugin-mocha";
import globals from "globals";
import js from "@eslint/js";
import svelte from 'eslint-plugin-svelte';
/** @type {import('eslint').Linter.Config[]} */
export default [
mocha.configs.recommended,
js.configs.recommended,
...svelte.configs.recommended,
{
languageOptions: {
globals: {
...globals.browser,
...globals.node, // Add this if you are using SvelteKit in non-SPA mode
},
ecmaVersion: "latest",
sourceType: "module",
}
},
{
files: ['**/*.svelte', '**/*.svelte.js'],
languageOptions: {
parserOptions: {
// We recommend importing and specifying svelte.config.js.
// By doing so, some rules in eslint-plugin-svelte will automatically read the configuration and adjust their behavior accordingly.
// While certain Svelte settings may be statically loaded from svelte.config.js even if you don’t specify it,
// explicitly specifying it ensures better compatibility and functionality.
// svelteConfig
}
},
plugins: {
"plugin:svelte/recommended": svelte
}
},
{
files: ['test/**/*.js'],
plugins: {
"plugin:mocha/recommended": mocha
},
languageOptions: {
globals: {
...globals.mocha
},
},
},
{
ignores: [
"output/*",
"data/*"
],
rules: {
// Override or add rule settings here, such as:
// 'svelte/rule-name': 'error'
}
}
];
================================================
FILE: lefthook.yml
================================================
# EXAMPLE USAGE
# Refer for explanation to following link:
# https://github.com/Arkweid/lefthook/blob/master/docs/full_guide.md
#
# pre-push:
# commands:
# packages-audit:
# tags: frontend security
# run: yarn audit
# gems-audit:
# tags: backend security
# run: bundle audit
#
pre-commit:
parallel: true
commands:
eslint:
glob: "*.{js,ts}"
run: npm run eslint {staged_files}
================================================
FILE: mise.toml
================================================
[tools]
node = "22.21.1"
================================================
FILE: package.json
================================================
{
"name": "before-dawn",
"productName": "Before Dawn",
"version": "0.38.0",
"description": "A desktop screensaver app using web technologies",
"author": "Colin Mitchell (http://muffinlabs.com)",
"license": "MIT",
"homepage": "https://github.com/muffinista/before-dawn/",
"release_server": "https://before-dawn-updates.muffinlabs.com",
"main": "output/main.js",
"type": "module",
"engines": {
"node": ">= 22.14.0"
},
"devport": 9081,
"scripts": {
"dev": "node bin/dev-runner.js",
"compile": "cross-env NODE_ENV=production webpack --mode production --config webpack.config.js",
"eslint-all": "eslint -c eslint.config.mjs src/**/*.js src/**/*.svelte test/**/*.js webpack*.js",
"eslint": "eslint -c eslint.config.mjs",
"postinstall": "electron-builder install-app-deps",
"pack": "npm run compile && electron-builder --dir",
"dist": "npm run compile && electron-builder --x64",
"test": "npm run compile && mocha -b test/**/*.js",
"test-ui": "cross-env DISABLE_SENTRY=true npm run compile && xvfb-maybe mocha test/ui/**/*.js",
"test-lib": "mocha test test/lib/**/*.js test/main/**/*.js",
"run-local": "node bin/build-icon.js && npm run compile && cross-env ELECTRON_IS_DEV=0 electron output/main.js",
"grab-screens": "bin/capture-screens.js",
"release": "node bin/build-icon.js && node bin/download-screensavers.js && npm run dist",
"publish-release": "node bin/generate-release.js && git push origin main"
},
"repository": {
"type": "git",
"url": "git://github.com/muffinista/before-dawn.git"
},
"dependencies": {
"@muffinista/goto-sleep": "github:muffinista/goto-sleep",
"auto-launch": "^5.0.6",
"conf": "^15.0.2",
"detect-fullscreen": "github:muffinista/detect-fullscreen",
"electron-is-dev": "^3.0.1",
"electron-log": "^5.4.3",
"forcefocus": "github:muffinista/forcefocus",
"fs-extra": "^11.3.3",
"glob": "^13.0.0",
"hide-cursor": "github:muffinista/hide-cursor",
"mkdirp": "^3.0.1",
"proper-lockfile": "^4.1.2",
"rimraf": "^6.1.2",
"semver": "^7.7.3",
"temp": "^0.9.4",
"yauzl": "^3.2.1"
},
"overrides": {
"@electron/rebuild": {
"node-abi": "4.17.0"
}
},
"devDependencies": {
"@arkweid/lefthook": "^0.7.7",
"@babel/core": "^7.28.5",
"@babel/eslint-parser": "^7.28.5",
"@babel/plugin-transform-runtime": "^7.28.5",
"@babel/preset-env": "^7.28.5",
"@electron/rebuild": "^4.0.2",
"@eslint/eslintrc": "^3.3.3",
"@eslint/js": "^9.39.2",
"@sentry/cli": "^3.0.1",
"@sentry/electron": "^7.5.0",
"@sentry/webpack-plugin": "^4.6.1",
"babel-loader": "^10.0.0",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^13.0.1",
"cross-env": "^10.1.0",
"css-loader": "^7.1.2",
"dotenv": "^17.2.3",
"electron": "^39.8.5",
"electron-builder": "^26.3.6",
"eslint": "^9.39.2",
"eslint-friendly-formatter": "^4.0.1",
"eslint-plugin-html": "^8.1.3",
"eslint-plugin-mocha": "^11.2.0",
"eslint-plugin-n": "^17.23.1",
"eslint-plugin-promise": "^7.2.1",
"eslint-plugin-svelte": "^3.13.1",
"eslint-webpack-plugin": "^5.0.2",
"globals": "^17.0.0",
"html-webpack-plugin": "^5.6.5",
"jimp": "^1.6.0",
"mini-css-extract-plugin": "^2.9.4",
"mocha": "^11.7.5",
"nock": "^14.0.10",
"node-gyp": "^12.1.0",
"node-loader": "^2.1.0",
"normalize.css": "^8.0.1",
"octokit": "^5.0.5",
"playwright": "^1.57.0",
"png-to-ico": "^3.0.1",
"sass": "^1.97.1",
"sass-loader": "^16.0.6",
"sinon": "^21.0.1",
"style-loader": "^4.0.0",
"svelte": "^5.53.6",
"svelte-eslint-parser": "^1.4.1",
"svelte-loader": "^3.2.4",
"tmp": "^0.2.5",
"url-loader": "^4.1.1",
"webpack": "^5.104.1",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.2",
"webpack-hot-middleware": "^2.26.1",
"xvfb-maybe": "^0.2.1"
},
"build": {
"files": [
"output/**/*",
"node_modules/**/*",
"package.json"
],
"extraResources": [
{
"from": "data/savers",
"to": "savers",
"filter": [
"**/*"
]
}
],
"appId": "Before Dawn",
"mac": {
"category": "public.app-category.entertainment",
"extendInfo": {
"LSUIElement": 1,
"NSMicrophoneUsageDescription": "Some screensavers detect sound to provide interactivity. You can decline this permission if you do not want that.",
"NSCameraUsageDescription": "Some screensavers can use your webcam to provide interactivity. You can decline this permission if you do not want that."
}
},
"nsis": {
"installerIcon": "build/icon.ico",
"perMachine": false
},
"win": {
"target": "nsis",
"icon": "build/icon.ico"
},
"asar": true,
"dmg": {
"contents": [
{
"x": 338,
"y": 14,
"type": "link",
"path": "/Applications"
},
{
"x": 192,
"y": 14,
"type": "file"
}
]
},
"linux": {
"category": "Amusement",
"target": "deb",
"executableName": "before-dawn",
"maintainer": "Colin Mitchell "
}
}
}
================================================
FILE: src/css/styles.scss
================================================
:root {
--preview-wrapper-width: 500px;
--preview-wrapper-height: 320px;
--preview-width: 500px;
--preview-height: 320px;
--preview-scale: 1.0;
--footer-height: 30px;
--space-at-top: 1.0rem;
--footer-padding: 25px;
--small-padding: 10px;
--font-size: 14px;
}
@import "normalize.css";
html {
height: 100%;
font-family: system-ui;
font-size: var(--font-size);
}
body {
min-height: 100%;
}
input, button {
font: small-caption;
}
select {
font: menu;
}
button {
cursor: pointer;
height: 21px;
padding: 0px 16px;
font-size: var(--font-size);
border: 1px solid;
border-radius: 4px;
border-top-color: rgb(198, 198, 198);
border-bottom-color: rgb(170, 170, 170);
border-left-color: rgb(192, 192, 192);
border-right-color: rgb(192, 192, 192);
}
h1 {
font-size: 1.1rem;
}
#about h1 {
font-size: 2.0rem;
}
small {
font-size: 95%;
}
.text-muted {
color: grey;
}
.form-check {
display: flex;
flex-direction: column;
margin-bottom: 1.0rem;
}
.btn:focus {
outline: none !important;
box-shadow: none !important;
}
.input-group-btn.spaced {
margin-left: 3px;
}
.input-group {
display: flex;
width: 100%;
}
#prefs, #settings, #editor, #about {
padding: 0.5rem;
}
//
// prefs page
//
#prefs {
display: grid;
grid-template-columns: var(--preview-wrapper-width) 1fr;
grid-template-rows: var(--preview-wrapper-height) 1fr var(--footer-height);
grid-template-areas: "saver-preview saver-list"
"saver-info basic-prefs"
"footer footer";
}
#prefs > header {
grid-area: header;
}
.platform-darwin .hide-on-darwin {
display: none;
}
.saver-list-wrapper {
grid-area: saver-list;
height: var(--preview-wrapper-height);
padding-left: 0.25rem;
}
.saver-list {
max-height: calc(var(--preview-wrapper-height) - 34px);
overflow-y: scroll;
margin-bottom: 0px;
font: small-caption;
li.list-group-item {
padding-left: 0.25rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding: 0.75rem 1.25rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
border-left: 1px solid rgba(0, 0, 0, 0.125);
}
li.list-group-item.active {
color: #fff;
background-color: #007bff;
border-color: #007bff;
}
label {
font-size: 0.9rem;
}
}
#prefs input[name=screensaver] {
display: none;
}
div.saver-detail {
grid-area: saver-preview;
height: var(--preview-wrapper-height);
overflow: hidden;
}
.saver-preview {
width: var(--preview-width);
height: var(--preview-height);
transform: scale(var(--preview-scale));
transform-origin: 0 0;
overflow: hidden;
border: 0;
}
.saver-preview::-webkit-scrollbar {
width: 0px;
height: 0px;
}
div.basic-prefs {
grid-area: basic-prefs;
margin-top: 1rem;
margin-right: 2rem;
}
div.saver-info {
grid-area: saver-info;
height: calc(99vh - var(--preview-wrapper-height) - var(--footer-height) - var(--space-at-top) - var(--small-padding));
overflow-y: auto;
}
div.saver-info #wrapper {
max-width: 98%;
}
#settings {
padding-top: 0.5rem;
}
footer.footer {
height: var(--footer-height);
grid-area: footer;
position: fixed;
bottom: 0;
left: 0;
right: 0;
align-items: center;
background-color: #f5f5f5;
padding-left: 5px;
padding-right: 5px;
display: flex;
justify-content: space-between;
}
#prefs h1 {
font-size: 1.4rem;
}
#advanced-prefs-form > h1 {
margin-top: 30px;
}
body > #editor > .content {
overflow-y: scroll;
}
body > #editor #settings > div {
padding-top: 35px;
}
ul {
padding-left: 0px;
list-style-type: none;
}
.hide {
display: none;
}
button.add-option {
margin-top: 12px;
}
.space-at-top {
margin-top: var(--space-at-top);
}
.space-at-bottom {
margin-bottom: 20px;
}
.saver-detail {
padding-left: 0px;
padding-right: 5px;
}
.saver-description .actions {
margin-top: 1rem;
margin-bottom: 1rem;
}
.hint {
color: #888888;
font-size: 95%;
}
h1 .hint {
font-size: 60%;
}
.window > footer {
padding: 5px;
}
.padded-top {
margin-top: 20px;
}
.padded-bottom {
padding-bottom: 60px;
}
//
// about page
//
#about {
overflow-x: hidden;
}
#about {
margin-left: 5px;
text-align: center;
}
#about h1, #about h2, #about h3, #about p {
overflow: visible;
margin-top: 5px;
margin-bottom: 5px;
}
#about h2 {
font-size: 1.3rem;
}
#about h3 {
font-size: 1.1rem;
}
#about p {
font-size: 120%;
}
#about svg {
width: 50%;
}
#options {
margin-bottom: 10px;
}
#options div.field {
display: flex;
}
#options div.input {
width: 75%;
align-self: center;
padding-right: 10px;
}
#options legend {
font-size: 95%;
width: 25%;
align-self: center;
margin: 0px;
}
form.submit-attempt {
input:invalid {
border: 2px dashed red;
}
}
form.entry {
margin-top: 5px;
margin-bottom: 5px;
padding-left: 1.5rem;
padding-bottom: 0.5rem;
border-bottom: 1px dashed #888888;
}
form.input {
margin-bottom: 10px;
display: flex;
flex-direction: column;
}
form.input input[type=text], input[type=range] {
width: 100%;
}
label.for-option {
font-weight: bold;
}
input[type="range"] {
max-width: 250px;
}
div.form-group {
margin-bottom: 1rem;
max-width: 95%;
label {
display: inline-block;
font-weight: bold;
margin-bottom: 0.5rem;
}
input, button, select {
display: block;
height: auto;
font-size: 1rem;
padding: .375rem .75rem;
line-height: 1.5;
}
input {
width: 100%;
}
input[type=checkbox] {
width: initial;
display: inline-block;
}
.hint {
display: block;
}
}
div.form-group.full-width {
max-width: 100%;
}
// new screensaver
#new {
padding: 0.5rem;
}
.need-setup-message {
height: 80vh;
}
.block {
display: block;
}
#editor {
h1, h2, h3, h4 {
margin-bottom: 0px;
}
h1, h2, h3, h4 {
& + small {
display: block;
margin-bottom: 1rem;
}
& + input {
margin-top: 0.5rem;
margin-left: 0.5rem;
}
}
}
input[type="checkbox"] + label {
margin-top: 0.5rem;
margin-left: 0.5rem;
}
div.notarize-wrapper {
position: fixed;
top: 10px;
right: 10px;
background: white;
min-height: 40px;
min-width: 100px;
max-width: 200px;
border-radius: 5px;
border-color: black;
border: 2px solid;
padding: 5px;
display: flex;
align-items: center;
justify-content: center;
z-index: 100000;
}
@keyframes fadeIn {
0% {opacity:0;}
100% {opacity:1;}
}
@keyframes fadeOut {
0% {opacity:1;}
100% {opacity:0;}
}
.notarize-in {
animation: fadeIn 1.0s;
}
.notarize-out {
animation: fadeOut 1.0s;
}
================================================
FILE: src/index.ejs
================================================
<%= htmlWebpackPlugin.options.title %>
================================================
FILE: src/lib/package.js
================================================
"use strict";
import fs from 'fs-extra';
import path from "path";
import temp from "temp";
import os from "os";
import { mkdirp } from "mkdirp";
import { rimrafSync } from "rimraf";
import * as yauzl from "yauzl"
import * as lockfile from "proper-lockfile";
import { Readable } from "stream";
import { finished } from "stream/promises";
import semver from "semver";
/**
* need source repo url
* call https://developer.github.com/v3/repos/releases/#get-the-latest-release
* check published_at
* if it's after stored value, download it!
*/
export default class Package {
constructor(_attrs) {
this.repo = _attrs.repo;
this.dest = _attrs.dest;
this.version = _attrs.version;
this.downloaded = false;
this.url = `https://api.github.com/repos/${this.repo}/releases/latest`;
if ( typeof(this.version) === "undefined" ) {
const saverPackageJson = path.join(this.dest, "package.json");
if ( fs.existsSync(saverPackageJson) ) {
this.version = JSON.parse(fs.readFileSync(saverPackageJson)).version;
}
}
if ( typeof(_attrs.log) === "undefined" ) {
_attrs.log = function() {};
}
this.logger = _attrs.log;
this.defaultHeaders = {
"User-Agent": "Before Dawn"
};
}
attrs() {
return {
dest: this.dest,
version: this.version,
downloaded: this.downloaded
};
}
async getReleaseInfo() {
this.logger(`get release info from ${this.url}`);
if ( this.data ) {
return this.data;
}
this.data = await fetch(this.url, this.defaultHeaders)
.then(res => res.json())
.then((json) => {
const remoteVersion = json.tag_name.replace(/^v/, "");
json.is_update = this.version === undefined || semver.gt(remoteVersion, this.version);
return json;
})
.catch((err) => {
this.logger(err);
if ( typeof(this.data) !== "undefined" ) {
return this.data;
}
else {
return {};
}
});
return this.data;
}
async hasUpdate() {
const data = await this.getReleaseInfo();
return data.is_update;
}
async checkLatestRelease(force) {
const data = await this.getReleaseInfo();
if ( force === true || data.is_update ) {
return this.downloadRelease();
}
else {
this.logger("no package update available");
return this.attrs();
}
}
async downloadRelease() {
this.logger("download package updates!");
const data = await this.getReleaseInfo();
let dest;
if ( this.useLocalFile ) {
dest = this.localZip;
}
else {
dest = await this.downloadFile(data.zipball_url);
}
await this.zipToSavers(dest);
this.downloaded = true;
this.updated_at = data.published_at;
return this.attrs();
}
async downloadFile(url, dest) {
if ( dest === undefined ) {
dest = temp.path({dir: os.tmpdir(), suffix: ".zip"});
}
const res = await fetch(url, this.defaultHeaders);
// https://stackoverflow.com/questions/37614649/how-can-i-download-and-save-a-file-using-the-fetch-api-node-js
const fileStream = fs.createWriteStream(dest);
await finished(Readable.fromWeb(res.body).pipe(fileStream));
return dest;
}
zipToSavers(tempName, dest) {
let self = this;
if ( dest === undefined ) {
dest = self.dest;
}
return new Promise(function (resolve, reject) {
lockfile.lock(dest, { realpath: false, stale: 30000 }).then((release) => {
yauzl.open(tempName, {lazyEntries: true, validateEntrySizes: false}, (err, zipfile) => {
if (err) {
release().then(() => {
reject(err);
});
}
else {
//
// clean out existing files
//
try {
rimrafSync(self.dest);
}
catch (err) {
self.logger(err);
}
zipfile.readEntry();
zipfile.on("entry", function(entry) {
var fullPath = entry.fileName;
// the incoming zip filename will have on extra directory on it
// projectName/dir/etc/file
//
// example: muffinista-before-dawn-screensavers-d388377/starfield/index.html
//
// let's get rid of the projectName
//
var parts = fullPath.split(/\//);
parts.shift();
fullPath = path.join(dest, path.join(...parts));
if (/\/$/.test(entry.fileName)) {
// directory file names end with '/'
mkdirp(fullPath).then(() => {
zipfile.readEntry();
}).catch((err) => {
release().then(() => {
return reject(err);
});
});
}
else {
// file entry
zipfile.openReadStream(entry, function(err, readStream) {
if (err) {
release().then(() => {
return reject(err);
});
}
// ensure parent directory exists
mkdirp(path.dirname(fullPath)).then(() => {
self.logger(`${entry.fileName} -> ${fullPath}`);
readStream.pipe(fs.createWriteStream(fullPath));
readStream.on("end", function() {
zipfile.readEntry();
});
}).catch((err) => {
release().then(() => {
return reject(err);
});
});
});
}
});
zipfile.on("end", function() {
release().then(() => {
resolve(self.attrs());
});
});
}
});
});
});
}
}
================================================
FILE: src/lib/prefs-schema.json
================================================
{
"saver": {
"type": "string",
"default": ""
},
"sourceRepo": {
"type": "string",
"default": "muffinista/before-dawn-screensavers"
},
"delay": {
"type": "number",
"default": 5
},
"sleep": {
"type": "number",
"default": 10
},
"lock": {
"type": "boolean",
"default": false
},
"disableOnBattery": {
"type": "boolean",
"default": true
},
"auto_start": {
"type": "boolean",
"default": false
},
"runOnSingleDisplay": {
"type": "boolean",
"default": true
},
"localSource": {
"type": "string",
"default": ""
},
"options": {
"default": {}
},
"sourceUpdatedAt": {
"default": "1970-01-01T00:00:00.000Z"
},
"updateCheckTimestamp": {
"default": "1970-01-01T00:00:00.000Z"
},
"launchShortcut": {
"type": "string",
"default": ""
},
"firstLoad": {
"type": "boolean"
}
}
================================================
FILE: src/lib/prefs.js
================================================
"use strict";
import * as path from "path";
import Conf from "conf";
const DEFAULTS = {
"saver": {
"type": "string",
"default": ""
},
"sourceRepo": {
"type": "string",
"default": "muffinista/before-dawn-screensavers"
},
"delay": {
"type": "number",
"default": 5
},
"sleep": {
"type": "number",
"default": 10
},
"lock": {
"type": "boolean",
"default": false
},
"disableOnBattery": {
"type": "boolean",
"default": true
},
"auto_start": {
"type": "boolean",
"default": false
},
"runOnSingleDisplay": {
"type": "boolean",
"default": true
},
"localSource": {
"type": "string",
"default": ""
},
"options": {
"default": {}
},
"sourceUpdatedAt": {
"default": "1970-01-01T00:00:00.000Z"
},
"updateCheckTimestamp": {
"default": "1970-01-01T00:00:00.000Z"
},
"launchShortcut": {
"type": "string",
"default": ""
},
"firstLoad": {
"type": "boolean"
}
};
class SaverPrefs {
constructor(baseConfigDir, rootDir=undefined, saversDir=undefined) {
this.baseDir = baseConfigDir;
if ( rootDir === undefined ) {
this.rootDir = this.baseDir;
}
else {
this.rootDir = rootDir;
}
if ( saversDir === undefined ) {
this.saversDir = path.join(this.rootDir, "savers");
}
else {
this.saversDir = saversDir;
}
this.systemSource = path.join(this.rootDir, "system-savers");
this.confOpts = {
schema: DEFAULTS,
clearInvalidConfig: true,
cwd: this.baseDir
};
if ( process.env.CONFIG_DIR ) {
this.confOpts.cwd = process.env.CONFIG_DIR;
}
this.reload();
}
get configFile() {
return this.store.path;
}
get data() {
let result = {};
let self = this;
Object.keys(DEFAULTS).forEach(function(name) {
result[name] = self.store.get(name);
});
return result;
}
get defaults() {
let result = {};
Object.keys(DEFAULTS).forEach(function(name) {
result[name] = DEFAULTS[name].default;
});
return result;
}
reload() {
this.store = new Conf(this.confOpts);
this.firstLoad = this.store.get("firstLoad", true);
if ( this.firstLoad === true ) {
this.store.set("firstLoad", false);
}
// if (this.saver) {
// this.saver = this.saver.split(path.sep).join(path.posix.sep);
// }
}
reset() {
this.store.clear();
this.store._write({});
}
get needSetup() {
return this.firstLoad === true ||
this.saver === undefined ||
this.saver === "";
}
get defaultSaversDir() {
return this.saversDir;
}
/**
* get a list of folders we should check for screensavers
*/
get sources() {
var local = this.localSource;
var system = this.systemSource;
var folders = [this.defaultSaversDir];
// if there's a local source, use that
if ( local !== "" ) {
folders = folders.concat( local );
}
folders = folders.concat( system );
return folders;
}
get systemSource() {
return this._systemSource;
}
set systemSource(val) {
this._systemSource = val;
}
//
// get options for the specified screensaver
//
getOptions(name) {
if ( typeof(name) === "undefined" ) {
name = this.saver;
}
const opts = this.store.get("options", {});
const result = opts[name];
if ( result === undefined ) {
return {};
}
return result;
}
}
Object.keys(DEFAULTS).forEach(function(name) {
Object.defineProperty(SaverPrefs.prototype, name, {
get() {
const result = this.store.get(name);
if ( name === "sourceUpdatedAt" || name === "updateCheckTimestamp" ) {
return new Date(result);
}
return result;
},
set(newval) {
if ( newval === undefined ) {
this.store.delete(name);
}
else {
if ( typeof(newval) === "object" && ( name === "sourceUpdatedAt" || name === "updateCheckTimestamp" )) {
newval = newval.toISOString();
}
this.store.set(name, newval);
}
}
});
});
export default SaverPrefs;
================================================
FILE: src/lib/saver-factory.js
================================================
"use strict";
import * as path from "path";
import fs from 'fs-extra'
export default class SaverFactory {
constructor(prefs, logger) {
this.prefs = prefs;
if ( logger !== undefined ) {
this.logger = logger;
}
else {
this.logger = function() {};
}
}
/**
* generate a screensaver template
*/
create(src, destDir, opts) {
if ( destDir === "" || destDir === undefined ) {
throw new Error("No local directory specified!");
}
this.logger(`SRC: ${src}`);
let contents = fs.readdirSync(src);
const defaults = {
"source": "index.html",
"options": []
};
opts = Object.assign({}, defaults, opts);
opts.key = opts.name.toLowerCase().
replace(/[^a-z0-9]+/gi, "-").
replace(/-$/, "").
replace(/^-/, "");
var dest = path.join(destDir, opts.key);
this.logger(`mkdir ${dest}`);
fs.mkdirpSync(dest);
contents.forEach(function(content) {
fs.copySync(path.join(src, content), path.join(dest, content));
});
//
// generate JSON file
//
var configDest = path.join(dest, "saver.json");
var content = fs.readFileSync( configDest );
contents = Object.assign({}, JSON.parse(content), opts);
fs.writeFileSync(configDest, JSON.stringify(contents, null, 2));
// add dest in case someone needs it
// but don't persist that data because that would be icky
opts.dest = path.join(dest, "saver.json");
return opts;
}
}
================================================
FILE: src/lib/saver-list.js
================================================
"use strict";
import fs from 'fs-extra';
import path from "path";
import { mkdirp } from "mkdirp";
import { rimraf } from "rimraf";
import { glob } from "glob";
const CONFIG_FILE_NAME = "config.json";
/**
* skip any folder which contains a '.before-dawn-skip' file
* this way we can have templates and documentation and things like
* that which won't get loaded into the app by mistake.
*/
var skipFolder = function(p) {
return fs.existsSync(path.join(p, ".before-dawn-skip"));
};
export default class SaverListManager {
constructor(opts, logger) {
this.prefs = opts.prefs;
this.loadedScreensavers = [];
if ( logger !== undefined ) {
this.logger = logger;
}
else {
this.logger = function() {};
}
this.baseDir = this.prefs.baseDir;
if ( opts.rootDir ) {
this.rootDir = opts.rootDir;
}
else {
this.rootDir = this.baseDir;
}
}
get defaultSaversDir() {
return this.prefs.saversDir;
}
async setup() {
let _self = this;
var configPath = path.join(_self.baseDir, CONFIG_FILE_NAME);
var saversDir = _self.defaultSaversDir;
var results = {
first: false,
setup: false
};
_self.logger("saversDir: " + saversDir, fs.existsSync(saversDir));
_self.logger("configPath: " + configPath);
// check for/create our main directory
// and our savers directory (which is a subdir
// of the main dir)
const made = await mkdirp(saversDir);
// check if we just created the folder,
// if there's no config yet,
// or if the savers folder was empty
if ( made === true || ! fs.existsSync(configPath) || fs.readdirSync(saversDir).length === 0 ) {
results.first = true;
}
results.setup = true;
return results;
}
/**
* reload all our data/config/etc
*/
reload(load_savers) {
this.logger("savers.reload");
return this.setup(load_savers).then(this.handlePackageChecks);
}
reset() {
this.loadedScreensavers = [];
}
normalizePath(p) {
return p.split(path.sep).join(path.posix.sep);
}
/**
* search for all screensavers we can find on the filesystem. if cb is specified,
* call it with data when done. if reload == true, don't use cached data.
*/
async list(force) {
let _self = this;
var folders = this.prefs.sources;
var pattern, savers;
var promises = [];
// exclude system screensavers from the cache check
// @todo get rid of this
var systemScreensaverCount = 1;
// use cached data if available
if ( this.loadedScreensavers.length > systemScreensaverCount &&
( typeof(force) === "undefined" || force === false ) ) {
return this.loadedScreensavers;
}
// note: using /**/ here instead of /*/ would
// also match all subdirectories, which might be desirable
// or even required, but is a lot slower, so not doing it
// for now
folders = folders.filter((el) => {
return el !== undefined && el !== "" && fs.existsSync(el);
});
folders.forEach((sourceFolder) => {
// glob doesn't work with windows style file paths, so convert
// to posix
sourceFolder = this.normalizePath(sourceFolder);
pattern = `${sourceFolder}/*/saver.json`;
savers = glob.sync(pattern);
for ( var i = 0; i < savers.length; i++ ) {
var f = this.normalizePath(savers[i]);
var folder = path.dirname(f);
// exclude skippable folders
var doLoad = ! folder.split(/[/|\\]/).reverse()[0].match(/^__/) &&
! skipFolder(folder);
if ( doLoad ) {
promises.push(this.loadFromFile(f));
}
}
});
// filter out failed promises here
// @see https://davidwalsh.name/promises-results
promises = promises.map(p => p.catch(() => undefined));
const data = await Promise.all(promises);
// remove any undefined screensavers
_self.loadedScreensavers = data.
filter(s => s !== undefined).
sort((a, b) => {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
});
return _self.loadedScreensavers;
}
/**
* pick a random screensaver
*/
random() {
var tmp = this.loadedScreensavers.filter((s) => {
return ( typeof(s.preload) === "undefined" );
});
var idx = Math.floor(Math.random() * tmp.length);
return tmp[idx];
}
async confirmExists(key) {
await this.list();
return this.getByKey(key) !== undefined;
}
/**
* look up a screensaver by key, and return it
*/
getByKey(key) {
key = this.normalizePath(key);
var result = this.loadedScreensavers.find((obj) => {
return obj.key === key;
});
return result;
}
/**
* load screensaver data from filesystem
*/
loadFromFile(src, settings) {
let _self = this;
src = this.normalizePath(src);
return new Promise(function (resolve, reject) {
fs.readFile(src, {encoding: "utf8"}, (err, content) => {
if ( err ) {
_self.logger("loadFromFile err", src, err);
reject(err);
}
else {
try {
var contents = JSON.parse(content);
var stub = path.dirname(src);
var s = _self.loadFromData(contents, stub, settings);
// add the source path as an attribute to make it easier
// to load/save/update this saver later if needed
s.src = src;
if ( s.valid ) {
resolve(s);
}
else {
_self.logger("loadFromFile not valid! " + src);
reject();
}
}
catch(e) {
_self.logger("loadFromFile exception", e);
reject(e);
}
}
});
});
}
loadFromData(contents, stub, settings) {
var src = this.normalizePath(this.prefs.localSource);
if ( typeof(stub) !== "undefined" ) {
contents.path = stub;
contents.key = this.normalizePath(path.join(stub, "saver.json"));
}
contents.editable = false;
if ( typeof(src) !== "undefined" && src !== "" ) {
contents.editable = (contents.key.indexOf(src) === 0);
}
if ( typeof(contents.settings) === "undefined" ) {
if ( settings === undefined ) {
if ( ! this.prefs.options ) {
this.prefs.options = {};
}
// ensure that all screensavers have options set
if ( this.prefs.options[contents.key] === undefined ) {
this.prefs.options[contents.key] = {};
}
settings = this.prefs.options[contents.key];
}
contents.settings = settings;
}
// set a URL
if ( typeof(contents.url) === "undefined" &&
contents.path !== undefined &&
contents.source !== undefined) {
contents.url = `file://${[contents.path, contents.source].join("/")}`;
}
if ( typeof(contents.published) === "undefined" ) {
contents.published = true;
}
if ( typeof(contents.requirements) === "undefined" ) {
contents.requirements = ["screen"];
}
contents.valid = typeof(contents.name) !== "undefined" &&
typeof(contents.description) !== "undefined" &&
contents.published === true;
return contents;
}
/**
* delete a screensaver -- this removes the directory that contains all files
* for the screensaver.
*/
async delete(s) {
var k = s.key;
var p = path.dirname(k);
if ( typeof(s) !== "undefined" && s.editable === true ) {
await rimraf(p);
return true;
}
else {
return false;
}
}
}
================================================
FILE: src/lib/saver.js
================================================
/**
* simple class for a screen saver
*/
// we will generate a list of requirements that screensavers need
// to work. for now, it's just a screengrab. to maintain
// compatability, we'll generate a default list if one isn't
// specified
const DEFAULT_REQUIREMENTS = ["screen"];
import fs from 'fs-extra';
import * as nodePath from "path";
export default class Saver {
constructor(_attrs) {
this.UNWRITABLE_KEYS = ["key", "path", "url", "settings", "editable"];
this.attrs = _attrs;
this.path = _attrs.path;
this.name = _attrs.name;
this.key = _attrs.key;
this.description = _attrs.description;
this.aboutUrl = _attrs.aboutUrl;
this.author = _attrs.author;
this.license = _attrs.license;
this.preload = _attrs.preload;
this.requirements = _attrs.requirements || DEFAULT_REQUIREMENTS;
// allow for a specified URL -- this way you could create a screensaver
// that pointed to a remote URL
this.url = _attrs.url;
if ( typeof(this.url) === "undefined" &&
_attrs.path !== undefined &&
_attrs.source !== undefined) {
this.url = `file://${[_attrs.path, _attrs.source].join("/")}`;
}
// keep track of our main saver.json file
this.src = _attrs.src;
if ( typeof(this.src) === "undefined" && typeof(this.key) !== "undefined" ) {
const baseDir = this.key.replace(/\\/g,"/").replace(/\/[^/]*$/, "");
this.src = [baseDir, "saver.json"].join("/");
}
this.published = _attrs.published;
if ( typeof(this.published) === "undefined" ) {
this.published = true;
}
// provide a default editable value (this will
// be set when loading to determine if the user
// can edit this screensaver or not)
this.editable = _attrs.editable;
if ( typeof(this.editable) === "undefined" ) {
this.editable = false;
}
this.valid = typeof(this.name) !== "undefined" &&
typeof(this.description) !== "undefined" &&
this.published === true;
if ( typeof(_attrs.options) === "undefined" ) {
_attrs.options = [];
}
this.options = _attrs.options;
if ( this.valid === true ) {
// figure out the settings from any defaults for this screensaver,
// and combine with incoming user-specified settings
this.settings = _attrs.options.map(function(o) {
return [o.name, o.default];
}).reduce(function(o, v) {
o[v[0]] = v[1];
return o;
}, {});
this.settings = Object.assign({}, this.settings, _attrs.settings);
// allow for custom preview URL -- if not specified, just use the default
// if it is specified, do some checks to see if it's a full URL or a filename
// in which case we will turn it into a full path
if ( typeof(this.attrs.previewUrl) === "undefined" ) {
this.previewUrl = this.url;
}
else if ( this.attrs.previewUrl.match(/:\/\//) ) {
this.previewUrl = this.attrs.previewUrl;
}
else {
this.previewUrl = this.path + "/" + this.attrs.previewUrl;
}
} // if valid
}
urlWithParams(opts={}) {
if ( !this.url.match(/^file:/) ) {
return this.url;
}
const urlParams = new URLSearchParams(opts);
if ( this.settings ) {
const keys = Object.keys(this.settings);
keys.forEach((k) => {
urlParams.append(k, this.settings[k]);
});
}
return `${this.url}?${urlParams.toString()}`;
}
toHash() {
return this.attrs;
}
toJSON(attrs) {
for ( var i = 0 ; i < this.UNWRITABLE_KEYS.length; i++ ) {
delete(attrs[this.UNWRITABLE_KEYS[i]]);
}
if ( attrs.requirements === undefined ) {
attrs.requirements = [];
}
else {
attrs.requirements = attrs.requirements.filter(r => r !== "none");
}
if ( attrs.requirements.length === 0 ) {
attrs.requirements = ["none"];
}
return JSON.stringify(attrs, null, 2);
}
write(attrs, configDest) {
if ( typeof(attrs) === "undefined" ) {
attrs = this.attrs;
}
if ( typeof(configDest) === "undefined" ) {
configDest = nodePath.join(this.path, "saver.json");
}
fs.writeFileSync(configDest, this.toJSON(attrs));
}
}
================================================
FILE: src/main/assets/global.css
================================================
body {margin:0; padding:0; overflow: hidden}
*, *:hover { cursor: none !important; }
canvas {display:block;}
canvas:focus {outline:0;}
================================================
FILE: src/main/assets/grabber.html
================================================
screen grabber
you should never see me
================================================
FILE: src/main/assets/grabber.mjs
================================================
/**
* This is the preload script for the screen grabber
*
*/
const { contextBridge, ipcRenderer } = require("electron");
/**
* look for an element in the DOM and create it if it doesn't exist
*/
var findOrCreate = function(type, screen_id) {
const id = `${type}${screen_id}`;
let el = document.getElementById(id);
if ( el === null ) {
el = document.createElement(type);
el.id = id;
document.body.appendChild(el);
}
return el;
};
/**
* Apply the video stream to the canvas. Return a context with a screenshot
*
* @param {*} video
* @param {*} canvas
*/
var applyVideoToCanvas = function(video, canvas) {
const width = video.videoWidth;
const height = video.videoHeight;
canvas.setAttribute("width", width);
canvas.setAttribute("height", height);
canvas.width = width;
canvas.height = height;
const context = canvas.getContext("2d");
context.drawImage(video, 0, 0, width, height);
return context;
};
let captureIndex = 0;
var screenToBuffer = async function(video) {
const tempName = `capture-index-${captureIndex}`;
const canvas = findOrCreate("canvas", tempName);
const context = applyVideoToCanvas(video, canvas);
const data = canvas.toDataURL("image/png", 1.0);
context.clearRect(0, 0, canvas.width, canvas.height);
const buffer = Buffer.from(data.split(",")[1], "base64");
canvas.remove();
return buffer;
};
/**
* cleanup video/media stream
* @param {*} video
* @param {*} s
*/
var cleanup = function(video, s) {
//
// stop video capture
// this seems to handle a problem where CPU load spikes
// after capture
//
if ( s !== undefined ) {
s.getVideoTracks().forEach((track) => {
track.stop();
});
}
// https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Using_HTML5_audio_and_video
video.pause();
video.src = "";
video.load();
video.remove();
};
/**
* capture the screen
* @param {*} id ID of the screen to capture
* @param {*} width
* @param {*} height
*/
var captureScreen = async function(id, width, height) {
const screen_opts = {
audio: false,
video: {
mandatory: {
// fun fact -- you need to use max here
// @see https://groups.google.com/a/chromium.org/forum/#!topic/chromium-apps/TP_rsnYVQWg
maxWidth: width,
maxHeight: height,
chromeMediaSource: "desktop"
}
}
};
screen_opts.video.mandatory.chromeMediaSourceId = id;
const video = findOrCreate("video", id);
// adding muted helps with some security errors
// @see https://stackoverflow.com/questions/49930680/
// how-to-handle-uncaught-in-promise-domexception-play-failed-because-the-use
video.muted = "muted";
const mediaStream = await navigator.mediaDevices.getUserMedia(screen_opts);
video.srcObject = mediaStream;
await video.play();
const result = await screenToBuffer(video, mediaStream);
cleanup(video, mediaStream);
return result;
}; // captureScreen
contextBridge.exposeInMainWorld(
"grabber",
{
init: () => {
ipcRenderer.on("request-screenshot", async (_event, opts) => {
const result = await captureScreen(opts.id, opts.width, opts.height);
ipcRenderer.send("screenshot-" + opts.id, {buffer: result});
});
}
}
);
================================================
FILE: src/main/assets/preload.mjs
================================================
const { contextBridge, ipcRenderer } = require("electron");
const api = {
platform: () => process.platform,
getDisplayBounds: async() => ipcRenderer.invoke("get-primary-display-bounds"),
getScreenshot: async() => ipcRenderer.invoke("get-primary-screenshot"),
getGlobals: async() => ipcRenderer.invoke("get-globals"),
addListener: (key, fn) => ipcRenderer.on(key, fn),
removeListener: (key, fn) => ipcRenderer.removeListener(key, fn),
getPrefs: async() => ipcRenderer.invoke("get-prefs"),
getDefaults: async() => ipcRenderer.invoke("get-defaults"),
openWindow: (name, opts) => ipcRenderer.send("open-window", name, opts),
closeWindow: (name) => ipcRenderer.send("close-window", name),
listSavers: async() => ipcRenderer.invoke("list-savers"),
loadSaver: async(src) => ipcRenderer.invoke("load-saver", src),
deleteSaverDialog: async(key) => ipcRenderer.invoke("delete-screensaver-dialog", key),
deleteSaver: async(key) => ipcRenderer.invoke("delete-saver", key),
saveScreensaver: async(saver, src) => ipcRenderer.invoke("save-screensaver", saver, src),
updatePrefs: async(prefs) => ipcRenderer.invoke("update-prefs", prefs),
setAutostart: (value) => ipcRenderer.send("set-autostart", value),
setGlobalLaunchShortcut: (value) => ipcRenderer.send("set-global-launch-shortcut", value),
displayUpdateDialog: () => ipcRenderer.send("display-update-dialog"),
resetToDefaultsDialog: async() => ipcRenderer.invoke("reset-to-defaults-dialog"),
openUrl: (url) => ipcRenderer.send("launch-url", url),
updateLocalSource: (ls) => ipcRenderer.invoke("update-local-source", ls),
createScreensaver: (opts) => ipcRenderer.invoke("create-screensaver", opts),
saversUpdated: (key) => ipcRenderer.send("savers-updated", key),
getScreensaverPackage: () => ipcRenderer.invoke("check-screensaver-package"),
downloadScreensaverPackage: () => ipcRenderer.invoke("download-screensaver-package"),
showOpenDialog: () => ipcRenderer.invoke("show-open-dialog"),
openFolder: (path) => ipcRenderer.send("open-folder", path),
watchFolder: (src) => ipcRenderer.send("watch-folder", src),
unwatchFolder: (src) => ipcRenderer.send("unwatch-folder", src),
onFolderUpdate: (cb) => ipcRenderer.on("folder-update", cb),
toggleDevTools: () => ipcRenderer.send("toggle-dev-tools"),
log: (payload) => ipcRenderer.send("console-log", payload)
};
contextBridge.exposeInMainWorld("api", api);
================================================
FILE: src/main/assets/shim.html
================================================
test shim
test shim!
go
================================================
FILE: src/main/assets/shim.js
================================================
const { contextBridge, ipcRenderer } = require("electron");
const shimApi = {
send: (cmd, opts, args={}) => ipcRenderer.send(cmd, opts, args),
getCurrentState: async () => ipcRenderer.invoke("get-current-state"),
getTrayItems: async () => ipcRenderer.invoke("get-tray-items"),
clickTray: (label) => {
ipcRenderer.invoke("click-tray-item", label);
}
};
contextBridge.exposeInMainWorld("shimApi", shimApi);
================================================
FILE: src/main/autostarter.js
================================================
"use strict";
import * as main from "./index.js";
import AutoLaunch from "auto-launch";
export function toggle(appName, value) {
var appLauncher = new AutoLaunch({
name: appName
});
if ( value === true ) {
appLauncher.isEnabled().then((isEnabled) => {
if ( isEnabled ) {
return;
}
appLauncher.
enable().
then((err) =>{
main.log.info("appLauncher enable", err);
}).catch((err) => {
main.log.info("appLauncher enable failed", err);
});
});
}
else {
main.log.info("set auto start == false");
appLauncher.isEnabled().then((isEnabled) => {
if ( !isEnabled ) {
return;
}
appLauncher.
disable().
then(function() {
main.log.info("appLauncher disabled");
}).
catch((err) => {
main.log.info("appLauncher disable failed", err);
});
});
}
}
================================================
FILE: src/main/bootstrap.js
================================================
import { readFile } from 'fs/promises';
export default async function bootstrapApp() {
const packageJSON = JSON.parse(
await readFile(
new URL('../../package.json', import.meta.url)
)
);
var version = undefined;
try {
version = packageJSON.version;
if ( ! process.env.BEFORE_DAWN_RELEASE_NAME ) {
process.env.BEFORE_DAWN_RELEASE_NAME = `${packageJSON.productName} ${packageJSON.version}`;
}
}
catch {
version = "0.0.0";
}
global.APP_NAME = "Before Dawn";
global.APP_DIR = "Before Dawn";
global.SAVER_REPO = "muffinista/before-dawn-screensavers";
global.APP_REPO = "muffinista/before-dawn";
global.APP_VERSION_BASE = version;
global.APP_VERSION = `v${version}`;
global.NEW_RELEASE_AVAILABLE = false;
global.HELP_URL = "https://muffinista.github.io/before-dawn/";
global.ISSUES_URL = "https://github.com/muffinista/before-dawn/issues";
global.APP_CREDITS = "by Colin Mitchell // muffinlabs.com";
if ( packageJSON.release_server ) {
global.RELEASE_SERVER = packageJSON.release_server;
global.RELEASE_CHECK_URL = `${global.RELEASE_SERVER}/update/${process.platform}/${global.APP_VERSION_BASE}`;
global.PACKAGE_DOWNLOAD_URL = `https://github.com/${global.APP_REPO}/releases/latest`;
}
}
================================================
FILE: src/main/dock.js
================================================
"use strict";
import {app, BrowserWindow} from "electron";
/**
* if we're using the dock, and all our windows are closed, hide the
* dock icon
*/
export const hideDockIfInactive = function() {
let openWindowCount = BrowserWindow.getAllWindows().
filter(win => (win !== undefined && win.noTray !== true) ).length;
if ( typeof(app.dock) !== "undefined" && openWindowCount === 0 ) {
app.dock.hide();
}
};
/**
* show the dock if it's available
*/
export const showDock = function() {
if ( typeof(app.dock) !== "undefined" ) {
app.dock.show();
}
};
================================================
FILE: src/main/index.dev.js
================================================
/**
* This file is used specifically and only for development. There shouldn't be
* any need to modify this file, but it can be used to extend your development
* environment.
*/
// Set environment for development
process.env.NODE_ENV = 'development';
// Require `main` process to boot app
require('./index');
================================================
FILE: src/main/index.js
================================================
"use strict";
// process.traceDeprecation = true;
// process.traceProcessWarnings = true;
/***
Welcome to....
____ __ ____
| __ ) ___ / _| ___ _ __ ___ | _ \ __ ___ ___ __
| _ \ / _ \ |_ / _ \| '__/ _ \ | | | |/ _` \ \ /\ / / '_ \
| |_) | __/ _| (_) | | | __/ | |_| | (_| |\ V V /| | | |
|____/ \___|_| \___/|_| \___| |____/ \__,_| \_/\_/ |_| |_|
a screensaver package built on the tools of the web. Enjoy!
*/
import { init } from '@sentry/electron';
if ( process.env.TEST_MODE === undefined && process.env.SENTRY_DSN !== undefined ) {
console.log(`setting up sentry with ${process.env.SENTRY_DSN}`);
try {
init({
dsn: process.env.SENTRY_DSN,
onFatalError: console.log
});
}
catch(e) {
console.log(e);
}
}
import {app,
BrowserWindow,
desktopCapturer,
dialog,
globalShortcut,
ipcMain,
Menu,
net,
session,
shell,
systemPreferences,
Tray,
powerMonitor} from "electron";
import isDev from 'electron-is-dev';
import log from 'electron-log';
import { screen as electronScreen } from "electron";
import * as fs from "fs";
import { readFile } from 'fs/promises';
import * as os from "os";
import * as path from "path";
import * as temp from "temp";
import * as url from "url";
import { execFile as exec } from "child_process";
import * as screenLock from "./screen.js";
import StateManager from "./state_manager.js";
import SaverPrefs from "../lib/prefs.js";
import SaverFactory from "../lib/saver-factory.js";
import Saver from "../lib/saver.js";
import SaverListManager from "../lib/saver-list.js";
import Package from "../lib/package.js";
import Power from "../main/power.js";
import * as menusAndTrays from "./menus.js";
import * as dock from "./dock.js";
import * as windows from "./windows.js";
import forceFocus from "forcefocus";
import ReleaseCheck from "./release_check.js";
import * as autostarter from "./autostarter.js";
/**
* try and guess if we are in fullscreen mode or not
*/
import FullScreen from "detect-fullscreen";
const { isFullscreen } = FullScreen;
const packageJSON = JSON.parse(
await readFile(
new URL('../../package.json', import.meta.url)
)
);
var releaseChecker;
// NOTE -- this needs to be global, otherwise the app icon gets
// garbage collected and won't show up in the system tray
let appIcon = null;
let debugMode = ( process.env.DEBUG_MODE !== undefined );
let testMode = ( process.env.TEST_MODE !== undefined );
let cursor;
if (testMode || debugMode) {
log.transports.console.format = "{h}:{i}:{s} {text}";
log.catchErrors();
}
//
// don't hide cursor in tests or in windows, since
// that causes the tray to stop working???
//
if ( testMode || process.platform === "win32" ) {
cursor = {
hide: () => {},
show: () => {}
};
}
else {
cursor = await import("hide-cursor");
cursor = cursor.default;
}
let exitOnQuit = false;
/**
* track some information about windows and preview bounds for the prefs window
* and editor window
*/
let handles = {
prefs: {
window: null,
bounds: {
width: 320,
height: 0
},
max: {
width: 320,
height: 320
}
},
settings: {
window: null
},
addNew: {
window: null
},
about: {
window: null
},
editor: {
window: null,
bounds: {
width: 320,
height: 0
},
max: {
width: 320,
height: 320
}
},
// shim: {
// window: null
// }
};
let trayMenu;
let prefs = undefined;
let savers = undefined;
let stateManager = undefined;
// usually we want to check power state before running, but
// we'll skip that check depending on the value of this toggle
// so that manually running screensaver works just fine
let checkPowerState = true;
const RELEASE_CHECK_INTERVAL = 1000 * 60 * 60 * 12;
// load a few global variables
import bootstrapApp from "./bootstrap.js";
await bootstrapApp();
const defaultWebPreferences = {
enableRemoteModule: false,
contextIsolation: true,
nodeIntegration: false,
nativeWindowOpen: true,
webSecurity: !isDev
};
const singleLock = app.requestSingleInstanceLock();
if (! singleLock ) {
console.log("looks like another copy of app is running, exiting!");
app.quit();
process.exit();
}
const power = new Power({
platform: process.platform,
method: powerMonitor.isOnBatteryPower
});
let screenData = [];
var listScreens = async function() {
try {
const sources = await desktopCapturer.getSources({
types: ["screen"],
});
screenData = sources;
return sources;
} catch(e) {
log.info(e);
}
return [];
};
/**
* Open the screengrab window
*
* @returns {Promise} Promise that resolves once window is loaded
*/
var openGrabberWindow = function() {
return new Promise((resolve) => {
log.info("openGrabberWindow");
const grabberUrl = `file://${getAssetsDir()}/grabber.html`;
var grabberWindow = new BrowserWindow({
show: false,
skipTaskbar: true,
width: 100,
height: 100,
x: 6000,
y: 2000,
webPreferences: {
...defaultWebPreferences,
preload: path.join(getAssetsDir(), "grabber.mjs")
}
});
// grabberWindow.noTray = true;
grabberWindow.once("ready-to-show", () => {
resolve(grabberWindow);
});
grabberWindow.loadURL(grabberUrl);
});
};
/**
* open our screen grabber tool and issue a screengrab request
* @param {Screen} s the screen to grab
* @returns {Promise} Promise that resolves with object containing URL of screenshot
*/
var grabScreen = function(s) {
log.info(`grab screen ${s.id}`);
let screen = screenData.find((s) => { return s.display_id.toString() === s.id.toString(); });
if ( ! screen ) {
screen = screenData[0];
}
return new Promise((resolve) => {
//
// bypass screen capture in test mode
// or if the user has blocked screen access
//
if (
(process.platform === "darwin" && systemPreferences.getMediaAccessStatus("screen") !== "granted" ) ||
testMode === true ) {
resolve({
url: path.join(getAssetsDir(), "color-bars.png")
});
}
else {
let windowRef;
ipcMain.once(`screenshot-${screen.id}`, function(_e, message) {
const tempName = temp.path({dir: os.tmpdir(), suffix:".png"});
fs.writeFileSync(tempName, message.buffer);
resolve({
url: tempName
});
// close the screen grabber window
try {
windowRef.close();
}
catch(ex) {
if ( typeof(Sentry) !== "undefined" ) {
// eslint-disable-next-line no-undef
Sentry.captureException(ex);
}
}
// rewrite file paths to always have unix slashes instead
// of windows slashes. sometimes windows slashes are fine, but
// there's a few situations where they won't render properly.
// message.url = message.url.split(path.sep).join(path.posix.sep);
resolve(message);
});
openGrabberWindow().then((w) => {
windowRef = w;
windowRef.webContents.send("request-screenshot", {
id: screen.id,
width: s.bounds.width,
height: s.bounds.height});
});
}
});
};
/**
* open a simple window that our mocha/spectron tests can use.
*
* this exists mostly because it's basically impossible to test
* an app that doesn't open a window.
*/
var openTestShim = function() {
var testWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
...defaultWebPreferences,
preload: path.join(getAssetsDir(), "shim.js")
}
});
const shimUrl = `file://${getAssetsDir()}/shim.html`;
testWindow.loadURL(shimUrl);
// testWindow.webContents.openDevTools();
};
let screenshots = {};
/**
* Open the preferences window
* @returns {Promise} Promise that resolves when prefs window is shown
*/
var openPrefsWindow = function() {
if ( handles.prefs.window !== null && handles.prefs.window !== undefined ) {
return new Promise((resolve) => {
handles.prefs.window.show();
resolve();
});
}
return new Promise((resolve) => {
const primary = electronScreen.getPrimaryDisplay();
// take a screenshot of the main screen for use in previews
grabScreen(primary).then((grab) => {
screenshots[primary.id] = grab.url;
const prefsUrl = getUrl("prefs.html");
handles.prefs.window = new BrowserWindow({
show: false,
width: 910,
height: 700,
minWidth: 800,
maxWidth: 910,
minHeight: 600,
resizable: true,
webPreferences: {
...defaultWebPreferences,
preload: path.join(getAssetsDir(), "preload.mjs")
},
icon: path.join(getAssetsDir(), "iconTemplate.png")
});
if ( !isDev && handles.prefs.window.removeMenu !== undefined ) {
handles.prefs.window.removeMenu();
}
handles.prefs.window.on("closed", () => {
handles.prefs.window = null;
dock.hideDockIfInactive(app);
});
handles.prefs.window.once("ready-to-show", () => {
handles.prefs.window.show();
dock.showDock(app);
});
handles.prefs.window.once("show", resolve);
log.info("loading " + prefsUrl);
handles.prefs.window.loadURL(prefsUrl);
});
});
};
var openSettingsWindow = function() {
if ( handles.settings.window !== null && handles.settings.window !== undefined ) {
return new Promise((resolve) => {
handles.settings.window.show();
resolve();
});
}
var settingsUrl = getUrl("settings.html");
handles.settings.window = new BrowserWindow({
show: false,
width:600,
height:650,
maxWidth: 600,
minWidth: 600,
resizable: true,
parent: handles.prefs.window,
modal: true,
icon: path.join(getAssetsDir(), "iconTemplate.png"),
webPreferences: {
...defaultWebPreferences,
preload: path.join(getAssetsDir(), "preload.mjs"),
}
});
// hide the file menu
if ( !isDev && handles.settings.window.removeMenu !== undefined ) {
handles.settings.window.removeMenu();
}
handles.settings.window.on("closed", () => {
handles.settings.window = null;
dock.hideDockIfInactive(app);
});
handles.settings.window.once("ready-to-show", () => {
handles.settings.window.show();
dock.showDock(app);
});
log.info(`open ${settingsUrl}`);
handles.settings.window.loadURL(settingsUrl);
};
/**
* handle new screensaver event. open the window to create a screensaver
*/
var addNewSaver = async function(opts) {
var newUrl = getUrl("new.html");
var primary = electronScreen.getPrimaryDisplay();
// take a screenshot of the main screen for use in previews
if ( !opts.screenshot) {
const grab = await grabScreen(primary);
opts.screenshot = grab.url;
}
handles.addNew.window = new BrowserWindow({
show: false,
width: 450,
height: 700,
resizable:true,
webPreferences: {
...defaultWebPreferences,
preload: path.join(getAssetsDir(), "preload.mjs"),
},
icon: path.join(getAssetsDir(), "iconTemplate.png")
});
handles.addNew.window.on("closed", () => {
handles.addNew.window = null;
dock.hideDockIfInactive(app);
});
handles.addNew.window.once("ready-to-show", () => {
handles.addNew.window.show();
dock.showDock(app);
});
handles.addNew.window.loadURL(newUrl);
};
/**
* Open the About window for the app
*/
var openAboutWindow = function() {
var aboutUrl = getUrl("about.html");
handles.about.window = new BrowserWindow({
show: false,
width:500,
height:600,
resizable:false,
icon: path.join(getAssetsDir(), "iconTemplate.png"),
webPreferences: {
...defaultWebPreferences,
preload: path.join(getAssetsDir(), "preload.mjs"),
}
});
if ( !isDev && handles.about.window.removeMenu !== undefined ) {
handles.about.window.removeMenu();
}
handles.about.window.on("closed", () => {
handles.about.window = null;
dock.hideDockIfInactive(app);
});
handles.about.window.once("ready-to-show", () => {
handles.about.window.show();
dock.showDock(app);
});
log.info(`open ${aboutUrl}`);
handles.about.window.loadURL(aboutUrl);
};
/**
* open the editor tool for a screensaver
* @param {Object} args object with arguments
* @param {string} args.src path to the JSON file for the screensaver
* @param {string} args.screenshot path to the screenshot to use when editing
*/
var openEditor = (args) => {
var key = args.src;
var screenshot = args.screenshot;
var editorUrl = getUrl("editor.html");
var target = editorUrl + "?" +
"src=" + encodeURIComponent(key) +
"&screenshot=" + encodeURIComponent(screenshot);
if ( handles.editor.window == null ) {
handles.editor.window = new BrowserWindow({
show: false,
webPreferences: {
...defaultWebPreferences,
preload: path.join(getAssetsDir(), "preload.mjs"),
},
});
}
handles.editor.window.screenshot = screenshot;
handles.editor.window.once("ready-to-show", () => {
handles.editor.window.send("args", args);
handles.editor.window.show();
if (process.env.NODE_ENV === "test") {
handles.editor.window.webContents.closeDevTools();
}
dock.showDock(app);
});
handles.editor.window.on("closed", () => {
handles.editor.window = null;
if ( handles.editor.preview ) {
handles.editor.preview.destroy();
}
handles.editor.preview = null;
dock.hideDockIfInactive(app);
});
handles.editor.window.loadURL(target);
};
/**
* get the BrowserWindow options we'll use to launch
* on the given screen.
*/
var getWindowOpts = function(s) {
var opts = {
backgroundColor: "#000000",
alwaysOnTop: true,
x: s.bounds.x,
y: s.bounds.y,
show: false,
roundedCorners: false,
titleBarStyle: "customButtonsOnHover",
webPreferences: {
...defaultWebPreferences
}
};
// osx will display window immediately if fullscreen is true
// so we default it to false there
if (process.platform !== "darwin" ) {
opts.fullscreen = true;
}
if ( testMode === true ) {
opts.fullscreen = false;
// opts.x = 100;
// opts.y = 100;
opts.show = true;
opts.width = 400;
opts.height = 400;
}
return opts;
};
var applyScreensaverWindowEvents = function(w) {
// Emitted when the window is closed.
w.once("closed", function() {
if (process.platform !== "win32" ) {
cursor.show();
}
windows.forceWindowClose(w);
});
// inject our custom CSS into the screensaver window
w.webContents.on("did-finish-load", function() {
log.info("did-finish-load");
if (!w.isDestroyed()) {
// load some global CSS we'll inject into running screensavers
const globalCSSCode = fs.readFileSync( path.join(getAssetsDir(), "global.css"), "ascii");
w.webContents.insertCSS(globalCSSCode);
}
});
// we could do something nice with either of these events
w.webContents.on("render-process-gone", log.info);
w.webContents.on("unresponsive", log.info);
};
/**
*
* @param {String} screenshot URL of screenshot
* @param {Saver} saver the screensaver to run
* @param {Screen} s the screen to run it on
* @param {Object} url_opts any options to pass on the url
* @param {number} tickCount hrtime value of when we started
*/
var runSaver = function(screenshot, saver, s, url_opts, tickCount) {
const windowOpts = getWindowOpts(s);
var w = new BrowserWindow(windowOpts);
w.isSaver = true;
if ( w.removeMenu !== undefined ) {
w.removeMenu();
}
let diff = process.hrtime(tickCount);
log.info(`run screensaver ${saver.name} on screen ${s.id} ${saver.url} ts: ${diff[0] * 1e9 + diff[1]}`);
return new Promise((resolve, reject) => {
try {
applyScreensaverWindowEvents(w);
w.webContents.once("did-fail-load", (_event, _code, description) => {
log.info(`did-fail-load: ${description}`);
windows.forceWindowClose(w);
reject(s.id, description);
});
w.once("ready-to-show", () => {
log.info("ready-to-show", s.id);
if ( testMode !== true ) {
windows.setFullScreen(w);
}
if (process.platform === "win32" ) {
log.info("force focus");
forceFocus.focusWindow(w);
}
diff = process.hrtime(tickCount);
log.info(`rendered in ${diff[0] * 1e9 + diff[1]} nanoseconds`);
resolve(s.id);
});
if ( typeof(screenshot) !== "undefined" ) {
log.info(`pass screenshot ${screenshot}`);
url_opts.screenshot = encodeURIComponent("file://" + screenshot);
}
// w.webContents.openDevTools();
// generate screensaver object, then get url to load
const saverObj = new Saver(saver);
const url = saverObj.urlWithParams(url_opts);
log.info("Loading " + url, s.id);
// and load the index.html of the app.
w.loadURL(url);
}
catch (e) {
log.info(e);
windows.forceWindowClose(w);
reject(s.id, e);
}
});
};
/**
* run the specified screensaver on the specified screen
*/
var runScreenSaverOnDisplay = function(saver, s) {
var size = s.bounds;
var url_opts = {
width: size.width,
height: size.height,
platform: process.platform
};
log.info("runScreenSaverOnDisplay", s.id);
// don't do anything if we don't actually have a screensaver
if ( typeof(saver) === "undefined" || saver === null ) {
log.info("no saver, exiting");
return Promise.resolve();
}
let tickCount = process.hrtime();
//
// if this screensaver uses a screengrab, get it.
// otherwise just boot it
//
const reqs = saver.requirements;
if ( reqs !== undefined && reqs.findIndex && reqs.findIndex((x) => { return x === "screen"; }) > -1 ) {
return grabScreen(s).then((message) => {
runSaver(message.url, saver, s, url_opts, tickCount);
});
}
else {
return runSaver(undefined, saver, s, url_opts, tickCount);
}
};
/**
* blank out the given screen
*/
var blankScreen = async function(s) {
if ( process.env.TEST_MODE ) {
log.info("refusing to blank screen in test mode");
return s.id;
}
const systemPath = getSystemDir();
const blankUrl = `file://${path.join(systemPath, "system-savers", "blank", "index.html")}`;
const saver = {
name: "Blank",
url: blankUrl
};
return runScreenSaverOnDisplay(saver, s);
};
/**
* get a list of displays connected to the computer.
*/
var getDisplays = function() {
var displays = [];
if ( debugMode === true || prefs.runOnSingleDisplay === true ) {
displays = [
electronScreen.getPrimaryDisplay()
];
}
else {
displays = electronScreen.getAllDisplays();
}
return displays;
};
/**
* get a list of the non primary displays connected to the computer
*/
var getNonPrimaryDisplays = function() {
var primary = electronScreen.getPrimaryDisplay();
return electronScreen.getAllDisplays().filter((d) => {
return d.id !== primary.id;
});
};
/**
* manually trigger screensaver by setting state to run
*/
var setStateToRunning = function() {
log.info("setStateToRunning");
// disable power state check
checkPowerState = false;
stateManager.run();
};
var setStateToPaused = function() {
log.info("setStateToPaused");
stateManager.pause();
stateManager.stopTicking();
};
var resetState = function() {
stateManager.reset();
};
/**
* return a promise the resolves to the path to the screensaver and its options
*/
var findScreensaver = function() {
const workingPath = getSystemDir();
// check if the user is running the random screensaver. if so, pick one!
const randomPath = path.join(workingPath, "system-savers", "random", "saver.json");
// log.info("random: " + randomPath);
if ( prefs.saver === randomPath ) {
return new Promise((resolve) => {
savers.list(() => {
// @todo s can be undefined
// https://sentry.io/organizations/colin-mitchell/issues/955633850/?project=172824&query=is%3Aunresolved&statsPeriod=14d&utc=false
let s = savers.random();
resolve(s.key, prefs.getOptions(s.key));
});
});
}
return Promise.resolve(prefs.saver, prefs.getOptions(prefs.saver));
};
/**
* run the user's chosen screensaver on any available screens
*/
var runScreenSaver = function() {
log.info("runScreenSaver");
const setupPromise = findScreensaver();
setupPromise.
then((saverKey, settings) => savers.loadFromFile(saverKey, settings)).
catch((err) => {
log.info("================ loading saver failed?");
log.info(err.message);
return undefined;
}).
then((saver) => {
let displays = [];
let blanks = [];
// make sure we have something to display
if ( typeof(saver) === "undefined" ) {
log.info("No screensaver defined! Just blank everything");
blanks = getDisplays().concat(getNonPrimaryDisplays());
}
else if ( testMode === true ) {
blanks = [];
} else {
displays = getDisplays();
if ( debugMode !== true && testMode !== true && prefs.runOnSingleDisplay === true ) {
blanks = getNonPrimaryDisplays();
}
}
// turn off idle checks for a couple seconds while loading savers
stateManager.ignoreReset(true);
cursor.hide();
//
// generate an array of promises for rendering screensavers on any screens
//
const promises = displays
.map((d) => runScreenSaverOnDisplay(saver, d))
.concat(
blanks.map((d) => blankScreen(d))
);
Promise.allSettled(promises).then((values) => {
log.info("final result", values);
setRunningInABit();
}).catch((e) => {
log.info("running screensaver failed");
log.info(e);
stateManager.reset();
cursor.show();
});
});
};
/**
* After a short delay, set state manager to running. This should
* help with mouse wiggle/etc
*/
var setRunningInABit = function() {
setTimeout(function() {
log.info("our work is done, set state to running");
stateManager.running();
}, 1500);
};
/**
* should we lock the user's screen when returning from running the saver?
*/
var shouldLockScreen = function() {
// we can't lock the screen on OSX because it would involve using
// private APIs and is a super pain in the butt
return ( prefs.lock === true );
};
/**
* stop the running screensaver
*/
var stopScreenSaver = function(fromBlank) {
log.info("received stopScreenSaver call");
if ( fromBlank !== true ) {
stateManager.reset();
}
// trigger lock screen before actually closing anything
else if ( shouldLockScreen() && screenLock.doLockScreen ) {
log.info("lock the screen");
screenLock.doLockScreen();
}
windows.closeRunningScreensavers();
cursor.show();
};
/**
* determine what our system directory is. this should basically be
* where the app exists, and where the system-savers directory and
* other critical files exist.
*/
var getSystemDir = function() {
if ( process.env.BEFORE_DAWN_SYSTEM_DIR !== undefined ) {
return process.env.BEFORE_DAWN_SYSTEM_DIR;
}
if ( process.env.TEST_MODE ) {
return app.getAppPath();
}
if ( app.isPackaged ) {
return path.join(app.getAppPath(), "output");
}
return path.join(app.getAppPath(), "..", "..", "output");
};
/**
* determine what our assets directory is. This is where global CSS,
* icons, etc, can be found.
*/
let getAssetsDir = function() {
if ( process.env.BEFORE_DAWN_ASSETS_DIR !== undefined ) {
return process.env.BEFORE_DAWN_ASSETS_DIR;
}
if ( app.isPackaged ) {
return path.join(app.getAppPath(), "output", "assets");
}
if ( process.env.TEST_MODE ) {
return path.join(app.getAppPath(), "assets");
}
return path.join(app.getAppPath(), "assets");
};
/**
* return the URL prefix we should use when loading app windows. if
* running in development mode with hot reload enabled, we'll use an
* HTTP request, otherwise we'll use a file:// url.
*/
var getUrl = function(dest) {
let baseUrl;
if ( !testMode && isDev ) {
let devPort;
try {
devPort = packageJSON.devport;
}
catch {
devPort = 9080;
}
baseUrl = `http://localhost:${devPort}`;
return new URL(dest, new URL(baseUrl)).toString();
}
log.info(`hey!!! ${app.getAppPath()}`);
if ( testMode ) {
return `file://${app.getAppPath()}/${dest}`;
}
return `file://${app.getAppPath()}/output/${dest}`;
};
var setupForTesting = function() {
if ( testMode === true ) {
log.info("opening shim for test mode");
openTestShim();
}
};
/**
* build and apply an application menu and tray menu
*/
var setupMenuAndTray = function() {
var menu = Menu.buildFromTemplate(menusAndTrays.buildMenuTemplate(app));
Menu.setApplicationMenu(menu);
//
// build the tray menu
//
trayMenu = Menu.buildFromTemplate(menusAndTrays.trayMenuTemplate());
trayMenu.items[3].visible = global.NEW_RELEASE_AVAILABLE;
const iconImage = menusAndTrays.trayIconImage();
appIcon = new Tray(iconImage);
appIcon.setToolTip(global.APP_NAME);
appIcon.setContextMenu(trayMenu);
// show tray menu on right click
// @todo should this be osx only?
appIcon.on("right-click", () => {
appIcon.popUpContextMenu();
});
appIcon.on("click", () => {
appIcon.popUpContextMenu();
});
};
/**
* setup any requirements for the app
*
* @returns {Promise} Promise that resolves with true if setup for first time, false if app was ready
*/
var setupIfNeeded = async function() {
log.info("setupIfNeeded");
if ( process.env.QUIET_MODE === "true" || process.env.NODE_ENV === "test" ) {
log.info("Quiet/test mode, skip setup checks!");
return false;
}
// check if we should download savers, set something up, etc
if ( process.env.FORCE_SETUP || prefs.needSetup ) {
// stop processing here, we know we need to setup
log.info("needSetup!");
return true;
}
// log.info(`checking if ${prefs.saver} is valid`);
const exists = await savers.confirmExists(prefs.saver);
if ( ! exists ) {
log.info("need to pick a new screensaver");
}
else {
log.info("looks like we are good to go");
}
return !exists;
};
/**
* open the preferences window if needed
*
* @param {Boolean} status true if we need to open the prefs window
*/
var openPrefsWindowIfNeeded = function(status) {
log.info("openPrefsWindowIfNeeded");
if ( status === true ) {
log.info("we do need to open prefs window");
return openPrefsWindow();
}
return Promise.resolve();
};
/**
* setup our periodic release check
*/
var setupReleaseCheck = function() {
if ( ! global.RELEASE_CHECK_URL ) {
log.info("no release server set, so no release checks");
return;
}
releaseChecker = new ReleaseCheck();
releaseChecker.setFeed(global.RELEASE_CHECK_URL);
releaseChecker.setLogger(log.info);
releaseChecker.onUpdate(() => {
global.NEW_RELEASE_AVAILABLE = true;
log.info("update available, show it");
getTrayMenu().items[3].visible = global.NEW_RELEASE_AVAILABLE;
});
releaseChecker.onNoUpdate(() => {
global.NEW_RELEASE_AVAILABLE = false;
log.info("no update available, hide it");
getTrayMenu().items[3].visible = global.NEW_RELEASE_AVAILABLE;
});
log.info("Run initial release check");
checkForNewRelease();
// check for a new release every 12 hours
log.info("Setup release check");
setInterval(checkForNewRelease, RELEASE_CHECK_INTERVAL);
};
/**
* Check if we should move the app to the actual application folder.
* This is important because the app is pretty fragile on OSX otherwise.
*/
var askAboutApplicationsFolder = function() {
if ( testMode === true || isDev === true || app.isInApplicationsFolder === undefined ) {
return;
}
if ( !app.isInApplicationsFolder() ) {
const chosen = dialog.showMessageBoxSync({
type: "question",
buttons: ["Move to Applications", "Do Not Move"],
message: "Move to Applications folder?",
detail: "Hello! I work better in your Applications folder, should I move myself there?"
});
if ( chosen === 0 ) {
app.moveToApplicationsFolder();
}
}
};
/**
* check for permissions to access certain systems on OSX
*/
var askAboutMediaAccess = async function() {
if (process.platform !== "darwin" || testMode === true ) {
return;
}
["microphone", "camera", "screen"].forEach(async (type) => {
log.info(type);
// note: this might be handy
// "mac-screen-capture-permissions": "^1.1.0",
// if ( type === "screen" ) {
// const {
// hasScreenCapturePermission,
// hasPromptedForPermission
// } = require('mac-screen-capture-permissions');
// const result = hasPromptedForPermission();
// const result2 = hasScreenCapturePermission();
// }
// https://www.electronjs.org/docs/api/system-preferences#systempreferencesaskformediaaccessmediatype-macos
log.info(`access to ${type}: ${systemPreferences.getMediaAccessStatus(type)}`);
// re: screen -- This permission can only be granted manually in the System
// Preferences. Therefore systemPreferences.askForMediaAccess() cannot be
// extended in the same way.
if ( systemPreferences.getMediaAccessStatus(type) !== "granted" && type !== "screen" ) {
await systemPreferences.askForMediaAccess(type);
}
});
};
const getPackage = function() {
const attrs = {
repo: prefs.sourceRepo,
dest: prefs.defaultSaversDir,
log: log.info,
fetch: net.fetch
};
console.log(attrs);
return new Package(attrs);
};
/**
* setup assorted IPC listeners
*/
let setupIPC = function() {
/**
* open the window specified by 'key', passing args along
*/
ipcMain.on("open-window", (_event, key, args) => {
windowMethods[key](args);
});
/**
* set screensaver state to paused
*/
ipcMain.on("pause", () => {
setStateToPaused();
});
/**
* set screensaver state to enabled
*/
ipcMain.on("enable", () => {
resetState();
});
/**
* close the window specified by 'key'
*/
ipcMain.on("close-window", (event, key) => {
if ( handles[key].window ) {
handles[key].window.close();
}
});
ipcMain.on("close-all-windows", () => {
console.log("close-all-windows");
Object.keys(handles).forEach(function(key) {
if ( handles[key].window ) {
console.log("close $key");
handles[key].window.close();
}
});
});
/**
* return prefs data to requester
*/
ipcMain.handle("get-prefs", () => {
log.info("get-prefs");
return prefs.data;
});
/**
* return a couple of global variables
*/
ipcMain.handle("get-globals", () => {
return {
APP_VERSION: global.APP_VERSION,
APP_REPO: global.APP_REPO,
NEW_RELEASE_AVAILABLE: global.NEW_RELEASE_AVAILABLE
};
});
/**
* return a list of screensavers
*/
ipcMain.handle("list-savers", async () => {
const entries = await savers.list();
return entries;
});
/**
* load and return the specified screensaver
*/
ipcMain.handle("load-saver", async (_event, key) => {
return await savers.loadFromFile(key);
});
/**
* delete the specified screensaver
*/
ipcMain.handle("delete-saver", async(_event, attrs) => {
log.info("delete-saver", attrs);
await savers.delete(attrs);
savers.reset();
prefs.reload();
});
/**
* update prefs with the incoming attrs
*/
ipcMain.handle("update-prefs", async(_event, attrs) => {
log.info("update-prefs", attrs);
// ensure a value for this
attrs.firstLoad = false;
if ( attrs.launchShortcut === undefined ) {
attrs.launchShortcut = "";
}
prefs.store.set(attrs);
savers.reset();
updateStateManager();
});
ipcMain.handle("check-screensaver-package", async() => {
log.info("check-screensaver-package");
return getPackage().getReleaseInfo();
});
ipcMain.handle("download-screensaver-package", async() => {
log.info("download-screensaver-package");
const result = await getPackage().downloadRelease();
log.info(result);
toggleSaversUpdated();
return result;
});
/**
* return the default settings for the app
*/
ipcMain.handle("get-defaults", async() => {
log.info("get-defaults");
log.info(prefs.defaults);
return prefs.defaults;
});
/**
* update the local source settings
*/
ipcMain.handle("update-local-source", async(_event, ls) => {
log.info("update-local-source", ls);
prefs.store.set("localSource", ls);
savers.reset();
});
/**
* create a new screensaver from our template
*/
ipcMain.handle("create-screensaver", async(_event, attrs) => {
const factory = new SaverFactory();
const src = path.join(getSystemDir(), "system-savers", "__template");
log.info(`create-screensaver from ${src}`);
const dest = prefs.localSource;
const data = factory.create(src, dest, attrs);
savers.reset();
return data;
});
/**
* save/update a screensaver object
*/
ipcMain.handle("save-screensaver", async(_event, attrs, dest) => {
const s = new Saver(attrs);
s.write(attrs, dest);
});
/**
* return the bounds of the primary screen to the requester
*/
ipcMain.handle("get-primary-display-bounds", () => {
return electronScreen.getPrimaryDisplay().bounds;
});
/**
* return a screengrab of the primary screen to the requester
*/
ipcMain.handle("get-primary-screenshot", () => {
return screenshots[electronScreen.getPrimaryDisplay().id];
});
/**
* load the requested URL in a browser
*/
ipcMain.on("launch-url", (_event, url) => {
shell.openExternal(url);
});
/**
* handle savers-updated event. this is sent when a screensaver is created/updated
*/
ipcMain.on("savers-updated", () => {
log.info("savers-updated");
toggleSaversUpdated();
});
/**
* set autostart value
*/
ipcMain.on("set-autostart", (_event, value) => {
log.info("set-autostart");
if ( process.env.TEST_MODE !== undefined ) {
log.info("we're in test mode, skipping autostart");
return;
}
autostarter.toggle(global.APP_NAME, value);
});
/**
* handle event to set global launch shortcut
*/
ipcMain.on("set-global-launch-shortcut", () => {
log.info("set-global-launch-shortcut");
setupLaunchShortcut();
});
/**
* run the users specified screensaver
*/
ipcMain.on("run-screensaver", () => {
log.info("run-screensaver");
setStateToRunning();
});
ipcMain.on("toggle-dev-tools", () => {
log.info("toggle-dev-tools");
if ( handles.editor.window !== null ) {
handles.editor.window.webContents.openDevTools();
}
});
ipcMain.on("console-log", (_event, payload) => {
log.info(payload);
});
/**
* open a folder
*/
ipcMain.on("open-folder", (_event, src) => {
var cmd;
var args = [];
// figure out the path to the screensaver folder. use
// decodeURIComponent to convert %20 to spaces
const filePath = path.dirname(decodeURIComponent(url.parse(src).path)); //.split(path.posix.sep).join(path.sep);
switch(process.platform) {
case "darwin":
cmd = "open";
args = [ filePath ];
break;
case "win32":
if (process.env.SystemRoot) {
cmd = path.join(process.env.SystemRoot, "explorer.exe");
}
else {
cmd = "explorer.exe";
}
args = [`${filePath}`];
break;
default:
// # Strip the filename from the path to make sure we pass a directory
// # path. If we pass xdg-open a file path, it will open that file in the
// # most suitable application instead, which is not what we want.
cmd = "xdg-open";
args = [ filePath ];
}
exec(cmd, args, function() {});
});
ipcMain.on("watch-folder", (event, src) => {
const webContents = event.sender;
const win = BrowserWindow.fromWebContents(webContents);
const folderPath = path.dirname(src);
// make sure folder actually exists
if ( fs.existsSync(folderPath) ) {
win.fsWatcher = fs.watch(folderPath, (eventType, filename) => {
if (filename && win?.webContents) {
win.webContents.send("folder-update", filename);
}
});
}
});
ipcMain.on("unwatch-folder", (event) => {
const webContents = event.sender;
const win = BrowserWindow.fromWebContents(webContents);
win.fsWatcher.close();
});
/**
* display a dialog about a package update
*/
if ( process.env.TEST_MODE === undefined ) {
ipcMain.on("display-update-dialog", async () => {
const result = await dialog.showMessageBox({
type: "info",
title: "Update Available!",
message: "There's a new update available! Would you like to download it?",
buttons: ["No", "Yes"],
defaultId: 0
});
if ( result.response === 1 ) {
const appRepo = global.APP_REPO;
shell.openExternal(`https://github.com/${appRepo}/releases/latest`);
}
});
}
/**
* display a dialog when the user wants to reset to default settings
*/
ipcMain.handle("reset-to-defaults-dialog", async () => {
const result = await dialog.showMessageBox({
type: "info",
title: "Are you sure?",
message: "Are you sure you want to reset to the default settings?",
buttons: ["No", "Yes"],
defaultId: 0
});
return result.response;
});
/**
* display a confirmation dialog for deleting a screensaver
*/
ipcMain.handle("delete-screensaver-dialog", async (_event, saver) => {
const result = await dialog.showMessageBox(
{
type: "info",
title: "Are you sure?",
message: "Are you sure you want to delete this screensaver?",
detail: `Deleting screensaver ${saver.name}`,
buttons: ["No", "Yes"],
defaultId: 0
});
return result.response;
});
/**
* display a folder chooser for setting local source
*/
ipcMain.handle("show-open-dialog", async () => {
const result = await dialog.showOpenDialog(
{
title: "Pick a screensaver directory",
message: "Pick a folder to store your custom screensavers",
properties: [ "openDirectory", "createDirectory" ]
});
return result;
});
//
// setup a couple of IPC methods we only use in tests
//
if ( testMode === true ) {
/**
* handle requests to get the current state of the app. this
* is currently only called by our test shim
*/
ipcMain.handle("get-current-state", async () => {
return stateManager.currentStateString;
});
/**
* get a list of tray item labels
*/
ipcMain.handle("get-tray-items", async () => {
return menusAndTrays.trayMenuTemplate().map(item => item.label);
});
/**
* fake a click on a tray item
*/
ipcMain.handle("click-tray-item", (_event, label) => {
log.info(`click-tray-item ${label}`);
const items = menusAndTrays.trayMenuTemplate();
const item = items.find(item => item.label === label);
item.click();
});
}
/**
* handle quit app events
*/
ipcMain.once("quit-app", () => {
log.info("quit-app");
quitApp();
});
};
/**
* handle initial startup of app
*/
var bootApp = async function() {
log.info("bootApp");
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({responseHeaders: Object.fromEntries(Object.entries(details.responseHeaders).filter(header => !/x-frame-options/i.test(header[0])))});
});
askAboutApplicationsFolder();
await askAboutMediaAccess();
global.NEW_RELEASE_AVAILABLE = false;
// ensure proper data in about panel when available
if ( app.setAboutPanelOptions ) {
app.setAboutPanelOptions({
applicationName: global.APP_NAME,
applicationVersion: global.APP_VERSION,
version: global.APP_VERSION_BASE,
credits: global.APP_CREDITS
});
}
let saversDir;
if ( process.env.SAVERS_DIR ) {
saversDir = process.env.SAVERS_DIR;
}
else if ( isDev ) {
saversDir = path.join(app.getAppPath(), "..", "..", "data", "savers");
log.info("hello from dev mode, 'node bin/download-screensavers' to grab screensavers");
}
else {
saversDir = path.join(process.resourcesPath, "savers");
}
const systemDir = getSystemDir();
let basePath;
// store our root path as a global variable so we can access it from screens
if ( process.env.BEFORE_DAWN_DIR !== undefined ) {
basePath = process.env.BEFORE_DAWN_DIR;
}
else {
basePath = app.getPath("userData");
}
log.info("use base path", basePath);
log.info("Loading prefs");
log.info(`baseDir: ${basePath}`);
log.info(`saversDir: ${saversDir}`);
log.info(`system savers: ${systemDir}/system-savers`);
prefs = new SaverPrefs(basePath, systemDir, saversDir);
savers = new SaverListManager({
prefs: prefs
});
await listScreens();
//
// setup some event handlers for when screen count changes, mostly
// to ensure that we wake up if the user plugs in or removes a
// monitor
//
["display-added", "display-removed"].forEach((type) => {
electronScreen.on(type, async () => {
log.info(type);
await listScreens();
windows.handleDisplayChange();
});
});
["suspend", "lock-screen"].forEach((type) => {
powerMonitor.on(type, (ev) => {
if ( stateManager.isTicking() ) {
log.info(`system ${type} event, stop screensavers`);
ev.preventDefault();
stateManager.stopTicking();
windows.closeRunningScreensavers();
}
});
});
if ( testMode !== true ) {
setInterval(() => {
if ( stateManager.isTicking() ) {
return;
}
const delayTime = prefs.delay > 0 ? prefs.delay * 60 : Number.POSITIVE_INFINITY;
const idleState = powerMonitor.getSystemIdleState(delayTime);
// don't restart state manager if we're paused
if ( ! stateManager.isTicking() && !stateManager.paused() && idleState === "active" ) {
log.info("looks like we are awake again lets go");
stateManager.reset();
stateManager.startTicking();
}
}, 10000);
}
powerMonitor.on("on-ac", () => {
log.info("system on-ac event, reset state manager");
stateManager.reset();
});
setupIPC();
stateManager = new StateManager();
stateManager.idleFn = powerMonitor.getSystemIdleTime;
updateStateManager();
let result = await setupIfNeeded();
await openPrefsWindowIfNeeded(result);
setupForTesting();
setupMenuAndTray();
setupReleaseCheck();
setupLaunchShortcut();
// don't show app in dock
dock.hideDockIfInactive(app);
// start the idle check
stateManager.startTicking();
};
/**
* toggle our 'ok to quit' variable and quit
*/
var quitApp = () => {
exitOnQuit = true;
app.quit();
};
/**
* run the screensaver, but only if there isn't an app in fullscreen mode right now
*/
var runScreenSaverIfNotFullscreen = function() {
log.info("runScreenSaverIfNotFullscreen");
if ( ! isFullscreen() ) {
log.info("I don't think we're in fullscreen mode");
runScreenSaver();
}
else {
log.info("looks like we are in fullscreen mode");
}
};
/**
* activate the screensaver, but only if we're plugged in, or if the user
* is fine with running on battery
*/
var runScreenSaverIfPowered = async function() {
log.info("runScreenSaverIfPowered");
if ( windows.screenSaverIsRunning() ) {
log.info("looks like we're already running");
return;
}
// check if we are on battery, and if we should be running in that case
if ( checkPowerState && prefs.disableOnBattery ) {
const isPowered = await power.charging();
if ( isPowered ) {
runScreenSaverIfNotFullscreen();
}
else {
log.info("I would run, but we're on battery :(");
stateManager.unrunnable();
}
}
else {
checkPowerState = true;
runScreenSaverIfNotFullscreen();
}
};
/**
* if the screensaver is running, blank the screen. otherwise,
* reset state machine
*/
var blankScreenIfNeeded = function() {
log.info("blankScreenIfNeeded");
if ( windows.screenSaverIsRunning() ) {
log.info("running, close windows");
stopScreenSaver(true);
screenLock.doSleep();
}
};
/**
* update the state manager with our
* timeout values, etc
*/
var updateStateManager = function() {
const delayTime = prefs.delay > 0 ? prefs.delay * 60 : Number.POSITIVE_INFINITY;
const blankOffset = process.platform === "win32" ? 0 : prefs.delay;
const blankTime = prefs.sleep > 0 ? (blankOffset + prefs.sleep) * 60 : Number.POSITIVE_INFINITY;
log.info(`updateStateManager idleTime: ${delayTime} blankTime: ${blankTime}`);
stateManager.setup({
idleTime: delayTime,
blankTime: blankTime,
onIdleTime: runScreenSaverIfPowered,
onBlankTime: blankScreenIfNeeded,
onReset: windows.closeRunningScreensavers,
logger: log.info
});
};
/**
* check for a new release of the app
*/
var checkForNewRelease = function() {
log.info("checkForNewRelease");
releaseChecker.checkLatestRelease();
};
/**
* setup a global shortcut to run a screensaver
*/
var setupLaunchShortcut = function() {
globalShortcut.unregisterAll();
if ( prefs.launchShortcut !== undefined && prefs.launchShortcut !== "" ) {
log.info(`register launch shortcut: ${prefs.launchShortcut}`);
try {
const ret = globalShortcut.register(prefs.launchShortcut, () => {
log.info("shortcut triggered!");
if ( handles.prefs.window && handles.prefs.window.isFocused() ) {
log.info("no shortcut when prefs active!");
return;
}
try {
// turn off idle checks for a couple seconds while loading savers
stateManager.ignoreReset(true);
setStateToRunning();
}
catch (e) {
log.info(e);
stateManager.ignoreReset(false);
}
finally {
setTimeout(function() {
stateManager.ignoreReset(false);
}, 2500);
}
});
if ( ! ret ) {
log.info("shortcut registration failed");
}
log.info(`registered? ${globalShortcut.isRegistered(prefs.launchShortcut)}`);
}
catch(e) {
log.info("shortcut registration threw an error?");
log.info(e);
}
}
};
/**
* return our state manager
* @returns {StateManager}
*/
let getStateManager = function() {
return stateManager;
};
/**
* return the app icon
* @returns {Tray}
*/
let getAppIcon = function() {
return appIcon;
};
/**
* return the tray menu
* @returns {Menu}
*/
let getTrayMenu = function() {
return trayMenu;
};
let updateTrayMenu = function() {
appIcon.setContextMenu(trayMenu);
}
/**
* if the user has updated one of their screensavers, we can let
* the prefs window know that it needs to reload
*/
let toggleSaversUpdated = (arg) => {
prefs.reload();
savers.reset();
if ( handles.prefs.window !== null ) {
handles.prefs.window.send("savers-updated", arg);
}
};
const windowMethods = {
editor: openEditor,
settings: openSettingsWindow,
prefs: openPrefsWindow,
about: openAboutWindow,
"add-new": addNewSaver
};
log.transports.file.level = "debug";
log.transports.file.maxSize = 1 * 1024 * 1024;
if (process.env.LOG_FILE) {
log.transports.file.resolvePathFn = () => process.env.LOG_FILE;
}
log.info(`Hello from version: ${global.APP_VERSION_BASE} running in ${isDev ? "development" : "production"}`);
if ( isDev ) {
app.name = global.APP_NAME;
log.info(`set app name to ${app.name}`);
if ( testMode !== true ) {
let userDataPath = path.join(app.getPath("appData"), app.name);
log.info(`set userData path to ${userDataPath}`);
app.setPath("userData", userDataPath);
}
}
/**
* make sure we're only running a single instance
*/
if ( testMode !== true ) {
app.on("second-instance", () => {
try {
if ( handles.prefs.window === null && handles.prefs.window !== undefined ) {
openPrefsWindow();
}
else {
if ( handles.prefs.window.isMinimized() ) {
handles.prefs.window.restore();
}
handles.prefs.window.focus();
}
}
catch(e) {
console.log(e);
}
});
}
// seems like we need to catch this event to keep OSX from exiting app after screensaver runs?
app.on("window-all-closed", function() {
log.info("window-all-closed");
});
app.on("before-quit", function() {
log.info("before-quit");
});
app.on("will-quit", function(e) {
log.info("will-quit");
if ( testMode !== true && isDev !== true && exitOnQuit !== true ) {
log.info(`don't quit! testMode: ${testMode} IS_DEV ${isDev} exitOnQuit ${exitOnQuit}`);
e.preventDefault();
}
else {
globalShortcut.unregisterAll();
}
});
app.once("quit", function() {
log.info("quit");
});
process.on("uncaughtException", function (ex) {
log.info(ex);
log.info(ex.stack);
});
log.info("readyto wait for bootApp");
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
app.whenReady().then(bootApp);
if ( testMode === true ) {
exports.getTrayMenuItems = function() {
return menusAndTrays.trayMenuTemplate();
};
}
export {
log,
setStateToRunning,
setStateToPaused,
resetState,
getAssetsDir,
getStateManager,
getAppIcon,
getTrayMenu,
updateTrayMenu,
openPrefsWindow,
openAboutWindow,
addNewSaver,
openEditor,
toggleSaversUpdated,
quitApp,
};
================================================
FILE: src/main/menus.js
================================================
"use strict";
import * as main from "./index.js";
import * as path from "path";
import {
nativeImage,
nativeTheme,
shell
} from "electron";
var openUrl = (url) => {
try {
shell.openExternal(url);
}
catch(e) {
main.log.info(e);
}
};
/**
* open the help section in a browser
*/
var openHelpUrl = () => { openUrl(global.HELP_URL); };
/**
* open the github issues url in a browser
*/
var openIssuesUrl = () => { openUrl(global.ISSUES_URL); };
/**
* open the website for the app
*/
var openHomepage = () => { openUrl("https://github.com/muffinista/before-dawn"); };
/**
* Build the menubar for the app
*
* @param {Application} a the main app instance
*/
export const buildMenuTemplate = function(a) {
var app = a;
var base = [
{
label: "File",
submenu: [
{
label: "Add New Screensaver",
accelerator: "CmdOrCtrl+N",
click: function() {
main.addNewSaver();
}
},
]
},
{
label: "Edit",
submenu: [
{
label: "Undo",
accelerator: "CmdOrCtrl+Z",
role: "undo"
},
{
label: "Redo",
accelerator: "Shift+CmdOrCtrl+Z",
role: "redo"
},
{
type: "separator"
},
{
label: "Cut",
accelerator: "CmdOrCtrl+X",
role: "cut"
},
{
label: "Copy",
accelerator: "CmdOrCtrl+C",
role: "copy"
},
{
label: "Paste",
accelerator: "CmdOrCtrl+V",
role: "paste"
},
{
label: "Select All",
accelerator: "CmdOrCtrl+A",
role: "selectall"
}
]
},
{
label: "View",
submenu: [
{
label: "Reload",
accelerator: "CmdOrCtrl+R",
click: function(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.reload();
}
}
},
{
label: "Toggle Developer Tools",
accelerator: (function() {
if (process.platform == "darwin") {
return "Alt+Command+I";
}
else {
return "Ctrl+Shift+I";
}
})(),
click: function(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.toggleDevTools();
}
}
}
]
},
{
label: "Window",
role: "window",
submenu: [
{
label: "Minimize",
accelerator: "CmdOrCtrl+M",
role: "minimize"
},
{
label: "Close",
accelerator: "CmdOrCtrl+W",
role: "close"
}
]
},
{
label: "Help",
role: "help",
submenu: [
{
label: "Learn More",
click: openHomepage
},
{
label: "Help",
click: openHelpUrl
}
]
}
];
if (process.platform == "darwin") {
var name = app.name;
base.unshift({
label: name,
submenu: [
{
label: "About " + name,
role: "about"
},
{
type: "separator"
},
{
label: "Services",
role: "services",
submenu: []
},
{
type: "separator"
},
{
label: "Hide " + name,
accelerator: "Command+H",
role: "hide"
},
{
label: "Hide Others",
accelerator: "Command+Alt+H",
role: "hideothers"
},
{
label: "Show All",
role: "unhide"
},
{
type: "separator"
},
{
label: "Quit",
accelerator: "Command+Q",
click: main.quitApp
}
]
});
}
return base;
};
/**
* build the tray menu template for the app
*/
export const trayMenuTemplate = function() {
return [
{
label: "Run Now",
click: function() {
setTimeout(main.setStateToRunning, 1000);
}
},
{
label: "Disable",
click: function() {
main.setStateToPaused();
updateTrayIcon();
main.getTrayMenu().items[1].visible = false;
main.getTrayMenu().items[2].visible = true;
main.updateTrayMenu();
}
},
{
label: "Enable",
click: function() {
main.resetState();
updateTrayIcon();
main.getTrayMenu().items[1].visible = true;
main.getTrayMenu().items[2].visible = false;
main.updateTrayMenu();
},
visible: false
},
{
label: "Update Available!",
click: function() {
shell.openExternal(global.PACKAGE_DOWNLOAD_URL);
},
visible: (global.NEW_RELEASE_AVAILABLE === true)
},
{
label: "Preferences",
click: () => {
main.openPrefsWindow();
}
},
{
label: "About " + global.APP_NAME,
click: () => {
main.openAboutWindow();
}
},
{
label: "Help",
click: () => {
openHelpUrl();
}
},
{
label: "Report a Bug",
click: () => {
openIssuesUrl();
}
},
{
label: "Quit",
click: () => {
main.quitApp();
}
}
];
};
/**
* get icons for the current platform
*/
export const getIcons = function() {
const useDarkIcon = !nativeTheme.shouldUseDarkColorsForSystemIntegratedUI;
const modifier = useDarkIcon ? '-dark' : '';
const icons = {
"win32" : {
active: path.join(main.getAssetsDir() , "icon.ico"),
paused: path.join(main.getAssetsDir(), "icon-paused.ico")
},
"default": {
active: path.join(main.getAssetsDir() , `iconTemplate${modifier}.png`),
paused: path.join(main.getAssetsDir() , `icon-pausedTemplate${modifier}.png`)
}
};
if ( icons[process.platform] ) {
return icons[process.platform];
}
return icons.default;
};
export const trayIconImage = function() {
var icons = getIcons();
let stateManager = main.getStateManager();
let iconPath;
if ( stateManager.currentState === stateManager.STATES.STATE_PAUSED ) {
iconPath = icons.paused;
}
else {
iconPath = icons.active;
}
main.log.info(`use icon ${iconPath}`);
return nativeImage.createFromPath(iconPath);
};
/**
* update tray icon to match our current state
*/
var updateTrayIcon = function() {
let appIcon = main.getAppIcon();
const iconImage = trayIconImage();
if ( !iconImage.isEmpty() ) {
appIcon.setImage(iconImage);
}
};
================================================
FILE: src/main/power.js
================================================
"use strict";
import { execFile } from "child_process";
export default class Power {
constructor(opts = {}) {
this.method = opts.method;
this.platform = opts.platform;
if ( this.platform === undefined ) {
this.platform = process.platform;
}
// https://stackoverflow.com/questions/651563/getting-the-last-element-of-a-split-string-array
this.commands = {
linux: {
cmd: "dbus-send",
opts: [
"--print-reply",
"--system",
"--dest=org.freedesktop.UPower",
"/org/freedesktop/UPower",
"org.freedesktop.DBus.Properties.Get",
"string:org.freedesktop.UPower",
"string:OnBattery"
]
},
};
this.default = true;
}
async rawData() {
if ( this.commands[this.platform] ) {
const cmd = this.commands[this.platform].cmd;
const opts = this.commands[this.platform].opts;
try {
return await this.query(cmd, opts);
}
catch {
return undefined;
}
}
return undefined;
}
async charging(raw = null) {
if ( this.method !== undefined) {
return !this.method();
}
if ( raw === null ) {
raw = await this.rawData();
}
if ( raw === undefined ) {
return this.default;
}
try {
// method return time=1630857381.345226 sender=:1.59 -> destination=:1.1404 serial=30 reply_serial=2
// variant boolean false
const result = raw.split("\n").find((line) => line.indexOf("variant") !== -1);
// OnBattery == false means we're plugged in
return result.indexOf("false") !== -1;
}
catch(e) {
console.log(e);
return this.default;
}
}
query(cmd, args) {
return new Promise((resolve) => {
execFile(cmd, args, (error, stdout, stderr) => {
if (error) {
console.warn(error);
}
resolve(stdout? stdout : stderr);
});
});
}
}
================================================
FILE: src/main/release_check.js
================================================
"use strict";
export default class ReleaseCheck {
constructor() {
this.onUpdateCallback = () => {};
this.onNoUpdateCallback = () => {};
this.logger = () => {};
}
setFeed(u) {
this.url = u;
}
setLogger(l) {
this.logger = l;
}
onUpdate(f) {
this.onUpdateCallback = f;
}
onNoUpdate(f) {
this.onNoUpdateCallback = f;
}
checkLatestRelease() {
this.logger(`check ${this.url} for new release`);
let _self = this;
fetch(this.url, {
timeout: 5000,
headers: {
"User-Agent": "Before Dawn"
}
}).then(function(response) {
if ( response.ok ) {
return response.json();
}
return undefined;
}).then(function(body) {
_self.logger(body);
if ( body !== undefined ) {
_self.onUpdateCallback(body);
}
else {
_self.onNoUpdateCallback();
}
}).catch(() => {
this.onNoUpdateCallback();
});
}
}
================================================
FILE: src/main/screen.js
================================================
"use strict";
import gotoSleep from "@muffinista/goto-sleep";
export const doLockScreen = gotoSleep.lockScreen;
export const doSleep = gotoSleep.gotoSleep;
================================================
FILE: src/main/state_manager.js
================================================
"use strict";
/**
* These are the possible states that the app can be in.
*/
const STATES = {
STATE_NONE: Symbol("none"), // initial state
STATE_IDLE: Symbol("idle"), // not running, waiting
STATE_LOADING: Symbol("loading"), // the liminal state when a screesaver is loaded but not 100%
STATE_RUNNING: Symbol("running"), // running a screensaver
STATE_BLANKED: Symbol("blanked"), // long idle, screen is blanked
STATE_PAUSED: Symbol("paused"), // screensaver is paused,
STATE_UNRUNNABLE: Symbol("unrunnable")
};
// check for updates every 5 seconds when idle
const IDLE_CHECK_RATE = 5000;
// check for updates every .25 second when active
const ACTIVE_CHECK_RATE = 250;
const IDLE_PADDING_CHECK = 1;
class StateManager {
constructor(fn) {
this.STATES = STATES;
this.currentState = STATES.STATE_NONE;
this._idleTime = () => {};
this._blankTime = () => {};
this._onIdleTime = () => {};
this._onBlankTime = () => {};
this._onReset = () => {};
this.logger = function() {};
this.lastTime = -1;
this.enteredStateTimestamp = -1;
this._ignoreReset = false;
this.keepTicking = true;
this._idleFn = fn;
this.rates = {
idle: IDLE_CHECK_RATE,
active: ACTIVE_CHECK_RATE
};
}
get currentTimeStamp() {
return process.hrtime()[0];
}
set idleFn(x) {
this._idleFn = x;
}
/**
* setup timing/callbacks
*/
setup(opts) {
if ( opts.logger !== undefined ) {
this.logger = opts.logger;
}
else {
this.logger = function() {};
}
if ( opts.idleTime && opts.onIdleTime ) {
this._idleTime = opts.idleTime;
this._onIdleTime = opts.onIdleTime;
}
if ( opts.blankTime && opts.onBlankTime ) {
this._blankTime = opts.blankTime;
this._onBlankTime = opts.onBlankTime;
}
if ( opts.onReset ) {
this._onReset = opts.onReset;
}
if ( opts.state ) {
this.switchState(opts.state);
}
else {
this.switchState(STATES.STATE_IDLE);
}
}
/**
* reset to idle and clear any timers
*/
reset() {
this.switchState(STATES.STATE_IDLE);
this.ignoreReset(false);
}
unrunnable() {
this.switchState(STATES.STATE_UNRUNNABLE);
}
/**
* pause the state machine
*/
pause() {
this.switchState(STATES.STATE_PAUSED);
}
paused() {
return this.currentState === STATES.STATE_PAUSED;
}
/**
* start running the state machine
*/
run() {
this.switchState(STATES.STATE_LOADING);
}
running() {
this.ignoreReset(false);
this.switchState(STATES.STATE_RUNNING);
}
/**
* handle calling the onIdleTime callback specified in setup
*/
onIdleTime() {
this._onIdleTime();
}
/**
* handle calling the onBlankTime callback specified in setup
*/
onBlankTime() {
this._onBlankTime();
}
onReset() {
this._onReset();
}
/**
* switch to a new state. if we're not already in that state, or if
* force == true, call onEnterState
*/
switchState(s, force) {
// we run onEnterState if the state has changed or if we need to
// force a reload. we also run it if the new state is idle, this
// should help with some weird issues where timers aren't being
// reset properly
const callEnterState = ( this.currentState !== s || s === STATES.STATE_IDLE || force === true);
this.currentState = s;
this.enteredStateTimestamp = this.currentTimeStamp;
if ( callEnterState ) {
this.onEnterState(s);
}
}
/**
* enter a new state. set any timers/etc needed
*/
onEnterState(s) {
switch (s) {
case STATES.STATE_IDLE:
this.onReset();
break;
case STATES.STATE_LOADING:
this.onIdleTime();
break;
case STATES.STATE_BLANKED:
this.onBlankTime();
break;
case STATES.STATE_PAUSED:
break;
}
}
getCurrentState() {
return this.currentState;
}
get currentStateString() {
return this.currentState.toString();
}
/**
* based on our current state, figure out the timestamp
* that we will enter the next state
*/
getNextTime() {
if ( this.currentState === STATES.STATE_RUNNING ) {
return this._blankTime;
}
return this._idleTime;
}
ignoreReset(val) {
this.logger(`set ignoreReset to ${val}`);
this._ignoreReset = val;
if ( this._ignoreReset === false ) {
this.lastTime = -1;
}
}
/**
* check idle time and determine if we should switch states
*/
tick(runAgain) {
if ( this.currentState !== STATES.STATE_NONE && this.currentState !== STATES.STATE_PAUSED ) {
const i = this._idleFn();
const nextTime = this.getNextTime();
const hadActivity = (i < this.lastTime ||
(this.currentState === STATES.STATE_RUNNING && i <= 10 && this.currentTimeStamp - i - IDLE_PADDING_CHECK > this.enteredStateTimestamp));
// this.logger(`${i} ${this.lastTime} -- ${this.currentStateString}`);
if ( this.currentState === STATES.STATE_PAUSED ) {
// do nothing
} else if ( hadActivity && this.currentState !== STATES.STATE_IDLE ) {
// we won't actually reset the state while a screensaver is
// loading, because sometimes we get zombie electron windows
// when we do that
if ( ! this._ignoreReset ) {
this.logger(`Current idle: ${i} Last idle: ${this.lastTime} -- ${this.currentStateString} -- reset`);
this.reset();
}
else {
this.logger(`Current idle: ${i} Last idle: ${this.lastTime} -- but ignoreReset is true`);
}
}
else if ( i >= nextTime && this.currentState !== STATES.STATE_BLANKED ) {
if ( this.currentState === STATES.STATE_IDLE) {
this.logger(`${i} >= ${nextTime} -- switch from ${this.currentStateString} to loading`);
this.switchState(STATES.STATE_LOADING);
}
else if ( this.currentState === STATES.STATE_RUNNING) {
this.logger(`${i} >= ${nextTime} -- switch from ${this.currentStateString} to blanked`);
this.switchState(STATES.STATE_BLANKED);
}
else {
// this.logger(`${i} >= ${nextTime} -- switch from ${this.currentStateString} to ????`);
}
}
if ( this.currentState !== STATES.STATE_LOADING ) {
this.lastTime = i;
}
}
if ( runAgain !== false ) {
this.scheduleTick();
}
}
scheduleTick() {
if ( this.keepTicking ) {
let rate = this.rates.idle;
if ( this.currentState === STATES.STATE_RUNNING ) {
rate = this.rates.active;
}
setTimeout(() => {
this.tick(true);
}, rate);
}
}
setupLogging() {
this.logger("setupLogging");
// clearInterval(this.loggingInterval);
// every minute or so, output the current state
this.loggingInterval = setInterval(() => {
this.logger(`Current idle: ${this._idleFn()} Last idle: ${this.lastTime} -- ${this.currentStateString}`);
}, 60000);
}
isTicking() {
return this.keepTicking === true;
}
startTicking() {
this.logger("startTicking");
this.keepTicking = true;
this.setupLogging();
this.scheduleTick();
}
stopTicking() {
this.logger("stopTicking");
this.keepTicking = false;
clearInterval(this.loggingInterval);
}
}
export default StateManager;
================================================
FILE: src/main/system-savers/__template/index.html
================================================
My Awesome Screensaver
Hello! I am a screensaver template!
I am located in .
Put the content of your screensaver here!
Incoming Values
(These parameters will be sent to your screensaver when it loads)
Helpful Snippets
Here's some code to load the incoming screenshot, so you can apply effects/etc to it:
<body>
<img id="screen" />
</body>
<script>
var img = document.getElementById("screen");
var url = unescape(decodeURIComponent(window.urlParams.screenshot));
img.src = url;
if ( typeof(window.urlParams.width) !== "undefined" ) {
img.width = window.urlParams.width;
img.height = window.urlParams.height;
}
</script>
It might make sense to hide any margins on elements in your screensaver with some CSS like this:
<style>
* {
padding: 0;
margin: 0;
}
</style>
================================================
FILE: src/main/system-savers/__template/saver.json
================================================
{
"name": "My Awesome Screensaver",
"description": "a description of my terrific screensaver",
"aboutUrl": "http://mywebsite.com/about",
"author": "my name/etc",
"source": "index.html"
}
================================================
FILE: src/main/system-savers/blank/index.html
================================================
Blank
================================================
FILE: src/main/system-savers/blank/saver.json
================================================
{
"name": "Blank screen",
"description": "Blank the screen",
"author": "Colin Mitchell",
"source": "index.html",
"requirements": []
}
================================================
FILE: src/main/system-savers/dimmer/index.html
================================================
Dimmer
================================================
FILE: src/main/system-savers/dimmer/saver.json
================================================
{
"name": "Dimmer",
"description": "Dim the screen a bit",
"author": "Colin Mitchell",
"source": "index.html",
"requirements": [
"screen"
]
}
================================================
FILE: src/main/system-savers/random/index.html
================================================
Random Screensaver
Hello!
This screensaver will pick a random screensaver and run it for you. Enjoy!
================================================
FILE: src/main/system-savers/random/saver.json
================================================
{
"name": "Random",
"description": "Pick a random screensaver each time",
"author": "Colin Mitchell",
"preload": "random",
"source": "index.html"
}
================================================
FILE: src/main/windows.js
================================================
"use strict";
import * as main from "./index.js";
import { BrowserWindow } from "electron";
var getSaverWindows = function() {
return BrowserWindow.getAllWindows().filter((w) => {
return w.isSaver === true;
});
};
/**
* check if the screensaver is still running
*/
export const screenSaverIsRunning = function() {
return ( getSaverWindows().length > 0 );
};
/**
* check if the specified window exists and isn't destroyed
*/
var activeWindowHandle = function(w) {
return (typeof(w) !== "undefined" && ! w.isDestroyed());
};
/**
* when the display count changes, close any running windows
*/
export const handleDisplayChange = function() {
// main.log.info("display change, let's close running screensavers");
closeRunningScreensavers();
};
/**
* close any running screensavers
*/
export const closeRunningScreensavers = function() {
main.log.info("closeRunningScreensavers");
attemptToStopScreensavers();
// be really aggressive about closing lagging windows
setTimeout(forcefullyCloseScreensavers, 2500);
setTimeout(forcefullyCloseScreensavers, 5000);
};
/**
* iterate through our list of running screensaver windows and attempt
* to close them nicely
*/
var attemptToStopScreensavers = function() {
getSaverWindows().forEach((w) => {
if ( activeWindowHandle(w) ) {
w.close();
}
});
};
/**
* iterate through our list of running screensaver windows and close
* them forcefully if needed
*/
var forcefullyCloseScreensavers = function() {
getSaverWindows().forEach((w) => {
if ( activeWindowHandle(w) ) {
w.destroy();
}
});
};
/**
* forcefully close a screensaver window
*/
export const forceWindowClose = function(w) {
// 100% close/kill this window
if ( typeof(w) !== "undefined" ) {
try {
w.destroy();
}
catch (e) {
main.log.info(e);
}
}
};
/**
* Set full screen mode for the given window. Use OSX's
* fast/simple fullscreen mode if available.
* @param {BrowserWindow} w the window to apply
*/
export const setFullScreen = function(w) {
if ( process.platform !== "darwin" ) {
w.setFullScreen(true);
}
else {
w.setSimpleFullScreen(true);
}
w.show();
// w.moveTop();
};
================================================
FILE: src/renderer/AboutScreen.svelte
================================================
Before Dawn
// screensaver fun //
{#await loadData() then}
{globals.APP_VERSION}
{/await}
An open-source screensaver project.
learn more
Having trouble?
please let us know!
App
icon
Sun by Ale Estrada from the Noun Project
Powered by
Electron
================================================
FILE: src/renderer/EditorScreen.svelte
================================================
{#if validOptions && validOptions.length > 0}
Preview settings
Tweak the values here and they will be sent along to your preview.
{/if}
================================================
FILE: src/renderer/NewScreensaverScreen.svelte
================================================
New Screensaver
{#if canAdd}
Screensavers in Before Dawn are web pages, so if you can use HTML,
CSS, and/or Javascript, you can make your own screensaver.
Use this form to create a new screensaver. A template will be
added to the system that you can fill in with your code.
{:else}
Screensavers in Before Dawn are web pages, so if you can use HTML,
CSS, and/or Javascript, you can make your own screensaver. But before
you can do that, you'll need to set a local directory!
{/if}
================================================
FILE: src/renderer/PrefsScreen.svelte
================================================
{#if saverObj !== undefined && saverObj.options}
{/if}
================================================
FILE: src/renderer/SettingsScreen.svelte
================================================
{#if hasScreensaverUpdate === true}
Download screensaver updates
{#if downloadingUpdates}
{/if}
{/if}
================================================
FILE: src/renderer/components/FolderChooser.svelte
================================================
...
X
================================================
FILE: src/renderer/components/Notarize.js
================================================
const NOTARIZE_DEFAULTS = {
wrapperClass: "notarize-wrapper",
interiorClass: "notarize",
timeout: 150000,
transitionIn: "notarize-in",
transitionOut: "notarize-out",
template: (args) => { return ``; }
};
export default class Notarize {
constructor(options={}) {
this.options = Object.assign({}, NOTARIZE_DEFAULTS, options);
return this;
}
show(contents) {
const body = document.querySelector("body");
const guts = this.options.template({
wrapperClass: [this.options.wrapperClass, this.options.transitionIn].join(" "),
interiorClass: this.options.interiorClass,
contents: contents
});
const el = this.toDom(guts);
body.insertBefore(el, body.firstChild);
el.addEventListener("animationend", this.handleTransitionIn.bind(this));
}
toDom(html) {
const template = document.createElement("template");
template.innerHTML = html.trim(); // Never return a text node of whitespace as the result
if (template.content) {
return template.content.firstChild;
}
return template.firstChild;
}
handleTransitionIn(ev) {
const el = ev.target;
el.removeEventListener("animationend", this.handleTransitionIn);
setTimeout(() => {
el.addEventListener("animationend", this.handleTransitionOut.bind(this));
el.classList.add(this.options.transitionOut);
}, this.options.timeout);
}
handleTransitionOut(ev) {
ev.target.removeEventListener("animationend", this.handleTransitionOut);
ev.target.parentNode.removeChild(ev.target);
}
}
================================================
FILE: src/renderer/components/SaverForm.svelte
================================================
================================================
FILE: src/renderer/components/SaverList.svelte
================================================
Screensavers
{#each savers as saver (saver.key)}
{/each}
================================================
FILE: src/renderer/components/SaverOptionInput.svelte
================================================
================================================
FILE: src/renderer/components/SaverOptions.svelte
================================================
{#each saver.options as option, index (option.name)}
{#if option.type === "boolean"}
{:else if option.type === "slider"}
{:else}
{/if}
{/each}
================================================
FILE: src/renderer/components/SaverSummary.svelte
================================================
{#if saver}
{saver.name}
{#if saver.aboutUrl && saver.aboutUrl !== ""}learn more {/if}
{#if saver.editable}
edit
delete
{/if}
{saver.description}
{#if saver.author && saver.author !== ""}
by: {saver.author}
{/if}
{/if}
================================================
FILE: src/renderer/components/Spinner.svelte
================================================
================================================
FILE: src/renderer/components/icons/BugIcon.svelte
================================================
================================================
FILE: src/renderer/components/icons/FolderIcon.svelte
================================================
================================================
FILE: src/renderer/components/icons/ReloadIcon.svelte
================================================
================================================
FILE: src/renderer/components/icons/SaveIcon.svelte
================================================
================================================
FILE: src/renderer/main.js
================================================
import "~/css/styles.scss";
import { mount } from 'svelte';
import PrefsScreen from "./PrefsScreen.svelte";
import SettingsScreen from "./SettingsScreen.svelte";
import AboutScreen from "./AboutScreen.svelte";
import NewScreensaverScreen from "./NewScreensaverScreen.svelte";
import EditorScreen from "./EditorScreen.svelte";
// import * as Sentry from "@sentry/electron/renderer";
// if ( process.env.SENTRY_DSN !== undefined ) {
// Sentry.init({
// dsn: process.env.SENTRY_DSN,
// enableNative: false,
// onFatalError: console.log
// });
// }
const actions = {
"prefs": PrefsScreen,
"settings": SettingsScreen,
"about": AboutScreen,
"new": NewScreensaverScreen,
"editor": EditorScreen
};
const id = document.querySelector("body").dataset.id;
const klass = actions[id];
const app = mount(klass, {
target: document.getElementById("root"), // entry point in ../public/index.html
});
export default app;
================================================
FILE: test/fixtures/bad-config.json
================================================
{
"source": {
"repo": ""
},
"saver": "before-dawn-screensavers/emoji/index.html",
"options": {
================================================
FILE: test/fixtures/config-2.json
================================================
{
"source": {
"repo": ""
},
"saver": "before-dawn-screensavers/blur/saver.json",
"options": {
"/Users/colin/Projects/before-dawn-screensavers/emoji/saver.json": {}
},
"delay": 10,
"lock": false,
"localSource": "/home/tester/my screensavers",
"disableOnBattery": true,
"sleep": 10
}
================================================
FILE: test/fixtures/config-with-options.json
================================================
{
"source": {
"repo": ""
},
"saver": "/Users/colin/Projects/before-dawn-screensavers/emoji/saver.json",
"options": {
"/Users/colin/Projects/before-dawn-screensavers/emoji/saver.json": {
"foo": "bar",
"level": 100
},
"/Users/colin/Projects/before-dawn-screensavers/key/saver.json": {
"baz": "boo",
"level": 10
}
},
"delay": 10,
"lock": false,
"localSource": "/home/tester/my screensavers",
"disableOnBattery": true,
"sleep": 10
}
================================================
FILE: test/fixtures/config.json
================================================
{
"source": {
"repo": ""
},
"saver": "before-dawn-screensavers/emoji/saver.json",
"options": {
"/Users/colin/Projects/before-dawn-screensavers/emoji/saver.json": {}
},
"delay": 10,
"lock": false,
"localSource": "/home/tester/my screensavers",
"disableOnBattery": false,
"runOnSingleDisplay": true,
"auto_start": true,
"sleep": 10
}
================================================
FILE: test/fixtures/default-repo.json
================================================
{
"sourceRepo": "mocha/screensavers",
"sourceUpdatedAt": "2018-01-07T15:59:04.499Z",
"saver": "before-dawn-screensavers/emoji/saver.json",
"options": {
"/Users/colin/Projects/before-dawn-screensavers/emoji/saver.json": {}
},
"delay": 10,
"lock": false,
"localSource": "/home/tester/my screensavers",
"disableOnBattery": true,
"sleep": 10
}
================================================
FILE: test/fixtures/index.html
================================================
screensaver
I AM A SCREENSAVER!
================================================
FILE: test/fixtures/invalid.json
================================================
{
"description": "A Screensaver",
"aboutUrl": "",
"author": "Colin Mitchell",
"source": "index.html",
"requirements": [],
"options": [
{
"index": 0,
"name": "load_url",
"type": "text",
"description": "Load the specified URL",
"min": "1",
"max": "100",
"default": ""
},
{
"index": 1,
"name": "sound",
"type": "boolean",
"description": "Play sound?",
"min": "1",
"max": "100",
"default": "true"
}
],
"editable": true
}
================================================
FILE: test/fixtures/no-requirements.json
================================================
{
"name": "Screensaver One",
"description": "A Screensaver",
"aboutUrl": "",
"author": "Colin Mitchell",
"source": "index.html",
"options": [
{
"index": 0,
"name": "load_url",
"type": "text",
"description": "Load the specified URL",
"min": "1",
"max": "100",
"default": ""
},
{
"index": 1,
"name": "sound",
"type": "boolean",
"description": "Play sound?",
"min": "1",
"max": "100",
"default": "true"
}
],
"editable": true
}
================================================
FILE: test/fixtures/old-config.json
================================================
{
"source": {
"repo": "muffinista/before-dawn-screensavers"
},
"sourceCheckTimestamp": 2512407042366,
"saver": "before-dawn-screensavers/emoji/index.html",
"options": {
"/Users/colin/Projects/before-dawn-screensavers/emoji/index.html": {}
},
"delay": 10,
"lock": false,
"localSource": "/home/tester/my screensavers",
"disableOnBattery": true,
"sleep": 10
}
================================================
FILE: test/fixtures/power/linux-charged.txt
================================================
method return time=1630857381.345226 sender=:1.59 -> destination=:1.1404 serial=30 reply_serial=2
variant boolean false
================================================
FILE: test/fixtures/power/linux-charging.txt
================================================
method return time=1630857381.345226 sender=:1.59 -> destination=:1.1404 serial=30 reply_serial=2
variant boolean false
================================================
FILE: test/fixtures/power/linux-discharging.txt
================================================
method return time=1630857381.345226 sender=:1.59 -> destination=:1.1404 serial=30 reply_serial=2
variant boolean true
================================================
FILE: test/fixtures/release-no-update.json
================================================
{
"url": "https://api.github.com/repos/muffinista/before-dawn-screensavers/releases/6625664",
"assets_url": "https://api.github.com/repos/muffinista/before-dawn-screensavers/releases/6625664/assets",
"upload_url": "https://uploads.github.com/repos/muffinista/before-dawn-screensavers/releases/6625664/assets{?name,label}",
"html_url": "https://github.com/muffinista/before-dawn-screensavers/releases/tag/v0.9.2",
"id": 6625664,
"tag_name": "v0.9.2",
"target_commitish": "main",
"name": "",
"draft": false,
"author": {
"login": "muffinista",
"id": 49172,
"avatar_url": "https://avatars1.githubusercontent.com/u/49172?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/muffinista",
"html_url": "https://github.com/muffinista",
"followers_url": "https://api.github.com/users/muffinista/followers",
"following_url": "https://api.github.com/users/muffinista/following{/other_user}",
"gists_url": "https://api.github.com/users/muffinista/gists{/gist_id}",
"starred_url": "https://api.github.com/users/muffinista/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/muffinista/subscriptions",
"organizations_url": "https://api.github.com/users/muffinista/orgs",
"repos_url": "https://api.github.com/users/muffinista/repos",
"events_url": "https://api.github.com/users/muffinista/events{/privacy}",
"received_events_url": "https://api.github.com/users/muffinista/received_events",
"type": "User",
"site_admin": false
},
"prerelease": false,
"created_at": "2017-06-06T23:54:52Z",
"published_at": "2017-06-06T23:55:44Z",
"assets": [],
"tarball_url": "https://api.github.com/repos/muffinista/before-dawn-screensavers/tarball/v0.9.2",
"zipball_url": "https://api.github.com/repos/muffinista/before-dawn-screensavers/zipball/v0.9.2",
"body": "",
"is_update": false
}
================================================
FILE: test/fixtures/release.json
================================================
{
"url": "https://api.github.com/repos/muffinista/before-dawn-screensavers/releases/6625664",
"assets_url": "https://api.github.com/repos/muffinista/before-dawn-screensavers/releases/6625664/assets",
"upload_url": "https://uploads.github.com/repos/muffinista/before-dawn-screensavers/releases/6625664/assets{?name,label}",
"html_url": "https://github.com/muffinista/before-dawn-screensavers/releases/tag/v0.9.2",
"id": 6625664,
"tag_name": "v0.9.2",
"target_commitish": "main",
"name": "",
"draft": false,
"author": {
"login": "muffinista",
"id": 49172,
"avatar_url": "https://avatars1.githubusercontent.com/u/49172?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/muffinista",
"html_url": "https://github.com/muffinista",
"followers_url": "https://api.github.com/users/muffinista/followers",
"following_url": "https://api.github.com/users/muffinista/following{/other_user}",
"gists_url": "https://api.github.com/users/muffinista/gists{/gist_id}",
"starred_url": "https://api.github.com/users/muffinista/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/muffinista/subscriptions",
"organizations_url": "https://api.github.com/users/muffinista/orgs",
"repos_url": "https://api.github.com/users/muffinista/repos",
"events_url": "https://api.github.com/users/muffinista/events{/privacy}",
"received_events_url": "https://api.github.com/users/muffinista/received_events",
"type": "User",
"site_admin": false
},
"prerelease": false,
"created_at": "2017-06-06T23:54:52Z",
"published_at": "2017-06-06T23:55:44Z",
"assets": [],
"tarball_url": "https://api.github.com/repos/muffinista/before-dawn-screensavers/tarball/v0.9.2",
"zipball_url": "https://api.github.com/repos/muffinista/before-dawn-screensavers/zipball/v0.9.2",
"body": "",
"is_update": true
}
================================================
FILE: test/fixtures/releases/updates.json
================================================
{"name":"v0.9.26","notes":"This release updates some packages, including Electron, fixes some icon rendering issues, and has some other minor cleanup.","pub_date":"2018-10-06T17:55:43Z","url":"https://github.com/muffinista/before-dawn/releases/download/v0.9.26/before-dawn-setup-0.9.26.exe"}
================================================
FILE: test/fixtures/saver.json
================================================
{
"name": "Screensaver One",
"description": "A Screensaver",
"aboutUrl": "",
"author": "Colin Mitchell",
"source": "index.html",
"requirements": [],
"options": [
{
"index": 0,
"name": "load_url",
"type": "text",
"description": "Load the specified URL",
"min": "1",
"max": "100",
"default": ""
},
{
"index": 1,
"name": "sound",
"type": "boolean",
"description": "Play sound?",
"min": "1",
"max": "100",
"default": "true"
}
],
"editable": true
}
================================================
FILE: test/fixtures/saver2.json
================================================
{
"name": "Saver Two",
"description": "Another Screensaver",
"aboutUrl": "",
"author": "Colin Mitchell",
"source": "index.html",
"requirements": [],
"options": [
{
"index": 0,
"name": "New Option I Guess",
"type": "slider",
"description": "Description",
"min": "1",
"max": "100",
"default": "75"
}
],
"editable": true
}
================================================
FILE: test/fixtures/test-savers.json
================================================
{"url":"https://api.github.com/repos/muffinista/before-dawn-screensavers/releases/19343721","assets_url":"https://api.github.com/repos/muffinista/before-dawn-screensavers/releases/19343721/assets","upload_url":"https://uploads.github.com/repos/muffinista/before-dawn-screensavers/releases/19343721/assets{?name,label}","html_url":"https://github.com/muffinista/before-dawn-screensavers/releases/tag/v0.9.35","id":19343721,"node_id":"MDc6UmVsZWFzZTE5MzQzNzIx","tag_name":"v0.9.35","target_commitish":"main","name":"version 0.9.35","draft":false,"author":{"login":"muffinista","id":49172,"node_id":"MDQ6VXNlcjQ5MTcy","avatar_url":"https://avatars1.githubusercontent.com/u/49172?v=4","gravatar_id":"","url":"https://api.github.com/users/muffinista","html_url":"https://github.com/muffinista","followers_url":"https://api.github.com/users/muffinista/followers","following_url":"https://api.github.com/users/muffinista/following{/other_user}","gists_url":"https://api.github.com/users/muffinista/gists{/gist_id}","starred_url":"https://api.github.com/users/muffinista/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/muffinista/subscriptions","organizations_url":"https://api.github.com/users/muffinista/orgs","repos_url":"https://api.github.com/users/muffinista/repos","events_url":"https://api.github.com/users/muffinista/events{/privacy}","received_events_url":"https://api.github.com/users/muffinista/received_events","type":"User","site_admin":false},"prerelease":false,"created_at":"2019-08-16T16:47:00Z","published_at":"2019-08-16T16:47:05Z","assets":[],"tarball_url":"https://api.github.com/repos/muffinista/before-dawn-screensavers/tarball/v0.9.35","zipball_url":"https://api.github.com/repos/muffinista/before-dawn-screensavers/zipball/v0.9.35","body":null}
================================================
FILE: test/helpers.js
================================================
/* eslint-disable mocha/no-exports */
import * as path from "path";
import fs from 'fs-extra';
import * as tmp from "tmp";
import temp from "temp";
import Conf from "conf";
import { _electron as playwright } from "playwright";
import electron from "electron";
import assert from "assert";
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
let windowCheckDelay = 5000;
let testTimeout = 50000;
let testRetryCount = 0;
let logPath;
let app;
if (process.env.CI) {
windowCheckDelay = 10000;
testTimeout = 60000;
testRetryCount = 3;
}
const delayStep = 10;
/**
* keep a list of window titles here so we can have
* a really clean system to open/load/wait for windows
*/
const windowTitles = {
new: "Before Dawn: Create Screensaver!",
about: "Before Dawn: About!",
editor: "Before Dawn: Editor",
prefs: "Before Dawn: Preferences",
settings: "Before Dawn: Settings"
};
export function specifyConfig(dest, name) {
fs.copySync(
path.join(__dirname, "fixtures", name + ".json"),
dest
);
}
export function setupConfig(workingDir, name="config", attrs={}) {
const dest = path.join(workingDir, "config.json");
fs.copySync(
path.join(__dirname, "fixtures", name + ".json"),
dest
);
if ( Object.keys(attrs) > 0 ) {
let store = new Conf({cwd: workingDir});
store.set(attrs);
}
}
export function setConfigValue(workingDir, name, value) {
let f = path.join(workingDir, "config.json");
let currentVals = JSON.parse(fs.readFileSync(f));
currentVals[name] = value;
fs.writeFileSync(f, JSON.stringify(currentVals));
}
export function addSaver(dest, name, source) {
// make a subdir in the savers directory and drop screensaver
// config there
if ( source === undefined ) {
source = "saver.json";
}
var src = path.join(__dirname, "fixtures", source);
var htmlSrc = path.join(__dirname, "fixtures", "index.html");
var testSaverDir = path.join(dest, name);
var saverJSONFile = path.join(testSaverDir, "saver.json");
var saverHTMLFile = path.join(testSaverDir, "index.html");
if ( ! fs.existsSync(dest) ) {
fs.mkdirSync(dest);
}
fs.mkdirSync(testSaverDir);
fs.copySync(src, saverJSONFile);
fs.copySync(htmlSrc, saverHTMLFile);
return saverJSONFile;
}
export function prefsToJSON(tmpdir) {
let testFile = path.join(tmpdir, "config.json");
let data = {};
try {
data = JSON.parse(fs.readFileSync(testFile));
}
catch {
data = {};
}
return data;
}
export function getTempDir() {
const base = tmp.dirSync().name;
if ( process.platform === "win32" && base.lastIndexOf("~") !== -1) {
return base.replace("RUNNER~1", "runneradmin");
}
return base;
}
export function savedConfig(p) {
var data = path.join(p, "config.json");
var json = fs.readFileSync(data);
return JSON.parse(json);
}
export function setupFullConfig(workingDir) {
let saversDir = getTempDir();
let saverJSONFile = addSaver(saversDir, "saver");
setupConfig(workingDir, "config", {
"saver": saverJSONFile
});
}
export function addLocalSource(workingDir, saversDir) {
var src = path.join(workingDir, "config.json");
var data = savedConfig(workingDir);
data.localSource = saversDir;
fs.writeFileSync(src, JSON.stringify(data));
}
export function removeLocalSource(workingDir) {
var src = path.join(workingDir, "config.json");
var data = savedConfig(workingDir);
data.localSource = "";
fs.writeFileSync(src, JSON.stringify(data));
}
/**
* Launch the application via playwright
*
* @param {string} workingDir
* @param {boolean} quietMode
* @returns application
*/
export async function application(workingDir, quietMode=false, logFile=undefined) {
let env = {
...process.env,
BEFORE_DAWN_DIR: workingDir,
CONFIG_DIR: workingDir,
SAVERS_DIR: workingDir,
TEST_MODE: true,
QUIET_MODE: quietMode,
ELECTRON_ENABLE_LOGGING: true,
LOG_FILE: logFile,
XDG_SESSION_TYPE: 'x11'
};
let a = await playwright.launch({
path: electron,
args: [
path.join(__dirname, "..", "output", "main.js")
],
env: env
});
a.logData = [];
a.once("window", (w) => {
w.on("console", (payload) => {
a.logData.push(payload);
});
});
// wait for the first window (our test shim) to open
await a.firstWindow();
app = a;
return a;
}
export async function dumpOutput(app) {
if (app) {
console.log(app.logData);
app.logData = [];
}
if (fs.existsSync(logPath)) {
console.log(fs.readFileSync(logPath));
}
}
/**
*
* @param {app} app electron application
* @param {string} windowName the name of the window to wait for
* @returns Page
*/
export async function waitFor(app, windowName) {
const title = windowTitles[windowName];
await waitForWindow(app, title);
return getWindowByTitle(app, title);
}
/**
* Kill the app
*
* @param {application} app
*/
export async function stopApp(app) {
try {
if (app ) {
await app.close();
}
}
catch(e) {
console.log(e);
}
}
/**
* Generate a lookup table of currently open windows
*
* @param {*} app
* @returns hash of window objects keyed by title
*/
export async function getWindowLookup(app) {
const windows = await app.windows();
const promises = windows.map(async (window) => {
try {
const title = await window.title();
return [title, window];
} catch {
// sometimes a window will be closing and trying to get the title
// will throw an error, but it's probably fine
return ["Missing window", window];
}
});
const results = await Promise.all(promises);
return results.reduce((map, obj) => {
map[obj[0]] = obj[1];
return map;
}, {});
}
/**
* Get window with the given title
*
* @param {*} app
* @param {*} title
* @returns
*/
export async function getWindowByTitle(app, title) {
// make sure the app is open
await app.firstWindow();
const lookup = await getWindowLookup(app);
return lookup[title];
}
/**
* wait for text on the given window
* @param {Page} window
* @param {string} lookup lookup to pull a specific DOM section
* @param {string} text text to look for
* @param {boolean} doAssert
*/
export async function waitForText(window, lookup, text, doAssert) {
const content = await window.textContent(lookup);
if ( doAssert === true ) {
assert(content.lastIndexOf(text) !== -1);
}
}
/**
* wait for ms milliseconds
* @param {*} ms
* @returns
*/
export function sleep (ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* wait for window with the specified name to be available
* @param {*} app
* @param {*} title
* @param {*} skipAssert
* @returns
*/
export async function waitForWindow(app, title, skipAssert) {
let result = -1;
for ( var totalTime = 0; totalTime < windowCheckDelay; totalTime += delayStep ) {
result = await getWindowByTitle(app, title);
if ( result ) {
return true;
}
else {
await sleep(delayStep);
}
}
if ( skipAssert !== true ) {
assert.notStrictEqual(-1, result, `window ${title} not opened`);
}
return result;
}
/**
* Use the shim window to send an IPC command to the app
* @param {*} app
* @param {*} method
* @param {*} opts
*/
export async function callIpc(app, method, opts={}) {
await waitForWindow(app, 'test shim');
const window = await getWindowByTitle(app, 'test shim');
await window.fill("#ipc", method);
await window.fill("#ipcopts", JSON.stringify(opts));
await window.click("text=go");
}
export function setupTest(test) {
test.timeout(testTimeout);
test.retries(testRetryCount);
// eslint-disable-next-line mocha/no-top-level-hooks
beforeEach(function () {
logPath = temp.path();
});
// eslint-disable-next-line mocha/no-top-level-hooks
afterEach(async function () {
if (this.currentTest.state !== "passed") {
await dumpOutput(app);
}
await stopApp(app);
});
}
================================================
FILE: test/lib/package.js
================================================
"use strict";
import assert from 'assert';
import path from "path";
import fs from "fs";
import * as tmp from "tmp";
import { rimrafSync } from 'rimraf'
import sinon from "sinon";
import nock from "nock";
import Package from "../../src/lib/package.js";
import * as helpers from "../helpers.js";
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
var attrs;
var workingDir;
var dataPath;
var zipPath;
var sandbox;
describe("Package", function() {
beforeEach(function() {
sandbox = sinon.createSandbox();
workingDir = helpers.getTempDir();
dataPath = path.join(__dirname, "..", "fixtures", "release.json");
const saverZipSource = path.join(__dirname, "..", "fixtures", "test-savers.zip");
zipPath = path.join(tmp.dirSync().name, "test-savers.zip");
fs.copyFileSync(saverZipSource, zipPath);
attrs = {
repo: "muffinista/before-dawn-screensavers",
dest: workingDir
};
});
afterEach(function () {
sandbox.restore();
});
describe("initialization", function() {
it("loads data", function() {
var p = new Package(attrs);
assert.equal(false, p.downloaded);
assert.equal(false, p.attrs().downloaded);
assert.equal(workingDir, p.dest);
assert.equal(workingDir, p.attrs().dest);
});
});
describe("getReleaseInfo", function() {
describe("withValidResponse", function() {
it("does stuff", async function() {
nock("https://api.github.com").
get("/repos/muffinista/before-dawn-screensavers/releases/latest").
replyWithFile(200, dataPath, {
"Content-Type": "application/json",
});
var p = new Package(attrs);
let results = await p.getReleaseInfo();
console.log(results);
assert.equal("muffinista", results.author.login);
});
});
describe("withReject", function() {
it("survives", async function() {
nock("https://api.github.com").
get("/repos/muffinista/before-dawn-screensavers/releases/latest").
replyWithError({
message: "something awful happened",
code: "AWFUL_ERROR",
});
var p = new Package(attrs);
let results = await p.getReleaseInfo();
assert.deepEqual({}, results);
});
});
});
describe("checkLatestRelease", function() {
var p;
describe("remote package", function() {
beforeEach(function() {
p = new Package(attrs);
});
it("calls downloadFile", async function() {
const data = JSON.parse(fs.readFileSync("./test/fixtures/release.json"));
sandbox.stub(p, "getReleaseInfo").
returns(data);
var df = sandbox.stub(p, "downloadFile").resolves(zipPath);
sandbox.stub(p, "zipToSavers").resolves({});
await p.checkLatestRelease();
assert(df.calledOnce);
});
it("doesnt call if not needed", async function() {
const data = JSON.parse(fs.readFileSync("./test/fixtures/release-no-update.json"));
sandbox.stub(p, "getReleaseInfo").
returns(data);
var cb = sinon.spy();
var df = sandbox.stub(p, "downloadFile");
await p.checkLatestRelease(cb);
assert(!df.calledOnce);
});
});
});
describe("downloadFile", function() {
var testUrl = "https://test.file/savers.zip";
beforeEach(function() {
nock("https://test.file").
get("/savers.zip").
reply(200, () => {
return fs.createReadStream(zipPath);
});
rimrafSync(workingDir);
fs.mkdirSync(workingDir);
});
it("works", async function() {
let p = new Package(attrs);
const dest = await p.downloadFile(testUrl);
assert(fs.existsSync(dest));
});
});
describe("zipToSavers", function() {
var p;
beforeEach(function() {
p = new Package(attrs);
rimrafSync(workingDir);
fs.mkdirSync(workingDir);
});
it("unzips files", async function() {
if (process.platform == "darwin") {
this.skip();
}
await p.zipToSavers(zipPath);
var testDest = path.resolve(workingDir, "sparks", "index.html");
assert(fs.existsSync(testDest));
});
it("recovers from errors", function(done) {
if (process.platform == "darwin") {
this.skip();
}
p.zipToSavers(dataPath).
then(() => {}).
catch( () => {
done();
});
});
it("keeps files on failure", function(done) {
if (process.platform == "darwin") {
this.skip();
}
helpers.addSaver(workingDir, "saver-one", "saver.json");
var testDest = path.resolve(workingDir, "saver-one", "saver.json");
assert(fs.existsSync(testDest));
p.zipToSavers(dataPath).catch( () => {
assert(fs.existsSync(testDest));
done();
});
});
it("removes files that arent needed", function(done) {
if (process.platform == "darwin") {
this.skip();
}
helpers.addSaver(workingDir, "saver-one", "saver.json");
var testDest = path.resolve(workingDir, "saver-one", "saver.json");
assert(fs.existsSync(testDest));
p.zipToSavers(zipPath).then(() => {
assert(!fs.existsSync(testDest));
done();
});
});
});
});
================================================
FILE: test/lib/prefs.js
================================================
"use strict";
import assert from 'assert';
import path from "path";
import * as tmp from "tmp";
import fs from "fs";
import * as helpers from "../helpers.js";
import SaverPrefs from "../../src/lib/prefs.js";
describe("SaverPrefs", function() {
var tmpdir, prefs;
beforeEach(function() {
tmpdir = tmp.dirSync().name;
});
describe("without config", function() {
beforeEach(function() {
prefs = new SaverPrefs(tmpdir);
});
it("should load", function() {
assert.equal(true, prefs.needSetup);
});
});
// reload
describe("reload", function() {
beforeEach(function() {
prefs = new SaverPrefs(tmpdir);
});
it("works with existing config", function() {
prefs.saver = "foo/bar/baz.json";
assert.equal("foo/bar/baz.json", prefs.saver);
prefs.reload();
assert.equal("foo/bar/baz.json", prefs.saver);
});
it("persists", function() {
prefs.saver = "foo/bar/baz.json";
prefs = new SaverPrefs(tmpdir);
prefs.reload();
assert.equal("foo/bar/baz.json", prefs.saver);
});
});
describe("needSetup", function() {
it("is false with config", function() {
prefs = new SaverPrefs(tmpdir);
assert.equal(true, prefs.needSetup);
prefs.localSource = "local/dir";
prefs.saver = "foo/bar/baz";
prefs = new SaverPrefs(tmpdir);
assert.equal(false, prefs.needSetup);
});
it("is true if saver is undefined", function() {
prefs = new SaverPrefs(tmpdir);
assert.equal(true, prefs.needSetup);
prefs.localSource = "local/dir";
prefs.saver = "foo/bar/baz";
prefs = new SaverPrefs(tmpdir);
assert.equal(true, !prefs.needSetup);
prefs.saver = undefined;
assert.equal(true, prefs.needSetup);
});
it("is true if saver is blank", function() {
prefs = new SaverPrefs(tmpdir);
assert.equal(true, prefs.needSetup);
prefs.localSource = "local/dir";
prefs.saver = "foo/bar/baz";
prefs = new SaverPrefs(tmpdir);
assert.equal(true, !prefs.needSetup);
prefs.saver = "";
assert.equal(true, prefs.needSetup);
});
});
// no source
describe("noSource", function() {
describe("with config", function() {
beforeEach(function() {
prefs = new SaverPrefs(tmpdir);
helpers.specifyConfig(prefs.configFile, "config");
});
it("is false if source repo", function() {
prefs.sourceRepo = "foo";
prefs.localSource = "";
assert.equal(true, !prefs.noSource);
});
it("is false if local source", function() {
prefs.store.delete("sourceRepo");
prefs.localSource = "foo";
assert.equal(true, !prefs.noSource);
});
});
});
// defaultSaversDir
describe("defaultSaversDir", function() {
beforeEach(function() {
prefs = new SaverPrefs(tmpdir);
});
it("is the working directory", function() {
let dest = path.join(tmpdir, "savers");
assert.equal(dest, prefs.defaultSaversDir);
});
});
// sources
describe("sources", function() {
let systemDir;
beforeEach(function() {
prefs = new SaverPrefs(tmpdir);
helpers.specifyConfig(prefs.configFile, "config");
systemDir = path.join(tmpdir, "system-savers");
});
it("includes localSource", function() {
let saversDir = path.join(tmpdir, "savers");
let localSourceDir = helpers.getTempDir();
prefs.localSource = localSourceDir;
let result = prefs.sources;
assert.deepStrictEqual(
[ saversDir, localSourceDir, systemDir ], result);
});
it("includes repo", function() {
prefs.sourceRepo = "foo";
let result = prefs.sources;
let dest = path.join(tmpdir, "savers");
assert.equal(true, result.lastIndexOf(dest) !== -1);
assert.equal(true, result.lastIndexOf(systemDir) !== -1);
});
it("includes both repo and localsource", function() {
let saversDir = path.join(tmpdir, "savers");
let localSourceDir = helpers.getTempDir();
prefs.localSource = localSourceDir;
prefs.sourceRepo = "foo";
let result = prefs.sources;
assert.deepEqual(
[ saversDir, localSourceDir, systemDir ], result);
});
it("includes system", function() {
fs.mkdirSync(systemDir);
let result = prefs.sources;
assert.equal(true, result.lastIndexOf(systemDir) !== -1);
});
});
// systemSource
describe("systemSource", function() {
beforeEach(function() {
prefs = new SaverPrefs(tmpdir);
});
it("works", function() {
let expected = path.join(tmpdir, "system-savers");
assert.equal(expected, prefs.systemSource);
});
});
// getOptions
describe("getOptions", function() {
beforeEach(function() {
prefs = new SaverPrefs(tmpdir);
helpers.specifyConfig(prefs.configFile, "config-with-options");
});
it("works without key", function() {
let opts = prefs.getOptions();
assert.deepEqual({ foo: "bar", level: 100 }, opts);
});
it("works with key", function() {
let opts = prefs.getOptions("/Users/colin/Projects/before-dawn-screensavers/key/saver.json");
assert.deepEqual({ baz: "boo", level: 10 }, opts);
});
it("returns empty hash when key is undefined", function() {
prefs.store.delete("saver");
let opts = prefs.getOptions();
assert.deepEqual({}, opts);
});
});
});
================================================
FILE: test/lib/saver-factory.js
================================================
"use strict";
import assert from 'assert';
import path from "path";
import fs from "fs-extra";
import { rimrafSync } from 'rimraf'
import * as mkdirp from "mkdirp";
import * as helpers from "../helpers.js";
import SaverPrefs from "../../src/lib/prefs.js";
import SaverFactory from "../../src/lib/saver-factory.js";
import SaverListManager from "../../src/lib/saver-list.js";
describe("SaverFactory", function() {
var savers;
var prefs;
var factory;
var workingDir;
var saversDir;
var systemDir;
beforeEach(function() {
// this will be the working directory of the app
workingDir = helpers.getTempDir();
// this will be the separate directory to hold screensavers
saversDir = helpers.getTempDir();
mkdirp.sync(workingDir);
mkdirp.sync(saversDir);
systemDir = path.join(workingDir, "system-savers");
fs.mkdirSync(systemDir);
helpers.addSaver(systemDir, "random-saver");
helpers.addSaver(systemDir, "__template");
prefs = new SaverPrefs(workingDir);
prefs.localSource = saversDir;
});
afterEach(function() {
if ( fs.existsSync(workingDir) ) {
rimrafSync(workingDir);
}
});
describe("create", function() {
it("works", async function() {
var templateSrc;
const attrs = {
name: "New Screensaver"
};
savers = new SaverListManager({
prefs: prefs
});
factory = new SaverFactory();
templateSrc = path.join(systemDir, "__template");
let data = await savers.list();
let oldCount = data.length;
const result = factory.create(templateSrc, saversDir, attrs);
data = await savers.list();
assert.equal(oldCount + 1, data.length);
assert.equal("new-screensaver", result.key);
assert.equal("New Screensaver", result.name);
const expectedDest = path.join(saversDir, "new-screensaver", "saver.json");
assert.equal(expectedDest, result.dest);
});
it("throws exception", function(done) {
assert.throws(
() => {
savers.create({
name:"New Screensaver"
});
},
Error);
done();
});
});
});
================================================
FILE: test/lib/saver-list.js
================================================
"use strict";
import assert from 'assert';
import path from "path";
import fs from "fs-extra";
import { rimrafSync } from 'rimraf'
import sinon from "sinon";
import * as helpers from "../helpers.js";
import SaverPrefs from "../../src/lib/prefs.js";
import SaverListManager from "../../src/lib/saver-list.js";
var sandbox;
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
describe("SaverListManager", function() {
var savers;
var prefs;
var workingDir;
var saversDir;
var systemDir;
var saverJSONFile;
beforeEach(function() {
sandbox = sinon.createSandbox();
// this will be the working directory of the app
workingDir = helpers.getTempDir();
// this will be the separate directory to hold screensavers
saversDir = helpers.getTempDir();
saverJSONFile = helpers.addSaver(saversDir, "saver");
helpers.addSaver(saversDir, "saver2");
systemDir = path.join(workingDir, "system-savers");
fs.mkdirSync(systemDir);
helpers.addSaver(systemDir, "random-saver");
helpers.addSaver(systemDir, "__template");
prefs = new SaverPrefs(workingDir);
prefs.localSource = saversDir;
savers = new SaverListManager({
prefs: prefs
});
});
afterEach(function() {
if ( fs.existsSync(workingDir) ) {
rimrafSync(workingDir);
}
sandbox.restore();
});
describe("setup", function() {
it("works", function(done) {
savers.setup().then((results) => {
assert(results.first);
assert(results.setup);
done();
});
});
});
describe("reload", function() {
it("works", function(done) {
savers.reload(true).then(() => {
done();
});
});
});
describe("loadFromFile", function() {
it("loads data", function(done) {
savers.loadFromFile(saverJSONFile).then((s) => {
assert.equal("Screensaver One", s.name);
done();
});
});
it("applies options", function(done) {
savers.loadFromFile(saverJSONFile, { "New Option I Guess": "25" }).then((s) => {
assert.equal(s.settings["New Option I Guess"], "25");
done();
});
});
it("rejects bad json", function(done) {
var f = path.join(__dirname, "../fixtures/index.html");
savers.loadFromFile(f, false, { "New Option I Guess": "25" }).
then(() => {
done(new Error("Expected method to reject."));
}).
catch((err) => {
assert(typeof(err) !== "undefined");
done();
}).
catch(done);
});
it("rejects invalid savers", function(done) {
var f = path.join(__dirname, "../fixtures/invalid.json");
savers.loadFromFile(f, false, {}).
then(() => {
done(new Error("Expected method to reject."));
}).
catch(() => {
done();
});
});
it("adds requirements if missing", function(done) {
var f = path.join(__dirname, "../fixtures/no-requirements.json");
savers.loadFromFile(f, false, {}).then((s) => {
assert.deepEqual(["screen"], s.requirements);
done();
});
});
});
describe("list", function() {
it("loads data", async function() {
const data = await savers.list();
assert.equal(3, data.length);
});
it("handles bad data", async function() {
helpers.addSaver(saversDir, "invalid", "invalid.json");
const data = await savers.list();
assert.equal(3, data.length);
});
it("uses cache", async function() {
let cache = [0, 1, 2, 3, 4, 5];
savers.loadedScreensavers = cache;
const data = await savers.list();
assert.deepEqual(cache, data);
});
it("forces reset", async function() {
let cache = [0, 1, 2, 3, 4, 5];
savers.loadedScreensavers = cache;
const data = await savers.list(true);
assert.notDeepEqual(cache, data);
assert.equal(3, data.length);
});
});
describe("reset", function() {
it("resets cache", async function() {
await savers.list();
assert.equal(3, savers.loadedScreensavers.length);
savers.reset();
assert.equal(0, savers.loadedScreensavers.length);
});
});
describe("random", function() {
it("returns something", async function() {
const data = await savers.list();
assert.equal(3, data.length);
let foo = savers.random();
assert(foo.key !== undefined);
});
});
describe("confirmExists", function() {
it("returns true if present", async function() {
let key = path.join(saversDir, "saver2", "saver.json");
const result = await savers.confirmExists(key);
assert(result);
});
it("returns false if not present", async function() {
let key = "junk";
const result = await savers.confirmExists(key);
assert(!result);
});
});
describe("getByKey", function() {
it("returns saver", async function() {
const data = await savers.list();
var key = data[2].key;
var s = savers.getByKey(key);
assert.equal("Screensaver One", s.name);
});
});
describe("delete", function() {
it("can delete if editable", async function() {
const data = await savers.list();
let s = data.find(s => s.editable);
const result = await savers.delete(s);
assert(result);
});
it("doesn't delete if not editable", async function() {
const data = await savers.list();
let s = data.find(s => !s.editable);
try {
await savers.delete(s);
}
catch {
assert(true);
}
});
});
});
================================================
FILE: test/lib/saver.js
================================================
"use strict";
import assert from 'assert';
import path from "path";
import fs from "fs-extra";
import * as tmp from "tmp";
import Saver from "../../src/lib/saver.js";
describe("Saver", function() {
const testName = "Test Screensaver";
const testDescription = "It's a screensaver, but for testing";
const attrs = {
"name": testName,
"description": testDescription,
"aboutUrl": "",
"author": "Colin Mitchell",
"path": "/tmp",
"source": "index.html",
"requirements": [],
"options": [
{
"index": 0,
"name": "New Option I Guess",
"type": "text",
"description": "Description",
"min": "1",
"max": "100",
"default": "50"
},
{
"index": 1,
"name": "New Option",
"type": "slider",
"description": "Description",
"min": "1",
"max": "100",
"default": "75"
}
],
"editable": true
};
var loadSaver = function(opts) {
if ( typeof(opts) === "undefined" ) {
opts = {};
}
var vals = Object.assign({}, attrs, opts);
return new Saver(vals);
};
describe("initialization", function() {
it("loads data", function() {
var s = loadSaver();
assert.equal(testName, s.name);
assert.equal(testDescription, s.description);
});
it("loads options", function() {
var s = loadSaver();
assert.equal(testName, s.name);
assert.equal(2, s.options.length);
});
it("is published by default", function() {
var s = loadSaver();
assert.equal(testName, s.name);
assert(s.published);
});
it("is not valid if not published", function() {
var s = loadSaver({published: false});
assert(!s.valid);
});
it("has default settings", function() {
var s = loadSaver();
assert.equal("75", s.settings["New Option"]);
assert.equal("50", s.settings["New Option I Guess"]);
});
it("merges user settings", function() {
var s = loadSaver({settings: []});
assert.equal("75", s.settings["New Option"]);
assert.equal("50", s.settings["New Option I Guess"]);
});
it("loads local previewUrl", function() {
var s = loadSaver({path: "path", previewUrl:"preview.html"});
assert.equal("path/preview.html", s.previewUrl);
});
});
describe("requirements", function() {
it("defaults to empty", function() {
var s = loadSaver();
assert.deepEqual([], s.requirements);
});
it("reads from incoming params", function() {
var s = loadSaver({requirements:["stuff"]});
assert.deepEqual(["stuff"], s.requirements);
});
});
describe("toHash", function() {
it("should return attributes", function() {
var s = loadSaver();
assert.deepEqual(attrs, s.toHash());
});
});
describe("urlWithParams", function() {
it("returns url if it is remote", function() {
var s = loadSaver({url: "http://muffinlabs.com"});
assert.deepEqual("http://muffinlabs.com", s.urlWithParams({foo: "bar"}));
});
it("returns url if not remote but no params", function() {
var s = loadSaver({path: "path", settings: {}});
assert.deepEqual("file://path/index.html?New+Option+I+Guess=50&New+Option=75", s.urlWithParams());
});
it("includes params", function() {
var s = loadSaver({path: "path", settings: {}});
assert.deepEqual("file://path/index.html?foo=bar&New+Option+I+Guess=50&New+Option=75", s.urlWithParams({foo: "bar"}));
});
});
describe("write", function() {
it("should write some output", function() {
var dest = tmp.fileSync().name;
var s = loadSaver();
s.attrs.name = "New Name To Write";
s.write(s.toHash(), dest);
var data = JSON.parse(fs.readFileSync(dest));
s = new Saver(data);
assert.equal("New Name To Write", s.name);
});
it("should work without a dest", function() {
var p = tmp.dirSync().name;
var dest = path.join(p, "saver.json");
var s = loadSaver({path: p});
s.attrs.name = "New Name To Write";
s.write(s.toHash());
var data = JSON.parse(fs.readFileSync(dest));
s = new Saver(data);
assert.equal("New Name To Write", s.name);
});
it("sets default requirements", function() {
var p = tmp.dirSync().name;
var dest = path.join(p, "saver.json");
var s = loadSaver({path: p});
delete s.attrs.requirements;
s.write(s.toHash());
var data = JSON.parse(fs.readFileSync(dest));
s = new Saver(data);
assert.equal("none", s.requirements[0]);
});
it("allows requirements", function() {
var p = tmp.dirSync().name;
var dest = path.join(p, "saver.json");
var s = loadSaver({path: p});
s.attrs.requirements = ["magic"];
s.write(s.toHash());
var data = JSON.parse(fs.readFileSync(dest));
s = new Saver(data);
assert.equal(1, s.requirements.length);
});
});
describe("published", function() {
it("defaults to true", function() {
var s = new Saver({
path:""
});
assert.equal(true, s.published);
});
it("accepts incoming value", function() {
var s = new Saver({
path:"",
published: false
});
assert.equal(false, s.published);
});
});
describe("valid", function() {
it("false without data", function() {
var s = new Saver({
path:""
});
assert.equal(false, s.valid);
});
it("false without name", function() {
var s = new Saver({
description:"description",
path:""
});
assert.equal(false, s.valid);
});
it("false without description", function() {
var s = new Saver({
name:"name",
path:""
});
assert.equal(false, s.valid);
});
it("false if not published", function() {
var s = new Saver({
name:"name",
description:"description",
published:false,
path:""
});
assert.equal(false, s.valid);
});
it("true if published with name and description", function() {
var s = new Saver({
name:"name",
description:"description",
path:""
});
assert.equal(true, s.valid);
});
});
describe("settings", function() {
it("defaults to empty", function() {
var s = new Saver({
name:"name",
description:"description",
path:""
});
assert.deepEqual({}, s.settings);
});
it("accepts incoming options", function() {
var s = new Saver({
name:"name",
description:"description",
path:"",
options: [
{
"name": "density",
"type": "slider",
"description": "how dense?",
"min": "1",
"max": "100",
"default": "75"
}
]
});
assert.deepEqual({density:"75"}, s.settings);
});
it("accepts incoming options and user settings", function() {
var s = new Saver({
name:"name",
description:"description",
path:"",
options: [
{
"name": "density",
"type": "slider",
"description": "how dense?",
"min": "1",
"max": "100",
"default": "75"
}
],
settings: {
density:"100",
other:"hello"
}
});
assert.deepEqual({density:"100", other:"hello"}, s.settings);
});
});
describe("url", function() {
it("uses url", function() {
var s = new Saver({
name:"name",
description:"description",
url:"http://yahoo.com/",
path:""
});
assert.equal("http://yahoo.com/", s.url);
});
});
});
================================================
FILE: test/main/power.js
================================================
/* eslint-disable mocha/no-setup-in-describe */
"use strict";
import assert from 'assert';
import path from "path";
import fs from "fs-extra";
import Power from "../../src/main/power.js";
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
describe("Power", function() {
describe("charging", function() {
const loadFixture = (platform, type) => {
const f = path.join(__dirname, `../fixtures/power/${platform}-${type}.txt`);
return fs.readFileSync(f).toString();
};
let power;
describe("unhandled platform", function() {
it("works", async function() {
const power = new Power({platform: "beos"});
assert(await power.charging());
});
});
describe("linux", function() {
let platform;
beforeEach(function() {
platform = "linux";
power = new Power(platform);
});
it("is correct when charged", async function() {
assert(await power.charging(loadFixture(platform, "charged")));
});
it("is correct when charging", async function() {
assert(await power.charging(loadFixture(platform, "charging")));
});
it("is correct when discharging", async function() {
assert.strictEqual(false, await power.charging(loadFixture(platform, "discharging")));
});
});
["darwin", "win32"].forEach((platform) => {
describe(platform, function() {
beforeEach(function() {
const method = () => {
return false;
};
power = new Power({platform, method});
});
it("returns the reverse of the method", async function() {
assert.strictEqual(true, await power.charging());
});
});
});
});
});
================================================
FILE: test/main/release_check.js
================================================
"use strict";
import assert from 'assert';
import path from "path";
import nock from "nock";
import ReleaseCheck from "../../src/main/release_check.js";
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
describe("ReleaseCheck", function() {
let releaseChecker;
let version = "0.1.1";
let server = "https://sillynotreal.domain";
let uriPath = `/update/win32/${version}`;
let url = `${server}${uriPath}`;
let fixturePath;
beforeEach(function() {
fixturePath = path.join(__dirname, "../fixtures/releases/updates.json");
releaseChecker = new ReleaseCheck();
});
it("handles updates", function(done) {
nock(server).
get(uriPath).
replyWithFile(200, fixturePath, {
"Content-Type": "application/json",
});
releaseChecker.setFeed(url);
releaseChecker.onUpdate((result) => {
assert.equal("v0.9.26", result.name);
done();
});
releaseChecker.checkLatestRelease();
});
it("handles no updates", function(done) {
nock(server).
get(uriPath).
reply(204, () => {
return "";
});
releaseChecker.setFeed(url);
releaseChecker.onNoUpdate(() => {
done();
});
releaseChecker.checkLatestRelease();
});
});
================================================
FILE: test/main/state_manager.js
================================================
"use strict";
import assert from 'assert';
import sinon from "sinon";
import StateManager from "../../src/main/state_manager.js";
const fakeIdler = {
getIdleTime: () => { return 0; }
};
describe("StateManager", function() {
let hitIdle, hitBlank, hitReset;
let sandbox;
let stateManager;
beforeEach(function() {
stateManager = new StateManager();
hitIdle = false;
hitBlank = false;
hitReset = false;
sandbox = sinon.createSandbox();
stateManager.reset();
stateManager.setup({
idleTime: 100,
blankTime: 200,
onIdleTime: () => {
hitIdle = true;
},
onBlankTime: () => {
hitBlank = true;
},
onReset: () => {
hitReset = true;
}
});
});
afterEach(function() {
stateManager.stopTicking();
sandbox.restore();
});
it("does nothing", function(done) {
sandbox.stub(fakeIdler, "getIdleTime").returns(0.01);
stateManager.idleFn = fakeIdler.getIdleTime;
stateManager.tick(false);
setTimeout(() => {
assert(!hitIdle);
assert(!hitBlank);
//assert(!hitReset);
done();
}, 50);
});
it("idles", function(done) {
sandbox.stub(fakeIdler, "getIdleTime").returns(200);
stateManager.idleFn = fakeIdler.getIdleTime;
stateManager.tick(false);
setTimeout(() => {
assert(hitIdle);
assert(!hitBlank);
done();
}, 50);
});
it("blanks", function(done) {
sandbox.stub(fakeIdler, "getIdleTime").returns(1000);
stateManager.idleFn = fakeIdler.getIdleTime;
stateManager.switchState(stateManager.STATES.STATE_RUNNING);
stateManager.tick(false);
setTimeout(() => {
assert(hitBlank);
done();
}, 50);
});
it("resets", function(done) {
var idleCount = sandbox.stub(fakeIdler, "getIdleTime");
idleCount.onCall(0).returns(3);
stateManager.idleFn = fakeIdler.getIdleTime;
stateManager.switchState(stateManager.STATES.STATE_RUNNING);
setTimeout(() => {
stateManager.tick(false);
assert(hitReset);
done();
}, 50);
});
});
================================================
FILE: test/ui/about.js
================================================
/* eslint-disable mocha/no-setup-in-describe */
"use strict";
import assert from 'assert';
import * as helpers from "../helpers.js";
import fs from "fs";
const packageJSON = JSON.parse(fs.readFileSync("./package.json"));
var workingDir;
let app;
describe("About", function() {
helpers.setupTest(this);
beforeEach(async function() {
workingDir = helpers.getTempDir();
helpers.setupFullConfig(workingDir);
app = await helpers.application(workingDir, true);
await helpers.callIpc(app, "open-window about");
});
it("has some text and current version number", async function() {
const window = await helpers.waitFor(app, "about");
const elem = await window.$("body");
const text = await elem.innerText();
assert(text.lastIndexOf("// screensaver fun //") !== -1);
assert(text.lastIndexOf(packageJSON.version) !== -1);
});
});
================================================
FILE: test/ui/bootstrap.js
================================================
/* eslint-disable mocha/no-setup-in-describe */
"use strict";
import assert from 'assert';
import path from "path";
import fs from "fs-extra";
import * as tmp from "tmp";
import * as helpers from "../helpers.js";
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
describe("bootstrap", function() {
const saverZipSource = path.join(__dirname, "..", "fixtures", "test-savers.zip");
const saverData = path.join(__dirname, "..", "fixtures", "test-savers.json");
let configDest;
var workingDir;
let app;
let saverZip;
helpers.setupTest(this);
beforeEach(function() {
saverZip = path.join(tmp.dirSync().name, "test-savers.zip");
fs.copyFileSync(saverZipSource, saverZip);
workingDir = helpers.getTempDir();
configDest = path.join(workingDir, "config.json");
});
describe("without config", function() {
beforeEach(async function() {
assert(!fs.existsSync(configDest));
app = await helpers.application(workingDir, false, saverZip, saverData);
});
it("creates config file and shows prefs", async function() {
await helpers.waitFor(app, "prefs");
assert(fs.existsSync(configDest));
// the test was crashing without waiting here a bit
await helpers.sleep(1000);
});
});
describe("with invalid config", function() {
beforeEach(async function() {
const dest = path.join(workingDir, "config.json");
fs.copySync(
path.join(__dirname, "..", "fixtures", "bad-config.json"),
dest
);
app = await helpers.application(workingDir, true);
});
it("creates config file and shows prefs", async function() {
await helpers.waitFor(app, "prefs");
assert(fs.existsSync(configDest));
const data = JSON.parse(fs.readFileSync(configDest));
assert.deepStrictEqual(5, data.delay);
});
});
});
================================================
FILE: test/ui/editor.js
================================================
/* eslint-disable mocha/no-setup-in-describe */
"use strict";
import assert from 'assert';
import path from "path";
import fs from "fs-extra";
import * as helpers from "../helpers.js";
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
var workingDir;
let app;
let window;
describe("Editor", function() {
var saverJSON;
helpers.setupTest(this);
beforeEach(async function() {
workingDir = helpers.getTempDir();
var saversDir = helpers.getTempDir();
saverJSON = helpers.addSaver(saversDir, "saver-one", "saver.json");
app = await helpers.application(workingDir, true);
await helpers.callIpc(app, "open-window editor", {
screenshot: "file://" + path.join(__dirname, "../fixtures/screenshot.png"),
src: saverJSON
});
window = await helpers.waitFor(app, "editor");
});
it("edits basic settings", async function() {
const val = await window.inputValue("#saver-form [name='name']");
assert.strictEqual("Screensaver One", val);
await window.fill("#saver-form [name='name']", "A New Name!!!");
await window.fill("#saver-form [name='description']", "A Thing I Made?");
await window.click("button.save");
await helpers.sleep(100);
var x = JSON.parse(fs.readFileSync(saverJSON)).name;
assert.strictEqual(x, "A New Name!!!");
});
it("adds and removes options", async function() {
await window.fill(".saver-option-input[data-index='0'] [name='name']", "My Option");
await window.fill(".saver-option-input[data-index='0'] [name='description']", "An Option I Guess?");
await window.click("button.add-option");
await window.fill(".saver-option-input[data-index='1'] [name='name']", "My Second Option");
await window.fill(".saver-option-input[data-index='1'] [name='description']", "Another Option I Guess?");
await window.selectOption(".saver-option-input[data-index='1'] select", {label: "yes/no"});
await window.click("button.add-option");
await window.fill(".saver-option-input[data-index='2'] [name='name']", "My Third Option");
await window.fill(".saver-option-input[data-index='2'] [name='description']", "Here We Go Again");
await window.selectOption(".saver-option-input[data-index='2'] select", {label: "slider"});
await window.click("button.save");
await helpers.sleep(100);
var data = JSON.parse(fs.readFileSync(saverJSON));
var opt = data.options[0];
assert.strictEqual("My Option", opt.name);
assert.strictEqual("An Option I Guess?", opt.description);
assert.strictEqual("text", opt.type);
opt = data.options[1];
assert.strictEqual("My Second Option", opt.name);
assert.strictEqual("Another Option I Guess?", opt.description);
assert.strictEqual("boolean", opt.type);
opt = data.options[2];
assert.strictEqual("My Third Option", opt.name);
assert.strictEqual("Here We Go Again", opt.description);
assert.strictEqual("slider", opt.type);
await window.click(".saver-option-input[data-index='1'] button.remove-option");
await window.click("button.save");
await helpers.sleep(100);
data = JSON.parse(fs.readFileSync(saverJSON));
opt = data.options[0];
assert.strictEqual("My Option", opt.name);
assert.strictEqual("An Option I Guess?", opt.description);
assert.strictEqual("text", opt.type);
opt = data.options[1];
assert.strictEqual("My Third Option", opt.name);
assert.strictEqual("Here We Go Again", opt.description);
assert.strictEqual("slider", opt.type);
});
});
================================================
FILE: test/ui/new.js
================================================
/* eslint-disable mocha/no-setup-in-describe */
"use strict";
import assert from 'assert';
import path from "path";
import fs from "fs-extra";
import * as helpers from "../helpers.js";
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
var saversDir;
var workingDir;
let app;
describe("Add New", function() {
let screensaverUrl;
helpers.setupTest(this);
beforeEach(async function() {
screensaverUrl = "file://" + path.join(__dirname, "../fixtures/screenshot.png");
saversDir = helpers.getTempDir();
workingDir = helpers.getTempDir();
app = await helpers.application(workingDir, true);
});
// describe("when not setup", function() {
// beforeEach(async function() {
// await helpers.callIpc(app, `open-window add-new ${screensaverUrl}`);
// });
// it("shows alert if not setup", async function() {
// const window = await helpers.waitFor(app, "new");
// const elem = await window.$("body");
// const text = await elem.innerText();
// assert(text.lastIndexOf("set a local directory") !== -1);
// });
// it.skip("can set local source", async function() {
// await helpers.waitForWindow(app, windowTitle);
// await helpers.waitForText(app, "body", "set a local directory", true);
// await helpers.click(app, "button.pick");
// await helpers.click(app, "button.save");
// await helpers.sleep(100);
// assert.equal("/not/a/real/path", currentPrefs().localSource);
// const res = await helpers.getElementText(app, "body");
// assert(res.lastIndexOf("Use this form") !== -1);
// });
// });
describe("when setup", function() {
let window;
beforeEach(async function() {
helpers.addLocalSource(workingDir, saversDir);
await helpers.callIpc(app, `open-window add-new ${screensaverUrl}`);
window = await helpers.waitFor(app, "new");
});
it("creates screensaver and shows editor", async function() {
const src = path.join(saversDir, "a-new-name", "saver.json");
await window.fill("[name='name']", "A New Name");
await window.fill("[name='description']", "A Thing I Made?");
await window.click("button.save");
await helpers.waitFor(app, "editor");
assert(fs.existsSync(src));
});
});
});
================================================
FILE: test/ui/prefs.js
================================================
/* eslint-disable mocha/no-setup-in-describe */
"use strict";
import assert from 'assert';
import * as helpers from "../helpers.js";
import SaverPrefs from "../../src/lib/prefs.js";
let app;
var workingDir;
var saversDir;
describe("Prefs", function() {
helpers.setupTest(this);
let currentPrefs = function() {
return new SaverPrefs(workingDir);
};
let window;
beforeEach(async function() {
workingDir = helpers.getTempDir();
saversDir = helpers.getTempDir();
helpers.setupConfig(workingDir);
helpers.addLocalSource(workingDir, saversDir);
helpers.addSaver(saversDir, "saver-one", "saver.json");
app = await helpers.application(workingDir, true);
await helpers.callIpc(app, "open-window prefs");
window = await helpers.waitFor(app, "prefs");
});
it("lists screensavers", async function() {
await helpers.waitForText(window, "body", "Screensaver One", true);
});
it("lists included default screensavers", async function() {
await helpers.waitForText(window, "body", "Random", true);
});
it("allows picking a screensaver", async function() {
await helpers.waitForText(window, "body", "Screensaver One", true);
await window.click("text=Screensaver One");
await helpers.waitForText(window, ".saver-description", "A Screensaver", true);
await window.click("button.save");
await helpers.waitForText(window, "body", "Changes saved!", true);
console.log(currentPrefs().saver);
assert(currentPrefs().saver.lastIndexOf("saver-one") !== -1);
});
it("sets options for screensaver", async function() {
await helpers.waitForText(window, "body", "Screensaver One", true);
await window.click("text=Screensaver One");
await helpers.waitForText(window, "body", "Load the specified URL", true);
await window.click("[name='sound'][value='false']");
await window.fill("[name='load_url']", "barfoo");
await window.click("button.save");
await helpers.waitForText(window, "body", "Changes saved!", true);
var options = currentPrefs().options;
var k = Object.keys(options).find((i) => {
return i.indexOf("saver-one") !== -1;
});
assert.strictEqual("barfoo", options[k].load_url);
assert(!options[k].sound);
});
it("sets timing options", async function() {
await helpers.waitForText(window, "body", "Activate after", true);
await window.selectOption("[name=delay]", {label: "30 minutes"});
await window.selectOption("[name=sleep]", {label: "15 minutes"});
await window.click("button.save");
await helpers.waitForText(window, "body", "Changes saved!", true);
assert.strictEqual(30, currentPrefs().delay);
assert.strictEqual(15, currentPrefs().sleep);
});
});
================================================
FILE: test/ui/settings.js
================================================
/* eslint-disable mocha/no-setup-in-describe */
"use strict";
import assert from 'assert';
import * as helpers from "../helpers.js";
import SaverPrefs from "../../src/lib/prefs.js";
let app;
var workingDir;
var saversDir;
describe("Settings", function() {
const closeWindowDelay = 750;
let window;
helpers.setupTest(this);
let currentPrefs = function() {
return new SaverPrefs(workingDir).data;
};
beforeEach(async function() {
workingDir = helpers.getTempDir();
saversDir = helpers.getTempDir();
helpers.setupConfig(workingDir);
helpers.addLocalSource(workingDir, saversDir);
helpers.addSaver(saversDir, "saver-one", "saver.json");
app = await helpers.application(workingDir, true);
await helpers.callIpc(app, "open-window prefs");
await helpers.waitForWindow(app, "Before Dawn: Preferences");
window = await helpers.waitFor(app, "prefs");
await window.click("button.settings");
window = await helpers.waitFor(app, "settings");
});
it("toggles checkboxes", async function() {
let oldConfig = currentPrefs();
await window.check("text=Lock screen after running");
await window.check("text=Disable when on battery?");
await window.uncheck("text=Auto start on login?");
await window.uncheck("text=Only run on the primary display?");
await window.click("button.save");
await helpers.sleep(closeWindowDelay);
let updatedPrefs = currentPrefs();
assert.strictEqual(!oldConfig.lock, updatedPrefs.lock);
assert.strictEqual(!oldConfig.disableOnBattery, updatedPrefs.disableOnBattery);
assert.strictEqual(!oldConfig.auto_start, updatedPrefs.auto_start);
assert.strictEqual(!oldConfig.runOnSingleDisplay, updatedPrefs.runOnSingleDisplay);
await helpers.waitFor(app, "prefs");
});
it("leaves checkboxes", async function() {
let oldConfig = currentPrefs();
await window.click("button.save");
await helpers.sleep(closeWindowDelay);
let updatedPrefs = currentPrefs();
assert.strictEqual(oldConfig.lock, updatedPrefs.lock);
assert.strictEqual(oldConfig.disableOnBattery, updatedPrefs.disableOnBattery);
assert.strictEqual(oldConfig.auto_start, updatedPrefs.auto_start);
assert.strictEqual(oldConfig.runOnSingleDisplay, updatedPrefs.runOnSingleDisplay);
await helpers.waitFor(app, "prefs");
});
// it.skip("allows setting path via dialog", async function() {
// const [fileChooser] = await Promise.all([
// window.waitForEvent("filechooser"),
// window.click("button.pick")
// ]);
// await fileChooser.setFiles("/not/a/real/path");
// await window.click("button.save");
// await helpers.sleep(closeWindowDelay);
// assert.strictEqual("/not/a/real/path", currentPrefs().localSource);
// await helpers.waitFor(app, "prefs");
// });
it("clears localSource", async function() {
let ls = currentPrefs().localSource;
assert( ls != "" && ls !== undefined);
await window.click("button.clear");
await helpers.sleep(50);
await window.click("button.save");
await helpers.sleep(closeWindowDelay);
assert.strictEqual("", currentPrefs().localSource);
await helpers.waitFor(app, "prefs");
});
// // dialogs don't work yet
// // @see https://github.com/microsoft/playwright/issues/8278
// it.skip("resets defaults", async function() {
// window = await helpers.waitFor(app, "settings");
// window.on("dialog", async dialog => {
// console.log(dialog.message());
// await dialog.accept();
// });
// await window.click("button.reset-to-defaults");
// await helpers.waitForText(window, "body", "Settings reset", true);
// await helpers.sleep(closeWindowDelay);
// assert.strictEqual("", currentPrefs().localSource);
// await helpers.waitFor(app, "prefs");
// });
});
================================================
FILE: test/ui/tray.js
================================================
/* eslint-disable mocha/no-setup-in-describe */
"use strict";
import assert from 'assert';
import * as helpers from "../helpers.js";
describe("tray", function() {
var workingDir;
let saversDir;
let app;
let window;
helpers.setupTest(this);
beforeEach(async function() {
workingDir = helpers.getTempDir();
saversDir = helpers.getTempDir();
let saverJSONFile = helpers.addSaver(saversDir, "saver");
helpers.setupConfig(workingDir, "config", {
"firstLoad": false,
"sourceRepo": "",
"localSource": saversDir,
"saver": saverJSONFile
});
app = await helpers.application(workingDir, true);
await helpers.waitForWindow(app, 'test shim');
window = await helpers.getWindowByTitle(app, 'test shim');
});
describe("run now", function() {
it("opens screensaver", async function() {
await window.click("button.RunNow");
await helpers.waitForText(window, "#currentState", "running");
});
});
describe("preferences", function() {
it("opens prefs window", async function() {
await window.click("button.Preferences");
assert(await helpers.waitForWindow(app, "Before Dawn: Preferences"));
});
});
describe("about", function() {
it("opens about window", async function() {
await window.click("button.AboutBeforeDawn");
await helpers.waitForWindow(app, "Before Dawn: About!");
assert(await helpers.getWindowByTitle(app, "Before Dawn: About!"));
});
});
describe("enable/disable", function() {
it("toggles app status", async function() {
await helpers.waitForText(window, "body", "idle");
await window.click("button.Disable");
await helpers.waitForText(window, "body", "paused");
await window.click("button.Enable");
await helpers.waitForText(window, "body", "idle");
});
});
});
================================================
FILE: tools/build-packages.sh
================================================
#!/bin/bash
DEST="/tmp/before-dawn-packages"
TARGET="$1"
WORKING_DIR="/tmp/before-dawn-build"
REPO="https://github.com/muffinista/before-dawn.git"
START_DIR=`pwd`
echo "== Cleaning up $WORKING_DIR"
rm -rf $WORKING_DIR
mkdir -p $WORKING_DIR
if [ "$LOCAL_BUILD" == "1" ]; then
echo "== Building from local copy =="
cp -r . $WORKING_DIR/
else
echo "== Checking Out Code =="
git clone $REPO $WORKING_DIR
fi
cd $WORKING_DIR
echo "== Building assets =="
cd app
npm install --save-dev
grunt
rm -rf node_modules
cd ..
echo "== Cleaning out node packages =="
npm prune
echo "== Installing node packages =="
npm install
echo "== BUILDING APP =="
npm run dist
echo "== Copying to $START_DIR/dist"
mkdir -p "$START_DIR/dist"
cp -r $WORKING_DIR/dist/* "$START_DIR/dist"
================================================
FILE: tools/update-build-version.js
================================================
'use strict';
const fs = require('fs');
const path = require('path');
var version = JSON.parse(fs.readFileSync("package.json")).version;
console.log("Specifying v" + version);
var build = JSON.parse(fs.readFileSync("build.json"));
build.win.version = version;
build.osx.version = version;
build.linux.version = version;
console.log(build);
fs.writeFileSync("build.json", JSON.stringify(build, null, 4));
================================================
FILE: webpack.config.js
================================================
import mainConfig from "./webpack.main.config.js";
import rendererConfig from "./webpack.renderer.config.js";
export default [mainConfig, rendererConfig];
================================================
FILE: webpack.main.config.js
================================================
"use strict";
import * as path from "path";
import webpack from "webpack";
import "dotenv/config";
import CopyWebpackPlugin from "copy-webpack-plugin";
import { CleanWebpackPlugin } from "clean-webpack-plugin";
import { sentryWebpackPlugin } from "@sentry/webpack-plugin";
import ESLintPlugin from "eslint-webpack-plugin";
import { readFile } from 'fs/promises';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const packageJSON = JSON.parse(
await readFile(
new URL('./package.json', import.meta.url)
)
);
const dependencies = packageJSON.dependencies;
const optionalDependencies = packageJSON.optionalDependencies || {};
const outputDir = path.join(__dirname, "output");
const COMMIT_SHA = process.env.SENTRY_RELEASE || process.env.GITHUB_SHA;
//
// get a list of node dependencies, and then
// convert it to an array of package names
// this prevents some warnings like:
//
// Critical dependency: the request of a dependency is an expression
//
// and
//
// ERROR in ./src/main/fullscreen.js
// Module not found: Error: Can't resolve 'winctl'
//
// Basically, webpack falls down when you're including node modules
const deps = [].concat(
Object.keys(dependencies),
Object.keys(optionalDependencies)
);
let mainConfig = {
devtool: "source-map",
mode: (process.env.NODE_ENV === "production" ? "production" : "development"),
entry: {
main: path.join(__dirname, "src", "main", "index.js")
},
experiments: {
outputModule: true,
},
externals: deps,
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"]
}
}
},
{
test: /\.node$/,
use: "node-loader"
}
]
},
node: {
__dirname: false,
__filename: false
},
optimization: {
emitOnErrors: false,
nodeEnv: (process.env.NODE_ENV === "production" ? "production" : "development")
},
output: {
filename: "[name].js",
path: outputDir,
sourceMapFilename: "[name].js.map",
chunkFormat: "module",
module: true
},
plugins: [
new ESLintPlugin({
fix: false,
configType: 'flat'
}),
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: []
}),
new CopyWebpackPlugin({
patterns: [
{
from: path.join(__dirname, "package.json"),
to: path.join(outputDir)
},
{
from: path.join(__dirname, "src", "main", "assets"),
to: path.join(outputDir, "assets"),
},
{
from: path.join(__dirname, "src", "main", "system-savers"),
to: path.join(outputDir, "system-savers"),
}
]
})
],
resolve: {
extensions: [".js", ".json"],
fallback: {
"child_process": false,
"url": false,
"fs": false,
"path": false,
"os": false,
"stream": false,
"stream/promises": false,
}
},
target: "electron-main"
};
/**
* Adjust mainConfig for development/production settings
*/
if (process.env.NODE_ENV === "production") {
mainConfig.devtool = "source-map";
if ( process.env.SENTRY_DSN ) {
mainConfig.plugins.push(
new webpack.EnvironmentPlugin(["SENTRY_DSN"])
);
}
mainConfig.plugins.push(
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify("production"),
"process.env.BEFORE_DAWN_RELEASE_NAME": JSON.stringify(COMMIT_SHA),
})
);
if ( process.env.SENTRY_AUTH_TOKEN && !process.env.DISABLE_SENTRY ) {
mainConfig.plugins.push(
sentryWebpackPlugin({
include: "src",
ignoreFile: ".sentrycliignore",
ignore: ["node_modules", "webpack.config.js", "webpack.main.config.js", "webpack.renderer.config.js"],
org: "colin-mitchell",
project: "before-dawn",
authToken: process.env.SENTRY_AUTH_TOKEN,
release: COMMIT_SHA,
})
);
}
}
export default mainConfig;
================================================
FILE: webpack.renderer.config.js
================================================
"use strict";
import * as path from "path";
import webpack from "webpack";
import "dotenv/config";
import HtmlWebpackPlugin from "html-webpack-plugin";
import MiniCssExtractPlugin from "mini-css-extract-plugin";
import { sentryWebpackPlugin } from "@sentry/webpack-plugin";
import ESLintPlugin from "eslint-webpack-plugin";
import { readFile } from 'fs/promises';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const packageJSON = JSON.parse(
await readFile(
new URL('./package.json', import.meta.url)
)
);
const productName = packageJSON.productName;
const outputDir = path.join(__dirname, "output");
const COMMIT_SHA = process.env.SENTRY_RELEASE || process.env.GITHUB_SHA;
var htmlPageOptions = function(id, title) {
return {
filename: `${id}.html`,
template: path.resolve(__dirname, "src/index.ejs"),
id: id,
title: `${productName}: ${title}`,
minify: {
collapseWhitespace: false,
removeAttributeQuotes: false,
removeComments: false
}
};
};
/**
* List of node_modules to include in webpack bundle
*
* Required for specific packages like Vue UI libraries
* that provide pure *.vue files that need compiling
* https://simulatedgreg.gitbooks.io/electron-vue/content/en/webpack-configurations.html#white-listing-externals
*/
let rendererConfig = {
devtool: "source-map",
entry: {
renderer: path.join(__dirname, "src", "renderer", "main.js")
},
module: {
rules: [
{
test: /\.(sa|sc|c)ss$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader",
"sass-loader",
]
},
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"]
}
}
},
{
test: /\.svelte$/,
use: {
loader: "svelte-loader",
options: {
emitCss: true,
}
},
},
{
// required to prevent errors from Svelte on Webpack 5+, omit on Webpack 4
test: /node_modules\/svelte\/.*\.mjs$/,
resolve: {
fullySpecified: false
}
},
]
},
node: {
__dirname: false,
__filename: false
},
plugins: [
new ESLintPlugin({
fix: false,
configType: 'flat',
extensions: ["js"]
}),
new HtmlWebpackPlugin(htmlPageOptions("prefs", "Preferences")),
new HtmlWebpackPlugin(htmlPageOptions("settings", "Settings")),
new HtmlWebpackPlugin(htmlPageOptions("editor", "Editor")),
new HtmlWebpackPlugin(htmlPageOptions("new", "Create Screensaver!")),
new HtmlWebpackPlugin(htmlPageOptions("about", "About!")),
new MiniCssExtractPlugin({
filename: "[name].css",
chunkFilename: "[id].css"
}),
],
optimization: {
emitOnErrors: false,
nodeEnv: (process.env.NODE_ENV === "production" ? "production" : "development")
},
output: {
filename: "[name].js",
library: "[name]",
libraryTarget: "var",
path: outputDir,
publicPath: ""
},
mode: (process.env.NODE_ENV === "production" ? "production" : "development"),
resolve: {
alias: {
// handy alias for the root path of render files
"@": path.join(__dirname, "src", "renderer"),
"~": path.join(__dirname, "src")
},
extensions: [".js", ".json", ".css", ".svelte"],
conditionNames: ["svelte", "browser", "import"]
},
target: "web"
};
/**
* Adjust rendererConfig for production settings
*/
if (process.env.NODE_ENV === "production") {
rendererConfig.devtool = "source-map";
if ( process.env.SENTRY_DSN ) {
rendererConfig.plugins.push(
new webpack.EnvironmentPlugin(["SENTRY_DSN"])
);
}
rendererConfig.plugins.push(
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify("production"),
"process.env.BEFORE_DAWN_RELEASE_NAME": JSON.stringify(COMMIT_SHA),
}),
new webpack.LoaderOptionsPlugin({
minimize: true
})
);
if ( process.env.SENTRY_AUTH_TOKEN && !process.env.DISABLE_SENTRY ) {
rendererConfig.plugins.push(
sentryWebpackPlugin({
include: "src",
ignoreFile: ".sentrycliignore",
ignore: ["node_modules", "webpack.config.js", "webpack.main.config.js", "webpack.renderer.config.js"],
org: "colin-mitchell",
project: "before-dawn",
authToken: process.env.SENTRY_AUTH_TOKEN,
release: COMMIT_SHA,
})
);
}
}
export default rendererConfig;