Follow me on my blog shymean.com

IPic is a great app for quickly uploading images to a map bed. Since non-members can only use the free Sina chart bed, because of the recent sina chart bed anti-theft chain and the validity of the picture, so I decided to implement a quick picture upload application.

After a brief comparison with Flutter Desktop, PyQT, and Electron, we decided to use Electron and quickly uploaded the clipboard images to Qiniu (non-advertising ~) in two or three nights.

This paper will review the entire development process and record the experience of developing Electron for the first time.

The complete project code is now available on Github.

The preparatory work

Development safety environment

Electric-forge is a scaffold for developing, packaging, and releasing electron, starting with installing electron and electric-Forge

# global install
npm install -g electron
npm install -g electron-forge

Initialize the project
electron-forge init oPic
#... Initialization may take a bit longer
Copy the code

Electron – Forge generates the basic project template for us.

If you are developing a GUI-like application, you can modify the relevant view file in SRC /index.html to experience developing a desktop application using Web technologies. If frameworks such as Vue and React are introduced, you can directly replace the file:// file with the webpack-dev-server service URL in the development environment

// mainWindow.loadURL(`file://${__dirname}/index.html`);
mainWindow.loadURL(`http://localhost:8080`);
Copy the code

Need to comb

Because this development target is the tool class application, rarely involves the UI level of development, sort out the requirements of the whole tool

  • Click the application icon in the top taskbar to display the pictures in the clipboard (such as copied image files, screenshots, etc.). The following is the working page of iPic

  • Click the image to be uploaded, and the background uploads the image to the picture bed, and automatically fills the IMAGE URL into the clipboard

    • Diagram bed configuration needs to be provided
    • For the capacity and flow of the graph bed, it is desirable to compress the image before uploading
  • Update the list of uploaded pictures. Click the uploaded picture, and the URL of the picture will be copied again

The whole requirement is relatively simple, mainly need to check the following interfaces in the Electron document

  • Display the app icon in the Apps bar at the top of the Mac and tap to bring up the options menu
  • Get clipboard image information and customize options menu bar to display images
  • Map bed configuration popover UI development, interaction with the main process
  • I wrote an img_qiniu_cDN a long time ago, which seems to still work
  • Image compression, originally wanted to use TinyPng API, found the number of times limit ~ find another library

That’s about it. Let’s start coding

The development of

Top App bar

Look at the system Tray API and construct a Tray instance

import { Tray } from "electron";
// Create the top icon
const createTray = app= > {
  	// upload@3x is the icon displayed on the tray
    const icon16 = path.resolve(__dirname, ".. /assets/[email protected]");
    const tray = new Tray(icon16);
    // Listen for the icon click event to open the options menu
    tray.on("click", () = > {const config = configUtil.getConfig();
        const template = [
            { label: "Waiting to upload".type: "normal".enabled: false },
            { label: "".type: "separator" },
            { label: "Uploaded".type: "normal".enabled: false },
            { label: "".type: "separator"}];// Todo build image option

        // Create contextMenu and pop up
        const contextMenu = Menu.buildFromTemplate(template);
        tray.popUpContextMenu(contextMenu);
    });
};
Copy the code

Gets the clipboard picture

reference

  • The clipboard API, the use ofclipboard.readImageGets the picture in the clipboard
  • NativeImage.readImageYou get an NativeImage wrapper object and need to look at the conversion between it and the original image file
import { clipboard } from "electron";

const uploadList = []; // Save the uploaded image in memory
const clipboardImageList = []; // Save the recently uploaded images

// Create a menuItem based on the clipboard image
const createClipboardImageItem = (a)= > {
    const clipboardImage = clipboard.readImage();
    // If there is any data on the clipboard, save it in clipboardImageList
    if(clipboardImage && ! clipboardImage.isEmpty()) {const radio = clipboardImage.getAspectRatio();
        // Create an icon to display in the menu bar
        const img = clipboardImage.resize({
            width: 100.height: radio / 100
        });

        // Temporarily store the image in clipboardImageList
        addToImageList(clipboardImageList, { img, raw: clipboardImage }, 1);
    }

    return clipboardImageList.map((row, index) = > {
        const { raw, img } = row;
        // Execute upload when clicking menu options

        const upload = (a)= > {
            const buffer = raw.toPNG();
            // Call uploadBufferImage to upload the image
            uploadBufferImage(buffer).then(url= > {
                // Update the list
                addToImageList(uploadList, { img, url });
                removeFromClipboardList(img);

                // Automatically copy the URL
                copyUrl(url);
                Util.showNotify('Upload to seven cows successfully, link${url}Has been copied to the clipboard);
            });
        };
        // Return to menu bar configuration
        return {
            label: (index + 1).toString(),
            icon: row.img, // The miniaturized image is passed to the icon configuration so that it can be displayed in the menu bar
            type: "normal".click: upload
        };
    });
};

// Create a record of uploaded images
const createUploadItem = (a)= >
    uploadList.map(({ img, url }, index) = > {
        const handler = (a)= > {
            const text = copyUrl(url);
            Util.showNotify(` link${text}Has been copied to the clipboard);
        };
        return {
            label: (index + 1).toString(),
            icon: img,
            type: "normal".click: handler
        };
    });
Copy the code

Seven cattle configuration

The application is lightweight enough to store data conveniently without using tools such as Nedb and simply save local files.

When you click the configuration item on the menu bar, a configuration window will pop up to fill in the configuration item, and the data will be saved in a local file when you confirm.

let settingWindow;
const openSettingWindow = (a)= > {
    settingWindow = new BrowserWindow({
        width: 600.height: 400
    });
    // Render a page using electron
    const url = `file://${path.resolve(__dirname, "./setting.html")}`;
    settingWindow.loadURL(url);
    // settingWindow.webContents.openDevTools();
    settingWindow.on("closed", () => {
        settingWindow = null;
    });
};

const template = [
    / /... Add a menu option
    { label: "Preferences".type: "normal".click: openSettingWindow }
];
Copy the code

In setting.html, implement a form submission page,

The configuration is then read and saved using the packaged local storage tool configUtil

const defaultConfig = {
    autoMarkdown: true.upload: {
        // Make sure that you have a nice bed
        qiNiu: {
            accessKey: "".secretKey: "".bucket: ""./ / warehouse
            host: "" // Resource domain name}}};const configFile = ".. /config.json";
// Get the configuration
function getConfig() {
    try {
        return require(configFile);
    } catch (e) {
        returndefaultConfig; }}// Save the configuration
function saveConfig(config) {
    const fileName = path.resolve(__dirname, configFile);
    return fs.writeFile(fileName, JSON.stringify(config));
}
Copy the code

In addition to the Map configuration, configUtil can also store application preferences, and is designed to support subsequent extensions to other map beds (although I’m using seven niu for now).

Image upload

Back to the previous uploadBufferImage method, since there is no way to directly upload the Electron NativeImage, the implementation idea here is as follows:

First read the seven Cow configuration, then write the buffer to a local temporary file, then upload the file to the server via the Qiniu SDK, and finally delete the temporary file

function qiNiuUpload(img) {
    try {
        const { upload: uploadConfig } = configUtil.getConfig();
        const upload = createUploadQiNiu(uploadConfig.qiNiu);

        return upload(img);
    } catch (e) {
        console.log("Missing config.json configuration file");
        return Promise.reject(e); }}// Upload the binary file
async function uploadBufferImage(buffer) {
    // Write a temporary image
    const fileName = `The ${Date.now()}_The ${Math.floor(Math.random() * 1000)}`;
    const filePath = path.resolve(__dirname, `.. /tmp/${fileName}.png`);

    await fs.writeFile(filePath, buffer); // Create temporary local files
    const url = await qiNiuUpload(filePath); // upload to qox
    await fs.unlinkSync(filePath); // Delete temporary files
    return url;
}
Copy the code

The createUploadQiNiu method encapsulates qiniuSDK, which was created three years ago

const qiniu = require("qiniu");
const path = require("path");
const createUploadQiNiu = opts= > {
    const { accessKey, secretKey, bucket, host } = opts;

    return filePath= > {
        const key = `oPic/${path.basename(filePath)}`;

        // Set the upload policy
        const putPolicy = new qiniu.rs.PutPolicy({
            scope: `${bucket}:${key}`
        });

        // Create an authentication object MAC based on the key to obtain the upload token
        const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
        const uploadToken = putPolicy.uploadToken(mac);

        // Configure objects
        const config = new qiniu.conf.Config();
        Z2 is south China
        config.zone = qiniu.zone.Zone_z2;

        // Extension parameters, mainly used for file fragment upload, can be ignored here
        const putExtra = new qiniu.form_up.PutExtra();

        // instantiate the upload object
        const formUploader = new qiniu.form_up.FormUploader(config);

        return new Promise((resolve, reject) = > {
            formUploader.putFile(
                uploadToken,
                key,
                filePath,
                putExtra,
                (respErr, respBody, respInfo) => {
                    if (respErr) {
                        reject(respErr);
                    }
                    if (respInfo && respInfo.statusCode === 200) {
                        // Concatenate the server path
                        const filename = host + key;
                        resolve(filename);
                    } else {
                        reject("respInfo is error"); }}); }); }; };Copy the code

Image compression

TinyPNG is the best image compression I’ve used so far, but there seems to be no open source compression algorithm. Currently, I can only call the API 500 times a month, so I tried Imagemin, and it looks acceptable.

const imageMin = require("imagemin");
const imageJPEG = require("imagemin-jpegtran");
const imagePNG = require("imagemin-pngquant");

// Image compression
function compressImage(filePath, destination) {
    return imageMin([filePath], {
        destination,
        plugins: [
            imageJPEG(),
            imagePNG({
                quality: [0.6.0.8]]}})); }Copy the code

Then compress it before uploading

await fs.writeFile(filePath, buffer); // Create temporary local files
// Compress the image into an image with the same name
if (needCompress) {
    await compressImage(filePath, folder);
}
const url = await qiNiuUpload(filePath); // upload to qox
Copy the code

packaging

After the feature is developed, pack it using electron Forge and output the out folder in the project root directory

npm run package
npm run make
Copy the code

In addition, the application of Electron packaging is too large. The code above is packed with 150M. I don’t know if my posture is wrong

summary

At this point, completed a simple version of the picture upload application, the basic can achieve daily needs (PS: now finally do not worry about the blog was sina map bed swallowed ~), also can complete their own a miss.

The whole project has been put on Github. Due to the short development time and the fact that Electron has not been used before, the code is a little bad. There are still some areas that can be iterated, such as

  • Upload Progress Display
  • System right-click menu quickly upload pictures

We’ll deal with it when we have time.