diff --git a/gaseous-server/Classes/Config.cs b/gaseous-server/Classes/Config.cs index 18e70d0..f357775 100644 --- a/gaseous-server/Classes/Config.cs +++ b/gaseous-server/Classes/Config.cs @@ -648,6 +648,13 @@ namespace gaseous_server.Classes return MetadataPath; } + public string LibraryMetadataDirectory_Hasheous() + { + string MetadataPath = Path.Combine(LibraryMetadataDirectory, "Hasheous"); + if (!Directory.Exists(MetadataPath)) { Directory.CreateDirectory(MetadataPath); } + return MetadataPath; + } + public string LibrarySignaturesDirectory { get @@ -668,7 +675,6 @@ namespace gaseous_server.Classes { if (!Directory.Exists(LibraryRootDirectory)) { Directory.CreateDirectory(LibraryRootDirectory); } if (!Directory.Exists(LibraryImportDirectory)) { Directory.CreateDirectory(LibraryImportDirectory); } - // if (!Directory.Exists(LibraryBIOSDirectory)) { Directory.CreateDirectory(LibraryBIOSDirectory); } if (!Directory.Exists(LibraryFirmwareDirectory)) { Directory.CreateDirectory(LibraryFirmwareDirectory); } if (!Directory.Exists(LibraryUploadDirectory)) { Directory.CreateDirectory(LibraryUploadDirectory); } if (!Directory.Exists(LibraryMetadataDirectory)) { Directory.CreateDirectory(LibraryMetadataDirectory); } diff --git a/gaseous-server/Classes/FileSignature.cs b/gaseous-server/Classes/FileSignature.cs index e358340..f6d5012 100644 --- a/gaseous-server/Classes/FileSignature.cs +++ b/gaseous-server/Classes/FileSignature.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Configuration; using System.IO.Compression; using System.Net; using gaseous_server.Classes.Metadata; @@ -311,11 +312,55 @@ namespace gaseous_server.Classes HasheousClient.Models.LookupItemModel? HasheousResult = null; try { - HasheousResult = hasheous.RetrieveFromHasheous(new HasheousClient.Models.HashLookupModel + // check the cache first + if (!Directory.Exists(Config.LibraryConfiguration.LibraryMetadataDirectory_Hasheous())) { - MD5 = hash.md5hash, - SHA1 = hash.sha1hash - }); + Directory.CreateDirectory(Config.LibraryConfiguration.LibraryMetadataDirectory_Hasheous()); + } + // create file name from hash object + string cacheFileName = hash.md5hash + "_" + hash.sha1hash + ".json"; + string cacheFilePath = Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Hasheous(), cacheFileName); + // use cache file if it exists and is less than 30 days old, otherwise fetch from hasheous. if the fetch from hasheous is successful, save it to the cache, if it fails, use the cache if it exists even if it's old + if (File.Exists(cacheFilePath)) + { + FileInfo cacheFile = new FileInfo(cacheFilePath); + if (cacheFile.LastWriteTimeUtc > DateTime.UtcNow.AddDays(-30)) + { + Logging.Log(Logging.LogType.Information, "Get Signature", "Using cached signature from Hasheous"); + HasheousResult = Newtonsoft.Json.JsonConvert.DeserializeObject(File.ReadAllText(cacheFilePath)); + } + } + + try + { + if (HasheousResult == null) + { + // fetch from hasheous + HasheousResult = hasheous.RetrieveFromHasheous(new HasheousClient.Models.HashLookupModel + { + MD5 = hash.md5hash, + SHA1 = hash.sha1hash + }); + + if (HasheousResult != null) + { + // save to cache + File.WriteAllText(cacheFilePath, Newtonsoft.Json.JsonConvert.SerializeObject(HasheousResult)); + } + } + } + catch (Exception ex) + { + if (File.Exists(cacheFilePath)) + { + Logging.Log(Logging.LogType.Warning, "Get Signature", "Error retrieving signature from Hasheous - using cached signature", ex); + HasheousResult = Newtonsoft.Json.JsonConvert.DeserializeObject(File.ReadAllText(cacheFilePath)); + } + else + { + Logging.Log(Logging.LogType.Warning, "Get Signature", "Error retrieving signature from Hasheous", ex); + } + } if (HasheousResult != null) { @@ -366,24 +411,36 @@ namespace gaseous_server.Classes { foreach (HasheousClient.Models.MetadataItem metadataResult in HasheousResult.Metadata) { - // only IGDB metadata is supported - if (metadataResult.Source == HasheousClient.Models.MetadataSources.IGDB) + if (metadataResult.ImmutableId.Length > 0) { - if (metadataResult.ImmutableId.Length > 0) + signature.MetadataSources.AddGame(long.Parse(metadataResult.ImmutableId), HasheousResult.Name, metadataResult.Source); + } + else if (metadataResult.Id.Length > 0) + { + switch (metadataResult.Source) { - signature.MetadataSources.AddGame(long.Parse(metadataResult.ImmutableId), HasheousResult.Name, metadataResult.Source); - } - else if (metadataResult.Id.Length > 0) - { - gaseous_server.Models.Game hasheousGame = Games.GetGame(HasheousClient.Models.MetadataSources.IGDB, metadataResult.Id); - signature.MetadataSources.AddGame((long)hasheousGame.Id, hasheousGame.Name, metadataResult.Source); - } - else - { - // no id or immutable id - use unknown game - signature.MetadataSources.AddGame(0, "Unknown Game", HasheousClient.Models.MetadataSources.None); + case HasheousClient.Models.MetadataSources.IGDB: + gaseous_server.Models.Game hasheousGame = Games.GetGame(HasheousClient.Models.MetadataSources.IGDB, metadataResult.Id); + signature.MetadataSources.AddGame((long)hasheousGame.Id, hasheousGame.Name, metadataResult.Source); + break; + + default: + if (long.TryParse(metadataResult.Id, out long id) == true) + { + signature.MetadataSources.AddGame(id, HasheousResult.Name, metadataResult.Source); + } + else + { + signature.MetadataSources.AddGame(0, "Unknown Game", HasheousClient.Models.MetadataSources.None); + } + break; } } + else + { + // no id or immutable id - use unknown game + signature.MetadataSources.AddGame(0, "Unknown Game", HasheousClient.Models.MetadataSources.None); + } } } } @@ -421,18 +478,20 @@ namespace gaseous_server.Classes else { Logging.Log(Logging.LogType.Warning, "Get Signature", "Error retrieving signature from Hasheous", ex); + throw; } } else { Logging.Log(Logging.LogType.Warning, "Get Signature", "Error retrieving signature from Hasheous", ex); - + throw; } } } catch (Exception ex) { Logging.Log(Logging.LogType.Warning, "Get Signature", "Error retrieving signature from Hasheous", ex); + throw; } } diff --git a/gaseous-server/Classes/ImportGames.cs b/gaseous-server/Classes/ImportGames.cs index 8b5b380..87b73cb 100644 --- a/gaseous-server/Classes/ImportGames.cs +++ b/gaseous-server/Classes/ImportGames.cs @@ -215,42 +215,42 @@ namespace gaseous_server.Classes } // populate map with the sources from the signature if they don't already exist - bool reloadMap = false; foreach (MetadataSources source in Enum.GetValues(typeof(MetadataSources))) { + bool sourceExists = false; + if (source != MetadataSources.None) { - // check the signature for the source, and if it exists, add it to the map if it's not already there - foreach (Signatures_Games.SourceValues.SourceValueItem signatureSource in signature.MetadataSources.Games) + // get the signature that matches this source + Signatures_Games.SourceValues.SourceValueItem? signatureSource = signature.MetadataSources.Games.Find(x => x.Source == source); + if (signatureSource == null) { - // check if the metadata map contains the source - bool sourceExists = false; - foreach (MetadataMap.MetadataMapItem mapSource in map.MetadataMapItems) - { - if (mapSource.SourceType == source) - { - sourceExists = true; - } - } + Logging.Log(Logging.LogType.Information, "Import Game", " No source found for " + source.ToString()); + continue; + } - if (sourceExists == false) + // get the metadata map for this source + MetadataMap.MetadataMapItem? mapSource = map.MetadataMapItems.Find(x => x.SourceType == source); + if (mapSource == null) + { + // add the source to the map + bool preferred = false; + if (source == Config.MetadataConfiguration.DefaultMetadataSource) { - // add the source to the map - bool preferred = false; - if (source == Config.MetadataConfiguration.DefaultMetadataSource) - { - preferred = true; - } - MetadataManagement.AddMetadataMapItem((long)map.Id, source, signatureSource.Id, preferred); - reloadMap = true; + preferred = true; } + MetadataManagement.AddMetadataMapItem((long)map.Id, source, signatureSource.Id, preferred); + } + else + { + // update the source in the map - do not modify the preferred status + MetadataManagement.UpdateMetadataMapItem((long)map.Id, source, signatureSource.Id, null); } } } - if (reloadMap == true) - { - map = MetadataManagement.GetMetadataMap((long)map.Id); - } + + // reload the map + map = MetadataManagement.GetMetadataMap((long)map.Id); // add or update the rom dbDict = new Dictionary(); diff --git a/gaseous-server/Classes/Metadata/Metadata.cs b/gaseous-server/Classes/Metadata/Metadata.cs index 0853c47..1610275 100644 --- a/gaseous-server/Classes/Metadata/Metadata.cs +++ b/gaseous-server/Classes/Metadata/Metadata.cs @@ -186,7 +186,7 @@ namespace gaseous_server.Classes.Metadata if (returnObject.GetType().GetProperty("Name") != null) { Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "SELECT SignatureGameName FROM view_MetadataMap WHERE `Id` = @id;"; + string sql = "SELECT * FROM MetadataMap JOIN MetadataMapBridge ON MetadataMap.Id = MetadataMapBridge.ParentMapId WHERE MetadataSourceId = @id AND MetadataSourceType = 0;"; DataTable dataTable = db.ExecuteCMD(sql, new Dictionary { { "@id", Id } diff --git a/gaseous-server/Classes/MetadataManagement.cs b/gaseous-server/Classes/MetadataManagement.cs index 78a1cc1..aebd481 100644 --- a/gaseous-server/Classes/MetadataManagement.cs +++ b/gaseous-server/Classes/MetadataManagement.cs @@ -127,6 +127,31 @@ namespace gaseous_server.Classes db.ExecuteCMD(sql, dbDict); } + public static void UpdateMetadataMapItem(long metadataMapId, HasheousClient.Models.MetadataSources SourceType, long sourceId, bool? preferred = null) + { + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = ""; + Dictionary dbDict = new Dictionary() + { + { "@metadataMapId", metadataMapId }, + { "@sourceType", SourceType }, + { "@sourceId", sourceId }, + { "@preferred", preferred } + }; + + if (preferred == true) + { + // set all other items to not preferred + sql = "UPDATE MetadataMapBridge SET Preferred = 0 WHERE ParentMapId = @metadataMapId; UPDATE MetadataMapBridge SET MetadataSourceId = @sourceId, Preferred = @preferred WHERE ParentMapId = @metadataMapId AND MetadataSourceType = @sourceType;"; + db.ExecuteCMD(sql, dbDict); + } + else + { + sql = "UPDATE MetadataMapBridge SET MetadataSourceId = @sourceId WHERE ParentMapId = @metadataMapId AND MetadataSourceType = @sourceType;"; + db.ExecuteCMD(sql, dbDict); + } + } + /// /// Gets a metadata map from the database. /// @@ -333,7 +358,7 @@ namespace gaseous_server.Classes // disabling forceRefresh forceRefresh = false; - // update platforms + // update platform metadata sql = "SELECT Id, `Name` FROM Platform;"; dt = db.ExecuteCMD(sql); @@ -356,7 +381,68 @@ namespace gaseous_server.Classes } ClearStatus(); - // update games + // update rom signatures - only valid if Haseheous is enabled + if (Config.MetadataConfiguration.SignatureSource == MetadataModel.SignatureSources.Hasheous) + { + // get all ROMs in the database + sql = "SELECT * FROM view_Games_Roms;"; + dt = db.ExecuteCMD(sql); + + StatusCounter = 1; + foreach (DataRow dr in dt.Rows) + { + SetStatus(StatusCounter, dt.Rows.Count, "Refreshing signature for ROM " + dr["Name"]); + + try + { + Logging.Log(Logging.LogType.Information, "Metadata Refresh", "(" + StatusCounter + "/" + dt.Rows.Count + "): Refreshing signature for ROM " + dr["Name"] + " (" + dr["Id"] + ")"); + + // get the hash of the ROM from the datarow + string? md5 = dr["MD5"] == DBNull.Value ? null : dr["MD5"].ToString(); + string? sha1 = dr["SHA1"] == DBNull.Value ? null : dr["SHA1"].ToString(); + Common.hashObject hash = new Common.hashObject(); + if (md5 != null) + { + hash.md5hash = md5; + } + if (sha1 != null) + { + hash.sha1hash = sha1; + } + + // get the library for the ROM + GameLibrary.LibraryItem library = GameLibrary.GetLibrary((int)dr["LibraryId"]); + + // get the signature for the ROM + FileInfo fi = new FileInfo(dr["Path"].ToString()); + FileSignature fileSignature = new FileSignature(); + gaseous_server.Models.Signatures_Games signature = fileSignature.GetFileSignature(library, hash, fi, fi.FullName); + + // validate the signature - if it is invalid, skip the rest of the loop + // validation rules: 1) signature must not be null, 2) signature must have a platform ID + if (signature == null || signature.Flags.PlatformId == null) + { + Logging.Log(Logging.LogType.Information, "Metadata Refresh", "Signature for " + dr["RomName"] + " is invalid - skipping metadata refresh"); + StatusCounter += 1; + continue; + } + + // update the signature in the database + Platform? signaturePlatform = Metadata.Platforms.GetPlatform((long)signature.Flags.PlatformId); + ImportGame.StoreGame(library, hash, signature, signaturePlatform, fi.FullName, (long)dr["Id"], false); + } + catch (Exception ex) + { + Logging.Log(Logging.LogType.Critical, "Metadata Refresh", "An error occurred while refreshing metadata for " + dr["RomName"], ex); + } + + StatusCounter += 1; + } + ClearStatus(); + } + + + // update game metadata if (forceRefresh == true) { // when forced, only update games with ROMs for diff --git a/gaseous-server/Controllers/V1.0/GamesController.cs b/gaseous-server/Controllers/V1.0/GamesController.cs index 0dfae0b..997a9cc 100644 --- a/gaseous-server/Controllers/V1.0/GamesController.cs +++ b/gaseous-server/Controllers/V1.0/GamesController.cs @@ -318,8 +318,8 @@ namespace gaseous_server.Controllers return NotFound(); } - string basePath = Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Game(game), imageTypePath); - string imagePath = Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Game(game), imageTypePath, size.ToString(), imageId + ".jpg"); + string basePath = Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Game(game), imageTypePath, metadataMap.SourceType.ToString()); + string imagePath = Path.Combine(basePath, size.ToString(), imageId + ".jpg"); if (!System.IO.File.Exists(imagePath)) { @@ -796,16 +796,67 @@ namespace gaseous_server.Controllers [MapToApiVersion("1.1")] [HttpGet] [Route("{MetadataMapId}/metadata")] - [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(MetadataMap), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GameMetadataSources(long MetadataMapId) { try { - List metadataMapItems = Classes.MetadataManagement.GetMetadataMap(MetadataMapId).MetadataMapItems; + MetadataMap metadataMap = Classes.MetadataManagement.GetMetadataMap(MetadataMapId); - // return metadataMapItems after first removing any items where sourceType = "TheGamesDb" - return Ok(metadataMapItems.Where(x => x.SourceType != HasheousClient.Models.MetadataSources.TheGamesDb).ToList()); + // return metadataMap, but filter out metadataMapItems that = "TheGamesDb" + MetadataMap filteredMetadataMap = new MetadataMap(); + filteredMetadataMap.MetadataMapItems = metadataMap.MetadataMapItems.Where(x => x.SourceType != HasheousClient.Models.MetadataSources.TheGamesDb).ToList(); + + metadataMap.MetadataMapItems = filteredMetadataMap.MetadataMapItems; + + return Ok(metadataMap); + } + catch + { + return NotFound(); + } + } + + [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] + [HttpPut] + [Route("{MetadataMapId}/metadata")] + [Authorize(Roles = "Admin")] + [ProducesResponseType(typeof(MetadataMap), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GameMetadataSources(long MetadataMapId, List metadataMapItems) + { + try + { + MetadataMap existingMetadataMap = Classes.MetadataManagement.GetMetadataMap(MetadataMapId); + + if (existingMetadataMap != null) + { + foreach (MetadataMap.MetadataMapItem metadataMapItem in metadataMapItems) + { + if (metadataMapItem.SourceType != HasheousClient.Models.MetadataSources.None) + { + // check if existingMetadataMap.MetadataMapItems contains metadataMapItem.SourceType + MetadataMap.MetadataMapItem existingMetadataMapItem = existingMetadataMap.MetadataMapItems.FirstOrDefault(x => x.SourceType == metadataMapItem.SourceType); + + if (existingMetadataMapItem != null) + { + MetadataManagement.UpdateMetadataMapItem(MetadataMapId, metadataMapItem.SourceType, metadataMapItem.SourceId, metadataMapItem.Preferred); + } + else + { + MetadataManagement.AddMetadataMapItem(MetadataMapId, metadataMapItem.SourceType, metadataMapItem.SourceId, metadataMapItem.Preferred); + } + } + } + + return Ok(Classes.MetadataManagement.GetMetadataMap(MetadataMapId)); + } + else + { + return NotFound(); + } } catch { diff --git a/gaseous-server/Models/MetadataMap.cs b/gaseous-server/Models/MetadataMap.cs index 143091d..2f7bc0b 100644 --- a/gaseous-server/Models/MetadataMap.cs +++ b/gaseous-server/Models/MetadataMap.cs @@ -1,3 +1,4 @@ +using gaseous_server.Classes.Metadata; using HasheousClient.Models; namespace gaseous_server.Models @@ -25,6 +26,52 @@ namespace gaseous_server.Models public HasheousClient.Models.MetadataSources SourceType { get; set; } public long SourceId { get; set; } public bool Preferred { get; set; } + public string SourceSlug + { + get + { + string slug = ""; + switch (SourceType) + { + case MetadataSources.IGDB: + Game game = Games.GetGame(SourceType, (long)SourceId); + if (game != null) + { + slug = game.Slug; + } + break; + + default: + slug = SourceId.ToString(); + break; + } + + return slug; + } + } + public string link + { + get + { + string link = ""; + switch (SourceType) + { + case MetadataSources.IGDB: + link = $"https://www.igdb.com/games/{SourceSlug}"; + break; + + case MetadataSources.TheGamesDb: + link = $"https://thegamesdb.net/game.php?id={SourceId}"; + break; + + default: + link = ""; + break; + } + + return link; + } + } } } } \ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/game.js b/gaseous-server/wwwroot/pages/game.js index 7077141..652fd07 100644 --- a/gaseous-server/wwwroot/pages/game.js +++ b/gaseous-server/wwwroot/pages/game.js @@ -15,6 +15,7 @@ function SetupPage() { ajaxCall('/api/v1.1/Games/' + gameId, 'GET', function (result) { // populate games page gameData = result; + console.log(gameData); switch (gameData.metadataSource) { case "IGDB": @@ -43,7 +44,7 @@ function SetupPage() { if (gameData.total_rating_count) { var criticscorelabel = document.getElementById('gametitle_criticrating_label'); - criticscorelabel.innerHTML = ' User Rating
' + "based on " + gameData.total_rating_count + " votes
" + criticscorelabel.innerHTML = ' User rating
based on ' + gameData.total_rating_count + ' votes
' } } @@ -610,7 +611,116 @@ class RomManagement { this.#SetupFixPlatformDropDown(); // add buttons - let platformEditButton = new ModalButton('Edit Platform', 0, this, async function (callingObject) { + let platformMappingButton = new ModalButton('Metadata Mapping', 0, this, async function (callingObject) { + let metadataModal = await new Modal('messagebox'); + await metadataModal.BuildModal(); + + // override the dialog size + metadataModal.modalElement.style = 'width: 600px; height: 400px; min-width: unset; min-height: 400px; max-width: unset; max-height: 400px;'; + + // set the title + metadataModal.modalElement.querySelector('#modal-header-text').innerHTML = callingObject.Platform.name + ' Metadata Mapping'; + + // set the content + let metadataContent = metadataModal.modalElement.querySelector('#modal-body'); + + // fetch the metadata map + let metadataMap = await fetch('/api/v1.1/Games/' + gameId + '/metadata', { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }).then(response => response.json()); + console.log(metadataMap); + + metadataMap.metadataMapItems.forEach(element => { + let itemSection = document.createElement('div'); + itemSection.className = 'section'; + + // header + let itemSectionHeader = document.createElement('div'); + itemSectionHeader.className = 'section-header'; + + let itemSectionHeaderRadio = document.createElement('input'); + itemSectionHeaderRadio.id = 'platformMappingSource_' + element.sourceType; + itemSectionHeaderRadio.type = 'radio'; + itemSectionHeaderRadio.name = 'platformMappingSource'; + itemSectionHeaderRadio.value = element.sourceType; + itemSectionHeaderRadio.style.margin = '0px'; + itemSectionHeaderRadio.style.height = 'unset'; + itemSectionHeaderRadio.addEventListener('change', () => { + console.log('Selected: ' + element.sourceType); + }); + if (element.preferred == true) { + itemSectionHeaderRadio.checked = true; + } + itemSectionHeader.appendChild(itemSectionHeaderRadio); + + let itemSectionHeaderLabel = document.createElement('label'); + itemSectionHeaderLabel.htmlFor = 'platformMappingSource_' + element.sourceType; + itemSectionHeaderLabel.style.marginLeft = '10px'; + itemSectionHeaderLabel.innerHTML = element.sourceType; + itemSectionHeader.appendChild(itemSectionHeaderLabel); + + itemSection.appendChild(itemSectionHeader); + + // content + let itemSectionContent = document.createElement('div'); + itemSectionContent.className = 'section-body'; + switch (element.sourceType) { + case 'None': + let noneContent = document.createElement('div'); + noneContent.className = 'section-body-content'; + + let noneContentLabel = document.createElement('label'); + noneContentLabel.innerHTML = 'No Metadata Source'; + noneContent.appendChild(noneContentLabel); + + itemSectionContent.appendChild(noneContent); + break; + + default: + let contentLabel2 = document.createElement('div'); + contentLabel2.innerHTML = 'ID: ' + element.sourceId; + itemSectionContent.appendChild(contentLabel2); + + let contentLabel3 = document.createElement('div'); + contentLabel3.innerHTML = 'Slug: ' + element.sourceSlug; + itemSectionContent.appendChild(contentLabel3); + + if (element.link) { + if (element.link.length > 0) { + let contentLabel4 = document.createElement('div'); + contentLabel4.innerHTML = 'Link: ' + element.link; + itemSectionContent.appendChild(contentLabel4); + } + } + break; + + } + itemSection.appendChild(itemSectionContent); + + metadataContent.appendChild(itemSection); + }); + + + // setup the buttons + let okButton = new ModalButton('OK', 1, callingObject, async function (callingObject) { + metadataModal.close(); + }); + metadataModal.addButton(okButton); + + let cancelButton = new ModalButton('Cancel', 0, metadataModal, async function (callingObject) { + metadataModal.close(); + }); + metadataModal.addButton(cancelButton); + + // show the dialog + await metadataModal.open(); + }); + this.romsModal.addButton(platformMappingButton); + + let platformEditButton = new ModalButton('Configure Emulator', 0, this, async function (callingObject) { let mappingModal = await new Modal('messagebox'); await mappingModal.BuildModal(); diff --git a/gaseous-server/wwwroot/styles/style.css b/gaseous-server/wwwroot/styles/style.css index 91ecff4..5e02484 100644 --- a/gaseous-server/wwwroot/styles/style.css +++ b/gaseous-server/wwwroot/styles/style.css @@ -2923,7 +2923,7 @@ button:not(.select2-selection__choice__remove):not(.select2-selection__clear):no display: flex; flex-wrap: wrap; flex-direction: row; - gap: 5px; + gap: 10px; } .platform_item { @@ -2942,6 +2942,9 @@ button:not(.select2-selection__choice__remove):not(.select2-selection__clear):no align-items: center; position: relative; overflow: hidden; + box-shadow: 5px 5px 19px 0px rgba(0, 0, 0, 0.44); + -webkit-box-shadow: 5px 5px 19px 0px rgba(0, 0, 0, 0.44); + -moz-box-shadow: 5px 5px 19px 0px rgba(0, 0, 0, 0.44); } .platform_item:hover { @@ -2971,11 +2974,14 @@ button:not(.select2-selection__choice__remove):not(.select2-selection__clear):no padding-left: 20px; padding-right: 20px; background-color: white; + border-right-style: solid; + border-right-width: 1px; + border-right-color: #000000; } .platform_image { - width: 70px; - max-height: 70px; + max-width: 90px; + max-height: 60px; } .platform_name_container { @@ -3049,6 +3055,7 @@ button:not(.select2-selection__choice__remove):not(.select2-selection__clear):no .metadata-attribution-icon { width: 30px; height: 30px; - margin-right: 5px; + margin-right: 13px; + margin-left: 10px; filter: invert(1); } \ No newline at end of file