Browse Source

Mask editor and fixes

tags/v1.4.4
Ivan Bravo Bravo 10 months ago
parent
commit
0cdd3ddd49
43 changed files with 1749 additions and 444 deletions
  1. 15
    0
      src/assets/css/base/_reset.scss
  2. 14
    1
      src/assets/css/components/_button.scss
  3. 2
    1
      src/assets/css/components/_notification.scss
  4. 0
    0
      src/assets/images/background.png
  5. 5
    5
      src/components/Layout/Jobbar.vue
  6. 0
    20
      src/components/Layout/Navigation.vue
  7. 202
    173
      src/components/Nudity/PhotoRun.vue
  8. 8
    18
      src/components/Nudity/Upload.vue
  9. 28
    12
      src/components/Settings/SettingsPreferences.vue
  10. 8
    14
      src/components/UI/index.js
  11. 17
    23
      src/electron/src/index.js
  12. 13
    7
      src/electron/src/modules/services/settings.js
  13. 7
    0
      src/electron/src/modules/tools/paths.js
  14. 14
    6
      src/electron/src/modules/tools/power.js
  15. 2
    2
      src/layouts/default.vue
  16. 8
    10
      src/middleware/checks.js
  17. 87
    0
      src/modules/editor.theme.js
  18. 44
    19
      src/modules/nudify/nudify.js
  19. 21
    9
      src/modules/nudify/photo-run.js
  20. 119
    41
      src/modules/nudify/photo.js
  21. 12
    0
      src/nuxt.config.js
  22. 5
    2
      src/package.json
  23. 0
    4
      src/pages/about.vue
  24. 0
    4
      src/pages/dreamnet.vue
  25. 1
    5
      src/pages/index.vue
  26. 10
    5
      src/pages/nudify/_id.vue
  27. 17
    3
      src/pages/nudify/_id/crop.vue
  28. 58
    0
      src/pages/nudify/_id/editor.vue
  29. 18
    18
      src/pages/nudify/_id/overlay.vue
  30. 8
    13
      src/pages/nudify/_id/results.vue
  31. 4
    4
      src/pages/settings/app.vue
  32. 1
    6
      src/pages/settings/folders.vue
  33. 0
    4
      src/pages/settings/notifications.vue
  34. 1
    5
      src/pages/settings/preferences.vue
  35. 0
    6
      src/pages/settings/telemetry.vue
  36. 37
    0
      src/patches/tui-image-editor+3.7.1.patch
  37. 26
    0
      src/scripts/release.js
  38. 235
    0
      src/static/assets/images/svg/icon-a.svg
  39. 224
    0
      src/static/assets/images/svg/icon-b.svg
  40. 224
    0
      src/static/assets/images/svg/icon-c.svg
  41. 224
    0
      src/static/assets/images/svg/icon-d.svg
  42. 29
    3
      src/tailwind.config.js
  43. 1
    1
      src/workers/fs/worker.js

+ 15
- 0
src/assets/css/base/_reset.scss View File

@@ -29,6 +29,21 @@ html,
@apply h-full;
}

dialog {
@apply m-auto p-0 bg-transparent text-white;
width: 32em;
max-width: 100%;

.dialog__content {
@apply p-4 bg-dark-500 rounded;
@apply flex flex-col justify-center;
}
}

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

.swal-content {
font-size: 16px;


+ 14
- 1
src/assets/css/components/_button.scss View File

@@ -1,8 +1,9 @@
.button {
@apply inline-flex items-center justify-center;
@apply border border-primary-500-30;
@apply px-4 rounded-sm outline-none;
@apply px-4 rounded-sm;
@apply text-primary-400 font-semibold uppercase;
@apply outline-none #{!important};
height: 40px;
transition: all 0.2s ease-in-out;

@@ -58,6 +59,18 @@
}
}

&.button--info {
@apply text-blue-400 border-blue-500-30;

&:hover {
@apply bg-blue-500-20;
}

&:active {
@apply bg-blue-500-30;
}
}

.icon {
@apply mr-2;
}

+ 2
- 1
src/assets/css/components/_notification.scss View File

@@ -1,5 +1,6 @@
.notification {
@apply mb-4 p-2 bg-transparent border-2 border-dark-500 text-sm text-generic-100 rounded-sm;
@apply mb-4 p-2 border-2 border-dark-500 rounded-sm;
@apply bg-dark-300 text-sm text-generic-100;

a {
@apply underline;

src/static/assets/images/background.png → src/assets/images/background.png View File


+ 5
- 5
src/components/Layout/Jobbar.vue View File

@@ -157,13 +157,13 @@ export default {
}

.jobs__list {
@apply flex-1 flex flex-wrap justify-between;
@apply flex-1;
@apply px-4 py-2 overflow-y-auto max-h-full;

.job {
@apply mb-2 cursor-pointer;
width: calc(1/3*100% - (1 - 1/3)*1rem);
height: 42px;
@apply inline-block mb-2 mr-2 cursor-pointer;
width: 48px;
height: 48px;
transition: all .1s ease-in-out;

&.job--running {
@@ -184,7 +184,7 @@ export default {
}

img {
@apply border-2 border-transparent;
@apply border-2 border-transparent rounded-full;
@apply w-full h-full;
}
}

+ 0
- 20
src/components/Layout/Navigation.vue View File

@@ -21,26 +21,6 @@
</nav>
</section>

<!-- Nice links -->
<section v-if="$provider.tools.system.online" class="menu-section">
<nav class="menu-items">
<app-external-link :href="$provider.services.nucleus.urls.web" class="menu-item">
<span class="icon">๐ŸŒŽ</span>
<span>Website</span>
</app-external-link>

<app-external-link :href="$provider.services.nucleus.urls.chat" class="menu-item">
<span class="icon">๐Ÿ’ฌ</span>
<span>Chat</span>
</app-external-link>

<app-external-link :href="$provider.services.nucleus.urls.forum" class="menu-item">
<span class="icon">๐Ÿ‘ฅ</span>
<span>Forum</span>
</app-external-link>
</nav>
</section>

<!-- Developer Navigation -->
<section v-if="isDev" class="menu-section">
<nav class="menu-items" />

+ 202
- 173
src/components/Nudity/PhotoRun.vue View File

@@ -1,100 +1,118 @@
<template>
<div class="c-photo-run">
<figure class="run__preview">
<app-photo v-if="previewDataURL" :src="previewDataURL" />
</figure>

<div class="box run__content">
<div class="box__content">
<details v-show="run.running || run.failed" class="run__section" open>
<summary class="section__title">
Status
</summary>

<div class="section__content">
<p v-show="run.running" class="text-white font-bold text-xl">
<font-awesome-icon icon="running" /> Running ({{ run.timer.duration }}s)
</p>

<p v-show="run.failed" class="text-danger-500 font-bold text-xl">
<font-awesome-icon icon="exclamation-circle" /> Error!
</p>
</div>
</details>

<details class="run__section" open>
<summary class="section__title">
Actions
</summary>

<div class="section__content">
<button v-show="run.finished" class="button button--success" @click.prevent="save">
Save
</button>

<button v-show="run.finished" class="button button--danger" @click.prevent="rerun">
Rerun
</button>

<button v-show="run.running" class="button button--danger" @click.prevent="cancel">
Cancel
</button>
</div>
</details>

<details class="run__section">
<summary class="section__title">
Preferences
</summary>

<div class="section__preferences">
<p>
<span class="preference__name">Boobs size</span>
<span class="preference__value">{{ run.preferences.body.boobs.size | size }}</span>
</p>

<p>
<span class="preference__name">Areola size</span>
<span class="preference__value">{{ run.preferences.body.areola.size | size }}</span>
</p>

<p>
<span class="preference__name">Nipple size</span>
<span class="preference__value">{{ run.preferences.body.nipple.size | size }}</span>
</p>

<p>
<span class="preference__name">Vagina size</span>
<span class="preference__value">{{ run.preferences.body.vagina.size | size }}</span>
</p>

<p>
<span class="preference__name">Pubic Hair size</span>
<span class="preference__value">{{ run.preferences.body.pubicHair.size | size }}</span>
</p>
</div>
</details>

<details class="run__section">
<summary class="section__title">
Console
</summary>

<div class="section__console">
<li v-for="(item, index) in run.cli.lines" :key="index" :class="item.css">
> {{ item.text }}
</li>
</div>
</details>
<div class="c-photo-run" :style="previewStyle">
<div class="run__content">
<div v-show="run.finished" class="content__item">
<p class="text-white">
<span><font-awesome-icon icon="heart" /></span>
<span>{{ run.timer.duration }}s</span>
</p>
</div>

<div v-show="run.running" class="content__item">
<p class="text-white">
<span><font-awesome-icon icon="running" /></span>
<span>{{ run.timer.duration }}s</span>
</p>
</div>

<div v-show="run.failed" class="content__item">
<p class="text-danger-500">
<span><font-awesome-icon icon="exclamation-circle" /></span>
<span>Error!</span>
</p>
</div>

<div v-show="run.finished" class="content__item">
<button v-tooltip="'Save photo'" class="button button--success button--sm" @click.prevent="save">
<font-awesome-icon icon="download" />
</button>
</div>

<div v-show="run.finished" class="content__item">
<button v-tooltip="'Rerun'" class="button button--danger button--sm" @click.prevent="rerun">
<font-awesome-icon icon="undo" />
</button>
</div>

<div v-show="run.running" class="content__item">
<button v-tooltip="'Stop'" class="button button--danger button--sm" @click.prevent="cancel">
<font-awesome-icon icon="stop" />
</button>
</div>

<div v-show="hasMaskfin" class="content__item">
<button v-tooltip="'View maskfin'" class="button button--info button--sm" @click.prevent="$refs.maskfinDialog.showModal()">
<font-awesome-icon icon="mask" />
</button>
</div>

<div v-if="false" class="content__item">
<button v-tooltip="'View preferences'" class="button button--info button--sm">
<font-awesome-icon icon="sliders-h" />
</button>
</div>

<div class="content__item">
<button v-tooltip="'View terminal'" class="button button--sm" @click.prevent="$refs.terminalDialog.showModal()">
<font-awesome-icon icon="terminal" />
</button>
</div>
</div>

<!-- Maskfin Dialog -->
<dialog ref="maskfinDialog">
<div class="dialog__content dialog__maskfin">
<div class="maskfin__preview">
<img :src="run.maskfinFile.dataURL">
</div>

<div class="maskfin__description">
<p>This is the Maskfin, a mask that represents in layers the areas that the algorithm will replace with the fake nude.</p>
<p>Click on the "Add to queue" button to add it as an additional photo, edit the layers and continue with the nudification. You can also save it to your computer, edit it with an external program and continue the nudification manually.</p>
<p>For more information please consult the <a :href="manualURL" target="_blank">manual</a>.</p>
</div>

<div class="dialog__buttons">
<button class="button" @click.prevent="addMaskToQueue">
Add to queue
</button>

<button class="button button--success" @click.prevent="saveMask">
Save
</button>

<button class="button button--danger" @click.prevent="$refs.maskfinDialog.close()">
Close
</button>
</div>
</div>
</dialog>

<!-- Terminal Dialog -->
<dialog ref="terminalDialog">
<div class="dialog__content">
<div class="terminal">
<li v-for="(item, index) in run.cli.lines" :key="index" :class="item.css">
> {{ item.text }}
</li>
</div>

<div class="dialog__buttons">
<button class="button button--danger" @click.prevent="$refs.terminalDialog.close()">
Close
</button>
</div>
</div>
</dialog>
</div>
</template>

<script>
import { isNil } from 'lodash'
import { Nudify } from '~/modules/nudify'

const { showSaveDialogSync } = $provider.api.dialog
const { nucleus } = $provider.services

export default {
filters: {
@@ -118,6 +136,22 @@ export default {
previewDataURL() {
return this.run.outputFile.dataURL
},

previewStyle() {
if (!this.previewDataURL) {
return {}
}

return { backgroundImage: `url(${this.previewDataURL})` }
},

hasMaskfin() {
return this.run.maskfinFile.exists
},

manualURL() {
return nucleus.urls?.docs?.manual || 'https://forum.dreamnet.tech'
},
},

watch: {
@@ -134,6 +168,8 @@ export default {
defaultPath: this.run.outputName,
filters: [
{ name: 'PNG', extensions: ['png'] },
{ name: 'JPG', extensions: ['jpg'] },
{ name: 'GIF', extensions: ['gif'] },
],
})

@@ -151,115 +187,108 @@ export default {
cancel() {
this.run.photo.cancelRun(this.run)
},
},
}
</script>

<style lang="scss">
.c-photo-run {
@apply flex flex-col;


/*
.__preview {
@apply flex justify-center items-center
rounded rounded-tr-none rounded-br-none
border-2 border-dark-500 border-r-0
text-3xl;
width: 125px;
height: 125px;
addMaskToQueue() {
Nudify.add(this.run.maskfinFile, { isMaskfin: true })
},

img {
@apply w-full h-full rounded rounded-tr-none rounded-br-none;
transition: all 0.15s ease-in-out;
saveMask() {
const savePath = showSaveDialogSync({
defaultPath: `maskfin-${this.run.outputName}`,
filters: [
{ name: 'PNG', extensions: ['png'] },
{ name: 'JPG', extensions: ['jpg'] },
{ name: 'GIF', extensions: ['gif'] },
],
})

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

.__content {
@apply flex-1 flex flex-col
bg-dark-500
rounded
rounded-tl-none
rounded-bl-none
px-4
shadow;
this.run.maskfinFile.copy(savePath)
},

width: 200px;
viewPreferences() {

.__section {
@apply py-2;
},

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

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

.__status {
@apply flex justify-center
text-xl font-bold;
}
},
},
}
</script>

.__buttons {
@apply flex justify-center items-center;
}
<style lang="scss">
.c-photo-run {
@apply bg-cover bg-center border border-dark-500;
@apply relative;
background-image: url('~@/assets/images/background.png');
height: 512px;

&:hover {
.run__content {
@apply opacity-100;
}
}
}

.__console {
@apply p-2 bg-black overflow-auto rounded;
height: 150px;
.run__content {
@apply opacity-0 bg-dark-500-90 w-full;
@apply absolute bottom-0;
@apply flex;
transition: all .1s linear;
height: 100px;

li {
@apply font-mono text-xs text-generic-100 mb-2 block;
.content__item {
@apply flex-1 flex justify-center items-center;

&.text-danger {
@apply text-danger;
}
}
}
&:not(:first-child) {
@apply mr-2;
}

.__preferences {
p {
@apply text-sm;
.button {
@apply w-full;
}

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

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

.run__preview {
@apply flex justify-center;
}
.dialog__maskfin {
a {
@apply text-primary-500 underline;
}

.run__content {
.run__section {
&:not(:last-child) {
@apply mb-4;
}
.maskfin__preview {
@apply mb-4;
}

.section__title {
@apply text-generic-300 font-semibold cursor-pointer outline-none;
.maskfin__description {
@apply text-sm mb-4;

p {
@apply mb-2;
}
}
}

.section__content {
@apply px-4 pt-2 text-center;
.dialog__buttons {
@apply flex;

.button {
@apply flex-1;

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

@@ -278,9 +307,9 @@ export default {
}
}

.section__console {
@apply p-2 bg-black overflow-auto rounded;
height: 150px;
.terminal {
@apply p-2 mb-2 bg-black overflow-auto rounded;
height: 400px;

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

+ 8
- 18
src/components/Nudity/Upload.vue View File

@@ -3,17 +3,17 @@
<div class="uploader__settings box box--items">
<div class="box__content">
<box-item
label="Upload Mode"
description="Select what should be done when uploading a new photo.">
label="Upload mode"
description="What will happen when uploading a photo.">
<select v-model="$settings.app.uploadMode" class="input">
<option value="none">
Stay
</option>
<option value="add-queue">
Start photo transformation
Start transformation
</option>
<option value="go-preferences">
Change photo preferences
Change preferences
</option>
</select>
</box-item>
@@ -40,10 +40,10 @@
<div class="box__header">
<h2 class="title">
<span class="icon"><font-awesome-icon icon="image" /></span>
<span>File.</span>
<span>Photo.</span>
</h2>
<h3 class="subtitle">
Select a file from your computer.
Select one or more photos from your computer.
</h3>
</div>

@@ -70,7 +70,7 @@
<span>Folder.</span>
</h2>
<h3 class="subtitle">
Select a folder from your computer. All valid photos will be transformed.
Select one or more folders on your computer. All valid photos inside will be uploaded.
</h3>
</div>

@@ -174,17 +174,7 @@ export default {
return
}

Swal.fire({
title: 'Importing files...',
text: 'One moment, please.',
showConfirmButton: false,
allowOutsideClick: false,
allowEscapeKey: false,
})

await Nudify.addFiles(files)

Swal.close()
},

/**
@@ -294,7 +284,7 @@ export default {
} else if (files.length > 0) {
const paths = map(files, 'path')
this.addFiles(paths)
nucleus.track('UPLOAD_DROP')
nucleus.track('UPLOAD_DROP_FILE')
}
},
},

+ 28
- 12
src/components/Settings/SettingsPreferences.vue View File

@@ -1,6 +1,6 @@
<template>
<div class="c-preferences">
<section class="box box--items">
<section v-show="currentValue.advanced.transformMode !== 'import-maskfin'" class="box box--items">
<div class="box__header">
<h2 class="title">
Runs
@@ -63,7 +63,7 @@
</div>
</section>

<section class="box box--items">
<section v-show="currentValue.advanced.transformMode !== 'import-maskfin'" class="box box--items">
<div class="box__header">
<h2 class="title">
Body
@@ -312,20 +312,14 @@

<div class="box__content">
<box-item
label="Scale mode"
description="Method that will be used to scale your photo to 512x512.">
label="Scale method"
description="Method to scale the photo to 512x512">
<select v-model="currentValue.advanced.scaleMode" class="input">
<option value="none">
None
</option>
<option value="cropjs">
Manual Crop (Not recommended)
</option>
<option value="overlay">
Overlay
</option>
<option value="auto-rescale">
Fixed scale
Fixed
</option>
<option value="auto-resize">
Scale and pad
@@ -333,12 +327,34 @@
<option value="auto-resize-crop">
Scale and crop
</option>
<option value="overlay">
Overlay
</option>
<option value="cropjs">
Manual crop (Not recommended)
</option>
</select>
</box-item>

<box-item
label="Transform method"
description="Transformation method, only recommended for advanced users.">
<select v-model="currentValue.advanced.transformMode" class="input">
<option value="normal">
Nudify
</option>
<option value="export-maskfin">
Nudify & Maskfin
</option>
<option value="import-maskfin">
Nudify with Maskfin
</option>
</select>
</box-item>

<box-item
label="Color transfer"
description="(Experimental) At the end of the transformation, a color transfer algorithm will be applied to try to recover the original colors of the photo.">
description="Use a experimental color transfer algorithm to try to recover the original colors of the photo.">
<select v-model="currentValue.advanced.useColorTransfer" class="input">
<option :value="true">
Enabled

+ 8
- 14
src/components/UI/index.js View File

@@ -1,27 +1,21 @@
/*
* 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/>.
*/
// DreamTime.
// Copyright (C) DreamNet. All rights reserved.
//
// 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. See <https://www.gnu.org/licenses/gpl-3.0.html>
//
// Written by Ivan Bravo Bravo <ivan@dreamnet.tech>, 2019.

import Vue from 'vue'
import Title from './AppTitle'
import ExternalLink from './AppExternalLink'
import Update from './AppUpdate'
import SectionItem from './BoxSectionItem'
import BoxItem from './BoxItem'
import AppPhoto from './AppPhoto'
import AppNews from './AppNews'

Vue.component('app-title', Title)
Vue.component('app-external-link', ExternalLink)
Vue.component('app-update', Update)
Vue.component('app-photo', AppPhoto)
Vue.component('app-news', AppNews)
Vue.component('box-section-item', SectionItem)
Vue.component('box-item', BoxItem)

+ 17
- 23
src/electron/src/index.js View File

@@ -28,11 +28,17 @@ const logger = Logger.create('electron')
config.rootDir = dirname(dirname(__dirname))

if (process.env.NODE_ENV === 'production') {
process.chdir(getPath('exe', '../'))
process.chdir(getPath('exe', '..'))
}

class DreamApp {
static async initialStart() {
/**
* @type {BrowserWindow}
*/
window


static async boot() {
// logger setup
Logger.setLogLevel(process.env.LOG || 'info')
Logger.setLogfile(getPath('userData', 'dreamtime.log'))
@@ -61,18 +67,17 @@ class DreamApp {
return true
})

await this.initialSetup()
}
// https://electronjs.org/docs/tutorial/notifications#windows
app.setAppUserModelId(process.execPath)

// https://pracucci.com/electron-slow-background-performances.html
app.commandLine.appendSwitch('disable-renderer-backgrounding')

/**
*
*/
static async initialSetup() {
// user settings.
await settings.initialSetup()

if (settings.app.disableHardwareAcceleration) {
logger.info('Disabling hardware acceleration.')
if (settings.app ?.disableHardwareAcceleration) {
logger.info('Hardware acceleration disabled.')
app.disableHardwareAcceleration()
}
}
@@ -92,19 +97,9 @@ class DreamApp {
* Prepare the application.
*/
static async setup() {
// https://electronjs.org/docs/tutorial/notifications#windows
app.setAppUserModelId(process.execPath)

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

// macos activate.
/*
app.on('activate', () => {
this.createWindow()
})
*/

// application exit.
app.on('will-quit', async (event) => {
logger.debug('Exiting...')
@@ -142,6 +137,7 @@ class DreamApp {
if (startsWith(url, 'http') || startsWith(url, 'mailto')) {
event.preventDefault()
shell.openExternal(url)
nucleus.track('EXTERNAL_LINK', { href: url })
return
}

@@ -157,8 +153,6 @@ class DreamApp {
showSaveImageAs: true,
})

logger.info('Starting services...')

// system stats.
await system.setup()

@@ -287,4 +281,4 @@ app.on('ready', async () => {
}
})

DreamApp.initialStart()
DreamApp.boot()

+ 13
- 7
src/electron/src/modules/services/settings.js View File

@@ -53,12 +53,12 @@ class SettingsService extends BaseService {
const cores = round(system.cores / 2) || 4

this.payload = {
version: 4,
version: 5,
welcome: true,
user: uuid(),

app: {
disableHardwareAcceleration: false,
disableHardwareAcceleration: true,
uploadMode: 'add-queue',
},

@@ -66,7 +66,6 @@ class SettingsService extends BaseService {
device: hasGPU ? 'GPU' : 'CPU',
gpus: [0],
cores,
disablePersistentGan: false,
usePython: process.env.NODE_ENV === 'development',
},

@@ -109,6 +108,7 @@ class SettingsService extends BaseService {

advanced: {
scaleMode: 'auto-rescale',
transformMode: 'normal',
useColorTransfer: false,
useWaifu: false,
},
@@ -123,7 +123,7 @@ class SettingsService extends BaseService {
folders: {
cropped: paths.getPath('temp'),
models: paths.getPath('userData', 'models'),
masks: paths.getPath('userData', 'masks'),
masks: paths.getPath('temp'),
cli: paths.getPath('userData', 'dreampower'),
},

@@ -167,7 +167,7 @@ class SettingsService extends BaseService {
const currentSettings = this.payload
const newSettings = cloneDeep(currentSettings)

// Upgrade 1 -> 2
// 1 -> 2
if (currentVersion === 1 && newVersion === 2) {
newSettings.version = 2
newSettings.preferences = this._default.preferences
@@ -188,7 +188,7 @@ class SettingsService extends BaseService {
newSettings.preferences.pubicHair.size = pubicHairSize
}

// Upgrade 2 -> 3
// 2 -> 3
if (currentVersion === 2 && newVersion === 3) {
const { processing, preferences } = currentSettings

@@ -225,7 +225,7 @@ class SettingsService extends BaseService {
}
}

// Upgrade 3 -> 4
// 3 -> 4
if (currentVersion === 3 && newVersion === 4) {
newSettings.version = 4
newSettings.app = {
@@ -234,6 +234,12 @@ class SettingsService extends BaseService {
}
}

// 4 -> 5
if (currentVersion === 4 && newVersion === 5) {
newSettings.version = 5
newSettings.preferences.advanced.transformMode = 'normal'
}

this.set(newSettings)
}
}

+ 7
- 0
src/electron/src/modules/tools/paths.js View File

@@ -7,6 +7,7 @@
//
// Written by Ivan Bravo Bravo <ivan@dreamnet.tech>, 2019.

import { attempt } from 'lodash'
import { join } from 'path'
import fs from 'fs-extra'
import { app } from 'electron'
@@ -59,6 +60,8 @@ export const getCheckpointsPath = (...args) => getPowerPath('checkpoints', ...ar
export const getCropPath = (...args) => {
let folder = settings.folders.cropped

attempt(() => fs.ensureDirSync(folder))

if (!fs.existsSync(folder)) {
folder = getPath('temp')
}
@@ -69,6 +72,8 @@ export const getCropPath = (...args) => {
export const getModelsPath = (...args) => {
let folder = settings.folders.models

attempt(() => fs.ensureDirSync(folder))

if (!fs.existsSync(folder)) {
folder = getPath('userData', 'models')
}
@@ -83,6 +88,8 @@ export const getModelsPath = (...args) => {
export const getMasksPath = (...args) => {
let folder = settings.folders.masks

attempt(() => fs.ensureDirSync(folder))

if (!fs.existsSync(folder)) {
folder = getPath('userData', 'masks')
}

+ 14
- 6
src/electron/src/modules/tools/power.js View File

@@ -19,6 +19,8 @@ import { settings } from '../services'
const logger = require('logplease').create('electron:power')

export function exec(args, options = {}) {
args.push('--debug')

if (settings.processing.usePython) {
// python script
args.unshift('main.py')
@@ -52,9 +54,10 @@ export function exec(args, options = {}) {
export const transform = (run) => {
// Independent preferences for the photo
const { preferences } = run
const { fileFinal, scaleMode, overlay } = run.photo

// input
const inputFilepath = run.photo.inputFile.path
const inputFilepath = fileFinal.path

// output
const outputFilepath = run.outputFile.path
@@ -62,7 +65,7 @@ export const transform = (run) => {
// CLI Args
const args = ['run', '--input', inputFilepath, '--output', outputFilepath]

// Device preferences
// device preferences
if (settings.processing.device === 'CPU') {
args.push('--cpu', '--n-cores', settings.processing.cores)
} else {
@@ -71,21 +74,26 @@ export const transform = (run) => {
}
}

// Advanced preferences
const { scaleMode, useColorTransfer } = preferences.advanced
// advanced preferences
const { useColorTransfer, transformMode } = preferences.advanced

if (scaleMode === 'overlay') {
const { overlay } = run.photo
args.push('--overlay', `${overlay.startX},${overlay.startY}:${overlay.endX},${overlay.endY}`)
} else if (scaleMode !== 'none' && scaleMode !== 'cropjs') {
args.push(`--${scaleMode}`)
}

if (transformMode === 'export-maskfin') {
args.push('--export-step', 4, '--export-step-path', run.maskfinFile.path)
} else if (transformMode === 'import-maskfin') {
args.push('--steps', '5:5')
}

if (useColorTransfer) {
args.push('--color-transfer')
}

// Body preferences
// body preferences
args.push('--bsize', preferences.body.boobs.size)
args.push('--asize', preferences.body.areola.size)
args.push('--nsize', preferences.body.nipple.size)

+ 2
- 2
src/layouts/default.vue View File

@@ -8,7 +8,7 @@

<layout-jobbar />

<div class="layout__content">
<div id="layout-content" class="layout__content">
<nuxt />
</div>
</div>
@@ -42,7 +42,7 @@ export default {
}

.layout__content {
@apply overflow-hidden overflow-y-auto;
@apply relative overflow-hidden overflow-y-auto;
grid-area: content;
height: calc(100vh - 80px);
}

+ 8
- 10
src/middleware/checks.js View File

@@ -1,13 +1,11 @@
/*
* 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/>.
*/
// DreamTime.
// Copyright (C) DreamNet. All rights reserved.
//
// 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. See <https://www.gnu.org/licenses/gpl-3.0.html>
//
// Written by Ivan Bravo Bravo <ivan@dreamnet.tech>, 2019.

const { settings } = $provider.services
const { system } = $provider.tools

+ 87
- 0
src/modules/editor.theme.js View File

@@ -0,0 +1,87 @@
// DreamTime.
// Copyright (C) DreamNet. All rights reserved.
//
// 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. See <https://www.gnu.org/licenses/gpl-3.0.html>
//
// Written by Ivan Bravo Bravo <ivan@dreamnet.tech>, 2019.

import resolveConfig from 'tailwindcss/resolveConfig'
import tailwindConfig from '~/tailwind.config'

const config = resolveConfig(tailwindConfig)

export const blackTheme = {
'common.bi.display': 'none',
'common.bisize.display': 'none',
'common.backgroundImage': 'none',
'common.backgroundColor': config.theme.colors.background,
'common.border': '0px',

// header
'header.backgroundImage': 'none',
'header.backgroundColor': 'transparent',
'header.border': '0px',

// load button
'loadButton.display': 'none',

// download button
'downloadButton.display': 'none',

// main icons
'menu.normalIcon.path': './assets/images/svg/icon-d.svg',
'menu.normalIcon.name': 'icon-d',
'menu.activeIcon.path': './assets/images/svg/icon-b.svg',
'menu.activeIcon.name': 'icon-b',
'menu.disabledIcon.path': './assets/images/svg/icon-a.svg',
'menu.disabledIcon.name': 'icon-a',
'menu.hoverIcon.path': './assets/images/svg/icon-c.svg',
'menu.hoverIcon.name': 'icon-c',
'menu.iconSize.width': '24px',
'menu.iconSize.height': '24px',

// submenu primary color
'submenu.backgroundColor': config.theme.colors.dark[500],
'submenu.partition.color': config.theme.colors.dark[100],

// submenu icons
'submenu.normalIcon.path': './assets/images/svg/icon-d.svg',
'submenu.normalIcon.name': 'icon-d',
'submenu.activeIcon.path': './assets/images/svg/icon-c.svg',
'submenu.activeIcon.name': 'icon-c',
'submenu.iconSize.width': '32px',
'submenu.iconSize.height': '32px',

// submenu labels
'submenu.normalLabel.color': '#8a8a8a',
'submenu.normalLabel.fontWeight': 'lighter',
'submenu.activeLabel.color': '#fff',
'submenu.activeLabel.fontWeight': 'lighter',

// checkbox style
'checkbox.border': '0px',
'checkbox.backgroundColor': '#fff',

// range style
'range.pointer.color': '#fff',
'range.bar.color': '#666',
'range.subbar.color': '#d1d1d1',

'range.disabledPointer.color': '#414141',
'range.disabledBar.color': '#282828',
'range.disabledSubbar.color': '#414141',

'range.value.color': '#fff',
'range.value.fontWeight': 'lighter',
'range.value.fontSize': '11px',
'range.value.border': '1px solid #353535',
'range.value.backgroundColor': '#151515',
'range.title.color': '#fff',
'range.title.fontWeight': 'lighter',

// colorpicker style
'colorpicker.button.border': '1px solid #1e1e1e',
'colorpicker.title.color': '#fff',
}

+ 44
- 19
src/modules/nudify/nudify.js View File

@@ -9,14 +9,13 @@

import {
startsWith, find, isNil,
filter, map, debounce,
filter, debounce,
remove, clone,
} from 'lodash'
import { join } from 'path'
import { basename } from 'path'
import Queue from 'better-queue'
import MemoryStore from 'better-queue-memory'
import Swal from 'sweetalert2'
import delay from 'delay'
import { events } from '../events'
import { Photo } from './photo'
import { File } from '../file'
@@ -25,9 +24,17 @@ import { getFilesMetadata } from '~/workers/fs'
const logger = require('logplease').create('nudify')

const { settings } = $provider.services
const { existsSync, statSync, readdir } = $provider.tools.fs

const MAX_PHOTOS = 1000

const Toast = Swal.mixin({
toast: true,
position: 'bottom-end',
showConfirmButton: false,
timer: 1500,
timerProgressBar: true,
target: '#layout-content',
})
export class Nudify {
/**
* @type {Queue}
@@ -121,8 +128,8 @@ export class Nudify {
*
* @param {File} input
*/
static add(file) {
const photo = new Photo(file)
static add(file, params = {}) {
const photo = new Photo(file, params)

const exists = find(this.photos, ['id', photo.id])

@@ -133,6 +140,7 @@ export class Nudify {
this.photos.unshift(photo)

logger.debug('Photo added!', photo.file.fullname)

this.emitUpdate()

if (this.photos.length > MAX_PHOTOS) {
@@ -140,12 +148,16 @@ export class Nudify {
this.photos.pop()
}

const { uploadMode } = settings.app
if (params.isMaskfin) {
window.$router.push(`/nudify/${photo.id}/editor`)
} else {
const { uploadMode } = settings.app

if (uploadMode === 'add-queue') {
this.addToQueue(photo)
} else if (uploadMode === 'go-preferences') {
window.$router.push(`/nudify/${photo.id}`)
if (uploadMode === 'add-queue') {
this.addToQueue(photo)
} else if (uploadMode === 'go-preferences') {
window.$router.push(`/nudify/${photo.id}/preferences`)
}
}
}

@@ -153,8 +165,9 @@ export class Nudify {
*
* @param {string} filepath
*/
static async addFile(filepath, skipErrors = false) {
static async addFile(filepath) {
const filesMetadata = await getFilesMetadata(filepath)
const multiple = filesMetadata.length > 1

filesMetadata.forEach((metadata) => {
const file = File.fromMetadata(metadata)
@@ -162,27 +175,39 @@ export class Nudify {
try {
this.add(file)
} catch (err) {
if (skipErrors) {
if (multiple) {
logger.warn('Error adding a photo, skipped.', err)
} else {
throw err
}
}
})

Toast.fire({
icon: 'success',
title: basename(filepath),
})
}

/**
*
* @param {string} paths
*/
static addFiles(paths) {
const promises = []
static async addFiles(paths) {
Swal.fire({
title: 'Importing files...',
text: 'One moment, please.',
showConfirmButton: false,
allowOutsideClick: false,
allowEscapeKey: false,
})

Swal.showLoading()

for (const path of paths) {
promises.push(this.addFile(path, true))
// eslint-disable-next-line no-await-in-loop
await this.addFile(path)
}

return Promise.all(promises)
}

/**
@@ -278,7 +303,7 @@ export class Nudify {
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#F44336',
confirmButtonText: 'Yes, forget it!',
confirmButtonText: 'Yes, forget it',
})

if (!response.value) {

+ 21
- 9
src/modules/nudify/photo-run.js View File

@@ -11,7 +11,6 @@ import {
isNil, isEmpty, truncate, deburr, forIn, cloneDeep,
} from 'lodash'
import deferred from 'deferred'
import moment from 'moment'
import { File } from '../file'
import { Timer } from '../timer'
import { rand } from '../helpers'
@@ -21,6 +20,7 @@ import preferencesConfig from '../config/preferences'
const { settings, nucleus } = $provider.services
const { transform } = $provider.tools.power
const { activeWindow } = $provider.util
const { getMasksPath } = $provider.tools.paths

export class PhotoRun {
/**
@@ -43,6 +43,11 @@ export class PhotoRun {
*/
outputFile

/**
* @type {File}
*/
maskfinFile

/**
* @type {string}
*/
@@ -88,7 +93,7 @@ export class PhotoRun {
}

get outputName() {
const now = moment().unix()
const now = Date.now()
const { file } = this.photo

const originalName = truncate(
@@ -96,9 +101,7 @@ export class PhotoRun {
{ length: 30, omission: '' },
)

const extension = file.mimetype === 'image/gif' ? 'gif' : 'png'

return `${originalName}-${now}-dreamtime.${extension}`
return `${originalName}-${this.id}${now}-dreamtime${file.extension}`
}

constructor(id, photo) {
@@ -108,6 +111,9 @@ export class PhotoRun {
// output file
this.outputFile = new File(photo.getFolderPath(this.outputName))

// maskfin file
this.maskfinFile = new File(getMasksPath(`maskfin-${this.outputName}`))

// preferences
this.preferences = cloneDeep(this.photo.preferences)
}
@@ -129,11 +135,13 @@ export class PhotoRun {
toObject() {
return {
photo: {
inputFile: this.photo.inputFile,
fileFinal: this.photo.fileFinal,
scaleMode: this.photo.scaleMode,
overlay: this.photo.overlay,
},
outputFile: this.outputFile,
preferences: this.preferences,
outputFile: this.outputFile,
maskfinFile: this.maskfinFile,
}
}

@@ -185,8 +193,13 @@ export class PhotoRun {
})

this.process.on('success', async () => {
await this.outputFile.open()
await Promise.all([
this.outputFile.open(),
this.maskfinFile.open(),
])

nucleus.track('DREAM_COMPLETED')

def.resolve()
})

@@ -194,7 +207,6 @@ export class PhotoRun {
if (fileError) {
def.reject(new AppError('DreamPower has transformed the photo but could not save it.', { title: `Run ${this.id} failed!`, level: 'warn' }))
} else {
console.log(this.getPowerError())
def.reject(this.getPowerError())
}
})

+ 119
- 41
src/modules/nudify/photo.js View File

@@ -8,7 +8,7 @@
// Written by Ivan Bravo Bravo <ivan@dreamnet.tech>, 2019.

import {
clone, isNil,
cloneDeep, isNil, merge,
} from 'lodash'
import Queue from 'better-queue'
import MemoryStore from 'better-queue-memory'
@@ -37,7 +37,12 @@ export class Photo {
/**
* @type {File}
*/
fileCropped
fileEditor

/**
* @type {File}
*/
fileCrop

/**
* @type {EventBus}
@@ -84,10 +89,15 @@ export class Photo {
timer = new Timer()

/**
* @type {Object}
* @type {require('cropperjs').default}
*/
cropper

/**
* @type {require('tui-image-editor').default}
*/
editor

/**
* @type {Object}
*/
@@ -128,9 +138,42 @@ export class Photo {
return this.running || this.finished
}

get inputFile() {
if (this.preferences.advanced.scaleMode === 'cropjs') {
return this.fileCropped
get scaleMode() {
const { scaleMode } = this.preferences.advanced

if (scaleMode === 'cropjs' && !this.fileCrop.exists) {
// no crop, automatically rescale for convenience
return 'auto-rescale'
}

return scaleMode
}

/**
* Final file to process.
*
* @type {File}
*/
get fileFinal() {
if (this.scaleMode === 'cropjs') {
return this.fileCrop
}

if (this.fileEditor.exists) {
return this.fileEditor
}

return this.file
}

/**
* File for the croppper.
*
* @type {File}
*/
get fileInput() {
if (this.fileEditor.exists) {
return this.fileEditor
}

return this.file
@@ -141,26 +184,89 @@ export class Photo {
* @param {File} file
* @param {*} [model]
*/
constructor(file, model) {
constructor(file, { isMaskfin = false, model = null } = {}) {
this.id = file.md5

this.file = file

this.fileCropped = file.mimetype === 'image/gif' ? new File(getCropPath(`${this.id}.gif`)) : new File(getCropPath(`${this.id}.png`))
this.fileEditor = new File(getCropPath(`${this.id}-editor${file.extension}`), 'editor')

this.preferences = clone(settings.preferences)
this.fileCrop = new File(getCropPath(`${this.id}-crop${file.extension}`), 'crop')

this._logger = Logger.create(`nudify:photo:${this.id}`)
this._logger = Logger.create(`nudify:photo:${file.fullname}`)

this._setupPreferences(isMaskfin)

this._validate()

this._setupQueue()
}

async syncEditor() {
if (isNil(this.editor)) {
return
}

const dataURL = this.editor.toDataURL({
format: this.file.extension.substring(1),
quality: 1,
multiplier: 1,
})

await this.fileEditor.writeDataURL(dataURL)
this._logger.debug(`Saved editor photo.`)
}

async syncCrop() {
if (isNil(this.cropper)) {
return
}

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

const dataURL = canvas.toDataURL(this.fileCrop.mimetype, 1)

await this.fileCrop.writeDataURL(dataURL)
this._logger.debug(`Saved crop photo.`)
}

getFolderPath(...args) {
return getModelsPath(this.folderName, ...args)
}

_setupPreferences(isMaskfin) {
this.preferences = cloneDeep(settings.preferences)

if (isMaskfin) {
const forcedPreferences = {
body: {
executions: 1,
randomize: false,
progressive: {
enabled: false,
},
},
advanced: {
scaleMode: 'auto-rescale',
transformMode: 'import-maskfin',
useColorTransfer: false,
},
}

this.preferences = merge(this.preferences, forcedPreferences)
}
}

_validate() {
const { exists, mimetype, path } = this.file

@@ -235,24 +341,17 @@ export class Photo {

async start() {
const { executions } = this.preferences.body
const { scaleMode } = this.preferences.advanced

if (executions === 0) {
return
}

if (scaleMode === 'cropjs') {
try {
await this.crop()
} catch (err) {
this.removeFromQueue()
throw err
}
}
await this.syncEditor()
await this.syncCrop()

this.reset()

this._logger.debug(`Transforming ${this.file.fullname} with ${executions} runs.`)
this._logger.debug(`Starting ${executions} runs.`)

this._onStart()

@@ -289,27 +388,6 @@ export class Photo {
this._onStart()
}

async crop() {
if (isNil(this.cropper)) {
throw new AppError('This photo has the manual crop selected, you must open the Crop at least once to continue.', { title: `${this.file.fullname} it is not ready.`, level: 'warn' })
}

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

const dataURL = canvas.toDataURL(this.fileCropped.mimetype, 1)
await this.fileCropped.writeDataURL(dataURL)
}

_run(run, cb) {
try {
run.start().then(() => {

+ 12
- 0
src/nuxt.config.js View File

@@ -48,6 +48,8 @@ module.exports = {
css: [
'tippy.js/dist/tippy.css',
'cropperjs/dist/cropper.css',
'tui-image-editor/dist/tui-image-editor.css',
'tui-color-picker/dist/tui-color-picker.css',

'~/assets/css/tailwind.scss',
'~/assets/css/fonts.scss',
@@ -127,6 +129,16 @@ module.exports = {
exclude: /(node_modules)/,
})

const urlLoader = config.module.rules.find((rule) => {
if (!rule.use || !rule.use[0]) {
return false
}

return rule.use[0].loader === 'url-loader'
})

urlLoader.use[0].options.limit = 100000000

if (isDev) {
config.devtool = 'source-map'
} else {

+ 5
- 2
src/package.json View File

@@ -4,7 +4,7 @@
"description": "DreamTime allows you to nudify photos of people.",
"author": "DreamNet",
"homepage": "https://time.dreamnet.tech",
"version": "1.2.1",
"version": "1.3.0",
"main": "electron/dist/index.js",
"license": "GPL-3.0-only",
"private": true,
@@ -16,6 +16,7 @@
"url": "https://github.com/dreamnettech/dreamtime/issues"
},
"scripts": {
"postinstall": "patch-package",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
"test": "env-cmd -e default,test mocha",
"development": "env-cmd -e default,development --no-override",
@@ -67,6 +68,7 @@
"delay": "^4.3.0",
"electron-context-menu": "^0.15.1",
"electron-util": "^0.13.0",
"fabric": "1.6.7",
"filesize": "^6.0.1",
"form-data": "^3.0.0",
"fs-extra": "^8.1.0",
@@ -93,6 +95,7 @@
"sweetalert2": "^9.4.0",
"systeminformation": "^4.15.3",
"tippy.js": "^5.1.1",
"tui-image-editor": "^3.7.1",
"unzipper": "^0.10.5",
"uuid": "^3.3.3",
"webpack-node-externals": "^1.7.2"
@@ -145,4 +148,4 @@
"tailwindcss-alpha": "hacknug/tailwindcss-alpha#feature/tests",
"worker-loader": "^2.0.0"
}
}
}

+ 0
- 4
src/pages/about.vue View File

@@ -203,10 +203,6 @@ export default {
},
},

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

methods: {
openAppPath() {
shell.openItem(getAppPath())

+ 0
- 4
src/pages/dreamnet.vue View File

@@ -67,10 +67,6 @@ export default {
},
},

created() {
nucleus.track('PAGE_COMMUNITY')
},

mounted() {
setTimeout(() => {
if (_.isNil(this.$refs.intro)) {

+ 1
- 5
src/pages/index.vue View File

@@ -1,10 +1,6 @@
<template>
<div class="home content-body">
<div v-if="alert" class="notification is-warning" v-html="alert" />

<div v-if="$nudify.photos.length >= 500" class="notification is-warning">
The application may become unstable by keeping {{ $nudify.photos.length }} photos in the queue.
</div>
<div v-if="alert" class="notification" v-html="alert" />

<!-- Quick Upload -->
<nudify-upload />

+ 10
- 5
src/pages/nudify/_id.vue View File

@@ -14,6 +14,11 @@
icon="sliders-h"
:href="`/nudify/${photo.id}/preferences`" />

<box-item
label="Editor"
icon="paint-brush"
:href="`/nudify/${photo.id}/editor`" />

<box-item
v-show="photo.preferences.advanced.scaleMode === 'cropjs'"
label="Crop"
@@ -43,7 +48,7 @@
<span>Remove from queue</span>
</button>

<button v-show="photo.running" class="button button--danger" @click.prevent="cancel">
<button v-show="photo.running" class="button button--danger" @click.prevent="stop">
<span class="icon"><font-awesome-icon icon="stop" /></span>
<span>Stop</span>
</button>
@@ -113,7 +118,7 @@ export default {
this.$router.push(`/nudify/${this.photo.id}/results`)
},

cancel() {
stop() {
this.photo.cancel()
},

@@ -128,18 +133,18 @@ export default {
async forget() {
const response = await Swal.fire({
title: 'Are you sure?',
text: 'Forgetting the photo will remove it from the queue (it will not delete the files) and free up memory.',
text: 'Forgetting the photo will remove it from the queue (but not the files) and free up memory.',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#F44336',
confirmButtonText: 'Yes, delete it!',
confirmButtonText: 'Yes, forget it',
})

if (!response.value) {
return
}

Nudify.remove(this.photo)
Nudify.forget(this.photo)

this.$router.push(`/`)
},

+ 17
- 3
src/pages/nudify/_id/crop.vue View File

@@ -5,6 +5,11 @@
</div>

<div class="cropper__help">
<button v-tooltip="'Get recent changes from the editor.'" class="button" @click.prevent="reload">
<span class="icon"><font-awesome-icon icon="sync" /></span>
<span>Reload</span>
</button>

<section class="box">
<div class="box__header">
<h2 class="title">
@@ -75,14 +80,14 @@ export default {
},

mounted() {
this.createCropper()
this.create()
},

methods: {
/**
*
*/
async createCropper() {
create() {
this.photo.cropper = new Cropper(this.$refs.cropCanvas, {
viewMode: 0,
dragMode: 'move',
@@ -101,7 +106,12 @@ export default {
wheelZoomRatio: 0.03,
})

this.cropper.replace(this.photo.file.dataURL)
this.reload()
},

async reload() {
await this.photo.syncEditor()
this.cropper.replace(this.photo.fileInput.dataURL)
},
},
}
@@ -115,6 +125,10 @@ export default {
.cropper__help {
@apply w-1/4 ml-4;

.button {
@apply mb-6 w-full;
}

.box p {
@apply text-sm mb-4;
}

+ 58
- 0
src/pages/nudify/_id/editor.vue View File

@@ -0,0 +1,58 @@
<template>
<div class="nudify-editor">
<div ref="imageEditor" class="editor" />
</div>
</template>

<script>
import ImageEditor from 'tui-image-editor'
import { blackTheme } from '~/modules/editor.theme'

export default {
computed: {
photo() {
return this.$parent.photo
},
},

mounted() {
this.create()
},

methods: {
/**
*
*/
async create() {
this.photo.editor = new ImageEditor(this.$refs.imageEditor, {
includeUI: {
loadImage: {
path: this.photo.file.dataURL,
name: this.photo.file.name,
},
theme: blackTheme,
initMenu: 'draw',
menu: ['draw', 'shape', 'flip', 'rotate', 'text', 'mask'],
},
usageStatistics: false,
})
},
},
}
</script>

<style lang="scss" scoped>
.nudify-editor {
@apply h-full;
}

.editor {
@apply w-full h-full;
}
</style>

<style lang="scss">
.tui-image-editor-control {
@apply bg-dark-600;
}
</style>

+ 18
- 18
src/pages/nudify/_id/overlay.vue View File

@@ -5,6 +5,11 @@
</div>

<div class="cropper__help">
<button v-tooltip="'Get recent changes from the editor.'" class="button" @click.prevent="reload">
<span class="icon"><font-awesome-icon icon="sync" /></span>
<span>Reload</span>
</button>

<section class="box">
<div class="box__header">
<h2 class="title">
@@ -45,20 +50,6 @@
</p>
</div>
</section>

<section class="box">
<div class="box__header">
<h2 class="title">
<font-awesome-icon icon="info-circle" /> Do not leave empty spaces.
</h2>
</div>

<div class="box__content">
<p>
Leaving empty spaces will cause the transformation to fail.
</p>
</div>
</section>
</div>
</div>
</template>
@@ -79,14 +70,14 @@ export default {
},

mounted() {
this.createCropper()
this.create()
},

methods: {
/**
*
*/
async createCropper() {
async create() {
this.$refs.cropCanvas.addEventListener('crop', () => {
const data = this.cropper.getData()

@@ -99,7 +90,7 @@ export default {
})

this.cropper = new Cropper(this.$refs.cropCanvas, {
viewMode: 0,
viewMode: 1,
dragMode: 'move',
cropBoxMovable: false,
cropBoxResizable: false,
@@ -116,7 +107,12 @@ export default {
wheelZoomRatio: 0.05,
})

this.cropper.replace(this.photo.file.dataURL)
this.reload()
},

async reload() {
await this.photo.syncEditor()
this.cropper.replace(this.photo.fileInput.dataURL)
},
},
}
@@ -130,6 +126,10 @@ export default {
.cropper__help {
@apply w-1/4 ml-4;

.button {
@apply mb-6 w-full;
}

.box p {
@apply text-sm mb-4;
}

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

@@ -1,18 +1,8 @@
<template>
<!-- Results -->
<div v-if="photo.started" class="nudify-results">
<div class="results__status">
<h2 v-if="!photo.running">
Dream come true, we hope it is pleasant.
</h2>

<h2 v-else>
Hurry up, We're Dreaming
</h2>
</div>

<!-- Runs -->
<div class="results__runs">
<div class="runs">
<nudify-photo-run v-for="(run, index) in photo.runs" :key="index" :run="run" />
</div>
</div>
@@ -66,11 +56,16 @@ export default {
}
}

.results__runs {
.runs {
@apply flex flex-wrap justify-between;

.c-photo-run {
width: calc(1/4*100% - (1 - 1/4)*1rem);
@apply mb-2;
width: calc(1/2*100% - (1 - 1/2)*1rem);

@screen xl {
width: calc(1/3*100% - (1 - 1/3)*1rem);
}
}
}
</style>

+ 4
- 4
src/pages/settings/app.vue View File

@@ -16,17 +16,17 @@
</box-item>

<box-item
label="On Upload"
description="Select what should be done when uploading a new photo.">
label="Upload mode"
description="What will happen when uploading a photo.">
<select v-model="currentValue.app.uploadMode" class="input">
<option value="none">
Stay
</option>
<option value="add-queue">
Start photo transformation
Start transformation
</option>
<option value="go-preferences">
Change photo preferences
Change preferences
</option>
</select>
</box-item>

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

@@ -25,9 +25,8 @@
</box-item>

<box-item
v-if="false"
label="Masks"
description="Location where the algorithm masks photos will be saved.">
description="Location where the algorithm masks photos will be saved. We recommend selecting a temporary folder.">
<input v-model="currentValue.folders.masks" class="input" readonly title="Change" @click.prevent="changeMasks">
</box-item>
</div>
@@ -46,10 +45,6 @@ const { dialog } = $provider.api
export default {
mixins: [VModel],

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

methods: {
showOpenDialog(path) {
const dir = dialog.showOpenDialogSync({

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

@@ -50,9 +50,5 @@ import { VModel } from '~/mixins'

export default {
mixins: [VModel],

</