mirror of
https://github.com/tauri-apps/tauri.git
synced 2025-01-22 20:37:18 +08:00
feat(icons): add and test icon generation for tauri (#55)
This commit is contained in:
parent
94ead187dc
commit
596f6218e6
9
.editorconfig
Normal file
9
.editorconfig
Normal file
@ -0,0 +1,9 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -62,3 +62,4 @@ package-lock.json
|
||||
|
||||
|
||||
src-tauri
|
||||
test/jest/tmp
|
64
README.md
64
README.md
@ -1,7 +1,7 @@
|
||||
# tauri [WIP]
|
||||
## A fresh take on creating cross-platform apps.
|
||||
[![status](https://img.shields.io/badge/Status-Internal%20Review-yellow.svg)](https://github.com/quasarframework/quasar/tree/tauri)
|
||||
[![version](https://img.shields.io/badge/Version-unreleased-yellow.svg)](https://github.com/tauri-apps/tauri/tree/dev) <img align="right" src="/tauri-logo.png" height="240" width="240">
|
||||
[![version](https://img.shields.io/badge/Version-unreleased-yellow.svg)](https://github.com/tauri-apps/tauri/tree/dev) <img align="right" src="/app-icon.png" height="240" width="240">
|
||||
|
||||
[![Chat Server](https://img.shields.io/badge/chat-on%20discord-7289da.svg)](https://discord.gg/SpmNs4S)
|
||||
[![devto](https://img.shields.io/badge/dev.to-blog-black.svg)](https://dev.to/tauri)
|
||||
@ -10,64 +10,64 @@
|
||||
[![support](https://img.shields.io/badge/Sponsor-Opencollective-blue.svg)](https://opencollective.com/tauri)
|
||||
|
||||
|
||||
**Tauri** is a tool for building tiny, blazing fast binaries for all
|
||||
**Tauri** is a tool for building tiny, blazing fast binaries for all
|
||||
major desktop platforms. It was incubated at Quasar Framework.
|
||||
|
||||
Whether you are just starting out making apps for your meetup or
|
||||
regularly crunch terabyte datasets, we are absolutely confident that
|
||||
Whether you are just starting out making apps for your meetup or
|
||||
regularly crunch terabyte datasets, we are absolutely confident that
|
||||
you will love using Tauri as much as we love making and maintaining it.
|
||||
|
||||
## Who Tauri is For
|
||||
Because of the way Tauri has been built and can be extended, developers
|
||||
are able to interface not only with the entire Rust ecosystem, but also
|
||||
with many other programming languages. Being freed of the heaviest thing
|
||||
in the universe and the many shortcomings of server-side Javascript
|
||||
Because of the way Tauri has been built and can be extended, developers
|
||||
are able to interface not only with the entire Rust ecosystem, but also
|
||||
with many other programming languages. Being freed of the heaviest thing
|
||||
in the universe and the many shortcomings of server-side Javascript
|
||||
suddenly opens up whole new avenues for high-performance, security-focused
|
||||
applications that need the purebred power, agility and community
|
||||
applications that need the purebred power, agility and community
|
||||
acceptance of a low-level language.
|
||||
|
||||
We expect to witness an entire new class of applications being built with
|
||||
Tauri. From a simple calender to locally crunching massive realtime
|
||||
We expect to witness an entire new class of applications being built with
|
||||
Tauri. From a simple calender to locally crunching massive realtime
|
||||
feeds at particle colliders or even mesh-network based distributed message-
|
||||
passing ecosystems - the bar has been raised and gauntlet thrown.
|
||||
passing ecosystems - the bar has been raised and gauntlet thrown.
|
||||
|
||||
What will you make?
|
||||
|
||||
## 5 Reasons to consider Tauri
|
||||
- **BUNDLE SIZE** of a vanilla Tauri app is less than 3 MB - about 140 MB smaller than what you get with Electron.
|
||||
- **MEMORY FOOTPRINT** is less than half of the size of an Electron app built from the same codebase.
|
||||
- **SECURITY** is Tauri's biggest priority and we take it so seriously that we innovate to keep hackers out of your apps.
|
||||
- **MEMORY FOOTPRINT** is less than half of the size of an Electron app built from the same codebase.
|
||||
- **SECURITY** is Tauri's biggest priority and we take it so seriously that we innovate to keep hackers out of your apps.
|
||||
- **RELIABILITY** of the underlying code base is why critical libraries have been forked and will be perpetually maintained.
|
||||
- **FLOSS** licensing is regretfully impossible with downstream Chromium consumers, like Electron. Sources: [0](https://lists.gnu.org/archive/html/libreplanet-discuss/2017-01/msg00056.html) [1](https://lists.gnu.org/archive/html/directory-discuss/2017-12/msg00008.html) [2](https://lists.gnu.org/archive/html/libreplanet-discuss/2019-02/msg00001.html)
|
||||
|
||||
## Technical Details
|
||||
The user interface in Tauri apps currently leverages Cocoa/WebKit on macOS,
|
||||
gtk-webkit2 on Linux and MSHTML (IE10/11) or Webkit via Edge on Windows.
|
||||
**Tauri** is based on the MIT licensed prior work known as
|
||||
The user interface in Tauri apps currently leverages Cocoa/WebKit on macOS,
|
||||
gtk-webkit2 on Linux and MSHTML (IE10/11) or Webkit via Edge on Windows.
|
||||
**Tauri** is based on the MIT licensed prior work known as
|
||||
[webview](https://github.com/zserge/webview).
|
||||
|
||||
The default binding to the underlying webview library currently uses Rust,
|
||||
but other languages like Golang or Python (and many others) are possible
|
||||
but other languages like Golang or Python (and many others) are possible
|
||||
(and only a PR away).
|
||||
|
||||
> Rust is blazingly fast and memory-efficient: with no runtime or garbage
|
||||
collector, it can power performance-critical services, run on embedded
|
||||
> Rust is blazingly fast and memory-efficient: with no runtime or garbage
|
||||
collector, it can power performance-critical services, run on embedded
|
||||
devices, and easily integrate with other languages. Rust’s rich type system
|
||||
and ownership model guarantee memory-safety and thread-safety — and enable
|
||||
you to eliminate many classes of bugs at compile-time. Rust has great
|
||||
you to eliminate many classes of bugs at compile-time. Rust has great
|
||||
documentation, a friendly compiler with useful error messages, and top-notch
|
||||
tooling — an integrated package manager and build tool, smart multi-editor
|
||||
support with auto-completion and type inspections, an auto-formatter, and
|
||||
support with auto-completion and type inspections, an auto-formatter, and
|
||||
more. - [https://www.rust-lang.org/](https://www.rust-lang.org/)
|
||||
|
||||
This combination of power, safety and usability are why we chose Rust to be
|
||||
the default binding for Tauri. It is our intention to provide the most safe
|
||||
and performant native app experience (for devs and app consumers), out of
|
||||
the box.
|
||||
and performant native app experience (for devs and app consumers), out of
|
||||
the box.
|
||||
|
||||
To this end, we have spent a great deal of time creating an especially secure
|
||||
localhost-free backend for the security conscious application-artisans. This
|
||||
means that your app does not use a localhost server, as is generally the case with
|
||||
To this end, we have spent a great deal of time creating an especially secure
|
||||
localhost-free backend for the security conscious application-artisans. This
|
||||
means that your app does not use a localhost server, as is generally the case with
|
||||
cordova apps. This also has the positive side effect, that less code is needed
|
||||
and the final binaries are smaller.
|
||||
|
||||
@ -93,12 +93,12 @@ you through the process. Here is a bit of a status report.
|
||||
- [ ] Tray (coming soon)
|
||||
- [x] Copy Buffer
|
||||
|
||||
#### API
|
||||
#### API
|
||||
- [ ] answer - enable rust to direct the UI
|
||||
- [ ] bridge - enable Quasar Bridge
|
||||
- [x] event - enable binding to message
|
||||
- [x] execute - STDOUT Passthrough with Command Invocation
|
||||
- [x] listFiles - list files in a directory
|
||||
- [x] listFiles - list files in a directory
|
||||
- [x] open - open link in a browser
|
||||
- [x] readBinaryFile - read binary file from local filesystem
|
||||
- [x] readTextFile - read text file from local filesystem
|
||||
@ -161,20 +161,20 @@ projects here in this repository, in order to guarantee the security of the
|
||||
code and our ability to enhance it with features that may not be needed for
|
||||
other consumers.
|
||||
|
||||
We hope that this code is useful, but make no claims to suitability or
|
||||
We hope that this code is useful, but make no claims to suitability or
|
||||
guarantees that it will work outside of the Quasar ecosystem.
|
||||
|
||||
This has been done with our best attempt at due diligence and in
|
||||
respect of the original authors. Thankyou - this project would never have
|
||||
been possible without your amazing contribution to open-source and we are
|
||||
honoured to carry the torch further. Of special note:
|
||||
- [zserge](https://github.com/zserge) for the original webview approach and
|
||||
- [zserge](https://github.com/zserge) for the original webview approach and
|
||||
go bindings
|
||||
- [Boscop](https://github.com/Boscop) for the Rust Bindings
|
||||
- [Burtonago](https://github.com/burtonageo) for the Cargo Bundle prototype
|
||||
|
||||
## Contributing
|
||||
Please make sure to read the [Contributing Guide](./.github/CONTRIBUTING.md)
|
||||
Please make sure to read the [Contributing Guide](./.github/CONTRIBUTING.md)
|
||||
before making a pull request.
|
||||
|
||||
Thank you to all the people who already contributed to Tauri!
|
||||
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 123 KiB |
@ -11,7 +11,7 @@ module.exports = {
|
||||
collectCoverage: true,
|
||||
coverageDirectory: '<rootDir>/test/jest/coverage',
|
||||
collectCoverageFrom: [
|
||||
'<rootDir>/mode/**/*.js',
|
||||
'<rootDir>/mode/**/*.js'
|
||||
],
|
||||
coverageReporters: ['json-summary', 'text', 'lcov'],
|
||||
coverageThreshold: {
|
||||
@ -29,7 +29,8 @@ module.exports = {
|
||||
moduleFileExtensions: ['js', 'json'],
|
||||
moduleNameMapper: {
|
||||
'^~/(.*)$': '<rootDir>/$1',
|
||||
'^mode/(.*)$': '<rootDir>/mode/$1'
|
||||
'^mode/(.*)$': '<rootDir>/mode/$1',
|
||||
'^test/(.*)$': '<rootDir>/test/$1'
|
||||
},
|
||||
transform: {}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
const
|
||||
parseArgs = require('minimist')
|
||||
const parseArgs = require('minimist')
|
||||
|
||||
const argv = parseArgs(process.argv.slice(2), {
|
||||
alias: {
|
||||
|
61
mode/bin/tauri-icon.js
Normal file
61
mode/bin/tauri-icon.js
Normal file
@ -0,0 +1,61 @@
|
||||
const parseArgs = require('minimist')
|
||||
const { appDir, tauriDir } = require('../helpers/app-paths')
|
||||
const logger = require('../helpers/logger')
|
||||
const log = logger('app:tauri')
|
||||
const warn = logger('app:tauri (icon)', 'red')
|
||||
const { tauricon } = require('../helpers/tauricon')
|
||||
const { resolve } = require('path')
|
||||
|
||||
/**
|
||||
* @type {object}
|
||||
* @property {boolean} h
|
||||
* @property {boolean} help
|
||||
* @property {string|boolean} f
|
||||
* @property {string|boolean} force
|
||||
* @property {boolean} l
|
||||
* @property {boolean} log
|
||||
* @property {boolean} c
|
||||
* @property {boolean} config
|
||||
* @property {boolean} s
|
||||
* @property {boolean} source
|
||||
* @property {boolean} t
|
||||
* @property {boolean} target
|
||||
*/
|
||||
const argv = parseArgs(process.argv.slice(2), {
|
||||
alias: {
|
||||
h: 'help',
|
||||
l: 'log',
|
||||
c: 'config',
|
||||
s: 'source',
|
||||
t: 'target'
|
||||
},
|
||||
boolean: ['h', 'l']
|
||||
})
|
||||
|
||||
if (argv.help) {
|
||||
console.log(`
|
||||
Description
|
||||
Create all the icons you need for your Tauri app.
|
||||
|
||||
Usage
|
||||
$ tauri icon
|
||||
|
||||
Options
|
||||
--help, -h Displays this message
|
||||
--log, l Logging [boolean]
|
||||
--icon, i Source icon (png, 1240x1240 with transparency)
|
||||
--target, t Target folder (default: 'src-tauri/icons')
|
||||
--compression, c Compression type [pngquant|optipng|zopfli]
|
||||
`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
tauricon.make(
|
||||
argv.i || resolve(appDir, 'app-icon.png'),
|
||||
argv.t || resolve(tauriDir, 'icons'),
|
||||
argv.c || 'optipng'
|
||||
).then(() => {
|
||||
log('(tauricon) Completed')
|
||||
}).catch(e => {
|
||||
warn(e)
|
||||
})
|
@ -1,5 +1,4 @@
|
||||
const
|
||||
parseArgs = require('minimist')
|
||||
const parseArgs = require('minimist')
|
||||
const appPaths = require('../helpers/app-paths')
|
||||
const logger = require('../helpers/logger')
|
||||
const log = logger('app:tauri')
|
||||
@ -13,6 +12,8 @@ const warn = logger('app:tauri (init)', 'red')
|
||||
* @property {string|boolean} force
|
||||
* @property {boolean} l
|
||||
* @property {boolean} log
|
||||
* @property {boolean} d
|
||||
* @property {boolean} directory
|
||||
*/
|
||||
const argv = parseArgs(process.argv.slice(2), {
|
||||
alias: {
|
||||
@ -44,7 +45,7 @@ const { inject } = require('../template')
|
||||
|
||||
const target = appPaths.tauriDir
|
||||
|
||||
if (inject(target, 'all', argv.f, argv.l, argv.d)) {
|
||||
if (inject(target, 'all', argv.f || null, argv.l || null, argv.d || null)) {
|
||||
log('tauri init successful')
|
||||
} else {
|
||||
warn('tauri init unsuccessful')
|
||||
|
@ -1,10 +1,18 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const cmds = ['init', 'dev', 'build', 'help']
|
||||
const cmds = ['init', 'dev', 'build', 'help', 'icon']
|
||||
|
||||
const cmd = process.argv[2]
|
||||
|
||||
/**
|
||||
* @description This is the bootstrapper that in turn calls subsequent
|
||||
* Tauri Commands
|
||||
*
|
||||
* @param {string|array} command
|
||||
*/
|
||||
const tauri = function (command) {
|
||||
if (typeof command === 'object') { // technically we just care about an array
|
||||
command = command[0]
|
||||
}
|
||||
if (!command || command === '-h' || command === '--help' || command === 'help') {
|
||||
console.log(`
|
||||
Description
|
||||
|
@ -1,5 +1,4 @@
|
||||
const
|
||||
{ existsSync } = require('fs')
|
||||
const { existsSync } = require('fs')
|
||||
const { resolve, join, normalize, sep } = require('path')
|
||||
|
||||
/**
|
||||
|
92
mode/helpers/tauricon.config.js
Normal file
92
mode/helpers/tauricon.config.js
Normal file
@ -0,0 +1,92 @@
|
||||
exports.options = {
|
||||
// folder determines in which path to drop the generated file
|
||||
// prefix is the first part of the generated file's name
|
||||
// infix adds e.g. '44x44' based on the size in sizes to the generated file's name
|
||||
// suffix adds a file-ending to the generated file's name
|
||||
// sizes determines the pixel width and height to use
|
||||
background_color: '#000074',
|
||||
theme_color: '#02aa9b',
|
||||
sharp: 'kernel: sharp.kernel.lanczos3', // one of [nearest|cubic|lanczos2|lanczos3]
|
||||
minify: {
|
||||
batch: false,
|
||||
overwrite: true,
|
||||
available: ['pngquant', 'optipng', 'zopfli'],
|
||||
type: 'pngquant',
|
||||
pngcrushOptions: {
|
||||
reduce: true
|
||||
},
|
||||
pngquantOptions: {
|
||||
quality: [0.6, 0.8],
|
||||
floyd: 0.1, // 0.1 - 1
|
||||
speed: 10 // 1 - 10
|
||||
},
|
||||
optipngOptions: {
|
||||
optimizationLevel: 4,
|
||||
bitDepthReduction: true,
|
||||
colorTypeReduction: true,
|
||||
paletteReduction: true
|
||||
},
|
||||
zopfliOptions: {
|
||||
transparent: true,
|
||||
more: true
|
||||
}
|
||||
},
|
||||
splash_type: 'generate',
|
||||
tauri: {
|
||||
linux: {
|
||||
folder: '.',
|
||||
prefix: '',
|
||||
infix: true,
|
||||
suffix: '.png',
|
||||
sizes: [
|
||||
32, 128
|
||||
]
|
||||
},
|
||||
linux_2x: {
|
||||
folder: '.',
|
||||
prefix: '128x128@2x',
|
||||
infix: false,
|
||||
suffix: '.png',
|
||||
sizes: [
|
||||
256
|
||||
]
|
||||
},
|
||||
defaults: {
|
||||
folder: '.',
|
||||
prefix: 'icon',
|
||||
infix: false,
|
||||
suffix: '.png',
|
||||
sizes: [
|
||||
512
|
||||
]
|
||||
},
|
||||
appx_logo: {
|
||||
folder: '.',
|
||||
prefix: 'StoreLogo',
|
||||
infix: false,
|
||||
suffix: '.png',
|
||||
sizes: [
|
||||
50
|
||||
]
|
||||
},
|
||||
appx_square: {
|
||||
folder: '.',
|
||||
prefix: 'Square',
|
||||
infix: true,
|
||||
suffix: 'Logo.png',
|
||||
sizes: [
|
||||
30,
|
||||
44,
|
||||
71,
|
||||
89,
|
||||
107,
|
||||
142,
|
||||
150,
|
||||
284,
|
||||
310
|
||||
]
|
||||
}
|
||||
// todo: look at capacitor and cordova for insight into what icons
|
||||
// we need for those distribution targets
|
||||
}
|
||||
}
|
420
mode/helpers/tauricon.js
Normal file
420
mode/helpers/tauricon.js
Normal file
@ -0,0 +1,420 @@
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* This is a module that takes an original image and resizes
|
||||
* it to common icon sizes and will put them in a folder.
|
||||
* It will retain transparency and can make special file
|
||||
* types. You can control the settings.
|
||||
*
|
||||
* @module tauricon
|
||||
* @exports tauricon
|
||||
* @author Daniel Thompson-Yvetot
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
const path = require('path')
|
||||
const sharp = require('sharp')
|
||||
const imagemin = require('imagemin')
|
||||
const pngquant = require('imagemin-pngquant')
|
||||
const optipng = require('imagemin-optipng')
|
||||
const zopfli = require('imagemin-zopfli')
|
||||
const png2icons = require('png2icons')
|
||||
const readChunk = require('read-chunk')
|
||||
const isPng = require('is-png')
|
||||
|
||||
const settings = require('./tauricon.config')
|
||||
let image = false
|
||||
|
||||
const {
|
||||
access,
|
||||
writeFileSync,
|
||||
ensureDir,
|
||||
ensureFileSync
|
||||
} = require('fs-extra')
|
||||
|
||||
const exists = async function (file) {
|
||||
try {
|
||||
await access(file)
|
||||
return true
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the first call that attempts to memoize the sharp(src).
|
||||
* If the source image cannot be found or if it is not a png, it
|
||||
* is a failsafe that will exit or throw.
|
||||
*
|
||||
* @param {string} src - a folder to target
|
||||
* @exits {error} if not a png, if not an image
|
||||
*/
|
||||
const checkSrc = async function (src) {
|
||||
if (image !== false) {
|
||||
return image
|
||||
} else {
|
||||
const srcExists = await exists(src)
|
||||
if (!srcExists) {
|
||||
image = false
|
||||
throw new Error('[ERROR] Source image for tauricon not found')
|
||||
} else {
|
||||
const buffer = await readChunk(src, 0, 8)
|
||||
if (isPng(buffer) === true) {
|
||||
return (image = sharp(src))
|
||||
} else {
|
||||
image = false
|
||||
throw new Error('[ERROR] Source image for tauricon is not a png')
|
||||
// exit because this is BAD!
|
||||
// Developers should catch () { } this as it is
|
||||
// the last chance to stop bad things happening.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort the folders in the current job for unique folders.
|
||||
*
|
||||
* @param {object} options - a subset of the settings
|
||||
* @returns {array} folders
|
||||
*/
|
||||
const uniqueFolders = function (options) {
|
||||
let folders = []
|
||||
for (const type in options) {
|
||||
if (options[type].folder) {
|
||||
folders.push(options[type].folder)
|
||||
}
|
||||
}
|
||||
folders = folders.sort().filter((x, i, a) => !i || x !== a[i - 1])
|
||||
return folders
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a hex color (like #212342) into r,g,b values
|
||||
*
|
||||
* @param {string} hex - hex colour
|
||||
* @returns {array} r,g,b
|
||||
*/
|
||||
const hexToRgb = function (hex) {
|
||||
// https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
|
||||
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
|
||||
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i
|
||||
hex = hex.replace(shorthandRegex, function (m, r, g, b) {
|
||||
return r + r + g + g + b + b
|
||||
})
|
||||
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
return result
|
||||
? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
}
|
||||
: null
|
||||
}
|
||||
|
||||
/**
|
||||
* validate image and directory
|
||||
* @param {string} src
|
||||
* @param {string} target
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const validate = async function (src, target) {
|
||||
if (target !== undefined) {
|
||||
await ensureDir(target)
|
||||
}
|
||||
return checkSrc(src)
|
||||
}
|
||||
|
||||
/**
|
||||
* Log progress in the command line
|
||||
*
|
||||
* @param {string} msg
|
||||
* @param {boolean} end
|
||||
*/
|
||||
const progress = function (msg) {
|
||||
console.log(msg)
|
||||
// process.stdout.write(` ${msg} \r`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a spinner on the command line
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* const spinnerInterval = spinner()
|
||||
* // later
|
||||
* clearInterval(spinnerInterval)
|
||||
* @returns {function} - the interval object
|
||||
*/
|
||||
const spinner = function () {
|
||||
return setInterval(() => {
|
||||
process.stdout.write('/ \r')
|
||||
setTimeout(() => {
|
||||
process.stdout.write('- \r')
|
||||
setTimeout(() => {
|
||||
process.stdout.write('\\ \r')
|
||||
setTimeout(() => {
|
||||
process.stdout.write('| \r')
|
||||
}, 100)
|
||||
}, 100)
|
||||
}, 100)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const tauricon = exports.tauricon = {
|
||||
validate: async function (src, target) {
|
||||
await validate(src, target)
|
||||
return typeof image === 'object'
|
||||
},
|
||||
version: function () {
|
||||
return require('../../package.json').version
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} src
|
||||
* @param {string} target
|
||||
* @param {string} strategy
|
||||
* @param {object} options
|
||||
*/
|
||||
make: async function (src, target, strategy, options) {
|
||||
const spinnerInterval = spinner()
|
||||
options = options || settings.options.tauri
|
||||
await this.validate(src, target)
|
||||
progress('Building Tauri icns and ico')
|
||||
await this.icns(src, target, options, strategy)
|
||||
progress('Building Tauri png icons')
|
||||
await this.build(src, target, options)
|
||||
if (strategy) {
|
||||
progress(`Minifying assets with ${strategy}`)
|
||||
await this.minify(target, options, strategy, 'batch')
|
||||
} else {
|
||||
console.log('no minify strategy')
|
||||
}
|
||||
progress('Tauricon Finished')
|
||||
clearInterval(spinnerInterval)
|
||||
return true
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a set of images according to the subset of options it knows about.
|
||||
*
|
||||
* @param {string} src - image location
|
||||
* @param {string} target - where to drop the images
|
||||
* @param {object} options - js object that defines path and sizes
|
||||
*/
|
||||
build: async function (src, target, options) {
|
||||
await this.validate(src, target) // creates the image object
|
||||
const buildify2 = async function (pvar) {
|
||||
try {
|
||||
const pngImage = image.resize(pvar[1], pvar[1])
|
||||
if (pvar[2]) {
|
||||
const rgb = hexToRgb(options.background_color)
|
||||
pngImage.flatten({
|
||||
background: { r: rgb.r, g: rgb.g, b: rgb.b, alpha: 1 }
|
||||
})
|
||||
}
|
||||
pngImage.png()
|
||||
await pngImage.toFile(pvar[0])
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
|
||||
let output
|
||||
const folders = uniqueFolders(options)
|
||||
for (const n in folders) {
|
||||
// make the folders first
|
||||
ensureDir(`${target}${path.sep}${folders[n]}`)
|
||||
}
|
||||
for (const optionKey in options) {
|
||||
const option = options[optionKey]
|
||||
// chain up the transforms
|
||||
for (const sizeKey in option.sizes) {
|
||||
const size = option.sizes[sizeKey]
|
||||
if (!option.splash) {
|
||||
const dest = `${target}/${option.folder}`
|
||||
if (option.infix === true) {
|
||||
output = `${dest}${path.sep}${option.prefix}${size}x${size}${option.suffix}`
|
||||
} else {
|
||||
output = `${dest}${path.sep}${option.prefix}${option.suffix}`
|
||||
}
|
||||
const pvar = [output, size, option.background]
|
||||
await buildify2(pvar)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Creates a set of splash images (COMING SOON!!!)
|
||||
*
|
||||
* @param {string} src - icon location
|
||||
* @param {string} splashSrc - splashscreen location
|
||||
* @param {string} target - where to drop the images
|
||||
* @param {object} options - js object that defines path and sizes
|
||||
*/
|
||||
splash: async function (src, splashSrc, target, options) {
|
||||
let output
|
||||
let block = false
|
||||
const rgb = hexToRgb(options.background_color)
|
||||
|
||||
// three options
|
||||
// options: splashscreen_type [generate | overlay | pure]
|
||||
// - generate (icon + background color) DEFAULT
|
||||
// - overlay (icon + splashscreen)
|
||||
// - pure (only splashscreen)
|
||||
|
||||
let sharpSrc
|
||||
if (splashSrc === src) {
|
||||
// prevent overlay or pure
|
||||
block = true
|
||||
}
|
||||
if (block === true || options.splashscreen_type === 'generate') {
|
||||
await this.validate(src, target)
|
||||
if (!image) {
|
||||
process.exit(1)
|
||||
}
|
||||
sharpSrc = sharp(src)
|
||||
sharpSrc.extend({
|
||||
top: 726,
|
||||
bottom: 726,
|
||||
left: 726,
|
||||
right: 726,
|
||||
background: {
|
||||
r: rgb.r,
|
||||
g: rgb.g,
|
||||
b: rgb.b,
|
||||
alpha: 1
|
||||
}
|
||||
})
|
||||
.flatten({ background: { r: rgb.r, g: rgb.g, b: rgb.b, alpha: 1 } })
|
||||
} else if (options.splashscreen_type === 'overlay') {
|
||||
sharpSrc = sharp(splashSrc)
|
||||
.flatten({ background: { r: rgb.r, g: rgb.g, b: rgb.b, alpha: 1 } })
|
||||
.composite([{
|
||||
input: src
|
||||
// blend: 'multiply' <= future work, maybe just a gag
|
||||
}])
|
||||
} else if (options.splashscreen_type === 'pure') {
|
||||
sharpSrc = sharp(splashSrc)
|
||||
.flatten({ background: { r: rgb.r, g: rgb.g, b: rgb.b, alpha: 1 } })
|
||||
}
|
||||
|
||||
const data = await sharpSrc.toBuffer()
|
||||
|
||||
for (const optionKey in options) {
|
||||
const option = options[optionKey]
|
||||
for (const sizeKey in option.sizes) {
|
||||
const size = option.sizes[sizeKey]
|
||||
if (option.splash) {
|
||||
const dest = `${target}${path.sep}${option.folder}`
|
||||
await ensureDir(dest)
|
||||
|
||||
if (option.infix === true) {
|
||||
output = `${dest}${path.sep}${option.prefix}${size}x${size}${option.suffix}`
|
||||
} else {
|
||||
output = `${dest}${path.sep}${option.prefix}${option.suffix}`
|
||||
}
|
||||
// console.log('p1', output, size)
|
||||
const pvar = [output, size]
|
||||
let sharpData = sharp(data)
|
||||
sharpData = sharpData.resize(pvar[1][0], pvar[1][1])
|
||||
await sharpData.toFile(pvar[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Minifies a set of images
|
||||
*
|
||||
* @param {string} target - image location
|
||||
* @param {object} options - where to drop the images
|
||||
* @param {string} strategy - which minify strategy to use
|
||||
* @param {string} mode - singlefile or batch
|
||||
*/
|
||||
minify: async function (target, options, strategy, mode) {
|
||||
let cmd
|
||||
const minify = settings.options.minify
|
||||
if (!minify.available.find(x => x === strategy)) {
|
||||
strategy = minify.type
|
||||
}
|
||||
switch (strategy) {
|
||||
case 'pngquant':
|
||||
cmd = pngquant(minify.pngquantOptions)
|
||||
break
|
||||
case 'optipng':
|
||||
cmd = optipng(minify.optipngOptions)
|
||||
break
|
||||
case 'zopfli':
|
||||
cmd = zopfli(minify.zopfliOptions)
|
||||
break
|
||||
}
|
||||
|
||||
const __minifier = async (pvar) => {
|
||||
await imagemin([pvar[0]], {
|
||||
destination: pvar[1],
|
||||
plugins: [cmd]
|
||||
}).catch(err => {
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
switch (mode) {
|
||||
case 'singlefile':
|
||||
await __minifier([target, path.dirname(target)], cmd)
|
||||
break
|
||||
case 'batch':
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const folders = uniqueFolders(options)
|
||||
for (const n in folders) {
|
||||
console.log('batch minify:', folders[n])
|
||||
await __minifier([
|
||||
`${target}${path.sep}${folders[n]}${path.sep}*.png`,
|
||||
`${target}${path.sep}${folders[n]}`
|
||||
], cmd)
|
||||
}
|
||||
break
|
||||
default:
|
||||
console.error('* [ERROR] Minify mode must be one of [ singlefile | batch]')
|
||||
process.exit(1)
|
||||
}
|
||||
return 'minified'
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates special icns and ico filetypes
|
||||
*
|
||||
* @param {string} src - image location
|
||||
* @param {string} target - where to drop the images
|
||||
* @param {object} options
|
||||
* @param {string} strategy
|
||||
*/
|
||||
icns: async function (src, target, options, strategy) {
|
||||
try {
|
||||
if (!image) {
|
||||
process.exit(1)
|
||||
}
|
||||
await this.validate(src, target)
|
||||
|
||||
const sharpSrc = sharp(src)
|
||||
const buf = await sharpSrc.toBuffer()
|
||||
|
||||
const out = await png2icons.createICNS(buf, png2icons.BICUBIC, 0)
|
||||
ensureFileSync(path.join(target, '/icon.icns'))
|
||||
writeFileSync(path.join(target, '/icon.icns'), out)
|
||||
|
||||
const out2 = await png2icons.createICO(buf, png2icons.BICUBIC, 0, true)
|
||||
ensureFileSync(path.join(target, '/icon.ico'))
|
||||
writeFileSync(path.join(target, '/icon.ico'), out2)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof exports !== 'undefined') {
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
exports = module.exports = tauricon
|
||||
}
|
||||
exports.tauricon = tauricon
|
||||
}
|
@ -36,10 +36,18 @@
|
||||
"fast-glob": "^3.0.4",
|
||||
"fs-extra": "^8.1.0",
|
||||
"html-webpack-inline-source-plugin": "^0.0.10",
|
||||
"imagemin": "^7.0.1",
|
||||
"imagemin-optipng": "^7.1.0",
|
||||
"imagemin-pngquant": "^8.0.0",
|
||||
"imagemin-zopfli": "^6.0.0",
|
||||
"is-png": "^2.0.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.template": "^4.5.0",
|
||||
"minimist": "^1.2.0",
|
||||
"ms": "^2.1.2",
|
||||
"png2icons": "^2.0.1",
|
||||
"read-chunk": "^3.2.0",
|
||||
"sharp": "^0.23.2",
|
||||
"webpack-merge": "^4.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -1,34 +1,32 @@
|
||||
const { tauri } = require('mode/bin/tauri')
|
||||
// const mockProcess = require('jest-mock-process')
|
||||
|
||||
|
||||
describe('[CLI] tauri.js', () => {
|
||||
it('displays a help message', async () => {
|
||||
jest.spyOn(console, 'log')
|
||||
jest.spyOn(process, 'exit').mockImplementation(() => true)
|
||||
let result = tauri('help')
|
||||
tauri('help')
|
||||
console.log(process.exit.mock.calls[0][0])
|
||||
expect(process.exit.mock.calls[0][0]).toBe(0)
|
||||
// console.log(console.log.mock.calls[0][0])
|
||||
expect(!!console.log.mock.calls[0][0]).toBe(true)
|
||||
result = tauri('--help')
|
||||
// console.log(console.log.mock.calls[2][0])
|
||||
tauri('--help')
|
||||
expect(!!console.log.mock.calls[2][0]).toBe(true)
|
||||
result = tauri('-h')
|
||||
tauri('-h')
|
||||
expect(!!console.log.mock.calls[3][0]).toBe(true)
|
||||
tauri(['help'])
|
||||
expect(!!console.log.mock.calls[4][0]).toBe(true)
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('will not run an unavailable command', async () => {
|
||||
jest.spyOn(console, 'log')
|
||||
let result = tauri('foo')
|
||||
tauri('foo')
|
||||
expect(console.log.mock.calls[0][0].split('.')[0]).toBe('Invalid command foo')
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('will pass on an available command', async () => {
|
||||
jest.spyOn(console, 'log')
|
||||
let result = tauri('init')
|
||||
tauri('init')
|
||||
expect(console.log.mock.calls[0][0].split('.')[0]).toBe('[tauri]: running init')
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
56
test/jest/__tests__/tauricon.spec.js
Normal file
56
test/jest/__tests__/tauricon.spec.js
Normal file
@ -0,0 +1,56 @@
|
||||
const { tauricon } = require('mode/helpers/tauricon')
|
||||
const { tauri } = require('mode/bin/tauri')
|
||||
|
||||
describe('[CLI] tauri-icon internals', () => {
|
||||
it('tells you the version', () => {
|
||||
const version = tauricon.version()
|
||||
expect(!!version).toBe(true)
|
||||
})
|
||||
it('gets you help', async () => {
|
||||
jest.spyOn(console, 'log')
|
||||
tauri(['icon', 'help'])
|
||||
expect(!!console.log.mock.calls[0][0]).toBe(true)
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('will not validate a non-file', async () => {
|
||||
try {
|
||||
await tauricon.validate('test/jest/fixtures/doesnotexist.png', 'test/jest/fixtures/')
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('[ERROR] Source image for tauricon not found')
|
||||
}
|
||||
})
|
||||
it('will not validate a non-png', async () => {
|
||||
try {
|
||||
await tauricon.validate('test/jest/fixtures/notAMeme.jpg', 'test/jest/fixtures/')
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('[ERROR] Source image for tauricon is not a png')
|
||||
}
|
||||
})
|
||||
it('can validate an image as PNG', async () => {
|
||||
const valid = await tauricon.validate('test/jest/fixtures/tauri-logo.png', 'test/jest/fixtures/')
|
||||
expect(valid).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* This test suite takes A LOT of time. Maybe 5 minutes...? You may blame
|
||||
* Zopfli, but don't blame us for trying to help you get the smallest
|
||||
* possible binaries!
|
||||
*/
|
||||
describe('[CLI] tauri-icon builder', () => {
|
||||
it('makes a set of icons with pngquant', async () => {
|
||||
const valid = await tauricon.make('test/jest/fixtures/tauri-logo.png', 'test/jest/tmp/pngquant', 'pngquant')
|
||||
expect(valid).toBe(true)
|
||||
})
|
||||
|
||||
it('makes a set of icons with optipng', async () => {
|
||||
const valid = await tauricon.make('test/jest/fixtures/tauri-logo.png', 'test/jest/tmp/optipng', 'optipng')
|
||||
expect(valid).toBe(true)
|
||||
})
|
||||
|
||||
it('makes a set of icons with zopfli', async () => {
|
||||
const valid = await tauricon.make('test/jest/fixtures/tauri-logo.png', 'test/jest/tmp/zopfli', 'zopfli')
|
||||
expect(valid).toBe(true)
|
||||
})
|
||||
})
|
BIN
test/jest/fixtures/notAMeme.jpg
Normal file
BIN
test/jest/fixtures/notAMeme.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 52 KiB |
BIN
test/jest/fixtures/tauri-logo.png
Normal file
BIN
test/jest/fixtures/tauri-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 123 KiB |
@ -1,4 +1,4 @@
|
||||
jest.setTimeout(1000)
|
||||
jest.setTimeout(50000)
|
||||
|
||||
global.Promise = require('promise')
|
||||
|
||||
@ -6,5 +6,4 @@ setTimeout(() => {
|
||||
// do nothing
|
||||
}, 1)
|
||||
|
||||
|
||||
require('dotenv').config({ path: '.env.jest' })
|
||||
|
Loading…
Reference in New Issue
Block a user