DNS record management, CSS grid, Regex, location.query
This commit is contained in:
@@ -44,7 +44,7 @@ local function renderDomains()
|
|||||||
|
|
||||||
for i, domain in ipairs(domains) do
|
for i, domain in ipairs(domains) do
|
||||||
local domainItem = gurt.create('div', {
|
local domainItem = gurt.create('div', {
|
||||||
style = 'domain-item'
|
style = 'domain-item cursor-pointer hover:bg-[#4b5563]'
|
||||||
})
|
})
|
||||||
|
|
||||||
local domainInfo = gurt.create('div', { style = 'w-full' })
|
local domainInfo = gurt.create('div', { style = 'w-full' })
|
||||||
@@ -54,51 +54,20 @@ local function renderDomains()
|
|||||||
style = 'font-bold text-lg'
|
style = 'font-bold text-lg'
|
||||||
})
|
})
|
||||||
|
|
||||||
local domainIP = gurt.create('div', {
|
|
||||||
text = 'IP: ' .. domain.ip,
|
|
||||||
style = 'text-[#6b7280]'
|
|
||||||
})
|
|
||||||
|
|
||||||
local domainStatus = gurt.create('div', {
|
local domainStatus = gurt.create('div', {
|
||||||
text = 'Status: ' .. (domain.status or 'Unknown'),
|
text = 'Status: ' .. (domain.status or 'Unknown'),
|
||||||
style = 'text-[#6b7280]'
|
style = 'text-[#6b7280]'
|
||||||
})
|
})
|
||||||
|
|
||||||
domainInfo:append(domainName)
|
domainInfo:append(domainName)
|
||||||
domainInfo:append(domainIP)
|
|
||||||
domainInfo:append(domainStatus)
|
domainInfo:append(domainStatus)
|
||||||
|
|
||||||
local actions = gurt.create('div', {
|
|
||||||
style = 'flex gap-2'
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Update IP button
|
|
||||||
local updateBtn = gurt.create('button', {
|
|
||||||
text = 'Update IP',
|
|
||||||
style = 'secondary-btn'
|
|
||||||
})
|
|
||||||
|
|
||||||
updateBtn:on('click', function()
|
|
||||||
local newIP = prompt('Enter new IP address for ' .. domain.name .. '.' .. domain.tld .. ':')
|
|
||||||
if newIP and newIP ~= '' then
|
|
||||||
updateDomainIP(domain.name, domain.tld, newIP)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- Delete button
|
|
||||||
local deleteBtn = gurt.create('button', { text = 'Delete', style = 'danger-btn' })
|
|
||||||
|
|
||||||
deleteBtn:on('click', function()
|
|
||||||
if confirm('Are you sure you want to delete ' .. domain.name .. '.' .. domain.tld .. '?') then
|
|
||||||
deleteDomain(domain.name, domain.tld)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
actions:append(updateBtn)
|
|
||||||
actions:append(deleteBtn)
|
|
||||||
|
|
||||||
domainItem:append(domainInfo)
|
domainItem:append(domainInfo)
|
||||||
domainItem:append(actions)
|
|
||||||
|
domainItem:on('click', function()
|
||||||
|
gurt.location.goto('/domain.html?name=' .. domain.name .. '.' .. domain.tld)
|
||||||
|
end)
|
||||||
|
|
||||||
domainsList:append(domainItem)
|
domainsList:append(domainItem)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
137
dns/frontend/domain.html
Normal file
137
dns/frontend/domain.html
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
<head>
|
||||||
|
<title>Domain Management</title>
|
||||||
|
<icon src="https://cdn-icons-png.flaticon.com/512/295/295128.png">
|
||||||
|
<meta name="theme-color" content="#0891b2">
|
||||||
|
<meta name="description" content="Manage DNS records for your domain">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
bg-[#171616] font-sans text-white
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-[#ef4444] text-3xl font-bold text-center
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
text-[#dc2626] text-xl font-semibold
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
text-[#fca5a5] text-lg font-medium
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
bg-[#262626] p-6 rounded-lg shadow-lg max-w-6xl
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn {
|
||||||
|
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#dc2626] text-white
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-btn {
|
||||||
|
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#ef4444] text-white
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-btn {
|
||||||
|
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#b91c1c] text-white
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-btn {
|
||||||
|
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#525252] text-white
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
flex flex-col gap-2 mb-4 w-full
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
w-full p-3 border border-gray-600 rounded-md bg-[#374151] text-white active:border-red-500
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-select {
|
||||||
|
w-full p-3 border border-gray-600 rounded-md bg-[#374151] text-white active:border-red-500 active:text-white
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
bg-[#262626] p-6 rounded-lg shadow border border-gray-700
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card {
|
||||||
|
bg-[#1f1f1f] p-4 rounded-lg border border-[#dc2626]
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-item {
|
||||||
|
bg-[#374151] p-4 rounded-lg border border-gray-700 mb-2 flex justify-between items-center
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-text {
|
||||||
|
text-[#fca5a5] text-sm
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-table {
|
||||||
|
w-full border
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-table th {
|
||||||
|
p-3 text-left border-b border-gray-600 text-[#dc2626]
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-table td {
|
||||||
|
p-3 border-b border-gray-700
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script src="domain.lua" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div style="container mt-6">
|
||||||
|
<div style="stats-card mb-6">
|
||||||
|
<div style="flex justify-between items-center w-full">
|
||||||
|
<div>
|
||||||
|
<h1 id="domain-title">Loading...</h1>
|
||||||
|
<p id="domain-status" style="text-[#6b7280]">Status: Loading...</p>
|
||||||
|
</div>
|
||||||
|
<div style="flex gap-2">
|
||||||
|
<button id="back-btn" style="secondary-btn">Back</button>
|
||||||
|
<button id="logout-btn" style="secondary-btn">Logout</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="card mb-6">
|
||||||
|
<h2 style="mb-4">Records</h2>
|
||||||
|
<div id="records-list">
|
||||||
|
<p id="records-loading">Loading DNS records...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="card">
|
||||||
|
<h2 style="mb-4">New Record</h2>
|
||||||
|
<div style="form-group">
|
||||||
|
<p>Type:</p>
|
||||||
|
<select id="record-type" style="form-select">
|
||||||
|
<option value="A">A</option>
|
||||||
|
<option value="AAAA">AAAA</option>
|
||||||
|
<option value="CNAME">CNAME</option>
|
||||||
|
<option value="TXT">TXT</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div style="form-group">
|
||||||
|
<p>Name:</p>
|
||||||
|
<input id="record-name" type="text" style="form-input" placeholder="@, www, *" pattern="^(?:\*|@|((?!-)[A-Za-z0-9-]{1,63}(?<!-)(\.(?!-)[A-Za-z0-9-]{1,63}(?<!-))*))$" />
|
||||||
|
</div>
|
||||||
|
<div style="form-group">
|
||||||
|
<p>Value:</p>
|
||||||
|
<input id="record-value" type="text" style="form-input" placeholder="192.168.1.1, example.com" />
|
||||||
|
</div>
|
||||||
|
<div style="form-group">
|
||||||
|
<p>TTL:</p>
|
||||||
|
<input id="record-ttl" type="text" style="form-input" placeholder="3600" pattern="^[0-9]+$" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="record-error" style="error-text hidden mb-2"></div>
|
||||||
|
<button id="add-record-btn" style="success-btn">Add Record</button>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
300
dns/frontend/domain.lua
Normal file
300
dns/frontend/domain.lua
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
local user = nil
|
||||||
|
local domain = nil
|
||||||
|
local records = {}
|
||||||
|
local authToken = nil
|
||||||
|
local domainName = nil
|
||||||
|
|
||||||
|
local domainTitle = gurt.select('#domain-title')
|
||||||
|
local domainStatus = gurt.select('#domain-status')
|
||||||
|
local recordsList = gurt.select('#records-list')
|
||||||
|
local loadingElement = gurt.select('#records-loading')
|
||||||
|
|
||||||
|
local function showError(elementId, message)
|
||||||
|
local element = gurt.select('#' .. elementId)
|
||||||
|
|
||||||
|
element.text = message
|
||||||
|
element.classList:remove('hidden')
|
||||||
|
end
|
||||||
|
|
||||||
|
local function hideError(elementId)
|
||||||
|
local element = gurt.select('#' .. elementId)
|
||||||
|
|
||||||
|
element.classList:add('hidden')
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Forward declarations
|
||||||
|
local loadRecords
|
||||||
|
local renderRecords
|
||||||
|
|
||||||
|
local function deleteRecord(recordId)
|
||||||
|
print('Deleting DNS record: ' .. recordId)
|
||||||
|
|
||||||
|
local response = fetch('gurt://localhost:8877/domain/' .. domainName .. '/records/' .. recordId, {
|
||||||
|
method = 'DELETE',
|
||||||
|
headers = {
|
||||||
|
Authorization = 'Bearer ' .. authToken
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if response:ok() then
|
||||||
|
print('DNS record deleted successfully')
|
||||||
|
|
||||||
|
-- Remove the record from local records array
|
||||||
|
for i = #records, 1, -1 do
|
||||||
|
if records[i].id == recordId then
|
||||||
|
table.remove(records, i)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Re-render the entire list from scratch
|
||||||
|
renderRecords()
|
||||||
|
else
|
||||||
|
print('Failed to delete DNS record: ' .. response:text())
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Actual implementation
|
||||||
|
loadRecords = function()
|
||||||
|
print('Loading DNS records for: ' .. domainName)
|
||||||
|
local response = fetch('gurt://localhost:8877/domain/' .. domainName .. '/records', {
|
||||||
|
headers = {
|
||||||
|
Authorization = 'Bearer ' .. authToken
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if response:ok() then
|
||||||
|
records = response:json()
|
||||||
|
print('Loaded ' .. #records .. ' DNS records')
|
||||||
|
renderRecords()
|
||||||
|
else
|
||||||
|
print('Failed to load DNS records: ' .. response:text())
|
||||||
|
records = {}
|
||||||
|
renderRecords()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
renderRecords = function(appendOnly)
|
||||||
|
if loadingElement then
|
||||||
|
loadingElement:remove()
|
||||||
|
loadingElement = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Clear everything if not appending
|
||||||
|
if not appendOnly then
|
||||||
|
local children = recordsList.children
|
||||||
|
while #children > 0 do
|
||||||
|
children[1]:remove()
|
||||||
|
children = recordsList.children
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #records == 0 then
|
||||||
|
local emptyMessage = gurt.create('div', {
|
||||||
|
text = 'No DNS records found. Add your first record below!',
|
||||||
|
style = 'text-center text-[#6b7280] py-8'
|
||||||
|
})
|
||||||
|
recordsList:append(emptyMessage)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Create header only if not appending or if list is empty
|
||||||
|
if not appendOnly or #recordsList.children == 0 then
|
||||||
|
local header = gurt.create('div', { style = 'w-full flex justify-between gap-4 p-4 bg-gray-600 font-bold border-b rounded-xl' })
|
||||||
|
|
||||||
|
local typeHeader = gurt.create('div', { text = 'Type' })
|
||||||
|
local nameHeader = gurt.create('div', { text = 'Name' })
|
||||||
|
local valueHeader = gurt.create('div', { text = 'Value' })
|
||||||
|
local ttlHeader = gurt.create('div', { text = 'TTL' })
|
||||||
|
local actionsHeader = gurt.create('div', { text = 'Actions' })
|
||||||
|
|
||||||
|
header:append(typeHeader)
|
||||||
|
header:append(nameHeader)
|
||||||
|
header:append(valueHeader)
|
||||||
|
header:append(ttlHeader)
|
||||||
|
header:append(actionsHeader)
|
||||||
|
recordsList:append(header)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Create records list - when appending, only render the last record; otherwise render all
|
||||||
|
local startIndex = appendOnly and #records or 1
|
||||||
|
for i = startIndex, #records do
|
||||||
|
local record = records[i]
|
||||||
|
local row = gurt.create('div', { style = 'w-full flex justify-between gap-4 p-4 border-b border-gray-600 hover:bg-[rgba(244, 67, 54, 0.2)]' })
|
||||||
|
|
||||||
|
local typeCell = gurt.create('div', { text = record.type, style = 'font-bold' })
|
||||||
|
local nameCell = gurt.create('div', { text = record.name or '@' })
|
||||||
|
local valueCell = gurt.create('div', { text = record.value, style = 'font-mono text-sm break-all' })
|
||||||
|
local ttlCell = gurt.create('div', { text = record.ttl or '3600' })
|
||||||
|
|
||||||
|
local actionsCell = gurt.create('div')
|
||||||
|
local deleteBtn = gurt.create('button', {
|
||||||
|
text = 'Delete',
|
||||||
|
style = 'danger-btn text-xs px-2 py-1'
|
||||||
|
})
|
||||||
|
|
||||||
|
deleteBtn:on('click', function()
|
||||||
|
if deleteBtn.text == 'Delete' then
|
||||||
|
deleteBtn.text = 'Confirm Delete'
|
||||||
|
else
|
||||||
|
deleteRecord(record.id)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
actionsCell:append(deleteBtn)
|
||||||
|
|
||||||
|
row:append(typeCell)
|
||||||
|
row:append(nameCell)
|
||||||
|
row:append(valueCell)
|
||||||
|
row:append(ttlCell)
|
||||||
|
row:append(actionsCell)
|
||||||
|
recordsList:append(row)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getDomainNameFromURL()
|
||||||
|
local nameParam = gurt.location.query.get('name')
|
||||||
|
if nameParam then
|
||||||
|
return nameParam:gsub('%%%.', '.')
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function updateDomainInfo()
|
||||||
|
if domain then
|
||||||
|
domainTitle.text = domain.name .. '.' .. domain.tld
|
||||||
|
domainStatus.text = 'Status: ' .. (domain.status or 'Unknown')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function loadDomain()
|
||||||
|
print('Loading domain details for: ' .. domainName)
|
||||||
|
local response = fetch('gurt://localhost:8877/domain/' .. domainName, {
|
||||||
|
headers = {
|
||||||
|
Authorization = 'Bearer ' .. authToken
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if response:ok() then
|
||||||
|
domain = response:json()
|
||||||
|
print('Loaded domain details')
|
||||||
|
updateDomainInfo()
|
||||||
|
loadRecords()
|
||||||
|
else
|
||||||
|
print('Failed to load domain: ' .. response:text())
|
||||||
|
--gurt.location.goto('/dashboard.html')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function checkAuth()
|
||||||
|
authToken = gurt.crumbs.get("auth_token")
|
||||||
|
|
||||||
|
if authToken then
|
||||||
|
print('Found auth token, checking validity...')
|
||||||
|
local response = fetch('gurt://localhost:8877/auth/me', {
|
||||||
|
headers = {
|
||||||
|
Authorization = 'Bearer ' .. authToken
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if response:ok() then
|
||||||
|
user = response:json()
|
||||||
|
print('Authentication successful for user: ' .. user.username)
|
||||||
|
|
||||||
|
domainName = getDomainNameFromURL()
|
||||||
|
if domainName then
|
||||||
|
loadDomain()
|
||||||
|
else
|
||||||
|
print('No domain name in URL, redirecting to dashboard')
|
||||||
|
--gurt.location.goto('/dashboard.html')
|
||||||
|
end
|
||||||
|
else
|
||||||
|
print('Token invalid, redirecting to login...')
|
||||||
|
gurt.crumbs.delete('auth_token')
|
||||||
|
--gurt.location.goto('../')
|
||||||
|
end
|
||||||
|
else
|
||||||
|
print('No auth token found, redirecting to login...')
|
||||||
|
--gurt.location.goto('../')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function addRecord(type, name, value, ttl)
|
||||||
|
hideError('record-error')
|
||||||
|
print('Adding DNS record: ' .. type .. ' ' .. name .. ' ' .. value)
|
||||||
|
|
||||||
|
local response = fetch('gurt://localhost:8877/domain/' .. domainName .. '/records', {
|
||||||
|
method = 'POST',
|
||||||
|
headers = {
|
||||||
|
['Content-Type'] = 'application/json',
|
||||||
|
Authorization = 'Bearer ' .. authToken
|
||||||
|
},
|
||||||
|
body = JSON.stringify({
|
||||||
|
type = type,
|
||||||
|
name = name,
|
||||||
|
value = value,
|
||||||
|
ttl = ttl
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if response:ok() then
|
||||||
|
print('DNS record added successfully')
|
||||||
|
|
||||||
|
-- Clear form
|
||||||
|
gurt.select('#record-name').value = ''
|
||||||
|
gurt.select('#record-value').value = ''
|
||||||
|
gurt.select('#record-ttl').value = '3600'
|
||||||
|
|
||||||
|
-- Add the new record to existing records array
|
||||||
|
local newRecord = response:json()
|
||||||
|
if newRecord and newRecord.id then
|
||||||
|
-- Server returned the created record, add it to our local array
|
||||||
|
table.insert(records, newRecord)
|
||||||
|
-- Render only the new record
|
||||||
|
renderRecords(true)
|
||||||
|
else
|
||||||
|
-- Server didn't return record details, reload to get the actual data
|
||||||
|
loadRecords()
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local error = response:text()
|
||||||
|
showError('record-error', 'Failed to add record: ' .. error)
|
||||||
|
print('Failed to add DNS record: ' .. error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function logout()
|
||||||
|
gurt.crumbs.delete('auth_token')
|
||||||
|
print('Logged out successfully')
|
||||||
|
--gurt.location.goto("../")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function goBack()
|
||||||
|
--gurt.location.goto("/dashboard.html")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Event handlers
|
||||||
|
gurt.select('#logout-btn'):on('click', logout)
|
||||||
|
gurt.select('#back-btn'):on('click', goBack)
|
||||||
|
|
||||||
|
gurt.select('#add-record-btn'):on('click', function()
|
||||||
|
local recordType = gurt.select('#record-type').value
|
||||||
|
local recordName = gurt.select('#record-name').value
|
||||||
|
local recordValue = gurt.select('#record-value').value
|
||||||
|
local recordTTL = tonumber(gurt.select('#record-ttl').value) or 3600
|
||||||
|
|
||||||
|
if not recordValue or recordValue == '' then
|
||||||
|
showError('record-error', 'Record value is required')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if not recordName or recordName == '' then
|
||||||
|
recordName = '@'
|
||||||
|
end
|
||||||
|
|
||||||
|
addRecord(recordType, recordName, recordValue, recordTTL)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Initialize
|
||||||
|
print('Domain management page initialized')
|
||||||
|
checkAuth()
|
||||||
@@ -94,6 +94,7 @@ enum HandlerType {
|
|||||||
UpdateDomain,
|
UpdateDomain,
|
||||||
DeleteDomain,
|
DeleteDomain,
|
||||||
GetUserDomains,
|
GetUserDomains,
|
||||||
|
CreateDomainRecord,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GurtHandler for AppHandler {
|
impl GurtHandler for AppHandler {
|
||||||
@@ -118,7 +119,13 @@ impl GurtHandler for AppHandler {
|
|||||||
|
|
||||||
let result = match handler_type {
|
let result = match handler_type {
|
||||||
HandlerType::Index => routes::index(app_state).await,
|
HandlerType::Index => routes::index(app_state).await,
|
||||||
HandlerType::GetDomain => routes::get_domain(&ctx, app_state).await,
|
HandlerType::GetDomain => {
|
||||||
|
if ctx.path().contains("/records") {
|
||||||
|
handle_authenticated!(ctx, app_state, routes::get_domain_records)
|
||||||
|
} else {
|
||||||
|
handle_authenticated!(ctx, app_state, routes::get_domain)
|
||||||
|
}
|
||||||
|
},
|
||||||
HandlerType::GetDomains => routes::get_domains(&ctx, app_state).await,
|
HandlerType::GetDomains => routes::get_domains(&ctx, app_state).await,
|
||||||
HandlerType::GetTlds => routes::get_tlds(app_state).await,
|
HandlerType::GetTlds => routes::get_tlds(app_state).await,
|
||||||
HandlerType::CheckDomain => routes::check_domain(&ctx, app_state).await,
|
HandlerType::CheckDomain => routes::check_domain(&ctx, app_state).await,
|
||||||
@@ -130,6 +137,13 @@ impl GurtHandler for AppHandler {
|
|||||||
HandlerType::CreateDomainInvite => handle_authenticated!(ctx, app_state, auth_routes::create_domain_invite),
|
HandlerType::CreateDomainInvite => handle_authenticated!(ctx, app_state, auth_routes::create_domain_invite),
|
||||||
HandlerType::RedeemDomainInvite => handle_authenticated!(ctx, app_state, auth_routes::redeem_domain_invite),
|
HandlerType::RedeemDomainInvite => handle_authenticated!(ctx, app_state, auth_routes::redeem_domain_invite),
|
||||||
HandlerType::GetUserDomains => handle_authenticated!(ctx, app_state, routes::get_user_domains),
|
HandlerType::GetUserDomains => handle_authenticated!(ctx, app_state, routes::get_user_domains),
|
||||||
|
HandlerType::CreateDomainRecord => {
|
||||||
|
if ctx.path().contains("/records") {
|
||||||
|
handle_authenticated!(ctx, app_state, routes::create_domain_record)
|
||||||
|
} else {
|
||||||
|
Ok(GurtResponse::new(GurtStatusCode::MethodNotAllowed).with_string_body("Method not allowed"))
|
||||||
|
}
|
||||||
|
},
|
||||||
HandlerType::CreateDomain => {
|
HandlerType::CreateDomain => {
|
||||||
// Check rate limit first
|
// Check rate limit first
|
||||||
if let Some(ref rate_limit_state) = rate_limit_state {
|
if let Some(ref rate_limit_state) = rate_limit_state {
|
||||||
@@ -142,7 +156,13 @@ impl GurtHandler for AppHandler {
|
|||||||
handle_authenticated!(ctx, app_state, routes::create_domain)
|
handle_authenticated!(ctx, app_state, routes::create_domain)
|
||||||
},
|
},
|
||||||
HandlerType::UpdateDomain => handle_authenticated!(ctx, app_state, routes::update_domain),
|
HandlerType::UpdateDomain => handle_authenticated!(ctx, app_state, routes::update_domain),
|
||||||
HandlerType::DeleteDomain => handle_authenticated!(ctx, app_state, routes::delete_domain),
|
HandlerType::DeleteDomain => {
|
||||||
|
if ctx.path().contains("/records/") {
|
||||||
|
handle_authenticated!(ctx, app_state, routes::delete_domain_record)
|
||||||
|
} else {
|
||||||
|
handle_authenticated!(ctx, app_state, routes::delete_domain)
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let duration = start_time.elapsed();
|
let duration = start_time.elapsed();
|
||||||
@@ -196,7 +216,6 @@ pub async fn start(cli: crate::Cli) -> std::io::Result<()> {
|
|||||||
|
|
||||||
server = server
|
server = server
|
||||||
.route(Route::get("/"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::Index })
|
.route(Route::get("/"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::Index })
|
||||||
.route(Route::get("/domain/*"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::GetDomain })
|
|
||||||
.route(Route::get("/domains"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::GetDomains })
|
.route(Route::get("/domains"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::GetDomains })
|
||||||
.route(Route::get("/tlds"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::GetTlds })
|
.route(Route::get("/tlds"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::GetTlds })
|
||||||
.route(Route::get("/check"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::CheckDomain })
|
.route(Route::get("/check"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::CheckDomain })
|
||||||
@@ -209,6 +228,8 @@ pub async fn start(cli: crate::Cli) -> std::io::Result<()> {
|
|||||||
.route(Route::post("/auth/redeem-domain-invite"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::RedeemDomainInvite })
|
.route(Route::post("/auth/redeem-domain-invite"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::RedeemDomainInvite })
|
||||||
.route(Route::get("/auth/domains"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::GetUserDomains })
|
.route(Route::get("/auth/domains"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::GetUserDomains })
|
||||||
.route(Route::post("/domain"), AppHandler { app_state: app_state.clone(), rate_limit_state: Some(rate_limit_state), handler_type: HandlerType::CreateDomain })
|
.route(Route::post("/domain"), AppHandler { app_state: app_state.clone(), rate_limit_state: Some(rate_limit_state), handler_type: HandlerType::CreateDomain })
|
||||||
|
.route(Route::get("/domain/*"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::GetDomain })
|
||||||
|
.route(Route::post("/domain/*"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::CreateDomainRecord })
|
||||||
.route(Route::put("/domain/*"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::UpdateDomain })
|
.route(Route::put("/domain/*"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::UpdateDomain })
|
||||||
.route(Route::delete("/domain/*"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::DeleteDomain });
|
.route(Route::delete("/domain/*"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::DeleteDomain });
|
||||||
|
|
||||||
|
|||||||
@@ -77,10 +77,12 @@ pub(crate) struct ResponseDomain {
|
|||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub(crate) struct ResponseDnsRecord {
|
pub(crate) struct ResponseDnsRecord {
|
||||||
|
pub(crate) id: i32,
|
||||||
|
#[serde(rename = "type")]
|
||||||
pub(crate) record_type: String,
|
pub(crate) record_type: String,
|
||||||
pub(crate) name: String,
|
pub(crate) name: String,
|
||||||
pub(crate) value: String,
|
pub(crate) value: String,
|
||||||
pub(crate) ttl: Option<i32>,
|
pub(crate) ttl: i32,
|
||||||
pub(crate) priority: Option<i32>,
|
pub(crate) priority: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,3 +119,43 @@ pub(crate) struct UserDomainResponse {
|
|||||||
pub(crate) page: u32,
|
pub(crate) page: u32,
|
||||||
pub(crate) limit: u32,
|
pub(crate) limit: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(crate) struct CreateDnsRecord {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub(crate) record_type: String,
|
||||||
|
pub(crate) name: Option<String>,
|
||||||
|
pub(crate) value: String,
|
||||||
|
#[serde(deserialize_with = "deserialize_ttl")]
|
||||||
|
pub(crate) ttl: Option<i32>,
|
||||||
|
pub(crate) priority: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_ttl<'de, D>(deserializer: D) -> Result<Option<i32>, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
let value: Option<serde_json::Value> = Option::deserialize(deserializer)?;
|
||||||
|
match value {
|
||||||
|
Some(serde_json::Value::Number(n)) => {
|
||||||
|
if let Some(f) = n.as_f64() {
|
||||||
|
Ok(Some(f as i32))
|
||||||
|
} else if let Some(i) = n.as_i64() {
|
||||||
|
Ok(Some(i as i32))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(_) => Ok(None),
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub(crate) struct DomainDetail {
|
||||||
|
pub(crate) name: String,
|
||||||
|
pub(crate) tld: String,
|
||||||
|
pub(crate) status: Option<String>,
|
||||||
|
}
|
||||||
|
|||||||
@@ -103,31 +103,38 @@ pub(crate) async fn create_domain(ctx: &ServerContext, app_state: AppState, clai
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn get_domain(ctx: &ServerContext, app_state: AppState) -> Result<GurtResponse> {
|
pub(crate) async fn get_domain(ctx: &ServerContext, app_state: AppState, claims: Claims) -> Result<GurtResponse> {
|
||||||
let path_parts: Vec<&str> = ctx.path().split('/').collect();
|
let path_parts: Vec<&str> = ctx.path().split('/').collect();
|
||||||
if path_parts.len() < 4 {
|
if path_parts.len() < 3 {
|
||||||
return Ok(GurtResponse::bad_request().with_string_body("Invalid path format. Expected /domain/{name}/{tld}"));
|
return Ok(GurtResponse::bad_request().with_string_body("Invalid path format. Expected /domain/{domainName}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = path_parts[2];
|
let domain_name = path_parts[2];
|
||||||
let tld = path_parts[3];
|
|
||||||
|
let domain_parts: Vec<&str> = domain_name.split('.').collect();
|
||||||
|
if domain_parts.len() < 2 {
|
||||||
|
return Ok(GurtResponse::bad_request().with_string_body("Invalid domain format. Expected name.tld"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = domain_parts[0];
|
||||||
|
let tld = domain_parts[1];
|
||||||
|
|
||||||
let domain: Option<Domain> = sqlx::query_as::<_, Domain>(
|
let domain: Option<Domain> = sqlx::query_as::<_, Domain>(
|
||||||
"SELECT id, name, tld, ip, user_id, status, denial_reason, created_at FROM domains WHERE name = $1 AND tld = $2 AND status = 'approved'"
|
"SELECT id, name, tld, ip, user_id, status, denial_reason, created_at FROM domains WHERE name = $1 AND tld = $2 AND user_id = $3"
|
||||||
)
|
)
|
||||||
.bind(name)
|
.bind(name)
|
||||||
.bind(tld)
|
.bind(tld)
|
||||||
|
.bind(claims.user_id)
|
||||||
.fetch_optional(&app_state.db)
|
.fetch_optional(&app_state.db)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
||||||
|
|
||||||
match domain {
|
match domain {
|
||||||
Some(domain) => {
|
Some(domain) => {
|
||||||
let response_domain = ResponseDomain {
|
let response_domain = DomainDetail {
|
||||||
name: domain.name,
|
name: domain.name,
|
||||||
tld: domain.tld,
|
tld: domain.tld,
|
||||||
ip: domain.ip,
|
status: domain.status,
|
||||||
records: None, // TODO: Implement DNS records
|
|
||||||
};
|
};
|
||||||
Ok(GurtResponse::ok().with_json_body(&response_domain)?)
|
Ok(GurtResponse::ok().with_json_body(&response_domain)?)
|
||||||
}
|
}
|
||||||
@@ -357,6 +364,192 @@ pub(crate) async fn get_user_domains(ctx: &ServerContext, app_state: AppState, c
|
|||||||
Ok(GurtResponse::ok().with_json_body(&response)?)
|
Ok(GurtResponse::ok().with_json_body(&response)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn get_domain_records(ctx: &ServerContext, app_state: AppState, claims: Claims) -> Result<GurtResponse> {
|
||||||
|
let path_parts: Vec<&str> = ctx.path().split('/').collect();
|
||||||
|
if path_parts.len() < 4 {
|
||||||
|
return Ok(GurtResponse::bad_request().with_string_body("Invalid path format. Expected /domain/{domainName}/records"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let domain_name = path_parts[2];
|
||||||
|
|
||||||
|
let domain_parts: Vec<&str> = domain_name.split('.').collect();
|
||||||
|
if domain_parts.len() < 2 {
|
||||||
|
return Ok(GurtResponse::bad_request().with_string_body("Invalid domain format. Expected name.tld"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = domain_parts[0];
|
||||||
|
let tld = domain_parts[1];
|
||||||
|
|
||||||
|
let domain: Option<Domain> = sqlx::query_as::<_, Domain>(
|
||||||
|
"SELECT id, name, tld, ip, user_id, status, denial_reason, created_at FROM domains WHERE name = $1 AND tld = $2 AND user_id = $3"
|
||||||
|
)
|
||||||
|
.bind(name)
|
||||||
|
.bind(tld)
|
||||||
|
.bind(claims.user_id)
|
||||||
|
.fetch_optional(&app_state.db)
|
||||||
|
.await
|
||||||
|
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
||||||
|
|
||||||
|
let domain = match domain {
|
||||||
|
Some(d) => d,
|
||||||
|
None => return Ok(GurtResponse::not_found().with_string_body("Domain not found or access denied"))
|
||||||
|
};
|
||||||
|
|
||||||
|
let records: Vec<DnsRecord> = sqlx::query_as::<_, DnsRecord>(
|
||||||
|
"SELECT id, domain_id, record_type, name, value, ttl, priority, created_at FROM dns_records WHERE domain_id = $1 ORDER BY created_at ASC"
|
||||||
|
)
|
||||||
|
.bind(domain.id.unwrap())
|
||||||
|
.fetch_all(&app_state.db)
|
||||||
|
.await
|
||||||
|
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
||||||
|
|
||||||
|
let response_records: Vec<ResponseDnsRecord> = records.into_iter().map(|record| {
|
||||||
|
ResponseDnsRecord {
|
||||||
|
id: record.id.unwrap(),
|
||||||
|
record_type: record.record_type,
|
||||||
|
name: record.name,
|
||||||
|
value: record.value,
|
||||||
|
ttl: record.ttl.unwrap_or(3600),
|
||||||
|
priority: record.priority,
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
Ok(GurtResponse::ok().with_json_body(&response_records)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn create_domain_record(ctx: &ServerContext, app_state: AppState, claims: Claims) -> Result<GurtResponse> {
|
||||||
|
let path_parts: Vec<&str> = ctx.path().split('/').collect();
|
||||||
|
if path_parts.len() < 4 {
|
||||||
|
return Ok(GurtResponse::bad_request().with_string_body("Invalid path format. Expected /domain/{domainName}/records"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let domain_name = path_parts[2];
|
||||||
|
|
||||||
|
let domain_parts: Vec<&str> = domain_name.split('.').collect();
|
||||||
|
if domain_parts.len() < 2 {
|
||||||
|
return Ok(GurtResponse::bad_request().with_string_body("Invalid domain format. Expected name.tld"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = domain_parts[0];
|
||||||
|
let tld = domain_parts[1];
|
||||||
|
|
||||||
|
let domain: Option<Domain> = sqlx::query_as::<_, Domain>(
|
||||||
|
"SELECT id, name, tld, ip, user_id, status, denial_reason, created_at FROM domains WHERE name = $1 AND tld = $2 AND user_id = $3"
|
||||||
|
)
|
||||||
|
.bind(name)
|
||||||
|
.bind(tld)
|
||||||
|
.bind(claims.user_id)
|
||||||
|
.fetch_optional(&app_state.db)
|
||||||
|
.await
|
||||||
|
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
||||||
|
|
||||||
|
let domain = match domain {
|
||||||
|
Some(d) => d,
|
||||||
|
None => return Ok(GurtResponse::not_found().with_string_body("Domain not found or access denied"))
|
||||||
|
};
|
||||||
|
|
||||||
|
let record_data: CreateDnsRecord = {
|
||||||
|
let body_bytes = ctx.body();
|
||||||
|
let body_str = std::str::from_utf8(body_bytes).unwrap_or("<invalid utf8>");
|
||||||
|
log::info!("Received JSON body: {}", body_str);
|
||||||
|
|
||||||
|
serde_json::from_slice(body_bytes)
|
||||||
|
.map_err(|e| {
|
||||||
|
log::error!("JSON parsing error: {} for body: {}", e, body_str);
|
||||||
|
GurtError::invalid_message("Invalid JSON")
|
||||||
|
})?
|
||||||
|
};
|
||||||
|
|
||||||
|
if record_data.record_type.is_empty() {
|
||||||
|
return Ok(GurtResponse::bad_request().with_string_body("Record type is required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let valid_types = ["A", "AAAA", "CNAME", "TXT", "MX", "NS", "SRV"];
|
||||||
|
if !valid_types.contains(&record_data.record_type.as_str()) {
|
||||||
|
return Ok(GurtResponse::bad_request().with_string_body("Invalid record type"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let record_name = record_data.name.unwrap_or_else(|| "@".to_string());
|
||||||
|
let ttl = record_data.ttl.unwrap_or(3600);
|
||||||
|
|
||||||
|
let record_id: (i32,) = sqlx::query_as(
|
||||||
|
"INSERT INTO dns_records (domain_id, record_type, name, value, ttl, priority) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id"
|
||||||
|
)
|
||||||
|
.bind(domain.id.unwrap())
|
||||||
|
.bind(&record_data.record_type)
|
||||||
|
.bind(&record_name)
|
||||||
|
.bind(&record_data.value)
|
||||||
|
.bind(ttl)
|
||||||
|
.bind(record_data.priority)
|
||||||
|
.fetch_one(&app_state.db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
log::error!("Failed to create DNS record: {}", e);
|
||||||
|
GurtError::invalid_message("Failed to create DNS record")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let response_record = ResponseDnsRecord {
|
||||||
|
id: record_id.0,
|
||||||
|
record_type: record_data.record_type,
|
||||||
|
name: record_name,
|
||||||
|
value: record_data.value,
|
||||||
|
ttl,
|
||||||
|
priority: record_data.priority,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(GurtResponse::ok().with_json_body(&response_record)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn delete_domain_record(ctx: &ServerContext, app_state: AppState, claims: Claims) -> Result<GurtResponse> {
|
||||||
|
let path_parts: Vec<&str> = ctx.path().split('/').collect();
|
||||||
|
if path_parts.len() < 5 {
|
||||||
|
return Ok(GurtResponse::bad_request().with_string_body("Invalid path format. Expected /domain/{domainName}/records/{recordId}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let domain_name = path_parts[2];
|
||||||
|
let record_id_str = path_parts[4];
|
||||||
|
|
||||||
|
let record_id: i32 = record_id_str.parse()
|
||||||
|
.map_err(|_| GurtError::invalid_message("Invalid record ID"))?;
|
||||||
|
|
||||||
|
let domain_parts: Vec<&str> = domain_name.split('.').collect();
|
||||||
|
if domain_parts.len() < 2 {
|
||||||
|
return Ok(GurtResponse::bad_request().with_string_body("Invalid domain format. Expected name.tld"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = domain_parts[0];
|
||||||
|
let tld = domain_parts[1];
|
||||||
|
|
||||||
|
let domain: Option<Domain> = sqlx::query_as::<_, Domain>(
|
||||||
|
"SELECT id, name, tld, ip, user_id, status, denial_reason, created_at FROM domains WHERE name = $1 AND tld = $2 AND user_id = $3"
|
||||||
|
)
|
||||||
|
.bind(name)
|
||||||
|
.bind(tld)
|
||||||
|
.bind(claims.user_id)
|
||||||
|
.fetch_optional(&app_state.db)
|
||||||
|
.await
|
||||||
|
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
||||||
|
|
||||||
|
let domain = match domain {
|
||||||
|
Some(d) => d,
|
||||||
|
None => return Ok(GurtResponse::not_found().with_string_body("Domain not found or access denied"))
|
||||||
|
};
|
||||||
|
|
||||||
|
let rows_affected = sqlx::query("DELETE FROM dns_records WHERE id = $1 AND domain_id = $2")
|
||||||
|
.bind(record_id)
|
||||||
|
.bind(domain.id.unwrap())
|
||||||
|
.execute(&app_state.db)
|
||||||
|
.await
|
||||||
|
.map_err(|_| GurtError::invalid_message("Database error"))?
|
||||||
|
.rows_affected();
|
||||||
|
|
||||||
|
if rows_affected == 0 {
|
||||||
|
return Ok(GurtResponse::not_found().with_string_body("DNS record not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(GurtResponse::ok().with_string_body("DNS record deleted successfully"))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
struct Error {
|
struct Error {
|
||||||
msg: &'static str,
|
msg: &'static str,
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ button_group = SubResource("ButtonGroup_06us3")
|
|||||||
flat = true
|
flat = true
|
||||||
|
|
||||||
[node name="ColorPickerButton" type="ColorPickerButton" parent="."]
|
[node name="ColorPickerButton" type="ColorPickerButton" parent="."]
|
||||||
|
visible = false
|
||||||
|
custom_minimum_size = Vector2(0, 32)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
toggle_mode = false
|
toggle_mode = false
|
||||||
|
|
||||||
@@ -107,13 +109,13 @@ theme = ExtResource("2_theme")
|
|||||||
value = 50.0
|
value = 50.0
|
||||||
|
|
||||||
[node name="SpinBox" type="SpinBox" parent="."]
|
[node name="SpinBox" type="SpinBox" parent="."]
|
||||||
visible = false
|
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme = SubResource("Theme_poopw")
|
theme = SubResource("Theme_poopw")
|
||||||
min_value = -99999.0
|
min_value = -99999.0
|
||||||
max_value = 99999.0
|
max_value = 99999.0
|
||||||
|
|
||||||
[node name="FileContainer" type="HBoxContainer" parent="."]
|
[node name="FileContainer" type="HBoxContainer" parent="."]
|
||||||
|
visible = false
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
|
|
||||||
[node name="FileButton" type="Button" parent="FileContainer"]
|
[node name="FileButton" type="Button" parent="FileContainer"]
|
||||||
|
|||||||
@@ -3,18 +3,12 @@
|
|||||||
[ext_resource type="Script" uid="uid://bmu8q4rm1wopd" path="res://Scripts/Tags/select.gd" id="1_select"]
|
[ext_resource type="Script" uid="uid://bmu8q4rm1wopd" path="res://Scripts/Tags/select.gd" id="1_select"]
|
||||||
[ext_resource type="Theme" uid="uid://bn6rbmdy60lhr" path="res://Scenes/Styles/BrowserText.tres" id="2_xq313"]
|
[ext_resource type="Theme" uid="uid://bn6rbmdy60lhr" path="res://Scenes/Styles/BrowserText.tres" id="2_xq313"]
|
||||||
|
|
||||||
[node name="select" type="Control"]
|
[node name="VBoxContainer" type="VBoxContainer"]
|
||||||
layout_mode = 3
|
offset_right = 40.0
|
||||||
anchors_preset = 10
|
offset_bottom = 40.0
|
||||||
anchor_right = 1.0
|
|
||||||
grow_horizontal = 2
|
|
||||||
size_flags_horizontal = 3
|
|
||||||
size_flags_vertical = 0
|
|
||||||
script = ExtResource("1_select")
|
script = ExtResource("1_select")
|
||||||
|
|
||||||
[node name="OptionButton" type="OptionButton" parent="."]
|
[node name="OptionButton" type="OptionButton" parent="."]
|
||||||
layout_mode = 0
|
layout_mode = 2
|
||||||
offset_right = 161.0
|
|
||||||
offset_bottom = 32.0
|
|
||||||
mouse_default_cursor_shape = 2
|
mouse_default_cursor_shape = 2
|
||||||
theme = ExtResource("2_xq313")
|
theme = ExtResource("2_xq313")
|
||||||
|
|||||||
@@ -585,6 +585,13 @@ static func parse_utility_class_internal(rule: CSSRule, utility_name: String) ->
|
|||||||
rule.properties["display"] = "inline-flex"
|
rule.properties["display"] = "inline-flex"
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if utility_name == "grid":
|
||||||
|
rule.properties["display"] = "grid"
|
||||||
|
return
|
||||||
|
if utility_name == "inline-grid":
|
||||||
|
rule.properties["display"] = "inline-grid"
|
||||||
|
return
|
||||||
|
|
||||||
# Flex direction
|
# Flex direction
|
||||||
match utility_name:
|
match utility_name:
|
||||||
"flex-row": rule.properties["flex-direction"] = "row"; return
|
"flex-row": rule.properties["flex-direction"] = "row"; return
|
||||||
@@ -638,6 +645,52 @@ static func parse_utility_class_internal(rule: CSSRule, utility_name: String) ->
|
|||||||
rule.properties["column-gap"] = SizeUtils.parse_size(val)
|
rule.properties["column-gap"] = SizeUtils.parse_size(val)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if utility_name.begins_with("grid-cols-"):
|
||||||
|
var val = utility_name.substr(10)
|
||||||
|
if val.is_valid_int():
|
||||||
|
rule.properties["grid-template-columns"] = val.to_int()
|
||||||
|
else:
|
||||||
|
rule.properties["grid-template-columns"] = val
|
||||||
|
return
|
||||||
|
if utility_name.begins_with("grid-rows-"):
|
||||||
|
var val = utility_name.substr(10)
|
||||||
|
if val.is_valid_int():
|
||||||
|
rule.properties["grid-template-rows"] = val.to_int()
|
||||||
|
else:
|
||||||
|
rule.properties["grid-template-rows"] = val
|
||||||
|
return
|
||||||
|
|
||||||
|
if utility_name.begins_with("col-span-"):
|
||||||
|
var val = utility_name.substr(9)
|
||||||
|
if val == "full":
|
||||||
|
rule.properties["grid-column"] = "1 / -1"
|
||||||
|
elif val.is_valid_int():
|
||||||
|
rule.properties["grid-column"] = "span " + val
|
||||||
|
return
|
||||||
|
if utility_name.begins_with("row-span-"):
|
||||||
|
var val = utility_name.substr(9)
|
||||||
|
if val == "full":
|
||||||
|
rule.properties["grid-row"] = "1 / -1"
|
||||||
|
elif val.is_valid_int():
|
||||||
|
rule.properties["grid-row"] = "span " + val
|
||||||
|
return
|
||||||
|
|
||||||
|
match utility_name:
|
||||||
|
"grid-cols-1": rule.properties["grid-template-columns"] = 1; return
|
||||||
|
"grid-cols-2": rule.properties["grid-template-columns"] = 2; return
|
||||||
|
"grid-cols-3": rule.properties["grid-template-columns"] = 3; return
|
||||||
|
"grid-cols-4": rule.properties["grid-template-columns"] = 4; return
|
||||||
|
"grid-cols-5": rule.properties["grid-template-columns"] = 5; return
|
||||||
|
"grid-cols-6": rule.properties["grid-template-columns"] = 6; return
|
||||||
|
"grid-cols-12": rule.properties["grid-template-columns"] = 12; return
|
||||||
|
"col-span-1": rule.properties["grid-column"] = "span 1"; return
|
||||||
|
"col-span-2": rule.properties["grid-column"] = "span 2"; return
|
||||||
|
"col-span-3": rule.properties["grid-column"] = "span 3"; return
|
||||||
|
"col-span-4": rule.properties["grid-column"] = "span 4"; return
|
||||||
|
"col-span-5": rule.properties["grid-column"] = "span 5"; return
|
||||||
|
"col-span-6": rule.properties["grid-column"] = "span 6"; return
|
||||||
|
"col-span-full": rule.properties["grid-column"] = "1 / -1"; return
|
||||||
|
|
||||||
# FLEX ITEM PROPERTIES
|
# FLEX ITEM PROPERTIES
|
||||||
if utility_name.begins_with("flex-grow-"):
|
if utility_name.begins_with("flex-grow-"):
|
||||||
var val = utility_name.substr(10)
|
var val = utility_name.substr(10)
|
||||||
|
|||||||
@@ -173,6 +173,76 @@ func _gurt_location_get_href_handler(vm: LuauVM) -> int:
|
|||||||
vm.lua_pushstring("")
|
vm.lua_pushstring("")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
func _gurt_location_query_get_handler(vm: LuauVM) -> int:
|
||||||
|
var key: String = vm.luaL_checkstring(1)
|
||||||
|
var query_params = get_current_query_params()
|
||||||
|
|
||||||
|
if query_params.has(key):
|
||||||
|
vm.lua_pushstring(query_params[key])
|
||||||
|
else:
|
||||||
|
vm.lua_pushnil()
|
||||||
|
return 1
|
||||||
|
|
||||||
|
func _gurt_location_query_has_handler(vm: LuauVM) -> int:
|
||||||
|
var key: String = vm.luaL_checkstring(1)
|
||||||
|
var query_params = get_current_query_params()
|
||||||
|
|
||||||
|
vm.lua_pushboolean(query_params.has(key))
|
||||||
|
return 1
|
||||||
|
|
||||||
|
func _gurt_location_query_getAll_handler(vm: LuauVM) -> int:
|
||||||
|
var key: String = vm.luaL_checkstring(1)
|
||||||
|
var query_params = get_current_query_params()
|
||||||
|
|
||||||
|
vm.lua_newtable()
|
||||||
|
|
||||||
|
if query_params.has(key):
|
||||||
|
var value = query_params[key]
|
||||||
|
if value is Array:
|
||||||
|
for i in range(value.size()):
|
||||||
|
vm.lua_pushstring(str(value[i]))
|
||||||
|
vm.lua_rawseti(-2, i + 1)
|
||||||
|
else:
|
||||||
|
vm.lua_pushstring(str(value))
|
||||||
|
vm.lua_rawseti(-2, 1)
|
||||||
|
|
||||||
|
return 1
|
||||||
|
|
||||||
|
func get_current_query_params() -> Dictionary:
|
||||||
|
var main_node = Engine.get_main_loop().current_scene
|
||||||
|
var current_url = ""
|
||||||
|
|
||||||
|
if main_node and main_node.has_method("get_current_url"):
|
||||||
|
current_url = main_node.get_current_url()
|
||||||
|
elif main_node and main_node.has_property("current_domain"):
|
||||||
|
current_url = main_node.current_domain
|
||||||
|
|
||||||
|
var query_params = {}
|
||||||
|
|
||||||
|
if "?" in current_url:
|
||||||
|
var query_string = current_url.split("?")[1]
|
||||||
|
if "#" in query_string:
|
||||||
|
query_string = query_string.split("#")[0]
|
||||||
|
|
||||||
|
for param in query_string.split("&"):
|
||||||
|
if "=" in param:
|
||||||
|
var key_value = param.split("=", false, 1)
|
||||||
|
var key = key_value[0].uri_decode()
|
||||||
|
var value = key_value[1].uri_decode() if key_value.size() > 1 else ""
|
||||||
|
|
||||||
|
if query_params.has(key):
|
||||||
|
if query_params[key] is Array:
|
||||||
|
query_params[key].append(value)
|
||||||
|
else:
|
||||||
|
query_params[key] = [query_params[key], value]
|
||||||
|
else:
|
||||||
|
query_params[key] = value
|
||||||
|
else:
|
||||||
|
var key = param.uri_decode()
|
||||||
|
query_params[key] = ""
|
||||||
|
|
||||||
|
return query_params
|
||||||
|
|
||||||
func _reload_current_page():
|
func _reload_current_page():
|
||||||
var main_node = Engine.get_main_loop().current_scene
|
var main_node = Engine.get_main_loop().current_scene
|
||||||
if main_node and main_node.has_method("reload_current_page"):
|
if main_node and main_node.has_method("reload_current_page"):
|
||||||
@@ -660,6 +730,12 @@ func _handle_text_setting(operation: Dictionary):
|
|||||||
var element_id = get_or_assign_element_id(element)
|
var element_id = get_or_assign_element_id(element)
|
||||||
var dom_node = dom_parser.parse_result.dom_nodes.get(element_id, null)
|
var dom_node = dom_parser.parse_result.dom_nodes.get(element_id, null)
|
||||||
if dom_node:
|
if dom_node:
|
||||||
|
if element.tag_name == "button":
|
||||||
|
var button_node = dom_node.get_node_or_null("ButtonNode")
|
||||||
|
if button_node and button_node is Button:
|
||||||
|
button_node.text = text
|
||||||
|
return
|
||||||
|
|
||||||
var text_node = get_dom_node(dom_node, "text")
|
var text_node = get_dom_node(dom_node, "text")
|
||||||
if text_node:
|
if text_node:
|
||||||
if text_node is RichTextLabel:
|
if text_node is RichTextLabel:
|
||||||
|
|||||||
@@ -24,8 +24,11 @@ pre { text-xl font-mono }
|
|||||||
button { text-[16px] bg-[#1b1b1b] rounded-md text-white hover:bg-[#2a2a2a] active:bg-[#101010] px-3 py-1.5 }
|
button { text-[16px] bg-[#1b1b1b] rounded-md text-white hover:bg-[#2a2a2a] active:bg-[#101010] px-3 py-1.5 }
|
||||||
button[disabled] { bg-[#666666] text-[#999999] cursor-not-allowed }
|
button[disabled] { bg-[#666666] text-[#999999] cursor-not-allowed }
|
||||||
|
|
||||||
input[type="text"] { text-[#000000] border border-[#000000] rounded-[3px] bg-transparent px-3 py-1.5 }
|
input { text-[#000000] border border-[#000000] rounded-[3px] bg-transparent px-3 py-1.5 }
|
||||||
input[type="text"]:active { border-[3px] border-[#000000] }
|
input:active { border-[3px] border-[#000000] }
|
||||||
|
|
||||||
|
select { text-[#000000] border border-[#000000] rounded-[3px] bg-transparent px-3 py-1.5 }
|
||||||
|
select:active { text-[#000000] border-[3px] border-[#000000] }
|
||||||
"""
|
"""
|
||||||
|
|
||||||
var HTML_CONTENT = """
|
var HTML_CONTENT = """
|
||||||
|
|||||||
@@ -197,16 +197,40 @@ func _on_text_changed(new_text: String, minlength: String, pattern: String) -> v
|
|||||||
is_valid = false
|
is_valid = false
|
||||||
|
|
||||||
if is_valid:
|
if is_valid:
|
||||||
line_edit.remove_theme_stylebox_override("normal")
|
if line_edit.has_focus():
|
||||||
line_edit.remove_theme_stylebox_override("focus")
|
apply_active_styles()
|
||||||
line_edit.modulate = Color.WHITE
|
|
||||||
else:
|
else:
|
||||||
var normal_style = create_red_border_style_from_theme(line_edit, "normal")
|
apply_normal_styles()
|
||||||
var focus_style = create_red_border_style_from_theme(line_edit, "focus")
|
else:
|
||||||
|
var normal_style = get_current_or_create_style(line_edit, "normal")
|
||||||
|
var focus_style = get_current_or_create_style(line_edit, "focus")
|
||||||
|
|
||||||
|
normal_style.border_color = Color.RED
|
||||||
|
normal_style.border_width_left = max(normal_style.border_width_left, 2)
|
||||||
|
normal_style.border_width_right = max(normal_style.border_width_right, 2)
|
||||||
|
normal_style.border_width_top = max(normal_style.border_width_top, 2)
|
||||||
|
normal_style.border_width_bottom = max(normal_style.border_width_bottom, 2)
|
||||||
|
|
||||||
|
focus_style.border_color = Color.RED
|
||||||
|
focus_style.border_width_left = max(focus_style.border_width_left, 2)
|
||||||
|
focus_style.border_width_right = max(focus_style.border_width_right, 2)
|
||||||
|
focus_style.border_width_top = max(focus_style.border_width_top, 2)
|
||||||
|
focus_style.border_width_bottom = max(focus_style.border_width_bottom, 2)
|
||||||
|
|
||||||
line_edit.add_theme_stylebox_override("normal", normal_style)
|
line_edit.add_theme_stylebox_override("normal", normal_style)
|
||||||
line_edit.add_theme_stylebox_override("focus", focus_style)
|
line_edit.add_theme_stylebox_override("focus", focus_style)
|
||||||
line_edit.modulate = Color.WHITE
|
|
||||||
|
func get_current_or_create_style(line_edit: LineEdit, style_name: String) -> StyleBoxFlat:
|
||||||
|
if line_edit.has_theme_stylebox_override(style_name):
|
||||||
|
var current_style = line_edit.get_theme_stylebox(style_name)
|
||||||
|
if current_style is StyleBoxFlat:
|
||||||
|
return (current_style as StyleBoxFlat).duplicate()
|
||||||
|
|
||||||
|
var theme_style = line_edit.get_theme_stylebox(style_name)
|
||||||
|
if theme_style is StyleBoxFlat:
|
||||||
|
return (theme_style as StyleBoxFlat).duplicate()
|
||||||
|
|
||||||
|
return StyleBoxFlat.new()
|
||||||
|
|
||||||
func create_red_border_style_from_theme(line_edit: LineEdit, style_name: String) -> StyleBoxFlat:
|
func create_red_border_style_from_theme(line_edit: LineEdit, style_name: String) -> StyleBoxFlat:
|
||||||
var original_style: StyleBoxFlat = line_edit.get_theme_stylebox(style_name)
|
var original_style: StyleBoxFlat = line_edit.get_theme_stylebox(style_name)
|
||||||
@@ -393,7 +417,6 @@ func apply_input_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
|
|||||||
|
|
||||||
var styles = parser.get_element_styles_with_inheritance(element, "", [])
|
var styles = parser.get_element_styles_with_inheritance(element, "", [])
|
||||||
|
|
||||||
|
|
||||||
# Apply text color to the active input control
|
# Apply text color to the active input control
|
||||||
var active_child = null
|
var active_child = null
|
||||||
for child in get_children():
|
for child in get_children():
|
||||||
@@ -422,21 +445,15 @@ func apply_input_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
|
|||||||
line_edit.add_theme_color_override("font_placeholder_color", placeholder_color)
|
line_edit.add_theme_color_override("font_placeholder_color", placeholder_color)
|
||||||
|
|
||||||
# Apply stylebox for borders, background, padding, etc.
|
# Apply stylebox for borders, background, padding, etc.
|
||||||
if BackgroundUtils.needs_background_wrapper(styles):
|
if BackgroundUtils.needs_background_wrapper(styles) or active_child is SpinBox:
|
||||||
apply_stylebox_to_input(active_child, styles)
|
apply_stylebox_to_input(active_child, styles)
|
||||||
|
|
||||||
var width = null
|
var width = null
|
||||||
var height = null
|
var height = null
|
||||||
|
|
||||||
if styles.has("width"):
|
if styles.has("width"):
|
||||||
if styles["width"] == "full":
|
|
||||||
var parent_styles = parser.get_element_styles_with_inheritance(element.parent, "", []) if element.parent else {}
|
|
||||||
if parent_styles.has("width"):
|
|
||||||
var parent_width = SizingUtils.parse_size_value(parent_styles["width"])
|
|
||||||
if parent_width:
|
|
||||||
width = parent_width
|
|
||||||
else:
|
|
||||||
width = SizingUtils.parse_size_value(styles["width"])
|
width = SizingUtils.parse_size_value(styles["width"])
|
||||||
|
|
||||||
if styles.has("height"):
|
if styles.has("height"):
|
||||||
height = SizingUtils.parse_size_value(styles["height"])
|
height = SizingUtils.parse_size_value(styles["height"])
|
||||||
|
|
||||||
@@ -447,7 +464,11 @@ func apply_input_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
|
|||||||
var new_height = max(active_child.custom_minimum_size.y, active_child.size.y)
|
var new_height = max(active_child.custom_minimum_size.y, active_child.size.y)
|
||||||
|
|
||||||
if width:
|
if width:
|
||||||
if SizingUtils.is_percentage(width):
|
if width == "100%":
|
||||||
|
active_child.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
new_width = 0
|
||||||
|
elif SizingUtils.is_percentage(width):
|
||||||
new_width = SizingUtils.calculate_percentage_size(width, SizingUtils.DEFAULT_VIEWPORT_WIDTH)
|
new_width = SizingUtils.calculate_percentage_size(width, SizingUtils.DEFAULT_VIEWPORT_WIDTH)
|
||||||
else:
|
else:
|
||||||
new_width = width
|
new_width = width
|
||||||
@@ -462,7 +483,7 @@ func apply_input_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
|
|||||||
|
|
||||||
active_child.custom_minimum_size = new_child_size
|
active_child.custom_minimum_size = new_child_size
|
||||||
|
|
||||||
if width:
|
if width and width != "100%":
|
||||||
active_child.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
active_child.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||||
if height:
|
if height:
|
||||||
active_child.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
active_child.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
||||||
@@ -473,7 +494,7 @@ func apply_input_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
|
|||||||
custom_minimum_size = new_child_size
|
custom_minimum_size = new_child_size
|
||||||
|
|
||||||
# Root Control adjusts size flags to match child
|
# Root Control adjusts size flags to match child
|
||||||
if width:
|
if width and width != "100%":
|
||||||
size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||||
else:
|
else:
|
||||||
size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
@@ -526,9 +547,10 @@ func apply_active_styles() -> void:
|
|||||||
if not active_child:
|
if not active_child:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Apply merged styles
|
|
||||||
if BackgroundUtils.needs_background_wrapper(merged_styles):
|
if BackgroundUtils.needs_background_wrapper(merged_styles):
|
||||||
apply_stylebox_to_input(active_child, merged_styles)
|
apply_stylebox_to_input(active_child, merged_styles)
|
||||||
|
elif active_child is SpinBox:
|
||||||
|
apply_stylebox_to_input(active_child, merged_styles)
|
||||||
|
|
||||||
func apply_normal_styles() -> void:
|
func apply_normal_styles() -> void:
|
||||||
if not _element or not _parser:
|
if not _element or not _parser:
|
||||||
@@ -536,7 +558,6 @@ func apply_normal_styles() -> void:
|
|||||||
|
|
||||||
var normal_styles = _parser.get_element_styles_with_inheritance(_element, "", [])
|
var normal_styles = _parser.get_element_styles_with_inheritance(_element, "", [])
|
||||||
|
|
||||||
# Find the active input control
|
|
||||||
var active_child = null
|
var active_child = null
|
||||||
for child in get_children():
|
for child in get_children():
|
||||||
if child.visible:
|
if child.visible:
|
||||||
@@ -546,25 +567,27 @@ func apply_normal_styles() -> void:
|
|||||||
if not active_child:
|
if not active_child:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Apply normal border styles
|
|
||||||
if BackgroundUtils.needs_background_wrapper(normal_styles):
|
if BackgroundUtils.needs_background_wrapper(normal_styles):
|
||||||
apply_stylebox_to_input(active_child, normal_styles)
|
apply_stylebox_to_input(active_child, normal_styles)
|
||||||
|
elif active_child is SpinBox:
|
||||||
|
apply_stylebox_to_input(active_child, normal_styles)
|
||||||
else:
|
else:
|
||||||
# Remove style overrides to use default theme
|
|
||||||
if active_child is LineEdit:
|
if active_child is LineEdit:
|
||||||
active_child.remove_theme_stylebox_override("normal")
|
active_child.remove_theme_stylebox_override("normal")
|
||||||
active_child.remove_theme_stylebox_override("focus")
|
active_child.remove_theme_stylebox_override("focus")
|
||||||
elif active_child is SpinBox:
|
elif active_child is SpinBox:
|
||||||
active_child.remove_theme_stylebox_override("normal")
|
active_child.remove_theme_stylebox_override("normal")
|
||||||
active_child.remove_theme_stylebox_override("focus")
|
active_child.remove_theme_stylebox_override("updown")
|
||||||
|
var line_edit = active_child.get_line_edit()
|
||||||
|
if line_edit:
|
||||||
|
line_edit.remove_theme_stylebox_override("normal")
|
||||||
|
line_edit.remove_theme_stylebox_override("focus")
|
||||||
elif active_child is Button:
|
elif active_child is Button:
|
||||||
active_child.remove_theme_stylebox_override("normal")
|
active_child.remove_theme_stylebox_override("normal")
|
||||||
|
|
||||||
func apply_stylebox_to_input(control: Control, styles: Dictionary) -> void:
|
func apply_stylebox_to_input(control: Control, styles: Dictionary) -> void:
|
||||||
var style_box = BackgroundUtils.create_stylebox_from_styles(styles)
|
var style_box = BackgroundUtils.create_stylebox_from_styles(styles)
|
||||||
|
|
||||||
# Set appropriate content margins for inputs if no padding is specified
|
|
||||||
# Check for all possible padding-related styles
|
|
||||||
var has_left_padding = styles.has("padding") or styles.has("padding-left")
|
var has_left_padding = styles.has("padding") or styles.has("padding-left")
|
||||||
var has_right_padding = styles.has("padding") or styles.has("padding-right")
|
var has_right_padding = styles.has("padding") or styles.has("padding-right")
|
||||||
var has_top_padding = styles.has("padding") or styles.has("padding-top")
|
var has_top_padding = styles.has("padding") or styles.has("padding-top")
|
||||||
@@ -580,13 +603,17 @@ func apply_stylebox_to_input(control: Control, styles: Dictionary) -> void:
|
|||||||
if not has_bottom_padding:
|
if not has_bottom_padding:
|
||||||
style_box.content_margin_bottom = 2.0
|
style_box.content_margin_bottom = 2.0
|
||||||
|
|
||||||
|
|
||||||
# Apply the style to the appropriate states
|
|
||||||
if control is LineEdit:
|
if control is LineEdit:
|
||||||
control.add_theme_stylebox_override("normal", style_box)
|
control.add_theme_stylebox_override("normal", style_box)
|
||||||
control.add_theme_stylebox_override("focus", style_box)
|
control.add_theme_stylebox_override("focus", style_box)
|
||||||
elif control is SpinBox:
|
elif control is SpinBox:
|
||||||
control.add_theme_stylebox_override("normal", style_box)
|
# NOTE: currently broken, it goes over the buttons, dont have time to fix
|
||||||
control.add_theme_stylebox_override("focus", style_box)
|
#style_box.expand_margin_right += 32.0 # More space for stepper buttons
|
||||||
|
|
||||||
|
var line_edit = control.get_line_edit()
|
||||||
|
if line_edit:
|
||||||
|
line_edit.add_theme_stylebox_override("normal", style_box)
|
||||||
|
line_edit.add_theme_stylebox_override("focus", style_box)
|
||||||
|
|
||||||
elif control is Button:
|
elif control is Button:
|
||||||
control.add_theme_stylebox_override("normal", style_box)
|
control.add_theme_stylebox_override("normal", style_box)
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ func _get_font_weight_multiplier() -> float:
|
|||||||
elif element_styles.has("font-extrabold"):
|
elif element_styles.has("font-extrabold"):
|
||||||
return 1.10
|
return 1.10
|
||||||
elif element_styles.has("font-bold"):
|
elif element_styles.has("font-bold"):
|
||||||
return 1.08
|
return 1.0
|
||||||
elif element_styles.has("font-semibold"):
|
elif element_styles.has("font-semibold"):
|
||||||
return 1.06
|
return 1.06
|
||||||
elif element_styles.has("font-medium"):
|
elif element_styles.has("font-medium"):
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ extends Control
|
|||||||
|
|
||||||
const BROWSER_TEXT = preload("res://Scenes/Styles/BrowserText.tres")
|
const BROWSER_TEXT = preload("res://Scenes/Styles/BrowserText.tres")
|
||||||
|
|
||||||
|
var _element: HTMLParser.HTMLElement
|
||||||
|
var _parser: HTMLParser
|
||||||
|
|
||||||
func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
|
func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
|
||||||
|
_element = element
|
||||||
|
_parser = parser
|
||||||
var option_button: OptionButton = $OptionButton
|
var option_button: OptionButton = $OptionButton
|
||||||
|
|
||||||
var selected_index = -1
|
var selected_index = -1
|
||||||
@@ -36,4 +41,87 @@ func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
|
|||||||
|
|
||||||
custom_minimum_size = option_button.size
|
custom_minimum_size = option_button.size
|
||||||
|
|
||||||
|
apply_select_styles(element, parser)
|
||||||
|
|
||||||
parser.register_dom_node(element, option_button)
|
parser.register_dom_node(element, option_button)
|
||||||
|
|
||||||
|
|
||||||
|
func apply_select_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
|
||||||
|
if not element or not parser:
|
||||||
|
return
|
||||||
|
|
||||||
|
StyleManager.apply_element_styles(self, element, parser)
|
||||||
|
|
||||||
|
var normal_styles = parser.get_element_styles_with_inheritance(element, "", [])
|
||||||
|
var hover_styles = parser.get_element_styles_with_inheritance(element, "hover", [])
|
||||||
|
var active_styles = parser.get_element_styles_with_inheritance(element, "active", [])
|
||||||
|
|
||||||
|
var option_button: OptionButton = $OptionButton
|
||||||
|
|
||||||
|
apply_select_text_colors(option_button, normal_styles, hover_styles, active_styles)
|
||||||
|
apply_select_background_styles(option_button, normal_styles, hover_styles, active_styles)
|
||||||
|
|
||||||
|
if normal_styles.has("width"):
|
||||||
|
if normal_styles["width"] == "100%":
|
||||||
|
size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
option_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
else:
|
||||||
|
var width = StyleManager.parse_size(normal_styles["width"])
|
||||||
|
if width:
|
||||||
|
custom_minimum_size.x = width
|
||||||
|
option_button.custom_minimum_size.x = width
|
||||||
|
size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||||
|
option_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||||
|
|
||||||
|
func apply_select_text_colors(option_button: OptionButton, normal_styles: Dictionary, hover_styles: Dictionary, active_styles: Dictionary) -> void:
|
||||||
|
var normal_color = normal_styles.get("color", Color.WHITE)
|
||||||
|
var hover_color = hover_styles.get("color", normal_color)
|
||||||
|
var active_color = active_styles.get("color", hover_color)
|
||||||
|
|
||||||
|
option_button.add_theme_color_override("font_color", normal_color)
|
||||||
|
option_button.add_theme_color_override("font_hover_color", hover_color)
|
||||||
|
option_button.add_theme_color_override("font_pressed_color", active_color)
|
||||||
|
option_button.add_theme_color_override("font_hover_pressed_color", active_color)
|
||||||
|
option_button.add_theme_color_override("font_focus_color", normal_color)
|
||||||
|
|
||||||
|
func apply_select_background_styles(option_button: OptionButton, normal_styles: Dictionary, hover_styles: Dictionary, active_styles: Dictionary) -> void:
|
||||||
|
var normal_merged = normal_styles.duplicate()
|
||||||
|
var hover_merged = normal_styles.duplicate()
|
||||||
|
var active_merged = normal_styles.duplicate()
|
||||||
|
|
||||||
|
for key in hover_styles:
|
||||||
|
hover_merged[key] = hover_styles[key]
|
||||||
|
|
||||||
|
for key in active_styles:
|
||||||
|
active_merged[key] = active_styles[key]
|
||||||
|
|
||||||
|
if BackgroundUtils.needs_background_wrapper(normal_merged):
|
||||||
|
var normal_stylebox = create_select_stylebox(normal_merged)
|
||||||
|
option_button.add_theme_stylebox_override("normal", normal_stylebox)
|
||||||
|
|
||||||
|
if BackgroundUtils.needs_background_wrapper(hover_merged):
|
||||||
|
var hover_stylebox = create_select_stylebox(hover_merged)
|
||||||
|
option_button.add_theme_stylebox_override("hover", hover_stylebox)
|
||||||
|
|
||||||
|
if BackgroundUtils.needs_background_wrapper(active_merged):
|
||||||
|
var active_stylebox = create_select_stylebox(active_merged)
|
||||||
|
option_button.add_theme_stylebox_override("pressed", active_stylebox)
|
||||||
|
|
||||||
|
func create_select_stylebox(styles: Dictionary) -> StyleBoxFlat:
|
||||||
|
var style_box = BackgroundUtils.create_stylebox_from_styles(styles)
|
||||||
|
|
||||||
|
var has_left_padding = styles.has("padding") or styles.has("padding-left")
|
||||||
|
var has_right_padding = styles.has("padding") or styles.has("padding-right")
|
||||||
|
var has_top_padding = styles.has("padding") or styles.has("padding-top")
|
||||||
|
var has_bottom_padding = styles.has("padding") or styles.has("padding-bottom")
|
||||||
|
|
||||||
|
if not has_left_padding:
|
||||||
|
style_box.content_margin_left = 5.0
|
||||||
|
if not has_right_padding:
|
||||||
|
style_box.content_margin_right = 20.0 # More space for dropdown arrow
|
||||||
|
if not has_top_padding:
|
||||||
|
style_box.content_margin_top = 2.0
|
||||||
|
if not has_bottom_padding:
|
||||||
|
style_box.content_margin_bottom = 2.0
|
||||||
|
|
||||||
|
return style_box
|
||||||
|
|||||||
84
flumi/Scripts/Utils/GridUtils.gd
Normal file
84
flumi/Scripts/Utils/GridUtils.gd
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
class_name GridUtils
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
static func apply_grid_container_properties(node: GridContainer, styles: Dictionary) -> void:
|
||||||
|
if styles.has("grid-template-columns"):
|
||||||
|
var cols = styles["grid-template-columns"]
|
||||||
|
if cols is int:
|
||||||
|
node.columns = cols
|
||||||
|
elif cols is String:
|
||||||
|
var parts = cols.split(" ")
|
||||||
|
node.columns = parts.size()
|
||||||
|
|
||||||
|
if styles.has("gap"):
|
||||||
|
var gap_value = parse_grid_value(styles["gap"])
|
||||||
|
if gap_value is int or gap_value is float:
|
||||||
|
node.add_theme_constant_override("h_separation", int(gap_value))
|
||||||
|
node.add_theme_constant_override("v_separation", int(gap_value))
|
||||||
|
|
||||||
|
if styles.has("column-gap"):
|
||||||
|
var gap_value = parse_grid_value(styles["column-gap"])
|
||||||
|
if gap_value is int or gap_value is float:
|
||||||
|
node.add_theme_constant_override("h_separation", int(gap_value))
|
||||||
|
|
||||||
|
if styles.has("row-gap"):
|
||||||
|
var gap_value = parse_grid_value(styles["row-gap"])
|
||||||
|
if gap_value is int or gap_value is float:
|
||||||
|
node.add_theme_constant_override("v_separation", int(gap_value))
|
||||||
|
|
||||||
|
static func apply_grid_item_properties(node: Control, styles: Dictionary) -> void:
|
||||||
|
var grid_properties: Dictionary = node.get_meta("grid_properties", {})
|
||||||
|
var changed = false
|
||||||
|
|
||||||
|
if styles.has("grid-column"):
|
||||||
|
grid_properties["grid-column"] = styles["grid-column"]
|
||||||
|
changed = true
|
||||||
|
|
||||||
|
if styles["grid-column"].begins_with("span "):
|
||||||
|
var span_count = styles["grid-column"].substr(5).to_int()
|
||||||
|
if span_count > 1:
|
||||||
|
node.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
node.set_meta("grid_column_span", span_count)
|
||||||
|
elif styles["grid-column"] == "1 / -1":
|
||||||
|
# Full span
|
||||||
|
node.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
node.set_meta("grid_column_span", -1)
|
||||||
|
|
||||||
|
if styles.has("grid-row"):
|
||||||
|
grid_properties["grid-row"] = styles["grid-row"]
|
||||||
|
changed = true
|
||||||
|
|
||||||
|
if changed:
|
||||||
|
node.set_meta("grid_properties", grid_properties)
|
||||||
|
|
||||||
|
static func parse_grid_value(val):
|
||||||
|
if val is float or val is int:
|
||||||
|
return val
|
||||||
|
|
||||||
|
if val is String:
|
||||||
|
var s_val = val.strip_edges()
|
||||||
|
if s_val.is_valid_float():
|
||||||
|
return s_val.to_float()
|
||||||
|
if s_val.ends_with("px"):
|
||||||
|
return s_val.trim_suffix("px").to_float()
|
||||||
|
if s_val == "auto":
|
||||||
|
return "auto"
|
||||||
|
|
||||||
|
return null
|
||||||
|
|
||||||
|
static func get_grid_item_span(span_property: String) -> Dictionary:
|
||||||
|
var result = {"start": -1, "end": -1, "span": 1}
|
||||||
|
|
||||||
|
if span_property.begins_with("span "):
|
||||||
|
var span_count = span_property.substr(5).to_int()
|
||||||
|
result["span"] = max(1, span_count)
|
||||||
|
elif span_property == "1 / -1":
|
||||||
|
# Full span
|
||||||
|
result["span"] = -1
|
||||||
|
else:
|
||||||
|
var parts = span_property.split(" / ")
|
||||||
|
if parts.size() == 2:
|
||||||
|
result["start"] = parts[0].to_int()
|
||||||
|
result["end"] = parts[1].to_int()
|
||||||
|
|
||||||
|
return result
|
||||||
1
flumi/Scripts/Utils/GridUtils.gd.uid
Normal file
1
flumi/Scripts/Utils/GridUtils.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://4p5lrhmdwgrj
|
||||||
@@ -341,32 +341,72 @@ static func _find_input_control_with_file_info(node: Node) -> Node:
|
|||||||
|
|
||||||
return null
|
return null
|
||||||
|
|
||||||
|
static func _get_select_value(element: HTMLParser.HTMLElement, dom_node: Node) -> String:
|
||||||
|
if dom_node is OptionButton:
|
||||||
|
var option_button = dom_node as OptionButton
|
||||||
|
var selected_index = option_button.selected
|
||||||
|
if selected_index >= 0 and selected_index < option_button.get_item_count():
|
||||||
|
var metadata = option_button.get_item_metadata(selected_index)
|
||||||
|
if metadata:
|
||||||
|
return str(metadata)
|
||||||
|
else:
|
||||||
|
return option_button.get_item_text(selected_index)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
static func _set_select_value(element: HTMLParser.HTMLElement, dom_node: Node, value: Variant) -> void:
|
||||||
|
if dom_node is OptionButton:
|
||||||
|
var option_button = dom_node as OptionButton
|
||||||
|
var target_value = str(value)
|
||||||
|
|
||||||
|
# Find the correct index to select
|
||||||
|
var selected_index = -1
|
||||||
|
|
||||||
|
# Find option with matching value
|
||||||
|
for i in range(option_button.get_item_count()):
|
||||||
|
var metadata = option_button.get_item_metadata(i)
|
||||||
|
var option_value = str(metadata) if metadata else option_button.get_item_text(i)
|
||||||
|
if option_value == target_value:
|
||||||
|
selected_index = i
|
||||||
|
break
|
||||||
|
|
||||||
|
# If no matching value found, try to find by text
|
||||||
|
if selected_index == -1:
|
||||||
|
for i in range(option_button.get_item_count()):
|
||||||
|
if option_button.get_item_text(i) == target_value:
|
||||||
|
selected_index = i
|
||||||
|
break
|
||||||
|
|
||||||
|
# Use call_deferred to set the property on main thread
|
||||||
|
if selected_index != -1:
|
||||||
|
option_button.call_deferred("set", "selected", selected_index)
|
||||||
|
|
||||||
static func _set_input_value(element: HTMLParser.HTMLElement, dom_node: Node, value: Variant) -> void:
|
static func _set_input_value(element: HTMLParser.HTMLElement, dom_node: Node, value: Variant) -> void:
|
||||||
var input_type = element.get_attribute("type").to_lower()
|
var input_type = element.get_attribute("type").to_lower()
|
||||||
|
|
||||||
match input_type:
|
match input_type:
|
||||||
"checkbox", "radio":
|
"checkbox", "radio":
|
||||||
if dom_node is CheckBox:
|
if dom_node is CheckBox:
|
||||||
dom_node.button_pressed = bool(value)
|
dom_node.call_deferred("set", "button_pressed", bool(value))
|
||||||
"color":
|
"color":
|
||||||
if dom_node is ColorPickerButton:
|
if dom_node is ColorPickerButton:
|
||||||
var color_value = str(value)
|
var color_value = str(value)
|
||||||
if color_value.begins_with("#"):
|
if color_value.begins_with("#"):
|
||||||
dom_node.color = Color.from_string(color_value, Color.WHITE)
|
var color = Color.from_string(color_value, Color.WHITE)
|
||||||
|
dom_node.call_deferred("set", "color", color)
|
||||||
"range":
|
"range":
|
||||||
if dom_node is HSlider:
|
if dom_node is HSlider:
|
||||||
dom_node.value = float(value)
|
dom_node.call_deferred("set", "value", float(value))
|
||||||
"number":
|
"number":
|
||||||
if dom_node is SpinBox:
|
if dom_node is SpinBox:
|
||||||
dom_node.value = float(value)
|
dom_node.call_deferred("set", "value", float(value))
|
||||||
"date":
|
"date":
|
||||||
if dom_node is DateButton and dom_node.has_method("set_date_from_string"):
|
if dom_node is DateButton and dom_node.has_method("set_date_from_string"):
|
||||||
dom_node.set_date_from_string(str(value))
|
dom_node.call_deferred("set_date_from_string", str(value))
|
||||||
_: # text, password, email, etc.
|
_: # text, password, email, etc.
|
||||||
if dom_node is LineEdit:
|
if dom_node is LineEdit:
|
||||||
dom_node.text = str(value)
|
dom_node.call_deferred("set", "text", str(value))
|
||||||
elif dom_node is TextEdit:
|
elif dom_node is TextEdit:
|
||||||
dom_node.text = str(value)
|
dom_node.call_deferred("set", "text", str(value))
|
||||||
|
|
||||||
# Helper functions
|
# Helper functions
|
||||||
static func find_element_by_id(element_id: String, dom_parser: HTMLParser) -> HTMLParser.HTMLElement:
|
static func find_element_by_id(element_id: String, dom_parser: HTMLParser) -> HTMLParser.HTMLElement:
|
||||||
@@ -931,7 +971,7 @@ static func _element_index_wrapper(vm: LuauVM) -> int:
|
|||||||
|
|
||||||
match key:
|
match key:
|
||||||
"value":
|
"value":
|
||||||
if lua_api and tag_name == "input":
|
if lua_api and (tag_name == "input" or tag_name == "select"):
|
||||||
vm.lua_getfield(1, "_element_id")
|
vm.lua_getfield(1, "_element_id")
|
||||||
var element_id: String = vm.lua_tostring(-1)
|
var element_id: String = vm.lua_tostring(-1)
|
||||||
vm.lua_pop(1)
|
vm.lua_pop(1)
|
||||||
@@ -940,8 +980,13 @@ static func _element_index_wrapper(vm: LuauVM) -> int:
|
|||||||
var dom_node = lua_api.dom_parser.parse_result.dom_nodes.get(element_id, null)
|
var dom_node = lua_api.dom_parser.parse_result.dom_nodes.get(element_id, null)
|
||||||
|
|
||||||
if element and dom_node:
|
if element and dom_node:
|
||||||
var input_value = _get_input_value(element, dom_node)
|
var value_result: String
|
||||||
vm.lua_pushstring(str(input_value))
|
if tag_name == "input":
|
||||||
|
value_result = str(_get_input_value(element, dom_node))
|
||||||
|
elif tag_name == "select":
|
||||||
|
value_result = _get_select_value(element, dom_node)
|
||||||
|
|
||||||
|
vm.lua_pushstring(value_result)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
# Fallback to empty string
|
# Fallback to empty string
|
||||||
@@ -1225,7 +1270,7 @@ static func _element_newindex_wrapper(vm: LuauVM) -> int:
|
|||||||
|
|
||||||
match key:
|
match key:
|
||||||
"value":
|
"value":
|
||||||
if tag_name == "input":
|
if tag_name == "input" or tag_name == "select":
|
||||||
vm.lua_getfield(1, "_element_id")
|
vm.lua_getfield(1, "_element_id")
|
||||||
var element_id: String = vm.lua_tostring(-1)
|
var element_id: String = vm.lua_tostring(-1)
|
||||||
vm.lua_pop(1)
|
vm.lua_pop(1)
|
||||||
@@ -1234,10 +1279,12 @@ static func _element_newindex_wrapper(vm: LuauVM) -> int:
|
|||||||
var dom_node = lua_api.dom_parser.parse_result.dom_nodes.get(element_id, null)
|
var dom_node = lua_api.dom_parser.parse_result.dom_nodes.get(element_id, null)
|
||||||
|
|
||||||
if element and dom_node:
|
if element and dom_node:
|
||||||
# Update the HTML element's value attribute
|
if tag_name == "input":
|
||||||
element.set_attribute("value", str(value))
|
element.set_attribute("value", str(value))
|
||||||
|
|
||||||
_set_input_value(element, dom_node, value)
|
_set_input_value(element, dom_node, value)
|
||||||
|
elif tag_name == "select":
|
||||||
|
element.set_attribute("value", str(value))
|
||||||
|
_set_select_value(element, dom_node, value)
|
||||||
return 0
|
return 0
|
||||||
"text":
|
"text":
|
||||||
var text: String = str(value) # Convert value to string
|
var text: String = str(value) # Convert value to string
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ static func connect_element_event(signal_node: Node, event_name: String, subscri
|
|||||||
elif signal_node is Control:
|
elif signal_node is Control:
|
||||||
var wrapper = func(event: InputEvent):
|
var wrapper = func(event: InputEvent):
|
||||||
LuaAudioUtils.mark_user_event()
|
LuaAudioUtils.mark_user_event()
|
||||||
subscription.lua_api._on_gui_input_click(subscription, event)
|
subscription.lua_api._on_gui_input_click(event, subscription)
|
||||||
signal_node.gui_input.connect(wrapper)
|
signal_node.gui_input.connect(wrapper)
|
||||||
subscription.connected_signal = "gui_input"
|
subscription.connected_signal = "gui_input"
|
||||||
subscription.connected_node = signal_node
|
subscription.connected_node = signal_node
|
||||||
|
|||||||
142
flumi/Scripts/Utils/Lua/Regex.gd
Normal file
142
flumi/Scripts/Utils/Lua/Regex.gd
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
class_name LuaRegexUtils
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
static func regex_new_handler(vm: LuauVM) -> int:
|
||||||
|
var pattern: String = vm.luaL_checkstring(1)
|
||||||
|
var regex = RegEx.new()
|
||||||
|
var result = regex.compile(pattern)
|
||||||
|
|
||||||
|
if result != OK:
|
||||||
|
vm.luaL_error("Invalid regex pattern: " + pattern)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
vm.lua_newtable()
|
||||||
|
vm.lua_pushobject(regex)
|
||||||
|
vm.lua_setfield(-2, "_regex")
|
||||||
|
|
||||||
|
vm.lua_pushcallable(regex_match_handler, "regex:match")
|
||||||
|
vm.lua_setfield(-2, "match")
|
||||||
|
|
||||||
|
vm.lua_pushcallable(regex_test_handler, "regex:test")
|
||||||
|
vm.lua_setfield(-2, "test")
|
||||||
|
|
||||||
|
return 1
|
||||||
|
|
||||||
|
static func regex_match_handler(vm: LuauVM) -> int:
|
||||||
|
vm.luaL_checktype(1, vm.LUA_TTABLE)
|
||||||
|
var subject: String = vm.luaL_checkstring(2)
|
||||||
|
|
||||||
|
vm.lua_getfield(1, "_regex")
|
||||||
|
var regex: RegEx = vm.lua_toobject(-1) as RegEx
|
||||||
|
vm.lua_pop(1)
|
||||||
|
|
||||||
|
if not regex:
|
||||||
|
vm.luaL_error("Invalid regex object")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
var result = regex.search(subject)
|
||||||
|
if not result:
|
||||||
|
vm.lua_pushnil()
|
||||||
|
return 1
|
||||||
|
|
||||||
|
vm.lua_newtable()
|
||||||
|
|
||||||
|
vm.lua_pushstring(result.get_string())
|
||||||
|
vm.lua_rawseti(-2, 1)
|
||||||
|
|
||||||
|
for i in range(1, result.get_group_count()):
|
||||||
|
var group = result.get_string(i)
|
||||||
|
vm.lua_pushstring(group)
|
||||||
|
vm.lua_rawseti(-2, i + 1)
|
||||||
|
|
||||||
|
return 1
|
||||||
|
|
||||||
|
static func regex_test_handler(vm: LuauVM) -> int:
|
||||||
|
vm.luaL_checktype(1, vm.LUA_TTABLE)
|
||||||
|
var subject: String = vm.luaL_checkstring(2)
|
||||||
|
|
||||||
|
vm.lua_getfield(1, "_regex")
|
||||||
|
var regex: RegEx = vm.lua_toobject(-1) as RegEx
|
||||||
|
vm.lua_pop(1)
|
||||||
|
|
||||||
|
if not regex:
|
||||||
|
vm.luaL_error("Invalid regex object")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
var result = regex.search(subject)
|
||||||
|
vm.lua_pushboolean(result != null)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
static func string_replace_handler(vm: LuauVM) -> int:
|
||||||
|
var subject: String = vm.luaL_checkstring(1)
|
||||||
|
|
||||||
|
if vm.lua_istable(2):
|
||||||
|
vm.lua_getfield(2, "_regex")
|
||||||
|
var regex: RegEx = vm.lua_toobject(-1) as RegEx
|
||||||
|
vm.lua_pop(1)
|
||||||
|
|
||||||
|
if not regex:
|
||||||
|
vm.luaL_error("Invalid regex object")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
var replacement: String = vm.luaL_checkstring(3)
|
||||||
|
var result = regex.sub(subject, replacement, false)
|
||||||
|
vm.lua_pushstring(result)
|
||||||
|
else:
|
||||||
|
var search: String = vm.luaL_checkstring(2)
|
||||||
|
var replacement: String = vm.luaL_checkstring(3)
|
||||||
|
|
||||||
|
var pos = subject.find(search)
|
||||||
|
if pos >= 0:
|
||||||
|
var result = subject.substr(0, pos) + replacement + subject.substr(pos + search.length())
|
||||||
|
vm.lua_pushstring(result)
|
||||||
|
else:
|
||||||
|
vm.lua_pushstring(subject)
|
||||||
|
|
||||||
|
return 1
|
||||||
|
|
||||||
|
static func string_replace_all_handler(vm: LuauVM) -> int:
|
||||||
|
var subject: String = vm.luaL_checkstring(1)
|
||||||
|
|
||||||
|
if vm.lua_istable(2):
|
||||||
|
vm.lua_getfield(2, "_regex")
|
||||||
|
var regex: RegEx = vm.lua_toobject(-1) as RegEx
|
||||||
|
vm.lua_pop(1)
|
||||||
|
|
||||||
|
if not regex:
|
||||||
|
vm.luaL_error("Invalid regex object")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
var replacement: String = vm.luaL_checkstring(3)
|
||||||
|
var result = regex.sub(subject, replacement, true)
|
||||||
|
vm.lua_pushstring(result)
|
||||||
|
else:
|
||||||
|
var search: String = vm.luaL_checkstring(2)
|
||||||
|
var replacement: String = vm.luaL_checkstring(3)
|
||||||
|
var result = subject.replace(search, replacement)
|
||||||
|
vm.lua_pushstring(result)
|
||||||
|
|
||||||
|
return 1
|
||||||
|
|
||||||
|
static func setup_regex_api(vm: LuauVM) -> void:
|
||||||
|
vm.lua_newtable()
|
||||||
|
|
||||||
|
vm.lua_pushcallable(regex_new_handler, "Regex.new")
|
||||||
|
vm.lua_setfield(-2, "new")
|
||||||
|
|
||||||
|
vm.lua_setglobal("Regex")
|
||||||
|
|
||||||
|
vm.lua_getglobal("string")
|
||||||
|
if vm.lua_isnil(-1):
|
||||||
|
vm.lua_pop(1)
|
||||||
|
vm.lua_newtable()
|
||||||
|
vm.lua_setglobal("string")
|
||||||
|
vm.lua_getglobal("string")
|
||||||
|
|
||||||
|
vm.lua_pushcallable(string_replace_handler, "string.replace")
|
||||||
|
vm.lua_setfield(-2, "replace")
|
||||||
|
|
||||||
|
vm.lua_pushcallable(string_replace_all_handler, "string.replaceAll")
|
||||||
|
vm.lua_setfield(-2, "replaceAll")
|
||||||
|
|
||||||
|
vm.lua_pop(1)
|
||||||
1
flumi/Scripts/Utils/Lua/Regex.gd.uid
Normal file
1
flumi/Scripts/Utils/Lua/Regex.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://c23cbsiegb0rq
|
||||||
@@ -315,6 +315,15 @@ func _setup_threaded_gurt_api():
|
|||||||
lua_vm.lua_pushstring(current_href)
|
lua_vm.lua_pushstring(current_href)
|
||||||
lua_vm.lua_setfield(-2, "href")
|
lua_vm.lua_setfield(-2, "href")
|
||||||
|
|
||||||
|
lua_vm.lua_newtable()
|
||||||
|
lua_vm.lua_pushcallable(lua_api._gurt_location_query_get_handler, "gurt.location.query.get")
|
||||||
|
lua_vm.lua_setfield(-2, "get")
|
||||||
|
lua_vm.lua_pushcallable(lua_api._gurt_location_query_has_handler, "gurt.location.query.has")
|
||||||
|
lua_vm.lua_setfield(-2, "has")
|
||||||
|
lua_vm.lua_pushcallable(lua_api._gurt_location_query_getAll_handler, "gurt.location.query.getAll")
|
||||||
|
lua_vm.lua_setfield(-2, "getAll")
|
||||||
|
lua_vm.lua_setfield(-2, "query")
|
||||||
|
|
||||||
lua_vm.lua_setfield(-2, "location")
|
lua_vm.lua_setfield(-2, "location")
|
||||||
|
|
||||||
var body_element = dom_parser.find_first("body")
|
var body_element = dom_parser.find_first("body")
|
||||||
@@ -345,6 +354,7 @@ func _setup_additional_lua_apis():
|
|||||||
LuaWebSocketUtils.setup_websocket_api(lua_vm)
|
LuaWebSocketUtils.setup_websocket_api(lua_vm)
|
||||||
LuaAudioUtils.setup_audio_api(lua_vm)
|
LuaAudioUtils.setup_audio_api(lua_vm)
|
||||||
LuaCrumbsUtils.setup_crumbs_api(lua_vm)
|
LuaCrumbsUtils.setup_crumbs_api(lua_vm)
|
||||||
|
LuaRegexUtils.setup_regex_api(lua_vm)
|
||||||
|
|
||||||
func _table_tostring_handler(vm: LuauVM) -> int:
|
func _table_tostring_handler(vm: LuauVM) -> int:
|
||||||
vm.luaL_checktype(1, vm.LUA_TTABLE)
|
vm.luaL_checktype(1, vm.LUA_TTABLE)
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ const BUTTON = preload("res://Scenes/Tags/button.tscn")
|
|||||||
const UL = preload("res://Scenes/Tags/ul.tscn")
|
const UL = preload("res://Scenes/Tags/ul.tscn")
|
||||||
const OL = preload("res://Scenes/Tags/ol.tscn")
|
const OL = preload("res://Scenes/Tags/ol.tscn")
|
||||||
const SELECT = preload("res://Scenes/Tags/select.tscn")
|
const SELECT = preload("res://Scenes/Tags/select.tscn")
|
||||||
const OPTION = preload("res://Scenes/Tags/option.tscn")
|
|
||||||
const TEXTAREA = preload("res://Scenes/Tags/textarea.tscn")
|
const TEXTAREA = preload("res://Scenes/Tags/textarea.tscn")
|
||||||
const DIV = preload("res://Scenes/Tags/div.tscn")
|
const DIV = preload("res://Scenes/Tags/div.tscn")
|
||||||
const AUDIO = preload("res://Scenes/Tags/audio.tscn")
|
const AUDIO = preload("res://Scenes/Tags/audio.tscn")
|
||||||
@@ -54,6 +53,8 @@ func _ready():
|
|||||||
ProjectSettings.set_setting("display/window/size/min_height", MIN_SIZE.y)
|
ProjectSettings.set_setting("display/window/size/min_height", MIN_SIZE.y)
|
||||||
DisplayServer.window_set_min_size(MIN_SIZE)
|
DisplayServer.window_set_min_size(MIN_SIZE)
|
||||||
|
|
||||||
|
call_deferred("render")
|
||||||
|
|
||||||
var current_domain = "" # Store current domain for display
|
var current_domain = "" # Store current domain for display
|
||||||
|
|
||||||
func resolve_url(href: String) -> String:
|
func resolve_url(href: String) -> String:
|
||||||
@@ -277,13 +278,14 @@ func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
|
|||||||
var styles = parser.get_element_styles_with_inheritance(element, "", [])
|
var styles = parser.get_element_styles_with_inheritance(element, "", [])
|
||||||
var hover_styles = parser.get_element_styles_with_inheritance(element, "hover", [])
|
var hover_styles = parser.get_element_styles_with_inheritance(element, "hover", [])
|
||||||
var is_flex_container = styles.has("display") and ("flex" in styles["display"])
|
var is_flex_container = styles.has("display") and ("flex" in styles["display"])
|
||||||
|
var is_grid_container = styles.has("display") and ("grid" in styles["display"])
|
||||||
|
|
||||||
var final_node: Control
|
var final_node: Control
|
||||||
var container_for_children: Node
|
var container_for_children: Node
|
||||||
|
|
||||||
# If this is an inline element AND not a flex container, do NOT recursively add child nodes for its children.
|
# If this is an inline element AND not a flex or grid container, do NOT recursively add child nodes for its children.
|
||||||
# Only create a node for the outermost inline group; nested inline tags are handled by BBCode.
|
# Only create a node for the outermost inline group; nested inline tags are handled by BBCode.
|
||||||
if element.is_inline_element() and not is_flex_container:
|
if element.is_inline_element() and not is_flex_container and not is_grid_container:
|
||||||
final_node = await create_element_node_internal(element, parser)
|
final_node = await create_element_node_internal(element, parser)
|
||||||
if not final_node:
|
if not final_node:
|
||||||
return null
|
return null
|
||||||
@@ -292,7 +294,24 @@ func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
|
|||||||
FlexUtils.apply_flex_item_properties(final_node, styles)
|
FlexUtils.apply_flex_item_properties(final_node, styles)
|
||||||
return final_node
|
return final_node
|
||||||
|
|
||||||
if is_flex_container:
|
if is_grid_container:
|
||||||
|
if element.tag_name == "div":
|
||||||
|
if BackgroundUtils.needs_background_wrapper(styles) or hover_styles.size() > 0:
|
||||||
|
final_node = BackgroundUtils.create_panel_container_with_background(styles, hover_styles)
|
||||||
|
var grid_container = GridContainer.new()
|
||||||
|
grid_container.name = "Grid_" + element.tag_name
|
||||||
|
var vbox = final_node.get_child(0) as VBoxContainer
|
||||||
|
vbox.add_child(grid_container)
|
||||||
|
container_for_children = grid_container
|
||||||
|
else:
|
||||||
|
final_node = GridContainer.new()
|
||||||
|
final_node.name = "Grid_" + element.tag_name
|
||||||
|
container_for_children = final_node
|
||||||
|
else:
|
||||||
|
final_node = GridContainer.new()
|
||||||
|
final_node.name = "Grid_" + element.tag_name
|
||||||
|
container_for_children = final_node
|
||||||
|
elif is_flex_container:
|
||||||
# The element's primary identity IS a flex container.
|
# The element's primary identity IS a flex container.
|
||||||
if element.tag_name == "div":
|
if element.tag_name == "div":
|
||||||
if BackgroundUtils.needs_background_wrapper(styles) or hover_styles.size() > 0:
|
if BackgroundUtils.needs_background_wrapper(styles) or hover_styles.size() > 0:
|
||||||
@@ -369,22 +388,43 @@ func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
|
|||||||
if flex_container_node is FlexContainer:
|
if flex_container_node is FlexContainer:
|
||||||
FlexUtils.apply_flex_container_properties(flex_container_node, styles)
|
FlexUtils.apply_flex_container_properties(flex_container_node, styles)
|
||||||
|
|
||||||
# Apply flex ITEM properties
|
if is_grid_container:
|
||||||
|
var grid_container_node = final_node
|
||||||
|
|
||||||
|
if final_node is GridContainer:
|
||||||
|
grid_container_node = final_node
|
||||||
|
elif final_node is MarginContainer and final_node.get_child_count() > 0:
|
||||||
|
var first_child = final_node.get_child(0)
|
||||||
|
if first_child is GridContainer:
|
||||||
|
grid_container_node = first_child
|
||||||
|
elif final_node is PanelContainer and final_node.get_child_count() > 0:
|
||||||
|
var vbox = final_node.get_child(0)
|
||||||
|
if vbox is VBoxContainer and vbox.get_child_count() > 0:
|
||||||
|
var potential_grid = vbox.get_child(0)
|
||||||
|
if potential_grid is GridContainer:
|
||||||
|
grid_container_node = potential_grid
|
||||||
|
|
||||||
|
if grid_container_node is GridContainer:
|
||||||
|
GridUtils.apply_grid_container_properties(grid_container_node, styles)
|
||||||
|
|
||||||
FlexUtils.apply_flex_item_properties(final_node, styles)
|
FlexUtils.apply_flex_item_properties(final_node, styles)
|
||||||
|
|
||||||
|
if not is_grid_container:
|
||||||
|
GridUtils.apply_grid_item_properties(final_node, styles)
|
||||||
|
|
||||||
# Skip ul/ol and non-flex forms, they handle their own children
|
# Skip ul/ol and non-flex forms, they handle their own children
|
||||||
var skip_general_processing = false
|
var skip_general_processing = false
|
||||||
|
|
||||||
if element.tag_name == "ul" or element.tag_name == "ol":
|
if element.tag_name == "ul" or element.tag_name == "ol":
|
||||||
skip_general_processing = true
|
skip_general_processing = true
|
||||||
elif element.tag_name == "form":
|
elif element.tag_name == "form":
|
||||||
skip_general_processing = not is_flex_container
|
skip_general_processing = not is_flex_container and not is_grid_container
|
||||||
|
|
||||||
if not skip_general_processing:
|
if not skip_general_processing:
|
||||||
for child_element in element.children:
|
for child_element in element.children:
|
||||||
# Only add child nodes if the child is NOT an inline element
|
# Only add child nodes if the child is NOT an inline element
|
||||||
# UNLESS the parent is a flex container (inline elements become flex items)
|
# UNLESS the parent is a flex or grid container (inline elements become flex/grid items)
|
||||||
if not child_element.is_inline_element() or is_flex_container:
|
if not child_element.is_inline_element() or is_flex_container or is_grid_container:
|
||||||
var child_node = await create_element_node(child_element, parser)
|
var child_node = await create_element_node(child_element, parser)
|
||||||
if child_node and is_instance_valid(container_for_children):
|
if child_node and is_instance_valid(container_for_children):
|
||||||
# Input elements register their own DOM nodes in their init() function
|
# Input elements register their own DOM nodes in their init() function
|
||||||
@@ -469,9 +509,6 @@ func create_element_node_internal(element: HTMLParser.HTMLElement, parser: HTMLP
|
|||||||
"select":
|
"select":
|
||||||
node = SELECT.instantiate()
|
node = SELECT.instantiate()
|
||||||
node.init(element, parser)
|
node.init(element, parser)
|
||||||
"option":
|
|
||||||
node = OPTION.instantiate()
|
|
||||||
node.init(element, parser)
|
|
||||||
"textarea":
|
"textarea":
|
||||||
node = TEXTAREA.instantiate()
|
node = TEXTAREA.instantiate()
|
||||||
node.init(element, parser)
|
node.init(element, parser)
|
||||||
@@ -482,9 +519,10 @@ func create_element_node_internal(element: HTMLParser.HTMLElement, parser: HTMLP
|
|||||||
var styles = parser.get_element_styles_with_inheritance(element, "", [])
|
var styles = parser.get_element_styles_with_inheritance(element, "", [])
|
||||||
var hover_styles = parser.get_element_styles_with_inheritance(element, "hover", [])
|
var hover_styles = parser.get_element_styles_with_inheritance(element, "hover", [])
|
||||||
var is_flex_container = styles.has("display") and ("flex" in styles["display"])
|
var is_flex_container = styles.has("display") and ("flex" in styles["display"])
|
||||||
|
var is_grid_container = styles.has("display") and ("grid" in styles["display"])
|
||||||
|
|
||||||
# For flex divs, let the general flex container logic handle them
|
# For flex or grid divs, let the general flex/grid container logic handle them
|
||||||
if is_flex_container:
|
if is_flex_container or is_grid_container:
|
||||||
return null
|
return null
|
||||||
|
|
||||||
# Create div container
|
# Create div container
|
||||||
|
|||||||
Reference in New Issue
Block a user