Commit 88725d89 authored by SpinShare's avatar SpinShare

added userdetail and manual install and fixed download queue

parent 69b916f5
This diff is collapsed.
......@@ -12,6 +12,7 @@
},
"main": "background.js",
"dependencies": {
"adm-zip": "^0.4.14",
"axios": "^0.19.2",
"core-js": "^3.6.4",
"electron-download-manager": "^2.1.2",
......
......@@ -12,7 +12,7 @@
</transition>
<transition name="slideDownloadOverlay">
<DownloadOverlay v-if="showDownloadOverlay" v-bind:downloadQueue="downloadQueue"></DownloadOverlay>
<DownloadOverlay v-if="showDownloadOverlay" v-bind:downloadQueue="downloadQueue" v-bind:finishedQueue="finishedQueue" v-bind:failedQueue="failedQueue"></DownloadOverlay>
</transition>
</div>
</template>
......@@ -24,7 +24,6 @@
import fs from 'fs';
import glob from 'glob';
import path from 'path';
import ncp from 'ncp';
import UserSettings from '@/modules/module.usersettings.js';
import SSAPI from '@/modules/module.api.js';
......@@ -46,6 +45,8 @@
data: function() {
return {
downloadQueue: [],
finishedQueue: [],
failedQueue: [],
downloadQueueProcessing: false,
showUpdateOverlay: false,
showDownloadOverlay: false,
......@@ -53,6 +54,10 @@
}
},
mounted: function() {
document.addEventListener('auxclick', function(e) {
e.preventDefault();
});
this.$root.$on('download', (url) => {
this.addToQueue(url);
});
......@@ -88,15 +93,28 @@
let srxdControl = new SRXD();
let userSettings = new UserSettings();
let queueItem = this.$data.downloadQueue.findIndex(function(i) {
return i.id === downloadItem.id;
});
console.info("████ #" + queueItem + " - '" + this.$data.downloadQueue[queueItem].title + "' ████");
if(downloadItem.status == 1) {
// Failed, add to failed Array
this.$data.failedQueue.push(queueItem);
this.$data.downloadQueue.splice(queueItem, 1);
} else {
// Finished, unpacking
srxdControl.extractBackup(downloadItem.downloadPath, path.basename(downloadItem.downloadPath)).then((extractResult) => {
if(extractResult) {
this.installBackup(extractResult, userSettings.get('gameDirectory')).then((result) => {
if(extractResult !== false) {
srxdControl.installBackup(extractResult, userSettings.get('gameDirectory')).then((result) => {
console.log("[COPY] Backup installed!");
this.$data.downloadQueueProcessing = false;
console.log("Queue Remaining: " + this.$data.downloadQueue.length);
console.log("[QUEUE] Remaining Items: " + this.$data.downloadQueue.length);
this.$data.downloadQueue.splice(this.$data.downloadQueue.findIndex(function(i) {
return i.id === downloadItem.id;
}), 1);
this.$data.finishedQueue.push(queueItem);
this.$data.downloadQueue.splice(queueItem, 1);
if(this.$data.downloadQueue.length > 0) {
this.processQueue();
......@@ -105,11 +123,32 @@
console.error(error);
});
} else {
console.error("Backup could not be loaded!");
console.error("[COPY] Backup could not be installed!");
this.$data.downloadQueueProcessing = false;
console.log("[QUEUE] Remaining Items: " + this.$data.downloadQueue.length);
this.$data.failedQueue.push(queueItem);
this.$data.downloadQueue.splice(queueItem, 1);
if(this.$data.downloadQueue.length > 0) {
this.processQueue();
}
}
}).catch(error => {
console.error(error);
this.$data.downloadQueueProcessing = false;
console.log("[QUEUE] Remaining Items: " + this.$data.downloadQueue.length);
this.$data.failedQueue.push(queueItem);
this.$data.downloadQueue.splice(queueItem, 1);
if(this.$data.downloadQueue.length > 0) {
this.processQueue();
}
});
}
});
},
methods: {
......@@ -133,19 +172,6 @@
closeOverlays: function() {
this.$data.showUpdateOverlay = false;
this.$data.showDownloadOverlay = false;
},
installBackup: async function(backupLocation, gameDirLocation) {
await ncp(backupLocation, gameDirLocation, function(error) {
if(error) {
console.error(error);
console.error("Couldn't copy backup!");
return true;
}
console.log("Copied Backup!");
});
return true;
}
}
}
......
......@@ -2,6 +2,10 @@ const { app, protocol, BrowserWindow, ipcMain } = require('electron');
const DownloadManager = require("electron-download-manager");
const { createProtocol } = require('vue-cli-plugin-electron-builder/lib');
const isDevelopment = process.env.NODE_ENV !== 'production';
const fs = require('fs');
const http = require('http');
const path = require('path');
const uniqid = require('uniqid');
let win;
let deeplinkingUrl;
......@@ -73,19 +77,43 @@ if (isDevelopment) {
}
ipcMain.on("download", (event, ipcData) => {
console.log("Download: " + ipcData.queueItem.title);
console.log("Starting download of > " + ipcData.queueItem.title);
DownloadManager.download({url: ipcData.queueItem.downloadPath}, (error, dlInfo) => {
download(ipcData.queueItem.downloadPath, uniqid(), (error, dlInfo) => {
if (error) {
console.log(error);
let downloadItem = {
id: ipcData.queueItem.id,
status: 1,
downloadPath: null
}
win.webContents.send("download-complete", downloadItem);
return;
}
let downloadItem = {
id: ipcData.queueItem.id,
downloadPath: dlInfo.filePath
status: 2,
downloadPath: dlInfo
}
win.webContents.send("download-complete", downloadItem);
});
});
function download(url, fileName, cb) {
let dest = path.join(app.getPath('temp'), fileName + ".zip");
let file = fs.createWriteStream(dest);
let request = http.get(url, function(response) {
response.pipe(file);
file.on('finish', function() {
file.close(cb(null, dest)); // async call of the callback
});
}).on('error', function(err) { // Handle errors
fs.unlink(dest); // Delete the file async. (But we don't check the result)
if (cb) cb(err.message, dest);
});
};
\ No newline at end of file
......@@ -11,9 +11,6 @@
</template>
<script>
import { remote } from 'electron';
const { clipboard } = remote;
export default {
name: 'SongInstallItem',
data: function() {
......@@ -23,7 +20,8 @@
mounted: function() {
},
methods: {
install: function(e) {
install: function() {
this.$parent.$parent.$emit('install');
}
}
}
......
<template>
<div class="song-item" v-on:contextmenu="showContextMenu($event)">
<div class="song-item" v-on:auxclick="shortDownload($event)" v-on:contextmenu="showContextMenu($event)">
<router-link :to="{ name: 'SongDetail', params: { id: id } }">
<div class="song-cover" :style="'background-image: url(' + cover + '), url(' + require('@/assets/img/defaultAlbumArt.jpg') + ');'">
<div class="song-charter-info">
......@@ -61,7 +61,16 @@
{ icon: "download", title: "Download", method: () => { this.download(); } }
]});
},
download: function() {
shortDownload: function(e) {
e.preventDefault();
if(e.which == 2) {
this.download(e);
}
},
download: function(e) {
e.preventDefault();
this.$root.$emit('download', {id: this.$props.id, cover: this.$props.cover, title: this.$props.title, artist: this.$props.artist, downloadPath: this.$props.zip});
}
}
......
......@@ -9,6 +9,8 @@
<div class="song-list">
<slot name="song-list"></slot>
</div>
<div class="noresults">
</div>
</div>
</template>
......
<template>
<div class="stream" v-if="isLive">
<div class="header">
<div class="viewers"><i class="mdi mdi-eye-outline"></i> {{ viewers }}</div>
<div class="title">{{ title }}</div>
</div>
<div class="video-container">
<iframe src="https://player.twitch.tv/?channel=spinshare&enableExtensions=true&muted=true&player=popout&volume=1"></iframe>
</div>
</div>
</template>
<script>
import { remote } from 'electron';
const { shell } = remote;
export default {
name: 'Stream',
props: [
'title',
'viewers',
'isLive'
]
}
</script>
<style scoped lang="less">
.stream {
margin-top: 50px;
& .header {
display: grid;
grid-template-columns: auto 1fr;
grid-gap: 15px;
margin-bottom: 15px;
& .viewers {
background: #ff294d;
color: #fff;
padding: 5px;
font-size: 12px;
font-weight: bold;
border-radius: 20px;
}
& .title {
letter-spacing: 0.25em;
font-size: 14px;
font-weight: bold;
text-transform: uppercase;
align-self: center;
}
}
& .video-container {
position: relative;
width: 100%;
padding-top: 50%;
& iframe {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 100%;
height: 100%;
border: 0px;
}
}
}
</style>
<template>
<div class="user-item">
<router-link :to="{ name: 'UserDetail', params: { id: id } }" class="user-item">
<div class="user-avatar" :style="'background-image: url(' + avatar + '), url(' + require('@/assets/img/defaultAvatar.jpg') + ');'"></div>
<div class="user-metadata">
<div class="user-username">{{ username }}</div>
</div>
</div>
</router-link>
</template>
<script>
......@@ -51,6 +51,8 @@
display: grid;
padding: 10px;
grid-gap: 15px;
color: #fff;
text-decoration: none;
grid-template-columns: 32px 1fr;
& .user-avatar {
......
......@@ -26,6 +26,22 @@ class SSAPI {
});
}
async getStreamStatus() {
let apiPath = this.apiBase + "streamStatus";
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;
......
......@@ -3,6 +3,8 @@ const fs = require('fs');
const path = require('path');
const rimraf = require('rimraf');
const unzipper = require('unzipper');
const ncp = require('ncp');
const admzip = require('adm-zip');
const uniqid = require('uniqid');
const { app } = require('electron').remote;
const UserSettings = require('./module.usersettings');
......@@ -20,51 +22,42 @@ class SRXD {
// Extract a local backup folder
async extractBackup(filePath, fileName) {
if(this.backupLocation != "") {
console.log("Unload previous Backup.");
console.log("{EXTRACT] Unloading previous Backup.");
this.unloadBackup();
}
console.log("Extracting Backup.");
console.log("[EXTRACT] Starting Extraction.");
this.backupLocation = path.join(app.getPath('temp'), "extract-" + uniqid());
console.info(this.backupLocation);
console.info("[EXTRACT] Backup Location: " + this.backupLocation);
// Unzip to temp/CustomSpeens/Song
await fs.createReadStream(filePath).pipe(unzipper.Extract({ path: this.backupLocation })).promise();
// Find SRTB & OGG files
let srtbFilesInBackupLocation = this.getFilesFromPath(this.backupLocation, ".srtb");
if(srtbFilesInBackupLocation.length < 1) {
console.error("No SRTB file found in backup.");
try {
console.log("[EXTRACT] Unzipping.");
console.log("[EXTRACT] " + filePath);
console.log("[EXTRACT] " + this.backupLocation);
let zip = admzip(filePath);
await zip.extractAllTo(this.backupLocation, true);
} catch(e) {
console.log("[EXTRACT] Couldn't unzip backup");
console.error(e);
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 installBackup(backupLocation, gameDirLocation) {
await ncp(backupLocation, gameDirLocation, function(error) {
if(error) {
console.error(error);
console.error("[COPY] Couldn't copy backup!");
return false;
}
console.info("[COPY] Done!");
return true;
});
}
async unloadBackup() {
......@@ -123,7 +116,11 @@ class SRXD {
if(fileExtension.length > 0) {
let finalPath = path.join(this.userSettings.get('gameDirectory'), "AlbumArt", fileExtension[0]);
try {
let base64Data = "data:image/jpg;base64," + fs.readFileSync(finalPath, { encoding: 'base64' });
} catch(e) {
return "";
}
return base64Data;
} else {
......
......@@ -10,6 +10,7 @@ import ViewStartupPopularSongs from '../views/StartupPopularSongs.vue';
import ViewSearch from '../views/Search.vue';
import ViewLibrary from '../views/Library.vue';
import ViewSongDetail from '../views/SongDetail.vue';
import ViewUserDetail from '../views/UserDetail.vue';
import ViewSettings from '../views/Settings.vue';
Vue.use(VueRouter);
......@@ -53,6 +54,10 @@ const routes = [{
path: '/song/:id',
name: 'SongDetail',
component: ViewSongDetail
},, {
path: '/user/:id',
name: 'UserDetail',
component: ViewUserDetail
}, {
path: '/settings',
name: 'Settings',
......
......@@ -20,6 +20,9 @@
</template>
<script>
import { remote } from 'electron';
const { dialog } = remote;
import fs from 'fs';
import glob from 'glob';
import path from 'path';
......@@ -66,6 +69,9 @@
this.$data.showDeleteOverlay = false;
this.$data.deleteFiles = "";
});
this.$on('install', () => {
this.install();
});
},
methods: {
refreshLibrary: function() {
......@@ -149,6 +155,35 @@
});
return connectedFiles;
},
install: function(e) {
dialog.showOpenDialog({ title: "Open Backup", properties: ['openFile', 'multiSelections'], filters: [{"name": "Backup Archive", "extensions": ["zip"]}] }).then(result => {
if(!result.canceled) {
result.filePaths.forEach((rawFilePath) => {
let filePath = glob.sync(rawFilePath);
if(filePath.length > 0) {
let srxdControl = new SRXD();
let userSettings = new UserSettings();
srxdControl.extractBackup(filePath[0], path.basename(filePath[0])).then((extractResult) => {
if(extractResult !== false) {
srxdControl.installBackup(extractResult, userSettings.get('gameDirectory')).then((result) => {
console.log("[COPY] Backup installed!");
setTimeout(() => {
this.refreshLibrary();
}, 250);
}).catch(error => {
console.error(error);
});
} else {
console.error("[COPY] Backup could not be installed!");
}
}).catch(error => {
console.error(error);
});
}
});
}
});
}
}
}
......
<template>
<div class="frontpage">
<div class="staff-promos">
<StaffPromoPlaceholder
v-if="isPromoLoading"
......@@ -10,19 +11,24 @@
v-bind:key="staffPromo.id"
v-bind="staffPromo" />
</div>
<Stream v-bind="streamStatus" />
</div>
</template>
<script>
import SSAPI from '@/modules/module.api.js';
import StaffPromo from '@/components/Startup/StaffPromo.vue';
import StaffPromoPlaceholder from '@/components/Startup/StaffPromoPlaceholder.vue';
import Stream from '@/components/Startup/Stream.vue';
export default {
name: 'StartupFrontpage',
data: function() {
return {
isPromoLoading: true,
staffPromos: []
staffPromos: [],
streamStatus: []
}
},
mounted: function() {
......@@ -32,10 +38,15 @@
this.$data.isPromoLoading = false;
this.$data.staffPromos = data;
});
ssapi.getStreamStatus().then((data) => {
this.$data.streamStatus = data;
});
},
components: {
StaffPromo,
StaffPromoPlaceholder
StaffPromoPlaceholder,
Stream
},
methods: {
}
......
<template>
<section class="section-user-detail">
<div class="user-detail-background" :style="'background-image: url(' + avatar + '), url(' + require('@/assets/img/defaultAvatar.jpg') + ');'" v-if="apiFinished">
<div class="user-detail-dim">
<div class="user-detail">
<div class="user-avatar" :style="'background-image: url(' + avatar + '), url(' + require('@/assets/img/defaultAvatar.jpg') + ');'"></div>
<div class="user-meta-data">
<div class="user-name">{{ username }}</div>
<div class="user-badge user-badge-verified">
<i class="mdi mdi-check-decagram"></i>
</div>
<div class="user-badge user-badge-patreon">
<i class="mdi mdi-patreon"></i>
</div>
</div>
</div>
</div>
</div>
<div class="user-detail-actions" v-if="apiFinished">
<button class="button-report button" v-on:click="OpenReport">{{ $t('userdetail.actions.reportButton') }}</button>
</div>
<SongRow
class="song-row-user"
:title="$t('userdetail.uploaded.header')"
v-if="apiFinished && songs.length > 0">
<template v-slot:song-list>
<SongItem
v-for="song in songs"
v-bind:key="song.id"
v-bind="song" />
</template>
</SongRow>
<Loading v-if="!apiFinished" />
</section>
</template>
<script>
import { remote } from 'electron';
const { clipboard, shell } = remote;
import SSAPI from '@/modules/module.api.js';
import SongRow from '@/components/Song/SongRow.vue';
import SongItem from '@/components/Song/SongItem.vue';
import Loading from '@/components/Loading.vue';
export default {
name: 'UserDetail',
components: {
SongRow,
SongItem,
Loading
},
data: function() {
return {
apiFinished: false,
id: 0,
username: "",
isVerified: false,
isPatreon: false,
avatar: "",
songs: []
}
},
mounted: function() {
let ssapi = new SSAPI(process.env.NODE_ENV === 'development');
ssapi.getUserDetail(this.$route.params.id).then((data) => {
this.$data.id = data.data.id;
this.$data.username = data.data.username;
this.$data.isVerified = data.data.isVerified;
this.$data.isPatreon = data.data.isPatreon;
this.$data.avatar = data.data.avatar;
this.$data.songs = data.data.songs;
this.$data.apiFinished = true;
});
},
methods: {
OpenReport: function() {
shell.openExternal("https://spinsha.re/report/user/" + this.$data.id);
}
}
}
</script>
<style scoped lang="less">
.section-user-detail {
& .user-detail-background {
background-size: cover;
background-position: center;
& .user-detail-dim {
backdrop-filter: blur(10px);
background: linear-gradient(180deg, rgba(0,0,0,0.4), #212629);
& .user-detail {
padding: 50px;
display: grid;
grid-template-columns: 200px 1fr;
grid-gap: 25px;
& .user-avatar {
width: 200px;
height: 200px;
align-self: center;
background: #eee;
border-radius: 50%;
background-size: cover;
background-position: center;
}
& .user-meta-data {
display: flex;
height: 48px;
align-items: center;
& .user-name {
font-weight: bold;
font-size: 48px;
}
& .user-badge {
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
margin-left: 20px;
}
}
}
}
}
& .user-detail-actions {
padding: 50px;
padding-top: 0px;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
grid-gap: 25px;
& .button {
padding: 15px 0px;
font-size: 16px;
transition: 0.2s ease-in-out all, 0.1s ease-in-out transform;
&.button-primary {
background: #fff;
color: #222;
&:hover {
background: #fff;
color: #222;
}
}
&:hover {
background: rgba(255,255,255,0.2);
color: #fff;
opacity: 0.6;
transform: translateY(-4px);
}
&:active {
transform: translateY(-2px);
}
}
}
& .song-row-user {
display: grid;
padding: 50px;
}
}
</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