From 67c04b5acc3f55ab9f447025df2e962368436711 Mon Sep 17 00:00:00 2001 From: tul <27+tul@noreply.localhost> Date: Mon, 3 Aug 2026 21:19:08 +0000 Subject: [PATCH] mobile support + api --- UI/library.lua | 301 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 231 insertions(+), 70 deletions(-) diff --git a/UI/library.lua b/UI/library.lua index 8578417..63f0e44 100644 --- a/UI/library.lua +++ b/UI/library.lua @@ -70,14 +70,20 @@ flags = {}, config_flags = {}, - connections = {}, + connections = {}, notifications = {}, playerlist_data = { players = {}, - player = {}, + player = {}, }, - colorpicker_open = false; - gui; + + labels = {}, + label_gradients = {}, + + is_mobile = uis.TouchEnabled and not uis.KeyboardEnabled, + + colorpicker_open = false; + gui; } local themes = { @@ -180,11 +186,9 @@ library.__index = library - -- configs live in their own folder per game, so each game keeps its own saves library.config_folder = library.directory .. "/configs/" .. library.game_id library.folders[#library.folders + 1] = "/configs/" .. library.game_id - -- ipairs, not next: the parent folder has to exist before the child for _, path in ipairs(library.folders) do local folder = library.directory .. path if not isfolder(folder) then @@ -348,7 +352,8 @@ local start frame.InputBegan:Connect(function(input) - if input.UserInputType == Enum.UserInputType.MouseButton1 then + if input.UserInputType == Enum.UserInputType.MouseButton1 + or input.UserInputType == Enum.UserInputType.Touch then dragging = true start = input.Position start_size = frame.Position @@ -356,13 +361,15 @@ end) frame.InputEnded:Connect(function(input) - if input.UserInputType == Enum.UserInputType.MouseButton1 then + if input.UserInputType == Enum.UserInputType.MouseButton1 + or input.UserInputType == Enum.UserInputType.Touch then dragging = false end end) - library:connection(uis.InputChanged, function(input, game_event) - if dragging and input.UserInputType == Enum.UserInputType.MouseMovement then + library:connection(uis.InputChanged, function(input, game_event) + if dragging and (input.UserInputType == Enum.UserInputType.MouseMovement + or input.UserInputType == Enum.UserInputType.Touch) then local viewport_x = camera.ViewportSize.X local viewport_y = camera.ViewportSize.Y @@ -572,6 +579,23 @@ themes.preset[theme] = color end + -- repaints the window gradient plus every floating label made by library.label + function library:refresh_gradients() + local seq = rgbseq{ + rgbkey(0, themes.preset["1"]), + rgbkey(0.5, themes.preset["2"]), + rgbkey(1, themes.preset["3"]), + } + + if library.gradient then + library.gradient.Color = seq + end + + for _, gradient in next, library.label_gradients do + gradient.Color = seq + end + end + function library:connection(signal, callback) local connection = signal:Connect(callback) @@ -708,12 +732,15 @@ }); -- - function cfg.toggle_menu(bool) - window_outline.Visible = bool + function cfg.toggle_menu(bool) + window_outline.Visible = bool end + -- the mobile toggle needs a handle on the menu it is toggling + library.main_window = cfg + return setmetatable(cfg, library) - end + end function library:tab(properties) local cfg = { @@ -913,22 +940,32 @@ end) end - function library:watermark(options) - local cfg = { - name = options.name or "nebulahax"; - } - + -- standalone floating panel -- the watermark's look, but reusable from anywhere. + -- writes go through the proxy, so `label.text = "hi"` updates the ui immediately: + -- + -- local label = library.label() + -- label.text = "hi" + -- label.position = Vector2.new(500, 500) + -- + -- pass a callback to make it tappable (dragging never counts as a tap). + function library.label(options) + -- tolerate library:label{} as well as library.label{} + if options == library then options = nil end + options = options or {} + + local cfg = {} + -- Instances local outline = library:create("Frame", { Parent = library.sgui; - Position = dim2(0, 50, 0, 50); + Position = dim_offset(50, 50); BorderColor3 = rgb(0, 0, 0); Size = dim2(0, 0, 0, 24); BorderSizePixel = 0; AutomaticSize = Enum.AutomaticSize.X; BackgroundColor3 = rgb(255, 255, 255) - }); library.watermark_outline = outline; library:draggify(outline); - + }); + local dark = library:create("Frame", { Parent = outline; BackgroundTransparency = 0.6; @@ -951,7 +988,7 @@ FontFace = fonts["ProggyClean"]; TextColor3 = rgb(255, 255, 255); BorderColor3 = rgb(0, 0, 0); - Text = cfg.name; + Text = ""; Parent = dark; Size = dim2(0, 0, 1, 0); Position = dim2(0, 1, 0, -1); @@ -961,41 +998,198 @@ AutomaticSize = Enum.AutomaticSize.X; TextSize = 12; BackgroundColor3 = rgb(255, 255, 255) - }); - - library.watermark_gradient = library:create("UIGradient", { + }); + + local gradient = library:create("UIGradient", { Color = rgbseq{ - rgbkey(0, themes.preset["1"]), + rgbkey(0, themes.preset["1"]), rgbkey(0.5, themes.preset["2"]), rgbkey(1, themes.preset["3"]), }; Parent = outline }); + + library.label_gradients[#library.label_gradients + 1] = gradient + + -- transparent lid so touch and mouse land on one instance + local hitbox = library:create("TextButton", { + Parent = outline; + BackgroundTransparency = 1; + Size = dim2(1, 0, 1, 0); + Text = ""; + AutoButtonColor = false; + BorderSizePixel = 0; + ZIndex = 5 + }); -- - function cfg.update_text(text) - text_title.Text = text + local state = { + text = options.text or options.name or "mirrorhack", + position = options.position or vec2(50, 50), + visible = options.visible ~= false, + callback = options.callback, + } + + -- declared up front so the handlers below close over the local, not a global + local proxy + local setters = {} + + function setters.text(value) + state.text = tostring(value) + text_title.Text = state.text end - cfg.update_text(cfg.name) + function setters.position(value) + state.position = value + outline.Position = typeof(value) == "UDim2" and value or dim_offset(value.X, value.Y) + end - return setmetatable(cfg, library) - end + function setters.visible(value) + state.visible = value and true or false + outline.Visible = state.visible + end + + function setters.callback(value) + state.callback = value + end + + -- drag, with a tap fallback -- a touch that barely moves is a click, not a drag + local dragging, drag_start, drag_origin, travelled = false, nil, nil, 0 + + hitbox.InputBegan:Connect(function(input) + if input.UserInputType ~= Enum.UserInputType.MouseButton1 + and input.UserInputType ~= Enum.UserInputType.Touch then return end + + dragging = true + travelled = 0 + drag_start = input.Position + drag_origin = outline.Position + end) + + library:connection(uis.InputChanged, function(input) + if not dragging then return end + if input.UserInputType ~= Enum.UserInputType.MouseMovement + and input.UserInputType ~= Enum.UserInputType.Touch then return end + + local delta = input.Position - drag_start + travelled = max(travelled, abs(delta.X) + abs(delta.Y)) + + -- AutomaticSize leaves Size.X.Offset at 0, so clamp against the measured width + setters.position(dim_offset( + clamp(drag_origin.X.Offset + delta.X, 0, camera.ViewportSize.X - outline.AbsoluteSize.X), + clamp(drag_origin.Y.Offset + delta.Y, 0, camera.ViewportSize.Y - outline.AbsoluteSize.Y) + )) + end) + + library:connection(uis.InputEnded, function(input) + if not dragging then return end + if input.UserInputType ~= Enum.UserInputType.MouseButton1 + and input.UserInputType ~= Enum.UserInputType.Touch then return end + + dragging = false + + if travelled <= 6 and state.callback then + state.callback(proxy) + end + end) + + cfg.instance = outline + cfg.label = text_title + cfg.gradient = gradient + cfg.hitbox = hitbox + + function cfg.update_text(text) + proxy.text = text + end + + function cfg.remove() + local index = find(library.label_gradients, gradient) + if index then remove(library.label_gradients, index) end + + index = find(library.labels, proxy) + if index then remove(library.labels, index) end + + outline:Destroy() + end + + proxy = setmetatable({}, { + __index = function(_, key) + local value = state[key] + if value ~= nil then return value end + + value = cfg[key] + if value ~= nil then return value end + + return library[key] + end, + + __newindex = function(_, key, value) + local setter = setters[key] + + if setter then + setter(value) + else + rawset(cfg, key, value) + end + end, + }) + + setters.text(state.text) + setters.position(state.position) + setters.visible(state.visible) + + library.labels[#library.labels + 1] = proxy + + return proxy + end + + function library:watermark(options) + options = options or {} + + local mark = library.label({ + text = options.name or options.text or "nebulahax", + position = options.position, + }) + + library.watermark_outline = mark.instance + library.watermark_gradient = mark.gradient + + return mark + end local watermark = library:watermark({name = "mirrorhack - 100 fps - 100 ping"}) local fps = 0 - local watermark_delay = tick() + local watermark_delay = tick() run.RenderStepped:Connect(function() fps += 1 - if tick() - watermark_delay > 1 then + if tick() - watermark_delay > 1 then watermark_delay = tick() - local ping = math.floor(stats.PerformanceStats.Ping:GetValue()) .. "ms" + local ping = math.floor(stats.PerformanceStats.Ping:GetValue()) .. "ms" watermark.update_text(string.format("mirrorhack - fps: %s - ping: %s", fps, ping)) fps = 0 end end) + + -- mobile has no keyboard, so the menu keybind is unreachable -- hand it a tappable + -- stand-in that sits under the watermark and toggles the same thing the bind does. + -- reads the live visibility rather than tracking its own flag, so tapping stays in + -- sync with anyone who toggled the menu by keybind + library.mobile_toggle = library.label({ + text = "toggle ui", + position = vec2(50, 82), + visible = library.is_mobile, + callback = function() + local window = library.main_window + + if window and window.main_outline then + window.toggle_menu(not window.main_outline.Visible) + elseif library.gui then + library.gui.Enabled = not library.gui.Enabled + end + end, + }) --[[ local pingTimeSec = game.Players.LocalPlayer:GetNetworkPing() @@ -2594,48 +2788,15 @@ section:keybind({name = "menu bind", callback = function(bool) window.toggle_menu(bool) end, default = true}) section:colorpicker({name = "gradient 1", callback = function(color) library:update_theme("1", color) - - library.gradient.Color = rgbseq{ - rgbkey(0, themes.preset["1"]), - rgbkey(0.5, themes.preset["2"]), - rgbkey(1, themes.preset["3"]), - }; - - library.watermark_gradient.Color = rgbseq{ - rgbkey(0, themes.preset["1"]), - rgbkey(0.5, themes.preset["2"]), - rgbkey(1, themes.preset["3"]), - }; + library:refresh_gradients() end, color = themes.preset["1"]}) section:colorpicker({name = "gradient 2", callback = function(color) library:update_theme("2", color) - - library.gradient.Color = rgbseq{ - rgbkey(0, themes.preset["1"]), - rgbkey(0.5, themes.preset["2"]), - rgbkey(1, themes.preset["3"]), - }; - - library.watermark_gradient.Color = rgbseq{ - rgbkey(0, themes.preset["1"]), - rgbkey(0.5, themes.preset["2"]), - rgbkey(1, themes.preset["3"]), - }; + library:refresh_gradients() end, color = themes.preset["2"]}) section:colorpicker({name = "gradient 3", callback = function(color) library:update_theme("3", color) - - library.gradient.Color = rgbseq{ - rgbkey(0, themes.preset["1"]), - rgbkey(0.5, themes.preset["2"]), - rgbkey(1, themes.preset["3"]), - }; - - library.watermark_gradient.Color = rgbseq{ - rgbkey(0, themes.preset["1"]), - rgbkey(0.5, themes.preset["2"]), - rgbkey(1, themes.preset["3"]), - }; + library:refresh_gradients() end, color = themes.preset["3"]}) main:column({})