// AssetVaultWindow.cs (v3 — polished UI) // Drop into your Unity project at: Assets/Editor/AssetVaultWindow.cs // Then open: Window > AssetVault Browser // // Source: https://assets.15.204.92.198.sslip.io/AssetVaultWindow.cs using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using UnityEditor; using UnityEngine; using UnityEngine.Networking; public class AssetVaultWindow : EditorWindow { // ======================================================================= // Config // ======================================================================= const string DEFAULT_BASE_URL = "https://assets.15.204.92.198.sslip.io"; const string PREF_KEY_URL = "AssetVault.BaseUrl"; static readonly string[] CATEGORIES = { "all", "image", "audio", "video", "model", "font", "code", "archive", "other" }; static readonly string[] CATEGORY_LABELS = { "All", "Images", "Audio", "Video", "Models", "Fonts", "Code", "Archives", "Other" }; // ======================================================================= // Design tokens // ======================================================================= static class T { public static readonly Color BG = new Color(0.043f, 0.063f, 0.114f); // slate-900 public static readonly Color BG_CARD = new Color(0.118f, 0.161f, 0.231f); // slate-800 public static readonly Color BG_CARD_HOVER = new Color(0.176f, 0.227f, 0.318f); // slate-700ish public static readonly Color BG_INPUT = new Color(0.027f, 0.043f, 0.078f); public static readonly Color BG_THUMB = new Color(0.078f, 0.114f, 0.180f); public static readonly Color BORDER = new Color(0.200f, 0.255f, 0.333f); // slate-700 public static readonly Color TEXT = new Color(0.961f, 0.969f, 0.984f); // slate-50 public static readonly Color TEXT_MUTED = new Color(0.580f, 0.640f, 0.722f); // slate-400 public static readonly Color TEXT_DIM = new Color(0.400f, 0.455f, 0.529f); // slate-500 public static readonly Color ACCENT = new Color(0.388f, 0.400f, 0.945f); // indigo-500 public static readonly Color ACCENT_DARK = new Color(0.310f, 0.310f, 0.776f); public static readonly Color DANGER = new Color(0.937f, 0.267f, 0.267f); // rose-500 public static readonly Color SUCCESS = new Color(0.063f, 0.722f, 0.506f); // emerald-500 public static readonly Color CAT_IMAGE = new Color(0.925f, 0.282f, 0.600f); // pink-500 public static readonly Color CAT_AUDIO = new Color(0.063f, 0.722f, 0.506f); // emerald-500 public static readonly Color CAT_VIDEO = new Color(0.659f, 0.333f, 0.969f); // purple-500 public static readonly Color CAT_MODEL = new Color(0.024f, 0.714f, 0.831f); // cyan-500 public static readonly Color CAT_FONT = new Color(0.961f, 0.620f, 0.043f); // amber-500 public static readonly Color CAT_CODE = new Color(0.055f, 0.647f, 0.914f); // sky-500 public static readonly Color CAT_ARCHIVE = new Color(0.976f, 0.451f, 0.086f); // orange-500 public static readonly Color CAT_OTHER = new Color(0.392f, 0.455f, 0.545f); // slate-500 public static readonly Color CAT_FOLDER = new Color(0.961f, 0.620f, 0.043f); // amber-500 public static Color Cat(string c) { switch (c) { case "image": return CAT_IMAGE; case "audio": return CAT_AUDIO; case "video": return CAT_VIDEO; case "model": return CAT_MODEL; case "font": return CAT_FONT; case "code": return CAT_CODE; case "archive": return CAT_ARCHIVE; default: return CAT_OTHER; } } } // ======================================================================= // Cached styles & textures // ======================================================================= static GUIStyle _sTitle, _sBody, _sBodyMuted, _sMini, _sMiniMuted, _sMiniBold, _sPill, _sBtnLabel, _sSearch, _sBig, _sBigMuted; static Texture2D _pixel; static Texture2D Pixel() { if (_pixel == null) { _pixel = new Texture2D(1, 1, TextureFormat.RGBA32, false); _pixel.SetPixel(0, 0, Color.white); _pixel.Apply(); _pixel.hideFlags = HideFlags.HideAndDontSave; } return _pixel; } static void EnsureStyles() { if (_sTitle != null) return; _sTitle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 13, normal = { textColor = T.TEXT } }; _sBody = new GUIStyle(EditorStyles.label) { fontSize = 11, normal = { textColor = T.TEXT } }; _sBodyMuted = new GUIStyle(EditorStyles.label) { fontSize = 11, normal = { textColor = T.TEXT_MUTED } }; _sMini = new GUIStyle(EditorStyles.miniLabel) { fontSize = 10, normal = { textColor = T.TEXT_MUTED } }; _sMiniMuted = new GUIStyle(EditorStyles.miniLabel) { fontSize = 10, normal = { textColor = T.TEXT_DIM } }; _sMiniBold = new GUIStyle(EditorStyles.miniBoldLabel) { fontSize = 11, normal = { textColor = T.TEXT } }; _sPill = new GUIStyle(EditorStyles.miniBoldLabel) { fontSize = 11, alignment = TextAnchor.MiddleCenter }; _sBtnLabel = new GUIStyle(EditorStyles.miniBoldLabel) { fontSize = 11, alignment = TextAnchor.MiddleCenter, normal = { textColor = Color.white } }; _sSearch = new GUIStyle(EditorStyles.label) { fontSize = 12, padding = new RectOffset(8, 8, 4, 4), normal = { textColor = T.TEXT } }; _sBig = new GUIStyle(EditorStyles.boldLabel) { fontSize = 22, alignment = TextAnchor.MiddleCenter, normal = { textColor = T.TEXT } }; _sBigMuted = new GUIStyle(EditorStyles.label) { fontSize = 12, alignment = TextAnchor.MiddleCenter, normal = { textColor = T.TEXT_MUTED } }; } // ======================================================================= // State // ======================================================================= string baseUrl; string search = ""; string status = ""; double statusUntil; string selectedCategory = "all"; Vector2 scroll; string currentFolderId; string currentFolderName; string playingId; bool loading; readonly List assets = new List(); readonly List folders = new List(); readonly Dictionary thumbs = new Dictionary(); readonly HashSet thumbsRequested = new HashSet(); readonly Dictionary audioCache = new Dictionary(); readonly HashSet audioLoading = new HashSet(); // ======================================================================= // Data classes // ======================================================================= [System.Serializable] public class AssetMeta { public string id; public string original_name; public string mime_type; public long size; public string category; public string tags; public string folder_id; } [System.Serializable] class AssetWrapper { public AssetMeta[] items; } [System.Serializable] public class FolderMeta { public string id; public string name; public long created_at; } [System.Serializable] class FolderWrapper { public FolderMeta[] items; } // ======================================================================= // Lifecycle // ======================================================================= [MenuItem("Window/AssetVault Browser")] public static void Open() { var w = GetWindow("AssetVault"); w.minSize = new Vector2(520, 620); w.Show(); } void OnEnable() { baseUrl = EditorPrefs.GetString(PREF_KEY_URL, DEFAULT_BASE_URL); wantsMouseMove = true; EditorApplication.update += UpdateTick; if (assets.Count == 0) Refresh(); } void OnDisable() { EditorApplication.update -= UpdateTick; StopPreview(); } void UpdateTick() { bool repaint = false; if (!string.IsNullOrEmpty(status) && EditorApplication.timeSinceStartup > statusUntil) { status = ""; repaint = true; } if (audioLoading.Count > 0) repaint = true; if (repaint) Repaint(); } void SetStatus(string msg, float seconds = 3f) { status = msg; statusUntil = EditorApplication.timeSinceStartup + seconds; Repaint(); } // ======================================================================= // OnGUI // ======================================================================= void OnGUI() { EnsureStyles(); // Full-window background var winRect = new Rect(0, 0, position.width, position.height); EditorGUI.DrawRect(winRect, T.BG); if (Event.current.type == EventType.MouseMove) Repaint(); DrawHeader(); DrawSearch(); DrawTabsAndBreadcrumb(); DrawSubheader(); DrawGrid(); DrawStatusToast(); } // ======================================================================= // Header // ======================================================================= void DrawHeader() { var rect = GUILayoutUtility.GetRect(position.width, 44, GUILayout.ExpandWidth(true)); DrawHorizontalRule(new Rect(rect.x, rect.yMax - 1, rect.width, 1), T.BORDER); // Title var titleRect = new Rect(rect.x + 14, rect.y + 8, 160, 24); var oldColor = GUI.color; GUI.color = T.ACCENT; GUI.Label(titleRect, "■", new GUIStyle(EditorStyles.boldLabel) { fontSize = 16 }); GUI.color = oldColor; GUI.Label(new Rect(titleRect.x + 18, titleRect.y + 1, 160, 24), "AssetVault", _sTitle); // URL field var urlRect = new Rect(rect.x + 200, rect.y + 11, position.width - 200 - 130, 22); DrawInputBg(urlRect); EditorGUI.BeginChangeCheck(); var newUrl = GUI.TextField(new Rect(urlRect.x + 6, urlRect.y + 3, urlRect.width - 12, urlRect.height - 6), baseUrl, _sBody); if (EditorGUI.EndChangeCheck()) { baseUrl = (newUrl ?? "").Trim(); EditorPrefs.SetString(PREF_KEY_URL, baseUrl); } // Refresh var refreshRect = new Rect(position.width - 122, rect.y + 11, 56, 22); if (DrawSecondaryButton(refreshRect, loading ? "…" : "Refresh")) Refresh(); // Stop var stopRect = new Rect(position.width - 60, rect.y + 11, 46, 22); bool playing = !string.IsNullOrEmpty(playingId); var oldEnabled = GUI.enabled; GUI.enabled = playing; if (DrawIconButton(stopRect, "■ Stop", playing ? T.DANGER : T.BG_CARD, playing ? Color.white : T.TEXT_DIM)) StopPreview(); GUI.enabled = oldEnabled; } // ======================================================================= // Search // ======================================================================= void DrawSearch() { var rect = GUILayoutUtility.GetRect(position.width, 38, GUILayout.ExpandWidth(true)); var inner = new Rect(rect.x + 14, rect.y + 6, rect.width - 28, 26); DrawInputBg(inner); // Magnifier icon var iconRect = new Rect(inner.x + 8, inner.y + 6, 14, 14); var icon = EditorGUIUtility.IconContent("d_Search Icon").image ?? EditorGUIUtility.IconContent("Search Icon").image; if (icon != null) { var c = GUI.color; GUI.color = T.TEXT_DIM; GUI.DrawTexture(iconRect, icon, ScaleMode.ScaleToFit); GUI.color = c; } // Field var fieldRect = new Rect(inner.x + 28, inner.y + 4, inner.width - 36, inner.height - 8); EditorGUI.BeginChangeCheck(); var s = string.IsNullOrEmpty(search) ? "" : search; var ns = GUI.TextField(fieldRect, s, _sBody); if (EditorGUI.EndChangeCheck()) { search = ns; Repaint(); } // Placeholder if (string.IsNullOrEmpty(search) && Event.current.type == EventType.Repaint) { var oldColor = GUI.color; GUI.color = T.TEXT_DIM; GUI.Label(new Rect(fieldRect.x, fieldRect.y - 1, fieldRect.width, fieldRect.height), "Search assets, tags, folders…", _sBody); GUI.color = oldColor; } } // ======================================================================= // Tabs (pill-style) + breadcrumb // ======================================================================= void DrawTabsAndBreadcrumb() { // Breadcrumb row (only when inside a folder) if (!string.IsNullOrEmpty(currentFolderId)) { var bc = GUILayoutUtility.GetRect(position.width, 26, GUILayout.ExpandWidth(true)); var backRect = new Rect(bc.x + 14, bc.y + 2, 70, 22); if (DrawSecondaryButton(backRect, "← All")) LeaveFolder(); var nameRect = new Rect(backRect.xMax + 10, bc.y + 4, position.width - backRect.xMax - 24, 22); var oldColor = GUI.color; GUI.color = T.CAT_FOLDER; GUI.Label(new Rect(nameRect.x, nameRect.y, 18, 18), "▸", _sMiniBold); GUI.color = oldColor; GUI.Label(new Rect(nameRect.x + 14, nameRect.y, nameRect.width - 14, 22), " " + (currentFolderName ?? ""), _sTitle); } // Pill tabs var present = new HashSet(assets.Select(a => a.category ?? "other")); var visibleIdxs = new List(); for (int i = 0; i < CATEGORIES.Length; i++) if (i == 0 || present.Contains(CATEGORIES[i])) visibleIdxs.Add(i); var rect = GUILayoutUtility.GetRect(position.width, 38, GUILayout.ExpandWidth(true)); float x = rect.x + 14; float y = rect.y + 6; float lineH = 26; float maxX = rect.xMax - 14; foreach (var idx in visibleIdxs) { var cat = CATEGORIES[idx]; var label = CATEGORY_LABELS[idx]; int n = idx == 0 ? assets.Count : assets.Count(a => a.category == cat); var fullLabel = label + " " + n; var size = _sPill.CalcSize(new GUIContent(fullLabel)); float w = size.x + 22; if (x + w > maxX) { x = rect.x + 14; y += lineH + 2; } var pillRect = new Rect(x, y, w, 22); bool selected = selectedCategory == cat; Color accent = idx == 0 ? T.ACCENT : T.Cat(cat); if (DrawPillTab(pillRect, fullLabel, selected, accent)) { selectedCategory = cat; } x += w + 6; } } // ======================================================================= // Subheader (asset count + import target) // ======================================================================= void DrawSubheader() { var rect = GUILayoutUtility.GetRect(position.width, 24, GUILayout.ExpandWidth(true)); int total = assets.Count; int filtered = total; if (selectedCategory != "all") filtered = assets.Count(a => a.category == selectedCategory); var leftRect = new Rect(rect.x + 14, rect.y + 4, position.width / 2 - 14, 18); var c = GUI.color; GUI.color = T.TEXT_MUTED; var label = filtered + (filtered == 1 ? " asset" : " assets"); if (filtered != total) label += " · " + total + " in scope"; GUI.Label(leftRect, label, _sMini); GUI.color = c; var rightRect = new Rect(position.width / 2, rect.y + 4, position.width / 2 - 14, 18); c = GUI.color; GUI.color = T.TEXT_DIM; var rightStyle = new GUIStyle(_sMiniMuted) { alignment = TextAnchor.MiddleRight }; GUI.Label(rightRect, "Import to: " + GetSelectedFolder(), rightStyle); GUI.color = c; } // ======================================================================= // Grid // ======================================================================= void DrawGrid() { const int cardW = 148; const int cardH = 208; const int gap = 10; // Filter IEnumerable visAssets = assets; if (selectedCategory != "all") visAssets = visAssets.Where(a => a.category == selectedCategory); if (!string.IsNullOrEmpty(search)) { var s = search.ToLowerInvariant(); visAssets = visAssets.Where(a => (a.original_name ?? "").ToLowerInvariant().Contains(s) || (a.tags ?? "").ToLowerInvariant().Contains(s)); } var assetList = visAssets.ToList(); IEnumerable visFolders = string.IsNullOrEmpty(currentFolderId) ? folders : Enumerable.Empty(); if (!string.IsNullOrEmpty(search)) visFolders = visFolders.Where(f => (f.name ?? "").ToLowerInvariant().Contains(search.ToLowerInvariant())); var folderList = visFolders.ToList(); // Empty state if (folderList.Count == 0 && assetList.Count == 0) { DrawEmptyState(); // Still need a scrollable area so layout is consistent scroll = EditorGUILayout.BeginScrollView(scroll); EditorGUILayout.EndScrollView(); return; } scroll = EditorGUILayout.BeginScrollView(scroll, GUILayout.ExpandHeight(true)); // Compute cols int contentW = (int)position.width - 28; // 14 padding each side int cols = Mathf.Max(1, (contentW + gap) / (cardW + gap)); // Render in rows manually so we control rect placement int total = folderList.Count + assetList.Count; int rows = (total + cols - 1) / cols; var areaRect = GUILayoutUtility.GetRect(contentW, rows * (cardH + gap) + gap, GUILayout.ExpandWidth(true)); float startX = areaRect.x + 14; float startY = areaRect.y + gap; int idx = 0; // Folders first foreach (var f in folderList) { int row = idx / cols, col = idx % cols; var r = new Rect(startX + col * (cardW + gap), startY + row * (cardH + gap), cardW, cardH); DrawFolderCard(r, f); idx++; } // Then assets foreach (var a in assetList) { int row = idx / cols, col = idx % cols; var r = new Rect(startX + col * (cardW + gap), startY + row * (cardH + gap), cardW, cardH); DrawAssetCard(r, a); idx++; } EditorGUILayout.EndScrollView(); } void DrawEmptyState() { var area = GUILayoutUtility.GetRect(position.width, 200, GUILayout.ExpandWidth(true)); var box = new Rect(area.x, area.y + 30, area.width, 140); // Big icon var iconRect = new Rect(box.x, box.y, box.width, 60); var c = GUI.color; GUI.color = T.TEXT_DIM; GUI.Label(iconRect, loading ? "…" : "□", new GUIStyle(_sBig) { fontSize = 40 }); GUI.color = c; var titleRect = new Rect(box.x, box.y + 64, box.width, 24); GUI.Label(titleRect, loading ? "Loading…" : (string.IsNullOrEmpty(search) ? "Nothing here yet" : "No matches"), _sBig); var descRect = new Rect(box.x, box.y + 94, box.width, 36); var desc = loading ? "Talking to " + baseUrl : !string.IsNullOrEmpty(search) ? "Try a different search term." : "Drop files into the AssetVault PWA, then click Refresh."; GUI.Label(descRect, desc, _sBigMuted); } // ======================================================================= // Asset card // ======================================================================= void DrawAssetCard(Rect r, AssetMeta a) { var ev = Event.current; bool hovered = r.Contains(ev.mousePosition); Color accent = T.Cat(a.category); // Card background DrawCardBg(r, accent, hovered); // Layout regions const int pad = 8; const int stripeH = 3; var thumbRect = new Rect(r.x + pad, r.y + stripeH + pad, r.width - pad * 2, r.width - pad * 2); var nameRect = new Rect(r.x + pad, thumbRect.yMax + 4, r.width - pad * 2, 14); var metaRect = new Rect(r.x + pad, nameRect.yMax, r.width - pad * 2, 12); var btnRect = new Rect(r.x + pad, r.yMax - pad - 22, r.width - pad * 2, 22); // Thumbnail bg if (ev.type == EventType.Repaint) EditorGUI.DrawRect(thumbRect, T.BG_THUMB); // Thumbnail content var tex = GetOrFetchThumb(a); if (tex != null) { DrawTextureLetterboxed(thumbRect, tex); } else { var icon = GetCategoryIcon(a.category); if (icon != null && ev.type == EventType.Repaint) { var iconRect = new Rect(thumbRect.x + thumbRect.width / 2f - 22, thumbRect.y + thumbRect.height / 2f - 22 - 6, 44, 44); var oldColor = GUI.color; GUI.color = new Color(accent.r, accent.g, accent.b, 0.75f); GUI.DrawTexture(iconRect, icon, ScaleMode.ScaleToFit); GUI.color = oldColor; } // Extension hint var ext = (Path.GetExtension(a.original_name ?? "") ?? "").TrimStart('.').ToUpperInvariant(); if (!string.IsNullOrEmpty(ext) && ev.type == EventType.Repaint) { var extRect = new Rect(thumbRect.x, thumbRect.yMax - 16, thumbRect.width, 14); var s = new GUIStyle(_sMini) { alignment = TextAnchor.MiddleCenter, normal = { textColor = T.TEXT_DIM } }; GUI.Label(extRect, ext, s); } } // Audio play overlay if (a.category == "audio") { var btn = new Rect(thumbRect.xMax - 30, thumbRect.yMax - 30, 26, 26); bool isPlaying = playingId == a.id; bool isLoading = audioLoading.Contains(a.id); string lbl = isLoading ? Spinner() : (isPlaying ? "■" : "▶"); Color bg = isPlaying ? T.DANGER : T.SUCCESS; if (DrawIconButton(btn, lbl, bg, Color.white)) { if (isPlaying) StopPreview(); else PlayAudio(a); } } // Name (bold) if (ev.type == EventType.Repaint) GUI.Label(nameRect, TruncateMid(a.original_name, 22), _sMiniBold); // Meta line: category dot + size if (ev.type == EventType.Repaint) { var dotRect = new Rect(metaRect.x, metaRect.y + 3, 6, 6); EditorGUI.DrawRect(dotRect, accent); var rest = new Rect(metaRect.x + 10, metaRect.y, metaRect.width - 10, metaRect.height); GUI.Label(rest, a.category + " · " + FormatSize(a.size), _sMini); } // Import button if (DrawPrimaryButton(btnRect, "Import", accent)) ImportAsset(a); // Double-click to import (anywhere on the thumb area) if (ev.type == EventType.MouseDown && ev.button == 0 && ev.clickCount == 2 && thumbRect.Contains(ev.mousePosition)) { ImportAsset(a); ev.Use(); } } // ======================================================================= // Folder card // ======================================================================= void DrawFolderCard(Rect r, FolderMeta f) { var ev = Event.current; bool hovered = r.Contains(ev.mousePosition); DrawCardBg(r, T.CAT_FOLDER, hovered); const int pad = 8; const int stripeH = 3; var thumbRect = new Rect(r.x + pad, r.y + stripeH + pad, r.width - pad * 2, r.width - pad * 2); var nameRect = new Rect(r.x + pad, thumbRect.yMax + 4, r.width - pad * 2, 14); var metaRect = new Rect(r.x + pad, nameRect.yMax, r.width - pad * 2, 12); var btnRect = new Rect(r.x + pad, r.yMax - pad - 22, r.width - pad * 2, 22); if (ev.type == EventType.Repaint) EditorGUI.DrawRect(thumbRect, new Color(T.CAT_FOLDER.r, T.CAT_FOLDER.g, T.CAT_FOLDER.b, 0.06f)); // Folder icon, big var icon = EditorGUIUtility.IconContent("Folder Icon").image; if (icon != null && ev.type == EventType.Repaint) { var iconRect = new Rect(thumbRect.x + thumbRect.width / 2f - 30, thumbRect.y + thumbRect.height / 2f - 30, 60, 60); GUI.DrawTexture(iconRect, icon, ScaleMode.ScaleToFit); } if (ev.type == EventType.Repaint) GUI.Label(nameRect, TruncateMid(f.name, 22), _sMiniBold); if (ev.type == EventType.Repaint) { var dotRect = new Rect(metaRect.x, metaRect.y + 3, 6, 6); EditorGUI.DrawRect(dotRect, T.CAT_FOLDER); var rest = new Rect(metaRect.x + 10, metaRect.y, metaRect.width - 10, metaRect.height); GUI.Label(rest, "folder", _sMini); } if (DrawPrimaryButton(btnRect, "Open", T.CAT_FOLDER)) EnterFolder(f); if (ev.type == EventType.MouseDown && ev.button == 0 && ev.clickCount == 2 && thumbRect.Contains(ev.mousePosition)) { EnterFolder(f); ev.Use(); } } // ======================================================================= // Status toast (bottom) // ======================================================================= void DrawStatusToast() { if (string.IsNullOrEmpty(status)) return; var toastH = 28; var rect = new Rect(14, position.height - toastH - 10, position.width - 28, toastH); if (Event.current.type == EventType.Repaint) { EditorGUI.DrawRect(rect, new Color(T.BG_CARD.r, T.BG_CARD.g, T.BG_CARD.b, 0.96f)); DrawBorder(rect, T.BORDER, 1); var dot = new Rect(rect.x + 10, rect.y + rect.height / 2f - 3, 6, 6); EditorGUI.DrawRect(dot, T.ACCENT); var lblRect = new Rect(rect.x + 24, rect.y, rect.width - 36, rect.height); var s = new GUIStyle(_sBody) { alignment = TextAnchor.MiddleLeft }; GUI.Label(lblRect, status, s); } } // ======================================================================= // Drawing primitives // ======================================================================= static void DrawHorizontalRule(Rect r, Color c) { if (Event.current.type == EventType.Repaint) EditorGUI.DrawRect(r, c); } static void DrawBorder(Rect r, Color c, float thickness) { EditorGUI.DrawRect(new Rect(r.x, r.y, r.width, thickness), c); EditorGUI.DrawRect(new Rect(r.x, r.yMax - thickness, r.width, thickness), c); EditorGUI.DrawRect(new Rect(r.x, r.y, thickness, r.height), c); EditorGUI.DrawRect(new Rect(r.xMax - thickness, r.y, thickness, r.height), c); } static void DrawInputBg(Rect r) { if (Event.current.type != EventType.Repaint) return; EditorGUI.DrawRect(r, T.BG_INPUT); DrawBorder(r, T.BORDER, 1); } static void DrawCardBg(Rect r, Color accent, bool hovered) { if (Event.current.type != EventType.Repaint) return; EditorGUI.DrawRect(r, hovered ? T.BG_CARD_HOVER : T.BG_CARD); // Top accent stripe EditorGUI.DrawRect(new Rect(r.x, r.y, r.width, 3), accent); DrawBorder(r, hovered ? new Color(accent.r, accent.g, accent.b, 0.6f) : T.BORDER, 1); } static bool DrawPillTab(Rect rect, string label, bool selected, Color accent) { bool clicked = GUI.Button(rect, GUIContent.none, GUIStyle.none); EditorGUIUtility.AddCursorRect(rect, MouseCursor.Link); if (Event.current.type == EventType.Repaint) { bool hovered = rect.Contains(Event.current.mousePosition); Color bg, fg; if (selected) { bg = accent; fg = Color.white; } else if (hovered) { bg = T.BG_CARD_HOVER; fg = T.TEXT; } else { bg = T.BG_CARD; fg = T.TEXT_MUTED; } EditorGUI.DrawRect(rect, bg); if (!selected) DrawBorder(rect, T.BORDER, 1); var oldColor = GUI.color; GUI.color = fg; GUI.Label(rect, label, _sPill); GUI.color = oldColor; } return clicked; } static bool DrawPrimaryButton(Rect rect, string label, Color accent) { bool clicked = GUI.Button(rect, GUIContent.none, GUIStyle.none); EditorGUIUtility.AddCursorRect(rect, MouseCursor.Link); if (Event.current.type == EventType.Repaint) { bool hovered = rect.Contains(Event.current.mousePosition); Color bg = hovered ? Brighten(accent, 0.10f) : accent; EditorGUI.DrawRect(rect, bg); var oldColor = GUI.color; GUI.color = Color.white; GUI.Label(rect, label, _sBtnLabel); GUI.color = oldColor; } return clicked; } static bool DrawSecondaryButton(Rect rect, string label) { bool clicked = GUI.Button(rect, GUIContent.none, GUIStyle.none); EditorGUIUtility.AddCursorRect(rect, MouseCursor.Link); if (Event.current.type == EventType.Repaint) { bool hovered = rect.Contains(Event.current.mousePosition); EditorGUI.DrawRect(rect, hovered ? T.BG_CARD_HOVER : T.BG_CARD); DrawBorder(rect, T.BORDER, 1); var oldColor = GUI.color; GUI.color = T.TEXT; GUI.Label(rect, label, _sBtnLabel); GUI.color = oldColor; } return clicked; } static bool DrawIconButton(Rect rect, string label, Color bg, Color fg) { bool clicked = GUI.Button(rect, GUIContent.none, GUIStyle.none); EditorGUIUtility.AddCursorRect(rect, MouseCursor.Link); if (Event.current.type == EventType.Repaint) { bool hovered = rect.Contains(Event.current.mousePosition); EditorGUI.DrawRect(rect, hovered ? Brighten(bg, 0.10f) : bg); var oldColor = GUI.color; GUI.color = fg; GUI.Label(rect, label, _sBtnLabel); GUI.color = oldColor; } return clicked; } static Color Brighten(Color c, float amt) { return new Color(Mathf.Clamp01(c.r + amt), Mathf.Clamp01(c.g + amt), Mathf.Clamp01(c.b + amt), c.a); } static void DrawTextureLetterboxed(Rect rect, Texture tex) { if (tex == null || Event.current.type != EventType.Repaint) return; float texAspect = (float)tex.width / Mathf.Max(1, tex.height); float rectAspect = rect.width / Mathf.Max(1f, rect.height); Rect inner; if (texAspect > rectAspect) { float h = rect.width / texAspect; inner = new Rect(rect.x, rect.y + (rect.height - h) * 0.5f, rect.width, h); } else { float w = rect.height * texAspect; inner = new Rect(rect.x + (rect.width - w) * 0.5f, rect.y, w, rect.height); } GUI.DrawTexture(inner, tex, ScaleMode.StretchToFill); } static string Spinner() { var frames = new[] { "·", "··", "···", " ··", " ·" }; return frames[(int)(EditorApplication.timeSinceStartup * 6) % frames.Length]; } // ======================================================================= // Networking // ======================================================================= Texture2D GetOrFetchThumb(AssetMeta a) { if (thumbs.TryGetValue(a.id, out var t) && t != null) return t; if (thumbsRequested.Contains(a.id)) return null; string url = null; if (a.category == "image") url = baseUrl.TrimEnd('/') + "/files/" + a.id; else if (a.category == "model") url = baseUrl.TrimEnd('/') + "/api/thumb/" + a.id; if (url == null) return null; thumbsRequested.Add(a.id); var req = UnityWebRequestTexture.GetTexture(url); req.SendWebRequest().completed += _ => { if (req.result == UnityWebRequest.Result.Success) { thumbs[a.id] = DownloadHandlerTexture.GetContent(req); Repaint(); } }; return null; } void Refresh() { loading = true; thumbs.Clear(); thumbsRequested.Clear(); Repaint(); var assetUrl = baseUrl.TrimEnd('/') + "/api/assets?folder_id=" + (string.IsNullOrEmpty(currentFolderId) ? "" : UnityWebRequest.EscapeURL(currentFolderId)); var aReq = UnityWebRequest.Get(assetUrl); aReq.SendWebRequest().completed += _ => { loading = false; if (aReq.result != UnityWebRequest.Result.Success) { SetStatus("Fetch failed: " + aReq.error, 5); return; } try { var w = JsonUtility.FromJson("{\"items\":" + aReq.downloadHandler.text + "}"); assets.Clear(); if (w != null && w.items != null) assets.AddRange(w.items); SetStatus("Loaded " + assets.Count + " asset" + (assets.Count == 1 ? "" : "s")); } catch (System.Exception e) { SetStatus("Parse error: " + e.Message, 5); } Repaint(); }; if (string.IsNullOrEmpty(currentFolderId)) { var fReq = UnityWebRequest.Get(baseUrl.TrimEnd('/') + "/api/folders"); fReq.SendWebRequest().completed += _ => { if (fReq.result == UnityWebRequest.Result.Success) { try { var w = JsonUtility.FromJson("{\"items\":" + fReq.downloadHandler.text + "}"); folders.Clear(); if (w != null && w.items != null) folders.AddRange(w.items); } catch { } Repaint(); } }; } else folders.Clear(); } void EnterFolder(FolderMeta f) { currentFolderId = f.id; currentFolderName = f.name; selectedCategory = "all"; Refresh(); } void LeaveFolder() { currentFolderId = null; currentFolderName = null; Refresh(); } void ImportAsset(AssetMeta a) { var folder = GetSelectedFolder(); var dst = (folder.TrimEnd('/', '\\') + "/" + SanitizeFileName(a.original_name)).Replace('\\', '/'); dst = AssetDatabase.GenerateUniqueAssetPath(dst); SetStatus("Downloading " + a.original_name + "…", 30); var url = baseUrl.TrimEnd('/') + "/download/" + a.id; var req = UnityWebRequest.Get(url); req.downloadHandler = new DownloadHandlerFile(dst); req.SendWebRequest().completed += _ => { if (req.result != UnityWebRequest.Result.Success) { SetStatus("Failed: " + req.error, 5); try { if (File.Exists(dst)) File.Delete(dst); } catch { } } else { AssetDatabase.ImportAsset(dst); EditorUtility.FocusProjectWindow(); var imported = AssetDatabase.LoadMainAssetAtPath(dst); if (imported != null) Selection.activeObject = imported; SetStatus("Imported " + a.original_name); } Repaint(); }; } // ======================================================================= // Audio // ======================================================================= void PlayAudio(AssetMeta a) { StopPreview(); if (audioCache.TryGetValue(a.id, out var clip) && clip != null) { PlayPreviewClipReflect(clip); playingId = a.id; SetStatus("Playing " + a.original_name); Repaint(); return; } if (audioLoading.Contains(a.id)) return; audioLoading.Add(a.id); var url = baseUrl.TrimEnd('/') + "/files/" + a.id; var type = GuessAudioType(a.original_name); var req = UnityWebRequestMultimedia.GetAudioClip(url, type); req.SendWebRequest().completed += _ => { audioLoading.Remove(a.id); if (req.result != UnityWebRequest.Result.Success) { SetStatus("Audio failed: " + req.error, 5); Repaint(); return; } var c = DownloadHandlerAudioClip.GetContent(req); if (c != null) { audioCache[a.id] = c; PlayPreviewClipReflect(c); playingId = a.id; SetStatus("Playing " + a.original_name); } Repaint(); }; Repaint(); } static AudioType GuessAudioType(string name) { switch (Path.GetExtension(name ?? "").ToLowerInvariant()) { case ".wav": return AudioType.WAV; case ".mp3": return AudioType.MPEG; case ".ogg": return AudioType.OGGVORBIS; case ".aif": case ".aiff": return AudioType.AIFF; case ".xm": return AudioType.XM; case ".mod": return AudioType.MOD; case ".it": return AudioType.IT; case ".s3m": return AudioType.S3M; default: return AudioType.UNKNOWN; } } static MethodInfo _playMethod, _stopMethod; const BindingFlags AUDIO_FLAGS = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; static void EnsureAudioMethods() { if (_playMethod != null && _stopMethod != null) return; var assembly = typeof(AudioImporter).Assembly; var audioUtil = assembly.GetType("UnityEditor.AudioUtil"); if (audioUtil == null) return; if (_playMethod == null) { foreach (var name in new[] { "PlayPreviewClip", "PlayClip" }) { var m = audioUtil.GetMethods(AUDIO_FLAGS) .Where(x => x.Name == name && x.GetParameters().Length > 0 && x.GetParameters()[0].ParameterType == typeof(AudioClip)) .OrderBy(x => x.GetParameters().Length) .FirstOrDefault(); if (m != null) { _playMethod = m; break; } } } if (_stopMethod == null) { foreach (var name in new[] { "StopAllPreviewClips", "StopAllClips", "StopPreviewClip", "StopClip" }) { var m = audioUtil.GetMethod(name, AUDIO_FLAGS, null, System.Type.EmptyTypes, null); if (m != null) { _stopMethod = m; break; } } } } public static void PlayPreviewClipReflect(AudioClip clip) { EnsureAudioMethods(); if (_playMethod == null || clip == null) return; var ps = _playMethod.GetParameters(); var args = new object[ps.Length]; args[0] = clip; for (int i = 1; i < ps.Length; i++) { var pt = ps[i].ParameterType; if (pt == typeof(int)) args[i] = 0; else if (pt == typeof(bool)) args[i] = false; else args[i] = pt.IsValueType ? System.Activator.CreateInstance(pt) : null; } try { _playMethod.Invoke(null, args); } catch (System.Exception e) { Debug.LogWarning("AssetVault: PlayPreviewClip failed: " + e.Message); } } public void StopPreview() { EnsureAudioMethods(); if (_stopMethod != null) try { _stopMethod.Invoke(null, null); } catch { } playingId = null; Repaint(); } // ======================================================================= // Helpers // ======================================================================= static string GetSelectedFolder() { foreach (var obj in Selection.GetFiltered(SelectionMode.Assets)) { var p = AssetDatabase.GetAssetPath(obj); if (string.IsNullOrEmpty(p)) continue; if (Directory.Exists(p)) return p.Replace('\\', '/'); var dir = Path.GetDirectoryName(p); if (!string.IsNullOrEmpty(dir)) return dir.Replace('\\', '/'); } return "Assets"; } static string SanitizeFileName(string name) { if (name == null) return "asset"; foreach (var c in Path.GetInvalidFileNameChars()) name = name.Replace(c, '_'); return name; } static Texture GetCategoryIcon(string cat) { string iconName; switch (cat) { case "image": iconName = "Texture Icon"; break; case "audio": iconName = "AudioClip Icon"; break; case "video": iconName = "VideoClip Icon"; break; case "model": iconName = "Mesh Icon"; break; case "font": iconName = "Font Icon"; break; case "code": iconName = "TextAsset Icon"; break; case "archive": iconName = "Folder Icon"; break; default: iconName = "DefaultAsset Icon"; break; } var c = EditorGUIUtility.IconContent(iconName); return c != null ? c.image : null; } static string TruncateMid(string s, int max) { if (string.IsNullOrEmpty(s) || s.Length <= max) return s; int half = (max - 1) / 2; return s.Substring(0, half) + "…" + s.Substring(s.Length - half); } static string FormatSize(long n) { if (n < 1024) return n + " B"; if (n < 1024L * 1024) return (n / 1024.0).ToString("F1") + " KB"; if (n < 1024L * 1024 * 1024) return (n / 1048576.0).ToString("F1") + " MB"; return (n / 1073741824.0).ToString("F2") + " GB"; } }