Browse Source

Lots of fixes

master
Benjamin Bädorf 4 years ago
parent
commit
1d365d5ac9
  1. 2
      .eslintrc.js
  2. 2
      config/index.js
  3. 3
      src/App.vue
  4. 4
      src/assets/scss/components/_app.scss
  5. 114
      src/assets/scss/components/_toaster.scss
  6. 4
      src/assets/scss/global.scss
  7. 21
      src/broadcast.js
  8. 88
      src/components/Toaster.vue
  9. 1
      src/components/ap/config/Index.vue
  10. 89
      src/components/ap/tracks/TrackForm.vue
  11. 21
      src/components/ap/tracks/TrackView.vue
  12. 29
      src/components/ap/users/UserForm.vue
  13. 2
      src/components/ap/users/UserView.vue
  14. 15
      src/components/player/Player.vue
  15. 1
      src/components/player/YtWrap.vue

2
.eslintrc.js

@ -37,6 +37,8 @@ module.exports = {
// allow dangling underscores on objects
'no-underscore-dangle': 0,
'no-param-reassign': 0,
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
}

2
config/index.js

@ -33,7 +33,7 @@ module.exports = {
assetsPublicPath: '/',
proxyTable: {
'/api': {
target: 'http://dev1.local:8008/',
target: 'http://dev1.local:8080/',
changeOrigin: true,
/*pathRewrite: {
'^/api': ''

3
src/App.vue

@ -1,5 +1,6 @@
<script>
import LnbNav from './components/LnbNav';
import Toaster from './components/Toaster';
import { state } from './app';
function getVariableCss(name, c) {
@ -16,6 +17,7 @@ export default {
name: 'app',
components: {
LnbNav,
Toaster,
},
computed: {
isTall() {
@ -50,6 +52,7 @@ export default {
<template>
<main class="app" :class="{ 'app_tall': isTall }">
<toaster></toaster>
<lnb-nav></lnb-nav>
<transition enter-active-class="animated fadeInDown" leave-active-class="animated fadeOutDown">
<router-view></router-view>

4
src/assets/scss/components/_app.scss

@ -1,3 +1,5 @@
$app-padding: 50px;
.app {
margin: auto;
max-width: 100%;
@ -9,7 +11,7 @@
border: 16px solid;
border-color: inherit;
transition: width 0.2s ease-out, height 0.2s ease-out;
padding: 50px;
padding: $app-padding;
overflow: hidden;
> * {

114
src/assets/scss/components/_toaster.scss

@ -0,0 +1,114 @@
$toaster-padding: 15px;
.toaster {
transition: all .1s ease-in;
display: flex;
padding: 10px $toaster-padding;
margin-bottom: 10px;
&:hover svg .outer {
display: none;
}
&_error {
background-color: var(--error);
}
&_success {
background-color: var(--success);
}
&__wrapper {
display: flex;
position: absolute;
flex-direction: column;
z-index: 1500;
top: 15px;
margin-left: -1 * $toaster-padding;
width: calc(100% - (2 * (#{$app-padding} - #{$toaster-padding})));
max-height: 100%;
}
&__message {
flex-grow: 1;
display: flex;
flex-direction: column;
align-content: center;
justify-content: center;
h4 {
margin: 0px;
}
}
&__type-icon {
font-size: 25px;
margin-right: 10px;
}
&__close {
width: 30px;
height: 30px;
align-self: flex-start;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
border-radius: 15px;
color: var(--secondary-8);
font-size: 29px;
position: relative;
&:hover {
color: var(--secondary);
border-bottom-color: transparent;
transition: color 0.1s ease-out;
}
&_running {
svg .outer {
animation-play-state: running;
animation-name: fill-circle;
animation-duration: 3s;
animation-delay: 0s;
animation-iteration-count: 1;
}
}
> span {
width: 100%;
}
svg {
position: absolute;
margin-top: -1px;
.outer {
fill: transparent;
stroke: var(--secondary-8);
stroke-width: 3;
stroke-dasharray: 100;
stroke-dashoffset: 100;
}
}
}
}
/* List animations */
.toaster-list-enter,
.toaster-list-leave-to {
opacity: 0;
transform: scale(95%, 90%);
}
/* Keyframes for the initial animation */
@keyframes fill-circle {
from {
stroke-dashoffset: 0;
}
to {
stroke-dashoffset: 100;
}
}

4
src/assets/scss/global.scss

@ -29,7 +29,8 @@ body {
@include colorVar(primary, #282c34);
@include colorVar(secondary, white);
@include colorVar(accent, black);
--error: #c52424;
--success: #249b17;
--color-transition: background-color 1s ease-out, color 1s ease-out;
}
@ -114,6 +115,7 @@ p a {
@import './components/player';
@import './components/yt-wrap';
@import './components/app';
@import './components/toaster';
@import './components/fb-connect';
@import './components/config';
@import './components/login';

21
src/broadcast.js

@ -0,0 +1,21 @@
const Broadcaster = {
listeners: {},
on(name, callback) {
if (!Array.isArray(this.listeners[name])) {
this.listeners[name] = [];
}
return (this.listeners[name].push(callback) - 1);
},
emit(name, ...args) {
if (Array.isArray(this.listeners[name])) {
for (let i = 0; i < this.listeners[name].length; i += 1) {
const cb = this.listeners[name][i];
if (cb && typeof cb === 'function') {
cb(...args);
}
}
}
},
};
export default Broadcaster;

88
src/components/Toaster.vue

@ -0,0 +1,88 @@
<script>
import Broadcaster from '../broadcast';
export default {
data: () => ({
iconMap: {
success: 'iconicstroke-check',
error: 'iconicstroke-x-alt',
},
messages: [],
}),
methods: {
startTimeout(message) {
this.$set(
message,
'timeout',
setTimeout(() => {
this.messages = this.messages.filter(m => m.id !== message.id);
}, 3000),
);
},
stopTimeout(message) {
clearTimeout(message.timeout);
this.$set(message, 'timeout', null);
},
toasterClass(message) {
const res = {};
res[`toaster_${message.type}`] = true;
return res;
},
closeButtonClass(message) {
const res = {
toaster__close_running: !!message.timeout,
};
return res;
},
deleteMessage(index) {
this.stopTimeout(this.messages[index]);
this.$delete(this.messages, index);
},
},
mounted() {
Broadcaster.on('toaster', (message) => {
const id = (+(Date.now())).toString();
message.id = id;
this.messages.push(message);
this.startTimeout(message);
if (this.messages.length > 3) {
for (let i = this.messages.length - 4; i >= 0; i -= 1) {
this.messages.shift();
}
}
});
},
};
</script>
<template>
<transition-group
name="toaster-list"
class="toaster__wrapper"
tag="div"
>
<div
class="toaster"
:class="toasterClass(message)"
v-for="(message, index) in messages"
:key="message.id"
@mouseenter="stopTimeout(message)"
@mouseleave="startTimeout(message)"
>
<div class="toaster__message">
<h4>{{ message.title }}</h4>
<p v-if="message.text">{{ message.text }}</p>
</div>
<button
@click="deleteMessage(index)"
class="toaster__close"
:class="closeButtonClass(message)"
>
<svg width="40" height="40">
<circle class="outer" cx="20" cy="20" r="16" transform="rotate(-90, 20, 20)"/>
</svg>
<span :class="iconMap[message.type]"></span>
</button>
</div>
</transition-group>
</template>

1
src/components/ap/config/Index.vue

@ -108,6 +108,7 @@
<h3>
SEO config
<button type="button" v-if="!editing" @click="editing = true">Edit</button>
<button type="button" v-if="editing" @click="editing = false">Cancel</button>
</h3>
<div class="config__field">

89
src/components/ap/tracks/TrackForm.vue

@ -2,8 +2,10 @@
import { getIdFromURL, getTimeFromURL } from 'vue-youtube-embed';
import { Chrome } from 'vue-color';
import { state, addTrack, updateTrack } from '../../../app';
import Broadcaster from '../../../broadcast';
import { stringToSeconds, secondsToString, debounce, defaultColors } from '../../../utility';
import UserName from '../../UserName';
import YtWrap from '../../player/YtWrap';
export default {
props: {
@ -23,8 +25,10 @@ export default {
components: {
UserName,
'chrome-picker': Chrome,
YtWrap,
},
data: () => ({
ytTimeWidth: 414,
uTrack: {
title: '',
artist: '',
@ -34,7 +38,6 @@ export default {
end: '',
},
maxTime: 0,
error: '',
showYouTube: false,
showColors: false,
youTubeLoaded: false,
@ -46,6 +49,18 @@ export default {
},
}),
computed: {
trackForUpdate() {
return {
id: this.track.id ? this.track.id : null,
title: this.uTrack.title,
artist: this.uTrack.artist,
release: this.uTrack.release,
url: this.url,
start: stringToSeconds(this.uTrack.start),
end: stringToSeconds(this.uTrack.end),
meta: this.hasCustomColors ? JSON.stringify(this.colors) : '{}',
};
},
url() {
return this.uTrack.url;
},
@ -59,17 +74,17 @@ export default {
return getIdFromURL(this.uTrack.url);
},
startStyle() {
const percent = ((stringToSeconds(this.uTrack.start) / this.maxTime) * 100);
const offset = ((stringToSeconds(this.uTrack.start) / this.maxTime) * this.ytTimeWidth);
return {
left: `${percent}%`,
left: `${offset}px`,
marginLeft: '12px',
};
},
endStyle() {
const cutTime = this.maxTime - stringToSeconds(this.uTrack.end);
const percent = ((cutTime / this.maxTime) * 100);
const offset = ((cutTime / this.maxTime) * this.ytTimeWidth);
return {
right: `${percent}%`,
right: `${offset}px`,
marginRight: '12px',
};
},
@ -138,28 +153,37 @@ export default {
return;
}
const track = {
id: this.track.id ? this.track.id : null,
title: this.uTrack.title,
artist: this.uTrack.artist,
release: this.uTrack.release,
url: this.url,
start: stringToSeconds(this.uTrack.start),
end: stringToSeconds(this.uTrack.end),
meta: this.hasCustomColors ? JSON.stringify(this.colors) : '{}',
};
const track = this.trackForUpdate;
if (this.track.id) {
updateTrack(track)
.then(() => this.$emit('saved', track))
.then(() => {
this.$emit('saved', track);
Broadcaster.emit('toaster', {
type: 'success',
title: `${track.title} successfully saved`,
});
})
.catch((err) => {
this.error = err.toString();
Broadcaster.emit('toaster', {
type: 'error',
title: err.toString(),
});
});
} else {
addTrack(track)
.then(() => this.$emit('saved', track))
.then(() => {
this.$emit('saved', track);
Broadcaster.emit('toaster', {
type: 'success',
title: `${track.title} successfully saved`,
});
})
.catch((err) => {
this.error = err.toString();
Broadcaster.emit('toaster', {
type: 'error',
title: err.toString(),
});
});
}
},
@ -177,10 +201,20 @@ export default {
this.colors.secondary = colors.secondary;
},
setStart(ev) {
this.uTrack.start = ev.target.value;
const value = ev.target.value;
const time = stringToSeconds(value);
if (time < 0 || time >= stringToSeconds(this.uTrack.end)) {
return;
}
this.uTrack.start = value;
},
setEnd(ev) {
this.uTrack.end = ev.target.value;
const value = ev.target.value;
const time = stringToSeconds(value);
if (time < stringToSeconds(this.uTrack.start) || time >= this.maxTime) {
return;
}
this.uTrack.end = value;
},
},
mounted() {
@ -201,27 +235,27 @@ export default {
<input v-model="uTrack.url" placeholder="URL" :class="{ active: !!uTrack.url }"/>
</div>
<div class="track-form__video">
<youtube
<yt-wrap
v-if="showYouTube"
:video-id="youTubeId"
:track="trackForUpdate"
@ready="onYouTubeReady"
@qued="onYouTubeCued"
:player-width="438"
:player-height="246"
:player-vars="{ start: startSeconds, autoplay: false }"
class="track-form__preview"
></youtube>
></yt-wrap>
<div v-if="youTubeLoaded" class="track-form__timing">
<input
:value="uTrack.start"
@keyup.prevent.enter="setStart"
@keydown.prevent.enter="setStart"
@blur="setStart"
placeholder="00:00"
@wheel.prevent="onTimeScroll('start', $event)"
:style="startStyle" />
<input
:value="uTrack.end"
@keyup.prevent.enter="setEnd"
@keydown.prevent.enter="setEnd"
@blur="setEnd"
:placeholder="maxTimeString"
@wheel.prevent="onTimeScroll('end', $event)"
@ -231,7 +265,7 @@ export default {
<p class="center">
<button type="button" @click="showColors = true" v-if="!showColors">Add custom colors</button>
<button type="button" @click="showColors = false" v-if="showColors">Hide</button>
<button type="button" @click="setDefaultColors">Reset</button>
<button type="button" @click="setDefaultColors" v-if="showColors">Reset</button>
</p>
<div class="track-form__colors" v-if="showColors">
<chrome-picker :value="colors.primary" @input="updateColor('primary', $event)"></chrome-picker>
@ -243,6 +277,5 @@ export default {
<button name="submit" type="submit" v-show="youTubeLoaded">Save</button>
<button type="button" @click.prevent="$emit('cancel')">Cancel</button>
</div>
<p v-if="error" v-html="error"></p>
</form>
</template>

21
src/components/ap/tracks/TrackView.vue

@ -1,6 +1,7 @@
<script>
import { getIdFromURL } from 'vue-youtube-embed';
import { deleteTrack, publishTrack, state } from '../../../app';
import Broadcaster from '../../../broadcast';
import { buildMeta } from '../../../utility';
import UserName from '../../UserName';
import ColoredEpisode from '../../ColoredEpisode';
@ -58,7 +59,12 @@ export default {
}
deleteTrack(this.track.id)
.catch(err => console.error(err));
.catch((err) => {
Broadcaster.emit('toaster', {
type: 'error',
title: err.toString(),
});
});
},
publish() {
if (this.published) {
@ -66,7 +72,18 @@ export default {
}
publishTrack(this.track.id, this.publishMessage)
.catch(err => console.error(err));
.then((track) => {
Broadcaster.emit('toaster', {
type: 'success',
title: `${track.title} published at episode ${track.episode}`,
});
})
.catch((err) => {
Broadcaster.emit('toaster', {
type: 'error',
title: err.toString(),
});
});
},
},
updated() {

29
src/components/ap/users/UserForm.vue

@ -1,5 +1,6 @@
<script>
import { state, addUser, updateUser } from '../../../app';
import Broadcaster from '../../../broadcast';
export default {
props: {
@ -21,7 +22,6 @@ export default {
role: '',
hosts: '',
},
error: '',
}),
watch: {
user() {
@ -46,15 +46,33 @@ export default {
if (this.user.id) {
updateUser(user)
.then(() => this.$emit('saved'))
.then(() => {
this.$emit('saved');
Broadcaster.emit('toaster', {
type: 'success',
title: `User ${user.name} successfully saved`,
});
})
.catch((err) => {
this.error = err.toString();
Broadcaster.emit('toaster', {
type: 'error',
title: err.toString(),
});
});
} else {
addUser(user)
.then(() => this.$emit('saved'))
.then(() => {
this.$emit('saved');
Broadcaster.emit('toaster', {
type: 'success',
title: `User ${user.name} successfully added`,
});
})
.catch((err) => {
this.error = err.toString();
Broadcaster.emit('toaster', {
type: 'error',
title: err.toString(),
});
});
}
},
@ -83,7 +101,6 @@ export default {
</select>
<input v-model="uUser.hosts" :placeholder="currUser.hosts.join(', ')" :class="{ active: uUser.hosts }" />
</div>
<p v-if="error" v-html="error"></p>
<div class="form-actions">
<button type="submit">Save</button>
<button @click.prevent="$emit('cancel')">Cancel</button>

2
src/components/ap/users/UserView.vue

@ -1,6 +1,6 @@
<script>
import { state } from '../../../app';
import UserForm from './UserForm';
import UserForm from './UserForm';
export default {
props: {

15
src/components/player/Player.vue

@ -1,6 +1,7 @@
<script>
import marked from 'marked';
import { state } from '../../app';
import Broadcaster from '../../broadcast';
import { shuffle, secondsToString, toggleFullscreen } from '../../utility';
import VolumeBar from './VolumeBar';
import TrackList from './TrackList';
@ -86,6 +87,15 @@ export default {
this.shuffle = !this.shuffle;
}
},
error() {
Broadcaster.emit('toaster', {
type: 'error',
title: `Couldn't play Ep. ${this.currentTrack.episode}: ${this.currentTrack.title} by ${this.currentTrack.artist}`,
});
this.next();
},
},
subscriptions() {
state.tracks
@ -179,6 +189,9 @@ export default {
totalTimeString() {
return secondsToString(this.duration);
},
currentTrack() {
return this.playlist[this.index];
},
},
beforeDestroy() {
document.removeEventListener('keydown', this.onKeyDown);
@ -224,7 +237,7 @@ export default {
@ping="currentTime = $event"
@paused="status = 'paused'"
@playing="playing"
@error="next"
@error="error"
@ended="next"
class="player__yt-wrap"
></yt-wrap>

1
src/components/player/YtWrap.vue

@ -47,7 +47,6 @@ export default {
this.unsetTimer();
this.$emit('playing', ev);
state.theme.next(JSON.parse(this.track.meta));
if (this.track.end === 0) {
this.track.end = Math.floor(this.player.getDuration());

Loading…
Cancel
Save