diff --git a/.gitignore b/.gitignore
index 4055d624e6..ec05c29b64 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,7 +2,8 @@
*.slo
*.lo
*.o
-
+*.mo
+
# Compiled Dynamic libraries
*.so
@@ -40,3 +41,4 @@ Makefile
CPackConfig.cmake
CPackSourceConfig.cmake
*.cbp
+/.vs
diff --git a/CMake/Packages/FindSDL2MIXER.cmake b/CMake/Packages/FindSDL2MIXER.cmake
new file mode 100644
index 0000000000..ce18011829
--- /dev/null
+++ b/CMake/Packages/FindSDL2MIXER.cmake
@@ -0,0 +1,49 @@
+#
+# this module look for SDL2_Mixer (http://www.libsdl.org) support
+# it will define the following values
+#
+# SDLMIXER_INCLUDE_DIR = where SDL_mixer.h can be found
+# SDLMIXER_LIBRARY = the library to link against SDL2_mixer
+# SDLMIXER_FOUND = set to 1 if SDL2_mixer is found
+#
+
+IF(SDL2_Mixer_INCLUDE_DIRS)
+
+ FIND_PATH(SDLMIXER_INCLUDE_DIR SDL2/SDL_mixer.h ${SDL2_Mixer_INCLUDE_DIRS})
+ FIND_LIBRARY(SDLMIXER_LIBRARY SDL2_mixer ${SDL2_Mixer_LIBRARY_DIRS})
+
+ELSE(SDL2_Mixer_INCLUDE_DIRS)
+
+ SET(TRIAL_LIBRARY_PATHS
+ $ENV{SDL2_MIXER_HOME}/lib
+ /usr/lib
+ /usr/local/lib
+ /sw/lib
+ )
+ SET(TRIAL_INCLUDE_PATHS
+ $ENV{SDL2_MIXER_HOME}/include/SDL2
+ /usr/include/SDL2
+ /usr/local/include/SDL2
+ /sw/include/SDL2
+ )
+
+ FIND_LIBRARY(SDLMIXER_LIBRARY SDL2_mixer ${TRIAL_LIBRARY_PATHS})
+ FIND_PATH(SDLMIXER_INCLUDE_DIR SDL_mixer.h ${TRIAL_INCLUDE_PATHS})
+
+ENDIF(SDL2_Mixer_INCLUDE_DIRS)
+
+IF(SDLMIXER_INCLUDE_DIR AND SDLMIXER_LIBRARY)
+ SET(SDLMIXER_FOUND 1 CACHE BOOL "Found SDL2_Mixer library")
+ELSE(SDLMIXER_INCLUDE_DIR AND SDLMIXER_LIBRARY)
+ SET(SDLMIXER_FOUND 0 CACHE BOOL "Not fount SDL2_Mixer library")
+ENDIF(SDLMIXER_INCLUDE_DIR AND SDLMIXER_LIBRARY)
+
+MARK_AS_ADVANCED(
+ SDLMIXER_INCLUDE_DIR
+ SDLMIXER_LIBRARY
+ SDLMIXER_FOUND
+)
+
+INCLUDE(FindPackageHandleStandardArgs)
+
+FIND_PACKAGE_HANDLE_STANDARD_ARGS(SDLMIXER REQUIRED_VARS SDLMIXER_INCLUDE_DIR SDLMIXER_LIBRARY)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index a167a808fb..96e9b418a4 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -57,6 +57,7 @@ endif()
find_package(Freetype REQUIRED)
find_package(FreeImage REQUIRED)
find_package(SDL2 REQUIRED)
+find_package(SDL2MIXER REQUIRED)
find_package(CURL REQUIRED)
find_package(VLC REQUIRED)
find_package(RapidJSON REQUIRED)
@@ -66,6 +67,11 @@ if(CEC)
find_package(libCEC REQUIRED)
endif()
+# i18n
+# if(MSVC)
+# find_package(Intl REQUIRED)
+# endif()
+
#add ALSA for Linux
if(${CMAKE_SYSTEM_NAME} MATCHES "Linux")
find_package(ALSA REQUIRED)
@@ -125,6 +131,7 @@ set(COMMON_INCLUDE_DIRS
${FREETYPE_INCLUDE_DIRS}
${FreeImage_INCLUDE_DIRS}
${SDL2_INCLUDE_DIR}
+ ${SDLMIXER_INCLUDE_DIR}
${CURL_INCLUDE_DIR}
${VLC_INCLUDE_DIR}
${RAPIDJSON_INCLUDE_DIRS}
@@ -132,6 +139,12 @@ set(COMMON_INCLUDE_DIRS
${CMAKE_CURRENT_SOURCE_DIR}/es-core/src
)
+# if(MSVC)
+# LIST(APPEND COMMON_INCLUDE_DIRS
+# ${Intl_INCLUDE_DIRS}
+# )
+# endif()
+
#add libCEC_INCLUDE_DIR
if(DEFINED libCEC_FOUND)
LIST(APPEND COMMON_INCLUDE_DIRS
@@ -184,14 +197,21 @@ endif()
set(COMMON_LIBRARIES
${FREETYPE_LIBRARIES}
- ${FreeImage_LIBRARIES}
+ ${FreeImage_LIBRARIES}
${SDL2_LIBRARY}
+ ${SDLMIXER_LIBRARY}
${CURL_LIBRARIES}
${VLC_LIBRARIES}
pugixml
nanosvg
)
+# if(MSVC)
+# LIST(APPEND COMMON_LIBRARIES
+# ${Intl_LIBRARIES}
+# )
+# endif()
+
#add libCEC_LIBRARIES
if(DEFINED libCEC_FOUND)
if(DEFINED BCMHOST)
diff --git a/README.md b/README.md
index 7543220f61..93b9438830 100644
--- a/README.md
+++ b/README.md
@@ -1,218 +1,350 @@
-EmulationStation
-================
-
-This is a fork of EmulationStation for RetroPie.
-EmulationStation is a cross-platform graphical front-end for emulators with controller navigation.
-
-Building
-========
-
-EmulationStation uses some C++11 code, which means you'll need to use at least g++-4.7 on Linux, or VS2010 on Windows, to compile.
-
-EmulationStation has a few dependencies. For building, you'll need CMake, SDL2, FreeImage, FreeType, cURL and RapidJSON. You also should probably install the `fonts-droid` package which contains fallback fonts for Chinese/Japanese/Korean characters, but ES will still work fine without it (this package is only used at run-time).
-
-**On Debian/Ubuntu:**
-All of this be easily installed with `apt-get`:
-```bash
-sudo apt-get install libsdl2-dev libfreeimage-dev libfreetype6-dev libcurl4-openssl-dev rapidjson-dev \
- libasound2-dev libgl1-mesa-dev build-essential cmake fonts-droid-fallback libvlc-dev \
- libvlccore-dev vlc-bin
-```
-**On Fedora:**
-All of this be easily installed with `dnf` (with rpmfusion activated) :
-```bash
-sudo dnf install SDL2-devel freeimage-devel freetype-devel curl-devel \
- alsa-lib-devel mesa-libGL-devel cmake \
- vlc-devel rapidjson-devel
-```
-
-Note this Repository uses a git submodule - to checkout the source and all submodules, use
-
-```bash
-git clone --recursive https://github.com/RetroPie/EmulationStation.git
-```
-
-or
-
-```bash
-git clone https://github.com/RetroPie/EmulationStation.git
-cd EmulationStation
-git submodule update --init
-```
-
-Then, generate and build the Makefile with CMake:
-```bash
-cd YourEmulationStationDirectory
-cmake .
-make
-```
-
-**On the Raspberry Pi:**
-
-Complete Raspberry Pi build instructions at [emulationstation.org](http://emulationstation.org/gettingstarted.html#install_rpi_standalone).
-
-**On Windows:**
-
-[FreeImage](http://downloads.sourceforge.net/freeimage/FreeImage3154Win32.zip)
-
-[FreeType2](http://download.savannah.gnu.org/releases/freetype/freetype-2.4.9.tar.bz2) (you'll need to compile)
-
-[SDL2](http://www.libsdl.org/release/SDL2-devel-2.0.8-VC.zip)
-
-[cURL](http://curl.haxx.se/download.html) (you'll need to compile or get the pre-compiled DLL version)
-
-[RapisJSON](https://github.com/tencent/rapidjson) (you'll need the `include/rapidsjon` added to the include path)
-
-(Remember to copy necessary .DLLs into the same folder as the executable: probably FreeImage.dll, freetype6.dll, SDL2.dll, libcurl.dll, and zlib1.dll. Exact list depends on if you built your libraries in "static" mode or not.)
-
-[CMake](http://www.cmake.org/cmake/resources/software.html) (this is used for generating the Visual Studio project)
-
-(If you don't know how to use CMake, here are some hints: run cmake-gui and point it at your EmulationStation folder. Point the "build" directory somewhere - I use EmulationStation/build. Click configure, choose "Visual Studio [year] Project", fill in red fields as they appear and keep clicking Configure (you may need to check "Advanced"), then click Generate.)
-
-
-Configuring
-===========
-
-**~/.emulationstation/es_systems.cfg:**
-When first run, an example systems configuration file will be created at `~/.emulationstation/es_systems.cfg`. `~` is `$HOME` on Linux, and `%HOMEPATH%` on Windows. This example has some comments explaining how to write the configuration file. See the "Writing an es_systems.cfg" section for more information.
-
-**Keep in mind you'll have to set up your emulator separately from EmulationStation!**
-
-**~/.emulationstation/es_input.cfg:**
-When you first start EmulationStation, you will be prompted to configure an input device. The process is thus:
-
-1. Hold a button on the device you want to configure. This includes the keyboard.
-
-2. Press the buttons as they appear in the list. Some inputs can be skipped by holding any button down for a few seconds (e.g. page up/page down).
-
-3. You can review your mappings by pressing up and down, making any changes by pressing A.
-
-4. Choose "SAVE" to save this device and close the input configuration screen.
-
-The new configuration will be added to the `~/.emulationstation/es_input.cfg` file.
-
-**Both new and old devices can be (re)configured at any time by pressing the Start button and choosing "CONFIGURE INPUT".** From here, you may unplug the device you used to open the menu and plug in a new one, if necessary. New devices will be appended to the existing input configuration file, so your old devices will remain configured.
-
-**If your controller stops working, you can delete the `~/.emulationstation/es_input.cfg` file to make the input configuration screen re-appear on next run.**
-
-
-You can use `--help` or `-h` to view a list of command-line options. Briefly outlined here:
-```
---resolution [width] [height] try and force a particular resolution
---gamelist-only skip automatic game search, only read from gamelist.xml
---ignore-gamelist ignore the gamelist (useful for troubleshooting)
---draw-framerate display the framerate
---no-exit don't show the exit option in the menu
---no-splash don't show the splash screen
---debug more logging, show console on Windows
---scrape scrape using command line interface
---windowed not fullscreen, should be used with --resolution
---vsync [1/on or 0/off] turn vsync on or off (default is on)
---max-vram [size] Max VRAM to use in Mb before swapping. 0 for unlimited
---force-kid Force the UI mode to be Kid
---force-kiosk Force the UI mode to be Kiosk
---force-disable-filters Force the UI to ignore applied filters in gamelist
---help, -h summon a sentient, angry tuba
-```
-
-As long as ES hasn't frozen, you can always press F4 to close the application.
-
-
-Writing an es_systems.cfg
-=========================
-
-Complete configuration instructions at [emulationstation.org](http://emulationstation.org/gettingstarted.html#config).
-
-The `es_systems.cfg` file contains the system configuration data for EmulationStation, written in XML. This tells EmulationStation what systems you have, what platform they correspond to (for scraping), and where the games are located.
-
-ES will check two places for an es_systems.cfg file, in the following order, stopping after it finds one that works:
-* `~/.emulationstation/es_systems.cfg`
-* `/etc/emulationstation/es_systems.cfg`
-
-The order EmulationStation displays systems reflects the order you define them in.
-
-**NOTE:** A system *must* have at least one game present in its "path" directory, or ES will ignore it! If no valid systems are found, ES will report an error and quit!
-
-Here's an example es_systems.cfg:
-
-```xml
-
-
-
-
-
-
- snes
-
-
- Super Nintendo Entertainment System
-
-
- ~/roms/snes
-
-
- .smc .sfc .SMC .SFC
-
-
- snesemulator %ROM%
-
-
-
- snes
-
-
- snes
-
-
-```
-
-The following "tags" are replaced by ES in launch commands:
-
-`%ROM%` - Replaced with absolute path to the selected ROM, with most Bash special characters escaped with a backslash.
-
-`%BASENAME%` - Replaced with the "base" name of the path to the selected ROM. For example, a path of "/foo/bar.rom", this tag would be "bar". This tag is useful for setting up AdvanceMAME.
-
-`%ROM_RAW%` - Replaced with the unescaped, absolute path to the selected ROM. If your emulator is picky about paths, you might want to use this instead of %ROM%, but enclosed in quotes.
-
-See [SYSTEMS.md](SYSTEMS.md) for some live examples in EmulationStation.
-
-gamelist.xml
-============
-
-The gamelist.xml file for a system defines metadata for games, such as a name, image (like a screenshot or box art), description, release date, and rating.
-
-If at least one game in a system has an image specified, ES will use the detailed view for that system (which displays metadata alongside the game list).
-
-*You can use ES's [scraping](http://en.wikipedia.org/wiki/Web_scraping) tools to avoid creating a gamelist.xml by hand.* There are two ways to run the scraper:
-
-* **If you want to scrape multiple games:** press start to open the menu and choose the "SCRAPER" option. Adjust your settings and press "SCRAPE NOW".
-* **If you just want to scrape one game:** find the game on the game list in ES and press select. Choose "EDIT THIS GAME'S METADATA" and then press the "SCRAPE" button at the bottom of the metadata editor.
-
-You can also edit metadata within ES by using the metadata editor - just find the game you wish to edit on the gamelist, press Select, and choose "EDIT THIS GAME'S METADATA."
-
-A command-line version of the scraper is also provided - just run emulationstation with `--scrape` *(currently broken)*.
-
-The switch `--ignore-gamelist` can be used to ignore the gamelist and force ES to use the non-detailed view.
-
-If you're writing a tool to generate or parse gamelist.xml files, you should check out [GAMELISTS.md](GAMELISTS.md) for more detailed documentation.
-
-
-Themes
-======
-
-By default, EmulationStation looks pretty ugly. You can fix that. If you want to know more about making your own themes (or editing existing ones), read [THEMES.md](THEMES.md)!
-
-I've put some themes up for download on my EmulationStation webpage: http://aloshi.com/emulationstation#themes
-
-If you're using RetroPie, you should already have a nice set of themes automatically installed!
-
-
--Alec "Aloshi" Lofquist
-http://www.aloshi.com
-http://www.emulationstation.org
+EmulationStation FCAMOD
+=======================
+
+This is a fork of EmulationStation containing many additions.
+This has been primary developped for Windows platform, but can be compiled for Linux & Raspberry Pi.
+
+Changes in my branch
+====================
+
+**System list :**
+- Support for Multiple Emulators/Cores in es_systems.cfg, and setting Emulator/Core per game.
+
+ ```xml
+ %HOME%\RetroArch\retroarch.exe -L %HOME%\RetroArch\cores\%CORE%_libretro.dll %ROM%
+
+
+
+ mame2003_plus
+ mame2003
+
+
+
+
+ fbalpha2012
+
+
+
+ ```
+**Grid view :**
+- Animations when size changes and during scrolling.
+- Supports having a label.
+ ```xml
+
+ 969A9E
+ 1 0.18
+
+
+ F6FAFF
+
+ ```
+- Layout can be defined by number of columns and rows ( you had to calculate manually the size of tiles in previous versions ). Zooming the selected item can also be defined simply.
+ ```xml
+
+ 4 3
+ 1.04
+ ```
+- Supports extended padding (top, left, bottom, right) :
+ ```xml
+
+ 0.03 0.13 0.03 0.08
+ ```
+
+- Supports video in the selected item (delay can be defined in the theme)
+ ```xml
+
+ 700
+ ```
+
+- Theme can define which image to use (image, thumbnail or marquee).
+ ```xml
+
+ marquee
+ ```
+
+- Theme can define the image sizing mode (minSize, maxSize or size). Gridtile items can define a padding.
+ ```xml
+
+ 24 24
+ minSize
+ ```
+
+- Supports md_image, md_video, md_name items... just like detailed view.
+- Ability to override grid size by system.
+
+**Detailed view :**
+- Supports md_video, md_marquee items like video view did : Video view is no longer useful.
+
+**Custom views & Theming:**
+- Allow creation of custom views, which inherits from one of the basic theme items ( basic, detailed, grid ).
+ ```xml
+
+
+ ```
+- Ability to select the view (or customview) to use globally or by system.
+- The theme can force the default view to use ( attribute defaultView )
+- Fully supports Retropie & Recalbox Themes.
+- Carousel supports element "logoPos" : this allows the logo not to be inevitably centered.
+- Image loading : the image bytes where duplicated 3 times in memory.
+- In previous versions, if a xml element was unknown in the theme, nothing was loaded.
+- Support for glows around text
+- Reflection for images ( table reflection effect )
+- Gradients for selected menu and list items.
+
+**Optimizations & Fixes:**
+- Faster loading time, using multithreading.
+- Optimized memory usage for files and gamelists.
+- The loading sequence displays a progress bar.
+- Reviewed SVG loading and size calculation mecanism. Previous versions unloaded/reloaded SVGs each time a new container needed to display it because of a size calculation problem.
+- Ability to disable "Preload UI" mecanism. This mecanism is used to preload the UI of gamelists of every system. Disable it adds a small lags when opening
+- Don't keep in memory the cache of image filenames when launching games -> It takes a lot of memory for nothing.
+- Skip parsing 'downloaded_images' and 'media' folders ( better loading time )
+- Added option "Optimize images Vram Use" : Don't load an image in it source resolution if it needs to be displayed smaller -> Resize images in memory to save VRAM. Introduce longer image loading time, but less VRAM use.
+- Fixed video starting : Videos started fading even if the video was not available yet ( but not really fading : there was no blending ).
+- Software clipping : Avoid rendering clipped items -> They were previously clipped by OpenGl scissors.
+- Carousel animation was corrupted if the carousel has to display only one item with 1
+- Font : Optimization when calculating text extend.
+- If XML writer fails, the gamelist.xml file become empty and set to 0Kb -> Added a mecanism to secure that. Also, previous gamelist.xml version is saved as gamelist.xml.old.
+
+**Menus :**
+- Cleaned menus + changed menu item order (by interest).
+- Full support for menu Theming.
+- Separated "Transition style" and "Game launch transition"
+- Added option "Boot on gamelist"
+- Added option "Hide system view"
+- Added option "Display favorites first in gamelist"
+
+**General :**
+- Localisation (French actually supported)
+- OSK : On-screen Keyboard.
+- Video elements can be added as extras.
+- Fixed : Don't show Games what are marked Hidden in gamelist.
+- Added a star icon before the name of the game when it is a favorite.
+- Corrected favorites ( and custom lists ) management.
+- Don't show Directories that contains only one Game : just Show the game.
+- Case insensitive file extensions.
+- Stop using "sortname" in gamelists. It is useful.
+
+**Windows specific :**
+- Natively portable. If file ".emulationstation/es_systems.cfg" relative to the exe folder.
+- Simplified "Quit" menu item ( no more popup asking to restart or turn off Windows )
+- Windows is now "Windowed No border" by default. On Windows, Exclusive fullscreen can be annoying...
+- Stop using _wsystem for launching games. Run games with ShellExecuteEx instead ( avoids command window )
+- Add an option to leave ES open with a black screen "Loading..." when launching games ( avoids showing windows desktop )
+- Don't load all fields in Medadata Editor ( too tricky to use on windows, better use an external tool ).
+- With some Nvidia GPUs when VSYNC is active, SDL_GL_SwapWindow takes a lot of CPU : Introduce a smart calculation based on display frequency to reduce the time SDL_GL_SwapWindow has to wait. This saves a lot of CPU load.
+
+Je crois que c'est ร peu prรจs tout...
+
+Building
+========
+
+EmulationStation uses some C++11 code, which means you'll need to use at least g++-4.7 on Linux, or VS2010 on Windows, to compile.
+
+EmulationStation has a few dependencies. For building, you'll need CMake, SDL2, FreeImage, FreeType, cURL and RapidJSON. You also should probably install the `fonts-droid` package which contains fallback fonts for Chinese/Japanese/Korean characters, but ES will still work fine without it (this package is only used at run-time).
+
+**On Debian/Ubuntu:**
+All of this be easily installed with `apt-get`:
+```bash
+sudo apt-get install libsdl2-dev libfreeimage-dev libfreetype6-dev libcurl4-openssl-dev rapidjson-dev \
+ libasound2-dev libgl1-mesa-dev build-essential cmake fonts-droid-fallback libvlc-dev \
+ libvlccore-dev vlc-bin
+```
+**On Fedora:**
+All of this be easily installed with `dnf` (with rpmfusion activated) :
+```bash
+sudo dnf install SDL2-devel freeimage-devel freetype-devel curl-devel \
+ alsa-lib-devel mesa-libGL-devel cmake \
+ vlc-devel rapidjson-devel
+```
+
+Note this Repository uses a git submodule - to checkout the source and all submodules, use
+
+```bash
+git clone --recursive https://github.com/RetroPie/EmulationStation.git
+```
+
+or
+
+```bash
+git clone https://github.com/RetroPie/EmulationStation.git
+cd EmulationStation
+git submodule update --init
+```
+
+Then, generate and build the Makefile with CMake:
+```bash
+cd YourEmulationStationDirectory
+cmake .
+make
+```
+
+**On the Raspberry Pi:**
+
+Complete Raspberry Pi build instructions at [emulationstation.org](http://emulationstation.org/gettingstarted.html#install_rpi_standalone).
+
+**On Windows:**
+
+[FreeImage](http://downloads.sourceforge.net/freeimage/FreeImage3154Win32.zip)
+
+[FreeType2](http://download.savannah.gnu.org/releases/freetype/freetype-2.4.9.tar.bz2) (you'll need to compile)
+
+[SDL2](http://www.libsdl.org/release/SDL2-devel-2.0.8-VC.zip)
+
+[cURL](http://curl.haxx.se/download.html) (you'll need to compile or get the pre-compiled DLL version)
+
+[RapisJSON](https://github.com/tencent/rapidjson) (you'll need the `include/rapidsjon` added to the include path)
+
+(Remember to copy necessary .DLLs into the same folder as the executable: probably FreeImage.dll, freetype6.dll, SDL2.dll, libcurl.dll, and zlib1.dll. Exact list depends on if you built your libraries in "static" mode or not.)
+
+[CMake](http://www.cmake.org/cmake/resources/software.html) (this is used for generating the Visual Studio project)
+
+(If you don't know how to use CMake, here are some hints: run cmake-gui and point it at your EmulationStation folder. Point the "build" directory somewhere - I use EmulationStation/build. Click configure, choose "Visual Studio [year] Project", fill in red fields as they appear and keep clicking Configure (you may need to check "Advanced"), then click Generate.)
+
+
+Configuring
+===========
+
+**~/.emulationstation/es_systems.cfg:**
+When first run, an example systems configuration file will be created at `~/.emulationstation/es_systems.cfg`. `~` is `$HOME` on Linux, and `%HOMEPATH%` on Windows. This example has some comments explaining how to write the configuration file. See the "Writing an es_systems.cfg" section for more information.
+
+**Keep in mind you'll have to set up your emulator separately from EmulationStation!**
+
+**~/.emulationstation/es_input.cfg:**
+When you first start EmulationStation, you will be prompted to configure an input device. The process is thus:
+
+1. Hold a button on the device you want to configure. This includes the keyboard.
+
+2. Press the buttons as they appear in the list. Some inputs can be skipped by holding any button down for a few seconds (e.g. page up/page down).
+
+3. You can review your mappings by pressing up and down, making any changes by pressing A.
+
+4. Choose "SAVE" to save this device and close the input configuration screen.
+
+The new configuration will be added to the `~/.emulationstation/es_input.cfg` file.
+
+**Both new and old devices can be (re)configured at any time by pressing the Start button and choosing "CONFIGURE INPUT".** From here, you may unplug the device you used to open the menu and plug in a new one, if necessary. New devices will be appended to the existing input configuration file, so your old devices will remain configured.
+
+**If your controller stops working, you can delete the `~/.emulationstation/es_input.cfg` file to make the input configuration screen re-appear on next run.**
+
+
+You can use `--help` or `-h` to view a list of command-line options. Briefly outlined here:
+```
+--resolution [width] [height] try and force a particular resolution
+--gamelist-only skip automatic game search, only read from gamelist.xml
+--ignore-gamelist ignore the gamelist (useful for troubleshooting)
+--draw-framerate display the framerate
+--no-exit don't show the exit option in the menu
+--no-splash don't show the splash screen
+--debug more logging, show console on Windows
+--scrape scrape using command line interface
+--windowed not fullscreen, may be used with --resolution
+--vsync [1/on or 0/off] turn vsync on or off (default is on)
+--max-vram [size] Max VRAM to use in Mb before swapping. 0 for unlimited
+--force-kid Force the UI mode to be Kid
+--force-kiosk Force the UI mode to be Kiosk
+--force-disable-filters Force the UI to ignore applied filters in gamelist
+--help, -h summon a sentient, angry tuba
+```
+
+As long as ES hasn't frozen, you can always press F4 to close the application.
+
+
+Writing an es_systems.cfg
+=========================
+
+Complete configuration instructions at [emulationstation.org](http://emulationstation.org/gettingstarted.html#config).
+
+The `es_systems.cfg` file contains the system configuration data for EmulationStation, written in XML. This tells EmulationStation what systems you have, what platform they correspond to (for scraping), and where the games are located.
+
+ES will check two places for an es_systems.cfg file, in the following order, stopping after it finds one that works:
+* `~/.emulationstation/es_systems.cfg`
+* `/etc/emulationstation/es_systems.cfg`
+
+The order EmulationStation displays systems reflects the order you define them in.
+
+**NOTE:** A system *must* have at least one game present in its "path" directory, or ES will ignore it! If no valid systems are found, ES will report an error and quit!
+
+Here's an example es_systems.cfg:
+
+```xml
+
+
+
+
+
+
+ snes
+
+
+ Super Nintendo Entertainment System
+
+
+ ~/roms/snes
+
+
+ .smc .sfc .SMC .SFC
+
+
+ snesemulator %ROM%
+
+
+
+ snes
+
+
+ snes
+
+
+```
+
+The following "tags" are replaced by ES in launch commands:
+
+`%ROM%` - Replaced with absolute path to the selected ROM, with most Bash special characters escaped with a backslash.
+
+`%BASENAME%` - Replaced with the "base" name of the path to the selected ROM. For example, a path of "/foo/bar.rom", this tag would be "bar". This tag is useful for setting up AdvanceMAME.
+
+`%ROM_RAW%` - Replaced with the unescaped, absolute path to the selected ROM. If your emulator is picky about paths, you might want to use this instead of %ROM%, but enclosed in quotes.
+
+See [SYSTEMS.md](SYSTEMS.md) for some live examples in EmulationStation.
+
+gamelist.xml
+============
+
+The gamelist.xml file for a system defines metadata for games, such as a name, image (like a screenshot or box art), description, release date, and rating.
+
+If at least one game in a system has an image specified, ES will use the detailed view for that system (which displays metadata alongside the game list).
+
+*You can use ES's [scraping](http://en.wikipedia.org/wiki/Web_scraping) tools to avoid creating a gamelist.xml by hand.* There are two ways to run the scraper:
+
+* **If you want to scrape multiple games:** press start to open the menu and choose the "SCRAPER" option. Adjust your settings and press "SCRAPE NOW".
+* **If you just want to scrape one game:** find the game on the game list in ES and press select. Choose "EDIT THIS GAME'S METADATA" and then press the "SCRAPE" button at the bottom of the metadata editor.
+
+You can also edit metadata within ES by using the metadata editor - just find the game you wish to edit on the gamelist, press Select, and choose "EDIT THIS GAME'S METADATA."
+
+A command-line version of the scraper is also provided - just run emulationstation with `--scrape` *(currently broken)*.
+
+The switch `--ignore-gamelist` can be used to ignore the gamelist and force ES to use the non-detailed view.
+
+If you're writing a tool to generate or parse gamelist.xml files, you should check out [GAMELISTS.md](GAMELISTS.md) for more detailed documentation.
+
+
+Themes
+======
+
+By default, EmulationStation looks pretty ugly. You can fix that. If you want to know more about making your own themes (or editing existing ones), read [THEMES.md](THEMES.md)!
+
+I've put some themes up for download on my EmulationStation webpage: http://aloshi.com/emulationstation#themes
+
+If you're using RetroPie, you should already have a nice set of themes automatically installed!
+
+
+-Alec "Aloshi" Lofquist
+http://www.aloshi.com
+http://www.emulationstation.org
diff --git a/THEMES.md b/THEMES.md
index c10b2e57d0..f5e9fe6634 100644
--- a/THEMES.md
+++ b/THEMES.md
@@ -69,8 +69,8 @@ How it works
Everything must be inside a `` tag.
-**The `` tag *must* be specified**. This is the version of the theming system the theme was designed for. The current version is 3.
-
+**The `` tag *must* be specified**. This is the version of the theming system the theme was designed for.
+The current version is 4.
A *view* can be thought of as a particular "screen" within EmulationStation. Views are defined like this:
@@ -109,6 +109,14 @@ Or, you can create your own elements by adding `extra="true"` (as is done in the
ValueHere
```
+A *customView* can be thought of as a particular "screen" within EmulationStation.
+Custom Views must inherit one of the standard views and are defined like this:
+
+```xml
+
+ ... define elements here ...
+
+```
@@ -124,7 +132,7 @@ You can include theme files within theme files, similar to `#include` in C (thou
`~/.emulationstation/all_themes.xml`:
```xml
- 3
+ 4
./all_themes/myfont.ttf
@@ -137,7 +145,7 @@ You can include theme files within theme files, similar to `#include` in C (thou
`~/.emulationstation/snes/theme.xml`:
```xml
- 3
+ 4
./../all_themes.xml
@@ -150,7 +158,7 @@ You can include theme files within theme files, similar to `#include` in C (thou
Is equivalent to this `snes/theme.xml`:
```xml
- 3
+ 4
./all_themes/myfont.ttf
@@ -170,7 +178,7 @@ Sometimes you want to apply the same properties to the same elements across mult
```xml
- 3
+ 4
./snes_art/snes_header.png
@@ -284,7 +292,7 @@ You can now change the order in which elements are rendered by setting `zIndex`
* `imagegrid name="gamegrid"` - 20
* Media
* `image name="md_image"` - 30
- * `video name="md_video"` - 30
+ * `video name="md_video"` - 31
* `image name="md_marquee"` - 35
* Metadata - 40
* Labels
@@ -392,6 +400,10 @@ Reference
* `image name="md_image"` - POSITION | SIZE | Z_INDEX
- Path is the "image" metadata for the currently selected game.
+ * `video name="md_video"` - POSITION | SIZE | Z_INDEX
+ - Path is the "video" metadata for the currently selected game.
+ * `image name="md_marquee"` - POSITION | SIZE | Z_INDEX
+ - Path is the "marquee" metadata for the currently selected game.
* `rating name="md_rating"` - ALL
- The "rating" metadata.
* `datetime name="md_releasedate"` - ALL
@@ -534,10 +546,33 @@ Reference
- Displays details of the system currently selected in the carousel.
* You can use extra elements (elements with `extra="true"`) to add your own backgrounds, etc. They will be displayed behind the carousel, and scroll relative to the carousel.
+#### menu
+* `helpsystem name="help"` - ALL
+ - The help system style for this view. If not defined, menus will have the same helpsystem as defined in system view.
+* `menuBackground name="menubg"` - COLOR | PATH | FADEPATH
+ - The background behind menus. you can set an image and/or change color (alpha supported)
+
+* `menuSwitch name="menuswitch"` - PATHON | PATHOFF
+ - Images for the on/off switch in menus
+* `menuSlider name="menuslider"` - PATH
+ - Image for the slider knob in menus
+* `menuButton name="menubutton"` - PATH | FILLEDPATH
+ - Images for menu buttons
+* `menuText name="menutext"` - FONTPATH | FONTSIZE | COLOR
+ - text for all menu entries
+* `menuText name="menutitle"` - FONTPATH | FONTSIZE | COLOR
+ - text for menu titles
+* `menuText name="menufooter"` - FONTPATH | FONTSIZE | COLOR
+ - text for menu footers or subtitles
+* `menuTextSmall name="menutextsmall"` - FONTPATH | FONTSIZE | COLOR
+ - text for menu entries in smallerfont
+
+menu is used to theme helpsystem and ES menus.
## Types of properties:
* NORMALIZED_PAIR - two decimals, in the range [0..1], delimited by a space. For example, `0.25 0.5`. Most commonly used for position (x and y coordinates) and size (width and height).
+* NORMALIZED_RECT - four decimals, in the range [0..1], delimited by a space. For example, `0.25 0.5 0.10 0.30`. Most commonly used for padding to store top, left, bottom and right coordinates.
* PATH - a path. If the first character is a `~`, it will be expanded into the environment variable for the home path (`$HOME` for Linux or `%HOMEPATH%` for Windows). If the first character is a `.`, it will be expanded to the theme file's directory, allowing you to specify resources relative to the theme file, like so: `./../general_art/myfont.ttf`.
* BOOLEAN - `true`/`1` or `false`/`0`.
* COLOR - a hexidecimal RGB or RGBA color (6 or 8 digits). If 6 digits, will assume the alpha channel is `FF` (not transparent).
@@ -580,20 +615,35 @@ Can be created as an extra.
- If true, component will be rendered, otherwise rendering will be skipped. Can be used to hide elements from a particular view.
* `zIndex` - type: FLOAT.
- z-index value for component. Components will be rendered in order of z-index value from low to high.
+* `reflexion` - type: NORMALIZED_PAIR.
+ - NEW : table reflexion effect. First item is top position alpha, second is bottom alpha.
+
#### imagegrid
* `pos` - type: NORMALIZED_PAIR.
* `size` - type: NORMALIZED_PAIR.
- The size of the grid. Take care the selected tile can go out of the grid size, so don't position the grid too close to another element or the screen border.
-* `margin` - type: NORMALIZED_PAIR.
+* `margin` - type: NORMALIZED_PAIR. Margin between tiles.
+* `padding` - type: NORMALIZED_RECT.
+ - NEW : Padding for displaying tiles.
+* `autoLayout` - type: NORMALIZED_PAIR.
+ - NEW : Number of column and rows in the grid (integer values).
+* `autoLayoutSelectedZoom` - type: FLOAT.
+ - NEW : Zoom factor to apply when a tile is selected.
+* `imageSource` - type: STRING.
+ - NEW : Selects the image to display. `thumbnail` by default, can also be set to `image` or `marquee`.
+* `showVideoAtDelay` - type: FLOAT.
+ - NEW : delay in millseconds to display video, when the tile is selected.
* `gameImage` - type: PATH.
- The default image used for games which doesn't have an image.
* `folderImage` - type: PATH.
- The default image used for folders which doesn't have an image.
* `scrollDirection` - type: STRING.
- `vertical` by default, can also be set to `horizontal`. Not that in `horizontal` mod, the tiles are ordered from top to bottom, then from left to right.
-
+* `zIndex` - type: FLOAT.
+ - NEW : z-index value for component. Components will be rendered in order of z-index value from low to high.
+
#### gridtile
* `size` - type: NORMALIZED_PAIR.
@@ -612,7 +662,13 @@ Can be created as an extra.
- Set the color of the center part of the ninepatch. The default tile background center color and selected tile background center color have no influence on each others.
* `backgroundEdgeColor` - type: COLOR.
- Set the color of the edge parts of the ninepatch. The default tile background edge color and selected tile background edge color have no influence on each others.
-
+* `selectionMode` - type: STRING.
+ - NEW : Selects if the background is over the full tile or only the image. `full` by default, can also be set to `image`.
+* `imageSizeMode` - type: STRING.
+ - NEW : Selects the image sizing mode. `maxSize` by default, can also be set to `minSize` (outer zoom) or `size` (stretch).
+* `reflexion` - type: NORMALIZED_PAIR.
+ - NEW : table reflexion effect. First item is top position alpha, second is bottom alpha.
+
#### video
* `pos` - type: NORMALIZED_PAIR.
@@ -638,7 +694,9 @@ Can be created as an extra.
- If true, component will be rendered, otherwise rendering will be skipped. Can be used to hide elements from a particular view.
* `zIndex` - type: FLOAT.
- z-index value for component. Components will be rendered in order of z-index value from low to high.
-
+* `path` - type: PATH.
+ - NEW : Path to video file if video is an extra.
+
#### text
Can be created as an extra.
@@ -670,6 +728,10 @@ Can be created as an extra.
- If true, component will be rendered, otherwise rendering will be skipped. Can be used to hide elements from a particular view.
* `zIndex` - type: FLOAT.
- z-index value for component. Components will be rendered in order of z-index value from low to high.
+* `glowColor` - type: COLOR;
+ - NEW : Defines the color of the glow around the text.
+* `glowSize` - type: FLOAT.
+ - NEW : Defines the size of the glow around the text.
#### textlist
@@ -679,6 +741,8 @@ Can be created as an extra.
- Where on the component `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the component exactly in the middle of the screen. If the "POSITION" and "SIZE" attributes are themable, "ORIGIN" is implied.
* `selectorColor` - type: COLOR.
- Color of the "selector bar."
+* `selectorColorEnd` - type: NORMALIZED_PAIR.
+ - NEW : Bottom color for the gradient of the "selector bar."
* `selectorImagePath` - type: PATH.
- Path to image to render in place of "selector bar."
* `selectorImageTile` - type: BOOLEAN.
@@ -804,6 +868,10 @@ EmulationStation borrows the concept of "nine patches" from Android (or "9-Slice
* `color` - type: COLOR.
- Controls the color of the carousel background.
- Default is FFFFFFD8
+* `colorEnd` - type: COLOR.
+ - NEW : Color for the end of gradient
+* `gradientType` - type: STRING.
+ - NEW : Sets the gradient direction. Accepted values are "horizontal" and "vertical".
* `logoSize` - type: NORMALIZED_PAIR. Default is "0.25 0.155"
* `logoScale` - type: FLOAT.
- Selected logo is increased in size by this scale
@@ -825,6 +893,26 @@ EmulationStation borrows the concept of "nine patches" from Android (or "9-Slice
- Default is 3
* `zIndex` - type: FLOAT.
- z-index value for component. Components will be rendered in order of z-index value from low to high.
+* `logoPos` - type: NORMALIZED_PAIR.
+ - NEW : Set the logo position if it is not centered.
+
+#### menuText & menuTextSmall
+
+* `color` - type: COLOR.
+ - Default is 777777FF
+* `fontPath` - type: PATH.
+ - Path to a truetype font (.ttf).
+* `fontSize` - type: FLOAT.
+ - Size of the font as a percentage of screen height (e.g. for a value of `0.1`, the text's height would be 10% of the screen height). Default is 0.085 for menutitle, 0.045 for menutext and 0.035 for menufooter and menutextsmall.
+* `separatorColor` - type: COLOR.
+ - Default is C6C7C6FF. Color of lines that separates menu entries.
+* `selectedColor` - type: COLOR.
+ - Default is FFFFFFFF. Color of text for selected menu entry.
+* `selectorColor` - type: COLOR.
+ - Default is 878787FF. Color of the selector bar.
+* `selectorColorEnd` - type: NORMALIZED_PAIR.
+ - NEW : Bottom color for the gradient of the "selector bar."
+
The help system is a special element that displays a context-sensitive list of actions the user can take at any time. You should try and keep the position constant throughout every screen. Keep in mind the "default" settings (including position) are used whenever the user opens a menu.
diff --git a/es-app/CMakeLists.txt b/es-app/CMakeLists.txt
index 4ff4fdcce9..fece7e1532 100644
--- a/es-app/CMakeLists.txt
+++ b/es-app/CMakeLists.txt
@@ -13,6 +13,9 @@ set(ES_HEADERS
${CMAKE_CURRENT_SOURCE_DIR}/src/FileFilterIndex.h
${CMAKE_CURRENT_SOURCE_DIR}/src/SystemScreenSaver.h
${CMAKE_CURRENT_SOURCE_DIR}/src/CollectionSystemManager.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/ApiSystem.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/ContentInstaller.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/NetworkThread.h
# GuiComponents
${CMAKE_CURRENT_SOURCE_DIR}/src/components/AsyncReqComponent.h
@@ -34,14 +37,16 @@ set(ES_HEADERS
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperMulti.h
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperStart.h
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistFilter.h
- ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.h
- ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiInfoPopup.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiThemeInstall.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.h
# Scrapers
${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/Scraper.h
${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/GamesDBJSONScraper.h
${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/GamesDBJSONScraperResources.h
${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/ScreenScraper.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/ThreadedScraper.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/md5.h
# Views
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/BasicGameListView.h
@@ -72,6 +77,9 @@ set(ES_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/src/FileFilterIndex.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/SystemScreenSaver.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/CollectionSystemManager.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/ApiSystem.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/ContentInstaller.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/NetworkThread.cpp
# GuiComponents
${CMAKE_CURRENT_SOURCE_DIR}/src/components/AsyncReqComponent.cpp
@@ -92,14 +100,16 @@ set(ES_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperMulti.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperStart.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistFilter.cpp
- ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.cpp
- ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiInfoPopup.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiThemeInstall.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.cpp
# Scrapers
${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/Scraper.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/GamesDBJSONScraper.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/GamesDBJSONScraperResources.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/ScreenScraper.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/ThreadedScraper.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/md5.cpp
# Views
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/BasicGameListView.cpp
@@ -158,8 +168,8 @@ SET(CPACK_RESOURCE_FILE README "${CMAKE_CURRENT_SOURCE_DIR}/README.md")
SET(CPACK_DEBIAN_PACKAGE_MAINTAINER "Alec Lofquist ")
SET(CPACK_DEBIAN_PACKAGE_SECTION "misc")
SET(CPACK_DEBIAN_PACKAGE_PRIORITY "extra")
-SET(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6, libsdl2-2.0-0, libfreeimage3, libfreetype6, libcurl3, libasound2")
-SET(CPACK_DEBIAN_PACKAGE_BUILDS_DEPENDS "debhelper (>= 8.0.0), cmake, g++ (>= 4.8), libsdl2-dev, libfreeimage-dev, libfreetype6-dev, libcurl4-openssl-dev, libasound2-dev, libgl1-mesa-dev, rapidjson-dev")
+SET(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6, libsdl2-2.0-0, libsdl2-mixer-2.0-0, libfreeimage3, libfreetype6, libcurl3, libasound2")
+SET(CPACK_DEBIAN_PACKAGE_BUILDS_DEPENDS "debhelper (>= 8.0.0), cmake, g++ (>= 4.8), libsdl2-dev, libsdl2-mixer-dev, libfreeimage-dev, libfreetype6-dev, libcurl4-openssl-dev, libasound2-dev, libgl1-mesa-dev, rapidjson-dev")
SET(CPACK_PACKAGE_VENDOR "emulationstation.org")
SET(CPACK_PACKAGE_VERSION "2.0.0~rc1")
diff --git a/es-app/src/ApiSystem.cpp b/es-app/src/ApiSystem.cpp
new file mode 100644
index 0000000000..dd2d300f1d
--- /dev/null
+++ b/es-app/src/ApiSystem.cpp
@@ -0,0 +1,440 @@
+#include "ApiSystem.h"
+#include "HttpReq.h"
+#include "utils/FileSystemUtil.h"
+#include "utils/StringUtil.h"
+#include
+#include
+#include
+#include "Log.h"
+#include "Window.h"
+#include "components/AsyncNotificationComponent.h"
+
+UpdateState::State ApiSystem::state = UpdateState::State::NO_UPDATE;
+
+class ThreadedUpdater
+{
+public:
+ ThreadedUpdater(Window* window) : mWindow(window)
+ {
+ ApiSystem::state = UpdateState::State::UPDATER_RUNNING;
+
+ mWndNotification = new AsyncNotificationComponent(window, false);
+ mWndNotification->updateTitle(_U("\uF019 ") + _("EMULATIONSTATION"));
+
+ mWindow->registerNotificationComponent(mWndNotification);
+ mHandle = new std::thread(&ThreadedUpdater::threadUpdate, this);
+ }
+
+ ~ThreadedUpdater()
+ {
+ mWindow->unRegisterNotificationComponent(mWndNotification);
+ delete mWndNotification;
+ }
+
+ void threadUpdate()
+ {
+ std::pair updateStatus = ApiSystem::updateSystem([this](const std::string info)
+ {
+ auto pos = info.find(">>>");
+ if (pos != std::string::npos)
+ {
+ std::string percent(info.substr(pos));
+ percent = Utils::String::replace(percent, ">", "");
+ percent = Utils::String::replace(percent, "%", "");
+ percent = Utils::String::replace(percent, " ", "");
+
+ int value = atoi(percent.c_str());
+
+ std::string text(info.substr(0, pos));
+ text = Utils::String::trim(text);
+
+ mWndNotification->updatePercent(value);
+ mWndNotification->updateText(text);
+ }
+ else
+ {
+ mWndNotification->updatePercent(-1);
+ mWndNotification->updateText(info);
+ }
+ });
+
+ if (updateStatus.second == 0)
+ {
+ ApiSystem::state = UpdateState::State::UPDATE_READY;
+
+ mWndNotification->updateTitle(_U("\uF019 ") + _("UPDATE IS READY"));
+ mWndNotification->updateText(_("RESTART EMULATIONSTATION TO APPLY"));
+
+ std::this_thread::yield();
+ std::this_thread::sleep_for(std::chrono::hours(12));
+ }
+ else
+ {
+ ApiSystem::state = UpdateState::State::NO_UPDATE;
+
+ std::string error = _("AN ERROR OCCURED") + std::string(": ") + updateStatus.first;
+ mWindow->displayNotificationMessage(error);
+ }
+
+ delete this;
+ }
+
+private:
+ std::thread* mHandle;
+ AsyncNotificationComponent* mWndNotification;
+ Window* mWindow;
+};
+
+void ApiSystem::startUpdate(Window* c)
+{
+#if WIN32
+ new ThreadedUpdater(c);
+#endif
+}
+
+std::string ApiSystem::checkUpdateVersion()
+{
+#if WIN32
+ std::string localVersion;
+ std::string localVersionFile = Utils::FileSystem::getExePath() + "/version.info";
+ if (Utils::FileSystem::exists(localVersionFile))
+ {
+ localVersion = Utils::FileSystem::readAllText(localVersionFile);
+ localVersion = Utils::String::replace(Utils::String::replace(localVersion, "\r", ""), "\n", "");
+ }
+
+ HttpReq httpreq("https://github.com/fabricecaruso/EmulationStation/releases/download/continuous-master/version.info");
+ if (httpreq.wait())
+ {
+ std::string serverVersion = httpreq.getContent();
+ serverVersion = Utils::String::replace(Utils::String::replace(serverVersion, "\r", ""), "\n", "");
+ if (!serverVersion.empty() && serverVersion != localVersion)
+ return serverVersion;
+ }
+#endif
+
+ return "";
+}
+
+#if WIN32
+#include
+#include // #include for _bstr_t
+#pragma comment(lib, "shell32.lib")
+#pragma comment (lib, "comsuppw.lib" ) // link with "comsuppw.lib" (or debug version: "comsuppwd.lib")
+
+bool unzipFile(const std::string fileName, const std::string dest)
+{
+ bool ret = false;
+
+ HRESULT hResult;
+ IShellDispatch* pISD;
+ Folder* pFromZip = nullptr;
+ VARIANT vDir, vFile, vOpt;
+
+ OleInitialize(NULL);
+ CoInitialize(NULL);
+
+ hResult = CoCreateInstance(CLSID_Shell, NULL, CLSCTX_INPROC_SERVER, IID_IShellDispatch, (void **)&pISD);
+
+ if (SUCCEEDED(hResult))
+ {
+ VariantInit(&vDir);
+ vDir.vt = VT_BSTR;
+
+ int zipDirLen = (lstrlenA(fileName.c_str()) + 1) * sizeof(WCHAR);
+ BSTR bstrZip = SysAllocStringByteLen(NULL, zipDirLen);
+ MultiByteToWideChar(CP_ACP, 0, fileName.c_str(), -1, bstrZip, zipDirLen);
+ vDir.bstrVal = bstrZip;
+
+ hResult = pISD->NameSpace(vDir, &pFromZip);
+
+ if (hResult == S_OK && pFromZip != nullptr)
+ {
+ if (!Utils::FileSystem::exists(dest))
+ Utils::FileSystem::createDirectory(dest);
+
+ Folder *pToFolder = NULL;
+
+ VariantInit(&vFile);
+ vFile.vt = VT_BSTR;
+
+ int fnLen = (lstrlenA(dest.c_str()) + 1) * sizeof(WCHAR);
+ BSTR bstrFolder = SysAllocStringByteLen(NULL, fnLen);
+ MultiByteToWideChar(CP_ACP, 0, dest.c_str(), -1, bstrFolder, fnLen);
+ vFile.bstrVal = bstrFolder;
+
+ hResult = pISD->NameSpace(vFile, &pToFolder);
+ if (hResult == S_OK && pToFolder)
+ {
+ FolderItems *fi = NULL;
+ pFromZip->Items(&fi);
+
+ VariantInit(&vOpt);
+ vOpt.vt = VT_I4;
+ vOpt.lVal = FOF_NO_UI; //4; // Do not display a progress dialog box
+
+ VARIANT newV;
+ VariantInit(&newV);
+ newV.vt = VT_DISPATCH;
+ newV.pdispVal = fi;
+ hResult = pToFolder->CopyHere(newV, vOpt);
+ if (hResult == S_OK)
+ ret = true;
+
+ pFromZip->Release();
+ pToFolder->Release();
+ }
+ }
+ pISD->Release();
+ }
+
+ CoUninitialize();
+ return ret;
+}
+
+bool downloadFile(const std::string url, const std::string fileName, const std::string label, const std::function& func)
+{
+ if (func != nullptr)
+ func("Downloading " + label);
+
+ HttpReq httpreq(url, fileName);
+ while (httpreq.status() == HttpReq::REQ_IN_PROGRESS)
+ {
+ if (func != nullptr)
+ func(std::string("Downloading " + label + " >>> " + std::to_string(httpreq.getPercent()) + " %"));
+
+ std::this_thread::sleep_for(std::chrono::milliseconds(20));
+ }
+
+ if (httpreq.status() != HttpReq::REQ_SUCCESS)
+ return false;
+
+ return true;
+}
+
+
+void deleteDirectoryFiles(const std::string path)
+{
+ auto files = Utils::FileSystem::getDirContent(path, true, true);
+ std::reverse(std::begin(files), std::end(files));
+ for (auto file : files)
+ {
+ if (Utils::FileSystem::isDirectory(file))
+ ::RemoveDirectoryA(Utils::FileSystem::getPreferredPath(file).c_str());
+ else
+ Utils::FileSystem::removeFile(file);
+ }
+}
+#endif
+
+std::pair ApiSystem::updateSystem(const std::function& func)
+{
+#if WIN32
+ std::string url = "https://github.com/fabricecaruso/EmulationStation/releases/download/continuous-master/EmulationStation-Win32-no-deps.zip";
+
+ std::string fileName = Utils::FileSystem::getFileName(url);
+ std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/update";
+
+ if (!Utils::FileSystem::exists(path))
+ Utils::FileSystem::createDirectory(path);
+ else
+ deleteDirectoryFiles(path);
+
+ std::string zipFile = path + "/" + fileName;
+
+ if (downloadFile(url, zipFile, "update", func))
+ {
+ if (func != nullptr)
+ func(std::string("Extracting update"));
+
+ unzipFile(Utils::FileSystem::getPreferredPath(zipFile), Utils::FileSystem::getPreferredPath(path));
+ Utils::FileSystem::removeFile(zipFile);
+
+ auto files = Utils::FileSystem::getDirContent(path, true, true);
+ for (auto file : files)
+ {
+
+ std::string relative = Utils::FileSystem::createRelativePath(file, path, false);
+ if (Utils::String::startsWith(relative, "./"))
+ relative = relative.substr(2);
+
+ std::string localPath = Utils::FileSystem::getExePath() + "/" + relative;
+
+ if (Utils::FileSystem::isDirectory(file))
+ {
+ if (!Utils::FileSystem::exists(localPath))
+ Utils::FileSystem::createDirectory(localPath);
+ }
+ else
+ {
+ if (Utils::FileSystem::exists(localPath))
+ {
+ Utils::FileSystem::removeFile(localPath + ".old");
+ rename(localPath.c_str(), (localPath + ".old").c_str());
+ }
+
+ if (Utils::FileSystem::copyFile(file, localPath))
+ {
+ Utils::FileSystem::removeFile(localPath + ".old");
+ Utils::FileSystem::removeFile(file);
+ }
+ }
+ }
+
+ deleteDirectoryFiles(path);
+
+ return std::pair("done.", 0);
+ }
+#endif
+
+ return std::pair("error.", 1);
+}
+
+std::vector ApiSystem::getThemesList()
+{
+ LOG(LogDebug) << "ApiSystem::getThemesList";
+
+ std::vector res;
+
+ HttpReq httpreq("https://batocera.org/upgrades/themes.txt");
+ if (httpreq.wait())
+ {
+ auto lines = Utils::String::split(httpreq.getContent(), '\n');
+ for (auto line : lines)
+ {
+ auto parts = Utils::String::splitAny(line, " \t");
+ if (parts.size() > 1)
+ {
+ auto themeName = parts[0];
+
+ std::string themeUrl = parts[1];
+ std::string themeFolder = Utils::FileSystem::getFileName(themeUrl);
+
+ bool themeExists = false;
+
+ std::vector paths{
+ Utils::FileSystem::getHomePath() + "/.emulationstation/themes",
+ "/etc/emulationstation/themes",
+ "/userdata/themes"
+ };
+
+ for (auto path : paths)
+ {
+ themeExists = Utils::FileSystem::isDirectory(path + "/" + themeName) ||
+ Utils::FileSystem::isDirectory(path + "/" + themeFolder) ||
+ Utils::FileSystem::isDirectory(path + "/" + themeFolder + "-master");
+
+ if (themeExists)
+ break;
+ }
+
+ ThemeDownloadInfo info;
+ info.installed = themeExists;
+ info.name = themeName;
+ info.url = themeUrl;
+
+ res.push_back(info);
+ }
+ }
+ }
+
+ return res;
+}
+
+bool downloadGitRepository(const std::string url, const std::string fileName, const std::string label, const std::function& func)
+{
+ if (func != nullptr)
+ func(_("Downloading") + " " + label);
+
+ long downloadSize = 0;
+
+ std::string statUrl = Utils::String::replace(url, "https://github.com/", "https://api.github.com/repos/");
+ if (statUrl != url)
+ {
+ HttpReq statreq(statUrl);
+ if (statreq.wait())
+ {
+ std::string content = statreq.getContent();
+ auto pos = content.find("\"size\": ");
+ if (pos != std::string::npos)
+ {
+ auto end = content.find(",", pos);
+ if (end != std::string::npos)
+ downloadSize = atoi(content.substr(pos + 8, end - pos - 8).c_str()) * 1024;
+ }
+ }
+ }
+
+ HttpReq httpreq(url + "/archive/master.zip", fileName);
+
+ int curPos = -1;
+ while (httpreq.status() == HttpReq::REQ_IN_PROGRESS)
+ {
+ if (downloadSize > 0)
+ {
+ double pos = httpreq.getPosition();
+ if (pos > 0 && curPos != pos)
+ {
+ if (func != nullptr)
+ {
+ std::string pc = std::to_string((int)(pos * 100.0 / downloadSize));
+ func(std::string(_("Downloading") + " " + label + " >>> " + pc + " %"));
+ }
+
+ curPos = pos;
+ }
+ }
+
+ std::this_thread::sleep_for(std::chrono::milliseconds(20));
+ }
+
+ if (httpreq.status() != HttpReq::REQ_SUCCESS)
+ return false;
+
+ return true;
+}
+
+std::pair ApiSystem::installTheme(std::string themeName, const std::function& func)
+{
+#if WIN32
+ for (auto theme : getThemesList())
+ {
+ if (theme.name != themeName)
+ continue;
+
+ std::string themeFileName = Utils::FileSystem::getFileName(theme.url);
+ std::string zipFile = Utils::FileSystem::getHomePath() + "/.emulationstation/themes/" + themeFileName + ".zip";
+ zipFile = Utils::String::replace(zipFile, "/", "\\");
+
+ if (downloadGitRepository(theme.url, zipFile, themeName, func))
+ {
+ if (func != nullptr)
+ func(_("Extracting") + " " + themeName);
+
+ if (!unzipFile(zipFile, Utils::String::replace(Utils::FileSystem::getHomePath() + "/.emulationstation/themes", "/", "\\")))
+ return std::pair(std::string("An error occured while extracting"), 1);
+
+ std::string folderName = Utils::FileSystem::getHomePath() + "/.emulationstation/themes/" + themeFileName + "-master";
+ if (Utils::FileSystem::isDirectory(folderName))
+ {
+ std::string finalfolderName = Utils::FileSystem::getParent(folderName) + "/" + themeName;
+
+ if (Utils::FileSystem::isDirectory(finalfolderName))
+ deleteDirectoryFiles(finalfolderName);
+
+ rename(folderName.c_str(), finalfolderName.c_str());
+
+ Utils::FileSystem::removeFile(zipFile);
+
+ return std::pair(std::string("OK"), 0);
+ }
+
+ return std::pair(std::string("Invalid extraction folder"), 1);
+ }
+
+ return std::pair(std::string("An error occured while downloading"), 1);
+ }
+#endif
+
+ return std::pair(std::string("Theme not found"), 1);
+}
\ No newline at end of file
diff --git a/es-app/src/ApiSystem.h b/es-app/src/ApiSystem.h
new file mode 100644
index 0000000000..603197f512
--- /dev/null
+++ b/es-app/src/ApiSystem.h
@@ -0,0 +1,41 @@
+#ifndef API_SYSTEM
+#define API_SYSTEM
+
+#include
+#include
+#include
+
+class Window;
+
+namespace UpdateState
+{
+ enum State
+ {
+ NO_UPDATE,
+ UPDATER_RUNNING,
+ UPDATE_READY
+ };
+}
+
+struct ThemeDownloadInfo
+{
+ bool installed;
+ std::string name;
+ std::string url;
+};
+
+class ApiSystem
+{
+public:
+ static UpdateState::State state;
+
+ static std::pair updateSystem(const std::function& func = nullptr);
+ static std::string checkUpdateVersion();
+ static void startUpdate(Window* c);
+
+ static std::vector getThemesList();
+ static std::pair installTheme(std::string themeName, const std::function& func = nullptr);
+};
+
+#endif
+
diff --git a/es-app/src/CollectionSystemManager.cpp b/es-app/src/CollectionSystemManager.cpp
index 6b665df78c..de71f138a9 100644
--- a/es-app/src/CollectionSystemManager.cpp
+++ b/es-app/src/CollectionSystemManager.cpp
@@ -1,1050 +1,1265 @@
-#include "CollectionSystemManager.h"
-
-#include "guis/GuiInfoPopup.h"
-#include "utils/FileSystemUtil.h"
-#include "utils/StringUtil.h"
-#include "views/gamelist/IGameListView.h"
-#include "views/ViewController.h"
-#include "FileData.h"
-#include "FileFilterIndex.h"
-#include "Log.h"
-#include "Settings.h"
-#include "SystemData.h"
-#include "ThemeData.h"
-#include
-#include
-
-std::string myCollectionsName = "collections";
-
-#define LAST_PLAYED_MAX 50
-
-/* Handling the getting, initialization, deinitialization, saving and deletion of
- * a CollectionSystemManager Instance */
-CollectionSystemManager* CollectionSystemManager::sInstance = NULL;
-
-CollectionSystemManager::CollectionSystemManager(Window* window) : mWindow(window)
-{
- CollectionSystemDecl systemDecls[] = {
- //type name long name //default sort // theme folder // isCustom
- { AUTO_ALL_GAMES, "all", "all games", "filename, ascending", "auto-allgames", false },
- { AUTO_LAST_PLAYED, "recent", "last played", "last played, descending", "auto-lastplayed", false },
- { AUTO_FAVORITES, "favorites", "favorites", "filename, ascending", "auto-favorites", false },
- { CUSTOM_COLLECTION, myCollectionsName, "collections", "filename, ascending", "custom-collections", true }
- };
-
- // create a map
- std::vector tempSystemDecl = std::vector(systemDecls, systemDecls + sizeof(systemDecls) / sizeof(systemDecls[0]));
-
- for (std::vector::const_iterator it = tempSystemDecl.cbegin(); it != tempSystemDecl.cend(); ++it )
- {
- mCollectionSystemDeclsIndex[(*it).name] = (*it);
- }
-
- // creating standard environment data
- mCollectionEnvData = new SystemEnvironmentData;
- mCollectionEnvData->mStartPath = "";
- std::vector exts;
- mCollectionEnvData->mSearchExtensions = exts;
- mCollectionEnvData->mLaunchCommand = "";
- std::vector allPlatformIds;
- allPlatformIds.push_back(PlatformIds::PLATFORM_IGNORE);
- mCollectionEnvData->mPlatformIds = allPlatformIds;
-
- std::string path = getCollectionsFolder();
- if(!Utils::FileSystem::exists(path))
- Utils::FileSystem::createDirectory(path);
-
- mIsEditingCustom = false;
- mEditingCollection = "Favorites";
- mEditingCollectionSystemData = NULL;
- mCustomCollectionsBundle = NULL;
-}
-
-CollectionSystemManager::~CollectionSystemManager()
-{
- assert(sInstance == this);
- removeCollectionsFromDisplayedSystems();
-
- // iterate the map
- for(std::map::const_iterator it = mCustomCollectionSystemsData.cbegin() ; it != mCustomCollectionSystemsData.cend() ; it++ )
- {
- if (it->second.isPopulated)
- {
- saveCustomCollection(it->second.system);
- }
- delete it->second.system;
- }
- sInstance = NULL;
-}
-
-CollectionSystemManager* CollectionSystemManager::get()
-{
- assert(sInstance);
- return sInstance;
-}
-
-void CollectionSystemManager::init(Window* window)
-{
- assert(!sInstance);
- sInstance = new CollectionSystemManager(window);
-}
-
-void CollectionSystemManager::deinit()
-{
- if (sInstance)
- {
- delete sInstance;
- }
-}
-
-void CollectionSystemManager::saveCustomCollection(SystemData* sys)
-{
- std::string name = sys->getName();
- std::unordered_map games = sys->getRootFolder()->getChildrenByFilename();
- bool found = mCustomCollectionSystemsData.find(name) != mCustomCollectionSystemsData.cend();
- if (found) {
- CollectionSystemData sysData = mCustomCollectionSystemsData.at(name);
- if (sysData.needsSave)
- {
- std::ofstream configFile;
- configFile.open(getCustomCollectionConfigPath(name));
- for(std::unordered_map::const_iterator iter = games.cbegin(); iter != games.cend(); ++iter)
- {
- std::string path = iter->first;
- configFile << path << std::endl;
- }
- configFile.close();
- }
- }
- else
- {
- LOG(LogError) << "Couldn't find collection to save! " << name;
- }
-}
-
-/* Methods to load all Collections into memory, and handle enabling the active ones */
-// loads all Collection Systems
-void CollectionSystemManager::loadCollectionSystems()
-{
- initAutoCollectionSystems();
- CollectionSystemDecl decl = mCollectionSystemDeclsIndex[myCollectionsName];
- mCustomCollectionsBundle = createNewCollectionEntry(decl.name, decl, false);
- // we will also load custom systems here
- initCustomCollectionSystems();
- if(Settings::getInstance()->getString("CollectionSystemsAuto") != "" || Settings::getInstance()->getString("CollectionSystemsCustom") != "")
- {
- // Now see which ones are enabled
- loadEnabledListFromSettings();
- // add to the main System Vector, and create Views as needed
- updateSystemsList();
- }
-}
-
-// loads settings
-void CollectionSystemManager::loadEnabledListFromSettings()
-{
- // we parse the auto collection settings list
- std::vector autoSelected = Utils::String::commaStringToVector(Settings::getInstance()->getString("CollectionSystemsAuto"));
-
- // iterate the map
- for(std::map::iterator it = mAutoCollectionSystemsData.begin() ; it != mAutoCollectionSystemsData.end() ; it++ )
- {
- it->second.isEnabled = (std::find(autoSelected.cbegin(), autoSelected.cend(), it->first) != autoSelected.cend());
- }
-
- // we parse the custom collection settings list
- std::vector customSelected = Utils::String::commaStringToVector(Settings::getInstance()->getString("CollectionSystemsCustom"));
-
- // iterate the map
- for(std::map::iterator it = mCustomCollectionSystemsData.begin() ; it != mCustomCollectionSystemsData.end() ; it++ )
- {
- it->second.isEnabled = (std::find(customSelected.cbegin(), customSelected.cend(), it->first) != customSelected.cend());
- }
-}
-
-// updates enabled system list in System View
-void CollectionSystemManager::updateSystemsList()
-{
- // remove all Collection Systems
- removeCollectionsFromDisplayedSystems();
- // add custom enabled ones
- addEnabledCollectionsToDisplayedSystems(&mCustomCollectionSystemsData);
-
- if(Settings::getInstance()->getBool("SortAllSystems"))
- {
- // sort custom individual systems with other systems
- std::sort(SystemData::sSystemVector.begin(), SystemData::sSystemVector.end(), systemSort);
-
- // move RetroPie system to end, before auto collections
- for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); )
- {
- if ((*sysIt)->getName() == "retropie")
- {
- SystemData* retroPieSystem = (*sysIt);
- sysIt = SystemData::sSystemVector.erase(sysIt);
- SystemData::sSystemVector.push_back(retroPieSystem);
- break;
- }
- else
- {
- sysIt++;
- }
- }
- }
-
- if(mCustomCollectionsBundle->getRootFolder()->getChildren().size() > 0)
- {
- mCustomCollectionsBundle->getRootFolder()->sort(getSortTypeFromString(mCollectionSystemDeclsIndex[myCollectionsName].defaultSort));
- SystemData::sSystemVector.push_back(mCustomCollectionsBundle);
- }
-
- // add auto enabled ones
- addEnabledCollectionsToDisplayedSystems(&mAutoCollectionSystemsData);
-
- // create views for collections, before reload
- for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); sysIt++)
- {
- if ((*sysIt)->isCollection())
- {
- ViewController::get()->getGameListView((*sysIt));
- }
- }
-
- // if we were editing a custom collection, and it's no longer enabled, exit edit mode
- if(mIsEditingCustom && !mEditingCollectionSystemData->isEnabled)
- {
- exitEditMode();
- }
-}
-
-/* Methods to manage collection files related to a source FileData */
-// updates all collection files related to the source file
-void CollectionSystemManager::refreshCollectionSystems(FileData* file)
-{
- if (!file->getSystem()->isGameSystem() || file->getType() != GAME)
- return;
-
- std::map allCollections;
- allCollections.insert(mAutoCollectionSystemsData.cbegin(), mAutoCollectionSystemsData.cend());
- allCollections.insert(mCustomCollectionSystemsData.cbegin(), mCustomCollectionSystemsData.cend());
-
- for(auto sysDataIt = allCollections.cbegin(); sysDataIt != allCollections.cend(); sysDataIt++)
- {
- updateCollectionSystem(file, sysDataIt->second);
- }
-}
-
-void CollectionSystemManager::updateCollectionSystem(FileData* file, CollectionSystemData sysData)
-{
- if (sysData.isPopulated)
- {
- // collection files use the full path as key, to avoid clashes
- std::string key = file->getFullPath();
-
- SystemData* curSys = sysData.system;
- const std::unordered_map& children = curSys->getRootFolder()->getChildrenByFilename();
- bool found = children.find(key) != children.cend();
- FileData* rootFolder = curSys->getRootFolder();
- FileFilterIndex* fileIndex = curSys->getIndex();
- std::string name = curSys->getName();
-
- if (found) {
- // if we found it, we need to update it
- FileData* collectionEntry = children.at(key);
- // remove from index, so we can re-index metadata after refreshing
- fileIndex->removeFromIndex(collectionEntry);
- collectionEntry->refreshMetadata();
- // found and we are removing
- if (name == "favorites" && file->metadata.get("favorite") == "false") {
- // need to check if still marked as favorite, if not remove
- ViewController::get()->getGameListView(curSys).get()->remove(collectionEntry, false);
- }
- else
- {
- // re-index with new metadata
- fileIndex->addToIndex(collectionEntry);
- ViewController::get()->onFileChanged(collectionEntry, FILE_METADATA_CHANGED);
- }
- }
- else
- {
- // we didn't find it here - we need to check if we should add it
- if (name == "recent" && file->metadata.get("playcount") > "0" && includeFileInAutoCollections(file) ||
- name == "favorites" && file->metadata.get("favorite") == "true") {
- CollectionFileData* newGame = new CollectionFileData(file, curSys);
- rootFolder->addChild(newGame);
- fileIndex->addToIndex(newGame);
- ViewController::get()->onFileChanged(file, FILE_METADATA_CHANGED);
- ViewController::get()->getGameListView(curSys)->onFileChanged(newGame, FILE_METADATA_CHANGED);
- }
- }
- rootFolder->sort(getSortTypeFromString(mCollectionSystemDeclsIndex[name].defaultSort));
- if (name == "recent")
- {
- trimCollectionCount(rootFolder, LAST_PLAYED_MAX);
- ViewController::get()->onFileChanged(rootFolder, FILE_METADATA_CHANGED);
- }
- else
- ViewController::get()->onFileChanged(rootFolder, FILE_SORTED);
- }
-}
-
-void CollectionSystemManager::trimCollectionCount(FileData* rootFolder, int limit)
-{
- SystemData* curSys = rootFolder->getSystem();
- while ((int)rootFolder->getChildrenListToDisplay().size() > limit)
- {
- CollectionFileData* gameToRemove = (CollectionFileData*)rootFolder->getChildrenListToDisplay().back();
- ViewController::get()->getGameListView(curSys).get()->remove(gameToRemove, false);
- }
-}
-
-// deletes all collection files from collection systems related to the source file
-void CollectionSystemManager::deleteCollectionFiles(FileData* file)
-{
- // collection files use the full path as key, to avoid clashes
- std::string key = file->getFullPath();
- // find games in collection systems
- std::map allCollections;
- allCollections.insert(mAutoCollectionSystemsData.cbegin(), mAutoCollectionSystemsData.cend());
- allCollections.insert(mCustomCollectionSystemsData.cbegin(), mCustomCollectionSystemsData.cend());
-
- for(auto sysDataIt = allCollections.begin(); sysDataIt != allCollections.end(); sysDataIt++)
- {
- if (sysDataIt->second.isPopulated)
- {
- const std::unordered_map& children = (sysDataIt->second.system)->getRootFolder()->getChildrenByFilename();
-
- bool found = children.find(key) != children.cend();
- if (found) {
- sysDataIt->second.needsSave = true;
- FileData* collectionEntry = children.at(key);
- SystemData* systemViewToUpdate = getSystemToView(sysDataIt->second.system);
- ViewController::get()->getGameListView(systemViewToUpdate).get()->remove(collectionEntry, false);
- }
- }
- }
-}
-
-// returns whether the current theme is compatible with Automatic or Custom Collections
-bool CollectionSystemManager::isThemeGenericCollectionCompatible(bool genericCustomCollections)
-{
- std::vector cfgSys = getCollectionThemeFolders(genericCustomCollections);
- for(auto sysIt = cfgSys.cbegin(); sysIt != cfgSys.cend(); sysIt++)
- {
- if(!themeFolderExists(*sysIt))
- return false;
- }
- return true;
-}
-
-bool CollectionSystemManager::isThemeCustomCollectionCompatible(std::vector stringVector)
-{
- if (isThemeGenericCollectionCompatible(true))
- return true;
-
- // get theme path
- auto themeSets = ThemeData::getThemeSets();
- auto set = themeSets.find(Settings::getInstance()->getString("ThemeSet"));
- if(set != themeSets.cend())
- {
- std::string defaultThemeFilePath = set->second.path + "/theme.xml";
- if (Utils::FileSystem::exists(defaultThemeFilePath))
- {
- return true;
- }
- }
-
- for(auto sysIt = stringVector.cbegin(); sysIt != stringVector.cend(); sysIt++)
- {
- if(!themeFolderExists(*sysIt))
- return false;
- }
- return true;
-}
-
-std::string CollectionSystemManager::getValidNewCollectionName(std::string inName, int index)
-{
- std::string name = inName;
-
- if(index == 0)
- {
- size_t remove = std::string::npos;
-
- // get valid name
- while((remove = name.find_first_not_of("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-[]() ")) != std::string::npos)
- {
- name.erase(remove, 1);
- }
- }
- else
- {
- name += " (" + std::to_string(index) + ")";
- }
-
- if(name == "")
- {
- name = "New Collection";
- }
-
- if(name != inName)
- {
- LOG(LogInfo) << "Had to change name, from: " << inName << " to: " << name;
- }
-
- // get used systems in es_systems.cfg
- std::vector systemsInUse = getSystemsFromConfig();
- // get folders assigned to custom collections
- std::vector autoSys = getCollectionThemeFolders(false);
- // get folder assigned to custom collections
- std::vector customSys = getCollectionThemeFolders(true);
- // get folders assigned to user collections
- std::vector userSys = getUserCollectionThemeFolders();
- // add them all to the list of systems in use
- systemsInUse.insert(systemsInUse.cend(), autoSys.cbegin(), autoSys.cend());
- systemsInUse.insert(systemsInUse.cend(), customSys.cbegin(), customSys.cend());
- systemsInUse.insert(systemsInUse.cend(), userSys.cbegin(), userSys.cend());
- for(auto sysIt = systemsInUse.cbegin(); sysIt != systemsInUse.cend(); sysIt++)
- {
- if (*sysIt == name)
- {
- if(index > 0) {
- name = name.substr(0, name.size()-4);
- }
- return getValidNewCollectionName(name, index+1);
- }
- }
- // if it matches one of the custom collections reserved names
- if (mCollectionSystemDeclsIndex.find(name) != mCollectionSystemDeclsIndex.cend())
- return getValidNewCollectionName(name, index+1);
- return name;
-}
-
-void CollectionSystemManager::setEditMode(std::string collectionName)
-{
- if (mCustomCollectionSystemsData.find(collectionName) == mCustomCollectionSystemsData.cend())
- {
- LOG(LogError) << "Tried to edit a non-existing collection: " << collectionName;
- return;
- }
- mIsEditingCustom = true;
- mEditingCollection = collectionName;
-
- CollectionSystemData* sysData = &(mCustomCollectionSystemsData.at(mEditingCollection));
- if (!sysData->isPopulated)
- {
- populateCustomCollection(sysData);
- }
- // if it's bundled, this needs to be the bundle system
- mEditingCollectionSystemData = sysData;
-
- GuiInfoPopup* s = new GuiInfoPopup(mWindow, "Editing the '" + Utils::String::toUpper(collectionName) + "' Collection. Add/remove games with Y.", 10000);
- mWindow->setInfoPopup(s);
-}
-
-void CollectionSystemManager::exitEditMode()
-{
- GuiInfoPopup* s = new GuiInfoPopup(mWindow, "Finished editing the '" + mEditingCollection + "' Collection.", 4000);
- mWindow->setInfoPopup(s);
- mIsEditingCustom = false;
- mEditingCollection = "Favorites";
-}
-
-// adds or removes a game from a specific collection
-bool CollectionSystemManager::toggleGameInCollection(FileData* file)
-{
- if (file->getType() == GAME)
- {
- GuiInfoPopup* s;
- bool adding = true;
- std::string name = file->getName();
- std::string sysName = mEditingCollection;
- if (mIsEditingCustom)
- {
- SystemData* sysData = mEditingCollectionSystemData->system;
- mEditingCollectionSystemData->needsSave = true;
- if (!mEditingCollectionSystemData->isPopulated)
- {
- populateCustomCollection(mEditingCollectionSystemData);
- }
- std::string key = file->getFullPath();
- FileData* rootFolder = sysData->getRootFolder();
- const std::unordered_map& children = rootFolder->getChildrenByFilename();
- bool found = children.find(key) != children.cend();
- FileFilterIndex* fileIndex = sysData->getIndex();
- std::string name = sysData->getName();
-
- SystemData* systemViewToUpdate = getSystemToView(sysData);
-
- if (found) {
- adding = false;
- // if we found it, we need to remove it
- FileData* collectionEntry = children.at(key);
- // remove from index
- fileIndex->removeFromIndex(collectionEntry);
- // remove from bundle index as well, if needed
- if(systemViewToUpdate != sysData)
- {
- systemViewToUpdate->getIndex()->removeFromIndex(collectionEntry);
- }
- ViewController::get()->getGameListView(systemViewToUpdate).get()->remove(collectionEntry, false);
- }
- else
- {
- // we didn't find it here, we should add it
- CollectionFileData* newGame = new CollectionFileData(file, sysData);
- rootFolder->addChild(newGame);
- fileIndex->addToIndex(newGame);
- ViewController::get()->getGameListView(systemViewToUpdate)->onFileChanged(newGame, FILE_METADATA_CHANGED);
- rootFolder->sort(getSortTypeFromString(mEditingCollectionSystemData->decl.defaultSort));
- ViewController::get()->onFileChanged(systemViewToUpdate->getRootFolder(), FILE_SORTED);
- // add to bundle index as well, if needed
- if(systemViewToUpdate != sysData)
- {
- systemViewToUpdate->getIndex()->addToIndex(newGame);
- }
- }
- updateCollectionFolderMetadata(sysData);
- }
- else
- {
- file->getSourceFileData()->getSystem()->getIndex()->removeFromIndex(file);
- MetaDataList* md = &file->getSourceFileData()->metadata;
- std::string value = md->get("favorite");
- if (value == "false")
- {
- md->set("favorite", "true");
- }
- else
- {
- adding = false;
- md->set("favorite", "false");
- }
- file->getSourceFileData()->getSystem()->getIndex()->addToIndex(file);
- refreshCollectionSystems(file->getSourceFileData());
- }
- if (adding)
- {
- s = new GuiInfoPopup(mWindow, "Added '" + Utils::String::removeParenthesis(name) + "' to '" + Utils::String::toUpper(sysName) + "'", 4000);
- }
- else
- {
- s = new GuiInfoPopup(mWindow, "Removed '" + Utils::String::removeParenthesis(name) + "' from '" + Utils::String::toUpper(sysName) + "'", 4000);
- }
- mWindow->setInfoPopup(s);
- return true;
- }
- return false;
-}
-
-SystemData* CollectionSystemManager::getSystemToView(SystemData* sys)
-{
- SystemData* systemToView = sys;
- FileData* rootFolder = sys->getRootFolder();
-
- FileData* bundleRootFolder = mCustomCollectionsBundle->getRootFolder();
- const std::unordered_map& bundleChildren = bundleRootFolder->getChildrenByFilename();
-
- // is the rootFolder bundled in the "My Collections" system?
- bool sysFoundInBundle = bundleChildren.find(rootFolder->getKey()) != bundleChildren.cend();
-
- if (sysFoundInBundle && sys->isCollection())
- {
- systemToView = mCustomCollectionsBundle;
- }
- return systemToView;
-}
-
-/* Handles loading a collection system, creating an empty one, and populating on demand */
-// loads Automatic Collection systems (All, Favorites, Last Played)
-void CollectionSystemManager::initAutoCollectionSystems()
-{
- for(std::map::const_iterator it = mCollectionSystemDeclsIndex.cbegin() ; it != mCollectionSystemDeclsIndex.cend() ; it++ )
- {
- CollectionSystemDecl sysDecl = it->second;
- if (!sysDecl.isCustom)
- {
- createNewCollectionEntry(sysDecl.name, sysDecl);
- }
- }
-}
-
-// this may come in handy if at any point in time in the future we want to
-// automatically generate metadata for a folder
-void CollectionSystemManager::updateCollectionFolderMetadata(SystemData* sys)
-{
- FileData* rootFolder = sys->getRootFolder();
-
- std::string desc = "This collection is empty.";
- std::string rating = "0";
- std::string players = "1";
- std::string releasedate = "N/A";
- std::string developer = "None";
- std::string genre = "None";
- std::string video = "";
- std::string thumbnail = "";
- std::string image = "";
-
- std::unordered_map games = rootFolder->getChildrenByFilename();
-
- if(games.size() > 0)
- {
- std::string games_list = "";
- int games_counter = 0;
- for(std::unordered_map::const_iterator iter = games.cbegin(); iter != games.cend(); ++iter)
- {
- games_counter++;
- FileData* file = iter->second;
-
- std::string new_rating = file->metadata.get("rating");
- std::string new_releasedate = file->metadata.get("releasedate");
- std::string new_developer = file->metadata.get("developer");
- std::string new_genre = file->metadata.get("genre");
- std::string new_players = file->metadata.get("players");
-
- rating = (new_rating > rating ? (new_rating != "" ? new_rating : rating) : rating);
- players = (new_players > players ? (new_players != "" ? new_players : players) : players);
- releasedate = (new_releasedate < releasedate ? (new_releasedate != "" ? new_releasedate : releasedate) : releasedate);
- developer = (developer == "None" ? new_developer : (new_developer != developer ? "Various" : new_developer));
- genre = (genre == "None" ? new_genre : (new_genre != genre ? "Various" : new_genre));
-
- switch(games_counter)
- {
- case 2:
- case 3:
- games_list += ", ";
- case 1:
- games_list += "'" + file->getName() + "'";
- break;
- case 4:
- games_list += " among other titles.";
- }
- }
-
- desc = "This collection contains " + std::to_string(games_counter) + " games, including " + games_list;
-
- FileData* randomGame = sys->getRandomGame();
-
- video = randomGame->getVideoPath();
- thumbnail = randomGame->getThumbnailPath();
- image = randomGame->getImagePath();
- }
-
-
- rootFolder->metadata.set("desc", desc);
- rootFolder->metadata.set("rating", rating);
- rootFolder->metadata.set("players", players);
- rootFolder->metadata.set("genre", genre);
- rootFolder->metadata.set("releasedate", releasedate);
- rootFolder->metadata.set("developer", developer);
- rootFolder->metadata.set("video", video);
- rootFolder->metadata.set("thumbnail", thumbnail);
- rootFolder->metadata.set("image", image);
-}
-
-void CollectionSystemManager::initCustomCollectionSystems()
-{
- std::vector systems = getCollectionsFromConfigFolder();
- for (auto nameIt = systems.cbegin(); nameIt != systems.cend(); nameIt++)
- {
- addNewCustomCollection(*nameIt);
- }
-}
-
-SystemData* CollectionSystemManager::getAllGamesCollection()
-{
- CollectionSystemData* allSysData = &mAutoCollectionSystemsData["all"];
- if (!allSysData->isPopulated)
- {
- populateAutoCollection(allSysData);
- }
- return allSysData->system;
-}
-
-SystemData* CollectionSystemManager::addNewCustomCollection(std::string name)
-{
- CollectionSystemDecl decl = mCollectionSystemDeclsIndex[myCollectionsName];
- decl.themeFolder = name;
- decl.name = name;
- decl.longName = name;
- return createNewCollectionEntry(name, decl);
-}
-
-// creates a new, empty Collection system, based on the name and declaration
-SystemData* CollectionSystemManager::createNewCollectionEntry(std::string name, CollectionSystemDecl sysDecl, bool index)
-{
- SystemData* newSys = new SystemData(name, sysDecl.longName, mCollectionEnvData, sysDecl.themeFolder, true);
-
- CollectionSystemData newCollectionData;
- newCollectionData.system = newSys;
- newCollectionData.decl = sysDecl;
- newCollectionData.isEnabled = false;
- newCollectionData.isPopulated = false;
- newCollectionData.needsSave = false;
-
- if (index)
- {
- if (!sysDecl.isCustom)
- {
- mAutoCollectionSystemsData[name] = newCollectionData;
- }
- else
- {
- mCustomCollectionSystemsData[name] = newCollectionData;
- }
- }
-
- return newSys;
-}
-
-// populates an Automatic Collection System
-void CollectionSystemManager::populateAutoCollection(CollectionSystemData* sysData)
-{
- SystemData* newSys = sysData->system;
- CollectionSystemDecl sysDecl = sysData->decl;
- FileData* rootFolder = newSys->getRootFolder();
- FileFilterIndex* index = newSys->getIndex();
- for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); sysIt++)
- {
- // we won't iterate all collections
- if ((*sysIt)->isGameSystem() && !(*sysIt)->isCollection()) {
- std::vector files = (*sysIt)->getRootFolder()->getFilesRecursive(GAME);
- for(auto gameIt = files.cbegin(); gameIt != files.cend(); gameIt++)
- {
- bool include = includeFileInAutoCollections((*gameIt));
- switch(sysDecl.type) {
- case AUTO_LAST_PLAYED:
- include = include && (*gameIt)->metadata.get("playcount") > "0";
- break;
- case AUTO_FAVORITES:
- // we may still want to add files we don't want in auto collections in "favorites"
- include = (*gameIt)->metadata.get("favorite") == "true";
- break;
- }
-
- if (include) {
- CollectionFileData* newGame = new CollectionFileData(*gameIt, newSys);
- rootFolder->addChild(newGame);
- index->addToIndex(newGame);
- }
- }
- }
- }
- rootFolder->sort(getSortTypeFromString(sysDecl.defaultSort));
- if (sysDecl.type == AUTO_LAST_PLAYED)
- trimCollectionCount(rootFolder, LAST_PLAYED_MAX);
- sysData->isPopulated = true;
-}
-
-// populates a Custom Collection System
-void CollectionSystemManager::populateCustomCollection(CollectionSystemData* sysData)
-{
- SystemData* newSys = sysData->system;
- sysData->isPopulated = true;
- CollectionSystemDecl sysDecl = sysData->decl;
- std::string path = getCustomCollectionConfigPath(newSys->getName());
-
- if(!Utils::FileSystem::exists(path))
- {
- LOG(LogInfo) << "Couldn't find custom collection config file at " << path;
- return;
- }
- LOG(LogInfo) << "Loading custom collection config file at " << path;
-
- FileData* rootFolder = newSys->getRootFolder();
- FileFilterIndex* index = newSys->getIndex();
-
- // get Configuration for this Custom System
- std::ifstream input(path);
-
- // get all files map
- std::unordered_map allFilesMap = getAllGamesCollection()->getRootFolder()->getChildrenByFilename();
-
- // iterate list of files in config file
-
- for(std::string gameKey; getline(input, gameKey); )
- {
- std::unordered_map::const_iterator it = allFilesMap.find(gameKey);
- if (it != allFilesMap.cend()) {
- CollectionFileData* newGame = new CollectionFileData(it->second, newSys);
- rootFolder->addChild(newGame);
- index->addToIndex(newGame);
- }
- else
- {
- LOG(LogInfo) << "Couldn't find game referenced at '" << gameKey << "' for system config '" << path << "'";
- }
- }
- rootFolder->sort(getSortTypeFromString(sysDecl.defaultSort));
- updateCollectionFolderMetadata(newSys);
-}
-
-/* Handle System View removal and insertion of Collections */
-void CollectionSystemManager::removeCollectionsFromDisplayedSystems()
-{
- // remove all Collection Systems
- for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); )
- {
- if ((*sysIt)->isCollection())
- {
- sysIt = SystemData::sSystemVector.erase(sysIt);
- }
- else
- {
- sysIt++;
- }
- }
-
- // remove all custom collections in bundle
- // this should not delete the objects from memory!
- FileData* customRoot = mCustomCollectionsBundle->getRootFolder();
- std::vector mChildren = customRoot->getChildren();
- for(auto it = mChildren.cbegin(); it != mChildren.cend(); it++)
- {
- customRoot->removeChild(*it);
- }
- // clear index
- mCustomCollectionsBundle->getIndex()->resetIndex();
- // remove view so it's re-created as needed
- ViewController::get()->removeGameListView(mCustomCollectionsBundle);
-}
-
-void CollectionSystemManager::addEnabledCollectionsToDisplayedSystems(std::map* colSystemData)
-{
- // add auto enabled ones
- for(std::map::iterator it = colSystemData->begin() ; it != colSystemData->end() ; it++ )
- {
- if(it->second.isEnabled)
- {
- // check if populated, otherwise populate
- if (!it->second.isPopulated)
- {
- if(it->second.decl.isCustom)
- {
- populateCustomCollection(&(it->second));
- }
- else
- {
- populateAutoCollection(&(it->second));
- }
- }
- // check if it has its own view
- if(!it->second.decl.isCustom || themeFolderExists(it->first) || !Settings::getInstance()->getBool("UseCustomCollectionsSystem"))
- {
- // exists theme folder, or we chose not to bundle it under the custom-collections system
- // so we need to create a view
- SystemData::sSystemVector.push_back(it->second.system);
- }
- else
- {
- FileData* newSysRootFolder = it->second.system->getRootFolder();
- mCustomCollectionsBundle->getRootFolder()->addChild(newSysRootFolder);
- mCustomCollectionsBundle->getIndex()->importIndex(it->second.system->getIndex());
- }
- }
- }
-}
-
-/* Auxiliary methods to get available custom collection possibilities */
-std::vector CollectionSystemManager::getSystemsFromConfig()
-{
- std::vector systems;
- std::string path = SystemData::getConfigPath(false);
-
- if(!Utils::FileSystem::exists(path))
- {
- return systems;
- }
-
- pugi::xml_document doc;
- pugi::xml_parse_result res = doc.load_file(path.c_str());
-
- if(!res)
- {
- return systems;
- }
-
- //actually read the file
- pugi::xml_node systemList = doc.child("systemList");
-
- if(!systemList)
- {
- return systems;
- }
-
- for(pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system"))
- {
- // theme folder
- std::string themeFolder = system.child("theme").text().get();
- systems.push_back(themeFolder);
- }
- std::sort(systems.begin(), systems.end());
- return systems;
-}
-
-// gets all folders from the current theme path
-std::vector CollectionSystemManager::getSystemsFromTheme()
-{
- std::vector systems;
-
- auto themeSets = ThemeData::getThemeSets();
- if(themeSets.empty())
- {
- // no theme sets available
- return systems;
- }
-
- std::map::const_iterator set = themeSets.find(Settings::getInstance()->getString("ThemeSet"));
- if(set == themeSets.cend())
- {
- // currently selected theme set is missing, so just pick the first available set
- set = themeSets.cbegin();
- Settings::getInstance()->setString("ThemeSet", set->first);
- }
-
- std::string themePath = set->second.path;
-
- if (Utils::FileSystem::exists(themePath))
- {
- Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(themePath);
-
- for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it)
- {
- if (Utils::FileSystem::isDirectory(*it))
- {
- //... here you have a directory
- std::string folder = *it;
- folder = folder.substr(themePath.size()+1);
-
- if(Utils::FileSystem::exists(set->second.getThemePath(folder)))
- {
- systems.push_back(folder);
- }
- }
- }
- }
- std::sort(systems.begin(), systems.end());
- return systems;
-}
-
-// returns the unused folders from current theme path
-std::vector CollectionSystemManager::getUnusedSystemsFromTheme()
-{
- // get used systems in es_systems.cfg
- std::vector systemsInUse = getSystemsFromConfig();
- // get available folders in theme
- std::vector themeSys = getSystemsFromTheme();
- // get folders assigned to custom collections
- std::vector autoSys = getCollectionThemeFolders(false);
- // get folder assigned to custom collections
- std::vector customSys = getCollectionThemeFolders(true);
- // get folders assigned to user collections
- std::vector userSys = getUserCollectionThemeFolders();
- // add them all to the list of systems in use
- systemsInUse.insert(systemsInUse.cend(), autoSys.cbegin(), autoSys.cend());
- systemsInUse.insert(systemsInUse.cend(), customSys.cbegin(), customSys.cend());
- systemsInUse.insert(systemsInUse.cend(), userSys.cbegin(), userSys.cend());
-
- for(auto sysIt = themeSys.cbegin(); sysIt != themeSys.cend(); )
- {
- if (std::find(systemsInUse.cbegin(), systemsInUse.cend(), *sysIt) != systemsInUse.cend())
- {
- sysIt = themeSys.erase(sysIt);
- }
- else
- {
- sysIt++;
- }
- }
- return themeSys;
-}
-
-// returns which collection config files exist in the user folder
-std::vector CollectionSystemManager::getCollectionsFromConfigFolder()
-{
- std::vector systems;
- std::string configPath = getCollectionsFolder();
-
- if (Utils::FileSystem::exists(configPath))
- {
- Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(configPath);
- for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it)
- {
- if (Utils::FileSystem::isRegularFile(*it))
- {
- // it's a file
- std::string filename = Utils::FileSystem::getFileName(*it);
-
- // need to confirm filename matches config format
- if (filename != "custom-.cfg" && Utils::String::startsWith(filename, "custom-") && Utils::String::endsWith(filename, ".cfg"))
- {
- filename = filename.substr(7, filename.size()-11);
- systems.push_back(filename);
- }
- else
- {
- LOG(LogInfo) << "Found non-collection config file in collections folder: " << filename;
- }
- }
- }
- }
- return systems;
-}
-
-// returns the theme folders for Automatic Collections (All, Favorites, Last Played) or generic Custom Collections folder
-std::vector CollectionSystemManager::getCollectionThemeFolders(bool custom)
-{
- std::vector systems;
- for(std::map::const_iterator it = mCollectionSystemDeclsIndex.cbegin() ; it != mCollectionSystemDeclsIndex.cend() ; it++ )
- {
- CollectionSystemDecl sysDecl = it->second;
- if (sysDecl.isCustom == custom)
- {
- systems.push_back(sysDecl.themeFolder);
- }
- }
- return systems;
-}
-
-// returns the theme folders in use for the user-defined Custom Collections
-std::vector CollectionSystemManager::getUserCollectionThemeFolders()
-{
- std::vector systems;
- for(std::map::const_iterator it = mCustomCollectionSystemsData.cbegin() ; it != mCustomCollectionSystemsData.cend() ; it++ )
- {
- systems.push_back(it->second.decl.themeFolder);
- }
- return systems;
-}
-
-// returns whether a specific folder exists in the theme
-bool CollectionSystemManager::themeFolderExists(std::string folder)
-{
- std::vector themeSys = getSystemsFromTheme();
- return std::find(themeSys.cbegin(), themeSys.cend(), folder) != themeSys.cend();
-}
-
-bool CollectionSystemManager::includeFileInAutoCollections(FileData* file)
-{
- // we exclude non-game files from collections (i.e. "kodi", entries from non-game systems)
- // if/when there are more in the future, maybe this can be a more complex method, with a proper list
- // but for now a simple string comparison is more performant
- return file->getName() != "kodi" && file->getSystem()->isGameSystem();
-}
-
-std::string getCustomCollectionConfigPath(std::string collectionName)
-{
- return getCollectionsFolder() + "/custom-" + collectionName + ".cfg";
-}
-
-std::string getCollectionsFolder()
-{
- return Utils::FileSystem::getGenericPath(Utils::FileSystem::getHomePath() + "/.emulationstation/collections");
-}
-
-bool systemSort(SystemData* sys1, SystemData* sys2)
-{
- std::string name1 = Utils::String::toUpper(sys1->getName());
- std::string name2 = Utils::String::toUpper(sys2->getName());
- return name1.compare(name2) < 0;
-}
+#include "CollectionSystemManager.h"
+
+#include "guis/GuiInfoPopup.h"
+#include "utils/FileSystemUtil.h"
+#include "utils/StringUtil.h"
+#include "views/gamelist/IGameListView.h"
+#include "views/ViewController.h"
+#include "FileData.h"
+#include "FileFilterIndex.h"
+#include "Log.h"
+#include "Settings.h"
+#include "SystemData.h"
+#include "ThemeData.h"
+#include
+#include
+#include "Gamelist.h"
+#include "FileSorts.h"
+
+std::string myCollectionsName = "collections";
+
+#define LAST_PLAYED_MAX 50
+
+/* Handling the getting, initialization, deinitialization, saving and deletion of
+ * a CollectionSystemManager Instance */
+CollectionSystemManager* CollectionSystemManager::sInstance = NULL;
+
+std::vector CollectionSystemManager::getSystemDecls()
+{
+ CollectionSystemDecl systemDecls[] = {
+ //type name long name //default sort // theme folder // isCustom
+ { AUTO_ALL_GAMES, "all", "all games", "filename, ascending", "auto-allgames", false, true },
+ { AUTO_LAST_PLAYED, "recent", "last played", "last played, descending", "auto-lastplayed", false, true },
+ { AUTO_FAVORITES, "favorites", "favorites", "filename, ascending", "auto-favorites", false, true },
+ { AUTO_AT2PLAYERS, "2players", "2 players", "filename, ascending", "auto-at2players", false, true },
+ { AUTO_AT4PLAYERS, "4players", "4 players", "filename, ascending", "auto-at4players", false, true },
+ { AUTO_NEVER_PLAYED, "neverplayed", "never played", "filename, ascending", "auto-neverplayed", false, true },
+
+ // Arcade meta
+ { AUTO_ARCADE, "arcade", "arcade", "filename, ascending", "arcade", false, true },
+
+ // Arcade systems
+ { CPS1_COLLECTION, "zcps1", "cps1", "filename, ascending", "cps1", false, false },
+ { CPS2_COLLECTION, "zcps2", "cps2", "filename, ascending", "cps2", false, false },
+ { CPS3_COLLECTION, "zcps3", "cps3", "filename, ascending", "cps3", false, false },
+ { CAVE_COLLECTION, "zcave", "cave", "filename, ascending", "cave", false, false },
+ { NEOGEO_COLLECTION, "zneogeo", "neogeo", "filename, ascending", "neogeo", false, false },
+ { SEGA_COLLECTION, "zsega", "sega", "filename, ascending", "sega", false, false },
+ { IREM_COLLECTION, "zirem", "irem", "filename, ascending", "irem", false, false },
+ { MIDWAY_COLLECTION, "zmidway", "midway", "filename, ascending", "midway", false, false },
+ { CAPCOM_COLLECTION, "zcapcom", "capcom", "filename, ascending", "capcom", false, false },
+ { TECMO_COLLECTION, "ztecmo", "tecmo", "filename, ascending", "tecmo", false, false },
+ { SNK_COLLECTION, "zsnk", "snk", "filename, ascending", "snk", false, false },
+ { NAMCO_COLLECTION, "znamco", "namco", "filename, ascending", "namco", false, false },
+ { TAITO_COLLECTION, "ztaito", "taito", "filename, ascending", "taito", false, false },
+ { KONAMI_COLLECTION, "zkonami", "konami", "filename, ascending", "konami", false, false },
+ { JALECO_COLLECTION, "zjaleco", "jaleco", "filename, ascending", "jaleco", false, false },
+ { ATARI_COLLECTION, "zatari", "atari", "filename, ascending", "atari", false, false },
+ { NINTENDO_COLLECTION, "znintendo", "nintendo", "filename, ascending", "nintendo", false, false },
+ { SAMMY_COLLECTION, "zsammy", "sammy", "filename, ascending", "sammy", false, false },
+ { ACCLAIM_COLLECTION, "zacclaim", "acclaim", "filename, ascending", "acclaim", false, false },
+ { PSIKYO_COLLECTION, "zpsiko", "psiko", "filename, ascending", "psiko", false, false },
+ { KANEKO_COLLECTION, "zkaneko", "kaneko", "filename, ascending", "kaneko", false, false },
+ { COLECO_COLLECTION, "zcoleco", "coleco", "filename, ascending", "coleco", false, false },
+ { ATLUS_COLLECTION, "zatlus", "atlus", "filename, ascending", "atlus", false, false },
+ { BANPRESTO_COLLECTION, "zbanpresto", "banpresto", "filename, ascending", "banpresto", false, false },
+
+ { CUSTOM_COLLECTION, myCollectionsName, "collections", "filename, ascending", "custom-collections", true, true }
+ };
+
+ return std::vector(systemDecls, systemDecls + sizeof(systemDecls) / sizeof(systemDecls[0]));
+}
+
+CollectionSystemManager::CollectionSystemManager(Window* window) : mWindow(window)
+{
+ // create a map
+ std::vector tempSystemDecl = getSystemDecls();
+
+ for (std::vector::const_iterator it = tempSystemDecl.cbegin(); it != tempSystemDecl.cend(); ++it )
+ mCollectionSystemDeclsIndex[(*it).name] = (*it);
+
+ // creating standard environment data
+ mCollectionEnvData = new SystemEnvironmentData;
+ mCollectionEnvData->mStartPath = "";
+ mCollectionEnvData->mLaunchCommand = "";
+ std::vector allPlatformIds;
+ allPlatformIds.push_back(PlatformIds::PLATFORM_IGNORE);
+ mCollectionEnvData->mPlatformIds = allPlatformIds;
+
+ std::string path = getCollectionsFolder();
+ if(!Utils::FileSystem::exists(path))
+ Utils::FileSystem::createDirectory(path);
+
+ mIsEditingCustom = false;
+ mEditingCollection = "Favorites";
+ mEditingCollectionSystemData = NULL;
+ mCustomCollectionsBundle = NULL;
+}
+
+CollectionSystemManager::~CollectionSystemManager()
+{
+ assert(sInstance == this);
+ removeCollectionsFromDisplayedSystems();
+
+ // iterate the map
+ for(std::map::const_iterator it = mCustomCollectionSystemsData.cbegin() ; it != mCustomCollectionSystemsData.cend() ; it++ )
+ {
+ if (it->second.isPopulated)
+ {
+ saveCustomCollection(it->second.system);
+ }
+ delete it->second.system;
+ }
+ sInstance = NULL;
+}
+
+CollectionSystemManager* CollectionSystemManager::get()
+{
+ assert(sInstance);
+ return sInstance;
+}
+
+void CollectionSystemManager::init(Window* window)
+{
+ assert(!sInstance);
+ sInstance = new CollectionSystemManager(window);
+}
+
+void CollectionSystemManager::deinit()
+{
+ if (sInstance)
+ {
+ delete sInstance;
+ }
+}
+
+void CollectionSystemManager::saveCustomCollection(SystemData* sys)
+{
+ std::string name = sys->getName();
+ auto games = sys->getRootFolder()->getChildren();
+
+ bool found = mCustomCollectionSystemsData.find(name) != mCustomCollectionSystemsData.cend();
+ if (found)
+ {
+ CollectionSystemData sysData = mCustomCollectionSystemsData.at(name);
+ if (sysData.needsSave)
+ {
+ auto home = Utils::FileSystem::getHomePath();
+
+ std::ofstream configFile;
+ configFile.open(getCustomCollectionConfigPath(name));
+ for(auto iter = games.cbegin(); iter != games.cend(); ++iter)
+ {
+ std::string path = (*iter)->getKey();
+
+ path = Utils::FileSystem::createRelativePath(path, "portnawak", true);
+
+ configFile << path << std::endl;
+ }
+ configFile.close();
+ }
+ }
+ else
+ {
+ LOG(LogError) << "Couldn't find collection to save! " << name;
+ }
+}
+
+/* Methods to load all Collections into memory, and handle enabling the active ones */
+// loads all Collection Systems
+void CollectionSystemManager::loadCollectionSystems(bool async)
+{
+ initAutoCollectionSystems();
+ CollectionSystemDecl decl = mCollectionSystemDeclsIndex[myCollectionsName];
+ mCustomCollectionsBundle = createNewCollectionEntry(decl.name, decl, false);
+ // we will also load custom systems here
+ initCustomCollectionSystems();
+ if(Settings::getInstance()->getString("CollectionSystemsAuto") != "" || Settings::getInstance()->getString("CollectionSystemsCustom") != "")
+ {
+ // Now see which ones are enabled
+ loadEnabledListFromSettings();
+
+
+ // add to the main System Vector, and create Views as needed
+ if (!async)
+ updateSystemsList();
+ }
+}
+
+// loads settings
+void CollectionSystemManager::loadEnabledListFromSettings()
+{
+ // we parse the auto collection settings list
+ std::vector autoSelected = Utils::String::commaStringToVector(Settings::getInstance()->getString("CollectionSystemsAuto"));
+
+ // iterate the map
+ for(std::map::iterator it = mAutoCollectionSystemsData.begin() ; it != mAutoCollectionSystemsData.end() ; it++ )
+ {
+ it->second.isEnabled = (std::find(autoSelected.cbegin(), autoSelected.cend(), it->first) != autoSelected.cend());
+ }
+
+ // we parse the custom collection settings list
+ std::vector customSelected = Utils::String::commaStringToVector(Settings::getInstance()->getString("CollectionSystemsCustom"));
+
+ // iterate the map
+ for(std::map::iterator it = mCustomCollectionSystemsData.begin() ; it != mCustomCollectionSystemsData.end() ; it++ )
+ {
+ it->second.isEnabled = (std::find(customSelected.cbegin(), customSelected.cend(), it->first) != customSelected.cend());
+ }
+}
+
+// updates enabled system list in System View
+void CollectionSystemManager::updateSystemsList()
+{
+ // remove all Collection Systems
+ removeCollectionsFromDisplayedSystems();
+
+ std::unordered_map map;
+ getAllGamesCollection()->getRootFolder()->createChildrenByFilenameMap(map);
+
+ // add custom enabled ones
+ addEnabledCollectionsToDisplayedSystems(&mCustomCollectionSystemsData, &map);
+
+ if (Settings::getInstance()->getBool("SortAllSystems"))
+ {
+ // sort custom individual systems with other systems
+ std::sort(SystemData::sSystemVector.begin(), SystemData::sSystemVector.end(), systemSort);
+
+ // move RetroPie system to end, before auto collections
+ for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); )
+ {
+ if ((*sysIt)->getName() == "retropie")
+ {
+ SystemData* retroPieSystem = (*sysIt);
+ sysIt = SystemData::sSystemVector.erase(sysIt);
+ SystemData::sSystemVector.push_back(retroPieSystem);
+ break;
+ }
+ else
+ {
+ sysIt++;
+ }
+ }
+ }
+
+ if(mCustomCollectionsBundle->getRootFolder()->getChildren().size() > 0)
+ SystemData::sSystemVector.push_back(mCustomCollectionsBundle);
+
+ // add auto enabled ones
+ addEnabledCollectionsToDisplayedSystems(&mAutoCollectionSystemsData, &map);
+ /*
+ // create views for collections, before reload
+ for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); sysIt++)
+ {
+ if ((*sysIt)->isCollection())
+ {
+ ViewController::get()->getGameListView((*sysIt));
+ }
+ }*/
+
+ // if we were editing a custom collection, and it's no longer enabled, exit edit mode
+ if(mIsEditingCustom && !mEditingCollectionSystemData->isEnabled)
+ {
+ exitEditMode();
+ }
+}
+
+/* Methods to manage collection files related to a source FileData */
+// updates all collection files related to the source file
+void CollectionSystemManager::refreshCollectionSystems(FileData* file)
+{
+ if (!file->getSystem()->isGameSystem() || file->getType() != GAME)
+ return;
+
+ std::map allCollections;
+ allCollections.insert(mAutoCollectionSystemsData.cbegin(), mAutoCollectionSystemsData.cend());
+ allCollections.insert(mCustomCollectionSystemsData.cbegin(), mCustomCollectionSystemsData.cend());
+
+ for(auto sysDataIt = allCollections.cbegin(); sysDataIt != allCollections.cend(); sysDataIt++)
+ {
+ updateCollectionSystem(file, sysDataIt->second);
+ }
+}
+
+void CollectionSystemManager::updateCollectionSystem(FileData* file, CollectionSystemData sysData)
+{
+ if (sysData.isPopulated)
+ {
+ // collection files use the full path as key, to avoid clashes
+ std::string key = file->getFullPath();
+
+ SystemData* curSys = sysData.system;
+ FileData* collectionEntry = curSys->getRootFolder()->FindByPath(key);
+
+ FolderData* rootFolder = curSys->getRootFolder();
+
+ std::string name = curSys->getName();
+
+ if (collectionEntry != nullptr)
+ {
+ // if we found it, we need to update it
+ // remove from index, so we can re-index metadata after refreshing
+ curSys->removeFromIndex(collectionEntry);
+ collectionEntry->refreshMetadata();
+ // found and we are removing
+ if (name == "favorites" && file->getMetadata().get("favorite") == "false") {
+ // need to check if still marked as favorite, if not remove
+ ViewController::get()->getGameListView(curSys).get()->remove(collectionEntry, false);
+
+ ViewController::get()->onFileChanged(file, FILE_METADATA_CHANGED);
+ ViewController::get()->getGameListView(curSys)->onFileChanged(collectionEntry, FILE_METADATA_CHANGED);
+ }
+ else
+ {
+ // re-index with new metadata
+ curSys->addToIndex(collectionEntry);
+ ViewController::get()->onFileChanged(collectionEntry, FILE_METADATA_CHANGED);
+ }
+ }
+ else
+ {
+ // we didn't find it here - we need to check if we should add it
+ if (name == "recent" && file->getMetadata().get("playcount") > "0" && includeFileInAutoCollections(file) ||
+ name == "favorites" && file->getMetadata().get("favorite") == "true") {
+ CollectionFileData* newGame = new CollectionFileData(file, curSys);
+ rootFolder->addChild(newGame);
+ curSys->addToIndex(newGame);
+
+ ViewController::get()->onFileChanged(file, FILE_METADATA_CHANGED);
+ ViewController::get()->getGameListView(curSys)->onFileChanged(newGame, FILE_METADATA_CHANGED);
+ }
+ }
+
+ curSys->updateDisplayedGameCount();
+
+ if (name == "recent")
+ {
+ sortLastPlayed(curSys);
+ trimCollectionCount(rootFolder, LAST_PLAYED_MAX);
+ ViewController::get()->onFileChanged(rootFolder, FILE_METADATA_CHANGED);
+ }
+ else
+ ViewController::get()->onFileChanged(rootFolder, FILE_SORTED);
+ }
+}
+
+void CollectionSystemManager::sortLastPlayed(SystemData* system)
+{
+ if (system->getName() != "recent")
+ return;
+
+ FolderData* rootFolder = system->getRootFolder();
+ system->setSortId(FileSorts::LASTPLAYED_DESCENDING);
+
+ const FileSorts::SortType& sort = FileSorts::getSortTypes().at(system->getSortId());
+
+ std::vector& childs = (std::vector&) rootFolder->getChildren();
+ std::sort(childs.begin(), childs.end(), sort.comparisonFunction);
+ if (!sort.ascending)
+ std::reverse(childs.begin(), childs.end());
+}
+
+void CollectionSystemManager::trimCollectionCount(FolderData* rootFolder, int limit)
+{
+ SystemData* curSys = rootFolder->getSystem();
+ std::shared_ptr listView = ViewController::get()->getGameListView(curSys, false);
+
+ auto& childs = rootFolder->getChildren();
+ while ((int)childs.size() > limit)
+ {
+ CollectionFileData* gameToRemove = (CollectionFileData*)childs.back();
+ if (listView == nullptr)
+ delete gameToRemove;
+ else
+ listView.get()->remove(gameToRemove, false);
+ }
+}
+
+// deletes all collection files from collection systems related to the source file
+void CollectionSystemManager::deleteCollectionFiles(FileData* file)
+{
+ // collection files use the full path as key, to avoid clashes
+ std::string key = file->getFullPath();
+ // find games in collection systems
+ std::map allCollections;
+ allCollections.insert(mAutoCollectionSystemsData.cbegin(), mAutoCollectionSystemsData.cend());
+ allCollections.insert(mCustomCollectionSystemsData.cbegin(), mCustomCollectionSystemsData.cend());
+
+ for(auto sysDataIt = allCollections.begin(); sysDataIt != allCollections.end(); sysDataIt++)
+ {
+ if (sysDataIt->second.isPopulated)
+ {
+ FileData* collectionEntry = (sysDataIt->second.system)->getRootFolder()->FindByPath(key);
+ if (collectionEntry != nullptr)
+ {
+ sysDataIt->second.needsSave = true;
+ SystemData* systemViewToUpdate = getSystemToView(sysDataIt->second.system);
+ ViewController::get()->getGameListView(systemViewToUpdate).get()->remove(collectionEntry, false);
+ }
+ }
+ }
+}
+
+// returns whether the current theme is compatible with Automatic or Custom Collections
+bool CollectionSystemManager::isThemeGenericCollectionCompatible(bool genericCustomCollections)
+{
+ std::vector cfgSys = getCollectionThemeFolders(genericCustomCollections);
+ for(auto sysIt = cfgSys.cbegin(); sysIt != cfgSys.cend(); sysIt++)
+ {
+ if(!themeFolderExists(*sysIt))
+ return false;
+ }
+ return true;
+}
+
+bool CollectionSystemManager::isThemeCustomCollectionCompatible(std::vector stringVector)
+{
+ if (isThemeGenericCollectionCompatible(true))
+ return true;
+
+ // get theme path
+ auto themeSets = ThemeData::getThemeSets();
+ auto set = themeSets.find(Settings::getInstance()->getString("ThemeSet"));
+ if(set != themeSets.cend())
+ {
+ std::string defaultThemeFilePath = set->second.path + "/theme.xml";
+ if (Utils::FileSystem::exists(defaultThemeFilePath))
+ {
+ return true;
+ }
+ }
+
+ for(auto sysIt = stringVector.cbegin(); sysIt != stringVector.cend(); sysIt++)
+ {
+ if(!themeFolderExists(*sysIt))
+ return false;
+ }
+ return true;
+}
+
+std::string CollectionSystemManager::getValidNewCollectionName(std::string inName, int index)
+{
+ std::string name = inName;
+
+ if(index == 0)
+ {
+ size_t remove = std::string::npos;
+
+ // get valid name
+ while((remove = name.find_first_not_of("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-[]() ")) != std::string::npos)
+ {
+ name.erase(remove, 1);
+ }
+ }
+ else
+ {
+ name += " (" + std::to_string(index) + ")";
+ }
+
+ if(name == "")
+ {
+ name = "New Collection";
+ }
+
+ if(name != inName)
+ {
+ LOG(LogInfo) << "Had to change name, from: " << inName << " to: " << name;
+ }
+
+ // get used systems in es_systems.cfg
+ std::vector systemsInUse = getSystemsFromConfig();
+ // get folders assigned to custom collections
+ std::vector autoSys = getCollectionThemeFolders(false);
+ // get folder assigned to custom collections
+ std::vector customSys = getCollectionThemeFolders(true);
+ // get folders assigned to user collections
+ std::vector userSys = getUserCollectionThemeFolders();
+ // add them all to the list of systems in use
+ systemsInUse.insert(systemsInUse.cend(), autoSys.cbegin(), autoSys.cend());
+ systemsInUse.insert(systemsInUse.cend(), customSys.cbegin(), customSys.cend());
+ systemsInUse.insert(systemsInUse.cend(), userSys.cbegin(), userSys.cend());
+ for(auto sysIt = systemsInUse.cbegin(); sysIt != systemsInUse.cend(); sysIt++)
+ {
+ if (*sysIt == name)
+ {
+ if(index > 0) {
+ name = name.substr(0, name.size()-4);
+ }
+ return getValidNewCollectionName(name, index+1);
+ }
+ }
+ // if it matches one of the custom collections reserved names
+ if (mCollectionSystemDeclsIndex.find(name) != mCollectionSystemDeclsIndex.cend())
+ return getValidNewCollectionName(name, index+1);
+ return name;
+}
+
+void CollectionSystemManager::setEditMode(std::string collectionName)
+{
+ if (mCustomCollectionSystemsData.find(collectionName) == mCustomCollectionSystemsData.cend())
+ {
+ LOG(LogError) << "Tried to edit a non-existing collection: " << collectionName;
+ return;
+ }
+ mIsEditingCustom = true;
+ mEditingCollection = collectionName;
+
+ CollectionSystemData* sysData = &(mCustomCollectionSystemsData.at(mEditingCollection));
+ if (!sysData->isPopulated)
+ populateCustomCollection(sysData);
+
+ // if it's bundled, this needs to be the bundle system
+ mEditingCollectionSystemData = sysData;
+
+ char strbuf[512];
+ snprintf(strbuf, 512, _("Editing the '%s' Collection. Add/remove games with Y.").c_str(), Utils::String::toUpper(collectionName).c_str());
+ mWindow->displayNotificationMessage(strbuf, 10000);
+}
+
+void CollectionSystemManager::exitEditMode()
+{
+ char strbuf[512];
+ snprintf(strbuf, 512, _("Finished editing the '%s' Collection.").c_str(), mEditingCollection.c_str());
+ mWindow->displayNotificationMessage(strbuf, 10000);
+ mIsEditingCustom = false;
+ mEditingCollection = "Favorites";
+}
+
+// adds or removes a game from a specific collection
+bool CollectionSystemManager::toggleGameInCollection(FileData* file)
+{
+ if (file->getType() == GAME)
+ {
+ GuiInfoPopup* s;
+ bool adding = true;
+ std::string name = file->getName();
+ std::string sysName = mEditingCollection;
+ if (mIsEditingCustom)
+ {
+ SystemData* sysData = mEditingCollectionSystemData->system;
+ mEditingCollectionSystemData->needsSave = true;
+ if (!mEditingCollectionSystemData->isPopulated)
+ populateCustomCollection(mEditingCollectionSystemData);
+
+ std::string key = file->getFullPath();
+ FolderData* rootFolder = sysData->getRootFolder();
+
+ FileData* collectionEntry = rootFolder->FindByPath(key);
+
+ std::string name = sysData->getName();
+
+ SystemData* systemViewToUpdate = getSystemToView(sysData);
+
+ if (collectionEntry != nullptr) {
+ adding = false;
+ // if we found it, we need to remove it
+ // remove from index
+ sysData->removeFromIndex(collectionEntry);
+ // remove from bundle index as well, if needed
+ if (systemViewToUpdate != sysData)
+ systemViewToUpdate->removeFromIndex(collectionEntry);
+
+ ViewController::get()->getGameListView(systemViewToUpdate).get()->remove(collectionEntry, false);
+ }
+ else
+ {
+ // we didn't find it here, we should add it
+ CollectionFileData* newGame = new CollectionFileData(file, sysData);
+ rootFolder->addChild(newGame);
+ sysData->addToIndex(newGame);
+ ViewController::get()->getGameListView(systemViewToUpdate)->onFileChanged(newGame, FILE_METADATA_CHANGED);
+ ViewController::get()->onFileChanged(systemViewToUpdate->getRootFolder(), FILE_SORTED);
+ // add to bundle index as well, if needed
+ if(systemViewToUpdate != sysData)
+ {
+ systemViewToUpdate->addToIndex(newGame);
+ }
+ }
+ updateCollectionFolderMetadata(sysData);
+ }
+ else
+ {
+ SystemData* sysData = file->getSourceFileData()->getSystem();
+ sysData->removeFromIndex(file);
+
+ MetaDataList* md = &file->getSourceFileData()->getMetadata();
+
+ std::string value = md->get("favorite");
+ if (value == "false")
+ md->set("favorite", "true");
+ else
+ {
+ adding = false;
+ md->set("favorite", "false");
+ }
+ sysData->addToIndex(file);
+ saveToGamelistRecovery(file);
+
+ refreshCollectionSystems(file->getSourceFileData());
+
+ SystemData* systemViewToUpdate = getSystemToView(sysData);
+ if (systemViewToUpdate != NULL)
+ {
+ ViewController::get()->onFileChanged(file, FILE_METADATA_CHANGED);
+ ViewController::get()->getGameListView(systemViewToUpdate)->onFileChanged(file, FILE_METADATA_CHANGED);
+ }
+
+
+ }
+
+ char trstring[512];
+
+ if (adding)
+ snprintf(trstring, 512, _("Added '%s' to '%s'").c_str(), Utils::String::removeParenthesis(name).c_str(), Utils::String::toUpper(sysName).c_str()); // batocera
+ else
+ snprintf(trstring, 512, _("Removed '%s' from '%s'").c_str(), Utils::String::removeParenthesis(name).c_str(), Utils::String::toUpper(sysName).c_str()); // batocera
+
+ mWindow->displayNotificationMessage(trstring, 4000);
+
+ return true;
+ }
+ return false;
+}
+
+SystemData* CollectionSystemManager::getSystemToView(SystemData* sys)
+{
+ SystemData* systemToView = sys;
+ FileData* rootFolder = sys->getRootFolder();
+
+ FolderData* bundleRootFolder = mCustomCollectionsBundle->getRootFolder();
+
+ // is the rootFolder bundled in the "My Collections" system?
+ bool sysFoundInBundle = bundleRootFolder->FindByPath(rootFolder->getKey()) != nullptr;
+ if (sysFoundInBundle && sys->isCollection())
+ {
+ systemToView = mCustomCollectionsBundle;
+ }
+ return systemToView;
+}
+
+/* Handles loading a collection system, creating an empty one, and populating on demand */
+// loads Automatic Collection systems (All, Favorites, Last Played)
+void CollectionSystemManager::initAutoCollectionSystems()
+{
+ for(std::map::const_iterator it = mCollectionSystemDeclsIndex.cbegin() ; it != mCollectionSystemDeclsIndex.cend() ; it++ )
+ {
+ CollectionSystemDecl sysDecl = it->second;
+ if (!sysDecl.isCustom)
+ {
+ createNewCollectionEntry(sysDecl.name, sysDecl);
+ }
+ }
+}
+
+// this may come in handy if at any point in time in the future we want to
+// automatically generate metadata for a folder
+void CollectionSystemManager::updateCollectionFolderMetadata(SystemData* sys)
+{
+ FolderData* rootFolder = sys->getRootFolder();
+
+ std::string desc = _("This collection is empty.");
+ std::string rating = "0";
+ std::string players = "1";
+ std::string releasedate = "N/A";
+ std::string developer = _("None");
+ std::string genre = _("None");
+ std::string video = "";
+ std::string thumbnail = "";
+ std::string image = "";
+
+ auto games = rootFolder->getChildren();
+
+ if(games.size() > 0)
+ {
+ std::string games_list = "";
+ int games_counter = 0;
+ for(auto iter = games.cbegin(); iter != games.cend(); ++iter)
+ {
+ games_counter++;
+ FileData* file = *iter;
+
+ std::string new_rating = file->getMetadata().get("rating");
+ std::string new_releasedate = file->getMetadata().get("releasedate");
+ std::string new_developer = file->getMetadata().get("developer");
+ std::string new_genre = file->getMetadata().get("genre");
+ std::string new_players = file->getMetadata().get("players");
+
+ rating = (new_rating > rating ? (new_rating != "" ? new_rating : rating) : rating);
+ players = (new_players > players ? (new_players != "" ? new_players : players) : players);
+ releasedate = (new_releasedate < releasedate ? (new_releasedate != "" ? new_releasedate : releasedate) : releasedate);
+ developer = (developer == _("None") ? new_developer : (new_developer != developer ? _("Various") : new_developer));
+ genre = (genre == _("None") ? new_genre : (new_genre != genre ? _("Various") : new_genre));
+
+ switch(games_counter)
+ {
+ case 2:
+ case 3:
+ games_list += ", ";
+ case 1:
+ games_list += "'" + file->getName() + "'";
+ break;
+ case 4:
+ games_list += " " + _("among other titles.");
+ }
+ }
+
+ desc = _("This collection contains") + " " + std::to_string(games_counter) + " " + _("games, including") + " " + games_list;
+
+ FileData* randomGame = sys->getRandomGame();
+ if (randomGame != nullptr)
+ {
+ video = randomGame->getVideoPath();
+ thumbnail = randomGame->getThumbnailPath();
+ image = randomGame->getImagePath();
+ }
+ }
+
+
+ rootFolder->getMetadata().set("desc", desc);
+ rootFolder->getMetadata().set("rating", rating);
+ rootFolder->getMetadata().set("players", players);
+ rootFolder->getMetadata().set("genre", genre);
+ rootFolder->getMetadata().set("releasedate", releasedate);
+ rootFolder->getMetadata().set("developer", developer);
+ rootFolder->getMetadata().set("video", video);
+ rootFolder->getMetadata().set("thumbnail", thumbnail);
+ rootFolder->getMetadata().set("image", image);
+}
+
+void CollectionSystemManager::initCustomCollectionSystems()
+{
+ std::vector systems = getCollectionsFromConfigFolder();
+ for (auto nameIt = systems.cbegin(); nameIt != systems.cend(); nameIt++)
+ {
+ addNewCustomCollection(*nameIt);
+ }
+}
+
+SystemData* CollectionSystemManager::getAllGamesCollection()
+{
+ CollectionSystemData* allSysData = &mAutoCollectionSystemsData["all"];
+ if (!allSysData->isPopulated)
+ {
+ populateAutoCollection(allSysData);
+ }
+ return allSysData->system;
+}
+
+SystemData* CollectionSystemManager::addNewCustomCollection(std::string name)
+{
+ CollectionSystemDecl decl = mCollectionSystemDeclsIndex[myCollectionsName];
+ decl.themeFolder = name;
+ decl.name = name;
+ decl.longName = name;
+ return createNewCollectionEntry(name, decl);
+}
+
+// creates a new, empty Collection system, based on the name and declaration
+SystemData* CollectionSystemManager::createNewCollectionEntry(std::string name, CollectionSystemDecl sysDecl, bool index)
+{
+ SystemData* newSys = new SystemData(name, sysDecl.longName, mCollectionEnvData, sysDecl.themeFolder, true);
+
+ CollectionSystemData newCollectionData;
+ newCollectionData.system = newSys;
+ newCollectionData.decl = sysDecl;
+ newCollectionData.isEnabled = false;
+ newCollectionData.isPopulated = false;
+ newCollectionData.needsSave = false;
+
+ if (index)
+ {
+ if (!sysDecl.isCustom)
+ {
+ mAutoCollectionSystemsData[name] = newCollectionData;
+ }
+ else
+ {
+ mCustomCollectionSystemsData[name] = newCollectionData;
+ }
+ }
+
+ return newSys;
+}
+
+// populates an Automatic Collection System
+void CollectionSystemManager::populateAutoCollection(CollectionSystemData* sysData)
+{
+ SystemData* newSys = sysData->system;
+ CollectionSystemDecl sysDecl = sysData->decl;
+ FolderData* rootFolder = newSys->getRootFolder();
+
+ for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); sysIt++)
+ {
+ std::vector platforms = (*sysIt)->getPlatformIds();
+ bool isArcade = std::find(platforms.begin(), platforms.end(), PlatformIds::ARCADE) != platforms.end();
+
+ // we won't iterate all collections
+ if ((*sysIt)->isGameSystem() && !(*sysIt)->isCollection())
+ {
+ std::vector files = (*sysIt)->getRootFolder()->getFilesRecursive(GAME);
+ for(auto gameIt = files.cbegin(); gameIt != files.cend(); gameIt++)
+ {
+ std::string systemarcadename;
+
+ bool include = includeFileInAutoCollections((*gameIt));
+ switch(sysDecl.type)
+ {
+ case AUTO_LAST_PLAYED:
+ include = include && (*gameIt)->getMetadata().get("playcount") > "0";
+ break;
+ case AUTO_NEVER_PLAYED:
+ include = include && !((*gameIt)->getMetadata("playcount") > "0");
+ break;
+ case AUTO_FAVORITES:
+ // we may still want to add files we don't want in auto collections in "favorites"
+ include = (*gameIt)->getMetadata().get("favorite") == "true";
+ break;
+ case AUTO_ARCADE:
+ include = include && isArcade;
+ break;
+ case CPS1_COLLECTION:
+ systemarcadename = "cps1";
+ break;
+ case CPS2_COLLECTION:
+ systemarcadename = "cps2";
+ break;
+ case CPS3_COLLECTION:
+ systemarcadename = "cps3";
+ break;
+ case CAVE_COLLECTION:
+ systemarcadename = "cave";
+ break;
+ case NEOGEO_COLLECTION:
+ systemarcadename = "neogeo";
+ break;
+ case SEGA_COLLECTION:
+ systemarcadename = "sega";
+ break;
+ case IREM_COLLECTION:
+ systemarcadename = "irem";
+ break;
+ case MIDWAY_COLLECTION:
+ systemarcadename = "midway";
+ break;
+ case CAPCOM_COLLECTION:
+ systemarcadename = "capcom";
+ break;
+ case TECMO_COLLECTION:
+ systemarcadename = "techmo";
+ break;
+ case SNK_COLLECTION:
+ systemarcadename = "snk";
+ break;
+ case NAMCO_COLLECTION:
+ systemarcadename = "namco";
+ break;
+ case TAITO_COLLECTION:
+ systemarcadename = "taito";
+ break;
+ case KONAMI_COLLECTION:
+ systemarcadename = "konami";
+ break;
+ case JALECO_COLLECTION:
+ systemarcadename = "jaleco";
+ break;
+ case ATARI_COLLECTION:
+ systemarcadename = "atari";
+ break;
+ case NINTENDO_COLLECTION:
+ systemarcadename = "nintendo";
+ break;
+ case SAMMY_COLLECTION:
+ systemarcadename = "sammy";
+ break;
+ case ACCLAIM_COLLECTION:
+ systemarcadename = "acclaim";
+ break;
+ case PSIKYO_COLLECTION:
+ systemarcadename = "psikyo";
+ break;
+ case KANEKO_COLLECTION:
+ systemarcadename = "kaneko";
+ break;
+ case COLECO_COLLECTION:
+ systemarcadename = "coleco";
+ break;
+ case ATLUS_COLLECTION:
+ systemarcadename = "atlus";
+ break;
+ case BANPRESTO_COLLECTION:
+ systemarcadename = "banpresto";
+ break;
+
+ case AUTO_AT2PLAYERS:
+ case AUTO_AT4PLAYERS:
+ {
+ std::string players = (*gameIt)->getMetadata("players");
+ if (players.empty())
+ include = false;
+ else
+ {
+ int min = -1;
+
+ auto split = players.rfind("+");
+ if (split != std::string::npos)
+ players = Utils::String::replace(players, "+", "-999");
+
+ split = players.rfind("-");
+ if (split != std::string::npos)
+ {
+ min = atoi(players.substr(0, split).c_str());
+ players = players.substr(split + 1);
+ }
+
+ int max = atoi(players.c_str());
+ int val = (sysDecl.type == AUTO_AT2PLAYERS ? 2 : 4);
+ include = min <= 0 ? (val == max) : (min <= val && val <= max);
+ }
+ }
+ break;
+ }
+
+ if (!systemarcadename.empty())
+ include = isArcade && (*gameIt)->getMetadata("arcadesystemname") == systemarcadename;
+
+ if (include)
+ {
+ CollectionFileData* newGame = new CollectionFileData(*gameIt, newSys);
+ rootFolder->addChild(newGame);
+ newSys->addToIndex(newGame);
+ }
+ }
+ }
+ }
+ if (sysDecl.type == AUTO_LAST_PLAYED)
+ {
+ sortLastPlayed(newSys);
+ trimCollectionCount(rootFolder, LAST_PLAYED_MAX);
+ }
+
+ sysData->isPopulated = true;
+}
+
+// populates a Custom Collection System
+void CollectionSystemManager::populateCustomCollection(CollectionSystemData* sysData, std::unordered_map* pMap)
+{
+ SystemData* newSys = sysData->system;
+ sysData->isPopulated = true;
+ CollectionSystemDecl sysDecl = sysData->decl;
+ std::string path = getCustomCollectionConfigPath(newSys->getName());
+
+ if(!Utils::FileSystem::exists(path))
+ {
+ LOG(LogInfo) << "Couldn't find custom collection config file at " << path;
+ return;
+ }
+ LOG(LogInfo) << "Loading custom collection config file at " << path;
+
+ FolderData* rootFolder = newSys->getRootFolder();
+
+ // get Configuration for this Custom System
+ std::ifstream input(path);
+
+ FolderData* folder = getAllGamesCollection()->getRootFolder();
+
+ std::unordered_map map;
+
+ if (pMap == nullptr)
+ {
+ folder->createChildrenByFilenameMap(map);
+ pMap = ↦
+ }
+
+ // iterate list of files in config file
+ for(std::string gameKey; getline(input, gameKey); )
+ {
+ gameKey = Utils::FileSystem::resolveRelativePath(gameKey, "portnawak", true);
+
+ std::unordered_map::const_iterator it = pMap->find(gameKey);
+ if (it != pMap->cend())
+ {
+ CollectionFileData* newGame = new CollectionFileData(it->second, newSys);
+ rootFolder->addChild(newGame);
+ newSys->addToIndex(newGame);
+ }
+ else
+ {
+ LOG(LogInfo) << "Couldn't find game referenced at '" << gameKey << "' for system config '" << path << "'";
+ }
+ }
+ updateCollectionFolderMetadata(newSys);
+}
+
+/* Handle System View removal and insertion of Collections */
+void CollectionSystemManager::removeCollectionsFromDisplayedSystems()
+{
+ // remove all Collection Systems
+ for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); )
+ {
+ if ((*sysIt)->isCollection())
+ {
+ sysIt = SystemData::sSystemVector.erase(sysIt);
+ }
+ else
+ {
+ sysIt++;
+ }
+ }
+
+ // remove all custom collections in bundle
+ // this should not delete the objects from memory!
+ FolderData* customRoot = mCustomCollectionsBundle->getRootFolder();
+ std::vector mChildren = customRoot->getChildren();
+ for(auto it = mChildren.cbegin(); it != mChildren.cend(); it++)
+ {
+ customRoot->removeChild(*it);
+ }
+ // clear index
+ mCustomCollectionsBundle->resetIndex();
+ // remove view so it's re-created as needed
+ ViewController::get()->removeGameListView(mCustomCollectionsBundle);
+}
+
+void CollectionSystemManager::addEnabledCollectionsToDisplayedSystems(std::map* colSystemData, std::unordered_map* pMap)
+{
+ // add auto enabled ones
+ for(std::map::iterator it = colSystemData->begin() ; it != colSystemData->end() ; it++ )
+ {
+ if(it->second.isEnabled)
+ {
+ // check if populated, otherwise populate
+ if (!it->second.isPopulated)
+ {
+ if(it->second.decl.isCustom)
+ {
+ populateCustomCollection(&(it->second), pMap);
+ }
+ else
+ {
+ populateAutoCollection(&(it->second));
+ }
+ }
+ // check if it has its own view
+ if(!it->second.decl.isCustom || themeFolderExists(it->first) || !Settings::getInstance()->getBool("UseCustomCollectionsSystem"))
+ {
+ if (it->second.decl.displayIfEmpty || it->second.system->getRootFolder()->getChildren().size() > 0)
+ {
+ // exists theme folder, or we chose not to bundle it under the custom-collections system
+ // so we need to create a view
+ if (it->second.isEnabled)
+ SystemData::sSystemVector.push_back(it->second.system);
+ }
+ }
+ else
+ {
+ FileData* newSysRootFolder = it->second.system->getRootFolder();
+ mCustomCollectionsBundle->getRootFolder()->addChild(newSysRootFolder);
+ mCustomCollectionsBundle->getIndex(true)->importIndex(it->second.system->getIndex(true));
+ }
+ }
+ }
+}
+
+/* Auxiliary methods to get available custom collection possibilities */
+std::vector CollectionSystemManager::getSystemsFromConfig()
+{
+ std::vector systems;
+ std::string path = SystemData::getConfigPath(false);
+
+ if(!Utils::FileSystem::exists(path))
+ {
+ return systems;
+ }
+
+ pugi::xml_document doc;
+ pugi::xml_parse_result res = doc.load_file(path.c_str());
+
+ if(!res)
+ {
+ return systems;
+ }
+
+ //actually read the file
+ pugi::xml_node systemList = doc.child("systemList");
+
+ if(!systemList)
+ {
+ return systems;
+ }
+
+ for(pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system"))
+ {
+ // theme folder
+ std::string themeFolder = system.child("theme").text().get();
+ systems.push_back(themeFolder);
+ }
+ std::sort(systems.begin(), systems.end());
+ return systems;
+}
+
+// gets all folders from the current theme path
+std::vector CollectionSystemManager::getSystemsFromTheme()
+{
+ std::vector systems;
+
+ auto themeSets = ThemeData::getThemeSets();
+ if(themeSets.empty())
+ {
+ // no theme sets available
+ return systems;
+ }
+
+ std::map::const_iterator set = themeSets.find(Settings::getInstance()->getString("ThemeSet"));
+ if(set == themeSets.cend())
+ {
+ // currently selected theme set is missing, so just pick the first available set
+ set = themeSets.cbegin();
+ Settings::getInstance()->setString("ThemeSet", set->first);
+ }
+
+ std::string themePath = set->second.path;
+
+ if (Utils::FileSystem::exists(themePath))
+ {
+ Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(themePath);
+
+ for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it)
+ {
+ if (Utils::FileSystem::isDirectory(*it))
+ {
+ //... here you have a directory
+ std::string folder = *it;
+ folder = folder.substr(themePath.size()+1);
+
+ if(Utils::FileSystem::exists(set->second.getThemePath(folder)))
+ {
+ systems.push_back(folder);
+ }
+ }
+ }
+ }
+ std::sort(systems.begin(), systems.end());
+ return systems;
+}
+
+// returns the unused folders from current theme path
+std::vector CollectionSystemManager::getUnusedSystemsFromTheme()
+{
+ // get used systems in es_systems.cfg
+ std::vector systemsInUse = getSystemsFromConfig();
+ // get available folders in theme
+ std::vector themeSys = getSystemsFromTheme();
+ // get folders assigned to custom collections
+ std::vector autoSys = getCollectionThemeFolders(false);
+ // get folder assigned to custom collections
+ std::vector customSys = getCollectionThemeFolders(true);
+ // get folders assigned to user collections
+ std::vector userSys = getUserCollectionThemeFolders();
+ // add them all to the list of systems in use
+ systemsInUse.insert(systemsInUse.cend(), autoSys.cbegin(), autoSys.cend());
+ systemsInUse.insert(systemsInUse.cend(), customSys.cbegin(), customSys.cend());
+ systemsInUse.insert(systemsInUse.cend(), userSys.cbegin(), userSys.cend());
+
+ for(auto sysIt = themeSys.cbegin(); sysIt != themeSys.cend(); )
+ {
+ if (std::find(systemsInUse.cbegin(), systemsInUse.cend(), *sysIt) != systemsInUse.cend())
+ {
+ sysIt = themeSys.erase(sysIt);
+ }
+ else
+ {
+ sysIt++;
+ }
+ }
+ return themeSys;
+}
+
+// returns which collection config files exist in the user folder
+std::vector CollectionSystemManager::getCollectionsFromConfigFolder()
+{
+ std::vector systems;
+ std::string configPath = getCollectionsFolder();
+
+ if (Utils::FileSystem::exists(configPath))
+ {
+ Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(configPath);
+ for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it)
+ {
+ if (Utils::FileSystem::isRegularFile(*it))
+ {
+ // it's a file
+ std::string filename = Utils::FileSystem::getFileName(*it);
+
+ // need to confirm filename matches config format
+ if (filename != "custom-.cfg" && Utils::String::startsWith(filename, "custom-") && Utils::String::endsWith(filename, ".cfg"))
+ {
+ filename = filename.substr(7, filename.size()-11);
+ systems.push_back(filename);
+ }
+ else
+ {
+ LOG(LogInfo) << "Found non-collection config file in collections folder: " << filename;
+ }
+ }
+ }
+ }
+ return systems;
+}
+
+// returns the theme folders for Automatic Collections (All, Favorites, Last Played) or generic Custom Collections folder
+std::vector CollectionSystemManager::getCollectionThemeFolders(bool custom)
+{
+ std::vector systems;
+ for(std::map::const_iterator it = mCollectionSystemDeclsIndex.cbegin() ; it != mCollectionSystemDeclsIndex.cend() ; it++ )
+ {
+ CollectionSystemDecl sysDecl = it->second;
+ if (sysDecl.isCustom == custom)
+ {
+ systems.push_back(sysDecl.themeFolder);
+ }
+ }
+ return systems;
+}
+
+// returns the theme folders in use for the user-defined Custom Collections
+std::vector CollectionSystemManager::getUserCollectionThemeFolders()
+{
+ std::vector systems;
+ for(std::map::const_iterator it = mCustomCollectionSystemsData.cbegin() ; it != mCustomCollectionSystemsData.cend() ; it++ )
+ {
+ systems.push_back(it->second.decl.themeFolder);
+ }
+ return systems;
+}
+
+// returns whether a specific folder exists in the theme
+bool CollectionSystemManager::themeFolderExists(std::string folder)
+{
+ std::vector themeSys = getSystemsFromTheme();
+ return std::find(themeSys.cbegin(), themeSys.cend(), folder) != themeSys.cend();
+}
+
+bool CollectionSystemManager::includeFileInAutoCollections(FileData* file)
+{
+ // we exclude non-game files from collections (i.e. "kodi", entries from non-game systems)
+ // if/when there are more in the future, maybe this can be a more complex method, with a proper list
+ // but for now a simple string comparison is more performant
+ return file->getName() != "kodi" && file->getSystem()->isGameSystem();
+}
+
+std::string getCustomCollectionConfigPath(std::string collectionName)
+{
+ return getCollectionsFolder() + "/custom-" + collectionName + ".cfg";
+}
+
+std::string getCollectionsFolder()
+{
+ return Utils::FileSystem::getGenericPath(Utils::FileSystem::getHomePath() + "/.emulationstation/collections");
+}
+
+bool systemSort(SystemData* sys1, SystemData* sys2)
+{
+ std::string name1 = Utils::String::toUpper(sys1->getName());
+ std::string name2 = Utils::String::toUpper(sys2->getName());
+ return name1.compare(name2) < 0;
+}
diff --git a/es-app/src/CollectionSystemManager.h b/es-app/src/CollectionSystemManager.h
index 23afe19320..60cf43f045 100644
--- a/es-app/src/CollectionSystemManager.h
+++ b/es-app/src/CollectionSystemManager.h
@@ -5,8 +5,10 @@
#include