Преглед изворни кода

Merge pull request #1539 from causefx/v2-develop

V2 develop
causefx пре 5 година
родитељ
комит
de49712860

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

@@ -58,7 +58,7 @@ class Organizr
 	
 	// ===================================
 	// Organizr Version
-	public $version = '2.1.83';
+	public $version = '2.1.120';
 	// ===================================
 	// Quick php Version check
 	public $minimumPHP = '7.2';
@@ -2302,6 +2302,22 @@ class Organizr
 					'label' => 'Enable',
 					'value' => $this->config['ssoOmbi']
 				)
+			),
+			'Jellyfin' => array(
+				array(
+					'type' => 'input',
+					'name' => 'jellyfinURL',
+					'label' => 'Jellyfin URL',
+					'value' => $this->config['jellyfinURL'],
+					'help' => 'Please make sure to use the same (sub)domain to access Jellyfin as Organizr\'s',
+					'placeholder' => 'http(s)://hostname:port'
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'ssoJellyfin',
+					'label' => 'Enable',
+					'value' => $this->config['ssoJellyfin']
+				)
 			)
 		);
 	}
@@ -2663,7 +2679,7 @@ class Organizr
 			'order' => 1,
 			'category' => 'Unsorted',
 			'category_id' => 0,
-			'image' => 'plugins/images/categories/unsorted.png',
+			'image' => 'fontawesome::question',
 			'default' => true
 		];
 		$response = [
@@ -3819,7 +3835,7 @@ class Organizr
 	{
 		$items = $this->getSettingsHomepage();
 		foreach ($items as $k => $v) {
-			if ($v['name'] === $item) {
+			if (strtolower($v['name']) === strtolower($item)) {
 				return $v;
 			}
 		}
@@ -4181,6 +4197,19 @@ class Organizr
 		return $newData;
 	}
 	
+	public function getTabByIdCheckUser($id)
+	{
+		$tabInfo = $this->getTabById($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 = [
@@ -5132,7 +5161,7 @@ class Organizr
 			$this->setAPIResponse('error', 'Id was not supplied', 422);
 			return false;
 		}
-		if ($id !== $this->user['userID']) {
+		if ((int)$id !== $this->user['userID']) {
 			if (!$this->qualifyRequest('1', true)) {
 				return false;
 			}
@@ -5170,8 +5199,9 @@ class Organizr
 		}
 		if (array_key_exists('group_id', $array)) {
 			if ($array['group_id'] == '') {
-				$this->setAPIResponse('error', 'group_id was set but empty', 409);
-				return false;
+				$array['group_id'] = 0;
+				//$this->setAPIResponse('error', 'group_id was set but empty', 409);
+				//return false;
 			}
 			if (!$this->qualifyRequest('1', false)) {
 				$this->setAPIResponse('error', 'Cannot change your own group_id', 401);
@@ -5748,6 +5778,7 @@ class Organizr
 			$url = $this->cleanPath($url);
 			$options = ($this->localURL($url)) ? array('verify' => false) : array();
 			$headers = [];
+			$apiData = $this->json_validator($this->apiData($requestObject)) ? json_encode($this->apiData($requestObject)) : $this->apiData($requestObject);
 			if ($header) {
 				if ($requestObject->hasHeader($header)) {
 					$headerKey = $requestObject->getHeaderLine($header);
@@ -5759,13 +5790,13 @@ class Organizr
 					$call = Requests::get($url, $headers, $options);
 					break;
 				case 'POST':
-					$call = Requests::post($url, $headers, $this->apiData($requestObject), $options);
+					$call = Requests::post($url, $headers, $apiData, $options);
 					break;
 				case 'DELETE':
 					$call = Requests::delete($url, $headers, $options);
 					break;
 				case 'PUT':
-					$call = Requests::put($url, $headers, $this->apiData($requestObject), $options);
+					$call = Requests::put($url, $headers, $apiData, $options);
 					break;
 				default:
 					$call = Requests::get($url, $headers, $options);
@@ -5863,6 +5894,8 @@ class Organizr
 						$results[$keyName] = $query->fetchAll();
 						break;
 					case 'fetch':
+						// PHP 8 Fix?
+						$query->setRowClass(null);
 						$results[$keyName] = $query->fetch();
 						break;
 					case 'getAffectedRows':
@@ -5872,6 +5905,8 @@ class Organizr
 						$results[$keyName] = $query->getRowCount();
 						break;
 					case 'fetchSingle':
+						// PHP 8 Fix?
+						$query->setRowClass(null);
 						$results[$keyName] = $query->fetchSingle();
 						break;
 					case 'query':
@@ -5888,4 +5923,4 @@ class Organizr
 		return count($request) > 1 ? $results : $results[$firstKey];
 	}
 	
-}
+}

+ 5 - 0
api/config/default.php

@@ -56,6 +56,7 @@ return array(
 	'ssoPlex' => false,
 	'ssoOmbi' => false,
 	'ssoTautulli' => false,
+	'ssoJellyfin' => false,
 	'sonarrURL' => '',
 	'sonarrUnmonitored' => false,
 	'sonarrToken' => '',
@@ -221,12 +222,15 @@ return array(
 	'homepageOrderJackett' => '31',
 	'homepageShowStreamNames' => false,
 	'homepageShowStreamNamesAuth' => '1',
+	'homepageUseCustomStreamNames' => false,
+	'homepageCustomStreamNames' => '',
 	'homepageStreamRefresh' => '60000',
 	'homepageRecentRefresh' => '60000',
 	'homepageDownloadRefresh' => '60000',
 	'homepageHealthChecksRefresh' => '60000',
 	'homepagePlexStreams' => false,
 	'homepagePlexStreamsAuth' => '1',
+	'homepagePlexStreamsExclude' => '',
 	'homepagePlexRecent' => false,
 	'homepageRecentLimit' => '50',
 	'homepagePlexRecentAuth' => '1',
@@ -341,6 +345,7 @@ return array(
 	'tautulliPopularTV' => true,
 	'tautulliHeader' => 'Tautulli',
 	'tautulliHeaderToggle' => true,
+	'tautulliFriendlyName' => true,
 	'homepagePiholeEnabled' => false,
 	'homepagePiholeAuth' => '1',
 	'homepagePiholeRefresh' => '10000',

+ 2 - 2
api/functions/auth-functions.php

@@ -434,7 +434,7 @@ trait AuthFunctions
 	public function plugin_auth_jellyfin($username, $password)
 	{
 		try {
-			$url = $this->qualifyURL($this->config['embyURL']) . '/Users/authenticatebyname';
+			$url = $this->qualifyURL($this->config['jellyfinURL']) . '/Users/authenticatebyname';
 			$headers = array(
 				'X-Emby-Authorization' => 'MediaBrowser Client="Organizr Auth", Device="Organizr", DeviceId="orgv2", Version="2.0"',
 				'Content-Type' => 'application/json',
@@ -453,7 +453,7 @@ trait AuthFunctions
 						'X-Emby-Authorization' => 'MediaBrowser Client="Organizr Auth", Device="Organizr", DeviceId="orgv2", Version="2.0", Token="' . $json['AccessToken'] . '"',
 						'Content-Type' => 'application/json',
 					);
-					$response = Requests::post($this->qualifyURL($this->config['embyURL']) . '/Sessions/Logout', $headers, array());
+					$response = Requests::post($this->qualifyURL($this->config['jellyfinURL']) . '/Sessions/Logout', $headers, array());
 					if ($response->success) {
 						return true;
 					}

+ 11 - 1
api/functions/normal-functions.php

@@ -477,7 +477,8 @@ trait NormalFunctions
 				$domain = $_SERVER['HTTP_HOST'];
 			}
 		}
-		$path = (str_replace("\\", "/", dirname($_SERVER['REQUEST_URI'])) !== '.') ?? '';
+		$path = str_replace("\\", "/", dirname($_SERVER['REQUEST_URI']));
+		$path = ($path !== '.') ? $path : '';
 		$url = $protocol . $domain . $path;
 		if (strpos($url, '/api') !== false) {
 			$url = explode('/api', $url);
@@ -547,6 +548,15 @@ trait NormalFunctions
 		$factor = floor((strlen($bytes) - 1) / 3);
 		return sprintf("%.{$dec}f %s", $bytes / (1024 ** $factor), $size[$factor]);
 	}
+	
+	public function json_validator($data = null)
+	{
+		if (!empty($data)) {
+			@json_decode($data);
+			return (json_last_error() === JSON_ERROR_NONE);
+		}
+		return false;
+	}
 }
 
 // Leave for deluge class

+ 1 - 1
api/functions/organizr-functions.php

@@ -336,7 +336,7 @@ trait OrganizrFunctions
 			$buttons .= '<button class="btn m-b-20 m-r-20 bg-primary text-muted waves-effect waves-light importUsersButton" onclick="importUsers(\'jellyfin\')" type="button"><span class="btn-label"><i class="mdi mdi-fish"></i></span><span lang="en">Import Jellyfin Users</span></button>';
 		}
 		if (!empty($this->config['embyURL']) && !empty($this->config['embyToken'])) {
-			$buttons .= '<button class="btn m-b-20 m-r-20 bg-emby text-muted waves-effect waves-light importUsersButton" onclick="importUsers(\'emby\')" type="button"><span class="btn-label"><i class="mdi mdi-emby"></i></span><span lang="en">Import Jellyfin Users</span></button>';
+			$buttons .= '<button class="btn m-b-20 m-r-20 bg-emby text-muted waves-effect waves-light importUsersButton" onclick="importUsers(\'emby\')" type="button"><span class="btn-label"><i class="mdi mdi-emby"></i></span><span lang="en">Import Emby Users</span></button>';
 		}
 		return ($buttons !== '') ? $buttons : $emptyButtons;
 	}

+ 38 - 1
api/functions/sso-functions.php

@@ -23,9 +23,46 @@ trait SSOFunctions
 				}
 			}
 		}
+		if ($this->config['ssoJellyfin']) {
+			$jellyfinToken = $this->getJellyfinToken($username, $password);
+			if ($jellyfinToken) {
+				$this->coookie('set', 'jellyfin_credentials', $jellyfinToken, $this->config['rememberMeDays'], false);
+				$this->writeLog('success', 'ITTATOKEN: ' . $jellyfinToken);
+			}
+		}
 		return true;
 	}
-	
+
+	public function getJellyfinToken($username, $password)
+	{
+		$token = null;
+		try {
+			$url = $this->qualifyURL($this->config['jellyfinURL']);
+			$headers = array(
+				'X-Emby-Authorization' => 'MediaBrowser Client="Organizr Jellyfin Tab", Device="Organizr_PHP", DeviceId="Organizr_SSO", Version="1.0"',
+				"Accept" => "application/json",
+				"Content-Type" => "application/json"
+			);
+			$data = array(
+				"Username" => $username,
+				"Pw" => $password
+			);
+			$endpoint = '/Users/authenticatebyname';
+			$options = ($this->localURL($url)) ? array('verify' => false) : array();
+			$response = Requests::post($url . $endpoint, $headers, json_encode($data), $options);
+			if ($response->success) {
+				$token = json_decode($response->body, true);
+				$this->writeLog('success', 'Jellyfin Token Function - Grabbed token.', $username);
+			} else {
+				$this->writeLog('error', 'Jellyfin Token Function - Jellyfin did not return Token', $username);
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Jellyfin Token Function - Error: ' . $e->getMessage(), $username);
+		}
+		
+		return '{"Servers":[{"ManualAddress":"'. $url . '","Id":"' . $token['ServerId'] . '","UserId":"' . $token['User']['Id'] . '","AccessToken":"' . $token['AccessToken'] . '"}]}';
+	}
+
 	public function getOmbiToken($username, $password, $oAuthToken = null, $fallback = false)
 	{
 		$token = null;

+ 1 - 1
api/homepage/jellyfin.php

@@ -539,7 +539,7 @@ trait JellyfinHomepageItem
 		$jellyfinItem['user'] = ($this->config['homepageShowStreamNames'] && $this->qualifyRequest($this->config['homepageShowStreamNamesAuth'])) ? @(string)$itemDetails['UserName'] : "";
 		$jellyfinItem['userThumb'] = '';
 		$jellyfinItem['userAddress'] = (isset($itemDetails['RemoteEndPoint']) ? $itemDetails['RemoteEndPoint'] : "x.x.x.x");
-		$jellyfinURL = $this->config['jellyfinURL'] . '/web/index.html#!/itemdetails.html?id=';
+		$jellyfinURL = $this->config['jellyfinURL'] . '/web/index.html#!/details?id=';
 		$jellyfinItem['address'] = $this->config['jellyfinTabURL'] ? rtrim($this->config['jellyfinTabURL'], '/') . "/web/#!/item/item.html?id=" . $jellyfinItem['uid'] : $jellyfinURL . $jellyfinItem['uid'] . "&serverId=" . $jellyfinItem['id'];
 		$jellyfinItem['nowPlayingOriginalImage'] = 'api/v2/homepage/image?source=jellyfin&type=' . $jellyfinItem['nowPlayingImageType'] . '&img=' . $jellyfinItem['nowPlayingThumb'] . '&height=' . $nowPlayingHeight . '&width=' . $nowPlayingWidth . '&key=' . $jellyfinItem['nowPlayingKey'] . '$' . $this->randString();
 		$jellyfinItem['originalImage'] = 'api/v2/homepage/image?source=jellyfin&type=' . $jellyfinItem['imageType'] . '&img=' . $jellyfinItem['thumb'] . '&height=' . $height . '&width=' . $width . '&key=' . $jellyfinItem['key'] . '$' . $this->randString();

+ 131 - 4
api/homepage/plex.php

@@ -5,6 +5,23 @@ trait PlexHomepageItem
 	
 	public function plexSettingsArray()
 	{
+		if ($this->config['plexID'] !== '' && $this->config['plexToken'] !== '') {
+			$loop = $this->plexLibraryList('key')['libraries'];
+			foreach ($loop as $key => $value) {
+				$libraryList[] = array(
+					'name' => $key,
+					'value' => $value
+				);
+			}
+		} else {
+			$libraryList = array(
+				array(
+					'name' => 'Refresh page to update List',
+					'value' => '',
+					'disabled' => true,
+				),
+			);
+		}
 		return array(
 			'name' => 'Plex',
 			'enabled' => strpos('personal', $this->config['license']) !== false,
@@ -87,6 +104,15 @@ trait PlexHomepageItem
 						'label' => 'User Information',
 						'value' => $this->config['homepageShowStreamNames']
 					),
+					array(
+						'type' => 'select2',
+						'class' => 'select2-multiple',
+						'id' => 'plex-stream-exclude-select',
+						'name' => 'homepagePlexStreamsExclude',
+						'label' => 'Libraries to Exclude',
+						'value' => $this->config['homepagePlexStreamsExclude'],
+						'options' => $libraryList
+					),
 					array(
 						'type' => 'select',
 						'name' => 'homepageShowStreamNamesAuth',
@@ -205,6 +231,39 @@ trait PlexHomepageItem
 								'value' => '3'
 							)
 						)
+					),
+					array(
+						'type' => 'blank',
+						'label' => ''
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'homepageUseCustomStreamNames',
+						'label' => 'Use custom names for users',
+						'value' => $this->config['homepageUseCustomStreamNames']
+					),
+					array(
+						'type' => 'html',
+						'name' => 'grabFromTautulli',
+						'label' => 'Grab from Tautulli. (Note, you must have set the Tautulli API key already)',
+						'override' => 6,
+						'html' => '<button type="button" onclick="getTautulliFriendlyNames()" class="btn btn-sm btn-success btn-rounded waves-effect waves-light b-none">Grab Names</button>',
+					),
+					array(
+						'type' => 'html',
+						'name' => 'homepageCustomStreamNamesAce',
+						'class' => 'jsonTextarea hidden',
+						'label' => 'Custom definitions for user names (JSON Object, with the key being the plex name, and the value what you want to override with)',
+						'override' => 12,
+						'html' => '<div id="homepageCustomStreamNamesAce" style="height: 300px;">' . htmlentities($this->config['homepageCustomStreamNames']) . '</div>',
+					),
+					array(
+						'type' => 'textbox',
+						'name' => 'homepageCustomStreamNames',
+						'class' => 'jsonTextarea hidden',
+						'id' => 'homepageCustomStreamNamesText',
+						'label' => '',
+						'value' => $this->config['homepageCustomStreamNames'],
 					)
 				),
 				'Test Connection' => array(
@@ -390,6 +449,7 @@ trait PlexHomepageItem
 			return false;
 		}
 		$ignore = array();
+		$exclude = explode(',', $this->config['homepagePlexStreamsExclude']);
 		$resolve = true;
 		$url = $this->qualifyURL($this->config['plexURL']);
 		$url = $url . "/status/sessions?X-Plex-Token=" . $this->config['plexToken'];
@@ -400,7 +460,7 @@ trait PlexHomepageItem
 			$items = array();
 			$plex = simplexml_load_string($response->body);
 			foreach ($plex as $child) {
-				if (!in_array($child['type'], $ignore) && isset($child['librarySectionID'])) {
+				if (!in_array($child['type'], $ignore) && !in_array($child['librarySectionID'], $exclude) && isset($child['librarySectionID'])) {
 					$items[] = $this->resolvePlexItem($child);
 				}
 			}
@@ -494,7 +554,6 @@ trait PlexHomepageItem
 			$this->setAPIResponse('error', 'Plex API error', 500);
 			return false;
 		}
-		
 	}
 	
 	public function getPlexHomepageMetadata($array)
@@ -674,7 +733,7 @@ trait PlexHomepageItem
 		$plexItem['bandwidthType'] = (string)$item->Session['location'];
 		$plexItem['sessionType'] = isset($item->TranscodeSession['progress']) ? 'Transcoding' : 'Direct Playing';
 		$plexItem['state'] = (((string)$item->Player['state'] == "paused") ? "pause" : "play");
-		$plexItem['user'] = ($this->config['homepageShowStreamNames'] && $this->qualifyRequest($this->config['homepageShowStreamNamesAuth'])) ? (string)$item->User['title'] : "";
+		$plexItem['user'] = $this->formatPlexUserName($item);
 		$plexItem['userThumb'] = ($this->config['homepageShowStreamNames'] && $this->qualifyRequest($this->config['homepageShowStreamNamesAuth'])) ? (string)$item->User['thumb'] : "";
 		$plexItem['userAddress'] = ($this->config['homepageShowStreamNames'] && $this->qualifyRequest($this->config['homepageShowStreamNamesAuth'])) ? (string)$item->Player['address'] : "x.x.x.x";
 		$plexItem['address'] = $this->config['plexTabURL'] ? $this->config['plexTabURL'] . "/web/index.html#!/server/" . $this->config['plexID'] . "/details?key=/library/metadata/" . $item['ratingKey'] : "https://app.plex.tv/web/app#!/server/" . $this->config['plexID'] . "/details?key=/library/metadata/" . $item['ratingKey'];
@@ -757,4 +816,72 @@ trait PlexHomepageItem
 		}
 		return $plexItem;
 	}
-}
+	
+	public function getTautulliFriendlyNames()
+	{
+		if (!$this->qualifyRequest(1)) {
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['tautulliURL']);
+		$url .= '/api/v2?apikey=' . $this->config['tautulliApikey'];
+		$url .= '&cmd=get_users';
+		$response = Requests::get($url, [], []);
+		$names = [];
+		try {
+			$response = json_decode($response->body, true);
+			foreach ($response['response']['data'] as $user) {
+				if ($user['user_id'] != 0) {
+					$names[$user['username']] = $user['friendly_name'];
+				}
+			}
+		} catch (Exception $e) {
+			$this->setAPIResponse('failure', null, 422, [$e->getMessage()]);
+		}
+		$this->setAPIResponse('success', null, 200, $names);
+	}
+	
+	private function formatPlexUserName($item)
+	{
+		$name = ($this->config['homepageShowStreamNames'] && $this->qualifyRequest($this->config['homepageShowStreamNamesAuth'])) ? (string)$item->User['title'] : "";
+		try {
+			if ($this->config['homepageUseCustomStreamNames']) {
+				$customNames = json_decode($this->config['homepageCustomStreamNames'], true);
+				if (array_key_exists($name, $customNames)) {
+					$name = $customNames[$name];
+				}
+			}
+		} catch (Exception $e) {
+			// don't do anythig if it goes wrong, like if the JSON is badly formatted
+		}
+		return $name;
+	}
+	
+	public function plexLibraryList($value = 'id')
+	{
+		
+		if (!empty($this->config['plexToken']) && !empty($this->config['plexID'])) {
+			$url = 'https://plex.tv/api/servers/' . $this->config['plexID'];
+			try {
+				$headers = array(
+					"Accept" => "application/json",
+					"X-Plex-Token" => $this->config['plexToken']
+				);
+				$response = Requests::get($url, $headers, array());
+				libxml_use_internal_errors(true);
+				if ($response->success) {
+					$libraryList = array();
+					$plex = simplexml_load_string($response->body);
+					foreach ($plex->Server->Section as $child) {
+						$libraryList['libraries'][(string)$child['title']] = (string)$child[$value];
+					}
+					$libraryList = array_change_key_case($libraryList, CASE_LOWER);
+					return $libraryList;
+				}
+			} catch (Requests_Exception $e) {
+				$this->writeLog('error', 'Plex Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				return false;
+			};
+		}
+		return false;
+	}
+}

+ 13 - 5
api/homepage/tautulli.php

@@ -139,6 +139,13 @@ trait TautulliHomepageItem
 						'value' => $this->config['homepageTautulliMiscAuth'],
 						'options' => $this->groupOptions
 					),
+					array(
+						'type' => 'switch',
+						'name' => 'tautulliFriendlyName',
+						'label' => 'Use Friendly Name',
+						'value' => $this->config['tautulliFriendlyName'],
+						'help' => 'Use the friendly name set in tautulli for users.',
+					),
 				),
 				'Test Connection' => array(
 					array(
@@ -157,7 +164,7 @@ trait TautulliHomepageItem
 			)
 		);
 	}
-	
+
 	public function testConnectionTautulli()
 	{
 		if (empty($this->config['tautulliURL'])) {
@@ -186,7 +193,7 @@ trait TautulliHomepageItem
 			return false;
 		}
 	}
-	
+
 	public function tautulliHomepagePermissions($key = null)
 	{
 		$permissions = [
@@ -211,7 +218,7 @@ trait TautulliHomepageItem
 			return [];
 		}
 	}
-	
+
 	public function homepageOrdertautulli()
 	{
 		if ($this->homepageItemPermissions($this->tautulliHomepagePermissions('main'))) {
@@ -227,7 +234,7 @@ trait TautulliHomepageItem
 				';
 		}
 	}
-	
+
 	public function getTautulliHomepageData()
 	{
 		if (!$this->homepageItemPermissions($this->tautulliHomepagePermissions('main'), true)) {
@@ -283,6 +290,7 @@ trait TautulliHomepageItem
 				'popularMovies' => $this->config['tautulliPopularMovies'],
 				'popularTV' => $this->config['tautulliPopularTV'],
 				'title' => $this->config['tautulliHeaderToggle'],
+				'friendlyName' => $this->config['tautulliFriendlyName'],
 			];
 			$ids = []; // Array of stat_ids to remove from the returned array
 			if (!$this->qualifyRequest($this->config['homepageTautulliLibraryAuth'])) {
@@ -319,4 +327,4 @@ trait TautulliHomepageItem
 		$this->setAPIResponse('success', null, 200, $api);
 		return $api;
 	}
-}
+}

+ 1 - 1
api/homepage/unifi.php

@@ -7,7 +7,7 @@ trait UnifiHomepageItem
 		return array(
 			'name' => 'Unifi',
 			'enabled' => true,
-			'image' => 'plugins/images/tabs/UniFi.png',
+			'image' => 'plugins/images/tabs/unifi.png',
 			'category' => 'Monitor',
 			'settings' => array(
 				'Enable' => array(

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

@@ -7,7 +7,6 @@ $app->get('/homepage/image', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/calendar', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -16,7 +15,6 @@ $app->get('/homepage/calendar', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/plex/streams', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -25,7 +23,6 @@ $app->get('/homepage/plex/streams', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/emby/streams', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -34,7 +31,6 @@ $app->get('/homepage/emby/streams', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/jellyfin/streams', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -43,7 +39,6 @@ $app->get('/homepage/jellyfin/streams', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/plex/recent', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -52,7 +47,6 @@ $app->get('/homepage/plex/recent', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/emby/recent', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -61,7 +55,6 @@ $app->get('/homepage/emby/recent', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/jellyfin/recent', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -70,7 +63,6 @@ $app->get('/homepage/jellyfin/recent', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/plex/playlists', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -79,7 +71,6 @@ $app->get('/homepage/plex/playlists', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->post('/homepage/plex/metadata', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -88,7 +79,6 @@ $app->post('/homepage/plex/metadata', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->post('/homepage/emby/metadata', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -97,7 +87,6 @@ $app->post('/homepage/emby/metadata', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->post('/homepage/jellyfin/metadata', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -106,7 +95,6 @@ $app->post('/homepage/jellyfin/metadata', function ($request, $response, $args)
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/plex/search/{query}', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -115,7 +103,6 @@ $app->get('/homepage/plex/search/{query}', function ($request, $response, $args)
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/pihole/stats', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -124,7 +111,6 @@ $app->get('/homepage/pihole/stats', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/rtorrent/queue', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -133,7 +119,6 @@ $app->get('/homepage/rtorrent/queue', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/sonarr/queue', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -142,7 +127,6 @@ $app->get('/homepage/sonarr/queue', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/sonarr/calendar', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -151,7 +135,6 @@ $app->get('/homepage/sonarr/calendar', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/radarr/queue', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -160,7 +143,6 @@ $app->get('/homepage/radarr/queue', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/radarr/calendar', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -169,7 +151,6 @@ $app->get('/homepage/radarr/calendar', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/lidarr/calendar', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -178,7 +159,6 @@ $app->get('/homepage/lidarr/calendar', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/couchpotato/calendar', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -187,7 +167,6 @@ $app->get('/homepage/couchpotato/calendar', function ($request, $response, $args
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/sickrage/calendar', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -196,7 +175,6 @@ $app->get('/homepage/sickrage/calendar', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/ical/calendar', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -205,7 +183,6 @@ $app->get('/homepage/ical/calendar', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/deluge/queue', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -214,7 +191,6 @@ $app->get('/homepage/deluge/queue', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/transmission/queue', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -223,7 +199,6 @@ $app->get('/homepage/transmission/queue', function ($request, $response, $args)
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/qbittorrent/queue', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -232,7 +207,6 @@ $app->get('/homepage/qbittorrent/queue', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/jdownloader/queue', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -241,7 +215,6 @@ $app->get('/homepage/jdownloader/queue', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/nzbget/queue', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -250,7 +223,6 @@ $app->get('/homepage/nzbget/queue', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/sabnzbd/queue', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -259,7 +231,6 @@ $app->get('/homepage/sabnzbd/queue', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->post('/homepage/sabnzbd/queue/resume', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -270,7 +241,6 @@ $app->post('/homepage/sabnzbd/queue/resume', function ($request, $response, $arg
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->post('/homepage/sabnzbd/queue/pause', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -281,7 +251,6 @@ $app->post('/homepage/sabnzbd/queue/pause', function ($request, $response, $args
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/unifi/data', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -290,7 +259,6 @@ $app->get('/homepage/unifi/data', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/tautulli/data', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -299,7 +267,14 @@ $app->get('/homepage/tautulli/data', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
+});
+$app->get('/homepage/tautulli/names', function ($request, $response, $args) {
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	$Organizr->getTautulliFriendlyNames();
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
 });
 $app->get('/homepage/netdata/data', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -308,7 +283,6 @@ $app->get('/homepage/netdata/data', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/monitorr/data', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -317,7 +291,6 @@ $app->get('/homepage/monitorr/data', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/speedtest/data', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -326,7 +299,6 @@ $app->get('/homepage/speedtest/data', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/octoprint/data', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -335,7 +307,6 @@ $app->get('/homepage/octoprint/data', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/weather/data', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -344,7 +315,6 @@ $app->get('/homepage/weather/data', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->post('/homepage/weather/coordinates', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -353,7 +323,6 @@ $app->post('/homepage/weather/coordinates', function ($request, $response, $args
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/healthchecks', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -362,7 +331,6 @@ $app->get('/homepage/healthchecks', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/healthchecks/{tags}', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -371,7 +339,6 @@ $app->get('/homepage/healthchecks/{tags}', function ($request, $response, $args)
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/ombi/requests[/{type}[/{limit}[/{offset}]]]', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -383,7 +350,6 @@ $app->get('/homepage/ombi/requests[/{type}[/{limit}[/{offset}]]]', function ($re
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->post('/homepage/ombi/requests/{type}/{id}', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -392,7 +358,6 @@ $app->post('/homepage/ombi/requests/{type}/{id}', function ($request, $response,
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->post('/homepage/ombi/requests/{type}/{id}/available', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -401,7 +366,6 @@ $app->post('/homepage/ombi/requests/{type}/{id}/available', function ($request,
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->post('/homepage/ombi/requests/{type}/{id}/unavailable', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -410,7 +374,6 @@ $app->post('/homepage/ombi/requests/{type}/{id}/unavailable', function ($request
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->post('/homepage/ombi/requests/{type}/{id}/approve', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -419,7 +382,6 @@ $app->post('/homepage/ombi/requests/{type}/{id}/approve', function ($request, $r
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->put('/homepage/ombi/requests/{type}/{id}/deny', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -428,7 +390,6 @@ $app->put('/homepage/ombi/requests/{type}/{id}/deny', function ($request, $respo
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->delete('/homepage/ombi/requests/{type}/{id}', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -437,7 +398,6 @@ $app->delete('/homepage/ombi/requests/{type}/{id}', function ($request, $respons
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/youtube/{query}', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -446,7 +406,6 @@ $app->get('/homepage/youtube/{query}', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->post('/homepage/scrape', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -455,7 +414,6 @@ $app->post('/homepage/scrape', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/homepage/jackett/{query}', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -464,5 +422,4 @@ $app->get('/homepage/jackett/{query}', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
-});
+});

+ 3 - 2
api/v2/routes/root.php

@@ -116,11 +116,12 @@ $app->get('/launch', function ($request, $response, $args) {
 	$GLOBALS['api']['response']['data']['status'] = $Organizr->status();
 	$GLOBALS['api']['response']['data']['sso'] = array(
 		'myPlexAccessToken' => isset($_COOKIE['mpt']) ? $_COOKIE['mpt'] : false,
-		'id_token' => isset($_COOKIE['Auth']) ? $_COOKIE['Auth'] : false
+		'id_token' => isset($_COOKIE['Auth']) ? $_COOKIE['Auth'] : false,
+		'jellyfin_credentials' => isset($_COOKIE['jellyfin_credentials']) ? $_COOKIE['jellyfin_credentials'] : false
 	);
 	$response->getBody()->write(jsonE($GLOBALS['api']));
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
 	
-});
+});

+ 11 - 0
api/v2/routes/tabs.php

@@ -9,6 +9,17 @@ $app->get('/tabs', function ($request, $response, $args) {
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
 	
+});
+$app->get('/tabs/{id}', function ($request, $response, $args) {
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	if ($Organizr->qualifyRequest(1, true)) {
+		$GLOBALS['api']['response']['data'] = $Organizr->getTabByIdCheckUser($args['id']);
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+	
 });
 $app->post('/tabs', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();

+ 3 - 0
css/light.css

@@ -15,6 +15,9 @@
     top: calc(50% - 3.5px);
     left: calc(50% - 3.5px);
 }
+.internal-listing.p-0.show {
+    width: 99%;
+}
 * {
     outline: 0 !important;
 }

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
css/light.min.css


+ 1 - 4
css/organizr.css

@@ -896,7 +896,7 @@ span.homepage-text {
 }
 .fc-scroller .simplebar-content {
     min-height: auto !important;
-    overflow-x: hidden !important;
+    overflow: hidden !important;
 	padding-bottom: 0px !important;
 }
 .simplebar-content {
@@ -1531,9 +1531,6 @@ span.fc-title {
 .dropdown-menu {
     width: inherit;
 }
-#organizrNewsPanel .panel-body {
-    background: #2d2c2c;
-}
 .pingTime {
     position: inherit;
     right: -30px;

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
css/organizr.min.css


+ 3 - 0
css/themes/Blue.css

@@ -17,6 +17,9 @@
   top: calc(50% - 3.5px);
   left: calc(50% - 3.5px);
 }
+#organizrNewsPanel .panel-body {
+    background: #fdfdfd;
+}
 /*Just change your choise color here its theme Colors*/
 body {
   background: #fff;

+ 3 - 0
css/themes/Organizr.css

@@ -360,4 +360,7 @@ span.select2-results {
 }
 .select2-container--default .select2-results__option[aria-selected=true] {
     background-color: #232323;
+}
+#organizrNewsPanel .panel-body {
+    background: #2d2c2c;
 }

+ 21 - 21
js/custom.js

@@ -436,7 +436,7 @@ $(document).on("click", ".reset-button", function(e) {
 		var post = {
 	        email:email
         };
-	    message('Submitting request...',html.message,activeInfo.settings.notifications.position,'#FFF','info','10000');
+	    message('Submitting request...','',activeInfo.settings.notifications.position,'#FFF','info','10000');
         organizrAPI2('POST','api/v2/users/recover',post).success(function(data) {
             var html = data.response;
             message('Recover Password',html.message,activeInfo.settings.notifications.position,'#FFF','success','10000');
@@ -848,25 +848,6 @@ function convertMinutesToMs(minutes){
         return (minutes * 1000) * 60;
     }
 }
-//EDIT TAB GET ID
-$(document).on("click", ".editTabButton", function () {
-    //tabActionTime
-    //tabActionType
-    $('#edit-tab-form [name=name]').val($(this).parent().parent().attr("data-name"));
-    $('#originalTabName').html($(this).parent().parent().attr("data-name"));
-    $('#edit-tab-form [name=url]').val($(this).parent().parent().attr("data-url"));
-    $('#edit-tab-form [name=url_local]').val($(this).parent().parent().attr("data-local-url"));
-    $('#edit-tab-form [name=ping_url]').val($(this).parent().parent().attr("data-ping-url"));
-    $('#edit-tab-form [name=image]').val($(this).parent().parent().attr("data-image"));
-    $('#edit-tab-form [name=id]').val($(this).parent().parent().attr("data-id"));
-    $('#edit-tab-form [name=timeout_ms]').val(convertMsToMinutes($(this).parent().parent().attr("data-tab-action-time")));
-    $('#edit-tab-form [name=timeout]').val($(this).parent().parent().attr("data-tab-action-type"));
-    if( $(this).parent().parent().attr("data-url").indexOf('/?v') > 0){
-        $('#edit-tab-form [name=url]').prop('disabled', 'true');
-    }else{
-        $('#edit-tab-form [name=url]').prop('disabled', null);
-    }
-});
 //EDIT TAB
 $(document).on("click", ".editTab", function () {
     var originalTabName = $('#originalTabName').html();
@@ -1861,4 +1842,23 @@ $(document).on('click', '.imageManagerItem', function() {
 $(document).on('click', '.close-editHomepageItemDiv',function () {
 	$('body').removeAttr('style');
 	$('html').removeAttr('style');
-})
+})
+// Control init of custom plex JSON editor
+$(document).on('click', '#homepage-Plex-form li a[aria-controls="Misc Options"]', function() {
+    var resizeEditor = function(jsonEditor) {
+        const aceEditor = jsonEditor;
+        const newHeight = aceEditor.getSession().getScreenLength() * (aceEditor.renderer.lineHeight + aceEditor.renderer.scrollBar.getWidth());
+        aceEditor.container.style.height = newHeight + 'px';
+        aceEditor.resize();
+    }
+
+    jsonEditor = ace.edit("homepageCustomStreamNamesAce");
+    var JsonMode = ace.require("ace/mode/javascript").Mode;
+    jsonEditor.session.setMode(new JsonMode());
+    jsonEditor.setTheme("ace/theme/idle_fingers");
+    jsonEditor.setShowPrintMargin(false);
+    jsonEditor.session.on('change', function(delta) {
+        $('#homepageCustomStreamNamesText').val(jsonEditor.getValue());
+        $('#customize-appearance-form-save').removeClass('hidden');
+    });
+}); 

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
js/custom.min.js


+ 87 - 38
js/functions.js

@@ -1674,6 +1674,7 @@ function homepageItemFormHTML(v){
 	`;
 }
 function editHomepageItem(item){
+	ajaxloader('.editHomepageItemBox-' + item, 'in');
 	organizrAPI2('GET','api/v2/settings/homepage/'+item).success(function(data) {
 		try {
 			let response = data.response;
@@ -1715,9 +1716,10 @@ function editHomepageItem(item){
 		}catch(e) {
 			organizrCatchError(e,data);
 		}
-
+		ajaxloader('.editHomepageItemBox-' + item);
 	}).fail(function(xhr) {
 		OrganizrApiError(xhr, 'Edit Homepage Failed');
+		ajaxloader('.editHomepageItemBox-' + item);
 	});
 }
 function buildHomepageItem(array){
@@ -1728,7 +1730,7 @@ function buildHomepageItem(array){
 				listing += `
 				<div class="col-lg-2 col-md-2 col-sm-4 col-xs-4">
 					<div class="white-box bg-org m-0">
-						<div class="el-card-item p-0">
+						<div class="el-card-item p-0 editHomepageItemBox-`+v.name+`">
 							<div class="el-card-avatar el-overlay-1">
 								<a onclick="editHomepageItem('`+v.name+`')"><img class="lazyload tabImages mouse" data-src="`+v.image+`"></a>
 							</div>
@@ -1739,25 +1741,6 @@ function buildHomepageItem(array){
 						</div>
 					</div>
 				</div>
-				<!--
-				<form id="homepage-`+v.name+`-form" class="mfp-hide white-popup mfp-with-anim homepageForm addFormTick">
-				    <fieldset style="border:0;" class="col-md-10 col-md-offset-1">
-                        <div class="panel bg-org panel-info">
-                            <div class="panel-heading">
-                                <span lang="en">`+v.name+`</span>
-                                <button type="button" class="btn bg-org btn-circle close-popup pull-right"><i class="fa fa-times"></i> </button>
-                                <button id="homepage-`+v.name+`-form-save" onclick="submitSettingsForm('homepage-`+v.name+`-form')" class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right hidden animated loop-animation rubberBand m-r-20" type="button"><span class="btn-label"><i class="fa fa-save"></i></span><span lang="en">Save</span></button>
-                            </div>
-                            <div class="panel-wrapper collapse in" aria-expanded="true">
-                                <div class="panel-body bg-org">
-                                    +buildFormGroup(v.settings)+
-                                </div>
-                            </div>
-                        </div>
-					</fieldset>
-				    <div class="clearfix"></div>
-				</form>
-				-->
 				`;
 			}
 		});
@@ -2072,6 +2055,12 @@ function sortHomepageItemHrefs(){
         }
     });
 }
+function checkTabHomepageItemList(name, url, urlLocal, id, check, tab) {
+	// might use this later
+	if (name.includes(check) || url.includes(check) || urlLocal.includes(check)) {
+		addEditHomepageItem(id, tab);
+	}
+}
 function checkTabHomepageItem(id, name, url, urlLocal){
     name = name.toLowerCase();
     url = url.toLowerCase();
@@ -2116,6 +2105,12 @@ function checkTabHomepageItem(id, name, url, urlLocal){
         addEditHomepageItem(id,'Ombi');
     }else if(name.includes('healthcheck') || url.includes('healthcheck') || urlLocal.includes('healthcheck')){
         addEditHomepageItem(id,'HealthChecks');
+    }else if(name.includes('jackett') || url.includes('jackett') || urlLocal.includes('jackett')){
+	    addEditHomepageItem(id,'Jackett');
+    }else if(name.includes('unifi') || url.includes('unifi') || urlLocal.includes('unifi')){
+	    addEditHomepageItem(id,'Unifi');
+    }else if(name.includes('tautulli') || url.includes('tautulli') || urlLocal.includes('tautulli')){
+	    addEditHomepageItem(id,'Tautulli');
     }
 }
 function addEditHomepageItem(id, type){
@@ -3077,17 +3072,10 @@ function buildTabEditorItem(array){
 		var buttonDisabled = v.url.indexOf('/page/settings') > 0 ? 'disabled' : '';
         var typeDisabled = v.url.indexOf('/v2/page/') > 0 ? 'disabled' : '';
 		tabList += `
-		<tr class="tabEditor" data-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-local-url="`+v.url_local+`" data-ping-url="`+v.ping_url+`" data-image="`+v.image+`" data-tab-action-type="`+v.timeout+`" data-tab-action-time="`+v.timeout_ms+`">
+		<tr class="tabEditor" 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-local-url="`+v.url_local+`" data-ping-url="`+v.ping_url+`" data-image="`+v.image+`" data-tab-action-type="`+v.timeout+`" data-tab-action-time="`+v.timeout_ms+`">
 			<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+`">
-			<input type="hidden" class="form-control" name="tab[`+v.id+`].url_local" value="`+v.url_local+`">
-			<input type="hidden" class="form-control" name="tab[`+v.id+`].name" value="`+v.name+`">
-			<input type="hidden" class="form-control" name="tab[`+v.id+`].url" value="`+v.url+`">
-			<input type="hidden" class="form-control" name="tab[`+v.id+`].ping_url" value="`+v.ping_url+`">
-			<input type="hidden" class="form-control" name="tab[`+v.id+`].image" value="`+v.image+`">
-			<input type="hidden" class="form-control" name="tab[`+v.id+`].timeout" value="`+v.timeout+`">
-			<input type="hidden" class="form-control" name="tab[`+v.id+`].timeout_ms" value="`+v.timeout_ms+`">
 			<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">
@@ -3110,13 +3098,39 @@ function buildTabEditorItem(array){
 			<td style="text-align:center"><input type="checkbox" class="js-switch splashSwitch" data-size="small" data-color="#99d683" data-secondary-color="#f96262" name="tab[`+v.id+`].splash" value="true" `+tof(v.splash,'c')+`/><input type="hidden" class="form-control" name="tab[`+v.id+`].splash" value="false"></td>
 			<td style="text-align:center"><input type="checkbox" class="js-switch pingSwitch" data-size="small" data-color="#99d683" data-secondary-color="#f96262" name="tab[`+v.id+`].ping" value="true" `+tof(v.ping,'c')+`/><input type="hidden" class="form-control" name="tab[`+v.id+`].ping" value="false"></td>
 			<td style="text-align:center"><input type="checkbox" class="js-switch preloadSwitch" data-size="small" data-color="#99d683" data-secondary-color="#f96262" name="tab[`+v.id+`].preload" value="true" `+tof(v.preload,'c')+`/><input type="hidden" class="form-control" name="tab[`+v.id+`].preload" value="false"></td>
-			<td style="text-align:center"><button type="button" class="btn btn-info btn-outline btn-circle btn-lg m-r-5 editTabButton popup-with-form" href="#edit-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-info btn-outline btn-circle btn-lg m-r-5 editTabButton popup-with-form" onclick="editTabForm('`+v.id+`')" href="#edit-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 `+deleteDisabled+`"><i class="ti-trash"></i></button></td>
 		</tr>
 		`;
 	});
 	return tabList;
 }
+function editTabForm(id){
+	organizrAPI2('GET','api/v2/tabs/' + id,true).success(function(data) {
+		try {
+			let response = data.response;
+			console.log(response);
+			$('#edit-tab-form [name=name]').val(response.data.name);
+			$('#originalTabName').html(response.data.name);
+			$('#edit-tab-form [name=url]').val(response.data.url);
+			$('#edit-tab-form [name=url_local]').val(response.data.url_local);
+			$('#edit-tab-form [name=ping_url]').val(response.data.ping_url);
+			$('#edit-tab-form [name=image]').val(response.data.image);
+			$('#edit-tab-form [name=id]').val(response.data.id);
+			$('#edit-tab-form [name=timeout_ms]').val(convertMsToMinutes(response.data.timeout_ms));
+			$('#edit-tab-form [name=timeout]').val(response.data.timeout);
+			if( response.data.url.indexOf('/?v') > 0){
+				$('#edit-tab-form [name=url]').prop('disabled', 'true');
+			}else{
+				$('#edit-tab-form [name=url]').prop('disabled', null);
+			}
+		}catch(e) {
+			organizrCatchError(e,data);
+		}
+	}).fail(function(xhr) {
+		OrganizrApiError(xhr, 'Tab Error');
+	});
+}
 function getSubmitSettingsFormValueSingle(form, index, value){
     var values = {};
     if(value !== '#987654' && index.includes('disable-pwd-mgr') == false) {
@@ -3245,6 +3259,7 @@ function submitHomepageOrder(){
 }
 function submitTabOrder(newTabs){
 	var data = [];
+	var process = false;
 	$.each(newTabs.tab, function(i,v) {
 		if(v.originalOrder == v.order){
 			delete newTabs.tab[i];
@@ -3254,8 +3269,14 @@ function submitTabOrder(newTabs){
 				"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");
+		$('.saveTabOrderButton').addClass('hidden');
+		return false;
+	}
 	var callbacks = $.Callbacks();
 	callbacks.add( buildTabEditor );
 	organizrAPI2('PUT','api/v2/tabs',data,true).success(function(data) {
@@ -6936,6 +6957,7 @@ function buildTautulliItem(array){
     var homestats = array.homestats.data;
     var libstats = array.libstats;
     var options = array.options;
+    var friendlyName = array.options.friendlyName;
     var buildLibraries = function(data){
         var libs = data.data;
         var movies = [];
@@ -7026,7 +7048,7 @@ function buildTautulliItem(array){
         card += (audio.length > 0) ? buildCard('artist', audio) : '';
         return card;
     };
-    var buildStats = function(data, stat){
+    var buildStats = function(data, stat, friendlyName = true){
         var card = '';
         data.forEach(e => {
             let classes = '';
@@ -7074,7 +7096,11 @@ function buildTautulliItem(array){
                                             var rowNameValue = '';
                                             var rowValue = '';
                                             if(stat == 'top_users') {
-                                                rowNameValue = item['user'];
+                                                if(friendlyName) {
+                                                    rowNameValue = item['friendly_name'];
+                                                } else {
+                                                    rowNameValue = item['user'];
+                                                }
                                                 rowValue = item['total_plays'];
                                             } else if(stat == 'top_platforms') {
                                                 rowNameValue = item['platform'];
@@ -7112,7 +7138,7 @@ function buildTautulliItem(array){
     cards += (options['popularTV']) ? buildStats(homestats, 'popular_tv') : '';
     cards += (options['topMovies']) ? buildStats(homestats, 'top_movies') : '';
     cards += (options['topTV']) ? buildStats(homestats, 'top_tv') : '';
-    cards += (options['topUsers']) ? buildStats(homestats, 'top_users') : '';
+    cards += (options['topUsers']) ? buildStats(homestats, 'top_users', friendlyName) : '';
     cards += (options['topPlatforms']) ? buildStats(homestats, 'top_platforms') : '';
     cards += '</div>';
     cards += '<div class="row tautulliLibraries">'
@@ -8315,6 +8341,24 @@ function tryUpdateNetdata(array){
     });
     return existing;
 }
+function getTautulliFriendlyNames()
+{
+    organizrAPI2('GET','api/v2/homepage/tautulli/names').success(function(data) {
+        try {
+            let response = data.response;
+            if(response.data !== null){
+                var string = JSON.stringify(response.data, null, 4);
+                jsonEditor = ace.edit("homepageCustomStreamNamesAce");
+                jsonEditor.setValue(string);
+                $('#homepage-Plex-form-save').removeClass('hidden');
+            }
+        }catch(e) {
+	        organizrCatchError(e,data);
+        }
+    }).fail(function(xhr) {
+	    OrganizrApiError(xhr);
+    });
+}
 function homepageJackett(){
 	if(activeInfo.settings.homepage.options.alternateHomepageHeaders){
 		var header = `
@@ -8427,8 +8471,8 @@ function searchJackett(){
 				{ "data": "Tracker" },
 				{ data: 'Title',
 					render: function ( data, type, row ) {
-						if(row.Comments !== null){
-							return '<a href="'+row.Comments+'" target="_blank">'+data+'</a>';
+						if(row.Details !== null){
+							return '<a href="'+row.Details+'" target="_blank">'+data+'</a>';
 						}else{
 							return data;
 						}
@@ -8452,8 +8496,8 @@ function searchJackett(){
 						if ( type === 'display' || type === 'filter' ) {
 							if(data !== null){
 								return '<a href="'+data+'" target="_blank"><i class="fa fa-magnet"></i></a>';
-							}else if(row.Comments !== null){
-								return '<a href="'+row.Comments+'" target="_blank"><i class="fa fa-cloud-download"></i></a>';
+							}else if(row.Details !== null){
+								return '<a href="'+row.Details+'" target="_blank"><i class="fa fa-cloud-download"></i></a>';
 							}else if(row.Guid !== null){
 								return '<a href="'+row.Guid+'" target="_blank"><i class="fa fa-cloud-download"></i></a>';
 							}else if(row.Link !== null){
@@ -9970,6 +10014,10 @@ function showPlexTokenForm(selector = null){
 		            <label class="control-label" for="plex-token-form-password" lang="en">Plex Password</label>
 		            <input type="password" class="form-control" id="plex-token-form-password" name="password"  required="">
 		        </div>
+		        <div class="form-group">
+		            <label class="control-label" for="plex-token-form-tfa" lang="en">Plex 2FA (if applicable)</label>
+		            <input type="text" class="form-control" id="plex-token-form-tfa" name="tfa" >
+		        </div>
 		    </fieldset>
 		    <button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none" onclick="getPlexToken('`+selector+`')" type="button"><span class="btn-label"><i class="fa fa-ticket"></i></span><span lang="en">Grab It</span></button>
 		    <div class="clearfix"></div>
@@ -9986,6 +10034,7 @@ function getPlexToken(selector) {
 	$('.plexTokenHeader').addClass('panel-info').removeClass('panel-warning').removeClass('panel-danger');
 	var plex_username = $('#get-plex-token-form [name=username]').val().trim();
 	var plex_password = $('#get-plex-token-form [name=password]').val().trim();
+	var plex_tfa = $('#get-plex-token-form [name=tfa]').val().trim();
 	if ((plex_password !== '') && (plex_password !== '')) {
 		$.ajax({
 			type: 'POST',
@@ -9997,7 +10046,7 @@ function getPlexToken(selector) {
 			url: 'https://plex.tv/users/sign_in.json',
 			data: {
 				'user[login]': plex_username,
-				'user[password]': plex_password,
+				'user[password]': plex_password + plex_tfa,
 				force: true
 			},
 			cache: false,

+ 14 - 14
js/langpack/da[Danish].json

@@ -181,12 +181,12 @@
         "Inactive Plugins": "Inactive Plugins",
         "Active Plugins": "Active Plugins",
         "Inactive": "Inaktiv",
-        "Everything Active": "Alt aktiv",
+        "Everything Active": "Alt aktivt",
         "Nothing Active": "Intet aktivt",
         "Choose Plex Machine": "Vælg plex maskine",
         "Test Speed to Server": "Test hastighed til server",
         "Test Server Speed": "Test server hastighed",
-        "Subject": "emne",
+        "Subject": "Emne",
         "To:": "Til:",
         "Email Users": "Email brugere",
         "E-Mail Center": "E-Mail Center",
@@ -204,9 +204,9 @@
         "Create/Send Invite": "Opret/send invitation",
         "Name or Username": "Navn eller Brugernavn",
         "New Invite": "Ny Invitation",
-        "Hover to show ": "Hover to show ",
-        "Parent Directory: ": "Parent Directory: ",
-        "The Database will contain sensitive information. Please place in directory outside of root Web Directory.": "Databasen vil indeholde følsom information. Placer den uden for root web directoriet",
+        "Hover to show ": "Hold musen over for at vise_",
+        "Parent Directory: ": "Rodmappen:_",
+        "The Database will contain sensitive information. Please place in directory outside of root Web Directory.": "Databasen vil indeholde sensitiv information. Placer den venligst uden for rod Web Mappen",
         "I Want to Help": "Jeg vil gerne hjælpe",
         "Head on over to POEditor and help us translate Organizr into your language": "Head on over to POEditor and help us translate Organizr into your language",
         "Want to help translate?": "Vil du hjælpe med at oversætte?",
@@ -219,22 +219,22 @@
         "Password Again": "Gentag Adgangskode",
         "Template": "Skabelon",
         "Plex Machine": "Plex maskine",
-        "Get Plex Machine": "Get Plex Machine",
+        "Get Plex Machine": "Hent Plex Maskine",
         "Grab It": "Grab It",
-        "Plex Password": "Plex Password",
-        "Plex Username": "Plex Username",
-        "Enter Plex Details": "Enter Plex Details",
-        "Get Plex Token": "Get Plex Token",
-        "Upload Image": "Upload Image",
-        "View Images": "View Images",
+        "Plex Password": "Plex Adgangskode",
+        "Plex Username": "Plex Brugernavn",
+        "Enter Plex Details": "Indtast Plex Informationer",
+        "Get Plex Token": "Hent Plex Token",
+        "Upload Image": "Upload Billede",
+        "View Images": "Vis Billeder",
         "Reload": "Genindlæs",
         "Unlock": "Lås op",
         "Browser Information": "Browser Information",
-        "Web Folder": "Web Folder",
+        "Web Folder": "Web Mappe",
         "Dependencies Missing": "Dependencies Missing",
         "Organizr Dependency Check": "Organizr Dependency Check",
         "Please make sure both Token and Machine are filled in": "Please make sure both Token and Machine are filled in",
-        "Loading Requests...": "Loading Requests...",
+        "Loading Requests...": "Indlæser Anmodninger",
         "Loading Recent...": "Loading Recent...",
         "Loading Now Playing...": "Loading Now Playing...",
         "Loading Playlists...": "Loading Playlists...",

+ 7 - 0
js/version.json

@@ -306,5 +306,12 @@
     "new": "Update check on load instead of settings",
     "fixed": "add clarification to checkRoute error message|Increase jackett timeout to 120 seconds|add language elements to more areas|rework calendar filter|fix checkroute if has subdir|fix dependency check",
     "notes": "Please join our discord if you have any issues|Please report bugs in GitHub issues page"
+  },
+  "2.1.120": {
+    "date": "2020-12-11 20:00",
+    "title": "Weekly update-ish",
+    "new": "Option to disable certain plex libraries on now playing (#1534)|Toggle to use friendly name in stats|jellyFin SSO|PHP8 support|add 2fa to plex token form|loading animation to homepage item loading|allow admin to make other admins",
+    "fixed": "fix Blue Light theme News background (#1538)|fix empty socks (#1520)|getServerPath function|tab saving on order change if nothing updated (#1530)|fix tab sort order saving with lots of tabs (#1175)|non admin from changing password|password reset|allow homepage item api lookup lowercase|change unsorted icon on new installs|unifi and ubnt image changes|missing ssoJellyfin var",
+    "notes": "New tab images|Please join our discord if you have any issues|Please report bugs in GitHub issues page"
   }
 }

BIN
plugins/images/tabs/bitwarden.png


+ 0 - 0
plugins/images/tabs/UniFi.png → plugins/images/tabs/ubnt.png


BIN
plugins/images/tabs/unifi.png


Неке датотеке нису приказане због велике количине промена