mirror of
https://github.com/Insality/druid.git
synced 2025-11-26 19:00:50 +01:00
Move core to sep repo
This commit is contained in:
@@ -1,253 +0,0 @@
|
||||
--- Main asset store module for Druid widgets
|
||||
--- Handles fetching widget data, displaying the store interface, and managing installations
|
||||
|
||||
local installer = require("druid.editor_scripts.core.asset_store.installer")
|
||||
local internal = require("druid.editor_scripts.core.asset_store.asset_store_internal")
|
||||
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 = {}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
|
||||
local function normalize_config(input)
|
||||
if type(input) == "string" then
|
||||
input = { store_url = input }
|
||||
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")
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
---@param all_items table - List of all widgets for dependency resolution
|
||||
---@param on_success function - Success callback
|
||||
---@param on_error function - Error callback
|
||||
local function handle_install(item, install_folder, all_items, on_success, on_error)
|
||||
print("Installing widget:", item.id)
|
||||
|
||||
local success, message = installer.install_widget(item, install_folder, all_items)
|
||||
|
||||
if success then
|
||||
print("Installation successful:", message)
|
||||
on_success(message)
|
||||
else
|
||||
print("Installation failed:", message)
|
||||
on_error(message)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function M.open(config_input)
|
||||
local config = normalize_config(config_input)
|
||||
|
||||
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 store items:", fetch_error)
|
||||
return
|
||||
end
|
||||
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)
|
||||
local all_items = editor.ui.use_state(initial_items)
|
||||
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("")
|
||||
|
||||
local authors = editor.ui.use_memo(internal.extract_authors, all_items)
|
||||
local tags = editor.ui.use_memo(internal.extract_tags, all_items)
|
||||
|
||||
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)
|
||||
|
||||
local filtered_items = editor.ui.use_memo(
|
||||
internal.filter_items_by_filters,
|
||||
all_items,
|
||||
search_query,
|
||||
filter_type,
|
||||
filter_author,
|
||||
filter_tag,
|
||||
install_folder
|
||||
)
|
||||
|
||||
local function on_install(item)
|
||||
handle_install(item, install_folder, all_items,
|
||||
function(message)
|
||||
set_install_status("Success: " .. message)
|
||||
end,
|
||||
function(message)
|
||||
set_install_status("Error: " .. message)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
local content_children = {}
|
||||
|
||||
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
|
||||
}))
|
||||
|
||||
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,
|
||||
}))
|
||||
|
||||
table.insert(content_children, search_ui.create({
|
||||
search_query = search_query,
|
||||
on_search = set_search_query,
|
||||
labels = config.labels.search,
|
||||
}))
|
||||
|
||||
if #filtered_items == 0 then
|
||||
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, 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
|
||||
|
||||
if install_status ~= "" then
|
||||
table.insert(content_children, editor.ui.label({
|
||||
text = install_status,
|
||||
color = install_status:find("Success") and editor.ui.COLOR.TEXT or editor.ui.COLOR.ERROR,
|
||||
alignment = editor.ui.ALIGNMENT.CENTER
|
||||
}))
|
||||
end
|
||||
|
||||
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_RESULT then
|
||||
if config.info_action then
|
||||
config.info_action()
|
||||
elseif config.info_url then
|
||||
internal.open_url(config.info_url)
|
||||
end
|
||||
end
|
||||
|
||||
return {}
|
||||
end
|
||||
|
||||
|
||||
return M
|
||||
@@ -1,196 +0,0 @@
|
||||
local installer = require("druid.editor_scripts.core.asset_store.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
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
-- base64 encode/decode (http://lua-users.org/wiki/BaseSixtyFour)
|
||||
|
||||
local M = {}
|
||||
|
||||
local b='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||
|
||||
function M.encode(data)
|
||||
return ((data:gsub('.', function(x)
|
||||
local r,byte_val='',x:byte()
|
||||
for i=8,1,-1 do r=r..(byte_val%2^i-byte_val%2^(i-1)>0 and '1' or '0') end
|
||||
return r;
|
||||
end)..'0000'):gsub('%d%d%d?%d?%d?%d?', function(x)
|
||||
if (#x < 6) then return '' end
|
||||
local c=0
|
||||
for i=1,6 do c=c+(x:sub(i,i)=='1' and 2^(6-i) or 0) end
|
||||
return b:sub(c+1,c+1)
|
||||
end)..({ '', '==', '=' })[#data%3+1])
|
||||
end
|
||||
|
||||
function M.decode(data)
|
||||
data = string.gsub(data, '[^'..b..'=]', '')
|
||||
return (data:gsub('.', function(x)
|
||||
if (x == '=') then return '' end
|
||||
local r,f='',(b:find(x)-1)
|
||||
for i=6,1,-1 do r=r..(f%2^i-f%2^(i-1)>0 and '1' or '0') end
|
||||
return r;
|
||||
end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x)
|
||||
if (#x ~= 8) then return '' end
|
||||
local c=0
|
||||
for i=1,8 do c=c+(x:sub(i,i)=='1' and 2^(8-i) or 0) end
|
||||
return string.char(c)
|
||||
end))
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -1,224 +0,0 @@
|
||||
--- Module for handling widget installation from zip files
|
||||
--- Downloads zip files and extracts them to the specified folder
|
||||
|
||||
local base64 = require("druid.editor_scripts.core.asset_store.base64")
|
||||
local path_replacer = require("druid.editor_scripts.core.asset_store.path_replacer")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class druid.core.item_info
|
||||
---@field id string
|
||||
---@field version string
|
||||
---@field title string
|
||||
---@field author string
|
||||
---@field description string
|
||||
---@field api string
|
||||
---@field author_url string
|
||||
---@field image string
|
||||
---@field manifest_url string
|
||||
---@field zip_url string
|
||||
---@field json_zip_url string
|
||||
---@field sha256 string
|
||||
---@field size number
|
||||
---@field depends string[]
|
||||
---@field tags string[]
|
||||
|
||||
|
||||
---Download a file from URL
|
||||
---@param url string - The URL to download from
|
||||
---@return string|nil, string|nil, table|nil - Downloaded content or nil, filename or nil, content list or nil
|
||||
local function download_file_zip_json(url)
|
||||
local response = http.request(url, { as = "json" })
|
||||
|
||||
if response.status ~= 200 then
|
||||
print("Failed to download file. HTTP status: " .. response.status)
|
||||
return nil
|
||||
end
|
||||
|
||||
local data = response.body
|
||||
local content_list = data.content -- Array of file paths from zip
|
||||
|
||||
return base64.decode(data.data), data.filename, content_list
|
||||
end
|
||||
|
||||
|
||||
---Find widget by dependency string (format: "author:widget_id@version" or "author@widget_id" or "widget_id")
|
||||
---@param dep_string string - Dependency string
|
||||
---@param all_items table - List of all available widgets
|
||||
---@return table|nil - Found widget item or nil
|
||||
local function find_widget_by_dependency(dep_string, all_items)
|
||||
if not dep_string or not all_items then
|
||||
return nil
|
||||
end
|
||||
|
||||
local author, widget_id
|
||||
|
||||
-- Try format: "author:widget_id@version" (e.g., "Insality:mini_graph@1")
|
||||
author, widget_id = dep_string:match("^([^:]+):([^@]+)@")
|
||||
if not author then
|
||||
-- Try format: "author@widget_id" (e.g., "insality@mini_graph")
|
||||
author, widget_id = dep_string:match("^([^@]+)@(.+)$")
|
||||
if not author then
|
||||
-- No author specified, search by widget_id only
|
||||
widget_id = dep_string
|
||||
end
|
||||
end
|
||||
|
||||
for _, item in ipairs(all_items) do
|
||||
if item.id == widget_id then
|
||||
-- If author was specified, check it matches (case-insensitive)
|
||||
if not author or string.lower(item.author or "") == string.lower(author) then
|
||||
return item
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
---Install widget dependencies recursively
|
||||
---@param item druid.core.item_info - Widget item
|
||||
---@param all_items table - List of all available widgets
|
||||
---@param install_folder string - Installation folder
|
||||
---@param installing_set table - Set of widget IDs currently being installed (to prevent cycles)
|
||||
---@return boolean, string|nil - Success status and message
|
||||
local function install_dependencies(item, all_items, install_folder, installing_set)
|
||||
if not item.depends or #item.depends == 0 then
|
||||
return true, nil
|
||||
end
|
||||
|
||||
installing_set = installing_set or {}
|
||||
|
||||
for _, dep_string in ipairs(item.depends) do
|
||||
local dep_item = find_widget_by_dependency(dep_string, all_items)
|
||||
if not dep_item then
|
||||
print("Warning: Dependency not found:", dep_string)
|
||||
-- Continue with other dependencies
|
||||
else
|
||||
-- Check if already installed
|
||||
if M.is_widget_installed(dep_item, install_folder) then
|
||||
print("Dependency already installed:", dep_item.id)
|
||||
else
|
||||
-- Check for circular dependencies
|
||||
if installing_set[dep_item.id] then
|
||||
print("Warning: Circular dependency detected:", dep_item.id)
|
||||
-- Continue with other dependencies
|
||||
else
|
||||
print("Installing dependency:", dep_item.id)
|
||||
local success, err = M.install_widget(dep_item, install_folder, all_items, installing_set)
|
||||
if not success then
|
||||
return false, "Failed to install dependency " .. dep_item.id .. ": " .. (err or "unknown error")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return true, nil
|
||||
end
|
||||
|
||||
|
||||
---Install a widget from a zip URL
|
||||
---@param item druid.core.item_info - Widget item data containing zip_url and id
|
||||
---@param install_folder string - Target folder to install to
|
||||
---@param all_items table|nil - Optional list of all widgets for dependency resolution
|
||||
---@param installing_set table|nil - Optional set of widget IDs currently being installed (to prevent cycles)
|
||||
---@return boolean, string - Success status and message
|
||||
function M.install_widget(item, install_folder, all_items, installing_set)
|
||||
if not item.json_zip_url or not item.id then
|
||||
return false, "Invalid widget data: missing json_zip_url or id"
|
||||
end
|
||||
|
||||
-- Install dependencies first if all_items is provided
|
||||
if all_items then
|
||||
installing_set = installing_set or {}
|
||||
if installing_set[item.id] then
|
||||
return false, "Circular dependency detected: " .. item.id
|
||||
end
|
||||
installing_set[item.id] = true
|
||||
local dep_success, dep_err = install_dependencies(item, all_items, install_folder, installing_set)
|
||||
if not dep_success then
|
||||
installing_set[item.id] = nil
|
||||
return false, dep_err or "Failed to install dependencies"
|
||||
end
|
||||
end
|
||||
|
||||
-- Download the zip file
|
||||
local zip_data, filename, content_list = download_file_zip_json(item.json_zip_url)
|
||||
if not zip_data or not filename then
|
||||
if installing_set then
|
||||
installing_set[item.id] = nil
|
||||
end
|
||||
return false, "Failed to download widget: " .. (filename or "unknown error")
|
||||
end
|
||||
|
||||
if content_list then
|
||||
print("Got file list from JSON:", #content_list, "files")
|
||||
else
|
||||
print("Warning: No content list in JSON data")
|
||||
end
|
||||
|
||||
|
||||
local zip_file_path = "." .. install_folder .. "/" .. filename
|
||||
local zip_file = io.open(zip_file_path, "wb")
|
||||
if not zip_file then
|
||||
if installing_set then
|
||||
installing_set[item.id] = nil
|
||||
end
|
||||
print("Directory does not exist: " .. install_folder)
|
||||
print("Please create the directory manually and try again.")
|
||||
return false, "Directory does not exist: " .. install_folder
|
||||
end
|
||||
|
||||
zip_file:write(zip_data)
|
||||
zip_file:close()
|
||||
print("Zip written to file: " .. zip_file_path)
|
||||
|
||||
-- Unzip the zip file
|
||||
local folder_path = "." .. install_folder .. "/" .. item.id
|
||||
|
||||
zip.unpack(zip_file_path, folder_path)
|
||||
print("Widget unpacked successfully")
|
||||
|
||||
-- Remove the zip file
|
||||
os.remove(zip_file_path)
|
||||
print("Zip file removed successfully")
|
||||
|
||||
-- Process paths within the extracted widget
|
||||
if content_list and #content_list > 0 then
|
||||
local success, err = path_replacer.process_widget_paths(folder_path, install_folder, item.id, item.author, content_list)
|
||||
if not success then
|
||||
print("Warning: Path replacement failed:", err)
|
||||
-- Don't fail installation if path replacement fails, just warn
|
||||
end
|
||||
else
|
||||
print("Warning: No file list available, skipping path replacement")
|
||||
end
|
||||
|
||||
if installing_set then
|
||||
installing_set[item.id] = nil
|
||||
end
|
||||
|
||||
return true, "Widget installed successfully"
|
||||
end
|
||||
|
||||
|
||||
---Check if a widget is already installed
|
||||
---@param item table - Widget item data containing id
|
||||
---@param install_folder string - Install folder to check in
|
||||
---@return boolean - True if widget is already installed
|
||||
function M.is_widget_installed(item, install_folder)
|
||||
local p = editor.resource_attributes(install_folder .. "/" .. item.id)
|
||||
return p.exists
|
||||
end
|
||||
|
||||
|
||||
---Get installation folder
|
||||
---@return string - Installation folder path
|
||||
function M.get_install_folder()
|
||||
return editor.prefs.get("druid.asset_install_folder")
|
||||
end
|
||||
|
||||
|
||||
return M
|
||||
@@ -1,121 +0,0 @@
|
||||
--- Module for replacing widget paths in installed files
|
||||
--- Handles path replacement from original widget structure to user's installation path
|
||||
|
||||
local system = require("druid.editor_scripts.defold_parser.system.parser_internal")
|
||||
|
||||
local M = {}
|
||||
|
||||
|
||||
---Replace paths in file content
|
||||
---@param content string - File content
|
||||
---@param author string - Author name (e.g., "Insality")
|
||||
---@param install_folder string - Installation folder (e.g., "widget")
|
||||
---@return string - Modified content
|
||||
local function replace_paths_in_content(content, author, install_folder)
|
||||
if not content or not author then
|
||||
return content
|
||||
end
|
||||
|
||||
-- Escape special characters for literal string replacement
|
||||
local function escape_pattern(str)
|
||||
return str:gsub("[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0")
|
||||
end
|
||||
|
||||
-- Remove leading / from install_folder if present
|
||||
local clean_install_folder = install_folder
|
||||
if clean_install_folder:sub(1, 1) == "/" then
|
||||
clean_install_folder = clean_install_folder:sub(2)
|
||||
end
|
||||
|
||||
-- Replace all paths with author: widget/Insality/* -> widget/*
|
||||
local author_path_pattern = escape_pattern(clean_install_folder .. "/" .. author .. "/")
|
||||
local target_path_prefix = clean_install_folder .. "/"
|
||||
content = content:gsub(author_path_pattern, target_path_prefix)
|
||||
|
||||
-- Replace all require statements with dots: widget.Insality.* -> widget.*
|
||||
local author_dots_pattern = escape_pattern(clean_install_folder .. "." .. author .. ".")
|
||||
local target_dots_prefix = clean_install_folder .. "."
|
||||
content = content:gsub(author_dots_pattern, target_dots_prefix)
|
||||
|
||||
-- Also replace paths that start with author directly: Insality/widget -> widget
|
||||
-- But only if they're in require statements or paths
|
||||
local author_start_pattern = escape_pattern(author .. "/")
|
||||
content = content:gsub(author_start_pattern, "")
|
||||
local author_start_dots_pattern = escape_pattern(author .. ".")
|
||||
content = content:gsub(author_start_dots_pattern, "")
|
||||
|
||||
return content
|
||||
end
|
||||
|
||||
|
||||
---Process widget paths in all files
|
||||
---@param folder_path string - Path to the unpacked widget folder
|
||||
---@param install_folder string - Installation folder (e.g., "widget")
|
||||
---@param widget_id string - Widget ID (e.g., "fps_panel")
|
||||
---@param author string|nil - Author name (e.g., "Insality")
|
||||
---@param file_list table - Optional list of file paths from zip content
|
||||
---@return boolean, string|nil - Success status and error message if any
|
||||
function M.process_widget_paths(folder_path, install_folder, widget_id, author, file_list)
|
||||
print("Processing widget paths in:", folder_path)
|
||||
|
||||
if not author then
|
||||
print("Warning: Missing author, skipping path replacement")
|
||||
return true, nil
|
||||
end
|
||||
|
||||
print("Replacing all paths with author:", author, "in install folder:", install_folder)
|
||||
|
||||
-- Get absolute project path
|
||||
local absolute_project_path = editor.external_file_attributes(".").path
|
||||
if not absolute_project_path:match("[\\/]$") then
|
||||
absolute_project_path = absolute_project_path .. "/"
|
||||
end
|
||||
|
||||
-- Clean folder_path
|
||||
local clean_folder_path = folder_path
|
||||
if clean_folder_path:sub(1, 1) == "." then
|
||||
clean_folder_path = clean_folder_path:sub(2)
|
||||
end
|
||||
if clean_folder_path:sub(1, 1) == "/" then
|
||||
clean_folder_path = clean_folder_path:sub(2)
|
||||
end
|
||||
|
||||
-- Process each file from the list
|
||||
local processed_count = 0
|
||||
for _, file_path_in_zip in ipairs(file_list) do
|
||||
-- Build full path to the file after unpacking
|
||||
local file_path = clean_folder_path .. "/" .. file_path_in_zip
|
||||
|
||||
-- Get absolute path
|
||||
local clean_file_path = file_path
|
||||
if clean_file_path:sub(1, 1) == "/" then
|
||||
clean_file_path = clean_file_path:sub(2)
|
||||
end
|
||||
local absolute_file_path = absolute_project_path .. clean_file_path
|
||||
|
||||
-- Read file content
|
||||
local content, err = system.read_file(absolute_file_path)
|
||||
if not content then
|
||||
print("Warning: Could not read file:", file_path, err)
|
||||
else
|
||||
-- Replace all paths with author
|
||||
local modified_content = replace_paths_in_content(content, author, install_folder)
|
||||
if modified_content ~= content then
|
||||
-- Write modified content back
|
||||
local success, write_err = system.write_file(absolute_file_path, modified_content)
|
||||
if success then
|
||||
processed_count = processed_count + 1
|
||||
print("Processed:", file_path)
|
||||
else
|
||||
print("Warning: Could not write file:", file_path, write_err)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
print("Path replacement complete. Processed", processed_count, "files")
|
||||
return true, nil
|
||||
end
|
||||
|
||||
|
||||
return M
|
||||
@@ -1,19 +0,0 @@
|
||||
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
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
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
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
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
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
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
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
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
|
||||
@@ -1,48 +0,0 @@
|
||||
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
|
||||
@@ -2,7 +2,6 @@ local assign_layers = require("druid.editor_scripts.assign_layers")
|
||||
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",
|
||||
@@ -79,20 +78,6 @@ function M.get_commands()
|
||||
end
|
||||
},
|
||||
|
||||
{
|
||||
label = "[Druid] Asset Store",
|
||||
locations = { "Edit" },
|
||||
run = function()
|
||||
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
|
||||
},
|
||||
|
||||
{
|
||||
label = "[Druid] Settings",
|
||||
locations = { "Edit" },
|
||||
|
||||
Reference in New Issue
Block a user