diff --git a/indra/llvfs/lldir.cpp b/indra/llvfs/lldir.cpp
index 00bcb32253a38a0374251c34728a64dff644b2a1..54babb6ad9af7c1ee926c7b4b3b5b7e15d55e1b1 100644
--- a/indra/llvfs/lldir.cpp
+++ b/indra/llvfs/lldir.cpp
@@ -1051,6 +1051,7 @@ void LLDir::dumpCurrentDirectories(LLError::ELevel level)
 	LL_VLOGS(level, "AppInit", "Directories") << "  CAFile:                " << getCAFile() << LL_ENDL;
 	LL_VLOGS(level, "AppInit", "Directories") << "  SkinBaseDir:           " << getSkinBaseDir() << LL_ENDL;
 	LL_VLOGS(level, "AppInit", "Directories") << "  SkinDir:               " << getSkinDir() << LL_ENDL;
+	LL_VLOGS(level, "AppInit", "Directories") << "  UserSkinDir:           " << getUserSkinDir() << LL_ENDL;
 }
 
 void LLDir::append(std::string& destpath, const std::string& name) const
diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt
index 0b5bf08097bd39c7968f986955e37eb78289178f..8f47dcfad2deaa5e825b546d4a928b3f1a07209b 100644
--- a/indra/newview/CMakeLists.txt
+++ b/indra/newview/CMakeLists.txt
@@ -68,6 +68,7 @@ endif(USE_FMODSTUDIO)
 
 include_directories(
     ${DBUSGLIB_INCLUDE_DIRS}
+    ${ZLIB_INCLUDE_DIRS}
     ${JSONCPP_INCLUDE_DIR}
     ${GLOD_INCLUDE_DIR}
     ${LLAUDIO_INCLUDE_DIRS}
@@ -109,6 +110,7 @@ include_directories(SYSTEM
     )
 
 set(viewer_SOURCE_FILES
+    alunzip.cpp
     groupchatlistener.cpp
     llaccountingcostmanager.cpp
     llaisapi.cpp
@@ -743,6 +745,7 @@ set(VIEWER_BINARY_NAME "secondlife-bin" CACHE STRING
 set(viewer_HEADER_FILES
     CMakeLists.txt
     ViewerInstall.cmake
+    alunzip.h
     groupchatlistener.h
     llaccountingcost.h
     llaccountingcostmanager.h
diff --git a/indra/newview/alunzip.cpp b/indra/newview/alunzip.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..195d29730d8db90c9a5b499f5ff7833b21787baa
--- /dev/null
+++ b/indra/newview/alunzip.cpp
@@ -0,0 +1,231 @@
+/**
+ * @file alunzip.cpp
+ * @brief Minizip wrapper
+ *
+ * Copyright (c) 2015, Cinder Roxley <cinder@sdf.org>
+ *
+ * Permission is hereby granted, free of charge, to any person or organization
+ * obtaining a copy of the software and accompanying documentation covered by
+ * this license (the "Software") to use, reproduce, display, distribute,
+ * execute, and transmit the Software, and to prepare derivative works of the
+ * Software, and to permit third-parties to whom the Software is furnished to
+ * do so, all subject to the following:
+ *
+ * The copyright notices in the Software and this entire statement, including
+ * the above license grant, this restriction and the following disclaimer,
+ * must be included in all copies of the Software, in whole or in part, and
+ * all derivative works of the Software, unless such copies or derivative
+ * works are solely in the form of machine-executable object code generated by
+ * a source language processor.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+ * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+ * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+ * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ */
+
+#include "llviewerprecompiledheaders.h"
+#include "alunzip.h"
+
+S32 CASE_SENTITIVITY = 1;
+size_t WRITE_BUFFER_SIZE = 8192;
+
+ALUnZip::ALUnZip(const std::string& filename)
+:	mFilename(filename)
+,	mValid(false)
+{
+	mZipfile = open(filename);
+	mValid = (mZipfile != nullptr);
+}
+
+ALUnZip::~ALUnZip()
+{
+	close();
+}
+
+bool ALUnZip::extract(const std::string& path)
+{
+	S32 error = UNZ_OK;
+	unz_global_info64 gi;
+	
+	unzGoToFirstFile(mZipfile);
+	
+	error = unzGetGlobalInfo64(mZipfile, &gi);
+	if (error != UNZ_OK)
+	{
+		LL_WARNS("ALUNZIP") << "Error unzipping " << mFilename << " - code: " << error << LL_ENDL;
+		return false;
+	}
+	
+	for (uLong i = 0; i < gi.number_entry ; i++)
+	{
+		if (extractCurrentFile(path) != UNZ_OK)
+			break;
+		if ((i + 1) < gi.number_entry)
+		{
+			error = unzGoToNextFile(mZipfile);
+			if (error != UNZ_OK)
+			{
+				LL_WARNS("ALUNZIP") << "Error unzipping " << mFilename << " - code: " << error << LL_ENDL;
+				break;
+			}
+		}
+	}
+	
+	return true;
+}
+
+S32 ALUnZip::extractCurrentFile(const std::string& path)
+{
+	S32 error = UNZ_OK;
+	char filename_inzip[256];
+	unz_file_info64 file_info;
+	
+	error = unzGetCurrentFileInfo64(mZipfile, &file_info, filename_inzip, sizeof(filename_inzip), nullptr, 0, nullptr, 0);
+	if (error != UNZ_OK)
+	{
+		LL_WARNS("ALUNZIP") << "Error unzipping " << mFilename << " - code: " << error << LL_ENDL;
+		return error;
+	}
+	
+	const std::string& inzip(filename_inzip);
+	const std::string& write_filename = gDirUtilp->add(path, inzip);
+	
+	LL_INFOS("ALUNZIP") << "Unpacking " << inzip << LL_ENDL;
+	if (LLStringUtil::endsWith(inzip, "/") || LLStringUtil::endsWith(inzip, "\\"))
+	{
+		if (!LLFile::isdir(write_filename))
+		{
+			if (LLFile::mkdir(write_filename) != 0)
+				LL_WARNS("ALUNZIP") << "Couldn't create directory at " << write_filename << LL_ENDL;
+		}
+		return error;
+	}
+	
+	size_t size_buf = WRITE_BUFFER_SIZE;
+	auto buf = std::make_unique<char[]>(size_buf);
+	if (buf == nullptr)
+	{
+		LL_WARNS("ALUNZIP") << "Error allocating memory!" << LL_ENDL;
+		return UNZ_INTERNALERROR;
+	}
+
+	error = unzOpenCurrentFile(mZipfile);
+	if (error != UNZ_OK)
+	{
+		LL_WARNS("ALUNZIP") << "Error unzipping " << mFilename << " - code: " << error << LL_ENDL;
+	}
+	
+	LLFILE* outfile = LLFile::fopen(write_filename, "wb");
+	if (outfile == nullptr)
+	{
+		LL_WARNS("ALUNZIP") << "Error opening " << write_filename << " for writing" << LL_ENDL;
+	}
+	else
+	{
+		do
+		{
+			error = unzReadCurrentFile(mZipfile, buf.get(), size_buf);
+			if (error < UNZ_OK)
+			{
+				LL_WARNS("ALUNZIP") << "Error unzipping " << mFilename << " - code: " << error << LL_ENDL;
+				break;
+			}
+			else if (error > UNZ_OK)
+			{
+				if (fwrite(buf.get(), error, 1, outfile) != 1)
+				{
+					LL_WARNS("ALUNZIP") << "Error writing out " << mFilename << LL_ENDL;
+					error = UNZ_ERRNO;
+					break;
+				}
+			}
+		} while (error > 0);
+		fclose(outfile);
+	}
+
+	
+	if (error == UNZ_OK)
+	{
+		error = unzCloseCurrentFile(mZipfile);
+		if (error != UNZ_OK)
+			LL_WARNS("ALUNZIP") << "Error unzipping " << mFilename << " - code: " << error << LL_ENDL;
+	}
+	else
+		unzCloseCurrentFile(mZipfile);
+	
+	return error;
+}
+
+bool ALUnZip::extractFile(const std::string& file_to_extract, char *buf, size_t bufsize)
+{
+	if (unzLocateFile(mZipfile, file_to_extract.c_str(), CASE_SENTITIVITY) != UNZ_OK)
+	{
+		LL_WARNS("ALUNZIP") << file_to_extract << " was not found in " << mFilename << LL_ENDL;
+		return false;
+	}
+	
+	S32 error = UNZ_OK;
+	
+	char filename_inzip[256];
+	unz_file_info64 file_info;
+	error = unzGetCurrentFileInfo64(mZipfile, &file_info, filename_inzip, sizeof(filename_inzip), nullptr, 0, nullptr, 0);
+	error = unzOpenCurrentFile(mZipfile);
+	error = unzReadCurrentFile(mZipfile, buf, bufsize);
+	unzCloseCurrentFile(mZipfile);
+	if (error != UNZ_OK)
+	{
+		LL_WARNS("ALUNZIP") << "Error unzipping " << mFilename << " - code: " << error << LL_ENDL;
+		return false;
+	}
+	
+	return true;
+}
+
+size_t ALUnZip::getSizeFile(const std::string& file_to_size)
+{
+	if (unzLocateFile(mZipfile, file_to_size.c_str(), CASE_SENTITIVITY) != UNZ_OK)
+	{
+		LL_WARNS("ALUNZIP") << file_to_size << " was not found in " << mFilename << LL_ENDL;
+		return 0;
+	}
+	
+	S32 error = UNZ_OK;
+	char filename_inzip[256];
+	unz_file_info64 file_info;
+	error = unzGetCurrentFileInfo64(mZipfile, &file_info, filename_inzip, sizeof(filename_inzip), nullptr, 0, nullptr, 0);
+	if (error != UNZ_OK)
+	{
+		LL_WARNS("ALUNZIP") << "Error unzipping " << mFilename << " - code: " << error << LL_ENDL;
+		return 0;
+	}
+	return file_info.uncompressed_size;
+}
+
+unzFile ALUnZip::open(const std::string& filename)
+{
+#ifdef USEWIN32IOAPI
+	zlib_filefunc64_def ffunc;
+
+	fill_win32_filefunc64A(&ffunc);
+	mZipfile = unzOpen2_64(filename.c_str(), &ffunc);
+#else // !USEWIN32IOAPI
+	mZipfile = unzOpen64(filename.c_str());
+#endif // !USEWIN32IOAPI
+	
+	if (mZipfile == nullptr)
+		LL_WARNS("ALUNZIP") << "Failed to open " << mFilename << LL_ENDL;
+	
+	return mZipfile;
+}
+
+void ALUnZip::close()
+{
+	unzClose(mZipfile);
+	mZipfile = nullptr;
+	mValid = false;
+}
diff --git a/indra/newview/alunzip.h b/indra/newview/alunzip.h
new file mode 100644
index 0000000000000000000000000000000000000000..59aa8b9a264b0c0f6ff36e8980f548e8a1e0f8c5
--- /dev/null
+++ b/indra/newview/alunzip.h
@@ -0,0 +1,74 @@
+/**
+ * @file alunzip.h
+ * @brief Minizip wrapper
+ *
+ * Copyright (c) 2015, Cinder Roxley <cinder@sdf.org>
+ *
+ * Permission is hereby granted, free of charge, to any person or organization
+ * obtaining a copy of the software and accompanying documentation covered by
+ * this license (the "Software") to use, reproduce, display, distribute,
+ * execute, and transmit the Software, and to prepare derivative works of the
+ * Software, and to permit third-parties to whom the Software is furnished to
+ * do so, all subject to the following:
+ *
+ * The copyright notices in the Software and this entire statement, including
+ * the above license grant, this restriction and the following disclaimer,
+ * must be included in all copies of the Software, in whole or in part, and
+ * all derivative works of the Software, unless such copies or derivative
+ * works are solely in the form of machine-executable object code generated by
+ * a source language processor.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+ * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+ * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+ * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ */
+
+#ifndef AL_UNZIP_H
+#define AL_UNZIP_H
+
+extern "C"
+{
+#include <minizip/unzip.h>
+}
+
+class ALUnZip
+{
+public:
+	ALUnZip(const std::string& filename);
+	~ALUnZip();
+	
+	/* ===============================================================
+	 * @name Archive manipulation functions
+	 * @{
+	 */
+	
+	/// Extracts the archive to a filesystem path
+	bool extract(const std::string& path);
+	
+	/// Extracts a single file from the archive into a memory buffer
+	bool extractFile(const std::string& file_to_extract, char *buf, size_t bufsize);
+	
+	/// Returns the uncompressed size of a file within the archive
+	size_t getSizeFile(const std::string& file_to_size);
+	
+	/// Returns true if the archive is valid
+	bool isValid() { return mValid; }
+	
+	//@}
+	
+private:
+	unzFile open(const std::string& filename);
+	void close();
+	S32 extractCurrentFile(const std::string& path);
+	
+	unzFile mZipfile;
+	std::string mFilename;
+	bool mValid;
+};
+
+#endif // LL_UNZIP_H
diff --git a/indra/newview/llfilepicker.cpp b/indra/newview/llfilepicker.cpp
index b6fd70452e7dfee28f5b4a843c13ce3e6aea91fd..919e2337ef49ad6d32cf6cecc0891d2bcde0758d 100644
--- a/indra/newview/llfilepicker.cpp
+++ b/indra/newview/llfilepicker.cpp
@@ -64,6 +64,7 @@ LLFilePicker LLFilePicker::sInstance;
 #define MODEL_FILTER L"Model files (*.dae)\0*.dae\0"
 #define SCRIPT_FILTER L"Script files (*.lsl)\0*.lsl\0"
 #define DICTIONARY_FILTER L"Dictionary files (*.dic; *.xcu)\0*.dic;*.xcu\0"
+#define ZIP_FILTER L"ZIP files (*.zip)\0*.zip\0"
 #endif
 
 #ifdef LL_DARWIN
@@ -227,6 +228,10 @@ BOOL LLFilePicker::setupFilter(ELoadFilter filter)
 		mOFN.lpstrFilter = DICTIONARY_FILTER \
 			L"\0";
 		break;
+	case FFLOAD_ZIP:
+		mOFN.lpstrFilter = ZIP_FILTER \
+			L"\0";
+		break;
 	default:
 		res = FALSE;
 		break;
@@ -642,6 +647,9 @@ std::vector<std::string>* LLFilePicker::navOpenFilterProc(ELoadFilter filter) //
             allowedv->push_back("dic");
             allowedv->push_back("xcu");
             break;
+        case FFLOAD_ZIP:
+            allowedv.push_back("zip");
+            break;
         case FFLOAD_DIRECTORY:
             break;
         default:
@@ -1188,6 +1196,12 @@ static std::string add_dictionary_filter_to_gtkchooser(GtkWindow *picker)
 							LLTrans::getString("dictionary_files") + " (*.dic; *.xcu)");
 }
 
+static std::string add_zip_filter_to_gtkchooser(GtkWindow *picker)
+{
+	return add_simple_mime_filter_to_gtkchooser(picker, HTTP_CONTENT_TEXT_PLAIN,
+												LLTrans::getString("zip_files") + " (*.zip)");
+}
+
 static std::string add_save_texture_filter_to_gtkchooser(GtkWindow *picker)
 {
 	GtkFileFilter *gfilter_tga = gtk_file_filter_new();
@@ -1366,6 +1380,9 @@ BOOL LLFilePicker::getOpenFile( ELoadFilter filter, bool blocking )
 		case FFLOAD_DICTIONARY:
 			filtername = add_dictionary_filter_to_gtkchooser(picker);
 			break;
+		case FFLOAD_ZIP:
+			filtername = add_zip_filter_to_gtkchooser(picker);
+			break;
 		default:;
 			break;
 		}
diff --git a/indra/newview/llfilepicker.h b/indra/newview/llfilepicker.h
index 2fc496a144a64a7ab2950a34f6736ec6cdca8c4b..c1fa147d2d7223c1f4a58103f8b0702788c656bb 100644
--- a/indra/newview/llfilepicker.h
+++ b/indra/newview/llfilepicker.h
@@ -88,7 +88,8 @@ class LLFilePicker
 		FFLOAD_SCRIPT = 11,
 		FFLOAD_DICTIONARY = 12,
         FFLOAD_DIRECTORY = 13,   // To call from lldirpicker.
-        FFLOAD_EXE = 14          // Note: EXE will be treated as ALL on Windows and Linux but not on Darwin
+        FFLOAD_EXE = 14,          // Note: EXE will be treated as ALL on Windows and Linux but not on Darwin
+		FFLOAD_ZIP = 15
 	};
 
 	enum ESaveFilter
diff --git a/indra/newview/llfloaterpreference.cpp b/indra/newview/llfloaterpreference.cpp
index 8a1419faa44dffa77694f7d2810861105ce61c48..174d2e4dacdb5feec56460069b1a48df05283b23 100644
--- a/indra/newview/llfloaterpreference.cpp
+++ b/indra/newview/llfloaterpreference.cpp
@@ -42,9 +42,11 @@
 #include "llcolorswatch.h"
 #include "llcombobox.h"
 #include "llcommandhandler.h"
+#include "lldiriterator.h"
 #include "lldirpicker.h"
 #include "lleventtimer.h"
 #include "llfeaturemanager.h"
+#include "llfilepicker.h"
 #include "llfocusmgr.h"
 //#include "llfirstuse.h"
 #include "llfloaterreg.h"
@@ -85,6 +87,7 @@
 #include "llfontgl.h"
 #include "llrect.h"
 #include "llstring.h"
+#include "alunzip.h"
 
 // project includes
 
@@ -96,6 +99,7 @@
 #include "llstartup.h"
 #include "lltextbox.h"
 #include "llui.h"
+#include "llversioninfo.h"
 #include "llviewerobjectlist.h"
 #include "llvoavatar.h"
 #include "llvovolume.h"
@@ -121,6 +125,9 @@
 #include "llfeaturemanager.h"
 #include "llviewertexturelist.h"
 
+#include <json/json.h>
+#include <utility>
+
 #include "llsearchableui.h"
 
 const F32 BANDWIDTH_UPDATER_TIMEOUT = 0.5f;
@@ -163,6 +170,8 @@ struct LabelTable : public LLInitParam::Block<LabelTable>
     {}
 };
 
+const std::string DEFAULT_SKIN = "default";
+
 class LLVoiceSetKeyDialog : public LLModalDialog
 {
 public:
@@ -181,6 +190,24 @@ class LLVoiceSetKeyDialog : public LLModalDialog
 	LLFloaterPreference* mParent;
 };
 
+typedef enum e_skin_type
+{
+	SYSTEM_SKIN,
+	USER_SKIN
+} ESkinType;
+
+typedef struct skin_t
+{
+	std::string mName = "Unknown";
+	std::string mAuthor = "Unknown";
+	std::string mUrl = "Unknown";
+	LLDate mDate = LLDate(0.0);
+	std::string mCompatVer = "Unknown";
+	std::string mNotes = LLStringUtil::null;
+	ESkinType mType = USER_SKIN;
+	
+} skin_t;
+
 LLVoiceSetKeyDialog::LLVoiceSetKeyDialog(const LLSD& key)
   : LLModalDialog(key),
 	mParent(NULL)
@@ -399,8 +426,6 @@ LLFloaterPreference::LLFloaterPreference(const LLSD& key)
 	mCommitCallbackRegistrar.add("Pref.WebClearCache",			boost::bind(&LLFloaterPreference::onClickBrowserClearCache, this));
 	mCommitCallbackRegistrar.add("Pref.SetCache",				boost::bind(&LLFloaterPreference::onClickSetCache, this));
 	mCommitCallbackRegistrar.add("Pref.ResetCache",				boost::bind(&LLFloaterPreference::onClickResetCache, this));
-	mCommitCallbackRegistrar.add("Pref.ClickSkin",				boost::bind(&LLFloaterPreference::onClickSkin, this,_1, _2));
-	mCommitCallbackRegistrar.add("Pref.SelectSkin",				boost::bind(&LLFloaterPreference::onSelectSkin, this));
 	mCommitCallbackRegistrar.add("Pref.VoiceSetKey",			boost::bind(&LLFloaterPreference::onClickSetKey, this));
 	mCommitCallbackRegistrar.add("Pref.VoiceSetMiddleMouse",	boost::bind(&LLFloaterPreference::onClickSetMiddleMouse, this));
 	mCommitCallbackRegistrar.add("Pref.SetSounds",				boost::bind(&LLFloaterPreference::onClickSetSounds, this));
@@ -442,6 +467,11 @@ LLFloaterPreference::LLFloaterPreference(const LLSD& key)
 	mCommitCallbackRegistrar.add("Pref.ClearLog",				boost::bind(&LLConversationLog::onClearLog, &LLConversationLog::instance()));
 	mCommitCallbackRegistrar.add("Pref.DeleteTranscripts",      boost::bind(&LLFloaterPreference::onDeleteTranscripts, this));
 	mCommitCallbackRegistrar.add("UpdateFilter", boost::bind(&LLFloaterPreference::onUpdateFilterTerm, this, false)); // <FS:ND/> Hook up for filtering
+
+	mCommitCallbackRegistrar.add("Pref.AddSkin", boost::bind(&LLFloaterPreference::onAddSkin, this));
+	mCommitCallbackRegistrar.add("Pref.RemoveSkin", boost::bind(&LLFloaterPreference::onRemoveSkin, this));
+	mCommitCallbackRegistrar.add("Pref.ApplySkin", boost::bind(&LLFloaterPreference::onApplySkin, this));
+	mCommitCallbackRegistrar.add("Pref.SelectSkin", boost::bind(&LLFloaterPreference::onSelectSkin, this, _2));
 }
 
 void LLFloaterPreference::processProperties( void* pData, EAvatarProcessorType type )
@@ -551,6 +581,9 @@ BOOL LLFloaterPreference::postBuild()
 	changed();
 
 	LLLogChat::getInstance()->setSaveHistorySignal(boost::bind(&LLFloaterPreference::onLogChatHistorySaved, this));
+	
+	loadUserSkins();
+	
 
 	LLSliderCtrl* fov_slider = getChild<LLSliderCtrl>("camera_fov");
 	fov_slider->setMinValue(LLViewerCamera::getInstance()->getMinView());
@@ -603,6 +636,266 @@ void LLFloaterPreference::onDoNotDisturbResponseChanged()
 	gSavedPerAccountSettings.setBOOL("DoNotDisturbResponseChanged", response_changed_flag );
 }
 
+
+////////////////////////////////////////////////////
+// Skins panel
+
+skin_t manifestFromJson(const std::string& filename, const ESkinType type)
+{
+	skin_t skin;
+	Json::Reader reader;
+	Json::Value root;
+
+	llifstream in;
+	in.open(filename);
+	if (in.is_open())
+	{
+		if (reader.parse(in, root, false))
+        {
+			skin.mName = root.get("name", "Unknown").asString();
+			skin.mAuthor = root.get("author", "Unknown").asString();
+			skin.mUrl = root.get("url", "Unknown").asString();
+			skin.mCompatVer = root.get("compatibility", "Unknown").asString();
+			skin.mDate = LLDate(root.get("date", "1983-04-20T00:00:00+00:00").asString());
+			skin.mNotes = root.get("notes", "").asString();
+			// If it's a system skin, the compatability version is always the current build
+			if (type == SYSTEM_SKIN)
+			{
+				skin.mCompatVer = LLVersionInfo::getShortVersion();
+			}
+        } 
+		else
+		{
+			LL_WARNS() << "Failed to parse " << filename << ": " << reader.getFormatedErrorMessages() << LL_ENDL;
+		}
+		in.close();
+	}
+	skin.mType = type;
+	return skin;
+}
+
+void LLFloaterPreference::loadUserSkins()
+{
+	mUserSkins.clear();
+	LLDirIterator sysiter(gDirUtilp->getSkinBaseDir(), "*");
+	bool found = true;
+	while (found)
+	{
+		std::string dir;
+		if ((found = sysiter.next(dir)))
+		{
+			const std::string& fullpath = gDirUtilp->add(gDirUtilp->getSkinBaseDir(), dir);
+			if (!LLFile::isdir(fullpath)) continue; // only directories!
+			
+			const std::string& manifestpath = gDirUtilp->add(fullpath, "manifest.json");
+			skin_t skin = manifestFromJson(manifestpath, SYSTEM_SKIN);
+			
+			mUserSkins.emplace(dir, skin);
+		}
+	}
+	
+	const std::string userskindir = gDirUtilp->add(gDirUtilp->getOSUserAppDir(), "skins");
+	if (LLFile::isdir(userskindir))
+	{
+		LLDirIterator iter(userskindir, "*");
+		found = true;
+		while (found)
+		{
+			std::string dir;
+			if ((found = iter.next(dir)))
+			{
+				const std::string& fullpath = gDirUtilp->add(userskindir, dir);
+				if (!LLFile::isdir(fullpath)) continue; // only directories!
+
+				const std::string& manifestpath = gDirUtilp->add(fullpath, "manifest.json");
+				skin_t skin = manifestFromJson(manifestpath, USER_SKIN);
+
+				mUserSkins.emplace(dir, skin);
+			}
+		}
+	}
+	reloadSkinList();
+}
+
+void LLFloaterPreference::reloadSkinList()
+{
+	LLScrollListCtrl* skin_list = getChild<LLScrollListCtrl>("skin_list");
+	const std::string current_skin = gSavedSettings.getString("SkinCurrent");
+	
+	skin_list->clearRows();
+
+	// User Downloaded Skins
+	for (const auto& skin : mUserSkins)
+	{
+		LLSD row;
+		row["id"] = skin.first;
+		row["columns"][0]["value"] = skin.second.mName == "Unknown" ? skin.first : skin.second.mName;
+		row["columns"][0]["font"]["style"] = current_skin == skin.first ? "BOLD" : "NORMAL";
+		skin_list->addElement(row);
+	}
+	skin_list->setSelectedByValue(current_skin, TRUE);
+	onSelectSkin(skin_list->getSelectedValue());
+}
+
+void LLFloaterPreference::onAddSkin()
+{
+	LLFilePicker& filepicker = LLFilePicker::instance();
+	if (filepicker.getOpenFile(LLFilePicker::FFLOAD_ZIP))
+	{
+		const std::string& package = filepicker.getFirstFile();
+		auto zip = std::make_unique<ALUnZip>(package);
+		if (zip->isValid())
+		{
+			size_t buf_size = zip->getSizeFile("manifest.json");
+			if (buf_size)
+			{
+				buf_size++;
+				buf_size *= sizeof(char);
+				auto buf = std::make_unique<char[]>(buf_size);
+				zip->extractFile("manifest.json", buf.get(), buf_size);
+				buf[buf_size - 1] = '\0'; // force.
+				std::stringstream ss;
+				ss << std::string(const_cast<const char*>(buf.get()), buf_size);
+				buf.reset();
+				
+				Json::Reader reader;
+				Json::Value root;
+				std::string errors;
+				if (reader.parse(ss, root, false))
+				{
+					const std::string& name = root.get("name", "Unknown").asString();
+					std::string pathname = gDirUtilp->add(gDirUtilp->getOSUserAppDir(), "skins");
+					if (!gDirUtilp->fileExists(pathname))
+					{
+						LLFile::mkdir(pathname);
+					}
+					pathname = gDirUtilp->add(pathname, name);
+					if (!LLFile::isdir(pathname) && (LLFile::mkdir(pathname) != 0))
+					{
+						LLNotificationsUtil::add("AddSkinUnpackFailed");
+					}
+					else if (!zip->extract(pathname))
+					{
+						LLNotificationsUtil::add("AddSkinUnpackFailed");
+					}
+					else
+					{
+						loadUserSkins();
+						LLNotificationsUtil::add("AddSkinSuccess", LLSD().with("PACKAGE", name));
+					}
+				}
+				else
+				{
+					LLNotificationsUtil::add("AddSkinCantParseManifest", LLSD().with("PACKAGE", package));
+				}
+			}
+			else
+			{
+				LLNotificationsUtil::add("AddSkinNoManifest", LLSD().with("PACKAGE", package));
+			}
+		}
+	}
+}
+
+void LLFloaterPreference::onRemoveSkin()
+{
+	LLScrollListCtrl* skin_list = findChild<LLScrollListCtrl>("skin_list");
+	if (skin_list)
+	{
+		LLSD args;
+		args["SKIN"] = skin_list->getSelectedValue().asString();
+		LLNotificationsUtil::add("ConfirmRemoveSkin", args, args,
+								 boost::bind(&LLFloaterPreference::callbackRemoveSkin, this, _1, _2));
+	}
+}
+
+void LLFloaterPreference::callbackRemoveSkin(const LLSD& notification, const LLSD& response)
+{
+	S32 option = LLNotificationsUtil::getSelectedOption(notification, response);
+	if (option == 0) // YES
+	{
+		const std::string& skin = notification["payload"]["SKIN"].asString();
+		std::string dir = gDirUtilp->add(gDirUtilp->getOSUserAppDir(), "skins");
+		dir = gDirUtilp->add(dir, skin);
+		if (gDirUtilp->deleteDirAndContents(dir) > 0)
+		{
+			skinmap_t::iterator iter = mUserSkins.find(skin);
+			if (iter != mUserSkins.end())
+				mUserSkins.erase(iter);
+			// If we just deleted the current skin, reset to default. It might not even be a good
+			// idea to allow this, but we'll see!
+			if (gSavedSettings.getString("SkinCurrent") == skin)
+			{
+				gSavedSettings.setString("SkinCurrent", DEFAULT_SKIN);
+			}
+			LLNotificationsUtil::add("RemoveSkinSuccess", LLSD().with("SKIN", skin));
+		}
+		else
+		{
+			LLNotificationsUtil::add("RemoveSkinFailure", LLSD().with("SKIN", skin));
+		}
+		reloadSkinList();
+	}
+}
+
+void LLFloaterPreference::callbackApplySkin(const LLSD& notification, const LLSD& response)
+{
+	S32 option = LLNotificationsUtil::getSelectedOption(notification, response);
+	switch (option)
+	{
+		case 0:	// Yes
+			gSavedSettings.setBOOL("ResetUserColorsOnLogout", TRUE);
+			break;
+		case 1:	// No
+			gSavedSettings.setBOOL("ResetUserColorsOnLogout", FALSE);
+			break;
+		case 2:	// Cancel
+			gSavedSettings.setString("SkinCurrent", sSkin);
+			reloadSkinList();
+			break;
+		default:
+			LL_WARNS() << "Unhandled option! How could this be?" << LL_ENDL;
+			break;
+	}
+}
+
+void LLFloaterPreference::onApplySkin()
+{
+	LLScrollListCtrl* skin_list = findChild<LLScrollListCtrl>("skin_list");
+	if (skin_list)
+	{
+		gSavedSettings.setString("SkinCurrent", skin_list->getSelectedValue().asString());
+		reloadSkinList();
+	}
+	if (sSkin != gSavedSettings.getString("SkinCurrent"))
+	{
+		LLNotificationsUtil::add("ChangeSkin", LLSD(), LLSD(),
+								 boost::bind(&LLFloaterPreference::callbackApplySkin, this, _1, _2));
+	}
+}
+
+void LLFloaterPreference::onSelectSkin(const LLSD& data)
+{
+	bool userskin = false;
+	skinmap_t::iterator iter = mUserSkins.find(data.asString());
+	if (iter != mUserSkins.end())
+	{
+		refreshSkinInfo(iter->second);
+		userskin = (iter->second.mType == USER_SKIN);
+	}
+	getChild<LLUICtrl>("remove_skin")->setEnabled(userskin);
+}
+
+void LLFloaterPreference::refreshSkinInfo(const skin_t& skin)
+{
+	getChild<LLTextBase>("skin_name")->setText(skin.mName);
+	getChild<LLTextBase>("skin_author")->setText(skin.mAuthor);
+	getChild<LLTextBase>("skin_homepage")->setText(skin.mUrl);
+	getChild<LLTextBase>("skin_date")->setText(skin.mDate.toHTTPDateString("%A, %d %b %Y"));
+	getChild<LLTextBase>("skin_compatibility")->setText(skin.mCompatVer);
+	getChild<LLTextBase>("skin_notes")->setText(skin.mNotes);
+}
+
 LLFloaterPreference::~LLFloaterPreference()
 {
 	LLConversationLog::instance().removeObserver(this);
@@ -640,8 +933,7 @@ void LLFloaterPreference::apply()
 	LLTabContainer* tabcontainer = getChild<LLTabContainer>("pref core");
 	if (sSkin != gSavedSettings.getString("SkinCurrent"))
 	{
-		LLNotificationsUtil::add("ChangeSkin");
-		refreshSkin(this);
+		sSkin = gSavedSettings.getString("SkinCurrent");
 	}
 	// Call apply() on all panels that derive from LLPanelPreference
 	for (child_list_t::const_iterator iter = tabcontainer->getChildList()->begin();
@@ -1220,25 +1512,6 @@ void LLFloaterPreference::onClickResetCache()
 	gSavedSettings.setString("CacheLocationTopFolder", top_folder);
 }
 
-void LLFloaterPreference::onClickSkin(LLUICtrl* ctrl, const LLSD& userdata)
-{
-	gSavedSettings.setString("SkinCurrent", userdata.asString());
-	ctrl->setValue(userdata.asString());
-}
-
-void LLFloaterPreference::onSelectSkin()
-{
-	std::string skin_selection = getChild<LLRadioGroup>("skin_selection")->getValue().asString();
-	gSavedSettings.setString("SkinCurrent", skin_selection);
-}
-
-void LLFloaterPreference::refreshSkin(void* data)
-{
-	LLPanel*self = (LLPanel*)data;
-	sSkin = gSavedSettings.getString("SkinCurrent");
-	self->getChild<LLRadioGroup>("skin_selection", true)->setValue(sSkin);
-}
-
 void LLFloaterPreference::buildPopupLists()
 {
 	LLScrollListCtrl& disabled_popups =
@@ -2409,21 +2682,6 @@ BOOL LLPanelPreference::postBuild()
 		getChildView("voice_unavailable")->setVisible( voice_disabled);
 		getChildView("enable_voice_check")->setVisible( !voice_disabled);
 	}
-	
-	//////////////////////PanelSkins ///////////////////
-	
-	if (hasChild("skin_selection", TRUE))
-	{
-		LLFloaterPreference::refreshSkin(this);
-
-		// if skin is set to a skin that no longer exists (silver) set back to default
-		if (getChild<LLRadioGroup>("skin_selection")->getSelectedIndex() < 0)
-		{
-			gSavedSettings.setString("SkinCurrent", "default");
-			LLFloaterPreference::refreshSkin(this);
-		}
-
-	}
 
 	//////////////////////PanelPrivacy ///////////////////
 	if (hasChild("media_enabled", TRUE))
diff --git a/indra/newview/llfloaterpreference.h b/indra/newview/llfloaterpreference.h
index a0f43bd8848e4560e3605d3c014d499e90e6ff37..2dbea6898135b681085872e098d2738dd4287659 100644
--- a/indra/newview/llfloaterpreference.h
+++ b/indra/newview/llfloaterpreference.h
@@ -47,6 +47,7 @@ class LLScrollListCtrl;
 class LLSliderCtrl;
 class LLSD;
 class LLTextBox;
+struct skin_t;
 
 namespace ll
 {
@@ -144,8 +145,6 @@ class LLFloaterPreference : public LLFloater, public LLAvatarPropertiesObserver,
 	void onClickSetCache();
 	void changeCachePath(const std::vector<std::string>& filenames, std::string proposed_name);
 	void onClickResetCache();
-	void onClickSkin(LLUICtrl* ctrl,const LLSD& userdata);
-	void onSelectSkin();
 	void onClickSetKey();
 	void setKey(KEY key);
 	void setMouse(LLMouseHandler::EClickType click);
@@ -201,6 +200,17 @@ class LLFloaterPreference : public LLFloater, public LLAvatarPropertiesObserver,
 	void updateMaxComplexity();
 	static bool loadFromFilename(const std::string& filename, std::map<std::string, std::string> &label_map);
 
+	
+	void loadUserSkins();
+	void reloadSkinList();
+	void onAddSkin();
+	void onRemoveSkin();
+	void callbackRemoveSkin(const LLSD& notification, const LLSD& response);
+	void onApplySkin();
+	void callbackApplySkin(const LLSD& notification, const LLSD& response);
+	void onSelectSkin(const LLSD& data);
+	void refreshSkinInfo(const skin_t& skin);
+
 	static std::string sSkin;
 	notifications_map mNotificationOptions;
 	bool mClickActionDirty; ///< Set to true when the click/double-click options get changed by user.
@@ -215,6 +225,10 @@ class LLFloaterPreference : public LLFloater, public LLAvatarPropertiesObserver,
 	
 	LLAvatarData mAvatarProperties;
 	std::string mSavedGraphicsPreset;
+
+	typedef std::map<std::string, skin_t> skinmap_t;
+	skinmap_t mUserSkins;
+	
 	LOG_CLASS(LLFloaterPreference);
 
 	LLSearchEditor *mFilterEdit;
diff --git a/indra/newview/skins/default/manifest.json b/indra/newview/skins/default/manifest.json
new file mode 100644
index 0000000000000000000000000000000000000000..6fc90b73408ef4767649661615b4e47a78854f10
--- /dev/null
+++ b/indra/newview/skins/default/manifest.json
@@ -0,0 +1,6 @@
+{
+  "name": "Second Life",
+  "author": "Linden Lab",
+  "url": "http://www.secondlife.com/",
+  "notes": "The classic skin from the Second Life Viewer"
+}
diff --git a/indra/newview/skins/default/xui/en/floater_preferences.xml b/indra/newview/skins/default/xui/en/floater_preferences.xml
index 0e62d50072eb1940726e1c00a3fa296340d97419..0608a822152eb79f34e4df7dfa9482399df19287 100644
--- a/indra/newview/skins/default/xui/en/floater_preferences.xml
+++ b/indra/newview/skins/default/xui/en/floater_preferences.xml
@@ -169,6 +169,11 @@ https://accounts.secondlife.com/change_email/
          layout="topleft"
          help_topic="preferences_uploads_tab"
          name="uploads" />
+        <panel
+         filename="panel_preferences_skins.xml"
+         label="Skinning"
+         layout="topleft"
+         name="skins" />
     </tab_container>
 
 </floater>
diff --git a/indra/newview/skins/default/xui/en/notifications.xml b/indra/newview/skins/default/xui/en/notifications.xml
index 6dd9b8ec7c1674eaf360a96471626f58c0252eb2..ba62372f7dbefa42ad44e62db152997f3b68ccd2 100644
--- a/indra/newview/skins/default/xui/en/notifications.xml
+++ b/indra/newview/skins/default/xui/en/notifications.xml
@@ -11684,4 +11684,60 @@ Unable to load the track from [TRACK1] into [TRACK2].
   <tag>fail</tag>
   </notification>
   
+  <!-- ALCHEMY BELOW THIS LINE -->
+  <notification
+   icon="alertmodal.tga"
+   name="ConfirmRemoveSkin"
+   type="alertmodal">
+    <tag>confirm</tag>
+    Are you sure you want to remove [SKIN]? This will erase it permenantly from your hard disk.
+    <usetemplate
+     ignoretext="Confirm removing skins"
+     name="okcancelignore"
+     notext="Cancel"
+     yestext="OK"/>
+  </notification>
+  <notification
+   icon="alertmodal.tga"
+   name="RemoveSkinSuccess"
+   type="alertmodal">
+    <tag>fail</tag>
+    Removed [SKIN] successfully.
+  </notification>
+  <notification
+   icon="alertmodal.tga"
+   name="RemoveSkinFailure"
+   type="alertmodal">
+    <tag>fail</tag>
+    Failed to remove [SKIN].
+  </notification>
+  <notification
+   icon="alertmodal.tga"
+   name="AddSkinNoManifest"
+   type="alertmodal">
+    <tag>fail</tag>
+    [PACKAGE] doesn't contain a manifest or is unreadable.
+  </notification>
+  <notification
+   icon="alertmodal.tga"
+   name="AddSkinCantParseManifest"
+   type="alertmodal">
+    <tag>fail</tag>
+    [PACKAGE] has an invalid manifest.
+  </notification>
+  <notification
+   icon="alertmodal.tga"
+   name="AddSkinUnpackFailed"
+   type="alertmodal">
+    <tag>fail</tag>
+    Failed to unpack the skin package. See the log for more details.
+  </notification>
+  <notification
+   icon="alertmodal.tga"
+   name="AddSkinSuccess"
+   type="alertmodal">
+    <tag>fail</tag>
+    [PACKAGE] was added successfully.
+  </notification>
+  
 </notifications>
diff --git a/indra/newview/skins/default/xui/en/panel_preferences_skins.xml b/indra/newview/skins/default/xui/en/panel_preferences_skins.xml
new file mode 100644
index 0000000000000000000000000000000000000000..6bee06a9abc1394720fb4e0a6399a16d0081e4bb
--- /dev/null
+++ b/indra/newview/skins/default/xui/en/panel_preferences_skins.xml
@@ -0,0 +1,203 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
+<panel
+ border="true"
+ follows="left|top|right|bottom"
+ height="408"
+ label="Skinning"
+ layout="topleft"
+ left="102"
+ name="skins"
+ top="1"
+ width="517">
+  <text
+   type="string"
+   length="1"
+   follows="left|top"
+   height="12"
+   layout="topleft"
+   left="30"
+   name="select_skin_text"
+   top="12"
+   width="180">
+   Select a skin:
+  </text>
+  <!-- Chooser -->
+  <scroll_list
+   height="225"
+   follows="left|top"
+   layout="topleft"
+   left="40"
+   name="skin_list"
+   draw_heading="false"
+   top_pad="8"
+   width="170">
+    <scroll_list.commit_callback
+     function="Pref.SelectSkin" />
+  </scroll_list>
+  <button
+   enabled="true"
+   follows="left|top"
+   height="19"
+   label="Apply"
+   layout="topleft"
+   left="40"
+   name="apply_skin"
+   tool_tip="Changes will take effect after you restart [APP_NAME]"
+   top_pad="4"
+   width="75">
+    <button.commit_callback
+     function="Pref.ApplySkin" />
+  </button>
+  <button
+   enabled="true"
+   follows="left|top"
+   height="19"
+   image_disabled="AddItem_Disabled"
+   image_selected="AddItem_Press"
+   image_unselected="AddItem_Off"
+   layout="topleft"
+   left_pad="52"
+   name="add_skin"
+   top_delta="0"
+   tool_tip="Add a new skin..."
+   width="19">
+    <button.commit_callback
+     function="Pref.AddSkin" />
+  </button>
+  <button
+   enabled="false"
+   follows="left|top"
+   height="19"
+   image_disabled="TrashItem_Disabled"
+   image_selected="TrashItem_Press"
+   image_unselected="TrashItem_Off"
+   layout="topleft"
+   left_pad="4"
+   name="remove_skin"
+   top_delta="0"
+   tool_tip="Remove selected skin..."
+   width="19">
+    <button.commit_callback
+     function="Pref.RemoveSkin" />
+  </button>
+  <!-- Info/Preview -->
+  <text
+   type="string"
+   length="1"
+   follows="left|top"
+   height="20"
+   layout="topleft"
+   font="SansSerifLarge"
+   left="215"
+   name="skin_name"
+   top="10"
+   right="-10">
+   Boring skin!
+  </text>
+  <text
+   type="string"
+   length="1"
+   follows="left|top"
+   height="14"
+   layout="topleft"
+   name="skin_author_label"
+   top_pad="2"
+   width="90"
+   value="Author:" />
+  <text
+   type="string"
+   length="1"
+   follows="left|top"
+   height="14"
+   layout="topleft"
+   name="skin_homepage_label"
+   top_pad="2"
+   width="90"
+   value="Homepage:" />
+  <text
+   type="string"
+   length="1"
+   follows="left|top"
+   height="14"
+   layout="topleft"
+   name="skin_date_label"
+   top_pad="2"
+   width="90"
+   value="Date:" />
+  <text
+   type="string"
+   length="1"
+   follows="left|top"
+   height="14"
+   layout="topleft"
+   name="skin_compatibility_label"
+   top_pad="2"
+   width="90"
+   value="Compatiblity:" />
+  <text
+   type="string"
+   length="1"
+   follows="left|top"
+   height="14"
+   layout="topleft"
+   name="skin_author"
+   top_delta="-48"
+   left_pad="1"
+   width="200">
+Nobody
+  </text>
+  <text
+   type="string"
+   length="1"
+   follows="left|top"
+   height="14"
+   layout="topleft"
+   name="skin_homepage"
+   top_pad="2"
+   width="200">
+http://www.example.com/
+  </text>
+  <text
+   type="string"
+   length="1"
+   follows="left|top"
+   height="14"
+   layout="topleft"
+   name="skin_date"
+   top_pad="2"
+   width="200">
+Jan 1, 2001
+  </text>
+  <text
+   type="string"
+   length="1"
+   follows="left|top"
+   height="14"
+   layout="topleft"
+   name="skin_compatibility"
+   top_pad="2"
+   width="200">
+0.0.0
+  </text>
+  <text
+   type="string"
+   length="1"
+   follows="left|top"
+   height="14"
+   layout="topleft"
+   name="skin_notes_label"
+   top_pad="2"
+   left="215"
+   width="90"
+   value="Notes:" />
+  <text
+   type="string"
+   length="1"
+   follows="left|top"
+   height="70"
+   layout="topleft"
+   name="skin_notes"
+   top_pad="2"
+   word_wrap="true"
+   width="291" />
+</panel>
diff --git a/indra/newview/skins/default/xui/en/strings.xml b/indra/newview/skins/default/xui/en/strings.xml
index e95d4701bbc81c66b30d802b264cc839f456f32b..e3163d6c85722e8645f7032a7c0396d54a0aafa9 100644
--- a/indra/newview/skins/default/xui/en/strings.xml
+++ b/indra/newview/skins/default/xui/en/strings.xml
@@ -573,6 +573,7 @@ http://secondlife.com/support for help fixing this problem.
 	<string name="choose_the_directory">Choose Directory</string>
 	<string name="script_files">Scripts</string>
 	<string name="dictionary_files">Dictionaries</string>
+	<string name="zip_files">Zip files</string>
 
   <!-- LSL Usage Hover Tips -->
   <!-- NOTE: For now these are set as translate="false", until DEV-40761 is implemented (to internationalize the rest of tooltips in the same window).
diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py
index c74dbb9e30230f389fa22f8e5486524747de25ce..2d1fea60e517c365da8e3731ca6ea75e3cf125ae 100755
--- a/indra/newview/viewer_manifest.py
+++ b/indra/newview/viewer_manifest.py
@@ -154,6 +154,7 @@ def construct(self):
                     self.path("*/xui/*/*.xml")
                     self.path("*/xui/*/widgets/*.xml")
                     self.path("*/*.xml")
+                    self.path("*/*.json")
 
                     # Local HTML files (e.g. loading screen)
                     # The claim is that we never use local html files any