Browse Source

Merge pull request #1601 from causefx/v2-develop

V2 develop
causefx 5 years ago
parent
commit
f00da8a065

+ 6 - 0
.gitignore

@@ -137,6 +137,7 @@ api/plugins/*
 !api/plugins/api/healthChecks.php
 !api/plugins/config/healthChecks.php
 !api/plugins/js/healthChecks.js
+!api/plugins/js/healthChecks-settings.js
 !api/plugins/php-mailer.php
 !api/plugins/api/php-mailer.php
 !api/plugins/config/php-mailer.php
@@ -156,6 +157,11 @@ api/plugins/*
 !api/plugins/misc/speedTest/telemetry.php
 !api/plugins/misc/speedTest/telemetry_settings.php
 !api/plugins/misc/speedTest/speedtest_worker.min.js
+!api/plugins/bookmark.php
+!api/plugins/api/bookmark.php
+!api/plugins/config/bookmark.php
+!api/plugins/js/bookmark-settings.js
+!api/plugins/css/bookmark.css
 
 # =========================
 # Custom files

+ 57 - 9
api/classes/organizr.class.php

@@ -60,7 +60,7 @@ class Organizr
 	
 	// ===================================
 	// Organizr Version
-	public $version = '2.1.195';
+	public $version = '2.1.235';
 	// ===================================
 	// Quick php Version check
 	public $minimumPHP = '7.2';
@@ -395,6 +395,7 @@ class Organizr
 		return ($encode) ? json_encode($files) : $files;
 	}
 	
+	/* Old function
 	public function pluginFiles($type)
 	{
 		$files = '';
@@ -414,6 +415,50 @@ class Organizr
 		}
 		return $files;
 	}
+	*/
+	public function pluginFiles($type, $settings = false)
+	{
+		$files = '';
+		switch ($type) {
+			case 'js':
+				foreach (glob(dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'js' . DIRECTORY_SEPARATOR . '*.js') as $filename) {
+					$keyOriginal = strtoupper(basename($filename, '.js'));
+					$key = str_replace('-SETTINGS', '', $keyOriginal);
+					$continue = false;
+					if ($settings) {
+						if (stripos($keyOriginal, '-SETTINGS') !== false) {
+							$continue = true;
+						}
+					} else {
+						if (stripos($keyOriginal, '-SETTINGS') == false) {
+							$continue = true;
+						}
+					}
+					switch ($key) {
+						case 'PHP-MAILER':
+							$key = 'PHPMAILER';
+							break;
+						default:
+							$key = $key;
+					}
+					if ($this->config[$key . '-enabled'] || $settings) {
+						if ($continue) {
+							$files .= '<script src="api/plugins/js/' . basename($filename) . '?v=' . $this->fileHash . '" defer="true"></script>';
+						}
+					}
+					
+				}
+				break;
+			case 'css':
+				foreach (glob(dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'css' . DIRECTORY_SEPARATOR . '*.css') as $filename) {
+					$files .= '<link href="api/plugins/css/' . basename($filename) . '?v=' . $this->fileHash . '" rel="stylesheet">';
+				}
+				break;
+			default:
+				break;
+		}
+		return $files;
+	}
 	
 	public function formKey($script = true)
 	{
@@ -1389,7 +1434,7 @@ class Organizr
 					'name' => 'headerColor',
 					'label' => 'Nav Bar Color',
 					'value' => $this->config['headerColor'],
-					'class' => 'pick-a-color',
+					'class' => 'pick-a-color-custom-options',
 					'attr' => 'data-original="' . $this->config['headerColor'] . '"'
 				),
 				array(
@@ -1397,7 +1442,7 @@ class Organizr
 					'name' => 'headerTextColor',
 					'label' => 'Nav Bar Text Color',
 					'value' => $this->config['headerTextColor'],
-					'class' => 'pick-a-color',
+					'class' => 'pick-a-color-custom-options',
 					'attr' => 'data-original="' . $this->config['headerTextColor'] . '"'
 				),
 				array(
@@ -1405,7 +1450,7 @@ class Organizr
 					'name' => 'sidebarColor',
 					'label' => 'Side Bar Color',
 					'value' => $this->config['sidebarColor'],
-					'class' => 'pick-a-color',
+					'class' => 'pick-a-color-custom-options',
 					'attr' => 'data-original="' . $this->config['sidebarColor'] . '"'
 				),
 				array(
@@ -1413,7 +1458,7 @@ class Organizr
 					'name' => 'sidebarTextColor',
 					'label' => 'Side Bar Text Color',
 					'value' => $this->config['sidebarTextColor'],
-					'class' => 'pick-a-color',
+					'class' => 'pick-a-color-custom-options',
 					'attr' => 'data-original="' . $this->config['sidebarTextColor'] . '"'
 				),
 				array(
@@ -1421,7 +1466,7 @@ class Organizr
 					'name' => 'accentColor',
 					'label' => 'Accent Color',
 					'value' => $this->config['accentColor'],
-					'class' => 'pick-a-color',
+					'class' => 'pick-a-color-custom-options',
 					'attr' => 'data-original="' . $this->config['accentColor'] . '"'
 				),
 				array(
@@ -1429,7 +1474,7 @@ class Organizr
 					'name' => 'accentTextColor',
 					'label' => 'Accent Text Color',
 					'value' => $this->config['accentTextColor'],
-					'class' => 'pick-a-color',
+					'class' => 'pick-a-color-custom-options',
 					'attr' => 'data-original="' . $this->config['accentTextColor'] . '"'
 				),
 				array(
@@ -1437,7 +1482,7 @@ class Organizr
 					'name' => 'buttonColor',
 					'label' => 'Button Color',
 					'value' => $this->config['buttonColor'],
-					'class' => 'pick-a-color',
+					'class' => 'pick-a-color-custom-options',
 					'attr' => 'data-original="' . $this->config['buttonColor'] . '"'
 				),
 				array(
@@ -1445,7 +1490,7 @@ class Organizr
 					'name' => 'buttonTextColor',
 					'label' => 'Button Text Color',
 					'value' => $this->config['buttonTextColor'],
-					'class' => 'pick-a-color',
+					'class' => 'pick-a-color-custom-options',
 					'attr' => 'data-original="' . $this->config['buttonTextColor'] . '"'
 				),
 				array(
@@ -3429,6 +3474,9 @@ class Organizr
 					'ombiDefaultFilterUnapproved' => $this->config['ombiDefaultFilterUnapproved'] ? true : false,
 					'ombiDefaultFilterDenied' => $this->config['ombiDefaultFilterDenied'] ? true : false
 				),
+				'jackett' => array(
+					'homepageJackettBackholeDownload' => $this->config['homepageJackettBackholeDownload'] ? true : false
+				),
 				'options' => array(
 					'alternateHomepageHeaders' => $this->config['alternateHomepageHeaders'],
 					'healthChecksTags' => $this->config['healthChecksTags'],

+ 1 - 0
api/config/default.php

@@ -130,6 +130,7 @@ return array(
 	'homepageJackettAuth' => '1',
 	'jackettURL' => '',
 	'jackettToken' => '',
+	'homepageJackettBackholeDownload' => false,
 	'homepageCalendarEnabled' => false,
 	'homepageCalendarAuth' => '4',
 	'calendariCal' => '',

+ 3 - 3
api/functions/sso-functions.php

@@ -171,13 +171,13 @@ trait SSOFunctions
 				//"password" => ($oAuthToken ? "" : $password), // not needed yet
 				"authToken" => $oAuthToken
 			);
-			$endpoint = '/api/v1/auth/login';
+			$endpoint = '/api/v1/auth/plex';
 			$options = $this->requestOptions($url, false, 60);
 			$response = Requests::post($url . $endpoint, $headers, json_encode($data), $options);
 			if ($response->success) {
 				$user = json_decode($response->body, true); // not really needed yet
 				$token = $response->cookies['connect.sid']->value;
-				$this->writeLog('success', 'Overseerr Token Function - Grabbed token', $user['username']);
+				$this->writeLog('success', 'Overseerr Token Function - Grabbed token', $user['plexUsername']);
 			} else {
 				if ($fallback) {
 					$this->writeLog('error', 'Overseerr Token Function - Overseerr did not return Token - Will retry using fallback credentials', $username);
@@ -197,4 +197,4 @@ trait SSOFunctions
 		}
 	}
 	
-}
+}

+ 47 - 2
api/homepage/jackett.php

@@ -41,7 +41,15 @@ trait JackettHomepageItem
 						'value' => $this->config['jackettToken']
 					)
 				),
-				'Options' => array(),
+				'Options' => array(
+				    array(
+                	    'type' => 'switch',
+                		'name' => 'homepageJackettBackholeDownload',
+                		'label' => 'Prefer black hole download',
+                		'help' => 'Prefer black hole download link instead of direct/magnet download',
+                		'value' => $this->config['homepageJackettBackholeDownload']
+                	)
+                ),
 			)
 		);
 	}
@@ -116,4 +124,41 @@ trait JackettHomepageItem
 		$this->setAPIResponse('success', null, 200, $api);
 		return $api;
 	}
-}
+
+	public function performJackettBackHoleDownload($url = null)
+	{
+		if (!$this->homepageItemPermissions($this->jackettHomepagePermissions('main'), true)) {
+			return false;
+		}
+		if (!$url) {
+			$this->setAPIResponse('error', 'URL was not supplied', 422);
+			return false;
+		}
+		$apiURL = $this->qualifyURL($this->config['jackettURL']);
+		$endpoint = $apiURL . $url;
+		error_log($endpoint);
+		try {
+			$headers = array();
+			$options = array('timeout' => 120);
+			$response = Requests::get($endpoint, $headers, $options);
+			if ($response->success) {
+				$apiData = json_decode($response->body, true);
+				$api['content'] = $apiData;
+				unset($apiData);
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Jackett blackhole download failed ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		if ($api['content'] && $api['content']['result'] == 'success') {
+			$this->setAPIResponse('success', null, 200, $api);
+		} else if ($api['content']) {
+			$this->setAPIResponse('error', $api['content']['error'], 400, $api);
+		} else {
+			$this->setAPIResponse('error', 'Unknown error', 400, $api);
+		}
+		return $api;
+	}
+}

+ 131 - 70
api/pages/settings-tab-editor-categories.php

@@ -11,86 +11,147 @@ function get_page_settings_tab_editor_categories($Organizr)
 	if (!$Organizr->qualifyRequest(1, true)) {
 		return false;
 	}
+	$iconSelectors = '
+		$(".categoryIconIconList").select2({
+			ajax: {
+				url: \'api/v2/icon\',
+				data: function (params) {
+					var query = {
+						search: params.term,
+						page: params.page || 1
+					}
+					return query;
+				},
+				processResults: function (data, params) {
+					params.page = params.page || 1;
+					return {
+						results: data.response.data.results,
+						pagination: {
+							more: (params.page * 20) < data.response.data.total
+						}
+					};
+				},
+				//cache: true
+			},
+			placeholder: \'Search for an icon\',
+			templateResult: formatIcon,
+			templateSelection: formatIcon
+		});
+		
+		$(".categoryIconImageList").select2({
+			 ajax: {
+				url: \'api/v2/image/select\',
+				data: function (params) {
+					var query = {
+						search: params.term,
+						page: params.page || 1
+					}
+					return query;
+				},
+				processResults: function (data, params) {
+					params.page = params.page || 1;
+					return {
+						results: data.response.data.results,
+						pagination: {
+							more: (params.page * 20) < data.response.data.total
+						}
+					};
+				},
+				//cache: true
+			},
+			placeholder: \'Search for an image\',
+			templateResult: formatImage,
+			templateSelection: formatImage
+		});
+	';
 	return '
 <script>
 buildCategoryEditor();
 $( \'#categoryEditorTable\' ).sortable({
-    stop: function () {
-        var inputs = $(\'input.order\');
-        var nbElems = inputs.length;
-        inputs.each(function(idx) {
-            $(this).val(idx + 1);
-        });
-        submitCategoryOrder();
-    }
+	stop: function () {
+		var inputs = $(\'input.order\');
+		var nbElems = inputs.length;
+		inputs.each(function(idx) {
+			$(this).val(idx + 1);
+		});
+		submitCategoryOrder();
+	}
 });
+' . $iconSelectors . '
 </script>
 <div class="panel bg-org panel-info">
-    <div class="panel-heading">
-        <span lang="en">Category Editor</span>
-        <button type="button" class="btn btn-success btn-circle pull-right popup-with-form m-r-5" href="#new-category-form" data-effect="mfp-3d-unfold"><i class="fa fa-plus"></i> </button>
-    </div>
-    <div class="table-responsive">
-        <form id="submit-categories-form" onsubmit="return false;">
-            <table class="table table-hover manage-u-table">
-                <thead>
-                    <tr>
-                        <th width="70" class="text-center">#</th>
-                        <th lang="en">NAME</th>
-                        <th lang="en" style="text-align:center">TABS</th>
-                        <th lang="en" style="text-align:center">DEFAULT</th>
-                        <th lang="en" style="text-align:center">EDIT</th>
-                        <th lang="en" style="text-align:center">DELETE</th>
-                    </tr>
-                </thead>
-                <tbody id="categoryEditorTable"><td class="text-center" colspan="6"><i class="fa fa-spin fa-spinner"></i></td></tbody>
-            </table>
-        </form>
-    </div>
+	<div class="panel-heading">
+		<span lang="en">Category Editor</span>
+		<button type="button" class="btn btn-success btn-circle pull-right popup-with-form m-r-5" href="#new-category-form" data-effect="mfp-3d-unfold"><i class="fa fa-plus"></i> </button>
+	</div>
+	<div class="table-responsive">
+		<form id="submit-categories-form" onsubmit="return false;">
+			<table class="table table-hover manage-u-table">
+				<thead>
+					<tr>
+						<th width="70" class="text-center">#</th>
+						<th lang="en">NAME</th>
+						<th lang="en" style="text-align:center">TABS</th>
+						<th lang="en" style="text-align:center">DEFAULT</th>
+						<th lang="en" style="text-align:center">EDIT</th>
+						<th lang="en" style="text-align:center">DELETE</th>
+					</tr>
+				</thead>
+				<tbody id="categoryEditorTable"><td class="text-center" colspan="6"><i class="fa fa-spin fa-spinner"></i></td></tbody>
+			</table>
+		</form>
+	</div>
 </div>
 <form id="new-category-form" class="mfp-hide white-popup-block mfp-with-anim">
-    <h1 lang="en">Add New Category</h1>
-    <fieldset style="border:0;">
-        <div class="form-group">
-            <label class="control-label" for="new-category-form-inputNameNew" lang="en">Category Name</label>
-            <input type="text" class="form-control" id="new-category-form-inputNameNew" name="category" required="" autofocus>
-        </div>
-        <div class="form-group">
-            <label class="control-label" for="new-category-form-inputImageNew" lang="en">Category Image</label>
-            <input type="text" class="form-control" id="new-category-form-inputImageNew" name="image"  required="">
-        </div>
-    </fieldset>
-    <button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none addNewCategory" type="button"><span class="btn-label"><i class="fa fa-plus"></i></span><span lang="en">Add Category</span></button>
-    <div class="clearfix"></div>
+	<h1 lang="en">Add New Category</h1>
+	<fieldset style="border:0;">
+		<div class="form-group">
+			<label class="control-label" for="new-category-form-inputNameNew" lang="en">Category Name</label>
+			<input type="text" class="form-control" id="new-category-form-inputNameNew" name="category" required="" autofocus>
+		</div>
+		<div class="row">
+			<div class="form-group col-lg-6">
+				<label class="control-label" for="new-category-form-chooseImage" lang="en">Choose Image</label>
+				<select class="form-control categoryIconImageList" id="new-category-form-chooseImage" name="chooseImage"><option lang="en">Select or type Image</option></select>
+			</div>
+			<div class="form-group col-lg-6">
+				<label class="control-label" for="new-category-form-chooseIcon" lang="en">Choose Icon</label>
+				<select class="form-control categoryIconIconList" id="new-category-form-chooseIcon" name="chooseIcon"><option lang="en">Select or type Icon</option></select>
+			</div>
+		</div>
+		<div class="form-group">
+			<label class="control-label" for="new-category-form-inputImageNew" lang="en">Category Image</label>
+			<input type="text" class="form-control" id="new-category-form-inputImageNew" name="image"  required="">
+		</div>
+	</fieldset>
+	<button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none addNewCategory" type="button"><span class="btn-label"><i class="fa fa-plus"></i></span><span lang="en">Add Category</span></button>
+	<div class="clearfix"></div>
 </form>
 <form id="edit-category-form" class="mfp-hide white-popup-block mfp-with-anim">
-    <input type="hidden" name="id" value="">
-    <h1 lang="en">Edit Category</h1>
-    <fieldset style="border:0;">
-        <div class="form-group">
-            <label class="control-label" for="edit-category-form-inputName" lang="en">Category Name</label>
-            <input type="text" class="form-control" id="edit-category-form-inputName" name="category" required="" autofocus>
-        </div>
-        <div class="form-group">
-            <label class="control-label" for="edit-category-form-inputImage" lang="en">Category Image</label>
-            <div class="panel panel-info">
-                <div class="panel-heading">
-                    <span lang="en">Image Legend</span>
-                    <div class="pull-right"><a href="#" data-perform="panel-collapse"><i class="ti-plus"></i></a></div>
-                </div>
-                <div class="panel-wrapper collapse" aria-expanded="false">
-                    <div class="panel-body">
-                        <p lang="en">You may use an image or icon in this field</p>
-                        <p lang="en">For images, use the following format:</p><code>url::path/to/image</code>
-                        <p lang="en">For icons, use the following format:</p><code>icon-type::icon-name</code> i.e. <code>fontawesome::home</code>
-                    </div>
-                </div>
-            </div>
-            <input type="text" class="form-control" id="edit-category-form-inputImage" name="image"  required="">
-        </div>
-    </fieldset>
-    <button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none editCategory" type="button"><span class="btn-label"><i class="fa fa-plus"></i></span><span lang="en">Edit Category</span></button>
-    <div class="clearfix"></div>
+	<input type="hidden" name="id" value="">
+	<h1 lang="en">Edit Category</h1>
+	<fieldset style="border:0;">
+		<div class="form-group">
+			<label class="control-label" for="edit-category-form-inputName" lang="en">Category Name</label>
+			<input type="text" class="form-control" id="edit-category-form-inputName" name="category" required="" autofocus>
+		</div>
+		<div class="row">
+			<div class="form-group col-lg-6">
+				<label class="control-label" for="edit-category-form-chooseImage" lang="en">Choose Image</label>
+				<select class="form-control categoryIconImageList" id="edit-category-form-chooseImage" name="chooseImage"><option lang="en">Select or type Image</option></select>
+			</div>
+			<div class="form-group col-lg-6">
+				<label class="control-label" for="edit-category-form-chooseIcon" lang="en">Choose Icon</label>
+				<select class="form-control categoryIconIconList" id="edit-category-form-chooseIcon" name="chooseIcon"><option lang="en">Select or type Icon</option></select>
+			</div>
+		</div>
+		<div class="form-group">
+			<label class="control-label" for="edit-category-form-inputImage" lang="en">Category Image</label>
+			<input type="text" class="form-control" id="edit-category-form-inputImage" name="image"  required="">
+		</div>
+	</fieldset>
+	<button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none editCategory" type="button"><span class="btn-label"><i class="fa fa-plus"></i></span><span lang="en">Edit Category</span></button>
+	<div class="clearfix"></div>
 </form>
 ';
-}
+}

+ 175 - 176
api/pages/settings-tab-editor-tabs.php

@@ -12,17 +12,16 @@ function get_page_settings_tab_editor_tabs($Organizr)
 		return false;
 	}
 	$iconSelectors = '
-
-	    $(".tabIconIconList").select2({
-	        ajax: {
-			    url: \'api/v2/icon\',
-			    data: function (params) {
+		$(".tabIconIconList").select2({
+			ajax: {
+				url: \'api/v2/icon\',
+				data: function (params) {
 					var query = {
 						search: params.term,
 						page: params.page || 1
 					}
 					return query;
-			    },
+				},
 				processResults: function (data, params) {
 					params.page = params.page || 1;
 					return {
@@ -32,7 +31,7 @@ function get_page_settings_tab_editor_tabs($Organizr)
 						}
 					};
 				},
-			    //cache: true
+				//cache: true
 			},
 			placeholder: \'Search for an icon\',
 			templateResult: formatIcon,
@@ -41,14 +40,14 @@ function get_page_settings_tab_editor_tabs($Organizr)
 		
 		$(".tabIconImageList").select2({
 			 ajax: {
-			    url: \'api/v2/image/select\',
-			    data: function (params) {
+				url: \'api/v2/image/select\',
+				data: function (params) {
 					var query = {
 						search: params.term,
 						page: params.page || 1
 					}
 					return query;
-			    },
+				},
 				processResults: function (data, params) {
 					params.page = params.page || 1;
 					return {
@@ -58,7 +57,7 @@ function get_page_settings_tab_editor_tabs($Organizr)
 						}
 					};
 				},
-			    //cache: true
+				//cache: true
 			},
 			placeholder: \'Search for an image\',
 			templateResult: formatImage,
@@ -70,185 +69,185 @@ function get_page_settings_tab_editor_tabs($Organizr)
 	buildTabEditor();
 	!function(a){function f(a,b){if(!(a.originalEvent.touches.length>1)){a.preventDefault();var c=a.originalEvent.changedTouches[0],d=document.createEvent("MouseEvents");d.initMouseEvent(b,!0,!0,window,1,c.screenX,c.screenY,c.clientX,c.clientY,!1,!1,!1,!1,0,null),a.target.dispatchEvent(d)}}if(a.support.touch="ontouchend"in document,a.support.touch){var e,b=a.ui.mouse.prototype,c=b._mouseInit,d=b._mouseDestroy;b._touchStart=function(a){var b=this;!e&&b._mouseCapture(a.originalEvent.changedTouches[0])&&(e=!0,b._touchMoved=!1,f(a,"mouseover"),f(a,"mousemove"),f(a,"mousedown"))},b._touchMove=function(a){e&&(this._touchMoved=!0,f(a,"mousemove"))},b._touchEnd=function(a){e&&(f(a,"mouseup"),f(a,"mouseout"),this._touchMoved||f(a,"click"),e=!1)},b._mouseInit=function(){var b=this;b.element.bind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),c.call(b)},b._mouseDestroy=function(){var b=this;b.element.unbind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),d.call(b)}}}(jQuery);
 	$( \'#tabEditorTable\' ).sortable({
-	    stop: function () {
-	        $(\'input.order\').each(function(idx) {
-	            $(this).val(idx + 1);
-	        });
-	        var newTabs = $( "#submit-tabs-form" ).serializeToJSON();
-	        newTabsGlobal = newTabs;
-	        $(\'.saveTabOrderButton\').removeClass(\'hidden\');
-	        //submitTabOrder(newTabs);
-	    }
+		stop: function () {
+			$(\'input.order\').each(function(idx) {
+				$(this).val(idx + 1);
+			});
+			var newTabs = $( "#submit-tabs-form" ).serializeToJSON();
+			newTabsGlobal = newTabs;
+			$(\'.saveTabOrderButton\').removeClass(\'hidden\');
+			//submitTabOrder(newTabs);
+		}
 	});
 	$( \'#tabEditorTable\' ).disableSelection();
 	' . $iconSelectors . '
 	</script>
 	<div class="panel bg-org panel-info">
-	    <div class="panel-heading">
-	        <span lang="en">Tab Editor</span>
-	        <button type="button" class="btn btn-info btn-circle pull-right popup-with-form m-r-5" href="#new-tab-form" data-effect="mfp-3d-unfold"><i class="fa fa-plus"></i> </button>
-	        <button type="button" class="btn btn-info btn-circle pull-right m-r-5 help-modal" data-modal="tabs"><i class="fa fa-question-circle"></i> </button>
-	    	<button onclick="submitTabOrder(newTabsGlobal)" class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right animated loop-animation rubberBand m-r-20 saveTabOrderButton hidden" type="button"><span class="btn-label"><i class="fa fa-save"></i></span><span lang="en">Save Tab Order</span></button>
-	    </div>
-	    <div class="table-responsive">
-	        <form id="submit-tabs-form" onsubmit="return false;">
-	            <table class="table table-hover manage-u-table">
-	                <thead>
-	                    <tr>
-	                        <th width="70" class="text-center">#</th>
-	                        <th lang="en">NAME</th>
-	                        <th lang="en">CATEGORY</th>
-	                        <th lang="en">GROUP</th>
-	                        <th lang="en">TYPE</th>
-	                        <th lang="en" style="text-align:center">DEFAULT</th>
-	                        <th lang="en" style="text-align:center">ACTIVE</th>
-	                        <th lang="en" style="text-align:center">SPLASH</th>
-	                        <th lang="en" style="text-align:center">PING</th>
-	                        <th lang="en" style="text-align:center">PRELOAD</th>
-	                        <th lang="en" style="text-align:center">EDIT</th>
-	                        <th lang="en" style="text-align:center">DELETE</th>
-	                    </tr>
-	                </thead>
-	                <tbody id="tabEditorTable">
-	                	<td class="text-center" colspan="12"><i class="fa fa-spin fa-spinner"></i></td>
+		<div class="panel-heading">
+			<span lang="en">Tab Editor</span>
+			<button type="button" class="btn btn-info btn-circle pull-right popup-with-form m-r-5" href="#new-tab-form" data-effect="mfp-3d-unfold"><i class="fa fa-plus"></i> </button>
+			<button type="button" class="btn btn-info btn-circle pull-right m-r-5 help-modal" data-modal="tabs"><i class="fa fa-question-circle"></i> </button>
+			<button onclick="submitTabOrder(newTabsGlobal)" class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right animated loop-animation rubberBand m-r-20 saveTabOrderButton hidden" type="button"><span class="btn-label"><i class="fa fa-save"></i></span><span lang="en">Save Tab Order</span></button>
+		</div>
+		<div class="table-responsive">
+			<form id="submit-tabs-form" onsubmit="return false;">
+				<table class="table table-hover manage-u-table">
+					<thead>
+						<tr>
+							<th width="70" class="text-center">#</th>
+							<th lang="en">NAME</th>
+							<th lang="en">CATEGORY</th>
+							<th lang="en">GROUP</th>
+							<th lang="en">TYPE</th>
+							<th lang="en" style="text-align:center">DEFAULT</th>
+							<th lang="en" style="text-align:center">ACTIVE</th>
+							<th lang="en" style="text-align:center">SPLASH</th>
+							<th lang="en" style="text-align:center">PING</th>
+							<th lang="en" style="text-align:center">PRELOAD</th>
+							<th lang="en" style="text-align:center">EDIT</th>
+							<th lang="en" style="text-align:center">DELETE</th>
+						</tr>
+					</thead>
+					<tbody id="tabEditorTable">
+						<td class="text-center" colspan="12"><i class="fa fa-spin fa-spinner"></i></td>
 					</tbody>
-	            </table>
-	        </form>
-	    </div>
+				</table>
+			</form>
+		</div>
 	</div>
 	<form id="new-tab-form" class="mfp-hide white-popup-block mfp-with-anim">
-	    <h1 lang="en">Add New Tab</h1>
-	    <fieldset style="border:0;">
-	        <div class="alert alert-success alert-dismissable tabTestMessage hidden">
-	            <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
-	            <span lang="en">Tab can be set as iFrame</span>
-	        </div>
-	        <div class="alert alert-danger alert-dismissable tabTestMessage hidden">
-	            <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
-	            <span lang="en">Please set tab as [New Window] on next screen</span>
-	        </div>
-	        <div class="form-group">
-	            <label class="control-label" for="new-tab-form-inputNameNew" lang="en">Tab Name</label>
-	            <input type="text" class="form-control" id="new-tab-form-inputNameNew" name="name" required="" autofocus>
-	        </div>
-	        <div class="form-group">
-	            <label class="control-label" for="new-tab-form-inputURLNew" lang="en">Tab URL</label>
-	            <input type="text" class="form-control" id="new-tab-form-inputURLNew" name="url"  required="">
-	        </div>
-	        <div class="form-group">
-	            <label class="control-label" for="new-tab-form-inputURLLocalNew" lang="en">Tab Local URL</label>
-	            <input type="text" class="form-control" id="new-tab-form-inputURLLocalNew" name="url_local">
-	        </div>
-	        <div class="form-group">
-	            <label class="control-label" for="new-tab-form-inputPingURLNew" lang="en">Ping URL</label>
-	            <input type="text" class="form-control" id="new-tab-form-inputPingURLNew" name="ping_url"  placeholder="host/ip:port">
-	        </div>
-	        <div class="row">
-		        <div class="form-group col-lg-6">
-		            <label class="control-label" for="new-tab-form-inputTabActionTypeNew" lang="en">Tab Auto Action</label>
-		                <select class="form-control" id="new-tab-form-inputTabActionTypeNew" name="timeout">
-		                    <option value="null">None</option>
-		                    <option value="1">Auto Close</option>
-		                    <option value="2">Auto Reload</option>
+		<h1 lang="en">Add New Tab</h1>
+		<fieldset style="border:0;">
+			<div class="alert alert-success alert-dismissable tabTestMessage hidden">
+				<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
+				<span lang="en">Tab can be set as iFrame</span>
+			</div>
+			<div class="alert alert-danger alert-dismissable tabTestMessage hidden">
+				<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
+				<span lang="en">Please set tab as [New Window] on next screen</span>
+			</div>
+			<div class="form-group">
+				<label class="control-label" for="new-tab-form-inputNameNew" lang="en">Tab Name</label>
+				<input type="text" class="form-control" id="new-tab-form-inputNameNew" name="name" required="" autofocus>
+			</div>
+			<div class="form-group">
+				<label class="control-label" for="new-tab-form-inputURLNew" lang="en">Tab URL</label>
+				<input type="text" class="form-control" id="new-tab-form-inputURLNew" name="url"  required="">
+			</div>
+			<div class="form-group">
+				<label class="control-label" for="new-tab-form-inputURLLocalNew" lang="en">Tab Local URL</label>
+				<input type="text" class="form-control" id="new-tab-form-inputURLLocalNew" name="url_local">
+			</div>
+			<div class="form-group">
+				<label class="control-label" for="new-tab-form-inputPingURLNew" lang="en">Ping URL</label>
+				<input type="text" class="form-control" id="new-tab-form-inputPingURLNew" name="ping_url"  placeholder="host/ip:port">
+			</div>
+			<div class="row">
+				<div class="form-group col-lg-6">
+					<label class="control-label" for="new-tab-form-inputTabActionTypeNew" lang="en">Tab Auto Action</label>
+						<select class="form-control" id="new-tab-form-inputTabActionTypeNew" name="timeout">
+							<option value="null">None</option>
+							<option value="1">Auto Close</option>
+							<option value="2">Auto Reload</option>
 						</select>
-		        </div>
-		        <div class="form-group col-lg-6">
-		            <label class="control-label" for="new-tab-form-inputTabActionTimeNew" lang="en">Tab Auto Action Minutes</label>
-		                <input type="number" class="form-control" id="new-tab-form-inputTabActionTimeNew" name="timeout_ms"  placeholder="0">
-		        </div>
-		    </div>
-	        <div class="row">
-		        <div class="form-group col-lg-4">
-		            <label class="control-label" for="new-tab-form-chooseImage" lang="en">Choose Image</label>
-		            <select class="form-control tabIconImageList" id="new-tab-form-chooseImage" name="chooseImage"><option lang="en">Select or type Image</option></select>
-		        </div>
-		        <div class="form-group col-lg-4">
-		            <label class="control-label" for="new-tab-form-chooseIcon" lang="en">Choose Icon</label>
+				</div>
+				<div class="form-group col-lg-6">
+					<label class="control-label" for="new-tab-form-inputTabActionTimeNew" lang="en">Tab Auto Action Minutes</label>
+						<input type="number" class="form-control" id="new-tab-form-inputTabActionTimeNew" name="timeout_ms"  placeholder="0">
+				</div>
+			</div>
+			<div class="row">
+				<div class="form-group col-lg-4">
+					<label class="control-label" for="new-tab-form-chooseImage" lang="en">Choose Image</label>
+					<select class="form-control tabIconImageList" id="new-tab-form-chooseImage" name="chooseImage"><option lang="en">Select or type Image</option></select>
+				</div>
+				<div class="form-group col-lg-4">
+					<label class="control-label" for="new-tab-form-chooseIcon" lang="en">Choose Icon</label>
 					<select class="form-control tabIconIconList" id="new-tab-form-chooseIcon" name="chooseIcon"><option lang="en">Select or type Icon</option></select>
-		        </div>
-		        <div class="form-group col-lg-4">
-		            <label class="control-label" for="new-tab-form-chooseBlackberry" lang="en">Choose Blackberry Theme Icon</label>
-		            <button id="new-tab-form-chooseBlackberry" class="btn btn-xs btn-primary waves-effect waves-light form-control" onclick="showBlackberryThemes(\'new-tab-form-inputImageNew\');" type="button">
-		                <i class="fa fa-search"></i>&nbsp; <span lang="en">Choose</span>
-		            </button>
-		        </div>
-		    </div>
-	        <div class="form-group">
-	            <label class="control-label" for="new-tab-form-inputImageNew" lang="en">Tab Image</label>
-	            <input type="text" class="form-control" id="new-tab-form-inputImageNew" name="image"  required="">
-	        </div>
-	    </fieldset>
-	    <button class="btn btn-sm btn-info btn-rounded waves-effect waves-light row b-none testTab" type="button"><span class="btn-label"><i class="fa fa-flask"></i></span><span lang="en">Test Tab</span></button>
-	    <button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none addNewTab" type="button"><span class="btn-label"><i class="fa fa-plus"></i></span><span lang="en">Add Tab</span></button>
-	    <div class="clearfix"></div>
+				</div>
+				<div class="form-group col-lg-4">
+					<label class="control-label" for="new-tab-form-chooseBlackberry" lang="en">Choose Blackberry Theme Icon</label>
+					<button id="new-tab-form-chooseBlackberry" class="btn btn-xs btn-primary waves-effect waves-light form-control" onclick="showBlackberryThemes(\'new-tab-form-inputImageNew\');" type="button">
+						<i class="fa fa-search"></i>&nbsp; <span lang="en">Choose</span>
+					</button>
+				</div>
+			</div>
+			<div class="form-group">
+				<label class="control-label" for="new-tab-form-inputImageNew" lang="en">Tab Image</label>
+				<input type="text" class="form-control" id="new-tab-form-inputImageNew" name="image"  required="">
+			</div>
+		</fieldset>
+		<button class="btn btn-sm btn-info btn-rounded waves-effect waves-light row b-none testTab" type="button"><span class="btn-label"><i class="fa fa-flask"></i></span><span lang="en">Test Tab</span></button>
+		<button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none addNewTab" type="button"><span class="btn-label"><i class="fa fa-plus"></i></span><span lang="en">Add Tab</span></button>
+		<div class="clearfix"></div>
 	</form>
 	<form id="edit-tab-form" class="mfp-hide white-popup-block mfp-with-anim">
-	    <input type="hidden" name="id" value="x">
-	    <span class="hidden" id="originalTabName"></span>
-	    <h1 lang="en">Edit Tab</h1>
-	    <fieldset style="border:0;">
-	        <div class="alert alert-success alert-dismissable tabEditTestMessage hidden">
-	            <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
-	            <span lang="en">Tab can be set as iFrame</span>
-	        </div>
-	        <div class="alert alert-danger alert-dismissable tabEditTestMessage hidden">
-	            <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
-	            <span lang="en">Please set tab as [New Window] on next screen</span>
-	        </div>
-	        <div class="form-group">
-	            <label class="control-label" for="edit-tab-form-inputName" lang="en">Tab Name</label>
-	            <input type="text" class="form-control" id="edit-tab-form-inputName" name="name" required="" autofocus>
-	        </div>
-	        <div class="form-group">
-	            <label class="control-label" for="edit-tab-form-inputURL" lang="en">Tab URL</label>
-	            <input type="text" class="form-control" id="edit-tab-form-inputURL" name="url"  required="">
-	        </div>
-	        <div class="form-group">
-	            <label class="control-label" for="edit-tab-form-inputLocalURL" lang="en">Tab Local URL</label>
-	            <input type="text" class="form-control" id="edit-tab-form-inputLocalURL" name="url_local">
-	        </div>
-	        <div class="form-group">
-	            <label class="control-label" for="edit-tab-form-pingURL" lang="en">Ping URL</label>
-	            <input type="text" class="form-control" id="edit-tab-form-pingURL" name="ping_url" placeholder="host/ip:port">
-	        </div>
-	        <div class="row">
-		        <div class="form-group col-lg-6">
-		            <label class="control-label" for="edit-tab-form-inputTabActionTypeNew" lang="en">Tab Auto Action</label>
-		                <select class="form-control" id="edit-tab-form-inputTabActionTypeNew" name="timeout">
-		                    <option value="null">None</option>
-		                    <option value="1">Auto Close</option>
-		                    <option value="2">Auto Reload</option>
+		<input type="hidden" name="id" value="x">
+		<span class="hidden" id="originalTabName"></span>
+		<h1 lang="en">Edit Tab</h1>
+		<fieldset style="border:0;">
+			<div class="alert alert-success alert-dismissable tabEditTestMessage hidden">
+				<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
+				<span lang="en">Tab can be set as iFrame</span>
+			</div>
+			<div class="alert alert-danger alert-dismissable tabEditTestMessage hidden">
+				<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
+				<span lang="en">Please set tab as [New Window] on next screen</span>
+			</div>
+			<div class="form-group">
+				<label class="control-label" for="edit-tab-form-inputName" lang="en">Tab Name</label>
+				<input type="text" class="form-control" id="edit-tab-form-inputName" name="name" required="" autofocus>
+			</div>
+			<div class="form-group">
+				<label class="control-label" for="edit-tab-form-inputURL" lang="en">Tab URL</label>
+				<input type="text" class="form-control" id="edit-tab-form-inputURL" name="url"  required="">
+			</div>
+			<div class="form-group">
+				<label class="control-label" for="edit-tab-form-inputLocalURL" lang="en">Tab Local URL</label>
+				<input type="text" class="form-control" id="edit-tab-form-inputLocalURL" name="url_local">
+			</div>
+			<div class="form-group">
+				<label class="control-label" for="edit-tab-form-pingURL" lang="en">Ping URL</label>
+				<input type="text" class="form-control" id="edit-tab-form-pingURL" name="ping_url" placeholder="host/ip:port">
+			</div>
+			<div class="row">
+				<div class="form-group col-lg-6">
+					<label class="control-label" for="edit-tab-form-inputTabActionTypeNew" lang="en">Tab Auto Action</label>
+						<select class="form-control" id="edit-tab-form-inputTabActionTypeNew" name="timeout">
+							<option value="null">None</option>
+							<option value="1">Auto Close</option>
+							<option value="2">Auto Reload</option>
 						</select>
-		        </div>
-		        <div class="form-group col-lg-6">
-		            <label class="control-label" for="edit-tab-form-inputTabActionTimeNew" lang="en">Tab Auto Action Minutes</label>
-		                <input type="number" class="form-control" id="edit-tab-form-inputTabActionTimeNew" name="timeout_ms">
-		        </div>
-		    </div>
-	        <div class="row">
-		        <div class="form-group col-lg-4">
-		            <label class="control-label" for="edit-tab-form-chooseImage" lang="en">Choose Image</label>
-		            <select class="form-control tabIconImageList" id="edit-tab-form-chooseImage" name="chooseImage"><option lang="en">Select or type Image</option></select>
-		        </div>
-		        <div class="form-group col-lg-4">
-		            <label class="control-label" for="edit-tab-form-chooseIcon" lang="en">Choose Icon</label>
+				</div>
+				<div class="form-group col-lg-6">
+					<label class="control-label" for="edit-tab-form-inputTabActionTimeNew" lang="en">Tab Auto Action Minutes</label>
+						<input type="number" class="form-control" id="edit-tab-form-inputTabActionTimeNew" name="timeout_ms">
+				</div>
+			</div>
+			<div class="row">
+				<div class="form-group col-lg-4">
+					<label class="control-label" for="edit-tab-form-chooseImage" lang="en">Choose Image</label>
+					<select class="form-control tabIconImageList" id="edit-tab-form-chooseImage" name="chooseImage"><option lang="en">Select or type Image</option></select>
+				</div>
+				<div class="form-group col-lg-4">
+					<label class="control-label" for="edit-tab-form-chooseIcon" lang="en">Choose Icon</label>
 					<select class="form-control tabIconIconList" id="edit-tab-form-chooseIcon" name="chooseIcon"><option lang="en">Select or type Icon</option></select>
-		        </div>
-		        <div class="form-group col-lg-4">
-		            <label class="control-label" for="edit-tab-form-chooseBlackberry" lang="en">Choose Blackberry Theme Icon</label>
-		            <button id="edit-tab-form-chooseBlackberry" class="btn btn-xs btn-primary waves-effect waves-light form-control" onclick="showBlackberryThemes(\'edit-tab-form-inputImage\');" type="button">
-		                <i class="fa fa-search"></i>&nbsp; <span lang="en">Choose</span>
-		            </button>
-		        </div>
-		    </div>
-	        <div class="form-group">
-	            <label class="control-label" for="edit-tab-form-inputImage" lang="en">Tab Image</label>
-	            <input type="text" class="form-control" id="edit-tab-form-inputImage" name="image"  required="">
-	        </div>
-	    </fieldset>
-	    <button class="btn btn-sm btn-info btn-rounded waves-effect waves-light row b-none testEditTab" type="button"><span class="btn-label"><i class="fa fa-flask"></i></span><span lang="en">Test Tab</span></button>
-	    <button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none editTab" type="button"><span class="btn-label"><i class="fa fa-check"></i></span><span lang="en">Edit Tab</span></button>
-	    <div class="clearfix"></div>
+				</div>
+				<div class="form-group col-lg-4">
+					<label class="control-label" for="edit-tab-form-chooseBlackberry" lang="en">Choose Blackberry Theme Icon</label>
+					<button id="edit-tab-form-chooseBlackberry" class="btn btn-xs btn-primary waves-effect waves-light form-control" onclick="showBlackberryThemes(\'edit-tab-form-inputImage\');" type="button">
+						<i class="fa fa-search"></i>&nbsp; <span lang="en">Choose</span>
+					</button>
+				</div>
+			</div>
+			<div class="form-group">
+				<label class="control-label" for="edit-tab-form-inputImage" lang="en">Tab Image</label>
+				<input type="text" class="form-control" id="edit-tab-form-inputImage" name="image"  required="">
+			</div>
+		</fieldset>
+		<button class="btn btn-sm btn-info btn-rounded waves-effect waves-light row b-none testEditTab" type="button"><span class="btn-label"><i class="fa fa-flask"></i></span><span lang="en">Test Tab</span></button>
+		<button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none editTab" type="button"><span class="btn-label"><i class="fa fa-check"></i></span><span lang="en">Edit Tab</span></button>
+		<div class="clearfix"></div>
 	</form>
 	';
 }

+ 1 - 1
api/pages/settings.php

@@ -12,7 +12,7 @@ function get_page_settings($Organizr)
 		return false;
 	}
 	$Organizr->writeLog('success', 'Admin Function -  Accessed Settings Page', $Organizr->user['username']);
-	return '
+	return $Organizr->pluginFiles('js', true) . '
 <script>
     (function() {
         updateCheck();

+ 205 - 0
api/plugins/api/bookmark.php

@@ -0,0 +1,205 @@
+<?php
+$app->get('/plugins/bookmark/settings', function ($request, $response, $args) {
+	$Bookmark = new Bookmark();
+	if ($Bookmark->checkRoute($request)) {
+		if ($Bookmark->qualifyRequest(1, true)) {
+			$GLOBALS['api']['response']['data'] = $Bookmark->_getSettings();
+		}
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->get('/plugins/bookmark/page', function ($request, $response, $args) {
+	$Bookmark = new Bookmark();
+	if ($Bookmark->_checkRequest($request) && $Bookmark->checkRoute($request)) {
+		if ($Bookmark->qualifyRequest(1, true)) {
+			$GLOBALS['api']['response']['data'] = $Bookmark->_getPage();
+		}
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->get('/plugins/bookmark/setup/tab', function ($request, $response, $args) {
+	$Bookmark = new Bookmark();
+	if ($Bookmark->_checkRequest($request) && $Bookmark->checkRoute($request)) {
+		if ($Bookmark->qualifyRequest(1, true)) {
+			$GLOBALS['api']['response']['data'] = $Bookmark->_checkForBookmarkTab();
+		}
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->get('/plugins/bookmark/setup/category', function ($request, $response, $args) {
+	$Bookmark = new Bookmark();
+	if ($Bookmark->_checkRequest($request) && $Bookmark->checkRoute($request)) {
+		if ($Bookmark->qualifyRequest(1, true)) {
+			$GLOBALS['api']['response']['data'] = $Bookmark->_checkForBookmarkCategories();
+		}
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->get('/plugins/bookmark/settings_tab_editor_bookmark_tabs', function ($request, $response, $args) {
+	$Bookmark = new Bookmark();
+	if ($Bookmark->_checkRequest($request) && $Bookmark->checkRoute($request)) {
+		if ($Bookmark->qualifyRequest(1, true)) {
+			$GLOBALS['api']['response']['data'] = $Bookmark->_getSettingsTabEditorBookmarkTabsPage();
+		}
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->get('/plugins/bookmark/settings_tab_editor_bookmark_categories', function ($request, $response, $args) {
+	$Bookmark = new Bookmark();
+	if ($Bookmark->_checkRequest($request) && $Bookmark->checkRoute($request)) {
+		if ($Bookmark->qualifyRequest(1, true)) {
+			$GLOBALS['api']['response']['data'] = $Bookmark->_getSettingsTabEditorBookmarkCategoriesPage();
+		}
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+// TABS
+$app->get('/plugins/bookmark/tabs', function ($request, $response, $args) {
+	$Bookmark = new Bookmark();
+	if ($Bookmark->_checkRequest($request) && $Bookmark->checkRoute($request)) {
+		if ($Bookmark->qualifyRequest(1, true)) {
+			$GLOBALS['api']['response']['data'] = $Bookmark->_getTabs();
+		}
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->get('/plugins/bookmark/tabs/{id}', function ($request, $response, $args) {
+	$Bookmark = new Bookmark();
+	if ($Bookmark->_checkRequest($request) && $Bookmark->checkRoute($request)) {
+		$GLOBALS['api']['response']['data'] = $Bookmark->_getTabByIdCheckUser($args['id']);
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->post('/plugins/bookmark/tabs', function ($request, $response, $args) {
+	$Bookmark = new Bookmark();
+	if ($Bookmark->_checkRequest($request) && $Bookmark->checkRoute($request)) {
+		if ($Bookmark->qualifyRequest(1, true)) {
+			$Bookmark->_addTab($Bookmark->apiData($request));
+		}
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->put('/plugins/bookmark/tabs', function ($request, $response, $args) {
+	$Bookmark = new Bookmark();
+	if ($Bookmark->_checkRequest($request) && $Bookmark->checkRoute($request)) {
+		if ($Bookmark->qualifyRequest(1, true)) {
+			$Bookmark->_updateTabOrder($Bookmark->apiData($request));
+		}
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->put('/plugins/bookmark/tabs/{id}', function ($request, $response, $args) {
+	$Bookmark = new Bookmark();
+	if ($Bookmark->_checkRequest($request) && $Bookmark->checkRoute($request)) {
+		if ($Bookmark->qualifyRequest(1, true)) {
+			$Bookmark->_updateTab($args['id'], $Bookmark->apiData($request));
+		}
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->delete('/plugins/bookmark/tabs/{id}', function ($request, $response, $args) {
+	$Bookmark = new Bookmark();
+	if ($Bookmark->_checkRequest($request) && $Bookmark->checkRoute($request)) {
+		if ($Bookmark->qualifyRequest(1, true)) {
+			$Bookmark->_deleteTab($args['id']);
+		}
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json')
+		->withStatus($GLOBALS['responseCode']);
+});
+// CATEGORIES
+$app->get('/plugins/bookmark/categories', function ($request, $response, $args) {
+	$Bookmark = new Bookmark();
+	if ($Bookmark->_checkRequest($request) && $Bookmark->checkRoute($request)) {
+		if ($Bookmark->qualifyRequest(1, true)) {
+			$GLOBALS['api']['response']['data'] = $Bookmark->_getTabs();
+		}
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->post('/plugins/bookmark/categories', function ($request, $response, $args) {
+	$Bookmark = new Bookmark();
+	if ($Bookmark->_checkRequest($request) && $Bookmark->checkRoute($request)) {
+		if ($Bookmark->qualifyRequest(1, true)) {
+			$Bookmark->_addCategory($Bookmark->apiData($request));
+		}
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->put('/plugins/bookmark/categories', function ($request, $response, $args) {
+	$Bookmark = new Bookmark();
+	if ($Bookmark->_checkRequest($request) && $Bookmark->checkRoute($request)) {
+		if ($Bookmark->qualifyRequest(1, true)) {
+			$Bookmark->_updateCategoryOrder($Bookmark->apiData($request));
+		}
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->put('/plugins/bookmark/categories/{id}', function ($request, $response, $args) {
+	$Bookmark = new Bookmark();
+	if ($Bookmark->_checkRequest($request) && $Bookmark->checkRoute($request)) {
+		if ($Bookmark->qualifyRequest(1, true)) {
+			$Bookmark->_updateCategory($args['id'], $Bookmark->apiData($request));
+		}
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->delete('/plugins/bookmark/categories/{id}', function ($request, $response, $args) {
+	$Bookmark = new Bookmark();
+	if ($Bookmark->_checkRequest($request) && $Bookmark->checkRoute($request)) {
+		if ($Bookmark->qualifyRequest(1, true)) {
+			$Bookmark->_deleteCategory($args['id']);
+		}
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json')
+		->withStatus($GLOBALS['responseCode']);
+});

+ 1079 - 0
api/plugins/bookmark.php

@@ -0,0 +1,1079 @@
+<?php
+// PLUGIN INFORMATION
+$GLOBALS['plugins'][]['Bookmark'] = array( // Plugin Name
+	'name' => 'Bookmark', // Plugin Name
+	'author' => 'leet1994', // Who wrote the plugin
+	'category' => 'Utilities', // One to Two Word Description
+	'link' => '', // Link to plugin info
+	'license' => 'personal,business', // License Type use , for multiple
+	'idPrefix' => 'BOOKMARK', // html element id prefix
+	'configPrefix' => 'BOOKMARK', // config file prefix for array items without the hypen
+	'dbPrefix' => 'BOOKMARK', // db prefix
+	'version' => '0.1.0', // SemVer of plugin
+	'image' => 'plugins/images/bookmark.png', // 1:1 non transparent image for plugin
+	'settings' => true, // does plugin need a settings modal?
+	'bind' => true, // use default bind to make settings page - true or false
+	'api' => 'api/v2/plugins/bookmark/settings', // api route for settings page
+	'homepage' => false // Is plugin for use on homepage? true or false
+);
+
+// Logo image under Public Domain from https://openclipart.org/detail/182527/open-book
+class Bookmark extends Organizr
+{
+	public function writeLog($type = 'error', $message = null, $username = null)
+	{
+		parent::writeLog($type, "Plugin 'Bookmark': " . $message, $username);
+	}
+	
+	public function _checkRequest($request)
+	{
+		$result = false;
+		if ($this->config['BOOKMARK-enabled'] && $this->hasDB()) {
+			if (!$this->_checkDatabaseTablesExist()) {
+				$this->_createDatabaseTables();
+			}
+			$result = true;
+		}
+		return $result;
+	}
+	
+	protected function _checkDatabaseTablesExist()
+	{
+		$response = [
+			array(
+				'function' => 'fetchSingle',
+				'query' => array(
+					"SELECT `name` FROM `sqlite_master` WHERE `type` = 'table' AND `name` = 'BOOKMARK-categories'"
+				),
+				'key' => 'BOOKMARK-categories'
+			),
+			array(
+				'function' => 'fetchSingle',
+				'query' => array(
+					"SELECT `name` FROM `sqlite_master` WHERE `type` = 'table' AND `name` = 'BOOKMARK-tabs'"
+				),
+				'key' => 'BOOKMARK-tabs'
+			),
+		];
+		$data = $this->processQueries($response);
+		return ($data["BOOKMARK-categories"] != false && $data["BOOKMARK-tabs"] != false);
+	}
+	
+	protected function _createDatabaseTables()
+	{
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => 'CREATE TABLE `BOOKMARK-categories` (
+					`id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
+					`order`	INTEGER,
+					`category`	TEXT UNIQUE,
+					`category_id`	INTEGER,
+					`default` INTEGER
+				);'
+			),
+			array(
+				'function' => 'query',
+				'query' => 'CREATE TABLE `BOOKMARK-tabs` (
+					`id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
+					`order`	INTEGER,
+					`category_id`	INTEGER,
+					`name`	TEXT,
+					`url`	TEXT,
+					`enabled`	INTEGER,
+					`group_id`	INTEGER,
+					`image`	TEXT,
+					`background_color` TEXT,
+					`text_color` TEXT
+				);'
+			)
+		];
+		$this->processQueries($response);
+	}
+	
+	public function _getSettings()
+	{
+		return array(
+			'custom' => '
+				<div class="row">
+					<div class="col-lg-6 col-sm-12 col-md-6">
+						<div class="white-box">
+							<h3 class="box-title" lang="en">Automatic Setup Tasks</h3>
+							<ul class="feeds">
+								<li class="bookmark-check-tab">
+									<div class="bg-info">
+										<i class="sticon ti-layout-tab-v text-white"></i>
+									</div>
+									<small lang="en">Checking for Bookmark tab...</small>
+									<span class="text-muted result"><i class="fa fa-spin fa-refresh"></i></span>
+								</li>
+								<li class="bookmark-check-category">
+									<div class="bg-success">
+										<i class="ti-layout-list-thumb text-white"></i>
+									</div>
+									<small lang="en">Checking for bookmark default category...</small>
+									<span class="text-muted result"><i class="fa fa-spin fa-refresh"></i></span>
+								</li>
+							</ul>
+						</div>
+					</div>
+					<div class="col-lg-6 col-sm-12 col-md-6">
+						<div class="panel panel-info">
+							<div class="panel-heading">
+								<span lang="en">Notice</span>
+							</div>
+							<div class="panel-wrapper collapse in" aria-expanded="true">
+								<div class="panel-body">
+									<ul class="list-icons">
+										<li><i class="fa fa-chevron-right text-info"></i> Add tab that points to <i>api/v2/plugins/bookmark/page</i> and set it\'s type to <i>Organizr</i>.</li>
+										<li><i class="fa fa-chevron-right text-info"></i> Create Bookmark categories in the new area in <i>Tab Editor</i>.</li>
+										<li><i class="fa fa-chevron-right text-info"></i> Create Bookmark tabs in the new area in <i>Tab Editor</i>.</li>
+										<li><i class="fa fa-chevron-right text-info"></i> Open your custom Bookmark page via menu.</li>
+									</ul>
+								</div>
+							</div>
+						</div>
+					</div>
+				</div>
+			'
+		);
+	}
+	
+	public function _getPage()
+	{
+		$bookmarks = '<div id="BOOKMARK-wrapper">';
+		foreach ($this->_getAllCategories() as $category) {
+			$tabs = $this->_getRelevantTabsForCategory($category['category_id']);
+			if (count($tabs) == 0) continue;
+			$bookmarks .= '<div class="BOOKMARK-category">
+				<div class="BOOKMARK-category-title">
+					' . $category['category'] . '
+				</div>
+				<div class="BOOKMARK-category-content">';
+			foreach ($tabs as $tab) {
+				$bookmarks .= '<a href="' . $tab['url'] . '" target="_SELF">
+					<div class="BOOKMARK-tab"
+						style="border-color: ' . $this->adjustBrightness($tab['background_color'], 0.3) . '; background: linear-gradient(90deg, ' . $this->adjustBrightness($tab['background_color'], -0.3) . ' 0%, ' . $tab['background_color'] . ' 70%, ' . $this->adjustBrightness($tab['background_color'], 0.1) . ' 100%);">
+						<span class="BOOKMARK-tab-image">' . $this->_iconPrefix($tab['image']) . '</span>
+						<span class="BOOKMARK-tab-title" style="color: ' . $tab['text_color'] . ';">' . $tab['name'] . '</span>
+					</div>
+				</a>';
+			}
+			$bookmarks .= '</div></div>';
+		}
+		$bookmarks .= '</div>';
+		return $bookmarks;
+	}
+	
+	protected function _iconPrefix($source)
+	{
+		$tabIcon = explode("::", $source);
+		$icons = array(
+			"materialize" => "mdi mdi-",
+			"fontawesome" => "fa fa-",
+			"themify" => "ti-",
+			"simpleline" => "icon-",
+			"weathericon" => "wi wi-",
+			"alphanumeric" => "fa-fw",
+		);
+		if (is_array($tabIcon) && count($tabIcon) == 2) {
+			if ($tabIcon[0] !== 'url' && $tabIcon[0] !== 'alphanumeric') {
+				return '<i class="' . $icons[$tabIcon[0]] . $tabIcon[1] . '"></i>';
+			} else if ($tabIcon[0] == 'alphanumeric') {
+				return '<i>' . $tabIcon[1] . '</i>';
+			} else {
+				return '<img src="' . $tabIcon[1] . '" alt="tabIcon" />';
+			}
+		} else {
+			return '<img src="' . $source . '" alt="tabIcon" />';
+		}
+	}
+	
+	protected function _getAllCategories()
+	{
+		$response = [
+			array(
+				'function' => 'fetchAll',
+				'query' => 'SELECT * FROM `BOOKMARK-categories` ORDER BY `order` ASC'
+			)
+		];
+		return $this->processQueries($response);
+	}
+	
+	protected function _getRelevantTabsForCategory($category_id)
+	{
+		$response = [
+			array(
+				'function' => 'fetchAll',
+				'query' => array(
+					"SELECT * FROM `BOOKMARK-tabs` WHERE `enabled`='1' AND `category_id`=? AND `group_id`>=? ORDER BY `order` ASC",
+					$category_id,
+					$this->getUserLevel()
+				)
+			)
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function _getTabs()
+	{
+		$response = [
+			array(
+				'function' => 'fetchAll',
+				'query' => 'SELECT * FROM `BOOKMARK-tabs` ORDER BY `order` ASC',
+				'key' => 'tabs'
+			),
+			array(
+				'function' => 'fetchAll',
+				'query' => 'SELECT * FROM `BOOKMARK-categories` ORDER BY `order` ASC',
+				'key' => 'categories'
+			),
+			array(
+				'function' => 'fetchAll',
+				'query' => 'SELECT * FROM `groups` ORDER BY `group_id` ASC',
+				'key' => 'groups'
+			)
+		];
+		return $this->processQueries($response);
+	}
+	
+	// Tabs
+	public function _getSettingsTabEditorBookmarkTabsPage()
+	{
+		$iconSelectors = '
+			$(".bookmarkTabIconIconList").select2({
+				ajax: {
+					url: \'api/v2/icon\',
+					data: function (params) {
+						var query = {
+							search: params.term,
+							page: params.page || 1
+						}
+						return query;
+					},
+					processResults: function (data, params) {
+						params.page = params.page || 1;
+						return {
+							results: data.response.data.results,
+							pagination: {
+								more: (params.page * 20) < data.response.data.total
+							}
+						};
+					},
+					//cache: true
+				},
+				placeholder: \'Search for an icon\',
+				templateResult: formatIcon,
+				templateSelection: formatIcon
+			});
+
+			$(".bookmarkTabIconImageList").select2({
+				 ajax: {
+					url: \'api/v2/image/select\',
+					data: function (params) {
+						var query = {
+							search: params.term,
+							page: params.page || 1
+						}
+						return query;
+					},
+					processResults: function (data, params) {
+						params.page = params.page || 1;
+						return {
+							results: data.response.data.results,
+							pagination: {
+								more: (params.page * 20) < data.response.data.total
+							}
+						};
+					},
+					//cache: true
+				},
+				placeholder: \'Search for an image\',
+				templateResult: formatImage,
+				templateSelection: formatImage
+			});
+		';
+		return '
+		<script>
+		buildBookmarkTabEditor();
+		!function(a){function f(a,b){if(!(a.originalEvent.touches.length>1)){a.preventDefault();var c=a.originalEvent.changedTouches[0],d=document.createEvent("MouseEvents");d.initMouseEvent(b,!0,!0,window,1,c.screenX,c.screenY,c.clientX,c.clientY,!1,!1,!1,!1,0,null),a.target.dispatchEvent(d)}}if(a.support.touch="ontouchend"in document,a.support.touch){var e,b=a.ui.mouse.prototype,c=b._mouseInit,d=b._mouseDestroy;b._touchStart=function(a){var b=this;!e&&b._mouseCapture(a.originalEvent.changedTouches[0])&&(e=!0,b._touchMoved=!1,f(a,"mouseover"),f(a,"mousemove"),f(a,"mousedown"))},b._touchMove=function(a){e&&(this._touchMoved=!0,f(a,"mousemove"))},b._touchEnd=function(a){e&&(f(a,"mouseup"),f(a,"mouseout"),this._touchMoved||f(a,"click"),e=!1)},b._mouseInit=function(){var b=this;b.element.bind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),c.call(b)},b._mouseDestroy=function(){var b=this;b.element.unbind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),d.call(b)}}}(jQuery);
+		$( \'#bookmarkTabEditorTable\' ).sortable({
+			stop: function () {
+				$(\'input.order\').each(function(idx) {
+					$(this).val(idx + 1);
+				});
+				var newTabs = $( "#submit-bookmark-tabs-form" ).serializeToJSON();
+				newBookmarkTabsGlobal = newTabs;
+				$(\'.saveBookmarkTabOrderButton\').removeClass(\'hidden\');
+				//submitTabOrder(newTabs);
+			}
+		});
+		$( \'#bookmarkTabEditorTable\' ).disableSelection();
+		' . $iconSelectors . '
+		</script>
+		<div class="panel bg-org panel-info">
+			<div class="panel-heading">
+				<span lang="en">Bookmark Tab Editor</span>
+				<button type="button" class="btn btn-info btn-circle pull-right popup-with-form m-r-5" href="#new-bookmark-tab-form" data-effect="mfp-3d-unfold"><i class="fa fa-plus"></i> </button>
+				<button onclick="submitBookmarkTabOrder(newBookmarkTabsGlobal)" class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right animated loop-animation rubberBand m-r-20 saveBookmarkTabOrderButton hidden" type="button"><span class="btn-label"><i class="fa fa-save"></i></span><span lang="en">Save Tab Order</span></button>
+			</div>
+			<div class="table-responsive">
+				<form id="submit-bookmark-tabs-form" onsubmit="return false;">
+					<table class="table table-hover manage-u-table">
+						<thead>
+							<tr>
+								<th width="70" class="text-center">#</th>
+								<th lang="en">NAME</th>
+								<th lang="en">CATEGORY</th>
+								<th lang="en">GROUP</th>
+								<th lang="en" style="text-align:center">ACTIVE</th>
+								<th lang="en" style="text-align:center">EDIT</th>
+								<th lang="en" style="text-align:center">DELETE</th>
+							</tr>
+						</thead>
+						<tbody id="bookmarkTabEditorTable">
+							<td class="text-center" colspan="12"><i class="fa fa-spin fa-spinner"></i></td>
+						</tbody>
+					</table>
+				</form>
+			</div>
+		</div>
+		<form id="new-bookmark-tab-form" class="mfp-hide white-popup-block mfp-with-anim">
+			<h1 lang="en">Add New Tab</h1>
+			<fieldset style="border:0;">
+				<div class="form-group">
+					<label class="control-label" for="new-bookmark-tab-form-inputNameNew" lang="en">Tab Name</label>
+					<input type="text" class="form-control" id="new-bookmark-tab-form-inputNameNew" name="name" required="" autofocus>
+				</div>
+				<div class="form-group">
+					<label class="control-label" for="new-bookmark-tab-form-inputURLNew" lang="en">Tab URL</label>
+					<input type="text" class="form-control" id="new-bookmark-tab-form-inputURLNew" name="url"  required="">
+				</div>
+				<div class="row">
+					<div class="form-group col-lg-4">
+						<label class="control-label" for="new-bookmark-tab-form-chooseImage" lang="en">Choose Image</label>
+						<select class="form-control bookmarkTabIconImageList" id="new-bookmark-tab-form-chooseImage" name="chooseImage"><option lang="en">Select or type Image</option></select>
+					</div>
+					<div class="form-group col-lg-4">
+						<label class="control-label" for="new-bookmark-tab-form-chooseIcon" lang="en">Choose Icon</label>
+						<select class="form-control bookmarkTabIconIconList" id="new-bookmark-tab-form-chooseIcon" name="chooseIcon"><option lang="en">Select or type Icon</option></select>
+					</div>
+					<div class="form-group col-lg-4">
+						<label class="control-label" for="new-bookmark-tab-form-chooseBlackberry" lang="en">Choose Blackberry Theme Icon</label>
+						<button id="new-bookmark-tab-form-chooseBlackberry" class="btn btn-xs btn-primary waves-effect waves-light form-control" onclick="showBlackberryThemes(\'new-bookmark-tab-form-inputImageNew\');" type="button">
+							<i class="fa fa-search"></i>&nbsp; <span lang="en">Choose</span>
+						</button>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label" for="new-bookmark-tab-form-inputImageNew" lang="en">Tab Image</label>
+					<input type="text" class="form-control" id="new-bookmark-tab-form-inputImageNew" name="image" required="">
+				</div>
+				<div class="row">
+					<div class="form-group col-lg-4">
+						<label class="control-label" for="new-bookmark-tab-form-inputBackgroundColorNew" lang="en">Background Color</label>
+						<input type="text" class="form-control" id="new-bookmark-tab-form-inputBackgroundColorNew" name="background_color" required="">
+					</div>
+					<div class="form-group col-lg-4">
+						<label class="control-label" for="new-bookmark-tab-form-inputTextColorNew" lang="en">Text Color</label>
+						<input type="text" class="form-control" id="new-bookmark-tab-form-inputTextColorNew" name="text_color" required="">
+					</div>
+				</div>
+			</fieldset>
+			<button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none addNewBookmarkTab" type="button"><span class="btn-label"><i class="fa fa-plus"></i></span><span lang="en">Add Tab</span></button>
+			<div class="clearfix"></div>
+		</form>
+		<form id="edit-bookmark-tab-form" class="mfp-hide white-popup-block mfp-with-anim">
+			<input type="hidden" name="id" value="x">
+			<span class="hidden" id="originalBookmarkTabName"></span>
+			<h1 lang="en">Edit Tab</h1>
+			<fieldset style="border:0;">
+				<div class="form-group">
+					<label class="control-label" for="edit-bookmark-tab-form-inputName" lang="en">Tab Name</label>
+					<input type="text" class="form-control" id="edit-bookmark-tab-form-inputName" name="name" required="" autofocus>
+				</div>
+				<div class="form-group">
+					<label class="control-label" for="edit-bookmark-tab-form-inputURL" lang="en">Tab URL</label>
+					<input type="text" class="form-control" id="edit-bookmark-tab-form-inputURL" name="url"  required="">
+				</div>
+				<div class="row">
+					<div class="form-group col-lg-4">
+						<label class="control-label" for="edit-bookmark-tab-form-chooseImage" lang="en">Choose Image</label>
+						<select class="form-control bookmarkTabIconImageList" id="edit-bookmark-tab-form-chooseImage" name="chooseImage"><option lang="en">Select or type Image</option></select>
+					</div>
+					<div class="form-group col-lg-4">
+						<label class="control-label" for="edit-bookmark-tab-form-chooseIcon" lang="en">Choose Icon</label>
+						<select class="form-control bookmarkTabIconIconList" id="edit-bookmark-tab-form-chooseIcon" name="chooseIcon"><option lang="en">Select or type Icon</option></select>
+					</div>
+					<div class="form-group col-lg-4">
+						<label class="control-label" for="edit-bookmark-tab-form-chooseBlackberry" lang="en">Choose Blackberry Theme Icon</label>
+						<button id="edit-bookmark-tab-form-chooseBlackberry" class="btn btn-xs btn-primary waves-effect waves-light form-control" onclick="showBlackberryThemes(\'edit-bookmark-tab-form-inputImage\');" type="button">
+							<i class="fa fa-search"></i>&nbsp; <span lang="en">Choose</span>
+						</button>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label" for="edit-bookmark-tab-form-inputImage" lang="en">Tab Image</label>
+					<input type="text" class="form-control" id="edit-bookmark-tab-form-inputImage" name="image"  required="">
+				</div>
+				<div class="row">
+					<div class="form-group col-lg-4">
+						<label class="control-label" for="new-bookmark-tab-form-inputBackgroundColor" lang="en">Background Color</label>
+						<input type="text" class="form-control" id="new-bookmark-tab-form-inputBackgroundColor" name="background_color" required="">
+					</div>
+					<div class="form-group col-lg-4">
+						<label class="control-label" for="new-bookmark-tab-form-inputTextColor" lang="en">Text Color</label>
+						<input type="text" class="form-control" id="new-bookmark-tab-form-inputTextColor" name="text_color" required="">
+					</div>
+				</div>
+			</fieldset>
+			<button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none editBookmarkTab" type="button"><span class="btn-label"><i class="fa fa-check"></i></span><span lang="en">Edit Tab</span></button>
+			<div class="clearfix"></div>
+		</form>
+		';
+	}
+	
+	public function _isBookmarkTabNameTaken($name, $id = null)
+	{
+		if ($id) {
+			$response = [
+				array(
+					'function' => 'fetchAll',
+					'query' => array(
+						'SELECT * FROM `BOOKMARK-tabs` WHERE `name` LIKE ? AND `id` != ?',
+						$name,
+						$id
+					)
+				),
+			];
+		} else {
+			$response = [
+				array(
+					'function' => 'fetchAll',
+					'query' => array(
+						'SELECT * FROM `BOOKMARK-tabs` WHERE `name` LIKE ?',
+						$name
+					)
+				),
+			];
+		}
+		return $this->processQueries($response);
+	}
+	
+	public function _getNextBookmarkTabOrder()
+	{
+		$response = [
+			array(
+				'function' => 'fetchSingle',
+				'query' => array(
+					'SELECT `order` from `BOOKMARK-tabs` ORDER BY `order` DESC'
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function _getBookmarkTabById($id)
+	{
+		$response = [
+			array(
+				'function' => 'fetch',
+				'query' => array(
+					'SELECT * FROM `BOOKMARK-tabs` WHERE `id` = ?',
+					$id
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function _getTabByIdCheckUser($id)
+	{
+		$tabInfo = $this->_getBookmarkTabById($id);
+		if ($tabInfo) {
+			if ($this->qualifyRequest($tabInfo['group_id'], true)) {
+				return $tabInfo;
+			}
+		} else {
+			$this->setAPIResponse('error', 'id not found', 404);
+			return false;
+		}
+	}
+	
+	public function _deleteTab($id)
+	{
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'DELETE FROM `BOOKMARK-tabs` WHERE id = ?',
+					$id
+				)
+			),
+		];
+		$tabInfo = $this->_getBookmarkTabById($id);
+		if ($tabInfo) {
+			$this->writeLog('success', 'Tab Delete Function -  Deleted Tab [' . $tabInfo['name'] . ']', $this->user['username']);
+			$this->setAPIResponse('success', 'Tab deleted', 204);
+			return $this->processQueries($response);
+		} else {
+			$this->setAPIResponse('error', 'id not found', 404);
+			return false;
+		}
+	}
+	
+	public function _addTab($array)
+	{
+		if (!$array) {
+			$this->setAPIResponse('error', 'no data was sent', 422);
+			return null;
+		}
+		$array = $this->checkKeys($this->getTableColumnsFormatted('BOOKMARK-tabs'), $array);
+		$array['group_id'] = ($array['group_id']) ?? $this->getDefaultGroupId();
+		$array['category_id'] = ($array['category_id']) ?? $this->_getDefaultBookmarkCategoryId();
+		$array['enabled'] = ($array['enabled']) ?? 0;
+		$array['order'] = ($array['order']) ?? $this->_getNextBookmarkTabOrder() + 1;
+		if (array_key_exists('name', $array)) {
+			if ($this->_isBookmarkTabNameTaken($array['name'])) {
+				$this->setAPIResponse('error', 'Tab name: ' . $array['name'] . ' is already taken', 409);
+				return false;
+			}
+		} else {
+			$this->setAPIResponse('error', 'Tab name was not supplied', 422);
+			return false;
+		}
+		if (!array_key_exists('url', $array)) {
+			$this->setAPIResponse('error', 'Tab url was not supplied', 422);
+			return false;
+		}
+		if (!array_key_exists('image', $array)) {
+			$this->setAPIResponse('error', 'Tab image was not supplied', 422);
+			return false;
+		}
+		if (array_key_exists('background_color', $array)) {
+			if (!$this->_checkColorHexCode($array['background_color'])) {
+				$this->setAPIResponse('error', 'Tab background color is invalid', 422);
+				return false;
+			}
+		} else {
+			$this->setAPIResponse('error', 'Tab background color was not supplied', 422);
+			return false;
+		}
+		if (array_key_exists('text_color', $array)) {
+			if (!$this->_checkColorHexCode($array['text_color'])) {
+				$this->setAPIResponse('error', 'Tab text color is invalid', 422);
+				return false;
+			}
+		} else {
+			$this->setAPIResponse('error', 'Tab text color was not supplied', 422);
+			return false;
+		}
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'INSERT INTO [BOOKMARK-tabs]',
+					$array
+				)
+			),
+		];
+		$this->setAPIResponse(null, 'Tab added');
+		$this->writeLog('success', 'Tab Editor Function -  Added Tab for [' . $array['name'] . ']', $this->user['username']);
+		return $this->processQueries($response);
+	}
+	
+	public function _updateTab($id, $array)
+	{
+		if (!$id || $id == '') {
+			$this->setAPIResponse('error', 'id was not set', 422);
+			return null;
+		}
+		if (!$array) {
+			$this->setAPIResponse('error', 'no data was sent', 422);
+			return null;
+		}
+		$tabInfo = $this->_getBookmarkTabById($id);
+		if ($tabInfo) {
+			$array = $this->checkKeys($tabInfo, $array);
+		} else {
+			$this->setAPIResponse('error', 'No tab info found', 404);
+			return false;
+		}
+		if (array_key_exists('name', $array)) {
+			if ($this->_isBookmarkTabNameTaken($array['name'], $id)) {
+				$this->setAPIResponse('error', 'Tab name: ' . $array['name'] . ' is already taken', 409);
+				return false;
+			}
+		}
+		if (array_key_exists('background_color', $array)) {
+			if (!$this->_checkColorHexCode($array['background_color'])) {
+				$this->setAPIResponse('error', 'Tab background color is invalid', 422);
+				return false;
+			}
+		}
+		if (array_key_exists('text_color', $array)) {
+			if (!$this->_checkColorHexCode($array['text_color'])) {
+				$this->setAPIResponse('error', 'Tab text color is invalid', 422);
+				return false;
+			}
+		}
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'UPDATE `BOOKMARK-tabs` SET',
+					$array,
+					'WHERE id = ?',
+					$id
+				)
+			),
+		];
+		$this->setAPIResponse(null, 'Tab info updated');
+		$this->writeLog('success', 'Tab Editor Function -  Edited Tab Info for [' . $tabInfo['name'] . ']', $this->user['username']);
+		return $this->processQueries($response);
+	}
+	
+	public function _updateTabOrder($array)
+	{
+		if (count($array) >= 1) {
+			foreach ($array as $tab) {
+				if (count($tab) !== 2) {
+					$this->setAPIResponse('error', 'data is malformed', 422);
+					break;
+				}
+				$id = $tab['id'] ?? null;
+				$order = $tab['order'] ?? null;
+				if ($id && $order) {
+					$response = [
+						array(
+							'function' => 'query',
+							'query' => array(
+								'UPDATE `BOOKMARK-tabs` set `order` = ? WHERE `id` = ?',
+								$order,
+								$id
+							)
+						),
+					];
+					$this->processQueries($response);
+					$this->setAPIResponse(null, 'Tab Order updated');
+				} else {
+					$this->setAPIResponse('error', 'data is malformed', 422);
+				}
+			}
+		} else {
+			$this->setAPIResponse('error', 'data is empty or not in array', 422);
+			return false;
+		}
+	}
+	
+	// Categories
+	public function _getSettingsTabEditorBookmarkCategoriesPage()
+	{
+		return '
+	<script>
+	buildBookmarkCategoryEditor();
+	$( \'#bookmarkCategoryEditorTable\' ).sortable({
+		stop: function () {
+			var inputs = $(\'input.order\');
+			var nbElems = inputs.length;
+			inputs.each(function(idx) {
+				$(this).val(idx + 1);
+			});
+			submitBookmarkCategoryOrder();
+		}
+	});
+	</script>
+	<div class="panel bg-org panel-info">
+		<div class="panel-heading">
+			<span lang="en">Bookmark Category Editor</span>
+			<button type="button" class="btn btn-success btn-circle pull-right popup-with-form m-r-5" href="#new-bookmark-category-form" data-effect="mfp-3d-unfold"><i class="fa fa-plus"></i> </button>
+		</div>
+		<div class="table-responsive">
+			<form id="submit-bookmark-categories-form" onsubmit="return false;">
+				<table class="table table-hover manage-u-table">
+					<thead>
+						<tr>
+							<th lang="en">NAME</th>
+							<th lang="en" style="text-align:center">TABS</th>
+							<th lang="en" style="text-align:center">DEFAULT</th>
+							<th lang="en" style="text-align:center">EDIT</th>
+							<th lang="en" style="text-align:center">DELETE</th>
+						</tr>
+					</thead>
+					<tbody id="bookmarkCategoryEditorTable"><td class="text-center" colspan="6"><i class="fa fa-spin fa-spinner"></i></td></tbody>
+				</table>
+			</form>
+		</div>
+	</div>
+	<form id="new-bookmark-category-form" class="mfp-hide white-popup-block mfp-with-anim">
+		<h1 lang="en">Add New Bookmark Category</h1>
+		<fieldset style="border:0;">
+			<div class="form-group">
+				<label class="control-label" for="new-bookmark-category-form-inputNameNew" lang="en">Category Name</label>
+				<input type="text" class="form-control" id="new-bookmark-category-form-inputNameNew" name="category" required="" autofocus>
+			</div>
+		</fieldset>
+		<button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none addNewBookmarkCategory" type="button"><span class="btn-label"><i class="fa fa-plus"></i></span><span lang="en">Add Category</span></button>
+		<div class="clearfix"></div>
+	</form>
+	<form id="edit-bookmark-category-form" class="mfp-hide white-popup-block mfp-with-anim">
+		<input type="hidden" name="id" value="">
+		<h1 lang="en">Edit Category</h1>
+		<fieldset style="border:0;">
+			<div class="form-group">
+				<label class="control-label" for="edit-bookmark-category-form-inputName" lang="en">Category Name</label>
+				<input type="text" class="form-control" id="edit-bookmark-category-form-inputName" name="category" required="" autofocus>
+			</div>
+		</fieldset>
+		<button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none editBookmarkCategory" type="button"><span class="btn-label"><i class="fa fa-plus"></i></span><span lang="en">Edit Category</span></button>
+		<div class="clearfix"></div>
+	</form>
+	';
+	}
+	
+	public function _getDefaultBookmarkCategoryId()
+	{
+		$response = [
+			array(
+				'function' => 'fetchSingle',
+				'query' => array(
+					'SELECT `category_id` FROM `BOOKMARK-categories` WHERE `default` = 1'
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function _getNextBookmarkCategoryOrder()
+	{
+		$response = [
+			array(
+				'function' => 'fetchSingle',
+				'query' => array(
+					'SELECT `order` from `BOOKMARK-categories` ORDER BY `order` DESC'
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function _getNextBookmarkCategoryId()
+	{
+		$response = [
+			array(
+				'function' => 'fetchSingle',
+				'query' => array(
+					'SELECT `category_id` from `BOOKMARK-categories` ORDER BY `category_id` DESC'
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function _isBookmarkCategoryNameTaken($name, $id = null)
+	{
+		if ($id) {
+			$response = [
+				array(
+					'function' => 'fetchAll',
+					'query' => array(
+						'SELECT * FROM `BOOKMARK-categories` WHERE `category` LIKE ? AND `id` != ?',
+						$name,
+						$id
+					)
+				),
+			];
+		} else {
+			$response = [
+				array(
+					'function' => 'fetchAll',
+					'query' => array(
+						'SELECT * FROM `BOOKMARK-categories` WHERE `category` LIKE ?',
+						$name
+					)
+				),
+			];
+		}
+		return $this->processQueries($response);
+	}
+	
+	public function _getBookmarkCategoryById($id)
+	{
+		$response = [
+			array(
+				'function' => 'fetch',
+				'query' => array(
+					'SELECT * FROM `BOOKMARK-categories` WHERE `id` = ?',
+					$id
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function _clearBookmarkCategoryDefault()
+	{
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'UPDATE `BOOKMARK-categories` SET `default` = 0'
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function _addCategory($array)
+	{
+		if (!$array) {
+			$this->setAPIResponse('error', 'no data was sent', 422);
+			return null;
+		}
+		$array = $this->checkKeys($this->getTableColumnsFormatted('BOOKMARK-categories'), $array);
+		$array['default'] = ($array['default']) ?? 0;
+		$array['order'] = ($array['order']) ?? $this->_getNextBookmarkCategoryOrder() + 1;
+		$array['category_id'] = ($array['category_id']) ?? $this->_getNextBookmarkCategoryId() + 1;
+		if (array_key_exists('category', $array)) {
+			if ($this->_isBookmarkCategoryNameTaken($array['category'])) {
+				$this->setAPIResponse('error', 'Category name: ' . $array['category'] . ' is already taken', 409);
+				return false;
+			}
+		} else {
+			$this->setAPIResponse('error', 'Category name was not supplied', 422);
+			return false;
+		}
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'INSERT INTO [BOOKMARK-categories]',
+					$array
+				)
+			),
+		];
+		$this->setAPIResponse(null, 'Category added');
+		$this->writeLog('success', 'Category Editor Function -  Added Category for [' . $array['category'] . ']', $this->user['username']);
+		$result = $this->processQueries($response);
+		$this->_correctDefaultCategory();
+		return $result;
+	}
+	
+	public function _updateCategory($id, $array)
+	{
+		if (!$id || $id == '') {
+			$this->setAPIResponse('error', 'id was not set', 422);
+			return null;
+		}
+		if (!$array) {
+			$this->setAPIResponse('error', 'no data was sent', 422);
+			return null;
+		}
+		$categoryInfo = $this->_getBookmarkCategoryById($id);
+		if ($categoryInfo) {
+			$array = $this->checkKeys($categoryInfo, $array);
+		} else {
+			$this->setAPIResponse('error', 'No category info found', 404);
+			return false;
+		}
+		if (array_key_exists('category', $array)) {
+			if ($this->_isBookmarkCategoryNameTaken($array['category'], $id)) {
+				$this->setAPIResponse('error', 'Category name: ' . $array['category'] . ' is already taken', 409);
+				return false;
+			}
+		}
+		if (array_key_exists('default', $array)) {
+			if ($array['default']) {
+				$this->_clearBookmarkCategoryDefault();
+			}
+		}
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'UPDATE `BOOKMARK-categories` SET',
+					$array,
+					'WHERE id = ?',
+					$id
+				)
+			),
+		];
+		$this->setAPIResponse(null, 'Category info updated');
+		$this->writeLog('success', 'Category Editor Function -  Edited Category Info for [' . $categoryInfo['category'] . ']', $this->user['username']);
+		$result = $this->processQueries($response);
+		$this->_correctDefaultCategory();
+		return $result;
+	}
+	
+	public function _updateCategoryOrder($array)
+	{
+		if (count($array) >= 1) {
+			foreach ($array as $category) {
+				if (count($category) !== 2) {
+					$this->setAPIResponse('error', 'data is malformed', 422);
+					break;
+				}
+				$id = $category['id'] ?? null;
+				$order = $category['order'] ?? null;
+				if ($id && $order) {
+					$response = [
+						array(
+							'function' => 'query',
+							'query' => array(
+								'UPDATE `BOOKMARK-categories` set `order` = ? WHERE `id` = ?',
+								$order,
+								$id
+							)
+						),
+					];
+					$this->processQueries($response);
+					$this->setAPIResponse(null, 'Category Order updated');
+				} else {
+					$this->setAPIResponse('error', 'data is malformed', 422);
+				}
+			}
+		} else {
+			$this->setAPIResponse('error', 'data is empty or not in array', 422);
+			return false;
+		}
+	}
+	
+	public function _deleteCategory($id)
+	{
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'DELETE FROM `BOOKMARK-categories` WHERE id = ?',
+					$id
+				)
+			),
+		];
+		$categoryInfo = $this->_getBookmarkCategoryById($id);
+		if ($categoryInfo) {
+			$this->writeLog('success', 'Category Delete Function -  Deleted Category [' . $categoryInfo['category'] . ']', $this->user['username']);
+			$this->setAPIResponse('success', 'Category deleted', 204);
+			$result = $this->processQueries($response);
+			$this->_correctDefaultCategory();
+			return $result;
+		} else {
+			$this->setAPIResponse('error', 'id not found', 404);
+			return false;
+		}
+	}
+	
+	protected function _correctDefaultCategory()
+	{
+		if ($this->_getDefaultBookmarkCategoryId() == null) {
+			$response = [
+				array(
+					'function' => 'query',
+					'query' => 'UPDATE `BOOKMARK-categories` SET `default` = 1 WHERE `category_id` = (SELECT `category_id` FROM `BOOKMARK-categories` ORDER BY `category_id` ASC LIMIT 0,1)'
+				)
+			];
+			return $this->processQueries($response);
+		}
+	}
+	
+	protected function _checkColorHexCode($hex)
+	{
+		return preg_match('/^\#([0-9a-fA-F]{3}){1,2}$/', $hex);
+	}
+	
+	/**
+	 * Increases or decreases the brightness of a color by a percentage of the current brightness.
+	 *
+	 * @param string $hexCode       Supported formats: `#FFF`, `#FFFFFF`, `FFF`, `FFFFFF`
+	 * @param float  $adjustPercent A number between -1 and 1. E.g. 0.3 = 30% lighter; -0.4 = 40% darker.
+	 *
+	 * @return  string
+	 *
+	 * @author  maliayas
+	 * @link    https://stackoverflow.com/questions/3512311/how-to-generate-lighter-darker-color-with-php
+	 */
+	protected function adjustBrightness($hexCode, $adjustPercent)
+	{
+		$hexCode = ltrim($hexCode, '#');
+		if (strlen($hexCode) == 3) {
+			$hexCode = $hexCode[0] . $hexCode[0] . $hexCode[1] . $hexCode[1] . $hexCode[2] . $hexCode[2];
+		}
+		$hexCode = array_map('hexdec', str_split($hexCode, 2));
+		foreach ($hexCode as &$color) {
+			$adjustableLimit = $adjustPercent < 0 ? $color : 255 - $color;
+			$adjustAmount = ceil($adjustableLimit * $adjustPercent);
+			$color = str_pad(dechex($color + $adjustAmount), 2, '0', STR_PAD_LEFT);
+		}
+		return '#' . implode($hexCode);
+	}
+	
+	public function _checkForBookmarkTab()
+	{
+		$response = [
+			array(
+				'function' => 'fetchAll',
+				'query' => array(
+					'SELECT * FROM tabs',
+					'WHERE url = ?',
+					'api/v2/plugins/bookmark/page'
+				)
+			),
+		];
+		$tab = $this->processQueries($response);
+		if ($tab) {
+			$this->setAPIResponse('success', 'Tab already exists', 200);
+			return $tab;
+		} else {
+			$createTab = $this->_createBookmarkTab();
+			if ($createTab) {
+				$tab = $this->processQueries($response);
+				$this->setAPIResponse('success', 'Tab created', 200);
+				return $tab;
+			} else {
+				$this->setAPIResponse('error', 'Tab creation error', 500);
+			}
+		}
+	}
+	
+	public function _createBookmarkTab()
+	{
+		$tabInfo = [
+			'order' => $this->getNextTabOrder() + 1,
+			'category_id' => $this->getDefaultCategoryId(),
+			'name' => 'Bookmarks',
+			'url' => 'api/v2/plugins/bookmark/page',
+			'default' => false,
+			'enabled' => true,
+			'group_id' => $this->getDefaultGroupId(),
+			'image' => 'fontawesome::book',
+			'type' => 0
+		];
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'INSERT INTO [tabs]',
+					$tabInfo
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function _checkForBookmarkCategories()
+	{
+		$categories = $this->_getAllCategories();
+		if ($categories) {
+			$this->setAPIResponse('success', 'Categories already exists', 200);
+			return $categories;
+		} else {
+			$createCategory = $this->_addCategory(['category' => 'Unsorted', 'default' => 1]);
+			if ($createCategory) {
+				$categories = $this->_getAllCategories();
+				$this->setAPIResponse('success', 'Category created', 200);
+				return $categories;
+			} else {
+				$this->setAPIResponse('error', 'Category creation error', 500);
+			}
+		}
+	}
+}

+ 3 - 1
api/plugins/chat.php

@@ -12,7 +12,9 @@ $GLOBALS['plugins'][]['chat'] = array( // Plugin Name
 	'configPrefix' => 'CHAT', // config file prefix for array items without the hypen
 	'version' => '1.0.0', // SemVer of plugin
 	'image' => 'plugins/images/chat.png', // 1:1 non transparent image for plugin
-	'settings' => true, // does plugin need a settings page? true or false
+	'settings' => true, // does plugin need a settings modal?
+	'bind' => true, // use default bind to make settings page - true or false
+	'api' => 'api/v2/plugins/chat/settings', // api route for settings page
 	'homepage' => false // Is plugin for use on homepage? true or false
 );
 

+ 4 - 0
api/plugins/config/bookmark.php

@@ -0,0 +1,4 @@
+<?php
+return array(
+    'BOOKMARK-enabled' => false
+);

+ 70 - 0
api/plugins/css/bookmark.css

@@ -0,0 +1,70 @@
+#BOOKMARK-wrapper {
+    display: flex;
+    flex-direction: column;
+    justify-content: flex-start;
+}
+
+.BOOKMARK-category {
+    text-align: center;
+    margin-bottom: 40px;
+}
+
+.BOOKMARK-category-title {
+    font-weight: 500;
+    color: #ddd;
+    font-size: large;
+}
+
+.BOOKMARK-category-content {
+    width: 80%;
+    margin: 0 auto;
+    display: flex;
+    flex-flow: row wrap;
+    justify-content: center;
+}
+
+.BOOKMARK-tab {
+    display: inline-flex;
+    justify-content: space-between;
+    align-items: center;
+    margin: 10px 10px 0 10px;
+    height: 50px;
+    width: 200px;
+    overflow: hidden;
+    border: 1px solid;
+    border-radius: 5px;
+    transition: all 0.2s ease-in-out;
+}
+
+.BOOKMARK-tab:hover {
+    filter: brightness(80%);
+}
+
+.BOOKMARK-tab-image {
+    width: 50px;
+    max-width: 50px;
+    height: 100%;
+    flex-grow: 33;
+}
+
+.BOOKMARK-tab-image img {
+    width: 100%;
+    height: 100%;
+    padding: 8px;
+    object-fit: contain;
+}
+
+.BOOKMARK-tab-image i {
+    width: 100%;
+    height: 100%;
+    line-height: 44px;
+    font-size: 2.2em;
+}
+
+.BOOKMARK-tab-title {
+    flex-grow: 67;
+    padding: 0 5px;
+    color: white;
+    text-align: left;
+    font-weight: 500;
+}

+ 4 - 2
api/plugins/healthChecks.php

@@ -10,7 +10,9 @@ $GLOBALS['plugins'][]['healthChecks'] = array( // Plugin Name
 	'configPrefix' => 'HEALTHCHECKS', // config file prefix for array items without the hyphen
 	'version' => '1.0.0', // SemVer of plugin
 	'image' => 'plugins/images/healthchecksio.png', // 1:1 non transparent image for plugin
-	'settings' => true, // does plugin need a settings page? true or false
+	'settings' => true, // does plugin need a settings modal?
+	'bind' => false, // use default bind to make settings page - true or false
+	'api' => false, // api route for settings page
 	'homepage' => false // Is plugin for use on homepage? true or false
 );
 
@@ -139,4 +141,4 @@ class HealthChecks extends Organizr
 			$this->setAPIResponse('error', 'User does not have access', 401);
 		}
 	}
-}
+}

+ 4 - 2
api/plugins/invites.php

@@ -10,7 +10,9 @@ $GLOBALS['plugins'][]['Invites'] = array( // Plugin Name
 	'configPrefix' => 'INVITES', // config file prefix for array items without the hypen
 	'version' => '1.0.0', // SemVer of plugin
 	'image' => 'plugins/images/invites.png', // 1:1 non transparent image for plugin
-	'settings' => true, // does plugin need a settings page? true or false
+	'settings' => true, // does plugin need a settings modal?
+	'bind' => true, // use default bind to make settings page - true or false
+	'api' => 'api/v2/plugins/invites/settings', // api route for settings page
 	'homepage' => false // Is plugin for use on homepage? true or false
 );
 
@@ -470,4 +472,4 @@ class Invites extends Organizr
 		return (!empty($plexUser) ? $plexUser : null);
 	}
 	
-}
+}

+ 571 - 0
api/plugins/js/bookmark-settings.js

@@ -0,0 +1,571 @@
+/* BOOKMARK JS FILE */
+// FUNCTIONS
+bookmarkLaunch();
+$('body').arrive('#settings-main-tab-editor .nav-tabs', {onceOnly: true}, function() {
+	bookmarkLaunch();
+});
+function bookmarkCheckForTab() {
+	// Let check for tab with bookmark url
+	organizrAPI2('GET', 'api/v2/plugins/bookmark/setup/tab').success(function (data) {
+		try {
+			let response = data.response;
+			$('.bookmark-check-tab small').text('Bookmark Tab');
+			$('.bookmark-check-tab .result').text(response.message);
+		} catch (e) {
+			organizrCatchError(e, data);
+		}
+	}).fail(function (xhr) {
+		OrganizrApiError(xhr);
+		$('.bookmark-check-tab .result').text('Error...');
+	});
+}
+$('body').arrive('.bookmark-check-tab', {onceOnly: false}, function() {
+	setTimeout(function(){
+		bookmarkCheckForTab()
+		bookmarkCheckForCategory();
+	}, 500);
+
+});
+function bookmarkCheckForCategory(){
+	// Let check for tab with bookmark url
+	organizrAPI2('GET','api/v2/plugins/bookmark/setup/category').success(function(data) {
+		try {
+			let response = data.response;
+			$('.bookmark-check-category small').text('Bookmark Categories');
+			$('.bookmark-check-category .result').text(response.message);
+		}catch(e) {
+			organizrCatchError(e,data);
+		}
+	}).fail(function(xhr) {
+		OrganizrApiError(xhr);
+		$('.bookmark-check-category .result').text('Error...');
+	});
+}
+function bookmarkLaunch(){
+	if(activeInfo.plugins["BOOKMARK-enabled"] == true){
+		bookmarkTabsLaunch();
+		bookmarkCategoriesLaunch();
+		pageLoad();
+	}
+}
+
+// TAB MANAGEMENT
+function bookmarkTabsLaunch(){
+	var menuList = `<li onclick="changeSettingsMenu('Settings::Tab Editor::Bookmark Tabs');loadSettingsPage2('api/v2/plugins/bookmark/settings_tab_editor_bookmark_tabs','#settings-tab-editor-tabs','Tab Editor');" role="presentation"><a id="settings-tab-editor-tabs-anchor" href="#settings-tab-editor-tabs" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="true"><span class="visible-xs"><i class="ti-layout-tab-v"></i></span><span class="hidden-xs" lang="en">Bookmark Tabs</span></a></li>`;
+	$('#settings-main-tab-editor .nav-tabs').append(menuList);
+}
+
+function buildBookmarkTabEditor(){
+	organizrAPI2('GET','api/v2/plugins/bookmark/tabs').success(function(data) {
+		try {
+			var response = data.response;
+		}catch(e) {
+			organizrCatchError(e,data);
+		}
+		$('#bookmarkTabEditorTable').html(buildBookmarkTabEditorItem(response.data));
+	}).fail(function(xhr) {
+		OrganizrApiError(xhr);
+	});
+}
+
+function buildBookmarkTabEditorItem(array){
+	var tabList = '';
+	$.each(array.tabs, function(i,v) {
+		tabList += `
+		<tr class="bookmarkTabEditor" data-order="`+v.order+`" data-original-order="`+v.order+`" data-id="`+v.id+`" data-group-id="`+v.group_id+`" data-category-id="`+v.category_id+`" data-name="`+v.name+`" data-url="`+v.url+`" data-image="`+v.image+`">
+			<input type="hidden" class="form-control" name="tab[`+v.id+`].id" value="`+v.id+`">
+			<input type="hidden" class="form-control order" name="tab[`+v.id+`].order" value="`+v.order+`">
+			<input type="hidden" class="form-control" name="tab[`+v.id+`].originalOrder" value="`+v.order+`">
+			<td style="text-align:center" class="text-center el-element-overlay">
+				<div class="el-card-item p-0">
+					<div class="el-card-avatar el-overlay-1 m-0">
+						<div class="bookmarkTabEditorIcon">`+iconPrefix(v.image)+`</div>
+						<div class="el-overlay bg-org">
+							<ul class="el-info">
+								<i class="fa fa-bars"></i>
+							</ul>
+						</div>
+					</div>
+				</div>
+			</td>
+			<td><span class="tooltip-info" data-toggle="tooltip" data-placement="right" title="" data-original-title="`+v.url+`">`+v.name+`</span></td>
+            `+buildBookmarkTabCategorySelect(array.categories,v.id, v.category_id)+`
+			`+buildBookmarkTabGroupSelect(array.groups,v.id, v.group_id)+`
+			<td style="text-align:center"><input type="checkbox" class="js-switch bookmarkEnabledSwitch" data-size="small" data-color="#99d683" data-secondary-color="#f96262" name="tab[`+v.id+`].enabled" value="true" `+tof(v.enabled,'c')+`/><input type="hidden" class="form-control" name="tab[`+v.id+`].enabled" value="false"></td>
+			<td style="text-align:center"><button type="button" class="btn btn-info btn-outline btn-circle btn-lg m-r-5 editBookmarkTabButton popup-with-form" onclick="editBookmarkTabForm('`+v.id+`')" href="#edit-bookmark-tab-form" data-effect="mfp-3d-unfold"><i class="ti-pencil-alt"></i></button></td>
+			<td style="text-align:center"><button type="button" class="btn btn-danger btn-outline btn-circle btn-lg m-r-5 bookmarkDeleteTab"><i class="ti-trash"></i></button></td>
+		</tr>
+		`;
+	});
+	return tabList;
+}
+
+function buildBookmarkTabGroupSelect(array, tabID, groupID){
+	var groupSelect = '';
+	var selected = '';
+	$.each(array, function(i,v) {
+		selected = '';
+		if(v.group_id == groupID){
+			selected = 'selected';
+		}
+		groupSelect += '<option '+selected+' value="'+v.group_id+'">'+v.group+'</option>';
+	});
+	return '<td><select name="tab['+tabID+'].group_id" class="form-control bookmarkTabGroupSelect">'+groupSelect+'</select></td>';
+}
+
+function buildBookmarkTabCategorySelect(array,tabID, categoryID){
+	var categorySelect = '';
+	var selected = '';
+	$.each(array, function(i,v) {
+		selected = '';
+		if(v.category_id == categoryID){
+			selected = 'selected';
+		}
+		categorySelect += '<option '+selected+' value="'+v.category_id+'">'+v.category+'</option>';
+	});
+	return '<td><select name="tab['+tabID+'].category_id" class="form-control bookmarkTabCategorySelect">'+categorySelect+'</select></td>';
+}
+
+function editBookmarkTabForm(id){
+	organizrAPI2('GET','api/v2/plugins/bookmark/tabs/' + id,true).success(function(data) {
+		try {
+			let response = data.response;
+			console.log(response);
+			$('#edit-bookmark-tab-form [name=name]').val(response.data.name);
+			$('#originalBookmarkTabName').html(response.data.name);
+			$('#edit-bookmark-tab-form [name=url]').val(response.data.url);
+			$('#edit-bookmark-tab-form [name=image]').val(response.data.image);
+			$('#edit-bookmark-tab-form [name=background_color]').val(response.data.background_color);
+			$('#edit-bookmark-tab-form [name=text_color]').val(response.data.text_color);
+			$('#edit-bookmark-tab-form [name=id]').val(response.data.id);
+			if( response.data.url.indexOf('/?v') > 0){
+				$('#edit-bookmark-tab-form [name=url]').prop('disabled', 'true');
+			}else{
+				$('#edit-bookmark-tab-form [name=url]').prop('disabled', null);
+			}
+		}catch(e) {
+			organizrCatchError(e,data);
+		}
+	}).fail(function(xhr) {
+		OrganizrApiError(xhr, 'Tab Error');
+	});
+}
+
+// CHANGE ENABLED TAB
+$(document).on("change", ".bookmarkEnabledSwitch", function () {
+	var id = $(this).parent().parent().attr("data-id");
+	var enabled = $(this).prop("checked") ? 1 : 0;
+	var callbacks = $.Callbacks();
+	organizrAPI2('PUT','api/v2/plugins/bookmark/tabs/' + id, {"enabled":enabled},true).success(function(data) {
+		try {
+			var response = data.response;
+		}catch(e) {
+			organizrCatchError(e,data);
+		}
+		message('Tab Enable Updated',response.message,activeInfo.settings.notifications.position,"#FFF","success","5000");
+		if(callbacks){ callbacks.fire(); }
+	}).fail(function(xhr) {
+		OrganizrApiError(xhr, 'Tab Enable Error');
+	});
+});
+// CHANGE TAB GROUP
+$(document).on("change", ".bookmarkTabGroupSelect", function (event) {
+	var id = $(this).parent().parent().attr("data-id");
+	var groupID = $(this).find("option:selected").val();
+	var callbacks = $.Callbacks();
+	organizrAPI2('PUT','api/v2/plugins/bookmark/tabs/' + id, {"group_id":groupID},true).success(function(data) {
+		try {
+			var response = data.response;
+		}catch(e) {
+			organizrCatchError(e,data);
+		}
+		message('Tab Group Updated',response.message,activeInfo.settings.notifications.position,"#FFF","success","5000");
+		if(callbacks){ callbacks.fire(); }
+	}).fail(function(xhr) {
+		OrganizrApiError(xhr, 'Tab Group Error');
+	});
+});
+// CHANGE TAB CATEGORY
+$(document).on("change", ".bookmarkTabCategorySelect", function () {
+	var id = $(this).parent().parent().attr("data-id");
+	var categoryID = $(this).find("option:selected").val();
+	console.log("CategoryID: " + categoryID);
+	var callbacks = $.Callbacks();
+	organizrAPI2('PUT','api/v2/plugins/bookmark/tabs/' + id, {"category_id":categoryID},true).success(function(data) {
+		try {
+			var response = data.response;
+		}catch(e) {
+			organizrCatchError(e,data);
+		}
+		message('Tab Category Updated',response.message,activeInfo.settings.notifications.position,"#FFF","success","5000");
+		if(callbacks){ callbacks.fire(); }
+	}).fail(function(xhr) {
+		OrganizrApiError(xhr, 'Tab Category Error');
+	});
+});
+//DELETE TAB
+$(document).on("click", ".bookmarkDeleteTab", function () {
+	var tab = $(this);
+	swal({
+		title: window.lang.translate('Delete ') + tab.parent().parent().attr("data-name") + '?',
+		icon: "warning",
+		buttons: {
+			cancel: window.lang.translate('No'),
+			confirm: window.lang.translate('Yes'),
+		},
+		dangerMode: true,
+		confirmButtonColor: "#DD6B55"
+	}).then(function(willDelete) {
+		if (willDelete) {
+			var id = tab.parent().parent().attr("data-id");
+			var callbacks = $.Callbacks();
+			callbacks.add( buildBookmarkTabEditor );
+			organizrAPI2('DELETE','api/v2/plugins/bookmark/tabs/' + id, null,true).success(function(data) {
+				message('Tab Deleted','',activeInfo.settings.notifications.position,"#FFF","success","5000");
+				if(callbacks){ callbacks.fire(); }
+			}).fail(function(xhr) {
+				OrganizrApiError(xhr, 'Tab Deleted Error');
+			});
+		}
+	});
+});
+//EDIT TAB
+$(document).on("click", ".editBookmarkTab", function () {
+	var originalTabName = $('#originalBookmarkTabName').html();
+	var tabInfo = $('#edit-bookmark-tab-form').serializeToJSON();
+	if (typeof tabInfo.id == 'undefined' || tabInfo.id == '') {
+		message('Edit Tab Error',' Could not get Tab ID',activeInfo.settings.notifications.position,'#FFF','error','5000');
+		return false;
+	}
+	if (typeof tabInfo.name == 'undefined' || tabInfo.name == '') {
+		message('Edit Tab Error',' Please set a Tab Name',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+		return false;
+	}
+	if (typeof tabInfo.image == 'undefined' || tabInfo.image == '') {
+		message('Edit Tab Error',' Please set a Tab Image',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+		return false;
+	}
+	if (typeof tabInfo.url == 'undefined' || tabInfo.url == '') {
+		message('Edit Tab Error',' Please set a Tab URL',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+		return false;
+	}
+	if (typeof tabInfo.background_color == 'undefined' || tabInfo.background_color == '') {
+		message('Edit Tab Error',' Please set a Background Color',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+		return false;
+	}
+	if (typeof tabInfo.text_color == 'undefined' || tabInfo.text_color == '') {
+		message('Edit Tab Error',' Please set a Text Color',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+		return false;
+	}
+	if(tabInfo.id !== '' && tabInfo.tabName !== '' && tabInfo.tabImage !== '' && tabInfo.background_color !== '' && tabInfo.text_color !== ''){
+		var callbacks = $.Callbacks();
+		callbacks.add( buildBookmarkTabEditor );
+		organizrAPI2('PUT','api/v2/plugins/bookmark/tabs/' + tabInfo.id,tabInfo,true).success(function(data) {
+			try {
+				var response = data.response;
+				console.log(response);
+			}catch(e) {
+				organizrCatchError(e,data);
+			}
+			message('Tab Updated',response.message,activeInfo.settings.notifications.position,"#FFF","success","5000");
+			if(callbacks){ callbacks.fire(); }
+			clearForm('#edit-bookmark-tab-form');
+			$.magnificPopup.close();
+		}).fail(function(xhr) {
+			OrganizrApiError(xhr, 'Tab Error');
+		});
+	}
+});
+//ADD NEW TAB
+$(document).on("click", ".addNewBookmarkTab", function () {
+	var tabInfo = $('#new-bookmark-tab-form').serializeToJSON();
+	var order = parseInt($('#bookmarkTabEditorTable').find('tr[data-order]').last().attr('data-order')) + 1;
+	tabInfo['order'] = isNaN(order) ? 1 : order;
+
+	if (typeof tabInfo.name == 'undefined' || tabInfo.name == '') {
+		message('Edit Tab Error',' Please set a Tab Name',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+		return false;
+	}
+	if (typeof tabInfo.image == 'undefined' || tabInfo.image == '') {
+		message('Edit Tab Error',' Please set a Tab Image',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+		return false;
+	}
+	if ((typeof tabInfo.url == 'undefined' || tabInfo.url == '')) {
+		message('Edit Tab Error',' Please set a Tab URL',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+		return false;
+	}
+	if (typeof tabInfo.background_color == 'undefined' || tabInfo.background_color == '') {
+		message('Edit Tab Error',' Please set a Background Color',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+		return false;
+	}
+	if (typeof tabInfo.text_color == 'undefined' || tabInfo.text_color == '') {
+		message('Edit Tab Error',' Please set a Text Color',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+		return false;
+	}
+	if(tabInfo.order !== '' && tabInfo.name !== '' && tabInfo.url !== '' && tabInfo.image !== '' && tabInfo.background_color !== '' && tabInfo.text_color !== ''){
+		var callbacks = $.Callbacks();
+		callbacks.add( buildBookmarkTabEditor );
+		organizrAPI2('POST','api/v2/plugins/bookmark/tabs',tabInfo,true).success(function(data) {
+			try {
+				var response = data.response;
+				$('.bookmarkTabIconImageList').val(null).trigger('change');
+				$('.bookmarkTabIconIconList').val(null).trigger('change');
+			}catch(e) {
+				organizrCatchError(e,data);
+			}
+			message('Tab Created',response.message,activeInfo.settings.notifications.position,"#FFF","success","5000");
+			if(callbacks){ callbacks.fire(); }
+			clearForm('#new-bookmark-tab-form');
+			$.magnificPopup.close();
+		}).fail(function(xhr) {
+			OrganizrApiError(xhr, 'Tab Error');
+		});
+	}
+});
+// CHANGE TAB ORDER
+function submitBookmarkTabOrder(newTabs){
+	var data = [];
+	var process = false;
+	$.each(newTabs.tab, function(i,v) {
+		if(v.originalOrder == v.order){
+			delete newTabs.tab[i];
+		}else{
+			let temp = {
+				"order":v.order,
+				"id":v.id
+			}
+			data.push(temp);
+			process = true;
+		}
+	})
+	if(!process){
+		message('Tab Order Warning','Order was not changed - Submission not needed',activeInfo.settings.notifications.position,"#FFF","warning","5000");
+		$('.saveBookmarkTabOrderButton').addClass('hidden');
+		return false;
+	}
+	var callbacks = $.Callbacks();
+	callbacks.add( buildBookmarkTabEditor );
+	organizrAPI2('PUT','api/v2/plugins/bookmark/tabs',data,true).success(function(data) {
+		try {
+			var response = data.response;
+		}catch(e) {
+			organizrCatchError(e,data);
+		}
+		message('Tab Order Updated',response.message,activeInfo.settings.notifications.position,"#FFF","success","5000");
+		if(callbacks){ callbacks.fire(); }
+		$('.saveBookmarkTabOrderButton').addClass('hidden');
+	}).fail(function(xhr) {
+		OrganizrApiError(xhr, 'Update Error');
+	});
+}
+
+$(document).on('change', "#new-bookmark-tab-form-chooseImage", function (e) {
+	var newIcon = $('#new-bookmark-tab-form-chooseImage').val();
+	if(newIcon !== 'Select or type Icon'){
+		$('#new-bookmark-tab-form-inputImageNew').val(newIcon);
+	}
+});
+$(document).on('change', "#edit-bookmark-tab-form-chooseImage", function (e) {
+	var newIcon = $('#edit-bookmark-tab-form-chooseImage').val();
+	if(newIcon !== 'Select or type Icon'){
+		$('#edit-bookmark-tab-form-inputImage').val(newIcon);
+	}
+});
+$(document).on('change', "#new-bookmark-tab-form-chooseIcon", function (e) {
+	var newIcon = $('#new-bookmark-tab-form-chooseIcon').val();
+	if(newIcon !== 'Select or type Icon'){
+		$('#new-bookmark-tab-form-inputImageNew').val(newIcon);
+	}
+});
+$(document).on('change', "#edit-bookmark-tab-form-chooseIcon", function (e) {
+	var newIcon = $('#edit-bookmark-tab-form-chooseIcon').val();
+	if(newIcon !== 'Select or type Icon'){
+		$('#edit-bookmark-tab-form-inputImage').val(newIcon);
+	}
+});
+
+// CATEGORY MANAGEMENT
+function bookmarkCategoriesLaunch(){
+	var menuList = `<li onclick="changeSettingsMenu('Settings::Tab Editor::Bookmark Categories');loadSettingsPage2('api/v2/plugins/bookmark/settings_tab_editor_bookmark_categories','#settings-tab-editor-tabs','Tab Editor');" role="presentation"><a id="settings-tab-editor-tabs-anchor" href="#settings-tab-editor-tabs" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="true"><span class="visible-xs"><i class="ti-layout-tab-v"></i></span><span class="hidden-xs" lang="en">Bookmark Categories</span></a></li>`;
+	$('#settings-main-tab-editor .nav-tabs').append(menuList);
+}
+
+function buildBookmarkCategoryEditor(){
+	organizrAPI2('GET','api/v2/plugins/bookmark/tabs').success(function(data) {
+		try {
+			var response = data.response;
+		}catch(e) {
+			organizrCatchError(e,data);
+		}
+		$('#bookmarkCategoryEditorTable').html(buildBookmarkCategoryEditorItem(response.data));
+	}).fail(function(xhr) {
+		OrganizrApiError(xhr);
+	});
+}
+
+function buildBookmarkCategoryEditorItem(array){
+	var categoryList = '';
+	$.each(array.categories, function(i,v) {
+		var tabCount = array.tabs.reduce(function (n, category) {
+			return n + (category.category_id == v.category_id);
+		}, 0);
+		var disabledDefault = (v.default == 1) ? 'disabled' : '';
+		var disabledDelete = (tabCount > 0) ? 'disabled' : '';
+		var defaultIcon = (v.default == 1) ? 'icon-user-following' : 'icon-user-follow';
+		var defaultColor = (v.default == 1) ? 'btn-info disabled' : 'btn-warning';
+		categoryList += `
+		<tr class="bookmarkCategoryEditor" data-id="`+v.id+`" data-order="`+v.order+`" data-category-id="`+v.category_id+`" data-name="`+v.category+`" data-default="`+tof(v.default)+`" data-tab-count="`+tabCount+`">
+			<input type="hidden" class="form-control order" name="category[`+v.id+`].order" value="`+v.order+`">
+			<input type="hidden" class="form-control" name="category[`+v.id+`].originalOrder" value="`+v.order+`">
+			<input type="hidden" class="form-control" name="category[`+v.id+`].name" value="`+v.category+`">
+			<input type="hidden" class="form-control" name="category[`+v.id+`].id" value="`+v.id+`">
+			<td>`+v.category+`</td>
+			<td style="text-align:center">`+tabCount+`</td>
+			<td style="text-align:center"><button type="button" class="btn `+defaultColor+` btn-outline btn-circle btn-lg m-r-5 changeDefaultBookmarkCategory" `+disabledDefault+`><i class="`+defaultIcon+`"></i></button></td>
+			<td style="text-align:center"><button type="button" class="btn btn-info btn-outline btn-circle btn-lg m-r-5 editBookmarkCategoryButton popup-with-form" href="#edit-bookmark-category-form" data-effect="mfp-3d-unfold"><i class="ti-pencil-alt"></i></button></td>
+			<td style="text-align:center"><button type="button" class="btn btn-danger btn-outline btn-circle btn-lg m-r-5 deleteBookmarkCategory" `+disabledDelete+`><i class="ti-trash"></i></button></td>
+		</tr>
+		`;
+	});
+	return categoryList;
+}
+
+//ADD NEW CATEGORY
+$(document).on("click", ".addNewBookmarkCategory", function () {
+	var categoryInfo = $('#new-bookmark-category-form').serializeToJSON();
+	var order = parseInt($('#bookmarkCategoryEditorTable').find('tr[data-order]').last().attr('data-order')) + 1;
+	categoryInfo['order'] = isNaN(order) ? 1 : order;
+
+	if (typeof categoryInfo.category == 'undefined' || categoryInfo.category == '') {
+		message('Edit Tab Error',' Please set a Category Name',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+		return false;
+	}
+	if(categoryInfo.category !== ''){
+		var callbacks = $.Callbacks();
+		callbacks.add( buildBookmarkCategoryEditor );
+		organizrAPI2('POST','api/v2/plugins/bookmark/categories',categoryInfo,true).success(function(data) {
+			try {
+				var response = data.response;
+				console.log(response);
+			}catch(e) {
+				organizrCatchError(e,data);
+			}
+			message('Category Added',response.message,activeInfo.settings.notifications.position,"#FFF","success","5000");
+			if(callbacks){ callbacks.fire(); }
+			clearForm('#new-bookmark-category-form');
+			$.magnificPopup.close();
+		}).fail(function(xhr) {
+			OrganizrApiError(xhr, 'Category Error');
+		});
+	}
+});
+//DELETE CATEGORY
+$(document).on("click", ".deleteBookmarkCategory", function () {
+	var category = $(this);
+	swal({
+		title: window.lang.translate('Delete ')+category.parent().parent().attr("data-name")+'?',
+		icon: "warning",
+		buttons: {
+			cancel: window.lang.translate('No'),
+			confirm: window.lang.translate('Yes'),
+		},
+		dangerMode: true,
+		confirmButtonColor: "#DD6B55"
+	}).then(function(willDelete) {
+		if (willDelete) {
+			var id = category.parent().parent().attr("data-id");
+			var callbacks = $.Callbacks();
+			callbacks.add( buildBookmarkCategoryEditor );
+			organizrAPI2('DELETE','api/v2/plugins/bookmark/categories/' + id, null,true).success(function(data) {
+				message('Category Deleted','',activeInfo.settings.notifications.position,"#FFF","success","5000");
+				if(callbacks){ callbacks.fire(); }
+			}).fail(function(xhr) {
+				OrganizrApiError(xhr, 'Category Deleted Error');
+			});
+		}
+	});
+});
+//EDIT CATEGORY GET ID
+$(document).on("click", ".editBookmarkCategoryButton", function () {
+	$('#edit-bookmark-category-form [name=category]').val($(this).parent().parent().attr("data-name"));
+	$('#edit-bookmark-category-form [name=id]').val($(this).parent().parent().attr("data-id"));
+});
+//EDIT CATEGORY
+$(document).on("click", ".editBookmarkCategory", function () {
+	var categoryInfo = $('#edit-bookmark-category-form').serializeToJSON();
+	if (typeof categoryInfo.id == 'undefined' || categoryInfo.id == '') {
+		message('Edit Tab Error',' Could not get Category ID',activeInfo.settings.notifications.position,'#FFF','error','5000');
+		return false;
+	}
+	if (typeof categoryInfo.category == 'undefined' || categoryInfo.category == '') {
+		message('Edit Tab Error',' Please set a Category Name',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+		return false;
+	}
+	if(categoryInfo.id !== '' && categoryInfo.category !== ''){
+		var callbacks = $.Callbacks();
+		callbacks.add( buildBookmarkCategoryEditor );
+		organizrAPI2('PUT','api/v2/plugins/bookmark/categories/' + categoryInfo.id,categoryInfo,true).success(function(data) {
+			try {
+				var response = data.response;
+				console.log(response);
+			}catch(e) {
+				organizrCatchError(e,data);
+			}
+			message('Category Updated',response.message,activeInfo.settings.notifications.position,"#FFF","success","5000");
+			if(callbacks){ callbacks.fire(); }
+			clearForm('#edit-bookmark-category-form');
+			$.magnificPopup.close();
+		}).fail(function(xhr) {
+			OrganizrApiError(xhr, 'Category Error');
+		});
+	}
+});
+//CHANGE DEFAULT CATEGORY
+$(document).on("click", ".changeDefaultBookmarkCategory", function () {
+	var id = $(this).parent().parent().attr("data-id");
+	var callbacks = $.Callbacks();
+	callbacks.add( buildBookmarkCategoryEditor );
+	organizrAPI2('PUT','api/v2/plugins/bookmark/categories/' + id, {"default":1},true).success(function(data) {
+		try {
+			var response = data.response;
+		}catch(e) {
+			organizrCatchError(e,data);
+		}
+		message('Default Category Updated',response.message,activeInfo.settings.notifications.position,"#FFF","success","5000");
+		if(callbacks){ callbacks.fire(); }
+	}).fail(function(xhr) {
+		OrganizrApiError(xhr, 'Default Cateogry Error');
+	});
+});
+// CHANGE CATEGORY ORDER
+function submitBookmarkCategoryOrder(){
+	var data = [];
+	var categories = $( "#submit-bookmark-categories-form" ).serializeToJSON();
+	var callbacks = $.Callbacks();
+	callbacks.add( buildCategoryEditor );
+	$.each(categories.category, function(i,v) {
+		if(v.originalOrder == v.order){
+			delete categories.category[i];
+		}else{
+			let temp = {
+				"order":v.order,
+				"id":v.id
+			}
+			data.push(temp);
+		}
+	})
+	organizrAPI2('PUT','api/v2/plugins/bookmark/categories',data,true).success(function(data) {
+		try {
+			var response = data.response;
+		}catch(e) {
+			organizrCatchError(e,data);
+		}
+		message('Category Order Updated',response.message,activeInfo.settings.notifications.position,"#FFF","success","5000");
+		if(callbacks){ callbacks.fire(); }
+		$('.saveTabOrderButton').addClass('hidden');
+	}).fail(function(xhr) {
+		OrganizrApiError(xhr, 'Update Error');
+	});
+}
+
+// TAB MANAGEMENT

+ 168 - 182
api/plugins/js/chat.js

@@ -1,200 +1,186 @@
 // FUNCTIONS FOR CHAT
-chatLaunch()
+$('body').arrive('#activeInfo', {onceOnly: true}, function() {
+	chatLaunch();
+});
 function chatLaunch(){
-    if(typeof activeInfo == 'undefined'){
-        setTimeout(function () {
-            chatLaunch();
-        }, 1000);
-    }else{
-        if(activeInfo.plugins["CHAT-enabled"] == true && activeInfo.plugins.includes["CHAT-authKey-include"] !== '' && activeInfo.plugins.includes["CHAT-appID-include"] !== '' && activeInfo.plugins.includes["CHAT-cluster-include"] !== ''){
-            if (activeInfo.user.groupID <= activeInfo.plugins.includes["CHAT-Auth-include"]) {
-                var menuList = `<li><a class=""  href="javascript:void(0)" onclick="tabActions(event,'chat','plugin');chatEntry();"><i class="fa fa-comments-o fa-fw"></i> <span lang="en">Chat</span><small class="chat-counter label label-rouded label-info pull-right hidden">0</small></a></li>`;
-				var htmlDOM = `
-                <div id="container-plugin-chat" class="plugin-container hidden">
-                    <div class="chat-main-box bg-org">
-                        <!-- .chat-left-panel -->
-                        <div class="chat-left-aside">
-                            <div class="open-panel"><i class="ti-angle-right"></i></div>
-                            <div class="chat-left-inner bg-org"><ul class="chatonline style-none "></ul></div>
-                        </div>
-                        <!-- .chat-left-panel -->
-                        <!-- .chat-right-panel -->
-                        <div class="chat-right-aside">
-                            <div class="chat-box">
-                                <ul class="chat-list p-t-30"></ul>
-                                <div class="row send-chat-box">
-                                    <div class="col-sm-12">
-                                        <textarea class="form-control chat-input-send" placeholder="Type your message" lang="en"></textarea>
-                                        <div class="custom-send">
-                                            <button type="button" class="btn btn-info btn-lg custom-send-button"><i class="fa fa-paper-plane fa-2x"></i> </button>
-                                        </div>
-                                    </div>
-                                </div>
-                            </div>
-                        </div>
-                        <!-- .chat-right-panel -->
-                    </div>
-                </div>
-		    	`;
-				$('.append-menu').after(menuList);
-	            $('.plugin-listing').append(htmlDOM);
-	            pageLoad();
-                // Enable pusher logging - don't include this in production
-                //Pusher.logToConsole = true;
-                // Add API Key & cluster here to make the connection
-                var pusher = new Pusher(activeInfo.plugins.includes["CHAT-authKey-include"], {
-                    cluster: activeInfo.plugins.includes["CHAT-cluster-include"],
-                    encrypted: true
-                });
-                // Enter a unique channel you wish your users to be subscribed in.
-                var channel = pusher.subscribe('org_channel');
-                // bind the server event to get the response data and append it to the message div
-                channel.bind('my-event',
-                    function(data) {
-                        formatMessage(data);
-                        $('.chat-list').append(formatMessage(data));
-                        $('.custom-send').html('<button type="button" class="btn btn-info btn-lg custom-send-button"><i class="fa fa-paper-plane fa-2x"></i> </button>');
-                        $(".chat-list").scrollTop($(".chat-list")[0].scrollHeight);
-                        if($('#container-plugin-chat').hasClass('hidden')){
-                            var chatSound =  new Audio(activeInfo.plugins.includes["CHAT-newMessageSound-include"]);
-                            chatSound.play();
-                            message(data.username,data.message,activeInfo.settings.notifications.position,"#FFF","success","20000");
-                            $('.profile-image').addClass('animated loop-animation rubberBand');
-                            $('.chat-counter').removeClass('hidden').html(parseInt($('.chat-counter').text()) + 1);
-                        }
-                    });
-                // check if the user is subscribed to the above channel
-                channel.bind('pusher:subscription_succeeded', function(members) {
-	                organizrConsole('Plugin Function','Chat Websocket Connected!');
-	                organizrConsole('Plugin Function','Connecting to Organizr Chat DB');
-                    getMessagesAndUsers(activeInfo.settings.homepage.refresh["CHAT-userRefreshTimeout"], true);
-                });
-                /*jslint browser: true*/
-                /*global $, jQuery, alert*/
-                $(document).ready(function () {
-                    "use strict";
-                    $('.chat-left-inner > .chatonline').slimScroll({
-                        height: '100%',
-                        position: 'right',
-                        size: "0px",
-                        color: '#dcdcdc'
-
-                    });
-                    $('.chat-list').slimScroll({
-                        height: '100%',
-                        position: 'right',
-                        size: "0px",
-                        color: '#dcdcdc',
-                        start: 'bottom',
-                    });
-                    $(".open-panel").on("click", function () {
-                        $(".chat-left-aside").toggleClass("open-pnl");
-                        $(".open-panel i").toggleClass("ti-angle-left");
-                    });
-                });
-			}
-        }
-    }
+	if(activeInfo.plugins["CHAT-enabled"] == true && activeInfo.plugins.includes["CHAT-authKey-include"] !== '' && activeInfo.plugins.includes["CHAT-appID-include"] !== '' && activeInfo.plugins.includes["CHAT-cluster-include"] !== ''){
+		if (activeInfo.user.groupID <= activeInfo.plugins.includes["CHAT-Auth-include"]) {
+			var menuList = `<li><a class=""  href="javascript:void(0)" onclick="tabActions(event,'chat','plugin');chatEntry();"><i class="fa fa-comments-o fa-fw"></i> <span lang="en">Chat</span><small class="chat-counter label label-rouded label-info pull-right hidden">0</small></a></li>`;
+			var htmlDOM = `
+			<div id="container-plugin-chat" class="plugin-container hidden">
+				<div class="chat-main-box bg-org">
+					<!-- .chat-left-panel -->
+					<div class="chat-left-aside">
+						<div class="open-panel"><i class="ti-angle-right"></i></div>
+						<div class="chat-left-inner bg-org"><ul class="chatonline style-none "></ul></div>
+					</div>
+					<!-- .chat-left-panel -->
+					<!-- .chat-right-panel -->
+					<div class="chat-right-aside">
+						<div class="chat-box">
+							<ul class="chat-list p-t-30"></ul>
+							<div class="row send-chat-box">
+								<div class="col-sm-12">
+									<textarea class="form-control chat-input-send" placeholder="Type your message" lang="en"></textarea>
+									<div class="custom-send">
+										<button type="button" class="btn btn-info btn-lg custom-send-button"><i class="fa fa-paper-plane fa-2x"></i> </button>
+									</div>
+								</div>
+							</div>
+						</div>
+					</div>
+					<!-- .chat-right-panel -->
+				</div>
+			</div>
+			`;
+			$('.append-menu').after(menuList);
+			$('.plugin-listing').append(htmlDOM);
+			pageLoad();
+			// Enable pusher logging - don't include this in production
+			//Pusher.logToConsole = true;
+			// Add API Key & cluster here to make the connection
+			var pusher = new Pusher(activeInfo.plugins.includes["CHAT-authKey-include"], {
+				cluster: activeInfo.plugins.includes["CHAT-cluster-include"],
+				encrypted: true
+			});
+			// Enter a unique channel you wish your users to be subscribed in.
+			var channel = pusher.subscribe('org_channel');
+			// bind the server event to get the response data and append it to the message div
+			channel.bind('my-event',
+				function(data) {
+					formatMessage(data);
+					$('.chat-list').append(formatMessage(data));
+					$('.custom-send').html('<button type="button" class="btn btn-info btn-lg custom-send-button"><i class="fa fa-paper-plane fa-2x"></i> </button>');
+					$(".chat-list").scrollTop($(".chat-list")[0].scrollHeight);
+					if($('#container-plugin-chat').hasClass('hidden')){
+						var chatSound =  new Audio(activeInfo.plugins.includes["CHAT-newMessageSound-include"]);
+						chatSound.play();
+						message(data.username,data.message,activeInfo.settings.notifications.position,"#FFF","success","20000");
+						$('.profile-image').addClass('animated loop-animation rubberBand');
+						$('.chat-counter').removeClass('hidden').html(parseInt($('.chat-counter').text()) + 1);
+					}
+				});
+			// check if the user is subscribed to the above channel
+			channel.bind('pusher:subscription_succeeded', function(members) {
+				organizrConsole('Plugin Function','Chat Websocket Connected!');
+				organizrConsole('Plugin Function','Connecting to Organizr Chat DB');
+				getMessagesAndUsers(activeInfo.settings.homepage.refresh["CHAT-userRefreshTimeout"], true);
+			});
+			/*jslint browser: true*/
+			/*global $, jQuery, alert*/
+			$(document).ready(function () {
+				"use strict";
+				$('.chat-left-inner > .chatonline').slimScroll({
+					height: '100%',
+					position: 'right',
+					size: "0px",
+					color: '#dcdcdc'
+				});
+				$('.chat-list').slimScroll({
+					height: '100%',
+					position: 'right',
+					size: "0px",
+					color: '#dcdcdc',
+					start: 'bottom',
+				});
+				$(".open-panel").on("click", function () {
+					$(".chat-left-aside").toggleClass("open-pnl");
+					$(".open-panel i").toggleClass("ti-angle-left");
+				});
+			});
+		}
+	}
 }
-$(document).on('click', '#CHAT-settings-button', function() {
-    ajaxloader(".content-wrap","in");
-	organizrAPI2('GET','api/v2/plugins/chat/settings').success(function(data) {
-        var response = data.response;
-        $('#CHAT-settings-items').html(buildFormGroup(response.data));
-    }).fail(function(xhr) {
-        console.error("Organizr Function: API Connection Failed");
-    });
-    ajaxloader();
-});
+
 //Chat functions!
 $(document).on('keypress', '.chat-input-send', function(ev) {
-    var keycode = (ev.keyCode ? ev.keyCode : ev.which);
-    if (keycode == '13') {
-        ev.preventDefault();
-        $('.custom-send-button').click();
-    }
+	var keycode = (ev.keyCode ? ev.keyCode : ev.which);
+	if (keycode == '13') {
+		ev.preventDefault();
+		$('.custom-send-button').click();
+	}
 });
 // Send the Message enter by User
 $('body').on('click', '.custom-send-button', function(e) {
-    e.preventDefault();
-    var message = $('.chat-input-send').val();
-    // Validate Name field
-    if (message !== '') {
-        organizrAPI2('POST','api/v2/plugins/chat/message',{ message : message }).success(function(data) {
-            // Nada yet
-        }).fail(function(xhr) {
-            console.error("Organizr Function: API Connection Failed");
-        });
-        // Clear the message input field
-        $('.chat-input-send').val('');
-        // Show a loading image while sending
-        $('.custom-send').html('<button type="button" class="btn btn-info btn-lg custom-send-button" disabled><i class="fa fa-spinner fa-pulse fa-2x"></i> </button>');
-    }
+	e.preventDefault();
+	var message = $('.chat-input-send').val();
+	// Validate Name field
+	if (message !== '') {
+		organizrAPI2('POST','api/v2/plugins/chat/message',{ message : message }).success(function(data) {
+			// Nada yet
+		}).fail(function(xhr) {
+			console.error("Organizr Function: API Connection Failed");
+		});
+		// Clear the message input field
+		$('.chat-input-send').val('');
+		// Show a loading image while sending
+		$('.custom-send').html('<button type="button" class="btn btn-info btn-lg custom-send-button" disabled><i class="fa fa-spinner fa-pulse fa-2x"></i> </button>');
+	}
 });
 function formatMessage(msg){
-    var className = 'odd';
-    if(msg.username == activeInfo.user.username){
-        if(activeInfo.user.username == 'Guest' && activeInfo.user.uid !== msg.uid){
-            className = '';
-        }
-    }else{
-        className = '';
-    }
-    return `
-        <li class="`+className+`">
-            <div class="chat-image"> <img alt="male" src="`+msg.gravatar+`"> </div>
-            <div class="chat-body">
-                <div class="chat-text">
-                    <h4>`+msg.username+`</h4>
-                    <p> `+msg.message+` </p> <b>`+moment.utc(msg.date, "YYYY-MM-DD hh:mm").local().format('LLL')+`</b> </div>
-            </div>
-        </li>
-    `;
+	var className = 'odd';
+	if(msg.username == activeInfo.user.username){
+		if(activeInfo.user.username == 'Guest' && activeInfo.user.uid !== msg.uid){
+			className = '';
+		}
+	}else{
+		className = '';
+	}
+	return `
+		<li class="`+className+`">
+			<div class="chat-image"> <img alt="male" src="`+msg.gravatar+`"> </div>
+			<div class="chat-body">
+				<div class="chat-text">
+					<h4>`+msg.username+`</h4>
+					<p> `+msg.message+` </p> <b>`+moment.utc(msg.date, "YYYY-MM-DD hh:mm").local().format('LLL')+`</b> </div>
+			</div>
+		</li>
+	`;
 }
 function formatUsers(array){
-    var users = {};
-    var userList = '';
-    array.reverse();
-    $.each(array, function (i, v){
-        if(!users.hasOwnProperty(v.username)){
-            users[v.username] = {
-                'last':v.date,
-                'gravatar':v.gravatar
-            }
-        }
-    });
-    $.each(users, function (i, v) {
-        userList += `
-            <li>
-                <a href="javascript:void(0)"><img src="`+v.gravatar+`" alt="user-img" class="img-circle"> <span>`+i+`<small class="text-success">`+moment.utc(v.last, "YYYY-MM-DD hh:mm[Z]").local().fromNow()+`</small></span></a>
-            </li>
-        `;
-    });
-    userList += '<li class="p-20"></li>';
-    return userList;
+	var users = {};
+	var userList = '';
+	array.reverse();
+	$.each(array, function (i, v){
+		if(!users.hasOwnProperty(v.username)){
+			users[v.username] = {
+				'last':v.date,
+				'gravatar':v.gravatar
+			}
+		}
+	});
+	$.each(users, function (i, v) {
+		userList += `
+			<li>
+				<a href="javascript:void(0)"><img src="`+v.gravatar+`" alt="user-img" class="img-circle"> <span>`+i+`<small class="text-success">`+moment.utc(v.last, "YYYY-MM-DD hh:mm[Z]").local().fromNow()+`</small></span></a>
+			</li>
+		`;
+	});
+	userList += '<li class="p-20"></li>';
+	return userList;
 }
 function chatEntry(){
-    $(".chat-list").scrollTop($(".chat-list")[0].scrollHeight);
-    $('.chat-input-send').focus();
-    $('.chat-counter').addClass('hidden').html('0');
+	$(".chat-list").scrollTop($(".chat-list")[0].scrollHeight);
+	$('.chat-input-send').focus();
+	$('.chat-counter').addClass('hidden').html('0');
 }
 function getMessagesAndUsers(timeout, initial = false){
-    var timeout = (typeof timeout !== 'undefined') ? timeout : activeInfo.settings.homepage.refresh["CHAT-userRefreshTimeout"];
-    organizrAPI2('GET','api/v2/plugins/chat/message').success(function(data) {
-        var response = data.response;
-        if(initial == true){
-            $.each(response.data, function (i, v){
-                $('.chat-list').append(formatMessage(v));
-            });
-        }
-        $('.chatonline').html(formatUsers(response.data));
-    }).fail(function(xhr) {
-        console.error("Organizr Function: API Connection Failed");
-    });
-    var timeoutTitle = 'ChatUserList';
-    if(typeof timeouts[timeoutTitle] !== 'undefined'){ clearTimeout(timeouts[timeoutTitle]); }
-    timeouts[timeoutTitle] = setTimeout(function(){ getMessagesAndUsers(timeout, false); }, timeout);
+	var timeout = (typeof timeout !== 'undefined') ? timeout : activeInfo.settings.homepage.refresh["CHAT-userRefreshTimeout"];
+	organizrAPI2('GET','api/v2/plugins/chat/message').success(function(data) {
+		var response = data.response;
+		if(initial == true){
+			$.each(response.data, function (i, v){
+				$('.chat-list').append(formatMessage(v));
+			});
+		}
+		$('.chatonline').html(formatUsers(response.data));
+	}).fail(function(xhr) {
+		console.error("Organizr Function: API Connection Failed");
+	});
+	var timeoutTitle = 'ChatUserList';
+	if(typeof timeouts[timeoutTitle] !== 'undefined'){ clearTimeout(timeouts[timeoutTitle]); }
+	timeouts[timeoutTitle] = setTimeout(function(){ getMessagesAndUsers(timeout, false); }, timeout);
 }
 $(document).on('click', '.profile-pic', function(e) {
-    $('.profile-image').removeClass('animated loop-animation rubberBand');
-});
+	$('.profile-image').removeClass('animated loop-animation rubberBand');
+});

+ 110 - 0
api/plugins/js/healthChecks-settings.js

@@ -0,0 +1,110 @@
+/* HEALTHCHECKS.IO JS FILE */
+
+// FUNCTIONS
+
+// EVENTS and LISTENERS
+
+// CHANGE CUSTOMIZE Options
+//
+$(document).on('click', '#HEALTHCHECKS-settings-button', function() {
+    ajaxloader(".content-wrap","in");
+    organizrAPI2('GET','api/v2/plugins/healthchecks/settings').success(function(data) {
+        var response = data.response;
+        $('#HEALTHCHECKS-settings-items').html(buildFormGroup(response.data));
+        var elAddButtonStart = $('#HEALTHCHECKS-settings-page [id*="Services"] .row.start');
+        var testone = $('#HEALTHCHECKS-settings-page [id*="Services"] .row.m-b-40').first('span')
+        var testtwo = $('#HEALTHCHECKS-settings-page [id*="Services"] .row.m-b-40 span')
+        $(elAddButtonStart).after('<div class="row"><button type="button" class="btn btn-info pull-right m-r-20 addNewHCService" ><i class="fa fa-plus"></i> Add New Service</button></div>');
+        $.each(testtwo, function(key,val) {
+            var el = $(val);
+            var text = el.text();
+            if(text === 'Service Name'){
+                $(this).after('&nbsp;<div class="pull-right text-danger removeHCService mouse"><i class="fa fa-close text-danger"></i></div>');
+            }
+        })
+
+    }).fail(function(xhr) {
+        console.error("Organizr Function: API Connection Failed");
+    });
+    ajaxloader();
+});
+$(document).on('click', '.addNewHCService', function() {
+    var lastEl = $('#HEALTHCHECKS-settings-page [name*="HEALTHCHECKS-all-items"]').last().attr('name');
+    var newNum = 0;
+    if(typeof lastEl !== 'undefined'){
+        lastEl = Number($('#HEALTHCHECKS-settings-page [name*="HEALTHCHECKS-all-items"]').last().attr('name').replace(/\D/g, ''));
+        newNum = lastEl + 1;
+    }
+    var copyEl = '' +
+        '<div class="row m-b-40">\n' +
+        '\t<!-- INPUT BOX  Yes Multiple -->\n' +
+        '\t<div class="col-md-6 p-b-10">\n' +
+        '\t\t<div class="form-group">\n' +
+        '\t\t\t<label class="control-label col-md-12"><span lang="en">Service Name</span>&nbsp;<div class="pull-right text-danger removeHCService mouse"><i class="fa fa-close text-danger"></i></div></label>\n' +
+        '\t\t\t<div class="col-md-12"> <input data-changed="false" lang="en" type="text" class="form-control" value="" name="HEALTHCHECKS-all-items[999999].name" data-type="input" data-label="Service Name" autocomplete="new-password"> </div> <!-- end div -->\n' +
+        '\t\t</div>\n' +
+        '\t</div>\n' +
+        '\t<!--/ INPUT BOX -->\n' +
+        '\n' +
+        '\t<!-- INPUT BOX  Yes Multiple -->\n' +
+        '\t<div class="col-md-6 p-b-10">\n' +
+        '\t\t<div class="form-group">\n' +
+        '\t\t\t<label class="control-label col-md-12"><span lang="en">UUID</span></label>\n' +
+        '\t\t\t<div class="col-md-12"> <input data-changed="false" lang="en" type="text" class="form-control" value="" name="HEALTHCHECKS-all-items[999999].uuid" data-type="input" data-label="UUID" autocomplete="new-password"> </div> <!-- end div -->\n' +
+        '\t\t</div>\n' +
+        '\t</div>\n' +
+        '\t<!--/ INPUT BOX -->\n' +
+        '\n' +
+        '\t<!-- INPUT BOX  Yes Multiple -->\n' +
+        '\t<div class="col-md-6 p-b-10">\n' +
+        '\t\t<div class="form-group">\n' +
+        '\t\t\t<label class="control-label col-md-12"><span lang="en">External URL</span></label>\n' +
+        '\t\t\t<div class="col-md-12"> <input data-changed="false" lang="en" type="text" class="form-control" value="" name="HEALTHCHECKS-all-items[999999].external" data-type="input" data-label="External URL" autocomplete="new-password"> </div> <!-- end div -->\n' +
+        '\t\t</div>\n' +
+        '\t</div>\n' +
+        '\t<!--/ INPUT BOX -->\n' +
+        '\n' +
+        '\t<!-- INPUT BOX  Yes Multiple -->\n' +
+        '\t<div class="col-md-6 p-b-10">\n' +
+        '\t\t<div class="form-group">\n' +
+        '\t\t\t<label class="control-label col-md-12"><span lang="en">Internal URL</span></label>\n' +
+        '\t\t\t<div class="col-md-12"> <input data-changed="false" lang="en" type="text" class="form-control" value="" name="HEALTHCHECKS-all-items[999999].internal" data-type="input" data-label="Internal URL" autocomplete="new-password"> </div> <!-- end div -->\n' +
+        '\t\t</div>\n' +
+        '\t</div>\n' +
+        '\t<!--/ INPUT BOX -->\n' +
+        '\n' +
+        '\t<!-- INPUT BOX  Yes Multiple -->\n' +
+        '\t<div class="col-md-6 p-b-10">\n' +
+        '\t\t<div class="form-group">\n' +
+        '\t\t\t<label class="control-label col-md-12"><span lang="en">Enabled</span></label>\n' +
+        '\t\t\t<div class="col-md-12"> <input data-changed="false" type="checkbox" class="js-switch" data-size="small" data-color="#99d683" data-secondary-color="#f96262" name="HEALTHCHECKS-all-items[999999].enabled" value="" checked="" data-type="switch" data-label="Enabled"><input data-changed="false" type="hidden" name="HEALTHCHECKS-all-items[999999].enabled" value=""> </div> <!-- end div -->\n' +
+        '\t\t</div>\n' +
+        '\t</div>\n' +
+        '\t<!--/ INPUT BOX -->\n' +
+        '</div>'
+//smallLabel+'<input data-changed="false" type="checkbox" class="js-switch'+extraClass+'" data-size="small" data-color="#99d683" data-secondary-color="#f96262"'+name+value+tof(item.value,'c')+id+disabled+type+label+attr+' /><input data-changed="false" type="hidden"'+name+'value="false">';
+    var elAddButtonStart = $('#HEALTHCHECKS-settings-page [id*="Services"] .row.start');
+    var copiedEl = $(copyEl).clone();
+    copiedEl.find("input").each(function() {
+        var currentName = $(this).attr("name");
+        var newName = currentName.replace('999999', newNum);
+        $(this).attr("name", newName);
+        $(this).attr("value", "");
+    });
+    $(copiedEl).appendTo(elAddButtonStart);
+    $(function () {
+        // Switchery
+        var elems = Array.prototype.slice.call(document.querySelectorAll('.js-switch'));
+        $('.js-switch').each(function() {
+            if ($(this).attr('data-switchery') !== 'true'){
+                new Switchery($(this)[0], $(this).data());
+            }
+        });
+    });
+
+});
+
+$(document).on('click', '.removeHCService', function() {
+    $(this).closest('.row').remove();
+    $('#HEALTHCHECKS-settings-page-save').removeClass('hidden');
+});

+ 1 - 110
api/plugins/js/healthChecks.js

@@ -1,110 +1 @@
-/* HEALTHCHECKS.IO JS FILE */
-
-// FUNCTIONS
-
-// EVENTS and LISTENERS
-
-// CHANGE CUSTOMIZE Options
-//
-$(document).on('click', '#HEALTHCHECKS-settings-button', function() {
-    ajaxloader(".content-wrap","in");
-    organizrAPI2('GET','api/v2/plugins/healthchecks/settings').success(function(data) {
-        var response = data.response;
-        $('#HEALTHCHECKS-settings-items').html(buildFormGroup(response.data));
-        var elAddButtonStart = $('#HEALTHCHECKS-settings-page [id*="Services"] .row.start');
-        var testone = $('#HEALTHCHECKS-settings-page [id*="Services"] .row.m-b-40').first('span')
-        var testtwo = $('#HEALTHCHECKS-settings-page [id*="Services"] .row.m-b-40 span')
-        $(elAddButtonStart).after('<div class="row"><button type="button" class="btn btn-info pull-right m-r-20 addNewHCService" ><i class="fa fa-plus"></i> Add New Service</button></div>');
-        $.each(testtwo, function(key,val) {
-            var el = $(val);
-            var text = el.text();
-            if(text === 'Service Name'){
-                $(this).after('&nbsp;<div class="pull-right text-danger removeHCService mouse"><i class="fa fa-close text-danger"></i></div>');
-            }
-        })
-
-    }).fail(function(xhr) {
-        console.error("Organizr Function: API Connection Failed");
-    });
-    ajaxloader();
-});
-$(document).on('click', '.addNewHCService', function() {
-    var lastEl = $('#HEALTHCHECKS-settings-page [name*="HEALTHCHECKS-all-items"]').last().attr('name');
-    var newNum = 0;
-    if(typeof lastEl !== 'undefined'){
-        lastEl = Number($('#HEALTHCHECKS-settings-page [name*="HEALTHCHECKS-all-items"]').last().attr('name').replace(/\D/g, ''));
-        newNum = lastEl + 1;
-    }
-    var copyEl = '' +
-        '<div class="row m-b-40">\n' +
-        '\t<!-- INPUT BOX  Yes Multiple -->\n' +
-        '\t<div class="col-md-6 p-b-10">\n' +
-        '\t\t<div class="form-group">\n' +
-        '\t\t\t<label class="control-label col-md-12"><span lang="en">Service Name</span>&nbsp;<div class="pull-right text-danger removeHCService mouse"><i class="fa fa-close text-danger"></i></div></label>\n' +
-        '\t\t\t<div class="col-md-12"> <input data-changed="false" lang="en" type="text" class="form-control" value="" name="HEALTHCHECKS-all-items[999999].name" data-type="input" data-label="Service Name" autocomplete="new-password"> </div> <!-- end div -->\n' +
-        '\t\t</div>\n' +
-        '\t</div>\n' +
-        '\t<!--/ INPUT BOX -->\n' +
-        '\n' +
-        '\t<!-- INPUT BOX  Yes Multiple -->\n' +
-        '\t<div class="col-md-6 p-b-10">\n' +
-        '\t\t<div class="form-group">\n' +
-        '\t\t\t<label class="control-label col-md-12"><span lang="en">UUID</span></label>\n' +
-        '\t\t\t<div class="col-md-12"> <input data-changed="false" lang="en" type="text" class="form-control" value="" name="HEALTHCHECKS-all-items[999999].uuid" data-type="input" data-label="UUID" autocomplete="new-password"> </div> <!-- end div -->\n' +
-        '\t\t</div>\n' +
-        '\t</div>\n' +
-        '\t<!--/ INPUT BOX -->\n' +
-        '\n' +
-        '\t<!-- INPUT BOX  Yes Multiple -->\n' +
-        '\t<div class="col-md-6 p-b-10">\n' +
-        '\t\t<div class="form-group">\n' +
-        '\t\t\t<label class="control-label col-md-12"><span lang="en">External URL</span></label>\n' +
-        '\t\t\t<div class="col-md-12"> <input data-changed="false" lang="en" type="text" class="form-control" value="" name="HEALTHCHECKS-all-items[999999].external" data-type="input" data-label="External URL" autocomplete="new-password"> </div> <!-- end div -->\n' +
-        '\t\t</div>\n' +
-        '\t</div>\n' +
-        '\t<!--/ INPUT BOX -->\n' +
-        '\n' +
-        '\t<!-- INPUT BOX  Yes Multiple -->\n' +
-        '\t<div class="col-md-6 p-b-10">\n' +
-        '\t\t<div class="form-group">\n' +
-        '\t\t\t<label class="control-label col-md-12"><span lang="en">Internal URL</span></label>\n' +
-        '\t\t\t<div class="col-md-12"> <input data-changed="false" lang="en" type="text" class="form-control" value="" name="HEALTHCHECKS-all-items[999999].internal" data-type="input" data-label="Internal URL" autocomplete="new-password"> </div> <!-- end div -->\n' +
-        '\t\t</div>\n' +
-        '\t</div>\n' +
-        '\t<!--/ INPUT BOX -->\n' +
-        '\n' +
-        '\t<!-- INPUT BOX  Yes Multiple -->\n' +
-        '\t<div class="col-md-6 p-b-10">\n' +
-        '\t\t<div class="form-group">\n' +
-        '\t\t\t<label class="control-label col-md-12"><span lang="en">Enabled</span></label>\n' +
-        '\t\t\t<div class="col-md-12"> <input data-changed="false" type="checkbox" class="js-switch" data-size="small" data-color="#99d683" data-secondary-color="#f96262" name="HEALTHCHECKS-all-items[999999].enabled" value="" checked="" data-type="switch" data-label="Enabled"><input data-changed="false" type="hidden" name="HEALTHCHECKS-all-items[999999].enabled" value=""> </div> <!-- end div -->\n' +
-        '\t\t</div>\n' +
-        '\t</div>\n' +
-        '\t<!--/ INPUT BOX -->\n' +
-        '</div>'
-//smallLabel+'<input data-changed="false" type="checkbox" class="js-switch'+extraClass+'" data-size="small" data-color="#99d683" data-secondary-color="#f96262"'+name+value+tof(item.value,'c')+id+disabled+type+label+attr+' /><input data-changed="false" type="hidden"'+name+'value="false">';
-    var elAddButtonStart = $('#HEALTHCHECKS-settings-page [id*="Services"] .row.start');
-    var copiedEl = $(copyEl).clone();
-    copiedEl.find("input").each(function() {
-        var currentName = $(this).attr("name");
-        var newName = currentName.replace('999999', newNum);
-        $(this).attr("name", newName);
-        $(this).attr("value", "");
-    });
-    $(copiedEl).appendTo(elAddButtonStart);
-    $(function () {
-        // Switchery
-        var elems = Array.prototype.slice.call(document.querySelectorAll('.js-switch'));
-        $('.js-switch').each(function() {
-            if ($(this).attr('data-switchery') !== 'true'){
-                new Switchery($(this)[0], $(this).data());
-            }
-        });
-    });
-
-});
-
-$(document).on('click', '.removeHCService', function() {
-    $(this).closest('.row').remove();
-    $('#HEALTHCHECKS-settings-page-save').removeClass('hidden');
-});
+/* HEALTHCHECKS.IO JS FILE IS NO LONGER NEEDED OR USED */

+ 397 - 421
api/plugins/js/invites.js

@@ -1,231 +1,223 @@
 /* INVITES JS FILE */
+$('body').arrive('#activeInfo', {onceOnly: true}, function() {
+	inviteLaunch();
+});
 // FUNCTIONS
-inviteLaunch()
 function inviteLaunch(){
-    if(typeof activeInfo == 'undefined'){
-        setTimeout(function () {
-            inviteLaunch();
-        }, 1000);
-    }else{
-        var menuList = '';
-    	var htmlDOM = `
-    	<div id="invite-area" class="white-popup mfp-with-anim mfp-hide">
-    		<div class="col-md-10 col-md-offset-1">
-    			<div class="invite-div"></div>
-    		</div>
-    	</div>
-    	`;
-        if(activeInfo.plugins["INVITES-enabled"] == true){
-            if (activeInfo.user.loggedin === true && activeInfo.user.groupID <= 1) {
-                menuList = `<li><a class="inline-popups inviteModal" href="#invite-area" data-effect="mfp-zoom-out"><i class="fa fa-ticket fa-fw"></i> <span lang="en">Manage Invites</span></a></li>`;
-                htmlDOM += `
-            	<div id="new-invite-area" class="white-popup mfp-with-anim mfp-hide">
-            		<div class="col-md-10 col-md-offset-1">
-                        <div class="col-md-12">
-                            <div class="panel panel-info m-b-0">
-                                <div class="panel-heading" lang="en">New Invite</div>
-                                <div class="panel-wrapper collapse in" aria-expanded="true">
-                                    <div class="panel-body">
-
-                                        <form id="new-invite-form">
-                                            <fieldset style="border:0;">
-                                            <div class="form-group">
-                                                <label class="control-label" for="new-invite-form-inputUsername" lang="en">Name or Username</label>
-                                                <input type="text" class="form-control" id="new-invite-form-inputUsername" name="username" required="" autofocus="">
-                                            </div>
-                                            <div class="form-group">
-                                                <label class="control-label" for="new-invite-form-inputEmail" lang="en">Email</label>
-                                                <input type="text" class="form-control" id="new-invite-form-inputEmail" name="email" required="" autofocus="">
-                                            </div>
-                                            </fieldset>
-                                            <button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none" onclick="createNewInvite();" type="button"><span class="btn-label"><i class="fa fa-plus"></i></span><span lang="en">Create/Send Invite</span></button>
-                                            <div class="clearfix"></div>
-                                        </form>
-
-
-
-                                    </div>
-                                </div>
-                            </div>
-                        </div>
-                        <div class="clearfix"></div>
-            		</div>
-            	</div>`;
-            }else if (activeInfo.user.loggedin === false){
-                menuList = `<li><a class="inline-popups inviteModal" href="#invite-area" data-effect="mfp-zoom-out"><i class="fa fa-ticket fa-fw"></i> <span lang="en">Use Invite Code</span></a></li>`;
-            }
-            $('.append-menu').after(menuList);
-            $('.organizr-area').after(htmlDOM);
-            pageLoad();
-            getInvite();
-        }
-    }
+	var menuList = '';
+	var htmlDOM = `
+	<div id="invite-area" class="white-popup mfp-with-anim mfp-hide">
+		<div class="col-md-10 col-md-offset-1">
+			<div class="invite-div"></div>
+		</div>
+	</div>
+	`;
+	if(activeInfo.plugins["INVITES-enabled"] == true){
+		if (activeInfo.user.loggedin === true && activeInfo.user.groupID <= 1) {
+			menuList = `<li><a class="inline-popups inviteModal" href="#invite-area" data-effect="mfp-zoom-out"><i class="fa fa-ticket fa-fw"></i> <span lang="en">Manage Invites</span></a></li>`;
+			htmlDOM += `
+			<div id="new-invite-area" class="white-popup mfp-with-anim mfp-hide">
+				<div class="col-md-10 col-md-offset-1">
+					<div class="col-md-12">
+						<div class="panel panel-info m-b-0">
+							<div class="panel-heading" lang="en">New Invite</div>
+							<div class="panel-wrapper collapse in" aria-expanded="true">
+								<div class="panel-body">
+									<form id="new-invite-form">
+										<fieldset style="border:0;">
+										<div class="form-group">
+											<label class="control-label" for="new-invite-form-inputUsername" lang="en">Name or Username</label>
+											<input type="text" class="form-control" id="new-invite-form-inputUsername" name="username" required="" autofocus="">
+										</div>
+										<div class="form-group">
+											<label class="control-label" for="new-invite-form-inputEmail" lang="en">Email</label>
+											<input type="text" class="form-control" id="new-invite-form-inputEmail" name="email" required="" autofocus="">
+										</div>
+										</fieldset>
+										<button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none" onclick="createNewInvite();" type="button"><span class="btn-label"><i class="fa fa-plus"></i></span><span lang="en">Create/Send Invite</span></button>
+										<div class="clearfix"></div>
+									</form>
+								</div>
+							</div>
+						</div>
+					</div>
+					<div class="clearfix"></div>
+				</div>
+			</div>`;
+		}else if (activeInfo.user.loggedin === false){
+			menuList = `<li><a class="inline-popups inviteModal" href="#invite-area" data-effect="mfp-zoom-out"><i class="fa fa-ticket fa-fw"></i> <span lang="en">Use Invite Code</span></a></li>`;
+		}
+		$('.append-menu').after(menuList);
+		$('.organizr-area').after(htmlDOM);
+		pageLoad();
+		getInvite();
+	}
 }
 function joinPlex(){
-    var username = $('#invitePlexJoinUsername');
-    var email = $('#invitePlexJoinEmail');
-    var password = $('#invitePlexJoinPassword');
-    if(username.val() == ''){
-        username.focus();
-        message('Invite Error',' Please Enter Username',activeInfo.settings.notifications.position,'#FFF','warning','5000');
-    }else if(email.val() == ''){
-        email.focus();
-        message('Invite Error',' Please Enter Email',activeInfo.settings.notifications.position,'#FFF','warning','5000');
-    }else if(password.val() == ''){
-        password.focus();
-        message('Invite Error',' Please Enter Password',activeInfo.settings.notifications.position,'#FFF','warning','5000');
-    }
-    if(email.val() !== '' && username.val() !== '' && password.val() !== ''){
-        organizrAPI2('POST','api/v2/plex/register',{username:username.val(), email:email.val(), password:password.val()}).success(function(data) {
-    		var response = data.response;
-            if(response.result === 'success'){
-                $('.invite-step-3-plex-no').toggleClass('hidden');
-                $('.invite-step-3-plex-yes').toggleClass('hidden');
-                message('Invite Function',' User Created',activeInfo.settings.notifications.position,'#FFF','success','5000');
-                $('#inviteUsernameInvite').val(username.val());
-                hasPlexUsername();
-            }else{
-                message('Invite Error',' '+response.message,activeInfo.settings.notifications.position,'#FFF','warning','5000');
-            }
-    	}).fail(function(xhr) {
-	        OrganizrApiError(xhr, 'Plex Signup Error');
-    	});
-    }
+	var username = $('#invitePlexJoinUsername');
+	var email = $('#invitePlexJoinEmail');
+	var password = $('#invitePlexJoinPassword');
+	if(username.val() == ''){
+		username.focus();
+		message('Invite Error',' Please Enter Username',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+	}else if(email.val() == ''){
+		email.focus();
+		message('Invite Error',' Please Enter Email',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+	}else if(password.val() == ''){
+		password.focus();
+		message('Invite Error',' Please Enter Password',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+	}
+	if(email.val() !== '' && username.val() !== '' && password.val() !== ''){
+		organizrAPI2('POST','api/v2/plex/register',{username:username.val(), email:email.val(), password:password.val()}).success(function(data) {
+			var response = data.response;
+			if(response.result === 'success'){
+				$('.invite-step-3-plex-no').toggleClass('hidden');
+				$('.invite-step-3-plex-yes').toggleClass('hidden');
+				message('Invite Function',' User Created',activeInfo.settings.notifications.position,'#FFF','success','5000');
+				$('#inviteUsernameInvite').val(username.val());
+				hasPlexUsername();
+			}else{
+				message('Invite Error',' '+response.message,activeInfo.settings.notifications.position,'#FFF','warning','5000');
+			}
+		}).fail(function(xhr) {
+			OrganizrApiError(xhr, 'Plex Signup Error');
+		});
+	}
 }
 
 function joinEmby(){
-    var username = $('#inviteEmbyJoinUsername');
-    var email = $('#inviteEmbyJoinEmail');
-    var password = $('#inviteEmbyJoinPassword');
-    if(username.val() == ''){
-        username.focus();
-        message('Invite Error',' Please Enter Username',activeInfo.settings.notifications.position,'#FFF','warning','5000');
-    }else if(email.val() == ''){
-        email.focus();
-        message('Invite Error',' Please Enter Email',activeInfo.settings.notifications.position,'#FFF','warning','5000');
-    }else if(password.val() == ''){
-        password.focus();
-        message('Invite Error',' Please Enter Password',activeInfo.settings.notifications.position,'#FFF','warning','5000');
-    }
-    if(email.val() !== '' && username.val() !== '' && password.val() !== ''){
-        organizrAPI2('POST','api/v2/emby/register',{username:username.val(), email:email.val(), password:password.val()}).success(function(data) {
-    		var response = data.response;
-            if(response.result === 'success'){
-                $('.invite-step-3-emby-no').toggleClass('hidden');
-                $('.invite-step-3-emby-yes').toggleClass('hidden');
-                message('Invite Function',' User Created',activeInfo.settings.notifications.position,'#FFF','success','5000');
-                $('#inviteUsernameInviteEmby').val(username.val());
-                hasEmbyUsername();
-            }else{
-                message('Invite Error',' '+response.message,activeInfo.settings.notifications.position,'#FFF','warning','5000');
-            }
-    	}).fail(function(xhr) {
-	        OrganizrApiError(xhr, 'Emby Signup Error');
-    	});
-    }
+	var username = $('#inviteEmbyJoinUsername');
+	var email = $('#inviteEmbyJoinEmail');
+	var password = $('#inviteEmbyJoinPassword');
+	if(username.val() == ''){
+		username.focus();
+		message('Invite Error',' Please Enter Username',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+	}else if(email.val() == ''){
+		email.focus();
+		message('Invite Error',' Please Enter Email',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+	}else if(password.val() == ''){
+		password.focus();
+		message('Invite Error',' Please Enter Password',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+	}
+	if(email.val() !== '' && username.val() !== '' && password.val() !== ''){
+		organizrAPI2('POST','api/v2/emby/register',{username:username.val(), email:email.val(), password:password.val()}).success(function(data) {
+			var response = data.response;
+			if(response.result === 'success'){
+				$('.invite-step-3-emby-no').toggleClass('hidden');
+				$('.invite-step-3-emby-yes').toggleClass('hidden');
+				message('Invite Function',' User Created',activeInfo.settings.notifications.position,'#FFF','success','5000');
+				$('#inviteUsernameInviteEmby').val(username.val());
+				hasEmbyUsername();
+			}else{
+				message('Invite Error',' '+response.message,activeInfo.settings.notifications.position,'#FFF','warning','5000');
+			}
+		}).fail(function(xhr) {
+			OrganizrApiError(xhr, 'Emby Signup Error');
+		});
+	}
 }
 
 function inviteHasAccount(type,value){
-    switch (type) {
-        case 'plex':
-            if(value){
-                $('.invite-step-2').toggleClass('hidden');
-                $('.invite-step-3-plex-yes').toggleClass('hidden');
-            }else{
-                $('.invite-step-2').toggleClass('hidden');
-                $('.invite-step-3-plex-no').toggleClass('hidden');
-            }
-            break;
-        case 'emby' :
-          if(value){
-            $('.invite-step-2').toggleClass('hidden');
-            $('.invite-step-3-emby-yes').toggleClass('hidden');
-          }else{
-            $('.invite-step-2').toggleClass('hidden');
-            $('.invite-step-3-emby-no').toggleClass('hidden');
-          }
-          break;
-        default:
-        alert(type+' is not set up yet');
-    }
+	switch (type) {
+		case 'plex':
+			if(value){
+				$('.invite-step-2').toggleClass('hidden');
+				$('.invite-step-3-plex-yes').toggleClass('hidden');
+			}else{
+				$('.invite-step-2').toggleClass('hidden');
+				$('.invite-step-3-plex-no').toggleClass('hidden');
+			}
+			break;
+		case 'emby' :
+		  if(value){
+			$('.invite-step-2').toggleClass('hidden');
+			$('.invite-step-3-emby-yes').toggleClass('hidden');
+		  }else{
+			$('.invite-step-2').toggleClass('hidden');
+			$('.invite-step-3-emby-no').toggleClass('hidden');
+		  }
+		  break;
+		default:
+		alert(type+' is not set up yet');
+	}
 }
 function hasPlexUsername(){
-    var code = $('#inviteCodeInput').val().toUpperCase();
-    var username = $('#inviteUsernameInvite');
-    if(username.val() == ''){
-        username.focus();
-        message('Invite Error',' Please Enter Username',activeInfo.settings.notifications.position,'#FFF','warning','5000');
-    }else{
-        var post = {
-            usedby:username.val()
-        };
-        ajaxloader(".content-wrap","in");
-        organizrAPI2('POST','api/v2/plugins/invites/' + code,post).success(function(data) {
-            var response = data.response;
-            if(response.result === 'success'){
-                $('.invite-step-3-plex-yes').toggleClass('hidden');
-                $('.invite-step-4-plex-accept').toggleClass('hidden');
-                if(local('get', 'invite')){
-            		local('remove', 'invite');
-            	}
-            }else{
-                message('Invite Error',response.message,activeInfo.settings.notifications.position,'#FFF','warning','5000');
-            }
-            ajaxloader();;
-        }).fail(function(xhr) {
-	        OrganizrApiError(xhr);
-            ajaxloader();
-        });
-    }
+	var code = $('#inviteCodeInput').val().toUpperCase();
+	var username = $('#inviteUsernameInvite');
+	if(username.val() == ''){
+		username.focus();
+		message('Invite Error',' Please Enter Username',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+	}else{
+		var post = {
+			usedby:username.val()
+		};
+		ajaxloader(".content-wrap","in");
+		organizrAPI2('POST','api/v2/plugins/invites/' + code,post).success(function(data) {
+			var response = data.response;
+			if(response.result === 'success'){
+				$('.invite-step-3-plex-yes').toggleClass('hidden');
+				$('.invite-step-4-plex-accept').toggleClass('hidden');
+				if(local('get', 'invite')){
+					local('remove', 'invite');
+				}
+			}else{
+				message('Invite Error',response.message,activeInfo.settings.notifications.position,'#FFF','warning','5000');
+			}
+			ajaxloader();;
+		}).fail(function(xhr) {
+			OrganizrApiError(xhr);
+			ajaxloader();
+		});
+	}
 }
 function hasEmbyUsername(){
-    var code = $('#inviteCodeInput').val().toUpperCase();
-    var username = $('#inviteUsernameInviteEmby');
-    if(username.val() == ''){
-        username.focus();
-        message('Invite Error',' Please Enter Username',activeInfo.settings.notifications.position,'#FFF','warning','5000');
-    }else{
-        var post = {
-            usedby:username.val()
-        };
-        ajaxloader(".content-wrap","in");
-        organizrAPI2('POST','api/v2/plugins/invites/' + code,post).success(function(data) {
-	        var response = data.response;
-	        if(response.result === 'success'){
-                $('.invite-step-3-emby-yes').toggleClass('hidden');
-                $('.invite-step-4-emby-accept').toggleClass('hidden');
-                if(local('get', 'invite')){
-            		local('remove', 'invite');
-            	}
-            }else{
-                message('Invite Error',response.message,activeInfo.settings.notifications.position,'#FFF','warning','5000');
-            }
-            ajaxloader();;
-        }).fail(function(xhr) {
-	        OrganizrApiError(xhr);
-            ajaxloader();
-        });
-    }
+	var code = $('#inviteCodeInput').val().toUpperCase();
+	var username = $('#inviteUsernameInviteEmby');
+	if(username.val() == ''){
+		username.focus();
+		message('Invite Error',' Please Enter Username',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+	}else{
+		var post = {
+			usedby:username.val()
+		};
+		ajaxloader(".content-wrap","in");
+		organizrAPI2('POST','api/v2/plugins/invites/' + code,post).success(function(data) {
+			var response = data.response;
+			if(response.result === 'success'){
+				$('.invite-step-3-emby-yes').toggleClass('hidden');
+				$('.invite-step-4-emby-accept').toggleClass('hidden');
+				if(local('get', 'invite')){
+					local('remove', 'invite');
+				}
+			}else{
+				message('Invite Error',response.message,activeInfo.settings.notifications.position,'#FFF','warning','5000');
+			}
+			ajaxloader();;
+		}).fail(function(xhr) {
+			OrganizrApiError(xhr);
+			ajaxloader();
+		});
+	}
 }
 function verifyInvite(){
-    var code = $('#inviteCodeInput').val().toUpperCase();
-    ajaxloader(".content-wrap","in");
-    organizrAPI2('GET','api/v2/plugins/invites/'+code).success(function(data) {
-        var response = data.response;
-        if(response.result === 'success'){
-            $('.invite-step-1').toggleClass('hidden');
-            $('.invite-step-2').toggleClass('hidden');
-        }else{
-            message('Invite Error',response.message,activeInfo.settings.notifications.position,'#FFF','warning','5000');
-        }
-        if(local('get', 'invite')){
-            local('remove', 'invite');
-        }
-        ajaxloader();;
-    }).fail(function(xhr) {
-	    OrganizrApiError(xhr);
-        ajaxloader();
-    });
+	var code = $('#inviteCodeInput').val().toUpperCase();
+	ajaxloader(".content-wrap","in");
+	organizrAPI2('GET','api/v2/plugins/invites/'+code).success(function(data) {
+		var response = data.response;
+		if(response.result === 'success'){
+			$('.invite-step-1').toggleClass('hidden');
+			$('.invite-step-2').toggleClass('hidden');
+		}else{
+			message('Invite Error',response.message,activeInfo.settings.notifications.position,'#FFF','warning','5000');
+		}
+		if(local('get', 'invite')){
+			local('remove', 'invite');
+		}
+		ajaxloader();;
+	}).fail(function(xhr) {
+		OrganizrApiError(xhr);
+		ajaxloader();
+	});
 }
 function getInvite(invite=null){
 	if(invite){
@@ -237,232 +229,216 @@ function getInvite(invite=null){
 	if(local('get', 'invite')){
 		//show error page
 		$('.inviteModal').trigger('click');
-        $('#inviteCodeInput').val(local('get', 'invite'));
+		$('#inviteCodeInput').val(local('get', 'invite'));
 		window.history.pushState({}, document.title, "./" );
-        local('remove', 'invite');
+		local('remove', 'invite');
 	}
 
 }
 function createNewInvite(){
-    var username = $('#new-invite-form-inputUsername');
-    var email = $('#new-invite-form-inputEmail');
-    if(username.val() == ''){
-        username.focus();
-        message('Invite Error',' Please Enter Username',activeInfo.settings.notifications.position,'#FFF','warning','5000');
-    }else if(email.val() == ''){
-        email.focus();
-        message('Invite Error',' Please Enter Email',activeInfo.settings.notifications.position,'#FFF','warning','5000');
-    }
+	var username = $('#new-invite-form-inputUsername');
+	var email = $('#new-invite-form-inputEmail');
+	if(username.val() == ''){
+		username.focus();
+		message('Invite Error',' Please Enter Username',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+	}else if(email.val() == ''){
+		email.focus();
+		message('Invite Error',' Please Enter Email',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+	}
 
-    if(email.val() !== '' && username.val() !== ''){
-        var post = {
-            code:createRandomString(6).toUpperCase(),
-            email:email.val(),
-            username:username.val(),
-        };
-        ajaxloader(".content-wrap","in");
-        organizrAPI2('POST','api/v2/plugins/invites',post).success(function(data) {
-            var response = data.response;
-            $.magnificPopup.close();
-            ajaxloader();
-            message('Invite',' Invite Created',activeInfo.settings.notifications.position,'#FFF','success','5000');
-        }).fail(function(xhr) {
-	        OrganizrApiError(xhr);
-            ajaxloader();
-            message('Invite Error',' An Error Occured',activeInfo.settings.notifications.position,'#FFF','error','5000');
-        });
-    }
+	if(email.val() !== '' && username.val() !== ''){
+		var post = {
+			code:createRandomString(6).toUpperCase(),
+			email:email.val(),
+			username:username.val(),
+		};
+		ajaxloader(".content-wrap","in");
+		organizrAPI2('POST','api/v2/plugins/invites',post).success(function(data) {
+			var response = data.response;
+			$.magnificPopup.close();
+			ajaxloader();
+			message('Invite',' Invite Created',activeInfo.settings.notifications.position,'#FFF','success','5000');
+		}).fail(function(xhr) {
+			OrganizrApiError(xhr);
+			ajaxloader();
+			message('Invite Error',' An Error Occured',activeInfo.settings.notifications.position,'#FFF','error','5000');
+		});
+	}
 
 }
 function deleteInvite(code, id){
-    ajaxloader(".content-wrap","in");
-    organizrAPI2('DELETE','api/v2/plugins/invites/' + code).success(function(data) {
-        var response = data.response;
-        $('#inviteItem-'+id).remove();
-        //$.magnificPopup.close();
-        ajaxloader();
-        message('Invite',' Invite Deleted',activeInfo.settings.notifications.position,'#FFF','success','5000');
-    }).fail(function(xhr) {
-        console.error("Organizr Function: API Connection Failed");
-        ajaxloader();
-        message('Invite Error',' An Error Occured',activeInfo.settings.notifications.position,'#FFF','error','5000');
-    });
+	ajaxloader(".content-wrap","in");
+	organizrAPI2('DELETE','api/v2/plugins/invites/' + code).success(function(data) {
+		var response = data.response;
+		$('#inviteItem-'+id).remove();
+		//$.magnificPopup.close();
+		ajaxloader();
+		message('Invite',' Invite Deleted',activeInfo.settings.notifications.position,'#FFF','success','5000');
+	}).fail(function(xhr) {
+		console.error("Organizr Function: API Connection Failed");
+		ajaxloader();
+		message('Invite Error',' An Error Occured',activeInfo.settings.notifications.position,'#FFF','error','5000');
+	});
 
 }
 // EVENTS and LISTENERS
 function buildInvites(array){
-    if(array.length == 0){
+	if(array.length == 0){
 		return '<h2 class="text-center" lang="en">No Invites</h2>';
 	}
-    var invites = '';
+	var invites = '';
 	$.each(array, function(i,v) {
-        v.dateused = (v.dateused) ? v.dateused : '-';
-        v.usedby = (v.usedby) ? v.usedby : '-';
-        v.ip = (v.ip) ? v.ip : '-';
-        invites += `
-        <tr id="inviteItem-`+v.id+`">
-            <td class="text-center">`+v.id+`</td>
-            <td>`+v.username+`</td>
-            <td>`+v.email+`</td>
-            <td>`+v.code+`</td>
-            <td>`+v.date+`</td>
-            <td>`+v.dateused+`</td>
-            <td>`+v.usedby+`</td>
-            <td>`+v.ip+`</td>
-            <td>`+v.valid+`</td>
-            <td><button type="button" class="btn btn-danger btn-outline btn-circle btn-lg m-r-5" onclick="deleteInvite('`+v.code+`','`+v.id+`');"><i class="ti-trash"></i></button></td>
-        </tr>
-        `;
-    });
-    return invites;
+		v.dateused = (v.dateused) ? v.dateused : '-';
+		v.usedby = (v.usedby) ? v.usedby : '-';
+		v.ip = (v.ip) ? v.ip : '-';
+		invites += `
+		<tr id="inviteItem-`+v.id+`">
+			<td class="text-center">`+v.id+`</td>
+			<td>`+v.username+`</td>
+			<td>`+v.email+`</td>
+			<td>`+v.code+`</td>
+			<td>`+v.date+`</td>
+			<td>`+v.dateused+`</td>
+			<td>`+v.usedby+`</td>
+			<td>`+v.ip+`</td>
+			<td>`+v.valid+`</td>
+			<td><button type="button" class="btn btn-danger btn-outline btn-circle btn-lg m-r-5" onclick="deleteInvite('`+v.code+`','`+v.id+`');"><i class="ti-trash"></i></button></td>
+		</tr>
+		`;
+	});
+	return invites;
 }
 $(document).on('click', '.inviteModal', function() {
-    var htmlDOM = '';
-    if (activeInfo.user.loggedin === true && activeInfo.user.groupID <= 1) {
-        ajaxloader(".content-wrap","in");
-        organizrAPI2('GET','api/v2/plugins/invites').success(function(data) {
-            var response = data.response;
-            var htmlDOM = '';
-            htmlDOM = `
-            <div class="col-md-12">
-                <div class="panel bg-org panel-info">
-                    <div class="panel-heading">
-                        <span lang="en">Manage Invites</span>
-                        <button type="button" class="btn btn-info btn-circle pull-right popup-with-form" href="#new-invite-area" data-effect="mfp-3d-unfold"><i class="fa fa-plus"></i> </button>
-                    </div>
-                    <div class="table-responsive">
-                        <table class="table table-hover manage-u-table">
-                            <thead>
-                                <tr>
-                                    <th width="70" class="text-center">#</th>
-                                    <th lang="en">USERNAME</th>
-                                    <th lang="en">EMAIL</th>
-                                    <th lang="en">INVITE CODE</th>
-                                    <th lang="en">DATE SENT</th>
-                                    <th lang="en">DATE USED</th>
-                                    <th lang="en">USED BY</th>
-                                    <th lang="en">IP ADDRESS</th>
-                                    <th lang="en">VALID</th>
-                                    <th lang="en">DELETE</th>
-                                </tr>
-                            </thead>
-                            <tbody id="manageInviteTable">
-                                `+buildInvites(response.data)+`
-                            </tbody>
-                        </table>
-                    </div>
-                </div>
-            </div>
-            <div class="clearfix"></div>
-            `;
-            $('.invite-div').html(htmlDOM);
-        }).fail(function(xhr) {
-            console.error("Organizr Function: API Connection Failed");
-        });
-        ajaxloader();
-    }else if (activeInfo.user.loggedin === false){
-        htmlDOM = `
-        <div class="col-md-12">
-            <div class="panel panel-info m-b-0">
-                <div class="panel-heading" lang="en">Use Invite Code</div>
-                <div class="panel-wrapper collapse in" aria-expanded="true">
-                    <div class="panel-body">
-                        <div class="form-group invite-step-1">
-                            <div class="input-group" style="width: 100%;">
-                                <div class="input-group-addon hidden-xs"><i class="ti-lock"></i></div>
-                                <input type="text" class="form-control text-uppercase" id="inviteCodeInput" placeholder="Code" autocomplete="off" autocorrect="off" autocapitalize="off" maxlength="6" spellcheck="false" autofocus="" required="">
-                            </div>
-                            <br />
-                            <button class="btn btn-block btn-info" onclick="verifyInvite();">Verify</button>
-
-                        </div>
-                        <div class="form-group invite-step-2 hidden">
-
-
-                            <div class="row">
-                                <h2 class="text-center" lang="en">Do you have a `+activeInfo.plugins.includes["INVITES-type-include"].toUpperCase()+` account?</h2>
-                                <div class="col-lg-6">
-                                    <button class="btn btn-block btn-info m-b-10" onclick="inviteHasAccount('`+activeInfo.plugins.includes["INVITES-type-include"]+`',true);" lang="en">Yes</button>
-                                </div>
-                                <div class="col-lg-6">
-                                    <button class="btn btn-block btn-primary m-b-10" onclick="inviteHasAccount('`+activeInfo.plugins.includes["INVITES-type-include"]+`',false);" lang="en">No</button>
-                                </div>
-                            </div>
-
-                        </div>
-                        <div class="form-group invite-step-3-plex-yes hidden">
-                            <div class="input-group" style="width: 100%;">
-                                <div class="input-group-addon hidden-xs"><i class="ti-user"></i></div>
-                                <input type="text" class="form-control" id="inviteUsernameInvite" placeholder="Plex Username or Email" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus="" required="">
-                            </div>
-                            <br />
-                            <button class="btn btn-block btn-info" onclick="hasPlexUsername();">Submit</button>
-                        </div>
-                        <div class="form-group invite-step-3-plex-no hidden">
-                            <div class="input-group" style="width: 100%;">
-                                <div class="input-group-addon hidden-xs"><i class="ti-user"></i></div>
-                                <input type="text" class="form-control" id="invitePlexJoinUsername" lang="en" placeholder="Username" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus="" required="">
-                            </div>
-                            <div class="input-group" style="width: 100%;">
-                                <div class="input-group-addon hidden-xs"><i class="ti-email"></i></div>
-                                <input type="text" class="form-control" id="invitePlexJoinEmail" lang="en" placeholder="E-Mail" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" required="">
-                            </div>
-                            <div class="input-group" style="width: 100%;">
-                                <div class="input-group-addon hidden-xs"><i class="ti-user"></i></div>
-                                <input type="password" class="form-control" id="invitePlexJoinPassword" lang="en" placeholder="Password" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"  required="">
-                            </div>
-                            <br />
-                            <button class="btn btn-block btn-info" onclick="joinPlex();">Submit</button>
-                        </div>
-                        <div class="form-group invite-step-4-plex-accept hidden">
-                            <h4 class="" lang="en">You have been invited.  Please check your email or goto <a href="https://plex.tv" target="_blank">PLEX.TV</a> and login to accept the invite.  Once you have done that, you may head back here and login with your credentials.</h4>
-                        </div>
-                        <!-- Begin Emby Invites -->
-                        <div class="form-group invite-step-3-emby-yes hidden">
-                            <div class="input-group" style="width: 100%;">
-                                <div class="input-group-addon hidden-xs"><i class="ti-user"></i></div>
-                                <input type="text" class="form-control" id="inviteUsernameInviteEmby" placeholder="Emby Username" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus="" required="">
-                            </div>
-                            <br />
-                            <button class="btn btn-block btn-info" onclick="hasEmbyUsername();">Submit</button>
-                        </div>
-                        <div class="form-group invite-step-3-emby-no hidden">
-                            <div class="input-group" style="width: 100%;">
-                                <div class="input-group-addon hidden-xs"><i class="ti-user"></i></div>
-                                <input type="text" class="form-control" id="inviteEmbyJoinUsername" lang="en" placeholder="Username" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus="" required="">
-                            </div>
-                            <div class="input-group" style="width: 100%;">
-                                <div class="input-group-addon hidden-xs"><i class="ti-email"></i></div>
-                                <input type="text" class="form-control" id="inviteEmbyJoinEmail" lang="en" placeholder="E-Mail" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" required="">
-                            </div>
-                            <div class="input-group" style="width: 100%;">
-                                <div class="input-group-addon hidden-xs"><i class="ti-user"></i></div>
-                                <input type="password" class="form-control" id="inviteEmbyJoinPassword" lang="en" placeholder="Password" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"  required="">
-                            </div>
-                            <br />
-                            <button class="btn btn-block btn-info" onclick="joinEmby();">Submit</button>
-                        </div>
-                        <div class="form-group invite-step-4-emby-accept hidden">
-                            <h4 class="" lang="en">You Have been added to emby!</h4>
-                        </div>
-                    </div>
-                </div>
-            </div>
-        </div>
-        <div class="clearfix"></div>
-        `;
-        $('.invite-div').html(htmlDOM);
-    }
-});
-
-$(document).on('click', '#INVITES-settings-button', function() {
-    ajaxloader(".content-wrap","in");
-    organizrAPI2('GET','api/v2/plugins/invites/settings').success(function(data) {
-        var response = data.response;
-        $('#INVITES-settings-items').html(buildFormGroup(response.data));
-        $('.selectpicker').selectpicker();
-    }).fail(function(xhr) {
-        console.error("Organizr Function: API Connection Failed");
-    });
-    ajaxloader();
+	var htmlDOM = '';
+	if (activeInfo.user.loggedin === true && activeInfo.user.groupID <= 1) {
+		ajaxloader(".content-wrap","in");
+		organizrAPI2('GET','api/v2/plugins/invites').success(function(data) {
+			var response = data.response;
+			var htmlDOM = '';
+			htmlDOM = `
+			<div class="col-md-12">
+				<div class="panel bg-org panel-info">
+					<div class="panel-heading">
+						<span lang="en">Manage Invites</span>
+						<button type="button" class="btn btn-info btn-circle pull-right popup-with-form" href="#new-invite-area" data-effect="mfp-3d-unfold"><i class="fa fa-plus"></i> </button>
+					</div>
+					<div class="table-responsive">
+						<table class="table table-hover manage-u-table">
+							<thead>
+								<tr>
+									<th width="70" class="text-center">#</th>
+									<th lang="en">USERNAME</th>
+									<th lang="en">EMAIL</th>
+									<th lang="en">INVITE CODE</th>
+									<th lang="en">DATE SENT</th>
+									<th lang="en">DATE USED</th>
+									<th lang="en">USED BY</th>
+									<th lang="en">IP ADDRESS</th>
+									<th lang="en">VALID</th>
+									<th lang="en">DELETE</th>
+								</tr>
+							</thead>
+							<tbody id="manageInviteTable">
+								`+buildInvites(response.data)+`
+							</tbody>
+						</table>
+					</div>
+				</div>
+			</div>
+			<div class="clearfix"></div>
+			`;
+			$('.invite-div').html(htmlDOM);
+		}).fail(function(xhr) {
+			console.error("Organizr Function: API Connection Failed");
+		});
+		ajaxloader();
+	}else if (activeInfo.user.loggedin === false){
+		htmlDOM = `
+		<div class="col-md-12">
+			<div class="panel panel-info m-b-0">
+				<div class="panel-heading" lang="en">Use Invite Code</div>
+				<div class="panel-wrapper collapse in" aria-expanded="true">
+					<div class="panel-body">
+						<div class="form-group invite-step-1">
+							<div class="input-group" style="width: 100%;">
+								<div class="input-group-addon hidden-xs"><i class="ti-lock"></i></div>
+								<input type="text" class="form-control text-uppercase" id="inviteCodeInput" placeholder="Code" autocomplete="off" autocorrect="off" autocapitalize="off" maxlength="6" spellcheck="false" autofocus="" required="">
+							</div>
+							<br />
+							<button class="btn btn-block btn-info" onclick="verifyInvite();">Verify</button>
+						</div>
+						<div class="form-group invite-step-2 hidden">
+							<div class="row">
+								<h2 class="text-center" lang="en">Do you have a `+activeInfo.plugins.includes["INVITES-type-include"].toUpperCase()+` account?</h2>
+								<div class="col-lg-6">
+									<button class="btn btn-block btn-info m-b-10" onclick="inviteHasAccount('`+activeInfo.plugins.includes["INVITES-type-include"]+`',true);" lang="en">Yes</button>
+								</div>
+								<div class="col-lg-6">
+									<button class="btn btn-block btn-primary m-b-10" onclick="inviteHasAccount('`+activeInfo.plugins.includes["INVITES-type-include"]+`',false);" lang="en">No</button>
+								</div>
+							</div>
+						</div>
+						<div class="form-group invite-step-3-plex-yes hidden">
+							<div class="input-group" style="width: 100%;">
+								<div class="input-group-addon hidden-xs"><i class="ti-user"></i></div>
+								<input type="text" class="form-control" id="inviteUsernameInvite" placeholder="Plex Username or Email" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus="" required="">
+							</div>
+							<br />
+							<button class="btn btn-block btn-info" onclick="hasPlexUsername();">Submit</button>
+						</div>
+						<div class="form-group invite-step-3-plex-no hidden">
+							<div class="input-group" style="width: 100%;">
+								<div class="input-group-addon hidden-xs"><i class="ti-user"></i></div>
+								<input type="text" class="form-control" id="invitePlexJoinUsername" lang="en" placeholder="Username" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus="" required="">
+							</div>
+							<div class="input-group" style="width: 100%;">
+								<div class="input-group-addon hidden-xs"><i class="ti-email"></i></div>
+								<input type="text" class="form-control" id="invitePlexJoinEmail" lang="en" placeholder="E-Mail" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" required="">
+							</div>
+							<div class="input-group" style="width: 100%;">
+								<div class="input-group-addon hidden-xs"><i class="ti-user"></i></div>
+								<input type="password" class="form-control" id="invitePlexJoinPassword" lang="en" placeholder="Password" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"  required="">
+							</div>
+							<br />
+							<button class="btn btn-block btn-info" onclick="joinPlex();">Submit</button>
+						</div>
+						<div class="form-group invite-step-4-plex-accept hidden">
+							<h4 class="" lang="en">You have been invited.  Please check your email or goto <a href="https://plex.tv" target="_blank">PLEX.TV</a> and login to accept the invite.  Once you have done that, you may head back here and login with your credentials.</h4>
+						</div>
+						<!-- Begin Emby Invites -->
+						<div class="form-group invite-step-3-emby-yes hidden">
+							<div class="input-group" style="width: 100%;">
+								<div class="input-group-addon hidden-xs"><i class="ti-user"></i></div>
+								<input type="text" class="form-control" id="inviteUsernameInviteEmby" placeholder="Emby Username" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus="" required="">
+							</div>
+							<br />
+							<button class="btn btn-block btn-info" onclick="hasEmbyUsername();">Submit</button>
+						</div>
+						<div class="form-group invite-step-3-emby-no hidden">
+							<div class="input-group" style="width: 100%;">
+								<div class="input-group-addon hidden-xs"><i class="ti-user"></i></div>
+								<input type="text" class="form-control" id="inviteEmbyJoinUsername" lang="en" placeholder="Username" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus="" required="">
+							</div>
+							<div class="input-group" style="width: 100%;">
+								<div class="input-group-addon hidden-xs"><i class="ti-email"></i></div>
+								<input type="text" class="form-control" id="inviteEmbyJoinEmail" lang="en" placeholder="E-Mail" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" required="">
+							</div>
+							<div class="input-group" style="width: 100%;">
+								<div class="input-group-addon hidden-xs"><i class="ti-user"></i></div>
+								<input type="password" class="form-control" id="inviteEmbyJoinPassword" lang="en" placeholder="Password" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"  required="">
+							</div>
+							<br />
+							<button class="btn btn-block btn-info" onclick="joinEmby();">Submit</button>
+						</div>
+						<div class="form-group invite-step-4-emby-accept hidden">
+							<h4 class="" lang="en">You Have been added to emby!</h4>
+						</div>
+					</div>
+				</div>
+			</div>
+		</div>
+		<div class="clearfix"></div>
+		`;
+		$('.invite-div').html(htmlDOM);
+	}
 });

+ 205 - 223
api/plugins/js/php-mailer.js

@@ -1,262 +1,244 @@
 /* PHP MAILER JS FILE */
-
+$('body').arrive('#activeInfo', {onceOnly: true}, function() {
+	phpmLaunch();
+});
 // FUNCTIONS
-phpmLaunch();
+
 function phpmLaunch(){
-    if(typeof activeInfo == 'undefined'){
-        setTimeout(function () {
-            phpmLaunch();
-        }, 1000);
-    }else{
-        if(activeInfo.plugins["PHPMAILER-enabled"] == true){
-            if (activeInfo.user.loggedin === true && activeInfo.user.groupID <= 1) {
-                var menuList = `<li><a class="inline-popups emailModal" href="#email-area" data-effect="mfp-zoom-out"><i class="fa fa-envelope fa-fw"></i> <span lang="en">E-Mail Center</span></a></li>`;
-                var htmlDOM = `
-            	<div id="email-area" class="white-popup mfp-with-anim mfp-hide">
-            		<div class="col-md-10 col-md-offset-1">
-            			<div class="email-div"></div>
-            		</div>
-            	</div>
-            	`;
-                $('.organizr-area').after(htmlDOM);
-                $('.append-menu').after(menuList);
-                pageLoad();
-            }
-        }
-    }
+	if(activeInfo.plugins["PHPMAILER-enabled"] == true){
+		if (activeInfo.user.loggedin === true && activeInfo.user.groupID <= 1) {
+			var menuList = `<li><a class="inline-popups emailModal" href="#email-area" data-effect="mfp-zoom-out"><i class="fa fa-envelope fa-fw"></i> <span lang="en">E-Mail Center</span></a></li>`;
+			var htmlDOM = `
+			<div id="email-area" class="white-popup mfp-with-anim mfp-hide">
+				<div class="col-md-10 col-md-offset-1">
+					<div class="email-div"></div>
+				</div>
+			</div>
+			`;
+			$('.organizr-area').after(htmlDOM);
+			$('.append-menu').after(menuList);
+			pageLoad();
+		}
+	}
+
 }
 function sendMail(){
-    var to = $('#sendEmailToInput').val();
-    var subject = $('#sendEmailSubjectInput').val();
-    var body = tinyMCE.get('sendEmail').getContent();
-    if(to == ''){
-        messageSingle('','Please Enter Email',activeInfo.settings.notifications.position,'#FFF','error','5000');
-    }else if(subject == ''){
-        messageSingle('','Please Enter Subject',activeInfo.settings.notifications.position,'#FFF','error','5000');
-    }else if(body == ''){
-        messageSingle('','Please Enter Body',activeInfo.settings.notifications.position,'#FFF','error','5000');
-    }else{
-	    messageSingle('','Sending Message',activeInfo.settings.notifications.position,'#FFF','success','5000');
-    }
-    if(to !== '' && subject !== '' && body !== ''){
-        var post = {
-            bcc:to,
-            subject:subject,
-            body:body
-        };
-        ajaxloader(".content-wrap","in");
-        organizrAPI2('POST','api/v2/plugins/php-mailer/email/send',post).success(function(data) {
-            var response = data.response;
-            if(response.result == 'success'){
-                $.magnificPopup.close();
-                messageSingle('',window.lang.translate('Email Sent Successful'),activeInfo.settings.notifications.position,'#FFF','success','5000');
-            }else{
-                messageSingle('',response.message,activeInfo.settings.notifications.position,'#FFF','error','5000');
-            }
-        }).fail(function(xhr) {
-	        OrganizrApiError(xhr);
-        });
-        ajaxloader();
-    }
+	var to = $('#sendEmailToInput').val();
+	var subject = $('#sendEmailSubjectInput').val();
+	var body = tinyMCE.get('sendEmail').getContent();
+	if(to == ''){
+		messageSingle('','Please Enter Email',activeInfo.settings.notifications.position,'#FFF','error','5000');
+	}else if(subject == ''){
+		messageSingle('','Please Enter Subject',activeInfo.settings.notifications.position,'#FFF','error','5000');
+	}else if(body == ''){
+		messageSingle('','Please Enter Body',activeInfo.settings.notifications.position,'#FFF','error','5000');
+	}else{
+		messageSingle('','Sending Message',activeInfo.settings.notifications.position,'#FFF','success','5000');
+	}
+	if(to !== '' && subject !== '' && body !== ''){
+		var post = {
+			bcc:to,
+			subject:subject,
+			body:body
+		};
+		ajaxloader(".content-wrap","in");
+		organizrAPI2('POST','api/v2/plugins/php-mailer/email/send',post).success(function(data) {
+			var response = data.response;
+			if(response.result == 'success'){
+				$.magnificPopup.close();
+				messageSingle('',window.lang.translate('Email Sent Successful'),activeInfo.settings.notifications.position,'#FFF','success','5000');
+			}else{
+				messageSingle('',response.message,activeInfo.settings.notifications.position,'#FFF','error','5000');
+			}
+		}).fail(function(xhr) {
+			OrganizrApiError(xhr);
+		});
+		ajaxloader();
+	}
 }
 function buildUserList(array){
-    var users = '';
-    var htmlDOM = '';
+	var users = '';
+	var htmlDOM = '';
 	$.each(array, function(i,v) {
-        users += '<option value="'+v+'">'+i+'</option>';
-    });
-    htmlDOM = `
-    <select multiple id="email-user-list" name="email-user-list[]">`+users+`</select>
-    <div class="button-box m-t-20">
-        <a id="select-all-users-list" class="btn btn-danger btn-outline" href="#">select all</a>
-        <a id="deselect-all-users-list" class="btn btn-info btn-outline" href="#">deselect all</a>
-        <a id="minimize-users-list" class="btn btn-primary btn-outline" href="#">minimize</a>
-    </div>`;
-    return htmlDOM;
+		users += '<option value="'+v+'">'+i+'</option>';
+	});
+	htmlDOM = `
+	<select multiple id="email-user-list" name="email-user-list[]">`+users+`</select>
+	<div class="button-box m-t-20">
+		<a id="select-all-users-list" class="btn btn-danger btn-outline" href="#">select all</a>
+		<a id="deselect-all-users-list" class="btn btn-info btn-outline" href="#">deselect all</a>
+		<a id="minimize-users-list" class="btn btn-primary btn-outline" href="#">minimize</a>
+	</div>`;
+	return htmlDOM;
 }
 function buildEmailModal(){
-    var htmlDOM = `
-    <div class="row">
-        <div class="col-md-12">
-            <div class="panel panel-info m-0">
-                <div class="panel-heading">
-                    <span lang="en">Email Users</span>
-                    <div class="btn-group pull-right">
-
+	var htmlDOM = `
+	<div class="row">
+		<div class="col-md-12">
+			<div class="panel panel-info m-0">
+				<div class="panel-heading">
+					<span lang="en">Email Users</span>
+					<div class="btn-group pull-right">
 						<button class="btn btn-info waves-effect waves-light loadUserList" type="button">
 							<i class="fa fa-user"></i>
 						</button>
-                        <button class="btn btn-info waves-effect waves-light" type="button" onclick="$('.mce-i-template').trigger('click');">
+						<button class="btn btn-info waves-effect waves-light" type="button" onclick="$('.mce-i-template').trigger('click');">
 							<i class="fa fa-files-o"></i>
 						</button>
-                        <button class="btn btn-info waves-effect waves-light unhide-user-list hidden" type="button">
+						<button class="btn btn-info waves-effect waves-light unhide-user-list hidden" type="button">
 							<i class="fa fa-eye"></i>
 						</button>
 						<button class="btn btn-info waves-effect waves-light" onclick="sendMail();"><i class="fa fa-paper-plane"></i></button>
-
-	                </div>
-                </div>
-                <div class="panel-wrapper collapse in main-email-panel" aria-expanded="true">
-                    <div class="panel-body">
-                        <div class="form-body">
-                            <div class="row">
-                                <div class="col-md-6">
-                                    <div class="form-group">
-                                        <label class="control-label" lang="en">To:</label>
-                                        <input type="text" id="sendEmailToInput" class="form-control"></div>
-                                </div>
-                                <div class="col-md-6">
-                                    <div class="form-group">
-                                        <label class="control-label" lang="en">Subject</label>
-                                        <input type="text" id="sendEmailSubjectInput" class="form-control"></div>
-                                </div>
-                                <div class="col-md-12" id="user-list-div">
-
-
-                                </div>
-                            </div>
-                            <!--/row-->
-                        </div>
-                    </div>
-                </div>
-            </div>
-        </div>
-    </div>
-    <textarea id="sendEmail" name="area"></textarea>
-    `;
-    $('.email-div').html(htmlDOM);
-    if ($("#sendEmail").length > 0) {
-        var templates = [];
-        if(activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-One"] !== ''){
-            templates.push(
-                {
-                    title: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-OneName"],
-                    description: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-OneSubject"],
-                    content: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-One"],
-                }
-            )
-        }
-        if(activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-Two"] !== ''){
-            templates.push(
-                {
-                    title: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-TwoName"],
-                    description: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-TwoSubject"],
-                    content: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-Two"],
-                }
-            )
-        }
-        if(activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-Three"] !== ''){
-            templates.push(
-                {
-                    title: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-ThreeName"],
-                    description: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-ThreeSubject"],
-                    content: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-Three"],
-                }
-            )
-        }
-        if(activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-Four"] !== ''){
-            templates.push(
-                {
-                    title: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-FourName"],
-                    description: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-FourSubject"],
-                    content: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-Four"],
-                }
-            )
-        }
-        tinymce.init({
-            selector: "textarea#sendEmail",
-            theme: "modern",
-            height: 300,
-            plugins: [
-                "advlist autolink link image lists charmap print preview hr anchor pagebreak spellchecker", "searchreplace wordcount visualblocks visualchars code fullscreen insertdatetime media nonbreaking", "save table contextmenu directionality emoticons template paste textcolor"
-            ],
-            toolbar: "insertfile template undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image | print preview media fullpage | forecolor backcolor",
-            templates: templates,
-            init_instance_callback: function (editor) {
-                editor.on('BeforeSetContent', function (e) {
-                    //tinyMCE.get('sendEmail').execCommand('selectAll');
-                    //tinyMCE.get('sendEmail').execCommand('delete');
-                    $.each(e.target.settings.templates, function(i,v) {
-                        if($.trim(v.content) == $.trim(e.content)){
-                            $('#sendEmailSubjectInput').val(v.description);
-                        }
-                    });
-                });
-              }
-        });
-    }
+					</div>
+				</div>
+				<div class="panel-wrapper collapse in main-email-panel" aria-expanded="true">
+					<div class="panel-body">
+						<div class="form-body">
+							<div class="row">
+								<div class="col-md-6">
+									<div class="form-group">
+										<label class="control-label" lang="en">To:</label>
+										<input type="text" id="sendEmailToInput" class="form-control"></div>
+								</div>
+								<div class="col-md-6">
+									<div class="form-group">
+										<label class="control-label" lang="en">Subject</label>
+										<input type="text" id="sendEmailSubjectInput" class="form-control"></div>
+								</div>
+								<div class="col-md-12" id="user-list-div"></div>
+							</div>
+							<!--/row-->
+						</div>
+					</div>
+				</div>
+			</div>
+		</div>
+	</div>
+	<textarea id="sendEmail" name="area"></textarea>
+	`;
+	$('.email-div').html(htmlDOM);
+	if ($("#sendEmail").length > 0) {
+		var templates = [];
+		if(activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-One"] !== ''){
+			templates.push(
+				{
+					title: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-OneName"],
+					description: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-OneSubject"],
+					content: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-One"],
+				}
+			)
+		}
+		if(activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-Two"] !== ''){
+			templates.push(
+				{
+					title: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-TwoName"],
+					description: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-TwoSubject"],
+					content: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-Two"],
+				}
+			)
+		}
+		if(activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-Three"] !== ''){
+			templates.push(
+				{
+					title: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-ThreeName"],
+					description: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-ThreeSubject"],
+					content: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-Three"],
+				}
+			)
+		}
+		if(activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-Four"] !== ''){
+			templates.push(
+				{
+					title: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-FourName"],
+					description: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-FourSubject"],
+					content: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-Four"],
+				}
+			)
+		}
+		tinymce.init({
+			selector: "textarea#sendEmail",
+			theme: "modern",
+			height: 300,
+			plugins: [
+				"advlist autolink link image lists charmap print preview hr anchor pagebreak spellchecker", "searchreplace wordcount visualblocks visualchars code fullscreen insertdatetime media nonbreaking", "save table contextmenu directionality emoticons template paste textcolor"
+			],
+			toolbar: "insertfile template undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image | print preview media fullpage | forecolor backcolor",
+			templates: templates,
+			init_instance_callback: function (editor) {
+				editor.on('BeforeSetContent', function (e) {
+					//tinyMCE.get('sendEmail').execCommand('selectAll');
+					//tinyMCE.get('sendEmail').execCommand('delete');
+					$.each(e.target.settings.templates, function(i,v) {
+						if($.trim(v.content) == $.trim(e.content)){
+							$('#sendEmailSubjectInput').val(v.description);
+						}
+					});
+				});
+			  }
+		});
+	}
 
 }
 // EVENTS and LISTENERS
 $(document).on("change", "#email-user-list", function () {
-    $('#sendEmailToInput').val($('#email-user-list').val());
+	$('#sendEmailToInput').val($('#email-user-list').val());
 });
 $(document).on('click', '.loadUserList', function() {
-    ajaxloader(".content-wrap","in");
-    organizrAPI2('GET','api/v2/plugins/php-mailer/email/list').success(function(data) {
-        var response = data.response;
-        $('#user-list-div').html(buildUserList(response.data));
-        $('#email-user-list').multiSelect();
-    }).fail(function(xhr) {
-	    OrganizrApiError(xhr);
-    });
-    ajaxloader();
+	ajaxloader(".content-wrap","in");
+	organizrAPI2('GET','api/v2/plugins/php-mailer/email/list').success(function(data) {
+		var response = data.response;
+		$('#user-list-div').html(buildUserList(response.data));
+		$('#email-user-list').multiSelect();
+	}).fail(function(xhr) {
+		OrganizrApiError(xhr);
+	});
+	ajaxloader();
 });
 $(document).on("click", ".emailModal", function(e) {
-    buildEmailModal();
+	buildEmailModal();
 });
 $(document).on("click", ".show-login", function(e) {
-    setTimeout(addForgotPassword, 1000);
+	setTimeout(addForgotPassword, 1000);
 });
 $(document).on("click", "#select-all-users-list", function(e) {
-    $('#email-user-list').multiSelect('select_all');
-    return false;
+	$('#email-user-list').multiSelect('select_all');
+	return false;
 });
 $(document).on("click", "#deselect-all-users-list", function(e) {
-    $('#email-user-list').multiSelect('deselect_all');
-    return false;
+	$('#email-user-list').multiSelect('deselect_all');
+	return false;
 });
 $(document).on("click", "#minimize-users-list, .unhide-user-list", function(e) {
-    $('.main-email-panel').toggleClass('hidden');
-    $('.loadUserList').toggleClass('hidden');
-    $('.unhide-user-list').toggleClass('hidden');
-    return false;
+	$('.main-email-panel').toggleClass('hidden');
+	$('.loadUserList').toggleClass('hidden');
+	$('.unhide-user-list').toggleClass('hidden');
+	return false;
 });
 function addForgotPassword(){
-    var item = '';
-    if(activeInfo.plugins["PHPMAILER-enabled"] == true){
-        if (activeInfo.user.loggedin === false) {
-            item = `<a href="javascript:void(0)" id="to-recover" class="text-dark pull-right"><i class="fa fa-lock m-r-5"></i> <span lang="en">Forgot pwd?</span></a>`;
-            $('.remember-me').after(item);
-        }
-    }
+	var item = '';
+	if(activeInfo.plugins["PHPMAILER-enabled"] == true){
+		if (activeInfo.user.loggedin === false) {
+			item = `<a href="javascript:void(0)" id="to-recover" class="text-dark pull-right"><i class="fa fa-lock m-r-5"></i> <span lang="en">Forgot pwd?</span></a>`;
+			$('.remember-me').after(item);
+		}
+	}
 }
-$(document).on('click', '#PHPMAILER-settings-button', function() {
-    ajaxloader(".content-wrap","in");
-    organizrAPI2('GET','api/v2/plugins/php-mailer/settings').success(function(data) {
-        var response = data.response;
-        $('#PHPMAILER-settings-items').html(buildFormGroup(response.data));
-    }).fail(function(xhr) {
-	    OrganizrApiError(xhr);
-    });
-    ajaxloader();
-});
 // SEND TEST EMAIL
 $(document).on('click', '.phpmSendTestEmail', function() {
-    messageSingle('',window.lang.translate('Sending Test E-Mail'),activeInfo.settings.notifications.position,'#FFF','info','5000');
-    ajaxloader(".content-wrap","in");
-    organizrAPI2('GET','api/v2/plugins/php-mailer/email/test').success(function(data) {
-        var response = data.response;
-        if(response.message !== null && response.message.indexOf('|||DEBUG|||') == 0){
-            messageSingle('',window.lang.translate('Press F11 to check Console for output'),activeInfo.settings.notifications.position,'#FFF','warning','5000');
-	        console.warn(response.message);
-        }else if(response.result == 'success') {
-            messageSingle('',window.lang.translate('Email Test Successful'),activeInfo.settings.notifications.position,'#FFF','success','20000');
-        }else{
-            messageSingle('',response.message,activeInfo.settings.notifications.position,'#FFF','error','5000');
-        }
-    }).fail(function(xhr, data) {
-	    OrganizrApiError(xhr, 'Mailer Error');
-    });
-    ajaxloader();
-});
+	messageSingle('',window.lang.translate('Sending Test E-Mail'),activeInfo.settings.notifications.position,'#FFF','info','5000');
+	ajaxloader(".content-wrap","in");
+	organizrAPI2('GET','api/v2/plugins/php-mailer/email/test').success(function(data) {
+		var response = data.response;
+		if(response.message !== null && response.message.indexOf('|||DEBUG|||') == 0){
+			messageSingle('',window.lang.translate('Press F11 to check Console for output'),activeInfo.settings.notifications.position,'#FFF','warning','5000');
+			console.warn(response.message);
+		}else if(response.result == 'success') {
+			messageSingle('',window.lang.translate('Email Test Successful'),activeInfo.settings.notifications.position,'#FFF','success','20000');
+		}else{
+			messageSingle('',response.message,activeInfo.settings.notifications.position,'#FFF','error','5000');
+		}
+	}).fail(function(xhr, data) {
+		OrganizrApiError(xhr, 'Mailer Error');
+	});
+	ajaxloader();
+});

+ 57 - 72
api/plugins/js/speedTest.js

@@ -1,4 +1,7 @@
 /* SPEEDTEST JS FILE */
+$('body').arrive('#activeInfo', {onceOnly: true}, function() {
+	speedTestLaunch();
+});
 function clamp(num, min, max) {
   return num <= min ? min : num >= max ? max : num;
 }
@@ -56,82 +59,64 @@ function initUI(){
 	$('#uploadPercent').attr('class', 'css-bar css-bar-0 css-bar-lg css-bar-warning pull-right').attr('data-label', '0Mbps');
 }
 // FUNCTIONS
-speedTestLaunch()
 function speedTestLaunch(){
-    if(typeof activeInfo == 'undefined'){
-        setTimeout(function () {
-            speedTestLaunch();
-        }, 1000);
-    }else{
-        if(activeInfo.plugins["SPEEDTEST-enabled"] == true){
-            if (activeInfo.user.groupID <= activeInfo.plugins.includes["SPEEDTEST-Auth-include"]) {
-                var menuList = `<li><a class="inline-popups speedTestModal" href="#speedtest-area" data-effect="mfp-zoom-out"><i class="fa fa-rocket fa-fw"></i> <span lang="en">Test Server Speed</span></a></li>`;
-				var htmlDOM = `
-		    	<div id="speedtest-area" class="white-popup mfp-with-anim mfp-hide">
-		    		<div class="col-md-4 col-md-offset-4">
-						<div class="panel bg-org panel-info">
-							<div class="panel-heading">
-								<span lang="en">Test Speed to Server</span>
-								<button id="startStopBtn" onclick="startStop()" class="btn btn-info waves-effect waves-light pull-right"><span lang="en" id="speedTestButtonText">Start</span> <i class="fa fa-rocket m-l-5"></i></button>
-							</div>
-							<div class="panel-body">
-								<div id="test">
-									<div class="row hidden-xs">
-										<div class="col-md-6 col-xs-6"><div id="downloadPercent" data-label="0Mbps" style="font-size: 15px;"></div></div>
-										<div class="col-md-6 col-xs-6"><div id="uploadPercent" data-label="0Mbps" style="font-size: 15px;"></div></div>
+	if(activeInfo.plugins["SPEEDTEST-enabled"] == true){
+		if (activeInfo.user.groupID <= activeInfo.plugins.includes["SPEEDTEST-Auth-include"]) {
+			var menuList = `<li><a class="inline-popups speedTestModal" href="#speedtest-area" data-effect="mfp-zoom-out"><i class="fa fa-rocket fa-fw"></i> <span lang="en">Test Server Speed</span></a></li>`;
+			var htmlDOM = `
+			<div id="speedtest-area" class="white-popup mfp-with-anim mfp-hide">
+				<div class="col-md-4 col-md-offset-4">
+					<div class="panel bg-org panel-info">
+						<div class="panel-heading">
+							<span lang="en">Test Speed to Server</span>
+							<button id="startStopBtn" onclick="startStop()" class="btn btn-info waves-effect waves-light pull-right"><span lang="en" id="speedTestButtonText">Start</span> <i class="fa fa-rocket m-l-5"></i></button>
+						</div>
+						<div class="panel-body">
+							<div id="test">
+								<div class="row hidden-xs">
+									<div class="col-md-6 col-xs-6"><div id="downloadPercent" data-label="0Mbps" style="font-size: 15px;"></div></div>
+									<div class="col-md-6 col-xs-6"><div id="uploadPercent" data-label="0Mbps" style="font-size: 15px;"></div></div>
+								</div>
+								<div class="progress progress-sm">
+									<div id="progress" class="progress-bar progress-bar-info active progress-bar-striped" role="progressbar" aria-valuenow="40" aria-valuemin="0" aria-valuemax="100" style="width: 0%">
+										<span class="sr-only">0% Complete (success)</span>
 									</div>
-									<div class="progress progress-sm">
-										<div id="progress" class="progress-bar progress-bar-info active progress-bar-striped" role="progressbar" aria-valuenow="40" aria-valuemin="0" aria-valuemax="100" style="width: 0%">
-											<span class="sr-only">0% Complete (success)</span>
+								</div>
+								<div class="white-box m-b-0">
+									<div class="user-btm-box">
+										<div class="col-md-3 col-xs-6 p-l-0 p-r-0 text-center">
+											<p class="text-success"><i class="ti-download fa-2x"></i></p>
+											<h1 id="dlText"></h1>
+											<h4 class="">Mbps</h4>
+										</div>
+										<div class="col-md-3 col-xs-6 p-l-0 p-r-0 text-center">
+											<p class="text-warning"><i class="ti-upload fa-2x"></i></p>
+											<h1 id="ulText"></h1>
+											<h4 class="">Mbps</h4>
+										</div>
+										<div class="col-md-3 col-xs-6 p-l-0 p-r-0 text-center">
+											<p class="text-purple"><i class="ti-direction-alt fa-2x"></i></p>
+											<h1 id="pingText"></h1>
+											<h4 class="">ms</h4>
+										</div>
+										<div class="col-md-3 col-xs-6 p-l-0 p-r-0 text-center">
+											<p class="text-info"><i class="ti-pulse fa-2x"></i></p>
+											<h1 id="jitText"></h1>
+											<h4 class="">ms</h4>
 										</div>
 									</div>
-				                    <div class="white-box m-b-0">
-				                        <div class="user-btm-box">
-				                            <div class="col-md-3 col-xs-6 p-l-0 p-r-0 text-center">
-				                                <p class="text-success"><i class="ti-download fa-2x"></i></p>
-				                                <h1 id="dlText"></h1>
-												<h4 class="">Mbps</h4>
-											</div>
-				                            <div class="col-md-3 col-xs-6 p-l-0 p-r-0 text-center">
-				                                <p class="text-warning"><i class="ti-upload fa-2x"></i></p>
-				                                <h1 id="ulText"></h1>
-												<h4 class="">Mbps</h4>
-											</div>
-				                            <div class="col-md-3 col-xs-6 p-l-0 p-r-0 text-center">
-				                                <p class="text-purple"><i class="ti-direction-alt fa-2x"></i></p>
-				                                <h1 id="pingText"></h1>
-												<h4 class="">ms</h4>
-											</div>
-				                            <div class="col-md-3 col-xs-6 p-l-0 p-r-0 text-center">
-				                                <p class="text-info"><i class="ti-pulse fa-2x"></i></p>
-				                                <h1 id="jitText"></h1>
-												<h4 class="">ms</h4>
-											</div>
-				                        </div>
-				                    </div>
 								</div>
-								<script type="text/javascript">initUI();</script>
 							</div>
-							<div class="panel-footer"> IP Address: <span id="ip"></span> </div>
+							<script type="text/javascript">initUI();</script>
 						</div>
-		    		</div>
-		    	</div>
-		    	`;
-				$('.append-menu').after(menuList);
-	            $('.organizr-area').after(htmlDOM);
-	            pageLoad();
-			}
-        }
-    }
-}
-
-$(document).on('click', '#SPEEDTEST-settings-button', function() {
-    ajaxloader(".content-wrap","in");
-    organizrAPI2('GET','api/v2/plugins/speedtest/settings').success(function(data) {
-        var response = data.response;
-        $('#SPEEDTEST-settings-items').html(buildFormGroup(response.data));
-    }).fail(function(xhr) {
-        console.error("Organizr Function: API Connection Failed");
-    });
-    ajaxloader();
-});
+						<div class="panel-footer"> IP Address: <span id="ip"></span> </div>
+					</div>
+				</div>
+			</div>
+			`;
+			$('.append-menu').after(menuList);
+			$('.organizr-area').after(htmlDOM);
+			pageLoad();
+		}
+	}
+}

+ 4 - 2
api/plugins/php-mailer.php

@@ -10,7 +10,9 @@ $GLOBALS['plugins'][]['PHP Mailer'] = array( // Plugin Name
 	'configPrefix' => 'PHPMAILER', // config file prefix for array items without the hyphen
 	'version' => '1.0.0', // SemVer of plugin
 	'image' => 'plugins/images/php-mailer.png', // 1:1 non transparent image for plugin
-	'settings' => true, // does plugin need a settings page? true or false
+	'settings' => true, // does plugin need a settings modal?
+	'bind' => true, // use default bind to make settings page - true or false
+	'api' => 'api/v2/plugins/php-mailer/settings', // api route for settings page
 	'homepage' => false // Is plugin for use on homepage? true or false
 );
 
@@ -535,4 +537,4 @@ class PhpMailer extends Organizr
 			)
 		);
 	}
-}
+}

+ 5 - 3
api/plugins/speedTest.php

@@ -4,13 +4,15 @@ $GLOBALS['plugins'][]['SpeedTest'] = array( // Plugin Name
 	'name' => 'SpeedTest', // Plugin Name
 	'author' => 'CauseFX', // Who wrote the plugin
 	'category' => 'Utilities', // One to Two Word Description
-	'link' => 'https://github.com/PHPMailer/PHPMailer', // Link to plugin info
+	'link' => '', // Link to plugin info
 	'license' => 'personal,business', // License Type use , for multiple
 	'idPrefix' => 'SPEEDTEST', // html element id prefix
 	'configPrefix' => 'SPEEDTEST', // config file prefix for array items without the hypen
 	'version' => '1.0.0', // SemVer of plugin
 	'image' => 'plugins/images/speedtest.png', // 1:1 non transparent image for plugin
-	'settings' => true, // does plugin need a settings page? true or false
+	'settings' => true, // does plugin need a settings modal?
+	'bind' => true, // use default bind to make settings page - true or false
+	'api' => 'api/v2/plugins/speedtest/settings', // api route for settings page
 	'homepage' => false // Is plugin for use on homepage? true or false
 );
 
@@ -30,4 +32,4 @@ class SpeedTest extends Organizr
 			)
 		);
 	}
-}
+}

+ 9 - 0
api/v2/routes/homepage.php

@@ -423,6 +423,15 @@ $app->get('/homepage/jackett/{query}', function ($request, $response, $args) {
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
 });
+$app->post('/homepage/jackett/download/', function ($request, $response, $args) {
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	$postData  = $request->getParsedBody();
+	$Organizr->performJackettBackHoleDownload($postData['url']);
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
 $app->get('/homepage/trakt/calendar', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
 	$Organizr->getTraktCalendar();

+ 1 - 1
api/v2/routes/token.php

@@ -13,7 +13,7 @@ $app->delete('/token/{id}', function ($request, $response, $args) {
 $app->post('/token/validate', function ($request, $response, $args) {
         $Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
         if ($Organizr->qualifyRequest(999, true)) {
-                $GLOBALS['api']['response']['data'] = $Organizr->jwtParse($_REQUEST["Token"]);
+                $GLOBALS['api']['response']['data'] = $Organizr->validateToken($_REQUEST["Token"]);
         }
         $response->getBody()->write(jsonE($GLOBALS['api']));
         return $response

+ 1 - 0
index.php

@@ -230,6 +230,7 @@ $Organizr = new Organizr();
 <script src="js/ua-parser.min.js"></script>
 <script src="js/plyr.js"></script>
 <script src="js/simplebar.js"></script>
+<script src="js/arrive.min.js"></script>
 <script src="https://apis.google.com/js/client.js?onload=googleApiClientReady"></script>
 <script src="js/functions.js?v=<?php echo $Organizr->fileHash; ?>"></script>
 <script src="js/custom.min.js?v=<?php echo $Organizr->fileHash; ?>"></script>

+ 461 - 0
js/arrive.js

@@ -0,0 +1,461 @@
+/*globals jQuery,Window,HTMLElement,HTMLDocument,HTMLCollection,NodeList,MutationObserver */
+/*exported Arrive*/
+/*jshint latedef:false */
+
+/*
+ * arrive.js
+ * v2.4.1
+ * https://github.com/uzairfarooq/arrive
+ * MIT licensed
+ *
+ * Copyright (c) 2014-2017 Uzair Farooq
+ */
+var Arrive = (function(window, $, undefined) {
+
+  "use strict";
+
+  if(!window.MutationObserver || typeof HTMLElement === 'undefined'){
+    return; //for unsupported browsers
+  }
+
+  var arriveUniqueId = 0;
+
+  var utils = (function() {
+    var matches = HTMLElement.prototype.matches || HTMLElement.prototype.webkitMatchesSelector || HTMLElement.prototype.mozMatchesSelector
+                  || HTMLElement.prototype.msMatchesSelector;
+
+    return {
+      matchesSelector: function(elem, selector) {
+        return elem instanceof HTMLElement && matches.call(elem, selector);
+      },
+      // to enable function overloading - By John Resig (MIT Licensed)
+      addMethod: function (object, name, fn) {
+        var old = object[ name ];
+        object[ name ] = function(){
+          if ( fn.length == arguments.length ) {
+            return fn.apply( this, arguments );
+          }
+          else if ( typeof old == 'function' ) {
+            return old.apply( this, arguments );
+          }
+        };
+      },
+      callCallbacks: function(callbacksToBeCalled, registrationData) {
+        if (registrationData && registrationData.options.onceOnly && registrationData.firedElems.length == 1) {
+          // as onlyOnce param is true, make sure we fire the event for only one item
+          callbacksToBeCalled = [callbacksToBeCalled[0]];
+        }
+
+        for (var i = 0, cb; (cb = callbacksToBeCalled[i]); i++) {
+          if (cb && cb.callback) {
+            cb.callback.call(cb.elem, cb.elem);
+          }
+        }
+
+        if (registrationData && registrationData.options.onceOnly && registrationData.firedElems.length == 1) {
+          // unbind event after first callback as onceOnly is true.
+          registrationData.me.unbindEventWithSelectorAndCallback.call(
+            registrationData.target, registrationData.selector, registrationData.callback);
+        }
+      },
+      // traverse through all descendants of a node to check if event should be fired for any descendant
+      checkChildNodesRecursively: function(nodes, registrationData, matchFunc, callbacksToBeCalled) {
+        // check each new node if it matches the selector
+        for (var i=0, node; (node = nodes[i]); i++) {
+          if (matchFunc(node, registrationData, callbacksToBeCalled)) {
+            callbacksToBeCalled.push({ callback: registrationData.callback, elem: node });
+          }
+
+          if (node.childNodes.length > 0) {
+            utils.checkChildNodesRecursively(node.childNodes, registrationData, matchFunc, callbacksToBeCalled);
+          }
+        }
+      },
+      mergeArrays: function(firstArr, secondArr){
+        // Overwrites default options with user-defined options.
+        var options = {},
+            attrName;
+        for (attrName in firstArr) {
+          if (firstArr.hasOwnProperty(attrName)) {
+            options[attrName] = firstArr[attrName];
+          }
+        }
+        for (attrName in secondArr) {
+          if (secondArr.hasOwnProperty(attrName)) {
+            options[attrName] = secondArr[attrName];
+          }
+        }
+        return options;
+      },
+      toElementsArray: function (elements) {
+        // check if object is an array (or array like object)
+        // Note: window object has .length property but it's not array of elements so don't consider it an array
+        if (typeof elements !== "undefined" && (typeof elements.length !== "number" || elements === window)) {
+          elements = [elements];
+        }
+        return elements;
+      }
+    };
+  })();
+
+
+  // Class to maintain state of all registered events of a single type
+  var EventsBucket = (function() {
+    var EventsBucket = function() {
+      // holds all the events
+
+      this._eventsBucket    = [];
+      // function to be called while adding an event, the function should do the event initialization/registration
+      this._beforeAdding    = null;
+      // function to be called while removing an event, the function should do the event destruction
+      this._beforeRemoving  = null;
+    };
+
+    EventsBucket.prototype.addEvent = function(target, selector, options, callback) {
+      var newEvent = {
+        target:             target,
+        selector:           selector,
+        options:            options,
+        callback:           callback,
+        firedElems:         []
+      };
+
+      if (this._beforeAdding) {
+        this._beforeAdding(newEvent);
+      }
+
+      this._eventsBucket.push(newEvent);
+      return newEvent;
+    };
+
+    EventsBucket.prototype.removeEvent = function(compareFunction) {
+      for (var i=this._eventsBucket.length - 1, registeredEvent; (registeredEvent = this._eventsBucket[i]); i--) {
+        if (compareFunction(registeredEvent)) {
+          if (this._beforeRemoving) {
+              this._beforeRemoving(registeredEvent);
+          }
+
+          // mark callback as null so that even if an event mutation was already triggered it does not call callback
+          var removedEvents = this._eventsBucket.splice(i, 1);
+          if (removedEvents && removedEvents.length) {
+            removedEvents[0].callback = null;
+          }
+        }
+      }
+    };
+
+    EventsBucket.prototype.beforeAdding = function(beforeAdding) {
+      this._beforeAdding = beforeAdding;
+    };
+
+    EventsBucket.prototype.beforeRemoving = function(beforeRemoving) {
+      this._beforeRemoving = beforeRemoving;
+    };
+
+    return EventsBucket;
+  })();
+
+
+  /**
+   * @constructor
+   * General class for binding/unbinding arrive and leave events
+   */
+  var MutationEvents = function(getObserverConfig, onMutation) {
+    var eventsBucket    = new EventsBucket(),
+        me              = this;
+
+    var defaultOptions = {
+      fireOnAttributesModification: false
+    };
+
+    // actual event registration before adding it to bucket
+    eventsBucket.beforeAdding(function(registrationData) {
+      var
+        target    = registrationData.target,
+        observer;
+
+      // mutation observer does not work on window or document
+      if (target === window.document || target === window) {
+        target = document.getElementsByTagName("html")[0];
+      }
+
+      // Create an observer instance
+      observer = new MutationObserver(function(e) {
+        onMutation.call(this, e, registrationData);
+      });
+
+      var config = getObserverConfig(registrationData.options);
+
+      observer.observe(target, config);
+
+      registrationData.observer = observer;
+      registrationData.me = me;
+    });
+
+    // cleanup/unregister before removing an event
+    eventsBucket.beforeRemoving(function (eventData) {
+      eventData.observer.disconnect();
+    });
+
+    this.bindEvent = function(selector, options, callback) {
+      options = utils.mergeArrays(defaultOptions, options);
+
+      var elements = utils.toElementsArray(this);
+
+      for (var i = 0; i < elements.length; i++) {
+        eventsBucket.addEvent(elements[i], selector, options, callback);
+      }
+    };
+
+    this.unbindEvent = function() {
+      var elements = utils.toElementsArray(this);
+      eventsBucket.removeEvent(function(eventObj) {
+        for (var i = 0; i < elements.length; i++) {
+          if (this === undefined || eventObj.target === elements[i]) {
+            return true;
+          }
+        }
+        return false;
+      });
+    };
+
+    this.unbindEventWithSelectorOrCallback = function(selector) {
+      var elements = utils.toElementsArray(this),
+          callback = selector,
+          compareFunction;
+
+      if (typeof selector === "function") {
+        compareFunction = function(eventObj) {
+          for (var i = 0; i < elements.length; i++) {
+            if ((this === undefined || eventObj.target === elements[i]) && eventObj.callback === callback) {
+              return true;
+            }
+          }
+          return false;
+        };
+      }
+      else {
+        compareFunction = function(eventObj) {
+          for (var i = 0; i < elements.length; i++) {
+            if ((this === undefined || eventObj.target === elements[i]) && eventObj.selector === selector) {
+              return true;
+            }
+          }
+          return false;
+        };
+      }
+      eventsBucket.removeEvent(compareFunction);
+    };
+
+    this.unbindEventWithSelectorAndCallback = function(selector, callback) {
+      var elements = utils.toElementsArray(this);
+      eventsBucket.removeEvent(function(eventObj) {
+          for (var i = 0; i < elements.length; i++) {
+            if ((this === undefined || eventObj.target === elements[i]) && eventObj.selector === selector && eventObj.callback === callback) {
+              return true;
+            }
+          }
+          return false;
+      });
+    };
+
+    return this;
+  };
+
+
+  /**
+   * @constructor
+   * Processes 'arrive' events
+   */
+  var ArriveEvents = function() {
+    // Default options for 'arrive' event
+    var arriveDefaultOptions = {
+      fireOnAttributesModification: false,
+      onceOnly: false,
+      existing: false
+    };
+
+    function getArriveObserverConfig(options) {
+      var config = {
+        attributes: false,
+        childList: true,
+        subtree: true
+      };
+
+      if (options.fireOnAttributesModification) {
+        config.attributes = true;
+      }
+
+      return config;
+    }
+
+    function onArriveMutation(mutations, registrationData) {
+      mutations.forEach(function( mutation ) {
+        var newNodes    = mutation.addedNodes,
+            targetNode = mutation.target,
+            callbacksToBeCalled = [],
+            node;
+
+        // If new nodes are added
+        if( newNodes !== null && newNodes.length > 0 ) {
+          utils.checkChildNodesRecursively(newNodes, registrationData, nodeMatchFunc, callbacksToBeCalled);
+        }
+        else if (mutation.type === "attributes") {
+          if (nodeMatchFunc(targetNode, registrationData, callbacksToBeCalled)) {
+            callbacksToBeCalled.push({ callback: registrationData.callback, elem: targetNode });
+          }
+        }
+
+        utils.callCallbacks(callbacksToBeCalled, registrationData);
+      });
+    }
+
+    function nodeMatchFunc(node, registrationData, callbacksToBeCalled) {
+      // check a single node to see if it matches the selector
+      if (utils.matchesSelector(node, registrationData.selector)) {
+        if(node._id === undefined) {
+          node._id = arriveUniqueId++;
+        }
+        // make sure the arrive event is not already fired for the element
+        if (registrationData.firedElems.indexOf(node._id) == -1) {
+          registrationData.firedElems.push(node._id);
+
+          return true;
+        }
+      }
+
+      return false;
+    }
+
+    arriveEvents = new MutationEvents(getArriveObserverConfig, onArriveMutation);
+
+    var mutationBindEvent = arriveEvents.bindEvent;
+
+    // override bindEvent function
+    arriveEvents.bindEvent = function(selector, options, callback) {
+
+      if (typeof callback === "undefined") {
+        callback = options;
+        options = arriveDefaultOptions;
+      } else {
+        options = utils.mergeArrays(arriveDefaultOptions, options);
+      }
+
+      var elements = utils.toElementsArray(this);
+
+      if (options.existing) {
+        var existing = [];
+
+        for (var i = 0; i < elements.length; i++) {
+          var nodes = elements[i].querySelectorAll(selector);
+          for (var j = 0; j < nodes.length; j++) {
+            existing.push({ callback: callback, elem: nodes[j] });
+          }
+        }
+
+        // no need to bind event if the callback has to be fired only once and we have already found the element
+        if (options.onceOnly && existing.length) {
+          return callback.call(existing[0].elem, existing[0].elem);
+        }
+
+        setTimeout(utils.callCallbacks, 1, existing);
+      }
+
+      mutationBindEvent.call(this, selector, options, callback);
+    };
+
+    return arriveEvents;
+  };
+
+
+  /**
+   * @constructor
+   * Processes 'leave' events
+   */
+  var LeaveEvents = function() {
+    // Default options for 'leave' event
+    var leaveDefaultOptions = {};
+
+    function getLeaveObserverConfig() {
+      var config = {
+        childList: true,
+        subtree: true
+      };
+
+      return config;
+    }
+
+    function onLeaveMutation(mutations, registrationData) {
+      mutations.forEach(function( mutation ) {
+        var removedNodes  = mutation.removedNodes,
+            callbacksToBeCalled = [];
+
+        if( removedNodes !== null && removedNodes.length > 0 ) {
+          utils.checkChildNodesRecursively(removedNodes, registrationData, nodeMatchFunc, callbacksToBeCalled);
+        }
+
+        utils.callCallbacks(callbacksToBeCalled, registrationData);
+      });
+    }
+
+    function nodeMatchFunc(node, registrationData) {
+      return utils.matchesSelector(node, registrationData.selector);
+    }
+
+    leaveEvents = new MutationEvents(getLeaveObserverConfig, onLeaveMutation);
+
+    var mutationBindEvent = leaveEvents.bindEvent;
+
+    // override bindEvent function
+    leaveEvents.bindEvent = function(selector, options, callback) {
+
+      if (typeof callback === "undefined") {
+        callback = options;
+        options = leaveDefaultOptions;
+      } else {
+        options = utils.mergeArrays(leaveDefaultOptions, options);
+      }
+
+      mutationBindEvent.call(this, selector, options, callback);
+    };
+
+    return leaveEvents;
+  };
+
+
+  var arriveEvents = new ArriveEvents(),
+      leaveEvents  = new LeaveEvents();
+
+  function exposeUnbindApi(eventObj, exposeTo, funcName) {
+    // expose unbind function with function overriding
+    utils.addMethod(exposeTo, funcName, eventObj.unbindEvent);
+    utils.addMethod(exposeTo, funcName, eventObj.unbindEventWithSelectorOrCallback);
+    utils.addMethod(exposeTo, funcName, eventObj.unbindEventWithSelectorAndCallback);
+  }
+
+  /*** expose APIs ***/
+  function exposeApi(exposeTo) {
+    exposeTo.arrive = arriveEvents.bindEvent;
+    exposeUnbindApi(arriveEvents, exposeTo, "unbindArrive");
+
+    exposeTo.leave = leaveEvents.bindEvent;
+    exposeUnbindApi(leaveEvents, exposeTo, "unbindLeave");
+  }
+
+  if ($) {
+    exposeApi($.fn);
+  }
+  exposeApi(HTMLElement.prototype);
+  exposeApi(NodeList.prototype);
+  exposeApi(HTMLCollection.prototype);
+  exposeApi(HTMLDocument.prototype);
+  exposeApi(Window.prototype);
+
+  var Arrive = {};
+  // expose functions to unbind all arrive/leave events
+  exposeUnbindApi(arriveEvents, Arrive, "unbindAllArrive");
+  exposeUnbindApi(leaveEvents, Arrive, "unbindAllLeave");
+
+  return Arrive;
+
+})(window, typeof jQuery === 'undefined' ? null : jQuery, undefined);

File diff suppressed because it is too large
+ 9 - 0
js/arrive.min.js


+ 30 - 26
js/custom.js

@@ -1074,7 +1074,7 @@ $(document).on("click", ".savecustomHTMLtwoTextarea", function () {
     $('.customHTMLtwoTextarea').val(customHTMLtwoEditor.getValue()).trigger('change');
 });
 
-$(document).on('focusout', 'input.pick-a-color', function(e) {
+$(document).on('focusout', 'input.pick-a-color-custom-options', function(e) {
     var original = $(this).attr('data-original');
     var newValue = $(this).val();
     if((original !== newValue) && (newValue !== '#987654') && newValue !== ''){
@@ -1669,30 +1669,6 @@ Mousetrap.bind('ctrl+shift+down', function(e) {
     nextTab.trigger("click");
     return false;
 });
-$(document).on('change', "#new-tab-form-chooseImage", function (e) {
-    var newIcon = $('#new-tab-form-chooseImage').val();
-    if(newIcon !== 'Select or type Icon'){
-        $('#new-tab-form-inputImageNew').val(newIcon);
-    }
-});
-$(document).on('change', "#edit-tab-form-chooseImage", function (e) {
-    var newIcon = $('#edit-tab-form-chooseImage').val();
-    if(newIcon !== 'Select or type Icon'){
-        $('#edit-tab-form-inputImage').val(newIcon);
-    }
-});
-$(document).on('change', "#new-tab-form-chooseIcon", function (e) {
-    var newIcon = $('#new-tab-form-chooseIcon').val();
-    if(newIcon !== 'Select or type Icon'){
-        $('#new-tab-form-inputImageNew').val(newIcon);
-    }
-});
-$(document).on('change', "#edit-tab-form-chooseIcon", function (e) {
-    var newIcon = $('#edit-tab-form-chooseIcon').val();
-    if(newIcon !== 'Select or type Icon'){
-        $('#edit-tab-form-inputImage').val(newIcon);
-    }
-});
 $(document).on('change', "#choose-calender-filter, #choose-calender-filter-status", function (e) {
     filter = $('#choose-calender-filter').val();
     filterDownload = $('#choose-calender-filter-status').val();
@@ -1896,4 +1872,32 @@ function checkMetadataDiv(target,type,classList){
 			});
 		}
 	});
-}
+}
+
+// Plugins settings bind
+$(document).on('click', '[id$=-settings-button]', function() {
+	let el = $(this)[0];
+	let bind = $(el).attr('data-bind');
+	let api = $(el).attr('data-api');
+	let prefix = $(el).attr('data-config-prefix');
+	if(bind == 'true' && api !== 'false' && prefix !== 'false'){
+		ajaxloader(".content-wrap","in");
+		organizrAPI2('GET',api).success(function(data) {
+			var response = data.response;
+			$('#'+prefix+'-settings-items').html(buildFormGroup(response.data));
+		}).fail(function(xhr) {
+			OrganizrApiError(xhr);
+		});
+		ajaxloader();
+	}
+});
+$(document).on('change', '[id*=-form-chooseI]', function (e) {
+	let el = $(this)[0];
+	let id = $(el).attr('id');
+	let newForm = (id.includes('new')) ? 'New' : '';
+	let pasteId = id.match(/(?:[a-z]*-){1,5}/) + 'inputImage' + newForm;
+	let newValue = $('#'+id).val();
+	if(newValue !== 'Select or type Icon'){
+		$('#'+pasteId).val(newValue);
+	}
+});

File diff suppressed because it is too large
+ 0 - 0
js/custom.min.js


+ 25 - 7
js/functions.js

@@ -669,14 +669,14 @@ function switchTab(tab, type, split = null){
 				newTab.addClass("show").removeClass('hidden');
                 setTabInfo(cleanClass(tab),'active',true);
 			}else{
-				$("#preloader").fadeIn();
+				//$("#preloader").fadeIn();
 				organizrConsole('Tab Function','Loading new tab for: '+tab);
 				$('#menu-'+tab+' a').children().addClass('tabLoaded');
 				newTab.addClass("show loaded").removeClass('hidden');
 				loadInternal(tabURL,cleanClass(tab), split);
                 setTabInfo(cleanClass(tab),'active',true);
                 setTabInfo(cleanClass(tab),'loaded',true);
-				$("#preloader").fadeOut();
+				//$("#preloader").fadeOut();
 			}
 			break;
 		case 1:
@@ -824,7 +824,7 @@ function reloadTab(tab, type){
 	$("#preloader").fadeOut();
 }
 function reloadCurrentTab(){
-	$("#preloader").fadeIn();
+	//$("#preloader").fadeIn();
 	organizrConsole('Tab Function','Reloading Current tab');
 	var iframe = $('.iFrame-listing').find('.show');
 	var internal = $('.internal-listing').find('.show');
@@ -864,7 +864,7 @@ function reloadCurrentTab(){
 		default:
 			console.error('Tab Function: Action not set');
 	}
-	$("#preloader").fadeOut();
+	//$("#preloader").fadeOut();
 }
 function loadNextTab(){
 	var next = $('#page-wrapper').find('.loaded').attr('data-name');
@@ -1156,7 +1156,7 @@ function buildPluginsItem(array){
 		var href = (v.settings == true) ? '#'+v.idPrefix+'-settings-page' : 'javascript:void(0);';
 		if(v.enabled == true){
 			var activeToggle = `<li><a class="btn default btn-outline disablePlugin" href="javascript:void(0);" data-plugin-name="`+v.name+`" data-config-prefix="`+v.configPrefix+`" data-config-name="`+v.configPrefix+`-enabled"><i class="ti-power-off fa-2x"></i></a></li>`;
-			var settings = `<li><a class="btn default btn-outline popup-with-form" href="`+href+`" data-effect="mfp-3d-unfold"data-plugin-name="`+v.name+`" id="`+v.idPrefix+`-settings-button" data-config-prefix="`+v.configPrefix+`"><i class="ti-panel fa-2x"></i></a></li>`;
+			var settings = `<li><a class="btn default btn-outline popup-with-form" href="`+href+`" data-effect="mfp-3d-unfold"data-plugin-name="`+v.name+`" id="`+v.idPrefix+`-settings-button" data-config-prefix="`+v.configPrefix+`" data-api="${v.api}" data-settings="${v.settings}" data-bind="${v.bind}"><i class="ti-panel fa-2x"></i></a></li>`;
 		}else{
 			var activeToggle = `<li><a class="btn default btn-outline enablePlugin" href="javascript:void(0);" data-plugin-name="`+v.name+`" data-config-prefix="`+v.configPrefix+`" data-config-name="`+v.configPrefix+`-enabled"><i class="ti-plug fa-2x"></i></a></li>`;
 			var settings = '';
@@ -1966,7 +1966,7 @@ function buildCustomizeAppearance(){
             $('.javaThemeTextarea').val(javaThemeEditor.getValue());
             $('#customize-appearance-form-save').removeClass('hidden');
         });
-		$("input.pick-a-color").ColorPickerSliders({
+		$("input.pick-a-color-custom-options").ColorPickerSliders({
 			placement: 'bottom',
 			color: '#987654',
 			hsvpanel: true,
@@ -8531,6 +8531,7 @@ function searchJackett(){
 	}
 	$.fn.dataTable.ext.errMode = 'none';
 	$('#jackettDataTable').DataTable().destroy();
+	let preferBlackholeDownload = activeInfo.settings.homepage.jackett.homepageJackettBackholeDownload;
 	let jackettTable = $("#jackettDataTable")
 		.on( 'error.dt', function ( e, settings, techNote, message ) {
 			console.log( 'An error has been reported by DataTables: ', message );
@@ -8579,7 +8580,9 @@ function searchJackett(){
 				{ data: 'MagnetUri',
 					render: function ( data, type, row ) {
 						if ( type === 'display' || type === 'filter' ) {
-							if(data !== null){
+							if(preferBlackholeDownload === true && row.BlackholeLink !== null){
+								return '<a onclick="jackettDownload(\''+row.BlackholeLink+'\');return false;" href="#"><i class="fa fa-cloud-download"></i></a>';
+							}else if(data !== null){
 								return '<a href="'+data+'" target="_blank"><i class="fa fa-magnet"></i></a>';
 							}else if(row.Details !== null){
 								return '<a href="'+row.Details+'" target="_blank"><i class="fa fa-cloud-download"></i></a>';
@@ -8604,6 +8607,19 @@ function searchJackett(){
 		} );
 
 }
+function jackettDownload(url) {
+	let blackholeLink=url.substring(url.indexOf("/bh/"));
+	var post = {
+		url: blackholeLink
+	};
+	organizrAPI2('POST', 'api/v2/homepage/jackett/download/', post, true)
+		.success(function() {
+			message('Torrent downloaded','',activeInfo.settings.notifications.position,"#FFF","success","5000");
+		})
+		.fail(function(xhr) {
+			OrganizrApiError(xhr, 'Error downloading torrent');
+		});
+}
 function homepageOctoprint(timeout){
     var timeout = (typeof timeout !== 'undefined') ? timeout : activeInfo.settings.homepage.refresh.homepageOctoprintRefresh;
     organizrAPI2('GET','api/v2/homepage/octoprint/data').success(function(data) {
@@ -10562,6 +10578,8 @@ function launch(){
 		        style:json.data.style,
 		        version:json.data.version
 	        };
+	        // Add element to signal activeInfo Ready
+	        $('#wrapper').after('<div id="activeInfo"></div>');
 	        console.info("%c Organizr %c ".concat(currentVersion, " "), "color: white; background: #66D9EF; font-weight: 700; font-size: 24px; font-family: Monospace;", "color: #66D9EF; background: white; font-weight: 700; font-size: 24px; font-family: Monospace;");
 	        console.info("%c Status %c ".concat("Starting Up...", " "), "color: white; background: #F92671; font-weight: 700;", "color: #F92671; background: white; font-weight: 700;");
 	        local('set','initial',true);

+ 8 - 1
js/version.json

@@ -336,10 +336,17 @@
     "notes": "This is a quick hotfix"
   },
   "2.1.195": {
-    "date": "2021-02-01 20:00",
+    "date": "2021-02-12 20:00",
     "title": "Weekly Update",
     "new": "ignore cert to transmission (#1177)|ignore cert to qbit (#1177)|no tabs api function|log functions",
     "fixed": "Jellyfin SSO fails behind reverse proxy (#1585)|Unable to use \"@\" in username during initial account setup (#1584)|Re-add JWT claims removed from token (#1577)|heart on dependency|refresh to add new user|organizr-area css",
     "notes": "remove spinner css classes"
+  },
+  "2.1.235": {
+    "date": "2021-02-26 19:30",
+    "title": "Weekly Update - kinda",
+    "new": "Bookmark plugin - leet1994|Added option to use blackhole download for Jackett homepage component (#1600)|Petio and Bookstack Images|Prometheus icon|arrive framework js|icon and image select to categories|consolidate binding of new image and new icon for all events|add new div for activeInfo being ready|settings changes to plugins array - now includes settings - bind - api path|change pluginFiles function to include option to include settings js file|update pluginFiles function",
+    "fixed": "Fix username in overseerr SSO logs (#1599)|Update /token/validate API endpoint (#1597)|Update Overseerr Plex auth endpoint|update all plugin js files|update bookmark php file for new template|new 2 bookmark endpoints for setup|update bookmark.png|update bookmark plugin to include setup function|visual changes to bookmark settings panel",
+    "notes": "remove preload spinner on reload"
   }
 }

BIN
plugins/images/bookmark.png


BIN
plugins/images/tabs/bookstack.png


BIN
plugins/images/tabs/petio.png


BIN
plugins/images/tabs/prometheus.png


Some files were not shown because too many files changed in this diff