This commit is contained in:
Insality
2025-11-11 01:36:18 +02:00
parent a28b15c35c
commit 138b2613ab
11 changed files with 814 additions and 652 deletions

View File

@@ -2,43 +2,88 @@
--- Handles fetching widget data, displaying the store interface, and managing installations
local installer = require("druid.editor_scripts.core.installer")
local ui_components = require("druid.editor_scripts.core.ui_components")
local internal = require("druid.editor_scripts.core.asset_store_internal")
local internal = require("druid.editor_scripts.core.asset_store.data")
local dialog_ui = require("druid.editor_scripts.core.asset_store.ui.dialog")
local filters_ui = require("druid.editor_scripts.core.asset_store.ui.filters")
local search_ui = require("druid.editor_scripts.core.asset_store.ui.search")
local settings_ui = require("druid.editor_scripts.core.asset_store.ui.settings")
local widget_list_ui = require("druid.editor_scripts.core.asset_store.ui.widget_list")
local M = {}
---Build type options array
---@return table
local function build_type_options()
return {"All", "Installed", "Not Installed"}
end
local INFO_RESULT = "asset_store_open_info"
local DEFAULT_INSTALL_PREF_KEY = "druid.asset_install_folder"
local DEFAULT_INSTALL_FOLDER = "/widget"
local DEFAULT_TITLE = "Asset Store"
local DEFAULT_INFO_BUTTON = "Info"
local DEFAULT_CLOSE_BUTTON = "Close"
local DEFAULT_EMPTY_SEARCH_MESSAGE = "No widgets found matching '%s'."
local DEFAULT_EMPTY_FILTER_MESSAGE = "No widgets found matching the current filters."
local DEFAULT_SEARCH_LABELS = {
search_tooltip = "Search for widgets by title, author, or description"
}
---Build author options array
---@param authors table
---@return table
local function build_author_options(authors)
local options = {"All Authors"}
for _, author in ipairs(authors) do
table.insert(options, author)
local function normalize_config(input)
if type(input) == "string" then
input = { store_url = input }
end
return options
end
assert(type(input) == "table", "asset_store.open expects a string URL or config table")
assert(input.store_url, "asset_store.open requires a store_url")
---Build tag options array
---@param tags table
---@return table
local function build_tag_options(tags)
local options = {"All Tags"}
for _, tag in ipairs(tags) do
table.insert(options, tag)
local config = {
store_url = input.store_url,
info_url = input.info_url,
title = input.title or DEFAULT_TITLE,
info_button_label = input.info_button_label or DEFAULT_INFO_BUTTON,
close_button_label = input.close_button_label or DEFAULT_CLOSE_BUTTON,
empty_search_message = input.empty_search_message or DEFAULT_EMPTY_SEARCH_MESSAGE,
empty_filter_message = input.empty_filter_message or DEFAULT_EMPTY_FILTER_MESSAGE,
install_prefs_key = input.install_prefs_key,
default_install_folder = input.default_install_folder or DEFAULT_INSTALL_FOLDER,
labels = input.labels or {},
info_action = input.info_action,
}
if config.install_prefs_key == nil then
config.install_prefs_key = DEFAULT_INSTALL_PREF_KEY
elseif config.install_prefs_key == false then
config.install_prefs_key = nil
end
return options
config.labels.search = config.labels.search or {}
for key, value in pairs(DEFAULT_SEARCH_LABELS) do
if config.labels.search[key] == nil then
config.labels.search[key] = value
end
end
return config
end
local function get_initial_install_folder(config)
if not config.install_prefs_key then
return config.default_install_folder
end
return editor.prefs.get(config.install_prefs_key) or config.default_install_folder
end
local function persist_install_folder(config, folder)
if not config.install_prefs_key then
return
end
editor.prefs.set(config.install_prefs_key, folder)
end
---Handle widget installation
---@param item table - Widget item to install
---@param install_folder string - Installation folder
@@ -60,50 +105,38 @@ local function handle_install(item, install_folder, all_items, on_success, on_er
end
---Open the asset store dialog
function M.open_asset_store(store_url)
print("Opening Druid Asset Store from:", store_url)
function M.open(config_input)
local config = normalize_config(config_input)
-- Fetch data synchronously before creating the dialog
local store_data, fetch_error = internal.download_json(store_url)
print("Opening " .. config.title .. " from:", config.store_url)
local store_data, fetch_error = internal.download_json(config.store_url)
if not store_data then
print("Failed to load widgets:", fetch_error)
print("Failed to load store items:", fetch_error)
return
end
print("Successfully loaded", #store_data.items, "widgets")
print("Successfully loaded", #store_data.items, "items")
local initial_items = store_data.items
local initial_install_folder = get_initial_install_folder(config)
local filter_overrides = config.labels.filters and { labels = config.labels.filters } or nil
local dialog_component = editor.ui.component(function(props)
-- State management
local all_items = editor.ui.use_state(initial_items)
local install_folder, set_install_folder = editor.ui.use_state(editor.prefs.get("druid.asset_install_folder") or installer.get_install_folder())
local install_folder, set_install_folder = editor.ui.use_state(initial_install_folder)
local search_query, set_search_query = editor.ui.use_state("")
local filter_type, set_filter_type = editor.ui.use_state("All")
local filter_author, set_filter_author = editor.ui.use_state("All Authors")
local filter_tag, set_filter_tag = editor.ui.use_state("All Tags")
local install_status, set_install_status = editor.ui.use_state("")
-- Extract unique authors and tags for dropdown options
local authors = editor.ui.use_memo(internal.extract_authors, all_items)
local tags = editor.ui.use_memo(internal.extract_tags, all_items)
-- Build dropdown options (memoized to avoid recreation on each render)
local type_options = editor.ui.use_memo(build_type_options)
local author_options = editor.ui.use_memo(build_author_options, authors)
local tag_options = editor.ui.use_memo(build_tag_options, tags)
local type_options = editor.ui.use_memo(filters_ui.build_type_options, filter_overrides)
local author_options = editor.ui.use_memo(filters_ui.build_author_options, authors, filter_overrides)
local tag_options = editor.ui.use_memo(filters_ui.build_tag_options, tags, filter_overrides)
-- Debug output
if #type_options > 0 then
print("Type options count:", #type_options, "first:", type_options[1])
end
if #author_options > 0 then
print("Author options count:", #author_options, "first:", author_options[1])
end
if #tag_options > 0 then
print("Tag options count:", #tag_options, "first:", tag_options[1])
end
-- Filter items based on all filters
local filtered_items = editor.ui.use_memo(
internal.filter_items_by_filters,
all_items,
@@ -114,7 +147,6 @@ function M.open_asset_store(store_url)
install_folder
)
-- Installation handlers
local function on_install(item)
handle_install(item, install_folder, all_items,
function(message)
@@ -126,116 +158,57 @@ function M.open_asset_store(store_url)
)
end
-- Build UI content
local content_children = {}
-- Settings section
table.insert(content_children, editor.ui.horizontal({
spacing = editor.ui.SPACING.MEDIUM,
children = {
editor.ui.label({
spacing = editor.ui.SPACING.MEDIUM,
text = "Installation Folder:",
color = editor.ui.COLOR.TEXT
}),
editor.ui.string_field({
value = install_folder,
on_value_changed = function(new_folder)
set_install_folder(new_folder)
editor.prefs.set("druid.asset_install_folder", new_folder)
end,
title = "Installation Folder:",
tooltip = "The folder to install the assets to",
}),
}
table.insert(content_children, settings_ui.create({
install_folder = install_folder,
on_install_folder_changed = function(new_folder)
set_install_folder(new_folder)
persist_install_folder(config, new_folder)
end,
labels = config.labels.settings
}))
-- Filter dropdowns section
table.insert(content_children, editor.ui.horizontal({
spacing = editor.ui.SPACING.MEDIUM,
children = {
-- Type filter dropdown
editor.ui.horizontal({
spacing = editor.ui.SPACING.SMALL,
children = {
editor.ui.label({
text = "Type:",
color = editor.ui.COLOR.TEXT
}),
editor.ui.select_box({
value = filter_type,
options = type_options,
on_value_changed = set_filter_type
})
}
}),
-- Author filter dropdown
editor.ui.horizontal({
spacing = editor.ui.SPACING.SMALL,
children = {
editor.ui.label({
text = "Author:",
color = editor.ui.COLOR.TEXT
}),
editor.ui.select_box({
value = filter_author,
options = author_options,
on_value_changed = set_filter_author
})
}
}),
-- Tag filter dropdown
editor.ui.horizontal({
spacing = editor.ui.SPACING.SMALL,
children = {
editor.ui.label({
text = "Tag:",
color = editor.ui.COLOR.TEXT
}),
editor.ui.select_box({
value = filter_tag,
options = tag_options,
on_value_changed = set_filter_tag
})
}
})
}
table.insert(content_children, filters_ui.create({
filter_type = filter_type,
filter_author = filter_author,
filter_tag = filter_tag,
type_options = type_options,
author_options = author_options,
tag_options = tag_options,
on_type_change = set_filter_type,
on_author_change = set_filter_author,
on_tag_change = set_filter_tag,
labels = config.labels.filters,
}))
-- Search section
table.insert(content_children, editor.ui.horizontal({
spacing = editor.ui.SPACING.MEDIUM,
children = {
editor.ui.label({
text = "Search:",
color = editor.ui.COLOR.TEXT
}),
editor.ui.string_field({
value = search_query,
on_value_changed = set_search_query,
title = "Search:",
tooltip = "Search for widgets by title, author, or description",
grow = true
})
},
table.insert(content_children, search_ui.create({
search_query = search_query,
on_search = set_search_query,
labels = config.labels.search,
}))
-- Main content area
if #filtered_items == 0 then
local message = search_query ~= "" and
"No widgets found matching '" .. search_query .. "'." or
"No widgets found matching the current filters."
local message = config.empty_filter_message
if search_query ~= "" then
message = string.format(config.empty_search_message, search_query)
end
table.insert(content_children, editor.ui.label({
text = message,
color = editor.ui.COLOR.HINT,
alignment = editor.ui.ALIGNMENT.CENTER
}))
else
table.insert(content_children, ui_components.create_widget_list(filtered_items, on_install))
table.insert(content_children, widget_list_ui.create(filtered_items, {
on_install = on_install,
open_url = internal.open_url,
is_installed = function(item)
return installer.is_widget_installed(item, install_folder)
end,
labels = config.labels.widget_card,
}))
end
-- Install status message
if install_status ~= "" then
table.insert(content_children, editor.ui.label({
text = install_status,
@@ -244,31 +217,33 @@ function M.open_asset_store(store_url)
}))
end
return editor.ui.dialog({
title = "Druid Asset Store",
content = editor.ui.vertical({
spacing = editor.ui.SPACING.MEDIUM,
padding = editor.ui.PADDING.SMALL,
grow = true,
children = content_children
}),
buttons = {
editor.ui.dialog_button({
text = "Info",
result = "info_assets_store",
}),
editor.ui.dialog_button({
text = "Close",
cancel = true
})
}
local buttons = {}
if config.info_url or config.info_action then
table.insert(buttons, editor.ui.dialog_button({
text = config.info_button_label,
result = INFO_RESULT,
}))
end
table.insert(buttons, editor.ui.dialog_button({
text = config.close_button_label,
cancel = true
}))
return dialog_ui.build({
title = config.title,
children = content_children,
buttons = buttons
})
end)
local result = editor.ui.show_dialog(dialog_component({}))
if result and result == "info_assets_store" then
editor.browse("https://github.com/Insality/core/blob/main/druid_widget_store.md")
if result and result == INFO_RESULT then
if config.info_action then
config.info_action()
elseif config.info_url then
internal.open_url(config.info_url)
end
end
return {}

View File

@@ -0,0 +1,196 @@
local installer = require("druid.editor_scripts.core.installer")
local M = {}
local function normalize_query(query)
if not query or query == "" then
return nil
end
return string.lower(query)
end
local function is_unlisted_visible(item, lower_query)
if not item.unlisted then
return true
end
if not lower_query or not item.id then
return false
end
return string.lower(item.id) == lower_query
end
function M.download_json(json_url)
local response = http.request(json_url, { as = "json" })
if response.status ~= 200 then
return nil, "Failed to fetch store data. HTTP status: " .. response.status
end
if not response.body or not response.body.items then
return nil, "Invalid store data format"
end
return response.body, nil
end
function M.filter_items(items, query)
if query == "" or query == nil then
return items
end
local filtered = {}
local lower_query = string.lower(query)
for _, item in ipairs(items) do
local matches = false
if item.id and string.find(string.lower(item.id), lower_query, 1, true) then
matches = true
elseif item.title and string.find(string.lower(item.title), lower_query, 1, true) then
matches = true
elseif item.author and string.find(string.lower(item.author), lower_query, 1, true) then
matches = true
elseif item.description and string.find(string.lower(item.description), lower_query, 1, true) then
matches = true
end
if not matches and item.tags then
for _, tag in ipairs(item.tags) do
if string.find(string.lower(tag), lower_query, 1, true) then
matches = true
break
end
end
end
if not matches and item.depends then
for _, dep in ipairs(item.depends) do
if string.find(string.lower(dep), lower_query, 1, true) then
matches = true
break
end
end
end
if matches then
table.insert(filtered, item)
end
end
return filtered
end
function M.extract_authors(items)
local authors = {}
local author_set = {}
for _, item in ipairs(items) do
if not item.unlisted and item.author and not author_set[item.author] then
author_set[item.author] = true
table.insert(authors, item.author)
end
end
table.sort(authors)
return authors
end
function M.extract_tags(items)
local tags = {}
local tag_set = {}
for _, item in ipairs(items) do
if not item.unlisted and item.tags then
for _, tag in ipairs(item.tags) do
if not tag_set[tag] then
tag_set[tag] = true
table.insert(tags, tag)
end
end
end
end
table.sort(tags)
return tags
end
function M.filter_items_by_filters(items, search_query, filter_type, filter_author, filter_tag, install_folder)
local lower_query = normalize_query(search_query)
local visible_items = {}
for _, item in ipairs(items) do
if is_unlisted_visible(item, lower_query) then
table.insert(visible_items, item)
end
end
local filtered = visible_items
if lower_query then
filtered = M.filter_items(filtered, search_query)
end
if filter_type and filter_type ~= "All" then
local type_filtered = {}
for _, item in ipairs(filtered) do
local is_installed = installer.is_widget_installed(item, install_folder)
if (filter_type == "Installed" and is_installed) or
(filter_type == "Not Installed" and not is_installed) then
table.insert(type_filtered, item)
end
end
filtered = type_filtered
end
if filter_author and filter_author ~= "All Authors" then
local author_filtered = {}
for _, item in ipairs(filtered) do
if item.author == filter_author then
table.insert(author_filtered, item)
end
end
filtered = author_filtered
end
if filter_tag and filter_tag ~= "All Tags" then
local tag_filtered = {}
for _, item in ipairs(filtered) do
if item.tags then
for _, tag in ipairs(item.tags) do
if tag == filter_tag then
table.insert(tag_filtered, item)
break
end
end
end
end
filtered = tag_filtered
end
return filtered
end
function M.open_url(url)
if not url then
print("No URL available for:", url)
end
editor.browse(url)
end
return M

View File

@@ -0,0 +1,19 @@
local M = {}
function M.build(params)
return editor.ui.dialog({
title = params.title or "Asset Store",
content = editor.ui.vertical({
spacing = params.spacing or editor.ui.SPACING.MEDIUM,
padding = params.padding or editor.ui.PADDING.SMALL,
grow = true,
children = params.children or {}
}),
buttons = params.buttons or {}
})
end
return M

View File

@@ -0,0 +1,119 @@
local DEFAULT_LABELS = {
type_label = "Type:",
author_label = "Author:",
tag_label = "Tag:",
all_types = "All",
installed = "Installed",
not_installed = "Not Installed",
all_authors = "All Authors",
all_tags = "All Tags",
}
local M = {}
local function build_labels(overrides)
if not overrides then
return DEFAULT_LABELS
end
local labels = {}
for key, value in pairs(DEFAULT_LABELS) do
labels[key] = overrides[key] or value
end
return labels
end
function M.build_type_options(overrides)
local labels = build_labels(overrides and overrides.labels)
return {
labels.all_types,
labels.installed,
labels.not_installed,
}
end
function M.build_author_options(authors, overrides)
local labels = build_labels(overrides and overrides.labels)
local options = {labels.all_authors}
for _, author in ipairs(authors or {}) do
table.insert(options, author)
end
return options
end
function M.build_tag_options(tags, overrides)
local labels = build_labels(overrides and overrides.labels)
local options = {labels.all_tags}
for _, tag in ipairs(tags or {}) do
table.insert(options, tag)
end
return options
end
function M.create(params)
local labels = build_labels(params and params.labels)
return editor.ui.horizontal({
spacing = editor.ui.SPACING.MEDIUM,
children = {
editor.ui.horizontal({
spacing = editor.ui.SPACING.SMALL,
children = {
editor.ui.label({
text = labels.type_label,
color = editor.ui.COLOR.TEXT
}),
editor.ui.select_box({
value = params.filter_type,
options = params.type_options,
on_value_changed = params.on_type_change
})
}
}),
editor.ui.horizontal({
spacing = editor.ui.SPACING.SMALL,
children = {
editor.ui.label({
text = labels.author_label,
color = editor.ui.COLOR.TEXT
}),
editor.ui.select_box({
value = params.filter_author,
options = params.author_options,
on_value_changed = params.on_author_change
})
}
}),
editor.ui.horizontal({
spacing = editor.ui.SPACING.SMALL,
children = {
editor.ui.label({
text = labels.tag_label,
color = editor.ui.COLOR.TEXT
}),
editor.ui.select_box({
value = params.filter_tag,
options = params.tag_options,
on_value_changed = params.on_tag_change
})
}
})
}
})
end
return M

View File

@@ -0,0 +1,49 @@
local DEFAULT_LABELS = {
search_label = "Search:",
search_title = "Search:",
search_tooltip = "Search for items",
}
local M = {}
local function build_labels(overrides)
if not overrides then
return DEFAULT_LABELS
end
local labels = {}
for key, value in pairs(DEFAULT_LABELS) do
labels[key] = overrides[key] or value
end
return labels
end
function M.create(params)
local labels = build_labels(params and params.labels)
return editor.ui.horizontal({
spacing = editor.ui.SPACING.MEDIUM,
children = {
editor.ui.label({
text = labels.search_label,
color = editor.ui.COLOR.TEXT
}),
editor.ui.string_field({
value = params.search_query or "",
on_value_changed = params.on_search,
title = labels.search_title,
tooltip = labels.search_tooltip,
grow = true
})
}
})
end
return M

View File

@@ -0,0 +1,49 @@
local DEFAULT_LABELS = {
install_label = "Installation Folder:",
install_title = "Installation Folder:",
install_tooltip = "The folder to install the assets to",
}
local M = {}
local function build_labels(overrides)
if not overrides then
return DEFAULT_LABELS
end
local labels = {}
for key, value in pairs(DEFAULT_LABELS) do
labels[key] = overrides[key] or value
end
return labels
end
function M.create(params)
local labels = build_labels(params and params.labels)
return editor.ui.horizontal({
spacing = editor.ui.SPACING.MEDIUM,
children = {
editor.ui.label({
spacing = editor.ui.SPACING.MEDIUM,
text = labels.install_label,
color = editor.ui.COLOR.TEXT
}),
editor.ui.string_field({
value = params.install_folder,
on_value_changed = params.on_install_folder_changed,
title = labels.install_title,
tooltip = labels.install_tooltip,
}),
}
})
end
return M

View File

@@ -0,0 +1,167 @@
local DEFAULT_LABELS = {
install_button = "Install",
api_button = "API",
example_button = "Example",
author_caption = "Author",
installed_tag = "✓ Installed",
tags_prefix = "Tags: ",
depends_prefix = "Depends: ",
size_separator = "",
unknown_size = "Unknown size",
unknown_version = "Unknown version",
}
local M = {}
local function format_size(size_bytes)
if not size_bytes then
return DEFAULT_LABELS.unknown_size
end
if size_bytes < 1024 then
return size_bytes .. " B"
elseif size_bytes < 1024 * 1024 then
return math.floor(size_bytes / 1024) .. " KB"
end
return math.floor(size_bytes / (1024 * 1024)) .. " MB"
end
local function build_labels(overrides)
if not overrides then
return DEFAULT_LABELS
end
local labels = {}
for key, value in pairs(DEFAULT_LABELS) do
labels[key] = overrides[key] or value
end
return labels
end
function M.create(item, context)
local labels = build_labels(context and context.labels)
local open_url = context and context.open_url or function(_) end
local on_install = context and context.on_install or function(...) end
local is_installed = context and context.is_installed or false
local size_text = format_size(item.size)
local version_text = item.version and ("v" .. item.version) or labels.unknown_version
local tags_text = item.tags and #item.tags > 0 and labels.tags_prefix .. table.concat(item.tags, ", ") or ""
local deps_text = item.depends and #item.depends > 0 and labels.depends_prefix .. table.concat(item.depends, ", ") or ""
local widget_details_children = {
editor.ui.horizontal({
spacing = editor.ui.SPACING.SMALL,
children = {
editor.ui.label({
text = item.title or item.id,
color = editor.ui.COLOR.OVERRIDE
}),
editor.ui.label({
text = version_text,
color = editor.ui.COLOR.WARNING
}),
editor.ui.label({
text = labels.size_separator .. size_text,
color = editor.ui.COLOR.HINT
}),
}
}),
editor.ui.paragraph({
text = item.description or "No description available",
color = editor.ui.COLOR.TEXT
})
}
if tags_text ~= "" then
table.insert(widget_details_children, editor.ui.label({
text = tags_text,
color = editor.ui.COLOR.HINT
}))
end
if deps_text ~= "" then
table.insert(widget_details_children, editor.ui.label({
text = deps_text,
color = editor.ui.COLOR.HINT
}))
end
if is_installed then
table.insert(widget_details_children, editor.ui.label({
text = labels.installed_tag,
color = editor.ui.COLOR.WARNING
}))
end
local button_children = {
editor.ui.button({
text = labels.install_button,
on_pressed = on_install,
enabled = not is_installed
})
}
if item.api then
table.insert(button_children, editor.ui.button({
text = labels.api_button,
on_pressed = function() open_url(item.api) end,
enabled = item.api ~= nil
}))
end
if item.example_url then
table.insert(button_children, editor.ui.button({
text = labels.example_button,
on_pressed = function() open_url(item.example_url) end,
enabled = item.example_url ~= nil
}))
end
table.insert(button_children, editor.ui.horizontal({ grow = true }))
if item.author_url then
table.insert(button_children, editor.ui.label({
text = labels.author_caption,
color = editor.ui.COLOR.HINT
}))
table.insert(button_children, editor.ui.button({
text = item.author or labels.author_caption,
on_pressed = function() open_url(item.author_url) end,
enabled = item.author_url ~= nil
}))
end
table.insert(widget_details_children, editor.ui.horizontal({
spacing = editor.ui.SPACING.SMALL,
children = button_children
}))
return editor.ui.horizontal({
spacing = editor.ui.SPACING.NONE,
padding = editor.ui.PADDING.SMALL,
children = {
editor.ui.label({
text = "•••",
color = editor.ui.COLOR.HINT
}),
editor.ui.vertical({
spacing = editor.ui.SPACING.SMALL,
grow = true,
children = widget_details_children
}),
}
})
end
return M

View File

@@ -0,0 +1,51 @@
local widget_card = require("druid.editor_scripts.core.asset_store.ui.widget_card")
local M = {}
local function noop(...)
end
local function build_context(overrides)
return {
on_install = overrides.on_install or noop,
open_url = overrides.open_url or noop,
labels = overrides.labels,
}
end
function M.create(items, overrides)
local card_context = build_context(overrides or {})
local is_installed = overrides and overrides.is_installed or function(_)
return false
end
local widget_items = {}
for _, item in ipairs(items) do
local context = {
on_install = function()
card_context.on_install(item)
end,
open_url = card_context.open_url,
labels = card_context.labels,
is_installed = is_installed(item),
}
table.insert(widget_items, widget_card.create(item, context))
end
return editor.ui.scroll({
content = editor.ui.vertical({
children = widget_items
})
})
end
return M

View File

@@ -1,213 +1,39 @@
local installer = require("druid.editor_scripts.core.installer")
local data = require("druid.editor_scripts.core.asset_store.data")
local M = {}
---Download a JSON file from a URL
---@param json_url string - The URL to download the JSON from
---@return table|nil, string|nil - The JSON data or nil, error message or nil
function M.download_json(json_url)
local response = http.request(json_url, { as = "json" })
if response.status ~= 200 then
return nil, "Failed to fetch store data. HTTP status: " .. response.status
end
if not response.body or not response.body.items then
return nil, "Invalid store data format"
end
return response.body, nil
return data.download_json(json_url)
end
local function is_unlisted_visible(item, lower_query)
if not item.unlisted then
return true
end
if not lower_query or lower_query == "" or not item.id then
return false
end
return string.lower(item.id) == lower_query
end
---Filter items based on search query
---Filter items based on search query
---@param items table - List of widget items
---@param query string - Search query
---@return table - Filtered items
function M.filter_items(items, query)
if query == "" then
return items
end
local filtered = {}
local lower_query = string.lower(query)
for _, item in ipairs(items) do
-- Search in title, author, description
local matches = false
if item.id and string.find(string.lower(item.id), lower_query, 1, true) then
matches = true
elseif item.title and string.find(string.lower(item.title), lower_query, 1, true) then
matches = true
elseif item.author and string.find(string.lower(item.author), lower_query, 1, true) then
matches = true
elseif item.description and string.find(string.lower(item.description), lower_query, 1, true) then
matches = true
end
-- Search in tags
if not matches and item.tags then
for _, tag in ipairs(item.tags) do
if string.find(string.lower(tag), lower_query, 1, true) then
matches = true
break
end
end
end
-- Search in dependencies
if not matches and item.depends then
for _, dep in ipairs(item.depends) do
if string.find(string.lower(dep), lower_query, 1, true) then
matches = true
break
end
end
end
if matches then
table.insert(filtered, item)
end
end
return filtered
return data.filter_items(items, query)
end
---Extract unique authors from items list
---@param items table - List of widget items
---@return table - Sorted list of unique authors
function M.extract_authors(items)
local authors = {}
local author_set = {}
for _, item in ipairs(items) do
if not item.unlisted and item.author and not author_set[item.author] then
author_set[item.author] = true
table.insert(authors, item.author)
end
end
table.sort(authors)
return authors
return data.extract_authors(items)
end
---Extract unique tags from items list
---@param items table - List of widget items
---@return table - Sorted list of unique tags
function M.extract_tags(items)
local tags = {}
local tag_set = {}
for _, item in ipairs(items) do
if not item.unlisted and item.tags then
for _, tag in ipairs(item.tags) do
if not tag_set[tag] then
tag_set[tag] = true
table.insert(tags, tag)
end
end
end
end
table.sort(tags)
return tags
return data.extract_tags(items)
end
---Filter items based on all filters (search, type, author, tag)
---@param items table - List of widget items
---@param search_query string - Search query
---@param filter_type string - Type filter: "All", "Installed", "Not Installed"
---@param filter_author string - Author filter: "All Authors" or specific author
---@param filter_tag string - Tag filter: "All Tags" or specific tag
---@param install_folder string - Installation folder to check installed status
---@return table - Filtered items
function M.filter_items_by_filters(items, search_query, filter_type, filter_author, filter_tag, install_folder)
local lower_query = nil
if search_query and search_query ~= "" then
lower_query = string.lower(search_query)
end
local visible_items = {}
for _, item in ipairs(items) do
if is_unlisted_visible(item, lower_query) then
table.insert(visible_items, item)
end
end
local filtered = visible_items
-- Filter by search query
if search_query and search_query ~= "" then
filtered = M.filter_items(filtered, search_query)
end
-- Filter by type (Installed/Not Installed)
if filter_type and filter_type ~= "All" then
local type_filtered = {}
for _, item in ipairs(filtered) do
local is_installed = installer.is_widget_installed(item, install_folder)
if (filter_type == "Installed" and is_installed) or
(filter_type == "Not Installed" and not is_installed) then
table.insert(type_filtered, item)
end
end
filtered = type_filtered
end
-- Filter by author
if filter_author and filter_author ~= "All Authors" then
local author_filtered = {}
for _, item in ipairs(filtered) do
if item.author == filter_author then
table.insert(author_filtered, item)
end
end
filtered = author_filtered
end
-- Filter by tag
if filter_tag and filter_tag ~= "All Tags" then
local tag_filtered = {}
for _, item in ipairs(filtered) do
if item.tags then
for _, tag in ipairs(item.tags) do
if tag == filter_tag then
table.insert(tag_filtered, item)
break
end
end
end
end
filtered = tag_filtered
end
return filtered
return data.filter_items_by_filters(items, search_query, filter_type, filter_author, filter_tag, install_folder)
end
---Open a URL in the default browser
---@param url string - The URL to open
function M.open_url(url)
if not url then
print("No URL available for:", url)
end
editor.browse(url)
end

View File

@@ -1,303 +0,0 @@
--- Module for reusable UI components in the asset store
--- Contains component builders for filters, widget items, and lists
local internal = require("druid.editor_scripts.core.asset_store_internal")
local installer = require("druid.editor_scripts.core.installer")
local M = {}
---Create a settings section with installation folder input
---@param install_path string - Current installation path
---@param on_change function - Callback when path changes
---@return userdata - UI component
function M.create_settings_section(install_path, on_change)
return editor.ui.vertical({
spacing = editor.ui.SPACING.SMALL,
children = {
editor.ui.label({
text = "Installation Folder:",
color = editor.ui.COLOR.TEXT
}),
editor.ui.label({
text = install_path,
color = editor.ui.COLOR.TEXT,
grow = true
})
}
})
end
---Extract unique authors from items list
---@param items table - List of widget items
---@return table - Sorted list of unique authors
local function extract_authors(items)
local authors = {}
local author_set = {}
for _, item in ipairs(items) do
if item.author and not author_set[item.author] then
author_set[item.author] = true
table.insert(authors, item.author)
end
end
table.sort(authors)
return authors
end
---Extract unique tags from items list
---@param items table - List of widget items
---@return table - Sorted list of unique tags
local function extract_tags(items)
local tags = {}
local tag_set = {}
for _, item in ipairs(items) do
if item.tags then
for _, tag in ipairs(item.tags) do
if not tag_set[tag] then
tag_set[tag] = true
table.insert(tags, tag)
end
end
end
end
table.sort(tags)
return tags
end
---Create filter section with author and tag dropdowns
---@param items table - List of all widget items
---@param author_filter string - Current author filter
---@param tag_filter string - Current tag filter
---@param on_author_change function - Callback for author filter change
---@param on_tag_change function - Callback for tag filter change
---@return userdata - UI component
function M.create_filter_section(items, author_filter, tag_filter, on_author_change, on_tag_change)
local authors = extract_authors(items)
local tags = extract_tags(items)
-- Build author options
local author_options = {"All Authors"}
for _, author in ipairs(authors) do
table.insert(author_options, author)
end
-- Build tag options
local tag_options = {"All Categories"}
for _, tag in ipairs(tags) do
table.insert(tag_options, tag)
end
return editor.ui.horizontal({
spacing = editor.ui.SPACING.MEDIUM,
children = {
editor.ui.vertical({
spacing = editor.ui.SPACING.SMALL,
children = {
editor.ui.label({
text = "Author:",
color = editor.ui.COLOR.TEXT
}),
editor.ui.label({
text = author_filter,
color = editor.ui.COLOR.TEXT
})
}
}),
editor.ui.vertical({
spacing = editor.ui.SPACING.SMALL,
children = {
editor.ui.label({
text = "Category:",
color = editor.ui.COLOR.TEXT
}),
editor.ui.label({
text = tag_filter,
color = editor.ui.COLOR.TEXT
})
}
})
}
})
end
---Format file size for display
---@param size_bytes number - Size in bytes
---@return string - Formatted size string
local function format_size(size_bytes)
if size_bytes < 1024 then
return size_bytes .. " B"
elseif size_bytes < 1024 * 1024 then
return math.floor(size_bytes / 1024) .. " KB"
else
return math.floor(size_bytes / (1024 * 1024)) .. " MB"
end
end
---Create a widget item card
---@param item table - Widget item data
---@param is_installed boolean - Whether widget is already installed
---@param on_install function - Callback for install button
---@return userdata - UI component
function M.create_widget_item(item, is_installed, on_install)
local size_text = item.size and format_size(item.size) or "Unknown size"
local version_text = item.version and "v" .. item.version or "Unknown version"
-- Create tags display
local tags_text = ""
if item.tags and #item.tags > 0 then
tags_text = "Tags: " .. table.concat(item.tags, ", ")
end
-- Create dependencies display
local deps_text = ""
if item.depends and #item.depends > 0 then
deps_text = "Depends: " .. table.concat(item.depends, ", ")
end
local widget_details_children = {
editor.ui.horizontal({
spacing = editor.ui.SPACING.SMALL,
children = {
editor.ui.label({
text = item.title or item.id,
color = editor.ui.COLOR.OVERRIDE
}),
editor.ui.label({
text = version_text,
color = editor.ui.COLOR.WARNING
}),
editor.ui.label({
text = "" .. size_text,
color = editor.ui.COLOR.HINT
}),
}
}),
-- Description
editor.ui.paragraph({
text = item.description or "No description available",
color = editor.ui.COLOR.TEXT
}),
}
if tags_text ~= "" then
table.insert(widget_details_children, editor.ui.label({
text = tags_text,
color = editor.ui.COLOR.HINT
}))
end
if deps_text ~= "" then
table.insert(widget_details_children, editor.ui.label({
text = deps_text,
color = editor.ui.COLOR.HINT
}))
end
if is_installed then
table.insert(widget_details_children, editor.ui.label({
text = "✓ Installed",
color = editor.ui.COLOR.WARNING
}))
end
-- Create button row at the bottom
local button_children = {
editor.ui.button({
text = "Install",
on_pressed = on_install,
enabled = not is_installed
}),
}
if item.api ~= nil then
table.insert(button_children, editor.ui.button({
text = "API",
on_pressed = function() internal.open_url(item.api) end,
enabled = item.api ~= nil
}))
end
if item.example_url ~= nil then
table.insert(button_children, editor.ui.button({
text = "Example",
on_pressed = function() internal.open_url(item.example_url) end,
enabled = item.example_url ~= nil
}))
end
-- Add spacer to push Author button to the right
table.insert(button_children, editor.ui.horizontal({ grow = true }))
if item.author_url ~= nil then
table.insert(button_children, editor.ui.label({
text = "Author",
color = editor.ui.COLOR.HINT
}))
table.insert(button_children, editor.ui.button({
text = item.author or "Author",
on_pressed = function() internal.open_url(item.author_url) end,
enabled = item.author_url ~= nil
}))
end
-- Add button row to widget details
table.insert(widget_details_children, editor.ui.horizontal({
spacing = editor.ui.SPACING.SMALL,
children = button_children
}))
return editor.ui.horizontal({
spacing = editor.ui.SPACING.NONE,
padding = editor.ui.PADDING.SMALL,
children = {
-- Widget icon placeholder
editor.ui.label({
text = "•••",
color = editor.ui.COLOR.HINT
}),
-- Widget details
editor.ui.vertical({
spacing = editor.ui.SPACING.SMALL,
grow = true,
children = widget_details_children
}),
}
})
end
---Create a scrollable list of widget items
---@param items table - List of widget items to display
---@param on_install function - Callback for install button
---@return userdata - UI component
function M.create_widget_list(items, on_install)
local widget_items = {}
local install_folder = editor.prefs.get("druid.asset_install_folder") or installer.get_install_folder()
for _, item in ipairs(items) do
local is_installed = installer.is_widget_installed(item, install_folder)
table.insert(widget_items, M.create_widget_item(item, is_installed,
function() on_install(item) end
))
end
return editor.ui.scroll({
content = editor.ui.vertical({
children = widget_items
})
})
end
return M

View File

@@ -3,6 +3,14 @@ local create_druid_widget = require("druid.editor_scripts.create_druid_widget")
local create_druid_gui_script = require("druid.editor_scripts.create_druid_gui_script")
local druid_settings = require("druid.editor_scripts.druid_settings")
local asset_store = require("druid.editor_scripts.core.asset_store")
-- Reuse tip: copy the snippet below into another editor script to open a custom store.
-- asset_store.open({
-- store_url = "https://example.com/store.json",
-- info_url = "https://example.com/docs",
-- title = "My Library Store",
-- install_prefs_key = "my_lib.asset_install_folder",
-- default_install_folder = "/my_widgets",
-- })
local M = {}
@@ -75,7 +83,13 @@ function M.get_commands()
label = "[Druid] Asset Store",
locations = { "Edit" },
run = function()
return asset_store.open_asset_store("https://insality.github.io/core/druid_widget_store.json")
return asset_store.open({
store_url = "https://insality.github.io/core/druid_widget_store.json",
info_url = "https://github.com/Insality/core/blob/main/druid_widget_store.md",
title = "Druid Asset Store",
install_prefs_key = "druid.asset_install_folder",
default_install_folder = DEFAULT_ASSET_INSTALL_FOLDER,
})
end
},