Commit e501f626 authored by SpinShare's avatar SpinShare

vue rewrite part 1

parent e7ad0f0a
> 1%
last 2 versions
not dead
......@@ -19,3 +19,6 @@ yarn-error.log*
*.njsproj
*.sln
*.sw?
#Electron-builder output
/dist_electron
\ No newline at end of file
# spinshare-client
# client
## Project setup
```
......
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "client",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"electron:build": "vue-cli-service electron:build",
"electron:serve": "vue-cli-service electron:serve",
"postinstall": "electron-builder install-app-deps",
"postuninstall": "electron-builder install-app-deps"
},
"main": "background.js",
"dependencies": {
"axios": "^0.19.2",
"core-js": "^3.6.4",
"electron-dl": "^3.0.0",
"glob": "^7.1.6",
"ncp": "^2.0.0",
"rimraf": "^3.0.2",
"uniqid": "^5.2.0",
"unzipper": "^0.10.11",
"vue": "^2.6.11",
"vue-axios": "^2.1.5",
"vue-router": "^3.1.6",
"vuex": "^3.1.3"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.3.0",
"@vue/cli-plugin-router": "~4.3.0",
"@vue/cli-plugin-vuex": "~4.3.0",
"@vue/cli-service": "~4.3.0",
"electron": "^6.0.0",
"less": "^3.0.4",
"less-loader": "^5.0.0",
"vue-cli-plugin-electron-builder": "~1.4.6",
"vue-template-compiler": "^2.6.11"
}
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
<!-- Styles -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&display=swap" />
<link rel="stylesheet" href="https://cdn.materialdesignicons.com/5.0.45/css/materialdesignicons.min.css" />
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
<template>
<div id="app">
<main>
<Navigation />
<router-view />
</main>
<ContextMenu>
</ContextMenu>
</div>
</template>
<script>
import Navigation from '@/components/Navigation/Navigation.vue';
import ContextMenu from '@/components/ContextMenu/ContextMenu.vue';
export default {
name: 'App',
components: {
Navigation,
ContextMenu
},
data: function() {
return {
downloadQueue: []
}
},
mounted: function() {
this.$root.$on('download', (url) => {
this.addToQueue(url);
});
},
methods: {
addToQueue: function(url) {
this.$data.downloadQueue.push(url);
console.log("Added " + url + " to queue");
}
},
watch: {
downloadQueue: function(queue) {
console.log("Downloading first item");
console.log(this.$data.downloadQueue);
}
}
}
</script>
<style lang="less">
html {
box-sizing: border-box;
-webkit-user-select: none;
}
*, *:before, *:after {
box-sizing: inherit;
}
html {
width: 100%;
height: 100%;
}
body {
width: 100%;
height: 100%;
margin: 0;
background: #212629;
color: #fff;
overflow-x: hidden;
font-family: 'Open Sans', sans-serif;
font-size: 14px;
}
::-webkit-scrollbar {
background: #212529;
width: 5px;
}
::-webkit-scrollbar-thumb {
background: #fff;
}
main {
display: grid;
margin-left: 60px;
min-height: 100vh;
overflow-y: scroll;
}
section {
&.section-library {
padding: 50px;
}
}
button, .button {
font-family: 'Open Sans', sans-serif;
font-size: 12px;
color: #fff;
background: rgba(255,255,255,0.2);
text-transform: uppercase;
font-weight: 700;
border-radius: 4px;
padding: 7px 14px;
border: 0px;
transition: 0.2s ease-in-out all;
&:hover {
background: #fff;
color: #222;
cursor: pointer;
}
&:focus {
outline: 0;
}
}
.user-row {
display: grid;
grid-template-rows: auto 1fr;
grid-gap: 5px;
& .user-header {
display: grid;
grid-template-columns: 1fr auto;
& .row-title {
letter-spacing: 0.25em;
font-size: 14px;
font-weight: bold;
text-transform: uppercase;
&.row-title-noactions {
margin: 10px 0px;
}
}
}
& .user-list {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-gap: 15px;
}
}
.user-item {
background: #383c3f;
transition: 0.2s ease-in-out transform, 0.2s ease-in-out box-shadow;
overflow: hidden;
border-radius: 6px;
display: grid;
padding: 10px;
grid-gap: 15px;
grid-template-columns: 32px 1fr;
& .user-avatar {
background: rgba(255,255,255,0.1);
background-size: cover;
background-position: center;
width: 32px;
height: 32px;
border-radius: 32px;
}
& .user-metadata {
display: flex;
align-items: center;
& .user-name {
font-weight: bold;
overflow: hidden;
white-space: nowrap;
}
& .user-badge {
font-size: 18px;
margin-left: 10px;
}
}
&:hover {
transform: scale(1.1);
cursor: pointer;
box-shadow: 0px 4px 20px 5px rgba(0, 0, 0, 0.4);
}
}
</style>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g>
<path class="st0" d="M122.89,100l-48.5-84c-4.62-8-16.17-8-20.78,0l-48.5,84c-4.62,8,1.15,18,10.39,18h96.99
C121.74,118,127.51,108,122.89,100z M77.62,59.2H58.48v9.96H75.7v7.68H58.48v9.96h19.14v7.68H50.38V51.52h27.24V59.2z"/>
</g>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g>
<path class="st0" d="M120.95,43.09L71.05,6.84c-4.21-3.06-9.9-3.06-14.11,0L7.05,43.09c-4.21,3.06-5.97,8.47-4.36,13.42
l19.06,58.65c1.61,4.94,6.21,8.29,11.41,8.29h61.67c5.2,0,9.81-3.35,11.41-8.29l19.06-58.65
C126.91,51.56,125.15,46.15,120.95,43.09z M56.9,56.51H37.76v9.96h17.22v7.68H37.76v9.96H56.9v7.68H29.66V48.83H56.9V56.51z
M90.26,91.79L80.9,76.31h-0.48l-9.36,15.48H60.98l14.1-22.44L62.06,48.83h10.02l8.34,14.04h0.48l8.34-14.04h10.02L86.24,69.35
l14.1,22.44H90.26z"/>
</g>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g>
<path class="st0" d="M120.95,43.09L71.05,6.84c-4.21-3.06-9.9-3.06-14.11,0L7.05,43.09c-4.21,3.06-5.97,8.47-4.36,13.42
l19.06,58.65c1.61,4.94,6.21,8.29,11.41,8.29h61.67c5.2,0,9.81-3.35,11.41-8.29l19.06-58.65
C126.91,51.56,125.15,46.15,120.95,43.09z M81.13,92.48h-8.1V74.36H54.97v18.12h-8.1V49.52h8.1v17.16h18.06V49.52h8.1V92.48z"/>
</g>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g>
<path class="st0" d="M119.51,55.51L72.49,8.49C67.8,3.8,60.2,3.8,55.51,8.49L8.49,55.51C3.8,60.2,3.8,67.8,8.49,72.49l47.03,47.03
c4.69,4.69,12.28,4.69,16.97,0l47.03-47.03C124.2,67.8,124.2,60.2,119.51,55.51z M81.37,85.48h-8.52L54.61,55.06h-0.48l0.48,8.28
v22.14h-7.98V42.52h9.42l17.28,28.8h0.48l-0.48-8.28V42.52h8.04V85.48z"/>
</g>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g>
<g>
<path class="st0" d="M113.43,28.35L70,3.27c-3.71-2.14-8.29-2.14-12,0L14.57,28.35c-3.71,2.14-6,6.11-6,10.39v50.14
c0,4.29,2.29,8.25,6,10.39L58,124.35c3.71,2.14,8.29,2.14,12,0l43.43-25.07c3.71-2.14,6-6.11,6-10.39V38.74
C119.43,34.45,117.14,30.49,113.43,28.35z M52.66,85.29L43.3,69.81h-0.48l-9.36,15.48H23.38l14.1-22.44L24.46,42.33h10.02
l8.34,14.04h0.48l8.34-14.04h10.02L48.64,62.85l14.1,22.44H52.66z M103.09,72.57c-1.02,2.64-2.49,4.9-4.41,6.78
c-1.92,1.88-4.24,3.34-6.96,4.38s-5.78,1.56-9.18,1.56H68.02V42.33h14.52c3.4,0,6.46,0.52,9.18,1.56s5.04,2.51,6.96,4.41
s3.39,4.17,4.41,6.81c1.02,2.64,1.53,5.54,1.53,8.7C104.62,67.01,104.11,69.93,103.09,72.57z"/>
</g>
<g>
<path class="st0" d="M92.92,53.79c-1.2-1.2-2.7-2.13-4.5-2.79s-3.88-0.99-6.24-0.99h-6.06v27.6h6.06c2.36,0,4.44-0.33,6.24-0.99
s3.3-1.59,4.5-2.79s2.1-2.65,2.7-4.35s0.9-3.59,0.9-5.67s-0.3-3.97-0.9-5.67S94.12,54.99,92.92,53.79z"/>
</g>
</g>
</svg>
const { app, protocol, BrowserWindow, ipcMain } = require('electron');
const { download } = require('electron-dl');
const { createProtocol } = require('vue-cli-plugin-electron-builder/lib');
const isDevelopment = process.env.NODE_ENV !== 'production';
let win;
let deeplinkingUrl;
let deeplinkingType;
protocol.registerSchemesAsPrivileged([{scheme: 'app', privileges: { secure: true, standard: true } }]);
protocol.registerSchemesAsPrivileged([{scheme: 'app', privileges: { secure: true, standard: true } }]);
protocol.registerSchemesAsPrivileged([{scheme: 'app', privileges: { secure: true, standard: true } }]);
function createWindow () {
win = new BrowserWindow({
title: "SpinSha.re",
width: 1300,
height: 700,
minHeight: 700,
minWidth: 1300,
webPreferences: {
nodeIntegration: true
}
});
if (process.env.WEBPACK_DEV_SERVER_URL) {
win.loadURL(process.env.WEBPACK_DEV_SERVER_URL);
if (!process.env.IS_TEST) win.webContents.openDevTools();
} else {
createProtocol('app');
win.loadURL('app://./index.html');
}
win.setMenuBarVisibility(false);
win.on('closed', () => {
win = null;
});
}
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (win === null) {
createWindow();
}
});
app.on('ready', async () => {
createWindow();
});
if (isDevelopment) {
if (process.platform === 'win32') {
process.on('message', data => {
if (data === 'graceful-exit') {
app.quit();
}
});
} else {
process.on('SIGTERM', () => {
app.quit();
});
}
}
ipcMain.on("download", (event, info) => {
console.log("Download Request Received");
download(BrowserWindow.getFocusedWindow(), info.url, info.properties)
.then(dl => win.webContents.send("download-complete", dl.getSavePath()));
});
\ No newline at end of file
<template>
<div ref="contextMenu" :class="'context-menu ' + (isActive ? 'active' : '')" :style="'top: ' + posY + 'px; left: ' + posX + 'px;'">
<div class="menu-item" v-for="item in items" v-on:click="item.method">
<div class="icon"><i :class="'mdi mdi-' + item.icon"></i></div>
<div class="text">{{ item.title }}</div>
</div>
</div>
</template>
<script>
export default {
name: 'ContextMenu',
data: function() {
return {
isActive: false,
posX: 0,
posY: 0,
items: []
}
},
mounted() {
this.$root.$on('showContextMenu', (data) => {
this.show(data.x, data.y, data.items);
});
this.$root.$on('hideContextMenu', () => {
this.hide();
});
},
methods: {
show: function(x, y, items) {
this.$data.items = items;
this.$data.posX = x;
this.$data.posY = y;
this.$data.isActive = true;
},
hide: function() {
this.$data.isActive = false;
this.$data.items = [];
}
},
created: function() {
document.addEventListener('click', () => {
this.hide();
});
}
}
</script>
<style scoped lang="less">
.context-menu {
position: absolute;
top: 0px;
left: 0px;
width: 250px;
background: #000;
border-radius: 6px;
z-index: 90;
display: none;
overflow: hidden;
box-shadow: 0px 2px 4px rgba(0,0,0,0.4);
& .menu-item {
padding: 10px;
display: grid;
grid-template-columns: 24px 1fr;
grid-gap: 10px;
& .icon {
width: 24px;
height: 24px;
display: flex;
justify-content: center;
align-items: center;
font-size: 20px;
}
& .text {
display: flex;
align-items: center;
}
&:hover {
background: rgba(255,255,255,0.2);
cursor: pointer;
}
}
&.active {
display: block;
}
}
</style>
<template>
<aside>
<nav>
<router-link to="/" class="item"><i class="mdi mdi-home-outline"></i></router-link>
<router-link to="/search" class="item"><i class="mdi mdi-magnify"></i></router-link>
<router-link to="/library" class="item"><i class="mdi mdi-music-box-multiple-outline"></i></router-link>
<div v-on:click="openExternal('https://www.patreon.com/spinshare');" class="item"><i class="mdi mdi-patreon"></i></div>
</nav>
<nav>
<div v-on:click="openExternal('steam://run/1058830');" class="item"><i class="mdi mdi-play-outline"></i></div>
<router-link to="/settings" class="item"><i class="mdi mdi-cog-outline"></i></router-link>
</nav>
</aside>
</template>
<script>
import { remote } from 'electron';
const { shell } = remote;
export default {
name: 'Navigation',
methods: {
openExternal: function(url) {
shell.openExternal(url);
}
}
}
</script>
<style scoped lang="less">
aside {
background: rgba(255,255,255,0.1);
display: grid;
position: fixed;
top: 0px;
left: 0px;
bottom: 0px;
grid-template-rows: 1fr auto;
& .item, & .external-item {
width: 60px;
height: 60px;
opacity: 0.4;
display: flex;
justify-content: center;
align-items: center;
background: transparent;
color: #fff;
transition: 0.2s ease opacity, 0.2s ease background;
&:hover {
opacity: 1;
cursor: pointer;
}
&.router-link-exact-active {
opacity: 1;
background: linear-gradient(135deg, #fd2f85, #7a34ec);
}
& .mdi {
font-size: 22px;
}
}
}
</style>
<template>
<div class="song-item" v-on:contextmenu="showContextMenu($event)">
<div class="song-cover" :style="'background-image: url(' + cover + '), url(' + require('@/assets/img/defaultAlbumArt.jpg') + ');'">
<div class="song-charter-info">
<div class="song-charter"><i class="mdi mdi-account-circle"></i><span>{{ charter }}</span></div>
</div>
</div>
<div class="song-metadata">
<div class="song-title">{{ title }}</div>
<div class="song-artist">{{ artist }}</div>
<div class="song-difficulties">
<img src="@/assets/img/difficultyEasy.svg" :class="hasEasyDifficulty ? 'active' : ''" />
<img src="@/assets/img/difficultyNormal.svg" :class="hasNormalDifficulty ? 'active' : ''" />
<img src="@/assets/img/difficultyHard.svg" :class="hasHardDifficulty ? 'active' : ''" />
<img src="@/assets/img/difficultyExtreme.svg" :class="hasExtremeDifficulty ? 'active' : ''" />
<img src="@/assets/img/difficultyXD.svg" :class="hasXDDifficulty ? 'active' : ''" />
</div>
</div>
</div>
</template>
<script>
import { remote } from 'electron';
const { clipboard } = remote;
export default {
name: 'SongItem',
props: [
'id',
'cover',
'title',
'subtitle',
'artist',
'charter',
'hasEasyDifficulty',
'hasNormalDifficulty',
'hasHardDifficulty',
'hasExtremeDifficulty',
'hasXDDifficulty',
'zip'
],
data: function() {
return {
isContextMenuActive: false
}
},
mounted: function() {
},
methods: {
showContextMenu: function(e) {
this.$root.$emit('showContextMenu', {
x: e.pageX,
y: e.pageY,
items: [
{ icon: "eye", title: "Open", method: function() { alert('TODO') }.bind(this) },
{ icon: "link", title: "Copy Link", method: function() { clipboard.writeText('https://spinsha.re/song/' + this.$props.id) }.bind(this) },
{ icon: "download", title: "Download", method: function() { this.$root.$emit('download', this.$props.zip); }.bind(this) }
]});
}
}
}
</script>
<style scoped lang="less">
.song-item {
background: rgba(255,255,255,0.1);
transition: 0.2s ease-in-out transform, 0.2s ease-in-out box-shadow;
overflow: hidden;
border-radius: 6px;
& .song-cover {
background: rgba(255,255,255,0.1);
background-size: cover;
width: 100%;
padding-top: 100%;
position: relative;
background-position: center;
& .song-charter {
position: absolute;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
background: linear-gradient(180deg, rgba(0,0,0,0.2), rgba(0,0,0,0.8));
opacity: 0;
padding: 15px;
overflow: hidden;
display: grid;
transition: 0.2s ease-in-out opacity;
grid-template-columns: auto 1fr;
grid-gap: 10px;
align-items: flex-end;
& .song-charter-info {
display: grid;
align-items: center;
& .mdi {
font-size: 18px;
}
& span {
font-size: 12px;
color: transparent;
transition: 0.2s ease-in-out color;
overflow: hidden;
white-space: nowrap;
}
}
}
}
& .song-metadata {
padding: 15px;
& .song-title {
font-weight: bold;
overflow: hidden;
white-space: nowrap;
}
& .song-artist {
margin-top: 5px;
opacity: 0.6;
overflow: hidden;
white-space: nowrap;
}
& .song-difficulties {
margin-top: 10px;
height: 20px;
display: flex;
& img {
height: 18px;
margin-right: 10px;
opacity: 0.3;
&.active {
opacity: 1;
}
}
}
}
&:hover {
transform: scale(1.1);
cursor: pointer;
box-shadow: 0px 4px 20px 5px rgba(0, 0, 0, 0.4);
& .song-cover {
& .song-charter {
opacity: 1;
}
}
}
}
</style>
<template>
<div class="song-item-loading">
<div class="song-cover"></div>
<div class="song-metadata">
<div class="song-title">&nbsp;</div>
<div class="song-artist">&nbsp;</div>
<div class="song-difficulties">
</div>
</div>
</div>
</template>
<script>
import { remote } from 'electron';
const { shell } = remote;
export default {
name: 'SongItemPlaceholder',
}
</script>
<style scoped lang="less">
.song-item-loading {
overflow: hidden;
border-radius: 6px;
background: linear-gradient(to right, rgba(255,255,255,0.1), rgba(255,255,255,0.2), rgba(255,255,255,0.1));
animation-name: songLoadingShimmer;
animation-fill-mode: forwards;
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-duration: 2s;
& .song-cover {
width: 100%;
padding-top: 100%;
opacity: 0;
position: relative;
background-position: center;
}
& .song-metadata {
padding: 15px;
& .song-artist {
margin-top: 5px;
}
& .song-difficulties {
margin-top: 10px;
height: 20px;
display: flex;
}
}
}
</style>
<template>
<div class="song-row song-row-new">
<div class="song-header">
<div class="row-title">{{ title }}</div>
<div class="row-controls">
<div class="item disabled row-controls-previous"><i class="mdi mdi-chevron-left"></i></div>
<div class="item row-controls-next"><i class="mdi mdi-chevron-right"></i></div>
</div>
</div>
<div class="song-list">
<slot></slot>
</div>
</div>
</template>
<script>
import { remote } from 'electron';
const { shell } = remote;
export default {
name: 'SongRow',
props: [
'title'
]
}
</script>
<style scoped lang="less">
.song-row {
display: grid;
grid-template-rows: auto 1fr;
grid-gap: 5px;
& .song-header {
display: grid;
grid-template-columns: 1fr auto;
& .row-title {
letter-spacing: 0.25em;
font-size: 14px;
font-weight: bold;
text-transform: uppercase;
&.row-title-noactions {
margin: 10px 0px;
}
}
& .row-controls {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 15px;
& .item {
width: 28px;
height: 28px;
font-size: 22px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
&.disabled {
opacity: 0.4;
}
&:not(.disabled):hover {
background: rgba(255,255,255,0.2);
cursor: pointer;
}
}
}
}
& .song-list {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;
grid-gap: 15px;
}
& .song-list-noresults {
display: none;
background: rgba(255,255,255,0.1);
border-radius: 6px;
padding: 25px;
opacity: 0.6;
text-align: center;
&.active {
display: block;
}
}
}
</style>
<template>
<div :style="'background-image: url(' + image_path + ');'" :class="isLoading ? 'staff-promo promo-loading' : 'staff-promo' ">
<div class="promo-type" :style="'color:' + color">{{ type }}</div>
<div class="promo-title" :style="'color:' + textColor" v-html="title"></div>
<div class="promo-button" :style="'background-color:' + color" v-on:click="buttonClick()">CHECK IT OUT</div>
</div>
</template>
<script>
import { remote } from 'electron';
const { shell } = remote;
export default {
name: 'StaffPromo',
methods: {
buttonClick: function() {
switch(this.$props.buttonType) {
case 0:
// Song
break;
case 1:
// Playlist (unused)
break;
case 2:
// Search Query
break;
case 3:
// External
shell.openExternal(this.$props.buttonData);
break;
}
}
},
props: [
'isLoading',
'image_path',
'type',
'title',
'buttonType',
'buttonData',
'textColor',
'color'
]
}
</script>
<style scoped lang="less">
.staff-promo {
background: #fff;
border-radius: 6px;
padding: 50px;
height: 256px;
display: grid;
transition: 0.2s ease all;
grid-template-rows: auto 100px auto;
& .promo-type {
color: #aaa;
justify-self: left;
font-size: 12px;
font-weight: bold;
letter-spacing: 0.3em;
text-transform: uppercase;
}
& .promo-title {
font-weight: bold;
font-size: 34px;
justify-self: left;
align-self: flex-start;
letter-spacing: -0.025em;
color: #222;
}
& .promo-button {
font-size: 14px;
font-weight: bold;
text-transform: uppercase;
padding: 10px 25px;
color: #fff;
background: #aaa;
justify-self: left;
border-radius: 6px;
transition: 0.2s ease-in-out all;
&:hover {
cursor: pointer;
opacity: 0.6;
}
}
}
</style>
<template>
<div class="staff-promo-placeholder"></div>
</template>
<script>
export default {
name: 'StaffPromoPlaceholder'
}
</script>
<style scoped lang="less">
.staff-promo-placeholder {
background: #fff;
border-radius: 6px;
padding: 50px;
height: 256px;
display: grid;
transition: 0.2s ease all;
grid-template-rows: auto 100px auto;
background: linear-gradient(to right, rgba(255,255,255,0.1), rgba(255,255,255,0.2), rgba(255,255,255,0.1));
animation-name: promoLoadingShimmer;
animation-fill-mode: forwards;
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-duration: 1s;
}
@keyframes promoLoadingShimmer {
from {
background-position: 0px 0px;
}
to {
background-position: 544px 0px;
}
}
</style>
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
Vue.config.productionTip = false;
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app');
\ No newline at end of file
const axios = require('axios');
class SSAPI {
constructor(isDev) {
if(isDev) {
this.apiBase = "http://localhost/www/spinshare/server/public/index.php/api/";
} else {
this.apiBase = "https://spinsha.re/api/";
}
this.supportedVersion = 1;
}
async ping() {
let apiPath = this.apiBase + "ping";
let supportedVersion = this.supportedVersion;
return axios.get(apiPath)
.then(function(response) {
if(response.data.version !== supportedVersion) {
throw new Error("Client is outdated!");
}
return response.data.data;
}).catch(function(error) {
throw new Error(error);
});
}
async getPromos() {
let apiPath = this.apiBase + "promos";
let supportedVersion = this.supportedVersion;
return axios.get(apiPath)
.then(function(response) {
if(response.data.version !== supportedVersion) {
throw new Error("Client is outdated!");
}
return response.data.data;
}).catch(function(error) {
throw new Error(error);
});
}
async getLatestVersion() {
let apiPath = this.apiBase + "latestVersion/" + process.platform;
let supportedVersion = this.supportedVersion;
return axios.get(apiPath)
.then(function(response) {
if(response.data.version !== supportedVersion) {
throw new Error("Client is outdated!");
}
return response.data.data;
}).catch(function(error) {
throw new Error(error);
});
}
async getNewSongs(_offset) {
let apiPath = this.apiBase + "songs/new/" + _offset;
let supportedVersion = this.supportedVersion;
return axios.get(apiPath)
.then(function(response) {
if(response.data.version !== supportedVersion) {
throw new Error("Client is outdated!");
}
return response.data.data;
}).catch(function(error) {
throw new Error(error);
});
}
async getPopularSongs(_offset) {
let apiPath = this.apiBase + "songs/popular/" + _offset;
let supportedVersion = this.supportedVersion;
return axios.get(apiPath)
.then(function(response) {
if(response.data.version !== supportedVersion) {
throw new Error("Client is outdated!");
}
return response.data.data;
}).catch(function(error) {
throw new Error(error);
});
}
async getSongDetail(_songId) {
let apiPath = this.apiBase + "song/" + _songId;
let supportedVersion = this.supportedVersion;
return axios.get(apiPath)
.then(function(response) {
if(response.data.version !== supportedVersion) {
throw new Error("Client is outdated!");
}
return response.data;
}).catch(function(error) {
throw new Error(error);
});
}
async getUserDetail(_userId) {
let apiPath = this.apiBase + "user/" + _userId;
let supportedVersion = this.supportedVersion;
return axios.get(apiPath)
.then(function(response) {
if(response.data.version !== supportedVersion) {
throw new Error("Client is outdated!");
}
return response.data;
}).catch(function(error) {
throw new Error(error);
});
}
async search(_searchQuery) {
let apiPath = this.apiBase + "search/" + _searchQuery;
let supportedVersion = this.supportedVersion;
return axios.get(apiPath)
.then(function(response) {
if(response.data.version !== supportedVersion) {
throw new Error("Client is outdated!");
}
return response.data;
}).catch(function(error) {
throw new Error(error);
});
}
}
module.exports = SSAPI;
const fs = require('fs');
const rimraf = require('rimraf');
const unzipper = require('unzipper');
const uniqid = require('uniqid');
class SRXD {
constructor() {
this.backupLocation = "";
this.srtbLocation = "";
this.songTrackInfo = {};
this.songLocation = "";
}
// Extract a local backup folder
async extractBackup(filePath, fileName) {
if(this.backupLocation != "") {
console.log("Unload previous Backup.");
this.unloadBackup();
}
console.log("Extracting Backup.");
this.backupLocation = path.join(tempDirLocation, "extract-" + uniqid());
console.info(this.backupLocation);
// Unzip to temp/CustomSpeens/Song
await fs.createReadStream(filePath).pipe(unzipper.Extract({ path: this.backupLocation })).promise();
console.log("Loading Backup.");
// Find SRTB & OGG files
let srtbFilesInBackupLocation = this.getFilesFromPath(this.backupLocation, ".srtb");
if(srtbFilesInBackupLocation.length < 1) {
console.error("No SRTB file found in backup.");
return false;
}
// Load SRTB file
this.srtbLocation = path.join(this.backupLocation, srtbFilesInBackupLocation[0]);
let srtbFile = JSON.parse( fs.readFileSync(this.srtbLocation) );
let songTrackInfo = "";
srtbFile.largeStringValuesContainer.values.forEach(function(value) {
if(value.key == "SO_TrackInfo_TrackInfo") {
songTrackInfo = value.val;
}
});
this.songTrackInfo = songTrackInfo;
// Load OGG file
let OggFilePath = fs.existsSync(path.join(this.backupLocation, "AudioClips"), ".ogg")
if (fs.existsSync(OggFilePath)) {
let oggFilesInBackupLocation = this.getFilesFromPath(OggFilePath);
this.songLocation = path.join(this.backupLocation, oggFilesInBackupLocation[0]);
// TODO: Backup Validation
return this.backupLocation;
}
else{
return this.backupLocation;
}
}
async unloadBackup() {
// Remove temp files
console.log("Removing Backup Folder: " + this.backupLocation);
try {
rimraf(this.backupLocation, function() { console.log("Removed backup folder."); });
} catch(error) {
console.error(error);
}
// Reset vars
this.backupLocation = "";
this.srtbLocation = "";
this.songTrackInfo = {};
this.songLocation = "";
}
getSongDetail(srtbPath) {
let srtbFile = JSON.parse( fs.readFileSync(srtbPath) );
let songTrackInfo = "";
let songOggInfo = "";
srtbFile.largeStringValuesContainer.values.forEach(function(value) {
if(value.key == "SO_TrackInfo_TrackInfo") {
songTrackInfo = JSON.parse( value.val );
}
if(value.key == "SO_ClipInfo_ClipInfo_0") {
songOggInfo = JSON.parse( value.val );
}
});
return [songTrackInfo, this.getSongCover(songTrackInfo.albumArtReference.assetName), this.getSongAssetDirectory(songOggInfo.clipAssetReference.assetName, "AudioClips"), this.getSongAssetDirectory(songTrackInfo.albumArtReference.assetName, "AlbumArt"), srtbPath];
}
// Used to find files by file extension
// by https://stackoverflow.com/a/52024318
getFilesFromPath(path, extension) {
let dir = fs.readdirSync( path );
return dir.filter( elm => elm.match(new RegExp(`.*\.(${extension})$`, 'ig')));
}
getLocalSongs(path) {
let dir = fs.readdirSync( path );
return dir.filter( elm => elm.match(new RegExp(`.*\.srtb$`, 'ig')));
}
getFileExtension(fileName, filePath){
let filePathJoined = path.join(filePath, fileName);
let files = glob.sync(filePathJoined+".*");
if (files[0] != undefined){return [path.parse(files[0]).base];}
else {return [];}
}
getSongCover(fileName) {
let fileExtension = this.getFileExtension(fileName, path.join(userSettings.get('gameDirectory'), "AlbumArt") );
if(fileExtension.length > 0) {
let finalPath = path.join(userSettings.get('gameDirectory'), "AlbumArt", fileExtension[0]);
let base64Data = "data:image/jpg;base64," + fs.readFileSync(finalPath, { encoding: 'base64' });
return base64Data;
} else {
return "";
}
}
getSongTrackInfo() {
return this.songTrackInfo;
}
//Gets directory of files to delete
getSongAssetDirectory(fileName, fileType) {
let fileExtension = this.getFileExtension(fileName, path.join(userSettings.get('gameDirectory'), fileType));
if (fileExtension.join() != '') {
let finalPath = path.join(userSettings.get('gameDirectory'), fileType, fileExtension.join());
return finalPath;
}
else {return fileName;}
}
//Deletes Files
deleteFiles(songDetail) {
let deleteFiles = [songDetail[2], songDetail[3], songDetail[4]];
deleteFiles.forEach(function(file) {
try{
let foundFiles = glob.sync(file);
if(foundFiles.length > 0) {
fs.unlinkSync(foundFiles[0]);
}
}
catch(err){}
});
RefreshLibrary();
}
}
module.exports = SRXD;
import Vue from 'vue';
import VueRouter from 'vue-router';
import VueAxios from 'vue-axios';
import axios from 'axios';
import ViewStartup from '../views/Startup.vue';
import ViewSearch from '../views/Search.vue';
import ViewLibrary from '../views/Library.vue';
import ViewSettings from '../views/Settings.vue';
Vue.use(VueRouter);
Vue.use(VueAxios, axios);
const routes = [{
path: '/',
name: 'Startup',
component: ViewStartup
}, {
path: '/search',
name: 'Search',
component: ViewSearch
}, {
path: '/library',
name: 'Library',
component: ViewLibrary
}, {
path: '/settings',
name: 'Settings',
component: ViewSettings
}];
const router = new VueRouter({
routes
});
export default router;
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
},
mutations: {
},
actions: {
},
modules: {
}
})
<template>
<section class="section-library">
LIBRARY
</section>
</template>
<script>
export default {
name: 'Library',
components: {
}
}
</script>
<style scoped lang="less">
section {
padding: 50px;
}
</style>
\ No newline at end of file
<template>
<section class="section-search">
<div class="search-bar">
<div class="show-all">
<div class="button button-label" v-on:click="searchAll()" locale="">Show all</div>
</div>
<input type="search" placeholder="Search for songs, tags &amp; profiles..." localeplaceholder="" v-on:change="search(this.value)">
</div>
</section>
</template>
<script>
export default {
name: 'Search',
components: {
},
methods: {
searchAll: function() {
},
search: function(searchQuery) {
}
}
}
</script>
<style scoped lang="less">
.section-search {
grid-template-rows: auto 1fr;
grid-gap: 25px;
padding: 50px;
display: grid;
& .search-bar {
background: rgba(255, 255, 255, 0.2);
border-radius: 4px;
display: grid;
grid-template-columns: auto 1fr;
input {
width: 100%;
font-family: 'Open Sans', sans-serif;
font-size: 14px;
color: #fff;
background: rgba(255,255,255,0.2);
border-radius: 4px;
padding: 14px 28px;
border: 0px;
transition: 0.2s ease-in-out all;
&:hover {
background: rgba(255,255,255,0.4);
color: #fff;
}
&:focus {
outline: 0;
}
&::placeholder {
color: rgba(255,255,255,0.6);
}
}
}
& .search-results {
display: grid;
grid-template-rows: auto auto auto 1fr;
grid-gap: 25px;
& .search-results-users {
display: none;
&.active {
display: grid;
}
}
& .search-results-songs {
display: none;
&.active {
display: grid;
}
}
& .search-results-noresults {
background: rgba(255,255,255,0.1);
border-radius: 6px;
padding: 25px;
display: none;
& .noresults-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 10px;
}
& .noresults-text {
opacity: 0.6;
}
&.active {
display: block;
}
}
}
}
</style>
\ No newline at end of file
<template>
<section class="section-settings">
SETTINGS
</section>
</template>
<script>
export default {
name: 'Settings',
components: {
}
}
</script>
<style scoped lang="less">
section {
padding: 50px;
}
</style>
\ No newline at end of file
<template>
<section class="section-startup">
<div class="staff-promos">
<StaffPromoPlaceholder
v-if="isPromoLoading"
v-for="n in 2" />
<StaffPromo
v-if="!isPromoLoading"
v-for="staffPromo in staffPromos"
v-bind:key="staffPromo.id"
v-bind="staffPromo" />
</div>
<SongRow
title="New Songs">
<SongItemPlaceholder
v-if="isNewSongsLoading"
v-for="n in 6" />
<SongItem
v-if="!isNewSongsLoading"
v-for="song in newSongs"
v-bind:key="song.id"
v-bind="song" />
</SongRow>
<SongRow
title="Popular Songs">
<SongItemPlaceholder
v-if="isPopularSongsLoading"
v-for="n in 6" />
<SongItem
v-if="!isPopularSongsLoading"
v-for="song in popularSongs"
v-bind:key="song.id"
v-bind="song" />
</SongRow>
</section>
</template>
<script>
import SSAPI from '@/modules/module.api.js';
import StaffPromo from '@/components/Startup/StaffPromo.vue';
import StaffPromoPlaceholder from '@/components/Startup/StaffPromoPlaceholder.vue';
import SongRow from '@/components/Song/SongRow.vue';
import SongItem from '@/components/Song/SongItem.vue';
import SongItemPlaceholder from '@/components/Song/SongItemPlaceholder.vue';
export default {
name: 'Startup',
data: function() {
return {
isPromoLoading: true,
staffPromos: [],
isNewSongsLoading: true,
newSongsOffset: 0,
newSongs: [],
isPopularSongsLoading: true,
popularSongsOffset: 0,
popularSongs: []
}
},
mounted: function() {
let ssapi = new SSAPI(true);
ssapi.getPromos().then((data) => {
this.$data.isPromoLoading = false;
this.$data.staffPromos = data;
});
ssapi.getNewSongs(this.$data.newSongsOffset).then((data) => {
this.$data.isNewSongsLoading = false;
this.$data.newSongs = data;
});
ssapi.getPopularSongs(this.$data.popularSongsOffset).then((data) => {
this.$data.isPopularSongsLoading = false;
this.$data.popularSongs = data;
});
},
components: {
StaffPromo,
StaffPromoPlaceholder,
SongRow,
SongItem,
SongItemPlaceholder
}
}
</script>
<style scoped lang="less">
section {
padding: 50px;
& .staff-promos {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 25px;
width: 1114px;
margin: 0 auto;
&:empty {
display: none;
}
}
& .song-row {
margin-top: 25px;
}
}
</style>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment