--[[
XYZ Scripts - BloxStrike
Teil 0: Gemeinsame Einstellungen (Key-System)
Teil 1: BloxStrike-Hub (wird nach dem Key-Check geladen, Home zeigt Free/Premium)
Teil 2: Key-System (Standalone Cloudflare Edge Authentication & HWID)
]]
-- ===== Gemeinsame Einstellungen (werden in xyz_settings.json gespeichert) =====
local Shared = {
HideName = false, -- Username durch wechselnde Zeichen ersetzen
SaveKey = true, -- Key speichern + beim naechsten Start automatisch laden
Language = "en", -- Sprache des Hubs (Standard: Englisch)
Theme = "Dark", -- Farbdesign des Hubs
KeyFile = "xyz_key.txt",
SettingsFile = "xyz_settings.json",
CurrentKey = nil,
}
do
local HS = game:GetService("HttpService")
if isfile and readfile and isfile(Shared.SettingsFile) then
pcall(function()
local d = HS:JSONDecode(readfile(Shared.SettingsFile))
if type(d) == "table" then
if type(d.HideName) == "boolean" then Shared.HideName = d.HideName end
if type(d.SaveKey) == "boolean" then Shared.SaveKey = d.SaveKey end
if type(d.Language) == "string" then Shared.Language = d.Language end
if type(d.Theme) == "string" then Shared.Theme = d.Theme end
end
end)
end
function Shared.Save()
if writefile then
pcall(function()
writefile(Shared.SettingsFile, HS:JSONEncode({
HideName = Shared.HideName,
SaveKey = Shared.SaveKey,
Language = Shared.Language,
Theme = Shared.Theme,
}))
end)
end
end
function Shared.WriteKey()
if writefile and Shared.CurrentKey and #Shared.CurrentKey > 4 then
pcall(function() writefile(Shared.KeyFile, Shared.CurrentKey) end)
end
end
function Shared.ClearKey()
if delfile and isfile and isfile(Shared.KeyFile) then
pcall(function() delfile(Shared.KeyFile) end)
end
end
-- Username-Anzeige: normal der echte Name, bei "Hide username" wild wechselnde chinesische Zeichen
local scrambleChars, bindings, started = {}, {}, false
local function rnd() return utf8.char(math.random(0x4E00, 0x9FA5)) end
function Shared.DisplayName(name)
if not Shared.HideName then return name end
local t = {}
for i = 1, #name do
scrambleChars[i] = scrambleChars[i] or rnd()
t[i] = scrambleChars[i]
end
return table.concat(t)
end
local function ensureTicker()
if started then return end
started = true
task.spawn(function()
while true do
task.wait(0.07)
if Shared.HideName then
for i = 1, 24 do -- jedes Zeichen wechselt zufaellig und unabhaengig
if scrambleChars[i] == nil or math.random() < 0.45 then
scrambleChars[i] = rnd()
end
end
end
for i = #bindings, 1, -1 do
local b = bindings[i]
if not b.label.Parent then
table.remove(bindings, i)
else
local s = Shared.DisplayName(b.name)
if b.fmt then s = b.fmt(s) end
if b.label.Text ~= s then b.label.Text = s end
end
end
if #bindings == 0 then
started = false
break
end
end
end)
end
-- Label an einen Usernamen binden (fmt optional, z.B. function(n) return "Hi " .. n end)
function Shared.BindName(label, name, fmt)
table.insert(bindings, {label = label, name = name, fmt = fmt})
local s = Shared.DisplayName(name)
label.Text = fmt and fmt(s) or s
ensureTicker()
end
end
-- ===== Teil 1: BloxStrike Hub =====
local function RunBloxStrike(keyData)
local XYZ_TIER = "Free"
if type(keyData) == "table" and keyData.tier then
local s = tostring(keyData.tier)
XYZ_TIER = s:sub(1, 1):upper() .. s:sub(2):lower()
end
if XYZ_TIER:lower():find("premium") then XYZ_TIER = "Premium" end
local ProtectionConfig = {
SecretKey = "2009",
-- The name of your Hub (shown in the kick message if they try to bypass)
HubName = "XYZ"
}
-- Anti-Bypass Logic: Checks if the Key System successfully set the global variable
if false then -- [bypassed] anti-exec guard; was: if not _G[ProtectionConfig.SecretKey]
local player = game:GetService("Players").LocalPlayer
if player then
player:Kick("\n🛡️ Unauthorized Execution 🛡️\n\nPlease use the official Key System to run " .. ProtectionConfig.HubName)
end
return -- Stops the rest of the script from loading!
end
-------------------------------------------------------------------------------
-- 👇 YOUR MAIN SCRIPT CODE STARTS HERE 👇
-------------------------------------------------------------------------------
print(ProtectionConfig.HubName .. " Loaded Successfully!")
if not game:IsLoaded() then
game.Loaded:Wait()
end
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Workspace = game:GetService("Workspace")
local UserInputService = game:GetService("UserInputService")
local HttpService = game:GetService("HttpService")
local Lighting = game:GetService("Lighting")
local TeleportService = game:GetService("TeleportService")
local StarterGui = game:GetService("StarterGui")
local VirtualInputManager = game:GetService("VirtualInputManager")
local VirtualUser = game:GetService("VirtualUser")
local LocalPlayer = Players.LocalPlayer
if false then -- [bypassed] gameid lock; was: if game.GameId ~= 7633926880
LocalPlayer:Kick("XYZ: this script is for BloxStrike only")
return
end
---@return function the game's own print when the executor exposes it
local function RuntimePrint()
local ok, renv = pcall(getrenv)
return ok and type(renv) == "table" and type(renv.print) == "function" and renv.print or print
end
local XYZBanner = {
Print = RuntimePrint(),
Started = os.clock(),
Last = os.clock(),
Done = 0,
Total = 4,
}
local MarioBanner = XYZBanner
function XYZBanner.Show()
local ok, executor = pcall(identifyexecutor)
if not ok or type(executor) ~= "string" then executor = "Unknown" end
local rule = string.rep("=", 54)
XYZBanner.Print(table.concat({
"",
[[
__ ____ ______
\ \/ /\ \ / /_ /
\ / \ V / / /
/ \ | | / /__
/_/\_\ |_| /____|
]],
rule,
" BLOXSTRIKE // XYZ Scripts // discord.gg/RzkdNVVku8",
" executor: " .. executor .. " // player: " .. game:GetService("Players").LocalPlayer.Name,
rule,
}, "\n"))
end
---@param label string what just finished loading
function XYZBanner.Step(label)
local now = os.clock()
XYZBanner.Done = math.min(XYZBanner.Done + 1, XYZBanner.Total)
local filled = math.floor(XYZBanner.Done / XYZBanner.Total * 20 + 0.5)
XYZBanner.Print(string.format("[XYZ] [%s] %3d%% %-24s +%dms",
string.rep("#", filled) .. string.rep(".", 20 - filled),
math.floor(XYZBanner.Done / XYZBanner.Total * 100), label, math.floor((now - XYZBanner.Last) * 1000)))
XYZBanner.Last = now
end
function XYZBanner.Ready()
local rule = string.rep("=", 54)
XYZBanner.Print(table.concat({
rule,
string.format(" >> XYZ READY in %dms", math.floor((os.clock() - XYZBanner.Started) * 1000)),
rule,
}, "\n"))
end
pcall(XYZBanner.Show)
pcall(XYZBanner.Step, "Core")
local environment = getgenv and getgenv() or _G
if environment.XYZUnload then pcall(environment.XYZUnload) end
if environment.BloxStrikeUnload then pcall(environment.BloxStrikeUnload) end
if not LPH_OBFUSCATED then
local function Passthrough(fn) return fn end
LPH_JIT, LPH_JIT_MAX, LPH_NO_VIRTUALIZE = Passthrough, Passthrough, Passthrough
end
local vector2New = Vector2.new
local cframeLookAt = CFrame.lookAt
local osClock = os.clock
-- AirFlow UI library (library.txt), embedded so the menu needs no download.
-- It lives in its own function so its locals don't count against the script
-- below it: Luau allows 200 locals per function. BuildInterface runs it.
local function LoadAirflow()
local TweenService = game:GetService("TweenService")
local UserInputService = game:GetService("UserInputService")
local GuiService = game:GetService("GuiService")
local RunService = game:GetService("RunService")
local HttpService = game:GetService("HttpService")
local Players = game:GetService("Players")
local TeleportService = game:GetService("TeleportService")
local MarketplaceService = game:GetService("MarketplaceService")
local StatsService = game:GetService("Stats")
local LocalPlayer = Players.LocalPlayer
local Library = {}
Library.Flags = {}
Library.Windows = {}
local function normalize(source, map)
local opts = {}
if type(source) == "string" then
opts.Name = source
elseif type(source) == "table" then
for key, value in pairs(source) do
opts[key] = value
end
end
for from, to in pairs(map or {}) do
if opts[to] == nil and opts[from] ~= nil then
opts[to] = opts[from]
end
end
if opts.Range then
opts.Min = opts.Min or opts.Range[1]
opts.Max = opts.Max or opts.Range[2]
end
return opts
end
local function finishElement(tab, opts, element, frame, kind)
element._type = kind
element._frame = frame
element._listeners = element._listeners or {}
local window = tab and tab.Window
local searchText = opts.Name or opts.Text or opts.Title
element._searchName = type(searchText) == "string" and searchText or ""
element._searchKey = string.lower(element._searchName)
if tab and tab._items then
table.insert(tab._items, element)
end
if opts.Flag then
Library.Flags[opts.Flag] = element
if window then
window:_flagCreated(opts.Flag, element)
end
end
element._userVisible = opts.Visible ~= false
function element:IsVisible()
return element._userVisible
end
function element:_isShown()
return element._userVisible and not (tab and tab._userVisible == false)
end
function element:SetVisible(visible)
visible = visible and true or false
if visible == element._userVisible or element._destroyed then
return
end
element._userVisible = visible
if not visible then
if element._onHide then
element._onHide()
end
if element.Open and type(element.SetOpen) == "function" then
element:SetOpen(false)
end
end
if frame then
frame.Visible = visible
end
if window then
window._controlsDirty = true
end
end
if frame and not element._userVisible then
frame.Visible = false
end
-- Tooltip: a string, or { Title, Text, Icon }. nil, false or "" hides it.
function element:SetTooltip(tip)
element._tooltip = tip
if frame and window and not element._tooltipBound then
element._tooltipBound = true
window:_attachTooltip(element, frame)
end
end
function element:GetTooltip()
return element._tooltip
end
if opts.Tooltip ~= nil and opts.Tooltip ~= false then
element:SetTooltip(opts.Tooltip)
end
function element:Destroy()
if element._destroyed then
return
end
element._destroyed = true
if window and window._tipOwner == element then
window:_hideTooltip()
end
for _, disconnect in ipairs(element._listeners) do
disconnect()
end
element._listeners = {}
if tab and tab._items then
local index = table.find(tab._items, element)
if index then
table.remove(tab._items, index)
end
end
if opts.Flag and Library.Flags[opts.Flag] == element then
Library.Flags[opts.Flag] = nil
end
if window then
window._controlsDirty = true
end
if frame then
frame:Destroy()
end
end
return element
end
Library.Theme = {
Background = Color3.fromRGB(20, 20, 22),
Surface = Color3.fromRGB(24, 24, 26),
Surface2 = Color3.fromRGB(30, 30, 33),
Surface3 = Color3.fromRGB(38, 38, 42),
Stroke = Color3.fromRGB(45, 45, 50),
StrokeHover = Color3.fromRGB(84, 84, 90),
Accent = Color3.fromRGB(170, 170, 180),
AccentDark = Color3.fromRGB(18, 18, 20),
Text = Color3.fromRGB(240, 240, 240),
Muted = Color3.fromRGB(140, 140, 145),
Warning = Color3.fromRGB(240, 190, 75),
Success = Color3.fromRGB(50, 230, 150),
Error = Color3.fromRGB(245, 85, 85),
}
-- Order decides which key wins when two theme colors are identical.
local THEME_KEYS = {
"Accent", "AccentDark", "Background", "Surface", "Surface2", "Surface3",
"Stroke", "StrokeHover", "Text", "Muted", "Warning", "Success", "Error",
}
local function rgb(r, g, b)
return Color3.fromRGB(r, g, b)
end
local function preset(background, surface, surface2, surface3, strokeColor, strokeHover, accent, accentDark, text, muted)
return {
Background = rgb(table.unpack(background)),
Surface = rgb(table.unpack(surface)),
Surface2 = rgb(table.unpack(surface2)),
Surface3 = rgb(table.unpack(surface3)),
Stroke = rgb(table.unpack(strokeColor)),
StrokeHover = rgb(table.unpack(strokeHover)),
Accent = rgb(table.unpack(accent)),
AccentDark = rgb(table.unpack(accentDark)),
Text = rgb(table.unpack(text)),
Muted = rgb(table.unpack(muted)),
}
end
Library.ThemePresets = {
-- Colours here must not be pure white, pure black or any other colour the
-- library hard-codes, or those objects would get bound to the theme.
XYZ = preset({ 20, 20, 22 }, { 24, 24, 26 }, { 30, 30, 33 }, { 38, 38, 42 }, { 45, 45, 50 }, { 84, 84, 90 }, { 170, 170, 180 }, { 18, 18, 20 }, { 240, 240, 240 }, { 140, 140, 145 }),
Midnight = preset({ 15, 18, 26 }, { 18, 22, 31 }, { 22, 26, 37 }, { 37, 44, 60 }, { 34, 40, 55 }, { 70, 84, 112 }, { 128, 160, 246 }, { 14, 18, 30 }, { 228, 232, 242 }, { 116, 126, 148 }),
Ocean = preset({ 12, 20, 24 }, { 15, 24, 29 }, { 18, 29, 35 }, { 31, 48, 56 }, { 29, 44, 52 }, { 58, 92, 104 }, { 110, 214, 222 }, { 10, 24, 28 }, { 226, 238, 240 }, { 108, 134, 140 }),
Rose = preset({ 22, 15, 18 }, { 27, 18, 22 }, { 32, 21, 26 }, { 48, 34, 40 }, { 45, 30, 37 }, { 98, 64, 78 }, { 246, 160, 186 }, { 30, 14, 20 }, { 240, 228, 232 }, { 138, 112, 120 }),
Emerald = preset({ 13, 20, 17 }, { 16, 24, 20 }, { 20, 29, 24 }, { 33, 47, 40 }, { 30, 44, 37 }, { 62, 96, 78 }, { 132, 226, 170 }, { 12, 26, 18 }, { 228, 240, 233 }, { 110, 136, 122 }),
Amber = preset({ 22, 18, 13 }, { 27, 22, 16 }, { 32, 26, 19 }, { 49, 41, 31 }, { 46, 38, 28 }, { 100, 82, 58 }, { 246, 196, 120 }, { 30, 22, 10 }, { 242, 236, 226 }, { 140, 126, 106 }),
Mono = preset({ 16, 16, 17 }, { 20, 20, 21 }, { 25, 25, 26 }, { 40, 40, 42 }, { 38, 38, 40 }, { 84, 84, 88 }, { 236, 236, 240 }, { 20, 20, 22 }, { 234, 234, 236 }, { 122, 122, 128 }),
Nebula = preset({ 14, 12, 28 }, { 18, 15, 35 }, { 23, 19, 43 }, { 39, 32, 68 }, { 35, 29, 62 }, { 84, 68, 140 }, { 199, 125, 255 }, { 22, 10, 40 }, { 236, 230, 252 }, { 124, 114, 160 }),
Synthwave = preset({ 20, 12, 30 }, { 25, 15, 37 }, { 31, 18, 45 }, { 52, 30, 72 }, { 47, 27, 66 }, { 110, 58, 150 }, { 255, 94, 198 }, { 38, 8, 30 }, { 250, 232, 246 }, { 150, 118, 160 }),
Sakura = preset({ 24, 14, 20 }, { 29, 17, 24 }, { 35, 20, 29 }, { 54, 32, 45 }, { 50, 29, 41 }, { 112, 66, 92 }, { 255, 158, 200 }, { 36, 12, 24 }, { 248, 232, 240 }, { 150, 112, 132 }),
Velvet = preset({ 20, 10, 16 }, { 25, 12, 20 }, { 31, 15, 25 }, { 50, 25, 40 }, { 46, 23, 37 }, { 104, 52, 82 }, { 222, 110, 170 }, { 30, 8, 20 }, { 244, 228, 238 }, { 142, 106, 126 }),
Crimson = preset({ 18, 10, 12 }, { 23, 12, 15 }, { 28, 14, 18 }, { 46, 24, 29 }, { 42, 22, 27 }, { 104, 46, 56 }, { 244, 74, 94 }, { 32, 8, 12 }, { 244, 230, 232 }, { 142, 106, 112 }),
Sunset = preset({ 22, 13, 14 }, { 27, 16, 17 }, { 33, 19, 21 }, { 52, 31, 32 }, { 48, 28, 30 }, { 112, 62, 58 }, { 255, 138, 92 }, { 34, 14, 8 }, { 248, 234, 228 }, { 150, 118, 110 }),
Gold = preset({ 14, 13, 11 }, { 18, 17, 14 }, { 23, 21, 17 }, { 38, 35, 28 }, { 35, 32, 26 }, { 92, 80, 54 }, { 232, 196, 122 }, { 30, 22, 8 }, { 240, 234, 222 }, { 136, 126, 106 }),
Cyber = preset({ 13, 13, 15 }, { 17, 17, 19 }, { 21, 21, 24 }, { 36, 36, 40 }, { 33, 33, 37 }, { 86, 84, 70 }, { 250, 226, 72 }, { 30, 26, 4 }, { 240, 240, 232 }, { 126, 124, 118 }),
Toxic = preset({ 11, 14, 11 }, { 14, 18, 14 }, { 18, 23, 18 }, { 31, 39, 30 }, { 28, 36, 28 }, { 74, 98, 60 }, { 184, 255, 74 }, { 18, 30, 6 }, { 232, 242, 228 }, { 116, 134, 110 }),
Matcha = preset({ 16, 19, 14 }, { 20, 23, 17 }, { 24, 28, 21 }, { 40, 46, 34 }, { 37, 42, 31 }, { 84, 98, 70 }, { 176, 214, 140 }, { 18, 26, 10 }, { 236, 240, 228 }, { 128, 136, 116 }),
Aurora = preset({ 10, 20, 22 }, { 12, 25, 27 }, { 15, 30, 33 }, { 26, 50, 53 }, { 24, 45, 48 }, { 50, 104, 104 }, { 94, 240, 180 }, { 6, 30, 22 }, { 224, 246, 240 }, { 102, 142, 134 }),
Frost = preset({ 13, 17, 23 }, { 16, 21, 28 }, { 20, 26, 34 }, { 34, 43, 56 }, { 31, 40, 52 }, { 72, 92, 118 }, { 164, 214, 255 }, { 12, 22, 34 }, { 230, 238, 246 }, { 114, 128, 146 }),
Abyss = preset({ 7, 11, 22 }, { 9, 14, 28 }, { 12, 18, 35 }, { 22, 31, 58 }, { 20, 28, 52 }, { 44, 64, 120 }, { 72, 148, 255 }, { 6, 14, 34 }, { 226, 234, 250 }, { 100, 116, 152 }),
-- Seasonal and themed palettes.
Halloween = preset({ 18, 12, 20 }, { 23, 15, 25 }, { 28, 18, 30 }, { 46, 30, 48 }, { 43, 28, 44 }, { 104, 62, 40 }, { 255, 138, 36 }, { 36, 16, 4 }, { 246, 234, 224 }, { 146, 120, 128 }),
Haunted = preset({ 14, 12, 20 }, { 18, 15, 26 }, { 22, 18, 32 }, { 38, 31, 54 }, { 35, 29, 50 }, { 70, 96, 64 }, { 156, 255, 110 }, { 14, 30, 8 }, { 232, 240, 228 }, { 120, 116, 140 }),
Christmas = preset({ 12, 20, 15 }, { 15, 25, 19 }, { 19, 30, 23 }, { 32, 50, 38 }, { 30, 46, 35 }, { 100, 54, 56 }, { 236, 72, 80 }, { 34, 8, 10 }, { 240, 244, 236 }, { 120, 142, 128 }),
Valentine = preset({ 24, 12, 17 }, { 30, 14, 21 }, { 36, 17, 25 }, { 58, 27, 40 }, { 54, 25, 37 }, { 130, 56, 84 }, { 255, 92, 140 }, { 40, 8, 20 }, { 252, 232, 240 }, { 160, 112, 132 }),
Lunar = preset({ 22, 10, 10 }, { 28, 12, 12 }, { 34, 15, 14 }, { 54, 24, 22 }, { 50, 22, 20 }, { 130, 92, 40 }, { 246, 196, 84 }, { 38, 16, 6 }, { 250, 236, 222 }, { 160, 120, 108 }),
Tropical = preset({ 10, 22, 24 }, { 12, 27, 30 }, { 15, 33, 36 }, { 26, 54, 58 }, { 24, 50, 54 }, { 44, 110, 110 }, { 255, 170, 90 }, { 36, 18, 6 }, { 232, 246, 244 }, { 104, 144, 142 }),
Dracula = preset({ 24, 25, 33 }, { 29, 30, 40 }, { 34, 36, 47 }, { 52, 55, 70 }, { 48, 50, 64 }, { 98, 114, 164 }, { 189, 147, 249 }, { 26, 18, 44 }, { 246, 246, 238 }, { 126, 132, 168 }),
Coffee = preset({ 20, 16, 13 }, { 25, 20, 16 }, { 30, 24, 19 }, { 48, 39, 31 }, { 45, 36, 29 }, { 104, 82, 62 }, { 214, 170, 124 }, { 32, 22, 12 }, { 240, 230, 218 }, { 142, 124, 108 }),
}
Library.ThemeName = "XYZ"
Library.DefaultTheme = "XYZ"
Library.Assets = {
Shadow = "rbxassetid://6014261993",
Glow = "rbxassetid://8992230677",
Logo = "layers", -- fallback only; the embedded XYZ mark replaces it when the executor can write files
}
local LUCIDE_URL = "https://raw.githubusercontent.com/Footagesus/Icons/refs/heads/main/lucide/dist/Icons.lua"
local lucideSet = nil
local function loadLucide()
if lucideSet ~= nil then
return lucideSet
end
local ok, result = pcall(function()
local source = game:HttpGet(LUCIDE_URL)
return loadstring(source)()
end)
if ok and type(result) == "table" then
lucideSet = result
else
lucideSet = false
warn("[AirFlow] lucide icons unavailable: " .. tostring(result))
end
return lucideSet
end
local FONT_PRESETS = {
ValleySans = {
Regular = "https://raw.githubusercontent.com/HelsinkiTypeStudio/valley-sans/main/fonts/ttf/ValleySans-Regular.ttf",
Medium = "https://raw.githubusercontent.com/HelsinkiTypeStudio/valley-sans/main/fonts/ttf/ValleySans-Medium.ttf",
SemiBold = "https://raw.githubusercontent.com/HelsinkiTypeStudio/valley-sans/main/fonts/ttf/ValleySans-SemiBold.ttf",
},
}
local FONT_WEIGHTS = {
Regular = { 400, Enum.FontWeight.Regular },
Medium = { 500, Enum.FontWeight.Medium },
SemiBold = { 600, Enum.FontWeight.SemiBold },
Bold = { 700, Enum.FontWeight.Bold },
}
function Library:LoadFont(opts)
opts = normalize(opts, {})
if type(writefile) ~= "function" or type(isfile) ~= "function" or typeof(getcustomasset) ~= "function" then
warn("[AirFlow] custom fonts need writefile, isfile and getcustomasset")
return false
end
local name = opts.Name or "CustomFont"
local weights = opts.Weights or FONT_PRESETS[name]
if type(weights) ~= "table" then
warn("[AirFlow] no font weights for " .. name)
return false
end
local folder = opts.Folder or "AirFlowFonts"
pcall(function()
if type(isfolder) == "function" and type(makefolder) == "function" and not isfolder(folder) then
makefolder(folder)
end
end)
local faces = {}
for weightName, url in pairs(weights) do
local info = FONT_WEIGHTS[weightName]
if info then
local path = folder .. "/" .. name .. "-" .. weightName .. ".ttf"
local ok = true
if not isfile(path) then
ok = pcall(function()
writefile(path, game:HttpGet(url))
end)
end
if ok then
table.insert(faces, { name = weightName, weight = info[1], style = "normal", assetId = getcustomasset(path) })
else
warn("[AirFlow] could not download " .. weightName .. " weight of " .. name)
end
end
end
if #faces == 0 then
return false
end
local familyPath = folder .. "/" .. name .. ".json"
local okWrite = pcall(function()
writefile(familyPath, HttpService:JSONEncode({ name = name, faces = faces }))
end)
if not okWrite then
return false
end
local family = getcustomasset(familyPath)
local function face(weightName, fallback)
local info = FONT_WEIGHTS[weightName]
if info and weights[weightName] then
return Font.new(family, info[2])
end
return fallback
end
Library.Fonts.Regular = face("Regular", Library.Fonts.Regular)
Library.Fonts.Medium = face("Medium", face("Regular", Library.Fonts.Medium))
Library.Fonts.Bold = face("SemiBold", face("Bold", Library.Fonts.Bold))
return true
end
function Library:PreloadIcons()
return loadLucide() ~= false
end
-- Numbers, digit strings and asset URLs are custom images; anything else is
-- a lucide name.
local function assetId(value)
if type(value) == "number" then
return "rbxassetid://" .. string.format("%.0f", value)
end
if type(value) == "string" then
if value:match("^%d+$") then
return "rbxassetid://" .. value
end
if value:find("^rbxassetid://") or value:find("^rbxasset://") or value:find("^rbxthumb://") or value:find("^http") or (Assets and value == Assets.Logo) then
return value
end
end
return nil
end
-- Returns image, rect offset, rect size, whether it is a custom image, and
-- whether that custom image should still take the theme tint.
local function resolveIcon(icon)
if typeof(icon) == "table" then
return assetId(icon.Image) or icon.Image, icon.RectOffset, icon.RectSize, true, icon.Tint == true
end
local asset = assetId(icon)
if asset then
return asset, nil, nil, true, false
end
if type(icon) ~= "string" then
return nil
end
local name = icon:gsub("^lucide:", "")
local set = loadLucide()
if not set then
return nil
end
local entry = (set.Icons and set.Icons[name]) or set[name]
if type(entry) == "table" then
local image = entry.Image
if type(image) == "number" then
image = "rbxassetid://" .. tostring(image)
end
local sheet = (set.Spritesheets and set.Spritesheets[tostring(image)]) or image
return sheet, entry.ImageRectPosition, entry.ImageRectSize
elseif type(entry) == "string" then
return entry
end
warn("[AirFlow] unknown lucide icon: " .. name)
return nil
end
local BUTTON_HINT_ICON = "chevron-right"
local FONT_FAMILY = "rbxasset://fonts/families/BuilderSans.json"
Library.Fonts = {
Regular = Font.new(FONT_FAMILY, Enum.FontWeight.Regular),
Medium = Font.new(FONT_FAMILY, Enum.FontWeight.Medium),
Bold = Font.new(FONT_FAMILY, Enum.FontWeight.SemiBold),
}
local Theme = Library.Theme
local Assets = Library.Assets
-- The XYZ mark, embedded as a PNG so it needs no external upload.
-- It is written to the workspace once and loaded with getcustomasset.
local EMBEDDED_LOGO = "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAUEElEQVR42u2deXRUVZ7Hv1UmAQ2kqhIQtRGOu3MUnHOkaYXW7CFhCe30HGDADRUwQhaSVBKykZDKQhJCIEBYbEBGSdxDBVBRRhsEEyBAB1t7xGY4smVjSSALS/LmDwZHaRqS1HLvq/f9nFN/4FG8777f73Pv971XrwBCCCGEEEIIIYQQQgghhBBCCCGEuAo6LRxkSEi4wlNNesO2bVYdBcBmJ8QlpaBjwxOiXSHo2PSEaFcGOi00fm3tPlYm6RXDh49waRHoXK3p2exENinILAOd2hufDU/UIgQZRaBTa/Oz8YkaRSCbBHRqanw2PXEVGcgiAh0bnxDtikAnc/Oz8YkWRCBSAjoZm5+NT7QmAlES0MnU+Gx+wt2Ac0Wgk6X52fiEInC+BPRsfkLE8M/q3pmPuuvZ/IRoVwI6Uc3Pxifk1pHA0XFAz+YnRN7dgKN3Ano2PyHalYCezU+IdiWgZ/MTol0J6Nn8hGhXAnoRB0EIkaN/7CoAvquPEMdjzz7TO3JQXP0JkTsK6Nn8hGhXAnpnDZYQIl9f2SwA5n5C1Hs9wO47AK7+hKhnF2CTAK63D5ufEOdLwJZdgN5ezU8IUV8UsFsE4OpPiPqiQK8EwNWfENfYBdhlB8DVnxB17gL0nEZCtEuPBcAr/4TIuwvoaQzgDoAQ7gB6t/oTQuSjJ31q0w6A239C5IsBjACEEPsKgBf/CFHPLqC7MYA7AEK4AyCEUACEEAqA+Z8QbVwH4A6AEO4ACCEUACGEAiCEUAAAeAGQEDXR0wuB3AEQwh0AIYQCIIRQAIQQCoAQQgEQQigAQojL4cYp+EfGjh2HqKhoKcbS0tKCWbNm4OzZs6qbxz/84Tm8/nqE8HF0dHRg5szX0NDQwOLmDuDWbN26BQcPHoBOpxP+MRgMiIyMVt0cGgwGvPDCi1LM4YYNb7H5KYCeUVRUhPb2NinGMnr0aPj7+6tq/qZPfwX9+vUTPo4ffzyMioqPWdAUQM9oaKjHmjVrpFjBdDodZs+OhMlkUsXcPfjgQwgLGyt8zhRFQXFxMbq6uljQFEDP2bJlMw4ckCcKREfHqGLe5syZA71eL3zOKio+xuHDP7CQKQBbokAh2tvbpZDA73//DPz9A6Ser8DAIDz++DDhc9XY2Ij169exgCkA26ivr8fq1askWl0jYTQapZyrvn37YsaMmVKMZenSJejo6GABUwC2s3lzJQ4c2C/FLsBoNCI6eq6U8zRt2vMYOHCg8DnasePPqKr6hoVLAdiPwsICaaKAr6+vdFHgnnvuwaRJk4XPTVtbG5YvX8aCpQDsHwVWrVopzV2B6OgYqaLA7NmR8PDwED4va9asxunTp1mwFID9qay0oqamRoqxGAwGxMTIEQV++9uRGDVqlPBx/PWv36Ky0spCpQAcR0FBvjRRwM/PX/gDQm5uboiMjBI+F52dnSgsLISi8BfsKQCHRoE6rFxZKk0UiImJFRoF/vjHf8fQoUOFz0N5eRmOHv0fFigF4His1k2oqamRQgAmkwlz58YJmQeTyYSXX54ufA5OnjyBDRveYmFSAM4jPz8PbW1yfFfA398ffn7OjwKzZr0OT09P4cdfWFiAS5cusSgpAOdRV1eH0tIV0kSBuLh4GAwGpx3/o48+irFjxwk/7s8++1SaC7MUgMbYtKkC+/fLEwXi4uKdctw6nQ5z58YJf96/paUZy5bxnj8FIJDc3Bxp7goEBgY5JQqEhobh8ccfF368y5aVoLn5HIuQAhAbBZYvL5FmPPHxZhgMRof9/XfccQciIsS/5aemZh8++eQTFiAFIJ6KigrU1OyTYhfg7e3t0CgwfforGDBA7PP+ly9fRkFBPguPApCHnJxsaaJAcHCwQ6LAvffei8mTpwg/vvXr1+HYsWMsOgpArihQUrJUmvGYzQl2vysQExMLd3d3ocd15MgRvP32f7LgKAAZo8DH2LdPjijg4+OD+Hiz3Y5t1KjRGD16tNBjAoC8vFxcuXKFxUYByEl2tkWaKBASMgZ+fn42H5O7uztiY+OkeMXXoUO1LDIKQOYocAolJUukeUAoKSkZXl5eNh3TlCn/gSFDhgg9jjNnzvB7/hSAOvjoo4+wd+8eKcbi7e0Nszmh1/+9j48PXn31NeHHUVhYgAsXLrC4KAB1YLFkSRMFQkPD4OvbuygQGRkFT09PoePftWsXtm//gkVFAaiHU6dOYcmSYmmiQHJyCry8enZXYNiwYRg3brzQcXd0dGDhwlwWFAWgxijwIfbu3SuFAAYMGICEhO5HAZ1OB7M5Ufjz/itXlqKuro7FRAGokwULMtDW1irFWMLCxnb7rkB4+EQ89thjQsf7t799j7KyjSwiCkDtUUCeuwIpKam3vCvg6emJOXMihf+sl8Vi4c96UQDq58MPP8DevXskiQIDkZCQdNPxzpoVgQEDBggdZ1nZRnz//XcsHgpA/SiKgoyMDLS1tUkhgfHjx8PX1/eGY73vvvswdepUoeOrr6/DihXLWTgUgCtFgZMoLl4szXjS0tJvGAXM5kS4ubkJHVt29tUvVhEKwKX44IP3sWePHFFg4MA7kZj46yjg5+cv/Hn/zz//HDt37mCxUACuGgXSpYkCEyaE/xwFPDw8YDYnCB1Pa2sr8vPzWCgUgOty8uRJLF5cJM1dgfT0DPTv3x8vvviS8Of9i4sXo7GxkUVCAbg277//Hqqrq6QYy5133ons7FzhP+t98OBBvP/+eywOCkAbUSA9PU2aKBAQECD0ef/Ozk5kZs7nz3pRANqKAkVFi6SJAiI/a9f+CT/++COLggLQFu+99y6qq6s03fzHjh3DqlUrWQwUgDajQGpqClpbWzU7BxkZ83Hx4kUWAwWg3SiwaFGhJld/q3UTqqq+YRFQANrm3XfLUVWlrShw7tw5LFzIe/4UAIGiKEhJSdZUFFi4MBdnz57lyacAyNUocAKFhfmaWP2rq6tQUVHBk04BkF9SXl6OqqpvXLr5L126hPnz03myKQByoyiQnDxPmpeJOuJTWroCR48e5cmmAMiNOHHiBPLyXPMlmIcPH8bq1at4kikAcvMoUIZvvnGtKAAAqanJ/FkvCoB0JwrMm5foUlGgvLwM+/fv58mlAEj3o0COSzR/U1MT8vMX8qRSAKQnbNy4Ebt371L9cWRmZuD8+fM8oRQA6WkUSExMQHt7m2pX/y+//C9s3bqFJ5MCIL2NAjk56owC7e3tSEtL40mkAIhtUeAd7N69S3UCKCpahJMnT/AEUgDE1ihgNptV9V2Bb789hHXr1vLkUQDEPlHgOHJyslWx8nd1dSEpKQmdnZ08cRQAsRfvvPM2du2SPwqsW7cWhw7V8oRRAMT+USAObW2t0Okg5efqtxoLeLIoAOIIjh8/DoslS9rxJSfPQ1tbG08UBUAcRVlZGZqbz0m3+m/Zshnbt2/nCVIRbpwC9TFjxmswmYxSjaml5TzS03nPnwIgDmXw4MGIj4/D/33BThq6uq6gq4s/7MEIQBxKXl4ubr/9DigKpPoYjSaXfY8BBUCkYOLEifD3D5Cu+a99QkPDMHHiRJ4oCoDYGy8vL2RkZErb/Nc+Fks2Bg4cyBNGARB7kpqaqorGMplMyM1lFKAAiN0YOfJ3mDJlqvSr/7XPmDFhCA9nFKAAiM24u7sjL+/qL+goiqKaT1aWBT4+A3gCKQBiC2+8MRsPPPCgqppfURQYjUbk5OTwBFIApLfcf/8DmDMnUrXjDw0NQ3h4OE8kBUB6Q25uLtzd3VW3+v/yk5mZBR8fH55MCoD0hEmTJmHkyN+puvmvRQGLhVGAAiDdxsfHB0lJyapv/mufMWPGYMKECTyxFADpDmlp6TAajS51TBkZmYwCFAC5Fc888wwmTAh3mdX//6OACVlZFp5gCoD8M/r27YusLIvLNf+1T0jIGIwfzyhAAZAbEhUVjd/8ZrDLCkBRFKSnz2cUoADI9Tz66L/glVdedfnjNJlMWLAgiyecAiA/nwS9HhaLBXq93qVX/2uf4OAQjB07jideAvhGIAmYNu15DBs2HIqinTfqpKfPR3V1FU6fPs0C4A5Au9x1112IiZmriZX/+geEMjIyWQAUgLZJS0uHp6enJo/9ahQYyyJgBNAmQUHBCAgI1NTW/3pSU9NRXV3NKMAdgLbo168fUlJSNbf1v1EUSEubz4KgALRFTMxcDBo0SOgYZPkFn5CQEISFMQpQABph+PDhmDx5itCV99NPP0FGxnxpdgIpKanw9uYDQhSAi3PbbbchIyMTOp1OWLN1dLSjoCAfmzdXYvv27RJFAf6yEAXg4rz88nQ89NDDQpttzZo1OHXqFABgwYIMNDc3SyGBoKBghIaGsUgoANdk8ODBiIiIEDqGkydPYN26tT//uampSapf9ElOToG3tzeLhQJwPdLS5sPDo4/QVbagIB8XL1781bgqK6346qsvpYkCqamMAhSAizFu3Hg89dRTQpurqqoKX3zxxQ3Hl5mZKU0UCAwMwpgxoSwaCsA1MBgMiI83C22qK1eu3HSr39TUiPz8PGnuCsybl8woQAG4BnFx8TCZTELHUF5ejiNH/n7Tf6eyshI7dvxZijkzGo1ITk5l8VAA6mbEiBHCX/F15swZrFy5olvjzcpagJaWFkmiQCBCQkJYRBSAOvHw8EBKSprwRiopWYrz5893a8yNjY0oKFgoTRRISmIUoABUyquvvoYhQ4YIbaDvvvsOmzZV9GjclZWV+PrrnVIIwGAwICkpmcVEAaiL+++/Hy+99LLQMSiKgvz8PHR1dfX4v7VYsrq9a3A0gYGBCA5mFKAAVIJOp0NKSirc3NyErp5bt25BbW1tr46hoaEBRUWF0kSBxMQk4RdSKQDSLZ577t8wfPgTQhumtbUVS5cusek4rFYrdu36WpookJiYxOKiAOTGx8cHs2fPEd4wb765Bk1NTTYfT3a2BefPn5dCAgEBgQgKCmaRUQDyEh9vRv/+/YWO4aeffkJ5eZld/q6GhgYUFxdJM78JCYmMAhSAnIwaNfrnV3yJ/BQVLcLly5ftdlxWqxW7d++WJgokJCSy2CgAubj99tuRkJAgvEF27tyJ3bt32f34cnIsuHDhghQS8PcPQGBgEIuOApCHmTNnYdCgu4Q2xqVLlxy2XW9oaMCSJcXS3BWIj4+H0cgoQAFIwCOPPIJJkyYLH0dZ2UYcP37cYX+/1boJ1dVVUsy50WiC2ZzA4qMABE+gXo+kpGShr/hSFAWNjY1Yv36dw483NzdHoijgj8DAQBYhBSCOSZMm4+GHHxbeDMuWlaC9vd3hx1tfX4+SkqXSRIHY2HgYjUYWIgXgfAYNGoQZM2YIH0dtbS22bfvMaf8/q3UT9u7dI0kUMCI+3sxipACcT1ycGX369BW6AnZ2dmLxYuffp8/NzUFra6sUuwA/P3/4+wewICkA5xEQEICnn35aePFXVlrxww//7fTjr6+vx7JlJRJFgThGAQrAOXh69kNUVIzwom9pacHq1auEzcPVKLBXmgeEYmPjWJwUgOOJiHhDipdUrF37JzQ3NwsdQ35+njQ/MXY1CvizQCkAxzFs2DCMHz9e+Ip35Mjf8fHHHwmfj7q6OpSWrpAmCsTExDIKUACOwc3NDXFxV684iy70JUuKe/WiD0dFgZqafdJEgZiYuSxWCsD+TJ06DUOHDhVe5F999SUOHDggzbwoioKFC69GARkk4OvrBz8/PxYsBWA/Bg8ejOeff0H4OC5evIgVK1ZINz/19fVYtapUmvFER89lFKAA7EdsbJzwV3wpioKyso1oaKiXco6sViv276+RJgpER8ewcCkA2wkNDcMTT/yr8KKuq6uz24s+HBUFCgrypYkCzz7rC19fRgEKwAYMBiNmzpwlRUGXlq74hx/2lI26ujqsXr1KmrsCUVFRMBgYBSiAXhIREQEvLy/h4zhwYD927tyhijmrrLTi4MGD0gg8KiqahUwB9Jwnn3wSgYFBwlexK1euYPnyZaqZN0VRUFiYj/b2dkmiwLN49llfFjQF0H369OmDyMhoKQrYat2Eo0ePqmr+6urq8Oaba6SJApGRUTAYDCxsCqB7TJv2PO6++27hhXvu3Dls2PCWKufQat2Ev/zloBQC8PLywpw5USzs69Dd6l8ICQlXfvnn2tp9nDVCJGb48BG/+vO2bVYddwCEEAqAEEIBEEIoAEIoAEIIBUAIoQAIIRQAIYQCIIRoWQDXP0V0/VNGhBB56MlTgNwBEMIdACGEAiCEUACEEArgV/BCICHy09MLgNwBEMIdACGEAiCEUAC8DkCINvI/dwCEcAdgP+sQQsSv/g4TQHe3FYQQcfSkTxkBCGEE6L1dGAMIkWf739NdOncAhHAHYF8LEUKcv/o7TQC8GEiIfPSmL+0WAbgLIERdq79NAuAugBB1r/427wB4R4AQ8au/LYux3tGDI4TI2182C4BRgBD1bf0dtgPgLoAQdaz+dhPAjSxECRDi2Oa3x+7bbjsASoAQdTW/3SMArwcQIn/ud/g1AO4CCFFH/9hdAIwChMi/9XfoDoASIET+5ndoBKAECJG7+R1+DYASIETe5ne4ACgBQuRtfgBw2m27kJBw5Ub/vLZ2H888YeN3c/FU3Q7gVgfD3QBh84tpfqcKgBIgRK7md2oE6E4cYCQgWm58Zze/MAHwugBh84ttfCkEwN0A4aov9js0Unx552YSoAiIqza+6OaXRgAUAWHjUwDdFgFlQNTY9DI1vtQC6K4EKAKilsaXsfmlFkBPRUAhENkaXubGV40AbJEBpUCc3exqaHrVCsBWERDiaNT2WjzVv8OPMiBseg0LgEIgbHgKgFIgbHZCCCGEEEIIIYQQQgghhBBCCAH+F03I2mvFzbi3AAAAAElFTkSuQmCC"
local function decodeBase64(data)
local lookup = {}
local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
for i = 1, #alphabet do
lookup[alphabet:byte(i)] = i - 1
end
local out = {}
for i = 1, #data, 4 do
local a, b, c, d = data:byte(i, i + 3)
local n = lookup[a] * 262144 + lookup[b] * 4096 + (lookup[c] or 0) * 64 + (lookup[d] or 0)
local x, y, z = n // 65536, n // 256 % 256, n % 256
if d ~= 61 then
out[#out + 1] = string.char(x, y, z)
elseif c ~= 61 then
out[#out + 1] = string.char(x, y)
else
out[#out + 1] = string.char(x)
end
end
return table.concat(out)
end
pcall(function()
local getasset = (typeof(getcustomasset) == "function" and getcustomasset)
or (typeof(getsynasset) == "function" and getsynasset)
if type(writefile) ~= "function" or typeof(getasset) ~= "function" then
return
end
local folder = "XYZAssets"
if type(isfolder) == "function" and type(makefolder) == "function" and not isfolder(folder) then
makefolder(folder)
end
local path = folder .. "/XYZLogo.png"
local png = decodeBase64(EMBEDDED_LOGO)
local fresh = type(isfile) == "function" and type(readfile) == "function" and isfile(path) and readfile(path) == png
if not fresh then
writefile(path, png)
end
local asset = getasset(path)
if type(asset) == "string" and asset ~= "" then
Assets.Logo = asset
end
end)
local Fonts = Library.Fonts
local TOUCH = UserInputService.TouchEnabled and not UserInputService.KeyboardEnabled
Library.Touch = TOUCH
local SIDEBAR_WIDTH = 188
local HEADER_HEIGHT = 62
local TAB_TILE_HEIGHT = 34
local CARD_HEIGHT = TOUCH and 54 or 48
local CARD_HEIGHT_DESC = TOUCH and 70 or 64
local DROPDOWN_OPTION_HEIGHT = TOUCH and 40 or 34
local CHIP_HEIGHT = TOUCH and 34 or 28
local PROFILE_HEIGHT = 64
local SEARCH_WIDTH = 176
local STACK_BELOW = 440
-- Elements read their geometry from one of these. Cards stand alone on a tab,
-- rows sit flush inside a groupbox.
local METRICS = {
Card = {
Compact = false,
Height = CARD_HEIGHT,
DescHeight = CARD_HEIGHT_DESC,
Chip = CHIP_HEIGHT,
Option = DROPDOWN_OPTION_HEIGHT,
TextSize = 14,
DescSize = 13,
TitleHeight = 18,
DescGap = 20,
DescLine = 17,
Slider = CARD_HEIGHT_DESC,
},
Row = {
Compact = true,
Height = TOUCH and 40 or 32,
DescHeight = TOUCH and 54 or 46,
Chip = TOUCH and 30 or 24,
Option = TOUCH and 34 or 28,
TextSize = 13,
DescSize = 12,
TitleHeight = 16,
DescGap = 17,
DescLine = 15,
Slider = TOUCH and 54 or 46,
},
}
local tweenInfoCache = {}
-- Theme bindings: every instance property painted with a theme color is
-- remembered by theme key, so SetTheme can repaint the whole UI live.
-- Held strongly: an Instance's Lua handle can be collected while the Instance
-- still exists, which would silently drop it from a weak table. Destroyed
-- objects are pruned instead, once they have been parentless for two sweeps.
local themeBindings = {}
local themeHooks = {}
local themeLookup = {}
local orphanMarks = {}
local bindingCount = 0
local sweepAt = 4000
local function pruneTable(map, counted)
for object in pairs(map) do
if object.Parent == nil then
if orphanMarks[object] then
map[object] = nil
orphanMarks[object] = nil
if counted then
bindingCount -= 1
end
else
orphanMarks[object] = true
end
else
orphanMarks[object] = nil
end
end
end
local function sweepBindings()
pruneTable(themeBindings, true)
pruneTable(themeHooks, false)
sweepAt = math.max(4000, bindingCount * 2)
end
local function colorKey(color)
return math.floor(color.R * 255 + 0.5) .. "," .. math.floor(color.G * 255 + 0.5) .. "," .. math.floor(color.B * 255 + 0.5)
end
local function rebuildThemeLookup()
table.clear(themeLookup)
for index = #THEME_KEYS, 1, -1 do
local name = THEME_KEYS[index]
themeLookup[colorKey(Library.Theme[name])] = name
end
end
rebuildThemeLookup()
local function bindColors(object, props)
for property, value in pairs(props) do
if typeof(value) == "Color3" then
local name = themeLookup[colorKey(value)]
local bound = themeBindings[object]
if name then
if not bound then
bound = {}
themeBindings[object] = bound
bindingCount += 1
if bindingCount >= sweepAt then
sweepBindings()
end
end
bound[property] = name
elseif bound then
bound[property] = nil
end
end
end
end
-- Repaints every bound property from the colours on screen to the new theme.
-- One Heartbeat loop lerps each theme key once per frame and assigns the
-- result, instead of creating a tween per object, which hitched the frame the
-- theme changed on large UIs. A change mid-fade picks up from what is shown.
local shownTheme = table.clone(Library.Theme)
local themeDriver = nil
local function paintBinding(object, bound, colors)
for property, name in pairs(bound) do
local color = colors[name]
if color then
object[property] = color
end
end
end
local function paintBindings(colors)
for object, bound in pairs(themeBindings) do
pcall(paintBinding, object, bound, colors)
end
end
local function driveTheme(duration)
if themeDriver then
themeDriver:Disconnect()
themeDriver = nil
end
local from, colors = {}, {}
for _, name in ipairs(THEME_KEYS) do
local target = Library.Theme[name]
local current = shownTheme[name] or target
if current ~= target then
from[name] = current
colors[name] = current
end
end
local function finish()
for _, name in ipairs(THEME_KEYS) do
colors[name] = Library.Theme[name]
shownTheme[name] = Library.Theme[name]
end
paintBindings(colors)
end
if duration <= 0 or next(from) == nil then
finish()
return
end
local elapsed = 0
themeDriver = RunService.Heartbeat:Connect(function(deltaTime)
elapsed += deltaTime
if elapsed >= duration then
themeDriver:Disconnect()
themeDriver = nil
finish()
return
end
local alpha = 1 - (1 - elapsed / duration) ^ 5
for name, start in pairs(from) do
local color = start:Lerp(Library.Theme[name], alpha)
colors[name] = color
shownTheme[name] = color
end
paintBindings(colors)
end)
end
-- Runs fn now and again whenever the theme changes, while owner exists.
local function onTheme(owner, fn)
themeHooks[owner] = fn
fn()
end
local function tween(object, props, duration, style, direction, raw)
duration = duration or 0.2
if not raw then
bindColors(object, props)
end
if duration <= 0 then
for key, value in pairs(props) do
object[key] = value
end
return nil
end
style = style or Enum.EasingStyle.Quart
direction = direction or Enum.EasingDirection.Out
local key = duration .. style.Name .. direction.Name
local info = tweenInfoCache[key]
if not info then
info = TweenInfo.new(duration, style, direction)
tweenInfoCache[key] = info
end
local t = TweenService:Create(object, info, props)
t:Play()
return t
end
local function create(className, props, children)
local inst = Instance.new(className)
for key, value in pairs(props) do
if key ~= "Parent" then
inst[key] = value
end
end
bindColors(inst, props)
if children then
for _, child in ipairs(children) do
child.Parent = inst
end
end
if props.Parent then
inst.Parent = props.Parent
end
return inst
end
-- Corners in pixels follow the player's corner radius setting: each one
-- remembers the radius it was drawn at, and Library:SetCornerRadius rescales
-- them all. Scale corners (pills, circles) stay as they are.
Library.Roundness = 1
-- A strong table: weak keys would drop the Lua handles for corners nothing
-- else in Lua holds, and those corners would never rescale. Destroyed ones
-- are pruned whenever the radius changes.
Library._cornerBases = {}
local function corner(parent, radius)
radius = radius or UDim.new(0, 8)
local object = create("UICorner", {
CornerRadius = radius.Scale == 0 and UDim.new(0, math.floor(radius.Offset * Library.Roundness + 0.5)) or radius,
Parent = parent,
})
if radius.Scale == 0 then
Library._cornerBases[object] = radius.Offset
end
return object
end
-- A corner that ignores the setting, for shapes that animate their own
-- radius (the minimized island, the morph panels).
function Library._fixedCorner(parent, radius)
return create("UICorner", { CornerRadius = radius, Parent = parent })
end
-- The radius the window's own corners are drawn at right now.
function Library._radius(pixels)
return math.floor(pixels * Library.Roundness + 0.5)
end
local function stroke(parent, color, transparency, thickness)
return create("UIStroke", {
Color = color or Theme.Stroke,
Transparency = transparency or 0,
Thickness = thickness or 1,
ApplyStrokeMode = Enum.ApplyStrokeMode.Border,
Parent = parent,
})
end
local function padding(parent, left, right, top, bottom)
return create("UIPadding", {
PaddingLeft = UDim.new(0, left or 0),
PaddingRight = UDim.new(0, right or 0),
PaddingTop = UDim.new(0, top or 0),
PaddingBottom = UDim.new(0, bottom or 0),
Parent = parent,
})
end
local function label(props)
local defaults = {
BackgroundTransparency = 1,
TextColor3 = Theme.Text,
TextSize = 14,
FontFace = Fonts.Medium,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Center,
TextTruncate = Enum.TextTruncate.AtEnd,
}
for key, value in pairs(props) do
defaults[key] = value
end
return create("TextLabel", defaults)
end
local function glow(parent, size, position, transparency, rotation)
local image = create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = position,
Size = size,
BackgroundTransparency = 1,
Image = Assets.Glow,
ImageColor3 = Color3.fromRGB(226, 218, 230),
ImageTransparency = 1,
Visible = false,
ZIndex = 0,
Parent = parent,
})
local gradient = create("UIGradient", {
Rotation = rotation or 90,
Parent = image,
})
onTheme(gradient, function()
gradient.Color = ColorSequence.new(Color3.fromRGB(255, 255, 255), Theme.Accent)
end)
return image
end
local function edgeHighlight(parent)
return create("Frame", {
Position = UDim2.fromOffset(0, 0),
Size = UDim2.new(1, 0, 0, 1),
BackgroundColor3 = Color3.new(1, 1, 1),
BackgroundTransparency = 1,
BorderSizePixel = 0,
ZIndex = 0,
Parent = parent,
})
end
local CUSTOM_ICON_SCALE = 1.35
local function applyIcon(imageLabel, icon, fixedSize)
local image, rectOffset, rectSize, custom, tint = resolveIcon(icon)
if not image then
return
end
imageLabel.Image = image
imageLabel.ImageRectOffset = rectOffset or Vector2.zero
imageLabel.ImageRectSize = rectSize or Vector2.zero
-- XYZ logo preserves its original gradient colours
if icon == Assets.Logo then
tint = false
end
local keepColours = custom and not tint
imageLabel:SetAttribute("CustomIcon", keepColours or nil)
if keepColours then
-- Original colours, and out of the theme's reach.
local bound = themeBindings[imageLabel]
if bound then
bound.ImageColor3 = nil
end
tween(imageLabel, { ImageColor3 = Color3.new(1, 1, 1) }, 0)
end
-- Uploaded images usually carry transparent padding and read smaller than
-- the lucide set, so they are drawn larger. A table icon can pick its own
-- Scale. Brand logos pass fixedSize, their slots are already sized for them.
local scale = custom and not fixedSize and icon ~= Assets.Logo and (typeof(icon) == "table" and tonumber(icon.Scale) or CUSTOM_ICON_SCALE) or 1
local scaler = imageLabel:FindFirstChild("CustomIconScale")
if scale ~= 1 then
scaler = scaler or create("UIScale", { Name = "CustomIconScale", Parent = imageLabel })
scaler.Scale = scale
elseif scaler then
scaler:Destroy()
end
end
-- Selection state for icons: tinted icons change colour, custom images keep
-- their colours and dim instead.
local function tintIcon(imageLabel, color, active, duration)
if imageLabel:GetAttribute("CustomIcon") then
tween(imageLabel, { ImageTransparency = active and 0 or 0.4 }, duration)
else
tween(imageLabel, { ImageColor3 = color }, duration)
end
end
local function defaultParent()
local hidden
pcall(function()
if typeof(gethui) == "function" then
hidden = gethui()
end
end)
if typeof(hidden) == "Instance" then
return hidden
end
local core
pcall(function()
core = game:GetService("CoreGui")
local probe = Instance.new("Folder")
probe.Parent = core
probe:Destroy()
end)
if core then
return core
end
return LocalPlayer:WaitForChild("PlayerGui")
end
local insetFrame, insetValue = nil, Vector2.zero
local function pointerPosition()
local frame = time()
if insetFrame ~= frame then
insetFrame = frame
insetValue = GuiService:GetGuiInset()
end
return UserInputService:GetMouseLocation() - insetValue
end
local function isPress(input)
return input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch
end
local function usingTouch()
return UserInputService:GetLastInputType() == Enum.UserInputType.Touch
end
-- A press that turned into a scroll is not a tap. Returns a check to call
-- from the click handler.
local function tapGuard(button, getScroller)
local startCanvas
button.InputBegan:Connect(function(input)
if isPress(input) then
local frame = getScroller()
startCanvas = frame and frame.CanvasPosition
end
end)
return function()
local frame = getScroller()
return not (startCanvas and frame and (frame.CanvasPosition - startCanvas).Magnitude > 4)
end
end
local function pointInside(point, object)
local position, size = object.AbsolutePosition, object.AbsoluteSize
return point.X >= position.X and point.X <= position.X + size.X
and point.Y >= position.Y and point.Y <= position.Y + size.Y
end
local function isMove(input)
return input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch
end
local function glowIcon(parent, icon, color, position)
local holder = create("Frame", {
AnchorPoint = Vector2.new(0, 0.5),
Position = position,
Size = UDim2.fromOffset(16, 16),
BackgroundTransparency = 1,
Parent = parent,
})
local image = create("ImageLabel", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
ImageColor3 = color,
ScaleType = Enum.ScaleType.Fit,
Parent = holder,
})
applyIcon(image, icon)
return holder, image
end
local function arrowIndicator(parent, anchorX, offsetX)
local holder = create("Frame", {
AnchorPoint = Vector2.new(anchorX, 0.5),
Position = UDim2.new(anchorX, offsetX, 0.5, 0),
Size = UDim2.fromOffset(18, 18),
BackgroundTransparency = 1,
Parent = parent,
})
local indicator = {}
local image = create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromOffset(16, 16),
BackgroundTransparency = 1,
ImageColor3 = Theme.Muted,
ScaleType = Enum.ScaleType.Fit,
Parent = holder,
})
applyIcon(image, "chevron-down")
if image.Image ~= "" then
function indicator:Set(open)
tween(image, {
Rotation = open and 180 or 0,
ImageColor3 = open and Theme.Accent or Theme.Muted,
}, 0.3, Enum.EasingStyle.Quint)
end
return indicator
end
image:Destroy()
local function glyph(rotation, transparency)
return label({
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromOffset(18, 18),
Text = "›",
TextSize = 22,
TextXAlignment = Enum.TextXAlignment.Center,
TextColor3 = Theme.Muted,
TextTransparency = transparency,
Rotation = rotation,
Parent = holder,
})
end
local down = glyph(90, 0)
local up = glyph(270, 1)
function indicator:Set(open)
tween(down, { TextTransparency = open and 1 or 0 }, 0.2)
tween(up, { TextTransparency = open and 0 or 1, TextColor3 = open and Theme.Accent or Theme.Muted }, 0.2)
end
return indicator
end
local ripplePool = {}
local function ripple(button)
local mouse = pointerPosition()
local diameter = math.max(button.AbsoluteSize.X, button.AbsoluteSize.Y) * 2.2
local circle = table.remove(ripplePool)
-- A pooled circle can't be reparented if it was destroyed along the way.
if circle and not pcall(function()
circle.Parent = nil
end) then
circle = nil
end
if not circle then
circle = create("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundColor3 = Theme.Accent,
BorderSizePixel = 0,
})
corner(circle, UDim.new(1, 0))
end
circle.Position = UDim2.fromOffset(mouse.X - button.AbsolutePosition.X, mouse.Y - button.AbsolutePosition.Y)
circle.Size = UDim2.fromOffset(0, 0)
circle.BackgroundColor3 = Theme.Accent
circle.BackgroundTransparency = 0.82
circle.ZIndex = button.ZIndex + 1
circle.Parent = button
tween(circle, { Size = UDim2.fromOffset(diameter, diameter), BackgroundTransparency = 1 }, 0.55)
task.delay(0.55, function()
-- A button destroyed mid-ripple takes the circle with it: drop it.
if circle.Parent ~= button or not button.Parent then
pcall(circle.Destroy, circle)
return
end
circle.Parent = nil
if #ripplePool < 4 then
table.insert(ripplePool, circle)
else
circle:Destroy()
end
end)
end
local function flashStroke(strokeObject)
if not strokeObject or strokeObject:GetAttribute("Hidden") then
return
end
tween(strokeObject, { Color = Theme.Accent, Transparency = 0.2 }, 0.08)
task.delay(0.12, function()
tween(strokeObject, { Color = Theme.Stroke, Transparency = 0 }, 0.35)
end)
end
local function hoverStroke(target, strokeObject)
target.MouseEnter:Connect(function()
tween(strokeObject, { Color = Theme.StrokeHover }, 0.12)
end)
target.MouseLeave:Connect(function()
tween(strokeObject, { Color = Theme.Stroke }, 0.25)
end)
end
local KEY_NAMES = {
[Enum.KeyCode.LeftControl] = "LCtrl",
[Enum.KeyCode.RightControl] = "RCtrl",
[Enum.KeyCode.LeftShift] = "LShift",
[Enum.KeyCode.RightShift] = "RShift",
[Enum.KeyCode.LeftAlt] = "LAlt",
[Enum.KeyCode.RightAlt] = "RAlt",
[Enum.KeyCode.Return] = "Enter",
[Enum.KeyCode.Escape] = "Esc",
[Enum.KeyCode.Backspace] = "Backspace",
}
local function keyName(keyCode)
if keyCode == nil then
return "None"
end
return KEY_NAMES[keyCode] or keyCode.Name
end
local function safeCall(callback, ...)
if type(callback) ~= "function" then
return
end
local ok, err = pcall(callback, ...)
if not ok then
warn("[AirFlow] callback error: " .. tostring(err))
end
end
-- Runs a user callback on its own thread and waits for it with task.wait.
-- A callback that yields inside an engine call (InvokeServer and the like)
-- resumes at game identity, which can no longer touch the GUI in CoreGui;
-- keeping that on a separate thread leaves the caller's identity intact.
local function isolatedCall(callback)
local result = nil
task.spawn(function()
result = table.pack(pcall(callback))
end)
while result == nil do
task.wait()
end
return table.unpack(result, 1, result.n)
end
local function numberFormat(min, max, step)
local decimals = 0
local text = tostring(step)
local dot = text:find("%.")
if dot then
decimals = #text - dot
end
local pattern = "%." .. decimals .. "f"
return {
snap = function(value)
value = math.floor(value / step + 0.5) * step
return math.clamp(value, min, max)
end,
format = function(value)
return string.format(pattern, value)
end,
}
end
local function metricsOf(tab)
return tab._metrics or METRICS.Card
end
-- Width of a value control inside a groupbox row. Every dropdown and input in
-- a groupbox uses the same share of the row, so their left edges line up.
local function rowControlWidth(frame, window)
local width = frame.AbsoluteSize.X / window.Scale.Scale
return math.clamp(math.floor(width * 0.48), 90, 220)
end
local function card(tab, className, height, opts)
local compact = metricsOf(tab).Compact
local props = {
Size = UDim2.new(1, 0, 0, height),
BackgroundColor3 = Theme.Surface2,
BackgroundTransparency = compact and 1 or 0,
BorderSizePixel = 0,
LayoutOrder = tab:_nextOrder(),
Parent = tab.List,
}
if className == "TextButton" then
props.AutoButtonColor = false
props.Text = ""
end
local frame = create(className, props)
frame:SetAttribute("NoDrag", true)
corner(frame)
local frameStroke = stroke(frame, Theme.Stroke, compact and 1 or 0)
if compact then
frameStroke:SetAttribute("Hidden", true)
end
return frame, frameStroke
end
local function titleDesc(parent, name, desc, rightReserve, metrics)
metrics = metrics or METRICS.Card
local title, descLabel
if desc then
local top = math.floor((metrics.DescHeight - (metrics.DescGap + metrics.DescLine)) / 2)
title = label({
Position = UDim2.fromOffset(14, top),
Size = UDim2.new(1, -(14 + rightReserve), 0, metrics.TitleHeight),
Text = name,
TextSize = metrics.TextSize,
Parent = parent,
})
descLabel = label({
Position = UDim2.fromOffset(14, top + metrics.DescGap),
Size = UDim2.new(1, -(14 + rightReserve), 0, metrics.DescLine),
Text = desc,
TextSize = metrics.DescSize,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
Parent = parent,
})
else
title = label({
Position = UDim2.fromOffset(14, 0),
Size = UDim2.new(1, -(14 + rightReserve), 1, 0),
Text = name,
TextSize = metrics.TextSize,
Parent = parent,
})
end
return title, descLabel
end
local function beginElement(tab, opts, map, className, rightReserve, fallbackName)
opts = normalize(opts, map)
local metrics = metricsOf(tab)
local height = opts.Desc and metrics.DescHeight or metrics.Height
local frame, frameStroke = card(tab, className or "Frame", height, opts)
hoverStroke(frame, frameStroke)
local title, descLabel
if rightReserve then
title, descLabel = titleDesc(frame, opts.Name or fallbackName, opts.Desc, rightReserve, metrics)
end
return opts, frame, frameStroke, height, title, descLabel
end
local function reserveRight(title, descLabel, width)
if title then
title.Size = UDim2.new(1, -(14 + width), title.Size.Y.Scale, title.Size.Y.Offset)
end
if descLabel then
descLabel.Size = UDim2.new(1, -(14 + width), 0, descLabel.Size.Y.Offset)
end
end
local Tab = {}
Tab.__index = Tab
function Tab:_nextOrder()
self._order = self._order + 1
return self._order
end
function Tab:Section(text)
local visible = not (type(text) == "table" and text.Visible == false)
if type(text) == "table" then
text = text.Name or text.Title or ""
end
local inset = metricsOf(self).Compact and 14 or 2
local holder = create("Frame", {
Size = UDim2.new(1, 0, 0, 28),
BackgroundTransparency = 1,
LayoutOrder = self:_nextOrder(),
Parent = self.List,
})
local text_ = label({
Position = UDim2.fromOffset(inset, 8),
Size = UDim2.new(0, 0, 0, 16),
AutomaticSize = Enum.AutomaticSize.X,
Text = string.upper(text),
TextSize = 12,
TextColor3 = Theme.Muted,
TextTruncate = Enum.TextTruncate.None,
Parent = holder,
})
local ruleInset = inset == 2 and 0 or inset
local rule = create("Frame", {
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, -ruleInset, 0, 16),
Size = UDim2.new(1, -12, 0, 1),
BackgroundColor3 = Theme.Stroke,
BorderSizePixel = 0,
Parent = holder,
})
local function fitRule()
rule.Size = UDim2.new(1, -(text_.AbsoluteSize.X / self.Window.Scale.Scale + inset + 12 + ruleInset), 0, 1)
end
text_:GetPropertyChangedSignal("AbsoluteSize"):Connect(fitRule)
task.defer(fitRule)
return finishElement(self, { Visible = visible }, {
Set = function(_, value)
text_.Text = string.upper(value)
end,
}, holder, "Section")
end
function Tab:Divider(opts)
local caption = type(opts) == "table" and (opts.Text or opts.Name) or type(opts) == "string" and opts or nil
if caption then
-- A centred caption with a rule on each side.
local inset = metricsOf(self).Compact and 14 or 2
local holder = create("Frame", {
Size = UDim2.new(1, 0, 0, 18),
BackgroundTransparency = 1,
LayoutOrder = self:_nextOrder(),
Parent = self.List,
})
local text_ = label({
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.new(0, 0, 0, 14),
AutomaticSize = Enum.AutomaticSize.X,
Text = caption,
TextSize = 11,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextTruncate = Enum.TextTruncate.None,
Parent = holder,
})
padding(text_, 8, 8)
local rules = {}
for index, anchor in ipairs({ 0, 1 }) do
rules[index] = create("Frame", {
AnchorPoint = Vector2.new(anchor, 0.5),
Position = UDim2.new(anchor, anchor == 0 and inset or -inset, 0.5, 0),
Size = UDim2.new(0.5, -inset, 0, 1),
BackgroundColor3 = Theme.Stroke,
BorderSizePixel = 0,
Parent = holder,
})
end
local function fitRules()
local half = text_.AbsoluteSize.X / self.Window.Scale.Scale / 2
for _, rule in ipairs(rules) do
rule.Size = UDim2.new(0.5, -(inset + half), 0, 1)
end
end
text_:GetPropertyChangedSignal("AbsoluteSize"):Connect(fitRules)
task.defer(fitRules)
return finishElement(self, { Visible = not (type(opts) == "table" and opts.Visible == false) }, {
Set = function(_, value)
text_.Text = tostring(value)
end,
}, holder, "Divider")
end
local line = create("Frame", {
Size = UDim2.new(1, 0, 0, 1),
BackgroundColor3 = Theme.Stroke,
BorderSizePixel = 0,
LayoutOrder = self:_nextOrder(),
Parent = self.List,
})
if metricsOf(self).Compact then
line.BackgroundTransparency = 1
line.Size = UDim2.new(1, 0, 0, 9)
create("Frame", {
Position = UDim2.fromOffset(14, 4),
Size = UDim2.new(1, -28, 0, 1),
BackgroundColor3 = Theme.Stroke,
BorderSizePixel = 0,
Parent = line,
})
end
return finishElement(self, { Visible = not (type(opts) == "table" and opts.Visible == false) }, {}, line, "Divider")
end
function Tab:Label(opts)
opts = normalize(opts, { Name = "Text", Title = "Text" })
local text_ = label({
Size = UDim2.new(1, 0, 0, 18),
Text = Library._rich(opts.Text or ""),
TextSize = 13,
FontFace = Fonts.Regular,
TextColor3 = Library._color(opts.Color) or Theme.Muted,
RichText = true,
LayoutOrder = self:_nextOrder(),
Parent = self.List,
})
if metricsOf(self).Compact then
text_.TextSize = 12
padding(text_, 14, 14)
else
padding(text_, 2)
end
local handle = {
Set = function(_, value)
text_.Text = Library._rich(value)
end,
Get = function()
return text_.Text
end,
SetColor = function(_, color)
tween(text_, { TextColor3 = Library._color(color) or Theme.Muted }, 0.25)
end,
}
if type(opts.Update) == "function" then
local rate = math.max(tonumber(opts.UpdateRate) or 1, 0.05)
local running = true
handle._listeners = handle._listeners or {}
table.insert(handle._listeners, function()
running = false
end)
task.spawn(function()
while running and text_.Parent do
local ok, value = isolatedCall(opts.Update)
if ok and value ~= nil then
text_.Text = Library._rich(value)
elseif not ok then
warn("[AirFlow] label update error: " .. tostring(value))
end
task.wait(rate)
end
end)
function handle:SetUpdateRate(seconds)
rate = math.max(tonumber(seconds) or rate, 0.05)
end
end
return finishElement(self, opts, handle, text_, "Label")
end
function Tab:Paragraph(opts)
opts = normalize(opts, { Title = "Name" })
local frame = card(self, "Frame", 0, opts)
frame.AutomaticSize = Enum.AutomaticSize.Y
if metricsOf(self).Compact then
padding(frame, 14, 14, 6, 8)
else
padding(frame, 14, 14, 11, 12)
end
create("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 4),
Parent = frame,
})
label({
Size = UDim2.new(1, 0, 0, 14),
Text = opts.Name or "",
LayoutOrder = 1,
Parent = frame,
})
local content = label({
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
Text = Library._rich(opts.Content or ""),
TextSize = 13,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextWrapped = true,
TextTruncate = Enum.TextTruncate.None,
TextYAlignment = Enum.TextYAlignment.Top,
RichText = true,
LayoutOrder = 2,
Parent = frame,
})
return finishElement(self, opts, {
Set = function(_, value)
content.Text = Library._rich(value)
end,
}, frame, "Paragraph")
end
local function rowButton(tab, opts)
local primary = opts.Style == "Primary"
local boxHeight = TOUCH and 34 or 28
local height = boxHeight + 6 + (opts.Desc and 16 or 0)
local button = card(tab, "TextButton", height, opts)
local box = create("Frame", {
Position = UDim2.fromOffset(14, 3),
Size = UDim2.new(1, -28, 0, boxHeight),
BackgroundColor3 = primary and Theme.Accent or Theme.Surface3,
BackgroundTransparency = primary and 0.12 or 0.45,
BorderSizePixel = 0,
ClipsDescendants = true,
Parent = button,
})
corner(box, UDim.new(0, 6))
local boxStroke = stroke(box, primary and Theme.Accent or Theme.Stroke, primary and 0.4 or 0)
local row = create("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundTransparency = 1,
Parent = box,
})
create("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 6),
Parent = row,
})
local textColor = primary and Theme.AccentDark or Theme.Text
if opts.Icon then
local image = create("ImageLabel", {
Size = UDim2.fromOffset(14, 14),
BackgroundTransparency = 1,
ImageColor3 = primary and Theme.AccentDark or Theme.Accent,
ScaleType = Enum.ScaleType.Fit,
LayoutOrder = 1,
Parent = row,
})
applyIcon(image, opts.Icon)
end
local title = label({
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
Text = opts.Name or "Button",
TextSize = 13,
TextColor3 = textColor,
TextTruncate = Enum.TextTruncate.None,
LayoutOrder = 2,
Parent = row,
})
if opts.Desc then
label({
Position = UDim2.fromOffset(14, boxHeight + 6),
Size = UDim2.new(1, -28, 0, 14),
Text = opts.Desc,
TextSize = 12,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
Parent = button,
})
end
local restBg = primary and 0.12 or 0.45
button.MouseEnter:Connect(function()
tween(box, { BackgroundTransparency = primary and 0 or 0.2 }, 0.12)
tween(boxStroke, primary and { Transparency = 0 } or { Color = Theme.StrokeHover }, 0.12)
end)
button.MouseLeave:Connect(function()
tween(box, { BackgroundTransparency = restBg }, 0.25)
tween(boxStroke, primary and { Transparency = 0.4 } or { Color = Theme.Stroke }, 0.25)
end)
button.MouseButton1Click:Connect(function()
ripple(box)
safeCall(opts.Callback)
end)
return finishElement(tab, opts, {
SetText = function(_, value)
title.Text = value
end,
}, button, "Button")
end
function Tab:Button(opts)
opts = normalize(opts, { Title = "Name", Description = "Desc" })
if metricsOf(self).Compact then
return rowButton(self, opts)
end
local primary = opts.Style == "Primary"
local height = opts.Desc and CARD_HEIGHT_DESC or CARD_HEIGHT
local button, buttonStroke = card(self, "TextButton", height, opts)
button.ClipsDescendants = true
if primary then
tween(button, { BackgroundColor3 = Theme.Accent, BackgroundTransparency = 0.12 }, 0)
tween(buttonStroke, { Color = Theme.Accent }, 0)
buttonStroke.Transparency = 0.4
button.MouseEnter:Connect(function()
tween(button, { BackgroundTransparency = 0 }, 0.12)
tween(buttonStroke, { Transparency = 0 }, 0.12)
end)
button.MouseLeave:Connect(function()
tween(button, { BackgroundTransparency = 0.12 }, 0.25)
tween(buttonStroke, { Transparency = 0.4 }, 0.25)
end)
else
hoverStroke(button, buttonStroke)
end
local textColor = primary and Theme.AccentDark or Theme.Text
local iconOffset = 0
if opts.Icon then
glowIcon(button, opts.Icon, primary and Theme.AccentDark or Theme.Accent, UDim2.new(0, 14, 0.5, 0))
iconOffset = 26
end
local title = titleDesc(button, opts.Name or "Button", opts.Desc, 30)
if iconOffset > 0 then
for _, child in ipairs(button:GetChildren()) do
if child:IsA("TextLabel") then
child.Position = child.Position + UDim2.fromOffset(iconOffset, 0)
child.Size = child.Size - UDim2.fromOffset(iconOffset, 0)
end
end
end
if primary then
for _, child in ipairs(button:GetChildren()) do
if child:IsA("TextLabel") then
tween(child, { TextColor3 = textColor }, 0)
end
end
end
glowIcon(button, BUTTON_HINT_ICON, primary and Theme.AccentDark or Theme.Muted, UDim2.new(1, -30, 0.5, 0))
button.MouseButton1Click:Connect(function()
ripple(button)
safeCall(opts.Callback)
end)
return finishElement(self, opts, {
SetText = function(_, value)
title.Text = value
end,
}, button, "Button")
end
function Tab:Toggle(opts)
local button, buttonStroke
opts, button, buttonStroke = beginElement(self, opts, { Title = "Name", Description = "Desc", CurrentValue = "Default", Value = "Default" }, "TextButton", 56, "Toggle")
local pill = create("Frame", {
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, -14, 0.5, 0),
Size = TOUCH and UDim2.fromOffset(44, 24) or UDim2.fromOffset(36, 20),
BackgroundColor3 = Theme.Surface3,
BorderSizePixel = 0,
Parent = button,
})
corner(pill, UDim.new(1, 0))
stroke(pill, Theme.Stroke)
local knob = create("Frame", {
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, 3, 0.5, 0),
Size = TOUCH and UDim2.fromOffset(18, 18) or UDim2.fromOffset(14, 14),
BackgroundColor3 = Theme.Muted,
BorderSizePixel = 0,
Parent = pill,
})
corner(knob, UDim.new(1, 0))
local pillGlow = create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.new(1, 24, 1, 24),
BackgroundTransparency = 1,
Image = Assets.Glow,
ImageColor3 = Theme.Accent,
ImageTransparency = 1,
ZIndex = 0,
Parent = pill,
})
local self_ = { Value = opts.Default == true }
local function render(animate)
local on = self_.Value
local duration = animate and 0.25 or 0
tween(pill, { BackgroundColor3 = on and Theme.Accent or Theme.Surface3 }, duration)
tween(pillGlow, { ImageTransparency = on and 0.75 or 1 }, duration)
local knobSize = TOUCH and 18 or 14
tween(knob, {
Position = on and UDim2.new(0, (TOUCH and 44 or 36) - 3 - knobSize, 0.5, 0) or UDim2.new(0, 3, 0.5, 0),
BackgroundColor3 = on and Theme.AccentDark or Theme.Muted,
}, duration, Enum.EasingStyle.Back)
if animate then
tween(knob, { Size = UDim2.fromOffset(knobSize + 4, knobSize - 2) }, 0.08)
task.delay(0.08, function()
tween(knob, { Size = UDim2.fromOffset(knobSize, knobSize) }, 0.2, Enum.EasingStyle.Back)
end)
end
end
local fromClick = false
function self_:Set(value, silent)
value = value == true
if value == self_.Value then
return
end
self_.Value = value
render(true)
if not fromClick then
flashStroke(buttonStroke)
end
if not silent then
safeCall(opts.Callback, value)
end
end
function self_:Get()
return self_.Value
end
render(false)
button.MouseButton1Click:Connect(function()
fromClick = true
self_:Set(not self_.Value)
fromClick = false
end)
local handle = finishElement(self, opts, self_, button, "Toggle")
if self_.Value then
safeCall(opts.Callback, true)
end
return handle
end
function Tab:Slider(opts)
opts = normalize(opts, { Title = "Name", Description = "Desc", CurrentValue = "Default", Value = "Default", Increment = "Step" })
local min = opts.Min or 0
local max = opts.Max or 100
local step = opts.Step or 1
local suffix = opts.Suffix or ""
local numbers = numberFormat(min, max, step)
local metrics = metricsOf(self)
local compact = metrics.Compact
local height = metrics.Slider
local frame, frameStroke = card(self, "Frame", height, opts)
hoverStroke(frame, frameStroke)
label({
Position = UDim2.fromOffset(14, compact and 5 or 12),
Size = UDim2.new(1, -120, 0, 18),
Text = opts.Name or "Slider",
TextSize = metrics.TextSize,
Parent = frame,
})
local valueChip = create("Frame", {
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, compact and -14 or -10, 0, compact and 2 or 9),
Size = UDim2.fromOffset(40, 24),
BackgroundColor3 = Theme.Surface,
BorderSizePixel = 0,
ClipsDescendants = true,
Parent = frame,
})
corner(valueChip, UDim.new(0, 6))
local chipStroke = stroke(valueChip)
local valueLabel = label({
Size = UDim2.new(1, 0, 1, 0),
TextSize = 13,
TextColor3 = Theme.Accent,
TextXAlignment = Enum.TextXAlignment.Center,
TextTruncate = Enum.TextTruncate.None,
Parent = valueChip,
})
local valueBox = create("TextBox", {
Position = UDim2.fromOffset(9, 0),
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundTransparency = 1,
Text = "",
TextColor3 = Theme.Text,
TextSize = 13,
FontFace = Fonts.Medium,
TextXAlignment = Enum.TextXAlignment.Left,
ClearTextOnFocus = false,
Visible = false,
Parent = valueChip,
})
create("UISizeConstraint", { MinSize = Vector2.new(14, 0), Parent = valueBox })
local suffixLabel = label({
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
Text = suffix,
TextSize = 13,
TextColor3 = Theme.Accent,
TextTruncate = Enum.TextTruncate.None,
Visible = false,
Parent = valueChip,
})
local chipButton = create("TextButton", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
Text = "",
AutoButtonColor = false,
ZIndex = 2,
Parent = valueChip,
})
local editing = false
local function fitChip(instant)
local width
if editing then
width = 9 + valueBox.AbsoluteSize.X / (self.Window.Scale.Scale) + suffixLabel.TextBounds.X + 9
suffixLabel.Position = UDim2.fromOffset(9 + valueBox.AbsoluteSize.X / (self.Window.Scale.Scale), 0)
else
width = valueLabel.TextBounds.X + 18
end
width = math.max(width, 36)
if instant then
valueChip.Size = UDim2.fromOffset(width, 24)
else
tween(valueChip, { Size = UDim2.fromOffset(width, 24) }, 0.2)
end
end
valueLabel:GetPropertyChangedSignal("TextBounds"):Connect(function()
if not editing then
fitChip(false)
end
end)
valueBox:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
if editing then
fitChip(false)
end
end)
local track = create("Frame", {
Position = UDim2.new(0, 14, 0, height - (compact and 13 or 18)),
Size = UDim2.new(1, -28, 0, 5),
BackgroundColor3 = Theme.Surface3,
BorderSizePixel = 0,
Parent = frame,
})
corner(track, UDim.new(1, 0))
local fill = create("Frame", {
Size = UDim2.new(0, 0, 1, 0),
BackgroundColor3 = Theme.Accent,
BorderSizePixel = 0,
Parent = track,
})
corner(fill, UDim.new(1, 0))
local knobGlow = create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0, 0, 0.5, 0),
Size = UDim2.fromOffset(28, 28),
BackgroundTransparency = 1,
Image = Assets.Glow,
ImageColor3 = Theme.Accent,
ImageTransparency = 0.85,
ZIndex = 2,
Parent = track,
})
local knob = create("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0, 0, 0.5, 0),
Size = UDim2.fromOffset(12, 12),
BackgroundColor3 = Theme.Accent,
BorderSizePixel = 0,
ZIndex = 3,
Parent = track,
})
corner(knob, UDim.new(1, 0))
local hit = create("TextButton", {
Position = UDim2.new(0, 8, 0, height - (compact and 13 or 18) - (TOUCH and 19 or 13)),
Size = UDim2.new(1, -16, 0, TOUCH and 40 or 28),
BackgroundTransparency = 1,
Text = "",
AutoButtonColor = false,
ZIndex = 4,
Parent = frame,
})
local self_ = { Value = math.clamp(opts.Default or min, min, max) }
local dragging = false
chipButton.MouseEnter:Connect(function()
tween(chipStroke, { Color = Theme.StrokeHover }, 0.12)
end)
chipButton.MouseLeave:Connect(function()
if not valueBox:IsFocused() then
tween(chipStroke, { Color = Theme.Stroke }, 0.2)
end
end)
chipButton.MouseButton1Click:Connect(function()
editing = true
valueBox.Text = numbers.format(self_.Value)
valueLabel.Visible = false
valueBox.Visible = true
suffixLabel.Visible = suffix ~= ""
chipButton.Visible = false
tween(chipStroke, { Color = Theme.StrokeHover }, 0.12)
valueBox:CaptureFocus()
task.defer(fitChip, false)
end)
valueBox.FocusLost:Connect(function()
local typed = tonumber(valueBox.Text)
editing = false
valueBox.Visible = false
suffixLabel.Visible = false
valueLabel.Visible = true
chipButton.Visible = true
tween(chipStroke, { Color = Theme.Stroke }, 0.2)
if typed then
self_:Set(typed)
end
fitChip(false)
end)
hit.MouseEnter:Connect(function()
if not dragging then
tween(knobGlow, { ImageTransparency = 0.78, Size = UDim2.fromOffset(34, 34) }, 0.15)
end
end)
hit.MouseLeave:Connect(function()
if not dragging then
tween(knobGlow, { ImageTransparency = 0.85, Size = UDim2.fromOffset(28, 28) }, 0.2)
end
end)
local snap = numbers.snap
local function fraction()
return (max - min) == 0 and 0 or (self_.Value - min) / (max - min)
end
local function moveTo(frac, duration, style)
style = style or Enum.EasingStyle.Linear
tween(fill, { Size = UDim2.new(frac, 0, 1, 0) }, duration, style)
tween(knob, { Position = UDim2.new(frac, 0, 0.5, 0) }, duration, style)
tween(knobGlow, { Position = UDim2.new(frac, 0, 0.5, 0) }, duration, style)
end
local function render(duration, style)
moveTo(fraction(), duration, style)
valueLabel.Text = numbers.format(self_.Value) .. suffix
end
local function settleTo(previousFrac)
local target = fraction()
local width = math.max(track.AbsoluteSize.X, 1)
local overshoot = (target >= previousFrac and 1 or -1) * (5 / width)
moveTo(math.clamp(target + overshoot, 0, 1), 0.22, Enum.EasingStyle.Quint)
task.delay(0.22, function()
if not dragging then
moveTo(fraction(), 0.18, Enum.EasingStyle.Quint)
end
end)
valueLabel.Text = numbers.format(self_.Value) .. suffix
end
local function bounceKnob()
tween(knob, { Size = UDim2.fromOffset(14, 12) }, 0.08)
tween(knobGlow, { Size = UDim2.fromOffset(32, 32), ImageTransparency = 0.78 }, 0.12)
task.delay(0.1, function()
tween(knob, { Size = UDim2.fromOffset(12, 12) }, 0.25, Enum.EasingStyle.Quint)
tween(knobGlow, { Size = UDim2.fromOffset(28, 28), ImageTransparency = 0.85 }, 0.25)
end)
end
function self_:Set(value, silent)
value = snap(tonumber(value) or min)
if value == self_.Value then
return
end
local previousFrac = fraction()
self_.Value = value
if dragging then
render(0.05)
else
settleTo(previousFrac)
bounceKnob()
flashStroke(frameStroke)
end
if not silent then
safeCall(opts.Callback, value)
end
end
function self_:Get()
return self_.Value
end
local function updateFromX(x)
local frac = math.clamp((x - track.AbsolutePosition.X) / track.AbsoluteSize.X, 0, 1)
self_:Set(min + (max - min) * frac)
end
hit.InputBegan:Connect(function(input)
if isPress(input) then
dragging = true
self_.Dragging = true
tween(knob, { Size = UDim2.fromOffset(16, 16) }, 0.15, Enum.EasingStyle.Back)
tween(knobGlow, { Size = UDim2.fromOffset(44, 44), ImageTransparency = 0.7 }, 0.15)
updateFromX(pointerPosition().X)
end
end)
self.Window:_listen("Changed", function(input)
if dragging and isMove(input) then
updateFromX(pointerPosition().X)
end
end, self_)
self.Window:_listen("Ended", function(input)
if dragging and isPress(input) then
dragging = false
self_.Dragging = false
tween(knob, { Size = UDim2.fromOffset(12, 12) }, 0.2)
tween(knobGlow, { Size = UDim2.fromOffset(28, 28), ImageTransparency = 0.85 }, 0.2)
safeCall(opts.OnRelease, self_.Value)
end
end, self_)
self_.Value = snap(self_.Value)
render(0)
task.defer(fitChip, true)
return finishElement(self, opts, self_, frame, "Slider")
end
function Tab:Dropdown(opts)
opts = normalize(opts, { Title = "Name", Description = "Desc", CurrentOption = "Default", Value = "Default", MultipleOptions = "Multi", Values = "Options" })
if opts.Multi and type(opts.Default) ~= "table" and opts.Default ~= nil then
opts.Default = { opts.Default }
elseif not opts.Multi and type(opts.Default) == "table" then
opts.Default = opts.Default[1]
end
local multi = opts.Multi == true
local options = opts.Options or {}
local frame, frameStroke, height
opts, frame, frameStroke, height = beginElement(self, opts, {}, "Frame")
local metrics = metricsOf(self)
local compact = metrics.Compact
local chipHeight = metrics.Chip
local window = self.Window
local ROW = TOUCH and 40 or 30
local MAX_ROWS = opts.MaxRows or 6
-- Stacked: the chip spans the whole row, with the name (if any) above it.
local stacked = compact and opts.Stacked == true
local stackTop = 2
if stacked then
stackTop = (opts.Name or "") ~= "" and 20 or 2
chipHeight = TOUCH and 34 or 30
height = stackTop + chipHeight + 4
frame.Size = UDim2.new(1, 0, 0, height)
end
local header = create("TextButton", {
Size = UDim2.new(1, 0, 0, height),
BackgroundTransparency = 1,
Text = "",
AutoButtonColor = false,
Parent = frame,
})
local titleLabel, descLabel
if not stacked then
titleLabel, descLabel = titleDesc(header, opts.Name or "Dropdown", opts.Desc, 180, metrics)
elseif stackTop > 2 then
titleLabel = label({
Position = UDim2.fromOffset(14, 1),
Size = UDim2.new(1, -28, 0, 16),
Text = opts.Name,
TextSize = metrics.TextSize,
Parent = header,
})
end
local chip = create("Frame", {
AnchorPoint = stacked and Vector2.zero or Vector2.new(1, 0.5),
Position = stacked and UDim2.fromOffset(14, stackTop) or UDim2.new(1, compact and -14 or -10, 0.5, 0),
Size = UDim2.fromOffset(60, chipHeight),
BackgroundColor3 = Theme.Surface,
BorderSizePixel = 0,
ClipsDescendants = true,
Parent = header,
})
corner(chip, UDim.new(0, 6))
local chipStroke = stroke(chip)
local valueLabel = label({
Position = UDim2.fromOffset(10, 0),
Size = UDim2.new(1, -34, 1, 0),
TextColor3 = Theme.Muted,
TextSize = 13,
TextTruncate = Enum.TextTruncate.None,
ClipsDescendants = true,
Parent = chip,
})
local arrow = arrowIndicator(chip, 1, -6)
local measure = label({
Size = UDim2.fromOffset(0, chipHeight),
AutomaticSize = Enum.AutomaticSize.X,
TextSize = 13,
Visible = false,
Parent = chip,
})
local fullText = ""
local function trimmed()
local maxText = (stacked and frame.AbsoluteSize.X / window.Scale.Scale - 28 or compact and rowControlWidth(frame, window) or 190) - 10 - 34
measure.Text = fullText
if measure.TextBounds.X <= maxText then
return fullText
end
local text = fullText
while #text > 1 do
text = text:sub(1, -2)
measure.Text = text .. ".."
if measure.TextBounds.X <= maxText then
return text .. ".."
end
end
return ".."
end
local function fitChip(instant)
if stacked then
chip.Size = UDim2.new(1, -28, 0, chipHeight)
return
end
local width
if compact then
width = rowControlWidth(frame, window)
else
width = math.clamp(valueLabel.TextBounds.X + 10 + 34, 60, 190)
end
reserveRight(titleLabel, descLabel, width + (compact and 24 or 20))
if instant then
chip.Size = UDim2.fromOffset(width, chipHeight)
else
tween(chip, { Size = UDim2.fromOffset(width, chipHeight) }, 0.2)
end
end
valueLabel:GetPropertyChangedSignal("TextBounds"):Connect(function()
if not compact then
fitChip(false)
end
end)
if compact then
-- The chip follows the row every frame; re-measuring the text waits
-- until the width stops changing so resizing stays cheap.
local lastWidth, pending = 0, false
frame:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
local width = frame.AbsoluteSize.X
if width == lastWidth then
return
end
lastWidth = width
fitChip(true)
if not pending then
pending = true
task.delay(0.12, function()
pending = false
valueLabel.Text = trimmed()
end)
end
end)
end
-- Popup: floats over the window under the chip, with search, bulk
-- actions for multi selection and a scrolling list of checkbox rows.
-- The popup is a plain clipped frame that unfolds out of the chip, so text
-- stays crisp. The shadow sits outside the clip.
local popup = create("Frame", {
AnchorPoint = Vector2.new(1, 0),
Size = UDim2.fromOffset(220, 0),
BackgroundTransparency = 1,
Visible = false,
ZIndex = 45,
Parent = window.Body,
})
popup:SetAttribute("NoDrag", true)
local popupShadow = create("ImageLabel", {
Position = UDim2.fromOffset(-18, -12),
Size = UDim2.new(1, 36, 1, 36),
BackgroundTransparency = 1,
Image = Assets.Shadow,
ImageColor3 = Color3.new(0, 0, 0),
ImageTransparency = 1,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(49, 49, 450, 450),
Parent = popup,
})
local panel = create("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundColor3 = Theme.Background,
BorderSizePixel = 0,
ClipsDescendants = true,
Parent = popup,
})
corner(panel, UDim.new(0, 10))
local panelStroke = stroke(panel, Theme.Stroke, 1)
edgeHighlight(panel)
local inner = create("Frame", {
Size = UDim2.new(1, 0, 0, 120),
BackgroundTransparency = 1,
Parent = panel,
})
padding(inner, 8, 8, 8, 8)
local searchHolder = create("Frame", {
Size = UDim2.new(1, 0, 0, 32),
BackgroundColor3 = Theme.Surface,
BorderSizePixel = 0,
Parent = inner,
})
corner(searchHolder, UDim.new(0, 7))
local searchStroke = stroke(searchHolder)
local searchIconHolder, searchIcon = glowIcon(searchHolder, "search", Theme.Muted, UDim2.new(0, 10, 0.5, 0))
searchIconHolder.Size = UDim2.fromOffset(14, 14)
local searchBox = create("TextBox", {
Position = UDim2.fromOffset(32, 0),
Size = UDim2.new(1, -40, 1, 0),
BackgroundTransparency = 1,
Text = "",
PlaceholderText = opts.SearchPlaceholder or "Search",
PlaceholderColor3 = Theme.Muted,
TextColor3 = Theme.Text,
TextSize = 13,
FontFace = Fonts.Regular,
TextXAlignment = Enum.TextXAlignment.Left,
TextTruncate = Enum.TextTruncate.AtEnd,
ClearTextOnFocus = false,
Parent = searchHolder,
})
searchBox.Focused:Connect(function()
tween(searchStroke, { Color = Theme.StrokeHover }, 0.15)
tween(searchIcon, { ImageColor3 = Theme.Accent }, 0.15)
end)
searchBox.FocusLost:Connect(function()
tween(searchStroke, { Color = Theme.Stroke }, 0.2)
tween(searchIcon, { ImageColor3 = Theme.Muted }, 0.2)
end)
local actions = create("Frame", {
Size = UDim2.new(1, 0, 0, 28),
BackgroundTransparency = 1,
Visible = multi,
Parent = inner,
})
local function actionButton(text, position)
local button = create("TextButton", {
Position = position,
Size = UDim2.new(0.5, -3, 1, 0),
BackgroundColor3 = Theme.Surface3,
BackgroundTransparency = 0.35,
BorderSizePixel = 0,
Text = "",
AutoButtonColor = false,
ClipsDescendants = true,
Parent = actions,
})
corner(button, UDim.new(0, 6))
local buttonStroke = stroke(button)
label({
Size = UDim2.fromScale(1, 1),
Text = text,
TextSize = 12,
TextXAlignment = Enum.TextXAlignment.Center,
Parent = button,
})
button.MouseEnter:Connect(function()
tween(button, { BackgroundTransparency = 0.05 }, 0.15)
tween(buttonStroke, { Color = Theme.StrokeHover }, 0.15)
end)
button.MouseLeave:Connect(function()
tween(button, { BackgroundTransparency = 0.35 }, 0.25)
tween(buttonStroke, { Color = Theme.Stroke }, 0.25)
end)
button.MouseButton1Click:Connect(function()
ripple(button)
end)
return button
end
local selectAll = actionButton(opts.SelectAllText or "Select all", UDim2.fromOffset(0, 0))
local clearAll = actionButton(opts.ClearAllText or "Clear all", UDim2.new(0.5, 3, 0, 0))
local list = create("ScrollingFrame", {
Size = UDim2.new(1, 0, 0, 100),
BackgroundTransparency = 1,
BorderSizePixel = 0,
ScrollBarThickness = TOUCH and 4 or 3,
ScrollBarImageColor3 = Theme.Accent,
ScrollBarImageTransparency = 0.4,
ScrollingDirection = Enum.ScrollingDirection.Y,
-- Sized by hand: AutomaticCanvasSize under the window's UIScale
-- sometimes leaves the canvas short, so the list would not scroll.
CanvasSize = UDim2.new(),
Parent = inner,
})
create("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 2),
Parent = list,
})
local noResults = label({
Size = UDim2.new(1, 0, 0, ROW),
Text = opts.NoResultsText or "No results",
TextSize = 13,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextXAlignment = Enum.TextXAlignment.Center,
LayoutOrder = 0,
Visible = false,
Parent = list,
})
local self_ = { Open = false }
local selected = {}
local optionButtons = {}
local filter = ""
local function matches(option)
return filter == "" or string.find(string.lower(tostring(option)), filter, 1, true) ~= nil
end
-- Option labels are rich text, so the part matching the search can light up.
local function escapeRich(text)
return (text:gsub("&", "&"):gsub("<", "<"):gsub(">", ">"))
end
local function optionText(option)
local text = tostring(option)
local at = filter ~= "" and string.find(string.lower(text), filter, 1, true)
if not at then
return escapeRich(text)
end
local stop = at + #filter - 1
return escapeRich(text:sub(1, at - 1))
.. '' .. escapeRich(text:sub(at, stop)) .. ""
.. escapeRich(text:sub(stop + 1))
end
if multi then
for _, value in ipairs(opts.Default or {}) do
selected[value] = true
end
elseif opts.Default ~= nil then
selected[opts.Default] = true
end
local function currentValue()
if multi then
local picked = {}
for _, option in ipairs(options) do
if selected[option] then
table.insert(picked, option)
end
end
return picked
end
for _, option in ipairs(options) do
if selected[option] then
return option
end
end
return nil
end
-- A picked row rests on a faint accent tint; hovering lifts either kind.
local function restRow(entry, duration)
local on, hover = entry.On == true, entry.Hover == true
tween(entry.Frame, {
BackgroundColor3 = on and Theme.Accent or Theme.Surface3,
BackgroundTransparency = on and (hover and 0.8 or 0.88) or (hover and 0.55 or 1),
}, duration)
tween(entry.Label, { TextColor3 = (on or hover) and Theme.Text or Theme.Muted }, duration)
end
local function paintOption(button, animate)
local on = selected[button.Option] == true
if button.On == on and animate then
return
end
button.On = on
local duration = animate and 0.18 or 0
restRow(button, duration)
if multi then
tween(button.Box, { BackgroundColor3 = on and Theme.Accent or Theme.Surface2 }, duration)
tween(button.BoxStroke, { Color = on and Theme.Accent or Theme.Stroke }, duration)
else
tween(button.Bar, {
BackgroundTransparency = on and 0 or 1,
Size = UDim2.fromOffset(3, on and ROW - 14 or 4),
}, animate and 0.3 or 0, Enum.EasingStyle.Quint)
end
tween(button.Check, { ImageTransparency = on and 0 or 1 }, duration)
if animate and on then
button.CheckScale.Scale = 0.5
tween(button.CheckScale, { Scale = 1 }, 0.3, Enum.EasingStyle.Back)
else
button.CheckScale.Scale = 1
end
end
local function renderValue(animate)
local value = currentValue()
if #options == 0 then
fullText = opts.EmptyText or "None"
elseif multi then
-- Two names at most, then a count, so a long pick stays readable.
if #value > 2 then
fullText = tostring(value[1]) .. ", " .. tostring(value[2]) .. " +" .. (#value - 2)
else
fullText = #value > 0 and table.concat(value, ", ") or (opts.NoneText or "None")
end
else
fullText = value ~= nil and tostring(value) or (opts.NoneText or "None")
end
valueLabel.Text = trimmed()
-- A picked value reads in full colour; "None" stays muted.
local picked = #options > 0 and (if multi then #value > 0 else value ~= nil)
tween(valueLabel, { TextColor3 = picked and Theme.Text or Theme.Muted }, animate and 0.15 or 0)
for _, button in pairs(optionButtons) do
paintOption(button, animate)
end
end
local function visibleCount()
local count = 0
for _, option in ipairs(options) do
if matches(option) then
count += 1
end
end
return count
end
local searchShown = true
local targetSize = Vector2.new(220, 120)
local opened = false
local function layoutPopup(instant)
searchShown = opts.Searchable ~= false and #options > (opts.SearchAfter or 0)
searchHolder.Visible = searchShown
local top = 0
if searchShown then
top += 38
end
actions.Position = UDim2.fromOffset(0, top)
if multi and #options > 0 then
actions.Visible = true
top += 34
else
actions.Visible = false
end
local count = visibleCount()
noResults.Visible = count == 0
local rows = math.clamp(count, 1, MAX_ROWS)
local listHeight = rows * ROW + (rows - 1) * 2
list.CanvasSize = UDim2.fromOffset(0, math.max(count, 1) * (ROW + 2) - 2)
local scale = window.Scale.Scale
-- Shrink the list to whichever side of the chip has more room, so the
-- popup never runs off a short window (phones in landscape).
local host = window:_popupHost(frame)
local bodyHeight = host.AbsoluteSize.Y / scale
local chipTop = (chip.AbsolutePosition.Y - host.AbsolutePosition.Y) / scale
local room = math.max(bodyHeight - (chipTop + chip.AbsoluteSize.Y / scale) - 14, chipTop - 14) - top - 16
if room >= ROW then
listHeight = math.min(listHeight, room)
end
list.Position = UDim2.fromOffset(0, top)
list.Size = UDim2.new(1, 0, 0, listHeight)
local width = math.max(math.floor(chip.AbsoluteSize.X / scale), opts.PopupWidth or 210)
local bodyWidth = host.AbsoluteSize.X / scale
width = math.min(width, math.max(bodyWidth - 16, 120))
targetSize = Vector2.new(width, top + listHeight + 16)
inner.Size = UDim2.new(1, 0, 0, targetSize.Y)
if not opened then
return
end
if instant then
popup.Size = UDim2.fromOffset(targetSize.X, targetSize.Y)
else
tween(popup, { Size = UDim2.fromOffset(targetSize.X, targetSize.Y) }, 0.22, Enum.EasingStyle.Quint)
end
end
local function applyFilter()
for option, button in pairs(optionButtons) do
local shown = matches(option)
button.Frame.Visible = shown
if shown then
button.Label.Text = optionText(option)
end
end
end
local scroller = nil
local function place()
local body = window:_popupHost(frame)
local scale = window.Scale.Scale
if scroller then
local centre = chip.AbsolutePosition + chip.AbsoluteSize / 2
if not pointInside(centre, scroller) then
self_:SetOpen(false)
return
end
end
local bodySize = body.AbsoluteSize / scale
local chipPosition = (chip.AbsolutePosition - body.AbsolutePosition) / scale
local chipSize = chip.AbsoluteSize / scale
local right = math.clamp(chipPosition.X + chipSize.X, targetSize.X + 8, math.max(bodySize.X - 8, targetSize.X + 8))
local below = chipPosition.Y + chipSize.Y + 6
-- Opening upward pins the content to the bottom edge so it unfolds from the chip.
if below + targetSize.Y > bodySize.Y - 8 and chipPosition.Y - 6 - targetSize.Y >= 8 then
popup.AnchorPoint = Vector2.new(1, 1)
popup.Position = UDim2.fromOffset(right, chipPosition.Y - 6)
inner.AnchorPoint = Vector2.new(0, 1)
inner.Position = UDim2.fromScale(0, 1)
else
popup.AnchorPoint = Vector2.new(1, 0)
popup.Position = UDim2.fromOffset(right, below)
inner.AnchorPoint = Vector2.zero
inner.Position = UDim2.fromScale(0, 0)
end
end
local stopFollowing = nil
local closeGeneration = 0
-- The page behind stops scrolling while the popup is open, so wheel and
-- swipe input always reach the list instead of dragging the page away.
local lockedScroller = nil
local function unlockScroller()
if lockedScroller then
lockedScroller.ScrollingEnabled = true
lockedScroller = nil
end
end
local function scrollToSelected()
list.CanvasPosition = Vector2.zero
if multi then
return
end
local value = currentValue()
if value == nil then
return
end
for index, option in ipairs(options) do
if option == value then
list.CanvasPosition = Vector2.new(0, math.max(0, (index - 2) * (ROW + 2)))
return
end
end
end
local function setOpen(open)
if open == self_.Open then
return
end
self_.Open = open
arrow:Set(open)
tween(chipStroke, { Color = open and Theme.Accent or Theme.Stroke, Transparency = open and 0.35 or 0 }, 0.2)
tween(chip, { BackgroundColor3 = open and Theme.Surface2 or Theme.Surface }, 0.2)
closeGeneration += 1
if open then
if window._closePopup and window._closePopup ~= self_._close then
window._closePopup()
end
window._closePopup = self_._close
scroller = frame:FindFirstAncestorWhichIsA("ScrollingFrame")
if scroller and scroller.ScrollingEnabled then
lockedScroller = scroller
scroller.ScrollingEnabled = false
end
-- Drawn over a floating panel when the dropdown lives in one.
popup.Parent = window:_popupHost(frame)
filter = ""
searchBox.Text = ""
searchBox.PlaceholderText = opts.SearchPlaceholder or (#options > 6 and ("Search " .. #options .. " options") or "Search")
applyFilter()
layoutPopup(true)
renderValue(false)
scrollToSelected()
opened = true
popup.Size = UDim2.fromOffset(targetSize.X, 0)
place()
popup.Visible = true
tween(popup, { Size = UDim2.fromOffset(targetSize.X, targetSize.Y) }, 0.3, Enum.EasingStyle.Quint)
tween(panelStroke, { Transparency = 0 }, 0.12)
tween(popupShadow, { ImageTransparency = 0.4 }, 0.3)
if not stopFollowing then
stopFollowing = window:_listen("Render", place)
end
else
if window._closePopup == self_._close then
window._closePopup = nil
end
unlockScroller()
if searchBox:IsFocused() then
searchBox:ReleaseFocus()
end
opened = false
tween(popup, { Size = UDim2.fromOffset(targetSize.X, 0) }, 0.2, Enum.EasingStyle.Quint)
tween(popupShadow, { ImageTransparency = 1 }, 0.16)
local generation = closeGeneration
task.delay(0.14, function()
if closeGeneration == generation and not self_.Open then
tween(panelStroke, { Transparency = 1 }, 0.06)
end
end)
task.delay(0.21, function()
if closeGeneration == generation and not self_.Open then
popup.Visible = false
if stopFollowing then
stopFollowing()
stopFollowing = nil
end
end
end)
end
end
self_._close = function()
setOpen(false)
end
local function commit()
renderValue(true)
safeCall(opts.Callback, currentValue())
end
local function buildOptions()
local keep = {}
for index, option in ipairs(options) do
keep[option] = index
end
for option, button in pairs(optionButtons) do
if not keep[option] then
button.Frame:Destroy()
optionButtons[option] = nil
end
end
for index, option in ipairs(options) do
local existing = optionButtons[option]
if existing then
existing.Frame.LayoutOrder = index
continue
end
local row = create("TextButton", {
Size = UDim2.new(1, -6, 0, ROW),
BackgroundColor3 = Theme.Surface3,
BackgroundTransparency = 1,
BorderSizePixel = 0,
Text = "",
AutoButtonColor = false,
LayoutOrder = index,
Parent = list,
})
corner(row, UDim.new(0, 7))
-- Multi selection ticks a checkbox on the left. A single pick
-- gets an accent bar on the left edge and a check on the right.
local box, boxStroke, bar, check
if multi then
box = create("Frame", {
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, 8, 0.5, 0),
Size = UDim2.fromOffset(18, 18),
BackgroundColor3 = Theme.Surface2,
BorderSizePixel = 0,
Parent = row,
})
corner(box, UDim.new(0, 5))
boxStroke = stroke(box, Theme.Stroke)
check = create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromOffset(12, 12),
BackgroundTransparency = 1,
ImageColor3 = Theme.AccentDark,
ImageTransparency = 1,
ScaleType = Enum.ScaleType.Fit,
Parent = box,
})
else
bar = create("Frame", {
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, 3, 0.5, 0),
Size = UDim2.fromOffset(3, 4),
BackgroundColor3 = Theme.Accent,
BackgroundTransparency = 1,
BorderSizePixel = 0,
Parent = row,
})
corner(bar, UDim.new(1, 0))
check = create("ImageLabel", {
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, -10, 0.5, 0),
Size = UDim2.fromOffset(14, 14),
BackgroundTransparency = 1,
ImageColor3 = Theme.Accent,
ImageTransparency = 1,
ScaleType = Enum.ScaleType.Fit,
Parent = row,
})
end
applyIcon(check, "check")
local checkScale = create("UIScale", { Parent = check })
local text_ = label({
Position = UDim2.fromOffset(multi and 36 or 14, 0),
Size = UDim2.new(1, multi and -44 or -46, 1, 0),
Text = optionText(option),
TextSize = 13,
TextColor3 = Theme.Muted,
RichText = true,
Parent = row,
})
local entry = { Option = option, Frame = row, Box = box, BoxStroke = boxStroke, Bar = bar, Check = check, CheckScale = checkScale, Label = text_, On = nil, Hover = false }
local isTap = tapGuard(row, function()
return list
end)
row.MouseEnter:Connect(function()
-- Touch fires enter without a matching leave; skip the hover.
if usingTouch() then
return
end
entry.Hover = true
restRow(entry, 0.15)
if boxStroke and not selected[option] then
tween(boxStroke, { Color = Theme.StrokeHover }, 0.15)
end
end)
row.MouseLeave:Connect(function()
entry.Hover = false
restRow(entry, 0.25)
if boxStroke and not selected[option] then
tween(boxStroke, { Color = Theme.Stroke }, 0.25)
end
end)
row.MouseButton1Click:Connect(function()
if not isTap() then
return
end
if selected[option] then
if not multi and opts.AllowNone == false then
setOpen(false)
return
end
selected[option] = nil
else
if not multi then
selected = {}
end
selected[option] = true
end
commit()
if not multi then
setOpen(false)
end
end)
optionButtons[option] = entry
paintOption(entry, false)
end
applyFilter()
if self_.Open then
layoutPopup(false)
end
end
selectAll.MouseButton1Click:Connect(function()
for _, option in ipairs(options) do
if matches(option) then
selected[option] = true
end
end
commit()
end)
clearAll.MouseButton1Click:Connect(function()
selected = {}
commit()
end)
function self_:Set(value, silent)
selected = {}
if multi then
for _, item in ipairs(type(value) == "table" and value or { value }) do
selected[item] = true
end
elseif value ~= nil then
selected[value] = true
end
renderValue(self_.Open)
flashStroke(frameStroke)
if not silent then
safeCall(opts.Callback, currentValue())
end
end
function self_:Get()
return currentValue()
end
local function sameValue(a, b)
if type(a) ~= "table" or type(b) ~= "table" then
return a == b
end
if #a ~= #b then
return false
end
for index, item in ipairs(a) do
if b[index] ~= item then
return false
end
end
return true
end
-- A kept pick only counts once its option exists, so a list filled in after a
-- config load changes the value; the callback runs so scripts see it.
function self_:Refresh(newOptions, keepSelection, silent)
local before = currentValue()
options = newOptions or {}
if not keepSelection then
selected = {}
end
buildOptions()
renderValue(false)
if not silent and not sameValue(before, currentValue()) then
safeCall(opts.Callback, currentValue())
end
end
-- Every pick, including ones whose option isn't in the list right now, so
-- saving never drops them.
function self_:_saveValue()
local picked = currentValue()
if multi then
local seen = {}
for _, item in ipairs(picked) do
seen[item] = true
end
for item in pairs(selected) do
if not seen[item] then
table.insert(picked, item)
end
end
return picked
end
if picked == nil then
picked = next(selected)
end
return picked
end
function self_:SetOpen(open)
setOpen(open == true)
end
local headerTap = tapGuard(header, function()
return frame:FindFirstAncestorWhichIsA("ScrollingFrame")
end)
header.MouseButton1Click:Connect(function()
if headerTap() then
setOpen(not self_.Open)
end
end)
-- The chip answers the pointer before it is opened.
header.MouseEnter:Connect(function()
if not self_.Open and not usingTouch() then
tween(chipStroke, { Color = Theme.StrokeHover }, 0.15)
tween(chip, { BackgroundColor3 = Theme.Surface2 }, 0.15)
end
end)
header.MouseLeave:Connect(function()
if not self_.Open then
tween(chipStroke, { Color = Theme.Stroke }, 0.25)
tween(chip, { BackgroundColor3 = Theme.Surface }, 0.25)
end
end)
searchBox:GetPropertyChangedSignal("Text"):Connect(function()
filter = string.lower(searchBox.Text)
applyFilter()
if self_.Open then
layoutPopup(false)
end
end)
self_._listeners = {
function()
setOpen(false)
unlockScroller()
if stopFollowing then
stopFollowing()
stopFollowing = nil
end
popup:Destroy()
end,
}
window:_listen("Began", function(input)
if not self_.Open then
return
end
if input.KeyCode == Enum.KeyCode.Escape then
setOpen(false)
elseif isPress(input) then
local point = pointerPosition()
if not pointInside(point, popup) and not pointInside(point, header) then
setOpen(false)
end
end
end, self_)
buildOptions()
renderValue(false)
task.defer(fitChip, true)
return finishElement(self, opts, self_, frame, "Dropdown")
end
Library._amountSuffixes = { k = 1e3, m = 1e6, b = 1e9, t = 1e12, qa = 1e15, qi = 1e18 }
function Library._parseAmount(text)
text = string.lower(tostring(text)):gsub("[%s,_%$]", "")
text = text:gsub("kg$", ""):gsub("/s$", "")
local number = tonumber(text)
if number then
return number
end
local digits, suffix = text:match("^(.-)(%a+)$")
local scale = suffix and Library._amountSuffixes[suffix]
number = digits and tonumber(digits)
if number and scale then
return number * scale
end
return nil
end
function Tab:Input(opts)
local frame, frameStroke, _, titleLabel, descLabel
opts, frame, frameStroke, _, titleLabel, descLabel = beginElement(self, opts, { Title = "Name", Description = "Desc", PlaceholderText = "Placeholder", CurrentValue = "Default", Value = "Default" }, "Frame", 160, "Input")
local compact = metricsOf(self).Compact
local boxHeight = compact and metricsOf(self).Chip + 2 or 30
local boxHolder = create("Frame", {
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, compact and -14 or -10, 0.5, 0),
Size = UDim2.fromOffset(170, boxHeight),
BackgroundColor3 = Theme.Surface,
BorderSizePixel = 0,
Parent = frame,
})
corner(boxHolder, UDim.new(0, 6))
local boxStroke = stroke(boxHolder)
local boxIcon = opts.Icon
if boxIcon then
glowIcon(boxHolder, boxIcon, Theme.Muted, UDim2.new(0, 8, 0.5, 0))
end
local box = create("TextBox", {
Position = UDim2.fromOffset(boxIcon and 30 or 8, 0),
Size = UDim2.new(1, boxIcon and -38 or -16, 1, 0),
BackgroundTransparency = 1,
Text = opts.Default or "",
PlaceholderText = opts.Placeholder or "",
PlaceholderColor3 = Theme.Muted,
TextColor3 = Theme.Text,
TextSize = compact and 13 or 14,
FontFace = Fonts.Regular,
TextXAlignment = Enum.TextXAlignment.Left,
ClearTextOnFocus = false,
TextTruncate = Enum.TextTruncate.None,
ClipsDescendants = true,
Parent = boxHolder,
})
boxHolder.ClipsDescendants = true
local lastText = box.Text
local value = opts.Numeric and Library._parseAmount(lastText) or nil
if opts.Numeric and not value then
lastText = ""
end
local focused = false
local textInset = (boxIcon and 30 or 8) + 8
local function fitBox(instant)
local width
if compact then
width = rowControlWidth(frame, self.Window)
else
local cardWidth = frame.AbsoluteSize.X / self.Window.Scale.Scale
local maxWidth = math.clamp(cardWidth - 14 - 110 - 20, 100, 200)
local measured = box.TextBounds.X
if #box.Text == 0 then
measured = math.min(box.TextBounds.X, 90)
end
width = math.clamp(measured + textInset + 12, 90, maxWidth) + (focused and 8 or 0)
width = math.min(width, maxWidth + 8)
end
reserveRight(titleLabel, descLabel, width + (compact and 24 or 20))
if instant then
boxHolder.Size = UDim2.fromOffset(width, boxHeight)
else
tween(boxHolder, { Size = UDim2.fromOffset(width, boxHeight) }, 0.18)
end
end
local lastWidth = 0
frame:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
if frame.AbsoluteSize.X ~= lastWidth then
lastWidth = frame.AbsoluteSize.X
fitBox(true)
end
end)
box:GetPropertyChangedSignal("Text"):Connect(function()
fitBox(false)
end)
box:GetPropertyChangedSignal("TextBounds"):Connect(function()
fitBox(false)
end)
task.defer(fitBox, true)
box.Focused:Connect(function()
focused = true
tween(boxStroke, { Color = Theme.StrokeHover }, 0.15)
fitBox(false)
end)
box.FocusLost:Connect(function(enterPressed)
focused = false
tween(boxStroke, { Color = Theme.Stroke }, 0.15)
fitBox(false)
if opts.Numeric then
local number = Library._parseAmount(box.Text)
if not number then
box.Text = lastText
return
end
lastText, value = box.Text, number
safeCall(opts.Callback, number, enterPressed)
return
end
safeCall(opts.Callback, box.Text, enterPressed)
end)
return finishElement(self, opts, {
Set = function(_, newValue)
box.Text = tostring(newValue)
if opts.Numeric then
local number = Library._parseAmount(box.Text)
if number then
lastText, value = box.Text, number
else
box.Text = lastText
end
end
flashStroke(frameStroke)
end,
Get = function()
if opts.Numeric then
return value
end
return box.Text
end,
_saveValue = opts.Numeric and function()
return lastText
end or nil,
}, frame, "Input")
end
function Tab:Keybind(opts)
local frame, frameStroke, _, titleLabel, descLabel
opts, frame, frameStroke, _, titleLabel, descLabel = beginElement(self, opts, { Title = "Name", Description = "Desc", CurrentKeybind = "Default", Value = "Default" }, "Frame", 110, "Keybind")
if type(opts.Default) == "string" then
opts.Default = Enum.KeyCode[opts.Default]
end
local compact = metricsOf(self).Compact
local chipHeight = metricsOf(self).Chip
local chip = create("TextButton", {
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, compact and -14 or -10, 0.5, 0),
Size = UDim2.fromOffset(44, chipHeight),
BackgroundColor3 = Theme.Surface,
BorderSizePixel = 0,
Text = "",
AutoButtonColor = false,
ClipsDescendants = true,
Parent = frame,
})
corner(chip, UDim.new(0, 6))
local chipStroke = stroke(chip)
local chipLabel = label({
Size = UDim2.new(1, 0, 1, 0),
TextSize = 13,
TextColor3 = Theme.Muted,
TextXAlignment = Enum.TextXAlignment.Center,
TextTruncate = Enum.TextTruncate.None,
Parent = chip,
})
local self_ = { Value = opts.Default, Listening = false }
local function fitChip(instant)
local width = math.max(chipLabel.TextBounds.X + 20, TOUCH and 44 or 36)
reserveRight(titleLabel, descLabel, width + (compact and 24 or 20))
if instant then
chip.Size = UDim2.fromOffset(width, chipHeight)
else
tween(chip, { Size = UDim2.fromOffset(width, chipHeight) }, 0.2)
end
end
chipLabel:GetPropertyChangedSignal("TextBounds"):Connect(function()
fitChip(false)
end)
local function render()
chipLabel.Text = self_.Listening and "..." or keyName(self_.Value)
tween(chipStroke, { Color = self_.Listening and Theme.StrokeHover or Theme.Stroke }, 0.15)
tween(chipLabel, { TextColor3 = self_.Listening and Theme.Accent or Theme.Muted }, 0.15)
end
function self_:Set(keyCode, silent)
local wasListening = self_.Listening
self_.Value = keyCode
self_.Listening = false
render()
if not wasListening then
flashStroke(frameStroke)
end
if not silent then
safeCall(opts.OnChanged, keyCode)
end
end
chip.MouseButton1Click:Connect(function()
self_.Listening = not self_.Listening
render()
end)
self.Window:_listen("Began", function(input, gameProcessed)
if input.UserInputType ~= Enum.UserInputType.Keyboard then
return
end
if self_.Listening then
self.Window._consumedKey = input.KeyCode
self.Window._consumedAt = os.clock()
if input.KeyCode == Enum.KeyCode.Escape then
self_.Listening = false
render()
else
self_:Set(input.KeyCode)
end
return
end
if not gameProcessed and self_.Value ~= nil and input.KeyCode == self_.Value then
safeCall(opts.Callback, input.KeyCode)
end
end, self_)
render()
task.defer(fitChip, true)
function self_:Get()
return self_.Value
end
self_._onHide = function()
if self_.Listening then
self_.Listening = false
render()
end
end
return finishElement(self, opts, self_, frame, "Keybind")
end
function Tab:ColorPicker(opts)
local PANEL_HEIGHT = 194
local frame, frameStroke, height
opts, frame, frameStroke, height = beginElement(self, opts, { Title = "Name", Description = "Desc", Color = "Default", CurrentValue = "Default", Value = "Default" }, "Frame")
frame.ClipsDescendants = true
local metrics = metricsOf(self)
local compact = metrics.Compact
local header = create("TextButton", {
Size = UDim2.new(1, 0, 0, height),
BackgroundTransparency = 1,
Text = "",
AutoButtonColor = false,
Parent = frame,
})
titleDesc(header, opts.Name or "Color", opts.Desc, 140, metrics)
-- Closed, the row shows the hex code beside a rounded swatch.
local swatch = create("Frame", {
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, compact and -38 or -34, 0.5, 0),
Size = UDim2.fromOffset(22, 22),
BorderSizePixel = 0,
Parent = header,
})
corner(swatch, UDim.new(0, 6))
stroke(swatch, Theme.Stroke)
local swatchHex = label({
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, compact and -68 or -64, 0.5, 0),
Size = UDim2.fromOffset(64, 16),
TextSize = 12,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextXAlignment = Enum.TextXAlignment.Right,
Parent = header,
})
local arrow = arrowIndicator(header, 1, compact and -14 or -12)
local panel = create("Frame", {
Position = UDim2.fromOffset(14, height + 2),
Size = UDim2.new(1, -28, 0, PANEL_HEIGHT - 12),
BackgroundTransparency = 1,
Visible = false,
Parent = frame,
})
-- Saturation runs left to right, brightness top to bottom.
local svBox = create("TextButton", {
Size = UDim2.new(1, 0, 0, 118),
BackgroundColor3 = Color3.new(1, 1, 1),
BorderSizePixel = 0,
Text = "",
AutoButtonColor = false,
Parent = panel,
})
corner(svBox, UDim.new(0, 8))
stroke(svBox, Theme.Stroke)
local svHueGradient = create("UIGradient", {
Color = ColorSequence.new(Color3.new(1, 1, 1), Color3.new(1, 0, 0)),
Parent = svBox,
})
local svDark = create("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundColor3 = Color3.new(0, 0, 0),
BorderSizePixel = 0,
Parent = svBox,
})
corner(svDark, UDim.new(0, 8))
create("UIGradient", {
Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 1),
NumberSequenceKeypoint.new(1, 0),
}),
Rotation = 90,
Parent = svDark,
})
-- Both handles are a white ring filled with the colour under them, with a
-- faint dark edge so they read on light and dark spots alike.
local function knob(parent, size)
local ring = create("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
Size = UDim2.fromOffset(size, size),
BackgroundColor3 = Color3.new(1, 1, 1),
BorderSizePixel = 0,
ZIndex = 3,
Parent = parent,
})
corner(ring, UDim.new(1, 0))
stroke(ring, Color3.new(0, 0, 0), 0.7)
local fill = create("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.new(1, -4, 1, -4),
BorderSizePixel = 0,
ZIndex = 3,
Parent = ring,
})
corner(fill, UDim.new(1, 0))
return ring, fill
end
local svCursor, svFill = knob(svBox, 14)
-- Hue is a slim horizontal track under the square.
local hueBar = create("TextButton", {
Position = UDim2.fromOffset(0, 132),
Size = UDim2.new(1, 0, 0, 10),
BackgroundColor3 = Color3.new(1, 1, 1),
BorderSizePixel = 0,
Text = "",
AutoButtonColor = false,
Parent = panel,
})
corner(hueBar, UDim.new(1, 0))
create("UIGradient", {
Color = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.fromRGB(255, 0, 0)),
ColorSequenceKeypoint.new(1 / 6, Color3.fromRGB(255, 255, 0)),
ColorSequenceKeypoint.new(2 / 6, Color3.fromRGB(0, 255, 0)),
ColorSequenceKeypoint.new(3 / 6, Color3.fromRGB(0, 255, 255)),
ColorSequenceKeypoint.new(4 / 6, Color3.fromRGB(0, 0, 255)),
ColorSequenceKeypoint.new(5 / 6, Color3.fromRGB(255, 0, 255)),
ColorSequenceKeypoint.new(1, Color3.fromRGB(255, 0, 0)),
}),
Parent = hueBar,
})
local hueCursor, hueFill = knob(hueBar, 16)
hueCursor.Position = UDim2.fromScale(0, 0.5)
-- Bottom row: a preview chip and hex field on the left, RGB on the right.
local hexHolder = create("Frame", {
Position = UDim2.fromOffset(0, 154),
Size = UDim2.fromOffset(compact and 110 or 124, 28),
BackgroundColor3 = Theme.Surface,
BorderSizePixel = 0,
Parent = panel,
})
corner(hexHolder, UDim.new(0, 7))
local hexStroke = stroke(hexHolder)
local preview = create("Frame", {
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, 6, 0.5, 0),
Size = UDim2.fromOffset(16, 16),
BorderSizePixel = 0,
Parent = hexHolder,
})
corner(preview, UDim.new(0, 4))
stroke(preview, Theme.Stroke)
local hexBox = create("TextBox", {
Position = UDim2.fromOffset(30, 0),
Size = UDim2.new(1, -38, 1, 0),
BackgroundTransparency = 1,
Text = "",
TextTruncate = Enum.TextTruncate.None,
ClipsDescendants = false,
TextColor3 = Theme.Text,
TextSize = 13,
FontFace = Fonts.Regular,
TextXAlignment = Enum.TextXAlignment.Left,
ClearTextOnFocus = false,
Parent = hexHolder,
})
local rgbLabel = label({
Position = UDim2.fromOffset(compact and 118 or 134, 154),
Size = UDim2.new(1, compact and -118 or -134, 0, 28),
TextXAlignment = Enum.TextXAlignment.Right,
TextSize = 12,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
Parent = panel,
})
local self_ = { Open = false }
local hue, sat, val = Color3.toHSV(opts.Default or Theme.Accent)
local dragTarget = nil
local function toHex(color)
return string.format("#%02X%02X%02X",
math.floor(color.R * 255 + 0.5),
math.floor(color.G * 255 + 0.5),
math.floor(color.B * 255 + 0.5))
end
local function render(duration)
local color = Color3.fromHSV(hue, sat, val)
self_.Value = color
swatch.BackgroundColor3 = color
preview.BackgroundColor3 = color
svFill.BackgroundColor3 = color
hueFill.BackgroundColor3 = Color3.fromHSV(hue, 1, 1)
swatchHex.Text = toHex(color)
svHueGradient.Color = ColorSequence.new(Color3.new(1, 1, 1), Color3.fromHSV(hue, 1, 1))
tween(svCursor, { Position = UDim2.fromScale(sat, 1 - val) }, duration, Enum.EasingStyle.Quint)
tween(hueCursor, { Position = UDim2.fromScale(hue, 0.5) }, duration, Enum.EasingStyle.Quint)
if not hexBox:IsFocused() then
hexBox.Text = toHex(color)
end
rgbLabel.Text = string.format("%d %d %d",
math.floor(color.R * 255 + 0.5),
math.floor(color.G * 255 + 0.5),
math.floor(color.B * 255 + 0.5))
end
local function commit(duration, silent)
render(duration)
if not silent then
safeCall(opts.Callback, self_.Value)
end
end
function self_:Set(color, silent)
hue, sat, val = Color3.toHSV(color)
commit(0.25, silent)
flashStroke(frameStroke)
end
function self_:Get()
return self_.Value
end
local function setOpen(open)
self_.Open = open
tween(frame, { Size = UDim2.new(1, 0, 0, open and (height + PANEL_HEIGHT) or height) }, 0.35, Enum.EasingStyle.Quint)
arrow:Set(open)
if open then
panel.Visible = true
else
task.delay(0.35, function()
if not self_.Open then
panel.Visible = false
end
end)
end
end
function self_:SetOpen(open)
setOpen(open == true)
end
header.MouseButton1Click:Connect(function()
setOpen(not self_.Open)
end)
local function updateSV(position)
sat = math.clamp((position.X - svBox.AbsolutePosition.X) / svBox.AbsoluteSize.X, 0, 1)
val = 1 - math.clamp((position.Y - svBox.AbsolutePosition.Y) / svBox.AbsoluteSize.Y, 0, 1)
commit(0.04)
end
local function updateHue(position)
hue = math.clamp((position.X - hueBar.AbsolutePosition.X) / hueBar.AbsoluteSize.X, 0, 0.999)
commit(0.04)
end
svBox.InputBegan:Connect(function(input)
if isPress(input) then
dragTarget = "sv"
tween(svCursor, { Size = UDim2.fromOffset(18, 18) }, 0.2, Enum.EasingStyle.Quint)
updateSV(pointerPosition())
end
end)
hueBar.InputBegan:Connect(function(input)
if isPress(input) then
dragTarget = "hue"
tween(hueCursor, { Size = UDim2.fromOffset(20, 20) }, 0.2, Enum.EasingStyle.Quint)
updateHue(pointerPosition())
end
end)
self.Window:_listen("Changed", function(input)
if not dragTarget or not isMove(input) then
return
end
local position = pointerPosition()
if dragTarget == "sv" then
updateSV(position)
else
updateHue(position)
end
end, self_)
self.Window:_listen("Ended", function(input)
if dragTarget and isPress(input) then
dragTarget = nil
tween(svCursor, { Size = UDim2.fromOffset(14, 14) }, 0.25, Enum.EasingStyle.Quint)
tween(hueCursor, { Size = UDim2.fromOffset(16, 16) }, 0.25, Enum.EasingStyle.Quint)
end
end, self_)
hexBox.Focused:Connect(function()
tween(hexStroke, { Color = Theme.StrokeHover }, 0.15)
end)
hexBox.FocusLost:Connect(function()
tween(hexStroke, { Color = Theme.Stroke }, 0.15)
local r, g, b = hexBox.Text:match("^%s*#?(%x%x)(%x%x)(%x%x)%s*$")
if r then
self_:Set(Color3.fromRGB(tonumber(r, 16), tonumber(g, 16), tonumber(b, 16)))
else
hexBox.Text = toHex(self_.Value)
end
end)
render(0)
return finishElement(self, opts, self_, frame, "ColorPicker")
end
function Tab:Stepper(opts)
local frame, frameStroke, _, titleLabel, descLabel
opts, frame, frameStroke, _, titleLabel, descLabel = beginElement(self, opts, { Title = "Name", Description = "Desc", CurrentValue = "Default", Value = "Default", Increment = "Step" }, "Frame", 150, "Stepper")
local min = opts.Min or 0
local max = opts.Max or 100
local step = opts.Step or 1
local suffix = opts.Suffix or ""
local numbers = numberFormat(min, max, step)
local compact = metricsOf(self).Compact
local chipHeight = metricsOf(self).Chip
local group = create("Frame", {
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, compact and -14 or -10, 0.5, 0),
Size = UDim2.fromOffset(0, chipHeight),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = Theme.Surface,
BorderSizePixel = 0,
Parent = frame,
})
corner(group, UDim.new(0, 6))
local groupStroke = stroke(group)
create("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
SortOrder = Enum.SortOrder.LayoutOrder,
VerticalAlignment = Enum.VerticalAlignment.Center,
Parent = group,
})
local function stepButton(text, order)
local button = create("TextButton", {
Size = UDim2.fromOffset(chipHeight, chipHeight),
BackgroundTransparency = 1,
Text = text,
TextColor3 = Theme.Muted,
TextSize = 18,
FontFace = Fonts.Medium,
AutoButtonColor = false,
LayoutOrder = order,
Parent = group,
})
button.MouseEnter:Connect(function()
tween(button, { TextColor3 = Theme.Accent }, 0.12)
end)
button.MouseLeave:Connect(function()
tween(button, { TextColor3 = Theme.Muted }, 0.2)
end)
return button
end
local minus = stepButton("−", 1)
local valueLabel = label({
Size = UDim2.new(0, 30, 1, 0),
TextSize = 13,
TextColor3 = Theme.Accent,
TextXAlignment = Enum.TextXAlignment.Center,
TextTruncate = Enum.TextTruncate.None,
LayoutOrder = 2,
Parent = group,
})
local plus = stepButton("+", 3)
local function fitValue(instant)
local width = math.max(valueLabel.TextBounds.X + 12, 30)
if instant then
valueLabel.Size = UDim2.new(0, width, 1, 0)
else
tween(valueLabel, { Size = UDim2.new(0, width, 1, 0) }, 0.2)
end
end
valueLabel:GetPropertyChangedSignal("TextBounds"):Connect(function()
fitValue(false)
end)
group:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
reserveRight(titleLabel, descLabel, group.AbsoluteSize.X / self.Window.Scale.Scale + (compact and 24 or 20))
end)
local self_ = { Value = math.clamp(opts.Default or min, min, max) }
local snap = numbers.snap
local fromButton = false
local function render()
valueLabel.Text = numbers.format(self_.Value) .. suffix
tween(minus, { TextTransparency = self_.Value <= min and 0.6 or 0 }, 0.15)
tween(plus, { TextTransparency = self_.Value >= max and 0.6 or 0 }, 0.15)
end
function self_:Set(value, silent)
value = snap(tonumber(value) or min)
if value == self_.Value then
return
end
self_.Value = value
render()
if not fromButton then
flashStroke(frameStroke)
end
if not silent then
safeCall(opts.Callback, value)
end
end
function self_:Get()
return self_.Value
end
local function bump(direction)
fromButton = true
self_:Set(self_.Value + direction * step)
fromButton = false
tween(groupStroke, { Color = Theme.StrokeHover }, 0.08)
task.delay(0.12, function()
tween(groupStroke, { Color = Theme.Stroke }, 0.2)
end)
end
local function holdRepeat(button, direction)
local holding = false
button.InputBegan:Connect(function(input)
if not isPress(input) then
return
end
holding = true
bump(direction)
task.delay(0.4, function()
while holding do
bump(direction)
task.wait(0.07)
end
end)
end)
button.InputEnded:Connect(function(input)
if isPress(input) then
holding = false
end
end)
button.MouseLeave:Connect(function()
holding = false
end)
end
holdRepeat(minus, -1)
holdRepeat(plus, 1)
render()
task.defer(fitValue, true)
return finishElement(self, opts, self_, frame, "Stepper")
end
function Tab:Progress(opts)
opts = normalize(opts, { Title = "Name", Description = "Desc", CurrentValue = "Default", Value = "Default" })
local metrics = metricsOf(self)
local compact = metrics.Compact
local extra = compact and 6 or 10
local frame, frameStroke = card(self, "Frame", (opts.Desc and metrics.DescHeight or metrics.Height) + extra, opts)
hoverStroke(frame, frameStroke)
local top
if opts.Desc then
top = math.floor((metrics.DescHeight - (metrics.DescGap + metrics.DescLine)) / 2) - 2
else
top = compact and 5 or 12
end
label({
Position = UDim2.fromOffset(14, top),
Size = UDim2.new(1, -110, 0, 18),
Text = opts.Name or "Progress",
TextSize = metrics.TextSize,
Parent = frame,
})
if opts.Desc then
label({
Position = UDim2.fromOffset(14, top + metrics.DescGap),
Size = UDim2.new(1, -110, 0, metrics.DescLine),
Text = opts.Desc,
TextSize = metrics.DescSize,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
Parent = frame,
})
end
local valueLabel = label({
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, -14, 0, top),
Size = UDim2.fromOffset(90, 18),
TextXAlignment = Enum.TextXAlignment.Right,
TextSize = 13,
TextColor3 = Library._color(opts.TextColor) or Theme.Accent,
RichText = true,
Parent = frame,
})
local track = create("Frame", {
Position = UDim2.new(0, 14, 1, compact and -12 or -16),
Size = UDim2.new(1, -28, 0, 5),
BackgroundColor3 = Theme.Surface3,
BorderSizePixel = 0,
Parent = frame,
})
corner(track, UDim.new(1, 0))
local fill = create("Frame", {
Size = UDim2.new(0, 0, 1, 0),
BackgroundColor3 = Library._color(opts.Color) or Theme.Accent,
BorderSizePixel = 0,
Parent = track,
})
corner(fill, UDim.new(1, 0))
local self_ = { Value = math.clamp(opts.Default or 0, 0, 1) }
local formatter = opts.Format
local function render(duration)
local frac = self_.Value
tween(fill, { Size = UDim2.new(frac, 0, 1, 0) }, duration, Enum.EasingStyle.Quint)
if type(formatter) == "function" then
valueLabel.Text = Library._rich(formatter(frac))
else
valueLabel.Text = string.format("%d%%", math.floor(frac * 100 + 0.5))
end
end
function self_:Set(value, silent)
value = math.clamp(tonumber(value) or 0, 0, 1)
if value == self_.Value then
return
end
self_.Value = value
render(0.35)
if not silent then
safeCall(opts.Callback, value)
end
end
function self_:Get()
return self_.Value
end
function self_:SetColor(color)
tween(fill, { BackgroundColor3 = Library._color(color) or Theme.Accent }, 0.25)
end
-- The percentage's colour; nil goes back to the accent.
function self_:SetTextColor(color)
tween(valueLabel, { TextColor3 = Library._color(color) or Theme.Accent }, 0.25)
end
-- Runs render now, and again on a theme change for theme tags in Format.
onTheme(valueLabel, function()
render(0)
end)
return finishElement(self, opts, self_, frame, "Progress")
end
-- An always-open list the player puts in order: drag a row (by its grip on
-- touch, anywhere on PC) or nudge it with the arrows. Past MaxRows the list
-- scrolls, and dragging near its top or bottom edge scrolls it along.
function Tab:OrderList(opts)
opts = normalize(opts, { Title = "Name", Description = "Desc", Options = "Items", Values = "Items" })
local metrics = metricsOf(self)
local compact = metrics.Compact
local window = self.Window
local ROW = TOUCH and 40 or 32
local GAP = 4
local STEP = ROW + GAP
local GRIP = TOUCH and 34 or 26
local ARROW = TOUCH and 30 or 22
local maxRows = math.max(math.floor(tonumber(opts.MaxRows) or 6), 1)
local numbered = opts.Numbered ~= false
local arrows = opts.Arrows ~= false
local frame = card(self, "Frame", 0, opts)
frame.AutomaticSize = Enum.AutomaticSize.Y
if compact then
padding(frame, 14, 14, 6, 8)
else
padding(frame, 14, 14, 11, 12)
end
create("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 6),
Parent = frame,
})
local name = tostring(opts.Name or "")
label({
Size = UDim2.new(1, 0, 0, 18),
Text = name,
TextSize = metrics.TextSize,
Visible = name ~= "",
LayoutOrder = 1,
Parent = frame,
})
if opts.Desc then
label({
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
Text = opts.Desc,
TextSize = metrics.DescSize,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextWrapped = true,
TextTruncate = Enum.TextTruncate.None,
LayoutOrder = 2,
Parent = frame,
})
end
local scroller = create("ScrollingFrame", {
Size = UDim2.new(1, 0, 0, ROW + 2),
BackgroundTransparency = 1,
BorderSizePixel = 0,
ScrollBarThickness = TOUCH and 4 or 3,
ScrollBarImageColor3 = Theme.Accent,
ScrollBarImageTransparency = 0.4,
ScrollingDirection = Enum.ScrollingDirection.Y,
-- Sized by hand, like the dropdown list, so it scrolls under UIScale.
CanvasSize = UDim2.new(),
ScrollingEnabled = false,
LayoutOrder = 3,
Parent = frame,
})
scroller:SetAttribute("NoDrag", true)
local content = create("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
Parent = scroller,
})
local empty = label({
Size = UDim2.new(1, 0, 0, ROW),
Text = tostring(opts.EmptyText or "Nothing to order"),
TextSize = 13,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextXAlignment = Enum.TextXAlignment.Center,
Visible = false,
Parent = content,
})
-- A lucide icon, or a text glyph when the icon set didn't load.
local function iconButton(parent, icon, glyph, rotation, props)
props.BackgroundTransparency = 1
props.Text = ""
props.AutoButtonColor = false
props.Parent = parent
local button = create("TextButton", props)
local image = create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = TOUCH and UDim2.fromOffset(16, 16) or UDim2.fromOffset(14, 14),
BackgroundTransparency = 1,
ImageColor3 = Theme.Muted,
ScaleType = Enum.ScaleType.Fit,
Parent = button,
})
applyIcon(image, icon)
if image.Image ~= "" then
return button, { Object = image, Color = "ImageColor3", Fade = "ImageTransparency" }
end
image:Destroy()
local text_ = label({
Size = UDim2.fromScale(1, 1),
Text = glyph,
TextSize = 18,
TextColor3 = Theme.Muted,
TextXAlignment = Enum.TextXAlignment.Center,
TextTruncate = Enum.TextTruncate.None,
Rotation = rotation,
Parent = button,
})
return button, { Object = text_, Color = "TextColor3", Fade = "TextTransparency" }
end
local function tint(icon, color, transparency, duration)
tween(icon.Object, { [icon.Color] = color, [icon.Fade] = transparency }, duration)
end
local self_ = {}
local entries = {}
-- The last order Set asked for, including items not in the list right now,
-- so a saved order still applies once Refresh brings those items in.
local remembered = {}
local dragging = nil
local nudge, beginDrag
local function slotY(slot)
return 1 + (slot - 1) * STEP
end
local function values()
local list = {}
for index, entry in ipairs(entries) do
list[index] = entry.Value
end
return list
end
local function layout(instant)
local count = #entries
for slot, entry in ipairs(entries) do
entry.Index.Text = tostring(slot)
if entry ~= dragging then
tween(entry.Frame, { Position = UDim2.fromOffset(3, slotY(slot)) }, instant and 0 or 0.22, Enum.EasingStyle.Quint)
end
if entry.UpIcon then
tween(entry.UpIcon.Object, { [entry.UpIcon.Fade] = slot == 1 and 0.7 or 0 }, instant and 0 or 0.15)
tween(entry.DownIcon.Object, { [entry.DownIcon.Fade] = slot == count and 0.7 or 0 }, instant and 0 or 0.15)
end
end
end
local function fit()
local count = #entries
local canvas = math.max(count, 1) * STEP - GAP + 2
local shown = math.clamp(count, 1, maxRows)
local height = shown * STEP - GAP + 2
local scrolling = count > maxRows
scroller.Size = UDim2.new(1, 0, 0, height)
scroller.CanvasSize = UDim2.fromOffset(0, canvas)
content.Size = UDim2.new(1, scrolling and -8 or 0, 0, canvas)
if not dragging then
scroller.ScrollingEnabled = scrolling
end
empty.Visible = count == 0
local maxScroll = math.max(canvas - height, 0)
if scroller.CanvasPosition.Y > maxScroll then
scroller.CanvasPosition = Vector2.new(0, maxScroll)
end
end
local function changed(silent)
self_.Value = values()
if not silent then
safeCall(opts.Callback, values())
end
end
local function listScroller()
if scroller.ScrollingEnabled then
return scroller
end
return frame:FindFirstAncestorWhichIsA("ScrollingFrame")
end
local function newEntry(value)
local row = create("Frame", {
Size = UDim2.new(1, -6, 0, ROW),
BackgroundColor3 = Theme.Surface,
BorderSizePixel = 0,
Parent = content,
})
corner(row, UDim.new(0, 7))
local entry = { Value = value, Frame = row }
entry.Stroke = stroke(row)
entry.Scale = create("UIScale", { Parent = row })
local grip
grip, entry.GripIcon = iconButton(row, "grip-vertical", "=", 0, {
Size = UDim2.new(0, GRIP, 1, 0),
})
local x = GRIP
entry.Index = label({
Position = UDim2.fromOffset(x, 0),
Size = UDim2.new(0, 22, 1, 0),
TextSize = 12,
TextColor3 = Theme.Accent,
TextTruncate = Enum.TextTruncate.None,
Visible = numbered,
Parent = row,
})
if numbered then
x += 22
end
local right = arrows and ARROW * 2 + 8 or 10
entry.Text = label({
Position = UDim2.fromOffset(x, 0),
Size = UDim2.new(1, -(x + right), 1, 0),
Text = tostring(value),
TextSize = 13,
Parent = row,
})
local buttons = {}
if arrows then
local up, down
up, entry.UpIcon = iconButton(row, "chevron-up", "›", -90, {
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, -(ARROW + 6), 0, 0),
Size = UDim2.new(0, ARROW, 1, 0),
})
down, entry.DownIcon = iconButton(row, "chevron-down", "›", 90, {
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, -4, 0, 0),
Size = UDim2.new(0, ARROW, 1, 0),
})
for button, direction in pairs({ [up] = -1, [down] = 1 }) do
local icon = direction < 0 and entry.UpIcon or entry.DownIcon
local isTap = tapGuard(button, listScroller)
button.MouseEnter:Connect(function()
if not usingTouch() then
tween(icon.Object, { [icon.Color] = Theme.Accent }, 0.12)
end
end)
button.MouseLeave:Connect(function()
tween(icon.Object, { [icon.Color] = Theme.Muted }, 0.2)
end)
button.MouseButton1Click:Connect(function()
if isTap() then
nudge(entry, direction)
end
end)
table.insert(buttons, button)
end
end
grip.InputBegan:Connect(function(input)
if isPress(input) then
beginDrag(entry)
end
end)
-- On PC the whole row drags; on touch only the grip does, so a swipe
-- across the rows still scrolls.
row.InputBegan:Connect(function(input)
if input.UserInputType ~= Enum.UserInputType.MouseButton1 then
return
end
local point = pointerPosition()
for _, button in ipairs(buttons) do
if pointInside(point, button) then
return
end
end
beginDrag(entry)
end)
row.MouseEnter:Connect(function()
if not usingTouch() and entry ~= dragging then
tween(entry.Stroke, { Color = Theme.StrokeHover }, 0.12)
tint(entry.GripIcon, Theme.Text, 0, 0.12)
end
end)
row.MouseLeave:Connect(function()
if entry ~= dragging then
tween(entry.Stroke, { Color = Theme.Stroke }, 0.25)
tint(entry.GripIcon, Theme.Muted, 0, 0.25)
end
end)
return entry
end
-- Reuses the rows whose item is still in the list, so a reorder slides
-- rows instead of rebuilding them.
local function build(list)
local pool = {}
for _, entry in ipairs(entries) do
pool[entry.Value] = pool[entry.Value] or {}
table.insert(pool[entry.Value], entry)
end
local nextEntries = {}
for index, value in ipairs(list) do
local bucket = pool[value]
local entry = bucket and table.remove(bucket, 1)
if not entry then
entry = newEntry(value)
entry.Frame.Position = UDim2.fromOffset(3, slotY(index))
end
nextEntries[index] = entry
end
for _, bucket in pairs(pool) do
for _, entry in ipairs(bucket) do
entry.Frame:Destroy()
end
end
entries = nextEntries
end
-- items sorted by where they sit in ranking; the rest keep their order after.
local function arrange(items, ranking)
local left = {}
for _, value in ipairs(items) do
left[value] = (left[value] or 0) + 1
end
local result = {}
for _, pass in ipairs({ ranking, items }) do
for _, value in ipairs(pass) do
if (left[value] or 0) > 0 then
left[value] -= 1
table.insert(result, value)
end
end
end
return result
end
local function scrollIntoView(slot)
local top = slotY(slot) - 1
local view = scroller.AbsoluteWindowSize.Y / window.Scale.Scale
local y = scroller.CanvasPosition.Y
if top < y then
y = top
elseif top + ROW + 2 > y + view then
y = top + ROW + 2 - view
end
if y ~= scroller.CanvasPosition.Y then
tween(scroller, { CanvasPosition = Vector2.new(0, math.max(y, 0)) }, 0.2)
end
end
function nudge(entry, direction)
if dragging then
return
end
local from = table.find(entries, entry)
local to = from and from + direction
if not to or to < 1 or to > #entries then
return
end
entries[from], entries[to] = entries[to], entries[from]
layout(false)
tween(entry.Stroke, { Color = Theme.Accent }, 0.08)
task.delay(0.18, function()
if entry ~= dragging then
tween(entry.Stroke, { Color = Theme.Stroke }, 0.3)
end
end)
scrollIntoView(to)
changed(false)
end
local grabOffset, lastPointer = 0, nil
local startEntries, stopRender, lockedPage = nil, nil, nil
-- Moves the held row under the pointer and slides the others out of its way.
local function follow(pointer)
local scale = window.Scale.Scale
local y = (pointer.Y - grabOffset - scroller.AbsolutePosition.Y) / scale + scroller.CanvasPosition.Y
y = math.clamp(y, slotY(1), slotY(#entries))
dragging.Frame.Position = UDim2.fromOffset(3, y)
local slot = math.clamp(math.floor((y - 1) / STEP + 0.5) + 1, 1, #entries)
local current = table.find(entries, dragging)
if current ~= slot then
table.remove(entries, current)
table.insert(entries, slot, dragging)
layout(false)
end
end
-- Holding the row near the list's top or bottom edge scrolls it, faster
-- the closer the pointer gets.
local function autoScroll(deltaTime)
if not dragging or not lastPointer then
return
end
local scale = window.Scale.Scale
local top = scroller.AbsolutePosition.Y
local bottom = top + scroller.AbsoluteSize.Y
local zone = ROW * scale
local speed = 0
if lastPointer.Y < top + zone then
speed = -(top + zone - lastPointer.Y) / zone
elseif lastPointer.Y > bottom - zone then
speed = (lastPointer.Y - (bottom - zone)) / zone
end
if speed == 0 then
return
end
local maxScroll = math.max(scroller.CanvasSize.Y.Offset - scroller.AbsoluteWindowSize.Y / scale, 0)
local y = math.clamp(scroller.CanvasPosition.Y + math.clamp(speed, -1.5, 1.5) * 360 * deltaTime, 0, maxScroll)
if y ~= scroller.CanvasPosition.Y then
scroller.CanvasPosition = Vector2.new(0, y)
follow(lastPointer)
end
end
function beginDrag(entry)
if dragging or #entries < 2 or self_._destroyed then
return
end
dragging = entry
startEntries = table.clone(entries)
lastPointer = pointerPosition()
grabOffset = lastPointer.Y - entry.Frame.AbsolutePosition.Y
entry.Frame.ZIndex = 5
tween(entry.Frame, { BackgroundColor3 = Theme.Surface3 }, 0.12)
tween(entry.Stroke, { Color = Theme.Accent }, 0.12)
tween(entry.Scale, { Scale = 1.02 }, 0.18, Enum.EasingStyle.Back)
tint(entry.GripIcon, Theme.Accent, 0, 0.12)
-- Neither the list nor the page scrolls under the finger while dragging.
scroller.ScrollingEnabled = false
local page = frame:FindFirstAncestorWhichIsA("ScrollingFrame")
if page and page.ScrollingEnabled then
lockedPage = page
page.ScrollingEnabled = false
end
stopRender = window:_listen("Render", autoScroll)
end
local function endDrag(silent)
local entry = dragging
if not entry then
return
end
dragging = nil
lastPointer = nil
if stopRender then
stopRender()
stopRender = nil
end
if lockedPage then
lockedPage.ScrollingEnabled = true
lockedPage = nil
end
tween(entry.Frame, { BackgroundColor3 = Theme.Surface }, 0.2)
tween(entry.Stroke, { Color = Theme.Stroke }, 0.25)
tween(entry.Scale, { Scale = 1 }, 0.2)
tint(entry.GripIcon, Theme.Muted, 0, 0.25)
task.delay(0.22, function()
if entry ~= dragging then
entry.Frame.ZIndex = 1
end
end)
layout(false)
fit()
for index, other in ipairs(entries) do
if startEntries[index] ~= other then
changed(silent)
break
end
end
end
window:_listen("Changed", function(input)
if dragging and isMove(input) then
lastPointer = pointerPosition()
follow(lastPointer)
end
end, self_)
window:_listen("Ended", function(input)
if dragging and isPress(input) then
endDrag(false)
end
end, self_)
table.insert(self_._listeners, function()
endDrag(true)
end)
self_._onHide = function()
endDrag(false)
end
-- Reorders the items already in the list. Unknown items are remembered for
-- a later Refresh; items missing from the list keep their order after.
function self_:Set(list, silent)
if type(list) ~= "table" then
return
end
endDrag(true)
remembered = table.clone(list)
build(arrange(values(), remembered))
layout(false)
fit()
changed(silent)
end
function self_:Get()
return values()
end
-- Replaces the items. Items that were already there keep the player's
-- order unless keepOrder is false; new ones go to the end.
function self_:Refresh(items, keepOrder, silent)
endDrag(true)
items = type(items) == "table" and table.clone(items) or {}
if keepOrder ~= false then
items = arrange(items, self_:_saveValue())
else
remembered = {}
end
build(items)
layout(false)
fit()
changed(silent)
end
-- The shown order, then remembered items that aren't in the list right now.
function self_:_saveValue()
local list = values()
local left = {}
for _, value in ipairs(list) do
left[value] = (left[value] or 0) + 1
end
for _, value in ipairs(remembered) do
if (left[value] or 0) > 0 then
left[value] -= 1
else
table.insert(list, value)
end
end
return list
end
build(type(opts.Items) == "table" and opts.Items or {})
layout(true)
fit()
self_.Value = values()
return finishElement(self, opts, self_, frame, "OrderList")
end
for name, method in pairs(table.clone(Tab)) do
if type(method) == "function" and name:sub(1, 1) ~= "_" and name:sub(1, 6) ~= "Create" then
Tab["Create" .. name] = method
end
end
local function detectExecutor()
local name
pcall(function()
if typeof(identifyexecutor) == "function" then
name = (identifyexecutor())
elseif typeof(getexecutorname) == "function" then
name = getexecutorname()
end
end)
if type(name) == "string" and #name > 0 then
return name
end
return RunService:IsStudio() and "Studio" or "Unknown"
end
local function executorReport(opts)
local executor = detectExecutor()
local supported = opts.SupportedExecutors
if type(supported) == "table" then
local lowered = string.lower(executor)
for _, name in ipairs(supported) do
if string.find(lowered, string.lower(tostring(name)), 1, true) then
return executor, true, "Your executor is supported and fully compatible."
end
end
return executor, false, "Not on the supported list. Some features may not work."
end
local capabilities = {
{ "writefile", writefile },
{ "readfile", readfile },
{ "getcustomasset", getcustomasset },
{ "setclipboard", setclipboard or toclipboard },
{ "request", request or http_request or (syn and syn.request) },
}
local missing = {}
for _, entry in ipairs(capabilities) do
if type(entry[2]) ~= "function" then
table.insert(missing, entry[1])
end
end
if #missing == 0 then
return executor, true, "Your executor is supported and fully compatible."
end
return executor, false, "Missing " .. table.concat(missing, ", ") .. ". Some features may not work."
end
-- "Don't ask again" on the unsupported executor warning: the executor's
-- name goes in a file in the script folder, and the warning is skipped while
-- the same executor is in use. Resetting the folder brings it back.
-- A table, not separate locals: the main chunk is at Luau's 200 local limit.
local UnsupportedSkip = {}
function UnsupportedSkip.Path(folder)
return tostring(folder) .. "/unsupported_skip.txt"
end
function UnsupportedSkip.Available()
return type(writefile) == "function" and type(readfile) == "function" and type(isfile) == "function"
end
function UnsupportedSkip.Saved(folder, executor)
if not UnsupportedSkip.Available() then
return false
end
local path = UnsupportedSkip.Path(folder)
local ok, saved = pcall(function()
return isfile(path) and readfile(path) or nil
end)
return ok and saved == executor
end
function UnsupportedSkip.Write(folder, path, content)
if not UnsupportedSkip.Available() then
return false
end
return pcall(function()
if type(isfolder) == "function" and type(makefolder) == "function" then
local built = ""
for segment in tostring(folder):gmatch("[^/\\]+") do
built = built == "" and segment or built .. "/" .. segment
if not isfolder(built) then
makefolder(built)
end
end
end
writefile(path, content)
end)
end
function UnsupportedSkip.Save(folder, executor)
return UnsupportedSkip.Write(folder, UnsupportedSkip.Path(folder), executor)
end
-- Custom disclaimers ticked "Don't show again": one key per line in
-- disclaimers.txt. The key is the disclaimer's Id, or a hash of its title
-- and text, so rewording a disclaimer shows it again.
function UnsupportedSkip.DisclaimerKey(entry)
if entry.Id ~= nil then
return (tostring(entry.Id):gsub("[\r\n]", " "))
end
local source = tostring(entry.Title) .. "\0" .. tostring(entry.Text)
local hash = 5381
for index = 1, #source do
hash = (hash * 33 + source:byte(index)) % 4294967296
end
return string.format("%08x", hash)
end
function UnsupportedSkip.DisclaimerAccepted(folder, key)
if not UnsupportedSkip.Available() then
return false
end
local path = tostring(folder) .. "/disclaimers.txt"
local ok, saved = pcall(function()
return isfile(path) and readfile(path) or ""
end)
if not ok or type(saved) ~= "string" then
return false
end
for line in saved:gmatch("[^\r\n]+") do
if line == key then
return true
end
end
return false
end
function UnsupportedSkip.AcceptDisclaimer(folder, key)
if UnsupportedSkip.DisclaimerAccepted(folder, key) then
return true
end
local path = tostring(folder) .. "/disclaimers.txt"
local ok, saved = pcall(function()
return isfile(path) and readfile(path) or ""
end)
saved = ok and type(saved) == "string" and saved or ""
if saved ~= "" and not saved:match("\n$") then
saved ..= "\n"
end
return UnsupportedSkip.Write(folder, path, saved .. key .. "\n")
end
local gameInfoCache = nil
local function fetchGameInfo()
if gameInfoCache == nil then
local ok, info = pcall(function()
return MarketplaceService:GetProductInfo(game.PlaceId)
end)
gameInfoCache = ok and type(info) == "table" and info or false
end
return gameInfoCache or nil
end
local function detectGameName()
local info = fetchGameInfo()
if info and info.Name then
return info.Name
end
return RunService:IsStudio() and "Studio" or "Unknown game"
end
local REGION_NAMES = {
US = "United States", GB = "United Kingdom", DE = "Germany", FR = "France",
NL = "Netherlands", SG = "Singapore", JP = "Japan", AU = "Australia",
BR = "Brazil", IN = "India", HK = "Hong Kong", CA = "Canada",
}
local function detectRegion(callback)
task.spawn(function()
local requester = request or http_request or (syn and syn.request) or (http and http.request)
local region
if type(requester) == "function" then
pcall(function()
local response = requester({ Url = "https://ipinfo.io/json", Method = "GET" })
local body = type(response) == "table" and (response.Body or response.body) or nil
if type(body) == "string" then
local data = HttpService:JSONDecode(body)
if type(data) == "table" and data.country then
local label = REGION_NAMES[data.country] or data.country
region = data.city and (data.city .. ", " .. label) or label
end
end
end)
end
callback(region or "Unavailable")
end)
end
local NOTIFY_COLORS = setmetatable({}, {
__index = function(_, kind)
if kind == "Success" or kind == "Warning" or kind == "Error" then
return Theme[kind]
end
return nil
end,
})
local Window = {}
Window.__index = Window
function Library.Window(_, opts)
opts = normalize(opts, { Name = "Title", LoadingSubtitle = "Subtitle", ToggleUIKeybind = "Keybind" })
if type(opts.Keybind) == "string" then
opts.Keybind = Enum.KeyCode[opts.Keybind]
end
local size = opts.Size or UDim2.fromOffset(760, 520)
local keybind = opts.Keybind or Enum.KeyCode.RightControl
local self = setmetatable({
Tabs = {},
CurrentTab = nil,
Open = true,
Keybind = keybind,
_connections = {},
_controls = {},
_inputListeners = { Began = {}, Changed = {}, Ended = {}, Render = {} },
_frameSteps = {},
_destroyed = false,
_identity = { Names = {}, Avatars = {} },
_pinned = {},
Minimized = false,
_hideName = false,
_hideAvatar = false,
Title = opts.Title or opts.Name or "XYZ",
_logoIcon = opts.Icon or Assets.Logo,
_sessionStart = os.clock(),
_islandLocked = opts.IslandDraggable == false,
}, Window)
local saving = type(opts.ConfigurationSaving) == "table" and opts.ConfigurationSaving or {}
self.ConfigFolder = saving.FolderName or "AirflowUI"
self.ConfigName = saving.FileName
self._configListeners = {}
self._pendingFlags = {}
local startTheme = opts.DefaultTheme or opts.Theme
if startTheme ~= nil then
Library.DefaultTheme = startTheme
end
self:_applyDefaultTheme()
self._uiScale = math.clamp(tonumber(opts.UIScale) or 1, 0.6, 1.5)
self._adaptive = opts.AdaptiveSize ~= false
self.UIScale = self._uiScale
if opts.Density ~= nil then
Library:SetDensity(opts.Density)
end
if opts.CornerRadius ~= nil then
Library:SetCornerRadius(opts.CornerRadius, true)
end
local function dispatch(kind)
return function(...)
for _, handler in ipairs(self._inputListeners[kind]) do
handler(...)
end
end
end
local renderDispatch = dispatch("Render")
table.insert(self._connections, RunService.RenderStepped:Connect(function(deltaTime)
renderDispatch(deltaTime)
for _, step in ipairs(self._frameSteps) do
step(deltaTime)
end
end))
table.insert(self._connections, UserInputService.InputBegan:Connect(dispatch("Began")))
table.insert(self._connections, UserInputService.InputChanged:Connect(dispatch("Changed")))
table.insert(self._connections, UserInputService.InputEnded:Connect(dispatch("Ended")))
local gui = create("ScreenGui", {
Name = opts.Name or "XYZUI",
IgnoreGuiInset = true,
ResetOnSpawn = false,
DisplayOrder = 999,
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
})
self.Gui = gui
local root = create("Frame", {
Name = "Window",
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = size,
BackgroundTransparency = 1,
Parent = gui,
})
self.Root = root
local scale = create("UIScale", { Parent = root })
self.Scale = scale
local shadow = create("ImageLabel", {
Position = UDim2.fromOffset(-25, -25),
Size = UDim2.new(1, 50, 1, 50),
BackgroundTransparency = 1,
Image = Assets.Shadow,
ImageColor3 = Color3.new(0, 0, 0),
ImageTransparency = 0.6,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(49, 49, 450, 450),
Parent = root,
})
self.Shadow = shadow
self._shadowRest = 0.6
-- The body is a plain Frame so resizing never re-renders the window into a
-- texture. It only moves into the fader CanvasGroup while fading.
local fader = create("CanvasGroup", {
Name = "Fader",
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
GroupTransparency = 1,
ZIndex = 2,
Parent = root,
})
self._fader = fader
self._bodyAlpha = 1
local body = create("Frame", {
Name = "Body",
Size = UDim2.fromScale(1, 1),
BackgroundColor3 = Theme.Background,
BorderSizePixel = 0,
ClipsDescendants = true,
ZIndex = 2,
Parent = fader,
})
self.Body = body
corner(body, UDim.new(0, 10))
self.BodyStroke = stroke(body, Theme.Stroke)
edgeHighlight(body)
-- Background image: made first so everything else in the body draws over it.
self._backgroundImage = create("ImageLabel", {
Name = "BackgroundImage",
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
ScaleType = Enum.ScaleType.Crop,
ImageTransparency = 1,
Visible = false,
ZIndex = 0,
Parent = body,
})
corner(self._backgroundImage, UDim.new(0, 10))
self.BackgroundTransparency = math.clamp(tonumber(opts.BackgroundTransparency) or 0.35, 0, 1)
self._backgroundGeneration = 0
-- The sidebar is a flush, solid rail down the left edge, split from the
-- content by a single hairline.
local sidebar = create("Frame", {
Name = "Sidebar",
Size = UDim2.new(0, SIDEBAR_WIDTH, 1, 0),
BackgroundTransparency = 1,
BorderSizePixel = 0,
ClipsDescendants = true,
Parent = body,
})
self.Sidebar = sidebar
-- One rounded fill that runs past the rail's right edge, where the rail
-- clips it: the left corners round with the window, the right edge is
-- square, and nothing overlaps, so it fades evenly when see-through.
local sidebarFill = create("Frame", {
Name = "Fill",
Size = UDim2.new(1, 24, 1, 0),
BackgroundColor3 = Theme.Surface,
BorderSizePixel = 0,
ZIndex = 0,
Parent = sidebar,
})
corner(sidebarFill, UDim.new(0, 10))
self.SidebarFill = sidebarFill
create("Frame", {
Name = "Divider",
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.fromScale(1, 0),
Size = UDim2.new(0, 1, 1, 0),
BackgroundColor3 = Theme.Stroke,
BorderSizePixel = 0,
ZIndex = 2,
Parent = sidebar,
})
local header = create("Frame", {
Name = "Header",
Size = UDim2.new(1, 0, 0, HEADER_HEIGHT),
BackgroundTransparency = 1,
Parent = sidebar,
})
local logoText = create("TextLabel", {
Text = "Xyz",
Position = UDim2.fromOffset(16, 7),
Size = UDim2.fromOffset(52, 30),
BackgroundTransparency = 1,
TextColor3 = Color3.fromRGB(255, 255, 255),
Font = Enum.Font.GothamBlack,
TextSize = 28,
TextXAlignment = Enum.TextXAlignment.Left,
Parent = header,
})
logoText:SetAttribute("NoTheme", true)
local logoGrad = create("UIGradient", {
Color = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.fromRGB(150, 150, 158)),
ColorSequenceKeypoint.new(0.5, Color3.fromRGB(255, 255, 255)),
ColorSequenceKeypoint.new(1, Color3.fromRGB(150, 150, 158)),
}),
Offset = Vector2.new(-1, 0),
Parent = logoText,
})
TweenService:Create(logoGrad, TweenInfo.new(2.4, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut, -1, false, 1.2), { Offset = Vector2.new(1, 0) }):Play()
label({
Position = UDim2.fromOffset(70, 7),
Size = UDim2.fromOffset(66, 30),
Text = "Scripts",
TextSize = 17,
FontFace = Fonts.Bold,
TextColor3 = Theme.Muted,
Parent = header,
})
local subtitle = opts.Version or opts.Subtitle
if subtitle then
label({
Position = UDim2.fromOffset(18, 40),
Size = UDim2.new(1, -30, 0, 14),
Text = tostring(subtitle),
TextSize = 12,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
Parent = header,
})
end
local profileEnabled = opts.Profile ~= false
local tabList = create("ScrollingFrame", {
Name = "Tabs",
Position = UDim2.fromOffset(0, HEADER_HEIGHT),
Size = UDim2.new(1, 0, 1, -(HEADER_HEIGHT + (profileEnabled and PROFILE_HEIGHT + 4 or 12))),
BackgroundTransparency = 1,
BorderSizePixel = 0,
ScrollBarThickness = 0,
ScrollingDirection = Enum.ScrollingDirection.Y,
AutomaticCanvasSize = Enum.AutomaticSize.Y,
CanvasSize = UDim2.new(),
Parent = sidebar,
})
self.TabList = tabList
-- The highlight sits beside the stack of buttons, so it scrolls and clips
-- with them and glides between tiles.
local indicator = create("Frame", {
Position = UDim2.fromOffset(8, 2),
Size = UDim2.new(1, -16, 0, TAB_TILE_HEIGHT),
BackgroundColor3 = Theme.Surface3,
BackgroundTransparency = 0.35,
BorderSizePixel = 0,
Visible = false,
Parent = tabList,
})
corner(indicator, UDim.new(0, 7))
-- The active tab is just a quiet neutral fill with white text; the bar
-- is kept for callers but stays hidden.
local pill = create("Frame", {
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, 0, 0.5, 0),
Size = UDim2.fromOffset(2, 14),
BackgroundColor3 = Theme.Accent,
Visible = false,
BorderSizePixel = 0,
Parent = indicator,
})
corner(pill, UDim.new(1, 0))
self._indicatorPill = pill
self.Indicator = indicator
local stack = create("Frame", {
Name = "Stack",
Position = UDim2.fromOffset(8, 2),
Size = UDim2.new(1, -16, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
ZIndex = 2,
Parent = tabList,
})
create("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 2),
Parent = stack,
})
self._tabStack = stack
-- More tabs than fit: a fade and a small "more" chip at the hidden end.
self:_overflowHint(sidebar, tabList, stack, true, Theme.Surface)
if profileEnabled then
self:_buildProfile(sidebar)
end
local content = create("Frame", {
Name = "Content",
Position = UDim2.fromOffset(SIDEBAR_WIDTH + 1, 0),
Size = UDim2.new(1, -(SIDEBAR_WIDTH + 1), 1, 0),
BackgroundTransparency = 1,
ClipsDescendants = true,
Parent = body,
})
self.Content = content
self:_sidebarResize(sidebar, tonumber(opts.SidebarWidth) or SIDEBAR_WIDTH)
self._outLayer = create("CanvasGroup", {
Name = "TransitionOut",
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
Visible = false,
ZIndex = 2,
Parent = content,
})
self._inLayer = create("CanvasGroup", {
Name = "TransitionIn",
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
Visible = false,
ZIndex = 3,
Parent = content,
})
-- Top-right controls: "-" minimises to the small bar, "X" unloads.
local function controlButton(right, closeStyle)
local button = create("TextButton", {
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, right, 0, 14),
Size = UDim2.fromOffset(34, 34),
BackgroundColor3 = closeStyle and Theme.Error or Theme.Surface3,
BackgroundTransparency = 1,
Text = "",
AutoButtonColor = false,
ZIndex = 5,
Parent = content,
})
button:SetAttribute("NoDrag", true)
corner(button, UDim.new(0, 8))
local parts = {}
if closeStyle then
for _, rotation in ipairs({ 45, -45 }) do
parts[#parts + 1] = create("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromOffset(16, 2),
Rotation = rotation,
BackgroundColor3 = Theme.Muted,
BorderSizePixel = 0,
Parent = button,
})
end
else
parts[1] = create("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromOffset(14, 2),
BackgroundColor3 = Theme.Muted,
BorderSizePixel = 0,
Parent = button,
})
end
for _, part in ipairs(parts) do
corner(part, UDim.new(1, 0))
end
button.MouseEnter:Connect(function()
tween(button, { BackgroundTransparency = closeStyle and 0.35 or 0.2 }, 0.15, Enum.EasingStyle.Quint)
for _, part in ipairs(parts) do
tween(part, { BackgroundColor3 = Theme.Text }, 0.15, Enum.EasingStyle.Quint)
end
end)
button.MouseLeave:Connect(function()
tween(button, { BackgroundTransparency = 1 }, 0.2, Enum.EasingStyle.Quint)
for _, part in ipairs(parts) do
tween(part, { BackgroundColor3 = Theme.Muted }, 0.2, Enum.EasingStyle.Quint)
end
end)
return button
end
local closeButton = controlButton(-14, true)
local minimizeButton = controlButton(-52, false)
minimizeButton.MouseButton1Click:Connect(function()
self:Minimize()
end)
closeButton.MouseButton1Click:Connect(function()
Library:Confirm({
Title = "Unload XYZ?",
Content = "Every feature is switched off and the menu closes.",
ConfirmText = "Unload",
Callback = function()
if type(opts.OnUnload) == "function" then
task.spawn(opts.OnUnload)
else
self:Destroy()
end
end,
})
end)
self.MinimizeButton = minimizeButton
self.CloseButton = closeButton
if opts.Search ~= false then
self:_buildSearch(content)
end
local notifyHolder = create("Frame", {
Name = "Notifications",
AnchorPoint = Vector2.new(1, 1),
Position = UDim2.new(1, -20, 1, -20),
Size = UDim2.new(0, 280, 1, -40),
BackgroundTransparency = 1,
Parent = gui,
})
local function fitToasts()
notifyHolder.Size = UDim2.new(0, math.min(280, gui.AbsoluteSize.X - 40), 1, -40)
end
table.insert(self._connections, gui:GetPropertyChangedSignal("AbsoluteSize"):Connect(fitToasts))
create("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
VerticalAlignment = Enum.VerticalAlignment.Bottom,
Padding = UDim.new(0, 4),
Parent = notifyHolder,
})
self.NotifyHolder = notifyHolder
self._notifyOrder = 0
self._toasts = {}
self.MaxNotifications = opts.MaxNotifications or 4
self._controlsDirty = true
table.insert(self._connections, body.DescendantAdded:Connect(function()
self._controlsDirty = true
end))
table.insert(self._connections, body.DescendantRemoving:Connect(function()
self._controlsDirty = true
end))
self:SetDragStyle(opts.DragStyle or (opts.DragSkeleton == false and "Off" or "Outline"))
self:SetIslandStyle(opts.IslandStyle)
if opts.Transparent then
self:SetTransparent(true)
end
if opts.Background then
task.spawn(self.SetBackground, self, opts.Background)
end
self:_enableDrag()
self.MaxSize = opts.MaxSize
self.KeepOnScreen = opts.KeepOnScreen ~= false
self:_enableResize(opts.MinSize or Vector2.new(480, 360))
-- The window's exact size rides along in configs as a hidden flag.
if opts.SaveSize ~= false then
local window = self
local sizeFlag = { _type = "WindowSize", _listeners = {} }
function sizeFlag:Get()
local current = window:GetSize()
return { Width = current.X, Height = current.Y }
end
function sizeFlag:Set(value)
if type(value) == "table" and tonumber(value.Width) and tonumber(value.Height) then
window:SetSize(Vector2.new(tonumber(value.Width), tonumber(value.Height)))
end
end
local flag = type(opts.SaveSize) == "string" and opts.SaveSize or "__WindowSize"
Library.Flags[flag] = sizeFlag
self:_flagCreated(flag, sizeFlag)
end
table.insert(self._connections, UserInputService.InputBegan:Connect(function(input, gameProcessed)
if gameProcessed then
return
end
if input.UserInputType == Enum.UserInputType.Keyboard and input.KeyCode == self.Keybind then
task.defer(function()
local consumed = self._consumedKey == input.KeyCode and os.clock() - (self._consumedAt or 0) < 0.2
self._consumedKey = nil
if not consumed and not self._destroyed then
if self.Minimized then
self:Restore()
else
self:Toggle(not self.Open)
end
end
end)
end
end))
pcall(function()
if typeof(syn) == "table" and typeof(syn.protect_gui) == "function" then
syn.protect_gui(gui)
end
end)
gui.Parent = opts.Parent or defaultParent()
scale.Scale = 0.9
shadow.ImageTransparency = 1
self.BodyStroke.Transparency = 1
root.Visible = false
self:_fitToScreen(true)
table.insert(self._connections, gui:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
self:_fitToScreen()
self:_clampToScreen()
end))
local toggleOpts = opts.ToggleButton
if toggleOpts == nil or toggleOpts == true then
toggleOpts = {}
end
if type(toggleOpts) == "table" then
self:_createToggleButton(toggleOpts)
end
-- The toggle button already brings a hidden window back.
local toggleShown = self.ToggleButton ~= nil and self:_toggleButtonAllowed()
if (opts.OpenButton ~= nil and opts.OpenButton ~= false) or (opts.OpenButton == nil and TOUCH and not toggleShown) then
self:_createOpenButton(type(opts.OpenButton) == "table" and opts.OpenButton or {})
end
if opts.Backdrop ~= false then
self:_buildBackdrop(type(opts.Backdrop) == "table" and opts.Backdrop or {})
end
self._introDone = false
table.insert(Library.Windows, self)
if opts.Home ~= false then
self:_buildHome(type(opts.Home) == "table" and opts.Home or {})
end
local loading = opts.Loading
if type(loading) == "table" then
opts.LoadingDuration = loading.Duration or opts.LoadingDuration
opts.LoadingText = loading.Text or loading.Subtitle or opts.LoadingText
opts.LoadingSteps = loading.Steps or opts.LoadingSteps
opts.LoadingTitle = loading.Title or opts.LoadingTitle
loading = loading.Enabled ~= false
end
-- An executor off the supported list (or missing the file / asset API)
-- stops at a warning card before the window opens.
local prompts = {}
local gate = opts.UnsupportedExecutor
if gate ~= false then
gate = type(gate) == "table" and gate or {}
local list = opts.SupportedExecutors or (type(opts.Home) == "table" and opts.Home.SupportedExecutors or nil)
local executor, supported, message = executorReport({ SupportedExecutors = list })
if not supported and not UnsupportedSkip.Saved(self.ConfigFolder, executor) then
local block = gate.Block == true
local folder = self.ConfigFolder
table.insert(prompts, {
Title = gate.Title or "Unsupported executor",
Subtitle = executor,
Text = block and ((gate.Text or message) .. " This script can't run here.") or (gate.Text or message),
Icon = "shield-alert",
Color = Theme.Warning,
ContinueText = "Continue anyway",
ExitText = "Exit",
Block = block,
RememberText = "Don't ask again on this executor",
Remember = function()
UnsupportedSkip.Save(folder, executor)
end,
})
end
end
-- Custom disclaimers queue up after the executor warning, in order. Each
-- one must be accepted (or skipped by an earlier "Don't show again").
local disclaimers = opts.Disclaimer or opts.Disclaimers
if type(disclaimers) == "string" or (type(disclaimers) == "table" and (disclaimers.Title ~= nil or disclaimers.Text ~= nil)) then
disclaimers = { disclaimers }
end
if type(disclaimers) == "table" then
for _, entry in ipairs(disclaimers) do
if type(entry) == "string" then
entry = { Text = entry }
end
if type(entry) == "table" then
local key = UnsupportedSkip.DisclaimerKey(entry)
local folder = self.ConfigFolder
local block = entry.Block == true
local rememberable = entry.Remember ~= false and not block
if not (rememberable and UnsupportedSkip.DisclaimerAccepted(folder, key)) then
local callback = entry.Callback
table.insert(prompts, {
Title = entry.Title or "Disclaimer",
Subtitle = entry.Subtitle,
Text = entry.Text or "",
Icon = entry.Icon or "info",
Color = entry.Color or Theme.Accent,
ContinueText = entry.AcceptText or "I understand",
ExitText = entry.DeclineText == nil and "Exit" or entry.DeclineText,
RememberText = rememberable and (entry.RememberText or "Don't show again") or nil,
Remember = function()
UnsupportedSkip.AcceptDisclaimer(folder, key)
end,
Callback = type(callback) == "function" and callback or nil,
Block = block,
})
end
end
end
end
if #prompts > 0 then
opts._prompts = prompts
end
if loading == false and opts._prompts then
opts.LoadingDuration = opts.LoadingDuration or 0.5
loading = true
end
if loading == false then
task.defer(function()
self:_playIntro()
end)
else
self:_showLoader(opts)
end
return self
end
Library.CreateWindow = Library.Window
function Library:Notify(opts)
local window = Library.Windows[#Library.Windows]
if window then
return window:Notify(opts)
end
end
function Library:Confirm(opts)
local window = Library.Windows[#Library.Windows]
if window then
return window:Confirm(opts)
end
end
function Library:Dialog(opts)
local window = Library.Windows[#Library.Windows]
if window then
return window:Dialog(opts)
end
end
local function isShown(object, stopAt, point)
local child = object
while child and child ~= stopAt and child:IsA("GuiObject") do
if not child.Visible then
return false
end
local parent = child.Parent
if parent and parent ~= stopAt and parent:IsA("GuiObject") and (parent.ClipsDescendants or parent:IsA("ScrollingFrame")) and not pointInside(point, parent) then
return false
end
child = parent
end
return true
end
function Window:_refreshControls()
local controls = {}
for _, object in ipairs(self.Body:GetDescendants()) do
if object:IsA("GuiButton") or object:IsA("TextBox") or object:GetAttribute("NoDrag") then
table.insert(controls, object)
end
end
self._controls = controls
self._controlsDirty = false
end
function Window:_overControl(point)
if self._dialog or self:_overPopout(point) then
return true
end
if self._controlsDirty then
self:_refreshControls()
end
for _, object in ipairs(self._controls) do
if object.Parent and pointInside(point, object) and isShown(object, self.Body, point) then
return true
end
end
return false
end
-- Tooltips: one shared card per window that follows the pointer over any
-- element built with Tooltip = "..." or { Title, Text, Icon }. It appears
-- after a short hover (straight away when moving from one tooltip to the
-- next), hides on press, and shows on a long press on touch screens.
function Window:_tooltipCard()
if self._tip then
return self._tip
end
local holder = create("CanvasGroup", {
Name = "Tooltip",
Size = UDim2.fromOffset(0, 0),
BackgroundTransparency = 1,
GroupTransparency = 1,
Visible = false,
ZIndex = 60,
Parent = self.Gui,
})
local tipScale = create("UIScale", { Parent = holder })
-- Bare text, no card: a dark outline keeps it readable over anything.
-- The holder is sized from the text by hand, since scale sizes inside an
-- automatic size would stretch it across the screen.
local PAD = 4
local panel = create("Frame", {
Position = UDim2.fromOffset(PAD, PAD),
Size = UDim2.fromOffset(0, 0),
AutomaticSize = Enum.AutomaticSize.XY,
BackgroundTransparency = 1,
Parent = holder,
})
create("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 2),
Parent = panel,
})
local function outline(object)
create("UIStroke", {
Color = Color3.new(0, 0, 0),
Transparency = 0.35,
Thickness = 1,
ApplyStrokeMode = Enum.ApplyStrokeMode.Contextual,
Parent = object,
})
end
local head = create("Frame", {
Size = UDim2.fromOffset(0, 18),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundTransparency = 1,
LayoutOrder = 1,
Parent = panel,
})
create("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 6),
Parent = head,
})
local icon = create("ImageLabel", {
Size = UDim2.fromOffset(14, 14),
BackgroundTransparency = 1,
ImageColor3 = Theme.Accent,
ScaleType = Enum.ScaleType.Fit,
LayoutOrder = 1,
Parent = head,
})
local title = label({
Size = UDim2.fromOffset(0, 18),
AutomaticSize = Enum.AutomaticSize.X,
TextSize = 13,
FontFace = Fonts.Bold,
TextTruncate = Enum.TextTruncate.None,
LayoutOrder = 2,
Parent = head,
})
outline(title)
local body = label({
Size = UDim2.fromOffset(0, 0),
AutomaticSize = Enum.AutomaticSize.XY,
TextSize = 12,
FontFace = Fonts.Medium,
TextWrapped = true,
TextTruncate = Enum.TextTruncate.None,
TextYAlignment = Enum.TextYAlignment.Top,
LineHeight = 1.1,
LayoutOrder = 2,
Parent = panel,
})
outline(body)
create("UISizeConstraint", { MaxSize = Vector2.new(240, math.huge), Parent = body })
local function fit()
local scale = math.max(tipScale.Scale, 0.01)
local size = panel.AbsoluteSize / scale
holder.Size = UDim2.fromOffset(size.X + PAD * 2, size.Y + PAD * 2)
end
panel:GetPropertyChangedSignal("AbsoluteSize"):Connect(fit)
local tip = {
Holder = holder,
Scale = tipScale,
Pad = PAD,
Head = head,
Icon = icon,
Title = title,
Body = body,
Shown = false,
Fit = fit,
}
self._tip = tip
-- Follow the pointer, and drop the card once its element is gone or off
-- screen (tab switched, window hidden or minimized).
self:_listen("Render", function(deltaTime)
local owner = self._tipOwner
if not owner then
return
end
local frame = owner._frame
if owner._destroyed or not self.Open or self.Minimized or not frame or not frame.Parent
or (owner._isShown and not owner:_isShown()) or not isShown(frame, self.Body, frame.AbsolutePosition + frame.AbsoluteSize / 2) then
self:_hideTooltip()
return
end
local pointer = pointerPosition() - self.Gui.AbsolutePosition
local pad = tip.Pad * tipScale.Scale
local size = holder.AbsoluteSize - Vector2.one * pad * 2
local view = self.Gui.AbsoluteSize
local x, y
if tip.Touch then
x, y = pointer.X - size.X / 2, pointer.Y - size.Y - 30
else
x, y = pointer.X + 14, pointer.Y + 20
if x + size.X > view.X - 8 then
x = pointer.X - size.X - 10
end
if y + size.Y > view.Y - 8 then
y = pointer.Y - size.Y - 12
end
end
local target = Vector2.new(
math.clamp(x, 8, math.max(8, view.X - size.X - 8)),
math.clamp(y, 8, math.max(8, view.Y - size.Y - 8))
)
local current = tip.Position
if not current or tip.Snap then
-- Rises a few pixels into place as it fades in.
current = target + Vector2.new(0, 6)
tip.Snap = false
else
current = current:Lerp(target, math.min(deltaTime * 22, 1))
end
tip.Position = current
holder.Position = UDim2.fromOffset(math.floor(current.X - pad + 0.5), math.floor(current.Y - pad + 0.5))
end)
-- A press anywhere (a dropdown chip inside the card, a key) hides it.
self:_listen("Began", function(input)
if self._tipOwner and not tip.Touch and (input.UserInputType == Enum.UserInputType.MouseButton1
or input.UserInputType == Enum.UserInputType.MouseButton2 or input.UserInputType == Enum.UserInputType.Keyboard) then
self:_hideTooltip()
end
end)
return tip
end
function Window:_showTooltip(element, touch)
local value = element._tooltip
local text, titleText, iconName
if type(value) == "table" then
text, titleText, iconName = value.Text or value.Content or value.Desc, value.Title or value.Name, value.Icon
elseif value ~= nil and value ~= false then
text = tostring(value)
end
if (text == nil or text == "") and (titleText == nil or titleText == "") then
return
end
local tip = self:_tooltipCard()
tip.Title.Text = titleText or ""
tip.Title.Visible = titleText ~= nil and titleText ~= ""
tip.Icon.Visible = iconName ~= nil
if iconName ~= nil then
applyIcon(tip.Icon, iconName)
end
tip.Head.Visible = tip.Title.Visible or iconName ~= nil
tip.Body.Text = text or ""
tip.Body.Visible = text ~= nil and text ~= ""
tip.Fit()
tip.Body.TextColor3 = Theme.Text
tip.Scale.Scale = math.max(self.Scale.Scale, 0.75)
tip.Touch = touch == true
local wasShown = tip.Shown
self._tipOwner = element
tip.Shown = true
tip.Generation = (tip.Generation or 0) + 1
local holder = tip.Holder
if not wasShown then
tip.Snap = true
holder.Visible = true
holder.GroupTransparency = 1
tip.Scale.Scale = tip.Scale.Scale * 0.96
end
tween(holder, { GroupTransparency = 0 }, 0.14, Enum.EasingStyle.Quad)
tween(tip.Scale, { Scale = math.max(self.Scale.Scale, 0.75) }, 0.18, Enum.EasingStyle.Back)
end
function Window:_hideTooltip(element)
local tip = self._tip
if not tip or not tip.Shown or (element and self._tipOwner ~= element) then
return
end
self._tipOwner = nil
self._tipHiddenAt = os.clock()
tip.Shown = false
tip.Generation = (tip.Generation or 0) + 1
local generation = tip.Generation
tween(tip.Holder, { GroupTransparency = 1 }, 0.1, Enum.EasingStyle.Quad)
task.delay(0.1, function()
if tip.Generation == generation then
tip.Holder.Visible = false
end
end)
end
function Window:_attachTooltip(element, frame)
local hovering, token, pressAt = false, 0, nil
local function begin(delay, touch)
token += 1
local mine = token
task.delay(delay, function()
if token ~= mine or not hovering or element._destroyed or self._destroyed then
return
end
-- A finger that moved was scrolling, not holding.
if touch and pressAt and (pointerPosition() - pressAt).Magnitude > 10 then
return
end
self:_showTooltip(element, touch)
end)
end
table.insert(element._listeners, (function()
local connections = {
frame.MouseEnter:Connect(function()
if usingTouch() then
return
end
hovering = true
local recent = os.clock() - (self._tipHiddenAt or 0) < 0.35 or self._tipOwner ~= nil
begin(recent and 0 or 0.45, false)
end),
frame.MouseLeave:Connect(function()
hovering = false
token += 1
self:_hideTooltip(element)
end),
frame.InputBegan:Connect(function(input)
if input.UserInputType == Enum.UserInputType.Touch then
hovering = true
pressAt = pointerPosition()
begin(0.5, true)
end
end),
frame.InputEnded:Connect(function(input)
if input.UserInputType == Enum.UserInputType.Touch then
hovering = false
token += 1
self:_hideTooltip(element)
end
end),
}
return function()
for _, connection in ipairs(connections) do
connection:Disconnect()
end
end
end)())
end
-- While dragging, a stand-in follows the pointer while the window stays put,
-- and the window glides into its spot on release. The look is the player's
-- Drag Style; "Off" drags the window itself.
Library.DragStyles = { "Outline", "Brackets", "Ghost", "Frame", "Off" }
local function makeSkeleton(window)
local root = window.Root
local style = window.DragStyle or "Outline"
local skeleton = create("Frame", {
AnchorPoint = root.AnchorPoint,
Position = root.Position,
Size = UDim2.fromOffset(root.AbsoluteSize.X, root.AbsoluteSize.Y),
BackgroundColor3 = style == "Ghost" and Theme.Background or Theme.Accent,
BackgroundTransparency = 1,
BorderSizePixel = 0,
ZIndex = 40,
Parent = window.Gui,
})
corner(skeleton, UDim.new(0, 10))
-- Each part fades from hidden to its shown values, and back on release.
local parts = {}
local function part(object, shown)
local hidden = {}
for key in pairs(shown) do
hidden[key] = 1
end
table.insert(parts, { Object = object, Shown = shown, Hidden = hidden })
end
local function brackets(length, inset)
for _, spot in ipairs({ Vector2.new(0, 0), Vector2.new(1, 0), Vector2.new(0, 1), Vector2.new(1, 1) }) do
local at = UDim2.new(spot.X, spot.X == 0 and inset or -inset, spot.Y, spot.Y == 0 and inset or -inset)
for _, size in ipairs({ UDim2.fromOffset(length, 2), UDim2.fromOffset(2, length) }) do
local bracket = create("Frame", {
AnchorPoint = spot,
Position = at,
Size = size,
BackgroundColor3 = Theme.Accent,
BackgroundTransparency = 1,
BorderSizePixel = 0,
Parent = skeleton,
})
corner(bracket, UDim.new(1, 0))
part(bracket, { BackgroundTransparency = 0 })
end
end
end
if style == "Brackets" then
-- Just the four corners of the window, a little longer.
brackets(22, 0)
elseif style == "Ghost" then
-- A see-through copy of the window's panel.
part(skeleton, { BackgroundTransparency = 0.35 })
part(stroke(skeleton, Theme.Stroke, 1, 1), { Transparency = 0 })
elseif style == "Frame" then
-- A single quiet line, nothing else.
part(stroke(skeleton, Theme.StrokeHover, 1, 1), { Transparency = 0 })
else
-- Outline: a faint accent wash, an outline and corner brackets.
part(skeleton, { BackgroundTransparency = 0.92 })
part(stroke(skeleton, Theme.Accent, 1, 1.5), { Transparency = 0.2 })
brackets(16, 6)
end
local scale = create("UIScale", { Scale = 1.02, Parent = skeleton })
for _, item in ipairs(parts) do
tween(item.Object, item.Shown, 0.15, Enum.EasingStyle.Quad)
end
tween(scale, { Scale = 1 }, 0.25, Enum.EasingStyle.Quint)
local function fade()
for _, item in ipairs(parts) do
tween(item.Object, item.Hidden, 0.2, Enum.EasingStyle.Quad)
end
task.delay(0.22, function()
skeleton:Destroy()
end)
end
return skeleton, fade
end
function Window:_enableDrag()
local dragging, moved = false, false
local grabOffset, pressPoint = Vector2.zero, Vector2.zero
local target = nil
local skeleton, fadeSkeleton = nil, nil
local function centre()
local root = self.Root
return root.AbsolutePosition + root.AbsoluteSize * root.AnchorPoint - self.Gui.AbsolutePosition
end
local function dropSkeleton()
if not skeleton then
return
end
local spot = Vector2.new(skeleton.Position.X.Offset, skeleton.Position.Y.Offset)
if self.KeepOnScreen then
spot = self:_clampedCentre(spot)
end
if self._clampTween then
self._clampTween:Cancel()
end
self._clampTween = tween(self.Root, { Position = UDim2.fromOffset(spot.X, spot.Y) }, 0.3, Enum.EasingStyle.Quint)
fadeSkeleton()
skeleton, fadeSkeleton = nil, nil
end
table.insert(self._connections, UserInputService.InputBegan:Connect(function(input)
if not isPress(input) then
return
end
if not self.Open or not self.Root.Visible then
return
end
local point = pointerPosition()
if self._resizeActive or (self._grip and pointInside(point, self._grip)) then
return
end
if not pointInside(point, self.Body) or self:_overControl(point) then
return
end
if self._clampTween then
self._clampTween:Cancel()
self._clampTween = nil
end
dragging, moved = true, false
pressPoint = point
grabOffset = point - centre()
end))
table.insert(self._connections, UserInputService.InputEnded:Connect(function(input)
if not dragging then
return
end
if isPress(input) then
dragging = false
target = nil
if skeleton then
dropSkeleton()
else
self:_clampToScreen()
end
end
end))
table.insert(self._frameSteps, function(deltaTime)
if not dragging then
return
end
local point = pointerPosition()
if not moved and (point - pressPoint).Magnitude > 3 then
moved = true
if self.DragSkeleton then
skeleton, fadeSkeleton = makeSkeleton(self)
end
end
if not moved then
return
end
-- Hidden or minimized mid-drag: land it where the outline is.
if skeleton and (not self.Open or not self.Root.Visible) then
dragging = false
dropSkeleton()
return
end
target = point - grabOffset
local alpha = 1 - math.exp(-deltaTime * 45)
if skeleton then
local current = Vector2.new(skeleton.Position.X.Offset, skeleton.Position.Y.Offset)
local step = current:Lerp(target, alpha)
skeleton.Position = UDim2.fromOffset(step.X, step.Y)
else
local step = centre():Lerp(target, alpha)
self.Root.Position = UDim2.fromOffset(step.X, step.Y)
end
end)
end
-- One of Library.DragStyles; anything else falls back to "Outline".
-- Returns the style it settled on.
function Window:SetDragStyle(style)
style = table.find(Library.DragStyles, style) and style or "Outline"
self.DragStyle = style
self.DragSkeleton = style ~= "Off"
return style
end
function Window:GetDragStyle()
return self.DragStyle
end
-- How the minimized island shows the session card: opening it while
-- hovered, or keeping it open the whole time it's minimized.
Library.IslandStyles = { "Show on Hover", "Always Show" }
-- One of Library.IslandStyles; anything else falls back to "Show on Hover".
-- Returns the style it settled on.
function Window:SetIslandStyle(style)
style = table.find(Library.IslandStyles, style) and style or "Show on Hover"
self.IslandStyle = style
if self._islandSettle then
self._islandSettle()
end
return style
end
function Window:GetIslandStyle()
return self.IslandStyle
end
-- The older on/off switch: off is the "Off" style, on brings back the
-- default style if it was off.
function Window:SetDragSkeleton(enabled)
if enabled == false then
self:SetDragStyle("Off")
elseif self.DragStyle == "Off" or not self.DragStyle then
self:SetDragStyle("Outline")
end
end
-- The sidebar's divider is a grab strip: dragging it sets the rail's width,
-- kept between a usable minimum and enough room for the content.
function Window:_sidebarResize(sidebar, width)
local divider = sidebar:FindFirstChild("Divider")
local want = width
local function apply(value, remember)
if remember ~= false then
want = value
end
local room = self.Body.AbsoluteSize.X / self.Scale.Scale - 300
value = math.floor(math.clamp(value, 150, math.max(math.min(300, room), 150)))
self.SidebarWidth = value
sidebar.Size = UDim2.new(0, value, 1, 0)
self.Content.Position = UDim2.fromOffset(value + 1, 0)
self.Content.Size = UDim2.new(1, -(value + 1), 1, 0)
end
self.SetSidebarWidth = function(_, value)
apply(tonumber(value) or SIDEBAR_WIDTH)
end
apply(width)
local grip = create("TextButton", {
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, 0, 0, 0),
Size = UDim2.new(0, 6, 1, 0),
BackgroundTransparency = 1,
Text = "",
AutoButtonColor = false,
ZIndex = 6,
Parent = sidebar,
})
grip:SetAttribute("NoDrag", true)
local dragging, startX, startWidth = false, 0, width
local function light(on)
if divider then
tween(divider, { BackgroundColor3 = on and Theme.Accent or Theme.Stroke }, 0.15)
end
end
grip.MouseEnter:Connect(function()
light(true)
end)
grip.MouseLeave:Connect(function()
if not dragging then
light(false)
end
end)
grip.InputBegan:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
dragging, startX, startWidth = true, input.Position.X, self.SidebarWidth
want = startWidth
light(true)
end
end)
self:_listen("Changed", function(input)
if dragging and isMove(input) then
apply(startWidth + (input.Position.X - startX) / self.Scale.Scale)
end
end)
self:_listen("Ended", function(input)
if dragging and (input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch) then
dragging = false
light(false)
end
end)
-- Shrinking the window pulls an over-wide rail back in.
self.Body:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
if not dragging then
apply(want, false)
end
end)
end
-- See-through window: the body and sidebar let the game show through, and the
-- shadow lightens so it doesn't smudge behind them.
function Window:SetTransparent(enabled)
self.Transparent = enabled == true
local on = self.Transparent
self._shadowRest = on and 0.85 or 0.6
tween(self.Body, { BackgroundTransparency = on and 0.35 or 0 }, 0.25)
-- See-through, the rail stays a touch more solid than the body so the
-- tabs keep their contrast; over a background image it lets more through.
local railAlpha = on and 0.5 or (self.Background and 0.45 or 0)
tween(self.SidebarFill, { BackgroundTransparency = railAlpha }, 0.25)
if self.Open and not self.Minimized and self.Root.Visible then
tween(self.Shadow, { ImageTransparency = self._shadowRest }, 0.25)
end
end
local function orderedChildren(parent)
local children = {}
for _, child in ipairs(parent:GetChildren()) do
if child:IsA("GuiObject") then
table.insert(children, child)
end
end
table.sort(children, function(a, b)
return a.LayoutOrder < b.LayoutOrder
end)
return children
end
local function popIn(object, delay, force)
if not object.Visible and not force then
return
end
object.Visible = false
task.delay(delay, function()
if not object.Parent then
return
end
local scaleObject = create("UIScale", { Scale = 0.94, Parent = object })
object.Visible = true
tween(scaleObject, { Scale = 1 }, 0.4, Enum.EasingStyle.Back)
task.delay(0.4, function()
scaleObject:Destroy()
end)
end)
end
function Window:_revealCards(tab, baseDelay)
local container = tab.CurrentSubTab or tab
if container._revealed then
return
end
container._revealed = true
local index = 0
local function reveal(object)
popIn(object, (baseDelay or 0) + index * 0.035)
index += 1
end
local columns = container._columnSet
for _, child in ipairs(orderedChildren(container.List)) do
if columns and child == columns.Holder then
local left, right = orderedChildren(columns.Left), orderedChildren(columns.Right)
for row = 1, math.max(#left, #right) do
if left[row] then
reveal(left[row])
end
if right[row] then
reveal(right[row])
end
end
else
reveal(child)
end
end
end
function Window:_playIntro(morph)
if self._introDone or self._destroyed then
return
end
self._introDone = true
self:_refreshBackdrop(true)
task.delay(0.4, function()
self:_refreshToggleButton()
end)
-- Build the minimized bar ahead of time so the first minimize doesn't
-- pay for it mid-click.
task.delay(1.5, function()
if not self._destroyed and not self.MiniBar then
self:_buildMiniBar()
end
end)
local root, shadow, scale = self.Root, self.Shadow, self.Scale
if self.CurrentTab then
self:_revealCards(self.CurrentTab, morph and 0.15 or 0.25)
end
root.Visible = true
if morph then
scale.Scale = self._fitScale or 1
root.Position = UDim2.fromScale(0.5, 0.5)
self:_fade(0, 0.3)
tween(self.BodyStroke, { Transparency = 0 }, 0.3)
tween(shadow, { ImageTransparency = 0.6 }, 0.3)
else
root.Position = UDim2.new(0.5, 0, 0.5, 24)
tween(scale, { Scale = self._fitScale or 1 }, 0.5, Enum.EasingStyle.Back)
tween(root, { Position = UDim2.fromScale(0.5, 0.5) }, 0.5, Enum.EasingStyle.Quint)
self:_fade(0, 0.35)
tween(self.BodyStroke, { Transparency = 0 }, 0.35)
end
if not morph then
tween(shadow, { ImageTransparency = 0.6 }, 0.5)
end
for index, tab in ipairs(self.Tabs) do
popIn(tab._button, 0.1 + index * 0.05, true)
end
self.Indicator.Visible = false
task.delay(0.15 + #self.Tabs * 0.05, function()
if self.CurrentTab then
self:_placeIndicator(self.CurrentTab)
end
end)
end
function Window:_showLoader(opts)
local duration = opts.LoadingDuration or 1.6
local gui = self.Gui
-- A compact card: logo, title and subtitle on top, a checklist of the
-- loading steps under a hairline, and a thin progress line along the
-- bottom edge. Everything but the prompt cards lives in one canvas group
-- so it can fade as a unit.
local steps = opts.LoadingSteps or { "Preparing interface", "Loading icons", "Almost there" }
local WIDTH, PAD, STEP = 340, 22, 24
local listTop = 80
local height = listTop + #steps * STEP + 18
local loader = create("CanvasGroup", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 0.5, 10),
Size = UDim2.fromOffset(WIDTH, height),
BackgroundColor3 = Theme.Background,
BorderSizePixel = 0,
GroupTransparency = 1,
ZIndex = 10,
Parent = gui,
})
local loaderCorner = corner(loader, UDim.new(0, 12))
local loaderStroke = stroke(loader, Theme.Stroke, 1)
local loaderScale = create("UIScale", { Scale = 0.97, Parent = loader })
local loaderShadow = create("ImageLabel", {
Position = UDim2.fromOffset(-25, -25),
Size = UDim2.new(1, 50, 1, 50),
BackgroundTransparency = 1,
Image = Assets.Shadow,
ImageColor3 = Color3.new(0, 0, 0),
ImageTransparency = 1,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(49, 49, 450, 450),
ZIndex = 0,
Parent = loader,
})
local content = create("CanvasGroup", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
Parent = loader,
})
local logo = create("ImageLabel", {
Position = UDim2.fromOffset(PAD, PAD),
Size = UDim2.fromOffset(30, 30),
BackgroundTransparency = 1,
ImageColor3 = Color3.new(1, 1, 1),
ScaleType = Enum.ScaleType.Fit,
Parent = content,
})
applyIcon(logo, opts.Icon or Assets.Logo, true)
local TEXT_X = PAD + 42
local titleLabel = label({
Position = UDim2.fromOffset(TEXT_X, PAD - 2),
Size = UDim2.new(1, -(TEXT_X + 70), 0, 18),
Text = opts.LoadingTitle or opts.Title or "XYZ",
TextSize = 15,
FontFace = Fonts.Bold,
Parent = content,
})
local statusLabel = label({
Position = UDim2.fromOffset(TEXT_X, PAD + 17),
Size = UDim2.new(1, -(TEXT_X + 70), 0, 15),
Text = opts.LoadingText or opts.Subtitle or "Loading",
TextSize = 12,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
Parent = content,
})
local percentLabel = label({
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, -PAD, 0, PAD + 6),
Size = UDim2.fromOffset(56, 16),
Text = "0%",
TextSize = 13,
FontFace = Fonts.Medium,
TextColor3 = Theme.Muted,
TextXAlignment = Enum.TextXAlignment.Right,
Parent = content,
})
create("Frame", {
Position = UDim2.fromOffset(PAD, listTop - 14),
Size = UDim2.new(1, -PAD * 2, 0, 1),
BackgroundColor3 = Theme.Stroke,
BorderSizePixel = 0,
Parent = content,
})
-- Each step: a marker and its text. Pending is a dim dot, the running
-- step a turning arc, and a finished one a check.
local rows = {}
for index, text in ipairs(steps) do
local row = create("Frame", {
Position = UDim2.fromOffset(PAD, listTop + (index - 1) * STEP),
Size = UDim2.new(1, -PAD * 2, 0, STEP),
BackgroundTransparency = 1,
Parent = content,
})
local dot = create("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0, 7, 0.5, 0),
Size = UDim2.fromOffset(6, 6),
BackgroundColor3 = Theme.Surface3,
BorderSizePixel = 0,
Parent = row,
})
corner(dot, UDim.new(1, 0))
local spinner = create("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0, 7, 0.5, 0),
Size = UDim2.fromOffset(12, 12),
BackgroundTransparency = 1,
Visible = false,
Parent = row,
})
corner(spinner, UDim.new(1, 0))
local spinnerStroke = create("UIStroke", {
Color = Theme.Accent,
Thickness = 2,
Parent = spinner,
})
create("UIGradient", {
Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0),
NumberSequenceKeypoint.new(0.5, 0),
NumberSequenceKeypoint.new(0.501, 1),
NumberSequenceKeypoint.new(1, 1),
}),
Parent = spinnerStroke,
})
local check = create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0, 7, 0.5, 0),
Size = UDim2.fromOffset(14, 14),
BackgroundTransparency = 1,
ImageColor3 = Theme.Accent,
ImageTransparency = 1,
ScaleType = Enum.ScaleType.Fit,
Parent = row,
})
applyIcon(check, "check")
local stepLabel = label({
Position = UDim2.fromOffset(24, 0),
Size = UDim2.new(1, -24, 1, 0),
Text = tostring(text),
TextSize = 13,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextTransparency = 1,
Parent = row,
})
rows[index] = { Dot = dot, Spinner = spinner, Check = check, Label = stepLabel, State = "pending" }
-- The rows fade in one after another once the card is up.
task.delay(0.15 + index * 0.06, function()
if loader.Parent and rows[index].State == "pending" then
tween(stepLabel, { TextTransparency = 0.45 }, 0.3)
end
end)
end
local function setStep(index, state)
local row = rows[index]
if not row or row.State == state then
return
end
row.State = state
row.Dot.Visible = state == "pending"
row.Spinner.Visible = state == "active"
tween(row.Check, { ImageTransparency = state == "done" and 0 or 1 }, 0.2)
tween(row.Label, {
TextTransparency = state == "pending" and 0.45 or 0,
TextColor3 = state == "active" and Theme.Text or Theme.Muted,
}, 0.2)
end
local track = create("Frame", {
AnchorPoint = Vector2.new(0, 1),
Position = UDim2.fromScale(0, 1),
Size = UDim2.new(1, 0, 0, 2),
BackgroundColor3 = Theme.Surface3,
BorderSizePixel = 0,
Parent = content,
})
local fill = create("Frame", {
Size = UDim2.fromScale(0, 1),
BackgroundColor3 = Theme.Accent,
BorderSizePixel = 0,
Parent = track,
})
tween(loader, { GroupTransparency = 0, Position = UDim2.fromScale(0.5, 0.5) }, 0.45, Enum.EasingStyle.Quint)
tween(loaderScale, { Scale = 1 }, 0.45, Enum.EasingStyle.Quint)
tween(loaderShadow, { ImageTransparency = 0.55 }, 0.4)
tween(fill, { Size = UDim2.fromScale(0.85, 1) }, duration * 0.8, Enum.EasingStyle.Quart)
-- The percentage follows the bar and the running step's arc turns.
local percentStep
percentStep = RunService.RenderStepped:Connect(function(deltaTime)
if not loader.Parent then
percentStep:Disconnect()
return
end
percentLabel.Text = math.floor(fill.Size.X.Scale * 100 + 0.5) .. "%"
for _, row in ipairs(rows) do
if row.State == "active" then
row.Spinner.Rotation = (row.Spinner.Rotation + deltaTime * 360) % 360
end
end
end)
task.spawn(loadLucide)
for index in ipairs(steps) do
task.delay(duration * (index - 1) / #steps, function()
if not loader.Parent then
return
end
if index > 1 then
setStep(index - 1, "done")
end
setStep(index, "active")
end)
end
local function completeSteps()
for index in ipairs(rows) do
setStep(index, "done")
end
end
local function finish()
completeSteps()
tween(content, { GroupTransparency = 1 }, 0.18)
local fit = self._fitScale or 1
local target = self.Root.Size
tween(loader, {
Size = UDim2.fromOffset(target.X.Offset * fit, target.Y.Offset * fit),
Position = UDim2.fromScale(0.5, 0.5),
}, 0.5, Enum.EasingStyle.Quint)
tween(loaderCorner, { CornerRadius = UDim.new(0, Library._radius(10)) }, 0.5, Enum.EasingStyle.Quint)
tween(loaderScale, { Scale = 1 }, 0.5, Enum.EasingStyle.Quint)
task.delay(0.28, function()
self:_playIntro(true)
tween(loader, { GroupTransparency = 1 }, 0.25)
tween(loaderStroke, { Transparency = 1 }, 0.2)
tween(loaderShadow, { ImageTransparency = 1 }, 0.2)
end)
task.delay(0.6, function()
loader:Destroy()
end)
end
-- Prompt cards (unsupported executor, custom disclaimers): the checklist
-- gives way to one card at a time, each with Exit and Continue, before
-- the window opens.
local function hideBar()
completeSteps()
tween(content, { GroupTransparency = 1 }, 0.18)
end
local function showCard(info, onContinue)
local WIDTH, PAD, MAX_BODY = 360, 24, 220
local tone = info.Color or Theme.Accent
-- Fixed width from the start, so the body wraps at its final width
-- while the loader is still resizing.
local warnCard = create("CanvasGroup", {
AnchorPoint = Vector2.new(0.5, 0),
Position = UDim2.fromScale(0.5, 0),
Size = UDim2.new(0, WIDTH, 1, 0),
BackgroundTransparency = 1,
GroupTransparency = 1,
ZIndex = 5,
Parent = loader,
})
local badge = create("Frame", {
Position = UDim2.fromOffset(PAD, PAD),
Size = UDim2.fromOffset(44, 44),
BackgroundColor3 = tone,
BackgroundTransparency = 0.86,
BorderSizePixel = 0,
Parent = warnCard,
})
corner(badge, UDim.new(0, 12))
stroke(badge, tone, 1).Transparency = 0.6
local badgeIcon = create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromOffset(22, 22),
BackgroundTransparency = 1,
ImageColor3 = tone,
ScaleType = Enum.ScaleType.Fit,
Parent = badge,
})
applyIcon(badgeIcon, info.Icon or "info")
label({
Position = UDim2.fromOffset(PAD + 58, info.Subtitle and PAD + 3 or PAD + 12),
Size = UDim2.new(1, -(PAD * 2 + 58), 0, 20),
Text = info.Title,
TextSize = 17,
Parent = warnCard,
})
if info.Subtitle then
label({
Position = UDim2.fromOffset(PAD + 58, PAD + 25),
Size = UDim2.new(1, -(PAD * 2 + 58), 0, 16),
Text = tostring(info.Subtitle),
TextSize = 13,
TextColor3 = tone,
Parent = warnCard,
})
end
-- Long text scrolls past MAX_BODY instead of pushing the card off screen.
local scroller = create("ScrollingFrame", {
Position = UDim2.fromOffset(PAD, PAD + 60),
Size = UDim2.new(1, -PAD * 2, 0, 16),
BackgroundTransparency = 1,
BorderSizePixel = 0,
ScrollBarThickness = TOUCH and 4 or 3,
ScrollBarImageColor3 = Theme.Accent,
ScrollBarImageTransparency = 0.4,
ScrollingDirection = Enum.ScrollingDirection.Y,
CanvasSize = UDim2.new(),
Parent = warnCard,
})
local body = label({
Size = UDim2.new(1, -8, 0, 0),
Text = tostring(info.Text),
TextSize = 13,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextWrapped = true,
TextTruncate = Enum.TextTruncate.None,
TextYAlignment = Enum.TextYAlignment.Top,
Parent = scroller,
})
local row = create("Frame", {
AnchorPoint = Vector2.new(0, 1),
Position = UDim2.new(0, PAD, 1, -PAD + 4),
Size = UDim2.new(1, -PAD * 2, 0, 34),
BackgroundTransparency = 1,
Parent = warnCard,
})
create("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 8),
Parent = row,
})
-- Don't ask again: only offered when Continue is, and the executor
-- can write the file that remembers it.
local remember = false
local rememberRow = nil
if not info.Block and info.RememberText and UnsupportedSkip.Available() then
local boxSize = TOUCH and 20 or 16
rememberRow = create("TextButton", {
AnchorPoint = Vector2.new(0, 1),
Position = UDim2.new(0, PAD, 1, -PAD + 4 - 34 - 12),
Size = UDim2.new(1, -PAD * 2, 0, TOUCH and 28 or 22),
BackgroundTransparency = 1,
Text = "",
AutoButtonColor = false,
Parent = warnCard,
})
local box = create("Frame", {
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.fromScale(0, 0.5),
Size = UDim2.fromOffset(boxSize, boxSize),
BackgroundColor3 = Theme.Surface2,
BorderSizePixel = 0,
Parent = rememberRow,
})
corner(box, UDim.new(0, 4))
local boxStroke = stroke(box, Theme.Stroke, 1)
local check = create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromOffset(boxSize - 4, boxSize - 4),
BackgroundTransparency = 1,
ImageColor3 = Theme.AccentDark,
ImageTransparency = 1,
ScaleType = Enum.ScaleType.Fit,
Parent = box,
})
applyIcon(check, "check")
local rememberLabel = label({
Position = UDim2.fromOffset(boxSize + 8, 0),
Size = UDim2.new(1, -(boxSize + 8), 1, 0),
Text = info.RememberText,
TextSize = 12,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
Parent = rememberRow,
})
rememberRow.MouseEnter:Connect(function()
tween(boxStroke, { Color = remember and Theme.Accent or Theme.StrokeHover }, 0.12)
tween(rememberLabel, { TextColor3 = Theme.Text }, 0.12)
end)
rememberRow.MouseLeave:Connect(function()
tween(boxStroke, { Color = remember and Theme.Accent or Theme.Stroke }, 0.2)
tween(rememberLabel, { TextColor3 = remember and Theme.Text or Theme.Muted }, 0.2)
end)
rememberRow.MouseButton1Click:Connect(function()
remember = not remember
tween(box, { BackgroundColor3 = remember and Theme.Accent or Theme.Surface2 }, 0.15)
tween(boxStroke, { Color = remember and Theme.Accent or Theme.Stroke }, 0.15)
tween(check, { ImageTransparency = remember and 0 or 1 }, 0.15)
tween(rememberLabel, { TextColor3 = remember and Theme.Text or Theme.Muted }, 0.15)
end)
end
local chosen = false
local function button(text, primary, order, callback)
local frame = create("TextButton", {
Size = UDim2.fromOffset(0, 34),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = primary and Theme.Accent or Theme.Surface2,
BackgroundTransparency = primary and 0.12 or 0,
BorderSizePixel = 0,
Text = "",
AutoButtonColor = false,
LayoutOrder = order,
Parent = row,
})
corner(frame, UDim.new(0, 7))
local frameStroke = stroke(frame, primary and Theme.Accent or Theme.Stroke, 1)
frameStroke.Transparency = primary and 0.4 or 0
padding(frame, 14, 14)
label({
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
Text = text,
TextSize = 13,
TextColor3 = primary and Theme.AccentDark or Theme.Text,
TextXAlignment = Enum.TextXAlignment.Center,
Parent = frame,
})
frame.MouseEnter:Connect(function()
if primary then
tween(frame, { BackgroundTransparency = 0 }, 0.12)
tween(frameStroke, { Transparency = 0 }, 0.12)
else
tween(frameStroke, { Color = Theme.StrokeHover }, 0.12)
end
end)
frame.MouseLeave:Connect(function()
if primary then
tween(frame, { BackgroundTransparency = 0.12 }, 0.2)
tween(frameStroke, { Transparency = 0.4 }, 0.2)
else
tween(frameStroke, { Color = Theme.Stroke }, 0.2)
end
end)
frame.MouseButton1Click:Connect(function()
if chosen then
return
end
chosen = true
callback()
end)
end
if info.ExitText ~= false then
button(info.ExitText or "Exit", info.Block, 1, function()
if info.Callback then
task.spawn(info.Callback, false)
end
tween(loader, { GroupTransparency = 1, Position = UDim2.new(0.5, 0, 0.5, 12) }, 0.25, Enum.EasingStyle.Quint)
tween(loaderShadow, { ImageTransparency = 1 }, 0.2)
task.delay(0.25, function()
self:Destroy()
end)
end)
end
if not info.Block then
button(info.ContinueText or "Continue", true, 2, function()
if remember and info.Remember then
info.Remember()
end
if info.Callback then
task.spawn(info.Callback, true)
end
tween(warnCard, { GroupTransparency = 1 }, 0.15)
task.delay(0.15, function()
warnCard:Destroy()
onContinue()
end)
end)
end
-- The card grows to fit the message, then it fades in. The wrapped
-- height can land a few frames late (font loading, the scroller's
-- layout), so the card refits whenever it changes.
local laidOut = false
-- TextBounds of wrapped text stops at the label's current height, so
-- the card would fit to its own clipped text; the full wrapped height
-- is measured at the body's width instead.
local function measure()
local ok, size = pcall(function()
local params = Instance.new("GetTextBoundsParams")
params.Text = body.Text
params.Font = body.FontFace
params.Size = body.TextSize
params.Width = WIDTH - PAD * 2 - 8
return game:GetService("TextService"):GetTextBoundsAsync(params)
end)
return ok and size and math.ceil(size.Y) + 2 or body.TextBounds.Y
end
local function fit(duration)
local textHeight = math.max(measure(), 16)
local bodyHeight = math.min(textHeight, MAX_BODY)
body.Size = UDim2.new(1, -8, 0, textHeight)
scroller.Size = UDim2.new(1, -PAD * 2, 0, bodyHeight)
scroller.CanvasSize = UDim2.fromOffset(0, textHeight)
local height = PAD + 60 + bodyHeight + 18 + 34 + PAD - 4
if rememberRow then
height += rememberRow.Size.Y.Offset + 10
end
tween(loader, { Size = UDim2.fromOffset(WIDTH, height) }, duration, Enum.EasingStyle.Quint)
end
body:GetPropertyChangedSignal("TextBounds"):Connect(function()
if laidOut and loader.Parent and warnCard.Parent then
fit(0.2)
end
end)
task.delay(0.15, function()
if not loader.Parent then
return
end
laidOut = true
fit(0.35)
task.delay(0.12, function()
tween(warnCard, { GroupTransparency = 0 }, 0.3)
badge.Size = UDim2.fromOffset(34, 34)
badge.Position = UDim2.fromOffset(PAD + 5, PAD + 5)
tween(badge, { Size = UDim2.fromOffset(44, 44), Position = UDim2.fromOffset(PAD, PAD) }, 0.45, Enum.EasingStyle.Back)
end)
end)
end
local function runPrompts(queue, index)
if not loader.Parent then
return
end
local info = queue[index]
if not info then
finish()
return
end
showCard(info, function()
runPrompts(queue, index + 1)
end)
end
task.delay(duration, function()
tween(fill, { Size = UDim2.fromScale(1, 1) }, 0.25, Enum.EasingStyle.Quint)
task.delay(0.25, function()
if not loader.Parent then
return
end
if opts._prompts then
hideBar()
runPrompts(opts._prompts, 1)
else
finish()
end
end)
end)
end
function Window:_fitToScreen(instant)
local viewport = self.Gui.AbsoluteSize
if viewport.X == 0 or viewport.Y == 0 then
return
end
local size = self.Root.Size
-- The player's UI scale, times the adaptive factor for this screen, shrunk
-- further when the window wouldn't fit.
local wanted = (self._uiScale or 1) * (self._adaptive ~= false and Library._adaptiveFactor(viewport) or 1)
local scale = math.min(wanted, (viewport.X - 24) / math.max(size.X.Offset, 1), (viewport.Y - 24) / math.max(size.Y.Offset, 1))
self._fitScale = math.max(scale, 0.45)
if self._introDone and self.Open then
if instant then
self.Scale.Scale = self._fitScale
else
tween(self.Scale, { Scale = self._fitScale }, 0.2)
end
end
end
-- Adaptive size: the window is drawn for a 1080p screen and grows on larger
-- ones (up to 1.75x on 4K) or shrinks a little on small laptops, so it covers
-- about the same share of any screen. 1080p-class screens, windowed or with
-- the topbar inset, stay at exactly 1. Touch screens only use the fit.
function Library._adaptiveFactor(viewport)
if TOUCH then
return 1
end
local factor = math.min(viewport.X / 1920, viewport.Y / 1020)
if factor > 0.9 and factor < 1.15 then
return 1
end
return math.clamp(math.floor(factor * 20 + 0.5) / 20, 0.85, 1.75)
end
function Window:SetAdaptiveSize(enabled)
self._adaptive = enabled ~= false
self:_fitToScreen()
task.delay(0.22, function()
if not self._destroyed then
self:_clampToScreen()
end
end)
end
function Window:GetAdaptiveSize()
return self._adaptive ~= false
end
-- UI scale: 0.6 to 1.5 on top of the automatic fit to small screens.
function Window:SetUIScale(value)
value = math.clamp(tonumber(value) or 1, 0.6, 1.5)
self.UIScale = value
self._uiScale = value
self:_fitToScreen()
task.delay(0.22, function()
if not self._destroyed then
self:_clampToScreen()
end
end)
end
function Window:GetUIScale()
return self._uiScale or 1
end
-- Density: the spacing between cards, groupboxes and rows. Shared by every
-- window, like the theme. Anything built with Library._space follows it.
Library.DensityModes = { Compact = 0.55, Default = 1, Comfortable = 1.45 }
Library.Density = "Default"
-- Small gaps (a groupbox's 2px rows) still move visibly between modes.
function Library._spaced(base, factor)
if base <= 0 then
return 0
end
return math.max(math.floor(base * factor + (factor - 1) * 4 + 0.5), 0)
end
function Library._space(object, base)
local factor = Library.DensityModes[Library.Density] or 1
if object:IsA("UIListLayout") or object:IsA("UIGridLayout") then
object:SetAttribute("Space", base)
object.Padding = UDim.new(0, Library._spaced(base, factor))
elseif object:IsA("UIPadding") then
-- base is { left, right, top, bottom }
object:SetAttribute("SpaceL", base[1])
object:SetAttribute("SpaceR", base[2])
object:SetAttribute("SpaceT", base[3])
object:SetAttribute("SpaceB", base[4])
object.PaddingLeft = UDim.new(0, math.floor(base[1] * math.sqrt(factor) + 0.5))
object.PaddingRight = UDim.new(0, math.floor(base[2] * math.sqrt(factor) + 0.5))
object.PaddingTop = UDim.new(0, Library._spaced(base[3], factor))
object.PaddingBottom = UDim.new(0, Library._spaced(base[4], factor))
end
return object
end
function Library:SetDensity(mode)
if type(mode) == "string" then
for name in pairs(Library.DensityModes) do
if name:lower() == mode:lower() then
mode = name
break
end
end
end
if not Library.DensityModes[mode] then
mode = "Default"
end
Library.Density = mode
for _, window in ipairs(Library.Windows) do
if window.Gui and window.Gui.Parent then
for _, object in ipairs(window.Gui:GetDescendants()) do
local base = object:GetAttribute("Space")
if base then
Library._space(object, base)
elseif object:GetAttribute("SpaceL") then
Library._space(object, {
object:GetAttribute("SpaceL"), object:GetAttribute("SpaceR"),
object:GetAttribute("SpaceT"), object:GetAttribute("SpaceB"),
})
end
end
end
end
return mode
end
function Window:SetDensity(mode)
return Library:SetDensity(mode)
end
function Window:GetDensity()
return Library.Density
end
-- Nearest centre that keeps the whole window inside the viewport.
function Window:_clampedCentre(centre)
local viewport = self.Gui.AbsoluteSize
local half = self.Root.AbsoluteSize / 2
return Vector2.new(
math.clamp(centre.X, math.min(half.X, viewport.X / 2), math.max(viewport.X - half.X, viewport.X / 2)),
math.clamp(centre.Y, math.min(half.Y, viewport.Y / 2), math.max(viewport.Y - half.Y, viewport.Y / 2))
)
end
function Window:_clampToScreen()
if not self.KeepOnScreen or self._resizeActive then
return
end
local root = self.Root
local centre = root.AbsolutePosition + root.AbsoluteSize / 2 - self.Gui.AbsolutePosition
local clamped = self:_clampedCentre(centre)
if (clamped - centre).Magnitude > 0.5 then
self._clampTween = tween(root, { Position = UDim2.fromOffset(clamped.X, clamped.Y) }, 0.25, Enum.EasingStyle.Quint)
end
end
function Window:_createOpenButton(opts)
local gui = self.Gui
local pill = create("TextButton", {
AnchorPoint = Vector2.new(0.5, 0),
Position = UDim2.new(0.5, 0, 0, 14),
Size = UDim2.fromOffset(0, 40),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = Theme.Background,
BorderSizePixel = 0,
Text = "",
AutoButtonColor = false,
ZIndex = 30,
Parent = gui,
})
corner(pill, UDim.new(1, 0))
stroke(pill, Theme.Stroke)
padding(pill, 12, 16)
local icon = create("ImageLabel", {
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, 0, 0.5, 0),
Size = UDim2.fromOffset(20, 20),
BackgroundTransparency = 1,
ImageColor3 = Color3.new(1, 1, 1),
ScaleType = Enum.ScaleType.Fit,
ZIndex = 31,
Parent = pill,
})
applyIcon(icon, opts.Icon or Assets.Logo, true)
label({
Position = UDim2.fromOffset(28, 0),
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
Text = opts.Title or "XYZ",
TextSize = 13,
TextTruncate = Enum.TextTruncate.None,
ZIndex = 31,
Parent = pill,
})
self.OpenButton = pill
local dragging, moved = false, false
local grab = Vector2.zero
pill.InputBegan:Connect(function(input)
if isPress(input) then
dragging = true
moved = false
grab = pointerPosition() - pill.AbsolutePosition
end
end)
table.insert(self._connections, UserInputService.InputChanged:Connect(function(input)
if not dragging then
return
end
if isMove(input) then
local point = pointerPosition() - grab - gui.AbsolutePosition
if (point - (pill.AbsolutePosition - gui.AbsolutePosition)).Magnitude > 3 then
moved = true
end
pill.AnchorPoint = Vector2.new(0, 0)
pill.Position = UDim2.fromOffset(
math.clamp(point.X, 0, math.max(gui.AbsoluteSize.X - pill.AbsoluteSize.X, 0)),
math.clamp(point.Y, 0, math.max(gui.AbsoluteSize.Y - pill.AbsoluteSize.Y, 0))
)
end
end))
table.insert(self._connections, UserInputService.InputEnded:Connect(function(input)
if dragging and isPress(input) then
dragging = false
if not moved then
self:Toggle()
end
end
end))
end
-- Square floating button that minimizes and restores the window. Shown on
-- touch devices only, or everywhere with Platform = "Both".
function Window:_toggleButtonAllowed()
return self._toggleEnabled and (TOUCH or self._togglePlatform == "Both")
end
function Window:_refreshToggleButton()
local button = self.ToggleButton
if not button then
return
end
local shown = self._introDone and not self._destroyed and self:_toggleButtonAllowed()
if shown and not button.Visible then
button.Visible = true
button.Size = UDim2.fromOffset(0, 0)
tween(button, { Size = UDim2.fromOffset(self._toggleSize, self._toggleSize) }, 0.45, Enum.EasingStyle.Quint)
elseif not shown then
button.Visible = false
end
-- Folded (window minimized or hidden): the outline takes the accent.
local folded = self.Minimized or not self.Open
tween(self._toggleStroke, { Color = folded and Theme.Accent or Theme.Stroke, Transparency = folded and 0.3 or 0 }, 0.35)
local icon = self._toggleIcon
tween(icon, { Rotation = folded and 180 or 0 }, 0.5, Enum.EasingStyle.Quint)
if not icon:GetAttribute("CustomIcon") then
tween(icon, { ImageColor3 = folded and Theme.Text or Theme.Accent }, 0.35)
end
end
function Window:_createToggleButton(opts)
local gui = self.Gui
local size = tonumber(opts.Size) or (TOUCH and 56 or 50)
local iconSize = math.floor(size * 0.46)
self._toggleEnabled = opts.Enabled ~= false
self._togglePlatform = opts.Platform == "Both" and "Both" or "Mobile"
self._toggleSize = size
local button = create("TextButton", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = opts.Position or UDim2.new(0, 18 + size / 2, 0.5, 0),
Size = UDim2.fromOffset(size, size),
BackgroundColor3 = Theme.Background,
BorderSizePixel = 0,
Text = "",
AutoButtonColor = false,
ClipsDescendants = true,
Visible = false,
ZIndex = 30,
Parent = gui,
})
corner(button, UDim.new(0, 12))
local buttonStroke = stroke(button, Theme.Stroke)
local icon = create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromOffset(iconSize, iconSize),
BackgroundTransparency = 1,
ImageColor3 = Theme.Accent,
ScaleType = Enum.ScaleType.Fit,
ZIndex = 31,
Parent = button,
})
local buttonScale = create("UIScale", { Parent = button })
self.ToggleButton = button
self._toggleStroke = buttonStroke
self._toggleIcon = icon
self:SetToggleButtonIcon(opts.Icon or "layout-grid")
local dragging, moved = false, false
local grab = Vector2.zero
button.InputBegan:Connect(function(input)
if isPress(input) then
dragging = true
moved = false
grab = pointerPosition() - button.AbsolutePosition
tween(buttonScale, { Scale = 0.94 }, 0.18, Enum.EasingStyle.Quint)
end
end)
table.insert(self._connections, UserInputService.InputChanged:Connect(function(input)
if not dragging or not isMove(input) then
return
end
local point = pointerPosition() - grab - gui.AbsolutePosition
if (point - (button.AbsolutePosition - gui.AbsolutePosition)).Magnitude > 3 then
moved = true
end
if moved then
button.AnchorPoint = Vector2.zero
button.Position = UDim2.fromOffset(
math.clamp(point.X, 0, math.max(gui.AbsoluteSize.X - button.AbsoluteSize.X, 0)),
math.clamp(point.Y, 0, math.max(gui.AbsoluteSize.Y - button.AbsoluteSize.Y, 0))
)
end
end))
table.insert(self._connections, UserInputService.InputEnded:Connect(function(input)
if not dragging or not isPress(input) then
return
end
dragging = false
tween(buttonScale, { Scale = 1 }, 0.4, Enum.EasingStyle.Quint)
if moved then
return
end
ripple(button)
if not self.Open then
self:Toggle(true)
elseif self.Minimized then
self:Restore()
else
self:Minimize()
end
end))
end
function Window:SetToggleButton(enabled)
self._toggleEnabled = enabled ~= false
self:_refreshToggleButton()
end
function Window:SetToggleButtonPlatform(platform)
self._togglePlatform = platform == "Both" and "Both" or "Mobile"
self:_refreshToggleButton()
end
function Window:SetToggleButtonIcon(icon)
local image = self._toggleIcon
if not image then
return
end
image.Image = ""
applyIcon(image, icon)
if image.Image == "" then
applyIcon(image, Assets.Logo)
end
if not image:GetAttribute("CustomIcon") then
tween(image, { ImageColor3 = (self.Minimized or not self.Open) and Theme.Text or Theme.Accent }, 0)
end
end
function Window:_enableResize(minSize)
local maxSize = self.MaxSize or Vector2.new(math.huge, math.huge)
self._minSize = minSize
local root = self.Root
local grip = create("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(1, 4, 1, 4),
Size = UDim2.fromOffset(32, 32),
BackgroundTransparency = 1,
Active = true,
ZIndex = 20,
Parent = root,
})
self._grip = grip
-- An arc that traces the window's own rounded corner: a rounded square
-- laid over the corner whose outline fades out away from it, so only the
-- curve and a short run of each edge show. Faint at rest, accent in use.
local gripArc = create("Frame", {
AnchorPoint = Vector2.new(1, 1),
Position = UDim2.fromOffset(12, 12),
Size = UDim2.fromOffset(26, 26),
BackgroundTransparency = 1,
ZIndex = 20,
Parent = grip,
})
corner(gripArc, UDim.new(0, 10))
local gripStroke = create("UIStroke", {
Color = Theme.Muted,
Thickness = 2,
Transparency = 0.55,
Parent = gripArc,
})
create("UIGradient", {
Rotation = 45,
Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 1),
NumberSequenceKeypoint.new(0.55, 1),
NumberSequenceKeypoint.new(0.85, 0),
NumberSequenceKeypoint.new(1, 0),
}),
Parent = gripStroke,
})
local function lightGrip(on)
tween(gripStroke, {
Color = on and Theme.Accent or Theme.Muted,
Transparency = on and 0 or 0.55,
}, on and 0.15 or 0.3, Enum.EasingStyle.Quint)
end
-- The size follows the pointer through a critically damped spring, so it
-- glides after the cursor and eases to rest when released instead of
-- snapping. While resizing, the window is anchored at its top-left corner
-- so only the right and bottom edges move and nothing jitters on half pixels.
local STIFFNESS = 20
local resizing = false
local startSize = Vector2.zero
local startPointer = Vector2.zero
local current = Vector2.zero
local velocity = Vector2.zero
local target = Vector2.zero
local applied = Vector2.zero
local function readSize()
return Vector2.new(root.Size.X.Offset, root.Size.Y.Offset)
end
local function pin()
if root.AnchorPoint == Vector2.zero then
return
end
-- A clamp tween still writing centre coordinates would fight the new anchor.
if self._clampTween then
self._clampTween:Cancel()
self._clampTween = nil
end
local topLeft = root.AbsolutePosition - self.Gui.AbsolutePosition
root.AnchorPoint = Vector2.zero
root.Position = UDim2.fromOffset(topLeft.X, topLeft.Y)
end
local function unpin()
local topLeft = root.AbsolutePosition - self.Gui.AbsolutePosition
local half = root.AbsoluteSize / 2
root.AnchorPoint = Vector2.new(0.5, 0.5)
root.Position = UDim2.fromOffset(topLeft.X + half.X, topLeft.Y + half.Y)
end
local function clampSize(size)
return Vector2.new(
math.floor(math.clamp(size.X, minSize.X, maxSize.X) + 0.5),
math.floor(math.clamp(size.Y, minSize.Y, maxSize.Y) + 0.5)
)
end
grip.InputBegan:Connect(function(input)
if not isPress(input) or not self.Open then
return
end
if not self._resizeActive then
pin()
current = readSize()
velocity = Vector2.zero
applied = current
end
self._resizeActive = true
resizing = true
startSize = current
startPointer = pointerPosition()
target = clampSize(startSize)
lightGrip(true)
end)
grip.MouseEnter:Connect(function()
if not resizing then
lightGrip(true)
end
end)
grip.MouseLeave:Connect(function()
if not resizing then
lightGrip(false)
end
end)
table.insert(self._connections, UserInputService.InputEnded:Connect(function(input)
if resizing and isPress(input) then
resizing = false
lightGrip(false)
end
end))
table.insert(self._frameSteps, function(deltaTime)
if not self._resizeActive then
return
end
if resizing then
local delta = (pointerPosition() - startPointer) / self.Scale.Scale
target = clampSize(startSize + delta)
end
local dt = math.min(deltaTime, 1 / 30)
local decay = math.exp(-STIFFNESS * dt)
local offset = current - target
local drift = (velocity + offset * STIFFNESS) * dt
velocity = (velocity - drift * STIFFNESS) * decay
current = target + (offset + drift) * decay
local rounded = Vector2.new(math.floor(current.X + 0.5), math.floor(current.Y + 0.5))
if rounded ~= applied then
applied = rounded
root.Size = UDim2.fromOffset(rounded.X, rounded.Y)
end
if not resizing and (current - target).Magnitude < 0.3 and velocity.Magnitude < 3 then
current = target
velocity = Vector2.zero
applied = target
root.Size = UDim2.fromOffset(target.X, target.Y)
unpin()
self._resizeActive = false
self:_fitToScreen()
self:_clampToScreen()
end
end)
end
-- The window's own size in pixels, before the UI scale and screen fit.
function Window:GetSize()
return Vector2.new(self.Root.Size.X.Offset, self.Root.Size.Y.Offset)
end
-- Resizes to an exact size, kept within MinSize and MaxSize. Ignored while the
-- player is dragging the grip.
function Window:SetSize(size, instant)
if typeof(size) == "UDim2" then
size = Vector2.new(size.X.Offset, size.Y.Offset)
end
if typeof(size) ~= "Vector2" or self._resizeActive then
return
end
local minSize = self._minSize or Vector2.zero
local maxSize = self.MaxSize or Vector2.new(math.huge, math.huge)
size = Vector2.new(
math.floor(math.clamp(size.X, minSize.X, maxSize.X) + 0.5),
math.floor(math.clamp(size.Y, minSize.Y, maxSize.Y) + 0.5)
)
if size == self:GetSize() then
return
end
local duration = (instant or not self.Root.Visible) and 0 or 0.3
tween(self.Root, { Size = UDim2.fromOffset(size.X, size.Y) }, duration, Enum.EasingStyle.Quint)
task.delay(duration + 0.02, function()
if not self._destroyed then
self:_fitToScreen()
self:_clampToScreen()
end
end)
end
-- Configs are plain JSON, one file per config, in the same shape as a share code:
-- {"Folder":"MyHub","Name":"farm","Version":1,"Config":"{\"AutoFarm\":{\"Value\":true,\"Type\":\"Toggle\"}}"}
-- Config holds the flags as their own JSON string, each one {"Value":...,"Type":...}.
-- Colours are {"Hex":"ff5a5a"}, keybinds are key names, an empty dropdown has no Value.
local CONFIG_VERSION = 1
local LEGACY_SHARE_PREFIX = "airflow:"
local function configPath(self, name)
return self.ConfigFolder .. "/" .. name .. ".json"
end
local function fileApiAvailable()
return type(writefile) == "function" and type(readfile) == "function" and type(isfile) == "function"
end
-- Makes every missing folder along the path, so FolderName can be "Hub/Game".
local function ensureFolder(folder)
if type(isfolder) ~= "function" or type(makefolder) ~= "function" then
return
end
local path = ""
for segment in tostring(folder):gmatch("[^/\\]+") do
path = path == "" and segment or path .. "/" .. segment
if not isfolder(path) then
makefolder(path)
end
end
end
local function colorToHex(color)
return string.format("%02x%02x%02x",
math.floor(color.R * 255 + 0.5),
math.floor(color.G * 255 + 0.5),
math.floor(color.B * 255 + 0.5))
end
local function colorFromValue(value)
if type(value) == "table" then
if type(value.Hex) == "string" then
value = value.Hex
elseif type(value[1]) == "number" then
-- Older configs stored { r, g, b } in 0-1.
return Color3.new(value[1], value[2] or 0, value[3] or 0)
end
end
if type(value) == "string" then
local r, g, b = value:match("^%s*#?(%x%x)(%x%x)(%x%x)%s*$")
if r then
return Color3.fromRGB(tonumber(r, 16), tonumber(g, 16), tonumber(b, 16))
end
end
return nil
end
local function serializeFlag(element)
local kind = element._type
local value = element:Get()
if type(element._saveValue) == "function" then
value = element:_saveValue()
end
if kind == "Keybind" then
return { Type = kind, Value = value and value.Name or nil }
elseif kind == "ColorPicker" then
return { Type = kind, Value = typeof(value) == "Color3" and { Hex = colorToHex(value) } or nil }
end
return { Type = kind, Value = value }
end
local function deserializeFlag(element, entry, silent)
local kind = element._type
local value = entry.Value
if kind == "Keybind" then
element:Set(type(value) == "string" and Enum.KeyCode[value] or nil, silent)
elseif kind == "ColorPicker" then
local color = colorFromValue(value)
if color then
element:Set(color, silent)
end
elseif kind == "Dropdown" then
-- A dropdown saved with nothing picked has no Value at all.
element:Set(value, silent)
elseif kind == "Toggle" then
if type(value) == "boolean" then
element:Set(value, silent)
end
elseif kind == "Slider" or kind == "Stepper" or kind == "Progress" then
local number = tonumber(value)
if number then
element:Set(number, silent)
end
elseif value ~= nil then
element:Set(value, silent)
end
end
local function entryMatches(element, entry)
return type(entry) == "table" and (entry.Type == nil or entry.Type == element._type)
end
-- Flags a config holds but no element has claimed yet are kept here. They are
-- applied when an element with that flag is created and written back on save,
-- so a config survives elements that only exist some of the time.
local function collectFlags(self)
local data = {}
if self and self._pendingFlags then
for flag, pending in pairs(self._pendingFlags) do
data[flag] = pending.Entry
end
end
for flag, element in pairs(Library.Flags) do
if element._type and type(element.Get) == "function" then
local ok, entry = pcall(serializeFlag, element)
if ok then
data[flag] = entry
end
end
end
return data
end
function Window:_applyFlags(data, silent)
silent = silent == true
self._pendingFlags = {}
for flag, entry in pairs(data) do
local element = Library.Flags[flag]
if element and type(element.Set) == "function" then
if entryMatches(element, entry) then
pcall(deserializeFlag, element, entry, silent)
end
elseif type(entry) == "table" then
self._pendingFlags[flag] = { Entry = entry, Silent = silent }
end
end
end
-- Every flagged element reports here: its starting value becomes its default,
-- and a value waiting from an earlier load is applied once the constructor returns.
function Window:_flagCreated(flag, element)
local ok, entry = pcall(serializeFlag, element)
if ok then
element._defaultEntry = entry
end
local pending = self._pendingFlags and self._pendingFlags[flag]
if not pending then
return
end
task.defer(function()
if self._pendingFlags[flag] ~= pending or Library.Flags[flag] ~= element or element._destroyed then
return
end
self._pendingFlags[flag] = nil
if entryMatches(element, pending.Entry) then
pcall(deserializeFlag, element, pending.Entry, pending.Silent)
end
end)
end
-- Anything listening for config changes (the config manager's status line).
function Window:_configChanged()
for _, handler in ipairs(self._configListeners) do
safeCall(handler)
end
end
function Window:OnConfigChanged(handler)
table.insert(self._configListeners, handler)
return function()
local index = table.find(self._configListeners, handler)
if index then
table.remove(self._configListeners, index)
end
end
end
local function validConfigName(name)
return type(name) == "string" and name ~= "" and not name:find('[\\/:%*%?"<>|]')
end
local function encodeConfig(self, name, flags)
return HttpService:JSONEncode({
Folder = self.ConfigFolder,
Name = name,
Version = CONFIG_VERSION,
Config = HttpService:JSONEncode(flags),
})
end
local B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
local B64_INDEX = {}
for index = 1, #B64 do
B64_INDEX[B64:byte(index)] = index - 1
end
-- Only for reading the old "airflow:" share codes.
local function base64Decode(text)
if #text % 4 ~= 0 then
return nil
end
local out = {}
for index = 1, #text, 4 do
local n, pad = 0, 0
for offset = 0, 3 do
local byte = text:byte(index + offset)
local value = B64_INDEX[byte]
if byte == 61 and index + 3 >= #text then
pad += 1
value = 0
elseif value == nil or pad > 0 then
return nil
end
n = n * 64 + value
end
if pad > 2 then
return nil
end
local chunk = string.char(bit32.extract(n, 16, 8), bit32.extract(n, 8, 8), bit32.extract(n, 0, 8))
table.insert(out, chunk:sub(1, 3 - pad))
end
return table.concat(out)
end
-- Reads a config file or a pasted code: the JSON above (Config as a string or
-- a table), a bare flag table, or an old "airflow:" code. Returns flags, info.
local function decodeConfig(text)
text = tostring(text or ""):match("^%s*(.-)%s*$")
if text:sub(1, #LEGACY_SHARE_PREFIX) == LEGACY_SHARE_PREFIX then
text = base64Decode((text:sub(#LEGACY_SHARE_PREFIX + 1):gsub("%s", "")))
if not text then
return nil, "invalid config code"
end
end
local ok, data = pcall(function()
return HttpService:JSONDecode(text)
end)
if not ok or type(data) ~= "table" then
return nil, "invalid config code"
end
local info = {}
-- A bare flag table could have a flag named Config, but its entry carries a Type.
if type(data.Config) == "string" or (type(data.Config) == "table" and data.Config.Type == nil) then
info.Folder = type(data.Folder) == "string" and data.Folder or nil
info.Name = type(data.Name) == "string" and data.Name or nil
info.Version = tonumber(data.Version)
local flags = data.Config
if type(flags) == "string" then
ok, flags = pcall(function()
return HttpService:JSONDecode(data.Config)
end)
if not ok then
return nil, "invalid config code"
end
end
if type(flags) ~= "table" then
return nil, "invalid config code"
end
data = flags
end
for flag, entry in pairs(data) do
if type(flag) ~= "string" or type(entry) ~= "table" then
return nil, "invalid config code"
end
end
return data, info
end
local function writeConfig(self, name, data)
if not validConfigName(name) then
return false, "invalid config name"
end
if not fileApiAvailable() then
return false, "file API unavailable"
end
local encoded, text = pcall(encodeConfig, self, name, data)
if not encoded then
return false, "could not encode config"
end
ensureFolder(self.ConfigFolder)
return pcall(writefile, configPath(self, name), text)
end
local function readConfig(self, name)
if not fileApiAvailable() then
return nil, "file API unavailable"
end
if not validConfigName(name) then
return nil, "invalid config name"
end
local path = configPath(self, name)
if not isfile(path) then
return nil, "no config named " .. tostring(name)
end
local ok, text = pcall(readfile, path)
if not ok or type(text) ~= "string" then
return nil, "could not read " .. name
end
local data, info = decodeConfig(text)
if not data then
return nil, "config is not valid JSON"
end
return data, info
end
function Window:ConfigExists(name)
return validConfigName(name) and fileApiAvailable() and isfile(configPath(self, name)) or false
end
function Window:SaveConfig(name)
name = name or self.ConfigName
if not name then
return false, "no config name"
end
local ok, err = writeConfig(self, name, collectFlags(self))
if ok then
self.ConfigName = name
end
return ok, err
end
function Window:LoadConfig(name, silent)
name = name or self.ConfigName
if not name then
return false, "no config name"
end
local data, err = readConfig(self, name)
if not data then
return false, err
end
self:_applyFlags(data, silent)
self.ConfigName = name
self.LoadedConfig = name
self:_configChanged()
return true
end
function Window:DeleteConfig(name)
if not fileApiAvailable() or type(delfile) ~= "function" then
return false, "file API unavailable"
end
if not validConfigName(name) then
return false, "invalid config name"
end
local path = configPath(self, name)
if not isfile(path) then
return false, "no config named " .. name
end
local ok, err = pcall(delfile, path)
if not ok then
return false, err
end
if self.LoadedConfig == name then
self.LoadedConfig = nil
end
self:_configChanged()
return true
end
-- Moves the file and anything pointing at it: the loaded name and both autoloads.
function Window:RenameConfig(name, newName)
if not validConfigName(newName) then
return false, "invalid config name"
end
if name == newName then
return true
end
if self:ConfigExists(newName) then
return false, newName .. " already exists"
end
local data, err = readConfig(self, name)
if not data then
return false, err
end
local ok
ok, err = writeConfig(self, newName, data)
if not ok then
return false, err
end
if type(delfile) == "function" then
pcall(delfile, configPath(self, name))
end
if self.LoadedConfig == name then
self.LoadedConfig = newName
end
if self.ConfigName == name then
self.ConfigName = newName
end
for _, scope in ipairs({ "Global", "Account" }) do
if self:GetAutoload(scope) == name then
self:SetAutoload(newName, scope)
end
end
self:_configChanged()
return true
end
function Window:ListConfigs()
local names = {}
if type(listfiles) ~= "function" or type(isfolder) ~= "function" or not isfolder(self.ConfigFolder) then
return names
end
local ok, paths = pcall(listfiles, self.ConfigFolder)
if not ok or type(paths) ~= "table" then
return names
end
for _, path in ipairs(paths) do
local name = path:match("([^/\\]+)%.json$")
if name then
table.insert(names, name)
end
end
table.sort(names, function(a, b)
return a:lower() < b:lower()
end)
return names
end
-- Puts every flagged element back to the value it was created with.
function Window:ResetConfig(silent)
self._pendingFlags = {}
for _, element in pairs(Library.Flags) do
if element._defaultEntry and type(element.Set) == "function" then
pcall(deserializeFlag, element, element._defaultEntry, silent == true)
end
end
self:_configChanged()
return true
end
-- Empties a folder and removes it, one file at a time when the executor has
-- no delfolder or it refuses a folder that still has files in it.
local function wipeFolder(path)
if type(isfolder) ~= "function" or not isfolder(path) then
return true
end
if type(delfolder) == "function" and pcall(delfolder, path) and not isfolder(path) then
return true
end
if type(listfiles) == "function" then
local ok, entries = pcall(listfiles, path)
for _, entry in ipairs(ok and entries or {}) do
if isfolder(entry) then
wipeFolder(entry)
elseif type(delfile) == "function" then
pcall(delfile, entry)
end
end
end
if type(delfolder) == "function" then
pcall(delfolder, path)
end
return not isfolder(path)
end
-- Deletes the script's whole workspace folder: configs, themes, the default
-- theme, autoload, settings and the execution count.
function Window:ClearWorkspace()
local ok = wipeFolder(self.ConfigFolder)
self.LoadedConfig = nil
for _, handler in ipairs(self._workspaceListeners or {}) do
safeCall(handler)
end
self:_configChanged()
return ok
end
-- Share codes are the same JSON a config file holds.
function Window:ExportConfig(name)
local data, err
if name then
data, err = readConfig(self, name)
if not data then
return nil, err
end
else
data = collectFlags(self)
name = self.LoadedConfig or self.ConfigName
end
local ok, code = pcall(encodeConfig, self, name, data)
if not ok then
return nil, "could not encode config"
end
return code
end
-- Reads a code without applying it. Returns flags and { Folder, Name, Version }.
function Window:DecodeConfig(code)
return decodeConfig(code)
end
-- saveAs: a name to save under, true for the name inside the code, or nil to
-- only apply it. Codes made for another folder are refused unless force is set.
function Window:ImportConfig(code, saveAs, force)
local data, info = decodeConfig(code)
if not data then
return false, info
end
if not force and info.Folder and info.Folder ~= self.ConfigFolder then
return false, "config is for " .. info.Folder
end
if saveAs == true then
saveAs = info.Name
if not validConfigName(saveAs) then
return false, "config has no usable name"
end
end
if saveAs then
local ok, err = writeConfig(self, saveAs, data)
if not ok then
return false, err
end
end
self:_applyFlags(data)
if saveAs then
self.ConfigName = saveAs
self.LoadedConfig = saveAs
end
self:_configChanged()
return true, nil, saveAs
end
local function readText(path)
if not fileApiAvailable() or not isfile(path) then
return nil
end
local ok, text = pcall(readfile, path)
if ok and type(text) == "string" and text ~= "" then
return text
end
return nil
end
local function removeFile(path)
if fileApiAvailable() and type(delfile) == "function" and isfile(path) then
pcall(delfile, path)
end
end
-- Manager settings (autoload mode) live beside the configs, apart
-- from the flags, so loading a config never changes them.
local function settingsPath(self)
return self.ConfigFolder .. "/configsettings.txt"
end
function Window:_readConfigSettings()
local text = readText(settingsPath(self))
if text then
local ok, data = pcall(function()
return HttpService:JSONDecode(text)
end)
if ok and type(data) == "table" then
return data
end
end
return {}
end
function Window:_writeConfigSettings(changes)
if not fileApiAvailable() then
return false, "file API unavailable"
end
local data = self:_readConfigSettings()
for key, value in pairs(changes) do
data[key] = value
end
ensureFolder(self.ConfigFolder)
return pcall(writefile, settingsPath(self), HttpService:JSONEncode(data))
end
-- Autoload: one config name kept next to the configs, either for every
-- account ("Global") or only the local player ("Account").
local function autoloadPath(self, scope)
if scope == "Account" then
return self.ConfigFolder .. "/autoload_" .. tostring(LocalPlayer and LocalPlayer.UserId or 0) .. ".txt"
end
return self.ConfigFolder .. "/autoload.txt"
end
function Window:GetAutoloadMode()
return self:_readConfigSettings().AutoloadMode == "Account" and "Account" or "Global"
end
function Window:GetAutoload(scope)
if scope then
return readText(autoloadPath(self, scope))
end
return readText(autoloadPath(self, "Account")) or readText(autoloadPath(self, "Global"))
end
function Window:SetAutoload(name, scope)
if not fileApiAvailable() then
return false, "file API unavailable"
end
scope = scope or self:GetAutoloadMode()
local ok, err = true, nil
if name == nil or name == "" then
removeFile(autoloadPath(self, scope))
if scope == "Global" then
removeFile(autoloadPath(self, "Account"))
end
else
ensureFolder(self.ConfigFolder)
ok, err = pcall(writefile, autoloadPath(self, scope), tostring(name))
if ok and scope == "Global" then
removeFile(autoloadPath(self, "Account"))
end
end
self:_configChanged()
return ok, err
end
-- Switching the mode moves the current autoload so only one stays active.
function Window:SetAutoloadMode(mode)
mode = mode == "Account" and "Account" or "Global"
if mode == self:GetAutoloadMode() then
return true
end
local current = self:GetAutoload()
local ok, err = self:_writeConfigSettings({ AutoloadMode = mode })
if not ok then
return false, err
end
removeFile(autoloadPath(self, mode == "Account" and "Global" or "Account"))
if current then
return self:SetAutoload(current, mode)
end
self:_configChanged()
return true
end
function Window:LoadAutoload(silent)
local name = self:GetAutoload()
if not name then
return false, "no autoload config"
end
return self:LoadConfig(name, silent)
end
-- Themes
Library._themeListeners = {}
local function toColor(value)
if typeof(value) == "Color3" then
return value
elseif type(value) == "string" then
local r, g, b = value:match("^%s*#?(%x%x)(%x%x)(%x%x)%s*$")
if r then
return Color3.fromRGB(tonumber(r, 16), tonumber(g, 16), tonumber(b, 16))
end
elseif type(value) == "table" then
local r, g, b = value[1] or value.R, value[2] or value.G, value[3] or value.B
if tonumber(r) and tonumber(g) and tonumber(b) then
return Color3.fromRGB(r, g, b)
end
end
return nil
end
local function hexOf(color)
return string.format("#%02X%02X%02X",
math.floor(color.R * 255 + 0.5),
math.floor(color.G * 255 + 0.5),
math.floor(color.B * 255 + 0.5))
end
-- Coloured text for statuses, metrics, table cells and labels. A theme key
-- used as a tag, online, follows theme changes; plain
-- rich text such as works too. Library.Colorize wraps
-- text for you. Table fields, not locals: the chunk is at Luau's 200 limit.
Library._toneTags = {}
for _, name in ipairs(THEME_KEYS) do
Library._toneTags[string.lower(name)] = name
end
-- A theme key, a Color3, "#rrggbb" or { r, g, b } as a Color3.
function Library._color(value)
if type(value) == "string" then
local key = Library._toneTags[string.lower(value)]
if key then
return Theme[key]
end
end
return toColor(value)
end
function Library._rich(text)
text = tostring(text == nil and "" or text)
if not string.find(text, "<", 1, true) then
return text
end
return (string.gsub(text, "<(/?)(%a+%d*)>", function(slash, name)
local key = Library._toneTags[string.lower(name)]
if not key then
return nil
end
if slash == "/" then
return ""
end
return ''
end))
end
-- Library.Colorize("12 ms", "Success") or Library:Colorize(text, Color3).
function Library.Colorize(...)
local args = { ... }
if args[1] == Library then
table.remove(args, 1)
end
local text, color = tostring(args[1] == nil and "" or args[1]), args[2]
local key = type(color) == "string" and Library._toneTags[string.lower(color)]
if key then
return "<" .. key .. ">" .. text .. "" .. key .. ">"
end
local resolved = toColor(color)
if not resolved then
return text
end
return '' .. text .. ""
end
-- The window's corner radius in pixels, 0 (square) to 20; every other
-- rounded corner scales with it. The default is 10. Corners ease to the new
-- radius instead of jumping. Returns the radius it settled on.
function Library:SetCornerRadius(pixels, instant)
pixels = math.clamp(math.floor((tonumber(pixels) or 10) + 0.5), 0, 20)
Library.Roundness = pixels / 10
for object, base in pairs(Library._cornerBases) do
if not object.Parent then
Library._cornerBases[object] = nil
else
local radius = UDim.new(0, math.floor(base * Library.Roundness + 0.5))
if instant then
object.CornerRadius = radius
else
tween(object, { CornerRadius = radius }, 0.25, Enum.EasingStyle.Quint)
end
end
end
return pixels
end
function Library:GetCornerRadius()
return math.floor(Library.Roundness * 10 + 0.5)
end
function Library:SetTheme(theme, instant)
if type(theme) == "string" then
local found = Library.ThemePresets[theme]
if not found then
return false
end
Library.ThemeName = theme
theme = found
elseif type(theme) ~= "table" then
return false
end
local changed = false
for _, name in ipairs(THEME_KEYS) do
local color = toColor(theme[name])
if color and color ~= Theme[name] then
Theme[name] = color
changed = true
end
end
if not changed then
return true
end
rebuildThemeLookup()
sweepBindings()
driveTheme(instant and 0 or 0.3)
for owner, fn in pairs(themeHooks) do
if owner.Parent then
pcall(fn)
end
end
for _, fn in ipairs(Library._themeListeners) do
pcall(fn)
end
return true
end
function Library:GetTheme()
local copy = {}
for _, name in ipairs(THEME_KEYS) do
copy[name] = Theme[name]
end
return copy
end
-- The script's starting theme: a preset name or a colour table. A default the
-- player picked in the theme manager still wins over it.
function Library:SetDefaultTheme(theme)
Library.DefaultTheme = theme
if theme == nil then
return true
end
local live = false
for _, window in ipairs(Library.Windows) do
if not window._destroyed then
if window:GetDefaultTheme() then
return true
end
live = true
end
end
return Library:SetTheme(theme, not live)
end
function Library:GetDefaultTheme()
return Library.DefaultTheme
end
local function themeFolder(self)
return self.ConfigFolder .. "/themes"
end
function Window:SaveTheme(name)
if not fileApiAvailable() then
return false, "file API unavailable"
end
if type(name) ~= "string" or name == "" then
return false, "theme needs a name"
end
ensureFolder(self.ConfigFolder)
ensureFolder(themeFolder(self))
local data = {}
for _, key in ipairs(THEME_KEYS) do
data[key] = hexOf(Theme[key])
end
return pcall(function()
writefile(themeFolder(self) .. "/" .. name .. ".json", HttpService:JSONEncode(data))
end)
end
function Window:LoadTheme(name, instant)
if Library.ThemePresets[name] then
return Library:SetTheme(name, instant)
end
local text = readText(themeFolder(self) .. "/" .. tostring(name) .. ".json")
if not text then
return false, "no theme named " .. tostring(name)
end
local ok, data = pcall(function()
return HttpService:JSONDecode(text)
end)
if not ok or type(data) ~= "table" then
return false, "theme is not valid JSON"
end
Library.ThemeName = name
return Library:SetTheme(data, instant)
end
function Window:DeleteTheme(name)
if not fileApiAvailable() or type(delfile) ~= "function" then
return false, "file API unavailable"
end
local path = themeFolder(self) .. "/" .. tostring(name) .. ".json"
if not isfile(path) then
return false, "no theme named " .. tostring(name)
end
delfile(path)
if self:GetDefaultTheme() == name then
self:SetDefaultTheme(nil)
end
return true
end
function Window:ListThemes()
local names = {}
if type(listfiles) ~= "function" or type(isfolder) ~= "function" or not isfolder(themeFolder(self)) then
return names
end
for _, path in ipairs(listfiles(themeFolder(self))) do
local name = path:match("([^/\\]+)%.json$")
if name then
table.insert(names, name)
end
end
table.sort(names)
return names
end
function Window:GetDefaultTheme()
return readText(themeFolder(self) .. "/default.txt")
end
function Window:SetDefaultTheme(name)
if not fileApiAvailable() then
return false, "file API unavailable"
end
ensureFolder(self.ConfigFolder)
ensureFolder(themeFolder(self))
local path = themeFolder(self) .. "/default.txt"
if name == nil or name == "" then
if isfile(path) and type(delfile) == "function" then
pcall(delfile, path)
end
return true
end
return pcall(writefile, path, tostring(name))
end
function Window:_applyDefaultTheme()
local name = self:GetDefaultTheme()
if name then
local ok, loaded = pcall(self.LoadTheme, self, name, true)
if ok and loaded then
return
end
end
if Library.DefaultTheme ~= nil then
pcall(Library.SetTheme, Library, Library.DefaultTheme, true)
end
end
-- Background images
-- Image assets anyone can use, from Fluent-modded's themes.
Library.BackgroundPresets = {
["Deep Violet"] = "rbxassetid://136310484943077",
["Blood Red"] = "rbxassetid://121343473918667",
["Cyanic"] = "rbxassetid://95656189244173",
["Amber Glow"] = "rbxassetid://107795771598485",
["Bloomings"] = "rbxassetid://133541508207801",
["Lavender Pink"] = "rbxassetid://126107479485287",
}
Library.BackgroundPresetOrder = { "Deep Violet", "Blood Red", "Cyanic", "Amber Glow", "Bloomings", "Lavender Pink" }
-- Table fields, not locals: the library chunk is at Luau's 200 local limit.
Library._imageExtensions = { png = true, jpg = true, jpeg = true, webp = true, gif = true, bmp = true }
function Window:_backgroundFolder()
return self.ConfigFolder .. "/backgrounds"
end
function Window:_trimBackground(source)
return (tostring(source or ""):match("^%s*(.-)%s*$"))
end
-- Turns a background source into something ImageLabel.Image takes. Web images
-- are downloaded once into backgrounds/cache and loaded with getcustomasset,
-- so this yields the first time a URL is used.
function Window:_resolveBackground(source)
source = self:_trimBackground(source)
local preset = Library.BackgroundPresets[source]
if preset then
return preset
end
if source:match("^%d+$") then
return "rbxassetid://" .. source
end
if source:match("^rbxassetid://%d+") or source:match("^rbxasset://") or source:match("^rbxthumb://")
or source:match("^https?://www%.roblox%.com/asset") then
return source
end
local canLoad = fileApiAvailable() and typeof(getcustomasset) == "function"
if source:match("^https?://") then
if not canLoad then
return nil, "web images need writefile and getcustomasset"
end
local clean = source:match("^[^?#]+") or source
local ext = (clean:match("%.(%w+)$") or "png"):lower()
if not Library._imageExtensions[ext] then
ext = "png"
end
local key = (clean:gsub("^https?://", ""):gsub("[^%w]", "_")):sub(-60)
local folder = self:_backgroundFolder() .. "/cache"
local path = folder .. "/" .. key .. "_" .. #source .. "." .. ext
if not isfile(path) then
local ok, body = pcall(function()
local request = (syn and syn.request) or http_request or request
if request then
local response = request({ Url = source, Method = "GET" })
return response and response.Body
end
return game:HttpGet(source, true)
end)
if not ok or type(body) ~= "string" or #body < 128 then
return nil, "could not download the image"
end
local head = body:sub(1, 64):lower()
if head:find("/backgrounds. nil or "None"
-- removes it. May yield while a web image downloads.
function Window:SetBackground(source)
self._backgroundGeneration += 1
local generation = self._backgroundGeneration
local image = self._backgroundImage
source = source ~= false and self:_trimBackground(source) or ""
if source == "" or source == "None" then
self.Background = nil
tween(image, { ImageTransparency = 1 }, 0.25)
task.delay(0.25, function()
if self._backgroundGeneration == generation then
image.Visible = false
image.Image = ""
end
end)
self:SetTransparent(self.Transparent)
return true
end
local asset, err = self:_resolveBackground(source)
if self._backgroundGeneration ~= generation or self._destroyed then
return false, "replaced by a newer background"
end
if not asset then
return false, err
end
self.Background = source
image.Image = asset
image.Visible = true
tween(image, { ImageTransparency = self.BackgroundTransparency }, 0.3)
self:SetTransparent(self.Transparent)
return true
end
function Window:GetBackground()
return self.Background
end
-- 0 shows the image fully, 1 hides it behind the theme background colour.
function Window:SetBackgroundTransparency(value)
self.BackgroundTransparency = math.clamp(tonumber(value) or 0.35, 0, 1)
if self.Background then
tween(self._backgroundImage, { ImageTransparency = self.BackgroundTransparency }, 0.15)
end
end
-- Preset names, then the image files in /backgrounds.
function Window:ListBackgrounds()
local names = table.clone(Library.BackgroundPresetOrder)
local folder = self:_backgroundFolder()
if type(listfiles) == "function" and type(isfolder) == "function" and isfolder(folder) then
local files = {}
for _, path in ipairs(listfiles(folder)) do
local name = path:match("([^/\]+)$")
local ext = name and name:match("%.(%w+)$")
if ext and Library._imageExtensions[ext:lower()] then
table.insert(files, name)
end
end
table.sort(files)
for _, name in ipairs(files) do
table.insert(names, name)
end
end
return names
end
local function emptyState(parent, icon, text)
local holder = create("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 0.5, -10),
Size = UDim2.fromOffset(200, 70),
BackgroundTransparency = 1,
Parent = parent,
})
local image = create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0),
Position = UDim2.new(0.5, 0, 0, 0),
Size = UDim2.fromOffset(26, 26),
BackgroundTransparency = 1,
ImageColor3 = Theme.Muted,
ImageTransparency = 0.15,
ScaleType = Enum.ScaleType.Fit,
Parent = holder,
})
applyIcon(image, icon)
local caption = label({
Position = UDim2.fromOffset(0, 36),
Size = UDim2.new(1, 0, 0, 16),
Text = text,
TextSize = 13,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextXAlignment = Enum.TextXAlignment.Center,
Parent = holder,
})
return holder, caption, image
end
local function fadeSlideIn(group, restY)
restY = restY or 0
group.GroupTransparency = 1
group.Position = UDim2.fromOffset(0, restY + 14)
group.Visible = true
tween(group, { GroupTransparency = 0, Position = UDim2.fromOffset(0, restY) }, 0.32, Enum.EasingStyle.Quint)
end
local function formatDuration(seconds)
seconds = math.max(math.floor(seconds), 0)
local days = math.floor(seconds / 86400)
local hours = math.floor(seconds / 3600) % 24
local minutes = math.floor(seconds / 60) % 60
if days > 0 then
return string.format("%dd %dh", days, hours)
elseif hours > 0 then
return string.format("%dh %dm", hours, minutes)
elseif minutes > 0 then
return string.format("%dm %ds", minutes, seconds % 60)
end
return string.format("%ds", seconds)
end
local function shortId(id)
if type(id) ~= "string" or id == "" then
return "Studio"
end
if #id > 16 then
return id:sub(1, 8) .. "..." .. id:sub(-4)
end
return id
end
local function copyToClipboard(text)
local setter = setclipboard or toclipboard or (Clipboard and Clipboard.set)
if type(setter) ~= "function" then
return false
end
return (pcall(setter, tostring(text)))
end
local function fetchServers(order)
local url = string.format(
"https://games.roblox.com/v1/games/%d/servers/Public?sortOrder=%s&limit=100&excludeFullGames=true",
game.PlaceId,
order
)
local ok, body = pcall(function()
return game:HttpGet(url)
end)
if not ok or type(body) ~= "string" then
return nil
end
local decoded, data = pcall(function()
return HttpService:JSONDecode(body)
end)
if not decoded or type(data) ~= "table" or type(data.data) ~= "table" then
return nil
end
return data.data
end
-- Small on/off pill used on the home card for the privacy switches.
local function miniSwitch(parent, initial, order, callback)
local pill = create("TextButton", {
Size = UDim2.fromOffset(30, 16),
BackgroundColor3 = Theme.Surface3,
BorderSizePixel = 0,
Text = "",
AutoButtonColor = false,
LayoutOrder = order,
Parent = parent,
})
corner(pill, UDim.new(1, 0))
stroke(pill)
local knob = create("Frame", {
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, 2, 0.5, 0),
Size = UDim2.fromOffset(12, 12),
BackgroundColor3 = Theme.Muted,
BorderSizePixel = 0,
Parent = pill,
})
corner(knob, UDim.new(1, 0))
local on = initial == true
local function render(animate)
local duration = animate and 0.22 or 0
tween(pill, { BackgroundColor3 = on and Theme.Accent or Theme.Surface3 }, duration)
tween(knob, {
Position = on and UDim2.new(0, 16, 0.5, 0) or UDim2.new(0, 2, 0.5, 0),
BackgroundColor3 = on and Theme.AccentDark or Theme.Muted,
}, duration, Enum.EasingStyle.Back)
end
pill.MouseButton1Click:Connect(function()
on = not on
render(true)
safeCall(callback, on)
end)
render(false)
return pill
end
local function softButton(parent, text, props, callback)
local button = create("TextButton", {
BackgroundColor3 = Theme.Surface3,
BackgroundTransparency = 0.45,
BorderSizePixel = 0,
Text = "",
AutoButtonColor = false,
ClipsDescendants = true,
Parent = parent,
})
for key, value in pairs(props) do
button[key] = value
end
corner(button, UDim.new(0, 6))
local buttonStroke = stroke(button)
local text_ = label({
Size = UDim2.fromScale(1, 1),
Text = text,
TextSize = 12,
TextXAlignment = Enum.TextXAlignment.Center,
Parent = button,
})
if button.AutomaticSize == Enum.AutomaticSize.X then
text_.Size = UDim2.new(0, 0, 1, 0)
text_.AutomaticSize = Enum.AutomaticSize.X
text_.TextTruncate = Enum.TextTruncate.None
padding(button, 14, 14)
end
button.MouseEnter:Connect(function()
tween(button, { BackgroundTransparency = 0.15 }, 0.12)
tween(buttonStroke, { Color = Theme.StrokeHover }, 0.12)
end)
button.MouseLeave:Connect(function()
tween(button, { BackgroundTransparency = 0.45 }, 0.25)
tween(buttonStroke, { Color = Theme.Stroke }, 0.25)
end)
button.MouseButton1Click:Connect(function()
ripple(button)
safeCall(callback)
end)
return button, text_
end
local function panelCard(parent, height, order)
local frame = create("Frame", {
Size = UDim2.new(1, 0, 0, height),
BackgroundColor3 = Theme.Surface2,
BorderSizePixel = 0,
LayoutOrder = order,
Parent = parent,
})
frame:SetAttribute("NoDrag", true)
corner(frame, UDim.new(0, 10))
stroke(frame)
return frame
end
local STAT_TYPES = {
Players = { "users", "Players" },
Friends = { "user-check", "Friends" },
Execs = { "zap", "Execs" },
Session = { "timer", "Session" },
Uptime = { "timer", "Session" },
FPS = { "gauge", "FPS" },
Ping = { "wifi", "Ping" },
Executor = { "terminal", "Executor" },
Game = { "gamepad-2", "Game" },
Region = { "globe", "Region" },
Time = { "clock", "Time" },
ServerAge = { "server", "Server age" },
Memory = { "cpu", "Memory" },
}
-- Health colour for a live metric: Success under good, Warning under bad,
-- Error past it. lowerIsBetter flips the scale (ping, memory).
function Library._healthColor(value, good, bad, lowerIsBetter)
if type(value) ~= "number" then
return Theme.Text
end
if lowerIsBetter then
return value <= good and Theme.Success or value <= bad and Theme.Warning or Theme.Error
end
return value >= good and Theme.Success or value >= bad and Theme.Warning or Theme.Error
end
local function bumpExecutions(folder)
if not fileApiAvailable() then
return nil
end
local path = folder .. "/executions.txt"
local count = 0
pcall(function()
ensureFolder(folder)
if isfile(path) then
count = tonumber(readfile(path)) or 0
end
end)
count += 1
pcall(writefile, path, tostring(count))
return count
end
-- Containers: a tab or sub tab owns a scrolling list. Elements added straight
-- to it stack full width, groupboxes go into two columns underneath.
local function buildContainer(owner, parent, top, emptyIcon, emptyText)
local list = create("ScrollingFrame", {
Position = UDim2.fromOffset(0, top),
Size = UDim2.new(1, 0, 1, -top),
BackgroundTransparency = 1,
BorderSizePixel = 0,
ScrollBarThickness = 2,
ScrollBarImageColor3 = Theme.Accent,
ScrollBarImageTransparency = 0.5,
ScrollingDirection = Enum.ScrollingDirection.Y,
AutomaticCanvasSize = Enum.AutomaticSize.Y,
CanvasSize = UDim2.new(),
Parent = parent,
})
Library._space(padding(list, 24, 24, 2, 24), { 24, 24, 2, 24 })
Library._space(create("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 8),
Parent = list,
}), 8)
owner.List = list
owner._items = {}
owner._groupboxes = {}
owner._childCount = 0
owner._emptyText = emptyText
owner._empty, owner._emptyCaption = emptyState(parent, emptyIcon, emptyText)
list.ChildAdded:Connect(function(child)
if child:IsA("GuiObject") then
owner._childCount += 1
owner:_refreshEmpty()
end
end)
list.ChildRemoved:Connect(function(child)
if child:IsA("GuiObject") then
owner._childCount = math.max(owner._childCount - 1, 0)
owner:_refreshEmpty()
end
end)
owner:_refreshEmpty()
return list
end
function Tab:_refreshEmpty()
local empty = self._empty
if not empty then
return
end
if self._subTabs then
empty.Visible = false
else
empty.Visible = self._childCount == 0
end
end
function Tab:_columns()
if self._columnSet then
return self._columnSet
end
local holder = create("Frame", {
Name = "Columns",
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
LayoutOrder = 100000,
Parent = self.List,
})
local layout = Library._space(create("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 10),
Parent = holder,
}), 10)
local function column(order)
local frame = create("Frame", {
Size = UDim2.new(0.5, -5, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
LayoutOrder = order,
Parent = holder,
})
Library._space(create("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 10),
Parent = frame,
}), 10)
return frame
end
local set = { Holder = holder, Left = column(1), Right = column(2), Counts = { 0, 0 }, Stacked = false }
local window = self.Window
-- Narrow windows stack the right column under the left one.
local function relayout()
local width = holder.AbsoluteSize.X / window.Scale.Scale
if width <= 0 then
return
end
local stacked = width < STACK_BELOW
local gap = layout.Padding.Offset
if stacked == set.Stacked and gap == set.Gap then
return
end
set.Stacked = stacked
set.Gap = gap
layout.FillDirection = stacked and Enum.FillDirection.Vertical or Enum.FillDirection.Horizontal
local size = stacked and UDim2.new(1, 0, 0, 0) or UDim2.new(0.5, -gap / 2, 0, 0)
set.Left.Size = size
set.Right.Size = size
end
holder:GetPropertyChangedSignal("AbsoluteSize"):Connect(relayout)
layout:GetPropertyChangedSignal("Padding"):Connect(relayout)
task.defer(relayout)
self._columnSet = set
return set
end
function Tab:Groupbox(opts, icon)
if self._groupbox then
return self.Parent:Groupbox(opts, icon)
end
opts = normalize(opts, { Title = "Name" })
if icon ~= nil and opts.Icon == nil then
opts.Icon = icon
end
local window = self.Window
local set = self:_columns()
local side = opts.Side
if side == "Left" or side == 1 then
side = 1
elseif side == "Right" or side == 2 then
side = 2
else
side = set.Counts[1] <= set.Counts[2] and 1 or 2
end
set.Counts[side] += 1
local box = create("Frame", {
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundColor3 = Theme.Surface2,
BorderSizePixel = 0,
LayoutOrder = set.Counts[side],
Parent = side == 1 and set.Left or set.Right,
})
box:SetAttribute("NoDrag", true)
corner(box, UDim.new(0, 10))
stroke(box)
create("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Parent = box,
})
-- The header is a filled strip: icon and bold title on the left, a
-- chevron on the right. Pressing anywhere on it folds the box.
local header = create("TextButton", {
Size = UDim2.new(1, 0, 0, 40),
BackgroundColor3 = Theme.Surface3,
BorderSizePixel = 0,
Text = "",
AutoButtonColor = false,
LayoutOrder = 1,
Parent = box,
})
corner(header, UDim.new(0, 10))
-- Squares the strip's lower corners so it meets the body flat.
local headerFill = create("Frame", {
AnchorPoint = Vector2.new(0, 1),
Position = UDim2.fromScale(0, 1),
Size = UDim2.new(1, 0, 0, 10),
BackgroundColor3 = Theme.Surface3,
BorderSizePixel = 0,
Parent = header,
})
local titleX = 14
if opts.Icon then
local headerIcon = create("ImageLabel", {
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, 14, 0.5, 0),
Size = UDim2.fromOffset(14, 14),
BackgroundTransparency = 1,
ImageColor3 = Theme.Accent,
ScaleType = Enum.ScaleType.Fit,
ZIndex = 2,
Parent = header,
})
applyIcon(headerIcon, opts.Icon)
titleX = 37
end
local collapseButton = create("ImageLabel", {
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, -14, 0.5, 0),
Size = UDim2.fromOffset(14, 14),
BackgroundTransparency = 1,
ImageColor3 = Theme.Muted,
ScaleType = Enum.ScaleType.Fit,
ZIndex = 2,
Parent = header,
})
applyIcon(collapseButton, "chevron-down")
-- Beside the chevron, a button that pops the box out into a floating
-- panel (and docks it back once it's out). PopOut = false leaves it off.
local canPop = opts.PopOut ~= false
local popButton, popIcon
if canPop then
popButton = create("TextButton", {
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, -34, 0.5, 0),
Size = UDim2.fromOffset(24, 24),
BackgroundColor3 = Theme.Surface2,
BackgroundTransparency = 1,
BorderSizePixel = 0,
Text = "",
AutoButtonColor = false,
ZIndex = 3,
Parent = header,
})
corner(popButton, UDim.new(0, 6))
popIcon = create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromOffset(13, 13),
BackgroundTransparency = 1,
ImageColor3 = Theme.Muted,
ImageTransparency = TOUCH and 0 or 0.45,
ScaleType = Enum.ScaleType.Fit,
ZIndex = 3,
Parent = popButton,
})
applyIcon(popIcon, "picture-in-picture-2")
end
local title = label({
Position = UDim2.fromOffset(titleX, 0),
Size = UDim2.new(1, -(titleX + (canPop and 66 or 40)), 1, 0),
Text = opts.Name or "Groupbox",
TextSize = 13,
FontFace = Fonts.Bold,
ZIndex = 2,
Parent = header,
})
local rule = create("Frame", {
Size = UDim2.new(1, 0, 0, 1),
BackgroundColor3 = Theme.Stroke,
BorderSizePixel = 0,
LayoutOrder = 2,
Parent = box,
})
local body = create("Frame", {
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
ClipsDescendants = true,
LayoutOrder = 3,
Parent = box,
})
local inner = create("Frame", {
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
Parent = body,
})
Library._space(padding(inner, 0, 0, 6, 8), { 0, 0, 6, 8 })
Library._space(create("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 2),
Parent = inner,
}), 2)
local group = setmetatable({
Name = opts.Name or "Groupbox",
Window = window,
Parent = self,
List = inner,
Frame = box,
_frame = box,
_order = 0,
_groupbox = true,
_metrics = METRICS.Row,
_items = {},
}, Tab)
table.insert(self._groupboxes, group)
local collapsed = false
local generation = 0
local function setCollapsed(value, instant)
value = value == true
if value == collapsed then
return
end
collapsed = value
generation += 1
local current = generation
local duration = instant and 0 or 0.32
-- Anything open inside the box (a dropdown list, a picker, a focused
-- box) closes with it, rather than floating over a folded header.
if value then
for _, element in ipairs(group._items) do
if element._onHide then
element._onHide()
end
if element.Open and type(element.SetOpen) == "function" then
element:SetOpen(false)
end
end
end
window._controlsDirty = true
local scale = window.Scale.Scale
if value then
body.AutomaticSize = Enum.AutomaticSize.None
body.Size = UDim2.new(1, 0, 0, body.AbsoluteSize.Y / scale)
tween(body, { Size = UDim2.new(1, 0, 0, 0) }, duration, Enum.EasingStyle.Quint)
else
tween(body, { Size = UDim2.new(1, 0, 0, inner.AbsoluteSize.Y / scale) }, duration, Enum.EasingStyle.Quint)
local function settle()
if generation == current and not collapsed then
body.AutomaticSize = Enum.AutomaticSize.Y
body.Size = UDim2.new(1, 0, 0, 0)
end
end
if duration > 0 then
task.delay(duration, settle)
else
settle()
end
end
local motion = instant and 0 or 0.3
-- Folded, the strip is the whole box: the hairline under it drops out
-- of the layout once the body has closed, so the box ends exactly at
-- the strip and both share the same rounded corners.
if value then
local function fold()
if generation == current and collapsed then
rule.Visible = false
headerFill.Visible = false
end
end
if duration > 0 then
task.delay(duration, fold)
else
fold()
end
else
rule.Visible = true
headerFill.Visible = true
end
tween(collapseButton, { Rotation = value and -90 or 0 }, motion, Enum.EasingStyle.Quint)
end
local function hover(on)
local duration = on and 0.15 or 0.25
tween(collapseButton, { ImageColor3 = on and Theme.Text or Theme.Muted }, duration)
if popIcon and not TOUCH then
tween(popIcon, { ImageTransparency = on and 0 or 0.45 }, duration)
end
end
header.MouseEnter:Connect(function()
hover(true)
end)
header.MouseLeave:Connect(function()
hover(false)
end)
local isTap = tapGuard(header, function()
return box:FindFirstAncestorWhichIsA("ScrollingFrame")
end)
-- Popping out: the box itself is lifted out of its column into a panel
-- on the window's pop-out layer, and a slot holds its place in the column
-- (pressing the slot docks it back). The header drags the panel; a drop
-- is clamped on screen and the spot is remembered for the next pop-out.
local popped, popGeneration = false, 0
local panel, panelScale, panelShadow, slot, stopPanel
local dragStart, dragOrigin, dragMoved, suppressClick = nil, nil, false, false
local animating = false
local function layerScale()
return math.max(window.Scale.Scale, 0.05)
end
local function toLayer(point)
return (point - window._popLayer.AbsolutePosition) / layerScale()
end
-- Keeps a panel's top-centre spot on screen, header always reachable.
local function clampSpot(spot, width, height)
local view = window._popLayer.AbsoluteSize / layerScale()
local margin = 8
local x = math.clamp(spot.X, width / 2 + margin, math.max(view.X - width / 2 - margin, width / 2 + margin))
local y = math.clamp(spot.Y, margin, math.max(view.Y - math.min(height, 120) - margin, margin))
return Vector2.new(x, y)
end
local function closeInner()
for _, element in ipairs(group._items) do
if element._onHide then
element._onHide()
end
if element.Open and type(element.SetOpen) == "function" then
element:SetOpen(false)
end
end
end
local function setPopIcon(out)
if popIcon then
applyIcon(popIcon, out and "minimize-2" or "picture-in-picture-2")
end
end
local function popOut()
if popped or not canPop or not box.Parent or group._destroyed then
return
end
-- A dock still in flight lands first, so the box is back in its column.
if group._settleDock then
group._settleDock()
end
local layer = window:_popoutLayer()
popped = true
group._popped = true
popGeneration += 1
local current = popGeneration
closeInner()
window:_closePopups()
local scale = layerScale()
local column = box.Parent
local from = toLayer(box.AbsolutePosition)
local width = box.AbsoluteSize.X / scale
local height = box.AbsoluteSize.Y / scale
-- The slot left behind: starts at the box's height so nothing below
-- jumps, then eases down to a slim bar.
slot = create("TextButton", {
Size = UDim2.new(1, 0, 0, height),
BackgroundColor3 = Theme.Surface2,
BackgroundTransparency = 0.6,
BorderSizePixel = 0,
Text = "",
AutoButtonColor = false,
ClipsDescendants = true,
LayoutOrder = box.LayoutOrder,
Parent = column,
})
slot:SetAttribute("NoDrag", true)
corner(slot, UDim.new(0, 10))
stroke(slot, Theme.Stroke, 1).Transparency = 0.3
local slotLabel = label({
Size = UDim2.new(1, -24, 0, 40),
Position = UDim2.fromOffset(12, 0),
Text = Library._rich(group.Name) .. ' · popped out, click to dock',
TextSize = 12,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextXAlignment = Enum.TextXAlignment.Center,
RichText = true,
TextTransparency = 1,
Parent = slot,
})
local thisSlot = slot
slot.MouseEnter:Connect(function()
tween(thisSlot, { BackgroundTransparency = 0.35 }, 0.15)
end)
slot.MouseLeave:Connect(function()
tween(thisSlot, { BackgroundTransparency = 0.6 }, 0.25)
end)
slot.MouseButton1Click:Connect(function()
if slot == thisSlot then
group:Dock()
end
end)
tween(slot, { Size = UDim2.new(1, 0, 0, 40) }, 0.42, Enum.EasingStyle.Quint)
tween(slotLabel, { TextTransparency = 0 }, 0.3)
slot.Visible = box.Visible
panel = create("Frame", {
Name = "PopOut",
AnchorPoint = Vector2.new(0.5, 0),
Position = UDim2.fromOffset(from.X + width / 2, from.Y),
Size = UDim2.fromOffset(width, height),
BackgroundTransparency = 1,
Parent = layer,
})
panel:SetAttribute("NoDrag", true)
panelScale = create("UIScale", { Parent = panel })
panelShadow = create("ImageLabel", {
Position = UDim2.fromOffset(-24, -18),
Size = UDim2.new(1, 48, 1, 44),
BackgroundTransparency = 1,
Image = Assets.Shadow,
ImageColor3 = Color3.new(0, 0, 0),
ImageTransparency = 1,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(49, 49, 450, 450),
ZIndex = 0,
Parent = panel,
})
table.insert(window._popPanels, panel)
box.Parent = panel
window._controlsDirty = true
setPopIcon(true)
-- Lifts off the column: a spring up and out to its float spot, with
-- the shadow deepening under it.
local spot = clampSpot(group._popSpot or Vector2.new(from.X + width / 2 + 22, from.Y - 14), width, height)
animating = true
panelScale.Scale = 0.97
tween(panel, { Position = UDim2.fromOffset(spot.X, spot.Y) }, 0.5, Enum.EasingStyle.Quint)
tween(panelScale, { Scale = 1 }, 0.55, Enum.EasingStyle.Back)
tween(panelShadow, { ImageTransparency = 0.35 }, 0.4)
task.delay(0.5, function()
if popGeneration == current then
animating = false
end
end)
-- Each frame: the panel follows the box's height (folding, rows
-- coming and going), mirrors its visibility, and stays on screen.
local thisPanel = panel
stopPanel = window:_listen("Render", function()
if panel ~= thisPanel or not thisPanel.Parent then
return
end
local boxHeight = box.AbsoluteSize.Y / layerScale()
if math.abs(thisPanel.Size.Y.Offset - boxHeight) > 0.5 then
thisPanel.Size = UDim2.fromOffset(thisPanel.Size.X.Offset, boxHeight)
end
thisPanel.Visible = box.Visible
if slot then
slot.Visible = box.Visible
end
if not animating and not dragStart then
local position = Vector2.new(thisPanel.Position.X.Offset, thisPanel.Position.Y.Offset)
local clamped = clampSpot(position, thisPanel.Size.X.Offset, boxHeight)
if (clamped - position).Magnitude > 0.5 then
thisPanel.Position = UDim2.fromOffset(clamped.X, clamped.Y)
end
end
end)
end
local function dock(instant)
if not popped then
return
end
popped = false
group._popped = false
popGeneration += 1
dragStart, dragMoved, suppressClick = nil, false, false
closeInner()
window:_closePopups()
local oldPanel, oldSlot, oldStop, oldScale, oldShadow = panel, slot, stopPanel, panelScale, panelShadow
panel, slot, stopPanel = nil, nil, nil
setPopIcon(false)
group._popSpot = Vector2.new(oldPanel.Position.X.Offset, oldPanel.Position.Y.Offset)
local settled = false
local function settle()
if settled then
return
end
settled = true
if group._settleDock == settle then
group._settleDock = nil
end
if oldStop then
oldStop()
end
local index = table.find(window._popPanels, oldPanel)
if index then
table.remove(window._popPanels, index)
end
-- Back into the column, unless it was popped again meanwhile.
if box.Parent == oldPanel then
if oldSlot and oldSlot.Parent and not group._destroyed then
box.Parent = oldSlot.Parent
box.LayoutOrder = oldSlot.LayoutOrder
end
end
if oldSlot then
oldSlot:Destroy()
end
if box.Parent ~= oldPanel then
oldPanel:Destroy()
end
animating = false
window._controlsDirty = true
end
group._settleDock = settle
-- Flies home only when its slot is on screen; otherwise (another tab,
-- the window hidden) it just drops back into place.
local canFly = not instant and oldSlot and oldSlot.Parent and window.Root.Visible and box.Visible
and oldSlot.AbsoluteSize.X > 0 and isShown(oldSlot, window.Body, oldSlot.AbsolutePosition + oldSlot.AbsoluteSize / 2)
if not canFly then
settle()
return
end
animating = true
local scale = layerScale()
local height = box.AbsoluteSize.Y / scale
local width = oldSlot.AbsoluteSize.X / scale
tween(oldSlot, { Size = UDim2.new(1, 0, 0, height) }, 0.36, Enum.EasingStyle.Quint)
local oldLabel = oldSlot:FindFirstChildWhichIsA("TextLabel")
if oldLabel then
tween(oldLabel, { TextTransparency = 1 }, 0.12)
end
-- The slot's top stays put as it grows, so its spot is the landing.
local target = toLayer(oldSlot.AbsolutePosition)
tween(oldPanel, {
Position = UDim2.fromOffset(target.X + width / 2, target.Y),
Size = UDim2.fromOffset(width, height),
}, 0.36, Enum.EasingStyle.Quint)
tween(oldScale, { Scale = 1 }, 0.2)
tween(oldShadow, { ImageTransparency = 1 }, 0.3)
task.delay(0.37, settle)
end
-- Dragging the floating panel by its header.
header.InputBegan:Connect(function(input)
if not popped or not isPress(input) or not panel then
return
end
dragStart = Vector2.new(input.Position.X, input.Position.Y)
dragOrigin = Vector2.new(panel.Position.X.Offset, panel.Position.Y.Offset)
dragMoved = false
suppressClick = false
end)
window:_listen("Changed", function(input)
if not dragStart or not panel or not isMove(input) then
return
end
local delta = Vector2.new(input.Position.X, input.Position.Y) - dragStart
if not dragMoved then
if delta.Magnitude < (TOUCH and 8 or 5) then
return
end
dragMoved, suppressClick = true, true
animating = false
closeInner()
window:_closePopups()
tween(panelScale, { Scale = 1.025 }, 0.18, Enum.EasingStyle.Quint)
tween(panelShadow, { ImageTransparency = 0.15 }, 0.18)
end
local spot = clampSpot(dragOrigin + delta / layerScale(), panel.Size.X.Offset, panel.Size.Y.Offset)
panel.Position = UDim2.fromOffset(spot.X, spot.Y)
end, group)
window:_listen("Ended", function(input)
if not dragStart or not isPress(input) then
return
end
dragStart = nil
if dragMoved and panel then
dragMoved = false
tween(panelScale, { Scale = 1 }, 0.4, Enum.EasingStyle.Back)
tween(panelShadow, { ImageTransparency = 0.35 }, 0.3)
group._popSpot = Vector2.new(panel.Position.X.Offset, panel.Position.Y.Offset)
end
end, group)
if popButton then
popButton.MouseEnter:Connect(function()
tween(popButton, { BackgroundTransparency = 0 }, 0.15)
tween(popIcon, { ImageColor3 = Theme.Text }, 0.15)
end)
popButton.MouseLeave:Connect(function()
tween(popButton, { BackgroundTransparency = 1 }, 0.25)
tween(popIcon, { ImageColor3 = Theme.Muted }, 0.25)
end)
popButton.MouseButton1Click:Connect(function()
if popped then
dock()
else
popOut()
end
end)
end
header.MouseButton1Click:Connect(function()
if suppressClick then
suppressClick = false
return
end
if isTap() then
setCollapsed(not collapsed)
end
end)
function group:PopOut()
popOut()
end
function group:Dock()
dock()
end
function group:IsPoppedOut()
return popped
end
function group:SetCollapsed(value)
setCollapsed(value)
end
function group:Collapse()
setCollapsed(true)
end
function group:Expand()
setCollapsed(false)
end
function group:IsCollapsed()
return collapsed
end
function group:SetTitle(text)
group.Name = tostring(text)
title.Text = group.Name
end
group._userVisible = opts.Visible ~= false
function group:IsVisible()
return group._userVisible
end
function group:SetVisible(visible)
visible = visible and true or false
if visible == group._userVisible then
return
end
group._userVisible = visible
if not visible then
for _, element in ipairs(group._items) do
if element._onHide then
element._onHide()
end
if element.Open and type(element.SetOpen) == "function" then
element:SetOpen(false)
end
end
end
box.Visible = visible
window._controlsDirty = true
end
if not group._userVisible then
box.Visible = false
end
function group:Destroy()
for index = #group._items, 1, -1 do
local element = group._items[index]
if type(element.Destroy) == "function" then
element:Destroy()
end
end
local owner = group.Parent
local index = table.find(owner._groupboxes, group)
if index then
table.remove(owner._groupboxes, index)
end
set.Counts[side] = math.max(set.Counts[side] - 1, 0)
dock(true)
group._destroyed = true
window._controlsDirty = true
box:Destroy()
end
if opts.Collapsed then
setCollapsed(true, true)
end
if opts.PoppedOut then
task.defer(popOut)
end
return group
end
local function sideGroupbox(side)
return function(self, opts, icon)
opts = normalize(opts, { Title = "Name" })
opts.Side = side
return self:Groupbox(opts, icon)
end
end
Tab.CreateGroupbox = Tab.Groupbox
Tab.AddGroupbox = Tab.Groupbox
Tab.AddLeftGroupbox = sideGroupbox("Left")
Tab.AddRightGroupbox = sideGroupbox("Right")
-- Button rows: equal-width buttons side by side.
function Tab:ButtonRow(opts)
local specs = type(opts) == "table" and (opts.Buttons or opts) or {}
local compact = metricsOf(self).Compact
local boxHeight = TOUCH and 34 or 30
local frame = card(self, "Frame", boxHeight + (compact and 4 or 14), {})
local inset = compact and 14 or 7
local holder = create("Frame", {
Position = UDim2.fromOffset(inset, compact and 2 or 7),
Size = UDim2.new(1, -inset * 2, 0, boxHeight),
BackgroundTransparency = 1,
Parent = frame,
})
local count = math.max(#specs, 1)
local handle = { Buttons = {} }
local names = {}
for index, spec in ipairs(specs) do
if type(spec) == "string" then
spec = { Name = spec }
end
local text = spec.Name or spec.Title or "Button"
table.insert(names, text)
local button, text_ = softButton(holder, text, {
Position = UDim2.new((index - 1) / count, (index - 1) * 6 / count, 0, 0),
Size = UDim2.new(1 / count, -6 * (count - 1) / count, 1, 0),
}, function()
safeCall(spec.Callback)
end)
text_.TextSize = 13
if spec.Style == "Primary" then
tween(button, { BackgroundColor3 = Theme.Accent, BackgroundTransparency = 0.12 }, 0)
tween(text_, { TextColor3 = Theme.AccentDark }, 0)
elseif spec.Style == "Danger" then
tween(text_, { TextColor3 = Theme.Error }, 0)
end
handle.Buttons[index] = { Button = button, Label = text_ }
end
function handle:SetText(index, text)
local entry = handle.Buttons[index]
if entry then
entry.Label.Text = tostring(text)
end
end
return finishElement(self, { Name = table.concat(names, " "), Visible = not (type(opts) == "table" and opts.Visible == false) }, handle, frame, "ButtonRow")
end
-- A text box with an action button beside it, for naming new configs and themes.
-- Without buttonText the box spans the row on its own.
local function actionInput(tab, placeholder, buttonText, callback)
local boxHeight = TOUCH and 34 or 30
local frame = card(tab, "Frame", boxHeight + 4, {})
local inset = metricsOf(tab).Compact and 14 or 7
local box
local button = buttonText and softButton(frame, buttonText, {
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, -inset, 0, 2),
Size = UDim2.fromOffset(0, boxHeight),
AutomaticSize = Enum.AutomaticSize.X,
}, function()
safeCall(callback, box.Text)
end)
local holder = create("Frame", {
Position = UDim2.fromOffset(inset, 2),
Size = UDim2.new(1, -(inset * 2 + (button and 70 or 0)), 0, boxHeight),
BackgroundColor3 = Theme.Surface,
BorderSizePixel = 0,
Parent = frame,
})
corner(holder, UDim.new(0, 6))
local holderStroke = stroke(holder)
box = create("TextBox", {
Position = UDim2.fromOffset(8, 0),
Size = UDim2.new(1, -16, 1, 0),
BackgroundTransparency = 1,
Text = "",
PlaceholderText = placeholder,
PlaceholderColor3 = Theme.Muted,
TextColor3 = Theme.Text,
TextSize = 13,
FontFace = Fonts.Regular,
TextXAlignment = Enum.TextXAlignment.Center,
TextTruncate = Enum.TextTruncate.AtEnd,
ClearTextOnFocus = false,
Parent = holder,
})
box.Focused:Connect(function()
tween(holderStroke, { Color = Theme.StrokeHover }, 0.15)
end)
box.FocusLost:Connect(function(enterPressed)
tween(holderStroke, { Color = Theme.Stroke }, 0.2)
if enterPressed and box.Text ~= "" then
safeCall(callback, box.Text)
end
end)
if button then
local function fit()
local width = button.AbsoluteSize.X / tab.Window.Scale.Scale
holder.Size = UDim2.new(1, -(inset * 2 + width + 6), 0, boxHeight)
end
button:GetPropertyChangedSignal("AbsoluteSize"):Connect(fit)
task.defer(fit)
end
return finishElement(tab, { Name = placeholder }, {
Get = function()
return box.Text
end,
Set = function(_, value)
box.Text = tostring(value or "")
end,
}, frame, "ActionInput")
end
local function trim(text)
return (tostring(text or ""):match("^%s*(.-)%s*$"))
end
-- Managers go in their own groupbox when added straight to a tab.
local function managerHost(tab, opts, name, icon)
if tab._groupbox then
return tab
end
local host = tab:Groupbox({ Name = opts.Name or name, Icon = opts.Icon or icon, Side = opts.Side })
-- Library chrome, not a script feature: the feature list skips it.
host._manager = true
return host
end
function Tab:ConfigManager(opts)
opts = normalize(opts, { Title = "Name" })
local host = managerHost(self, opts, "Configs", "folder")
local window = self.Window
local manager = {}
local list, status, modeList
local function notify(ok, title, detail)
window:Notify({ Title = title, Content = detail, Type = ok and "Success" or "Error", Duration = 3 })
end
local function refreshStatus()
status:Set(("loaded: %s | autoload: %s"):format(window.LoadedConfig or "none", window:GetAutoload() or "none"))
end
local function pickedName()
local name = list:Get()
if not name then
notify(false, "No config selected", "Pick one from the list first")
end
return name
end
local nameInput = actionInput(host, opts.Placeholder or "config name", nil, function(text)
manager:Create(text)
end)
local names = window:ListConfigs()
local initial = window.LoadedConfig or window:GetAutoload()
list = host:Dropdown({
Stacked = true,
Options = names,
Default = table.find(names, initial) and initial or nil,
EmptyText = "--",
NoneText = "--",
})
host:ButtonRow({
{ Name = "Create", Callback = function() manager:Create(nameInput:Get()) end },
{ Name = "Save", Callback = function() manager:Save() end },
})
host:ButtonRow({
{ Name = "Load", Callback = function() manager:Load() end },
{ Name = "Delete", Style = "Danger", Callback = function() manager:Delete() end },
})
host:ButtonRow({
{ Name = "Set autoload", Callback = function() manager:SetAutoload() end },
{ Name = "Clear autoload", Callback = function() manager:ClearAutoload() end },
})
host:ButtonRow({
{ Name = "Rename", Callback = function() manager:Rename(nil, nameInput:Get()) end },
{ Name = "Reset", Style = "Danger", Callback = function() manager:Reset() end },
})
status = host:Label("loaded: none | autoload: none")
modeList = { Get = function() return nil end, Set = function() end }
host:Divider({ Text = "share" })
host:Button({ Name = "Copy code", Callback = function() manager:Export() end })
local codeInput = actionInput(host, "paste a config code...", nil, function(text)
manager:Import(text)
end)
host:Button({ Name = "Import code", Callback = function() manager:Import(codeInput:Get()) end })
function manager:Refresh()
local names = window:ListConfigs()
local keep = list:Get()
list:Refresh(names, true)
if keep and not table.find(names, keep) then
list:Set(window.LoadedConfig and table.find(names, window.LoadedConfig) and window.LoadedConfig or nil, true)
end
refreshStatus()
end
local function select(name)
manager:Refresh()
list:Set(name, true)
end
function manager:Create(name)
name = trim(name)
if name == "" then
notify(false, "Name the config", "Type a name before creating it")
return
end
if not validConfigName(name) then
notify(false, "Invalid name", 'Names cannot contain \\ / : * ? " < > |')
return
end
local ok, err = window:SaveConfig(name)
if ok then
window.LoadedConfig = name
nameInput:Set("")
select(name)
end
notify(ok, ok and "Config created" or "Create failed", ok and name or tostring(err))
end
function manager:Save(name)
name = name or list:Get()
if not name then
local typed = trim(nameInput:Get())
if typed == "" then
notify(false, "No config selected", "Pick one from the list or type a name")
return
end
return manager:Create(typed)
end
local ok, err = window:SaveConfig(name)
if ok then
window.LoadedConfig = name
refreshStatus()
end
notify(ok, ok and "Config saved" or "Save failed", ok and name or tostring(err))
end
function manager:Load(name)
name = name or pickedName()
if not name then
return
end
local ok, err = window:LoadConfig(name)
notify(ok, ok and "Config loaded" or "Load failed", ok and name or tostring(err))
end
function manager:Delete(name)
name = name or pickedName()
if not name then
return
end
window:Confirm({
Title = "Delete config",
Content = "Remove " .. name .. "? This cannot be undone.",
Icon = "trash-2",
ConfirmText = "Delete",
Callback = function()
local ok, err = window:DeleteConfig(name)
if ok then
if window:GetAutoload("Account") == name then
window:SetAutoload(nil, "Account")
end
if window:GetAutoload("Global") == name then
window:SetAutoload(nil, "Global")
end
list:Set(nil, true)
end
manager:Refresh()
notify(ok, ok and "Config deleted" or "Delete failed", ok and name or tostring(err))
end,
})
end
function manager:Rename(name, newName)
name = name or pickedName()
if not name then
return
end
newName = trim(newName)
if newName == "" then
notify(false, "Name the config", "Type the new name in the name box")
return
end
if not validConfigName(newName) then
notify(false, "Invalid name", 'Names cannot contain \\ / : * ? " < > |')
return
end
local ok, err = window:RenameConfig(name, newName)
if ok then
nameInput:Set("")
select(newName)
end
notify(ok, ok and "Config renamed" or "Rename failed", ok and (name .. " -> " .. newName) or tostring(err))
end
function manager:Reset()
window:Confirm({
Title = "Reset settings",
Content = "Put every setting back to its default and delete this script's folder? Saved configs, themes and autoload are removed for good.",
Icon = "rotate-ccw",
ConfirmText = "Reset",
Callback = function()
window:ResetConfig()
local wiped = window:ClearWorkspace()
window:_applyDefaultTheme()
list:Set(nil, true)
manager:Refresh()
modeList:Set("All accounts", true)
notify(wiped, wiped and "Settings reset" or "Reset incomplete",
wiped and "Everything is back to its default" or "Some files in " .. window.ConfigFolder .. " could not be deleted")
end,
})
end
function manager:SetAutoload(name)
name = name or pickedName()
if not name then
return
end
local ok, err = window:SetAutoload(name)
notify(ok, ok and "Autoload set" or "Autoload failed", ok and name or tostring(err))
end
function manager:ClearAutoload()
local current = window:GetAutoload()
if not current then
notify(false, "No autoload", "Nothing is set to autoload")
return
end
local ok, err = window:SetAutoload(nil)
notify(ok, ok and "Autoload cleared" or "Clear failed", ok and current or tostring(err))
end
function manager:ToggleAutoload(name)
name = name or pickedName()
if not name then
return
end
if window:GetAutoload() == name then
manager:ClearAutoload()
else
manager:SetAutoload(name)
end
end
function manager:SetAutoloadMode(mode)
local ok, err = window:SetAutoloadMode(mode)
local label_ = mode == "Account" and "This account" or "All accounts"
if modeList:Get() ~= label_ then
modeList:Set(label_, true)
end
notify(ok ~= false, ok ~= false and "Autoload mode" or "Mode change failed", ok ~= false and label_ or tostring(err))
end
function manager:Export(name)
local code, err = window:ExportConfig(name)
if not code then
notify(false, "Export failed", tostring(err))
return nil
end
window:CopyToClipboard(code, "Config code")
return code
end
function manager:Import(code)
code = trim(code)
if code == "" then
notify(false, "Nothing to import", "Paste a config code first")
return
end
local data, info = window:DecodeConfig(code)
if not data then
notify(false, "Import failed", "Invalid config code")
return
end
-- The typed name wins; otherwise the code's own name, never overwriting a config.
local saveAs = trim(nameInput:Get())
if saveAs == "" then
saveAs = nil
if validConfigName(info.Name) then
saveAs = info.Name
local index = 2
while window:ConfigExists(saveAs) do
saveAs = ("%s (%d)"):format(info.Name, index)
index += 1
end
end
elseif not validConfigName(saveAs) then
notify(false, "Invalid name", 'Names cannot contain \\ / : * ? " < > |')
return
end
local function run()
local ok, err = window:ImportConfig(code, saveAs, true)
if ok then
codeInput:Set("")
if saveAs then
nameInput:Set("")
select(saveAs)
end
end
notify(ok, ok and "Config imported" or "Import failed", ok and (saveAs or "Applied to current settings") or tostring(err))
end
if info.Folder and info.Folder ~= window.ConfigFolder then
window:Confirm({
Title = "Different script",
Content = ("This config was made for %s. Import it anyway? Only matching settings are applied."):format(info.Folder),
Icon = "triangle-alert",
ConfirmText = "Import",
Callback = run,
})
return
end
run()
end
local disconnect = window:OnConfigChanged(function()
-- Configs can arrive from elsewhere (a cloud install), so the list is reread.
manager:Refresh()
if window.LoadedConfig and list:Get() == nil then
list:Set(window.LoadedConfig, true)
end
end)
status._listeners = status._listeners or {}
table.insert(status._listeners, disconnect)
refreshStatus()
return manager
end
local PRESET_ORDER = {
"XYZ", "Midnight", "Nebula", "Synthwave", "Sakura", "Velvet", "Rose", "Crimson", "Sunset", "Amber", "Gold",
"Cyber", "Toxic", "Matcha", "Emerald", "Aurora", "Ocean", "Frost", "Abyss", "Mono",
"Halloween", "Haunted", "Christmas", "Valentine", "Lunar", "Tropical", "Dracula", "Coffee",
}
local function presetNames()
local names, seen = {}, {}
for _, name in ipairs(PRESET_ORDER) do
if Library.ThemePresets[name] then
table.insert(names, name)
seen[name] = true
end
end
local extra = {}
for name in pairs(Library.ThemePresets) do
if not seen[name] then
table.insert(extra, name)
end
end
table.sort(extra)
for _, name in ipairs(extra) do
table.insert(names, name)
end
return names
end
function Tab:ThemeManager(opts)
opts = normalize(opts, { Title = "Name" })
local host = managerHost(self, opts, "Themes", "palette")
local window = self.Window
local manager = {}
local themes, status, defaultRow, presets
local weather, weatherMode, dim, transparent, dragSkeleton, islandStyle
local background, backgroundInput, backgroundOpacity, backgroundLabel
local uiScale, density, adaptive, cornerRadius
local function notify(ok, title, detail)
window:Notify({ Title = title, Content = detail, Type = ok and "Success" or "Error", Duration = 3 })
end
local function refreshStatus() end -- no theme list / default row any more
local function pickedTheme()
local name = themes:Get()
if not name then
notify(false, "No theme selected", "Pick one from the list first")
end
return name
end
-- Preset / theme list were removed: harmless stand-ins keep the config code below working.
presets = { Get = function() return nil end, Set = function() end }
themes = { Get = function() return nil end, Set = function() end, Refresh = function() end }
local labels = { None = "None", Snow = "Snow", Rain = "Rain", Ember = "Hell Fire", Sakura = "Sakura", Fireflies = "Fireflies", Matrix = "Matrix" }
-- Weather mode is fixed to UI (no dropdowns for weather any more).
if window._backdrop then pcall(window.SetWeatherMode, window, "UI") end
if opts.Transparent ~= false then
transparent = host:Toggle({
Name = "Transparent",
Default = window.Transparent == true,
Callback = function(value)
window:SetTransparent(value)
end,
})
end
if opts.DragSkeleton ~= false and opts.DragStyle ~= false then
dragSkeleton = host:Dropdown({
Name = "Drag Style",
Options = Library.DragStyles,
Default = window.DragStyle,
AllowNone = false,
Callback = function(style)
if style then
window:SetDragStyle(style)
end
end,
})
end
if opts.AdaptiveSize ~= false then
adaptive = host:Toggle({
Name = "Adaptive Size",
Default = window:GetAdaptiveSize(),
Callback = function(value)
window:SetAdaptiveSize(value)
end,
})
end
if opts.Scale ~= false then
uiScale = host:Slider({
Name = "UI Scale",
Min = 60,
Max = 150,
Increment = 5,
Default = math.floor(window:GetUIScale() * 100 + 0.5),
Suffix = "%",
-- Rescaling mid-drag would move the slider out from under the
-- pointer, so a drag applies on release.
Callback = function(value)
if not (uiScale and uiScale.Dragging) then
window:SetUIScale(value / 100)
end
end,
OnRelease = function(value)
window:SetUIScale(value / 100)
end,
})
end
if opts.CornerRadius ~= false then
cornerRadius = host:Slider({
Name = "Corner Radius",
Min = 0,
Max = 20,
Increment = 1,
Default = Library:GetCornerRadius(),
Suffix = "px",
Callback = function(value)
Library:SetCornerRadius(value)
end,
})
end
if opts.Background ~= false then
backgroundLabel = function(source)
if source == nil or source == "" then
return "None"
end
return source
end
local lastSource = (window.Background and window.Background ~= "") and window.Background or nil
local function applyBackground(source)
task.spawn(function()
local ok, err = window:SetBackground(source)
if not ok and err ~= "replaced by a newer background" then
notify(false, "Background failed", tostring(err))
if background then
background:Set(false, true)
end
end
end)
end
-- One on/off switch; below it only the image id / link box.
background = host:Toggle({
Name = "Background",
Default = lastSource ~= nil,
Callback = function(value)
if value then
if lastSource then
applyBackground(lastSource)
else
notify(false, "No image", "Paste an image id or link below first")
background:Set(false, true)
end
else
if window.Background and window.Background ~= "" then
lastSource = window.Background
end
applyBackground("None")
end
end,
})
backgroundInput = actionInput(host, "image id or url", "Set", function(text)
text = trim(text)
if text == "" then
notify(false, "No image", "Paste an image id or link first")
return
end
lastSource = text
background:Set(true, true)
applyBackground(text)
end)
end
local nameInput = { Set = function() end }
-- Theme save / load / delete / set default were removed; only the colours stay.
function manager:Refresh()
refreshStatus()
end
function manager:Create(name)
name = trim(name)
if name == "" then
notify(false, "Name the theme", "Type a name before creating it")
return
end
if Library.ThemePresets[name] then
notify(false, "Name taken", name .. " is a built-in preset")
return
end
local ok, err = window:SaveTheme(name)
manager:Refresh()
if ok then
themes:Set(name, true)
nameInput:Set("")
refreshStatus()
end
notify(ok, ok and "Theme created" or "Create failed", ok and name or tostring(err))
end
function manager:Save(name)
name = name or pickedTheme()
if not name then
return
end
local ok, err = window:SaveTheme(name)
notify(ok, ok and "Theme saved" or "Save failed", ok and name or tostring(err))
end
function manager:Load(name)
name = name or pickedTheme()
if not name then
return
end
local ok, err = window:LoadTheme(name)
if ok then
presets:Set(nil, true)
end
notify(ok ~= false, ok and "Theme loaded" or "Load failed", ok and name or tostring(err))
end
function manager:Delete(name)
name = name or pickedTheme()
if not name then
return
end
window:Confirm({
Title = "Delete theme",
Content = "Remove " .. name .. "? This cannot be undone.",
Icon = "trash-2",
ConfirmText = "Delete",
Callback = function()
local ok, err = window:DeleteTheme(name)
manager:Refresh()
notify(ok, ok and "Theme deleted" or "Delete failed", ok and name or tostring(err))
end,
})
end
function manager:ToggleDefault(name)
name = name or themes:Get() or presets:Get()
if not name then
notify(false, "Nothing selected", "Pick a theme or a preset first")
return
end
local clearing = window:GetDefaultTheme() == name
local ok, err = window:SetDefaultTheme(not clearing and name or nil)
refreshStatus()
notify(ok ~= false, clearing and "Default cleared" or "Default theme set", ok ~= false and name or tostring(err))
end
if opts.Customize ~= false then
host:Divider({ Text = "Color" })
local fields = opts.Colors or {
{ "Accent", "Accent" },
{ "Background", "Background" },
{ "Surface2", "Surface" },
{ "Text", "Text" },
{ "Muted", "Muted text" },
}
for _, field in ipairs(fields) do
local key, title = field[1], field[2] or field[1]
local pending, scheduled = nil, false
local picker
picker = host:ColorPicker({
Name = title,
Default = Theme[key],
Callback = function(color)
-- Dragging the picker fires every frame; repaint at most
-- a dozen times a second.
pending = color
if scheduled then
return
end
scheduled = true
task.delay(0.08, function()
scheduled = false
Library:SetTheme({ [key] = pending }, true)
presets:Set(nil, true)
end)
end,
})
table.insert(Library._themeListeners, function()
if picker.Value ~= Theme[key] then
picker:Set(Theme[key], true)
end
end)
end
end
-- One hidden flag carries the palette and the look settings above, so
-- configs save and load the theme. Not a flag per toggle: flagged toggles
-- would count as running features on the minimized island.
if opts.Flag ~= false then
local flag = type(opts.Flag) == "string" and opts.Flag or "__Theme"
local look = { _type = "Theme", _searchName = "Theme", _searchKey = "theme", _listeners = {} }
function look:Get()
local colors = {}
for _, key in ipairs(THEME_KEYS) do
colors[key] = colorToHex(Theme[key])
end
return {
Name = Library.ThemeName,
Colors = colors,
Weather = weather and window.Weather or nil,
WeatherMode = weatherMode and window.WeatherMode or nil,
-- if-expressions, not and/or: a switched-off value is false, not nil.
Dim = if dim then window._backdropDim == true else nil,
Transparent = if transparent then window.Transparent == true else nil,
DragStyle = dragSkeleton and window.DragStyle or nil,
IslandStyle = islandStyle and window.IslandStyle or nil,
CornerRadius = cornerRadius and Library:GetCornerRadius() or nil,
UIScale = uiScale and window:GetUIScale() or nil,
AdaptiveSize = if adaptive then window:GetAdaptiveSize() else nil,
Density = density and Library.Density or nil,
-- "" is no background; a missing field leaves it alone on load.
Background = background and (window.Background or "") or nil,
BackgroundTransparency = background and window.BackgroundTransparency or nil,
}
end
function look:Set(value)
if type(value) ~= "table" then
return
end
if type(value.Colors) == "table" then
Library:SetTheme(value.Colors)
end
local name = type(value.Name) == "string" and value.Name or nil
if name and Library.ThemePresets[name] then
Library.ThemeName = name
presets:Set(name, true)
themes:Set(nil, true)
else
if name then
Library.ThemeName = name
end
presets:Set(nil, true)
themes:Set(name and table.find(window:ListThemes(), name) and name or nil, true)
end
if weather and type(value.Weather) == "string" then
window:SetWeather(value.Weather)
weather:Set(labels[window.Weather] or "None", true)
end
if weatherMode and type(value.WeatherMode) == "string" then
window:SetWeatherMode(value.WeatherMode)
weatherMode:Set(window.WeatherMode, true)
end
if dim and type(value.Dim) == "boolean" then
window:SetDim(value.Dim)
dim:Set(value.Dim, true)
end
if transparent and type(value.Transparent) == "boolean" then
window:SetTransparent(value.Transparent)
transparent:Set(value.Transparent, true)
end
if dragSkeleton and type(value.DragStyle) == "string" then
dragSkeleton:Set(window:SetDragStyle(value.DragStyle), true)
elseif dragSkeleton and type(value.DragSkeleton) == "boolean" then
-- Configs saved before drag styles.
window:SetDragSkeleton(value.DragSkeleton)
dragSkeleton:Set(window.DragStyle, true)
end
if islandStyle and type(value.IslandStyle) == "string" then
islandStyle:Set(window:SetIslandStyle(value.IslandStyle), true)
end
if cornerRadius and tonumber(value.CornerRadius) then
cornerRadius:Set(Library:SetCornerRadius(value.CornerRadius), true)
end
if adaptive and type(value.AdaptiveSize) == "boolean" then
window:SetAdaptiveSize(value.AdaptiveSize)
adaptive:Set(value.AdaptiveSize, true)
end
if uiScale and tonumber(value.UIScale) then
window:SetUIScale(tonumber(value.UIScale))
uiScale:Set(math.floor(window:GetUIScale() * 100 + 0.5), true)
end
if density and type(value.Density) == "string" then
density:Set(Library:SetDensity(value.Density), true)
end
if background and tonumber(value.BackgroundTransparency) then
window:SetBackgroundTransparency(tonumber(value.BackgroundTransparency))
end
if background and type(value.Background) == "string" and value.Background ~= (window.Background or "") then
local source = value.Background
background:Set(source ~= "", true)
-- A web image may download first; don't hold up the rest of the config.
task.spawn(window.SetBackground, window, source)
end
refreshStatus()
end
Library.Flags[flag] = look
window:_flagCreated(flag, look)
end
-- A reset wipes the themes folder, so drop the stale list.
window._workspaceListeners = window._workspaceListeners or {}
table.insert(window._workspaceListeners, function()
themes:Set(nil, true)
manager:Refresh()
end)
refreshStatus()
return manager
end
Tab.CreateButtonRow = Tab.ButtonRow
Tab.CreateConfigManager = Tab.ConfigManager
Tab.CreateThemeManager = Tab.ThemeManager
-- Status: read-only key/value lines, mostly for groupboxes, in several styles.
local STATUS_HEIGHT = { Plain = 22, Row = 24, Badge = 28, Dot = 24, Bar = 38, Stat = 46 }
-- Tone: a theme key, a Color3 or "#rrggbb".
local function toneColor(tone)
return Library._color(tone) or Theme.Accent
end
local function formatNumber(value)
if value == math.floor(value) then
return tostring(math.floor(value))
end
return string.format("%.1f", value)
end
local function parseFraction(value, max)
if type(value) == "number" and type(max) == "number" then
return value, max
end
if type(value) == "string" then
local cleaned = (value:gsub(",", ""))
local current, total = cleaned:match("^%s*(%-?[%d%.]+)%s*/%s*(%-?[%d%.]+)")
if current then
return tonumber(current), tonumber(total)
end
end
return nil, nil
end
function Tab:Status(opts)
opts = normalize(opts, { Title = "Name", Text = "Value", Content = "Value", CurrentValue = "Value", Default = "Value" })
local style = STATUS_HEIGHT[opts.Style] and opts.Style or "Plain"
local compact = metricsOf(self).Compact
local window = self.Window
local height = STATUS_HEIGHT[style]
local pad = compact and 0 or 8
local frame = card(self, "Frame", height + pad * 2, opts)
local holder = create("Frame", {
Position = UDim2.fromOffset(14, pad),
Size = UDim2.new(1, -28, 0, height),
BackgroundTransparency = 1,
Parent = frame,
})
local handle = { Value = opts.Value, Max = opts.Max, Tone = opts.Tone or "Accent", Name = opts.Name or "", Color = opts.Color or opts.ValueColor }
local keyLabel, valueLabel, chip, chipStroke, dot, pulse, fill
local tinted = false
local function keyText()
return style == "Plain" and (handle.Name .. ":") or handle.Name
end
if style == "Plain" then
create("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 5),
Parent = holder,
})
keyLabel = label({
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
TextSize = 12,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextTruncate = Enum.TextTruncate.None,
LayoutOrder = 1,
Parent = holder,
})
valueLabel = label({
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
TextSize = 12,
TextColor3 = Theme.Text,
TextTruncate = Enum.TextTruncate.None,
LayoutOrder = 2,
Parent = holder,
})
elseif style == "Stat" then
keyLabel = label({
Position = UDim2.fromOffset(0, 3),
Size = UDim2.new(1, 0, 0, 14),
TextSize = 11,
TextColor3 = Theme.Muted,
Parent = holder,
})
valueLabel = label({
Position = UDim2.fromOffset(0, 18),
Size = UDim2.new(1, 0, 0, 24),
TextSize = 20,
FontFace = Fonts.Bold,
TextColor3 = Theme.Text,
Parent = holder,
})
else
local keyX = 0
if style == "Dot" then
keyX = 18
pulse = create("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0, 5, 0.5, 0),
Size = UDim2.fromOffset(8, 8),
BackgroundColor3 = toneColor(handle.Tone),
BackgroundTransparency = 0.4,
BorderSizePixel = 0,
Parent = holder,
})
corner(pulse, UDim.new(1, 0))
dot = create("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0, 5, 0.5, 0),
Size = UDim2.fromOffset(8, 8),
BackgroundColor3 = toneColor(handle.Tone),
BorderSizePixel = 0,
Parent = holder,
})
corner(dot, UDim.new(1, 0))
if opts.Pulse ~= false then
TweenService:Create(pulse, TweenInfo.new(1.4, Enum.EasingStyle.Quad, Enum.EasingDirection.Out, -1), {
Size = UDim2.fromOffset(20, 20),
BackgroundTransparency = 1,
}):Play()
else
pulse.Visible = false
end
end
local rowHeight = style == "Bar" and 18 or height
keyLabel = label({
Position = UDim2.fromOffset(keyX, 0),
Size = UDim2.new(0.55, -keyX, 0, rowHeight),
TextSize = 12,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
Parent = holder,
})
if style == "Badge" then
tinted = true
chip = create("Frame", {
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, 0, 0.5, 0),
Size = UDim2.fromOffset(0, 22),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = toneColor(handle.Tone),
BackgroundTransparency = 0.86,
BorderSizePixel = 0,
Parent = holder,
})
corner(chip, UDim.new(1, 0))
chipStroke = stroke(chip, toneColor(handle.Tone), 0.6)
padding(chip, 10, 10)
valueLabel = label({
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
TextSize = 12,
TextColor3 = toneColor(handle.Tone),
TextTruncate = Enum.TextTruncate.None,
Parent = chip,
})
else
valueLabel = label({
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, 0, 0, 0),
Size = UDim2.new(0.45, 0, 0, rowHeight),
TextSize = 12,
TextColor3 = Theme.Text,
TextXAlignment = Enum.TextXAlignment.Right,
Parent = holder,
})
end
if style == "Bar" then
local track = create("Frame", {
Position = UDim2.fromOffset(0, 25),
Size = UDim2.new(1, 0, 0, 5),
BackgroundColor3 = Theme.Surface3,
BorderSizePixel = 0,
ClipsDescendants = true,
Parent = holder,
})
corner(track, UDim.new(1, 0))
fill = create("Frame", {
Size = UDim2.new(0, 0, 1, 0),
BackgroundColor3 = toneColor(handle.Tone),
BorderSizePixel = 0,
Parent = track,
})
corner(fill, UDim.new(1, 0))
end
end
local lastText = nil
local function render(animate)
keyLabel.Text = Library._rich(keyText())
local value = handle.Value
local text = value == nil and (opts.Placeholder or "-") or tostring(value)
if style == "Bar" then
local current, total = parseFraction(value, handle.Max)
if current and total then
handle.Max = total
text = formatNumber(current) .. " / " .. formatNumber(total)
local fraction = total > 0 and math.clamp(current / total, 0, 1) or 0
tween(fill, { Size = UDim2.new(fraction, 0, 1, 0) }, animate and 0.4 or 0, Enum.EasingStyle.Quint)
elseif type(value) == "number" and value <= 1 then
text = string.format("%d%%", math.floor(value * 100 + 0.5))
tween(fill, { Size = UDim2.new(math.clamp(value, 0, 1), 0, 1, 0) }, animate and 0.4 or 0, Enum.EasingStyle.Quint)
end
end
text = Library._rich((opts.Prefix or "") .. text .. (opts.Suffix or ""))
local changed = lastText ~= nil and text ~= lastText
lastText = text
valueLabel.Text = text
local tone = toneColor(handle.Tone)
local duration = animate and 0.25 or 0
-- Color paints the whole value; inline tags still win over it.
local valueColor = not tinted and Library._color(handle.Color) or Theme.Text
if not tinted and not (changed and animate and opts.Flash ~= false) then
tween(valueLabel, { TextColor3 = valueColor }, duration)
end
if chip then
tween(chip, { BackgroundColor3 = tone }, duration)
tween(chipStroke, { Color = tone }, duration)
tween(valueLabel, { TextColor3 = tone }, duration)
end
if dot then
tween(dot, { BackgroundColor3 = tone }, duration)
tween(pulse, { BackgroundColor3 = tone }, 0)
end
if fill then
tween(fill, { BackgroundColor3 = tone }, duration)
end
-- A brief accent flash marks a changed value.
if changed and animate and not tinted and opts.Flash ~= false then
tween(valueLabel, { TextColor3 = Theme.Accent }, 0.08)
task.delay(0.1, function()
if valueLabel.Parent then
tween(valueLabel, { TextColor3 = Library._color(handle.Color) or Theme.Text }, 0.45)
end
end)
end
end
keyLabel.RichText = true
valueLabel.RichText = true
-- Theme tags in the text are baked to colours, so repaint on theme change.
onTheme(holder, function()
if lastText ~= nil then
render(false)
end
end)
function handle:Set(value, max)
handle.Value = value
if max ~= nil then
handle.Max = max
end
render(true)
end
function handle:Get()
return handle.Value
end
function handle:SetTone(tone)
handle.Tone = tone
render(true)
end
-- nil goes back to the normal text colour.
function handle:SetColor(color)
handle.Color = color
render(true)
end
function handle:SetName(name)
handle.Name = tostring(name)
render(false)
end
function handle:Text()
return valueLabel.Text
end
-- The value's resting colour, for the copy pinned on the minimized island.
function handle._shownColor()
if handle.Color ~= nil then
return Library._color(handle.Color) or Theme.Text
end
if style == "Badge" or style == "Dot" or style == "Bar" then
return toneColor(handle.Tone)
end
return Theme.Text
end
handle._listeners = {}
if type(opts.Update) == "function" then
local rate = math.max(tonumber(opts.UpdateRate) or 1, 0.05)
local running = true
table.insert(handle._listeners, function()
running = false
end)
task.spawn(function()
while running and frame.Parent do
local ok, value, extra = isolatedCall(opts.Update)
if ok then
if value ~= nil then
handle.Value = value
end
if type(extra) == "number" then
handle.Max = extra
elseif type(extra) == "string" or typeof(extra) == "Color3" then
handle.Tone = extra
end
render(true)
else
warn("[AirFlow] status update error: " .. tostring(value))
end
task.wait(rate)
end
end)
function handle:SetUpdateRate(seconds)
rate = math.max(tonumber(seconds) or rate, 0.05)
end
end
if opts.Pin then
table.insert(window._pinned, handle)
table.insert(handle._listeners, function()
local index = table.find(window._pinned, handle)
if index then
table.remove(window._pinned, index)
end
end)
end
render(false)
return finishElement(self, opts, handle, frame, "Status")
end
function Tab:Statuses(entries, shared)
local handles = {}
for index, entry in ipairs(entries or {}) do
local entryOpts = type(entry) == "table" and table.clone(entry) or { Name = tostring(entry) }
for key, value in pairs(shared or {}) do
if entryOpts[key] == nil then
entryOpts[key] = value
end
end
local handle = self:Status(entryOpts)
handles[entryOpts.Name or entryOpts.Title or index] = handle
end
return handles
end
-- Status list: a read-only block of rows that grows up to MaxRows, then
-- scrolls inside itself. Rows are reused between refreshes so nothing flickers.
function Tab:StatusList(opts)
opts = normalize(opts, { Title = "Name" })
local compact = metricsOf(self).Compact
local window = self.Window
local maxRows = math.max(math.floor(tonumber(opts.MaxRows) or 5), 1)
local frame = card(self, "Frame", 0, opts)
frame.AutomaticSize = Enum.AutomaticSize.Y
if compact then
padding(frame, 14, 14, 6, 8)
else
padding(frame, 14, 14, 11, 12)
end
create("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 4),
Parent = frame,
})
local heading = label({
Size = UDim2.new(1, 0, 0, 16),
Text = string.upper(tostring(opts.Name or "")),
TextSize = 11,
TextColor3 = Theme.Muted,
Visible = tostring(opts.Name or "") ~= "",
LayoutOrder = 1,
Parent = frame,
})
local scroller = create("ScrollingFrame", {
Size = UDim2.new(1, 0, 0, 0),
BackgroundTransparency = 1,
BorderSizePixel = 0,
ScrollBarThickness = 2,
ScrollBarImageColor3 = Theme.Accent,
ScrollBarImageTransparency = 0.4,
ScrollingDirection = Enum.ScrollingDirection.Y,
AutomaticCanvasSize = Enum.AutomaticSize.Y,
CanvasSize = UDim2.new(),
ScrollingEnabled = false,
LayoutOrder = 2,
Parent = frame,
})
scroller:SetAttribute("NoDrag", true)
local content = create("Frame", {
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
Parent = scroller,
})
local contentPadding = padding(content, 0, 0, 0, 0)
create("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 2),
Parent = content,
})
local empty = label({
Size = UDim2.new(1, 0, 0, 24),
AutomaticSize = Enum.AutomaticSize.Y,
Text = tostring(opts.EmptyText or "Nothing yet"),
TextSize = 13,
LineHeight = 1.1,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextWrapped = true,
TextTruncate = Enum.TextTruncate.None,
LayoutOrder = 0,
Parent = content,
})
local rowObjects, rows, signature, raw = {}, {}, nil, {}
local handle = { _listeners = {} }
-- Fits the list to its first MaxRows rows, measured, so wrapped rows count
-- at their real height. Past MaxRows the list scrolls.
local function fit()
if handle._destroyed then
return
end
local scale = window.Scale.Scale
local count = #rows
local height = 0
if count == 0 then
height = empty.AbsoluteSize.Y / scale
else
local shown = math.min(count, maxRows)
for index = 1, shown do
height += rowObjects[index].Frame.AbsoluteSize.Y / scale
end
height += (shown - 1) * 2
end
height = math.ceil(height)
local scrolling = count > maxRows
scroller.ScrollingEnabled = scrolling
contentPadding.PaddingRight = UDim.new(0, scrolling and 8 or 0)
if scroller.Size.Y.Offset ~= height then
scroller.Size = UDim2.new(1, 0, 0, height)
end
local maxScroll = math.max(scroller.AbsoluteCanvasSize.Y - scroller.AbsoluteWindowSize.Y, 0) / scale
if scroller.CanvasPosition.Y > maxScroll then
scroller.CanvasPosition = Vector2.new(0, maxScroll)
end
end
local function fitText(row)
if row.Value.Visible then
row.Text.Size = UDim2.new(1, -(math.ceil(row.Value.AbsoluteSize.X / window.Scale.Scale) + 12), 0, 24)
else
row.Text.Size = UDim2.new(1, 0, 0, 24)
end
end
local function newRow(order)
local holder = create("Frame", {
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
LayoutOrder = order,
Parent = content,
})
local row = {
Frame = holder,
Text = label({
Size = UDim2.new(1, 0, 0, 24),
AutomaticSize = Enum.AutomaticSize.Y,
Text = "",
TextSize = 13,
LineHeight = 1.1,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextWrapped = true,
TextTruncate = Enum.TextTruncate.None,
RichText = true,
Parent = holder,
}),
Value = label({
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, 0, 0, 0),
Size = UDim2.new(0, 0, 0, 24),
AutomaticSize = Enum.AutomaticSize.X,
Text = "",
TextSize = 12,
FontFace = Fonts.Medium,
TextColor3 = Theme.Text,
RichText = true,
TextXAlignment = Enum.TextXAlignment.Right,
TextTruncate = Enum.TextTruncate.None,
Visible = false,
Parent = holder,
}),
}
row.Connections = {
holder:GetPropertyChangedSignal("AbsoluteSize"):Connect(fit),
row.Value:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
fitText(row)
end),
}
return row
end
local function normalizeRow(entry)
if type(entry) == "table" then
local text = entry.Text or entry.Name or entry[1]
local tone = entry.Tone or entry.Color
return {
Text = tostring(text == nil and "" or text),
Value = entry.Value ~= nil and tostring(entry.Value) or nil,
Tone = (type(tone) == "string" or typeof(tone) == "Color3") and tone or nil,
}
end
return { Text = tostring(entry) }
end
local function rowKey(entry)
return entry.Text .. "\0" .. (entry.Value or "\1") .. "\0" .. tostring(entry.Tone or "")
end
-- Tone tints the value, or the text when there is no value.
local function paint(row, entry)
local key = rowKey(entry)
if row.Key == key then
return
end
row.Key = key
row.Text.Text = Library._rich(entry.Text)
local tone = entry.Tone and Library._color(entry.Tone) or nil
if entry.Value then
row.Value.Text = Library._rich(entry.Value)
row.Value.Visible = true
tween(row.Value, { TextColor3 = tone or Theme.Text }, 0)
tween(row.Text, { TextColor3 = Theme.Muted }, 0)
else
row.Value.Visible = false
tween(row.Text, { TextColor3 = tone or Theme.Muted }, 0)
end
fitText(row)
end
local function dropRow(row)
for _, connection in ipairs(row.Connections) do
connection:Disconnect()
end
row.Frame:Destroy()
end
function handle:Set(list)
if type(list) ~= "table" or handle._destroyed then
return
end
local nextRows, keys = {}, {}
raw = {}
for index, entry in ipairs(list) do
raw[index] = type(entry) == "table" and table.clone(entry) or entry
local row = normalizeRow(entry)
nextRows[index] = row
keys[index] = rowKey(row)
end
local nextSignature = table.concat(keys, "\2") .. "#" .. #nextRows
if nextSignature == signature then
return
end
signature = nextSignature
rows = nextRows
for index, entry in ipairs(rows) do
local row = rowObjects[index]
if not row then
row = newRow(index)
rowObjects[index] = row
end
paint(row, entry)
end
for index = #rowObjects, #rows + 1, -1 do
dropRow(rowObjects[index])
rowObjects[index] = nil
end
empty.Visible = #rows == 0
fit()
end
function handle:Get()
local copy = {}
for index, entry in ipairs(raw) do
copy[index] = type(entry) == "table" and table.clone(entry) or entry
end
return copy
end
function handle:Clear()
handle:Set({})
end
function handle:SetName(text)
text = tostring(text or "")
heading.Text = string.upper(text)
heading.Visible = text ~= ""
handle._searchName = text
handle._searchKey = string.lower(text)
end
local rate = math.max(tonumber(opts.UpdateRate) or 1, 0.05)
function handle:SetUpdateRate(seconds)
rate = math.max(tonumber(seconds) or rate, 0.05)
end
-- Theme tags are baked into the text, so a theme change repaints the rows.
onTheme(content, function()
for index, row in ipairs(rowObjects) do
if rows[index] then
row.Key = nil
paint(row, rows[index])
end
end
end)
local emptyConnection = empty:GetPropertyChangedSignal("AbsoluteSize"):Connect(fit)
table.insert(handle._listeners, function()
handle._destroyed = true
emptyConnection:Disconnect()
for _, row in ipairs(rowObjects) do
for _, connection in ipairs(row.Connections) do
connection:Disconnect()
end
end
end)
-- Refreshes only while someone can see it: window open and not minimized,
-- its tab and sub tab showing, and neither it nor its groupbox hidden.
local function onScreen()
if window._destroyed or not window.Open or window.Minimized or not handle._userVisible then
return false
end
local page = self
if page._groupbox then
if page._userVisible == false then
return false
end
page = page.Parent
end
if page._isSubTab then
if page.Parent.CurrentSubTab ~= page then
return false
end
page = page.Parent
end
return window.CurrentTab == page
end
if type(opts.Update) == "function" then
local alive = true
table.insert(handle._listeners, function()
alive = false
end)
task.defer(function()
local first = true
while alive and frame.Parent do
if first or onScreen() then
first = false
local ok, result = isolatedCall(opts.Update)
if ok then
if result ~= nil then
handle:Set(result)
end
else
warn("[AirFlow] status list update error: " .. tostring(result))
end
end
task.wait(rate)
end
end)
end
handle:Set(opts.Rows or {})
return finishElement(self, opts, handle, frame, "StatusList")
end
Tab.CreateStatus = Tab.Status
Tab.AddStatus = Tab.Status
Tab.CreateStatuses = Tab.Statuses
Tab.CreateStatusList = Tab.StatusList
Tab.AddStatusList = Tab.StatusList
-- Image and Viewport: media cards. Image shows a picture from the same sources
-- as window backgrounds, plus "Avatar" for the player's headshot. Viewport
-- shows a 3D model or the player's avatar, turning slowly; drag it to spin it
-- by hand.
function Tab:_mediaCard(opts, defaultHeight)
opts = normalize(opts, { Title = "Name", Description = "Desc" })
local metrics = metricsOf(self)
local compact = metrics.Compact
local mediaHeight = math.clamp(tonumber(opts.Height) or defaultHeight, 40, 600)
local inset = compact and 14 or 8
local hasHead = type(opts.Name) == "string" and opts.Name ~= ""
local titleTop = compact and 4 or 10
local mediaTop = hasHead and (titleTop + (opts.Desc and 38 or 22)) or (compact and 4 or inset)
local height = mediaTop + mediaHeight + (compact and 6 or inset)
local frame, frameStroke = card(self, "Frame", height, opts)
hoverStroke(frame, frameStroke)
local title, descLabel
if hasHead then
title = label({
Position = UDim2.fromOffset(14, titleTop),
Size = UDim2.new(1, -28, 0, metrics.TitleHeight),
Text = opts.Name,
TextSize = metrics.TextSize,
Parent = frame,
})
if opts.Desc then
descLabel = label({
Position = UDim2.fromOffset(14, titleTop + metrics.TitleHeight + 1),
Size = UDim2.new(1, -28, 0, metrics.DescLine),
Text = opts.Desc,
TextSize = metrics.DescSize,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
Parent = frame,
})
end
end
local media = create("Frame", {
Position = UDim2.fromOffset(inset, mediaTop),
Size = UDim2.new(1, -inset * 2, 0, mediaHeight),
BackgroundColor3 = Theme.Surface3,
BackgroundTransparency = 0.5,
BorderSizePixel = 0,
ClipsDescendants = true,
Parent = frame,
})
corner(media, UDim.new(0, 6))
stroke(media, Theme.Stroke)
local placeholder = create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromOffset(22, 22),
BackgroundTransparency = 1,
ImageColor3 = Theme.Muted,
ImageTransparency = 0.3,
ScaleType = Enum.ScaleType.Fit,
Parent = media,
})
-- A caption over the bottom of the picture, on a dark fade.
local captionHolder = create("Frame", {
AnchorPoint = Vector2.new(0, 1),
Position = UDim2.fromScale(0, 1),
Size = UDim2.new(1, 0, 0, 30),
BackgroundColor3 = Color3.new(0, 0, 0),
BackgroundTransparency = 0.35,
BorderSizePixel = 0,
Visible = false,
ZIndex = 3,
Parent = media,
})
create("UIGradient", {
Rotation = 90,
Transparency = NumberSequence.new(1, 0),
Parent = captionHolder,
})
local caption = label({
Position = UDim2.fromOffset(10, 8),
Size = UDim2.new(1, -20, 1, -10),
TextSize = 12,
TextColor3 = Color3.new(1, 1, 1),
ZIndex = 4,
Parent = captionHolder,
})
local function setCaption(text)
text = text ~= nil and tostring(text) or ""
caption.Text = text
captionHolder.Visible = text ~= ""
end
setCaption(opts.Caption)
return opts, {
Frame = frame,
Stroke = frameStroke,
Media = media,
Placeholder = placeholder,
Title = title,
Desc = descLabel,
SetCaption = setCaption,
}
end
function Tab:Image(opts)
local parts
opts, parts = self:_mediaCard(opts, 160)
local window = self.Window
local media = parts.Media
applyIcon(parts.Placeholder, "image")
-- Enum lookups throw on unknown names.
local function scaleType(name, fallback)
local ok, value = pcall(function()
return Enum.ScaleType[tostring(name)]
end)
return ok and value or fallback
end
local image = create("ImageLabel", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
ImageTransparency = 1,
ScaleType = scaleType(opts.ScaleType or "Crop", Enum.ScaleType.Crop),
ZIndex = 2,
Parent = media,
})
corner(image, UDim.new(0, 6))
if opts.Color then
image.ImageColor3 = opts.Color
end
local handle = { Value = nil }
local generation = 0
-- Yields while a web image downloads. Returns ok, err like SetBackground.
function handle:SetImage(source)
generation += 1
local current = generation
handle.Value = source
if source == nil or source == "" or source == "None" then
tween(image, { ImageTransparency = 1 }, 0.2)
tween(parts.Placeholder, { ImageTransparency = 0.3 }, 0.2)
return true
end
local asset, err
if type(source) == "number" then
asset = "rbxassetid://" .. math.floor(source)
elseif type(source) == "string" and ({ avatar = true, headshot = true })[source:lower()] then
asset = "rbxthumb://type=AvatarHeadShot&id=" .. LocalPlayer.UserId .. "&w=420&h=420"
else
local ok, result, message = pcall(window._resolveBackground, window, tostring(source))
if ok then
asset, err = result, message
else
err = tostring(result)
end
end
if generation ~= current then
return false, "replaced by a newer image"
end
if not asset then
warn("[AirFlow] image: " .. tostring(err))
return false, err
end
image.ImageTransparency = 1
image.Image = asset
-- Fade in once it has loaded, or after a few seconds regardless.
task.spawn(function()
local waited = 0
while not image.IsLoaded and waited < 4 and generation == current and image.Parent do
waited += task.wait(0.05)
end
if generation == current and image.Parent then
tween(image, { ImageTransparency = 0 }, 0.35)
tween(parts.Placeholder, { ImageTransparency = 1 }, 0.2)
end
end)
return true
end
handle.Set = handle.SetImage
function handle:Get()
return handle.Value
end
function handle:SetCaption(text)
parts.SetCaption(text)
end
function handle:SetScaleType(name)
image.ScaleType = scaleType(name, image.ScaleType)
end
function handle:SetColor(color)
image.ImageColor3 = typeof(color) == "Color3" and color or Color3.new(1, 1, 1)
end
function handle:SetTitle(text)
if parts.Title then
parts.Title.Text = tostring(text)
end
end
if opts.Image ~= nil then
task.spawn(handle.SetImage, handle, opts.Image)
end
return finishElement(self, opts, handle, parts.Frame, "Image")
end
function Tab:Viewport(opts)
local parts
opts, parts = self:_mediaCard(opts, 180)
local window = self.Window
local media = parts.Media
applyIcon(parts.Placeholder, "box")
local viewport = create("ViewportFrame", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
ImageTransparency = 1,
Ambient = opts.Ambient or Color3.fromRGB(165, 160, 175),
LightColor = opts.LightColor or Color3.fromRGB(255, 250, 245),
LightDirection = Vector3.new(-0.6, -1, -0.8),
ZIndex = 2,
Parent = media,
})
local camera = Instance.new("Camera")
camera.FieldOfView = math.clamp(tonumber(opts.FieldOfView) or 35, 10, 90)
camera.Parent = viewport
viewport.CurrentCamera = camera
local world = Instance.new("WorldModel")
world.Parent = viewport
local handle = {
Model = nil,
Spin = opts.Spin ~= false,
SpinSpeed = tonumber(opts.SpinSpeed) or 25,
}
local angle = math.rad(tonumber(opts.Angle) or 0)
local pitch = math.rad(tonumber(opts.Pitch) or 10)
local distanceScale = tonumber(opts.Distance) or 1
local center, distance, basis = nil, 10, CFrame.new()
local source = nil
local generation = 0
local dragging, lastX, resumeAt = false, 0, 0
local function place()
if not center then
return
end
local direction = basis:VectorToWorldSpace(Vector3.new(
math.sin(angle) * math.cos(pitch),
math.sin(pitch),
-math.cos(angle) * math.cos(pitch)
))
camera.CFrame = CFrame.lookAt(center + direction * distance, center)
end
local function fit(model)
local cf, size
if model:IsA("Model") then
cf, size = model:GetBoundingBox()
basis = model:GetPivot().Rotation
elseif model:IsA("BasePart") then
cf, size = model.CFrame, model.Size
basis = model.CFrame.Rotation
else
return false
end
center = cf.Position
local radius = math.max(size.Magnitude / 2, 0.5)
distance = radius / math.tan(math.rad(camera.FieldOfView / 2)) * distanceScale
place()
return true
end
-- A copy with its scripts and sounds stripped and every part anchored.
local function prepare(instance)
local copy = instance
if opts.Clone ~= false then
local archivable = instance.Archivable
pcall(function()
instance.Archivable = true
end)
local ok, result = pcall(instance.Clone, instance)
pcall(function()
instance.Archivable = archivable
end)
copy = ok and result or nil
end
if not copy then
return nil
end
local function clean(object)
if object:IsA("LuaSourceContainer") or object:IsA("Sound") or object:IsA("ForceField") then
object:Destroy()
elseif object:IsA("BasePart") then
object.Anchored = true
object.CanCollide = false
end
end
for _, object in ipairs(copy:GetDescendants()) do
pcall(clean, object)
end
if copy:IsA("BasePart") then
copy.Anchored = true
end
local humanoid = copy:FindFirstChildOfClass("Humanoid")
if humanoid then
humanoid.DisplayDistanceType = Enum.HumanoidDisplayDistanceType.None
end
return copy
end
local function resolve(value)
if type(value) == "function" then
local ok, result = pcall(value)
value = ok and result or nil
end
if type(value) == "string" and ({ avatar = true, player = true, character = true })[value:lower()] then
value = LocalPlayer
end
if typeof(value) == "Instance" and value:IsA("Player") then
local character = value.Character
if not character then
character = value.CharacterAdded:Wait()
-- Let the body parts and accessories load in.
task.wait(0.5)
end
value = character
end
return typeof(value) == "Instance" and value or nil
end
-- Yields while waiting for a character to spawn.
function handle:SetModel(value)
generation += 1
local current = generation
source = value
if handle.Model then
handle.Model:Destroy()
handle.Model = nil
end
center = nil
if value == nil then
tween(viewport, { ImageTransparency = 1 }, 0.2)
tween(parts.Placeholder, { ImageTransparency = 0.3 }, 0.2)
return true
end
local instance = resolve(value)
if generation ~= current then
return false, "replaced by a newer model"
end
if not instance then
return false, "no model to show"
end
local copy = prepare(instance)
if not copy then
return false, "could not copy the model"
end
copy.Parent = world
handle.Model = copy
if not fit(copy) then
copy:Destroy()
handle.Model = nil
return false, "not a Model or a part"
end
viewport.ImageTransparency = 1
tween(viewport, { ImageTransparency = 0 }, 0.35)
tween(parts.Placeholder, { ImageTransparency = 1 }, 0.2)
return true
end
handle.Set = handle.SetModel
function handle:Get()
return handle.Model
end
-- Copies the source again, e.g. after the character respawned or changed.
function handle:Refresh()
return handle:SetModel(source)
end
function handle:SetSpin(enabled)
handle.Spin = enabled ~= false
end
function handle:SetSpinSpeed(degrees)
handle.SpinSpeed = tonumber(degrees) or handle.SpinSpeed
end
function handle:SetAngle(degrees)
angle = math.rad(tonumber(degrees) or 0)
place()
end
function handle:SetCaption(text)
parts.SetCaption(text)
end
function handle:SetTitle(text)
if parts.Title then
parts.Title.Text = tostring(text)
end
end
media.InputBegan:Connect(function(input)
if isPress(input) and handle.Model then
dragging = true
lastX = pointerPosition().X
end
end)
window:_listen("Changed", function(input)
if dragging and isMove(input) then
local x = pointerPosition().X
angle -= (x - lastX) * 0.012
lastX = x
place()
end
end, handle)
window:_listen("Ended", function(input)
if dragging and isPress(input) then
dragging = false
resumeAt = os.clock() + 1.5
end
end, handle)
window:_listen("Render", function(deltaTime)
if handle.Spin and center and not dragging and os.clock() >= resumeAt and window.Open and not window.Minimized
and parts.Frame.Visible then
angle += math.rad(handle.SpinSpeed) * deltaTime
place()
end
end, handle)
if opts.Model ~= nil then
task.spawn(handle.SetModel, handle, opts.Model)
end
return finishElement(self, opts, handle, parts.Frame, "Viewport")
end
-- Table: rows under a header, sortable by clicking a column. Rank = true adds
-- a # column with medals for the top three, for leaderboards.
function Tab:Table(opts)
opts = normalize(opts, { Title = "Name", Description = "Desc", Data = "Rows", OnRowClick = "Callback" })
local window = self.Window
local metrics = metricsOf(self)
local compact = metrics.Compact
local rowHeight = tonumber(opts.RowHeight) or (TOUCH and 32 or 26)
local maxRows = math.max(math.floor(tonumber(opts.MaxRows) or 8), 1)
local headerHeight = TOUCH and 28 or 24
local textSize = compact and 12 or 13
local inset = compact and 14 or 8
local hasHead = type(opts.Name) == "string" and opts.Name ~= ""
local titleTop = compact and 4 or 10
local searchable = opts.Search ~= false
local searchHeight = TOUCH and 30 or 26
local searchTop = hasHead and (titleTop + (opts.Desc and 38 or 22)) or (compact and 4 or inset)
local gridTop = searchTop + (searchable and searchHeight + 6 or 0)
local bottom = compact and 6 or inset
local frame, frameStroke = card(self, "Frame", gridTop + headerHeight + rowHeight + bottom, opts)
hoverStroke(frame, frameStroke)
local title
if hasHead then
title = label({
Position = UDim2.fromOffset(14, titleTop),
Size = UDim2.new(1, -28, 0, metrics.TitleHeight),
Text = opts.Name,
TextSize = metrics.TextSize,
Parent = frame,
})
if opts.Desc then
label({
Position = UDim2.fromOffset(14, titleTop + metrics.TitleHeight + 1),
Size = UDim2.new(1, -28, 0, metrics.DescLine),
Text = opts.Desc,
TextSize = metrics.DescSize,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
Parent = frame,
})
end
end
-- Search: filters rows by any cell's text as you type. A count of what
-- matches shows on the right while it filters.
local query = ""
local searchBox, searchCount
if searchable then
local holder = create("Frame", {
Position = UDim2.fromOffset(inset, searchTop),
Size = UDim2.new(1, -inset * 2, 0, searchHeight),
BackgroundColor3 = Theme.Surface,
BorderSizePixel = 0,
Parent = frame,
})
holder:SetAttribute("NoDrag", true)
corner(holder, UDim.new(0, 6))
local searchStroke = stroke(holder)
local iconHolder, searchIcon = glowIcon(holder, "search", Theme.Muted, UDim2.new(0, 9, 0.5, 0))
iconHolder.Size = UDim2.fromOffset(13, 13)
searchCount = label({
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, -9, 0, 0),
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
Text = "",
TextSize = 11,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextTruncate = Enum.TextTruncate.None,
Visible = false,
Parent = holder,
})
searchBox = create("TextBox", {
Position = UDim2.fromOffset(29, 0),
Size = UDim2.new(1, -90, 1, 0),
BackgroundTransparency = 1,
Text = "",
PlaceholderText = opts.SearchPlaceholder or "Search",
PlaceholderColor3 = Theme.Muted,
TextColor3 = Theme.Text,
TextSize = 12,
FontFace = Fonts.Regular,
TextXAlignment = Enum.TextXAlignment.Left,
TextTruncate = Enum.TextTruncate.AtEnd,
ClearTextOnFocus = false,
Parent = holder,
})
searchBox.Focused:Connect(function()
tween(searchStroke, { Color = Theme.StrokeHover }, 0.15)
tween(searchIcon, { ImageColor3 = Theme.Accent }, 0.15)
end)
searchBox.FocusLost:Connect(function()
tween(searchStroke, { Color = Theme.Stroke }, 0.2)
tween(searchIcon, { ImageColor3 = Theme.Muted }, 0.2)
end)
end
local grid = create("Frame", {
Position = UDim2.fromOffset(inset, gridTop),
Size = UDim2.new(1, -inset * 2, 0, headerHeight + rowHeight),
BackgroundColor3 = Theme.Surface,
BackgroundTransparency = 0.2,
BorderSizePixel = 0,
ClipsDescendants = true,
Parent = frame,
})
corner(grid, UDim.new(0, 6))
stroke(grid, Theme.Stroke)
local header = create("Frame", {
Size = UDim2.new(1, 0, 0, headerHeight),
BackgroundColor3 = Theme.Surface3,
BackgroundTransparency = 0.45,
BorderSizePixel = 0,
Parent = grid,
})
create("Frame", {
AnchorPoint = Vector2.new(0, 1),
Position = UDim2.fromScale(0, 1),
Size = UDim2.new(1, 0, 0, 1),
BackgroundColor3 = Theme.Stroke,
BorderSizePixel = 0,
Parent = header,
})
local body = create("ScrollingFrame", {
Position = UDim2.fromOffset(0, headerHeight),
Size = UDim2.new(1, 0, 1, -headerHeight),
BackgroundTransparency = 1,
BorderSizePixel = 0,
ScrollBarThickness = 2,
ScrollBarImageColor3 = Theme.Accent,
ScrollBarImageTransparency = 0.4,
ScrollingDirection = Enum.ScrollingDirection.Y,
AutomaticCanvasSize = Enum.AutomaticSize.Y,
CanvasSize = UDim2.new(),
Parent = grid,
})
create("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Parent = body,
})
local empty = label({
Position = UDim2.fromOffset(0, headerHeight),
Size = UDim2.new(1, 0, 0, rowHeight),
Text = opts.EmptyText or "Nothing here yet",
TextSize = 12,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextXAlignment = Enum.TextXAlignment.Center,
Parent = grid,
})
local handle = { Rows = {} }
local shownCount = nil
local columns, headerCells = {}, {}
local pool = {}
local sortKey, sortDescending = nil, false
local shownHeight = nil
local MEDALS = {
Color3.fromRGB(245, 200, 90),
Color3.fromRGB(200, 206, 218),
Color3.fromRGB(214, 146, 96),
}
local function alignment(column)
local align = type(column.Align) == "string" and column.Align:lower() or nil
if align == "right" then
return Enum.TextXAlignment.Right
elseif align == "center" or align == "centre" then
return Enum.TextXAlignment.Center
end
return Enum.TextXAlignment.Left
end
-- Widths: a number above 1 is pixels, up to 1 is a share of what the pixel
-- columns leave, and the rest split whatever share is left.
local function layoutColumns()
local pixels, shares, autos = 0, 0, 0
for _, column in ipairs(columns) do
local width = tonumber(column.Width)
if width and width > 1 then
pixels += width
elseif width and width > 0 then
shares += width
else
autos += 1
end
end
local left = math.max(1 - shares, 0)
for _, column in ipairs(columns) do
local width = tonumber(column.Width)
local share
if width and width > 1 then
column.UDim = UDim.new(0, width)
else
share = (width and width > 0) and width or (autos > 0 and left / autos or 0)
column.UDim = UDim.new(share, -pixels * share)
end
end
end
local function columnValue(row, column)
if column.Rank then
return nil
end
if row[1] ~= nil or column.Key == nil then
return row[column.Index]
end
return row[column.Key]
end
local function display(value, column, row)
if type(column.Format) == "function" then
local ok, text = pcall(column.Format, value, row)
if ok and text ~= nil then
return tostring(text)
end
end
if type(value) == "number" then
local whole = value == math.floor(value)
local text = whole and string.format("%d", value) or string.format("%.2f", value)
-- Thousands separators on the whole part.
local sign, int, rest = text:match("^(%-?)(%d+)(.*)$")
if int then
int = int:reverse():gsub("(%d%d%d)", "%1,"):reverse():gsub("^,", "")
return sign .. int .. rest
end
return text
end
if value == nil then
return ""
end
return tostring(value)
end
local function paintHeader()
for _, cell in ipairs(headerCells) do
local column = cell.Column
local active = sortKey ~= nil and sortKey == column
tween(cell.Label, { TextColor3 = active and Theme.Text or Theme.Muted }, 0.15)
cell.Arrow.Visible = active
if active then
tween(cell.Arrow, { Rotation = sortDescending and 0 or 180 }, 0.2, Enum.EasingStyle.Quint)
end
end
end
local function sortedRows()
local list = {}
for index, row in ipairs(handle.Rows) do
list[index] = { Row = row, Index = index }
end
if sortKey then
local column = sortKey
table.sort(list, function(a, b)
local x, y = columnValue(a.Row, column), columnValue(b.Row, column)
local nx, ny = tonumber(x), tonumber(y)
if nx and ny then
if nx ~= ny then
if sortDescending then
return nx > ny
end
return nx < ny
end
elseif x ~= nil and y ~= nil then
local sx, sy = string.lower(tostring(x)), string.lower(tostring(y))
if sx ~= sy then
if sortDescending then
return sx > sy
end
return sx < sy
end
elseif x ~= y then
-- Empty cells sink to the bottom either way.
return x ~= nil
end
return a.Index < b.Index
end)
end
-- Ranks come from the full order, so filtering never renumbers them.
for position, item in ipairs(list) do
item.Rank = position
end
if query == "" then
return list
end
local shown = {}
for _, item in ipairs(list) do
for _, column in ipairs(columns) do
if not column.Rank then
local text = display(columnValue(item.Row, column), column, item.Row)
text = string.lower((string.gsub(text, "<[^>]->", "")))
if string.find(text, query, 1, true) then
table.insert(shown, item)
break
end
end
end
end
return shown
end
local function newRow()
local rowFrame = create("TextButton", {
Size = UDim2.new(1, 0, 0, rowHeight),
BackgroundColor3 = Theme.Surface3,
BackgroundTransparency = 1,
BorderSizePixel = 0,
AutoButtonColor = false,
Text = "",
Parent = body,
})
-- The accent bar on a highlighted row sits beside the cells, outside
-- their layout.
local marker = create("Frame", {
Position = UDim2.new(0, 3, 0.5, 0),
AnchorPoint = Vector2.new(0, 0.5),
Size = UDim2.new(0, 2, 1, -10),
BackgroundColor3 = Theme.Accent,
BackgroundTransparency = 1,
BorderSizePixel = 0,
Parent = rowFrame,
})
corner(marker, UDim.new(1, 0))
local cellsFrame = create("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
Parent = rowFrame,
})
padding(cellsFrame, 10, 10)
create("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Parent = cellsFrame,
})
local entry = { Frame = rowFrame, Marker = marker, Cells = {}, Hover = false }
for order, column in ipairs(columns) do
local cell = create("Frame", {
Size = UDim2.new(column.UDim.Scale, column.UDim.Offset, 1, 0),
BackgroundTransparency = 1,
LayoutOrder = order,
Parent = cellsFrame,
})
local text = label({
Size = UDim2.new(1, -6, 1, 0),
TextSize = textSize,
FontFace = column.Rank and Fonts.Bold or Fonts.Regular,
TextColor3 = Theme.Text,
TextXAlignment = alignment(column),
RichText = not column.Rank,
Parent = cell,
})
local badge
if column.Rank then
text.Size = UDim2.fromScale(1, 1)
text.TextXAlignment = Enum.TextXAlignment.Center
badge = create("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromOffset(rowHeight - 8, rowHeight - 8),
BackgroundTransparency = 1,
BorderSizePixel = 0,
ZIndex = 0,
Parent = cell,
})
corner(badge, UDim.new(1, 0))
end
entry.Cells[order] = { Label = text, Badge = badge }
end
local canTap = tapGuard(rowFrame, function()
return body
end)
rowFrame.MouseEnter:Connect(function()
entry.Hover = true
tween(rowFrame, { BackgroundTransparency = 0.35 }, 0.1)
end)
rowFrame.MouseLeave:Connect(function()
entry.Hover = false
tween(rowFrame, {
BackgroundColor3 = entry.Highlighted and Theme.Accent or Theme.Surface3,
BackgroundTransparency = entry.Rest or 1,
}, 0.2)
end)
rowFrame.MouseButton1Click:Connect(function()
if entry.Data and canTap() then
safeCall(opts.Callback, entry.Data, entry.Index)
end
end)
return entry
end
local function fitHeight(instant)
local count = shownCount or #handle.Rows
local visible = math.clamp(count, 1, maxRows)
local gridHeight = headerHeight + visible * rowHeight
local total = gridTop + gridHeight + bottom
if shownHeight == total then
return
end
local duration = (instant or shownHeight == nil) and 0 or 0.25
shownHeight = total
tween(grid, { Size = UDim2.new(1, -inset * 2, 0, gridHeight) }, duration, Enum.EasingStyle.Quint)
tween(frame, { Size = UDim2.new(1, 0, 0, total) }, duration, Enum.EasingStyle.Quint)
end
local function render()
local list = sortedRows()
for position, item in ipairs(list) do
local entry = pool[position]
if not entry then
entry = newRow()
pool[position] = entry
end
local row = item.Row
entry.Data = row
entry.Index = item.Index
entry.Frame.LayoutOrder = position
entry.Frame.Visible = true
local highlighted = row.Highlight == true
if not highlighted and type(opts.Highlight) == "function" then
local ok, result = pcall(opts.Highlight, row, item.Index)
highlighted = ok and result == true
end
entry.Highlighted = highlighted
entry.Rest = highlighted and 0.86 or (position % 2 == 0 and 0.78 or 1)
if not entry.Hover then
entry.Frame.BackgroundColor3 = highlighted and Theme.Accent or Theme.Surface3
entry.Frame.BackgroundTransparency = entry.Rest
bindColors(entry.Frame, { BackgroundColor3 = entry.Frame.BackgroundColor3 })
end
entry.Marker.BackgroundTransparency = highlighted and 0 or 1
for order, column in ipairs(columns) do
local cell = entry.Cells[order]
if column.Rank then
local medal = MEDALS[item.Rank]
cell.Label.Text = tostring(item.Rank)
cell.Label.TextColor3 = medal and Color3.fromRGB(28, 22, 18) or Theme.Muted
bindColors(cell.Label, { TextColor3 = cell.Label.TextColor3 })
cell.Badge.BackgroundColor3 = medal or Theme.Surface3
cell.Badge.BackgroundTransparency = medal and 0.05 or 1
else
local value = columnValue(row, column)
cell.Label.Text = Library._rich(display(value, column, row))
-- Column Color: a colour, a theme key or a function(value, row)
-- returning either. A row's Colors[key or index] beats it.
local wanted = column.Color
if type(wanted) == "function" then
local ok, result = pcall(wanted, value, row)
wanted = ok and result or nil
end
if type(row.Colors) == "table" then
local own = row.Colors[column.Key or column.Index]
if own == nil then
own = row.Colors[column.Index]
end
if own ~= nil then
wanted = own
end
end
local color = Library._color(wanted) or (highlighted and Theme.Accent or Theme.Text)
cell.Label.TextColor3 = color
bindColors(cell.Label, { TextColor3 = color })
end
end
end
for position = #list + 1, #pool do
pool[position].Frame.Visible = false
pool[position].Data = nil
end
shownCount = #list
empty.Visible = #list == 0
empty.Text = (#list == 0 and query ~= "" and #handle.Rows > 0) and (opts.NoMatchText or "No rows match")
or (opts.EmptyText or "Nothing here yet")
if searchCount then
searchCount.Visible = query ~= ""
searchCount.Text = #list .. " / " .. #handle.Rows
end
fitHeight()
end
local function buildHeader()
for _, cell in ipairs(headerCells) do
cell.Frame:Destroy()
end
table.clear(headerCells)
for _, entry in ipairs(pool) do
entry.Frame:Destroy()
end
table.clear(pool)
local row = header:FindFirstChild("Cells") or create("Frame", {
Name = "Cells",
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
Parent = header,
})
if not row:FindFirstChildOfClass("UIListLayout") then
padding(row, 10, 10)
create("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Parent = row,
})
end
for order, column in ipairs(columns) do
local cell = create("TextButton", {
Size = UDim2.new(column.UDim.Scale, column.UDim.Offset, 1, 0),
BackgroundTransparency = 1,
AutoButtonColor = false,
Text = "",
LayoutOrder = order,
Parent = row,
})
local align = column.Rank and Enum.TextXAlignment.Center or alignment(column)
local right = align == Enum.TextXAlignment.Right
local text = label({
Size = UDim2.new(1, align == Enum.TextXAlignment.Center and 0 or -16, 1, 0),
Position = UDim2.fromOffset(right and 16 or 0, 0),
Text = string.upper(column.Name),
TextSize = 11,
FontFace = Fonts.Bold,
TextColor3 = Theme.Muted,
TextXAlignment = align,
Parent = cell,
})
local arrow = create("ImageLabel", {
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, 0, 0.5, 0),
Size = UDim2.fromOffset(12, 12),
BackgroundTransparency = 1,
ImageColor3 = Theme.Accent,
ScaleType = Enum.ScaleType.Fit,
Visible = false,
Parent = cell,
})
applyIcon(arrow, "chevron-down")
-- The arrow sits just after the header text (before it when the
-- column is right aligned).
local function placeArrow()
local width = text.TextBounds.X
if right then
arrow.Position = UDim2.new(1, -(width + 15), 0.5, 0)
elseif align == Enum.TextXAlignment.Center then
arrow.Position = UDim2.new(0.5, width / 2 + 3, 0.5, 0)
else
local cellWidth = cell.AbsoluteSize.X / math.max(window.Scale.Scale, 0.01)
arrow.Position = UDim2.new(0, math.clamp(width + 4, 0, math.max(cellWidth - 12, 0)), 0.5, 0)
end
end
text:GetPropertyChangedSignal("TextBounds"):Connect(placeArrow)
cell:GetPropertyChangedSignal("AbsoluteSize"):Connect(placeArrow)
task.defer(placeArrow)
local sortable = column.Sortable and not column.Rank
if sortable then
cell.MouseEnter:Connect(function()
if sortKey ~= column then
tween(text, { TextColor3 = Theme.Text }, 0.12)
end
end)
cell.MouseLeave:Connect(function()
if sortKey ~= column then
tween(text, { TextColor3 = Theme.Muted }, 0.2)
end
end)
cell.MouseButton1Click:Connect(function()
-- Numbers sort high to low first, text A to Z; the second
-- click flips it and the third goes back to the given order.
if sortKey ~= column then
local numeric = false
for _, data in ipairs(handle.Rows) do
local value = columnValue(data, column)
if value ~= nil then
numeric = tonumber(value) ~= nil
break
end
end
sortKey, sortDescending = column, numeric
column.FirstDescending = numeric
elseif sortDescending == column.FirstDescending then
sortDescending = not sortDescending
else
sortKey = nil
end
paintHeader()
render()
end)
end
table.insert(headerCells, { Frame = cell, Label = text, Arrow = arrow, Column = column })
end
paintHeader()
end
function handle:SetColumns(list)
table.clear(columns)
if opts.Rank then
table.insert(columns, { Name = "#", Rank = true, Width = TOUCH and 40 or 36, Sortable = false })
end
for index, column in ipairs(type(list) == "table" and list or {}) do
if type(column) ~= "table" then
column = { Name = tostring(column) }
end
local name = column.Name or column.Title or ("Column " .. index)
table.insert(columns, {
Name = name,
Key = column.Key or name,
Index = index,
Width = column.Width,
Align = column.Align,
Format = column.Format,
Color = column.Color,
Sortable = column.Sortable ~= false and opts.Sortable ~= false,
})
end
sortKey = nil
layoutColumns()
buildHeader()
render()
end
local function findColumn(key)
if type(key) == "number" then
return columns[key + (opts.Rank and 1 or 0)]
end
for _, column in ipairs(columns) do
if not column.Rank and (column.Key == key or column.Name == key) then
return column
end
end
return nil
end
function handle:SetRows(rows)
handle.Rows = {}
for index, row in ipairs(type(rows) == "table" and rows or {}) do
handle.Rows[index] = type(row) == "table" and row or { row }
end
render()
end
handle.Set = handle.SetRows
function handle:GetRows()
return handle.Rows
end
handle.Get = handle.GetRows
function handle:AddRow(row, position)
row = type(row) == "table" and row or { row }
if tonumber(position) then
table.insert(handle.Rows, math.clamp(math.floor(position), 1, #handle.Rows + 1), row)
else
table.insert(handle.Rows, row)
end
render()
return #handle.Rows
end
-- Replaces a row (by its index in the list you gave) and flashes it.
function handle:UpdateRow(index, row)
if not handle.Rows[index] then
return false
end
handle.Rows[index] = type(row) == "table" and row or { row }
render()
for _, entry in ipairs(pool) do
if entry.Index == index and entry.Frame.Visible then
local restColor = entry.Highlighted and Theme.Accent or Theme.Surface3
tween(entry.Frame, { BackgroundColor3 = Theme.Accent, BackgroundTransparency = 0.7 }, 0.08)
task.delay(0.1, function()
if not entry.Hover then
tween(entry.Frame, { BackgroundColor3 = restColor, BackgroundTransparency = entry.Rest }, 0.5)
end
end)
break
end
end
return true
end
function handle:RemoveRow(index)
if not handle.Rows[index] then
return false
end
table.remove(handle.Rows, index)
render()
return true
end
function handle:Clear()
handle:SetRows({})
end
-- Sort(nil) goes back to the given order.
function handle:Sort(key, descending)
sortKey = key ~= nil and findColumn(key) or nil
sortDescending = descending == true
if sortKey then
sortKey.FirstDescending = sortDescending
end
paintHeader()
render()
end
function handle:SetTitle(text)
if title then
title.Text = tostring(text)
end
end
function handle:SetMaxRows(count)
maxRows = math.max(math.floor(tonumber(count) or maxRows), 1)
fitHeight()
end
handle:SetColumns(opts.Columns or {})
handle:SetRows(opts.Rows or {})
if opts.SortBy ~= nil then
handle:Sort(opts.SortBy, opts.SortDescending ~= false)
end
fitHeight(true)
if searchBox then
searchBox:GetPropertyChangedSignal("Text"):Connect(function()
local text = string.lower(trim(searchBox.Text))
if text ~= query then
query = text
body.CanvasPosition = Vector2.zero
render()
end
end)
end
function handle:SetSearch(text)
if searchBox then
searchBox.Text = tostring(text or "")
end
end
-- Theme tags in cells are baked to colours; repaint them on theme change.
onTheme(grid, render)
return finishElement(self, opts, handle, frame, "Table")
end
Tab.CreateImage = Tab.Image
Tab.AddImage = Tab.Image
Tab.CreateViewport = Tab.Viewport
Tab.AddViewport = Tab.Viewport
Tab.CreateTable = Tab.Table
Tab.AddTable = Tab.Table
Tab.CreateLeaderboard = Tab.Table
local function settleSubOut(ui)
if ui.OutFrame then
ui.OutFrame.Parent = ui.Area
ui.OutFrame.Visible = false
ui.OutFrame = nil
end
ui.Out.Visible = false
end
local function settleSubIn(ui)
if ui.InFrame then
ui.InFrame.Parent = ui.Area
ui.InFrame.Position = UDim2.fromOffset(0, 0)
ui.InFrame = nil
end
ui.In.Visible = false
end
function Tab:_setupSubTabs()
local top = self._headerBottom
local page = self._page
local strip = create("ScrollingFrame", {
Name = "SubTabs",
Position = UDim2.fromOffset(24, top - 4),
Size = UDim2.new(1, -48, 0, 32),
BackgroundTransparency = 1,
BorderSizePixel = 0,
ScrollBarThickness = 0,
ScrollingDirection = Enum.ScrollingDirection.X,
AutomaticCanvasSize = Enum.AutomaticSize.X,
CanvasSize = UDim2.new(),
Parent = page,
})
strip:SetAttribute("NoDrag", true)
local highlight = create("Frame", {
Size = UDim2.fromOffset(0, 32),
BackgroundColor3 = Theme.Surface3,
BackgroundTransparency = 0.35,
BorderSizePixel = 0,
Visible = false,
Parent = strip,
})
corner(highlight, UDim.new(0, 8))
stroke(highlight, Theme.Stroke)
local row = create("Frame", {
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundTransparency = 1,
ZIndex = 2,
Parent = strip,
})
create("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 4),
Parent = row,
})
self.Window:_overflowHint(page, strip, row, false, Theme.Background)
local areaTop = top + 36
local area = create("Frame", {
Name = "SubPages",
Position = UDim2.fromOffset(0, areaTop),
Size = UDim2.new(1, 0, 1, -areaTop),
BackgroundTransparency = 1,
ClipsDescendants = true,
Parent = page,
})
local ui = {
Strip = strip,
Row = row,
Highlight = highlight,
Area = area,
Out = create("CanvasGroup", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
Visible = false,
ZIndex = 2,
Parent = area,
}),
In = create("CanvasGroup", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
Visible = false,
ZIndex = 3,
Parent = area,
}),
Generation = 0,
}
self._sub = ui
self._subTabs = {}
self.List.Visible = false
self:_refreshEmpty()
-- Keep the pill under the selected tab when widths change after layout.
row:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
if self.CurrentSubTab and not ui.Moving then
self:_placeSubHighlight(self.CurrentSubTab, true)
end
end)
end
function Tab:_placeSubHighlight(sub, instant)
local ui = self._sub
local scale = self.Window.Scale.Scale
local button = sub._button
local width = button.AbsoluteSize.X / scale
if width <= 0 then
return
end
local x = (button.AbsolutePosition.X - ui.Row.AbsolutePosition.X) / scale
local props = { Position = UDim2.fromOffset(x, 0), Size = UDim2.fromOffset(width, 32) }
if instant or not ui.Highlight.Visible then
ui.Highlight.Visible = true
ui.Highlight.Position = props.Position
ui.Highlight.Size = props.Size
else
ui.Moving = true
tween(ui.Highlight, props, 0.35, Enum.EasingStyle.Quint)
task.delay(0.36, function()
ui.Moving = false
end)
end
local view = ui.Strip.AbsoluteSize.X / scale
local canvasX = ui.Strip.CanvasPosition.X
if x < canvasX then
tween(ui.Strip, { CanvasPosition = Vector2.new(math.max(x - 8, 0), 0) }, 0.3, Enum.EasingStyle.Quint)
elseif x + width > canvasX + view then
tween(ui.Strip, { CanvasPosition = Vector2.new(x + width - view + 8, 0) }, 0.3, Enum.EasingStyle.Quint)
end
end
function Tab:SubTab(opts, icon)
if self._groupbox or self._isSubTab then
return self.Parent:SubTab(opts, icon)
end
opts = normalize(opts, { Title = "Name", Description = "Desc" })
if icon ~= nil and opts.Icon == nil then
opts.Icon = icon
end
if not self._subTabs then
self:_setupSubTabs()
end
local ui = self._sub
local index = #self._subTabs + 1
local sub = setmetatable({
Name = opts.Name or ("Page " .. index),
Window = self.Window,
Parent = self,
_order = 0,
_isSubTab = true,
_index = index,
_iconName = opts.Icon,
}, Tab)
local frame = create("Frame", {
Name = sub.Name,
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
Visible = false,
Parent = ui.Area,
})
sub._frame = frame
buildContainer(sub, frame, 0, opts.Icon or self._iconName or "layout-grid", opts.EmptyText or "Nothing here yet")
local button = create("TextButton", {
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundTransparency = 1,
Text = "",
AutoButtonColor = false,
LayoutOrder = index,
Parent = ui.Row,
})
padding(button, 14, 14)
create("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 6),
Parent = button,
})
if opts.Icon then
local image = create("ImageLabel", {
Size = UDim2.fromOffset(14, 14),
BackgroundTransparency = 1,
ImageColor3 = Theme.Muted,
ScaleType = Enum.ScaleType.Fit,
LayoutOrder = 1,
Parent = button,
})
applyIcon(image, opts.Icon)
if image:GetAttribute("CustomIcon") then
image.ImageTransparency = 0.4
end
sub._iconImage = image
end
sub._label = label({
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
Text = sub.Name,
TextSize = 13,
TextColor3 = Theme.Muted,
TextTruncate = Enum.TextTruncate.None,
LayoutOrder = 2,
Parent = button,
})
sub._button = button
button.MouseEnter:Connect(function()
if self.CurrentSubTab ~= sub then
tween(sub._label, { TextColor3 = Theme.Text }, 0.12)
end
end)
button.MouseLeave:Connect(function()
if self.CurrentSubTab ~= sub then
tween(sub._label, { TextColor3 = Theme.Muted }, 0.2)
end
end)
button.MouseButton1Click:Connect(function()
self:SelectSubTab(sub)
end)
table.insert(self._subTabs, sub)
if index == 1 then
self:SelectSubTab(sub, true)
end
return sub
end
Tab.CreateSubTab = Tab.SubTab
Tab.AddSubTab = Tab.SubTab
function Tab:SelectSubTab(sub, instant)
if type(sub) ~= "table" then
for index, entry in ipairs(self._subTabs or {}) do
if entry.Name == sub or index == sub then
sub = entry
break
end
end
end
if type(sub) ~= "table" or not self._sub then
return
end
local previous = self.CurrentSubTab
if previous == sub then
return
end
self.Window:_resetSearch()
self.Window:_closePopups()
self.CurrentSubTab = sub
local ui = self._sub
settleSubOut(ui)
settleSubIn(ui)
ui.Generation += 1
local generation = ui.Generation
local fade = instant and 0 or 0.2
if previous then
tween(previous._label, { TextColor3 = Theme.Muted }, fade)
if previous._iconImage then
tintIcon(previous._iconImage, Theme.Muted, false, fade)
end
end
tween(sub._label, { TextColor3 = Theme.Text }, fade)
if sub._iconImage then
tintIcon(sub._iconImage, Theme.Accent, true, fade)
end
self:_placeSubHighlight(sub, instant)
if instant or not previous then
if previous then
previous._frame.Visible = false
end
sub._frame.Visible = true
return
end
local direction = sub._index > previous._index and 1 or -1
ui.OutFrame = previous._frame
previous._frame.Parent = ui.Out
ui.Out.GroupTransparency = 0
ui.Out.Position = UDim2.fromOffset(0, 0)
ui.Out.Visible = true
tween(ui.Out, { GroupTransparency = 1, Position = UDim2.fromOffset(-direction * 28, 0) }, 0.2)
ui.InFrame = sub._frame
sub._frame.Visible = true
sub._frame.Parent = ui.In
ui.In.GroupTransparency = 1
ui.In.Position = UDim2.fromOffset(direction * 28, 0)
ui.In.Visible = true
tween(ui.In, { GroupTransparency = 0, Position = UDim2.fromOffset(0, 0) }, 0.34, Enum.EasingStyle.Quint)
task.delay(0.2, function()
if ui.Generation == generation then
settleSubOut(ui)
end
end)
task.delay(0.34, function()
if ui.Generation == generation then
settleSubIn(ui)
end
end)
end
-- Search: one box over the whole window. Typing lists matching tabs, pages,
-- groupboxes and controls from every tab; picking one opens its tab and page,
-- scrolls it into view and flashes it.
Window._searchIcons = {
Tab = "app-window",
Page = "panels-top-left",
Groupbox = "layout-grid",
Button = "mouse-pointer-click",
ButtonRow = "mouse-pointer-click",
Toggle = "toggle-right",
Slider = "sliders-horizontal",
Dropdown = "list",
Input = "text-cursor-input",
Keybind = "keyboard",
ColorPicker = "palette",
Stepper = "chevrons-up-down",
Progress = "activity",
Status = "activity",
StatusList = "activity",
Label = "type",
Paragraph = "type",
OrderList = "list-ordered",
}
function Window:_searchResults(query)
local results = {}
local function consider(name, entry)
if type(name) ~= "string" or name == "" then
return
end
local at = string.find(string.lower(name), query, 1, true)
if not at then
return
end
entry.Name = name
entry.At = at
-- Name starts first, then word starts, then anywhere inside a word.
entry.Rank = at == 1 and 0 or (string.find(string.sub(name, at - 1, at - 1), "[%s%p]") and 1 or 2)
entry.Order = #results + 1
table.insert(results, entry)
end
local function shown(element)
return not element._destroyed
and element._type ~= "Section" and element._type ~= "Divider"
and (element._isShown == nil or element:_isShown())
end
for _, tab in ipairs(self.Tabs) do
consider(tab.Name, { Kind = "Tab", Tab = tab, Icon = tab._iconName })
for _, page in ipairs(tab._subTabs or { tab }) do
local sub = page._isSubTab and page or nil
local path = tab.Name
if sub then
consider(sub.Name, { Kind = "Page", Tab = tab, Sub = sub, Path = tab.Name })
path = tab.Name .. " › " .. sub.Name
end
if not page._noSearch then
for _, element in ipairs(page._items or {}) do
if shown(element) then
consider(element._searchName, { Kind = element._type, Tab = tab, Sub = sub, Target = element, Path = path })
end
end
for _, group in ipairs(page._groupboxes or {}) do
if group._userVisible ~= false then
consider(group.Name, { Kind = "Groupbox", Tab = tab, Sub = sub, Target = group, Path = path })
local groupPath = path .. " › " .. group.Name
for _, element in ipairs(group._items) do
if shown(element) then
consider(element._searchName, { Kind = element._type, Tab = tab, Sub = sub, Target = element, Group = group, Path = groupPath })
end
end
end
end
end
end
end
table.sort(results, function(a, b)
if a.Rank ~= b.Rank then
return a.Rank < b.Rank
end
return a.Order < b.Order
end)
return results
end
-- Marks a found control: it pops, a soft accent glow blooms around it, a
-- sheen of light sweeps across it and a bright spot runs round its border,
-- then everything breathes once more and fades.
function Window:_flash(frame, radius)
for _, name in ipairs({ "SearchFlash", "SearchGlow" }) do
local old = frame:FindFirstChild(name)
if old then
old:Destroy()
end
end
radius = radius or 8
local accent = Theme.Accent
local bright = accent:Lerp(Color3.new(1, 1, 1), 0.65)
-- Outer glow: a wide, faint stroke around a second outline.
local halo = create("Frame", {
Name = "SearchGlow",
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
ZIndex = 10,
Parent = frame,
})
corner(halo, UDim.new(0, radius))
local haloStroke = create("UIStroke", {
Color = accent,
Thickness = 1,
Transparency = 1,
ApplyStrokeMode = Enum.ApplyStrokeMode.Border,
Parent = halo,
})
-- Wash with a moving sheen: a brighter diagonal band crosses it once.
local wash = create("Frame", {
Name = "SearchFlash",
Size = UDim2.fromScale(1, 1),
BackgroundColor3 = accent,
BackgroundTransparency = 1,
BorderSizePixel = 0,
ZIndex = 10,
Parent = frame,
})
corner(wash, UDim.new(0, radius))
local sheen = create("UIGradient", {
Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0.7),
NumberSequenceKeypoint.new(0.4, 0.55),
NumberSequenceKeypoint.new(0.5, 0),
NumberSequenceKeypoint.new(0.6, 0.55),
NumberSequenceKeypoint.new(1, 0.7),
}),
Rotation = 18,
Offset = Vector2.new(-1, 0),
Parent = wash,
})
-- Border light: the ring's gradient turns, so a bright spot runs round it.
local ring = create("UIStroke", {
Color = Color3.new(1, 1, 1),
Thickness = 1.6,
Transparency = 1,
ApplyStrokeMode = Enum.ApplyStrokeMode.Border,
Parent = wash,
})
local runner = create("UIGradient", {
Color = ColorSequence.new({
ColorSequenceKeypoint.new(0, accent),
ColorSequenceKeypoint.new(0.5, bright),
ColorSequenceKeypoint.new(1, accent),
}),
Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0.45),
NumberSequenceKeypoint.new(0.5, 0),
NumberSequenceKeypoint.new(1, 0.45),
}),
Parent = ring,
})
-- Groupboxes clip their rows, so anything drawn outside a control is cut
-- off at its corners. The strokes go on the inside of the control where
-- the engine supports it; otherwise everything is inset by its width.
local inside = pcall(function()
haloStroke.BorderStrokePosition = Enum.BorderStrokePosition.Inner
ring.BorderStrokePosition = Enum.BorderStrokePosition.Inner
end)
local glowWidth = inside and 6 or 3
if not inside then
halo.Position = UDim2.fromOffset(glowWidth + 1, glowWidth + 1)
halo.Size = UDim2.new(1, -(glowWidth + 1) * 2, 1, -(glowWidth + 1) * 2)
wash.Position = UDim2.fromOffset(2, 2)
wash.Size = UDim2.new(1, -4, 1, -4)
end
-- A small pop, unless something else is scaling the control already. It
-- never grows past full size, which would be clipped as well.
local pop
if not frame:FindFirstChildOfClass("UIScale") then
pop = create("UIScale", { Scale = 0.975, Parent = frame })
tween(pop, { Scale = 1 }, 0.4, Enum.EasingStyle.Quint)
end
tween(wash, { BackgroundTransparency = 0.8 }, 0.2, Enum.EasingStyle.Quad)
tween(ring, { Transparency = 0 }, 0.2, Enum.EasingStyle.Quad)
tween(haloStroke, { Thickness = glowWidth, Transparency = 0.72 }, 0.35, Enum.EasingStyle.Quint)
tween(sheen, { Offset = Vector2.new(1, 0) }, 0.8, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut)
tween(runner, { Rotation = 540 }, 2.2, Enum.EasingStyle.Quad)
task.spawn(function()
task.wait(0.75)
-- One softer breath, then fade out.
tween(wash, { BackgroundTransparency = 0.9 }, 0.35, Enum.EasingStyle.Sine)
tween(haloStroke, { Thickness = glowWidth / 2, Transparency = 0.85 }, 0.35, Enum.EasingStyle.Sine)
task.wait(0.35)
tween(wash, { BackgroundTransparency = 0.84 }, 0.3, Enum.EasingStyle.Sine)
tween(haloStroke, { Thickness = glowWidth * 0.85, Transparency = 0.76 }, 0.3, Enum.EasingStyle.Sine)
task.wait(0.4)
tween(wash, { BackgroundTransparency = 1 }, 0.55, Enum.EasingStyle.Quad)
tween(ring, { Transparency = 1 }, 0.65, Enum.EasingStyle.Quad)
tween(haloStroke, { Thickness = glowWidth, Transparency = 1 }, 0.65, Enum.EasingStyle.Quad)
task.wait(0.7)
wash:Destroy()
halo:Destroy()
if pop then
pop:Destroy()
end
end)
end
function Window:_revealResult(result)
local tab, sub, target, group = result.Tab, result.Sub, result.Target, result.Group
if self.CurrentTab ~= tab then
self:SelectTab(tab)
end
if sub then
tab:SelectSubTab(sub)
end
if not target then
local button = sub and sub._button or tab._button
task.delay(0.1, function()
if button.Parent then
self:_flash(button, sub and 8 or 10)
end
end)
return
end
local expanding = group ~= nil and group:IsCollapsed()
if expanding then
group:Expand()
end
local list = (sub or tab).List
task.spawn(function()
-- Let the freshly shown page lay out before measuring it.
task.wait()
task.wait()
if expanding then
task.wait(0.3)
end
local frame = target._frame
if self._destroyed or target._destroyed or not frame or not frame.Parent or not list then
return
end
local scale = self.Scale.Scale
local top = (frame.AbsolutePosition.Y - list.AbsolutePosition.Y) / scale + list.CanvasPosition.Y
local height = frame.AbsoluteSize.Y / scale
local view = list.AbsoluteWindowSize.Y / scale
local limit = math.max(list.AbsoluteCanvasSize.Y / scale - view, 0)
local y = math.clamp(top + height / 2 - view / 2, 0, limit)
if math.abs(y - list.CanvasPosition.Y) > 2 then
tween(list, { CanvasPosition = Vector2.new(0, y) }, 0.45, Enum.EasingStyle.Quint)
task.wait(0.25)
end
if frame.Parent then
self:_flash(frame, target._groupbox and 10 or 8)
end
end)
end
function Window:_resetSearch()
if self.SearchBox and self.SearchBox.Text ~= "" then
self.SearchBox.Text = ""
end
end
-- Page titles stop short of the search box and close button.
function Window:_fitTitle(tab)
local right = self.SearchBox and (self._searchWidth or SEARCH_WIDTH) + 114 or 98
local reserve = tab._titleX + right
tab._title.Size = UDim2.new(1, -reserve, 0, 24)
if tab._desc then
tab._desc.Size = UDim2.new(1, -reserve, 0, 16)
end
end
function Window:_buildSearch(content)
-- No box: the icon and text sit on a thin baseline. Focus draws an
-- accent line out from the middle of it.
local holder = create("Frame", {
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, -98, 0, 14),
Size = UDim2.fromOffset(SEARCH_WIDTH, 34),
BackgroundTransparency = 1,
BorderSizePixel = 0,
ZIndex = 5,
Parent = content,
})
holder:SetAttribute("NoDrag", true)
local baseline = create("Frame", {
AnchorPoint = Vector2.new(0, 1),
Position = UDim2.new(0, 0, 1, -2),
Size = UDim2.new(1, 0, 0, 1),
BackgroundColor3 = Theme.Stroke,
BorderSizePixel = 0,
Parent = holder,
})
local focusLine = create("Frame", {
AnchorPoint = Vector2.new(0.5, 1),
Position = UDim2.new(0.5, 0, 1, -1),
Size = UDim2.new(0, 0, 0, 2),
BackgroundColor3 = Theme.Accent,
BorderSizePixel = 0,
ZIndex = 2,
Parent = holder,
})
corner(focusLine, UDim.new(1, 0))
-- Kept for the focus handlers below, which recolour it.
local _, icon = glowIcon(holder, "search", Theme.Muted, UDim2.new(0, 2, 0.5, -1))
icon.Parent.Size = UDim2.fromOffset(15, 15)
local box = create("TextBox", {
Position = UDim2.fromOffset(26, 0),
Size = UDim2.new(1, -28, 1, -2),
BackgroundTransparency = 1,
Text = "",
PlaceholderText = "Search",
PlaceholderColor3 = Theme.Muted,
TextColor3 = Theme.Text,
TextSize = 13,
FontFace = Fonts.Regular,
TextXAlignment = Enum.TextXAlignment.Left,
TextTruncate = Enum.TextTruncate.AtEnd,
ClearTextOnFocus = false,
Parent = holder,
})
holder.MouseEnter:Connect(function()
if not box:IsFocused() then
tween(baseline, { BackgroundColor3 = Theme.StrokeHover }, 0.15)
end
end)
holder.MouseLeave:Connect(function()
tween(baseline, { BackgroundColor3 = Theme.Stroke }, 0.25)
end)
self.SearchBox = box
-- Results float over the window under the box, like a dropdown popup.
local rowHeight = TOUCH and 40 or 34
local maxRows = 8
local popup = create("Frame", {
AnchorPoint = Vector2.new(1, 0),
Size = UDim2.fromOffset(300, 0),
BackgroundTransparency = 1,
Visible = false,
ZIndex = 45,
Parent = self.Body,
})
popup:SetAttribute("NoDrag", true)
local popupShadow = create("ImageLabel", {
Position = UDim2.fromOffset(-18, -12),
Size = UDim2.new(1, 36, 1, 36),
BackgroundTransparency = 1,
Image = Assets.Shadow,
ImageColor3 = Color3.new(0, 0, 0),
ImageTransparency = 1,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(49, 49, 450, 450),
Parent = popup,
})
local panel = create("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundColor3 = Theme.Background,
BorderSizePixel = 0,
ClipsDescendants = true,
Parent = popup,
})
corner(panel, UDim.new(0, 10))
local panelStroke = stroke(panel, Theme.Stroke, 1)
edgeHighlight(panel)
local list = create("ScrollingFrame", {
Position = UDim2.fromOffset(6, 6),
Size = UDim2.new(1, -12, 1, -12),
BackgroundTransparency = 1,
BorderSizePixel = 0,
ScrollBarThickness = TOUCH and 4 or 3,
ScrollBarImageColor3 = Theme.Accent,
ScrollBarImageTransparency = 0.4,
ScrollingDirection = Enum.ScrollingDirection.Y,
CanvasSize = UDim2.new(),
Parent = panel,
})
create("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 2),
Parent = list,
})
local noResults = label({
Size = UDim2.new(1, 0, 0, rowHeight),
Text = "No results",
TextSize = 13,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextXAlignment = Enum.TextXAlignment.Center,
LayoutOrder = 0,
Visible = false,
Parent = list,
})
local rows = {}
local results = {}
local selected = 1
local open = false
local generation = 0
local shownSize = Vector2.new(300, 0)
local stopFollowing = nil
local closeResults
local function place()
local scale = self.Scale.Scale
local bodyPosition = self.Body.AbsolutePosition
local bodyWidth = self.Body.AbsoluteSize.X / scale
local right = (holder.AbsolutePosition.X + holder.AbsoluteSize.X - bodyPosition.X) / scale
local top = (holder.AbsolutePosition.Y + holder.AbsoluteSize.Y - bodyPosition.Y) / scale + 6
right = math.clamp(right, shownSize.X + 8, math.max(bodyWidth - 8, shownSize.X + 8))
popup.Position = UDim2.fromOffset(right, top)
end
local function measure()
local scale = self.Scale.Scale
local bodySize = self.Body.AbsoluteSize / scale
local below = (holder.AbsolutePosition.Y + holder.AbsoluteSize.Y - self.Body.AbsolutePosition.Y) / scale + 6
local count = math.max(#rows, 1)
local height = math.min(count, maxRows) * (rowHeight + 2) - 2 + 12
height = math.max(math.min(height, bodySize.Y - below - 12), rowHeight + 12)
local width = math.min(math.max(holder.AbsoluteSize.X / scale, 300), math.max(bodySize.X - 16, 160))
shownSize = Vector2.new(width, height)
end
local function paintSelection()
for index, row in ipairs(rows) do
local on = index == selected
tween(row.Button, { BackgroundTransparency = on and 0.35 or 1 }, 0.12)
tintIcon(row.Icon, on and Theme.Accent or Theme.Muted, on, 0.12)
end
end
local function keepSelectionInView()
local rowTop = (selected - 1) * (rowHeight + 2)
local view = list.AbsoluteWindowSize.Y / self.Scale.Scale
local y = list.CanvasPosition.Y
if rowTop < y then
y = rowTop
elseif rowTop + rowHeight > y + view then
y = rowTop + rowHeight - view
else
return
end
list.CanvasPosition = Vector2.new(0, math.max(y, 0))
end
local function setOpen(value)
if value == open then
return
end
open = value
generation += 1
local current = generation
if value then
if self._closePopup and self._closePopup ~= closeResults then
self._closePopup()
end
self._closePopup = closeResults
place()
popup.Size = UDim2.fromOffset(shownSize.X, 0)
popup.Visible = true
tween(popup, { Size = UDim2.fromOffset(shownSize.X, shownSize.Y) }, 0.26, Enum.EasingStyle.Quint)
tween(panelStroke, { Transparency = 0 }, 0.12)
tween(popupShadow, { ImageTransparency = 0.55 }, 0.26)
if not stopFollowing then
stopFollowing = self:_listen("Render", place)
end
else
if self._closePopup == closeResults then
self._closePopup = nil
end
tween(popup, { Size = UDim2.fromOffset(shownSize.X, 0) }, 0.18, Enum.EasingStyle.Quint)
tween(popupShadow, { ImageTransparency = 1 }, 0.14)
task.delay(0.19, function()
if generation == current and not open then
popup.Visible = false
panelStroke.Transparency = 1
if stopFollowing then
stopFollowing()
stopFollowing = nil
end
end
end)
end
end
closeResults = function()
setOpen(false)
end
local function choose(result)
if not result then
return
end
setOpen(false)
box.Text = ""
if box:IsFocused() then
box:ReleaseFocus()
end
self:_revealResult(result)
end
local function escape(text)
return (string.gsub(string.gsub(string.gsub(text, "&", "&"), "<", "<"), ">", ">"))
end
local function render(query)
for _, row in ipairs(rows) do
row.Button:Destroy()
end
table.clear(rows)
results = self:_searchResults(query)
selected = 1
local accent = colorToHex(Theme.Accent)
for index, result in ipairs(results) do
if index > 50 then
break
end
local button = create("TextButton", {
Size = UDim2.new(1, 0, 0, rowHeight),
BackgroundColor3 = Theme.Surface3,
BackgroundTransparency = 1,
Text = "",
AutoButtonColor = false,
LayoutOrder = index,
Parent = list,
})
corner(button, UDim.new(0, 7))
-- One line of breadcrumbs: tab, sub tab and groupbox in muted
-- text, each split by a chevron, ending on the match in accent.
local crumbs = create("Frame", {
Position = UDim2.fromOffset(10, 0),
Size = UDim2.new(1, -20, 1, 0),
BackgroundTransparency = 1,
ClipsDescendants = true,
Parent = button,
})
create("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 5),
Parent = crumbs,
})
local part = 0
local firstIcon = nil
local function crumbIcon(icon)
if not icon then
return
end
part += 1
local image = create("ImageLabel", {
Size = UDim2.fromOffset(14, 14),
BackgroundTransparency = 1,
ImageColor3 = Theme.Muted,
ScaleType = Enum.ScaleType.Fit,
LayoutOrder = part,
Parent = crumbs,
})
applyIcon(image, icon)
firstIcon = firstIcon or image
end
local function crumbText(text, color, rich)
part += 1
return label({
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
Text = text,
RichText = rich == true,
TextSize = 13,
FontFace = color and Fonts.Medium or Fonts.Regular,
TextColor3 = color or Theme.Muted,
TextTruncate = Enum.TextTruncate.None,
LayoutOrder = part,
Parent = crumbs,
})
end
local function chevron()
crumbText(">")
end
local tab, sub, group = result.Tab, result.Sub, result.Group
local onTab = result.Kind == "Tab"
local onSub = result.Kind == "Page"
local onGroup = result.Kind == "Groupbox"
crumbIcon(tab._iconName or "layout-grid")
if not onTab then
crumbText(tab.Name)
if sub then
chevron()
crumbIcon(sub._iconName or "layers")
if not onSub then
crumbText(sub.Name)
end
end
if group and not onGroup then
chevron()
crumbText(group.Name)
end
if not onSub then
chevron()
end
end
-- The match itself: accent, with the typed part in bold.
local name = result.Name
local stop = result.At + #query
local marked = escape(string.sub(name, 1, result.At - 1))
.. "" .. escape(string.sub(name, result.At, stop - 1)) .. ""
.. escape(string.sub(name, stop))
crumbText(marked, Theme.Accent, true)
local image = firstIcon
local isTap = tapGuard(button, function()
return list
end)
button.MouseEnter:Connect(function()
if not usingTouch() then
selected = index
paintSelection()
end
end)
button.MouseButton1Click:Connect(function()
if isTap() then
choose(result)
end
end)
table.insert(rows, { Button = button, Icon = image })
end
noResults.Visible = #rows == 0
list.CanvasSize = UDim2.fromOffset(0, math.max(#rows, 1) * (rowHeight + 2) - 2)
list.CanvasPosition = Vector2.zero
paintSelection()
measure()
if open then
place()
tween(popup, { Size = UDim2.fromOffset(shownSize.X, shownSize.Y) }, 0.2, Enum.EasingStyle.Quint)
end
end
local function update()
local query = string.match(string.lower(box.Text), "^%s*(.-)%s*$")
if query == "" then
setOpen(false)
return
end
render(query)
setOpen(true)
end
box.Focused:Connect(function()
tween(focusLine, { Size = UDim2.new(1, 0, 0, 2) }, 0.35, Enum.EasingStyle.Quint)
tween(icon, { ImageColor3 = Theme.Accent }, 0.15)
if box.Text ~= "" then
update()
end
end)
box.FocusLost:Connect(function(enterPressed)
tween(focusLine, { Size = UDim2.new(0, 0, 0, 2) }, 0.3, Enum.EasingStyle.Quint)
tween(icon, { ImageColor3 = Theme.Muted }, 0.2)
if enterPressed and open then
choose(results[selected])
end
end)
box:GetPropertyChangedSignal("Text"):Connect(update)
self:_listen("Began", function(input)
if not open then
return
end
if input.KeyCode == Enum.KeyCode.Up or input.KeyCode == Enum.KeyCode.Down then
if #rows > 0 then
local step = input.KeyCode == Enum.KeyCode.Up and -1 or 1
selected = math.clamp(selected + step, 1, #rows)
paintSelection()
keepSelectionInView()
end
elseif input.KeyCode == Enum.KeyCode.Escape then
setOpen(false)
elseif isPress(input) then
local point = pointerPosition()
if not pointInside(point, popup) and not pointInside(point, holder) then
setOpen(false)
end
end
end)
local function fit()
local width = content.AbsoluteSize.X / self.Scale.Scale
local searchWidth = math.clamp(math.floor(width * 0.32), 110, SEARCH_WIDTH)
holder.Size = UDim2.fromOffset(searchWidth, 34)
if searchWidth ~= self._searchWidth then
self._searchWidth = searchWidth
for _, tab in ipairs(self.Tabs) do
self:_fitTitle(tab)
end
end
end
content:GetPropertyChangedSignal("AbsoluteSize"):Connect(fit)
task.defer(fit)
end
-- Identity: the player's name and headshot shown in the sidebar and on the
-- home tab. The privacy switches hide them everywhere at once.
function Window:_trackName(textLabel, kind)
table.insert(self._identity.Names, { Label = textLabel, Kind = kind })
self:_renderIdentity()
end
function Window:_trackAvatar(image, placeholder)
table.insert(self._identity.Avatars, { Image = image, Placeholder = placeholder })
self:_renderIdentity(true)
end
function Window:_renderIdentity(instant)
Shared.HideName = self._hideName == true
for _, entry in ipairs(self._identity.Names) do
local isUser = entry.Kind == "User"
local realName = isUser and LocalPlayer.Name or LocalPlayer.DisplayName
if not entry.Bound then
entry.Bound = true
Shared.BindName(entry.Label, realName, isUser and function(text) return "@" .. text end or nil)
end
local shown = Shared.DisplayName(realName)
entry.Label.Text = isUser and ("@" .. shown) or shown
end
local duration = instant and 0 or 0.2
for _, entry in ipairs(self._identity.Avatars) do
tween(entry.Image, { ImageTransparency = self._hideAvatar and 1 or 0 }, duration)
tween(entry.Placeholder, { ImageTransparency = self._hideAvatar and 0.2 or 1 }, duration)
end
end
function Window:SetHideName(hidden)
self._hideName = hidden == true
self:_renderIdentity()
if self._hidePaint then self._hidePaint(0.2) end
pcall(Shared.Save)
end
function Window:SetHideAvatar(hidden)
self._hideAvatar = hidden == true
self:_renderIdentity()
end
local function avatar(window, parent, props, ringColor, ringTransparency, ringThickness)
local holder = create("Frame", {
BackgroundColor3 = Theme.Surface3,
BorderSizePixel = 0,
Parent = parent,
})
for key, value in pairs(props) do
holder[key] = value
end
corner(holder, UDim.new(1, 0))
stroke(holder, ringColor or Theme.Stroke, ringTransparency or 0, ringThickness or 1)
local image = create("ImageLabel", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
Image = "rbxthumb://type=AvatarHeadShot&id=" .. tostring(LocalPlayer.UserId) .. "&w=150&h=150",
Parent = holder,
})
corner(image, UDim.new(1, 0))
local placeholder = create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromScale(0.45, 0.45),
BackgroundTransparency = 1,
ImageColor3 = Theme.Muted,
ImageTransparency = 1,
ScaleType = Enum.ScaleType.Fit,
Parent = holder,
})
applyIcon(placeholder, "user")
window:_trackAvatar(image, placeholder)
return holder
end
function Window:_buildProfile(sidebar)
create("Frame", {
AnchorPoint = Vector2.new(0.5, 1),
Position = UDim2.new(0.5, 0, 1, -PROFILE_HEIGHT),
Size = UDim2.new(1, -20, 0, 1),
BackgroundColor3 = Theme.Stroke,
BorderSizePixel = 0,
Parent = sidebar,
})
local card = create("TextButton", {
AnchorPoint = Vector2.new(0.5, 1),
Position = UDim2.new(0.5, 0, 1, -8),
Size = UDim2.new(1, -16, 0, PROFILE_HEIGHT - 16),
BackgroundColor3 = Theme.Surface2,
BackgroundTransparency = 0,
Text = "",
AutoButtonColor = false,
Parent = sidebar,
})
corner(card, UDim.new(0, 8))
local cardStroke = stroke(card, Theme.Stroke, 0)
local head = avatar(self, card, {
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, 8, 0.5, 0),
Size = UDim2.fromOffset(32, 32),
})
local online = create("Frame", {
AnchorPoint = Vector2.new(1, 1),
Position = UDim2.new(1, 1, 1, 1),
Size = UDim2.fromOffset(9, 9),
BackgroundColor3 = Theme.Success,
BorderSizePixel = 0,
ZIndex = 2,
Parent = head,
})
corner(online, UDim.new(1, 0))
stroke(online, Theme.Surface2, 0, 2)
local name = label({
Position = UDim2.new(0, 48, 0.5, -15),
Size = UDim2.new(1, -104, 0, 15),
TextSize = 12,
FontFace = Fonts.Bold,
Parent = card,
})
self:_trackName(name, "Display")
local gameLabel = label({
Position = UDim2.new(0, 48, 0.5, 1),
Size = UDim2.new(1, -104, 0, 13),
Text = "…",
TextSize = 11,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
Parent = card,
})
local chevron = create("ImageLabel", {
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, -8, 0.5, 0),
Size = UDim2.fromOffset(14, 14),
BackgroundTransparency = 1,
ImageColor3 = Theme.Muted,
ScaleType = Enum.ScaleType.Fit,
Parent = card,
})
applyIcon(chevron, "chevron-right")
task.spawn(function()
gameLabel.Text = detectGameName()
end)
-- Hide name: Schalter unten links in der Profilkarte
local hideButton = create("TextButton", {
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, -28, 0.5, 0),
Size = UDim2.fromOffset(24, 24),
BackgroundColor3 = Theme.Surface3,
BackgroundTransparency = 0.2,
BorderSizePixel = 0,
Text = "",
AutoButtonColor = false,
ZIndex = 3,
Parent = card,
})
corner(hideButton, UDim.new(0, 6))
local hideStroke = stroke(hideButton)
local hideIcon = create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromOffset(13, 13),
BackgroundTransparency = 1,
ImageColor3 = Theme.Muted,
ScaleType = Enum.ScaleType.Fit,
ZIndex = 4,
Parent = hideButton,
})
applyIcon(hideIcon, "eye-off")
self._hidePaint = function(duration)
local on = self._hideName == true
tween(hideButton, { BackgroundColor3 = on and Theme.Accent or Theme.Surface3, BackgroundTransparency = on and 0.85 or 0.2 }, duration)
tween(hideStroke, { Color = on and Theme.Accent or Theme.Stroke, Transparency = on and 0.6 or 0 }, duration)
tween(hideIcon, { ImageColor3 = on and Theme.Accent or Theme.Muted }, duration)
end
self._hidePaint(0)
hideButton.MouseButton1Click:Connect(function()
self:SetHideName(not self._hideName)
end)
card.MouseEnter:Connect(function()
if usingTouch() then
return
end
tween(cardStroke, { Color = Theme.StrokeHover }, 0.12)
tween(chevron, { ImageColor3 = Theme.Text, Position = UDim2.new(1, -6, 0.5, 0) }, 0.15)
end)
card.MouseLeave:Connect(function()
tween(cardStroke, { Color = Theme.Stroke }, 0.2)
tween(chevron, { ImageColor3 = Theme.Muted, Position = UDim2.new(1, -8, 0.5, 0) }, 0.2)
end)
card.MouseButton1Click:Connect(function()
if self.Home then
self:SelectTab(self.Home)
end
end)
self.Profile = card
end
-- Server actions
function Window:Rejoin()
self:Notify({ Title = "Rejoining", Content = "Teleporting back into this server", Icon = "refresh-cw", Duration = 3 })
task.spawn(function()
local ok, err = pcall(function()
if #Players:GetPlayers() <= 1 or game.JobId == "" then
TeleportService:Teleport(game.PlaceId, LocalPlayer)
else
TeleportService:TeleportToPlaceInstance(game.PlaceId, game.JobId, LocalPlayer)
end
end)
if not ok then
self:Notify({ Title = "Rejoin failed", Content = tostring(err), Type = "Error", Duration = 4 })
end
end)
end
local function hopServers(window, order, pick, title)
task.spawn(function()
local servers = fetchServers(order)
if not servers then
window:Notify({ Title = title .. " failed", Content = "Could not load the server list", Type = "Error", Duration = 4 })
return
end
local candidates = {}
for _, server in ipairs(servers) do
if type(server) == "table" and server.id ~= game.JobId then
local playing, capacity = tonumber(server.playing), tonumber(server.maxPlayers)
if playing and capacity and playing < capacity then
table.insert(candidates, server)
end
end
end
local target = #candidates > 0 and pick(candidates) or nil
if not target then
window:Notify({ Title = title .. " failed", Content = "No other open server found", Type = "Warning", Duration = 4 })
return
end
window:Notify({
Title = title,
Content = string.format("Joining a server with %d/%d players", target.playing, target.maxPlayers),
Icon = "server",
Duration = 3,
})
local ok, err = pcall(function()
TeleportService:TeleportToPlaceInstance(game.PlaceId, target.id, LocalPlayer)
end)
if not ok then
window:Notify({ Title = title .. " failed", Content = tostring(err), Type = "Error", Duration = 4 })
end
end)
end
function Window:ServerHop()
hopServers(self, "Desc", function(list)
return list[math.random(1, #list)]
end, "Server hop")
end
function Window:JoinLowestServer()
hopServers(self, "Asc", function(list)
table.sort(list, function(a, b)
return a.playing < b.playing
end)
return list[1]
end, "Lowest server")
end
function Window:CopyToClipboard(text, what)
local ok = copyToClipboard(text)
self:Notify({
Title = ok and "Copied" or "Clipboard unavailable",
Content = ok and (what or tostring(text)) or "Your executor has no setclipboard",
Type = ok and "Success" or "Error",
Icon = ok and "clipboard-check" or nil,
Duration = 2.5,
})
return ok
end
-- Home
-- Home reads top to bottom: a banner greeting the player, a performance card
-- with live graphs, the server and links side by side, and a status line.
-- A Home card that grows with its stacked contents.
function Window:_homeCard(parent, order)
local card = panelCard(parent, 0, order)
card.AutomaticSize = Enum.AutomaticSize.Y
padding(card, 16, 16, 14, 14)
create("UIListLayout", { SortOrder = Enum.SortOrder.LayoutOrder, Parent = card })
return card
end
-- A small caps heading at the top of a Home card.
function Window:_homeCaption(parent, text, order)
return label({
Size = UDim2.new(1, 0, 0, 22),
Text = string.upper(text),
TextSize = 10,
TextColor3 = Theme.Muted,
TextYAlignment = Enum.TextYAlignment.Top,
LayoutOrder = order,
Parent = parent,
})
end
-- A rounded chip of an icon and short text. Returns the chip, its label and
-- its icon so callers can recolour them.
function Window:_homeChip(parent, order, icon, text, color)
local chip = create("TextButton", {
Size = UDim2.fromOffset(0, 24),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = Theme.Surface3,
BackgroundTransparency = 0.2,
BorderSizePixel = 0,
Text = "",
AutoButtonColor = false,
LayoutOrder = order,
Parent = parent,
})
corner(chip, UDim.new(0, 6))
local chipStroke = stroke(chip)
padding(chip, 8, 9)
create("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 6),
Parent = chip,
})
local image = create("ImageLabel", {
Size = UDim2.fromOffset(12, 12),
BackgroundTransparency = 1,
ImageColor3 = color or Theme.Muted,
ScaleType = Enum.ScaleType.Fit,
LayoutOrder = 1,
Parent = chip,
})
applyIcon(image, icon)
local text_ = label({
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
Text = text,
TextSize = 12,
TextColor3 = color or Theme.Muted,
TextTruncate = Enum.TextTruncate.None,
LayoutOrder = 2,
Parent = chip,
})
return chip, text_, image, chipStroke
end
-- A chip that flips on press: on, it takes the accent; off, it sits muted.
function Window:_homeToggleChip(parent, order, icon, text, initial, callback)
local chip, text_, image, chipStroke = self:_homeChip(parent, order, icon, text)
local on = initial == true
local function paint(duration)
tween(chip, { BackgroundColor3 = on and Theme.Accent or Theme.Surface3, BackgroundTransparency = on and 0.85 or 0.2 }, duration)
tween(chipStroke, { Color = on and Theme.Accent or Theme.Stroke, Transparency = on and 0.6 or 0 }, duration)
tween(text_, { TextColor3 = on and Theme.Accent or Theme.Muted }, duration)
tween(image, { ImageColor3 = on and Theme.Accent or Theme.Muted }, duration)
end
paint(0)
chip.MouseButton1Click:Connect(function()
on = not on
paint(0.2)
safeCall(callback, on)
end)
return chip
end
function Window:_homeProfile(list, opts, order)
local banner = panelCard(list, 140, order)
banner.ClipsDescendants = true
-- The avatar bust stands on the banner's right edge and fades in from
-- the left, so it reads as part of the card rather than a framed photo.
local bust = create("ImageLabel", {
AnchorPoint = Vector2.new(1, 1),
Position = UDim2.new(1, -16, 1, 0),
Size = UDim2.fromOffset(140, 140),
BackgroundTransparency = 1,
Image = "rbxthumb://type=AvatarBust&id=" .. tostring(LocalPlayer.UserId) .. "&w=420&h=420",
ScaleType = Enum.ScaleType.Fit,
Parent = banner,
})
create("UIGradient", {
Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 1),
NumberSequenceKeypoint.new(0.45, 0),
NumberSequenceKeypoint.new(1, 0),
}),
Parent = bust,
})
local bustPlaceholder = create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.6, 0.55),
Size = UDim2.fromOffset(40, 40),
BackgroundTransparency = 1,
ImageColor3 = Theme.Muted,
ImageTransparency = 1,
ScaleType = Enum.ScaleType.Fit,
Parent = bust,
})
applyIcon(bustPlaceholder, "user")
self:_trackAvatar(bust, bustPlaceholder)
local textWidth = UDim2.new(1, -190, 0, 0)
label({
Position = UDim2.fromOffset(22, 20),
Size = textWidth + UDim2.fromOffset(0, 14),
Text = string.upper(opts.Welcome or "Welcome back"),
TextSize = 10,
TextColor3 = Theme.Muted,
Parent = banner,
})
local nameLabel = label({
Position = UDim2.fromOffset(22, 35),
Size = textWidth + UDim2.fromOffset(0, 32),
TextSize = 26,
FontFace = Fonts.Bold,
Parent = banner,
})
self:_trackName(nameLabel, "Display")
local userLabel = label({
Position = UDim2.fromOffset(22, 67),
Size = textWidth + UDim2.fromOffset(0, 16),
TextSize = 13,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
Parent = banner,
})
self:_trackName(userLabel, "User")
-- Chips along the bottom: tier, time left, then the privacy toggles.
local chips = create("Frame", {
Position = UDim2.new(0, 22, 1, -40),
Size = UDim2.new(1, -44, 0, 24),
BackgroundTransparency = 1,
Parent = banner,
})
create("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 6),
Parent = chips,
})
local tier = opts.Tier
if tier == nil then
tier = "Free"
end
if tier then
local tierChip, tierText, tierIcon = self:_homeChip(chips, 1, opts.TierIcon or self._logoIcon, tostring(tier), Theme.Accent)
tierChip.Active = false
tierText.FontFace = Fonts.Bold
tierIcon.Size = UDim2.fromOffset(13, 13)
self.TierLabel = tierText
end
local expiryChip, expiryText = self:_homeChip(chips, 2, "clock", "")
expiryChip.Active = false
expiryChip.Visible = false
self:_homeToggleChip(chips, 4, "user-x", "Avatar", self._hideAvatar, function(on)
self:SetHideAvatar(on)
end)
return function()
local value = opts.Expiry
if type(value) == "function" then
local ok, result = pcall(value)
value = ok and result or nil
end
if type(value) == "number" then
local left = value - os.time()
expiryText.Text = left > 0 and (formatDuration(left) .. " left") or "Expired"
elseif value ~= nil then
expiryText.Text = tostring(value)
end
expiryChip.Visible = expiryText.Text ~= ""
end
end
-- A large live readout with a short bar history under it. Returns the value
-- label and a push(value, color) that adds a sample.
function Window:_homeGraph(parent, order, icon, title, ceiling)
local cell = create("Frame", {
BackgroundTransparency = 1,
LayoutOrder = order,
Parent = parent,
})
local holder = glowIcon(cell, icon, Theme.Muted, UDim2.new(0, 0, 0, 7))
holder.Size = UDim2.fromOffset(12, 12)
label({
Position = UDim2.fromOffset(18, 0),
Size = UDim2.new(1, -18, 0, 14),
Text = string.upper(title),
TextSize = 10,
TextColor3 = Theme.Muted,
Parent = cell,
})
local value = label({
Position = UDim2.fromOffset(0, 16),
Size = UDim2.new(1, 0, 0, 28),
Text = "…",
TextSize = 24,
FontFace = Fonts.Bold,
RichText = true,
Parent = cell,
})
local track = create("Frame", {
Position = UDim2.fromOffset(0, 50),
Size = UDim2.new(1, 0, 0, 26),
BackgroundTransparency = 1,
Parent = cell,
})
create("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Bottom,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 2),
Parent = track,
})
local COUNT = 24
local bars, samples = {}, {}
for index = 1, COUNT do
local bar = create("Frame", {
Size = UDim2.new(1 / COUNT, -2, 0, 2),
BackgroundColor3 = Theme.Surface3,
BorderSizePixel = 0,
LayoutOrder = index,
Parent = track,
})
corner(bar, UDim.new(0, 2))
bars[index] = bar
end
local function push(sample, color)
table.insert(samples, sample)
if #samples > COUNT then
table.remove(samples, 1)
end
local top = ceiling or 1
for _, s in ipairs(samples) do
top = math.max(top, s)
end
local offset = COUNT - #samples
for index, bar in ipairs(bars) do
local s = samples[index - offset]
local latest = index == COUNT
local height = s and math.max(s / top, 0.08) or 0
tween(bar, {
Size = UDim2.new(1 / COUNT, -2, height, s and 0 or 2),
BackgroundColor3 = (latest and color) or (s and Theme.Muted or Theme.Surface3),
BackgroundTransparency = latest and 0 or (s and 0.6 or 0),
}, 0.35, Enum.EasingStyle.Quint)
end
end
return value, push
end
-- A small figure: value over a caps caption.
function Window:_homeFigure(parent, order, title)
local cell = create("Frame", {
BackgroundTransparency = 1,
LayoutOrder = order,
Parent = parent,
})
local value = label({
Size = UDim2.new(1, -8, 0, 20),
Text = "…",
TextSize = 15,
FontFace = Fonts.Bold,
RichText = true,
Parent = cell,
})
label({
Position = UDim2.fromOffset(0, 21),
Size = UDim2.new(1, -8, 0, 14),
Text = string.upper(title),
TextSize = 10,
TextColor3 = Theme.Muted,
Parent = cell,
})
return value
end
-- A square tile button: an icon over a short label.
function Window:_homeAction(parent, order, icon, text, callback)
local button = create("TextButton", {
BackgroundColor3 = Theme.Surface3,
BackgroundTransparency = 0.5,
BorderSizePixel = 0,
Text = "",
AutoButtonColor = false,
ClipsDescendants = true,
LayoutOrder = order,
Parent = parent,
})
corner(button, UDim.new(0, 8))
local buttonStroke = stroke(button)
local image = create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0),
Position = UDim2.new(0.5, 0, 0, 11),
Size = UDim2.fromOffset(16, 16),
BackgroundTransparency = 1,
ImageColor3 = Theme.Text,
ScaleType = Enum.ScaleType.Fit,
Parent = button,
})
applyIcon(image, icon)
label({
Position = UDim2.new(0, 4, 0, 32),
Size = UDim2.new(1, -8, 0, 14),
Text = text,
TextSize = 11,
TextColor3 = Theme.Muted,
TextXAlignment = Enum.TextXAlignment.Center,
Parent = button,
})
button.MouseEnter:Connect(function()
tween(button, { BackgroundTransparency = 0 }, 0.15, Enum.EasingStyle.Quint)
tween(buttonStroke, { Color = Theme.StrokeHover }, 0.15)
tween(image, { ImageColor3 = Theme.Accent }, 0.15)
end)
button.MouseLeave:Connect(function()
tween(button, { BackgroundTransparency = 0.5 }, 0.25, Enum.EasingStyle.Quint)
tween(buttonStroke, { Color = Theme.Stroke }, 0.25)
tween(image, { ImageColor3 = Theme.Text }, 0.25)
end)
button.MouseButton1Click:Connect(function()
ripple(button)
safeCall(callback)
end)
return button
end
function Window:_homeGame(parent, order)
local card = self:_homeCard(parent, order)
local head = create("Frame", {
Size = UDim2.new(1, 0, 0, 46),
BackgroundTransparency = 1,
LayoutOrder = 1,
Parent = card,
})
local icon = create("ImageLabel", {
Size = UDim2.fromOffset(44, 44),
BackgroundColor3 = Theme.Surface3,
BorderSizePixel = 0,
Image = "rbxthumb://type=GameIcon&id=" .. tostring(game.GameId) .. "&w=150&h=150",
Parent = head,
})
corner(icon, UDim.new(0, 10))
stroke(icon)
local gameName = label({
Position = UDim2.fromOffset(56, 3),
Size = UDim2.new(1, -56, 0, 20),
Text = "Loading",
TextSize = 15,
FontFace = Fonts.Bold,
Parent = head,
})
local detail = label({
Position = UDim2.fromOffset(56, 24),
Size = UDim2.new(1, -56, 0, 16),
Text = "",
TextSize = 12,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
Parent = head,
})
local owner = nil
task.spawn(function()
local info = fetchGameInfo()
gameName.Text = info and info.Name or detectGameName()
owner = info and type(info.Creator) == "table" and info.Creator.Name or nil
end)
local actions = create("Frame", {
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
LayoutOrder = 2,
Parent = card,
})
padding(actions, 0, 0, 12, 0)
create("UIGridLayout", {
CellPadding = UDim2.fromOffset(6, 6),
CellSize = UDim2.new(0.25, -5, 0, 54),
SortOrder = Enum.SortOrder.LayoutOrder,
Parent = actions,
})
self:_homeAction(actions, 1, "refresh-cw", "Rejoin", function()
self:Rejoin()
end)
self:_homeAction(actions, 2, "shuffle", "Hop", function()
self:ServerHop()
end)
self:_homeAction(actions, 3, "users", "Lowest", function()
self:JoinLowestServer()
end)
self:_homeAction(actions, 4, "copy", "Job ID", function()
self:CopyToClipboard(game.JobId, "Job ID")
end)
label({
Size = UDim2.new(1, 0, 0, 26),
Text = "Job " .. shortId(game.JobId) .. " · Place " .. tostring(game.PlaceId),
TextSize = 11,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextYAlignment = Enum.TextYAlignment.Bottom,
LayoutOrder = 3,
Parent = card,
})
-- The detail line keeps the player count live.
return function()
local count = #Players:GetPlayers() .. "/" .. Players.MaxPlayers .. " players"
detail.Text = owner and ("by " .. owner .. " · " .. count) or count
end
end
-- One row of the links card: icon, title and text, and a round icon button.
function Window:_homeLink(card, order, icon, title, text, buttonIcon, callback)
local row = create("Frame", {
Size = UDim2.new(1, 0, 0, 46),
BackgroundTransparency = 1,
LayoutOrder = order,
Parent = card,
})
create("Frame", {
Name = "Rule",
Size = UDim2.new(1, 0, 0, 1),
BackgroundColor3 = Theme.Stroke,
BorderSizePixel = 0,
Parent = row,
})
local holder = glowIcon(row, icon, Theme.Accent, UDim2.new(0, 0, 0.5, 0))
holder.Size = UDim2.fromOffset(16, 16)
local titleLabel = label({
Position = UDim2.fromOffset(28, 6),
Size = UDim2.new(1, -68, 0, 18),
Text = title,
TextSize = 13,
Parent = row,
})
local textLabel = label({
Position = UDim2.fromOffset(28, 24),
Size = UDim2.new(1, -68, 0, 15),
Text = text,
TextSize = 12,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
Parent = row,
})
local button = create("TextButton", {
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, 0, 0.5, 0),
Size = UDim2.fromOffset(30, 30),
BackgroundColor3 = Theme.Surface3,
BackgroundTransparency = 0.5,
BorderSizePixel = 0,
Text = "",
AutoButtonColor = false,
Parent = row,
})
corner(button, UDim.new(0, 8))
local buttonStroke = stroke(button)
local image = create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromOffset(14, 14),
BackgroundTransparency = 1,
ImageColor3 = Theme.Muted,
ScaleType = Enum.ScaleType.Fit,
Parent = button,
})
applyIcon(image, buttonIcon)
button.MouseEnter:Connect(function()
tween(button, { BackgroundTransparency = 0 }, 0.15, Enum.EasingStyle.Quint)
tween(buttonStroke, { Color = Theme.StrokeHover }, 0.15)
tween(image, { ImageColor3 = Theme.Accent }, 0.15)
end)
button.MouseLeave:Connect(function()
tween(button, { BackgroundTransparency = 0.5 }, 0.25, Enum.EasingStyle.Quint)
tween(buttonStroke, { Color = Theme.Stroke }, 0.25)
tween(image, { ImageColor3 = Theme.Muted }, 0.25)
end)
button.MouseButton1Click:Connect(function()
safeCall(callback)
end)
return row, titleLabel, textLabel
end
-- Builds the links card into parent, or nothing when there are no links and
-- the feature list is off. Returns the card (or nil) and the feature refresh.
function Window:_homeLinks(parent, opts, order)
local links = {}
if opts.Discord then
table.insert(links, {
Icon = opts.DiscordIcon or "message-circle",
Title = opts.DiscordTitle or "Community",
Text = opts.Discord,
})
end
if opts.Website then
table.insert(links, {
Icon = opts.WebsiteIcon or "globe",
Title = opts.WebsiteTitle or "Website",
Text = opts.Website,
})
end
for _, link in ipairs(opts.Links or {}) do
table.insert(links, link)
end
local features = opts.Features
if #links == 0 and features == false then
return nil, function() end
end
local card = self:_homeCard(parent, order)
self:_homeCaption(card, "Links", 0)
for index, link in ipairs(links) do
local action = type(link.Callback) == "function"
self:_homeLink(card, index, link.Icon or "link", link.Title or "", link.Text or "", action and "arrow-up-right" or "copy", function()
if action then
safeCall(link.Callback)
else
self:CopyToClipboard(link.Copy or link.Text, link.Title)
end
end)
end
local refresh = function() end
if features ~= false then
if type(features) == "table" then
self._featuresTitle = features.Title
self._features = features.List or (#features > 0 and features or nil)
end
local _, _, text_ = self:_homeLink(card, 100,
type(features) == "table" and features.Icon or "list-checks",
type(features) == "table" and features.Title or "Feature list",
"", "chevron-right", function()
self:ShowFeatures()
end)
refresh = function()
local desc = type(features) == "table" and features.Desc
text_.Text = desc or (self:_featureSummary())
end
task.defer(refresh)
end
-- The first row needs no rule above it.
local first = nil
for _, child in ipairs(card:GetChildren()) do
if child:IsA("Frame") and child:FindFirstChild("Rule") and (not first or child.LayoutOrder < first.LayoutOrder) then
first = child
end
end
if first then
first.Rule.Visible = false
end
return card, refresh
end
-- A quiet status line under the cards: executor support on the left, the
-- hide key on the right.
function Window:_homeExecutor(list, opts, order)
local line = create("Frame", {
Size = UDim2.new(1, 0, 0, 26),
BackgroundTransparency = 1,
LayoutOrder = order,
Parent = list,
})
local executor, supported, message = executorReport(opts)
local dot = create("Frame", {
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, 4, 0.5, 0),
Size = UDim2.fromOffset(7, 7),
BackgroundColor3 = supported and Theme.Success or Theme.Warning,
BorderSizePixel = 0,
Parent = line,
})
corner(dot, UDim.new(1, 0))
local left = create("Frame", {
Position = UDim2.fromOffset(18, 0),
Size = UDim2.new(1, -170, 1, 0),
BackgroundTransparency = 1,
ClipsDescendants = true,
Parent = line,
})
create("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 8),
Parent = left,
})
label({
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
Text = executor,
TextSize = 12,
TextTruncate = Enum.TextTruncate.None,
LayoutOrder = 1,
Parent = left,
})
label({
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
Text = message,
TextSize = 12,
FontFace = Fonts.Regular,
TextColor3 = supported and Theme.Muted or Theme.Warning,
TextTruncate = Enum.TextTruncate.None,
LayoutOrder = 2,
Parent = left,
})
local hint = create("Frame", {
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, -2, 0.5, 0),
Size = UDim2.fromOffset(0, 22),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundTransparency = 1,
Parent = line,
})
create("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 6),
Parent = hint,
})
label({
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
Text = TOUCH and "Tap the pill or" or "Hide with",
TextSize = 12,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextTruncate = Enum.TextTruncate.None,
LayoutOrder = 1,
Parent = hint,
})
local chip = create("Frame", {
Size = UDim2.fromOffset(0, 20),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = Theme.Surface3,
BorderSizePixel = 0,
LayoutOrder = 2,
Parent = hint,
})
corner(chip, UDim.new(0, 5))
stroke(chip)
padding(chip, 6, 6)
self._keyChipLabel = label({
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
Text = keyName(self.Keybind),
TextSize = 11,
FontFace = Fonts.Bold,
TextTruncate = Enum.TextTruncate.None,
Parent = chip,
})
end
-- Feature list: every control the script adds, grouped by tab, or a list the
-- script writes itself. Items that match a control jump to it when picked.
Window._featureKinds = {
Toggle = true,
Slider = true,
Dropdown = true,
Input = true,
Keybind = true,
ColorPicker = true,
Stepper = true,
Button = true,
ButtonRow = true,
OrderList = true,
}
function Window:_featureGroups()
local found, byName = {}, {}
for _, tab in ipairs(self.Tabs) do
if tab ~= self.Home and tab._userVisible ~= false then
local group = { Name = tab.Name, Icon = tab._iconName, Items = {} }
for _, page in ipairs(tab._subTabs or { tab }) do
local sub = page._isSubTab and page or nil
local function add(name, element, box, path)
if type(name) ~= "string" or name == "" then
return
end
local item = {
Name = name,
Kind = element._type == "ButtonRow" and "Button" or element._type,
Path = path,
Result = { Tab = tab, Sub = sub, Target = element, Group = box },
}
table.insert(group.Items, item)
byName[string.lower(name)] = byName[string.lower(name)] or item
end
local function addElement(element, box, path)
if element._destroyed or not self._featureKinds[element._type] or (element._isShown and not element:_isShown()) then
return
end
if element._type == "ButtonRow" and element.Buttons then
for _, button in ipairs(element.Buttons) do
add(button.Label.Text, element, box, path)
end
else
add(element._searchName, element, box, path)
end
end
if not page._noSearch then
for _, element in ipairs(page._items or {}) do
addElement(element, nil, sub and sub.Name or nil)
end
for _, box in ipairs(page._groupboxes or {}) do
if box._userVisible ~= false and not box._manager then
for _, element in ipairs(box._items) do
addElement(element, box, sub and (sub.Name .. " › " .. box.Name) or box.Name)
end
end
end
end
end
if #group.Items > 0 then
table.insert(found, group)
end
end
end
local custom = self._features
if type(custom) ~= "table" then
return found
end
-- A written list: groups of items, or bare items, which gather into one
-- group where the first one appears.
local function customItem(entry)
if type(entry) ~= "table" then
entry = { Name = entry }
end
local name = tostring(entry.Name or entry.Title or entry[1] or "")
local link = byName[string.lower(name)]
return {
Name = name,
Desc = entry.Desc or entry.Description,
Tag = entry.Tag,
Icon = entry.Icon,
Kind = link and link.Kind,
Path = link and link.Path,
Result = link and link.Result,
Callback = entry.Callback,
}
end
local groups, loose = {}, nil
for _, entry in ipairs(custom) do
if type(entry) == "table" and type(entry.Items) == "table" then
local group = { Name = tostring(entry.Name or entry.Title or "Features"), Icon = entry.Icon, Items = {} }
for _, item in ipairs(entry.Items) do
table.insert(group.Items, customItem(item))
end
table.insert(groups, group)
else
if not loose then
loose = { Name = self._featuresTitle or "Features", Items = {} }
table.insert(groups, loose)
end
table.insert(loose.Items, customItem(entry))
end
end
return groups
end
function Window:_featureSummary()
local groups = self:_featureGroups()
local count = 0
for _, group in ipairs(groups) do
count += #group.Items
end
local text = count .. (count == 1 and " feature" or " features")
if type(self._features) ~= "table" and #groups > 0 then
text ..= " across " .. #groups .. (#groups == 1 and " tab" or " tabs")
end
return text, groups, count
end
-- Opens the feature list over the window. Search filters it; picking an item
-- linked to a control closes the list and jumps to that control.
function Window:ShowFeatures()
if self._dialog then
self._dialog.Close()
end
local summary, groups, total = self:_featureSummary()
local PAD = 16
local ROW = TOUCH and 44 or 38
local body = self.Body
local overlay = create("TextButton", {
Size = UDim2.fromScale(1, 1),
BackgroundColor3 = Color3.new(0, 0, 0),
BackgroundTransparency = 1,
Text = "",
AutoButtonColor = false,
ZIndex = 40,
Parent = body,
})
-- A canvas group so the whole list fades as one on open and close.
local card_ = create("CanvasGroup", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 0.5, 10),
Size = UDim2.fromOffset(420, 460),
BackgroundColor3 = Theme.Background,
BorderSizePixel = 0,
GroupTransparency = 1,
Active = true,
Parent = overlay,
})
corner(card_, UDim.new(0, 12))
local cardStroke = stroke(card_, Theme.Stroke, 1)
local cardScale = create("UIScale", { Scale = 0.94, Parent = card_ })
local function fit()
local scale = self.Scale.Scale
local size = body.AbsoluteSize / scale
card_.Size = UDim2.fromOffset(math.min(440, size.X - 24), math.min(480, size.Y - 24))
end
fit()
local fitConnection = body:GetPropertyChangedSignal("AbsoluteSize"):Connect(fit)
local iconHolder = glowIcon(card_, "list-checks", Theme.Accent, UDim2.new(0, PAD, 0, PAD + 10))
iconHolder.Size = UDim2.fromOffset(18, 18)
label({
Position = UDim2.fromOffset(PAD + 28, PAD),
Size = UDim2.new(1, -(PAD * 2 + 28 + 36), 0, 18),
Text = self._featuresTitle or "Features",
TextSize = 15,
Parent = card_,
})
label({
Position = UDim2.fromOffset(PAD + 28, PAD + 19),
Size = UDim2.new(1, -(PAD * 2 + 28 + 36), 0, 14),
Text = summary,
TextSize = 12,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
Parent = card_,
})
local closeButton = create("TextButton", {
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, -(PAD - 6), 0, PAD - 6),
Size = TOUCH and UDim2.fromOffset(40, 40) or UDim2.fromOffset(32, 32),
BackgroundColor3 = Theme.Surface3,
BackgroundTransparency = 1,
Text = "",
AutoButtonColor = false,
Parent = card_,
})
corner(closeButton, UDim.new(0, 8))
local closeIcon = create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromOffset(16, 16),
BackgroundTransparency = 1,
ImageColor3 = Theme.Muted,
ScaleType = Enum.ScaleType.Fit,
Parent = closeButton,
})
applyIcon(closeIcon, "x")
if closeIcon.Image == "" then
closeIcon:Destroy()
label({ Size = UDim2.fromScale(1, 1), Text = "×", TextSize = 20, TextColor3 = Theme.Muted, TextXAlignment = Enum.TextXAlignment.Center, Parent = closeButton })
end
closeButton.MouseEnter:Connect(function()
tween(closeButton, { BackgroundTransparency = 0.5 }, 0.12)
end)
closeButton.MouseLeave:Connect(function()
tween(closeButton, { BackgroundTransparency = 1 }, 0.2)
end)
local top = PAD + 44
local searchHolder = create("Frame", {
Position = UDim2.fromOffset(PAD, top),
Size = UDim2.new(1, -PAD * 2, 0, TOUCH and 36 or 32),
BackgroundColor3 = Theme.Surface,
BorderSizePixel = 0,
Parent = card_,
})
corner(searchHolder, UDim.new(0, 7))
local searchStroke = stroke(searchHolder)
local _, searchIcon = glowIcon(searchHolder, "search", Theme.Muted, UDim2.new(0, 10, 0.5, 0))
searchIcon.Parent.Size = UDim2.fromOffset(14, 14)
local searchBox = create("TextBox", {
Position = UDim2.fromOffset(32, 0),
Size = UDim2.new(1, -40, 1, 0),
BackgroundTransparency = 1,
Text = "",
PlaceholderText = "Search features",
PlaceholderColor3 = Theme.Muted,
TextColor3 = Theme.Text,
TextSize = 13,
FontFace = Fonts.Regular,
TextXAlignment = Enum.TextXAlignment.Left,
TextTruncate = Enum.TextTruncate.AtEnd,
ClearTextOnFocus = false,
Parent = searchHolder,
})
searchBox.Focused:Connect(function()
tween(searchStroke, { Color = Theme.StrokeHover }, 0.15)
tween(searchIcon, { ImageColor3 = Theme.Accent }, 0.15)
end)
searchBox.FocusLost:Connect(function()
tween(searchStroke, { Color = Theme.Stroke }, 0.2)
tween(searchIcon, { ImageColor3 = Theme.Muted }, 0.2)
end)
top += (TOUCH and 36 or 32) + 10
local scroller = create("ScrollingFrame", {
Position = UDim2.fromOffset(PAD, top),
Size = UDim2.new(1, -PAD * 2 + 6, 1, -(top + PAD - 4)),
BackgroundTransparency = 1,
BorderSizePixel = 0,
ScrollBarThickness = TOUCH and 4 or 3,
ScrollBarImageColor3 = Theme.Accent,
ScrollBarImageTransparency = 0.4,
ScrollingDirection = Enum.ScrollingDirection.Y,
CanvasSize = UDim2.new(),
Parent = card_,
})
local content = create("Frame", {
Size = UDim2.new(1, -10, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
Parent = scroller,
})
local layout = create("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 2),
Parent = content,
})
-- Canvas sized by hand, as elsewhere, so it scrolls under UIScale.
layout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(function()
scroller.CanvasSize = UDim2.fromOffset(0, layout.AbsoluteContentSize.Y / self.Scale.Scale + 4)
end)
local noResults = label({
Size = UDim2.new(1, 0, 0, 60),
Text = total == 0 and "No features yet" or "No matches",
TextSize = 13,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextXAlignment = Enum.TextXAlignment.Center,
LayoutOrder = 0,
Visible = total == 0,
Parent = content,
})
local dialog = {}
local closed = false
local stopEscape = nil
function dialog.Close()
if closed then
return
end
closed = true
if self._dialog == dialog then
self._dialog = nil
end
fitConnection:Disconnect()
if stopEscape then
-- Deferred: Close can run from inside the input dispatch loop.
task.defer(stopEscape)
end
if searchBox:IsFocused() then
searchBox:ReleaseFocus()
end
tween(overlay, { BackgroundTransparency = 1 }, 0.18)
tween(card_, { Position = UDim2.new(0.5, 0, 0.5, 8), GroupTransparency = 1 }, 0.18, Enum.EasingStyle.Quint)
tween(cardScale, { Scale = 0.96 }, 0.18, Enum.EasingStyle.Quint)
tween(cardStroke, { Transparency = 1 }, 0.12)
task.delay(0.2, function()
overlay:Destroy()
end)
end
local sections = {}
local order = 0
for _, group in ipairs(groups) do
order += 1
local header = create("Frame", {
Size = UDim2.new(1, 0, 0, 30),
BackgroundTransparency = 1,
LayoutOrder = order,
Parent = content,
})
local headIcon = glowIcon(header, group.Icon or "layout-grid", Theme.Accent, UDim2.new(0, 2, 0, 18))
headIcon.Size = UDim2.fromOffset(14, 14)
label({
Position = UDim2.fromOffset(24, 10),
Size = UDim2.new(1, -70, 0, 16),
Text = string.upper(group.Name),
TextSize = 11,
TextColor3 = Theme.Muted,
Parent = header,
})
local countLabel = label({
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, -4, 0, 10),
Size = UDim2.new(0, 40, 0, 16),
Text = tostring(#group.Items),
TextSize = 11,
TextColor3 = Theme.Muted,
TextXAlignment = Enum.TextXAlignment.Right,
Parent = header,
})
local section = { Header = header, Count = countLabel, Rows = {} }
for _, item in ipairs(group.Items) do
order += 1
local sub = item.Desc or item.Path
local clickable = item.Result ~= nil or type(item.Callback) == "function"
local row = create("TextButton", {
Size = UDim2.new(1, 0, 0, sub and ROW + 4 or ROW - 4),
BackgroundColor3 = Theme.Surface2,
BackgroundTransparency = 1,
BorderSizePixel = 0,
Text = "",
AutoButtonColor = false,
LayoutOrder = order,
Parent = content,
})
corner(row, UDim.new(0, 8))
local rowIcon = glowIcon(row, item.Icon or self._searchIcons[item.Kind] or "check", Theme.Muted, UDim2.new(0, 10, 0.5, 0))
rowIcon.Size = UDim2.fromOffset(14, 14)
local right = 12 + (clickable and 22 or 0)
local tag
if item.Tag then
tag = create("Frame", {
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, -(clickable and 30 or 10), 0.5, 0),
Size = UDim2.fromOffset(0, 20),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = Theme.Accent,
BackgroundTransparency = 0.85,
BorderSizePixel = 0,
Parent = row,
})
corner(tag, UDim.new(0, 5))
padding(tag, 7, 7)
label({
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
Text = tostring(item.Tag),
TextSize = 11,
TextColor3 = Theme.Accent,
TextTruncate = Enum.TextTruncate.None,
Parent = tag,
})
end
local nameLabel = label({
Position = UDim2.fromOffset(34, sub and 5 or 0),
Size = UDim2.new(1, -(34 + right), 0, sub and 18 or ROW - 4),
Text = item.Name,
TextSize = 13,
Parent = row,
})
local subLabel
if sub then
subLabel = label({
Position = UDim2.fromOffset(34, 23),
Size = UDim2.new(1, -(34 + right), 0, 14),
Text = sub,
TextSize = 11,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
Parent = row,
})
end
if tag then
local function reserveTag()
local width = 34 + right + tag.AbsoluteSize.X / self.Scale.Scale + 8
nameLabel.Size = UDim2.new(1, -width, 0, nameLabel.Size.Y.Offset)
if subLabel then
subLabel.Size = UDim2.new(1, -width, 0, 14)
end
end
tag:GetPropertyChangedSignal("AbsoluteSize"):Connect(reserveTag)
task.defer(reserveTag)
end
if clickable then
local go = create("ImageLabel", {
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, -10, 0.5, 0),
Size = UDim2.fromOffset(14, 14),
BackgroundTransparency = 1,
ImageColor3 = Theme.Muted,
ImageTransparency = 0.4,
ScaleType = Enum.ScaleType.Fit,
Parent = row,
})
applyIcon(go, "arrow-up-right")
end
local isTap = tapGuard(row, function()
return scroller
end)
row.MouseEnter:Connect(function()
if not usingTouch() then
tween(row, { BackgroundTransparency = 0.3 }, 0.12)
end
end)
row.MouseLeave:Connect(function()
tween(row, { BackgroundTransparency = 1 }, 0.2)
end)
row.MouseButton1Click:Connect(function()
if not clickable or not isTap() then
return
end
dialog.Close()
if item.Result then
self:_revealResult(item.Result)
end
safeCall(item.Callback)
end)
table.insert(section.Rows, { Frame = row, Key = string.lower(item.Name .. " " .. (sub or "")) })
end
table.insert(sections, section)
end
searchBox:GetPropertyChangedSignal("Text"):Connect(function()
local query = string.lower(searchBox.Text)
local any = false
for _, section in ipairs(sections) do
local shown = 0
for _, row in ipairs(section.Rows) do
local match = query == "" or string.find(row.Key, query, 1, true) ~= nil
row.Frame.Visible = match
if match then
shown += 1
end
end
section.Header.Visible = shown > 0
section.Count.Text = tostring(shown)
any = any or shown > 0
end
noResults.Visible = not any
scroller.CanvasPosition = Vector2.zero
end)
closeButton.MouseButton1Click:Connect(dialog.Close)
overlay.MouseButton1Click:Connect(dialog.Close)
stopEscape = self:_listen("Began", function(input)
if input.KeyCode == Enum.KeyCode.Escape then
dialog.Close()
end
end)
self._dialog = dialog
tween(overlay, { BackgroundTransparency = 0.45 }, 0.25)
tween(card_, { Position = UDim2.fromScale(0.5, 0.5), GroupTransparency = 0 }, 0.3, Enum.EasingStyle.Quint)
tween(cardScale, { Scale = 1 }, 0.4, Enum.EasingStyle.Back)
return dialog
end
function Window:_homeStats(list, opts, order, tab)
local show = opts.Stats or { "Players", "Execs", "Session", "FPS", "Ping" }
-- FPS and ping get large readouts with a bar history; every other stat
-- is a small figure in the row under them.
local card = self:_homeCard(list, order)
local graphs = create("Frame", {
Size = UDim2.new(1, 0, 0, 76),
BackgroundTransparency = 1,
LayoutOrder = 1,
Parent = card,
})
create("UIGridLayout", {
CellPadding = UDim2.fromOffset(24, 0),
CellSize = UDim2.new(0.5, -12, 0, 76),
SortOrder = Enum.SortOrder.LayoutOrder,
Parent = graphs,
})
local rule = create("Frame", {
Size = UDim2.new(1, 0, 0, 1),
BackgroundColor3 = Theme.Stroke,
BorderSizePixel = 0,
LayoutOrder = 2,
Parent = card,
})
local figures = create("Frame", {
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
LayoutOrder = 3,
Parent = card,
})
padding(figures, 0, 0, 12, 0)
local figureGrid = create("UIGridLayout", {
CellPadding = UDim2.fromOffset(0, 8),
CellSize = UDim2.new(0.25, 0, 0, 36),
SortOrder = Enum.SortOrder.LayoutOrder,
Parent = figures,
})
local values, sparks = {}, {}
local graphCount, figureCount = 0, 0
for index, name in ipairs(show) do
local kind = STAT_TYPES[name]
if kind and not values[name] then
if name == "FPS" or name == "Ping" then
graphCount += 1
values[name], sparks[name] = self:_homeGraph(graphs, index, kind[1], kind[2], name == "FPS" and 60 or 100)
else
figureCount += 1
values[name] = self:_homeFigure(figures, index, kind[2])
end
end
end
graphs.Visible = graphCount > 0
figures.Visible = figureCount > 0
rule.Visible = graphCount > 0 and figureCount > 0
if graphCount == 0 then
padding(figures, 0, 0, 0, 0)
end
local function fitFigures()
local width = figures.AbsoluteSize.X / self.Scale.Scale
local columns = math.clamp(figureCount, 1, width >= 440 and 5 or 3)
figureGrid.CellSize = UDim2.new(1 / columns, 0, 0, 36)
end
figures:GetPropertyChangedSignal("AbsoluteSize"):Connect(fitFigures)
task.defer(fitFigures)
if values.Executor then
values.Executor.Text = detectExecutor()
end
if values.Game then
values.Game.Text = "Loading"
task.spawn(function()
values.Game.Text = detectGameName()
end)
end
if values.Region then
values.Region.Text = "Loading"
detectRegion(function(region)
values.Region.Text = region
end)
end
if values.Execs then
local executions = bumpExecutions(opts.StatsFolder or self.ConfigFolder)
values.Execs.Text = executions and tostring(executions) or "—"
end
if values.Friends then
local friends = {}
local function recount()
local total = 0
for _, player in ipairs(Players:GetPlayers()) do
if player ~= LocalPlayer then
local known = friends[player.UserId]
if known == nil then
local ok, result = pcall(function()
return LocalPlayer:IsFriendsWith(player.UserId)
end)
known = ok and result == true
friends[player.UserId] = known
end
if known then
total += 1
end
end
end
values.Friends.Text = tostring(total)
end
task.spawn(recount)
table.insert(self._connections, Players.PlayerAdded:Connect(function()
task.spawn(recount)
end))
table.insert(self._connections, Players.PlayerRemoving:Connect(function()
task.defer(recount)
end))
end
local session = values.Session or values.Uptime
local started = os.clock()
local frames = 0
self:_listen("Render", function()
frames += 1
end)
-- FPS, ping and memory are tinted by how healthy they are, unless the
-- script turns that off with StatColors = false.
local tinted = opts.StatColors ~= false
local function tint(valueLabel, color)
if tinted and valueLabel.TextColor3 ~= color then
tween(valueLabel, { TextColor3 = color }, 0.3)
end
end
return function()
if values.FPS then
values.FPS.Text = tostring(frames)
tint(values.FPS, Library._healthColor(frames, 50, 30))
sparks.FPS(frames, Library._healthColor(frames, 50, 30))
end
frames = 0
if values.Ping then
local ok, ping = pcall(function()
return math.floor(LocalPlayer:GetNetworkPing() * 1000)
end)
ping = ok and ping or 0
values.Ping.Text = ping .. "ms"
tint(values.Ping, Library._healthColor(ping, 90, 180, true))
sparks.Ping(ping, Library._healthColor(ping, 90, 180, true))
end
if values.Players then
values.Players.Text = #Players:GetPlayers() .. "/" .. Players.MaxPlayers
end
if session then
session.Text = formatDuration(os.clock() - started)
end
if values.Time then
values.Time.Text = os.date(opts.TimeFormat or "%H:%M")
end
if values.ServerAge then
values.ServerAge.Text = formatDuration(workspace.DistributedGameTime)
end
if values.Memory then
local ok, memory = pcall(function()
return StatsService:GetTotalMemoryUsageMb()
end)
values.Memory.Text = ok and string.format("%d MB", memory) or "—"
tint(values.Memory, ok and Library._healthColor(memory, 1500, 3000, true) or Theme.Text)
end
end, function()
frames = 0
end
end
local function homePage(sub, page)
local list = sub.List
if type(page.Content) == "string" then
local card_ = panelCard(list, 0, 1)
card_.AutomaticSize = Enum.AutomaticSize.Y
padding(card_, 16, 16, 14, 16)
label({
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
Text = page.Content,
TextSize = 13,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextWrapped = true,
TextTruncate = Enum.TextTruncate.None,
TextYAlignment = Enum.TextYAlignment.Top,
Parent = card_,
})
end
if type(page.Entries) == "table" then
for index, entry in ipairs(page.Entries) do
local card_ = panelCard(list, 0, index + 1)
card_.AutomaticSize = Enum.AutomaticSize.Y
padding(card_, 16, 16, 14, 16)
create("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 6),
Parent = card_,
})
local head = create("Frame", {
Size = UDim2.new(1, 0, 0, 20),
BackgroundTransparency = 1,
LayoutOrder = 1,
Parent = card_,
})
label({
Size = UDim2.new(1, -90, 1, 0),
Text = entry.Title or entry.Version or "Update",
TextSize = 15,
Parent = head,
})
if entry.Date or entry.Tag then
local chip = create("Frame", {
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, 0, 0.5, 0),
Size = UDim2.fromOffset(0, 20),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = Theme.Surface,
BorderSizePixel = 0,
Parent = head,
})
corner(chip, UDim.new(0, 5))
stroke(chip)
padding(chip, 8, 8)
label({
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
Text = entry.Tag or entry.Date,
TextSize = 11,
TextColor3 = Theme.Muted,
TextTruncate = Enum.TextTruncate.None,
Parent = chip,
})
end
local body = entry.Content or entry.Body
if type(entry.Changes) == "table" then
body = "• " .. table.concat(entry.Changes, "\n• ")
end
if body then
label({
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
Text = body,
TextSize = 13,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextWrapped = true,
TextTruncate = Enum.TextTruncate.None,
TextYAlignment = Enum.TextYAlignment.Top,
LayoutOrder = 2,
Parent = card_,
})
end
end
end
if type(page.Build) == "function" then
safeCall(page.Build, sub, list)
else
sub._noSearch = true
end
end
function Window:_buildHome(opts)
self._hideName = opts.HideName == true or Shared.HideName == true
self._hideAvatar = opts.HideAvatar == true
self:_renderIdentity(true)
local tab = self:Tab({
Name = opts.Name or "Home",
Desc = opts.Desc,
Icon = opts.Icon or "house",
PageTitle = opts.Title or "Home",
})
tab._order = 100
self.Home = tab
local pages = opts.Pages or opts.Tabs
local overview = tab
if type(pages) == "table" and #pages > 0 then
overview = tab:SubTab({ Name = opts.OverviewName or "Overview", Icon = opts.TabIcon or "layout-grid" })
end
overview._noSearch = true
overview._order = 100
local list = overview.List
local refreshExpiry = self:_homeProfile(list, opts, 1)
local refreshStats, resetStats = self:_homeStats(list, opts, 2, tab)
-- Server and links side by side, stacked when narrow.
local split = create("Frame", {
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
LayoutOrder = 3,
Parent = list,
})
local splitLayout = create("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 10),
Parent = split,
})
local function column(order_)
local frame = create("Frame", {
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
LayoutOrder = order_,
Parent = split,
})
create("UIListLayout", { SortOrder = Enum.SortOrder.LayoutOrder, Parent = frame })
return frame
end
local leftColumn, rightColumn = column(1), column(2)
local refreshGame = self:_homeGame(leftColumn, 1)
local linksCard, refreshFeatures = self:_homeLinks(rightColumn, opts, 1)
if not linksCard then
rightColumn:Destroy()
end
local function fitSplit()
local wide = linksCard ~= nil and split.AbsoluteSize.X / self.Scale.Scale >= 540
splitLayout.FillDirection = wide and Enum.FillDirection.Horizontal or Enum.FillDirection.Vertical
local size = wide and UDim2.new(0.5, -5, 0, 0) or UDim2.new(1, 0, 0, 0)
leftColumn.Size = size
if linksCard then
rightColumn.Size = size
end
end
split:GetPropertyChangedSignal("AbsoluteSize"):Connect(fitSplit)
task.defer(fitSplit)
self:_homeExecutor(list, opts, 4)
if type(pages) == "table" then
for _, page in ipairs(pages) do
homePage(tab:SubTab({ Name = page.Name or page.Title or "Page", Icon = page.Icon }), page)
end
end
refreshExpiry()
refreshStats()
refreshGame()
task.spawn(function()
while not self._destroyed and tab._page.Parent do
task.wait(1)
if self.Open and self.CurrentTab == tab then
refreshStats()
refreshExpiry()
refreshGame()
refreshFeatures()
else
resetStats()
end
end
end)
return tab
end
function Window:Tab(opts, icon)
opts = normalize(opts, { Title = "Name", Description = "Desc" })
if icon ~= nil and opts.Icon == nil then
opts.Icon = icon
end
local tab = setmetatable({
Name = opts.Name or "Tab",
Window = self,
_order = 0,
}, Tab)
local button = create("TextButton", {
Size = UDim2.new(1, 0, 0, TAB_TILE_HEIGHT),
BackgroundColor3 = Theme.Surface3,
BackgroundTransparency = 1,
Text = "",
AutoButtonColor = false,
LayoutOrder = #self.Tabs + 1,
Parent = self._tabStack,
})
corner(button, UDim.new(0, 7))
local buttonStroke = stroke(button, Theme.Stroke, 1)
buttonStroke.Enabled = false
tab._button = button
local hasIcon = opts.Icon ~= nil
if hasIcon then
local iconImage = create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0, 20, 0.5, 0),
Size = UDim2.fromOffset(16, 16),
BackgroundTransparency = 1,
ImageColor3 = Theme.Muted,
ScaleType = Enum.ScaleType.Fit,
Parent = button,
})
applyIcon(iconImage, opts.Icon)
if iconImage:GetAttribute("CustomIcon") then
iconImage.ImageTransparency = 0.4
end
tab._icon = iconImage
end
tab._label = label({
Position = UDim2.fromOffset(hasIcon and 38 or 14, 0),
Size = UDim2.new(1, hasIcon and -46 or -22, 1, 0),
Text = tab.Name,
TextSize = 13,
TextColor3 = Theme.Muted,
Parent = button,
})
local page = create("Frame", {
Name = tab.Name,
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
Visible = false,
Parent = self.Content,
})
tab._page = page
local titleX = 24
if hasIcon then
local holder = glowIcon(page, opts.Icon, Theme.Accent, UDim2.new(0, 24, 0, 32))
holder.Size = UDim2.fromOffset(20, 20)
titleX = 54
end
tab._titleX = titleX
tab._title = label({
Position = UDim2.fromOffset(titleX, 20),
Size = UDim2.new(1, -titleX, 0, 24),
Text = opts.PageTitle or tab.Name,
TextSize = 22,
Parent = page,
})
if opts.Desc then
tab._desc = label({
Position = UDim2.fromOffset(titleX, 44),
Size = UDim2.new(1, -titleX, 0, 16),
Text = opts.Desc,
TextSize = 13,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
Parent = page,
})
end
local listTop = opts.Desc and 70 or 58
tab._headerBottom = listTop
tab._iconName = opts.Icon
buildContainer(tab, page, listTop, opts.Icon or "layout-grid", opts.EmptyText or "Nothing here yet")
local pressScale = create("UIScale", { Parent = button })
local function release()
tween(pressScale, { Scale = 1 }, 0.3, Enum.EasingStyle.Quint)
end
button.MouseEnter:Connect(function()
if self.CurrentTab ~= tab and not usingTouch() then
tween(button, { BackgroundTransparency = 0.75 }, 0.2, Enum.EasingStyle.Quint)
tween(tab._label, { TextColor3 = Theme.Text }, 0.2)
if tab._icon then
tintIcon(tab._icon, Theme.Text, true, 0.2)
end
end
end)
button.MouseLeave:Connect(function()
release()
if self.CurrentTab ~= tab then
tween(button, { BackgroundTransparency = 1 }, 0.3, Enum.EasingStyle.Quint)
tween(tab._label, { TextColor3 = Theme.Muted }, 0.3)
if tab._icon then
tintIcon(tab._icon, Theme.Muted, false, 0.3)
end
end
end)
button.MouseButton1Down:Connect(function()
tween(pressScale, { Scale = 0.97 }, 0.15, Enum.EasingStyle.Quint)
end)
button.MouseButton1Up:Connect(release)
local isTap = tapGuard(button, function()
return self.TabList
end)
button.MouseButton1Click:Connect(function()
release()
if isTap() then
self:SelectTab(tab)
end
end)
tab._stroke = buttonStroke
self:_fitTitle(tab)
if not self._introDone then
button.Visible = false
end
table.insert(self.Tabs, tab)
if #self.Tabs == 1 then
task.defer(function()
if not self.CurrentTab then
self:SelectTab(tab)
end
end)
end
return tab
end
Window.CreateTab = Window.Tab
-- Cloud configs: a tab for browsing, previewing, installing and publishing
-- configs other players shared. The library draws it; OnFetch, OnPublish and
-- the other callbacks are the script's backend. Favorites, installs and the
-- sort order are remembered in the script's settings file.
function Window:CloudConfigs(opts)
opts = normalize(opts, { Title = "Name", Description = "Desc" })
local window = self
local tab = self:Tab({
Name = opts.Name or "Cloud",
Desc = opts.Desc,
Icon = opts.Icon or "cloud",
PageTitle = opts.PageTitle,
})
tab._noSearch = true
tab.List.Visible = false
if tab._empty then
tab._empty:Destroy()
tab._empty = nil
end
local CARD = 116
local ROW = TOUCH and 36 or 32
local CHIP = TOUCH and 30 or 26
local SORTS = { "Popular", "New", "Top", "Installs" }
local SORT_NAMES = { Popular = "Popular", New = "Newest", Top = "Top rated", Installs = "Most installed" }
local FILTERS = { "All", "Favorites", "Mine", "Installed" }
local NAME_LIMIT = 40
local DESC_LIMIT = math.max(math.floor(tonumber(opts.DescriptionLimit) or 300), 20)
local pageSize = math.max(math.floor(tonumber(opts.PageSize) or 20), 1)
local tagList = type(opts.Tags) == "table" and opts.Tags or {}
local maxTags = math.max(math.floor(tonumber(opts.MaxTags) or 3), 0)
local anonName = tostring(opts.StreamerName or "Ouroboros User")
local cloud = { Tab = tab }
local records, byId = {}, {}
local cards = {}
local hasMore, page, token = false, 0, 0
local loading = false
local query = { Search = "", Sort = "Popular", Filter = "All", Tag = nil }
local views, currentView = {}, nil
local detail, detailRecord, detailToken = {}, nil, 0
local stored = window:_readConfigSettings()
local favorites = type(stored.CloudFavorites) == "table" and stored.CloudFavorites or {}
local installed = type(stored.CloudInstalled) == "table" and stored.CloudInstalled or {}
if table.find(SORTS, stored.CloudSort) then
query.Sort = stored.CloudSort
elseif table.find(SORTS, opts.Sort) then
query.Sort = opts.Sort
end
local function persist(key, value)
window:_writeConfigSettings({ [key] = value })
end
local function notify(ok, title, detail_)
window:Notify({ Title = title, Content = detail_, Type = ok and "Success" or "Error", Duration = 3 })
end
-- The Home tab's hide-name switch doubles as streamer mode.
local function streamerOn()
return window._hideName == true
end
-- Records
local function countFlags(code)
local data = type(code) == "string" and window:DecodeConfig(code)
if not data then
return nil
end
local count = 0
for _ in pairs(data) do
count += 1
end
return count
end
local function compact(number)
number = math.max(math.floor(tonumber(number) or 0), 0)
if number >= 1e6 then
return (("%.1f"):format(number / 1e6):gsub("%.0$", "")) .. "m"
elseif number >= 1e3 then
return (("%.1f"):format(number / 1e3):gsub("%.0$", "")) .. "k"
end
return tostring(number)
end
local function ago(stamp)
stamp = tonumber(stamp)
if not stamp then
return nil
end
local delta = os.time() - stamp
if delta < 60 then
return "just now"
elseif delta < 3600 then
return math.floor(delta / 60) .. "m ago"
elseif delta < 86400 then
return math.floor(delta / 3600) .. "h ago"
elseif delta < 604800 then
return math.floor(delta / 86400) .. "d ago"
elseif delta < 2592000 then
return math.floor(delta / 604800) .. "w ago"
end
return os.date("%d %b %Y", stamp)
end
local function escape(text)
return (text:gsub("&", "&"):gsub("<", "<"):gsub(">", ">"):gsub('"', """):gsub("'", "'"))
end
-- Rich text with every match of the search drawn in the accent.
local function highlight(text)
local needle = string.lower(query.Search)
if needle == "" then
return escape(text)
end
local lower = string.lower(text)
local hex = colorToHex(Theme.Accent)
local out, from = {}, 1
while true do
local first, last = string.find(lower, needle, from, true)
if not first then
break
end
table.insert(out, escape(text:sub(from, first - 1)))
table.insert(out, ('%s'):format(hex, escape(text:sub(first, last))))
from = last + 1
end
table.insert(out, escape(text:sub(from)))
return table.concat(out)
end
local function normalizeRecord(raw)
if type(raw) ~= "table" then
return nil
end
local id = raw.Id ~= nil and tostring(raw.Id) or HttpService:GenerateGUID(false)
local record = byId[id] or { Id = id }
for key, value in pairs(raw) do
if key ~= "Id" then
record[key] = value
end
end
record.Name = tostring(record.Name or "Untitled")
record.Author = tostring(record.Author or "Unknown")
record.Description = record.Description ~= nil and tostring(record.Description) or ""
local tags = {}
for _, tag in ipairs(type(record.Tags) == "table" and record.Tags or {}) do
table.insert(tags, tostring(tag))
end
record.Tags = tags
record.Installs = tonumber(record.Installs) or 0
record.Likes = tonumber(record.Likes) or 0
record.Updated = tonumber(record.Updated or record.Created)
record.Liked = record.Liked == true
if type(record.Code) ~= "string" or record.Code == "" then
record.Code = nil
end
record.Count = tonumber(record.Count) or countFlags(record.Code)
record.Mine = record.Mine == true or tonumber(record.AuthorId) == LocalPlayer.UserId or tonumber(record.OwnerId) == LocalPlayer.UserId
byId[id] = record
return record
end
local function publicRecord(record)
return {
Id = record.Id,
Name = record.Name,
Description = record.Description,
Author = record.Author,
AuthorId = record.AuthorId,
Tags = table.clone(record.Tags),
Installs = record.Installs,
Likes = record.Likes,
Liked = record.Liked,
Updated = record.Updated,
Code = record.Code,
Count = record.Count,
Folder = record.Folder,
Mine = record.Mine,
}
end
-- What is kept locally for favorites and installs: no code, it is refetched.
local function summary(record)
local kept = publicRecord(record)
kept.Code = nil
kept.Liked = nil
return kept
end
local function installState(record)
local mark = installed[record.Id]
if type(mark) ~= "table" then
return nil
end
if record.Updated and tonumber(mark.Updated) and record.Updated > tonumber(mark.Updated) then
return "Update"
end
return "Installed"
end
local function otherScript(record)
return type(record.Folder) == "string" and record.Folder ~= window.ConfigFolder
end
-- Small pieces
local function iconButton(parent, icon, glyph, size, plain)
local button = create("TextButton", {
Size = UDim2.fromOffset(size, size),
BackgroundColor3 = Theme.Surface2,
BackgroundTransparency = plain and 1 or 0,
Text = "",
AutoButtonColor = false,
Parent = parent,
})
corner(button, UDim.new(0, 8))
if not plain then
hoverStroke(button, stroke(button))
end
local image = create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromOffset(15, 15),
BackgroundTransparency = 1,
ImageColor3 = Theme.Muted,
ScaleType = Enum.ScaleType.Fit,
Parent = button,
})
applyIcon(image, icon)
if image.Image == "" then
image:Destroy()
image = label({
Size = UDim2.fromScale(1, 1),
Text = glyph,
TextSize = 15,
TextColor3 = Theme.Muted,
TextXAlignment = Enum.TextXAlignment.Center,
TextTruncate = Enum.TextTruncate.None,
Parent = button,
})
end
return button, image
end
local function primary(button, text_)
tween(button, { BackgroundColor3 = Theme.Accent, BackgroundTransparency = 0.12 }, 0)
tween(text_, { TextColor3 = Theme.AccentDark }, 0)
text_.FontFace = Fonts.Bold
end
local function clear(parent)
for _, child in ipairs(parent:GetChildren()) do
if child:IsA("GuiObject") then
child:Destroy()
end
end
end
local function chip(parent, text, order, onClick)
local button = create("TextButton", {
Size = UDim2.fromOffset(0, CHIP),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = Theme.Surface2,
Text = "",
AutoButtonColor = false,
LayoutOrder = order,
Parent = parent,
})
button:SetAttribute("NoDrag", true)
corner(button, UDim.new(1, 0))
local chipStroke = stroke(button)
padding(button, 12, 12)
local text_ = label({
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
Text = text,
TextSize = 12,
TextColor3 = Theme.Muted,
TextTruncate = Enum.TextTruncate.None,
Parent = button,
})
local object = { Button = button, Label = text_ }
function object.Set(on, instant)
local duration = instant and 0 or 0.15
tween(button, { BackgroundColor3 = on and Theme.Accent or Theme.Surface2 }, duration)
tween(text_, { TextColor3 = on and Theme.AccentDark or Theme.Muted }, duration)
tween(chipStroke, { Transparency = on and 1 or 0 }, duration)
end
if onClick then
button.MouseButton1Click:Connect(onClick)
else
button.Active = false
end
return object
end
local function tagPill(parent, text, order)
local pill = label({
Size = UDim2.fromOffset(0, 18),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = Theme.Surface3,
BackgroundTransparency = 0.2,
Text = text,
TextSize = 11,
TextColor3 = Theme.Muted,
TextXAlignment = Enum.TextXAlignment.Center,
TextTruncate = Enum.TextTruncate.None,
LayoutOrder = order,
Parent = parent,
})
corner(pill, UDim.new(0, 5))
padding(pill, 7, 7)
return pill
end
local function hlist(parent, gap, wraps)
return create("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, gap),
Wraps = wraps == true,
Parent = parent,
})
end
local function vlist(parent, gap)
return create("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, gap),
Parent = parent,
})
end
local function wrapped(parent, text, size, color, order, font)
return label({
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
Text = text,
TextSize = size,
FontFace = font or Fonts.Regular,
TextColor3 = color or Theme.Text,
TextWrapped = true,
TextTruncate = Enum.TextTruncate.None,
TextYAlignment = Enum.TextYAlignment.Top,
LayoutOrder = order,
Parent = parent,
})
end
local function scroller(parent, visible)
local frame = create("ScrollingFrame", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
BorderSizePixel = 0,
ScrollBarThickness = 3,
ScrollBarImageColor3 = Theme.Accent,
ScrollBarImageTransparency = 0.4,
ScrollingDirection = Enum.ScrollingDirection.Y,
AutomaticCanvasSize = Enum.AutomaticSize.Y,
CanvasSize = UDim2.new(),
Visible = visible ~= false,
Parent = parent,
})
frame:SetAttribute("NoDrag", true)
return frame
end
-- A text box on the surface colour. Multi-line boxes show a counter.
local function field(parent, order, placeholder, height, limit)
local multi = height > ROW
local holder = create("Frame", {
Size = UDim2.new(1, 0, 0, height),
BackgroundColor3 = Theme.Surface,
BorderSizePixel = 0,
ClipsDescendants = true,
LayoutOrder = order,
Parent = parent,
})
corner(holder, UDim.new(0, 8))
local holderStroke = stroke(holder)
local box = create("TextBox", {
Position = UDim2.fromOffset(10, multi and 8 or 0),
Size = UDim2.new(1, -20, 1, multi and -24 or 0),
BackgroundTransparency = 1,
Text = "",
PlaceholderText = placeholder,
PlaceholderColor3 = Theme.Muted,
TextColor3 = Theme.Text,
TextSize = 13,
FontFace = Fonts.Regular,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = multi and Enum.TextYAlignment.Top or Enum.TextYAlignment.Center,
MultiLine = multi,
TextWrapped = multi,
ClearTextOnFocus = false,
Parent = holder,
})
local counter = multi and label({
AnchorPoint = Vector2.new(1, 1),
Position = UDim2.new(1, -10, 1, -4),
Size = UDim2.fromOffset(0, 14),
AutomaticSize = Enum.AutomaticSize.X,
Text = "",
TextSize = 11,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextTruncate = Enum.TextTruncate.None,
Parent = holder,
})
box.Focused:Connect(function()
tween(holderStroke, { Color = Theme.StrokeHover }, 0.15)
end)
box.FocusLost:Connect(function()
tween(holderStroke, { Color = Theme.Stroke }, 0.2)
end)
box:GetPropertyChangedSignal("Text"):Connect(function()
local text = box.Text
local length = utf8.len(text) or #text
if limit and length > limit then
local cut = utf8.offset(text, limit + 1)
box.Text = cut and text:sub(1, cut - 1) or text:sub(1, limit)
return
end
if counter then
counter.Text = limit and (length .. "/" .. limit) or ""
end
end)
if counter and limit then
counter.Text = "0/" .. limit
end
return box, holderStroke
end
local function uniqueName(base)
base = trim((tostring(base or ""):gsub('[\\/:%*%?"<>|]', "")))
if base == "" then
base = "Cloud config"
end
local name, index = base, 2
while window:ConfigExists(name) do
name = ("%s (%d)"):format(base, index)
index += 1
end
return name
end
-- Layout: the page holds three views that slide over each other.
local top = tab._headerBottom
-- One pixel wider than the content on every side: root clips the slide,
-- and the strokes along its edges would be clipped with it.
local root = create("Frame", {
Position = UDim2.fromOffset(23, top - 1),
Size = UDim2.new(1, -46, 1, -(top + 14)),
BackgroundTransparency = 1,
ClipsDescendants = true,
Parent = tab._page,
})
for _, name in ipairs({ "Browse", "Detail", "Publish" }) do
views[name] = create("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
Visible = false,
Parent = root,
})
padding(views[name], 1, 1, 1, 1)
end
local function showView(view)
if currentView == view then
return
end
local previous = currentView
currentView = view
if previous then
previous.Visible = false
end
local fromX = view == views.Browse and -24 or 24
view.Position = UDim2.fromOffset(fromX, 0)
view.Visible = true
tween(view, { Position = UDim2.fromOffset(0, 0) }, 0.3, Enum.EasingStyle.Quint)
end
-- Browse: search, publish and refresh, the filter strip, then results.
local browse = views.Browse
local toolbar = create("Frame", {
Size = UDim2.new(1, 0, 0, ROW),
BackgroundTransparency = 1,
Parent = browse,
})
local publishButton, publishText = softButton(toolbar, "Publish", {
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.fromScale(1, 0),
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
}, function()
cloud:OpenPublish()
end)
primary(publishButton, publishText)
publishButton.Visible = type(opts.OnPublish) == "function"
local refreshButton, refreshIcon = iconButton(toolbar, "refresh-cw", "R", ROW)
refreshButton.AnchorPoint = Vector2.new(1, 0)
local searchHolder = create("Frame", {
Size = UDim2.new(1, -(ROW + 8), 1, 0),
BackgroundColor3 = Theme.Surface2,
BorderSizePixel = 0,
Parent = toolbar,
})
corner(searchHolder, UDim.new(0, 8))
local searchStroke = stroke(searchHolder)
local searchIcon, searchImage = glowIcon(searchHolder, "search", Theme.Muted, UDim2.new(0, 10, 0.5, 0))
searchIcon.Size = UDim2.fromOffset(14, 14)
local searchBox = create("TextBox", {
Position = UDim2.fromOffset(32, 0),
Size = UDim2.new(1, -62, 1, 0),
BackgroundTransparency = 1,
Text = "",
PlaceholderText = opts.SearchPlaceholder or (TOUCH and "Search configs..." or "Search configs... ( / )"),
PlaceholderColor3 = Theme.Muted,
TextColor3 = Theme.Text,
TextSize = 13,
FontFace = Fonts.Regular,
TextXAlignment = Enum.TextXAlignment.Left,
ClearTextOnFocus = false,
ClipsDescendants = true,
Parent = searchHolder,
})
local searchClear = iconButton(searchHolder, "x", "x", 24, true)
searchClear.AnchorPoint = Vector2.new(1, 0.5)
searchClear.Position = UDim2.new(1, -4, 0.5, 0)
searchClear.Visible = false
local fetch
local searchToken = 0
local recent = type(stored.CloudRecent) == "table" and stored.CloudRecent or {}
local recentFrame = create("Frame", {
Position = UDim2.fromOffset(0, ROW + 4),
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundColor3 = Theme.Surface2,
BorderSizePixel = 0,
Visible = false,
ZIndex = 5,
Parent = browse,
})
corner(recentFrame, UDim.new(0, 8))
stroke(recentFrame)
padding(recentFrame, 6, 6, 6, 6)
vlist(recentFrame, 2)
local function rememberSearch(text)
text = trim(text)
if #text < 2 then
return
end
for index = #recent, 1, -1 do
if string.lower(recent[index]) == string.lower(text) then
table.remove(recent, index)
end
end
table.insert(recent, 1, text)
while #recent > 6 do
table.remove(recent)
end
persist("CloudRecent", recent)
end
local function showRecent(show)
show = show and #recent > 0 and searchBox.Text == ""
if show == recentFrame.Visible then
return
end
recentFrame.Visible = show
if not show then
return
end
clear(recentFrame)
local header = create("Frame", {
Size = UDim2.new(1, 0, 0, 20),
BackgroundTransparency = 1,
LayoutOrder = 0,
Parent = recentFrame,
})
label({
Position = UDim2.fromOffset(8, 0),
Size = UDim2.new(1, -60, 1, 0),
Text = "Recent searches",
TextSize = 11,
TextColor3 = Theme.Muted,
Parent = header,
})
local wipe = create("TextButton", {
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, -6, 0, 0),
Size = UDim2.fromOffset(0, 20),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundTransparency = 1,
Text = "Clear",
TextSize = 11,
FontFace = Fonts.Medium,
TextColor3 = Theme.Accent,
AutoButtonColor = false,
Parent = header,
})
wipe.MouseButton1Down:Connect(function()
recent = {}
persist("CloudRecent", recent)
showRecent(false)
end)
for index, text in ipairs(recent) do
local row = create("TextButton", {
Size = UDim2.new(1, 0, 0, TOUCH and 32 or 28),
BackgroundColor3 = Theme.Surface3,
BackgroundTransparency = 1,
Text = "",
AutoButtonColor = false,
LayoutOrder = index,
Parent = recentFrame,
})
corner(row, UDim.new(0, 6))
local icon = glowIcon(row, "history", Theme.Muted, UDim2.new(0, 8, 0.5, 0))
icon.Size = UDim2.fromOffset(13, 13)
label({
Position = UDim2.fromOffset(28, 0),
Size = UDim2.new(1, -34, 1, 0),
Text = text,
TextSize = 13,
FontFace = Fonts.Regular,
Parent = row,
})
row.MouseEnter:Connect(function()
tween(row, { BackgroundTransparency = 0.5 }, 0.1)
end)
row.MouseLeave:Connect(function()
tween(row, { BackgroundTransparency = 1 }, 0.15)
end)
row.MouseButton1Down:Connect(function()
searchToken += 1
searchBox.Text = text
query.Search = text
rememberSearch(text)
showRecent(false)
searchBox:ReleaseFocus()
fetch(false)
end)
end
end
local function fitToolbar()
local scale = window.Scale.Scale
local publishWidth = publishButton.Visible and (publishButton.AbsoluteSize.X / scale + 8) or 0
refreshButton.Position = UDim2.new(1, -publishWidth, 0, 0)
searchHolder.Size = UDim2.new(1, -(publishWidth + ROW + 8), 1, 0)
recentFrame.Size = UDim2.new(1, -(publishWidth + ROW + 8), 0, 0)
end
publishButton:GetPropertyChangedSignal("AbsoluteSize"):Connect(fitToolbar)
task.defer(fitToolbar)
-- A pixel taller than the chips on each side, so their strokes aren't clipped.
local filterRow = create("Frame", {
Position = UDim2.fromOffset(0, ROW + 7),
Size = UDim2.new(1, 0, 0, CHIP + 2),
BackgroundTransparency = 1,
Parent = browse,
})
local sortButton = create("TextButton", {
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, -1, 0, 1),
Size = UDim2.fromOffset(0, CHIP),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = Theme.Surface2,
Text = "",
AutoButtonColor = false,
Parent = filterRow,
})
corner(sortButton, UDim.new(1, 0))
hoverStroke(sortButton, stroke(sortButton))
padding(sortButton, 10, 12)
hlist(sortButton, 6)
local sortIcon = glowIcon(sortButton, "arrow-down-up", Theme.Accent, UDim2.new())
sortIcon.Size = UDim2.fromOffset(13, 13)
local sortLabel = label({
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
Text = SORT_NAMES[query.Sort],
TextSize = 12,
TextTruncate = Enum.TextTruncate.None,
LayoutOrder = 2,
Parent = sortButton,
})
local filterStrip = create("ScrollingFrame", {
Size = UDim2.new(1, -120, 1, 0),
BackgroundTransparency = 1,
BorderSizePixel = 0,
ScrollBarThickness = 0,
ScrollingDirection = Enum.ScrollingDirection.X,
AutomaticCanvasSize = Enum.AutomaticSize.X,
CanvasSize = UDim2.new(),
Parent = filterRow,
})
filterStrip:SetAttribute("NoDrag", true)
hlist(filterStrip, 6)
padding(filterStrip, 1, 1, 1, 1)
local function fitFilters()
local width = sortButton.AbsoluteSize.X / window.Scale.Scale
filterStrip.Size = UDim2.new(1, -(width + 8), 1, 0)
end
sortButton:GetPropertyChangedSignal("AbsoluteSize"):Connect(fitFilters)
task.defer(fitFilters)
local resultsTop = ROW + 8 + CHIP + 10
local results = scroller(browse)
results.Position = UDim2.fromOffset(0, resultsTop)
results.Size = UDim2.new(1, 0, 1, -resultsTop)
padding(results, 1, 6, 1, 12)
vlist(results, 10)
local summaryRow = create("Frame", {
Size = UDim2.new(1, 0, 0, 16),
BackgroundTransparency = 1,
LayoutOrder = 0,
Parent = results,
})
local summaryLabel = label({
Size = UDim2.new(1, -100, 1, 0),
Text = "",
RichText = true,
TextSize = 12,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
Parent = summaryRow,
})
local resetButton = create("TextButton", {
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.fromScale(1, 0),
Size = UDim2.fromOffset(0, 16),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundTransparency = 1,
Text = "Clear filters",
TextSize = 12,
FontFace = Fonts.Medium,
TextColor3 = Theme.Accent,
AutoButtonColor = false,
Visible = false,
Parent = summaryRow,
})
local resetFilters
local function filtered()
return query.Search ~= "" or query.Tag ~= nil or query.Filter ~= "All"
end
-- "12 configs in Favorites, tagged PvP, matching "boss"", or "Searching..."
local function renderSummary(pending)
local parts = {}
if query.Filter ~= "All" then
table.insert(parts, "in " .. query.Filter)
end
if query.Tag then
table.insert(parts, "tagged " .. escape(query.Tag))
end
if query.Search ~= "" then
table.insert(parts, ('matching "%s"'):format(colorToHex(Theme.Accent), escape(query.Search)))
end
local context = #parts > 0 and (" " .. table.concat(parts, ", ")) or ""
if pending or (loading and #records == 0) then
summaryLabel.Text = "Searching" .. context .. "..."
else
local count = #records
summaryLabel.Text = ("%d%s config%s%s"):format(count, hasMore and "+" or "", (count == 1 and not hasMore) and "" or "s", context)
end
resetButton.Visible = filtered()
end
local spin = nil
local function busy(on)
loading = on
if on and not spin then
refreshIcon.Rotation = 0
spin = TweenService:Create(refreshIcon, TweenInfo.new(0.9, Enum.EasingStyle.Linear, Enum.EasingDirection.In, -1), { Rotation = 360 })
spin:Play()
elseif not on and spin then
spin:Cancel()
spin = nil
tween(refreshIcon, { Rotation = 360 }, 0.35, Enum.EasingStyle.Quint)
end
renderSummary()
end
local grid = create("Frame", {
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
LayoutOrder = 1,
Parent = results,
})
local gridLayout = create("UIGridLayout", {
CellSize = UDim2.new(1, 0, 0, CARD),
CellPadding = UDim2.fromOffset(8, 8),
SortOrder = Enum.SortOrder.LayoutOrder,
Parent = grid,
})
local function fitGrid()
local width = results.AbsoluteSize.X / window.Scale.Scale
local columns = width >= 620 and 2 or 1
gridLayout.CellSize = UDim2.new(1 / columns, -(columns - 1) * 8 / columns, 0, CARD)
end
results:GetPropertyChangedSignal("AbsoluteSize"):Connect(fitGrid)
task.defer(fitGrid)
local moreButton = softButton(results, "Load more", {
Size = UDim2.new(1, 0, 0, 32),
LayoutOrder = 2,
Visible = false,
}, function()
cloud:LoadMore()
end)
local stateFrame = create("Frame", {
Size = UDim2.new(1, 0, 0, 180),
BackgroundTransparency = 1,
Visible = false,
LayoutOrder = 3,
Parent = results,
})
-- Loading placeholders that pulse where cards will land.
local skeletons = {}
local function addSkeletons(count)
for _ = 1, count do
local cell = create("Frame", {
BackgroundColor3 = Theme.Surface2,
BorderSizePixel = 0,
LayoutOrder = 100000 + #skeletons,
Parent = grid,
})
corner(cell, UDim.new(0, 10))
stroke(cell)
for index, spec in ipairs({ { 14, 0.45 }, { 36, 0.3 }, { 58, 0.8 }, { 76, 0.6 } }) do
local bar = create("Frame", {
Position = UDim2.fromOffset(14, spec[1]),
Size = UDim2.new(spec[2], -14, 0, index == 1 and 14 or 10),
BackgroundColor3 = Theme.Surface3,
BorderSizePixel = 0,
Parent = cell,
})
corner(bar, UDim.new(0, 4))
TweenService:Create(bar, TweenInfo.new(0.75, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut, -1, true), {
BackgroundTransparency = 0.65,
}):Play()
end
table.insert(skeletons, cell)
end
end
local function clearSkeletons()
for _, cell in ipairs(skeletons) do
cell:Destroy()
end
skeletons = {}
end
-- kind: nil hides it, or "empty" / "error" with a message.
local function setState(kind, text)
clear(stateFrame)
stateFrame.Visible = kind ~= nil
if not kind then
return
end
local holder, caption = emptyState(stateFrame, kind == "error" and "cloud-off" or "search-x", text)
holder.Size = UDim2.fromOffset(280, 70)
holder.Position = UDim2.new(0.5, 0, 0, 60)
caption.TextWrapped = true
caption.TextTruncate = Enum.TextTruncate.None
caption.Size = UDim2.new(1, 0, 0, 32)
if kind == "error" or filtered() then
softButton(stateFrame, kind == "error" and "Try again" or "Clear filters", {
AnchorPoint = Vector2.new(0.5, 0),
Position = UDim2.new(0.5, 0, 0, 132),
Size = UDim2.fromOffset(0, 30),
AutomaticSize = Enum.AutomaticSize.X,
}, function()
if kind == "error" then
cloud:Refresh()
else
resetFilters()
end
end)
end
renderSummary()
end
-- Cards
local openDetail, installRecord, refreshRecord
local function renderCard(card)
local record = card.Record
card.Title.Text = highlight(record.Name)
local meta = { "by " .. record.Author }
local when = ago(record.Updated)
if when then
table.insert(meta, when)
end
if record.Count then
table.insert(meta, record.Count .. (record.Count == 1 and " setting" or " settings"))
end
card.Meta.Text = table.concat(meta, " · ")
card.Desc.Text = record.Description ~= "" and highlight(record.Description) or "No description"
card.Desc.TextTransparency = record.Description ~= "" and 0.2 or 0.55
card.Likes.Text = compact(record.Likes)
card.Installs.Text = compact(record.Installs)
tween(card.Heart, { ImageColor3 = record.Liked and Theme.Accent or Theme.Muted }, 0.15)
local state = installState(record)
local badge, color
if state == "Update" then
badge, color = "UPDATE", Theme.Accent
elseif state == "Installed" then
badge, color = "INSTALLED", Theme.Success
elseif record.Mine then
badge, color = "YOURS", Theme.Accent
elseif otherScript(record) then
badge, color = "OTHER SCRIPT", Theme.Warning
end
card.Badge.Visible = badge ~= nil
if badge then
card.Badge.Text = badge
tween(card.Badge, { TextColor3 = color, BackgroundColor3 = color }, 0)
end
card.Star.Visible = favorites[record.Id] ~= nil
card.InstallText.Text = state == "Update" and "Update" or (state == "Installed" and "Reinstall" or "Install")
clear(card.TagRow)
for index, tag in ipairs(record.Tags) do
if index > 3 then
tagPill(card.TagRow, "+" .. (#record.Tags - 3), index)
break
end
tagPill(card.TagRow, tag, index)
end
end
local function buildCard(record, order)
local frame = create("TextButton", {
BackgroundColor3 = Theme.Surface2,
BorderSizePixel = 0,
Text = "",
AutoButtonColor = false,
LayoutOrder = order,
Parent = grid,
})
frame:SetAttribute("NoDrag", true)
corner(frame, UDim.new(0, 10))
hoverStroke(frame, stroke(frame))
padding(frame, 14, 14, 12, 12)
local card = { Record = record, Frame = frame }
card.Title = label({
Size = UDim2.new(1, -110, 0, 18),
RichText = true,
TextSize = 14,
FontFace = Fonts.Bold,
Parent = frame,
})
local corner_ = create("Frame", {
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.fromScale(1, 0),
Size = UDim2.fromOffset(0, 18),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundTransparency = 1,
Parent = frame,
})
local cornerLayout = hlist(corner_, 6)
cornerLayout.HorizontalAlignment = Enum.HorizontalAlignment.Right
card.Star = label({
Size = UDim2.fromOffset(14, 18),
Text = "★",
TextSize = 13,
TextColor3 = Theme.Accent,
TextXAlignment = Enum.TextXAlignment.Center,
TextTruncate = Enum.TextTruncate.None,
LayoutOrder = 1,
Parent = corner_,
})
card.Badge = label({
Size = UDim2.fromOffset(0, 17),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundTransparency = 0.85,
TextSize = 10,
FontFace = Fonts.Bold,
TextXAlignment = Enum.TextXAlignment.Center,
TextTruncate = Enum.TextTruncate.None,
LayoutOrder = 2,
Parent = corner_,
})
corner(card.Badge, UDim.new(0, 5))
padding(card.Badge, 6, 6)
card.Meta = label({
Position = UDim2.fromOffset(0, 20),
Size = UDim2.new(1, 0, 0, 16),
TextSize = 12,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
Parent = frame,
})
card.Desc = label({
Position = UDim2.fromOffset(0, 41),
RichText = true,
Size = UDim2.new(1, 0, 0, 30),
TextSize = 12,
FontFace = Fonts.Regular,
TextWrapped = true,
TextYAlignment = Enum.TextYAlignment.Top,
Parent = frame,
})
local bottom = create("Frame", {
AnchorPoint = Vector2.new(0, 1),
Position = UDim2.fromScale(0, 1),
Size = UDim2.new(1, 0, 0, 26),
BackgroundTransparency = 1,
Parent = frame,
})
card.TagRow = create("Frame", {
Size = UDim2.new(1, -190, 1, 0),
BackgroundTransparency = 1,
ClipsDescendants = true,
Parent = bottom,
})
hlist(card.TagRow, 4)
local install, installText = softButton(bottom, "Install", {
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, 0, 0.5, 0),
Size = UDim2.fromOffset(76, 26),
}, function()
installRecord(record)
end)
primary(install, installText)
card.InstallText = installText
local stats = create("Frame", {
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, -86, 0.5, 0),
Size = UDim2.fromOffset(0, 20),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundTransparency = 1,
Parent = bottom,
})
hlist(stats, 4)
local function stat(icon, order_)
local holder, image = glowIcon(stats, icon, Theme.Muted, UDim2.new())
holder.Size = UDim2.fromOffset(12, 12)
holder.LayoutOrder = order_
local text_ = label({
Size = UDim2.fromOffset(0, 20),
AutomaticSize = Enum.AutomaticSize.X,
TextSize = 12,
TextColor3 = Theme.Muted,
TextTruncate = Enum.TextTruncate.None,
LayoutOrder = order_ + 1,
Parent = stats,
})
return text_, image
end
card.Likes, card.Heart = stat("heart", 1)
create("Frame", { Size = UDim2.fromOffset(4, 1), BackgroundTransparency = 1, LayoutOrder = 3, Parent = stats })
card.Installs = stat("download", 4)
local isTap = tapGuard(frame, function()
return results
end)
frame.MouseButton1Click:Connect(function()
if isTap() then
if query.Search ~= "" then
rememberSearch(query.Search)
end
openDetail(record)
end
end)
cards[record.Id] = card
renderCard(card)
return card
end
local function renderList()
for _, card in pairs(cards) do
card.Frame:Destroy()
end
cards = {}
for index, record in ipairs(records) do
buildCard(record, index)
end
end
-- Favorites and Installed are kept on this device, so they filter locally.
local function localRecords()
local source = query.Filter == "Favorites" and favorites or installed
local search = string.lower(query.Search)
local list = {}
for id, entry in pairs(source) do
local kept = query.Filter == "Favorites" and entry or (type(entry) == "table" and entry.Record)
if type(kept) == "table" then
kept = table.clone(kept)
kept.Id = id
local record = normalizeRecord(kept)
local matches = search == "" or string.find(string.lower(record.Name), search, 1, true)
or string.find(string.lower(record.Description), search, 1, true)
if matches and query.Tag then
matches = table.find(record.Tags, query.Tag) ~= nil
end
if matches then
table.insert(list, record)
end
end
end
table.sort(list, function(a, b)
if query.Sort == "New" then
return (a.Updated or 0) > (b.Updated or 0)
elseif query.Sort == "Installs" then
return a.Installs > b.Installs
end
return a.Likes > b.Likes
end)
return list
end
local function emptyText()
if query.Filter == "Favorites" then
return "No favorites yet. Open a config and press Favorite to keep it here."
elseif query.Filter == "Installed" then
return "Nothing installed from the cloud yet"
elseif query.Filter == "Mine" then
return "You haven't published anything yet"
elseif query.Search ~= "" or query.Tag then
return "No configs match your search"
end
return tostring(opts.EmptyText or "No configs shared yet. Be the first!")
end
function fetch(more)
token += 1
local myToken = token
if query.Filter == "Favorites" or query.Filter == "Installed" then
busy(false)
clearSkeletons()
records = localRecords()
hasMore = false
renderList()
moreButton.Visible = false
setState(#records == 0 and "empty" or nil, emptyText())
return
end
if type(opts.OnFetch) ~= "function" then
renderList()
setState(#records == 0 and "empty" or nil, emptyText())
return
end
if more then
if not hasMore then
return
end
page += 1
else
page = 1
records = {}
renderList()
results.CanvasPosition = Vector2.zero
end
busy(true)
clearSkeletons()
addSkeletons(more and 2 or 4)
moreButton.Visible = false
setState(nil)
local fetchQuery = {
Search = query.Search,
Sort = query.Sort,
Filter = query.Filter,
Tag = query.Tag,
Page = page,
PageSize = pageSize,
Folder = window.ConfigFolder,
UserId = LocalPlayer.UserId,
}
task.spawn(function()
local ok, list, extra = isolatedCall(function()
return opts.OnFetch(fetchQuery)
end)
if myToken ~= token then
return
end
busy(false)
clearSkeletons()
if not ok or type(list) ~= "table" then
if more then
page -= 1
end
if not ok then
warn("[AirFlow] cloud OnFetch error: " .. tostring(list))
end
hasMore = more and hasMore or false
moreButton.Visible = false
setState("error", not ok and "Could not load configs" or tostring(extra or "Could not load configs"))
return
end
hasMore = extra == true
local seen = {}
for _, record in ipairs(records) do
seen[record] = true
end
for _, raw in ipairs(list) do
local record = normalizeRecord(raw)
if record and not seen[record] then
seen[record] = true
table.insert(records, record)
buildCard(record, #records)
end
end
moreButton.Visible = hasMore
setState(#records == 0 and "empty" or nil, emptyText())
end)
end
-- Near the bottom, the next page loads by itself.
results:GetPropertyChangedSignal("CanvasPosition"):Connect(function()
if not hasMore or loading then
return
end
local scale = window.Scale.Scale
local remaining = results.AbsoluteCanvasSize.Y / scale - results.AbsoluteWindowSize.Y / scale - results.CanvasPosition.Y
if remaining < 160 then
fetch(true)
end
end)
-- Filters and sorting
local filterChips, tagChips = {}, {}
local function renderFilters(instant)
for name, object in pairs(filterChips) do
object.Set(query.Filter == name, instant)
end
for name, object in pairs(tagChips) do
object.Set(query.Tag == name, instant)
end
sortLabel.Text = SORT_NAMES[query.Sort]
end
for index, name in ipairs(FILTERS) do
if not (name == "Mine" and opts.MineFilter == false) then
filterChips[name] = chip(filterStrip, name, index, function()
if query.Filter ~= name then
query.Filter = name
renderFilters()
fetch(false)
end
end)
end
end
if #tagList > 0 then
create("Frame", {
Size = UDim2.fromOffset(1, 16),
BackgroundColor3 = Theme.Stroke,
BorderSizePixel = 0,
LayoutOrder = 10,
Parent = filterStrip,
})
end
for index, tag in ipairs(tagList) do
tag = tostring(tag)
tagChips[tag] = chip(filterStrip, tag, 10 + index, function()
query.Tag = query.Tag ~= tag and tag or nil
renderFilters()
fetch(false)
end)
end
sortButton.MouseButton1Click:Connect(function()
local index = table.find(SORTS, query.Sort) or 1
query.Sort = SORTS[index % #SORTS + 1]
persist("CloudSort", query.Sort)
renderFilters()
fetch(false)
end)
function resetFilters()
searchToken += 1
searchBox.Text = ""
query.Search, query.Tag, query.Filter = "", nil, "All"
renderFilters()
fetch(false)
end
resetButton.MouseButton1Click:Connect(resetFilters)
-- Typing searches after a short pause; Enter searches now.
searchBox:GetPropertyChangedSignal("Text"):Connect(function()
searchClear.Visible = searchBox.Text ~= ""
showRecent(searchBox:IsFocused())
searchToken += 1
local mine = searchToken
local text = trim(searchBox.Text)
if text ~= query.Search then
renderSummary(true)
end
task.delay(0.35, function()
text = trim(searchBox.Text)
if mine == searchToken and text ~= query.Search then
query.Search = text
fetch(false)
end
end)
end)
searchBox.Focused:Connect(function()
tween(searchStroke, { Color = Theme.Accent }, 0.15)
tween(searchImage, { ImageColor3 = Theme.Accent }, 0.15)
showRecent(true)
end)
searchBox.FocusLost:Connect(function(enterPressed)
tween(searchStroke, { Color = Theme.Stroke }, 0.2)
tween(searchImage, { ImageColor3 = Theme.Muted }, 0.2)
rememberSearch(searchBox.Text)
-- Late enough for a press on a recent search to land first.
task.delay(0.15, function()
if not searchBox:IsFocused() then
showRecent(false)
end
end)
if enterPressed then
searchToken += 1
local text = trim(searchBox.Text)
if text ~= query.Search then
query.Search = text
fetch(false)
end
end
end)
searchClear.MouseButton1Click:Connect(function()
searchToken += 1
searchBox.Text = ""
if query.Search ~= "" then
query.Search = ""
fetch(false)
end
end)
-- "/" jumps to the search box while the list is showing.
window:_listen("Began", function(input, processed)
if processed or input.KeyCode ~= Enum.KeyCode.Slash then
return
end
if window.Open and window.CurrentTab == tab and currentView == views.Browse and not UserInputService:GetFocusedTextBox() then
task.defer(function()
searchBox:CaptureFocus()
searchBox.Text = searchBox.Text:gsub("/$", "")
end)
end
end, cloud)
refreshButton.MouseButton1Click:Connect(function()
cloud:Refresh()
end)
-- Actions on a record
-- Runs callback with the record's code, asking the backend for it first
-- when the list came without codes.
local function withCode(record, callback, onFail)
if record.Code then
callback(record.Code)
return
end
local function fail(reason)
if onFail then
onFail(reason)
else
notify(false, "Could not load config", reason)
end
end
if type(opts.OnFetchCode) ~= "function" then
fail("This config has no code")
return
end
task.spawn(function()
local ok, code, reason = isolatedCall(function()
return opts.OnFetchCode(publicRecord(record))
end)
if ok and type(code) == "string" and code ~= "" then
record.Code = code
record.Count = record.Count or countFlags(code)
callback(code)
else
fail(tostring(reason or (not ok and "the request failed") or "no code came back"))
end
end)
end
function installRecord(record)
if record._busy then
return
end
record._busy = true
withCode(record, function(code)
record._busy = nil
local data, info = window:DecodeConfig(code)
if not data then
notify(false, "Install failed", "The config code is invalid")
return
end
local count = 0
for _ in pairs(data) do
count += 1
end
local mark = installed[record.Id]
local saveAs = nil
if fileApiAvailable() then
-- An update overwrites the config it installed last time.
saveAs = type(mark) == "table" and type(mark.Name) == "string" and window:ConfigExists(mark.Name) and mark.Name
or uniqueName(record.Name)
end
local other = info.Folder and info.Folder ~= window.ConfigFolder
local function apply(save)
local name = save and saveAs or nil
local ok, err = window:ImportConfig(code, name, true)
if not ok then
notify(false, "Install failed", tostring(err))
return
end
installed[record.Id] = { Updated = record.Updated or os.time(), Name = name, At = os.time(), Record = summary(record) }
persist("CloudInstalled", installed)
record.Installs += 1
refreshRecord(record)
notify(true, "Config installed", name and ("Saved as " .. name) or "Applied to your current settings")
if opts.OnInstall then
task.spawn(safeCall, opts.OnInstall, publicRecord(record), name)
end
end
local content = ("%s by %s changes %d setting%s."):format(record.Name, record.Author, count, count == 1 and "" or "s")
if other then
content ..= (" It was made for %s, so only matching settings apply."):format(info.Folder)
end
if saveAs then
content ..= (" Install also saves it as %s; Apply only doesn't."):format(saveAs)
end
local buttons = { { Title = "Cancel" } }
table.insert(buttons, { Title = "Apply only", Variant = not saveAs and "Primary" or nil, Callback = function()
apply(false)
end })
if saveAs then
table.insert(buttons, { Title = "Install", Variant = "Primary", Callback = function()
apply(true)
end })
end
window:Dialog({
Title = installState(record) == "Update" and "Update config" or "Install config",
Content = content,
Icon = other and "triangle-alert" or "download",
Buttons = buttons,
})
end, function(reason)
record._busy = nil
notify(false, "Could not load config", reason)
end)
end
local function toggleLike(record)
if type(opts.OnLike) ~= "function" or record._liking then
return
end
local liked = not record.Liked
record.Liked = liked
record.Likes = math.max(record.Likes + (liked and 1 or -1), 0)
record._liking = true
refreshRecord(record)
task.spawn(function()
local ok, result, reason = isolatedCall(function()
return opts.OnLike(publicRecord(record), liked)
end)
record._liking = nil
if not ok or result == false then
record.Liked = not liked
record.Likes = math.max(record.Likes + (liked and -1 or 1), 0)
refreshRecord(record)
notify(false, "Like not saved", tostring(reason or "Try again in a moment"))
end
end)
end
local function toggleFavorite(record)
local on = favorites[record.Id] == nil
favorites[record.Id] = on and summary(record) or nil
persist("CloudFavorites", favorites)
refreshRecord(record)
window:Notify({
Title = on and "Added to favorites" or "Removed from favorites",
Content = record.Name,
Icon = "star",
Duration = 2,
})
end
local function removeRecord(record)
local index = table.find(records, record)
if index then
table.remove(records, index)
end
byId[record.Id] = nil
local card = cards[record.Id]
if card then
card.Frame:Destroy()
cards[record.Id] = nil
end
if #records == 0 and not loading then
setState("empty", emptyText())
end
end
local function deleteRecord(record)
window:Confirm({
Title = "Delete config",
Content = ("Remove %s from the cloud? People who installed it keep their copy."):format(record.Name),
Icon = "trash-2",
ConfirmText = "Delete",
Callback = function()
task.spawn(function()
local ok, result, reason = isolatedCall(function()
return opts.OnDelete(publicRecord(record))
end)
if not ok or result == false then
notify(false, "Delete failed", tostring(reason or "Try again in a moment"))
return
end
favorites[record.Id] = nil
persist("CloudFavorites", favorites)
removeRecord(record)
showView(views.Browse)
notify(true, "Config deleted", record.Name)
end)
end,
})
end
local function reportRecord(record)
local reasons = type(opts.ReportReasons) == "table" and opts.ReportReasons or { "Broken", "Spam", "Inappropriate" }
local buttons = { { Title = "Cancel" } }
for _, reason in ipairs(reasons) do
table.insert(buttons, { Title = tostring(reason), Callback = function()
task.spawn(function()
local ok, result = isolatedCall(function()
return opts.OnReport(publicRecord(record), tostring(reason))
end)
local sent = ok and result ~= false
notify(sent, sent and "Report sent" or "Report failed", sent and "Thanks for letting us know" or "Try again in a moment")
end)
end })
end
window:Dialog({
Title = "Report config",
Content = ("What's wrong with %s?"):format(record.Name),
Icon = "flag",
Buttons = buttons,
})
end
-- Detail view
local detailList = scroller(views.Detail)
padding(detailList, 1, 8, 1, 16)
vlist(detailList, 10)
local function valueText(entry)
local value = type(entry) == "table" and entry.Value or nil
if value == nil then
return "None"
elseif type(value) == "boolean" then
return value and "On" or "Off"
elseif type(value) == "number" then
return (("%.2f"):format(value):gsub("%.?0+$", ""))
elseif type(value) == "table" then
if type(value.Hex) == "string" then
return "#" .. string.upper(value.Hex)
elseif type(value.Colors) == "table" then
local text = type(value.Name) == "string" and value.Name or "Custom"
if type(value.Background) == "string" and value.Background ~= "" then
text ..= ", image"
end
return text
end
local parts = {}
for index, item in ipairs(value) do
parts[index] = tostring(item)
end
return #parts > 0 and table.concat(parts, ", ") or "None"
end
return tostring(value)
end
local function encoded(entry)
local ok, text = pcall(HttpService.JSONEncode, HttpService, type(entry) == "table" and entry.Value or nil)
return ok and text or tostring(entry)
end
local function renderDetail()
local record = detailRecord
if not record or not detail.Meta then
return
end
detail.Title.Text = record.Name
local meta = { "by " .. record.Author }
local when = ago(record.Updated)
if when then
table.insert(meta, "updated " .. when)
end
if otherScript(record) then
table.insert(meta, "made for " .. record.Folder)
end
detail.Meta.Text = table.concat(meta, " · ")
detail.Stats.Text = ("%s like%s · %s install%s"):format(
compact(record.Likes), record.Likes == 1 and "" or "s",
compact(record.Installs), record.Installs == 1 and "" or "s")
if detail.BuiltFor ~= record or detail.BuiltMine ~= record.Mine then
detail.BuiltFor, detail.BuiltMine = record, record.Mine
local row = detail.Actions
clear(row)
local order = 0
local function action(text, style, callback)
order += 1
local button, text_ = softButton(row, text, {
Size = UDim2.new(0, 0, 0, 30),
AutomaticSize = Enum.AutomaticSize.X,
LayoutOrder = order,
}, callback)
text_.TextSize = 13
if style == "Primary" then
primary(button, text_)
elseif style == "Danger" then
tween(text_, { TextColor3 = Theme.Error }, 0)
end
return text_
end
detail.InstallLabel = action("Install", "Primary", function()
installRecord(record)
end)
if type(opts.OnLike) == "function" then
detail.LikeLabel = action("Like", nil, function()
toggleLike(record)
end)
end
detail.FavoriteLabel = action("Favorite", nil, function()
toggleFavorite(record)
end)
action("Copy code", nil, function()
withCode(record, function(code)
window:CopyToClipboard(code, "Config code")
end)
end)
if record.Mine then
if type(opts.OnUpdate) == "function" then
action("Edit", nil, function()
cloud:OpenPublish(record)
end)
end
if type(opts.OnDelete) == "function" then
action("Delete", "Danger", function()
deleteRecord(record)
end)
end
elseif type(opts.OnReport) == "function" then
action("Report", "Danger", function()
reportRecord(record)
end)
end
end
local state = installState(record)
detail.InstallLabel.Text = state == "Update" and "Update" or (state == "Installed" and "Reinstall" or "Install")
if detail.LikeLabel then
detail.LikeLabel.Text = (record.Liked and "Liked" or "Like") .. " · " .. compact(record.Likes)
end
detail.FavoriteLabel.Text = favorites[record.Id] and "Favorited" or "Favorite"
end
-- Every setting in the config next to the player's own: changed ones
-- first, then the same ones, then flags this script doesn't have.
local function fillPreview(record, myToken)
local function mismatch(text)
if detailToken == myToken then
detail.Summary.Text = text
end
end
withCode(record, function(code)
if detailToken ~= myToken then
return
end
local data = window:DecodeConfig(code)
if not data then
mismatch("The config code is invalid")
return
end
local current = collectFlags(window)
local rows = {}
local changed = 0
for flag, entry in pairs(data) do
local element = Library.Flags[flag]
local known = element ~= nil
local differs = known and encoded(entry) ~= encoded(current[flag])
if differs then
changed += 1
end
table.insert(rows, {
Name = element and element._searchName ~= "" and element._searchName or flag,
Value = valueText(entry),
Known = known,
Changed = differs,
Rank = differs and 0 or (known and 1 or 2),
})
end
table.sort(rows, function(a, b)
if a.Rank ~= b.Rank then
return a.Rank < b.Rank
end
return string.lower(a.Name) < string.lower(b.Name)
end)
record.Count = #rows
detail.Summary.Text = changed == 0 and ("%d settings, all the same as yours"):format(#rows)
or ("%d settings, %d different from yours"):format(#rows, changed)
clear(detail.Rows)
for index, item in ipairs(rows) do
if index > 150 then
wrapped(detail.Rows, ("and %d more"):format(#rows - 150), 12, Theme.Muted, index)
break
end
local rowFrame = create("Frame", {
Size = UDim2.new(1, 0, 0, 30),
BackgroundColor3 = Theme.Surface2,
BackgroundTransparency = item.Changed and 0 or 0.5,
BorderSizePixel = 0,
LayoutOrder = index,
Parent = detail.Rows,
})
corner(rowFrame, UDim.new(0, 7))
if item.Changed then
local dot = create("Frame", {
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, 10, 0.5, 0),
Size = UDim2.fromOffset(6, 6),
BackgroundColor3 = Theme.Accent,
BorderSizePixel = 0,
Parent = rowFrame,
})
corner(dot, UDim.new(1, 0))
end
label({
Position = UDim2.fromOffset(24, 0),
Size = UDim2.new(0.55, -24, 1, 0),
Text = item.Known and item.Name or (item.Name .. " (not in this script)"),
TextSize = 12,
TextColor3 = item.Known and Theme.Text or Theme.Muted,
Parent = rowFrame,
})
label({
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, -10, 0, 0),
Size = UDim2.new(0.45, -10, 1, 0),
Text = item.Value,
TextSize = 12,
FontFace = Fonts.Regular,
TextColor3 = item.Changed and Theme.Accent or Theme.Muted,
TextXAlignment = Enum.TextXAlignment.Right,
Parent = rowFrame,
})
end
if cards[record.Id] then
renderCard(cards[record.Id])
end
end, mismatch)
end
function openDetail(record)
detailRecord = record
detailToken += 1
local myToken = detailToken
clear(detailList)
detail = {}
detailList.CanvasPosition = Vector2.zero
softButton(detailList, "Back", {
Size = UDim2.new(0, 0, 0, 28),
AutomaticSize = Enum.AutomaticSize.X,
LayoutOrder = 1,
}, function()
showView(views.Browse)
end)
detail.Title = wrapped(detailList, record.Name, 20, Theme.Text, 2, Fonts.Bold)
detail.Meta = wrapped(detailList, "", 12, Theme.Muted, 3)
if record.Description ~= "" then
wrapped(detailList, record.Description, 13, Theme.Text, 4)
end
if #record.Tags > 0 then
local tagsRow = create("Frame", {
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
LayoutOrder = 5,
Parent = detailList,
})
hlist(tagsRow, 4, true)
for index, tag in ipairs(record.Tags) do
tagPill(tagsRow, tag, index)
end
end
detail.Stats = wrapped(detailList, "", 12, Theme.Muted, 6)
detail.Actions = create("Frame", {
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
LayoutOrder = 7,
Parent = detailList,
})
hlist(detail.Actions, 6, true)
create("Frame", {
Size = UDim2.new(1, 0, 0, 1),
BackgroundColor3 = Theme.Stroke,
BorderSizePixel = 0,
LayoutOrder = 8,
Parent = detailList,
})
wrapped(detailList, "Settings", 14, Theme.Text, 9, Fonts.Bold)
detail.Summary = wrapped(detailList, "Loading settings...", 12, Theme.Muted, 10)
detail.Rows = create("Frame", {
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
LayoutOrder = 11,
Parent = detailList,
})
vlist(detail.Rows, 4)
renderDetail()
showView(views.Detail)
fillPreview(record, myToken)
end
function refreshRecord(record)
local card = cards[record.Id]
if card then
renderCard(card)
end
if detailRecord == record and currentView == views.Detail then
renderDetail()
end
end
-- Publish view: also the edit form for the player's own configs.
local form = { Mode = "Publish", Record = nil, Source = false, Tags = {}, Busy = false }
local publishList = scroller(views.Publish)
padding(publishList, 1, 8, 1, 16)
vlist(publishList, 8)
softButton(publishList, "Back", {
Size = UDim2.new(0, 0, 0, 28),
AutomaticSize = Enum.AutomaticSize.X,
LayoutOrder = 1,
}, function()
showView(form.Mode == "Edit" and form.Record and views.Detail or views.Browse)
end)
local formTitle = wrapped(publishList, "Publish a config", 20, Theme.Text, 2, Fonts.Bold)
wrapped(publishList, "Name", 12, Theme.Muted, 3)
local nameBox = field(publishList, 4, "What is it for?", ROW, NAME_LIMIT)
wrapped(publishList, "Description", 12, Theme.Muted, 5)
local descBox = field(publishList, 6, "What does it do, and how should people use it?", 90, DESC_LIMIT)
local tagsTitle = wrapped(publishList, "Tags", 12, Theme.Muted, 7)
local tagsRow = create("Frame", {
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
LayoutOrder = 8,
Parent = publishList,
})
hlist(tagsRow, 6, true)
tagsTitle.Visible = #tagList > 0 and maxTags > 0
tagsRow.Visible = tagsTitle.Visible
wrapped(publishList, "Settings to share", 12, Theme.Muted, 9)
local sourceRow = create("Frame", {
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
LayoutOrder = 10,
Parent = publishList,
})
hlist(sourceRow, 6, true)
local sourceSummary = wrapped(publishList, "", 12, Theme.Muted, 11)
local identityNote = wrapped(publishList, "", 12, Theme.Muted, 12)
local formError = wrapped(publishList, "", 12, Theme.Error, 13)
formError.Visible = false
local submitRow = create("Frame", {
Size = UDim2.new(1, 0, 0, 32),
BackgroundTransparency = 1,
LayoutOrder = 14,
Parent = publishList,
})
hlist(submitRow, 6)
local submitButton, submitText = softButton(submitRow, "Publish", {
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
LayoutOrder = 1,
}, function()
cloud:_submit()
end)
primary(submitButton, submitText)
softButton(submitRow, "Cancel", {
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
LayoutOrder = 2,
}, function()
showView(form.Mode == "Edit" and form.Record and views.Detail or views.Browse)
end)
local formTagChips = {}
local function renderFormTags()
for tag, object in pairs(formTagChips) do
object.Set(table.find(form.Tags, tag) ~= nil)
end
tagsTitle.Text = maxTags > 0 and ("Tags (%d/%d)"):format(#form.Tags, maxTags) or "Tags"
end
for index, tag in ipairs(tagList) do
tag = tostring(tag)
formTagChips[tag] = chip(tagsRow, tag, index, function()
local at = table.find(form.Tags, tag)
if at then
table.remove(form.Tags, at)
elseif #form.Tags < maxTags then
table.insert(form.Tags, tag)
else
notify(false, "Too many tags", ("Pick up to %d"):format(maxTags))
end
renderFormTags()
end)
end
-- The code and setting count for the picked source, or nil and why not.
local function sourceCode(name)
if form.Source == "Keep" then
return nil, form.Record and form.Record.Count
elseif form.Source then
local code, err = window:ExportConfig(form.Source)
if not code then
return nil, nil, err
end
return code, countFlags(code)
end
local ok, code = pcall(encodeConfig, window, name ~= "" and name or "Cloud config", collectFlags(window))
if not ok then
return nil, nil, "could not encode your settings"
end
return code, countFlags(code)
end
local sourceChips = {}
local function renderSource()
for key, object in pairs(sourceChips) do
object.Set(key == (form.Source or "*current"))
end
local _, count, err = sourceCode(trim(nameBox.Text))
if err then
sourceSummary.Text = tostring(err)
elseif form.Source == "Keep" then
sourceSummary.Text = "Keeps the settings already published" .. (count and (" (" .. count .. ")") or "")
else
sourceSummary.Text = ("%d setting%s from %s"):format(count or 0, count == 1 and "" or "s", form.Source or "your current settings")
end
end
local function buildSources()
clear(sourceRow)
sourceChips = {}
local order = 0
local function add(key, text)
order += 1
sourceChips[key] = chip(sourceRow, text, order, function()
form.Source = key ~= "*current" and key or false
renderSource()
end)
end
if form.Mode == "Edit" then
add("Keep", "Keep published settings")
end
add("*current", "Current settings")
for _, name in ipairs(window:ListConfigs()) do
add(name, name)
end
renderSource()
end
local function showError(text)
formError.Text = tostring(text)
formError.Visible = true
end
function cloud:OpenPublish(record)
local editing = type(record) == "table" and record.Id and byId[record.Id] or nil
if editing then
form.Mode, form.Record = "Edit", editing
form.Source = "Keep"
form.Tags = table.clone(editing.Tags)
nameBox.Text = editing.Name
descBox.Text = editing.Description
elseif form.Mode == "Edit" then
-- Leaving an edit starts a fresh draft; a publish draft is kept.
form.Mode, form.Record, form.Source, form.Tags = "Publish", nil, false, {}
nameBox.Text = ""
descBox.Text = ""
end
formTitle.Text = editing and "Edit config" or "Publish a config"
submitText.Text = editing and "Save changes" or "Publish"
formError.Visible = false
local streamer = streamerOn()
identityNote.Text = streamer and ("Your name is hidden, so you publish as " .. anonName)
or ("You publish as " .. LocalPlayer.DisplayName)
renderFormTags()
buildSources()
publishList.CanvasPosition = Vector2.zero
showView(views.Publish)
end
nameBox:GetPropertyChangedSignal("Text"):Connect(function()
formError.Visible = false
end)
function cloud:_submit()
if form.Busy then
return
end
local name = trim(nameBox.Text)
if name == "" then
showError("Give your config a name")
return
end
local code, count, err = sourceCode(name)
if err then
showError(err)
return
end
if form.Source ~= "Keep" and (count or 0) == 0 then
showError("There are no settings to share")
return
end
local editing = form.Mode == "Edit" and form.Record or nil
local callback = editing and opts.OnUpdate or opts.OnPublish
if type(callback) ~= "function" then
showError(editing and "Editing isn't available" or "Publishing isn't available")
return
end
local streamer = streamerOn()
local payload = {
Name = name,
Description = trim(descBox.Text),
Tags = table.clone(form.Tags),
Code = code,
Count = count,
Folder = window.ConfigFolder,
Author = streamer and anonName or LocalPlayer.DisplayName,
AuthorId = not streamer and LocalPlayer.UserId or nil,
OwnerId = LocalPlayer.UserId,
Streamer = streamer,
}
form.Busy = true
submitText.Text = editing and "Saving..." or "Publishing..."
formError.Visible = false
task.spawn(function()
local ok, result, reason = isolatedCall(function()
if editing then
return callback(publicRecord(editing), payload)
end
return callback(payload)
end)
form.Busy = false
submitText.Text = editing and "Save changes" or "Publish"
if not ok or result == false then
if not ok then
warn("[AirFlow] cloud publish error: " .. tostring(result))
end
showError(ok and reason and tostring(reason) or "Something went wrong, try again")
return
end
local merged = table.clone(payload)
merged.Code = code or (editing and editing.Code)
merged.Updated = os.time()
if type(result) == "table" then
for key, value in pairs(result) do
merged[key] = value
end
end
if editing then
merged.Id = editing.Id
normalizeRecord(merged)
if favorites[editing.Id] then
favorites[editing.Id] = summary(editing)
persist("CloudFavorites", favorites)
end
refreshRecord(editing)
notify(true, "Config updated", name)
openDetail(editing)
else
nameBox.Text = ""
descBox.Text = ""
form.Tags = {}
form.Source = false
if type(result) == "table" then
merged.Mine = true
local record = normalizeRecord(merged)
if query.Filter == "All" or query.Filter == "Mine" then
table.insert(records, 1, record)
renderList()
setState(nil)
end
notify(true, "Config published", name)
openDetail(record)
else
notify(true, "Config published", name)
showView(views.Browse)
fetch(false)
end
end
end)
end
-- Handle
function cloud:Refresh()
fetch(false)
end
function cloud:LoadMore()
if not loading then
fetch(true)
end
end
-- For backends that push results instead of answering OnFetch.
function cloud:SetConfigs(list, more)
token += 1
busy(false)
clearSkeletons()
records = {}
for _, raw in ipairs(type(list) == "table" and list or {}) do
local record = normalizeRecord(raw)
if record then
table.insert(records, record)
end
end
hasMore = more == true
renderList()
moreButton.Visible = hasMore
setState(#records == 0 and "empty" or nil, emptyText())
end
function cloud:AddConfigs(list, more)
token += 1
busy(false)
clearSkeletons()
for _, raw in ipairs(type(list) == "table" and list or {}) do
local record = normalizeRecord(raw)
if record and not table.find(records, record) then
table.insert(records, record)
buildCard(record, #records)
end
end
hasMore = more == true
moreButton.Visible = hasMore
setState(#records == 0 and "empty" or nil, emptyText())
end
function cloud:UpdateConfig(id, changes)
local record = byId[tostring(id)]
if record and type(changes) == "table" then
local merged = table.clone(changes)
merged.Id = record.Id
normalizeRecord(merged)
refreshRecord(record)
end
end
function cloud:RemoveConfig(id)
local record = byId[tostring(id)]
if record then
removeRecord(record)
if detailRecord == record and currentView == views.Detail then
showView(views.Browse)
end
end
end
function cloud:SetLoading(on)
token += 1
busy(on == true)
clearSkeletons()
if loading then
addSkeletons(4)
setState(nil)
end
end
function cloud:SetError(text)
token += 1
busy(false)
clearSkeletons()
setState(text and "error" or nil, tostring(text or ""))
end
function cloud:Open(id)
local record = byId[tostring(id)]
if record then
window:SelectTab(tab)
openDetail(record)
end
end
function cloud:Back()
showView(views.Browse)
end
function cloud:GetQuery()
return table.clone(query)
end
function cloud:GetFavorites()
local list = {}
for id, kept in pairs(favorites) do
local copy = table.clone(kept)
copy.Id = id
table.insert(list, copy)
end
return list
end
function cloud:GetInstalled()
local list = {}
for id, mark in pairs(installed) do
table.insert(list, { Id = id, Name = mark.Name, Updated = mark.Updated, Record = mark.Record })
end
return list
end
renderFilters(true)
showView(views.Browse)
-- The first fetch waits until the tab is opened, so a hidden tab costs nothing.
local fetched = false
local function firstFetch()
if not fetched and tab._page.Visible then
fetched = true
fetch(false)
end
end
tab._page:GetPropertyChangedSignal("Visible"):Connect(firstFetch)
if type(opts.OnFetch) ~= "function" then
fetched = true
setState("empty", emptyText())
else
task.defer(firstFetch)
end
return cloud
end
Window.CreateCloudConfigs = Window.CloudConfigs
Window.CloudConfig = Window.CloudConfigs
Window.CreateCloudConfig = Window.CloudConfigs
-- Tab badges were removed. These stay as no-ops so older scripts that call
-- them keep running.
function Tab:SetBadge() end
function Tab:GetBadge()
return nil
end
function Window:Dialog(opts)
opts = normalize(opts, { Text = "Content", Message = "Content" })
if self._dialog then
self._dialog.Close()
end
local WIDTH = 300
local PAD = 18
local overlay = create("TextButton", {
Size = UDim2.fromScale(1, 1),
BackgroundColor3 = Color3.new(0, 0, 0),
BackgroundTransparency = 1,
Text = "",
AutoButtonColor = false,
ZIndex = 40,
Parent = self.Body,
})
local card = create("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 0.5, 10),
Size = UDim2.fromOffset(WIDTH, 120),
BackgroundColor3 = Theme.Background,
BackgroundTransparency = 1,
BorderSizePixel = 0,
ZIndex = 41,
Parent = overlay,
})
corner(card, UDim.new(0, 10))
local cardStroke = stroke(card, Theme.Stroke, 1)
local cardScale = create("UIScale", { Scale = 0.94, Parent = card })
local fading = {}
local titleOffset = 0
if opts.Icon then
local holder, image = glowIcon(card, opts.Icon, Theme.Accent, UDim2.new(0, PAD, 0, PAD + 9))
image.ImageTransparency = 1
holder.ZIndex = 42
image.ZIndex = 42
table.insert(fading, { image, "ImageTransparency", 0 })
titleOffset = 24
end
local title = label({
Position = UDim2.fromOffset(PAD + titleOffset, PAD),
Size = UDim2.new(1, -(PAD * 2 + titleOffset), 0, 18),
Text = opts.Title or "Are you sure?",
TextSize = 15,
TextTransparency = 1,
ZIndex = 42,
Parent = card,
})
table.insert(fading, { title, "TextTransparency", 0 })
local contentHeight = 0
if opts.Content then
local content = label({
Position = UDim2.fromOffset(PAD, PAD + 24),
Size = UDim2.new(1, -PAD * 2, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
Text = opts.Content,
TextSize = 13,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextWrapped = true,
TextTransparency = 1,
TextTruncate = Enum.TextTruncate.None,
TextYAlignment = Enum.TextYAlignment.Top,
ZIndex = 42,
Parent = card,
})
table.insert(fading, { content, "TextTransparency", 0 })
contentHeight = math.max(content.TextBounds.Y, 16) + 6
content:GetPropertyChangedSignal("TextBounds"):Connect(function()
local newHeight = math.max(content.TextBounds.Y, 16) + 6
if newHeight ~= contentHeight then
contentHeight = newHeight
card.Size = UDim2.fromOffset(WIDTH, PAD + 24 + contentHeight + 12 + 34 + PAD)
local rowFrame = card:FindFirstChild("ButtonRow")
if rowFrame then
rowFrame.Position = UDim2.fromOffset(PAD, PAD + 24 + contentHeight + 12)
end
end
end)
end
local row = create("Frame", {
Name = "ButtonRow",
Position = UDim2.fromOffset(PAD, PAD + 24 + contentHeight + 12),
Size = UDim2.new(1, -PAD * 2, 0, 34),
BackgroundTransparency = 1,
ZIndex = 42,
Parent = card,
})
create("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 8),
Parent = row,
})
card.Size = UDim2.fromOffset(WIDTH, PAD + 24 + contentHeight + 12 + 34 + PAD)
local dialog = {}
local closed = false
function dialog.Close()
if closed then
return
end
closed = true
if self._dialog == dialog then
self._dialog = nil
end
tween(overlay, { BackgroundTransparency = 1 }, 0.18)
tween(card, { BackgroundTransparency = 1, Position = UDim2.new(0.5, 0, 0.5, 8) }, 0.18, Enum.EasingStyle.Quint)
tween(cardScale, { Scale = 0.96 }, 0.18, Enum.EasingStyle.Quint)
tween(cardStroke, { Transparency = 1 }, 0.12)
for _, entry in ipairs(fading) do
tween(entry[1], { [entry[2]] = 1 }, 0.12)
end
task.delay(0.2, function()
overlay:Destroy()
end)
end
for index, spec in ipairs(opts.Buttons or {}) do
local primary = spec.Variant == "Primary"
local buttonFrame = create("TextButton", {
Size = UDim2.fromOffset(0, 34),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = primary and Theme.Accent or Theme.Surface2,
BackgroundTransparency = 1,
BorderSizePixel = 0,
Text = "",
AutoButtonColor = false,
ClipsDescendants = true,
LayoutOrder = index,
ZIndex = 43,
Parent = row,
})
corner(buttonFrame, UDim.new(0, 7))
local buttonStroke = stroke(buttonFrame, primary and Theme.Accent or Theme.Stroke, 1)
padding(buttonFrame, 14, 14)
local text = label({
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
Text = spec.Title or spec.Name or "OK",
TextSize = 13,
TextColor3 = primary and Theme.AccentDark or Theme.Text,
TextXAlignment = Enum.TextXAlignment.Center,
TextTransparency = 1,
ZIndex = 44,
Parent = buttonFrame,
})
local restBg = primary and 0.12 or 0
local restStroke = primary and 0.4 or 0
table.insert(fading, { buttonFrame, "BackgroundTransparency", restBg })
table.insert(fading, { buttonStroke, "Transparency", restStroke })
table.insert(fading, { text, "TextTransparency", 0 })
buttonFrame.MouseEnter:Connect(function()
if closed then
return
end
if primary then
tween(buttonFrame, { BackgroundTransparency = 0 }, 0.12)
tween(buttonStroke, { Transparency = 0 }, 0.12)
else
tween(buttonStroke, { Color = Theme.StrokeHover }, 0.12)
end
end)
buttonFrame.MouseLeave:Connect(function()
if closed then
return
end
if primary then
tween(buttonFrame, { BackgroundTransparency = restBg }, 0.2)
tween(buttonStroke, { Transparency = restStroke }, 0.2)
else
tween(buttonStroke, { Color = Theme.Stroke }, 0.2)
end
end)
buttonFrame.MouseButton1Click:Connect(function()
dialog.Close()
safeCall(spec.Callback)
end)
end
if opts.CloseOnBackdrop ~= false then
overlay.MouseButton1Click:Connect(function()
dialog.Close()
safeCall(opts.OnCancel)
end)
end
self._dialog = dialog
tween(overlay, { BackgroundTransparency = 0.45 }, 0.25)
tween(card, { BackgroundTransparency = 0, Position = UDim2.fromScale(0.5, 0.5) }, 0.3, Enum.EasingStyle.Quint)
tween(cardStroke, { Transparency = 0 }, 0.25)
tween(cardScale, { Scale = 1 }, 0.4, Enum.EasingStyle.Back)
for _, entry in ipairs(fading) do
tween(entry[1], { [entry[2]] = entry[3] }, 0.25)
end
return dialog
end
function Window:Confirm(opts)
opts = normalize(opts, { Text = "Content", Message = "Content" })
return self:Dialog({
Title = opts.Title or "Are you sure?",
Content = opts.Content,
Icon = opts.Icon,
OnCancel = opts.OnCancel,
Buttons = {
{ Title = opts.CancelText or "Cancel", Callback = opts.OnCancel },
{ Title = opts.ConfirmText or "Confirm", Variant = "Primary", Callback = opts.Callback },
},
})
end
function Window:_listen(kind, handler, owner)
local list = self._inputListeners[kind]
table.insert(list, handler)
local function disconnect()
for index, entry in ipairs(list) do
if entry == handler then
table.remove(list, index)
break
end
end
end
if owner then
owner._listeners = owner._listeners or {}
table.insert(owner._listeners, disconnect)
end
return disconnect
end
function Window:_indicatorY(tab)
local stack = self._tabStack
return (tab._button.AbsolutePosition.Y - stack.AbsolutePosition.Y) / self.Scale.Scale + stack.Position.Y.Offset
end
function Window:_placeIndicator(tab)
local indicator = self.Indicator
local targetY = self:_indicatorY(tab)
if not indicator.Visible then
indicator.Visible = true
indicator.Position = UDim2.fromOffset(8, targetY)
indicator.BackgroundTransparency = 1
tween(indicator, { BackgroundTransparency = 0.35 }, 0.3)
end
tween(indicator, { Position = UDim2.fromOffset(8, targetY) }, 0.45, Enum.EasingStyle.Quint)
end
-- While a tab list or sub tab strip runs past an end, that end gets a fade
-- and a small chip with a chevron and how many tabs are out of sight there.
-- Pressing the chip scrolls most of a view toward them.
function Window:_overflowHint(host, scroller, items, vertical, fadeColor)
local overlay = create("Frame", {
AnchorPoint = scroller.AnchorPoint,
Position = scroller.Position,
Size = scroller.Size,
BackgroundTransparency = 1,
ZIndex = 3,
Parent = host,
})
local function mirror()
overlay.Position = scroller.Position
overlay.Size = scroller.Size
overlay.Visible = scroller.Visible
end
for _, property in ipairs({ "Position", "Size", "Visible" }) do
scroller:GetPropertyChangedSignal(property):Connect(mirror)
end
local function axis(vector)
return vertical and vector.Y or vector.X
end
local function scale()
return self.Scale and self.Scale.Scale or 1
end
local function scrollBy(direction)
local view = axis(scroller.AbsoluteWindowSize) / scale()
local limit = math.max(axis(scroller.AbsoluteCanvasSize) / scale() - view, 0)
local target = math.clamp(axis(scroller.CanvasPosition) + direction * view * 0.75, 0, limit)
tween(scroller, { CanvasPosition = vertical and Vector2.new(0, target) or Vector2.new(target, 0) }, 0.35, Enum.EasingStyle.Quint)
end
local hints = {}
for _, direction in ipairs({ -1, 1 }) do
local far = direction > 0
local fade = create("Frame", {
AnchorPoint = vertical and Vector2.new(0, far and 1 or 0) or Vector2.new(far and 1 or 0, 0),
Position = vertical and UDim2.fromScale(0, far and 1 or 0) or UDim2.fromScale(far and 1 or 0, 0),
Size = vertical and UDim2.new(1, 0, 0, 34) or UDim2.new(0, 48, 1, 0),
BackgroundColor3 = fadeColor,
BackgroundTransparency = 1,
BorderSizePixel = 0,
Visible = false,
Parent = overlay,
})
create("UIGradient", {
Rotation = vertical and (far and 270 or 90) or (far and 180 or 0),
Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0),
NumberSequenceKeypoint.new(0.4, 0.2),
NumberSequenceKeypoint.new(1, 1),
}),
Parent = fade,
})
local home = vertical and UDim2.new(0.5, 0, far and 1 or 0, far and -5 or 5) or UDim2.new(far and 1 or 0, far and -2 or 2, 0.5, 0)
local chip = create("TextButton", {
AnchorPoint = vertical and Vector2.new(0.5, far and 1 or 0) or Vector2.new(far and 1 or 0, 0.5),
Position = home,
Size = UDim2.fromOffset(0, 20),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = Theme.Surface3,
BackgroundTransparency = 1,
BorderSizePixel = 0,
Text = "",
AutoButtonColor = false,
Visible = false,
ZIndex = 2,
Parent = overlay,
})
corner(chip, UDim.new(1, 0))
local chipStroke = stroke(chip, Theme.Stroke, 1)
padding(chip, 7, 7)
create("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 3),
Parent = chip,
})
local arrow = create("ImageLabel", {
Size = UDim2.fromOffset(12, 12),
BackgroundTransparency = 1,
ImageColor3 = Theme.Accent,
ImageTransparency = 1,
ScaleType = Enum.ScaleType.Fit,
LayoutOrder = 1,
ZIndex = 2,
Parent = chip,
})
applyIcon(arrow, vertical and (far and "chevron-down" or "chevron-up") or (far and "chevron-right" or "chevron-left"))
local arrowFade = "ImageTransparency"
if arrow.Image == "" then
arrow:Destroy()
arrow = label({
Size = UDim2.fromOffset(10, 20),
Text = "›",
TextSize = 16,
TextColor3 = Theme.Accent,
TextTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Center,
TextTruncate = Enum.TextTruncate.None,
Rotation = vertical and (far and 90 or -90) or (far and 0 or 180),
LayoutOrder = 1,
ZIndex = 2,
Parent = chip,
})
arrowFade = "TextTransparency"
end
local count = label({
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
Text = "",
TextSize = 11,
TextColor3 = Theme.Text,
TextTransparency = 1,
TextTruncate = Enum.TextTruncate.None,
Visible = false,
LayoutOrder = 2,
ZIndex = 2,
Parent = chip,
})
chip.MouseEnter:Connect(function()
if not usingTouch() then
tween(chipStroke, { Color = Theme.StrokeHover }, 0.12)
end
end)
chip.MouseLeave:Connect(function()
tween(chipStroke, { Color = Theme.Stroke }, 0.2)
end)
chip.MouseButton1Click:Connect(function()
scrollBy(direction)
end)
hints[direction] = {
Direction = direction,
Fade = fade,
Chip = chip,
Stroke = chipStroke,
Arrow = arrow,
ArrowFade = arrowFade,
Count = count,
Home = home,
Shown = false,
Generation = 0,
}
end
local function show(hint, on)
if hint.Shown == on then
return
end
hint.Shown = on
hint.Generation += 1
local away = hint.Home + (vertical and UDim2.fromOffset(0, hint.Direction * 6) or UDim2.fromOffset(hint.Direction * 6, 0))
local value = on and 0 or 1
if on then
hint.Fade.Visible = true
hint.Chip.Visible = true
hint.Chip.Position = away
tween(hint.Chip, { Position = hint.Home, BackgroundTransparency = 0.08 }, 0.3, Enum.EasingStyle.Quint)
else
tween(hint.Chip, { Position = away, BackgroundTransparency = 1 }, 0.2, Enum.EasingStyle.Quint)
local generation = hint.Generation
task.delay(0.21, function()
if hint.Generation == generation then
hint.Fade.Visible = false
hint.Chip.Visible = false
end
end)
end
tween(hint.Fade, { BackgroundTransparency = value }, 0.2)
tween(hint.Stroke, { Transparency = value }, 0.2)
tween(hint.Arrow, { [hint.ArrowFade] = value }, 0.2)
tween(hint.Count, { TextTransparency = value }, 0.2)
end
local function refresh()
local view = axis(scroller.AbsoluteWindowSize)
local overflow = (axis(scroller.AbsoluteCanvasSize) - view) / scale()
local position = axis(scroller.CanvasPosition)
local start = axis(scroller.AbsolutePosition)
local before, after = 0, 0
for _, item in ipairs(items:GetChildren()) do
if item:IsA("GuiObject") and item.Visible and axis(item.AbsoluteSize) > 0 then
local centre = axis(item.AbsolutePosition) + axis(item.AbsoluteSize) / 2 - start
if centre < 0 then
before += 1
elseif centre > view then
after += 1
end
end
end
for direction, hint in pairs(hints) do
local hidden = direction < 0 and before or after
hint.Count.Text = tostring(hidden)
hint.Count.Visible = hidden > 0
show(hint, overflow > 1 and (direction < 0 and position > 1 or direction > 0 and position < overflow - 1))
end
end
for _, property in ipairs({ "CanvasPosition", "AbsoluteCanvasSize", "AbsoluteWindowSize" }) do
scroller:GetPropertyChangedSignal(property):Connect(refresh)
end
task.defer(refresh)
end
function Window:_scrollTabIntoView(tab)
local list = self.TabList
local scale = self.Scale.Scale
local top = (tab._button.AbsolutePosition.Y - list.AbsolutePosition.Y) / scale + list.CanvasPosition.Y
local bottom = top + tab._button.AbsoluteSize.Y / scale
local view = list.AbsoluteWindowSize.Y / scale
local y = list.CanvasPosition.Y
if top - 8 < y then
y = top - 8
elseif bottom + 8 > y + view then
y = bottom + 8 - view
else
return
end
tween(list, { CanvasPosition = Vector2.new(0, math.max(0, y)) }, 0.35, Enum.EasingStyle.Quint)
end
function Window:_closePopups()
if self._closePopup then
self._closePopup()
end
end
-- Popped-out groupboxes float on one screen-wide layer above the window. It
-- carries the window's scale, so a panel keeps the size it had in its column,
-- and it shows while the window is open (minimized included).
function Window:_popoutLayer()
if self._popLayer then
return self._popLayer
end
local layer = create("Frame", {
Name = "PopOuts",
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
ZIndex = 20,
Parent = self.Gui,
})
local layerScale = create("UIScale", { Parent = layer })
self._popLayer = layer
self._popPanels = {}
self:_listen("Render", function()
local scale = math.max(self.Scale.Scale, 0.05)
if math.abs(layerScale.Scale - scale) > 1e-4 then
layerScale.Scale = scale
layer.Size = UDim2.fromScale(1 / scale, 1 / scale)
end
layer.Visible = self.Open == true and not self._destroyed
end)
return layer
end
-- Where a control's popup is drawn: the pop-out layer for one inside a
-- floating panel, otherwise the window body.
function Window:_popupHost(object)
local layer = self._popLayer
if layer and object and object:IsDescendantOf(layer) then
return layer
end
return self.Body
end
-- A floating panel under the point counts as a control, so pressing it never
-- starts a window drag.
function Window:_overPopout(point)
local layer = self._popLayer
if not layer or not layer.Visible then
return false
end
for _, panel in ipairs(self._popPanels) do
if panel.Parent and panel.Visible and pointInside(point, panel) then
return true
end
end
return false
end
-- Fades the whole body by lending it to the fader CanvasGroup, then hands it
-- back to the root once fully shown so it renders directly again.
function Window:_fade(target, duration)
local fader, body = self._fader, self.Body
self._fadeGeneration = (self._fadeGeneration or 0) + 1
local generation = self._fadeGeneration
if body.Parent ~= fader then
fader.GroupTransparency = self._bodyAlpha
body.Parent = fader
end
fader.Visible = true
self._bodyAlpha = target
tween(fader, { GroupTransparency = target }, duration)
task.delay(duration + 0.03, function()
if self._fadeGeneration == generation and target <= 0 and not self._destroyed then
body.Parent = self.Root
fader.Visible = false
end
end)
end
function Window:SelectTab(tab)
if self.CurrentTab == tab then
return
end
local previous = self.CurrentTab
self:_resetSearch()
self:_closePopups()
self.CurrentTab = tab
self:_settleTransition()
local generation = (self._transitionGeneration or 0) + 1
self._transitionGeneration = generation
if previous then
tween(previous._label, { TextColor3 = Theme.Muted }, 0.3)
if previous._icon then
tintIcon(previous._icon, Theme.Muted, false, 0.3)
end
local outLayer = self._outLayer
previous._page.Parent = outLayer
self._outPage = previous._page
outLayer.GroupTransparency = 0
outLayer.Position = UDim2.fromOffset(0, 0)
outLayer.Visible = true
tween(outLayer, { GroupTransparency = 1, Position = UDim2.fromOffset(0, -6) }, 0.2, Enum.EasingStyle.Quint)
task.delay(0.18, function()
if self._transitionGeneration == generation then
self:_settleOut()
end
end)
end
tween(tab._button, { BackgroundTransparency = 1 }, 0.2)
tween(tab._stroke, { Transparency = 1 }, 0.2)
tween(tab._label, { TextColor3 = Theme.Text }, 0.3)
if tab._icon then
tintIcon(tab._icon, Theme.Text, true, 0.3)
end
self:_placeIndicator(tab)
self:_scrollTabIntoView(tab)
local page = tab._page
page.Position = UDim2.fromOffset(0, 0)
page.Visible = true
page.Parent = self._inLayer
self._inPage = page
fadeSlideIn(self._inLayer)
task.delay(0.32, function()
if self._transitionGeneration == generation then
self:_settleIn()
end
end)
end
function Window:_settleOut()
local page = self._outPage
if page then
page.Parent = self.Content
page.Visible = false
self._outPage = nil
end
self._outLayer.Visible = false
end
function Window:_settleIn()
local page = self._inPage
if page then
page.Parent = self.Content
page.Position = UDim2.fromOffset(0, 0)
self._inPage = nil
end
self._inLayer.Visible = false
end
function Window:_settleTransition()
self:_settleOut()
self:_settleIn()
end
-- Backdrop: a black tint over the game while the window is up, with an
-- optional weather layer (rain, snow or hell fire embers) drifting through it.
-- It fades out when the window is minimized or hidden and sweeps back in with
-- a gust when it comes back.
local WEATHER = {
-- The rain texture is a wide sheet with one faint streak down the middle
-- (about 1/60 of its width, 57% of its height), so it is stretched much
-- wider than the drop it draws. See RAIN_STREAK.
Rain = { Image = "rbxassetid://241868005", Count = 90, Stretch = true },
Snow = { Image = "rbxassetid://99851851", Count = 60 },
Ember = { Image = "rbxassetid://242205518", Count = 55 },
-- Drawn with plain frames and text, no images: petals are tinted pills
-- that flip as they fall, matrix columns are text strips.
Sakura = { Petal = true, Count = 46, Glow = Color3.fromRGB(255, 150, 190), GlowAmount = 0.1 },
Fireflies = { Image = Assets.Glow, Count = 34, Glow = Color3.fromRGB(70, 150, 60), GlowAmount = 0.16 },
Matrix = {
Text = true,
Count = 38,
Glow = Color3.fromRGB(30, 200, 90),
GlowAmount = 0.07,
Glyphs = "01234567890123456789アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン+-*=<>:",
},
}
local WEATHER_NAMES = {
rain = "Rain",
snow = "Snow",
ember = "Ember",
embers = "Ember",
fire = "Ember",
hellfire = "Ember",
sakura = "Sakura",
petals = "Sakura",
cherryblossom = "Sakura",
firefly = "Fireflies",
fireflies = "Fireflies",
matrix = "Matrix",
matrixrain = "Matrix",
code = "Matrix",
}
local RAIN_STREAK = Vector2.new(7 / 420, 151 / 263)
local weatherRandom = Random.new()
-- "UI" keeps the weather inside the window, behind its content; anything
-- else drifts it across the whole screen.
local function weatherMode(mode)
if type(mode) == "string" and ({ ui = true, window = true, inside = true })[mode:lower()] then
return "UI"
end
return "Screen"
end
local function weatherName(name)
if type(name) ~= "string" then
return "None"
end
return WEATHER_NAMES[(name:lower():gsub("[%s_%-]", ""))] or "None"
end
local function roll(min, max)
return weatherRandom:NextNumber(min, max)
end
-- Places a particle. `anywhere` scatters it over the whole screen (first
-- frame after the backdrop appears), otherwise it enters from its edge.
local function seedParticle(kind, particle, width, height, anywhere)
particle.Time = roll(0, 10)
particle.Phase = roll(0, math.pi * 2)
if kind == "Rain" then
local w = roll(2, 3.5)
particle.Size = Vector2.new(w, roll(26, 52))
particle.VY = roll(950, 1400)
particle.VX = particle.VY * 0.12
particle.Sway, particle.Spin = 0, 0
particle.Rotation = -math.deg(math.atan2(particle.VX, particle.VY))
particle.Alpha = roll(0, 0.35)
particle.X = roll(-0.15 * width, width)
particle.Y = anywhere and roll(-40, height) or roll(-120, -particle.Size.Y)
elseif kind == "Snow" then
local s = roll(6, 17)
particle.Size = Vector2.new(s, s)
particle.VY = roll(30, 55) * (0.6 + s / 17)
particle.VX = roll(-8, 14)
particle.Sway = roll(12, 38)
particle.Frequency = roll(0.5, 1.3)
particle.Spin = roll(-60, 60)
particle.Rotation = roll(0, 360)
particle.Alpha = roll(0.05, 0.45)
particle.X = roll(0, width)
particle.Y = anywhere and roll(-20, height) or roll(-60, -s)
elseif kind == "Sakura" then
local s = roll(7, 14)
particle.Size = Vector2.new(s, s * roll(0.55, 0.75))
particle.VY = roll(35, 70)
particle.VX = roll(18, 45)
particle.Sway = roll(14, 34)
particle.Frequency = roll(0.6, 1.4)
particle.Flutter = roll(1.2, 2.6)
particle.Spin = roll(-90, 90)
particle.Rotation = roll(0, 360)
particle.Alpha = roll(0.05, 0.35)
particle.Color = Color3.fromRGB(255, 205, 220):Lerp(Color3.fromRGB(255, 140, 180), roll(0, 1))
particle.X = roll(-0.25 * width, width)
particle.Y = anywhere and roll(-20, height) or roll(-60, -s)
elseif kind == "Fireflies" then
-- They wander instead of crossing the screen, and live a few seconds:
-- fade in, pulse, fade out, then turn up somewhere else.
local s = roll(14, 28)
particle.Size = Vector2.new(s, s)
particle.VY = roll(-14, 8)
particle.VX = roll(-12, 12)
particle.Sway = roll(10, 30)
particle.SwayY = roll(8, 22)
particle.Frequency = roll(0.3, 0.8)
particle.Spin, particle.Rotation = 0, 0
particle.Alpha = roll(0, 0.25)
particle.Life = roll(5, 11)
particle.Age = anywhere and roll(0, particle.Life) or 0
particle.Pulse = roll(1.5, 3.5)
particle.Color = Color3.fromRGB(200, 255, 120):Lerp(Color3.fromRGB(255, 230, 120), roll(0, 1))
particle.X = roll(0, width)
particle.Y = roll(height * 0.15, height)
elseif kind == "Matrix" then
local definition = WEATHER.Matrix
if not definition.List then
definition.List = {}
for _, code in utf8.codes(definition.Glyphs) do
table.insert(definition.List, utf8.char(code))
end
end
local list = definition.List
local s = math.floor(roll(12, 19))
local length = math.floor(roll(8, 20))
particle.TextSize = s
particle.Size = Vector2.new(s + 6, s * length)
particle.VY = roll(160, 360) * (s / 15)
particle.VX, particle.Sway, particle.Spin, particle.Rotation = 0, 0, 0, 0
-- Smaller columns read as further away: dimmer.
particle.Alpha = roll(0, 0.25) + (19 - s) * 0.04
particle.X = math.floor(roll(0, width) / 18) * 18 + 9
particle.Y = anywhere and roll(-particle.Size.Y / 2, height) or -particle.Size.Y / 2 - roll(0, 240)
particle.Chars = {}
for index = 1, length do
particle.Chars[index] = list[math.random(#list)]
end
particle.Shuffle = roll(0.05, 0.2)
else
local s = roll(4, 12)
particle.Size = Vector2.new(s, s)
particle.VY = -roll(60, 170)
particle.VX = roll(-12, 12)
particle.Sway = roll(8, 28)
particle.Frequency = roll(1, 2.4)
particle.Spin = roll(-140, 140)
particle.Rotation = roll(0, 360)
particle.Alpha = roll(0, 0.3)
particle.Color = Color3.fromRGB(255, 70, 20):Lerp(Color3.fromRGB(255, 190, 70), roll(0, 1))
particle.X = roll(0, width)
particle.Y = anywhere and roll(height * 0.2, height + 10) or height + roll(10, 80)
end
local label = particle.Label
if kind == "Rain" then
label.Size = UDim2.fromOffset(particle.Size.X / RAIN_STREAK.X, particle.Size.Y / RAIN_STREAK.Y)
else
label.Size = UDim2.fromOffset(particle.Size.X, particle.Size.Y)
end
if kind == "Matrix" then
label.TextSize = particle.TextSize
label.Text = table.concat(particle.Chars, "\n")
elseif kind == "Sakura" then
label.BackgroundColor3 = particle.Color
else
label.ImageColor3 = particle.Color
or (kind == "Rain" and Color3.fromRGB(190, 210, 245) or Color3.new(1, 1, 1))
end
end
function Window:_buildBackdrop(opts)
local layer = create("Frame", {
Name = "Backdrop",
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
Visible = false,
ZIndex = 0,
Parent = self.Gui,
})
local tint = create("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundColor3 = Color3.new(0, 0, 0),
BackgroundTransparency = 1,
BorderSizePixel = 0,
ZIndex = 0,
Parent = layer,
})
-- Darker towards the top and bottom edges.
create("UIGradient", {
Rotation = 90,
Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0),
NumberSequenceKeypoint.new(0.5, 0.3),
NumberSequenceKeypoint.new(1, 0),
}),
Parent = tint,
})
-- Heat rising from the bottom of the screen, hell fire only.
local heat = create("Frame", {
AnchorPoint = Vector2.new(0, 1),
Position = UDim2.fromScale(0, 1),
Size = UDim2.fromScale(1, 0.5),
BackgroundColor3 = Color3.fromRGB(255, 60, 15),
BackgroundTransparency = 1,
BorderSizePixel = 0,
ZIndex = 1,
Parent = layer,
})
create("UIGradient", {
Rotation = 90,
Transparency = NumberSequence.new(1, 0),
Parent = heat,
})
-- Holds the heat and the particles so both move together between the
-- screen and the window.
local field = create("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
ClipsDescendants = true,
ZIndex = 2,
Parent = layer,
})
heat.Parent = field
self._backdrop = layer
self._backdropTintFrame = tint
self._backdropHeat = heat
self._weatherField = field
self._weatherParticles = {}
self._backdropFade = 0
self._weatherKick = 0
self._backdropShown = false
self._backdropEnabled = opts.Enabled ~= false
self._backdropTint = math.clamp(tonumber(opts.Tint) or 0.45, 0, 1)
self._backdropDim = opts.Dim ~= false
self._dimLevel = self._backdropDim and 1 or 0
self._weatherDensity = math.clamp(tonumber(opts.Density) or 1, 0, 3)
self._weatherSpeed = math.clamp(tonumber(opts.Speed) or 1, 0.1, 3)
self.Weather = weatherName(opts.Weather == nil and "Matrix" or opts.Weather)
self.WeatherMode = "Screen"
self:SetWeatherMode(opts.Mode)
self:_rebuildWeather()
table.insert(self._frameSteps, function(deltaTime)
self:_stepBackdrop(deltaTime)
end)
end
-- The area the particles cover, in the field's own (unscaled) units.
function Window:_weatherSize()
if self.WeatherMode == "UI" then
return Vector2.new(self.Root.Size.X.Offset, self.Root.Size.Y.Offset)
end
return self.Gui.AbsoluteSize
end
function Window:_rebuildWeather()
for _, particle in ipairs(self._weatherParticles) do
particle.Label:Destroy()
end
self._weatherParticles = {}
local definition = WEATHER[self.Weather]
if not definition then
return
end
-- The glow along the bottom: red heat for hell fire, a soft tint for the
-- others that ask for one.
self._backdropHeat.BackgroundColor3 = definition.Glow or Color3.fromRGB(255, 60, 15)
local size = self:_weatherSize()
-- The window is a fraction of the screen, so it gets fewer particles.
local count = definition.Count * self._weatherDensity * (self.WeatherMode == "UI" and 0.45 or 1)
for _ = 1, math.floor(count + 0.5) do
local instance, property
if definition.Text then
property = "TextTransparency"
instance = create("TextLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
Text = "",
TextColor3 = Color3.fromRGB(90, 255, 140),
TextTransparency = 1,
FontFace = Font.fromEnum(Enum.Font.Code),
TextXAlignment = Enum.TextXAlignment.Center,
TextYAlignment = Enum.TextYAlignment.Top,
LineHeight = 1,
ZIndex = 2,
Parent = self._weatherField,
})
-- A fading tail and a pale leading glyph.
create("UIGradient", {
Rotation = 90,
Color = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.new(1, 1, 1)),
ColorSequenceKeypoint.new(0.88, Color3.new(1, 1, 1)),
ColorSequenceKeypoint.new(1, Color3.fromRGB(235, 255, 240)),
}),
Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 1),
NumberSequenceKeypoint.new(0.6, 0.45),
NumberSequenceKeypoint.new(1, 0),
}),
Parent = instance,
})
elseif definition.Petal then
property = "BackgroundTransparency"
instance = create("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundColor3 = Color3.fromRGB(255, 190, 210),
BackgroundTransparency = 1,
BorderSizePixel = 0,
ZIndex = 2,
Parent = self._weatherField,
})
corner(instance, UDim.new(1, 0))
create("UIGradient", {
Rotation = 35,
Color = ColorSequence.new(Color3.new(1, 1, 1), Color3.fromRGB(235, 200, 215)),
Parent = instance,
})
else
property = "ImageTransparency"
instance = create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
Image = definition.Image,
ImageTransparency = 1,
ScaleType = definition.Stretch and Enum.ScaleType.Stretch or Enum.ScaleType.Fit,
ZIndex = 2,
Parent = self._weatherField,
})
end
local particle = { Label = instance, Property = property }
seedParticle(self.Weather, particle, size.X, size.Y, true)
table.insert(self._weatherParticles, particle)
end
end
function Window:_stepBackdrop(deltaTime)
local target = self._backdropShown and 1 or 0
local fade = self._backdropFade
if fade == target and target == 0 then
return
end
if fade < target then
fade = math.min(fade + deltaTime / 0.45, target)
elseif fade > target then
fade = math.max(fade - deltaTime / 0.25, target)
end
self._backdropFade = fade
if fade <= 0 then
self._backdrop.Visible = false
self._weatherField.Visible = false
return
end
local eased = 1 - (1 - fade) ^ 3
local kind = self.Weather
-- The dim eases on its own so turning it off doesn't touch the weather.
local dim = self._dimLevel
if self._backdropDim then
dim = math.min(dim + deltaTime / 0.3, 1)
else
dim = math.max(dim - deltaTime / 0.3, 0)
end
self._dimLevel = dim
self._backdropTintFrame.BackgroundTransparency = 1 - self._backdropTint * eased * dim
local definition = WEATHER[kind]
self._backdropHeat.BackgroundTransparency = kind == "Ember"
and 1 - (0.22 + math.sin(os.clock() * 2.1) * 0.05) * eased
or definition and definition.GlowAmount and 1 - (definition.GlowAmount + math.sin(os.clock() * 0.8) * definition.GlowAmount * 0.25) * eased
or 1
-- The gust on restore: particles rush in fast and settle back.
local kick = self._weatherKick
self._weatherKick = math.max(kick - deltaTime * 1.3, 0)
local speed = self._weatherSpeed * (1 + kick * kick * 2.5)
local size = self:_weatherSize()
local width, height = size.X, size.Y
for _, particle in ipairs(self._weatherParticles) do
particle.Time += deltaTime
particle.X += particle.VX * deltaTime * speed
particle.Y += particle.VY * deltaTime * speed
particle.Rotation += particle.Spin * deltaTime * speed
local margin = math.max(particle.Size.X, particle.Size.Y)
if particle.Life then
particle.Age += deltaTime
end
if (particle.Life and particle.Age > particle.Life)
or (not particle.Life and ((particle.VY > 0 and particle.Y > height + margin) or (particle.VY < 0 and particle.Y < -margin))) then
seedParticle(kind, particle, width, height, false)
elseif particle.X > width + margin * 4 then
particle.X -= width + margin * 6
elseif particle.X < -margin * 4 then
particle.X += width + margin * 6
end
local x = particle.X
local alpha = 1 - particle.Alpha
if particle.Sway ~= 0 then
x += math.sin(particle.Time * particle.Frequency + particle.Phase) * particle.Sway
end
local y = particle.Y
local label = particle.Label
if kind == "Ember" then
-- Flicker, and burn out on the way up.
alpha *= math.clamp(particle.Y / (height * 0.85), 0, 1) ^ 0.6
alpha *= 0.75 + math.sin(particle.Time * 9 + particle.Phase) * 0.25
elseif kind == "Sakura" then
-- Flip: the petal narrows and widens as it turns over.
local turn = math.abs(math.cos(particle.Time * particle.Flutter + particle.Phase))
label.Size = UDim2.fromOffset(particle.Size.X * (0.3 + turn * 0.7), particle.Size.Y)
elseif kind == "Fireflies" then
y += math.cos(particle.Time * particle.Frequency * 1.3 + particle.Phase) * particle.SwayY
local age, life = particle.Age, particle.Life
alpha *= math.clamp(math.min(age / 1.2, (life - age) / 1.2), 0, 1)
alpha *= 0.5 + math.sin(particle.Time * particle.Pulse + particle.Phase) * 0.5
elseif kind == "Matrix" then
particle.Shuffle -= deltaTime * speed
if particle.Shuffle <= 0 then
particle.Shuffle = roll(0.05, 0.2)
local list = WEATHER.Matrix.List
local chars = particle.Chars
chars[math.random(#chars)] = list[math.random(#list)]
chars[#chars] = list[math.random(#list)]
label.Text = table.concat(chars, "\n")
end
end
label.Position = UDim2.fromOffset(x, y)
label.Rotation = particle.Rotation
label[particle.Property or "ImageTransparency"] = 1 - alpha * eased
end
end
function Window:_refreshBackdrop(gust)
if not self._backdrop then
return
end
local shown = self._backdropEnabled and self._introDone and self.Open and not self.Minimized and not self._destroyed
if shown and not self._backdropShown then
self._backdrop.Visible = true
self._weatherField.Visible = true
if self._backdropFade <= 0 then
local size = self:_weatherSize()
for _, particle in ipairs(self._weatherParticles) do
seedParticle(self.Weather, particle, size.X, size.Y, true)
end
end
if gust then
self._weatherKick = 1
end
end
self._backdropShown = shown == true
end
function Window:SetBackdrop(enabled)
if not self._backdrop then
return
end
self._backdropEnabled = enabled ~= false
self:_refreshBackdrop(true)
end
function Window:SetBackdropTint(amount)
if not self._backdrop then
return
end
self._backdropTint = math.clamp(tonumber(amount) or 0.45, 0, 1)
end
-- Turns the black tint on or off, leaving the weather running.
function Window:SetDim(enabled)
if not self._backdrop then
return
end
self._backdropDim = enabled ~= false
end
function Window:SetWeatherMode(mode)
if not self._backdrop then
return
end
mode = weatherMode(mode)
local field = self._weatherField
if mode == "UI" then
-- Under everything else in the body: sidebar, content and glows.
field.ZIndex = 0
field.Parent = self.Body
else
field.ZIndex = 2
field.Parent = self._backdrop
end
if mode == self.WeatherMode then
return
end
self.WeatherMode = mode
self:_rebuildWeather()
if self._backdropShown then
self._weatherKick = 1
end
end
function Window:SetWeather(name)
if not self._backdrop then
return
end
local kind = weatherName(name)
if kind == self.Weather then
return
end
self.Weather = kind
self:_rebuildWeather()
if self._backdropShown then
self._weatherKick = 1
end
end
function Window:SetWeatherDensity(density)
if not self._backdrop then
return
end
self._weatherDensity = math.clamp(tonumber(density) or 1, 0, 3)
self:_rebuildWeather()
end
function Window:SetWeatherSpeed(speed)
if not self._backdrop then
return
end
self._weatherSpeed = math.clamp(tonumber(speed) or 1, 0.1, 3)
end
-- Minimize: the window folds into a Dynamic Island style pill hanging from
-- the top of the screen. Compact, it shows the logo and a live meter of
-- running toggles. Hovering springs it open into a session card: the
-- player, the game and session time, frame rate and ping meters, the server
-- and running counts, and the key that reopens the window. A
-- notification while minimized opens it briefly to show it. Click or tap
-- restores.
-- One table, not separate locals: the main chunk is at Luau's 200 local limit.
local Mini = {
-- Sized after the iPhone 18 island: a slimmer, shorter pill sitting close
-- to the top edge, opening into a wide card with deep rounded corners.
Top = 8, -- gap from the top of the screen
Width = TOUCH and 176 or 168, -- compact
Height = TOUCH and 36 or 32,
Wide = 340, -- expanded and activity
Radius = 34, -- corner of the expanded and activity cards
Bars = 4,
Pinned = 6,
}
-- The morph is carried by a plain "ghost" panel drawn over the window. The
-- window body itself never moves or fades, so nothing has to be laid out
-- again mid-animation: the ghost covers it, the window is hidden underneath,
-- and on the way back the window is shown under the ghost before it clears.
local function makeGhost(window, position, size, radius)
if window._ghost then
window._ghost:Destroy()
end
local ghost = create("Frame", {
Position = UDim2.fromOffset(position.X, position.Y),
Size = UDim2.fromOffset(size.X, size.Y),
BackgroundColor3 = Theme.Background,
BackgroundTransparency = 1,
BorderSizePixel = 0,
ZIndex = 29,
Parent = window.Gui,
})
local ghostCorner = Library._fixedCorner(ghost, UDim.new(0, radius))
local ghostStroke = stroke(ghost, Theme.Stroke, 1)
window._ghost = ghost
return ghost, ghostStroke, ghostCorner
end
local function morphGhost(ghost, ghostCorner, position, size, radius, duration)
tween(ghost, {
Position = UDim2.fromOffset(position.X, position.Y),
Size = UDim2.fromOffset(size.X, size.Y),
}, duration, Enum.EasingStyle.Quint)
tween(ghostCorner, { CornerRadius = UDim.new(0, radius) }, duration, Enum.EasingStyle.Quint)
end
local function dissolve(window, ghost, ghostStroke, duration)
tween(ghost, { BackgroundTransparency = 1 }, duration, Enum.EasingStyle.Quad)
tween(ghostStroke, { Transparency = 1 }, duration, Enum.EasingStyle.Quad)
task.delay(duration + 0.02, function()
if window._ghost == ghost then
window._ghost = nil
end
ghost:Destroy()
end)
end
function Window:_buildMiniBar()
if self.MiniBar then
return self.MiniBar
end
local gui = self.Gui
-- The island itself: a black pill hanging from the top of the screen. It
-- is a canvas so its rounded shape clips whatever layer is showing.
local island = create("CanvasGroup", {
Name = "Island",
AnchorPoint = Vector2.new(0.5, 0),
Position = UDim2.new(0.5, 0, 0, Mini.Top),
Size = UDim2.fromOffset(Mini.Width, Mini.Height),
BackgroundColor3 = Color3.new(0, 0, 0),
BorderSizePixel = 0,
GroupTransparency = 1,
Visible = false,
ZIndex = 30,
Parent = gui,
})
local islandCorner = Library._fixedCorner(island, UDim.new(0, Mini.Height / 2))
local islandScale = create("UIScale", { Parent = island })
self._miniScale = islandScale
-- How far the split bubble is out of the pill: drawn back in while the
-- island is open, budded off again once it settles to the pill.
local merge = create("NumberValue", { Value = 1 })
local mergeTween
self._miniOrb = island
-- The wide layers keep a fixed width, so nothing reflows while the island
-- grows around them.
local function layer(name)
return create("CanvasGroup", {
Name = name,
AnchorPoint = Vector2.new(0.5, 0),
Position = UDim2.fromScale(0.5, 0),
Size = UDim2.new(0, Mini.Wide, 0, 400),
BackgroundTransparency = 1,
GroupTransparency = 1,
Parent = island,
})
end
-- Compact: the logo leading, a live meter of running toggles trailing.
-- A graphite surface, lit from the top, so the pill isn't a flat black
-- slab; it fades with the layer, leaving the open card black.
local compact = create("CanvasGroup", {
Name = "Compact",
Size = UDim2.fromScale(1, 1),
BackgroundColor3 = Color3.fromRGB(30, 30, 34),
BorderSizePixel = 0,
Parent = island,
})
create("UIGradient", {
Rotation = 90,
Color = ColorSequence.new(Color3.fromRGB(255, 255, 255), Color3.fromRGB(120, 120, 120)),
Parent = compact,
})
-- The logo sits in a soft accent disc, like an app glyph.
local logoDisc = create("Frame", {
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, 5, 0.5, 0),
Size = UDim2.fromOffset(Mini.Height - 10, Mini.Height - 10),
BackgroundColor3 = Theme.Accent,
BackgroundTransparency = 0.82,
BorderSizePixel = 0,
Parent = compact,
})
corner(logoDisc, UDim.new(1, 0))
local logo = create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromOffset(16, 16),
BackgroundTransparency = 1,
ImageColor3 = Theme.Accent,
ScaleType = Enum.ScaleType.Fit,
Parent = logoDisc,
})
applyIcon(logo, self._logoIcon, true)
-- The script's name beside the logo, so the pill reads at a glance.
local miniTitle = label({
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, Mini.Height + 2, 0.5, 0),
Size = UDim2.new(1, -Mini.Height - 2 - 72, 0, 18),
Text = self.Title,
TextSize = 13,
FontFace = Fonts.Bold,
TextColor3 = Color3.fromRGB(244, 244, 246),
Parent = compact,
})
local meter = create("Frame", {
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, -14, 0.5, 0),
Size = UDim2.fromOffset(0, 18),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundTransparency = 1,
Parent = compact,
})
create("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 7),
Parent = meter,
})
local barsHolder = create("Frame", {
Size = UDim2.fromOffset(Mini.Bars * 3 + (Mini.Bars - 1) * 2, 16),
BackgroundTransparency = 1,
Visible = false,
LayoutOrder = 1,
Parent = meter,
})
local bars = {}
for index = 1, Mini.Bars do
local bar = create("Frame", {
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, (index - 1) * 5, 0.5, 0),
Size = UDim2.fromOffset(3, 3),
BackgroundColor3 = Theme.Muted,
BorderSizePixel = 0,
Parent = barsHolder,
})
corner(bar, UDim.new(1, 0))
bars[index] = bar
end
local countLabel = label({
Size = UDim2.fromOffset(0, 18),
AutomaticSize = Enum.AutomaticSize.X,
Text = "",
TextSize = 13,
FontFace = Fonts.Bold,
TextTruncate = Enum.TextTruncate.None,
Visible = false,
LayoutOrder = 2,
Parent = meter,
})
-- Idle (nothing running): a health dot tinted by frame rate and the
-- session clock, so the pill never sits empty.
local idleDot = create("Frame", {
Size = UDim2.fromOffset(6, 6),
BackgroundColor3 = Theme.Accent,
BorderSizePixel = 0,
LayoutOrder = -1,
Parent = meter,
})
corner(idleDot, UDim.new(1, 0))
local idleLabel = label({
Size = UDim2.fromOffset(0, 18),
AutomaticSize = Enum.AutomaticSize.X,
Text = "0s",
TextSize = 12,
FontFace = Fonts.Bold,
TextColor3 = Color3.fromRGB(220, 220, 224),
TextTruncate = Enum.TextTruncate.None,
LayoutOrder = 0,
Parent = meter,
})
-- Expanded, on hover: the player's session.
local expanded = layer("Expanded")
local expandedList = create("Frame", {
Position = UDim2.fromOffset(16, 14),
Size = UDim2.new(1, -32, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
Parent = expanded,
})
create("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 8),
Parent = expandedList,
})
-- The open card is the player's session, like a now-playing card: who
-- and where on top with the session clock, two wide meters for frame
-- rate and ping, then chips for the server and what's running.
local top = create("Frame", {
Size = UDim2.new(1, 0, 0, 48),
BackgroundTransparency = 1,
LayoutOrder = 1,
Parent = expandedList,
})
local face = create("ImageLabel", {
Size = UDim2.fromOffset(48, 48),
BackgroundColor3 = Color3.fromRGB(38, 38, 41),
BorderSizePixel = 0,
Image = "rbxthumb://type=AvatarHeadShot&id=" .. tostring(LocalPlayer.UserId) .. "&w=150&h=150",
Parent = top,
})
corner(face, UDim.new(0, 12))
local facePlaceholder = create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromOffset(20, 20),
BackgroundTransparency = 1,
ImageColor3 = Theme.Muted,
ImageTransparency = 1,
ScaleType = Enum.ScaleType.Fit,
Parent = face,
})
applyIcon(facePlaceholder, "user")
self:_trackAvatar(face, facePlaceholder)
local nameLabel = label({
Position = UDim2.fromOffset(60, 5),
Size = UDim2.new(1, -150, 0, 20),
TextSize = 15,
FontFace = Fonts.Bold,
TextColor3 = Color3.fromRGB(244, 244, 246),
Parent = top,
})
self:_trackName(nameLabel, "Display")
local tabLabel = label({
Position = UDim2.fromOffset(60, 26),
Size = UDim2.new(1, -150, 0, 15),
Text = "",
TextSize = 12,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
Parent = top,
})
task.spawn(function()
tabLabel.Text = detectGameName()
end)
local sessionLabel = label({
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, 0, 0, 3),
Size = UDim2.fromOffset(90, 24),
Text = "0s",
TextSize = 20,
FontFace = Fonts.Bold,
TextColor3 = Color3.fromRGB(244, 244, 246),
TextXAlignment = Enum.TextXAlignment.Right,
Parent = top,
})
label({
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, 0, 0, 27),
Size = UDim2.fromOffset(90, 14),
Text = "session",
TextSize = 11,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextXAlignment = Enum.TextXAlignment.Right,
Parent = top,
})
-- Two readouts: a large number with its unit, a caption, and a thin
-- line under them that fills and tints with how healthy it is.
local meters = create("Frame", {
Size = UDim2.new(1, 0, 0, 42),
BackgroundTransparency = 1,
LayoutOrder = 2,
Parent = expandedList,
})
local function meterPill(position, caption)
local cell = create("Frame", {
Position = position,
Size = UDim2.new(0.5, -10, 1, 0),
BackgroundTransparency = 1,
Parent = meters,
})
local value = label({
Size = UDim2.new(1, -70, 0, 24),
Text = "",
TextSize = 20,
FontFace = Fonts.Bold,
TextColor3 = Color3.fromRGB(244, 244, 246),
RichText = true,
Parent = cell,
})
label({
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, 0, 0, 6),
Size = UDim2.fromOffset(70, 14),
Text = caption,
TextSize = 10,
TextColor3 = Theme.Muted,
TextXAlignment = Enum.TextXAlignment.Right,
Parent = cell,
})
local track = create("Frame", {
AnchorPoint = Vector2.new(0, 1),
Position = UDim2.new(0, 0, 1, -4),
Size = UDim2.new(1, 0, 0, 3),
BackgroundColor3 = Color3.fromRGB(38, 38, 42),
BorderSizePixel = 0,
Parent = cell,
})
corner(track, UDim.new(1, 0))
local fill = create("Frame", {
Size = UDim2.fromScale(0, 1),
BackgroundColor3 = Theme.Success,
BorderSizePixel = 0,
Parent = track,
})
corner(fill, UDim.new(1, 0))
return fill, value
end
local fpsFill, fpsValue = meterPill(UDim2.fromScale(0, 0), "FRAME RATE")
local pingFill, pingValue = meterPill(UDim2.new(0.5, 10, 0, 0), "PING")
-- Chips: a muted name and a bold value, the running count in accent.
local chips = create("Frame", {
Size = UDim2.new(1, 0, 0, 30),
BackgroundTransparency = 1,
LayoutOrder = 3,
Parent = expandedList,
})
create("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 8),
Parent = chips,
})
local function chipOf(order, name, color)
local chipFrame = create("Frame", {
Size = UDim2.fromOffset(0, 30),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = Color3.fromRGB(32, 32, 35),
BorderSizePixel = 0,
LayoutOrder = order,
Parent = chips,
})
corner(chipFrame, UDim.new(1, 0))
padding(chipFrame, 14, 14)
create("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 7),
Parent = chipFrame,
})
label({
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
Text = name,
TextSize = 12,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextTruncate = Enum.TextTruncate.None,
LayoutOrder = 1,
Parent = chipFrame,
})
return label({
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
Text = "",
TextSize = 12,
FontFace = Fonts.Bold,
TextColor3 = color or Color3.fromRGB(244, 244, 246),
TextTruncate = Enum.TextTruncate.None,
LayoutOrder = 2,
Parent = chipFrame,
})
end
local playersValue = chipOf(1, "Players")
local bigCount = chipOf(2, "Running", Theme.Accent)
local tabValue = chipOf(3, "Tab")
-- Pinned statuses are no longer shown here; the holder stays so the
-- refresh below has somewhere to keep its rows.
local rowsHolder = create("Frame", {
Size = UDim2.new(1, 0, 0, 0),
BackgroundTransparency = 1,
Visible = false,
LayoutOrder = 4,
Parent = expandedList,
})
create("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 2),
Parent = rowsHolder,
})
-- What's running, by name, so the count above has something behind it.
local runningNames = label({
Size = UDim2.new(1, 0, 0, 15),
Text = "",
TextSize = 12,
FontFace = Fonts.Regular,
TextColor3 = Color3.fromRGB(200, 200, 206),
RichText = true,
Visible = false,
LayoutOrder = 4,
Parent = expandedList,
})
local hint = label({
Size = UDim2.new(1, 0, 0, 14),
TextSize = 11,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
LayoutOrder = 5,
Parent = expandedList,
})
-- Frame rate is counted every frame; the card's values refresh twice a
-- second while it's open.
local sessionStart = self._sessionStart or os.clock()
local frames, frameClock, fps = 0, os.clock(), 60
self:_listen("Render", function()
frames += 1
local now = os.clock()
if now - frameClock >= 0.5 then
fps = math.floor(frames / (now - frameClock) + 0.5)
frames, frameClock = 0, now
end
end)
local function refreshSession()
sessionLabel.Text = formatDuration(os.clock() - sessionStart)
idleLabel.Text = sessionLabel.Text
tween(idleDot, { BackgroundColor3 = Library._healthColor(fps, 50, 30) }, 0.4)
local unit = ' %s'
fpsValue.Text = fps .. string.format(unit, "fps")
tween(fpsFill, {
Size = UDim2.fromScale(math.clamp(fps / 60, 0.05, 1), 1),
BackgroundColor3 = Library._healthColor(fps, 50, 30),
}, 0.4, Enum.EasingStyle.Quint)
local okPing, ping = pcall(function()
return math.floor(LocalPlayer:GetNetworkPing() * 1000)
end)
ping = okPing and ping or 0
pingValue.Text = ping .. string.format(unit, "ms")
-- Lower ping fills more: 40ms or less is full, 400ms and up is low.
tween(pingFill, {
Size = UDim2.fromScale(math.clamp(1 - (ping - 40) / 360, 0.05, 1), 1),
BackgroundColor3 = Library._healthColor(ping, 90, 180, true),
}, 0.4, Enum.EasingStyle.Quint)
playersValue.Text = #Players:GetPlayers() .. "/" .. Players.MaxPlayers
local tab = self.CurrentTab
tabValue.Text = tab and tab.Name or "-"
end
task.spawn(function()
while island.Parent do
if island.Visible and self.Minimized then
refreshSession()
end
task.wait(0.5)
end
end)
-- Activity: a notification that arrives while minimized drops in here.
local activity = layer("Activity")
local activityList = create("Frame", {
Position = UDim2.fromOffset(16, 13),
Size = UDim2.new(1, -32, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
Parent = activity,
})
local activityChip = create("Frame", {
Size = UDim2.fromOffset(34, 34),
BackgroundColor3 = Theme.Accent,
BackgroundTransparency = 0.85,
BorderSizePixel = 0,
Parent = activityList,
})
corner(activityChip, UDim.new(1, 0))
local activityIcon = create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromOffset(17, 17),
BackgroundTransparency = 1,
ImageColor3 = Theme.Accent,
ScaleType = Enum.ScaleType.Fit,
Parent = activityChip,
})
local activityText = create("Frame", {
Position = UDim2.fromOffset(46, 0),
Size = UDim2.new(1, -46, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
Parent = activityList,
})
create("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 1),
Parent = activityText,
})
local activityTitle = label({
Size = UDim2.new(1, 0, 0, 18),
TextSize = 14,
FontFace = Fonts.Bold,
LayoutOrder = 1,
Parent = activityText,
})
local activityBody = label({
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
TextSize = 12,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextWrapped = true,
TextTruncate = Enum.TextTruncate.None,
TextYAlignment = Enum.TextYAlignment.Top,
MaxVisibleGraphemes = 140,
LayoutOrder = 2,
Parent = activityText,
})
local hit = create("TextButton", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
Text = "",
AutoButtonColor = false,
ZIndex = 10,
Parent = island,
})
-- Morphing between shapes: the island springs to its new size while the
-- old layer fades out and the new one fades in just behind it.
local layers = { Compact = compact, Expanded = expanded, Activity = activity }
local mode = "Compact"
local modeToken = 0
local sizeTween, cornerTween
local hovered, pressed = false, false
local hoverToken = 0
local dragFrom, dragAnchor, dragging, dragged = nil, nil, false, false
local activityToken = 0
local activityLive = false
local function shapeFor(name)
if name == "Expanded" then
return Vector2.new(Mini.Wide, math.floor(expandedList.AbsoluteSize.Y + 28 + 0.5)), Mini.Radius
elseif name == "Activity" then
return Vector2.new(Mini.Wide, math.floor(math.max(activityList.AbsoluteSize.Y, 34) + 26 + 0.5)), Mini.Radius
end
return Vector2.new(Mini.Width, Mini.Height), Mini.Height / 2
end
-- Where the island hangs: the top centre of its shape, as fractions of
-- the screen so it keeps its spot across resolutions. Dragging moves it;
-- each shape is clamped on screen, so the wide card slides in from an edge
-- and the pill goes back to its spot when it folds.
local saved = self:_readConfigSettings().IslandPosition
if type(saved) == "table" and tonumber(saved.X) and tonumber(saved.Y) then
self._islandAnchor = Vector2.new(math.clamp(tonumber(saved.X), 0, 1), math.clamp(tonumber(saved.Y), 0, 1))
end
local positionTween
local function anchorPixels()
local view = gui.AbsoluteSize
local anchor = self._islandAnchor
if not anchor then
return Vector2.new(view.X / 2, Mini.Top)
end
return Vector2.new(anchor.X * view.X, anchor.Y * view.Y)
end
local function islandSpot(size, from)
local view = gui.AbsoluteSize
local spot = from or anchorPixels()
local margin = 6
local x = math.clamp(spot.X, size.X / 2 + margin, math.max(view.X - size.X / 2 - margin, size.X / 2 + margin))
local y = math.clamp(spot.Y, margin, math.max(view.Y - size.Y - margin, margin))
return Vector2.new(x, y)
end
local function placeIsland(size, duration, style)
local spot = islandSpot(size)
if positionTween then
positionTween:Cancel()
positionTween = nil
end
if duration and duration > 0 then
positionTween = tween(island, { Position = UDim2.fromOffset(spot.X, spot.Y) }, duration, style or Enum.EasingStyle.Quint)
else
island.Position = UDim2.fromOffset(spot.X, spot.Y)
end
end
self._islandReset = function()
self._islandAnchor = nil
if island.Visible then
placeIsland(Vector2.new(island.Size.X.Offset, island.Size.Y.Offset), 0.35)
end
end
local function setMode(name, instant)
local size, radius = shapeFor(name)
local changed = name ~= mode
mode = name
modeToken += 1
local token = modeToken
local current = Vector2.new(island.Size.X.Offset, island.Size.Y.Offset)
local growing = size.X > current.X + 1 or size.Y > current.Y + 1
local duration = instant and 0 or (growing and 0.5 or 0.38)
-- A snap must stop a morph still in flight, or it keeps writing the
-- old shape over the new one.
if sizeTween then
sizeTween:Cancel()
end
if cornerTween then
cornerTween:Cancel()
end
placeIsland(size, duration)
sizeTween = tween(island, { Size = UDim2.fromOffset(size.X, size.Y) }, duration, growing and Enum.EasingStyle.Back or Enum.EasingStyle.Quint)
cornerTween = tween(islandCorner, { CornerRadius = UDim.new(0, radius) }, duration, Enum.EasingStyle.Quint)
-- The iOS island gives a soft squeeze as it swells open, then springs
-- back, so the morph reads as one elastic body rather than a resize.
if changed and not instant and not pressed and not dragging then
islandScale.Scale = growing and 0.96 or 1.03
tween(islandScale, { Scale = 1 }, growing and 0.55 or 0.4, Enum.EasingStyle.Back)
end
if changed or instant then
if mergeTween then
mergeTween:Cancel()
mergeTween = nil
end
if instant then
merge.Value = name == "Compact" and 1 or 0
elseif name == "Compact" then
-- Wait for the pill to shrink most of the way, then bud off.
task.delay(duration * 0.55, function()
if modeToken == token then
mergeTween = tween(merge, { Value = 1 }, 0.6, Enum.EasingStyle.Back)
end
end)
else
mergeTween = tween(merge, { Value = 0 }, 0.2, Enum.EasingStyle.Quint)
end
end
if not changed and not instant then
-- A refit (same shape, new height) bumps the token, which cancels
-- a fade-in still waiting from the morph; finish it here or the
-- card stays black until the next hover.
local target = layers[name]
if target.GroupTransparency > 0 then
tween(target, { GroupTransparency = 0 }, 0.22, Enum.EasingStyle.Quad)
end
if target ~= compact and target.Position ~= UDim2.fromScale(0.5, 0) then
tween(target, { Position = UDim2.fromScale(0.5, 0) }, 0.32, Enum.EasingStyle.Quint)
end
return
end
for key, group in pairs(layers) do
if key ~= name then
tween(group, { GroupTransparency = 1 }, instant and 0 or 0.14, Enum.EasingStyle.Quad)
end
end
local target = layers[name]
if instant then
target.GroupTransparency = 0
-- Compact fills the island from its corner; only the wide
-- layers hang from the centre.
if target ~= compact then
target.Position = UDim2.fromScale(0.5, 0)
end
else
-- The new layer rises a few pixels into place as it fades in, so
-- the content reads as arriving with the shape, not popping in.
if target ~= compact then
target.Position = UDim2.new(0.5, 0, 0, 6)
end
task.delay(0.1, function()
if modeToken == token then
tween(target, { GroupTransparency = 0 }, 0.22, Enum.EasingStyle.Quad)
if target ~= compact then
tween(target, { Position = UDim2.fromScale(0.5, 0) }, 0.32, Enum.EasingStyle.Quint)
end
end
end)
end
end
local function settleMode()
if hovered and not TOUCH and not dragging then
setMode("Expanded")
elseif activityLive then
setMode("Activity")
elseif self.IslandStyle == "Always Show" and not dragging then
self:_refreshMini()
refreshSession()
setMode("Expanded")
else
setMode("Compact")
end
end
self._islandSettle = function()
if self.Minimized and island.Visible and not dragging then
settleMode()
end
end
-- Called by Minimize once the window is out of the way: the island is
-- reset to its compact pill and its spot handed back for the ghost.
function self:_miniLand()
hovered, activityLive = false, false
activityToken += 1
islandScale.Scale = 1
setMode("Compact", true)
local size = Vector2.new(Mini.Width, Mini.Height)
local spot = islandSpot(size)
return Vector2.new(spot.X - size.X / 2, spot.Y), size
end
hit.MouseEnter:Connect(function()
if usingTouch() or dragging then
return
end
hovered = true
hoverToken += 1
local token = hoverToken
-- Already open (the pointer left and came back within the grace):
-- just stay. Otherwise lean in a touch, and only open once the
-- pointer rests, so sweeping past the island doesn't burst it open.
if mode == "Expanded" then
return
end
if not pressed then
tween(islandScale, { Scale = 1.04 }, 0.18, Enum.EasingStyle.Quint)
end
task.delay(0.12, function()
if hoverToken == token and hovered and self.Minimized and not dragging then
self:_refreshMini()
refreshSession()
setMode("Expanded")
end
end)
end)
hit.MouseLeave:Connect(function()
hovered = false
hoverToken += 1
local token = hoverToken
if not pressed then
tween(islandScale, { Scale = 1 }, 0.25, Enum.EasingStyle.Quint)
end
-- A grace period: the card shifts while it morphs and clamps on
-- screen, so a pointer grazing its edge shouldn't snap it shut.
task.delay(mode == "Expanded" and 0.28 or 0.1, function()
if hoverToken == token and not hovered and self.Minimized then
settleMode()
end
end)
end)
-- Press and pull to move the island anywhere on screen. It folds to the
-- pill while carried, lifts a little, and a drop near its home at the top
-- centre snaps back there. A drag never counts as the click that restores.
hit.InputBegan:Connect(function(input)
if not isPress(input) then
return
end
pressed = true
dragged = false
tween(islandScale, { Scale = 0.95 }, 0.12, Enum.EasingStyle.Quad)
if not self._islandLocked then
dragFrom = Vector2.new(input.Position.X, input.Position.Y)
dragAnchor = islandSpot(Vector2.new(Mini.Width, Mini.Height))
end
end)
self:_listen("Changed", function(input)
if not dragFrom or not isMove(input) or not self.Minimized then
return
end
local delta = Vector2.new(input.Position.X, input.Position.Y) - dragFrom
if not dragging then
if delta.Magnitude < (TOUCH and 10 or 6) then
return
end
dragging, dragged = true, true
hovered = false
setMode("Compact")
tween(islandScale, { Scale = 1.06 }, 0.2, Enum.EasingStyle.Quint)
end
local spot = islandSpot(Vector2.new(Mini.Width, Mini.Height), dragAnchor + delta)
local view = gui.AbsoluteSize
self._islandAnchor = Vector2.new(spot.X / math.max(view.X, 1), spot.Y / math.max(view.Y, 1))
if positionTween then
positionTween:Cancel()
positionTween = nil
end
island.Position = UDim2.fromOffset(spot.X, spot.Y)
end)
table.insert(self._connections, UserInputService.InputEnded:Connect(function(input)
if not isPress(input) then
return
end
dragFrom = nil
if pressed then
pressed = false
tween(islandScale, { Scale = 1 }, 0.35, Enum.EasingStyle.Back)
end
if dragging then
dragging = false
local home = Vector2.new(gui.AbsoluteSize.X / 2, Mini.Top)
local spot = islandSpot(Vector2.new(Mini.Width, Mini.Height))
if (spot - home).Magnitude < 28 then
self._islandAnchor = nil
placeIsland(Vector2.new(Mini.Width, Mini.Height), 0.35, Enum.EasingStyle.Back)
end
local anchor = self._islandAnchor
self:_writeConfigSettings({ IslandPosition = anchor and { X = anchor.X, Y = anchor.Y } or false })
hovered = not usingTouch() and pointInside(pointerPosition(), island)
task.delay(0.1, function()
if self.Minimized and not dragging then
settleMode()
end
end)
end
end))
-- Keeps the island on screen when the viewport changes size.
table.insert(self._connections, gui:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
if island.Visible and not dragging then
placeIsland(Vector2.new(island.Size.X.Offset, island.Size.Y.Offset), 0)
end
end))
hit.MouseButton1Click:Connect(function()
if dragged then
dragged = false
return
end
if self.Minimized then
self:Restore()
end
end)
-- Restore hides the island under the pointer, so MouseLeave never fires.
self._miniUnhover = function()
hovered, pressed, activityLive = false, false, false
dragFrom, dragging = nil, false
activityToken += 1
islandScale.Scale = 1
-- Clicked open while expanded: once the island has faded out, fold it
-- back to the pill so the next minimize starts from it.
task.delay(0.15, function()
if not self.Minimized then
setMode("Compact", true)
end
end)
end
-- A notification while minimized: the island opens to show it for a few
-- seconds, then folds back.
self._islandActivity = function(opts)
if not self.Minimized or not island.Visible then
return
end
local color = NOTIFY_COLORS[opts.Type] or Theme.Accent
activityTitle.Text = tostring(opts.Title or "Notification")
activityTitle.TextColor3 = NOTIFY_COLORS[opts.Type] or Theme.Text
activityBody.Text = tostring(opts.Content or "")
activityBody.Visible = activityBody.Text ~= ""
local icon = opts.Icon
if icon == nil then
icon = ({ Success = "circle-check", Warning = "triangle-alert", Error = "circle-x" })[opts.Type] or "bell"
end
applyIcon(activityIcon, icon)
if not activityIcon:GetAttribute("CustomIcon") then
activityIcon.ImageColor3 = color
end
activityChip.BackgroundColor3 = color
activityLive = true
activityToken += 1
local token = activityToken
task.defer(function()
if activityToken == token and not hovered and not dragging then
setMode("Activity")
islandScale.Scale = 0.96
tween(islandScale, { Scale = 1 }, 0.4, Enum.EasingStyle.Back)
end
end)
task.delay(math.clamp(tonumber(opts.Duration) or 3, 1.5, 5), function()
if activityToken == token then
activityLive = false
if self.Minimized and not hovered then
settleMode()
end
end
end)
end
-- The meter dances while anything runs and lies flat when nothing does.
local running = 0
self:_listen("Render", function()
if island.Visible and running > 0 and mode == "Compact" then
local now = os.clock()
for index, bar in ipairs(bars) do
local wave = 0.5 + 0.5 * math.sin(now * (6 + index * 1.4) + index * 1.9)
bar.Size = UDim2.fromOffset(3, math.floor(4 + wave * 11 + 0.5))
end
end
end)
-- Split, like iOS with two Live Activities: when toggles are running and
-- a status is pinned, the pill keeps the status (logo leading, value
-- trailing) and the running meter buds off into its own little circle
-- beside it. One activity alone stays in the single pill.
local pinValue = label({
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, -14, 0.5, 0),
Size = UDim2.new(1, -52, 0, 18),
TextSize = 13,
FontFace = Fonts.Bold,
TextXAlignment = Enum.TextXAlignment.Right,
RichText = true,
TextTransparency = 1,
Parent = compact,
})
local bubble = create("TextButton", {
Name = "IslandBubble",
AnchorPoint = Vector2.new(0.5, 0.5),
Size = UDim2.fromOffset(0, 0),
BackgroundColor3 = Color3.fromRGB(26, 26, 30),
BorderSizePixel = 0,
Text = "",
AutoButtonColor = false,
Visible = false,
ZIndex = 30,
Parent = gui,
})
corner(bubble, UDim.new(1, 0))
local split = create("NumberValue", { Value = 0 })
local splitOn = false
local pinnedLead = nil
local function setSplit(on)
if on == splitOn then
return
end
splitOn = on
barsHolder.Parent = on and bubble or meter
barsHolder.AnchorPoint = on and Vector2.new(0.5, 0.5) or Vector2.new(0, 0)
barsHolder.Position = on and UDim2.fromScale(0.5, 0.5) or UDim2.new()
meter.Visible = not on
tween(pinValue, { TextTransparency = on and 0 or 1 }, 0.2, Enum.EasingStyle.Quad)
-- Buds off with a spring, and is drawn back in quicker.
tween(split, { Value = on and 1 or 0 }, on and 0.55 or 0.3, on and Enum.EasingStyle.Back or Enum.EasingStyle.Quint)
if on then
islandScale.Scale = 0.96
tween(islandScale, { Scale = 1 }, 0.45, Enum.EasingStyle.Back)
end
end
self:_listen("Render", function()
local t = split.Value * merge.Value
local shown = island.Visible and t > 0.02 and island.GroupTransparency < 0.5
bubble.Visible = shown
if not shown then
return
end
local origin = gui.AbsolutePosition
local pos, size = island.AbsolutePosition - origin, island.AbsoluteSize
local d = math.min(size.Y, Mini.Height)
local gap = 6
-- Buds off the side with room to spare: the right, or the left near
-- the right edge of the screen.
local right = pos.X + size.X + gap + d <= gui.AbsoluteSize.X
local edge = right and pos.X + size.X or pos.X
local dir = right and 1 or -1
local inner = edge - dir * d * 0.6
local outer = edge + dir * (gap + d / 2)
local x = inner + (outer - inner) * t
local scale = math.clamp(0.35 + 0.65 * t, 0, 1.2)
-- Liquid: mid-flight the drop stretches along its path and thins,
-- then rounds back into a circle as it settles.
local stretch = math.sin(math.clamp(t, 0, 1) * math.pi)
bubble.Position = UDim2.fromOffset(x, pos.Y + d / 2)
bubble.Size = UDim2.fromOffset(d * scale * (1 + 0.4 * stretch), d * scale * (1 - 0.18 * stretch))
bubble.BackgroundTransparency = island.GroupTransparency
end)
bubble.MouseButton1Click:Connect(function()
if self.Minimized then
self:Restore()
end
end)
local cardRows = {}
function self:_refreshMini()
local count = 0
for _, element in pairs(Library.Flags) do
if element._type == "Toggle" and element.Value == true then
count += 1
end
end
if count ~= running then
local rising = count > running
running = count
countLabel.Text = tostring(count)
for _, bar in ipairs(bars) do
tween(bar, { BackgroundColor3 = count > 0 and Theme.Accent or Theme.Muted }, 0.25)
if count == 0 then
tween(bar, { Size = UDim2.fromOffset(3, 3) }, 0.25)
end
end
-- A small bump, like a live activity updating.
if rising and mode == "Compact" and island.Visible then
islandScale.Scale = 1.06
tween(islandScale, { Scale = 1 }, 0.4, Enum.EasingStyle.Back)
end
end
bigCount.Text = tostring(count)
local names = {}
for _, element in pairs(Library.Flags) do
if element._type == "Toggle" and element.Value == true and element._searchName ~= "" then
table.insert(names, element._searchName)
end
end
table.sort(names)
local shownNames = {}
for index = 1, math.min(#names, 3) do
shownNames[index] = Library._rich(names[index])
end
local extra = #names - #shownNames
runningNames.Text = 'Running ' .. table.concat(shownNames, ' · ')
.. (extra > 0 and string.format(' +%d', extra) or "")
runningNames.Visible = #names > 0
pinnedLead = nil
for _, handle in ipairs(self._pinned) do
if not handle._isShown or handle:_isShown() then
pinnedLead = handle
break
end
end
if pinnedLead then
local text = tostring(pinnedLead:Text() or "")
-- A status with nothing to show yet doesn't take the pill.
if text:gsub("<[^>]*>", ""):match("%S") then
pinValue.Text = text
local color = pinnedLead._shownColor and pinnedLead._shownColor() or Theme.Text
-- Too dark to read on the black pill: fall back to text colour.
local _, _, value = color:ToHSV()
pinValue.TextColor3 = value < 0.35 and Theme.Text or color
else
pinnedLead = nil
end
end
setSplit(running > 0 and pinnedLead ~= nil)
-- The pill's own meter: the clock when idle or when the bars budded
-- off without a status to keep, the bars and count otherwise.
local barsHere = running > 0 and not splitOn
idleDot.Visible = not barsHere
idleLabel.Visible = not barsHere
countLabel.Visible = barsHere
barsHolder.Visible = running > 0
meter.Visible = not splitOn
miniTitle.Visible = not splitOn
local key = typeof(self.Keybind) == "EnumItem" and keyName(self.Keybind)
hint.Text = TOUCH and "Tap to open" or (key and ("Click or press " .. key .. " to open") or "Click to open")
local list = {}
for _, handle in ipairs(self._pinned) do
if (not handle._isShown or handle:_isShown()) and #list < Mini.Pinned then
table.insert(list, handle)
end
end
rowsHolder.Visible = false
for index = 1, math.max(#list, #cardRows) do
local handle = list[index]
local row = cardRows[index]
if handle then
if not row then
local frame = create("Frame", {
Size = UDim2.new(1, 0, 0, 18),
BackgroundTransparency = 1,
LayoutOrder = index,
Parent = rowsHolder,
})
row = {
Frame = frame,
Key = label({
Size = UDim2.new(0.5, -4, 1, 0),
TextSize = 12,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
RichText = true,
Parent = frame,
}),
Value = label({
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.fromScale(1, 0),
Size = UDim2.new(0.5, -4, 1, 0),
TextSize = 12,
TextXAlignment = Enum.TextXAlignment.Right,
RichText = true,
Parent = frame,
}),
}
cardRows[index] = row
end
row.Frame.Visible = true
row.Key.Text = Library._rich(handle.Name)
row.Value.Text = handle:Text()
-- Carries the status's own colour: its Color, or its tone
-- for the tinted styles (badge, dot, bar).
local color = handle._shownColor and handle._shownColor() or Theme.Text
if row.Value.TextColor3 ~= color then
tween(row.Value, { TextColor3 = color }, 0.2)
end
elseif row then
row.Frame.Visible = false
end
end
-- Rows coming and going change the expanded height.
if mode == "Expanded" and (hovered or self.IslandStyle == "Always Show") then
task.defer(function()
if mode == "Expanded" and (hovered or self.IslandStyle == "Always Show") and self.Minimized then
local size = shapeFor("Expanded")
if math.abs(size.Y - island.Size.Y.Offset) > 1 then
setMode("Expanded")
end
end
end)
end
end
self.MiniBar = island
self.Island = island
return island
end
function Window:Minimize()
if self.Minimized or not self._introDone or not self.Open or self._destroyed then
return
end
self.Minimized = true
self._minimizeGeneration = (self._minimizeGeneration or 0) + 1
local generation = self._minimizeGeneration
self:_closePopups()
self:_refreshToggleButton()
self:_refreshBackdrop()
local bar = self:_buildMiniBar()
self:_refreshMini()
local gui = self.Gui
-- Starts from whatever is on screen: the window, or a ghost still
-- mid-flight if a restore was interrupted.
local fromPosition = self.Root.AbsolutePosition - gui.AbsolutePosition
local fromSize = self.Root.AbsoluteSize
local fromAlpha = 1
if self._ghost then
fromPosition = self._ghost.AbsolutePosition - gui.AbsolutePosition
fromSize = self._ghost.AbsoluteSize
fromAlpha = self._ghost.BackgroundTransparency
end
-- 1. The ghost fades in over the window while it eases back a touch.
local ghost, ghostStroke, ghostCorner = makeGhost(self, fromPosition, fromSize, Library._radius(10))
ghost.BackgroundTransparency = fromAlpha
ghostStroke.Transparency = fromAlpha
tween(ghost, { BackgroundTransparency = 0 }, 0.11, Enum.EasingStyle.Quad)
tween(ghostStroke, { Transparency = 0 }, 0.11, Enum.EasingStyle.Quad)
tween(self.Scale, { Scale = (self._fitScale or 1) * 0.985 }, 0.11, Enum.EasingStyle.Quad)
tween(self.Shadow, { ImageTransparency = 1 }, 0.2)
bar.Visible = true
bar.GroupTransparency = 1
task.delay(0.11, function()
if self._minimizeGeneration ~= generation or self._destroyed then
return
end
-- 2. Covered by the ghost, the window is hidden and the ghost folds
-- down into the island.
self.Root.Visible = false
self.Scale.Scale = self._fitScale or 1
local target, size = self:_miniLand(fromPosition + fromSize / 2)
morphGhost(ghost, ghostCorner, target, size, math.floor(size.Y / 2), 0.36)
task.delay(0.3, function()
if self._minimizeGeneration ~= generation or self._destroyed then
return
end
-- 3. The island fades in on top as the ghost lands.
self._miniScale.Scale = 0.9
tween(self._miniScale, { Scale = 1 }, 0.3, Enum.EasingStyle.Quint)
tween(bar, { GroupTransparency = 0 }, 0.18, Enum.EasingStyle.Quad)
dissolve(self, ghost, ghostStroke, 0.2)
-- Always Show: once landed as the pill, swell open into the card.
if self.IslandStyle == "Always Show" then
task.delay(0.22, function()
if self._minimizeGeneration == generation and self._islandSettle then
self._islandSettle()
end
end)
end
end)
end)
task.spawn(function()
while self.Minimized and self._minimizeGeneration == generation and not self._destroyed do
self:_refreshMini()
task.wait(0.5)
end
end)
end
function Window:Restore()
if not self.Minimized or self._destroyed then
return
end
self.Minimized = false
self._minimizeGeneration = (self._minimizeGeneration or 0) + 1
local generation = self._minimizeGeneration
self:_refreshToggleButton()
self:_refreshBackdrop(true)
local gui = self.Gui
local bar = self.MiniBar
local fit = self._fitScale or 1
local size = Vector2.new(self.Root.Size.X.Offset, self.Root.Size.Y.Offset) * fit
local centre = self.Root.AbsolutePosition + self.Root.AbsoluteSize / 2 - gui.AbsolutePosition
local target = centre - size / 2
-- Picks up from whatever is on screen: a ghost still mid-flight if
-- minimize was interrupted, otherwise the island.
local fromPosition, fromSize = target, size
local fromAlpha = 1
if self._ghost then
fromPosition = self._ghost.AbsolutePosition - gui.AbsolutePosition
fromSize = self._ghost.AbsoluteSize
fromAlpha = self._ghost.BackgroundTransparency
elseif bar and bar.Visible then
fromPosition = self._miniOrb.AbsolutePosition - gui.AbsolutePosition
fromSize = self._miniOrb.AbsoluteSize
end
if bar and bar.Visible then
if self._miniUnhover then
self._miniUnhover()
end
tween(bar, { GroupTransparency = 1 }, 0.12, Enum.EasingStyle.Quad)
task.delay(0.12, function()
if self._minimizeGeneration == generation then
bar.Visible = false
end
end)
end
local ghost, ghostStroke, ghostCorner = makeGhost(self, fromPosition, fromSize, math.floor(math.min(fromSize.X, fromSize.Y) / 2))
ghost.BackgroundTransparency = fromAlpha
ghostStroke.Transparency = fromAlpha
tween(ghost, { BackgroundTransparency = 0 }, 0.1, Enum.EasingStyle.Quad)
tween(ghostStroke, { Transparency = 0 }, 0.1, Enum.EasingStyle.Quad)
morphGhost(ghost, ghostCorner, target, size, Library._radius(10), 0.4)
tween(self.Shadow, { ImageTransparency = self._shadowRest }, 0.45)
-- Shown under the ghost a little early so its first frames are drawn
-- while still covered, then the ghost clears to reveal it.
task.delay(0.3, function()
if self._minimizeGeneration ~= generation or self._destroyed then
return
end
self.Scale.Scale = fit
self.BodyStroke.Transparency = 0
self.Root.Visible = true
end)
task.delay(0.4, function()
if self._ghost == ghost and self._minimizeGeneration == generation then
dissolve(self, ghost, ghostStroke, 0.22)
end
end)
end
-- The minimized island can be dragged anywhere; its spot is remembered in
-- the config folder. These lock it in place or send it back to the top.
function Window:SetIslandDraggable(enabled)
self._islandLocked = enabled == false
end
function Window:ResetIslandPosition()
self._islandAnchor = nil
if self._islandReset then
self._islandReset()
end
self:_writeConfigSettings({ IslandPosition = false })
end
function Window:SetMinimized(minimized)
if minimized then
self:Minimize()
else
self:Restore()
end
end
function Window:Toggle(open)
if not self._introDone then
return
end
if self.Minimized then
if open ~= false then
self:Restore()
end
return
end
if open == nil then
open = not self.Open
end
if open == self.Open then
return
end
self.Open = open
self:_refreshToggleButton()
self:_refreshBackdrop(open)
-- Like minimize, a plain ghost panel carries the scale and fade. The body
-- is never reparented into the fader or rescaled, which relaid out the
-- whole window and stuttered on the keypress frame.
self._minimizeGeneration = (self._minimizeGeneration or 0) + 1
local generation = self._minimizeGeneration
local gui = self.Gui
local fit = self._fitScale or 1
local size = Vector2.new(self.Root.Size.X.Offset, self.Root.Size.Y.Offset) * fit
local centre = self.Root.AbsolutePosition + self.Root.AbsoluteSize / 2 - gui.AbsolutePosition
local shrunk = size * 0.94
local fromPosition, fromSize, fromAlpha
if self._ghost then
fromPosition = self._ghost.AbsolutePosition - gui.AbsolutePosition
fromSize = self._ghost.AbsoluteSize
fromAlpha = self._ghost.BackgroundTransparency
end
if open then
local ghost, ghostStroke, ghostCorner = makeGhost(self, fromPosition or centre - shrunk / 2, fromSize or shrunk, Library._radius(10))
ghost.BackgroundTransparency = fromAlpha or 1
ghostStroke.Transparency = fromAlpha or 1
tween(ghost, { BackgroundTransparency = 0 }, 0.12, Enum.EasingStyle.Quad)
tween(ghostStroke, { Transparency = 0 }, 0.12, Enum.EasingStyle.Quad)
morphGhost(ghost, ghostCorner, centre - size / 2, size, Library._radius(10), 0.3)
tween(self.Shadow, { ImageTransparency = self._shadowRest }, 0.3)
-- Shown under the ghost once it covers the spot, then revealed.
task.delay(0.2, function()
if self._minimizeGeneration ~= generation or self._destroyed then
return
end
self.Scale.Scale = fit
self.BodyStroke.Transparency = 0
self.Root.Visible = true
end)
task.delay(0.3, function()
if self._ghost == ghost and self._minimizeGeneration == generation then
dissolve(self, ghost, ghostStroke, 0.18)
end
end)
else
self:_closePopups()
local position = fromPosition or self.Root.AbsolutePosition - gui.AbsolutePosition
local current = fromSize or self.Root.AbsoluteSize
local ghost, ghostStroke, ghostCorner = makeGhost(self, position, current, Library._radius(10))
ghost.BackgroundTransparency = fromAlpha or 1
ghostStroke.Transparency = fromAlpha or 1
tween(ghost, { BackgroundTransparency = 0 }, 0.08, Enum.EasingStyle.Quad)
tween(ghostStroke, { Transparency = 0 }, 0.08, Enum.EasingStyle.Quad)
tween(self.Shadow, { ImageTransparency = 1 }, 0.16)
task.delay(0.08, function()
if self._minimizeGeneration ~= generation or self._destroyed then
return
end
self.Root.Visible = false
morphGhost(ghost, ghostCorner, centre - shrunk / 2, shrunk, Library._radius(10), 0.2)
dissolve(self, ghost, ghostStroke, 0.16)
end)
end
end
function Window:SetKeepOnScreen(enabled)
self.KeepOnScreen = enabled ~= false
if self.KeepOnScreen then
self:_clampToScreen()
end
end
function Window:SetKeybind(keyCode)
self.Keybind = keyCode
if self._keyChipLabel then
self._keyChipLabel.Text = keyName(keyCode)
end
end
function Window:Notify(opts)
opts = normalize(opts, { Text = "Content", Message = "Content", Image = "Icon" })
if self.Minimized and self._islandActivity then
self._islandActivity(opts)
end
local duration = opts.Duration or 4
local titleColor = NOTIFY_COLORS[opts.Type] or Theme.Text
self._notifyOrder = self._notifyOrder + 1
local outer = create("Frame", {
Size = UDim2.new(1, 0, 0, 0),
BackgroundTransparency = 1,
LayoutOrder = self._notifyOrder,
Parent = self.NotifyHolder,
})
local slide = create("Frame", {
Position = UDim2.fromOffset(320, 0),
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
Parent = outer,
})
local shadow = create("ImageLabel", {
Position = UDim2.fromOffset(-20, -20),
Size = UDim2.new(1, 40, 1, 40),
BackgroundTransparency = 1,
Image = Assets.Shadow,
ImageColor3 = Color3.new(0, 0, 0),
ImageTransparency = 1,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(49, 49, 450, 450),
ZIndex = 0,
Parent = slide,
})
local toast = create("CanvasGroup", {
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundColor3 = Theme.Background,
BorderSizePixel = 0,
GroupTransparency = 1,
Parent = slide,
})
corner(toast, UDim.new(0, 10))
local toastStroke = stroke(toast, Theme.Stroke)
edgeHighlight(toast)
glow(toast, UDim2.fromOffset(260, 120), UDim2.new(1, -10, 0, -10), 0.86, 90)
local inner = create("Frame", {
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
Parent = toast,
})
padding(inner, 16, 16, 14, 24)
local titleOffset = 0
if opts.Icon then
glowIcon(inner, opts.Icon, titleColor == Theme.Text and Theme.Accent or titleColor, UDim2.new(0, 0, 0, 8))
titleOffset = 24
end
label({
Position = UDim2.fromOffset(titleOffset, 0),
Size = UDim2.new(1, -28 - titleOffset, 0, 16),
Text = opts.Title or "Notification",
TextSize = 14,
TextColor3 = titleColor,
Parent = inner,
})
local closeButton = create("TextButton", {
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, 6, 0, -5),
Size = UDim2.fromOffset(24, 24),
BackgroundTransparency = 1,
Text = "×",
TextColor3 = Theme.Muted,
TextSize = 22,
FontFace = Fonts.Bold,
AutoButtonColor = false,
Parent = inner,
})
closeButton.MouseEnter:Connect(function()
tween(closeButton, { TextColor3 = Theme.Text }, 0.15)
end)
closeButton.MouseLeave:Connect(function()
tween(closeButton, { TextColor3 = Theme.Muted }, 0.2)
end)
if opts.Content then
label({
Position = UDim2.fromOffset(titleOffset, 21),
Size = UDim2.new(1, -titleOffset, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
Text = opts.Content,
TextSize = 13,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextWrapped = true,
TextTruncate = Enum.TextTruncate.None,
TextYAlignment = Enum.TextYAlignment.Top,
Parent = inner,
})
end
local track = create("Frame", {
AnchorPoint = Vector2.new(0, 1),
Position = UDim2.new(0, 16, 1, -8),
Size = UDim2.new(1, -32, 0, 3),
BackgroundColor3 = Theme.Surface3,
BorderSizePixel = 0,
Parent = toast,
})
corner(track, UDim.new(1, 0))
local progress = create("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundColor3 = Theme.Accent,
BorderSizePixel = 0,
Parent = track,
})
corner(progress, UDim.new(1, 0))
task.defer(function()
if outer.Parent then
tween(outer, { Size = UDim2.new(1, 0, 0, toast.AbsoluteSize.Y) }, 0.3, Enum.EasingStyle.Quint)
end
end)
tween(slide, { Position = UDim2.fromOffset(0, 0) }, 0.5, Enum.EasingStyle.Back)
tween(toast, { GroupTransparency = 0 }, 0.3)
tween(shadow, { ImageTransparency = 0.6 }, 0.4)
tween(progress, { Size = UDim2.fromScale(0, 1) }, duration, Enum.EasingStyle.Linear)
local dismissed = false
local function dismiss()
if dismissed then
return
end
dismissed = true
for index, entry in ipairs(self._toasts) do
if entry == dismiss then
table.remove(self._toasts, index)
break
end
end
tween(slide, { Position = UDim2.fromOffset(320, 0) }, 0.3, Enum.EasingStyle.Quint)
tween(toast, { GroupTransparency = 1 }, 0.2)
tween(toastStroke, { Transparency = 1 }, 0.15)
tween(shadow, { ImageTransparency = 1 }, 0.2)
task.delay(0.22, function()
outer.ClipsDescendants = true
tween(outer, { Size = UDim2.new(1, 0, 0, -4) }, 0.22, Enum.EasingStyle.Quint)
task.delay(0.24, function()
outer:Destroy()
end)
end)
end
task.delay(duration, dismiss)
closeButton.MouseButton1Click:Connect(dismiss)
table.insert(self._toasts, dismiss)
while #self._toasts > self.MaxNotifications do
local oldest = table.remove(self._toasts, 1)
oldest()
end
return { Dismiss = dismiss }
end
function Window:Destroy()
if self._destroyed then
return
end
self._destroyed = true
for index, entry in ipairs(Library.Windows) do
if entry == self then
table.remove(Library.Windows, index)
break
end
end
for _, connection in ipairs(self._connections) do
pcall(function()
connection:Disconnect()
end)
end
self._connections = {}
self._inputListeners = { Began = {}, Changed = {}, Ended = {}, Render = {} }
self._frameSteps = {}
local gui = self.Gui
local removed = false
local function remove()
if removed then
return
end
removed = true
if not pcall(gui.Destroy, gui) then
pcall(function()
gui.Enabled = false
end)
end
end
-- The fade out is decoration only. Input is already cut at this point, so
-- if any of it fails the gui is removed straight away instead of being
-- left frozen on screen.
local ok, err = pcall(function()
if self._dialog then
self._dialog.Close()
end
self:_closePopups()
local fit = self._fitScale or 1
tween(self.Scale, { Scale = fit * 0.92 }, 0.22, Enum.EasingStyle.Quint)
self:_fade(1, 0.2)
tween(self.BodyStroke, { Transparency = 1 }, 0.12)
tween(self.Shadow, { ImageTransparency = 1 }, 0.2)
if self.MiniBar and self.MiniBar.Visible then
tween(self.MiniBar, { GroupTransparency = 1 }, 0.18)
end
if self._backdrop then
-- Frame steps are already cut, so the weather stops where it is.
self._weatherField.Visible = false
tween(self._backdropTintFrame, { BackgroundTransparency = 1 }, 0.2)
tween(self._backdropHeat, { BackgroundTransparency = 1 }, 0.2)
end
for _, button in ipairs({ self.ToggleButton, self.OpenButton }) do
if typeof(button) == "Instance" and button:IsA("GuiObject") then
tween(button, { Size = UDim2.fromOffset(0, 0) }, 0.2, Enum.EasingStyle.Quint)
end
end
end)
if not ok then
warn("[AirFlow] unload animation failed, removing the window directly: " .. tostring(err))
remove()
return
end
task.delay(0.24, remove)
end
Window.Unload = Window.Destroy
-- Unloads every window the library created.
function Library:Destroy()
for index = #Library.Windows, 1, -1 do
local window = Library.Windows[index]
if window then
window:Destroy()
end
end
end
Library.Unload = Library.Destroy
-- A script thread that yielded inside an engine call (InvokeServer and the
-- like) resumes at game identity and can no longer touch the GUI in CoreGui.
-- Every public call checks for that: it raises the thread back to the identity
-- the library loaded at, or, when the executor cannot, runs the call on a
-- trusted Heartbeat thread and waits for its result.
local getIdentity = getthreadidentity or getidentity or get_thread_identity or (syn and syn.get_thread_identity)
local setIdentity = setthreadidentity or setidentity or set_thread_identity or (syn and syn.set_thread_identity)
local loadIdentity = nil
if getIdentity then
local ok, identity = pcall(getIdentity)
if ok and type(identity) == "number" then
loadIdentity = identity
end
end
local function threadLowered()
local window = Library.Windows[#Library.Windows]
local probe = window and window.Gui
if typeof(probe) ~= "Instance" then
return false
end
return not pcall(function()
return probe.Name
end)
end
local trustedQueue = {}
RunService.Heartbeat:Connect(function()
if #trustedQueue == 0 then
return
end
local queue = trustedQueue
trustedQueue = {}
for _, job in ipairs(queue) do
job()
end
end)
local function runTrusted(fn, ...)
local args = table.pack(...)
local result = nil
table.insert(trustedQueue, function()
result = table.pack(pcall(fn, table.unpack(args, 1, args.n)))
end)
while result == nil do
task.wait()
end
if not result[1] then
error(result[2], 0)
end
return table.unpack(result, 2, result.n)
end
local guardedObjects = setmetatable({}, { __mode = "k" })
local guardObject
local function guarded(fn)
return function(...)
if threadLowered() then
if setIdentity then
pcall(setIdentity, loadIdentity or 8)
end
if threadLowered() then
return guardObject(runTrusted(fn, ...))
end
end
return guardObject(fn(...))
end
end
-- Elements carry their methods as closures, so each returned object gets its
-- public functions wrapped too; windows and tabs share the guarded metatables.
function guardObject(...)
for index = 1, select("#", ...) do
local object = select(index, ...)
if type(object) == "table" and not guardedObjects[object] then
local meta = getmetatable(object)
if meta ~= Window and meta ~= Tab then
guardedObjects[object] = true
for key, value in pairs(object) do
if type(key) == "string" and type(value) == "function" and key:sub(1, 1) ~= "_" then
object[key] = guarded(value)
elseif type(key) == "number" and type(value) == "table" then
guardObject(value)
end
end
end
end
end
return ...
end
for _, class in ipairs({ Library, Window, Tab }) do
for key, value in pairs(class) do
if type(key) == "string" and type(value) == "function" and key:sub(1, 1) ~= "_" then
class[key] = guarded(value)
end
end
end
return Library
end
local Library, T
local xDTaraZ = setmetatable({}, {
__newindex = function(self, key, value)
rawset(self, key, type(value) == "function" and LPH_JIT(value) or value)
end,
})
xDTaraZ.Config = {
Discord = "https://discord.gg/RzkdNVVku8",
UpdateLog = {
{ "2026-10-10", "Silent Aim and Ragebot hit again after the game update\nFixed the game crashing when you shoot\nFixed ESP Team Check and health\nAim and ESP now include bots\nSkin pictures, turning skins off restores the stock look\nAuto Rebuy works from round 1, No Flash/No Smoke/FOV restore when off" },
{ "2026-10-03", "XYZ UI is back\nBetter executor support\nImproved Combat & Movement" },
},
SaveFolder = "XYZ",
StatusInterval = 1,
AimRenderPriority = Enum.RenderPriority.Camera.Value + 1,
RefireGap = 0.12,
TriggerRay = 2000,
SilentFocusTime = 1,
SeeThrough = { "Characters", "Debris", "ThirdPersonWeaponStash" },
EspCacheMax = 256,
SoundSelfRange = 6,
GhostSnap = 45,
LeadExtra = 0.02,
LeadMaxSpeed = 60,
SpreadMethods = { "getTrueSpread", "getSpreadForConfig", "getMovementSpreadForConfig", "getBaseSpreadForConfig", "getBaseSpread" },
SoundMaxMarks = 24,
SoundColor = Color3.fromRGB(255, 196, 64),
RedeemGap = 1.1,
RebuyGap = 0.25,
AlertTries = 20,
AlertGap = 0.5,
RequireTimeout = 5,
MaxFails = 5,
FailWindow = 10,
CombatToggles = { "Aimbot", "Triggerbot", "Ragebot", "AimbotShowFov", "SilentShowFov" },
ModuleFeatures = {
Net = { "AutoRebuy" },
Character = { "BunnyHop" },
Skins = { "SkinChanger" },
},
Codes = {
"1MGROUPMEMBERS", "MYFAULTYALL", "RIANOMINATED2026", "LORE", "DUST_II", "MICHAELSRETURN",
"FREEDOM", "HAPPYBDAYYUUTO", "GAMEBROKE318", "OHNEPIXEL", "NEBULA", "REACTORDELAY", "BUTTERFLYCASE", "TRADEUPS",
},
BoneParts = {
Head = { "Head" },
Torso = { "UpperTorso", "Torso", "HumanoidRootPart" },
},
BuyStates = { ["Buy Period"] = true, ["Warmup"] = true },
StickySlack = 1.5,
HopGap = 0.05,
HookSettle = 1,
SkinFile = "BloxStrike/skins.json",
SkinWears = { "Factory New", "Minimal Wear", "Field-Tested", "Well-Worn", "Battle-Scarred" },
SkinKinds = { Melee = "Knives", Glove = "Gloves", Grenade = "Grenades", C4 = "Gear", ["Zeus x27"] = "Gear" },
SkinOrder = { "Pistol", "SMG", "Rifle", "Sniper", "Heavy", "Shotgun", "Machine Gun", "Knives", "Gloves", "Grenades", "Gear" },
SkinRanks = { Forbidden = 9, Special = 8, Red = 7, Pink = 6, Purple = 5, Blue = 4, LightBlue = 3, Gray = 2, White = 1 },
SkinMainRows = 5,
SkinArms = { ["Left Arm"] = true, ["Right Arm"] = true },
GloveKey = "@Glove",
FovColors = {
Aimbot = Color3.fromRGB(232, 160, 76),
Silent = Color3.fromRGB(110, 170, 255),
},
EspFocusColor = Color3.fromRGB(255, 214, 64),
FovLayer = 50,
}
xDTaraZ.State = {
Alive = true,
Connections = {},
Halted = {},
Notices = {},
AimTarget = nil,
AimPart = nil,
RageTarget = nil,
RagePart = nil,
SilentTarget = nil,
Firing = false,
PressedAt = 0,
LastShot = 0,
Shots = 0,
Redirected = 0,
Bought = {},
LightSaved = nil,
FovSaved = nil,
ViewSaved = nil,
Hooks = {},
TriggerWarned = false,
HopAt = 0,
HopCrouched = false,
SpaceHeld = false,
FovBound = false,
FovBase = 0,
}
xDTaraZ.Options = {
Aimbot = false,
AimbotSmooth = 1,
AimbotBone = "Head",
AimbotPriority = "Crosshair",
AimbotFov = 150,
AimbotShowFov = false,
AimbotVisible = true,
AimbotSticky = true,
AimbotMaxDistance = 1500,
SilentAim = false,
SilentHitChance = 100,
SilentHeadChance = 100,
SilentPriority = "Crosshair",
SilentFov = 220,
SilentShowFov = false,
SilentVisible = true,
SilentMaxDistance = 2000,
SilentPredict = false,
Triggerbot = false,
TriggerDelay = 0,
TriggerHitChance = 100,
Ragebot = false,
RageVisible = true,
RageMaxDistance = 2000,
NoRecoil = false,
NoSpread = false,
Fullbright = false,
NoFlash = false,
NoSmoke = false,
SoundEsp = false,
SoundEspLife = 3,
EspMemory = 5,
CameraFov = false,
CameraFovValue = 100,
InfiniteJump = false,
BunnyHop = false,
BunnyCrouch = true,
SkinChanger = false,
AutoRebuy = false,
AntiAfk = false,
}
---Executor capability probes. The old UI library shipped these; AirFlow does not, so the script owns them now.
---Every probe runs the API on a throwaway fixture, because some executors ship stubs that pass type() but do nothing.
xDTaraZ.Compat = { Probes = {} }
xDTaraZ.Compat.Api = {
HookFunction = hookfunction or replaceclosure,
RestoreFunction = restorefunction,
IsFunctionHooked = isfunctionhooked,
GetGc = getgc,
GetUpvalue = getupvalue or (debug and debug.getupvalue),
}
---Probes run inline (this __index cannot yield), so a probe that yields counts as unsupported.
xDTaraZ.Compat.Caps = setmetatable({}, {
__index = function(caps, name)
local probe = xDTaraZ.Compat.Probes[name]
if not probe then return false end
local thread = coroutine.create(probe)
local ok, works = coroutine.resume(thread)
local has = ok and coroutine.status(thread) == "dead" and works == true
if coroutine.status(thread) == "suspended" then pcall(coroutine.close, thread) end
rawset(caps, name, has)
return has
end,
})
function xDTaraZ.Compat.Probes.HookFunction()
local hook = xDTaraZ.Compat.Api.HookFunction
if not hook then return false end
local function Target() return "plain" end
local original = hook(Target, function() return "hooked" end)
local works = Target() == "hooked" and type(original) == "function" and original() == "plain"
if type(original) == "function" then pcall(hook, Target, original) end
return works
end
function xDTaraZ.Compat.Probes.Gc()
local getGc = xDTaraZ.Compat.Api.GetGc
if not getGc then return false end
local function Marker() return "gc" end
for _, value in ipairs(getGc(false)) do
if value == Marker then return true end
end
return false
end
function xDTaraZ.Compat.Probes.Upvalues()
local getUpvalue = xDTaraZ.Compat.Api.GetUpvalue
if not getUpvalue then return false end
local marker = {}
local function Holder() return marker end
return getUpvalue(Holder, 1) == marker
end
---The FOV circles set these every frame; a shim that only knows Line would throw.
function xDTaraZ.Compat.Probes.Drawing()
if type(Drawing) ~= "table" and type(Drawing) ~= "userdata" then return false end
local okNew, circle = pcall(Drawing.new, "Circle")
if not okNew or circle == nil then return false end
local ok = pcall(function()
circle.Thickness, circle.NumSides, circle.Filled = 1.5, 64, false
circle.Position, circle.Radius, circle.Visible = Vector2.new(-100, -100), 10, false
end)
pcall(circle.Remove or circle.Destroy, circle)
return ok
end
---@param caps string|string[]
---@return boolean, string? false + the first missing cap
function xDTaraZ.Compat.Has(caps)
for _, name in ipairs(type(caps) == "table" and caps or { caps }) do
if not xDTaraZ.Compat.Caps[name] then return false, name end
end
return true
end
---Undoes a hook: the executor's restorefunction first, else `rehook`.
function xDTaraZ.Compat.Unhook(target, rehook)
local api = xDTaraZ.Compat.Api
if target and api.RestoreFunction and pcall(api.RestoreFunction, target) then
if not api.IsFunctionHooked then return end
local ok, still = pcall(api.IsFunctionHooked, target)
if ok and not still then return end
end
rehook()
end
xDTaraZ.Caps = setmetatable({}, {
__index = function(_, name)
return xDTaraZ.Compat.Caps[name] == true
end,
})
xDTaraZ.Util = {}
---@param detail any extra context for the warn; the player only sees `text`
function xDTaraZ.Util.Alert(text, detail)
warn("[BloxStrike] menu:", detail or text)
task.spawn(function()
for _ = 1, xDTaraZ.Config.AlertTries do
if pcall(StarterGui.SetCore, StarterGui, "SendNotification", { Title = "XYZ", Text = text, Duration = 10 }) then return end
task.wait(xDTaraZ.Config.AlertGap)
end
end)
end
---@return table? nil after the player was told on screen
function xDTaraZ.Util.LoadLibrary()
local ran, loaded = pcall(LoadAirflow)
if ran and type(loaded) == "table" then return loaded end
xDTaraZ.Util.Alert("The menu failed to load on this executor: " .. tostring(loaded):match("^[^\n]*"), loaded)
return nil
end
function xDTaraZ.Util.Try(label, fn, ...)
local ok, err = pcall(fn, ...)
if not ok then warn("[BloxStrike] " .. label .. ":", err) end
return ok
end
function xDTaraZ.Util.Copy(text)
local fn = setclipboard or toclipboard
if not fn then return false end
return pcall(fn, text)
end
---@return function? original, nil when already hooked or the executor can't hook it
function xDTaraZ.Util.Hook(key, fn, replacement)
if type(fn) ~= "function" or xDTaraZ.State.Hooks[key] or not xDTaraZ.Caps.HookFunction then return nil end
local ok, original = pcall(hookfunction, fn, replacement)
if not ok or type(original) ~= "function" then
warn("[BloxStrike] hook " .. key .. ":", ok and "executor returned no original" or original)
return nil
end
xDTaraZ.State.Hooks[key] = { Fn = fn, Original = original }
return original
end
function xDTaraZ.Util.UnhookAll()
for key, hook in pairs(xDTaraZ.State.Hooks) do
local ok, err = pcall(xDTaraZ.Compat.Unhook, hook.Fn, function() hookfunction(hook.Fn, hook.Original) end)
if not ok then warn("[BloxStrike] unhook " .. key .. ":", err) end
end
table.clear(xDTaraZ.State.Hooks)
end
function xDTaraZ.Util.Decode(raw)
if type(raw) ~= "string" then return nil end
local ok, decoded = pcall(HttpService.JSONDecode, HttpService, raw)
return ok and decoded or nil
end
function xDTaraZ:Connect(signal, handler)
local conn = signal:Connect(handler)
table.insert(self.State.Connections, conn)
return conn
end
xDTaraZ.GameLib = { Missing = {} }
---@param parent Instance? where it lives today
---@param home string parent name a moved copy must still have
---@return Instance? nil after a warning that names it
function xDTaraZ.GameLib.Find(parent, name, class, home)
local found = parent and parent:FindFirstChild(name)
if found and found:IsA(class) then return found end
for _, desc in ipairs(ReplicatedStorage:GetDescendants()) do
if desc.Name == name and desc:IsA(class) and desc.Parent and desc.Parent.Name == home then return desc end
end
xDTaraZ.GameLib.Missing[name] = "Absent"
warn("[BloxStrike] " .. home .. "." .. name .. " not found, features that need it are blocked")
return nil
end
---@return any module, or nil when this executor can't require it (never throws)
function xDTaraZ.GameLib.Require(inst)
if not (inst and inst:IsA("ModuleScript")) then return nil end
local ok, mod = pcall(require, inst)
if ok then return mod end
local done, okAgain, again = false, false, nil
task.spawn(function()
pcall(setthreadidentity, 2)
local read, identity = pcall(getthreadidentity)
if read and identity == 2 then okAgain, again = pcall(require, inst) end
done = true
end)
local deadline = osClock() + xDTaraZ.Config.RequireTimeout
while not done and osClock() < deadline do task.wait() end
if okAgain then return again end
warn("[BloxStrike] require " .. inst.Name .. ":", mod)
return nil
end
do
local database = ReplicatedStorage:FindFirstChild("Database")
local security = database and database:FindFirstChild("Security")
local custom = database and database:FindFirstChild("Custom")
local controllers = ReplicatedStorage:FindFirstChild("Controllers")
local find = xDTaraZ.GameLib.Find
xDTaraZ.GameLib.Net = xDTaraZ.GameLib.Require(find(security, "Remotes", "ModuleScript", "Security"))
xDTaraZ.GameLib.Character = xDTaraZ.GameLib.Require(find(controllers, "CharacterController", "ModuleScript", "Controllers"))
xDTaraZ.GameLib.Camera = xDTaraZ.GameLib.Require(find(controllers, "CameraController", "ModuleScript", "Controllers"))
xDTaraZ.GameLib.WeaponFolder = find(custom, "Weapons", "Folder", "Custom")
xDTaraZ.GameLib.Rarities = xDTaraZ.GameLib.Require(find(custom, "Rarities", "ModuleScript", "GameStats"))
local vfx = ReplicatedStorage:FindFirstChild("Components") and ReplicatedStorage.Components:FindFirstChild("VFXLibary", true)
xDTaraZ.GameLib.Smoke = xDTaraZ.GameLib.Require(find(vfx, "CreateVoxelSmoke", "ModuleScript", "VFXLibary"))
xDTaraZ.GameLib.Flash = xDTaraZ.GameLib.Require(find(vfx, "FlashEffect", "ModuleScript", "VFXLibary"))
end
xDTaraZ.Weapons = {}
do
local folder = xDTaraZ.GameLib.WeaponFolder
for _, module in ipairs(folder and folder:GetChildren() or {}) do
local data = module:IsA("ModuleScript") and xDTaraZ.GameLib.Require(module)
if type(data) == "table" and data.FireRate then xDTaraZ.Weapons[module.Name] = data end
end
end
xDTaraZ.Player = {}
function xDTaraZ.Player.Character()
local chars = Workspace:FindFirstChild("Characters")
return (chars and chars:FindFirstChild(LocalPlayer.Name)) or LocalPlayer.Character
end
function xDTaraZ.Player.Alive()
local char = xDTaraZ.Player.Character()
return char ~= nil and char.Parent ~= nil and tostring(LocalPlayer:GetAttribute("Dead")) ~= "true"
end
function xDTaraZ.Player.Humanoid()
local char = xDTaraZ.Player.Character()
return char and char:FindFirstChildOfClass("Humanoid")
end
---@return table? the game's own local character state; nil outside a round
function xDTaraZ.Player.Body()
local controller = xDTaraZ.GameLib.Character
if not controller then return nil end
local ok, body = pcall(controller.getCurrentCharacter)
return ok and body or nil
end
function xDTaraZ.Player.Equipped()
return xDTaraZ.Util.Decode(LocalPlayer:GetAttribute("CurrentEquipped"))
end
xDTaraZ.Target = { Motion = setmetatable({}, { __mode = "k" }) }
local losParams = RaycastParams.new()
losParams.FilterType = Enum.RaycastFilterType.Exclude
local losFilter = table.create(3)
local wallParams = RaycastParams.new()
wallParams.FilterType = Enum.RaycastFilterType.Exclude
function xDTaraZ.Target.Candidates()
local list = {}
local chars = Workspace:FindFirstChild("Characters")
if not chars then return list end
for _, model in ipairs(chars:GetChildren()) do
if model.Name ~= LocalPlayer.Name and model:IsA("Model") then list[#list + 1] = model end
end
return list
end
function xDTaraZ.Target.Owner(model)
return Players:FindFirstChild(model.Name)
end
---@return string? team of a character: owner's Team attribute, else the team that wears this character model
function xDTaraZ.Target.TeamOf(model)
local owner = model and xDTaraZ.Target.Owner(model)
local team = owner and owner:GetAttribute("Team")
if team then return team end
local look = model and model:GetAttribute("CharacterName")
if not look then return nil end
if look == Workspace:GetAttribute("CTCharacter") then return "Counter-Terrorists" end
if look == Workspace:GetAttribute("TCharacter") then return "Terrorists" end
return nil
end
function xDTaraZ.Target.MyTeam()
return LocalPlayer:GetAttribute("Team") or xDTaraZ.Target.TeamOf(xDTaraZ.Player.Character())
end
function xDTaraZ.Target.IsEnemy(model)
if tostring(model:GetAttribute("Dead")) == "true" then return false end
if not xDTaraZ.Target.Owner(model) and model:GetAttribute("Health") == nil then return false end
if xDTaraZ.Target.FreeForAll() then return true end
local mine = xDTaraZ.Target.MyTeam()
if mine == nil then return false end
return xDTaraZ.Target.TeamOf(model) ~= mine
end
---Samples every enemy's head each frame so shots can lead a moving target.
function xDTaraZ.Target.Track()
local motion, now = xDTaraZ.Target.Motion, osClock()
for _, model in ipairs(xDTaraZ.Target.Candidates()) do
local head = model:FindFirstChild("Head")
if not head then continue end
local last = motion[head]
if last and now > last.Time then
local step = (head.Position - last.Pos) / (now - last.Time)
last.Vel = last.Vel and last.Vel:Lerp(step, 0.5) or step
last.Pos, last.Time = head.Position, now
else
motion[head] = { Pos = head.Position, Time = now, Vel = Vector3.zero }
end
end
end
---@return Vector3 where the part will be once the shot reaches the server
function xDTaraZ.Target.Lead(part)
if not xDTaraZ.Options.SilentPredict then return part.Position end
local track = xDTaraZ.Target.Motion[part] or xDTaraZ.Target.Motion[part.Parent and part.Parent:FindFirstChild("Head")]
local vel = track and track.Vel or Vector3.zero
if vel.Magnitude > xDTaraZ.Config.LeadMaxSpeed then return part.Position end
local ok, ping = pcall(LocalPlayer.GetNetworkPing, LocalPlayer)
return part.Position + vel * ((ok and ping or 0.05) + xDTaraZ.Config.LeadExtra)
end
---@return boolean true in Deathmatch, where the team picker is only cosmetic
function xDTaraZ.Target.FreeForAll()
return tostring(Workspace:GetAttribute("Gamemode")):find("Deathmatch") ~= nil
end
function xDTaraZ.Target.Part(model, bone)
for _, name in ipairs(xDTaraZ.Config.BoneParts[bone] or xDTaraZ.Config.BoneParts.Head) do
local part = model:FindFirstChild(name)
if part and part:IsA("BasePart") then return part end
end
return model:FindFirstChild("HumanoidRootPart")
end
function xDTaraZ.Target.Visible(from, model, point)
local skip = { Workspace.CurrentCamera, model }
for _, name in ipairs(xDTaraZ.Config.SeeThrough) do
local folder = Workspace:FindFirstChild(name)
if folder then skip[#skip + 1] = folder end
end
wallParams.FilterDescendantsInstances = skip
return Workspace:Raycast(from, point - from, wallParams) == nil
end
---@param cfg table { Bone, Fov?, Visible, MaxDistance, Priority } Fov nil = whole map
---@return Model?, BasePart?
function xDTaraZ.Target.Pick(cfg)
local cam = Workspace.CurrentCamera
local origin = cam.CFrame.Position
local center = cam.ViewportSize / 2
local best, bestPart, bestScore
for _, model in ipairs(xDTaraZ.Target.Candidates()) do
if not xDTaraZ.Target.IsEnemy(model) then continue end
local part = xDTaraZ.Target.Part(model, cfg.Bone)
if not part then continue end
local dist = (part.Position - origin).Magnitude
if dist > cfg.MaxDistance then continue end
local screen, onScreen = cam:WorldToViewportPoint(part.Position)
local screenDist = (vector2New(screen.X, screen.Y) - center).Magnitude
if cfg.Fov and (not onScreen or screenDist > cfg.Fov) then continue end
if cfg.Visible and not xDTaraZ.Target.Visible(origin, model, part.Position) then continue end
local score = dist
if cfg.Priority == "Crosshair" then
score = onScreen and screenDist or 1e6 + dist
elseif cfg.Priority == "Health" then
score = (tonumber(model:GetAttribute("Health")) or 100) * 1e4 + dist
end
if not bestScore or score < bestScore then best, bestPart, bestScore = model, part, score end
end
return best, bestPart
end
---@return boolean still a valid lock, with a looser fov
function xDTaraZ.Target.StillValid(model, part, cfg)
if not (model and part and part.Parent and xDTaraZ.Target.IsEnemy(model)) then return false end
local cam = Workspace.CurrentCamera
local origin = cam.CFrame.Position
if (part.Position - origin).Magnitude > cfg.MaxDistance then return false end
if cfg.Visible and not xDTaraZ.Target.Visible(origin, model, part.Position) then return false end
if not cfg.Fov then return true end
local screen, onScreen = cam:WorldToViewportPoint(part.Position)
local center = cam.ViewportSize / 2
return onScreen and (vector2New(screen.X, screen.Y) - center).Magnitude <= cfg.Fov * xDTaraZ.Config.StickySlack
end
---@return Model? enemy under the crosshair right now
function xDTaraZ.Target.UnderCrosshair()
local cam = Workspace.CurrentCamera
losFilter[1], losFilter[2], losFilter[3] = xDTaraZ.Player.Character(), cam, nil
losParams.FilterDescendantsInstances = losFilter
local hit = Workspace:Raycast(cam.CFrame.Position, cam.CFrame.LookVector * xDTaraZ.Config.TriggerRay, losParams)
if not hit then return nil end
local chars = Workspace:FindFirstChild("Characters")
local model = hit.Instance
while model and model.Parent ~= chars do model = model.Parent end
if model and model:IsA("Model") and xDTaraZ.Target.IsEnemy(model) then return model end
return nil
end
local function roll(percent)
return percent >= 100 or math.random() * 100 < percent
end
xDTaraZ.Aimbot = {}
function xDTaraZ.Aimbot.Config()
local o = xDTaraZ.Options
return { Bone = o.AimbotBone, Fov = o.AimbotFov, Visible = o.AimbotVisible, MaxDistance = o.AimbotMaxDistance, Priority = o.AimbotPriority }
end
function xDTaraZ.Aimbot.Step()
local state, o = xDTaraZ.State, xDTaraZ.Options
if not o.Aimbot then state.AimTarget, state.AimPart = nil, nil return end
local cfg = xDTaraZ.Aimbot.Config()
if o.AimbotSticky and xDTaraZ.Target.StillValid(state.AimTarget, state.AimPart, cfg) then return end
state.AimTarget, state.AimPart = xDTaraZ.Target.Pick(cfg)
end
---@param dt number frame time from RenderStep
function xDTaraZ.Aimbot.Lock(dt)
local o, state = xDTaraZ.Options, xDTaraZ.State
local part, smooth = nil, 1
if o.Ragebot and state.RagePart and state.RagePart.Parent then
part = state.RagePart
elseif o.Aimbot and state.AimPart and state.AimPart.Parent then
part, smooth = state.AimPart, math.max(o.AimbotSmooth, 1)
end
if not part then return end
local cam = Workspace.CurrentCamera
local origin = cam.CFrame.Position
local want = (part.Position - origin).Unit
local alpha = 1 - (1 - 1 / smooth) ^ ((dt or 1 / 60) * 60)
local look = smooth <= 1 and want or cam.CFrame.LookVector:Lerp(want, alpha).Unit
cam.CFrame = cframeLookAt(origin, origin + look)
end
xDTaraZ.Silent = {}
---@param forced boolean ragebot shot: always hits, no fov
function xDTaraZ.Silent.Config(bone, forced)
local o = xDTaraZ.Options
if forced then return { Bone = bone, Visible = o.RageVisible, MaxDistance = o.RageMaxDistance, Priority = "Distance" } end
return { Bone = bone, Fov = o.SilentFov, Visible = o.SilentVisible, MaxDistance = o.SilentMaxDistance, Priority = o.SilentPriority }
end
---@return BasePart? part this shot should go to, nil = shoot where you look
function xDTaraZ.Silent.Aim()
local o, state = xDTaraZ.Options, xDTaraZ.State
state.SilentTarget = nil
if not (o.SilentAim or o.Ragebot) then return nil end
local forced = o.Ragebot
if not forced and not roll(o.SilentHitChance) then return nil end
local bone = (forced or roll(o.SilentHeadChance)) and "Head" or "Torso"
local target, part = xDTaraZ.Target.Pick(xDTaraZ.Silent.Config(bone, forced))
state.SilentTarget = target
return part
end
---The server checks bullets against where you really look, so each new bullet turns the camera to the target and the view goes back as the shot is sent, before anything is drawn.
function xDTaraZ.Silent.InstallHook()
local gun = xDTaraZ.Guns.FindClass()
if gun then
local original
original = xDTaraZ.Util.Hook("Shoot", gun.shoot, function(...)
local state = xDTaraZ.State
state.LastShot = osClock()
state.Shots += 1
return original(...)
end)
end
local bullet = xDTaraZ.Guns.FindBullet()
if bullet and type(bullet.create) == "function" then
local create
create = xDTaraZ.Util.Hook("Bullet", bullet.create, function(...)
xDTaraZ.Silent.Flick()
return create(...)
end)
end
xDTaraZ.Guns.HookSpread()
local net = xDTaraZ.GameLib.Net
local packet = net and net.Inventory and net.Inventory.ShootWeapon
if not packet then return end
local send
send = xDTaraZ.Util.Hook("Send", packet.Send, function(...)
xDTaraZ.Silent.RestoreView()
return send(...)
end)
end
---Turns the camera onto the silent aim target for this bullet.
function xDTaraZ.Silent.Flick()
local ok, part = pcall(xDTaraZ.Silent.Aim)
local cam, state = Workspace.CurrentCamera, xDTaraZ.State
if not (ok and part and cam) then return end
state.ViewSaved = state.ViewSaved or cam.CFrame
cam.CFrame = cframeLookAt(cam.CFrame.Position, xDTaraZ.Target.Lead(part))
state.Redirected += 1
end
---Puts your view back before the game's camera runs, so silent aim never moves what you see.
function xDTaraZ.Silent.RestoreView()
local state, cam = xDTaraZ.State, Workspace.CurrentCamera
if not state.ViewSaved then return end
if cam then cam.CFrame = state.ViewSaved end
state.ViewSaved = nil
end
xDTaraZ.Trigger = {}
function xDTaraZ.Trigger.Press(down)
if down and xDTaraZ.UI.MenuOpen() then return end
xDTaraZ.State.Firing = down
if down then xDTaraZ.State.PressedAt = osClock() end
local center = Workspace.CurrentCamera.ViewportSize / 2
pcall(VirtualInputManager.SendMouseButtonEvent, VirtualInputManager, center.X, center.Y, 0, down, game, 0)
end
---@param want boolean holds auto guns and re-clicks semi-auto ones
function xDTaraZ.Trigger.Fire(want)
local state, now = xDTaraZ.State, osClock()
if not want then
if state.Firing then xDTaraZ.Trigger.Press(false) end
return
end
if not state.Firing then
xDTaraZ.Trigger.Press(true)
return
end
local gap = xDTaraZ.Config.RefireGap
if now - state.PressedAt > gap and now - state.LastShot > gap then xDTaraZ.Trigger.Press(false) end
end
function xDTaraZ.Trigger.Wanted()
local o, state = xDTaraZ.Options, xDTaraZ.State
if not o.Triggerbot then return false end
local model = xDTaraZ.Target.UnderCrosshair()
if model ~= state.TriggerModel then
state.TriggerModel = model
state.TriggerSeen = model and osClock() or nil
state.TriggerRoll = model ~= nil and roll(o.TriggerHitChance)
end
if not model or not state.TriggerRoll then return false end
return osClock() - state.TriggerSeen >= o.TriggerDelay / 1000
end
xDTaraZ.Rage = {}
function xDTaraZ.Rage.Step()
local state = xDTaraZ.State
if not xDTaraZ.Options.Ragebot then state.RageTarget, state.RagePart = nil, nil return false end
state.RageTarget, state.RagePart = xDTaraZ.Target.Pick(xDTaraZ.Silent.Config("Head", true))
return state.RageTarget ~= nil
end
xDTaraZ.Combat = { Circles = {} }
---@return ScreenGui made once; hidden gui first, then CoreGui, then PlayerGui
function xDTaraZ.Combat.Screen()
local screen = xDTaraZ.Combat.Gui
if screen and screen.Parent then return screen end
screen = Instance.new("ScreenGui")
screen.Name = "XYZFov"
screen.IgnoreGuiInset = true
screen.ResetOnSpawn = false
screen.DisplayOrder = xDTaraZ.Config.FovLayer
local function Mount(parent) screen.Parent = parent end
local okHui, hui = pcall(gethui)
local mounted = okHui and hui ~= nil and pcall(Mount, hui)
if not mounted then mounted = pcall(Mount, game:GetService("CoreGui")) end
if not mounted then Mount(LocalPlayer:FindFirstChildOfClass("PlayerGui")) end
xDTaraZ.Combat.Gui = screen
return screen
end
---@return table { Drawing = circle }, or { Frame = ring } drawn with Gui when Drawing is missing
function xDTaraZ.Combat.NewCircle(color)
if xDTaraZ.Caps.Drawing then
local circle = Drawing.new("Circle")
circle.Thickness, circle.NumSides, circle.Filled, circle.Color = 1.5, 64, false, color
return { Drawing = circle }
end
local frame = Instance.new("Frame")
frame.AnchorPoint = vector2New(0.5, 0.5)
frame.BackgroundTransparency = 1
local round = Instance.new("UICorner")
round.CornerRadius = UDim.new(1, 0)
round.Parent = frame
local edge = Instance.new("UIStroke")
edge.Thickness, edge.Color = 1.5, color
edge.Parent = frame
frame.Parent = xDTaraZ.Combat.Screen()
return { Frame = frame }
end
function xDTaraZ.Combat.Circle(key, show, radius)
local circle = xDTaraZ.Combat.Circles[key]
if not circle then
if not show then return end
circle = xDTaraZ.Combat.NewCircle(xDTaraZ.Config.FovColors[key])
xDTaraZ.Combat.Circles[key] = circle
end
local center = Workspace.CurrentCamera.ViewportSize / 2
local drawing, frame = circle.Drawing, circle.Frame
if drawing then
drawing.Visible, drawing.Position, drawing.Radius = show, center, radius
return
end
frame.Visible = show
frame.Position = UDim2.fromOffset(center.X, center.Y)
frame.Size = UDim2.fromOffset(radius * 2, radius * 2)
end
function xDTaraZ.Combat.Step()
local o, state = xDTaraZ.Options, xDTaraZ.State
xDTaraZ.Combat.Circle("Aimbot", o.AimbotShowFov, o.AimbotFov)
xDTaraZ.Combat.Circle("Silent", o.SilentShowFov, o.SilentFov)
if not xDTaraZ.Player.Alive() then
state.AimTarget, state.AimPart, state.RageTarget, state.RagePart, state.SilentTarget = nil, nil, nil, nil, nil
xDTaraZ.Trigger.Fire(false)
return
end
if o.SilentAim or o.Ragebot then xDTaraZ.Target.Track() end
xDTaraZ.Aimbot.Step()
local rage = xDTaraZ.Rage.Step()
xDTaraZ.Trigger.Fire(rage or xDTaraZ.Trigger.Wanted())
end
function xDTaraZ.Combat.Rest()
local state = xDTaraZ.State
state.AimTarget, state.AimPart, state.RageTarget, state.RagePart, state.SilentTarget = nil, nil, nil, nil, nil
xDTaraZ.Trigger.Fire(false)
for key in pairs(xDTaraZ.Combat.Circles) do
xDTaraZ.Combat.Circle(key, false, 0)
end
end
function xDTaraZ.Combat.Start()
RunService:BindToRenderStep("xDTaraZView", Enum.RenderPriority.First.Value, xDTaraZ.Silent.RestoreView)
RunService:BindToRenderStep("xDTaraZAim", xDTaraZ.Config.AimRenderPriority, xDTaraZ.Aimbot.Lock)
xDTaraZ:Connect(RunService.Heartbeat, function()
local ok, err = pcall(xDTaraZ.Combat.Step)
if ok then
xDTaraZ.Faults.Clear("Combat")
else
xDTaraZ.Faults.Report("Combat", err, xDTaraZ.Config.CombatToggles, xDTaraZ.Combat.Rest)
end
end)
end
function xDTaraZ.Combat.Unload()
xDTaraZ.Trigger.Fire(false)
pcall(RunService.UnbindFromRenderStep, RunService, "xDTaraZAim")
pcall(RunService.UnbindFromRenderStep, RunService, "xDTaraZView")
for key, circle in pairs(xDTaraZ.Combat.Circles) do
xDTaraZ.Combat.Circles[key] = nil
if circle.Frame then
circle.Frame:Destroy()
else
pcall(function() circle.Drawing:Remove() end)
end
end
if xDTaraZ.Combat.Gui then
xDTaraZ.Combat.Gui:Destroy()
xDTaraZ.Combat.Gui = nil
end
end
xDTaraZ.Guns = { Signature = nil, Warned = false }
function xDTaraZ.Guns.HookKick()
local cam = xDTaraZ.GameLib.Camera
if not cam then return end
local kick, aim, rotation
kick = xDTaraZ.Util.Hook("Kick", cam.weaponKick, function(...)
if xDTaraZ.Options.NoRecoil then return end
return kick(...)
end)
aim = xDTaraZ.Util.Hook("Recoil", cam.setWeaponRecoil, function(...)
if xDTaraZ.Options.NoRecoil then return end
return aim(...)
end)
rotation = xDTaraZ.Util.Hook("KickRotation", cam.getWeaponKickRotation, function(...)
if xDTaraZ.Options.NoRecoil then return Vector3.zero end
return rotation(...)
end)
end
---@return boolean true while any scope or zoom is up
function xDTaraZ.Guns.Scoped()
if LocalPlayer:GetAttribute("IsSniperScoped") == true then return true end
local cam = xDTaraZ.GameLib.Camera
local getter = cam and cam.getTargetFOV
if type(getter) ~= "function" then return false end
local ok, target = pcall(getter)
if not ok or type(target) ~= "number" then return false end
local state = xDTaraZ.State
if target > state.FovBase then state.FovBase = target end
return target < state.FovBase
end
function xDTaraZ.Guns.StepFov()
local camera, state = Workspace.CurrentCamera, xDTaraZ.State
if not camera then return end
if not xDTaraZ.Options.CameraFov then
if state.FovSaved then camera.FieldOfView, state.FovSaved = state.FovSaved, nil end
state.FovBase = 0
return
end
if xDTaraZ.Guns.Scoped() then return end
state.FovSaved = state.FovSaved or camera.FieldOfView
camera.FieldOfView = xDTaraZ.Options.CameraFovValue
end
function xDTaraZ.Guns.BindFov()
local state = xDTaraZ.State
if state.FovBound then return end
state.FovBound = true
RunService:BindToRenderStep("xDTaraZFov", Enum.RenderPriority.Camera.Value + 3, xDTaraZ.Guns.StepFov)
end
function xDTaraZ.Guns.UnbindFov()
local state, camera = xDTaraZ.State, Workspace.CurrentCamera
if state.FovSaved and camera then camera.FieldOfView, state.FovSaved = state.FovSaved, nil end
state.FovBound = false
pcall(RunService.UnbindFromRenderStep, RunService, "xDTaraZFov")
end
function xDTaraZ.Guns.FindClass()
if not xDTaraZ.Caps.Gc or not xDTaraZ.Caps.HookFunction then return nil end
for _, entry in ipairs(getgc(true)) do
if type(entry) == "table" and rawget(entry, "getSpread") and rawget(entry, "shoot") and rawget(entry, "setupRecoil") then return entry end
end
return nil
end
---@return table? the bullet class that rolls spread for every shot
function xDTaraZ.Guns.FindBullet()
if not xDTaraZ.Caps.Gc or not xDTaraZ.Caps.HookFunction then return nil end
for _, entry in ipairs(getgc(true)) do
if type(entry) == "table" and rawget(entry, "getTrueSpread") and rawget(entry, "getSpreadForConfig") then return entry end
end
return nil
end
---Every spread roll the bullet class makes returns 0 with No Spread, and while a silent aim shot is being built.
function xDTaraZ.Guns.HookSpread()
local bullet = xDTaraZ.Guns.FindBullet()
if not bullet then return end
for _, method in ipairs(xDTaraZ.Config.SpreadMethods) do
if type(bullet[method]) ~= "function" then continue end
local original
original = xDTaraZ.Util.Hook("Spread:" .. method, bullet[method], function(...)
local value = original(...)
if type(value) == "number" and (xDTaraZ.Options.NoSpread or xDTaraZ.State.ViewSaved) then return 0 end
return value
end)
end
end
xDTaraZ.Esp = { Count = 0, Decoded = {}, CacheSize = 0, Ghosts = {}, Folder = nil }
---@return table? JSON attribute decoded once per distinct raw string
function xDTaraZ.Esp.Attr(owner, key)
local raw = owner:GetAttribute(key)
if type(raw) ~= "string" then return nil end
local esp = xDTaraZ.Esp
if esp.Decoded[raw] == nil then
esp.CacheSize += 1
if esp.CacheSize > xDTaraZ.Config.EspCacheMax then table.clear(esp.Decoded) esp.CacheSize = 1 end
esp.Decoded[raw] = xDTaraZ.Util.Decode(raw) or false
end
return esp.Decoded[raw] or nil
end
---@return string " C4 Kit Helmet Scoped" style tags, "" when none
function xDTaraZ.Esp.Flags(owner)
if not owner then return "" end
local flags = {}
local armor = xDTaraZ.Esp.Attr(owner, "Armor")
local bomb = xDTaraZ.Esp.Attr(owner, "Slot5")
if bomb and bomb.Weapon == "C4" then flags[#flags + 1] = "C4" end
if owner:GetAttribute("HasDefuseKit") == true then table.insert(flags, "Kit") end
if armor and type(armor.Type) == "string" and armor.Type:find("Helmet") then flags[#flags + 1] = "Helmet" end
if owner:GetAttribute("IsSniperScoped") == true then flags[#flags + 1] = "Scoped" end
return #flags > 0 and " " .. table.concat(flags, " ") or ""
end
---@return Model? whatever aimbot, silent aim or ragebot is locked on
function xDTaraZ.Esp.Focus()
local state = xDTaraZ.State
local silent = osClock() - state.LastShot < xDTaraZ.Config.SilentFocusTime and state.SilentTarget or nil
return state.AimTarget or silent or state.RageTarget
end
---@return table one lasting stand-in per player, so a body that blinks in and out of view never rebuilds its box
function xDTaraZ.Esp.Ghost(name)
local esp = xDTaraZ.Esp
local ghost = esp.Ghosts[name]
if ghost and ghost.Model.Parent then return ghost end
if not (esp.Folder and esp.Folder.Parent) then
esp.Folder = Instance.new("Folder")
esp.Folder.Name = "xDTaraZGhosts"
esp.Folder.Parent = xDTaraZ.Visuals.EnsureGui()
end
local model = Instance.new("Model")
model.Name = name
local root = Instance.new("Part")
root.Name, root.Size, root.Transparency = "HumanoidRootPart", Vector3.new(2, 2, 1), 1
root.Anchored, root.CanCollide, root.CanQuery, root.CanTouch = true, false, false, false
root.Parent = model
model.PrimaryPart = root
model.Parent = esp.Folder
ghost = { Model = model, Root = root, Seen = 0, Heard = 0 }
esp.Ghosts[name] = ghost
return ghost
end
---@return table xDTaraZ.Visuals row for one player, live body or last known spot
function xDTaraZ.Esp.Row(ghost, model, owner, mine, focus)
local equipped = owner and xDTaraZ.Esp.Attr(owner, "CurrentEquipped")
local label = owner and owner.DisplayName or ghost.Model.Name
if equipped and equipped.Name then label = label .. " [" .. equipped.Name .. "]" end
local live = model and model:FindFirstChild("HumanoidRootPart")
if not live then label = label .. " ?" end
return {
Model = ghost.Model,
Root = live or ghost.Root,
Adornee = model or false,
Name = label .. xDTaraZ.Esp.Flags(owner),
Health = tonumber(owner and owner:GetAttribute("Health")) or tonumber(model and model:GetAttribute("Health")) or 100,
MaxHealth = tonumber(model and model:GetAttribute("MaxHealth")) or 100,
Friendly = ghost.Friendly,
Color = model ~= nil and model == focus and xDTaraZ.Config.EspFocusColor or nil,
}
end
---@return table[] targets in the shape xDTaraZ.Visuals expects; bots count as enemies
function xDTaraZ.Esp.Targets()
local esp, now = xDTaraZ.Esp, osClock()
local list = {}
local mine = xDTaraZ.Target.MyTeam()
local ffa = xDTaraZ.Target.FreeForAll()
local focus = xDTaraZ.Esp.Focus()
pcall(xDTaraZ.Sound.Install)
local live = {}
esp.Searched = esp.Searched or {}
local function Add(model)
local owner = xDTaraZ.Target.Owner(model)
if xDTaraZ.Esp.IsDead(model, owner) then
xDTaraZ.Esp.Forget(model.Name)
return
end
local hrp = model:FindFirstChild("HumanoidRootPart")
if not hrp then return end
local ghost = esp.Ghost(model.Name)
ghost.Root.CFrame, ghost.Seen = hrp.CFrame, now
ghost.Friendly = not ffa and mine ~= nil and xDTaraZ.Target.TeamOf(model) == mine
live[model.Name] = true
list[#list + 1] = esp.Row(ghost, model, owner, mine, focus)
end
for _, model in ipairs(xDTaraZ.Target.Candidates()) do
Add(model)
end
-- Every player counts, also those whose body is not in Workspace.Characters right now
-- (streamed out, respawning, parked somewhere else). The search is throttled per player.
for _, player in ipairs(Players:GetPlayers()) do
if player == LocalPlayer or live[player.Name] then continue end
local model = player.Character
if not (model and model.Parent and model:FindFirstChild("HumanoidRootPart")) then
model = nil
if now - (esp.Searched[player.Name] or 0) > 1 then
esp.Searched[player.Name] = now
local found = Workspace:FindFirstChild(player.Name, true)
if found and found:IsA("Model") and found:FindFirstChild("HumanoidRootPart")
and not (esp.Folder and found:IsDescendantOf(esp.Folder)) then
model = found
end
end
end
if model then Add(model) end
end
local memory = xDTaraZ.Options.EspMemory
for name, ghost in pairs(esp.Ghosts) do
if live[name] then continue end
local owner = Players:FindFirstChild(name)
if owner and xDTaraZ.Esp.IsDead(nil, owner) then
xDTaraZ.Esp.Forget(name)
continue
end
local gone = not ghost.Model.Parent or (not owner and now - ghost.Seen > memory)
if gone then
if not owner then
ghost.Model:Destroy()
esp.Ghosts[name] = nil
end
continue
end
-- Players stay on the ESP at their last known spot until they die or leave; only bots time out.
if not owner and now - math.max(ghost.Seen, ghost.Heard) > memory then continue end
if ghost.Seen == 0 and ghost.Heard == 0 then continue end
list[#list + 1] = esp.Row(ghost, nil, owner, mine, focus)
end
esp.Count = #list
return list
end
---@return boolean true once a player or bot is dead, by body flag or 0 health
function xDTaraZ.Esp.IsDead(model, owner)
if model and (tostring(model:GetAttribute("Dead")) == "true" or (tonumber(model:GetAttribute("Health")) or 1) <= 0) then return true end
if owner and (owner:GetAttribute("Dead") == true or (tonumber(owner:GetAttribute("Health")) or 1) <= 0) then return true end
return false
end
---Drops the remembered spot of someone who died, so no box stays where they fell.
function xDTaraZ.Esp.Forget(name)
local ghost = xDTaraZ.Esp.Ghosts[name]
if ghost then ghost.Seen, ghost.Heard = 0, 0 end
end
---Moves the stand-in of the hidden enemy closest to a footstep or shot, so hidden players keep a box.
function xDTaraZ.Esp.Hear(pos, weapon)
local esp, now = xDTaraZ.Esp, osClock()
local best, bestDist = nil, xDTaraZ.Config.GhostSnap
for name, ghost in pairs(esp.Ghosts) do
if ghost.Friendly or now - ghost.Seen < 0.3 then continue end
local owner = Players:FindFirstChild(name)
local equipped = owner and xDTaraZ.Esp.Attr(owner, "CurrentEquipped")
if weapon and equipped and equipped.Name and equipped.Name ~= weapon then continue end
local dist = (ghost.Root.Position - pos).Magnitude
if dist < bestDist then best, bestDist = ghost, dist end
end
if not best then return end
best.Root.CFrame = CFrame.new(pos + Vector3.new(0, 2.5, 0))
best.Heard = now
end
xDTaraZ.Sound = { Marks = {}, Entry = nil, Original = nil, Folder = nil }
---Hooks into the game's own sound listener: every footstep and shot of every player arrives with its position, even behind walls.
function xDTaraZ.Sound.Install()
local sound = xDTaraZ.Sound
if sound.Entry then return true end
local net = xDTaraZ.GameLib.Net
local packet = net and net.Sound and net.Sound.ReplicateSound
if not (packet and xDTaraZ.Caps.Upvalues) then return false end
local ok, listeners = pcall(function() return getupvalues(packet.Listen)[1] end)
if not ok or type(listeners) ~= "table" then return false end
for _, entry in ipairs(listeners) do
if type(entry) == "table" and type(entry.callback) == "function" then
sound.Entry, sound.Original = entry, entry.callback
entry.callback = function(info, ...)
xDTaraZ.Util.Try("sound esp", xDTaraZ.Sound.Heard, info)
return sound.Original(info, ...)
end
return true
end
end
return false
end
function xDTaraZ.Sound.Uninstall()
local sound = xDTaraZ.Sound
if sound.Entry then sound.Entry.callback = sound.Original end
sound.Entry, sound.Original = nil, nil
for _, mark in ipairs(sound.Marks) do mark.Gui:Destroy() end
table.clear(sound.Marks)
end
---@return boolean true when the sound came from you or a teammate you can see
function xDTaraZ.Sound.Friendly(pos)
local range = xDTaraZ.Config.SoundSelfRange
local me = xDTaraZ.Player.Character()
if me and (me:GetPivot().Position - pos).Magnitude < range then return true end
if xDTaraZ.Target.FreeForAll() then return false end
for _, model in ipairs(xDTaraZ.Target.Candidates()) do
if not xDTaraZ.Target.IsEnemy(model) and (model:GetPivot().Position - pos).Magnitude < range then return true end
end
return false
end
function xDTaraZ.Sound.Heard(info)
if type(info) ~= "table" or typeof(info.Position) ~= "Vector3" then return end
local floor = info.Class == "FloorSounds"
if not floor and info.Name ~= "Shoot" then return end
if xDTaraZ.Sound.Friendly(info.Position) then return end
xDTaraZ.Esp.Hear(info.Position, not floor and tostring(info.Class) or nil)
if xDTaraZ.Options.SoundEsp then xDTaraZ.Sound.Mark(info.Position, floor and "Steps" or tostring(info.Class)) end
end
function xDTaraZ.Sound.Mark(pos, label)
local sound = xDTaraZ.Sound
if not (sound.Folder and sound.Folder.Parent) then
sound.Folder = Instance.new("Folder")
sound.Folder.Name = "xDTaraZSounds"
sound.Folder.Parent = Workspace.Terrain
end
if #sound.Marks >= xDTaraZ.Config.SoundMaxMarks then
table.remove(sound.Marks, 1).Gui:Destroy()
end
local holder = Instance.new("Attachment")
holder.WorldPosition = pos
local gui = Instance.new("BillboardGui")
gui.AlwaysOnTop, gui.Size, gui.LightInfluence = true, UDim2.fromOffset(120, 34), 0
local text = Instance.new("TextLabel")
text.BackgroundTransparency, text.Size, text.Font, text.TextSize = 1, UDim2.fromScale(1, 1), Enum.Font.GothamBold, 13
text.TextColor3, text.TextStrokeTransparency = xDTaraZ.Config.SoundColor, 0.3
text.Parent = gui
gui.Parent = holder
holder.Parent = Workspace.Terrain
gui.Adornee = holder
sound.Marks[#sound.Marks + 1] = { Gui = holder, Text = text, Label = label, Born = osClock() }
end
---Fades markers and keeps their distance fresh.
function xDTaraZ.Sound.Step()
local sound, life = xDTaraZ.Sound, xDTaraZ.Options.SoundEspLife
local cam = Workspace.CurrentCamera
for index = #sound.Marks, 1, -1 do
local mark = sound.Marks[index]
local age = osClock() - mark.Born
if age > life or not xDTaraZ.Options.SoundEsp then
mark.Gui:Destroy()
table.remove(sound.Marks, index)
else
local dist = cam and math.floor((mark.Gui.WorldPosition - cam.CFrame.Position).Magnitude) or 0
mark.Text.Text = ("%s\n%dm"):format(mark.Label, dist)
mark.Text.TextTransparency = age / life * 0.8
end
end
end
xDTaraZ.World = {}
function xDTaraZ.World.Step()
local opts, state = xDTaraZ.Options, xDTaraZ.State
if opts.Fullbright then
state.LightSaved = state.LightSaved or { Lighting.Brightness, Lighting.ClockTime, Lighting.FogEnd, Lighting.GlobalShadows, Lighting.Ambient }
Lighting.Brightness, Lighting.ClockTime, Lighting.FogEnd, Lighting.GlobalShadows = 2, 14, 1e6, false
Lighting.Ambient = Color3.fromRGB(178, 178, 178)
elseif state.LightSaved then
Lighting.Brightness, Lighting.ClockTime, Lighting.FogEnd, Lighting.GlobalShadows, Lighting.Ambient = table.unpack(state.LightSaved)
state.LightSaved = nil
end
end
---No Flash and No Smoke stop the game from making the effect at all, and clear what is already on screen.
function xDTaraZ.World.HookEffects()
local smoke, flash = xDTaraZ.GameLib.Smoke, xDTaraZ.GameLib.Flash
if smoke and type(smoke.Create) == "function" then
local create
create = xDTaraZ.Util.Hook("Smoke", smoke.Create, function(...)
if xDTaraZ.Options.NoSmoke then return nil end
return create(...)
end)
end
if flash and type(flash.Flash) == "function" then
local blind
blind = xDTaraZ.Util.Hook("Flash", flash.Flash, function(...)
if xDTaraZ.Options.NoFlash then return nil end
return blind(...)
end)
end
if xDTaraZ.Options.NoSmoke and smoke then pcall(smoke.DestroyAll) end
if xDTaraZ.Options.NoFlash and flash then pcall(flash.CancelFlash) end
end
function xDTaraZ.World.OnIdled()
if not xDTaraZ.Options.AntiAfk then return end
pcall(function()
VirtualUser:CaptureController()
VirtualUser:ClickButton2(vector2New())
end)
end
function xDTaraZ.World.OnJump()
if not xDTaraZ.Options.InfiniteJump or xDTaraZ.Player.Body() then return end
local hum = xDTaraZ.Player.Humanoid()
if hum then hum:ChangeState(Enum.HumanoidStateType.Jumping) end
end
function xDTaraZ.World.SetCrouch(down)
local state = xDTaraZ.State
if state.HopCrouched == down then return end
state.HopCrouched = down
local controller = xDTaraZ.GameLib.Character
if not controller then return end
if not down and UserInputService:IsKeyDown(Enum.KeyCode.LeftControl) then return end
controller.crouch(down)
end
function xDTaraZ.World.OnSpace(input, down, processed)
if input.KeyCode ~= Enum.KeyCode.Space then return end
if down and UserInputService:GetFocusedTextBox() then return end
xDTaraZ.State.SpaceHeld = down
end
function xDTaraZ.World.Hop()
local opts, state = xDTaraZ.Options, xDTaraZ.State
local body = opts.BunnyHop and state.SpaceHeld and xDTaraZ.Player.Alive() and xDTaraZ.Player.Body()
if not body or UserInputService:GetFocusedTextBox() then
xDTaraZ.World.SetCrouch(false)
return
end
local controller = xDTaraZ.GameLib.Character
local grounded = body.OnGround == true
xDTaraZ.World.SetCrouch(opts.BunnyCrouch and not grounded)
controller.jump(false)
if not grounded or osClock() - state.HopAt < xDTaraZ.Config.HopGap then return end
state.HopAt = osClock()
controller.jump()
end
function xDTaraZ.World.HopStep()
local ok, err = pcall(xDTaraZ.World.Hop)
if ok then
xDTaraZ.Faults.Clear("Bunny Hop")
else
xDTaraZ.Faults.Report("Bunny Hop", err, { "BunnyHop" }, xDTaraZ.World.Hop)
end
end
function xDTaraZ.World.Rejoin()
pcall(TeleportService.TeleportToPlaceInstance, TeleportService, game.PlaceId, game.JobId, LocalPlayer)
end
function xDTaraZ.World.ServerHop()
pcall(TeleportService.Teleport, TeleportService, game.PlaceId, LocalPlayer)
end
xDTaraZ.Skins = { Map = {}, Applied = setmetatable({}, { __mode = "k" }), Catalog = nil, Rarity = {}, Images = {}, Colors = {}, Originals = setmetatable({}, { __mode = "k" }) }
function xDTaraZ.Skins.Folder()
local assets = ReplicatedStorage:FindFirstChild("Assets")
return assets and assets:FindFirstChild("Skins")
end
---@return string? category name, nil = not a weapon look
function xDTaraZ.Skins.CategoryOf(name, kind)
local mapped = xDTaraZ.Config.SkinKinds[kind]
if mapped then return mapped end
if kind ~= "Weapon" then return nil end
local gun = xDTaraZ.Weapons[name]
return gun and gun.Type or "Other"
end
function xDTaraZ.Skins.Build()
local skins = xDTaraZ.Skins
local folder = skins.Folder()
local listing = xDTaraZ.Util.Decode(ReplicatedStorage:GetAttribute("AvaiableSkins")) or {}
local ranks = xDTaraZ.Config.SkinRanks
local catalog = {}
for name, entries in pairs(listing) do
if type(entries) ~= "table" or not (folder and folder:FindFirstChild(name)) then continue end
local _, first = next(entries)
local category = skins.CategoryOf(name, type(first) == "table" and first.type)
if not category then continue end
catalog[category] = catalog[category] or {}
table.insert(catalog[category], name)
skins.Rarity[name], skins.Images[name], skins.Colors[name] = {}, {}, {}
for skin, entry in pairs(entries) do
if type(entry) ~= "table" then continue end
skins.Rarity[name][skin] = ranks[entry.rarity] or 0
local look = type(entry.wearImages) == "table" and entry.wearImages[1]
skins.Images[name][skin] = look and look.assetId
local rarity = xDTaraZ.GameLib.Rarities and xDTaraZ.GameLib.Rarities[entry.rarity]
skins.Colors[name][skin] = type(rarity) == "table" and rarity.Color or nil
end
end
for _, names in pairs(catalog) do table.sort(names) end
skins.Catalog = catalog
end
---@return string[] categories in display order
function xDTaraZ.Skins.Categories()
if not xDTaraZ.Skins.Catalog then xDTaraZ.Skins.Build() end
local list, seen = {}, {}
for _, category in ipairs(xDTaraZ.Config.SkinOrder) do
if xDTaraZ.Skins.Catalog[category] then list[#list + 1] = category seen[category] = true end
end
for category in pairs(xDTaraZ.Skins.Catalog) do
if not seen[category] then list[#list + 1] = category end
end
if #list == 0 then list[1] = "-" end
return list
end
function xDTaraZ.Skins.Items(category)
if not xDTaraZ.Skins.Catalog then xDTaraZ.Skins.Build() end
local names = xDTaraZ.Skins.Catalog[category or ""]
return (names and #names > 0) and names or { "-" }
end
---@return string[] rarest first
function xDTaraZ.Skins.List(weapon)
local folder = xDTaraZ.Skins.Folder()
local holder = folder and folder:FindFirstChild(weapon or "")
if not holder then return { "-" } end
local ranks = xDTaraZ.Skins.Rarity[weapon] or {}
local names = {}
for _, child in ipairs(holder:GetChildren()) do names[#names + 1] = child.Name end
table.sort(names, function(a, b)
local ra, rb = ranks[a] or 0, ranks[b] or 0
if ra ~= rb then return ra > rb end
return a < b
end)
return #names > 0 and names or { "-" }
end
---@param view string "Camera" or "Character"
---@return Folder? best wear available
function xDTaraZ.Skins.Source(weapon, skin, view)
local folder = xDTaraZ.Skins.Folder()
local paint = folder and folder:FindFirstChild(weapon) and folder[weapon]:FindFirstChild(skin)
local side = paint and paint:FindFirstChild(view)
if not side then return nil end
for _, wear in ipairs(xDTaraZ.Config.SkinWears) do
if side:FindFirstChild(wear) then return side[wear] end
end
return side:FindFirstChildOfClass("Folder")
end
function xDTaraZ.Skins.Paint(model, weapon, skin, view)
local source = xDTaraZ.Skins.Source(weapon, skin, view)
if not source then return false end
local whole = source:FindFirstChild("SurfaceAppearance")
local arms = xDTaraZ.Config.SkinArms
for _, part in ipairs(model:GetDescendants()) do
if not part:IsA("BasePart") then continue end
local old = part:FindFirstChildOfClass("SurfaceAppearance")
local look = source:FindFirstChild(part.Name)
if not look and whole and old and not arms[part.Name] then look = whole end
if not look then continue end
local originals = xDTaraZ.Skins.Originals
if originals[part] == nil then originals[part] = old and old:Clone() or false end
if old then old:Destroy() end
look:Clone().Parent = part
end
return true
end
---Puts back the look each part had before we painted it.
function xDTaraZ.Skins.Reset(model)
local originals = xDTaraZ.Skins.Originals
for _, part in ipairs(model:GetDescendants()) do
local saved = part:IsA("BasePart") and originals[part]
if saved == nil then continue end
local current = part:FindFirstChildOfClass("SurfaceAppearance")
if current then current:Destroy() end
if saved then saved:Clone().Parent = part end
originals[part] = nil
end
end
function xDTaraZ.Skins.WeaponOf(model)
local folder = xDTaraZ.Skins.Folder()
if folder and folder:FindFirstChild(model.Name) then return model.Name end
local equipped = xDTaraZ.Player.Equipped()
return equipped and (equipped.Weapon or equipped.Name)
end
function xDTaraZ.Skins.Models()
local list = {}
for _, child in ipairs(Workspace.CurrentCamera:GetChildren()) do
if child:IsA("Model") then list[#list + 1] = { child, "Camera" } end
end
local char = xDTaraZ.Player.Character()
local folder = xDTaraZ.Skins.Folder()
if char and folder then
for _, child in ipairs(char:GetChildren()) do
if child:IsA("Model") and folder:FindFirstChild(child.Name) then list[#list + 1] = { child, "Character" } end
end
end
return list
end
function xDTaraZ.Skins.Step()
local skins = xDTaraZ.Skins
local on = xDTaraZ.Options.SkinChanger
local glove = on and skins.Map[xDTaraZ.Config.GloveKey]
local gloveSkin = glove and skins.Map[glove]
for _, entry in ipairs(skins.Models()) do
local model, view = entry[1], entry[2]
local weapon = skins.WeaponOf(model)
local skin = on and weapon and skins.Map[weapon] or nil
local key = (skin or gloveSkin) and (tostring(weapon) .. "|" .. tostring(skin) .. "|" .. tostring(gloveSkin)) or nil
if skins.Applied[model] == key then continue end
if skins.Applied[model] then skins.Reset(model) end
if skin then skins.Paint(model, weapon, skin, view) end
if gloveSkin and view == "Camera" then skins.Paint(model, glove, gloveSkin, view) end
skins.Applied[model] = key
end
end
function xDTaraZ.Skins.Set(weapon, skin, quiet)
if not weapon or weapon == "-" or not skin or skin == "-" then return false end
local map, gloveKey = xDTaraZ.Skins.Map, xDTaraZ.Config.GloveKey
local isGlove = xDTaraZ.Skins.Catalog and table.find(xDTaraZ.Skins.Catalog.Gloves or {}, weapon)
if skin == "Default" then
if map[weapon] == nil then return false end
map[weapon] = nil
if map[gloveKey] == weapon then map[gloveKey] = nil end
else
if map[weapon] == skin then return false end
map[weapon] = skin
if isGlove then map[gloveKey] = weapon end
end
if not quiet then xDTaraZ.Skins.Save() end
return true
end
---@param pick fun(list: string[]): string chooses one skin per item
---@return number items changed
function xDTaraZ.Skins.SetAll(pick)
local count = 0
for _, category in ipairs(xDTaraZ.Skins.Categories()) do
if category == "Gloves" then continue end
for _, weapon in ipairs(xDTaraZ.Skins.Items(category)) do
xDTaraZ.Skins.Set(weapon, pick(xDTaraZ.Skins.List(weapon)), true)
count += 1
end
end
xDTaraZ.Skins.Save()
return count
end
function xDTaraZ.Skins.Save()
if type(writefile) ~= "function" then return end
local ok, err = pcall(function()
if type(isfolder) == "function" and not isfolder(xDTaraZ.Config.SaveFolder) then makefolder(xDTaraZ.Config.SaveFolder) end
writefile(xDTaraZ.Config.SkinFile, HttpService:JSONEncode(xDTaraZ.Skins.Map))
end)
if not ok then warn("[BloxStrike] skins save:", err) end
end
function xDTaraZ.Skins.Load()
if type(readfile) ~= "function" or type(isfile) ~= "function" then return end
local ok, raw = pcall(function()
if isfile(xDTaraZ.Config.SkinFile) then
return readfile(xDTaraZ.Config.SkinFile)
elseif isfile("BloxStrike/skins.json") then
return readfile("BloxStrike/skins.json")
end
end)
local map = ok and xDTaraZ.Util.Decode(raw)
if type(map) == "table" then xDTaraZ.Skins.Map = map end
end
xDTaraZ.Economy = {}
function xDTaraZ.Economy.RedeemAll()
local net = xDTaraZ.GameLib.Net
local packet = net and net.Dashboard and net.Dashboard.RedeemCode
if not packet or xDTaraZ.State.Redeeming then return 0 end
xDTaraZ.State.Redeeming = true
local sent = 0
for _, code in ipairs(xDTaraZ.Config.Codes) do
if pcall(packet.Send, code) then sent += 1 end
task.wait(xDTaraZ.Config.RedeemGap)
end
xDTaraZ.State.Redeeming = false
return sent
end
function xDTaraZ.Economy.HookBuys()
local net = xDTaraZ.GameLib.Net
local packet = net and net.Inventory and net.Inventory.BuyMenuPurchase
if not packet then return end
local original
original = xDTaraZ.Util.Hook("Buy", packet.Send, function(payload, ...)
if type(payload) == "table" and payload.Name and not xDTaraZ.State.Replaying then
local list = xDTaraZ.State.Bought
for index, entry in ipairs(list) do
if entry.Name == payload.Name then table.remove(list, index) break end
end
list[#list + 1] = { Equipment = payload.Equipment, Path = payload.Path, Name = payload.Name }
end
return original(payload, ...)
end)
end
function xDTaraZ.Economy.Rebuy()
local net = xDTaraZ.GameLib.Net
local packet = net and net.Inventory and net.Inventory.BuyMenuPurchase
if not packet or #xDTaraZ.State.Bought == 0 or xDTaraZ.State.Replaying then return 0 end
xDTaraZ.State.Replaying = true
local sent = 0
for _, entry in ipairs(xDTaraZ.State.Bought) do
if pcall(packet.Send, { Equipment = entry.Equipment, Path = entry.Path, Name = entry.Name }) then sent += 1 end
task.wait(xDTaraZ.Config.RebuyGap)
end
xDTaraZ.State.Replaying = false
return sent
end
---Buys once each time the game enters a buy phase, so warmup and round 1 each get their own rebuy.
function xDTaraZ.Economy.Step()
local state = xDTaraZ.State
local phase = Workspace:GetAttribute("GameState")
local entered = xDTaraZ.Config.BuyStates[phase] and phase ~= state.LastPhase
state.LastPhase = phase
if not entered then state.RebuyPending = state.RebuyPending and xDTaraZ.Config.BuyStates[phase] return end
state.RebuyPending = true
end
function xDTaraZ.Economy.Tick()
local state = xDTaraZ.State
xDTaraZ.Economy.Step()
if not (xDTaraZ.Options.AutoRebuy and state.RebuyPending and xDTaraZ.Player.Alive()) then return end
state.RebuyPending = false
task.spawn(xDTaraZ.Economy.Rebuy)
end
function xDTaraZ.Economy.BoughtText()
local names = {}
for _, entry in ipairs(xDTaraZ.State.Bought) do names[#names + 1] = entry.Name end
return #names > 0 and table.concat(names, ", ") or "-"
end
xDTaraZ.Spectators = {}
function xDTaraZ.Spectators.Text()
local count = tonumber(LocalPlayer:GetAttribute("Spectators")) or 0
if count == 0 then return "None" end
local names = {}
for _, player in ipairs(Players:GetPlayers()) do
if player ~= LocalPlayer and tostring(player:GetAttribute("IsSpectating")) == "true" and tostring(player:GetAttribute("Dead")) == "true" then
names[#names + 1] = player.DisplayName
end
end
return count .. " watching" .. (#names > 0 and (" (dead: " .. table.concat(names, ", ") .. ")") or "")
end
xDTaraZ.Faults = { Streaks = {} }
function xDTaraZ.Faults.Clear(name)
xDTaraZ.Faults.Streaks[name] = nil
end
function xDTaraZ.Faults.AnyOn(toggles)
for _, key in ipairs(toggles) do
if xDTaraZ.Options[key] then return true end
end
return false
end
---@param restore function? the feature's own off path
function xDTaraZ.Faults.Halt(name, err, toggles, restore)
warn("[BloxStrike] " .. name .. " stopped:", err)
for _, key in ipairs(toggles) do
xDTaraZ.Options[key] = false
end
if restore then xDTaraZ.Util.Try(name .. " restore", restore) end
local halted = xDTaraZ.State.Halted
halted[#halted + 1] = { name, tostring(err):match("^[^\n]*"), toggles }
end
---@param toggles string[] turned off once the feature keeps failing; empty = keeps running, warns once per streak
---@param restore function? run once when the feature is stopped
---@return boolean true while the feature is stopped
function xDTaraZ.Faults.Report(name, err, toggles, restore)
local now, streaks = osClock(), xDTaraZ.Faults.Streaks
local streak = streaks[name]
if not streak or (streak.Halted and xDTaraZ.Faults.AnyOn(toggles)) then
streak = { Count = 0, First = now, Warned = false, Halted = false }
streaks[name] = streak
end
streak.Count += 1
if streak.Warned then return streak.Halted end
if streak.Count < xDTaraZ.Config.MaxFails or now - streak.First < xDTaraZ.Config.FailWindow then return false end
streak.Warned = true
if #toggles == 0 then
warn("[BloxStrike] " .. name .. " keeps failing:", err)
return false
end
streak.Halted = true
xDTaraZ.Faults.Halt(name, err, toggles, restore)
return true
end
xDTaraZ.Scheduler = { Jobs = {} }
---@param toggles string[] options the job serves; switched off if it keeps failing
---@param restore function? the job's off path, run once if it gets stopped
function xDTaraZ.Scheduler.Every(name, interval, fn, toggles, restore)
xDTaraZ.Scheduler.Jobs[name] = { Interval = interval, Fn = fn, Next = 0, Toggles = toggles, Restore = restore }
end
---@return boolean a halted job runs again once one of its toggles is back on
function xDTaraZ.Scheduler.Resume(name, job)
if not xDTaraZ.Faults.AnyOn(job.Toggles) then return false end
job.Halted = false
xDTaraZ.Faults.Clear(name)
return true
end
function xDTaraZ.Scheduler.Boot()
xDTaraZ:Connect(RunService.Heartbeat, function()
local now = osClock()
for name, job in pairs(xDTaraZ.Scheduler.Jobs) do
if now < job.Next then continue end
job.Next = now + job.Interval
if job.Halted and not xDTaraZ.Scheduler.Resume(name, job) then continue end
local ok, err = pcall(job.Fn)
if ok then
xDTaraZ.Faults.Clear(name)
else
job.Halted = xDTaraZ.Faults.Report(name, err, job.Toggles, job.Restore)
end
end
end)
end
---Player ESP drawn with plain GuiObjects (no Drawing API needed). The old UI library had this built in;
---AirFlow has no ESP, so it lives in the script now. Targets come from xDTaraZ.Esp.Targets.
xDTaraZ.Visuals = {
Settings = {
Enabled = false,
Box = true,
BoxStyle = "Full",
BoxFill = false,
Name = true,
Health = true,
HealthText = false,
Distance = true,
Tracer = false,
TracerOrigin = "Bottom",
HeadDot = false,
Chams = false,
Arrows = false,
Radar = false,
RadarRange = 250,
RadarSize = 180,
TeamCheck = true,
MaxDistance = math.huge,
TextSize = 13,
EnemyColor = Color3.fromRGB(232, 88, 76),
FriendColor = Color3.fromRGB(96, 196, 120),
},
Entries = {},
Targets = {},
LastScan = 0,
ScanInterval = 0.2,
Radar = { Dots = {} },
}
function xDTaraZ.Visuals.New(class, props)
local object = Instance.new(class)
local parent = props.Parent
props.Parent = nil
for key, value in pairs(props) do object[key] = value end
if parent then object.Parent = parent end
return object
end
function xDTaraZ.Visuals.EnsureGui()
local visuals = xDTaraZ.Visuals
if visuals.Gui and visuals.Gui.Parent then return visuals.Gui end
local screen = visuals.New("ScreenGui", { Name = "XYZVisuals", IgnoreGuiInset = true, ResetOnSpawn = false, DisplayOrder = 5, ZIndexBehavior = Enum.ZIndexBehavior.Sibling })
local function Mount(parent) screen.Parent = parent end
local okHui, hui = pcall(gethui)
local mounted = okHui and hui ~= nil and pcall(Mount, hui)
if not mounted then mounted = pcall(Mount, game:GetService("CoreGui")) end
if not mounted then Mount(LocalPlayer:FindFirstChildOfClass("PlayerGui")) end
visuals.Gui = screen
return screen
end
function xDTaraZ.Visuals.Label(parent, alignY)
return xDTaraZ.Visuals.New("TextLabel", {
BackgroundTransparency = 1,
AnchorPoint = Vector2.new(0.5, alignY),
Size = UDim2.fromOffset(200, 16),
Font = Enum.Font.GothamBold,
TextColor3 = Color3.new(1, 1, 1),
TextStrokeTransparency = 0.35,
Visible = false,
Parent = parent,
})
end
---@return table reusable GUI pieces for one ESP target
function xDTaraZ.Visuals.BuildEntry(parent)
local new = xDTaraZ.Visuals.New
local entry = {}
entry.Box = new("Frame", { BackgroundTransparency = 1, Visible = false, Parent = parent })
entry.BoxStroke = new("UIStroke", { Thickness = 1.5, Parent = entry.Box })
entry.BoxOutline = new("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(-1, -1), Size = UDim2.new(1, 2, 1, 2), Parent = entry.Box })
new("UIStroke", { Thickness = 1, Color = Color3.new(0, 0, 0), Transparency = 0.3, Parent = entry.BoxOutline })
entry.Corners = {}
for index = 1, 8 do
entry.Corners[index] = new("Frame", { BorderSizePixel = 0, Visible = false, Parent = parent })
end
entry.HealthBack = new("Frame", { BackgroundColor3 = Color3.new(0, 0, 0), BackgroundTransparency = 0.35, BorderSizePixel = 0, Visible = false, Parent = parent })
entry.HealthFill = new("Frame", { AnchorPoint = Vector2.new(0, 1), Position = UDim2.fromScale(0, 1), BorderSizePixel = 0, Parent = entry.HealthBack })
entry.HealthText = xDTaraZ.Visuals.Label(parent, 0.5)
entry.Name = xDTaraZ.Visuals.Label(parent, 1)
entry.Distance = xDTaraZ.Visuals.Label(parent, 0)
entry.Tracer = new("Frame", { AnchorPoint = Vector2.new(0.5, 0.5), BorderSizePixel = 0, BackgroundColor3 = Color3.new(1, 1, 1), Visible = false, Parent = parent })
entry.Dot = new("Frame", { AnchorPoint = Vector2.new(0.5, 0.5), Size = UDim2.fromOffset(6, 6), BorderSizePixel = 0, Visible = false, Parent = parent })
new("UICorner", { CornerRadius = UDim.new(1, 0), Parent = entry.Dot })
entry.Arrow = xDTaraZ.Visuals.Label(parent, 0.5)
entry.Arrow.Text = "▲"
entry.Arrow.Size = UDim2.fromOffset(20, 20)
return entry
end
function xDTaraZ.Visuals.HideEntry(entry)
entry.Box.Visible = false
for _, corner in ipairs(entry.Corners) do corner.Visible = false end
entry.HealthBack.Visible, entry.HealthText.Visible = false, false
entry.Name.Visible, entry.Distance.Visible = false, false
entry.Tracer.Visible, entry.Dot.Visible, entry.Arrow.Visible = false, false, false
if entry.Highlight then entry.Highlight.Enabled = false end
end
function xDTaraZ.Visuals.DestroyEntry(entry)
for _, key in ipairs({ "Box", "HealthBack", "HealthText", "Name", "Distance", "Tracer", "Dot", "Arrow", "Highlight" }) do
if entry[key] then pcall(entry[key].Destroy, entry[key]) end
end
for _, corner in ipairs(entry.Corners) do pcall(corner.Destroy, corner) end
end
function xDTaraZ.Visuals.SetLine(line, from, to, thickness)
local delta = to - from
line.Position = UDim2.fromOffset((from.X + to.X) / 2, (from.Y + to.Y) / 2)
line.Size = UDim2.fromOffset(delta.Magnitude, thickness)
line.Rotation = math.deg(math.atan2(delta.Y, delta.X))
line.Visible = true
end
---Places the 2D box, bars and labels from a screen rect.
function xDTaraZ.Visuals.Layout(entry, x, y, w, h, info, color, origin)
local settings = xDTaraZ.Visuals.Settings
local full = settings.Box and settings.BoxStyle ~= "Corner"
entry.Box.Visible = full
if full then
entry.Box.Position, entry.Box.Size = UDim2.fromOffset(x, y), UDim2.fromOffset(w, h)
entry.BoxStroke.Color = color
entry.Box.BackgroundTransparency = settings.BoxFill and 0.82 or 1
entry.Box.BackgroundColor3 = color
end
local corner = settings.Box and settings.BoxStyle == "Corner"
local cw, ch = math.max(w / 4, 3), math.max(h / 5, 3)
local spots = {
{ x, y, cw, 2 }, { x, y, 2, ch }, { x + w - cw, y, cw, 2 }, { x + w - 2, y, 2, ch },
{ x, y + h - 2, cw, 2 }, { x, y + h - ch, 2, ch }, { x + w - cw, y + h - 2, cw, 2 }, { x + w - 2, y + h - ch, 2, ch },
}
for index, piece in ipairs(entry.Corners) do
piece.Visible = corner
if corner then
local spot = spots[index]
piece.Position, piece.Size, piece.BackgroundColor3 = UDim2.fromOffset(spot[1], spot[2]), UDim2.fromOffset(spot[3], spot[4]), color
end
end
local ratio = math.clamp((info.Health or 0) / math.max(info.MaxHealth or 100, 1), 0, 1)
entry.HealthBack.Visible = settings.Health
if settings.Health then
entry.HealthBack.Position, entry.HealthBack.Size = UDim2.fromOffset(x - 6, y), UDim2.fromOffset(3, h)
entry.HealthFill.Size = UDim2.fromScale(1, ratio)
entry.HealthFill.BackgroundColor3 = Color3.fromRGB(232, 88, 76):Lerp(Color3.fromRGB(96, 214, 110), ratio)
end
entry.HealthText.Visible = settings.HealthText
if settings.HealthText then
local hp = math.floor(info.Health or 0)
if entry.LastHp ~= hp then
entry.LastHp = hp
entry.HealthText.Text = tostring(hp)
end
entry.HealthText.TextSize = settings.TextSize - 2
entry.HealthText.Position = UDim2.fromOffset(x - 20, y + h * (1 - ratio))
end
entry.Name.Visible = settings.Name
if settings.Name then
local name = info.Name or ""
if entry.LastName ~= name then
entry.LastName = name
entry.Name.Text = name
end
entry.Name.TextSize, entry.Name.TextColor3 = settings.TextSize, color
entry.Name.Position = UDim2.fromOffset(x + w / 2, y - 2)
end
entry.Distance.Visible = settings.Distance and info.Distance ~= nil
if entry.Distance.Visible then
local distance = math.floor(info.Distance)
if entry.LastDist ~= distance then
entry.LastDist = distance
entry.Distance.Text = distance .. "m"
end
entry.Distance.TextSize = settings.TextSize - 1
entry.Distance.Position = UDim2.fromOffset(x + w / 2, y + h + 2)
end
entry.Dot.Visible = settings.HeadDot
if settings.HeadDot then
entry.Dot.Position, entry.Dot.BackgroundColor3 = UDim2.fromOffset(x + w / 2, y + h * 0.1), color
end
if settings.Tracer and origin then
entry.Tracer.BackgroundColor3 = color
xDTaraZ.Visuals.SetLine(entry.Tracer, origin, Vector2.new(x + w / 2, y + h), 1.5)
else
entry.Tracer.Visible = false
end
end
function xDTaraZ.Visuals.TracerOrigin(viewport)
local mode = xDTaraZ.Visuals.Settings.TracerOrigin
if mode == "Center" then return viewport / 2 end
if mode == "Mouse" then return UserInputService:GetMouseLocation() end
return Vector2.new(viewport.X / 2, viewport.Y)
end
function xDTaraZ.Visuals.Chams(entry, model, color, on)
if not on then
if entry.Highlight then entry.Highlight.Enabled = false end
return
end
if not entry.Highlight then
entry.Highlight = xDTaraZ.Visuals.New("Highlight", { DepthMode = Enum.HighlightDepthMode.AlwaysOnTop, FillTransparency = 0.55, OutlineTransparency = 0, Parent = xDTaraZ.Visuals.EnsureGui() })
end
entry.Highlight.Adornee = model
entry.Highlight.FillColor, entry.Highlight.OutlineColor = color, color
entry.Highlight.Enabled = true
end
function xDTaraZ.Visuals.Arrow(entry, screen, viewport, color)
local center = viewport / 2
local dir = Vector2.new(screen.X, screen.Y) - center
if screen.Z < 0 then dir = -dir end
if dir.Magnitude < 1 then return end
local unit = dir.Unit
local radius = math.min(viewport.X, viewport.Y) / 2 - 40
local pos = center + unit * radius
entry.Arrow.Position = UDim2.fromOffset(pos.X, pos.Y)
entry.Arrow.Rotation = math.deg(math.atan2(unit.Y, unit.X)) + 90
entry.Arrow.TextColor3 = color
entry.Arrow.TextSize = 18
entry.Arrow.Visible = true
end
function xDTaraZ.Visuals.Scan()
local visuals = xDTaraZ.Visuals
local now = osClock()
if now - visuals.LastScan < visuals.ScanInterval then return end
visuals.LastScan = now
local ok, list = pcall(xDTaraZ.Esp.Targets)
if ok and type(list) == "table" then visuals.Targets = list end
end
function xDTaraZ.Visuals.Render()
local visuals = xDTaraZ.Visuals
local settings = visuals.Settings
if not settings.Enabled then return end
visuals.Scan()
local cam = Workspace.CurrentCamera
if not cam then return end
local gui = visuals.EnsureGui()
local viewport = cam.ViewportSize
local origin = visuals.TracerOrigin(viewport)
local camPos = cam.CFrame.Position
local seen = {}
for _, info in ipairs(visuals.Targets) do
local model = info.Model
if typeof(model) ~= "Instance" or not model.Parent then continue end
if settings.TeamCheck and info.Friendly then continue end
local root = info.Root or model:FindFirstChild("HumanoidRootPart") or model.PrimaryPart
if not root then continue end
local distance = (root.Position - camPos).Magnitude
if distance > settings.MaxDistance then continue end
seen[model] = true
local entry = visuals.Entries[model]
if not entry then
entry = visuals.BuildEntry(gui)
visuals.Entries[model] = entry
end
local color = info.Color or (info.Friendly and settings.FriendColor or settings.EnemyColor)
visuals.Chams(entry, info.Adornee or model, color, settings.Chams and (info.Adornee ~= false))
local top, onTop = cam:WorldToViewportPoint(root.Position + Vector3.new(0, 3, 0))
local bottom, onBottom = cam:WorldToViewportPoint(root.Position - Vector3.new(0, 3.5, 0))
if not (onTop and onBottom) then
visuals.HideEntry(entry)
if settings.Chams and entry.Highlight and info.Adornee ~= false then entry.Highlight.Enabled = true end
if settings.Arrows then visuals.Arrow(entry, cam:WorldToViewportPoint(root.Position), viewport, color) end
continue
end
entry.Arrow.Visible = false
local h = math.max(bottom.Y - top.Y, 4)
local w = h * 0.55
info.Distance = distance
visuals.Layout(entry, top.X - w / 2, top.Y, w, h, info, color, origin)
end
for model, entry in pairs(visuals.Entries) do
if not seen[model] then
if model.Parent then
visuals.HideEntry(entry)
else
visuals.DestroyEntry(entry)
visuals.Entries[model] = nil
end
end
end
end
function xDTaraZ.Visuals.ClearAll()
local visuals = xDTaraZ.Visuals
for model, entry in pairs(visuals.Entries) do
visuals.DestroyEntry(entry)
visuals.Entries[model] = nil
end
end
---Circular minimap centred on you, turned with the camera. Drag it to move it.
function xDTaraZ.Visuals.RadarBuild()
local visuals = xDTaraZ.Visuals
local radar = visuals.Radar
if radar.Frame and radar.Frame.Parent then return radar end
local new = visuals.New
local frame = new("Frame", { Name = "Radar", Position = UDim2.fromOffset(24, 180), BackgroundColor3 = Color3.fromRGB(16, 16, 22), BackgroundTransparency = 0.25, Active = true, Visible = false, Parent = visuals.EnsureGui() })
new("UICorner", { CornerRadius = UDim.new(0.5, 0), Parent = frame })
new("UIStroke", { Thickness = 2, Color = Color3.fromRGB(232, 160, 76), Transparency = 0.2, Parent = frame })
for _, horizontal in ipairs({ true, false }) do
new("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5),
Size = horizontal and UDim2.new(1, -12, 0, 1) or UDim2.new(0, 1, 1, -12),
BackgroundColor3 = Color3.new(1, 1, 1), BackgroundTransparency = 0.8, BorderSizePixel = 0, Parent = frame,
})
end
local ring = new("Frame", { AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5), Size = UDim2.fromScale(0.5, 0.5), BackgroundTransparency = 1, Parent = frame })
new("UICorner", { CornerRadius = UDim.new(0.5, 0), Parent = ring })
new("UIStroke", { Thickness = 1, Color = Color3.new(1, 1, 1), Transparency = 0.75, Parent = ring })
new("TextLabel", { AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5), Size = UDim2.fromOffset(16, 16), BackgroundTransparency = 1, Text = "▲", TextSize = 14, Font = Enum.Font.GothamBold, TextColor3 = Color3.new(1, 1, 1), ZIndex = 3, Parent = frame })
local dragStart, startPos
frame.InputBegan:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
dragStart, startPos = input.Position, frame.Position
local release
release = input.Changed:Connect(function()
if input.UserInputState == Enum.UserInputState.End then
dragStart = nil
release:Disconnect()
end
end)
end
end)
xDTaraZ:Connect(UserInputService.InputChanged, function(input)
if not dragStart or not frame.Parent then return end
if input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch then
local delta = input.Position - dragStart
frame.Position = startPos + UDim2.fromOffset(delta.X, delta.Y)
end
end)
radar.Frame = frame
return radar
end
function xDTaraZ.Visuals.RadarDot(index)
local radar = xDTaraZ.Visuals.Radar
local dot = radar.Dots[index]
if not dot then
dot = xDTaraZ.Visuals.New("Frame", { AnchorPoint = Vector2.new(0.5, 0.5), Size = UDim2.fromOffset(7, 7), BorderSizePixel = 0, ZIndex = 2, Parent = radar.Frame })
xDTaraZ.Visuals.New("UICorner", { CornerRadius = UDim.new(0.5, 0), Parent = dot })
radar.Dots[index] = dot
end
return dot
end
function xDTaraZ.Visuals.RadarRender()
local visuals = xDTaraZ.Visuals
local settings, radar = visuals.Settings, visuals.Radar
if not settings.Radar then
if radar.Frame then radar.Frame.Visible = false end
return
end
visuals.RadarBuild()
visuals.Scan()
local cam = Workspace.CurrentCamera
local char = LocalPlayer.Character
local root = char and char:FindFirstChild("HumanoidRootPart")
local size = settings.RadarSize
radar.Frame.Size = UDim2.fromOffset(size, size)
radar.Frame.Visible = cam ~= nil and root ~= nil
if not radar.Frame.Visible then return end
local look = cam.CFrame.LookVector * Vector3.new(1, 0, 1)
look = look.Magnitude > 0 and look.Unit or Vector3.new(0, 0, -1)
local right = Vector3.new(-look.Z, 0, look.X)
local radius = size / 2 - 6
local used = 0
for _, info in ipairs(visuals.Targets) do
local model = info.Model
local part = info.Root or (typeof(model) == "Instance" and (model:FindFirstChild("HumanoidRootPart") or model.PrimaryPart))
if not part or (settings.TeamCheck and info.Friendly) then continue end
local rel = part.Position - root.Position
local offset = Vector2.new(rel:Dot(right), -rel:Dot(look)) / settings.RadarRange * radius
local outside = offset.Magnitude > radius
if outside then offset = offset.Unit * radius end
used += 1
local dot = visuals.RadarDot(used)
dot.Position = UDim2.new(0.5, offset.X, 0.5, offset.Y)
dot.BackgroundColor3 = info.Color or (info.Friendly and settings.FriendColor or settings.EnemyColor)
dot.BackgroundTransparency = outside and 0.55 or 0
dot.Visible = true
end
for index = used + 1, #radar.Dots do radar.Dots[index].Visible = false end
end
function xDTaraZ.Visuals.Step()
local visuals = xDTaraZ.Visuals
local ok, err = pcall(visuals.Render)
if not ok then warn("[BloxStrike] esp:", err) end
ok, err = pcall(visuals.RadarRender)
if not ok then warn("[BloxStrike] radar:", err) end
end
function xDTaraZ.Visuals:Set(key, value)
self.Settings[key] = value
if key == "Enabled" and not value then xDTaraZ.Visuals.ClearAll() end
end
function xDTaraZ.Visuals:Get(key)
return self.Settings[key]
end
function xDTaraZ.Visuals.Destroy()
local visuals = xDTaraZ.Visuals
visuals.ClearAll()
if visuals.Gui then pcall(visuals.Gui.Destroy, visuals.Gui) end
visuals.Gui = nil
visuals.Radar = { Dots = {} }
end
xDTaraZ.UI = { Labels = {}, Widgets = {}, Binds = {}, Window = nil }
---Thai when the Roblox locale is Thai, else English (the old library's Language = "Auto").
xDTaraZ.UI.Thai = tostring(LocalPlayer.LocaleId):lower():sub(1, 2) == "th"
T = function(en, th) return xDTaraZ.UI.Thai and th or en end
function xDTaraZ.UI.Detach(fn)
return function(...)
local packed = table.pack(...)
task.defer(function()
local ok, err = pcall(fn, table.unpack(packed, 1, packed.n))
if not ok then warn("[BloxStrike] ui:", err) end
end)
end
end
---Runs `fn` every `interval` seconds until the script unloads.
function xDTaraZ.UI.Every(interval, fn)
task.spawn(function()
while xDTaraZ.State.Alive do
local ok, err = pcall(fn)
if not ok then warn("[BloxStrike] ui loop:", err) end
task.wait(interval)
end
end)
end
---@return boolean true while the menu covers the screen
function xDTaraZ.UI.MenuOpen()
local window = xDTaraZ.UI.Window
return window ~= nil and not window._destroyed and window.Open == true and not window.Minimized
end
local notifyIcons = { Info = "info", Coin = "coins" }
local notifyTypes = { Success = "Success", Warning = "Warning", Warn = "Warning", Error = "Error" }
function xDTaraZ.UI.Notify(title, content, duration, kind)
if not Library then return end
local ok, err = pcall(Library.Notify, Library, {
Title = tostring(title),
Content = tostring(content),
Duration = duration or 4,
Type = notifyTypes[kind],
Icon = notifyIcons[kind],
})
if not ok then warn("[BloxStrike] notify:", err) end
end
---One control the script reads and writes. AirFlow elements only take a single Callback, so this adds
---OnChanged listeners and guards (a guard returning false vetoes the value before any listener sees it).
function xDTaraZ.UI.Widget(idx, name)
local widget = { Idx = idx, Name = name or idx, Listeners = {}, Guards = {}, Value = nil, Element = nil }
function widget:OnChanged(fn) table.insert(self.Listeners, fn) end
function widget:AddGuard(fn) table.insert(self.Guards, fn) end
function widget:SetValue(value)
if self.Element then self.Element:Set(value) end
end
function widget:Dispatch(value)
for _, guard in ipairs(self.Guards) do
local ok, allowed = pcall(guard, value)
if ok and allowed == false then return end
end
self.Value = value
for _, fn in ipairs(self.Listeners) do
local ok, err = pcall(fn, value)
if not ok then warn("[BloxStrike] " .. idx .. ":", err) end
end
end
xDTaraZ.UI.Widgets[idx] = widget
return widget
end
---@param info table { Name, Desc, Default }
function xDTaraZ.UI.Toggle(group, idx, info)
local widget = xDTaraZ.UI.Widget(idx, info.Name)
widget.Element = group:CreateToggle({
Name = info.Name,
Desc = info.Desc,
CurrentValue = info.Default == true,
Flag = idx,
Callback = function(value) widget:Dispatch(value) end,
})
widget.Value = widget.Element:Get()
return widget
end
---@param info table { Name, Desc, Min, Max, Default, Increment, Suffix }
function xDTaraZ.UI.Slider(group, idx, info)
local widget = xDTaraZ.UI.Widget(idx, info.Name)
widget.Element = group:CreateSlider({
Name = info.Name,
Range = { info.Min, info.Max },
Increment = info.Increment or 1,
Suffix = info.Suffix,
CurrentValue = info.Default,
Tooltip = info.Desc,
Flag = idx,
Callback = function(value) widget:Dispatch(value) end,
})
widget.Value = widget.Element:Get()
return widget
end
---@param info table { Name, Desc, Options, Default, Searchable, NoSave }
function xDTaraZ.UI.Dropdown(group, idx, info)
local widget = xDTaraZ.UI.Widget(idx, info.Name)
widget.Element = group:CreateDropdown({
Name = info.Name,
Options = info.Options,
CurrentOption = info.Default,
AllowNone = false,
Searchable = info.Searchable == true,
Tooltip = info.Desc,
Flag = not info.NoSave and idx or nil,
Callback = function(value) widget:Dispatch(value) end,
})
widget.Value = widget.Element:Get()
return widget
end
function xDTaraZ.UI.Color(group, idx, info)
local widget = xDTaraZ.UI.Widget(idx, info.Name)
widget.Element = group:CreateColorPicker({
Name = info.Name,
Color = info.Default,
Flag = idx,
Callback = function(value) widget:Dispatch(value) end,
})
widget.Value = widget.Element:Get()
return widget
end
---Feeds a widget's current value and every change into `fn`.
function xDTaraZ.UI.Feed(widget, fn)
pcall(fn, widget.Value)
widget:OnChanged(fn)
end
---Keybind + key mode for a feature toggle. AirFlow keybinds only fire on press, so Toggle / Hold / Always
---(the old library's key picker modes) are handled here. Backspace or Delete clears the key.
function xDTaraZ.UI.KeyBind(group, toggleIdx, keyIdx, defaultKey, defaultMode)
local bind = { Toggle = toggleIdx, Mode = defaultMode or "Toggle" }
bind.Key = group:CreateKeybind({
Name = T("Keybind", "ปุ่มลัด"),
CurrentKeybind = defaultKey ~= "None" and defaultKey or nil,
Flag = keyIdx,
OnChanged = function(key)
if key == Enum.KeyCode.Backspace or key == Enum.KeyCode.Delete then
task.defer(function() bind.Key:Set(nil, true) end)
end
end,
})
local mode = xDTaraZ.UI.Dropdown(group, toggleIdx .. "Mode", { Name = T("Key mode", "โหมดปุ่ม"), Options = { "Toggle", "Hold", "Always" }, Default = bind.Mode })
mode:OnChanged(function(value) xDTaraZ.UI.SetKeyMode(bind, value) end)
table.insert(xDTaraZ.UI.Binds, bind)
return bind
end
function xDTaraZ.UI.SetKeyMode(bind, mode)
if mode ~= "Toggle" and mode ~= "Hold" and mode ~= "Always" then return end
bind.Mode = mode
local widget = xDTaraZ.UI.Widgets[bind.Toggle]
if not widget then return end
if mode == "Always" then
widget:SetValue(true)
elseif mode == "Hold" then
widget:SetValue(false)
end
end
---Feature keys work even when the game also uses the key; never while typing or while rebinding.
function xDTaraZ.UI.OnKey(input, down)
if input.UserInputType ~= Enum.UserInputType.Keyboard then return end
if down and UserInputService:GetFocusedTextBox() then return end
local window = xDTaraZ.UI.Window
local consumed = window ~= nil and window._consumedKey == input.KeyCode and osClock() - (window._consumedAt or 0) < 0.25
for _, bind in ipairs(xDTaraZ.UI.Binds) do
local ok, key = pcall(bind.Key.Get, bind.Key)
if not ok or key == nil or key ~= input.KeyCode or bind.Key.Listening then continue end
local widget = xDTaraZ.UI.Widgets[bind.Toggle]
if not widget then continue end
if bind.Mode == "Hold" then
if not (down and consumed) then widget:SetValue(down) end
elseif bind.Mode == "Toggle" and down and not consumed then
widget:SetValue(not widget.Value)
end
end
end
---Vetoes turning `idx` on and tells the player why.
local function Veto(idx, message)
local widget = xDTaraZ.UI.Widgets[idx]
if not widget then return end
widget:AddGuard(function(value)
if value ~= true then return true end
local text = message()
if not text then return true end
xDTaraZ.UI.Notify("XYZ", text, 4, "Warning")
task.defer(widget.SetValue, widget, false)
return false
end)
end
---Blocks a hook-only feature on executors that cannot run it.
function xDTaraZ.UI.NeedCap(idx, caps)
local widget = xDTaraZ.UI.Widgets[idx]
Veto(idx, function()
if xDTaraZ.Compat.Has(caps) then return nil end
local feature = widget and widget.Name or idx
return T(feature .. " is not supported on this executor", feature .. " ใช้กับ executor นี้ไม่ได้")
end)
end
---Blocks a feature for good (missing game module, unsupported executor).
function xDTaraZ.UI.Block(idx, reason)
local widget = xDTaraZ.UI.Widgets[idx]
if widget then widget.Blocked = reason end
Veto(idx, function()
return (widget and widget.Name or idx) .. ": " .. tostring(reason)
end)
end
function xDTaraZ.UI.Bind(widget, key)
local function Apply(value) xDTaraZ.Options[key] = value end
Apply(widget.Value)
widget:OnChanged(Apply)
end
local priorities = { "Crosshair", "Distance", "Health" }
function xDTaraZ.UI.BuildMain(window)
local tab = window:CreateTab({ Name = T("Main", "หลัก"), Icon = "star", Desc = T("Status and links", "สถานะและลิงก์") })
local status = tab:AddLeftGroupbox({ Name = T("Status", "สถานะ"), Icon = "activity" })
local rows = status:CreateStatuses({
{ Name = T("Map", "แมพ"), Value = "-" },
{ Name = T("Round", "รอบ"), Value = "-" },
}, { Style = "Row" })
local labels = xDTaraZ.UI.Labels
labels.Map, labels.Round = rows[T("Map", "แมพ")], rows[T("Round", "รอบ")]
labels.Spectators = status:CreateParagraph({ Title = T("Spectators", "คนดูเรา"), Content = "-" })
local discord = tab:AddRightGroupbox({ Name = T("Discord", "ดิสคอร์ด"), Icon = "link" })
discord:CreateLabel(xDTaraZ.Config.Discord)
discord:CreateButton({ Name = T("Copy Discord Link", "คัดลอกลิงก์ดิสคอร์ด"), Style = "Primary", Callback = xDTaraZ.UI.Detach(function()
if xDTaraZ.Util.Copy(xDTaraZ.Config.Discord) then
xDTaraZ.UI.Notify(T("Discord", "ดิสคอร์ด"), T("Link copied", "คัดลอกลิงก์แล้ว"), 3, "Success")
else
xDTaraZ.UI.Notify(T("Discord", "ดิสคอร์ด"), xDTaraZ.Config.Discord, 6, "Info")
end
end) })
end
function xDTaraZ.UI.BuildAimbot(tab)
local main = tab:AddLeftGroupbox({ Name = T("Aimbot", "เล็งอัตโนมัติ"), Icon = "crosshair" })
xDTaraZ.UI.Toggle(main, "Aimbot", { Name = T("Aimbot", "เล็งอัตโนมัติ"), Desc = T("Locks your view onto the enemy in the FOV", "ล็อคกล้องไปที่ศัตรูในวง FOV") })
xDTaraZ.UI.KeyBind(main, "Aimbot", "AimbotKey", "E", "Toggle")
xDTaraZ.UI.Slider(main, "AimbotSmooth", { Name = T("Smoothness", "ความนุ่ม"), Desc = T("1 = instant snap", "1 = หันทันที"), Min = 1, Max = 20, Default = 1 })
xDTaraZ.UI.Toggle(main, "AimbotSticky", { Name = T("Sticky target", "ล็อคเป้าเดิม"), Desc = T("Keeps the same enemy until it is lost", "ไม่สลับเป้าจนกว่าเป้าเดิมจะหลุด"), Default = true })
local target = tab:AddRightGroupbox({ Name = T("Aimbot Targeting", "การเลือกเป้า (เล็ง)"), Icon = "target" })
xDTaraZ.UI.Dropdown(target, "AimbotBone", { Name = T("Aim part", "จุดที่เล็ง"), Options = { "Head", "Torso" }, Default = "Head" })
xDTaraZ.UI.Dropdown(target, "AimbotPriority", { Name = T("Priority", "เลือกเป้าตาม"), Options = priorities, Default = "Crosshair" })
xDTaraZ.UI.Toggle(target, "AimbotVisible", { Name = T("Visible only", "เฉพาะที่มองเห็น"), Default = true })
xDTaraZ.UI.Slider(target, "AimbotFov", { Name = T("FOV", "ขนาดวง"), Min = 20, Max = 800, Default = 150, Suffix = "px" })
xDTaraZ.UI.Toggle(target, "AimbotShowFov", { Name = T("Show FOV circle", "แสดงวง FOV") })
xDTaraZ.UI.Slider(target, "AimbotMaxDistance", { Name = T("Max distance", "ระยะสูงสุด"), Min = 50, Max = 3000, Default = 1500, Suffix = "m" })
end
function xDTaraZ.UI.BuildSilent(tab)
local main = tab:AddLeftGroupbox({ Name = T("Silent Aim", "ไซเลนต์เอม"), Icon = "bomb" })
xDTaraZ.UI.Toggle(main, "SilentAim", { Name = T("Silent aim", "ไซเลนต์เอม"), Desc = T("Shots you fire go to the enemy in the FOV", "นัดที่ยิงพุ่งไปหาศัตรูในวง FOV") })
xDTaraZ.UI.KeyBind(main, "SilentAim", "SilentKey", "None", "Toggle")
xDTaraZ.UI.Slider(main, "SilentHitChance", { Name = T("Hit chance", "โอกาสโดน"), Desc = T("Lower looks more legit", "ยิ่งต่ำยิ่งดูเนียน"), Min = 1, Max = 100, Default = 100, Suffix = "%" })
xDTaraZ.UI.Slider(main, "SilentHeadChance", { Name = T("Headshot chance", "โอกาสเข้าหัว"), Desc = T("The rest go to the body", "ที่เหลือเข้าลำตัว"), Min = 0, Max = 100, Default = 100, Suffix = "%" })
xDTaraZ.UI.NeedCap("SilentAim", { "HookFunction", "Gc" })
local target = tab:AddRightGroupbox({ Name = T("Silent Targeting", "การเลือกเป้า (ไซเลนต์)"), Icon = "target" })
xDTaraZ.UI.Dropdown(target, "SilentPriority", { Name = T("Priority", "เลือกเป้าตาม"), Options = priorities, Default = "Crosshair" })
xDTaraZ.UI.Toggle(target, "SilentVisible", { Name = T("Visible only", "เฉพาะที่มองเห็น"), Desc = T("Off = also through walls", "ปิด = ยิงทะลุกำแพงด้วย"), Default = true })
xDTaraZ.UI.Slider(target, "SilentFov", { Name = T("FOV", "ขนาดวง"), Min = 20, Max = 1000, Default = 220, Suffix = "px" })
xDTaraZ.UI.Toggle(target, "SilentShowFov", { Name = T("Show FOV circle", "แสดงวง FOV") })
xDTaraZ.UI.Slider(target, "SilentMaxDistance", { Name = T("Max distance", "ระยะสูงสุด"), Min = 50, Max = 3000, Default = 2000, Suffix = "m" })
xDTaraZ.UI.Toggle(target, "SilentPredict", { Name = T("Lead moving targets", "ยิงดักคนที่วิ่ง") })
end
function xDTaraZ.UI.BuildTrigger(tab)
local trigger = tab:AddLeftGroupbox({ Name = T("Triggerbot", "ยิงอัตโนมัติ"), Icon = "zap" })
xDTaraZ.UI.Toggle(trigger, "Triggerbot", { Name = T("Triggerbot", "ยิงอัตโนมัติ"), Desc = T("Fires the moment an enemy is under your crosshair", "ยิงทันทีที่ศัตรูอยู่ใต้เป้า") })
xDTaraZ.UI.KeyBind(trigger, "Triggerbot", "TriggerKey", "T", "Toggle")
xDTaraZ.UI.Slider(trigger, "TriggerDelay", { Name = T("Reaction delay", "ดีเลย์ก่อนยิง"), Min = 0, Max = 400, Default = 0, Suffix = "ms" })
xDTaraZ.UI.Slider(trigger, "TriggerHitChance", { Name = T("Fire chance", "โอกาสยิง"), Min = 1, Max = 100, Default = 100, Suffix = "%" })
local rage = tab:AddRightGroupbox({ Name = T("Ragebot", "เรจบอท"), Icon = "bomb" })
xDTaraZ.UI.Toggle(rage, "Ragebot", { Name = T("Ragebot", "เรจบอท"), Desc = T("Snaps to and kills any enemy it can reach on its own", "หันไปยิงศัตรูที่ยิงถึงเองทันที") })
xDTaraZ.UI.KeyBind(rage, "Ragebot", "RageKey", "None", "Toggle")
xDTaraZ.UI.Toggle(rage, "RageVisible", { Name = T("Visible only", "เฉพาะที่มองเห็น"), Desc = T("Off = also through walls", "ปิด = ยิงทะลุกำแพงด้วย"), Default = true })
xDTaraZ.UI.Slider(rage, "RageMaxDistance", { Name = T("Max distance", "ระยะสูงสุด"), Min = 50, Max = 3000, Default = 2000, Suffix = "m" })
end
function xDTaraZ.UI.BuildGuns(tab)
local mods = tab:AddLeftGroupbox({ Name = T("Gun Mods", "ม็อดปืน"), Icon = "swords" })
xDTaraZ.UI.Toggle(mods, "NoRecoil", { Name = T("No recoil", "ไม่มีแรงถีบ"), Desc = T("Your view stays still while spraying", "จอนิ่งตอนกดยิงค้าง") })
xDTaraZ.UI.Toggle(mods, "NoSpread", { Name = T("No spread", "ไม่มีการกระจาย"), Desc = T("Every bullet lands on the crosshair", "ทุกนัดลงกลางเป้า") })
xDTaraZ.UI.NeedCap("NoRecoil", "HookFunction")
xDTaraZ.UI.NeedCap("NoSpread", { "HookFunction", "Gc" })
end
function xDTaraZ.UI.BuildCombat(window)
local tab = window:CreateTab({ Name = T("Combat", "การต่อสู้"), Icon = "crosshair", Desc = T("Aim, triggers and gun mods", "เล็ง ยิงอัตโนมัติ และม็อดปืน") })
local pages = {
{ T("Aimbot", "เล็งอัตโนมัติ"), xDTaraZ.UI.BuildAimbot },
{ T("Silent Aim", "ไซเลนต์เอม"), xDTaraZ.UI.BuildSilent },
{ T("Trigger & Rage", "ยิงออโต้ & เรจ"), xDTaraZ.UI.BuildTrigger },
{ T("Gun Mods", "ม็อดปืน"), xDTaraZ.UI.BuildGuns },
}
for _, page in ipairs(pages) do
xDTaraZ.Util.Try("build " .. page[1], page[2], tab:CreateSubTab({ Name = page[1] }))
end
end
function xDTaraZ.UI.BuildEsp(tab)
local function Setting(key)
return function(value) xDTaraZ.Visuals:Set(key, value) end
end
local feed = xDTaraZ.UI.Feed
local main = tab:AddLeftGroupbox({ Name = "ESP", Icon = "eye" })
feed(xDTaraZ.UI.Toggle(main, "Esp", { Name = T("Enable ESP", "เปิด ESP") }), Setting("Enabled"))
feed(xDTaraZ.UI.Toggle(main, "EspTeam", { Name = T("Team check", "เช็คทีม"), Default = true }), Setting("TeamCheck"))
local colors = tab:AddLeftGroupbox({ Name = T("Colors", "สี"), Icon = "palette" })
feed(xDTaraZ.UI.Color(colors, "EspEnemyColor", { Name = T("Enemy", "ศัตรู"), Default = xDTaraZ.Visuals.Settings.EnemyColor }), Setting("EnemyColor"))
feed(xDTaraZ.UI.Color(colors, "EspFriendColor", { Name = T("Friendly", "พวกเดียวกัน"), Default = xDTaraZ.Visuals.Settings.FriendColor }), Setting("FriendColor"))
local radar = tab:AddLeftGroupbox({ Name = T("Radar", "เรดาร์"), Icon = "compass" })
feed(xDTaraZ.UI.Toggle(radar, "Radar", { Name = T("Radar", "เรดาร์"), Desc = T("Minimap of nearby players, drag to move", "มินิแมพผู้เล่นรอบตัว ลากย้ายได้") }), Setting("Radar"))
feed(xDTaraZ.UI.Slider(radar, "RadarRange", { Name = T("Radar range", "ระยะเรดาร์"), Min = 50, Max = 1000, Default = 250, Increment = 10, Suffix = "m" }), Setting("RadarRange"))
feed(xDTaraZ.UI.Slider(radar, "RadarSize", { Name = T("Radar size", "ขนาดเรดาร์"), Min = 120, Max = 320, Default = 180, Suffix = "px" }), Setting("RadarSize"))
local look = tab:AddRightGroupbox({ Name = T("Elements", "องค์ประกอบ"), Icon = "box" })
feed(xDTaraZ.UI.Toggle(look, "EspBox", { Name = T("Box", "กล่อง"), Default = true }), Setting("Box"))
feed(xDTaraZ.UI.Dropdown(look, "EspBoxStyle", { Name = T("Box style", "แบบกล่อง"), Options = { "Full", "Corner" }, Default = "Full" }), Setting("BoxStyle"))
feed(xDTaraZ.UI.Toggle(look, "EspFill", { Name = T("Box fill", "ถมสีกล่อง") }), Setting("BoxFill"))
feed(xDTaraZ.UI.Toggle(look, "EspName", { Name = T("Name", "ชื่อ"), Default = true }), Setting("Name"))
feed(xDTaraZ.UI.Toggle(look, "EspHealth", { Name = T("Health bar", "แถบเลือด"), Default = true }), Setting("Health"))
feed(xDTaraZ.UI.Toggle(look, "EspHealthText", { Name = T("Health number", "ตัวเลขเลือด") }), Setting("HealthText"))
feed(xDTaraZ.UI.Toggle(look, "EspDistance", { Name = T("Distance", "ระยะ"), Default = true }), Setting("Distance"))
feed(xDTaraZ.UI.Toggle(look, "EspDot", { Name = T("Head dot", "จุดหัว") }), Setting("HeadDot"))
feed(xDTaraZ.UI.Toggle(look, "EspTracer", { Name = T("Tracers", "เส้นนำสายตา") }), Setting("Tracer"))
feed(xDTaraZ.UI.Dropdown(look, "EspTracerFrom", { Name = T("Tracer from", "เส้นเริ่มจาก"), Options = { "Bottom", "Center", "Mouse" }, Default = "Bottom" }), Setting("TracerOrigin"))
feed(xDTaraZ.UI.Toggle(look, "EspChams", { Name = T("Chams", "เรืองแสงทะลุกำแพง") }), Setting("Chams"))
feed(xDTaraZ.UI.Toggle(look, "EspArrows", { Name = T("Off-screen arrows", "ลูกศรนอกจอ") }), Setting("Arrows"))
end
function xDTaraZ.UI.BuildWorld(tab)
local world = tab:AddLeftGroupbox({ Name = T("World", "โลก"), Icon = "sun" })
xDTaraZ.UI.Toggle(world, "Fullbright", { Name = T("Fullbright", "สว่างทั้งแมพ") })
xDTaraZ.UI.Toggle(world, "NoFlash", { Name = T("No flash", "กันแฟลช"), Desc = T("Flashbangs do not blind you", "แฟลชไม่ทำให้ตาบอด") })
xDTaraZ.UI.Toggle(world, "NoSmoke", { Name = T("No smoke", "ไม่มีควัน"), Desc = T("See through smoke grenades", "มองทะลุควัน") })
local heard = tab:AddLeftGroupbox({ Name = T("Hidden Enemies", "ศัตรูหลังกำแพง"), Icon = "ear" })
xDTaraZ.UI.Toggle(heard, "SoundEsp", { Name = T("Sound ESP", "ESP จากเสียง"), Desc = T("Shows where enemies walk and shoot, even behind walls", "โชว์จุดที่ศัตรูเดินและยิง แม้อยู่หลังกำแพง") })
xDTaraZ.UI.Slider(heard, "SoundEspLife", { Name = T("Show for", "แสดงนาน"), Min = 1, Max = 8, Default = 3, Suffix = "s" })
xDTaraZ.UI.Slider(heard, "EspMemory", { Name = T("Keep hidden enemies", "จำศัตรูที่หลบ"), Desc = T("ESP keeps a box where an enemy was last seen or heard", "ESP ค้างกล่องไว้ที่ที่เห็นหรือได้ยินศัตรูล่าสุด"), Min = 0, Max = 15, Default = 5, Suffix = "s" })
xDTaraZ.UI.NeedCap("SoundEsp", "Upvalues")
local cam = tab:AddRightGroupbox({ Name = T("Camera", "กล้อง"), Icon = "camera" })
xDTaraZ.UI.Toggle(cam, "CameraFov", { Name = T("Custom FOV", "ปรับมุมมอง") })
xDTaraZ.UI.Slider(cam, "CameraFovValue", { Name = T("FOV", "มุมมอง"), Min = 70, Max = 120, Default = 100 })
end
function xDTaraZ.UI.BuildVisuals(window)
local tab = window:CreateTab({ Name = T("Visuals", "การมองเห็น"), Icon = "eye", Desc = T("ESP, world and camera", "ESP โลก และกล้อง") })
xDTaraZ.Util.Try("build esp", xDTaraZ.UI.BuildEsp, tab:CreateSubTab({ Name = "ESP" }))
xDTaraZ.Util.Try("build world", xDTaraZ.UI.BuildWorld, tab:CreateSubTab({ Name = T("World", "โลก") }))
end
local skinTitles = {
Pistol = { "Pistols", "ปืนพก", "coins" },
SMG = { "SMGs", "ปืนกลมือ", "zap" },
Rifle = { "Rifles", "ปืนไรเฟิล", "crosshair" },
Sniper = { "Snipers", "สไนเปอร์", "target" },
Heavy = { "Heavy", "ปืนหนัก", "bomb" },
Shotgun = { "Shotguns", "ลูกซอง", "bomb" },
["Machine Gun"] = { "Machine Guns", "ปืนกล", "zap" },
Knives = { "Knives", "มีด", "swords" },
Gloves = { "Gloves", "ถุงมือ", "shield" },
Grenades = { "Grenades", "ระเบิด", "bomb" },
Gear = { "Gear", "อุปกรณ์", "settings" },
}
---Skin pickers save to their own skins.json, so they carry no config flag.
function xDTaraZ.UI.SkinGroup(tab, category, side)
local title = skinTitles[category] or { category, category, "star" }
local group = tab:AddGroupbox({ Name = T(title[1], title[2]), Icon = title[3], Side = side })
for _, weapon in ipairs(xDTaraZ.Skins.Items(category)) do
if weapon == "-" then continue end
local values = { "Default" }
for _, skin in ipairs(xDTaraZ.Skins.List(weapon)) do values[#values + 1] = skin end
local widget = xDTaraZ.UI.Dropdown(group, "Skin_" .. weapon, { Name = weapon, Options = values, Default = xDTaraZ.Skins.Map[weapon] or "Default", Searchable = #values > 8, NoSave = true })
widget:OnChanged(function(skin)
if skin then xDTaraZ.Skins.Set(weapon, skin) end
end)
end
end
---@param heights table rows used per side so far, updated in place
function xDTaraZ.UI.PlaceSkinGroup(tab, category, heights)
local side = heights.Left <= heights.Right and "Left" or "Right"
local rows = 1
for _, weapon in ipairs(xDTaraZ.Skins.Items(category)) do
if weapon ~= "-" then rows += 1 end
end
if rows == 1 then return end
heights[side] += rows
xDTaraZ.Util.Try("skins " .. category, xDTaraZ.UI.SkinGroup, tab, category, side)
end
---Shows the saved skin in every picker without setting it again.
function xDTaraZ.UI.SyncSkins()
for _, category in ipairs(xDTaraZ.Skins.Categories()) do
for _, weapon in ipairs(xDTaraZ.Skins.Items(category)) do
local widget = xDTaraZ.UI.Widgets["Skin_" .. weapon]
if widget and widget.Element then
local skin = xDTaraZ.Skins.Map[weapon] or "Default"
pcall(widget.Element.Set, widget.Element, skin, true)
widget.Value = skin
end
end
end
end
function xDTaraZ.UI.BuildSkins(window)
local tab = window:CreateTab({ Name = T("Skins", "สกิน"), Icon = "sparkles", Desc = T("Any skin, only on your screen", "ใส่สกินไหนก็ได้ เห็นแค่บนจอคุณ") })
local gearPage = tab:CreateSubTab({ Name = T("Knives & Gear", "มีดและอุปกรณ์") })
local main = gearPage:AddLeftGroupbox({ Name = T("Skin Changer", "เปลี่ยนสกิน"), Icon = "sparkles" })
xDTaraZ.UI.Toggle(main, "SkinChanger", { Name = T("Skin changer", "เปลี่ยนสกิน"), Desc = T("Pick a skin below and it shows right away", "เลือกสกินด้านล่างแล้วขึ้นทันที") })
main:CreateButton({ Name = T("Rarest On Everything", "หายากสุดทุกอัน"), Style = "Primary", Callback = xDTaraZ.UI.Detach(function()
local count = xDTaraZ.Skins.SetAll(function(list) return list[1] end)
xDTaraZ.UI.SyncSkins()
xDTaraZ.UI.Notify("Skins", "Set " .. count .. " items", 3, "Success")
end) })
main:CreateButtonRow({
{ Name = T("Random", "สุ่ม"), Callback = xDTaraZ.UI.Detach(function()
xDTaraZ.Skins.SetAll(function(list) return list[math.random(#list)] end)
xDTaraZ.UI.SyncSkins()
end) },
{ Name = T("Reset All", "ล้างทั้งหมด"), Style = "Danger", Callback = xDTaraZ.UI.Detach(function()
table.clear(xDTaraZ.Skins.Map)
xDTaraZ.Skins.Save()
xDTaraZ.UI.SyncSkins()
end) },
})
local gear = { Knives = true, Gloves = true, Grenades = true, Gear = true }
local heights = { Left = xDTaraZ.Config.SkinMainRows, Right = 0 }
for _, category in ipairs({ "Knives", "Gloves", "Grenades", "Gear" }) do
xDTaraZ.UI.PlaceSkinGroup(gearPage, category, heights)
end
local guns = tab:CreateSubTab({ Name = T("Gun Skins", "สกินปืน") })
heights = { Left = 0, Right = 0 }
local ok, categories = pcall(xDTaraZ.Skins.Categories)
for _, category in ipairs(ok and categories or {}) do
if gear[category] then continue end
xDTaraZ.UI.PlaceSkinGroup(guns, category, heights)
end
end
function xDTaraZ.UI.BuildMisc(window)
local tab = window:CreateTab({ Name = T("Misc", "อื่นๆ"), Icon = "wrench", Desc = T("Buying and utility", "ซื้อของ และอื่นๆ") })
local buy = tab:AddLeftGroupbox({ Name = T("Auto Buy", "ซื้ออัตโนมัติ"), Icon = "shopping-cart" })
xDTaraZ.UI.Toggle(buy, "AutoRebuy", { Name = T("Auto rebuy", "ซื้อซ้ำอัตโนมัติ"), Desc = T("Buys your last loadout every round", "ซื้อชุดล่าสุดของคุณให้ทุกรอบ") })
xDTaraZ.UI.NeedCap("AutoRebuy", "HookFunction")
xDTaraZ.UI.Labels.Bought = buy:CreateParagraph({ Title = T("Saved loadout", "ชุดที่จำไว้"), Content = "-" })
buy:CreateButtonRow({
{ Name = T("Rebuy Now", "ซื้อซ้ำตอนนี้"), Style = "Primary", Callback = xDTaraZ.UI.Detach(function()
local sent = xDTaraZ.Economy.Rebuy()
xDTaraZ.UI.Notify("Auto Buy", sent > 0 and ("Bought " .. sent .. " items") or "Buy something once first", 3, sent > 0 and "Success" or "Info")
end) },
{ Name = T("Clear", "ล้าง"), Callback = xDTaraZ.UI.Detach(function()
table.clear(xDTaraZ.State.Bought)
end) },
})
local util = tab:AddRightGroupbox({ Name = T("Utility", "อรรถประโยชน์"), Icon = "settings-2" })
xDTaraZ.UI.Toggle(util, "AntiAfk", { Name = T("Anti AFK", "กันหลุด AFK") })
util:CreateButtonRow({
{ Name = T("Rejoin", "เข้าใหม่"), Callback = xDTaraZ.UI.Detach(xDTaraZ.World.Rejoin) },
{ Name = T("Server Hop", "ย้ายเซิร์ฟ"), Callback = xDTaraZ.UI.Detach(xDTaraZ.World.ServerHop) },
})
end
function xDTaraZ.UI.BuildSettings(window)
local tab = window:CreateTab({ Name = T("Settings", "ตั้งค่า"), Icon = "settings" })
local interface = tab:AddLeftGroupbox({ Name = T("Interface", "หน้าตา"), Icon = "monitor" })
interface:CreateKeybind({
Name = T("Menu key", "ปุ่มเปิดเมนู"),
CurrentKeybind = "RightControl",
Flag = "MenuKey",
OnChanged = function(key)
if key then window:SetKeybind(key) end
end,
})
interface:CreateToggle({
Name = T("Save Key for next load", "จำคีย์สำหรับครั้งถัดไป"),
CurrentValue = Shared.SaveKey ~= false,
Callback = function(value)
Shared.SaveKey = value
Shared.Save()
if value then Shared.WriteKey() else Shared.ClearKey() end
end,
})
tab:CreateConfigManager({ Name = T("Configs", "คอนฟิก"), Side = "Left" })
tab:CreateThemeManager({ Name = "Theme", Side = "Right" })
end
---Saves every setting (toggles, sliders, keybinds, colours) in the background; no button needed.
function xDTaraZ.UI.SaveNow(window)
window = window or xDTaraZ.UI.Window
if not window or window._destroyed then return end
local previous = window.ConfigName
pcall(window.SaveConfig, window, "AutoSave")
window.ConfigName = previous
end
function xDTaraZ.UI.StartAutoSave(window)
task.spawn(function()
while xDTaraZ.State.Alive and not window._destroyed do
task.wait(2)
if xDTaraZ.State.Alive and not window._destroyed then
xDTaraZ.UI.SaveNow(window)
end
end
end)
end
function xDTaraZ.UI.RefreshStatus()
local labels, state = xDTaraZ.UI.Labels, xDTaraZ.State
if labels.Map then labels.Map:Set(tostring(Workspace:GetAttribute("Map") or "-")) end
if labels.Round then labels.Round:Set(tostring(Workspace:GetAttribute("GameState") or "-")) end
if labels.Money then labels.Money:Set("$" .. tostring(LocalPlayer:GetAttribute("Money") or 0)) end
if labels.Target then
local target = state.RageTarget or state.AimTarget or state.SilentTarget
labels.Target:Set(target and target.Name or "none")
end
if labels.Kills then labels.Kills:Set(tostring(LocalPlayer:GetAttribute("Kills") or 0)) end
if labels.Shots then labels.Shots:Set(("%d / %d"):format(state.Shots, state.Redirected)) end
if labels.Spectators then labels.Spectators:Set(xDTaraZ.Spectators.Text()) end
if labels.Esp then
labels.Esp:Set(xDTaraZ.Visuals:Get("Enabled") and (xDTaraZ.Esp.Count .. " players") or "Off")
end
if labels.Bought then labels.Bought:Set(xDTaraZ.Economy.BoughtText()) end
end
---Turns off what the scheduler halted and shows queued notices; runs on the UI loop, never on a game thread.
function xDTaraZ.UI.Drain()
local state = xDTaraZ.State
if not (state.Halted[1] or state.Notices[1]) then return end
local stopped, notices = table.clone(state.Halted), table.clone(state.Notices)
table.clear(state.Halted)
table.clear(state.Notices)
for _, halt in ipairs(stopped) do
for _, key in ipairs(halt[3]) do
local widget = xDTaraZ.UI.Widgets[key]
if widget and widget.Value == true then xDTaraZ.Util.Try("halt " .. key, widget.SetValue, widget, false) end
end
xDTaraZ.UI.Notify("XYZ", halt[1] .. " stopped: " .. halt[2], 6, "Error")
end
for _, notice in ipairs(notices) do
xDTaraZ.UI.Notify(notice[1], notice[2], 5, "Warning")
end
end
---Hooks go in the first time a feature that needs them is switched on, never at load.
function xDTaraZ.UI.HookOnDemand()
local installers = {
SilentAim = xDTaraZ.Silent.InstallHook,
Ragebot = xDTaraZ.Silent.InstallHook,
Triggerbot = xDTaraZ.Silent.InstallHook,
NoRecoil = xDTaraZ.Guns.HookKick,
CameraFov = xDTaraZ.Guns.BindFov,
NoSpread = xDTaraZ.Guns.HookSpread,
NoSmoke = xDTaraZ.World.HookEffects,
NoFlash = xDTaraZ.World.HookEffects,
AutoRebuy = xDTaraZ.Economy.HookBuys,
SoundEsp = xDTaraZ.Sound.Install,
}
for idx, install in pairs(installers) do
local widget = xDTaraZ.UI.Widgets[idx]
if not widget then continue end
widget:OnChanged(function(on)
if on then task.defer(xDTaraZ.Util.Try, "hook " .. idx, install) end
end)
if widget.Value == true then task.defer(xDTaraZ.Util.Try, "hook " .. idx, install) end
end
local trigger = xDTaraZ.UI.Widgets.Triggerbot
if trigger then
trigger:OnChanged(function(on)
if on then task.delay(xDTaraZ.Config.HookSettle, xDTaraZ.UI.WarnReducedTrigger) end
end)
end
end
---Triggerbot fires without the shoot hook; only shot pacing is less exact, so say so once.
function xDTaraZ.UI.WarnReducedTrigger()
local state = xDTaraZ.State
if state.Hooks.Shoot or state.TriggerWarned then return end
state.TriggerWarned = true
table.insert(state.Notices, { T("Triggerbot", "ยิงอัตโนมัติ"), T("Running in reduced mode on this executor; shot pacing is less exact", "ทำงานแบบจำกัดบน executor นี้ จังหวะยิงอาจไม่แม่นเท่าที่ควร") })
end
function xDTaraZ.UI.BlockMissing()
local absent = xDTaraZ.GameLib.Missing
local missing = {
Net = xDTaraZ.GameLib.Net == nil and (absent.Remotes or true),
Character = xDTaraZ.GameLib.Character == nil and (absent.CharacterController or true),
Skins = xDTaraZ.Skins.Folder() == nil and "Absent",
}
if missing.Skins then warn("[BloxStrike] Assets.Skins not found, the skin changer is blocked") end
local unsupported = T("Not available on this executor", "ใช้กับ executor นี้ไม่ได้")
local outdated = T("Changed by a game update, wait for a script update", "เกมอัปเดตแล้ว รอสคริปต์อัปเดต")
for source, features in pairs(xDTaraZ.Config.ModuleFeatures) do
local why = missing[source]
if not why then continue end
for _, idx in ipairs(features) do
xDTaraZ.UI.Block(idx, why == "Absent" and outdated or unsupported)
end
end
end
function xDTaraZ.UI.Build(window)
xDTaraZ.Util.Try("skins load", xDTaraZ.Skins.Load)
for _, build in ipairs({ xDTaraZ.UI.BuildMain, xDTaraZ.UI.BuildCombat, xDTaraZ.UI.BuildVisuals, xDTaraZ.UI.BuildSkins, xDTaraZ.UI.BuildMisc }) do
xDTaraZ.Util.Try("build", build, window)
end
xDTaraZ.Util.Try("settings tab", xDTaraZ.UI.BuildSettings, window)
for key in pairs(xDTaraZ.Options) do
local widget = xDTaraZ.UI.Widgets[key]
if widget then xDTaraZ.Util.Try("bind " .. key, xDTaraZ.UI.Bind, widget, key) end
end
xDTaraZ.Util.Try("hooks", xDTaraZ.UI.HookOnDemand)
xDTaraZ.Util.Try("missing modules", xDTaraZ.UI.BlockMissing)
xDTaraZ:Connect(UserInputService.InputBegan, function(input) xDTaraZ.UI.OnKey(input, true) end)
xDTaraZ:Connect(UserInputService.InputEnded, function(input) xDTaraZ.UI.OnKey(input, false) end)
xDTaraZ.UI.Every(xDTaraZ.Config.StatusInterval, xDTaraZ.UI.RefreshStatus)
xDTaraZ.UI.Every(xDTaraZ.Config.StatusInterval, xDTaraZ.UI.Drain)
end
function xDTaraZ.UI.Unload()
xDTaraZ:Unload()
local window = xDTaraZ.UI.Window
if window and not window._destroyed then pcall(window.Destroy, window) end
end
function xDTaraZ.Boot()
xDTaraZ.Util.Try("skins load", xDTaraZ.Skins.Load)
xDTaraZ.Util.Try("combat", xDTaraZ.Combat.Start)
xDTaraZ:Connect(RunService.RenderStepped, xDTaraZ.Visuals.Step)
xDTaraZ:Connect(LocalPlayer.Idled, xDTaraZ.World.OnIdled)
xDTaraZ:Connect(UserInputService.JumpRequest, xDTaraZ.World.OnJump)
xDTaraZ:Connect(RunService.Heartbeat, xDTaraZ.World.HopStep)
xDTaraZ:Connect(UserInputService.InputBegan, function(input, processed) xDTaraZ.World.OnSpace(input, true, processed) end)
xDTaraZ:Connect(UserInputService.InputEnded, function(input) xDTaraZ.World.OnSpace(input, false) end)
xDTaraZ.Scheduler.Every("World", 0.2, xDTaraZ.World.Step, { "Fullbright" }, xDTaraZ.World.Step)
xDTaraZ.Scheduler.Every("Auto Rebuy", 0.5, xDTaraZ.Economy.Tick, { "AutoRebuy" })
xDTaraZ.Scheduler.Every("Sound ESP", 0.1, xDTaraZ.Sound.Step, { "SoundEsp" }, xDTaraZ.Sound.Step)
xDTaraZ.Scheduler.Every("Skin Changer", 0.15, xDTaraZ.Skins.Step, { "SkinChanger" }, xDTaraZ.Skins.Step)
xDTaraZ.Util.Try("scheduler", xDTaraZ.Scheduler.Boot)
end
function xDTaraZ:Unload()
if not self.State.Alive then return end
self.State.Alive = false
pcall(xDTaraZ.Sound.Uninstall)
pcall(xDTaraZ.Visuals.Destroy)
if xDTaraZ.Esp.Folder then pcall(function() xDTaraZ.Esp.Folder:Destroy() end) end
xDTaraZ.Combat.Unload()
for _, key in ipairs({ "NoRecoil", "NoSpread", "Fullbright", "CameraFov", "BunnyHop", "SkinChanger" }) do
xDTaraZ.Options[key] = false
end
pcall(xDTaraZ.World.Step)
pcall(xDTaraZ.World.SetCrouch, false)
xDTaraZ.Guns.UnbindFov()
pcall(xDTaraZ.Skins.Step)
for _, conn in ipairs(self.State.Connections) do pcall(function() conn:Disconnect() end) end
table.clear(self.State.Connections)
xDTaraZ.Util.UnhookAll()
environment.XYZUnload = nil
environment.BloxStrikeUnload = nil
end
---@return boolean false when the menu could not be opened
local function BuildInterface()
Library = xDTaraZ.Util.LoadLibrary()
if not Library then return false end
pcall(XYZBanner.Step, "UI library")
xDTaraZ.Library = Library
local opened, window = pcall(Library.CreateWindow, Library, {
Name = "XYZ",
Title = "XYZ Scripts",
LoadingTitle = "XYZ Scripts",
LoadingSubtitle = "BloxStrike",
Subtitle = "BloxStrike",
Loading = false, -- no little loading window after the key
OnUnload = function()
xDTaraZ.UI.SaveNow() -- keep the last state for the next start
xDTaraZ.UI.Unload()
local play = environment.XYZ_PLAY_UNLOAD
if type(play) == "function" then task.spawn(pcall, play) end
end,
ToggleUIKeybind = "RightControl",
Theme = "XYZ",
DefaultTheme = "XYZ",
Weather = "Matrix",
ConfigurationSaving = { Enabled = true, FolderName = xDTaraZ.Config.SaveFolder .. "/Configs" },
ToggleButton = { Platform = "Mobile", Icon = "layout-grid" },
Home = {
Discord = xDTaraZ.Config.Discord:gsub("^https?://", ""),
Stats = { "Players", "Session", "FPS", "Ping" },
Tier = XYZ_TIER, -- "Free" or "Premium" (from the key)
Features = false, -- Feature list ausgeblendet
},
})
if not opened or type(window) ~= "table" then
xDTaraZ.Util.Alert("The menu failed to load on this executor: " .. tostring(window):match("^[^\n]*"), window)
return false
end
xDTaraZ.UI.Window = window
-- The window can also close itself (Exit on the executor warning); unload the features with it.
if typeof(window.Gui) == "Instance" then
window.Gui.Destroying:Connect(function()
if xDTaraZ.State.Alive then task.spawn(xDTaraZ.Unload, xDTaraZ) end
end)
end
xDTaraZ.UI.Build(window)
task.defer(xDTaraZ.Util.Try, "boot", xDTaraZ.Boot)
task.defer(xDTaraZ.Util.Try, "autoload config", function()
-- Last used settings come back automatically; a manually set autoload is only the fallback.
local loaded = false
if window:ConfigExists("AutoSave") then
loaded = window:LoadConfig("AutoSave") == true
end
if not loaded then window:LoadAutoload() end
xDTaraZ.UI.StartAutoSave(window)
end)
return true
end
environment.XYZUnload = function()
xDTaraZ.UI.Unload()
end
environment.BloxStrikeUnload = environment.XYZUnload
pcall(XYZBanner.Step, "Systems")
if not BuildInterface() then return end
pcall(XYZBanner.Step, "Interface")
pcall(XYZBanner.Ready)
end
do
local env = (getgenv and getgenv()) or _G
env.XYZ_ON_SUCCESS = RunBloxStrike -- Das Key-System ruft das nach erfolgreichem Key-Check auf
end
task.spawn(function()
task.wait(1)
RunBloxStrike({
tier = "PERMANENT",
valid = true,
remainingHours = "Lifetime"
})
end)