Browse Source

- v1.1.0 RC

- Option to skip the Cropper tool and pass the original photo to the algorithm if it is already 512x512
- Notification system for updates and transformations
- Randomize and Progresive preferences per run
- Fix: When saving a photo and changing its name it was saved without extension
- Error messages for most of the problems that occur during the transformation with a possible solution.
- Changes in the results page design
tags/v1.4.4
Ivan Bravo Bravo 1 year ago
parent
commit
d8bfe506a4
42 changed files with 1278 additions and 344 deletions
  1. 1
    1
      src/.env
  2. 4
    0
      src/assets/css/components/_notification.scss
  3. 207
    32
      src/components/Nudity/Job.vue
  4. 6
    0
      src/components/Nudity/Upload.vue
  5. 234
    101
      src/components/Settings/SettingsPreferences.vue
  6. 1
    0
      src/components/UI/AppExternalLink.vue
  7. 1
    1
      src/components/UI/AppPhoto.vue
  8. 20
    2
      src/components/UI/BoxSectionItem.vue
  9. 3
    0
      src/electron/index.js
  10. 1
    0
      src/electron/modules/rollbar.js
  11. 127
    42
      src/electron/modules/settings.js
  12. 6
    1
      src/electron/tools/fs.js
  13. 7
    6
      src/electron/tools/index.js
  14. 1
    1
      src/electron/tools/shell.js
  15. 2
    0
      src/middleware/checks.js
  16. 5
    0
      src/modules/file.js
  17. 16
    0
      src/modules/helpers.js
  18. 226
    59
      src/modules/models/photo-job.js
  19. 17
    7
      src/modules/models/photo.js
  20. 6
    1
      src/modules/update/dreamtime.js
  21. 31
    0
      src/modules/updater.js
  22. 19
    8
      src/modules/web-error.js
  23. 2
    2
      src/package.json
  24. 10
    2
      src/pages/index.vue
  25. 26
    14
      src/pages/nudity/crop.vue
  26. 58
    50
      src/pages/nudity/results.vue
  27. 14
    4
      src/pages/system/about.vue
  28. 1
    0
      src/pages/system/settings.vue
  29. 3
    1
      src/pages/system/settings/folders.vue
  30. 45
    0
      src/pages/system/settings/notifications.vue
  31. 5
    1
      src/pages/system/settings/preferences.vue
  32. 2
    1
      src/pages/system/settings/processing.vue
  33. 3
    1
      src/pages/system/settings/telemetry.vue
  34. 4
    3
      src/plugins/boot.client.js
  35. 0
    3
      src/scripts/.gitignore
  36. 1
    0
      src/scripts/teamcity/.gitignore
  37. 65
    0
      src/scripts/teamcity/ubuntu16/build-cpu.sh
  38. 9
    0
      src/scripts/teamcity/ubuntu16/build.sh
  39. 9
    0
      src/scripts/teamcity/ubuntu16/setup.sh
  40. 66
    0
      src/scripts/teamcity/windows/build-cpu.ps1
  41. 7
    0
      src/scripts/teamcity/windows/build.ps1
  42. 7
    0
      src/scripts/teamcity/windows/setup.ps1

+ 1
- 1
src/.env View File

@@ -1,6 +1,6 @@
APP_NAME = DreamTime
APP_STATUS = stable
APP_VERSION = 1.0.1
APP_VERSION = 1.1.0

SERVER_PORT = 3000
SERVER_HOST = localhost

+ 4
- 0
src/assets/css/components/_notification.scss View File

@@ -5,6 +5,10 @@
@apply underline;
}

&.is-warning {
@apply bg-warning border-warning;
}

&.is-danger {
@apply bg-danger-25 border-danger-20;
}

+ 207
- 32
src/components/Nudity/Job.vue View File

@@ -1,37 +1,85 @@
<template>
<div class="c-nudity-job">
<div v-if="job.hasFinished" class="job-status">
<p class="status-title">{{ job.timer.duration }}s</p>
<figure class="__preview">
<img v-if="job.hasFinished" :src="outputDataURL" />
<span v-else v-tooltip="'Loading...'">๐Ÿ’ญ</span>
</figure>

<div class="buttons">
<button class="button is-success" @click.prevent="save">Save</button>
<button class="button is-danger is-sm" @click.prevent="rerun">Rerun</button>
</div>
</div>
<div class="__content">
<!-- Actions -->
<details class="__section" open>
<summary class="__title">Actions</summary>

<div v-else-if="job.hasFailed" class="job-status">
<p class="status-title text-danger">FAIL!</p>
<div class="__buttons">
<button v-if="job.hasFinished" class="button is-success" @click.prevent="save">Save</button>
<button v-if="job.hasFinished || job.hasFailed" class="button is-danger is-sm" @click.prevent="rerun">Rerun</button>
</div>
</details>

<div class="buttons">
<button class="button is-sm" @click.prevent="rerun">Rerun</button>
</div>
</div>
<details v-if="job.hasFinished || job.isLoading" class="__section" open>
<summary class="__title">Duration</summary>

<div v-else-if="job.isLoading" class="job-status">
<p class="status-title">{{ job.timer.duration }}s</p>
<p class="status-text">Processing...</p>
</div>
<div class="__status">
<p>{{ job.timer.duration }}s</p>
</div>
</details>

<div v-else class="job-status">
<p class="status-text">Pending</p>
</div>
<details v-else-if="job.hasFailed" class="__section" open>
<summary class="__title">Status</summary>

<div class="job-photos">
<app-photo v-if="job.hasFinished" :src="outputDataURL">Result</app-photo>
</div>
<div class="__status text-danger">
<p>Fail</p>
</div>
</details>

<details v-else class="__section" open>
<summary class="__title">Status</summary>

<div class="__status">
<p>Pending...</p>
</div>
</details>

<!-- Preferences -->
<details class="__section">
<summary class="__title">Preferences</summary>

<div class="__preferences">
<p>
<span class="__name">Boobs size</span>
<span class="__value">{{ job.preferences.boobs.size | size }}</span>
</p>

<div class="job-console">
<li v-for="(item, index) in job.cli.lines" :key="index" :class="item.css">{{ item.text }}</li>
<p>
<span class="__name">Areola size</span>
<span class="__value">{{ job.preferences.areola.size | size }}</span>
</p>

<p>
<span class="__name">Nipple size</span>
<span class="__value">{{ job.preferences.nipple.size | size }}</span>
</p>

<p>
<span class="__name">Vagina size</span>
<span class="__value">{{ job.preferences.vagina.size | size }}</span>
</p>

<p>
<span class="__name">Pubic Hair size</span>
<span class="__value">{{ job.preferences.pubicHair.size | size }}</span>
</p>
</div>
</details>

<!-- Console -->
<details class="__section">
<summary class="__title">Console</summary>

<div class="__console">
<li v-for="(item, index) in job.cli.lines" :key="index" :class="item.css">> {{ item.text }}</li>
</div>
</details>
</div>
</div>
</template>
@@ -40,6 +88,11 @@
import _ from 'lodash'

export default {
filters: {
size(value) {
return Number.parseFloat(value).toFixed(2)
}
},
props: {
job: {
type: Object,
@@ -60,9 +113,15 @@ export default {
},

methods: {
view() {},

save() {
const savePath = $tools.shell.showSaveDialogSync({
defaultPath: this.job.getFileName()
const savePath = $tools.shell.showSaveDialog({
defaultPath: this.job.getFileName(),
filters: [
{ name: 'PNG', extensions: ['png'] }
// { name: 'JPEG', extensions: ['jpg'] }
]
})

if (_.isNil(savePath)) {
@@ -80,9 +139,94 @@ export default {
</script>

<style lang="scss">
.c-nudity-job {
@apply flex;

.__preview {
@apply flex justify-center items-center
rounded rounded-tr-none rounded-br-none
border-2 border-dark border-r-0
text-3xl;
width: 125px;
height: 125px;

img {
@apply w-full h-full rounded rounded-tr-none rounded-br-none;
transition: all 0.15s ease-in-out;

&:hover {
@apply rounded z-50;
transform: scale(3);
}
}
}

.__content {
@apply flex-1 flex flex-col
bg-dark
rounded
rounded-tl-none
rounded-bl-none
px-4
shadow;

width: 200px;

.__section {
@apply py-3;

&:not(:last-child) {
@apply border-b border-dark-400;
}

.__title {
@apply text-xs uppercase text-generic-300 mb-2 cursor-pointer;
}

.__status {
@apply flex justify-center
text-xl font-bold;
}

.__buttons {
@apply flex justify-center items-center;
}

.__console {
@apply p-3 bg-black overflow-auto rounded;
height: 150px;

li {
@apply font-mono text-xs text-generic-100 mb-3 block;

&.text-danger {
@apply text-danger;
}
}
}

.__preferences {
p {
@apply text-sm;

.__name {
@apply inline-block text-generic-300;
width: 150px;
}

.__value {
@apply inline-block font-bold text-generic-100;
}
}
}
}
}
}

/*
.c-nudity-job {
@apply flex pb-4;
min-height: 150px;
height: 170px;

&:not(:first-child) {
@apply pt-4;
@@ -92,14 +236,28 @@ export default {
@apply border-b border-dark-400;
}

.job-section {
@apply p-2 mb-0;
width: 200px;

&:not(:last-child) {
@apply mr-5;
}
}

.job-photos {
@apply flex-1 inline-flex;
width: auto;
}

.job-status {
@apply flex flex-col justify-center items-center mr-5 mb-0;
@apply flex flex-col justify-center items-center;
width: 150px;

& > div {
@apply text-center;
}

.buttons {
@apply flex flex-col justify-center items-center mt-3;
}
@@ -118,9 +276,7 @@ export default {
}

.job-console {
@apply bg-black p-2 overflow-auto rounded;
width: 200px;
height: 150px;
@apply bg-black overflow-auto rounded;

li {
@apply font-mono text-xs text-generic-100 mb-3 block;
@@ -130,5 +286,24 @@ export default {
}
}
}

.job-preferences {
@apply flex flex-col justify-center items-center;

p {
@apply text-sm;

.preference-name {
@apply inline-block;
width: 150px;
}

.preference-value {
@apply inline-block;
@apply font-bold;
}
}
}
}
*/
</style>

+ 6
- 0
src/components/Nudity/Upload.vue View File

@@ -151,6 +151,8 @@ export default {
return
}

$nucleus.track('UPLOAD_SELECTED')

this.startFromFile(files[0])
event.target.value = ''
},
@@ -164,6 +166,8 @@ export default {
return
}

$nucleus.track('UPLOAD_URL')

this.startFromURL(this.webAddress)
},

@@ -204,8 +208,10 @@ export default {
const externalURL = event.dataTransfer.getData('url')

if (files.length > 0) {
$nucleus.track('UPLOAD_DROP')
this.startFromFile(files[0])
} else if (externalURL.length > 0) {
$nucleus.track('UPLOAD_DROP_URL')
this.startFromURL(externalURL)
}
}

+ 234
- 101
src/components/Settings/SettingsPreferences.vue View File

@@ -1,105 +1,238 @@
<template>
<section class="box box-section">
<box-section-item label="Boob Size" :description="`Current value: ${currentValue.boobsSize}`">
<div class="slider-container">
<input
v-model="currentValue.boobsSize"
type="range"
class="slider"
in="0.3"
max="2"
step="0.1" />
<span class="min">0.3</span>
<span class="max">2.0</span>
</div>
</box-section-item>

<box-section-item label="Areola Size" :description="`Current value: ${currentValue.areolaSize}`">
<div class="slider-container">
<input
v-model="currentValue.areolaSize"
type="range"
class="slider"
in="0.3"
max="2"
step="0.1" />
<span class="min">0.3</span>
<span class="max">2.0</span>
</div>
</box-section-item>

<box-section-item label="Nipple Size" :description="`Current value: ${currentValue.nippleSize}`">
<div class="slider-container">
<input
v-model="currentValue.nippleSize"
type="range"
class="slider"
in="0.3"
max="2"
step="0.1" />
<span class="min">0.3</span>
<span class="max">2.0</span>
</div>
</box-section-item>

<box-section-item label="Vagina Size" :description="`Current value: ${currentValue.vaginaSize}`">
<div class="slider-container">
<input
v-model="currentValue.vaginaSize"
type="range"
class="slider"
in="0.3"
max="1.5"
step="0.1"></input>
<span class="min">0.3</span>
<span class="max">1.5</span>
</div>
</box-section-item>

<box-section-item label="Pubic Hair" :description="`Current value: ${currentValue.pubicHairSize}`">
<div class="slider-container">
<input
v-model="currentValue.pubicHairSize"
type="range"
class="slider"
in="0"
max="2"
step="0.1"></input>
<span class="min">None</span>
<span class="max">2.0</span>
</div>
</box-section-item>

<box-section-item
label="Number of executions"
description="The transformation can be different each time, here you can set how many times your photo will be processed. In the end you can save the one you like best.">
<input v-model="currentValue.executions" type="number" min="1" class="input" />
</box-section-item>

<box-section-item v-if="false" label="Photo restoration" description="Restore the cropped photo to the original photo.">
<select v-model="currentValue.useRestoration" class="input">
<option :value="true">Enabled</option>
<option :value="false">Disabled</option>
</select>
</box-section-item>

<box-section-item v-if="false" label="waifu2x" description="waifu2x will try to resize your transformed photo to 1024x1024 with the least possible quality loss.">
<select v-model="currentValue.useWaifu" class="input">
<option :value="true">Enabled</option>
<option :value="false">Disabled</option>
</select>
</box-section-item>

<box-section-item
v-if="false"
label="Use Custom Mask"
hint="(Advanced) You can edit the masks of the photo before processing.">
<select v-model="currentValue.useCustomMask" class="input">
<option :value="false">Disabled</option>
<option :value="true">Enabled</option>
</select>
</box-section-item>
</section>
<div class="c-settings-preferences">
<section class="box box-section">
<box-section-item
label="Number of runs"
description="The transformation can be different each time, here you can set how many times your photo will be processed. In the end you can save the one you like best.">
<input v-model="currentValue.executions" type="number" min="1" class="input" />
</box-section-item>

<box-section-item
label="Randomize every run"
description="Random preferences will be set at each run">
<select v-model="currentValue.randomizePreferences" class="input">
<option :value="true">Enabled</option>
<option :value="false">Disabled</option>
</select>
</box-section-item>

<box-section-item
v-show="!currentValue.randomizePreferences"
label="Progressive every run"
description="Preferences will increase their value +0.2 in each run">
<select v-model="currentValue.progressivePreferences" class="input">
<option :value="true">Enabled</option>
<option :value="false">Disabled</option>
</select>
</box-section-item>
</section>

<section class="box box-section">
<box-section-item label="Boob Size" :description="`Current value: ${currentValue.boobs.size}`">
<div class="slider-container">
<input
v-model="currentValue.boobs.size"
type="range"
class="slider"
in="0.3"
max="2"
step="0.1" />
<span class="min">0.3</span>
<span class="max">2.0</span>
</div>
</box-section-item>

<box-section-item
v-show="currentValue.randomizePreferences"
label="Randomize"
description="Randomize the value in each run.">
<select v-model="currentValue.boobs.randomize" class="input">
<option :value="true">Enabled</option>
<option :value="false">Disabled</option>
</select>
</box-section-item>

<box-section-item
v-show="!currentValue.randomizePreferences && currentValue.progressivePreferences"
label="Progressive"
description="Increase the value progressively in each run">
<select v-model="currentValue.boobs.progressive" class="input">
<option :value="true">Enabled</option>
<option :value="false">Disabled</option>
</select>
</box-section-item>
</section>

<section class="box box-section">
<box-section-item label="Areola Size" :description="`Current value: ${currentValue.areola.size}`">
<div class="slider-container">
<input
v-model="currentValue.areola.size"
type="range"
class="slider"
in="0.3"
max="2"
step="0.1" />
<span class="min">0.3</span>
<span class="max">2.0</span>
</div>
</box-section-item>

<box-section-item
v-show="currentValue.randomizePreferences"
label="Randomize"
description="Randomize the value in each run.">
<select v-model="currentValue.areola.randomize" class="input">
<option :value="true">Enabled</option>
<option :value="false">Disabled</option>
</select>
</box-section-item>

<box-section-item
v-show="!currentValue.randomizePreferences && currentValue.progressivePreferences"
label="Progressive"
description="Increase the value progressively in each run">
<select v-model="currentValue.areola.progressive" class="input">
<option :value="true">Enabled</option>
<option :value="false">Disabled</option>
</select>
</box-section-item>
</section>

<section class="box box-section">
<box-section-item label="Nipple Size" :description="`Current value: ${currentValue.nipple.size}`">
<div class="slider-container">
<input
v-model="currentValue.nipple.size"
type="range"
class="slider"
in="0.3"
max="2"
step="0.1" />
<span class="min">0.3</span>
<span class="max">2.0</span>
</div>
</box-section-item>

<box-section-item
v-show="currentValue.randomizePreferences"
label="Randomize"
description="Randomize the value in each run.">
<select v-model="currentValue.nipple.randomize" class="input">
<option :value="true">Enabled</option>
<option :value="false">Disabled</option>
</select>
</box-section-item>

<box-section-item
v-show="!currentValue.randomizePreferences && currentValue.progressivePreferences"
label="Progressive"
description="Increase the value progressively in each run">
<select v-model="currentValue.nipple.progressive" class="input">
<option :value="true">Enabled</option>
<option :value="false">Disabled</option>
</select>
</box-section-item>
</section>

<section class="box box-section">
<box-section-item label="Vagina Size" :description="`Current value: ${currentValue.vagina.size}`">
<div class="slider-container">
<input
v-model="currentValue.vagina.size"
type="range"
class="slider"
in="0.3"
max="1.5"
step="0.1"></input>
<span class="min">0.3</span>
<span class="max">1.5</span>
</div>
</box-section-item>

<box-section-item
v-show="currentValue.randomizePreferences"
label="Randomize"
description="Randomize the value in each run.">
<select v-model="currentValue.vagina.randomize" class="input">
<option :value="true">Enabled</option>
<option :value="false">Disabled</option>
</select>
</box-section-item>

<box-section-item
v-show="!currentValue.randomizePreferences && currentValue.progressivePreferences"
label="Progressive"
description="Increase the value progressively in each run">
<select v-model="currentValue.vagina.progressive" class="input">
<option :value="true">Enabled</option>
<option :value="false">Disabled</option>
</select>
</box-section-item>
</section>

<section class="box box-section">
<box-section-item label="Pubic Hair" :description="`Current value: ${currentValue.pubicHair.size}`">
<div class="slider-container">
<input
v-model="currentValue.pubicHair.size"
type="range"
class="slider"
in="0"
max="2"
step="0.1"></input>
<span class="min">Disabled</span>
<span class="max">2.0</span>
</div>
</box-section-item>

<box-section-item
v-show="currentValue.randomizePreferences"
label="Randomize"
description="Randomize the value in each run.">
<select v-model="currentValue.pubicHair.randomize" class="input">
<option :value="true">Enabled</option>
<option :value="false">Disabled</option>
</select>
</box-section-item>

<box-section-item
v-show="!currentValue.randomizePreferences && currentValue.progressivePreferences"
label="Progressive"
description="Increase the value progressively in each run">
<select v-model="currentValue.pubicHair.progressive" class="input">
<option :value="true">Enabled</option>
<option :value="false">Disabled</option>
</select>
</box-section-item>
</section>

<section class="box box-section">
<box-section-item v-if="false" label="Photo restoration" description="Restore the cropped photo to the original photo.">
<select v-model="currentValue.useRestoration" class="input">
<option :value="true">Enabled</option>
<option :value="false">Disabled</option>
</select>
</box-section-item>

<box-section-item v-if="false" label="waifu2x" description="waifu2x will try to resize your transformed photo to 1024x1024 with the least possible quality loss.">
<select v-model="currentValue.useWaifu" class="input">
<option :value="true">Enabled</option>
<option :value="false">Disabled</option>
</select>
</box-section-item>

<box-section-item
v-if="false"
label="Use Custom Mask"
hint="(Advanced) You can edit the masks of the photo before processing.">
<select v-model="currentValue.useCustomMask" class="input">
<option :value="false">Disabled</option>
<option :value="true">Enabled</option>
</select>
</box-section-item>
</section>
</div>
</template>

<script>

+ 1
- 0
src/components/UI/AppExternalLink.vue View File

@@ -15,6 +15,7 @@ export default {

methods: {
openExternal() {
$nucleus.track('EXTERNAL_LINK', { href: this.href })
$tools.shell.openExternal(this.href)
}
}

+ 1
- 1
src/components/UI/AppPhoto.vue View File

@@ -31,7 +31,7 @@ export default {
transition: all 0.15s ease-in-out;

&:hover {
@apply absolute rounded z-50;
@apply rounded z-50;
transform: scale(2.5);
}
}

+ 20
- 2
src/components/UI/BoxSectionItem.vue View File

@@ -1,5 +1,5 @@
<template>
<itemtag :tag="tag" :href="href" class="box-section-item">
<itemtag :version="version" :tag="tag" :href="href" class="box-section-item">
<slot name="icon">
<figure v-if="isURL(icon)" class="item-icon">
<img :src="icon" />
@@ -29,8 +29,21 @@ export default {
components: {
itemtag: {
name: 'itemtag',
props: ['tag', 'href'],
props: ['tag', 'href', 'version'],
computed: {
isVisible() {
if (_.isNil(this.version)) {
return true
}

return this.version === $dream.version
}
},
render(createElement) {
if (!this.isVisible) {
return null
}

return createElement(
this.tag,
{
@@ -63,6 +76,11 @@ export default {
href: {
type: String,
default: undefined
},

version: {
type: String,
default: undefined
}
},


+ 3
- 0
src/electron/index.js View File

@@ -65,6 +65,9 @@ class DreamApp {
* Prepare the application for use
*/
static async setup() {
// https://electronjs.org/docs/tutorial/notifications#windows
app.setAppUserModelId(process.execPath)

// https://github.com/sindresorhus/electron-util#enforcemacosapplocation-macos
utils.enforceMacOSAppLocation()


+ 1
- 0
src/electron/modules/rollbar.js View File

@@ -26,6 +26,7 @@ const instance = {
payload: {
environment:
process.env.NODE_ENV !== 'development' ? 'production' : 'development',
settings: settings._settings,
person: {
id: settings.user
},

+ 127
- 42
src/electron/modules/settings.js View File

@@ -20,51 +20,22 @@ const settings = {
* Initialize the settings
*/
async init() {
await this._initDefault()

this._path = tools.paths.getRoot('settings.json')
this._settings = {}

await this.ensure()
this.load()
},
await this._ensure()

/**
* Returns the value of the settings in the path
*
* @param {string} path
*/
get(path = '') {
if (path.length === 0) {
return this._settings
}
this.load()

return _.get(this._settings, path)
await this._upgrade()
},

/**
* Set a new value in the settings
*
* @param {any} path
* @param {any} payload
*/
set(path, payload) {
if (_.isPlainObject(path)) {
this._settings = path
this.save()
}

this._settings = _.set(this._settings, path, payload)
this.save()
},

/**
* Make sure the settings file exists
*/
async ensure() {
if (fs.existsSync(this._path)) {
// Exists
return
}

async _initDefault() {
let hasGPU = false

try {
@@ -72,7 +43,8 @@ const settings = {
// eslint-disable-next-line
} catch (err) {}

const defaultSettings = {
this._default = {
version: 2,
welcome: true,
user: uuid(),

@@ -83,18 +55,47 @@ const settings = {
},

preferences: {
boobsSize: '1',
areolaSize: '1',
nippleSize: '1',
vaginaSize: '0.75',
pubicHairSize: '1',
boobs: {
size: '1',
randomize: true,
progressive: true
},
areola: {
size: '1',
randomize: false,
progressive: true
},
nipple: {
size: '1',
randomize: false,
progressive: true
},
vagina: {
size: '0.75',
randomize: true,
progressive: true
},
pubicHair: {
size: '1',
randomize: true,
progressive: true
},

executions: 1,
randomizePreferences: false,
progressivePreferences: false,

useWaifu: false, // weebs out ๐Ÿ˜ก๐Ÿ‘‰๐Ÿšช
useRestoration: true,
useCustomMask: false
},

notifications: {
run: false,
allRuns: true,
update: true
},

folders: {
cropped: tools.paths.get('temp'),
models: tools.paths.get('userData', 'models'),
@@ -106,9 +107,19 @@ const settings = {
enabled: true
}
}
},

/**
* Make sure the settings file exists
*/
async _ensure() {
if (fs.existsSync(this._path)) {
// Exists
return
}

try {
fs.writeFileSync(this._path, JSON.stringify(defaultSettings, null, 2))
fs.writeFileSync(this._path, JSON.stringify(this._default, null, 2))
} catch (err) {
if (is.windows) {
api.dialog.showErrorBox(
@@ -130,6 +141,72 @@ const settings = {
}
},

/**
* Check if it is necessary to update the settings file.
* - Ugly code, here we go!
*/
async _upgrade() {
const version = this._settings.version || 1
const currentVersion = this._default.version

if (currentVersion === version) {
return
}

if (version === 1 && currentVersion === 2) {
const newSettings = _.cloneDeep(this._settings)

newSettings.version = 2
newSettings.preferences = this._default.preferences
newSettings.notifications = this._default.notifications

const {
boobsSize,
areolaSize,
nippleSize,
vaginaSize,
pubicHairSize
} = this._settings.preferences

newSettings.preferences.boobs.size = boobsSize
newSettings.preferences.areola.size = areolaSize
newSettings.preferences.nipple.size = nippleSize
newSettings.preferences.vagina.size = vaginaSize
newSettings.preferences.pubicHair.size = pubicHairSize

this.set(newSettings)
}
},

/**
* Returns the value of the settings in the path
*
* @param {string} path
*/
get(path = '') {
if (path.length === 0) {
return this._settings
}

return _.get(this._settings, path)
},

/**
* Set a new value in the settings
*
* @param {any} path
* @param {any} payload
*/
set(path, payload) {
if (_.isPlainObject(path)) {
this._settings = path
this.save()
}

this._settings = _.set(this._settings, path, payload)
this.save()
},

/**
* Load the settings file. If it is already loaded then it refreshes.
*/
@@ -145,6 +222,14 @@ const settings = {
async save() {
const payload = JSON.stringify(this._settings, null, 2)
fs.writeFileSync(this._path, payload)

if (window && window.$rollbar) {
$rollbar.configure({
payload: {
settings: this._settings
}
})
}
}
}


+ 6
- 1
src/electron/tools/fs.js View File

@@ -169,7 +169,12 @@ module.exports = {
...options
}

const fileName = options.fileName || path.basename(url)
const fileName =
options.fileName ||
path
.basename(url)
.split('?')[0]
.split('#')[0]
let filePath = path.join(options.directory, fileName)

const deleteFile = () => {

+ 7
- 6
src/electron/tools/index.js View File

@@ -83,7 +83,7 @@ module.exports = {
*/
transform(job) {
// Independent preferences for the photo
const preferences = job.getPhoto().getPreferences()
const preferences = job.getPreferences()

// Cropped photo
const inputFilePath = job
@@ -105,19 +105,19 @@ module.exports = {
{
// Preferences
cliArgs.push('--bsize')
cliArgs.push(preferences.boobsSize)
cliArgs.push(preferences.boobs.size)

cliArgs.push('--asize')
cliArgs.push(preferences.areolaSize)
cliArgs.push(preferences.areola.size)

cliArgs.push('--nsize')
cliArgs.push(preferences.nippleSize)
cliArgs.push(preferences.nipple.size)

cliArgs.push('--vsize')
cliArgs.push(preferences.vaginaSize)
cliArgs.push(preferences.vagina.size)

cliArgs.push('--hsize')
cliArgs.push(preferences.pubicHairSize)
cliArgs.push(preferences.pubicHair.size)
}

if ($settings.processing.device === 'CPU') {
@@ -132,6 +132,7 @@ module.exports = {
debug('The transformation process has begun!', {
inputFilePath,
outputFilePath,
preferences,
cliArgs,
job
})

+ 1
- 1
src/electron/tools/shell.js View File

@@ -38,7 +38,7 @@ module.exports = {
*
* @param {...any} args
*/
showSaveDialogSync(...args) {
showSaveDialog(...args) {
return api.dialog.showSaveDialog(...args)
},


+ 2
- 0
src/middleware/checks.js View File

@@ -16,6 +16,8 @@ import { platform } from '~/modules'
* otherwise redirects him to the appropriate page.
*/
export default function({ route, redirect }) {
window.$redirect = redirect

if ($settings.welcome) {
// First time execution!
if (route.path !== '/system/welcome') {

+ 5
- 0
src/modules/file.js View File

@@ -45,6 +45,10 @@ export default class File {
this.reload(path)
}

/**
*
* @param {*} path
*/
reload(path) {
if (_.isNil(path)) {
path = this.getPath()
@@ -118,6 +122,7 @@ export default class File {
}

$tools.fs.unlink(this.getPath())
this.reload()
}

/**

+ 16
- 0
src/modules/helpers.js View File

@@ -0,0 +1,16 @@
/*
* DreamTime | (C) 2019 by Ivan Bravo Bravo <ivan@dreamnet.tech>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License 3.0 as published by
* the Free Software Foundation.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

const rand = (min, max) => {
return Math.random() * (max - min) + min
}

export { rand }

+ 226
- 59
src/modules/models/photo-job.js View File

@@ -1,7 +1,10 @@
import _ from 'lodash'
import moment from 'moment'
import Deferred from 'deferred'

import File from '../file'
import Timer from '../timer'
import { rand } from '../helpers'
import WebError from '~/modules/web-error'

const debug = require('debug').default('app:modules:models:photo-job')
@@ -11,6 +14,7 @@ export default class PhotoJob {
this.id = id
this.photo = photo

// Transformation Process event bus
this.process = undefined

// Output file, this is the photo already transformed!
@@ -19,11 +23,13 @@ export default class PhotoJob {
// Clean initialization
this.reset()

/*
this.debug(`Job created`, {
id: this.id,
photo: this.photo,
file: this.file
})
*/
}

/**
@@ -36,6 +42,9 @@ export default class PhotoJob {
error: ''
}

// Transformation Preferences
this.preferences = _.cloneDeep(this.photo.preferences)

this.isLoading = false
this.hasFailed = false
this.hasFinished = false
@@ -68,6 +77,18 @@ export default class PhotoJob {
this.isLoading = false
this.hasFinished = true
this.timer.stop()

const activeWindow = $tools.utils.activeWindow()

if (!activeWindow.isFocused() && $settings.notifications.run) {
const notification = new Notification(`๐Ÿ’ญ Run #${this.id} has finished`, {
body: 'Now you can save the dream'
})

notification.onclick = () => {
activeWindow.focus()
}
}
}

/**
@@ -93,6 +114,13 @@ export default class PhotoJob {
return this.photo
}

/**
*
*/
getPreferences() {
return this.preferences
}

/**
* Transformed File Name
*/
@@ -129,7 +157,7 @@ export default class PhotoJob {
return {
type: 'debug',
message:
"Found no NVIDIA driver on your system. Please check that you have an NVIDIA GPU and installed a driver from [here](http://www.nvidia.com/Download/index.aspx). If you don't have an NVIDIA GPU please change the Device option in Settings to **CPU**."
"Found no NVIDIA driver on your system. Please check that you have an NVIDIA GPU and installed a driver from [here](http://www.nvidia.com/Download/index.aspx). If you don't have an NVIDIA GPU please change the Device option in **Settings** to **CPU**."
}
}

@@ -137,7 +165,7 @@ export default class PhotoJob {
return {
type: 'debug',
message:
'The NVIDIA driver installed on your system is too old! Please update the drivers [here](http://www.nvidia.com/Download/index.aspx), if the drivers are up to date then your GPU may not be compatible.'
'The NVIDIA driver installed on your system is too old! Please update the drivers [here](http://www.nvidia.com/Download/index.aspx), if the drivers are up to date then your GPU may not be compatible. You can also change the Device option in **Settings** to **CPU**.'
}
}

@@ -145,7 +173,7 @@ export default class PhotoJob {
return {
type: 'debug',
message:
'We are sorry but your GPU is not powerful enough to run this program. Please change the Device option in Settings to **CPU**.'
'Your GPU is not powerful enough to run this program. Please change the Device option in **Settings** to **CPU**.'
}
}

@@ -153,7 +181,7 @@ export default class PhotoJob {
return {
type: 'debug',
message:
'Apparently you have run out of RAM on your system! Try a photo of smaller size or free all possible memory of your system.'
'Apparently you have run out of RAM on your system! Try a photo of smaller size or free all possible memory.'
}
}

@@ -161,7 +189,7 @@ export default class PhotoJob {
return {
type: 'debug',
message:
'Apparently you have run out of RAM on your GPU! Try a photo of smaller size.'
'Apparently you have run out of RAM on your GPU! Try a photo of smaller size or free all possible GPU use.'
}
}

@@ -173,6 +201,30 @@ export default class PhotoJob {
}
}

if (message.includes('invalid device ordinal')) {
return {
type: 'debug',
message:
'A valid GPU device was not found in the indicated GPU option, please try another. You can also change the Device option in **Settings** to **CPU**.'
}
}

if (message.includes('image is not 512 x 512')) {
return {
type: 'debug',
message:
'The photo is not 512x512, please make sure you have uploaded the correct photo or enable and use the cropper.'
}
}

if (message.includes('loading Python')) {
return {
type: 'debug',
message:
'There was a problem loading a necessary DreamPower file. It is possible that your installation is corrupt, download the program again and reinstall.'
}
}

return {
type: 'error',
message: `The process has been interrupted by an unknown error, this may be caused by a corrupt installation, please check the console for more information.`
@@ -193,80 +245,195 @@ export default class PhotoJob {
/**
*
*/
start() {
return new Promise((resolve, reject) => {
const onSpawnError = error => {
reject(
new WebError(
`Unable to start DreamPower!\n
Could not find the executable to DreamPower, in Settings please make sure that the option "DreamPower Folder" is valid, if you are not a developer make sure that the option "Use Python" is disabled.`,
error,
'warning'
)
beforeStart() {
this.customizePreferences()
}

/**
*
*/
customizePreferences() {
if (this.preferences.randomizePreferences) {
// Randomize
if (this.preferences.boobs.randomize) {
this.preferences.boobs.size = rand(0.3, 2.0)
}

if (this.preferences.areola.randomize) {
this.preferences.areola.size = rand(0.3, 2.0)
}

if (this.preferences.nipple.randomize) {
this.preferences.nipple.size = rand(0.3, 2.0)
}

if (this.preferences.vagina.randomize) {
this.preferences.vagina.size = rand(0.3, 1.5)
}

if (this.preferences.pubicHair.randomize) {
this.preferences.pubicHair.size = rand(0, 2.0)
}
} else if (this.preferences.progressivePreferences) {
// Progressive
const add = 0.2 * (this.id - 1)

if (this.preferences.boobs.progressive) {
this.preferences.boobs.size = Number.parseFloat(
this.preferences.boobs.size
)
this.preferences.boobs.size += add
this.preferences.boobs.size = Math.min(this.preferences.boobs.size, 2.0)
}

try {
this.process = $tools.transform(this)
} catch (error) {
onSpawnError(error)
return
if (this.preferences.areola.progressive) {
this.preferences.areola.size = Number.parseFloat(
this.preferences.areola.size
)
this.preferences.areola.size += add
this.preferences.areola.size = Math.min(
this.preferences.areola.size,
2.0
)
}

if (this.preferences.nipple.progressive) {
this.preferences.nipple.size = Number.parseFloat(
this.preferences.nipple.size
)
this.preferences.nipple.size += add
this.preferences.nipple.size = Math.min(
this.preferences.nipple.size,
2.0
)
}

if (this.preferences.vagina.progressive) {
this.preferences.vagina.size = Number.parseFloat(
this.preferences.vagina.size
)
this.preferences.vagina.size += add
this.preferences.vagina.size = Math.min(
this.preferences.vagina.size,
1.5
)
}

this.process.on('error', error => {
// Error before starting
if (this.preferences.pubicHair.progressive) {
this.preferences.pubicHair.size = Number.parseFloat(
this.preferences.pubicHair.size
)
this.preferences.pubicHair.size += add
this.preferences.pubicHair.size = Math.min(
this.preferences.pubicHair.size,
2.0
)
}
}
}

/**
*
*/
start() {
const deferred = Deferred()

const onSpawnError = error => {
deferred.reject(
new WebError(
'Unable to start DreamPower!',
`There was a problem trying to start DreamPower, in **Settings** please make sure that the option **DreamPower Folder** is valid, if you are not a developer make sure that the option **Use Python** is disabled.`,
{
error,
type: 'debug'
}
)
)
}

this.beforeStart()

try {
this.process = $tools.transform(this)
} catch (error) {
setTimeout(() => {
onSpawnError(error)
})
}, 0)
return deferred.promise
}

this.process.on('stdout', output => {
// Output generated by the CLI
output = output
.toString()
.trim()
.split('\n')

output.forEach(text => {
this.cli.lines.unshift({
text,
css: {}
})
})
})
this.process.on('error', error => {
// Error before starting
onSpawnError(error)
})

this.process.on('stdout', output => {
// Output generated by the CLI
output = output
.toString()
.trim()
.split('\n')

this.process.on('stderr', output => {
// CLI error
output.forEach(text => {
this.cli.lines.unshift({
text: output,
css: {
'text-danger': true
}
text,
css: {}
})
})
})

this.cli.error += `${output}\n`
this.process.on('stderr', output => {
// CLI error
this.cli.lines.unshift({
text: output,
css: {
'text-danger': true
}
})

this.process.on('ready', code => {
if (code === 0 || _.isNil(code)) {
// The process has been completed successfully
// Update the output file information.
this.file.update()
this.process = undefined
resolve()
} else {
this.process = undefined
this.cli.error += `${output}\n`
})

this.process.on('ready', code => {
this.process = undefined

const err = this.getCliError()
if (code === 0 || _.isNil(code)) {
// The process has been completed successfully
// Reload the output file information.
this.file.reload()

reject(
if (this.file.exists()) {
$nucleus.track('DREAM_COMPLETED')
deferred.resolve()
} else {
deferred.reject(
new WebError(
`Transformation #${this.id} failed`,
err.message,
Error(this.cli.error),
err.type
'DreamPower has reported that the photo has been transformed but the file does not exist! This may be due to a problem saving the photo, verify that DreamPower has write permissions and that your Antivirus is not detecting false positives. It is also possible that DreamPower is ending abruptly due to a major problem.',
{
type: 'warning',
extra: {
output: this.cli.lines
}
}
)
)
}
})
} else {
const err = this.getCliError()

deferred.reject(
new WebError(`Transformation #${this.id} failed`, err.message, {
error: Error(this.cli.error),
type: err.type,
extra: {
output: this.cli.lines
}
})
)
}
})

return deferred.promise
}
}

+ 17
- 7
src/modules/models/photo.js View File

@@ -56,7 +56,7 @@ export default class Photo {
}
},
{
maxRetries: 3,
maxRetries: 2,
retryDelay: 1000,
maxTimeout:
$settings.processing.device === 'GPU' ? 60 * 1000 : 300 * 1000,
@@ -72,7 +72,6 @@ export default class Photo {
})

this.queue.on('empty', () => {
debug('empty')
// this.onFinish()
})

@@ -105,12 +104,14 @@ export default class Photo {
}
})

/*
this.debug(`Photo created`, {
uuid: this.uuid,
model: this.model,
sourceFile: this.sourceFile,
croppedFile: this.croppedFile
})
*/
}

reset() {
@@ -155,6 +156,18 @@ export default class Photo {

this.isLoading = false
this.timer.stop()

const activeWindow = $tools.utils.activeWindow()

if (!activeWindow.isFocused() && $settings.notifications.allRuns) {
const notification = new Notification(`๐Ÿ’ญ All runs have finished`, {
body: 'Now you can save all the dreams you like'
})

notification.onclick = () => {
activeWindow.focus()
}
}
}

/**
@@ -233,11 +246,8 @@ export default class Photo {
*/
async start() {
if (this.preferences.executions === 0) {
swal(
'Invalid Configuration',
'Please set 1 or more executions',
'warning'
)
swal('Invalid Configuration', 'Please set 1 or more runs', 'warning')

return
}


+ 6
- 1
src/modules/update/dreamtime.js View File

@@ -10,6 +10,7 @@
*/

import _ from 'lodash'
import path from 'path'
import Base from './base'
import dream from '../dream'

@@ -51,6 +52,10 @@ export default class extends Base {
}

async install(filePath) {
$tools.shell.openItem(filePath)
try {
$tools.shell.openItem(filePath)
} catch (err) {
$tools.shell.openItem(path.dirname(filePath))
}
}
}

+ 31
- 0
src/modules/updater.js View File

@@ -12,6 +12,7 @@
// eslint-disable-next-line
import _ from 'lodash'

import platform from './platform'
import DreamTime from './update/dreamtime'
import Checkpoints from './update/checkpoints'

@@ -34,6 +35,36 @@ export default {
await this.dreamtime.fetch()
await this.checkpoints.fetch()

if ($settings.notifications.update) {
if (this.dreamtime.available) {
const dreamtimeNotification = new Notification(
`๐ŸŽ‰ DreamTime ${this.dreamtime.latest.tag_name} available!`,
{
body: 'A new version of DreamTime is available for download.'
}
)

dreamtimeNotification.onclick = () => {
window.$redirect('/system/about')
$tools.utils.activeWindow().focus()
}
}

if (platform.requirements.checkpoints && this.checkpoints.available) {
const checkpointsNotification = new Notification(
`โœจ Checkpoints ${this.checkpoints.latest.tag_name} available!`,
{
body: 'A new version of the Checkpoints is available for download.'
}
)

checkpointsNotification.onclick = () => {
window.$redirect('/system/about')
$tools.utils.activeWindow().focus()
}
}
}

debug('Updater initialized!', {
dreamtime: this.dreamtime,
checkpoints: this.checkpoints

+ 19
- 8
src/modules/web-error.js View File

@@ -14,17 +14,24 @@ import { markdown } from 'markdown'
import swal from 'sweetalert'

class WebError extends Error {
constructor(title, message, error, level = 'error') {
constructor(title, message, opts = {}) {
super(message)

opts = {
error: undefined,
level: 'error',
extra: {},
...opts
}

this.title = title
this.error = error
this.level = level
this.opts = opts
}

report() {
let { message } = this
const { error, level } = this
const { title } = this
const { error, level, extra } = this.opts

console.log('Reporting error...', {
message,
@@ -34,20 +41,24 @@ class WebError extends Error {
})

if ($rollbar.isEnabled) {
const response = $rollbar[level](error || Error(this.message))
const response = $rollbar[level](error || Error(this.message), {
title,
message,
...extra
})

if (response.uuid) {
message += `
\nFor more information please report the following URL on Github or to the developers:
\nFor more information please report the following URL on [Github](https://github.com/private-dreamnet/dreamtime/issues) or [here](https://git.dreamnet.tech/dreamnet/dreamtime/issues):
[https://rollbar.com/occurrence/uuid/?uuid=${response.uuid}](https://rollbar.com/occurrence/uuid/?uuid=${response.uuid})`
} else {
message += `
\nFor more information please take a screenshot and report the following on Github or to the developers:\n
\nFor more information please take a screenshot and report the following on [Github](https://github.com/private-dreamnet/dreamtime/issues) or [here](https://git.dreamnet.tech/dreamnet/dreamtime/issues):\n
${error}`
}
} else if (error) {
message += `
\nFor more information please take a screenshot and report the following on Github or to the developers:\n
\nFor more information please take a screenshot and report the following on [Github](https://github.com/private-dreamnet/dreamtime/issues) or [here](https://git.dreamnet.tech/dreamnet/dreamtime/issues):\n
${error}`
}


+ 2
- 2
src/package.json View File

@@ -1,9 +1,9 @@
{
"name": "dreamtime",
"version": "1.0.1",
"main": "electron/index.js",
"author": "DreamNet <dreamtime@dreamnet.tech>",
"homepage": "https://time.dreamnet.tech",
"version": "1.1.0",
"main": "electron/index.js",
"license": "GPL-3.0-only",
"private": true,
"repository": {

+ 10
- 2
src/pages/index.vue View File

@@ -2,15 +2,17 @@
<div class="home">
<app-title>
<h1 class="title">
๐Ÿ“ท Welcome to private entertainment
๐Ÿ“ท Nudify: It's {{ $dream.name }}
</h1>

<h3 class="subtitle">
#freedom #opensource #rule34
Select a photo and have a good dream
</h3>
</app-title>

<div class="content-body">
<div v-if="alert" class="notification is-warning text-lg" v-html="alert" />

<!-- Quick Upload -->
<nudity-upload />
</div>
@@ -23,6 +25,12 @@ import path from 'path'
export default {
data: () => ({}),

computed: {
alert() {
return $nucleus.isEnabled ? $nucleus.alerts.index : undefined
}
},

created() {}
}
</script>

+ 26
- 14
src/pages/nudity/crop.vue View File

@@ -17,7 +17,7 @@
<p class="box-title">๐Ÿ“ท Photo cropping</p>

<p class="help-text">
For now the algorithm can only work with photos of the size of 512x512.
To be able to generate the dream correctly it is necessary to resize your photo to the size of 512x512
</p>

<p class="help-text">
@@ -29,6 +29,12 @@
</p>
</section>

<section class="box">
<label for="is-cropped">
<input id="is-cropped" v-model="isCropped" type="checkbox" /> My photo is already the size of 512x512, do not crop. <span v-tooltip="'Select this option to ignore the cropping tool on the right and pass the original photo directly to the transformation algorithm.'" class="underline text-sm">?</span>
</label>
</section>

<section class="box">
<p class="box-title">๐Ÿ•ต๏ธโ€๏ธ How to obtain better results?</p>
<p class="help-text">
@@ -69,6 +75,8 @@ export default {
data: () => ({
// Instance of CropperJS
cropper: undefined,

isCropped: false,
isPreferencesVisible: false
}),

@@ -126,19 +134,23 @@ export default {
console.log(data)
*/

const canvas = this.cropper.getCroppedCanvas({
width: 512,
height: 512,
minWidth: 512,
minHeight: 512,
maxWidth: 512,
maxHeight: 512,
fillColor: 'white',
imageSmoothingEnabled: true,
imageSmoothingQuality: 'high'
})

await $tools.crop(this.photo, canvas)
if (!this.isCropped) {
const canvas = this.cropper.getCroppedCanvas({
width: 512,
height: 512,
minWidth: 512,
minHeight: 512,
maxWidth: 512,
maxHeight: 512,
fillColor: 'white',
imageSmoothingEnabled: true,
imageSmoothingQuality: 'high'
})

await $tools.crop(this.photo, canvas)
} else {
this.photo.croppedFile = this.photo.sourceFile
}
},

/**

+ 58
- 50
src/pages/nudity/results.vue View File

@@ -12,42 +12,42 @@

<div class="content-body">
<!-- Stats -->
<div class="nudify-results-stats">
<div class="stats-item">
<app-photo :src="sourceDataURL">Original</app-photo>
</div>
<div class="__summary">
<div class="__content">
<div class="__status box">
<p v-if="!photo.isLoading">๐Ÿ“ธ Dream completed<br>Has it been a good dream?</p>
<p v-else>๐Ÿ’ญ Dreaming... this will take a moment</p>
</div>

<div class="stats-item">
<app-photo :src="croppedDataURL">Cropped</app-photo>
</div>
<div class="__photos">
<app-photo :src="sourceDataURL">Original</app-photo>
<app-photo :src="croppedDataURL">Cropped</app-photo>
</div>

<div v-if="!photo.isLoading" class="box stats-item stats-actions">
<p>Transformation completed</p>
<div v-show="!photo.isLoading" class="__actions">
<div class="buttons">
<button type="button" class="button is-success" @click.prevent="togglePreferences">
<span v-if="!isPreferencesVisible">Change Preferences</span>
<span v-else>Results</span>
</button>

<div class="buttons">
<button type="button" class="button is-success" @click.prevent="togglePreferences">
<span v-if="!isPreferencesVisible">Change Preferences</span>
<span v-else>Results</span>
</button>
<button type="button" class="button is-danger" @click.prevent="rerun">Rerun all</button>
</div>
<button type="button" class="button is-danger" @click.prevent="rerun">Rerun all</button>
</div>

<div class="buttons">
<button type="button" class="button is-sm" @click.prevent="openFolder">Open Folder</button>
<nuxt-link to="/" class="button is-sm">Another photo</nuxt-link>
<div class="buttons">
<button type="button" class="button" @click.prevent="openFolder">Open Folder</button>
<nuxt-link to="/" class="button">Another photo</nuxt-link>
</div>
</div>
</div>

<div v-else class="box stats-item stats-actions">
<p>Relax, this will take a moment...</p>

<div class="buttons">
<div v-show="photo.isLoading" class="__actions justify-center items-center">
<button type="button" class="button is-danger" @click.prevent="cancel">Cancel</button>
</div>
</div>
</div>

<div v-show="!isPreferencesVisible" class="box">
<!-- Jobs -->
<div v-show="!isPreferencesVisible" class="__jobs">
<nudity-job v-for="(job, index) in photo.jobs" :key="index" :job="job" />
</div>

@@ -139,44 +139,52 @@ export default {

<style lang="scss">
.nudify-results {
.nudify-results-stats {
@apply flex mb-5;
.content-body {
@apply flex;
}

.stats-item {
@apply mb-0;
.__summary {
@apply flex-1 mr-5;

&:not(:last-child) {
@apply mr-5;
}
.__content {
@apply sticky top-0;
}

.stats-actions {
@apply flex-1 flex flex-col justify-center items-center;
.__status {
@apply flex justify-center items-center
text-xl;
}

.buttons {
@apply mt-3;
.__photos {
@apply flex justify-center
mb-5;

.app-photo {
&:not(:last-child) {
@apply mr-5;
}
}
}

.box {
&:not(:last-child) {
@apply mr-3;
}
.__actions {
@apply flex;

/*
&.is-photo {
@apply flex flex-col justify-center items-center;
.buttons {
@apply flex-1 flex-col justify-center;
}
*/
}
}

figure {
@apply flex justify-center;
box-sizing: content-box;
.__jobs {
@apply flex-1 flex flex-col;

img {
}
}
.c-nudity-job {
@apply mr-5 mb-5;
}
}

.c-settings-preferences {
@apply flex-1;
}
}
</style>

+ 14
- 4
src/pages/system/about.vue View File

@@ -11,6 +11,8 @@
</app-title>

<div class="about-body">
<div v-if="alert" class="notification is-warning text-lg" v-html="alert" />

<!-- Limited! -->
<section v-if="$platform.isLimited" class="box box-section">
<box-section-item
@@ -80,7 +82,7 @@
:description="item.description"
:icon="item.icon"
:href="item.href"
class="about-item" />
:version="item.version" />
</section>

<!-- DreamPower -->
@@ -105,7 +107,8 @@
:label="item.label"
:description="item.description"
:icon="item.icon"
:href="item.href" />
:href="item.href"
:version="item.version" />
</section>

<!-- DreamNet -->
@@ -126,7 +129,8 @@
:label="item.label"
:description="item.description"
:icon="item.icon"
:href="item.href" />
:href="item.href"
:version="item.version" />
</section>

<!-- Contributors -->
@@ -209,10 +213,16 @@ export default {

developers() {
return $nucleus.isEnabled ? $nucleus.about.developers : []
},

alert() {
return $nucleus.isEnabled ? $nucleus.alerts.about : undefined
}
},

created() {},
created() {
$nucleus.track('PAGE_ABOUT')
},

methods: {
openGUI() {

+ 1
- 0
src/pages/system/settings.vue View File

@@ -14,6 +14,7 @@
<div class="buttons is-group is-center">
<nuxt-link to="/system/settings/processing" class="button is-outlined is-sm">Processing</nuxt-link>
<nuxt-link to="/system/settings/preferences" class="button is-outlined is-sm">Preferences</nuxt-link>
<nuxt-link to="/system/settings/notifications" class="button is-outlined is-sm">Notifications</nuxt-link>
<nuxt-link to="/system/settings/folders" class="button is-outlined is-sm">Folders</nuxt-link>
<!--<nuxt-link to="/settings/theme" class="button is-outlined is-sm">Theme</nuxt-link>-->
<nuxt-link to="/system/settings/telemetry" class="button is-outlined is-sm">Telemetry</nuxt-link>

+ 3
- 1
src/pages/system/settings/folders.vue View File

@@ -44,7 +44,9 @@ export default {

data: () => ({}),

created() {},
created() {
$nucleus.track('PAGE_SETTINGS_FOLDERS')
},

methods: {
showOpenDialog(path) {

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

@@ -0,0 +1,45 @@
<template>
<div class="settings-fields">
<section class="box box-section">
<box-section-item
label="On Run"
description="Show a notification when the program is not in the foreground and a single run have finished.">
<select v-model="currentValue.notifications.run" class="input">
<option :value="true">Enabled</option>
<option :value="false">Disabled</option>
</select>
</box-section-item>

<box-section-item
label="On Dream"
description="Show a notification when the program is not in the foreground and all the runs have finished.">
<select v-model="currentValue.notifications.allRuns" class="input">
<option :value="true">Enabled</option>
<option :value="false">Disabled</option>
</select>
</box-section-item>

<box-section-item
label="On Update"
:description="`Show a notification when there is a new version of ${$dream.name} or the Checkpoints.`">
<select v-model="currentValue.notifications.update" class="input">