Browse Source

A lot of unstable design and internal changes.

tags/v1.5.0-rc2.2
Ivan Bravo Bravo 5 months ago
parent
commit
2aaa3765d9
58 changed files with 2078 additions and 1179 deletions
  1. 71
    106
      src/.eslintrc.js
  2. 0
    0
      src/.npmrc.old
  3. 35
    6
      src/assets/css/components/_box.scss
  4. 2
    2
      src/assets/css/components/_button.scss
  5. 2
    11
      src/assets/css/components/_container.scss
  6. 4
    8
      src/assets/css/components/_form.scss
  7. 2
    2
      src/assets/css/components/_notification.scss
  8. 1
    6
      src/assets/css/reset/_base.scss
  9. 2
    2
      src/assets/css/reset/_libs.scss
  10. 8
    11
      src/assets/css/reset/_scrollbar.scss
  11. 5
    0
      src/assets/css/utilities/all.scss
  12. BIN
      src/assets/images/ouch/blogging.png
  13. 1
    0
      src/assets/images/undraw/undraw_depi_wexf.svg
  14. 1
    0
      src/assets/images/undraw/undraw_female_avatar_w3jk.svg
  15. 1
    0
      src/assets/images/undraw/undraw_logic_4ocy.svg
  16. 1
    0
      src/assets/images/undraw/undraw_personal_settings_kihd.svg
  17. 1
    0
      src/assets/images/undraw/undraw_photograph_rde1.svg
  18. 1
    0
      src/assets/images/undraw/undraw_throw_down_ub2l.svg
  19. 112
    0
      src/components/Help/Lesson.vue
  20. 4
    0
      src/components/Help/index.js
  21. 71
    0
      src/components/Layout/Menubar.vue
  22. 181
    0
      src/components/Layout/Navbar.backup.vue
  23. 168
    49
      src/components/Layout/Navbar.vue
  24. 96
    0
      src/components/Layout/Titlebar.vue
  25. 0
    180
      src/components/Layout/Topbar.vue
  26. 6
    4
      src/components/Layout/index.js
  27. 2
    126
      src/components/Nudity/Upload.vue
  28. 46
    0
      src/components/Page/PageHeader.vue
  29. 4
    0
      src/components/Page/index.js
  30. 170
    0
      src/components/Queue/QueueBar.backup.vue
  31. 72
    67
      src/components/Queue/QueueBar.vue
  32. 1
    2
      src/components/Queue/QueuePhoto.vue
  33. 1
    1
      src/components/Queue/index.js
  34. 159
    0
      src/components/UI/MenuItem.vue
  35. 9
    6
      src/components/UI/index.js
  36. 2
    0
      src/components/index.js
  37. 97
    0
      src/layouts/default.backup.vue
  38. 15
    18
      src/layouts/default.vue
  39. 14
    0
      src/mixins/BaseMixin.js
  40. 35
    0
      src/modules/config/help.yml
  41. 21
    0
      src/modules/help.js
  42. 1
    0
      src/modules/index.js
  43. 29
    82
      src/nuxt.config.js
  44. 15
    8
      src/package.json
  45. 62
    35
      src/pages/about.vue
  46. 12
    10
      src/pages/dreamnet.vue
  47. 47
    0
      src/pages/help.vue
  48. 313
    17
      src/pages/index.vue
  49. 8
    311
      src/pages/nudify/_id/overlay.vue
  50. 39
    40
      src/pages/settings.vue
  51. 17
    14
      src/pages/settings/app.vue
  52. 13
    10
      src/pages/settings/notifications.vue
  53. 32
    19
      src/pages/settings/processing.vue
  54. 2
    2
      src/plugins/boot.js
  55. 4
    0
      src/plugins/setup.js
  56. 4
    0
      src/plugins/vue-portal.js
  57. 12
    0
      src/stylelint.config.js
  58. 44
    24
      src/tailwind.config.js

+ 71
- 106
src/.eslintrc.js View File

@@ -1,20 +1,10 @@
module.exports = {
root: true,
env: {
browser: true,
node: true,
mocha: true
},
extends: [
"@nuxtjs",
"airbnb-base",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:promise/recommended",
"plugin:lodash/recommended",
"plugin:vue/recommended",
"plugin:nuxt/recommended",
"plugin:mocha/recommended"
],
globals: {
$provider: false,
AppError: false,
@@ -24,109 +14,84 @@ module.exports = {
consola: false
},
parserOptions: {
parser: "babel-eslint",
parser: 'babel-eslint',
ecmaVersion: 2020,
allowImportExportEverywhere: true
},
extends: [
'@nuxtjs',
'plugin:nuxt/recommended',
'airbnb-base',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:promise/recommended',
'plugin:lodash/recommended',
'plugin:vue/recommended',
'plugin:mocha/recommended'
],
plugins: [
"import",
"promise",
"lodash",
"vue",
"mocha"
'nuxt',
'import',
'promise',
'lodash',
'vue',
'mocha'
],
root: true,
rules: {
"no-param-reassign": "off",
"class-methods-use-this": "off",
"no-trailing-spaces": "warn",
"comma-dangle": "warn",
"global-require": "off",
"import/default": "warn",
"import/no-webpack-loader-syntax": "off",
"import/order": ['error'],
"import/prefer-default-export": "off",
"import/no-extraneous-dependencies": "off",
"import/named": "warn",
"import/no-cycle": "off",
"promise/no-callback-in-promise": "off",
"promise/catch-or-return": "off",
"linebreak-style": "warn",
"new-parens": "off",
"lodash/import-scope": [
"off",
"member"
],
"lodash/prefer-constant": "off",
"lodash/prefer-immutable-method": "warn",
"lodash/prefer-includes": "warn",
"lodash/prefer-lodash-method": "off",
"lodash/prefer-lodash-typecheck": "warn",
"lodash/prefer-noop": "off",
"lodash/prefer-spread": "off",
"import/extensions": "off",
"max-len": "off",
"func-names": "off",
"no-await-in-loop": "warn",
"no-console": "warn",
"no-continue": "off",
"no-debugger": "error",
"no-lone-blocks": "error",
"no-restricted-globals": "warn",
"no-restricted-syntax": "off",
"no-shadow": "off",
"no-underscore-dangle": [
"error",
{
allowAfterThis: true
}
],
"no-unreachable": "warn",
"no-unused-vars": "warn",
"no-useless-constructor": "warn",
"nuxt/no-globals-in-created": "off",
"object-shorthand": [
"error",
"always"
],
"padded-blocks": [
"error",
"never"
],
"prefer-spread": "off",
"quote-props": [
"error",
"as-needed"
],
quotes: [
"error",
"single",
{
allowTemplateLiterals: true
}
],
semi: [
"error",
"never"
],
"spaced-comment": "warn",
"vue/html-closing-bracket-newline": [
"warn",
{
multiline: "never",
singleline: "never"
'import/named': 'error',
'import/no-cycle': 'off',
'import/no-extraneous-dependencies': 'off',
'import/no-webpack-loader-syntax': 'off',
'import/order': 'error',
'import/prefer-default-export': 'off',
'import/no-duplicates': 'off',
'lodash/import-scope': ['error', 'member'],
'lodash/prefer-constant': 'off',
'lodash/prefer-immutable-method': 'warn',
'lodash/prefer-includes': 'warn',
'lodash/prefer-lodash-method': 'off',
'lodash/prefer-lodash-typecheck': 'warn',
'lodash/prefer-noop': 'off',
'lodash/prefer-spread': 'off',
'vue/no-v-html': 'off',
'vue/singleline-html-element-content-newline': 'warn',
'vue/html-closing-bracket-newline': ['warn', { multiline: 'never', singleline: 'never' }],
'vue/max-attributes-per-line': ['warn', {
'singleline': 1,
'multiline': {
'max': 1,
'allowFirstLine': true
}
],
"vue/html-indent": [
"warn",
2
],
"vue/html-self-closing": "error",
"vue/no-v-html": "off",
"vue/singleline-html-element-content-newline": "warn",
'nuxt/no-cjs-in-config': 'off'
}],
'nuxt/no-cjs-in-config': 'off',
'nuxt/no-globals-in-created': 'off',
'linebreak-style': 'error',
'max-len': ['warn', { code: 120 }],
'no-await-in-loop': 'warn',
'no-continue': 'off',
'no-param-reassign': 'off',
'no-restricted-globals': 'warn',
'no-restricted-syntax': 'off',
'no-trailing-spaces': 'warn',
'no-tabs': 'error',
'no-undef': 'warn',
'class-methods-use-this': 'off',
'comma-dangle': 'warn',
'func-names': 'off',
'global-require': 'off',
'prefer-arrow-callback': 'off',
'no-underscore-dangle': ['error', { allowAfterThis: true }],
'object-shorthand': ['error', 'always'],
'padded-blocks': ['error', 'never'],
'prefer-spread': 'off',
'promise/no-callback-in-promise': 'off',
'quote-props': ['error', 'as-needed'],
'spaced-comment': 'warn',
'quotes': ['error', 'single'],
'semi': ['error', 'never']
},
settings: {
"import/resolver": {
'import/resolver': {
nuxt: {},
node: {},
webpack: {}

src/.npmrc → src/.npmrc.old View File


+ 35
- 6
src/assets/css/components/_box.scss View File

@@ -10,13 +10,22 @@
*/

.box {
@apply bg-dark-600 shadow mb-6 border-t border-l border-r border-dark-100;
@apply flex flex-col;
@apply pb-3 bg-dark-500 shadow-lg rounded-lg;

&:not(:last-child) {
@apply mb-6;
}

.box__photo {
@apply relative;
@apply bg-cover bg-center;
@apply bg-cover bg-center bg-no-repeat;
min-height: 120px;

&:first-child {
@apply rounded-t-lg;
}

.box__photo__content {
@apply absolute w-full h-full;
@apply bg-black-70;
@@ -24,10 +33,14 @@
}

.box__header {
@apply px-4 pt-2;
@apply px-6;

&:not(:first-child) {
@apply pt-3;
}

.title {
@apply text-base font-semibold text-generic-200;
@apply font-semibold text-generic-300;
}

.subtitle {
@@ -36,11 +49,27 @@
}

.box__content {
@apply px-4 py-2;
@apply flex-1 px-6;

&:not(:first-child) {
@apply pt-3;
}

&:not(:last-child) {
@apply pb-3;
}

p {
@apply text-sm mb-3;
}

.item {
@apply px-0;
}
}

.box__footer {
@apply px-4 py-2 text-sm;
@apply px-6 pt-3;
@apply border-t border-dark-400;
}
}

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

@@ -11,8 +11,8 @@

.button {
@apply inline-flex items-center justify-center;
@apply border border-primary-500-30;
@apply px-4 rounded;
@apply border border-primary-500;
@apply px-4 rounded-lg;
@apply text-primary-400 font-semibold uppercase;
@apply outline-none #{!important};
height: 40px;

+ 2
- 11
src/assets/css/components/_container.scss View File

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

.content-body,
.content__body {
@apply p-6;
}

.content__body {
@apply p-6;
}

.wrapper {
@apply ml-auto mr-auto;
.container {
@apply p-6 ml-auto mr-auto;
max-width: 1920px;
}

+ 4
- 8
src/assets/css/components/_form.scss View File

@@ -22,13 +22,13 @@
}

.input {
@apply border border-dark-300 bg-dark-700;
@apply rounded py-2 px-4 w-full text-generic-300 shadow-inner;
@apply border border-dark-400 bg-dark-500 text-generic-500;
@apply py-2 px-3 rounded w-full shadow-inner;
@include transition('background-color, color');
outline: none !important;
transition: all .2s ease-in-out;

&:hover, &:focus {
@apply text-generic-100 shadow-inner-md;
@apply text-white bg-dark-400;
}

&::placeholder {
@@ -43,7 +43,3 @@
@apply text-sm px-2;
}
}

select.input {
@apply cursor-pointer;
}

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

@@ -10,8 +10,8 @@
*/

.notification {
@apply mb-4 py-2 px-4 border-2 border-dark-100 rounded-sm;
@apply bg-transparent text-generic-500 text-sm;
@apply mb-6 py-2 px-3 border-2 border-dark-100 bg-dark-100;
@apply text-sm text-black font-semibold;

a {
@apply underline;

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

@@ -17,8 +17,7 @@
}

html {
@apply bg-black text-generic-500 font-sans;
//background-image: url('~assets/images/papyrus-dark.webp'); /* Background pattern from Toptal Subtle Patterns */
@apply bg-background text-generic-500 font-sans;
font-size: 16px;
text-size-adjust: 100%;
font-smoothing: antialiased;
@@ -35,7 +34,3 @@ html,
#__layout {
@apply h-full;
}

.title {
@apply font-serif;
}

+ 2
- 2
src/assets/css/reset/_libs.scss View File

@@ -61,8 +61,8 @@

.introjs-tooltip {
@apply bg-dark-500-90 #{!important};
min-width: 350px !important;
max-width: 400px !important;
min-width: 350px !important;
}

.introjs-arrow {
@@ -75,4 +75,4 @@

.introjs-overlay {
backdrop-filter: blur(10px);
}
}

+ 8
- 11
src/assets/css/reset/_scrollbar.scss View File

@@ -11,10 +11,9 @@


// the entire scrollbar
*::-webkit-scrollbar
{
@apply bg-dark-800;
width: 10px;
*::-webkit-scrollbar {
@apply bg-dark-900;
width: 10px;
}

// the buttons on the scrollbar (arrows pointing upwards and downwards).
@@ -23,9 +22,8 @@
}

// the draggable scrolling handle.
*::-webkit-scrollbar-thumb
{
@apply bg-dark-100;
*::-webkit-scrollbar-thumb {
@apply bg-dark-300;

&:hover {
@apply bg-primary-700;
@@ -33,10 +31,9 @@
}

// he track (progress bar) of the scrollbar.
*::-webkit-scrollbar-track
{
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
@apply bg-dark-800;
*::-webkit-scrollbar-track {
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
@apply bg-dark-800;
}

// the part of the track (progress bar) not covered by the handle.

+ 5
- 0
src/assets/css/utilities/all.scss View File

@@ -0,0 +1,5 @@
@mixin transition($property, $duration: .1s, $timing: ease-in-out) {
transition-property: #{$property};
transition-duration: $duration;
transition-timing-function: $timing;
}

BIN
src/assets/images/ouch/blogging.png View File


+ 1
- 0
src/assets/images/undraw/undraw_depi_wexf.svg
File diff suppressed because it is too large
View File


+ 1
- 0
src/assets/images/undraw/undraw_female_avatar_w3jk.svg
File diff suppressed because it is too large
View File


+ 1
- 0
src/assets/images/undraw/undraw_logic_4ocy.svg
File diff suppressed because it is too large
View File


+ 1
- 0
src/assets/images/undraw/undraw_personal_settings_kihd.svg
File diff suppressed because it is too large
View File


+ 1
- 0
src/assets/images/undraw/undraw_photograph_rde1.svg
File diff suppressed because it is too large
View File


+ 1
- 0
src/assets/images/undraw/undraw_throw_down_ub2l.svg
File diff suppressed because it is too large
View File


+ 112
- 0
src/components/Help/Lesson.vue View File

@@ -0,0 +1,112 @@
<template>
<div class="box lesson"
:class="{ 'lesson--small': small }"
@click="$emit('click')">
<div class="box__photo"
:class="[`photo--${lesson.photo}`]" />

<div class="box__header">
<span class="title">{{ lesson.title }}</span>
</div>

<div class="box__content"
v-html="content" />

<div v-if="!small"
class="box__footer text-center">
<a v-for="(button,key) in lesson.buttons"
:key="key"
:href="button.href"
target="_blank"
class="button button--sm">
{{ button.text }}
</a>
</div>
</div>
</template>

<script>
import MarkdownIt from 'markdown-it'
import { truncate } from 'lodash'

const md = new MarkdownIt()

export default {
props: {
lesson: {
type: Object,
required: true,
},

small: {
type: Boolean,
default: false,
},
},

computed: {
content() {
let content

if (this.small) {
content = this.lesson.summary || truncate(this.lesson.content, { length: 80 })
} else {
content = this.lesson.content
}

return md.render(content)
},
},
}
</script>

<style lang="scss" scoped>
.lesson {
&::v-deep ul {
@apply list-disc;

li {
@apply text-sm ml-3;
}
}
}

.lesson--small {
.title {
@apply text-sm;
}

.box__content {
&::v-deep p {
@apply text-xs #{!important};
}
}

.box__photo {
min-height: 80px !important;
}
}

.box__photo {
@apply bg-contain;

&.photo--tips {
background-color: #e0719e;
background-image: url('~assets/images/undraw/undraw_depi_wexf.svg')
}

&.photo--drag {
background-color: #778BB0;
background-image: url('~assets/images/undraw/undraw_logic_4ocy.svg')
}

&.photo--settings {
background-color: #46766B;
background-image: url('~assets/images/undraw/undraw_personal_settings_kihd.svg')
}
}

.box__content {
@apply text-sm;
}
</style>

+ 4
- 0
src/components/Help/index.js View File

@@ -0,0 +1,4 @@
import Vue from 'vue'
import HelpLesson from './Lesson.vue'

Vue.component('HelpLesson', HelpLesson)

+ 71
- 0
src/components/Layout/Menubar.vue View File

@@ -0,0 +1,71 @@
<template>
<div class="menu">
<section>
<!-- Upload Mode -->
<select
v-model="$settings.app.uploadMode"
v-tooltip="{ content: 'Upload mode. What will happen when uploading a photo.', placement: 'right' }"
class="input input--menu">
<option value="none">
Add to Pending
</option>
<option value="add-queue">
Add to Queue
</option>
<option value="go-preferences">
Add to Pending and Open preferences
</option>
</select>
</section>

<!-- Custom menu -->
<portal-target name="menu"
class="menu__custom" />

<!-- Random Lesson -->
<section v-if="help.randomLesson">
<HelpLesson :lesson="help.randomLesson"
:small="true"
@click="$router.push('/help')" />
</section>
</div>
</template>

<script>
import { Help } from '~/modules'

export default {
data: () => ({
help: Help,
}),
}
</script>

<style lang="scss" scoped>
.menu {
@apply flex flex-col;
@apply p-3 bg-dark-500;
grid-area: menu;

section {
&:not(:last-child) {
@apply pb-6 border-b-2 border-dark-400;
@apply mb-3;
}
}
}

.menu__custom {
@apply flex-1;
}

.lesson {
@apply bg-dark-900 cursor-pointer;
@apply shadow;
@include transition('background-color, box-shadow', 0.2s);

&:hover {
@apply bg-dark-800 shadow-lg;
}
}
</style>

+ 181
- 0
src/components/Layout/Navbar.backup.vue View File

@@ -0,0 +1,181 @@
<template>
<div class="layout__navbar">
<div class="navbar__left">
<nuxt-link v-if="canNudify" to="/" class="navbar__item navbar__item--home">
Upload
</nuxt-link>

<nuxt-link id="settings" class="navbar__item" to="/settings">
Settings
</nuxt-link>

<nuxt-link v-if="unlockedBadTime" v-tooltip="{ content: 'Mini-game. Can you survive until the end? ๐ŸŽฎ๐Ÿ’€', placement: 'bottom' }" class="navbar__item" to="/games/badtime">
Bad Time Game
</nuxt-link>

<a v-if="isDev" class="navbar__item" @click.prevent="createError">
Force Error
</a>
</div>

<div class="navbar__right">
<nuxt-link v-tooltip="{placement: 'bottom', content: 'Alert Center'}" class="navbar__icon" to="/alerts">
<font-awesome-icon v-if="hasAlerts" icon="exclamation-triangle" class="alerts--active" />
<font-awesome-icon v-else icon="check-circle" class="alerts--ok" />
</nuxt-link>

<nuxt-link v-tooltip="{placement: 'bottom', content: 'About'}" class="navbar__icon" to="/about">
<font-awesome-icon icon="info-circle" />
</nuxt-link>

<nuxt-link
v-if="$provider.system.online"
v-tooltip="{placement: 'bottom', content: 'DreamNet'}"
class="navbar__icon"
to="/dreamnet">
<font-awesome-icon icon="users" />
</nuxt-link>

<a
v-if="$provider.system.online"
id="guide"
v-tooltip="{placement: 'bottom', content: 'User\'s Guide.'}"
class="navbar__icon"
:href="manualURL"
target="_blank">
<font-awesome-icon icon="question-circle" />
</a>

<a
v-if="$provider.system.online"
v-tooltip="{placement: 'bottom', content: 'Donate and get benefits!'}"
class="navbar__icon"
:href="donateUrl"
target="_blank">
<font-awesome-icon :icon="['fab', 'patreon']" />
</a>
</div>
</div>
</template>

<script>
import { requirements, settings } from '~/modules/system'
import { dreamtrack } from '~/modules/services'
import { events } from '~/modules'

export default {
data: () => ({
unlockedBadTime: settings.achievements.badtime,
}),

computed: {
canNudify() {
return requirements.canNudify
},

donateUrl() {
return dreamtrack.get('urls.support.patreon', 'https://www.patreon.com/dreamnet')
},

manualURL() {
return dreamtrack.get('urls.docs.manual', 'https://time.dreamnet.tech/docs/guide/upload')
},

isDev() {
return process.env.NODE_ENV === 'development'
},

hasAlerts() {
return requirements.hasAlerts
},
},

mounted() {
events.on('achievements.badtime', () => {
this.unlockedBadTime = true
})
},

methods: {
createError() {
throw new Error('UI TEST ERROR')
},
},
}
</script>

<style lang="scss" scoped>
@keyframes alertAnim {
0% {
@apply text-danger-500;
}
50% {
@apply text-warning-500;
}
100% {
@apply text-danger-500;
}
}

.layout__navbar {
@apply flex bg-dark-500 z-10;
@apply border-b border-dark-100;
height: 50px;

.navbar__left,
.navbar__right {
@apply flex items-center;
}

.navbar__left {
@apply flex-1 mr-2;
}

.navbar__right {
@apply justify-end;
}

.navbar__item {
@apply mx-6 text-sm uppercase font-bold;

&:hover {
@apply text-white;
}

&:not(.navbar__item--home) {
&.nuxt-link-active {
@apply text-primary-500;
}
}

&.navbar__item--home {
&.nuxt-link-exact-active {
@apply text-primary-500;
}
}
}

.navbar__icon {
@apply mx-4;

&:hover {
@apply text-white;
}

&.nuxt-link-active {
@apply text-primary-500;
}
}
}

.alerts--active {
animation-name: alertAnim;
animation-iteration-count: infinite;
animation-duration: 3s;
animation-timing-function: ease-in-out;
}

.alerts--ok {
@apply text-success-500;
}
</style>

+ 168
- 49
src/components/Layout/Navbar.vue View File

@@ -1,74 +1,90 @@
<template>
<div class="layout__navbar">
<div class="navbar__left">
<nuxt-link v-if="canNudify" to="/" class="navbar__item navbar__item--home">
Upload
</nuxt-link>
<div class="nav">
<div class="nav__left">
<div v-tooltip="$dream.version"
class="nav__item nav__item--logo">
<span>{{ $dream.name }}</span>
</div>

<nuxt-link id="settings" class="navbar__item" to="/settings">
Settings
</nuxt-link>
<div class="nav__item nav__item--greetings">
<span v-if="!isBadTime">{{ greetings }}</span>
<span v-else><img src="~/assets/images/games/sans.png"> i don't like what you are doing.</span>
</div>
</div>

<nuxt-link v-if="unlockedBadTime" v-tooltip="{ content: 'Mini-game. Can you survive until the end? ๐ŸŽฎ๐Ÿ’€', placement: 'bottom' }" class="navbar__item" to="/games/badtime">
Bad Time Game
<div class="nav__center">
<nuxt-link v-tooltip="'Upload'"
to="/"
class="nav__item nav__item--link">
<font-awesome-icon icon="upload" />
</nuxt-link>

<a v-if="isDev" class="navbar__item" @click.prevent="createError">
Force Error
</a>
</div>
<div v-tooltip="'My panel'"
class="nav__item nav__item--link">
<img :src="avatar"
alt="Me">
</div>

<div class="navbar__right">
<nuxt-link v-tooltip="{placement: 'bottom', content: 'Alert Center'}" class="navbar__icon" to="/alerts">
<font-awesome-icon v-if="hasAlerts" icon="exclamation-triangle" class="alerts--active" />
<font-awesome-icon v-else icon="check-circle" class="alerts--ok" />
</nuxt-link>
<div v-tooltip="'Advanced Mode'"
class="nav__item nav__item--link">
<font-awesome-icon icon="mask" />
</div>
</div>

<nuxt-link v-tooltip="{placement: 'bottom', content: 'About'}" class="navbar__icon" to="/about">
<font-awesome-icon icon="info-circle" />
</nuxt-link>
<div class="nav__right">
<div v-if="isBadTimeAvailable"
v-tooltip="'Bad Time Game'"
class="nav__item nav__item--button">
<img src="~/assets/images/games/sans.png">
</div>

<nuxt-link
v-if="$provider.system.online"
v-tooltip="{placement: 'bottom', content: 'DreamNet'}"
class="navbar__icon"
to="/dreamnet">
<font-awesome-icon icon="users" />
<nuxt-link v-tooltip="'Settings'"
to="/settings"
class="nav__item nav__item--button">
<font-awesome-icon icon="cog" />
</nuxt-link>

<a
v-if="$provider.system.online"
id="guide"
v-tooltip="{placement: 'bottom', content: 'User\'s Guide.'}"
class="navbar__icon"
:href="manualURL"
target="_blank">
<font-awesome-icon icon="question-circle" />
</a>

<a
v-if="$provider.system.online"
v-tooltip="{placement: 'bottom', content: 'Donate and get benefits!'}"
class="navbar__icon"
:href="donateUrl"
target="_blank">
<font-awesome-icon :icon="['fab', 'patreon']" />
</a>
</div>
</div>
</template>

<script>
import dayjs from 'dayjs'
import Avatars from '@dicebear/avatars'
import sprites from '@dicebear/avatars-jdenticon-sprites'
import { requirements, settings } from '~/modules/system'
import { dreamtrack } from '~/modules/services'
import { events } from '~/modules'

export default {
data: () => ({
unlockedBadTime: settings.achievements.badtime,
isBadTimeAvailable: settings.achievements.badtime,
isBadTime: false,
}),

computed: {
avatar() {
const avatars = new Avatars(sprites, { base64: true })
return avatars.create(settings.user)
},

greetings() {
const hours = dayjs().hour()

if (hours >= 6 && hours <= 11) {
return 'โ˜• Good morning'
}

if (hours >= 12 && hours <= 19) {
return '๐ŸŒž Good afternoon'
}

if (hours >= 0 && hours <= 5) {
return '๐Ÿ Sweet dreams'
}

return '๐ŸŒ› Good night'
},

canNudify() {
return requirements.canNudify
},
@@ -92,7 +108,17 @@ export default {

mounted() {
events.on('achievements.badtime', () => {
this.unlockedBadTime = true
this.isBadTimeAvailable = true
})

this.$router.afterEach((to) => {
if (to.path === '/games/badtime') {
this.$dream.name = 'BadDreamTime'
this.isBadTime = true
} else {
this.$dream.name = process.env.npm_package_displayName
this.isBadTime = false
}
})
},

@@ -105,6 +131,18 @@ export default {
</script>

<style lang="scss" scoped>
@keyframes logoAnim {
0% {
background-position: 0% 0%;
}
50% {
background-position: 100% 0%;
}
100% {
background-position: 0% 0%;
}
}

@keyframes alertAnim {
0% {
@apply text-danger-500;
@@ -117,9 +155,90 @@ export default {
}
}

.nav {
@apply flex z-10;
@apply h-full bg-dark-500 border-b-2 border-dark-600 shadow;
grid-area: nav;

.nav__left,
.nav__center,
.nav__right {
@apply flex-1 flex;
}

.nav__left {
@apply flex-1;
}

.nav__center {
@apply justify-center;
}

.nav__right {
@apply justify-end items-center;
}
}

.nav__item {
@apply flex items-center;

img {
@apply rounded-full;
height: 30px;
}

&.nav__item--link {
@apply justify-center;
@apply border-b-2 border-transparent text-lg;
width: 100px;

&:hover {
@apply text-primary-500 border-primary-500;
}
}

&.nav__item--button {
@apply justify-center;
@apply rounded-full text-lg mr-3;
width: 40px;
height: 40px;

&:hover {
@apply bg-dark-800;
}

img {
height: 20px;
}
}

&.nav__item--logo {
@apply text-white text-sm font-bold px-6 select-none;

background: rgb(99, 66, 245);
background: linear-gradient(
40deg,
rgba(99, 66, 245, 1) 0%,
rgba(239, 125, 199, 1) 100%
);
background-size: 200% 100%;
background-position: 0% 0%;

animation-name: logoAnim;
animation-iteration-count: infinite;
animation-duration: 10s;
animation-timing-function: ease-in-out;
}

&.nav__item--greetings {
@apply text-white text-sm font-light px-3 select-none;
}
}

.layout__navbar {
@apply flex bg-dark-500 z-10;
@apply border-b border-dark-100;
grid-area: nav;
height: 50px;

.navbar__left,

+ 96
- 0
src/components/Layout/Titlebar.vue View File

@@ -0,0 +1,96 @@
<template>
<div class="titlebar">
<div class="titlebar__drag" />

<div class="titlebar__buttons">
<button id="minimize" type="button" @click="minimize">
<font-awesome-icon icon="minus" />
</button>

<button id="maximize" type="button" @click="maximize">
<font-awesome-icon :icon="['far', 'square']" />
</button>

<button id="close" type="button" class="close" @click="close">
<font-awesome-icon icon="times" />
</button>
</div>
</div>
</template>

<script>
const { getCurrentWindow } = require('electron').remote

const { api } = $provider.util

export default {
methods: {
minimize() {
try {
getCurrentWindow().minimize()
} catch (error) {
throw new Exception('There was a problem trying to minimize the window.', error)
}
},

maximize() {
try {
getCurrentWindow().maximize()
} catch (error) {
throw new Exception('There was a problem trying to maximize the window.', error)
}
},

close() {
api.app.quit()
},
},
}
</script>

<style lang="scss" scoped>


.titlebar {
@apply flex justify-end;
@apply bg-black text-white;
grid-area: title;
height: 30px;
z-index: 9999999;

.topbar__badtime {
@apply flex items-center justify-center;
@apply lowercase font-bold text-sm;
font-family: "Comic Sans MS", serif;

img {
@apply mr-2;
height: 18px;
}
}

.titlebar__drag {
@apply flex-1;
-webkit-app-region: drag;
}

.titlebar__buttons {
@apply flex;

button {
@apply flex items-center justify-center outline-none;
@apply text-xs;
width: 50px;
height: 30px;

&:hover {
@apply bg-dark-800;

&.close {
@apply bg-danger-500;
}
}
}
}
}
</style>

+ 0
- 180
src/components/Layout/Topbar.vue View File

@@ -1,180 +0,0 @@
<template>
<div class="layout__topbar">
<div class="topbar__left">
<div class="topbar__logo">
{{ $dream.name }} {{ $dream.version }}
</div>

<div v-show="!badTime" class="topbar__greetings">
{{ greetings }}
</div>

<div v-show="badTime" class="topbar__badtime">
<img src="~/assets/images/games/sans.png">
i don't like what you are doing.
</div>
</div>

<div class="topbar__buttons">
<button id="minimize" type="button" @click="minimize">
<font-awesome-icon icon="minus" />
</button>

<button id="maximize" type="button" @click="maximize">
<font-awesome-icon :icon="['far', 'square']" />
</button>

<button id="close" type="button" class="close" @click="close">
<font-awesome-icon icon="times" />
</button>
</div>
</div>
</template>

<script>
import dayjs from 'dayjs'

const { getCurrentWindow } = require('electron').remote

const { api } = $provider.util

export default {
data: () => ({
badTime: false,
}),

computed: {
greetings() {
const hours = dayjs().hour()

if (hours >= 6 && hours <= 11) {
return 'โ˜• Good morning'
}

if (hours >= 12 && hours <= 19) {
return '๐ŸŒž Good afternoon'
}

if (hours >= 0 && hours <= 5) {
return '๐Ÿ Sweet dreams'
}

return '๐ŸŒ› Good night'
},
},

mounted() {
this.$router.afterEach((to) => {
if (to.path === '/games/badtime') {
this.$dream.name = 'BadDreamTime'
this.badTime = true
} else {
this.$dream.name = process.env.npm_package_displayName
this.badTime = false
}
})
},

methods: {
minimize() {
try {
getCurrentWindow().minimize()
} catch (error) {
throw new Exception('There was a problem trying to minimize the window.', error)
}
},

maximize() {
try {
getCurrentWindow().maximize()
} catch (error) {
throw new Exception('There was a problem trying to maximize the window.', error)
}
},

close() {
api.app.quit()
},
},
}
</script>

<style lang="scss" scoped>
@keyframes bgAnim {
0% {
background-position: 0% 0%;
}
50% {
background-position: 100% 0%;
}
100% {
background-position: 0% 0%;
}
}

.layout__topbar {
@apply flex bg-black text-white;
height: 30px;
z-index: 9999999;

.topbar__left {
@apply flex-1 flex;
@apply text-sm;
-webkit-app-region: drag;
}

.topbar__logo {
@apply flex flex-col items-center justify-center mr-4;
@apply font-bold px-4;
background: rgb(99, 66, 245);

background: linear-gradient(
40deg,
rgba(99, 66, 245, 1) 0%,
rgba(239, 125, 199, 1) 100%
);

background-size: 200% 100%;

animation-name: bgAnim;
animation-iteration-count: infinite;
animation-duration: 10s;
animation-timing-function: ease-in-out;
}

.topbar__greetings {
@apply flex items-center justify-center;
@apply font-light;
}

.topbar__badtime {
@apply flex items-center justify-center;
@apply lowercase font-bold text-sm;
font-family: "Comic Sans MS", serif;

img {
@apply mr-2;
height: 18px;
}
}

.topbar__buttons {
@apply flex;

button {
@apply flex items-center justify-center outline-none;
@apply text-xs;
width: 50px;
height: 30px;

&:hover {
@apply bg-gray-700;

&.close {
@apply bg-danger-500;
}
}
}
}
}
</style>

+ 6
- 4
src/components/Layout/index.js View File

@@ -1,7 +1,9 @@
import Vue from 'vue'

import LayoutTopbar from './Topbar'
import LayoutNavbar from './Navbar'
import Titlebar from './Titlebar'
import Navbar from './Navbar'
import Menubar from './Menubar.vue'

Vue.component('LayoutTopbar', LayoutTopbar)
Vue.component('LayoutNavbar', LayoutNavbar)
Vue.component('Titlebar', Titlebar)
Vue.component('Navbar', Navbar)
Vue.component('Menubar', Menubar)

+ 2
- 126
src/components/Nudity/Upload.vue View File

@@ -2,129 +2,7 @@
<div id="uploader" class="uploader">
<!-- Uploader Selection -->
<div class="uploader__selection">
<div class="selection__menu">
<select
id="uploader-settings"
v-model="$settings.app.uploadMode"
v-tooltip="{ content: 'Upload mode. What will happen when uploading a photo.', placement: 'right' }"
class="input">
<option value="add-queue">
Put in Queue
</option>
<option value="none">
Put in Pending
</option>
<option value="go-preferences">
Put in Pending and Open preferences
</option>
</select>

<div id="uploader-methods" class="box box--items">
<div class="box__content">
<box-item
label="Instagram"
:icon="['fab', 'instagram']"
:is-link="true"
:class="{'box__item--active': selectionId === 1}"
@click="selectionId = 1" />

<box-item
label="Web"
icon="globe"
:is-link="true"
:class="{'box__item--active': selectionId === 0}"
@click="selectionId = 0" />

<box-item
label="File"
icon="file"
:is-link="true"
:class="{'box__item--active': selectionId === 2}"
@click="selectionId = 2" />

<box-item
label="Folder"
icon="folder-open"
:is-link="true"
:class="{'box__item--active': selectionId === 3}"
@click="selectionId = 3" />

<box-item
label="Examples"
icon="images"
href="https://time.dreamnet.tech/docs/guide/photos" />
</div>
</div>

<div class="box uploader__hint">
<div class="box__content">
<p>
<font-awesome-icon icon="exclamation-circle" />
You can drag and drop photos and folders into the application no matter where you are.
</p>
</div>
</div>
</div>

<div class="selection__content">
<!-- Web Address -->
<div v-show="selectionId === 0" class="selection__content__body">
<input v-model="webAddress" type="url" class="input mb-2" placeholder="https://" data-private="lipsum">

<p class="help">
Enter the web address of a photo that ends in a valid extension. <i>(jpg, png, gif)</i>
</p>

<button class="button" @click="openUrl">
<span class="icon"><font-awesome-icon icon="globe" /></span>
<span>Submit</span>
</button>
</div>

<!-- Instagram -->
<div v-show="selectionId === 1" class="selection__content__body">
<input v-model="instagramPhoto" type="url" class="input mb-2" placeholder="https://www.instagram.com/p/dU4fHDw-Ho/" data-private="lipsum">

<p class="help">
Enter the web address or ID of an Instagram photo.
</p>

<button class="button" @click="openInstagramPhoto">
<span class="icon"><font-awesome-icon :icon="['fab', 'instagram']" /></span>
<span>Submit</span>
</button>
</div>

<!-- File -->
<div v-show="selectionId === 2" class="selection__content__body">
<input
v-show="false"
ref="photo"
type="file"
accept="image/jpeg, image/png, image/gif"
multiple
@change="openFile">

<button class="button" @click.prevent="$refs.photo.click()">
<span>Open File</span>
</button>

<p class="help">
Select one or more photos from your computer.
</p>
</div>

<!-- Folder -->
<div v-show="selectionId === 3" class="selection__content__body">
<button class="button" @click.prevent="openFolder">
<span>Open folder</span>
</button>

<p class="help">
Select a folder from your computer. All valid photos inside will be uploaded.
</p>
</div>
</div>
<div class="selection__content" />
</div>
</div>
</template>
@@ -150,9 +28,7 @@ export default {
},

data: () => ({
selectionId: 1,
webAddress: '',
instagramPhoto: '',

}),

watch: {

+ 46
- 0
src/components/Page/PageHeader.vue View File

@@ -0,0 +1,46 @@
<template>
<div class="header">
<div class="header__left">
<slot />
</div>

<div v-if="this.$slots.center" class="header__center">
<slot name="center" />
</div>

<div v-if="this.$slots.right" class="header__right">
<slot name="right" />
</div>
</div>
</template>

<style lang="scss" scoped>
.header {
@apply flex mb-9;
}

.header__left {
@apply flex-1;

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

.title {
@apply text-lg font-semibold text-generic-100;

.icon {
@apply mr-2;
}
}

.subtitle {
@apply font-thin;

.help {
@apply ml-2;
cursor: help;
}
}
</style>

+ 4
- 0
src/components/Page/index.js View File

@@ -0,0 +1,4 @@
import Vue from 'vue'
import PageHeader from './PageHeader'

Vue.component('PageHeader', PageHeader)

+ 170
- 0
src/components/Queue/QueueBar.backup.vue View File

@@ -0,0 +1,170 @@
<template>
<div id="queuebar" class="layout__jobbar">
<section id="queuebar-running">
<div class="section__header">
<div class="section__title">
<span class="icon"><font-awesome-icon icon="running" /></span>
<span>Queue</span>
</div>

<div v-show="$nudify.waiting.length > 0" class="section__actions">
<button
v-tooltip="{placement: 'bottom', content: 'Forget waiting'}"
class="button button--danger button--xs"
@click.prevent="$nudify.forgetAll('waiting')">
<font-awesome-icon icon="trash-alt" />
</button>

<button
v-tooltip="{placement: 'bottom', content: 'Cancel waiting' }"
class="button button--xs"
@click.prevent="$nudify.cancelAll('waiting')">
<font-awesome-icon icon="stop" />
</button>
</div>
</div>

<div class="jobs__list">
<QueuePhoto
v-for="(photo, index) of $nudify.waiting"
:key="index"
:photo="photo"
data-private />
</div>
</section>

<section id="queuebar-pending">
<div class="section__header">
<div class="section__title">
<span class="icon"><font-awesome-icon icon="clipboard-list" /></span>
<span>Pending</span>
</div>

<div v-show="$nudify.pending.length > 0" class="section__actions">
<button
v-tooltip="'Forget all'"
class="button button--danger button--xs"
@click.prevent="$nudify.forgetAll()">
<font-awesome-icon icon="trash-alt" />
</button>

<button
v-tooltip="'Start all'"
class="button button--xs"
@click.prevent="$nudify.addAll()">
<font-awesome-icon icon="play" />
</button>
</div>
</div>

<div class="jobs__list">
<QueuePhoto
v-for="(photo, index) of $nudify.pending"
:key="index"
:photo="photo"
data-private />
</div>
</section>

<section id="queuebar-finished">
<div class="section__header">
<div class="section__title">
<span class="icon"><font-awesome-icon icon="clipboard-check" /></span>
<span>Finished</span>
</div>

<div v-show="$nudify.finished.length > 0" class="section__actions">
<button
v-tooltip="'Forget all'"
class="button button--danger button--xs"
@click.prevent="$nudify.forgetAll('finished')">
<font-awesome-icon icon="trash-alt" />
</button>

<button
v-tooltip="'Rerun all'"
class="button button--xs"
@click.prevent="$nudify.addAll('finished')">
<font-awesome-icon icon="undo" />
</button>
</div>
</div>

<div class="jobs__list">
<QueuePhoto
v-for="(photo, index) of $nudify.finished"
:key="index"
:photo="photo"
data-private />
</div>
</section>
</div>
</template>

<script>
export default {

}
</script>

<style lang="scss" scoped>
.layout__jobbar {
@apply relative flex flex-col;
@apply bg-dark-500 py-2 z-10;
@apply border-l border-dark-100;
}

section {
@apply flex-1 flex flex-col;
@apply overflow-hidden;
height: calc((100vh - 80px) / 3);

&:not(:first-child) {
.section__header {
&::before, &::after {
@apply block border-b;
@apply absolute right-0 pointer-events-none z-0;
content: " ";
left: 100px;
}

&::before {
@apply border-dark-200;
top: 18px;
}

&::after {
@apply border-dark-400;
top: 19px;
}
}
}

.section__header {
@apply px-4 pt-2 text-sm text-white font-semibold;
@apply relative flex items-center;

.icon {
@apply mr-2;
}

.section__title {
@apply flex-1 z-10;
}
}

.section__actions {
@apply flex flex-1 justify-end ml-2 z-10 bg-dark-500;

.button {
@apply ml-2;
}
}
}

.jobs__list {
@apply flex flex-wrap flex-1;
@apply py-2 overflow-y-auto max-h-full;
will-change: scroll-position;
}
</style>

+ 72
- 67
src/components/Queue/QueueBar.vue View File

@@ -1,103 +1,62 @@
<template>
<div id="queuebar" class="layout__jobbar">
<section id="queuebar-running">
<div class="section__header">
<div class="section__title">
<div id="queuebar"
class="queue">
<div class="queue__section queue__section--running">
<div class="queue__header">
<p class="title">
<span class="icon"><font-awesome-icon icon="running" /></span>
<span>Queue</span>
</div>

<div v-show="$nudify.waiting.length > 0" class="section__actions">
<button
v-tooltip="{placement: 'bottom', content: 'Forget waiting'}"
class="button button--danger button--xs"
@click.prevent="$nudify.forgetAll('waiting')">
<font-awesome-icon icon="trash-alt" />
</button>

<button
v-tooltip="{placement: 'bottom', content: 'Cancel waiting' }"
class="button button--xs"
@click.prevent="$nudify.cancelAll('waiting')">
<font-awesome-icon icon="stop" />
</button>
</div>
<span class="separator">ยท</span>
<span>{{ $nudify.waiting.length }}</span>
</p>
</div>

<div class="jobs__list">
<div class="queue__content">
<QueuePhoto
v-for="(photo, index) of $nudify.waiting"
:key="index"
:photo="photo"
data-private />
</div>
</section>
</div>

<section id="queuebar-pending">
<div class="section__header">
<div class="section__title">
<div class="queue__section queue__section--pending">
<div class="queue__header">
<p class="title">
<span class="icon"><font-awesome-icon icon="clipboard-list" /></span>
<span>Pending</span>
</div>

<div v-show="$nudify.pending.length > 0" class="section__actions">
<button
v-tooltip="'Forget all'"
class="button button--danger button--xs"
@click.prevent="$nudify.forgetAll()">
<font-awesome-icon icon="trash-alt" />
</button>

<button
v-tooltip="'Start all'"
class="button button--xs"
@click.prevent="$nudify.addAll()">
<font-awesome-icon icon="play" />
</button>
</div>
<span class="separator">ยท</span>
<span>{{ $nudify.pending.length }}</span>
</p>
</div>

<div class="jobs__list">
<div class="queue__content">
<QueuePhoto
v-for="(photo, index) of $nudify.pending"
:key="index"
:photo="photo"
data-private />
</div>
</section>
</div>

<section id="queuebar-finished">
<div class="section__header">
<div class="section__title">
<div class="queue__section queue__section--finished">
<div class="queue__header">
<p class="title">
<span class="icon"><font-awesome-icon icon="clipboard-check" /></span>
<span>Finished</span>
</div>

<div v-show="$nudify.finished.length > 0" class="section__actions">
<button
v-tooltip="'Forget all'"
class="button button--danger button--xs"
@click.prevent="$nudify.forgetAll('finished')">
<font-awesome-icon icon="trash-alt" />
</button>

<button
v-tooltip="'Rerun all'"
class="button button--xs"
@click.prevent="$nudify.addAll('finished')">
<font-awesome-icon icon="undo" />
</button>
</div>
<span class="separator">ยท</span>
<span>{{ $nudify.finished.length }}</span>
</p>
</div>

<div class="jobs__list">
<div class="queue__content">
<QueuePhoto
v-for="(photo, index) of $nudify.finished"
:key="index"
:photo="photo"
data-private />
</div>
</section>
</div>
</div>
</template>

@@ -108,6 +67,51 @@ export default {
</script>

<style lang="scss" scoped>
.queue {
@apply flex flex-col;
@apply bg-dark-500;
grid-area: queue;
}

.queue__section {
@apply flex-1 flex flex-col;

&.queue__section--running {
}

&.queue__section--pending {
}

&.queue__section--finished {
}
}

.queue__header {
@apply p-3;

.title {
@apply text-sm font-bold;
}

.icon {
@apply mr-2;
}

.separator {
@apply mx-2;
}
}

.queue__content {
@apply flex-1;
@apply overflow-hidden overflow-x-auto whitespace-no-wrap;

.photo {
@apply inline-block;
}
}

/*
.layout__jobbar {
@apply relative flex flex-col;
@apply bg-dark-500 py-2 z-10;
@@ -167,4 +171,5 @@ section {
@apply py-2 overflow-y-auto max-h-full;
will-change: scroll-position;
}
*/
</style>

+ 1
- 2
src/components/Queue/QueuePhoto.vue View File

@@ -84,9 +84,8 @@ export default {

<style lang="scss" scoped>
.photo {
@apply w-1/2 relative border-2 border-dark-300;
@apply w-1/2 h-full relative border-2 border-dark-800;
background-image: url("~@/assets/images/curls.png");
height: 150px;
will-change: transform;

&.photo--running {

+ 1
- 1
src/components/Queue/index.js View File

@@ -11,5 +11,5 @@ import Vue from 'vue'
import QueueBar from './QueueBar'
import QueuePhoto from './QueuePhoto'

Vue.component('QueueBar', QueueBar)
Vue.component('Queuebar', QueueBar)
Vue.component('QueuePhoto', QueuePhoto)

+ 159
- 0
src/components/UI/MenuItem.vue View File

@@ -0,0 +1,159 @@
<template>
<div class="item"
:class="cssClass"
@click="click">
<!-- Icon -->
<slot name="icon">
<div v-if="icon"
class="item__icon">
<img v-if="isImageIcon"
:src="icon">
<font-awesome-icon v-else
:icon="icon" />
</div>
</slot>

<!-- Title & Description -->
<div v-if="label"
class="item__label">
<span class="item__title"
v-html="label" />

<slot name="description">
<span v-if="description"
class="item__description"
v-html="description" />
</slot>
</div>

<!-- Action -->
<div v-if="$slots.default"
class="item__action">
<slot />
</div>
</div>
</template>

<script>
import { isNil, startsWith } from 'lodash'
import { dreamtrack } from '~/modules/services'

const { shell } = $provider.api

export default {
props: {
icon: {
type: [String, Array],
default: undefined,
},

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

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

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

isLink: {
type: Boolean,
default: false,
},
},

computed: {
hasIcon() {
return !isNil(this.icon) || !isNil(this.$slots.icon)
},

isImageIcon() {
return startsWith(this.icon, 'http') || startsWith(this.icon, '/')
},

cssClass() {
return {
'item--link': !isNil(this.href) || this.isLink,
}
},
},

methods: {
click() {
this.$emit('click')

if (!isNil(this.href)) {
if (startsWith(this.href, '/')) {
this.$router.push(this.href)
} else {
dreamtrack.track('CLICK_LINK', { href: this.href })
shell.openExternal(this.href)
}
}
},
},
}
</script>

<style lang="scss" scoped>
.item {
@apply flex p-3 rounded;
@apply border-b border-t border-transparent;
@include transition('background-color, color, border-color');
min-height: 50px;

&.item--active {
@apply bg-dark-400 border-dark-800 text-white;

.item__icon, .item__title {
@apply text-generic-500;
}
}

&.item--link {
@apply cursor-pointer;

&:hover {
@apply bg-dark-400 border-dark-800 text-white;

.item__icon, .item__title {
@apply text-generic-500;
}
}
}
}

.item__icon {
@apply mr-2 flex items-center justify-center;
@apply text-2xl text-generic-500;
width: 42px;
min-width: 42px;
}

.item__label {
@apply flex-1 flex flex-col justify-center;
@apply text-generic-500;

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

.item__title {
@apply block font-semibold;
}