Initial commit

This commit is contained in:
Oliver 2021-11-29 17:04:26 -06:00
commit 9212949716
27 changed files with 24369 additions and 0 deletions

133
src/App.vue Normal file
View file

@ -0,0 +1,133 @@
<template>
<div class="maximize_size">
<LoginCard v-if="!is_authed" />
<MainView
v-else
:preview_mode="is_preview"
:dev_mode="is_dev"
/>
<Themes
v-if="show_theme_modal"
@close="show_theme_modal = false"
/>
<SiteInfo
v-if="show_site_info"
@close="show_site_info = false"
/>
<div id="info-button">
<button @click.stop="show_site_info = true">
<Icon
type="info"
:size="35"
:inner-size="35"
primary="--icon-primary"
/>
</button>
</div>
<div id="theme-button">
<button @click.stop="show_theme_modal = true">
<Icon
type="palette"
:size="35"
:inner-size="35"
primary="--icon-primary"
/>
</button>
</div>
</div>
</template>
<script>
// Import Misc JS things
import "./js/prototypes.js";
// Import components
import ThemePicker from './components/modals/ThemeModal.vue';
import SiteInfo from './components/modals/SiteInfo.vue';
import MainView from './components/MainView.vue';
import Icon from './components/Icon.vue';
export default {
name: 'App',
components: {
"LoginCard": LoginCard,
"MainView": MainView,
"Themes": ThemePicker,
"Icon": Icon,
"SiteInfo": SiteInfo,
},
data() {return {
show_theme_modal: false,
show_site_info: false,
}},
computed: {
is_authed() {
let params = new URLSearchParams(window.location.hash.slice(1));
if (params.get(`access_token`)) {
if (sessionStorage.getItem(this.storage_key.state) === params.get(`state`)) {
console.info(`State compare success`)
// Modify sessionStorage
sessionStorage.setItem(this.storage_key.token, params.get(`access_token`));
sessionStorage.removeItem(this.storage_key.state);
window.location.hash = ``;
} else {
console.error(`State compare failed`);
};
};
if (sessionStorage.getItem(this.storage_key.token)) {
return true;
};
return false
}
}
}
</script>
<style>
@import "./css/transitions.css";
@import "./css/scrollbar.css";
@import "./css/tooltips.css";
html, body, .maximize_size {
user-select: none !important;
font-family: var(--fonts);
overflow-x: hidden;
height: 100%;
width: 100%;
padding: 0;
margin: 0;
}
body {
background-color: var(--background);
color: var(--background-text);
}
/* Allows for better theming of the anchor text */
a { color: var(--link); }
a:visited { color: var(--visited-link); }
#theme-button {
position: absolute;
display: block;
bottom: 5px;
right: 5px;
}
#theme-button > button {
padding: 5px;
}
#info-button {
position: absolute;
display: block;
bottom: 5px;
left: 5px;
}
#info-button > button {
padding: 5px;
}
</style>

136
src/components/Icon.vue Normal file
View file

@ -0,0 +1,136 @@
<template>
<div id="icon" :style="div_styles">
<span v-if="type === 'palette'" :style="span_styles">
<svg
:width="innerSize"
:height="innerSize"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
:width="innerSize"
:height="innerSize"
fill="none"
rx="0"
ry="0"
/>
<path
class="primary"
:style="primary_styles"
fill-rule="evenodd"
clip-rule="evenodd"
d="M12.2516 20.3664C12.173 20.3884 12.0894 20.4 12 20.4C11.9227 20.4 11.8455 20.3993 11.7686 20.3978C11.7126 20.3993 11.6564 20.4 11.6 20.4C11.2372 20.4 10.8834 20.3691 10.5425 20.3104C5.7105 19.7107 2 16.1737 2 11.9C2 7.2056 6.47715 3.40002 12 3.40002C15.7082 3.40002 18.945 5.11567 20.6717 7.66406C20.6905 7.69181 20.7029 7.72492 20.7093 7.76311C21.2826 8.51263 21.5525 9.44282 21.3862 10.386C21.1499 11.7265 20.0945 12.7409 18.7432 13.1075C18.6959 13.1025 18.6482 13.1 18.6001 13.1C18.3889 13.1 18.1853 13.1491 17.9942 13.2401L17.9745 13.2418L17.9718 13.251C17.175 13.6461 16.6001 14.7723 16.6001 16.1V16.1001C16.6001 16.1342 16.6001 16.1683 16.6009 16.2021L16.5988 16.3132L16.5992 16.3295L16.5986 16.3764L16.5999 16.3784L16.6 16.4C16.6 18.4325 14.7051 20.1109 12.2516 20.3664ZM12.0566 19.1638C11.9755 19.1925 11.8879 19.2078 11.7928 19.2078C7.0471 19.2078 3.19995 15.9378 3.19995 11.9039C3.19995 7.87006 7.0471 4.59998 11.7928 4.59998C14.882 4.59998 17.8404 5.876 19.3701 7.94922C20.0399 8.54236 20.3957 9.32338 20.2583 10.103C20.0591 11.2322 19.4734 11.9526 18.2539 12.0036C17.5102 11.955 16.5185 12.6539 15.9177 13.9423C15.752 14.2978 15.6278 14.6532 15.5447 14.9934C15.5431 15.0019 15.5412 15.0106 15.539 15.0195C15.439 15.4289 15.4192 15.7734 15.4218 15.8945L15.419 15.8969C15.4158 16.046 15.4228 16.1878 15.44 16.3202C15.3393 17.8505 13.8796 19.0768 12.0566 19.1638ZM13.5 7C13.5 7.82843 12.8284 8.5 12 8.5C11.1715 8.5 10.5 7.82843 10.5 7C10.5 6.17157 11.1715 5.5 12 5.5C12.8284 5.5 13.5 6.17157 13.5 7ZM8.99995 8.5C8.99995 9.32843 8.32838 10 7.49995 10C6.67152 10 5.99995 9.32843 5.99995 8.5C5.99995 7.67157 6.67152 7 7.49995 7C8.32838 7 8.99995 7.67157 8.99995 8.5ZM11.5 18C12.8807 18 14 17.1046 14 16C14 14.8954 12.8807 14 11.5 14C10.1192 14 8.99995 14.8954 8.99995 16C8.99995 17.1046 10.1192 18 11.5 18ZM11.5 16.8C12.2179 16.8 12.8 16.4418 12.8 16C12.8 15.5582 12.2179 15.2 11.5 15.2C10.782 15.2 10.2 15.5582 10.2 16C10.2 16.4418 10.782 16.8 11.5 16.8ZM16.5 10C17.3284 10 18 9.32843 18 8.5C18 7.67157 17.3284 7 16.5 7C15.6715 7 15 7.67157 15 8.5C15 9.32843 15.6715 10 16.5 10ZM5.69995 14C6.52838 14 7.19995 13.3284 7.19995 12.5C7.19995 11.6716 6.52838 11 5.69995 11C4.87152 11 4.19995 11.6716 4.19995 12.5C4.19995 13.3284 4.87152 14 5.69995 14Z"
/>
</svg>
</span>
<span v-else-if="type === 'info'" :style="span_styles">
<svg
:width="innerSize"
:height="innerSize"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
:width="innerSize"
:height="innerSize"
fill="none"
rx="0"
ry="0"
/>
<path
class="primary"
:style="primary_styles"
fill-rule="evenodd"
clip-rule="evenodd"
d="M2 12C2 6.49 6.49 2 12 2C17.51 2 22 6.49 22 12C22 17.51 17.51 22 12 22C6.49 22 2 17.51 2 12ZM4 12C4 16.41 7.59 20 12 20C16.41 20 20 16.41 20 12C20 7.59 16.41 4 12 4C7.59 4 4 7.59 4 12ZM12 9C12.5523 9 13 8.55228 13 8C13 7.44772 12.5523 7 12 7C11.4477 7 11 7.44772 11 8C11 8.55228 11.4477 9 12 9ZM12 10C11.45 10 11 10.45 11 11V16C11 16.55 11.45 17 12 17C12.55 17 13 16.55 13 16V11C13 10.45 12.55 10 12 10Z"
/>
</svg>
</span>
</div>
</template>
<script>
export default {
name: `Icon`,
props: {
type: {
type: String,
required: true,
},
primary: {
type: String,
default: `--icon-primary`,
required: false,
},
secondary: {
type: String,
default: `--icon-secondary`,
required: false,
},
size: {
type: Number,
default: 25,
required: false,
},
innerSize: {
type: Number,
default: null,
required: false,
},
background: {
type: String,
default: null,
required: false,
},
border: {
type: Number,
default: 0,
required: false,
}
},
data() { return {
div_styles: {
"background-color": this.background ? `var(${this.background})` : null,
"border-radius": `${this.border}px`,
"width": `${this.size}px`,
"height": `${this.size}px`,
},
}},
computed: {
span_styles() {
if (this.innerSize) {
return {
"width": `${this.innerSize}px`,
"height": `${this.innerSize}px`,
}
}
let x = Math.floor(this.size * 0.6);
return {
width: `${x}px`,
height: `${x}px`,
}
},
primary_styles() {
return {
"fill": `var(${this.primary})`,
};
},
secondary_styles() {
return {
"fill": `var(${this.secondary})`
}
}
}
}
</script>
<style>
#icon {
justify-content: center;
vertical-align: middle;
display: inline-flex;
align-items: center;
}
</style>

View file

@ -0,0 +1,107 @@
<template>
<div id="login_screen" class="maximize_size">
<div class="card">
<div
v-if="error"
class="alert error"
>
{{ error }}
</div>
<a :href="auth_url">
<button>Login</button>
</a>
</div>
</div>
</template>
<script>
export default {
name: 'LoginView',
data() { return {
auth_base: ``,
use_state: true,
client_id: ``,
scopes: [],
show_dialog: process.env.NODE_ENV !== `production`,
}},
computed: {
auth_url() {
let params = [
`client_id=${this.client_id}`,
`response_type=token`,
`redirect_uri=${encodeURIComponent(this.auth_redirect)}`,
`scope=${encodeURIComponent(this.scopes.join(" "))}`,
`show_dialog=${this.show_dialog}`
];
// Create the state data if we are using it
if (this.use_state) {
let state = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
params.push(`state=${state}`);
sessionStorage.setItem(this.storage_key.state, state);
};
return `${this.auth_base}?${params.join("&")}`;
},
error() {
let error_message = (new URLSearchParams(window.location.search.slice(1))).get(`error`);
return error_message ? error_message : ``;
},
}
}
</script>
<style scoped>
#login_screen {
font-family: var(--fonts);
justify-content: center;
align-items: center;
display: flex;
height: 100%;
width: 100%;
}
.card {
border-radius: var(--corner-rounding);
background-color: var(--card-colour);
color: var(--card-text);
text-align: center;
padding: 15px;
width: 90%;
}
button {
background-color: var(--button-background);
color: var(--button-text);
font-family: var(--fonts);
border-radius: 50px;
padding: 10px 20px;
border-style: none;
font-size: larger;
outline: none;
}
button:hover { cursor: pointer; }
.alert {
margin-bottom: 10px;
padding: 5px;
}
.error {
background-color: var(--error-background);
border-radius: var(--corner-rounding);
border-color: var(--error);
color: var(--error-text);
border-style: solid;
border-width: 2px;
}
@media only screen and (min-width: 768px) {
.card {
padding: 30px;
width: 33%;
}
}
</style>

View file

@ -0,0 +1,22 @@
<template>
<div id="main_screen">
</div>
</template>
<script>
import * as axios from "axios";
export default {
name: `MainView`,
props: {},
components: {},
data() { return {}},
computed: {},
methods: {},
mounted() {},
}
</script>
<style scoped>
</style>

View file

@ -0,0 +1,63 @@
<template id="info-modal">
<transition name="fade" @after-enter="content = true">
<div
v-if="container"
class="modal-container"
@click.self.stop="content = false"
>
<transition name="burst" @after-leave="$emit('close')">
<div v-if="content" class="modal">
<p></p>
</div>
</transition>
</div>
</transition>
</template>
<script>
export default {
name: `SiteInfoModal`,
data() {return {
container: false,
content: false,
}},
mounted() {
this.$nextTick(function() {
this.container = true;
});
},
}
</script>
<style scoped>
.modal-container {
background-color: var(--modal-container-background);
justify-content: center;
align-items: center;
position: fixed;
display: flex;
height: 100vh;
width: 100vw;
z-index: 10;
left: 0;
top: 0;
}
.modal {
background-color: var(--modal-background);
border-radius: var(--corner-rounding);
color: var(--modal-text);
text-align: center;
max-height: 85%;
padding: 0 15px;
z-index: 11;
width: 90%;
}
@media only screen and (min-width: 768px) {
.modal {
width: 50%;
max-height: 75%;
}
}
</style>

View file

@ -0,0 +1,127 @@
<template id="theme-modal">
<transition name="fade" @after-enter="content = true">
<div
v-if="container"
class="modal-container"
@click.self.stop="content = false"
>
<transition name="burst" @after-leave="$emit('close')">
<div v-if="content" class="modal">
<h2 class="center">Available Themes</h2>
<div
v-for="theme of available_themes"
:key="theme.filename"
class="theme-card"
@click.stop="chosen_theme = theme.filename"
>
<h3>
<input
:id="'select_theme' + theme.filename"
v-model="chosen_theme"
type="radio"
:value="theme.filename"
>
<label :for="'select_theme' + theme.filename">{{ theme.name }}</label>
</h3>
<p>
{{ theme.description }}
</p>
</div>
</div>
</transition>
</div>
</transition>
</template>
<script>
export default {
name: `ThemesListModal`,
data() {return {
container: false,
content: false,
chosen_theme: `dark`,
default_theme: `dark`,
style_id: `colour-transition`,
themes: [
{
name: `Dark`,
filename: `dark`,
description: `The default theme of the website, this uses darker background colours with lighter coloured accents.`,
show() { return true },
},
],
}},
mounted() {
this.chosen_theme = localStorage.getItem(`tl-theme`) || this.default_theme;
// Add the CSS to the document so that we can transition nicely
let colour_transition = document.createElement(`style`);
colour_transition.id = this.style_id;
colour_transition.innerText=`/* Transition colours for theme changes */
* {
transition: color .5s;
transition: background-color .5s;
}`;
document.body.appendChild(colour_transition);
this.$nextTick(function() {
this.container = true;
});
},
computed: {
available_themes() {
let themes = [];
for (var theme of this.themes) {
if (theme.show()) {
themes.push(theme);
};
};
return themes;
},
},
watch: {
chosen_theme(val) {
localStorage.setItem(`tl-theme`, val);
document.getElementById(`theme`).href = `static/css/theme/${val}.css`;
}
},
beforeDestroy() {
document.body.removeChild(document.getElementById(this.style_id));
},
}
</script>
<style scoped>
.modal-container {
background-color: var(--modal-container-background);
justify-content: center;
align-items: center;
position: fixed;
display: flex;
height: 100vh;
width: 100vw;
z-index: 10;
left: 0;
top: 0;
}
.modal {
background-color: var(--modal-background);
border-radius: var(--corner-rounding);
color: var(--modal-text);
text-align: center;
max-height: 85%;
padding: 0 15px;
z-index: 11;
width: 90%;
}
@media only screen and (min-width: 768px) {
.modal {
width: 50%;
max-height: 75%;
}
}
</style>

27
src/css/scrollbar.css Normal file
View file

@ -0,0 +1,27 @@
/* width */
::-webkit-scrollbar {
width: 10px;
}
/* Track */
::-webkit-scrollbar-track {
background: var(--scrollbar-background);
border-radius: 5px;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: var(--scrollbar-handle);
border-radius: 5px;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-handle-hover);
}
* {
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-handle) var(--scrollbar-background);
}

105
src/css/tooltips.css Normal file
View file

@ -0,0 +1,105 @@
.tooltip {
display: block !important;
z-index: 10000;
}
.tooltip .tooltip-inner {
background: var(--tooltip-colour);
color: var(--tooltip-text);
border-radius: 16px;
padding: 5px 10px 4px;
}
.tooltip .tooltip-arrow {
width: 0;
height: 0;
border-style: solid;
position: absolute;
margin: 5px;
border-color: var(--tooltip-colour);
z-index: 1;
}
.tooltip[x-placement^="top"] {
margin-bottom: 5px;
}
.tooltip[x-placement^="top"] .tooltip-arrow {
border-width: 5px 5px 0 5px;
border-left-color: transparent !important;
border-right-color: transparent !important;
border-bottom-color: transparent !important;
bottom: -5px;
left: calc(50% - 5px);
margin-top: 0;
margin-bottom: 0;
}
.tooltip[x-placement^="bottom"] {
margin-top: 5px;
}
.tooltip[x-placement^="bottom"] .tooltip-arrow {
border-width: 0 5px 5px 5px;
border-left-color: transparent !important;
border-right-color: transparent !important;
border-top-color: transparent !important;
top: -5px;
left: calc(50% - 5px);
margin-top: 0;
margin-bottom: 0;
}
.tooltip[x-placement^="right"] {
margin-left: 5px;
}
.tooltip[x-placement^="right"] .tooltip-arrow {
border-width: 5px 5px 5px 0;
border-left-color: transparent !important;
border-top-color: transparent !important;
border-bottom-color: transparent !important;
left: -5px;
top: calc(50% - 5px);
margin-left: 0;
margin-right: 0;
}
.tooltip[x-placement^="left"] {
margin-right: 5px;
}
.tooltip[x-placement^="left"] .tooltip-arrow {
border-width: 5px 0 5px 5px;
border-top-color: transparent !important;
border-right-color: transparent !important;
border-bottom-color: transparent !important;
right: -5px;
top: calc(50% - 5px);
margin-left: 0;
margin-right: 0;
}
.tooltip.popover .popover-inner {
background: var(--tooltip-colour);
color: var(--tooltip-text);
padding: 24px;
border-radius: 5px;
box-shadow: 0 5px 30px rgba(black, .1);
}
.tooltip.popover .popover-arrow {
border-color: var(--tooltip-colour);
}
.tooltip[aria-hidden='true'] {
visibility: hidden;
opacity: 0;
transition: opacity .15s, visibility .15s;
}
.tooltip[aria-hidden='false'] {
visibility: visible;
opacity: 1;
transition: opacity .15s;
}

62
src/css/transitions.css Normal file
View file

@ -0,0 +1,62 @@
/* Transition for modal background appearing/disappearing */
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
/* Transition for modal card appearing disappearing */
.burst-enter-active {
animation: burst-in .5s;
}
.burst-leave-active { animation: burst-out .5s; }
@keyframes burst-in {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
@keyframes burst-out {
0% {
transform: scale(1);
}
100% {
transform: scale(0);
}
}
@media only screen and (min-width: 768px) {
@keyframes burst-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.25);
}
100% {
transform: scale(1);
}
}
@keyframes burst-out {
0% {
transform: scale(1);
}
50% {
transform: scale(1.25);
}
100% {
transform: scale(0);
}
}
}

13
src/js/constants.js Normal file
View file

@ -0,0 +1,13 @@
const DEV_URL = `http://localhost:8080`;
const PROD_URL = ``;
export const HOME_PAGE = process.env.NODE_ENV === `production`
? PROD_URL
: DEV_URL
export const AUTH_REDIRECT = `${HOME_PAGE}`;
export const STORAGE_KEYS = {
token: `auth-token`,
state: `auth-state`
};

10
src/js/prototypes.js vendored Normal file
View file

@ -0,0 +1,10 @@
String.prototype.toTitleCase = function () {
let words = this.split(` `);
let new_words = [];
for (var word of words) {
new_words.push(
`${word[0].toUpperCase()}${word.slice(1).toLowerCase()}`
);
};
return new_words.join(` `);
}

56
src/main.js Normal file
View file

@ -0,0 +1,56 @@
import Vue from 'vue';
import VTooltip from 'v-tooltip';
import * as clipboard from 'clipboard-polyfill/text';
import TextareaAutosize from 'vue-textarea-autosize';
import VueEllipseProgress from 'vue-ellipse-progress';
import App from './App.vue';
import {
AUTH_REDIRECT,
SPOTIFY_API_BASE,
STORAGE_KEYS,
HOME_PAGE
} from './js/constants';
Vue.config.productionTip = false;
VTooltip.enabled = window.innerWidth > 768
// Third-party plugins
Vue.use(VTooltip);
Vue.use(TextareaAutosize);
Vue.use(VueEllipseProgress, `percent`);
// global mixings
Vue.mixin({
data() {return {
api_url: SPOTIFY_API_BASE,
auth_redirect: AUTH_REDIRECT,
storage_key: STORAGE_KEYS,
home_page: HOME_PAGE,
}},
computed: {
api_token() {
return sessionStorage.getItem(this.storage_key.token);
},
},
methods: {
css_var(var_name) {
return getComputedStyle(document.documentElement).getPropertyValue(var_name);
},
auth_expired(error = null) {
sessionStorage.removeItem(this.storage_key.token);
window.location.hash = ``;
if (error) {
window.location.href = `${this.home_page}?error=${error}`;
} else {
window.location.href = this.home_page;
};
},
copy_text: clipboard.writeText,
},
});
// eslint-disable-next-line
const app = new Vue({
render: h => h(App),
}).$mount('#app')