diff --git a/package-lock.json b/package-lock.json
index d63fee3..295372a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "beatdrop",
- "version": "2.5.2",
+ "version": "2.5.5",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/package.json b/package.json
index 8b72e87..74748d0 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "beatdrop",
"description": "A desktop app for downloading Beat Saber songs.",
"author": "Nathaniel Johns (StarGazer1258)",
- "version": "2.5.2",
+ "version": "2.5.5",
"private": false,
"license": "CC-BY-NC-SA-4.0",
"repository": {
diff --git a/src/actions/detailsActions.js b/src/actions/detailsActions.js
index 22c4dab..6d16c41 100644
--- a/src/actions/detailsActions.js
+++ b/src/actions/detailsActions.js
@@ -2,6 +2,7 @@ import { LOAD_DETAILS, CLEAR_DETAILS, SET_DETAILS_LOADING, SET_VIEW, DISPLAY_WAR
import { SONG_DETAILS } from '../views'
import AdmZip from 'adm-zip'
+import { hashAndWriteToMetadata } from './queueActions'
const { remote } = window.require('electron')
const fs = remote.require('fs')
const path = remote.require('path')
@@ -10,7 +11,7 @@ const path = remote.require('path')
* Loads and presents the details page for a song from a file.
* @param {string} file The path to the song
*/
-export const loadDetailsFromFile = file => dispatch => {
+export const loadDetailsFromFile = file => (dispatch, getState) => {
dispatch({
type: CLEAR_DETAILS
})
@@ -27,19 +28,23 @@ export const loadDetailsFromFile = file => dispatch => {
let details = JSON.parse(data)
let dir = path.dirname(file)
details.coverURL = `file://${ path.join(dir, (details.coverImagePath || details._coverImageFilename)) }`
- details.file = path.join(dir, 'info.json' || 'info.dat')
- dispatch({
- type: LOAD_DETAILS,
- payload: details
- })
- dispatch({
- type: LOAD_DETAILS,
- payload: { audioSource: `file://${ path.join(dir, details._songFilename) }` }
- })
- dispatch({
- type: SET_DETAILS_LOADING,
- payload: false
- })
+ details.file = path.join(dir, 'info.dat')
+ hashAndWriteToMetadata(path.join(dir, 'info.dat'))(dispatch, getState)
+ .then(hash => {
+ details.hash = hash
+ dispatch({
+ type: LOAD_DETAILS,
+ payload: details
+ })
+ dispatch({
+ type: LOAD_DETAILS,
+ payload: { audioSource: `file://${ path.join(dir, details._songFilename) }` }
+ })
+ dispatch({
+ type: SET_DETAILS_LOADING,
+ payload: false
+ })
+ })
})
}
diff --git a/src/actions/playlistsActions.js b/src/actions/playlistsActions.js
index 4062574..68cdc00 100644
--- a/src/actions/playlistsActions.js
+++ b/src/actions/playlistsActions.js
@@ -1,11 +1,11 @@
import { FETCH_LOCAL_PLAYLISTS, LOAD_NEW_PLAYLIST_IMAGE, SET_NEW_PLAYLIST_OPEN, SET_PLAYLIST_PICKER_OPEN, CLEAR_PLAYLIST_DIALOG, LOAD_PLAYLIST_DETAILS, LOAD_PLAYLIST_SONGS, CLEAR_PLAYLIST_DETAILS, SET_PLAYLIST_EDITING, SET_VIEW, SET_LOADING, DISPLAY_WARNING } from './types'
import { PLAYLIST_LIST, PLAYLIST_DETAILS } from '../views'
import { defaultPlaylistIcon } from '../b64Assets'
+import { hashAndWriteToMetadata } from './queueActions';
const { remote } = window.require('electron')
const fs = remote.require('fs')
const path = remote.require('path')
-const md5 = remote.require('md5')
export const fetchLocalPlaylists = (doSetView) => (dispatch, getState) => {
let state = getState()
@@ -313,51 +313,19 @@ export const addSongToPlaylist = (song, playlistFile) => (dispatch, getState) =>
return
}
let playlist = JSON.parse(data)
- if(song.hash || song.hashMd5) {
- if(song.key) {
- playlist.songs.push({
- hash: song.hash || song.hashMd5,
- key: song.key,
- songName: song.name || song.songName
- })
- } else {
- playlist.songs.push({
- hash: song.hash || song.hashMd5,
- songName: song.name || song.songName
- })
- }
+ if(song.hash) {
+ playlist.songs.push({
+ hash: song.hash,
+ songName: song.name || song._songName
+ })
} else {
- if(song.file) {
- let file = song.file
- delete song.file
- let to_hash = ''
- for(let i = 0; i < song.difficultyLevels.length; i++) {
- try {
- to_hash += fs.readFileSync(path.join(path.dirname(file), song.difficultyLevels[i].jsonPath), 'UTF8')
- } catch(err) {
- dispatch({
- type: DISPLAY_WARNING,
- action: { text: 'Error reading difficulty level information, the song\'s files may be corrupt. Try redownloading the song and try again.' }
- })
- return
- }
- }
- let hash = md5(to_hash)
- song.hash = hash
- fs.writeFile(file, JSON.stringify(song), 'UTF8', (err) => { if(err) return })
- if(song.key) {
- playlist.songs.push({
- hash: hash,
- key: song.key,
- songName: song.name || song.songName
- })
- } else {
+ hashAndWriteToMetadata(song.file)(dispatch, getState)
+ .then(hash => {
playlist.songs.push({
- hash: hash,
- songName: song.name || song.songName
+ hash,
+ songName: song.name || song._songName
})
- }
- }
+ })
}
fs.writeFile(playlistFile, JSON.stringify(playlist), 'UTF8', (err) => {
diff --git a/src/actions/queueActions.js b/src/actions/queueActions.js
index 720f9ad..432690c 100644
--- a/src/actions/queueActions.js
+++ b/src/actions/queueActions.js
@@ -5,7 +5,7 @@ import { isModInstalled, installEssentialMods } from './modActions';
const { remote } = window.require('electron')
const fs = remote.require('fs')
const path = remote.require('path')
-const md5 = remote.require('md5')
+const crypto = remote.require('crypto')
const AdmZip = remote.require('adm-zip')
const request = remote.require('request')
const rimraf = remote.require('rimraf')
@@ -85,26 +85,12 @@ export const downloadSong = (identity) => (dispatch, getState) => {
}
let zip = new AdmZip(data)
let zipEntries = zip.getEntries()
- let infoEntry, infoObject
+ let infoEntry
for(let i = 0; i < zipEntries.length; i++) {
- if(zipEntries[i].entryName.substr(zipEntries[i].entryName.length - 9, 9) === 'info.json' || zipEntries[i].entryName.substr(zipEntries[i].entryName.length - 8, 8) === 'info.dat') {
+ if(zipEntries[i].entryName.split(path.sep).pop() === 'info.dat') {
infoEntry = zipEntries[i]
}
}
- try {
- infoObject = JSON.parse(infoEntry.getData().toString('UTF8'))
- } catch(err) {
- dispatch({
- type: DISPLAY_WARNING,
- payload: {
- text: `There was an error unpacking the song "${song.name}." The song's files may be corrupt or use formatting other than UTF-8 (Why UTF-8? The IETF says so! https://tools.ietf.org/html/rfc8259#section-8.1). Please try again and contact the song's uploader, ${song.uploader.username}, if problem persists.`
- }
- })
- return
- }
- infoObject.key = song.key
- infoObject.hash = hash
- zip.updateFile(infoEntry.entryName, JSON.stringify(infoObject))
let extractTo
switch(getState().settings.folderStructure) {
case 'keySongNameArtistName':
@@ -121,6 +107,42 @@ export const downloadSong = (identity) => (dispatch, getState) => {
break
}
zip.extractAllTo(path.join(getState().settings.installationDirectory, 'Beat Saber_Data', 'CustomLevels', extractTo))
+ let metadataFile = path.join(getState().settings.installationDirectory, 'Beat Saber_Data', 'CustomLevels', extractTo, 'metadata.dat')
+ fs.access(metadataFile, accessErr => {
+ if(accessErr) {
+ fs.writeFile(metadataFile, JSON.stringify({ key: song.key, hash: song.hash, downloadTime: utc }), err => {
+ if(err) {
+ dispatch({
+ type: DISPLAY_WARNING,
+ payload: {
+ text: `Failed to write metadata file for ${ song.name }. Go to settings and press "Scan for Songs" to try again.`
+ }
+ })
+ }
+ })
+ return
+ }
+ fs.readFile(metadataFile, 'UTF-8', (err, metadata) => {
+ if(err) {
+ dispatch({
+ type: DISPLAY_WARNING,
+ payload: {
+ text: `Failed to read metadata file for ${ song.name }. Go to settings and press "Scan for Songs" to try again.`
+ }
+ })
+ }
+ fs.writeFile(metadataFile, JSON.stringify({ ...JSON.parse(metadata), key: song.key, hash: song.hash, downloadTime: utc }), err => {
+ if(err) {
+ dispatch({
+ type: DISPLAY_WARNING,
+ payload: {
+ text: `Failed to write metadata file for ${ song.name }. Go to settings and press "Scan for Songs" to try again.`
+ }
+ })
+ }
+ })
+ })
+ })
dispatch({
type: SET_DOWNLOADED_SONGS,
payload: [...getState().songs.downloadedSongs, { hash, file: path.join(getState().settings.installationDirectory, 'Beat Saber_Data', 'CustomLevels', extractTo, infoEntry.entryName) }]
@@ -393,6 +415,65 @@ export const deleteSong = (identity) => (dispatch, getState) => {
})
}
+export const hashAndWriteToMetadata = (infoFile) => dispatch => {
+ return new Promise((resolve, reject) => {
+ let metadataFile = path.join(path.dirname(infoFile), 'metadata.dat')
+ fs.readFile(infoFile, { encoding: 'UTF-8' }, (infoReadErr, infoData) => { // Read the info.dat file
+ if(infoReadErr) {
+ dispatch({
+ type: DISPLAY_WARNING,
+ payload: {
+ text: `Failed to read info file ${ infoFile }. Go to settings and press "Scan for Songs" to try again.`
+ }
+ })
+ reject(infoReadErr)
+ }
+ let song = JSON.parse(infoData),
+ dataToHash = '',
+ fileToHash,
+ hash
+ fs.readFile(metadataFile, 'UTF-8', (readMetadataErr, metadataData) => {
+ if(readMetadataErr || !JSON.parse(metadataData).hasOwnProperty('hash')) {
+ try {
+ dataToHash += infoData
+ for(let set = 0; set < song._difficultyBeatmapSets.length; set++) {
+ for (let map = 0; map < song._difficultyBeatmapSets[set]._difficultyBeatmaps.length; map++) {
+ fileToHash = path.join(path.dirname(infoFile), song._difficultyBeatmapSets[set]._difficultyBeatmaps[map]._beatmapFilename)
+ dataToHash += fs.readFileSync(fileToHash, 'UTF8')
+ }
+ }
+ hash = crypto.createHash('sha1') // Calculate song hash
+ .update(dataToHash)
+ .digest('hex')
+ } catch(hashingErr) {
+ dispatch({
+ type: DISPLAY_WARNING,
+ payload: {
+ text: `Failed to calculate hash: ${ fileToHash } could not be accessed.`
+ }
+ })
+ reject(hashingErr)
+ }
+ fs.writeFile(metadataFile, JSON.stringify({ ...(readMetadataErr ? {} : JSON.parse(metadataData)), hash: (readMetadataErr ? hash : JSON.parse(metadataData).hash), scannedTime: Date.now() }), writeErr => { // Save metadata.dat file
+ if(writeErr) {
+ dispatch({
+ type: DISPLAY_WARNING,
+ payload: {
+ text: `Failed to write metadata file for ${ song.name }. Go to settings and press "Scan for Songs" to try again.`
+ }
+ })
+ reject(writeErr)
+ }
+ })
+ } else {
+ hash = JSON.parse(metadataData).hash
+ }
+ resolve(hash)
+ })
+ })
+ })
+}
+
export const checkDownloadedSongs = () => (dispatch, getState) => {
let discoveredFiles = 0, processedFiles = 0
dispatch({
@@ -403,7 +484,7 @@ export const checkDownloadedSongs = () => (dispatch, getState) => {
type: SET_PROCESSED_FILES,
payload: processedFiles
})
- const walk = function(pathName, cb) {
+ function walk(pathName, cb) {
let songs = []
fs.readdir(pathName, (err, files) => {
if(err) return cb(err)
@@ -427,55 +508,37 @@ export const checkDownloadedSongs = () => (dispatch, getState) => {
if(!--pending) cb(null, songs)
})
} else {
- if(files[i].toLowerCase() === 'info.dat' || files[i].toLowerCase() === 'info.json') {
- fs.readFile(file, { encoding: 'UTF-8' }, (err, data) => {
- if(err) return cb(err)
- let song = JSON.parse(data)
- if(song.hasOwnProperty('hash')) {
- songs.push({ hash: song.hash, file })
- dispatch({
- type: SET_PROCESSED_FILES,
- payload: ++processedFiles
- })
- if(!--pending) cb(null, songs)
- } else {
- let to_hash = ''
- try {
- for (let i = 0; i < song.difficultyLevels.length; i++) {
- to_hash += fs.readFileSync(path.join(path.dirname(file), song.difficultyLevels[i].jsonPath), 'UTF8')
- }
- let hash = md5(to_hash)
- song.hash = hash
- fs.writeFile(file, JSON.stringify(song), 'UTF8', (err) => { if(err) return })
+ switch(files[i].toLowerCase()) {
+ case 'info.dat': // In case of an info file
+ hashAndWriteToMetadata(file)(dispatch, getState)
+ .then(hash => {
songs.push({ hash, file })
- } catch(err) {
dispatch({
- type: DISPLAY_WARNING,
- payload: {
- text: `Failed to generate hash: a file could not be accessed.`
- }
+ type: SET_PROCESSED_FILES,
+ payload: ++processedFiles
})
- }
- dispatch({
- type: SET_PROCESSED_FILES,
- payload: ++processedFiles
+ if(!--pending) cb(null, songs)
+ }, _ => {
+ dispatch({
+ type: SET_PROCESSED_FILES,
+ payload: ++processedFiles
+ })
+ if(!--pending) cb(null, songs)
})
- if(!--pending) cb(null, songs)
- }
- })
- } else {
- dispatch({
- type: SET_PROCESSED_FILES,
- payload: ++processedFiles
- })
- if(!--pending) cb(null, songs)
+ break
+ default:
+ dispatch({
+ type: SET_PROCESSED_FILES,
+ payload: ++processedFiles
+ })
+ if(!--pending) cb(null, songs)
+ break
}
}
})
}
})
}
-
dispatch({
type: SET_SCANNING_FOR_SONGS,
payload: true
@@ -483,18 +546,18 @@ export const checkDownloadedSongs = () => (dispatch, getState) => {
walk(path.join(getState().settings.installationDirectory, 'Beat Saber_Data', 'CustomLevels'), (err, songs) => {
if (err) {
dispatch({
+ type: DISPLAY_WARNING,
+ payload: {
+ text: `Could not find CustomLevels folder. Please make sure you have your installation directory and type are set correctly.`
+ }
+ })
+ fs.mkdir(path.join(getState().settings.installationDirectory, 'Beat Saber_Data', 'CustomLevels'), () => {
+ dispatch({
type: DISPLAY_WARNING,
payload: {
- text: `Could not find Custom Levels directory. Please make sure you have your installation directory and type are set correctly.`
+ text: `Attempted to create CustomLevels folder and failed, likely due to permissions.`
}
- })
- fs.mkdir(path.join(getState().settings.installationDirectory, 'Beat Saber_Data', 'CustomLevels'), () => {
- dispatch({
- type: DISPLAY_WARNING,
- payload: {
- text: `Attempted to create Custom levels folder and failed, likely due to permissions.`
- }
- })
+ })
})
}
dispatch({
diff --git a/src/actions/songListActions.js b/src/actions/songListActions.js
index 93ff542..0bc928e 100644
--- a/src/actions/songListActions.js
+++ b/src/actions/songListActions.js
@@ -1,6 +1,7 @@
import { FETCH_NEW, FETCH_TOP_DOWNLOADS, FETCH_TOP_FINISHED, FETCH_LOCAL_SONGS, ADD_BSABER_RATING, SET_SCROLLTOP, SET_LOADING, SET_LOADING_MORE, LOAD_MORE, SET_RESOURCE, SET_VIEW, DISPLAY_WARNING } from './types'
import { SONG_LIST } from '../views'
import { BEATSAVER, LIBRARY } from '../constants/resources'
+import { hashAndWriteToMetadata } from './queueActions';
const { remote } = window.require('electron')
const fs = remote.require('fs')
@@ -178,29 +179,43 @@ export const fetchLocalSongs = () => (dispatch, getState) => {
for(let i = 0; i < downloadedSongs.length; i++) {
fs.readFile(downloadedSongs[i].file, 'UTF-8', (err, data) => {
if(err) {
+ dispatch({
+ type: DISPLAY_WARNING,
+ payload: {
+ text: `Failed to read ${ downloadedSongs[i].file }: ${ err }`
+ }
+ })
return
}
let song
try {
song = JSON.parse(data)
} catch(err) {
+ dispatch({
+ type: DISPLAY_WARNING,
+ payload: {
+ text: `Failed to parse song: ${ err }`
+ }
+ })
return
}
song.coverUrl = `file://${ path.join(path.dirname(downloadedSongs[i].file), (song.coverImagePath || song._coverImageFilename)) }`
song.file = downloadedSongs[i].file
- songs.push(song)
- console.log(song)
- if(i >= downloadedSongs.length - 1) {
- console.log('Bam!')
- dispatch({
- type: FETCH_LOCAL_SONGS,
- payload: songs
- })
- dispatch({
- type: SET_LOADING,
- payload: false
+ hashAndWriteToMetadata(downloadedSongs[i].file)(dispatch, getState)
+ .then(hash => {
+ song.hash = hash
+ songs.push(song)
+ if(i >= downloadedSongs.length - 1) {
+ dispatch({
+ type: FETCH_LOCAL_SONGS,
+ payload: songs
+ })
+ dispatch({
+ type: SET_LOADING,
+ payload: false
+ })
+ }
})
- }
})
}
}
diff --git a/src/components/PlaylistPicker.js b/src/components/PlaylistPicker.js
new file mode 100644
index 0000000..72a5287
--- /dev/null
+++ b/src/components/PlaylistPicker.js
@@ -0,0 +1,58 @@
+import React, { Component } from 'react'
+import '../css/PlaylistPicker.scss'
+
+import Button from './Button'
+
+import addIcon from '../assets/add-filled.png'
+import { defaultPlaylistIcon } from '../b64Assets'
+
+import { connect } from 'react-redux'
+import { setPlaylistPickerOpen, setNewPlaylistDialogOpen, clearPlaylistDialog, createNewPlaylist, addSongToPlaylist, loadPlaylistCoverImage } from '../actions/playlistsActions'
+
+class PlaylistPicker extends Component {
+ render() {
+ return (
+ <>
+
+
+
Add to playlist: { this.props.setPlaylistPickerOpen(false) } }>Cancel
+
+ {this.props.playlists.map((playlist, i) => {
+ return
{ this.props.addSongToPlaylist(this.props.song, playlist.file); this.props.setPlaylistPickerOpen(false) } }>
{playlist.playlistTitle}
{playlist.playlistAuthor}
{playlist.songs.length} Songs
+ })}
+
{ this.props.setNewPlaylistDialogOpen(true); this.props.setPlaylistPickerOpen(false) } }>
+
+
+
+
+
+
+
New Playlist
+
+
Cover Image (Click to Change)
+
{this.props.loadPlaylistCoverImage(e.target.files[0].path || this.props.settings.newCoverImageSource)} } />
+
+
+ Playlist Title
+
+ Playlist Author
+
+ Playlist Description
+
+ { this.props.createNewPlaylist({ playlistTitle: document.getElementById('new-playlist-title').value || 'Untitled Playlist', playlistAuthor: document.getElementById('new-playlist-author').value || 'Anonymous', playlistDescription: document.getElementById('new-playlist-description').value || 'This playlist has no description.' }); this.props.setNewPlaylistDialogOpen(false); this.props.setPlaylistPickerOpen(true); this.props.clearPlaylistDialog() } }>Create Playlist
+ { this.props.setNewPlaylistDialogOpen(false); this.props.setPlaylistPickerOpen(true); this.props.clearPlaylistDialog() } }>Cancel
+
+
+
+ >
+ )
+ }
+}
+
+const mapStateToProps = state => ({
+ playlists: state.playlists.playlists,
+ open: state.playlists.pickerOpen,
+ newPlaylistDialogOpen: state.playlists.newPlaylistDialogOpen,
+})
+
+export default connect(mapStateToProps, { setPlaylistPickerOpen, setNewPlaylistDialogOpen, clearPlaylistDialog, createNewPlaylist, addSongToPlaylist, loadPlaylistCoverImage })(PlaylistPicker)
\ No newline at end of file
diff --git a/src/components/ReleaseNotesModal.js b/src/components/ReleaseNotesModal.js
index 9b0d220..7d6fa64 100644
--- a/src/components/ReleaseNotesModal.js
+++ b/src/components/ReleaseNotesModal.js
@@ -36,6 +36,7 @@ class ReleaseNotesModal extends Component {
Fixed a bug where app would crash when moving to next song in queue after error.
2.5.1: Fixed bugs in new local song code.
2.5.2: Fixed crash when searching for songs.
+ 2.5.3: Fix { e.preventDefault(); e.stopPropagation(); window.require('electron').shell.openExternal(e.target.href) } }>#45. This is basically implemeting the new hashing calculation, so it should fix numerous issues, such as song not appearing as downloaded, songs showing the wrong leaderboards in-game, playlists not wokring properly, etc.
{ this.props.setLatestReleaseNotes(require('../../package.json').version) } }>Awesome!
diff --git a/src/components/SettingsView.js b/src/components/SettingsView.js
index 6f2c9bc..8a88265 100644
--- a/src/components/SettingsView.js
+++ b/src/components/SettingsView.js
@@ -72,7 +72,7 @@ class SettingsView extends Component {
{ this.props.setGameVersion(e.target.value) } }>
- { this.state.gameVersions.map(version => { version } ) }
+ { this.state.gameVersions.map(version => { version } ) }
@@ -113,7 +113,7 @@ class SettingsView extends Component {
Beta
{this.props.settings.updateChannel === 'beta' ? <>Warning: Beta builds are unstable, untested and may result in unexpected crashes, loss of files and other adverse effects! By updating to a beta build, you understand and accept these risks. > : null}
- { ipcRenderer.send('electron-updater', 'check-for-updates') } }>{this.updateValue()}
+ { ipcRenderer.send('electron-updater', 'check-for-updates') } }>{ this.updateValue() }
Credits
diff --git a/src/components/SongDetails.js b/src/components/SongDetails.js
index 981323a..e85567a 100644
--- a/src/components/SongDetails.js
+++ b/src/components/SongDetails.js
@@ -3,21 +3,20 @@ import '../css/SongDetails.scss'
import { connect } from 'react-redux'
import { downloadSong, deleteSong, checkDownloadedSongs } from '../actions/queueActions'
-import { setPlaylistPickerOpen, setNewPlaylistDialogOpen, clearPlaylistDialog, createNewPlaylist, addSongToPlaylist, loadPlaylistCoverImage } from '../actions/playlistsActions'
+import { setPlaylistPickerOpen } from '../actions/playlistsActions'
import { setView } from '../actions/viewActions'
import { displayWarning } from '../actions/warningActions'
import Badge from './Badge'
-import Button from './Button'
import downloadIcon from '../assets/download-filled.png'
import deleteIcon from '../assets/delete-filled.png'
import addIcon from '../assets/add-filled.png'
import moreIcon from '../assets/more-filled.png'
-import { defaultPlaylistIcon } from '../b64Assets'
import { ContextMenu, MenuItem, ContextMenuTrigger } from "react-contextmenu";
import Linkify from 'react-linkify'
+import PlaylistPicker from './PlaylistPicker';
const { shell, clipboard } = window.require('electron')
const exitDetailsShortcut = function (e) { if(e.keyCode === 27) { this.props.setView(this.props.previousView) } }
@@ -209,37 +208,7 @@ class SongDetails extends Component {
-
-
-
Add to playlist: { this.props.setPlaylistPickerOpen(false) } }>Cancel
-
- {this.props.playlists.map((playlist, i) => {
- return
{ this.props.addSongToPlaylist(this.props.details, playlist.file); this.props.setPlaylistPickerOpen(false) } }>
{playlist.playlistTitle}
{playlist.playlistAuthor}
{playlist.songs.length} Songs
- })}
-
{ this.props.setNewPlaylistDialogOpen(true); this.props.setPlaylistPickerOpen(false) } }>
-
-
-
-
-
-
-
New Playlist
-
-
Cover Image (Click to Change)
-
{this.props.loadPlaylistCoverImage(e.target.files[0].path || this.props.settings.newCoverImageSource)} } />
-
-
- Playlist Title
-
- Playlist Author
-
- Playlist Description
-
- { this.props.createNewPlaylist({ playlistTitle: document.getElementById('new-playlist-title').value || 'Untitled Playlist', playlistAuthor: document.getElementById('new-playlist-author').value || 'Anonymous', playlistDescription: document.getElementById('new-playlist-description').value || 'This playlist has no description.' }); this.props.setNewPlaylistDialogOpen(false); this.props.setPlaylistPickerOpen(true); this.props.clearPlaylistDialog() } }>Create Playlist
- { this.props.setNewPlaylistDialogOpen(false); this.props.setPlaylistPickerOpen(true); this.props.clearPlaylistDialog() } }>Cancel
-
-
-
+
)
}
@@ -250,11 +219,8 @@ const mapStateToProps = (state) => ({
queueItems: state.queue.items,
details: state.details,
previousView: state.view.previousView,
- playlistPickerOpen: state.playlists.pickerOpen,
- playlists: state.playlists.playlists,
downloadedSongs: state.songs.downloadedSongs,
- newPlaylistDialogOpen: state.playlists.newPlaylistDialogOpen,
newCoverImageSource: state.playlists.newCoverImageSource
})
-export default connect(mapStateToProps, { downloadSong, deleteSong, setView, setPlaylistPickerOpen, setNewPlaylistDialogOpen, clearPlaylistDialog, createNewPlaylist, addSongToPlaylist, loadPlaylistCoverImage, displayWarning, checkDownloadedSongs })(SongDetails)
\ No newline at end of file
+export default connect(mapStateToProps, { downloadSong, deleteSong, setView, displayWarning, checkDownloadedSongs, setPlaylistPickerOpen })(SongDetails)
\ No newline at end of file
diff --git a/src/components/SongList.js b/src/components/SongList.js
index 9d35286..d55ff6f 100644
--- a/src/components/SongList.js
+++ b/src/components/SongList.js
@@ -5,19 +5,16 @@ import '../css/SongList.scss'
import { connect } from 'react-redux'
import { loadMore } from '../actions/songListActions'
import { downloadSong, deleteSong, checkDownloadedSongs } from '../actions/queueActions'
-import { setPlaylistPickerOpen, setNewPlaylistDialogOpen, clearPlaylistDialog, createNewPlaylist, addSongToPlaylist } from '../actions/playlistsActions'
+import { setPlaylistPickerOpen } from '../actions/playlistsActions'
import { displayWarning } from '../actions/warningActions'
import SongListItem from './SongListItem'
import LoadMore from './LoadMore';
-import Button from './Button'
-
-import addIcon from '../assets/add-filled.png'
-import { defaultPlaylistIcon } from '../b64Assets'
import { ContextMenu, MenuItem, ContextMenuTrigger } from "react-contextmenu";
import { makeRenderKey } from '../utilities'
+import PlaylistPicker from './PlaylistPicker';
const { clipboard, shell } = window.require('electron')
@@ -121,37 +118,7 @@ class SongList extends Component {
)
})}
-
-
-
Add to playlist: { this.props.setPlaylistPickerOpen(false) } }>Cancel
-
- {this.props.playlists.map((playlist, i) => {
- return
{ this.props.addSongToPlaylist(this.state.song, playlist.file); this.props.setPlaylistPickerOpen(false) } }>
{playlist.playlistTitle}
{playlist.playlistAuthor}
{playlist.songs.length} Songs
- })}
-
{ this.props.setNewPlaylistDialogOpen(true); this.props.setPlaylistPickerOpen(false) } }>
-
-
-
-
-
-
-
New Playlist
-
-
Cover Image (Click to Change)
-
{this.props.loadPlaylistCoverImage(e.target.files[0].path || this.props.settings.newCoverImageSource)} } />
-
-
- Playlist Title
-
- Playlist Author
-
- Playlist Description
-
- { this.props.createNewPlaylist({ playlistTitle: document.getElementById('new-playlist-title').value || 'Untitled Playlist', playlistAuthor: document.getElementById('new-playlist-author').value || 'Anonymous', playlistDescription: document.getElementById('new-playlist-description').value || 'This playlist has no description.' }); this.props.setNewPlaylistDialogOpen(false); this.props.setPlaylistPickerOpen(true); this.props.clearPlaylistDialog() } }>Create Playlist
- { this.props.setNewPlaylistDialogOpen(false); this.props.setPlaylistPickerOpen(true); this.props.clearPlaylistDialog() } }>Cancel
-
-
-
+
)
}
@@ -169,12 +136,10 @@ SongList.propTypes = {
const mapStateToProps = state => ({
view: state.view,
songs: state.songs,
- playlists: state.playlists.playlists,
scrollTop: state.songs.scrollTop,
loading: state.loading,
loadingMore: state.loadingMore,
- autoLoadMore: state.settings.autoLoadMore,
- playlistPickerOpen: state.playlists.pickerOpen
+ autoLoadMore: state.settings.autoLoadMore
})
-export default connect(mapStateToProps, { loadMore, downloadSong, deleteSong, setPlaylistPickerOpen, setNewPlaylistDialogOpen, clearPlaylistDialog, createNewPlaylist, addSongToPlaylist, displayWarning, checkDownloadedSongs })(SongList)
\ No newline at end of file
+export default connect(mapStateToProps, { loadMore, downloadSong, deleteSong, setPlaylistPickerOpen, displayWarning, checkDownloadedSongs })(SongList)
\ No newline at end of file
diff --git a/src/css/PlaylistPicker.scss b/src/css/PlaylistPicker.scss
new file mode 100644
index 0000000..64ba7c2
--- /dev/null
+++ b/src/css/PlaylistPicker.scss
@@ -0,0 +1,192 @@
+@import './StyleConstants';
+
+#playlist-picker {
+ position: absolute;
+ top: 35px;
+ left: 0;
+ width: 100%;
+ height: calc(100% - 35px);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background: rgba(0, 0, 0, 0.5);
+ animation: playlist-picker-fade-in .2s forwards;
+
+ h1 {
+ margin-left: 25px;
+ display: inline-block;
+ }
+
+ .button {
+ float: right;
+ margin-top: 25px;
+ margin-right: 25px;
+ }
+
+ #playlist-picker-inner {
+ background: white;
+ width: 575px;
+ height: 500px;
+ max-width: 575px;
+ max-height: 500px;
+ border-radius: 5px;
+ padding: 10px;
+ opacity: 1;
+ animation: playlist-picker-drop-in .2s forwards;
+ }
+
+ #playlist-picker-table {
+ display: flex;
+ flex-flow: row wrap;
+ align-content: flex-start;
+ overflow-y: scroll;
+ height: 415px;
+ }
+
+ .playlist-picker-item {
+ width: calc(50% - 30px);
+ height: 50px;
+ list-style: none;
+ display: flex;
+ align-items: flex-start;
+ padding: 10px;
+ background: rgba(0, 0, 0, 0.1);
+ border-radius: 5px;
+ margin: 5px;
+ cursor: pointer;
+ transition: 0.2s;
+ font-size: 11pt;
+
+ img {
+ min-width: 50px;
+ min-height: 50px;
+ max-width: 50px;
+ max-height: 50px;
+ border-radius: 5px;
+ margin-right: 10px;
+ box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.25);
+ background: silver;
+ }
+
+ .playlist-picker-item-title, .playlist-picker-item-author {
+ white-space: nowrap;
+ width: 195px;
+ overflow: hidden;
+ text-overflow: ellipsis
+ }
+
+ .playlist-picker-item-title {
+ font-weight: bold;
+ }
+
+ &:hover {
+ background: rgba(0, 0, 0, 0.2);
+ }
+ }
+}
+
+#new-playlist-dialog {
+ display: flex;
+ flex-flow: row nowrap;
+ position: absolute;
+ top: calc(50% - 245px);
+ left: calc(50% - 420px);
+ width: 800px;
+ height: 450px;
+ background: white;
+ padding: 20px;
+ transform-origin: 0 0;
+ animation: new-playlist-drop-in .2s forwards ease-out;
+
+ label {
+ display: block;
+ margin-right: 5px;
+ font-weight: bold;
+ }
+
+ #new-playlist-cover-image {
+ display: none;
+ }
+
+ #new-playlist-add-cover-image {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: 32pt;
+ font-weight: bold;
+ color: white;
+ width: 200px;
+ height: 200px;
+ margin-bottom: 5px;
+ background: rgba(0, 0, 0, 0.1);
+ border-radius: 5px;
+ cursor: pointer;
+ overflow: hidden;
+
+ img {
+ width: 200px;
+ height: 200px;
+ }
+ }
+
+ #new-playlist-info {
+ margin-top: 30px;
+ margin-left: 30px;
+ }
+}
+
+.theme-dark, .theme-hc {
+ #playlist-picker-inner {
+ color: white;
+ }
+
+ .close-icon {
+ background: url(../assets/dark/close.png);
+ background-size: 25px;
+ }
+}
+
+.theme-dark {
+ #playlist-picker-inner {
+ background: $theme-dark-background-color !important;
+ }
+
+ #new-playlist-dialog {
+ background: $theme-dark-background-color;
+ }
+}
+
+.theme-hc {
+ #playlist-picker-inner {
+ background: $theme-hc-background-color !important;
+ border: 1px solid $theme-hc-line-color;
+
+ .playlist-picker-item {
+ max-width: calc(50% - 32px);
+ border: 1px solid $theme-hc-line-color;
+
+ img {
+ border: 1px solid $theme-hc-line-color;
+ }
+ }
+ }
+
+ #new-playlist-dialog {
+ background: $theme-hc-background-color;
+ border: 1px solid $theme-hc-line-color;
+ }
+}
+
+
+@keyframes playlist-picker-fade-in {
+ from {
+ background: rgba(0, 0, 0, 0);
+ }
+}
+
+@keyframes playlist-picker-drop-in {
+ from {
+ opacity: 0.0;
+ transform: translateY(-500px);
+ }
+}
\ No newline at end of file
diff --git a/src/css/SongDetails.scss b/src/css/SongDetails.scss
index bd5a000..ef6f3f2 100644
--- a/src/css/SongDetails.scss
+++ b/src/css/SongDetails.scss
@@ -18,141 +18,6 @@
}
}
- #playlist-picker {
- position: absolute;
- top: 35px;
- left: 0;
- width: 100%;
- height: calc(100% - 35px);
- display: flex;
- justify-content: center;
- align-items: center;
- background: rgba(0, 0, 0, 0.5);
- animation: playlist-picker-fade-in .2s forwards;
-
- h1 {
- margin-left: 25px;
- display: inline-block;
- }
-
- .button {
- float: right;
- margin-top: 25px;
- margin-right: 25px;
- }
-
- #playlist-picker-inner {
- background: white;
- width: 575px;
- height: 500px;
- max-width: 575px;
- max-height: 500px;
- border-radius: 5px;
- padding: 10px;
- opacity: 1;
- animation: playlist-picker-drop-in .2s forwards;
- }
-
- #playlist-picker-table {
- display: flex;
- flex-flow: row wrap;
- align-content: flex-start;
- overflow-y: scroll;
- height: 415px;
- }
-
- .playlist-picker-item {
- width: calc(50% - 30px);
- height: 50px;
- list-style: none;
- display: flex;
- align-items: flex-start;
- padding: 10px;
- background: rgba(0, 0, 0, 0.1);
- border-radius: 5px;
- margin: 5px;
- cursor: pointer;
- transition: 0.2s;
- font-size: 11pt;
-
- img {
- min-width: 50px;
- min-height: 50px;
- max-width: 50px;
- max-height: 50px;
- border-radius: 5px;
- margin-right: 10px;
- box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.25);
- background: silver;
- }
-
- .playlist-picker-item-title, .playlist-picker-item-author {
- white-space: nowrap;
- width: 195px;
- overflow: hidden;
- text-overflow: ellipsis
- }
-
- .playlist-picker-item-title {
- font-weight: bold;
- }
-
- &:hover {
- background: rgba(0, 0, 0, 0.2);
- }
- }
- }
-
- #new-playlist-dialog {
- display: flex;
- flex-flow: row nowrap;
- position: absolute;
- top: calc(50% - 245px);
- left: calc(50% - 420px);
- width: 800px;
- height: 450px;
- background: white;
- padding: 20px;
- transform-origin: 0 0;
- animation: new-playlist-drop-in .2s forwards ease-out;
-
- label {
- display: block;
- margin-right: 5px;
- font-weight: bold;
- }
-
- #new-playlist-cover-image {
- display: none;
- }
-
- #new-playlist-add-cover-image {
- display: flex;
- justify-content: center;
- align-items: center;
- font-size: 32pt;
- font-weight: bold;
- color: white;
- width: 200px;
- height: 200px;
- margin-bottom: 5px;
- background: rgba(0, 0, 0, 0.1);
- border-radius: 5px;
- cursor: pointer;
- overflow: hidden;
-
- img {
- width: 200px;
- height: 200px;
- }
- }
-
- #new-playlist-info {
- margin-top: 30px;
- margin-left: 30px;
- }
- }
-
.cover-image {
width: 15vw;
height: 15vw;
@@ -345,50 +210,7 @@
}
}
-//TODO Set correct color for each theme
-.theme-dark, .theme-hc {
- #song-details {
- #playlist-picker-inner {
- color: white;
- }
-
- .close-icon {
- background: url(../assets/dark/close.png);
- background-size: 25px;
- }
- }
-}
-
-.theme-dark {
- #playlist-picker-inner {
- background: $theme-dark-background-color !important;
- }
-
- #new-playlist-dialog {
- background: $theme-dark-background-color;
- }
-}
-
.theme-hc {
- #playlist-picker-inner {
- background: $theme-hc-background-color !important;
- border: 1px solid $theme-hc-line-color;
-
- .playlist-picker-item {
- max-width: calc(50% - 32px);
- border: 1px solid $theme-hc-line-color;
-
- img {
- border: 1px solid $theme-hc-line-color;
- }
- }
- }
-
- #new-playlist-dialog {
- background: $theme-hc-background-color;
- border: 1px solid $theme-hc-line-color;
- }
-
.action-buttons .action-button {
background-color: rgba($theme-hc-accent-color, 0.8) !important;
@@ -415,19 +237,6 @@
}
}
-@keyframes playlist-picker-fade-in {
- from {
- background: rgba(0, 0, 0, 0);
- }
-}
-
-@keyframes playlist-picker-drop-in {
- from {
- opacity: 0.0;
- transform: translateY(-500px);
- }
-}
-
@keyframes song-details-fade-in {
from {
opacity: 0;