How to build a JavaScript Desktop App That Saves Windows Spotlight Images; An Electron Story

April 12, 2018 0 Comments

How to build a JavaScript Desktop App That Saves Windows Spotlight Images; An Electron Story

 

 

One of the ways my computer shows me she’s missed me is by greeting me with a gorgeous Windows Spotlight lock screen image when I log in. Flattering as that is, if one such image caught my eye, the chances of me seeing it again are slim since Windows automatically deletes it from the system (eventually). Well, I decided to make a simple desktop app that not only saves the images but lets you use one of them as the desktop wallpaper.

I suffer from a severe case of JavaScript fever so the obvious move was to use Github’s Electron framework to make the desktop app. Since this is going to be a Windows only app, another no-brainer was to use WinJs (an open source JavaScript library developed by Microsoft) for a truly native look. Now that we’ve got our toolbox ready it’s time to draw up a clear game plan. The apps mission (should it accept it #007) is to:

  1. Get Spotlight Lock Screen images from where they’re stored.
  2. Copy the image files to a “safe” folder before the system deletes them.
  3. Allow the user to choose and set one as their wallpaper.
  4. Repeat 1 and 2 regularly to save new images whenever Windows downloads them

That doesn’t sound too bad. Enough chit-chat, UNLEASH THE CODE!

Unleash the code!!!

Create a folder where your code will reside and using the command prompt or Powershell navigate to that folder. Cool tip: Write “cmd” or “powershell” in the address bar of the folder to open the command line interface at that folder’s directory. Assuming that you have Node and NPM installed (follow this simple guide if you haven’t), run the following command:

npm init

Fill in the requested information or press enter until the process is over to create a package.json file with default info in it. Open the package.json file in your favourite text editor (mine is VS Code), and edit the scripts key such that the file looks something like this:

{
"name": "spotlight-saver",
"version": "1.0.0",
"description": "Simple JavaScript Desktop App That Stores Windows Spotlight Images",
"main": "main.js",
"scripts": {
"start": "electron ."
},
"author": "JamzyKimani",
"license": "ISC",
}

Now let’s install electron to the app. Run this command line script in the apps directory:

npm install electron --save-dev

After that’s done, we need to create our apps main entry point (the main process for all you Electronites). Paste the following code in a new file on your code editor and save it as main.js inside the app folder:

const {app, BrowserWindow, dialog, ipcMain, shell, Tray, Menu} = require('electron')
const path = require('path')
const url = require('url')
let win
function createWindow () {
// Create the browser window.
win = new BrowserWindow({width: 800, height: 600})
// and load the index.html of the app.
win.loadURL(url.format({
//here's where we load the index.html file
pathname: path.join(_dirname, './src/index.html'),
protocol: 'file:',
slashes: true
}))
// Open the DevTools.
win.webContents.openDevTools()
// Emitted when the window is closed.
win.on('closed', () => {
win = null
})
//removes default main menu for the app
Menu.setApplicationMenu(null);
}
app.on('ready', createWindow)
// Quit when all windows are closed.
app.on('window-all-closed', () => {
//no need to check for darwin (macOS) this is a windows only app
app.quit()
})
app.on('activate', () => {
if (win === null) {
createWindow()
}
})

The above code is an abridged version of the “First Electron App” tutorial’s main.js file. Check that out if you’re new to Electron. Let’s create a more organized file structure in perpetration for the code that will drive the UI part of our app. Modify the application’s directory to look like this:

“Spotlight-Saver” is the apps main folder. Nodemodules folder is automatically created by NPM. We haven’t created the main.css, index.js and index.html. Let’s do that next.

You might be wondering when we made some of the files. Don’t worry, you didn’t miss a step. Let’s make them now, starting with the index.html file.

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Windows Lockscreen Image Saver</title>
<link rel="stylesheet" href="./css/main.css" />

</head>
<body>
<script src="./js/index.js" ></script>
</body>
</html>

That’s a basic HTML template. It’s doesn’t do a whole lot (or anything for that matter) so let’s change that. Ain’t nobody got time for writing custom HTML/CSS if they don’t have to, so in comes WinJs. Run this command in cmd/Powershell under the apps root directory:

npm install winjs  --save

I was able to quickly figure out how to create a Windows-style splitview (that’s a “navigation drawer” for all you material design folks) while messing around in the WinJs playground site. For it to work you need to import WinJs stylesheets into the index.html file such that the <head> tag looks something like this:

<head>
<meta charset="UTF-8">
<title>Windows Lockscreen Image Saver</title>
<link rel="stylesheet" href="./css/main.css" />
<link href="../nodemodules/winjs/css/ui-dark.css" rel="stylesheet" />
</head>

Also import WinJs scripts towards the end of the body tag to make the following markup work properly. The <body> tag in the index.html file now looks like:

We also need to import the WinJs module and instantiate it in the src/js/index.js file. Create the file and paste the following code inside it:

const {ipcRenderer, electron} = require('electron');
const WinJS = require('winjs')
WinJS.UI.processAll().done(function () {
var splitView = document.querySelector(".splitView").winControl;
});

Finally, the css (src/css/main.css) is the bow that wraps this wonderful Win.Js present together (most of this is pasted verbatim from the WinJs playground):

Let’s preview how the app looks so far. Open the command line at the root directory of the app (cool tip: Ctrl+in VS code opens a command line in the code editor&#x1F525; ) and execute the following:</p><pre id="fa3a" class="graf graf--pre graf-after--p">npm start</pre><p id="6c2b" class="graf graf--p graf-after--pre">The app should start up and if all went well, this is what you should see:</p><figure id="df57" class="graf graf--figure graf-after--p"><img class="progressiveMedia-noscript js-progressiveMedia-inner" src="https://cdn-images-1.medium.com/max/1600/1*YKTVozCp1zSJ9-GgAbPWSQ.gif"></figure><p id="2b32" class="graf graf--p graf-after--figure">That&#x2019;s 80% of the UI done and dusted, but we haven&#x2019;t yet tackled any of the objectives we outlined when we started. Let&#x2019;s take care of objectives <strong class="markup--strong markup--p-strong">1</strong> and <strong class="markup--strong markup--p-strong">2 </strong>next. A quick Web search reveals that the Spotlight images hide out in the folder below:</p><pre id="4c0d" class="graf graf--pre graf-after--p"><em class="markup--em markup--pre-em">\AppData\Local\Packages\Microsoft.Windows.ContentDeliveryManager_cw5n1h2txyewy\LocalState\Assets</em></pre><figure id="fd10" class="graf graf--figure graf-after--pre"><img class="progressiveMedia-noscript js-progressiveMedia-inner" src="https://cdn-images-1.medium.com/max/1600/1*p4XaFQu_7ytU-G-TaNuKrw.jpeg"><figcaption class="imageCaption">The folder where Windows Spotlight Lockscreen images are stored looks like&#xA0;this</figcaption></figure><p id="92d2" class="graf graf--p graf-after--figure">The good news is the images are definitely in this folder; The bad news is they are hidden within a clutter of extensionless files with completely cryptic filenames. It&#x2019;s our apps job to sift through the clutter and get the images. Let&#x2019;s get to it.</p><p id="43c0" class="graf graf--p graf-after--p">Since every Electron app has full access to Node.js core modules, let&#x2019;s import the <em class="markup--em markup--p-em">fs </em>(file system) module and <em class="markup--em markup--p-em">os</em> (operating system) modules to our<em class="markup--em markup--p-em"> main.js </em>file. These modules will allow easy access to relevant files and folders on our system. Let&#x2019;s also define the path to the Spotlight images folder. The imports section of the<em class="markup--em markup--p-em"> main.js</em> file should now look like this:</p><pre id="5260" class="graf graf--pre graf-after--p">const {app, BrowserWindow, dialog, ipcMain, shell, Tray, Menu} = require(&apos;electron&apos;)<br>const path = require(&apos;path&apos;)<br>const url = require(&apos;url&apos;)<br>const fs = require(&apos;fs&apos;)<br>const os = require(&apos;os&apos;)</pre><pre id="48c7" class="graf graf--pre graf-after--pre">//gets the username of the os&apos;s logged in user<br>var username = os.userInfo().username;<br>var spotlightFolder =<br>C:/Users/${username}/AppData/Local/Packages/Microsoft.Windows.ContentDeliveryManagercw5n1h2txyewy/LocalState/Assets;</pre><p id="e7a1" class="graf graf--p graf-after--pre">Now define the path to the folder where the images will be stored (mine will be in the user&#x2019;s <em class="markup--em markup--p-em">Pictures </em>folder inside a sub-folder named &#x201C;Spotlight_Images&#x201D;). This is achieved by a function that creates the folder if it doesn&#x2019;t exist and returns the directory path.</p><figure id="4e8b" class="graf graf--figure graf--iframe graf-after--p"><div class="aspectRatioPlaceholder is-locked"></div></figure><p id="81f4" class="graf graf--p graf-after--figure">The system folder that contains the Windows Spotlight images also contains portrait-style images, tiny icons and some non-image files which we&#x2019;re not interested in. To help us get only the files we actually want, let&#x2019;s import a powerfull image processing module, <em class="markup--em markup--p-em">Jimp, </em>into our app. While <em class="markup--em markup--p-em">Jimp</em> is mainly used for editing and transforming images on the fly, it will also help us to analyze images in the Spotlight images folder so that we can ignore those we don&#x2019;t need. It&#x2019;s main advantage in this case is that it can read image files even if they lack file extensions. Execute the following command line prompt in the apps root folder to install <em class="markup--em markup--p-em">Jimp</em> into the project.</p><pre id="4bc5" class="graf graf--pre graf-after--p">npm install jimp --save</pre><p id="e6d3" class="graf graf--p graf-after--pre">Also remember to import <em class="markup--em markup--p-em">Jimp</em> in the<em class="markup--em markup--p-em"> main.js </em>imports section<em class="markup--em markup--p-em">.</em></p><pre id="3a9e" class="graf graf--pre graf-after--p">...<br>const fs = require(&apos;fs&apos;)<br>const os = require(&apos;os&apos;)<br>const Jimp = require(&apos;jimp&apos;)</pre><p id="9b17" class="graf graf--p graf-after--pre">Time to get those wallpaper image files to the app&#x2019;s images folder. Paste these two functions at the bottom of the <em class="markup--em markup--p-em">main.js </em>file.</p><figure id="5085" class="graf graf--figure graf--iframe graf-after--p"><div class="aspectRatioPlaceholder is-locked"></div></figure><p id="fa85" class="graf graf--p graf-after--figure">The functions look a bit verbose but it&#x2019;s because I made an effort to explain virtually every line (don&#x2019;t be intimidated). Since the Spotlight folder files don&#x2019;t have file extensions the <em class="markup--em markup--p-em">readAnonymFile()</em> function uses <em class="markup--em markup--p-em">Jimp</em> to parse through the file. If it encounters an error while reading the file, that means the file isn&#x2019;t an image file. A non-image file is marked with &#x2018;status: &#x201C;reject&#x201D;&#x2019; and is later ignored in the <em class="markup--em markup--p-em">updateImagesFolder(). </em>Brush up on Promise functions or read through the <a href="https://github.com/oliver-moran/jimp/blob/master/README.md" class="markup--anchor markup--p-anchor"><em class="markup--em markup--p-em">jimp readme</em></a> file if you can&#x2019;t understand any of the functions.</p><p id="68e8" class="graf graf--p graf-after--p">Now that the functions are defined, add the following line at the very end of the <em class="markup--em markup--p-em">createWindow</em> function in the <em class="markup--em markup--p-em">main.js</em> file:</p><pre id="b126" class="graf graf--pre graf-after--p">function createWindow () {<br> ...<br> Menu.setApplicationMenu(null);<br> updateImagesFolder(appImgsFolder,spotlightFolder)//&lt;-add this line <br>}<br>app.on(&apos;ready&apos;, createWindow)</pre><p id="b40a" class="graf graf--p graf-after--pre">If you run the app at this point while the app&#x2019;s images folder is open you will be able to see wonderful Spotlight images showing up in the folder. Here&#x2019;s how mine turned out:</p><figure id="b258" class="graf graf--figure graf-after--p"><img class="progressiveMedia-noscript js-progressiveMedia-inner" src="https://cdn-images-1.medium.com/max/1600/1*fGzcq3NBNZmbJkzl206j4A.gif"><figcaption class="imageCaption">Hope the commentary makes up for the picture&#xA0;quality</figcaption></figure><p id="4d37" class="graf graf--p graf-after--figure">With that, objectives <strong class="markup--strong markup--p-strong">1</strong> and <strong class="markup--strong markup--p-strong">2</strong> on our mission statement are done&#x1F387;&#x1F387;&#x1F387;. Now it&#x2019;s <strong class="markup--strong markup--p-strong">objective</strong> <strong class="markup--strong markup--p-strong">3</strong>&#x2019;s turn. First, the app needs to show a gallery of the images. We need to listen for the renderer&#x2019;s &#x201C;<em class="markup--em markup--p-em">did-finish-load</em>&#x201D; event and send the window a triggering event so it can begin showing the images. Let&#x2019;s add the &#x201C;<em class="markup--em markup--p-em">did-finish-load</em>&#x201D; event listener inside the <em class="markup--em markup--p-em">createWindow </em>function:</p><pre id="6a30" class="graf graf--pre graf-after--p">function createWindow () {<br> //...some other code is here<br> Menu.setApplicationMenu(null);<br> updateImagesFolder(appImgsFolder, spotlightFolder)</pre><pre id="5a0b" class="graf graf--pre graf-after--pre">win.webContents.on(&apos;did-finish-load&apos;, () =&gt; {</pre><pre id="7e1e" class="graf graf--pre graf-after--pre">//fetch filenames in the images folder after it has been updated<br> var imgsFolderFiles = fs.readdirSync(appImgsFolder);</pre><pre id="7fdf" class="graf graf--pre graf-after--pre">//payload defines the message we send to the ui window<br> var payload = {<br> imgsFolder : appImgsFolder, <br> imgsFolderFiles : imgsFolderFiles }<br> <br> //msg sent as an event called &apos;refresh images&apos; with the payload<br> win.webContents.send(&apos;refreshImages&apos;, payload );<br> })<br>}</pre><p id="0118" class="graf graf--p graf-after--pre">We also need to listen to the event sent by the main process within the <em class="markup--em markup--p-em">/src/js/index.js </em>file. Edit the <em class="markup--em markup--p-em">/src/js/index.js </em>file such that it looks like this</p><figure id="efd3" class="graf graf--figure graf--iframe graf-after--p"><div class="aspectRatioPlaceholder is-locked"></div></figure><p id="1429" class="graf graf--p graf-after--figure">Notice how we use Electron&#x2019;s ipc (inter-process communicator) to listen to the &#x2018;refreshImages&#x2019; event sent from the <em class="markup--em markup--p-em">main.js</em> file. The event handler then calls the <em class="markup--em markup--p-em">refreshImages</em> function which renders the images on the app window. Well, not quiet. We still need to style the images so that they can show up on the screen. Add the following lines of ccs to the <em class="markup--em markup--p-em">src/css/main.css</em> file:</p><pre id="5d11" class="graf graf--pre graf-after--p">/*add to the end of src/css/main.css file */<br>.gallery-img {<br> transition: 0.3s;<br> flex-grow: 1;<br> width: 240px;<br> height: 180px;<br> max-height: 180px;<br> margin-right: 20px;<br> margin-top: 20px;<br> background-color: black;<br> position: relative;<br> }<br> <br>.gallery-img:last-child{<br> margin-right: 20px;<br> margin-top: 20px;<br> max-width: 47%;<br> min-width: 47%;<br> margin-bottom: 50px;<br>}</pre><pre id="727a" class="graf graf--pre graf-after--pre">@media screen and (max-width: 650px) {<br> .gallery-img:last-child{<br> min-width: 97%;<br> margin-bottom: 50px;<br> }<br>}<br> <br>.gallery-img:hover {<br> box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19)<br>}</pre><p id="9ab7" class="graf graf--p graf-after--pre">With that your app should look like this:</p><figure id="2e03" class="graf graf--figure graf-after--p"><img class="progressiveMedia-noscript js-progressiveMedia-inner" src="https://cdn-images-1.medium.com/max/1600/1*0Je_5NMsMcp9zyVVzh7taw.jpeg"><figcaption class="imageCaption">Of course your&#x2019;s most likely will have different images</figcaption></figure><p id="3dab" class="graf graf--p graf--startsWithDoubleQuote graf-after--figure">&#x201C;It looks great&#x2026; But what if the user wants to set the a desktop image?&#x201D; I&#x2019;m glad you asked. Let&#x2019;s tweak the <em class="markup--em markup--p-em">/src/js/index.js </em>code and introduce a button that does just that. Edit the following line (it&#x2019;s inside the <em class="markup--em markup--p-em">refreshImages</em> function)&#x2026;</p><pre id="2d66" class="graf graf--pre graf-after--p">imgsHTML += <br><div class="gallery-img"
style="background-image:url('${imagesFolder}/${imageFile}');
background-size: 100% 100%;" >
</div>
`

to this:

We’ve introduced a button with a click event handler function called “setAsWallpaper”. There’s now also a mouseout and a mouseover event on the .gallery-image div so that the button will only be visible when you hover over a picture. Let’s add the functions that are triggered by the events to the /src/js/index.js (paste anywhere in the file):

Here’s the button’s css (add to /src/css/main.css).

.btn {
border: none;
color: white;
text-align: center;
vertical-align: middle;
line-height: 50px;
font-size: 14px;
cursor: pointer;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}

.info {background-color: #2196F3; width: 100%; height: 0px; opacity: 0; transition: 0.3s; position: absolute; bottom: 0; left: 0} /* Blue /
.info:hover {background: #0b7dda;}

You might have noticed that the setAsWallpaper function sends an event to the main.js file. Let’s handle that event. We need to install an NPM package called Wallpaper for that event to work. The module let’s us change the desktop wallpaper. Install it via the command line and require it in the main.js file:

//perform this in command prompt/powershell
npm install --save wallpaper
-------------------------------------------------------
//add this line to the main.js file, near the top:
const wallpaper = require('wallpaper')

Let’s handle the ‘changeDesktopWallpaper’ event in the main.js file:

/* add to the bottom of the main.js file /
ipcMain.on('changeDesktopWallpaper',(event, imgPath) => {
wallpaper.set(imgPath)
})

With that, you should have a little something like this (and that’s Objective 3 done ):

Now we’re cooking with gasoline!!! 🔥🔥🔥

For the final objective we need the app to update the images folder even when the user hasn’t opened the app. The most efficient way to do this is to set it up such that the app runs the update script when the user logs into the machine (on Windows startup). For that let’s use the Auto Launch NPM package.

//perform this in the command prompt/powershell
npm install auto-launch --save
-------------------------------------------------------
//add this code block to the main.js file, near the top:
const AutoLaunch = require('auto-launch');
var appAutoLauncher = new AutoLaunch({
name: 'spotlight-image-saver',
isHidden: true,
});
appAutoLauncher.isEnabled()
.then(function(isEnabled){
if(isEnabled){
return;
}
appAutoLauncher.enable();
})
.catch(function(err){
console.log(err)
});

DISCLAIMER: The auto-launch package doesn’t work (as expected) in development mode but it works perfectly when the app is already packed and has a .exe launcher. However, there is a simple way to test your script in the dev environment.

KINDA TECHNICAL: If you look at the script we’ve just added to main.js, you’ll notice that the options object passed to the new AutoLaunch() class instanctor has an isHidden parameter. This means that when the app is opened by Auto-Launch it is flagged with a --hidden argument which can be accessed via node’s process.argv object. We can manually set this --hidden argument to simulate how the app will behave during an Auto-Launch startup. To do this, run the following line instead of npm start (use Powershell to run this if you have trouble running it with Cmd) :

./nodemodules/.bin/electron main.js --hidden

As of now this special startup won’t look any different from the npm start startup since we haven’t checked for the --hidden argument in code. What you practically need to do is check if the --hidden argument was passed (this signifies that Auto-Launch has started your app) and perform auto-launch specific functions (i.e just update the images folder and shut down; The UI should not even be visible).

To achieve this we, edit the createWindow function to cater for the two (auto-launch and user-initiated) possible launch eventualities. The final main.js script looks like this (I’ve also added event handlers for the “open images folder” and “about software” splitview buttons):

/ ADD THESE TWO EVENT LISTENERS TO src/js/index.js 
they are click handlers for the splitview buttons **/
document.getElementById('openFolderBtn').addEventListener('click', function(){
ipcRenderer.send('openImagesFolder')
})
document.getElementById('aboutSoftwareBtn').addEventListener('click', function(){
ipcRenderer.send('showAboutInfo')
})

The edits enable the app to start silently in the background with only a tray icon visible. Save the icon below in the root folder of you app and rename it to app-icon.png for the tray icon to work.

Download above image and save it as app-icon.png in your apps root directory. Or make your own icon .

The app just updates the images folder and closes. The gif below shows the app’s behavior when the alternate startup is used. Notice the console script and logs, the tray icon, and the images folder being updated.

To replicate the video results just delete the images in your apps folder and run the script.

Final Steps

That’s it! All the objectives are complete. The only remaining step is to package our app (make an .exe file for it). For that we’ll use the electron-packager NPM module. Execute the following commands:

npm install electron-packager -g
npm install electron-packager -D

The first line saves electron-packager globally in your computer while the second line installs it in your app. Next execute the following command in your apps root folder:

electron-packager . --platform=win32

When it’s done you’ll notice a new folder in the root of your project. Inside that folder is a .exe file with your apps name on it. Click on the .exe launcher to start up your packaged app. That’s it! You’re app is packaged and now Auto Launcher will work as expected.

Conclusion:

Well, that’s it for this article but that doesn’t mean you should stop here. There are many ways you can improve this app. For example, use Jimp to add awesome filters to the images or create a desktop wallpaper slideshow. Oops, I’ve already done that last one. Check out a slightly more advanced version of this app here (star please) or get all the source files for this one here. Please share your improvements with me (@jamzyKimani on Twitter) or make pull requests on mine so that we can share with others. Thanks, and happy coding.

!IMPORTANT: When I said that the Auto Launch module doesn’t work in development mode what I meant was that during start up it starts the electron app located here:

[path to your app folder]\nodemodules\electron\dist\electron.exe

This is because your app isn’t packaged yet and doesn’t have a launcher of it’s own. You’ll notice that the above mentioned app will start automatically when you log in. To prevent this from happening I recommend you comment out the code block below in the main.js file until the app is ready to be packaged:

appAutoLauncher.isEnabled()
.then(function(isEnabled){
if(isEnabled){
return;
}
appAutoLauncher.enable();
})
.catch(function(err){
console.log(err)
});

If you’ve already noticed this unusual startup behavior, search for “regedit” in the Search Windows field next to the Windows start menu button and navigate to:

\HKEYCURRENTUSER\Software\Microsoft\Windows\CurrentVersion\Run

Delete the registry key named “electron” and that should take care of it.


Tag cloud