master
Armored Dragon 2023-11-16 13:13:08 -06:00
parent dfefb360cf
commit 676f1bf257
6 changed files with 846 additions and 2 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.code-workspace

View File

@ -1,3 +1,11 @@
# Resonite-Inventory-Viewer # Resonite Storage Browser
A web-app that neatly displays the contents of a Resonite inventory. This is a simple in-browser inventory viewer for Resonite. The goal for this app is to make it easier to find out what is eating so much storage space in a given account.
## Instructions
This website uses a JSON file produced by Resonite. In order to use this tool, you need to get your "Record Usage JSON" file.
To obtain this file you need to login to your Resonite account and send the Resonite bot the message `/requestRecordUsageJSON`. After you submit this command, Resonite will email you shortly after with an attachment that contains the file you will need.
Once you have your file, continue to [the website](https://armored-dragon.github.io/Resonite-Inventory-Viewer/) and select the `.json` file in the file browser input, and this app should take care of the rest.

266
index.css Normal file
View File

@ -0,0 +1,266 @@
body {
margin: 0;
background-color: #11151d;
height: 100vh;
display: flex;
color: #e1e1e0;
font-family: Verdana, Geneva, Tahoma, sans-serif;
}
.rounded-corners {
border-radius: 10px;
}
.page {
width: 1200px;
margin: 0 auto;
display: flex;
flex-direction: column;
}
.page .import-notice {
margin: auto;
width: 600px;
min-height: 100px;
background-color: #2b2f35;
padding: 5px;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.page .import-notice h1 {
color: #e1e1e0;
font-weight: bold;
font-size: 22px;
text-align: center;
}
.page .import-notice input {
margin: auto;
color: #cecece;
text-align: center;
padding: 5px;
box-sizing: border-box;
background-color: rgba(0, 0, 0, 0.3137254902);
margin-bottom: 15px;
}
.page .import-notice sub {
text-align: center;
color: #e1e1e0;
}
.page .import-notice sub a {
color: #ff6600;
}
.page .file-list {
display: flex;
flex-direction: column;
}
.page .file-list .storage-overview {
background-color: #2b2f35;
width: 100%;
margin-top: 20px;
padding: 10px;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.page .file-list .storage-overview .title {
font-size: 24px;
font-weight: bold;
margin-bottom: 10px;
display: flex;
flex-direction: row;
}
.page .file-list .storage-overview .title .measurement {
margin-left: auto;
font-size: 22px;
}
.page .file-list .storage-overview .title .measurement .storage-quota {
font-size: 16px;
color: #86888b;
}
.page .file-list .storage-overview .storage-bar {
height: 30px;
width: 100%;
background-color: rgba(0, 0, 0, 0.6392156863);
padding: 5px;
box-sizing: border-box;
border: 0;
}
.page .file-list .storage-overview .storage-bar::-moz-progress-bar,
.page .file-list .storage-overview .storage-bar::-webkit-progress-bar {
background-color: rgb(0, 162, 255);
border-radius: 5px;
}
.page .file-list .filters {
width: 100%;
height: 30px;
margin-top: 20px;
display: flex;
flex-direction: row;
}
.page .file-list .filters select {
background-color: #2b2f35;
border: 0;
padding: 0 10px;
width: 200px;
height: 100%;
color: white;
margin-right: 5px;
}
.page .file-list .filters label {
margin: auto 0;
}
.page .file-list .filters .search {
width: 50%;
height: 100%;
background-color: #2b2f35;
margin-left: auto;
}
.page .file-list .filters .search input {
background-color: rgba(0, 0, 0, 0.6392156863);
border: 0;
padding: 0;
outline: 0;
color: white;
height: 100%;
width: 100%;
text-indent: 10px;
}
.page .file-list .filters .search input::-moz-placeholder {
font-style: italic;
}
.page .file-list .filters .search input::placeholder {
font-style: italic;
}
.page .file-list .item-list {
display: flex;
flex-flow: row wrap;
place-content: space-around;
width: 100%;
height: -moz-fit-content;
height: fit-content;
margin-top: 20px;
}
.page .file-list .item-list .entry {
width: 200px;
background-color: #2b2f35;
padding: 5px;
margin-bottom: 20px;
}
.page .file-list .item-list .entry .thumbnail {
width: 200px;
height: 200px;
margin: 0 auto 5px auto;
display: flex;
}
.page .file-list .item-list .entry .thumbnail img {
max-height: 100%;
max-width: 100%;
margin: auto;
}
.page .file-list .item-list .entry .info .title {
margin-bottom: 10px;
color: #61d1fa;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.page .file-list .item-list .entry .info .info-blob {
color: gray;
display: flex;
flex-direction: row;
margin-bottom: 5px;
}
.page .file-list .item-list .entry .info .info-blob .value {
color: white;
margin-left: auto;
}
dialog {
background-color: #2b2f35;
border: 0;
outline: 0;
}
dialog .content {
display: flex;
flex-direction: column;
width: -moz-fit-content;
width: fit-content;
}
dialog .content .info {
display: flex;
flex-direction: row;
width: 700px;
}
dialog .content .info .left {
margin-right: 10px;
}
dialog .content .info .left .thumbnail {
width: 200px;
height: 200px;
}
dialog .content .info .left .thumbnail img {
max-width: 100%;
max-height: 100%;
}
dialog .content .info .right {
flex-grow: 1;
}
dialog .content .info .right .entry {
display: flex;
flex-direction: row;
color: white;
width: 100%;
background-color: #1d2024;
padding: 5px;
box-sizing: border-box;
}
dialog .content .info .right .entry .title {
color: #bbb;
}
dialog .content .info .right .entry .value {
margin-left: auto;
padding-left: 20px;
}
dialog .content .info .right .entry:nth-child(even) {
background-color: #191c20;
}
dialog .content .controls {
display: flex;
margin-top: 15px;
}
dialog .content .controls button {
margin: auto;
padding: 5px;
box-sizing: border-box;
flex-grow: 1;
cursor: pointer;
background-color: #ff7676;
border: 0;
font-weight: bold;
}
dialog::backdrop {
background-color: rgba(0, 0, 0, 0.6392156863);
}
.hidden {
display: none !important;
}
@media screen and (max-width: 1200px) {
.page {
width: 95%;
}
}
@media screen and (max-width: 800px) {
dialog .content .info {
flex-direction: column;
width: 100%;
}
dialog .content .info .left .thumbnail {
margin: auto;
width: 100%;
margin-bottom: 10px;
}
}

106
index.html Normal file
View File

@ -0,0 +1,106 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="index.css" />
<title>Resonite Inventory Viewer</title>
</head>
<body>
<dialog id="item-info-modal" class="rounded-corners">
<div class="content">
<div class="info">
<div class="left">
<div class="thumbnail">
<img data-thumbnail class="rounded-corners" src="" />
</div>
</div>
<div class="right">
<div class="entry">
<span class="title">Name</span>
<span data-name class="value"></span>
</div>
<div class="entry">
<span class="title">ID</span>
<span data-id class="value"></span>
</div>
<div class="entry">
<span class="title">Size</span>
<span data-size class="value"></span>
</div>
<div class="entry">
<span class="title">Path</span>
<span data-path class="value"></span>
</div>
<div class="entry">
<span class="title">Assets</span>
<span data-assets class="value"></span>
</div>
<div class="entry">
<span class="title">Unique Assets</span>
<span data-unique-assets class="value"></span>
</div>
<div class="entry">
<span class="title">Message Item?</span>
<span data-message-item class="value"></span>
</div>
</div>
</div>
<div class="controls">
<button class="rounded-corners" onclick="closeItemModal()">Close</button>
</div>
</div>
</dialog>
<div class="page">
<div id="file-selection-modal" class="import-notice rounded-corners">
<h1>Select your inventory .JSON file</h1>
<input id="file-import" class="rounded-corners" type="file" />
<sub>Inventory browser created by <a href="https://armoreddragon.com">Armored Dragon</a></sub>
</div>
<div id="file-browser" class="file-list hidden">
<div class="storage-overview rounded-corners">
<div class="title">
Storage Usage <span class="measurement"><span id="total-used-space">0 MB</span><span class="storage-quota">/ 1 GB</span></span>
</div>
<progress class="storage-bar rounded-corners" value="0" max="1"></progress>
</div>
<div class="filters">
<!-- <select class="rounded-corners">
<option disabled>Order</option>
<option>Estimate Size Asc</option>
<option>Estimate Size Desc</option>
</select> -->
<label><input type="checkbox" id="only-message-items" value="value" />Only Message Items</label>
<div class="search rounded-corners">
<input id="search" class="rounded-corners" type="text" placeholder="Search..." />
</div>
</div>
<div class="item-list"></div>
</div>
</div>
</body>
<template id="entry-template">
<div class="entry rounded-corners" data-info="">
<div class="thumbnail">
<img class="rounded-corners" src="" />
</div>
<div class="info">
<div class="id hidden"></div>
<div class="title"></div>
<div class="info-blob">Size:<span class="value"></span></div>
<div class="info-blob">Linked Assets:<span class="value"></span></div>
<div class="info-blob">Unique Assets:<span class="value"></span></div>
</div>
</div>
</template>
<script src="./index.js"></script>
</html>

147
index.js Normal file
View File

@ -0,0 +1,147 @@
let resonite_item_list = [];
let asset_list = []; // An "Asset" is a linked asset per each item. One item can have lots of assets.
let total_bytes = 0;
// QOL functions
const qs = (selector) => document.querySelector(selector);
const qsa = (selector) => document.querySelectorAll(selector);
// Elements
const file_import_input = qs("#file-import");
const item_list = qs("#file-browser .item-list");
const item_template = qs("#entry-template");
// Modal
const item_modal = qs("#item-info-modal");
const modal_name = qs("#item-info-modal [data-name]");
const modal_id = qs("#item-info-modal [data-id]");
const modal_size = qs("#item-info-modal [data-size]");
const modal_path = qs("#item-info-modal [data-path]");
const modal_assets = qs("#item-info-modal [data-assets]");
const modal_unique_assets = qs("#item-info-modal [data-unique-assets]");
const modal_message_item = qs("#item-info-modal [data-message-item]");
const modal_thumbnail = qs("#item-info-modal [data-thumbnail]");
const only_message_items_setting = qs("#only-message-items");
const search_setting = qs("#search");
// Event Listeners
file_import_input.addEventListener("change", newInventoryFile);
only_message_items_setting.addEventListener("change", filterChange);
search_setting.addEventListener("change", filterChange);
// Logic
function newInventoryFile() {
// Show the file list
qs("#file-selection-modal").classList.add("hidden");
qs("#file-browser").classList.remove("hidden");
// Get the file
if (file_import_input.files.length <= 0) return;
let reader = new FileReader();
reader.addEventListener("load", () => handleFileContents(JSON.parse(reader.result)));
reader.readAsText(file_import_input.files[0]);
}
function handleFileContents(inventory_json) {
asset_list = []; // Reset the asset list
item_list.innerHTML = ""; // Clear the asset list
inventory_json.forEach(newInventoryEntry);
// Calculate storage overview values
qs("#total-used-space").innerText = `${(total_bytes / 1000000).toFixed(2)} MB`;
qs(".storage-overview progress").value = total_bytes / 1000000000;
}
function filterChange() {
let settings = {
only_message_items: qs("#only-message-items").checked,
search: search_setting.value.toLowerCase(),
};
let filtered_list = [];
resonite_item_list.forEach((item) => {
if (settings.only_message_items && !item.message_item) return;
if (search && !item.name.toLowerCase().includes(settings.search)) return;
filtered_list.push(item);
});
item_list.innerHTML = ""; // Clear the asset list
filtered_list.forEach(addItemToView);
}
function newInventoryEntry(item) {
let estimate_size = 0;
let assets = 0;
let unique_assets = 0;
let thumbnail_url = "";
// Get the linked assets of the item
item.assetManifest.forEach((asset) => {
const in_array = asset_list.some((obj) => obj.hash === asset.hash);
assets++;
if (!in_array) {
asset_list.push(asset);
estimate_size += asset.bytes;
total_bytes += asset.bytes;
unique_assets++;
}
});
// Get the thumbnail
if (item.thumbnailUri) thumbnail_url = `https://assets.resonite.com/${item.thumbnailUri.replace("resdb:///", "").replace(".webp", "")}`;
// Format the list to allow the user to easily add additional filters
const item_info = {
name: item.name,
directory: item.path,
estimate_size: (estimate_size / 1000000).toFixed(2),
assets: assets,
unique_assets: unique_assets,
message_item: item.tags.includes("message_item"),
id: item.id,
thumbnail_url: thumbnail_url,
};
resonite_item_list.push(item_info);
addItemToView(item_info);
}
function addItemToView(item_info) {
// Add the item to the view
const item_html = item_template.content.cloneNode(true);
// Edit the contents of the template
item_html.querySelector(".thumbnail img").src = item_info.thumbnail_url;
item_html.querySelector(".title").innerText = `${item_info.name}`;
item_html.querySelectorAll(".info-blob .value")[0].innerText = `${item_info.estimate_size} MB`;
item_html.querySelectorAll(".info-blob .value")[1].innerText = `${item_info.assets}`;
item_html.querySelectorAll(".info-blob .value")[2].innerText = `${item_info.unique_assets}`;
item_html.querySelector(".id").innerText = item_info.id;
// Add to the list
item_list.appendChild(item_html);
item_list.children[item_list.children.length - 1].setAttribute("onclick", `showItemInModal(${JSON.stringify(item_info)})`);
}
function showItemInModal(item_info) {
modal_name.innerText = item_info.name;
modal_id.innerText = item_info.id;
modal_size.innerText = `${item_info.estimate_size} MB`;
modal_path.innerText = item_info.directory;
modal_assets.innerText = item_info.assets;
modal_unique_assets.innerText = item_info.unique_assets;
modal_message_item.innerText = item_info.message_item;
modal_thumbnail.src = item_info.thumbnail_url;
item_modal.showModal();
}
function closeItemModal() {
item_modal.close();
}

316
index.scss Normal file
View File

@ -0,0 +1,316 @@
$n-primary: #11151d;
$n-secondary: #2b2f35;
$n-tertiary: #86888b;
$n-quaternary: #e1e1e0; // WHY IS IT 0? OCD #triggered
$h-yellow: #f8f770;
$h-green: #59eb5c;
$h-red: #ff7676;
$h-purple: #ba64f2;
$h-blue: #61d1fa;
$h-orange: #e69e50;
$bg-stacker: #000000a3;
body {
margin: 0;
background-color: #11151d;
height: 100vh;
display: flex;
color: $n-quaternary;
font-family: Verdana, Geneva, Tahoma, sans-serif;
}
.rounded-corners {
border-radius: 10px;
}
.page {
width: 1200px;
margin: 0 auto;
display: flex;
flex-direction: column;
.import-notice {
margin: auto;
width: 600px;
min-height: 100px;
background-color: #2b2f35;
padding: 5px;
box-sizing: border-box;
display: flex;
flex-direction: column;
h1 {
color: $n-quaternary;
font-weight: bold;
font-size: 22px;
text-align: center;
}
input {
margin: auto;
color: #cecece;
text-align: center;
padding: 5px;
box-sizing: border-box;
background-color: #00000050;
margin-bottom: 15px;
}
sub {
text-align: center;
color: $n-quaternary;
a {
color: #ff6600;
}
}
}
.file-list {
display: flex;
flex-direction: column;
.storage-overview {
background-color: $n-secondary;
width: 100%;
margin-top: 20px;
padding: 10px;
box-sizing: border-box;
display: flex;
flex-direction: column;
.title {
font-size: 24px;
font-weight: bold;
margin-bottom: 10px;
display: flex;
flex-direction: row;
.measurement {
margin-left: auto;
font-size: 22px;
.storage-quota {
font-size: 16px;
color: $n-tertiary;
}
}
}
.storage-bar {
height: 30px;
width: 100%;
background-color: $bg-stacker;
padding: 5px;
box-sizing: border-box;
border: 0;
}
.storage-bar::-moz-progress-bar,
.storage-bar::-webkit-progress-bar {
background-color: rgb(0, 162, 255);
border-radius: 5px;
}
}
.filters {
width: 100%;
height: 30px;
margin-top: 20px;
display: flex;
flex-direction: row;
select {
background-color: $n-secondary;
border: 0;
padding: 0 10px;
width: 200px;
height: 100%;
color: white;
margin-right: 5px;
}
label {
margin: auto 0;
}
.search {
width: 50%;
height: 100%;
background-color: $n-secondary;
margin-left: auto;
input {
background-color: $bg-stacker;
border: 0;
padding: 0;
outline: 0;
color: white;
height: 100%;
width: 100%;
text-indent: 10px;
}
input::placeholder {
font-style: italic;
}
}
}
.item-list {
display: flex;
flex-flow: row wrap;
place-content: space-around;
width: 100%;
height: fit-content;
margin-top: 20px;
.entry {
width: 200px;
// height: 200px;
background-color: $n-secondary;
padding: 5px;
margin-bottom: 20px;
.thumbnail {
width: 200px;
height: 200px;
margin: 0 auto 5px auto;
display: flex;
img {
max-height: 100%;
max-width: 100%;
margin: auto;
}
}
.info {
.title {
margin-bottom: 10px;
color: $h-blue;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.info-blob {
color: gray;
display: flex;
flex-direction: row;
margin-bottom: 5px;
.value {
color: white;
margin-left: auto;
}
}
}
}
}
}
}
dialog {
background-color: $n-secondary;
border: 0;
outline: 0;
.content {
display: flex;
flex-direction: column;
width: fit-content;
.info {
display: flex;
flex-direction: row;
width: 700px;
.left {
margin-right: 10px;
.thumbnail {
width: 200px;
height: 200px;
img {
max-width: 100%;
max-height: 100%;
}
}
}
.right {
flex-grow: 1;
.entry {
display: flex;
flex-direction: row;
color: white;
width: 100%;
background-color: #1d2024;
padding: 5px;
box-sizing: border-box;
.title {
color: #bbb;
}
.value {
margin-left: auto;
padding-left: 20px;
}
}
.entry:nth-child(even) {
background-color: #191c20;
}
}
}
.controls {
display: flex;
margin-top: 15px;
button {
margin: auto;
padding: 5px;
box-sizing: border-box;
flex-grow: 1;
cursor: pointer;
background-color: $h-red;
border: 0;
font-weight: bold;
}
}
}
}
dialog::backdrop {
background-color: $bg-stacker;
}
.hidden {
display: none !important;
}
@media screen and (max-width: 1200px) {
.page {
width: 95%;
}
}
@media screen and (max-width: 800px) {
dialog {
.content {
.info {
flex-direction: column;
width: 100%;
.left {
.thumbnail {
margin: auto;
width: 100%;
margin-bottom: 10px;
}
}
}
}
}
}