diff --git a/druid/editor_scripts/core/asset_store.lua b/druid/editor_scripts/core/asset_store.lua index cb4c5e0..f5f02d7 100644 --- a/druid/editor_scripts/core/asset_store.lua +++ b/druid/editor_scripts/core/asset_store.lua @@ -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 {} diff --git a/druid/editor_scripts/core/asset_store/data.lua b/druid/editor_scripts/core/asset_store/data.lua new file mode 100644 index 0000000..18ab864 --- /dev/null +++ b/druid/editor_scripts/core/asset_store/data.lua @@ -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 + diff --git a/druid/editor_scripts/core/asset_store/ui/dialog.lua b/druid/editor_scripts/core/asset_store/ui/dialog.lua new file mode 100644 index 0000000..7b6dfac --- /dev/null +++ b/druid/editor_scripts/core/asset_store/ui/dialog.lua @@ -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 + diff --git a/druid/editor_scripts/core/asset_store/ui/filters.lua b/druid/editor_scripts/core/asset_store/ui/filters.lua new file mode 100644 index 0000000..efa750d --- /dev/null +++ b/druid/editor_scripts/core/asset_store/ui/filters.lua @@ -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 + diff --git a/druid/editor_scripts/core/asset_store/ui/search.lua b/druid/editor_scripts/core/asset_store/ui/search.lua new file mode 100644 index 0000000..223b0d2 --- /dev/null +++ b/druid/editor_scripts/core/asset_store/ui/search.lua @@ -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 + diff --git a/druid/editor_scripts/core/asset_store/ui/settings.lua b/druid/editor_scripts/core/asset_store/ui/settings.lua new file mode 100644 index 0000000..6731f52 --- /dev/null +++ b/druid/editor_scripts/core/asset_store/ui/settings.lua @@ -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 + diff --git a/druid/editor_scripts/core/asset_store/ui/widget_card.lua b/druid/editor_scripts/core/asset_store/ui/widget_card.lua new file mode 100644 index 0000000..1884d80 --- /dev/null +++ b/druid/editor_scripts/core/asset_store/ui/widget_card.lua @@ -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 + diff --git a/druid/editor_scripts/core/asset_store/ui/widget_list.lua b/druid/editor_scripts/core/asset_store/ui/widget_list.lua new file mode 100644 index 0000000..3322183 --- /dev/null +++ b/druid/editor_scripts/core/asset_store/ui/widget_list.lua @@ -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 + diff --git a/druid/editor_scripts/core/asset_store_internal.lua b/druid/editor_scripts/core/asset_store_internal.lua index 5273d8a..0bd9d96 100644 --- a/druid/editor_scripts/core/asset_store_internal.lua +++ b/druid/editor_scripts/core/asset_store_internal.lua @@ -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 diff --git a/druid/editor_scripts/core/ui_components.lua b/druid/editor_scripts/core/ui_components.lua deleted file mode 100644 index 82ec423..0000000 --- a/druid/editor_scripts/core/ui_components.lua +++ /dev/null @@ -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 diff --git a/druid/editor_scripts/druid.editor_script b/druid/editor_scripts/druid.editor_script index 754732f..2431208 100644 --- a/druid/editor_scripts/druid.editor_script +++ b/druid/editor_scripts/druid.editor_script @@ -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 },