- Improvement: It is now possible to restore the settings to their default values.tags/v1.5.6
@@ -33,5 +33,5 @@ dialog { | |||
} | |||
dialog::backdrop { | |||
background: rgba(25, 25, 26, 0.90); | |||
background: rgba(0,0,0, 0.90); | |||
} |
@@ -167,7 +167,8 @@ export default { | |||
@apply text-lg; | |||
} | |||
&:hover { | |||
&:hover, | |||
&.nuxt-link-exact-active { | |||
@apply text-primary border-primary; | |||
} | |||
} |
@@ -174,7 +174,7 @@ export default { | |||
&::v-deep { | |||
.item__action { | |||
@apply flex items-center justify-center; | |||
max-width: 300px; | |||
//max-width: 300px; | |||
} | |||
} | |||
} |
@@ -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> | |||
@@ -71,7 +71,7 @@ class DreamApp { | |||
process.on('unhandledRejection', (err) => { | |||
logger.warn('Unhandled rejection!', err) | |||
AppError.handle(err) | |||
AppError.handle(err, 'warning') | |||
return true | |||
}) |
@@ -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, | |||
}) | |||
} | |||
@@ -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 | |||
} | |||
/** |
@@ -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", |
@@ -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) |
@@ -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' | |||
} | |||
/** |
@@ -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') | |||
} | |||
/** |
@@ -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" | |||
}, |
@@ -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> |
@@ -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> |
@@ -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> |
@@ -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> |
@@ -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> | |||
@@ -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> |
@@ -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> | |||
@@ -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> | |||
@@ -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> |
@@ -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 |
@@ -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" |