Browse Source

Add mod update support

Remake downloads queue into page

Add reset app button to settings

Fix bug where songs would be scanned twice on initial setup

Fix bug where app would crash randomly when downloading songs and mods

Fix #65
StarGazer1258 4 năm trước cách đây
mục cha
commit
0b2ffd68a4

+ 1 - 1
package-lock.json

@@ -1,6 +1,6 @@
 {
   "name": "beatdrop",
-  "version": "2.5.9-beta",
+  "version": "2.6.0-beta",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {

+ 1 - 1
package.json

@@ -2,7 +2,7 @@
   "name": "beatdrop",
   "description": "A desktop app for downloading Beat Saber songs.",
   "author": "Nathaniel Johns (StarGazer1258)",
-  "version": "2.5.9",
+  "version": "2.6.0-beta",
   "private": false,
   "license": "CC-BY-NC-SA-4.0",
   "repository": {

+ 3 - 6
public/electron.js

@@ -23,7 +23,8 @@ let launchEvents = {
     details: [],
     install: [],
     uninstall: []
-  }
+  },
+  files: []
 }
 
 ipcMain.on('launch-events', (_, event, message) => {
@@ -155,7 +156,7 @@ function handleArgs(argv, sendImmediately) {
   // handle files
   const args = argv.filter((_, i) => !(i < (isDev ? 2 : 1)))
   const { ext } = path.parse(args[0])
-  return handleFiles(args[0], ext);
+  launchEvents.files.push({ file: args[0], ext })
 }
 
 function handleBeatdrop(argv, sendImmediately){
@@ -220,10 +221,6 @@ function handleBeatdrop(argv, sendImmediately){
   }
 }
 
-function handleFiles(path, ext){
- mainWindow.webContents.send('file-open', path, ext);
-}
-
 function createWindow() {
   mainWindow = new BrowserWindow({
     width: 1080, 

+ 7 - 0
src/actions/appActions.js

@@ -0,0 +1,7 @@
+import { RESET_APP } from "./types"
+
+export const resetApp = () => dispatch => {
+  dispatch({
+    type: RESET_APP
+  })
+}

+ 66 - 41
src/actions/modActions.js

@@ -1,4 +1,4 @@
-import { SET_MOD_LIST, SET_RESOURCE, SET_LOADING, LOAD_MOD_DETAILS, INSTALL_MOD, SET_SCANNING_FOR_MODS, SET_INSTALLED_MODS, DISPLAY_WARNING, UNINSTALL_MOD, CLEAR_MODS, ADD_TO_QUEUE, UPDATE_PROGRESS, ADD_DEPENDENT, SET_MOD_ACTIVE, ADD_PENDING_MOD, SET_PATCHING } from './types'
+import { SET_MOD_LIST, SET_RESOURCE, SET_LOADING, LOAD_MOD_DETAILS, INSTALL_MOD, SET_SCANNING_FOR_MODS, SET_INSTALLED_MODS, DISPLAY_WARNING, UNINSTALL_MOD, CLEAR_MODS, ADD_TO_QUEUE, UPDATE_PROGRESS, ADD_DEPENDENT, SET_MOD_ACTIVE, ADD_PENDING_MOD, SET_PATCHING, SET_MOD_UPDATE_AVAILABLE, CLEAR_MOD_UPDATES } from './types'
 import { MODS_VIEW, MOD_DETAILS } from '../constants/views'
 
 import { BEATMODS, LIBRARY } from '../constants/resources'
@@ -130,7 +130,6 @@ export const fetchLocalMods = () => (dispatch, getState) => {
       let m = []
       for(let i = 0; i < installedMods.length; i++) {
         if(beatModsResponse.filter(mod => mod._id === installedMods[i].id)[0]) m.push(beatModsResponse.filter(mod => mod._id === installedMods[i].id)[0])
-        console.log(installedMods[i].name)
       }
       dispatch({
         type: SET_MOD_LIST,
@@ -169,7 +168,6 @@ export const fetchActivatedMods = () => (dispatch, getState) => {
       let m = []
       for(let i = 0; i < activatedMods.length; i++) {
         if(beatModsResponse.filter(mod => mod.name === activatedMods[i].name)[0]) m.push(beatModsResponse.filter(mod => mod.name === activatedMods[i].name)[0])
-        console.log(activatedMods[i].name)
       }
       dispatch({
         type: SET_MOD_LIST,
@@ -212,13 +210,10 @@ export const loadModDetails = modId => dispatch => {
 }
 
 export const installMod = (modName, version, dependencyOf = '') => (dispatch, getState) => {
-  console.log(`Asked to install ${modName}`)
   if(isModPendingInstall(modName)(dispatch, getState)) {
-    console.log(`${modName} is already pending install!`)
     return
   } else {
     if(!isModInstalled(modName)(dispatch, getState)) {
-      console.log(`${modName} isn't installed, adding it to pending installs...`)
       dispatch({
         type: ADD_PENDING_MOD,
         payload: modName
@@ -227,7 +222,6 @@ export const installMod = (modName, version, dependencyOf = '') => (dispatch, ge
   }
   if(gamePatchedWith()(dispatch, getState) === 'NONE' && !isModPendingInstall('BSIPA')(dispatch, getState)) patchGame()(dispatch, getState)
   if(isModInstalled(modName)(dispatch, getState)) {
-    console.log(`${modName} is already installed!`)
     if(!getState().mods.installedMods.filter(mod => mod.name === modName)[0].dependencyOf.includes(dependencyOf)) {
       dispatch({
         type: ADD_DEPENDENT,
@@ -239,17 +233,13 @@ export const installMod = (modName, version, dependencyOf = '') => (dispatch, ge
     }
     return
   }
-  console.log(`Fetching ${modName} from BeatMods...`)
-  fetch(`https://beatmods.com/api/v1/mod?status=approved&name=${ encodeURIComponent(modName) }&gameVersion=${ getState().settings.gameVersion }`)
+  fetch(`https://beatmods.com/api/v1/mod?status=approved&status=inactive&name=${ encodeURIComponent(modName) }&gameVersion=${ getState().settings.gameVersion }`)
     .then(res => res.json())
     .then(beatModsResponse => {
-      console.log(`Got the BeatMods response for ${ modName }`)
-      console.log(JSON.stringify(beatModsResponse))
-      if(beatModsResponse.length === 0) return
-      let latestVersion = beatModsResponse[0].version
-      let latestIndex = 0
-      for(let i = 0; i < beatModsResponse; i++) {
-        if(semver.satisfies(beatModsResponse[i].version, `^${ latestVersion }`)) {
+      let latestVersion = '0.0.0'
+      let latestIndex = -1
+      for(let i = 0; i < beatModsResponse.length; i++) {
+        if(beatModsResponse[i].name === modName && (semver.satisfies(beatModsResponse[i].version, `^${ latestVersion }`) || latestVersion === '0.0.0')) {
           latestVersion = beatModsResponse[i].version
           latestIndex = i
         }
@@ -259,7 +249,6 @@ export const installMod = (modName, version, dependencyOf = '') => (dispatch, ge
       let req
       let len = 0
       let chunks = 0
-      console.log(mod)
       dispatch({
         type: ADD_TO_QUEUE,
         payload: {
@@ -273,19 +262,15 @@ export const installMod = (modName, version, dependencyOf = '') => (dispatch, ge
 
       let dependsOn = []
       // Install Dependencies
-      console.log(`Installing dependencies for ${ modName }...`)
       for(let i = 0; i < mod.dependencies.length; i++) {
-        console.log(`You gonna install ${ mod.dependencies[i].name }?`)
         installMod(mod.dependencies[i].name, mod.dependencies[i].version, modName)(dispatch, getState)
         dependsOn.push(mod.dependencies[i].name)
       }
-      console.log(`Dependencies found: ${ dependsOn.join(', ') }`)
 
       // Install Mod
-      console.log(`Installing ${ modName }...`)
       if(mod.downloads.some(version => version.type === 'universal')) {
         req = request.get({ url: `https://beatmods.com${mod.downloads.filter(version => version.type === 'universal')[0].url}`, encoding: null }, (err, r, data) => {
-          if(r) if(err || (r.hasOwnProperty('statusCode') && r.statusCode !== 200)) {
+          if(err) {
             dispatch({
               type: DISPLAY_WARNING,
               payload: { text: `An error occured while downloading ${modName}. There may have been a connection error.
@@ -294,7 +279,11 @@ export const installMod = (modName, version, dependencyOf = '') => (dispatch, ge
             return
           }
           let zip = new AdmZip(data)
-          zip.extractAllTo(getState().settings.installationDirectory)
+          if(modName === 'BSIPA') {
+            zip.extractAllTo(getState().settings.installationDirectory)
+          } else {
+            zip.extractAllTo(path.join(getState().settings.installationDirectory, 'IPA', 'Pending'))
+          }
   
           let entries = zip.getEntries()
           let files = []
@@ -313,7 +302,6 @@ export const installMod = (modName, version, dependencyOf = '') => (dispatch, ge
               payload: false
             })
           }
-
           dispatch({
             type: INSTALL_MOD,
             payload: {
@@ -332,7 +320,7 @@ export const installMod = (modName, version, dependencyOf = '') => (dispatch, ge
         let installationType = getState().settings.installationType
         if(mod.downloads.some(version => version.type === installationType)) {
           req = request.get({ url: `https://beatmods.com${mod.downloads.filter(version => version.type === installationType)[0].url}`, encoding: null }, (err, r, data) => {
-            if(r) if(err || r.statusCode !== 200) {
+            if(err) {
               dispatch({
                 type: DISPLAY_WARNING,
                 payload: { text: `An error occured while downloading ${modName}. There may have been a connection error.
@@ -410,10 +398,12 @@ export const uninstallMod = modName => (dispatch, getState) => {
       type: UNINSTALL_MOD,
       payload: modIndex
     })
+    checkModsForUpdates()(dispatch, getState)
   }
 }
 
 export const activateMod = modName => (dispatch, getState) => {
+  checkModsForUpdates()(dispatch, getState)
   if(isModInstalled(modName)(dispatch, getState) && !isModActive(modName)(dispatch, getState)) {
     try {
       let mod = getState().mods.installedMods.filter(mod => mod.name === modName)[0]
@@ -432,6 +422,7 @@ export const activateMod = modName => (dispatch, getState) => {
 }
 
 export const deactivateMod = modName => (dispatch, getState) => {
+  checkModsForUpdates()(dispatch, getState)
   if(isModInstalled(modName)(dispatch, getState) && isModActive(modName)(dispatch, getState)) {
     let mod = getState().mods.installedMods.filter(mod => mod.name === modName)[0]
     let zip = new AdmZip()
@@ -494,6 +485,7 @@ export const checkInstalledMods = () => (dispatch, getState) => {
           type: SET_SCANNING_FOR_MODS,
           payload: false
         })
+        checkModsForUpdates()(dispatch, getState)
         return
       }
     }
@@ -563,7 +555,6 @@ export const checkInstalledMods = () => (dispatch, getState) => {
               })
             }
           }
-          console.log('Plugins Done')
           pluginsEnded = true
           checkIfDone()
         } else {
@@ -619,7 +610,6 @@ export const checkInstalledMods = () => (dispatch, getState) => {
                 })
               }
           }
-          console.log('Managed Done')
           managedEnded = true
           checkIfDone()
         } else {
@@ -640,20 +630,6 @@ export const patchGame = () => (dispatch, getState) => {
   let patchedWith = gamePatchedWith()(dispatch, getState)
   if(patchedWith === 'NONE') {
     installMod('BSIPA', '')(dispatch, getState)
-    /*
-    fetch('https://api.github.com/repos/nike4613/BeatSaber-IPA-Reloaded/releases/latest')
-      .then(res => res.json())
-      .then(latestRelease => {
-        request.get(latestRelease.assets[latestRelease.assets.findIndex(asset => asset.name === 'Release.zip')].browser_download_url, { encoding: null }, (err, r, data) => {
-          AdmZip(data).extractAllTo(getState().settings.installationDirectory)
-          execFile(path.join(getState().settings.installationDirectory, 'IPA.exe'), ['-n'], { cwd: getState().settings.installationDirectory })
-          dispatch({
-            type: 'DISPLAY_WARNING',
-            payload: { text: 'Game successfully patched with BSIPA.', color: 'lightgreen' }
-          })
-        })
-      })
-      */
   } else {
     if(patchedWith === 'BSIPA') {
       dispatch({
@@ -668,6 +644,55 @@ export const patchGame = () => (dispatch, getState) => {
   }
 }
 
+export const updateMod = modName => (dispatch, getState) => {
+  let modIndex = getState().mods.installedMods.findIndex(mod => mod.name === modName)
+  dispatch({
+    type: UNINSTALL_MOD,
+    payload: modIndex
+  })
+  installMod(modName)(dispatch, getState)
+}
+
+export const checkModsForUpdates = () => (dispatch, getState) => {
+  dispatch({
+    type: CLEAR_MOD_UPDATES
+  })
+  let installedMods = getState().mods.installedMods
+  for(let m = 0; m < installedMods.length; m++) {
+    fetch(`https://beatmods.com/api/v1/mod?status=approved&name=${ installedMods[m].name }&gameVersion=${ getState().settings.gameVersion }`)
+      .then(res => res.json())
+      .then(beatModsResponse => {
+        if(beatModsResponse.length === 0) return
+        let latestVersion = beatModsResponse[0].version
+        for(let i = 0; i < beatModsResponse; i++) {
+          if(semver.satisfies(beatModsResponse[i].version, `^${ latestVersion }`)) {
+            latestVersion = beatModsResponse[i].version
+          }
+        }
+        if(semver.gt(latestVersion, installedMods[m].version)) {
+          dispatch({
+            type: SET_MOD_UPDATE_AVAILABLE,
+            payload: {
+              modIndex: m,
+              updateAvailable: true,
+              latestVersion
+            }
+          })
+          if(getState().mods.updates === 1) {
+            dispatch({
+              type: DISPLAY_WARNING,
+              payload: {
+                text: 'Mod updates are available. Go to downloads to install updates.',
+                color: 'gold',
+                timeout: 5000
+              }
+            })
+          }
+        }
+      })
+  }
+}
+
 export const gamePatchedWith = () => (dispatch, getState) => {
   let installationType = 'NONE'
   try {

+ 0 - 1
src/actions/playlistsActions.js

@@ -297,7 +297,6 @@ export const savePlaylistDetails = details => (dispatch, getState) => {
 }
 
 export const setPlaylistEditing = isEditing => dispatch => {
-  console.log(`Editing: ${ isEditing }`)
   dispatch({
     type: SET_PLAYLIST_EDITING,
     payload: isEditing

+ 1 - 8
src/actions/queueActions.js

@@ -1,4 +1,4 @@
-import { SET_QUEUE_OPEN, ADD_TO_QUEUE, CLEAR_QUEUE, UPDATE_PROGRESS, SET_DOWNLOADED_SONGS, SET_DOWNLOADING_COUNT, SET_WAIT_LIST, DISPLAY_WARNING, SET_SCANNING_FOR_SONGS, SET_DISCOVERED_FILES, SET_PROCESSED_FILES } from './types'
+import { ADD_TO_QUEUE, CLEAR_QUEUE, UPDATE_PROGRESS, SET_DOWNLOADED_SONGS, SET_DOWNLOADING_COUNT, SET_WAIT_LIST, DISPLAY_WARNING, SET_SCANNING_FOR_SONGS, SET_DISCOVERED_FILES, SET_PROCESSED_FILES } from './types'
 import { SONG_LIST } from '../constants/views'
 import { isModInstalled, installEssentialMods } from './modActions'
 import { setView } from './viewActions'
@@ -11,13 +11,6 @@ const AdmZip = remote.require('adm-zip')
 const request = remote.require('request')
 const rimraf = remote.require('rimraf')
 
-export const setQueueOpen = open => dispatch => {
-  dispatch({
-    type: SET_QUEUE_OPEN,
-    payload: open
-  })
-}
-
 /**
  * Downloads a song.
  * @param {string} identity The hash/key of the song to download

+ 7 - 10
src/actions/settingsActions.js

@@ -1,36 +1,33 @@
-import { SET_SETTINGS_OPEN, SET_INSTALLATION_DIRECTORY, SET_AUTO_LOAD_MORE, SET_OFFLINE_MODE, SET_THEME, SET_THEME_IMAGE, SET_FOLDER_STRUCTURE, SET_UPDATE_CHANNEL, SET_LATEST_RELEASE_NOTES, SET_INSTALLATION_TYPE, SET_GAME_VERSION } from './types'
+import { SET_INSTALLATION_DIRECTORY, SET_AUTO_LOAD_MORE, SET_OFFLINE_MODE, SET_THEME, SET_THEME_IMAGE, SET_FOLDER_STRUCTURE, SET_UPDATE_CHANNEL, SET_LATEST_RELEASE_NOTES, SET_INSTALLATION_TYPE, SET_GAME_VERSION } from './types'
 
 import { checkDownloadedSongs } from './queueActions'
+import { checkInstalledMods, checkModsForUpdates } from './modActions'
 
 const { ipcRenderer } = window.require('electron')
 
-export const setSettingsOpen = isOpen => dispatch => {
-  dispatch({
-    type: SET_SETTINGS_OPEN,
-    payload: isOpen
-  })
-}
-
 export const setInstallationDirectory = directory => (dispatch, getState) => {
   dispatch({
     type: SET_INSTALLATION_DIRECTORY,
     payload: directory
   })
   checkDownloadedSongs()(dispatch, getState)
+  checkInstalledMods()(dispatch, getState)
 }
 
-export const setInstallationType = type => dispatch => {
+export const setInstallationType = type => (dispatch, getState) => {
   dispatch({
     type: SET_INSTALLATION_TYPE,
     payload: type
   })
+  checkModsForUpdates()(dispatch, getState)
 }
 
-export const setGameVersion = version => dispatch => {
+export const setGameVersion = version => (dispatch, getState) => {
   dispatch({
     type: SET_GAME_VERSION,
     payload: version
   })
+  checkModsForUpdates()(dispatch, getState)
 }
 
 export const setAutoLoadMore = willAutoLoadMore => dispatch => {

+ 5 - 4
src/actions/types.js

@@ -1,4 +1,4 @@
-//Root
+// Root
 export const RESET_APP                  = 'RESET_APP'
 
 // SongList
@@ -19,7 +19,7 @@ export const SET_PROCESSED_FILES        = 'SET_PROCESSED_FILES'
 export const SET_LOADING                = 'SET_LOADING'
 export const SET_LOADING_MORE           = 'SET_LOADING_MORE'
 
-//Mods
+// Mods
 export const SET_MOD_LIST               = 'SET_MOD_LIST'
 export const APPEND_MOD_LIST            = 'APPEND_MOD_LIST'
 export const LOAD_MOD_DETAILS           = 'LOAD_MOD_DETAILS'
@@ -33,6 +33,9 @@ export const ADD_DEPENDENT              = 'ADD_DEPENDENT'
 export const REMOVE_DEPENDENT           = 'REMOVE_DEPENDENT'
 export const SET_MOD_ACTIVE             = 'SET_MOD_ACTIVE'
 export const SET_PATCHING               = 'SET_PATCHING'
+export const SET_MOD_UPDATE_AVAILABLE   = 'SET_MOD_UPDATE_AVAILABLE'
+export const CLEAR_MOD_UPDATES              = 'CLEAR_MOD_UPDATES'
+export const UPDATE_MOD                 = 'UPDATE_MOD'
 
 // Playlists
 export const LOAD_NEW_PLAYLIST_IMAGE    = 'LOAD_NEW_PLAYLIST_IMAGE'
@@ -70,7 +73,6 @@ export const RESIZE_SIDEBAR             = 'RESIZE_SIDEBAR'
 export const SET_SECTION                = 'SET_SECTION'
 
 // Settings
-export const SET_SETTINGS_OPEN          = 'SET_SETTINGS_OPEN'
 export const SET_INSTALLATION_DIRECTORY = 'SET_INSTALLATION_DIRECTORY'
 export const SET_INSTALLATION_TYPE      = 'SET_INSTALLATION_TYPE'
 export const SET_GAME_VERSION           = 'SET_GAME_VERSION'
@@ -92,7 +94,6 @@ export const SET_SEARCH_SOURCES         = 'SET_SEARCH_SOURCES'
 export const SUBMIT_SEARCH              = 'SUBMIT_SEARCH'
 
 // Queue
-export const SET_QUEUE_OPEN             = 'SET_QUEUE_OPEN'
 export const ADD_TO_QUEUE               = 'ADD_TO_QUEUE'
 export const REMOVE_FROM_QUEUE          = 'REMOVE_FROM_QUEUE'
 export const CLEAR_QUEUE                = 'CLEAR_QUEUE'

+ 11 - 12
src/components/App.js

@@ -16,7 +16,7 @@ import { connect } from 'react-redux'
 
 import { setHasError, setErrorMessage } from '../actions/windowActions'
 import { downloadSong } from '../actions/queueActions'
-import { loadModDetails, installMod } from '../actions/modActions'
+import { loadModDetails, installMod, checkModsForUpdates } from '../actions/modActions'
 import { loadDetailsFromKey } from '../actions/detailsActions'
 import { setView } from '../actions/viewActions'
 import { fetchLocalPlaylists } from '../actions/playlistsActions'
@@ -30,6 +30,7 @@ const path = window.require('path')
 class App extends Component {
   
   componentDidMount() {
+    checkModsForUpdates()(store.dispatch, store.getState)
     ipcRenderer.send('launch-events', 'check-launch-events')
     ipcRenderer.on('launch-events', (_, event, message) => {
       switch(event) {
@@ -57,22 +58,20 @@ class App extends Component {
           for(let i = 0; i < message.mods.install.length; i++) {
             installMod(message.mods.install[i], '')(store.dispatch, store.getState)
           }
+          for(let i = 0; i < message.files.length; i++) {
+            let dir = message.files[i].file.split(path.sep)
+            let filename = dir[dir.length - 1] + message.files[i].ext
+            let newPath = path.join(store.getState().settings.installationDirectory, "Playlists", filename);
+            fs.rename(message.files[i].file, newPath, (err) => {
+              if (err) throw err;
+              fetchLocalPlaylists(true)(store.dispatch, store.getState)
+            })
+          }
           return
         default:
           return
       }
     })
-
-    // handle beatsaber playlist file opens
-    ipcRenderer.on('file-open', (_, file, ext) => {
-      let dir = file.split(path.sep)
-      let filename = dir[dir.length - 1] + ext
-      let newPath = path.join(store.getState().settings.installationDirectory, "Playlists", filename);
-      fs.rename(file, newPath, (err) => {
-        if (err) throw err;
-        fetchLocalPlaylists(true)(store.dispatch, store.getState)
-      })
-    })
   }
 
   componentDidCatch(error, info) {

+ 1 - 1
src/components/CrashMessage.js

@@ -21,7 +21,7 @@ class CrashMessage extends Component {
             <h1>Oops! BeatDrop has crashed!</h1>
             <p>If you want to help with fixing this, provide a screenshot of this message and <a href="https://github.com/StarGazer1258/BeatDrop/issues/new?assignees=&amp;labels=&amp;template=bug_report.md&amp;title=" onClick={ (e) => { e.preventDefault(); e.stopPropagation(); window.require('electron').shell.openExternal(e.target.href) } }>file a bug report in the GitHub repo</a>.<br /><b>Please search for your issue first before filing a duplicate issue.</b></p>
             <p className="errorMessage">{ this.props.errorMessage.name }{ this.props.errorInfo.componentStack }</p>
-            <p>In the meantime, you can try resetting the view back the the welcome screen. If that doesn't work, you may have to reset the application entirely. This will reset all of you preferences, but will not delete any files.</p>
+            <p>In the meantime, you can try resetting the view back the the welcome screen. If that doesn't work, you may have to reset the application entirely. This will reset all of your preferences, but will not delete any files.</p>
             <Button type="primary" onClick={ () => { this.props.setView(WELCOME); this.props.setHasError(false) } }>Reset View</Button>
             <Button type="destructive" onClick={ () => { store.dispatch({ type: RESET_APP }); this.props.setHasError(false) } }>Reset Everything</Button>
           </div>

+ 77 - 0
src/components/DownloadsView.js

@@ -0,0 +1,77 @@
+import React, { Component } from 'react'
+import '../css/DownloadsView.scss'
+
+import { connect } from 'react-redux'
+
+import Button from './Button'
+import ProgressBar from './ProgressBar'
+import { updateMod } from '../actions/modActions'
+import { clearQueue } from '../actions/queueActions'
+
+class DownloadsView extends Component {
+
+  constructor(props) {
+    super(props)
+
+    this.state = {
+      updates: []
+    }
+  }
+
+  componentDidMount() {
+    let updates = []
+    for(let i = 0; i < this.props.installedMods.length; i++) {
+      if(this.props.installedMods[i].updateAvailable) updates.push(this.props.installedMods[i])
+    }
+    this.setState({ updates })
+  }
+
+  componentDidUpdate(prevProps) {
+    if(prevProps.updates !== this.props.updates) {
+      let updates = []
+      for(let i = 0; i < this.props.installedMods.length; i++) {
+        if(this.props.installedMods[i].updateAvailable) updates.push(this.props.installedMods[i])
+      }
+      this.setState({ updates })
+    }
+  }
+
+  render() {
+      return (
+        <div id="downloads-view" className="view">
+          { 
+            this.props.updates > 0 ? 
+              <>
+                <div className="updates-title"><h1>{ this.props.updates } New Updates</h1><Button type="primary" onClick={ () => { for(let i = 0; i < this.state.updates.length; i++) { this.props.updateMod(this.state.updates[i].name) } } }>Update All</Button></div>
+                <ul className="updates-list">
+                  {
+                    this.state.updates.map((mod, i) => {
+                      return <li key={ i }>{ mod.name } { mod.version } ➜ { mod.latestVersion } <div className="right"><Button type="primary" onClick={ () => { this.props.updateMod(mod.name) } }>Update</Button><Button>Ignore</Button></div></li>
+                    })
+                  }
+                </ul>
+              </>
+              : null
+            }
+            <div className="updates-title"><h1>Downloads</h1><Button type="destructive" onClick={ () => { this.props.clearQueue() } }>Clear</Button></div>
+            <ul className="downloads-list">
+              {
+                this.props.items.length > 0 ?
+                  this.props.items.map((item, i) => {
+                    return <li key={ i }><img src={ item.image } alt={ item.title } /><div className="download-info"><div className="title">{ item.title }</div><div className="author">{ item.author }</div><span className="download-progress"><ProgressBar progress={ item.progress } /></span><span>{ item.progress < 100 ? item.progress + '%' : 'Done' }</span></div></li>
+                  })
+                : <li className="no-downloads">No Recent Downloads</li>
+              }
+            </ul>
+        </div>
+      )
+  }
+}
+
+const mapStateToProps = state => ({
+  installedMods: state.mods.installedMods,
+  updates: state.mods.updates,
+  items: state.queue.items
+})
+
+export default connect(mapStateToProps, { updateMod, clearQueue })(DownloadsView)

+ 7 - 3
src/components/ReleaseNotesModal.js

@@ -25,13 +25,17 @@ class ReleaseNotesModal extends Component {
             <h2 style={ { color: 'lightgreen' } }>What's new?</h2>
             <hr style={ { borderColor: 'lightgreen' } } />
             <ul>
-              <li>Let's all give a big <b>thank you</b> to our new Patreon Patron <b>Iryna Pavlova!</b></li>
-              <li>I'm a college student with bills to pay, can you spare some change?<br /><a href="https://www.patreon.com/bePatron?u=18487054" onClick={ (e) => { e.preventDefault(); window.require('electron').shell.openExternal('https://www.patreon.com/bePatron?u=18487054') } }>Become a Patreon Patron!</a></li>
+              <li>You can now <b>update mods</b> through the <b>revamped downloads page!</b></li>
+              <li>Did I mention we <b>remade the download queue?</b> It's now it's own page with an <b>updates section (when applicable.)</b></li>
+              <li>BeatDrop will also <b>search for mod updates periodically</b> and <b>notify you if updates are available.</b></li>
+              <li>As requested, you can now <b>reset the app from settings.</b></li>
             </ul>
             <h2 style={ { color: 'salmon' } }>What's fixed?</h2>
             <hr style={ { borderColor: 'salmon' } } />
             <ul>
-              <li><b>Mod support</b> is back baby!! I appologize for not fixing this sooner, but life can be a mess sometimes. Thank you to everyone for waiting so patiently. Now I can get back to work and more features!</li>
+              <li>Fixed bug where <b>app would crash randomly after downloading songs and mods.</b></li>
+              <li>Fixed bug where <b>songs would be scanned twice on inital setup.</b></li>
+              <li>Fixed <a href="https://github.com/StarGazer1258/BeatDrop/issues/65" onClick={ (e) => { e.preventDefault(); e.stopPropagation(); window.require('electron').shell.openExternal(e.target.href) } }>#65.</a></li>
             </ul>
             <br />
             <Button type="primary" onClick={ () => { this.props.setLatestReleaseNotes(require('../../package.json').version) } }>Awesome!</Button>

+ 13 - 5
src/components/SettingsView.js

@@ -3,7 +3,8 @@ import { connect } from 'react-redux'
 import PropTypes from 'prop-types'
 import { setInstallationDirectory, setInstallationType, setGameVersion, setAutoLoadMore, setOfflineMode, setTheme, setThemeImage, setFolderStructure, setUpdateChannel, setLatestReleaseNotes } from '../actions/settingsActions'
 import { checkDownloadedSongs } from '../actions/queueActions'
-import { checkInstalledMods } from '../actions/modActions'
+import { checkInstalledMods, checkModsForUpdates } from '../actions/modActions'
+import { resetApp } from '../actions/appActions'
 import '../css/SettingsView.scss'
 import Button from './Button'
 import Toggle from './Toggle'
@@ -86,6 +87,7 @@ class SettingsView extends Component {
         <hr />
         <h2>Downloads</h2>
         <Button onClick={ this.props.checkDownloadedSongs }>Scan for Songs</Button><Button onClick={ this.props.checkInstalledMods }>{ this.props.scanningForMods ? 'Scanning...' : 'Scan for Mods' }</Button><br /><br />
+        <Button onClick={ () => { this.props.checkModsForUpdates(true) } }>Check for Mod Updates</Button><br /><br />
         <label htmlFor="folder-structure-select">Folder Structure</label><br /><br />
         <select id="folder-structure-select" name="folder-structure-select" value={ this.props.settings.folderStructure } onChange={ (e) => { this.props.setFolderStructure(e.target.value) } }>
           <option value="keySongNameArtistName">Key ( Song Name - Song Artist )</option>
@@ -116,6 +118,11 @@ class SettingsView extends Component {
         <Button type={ this.state.updateStatus === 'error' ? 'destructive' : null } onClick={ () => { ipcRenderer.send('electron-updater', 'check-for-updates') } }>{ this.updateValue() }</Button>
         <br /><br />
         <hr />
+        <h2>DANGER ZONE</h2>
+        <i>Please don't touch these unless you know what you're doing.</i><br /><br />
+        <Button type="destructive" onClick={ () => { this.props.resetApp() } }>Reset App</Button>
+        <br /><br />
+        <hr />
         <h2>Credits</h2>
         <b>BeatDrop Developers</b><br />
         <ul>
@@ -146,16 +153,17 @@ class SettingsView extends Component {
           <li>
           <b>Wave Tier</b>
           <ul>
-            <li>Shane R. Monroe ($40)</li>
+            <li>Shane R. Monroe ($50)</li>
             <li>Carize ($30)</li>
             <li>Myles Hecht ($30)</li>
+            <li>Iryna Pavlova ($20)</li>
             <li>Marc Smith ($10)</li>
-            <li>Iryna Pavlova ($10)</li>
+            <li>Kirk Miller ($10)</li>
             <li></li>
           </ul>
           </li>
           <li>
-            <b>Hurricane Tiere</b>
+            <b>Hurricane Tier</b>
             <ul>
               <li><i>Your name here...</i></li>
             </ul>
@@ -190,6 +198,6 @@ const mapStateToProps = state => ({
   scanningForMods: state.mods.scanning
 })
 
-export default connect(mapStateToProps, { setInstallationDirectory, setInstallationType, setGameVersion, setAutoLoadMore, setOfflineMode, setTheme, setThemeImage, setFolderStructure, setUpdateChannel, setLatestReleaseNotes, checkDownloadedSongs, checkInstalledMods })(SettingsView)
+export default connect(mapStateToProps, { setInstallationDirectory, setInstallationType, setGameVersion, setAutoLoadMore, setOfflineMode, setTheme, setThemeImage, setFolderStructure, setUpdateChannel, setLatestReleaseNotes, checkDownloadedSongs, checkInstalledMods, checkModsForUpdates, resetApp })(SettingsView)
 
 //<input type="checkbox" name="auto-refresh" id="auto-refresh" checked={this.props.settings.autoRefresh} onClick={() => this.props.setAutoLoadMore(!this.props.settings.autoLoadMore)} /><label htmlFor="auto-refresh">Refresh feed every </label><input type="number" name="auto-refresh-interval" id="auto-refresh-interval"/><label htmlFor="auto-refresh-interval"> seconds</label>

+ 5 - 21
src/components/SideBar.js

@@ -4,8 +4,9 @@ import '../css/SideBar.scss'
 import { connect } from 'react-redux'
 import PropTypes from 'prop-types'
 
+import Badge from './Badge'
+
 import { setView } from '../actions/viewActions'
-import { setQueueOpen } from '../actions/queueActions'
 import { fetchNew, fetchTopDownloads, fetchTopFinished, fetchLocalSongs } from '../actions/songListActions'
 import { fetchApprovedMods, fetchRecommendedMods, fetchModCategories, fetchLocalMods, fetchActivatedMods, checkInstalledMods } from '../actions/modActions'
 import { fetchLocalPlaylists } from '../actions/playlistsActions'
@@ -16,24 +17,7 @@ import * as VIEWS from '../constants/views'
 import * as RESOURCES from '../constants/resources'
 import * as SECTIONS from '../constants/sections'
 
-const closeQueueAction = function (e) { if(!e.target.classList.contains('i-download-queue') && this.props.isQueueOpen) { this.props.setQueueOpen(false) } }
-
 class SideBar extends Component {
-
-  constructor(props) {
-    super(props)
-
-    this.closeQueueAction = closeQueueAction.bind(this)
-  }
-
-  componentDidMount() {
-    document.addEventListener('mouseup', this.closeQueueAction)
-  }
-
-  componentWillUnmount() {
-    document.removeEventListener('mouseup', this.closeQueueAction)
-  }
-
   render() {
     return (
       <div id='sidebar' className={ this.props.sidebarOpen ? '' : 'collapsed' }>
@@ -69,7 +53,7 @@ class SideBar extends Component {
         <div className="buttons-bottom">
           <div id="settings-button" className={ this.props.view === VIEWS.SETTINGS ? 'open' : '' } title="Settings" onClick={ () => this.props.setView(VIEWS.SETTINGS) }> Settings</div>
           <div id="search-button" className={ this.props.view === VIEWS.SEARCH ? 'open' : '' } title="Search" onClick={ () => this.props.setView(VIEWS.SEARCH) }> Search</div>
-          <div id="queue-button" className={ `i-download-queue${this.props.isQueueOpen ? ' open' : ''}` } title="Download Queue" onClick={ () => { this.props.setQueueOpen(!this.props.isQueueOpen) } }>Downloads</div>
+          <div id="queue-button" className={ this.props.view === VIEWS.DOWNLOADS ? 'open' : '' } title="Downloads" onClick={ () => { this.props.setView(VIEWS.DOWNLOADS) } }>Downloads{ this.props.updates > 0 ? <>&nbsp;<Badge backgroundColor="red">{ this.props.updates }</Badge></> : null }</div>
           <div id="donate-button" className={ this.props.view === VIEWS.DONATE ? 'open' : '' } title="Donate" onClick={ () => { this.props.setView(VIEWS.DONATE) } }> Donate</div>
         </div>
       </div>
@@ -100,10 +84,10 @@ const mapStateToProps = state => ({
   sidebarOpen: state.sidebar.isOpen,
   offlineMode: state.settings.offlineMode,
   section: state.sidebar.section,
-  isQueueOpen: state.queue.isOpen
+  updates: state.mods.updates
 })
 
-export default connect(mapStateToProps, { fetchNew, fetchTopDownloads, fetchTopFinished, fetchLocalSongs, fetchLocalPlaylists, fetchApprovedMods, fetchLocalMods, fetchActivatedMods, fetchRecommendedMods, fetchModCategories, setResource, resizeSidebar, setSection, checkInstalledMods, setView, setQueueOpen })(SideBar)
+export default connect(mapStateToProps, { fetchNew, fetchTopDownloads, fetchTopFinished, fetchLocalSongs, fetchLocalPlaylists, fetchApprovedMods, fetchLocalMods, fetchActivatedMods, fetchRecommendedMods, fetchModCategories, setResource, resizeSidebar, setSection, checkInstalledMods, setView })(SideBar)
 
 /*
 <li className={ `library-conflicted-mods${this.props.view === MODS_VIEW && this.props.resource === RESOURCES.LIBRARY.MODS.CONFLICTS ? ' selected' : ''}` }>Mod Packs</li>

+ 1 - 1
src/components/SongScanningDialog.js

@@ -24,7 +24,7 @@ class SongScanningDialog extends Component {
       this.state.open ?
         <Modal width={ 575 } height={ 330 } onClose={ () => { this.setState({ open: false }) } }>
           <h1 className={ `scanning-text theme-${this.props.theme}` }>{ !this.props.scanning ? `Finished scanning for songs.` : 'Scanning for songs...' }</h1>
-          <ProgressBar progress={ this.props.processedFiles / this.props.discoveredFiles * 100 } />
+          <div className="song-scanning-progress"><ProgressBar progress={ this.props.processedFiles / this.props.discoveredFiles * 100 } /></div>
           <h5 className="scanning-text">{ `${ this.props.processedFiles } / ${ this.props.discoveredFiles } Files scanned.${ !this.props.scanning ? ` | ${this.props.songs.length } songs discovered.` : '..' }` }</h5>
           { !this.props.scanning ? <h5 className="scanning-text">Click outside to exit.</h5> : null }
         </Modal>

+ 6 - 0
src/components/ViewSwitcher.js

@@ -19,6 +19,8 @@ import DonateView from './DonateView'
 import ModsView from './ModsView'
 import ModDetails from './ModDetails'
 import ModsListView from './ModsListView';
+import DiagnosticsView from './DiagnosticsView'
+import DownloadsView from './DownloadsView'
 
 function Songs(props) {
   switch(props.subView) {
@@ -55,6 +57,10 @@ function MainView(props) {
       return <PlaylistDetails />
     case VIEWS.MOD_DETAILS:
       return <ModDetails />
+    case VIEWS.DIAGNOSTICS:
+      return <DiagnosticsView />
+    case VIEWS.DOWNLOADS:
+      return <DownloadsView />
     default:
       return <Songs subView={ props.subView } />
   }

+ 2 - 2
src/components/Warning.js

@@ -40,8 +40,8 @@ class Warning extends Component {
 
   render() {
     return (
-      <div className="warning" style={ { backgroundColor: this.props.color || 'rgb(255, 128, 128)', transform: `translateY(${this.state.translateY})`, opacity: this.state.opacity } }>
-        <span>{this.props.text}</span>
+      <div className="warning" style={ { backgroundColor: this.props.color || 'rgb(255, 128, 128)', transform: `translateY(${ this.state.translateY })`, opacity: this.state.opacity } }>
+        <span>{ this.props.text }</span>
         <span className="remove-warning" onClick={ () => { this.props.removeWarning(this.props.index) } }><img src={ xIcon } alt="X"/></span>
       </div>
     )

+ 2 - 2
src/components/WelcomePage.js

@@ -26,7 +26,7 @@ class WelcomePage extends Component {
   }
 
   componentDidMount() {
-    if(this.props.settings.installationDirectory === ''   ||
+    if(this.props.settings.installationDirectory === 'Please Choose a Folder...'   ||
        this.props.settings.installationType === 'choose'  ||
        this.props.settings.installationType === undefined ||
        this.props.settings.gameVersion === 'choose'       ||
@@ -80,7 +80,7 @@ class WelcomePage extends Component {
             <br /><br />
             { this.props.settings.installationType === 'steam' && this.props.settings.installationDirectory.includes('Oculus') ? <><span style={ { fontWeight: 'bold', color: 'salmon' } }>Warning: BeatDrop has detected that you may be using the Oculus version of BeatSaber. If this is the case, please set "Installation Type" to "Oculus". Otherwise, you can ignore this message.</span><br /><br /></> : null }
             { this.props.settings.installationType === 'oculus' && this.props.settings.installationDirectory.includes('Steam') ? <><span style={ { fontWeight: 'bold', color: 'salmon' } }>Warning: BeatDrop has detected that you may be using the Steam version of BeatSaber. If this is the case, please set "Installation Type" to "Steam". Otherwise, you can ignore this message.</span><br /><br /></> : null }
-            <Button type='primary' onClick={ () => { this.props.checkDownloadedSongs(); this.props.checkInstalledMods(); this.setState({ modalOpen: false }) } } disabled={ this.props.settings.installationDirectory === '' || this.props.settings.installationType === 'choose' || this.props.settings.installationType === undefined || this.props.settings.gameVersion === 'choose' || this.props.settings.gameVersion === undefined }>Continue</Button>
+            <Button type='primary' onClick={ () => { this.setState({ modalOpen: false }) } } disabled={ this.props.settings.installationDirectory === 'Please Choose a Folder...' || this.props.settings.installationType === 'choose' || this.props.settings.installationType === undefined || this.props.settings.gameVersion === 'choose' || this.props.settings.gameVersion === undefined }>Continue</Button>
           </Modal> : null }
       </div>
     )

+ 2 - 0
src/constants/views.js

@@ -8,6 +8,8 @@ export const MOD_DETAILS      = 'MOD_DETAILS'
 export const SETTINGS         = 'SETTINGS'
 export const SEARCH           = 'SEARCH'
 export const DONATE           = 'DONATE'
+export const DIAGNOSTICS      = 'DIAGNOSTICS'
+export const DOWNLOADS        = 'DOWNLOADS'
 
 export const LIST             = 'list'
 export const COMPACT_LIST     = 'compact-list'

+ 13 - 0
src/css/App.scss

@@ -82,6 +82,19 @@ hr {
   margin-top: 20px;
 }
 
+.view {
+  height: 100%;
+  overflow-y: scroll;
+
+  padding: 0 50px;
+
+  h1 {
+    margin-top: 30px;
+    margin-bottom: 10px;
+    font-size: 32pt;
+  }
+}
+
 .hidden {
   display: none !important;
 }

+ 76 - 0
src/css/DownloadsView.scss

@@ -0,0 +1,76 @@
+#downloads-view {
+  .updates-title {
+    margin-top: 30px;
+
+    display: flex;
+    flex-flow: row nowrap;
+    justify-content: space-between;
+    align-items: center;
+
+    h1 {
+      margin: 0;
+    }
+  }
+
+  .updates-list {
+    list-style: none;
+    padding: 0;
+
+    li {
+      display: flex;
+      flex-flow: row nowrap;
+      justify-content: space-between;
+      align-items: center;
+      padding: 15px;
+      border-bottom: 1px dashed rgba(0, 0, 0, 0.3);
+    }
+  }
+
+  .downloads-list {
+    list-style: none;
+    padding: 0;
+
+    li {
+      height: 100px;
+      display: flex;
+      flex-flow: row nowrap;
+
+      img {
+        width: 80px;
+        height: 80px;
+        margin: 10px 10px;
+        border-radius: 5px;
+        position: relative;
+        filter: drop-shadow(2px 2px 3px rgba(0,0,0,0.25));
+      }
+      
+      .download-info {
+        width: calc(100% - 100px);
+      }
+
+      .title {
+        font-size: 18pt;
+        font-weight: 900;
+        margin-top: 3px;
+        margin-bottom: -3px;
+        max-width: calc(100vw - 460px);
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+      }
+
+      
+
+      .download-progress {
+        display: inline-block;
+        width: 90%;
+        margin-right: 10px;
+      }
+    }
+
+    .no-downloads {
+      color: grey;
+      margin-bottom: 30px;
+    }
+  }
+}

+ 2 - 3
src/css/ProgressBar.scss

@@ -1,10 +1,9 @@
 .progress-bar {
+  display: inline-block;
   background: silver;
   height: 10px;
   border-radius: 5px;
-  width: 80%;
-  margin: 0 10%;
-  margin-top: 50px;
+  width: 100%;
   overflow: hidden;
 
   .progress-bar-inner {

+ 5 - 0
src/css/SideBar.scss

@@ -49,6 +49,11 @@
         background-position: center center;
         flex-shrink: 0;
       }
+
+      &.open {
+        font-weight: bold;
+        color: $beatdrop-blue;
+      }
     }
 
     #settings-button {

+ 6 - 1
src/css/SongScanningDialog.scss

@@ -21,9 +21,14 @@ h5.scanning-text {
   margin: 10px 0;
 }
 
-#scanning-text.theme-dark, #scanning-text.theme-hc {
+.scanning-text.theme-dark, .scanning-text.theme-hc {
   &::before {
     background: url(../assets/dark/update.png);
     background-repeat: no-repeat;
   }
+}
+
+.song-scanning-progress {
+  margin: 0 10%;
+  margin-top: 40px;
 }

+ 19 - 1
src/reducers/modReducer.js

@@ -1,10 +1,11 @@
-import { SET_MOD_LIST, APPEND_MOD_LIST, LOAD_MOD_DETAILS, INSTALL_MOD, UNINSTALL_MOD, CLEAR_MODS, SET_INSTALLED_MODS, SET_SCANNING_FOR_MODS, SET_MOD_ACTIVE, ADD_PENDING_MOD, ADD_DEPENDENT, SET_PATCHING, REMOVE_DEPENDENT } from '../actions/types'
+import { SET_MOD_LIST, APPEND_MOD_LIST, LOAD_MOD_DETAILS, INSTALL_MOD, UNINSTALL_MOD, CLEAR_MODS, SET_INSTALLED_MODS, SET_SCANNING_FOR_MODS, SET_MOD_ACTIVE, ADD_PENDING_MOD, ADD_DEPENDENT, SET_PATCHING, REMOVE_DEPENDENT, SET_MOD_UPDATE_AVAILABLE, CLEAR_MOD_UPDATES } from '../actions/types'
 
 const initialState = {
   mods: [],
   modDetails: {},
   installedMods: [],
   pendingInstall: [],
+  updates: 0,
   scanning: false,
   patching: false
 }
@@ -49,6 +50,7 @@ export default function(state = initialState, action) {
       return removedDependantState
     case UNINSTALL_MOD:
       let uninstalledState = { ...state }
+      if(uninstalledState.installedMods[action.payload].updateAvailable) uninstalledState.updates--
       uninstalledState.installedMods.splice(action.payload, 1)
       return uninstalledState
     case ADD_PENDING_MOD:
@@ -79,6 +81,22 @@ export default function(state = initialState, action) {
         ...state,
         patching: action.payload
       }
+    case SET_MOD_UPDATE_AVAILABLE:
+      let updatedState = { ...state }
+      updatedState.installedMods[action.payload.modIndex].updateAvailable = action.payload.updateAvailable
+      if(action.payload.updateAvailable) {
+        updatedState.installedMods[action.payload.modIndex].latestVersion = action.payload.latestVersion
+        updatedState.updates++
+      }
+      return updatedState
+    case CLEAR_MOD_UPDATES:
+      let clearedUpdatesState = { ...state }
+      for(let i = 0; i < clearedUpdatesState.installedMods.length; i++) {
+        clearedUpdatesState.installedMods[i].updateAvailable = false
+        clearedUpdatesState.installedMods[i].latestVersion = clearedUpdatesState.installedMods[i].vesion
+      }
+      clearedUpdatesState.updates = 0
+      return clearedUpdatesState
     default:
       return state
   }

+ 1 - 7
src/reducers/queueReducer.js

@@ -1,17 +1,11 @@
-import { SET_QUEUE_OPEN, ADD_TO_QUEUE, CLEAR_QUEUE, UPDATE_PROGRESS } from '../actions/types'
+import { ADD_TO_QUEUE, CLEAR_QUEUE, UPDATE_PROGRESS } from '../actions/types'
 
 const initialState = {
-  isOpen: false,
   items: []
 }
 
 export default function(state = initialState, action) {
   switch(action.type) {
-    case SET_QUEUE_OPEN:
-      return {
-        ...state,
-        isOpen: action.payload
-      }
     case ADD_TO_QUEUE:
       let items = [...state.items]
       if(items.length >= 25) items.pop()

+ 2 - 8
src/reducers/settingsReducer.js

@@ -1,8 +1,7 @@
-import { SET_INSTALLATION_DIRECTORY, SET_AUTO_LOAD_MORE, SET_OFFLINE_MODE, SET_SETTINGS_OPEN, SET_THEME, SET_THEME_IMAGE, SET_FOLDER_STRUCTURE, SET_UPDATE_CHANNEL, SET_LATEST_RELEASE_NOTES, SET_INSTALLATION_TYPE, SET_GAME_VERSION } from '../actions/types'
+import { SET_INSTALLATION_DIRECTORY, SET_AUTO_LOAD_MORE, SET_OFFLINE_MODE, SET_THEME, SET_THEME_IMAGE, SET_FOLDER_STRUCTURE, SET_UPDATE_CHANNEL, SET_LATEST_RELEASE_NOTES, SET_INSTALLATION_TYPE, SET_GAME_VERSION } from '../actions/types'
 
 const initialState = {
-  isOpen: false,
-  installationDirectory: "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Beat Saber",
+  installationDirectory: "Please Choose a Folder...",
   installationType: "choose",
   gameVersion: "choose",
   autoLoadMore: true,
@@ -16,11 +15,6 @@ const initialState = {
 
 export default function(state = initialState, action) {
   switch(action.type) {
-    case SET_SETTINGS_OPEN:
-      return {
-        ...state,
-        isOpen: action.payload
-      }
     case SET_INSTALLATION_DIRECTORY:
       return {
         ...state,

+ 40 - 3
src/store.js

@@ -5,12 +5,23 @@ import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2'
 import thunk from 'redux-thunk'
 import appReducer from './reducers'
 
+import { WELCOME, SONG_LIST } from './constants/views'
+import { SONGS } from './constants/sections'
+
 import { RESET_APP } from './actions/types'
 
 const initialState = {
-  window: {
-    isMaximized: false,
-    isTranslucent: true
+  mods: {
+    mods: [],
+    modDetails: {},
+    installedMods: [],
+    pendingInstall: [],
+    updates: 0,
+    scanning: false,
+    patching: false
+  },
+  queue: {
+    items: []
   },
   songs: {
     songs: [],
@@ -23,6 +34,32 @@ const initialState = {
       beatSaver: [],
       library: []
     }
+  },
+  settings: {
+    installationDirectory: "Please Choose a Folder...",
+    installationType: "choose",
+    gameVersion: "choose",
+    autoLoadMore: true,
+    offlineMode: false,
+    theme: 'light',
+    themeImagePath: '',
+    folderStructure: 'keySongNameArtistName',
+    updateChannel: 'latest',
+    latestReleaseNotes: '0.0.0'
+  },
+  sidebar: {
+    isOpen: true,
+    section: SONGS
+  },
+  view: {
+    previousView: SONG_LIST,
+    view: WELCOME,
+    subView: 'list'
+  },
+  warnings: [],
+  window: {
+    isMaximized: false,
+    isTranslucent: true
   }
 }