diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1bb4614
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*.code-workspace
\ No newline at end of file
diff --git a/README.md b/README.md
index a86b926..39d7958 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,11 @@
-# Resonite-Inventory-Viewer
+# Resonite Storage Browser
-A web-app that neatly displays the contents of a Resonite inventory.
\ No newline at end of file
+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.
diff --git a/index.css b/index.css
new file mode 100644
index 0000000..1482443
--- /dev/null
+++ b/index.css
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..13ae421
--- /dev/null
+++ b/index.html
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+ Resonite Inventory Viewer
+
+
+
+
+
+
+
Select your inventory .JSON file
+
+
Inventory browser created by Armored Dragon
+
+
+
+
+
+ Storage Usage 0 MB/ 1 GB
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Size:
+
Linked Assets:
+
Unique Assets:
+
+
+
+
+
+
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..3adfb66
--- /dev/null
+++ b/index.js
@@ -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();
+}
diff --git a/index.scss b/index.scss
new file mode 100644
index 0000000..4b93549
--- /dev/null
+++ b/index.scss
@@ -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;
+ }
+ }
+ }
+ }
+ }
+}