local UI = require(script.Parent:WaitForChild("UI"))

local Market = {}
Market.__index = Market

function Market.new(_K)
	local scroller = UI.new "Scroller" {
		Name = "Market",
	}

	task.defer(function()
		local donationLevel = _K.LocalPlayer:GetAttribute("_KDonationLevel")

		local equippedUGC = {}
		local debounceEquip = _K.Util.Function.debounce(0.25, function(id, equip, equipped, name, button)
			local existing = equippedUGC[equip]
			if existing and existing ~= button then
				existing.BackgroundColor3(Color3.fromRGB(0, 200, 0))
				existing.Label("WEAR")
			end

			if equipped then
				equippedUGC[equip] = button
				button.BackgroundColor3(Color3.fromRGB(200, 0, 0))
				button.Label("HIDE")
			else
				equippedUGC[equip] = nil
				button.BackgroundColor3(Color3.fromRGB(0, 200, 0))
				button.Label("WEAR")
			end

			_K.Remote.VIPUGCMethod:Fire(id, equip, equipped, name)
		end)

		-- THIRD PARTY PURCHASES HANDLING

		local STUDIO_DEBUG = _K.DEBUG and _K.IsStudio
		local DEBUG_SHOW_PRICES = STUDIO_DEBUG

		local function updateAssetPriceText(label: TextLabel, assetId: number, ownedText: string?)
			local success, owned = _K.Util.Retry(function()
				return _K.Service.Marketplace:PlayerOwnsAssetAsync(_K.LocalPlayer, assetId)
			end)

			if success then
				if owned and not DEBUG_SHOW_PRICES then
					label.Text = ownedText or "OWNED"
					label.TextTransparency = 0.5
					return
				end

				local conn
				conn = _K.Service.Marketplace.PromptBulkPurchaseFinished:Connect(function(player, status, results)
					if player ~= _K.LocalPlayer or (results.Items[1].id ~= tostring(assetId)) then
						return
					end
					if status == Enum.MarketplaceBulkPurchasePromptStatus.Completed then
						conn:Disconnect()
						owned = true
						label.Text = ownedText or "OWNED"
						label.TextTransparency = 0.5
					end
				end)

				local ok, result = _K.Util.Retry(function()
					return _K.Service.Marketplace:GetProductInfoAsync(assetId, Enum.InfoType.Asset).PriceInRobux
				end)
				if ok then
					if not owned or DEBUG_SHOW_PRICES then
						label.Text = `<b>{result or "?"}</b>`
					end
				end
			end
		end

		local function updatePassPriceText(label: TextLabel, gamePassId: number, ownedText: string?)
			local success, owned = _K.Util.Retry(function()
				return _K.Service.Marketplace:UserOwnsGamePassAsync(_K.LocalPlayer.UserId, gamePassId)
			end)

			if success then
				if owned and not DEBUG_SHOW_PRICES then
					label.Text = ownedText or "OWNED"
					label.TextTransparency = 0.5
					return
				end

				local conn
				conn = _K.Service.Marketplace.PromptGamePassPurchaseFinished:Connect(function(userId, passId, purchased)
					if purchased and passId == gamePassId and userId == _K.LocalPlayer.UserId then
						conn:Disconnect()
						owned = true
						label.Text = ownedText or "OWNED"
						label.TextTransparency = 0.5
					end
				end)

				local ok, result = _K.Util.Retry(function()
					return _K.Service.Marketplace:GetProductInfoAsync(gamePassId, Enum.InfoType.GamePass).PriceInRobux
				end)
				if ok then
					if not owned or DEBUG_SHOW_PRICES then
						label.Text = `<b>{result or "?"}</b>`
					end
				end
			end
		end

		local function updateTitleLabel(button, name: string, display: string?)
			local titleRef = string.lower(name)
			local definition = shared._KVIPTitle.definition[titleRef]
			local label = button._content.Label

			UI.new "Spacer" {
				Parent = button._content,
				LayoutOrder = 2,
			}
			UI.edit(label, {
				AutomaticSize = Enum.AutomaticSize.X,
				Size = UDim2.new(0, 0, 1, 0),
				FontFace = Font.fromEnum(Enum.Font.FredokaOne),
				TextSize = UI.Theme.FontSizeLarge,
				TextColor3 = Color3.new(1, 1, 1),
			})
			label.UIFlexItem.FlexMode = Enum.UIFlexMode.Shrink

			if definition then
				label.Name = "Title"
				label.Text = `<stroke thickness="1">{name}</stroke>{display or ""}`

				UI.new "UIGradient" {
					Parent = label,
					Color = ColorSequence.new(
						definition.topColor or Color3.new(1, 1, 1),
						definition.color or Color3.new(1, 1, 1)
					),
					Rotation = definition.rotation or 90,
				}
				button._content:AddTag("_K" .. titleRef)
			else
				UI.new "UIStroke" {
					Parent = label,
					Thickness = 1,
				}
			end
		end

		-- CUSTOM UGC

		local customUGC = UI.new "Frame" {
			Name = "UGC",
			AutomaticSize = Enum.AutomaticSize.Y,
			BackgroundTransparency = 1,
			Size = UDim2.new(1, -2, 0, 0),

			UI.new "UIListLayout" {
				SortOrder = Enum.SortOrder.LayoutOrder,
				FillDirection = Enum.FillDirection.Horizontal,
				HorizontalFlex = Enum.UIFlexAlignment.SpaceEvenly,
				HorizontalAlignment = Enum.HorizontalAlignment.Center,
				Padding = UI.Theme.PaddingWithStroke,
				Wraps = true,
			},
		}

		for i, ugc in _K.VIP.purchasables.scriptedUGC do
			task.defer(function()
				local owned = (donationLevel or 0) >= (ugc.level or 3)
				ugc.price = if owned then "WEAR" else "?"

				local frame = UI.new "Frame" {
					Parent = customUGC,
					LayoutOrder = i,
					AutomaticSize = Enum.AutomaticSize.XY,
					BackgroundTransparency = 1,

					UI.new "ImageLabel" {
						BackgroundColor3 = Color3.new(0, 0, 0),
						BackgroundTransparency = 0.875,
						Size = UDim2.new(0, 80, 0, 80),
						Image = "rbxasset://textures/meshPartFallback.png",
						ImageTransparency = 0.75,
						ScaleType = Enum.ScaleType.Tile,
						TileSize = UDim2.fromOffset(16, 16),

						UI.new "UICorner" {
							CornerRadius = UI.Theme.CornerPadded,
						},
						UI.new "Stroke" {},

						UI.new "ImageLabel" {
							BackgroundTransparency = 1,
							Size = UDim2.new(1, 0, 1, 0),
							Image = `rbxthumb://type=Asset&id={ugc.id}&w=150&h=150`,

							UI.new "ImageLabel" {
								BackgroundTransparency = 1,
								Size = UDim2.new(0, 28, 0, 28),
								Position = UDim2.new(0, 2, 0, 2),
								Image = "rbxasset://textures/ui/InspectMenu/ico_favorite@2x.png",
								ImageColor3 = Color3.new(0, 0, 0),
								ImageTransparency = 0.875,
							},
							UI.new "ImageButton" {
								BackgroundTransparency = 1,
								Position = UDim2.new(0, 4, 0, 4),
								Size = UDim2.new(0, 24, 0, 24),
								Image = "rbxasset://textures/ui/InspectMenu/ico_favorite@2x.png",
								ImageColor3 = Color3.new(1, 0.8, 0),
								Activated = function()
									pcall(function()
										_K.Service.AvatarEditor:PromptSetFavorite(ugc.id, 1, true)
									end)
								end,
								UI.new "Tooltip" {
									Text = "Add to Favorites",
								},
							},
						},
					},
				}

				local button, try, trying
				local function tryDone()
					trying = nil
					if not owned then
						button.Label(`<b>{ugc.price}</b>`)
						button._content.IconFrame.Visible = true
						button._content.Label.TextXAlignment = Enum.TextXAlignment.Left
					end
				end

				button = UI.new "Button" {
					Parent = frame,
					Icon = if owned then nil else "rbxasset://textures/ui/common/robux.png",
					IconProperties = { ImageColor3 = UI.Theme.PrimaryText },
					IconRightAlign = not owned,
					Label = `<b>{ugc.price}</b>`,
					LabelProperties = {
						FontFace = UI.Theme.FontMono,
						TextSize = UI.Theme.FontSizeLarge,
						TextXAlignment = if owned then nil else Enum.TextXAlignment.Left,
						TextTruncate = Enum.TextTruncate.None,
					},
					BackgroundColor3 = Color3.fromRGB(0, 200, 0),
					BackgroundTransparency = 0.5,
					Size = UDim2.new(0, 80, 0, 32),
					Position = UDim2.fromOffset(0, 88),

					[UI.Attribute] = {
						Price = ugc.price,
					},

					Activated = function()
						if try then
							try, trying = nil, true
							local existing = equippedUGC[ugc.equip]
							debounceEquip(ugc.id, ugc.equip, true, ugc.name, button)
							task.delay(15, function()
								if trying and equippedUGC[ugc.equip] == button then
									debounceEquip(ugc.id, ugc.equip, false, ugc.name, button)
								end
								task.delay(0.25, tryDone)
							end)
							_K.Notify({
								From = "_K",
								Text = `Trying <b>{ugc.name}</b>.`,
								Duration = 15,
							})

							if existing and existing ~= button then
								task.delay(0.25, function()
									existing.Label(`<b>{existing._instance:GetAttribute("Price")}</b>`)
									existing._content.IconFrame.Visible = true
									existing._content.Label.TextXAlignment = Enum.TextXAlignment.Left
								end)
							end

							return
						end

						local equipped = equippedUGC[ugc.equip]
						if owned or (trying and equipped == button) then
							equipped = equipped and equipped == button
							debounceEquip(ugc.id, ugc.equip, not equipped, ugc.name, button)
							if trying and equipped then
								task.delay(0.25, tryDone)
							end
							return
						end

						_K.PromptBulkPurchase(ugc.id)
					end,
				}

				if not owned then
					try = true
					button.Label("TRY")
					button._content.IconFrame.Visible = false
					button._content.Label.TextXAlignment = Enum.TextXAlignment.Center

					local con, con2
					local function setOwned()
						if con then
							con:Disconnect()
							con = nil
						end
						if con2 then
							con2:Disconnect()
							con2 = nil
						end
						owned, try = true, nil
						button.Label("WEAR")
						button._content.IconFrame.Visible = false
						button._content.Label.TextXAlignment = Enum.TextXAlignment.Center
					end

					con = _K.Service.Marketplace.PromptPurchaseFinished:Connect(function(player, assetId, purchased)
						if purchased and assetId == ugc.id and player == _K.LocalPlayer then
							setOwned()
						end
					end)

					con2 = _K.LocalPlayer:GetAttributeChangedSignal("_KDonationLevel"):Connect(function()
						donationLevel = math.max(donationLevel or 0, _K.LocalPlayer:GetAttribute("_KDonationLevel"))
						if (donationLevel or 0) >= (ugc.level or 3) then
							setOwned()
						end
					end)

					task.spawn(function()
						local ok, result = _K.Util.Retry(function()
							if owned then
								return true
							end
							return _K.Service.Marketplace:PlayerOwnsAssetAsync(_K.LocalPlayer, ugc.id)
						end)
						if ok and result then
							setOwned()
						end
					end)

					local ok, result = _K.Util.Retry(function()
						return _K.Service.Marketplace:GetProductInfoAsync(ugc.id, Enum.InfoType.Asset).PriceInRobux
					end)
					if ok then
						ugc.price = result or "?"
						button._instance:SetAttribute("Price", ugc.price)
					end
				end

				if ugc.tip then
					UI.new "Tooltip" {
						Parent = button._instance,
						Text = ugc.tip,
						Hovering = button._hovering,
					}
				end
			end)
		end

		-- SUPPORT GAMEPASSES

		local gamePasses = UI.new "Frame" {
			Name = "GamePasses",
			AutomaticSize = Enum.AutomaticSize.Y,
			BackgroundTransparency = 1,
			Size = UDim2.new(1, -2, 0, 0),

			UI.new "UIListLayout" {
				SortOrder = Enum.SortOrder.LayoutOrder,
				FillDirection = Enum.FillDirection.Horizontal,
				HorizontalFlex = Enum.UIFlexAlignment.SpaceEvenly,
				HorizontalAlignment = Enum.HorizontalAlignment.Center,
				Padding = UI.Theme.PaddingWithStroke,
				Wraps = true,
			},
		}

		local remoteEquipped = {}
		local function donorEquip(name, button, remote)
			local equipped = remoteEquipped[remote]
			if equipped then
				equipped._content.Price.Text = `<b>SHOW</b>`
			end
			if equipped ~= button then
				remoteEquipped[remote] = button
				button._content.Price.Text = `<b>HIDE</b>`
				remote:Fire(name)
			else
				remoteEquipped[remote] = nil
				remote:Fire()
			end
		end

		local function generateDonationButtons(parent: Instance)
			for i, pass in _K.VIP.purchasables.donations do
				local isTitle = _K.Data.settings.vip and i > 4
				local remote = if isTitle then _K.Remote.Title else _K.Remote.DonorTrail
				local button
				button = UI.new "Button" {
					Parent = parent,
					LayoutOrder = i,
					Icon = "rbxasset://textures/ui/common/robux.png",
					IconProperties = { ImageColor3 = UI.Theme.PrimaryText },
					IconRightAlign = true,
					Label = `<font color='{pass.color}'><b>{pass.name}</b></font>`,
					TextXAlignment = Enum.TextXAlignment.Left,
					Size = UDim2.new(0, 128, 0, 32),

					UI.new "TextLabel" {
						Name = "Price",
						LayoutOrder = 8,
						AutoLocalize = false,
						AutomaticSize = Enum.AutomaticSize.XY,
						BackgroundTransparency = 1,
						FontFace = UI.Theme.FontMono,
						TextSize = UI.Theme.FontSizeLarge,
						TextColor3 = UI.Theme.PrimaryText,
						TextStrokeColor3 = UI.Theme.Primary,
						TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
						TextWrapped = true,
						Size = function()
							return UDim2.new(0, UI.Theme.FontSizeLarge(), 0, 32)
						end,
						Text = `?`,
						RichText = true,
					},

					Activated = function()
						if
							button._content.Price.Text == `<b>SHOW</b>`
							or button._content.Price.Text == `<b>HIDE</b>`
						then
							donorEquip(pass.name, button, remote)
						else
							_K.PromptBulkPurchase(pass.assetId)
						end
					end,
				}
				UI.new "UIFlexItem" {
					Parent = button._instance,
					FlexMode = Enum.UIFlexMode.Fill,
				}

				local perkText = if i > 5
					then "Unlocks:\n💬 Chat commands\n👑 All animated accessories\n✨ Custom particle effects\n😎 Exclusive title"
					elseif
						i > 4
					then "Unlocks:\n💬 Chat commands\n👑 Floating crown\n✨ Custom particle effects\n😎 Exclusive title"
					elseif i > 3 then "Unlocks:\n💬 Chat commands\n✨ Custom particle effects"
					else "Unlocks a particle effect!"

				UI.new "Tooltip" {
					Parent = button._instance,
					Text = `We've poured thousands of hours into Kohl's Admin.\nIf you enjoy it, please donate! ❤\n\n<font color='#0f0'><b>{perkText}`,
					Hovering = button._hovering,
				}

				updateTitleLabel(button, pass.name)

				task.spawn(function()
					if not DEBUG_SHOW_PRICES then
						local success, owned = _K.Util.Retry(function()
							return _K.Service.Marketplace:UserOwnsGamePassAsync(_K.LocalPlayer.UserId, pass.id)
						end)
						if success and owned then
							button._content.Price.Text = `<b>SHOW</b>`
							return
						end
					end
					updateAssetPriceText(button._content.Price, pass.assetId, `<b>SHOW</b>`)
				end)
			end
		end

		_K.client.generateDonationButtons = generateDonationButtons

		generateDonationButtons(gamePasses)

		local vipList = UI.new "Frame" {
			Name = "VIP",
			AutomaticSize = Enum.AutomaticSize.Y,
			BackgroundTransparency = 1,
			Size = UDim2.new(1, -2, 0, 0),

			UI.new "UIListLayout" {
				SortOrder = Enum.SortOrder.LayoutOrder,
				FillDirection = Enum.FillDirection.Horizontal,
				HorizontalAlignment = Enum.HorizontalAlignment.Center,
				Padding = UI.Theme.PaddingWithStroke,
				Wraps = true,
			},
		}

		local halfButtonSize = UI.compute(function()
			return UDim2.new(0.5, -UI.Theme.Padding().Offset, 0, 32)
		end)

		local vipDeveloperPassId = if _K.Data.roles.vip and _K.Data.roles.vip.gamepasses
			then _K.Data.roles.vip.gamepasses[1]
			else nil

		local vipButtonGlobal
		vipButtonGlobal = UI.new "Button" {
			Parent = vipList,
			LayoutOrder = 3,
			Icon = "rbxasset://textures/ui/common/robux.png",
			IconProperties = { ImageColor3 = UI.Theme.PrimaryText },
			IconRightAlign = true,
			Label = "VIP",
			TextXAlignment = Enum.TextXAlignment.Left,
			Size = halfButtonSize,

			UI.new "TextLabel" {
				Name = "Price",
				LayoutOrder = 8,
				AutoLocalize = false,
				AutomaticSize = Enum.AutomaticSize.XY,
				BackgroundTransparency = 1,
				FontFace = UI.Theme.FontMono,
				TextSize = UI.Theme.FontSizeLarge,
				TextColor3 = UI.Theme.PrimaryText,
				TextStrokeColor3 = UI.Theme.Primary,
				TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
				TextWrapped = true,
				Size = function()
					return UDim2.new(0, UI.Theme.FontSizeLarge(), 0, 32)
				end,
				Text = "?",
				RichText = true,
			},
			Activated = function()
				if
					vipButtonGlobal._content.Price.Text == `<b>SHOW</b>`
					or vipButtonGlobal._content.Price.Text == `<b>HIDE</b>`
				then
					donorEquip("VIP", vipButtonGlobal, _K.Remote.Title)
				else
					_K.PromptPurchaseVIP()
				end
			end,
		}

		UI.new "Tooltip" {
			Parent = vipButtonGlobal,
			Text = "✨ Become a VIP\n\nUnlock VIP commands globally!",
			Hovering = vipButtonGlobal._hovering,
		}

		updateTitleLabel(
			vipButtonGlobal,
			"VIP",
			`  <font transparency="0.75"><stroke thickness="1" transparency="0.75">(Global)</stroke></font>`
		)

		local vipButtonDeveloper = UI.new "Button" {
			Parent = vipList,
			LayoutOrder = 4,
			Icon = "rbxasset://textures/ui/common/robux.png",
			IconProperties = { ImageColor3 = UI.Theme.PrimaryText },
			IconRightAlign = true,
			Label = "VIP",
			TextXAlignment = Enum.TextXAlignment.Left,
			Size = halfButtonSize,
			Visible = if vipDeveloperPassId then true else false,

			UI.new "TextLabel" {
				Name = "Price",
				LayoutOrder = 8,
				AutoLocalize = false,
				AutomaticSize = Enum.AutomaticSize.XY,
				BackgroundTransparency = 1,
				FontFace = UI.Theme.FontMono,
				TextSize = UI.Theme.FontSizeLarge,
				TextColor3 = UI.Theme.PrimaryText,
				TextStrokeColor3 = UI.Theme.Primary,
				TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
				TextWrapped = true,
				Size = function()
					return UDim2.new(0, UI.Theme.FontSizeLarge(), 0, 32)
				end,
				Text = "?",
				RichText = true,
			},
			Activated = function()
				_K.Service.Marketplace:PromptGamePassPurchase(_K.LocalPlayer, vipDeveloperPassId)
			end,
		}

		updateTitleLabel(
			vipButtonDeveloper,
			"VIP",
			`  <font transparency="0.75"><stroke thickness="1" transparency="0.75">(Game)</stroke></font>`
		)

		task.defer(function()
			local success, owned = _K.Util.Retry(function()
				return _K.Service.Marketplace:UserOwnsGamePassAsync(_K.LocalPlayer.UserId, 5411126)
			end)
			if success and owned then
				task.defer(updatePassPriceText, vipButtonGlobal._content.Price, 5411126, `<b>SHOW</b>`)
			else
				local VIP_UGC_ID = 110019084552349
				local VIP_UGC_SALE_ID = 115444443724463
				local ok, ugcOwned = _K.Util.Retry(function()
					return _K.Service.Marketplace:PlayerOwnsAssetAsync(_K.LocalPlayer, VIP_UGC_ID)
				end)
				task.defer(updateAssetPriceText, vipButtonGlobal._content.Price, VIP_UGC_ID, `<b>SHOW</b>`)
				if not ok or ugcOwned then
					return
				end
				task.delay(600, updateAssetPriceText, vipButtonGlobal._content.Price, VIP_UGC_SALE_ID, `<b>SHOW</b>`)
			end
		end)

		if vipDeveloperPassId then
			task.defer(updatePassPriceText, vipButtonDeveloper._content.Price, vipDeveloperPassId)
		end

		local gamesTooltipText = "Find games using Kohl's Admin!\n\n"
			.. "Check the Kohl's Admin Settings script in Studio to add your game to the charts.\n\n"
			.. "Game Creators get up to 40% when "
			.. "Third Party Purchases are enabled in their Roblox Game Settings!"

		UI.edit(scroller._instance, {

			UI.new "TextLabel" {
				Name = "Status",
				AutomaticSize = Enum.AutomaticSize.XY,
				BackgroundColor3 = Color3.new(1, 0, 0),
				BackgroundTransparency = 0.75,
				FontFace = UI.Theme.FontMono,
				TextSize = UI.Theme.FontSizeSmall,
				TextColor3 = Color3.new(1, 1, 1),
				Text = `<sc>VIP benefits are <b>disabled</b> in this game!</sc>`,
				TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
				TextWrapped = true,
				RichText = true,
				Visible = function()
					return not _K.client.settings.vip()
				end,

				UI.new "UICorner" {
					CornerRadius = UI.Theme.CornerPadded,
				},
				UI.new "UIPadding" {
					PaddingLeft = UI.Theme.Padding,
					PaddingRight = UI.Theme.Padding,
					PaddingTop = UI.Theme.PaddingHalf,
					PaddingBottom = UI.Theme.PaddingHalf,
				},
			},

			UI.new "TextLabel" {
				AutoLocalize = false,
				AutomaticSize = Enum.AutomaticSize.Y,
				BackgroundTransparency = 1,
				FontFace = UI.Theme.Font,
				TextSize = UI.Theme.FontSize,
				TextColor3 = UI.Theme.PrimaryText,
				TextStrokeColor3 = UI.Theme.Primary,
				TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
				TextWrapped = true,
				Size = UDim2.new(1, 0, 0, 0),
				Text = "❤ Kohl's Admin?\nDonate for cool perks and more updates!",
				RichText = true,
			},

			vipList,
			gamePasses,
			customUGC,

			UI.new "TextLabel" {
				AutoLocalize = false,
				AutomaticSize = Enum.AutomaticSize.XY,
				BackgroundTransparency = 1,
				FontFace = UI.Theme.Font,
				TextSize = UI.Theme.FontSize,
				TextColor3 = UI.Theme.PrimaryText,
				TextStrokeColor3 = UI.Theme.Primary,
				TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
				TextWrapped = true,
				Size = UDim2.new(0, 0, 0, 0),
				Text = "Looking for more games with Kohl's Admin?",
				RichText = true,
				UI.new "Tooltip" {
					Text = gamesTooltipText,
				},
			},

			UI.new "Frame" {
				AutomaticSize = Enum.AutomaticSize.XY,
				BackgroundTransparency = 1,

				UI.new "UIListLayout" {
					FillDirection = Enum.FillDirection.Horizontal,
					Padding = UI.Theme.PaddingWithStroke,
					SortOrder = Enum.SortOrder.LayoutOrder,
					VerticalAlignment = Enum.VerticalAlignment.Center,
				},

				UI.new "ImageButton" {
					BackgroundTransparency = 1,
					Size = UDim2.new(0, 24, 0, 24),
					Image = "rbxasset://textures/ui/InspectMenu/ico_favorite@2x.png",
					ImageColor3 = Color3.new(1, 0.8, 0),
					Activated = function()
						pcall(function()
							_K.Service.AvatarEditor:PromptSetFavorite(17873329124, 1, true)
						end)
					end,
					UI.new "Tooltip" {
						Text = "Add to Favorites",
					},
				},
				UI.new "Link" {
					Name = "CatalogLink",
					Text = "roblox.com/games/17873329124",
					Tooltip = `<b>Copy:</b> Ctrl + C\n\n{gamesTooltipText}`,
				},
			},
		})

		UI.edit(scroller._instance.UIListLayout, {
			Padding = UI.Theme.PaddingWithStroke,
			HorizontalAlignment = "Center",
		})
	end)

	return scroller
end

return Market


local UI = require(script.Parent:WaitForChild("UI"))

local Members = {}
Members.__index = Members

function Members.new(_K)
	local escape = _K.Util.String.escapeRichText

	local inputSize = function()
		local size = UI.Theme.FontSize()
		local padding = UI.Theme.Padding().Offset
		return UDim2.fromOffset(0, size + padding)
	end

	local roleChoice = UI.state({ Id = "vip", Name = "VIP" })
	local roleChoices = UI.state({ { Id = "vip", Name = "VIP" } })
	local roleDialogTarget = UI.state()
	local roleDialogInput
	local function resetRoleTargetInput()
		roleDialogTarget(nil)
		roleDialogInput.Value("")
	end

	roleDialogInput = UI.new "Input" {
		AutomaticSize = Enum.AutomaticSize.X,
		FontFace = UI.Theme.FontMono,
		FontSize = UI.Theme.FontSize,
		MaxChars = 20,
		Placeholder = "UserId or Username",
		Size = inputSize,
		Value = "",

		UI.new "UIFlexItem" {
			FlexMode = Enum.UIFlexMode.Shrink,
		},

		[UI.Hook] = {
			Value = function(value)
				local userId
				if tonumber(value) then
					userId = tonumber(value)
				else
					local ok, result = pcall(_K.Service.Players.GetUserIdFromNameAsync, _K.Service.Players, value)
					userId = ok and result
				end

				local fauxArg = {
					_K = _K,
					definition = _K.Registry.commands.role.args[1],
					command = {
						from = _K.LocalPlayer.UserId,
						fromRank = _K.Auth.getRank(_K.LocalPlayer.UserId),
						fromRole = {},
						rank = _K.Auth.hasCommand(_K.LocalPlayer.UserId, "role") or 0,
					},
				}

				_K.Data.targetLimits[_K.LocalPlayer.UserId] = 0
				if userId and not _K.Auth.targetUserArgument(fauxArg, userId, userId) then
					userId = nil
					task.defer(resetRoleTargetInput)
				end

				local member = _K.Data.members[tostring(userId)] or {}
				local availableRoles = {}
				for rank, role in _K.Data.rolesList do
					if rank == 0 or (rank >= _K.client.rank._value and _K.creatorId ~= _K.LocalPlayer.UserId) then
						continue
					end
					local hasRole = if _K.Auth.hasPermission(_K.LocalPlayer.UserId, "saveRoles")
						then table.find(member.persist or {}, role._key)
						else table.find(member.roles or {}, role._key)
					if hasRole then
						continue
					end
					table.insert(availableRoles, { Id = role._key, Name = role.name })
				end
				if availableRoles then
					roleChoice(availableRoles[1])
					roleChoices(availableRoles)
				else
					roleChoice("")
					roleChoices({})
				end
				roleDialogTarget(userId)
			end,
		},
	}

	local newRoleDialog
	newRoleDialog = UI.new "Dialog" {
		Parent = UI.LayerTop,
		BackgroundTransparency = 0,
		Draggable = true,
		Title = "Add Role",
		Visible = false,
		ExitButton = true,
		Modal = true,
		Close = function()
			newRoleDialog.Visible(false)
		end,

		UI.new "UIPadding" {
			PaddingTop = UI.Theme.Padding,
			PaddingBottom = UI.Theme.Padding,
			PaddingLeft = UI.Theme.Padding,
			PaddingRight = UI.Theme.Padding,
		},

		UI.new "ListItem" {
			Text = "User",
			UI.new "ImageLabel" {
				BackgroundTransparency = UI.Theme.TransparencyHeavy,
				BackgroundColor3 = UI.Theme.PrimaryText,
				Size = inputSize,
				Image = function()
					return `rbxthumb://type=AvatarHeadShot&id={roleDialogTarget()}&w=48&h=48`
				end,
				UI.new "UIAspectRatioConstraint" {
					AspectType = Enum.AspectType.ScaleWithParentSize,
					DominantAxis = Enum.DominantAxis.Height,
				},
				UI.new "UICorner" {
					CornerRadius = UI.Theme.CornerPadded,
				},
			},
			roleDialogInput,
		},

		UI.new "ListItem" {
			Text = "Role",
			UI.new "Select" {
				Value = roleChoice,
				Choices = roleChoices,
			},
			Visible = function()
				return roleDialogTarget() and #roleChoices() > 0
			end,
		},

		UI.new "Button" {
			Label = "Confirm",
			Activated = function()
				if roleDialogTarget._value then
					local save = _K.Registry.commands.role.LocalPlayerAuthorized
					local cmd =
						`{_K.getCommandPrefix()}{if save then "" else "temp"}role {roleDialogTarget()} {roleChoice().Id}`
					_K.Process.runCommands(_K, _K.LocalPlayer.UserId, cmd)
					newRoleDialog.Visible(false)
					resetRoleTargetInput()
				end
			end,
		},
	}

	local newMemberButton = UI.new "Button" {
		LayoutOrder = 3,
		ActiveSound = false,
		Icon = UI.Theme.Image.Add,
		IconProperties = {
			ImageColor3 = UI.Theme.PrimaryText,
		},
		Size = UDim2.new(1, 0, 1, 0),
		SizeConstraint = Enum.SizeConstraint.RelativeYY,

		UI.new "UIPadding" {
			PaddingTop = UI.Theme.Padding,
			PaddingBottom = UI.Theme.Padding,
			PaddingLeft = UI.Theme.Padding,
			PaddingRight = UI.Theme.Padding,
		},

		Visible = function()
			_K.client.rank()
			return _K.Registry.commands.role.LocalPlayerAuthorized
				or _K.Registry.commands.temprole.LocalPlayerAuthorized
		end,

		Activated = function()
			if not newRoleDialog.Visible._value then
				roleDialogTarget(nil)
				roleDialogInput.Value("")
				UI.Sound.Hover03:Play()
			end
			newRoleDialog.Visible(not newRoleDialog.Visible._value)
		end,
	}
	UI.new "Tooltip" { Parent = newMemberButton, Text = "New Member", Hovering = newMemberButton._hovering }
	UI.edit(newMemberButton._instance.UICorner, { CornerRadius = UI.Theme.CornerPadded })

	local itemSize = UI.compute(function()
		_K.client.rank()
		local font = UI.Theme.Font()
		local size = UI.Theme.FontSize()
		local padding = UI.Theme.Padding().Offset
		local maxWidth: number = size

		local params = Instance.new("GetTextBoundsParams")
		params.Font = font
		params.Size = size
		params.Width = math.huge
		params.RichText = true

		for _, role in _K.Data.roles do
			params.Text = role.name
			maxWidth = math.max(maxWidth, UI.retryGetTextBoundsAsync(params).X)
		end

		local cloudWidth = if _K.Auth.hasPermission(_K.LocalPlayer.UserId, "saveRoles") then size + padding else 0
		return UDim2.fromOffset(maxWidth + padding * 3 + size + cloudWidth, math.round(size + padding))
	end)

	local roleMenu = UI.new "Menu" {
		AutomaticSize = Enum.AutomaticSize.XY,
		RightAlign = true,
		Size = UDim2.new(),
	}
	roleMenu._content.AutomaticSize = Enum.AutomaticSize.X

	local target = UI.state()

	local function hoverClick(value)
		if value then
			UI.Sound.Hover02:Play()
		end
	end

	for roleId, role in _K.Data.roles do
		if not role._rank or role._rank == 0 then
			continue
		end
		UI.new "Button" {
			Parent = roleMenu,
			LayoutOrder = math.min(1e9, role._rank :: number),
			BackgroundTransparency = 1,
			Label = role.name,
			Icon = UI.Theme.Image.Circle,
			IconProperties = {
				ImageColor3 = Color3.fromHex(role.color),
				Size = UDim2.fromScale(0.5, 0.5),
				SizeConstraint = Enum.SizeConstraint.RelativeYY,
				Visible = true,
			},
			Padding = UDim.new(),
			Size = itemSize,
			TextXAlignment = Enum.TextXAlignment.Left,
			Visible = function()
				local member = target() or {}
				local hasCommand = _K.Registry.commands.role.LocalPlayerAuthorized
					or _K.Registry.commands.temprole.LocalPlayerAuthorized
				local hasRole = if _K.Auth.hasPermission(_K.LocalPlayer.UserId, "saveRoles")
					then table.find(member.persist or {}, roleId)
					else table.find(member.roles or {}, roleId)
				return (_K.client.rank() > role._rank or _K.creatorId == _K.LocalPlayer.UserId)
					and hasCommand
					and not hasRole
			end,

			UI.new "ImageLabel" {
				LayoutOrder = 9,
				BackgroundTransparency = 1,
				Image = UI.Theme.Image.Cloud_Save,
				ScaleType = Enum.ScaleType.Fit,
				Size = function()
					return UDim2.new(1, UI.Theme.Padding().Offset, 1, 0)
				end,
				SizeConstraint = Enum.SizeConstraint.RelativeYY,
				Visible = function()
					_K.client.rank()
					return _K.Auth.hasPermission(_K.LocalPlayer.UserId, "saveRoles")
				end,
			},

			Activated = function()
				UI.deactivateState(roleMenu.Visible, "floating")
				if _K.client.rank._value > role._rank or _K.creatorId == _K.LocalPlayer.UserId then
					local save = _K.Registry.commands.role.LocalPlayerAuthorized
					local cmd = `{_K.getCommandPrefix()}{if save then "" else "temp"}role {target().userId} {roleId}`
					_K.Process.runCommands(_K, _K.LocalPlayer.UserId, cmd)
				end
			end,
		}._hovering
			:Connect(hoverClick)
	end

	local function roleRemoveCornerRadius()
		local padding = UI.Theme.Padding()
		local radius = UI.Theme.CornerRadius()
		return UDim.new(radius.Scale, radius.Offset - padding.Offset * 0.75)
	end

	local function roleChip(roleId: string, member, item)
		local role = _K.Data.roles[roleId]
		local color = Color3.fromHex(role.color)

		local canRemove = UI.compute(function() -- TODO need to check the highest role chip
			local rank = 0
			for _, id in item.roles() do
				rank = math.max(rank, _K.Data.roles[id]._rank)
			end
			return (_K.client.rank() > rank or _K.creatorId == _K.LocalPlayer.UserId)
				and _K.Registry.commands.unrole.LocalPlayerAuthorized
		end)

		local button = UI.new "Button" {
			BackgroundTransparency = 1,
			Size = UDim2.new(1, 0, 1, 0),
			SizeConstraint = Enum.SizeConstraint.RelativeYY,
			Icon = UI.Theme.Image.Close_Bold,
			IconProperties = {
				ImageColor3 = UI.Theme.PrimaryText,
				Size = UDim2.fromScale(0.5, 0.5),
			},
			Text = "",
			Visible = canRemove,

			Activated = function()
				if
					(_K.client.rank() > role._rank or _K.creatorId == _K.LocalPlayer.UserId)
					and _K.Registry.commands.unrole.LocalPlayerAuthorized
				then
					local target = member.name or member.userId
					if string.find(target, "%s") then
						target = `"{target}"`
					end
					local cmd = `{_K.getCommandPrefix()}unrole {target} {roleId}`
					_K.Process.runCommands(_K, _K.LocalPlayer.UserId, cmd)
				end
			end,
		}
		UI.new "Tooltip" { Parent = button, Text = "Remove Role", Hovering = button._hovering }
		button._instance.UIStroke:Destroy()

		UI.edit(button._instance.UICorner, { CornerRadius = roleRemoveCornerRadius })

		return UI.new "Frame" {
			AutomaticSize = Enum.AutomaticSize.X,
			BackgroundColor3 = color,
			BackgroundTransparency = 0.75,
			Size = UDim2.new(0, 0, 1, 0),

			UI.new "UICorner" {
				CornerRadius = UI.Theme.CornerPadded,
			},

			UI.new "UIPadding" {
				PaddingLeft = function()
					return if canRemove() then UI.Theme.PaddingHalf() else UI.Theme.Padding()
				end,
				PaddingRight = UI.Theme.Padding,
				PaddingTop = UI.Theme.PaddingHalf,
				PaddingBottom = UI.Theme.PaddingHalf,
			},

			UI.new "UIListLayout" {
				VerticalAlignment = Enum.VerticalAlignment.Center,
				FillDirection = Enum.FillDirection.Horizontal,
				SortOrder = Enum.SortOrder.LayoutOrder,
				Padding = UI.Theme.PaddingHalf,
			},

			button,

			UI.new "TextLabel" {
				AutoLocalize = false,
				AutomaticSize = Enum.AutomaticSize.X,
				BackgroundTransparency = 1,
				Size = UDim2.new(0, 0, 1, 0),
				FontFace = UI.Theme.Font,
				Text = role.name,
				TextSize = function()
					return UI.Theme.FontSize() - UI.Theme.PaddingHalf().Offset
				end,
				TextColor3 = UI.Theme.PrimaryText,
				TextStrokeColor3 = UI.Theme.Primary,
				TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
			},

			UI.new "ImageLabel" {
				LayoutOrder = 9,
				BackgroundTransparency = 1,
				Image = UI.Theme.Image.Cloud_Done,
				ScaleType = Enum.ScaleType.Fit,
				Size = UDim2.fromScale(1, 1),
				SizeConstraint = Enum.SizeConstraint.RelativeYY,
				Visible = function()
					return table.find(member.persist or {}, roleId)
				end,
			},
		}
	end

	local function createItem(self, log)
		local item = {
			roles = UI.state({}),
			target = UI.state(),
		}

		local addButton
		addButton = UI.new "Button" {
			ActiveSound = false,
			Icon = UI.Theme.Image.Add,
			IconProperties = {
				ImageColor3 = UI.Theme.PrimaryText,
				Size = UDim2.fromScale(0.5, 0.5),
			},
			Size = UDim2.new(1, 0, 1, 0),
			SizeConstraint = Enum.SizeConstraint.RelativeYY,
			Visible = function()
				local _rank = _K.client.rank()
				return _K.Registry.commands.role.LocalPlayerAuthorized
					or _K.Registry.commands.temprole.LocalPlayerAuthorized
			end,

			Activated = function()
				target(item.target(), true)
				local count = 0
				for _, child in roleMenu._content:GetChildren() do
					if child:IsA("TextButton") and child.Visible then
						count += 1
					end
				end
				roleMenu._instance.Size = UDim2.fromOffset(0, math.min(count, 7.5) * itemSize().Y.Offset)
				roleMenu.Adornee(addButton._instance, true)
				UI.toggleState(roleMenu.Visible, "floating")
				local value = UI.raw(roleMenu.Visible)
				if value then
					UI.Sound.Hover03:Play()
				else
					UI.Sound.Hover01:Play()
				end
			end,
		}
		UI.new "Tooltip" { Parent = addButton, Text = "Add Role", Hovering = addButton._hovering }
		UI.edit(addButton._instance.UICorner, { CornerRadius = UI.Theme.CornerPadded })

		item._instance = UI.new "Frame" {
			BackgroundTransparency = 1,
			Size = self.ItemSize,

			UI.new "UIPadding" {
				PaddingBottom = UI.Theme.Padding,
			},

			UI.new "UIListLayout" {
				VerticalAlignment = Enum.VerticalAlignment.Center,
				FillDirection = Enum.FillDirection.Horizontal,
				SortOrder = Enum.SortOrder.LayoutOrder,
				Padding = UI.Theme.Padding,
			},

			UI.new "ImageLabel" {
				BackgroundTransparency = UI.Theme.TransparencyHeavy,
				BackgroundColor3 = UI.Theme.PrimaryText,
				Size = UDim2.new(1, 0, 1, 0),
				SizeConstraint = Enum.SizeConstraint.RelativeYY,

				UI.new "UICorner" {
					CornerRadius = UI.Theme.CornerPadded,
				},
				UI.new "Stroke" {},
			},

			UI.new "TextLabel" {
				AutoLocalize = false,
				AutomaticSize = Enum.AutomaticSize.X,
				BackgroundTransparency = 1,
				Size = UDim2.new(0, 0, 1, 0),
				RichText = true,
				FontFace = UI.Theme.Font,
				TextSize = UI.Theme.FontSize,
				TextColor3 = UI.Theme.PrimaryText,
				TextStrokeColor3 = UI.Theme.Primary,
				TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
				TextTruncate = Enum.TextTruncate.SplitWord,
				TextXAlignment = Enum.TextXAlignment.Left,
			},

			UI.new "ScrollingFrame" {
				AutomaticSize = Enum.AutomaticSize.X,
				AutomaticCanvasSize = Enum.AutomaticSize.X,
				BackgroundTransparency = 1,
				BorderSizePixel = 0,
				CanvasSize = UDim2.new(),
				Size = UDim2.new(0, 0, 1, 0),
				ScrollBarThickness = 0,
				UI.new "UIFlexItem" { FlexMode = Enum.UIFlexMode.Shrink },
				UI.new "UIListLayout" {
					VerticalAlignment = Enum.VerticalAlignment.Center,
					FillDirection = Enum.FillDirection.Horizontal,
					SortOrder = Enum.SortOrder.LayoutOrder,
					Padding = UI.Theme.Padding,
				},
				function()
					local chips = {}
					local target = item.target()
					for _, roleId in item.roles() do
						table.insert(chips, roleChip(roleId, target, item))
					end
					return chips
				end,
			},
			addButton,
		}

		return item
	end

	local function isDifferent(a: { string? }?, b: { string? }?)
		if not a ~= not b then
			return true
		end
		if a and b then
			if #a ~= #b then
				return true
			end
			for k, v in a do
				if b[k] ~= v then
					return true
				end
			end
		end
		return false
	end

	local function renderItem(self, item, userId)
		local member = _K.Data.members[userId]
		if member and member.renderText then
			local instance = item._instance
			instance.TextLabel.Text = member.renderText
			instance.ImageLabel.Image = `rbxthumb://type=AvatarHeadShot&id={member.userId}&w=48&h=48`
			item.target(member)
			if isDifferent(item.roles._value, member.roles) then
				item.roles(member.roles)
			end
		end
	end

	local scroller = UI.new "ScrollerFast" {
		Name = "Members",
		List = _K.client.members,
		Enabled = false,
		DictList = true,
		FilterInput = true,
		Visible = false,
		CreateItem = createItem,
		RenderItem = renderItem,
	}

	scroller._input._instance = UI.new "Frame" {
		Parent = scroller,
		Name = "FilterInput",

		BackgroundTransparency = 1,
		Size = function()
			return UDim2.new(0, 0, 0, UI.Theme.FontSize() + UI.Theme.PaddingDouble().Offset)
		end,

		UI.new "UIListLayout" {
			FillDirection = Enum.FillDirection.Horizontal,
			Padding = UI.Theme.PaddingWithStroke,
			SortOrder = Enum.SortOrder.LayoutOrder,
		},
		UI.edit(scroller._input, { UI.new "UIFlexItem" { FlexMode = Enum.UIFlexMode.Fill } }),
		newMemberButton,
	}

	local function updateCache(member)
		member.nameCache = member.name
		member.rolesCache = if member.roles then table.clone(member.roles) else nil
		member.persistCache = if member.persist then table.clone(member.persist) else nil

		if member.roles then
			local roles = {}
			for _, roleId in member.roles do
				local role = _K.Data.roles[roleId]
				if role then
					table.insert(roles, role.name)
				end
			end

			member.roleString = table.concat(roles, " | ") or ""
		else
			member.roleString = ""
		end

		member.richText = `<b>{member.name or "UNKNOWN"}</b> [{member.userId}]`
		member.rawText = `{member.name or "UNKNOWN"} [{member.userId}]`
		member.filterText = string.lower(`{member.rawText} {member.roleString}`)
	end

	local function filterTest(self, list)
		local filter = string.lower(scroller._input._input.Text)
		self._filter = filter

		local noFilter = string.find(self._filter, "^%s*$")

		local new = {}
		for _, userId in list do
			local member = _K.Data.members[userId]
			if not member then
				continue
			end

			if
				member.name ~= member.nameCache
				or isDifferent(member.roles, member.rolesCache)
				or isDifferent(member.persist, member.persistCache)
			then
				member.userId = userId
				updateCache(member)
			end

			if noFilter then
				member.renderText = member.richText
				table.insert(new, userId)
			else
				local filterFound = string.find(member.filterText, filter, 1, true)
				if filterFound then
					local pre = escape(string.sub(member.rawText, 1, filterFound - 1))
					local query = escape(string.sub(member.rawText, filterFound, filterFound + #filter - 1))
					local post = escape(string.sub(member.rawText, filterFound + #filter))
					member.renderText =
						`<font transparency="0.5">{pre}</font><b>{query}</b><font transparency="0.5">{post}</font>`
					table.insert(new, userId)
				end
			end
		end
		return new
	end

	task.defer(scroller.filter, scroller, filterTest)

	return scroller
end

return Members


local _K = require(script.Parent.Parent)
local UI = _K.UI

_K.client.updateInterfaceAuth = _K.Util.Function.debounce(0.2, function()
	local userId = _K.LocalPlayer.UserId
	local userRank = _K.Auth.getRank(userId)
	_K.client.rank(userRank)

	local commandBarRank = _K.client.settings.commandBarRank._value
	local dashboardRank = _K.client.settings.dashboardRank._value
	local commandsButtonRank = _K.client.settings.commandsButtonRank._value
	local dashboardButtonRank = _K.client.settings.dashboardButtonRank._value

	if commandBarRank == nil then
		commandBarRank = 0
	end
	if dashboardRank == nil then
		dashboardRank = 0
	end
	if dashboardButtonRank == nil then
		dashboardButtonRank = 0
	end

	_K.client.commandBarEnabled = commandBarRank and userRank >= commandBarRank
	_K.client.dashboardEnabled = dashboardRank and userRank >= dashboardRank
	_K.client.dashboardButtonEnabled = _K.client.dashboardEnabled
		and dashboardButtonRank
		and userRank >= dashboardButtonRank
	_K.client.commandsButtonEnabled = not _K.client.dashboardButtonEnabled
		and commandsButtonRank
		and userRank >= commandsButtonRank

	_K.client.toggleCommandBar._instance.Visible = _K.client.commandBarEnabled
	if not _K.client.commandBarEnabled and _K.client.CommandBar and _K.client.CommandBar.Bar.Visible then
		_K.client.CommandBar.hide()
	end

	if typeof(_K.client.dashboardButton) == "Instance" and _K.client.dashboardButton:IsA("GuiObject") then
		_K.client.dashboardButton.Visible = _K.client.dashboardButtonEnabled
	else
		_K.client.dashboardButton:setEnabled(_K.client.dashboardButtonEnabled)
	end

	if typeof(_K.client.commandsButton) == "Instance" and _K.client.commandsButton:IsA("GuiObject") then
		_K.client.commandsButton.Visible = _K.client.commandsButtonEnabled
	else
		_K.client.commandsButton:setEnabled(_K.client.commandsButtonEnabled)
	end

	if not _K.client.dashboardEnabled then
		_K.client.dashboard.Window._instance.Visible = false
	end

	_K.client.dashboard.Bans._instance:SetAttribute("Enabled", _K.Auth.hasCommand(userId, "bans"))
	_K.client.dashboard.Logs._instance:SetAttribute("Enabled", _K.Auth.hasCommand(userId, "logs"))
	_K.client.dashboard.Members._instance:SetAttribute("Enabled", _K.Auth.hasCommand(userId, "members"))
	_K.client.dashboard.Settings._instance:SetAttribute("Enabled", _K.Auth.hasCommand(userId, "settings"))

	for _, command in _K.Registry.commands do
		command.LocalPlayerAuthorized = _K.Auth.hasCommand(userId, command)
		if not command.RestrictedToRole then
			for rank, roleData in _K.Data.rolesList do
				if _K.Auth.roleCanUseCommand(roleData._key, command) then
					command.RestrictedToRole = roleData
					break
				end
			end
		end
	end
	_K.client.dashboard.Commands:updateList()
	_K.client.CommandBar.updateCompletionData()

	if _K.Registry.commands.tools and _K.Registry.commands.tools.LocalPlayerAuthorized then
		_K.Remote.Tools:Fire()
	end

	_K.Hook.authChanged:Fire()
end)

_K.client.settings.commandBarRank:Connect(_K.client.updateInterfaceAuth)
_K.client.settings.dashboardRank:Connect(_K.client.updateInterfaceAuth)
_K.client.settings.dashboardButtonRank:Connect(_K.client.updateInterfaceAuth)

_K.Remote.Announce:Connect(_K.Announce)
_K.Remote.Notify:Connect(_K.Notify)

_K.Remote.SetCore:Connect(function(parameter: string, value: any)
	_K.Service.StarterGui:SetCore(parameter, value)
end)

local requestingCommand = {}
_K.Remote.RequestCommand.OnInvoke = function(from: Player, text: string)
	if requestingCommand[from.UserId] then
		return
	end
	requestingCommand[from.UserId] = true

	local actionSignal = _K.Util.Signal.new()
	_K.Notify({
		Text = `\t<font transparency="0.5">{text}</font>`,
		ActionText = "Permit this action?",
		Action = true,
		ExitButton = false,
		LeftAction = true,
		RightAction = true,
		Duration = 30,

		_K.client.UserFrame(from.UserId, from.DisplayName),

		[UI.Hook] = {
			Action = function(v)
				actionSignal:Fire(v)
			end,
		},
	})

	task.delay(30, actionSignal.Fire, actionSignal, false)

	local action = actionSignal:Wait() == true
	requestingCommand[from.UserId] = nil
	return action
end

-- general data replication

_K.Remote.Ban:Connect(function(userId, ban)
	_K.Data.bans[userId] = ban
	_K.client.bans(_K.Data.bans)
end)

_K.Remote.Bans:Connect(function(bans)
	if bans then
		_K.Data.bans = bans
		_K.client.bans(_K.Data.bans)
	end
end)

_K.Remote.DisplayBubble:Connect(function(character: Model, message: string)
	_K.Service.TextChat:DisplayBubble(character, message)
end)

_K.Remote.Member:Connect(function(userId, member)
	local viewas = _K.Registry.commands.viewas
	if viewas.env.originalMember and userId == tostring(_K.LocalPlayer.UserId) then
		viewas.env.originalMember = member
		member = _K.Data.members[userId]
	end

	_K.Data.members[userId] = member
	_K.client.members(_K.Data.members)
	_K.client.updateInterfaceAuth()
end)

_K.Remote.Members:Connect(function(members)
	local viewas = _K.Registry.commands.viewas
	if viewas.env.originalMember then
		local key = tostring(_K.LocalPlayer.UserId)
		for userId, member in members do
			if userId == key then
				viewas.env.originalMember = member
				members[userId] = _K.Data.members[userId]
				break
			end
		end
	end

	_K.Data.members = members
	_K.client.members(_K.Data.members)
	_K.client.updateInterfaceAuth()
end)

_K.Remote.Log:Connect(function(log)
	if not _K.client.ready then
		return
	end

	table.insert(_K.Data.logs, log)

	_K.removeExcessLogs()

	task.defer(_K.client.dashboard.Logs.updateList, _K.client.dashboard.Logs)
end)

_K.Remote.Logs:Connect(function(logsBuffer)
	if not _K.client.ready then
		return
	end

	_K.Util.Defer.reset()

	local ok, result = pcall(_K.Z.des, _K.Data.Schema.logsRemote, logsBuffer)
	if not ok then
		warn(result)
		return
	end

	local logs = result
	_K.Util.Defer.wait()

	table.move(logs, 1, #logs, #_K.Data.logs + 1, _K.Data.logs)
	_K.Util.Defer.wait()

	table.sort(_K.Data.logs, _K.Logger.sortTime)
	_K.Util.Defer.wait()

	_K.removeExcessLogs()

	_K.client.dashboard.Logs:updateList()
end)

_K.Remote.Settings:Connect(function(settings)
	if settings.announcement and not _K.Data.savedSettings.announcement then
		local msg, from = unpack(settings.announcement)
		_K.Announce({
			From = from,
			Text = msg,
			Duration = 0,
		})
	end

	local useSaved = _K.Data.savedSettings.useSavedSettings or settings.useSavedSettings
	for key, value in settings do
		value = UI.raw(value)
		if
			key ~= "theme"
			and string.find(key :: string, "theme", 1, true) == 1
			and (_K.Data.settings.changeThemeAuthority == "Studio" or settings.theme ~= "Custom (DataStore)")
		then
			continue
		end
		_K.Data.savedSettings[key] = value
		if useSaved and _K.client.settings[key] then
			_K.client.settings[key](value)
		end
	end
	_K.client.updateInterfaceAuth()
end)

_K.Remote.Role:Connect(function(role, roleData)
	_K.Data.roles[role] = roleData
	_K.client.updateInterfaceAuth()
end)

_K.Remote.Roles:Connect(function(roles)
	_K.Util.Table.merge(_K.Data.roles, roles)
	_K.client.updateInterfaceAuth()
end)

_K.Remote.RolesNotification:Connect(function()
	_K.client.rolesNotification()
end)
return true


local _K = require(script.Parent.Parent)
local UI = _K.UI
local announcements = {}
local notifications = {}

local function resetOffset(announce: boolean?)
	local heightOffset = 0
	for i, list in ipairs(if announce then announcements else notifications) do
		local card, _, offset = unpack(list)
		offset(heightOffset, true)
		heightOffset += card.AbsoluteSize.Y + UI.Theme.Padding._value.Offset
	end
	return heightOffset
end

local function hide(info, announce: boolean?)
	local list = if announce then announcements else notifications
	local card, visible = unpack(info)
	local found = table.find(list, info)
	if not found then
		return
	end
	UI.Sound.Swipe:Play()

	table.remove(list, found)
	resetOffset(announce)
	card.ZIndex = -1
	visible(nil)
	task.delay(1 + UI.Theme.TweenOut._value.Time, card.Destroy, card)
end

return function(dialogOverride)
	if type(dialogOverride) ~= "table" then
		return
	end

	if not dialogOverride.Text then
		dialogOverride.Text = "No text provided"
	end

	local visible = UI.state(false)
	local offset = UI.state(0)
	local self

	local activated = dialogOverride.Activated
	local announce = dialogOverride.Announce
	local closeCallback = dialogOverride.Close
	local from = dialogOverride.From
	local name = dialogOverride.Name
	local image = dialogOverride.Image
	local sound = dialogOverride.Sound
	local userFrame = dialogOverride.UserFrame
	dialogOverride.Activated = nil
	dialogOverride.Announce = nil
	dialogOverride.Close = nil
	dialogOverride.From = nil
	dialogOverride.Name = nil
	dialogOverride.Image = nil
	dialogOverride.Sound = nil
	dialogOverride.UserFrame = nil

	local function close()
		if not self then
			return
		end
		if closeCallback then
			task.spawn(closeCallback, self)
		end
		hide(self, announce)
	end

	if announce and not dialogOverride.TextSize then
		dialogOverride.TextSize = _K.UI.Theme.FontSizeLarge
	end

	local dialog
	dialog = UI.new("Dialog")(_K.Util.Table.merge({
		Parent = UI.LayerTopInset,
		Close = close,
		Duration = 4 + 0.0625 * #dialogOverride.Text,
		ExitButton = true,

		Size = UDim2.fromScale(0.5, 0),
		AnchorPoint = if announce then Vector2.new(0.5, 1) else Vector2.new(1, 1),
		Position = if announce
			then UI.spring(function()
				local padding = UI.Theme.Padding().Offset
				local height = UI.TopbarInset().Height
				local y = if visible() then padding + offset() + dialog._instance.AbsoluteSize.Y else -height - 32
				return UDim2.new(0.5, 0, 0, y)
			end, 16, 0.5)
			else UI.spring(function()
				local padding = UI.Theme.Padding().Offset
				return UDim2.new(1, if visible() then -padding else 256 + 32, 1, -(padding + offset()))
			end, 16, 0.625),
		if announce
			then UI.new "UIPadding" {
				PaddingLeft = UI.Theme.Padding,
				PaddingRight = UI.Theme.Padding,
				PaddingTop = UI.Theme.Padding,
				PaddingBottom = UI.Theme.Padding,
			}
			else nil,
	}, dialogOverride))

	if type(activated) == "function" then
		dialog._instance.Active = true
		dialog._instance.Activated:Once(function()
			task.spawn(activated)
			close()
		end)
	end

	UI.new "UISizeConstraint" {
		Parent = dialog._instance,
		MaxSize = Vector2.new(if announce then 512 else 256, 9e9),
	}

	UI.Haptic.Notification:Play()

	if userFrame or from then
		userFrame = if userFrame then _K.client.UserFrame(unpack(userFrame)) else _K.client.UserFrame(from, name, image)
		UI.edit(userFrame, {
			Parent = dialog,

			UI.new "UIListLayout" {
				SortOrder = Enum.SortOrder.LayoutOrder,
				Padding = UDim.new(0, 4),
				FillDirection = Enum.FillDirection.Horizontal,
			},
			UI.new "Spacer" {
				LayoutOrder = 8,
			},
			dialog._exitButton,
		})
	end

	local list = if announce then announcements else notifications

	self = { dialog._instance, visible, offset, dialogOverride.Duration }
	table.insert(list, 1, self)
	task.defer(function()
		visible(true)
	end)

	if resetOffset(announce) > UI.LayerTopInset.AbsoluteSize.Y / (if announce then 2 else 1) then
		for index = #list, 1, -1 do
			local _card, _visible, _offset, delay = unpack(list[index])
			if delay == 0 then
				continue
			end
			hide(list[index], announce)
			break
		end
	end

	UI.Sound[sound or if announce then "Notification_Low" else "Notification_High"]:Play()

	return dialog
end


local UI = require(script.Parent:WaitForChild("UI"))

local Servers = {}
Servers.__index = Servers

function Servers.new(_K)
	local servers = _K.Data.servers

	local placeNames = {}
	local function getPlaceName(placeId: number)
		if placeNames[placeId] then
			return placeNames[placeId]
		end

		local success, info = pcall(function()
			return _K.Service.Marketplace:GetProductInfo(placeId)
		end)

		local name = if success then info.Name else "Unknown"
		placeNames[placeId] = name

		return name
	end

	_K.Remote.Servers:Connect(function(jobId: string?, data: any?)
		if not jobId then -- bulk update
			if type(data) ~= "table" then
				return
			end
			table.clear(servers)
			for k, v in data do
				servers[k] = v
			end
		else -- single update
			servers[jobId] = data
		end
		_K.client.servers(servers)
	end)

	local merge = _K.Util.Table.merge

	local buttonSize = UI.compute(function()
		return UDim2.new(0.2, 0, 1, -UI.Theme.Padding().Offset)
	end)

	local function icon(properties)
		return UI.new "ImageLabel"(merge({
			BackgroundTransparency = 1,
			Size = UDim2.new(0.5, 0, 0.5, 0),
			SizeConstraint = Enum.SizeConstraint.RelativeYY,
			ImageColor3 = UI.Theme.PrimaryText,
			ScaleType = Enum.ScaleType.Fit,
		}, properties))
	end

	local function label(properties)
		return UI.new "TextLabel"(merge({
			AutoLocalize = false,
			AutomaticSize = Enum.AutomaticSize.X,
			BackgroundTransparency = 1,
			Size = UDim2.new(0, 0, 1, 0),
			RichText = true,
			FontFace = UI.Theme.FontMono,
			TextColor3 = UI.Theme.PrimaryText,
			TextSize = UI.Theme.FontSize,
			TextStrokeColor3 = UI.Theme.Primary,
			TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
			TextTruncate = Enum.TextTruncate.SplitWord,
			TextXAlignment = Enum.TextXAlignment.Left,
			TextYAlignment = Enum.TextYAlignment.Center,
		}, properties))
	end

	local function createItem(self, server)
		local lineItem = {} :: any

		lineItem._instance = UI.new "Frame" {
			BackgroundTransparency = 1,
			Size = self.ItemSize,

			UI.new "UIListLayout" {
				Padding = UI.Theme.Padding,
				FillDirection = Enum.FillDirection.Horizontal,
				SortOrder = Enum.SortOrder.LayoutOrder,
				VerticalAlignment = Enum.VerticalAlignment.Center,
			},

			icon { Image = UI.Theme.Image.Host },
			label { Name = "Id" },

			icon { Image = UI.Theme.Image.Graph1 },
			label {
				Name = "Place",
				FontFace = UI.Theme.Font,
				UI.new "UIFlexItem" {
					FlexMode = Enum.UIFlexMode.Shrink,
				},
			},

			UI.new "Spacer" {},

			icon { Image = UI.Theme.Image.Hourglass_Bottom },
			label { Name = "Age" },

			icon { Image = UI.Theme.Image.Play },
			label { Name = "Count" },

			UI.new "Button" {
				Label = "Join",
				Size = buttonSize,

				UI.new "UIFlexItem" {
					FlexMode = Enum.UIFlexMode.Shrink,
				},

				Activated = function()
					if lineItem.placeId and lineItem.jobId then
						_K.Util.SafeTeleport(lineItem.placeId, lineItem.jobId)
					end
				end,
			},
		}

		return lineItem
	end

	local playerCountFormat = `%-{#tostring(_K.Service.Players.MaxPlayers)}d`

	local function renderItem(self, lineItem, jobId)
		local now = tick()
		lineItem.lastRender = now
		lineItem.jobId = jobId

		local data = servers[jobId]
		if data then
			local placeId, playerCount, startTime, _lastUpdate = unpack(data)

			lineItem.placeId = placeId

			lineItem._instance.Id.Text = string.sub(jobId, -6, -1)
			lineItem._instance.Count.Text = string.format(playerCountFormat, playerCount)

			task.spawn(function()
				local name = getPlaceName(placeId)
				if lineItem.lastRender == now then
					lineItem._instance.Place.Text = name
				end
			end)

			task.spawn(function()
				while lineItem.lastRender == now do
					lineItem._instance.Age.Text = _K.Util.Time.clock(DateTime.now().UnixTimestamp - startTime)
					task.wait(1)
				end
			end)
		end
	end

	local scroller = UI.new "ScrollerFast" {
		Name = "Servers",
		List = _K.client.servers,
		DictList = true,
		Enabled = false,
		Visible = false,
		ItemSize = function()
			return UDim2.new(1, 0, 0, UI.Theme.FontSize * 2 + UI.Theme.Padding().Offset)
		end,
		CreateItem = createItem,
		RenderItem = renderItem,
	}

	task.defer(scroller.filter, scroller, function(self, list)
		return list
	end)

	task.defer(scroller.sort, scroller, function(a, b)
		local A, B = servers[a], servers[b]
		if A and B then
			if A[1] == B[1] then
				return A[2] > B[2]
			else
				return A[1] < B[1]
			end
		end
		return a < b
	end)

	return scroller
end

return Servers


local UI = require(script.Parent:WaitForChild("UI"))

local Settings = { _K = nil }
Settings.__index = Settings

local filterRichFormat = `<font transparency="0.5">%s</font><b>%s</b><font transparency="0.5">%s</font>`

function Settings.new(_K)
	local settingsDialog
	local originalValues = {}
	local pendingChanges = {}

	local function resetChanges()
		for key, value in originalValues do
			_K.Data.savedSettings[key] = value
			if _K.client.settings[key] then
				_K.client.settings[key](value, true)
			end
		end
		table.clear(originalValues)
		table.clear(pendingChanges)
	end

	local function confirmChanges()
		_K.Remote.Settings:Fire(pendingChanges)
		for key, value in pendingChanges do
			_K.Data.savedSettings[key] = value
			if _K.client.settings[key] then
				_K.client.settings[key](value, true)
			end
		end
		table.clear(originalValues)
		table.clear(pendingChanges)
	end

	local function change(key, value, current, skipCustomCheck)
		if current == nil and string.find(key, "theme", 1, true) == 1 then
			current = UI.Theme[string.sub(key, 6)]
		end

		if not _K.client.ready or _K.Util.Table.deepCompare(value, UI.raw(current)) then
			return
		end

		if originalValues[key] == value then
			pendingChanges[key] = nil
		elseif _K.Data.savedSettings[key] ~= value then
			pendingChanges[key] = value
			if not originalValues[key] then
				local original = _K.Data.savedSettings[key]
				originalValues[key] = if type(original) == "table" then table.clone(original) else original
			end
		end

		if current then
			current(value)
		end

		local theme = _K.Data.savedSettings.theme
		local themeModule = UI.Themes[theme]

		local different = false
		local differentTheme = false
		for k, v in pendingChanges do
			if
				themeModule
				and k ~= "theme"
				and string.find(k :: string, "theme", 1, true) == 1
				and UI.raw(themeModule[string.sub(k :: string, 6)]) ~= v
			then
				differentTheme = true
			end
			if v ~= originalValues[k] then
				different = true
			end
		end

		if settingsDialog then
			settingsDialog._instance.Visible = different
		end

		if key ~= "theme" and string.find(key :: string, "theme", 1, true) == 1 and not skipCustomCheck then
			local newTheme = if differentTheme then "Custom (DataStore)" else theme
			if pendingChanges.theme ~= newTheme then
				change("theme", newTheme)
			end
		elseif key == "theme" then
			themeModule = UI.Themes[value]
			if themeModule then
				_K.Data.savedSettings.theme = value
				-- FIX: update images and sounds
				for key, value in themeModule do
					local dataKey = "theme" .. key
					local existing = _K.Data.savedSettings[dataKey]
					if existing ~= nil and existing ~= UI.raw(value) then
						change(dataKey, UI.raw(value), nil, true)
					end
				end
			end
			_K.client.settings.theme(value)
		end
	end

	local ignoredDefaultSettings = {
		announcement = true,
		announcements = true,
		notifications = true,
		freeAdmin = true,
		prefix = true,
	}

	local settingsScroller = UI.new "Scroller" {
		UI.new "UIFlexItem" {
			FlexMode = Enum.UIFlexMode.Fill,
		},
		UI.new "Button" {
			BackgroundColor3 = UI.Theme.Invalid,
			LayoutOrder = 2 ^ 31 - 1,
			Label = "Restore Default Settings",
			Visible = _K.client.settings.useSavedSettings,

			Activated = function()
				for k, v in _K.Data.defaultSettings do
					if not ignoredDefaultSettings[k] and _K.Data.savedSettings[k] ~= v then
						change(k, v)
						if _K.client.settings[k] then
							_K.client.settings[k](v)
						end
					end
				end
			end,
		},
	}

	local lineItemHeight = UI.compute(function()
		return UI.Theme.FontSize() + UI.Theme.Padding().Offset
	end)

	local inputSize = UI.compute(function()
		return UDim2.new(0, 0, 0, lineItemHeight())
	end)

	local themeLineItems = { Name = "Appearance" }

	local rankChoices = table.clone(_K.Data.rolesList)
	local disabledRankChoice = { _rank = false, name = "Disabled" }
	table.insert(rankChoices, disabledRankChoice)

	local function rankItem(setting: string, text: string, tooltip: string, callback)
		return UI.new "ListItem" {
			Text = text,
			Tooltip = tooltip,
			UI.new "Select" {
				Value = function()
					local current = _K.client.settings[setting]()
					return if current then _K.Auth.getRoleFromRank(current) else disabledRankChoice
				end,
				Choices = rankChoices,
				[UI.Hook] = {
					Value = function(value)
						change(setting, value._rank, _K.client.settings[setting])
						if callback then
							callback()
						end
					end,
				},
			},
		}
	end

	local function switchItem(setting: string, text: string, tooltip: string?)
		return UI.new "ListItem" {
			Text = text,
			Tooltip = tooltip,
			UI.new "Switch" {
				Value = function()
					return _K.client.settings[setting]()
				end,
				[UI.Hook] = {
					Value = function(value)
						change(setting, value, _K.client.settings[setting])
					end,
				},
			},
		}
	end

	local function _sliderItem(setting: string, text: string, tooltip: string?)
		return UI.new "ListItem" {
			Text = text,
			Tooltip = tooltip,
			UI.new "Slider" {
				Value = function()
					return _K.client.settings[setting]()
				end,
				[UI.Hook] = {
					Value = function(value)
						change(setting, value, _K.client.settings[setting])
					end,
				},
			},
		}
	end

	local freeAdminCache = {}
	local reservedPrefixes = { ",", " ", "`", '"', "@", "$", "%", "*", "~" }
	local settingsLineItems = {
		{
			Name = "Admin",
			UI.new "ListItem" {
				Text = "Command Prefix",
				Tooltip = "The character to trigger a command.",
				UI.new "Input" {
					FontFace = UI.Theme.FontMono,
					FontSize = UI.Theme.FontSize,
					Size = inputSize,
					Placeholder = ";",
					MaxChars = 1,
					Value = function()
						return _K.client.settings.prefix()[1]
					end,
					Validate = function(text)
						return not (text == "" or table.find(reservedPrefixes, text))
					end,

					[UI.Hook] = {
						Value = function(value)
							if value == _K.client.settings.prefix()[1] then
								return
							end
							_K.client.settings.prefix._value[1] = value
							change("prefix", { value, unpack(_K.client.settings.prefix._value, 2) })
						end,
					},
				},
			},
			UI.new "ListItem" {
				Text = "Free Admin",
				Tooltip = "🚨 Role given to every player who joins",
				UI.new "Select" {
					Value = function()
						local freeAdmin = _K.client.settings.freeAdmin()
						return if type(freeAdmin) == "table" and freeAdmin[1]
							then _K.Data.roles[freeAdmin[1]]
							else disabledRankChoice
					end,
					Choices = rankChoices,
					[UI.Hook] = {
						Value = function(value)
							local roleId = value._key
							value = freeAdminCache[roleId]
							if not value then
								value = { roleId }
								if roleId ~= nil then
									freeAdminCache[roleId] = value
								end
							end

							local freeAdmin = _K.client.settings.freeAdmin()
							local current = if type(freeAdmin) == "table" and freeAdmin[1]
								then _K.Data.roles[freeAdmin[1]]
								else disabledRankChoice

							if roleId == current._key then
								return
							end

							change("freeAdmin", value)
							_K.client.settings.freeAdmin(
								_K.Util.Table.merge(table.clone(_K.Data.defaultSettings.freeAdmin), value)
							)
						end,
					},
				},
			},
			rankItem("commandBarRank", "Command Bar Rank", "Minimum role to use the Command Bar"),
			rankItem("dashboardRank", "Dashboard Rank", "Minimum role to use the Dashboard", function()
				if _K.client.settings.dashboardRank() == false then
					_K.Process.runCommands(_K, _K.LocalPlayer.UserId, ";settings")
				end
			end),
			rankItem("commandsButtonRank", "Commands Button Rank", "Minimum role to show the Commands Button"),
			rankItem("dashboardButtonRank", "Dashboard Button Rank", "Minimum role to show the Dashboard Button"),
			rankItem(
				"joinNotificationRank",
				"Join Notification Rank",
				"Minimum role to show the default join notification"
			),
			switchItem(
				"addToCharts",
				"Add Game to Charts",
				"Adds the game to a list of games using Kohl's Admin, check About for details."
			),
			switchItem(
				"vip",
				"Global VIP Features",
				"Toggles VIP features for the admin.\n<b>Required if game is added to charts!</b>"
			),
			switchItem("chatCommands", "Chat Commands", "Toggle commands via chat"),
			switchItem(
				"chatCommandsNotification",
				"Chat Commands Notification",
				"Shows users a notification to purchase VIP (non-abusable) chat commands on join, 40% of the purchase goes to the game creator."
			),
			switchItem("commandRequests", "Command Requests", "Toggle commands requests on same or higher rank users"),
			switchItem("onlyShowUsableCommands", "Only Show Usable Commands", "Show only purchasable/usable commands"),
			switchItem(
				"getKohlsAdminPopup",
				"Get Kohl's Admin Popup",
				"Shows users a popup after using a few commands to get the Kohl's Admin model"
			),
			switchItem(
				"wrongPrefixWarning",
				"Wrong Prefix Warning",
				"Warns users when the wrong command prefix is used"
			),
			switchItem("saveLogs", "Save Logs", "Toggle the saving and syncing of logs across servers"),
			switchItem("saveChatLogs", "Save Chat Logs", "Toggle the saving and syncing of chat logs across servers"),
			switchItem(
				"saveLogsForAllRoles",
				"Save Logs For All Roles",
				"Toggle the saving and syncing of command logs for all roles, instead of only logging non-purchasable restricted roles"
			),
		},

		{
			Name = "Local",
			UI.new "ListItem" {
				Text = "Custom Command Prefix",
				Tooltip = "A custom character to trigger a command, only affects the client.",
				UI.new "Input" {
					FontFace = UI.Theme.FontMono,
					FontSize = UI.Theme.FontSize,
					Size = inputSize,
					Placeholder = ";",
					MaxChars = 1,
					Value = function()
						return _K.client.playerPrefix() or _K.client.settings.prefix()[1]
					end,
					Validate = function(text)
						return not (text == "" or table.find(reservedPrefixes, text))
					end,

					[UI.Hook] = {
						Value = function(value)
							if value == _K.client.playerPrefix() then
								return
							end
							_K.client.playerPrefix(value)
							_K.Remote.PlayerPrefix:Fire(value)
						end,
					},
				},
			},
			UI.new "ListItem" {
				Text = "Command Bar Hotkey",
				Tooltip = "The keyboard shortcut to open the command bar.",
				UI.new "Input" {
					FontFace = UI.Theme.FontMono,
					FontSize = UI.Theme.FontSize,
					Size = inputSize,
					Placeholder = ";",
					HotkeyInput = true,
					Value = UI.UserInputService:GetStringForKeyCode(_K.client.hotkeys.commandBar.key._value),
					Hotkey = _K.client.hotkeys.commandBar.key,
					Modifiers = _K.client.hotkeys.commandBar.mods,
				},
			},
			UI.new "ListItem" {
				Text = "Dashboard Hotkey",
				Tooltip = "The keyboard shortcut to open the dashboard.",
				UI.new "Input" {
					FontFace = UI.Theme.FontMono,
					FontSize = UI.Theme.FontSize,
					Size = inputSize,
					Placeholder = "'",
					HotkeyInput = true,
					Value = UI.UserInputService:GetStringForKeyCode(_K.client.hotkeys.dashboard.key._value),
					Hotkey = _K.client.hotkeys.dashboard.key,
					Modifiers = _K.client.hotkeys.dashboard.mods,
				},
			},
		},

		themeLineItems,

		{
			Name = "Sound",
			switchItem("themeSoundEnabled", "UI Sounds"),
			switchItem("themeTypingSounds", "Typing sounds"),
			switchItem("themeTypingSoundsOnEveryTextBox", "Typing sounds on every TextBox"),
		},
	}

	task.defer(function()
		_K.Util.Defer.reset()

		-- Theme select
		local themeList = UI.compute(function()
			local themes = {}
			for key in UI.Themes do
				table.insert(themes, key)
			end
			table.sort(themes)
			table.insert(themes, "Custom (DataStore)")
			return themes
		end)
		table.insert(
			themeLineItems,
			UI.new "ListItem" {
				Text = "Theme",
				Tooltip = "Changes the theme of Kohl's Admin interfaces.",
				UI.new "Select" {
					Value = function()
						return _K.client.settings.theme()
					end,
					Choices = themeList,
					[UI.Hook] = {
						Value = function(value)
							change("theme", value)
						end,
					},
				},
			}
		)

		-- font faces
		do
			local lines = { Font = nil, FontMono = nil }
			for _, key in { "Font", "FontMono" } do
				lines[key] = UI.new "ListItem" {
					Text = key:gsub("(.)(%u)", "%1 %2"),
					UI.new "TextLabel" {
						BackgroundTransparency = 1,
						AutomaticSize = Enum.AutomaticSize.XY,
						FontFace = UI.Theme[key],
						Text = "Loading Fonts...",
						TextColor3 = UI.Theme.PrimaryText,
						TextSize = UI.Theme.FontSize,
						TextTransparency = UI.Theme.TransparencyBalanced,
					},
				}
				table.insert(themeLineItems, lines[key])
			end

			task.defer(function()
				while true do
					local dashboardView = _K.client.dashboard.Tabs
						and _K.client.dashboard.Tabs.CurrentPage._value == _K.client.dashboard.Settings._instance
					local windowView = _K.client.dashboard.Settings._instance.Parent
						and not _K.client.dashboard.Settings._instance:IsDescendantOf(_K.client.dashboard.Tabs._content)
					if dashboardView or windowView then
						break
					end
					task.wait()
				end

				local fonts = {}
				local mono = {
					16658246179,
					Enum.Font.Code,
					12187606783,
					Enum.Font.Arcade,
					Enum.Font.RobotoMono,
					12187371840,
					12187362578,
				}
				for id in UI.Fonts do
					table.insert(fonts, Font.fromId(tonumber(id) :: number))
				end
				for i, font in mono do
					mono[i] = if type(font) == "number" then Font.fromId(font) else Font.fromEnum(font)
				end
				for i, font in Enum.Font:GetEnumItems() do
					if font.Name == "Unknown" then
						continue
					end
					table.insert(fonts, Font.fromEnum(font))
				end
				table.sort(fonts, function(a, b)
					return UI.getFontName(a) < UI.getFontName(b)
				end)

				task.spawn(function()
					local temp = Instance.new("Frame")
					temp.BackgroundTransparency = 1
					temp.Parent = UI.LayerTop

					for _, key in { "Font", "FontMono" } do
						task.spawn(function()
							local list = if key == "Font" then fonts else mono
							local loader = (if key == "Font" then lines.Font else lines.FontMono)._content.TextLabel
							local content = {}
							for i = 1, #list, 8 do
								for j = 1, 8 do
									local font = list[i + j]
									if not font then
										break
									end
									local label = Instance.new("TextLabel")
									label.BackgroundTransparency = 1
									label.TextTransparency = 1
									label.FontFace = font
									label.Parent = temp
									content[j] = label
								end

								loader.Text = `Loading fonts... {i}/{#list}`
								_K.Service.ContentProvider:PreloadAsync(content)
							end
							table.clear(content)

							UI.new "Select" {
								Parent = lines[key],
								FontFace = UI.Theme[key],
								Value = function()
									return UI.Theme[key]()
								end,
								Choices = list,
								[UI.Hook] = {
									Value = function(value)
										change("theme" .. key, value)
									end,
								},
							}

							loader:Destroy()
							if key == "Font" then
								temp:Destroy()
							end
						end)
					end
				end)
			end)
		end
		_K.Util.Defer.wait()

		local lastSelected
		local lastSelectedY = 0
		settingsScroller._instance:GetPropertyChangedSignal("AbsoluteCanvasSize"):Connect(function()
			if not lastSelected then
				return
			end
			local change, selectedY = 0, lastSelected.AbsolutePosition.Y + lastSelected.AbsoluteSize.Y / 2
			if settingsScroller._instance.CanvasPosition.Y > 0 then
				change = selectedY - lastSelectedY
				settingsScroller._instance.CanvasPosition += Vector2.new(0, change)
			end
			lastSelectedY = selectedY - change
		end)

		do -- Font Size
			local slider
			slider = UI.new "Slider" {
				Snap = 25,
				Value = function()
					return math.clamp(UI.Theme.FontSize() - 7, 1, 25)
				end,
				UI.new "UIFlexItem" {
					FlexMode = Enum.UIFlexMode.Fill,
				},
				[UI.Hook] = {
					Value = function(value)
						if value + 7 ~= UI.Theme.FontSize() then
							lastSelected = slider._instance
							lastSelectedY = lastSelected.AbsolutePosition.Y + lastSelected.AbsoluteSize.Y / 2
						end
						change("themeFontSize", value + 7)
					end,
				},
			}
			local listItem = UI.new "ListItem" {
				Text = "Font Size",
				UI.new "Input" {
					FontFace = UI.Theme.FontMono,
					FontSize = UI.Theme.FontSize,
					Size = inputSize,
					NumberOnly = true,
					NumberRange = NumberRange.new(8, 32),
					Placeholder = "",
					Value = function()
						return UI.Theme.FontSize()
					end,
					[UI.Hook] = {
						Value = function(value)
							local number = tonumber(value)
							if not number then
								return
							end
							change("themeFontSize", number)
						end,
					},
				},
				slider,
			}
			listItem._label.Size = UDim2.new(0.5, 0, 0, 0)
			table.insert(themeLineItems, listItem)
		end
		_K.Util.Defer.wait()

		-- UDim options
		for i, key in { "CornerRadius", "Padding" } do
			local slider
			slider = UI.new "Slider" {
				Snap = 17,
				Value = function()
					return math.clamp(UI.Theme[key]().Offset + 1, 1, 17)
				end,
				UI.new "UIFlexItem" {
					FlexMode = Enum.UIFlexMode.Fill,
				},
				[UI.Hook] = {
					Value = function(value)
						if value == math.clamp(UI.Theme[key]().Offset + 1, 1, 17) then
							return
						end
						lastSelected = slider._instance
						lastSelectedY = lastSelected.AbsolutePosition.Y + lastSelected.AbsoluteSize.Y / 2
						value = UDim.new(0, value - 1)
						change("theme" .. key, value)
					end,
				},
			}
			local listItem = UI.new "ListItem" {
				Text = key:gsub("(.)(%u)", "%1 %2"),
				UI.new "Input" {
					FontFace = UI.Theme.FontMono,
					FontSize = UI.Theme.FontSize,
					Size = inputSize,
					NumberOnly = true,
					NumberRange = NumberRange.new(0, 16),
					Placeholder = "",
					Value = function()
						return UI.Theme[key]().Offset
					end,
					[UI.Hook] = {
						Value = function(value)
							local number = tonumber(value)
							if not number then
								return
							end
							value = UDim.new(0, number)
							change("theme" .. key, value)
						end,
					},
				},
				slider,
			}
			listItem._label.Size = UDim2.new(0.5, 0, 0, 0)
			table.insert(themeLineItems, listItem)
		end
		_K.Util.Defer.wait()

		do -- Background Image
			table.insert(
				themeLineItems,
				UI.new "ListItem" {
					Text = "Background Image AssetId",
					Tooltip = "Changes the Background Image of windows and dialogs.",
					UI.new "Input" {
						FontFace = UI.Theme.FontMono,
						FontSize = UI.Theme.FontSize,
						Size = inputSize,
						NumberOnly = true,
						NumberRange = NumberRange.new(0, math.huge),
						Placeholder = "12345",
						Value = function()
							return tonumber(string.match(UI.Theme.BackgroundImage(), "(%d+)$")) or 0
						end,
						[UI.Hook] = {
							Value = function(value)
								print(value)
								local number = tonumber(value)
								if not number then
									return
								end
								change("themeBackgroundImage", "rbxassetid://" .. value)
							end,
						},
					},
				}
			)

			local scaleTypes = { Enum.ScaleType.Crop, Enum.ScaleType.Fit, Enum.ScaleType.Stretch }

			table.insert(
				themeLineItems,
				UI.new "ListItem" {
					Text = "Background Image ScaleType",
					Tooltip = "Changes the ScaleType of the Background Image.",
					UI.new "Select" {
						Value = function()
							return _K.client.settings.themeBackgroundImageScaleType()
						end,
						Choices = scaleTypes,
						[UI.Hook] = {
							Value = function(value)
								change("themeBackgroundImageScaleType", value)
							end,
						},
					},
				}
			)
		end

		do -- Transparency
			for _, data in
				{
					{ key = "BackgroundImageTransparency", text = "Background Image Transparency" },
					{ key = "Transparency", text = "Background Transparency" },
					{ key = "TextStrokeTransparency", text = "Text Stroke Transparency" },
				}
			do
				local slider
				slider = UI.new "Slider" {
					Value = function()
						return UI.Theme[data.key]()
					end,
					UI.new "UIFlexItem" {
						FlexMode = Enum.UIFlexMode.Fill,
					},
					[UI.Hook] = {
						Value = function(value)
							change("theme" .. data.key, value)
						end,
					},
				}
				local listItem = UI.new "ListItem" {
					Text = data.text,
					UI.new "Input" {
						FontFace = UI.Theme.FontMono,
						FontSize = UI.Theme.FontSize,
						Size = inputSize,
						MaxChars = 5,
						NumberOnly = true,
						NumberRange = NumberRange.new(0, 1),
						Placeholder = "",
						Value = function()
							return string.sub(UI.Theme[data.key](), 1, 5)
						end,
						[UI.Hook] = {
							Value = function(value)
								local number = tonumber(value)
								if not tonumber(value) or math.abs(number - UI.Theme[data.key]()) < 0.001 then
									return
								end
								change("theme" .. data.key, number)
							end,
						},
					},
					slider,
				}
				listItem._label.Size = UDim2.new(0.5, 0, 0, 0)
				table.insert(themeLineItems, listItem)
			end
		end

		-- Color options
		for index, key in
			{
				"Border",
				"BackgroundGradient",
				"Primary",
				"PrimaryText",
				"Secondary",
				"SecondaryText",
				"Valid",
				"Invalid",
			}
		do
			_K.Util.Defer.wait()
			local color = UI.new "Color" {
				Value = function()
					return UI.Theme[key]()
				end,
				[UI.Hook] = {
					Value = function(value)
						change("theme" .. key, value)
					end,
				},
			}
			local listItem = UI.new "ListItem" {
				Text = key:gsub("(.)(%u)", "%1 %2") .. " Color",
				color,
			}
			if key == "Border" then
				listItem.Text("UI Borders")
				UI.new "Tooltip" { Parent = color, Text = "Border Color Start" }
				UI.new "Switch" {
					Parent = listItem,
					LayoutOrder = -1,
					Value = function()
						return _K.client.settings.themeStrokeEnabled()
					end,
					UI.new "Tooltip" { Text = "Toggle UI Borders" },
					[UI.Hook] = {
						Value = function(value)
							change("themeStrokeEnabled", value, _K.client.settings.themeStrokeEnabled)
						end,
					},
				}
				local input = UI.new "Input" {
					Parent = listItem,
					LayoutOrder = 0,
					FontFace = UI.Theme.FontMono,
					FontSize = UI.Theme.FontSize,
					Size = inputSize,
					MaxChars = 5,
					NumberOnly = true,
					NumberRange = NumberRange.new(-360, 360),
					Placeholder = "",
					Value = function()
						return UI.Theme.BorderAngle()
					end,
					[UI.Hook] = {
						Value = function(value)
							local number = tonumber(value)
							if not tonumber(value) or math.abs(number - UI.Theme.BorderAngle()) < 0.001 then
								return
							end
							change("themeBorderAngle", number)
						end,
					},
				}
				UI.new "Tooltip" { Parent = input._displayLabel, Text = "Border Rotation" }
				UI.new "Color" {
					Parent = listItem,
					LayoutOrder = 2,
					Value = function()
						return UI.Theme[key .. "Stop"]()
					end,
					UI.new "Tooltip" { Text = "Border Color Stop" },
					[UI.Hook] = {
						Value = function(value)
							change("theme" .. key .. "Stop", value)
						end,
					},
				}
			elseif key == "BackgroundGradient" then
				listItem.Text("Background Gradient")
				UI.new "Tooltip" { Parent = color, Text = "Gradient Color Start" }
				UI.new "Switch" {
					Parent = listItem,
					LayoutOrder = -1,
					Value = function()
						return _K.client.settings.themeBackgroundGradientEnabled()
					end,
					UI.new "Tooltip" { Text = "Toggle Background Gradients" },
					[UI.Hook] = {
						Value = function(value)
							change(
								"themeBackgroundGradientEnabled",
								value,
								_K.client.settings.themeBackgroundGradientEnabled
							)
						end,
					},
				}
				local input = UI.new "Input" {
					Parent = listItem,
					LayoutOrder = 0,
					FontFace = UI.Theme.FontMono,
					FontSize = UI.Theme.FontSize,
					Size = inputSize,
					MaxChars = 5,
					NumberOnly = true,
					NumberRange = NumberRange.new(-360, 360),
					Placeholder = "",
					Value = function()
						return UI.Theme.BackgroundGradientAngle()
					end,
					[UI.Hook] = {
						Value = function(value)
							local number = tonumber(value)
							if not tonumber(value) or math.abs(number - UI.Theme.BackgroundGradientAngle()) < 0.001 then
								return
							end
							change("themeBackgroundGradientAngle", number)
						end,
					},
				}
				UI.new "Tooltip" { Parent = input._displayLabel, Text = "Gradient Rotation" }
				UI.new "Color" {
					Parent = listItem,
					LayoutOrder = 2,
					Value = function()
						return UI.Theme[key .. "Stop"]()
					end,
					UI.new "Tooltip" { Text = "Gradient Color Stop" },
					[UI.Hook] = {
						Value = function(value)
							change("theme" .. key .. "Stop", value)
						end,
					},
				}
			else
				UI.new "Tooltip" { Parent = color, Text = listItem.Text() }
			end
			table.insert(themeLineItems, listItem)
		end

		local savedSettingsSwitch =
			switchItem("useSavedSettings", "Use Saved Settings", "Toggles using saved or studio-only settings.")
		UI.edit(savedSettingsSwitch._label, {
			FontFace = UI.Theme.FontBold,
		})
		UI.edit(savedSettingsSwitch, { LayoutOrder = -1, Parent = settingsScroller })

		for layoutOrder, definition in ipairs(settingsLineItems) do
			settingsLineItems[definition.Name] = UI.new("List")(_K.Util.Table.merge({
				Parent = settingsScroller,
				Label = definition.Name,
				Collapsible = true,
				Padding = UI.Theme.PaddingDouble,
				LayoutOrder = layoutOrder,
			}, definition))
		end

		local function updateSettingsAuth()
			local settingsAuth = _K.Auth.hasPermission(_K.LocalPlayer.UserId, "settings")
			local themeAuthority = _K.Data.settings.changeThemeAuthority
			settingsScroller._instance.Appearance.Visible = themeAuthority == "Client"
				or (themeAuthority == "Server" and settingsAuth)
			settingsScroller._instance.Admin.Visible = settingsAuth and _K.client.settings.useSavedSettings._value
			savedSettingsSwitch._instance.Visible = settingsAuth
		end
		_K.Hook.authChanged:Connect(updateSettingsAuth)
		_K.client.settings.useSavedSettings:Connect(updateSettingsAuth)
		updateSettingsAuth()
	end)

	local function close(self)
		self.Activated(false)
	end

	settingsDialog = UI.new "Dialog" {
		BackgroundTransparency = UI.Theme.TransparencyHeavy,
		BackgroundColor3 = UI.Theme.Secondary,
		AutomaticSize = Enum.AutomaticSize.Y,
		Size = function()
			return UDim2.new(1, -UI.Theme.Padding().Offset - 8, 0, 0)
		end,
		ActionText = "Save changes"
			.. (_K.Data.settings.changeThemeAuthority == "Server" and " and update for everyone?" or "?"),
		Action = true,
		LeftAction = true,
		RightAction = true,
		Visible = false,
		Close = function(self)
			task.defer(close, self)
		end,

		[UI.Hook] = {
			Action = function(v)
				settingsDialog._instance.Visible = false
				if v then
					confirmChanges()
				else
					resetChanges()
				end
			end,
		},
	}
	settingsDialog._content:Destroy()

	local filterInput = UI.new "Input" {
		ClearTextButton = true,
		Fill = true,
		Placeholder = "Search",
		Icon = UI.Theme.Image.Search,
		IconProperties = {
			ImageColor3 = UI.Theme.PrimaryText,
			Size = UDim2.fromOffset(18, 18),
		},
	}

	local function filterSettings()
		local filter = string.lower(filterInput._input.Text)
		for _, lineItems in ipairs(settingsLineItems) do
			local list = settingsScroller._instance[lineItems.Name]
			if filter == "" then
				list.Visible = true
				for _, lineItem in ipairs(lineItems) do
					lineItem.Text(lineItem._instance.Name)
					lineItem._instance.Visible = true
				end
				continue
			end

			local listFound = string.find(string.lower(lineItems.Name), filter :: string, 1, true)
			if listFound then
				list.Visible = true
				settingsLineItems[lineItems.Name].Collapsed(false)
			else
				list.Visible = false
			end

			for _, lineItem in ipairs(lineItems) do
				local rawText = lineItem._instance.Name
				local found = string.find(string.lower(rawText), filter :: string, 1, true)
				if found then
					list.Visible = true
					settingsLineItems[lineItems.Name].Collapsed(false)
				end
				local text = if found
					then string.format(
						filterRichFormat,
						_K.Util.String.escapeRichText(string.sub(rawText, 1, found - 1)),
						_K.Util.String.escapeRichText(string.sub(rawText, found, found + #filter - 1)),
						_K.Util.String.escapeRichText(string.sub(rawText, found + #filter))
					)
					else `<font transparency="0.5">{rawText}</font>`
				lineItem.Text(text)
				lineItem._instance.Visible = found or listFound
			end
		end
	end

	filterInput._input:GetPropertyChangedSignal("Text"):Connect(filterSettings)

	local settingsFrame = UI.new "Frame" {
		Name = "Settings",
		BackgroundTransparency = 1,
		Size = UDim2.new(1, 0, 1, 0),

		UI.new "UIListLayout" { SortOrder = Enum.SortOrder.LayoutOrder, Padding = UI.Theme.Padding },
		filterInput,
		settingsScroller,
		settingsDialog,
	}

	return {
		_instance = settingsFrame,
		_input = filterInput,
		_scroller = settingsScroller._instance,
	}
end

return Settings


local BaseClass = {}
BaseClass.__index = BaseClass

function BaseClass:Destroy()
	for key, value in self do
		if typeof(value) == "Instance" then
			value:Destroy()
		end
	end
end

return BaseClass


local UI = require(script.Parent.Parent) :: any
local BaseClass = require(script.Parent:WaitForChild("BaseClass"))

local Tween = game:GetService("TweenService")

local DEFAULT = {
	--- A test property.
	Icon = "",
	IconProperties = {},
	IconRightAlign = false,
	Label = "",
	LabelProperties = {},
	ActiveSound = true,
	AutomaticSize = Enum.AutomaticSize.None,
	BackgroundColor3 = UI.Theme.Secondary,
	BackgroundTransparency = UI.Theme.TransparencyHeavy,
	HoverTransparency = 0.75,
	FontFace = UI.Theme.Font,
	TextSize = UI.Theme.FontSize,
	TextScaled = false,
	TextXAlignment = Enum.TextXAlignment.Center,
	TextYAlignment = Enum.TextYAlignment.Center,
	Padding = UI.Theme.Padding,
}

type Properties = typeof(DEFAULT) & TextButton

local Class = {}
Class.__index = Class
setmetatable(Class, BaseClass)

function Class.new(properties: Properties?)
	local self = table.clone(DEFAULT)
	self.IconProperties, self.LabelProperties = {}, {}
	UI.makeStatefulDefaults(self, properties)

	local hovering = UI.state(false) :: boolean
	self._hovering = hovering

	local autoSize = UI.compute(function()
		local auto, x, y = self.AutomaticSize(), 1, 1
		if auto == Enum.AutomaticSize.XY then
			x, y = 0, 0
		elseif auto == Enum.AutomaticSize.X then
			x = 0
		elseif auto == Enum.AutomaticSize.Y then
			y = 0
		end
		return UDim2.fromScale(x, y)
	end)

	self._content = UI.new "Frame" {
		Name = "UIContent",
		AutomaticSize = self.AutomaticSize,
		BackgroundTransparency = 1,
		Size = autoSize,

		UI.new "UIListLayout" {
			FillDirection = Enum.FillDirection.Horizontal,
			SortOrder = Enum.SortOrder.LayoutOrder,
			VerticalAlignment = Enum.VerticalAlignment.Center,
		},

		UI.edit(
			UI.new "TextLabel" {
				Name = "Label",
				LayoutOrder = 2,
				AutomaticSize = self.AutomaticSize,
				AutoLocalize = false,
				BackgroundTransparency = 1,
				Size = autoSize,
				FontFace = self.FontFace,
				RichText = true,
				Text = function()
					return tostring(self.Label)
				end,
				TextSize = self.TextSize,
				TextScaled = self.TextScaled,
				TextColor3 = UI.Theme.PrimaryText,
				TextStrokeColor3 = UI.Theme.Primary,
				TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
				TextTruncate = Enum.TextTruncate.SplitWord,
				TextXAlignment = self.TextXAlignment,
				TextYAlignment = self.TextYAlignment,
				Visible = function()
					return self.Label() ~= ""
				end,

				UI.new "UIFlexItem" {
					FlexMode = Enum.UIFlexMode.Fill,
				},
				UI.new "UIPadding" {
					PaddingLeft = self.Padding,
					PaddingRight = self.Padding,
				},
			},
			self.LabelProperties._value
		),

		UI.new "Frame" {
			Name = "IconFrame",
			LayoutOrder = function()
				return if self.IconRightAlign() then 9 else -9
			end,
			BackgroundTransparency = 1,
			Size = UDim2.new(1, 0, 1, 0),
			SizeConstraint = Enum.SizeConstraint.RelativeYY,
			Visible = function()
				return self.Icon() ~= ""
			end,

			UI.edit(
				UI.new "ImageLabel" {
					Name = "Icon",
					AnchorPoint = Vector2.new(0.5, 0.5),
					Position = UDim2.new(0.5, 0, 0.5, 0),
					BackgroundTransparency = 1,
					Size = UDim2.new(1, 0, 1, 0),
					Image = self.Icon,
					ScaleType = Enum.ScaleType.Fit,
				},
				self.IconProperties._value
			),
		},
	} :: Frame & {
		UIListLayout: UIListLayout,
		Label: TextLabel & { UIFlexItem: UIFlexItem, UIPadding: UIPadding },
		IconFrame: Frame & { Icon: ImageLabel },
	}

	local ripple = UI.new "Frame" {
		Name = "Ripple",
		Active = false,
		AnchorPoint = Vector2.new(0.5, 0),
		BackgroundTransparency = 1,
		BackgroundColor3 = UI.Theme.Secondary,

		UI.new "UICorner" {
			CornerRadius = UI.Theme.CornerPadded,
		},
	}

	local instance = UI.new "TextButton" {
		AutomaticSize = self.AutomaticSize,
		AutoLocalize = false,
		Name = "Button",
		Active = true,
		AutoButtonColor = false,
		BackgroundColor3 = self.BackgroundColor3,
		BackgroundTransparency = function()
			return if hovering() then self.HoverTransparency() else self.BackgroundTransparency()
		end,
		ClipsDescendants = true,
		Text = "",
		TextTransparency = 1,
		Size = function()
			local auto = self.AutomaticSize()
			local x = if auto == Enum.AutomaticSize.X or auto == Enum.AutomaticSize.XY then 0 else 1
			return UDim2.new(x, 0, 0, UI.Theme.FontSize + UI.Theme.Padding().Offset * 2)
		end,

		UI.new "UICorner" {
			CornerRadius = UI.Theme.CornerPadded,
		},
		UI.new "Stroke" {},

		self._content,
		ripple,

		Activated = function()
			if self.ActiveSound._value then
				UI.Sound.Hover01:Play()
			end
		end,
		InputBegan = function(input, processed)
			if
				processed
				or (
					input.UserInputType ~= Enum.UserInputType.MouseButton1
					and input.UserInputType ~= Enum.UserInputType.Touch
				)
			then
				return
			end
			local x = input.Position.X
			ripple.Size = UDim2.new(0, 0, 1, 0)
			ripple.Position = UDim2.new(0, x - self._instance.AbsolutePosition.x, 0, 0)
			Tween:Create(ripple, UI.Theme.TweenOut._value, {
				BackgroundTransparency = 0.9,
				Size = UDim2.new(1, 0, 1, 0),
				Position = UDim2.new(0.5, 0, 0, 0),
			}):Play()
		end,
		InputChanged = function(input, processed)
			if processed then
				return
			end
			if
				input.UserInputType == Enum.UserInputType.MouseMovement
				or input.UserInputType == Enum.UserInputType.Touch
			then
				if UI.sinkInput(input.Position.X, input.Position.Y, self._instance) then
					UI.deactivateState(hovering, "hover")
					return
				end
				UI.activateState(hovering, "hover")
			end
		end,
		InputEnded = function(input)
			local isMoveOrTouch = input.UserInputType == Enum.UserInputType.MouseMovement
				or input.UserInputType == Enum.UserInputType.Touch
			if isMoveOrTouch then
				UI.deactivateState(hovering, "hover")
			end
			if isMoveOrTouch or input.UserInputType == Enum.UserInputType.MouseButton1 then
				Tween:Create(ripple, UI.Theme.TweenOut._value, { BackgroundTransparency = 1 }):Play()
			end
		end,
	}
	self._instance = instance :: TextButton & {
		UICorner: UICorner,
		UIStroke: UIStroke,
		UIContent: typeof(self._content),
		Ripple: typeof(ripple) & { UICorner: UICorner },
	}

	return setmetatable(self, Class) :: typeof(self) & typeof(Class)
end

return Class


local UI = require(script.Parent.Parent) :: any
local BaseClass = require(script.Parent:WaitForChild("BaseClass"))

local DEFAULT = {
	--- Whether or not the checkbox is selected.
	Value = false,
}

type Properties = typeof(DEFAULT) & TextButton

local Class = {}
Class.__index = Class
setmetatable(Class, BaseClass)

local boxSize = UI.compute(function()
	return UDim2.fromOffset(UI.Theme.FontSizeLarger(), UI.Theme.FontSizeLarger())
end)

function Class.new(properties: Properties?)
	local self = table.clone(DEFAULT)
	UI.makeStatefulDefaults(self, properties)

	self._instance = UI.new "TextButton" {
		AutoLocalize = false,
		Name = "Checkbox",
		Active = true,
		AnchorPoint = Vector2.new(1, 0.5),
		BackgroundColor3 = UI.Theme.Secondary,
		BackgroundTransparency = UI.Theme.TransparencyHeavy,
		Position = UDim2.new(1, 0, 0.5, 0),
		Text = "",
		TextTransparency = 1,
		Size = boxSize,

		UI.new "UICorner" {
			CornerRadius = UI.Theme.CornerPadded,
		},

		UI.new "Stroke" {},

		UI.new "UIPadding" {
			PaddingTop = UI.Theme.PaddingHalf,
			PaddingRight = UI.Theme.PaddingHalf,
			PaddingBottom = UI.Theme.PaddingHalf,
			PaddingLeft = UI.Theme.PaddingHalf,
		},

		UI.new "Frame" {
			Name = "Checkmark",
			BackgroundTransparency = 1,
			ClipsDescendants = true,
			Size = UI.tween(function()
				return UDim2.new(if self.Value() then 1 else 0, 0, 1, 0)
			end, UI.Theme.TweenOut),

			UI.new "ImageLabel" {
				AnchorPoint = Vector2.new(0, 0.5),
				BackgroundTransparency = 1,
				Position = UDim2.new(0, 0, 0.5, 0),
				Size = UDim2.fromScale(1, 1),
				SizeConstraint = Enum.SizeConstraint.RelativeYY,
				Image = UI.Theme.Image.Check,
				ImageColor3 = UI.Theme.Secondary,
			},
		},

		Activated = function()
			local value = not UI.raw(self.Value)
			self.Value(value)
			if value then
				UI.Sound.Hover03:Play()
			else
				UI.Sound.Hover01:Play()
			end
		end,
	} :: TextButton & {
		UICorner: UICorner,
		UIStroke: UIStroke,
		UIPadding: UIPadding,
		Checkmark: Frame & { ImageLabel: ImageLabel },
	}

	return setmetatable(self, Class) :: typeof(self) & typeof(Class)
end

return Class


local UI = require(script.Parent.Parent) :: any
local BaseClass = require(script.Parent:WaitForChild("BaseClass"))

local DEFAULT = {
	Alpha = 0,
	Image = UI.Theme.Image.Circle,
}

type Properties = typeof(DEFAULT) & Frame

local halfMask = NumberSequence.new({
	NumberSequenceKeypoint.new(0, 0),
	NumberSequenceKeypoint.new(0.5, 0),
	NumberSequenceKeypoint.new(0.5, 1),
	NumberSequenceKeypoint.new(1, 1),
})

local Class = {}
Class.__index = Class
setmetatable(Class, BaseClass)

function Class.new(properties: Properties?)
	local self = table.clone(DEFAULT)
	UI.makeStatefulDefaults(self, properties)

	self._instance = UI.new "Frame" {
		Name = "CircleProgress",
		AnchorPoint = Vector2.new(0, 0),
		BackgroundTransparency = 1,
		Size = UDim2.fromOffset(64, 64),

		UI.new "Frame" {
			Name = "Left",
			BackgroundTransparency = 1,
			ClipsDescendants = true,
			Size = UDim2.fromScale(0.5, 1),
			Visible = function()
				return self.Alpha() > 0.5
			end,

			UI.new "ImageLabel" {
				BackgroundTransparency = 1,
				ImageColor3 = UI.Theme.Secondary,
				Image = self.Image,
				Size = UDim2.fromScale(2, 1),

				UI.new "UIGradient" {
					Transparency = halfMask,
					Rotation = function()
						return math.clamp(self.Alpha() * 360 - 360, -180, 0)
					end,
				},
			},
		},

		UI.new "Frame" {
			Name = "Right",
			BackgroundTransparency = 1,
			ClipsDescendants = true,
			Position = UDim2.fromScale(0.5, 0),
			Size = UDim2.fromScale(0.5, 1),
			Visible = function()
				return self.Alpha() > 0
			end,

			UI.new "ImageLabel" {
				BackgroundTransparency = 1,
				ImageColor3 = UI.Theme.Secondary,
				Image = self.Image,
				Position = UDim2.fromScale(-1, 0),
				Size = UDim2.fromScale(2, 1),

				UI.new "UIGradient" {
					Transparency = halfMask,
					Rotation = function()
						return math.clamp(self.Alpha() * 360, 0, 180)
					end,
				},
			},
		},
	} :: Frame & {
		Left: Frame & { ImageLabel: ImageLabel & { UIGradient: UIGradient } },
		Right: Frame & { ImageLabel: ImageLabel & { UIGradient: UIGradient } },
	}

	return setmetatable(self, Class) :: typeof(self) & typeof(Class)
end

return Class


local UI = require(script.Parent.Parent) :: any
local BaseClass = require(script.Parent:WaitForChild("BaseClass"))
local ColorPicker = require(script.Parent:WaitForChild("ColorPicker"))
type ColorPicker = typeof(ColorPicker.new())

-- a simple color square, when clicked shows a color picker dialog

local DEFAULT = {
	Text = "ListItem Label",
	Value = Color3.new(1, 0, 0),
}

type Properties = typeof(DEFAULT) & TextButton

local Class = {}
Class.__index = Class
setmetatable(Class, BaseClass)

function Class.new(properties: Properties?)
	local self = table.clone(DEFAULT)
	UI.makeStatefulDefaults(self, properties)

	self._instance = UI.new "TextButton" {
		AutoLocalize = false,
		Text = "",
		BackgroundColor3 = self.Value,
		AnchorPoint = Vector2.new(1, 0.5),
		Size = function()
			local height = UI.Theme.FontSize() + UI.Theme.Padding().Offset
			return UDim2.new(0, height, 0, height)
		end,
		Position = UDim2.new(1, 0, 0.5, 0),

		UI.new "UICorner" {
			CornerRadius = UI.Theme.CornerPadded,
		},
		UI.new "Stroke" {},

		Activated = function()
			UI.toggleState(self._dialog.Visible, "floating")
			local value = UI.raw(self._dialog.Visible)
			if value then
				UI.Sound.Hover03:Play()
			else
				UI.Sound.Hover01:Play()
			end
		end,
	} :: TextButton & { UICorner: UICorner, UIStroke: UIStroke }

	self._dialog = UI.new "ColorPicker" {
		Adornee = self._instance,
		RightAlign = true,
		Value = self.Value,
		BackgroundColor3 = UI.Theme.Primary,
		BackgroundTransparency = 0,
	} :: ColorPicker

	return setmetatable(self, Class) :: typeof(self) & typeof(Class)
end

return Class