Browse Source

- Improvement: It is now possible to update some components using alternative protocols to HTTP such as IPFS and Torrent, this allows users with unstable or slow connections to download big updates more reliably and avoid errors during installation.

- Improvement: It is now possible to restore the settings to their default values.
tags/v1.5.6
Ivan Bravo Bravo 3 months ago
parent
commit
cb00fb7faa

+ 1
- 1
src/assets/css/reset/_dialog.scss View File

@@ -33,5 +33,5 @@ dialog {
}

dialog::backdrop {
background: rgba(25, 25, 26, 0.90);
background: rgba(0,0,0, 0.90);
}

+ 2
- 1
src/components/Layout/Navbar.vue View File

@@ -167,7 +167,8 @@ export default {
@apply text-lg;
}

&:hover {
&:hover,
&.nuxt-link-exact-active {
@apply text-primary border-primary;
}
}

+ 1
- 1
src/components/Settings/SettingsField.vue View File

@@ -174,7 +174,7 @@ export default {
&::v-deep {
.item__action {
@apply flex items-center justify-center;
max-width: 300px;
//max-width: 300px;
}
}
}

+ 32
- 7
src/components/UI/ProjectUpdate.vue View File

@@ -15,9 +15,14 @@
</h2>
</div>

<!-- Preparing -->
<div v-if="isPreparing" class="update__status">
Preparing download...
</div>

<!-- Downloading -->
<div v-if="isDownloading && updater.update.progress >= 0" class="update__status">
Downloading ~ <strong>{{ updater.update.progress }}%</strong> ~ {{ updater.update.written | size }}/{{ updater.update.total | size }} MB.
<div v-else-if="isDownloading && updater.update.progress >= 0" class="update__status">
Downloading ~ <strong>{{ updater.update.progress }}%</strong> ~ {{ updater.update.written | size }}/{{ updater.update.total | size }} MB. <span v-if="updater.update.peers > 0">({{ updater.update.peers }} peers)</span>
</div>

<!-- Downloading -->
@@ -54,7 +59,7 @@
Update
</button>

<button v-else-if="updater.update.active"
<button v-if="updater.update.active"
key="update-cancel"
class="button button--danger"
@click.prevent="updater.cancel()">
@@ -65,7 +70,7 @@
<button v-if="updater.downloadUrls.length > 0"
v-tooltip="'List of links to download the update manually.'"
class="button button--info"
@click.prevent="$refs.mirrorsDialog.show()">
@click.prevent="$refs.mirrorsDialog.showModal()">
<span class="icon"><font-awesome-icon icon="link" /></span>
Mirrors
</button>
@@ -103,8 +108,10 @@
<dialog ref="mirrorsDialog">
<div class="dialog__content">
<ul class="mirrors">
<li v-for="(item, index) in updater.downloadUrls" :key="index">
<a :href="item" target="_blank">{{ item | domain }}</a>
<li v-for="(url, index) in updater.downloadAllUrls" :key="index">
<a v-if="isTorrent(url)" :href="url" target="_blank">Torrent ({{ url | domain }})</a>
<a v-else-if="isIPFS(url)" :href="`https://gateway.ipfs.io/ipfs/${url}?filename=${updater.filename}`" target="_blank">IPFS</a>
<a v-else :href="url" target="_blank">HTTP ({{ url | domain }})</a>
</li>
</ul>

@@ -119,7 +126,7 @@
</template>

<script>
import { toNumber } from 'lodash'
import { toNumber, startsWith, endsWith } from 'lodash'
import * as projects from '~/modules/projects'

export default {
@@ -135,6 +142,10 @@ export default {
},

domain(value) {
if (startsWith(value, 'magnet:')) {
return 'magnet'
}

return (new URL(value)).hostname
},
},
@@ -163,6 +174,10 @@ export default {
return this.updater?.currentVersion || 'v0.0.0'
},

isPreparing() {
return this.updater?.update?.status === 'preparing'
},

isDownloading() {
return this.updater?.update?.status === 'downloading'
},
@@ -180,6 +195,16 @@ export default {
beforeDestroy() {
this.updater.cancel()
},

methods: {
isTorrent(url) {
return startsWith(url, 'magnet:') || endsWith(url, '.torrent')
},

isIPFS(url) {
return startsWith(url, 'Qm')
},
},
}
</script>


+ 1
- 1
src/electron/src/index.js View File

@@ -71,7 +71,7 @@ class DreamApp {

process.on('unhandledRejection', (err) => {
logger.warn('Unhandled rejection!', err)
AppError.handle(err)
AppError.handle(err, 'warning')

return true
})

+ 3
- 3
src/electron/src/modules/app-error.js View File

@@ -87,7 +87,7 @@ export class AppError extends Error {
attempt(() => {
logger[level](this.message, { error })

if (!quiet) {
if (!quiet && level === 'error') {
this.show()
}
})
@@ -97,7 +97,7 @@ export class AppError extends Error {
}
}

static handle(error) {
static handle(error, level = 'error') {
let appError = error

if (!(error instanceof AppError)) {
@@ -114,7 +114,7 @@ export class AppError extends Error {
appError = new AppError('The application has encountered an unexpected error.', {
error: exception,
title: 'Unexpected error!',
level: 'error',
level,
})
}


+ 325
- 64
src/electron/src/modules/tools/fs.js View File

@@ -1,4 +1,6 @@
import { attempt } from 'lodash'
import {
attempt, startsWith, merge, endsWith,
} from 'lodash'
import { basename, join } from 'path'
import fs from 'fs-extra'
import { app, dialog } from 'electron'
@@ -6,14 +8,26 @@ import axios from 'axios'
import https from 'https'
import deferred from 'deferred'
import chokidar from 'chokidar'
import { getAppResourcesPath } from './paths'
import { AppError } from '../app-error'
import WebTorrent from 'webtorrent'
import IpfsCtl from 'ipfsd-ctl'
import toStream from 'it-to-stream'
import all from 'it-all'
import { EventEmitter } from 'events'
import { getAppResourcesPath, getPath } from './paths'

const logger = require('@dreamnet/logplease').create('electron:modules:tools:fs')

// eslint-disable-next-line node/no-deprecated-api
export * from 'fs-extra'

/**
* @typedef DownloadOptions
* @property {boolean} showSaveAs
* @property {string} directory
* @property {string} filename
* @property {string} filepath
*/

/**
* Returns the base64 of a dataURL
* @param {*} dataURL
@@ -114,99 +128,346 @@ export function extractSeven(path, destinationPath) {

/**
*
*
* @export
* @param {string} url
* @param {Object} [options]
* @param {*} options
* @param {EventEmitter} events
* @param {fs.WriteStream} writeStream
*/
export function download(url, options = {}) {
const EventBus = require('js-event-bus')
const bus = new EventBus()
export async function downloadFromHttp(url, options, events, writeStream) {
let readStream
let headers

try {
const request = await axios.request({
url,
responseType: 'stream',
maxContentLength: -1,
httpsAgent: new https.Agent({ rejectUnauthorized: false }),
})

// eslint-disable-next-line no-param-reassign
options = {
showSaveAs: false,
directory: app.getPath('downloads'),
filename: basename(url).split('?')[0].split('#')[0],
...options,
readStream = request.data
headers = request.headers
} catch (err) {
events.emit('error', err)
return
}

let filepath = join(options.directory, options.filename)
let cancelled = false

if (options.showSaveAs) {
// save as dialog
filepath = dialog.showSaveDialogSync({
defaultPath: filepath,
// Close handler
events.on('close', () => {
attempt(() => {
if (readStream) {
readStream.destroy()
}
})
}
})

const writeStream = fs.createWriteStream(filepath)
const contentLength = headers['content-length'] || -1

readStream.on('error', (err) => {
events.emit('error', err)
})

readStream.on('data', () => {
events.emit('progress', {
progress: (writeStream.bytesWritten / contentLength),
written: (writeStream.bytesWritten / 1048576).toFixed(2),
total: (contentLength / 1048576).toFixed(2),
})
})

axios.request({
url,
responseType: 'stream',
maxContentLength: -1,
httpsAgent: new https.Agent({ rejectUnauthorized: false }),
}).then(({ data, headers }) => {
const contentLength = headers['content-length'] || -1
readStream.pipe(writeStream)
}

data.on('data', () => {
const progress = writeStream.bytesWritten / contentLength
/**
*
*
* @export
* @param {string} cid
* @param {DownloadOptions} options
* @param {EventEmitter} events
* @param {fs.WriteStream} writeStream
*/
export async function downloadFromIPFS(cid, options, events, writeStream) {
/** @type {import('ipfsd-ctl/src/ipfsd-daemon')} */
let node
let stats
let readStream

// Close handler
events.on('close', () => {
attempt(() => {
if (readStream) {
readStream.destroy()
}

bus.emit('progress', null, {
progress,
written: (writeStream.bytesWritten / 1048576).toFixed(2),
total: (contentLength / 1048576).toFixed(2),
})
if (node) {
node.stop()
node = null
}
})
})

data.on('error', (err) => {
throw new AppError(err, { title: 'Download failed.' })
// Utility functions
const createNode = async function () {
logger.debug('Creating IPFS node...')
logger.debug(require('go-ipfs-dep').path())

fs.ensureDirSync(getPath('temp', 'ipfs'))

node = await IpfsCtl.createController({
ipfsHttpModule: require('ipfs-http-client'),
ipfsBin: require('go-ipfs-dep').path().replace('app.asar', 'app.asar.unpacked'),
ipfsOptions: {
repo: getPath('temp', 'ipfs'),
start: true,
init: true,
},
remote: false,
disposable: false,
test: false,
type: 'go',
})

writeStream.on('error', (err) => {
throw new AppError(err, { title: 'Download failed.' })
await node.init()

await node.start()

if (!node.api) {
logger.debug(node)
throw new Error('The IPFS node was not created correctly.')
}
}

const connectToProviders = async function () {
logger.debug('Connecting to providers...')

try {
await node.api.swarm.connect('/ip4/64.227.19.223/tcp/4001/p2p/QmSarArpxemsPESa6FNkmuu9iSE1QWqPX2R3Aw6f5jq4D5')
await node.api.swarm.connect('/ip4/64.227.27.182/tcp/4001/p2p/QmRjLSisUCHVpFa5ELVvX3qVPfdxajxWJEHs9kN3EcxAW6')
await node.api.swarm.connect('/dns4/mariana.dreamnet.tech/tcp/4001/p2p/QmcWoy1FzBicbYuopNT2rT6EDQSBDfco1TxibEyYgWbiMq')
} catch (err) {
logger.warn(err)
}
}

try {
await createNode()

await connectToProviders()

if (!node) {
// It seems that the user has canceled it
return
}

stats = await node.api.object.stat(cid, { timeout: 3000 })

logger.debug('Downloading...')

readStream = toStream.readable(node.api.cat(cid))
} catch (err) {
events.emit('error', err)
return
}

// eslint-disable-next-line promise/always-return
all(node.api.dht.findProvs(cid, { timeout: 10000 })).then((provs) => {
events.emit('peers', provs.length)
}).catch(() => { })

readStream.on('error', (err) => {
events.emit('error', err)
})

readStream.on('data', () => {
const progress = writeStream.bytesWritten / stats.CumulativeSize

events.emit('progress', {
progress,
written: (writeStream.bytesWritten / 1048576).toFixed(2),
total: (stats.CumulativeSize / 1048576).toFixed(2),
})
})

readStream.pipe(writeStream)
}

/**
*
*
* @export
* @param {string} magnetURI
* @param {DownloadOptions} options
* @param {EventEmitter} events
* @param {fs.WriteStream} writeStream
*/
export function downloadFromTorrent(magnetURI, options, events, writeStream) {
let client
let torrent

writeStream.on('finish', () => {
if (cancelled) {
bus.emit('cancelled')
return
// Error handler
events.on('close', () => {
attempt(() => {
if (torrent) {
torrent.destroy()
}

if (!fs.existsSync(filepath)) {
throw new AppError('The file was not saved correctly.', { title: 'Download failed.' })
if (client) {
client.destroy()
}
})
})

try {
client = new WebTorrent()

torrent = client.add(magnetURI)
} catch (err) {
events.emit('error', err)
return
}

const timeout = setTimeout(() => {
events.emit('error', new Error('timeout'))
}, 3000)

client.on('error', (err) => {
events.emit('error', err)
})

torrent.on('error', (err) => {
events.emit('error', err)
})

torrent.on('metadata', () => {
clearTimeout(timeout)

if (torrent.files.length > 1) {
events.emit('error', new Error('The torrent contains more than one file.'))
return
}

events.emit('peers', torrent.numPeers)

const file = torrent.files[0]

file.createReadStream().pipe(writeStream)
})

torrent.on('download', () => {
events.emit('progress', {
progress: torrent.progress,
written: (torrent.downloaded / 1048576).toFixed(2),
total: (torrent.length / 1048576).toFixed(2),
})
})

torrent.on('wire', () => {
events.emit('peers', torrent.numPeers)
})

/*
torrent.on('done', () => {
// Finish & Close
events.emit('finish', options.filepath)
events.emit('close')
})
*/
}

/**
*
* @param {string} url
* @param {DownloadOptions} [options]
*/
export function download(url, options = {}) {
const events = new EventEmitter()

let cancelled = false

// Options setup
options = merge({
showSaveAs: false,
directory: app.getPath('downloads'),
filename: basename(url).split('?')[0].split('#')[0],
}, options)

bus.emit('finish', null, filepath)
options.filepath = join(options.directory, options.filename)

if (options.showSaveAs) {
options.filepath = dialog.showSaveDialogSync({
defaultPath: options.filepath,
})
}

// Write stream
const writeStream = fs.createWriteStream(options.filepath)

writeStream.on('error', (err) => {
events.emit('error', err)
})

data.pipe(writeStream)
writeStream.on('finish', () => {
if (cancelled) {
events.emit('cancelled')
return
}

bus.on('cancel', () => {
cancelled = true
// Finish & Close
events.emit('finish', options.filepath)
events.emit('close')
})

attempt(() => {
writeStream.destroy()
data.destroy()
fs.unlinkSync(filepath)
})
// Cancel handler
events.on('cancel', () => {
cancelled = true

logger.info('Download cancelled by user.')
bus.emit('cancelled')
attempt(() => {
fs.unlinkSync(options.filepath)
})

return true
}).catch((err) => {
logger.info('Download cancelled by user.')

// Cancelled & Close
events.emit('cancelled')
events.emit('close')
})

// Error handler
events.on('error', (err) => {
attempt(() => {
writeStream.destroy(err)
fs.unlinkSync(filepath)
fs.unlinkSync(options.filepath)
})

logger.warn('Download cancelled due to an error.', err)
bus.emit('error', null, err)
logger.warn('Download error:', err)

// Error & Close
events.emit('close')
})

return bus
// Close handler
events.on('close', () => {
attempt(() => {
writeStream.destroy()
})
})

// Download!
if (startsWith(url, 'Qm')) {
downloadFromIPFS(url, options, events, writeStream)
} else if (startsWith(url, 'magnet:') || endsWith(url, '.torrent')) {
downloadFromTorrent(url, options, events, writeStream)
} else if (startsWith(url, 'http')) {
downloadFromHttp(url, options, events, writeStream)
} else {
setTimeout(() => {
events.emit('error', new Error('Invalid download address.'))
}, 0)
}

return events
}

/**

+ 5
- 0
src/modules/config/cli-errors.json View File

@@ -39,6 +39,11 @@
"title": "System out of memory",
"message": "Please buy more RAM before using DreamTime. Also verify that your system's RAM is in good condition."
},
{
"query": "Cannot allocate memory",
"title": "System out of memory",
"message": "Please buy more RAM before using DreamTime. Also verify that your system's RAM is in good condition."
},
{
"query": "CUDA out of memory",
"title": "GPU out of memory",

+ 2
- 2
src/modules/imagemagick.js View File

@@ -1,7 +1,7 @@
/* eslint-disable no-underscore-dangle */
import fs from 'fs'
import { initializeImageMagick, ImageMagick as ImageMagickWASM } from '@imagemagick/magick-wasm'
import { withString } from '@imagemagick/magick-wasm/util/string'
import { _withString } from '@imagemagick/magick-wasm/native/string'
import { Exception } from '@imagemagick/magick-wasm/exception/exception'
import { MagickFormat } from '@imagemagick/magick-wasm/magick-format'
import { MagickImage } from '@imagemagick/magick-wasm/magick-image'
@@ -72,7 +72,7 @@ export class ImageMagick {

await new Promise((resolve) => {
Exception.use((exception) => {
withString(`${geometry.width}x${geometry.height}+${geometry.x}+${geometry.y}!`, (geometryPtr) => {
_withString(`${geometry.width}x${geometry.height}+${geometry.x}+${geometry.y}!`, (geometryPtr) => {
ImageMagickWASM._api._MagickImage_BackgroundColor_Set(this.image._instance, 'white')
const newImage = ImageMagickWASM._api._MagickImage_Crop(this.image._instance, geometryPtr, exception.ptr)
this.image._setInstance(newImage, exception)

+ 2
- 1
src/modules/services/rollbar.js View File

@@ -16,6 +16,7 @@ import { BaseService } from './base'
import { dreamtrack } from './dreamtrack'
import { Consola } from '../consola'
import { settings } from '../system/settings'
import { requirements } from '../system'

const { system } = $provider
const { execSync } = remote.require('child_process')
@@ -38,7 +39,7 @@ class RollbarService extends BaseService {
* @type {boolean}
*/
get can() {
return system.online && isString(this.accessToken) && settings.telemetry.bugs // && process.env.NODE_ENV === 'production'
return system.online && isString(this.accessToken) && settings.telemetry.bugs && requirements.canNudify // && process.env.NODE_ENV === 'production'
}

/**

+ 125
- 48
src/modules/updater/base.js View File

@@ -10,10 +10,10 @@
import {
isNil, isArray, isPlainObject, find,
startsWith, filter, isEmpty, toNumber, isString,
endsWith, attempt,
} from 'lodash'
import axios from 'axios'
import compareVersions from 'compare-versions'
import deferred from 'deferred'
import filesize from 'filesize'
import delay from 'delay'
import { basename } from 'path'
@@ -22,7 +22,9 @@ import { Consola } from '../consola'

const { system } = $provider
const { getPath } = $provider.paths
const { existsSync, statSync, download } = $provider.fs
const {
existsSync, statSync, download, unlinkSync,
} = $provider.fs
const { dialog } = $provider.api
const { platform } = $provider.util

@@ -34,6 +36,13 @@ const extRegex = /(?:\.(zip|7z|exe|dmg|snap))?$/
*/
const GITHUB_API = 'https://api.github.com/repos'

const DMETHOD = {
ANY: 0,
HTTP: 1,
IPFS: 2,
TORRENT: 3,
}

export class BaseUpdater {
/**
* @type {boolean}
@@ -81,12 +90,49 @@ export class BaseUpdater {
/**
* @type {Array}
*/
downloadUrls = []
downloadAllUrls = []

/**
*
* @type {DMETHOD}
*/
downloadMethod = DMETHOD.ANY

getDownloadUrls(method) {
switch (method) {
case DMETHOD.ANY:
return this.downloadAllUrls

case DMETHOD.HTTP:
return this.downloadAllUrls.filter((url) => startsWith(url, 'http') && !endsWith(url, '.torrent'))

case DMETHOD.IPFS:
return this.downloadAllUrls.filter((url) => startsWith(url, 'Qm'))

case DMETHOD.TORRENT:
return this.downloadAllUrls.filter((url) => startsWith(url, 'magnet:') || endsWith(url, '.torrent'))

default:
return []
}
}

get downloadUrls() {
return this.getDownloadUrls(this.downloadMethod)
}

get hasIPFSUrls() {
return this.getDownloadUrls(DMETHOD.IPFS).length > 0
}

get hasTorrentUrls() {
return this.getDownloadUrls(DMETHOD.TORRENT).length > 0
}

/**
* @type {EventEmitter}
*/
downloadEvents
events

/**
* @type {Object}
@@ -97,6 +143,7 @@ export class BaseUpdater {
progress: 0,
written: -1,
total: -1,
peers: -1,
}

/**
@@ -162,11 +209,11 @@ export class BaseUpdater {
* @type {string}
*/
get filename() {
if (this.downloadUrls.length === 0) {
if (this.downloadAllUrls.length === 0) {
return null
}

for (const url of this.downloadUrls) {
for (const url of this.downloadAllUrls) {
const filename = basename(url).split('?')[0].split('#')[0]

if (isEmpty(filename) || isNil(extRegex.exec(filename)[1])) {
@@ -226,7 +273,7 @@ export class BaseUpdater {

this.refresh()

if (this.downloadUrls.length === 0) {
if (this.downloadAllUrls.length === 0) {
throw new Warning('No available download links found, please try again later.')
}

@@ -242,7 +289,7 @@ export class BaseUpdater {
}

refresh() {
this.downloadUrls = this._getDownloadUrls()
this.downloadAllUrls = this._getDownloadUrls()
}

/**
@@ -286,10 +333,6 @@ export class BaseUpdater {
urls.push(asset.browser_download_url)
}

// for now we use only the http/https protocol
// todo: ipfs
urls = urls.filter((item) => startsWith(item, 'http'))

return urls
}

@@ -361,6 +404,10 @@ export class BaseUpdater {
await this._install(filepath)
return
}

attempt(() => {
unlinkSync(filepath)
})
}

filepath = await this._download()
@@ -373,6 +420,22 @@ export class BaseUpdater {
}
}

pause() {
if (isNil(this.events)) {
return
}

this.events.emit('pause')
}

resume() {
if (isNil(this.events)) {
return
}

this.events.emit('resume')
}

/**
*
*/
@@ -381,7 +444,7 @@ export class BaseUpdater {
this._setUpdateProgress('installing')

// Avoid opening it while it is in use.
await delay(3000)
await delay(2000)

await this.install(filepath)
} catch (err) {
@@ -395,8 +458,11 @@ export class BaseUpdater {
async _download() {
let filepath

this.consola.debug('Download URLS:')
this.consola.debug(this.downloadUrls)

for (const url of this.downloadUrls) {
this._setUpdateProgress('downloading')
this._setUpdateProgress('preparing')

try {
// eslint-disable-next-line no-await-in-loop
@@ -424,58 +490,69 @@ export class BaseUpdater {
*/
_downloadFrom(url) {
this.consola.info(`Downloading update from: ${url}`)
const def = deferred()

this.downloadEvents = download(url, {
filename: this.filename,
})
return new Promise((resolve, reject) => {
this.events = download(url, {
filename: this.filename,
})

this.downloadEvents.on('progress', (payload) => {
if (payload.total > 0) {
this.update.progress = toNumber(payload.progress * 100).toFixed(2)
} else {
this.update.progress = -1
}
this.events.on('progress', (payload) => {
this._setUpdateProgress('downloading')

this.update.total = payload.total
this.update.written = payload.written
})
if (payload.total > 0) {
this.update.progress = toNumber(payload.progress * 100).toFixed(2)
} else {
this.update.progress = -1
}

this.downloadEvents.on('error', (err) => {
this.downloadEvents = null
def.reject(err)
})
this.update.total = payload.total
this.update.written = payload.written
})

this.events.on('peers', (value) => {
this.update.peers = value
})

this.downloadEvents.on('finish', (filepath) => {
const stats = statSync(filepath)
const size = filesize(stats.size, { exponent: 2, output: 'object' })
this.events.on('error', (err) => {
this.events = null
reject(err)
})

if (size.value < 20) {
// todo: better corrupt detection
def.reject(new Warning('Unable to download update.', 'The file is corrupt.'))
}
this.events.on('finish', (filepath) => {
this.events = null

this.downloadEvents = null
def.resolve(filepath)
})
if (!filepath || !existsSync(filepath)) {
reject(new Warning('Unable to download update.', 'The file has been downloaded but has not been saved.'))
return
}

this.downloadEvents.on('cancelled', () => {
this.downloadEvents = null
def.resolve()
})
const stats = statSync(filepath)
const size = filesize(stats.size, { exponent: 2, output: 'object' })

return def.promise
if (size.value < 20) {
reject(new Warning('Unable to download update.', 'The file has been downloaded corrupted.'))
return
}

resolve(filepath)
})

this.events.on('cancelled', () => {
this.events = null
resolve()
})
})
}

/**
*
*/
cancel() {
if (isNil(this.downloadEvents)) {
if (isNil(this.events)) {
return
}

this.downloadEvents.emit('cancel')
this.events.emit('cancel')
}

/**

+ 22
- 15
src/package.json View File

@@ -47,18 +47,18 @@
"@fortawesome/free-regular-svg-icons": "^5.14.0",
"@fortawesome/free-solid-svg-icons": "^5.14.0",
"@fortawesome/vue-fontawesome": "^0.1.10",
"@imagemagick/magick-wasm": "^0.0.2",
"@imagemagick/magick-wasm": "^0.0.3",
"@sweetalert2/theme-dark": "^3.2.0",
"axios": "^0.19.2",
"chokidar": "^3.4.1",
"chokidar": "^3.4.2",
"combokeys": "^3.0.1",
"compare-versions": "^3.6.0",
"cropperjs": "^1.5.7",
"cryptr": "^6.0.2",
"dayjs": "^1.8.31",
"dayjs": "^1.8.33",
"deferred": "^0.7.11",
"delay": "^4.4.0",
"electron-context-menu": "^2.2.0",
"electron-context-menu": "^2.3.0",
"electron-devtools-installer": "^3.1.1",
"electron-util": "^0.14.2",
"emoji-strip": "^1.0.1",
@@ -67,21 +67,27 @@
"filesize": "^6.1.0",
"form-data": "^3.0.0",
"fs-extra": "^9.0.1",
"go-ipfs-dep": "^0.6.0",
"he": "^1.2.0",
"instagram-save": "^1.3.2",
"intro.js": "^2.9.3",
"ipfs": "^0.49.0",
"ipfs-http-client": "^44.0.3",
"ipfsd-ctl": "^4.1.0",
"is-online": "^8.4.0",
"it-all": "^1.0.2",
"it-to-stream": "^0.1.2",
"js-event-bus": "^1.0.3",
"js-yaml": "^3.14.0",
"lodash": "^4.17.19",
"logrocket": "^1.0.10",
"lodash": "^4.17.20",
"logrocket": "^1.0.11",
"markdown-it": "^11.0.0",
"md5-file": "^5.0.0",
"melanke-watchjs": "^1.5.2",
"mime-types": "^2.1.27",
"node-7z": "^2.1.1",
"normalize-path": "^3.0.0",
"nuxt": "^2.14.0",
"nuxt": "^2.14.3",
"patch-package": "^6.2.2",
"popmotion": "^8.7.3",
"portal-vue": "^2.1.7",
@@ -89,7 +95,7 @@
"promise-worker": "^2.0.1",
"randomcolor": "^0.6.2",
"regedit": "^3.0.3",
"rollbar": "^2.19.1",
"rollbar": "^2.19.2",
"semver-regex": "^3.1.1",
"slash": "^3.0.0",
"sourcemapped-stacktrace": "^1.1.11",
@@ -100,11 +106,12 @@
"unique-names-generator": "^4.3.0",
"unzipper": "^0.10.11",
"uuid": "^8.3.0",
"vue-slider-component": "^3.2.2"
"vue-slider-component": "^3.2.4",
"webtorrent": "^0.108.6"
},
"devDependencies": {
"@babel/cli": "^7.10.5",
"@babel/core": "^7.11.0",
"@babel/core": "^7.11.1",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/plugin-proposal-export-default-from": "^7.10.4",
"@babel/plugin-proposal-optional-chaining": "^7.11.0",
@@ -113,17 +120,17 @@
"@nuxtjs/eslint-config": "^3.1.0",
"@nuxtjs/eslint-module": "^2.0.0",
"@nuxtjs/style-resources": "^1.0.0",
"@nuxtjs/tailwindcss": "^2.1.1",
"@nuxtjs/tailwindcss": "^3.0.0",
"babel-eslint": "^10.1.0",
"babel-plugin-lodash": "^3.3.4",
"babel-plugin-module-resolver": "^4.0.0",
"babel-plugin-transform-inline-environment-variables": "^0.4.3",
"babel-watch": "^7.0.0",
"cross-env": "^7.0.2",
"electron": "9.1.2",
"electron": "9.2.1",
"electron-builder": "^22.8.0",
"env-cmd": "^10.1.0",
"eslint": "^7.6.0",
"eslint": "^7.7.0",
"eslint-config-airbnb-base": "^14.2.0",
"eslint-config-standard": ">=14.1.1",
"eslint-import-resolver-node": "^0.3.4",
@@ -148,10 +155,10 @@
"ngrok": "^3.2.7",
"nuxt-purgecss": "^1.0.0",
"sass": "^1.26.10",
"sass-loader": "^9.0.2",
"sass-loader": "^9.0.3",
"shx": "^0.3.2",
"spectron": "^11.1.0",
"tailwindcss": "^1.6.0",
"tailwindcss": "1.6.3",
"tailwindcss-alpha": "hacknug/tailwindcss-alpha#feature/tests",
"worker-loader": "^2.0.0"
},

+ 1
- 1
src/pages/nudify/_id/results.vue View File

@@ -49,7 +49,7 @@
</template>
</PageHeader>

<div v-if="photo.isScaleModeCorrected" class="notification notification--warning">
<div v-if="photo.isScaleModeCorrected && !photo.pending" class="notification notification--warning">
<span class="icon"><font-awesome-icon icon="exclamation-triangle" /></span>
You have selected the scale method <strong>{{ photo.preferences.advanced.scaleMode }}</strong> but have not used the tool! <strong>Automatic Resize</strong> will be used instead.
</div>

+ 29
- 0
src/pages/settings/app.vue View File

@@ -11,6 +11,10 @@
</h3>

<template v-slot:right>
<button class="button button--danger" @click="reset()">
<span>Reset</span>
</button>

<button v-tooltip="'Open the developer tools. This includes the application logs.'"
class="button"
@click.prevent="openDevTools">
@@ -80,6 +84,8 @@
</template>

<script>
import { cloneDeep, merge } from 'lodash'
import Swal from 'sweetalert2/dist/sweetalert2'
import { VModel } from '~/mixins'
import { events } from '~/modules'
import { requirements } from '~/modules/system'
@@ -100,6 +106,29 @@ export default {
onChangeAds() {
events.emit('settings:ads')
},

async reset() {
const response = await Swal.fire({
title: 'Are you sure?',
text: 'This will set all options in this section to their default values.',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#F44336',
confirmButtonText: 'Yes',
})

if (!response.value) {
return
}

// eslint-disable-next-line no-underscore-dangle
const settings = cloneDeep($provider.settings._default.app)
delete settings.window

this.value$.app = merge(this.value$.app, settings)

window.$redirect('/')
},
},
}
</script>

+ 35
- 2
src/pages/settings/folders.vue View File

@@ -9,6 +9,12 @@
<h3 class="subtitle">
Change the location of the components and files.
</h3>

<template v-slot:right>
<button class="button button--danger" @click="reset()">
<span>Reset</span>
</button>
</template>
</PageHeader>

<div v-if="$dream.isPortable" class="notification">
@@ -102,12 +108,13 @@
</template>

<script>
import { isNil } from 'lodash'
import { isNil, cloneDeep, merge } from 'lodash'
import Swal from 'sweetalert2/dist/sweetalert2'
import { VModel } from '~/mixins'

const { paths } = $provider
const { existsSync } = $provider.fs
const { dialog } = $provider.api
const { dialog, app } = $provider.api

export default {
mixins: [VModel],
@@ -170,6 +177,32 @@ export default {
const dir = this.showOpenDialog(this.value$.folders.waifu)
this.value$.folders.waifu = dir
},

async reset() {
const response = await Swal.fire({
title: 'Are you sure?',
text: 'This will set all options in this section to their default values.',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#F44336',
confirmButtonText: 'Yes',
})

if (!response.value) {
return
}

// eslint-disable-next-line no-underscore-dangle
const settings = cloneDeep($provider.settings._default.folders)

this.value$.folders = merge(this.value$.folders, settings)

// Ugly...
setTimeout(() => {
app.relaunch()
app.quit()
}, 1000)
},
},
}
</script>

+ 32
- 0
src/pages/settings/notifications.vue View File

@@ -9,6 +9,12 @@
<h3 class="subtitle">
Desktop notification settings.
</h3>

<template v-slot:right>
<button class="button button--danger" @click="reset()">
<span>Reset</span>
</button>
</template>
</PageHeader>

<section class="box">
@@ -24,9 +30,35 @@
</template>

<script>
import { cloneDeep, merge } from 'lodash'
import Swal from 'sweetalert2/dist/sweetalert2'
import { VModel } from '~/mixins'

export default {
mixins: [VModel],

methods: {
async reset() {
const response = await Swal.fire({
title: 'Are you sure?',
text: 'This will set all options in this section to their default values.',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#F44336',
confirmButtonText: 'Yes',
})

if (!response.value) {
return
}

// eslint-disable-next-line no-underscore-dangle
const settings = cloneDeep($provider.settings._default.notifications)

this.value$.notifications = merge(this.value$.notifications, settings)

window.$redirect('/')
},
},
}
</script>

+ 33
- 0
src/pages/settings/preferences.vue View File

@@ -10,6 +10,12 @@
Default preferences for new photos.
<AppTip tooltip="You can change these options individually in each photo." />
</h3>

<template v-slot:right>
<button class="button button--danger" @click="reset()">
<span>Reset</span>
</button>
</template>
</PageHeader>

<AppNotification v-if="value$.preferences.mode === 3" name="advanced-mode" class="notification--info">
@@ -22,10 +28,37 @@
</template>

<script>
import { cloneDeep, merge } from 'lodash'
import Swal from 'sweetalert2/dist/sweetalert2'
import { VModel } from '~/mixins'

export default {
mixins: [VModel],

methods: {
async reset() {
const response = await Swal.fire({
title: 'Are you sure?',
text: 'This will set all options in this section to their default values.',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#F44336',
confirmButtonText: 'Yes',
})

if (!response.value) {
return
}

// eslint-disable-next-line no-underscore-dangle
const settings = cloneDeep($provider.settings._default.preferences)
delete settings.advanced.device

this.value$.preferences = merge(this.value$.preferences, settings)

window.$redirect('/')
},
},
}
</script>


+ 32
- 0
src/pages/settings/processing.vue View File

@@ -9,6 +9,12 @@
<h3 class="subtitle">
Settings that affect the use of resources for the nudification.
</h3>

<template v-slot:right>
<button class="button button--danger" @click="reset()">
<span>Reset</span>
</button>
</template>
</PageHeader>

<AppNotification name="device-change">
@@ -53,6 +59,8 @@
</template>

<script>
import { cloneDeep, merge } from 'lodash'
import Swal from 'sweetalert2/dist/sweetalert2'
import { VModel } from '~/mixins'

export default {
@@ -63,5 +71,29 @@ export default {
return process.platform === 'darwin'
},
},

methods: {
async reset() {
const response = await Swal.fire({
title: 'Are you sure?',
text: 'This will set all options in this section to their default values.',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#F44336',
confirmButtonText: 'Yes',
})

if (!response.value) {
return
}

// eslint-disable-next-line no-underscore-dangle
const settings = cloneDeep($provider.settings._default.processing)

this.value$.processing = merge(this.value$.processing, settings)

window.$redirect('/')
},
},
}
</script>

+ 2
- 2
src/pages/settings/share.vue View File

@@ -7,7 +7,7 @@
</h2>

<h3 class="subtitle">
Share your photo preferences with the world.
Share or save your photo preferences.
</h3>
</PageHeader>

@@ -30,7 +30,7 @@
Export
</h2>
<h3 class="subtitle">
Share your preferences or save them for later.
Save your photo preferences in an easy-to-remember ID.
</h3>
</div>


+ 37
- 0
src/pages/settings/telemetry.vue View File

@@ -9,8 +9,19 @@
<h3 class="subtitle">
Settings for sending usage information to improve the application.
</h3>

<template v-slot:right>
<button class="button button--danger" @click="reset()">
<span>Reset</span>
</button>
</template>
</PageHeader>

<div class="notification">
<span class="icon"><font-awesome-icon icon="info-circle" /></span>
<span>Changing this options needs a restart to take effect.</span>
</div>

<section class="box">
<div class="box__content">
<SettingsField v-model="value$" field-id="user" readonly />
@@ -24,10 +35,36 @@
</template>

<script>
import { cloneDeep, merge } from 'lodash'
import Swal from 'sweetalert2/dist/sweetalert2'
import { VModel } from '~/mixins'

export default {
mixins: [VModel],

methods: {
async reset() {
const response = await Swal.fire({
title: 'Are you sure?',
text: 'This will set all options in this section to their default values.',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#F44336',
confirmButtonText: 'Yes',
})

if (!response.value) {
return
}

// eslint-disable-next-line no-underscore-dangle
const settings = cloneDeep($provider.settings._default.telemetry)

this.value$.telemetry = merge(this.value$.telemetry, settings)

window.$redirect('/')
},
},
}
</script>


+ 28
- 0
src/pages/wizard/checkpoints.vue View File

@@ -41,6 +41,34 @@
<AppBox>
<ProjectUpdate project="checkpoints" />
</AppBox>

<AppBox title="Settings.">
<!-- Download protocol. -->
<MenuItem
label="Download protocol.">
<template v-slot:description>
<span class="item__description">Select the protocol that will be used to download the file. <AppTip tooltip="- <strong>Any</strong>: Use all protocols if necessary.<br>- <strong>HTTP</strong>: Download the file from verified servers. Fastest and most reliable for most connections.<br>- <strong>Torrent & IPFS</strong>: Download the file from other computers. It is possible to cancel the download and resume it later. More reliable for low speed connections. May require a few minutes of preparation before starting the download." /></span>
</template>

<select v-model="updater.downloadMethod" class="input">
<option :value="0">
Any
</option>

<option :value="1">
HTTP
</option>

<option v-if="updater.hasTorrentUrls" :value="3">
Torrent
</option>

<option v-if="updater.hasIPFSUrls" :value="2">
IPFS
</option>
</select>
</MenuItem>
</AppBox>
</div>
</div>
</template>

+ 26
- 0
src/pages/wizard/power.vue View File

@@ -57,6 +57,32 @@
</AppBox>

<AppBox title="Settings.">
<!-- Download protocol. -->
<MenuItem
label="Download protocol.">
<template v-slot:description>
<span class="item__description">Select the protocol that will be used to download the file. <AppTip tooltip="- <strong>Any</strong>: Use all protocols if necessary.<br>- <strong>HTTP</strong>: Download the file from verified servers. Fastest and most reliable for most connections.<br>- <strong>Torrent & IPFS</strong>: Download the file from other computers with the option to cancel at any time and resume later. More reliable for unstable and low speed connections. May require a few minutes of preparation before starting the download." /></span>
</template>

<select v-model="updater.downloadMethod" class="input">
<option :value="0">
Any
</option>

<option :value="1">
HTTP
</option>

<option v-if="updater.hasTorrentUrls" :value="3">
Torrent
</option>

<option v-if="updater.hasIPFSUrls" :value="2">
IPFS
</option>
</select>
</MenuItem>

<SettingsField v-if="!isMacOS"
field-id="preferences.advanced.device"
ignore-hardcoded

+ 26
- 0
src/pages/wizard/waifu.vue View File

@@ -63,6 +63,32 @@
</AppBox>

<AppBox title="Settings.">
<!-- Download protocol. -->
<MenuItem
label="Download protocol.">
<template v-slot:description>
<span class="item__description">Select the protocol that will be used to download the file. <AppTip tooltip="- <strong>Any</strong>: Use all protocols if necessary.<br>- <strong>HTTP</strong>: Download the file from verified servers. Fastest and most reliable for most connections.<br>- <strong>Torrent & IPFS</strong>: Download the file from other computers. It is possible to cancel the download and resume it later. More reliable for low speed connections. May require a few minutes of preparation before starting the download." /></span>
</template>

<select v-model="updater.downloadMethod" class="input">
<option :value="0">
Any
</option>

<option :value="1">
HTTP
</option>

<option v-if="updater.hasTorrentUrls" :value="3">
Torrent
</option>

<option v-if="updater.hasIPFSUrls" :value="2">
IPFS
</option>
</select>
</MenuItem>

<SettingsField v-if="!$dreamtime.isPortable" label="Location" field-id="folders.waifu">
<input
v-model="$settings.folders.waifu"

+ 4596
- 290
src/yarn.lock
File diff suppressed because it is too large
View File


Loading…
Cancel
Save