// ===================== Core.cs ===================== using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web.Script.Serialization; using System.Windows.Forms; namespace Fragline { static class Native { [DllImport("user32.dll")] public static extern bool SetProcessDPIAware(); [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll")] public static extern bool RegisterHotKey(IntPtr hWnd, int id, uint mod, uint vk); [DllImport("user32.dll")] public static extern bool UnregisterHotKey(IntPtr hWnd, int id); [DllImport("user32.dll")] public static extern short GetAsyncKeyState(int vKey); [DllImport("dwmapi.dll")] public static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int val, int size); [DllImport("uxtheme.dll", CharSet = CharSet.Unicode)] public static extern int SetWindowTheme(IntPtr hwnd, string app, string idList); [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, string lParam); [DllImport("user32.dll")] public static extern bool FlashWindowEx(ref FLASHWINFO info); [DllImport("user32.dll")] public static extern bool ReleaseCapture(); [DllImport("user32.dll")] public static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll")] public static extern bool GetLastInputInfo(ref LASTINPUTINFO info); [DllImport("user32.dll")] public static extern bool DestroyIcon(IntPtr hIcon); [DllImport("shell32.dll")] static extern int SHQueryUserNotificationState(out int state); [DllImport("gdi32.dll")] public static extern IntPtr AddFontMemResourceEx(IntPtr pbFont, uint cbFont, IntPtr pdv, out uint pcFonts); [StructLayout(LayoutKind.Sequential)] public struct FLASHWINFO { public uint cbSize; public IntPtr hwnd; public uint dwFlags; public uint uCount; public uint dwTimeout; } [StructLayout(LayoutKind.Sequential)] public struct LASTINPUTINFO { public uint cbSize; public uint dwTime; } // Korisnik je u punom zaslonu (igra, prezentacija): tada ne guramo toast obavijesti. // QUNS_BUSY=2, QUNS_RUNNING_D3D_FULL_SCREEN=3, QUNS_PRESENTATION_MODE=4 public static bool FullScreenBusy() { try { int s; return SHQueryUserNotificationState(out s) == 0 && (s == 2 || s == 3 || s == 4); } catch { return false; } } public static int IdleSeconds() { try { var li = new LASTINPUTINFO { cbSize = (uint)Marshal.SizeOf(typeof(LASTINPUTINFO)) }; if (!GetLastInputInfo(ref li)) return 0; return (int)(unchecked((uint)Environment.TickCount - li.dwTime) / 1000); } catch { return 0; } } public static void Flash(Form f) { try { var fi = new FLASHWINFO { hwnd = f.Handle, dwFlags = 3 | 12, uCount = 0, dwTimeout = 0 }; fi.cbSize = (uint)Marshal.SizeOf(fi); FlashWindowEx(ref fi); } catch { } } } // Boje i oblici u stilu Discorda static class Theme { public static Color Rail, Bg, Bg2, Panel, Panel2, Panel3, Line, Shadow, Text, Text2, Muted, Accent, Button, OnAccent = Color.White, Play = C("#23a55a"), On = C("#23a55a"), Red = C("#f23f43"), Idle = C("#f0b232"), Dnd = C("#f23f43"), Off = C("#80848e"), UserBar, Input, Field, Secondary, Track, Link, Menu, MsgHover, Pip, Stage, Canvas, Accent2; public static bool Light, Animated, ClassicLayout, SquareAvatars, Dash; public static readonly string[] ThemeKeys = { "dash", "nova", "dark", "midnight", "fragline", "xfire", "aurora", "light" }; public static readonly string[] ThemeNames = { "Ploča (ljubičasto-ružičasta)", "Fragline (kao web)", "Fragline (crna)", "Ponoć (crna)", "Fragline (plava noć)", "Klasična plava (Xfire)", "Aurora (animirana)", "Svijetla" }; public static readonly string[] LayoutKeys = { "dock", "discord", "xfire" }; public static readonly string[] LayoutNames = { "Dock (novo)", "Gornja traka", "Klasični (kao Xfire)" }; public static readonly string[] AccentKeys = { "auto", "sunset", "spot", "neon", "blurple", "orange", "purple", "pink" }; public static readonly string[] AccentNames = { "Prema temi", "Ljubičasto-ružičasta", "Zelena", "Neon (ljubičasto-cijan)", "Plava", "Narančasta", "Ljubičasta", "Ružičasta" }; // Postavlja boje teme; mijenja se samo pri pokretanju public static void Apply(string theme, string accent) { Light = theme == "light"; Animated = theme == "aurora"; Dash = theme == "dash"; SquareAvatars = false; Text = C("#f2f3f5"); Text2 = C("#dbdee1"); Muted = C("#949ba4"); Link = C("#00a8fc"); Pip = Color.White; // statusne boje (vrate se na default; "nova" ih mijenja ispod) On = C("#23a55a"); Play = C("#23a55a"); Idle = C("#f0b232"); Dnd = C("#f23f43"); Red = C("#f23f43"); Off = C("#80848e"); switch (theme) { case "dash": // "Ploca": tamnoplave staklene kartice na plavo-sivoj podlozi, ljubicasto-ruzicasti naglasak Rail = C("#2b3048"); Panel = C("#363c5a"); Bg = C("#2b3048"); Panel2 = C("#3f4666"); Panel3 = C("#4a5276"); Line = C("#424968"); Shadow = C("#161a2b"); UserBar = C("#363c5a"); Input = C("#2f3550"); Field = C("#262b42"); Secondary = C("#4a5276"); Track = C("#262b42"); Menu = C("#363c5a"); MsgHover = C("#323852"); Stage = C("#232740"); Text = C("#ffffff"); Text2 = C("#d3d7ea"); Muted = C("#9097b5"); Link = C("#b7a6ff"); break; case "nova": // identično webu (fragline.css): mornarska pozadina + narančasti akcent Rail = C("#0b0d14"); Panel = C("#141826"); Bg = C("#0b0d14"); Panel2 = C("#1b2031"); Panel3 = C("#232940"); Line = C("#242a3c"); Shadow = C("#000000"); UserBar = C("#0f121b"); Input = C("#1b2031"); Field = C("#1b2031"); Secondary = C("#232940"); Track = C("#1b2031"); Menu = C("#141826"); MsgHover = C("#1b2031"); Stage = C("#0b0d14"); Text = C("#eef0f6"); Text2 = C("#dbe0ec"); Muted = C("#8a90a6"); Link = C("#ff8a3d"); Pip = Color.White; On = C("#43d68a"); Play = C("#ffb547"); Idle = C("#ffb547"); Dnd = C("#ff5f6d"); Red = C("#ff5f6d"); Off = C("#8a90a6"); SquareAvatars = true; break; case "midnight": Rail = C("#000000"); Panel = C("#0a0a0c"); Bg = C("#121214"); Panel2 = C("#1c1c20"); Panel3 = C("#26262b"); Line = C("#24242a"); Shadow = C("#000000"); UserBar = C("#050506"); Input = C("#1c1c20"); Field = C("#050506"); Secondary = C("#2f3036"); Track = C("#2f3036"); Menu = C("#000000"); MsgHover = C("#18181b"); Stage = C("#000000"); break; case "fragline": Rail = C("#07090f"); Panel = C("#0f121b"); Bg = C("#141826"); Panel2 = C("#1b2031"); Panel3 = C("#232940"); Line = C("#262c40"); Shadow = C("#07090f"); UserBar = C("#0b0d14"); Input = C("#1b2031"); Field = C("#0b0d14"); Secondary = C("#2a3150"); Track = C("#2a3150"); Menu = C("#0b0d14"); MsgHover = C("#181c2b"); Stage = C("#07090f"); Text = C("#eef0f6"); Muted = C("#8a90a6"); break; case "xfire": Rail = C("#0a0f17"); Panel = C("#111a28"); Bg = C("#0d131c"); Panel2 = C("#1b2638"); Panel3 = C("#24344d"); Line = C("#22304a"); Shadow = C("#05080d"); UserBar = C("#101824"); Input = C("#162131"); Field = C("#0a0f17"); Secondary = C("#2b3c58"); Track = C("#2b3c58"); Menu = C("#0d131c"); MsgHover = C("#141d2b"); Stage = C("#070b11"); Text = C("#e8eef8"); Text2 = C("#c3cfe0"); Muted = C("#7f8ea6"); Link = C("#6fb4ff"); break; case "aurora": Rail = C("#05060a"); Panel = C("#0b0d14"); Bg = C("#10121a"); Panel2 = C("#1a1d29"); Panel3 = C("#242838"); Line = C("#222636"); Shadow = C("#05060a"); UserBar = C("#07080d"); Input = C("#1a1d29"); Field = C("#07080d"); Secondary = C("#2a2f42"); Track = C("#2a2f42"); Menu = C("#07080d"); MsgHover = C("#151824"); Stage = C("#05060a"); break; case "light": Rail = C("#e9ecf3"); Panel = C("#f6f7fb"); Bg = C("#ffffff"); Panel2 = C("#eef0f6"); Panel3 = C("#e3e7f1"); Line = C("#e3e6ee"); Shadow = C("#d9dde7"); UserBar = C("#eef1f7"); Input = C("#eef0f6"); Field = C("#e9ecf3"); Secondary = C("#8a93a6"); Track = C("#d3d8e3"); Menu = C("#ffffff"); MsgHover = C("#f4f6fa"); Stage = C("#e9ecf3"); Text = C("#12151c"); Text2 = C("#2c3340"); Muted = C("#6a7385"); Link = C("#2f6fe0"); Pip = C("#12151c"); break; default: // glavna tema: crna kao Spotify (ploce #121212 na crnoj podlozi) Rail = C("#000000"); Panel = C("#121212"); Bg = C("#121212"); Panel2 = C("#1f1f1f"); Panel3 = C("#2a2a2a"); Line = C("#282828"); Shadow = C("#000000"); UserBar = C("#000000"); Input = C("#242424"); Field = C("#242424"); Secondary = C("#2a2a2a"); Track = C("#4d4d4d"); Menu = C("#282828"); MsgHover = C("#1a1a1a"); Stage = C("#000000"); Text = C("#ffffff"); Text2 = C("#b3b3b3"); Muted = C("#a7a7a7"); Link = C("#3d91f4"); break; } Bg2 = Bg; Canvas = Light ? Shade(Rail, -0.1f) : Shade(Rail, -0.45f); OnAccent = OnColor(Accent); if (accent == null || accent == "auto") accent = theme == "dash" ? "sunset" : theme == "nova" ? "nova" : theme == "fragline" || theme == "xfire" ? "orange" : theme == "aurora" ? "purple" : theme == "light" ? "neon" : "spot"; Accent2 = Color.Empty; switch (accent) { case "nova": Accent = C("#ff8a3d"); Accent2 = C("#ffb547"); break; case "sunset": Accent = C("#8b6cf6"); Accent2 = C("#ef5ea8"); break; case "spot": Accent = C("#1db954"); Accent2 = C("#1ed760"); break; case "neon": Accent = C("#7c5cff"); Accent2 = C("#19c3ff"); break; case "orange": Accent = C("#ff7a2f"); break; case "green": Accent = C("#248046"); break; case "purple": Accent = C("#8e5cf7"); break; case "pink": Accent = C("#eb459e"); break; default: Accent = C("#5865f2"); break; } Button = Accent; // drugi kraj prijelaza boje (gumbi, odabrane stavke, oblacici poruka) if (Accent2 == Color.Empty) Accent2 = Shade(Accent, Light ? -0.2f : 0.3f); // Nova (web): narančasti gradijentni gumb s tamnim tekstom (--ontext) if (theme == "nova") OnAccent = C("#1b0f04"); } public static float S = 1f; static string display = "Segoe UI Semibold", body = "Segoe UI"; static bool displayBold; public static string DisplayFace { get { return display; } } static string icons; // Ikone iz fonta Segoe MDL2 Assets (Windows 10/11) public const string IcoMic = "\uE720", IcoHead = "\uE7F6", IcoGear = "\uE713", IcoPeople = "\uE716", IcoAdd = "\uE710", IcoMsg = "\uE8BD", IcoSpeaker = "\uE767", IcoHangup = "\uE778", IcoHistory = "\uE81C", IcoClose = "\uE711", IcoSend = "\uE724", IcoContact = "\uE77B", IcoGame = "\uE7FC", IcoGlobe = "\uE774", IcoCheck = "\uE73E", IcoPhone = "\uE717"; static Color C(string h) { return ColorTranslator.FromHtml(h); } // crni ili bijeli tekst, ovisno o tome koliko je pozadina svijetla (zelena kao Spotify = crni tekst) public static Color OnColor(Color c) { double l = (0.299 * c.R + 0.587 * c.G + 0.114 * c.B) / 255.0; return l > 0.55 ? Color.Black : Color.White; } public static int D(float v) { return (int)Math.Round(v * S); } public static void Init() { using (var g = Graphics.FromHwnd(IntPtr.Zero)) S = Math.Max(1f, g.DpiX / 96f); Apply(Prefs.Str("theme", "dash"), Prefs.Str("accent", "auto")); ClassicLayout = Prefs.Str("layout", "dock") == "xfire"; var names = FontFamily.Families.Select(f => f.Name).ToList(); // Windows 11 ima noviji, moderniji font if (names.Contains("Segoe UI Variable Text")) body = "Segoe UI Variable Text"; if (names.Contains("Segoe UI Variable Display Semibold")) display = "Segoe UI Variable Display Semibold"; else if (names.Contains("Segoe UI Variable Display")) display = "Segoe UI Variable Display"; if (names.Contains("Segoe Fluent Icons")) icons = "Segoe Fluent Icons"; // web brend-fontovi su ugrađeni u exe (Program.LoadFonts) → koristi ih za "nova" (web) izgled if (!ClassicLayout && Prefs.Str("theme", "dash") == "nova") { body = "Plus Jakarta Sans"; display = "Chakra Petch"; displayBold = false; } // klasicni (Xfire) raspored: cijela aplikacija u Tahomi, kao pravi Xfire if (ClassicLayout && names.Contains("Tahoma")) { body = "Tahoma"; display = "Tahoma"; displayBold = true; } else if (names.Contains("Segoe MDL2 Assets")) icons = "Segoe MDL2 Assets"; } public static Font Display(float px) { return new Font(display, px * S, displayBold ? FontStyle.Bold : FontStyle.Regular, GraphicsUnit.Pixel); } public static bool DisplayBold { get { return displayBold; } } // Crta ikonu; bez fonta s ikonama (Windows 7) crta kratki tekst umjesto nje public static void Icon(Graphics g, string glyph, string fallback, Rectangle r, Color c, float px) { const TextFormatFlags fl = TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding; if (icons != null) using (var f = new Font(icons, px * S, FontStyle.Regular, GraphicsUnit.Pixel)) TextRenderer.DrawText(g, glyph, f, r, c, fl); else using (var f = Body(Math.Max(9f, px * 0.6f), FontStyle.Bold)) TextRenderer.DrawText(g, fallback, f, r, c, fl); } // Precrtana ikona (isključen mikrofon ili zvuk) public static void Slash(Graphics g, Rectangle r, Color bg) { var st = g.Save(); g.SmoothingMode = SmoothingMode.AntiAlias; float m = r.Width * 0.22f; using (var pen = new Pen(bg, D(4))) g.DrawLine(pen, r.X + m, r.Y + m, r.Right - m, r.Bottom - m); using (var pen = new Pen(Red, D(2))) g.DrawLine(pen, r.X + m, r.Y + m, r.Right - m, r.Bottom - m); g.Restore(st); } // zaobljeni rubovi cijele kontrole (za plutajuce kartice u modernom izgledu) public static void RoundRegion(Control c, int rad) { if (c.Width <= 0 || c.Height <= 0) return; var old = c.Region; using (var p = Round(new RectangleF(0, 0, c.Width, c.Height), rad)) c.Region = new Region(p); if (old != null) old.Dispose(); } public static void Cue(TextBox tb, string text) { EventHandler h = (s, e) => { try { Native.SendMessage(tb.Handle, 0x1501, (IntPtr)1, text); } catch { } }; if (tb.IsHandleCreated) h(tb, EventArgs.Empty); else tb.HandleCreated += h; } // Status za točkicu: online, idle, dnd ili off public static string Kind(string status, string presence) { if (status == null || status == "off") return "off"; if (presence == "dnd") return "dnd"; if (presence == "idle") return "idle"; return "online"; } public static string KindText(string kind) { switch (kind) { case "idle": return "Odsutan"; case "dnd": return "Ne smetaj"; case "invisible": return "Nevidljiv"; case "off": return "Offline"; default: return "Online"; } } public static Color KindColor(string kind) { switch (kind) { case "idle": return Idle; case "dnd": return Dnd; case "off": case "invisible": return Off; default: return On; } } public static void StatusDot(Graphics g, RectangleF dr, string kind, Color ringBg) { g.SmoothingMode = SmoothingMode.AntiAlias; var ring = dr; ring.Inflate(dr.Width * 0.2f, dr.Width * 0.2f); using (var b = new SolidBrush(ringBg)) g.FillEllipse(b, ring); using (var b = new SolidBrush(KindColor(kind))) g.FillEllipse(b, dr); using (var cut = new SolidBrush(ringBg)) { if (kind == "idle") g.FillEllipse(cut, dr.X - dr.Width * 0.12f, dr.Y - dr.Height * 0.12f, dr.Width * 0.62f, dr.Height * 0.62f); else if (kind == "dnd") using (var p = Round(new RectangleF(dr.X + dr.Width * 0.2f, dr.Y + dr.Height * 0.4f, dr.Width * 0.6f, dr.Height * 0.2f), dr.Height * 0.1f)) g.FillPath(cut, p); else if (kind == "off" || kind == "invisible") { var inner = dr; inner.Inflate(-dr.Width * 0.25f, -dr.Height * 0.25f); g.FillEllipse(cut, inner); } } } static readonly Dictionary masks = new Dictionary(); // Slika izrezana u krug (rad < 0) ili zaobljeni pravokutnik, spremljena za ponovno crtanje. // Tako na platno ne treba stavljati clip, koji je znao "pojesti" tekst iscrtan nakon slike. public static Image Masked(Image img, int w, int h, float rad) { if (img == null || w <= 0 || h <= 0) return img; var key = System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(img) + ":" + w + "x" + h + ":" + rad; lock (masks) { Bitmap cached; if (masks.TryGetValue(key, out cached)) return cached; } var bmp = new Bitmap(w, h, System.Drawing.Imaging.PixelFormat.Format32bppPArgb); using (var scaled = new Bitmap(w, h)) { using (var gs = Graphics.FromImage(scaled)) { gs.InterpolationMode = InterpolationMode.HighQualityBicubic; gs.PixelOffsetMode = PixelOffsetMode.HighQuality; gs.DrawImage(img, 0, 0, w, h); } using (var g = Graphics.FromImage(bmp)) using (var tb = new TextureBrush(scaled)) using (var p = rad < 0 ? new GraphicsPath() : Round(new RectangleF(0, 0, w, h), rad)) { g.SmoothingMode = SmoothingMode.AntiAlias; if (rad < 0) p.AddEllipse(0, 0, w, h); g.FillPath(tb, p); } } lock (masks) masks[key] = bmp; return bmp; } // Okrugli avatar: slika s weba ili inicijal na boji korisnika, s točkicom statusa public static void Avatar(Graphics g, RectangleF r, string colorHex, string name, string url, Control notify, string kind, Color ringBg) { var st = g.Save(); g.SmoothingMode = SmoothingMode.AntiAlias; // web koristi zaobljene kvadrate (radius ~30 %), a ne krugove float arad = SquareAvatars ? r.Width * 0.3f : -1f; var img = ImageCache.Get(url, notify); if (img != null) { int w = (int)Math.Round(r.Width), h = (int)Math.Round(r.Height); g.DrawImage(Masked(img, w, h, arad), (int)Math.Round(r.X), (int)Math.Round(r.Y), w, h); } else { var c = Hex(colorHex, Off); if (SquareAvatars) using (var p = Round(r, arad)) using (var b = new SolidBrush(c)) g.FillPath(b, p); else using (var b = new SolidBrush(c)) g.FillEllipse(b, r); var init = string.IsNullOrEmpty(name) ? "?" : name.Substring(0, 1).ToUpperInvariant(); using (var f = new Font(display, Math.Max(6f, r.Height * 0.42f), FontStyle.Regular, GraphicsUnit.Pixel)) TextRenderer.DrawText(g, init, f, Rectangle.Round(r), Color.White, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding); } g.Restore(st); if (kind == null) return; float ds = Math.Max(D(8), r.Width * 0.3f); StatusDot(g, new RectangleF(r.Right - ds * 0.85f, r.Bottom - ds * 0.85f, ds, ds), kind, ringBg); } public static void Pill(Graphics g, string txt, Rectangle at, bool rightAlign, Color ring) { using (var f = Body(11.5f, FontStyle.Bold)) { var sz = TextRenderer.MeasureText(txt, f); int h = D(16), w = Math.Max(h, sz.Width + D(4)); var r = new Rectangle(rightAlign ? at.Right - w : at.X, at.Y + (at.Height - h) / 2, w, h); g.SmoothingMode = SmoothingMode.AntiAlias; if (ring != Color.Empty) { var rr = r; rr.Inflate(D(3), D(3)); using (var p = Round(rr, rr.Height / 2f)) using (var b = new SolidBrush(ring)) g.FillPath(b, p); } using (var p = Round(r, h / 2f)) using (var b = new SolidBrush(Red)) g.FillPath(b, p); TextRenderer.DrawText(g, txt, f, r, Color.White, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding); } } public static Font Body(float px, FontStyle st = FontStyle.Regular) { return new Font(body, px * S, st, GraphicsUnit.Pixel); } public static GraphicsPath Round(RectangleF r, float rad) { var p = new GraphicsPath(); float d = Math.Max(1f, Math.Min(rad * 2, Math.Min(r.Width, r.Height))); p.AddArc(r.X, r.Y, d, d, 180, 90); p.AddArc(r.Right - d, r.Y, d, d, 270, 90); p.AddArc(r.Right - d, r.Bottom - d, d, d, 0, 90); p.AddArc(r.X, r.Bottom - d, d, d, 90, 90); p.CloseFigure(); return p; } public static void DrawFlame(Graphics g, RectangleF box) { var st = g.Save(); g.SmoothingMode = SmoothingMode.AntiAlias; g.TranslateTransform(box.X, box.Y); g.ScaleTransform(box.Width / 24f, box.Height / 24f); g.TranslateTransform(-4f, -2f); using (var o = new GraphicsPath()) { o.AddBezier(16, 3, 18, 8, 24, 10, 24, 17); o.AddArc(8, 9, 16, 16, 0, 180); o.AddBezier(8, 17, 8, 13, 10, 11, 11, 9); o.AddBezier(11, 9, 12, 12, 13, 13, 15, 13); o.AddBezier(15, 13, 14, 9, 14, 6, 16, 3); o.CloseFigure(); using (var b = new SolidBrush(Color.FromArgb(255, 122, 47))) g.FillPath(b, o); } using (var i = new GraphicsPath()) { i.AddBezier(16, 15, 17, 18, 20, 18, 20, 22); i.AddArc(12, 18, 8, 8, 0, 180); i.AddBezier(12, 22, 12, 20, 14, 18, 16, 15); i.CloseFigure(); using (var b = new SolidBrush(Color.FromArgb(255, 209, 102))) g.FillPath(b, i); } g.Restore(st); } // Kartica s mekom sjenom (staklene plohe nadzorne ploce) public static void Card(Graphics g, Rectangle r, float rad, Color fill) { var st = g.Save(); g.SmoothingMode = SmoothingMode.AntiAlias; for (int i = 3; i >= 1; i--) { var sr = r; sr.Offset(0, D(i * 2)); sr.Inflate(D(i), D(i)); using (var p = Round(sr, rad + D(i))) using (var b = new SolidBrush(Color.FromArgb(22, Shadow))) g.FillPath(b, p); } using (var p = Round(r, rad)) { using (var b = new LinearGradientBrush(new Rectangle(r.X, r.Y - 1, r.Width, r.Height + 2), Shade(fill, Light ? 0 : 0.05f), fill, 90f)) g.FillPath(b, p); using (var pen = new Pen(Color.FromArgb(Light ? 40 : 20, Light ? Color.Black : Color.White))) g.DrawPath(pen, p); } g.Restore(st); } // Mala strelica (otvoreno = prema dolje), crtana, da ne ovisi o znakovima iz fonta // Kvacica nacrtana linijama (ne ovisi o fontu sa simbolima); px je velicina kao kod Icon public static void Check(Graphics g, RectangleF r, Color c, float px) { var st = g.Save(); g.SmoothingMode = SmoothingMode.AntiAlias; float s = D(px) * 0.42f, cx = r.X + r.Width / 2f, cy = r.Y + r.Height / 2f + s * 0.08f; using (var pen = new Pen(c, Math.Max(1.5f, D(px) * 0.15f)) { StartCap = LineCap.Round, EndCap = LineCap.Round, LineJoin = LineJoin.Round }) g.DrawLines(pen, new[] { new PointF(cx - s, cy), new PointF(cx - s * 0.32f, cy + s * 0.68f), new PointF(cx + s, cy - s * 0.68f) }); g.Restore(st); } public static void Chevron(Graphics g, RectangleF r, bool open, Color c) { var st = g.Save(); g.SmoothingMode = SmoothingMode.AntiAlias; float cx = r.X + r.Width / 2f, cy = r.Y + r.Height / 2f, s = Math.Min(r.Width, r.Height) * 0.28f; using (var pen = new Pen(c, Math.Max(1.5f, D(1.6f))) { StartCap = LineCap.Round, EndCap = LineCap.Round, LineJoin = LineJoin.Round }) { if (open) g.DrawLines(pen, new[] { new PointF(cx - s, cy - s / 2), new PointF(cx, cy + s / 2), new PointF(cx + s, cy - s / 2) }); else g.DrawLines(pen, new[] { new PointF(cx - s / 2, cy - s), new PointF(cx + s / 2, cy), new PointF(cx - s / 2, cy + s) }); } g.Restore(st); } // FontFamily baca iznimku ako font ne postoji (npr. stariji Windows), pa uzmi zamjenski static FontFamily SafeFamily(string name) { try { return new FontFamily(name); } catch { try { return new FontFamily("Segoe UI"); } catch { return new FontFamily(System.Drawing.Text.GenericFontFamilies.SansSerif); } } } // Natpis "Fragline": oba dijela iz istog vektorskog puta, pa su uvijek u istoj liniji public static void Wordmark(Graphics g, float x, float centerY, float px, Color first) { var st = g.Save(); g.SmoothingMode = SmoothingMode.AntiAlias; using (var fam = SafeFamily(DisplayFace)) using (var a = new GraphicsPath()) using (var b = new GraphicsPath()) { var style = displayBold ? FontStyle.Bold : FontStyle.Regular; float em = px * S; a.AddString("Frag", fam, (int)style, em, new PointF(0, 0), StringFormat.GenericTypographic); var ab = a.GetBounds(); b.AddString("line", fam, (int)style, em, new PointF(ab.Right + em * 0.03f, 0), StringFormat.GenericTypographic); var all = RectangleF.Union(ab, b.GetBounds()); using (var m = new Matrix()) { m.Translate(x - all.X, centerY - all.Y - all.Height / 2f); a.Transform(m); b.Transform(m); } using (var br = new SolidBrush(first)) g.FillPath(br, a); var bb = b.GetBounds(); using (var br = new LinearGradientBrush(new RectangleF(bb.X - 1, bb.Y, bb.Width + 2, bb.Height), Accent, Accent2, 0f)) g.FillPath(br, b); } g.Restore(st); } public static void DarkTitle(Form f) { // Windows 11: zaobljeni kutovi prozora try { int round = 2; Native.DwmSetWindowAttribute(f.Handle, 33, ref round, 4); } catch { } if (Light) return; try { int on = 1; if (Native.DwmSetWindowAttribute(f.Handle, 20, ref on, 4) != 0) Native.DwmSetWindowAttribute(f.Handle, 19, ref on, 4); } catch { } if (ClassicLayout) return; // Windows 11: naslovna traka u boji pozadine i rub prozora u boji naglaska (COLORREF = 0x00BBGGRR) try { int cap = Bg.R | (Bg.G << 8) | (Bg.B << 16), edge = Accent.R | (Accent.G << 8) | (Accent.B << 16), txt = Text.R | (Text.G << 8) | (Text.B << 16); Native.DwmSetWindowAttribute(f.Handle, 35, ref cap, 4); Native.DwmSetWindowAttribute(f.Handle, 34, ref edge, 4); Native.DwmSetWindowAttribute(f.Handle, 36, ref txt, 4); } catch { } } public static void DarkScroll(Control c) { if (Light) return; EventHandler h = (s, e) => { try { Native.SetWindowTheme(c.Handle, "DarkMode_Explorer", null); } catch { } }; if (c.IsHandleCreated) h(c, EventArgs.Empty); else c.HandleCreated += h; } public static Color Shade(Color c, float f) { Func m = v => Math.Max(0, Math.Min(255, f >= 0 ? (int)(v + (255 - v) * f) : (int)(v * (1 + f)))); return Color.FromArgb(c.A, m(c.R), m(c.G), m(c.B)); } public static Color Hex(string h, Color fallback) { try { return string.IsNullOrEmpty(h) ? fallback : ColorTranslator.FromHtml(h); } catch { return fallback; } } public static string Since(string iso) { DateTimeOffset d; if (string.IsNullOrEmpty(iso) || !DateTimeOffset.TryParse(iso, CultureInfo.InvariantCulture, DateTimeStyles.None, out d)) return null; int m = Math.Max(1, (int)Math.Round((DateTimeOffset.UtcNow - d).TotalMinutes)); return m < 60 ? m + " min" : (m / 60) + " h" + (m % 60 > 0 ? " " + (m % 60) + " min" : ""); } } // Animacija za temu "Aurora": boje koje polako putuju kroz duginu paletu static class Anim { public static float Phase; static Timer timer; public static event Action Frame; public static void Start() { if (!Theme.Animated || timer != null) return; timer = new Timer { Interval = 40 }; timer.Tick += (s, e) => { Phase = (Phase + 0.04f / 14f) % 1f; if (Frame != null) Frame(); }; timer.Start(); } // Kontrola se ponovno iscrtava na svaki korak animacije (samo dok je vidljiva) public static void Hook(Control c, Func area = null) { if (!Theme.Animated) return; Frame += () => { if (c.IsDisposed || !c.IsHandleCreated || !c.Visible) return; var f = c.FindForm(); if (f != null && (!f.Visible || f.WindowState == FormWindowState.Minimized)) return; if (area == null) c.Invalidate(); else c.Invalidate(area()); }; } public static Color Hue(float offset, float sat, float val) { float h = ((Phase + offset) % 1f + 1f) % 1f * 6f; int i = (int)h; float f = h - i, p = val * (1 - sat), q = val * (1 - sat * f), t = val * (1 - sat * (1 - f)); float r, g, b; switch (i % 6) { case 0: r = val; g = t; b = p; break; case 1: r = q; g = val; b = p; break; case 2: r = p; g = val; b = t; break; case 3: r = p; g = q; b = val; break; case 4: r = t; g = p; b = val; break; default: r = val; g = p; b = q; break; } return Color.FromArgb((int)(r * 255), (int)(g * 255), (int)(b * 255)); } // Pozadina od dvije boje koje se miješaju i polako mijenjaju public static void Fill(Graphics g, Rectangle r, float sat, float val, float angle) { if (r.Width <= 0 || r.Height <= 0) return; using (var b = new LinearGradientBrush(r, Hue(0f, sat, val), Hue(0.28f, sat, val * 0.75f), angle)) { var blend = new ColorBlend(3); blend.Colors = new[] { Hue(0f, sat, val), Hue(0.14f, sat * 0.9f, val * 0.6f), Hue(0.28f, sat, val * 0.85f) }; blend.Positions = new[] { 0f, 0.5f, 1f }; b.InterpolationColors = blend; g.FillRectangle(b, r); } } } static class Program { public static Icon AppIcon; static readonly Dictionary loaded = new Dictionary(); [STAThread] static void Main(string[] args) { AppDomain.CurrentDomain.AssemblyResolve += Resolve; Run(args); } // Ugrađeni web fontovi (Chakra Petch, Plus Jakarta Sans) — registriramo ih // za cijeli proces (GDI + GDI+), pa rade i s TextRenderer.DrawText. static void LoadFonts() { var asm = Assembly.GetExecutingAssembly(); foreach (var n in asm.GetManifestResourceNames()) { if (!n.StartsWith("font.") || !n.EndsWith(".ttf", StringComparison.OrdinalIgnoreCase)) continue; try { using (var st = asm.GetManifestResourceStream(n)) { var buf = new byte[st.Length]; int read = 0; while (read < buf.Length) { int k = st.Read(buf, read, buf.Length - read); if (k <= 0) break; read += k; } var p = System.Runtime.InteropServices.Marshal.AllocCoTaskMem(buf.Length); System.Runtime.InteropServices.Marshal.Copy(buf, 0, p, buf.Length); uint c; Native.AddFontMemResourceEx(p, (uint)buf.Length, IntPtr.Zero, out c); // p se namjerno ne oslobađa — font mora živjeti do kraja procesa } } catch { } } } // NAudio i Opus su ugradjeni u exe kao resursi static Assembly Resolve(object sender, ResolveEventArgs e) { var name = new AssemblyName(e.Name).Name; lock (loaded) { Assembly a; if (loaded.TryGetValue(name, out a)) return a; using (var st = Assembly.GetExecutingAssembly().GetManifestResourceStream("lib." + name + ".dll")) { if (st == null) return null; var buf = new byte[st.Length]; int read = 0; while (read < buf.Length) { int n = st.Read(buf, read, buf.Length - read); if (n <= 0) break; read += n; } a = Assembly.Load(buf); loaded[name] = a; return a; } } } [MethodImpl(MethodImplOptions.NoInlining)] static void Run(string[] args) { bool created; try { Native.SetProcessDPIAware(); } catch { } Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); LoadFonts(); Prefs.Load(); Theme.Init(); AppIcon = LoadIcon(); using (var mutex = new System.Threading.Mutex(true, "FraglineClient", out created)) { // nakon "Primijeni i ponovno pokreni" stara instanca se jos gasi, pa pricekaj nju if (!created && args.Contains("--restarted")) { try { created = mutex.WaitOne(10000); } catch (System.Threading.AbandonedMutexException) { created = true; } } if (created) { try { var old = Application.ExecutablePath + ".old"; if (File.Exists(old)) File.Delete(old); } catch { } } if (!created) { Dialog.Info(null, "Fragline već radi", "Fragline je već pokrenut. Dvoklikni na ikonu plamena dolje desno kraj sata da ga otvoriš."); return; } ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; Anim.Start(); Application.Run(new TrayApp(args.Contains("--tray"))); } } static Icon LoadIcon() { #if HAS_ICON try { return Icon.ExtractAssociatedIcon(Application.ExecutablePath); } catch { } #endif var bmp = new Bitmap(32, 32); using (var g = Graphics.FromImage(bmp)) { g.Clear(Color.Transparent); Theme.DrawFlame(g, new RectangleF(0, 0, 32, 32)); } return Icon.FromHandle(bmp.GetHicon()); } } static class J { static readonly JavaScriptSerializer js = new JavaScriptSerializer { MaxJsonLength = 8 * 1024 * 1024 }; public static object Parse(string s) { try { return js.DeserializeObject(s); } catch { return null; } } public static string Ser(object o) { return js.Serialize(o); } public static Dictionary D(object o, string k) { var d = o as Dictionary; object v; return d != null && d.TryGetValue(k, out v) ? v as Dictionary : null; } public static object[] A(object o, string k) { var d = o as Dictionary; object v; if (d != null && d.TryGetValue(k, out v)) return v as object[] ?? new object[0]; return new object[0]; } public static string S(object o, string k) { var d = o as Dictionary; object v; return d != null && d.TryGetValue(k, out v) && v != null ? Convert.ToString(v, CultureInfo.InvariantCulture) : null; } public static long L(object o, string k) { long r; return long.TryParse(S(o, k), NumberStyles.Any, CultureInfo.InvariantCulture, out r) ? r : 0; } public static bool B(object o, string k) { var d = o as Dictionary; object v; return d != null && d.TryGetValue(k, out v) && v is bool && (bool)v; } } static class Prefs { static readonly string PrefFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Fragline", "prefs.txt"); static readonly Dictionary v = new Dictionary(); public static void Load() { try { foreach (var line in File.ReadAllLines(PrefFile)) { int i = line.IndexOf('='); if (i > 0) v[line.Substring(0, i)] = line.Substring(i + 1); } } catch { } } static void Save() { try { Directory.CreateDirectory(Path.GetDirectoryName(PrefFile)); File.WriteAllLines(PrefFile, v.Select(kv => kv.Key + "=" + kv.Value).ToArray()); } catch { } } public static bool Bool(string k, bool d) { string s; return v.TryGetValue(k, out s) ? s == "1" : d; } public static string Str(string k, string d) { string s; return v.TryGetValue(k, out s) && s.Length > 0 ? s : d; } public static int Int(string k, int d) { string s; int r; return v.TryGetValue(k, out s) && int.TryParse(s, out r) ? r : d; } public static float Float(string k, float d) { string s; float r; return v.TryGetValue(k, out s) && float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out r) ? r : d; } public static void Set(string k, object val) { v[k] = val is bool ? ((bool)val ? "1" : "0") : Convert.ToString(val, CultureInfo.InvariantCulture); Save(); } } static class ImageCache { static readonly Dictionary map = new Dictionary(); static readonly Dictionary failed = new Dictionary(); static readonly HashSet pending = new HashSet(); public static Image Get(string url, Control notify) { if (string.IsNullOrEmpty(url)) return null; lock (map) { Image img; if (map.TryGetValue(url, out img) && img != null) return img; DateTime when; if (failed.TryGetValue(url, out when) && (DateTime.UtcNow - when).TotalSeconds < 60) return null; if (pending.Contains(url)) return null; pending.Add(url); } Task.Run(() => { Image result = null; try { using (var wc = new WebClient()) { var bytes = wc.DownloadData(url); using (var ms = new MemoryStream(bytes)) using (var src = Image.FromStream(ms)) result = new Bitmap(src); } } catch { } lock (map) { map[url] = result; pending.Remove(url); if (result == null) failed[url] = DateTime.UtcNow; else failed.Remove(url); } if (result != null && notify != null && notify.IsHandleCreated) { try { notify.BeginInvoke((Action)(() => notify.Invalidate())); } catch { } } }); return null; } } static class Api { #if PREVIEW // samo probna verzija za razvoj: adresa servera iz okoline public static readonly string Base = Environment.GetEnvironmentVariable("FRAGLINE_BASE") ?? "http://95.211.43.191:8080"; #else public const string Base = "http://95.211.43.191:8080"; #endif static string Esc(string s) { var sb = new StringBuilder(); foreach (var c in s) { if (c == '"') sb.Append("\\\""); else if (c == '\\') sb.Append("\\\\"); else if (c < 32) sb.AppendFormat("\\u{0:x4}", (int)c); else sb.Append(c); } return sb.ToString(); } static WebClient Client(string token) { var wc = new WebClient { Encoding = Encoding.UTF8 }; wc.Headers[HttpRequestHeader.Accept] = "application/json"; if (token != null) wc.Headers[HttpRequestHeader.Authorization] = "Bearer " + token; return wc; } static string Post(string path, string json, string token) { using (var wc = Client(token)) { wc.Headers[HttpRequestHeader.ContentType] = "application/json"; return wc.UploadString(Base + path, "POST", json); } } public static string Get(string path, string token) { using (var wc = Client(token)) return wc.DownloadString(Base + path); } public static string PostJ(string path, object body, string token) { return Post(path, J.Ser(body), token); } public static string Field(string json, string name) { var m = Regex.Match(json ?? "", "\"" + name + "\"\\s*:\\s*(null|\"((?:[^\"\\\\]|\\\\.)*)\")"); if (!m.Success || m.Groups[1].Value == "null") return null; return Regex.Unescape(m.Groups[2].Value); } public static string ErrorMessage(WebException e) { try { if (e.Response != null) using (var r = new StreamReader(e.Response.GetResponseStream())) { var msg = Field(r.ReadToEnd(), "message"); // Laravel dodaje "(and 2 more errors)", a nama treba samo prva poruka if (!string.IsNullOrEmpty(msg)) return Regex.Replace(msg, @"\s*\(and \d+ more errors?\)$", ""); } } catch { } return "Nema veze sa serverom."; } public static int Status(WebException e) { var r = e.Response as HttpWebResponse; return r == null ? 0 : (int)r.StatusCode; } public static string Login(string user, string pass) { return Post("/client/login", "{\"username\":\"" + Esc(user) + "\",\"password\":\"" + Esc(pass) + "\"}", null); } public static string Heartbeat(string token, string[] procs, string presence) { return Post("/client/heartbeat", "{\"presence\":\"" + Esc(presence ?? "online") + "\",\"processes\":[" + string.Join(",", procs.Select(p => "\"" + Esc(p) + "\"")) + "]}", token); } public static string Upload(string token, string file) { return UploadTo("/client/screenshot", token, file); } public static string UploadTo(string path, string token, string file) { using (var wc = Client(token)) return Encoding.UTF8.GetString(wc.UploadFile(Base + path, "POST", file)); } } class Friend { public string Username, Color, Avatar, Presence, Status, Game, GameIcon, Since, Voice; public int Unread; public string Kind { get { return Theme.Kind(Status, Presence); } } } class VRoom { public string Key, Label, Owner, Privacy; public bool Custom, Mine; public int Limit; public List Members = new List(); } class VMember { public string Name, Color, Avatar; public bool Speaking; } class FriendReq { public long Id; public string Username, Color, Avatar; public bool Incoming; } class ChatMsg { public long Id; public bool Mine; public string Body; public DateTime At; } class NotifItem { public string Id, Who, WhoColor, Text, Extra, Icon, Url, Ago; public bool New; } class LfgItem { public long Id; public string Game, GameName, Author, Note; public int Slots, ExpiresMin; public List Joined = new List(); public bool Mine, JoinedByMe; } // Kratki zvuk za novu poruku, napravljen u memoriji static class Ding { static byte[] wav, ringIn, ringOut; static System.Media.SoundPlayer loop; // Zvonjenje dok traje poziv (dolazni ili odlazni), u petlji dok se ne zaustavi public static void Ring(bool incoming) { StopRing(); try { if (incoming && ringIn == null) ringIn = Pattern(true); if (!incoming && ringOut == null) ringOut = Pattern(false); loop = new System.Media.SoundPlayer(new MemoryStream(incoming ? ringIn : ringOut)); loop.PlayLooping(); } catch { } } public static void StopRing() { try { if (loop != null) loop.Stop(); } catch { } loop = null; } static byte[] Pattern(bool incoming) { const int rate = 22050; var pcm = new List(); Action tone = (f1, f2, dur, vol) => { int n = (int)(rate * dur); for (int i = 0; i < n; i++) { double t = (double)i / rate, env = Math.Min(1.0, Math.Min(i, n - i) / (rate * 0.01)); pcm.Add((short)((Math.Sin(2 * Math.PI * f1 * t) + Math.Sin(2 * Math.PI * f2 * t)) * 0.5 * env * vol * short.MaxValue)); } }; Action pause = dur => { for (int i = 0; i < (int)(rate * dur); i++) pcm.Add(0); }; if (incoming) { // dva kratka zvona pa stanka, kao telefon for (int k = 0; k < 2; k++) { tone(784, 988, 0.12, 0.28); pause(0.04); tone(988, 1175, 0.16, 0.28); pause(0.18); } pause(1.2); } else { // ton zvonjenja koji cuje pozivatelj tone(425, 450, 1.0, 0.12); pause(2.0); } return ToWav(pcm, rate); } static byte[] ToWav(List pcm, int rate) { using (var ms = new MemoryStream()) using (var w = new BinaryWriter(ms)) { w.Write(Encoding.ASCII.GetBytes("RIFF")); w.Write(36 + pcm.Count * 2); w.Write(Encoding.ASCII.GetBytes("WAVEfmt ")); w.Write(16); w.Write((short)1); w.Write((short)1); w.Write(rate); w.Write(rate * 2); w.Write((short)2); w.Write((short)16); w.Write(Encoding.ASCII.GetBytes("data")); w.Write(pcm.Count * 2); foreach (var s in pcm) w.Write(s); w.Flush(); return ms.ToArray(); } } public static void Play() { try { if (wav == null) wav = Make(); new System.Media.SoundPlayer(new MemoryStream(wav)).Play(); } catch { } } static byte[] Make() { const int rate = 22050; var pcm = new List(); Action tone = (freq, dur) => { int n = (int)(rate * dur); for (int i = 0; i < n; i++) { double t = (double)i / rate, env = Math.Min(1.0, i / (rate * 0.004)) * Math.Exp(-t * 16); pcm.Add((short)(Math.Sin(2 * Math.PI * freq * t) * env * 0.3 * short.MaxValue)); } }; tone(988, 0.11); tone(1319, 0.24); using (var ms = new MemoryStream()) using (var w = new BinaryWriter(ms)) { w.Write(Encoding.ASCII.GetBytes("RIFF")); w.Write(36 + pcm.Count * 2); w.Write(Encoding.ASCII.GetBytes("WAVEfmt ")); w.Write(16); w.Write((short)1); w.Write((short)1); w.Write(rate); w.Write(rate * 2); w.Write((short)2); w.Write((short)16); w.Write(Encoding.ASCII.GetBytes("data")); w.Write(pcm.Count * 2); foreach (var s in pcm) w.Write(s); w.Flush(); return ms.ToArray(); } } } } // ===================== Controls.cs ===================== using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; namespace Fragline { // Glatki prijelaz 0..1 (hover, fokus, prekidac); tajmer radi samo dok animacija traje class Fx { readonly Control c; readonly System.Windows.Forms.Timer t = new System.Windows.Forms.Timer { Interval = 15 }; readonly float speed; float target; public float V; public Fx(Control c, float speed = 0.3f) { this.c = c; this.speed = speed; t.Tick += (s, e) => { V += (target - V) * this.speed; if (Math.Abs(target - V) < 0.02f) { V = target; t.Stop(); } if (!c.IsDisposed) c.Invalidate(); }; c.Disposed += (s, e) => t.Dispose(); } public void To(float v) { if (v == target) return; target = v; if (Theme.ClassicLayout || !c.IsHandleCreated || !c.Visible) { V = v; c.Invalidate(); return; } t.Start(); } public void Set(float v) { target = v; V = v; t.Stop(); c.Invalidate(); } public static Color Mix(Color a, Color b, float k) { k = Math.Max(0f, Math.Min(1f, k)); return Color.FromArgb((int)(a.A + (b.A - a.A) * k), (int)(a.R + (b.R - a.R) * k), (int)(a.G + (b.G - a.G) * k), (int)(a.B + (b.B - a.B) * k)); } // obrub u prijelazu boje (za fokus i otvorene izbornike) public static void GlowBorder(Graphics g, GraphicsPath p, RectangleF r, float k, float width) { if (k <= 0.01f) return; int a = (int)(255 * k); using (var br = new LinearGradientBrush(new RectangleF(r.X - 1, r.Y - 1, r.Width + 2, r.Height + 2), Color.FromArgb(a, Theme.Accent), Color.FromArgb(a, Theme.Accent2), 0f)) using (var pen = new Pen(br, width)) g.DrawPath(pen, p); } // staklena povrsina: blagi sjaj gore koji se gubi prema sredini public static void Gloss(Graphics g, GraphicsPath p, RectangleF r, int alpha) { var st = g.Save(); g.SetClip(p, CombineMode.Intersect); var top = new RectangleF(r.X, r.Y, r.Width, r.Height * 0.55f + 1); using (var b = new LinearGradientBrush(top, Color.FromArgb(alpha, 255, 255, 255), Color.FromArgb(0, 255, 255, 255), 90f)) g.FillRectangle(b, top); g.Restore(st); } } class FlatButton : Control, IButtonControl { public bool Primary, Danger; public string Glyph; public Color? Fore, Back; bool hover, down, fixingFont; readonly Fx fx; public DialogResult DialogResult { get; set; } public FlatButton(string text, bool primary) { Text = text; Primary = primary; SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint | ControlStyles.ResizeRedraw | ControlStyles.Selectable, true); Cursor = Cursors.Hand; Font = Theme.Display(14); TabStop = true; fx = new Fx(this); } // podebljani font zamijeni polu-debelim (Segoe UI Semibold), kao na Discordu protected override void OnFontChanged(EventArgs e) { if (!fixingFont && Font.Bold && !Theme.DisplayBold) { fixingFont = true; Font = new Font(Theme.DisplayFace, Font.Size, FontStyle.Regular, Font.Unit); fixingFont = false; } base.OnFontChanged(e); } public void NotifyDefault(bool value) { } public void PerformClick() { if (Enabled) OnClick(EventArgs.Empty); } protected override void OnMouseEnter(EventArgs e) { hover = true; fx.To(1); Invalidate(); base.OnMouseEnter(e); } protected override void OnMouseLeave(EventArgs e) { hover = false; down = false; fx.To(0); Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseDown(MouseEventArgs e) { down = true; Focus(); Invalidate(); base.OnMouseDown(e); } protected override void OnMouseUp(MouseEventArgs e) { down = false; Invalidate(); base.OnMouseUp(e); } protected override void OnEnabledChanged(EventArgs e) { Invalidate(); base.OnEnabledChanged(e); } protected override void OnGotFocus(EventArgs e) { Invalidate(); base.OnGotFocus(e); } protected override void OnLostFocus(EventArgs e) { Invalidate(); base.OnLostFocus(e); } protected override void OnTextChanged(EventArgs e) { Invalidate(); base.OnTextChanged(e); } protected override void OnKeyDown(KeyEventArgs e) { if (e.KeyCode == Keys.Space || e.KeyCode == Keys.Enter) PerformClick(); base.OnKeyDown(e); } protected override void OnPaint(PaintEventArgs e) { if (Theme.ClassicLayout) { PaintClassic(e); return; } // "Glass": pilula; glavni gumb u svjetlecem prijelazu boje sa sjajem gore, // sporedni je prozirno staklo s obrubom u prijelazu boje koji zasvijetli pod misem var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; var pbg = Parent != null ? Parent.BackColor : Theme.Bg; g.Clear(pbg); float h = fx.V, inset = down ? 1.5f : 0.5f; var r = new RectangleF(inset, inset, Width - 1 - inset * 2, Height - 1 - inset * 2); bool secondary = !Primary && !Danger && Back == null; Color fc; using (var p = Theme.Round(r, Math.Min(r.Height / 2f, Theme.D(22)))) { if (secondary) { var fill = Fx.Mix(pbg, Theme.Light ? Color.Black : Color.White, (Theme.Light ? 0.04f : 0.07f) + 0.06f * h); using (var b = new SolidBrush(fill)) g.FillPath(b, p); using (var pen = new Pen(Fx.Mix(pbg, Theme.Light ? Color.Black : Color.White, 0.14f), 1f)) g.DrawPath(pen, p); Fx.GlowBorder(g, p, r, h * 0.9f, 1.4f); fc = Fore ?? Theme.Text; } else { Color c1, c2; if (Back != null) { c1 = Back.Value; c2 = Theme.Shade(Back.Value, -0.08f); } else if (Danger) { c1 = ColorTranslator.FromHtml("#ff5f6d"); c2 = ColorTranslator.FromHtml("#e0344a"); } else if (Theme.Accent2 != Theme.Accent) { c1 = Theme.Accent; c2 = Theme.Accent2; } else { c1 = Theme.Button; c2 = Theme.Shade(Theme.Button, -0.12f); } float sh = down ? -0.1f : 0.1f * h; c1 = Theme.Shade(c1, sh); c2 = Theme.Shade(c2, sh); if (!Enabled) { c1 = Fx.Mix(c1, pbg, 0.55f); c2 = Fx.Mix(c2, pbg, 0.55f); } using (var b = new LinearGradientBrush(new RectangleF(r.X - 1, r.Y - 1, r.Width + 2, r.Height + 2), c1, c2, 0f)) g.FillPath(b, p); Fx.Gloss(g, p, r, Enabled ? 34 + (int)(20 * h) : 14); // tanka svijetla crta uz gornji rub using (var br = new LinearGradientBrush(new RectangleF(r.X, r.Y - 1, r.Width, r.Height + 2), Color.FromArgb(Enabled ? 70 : 25, 255, 255, 255), Color.FromArgb(0, 255, 255, 255), 90f)) using (var pen = new Pen(br, 1f)) g.DrawPath(pen, p); fc = Fore ?? (Back == null && !Danger && Theme.Accent2 == Theme.Accent ? Theme.OnAccent : Color.White); } if (Focused && !down && ShowFocusCues) { var fr = r; fr.Inflate(-2.5f, -2.5f); using (var p2 = Theme.Round(fr, Math.Min(fr.Height / 2f, Theme.D(20)))) using (var pen = new Pen(Color.FromArgb(150, 255, 255, 255), 1.2f)) g.DrawPath(pen, p2); } } if (!Enabled) fc = Color.FromArgb(150, fc); var tr = Rectangle.Round(r); if (down) tr.Offset(0, 1); const TextFormatFlags tf = TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix; if (Glyph != null) { var tsz = TextRenderer.MeasureText(Text, Font, Size.Empty, TextFormatFlags.NoPadding); int gw = Theme.D(18), gap = Theme.D(8), total = gw + (Text.Length > 0 ? gap + tsz.Width : 0); int gx = tr.X + (tr.Width - total) / 2; Theme.Icon(g, Glyph, "", new Rectangle(gx, tr.Y, gw, tr.Height), fc, 14); if (Text.Length > 0) TextRenderer.DrawText(g, Text, Font, new Rectangle(gx + gw + gap, tr.Y, tsz.Width + 2, tr.Height), fc, TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding | TextFormatFlags.NoPrefix); return; } TextRenderer.DrawText(g, Text, Font, tr, fc, tf); } // stari izgled (Xfire raspored) void PaintClassic(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(Parent != null ? Parent.BackColor : Theme.Bg); var r = new RectangleF(0.5f, 0.5f, Width - 1.5f, Height - 1.5f); bool secondary = !Primary && !Danger && Back == null; Color bg = Back ?? (Danger ? ColorTranslator.FromHtml("#da373c") : Primary ? Theme.Button : Theme.Secondary); // sekundarni gumb pod misem posvijetli, ostali potamne (kao na Discordu) if (hover) bg = secondary && !Theme.Light ? Theme.Shade(bg, 0.14f) : Theme.Shade(bg, -0.14f); if (down) bg = Theme.Shade(bg, -0.12f); if (!Enabled) bg = Color.FromArgb(Math.Max(0, bg.A - 0), Theme.Shade(bg, -0.3f)); // web .btn: radius 12 (ne puna pilula); primarni je narančasti gradijent bool grad = Primary && !Danger && Back == null && Theme.Accent2 != Theme.Accent && !Theme.Light; using (var p = Theme.Round(r, Theme.ClassicLayout ? Theme.D(4) : Theme.D(12))) { if (grad && Width > 0 && Height > 0) { float sh = down ? -0.12f : hover ? 0.08f : 0f; using (var b = new LinearGradientBrush(Rectangle.Round(r), Theme.Shade(Theme.Accent, sh), Theme.Shade(Theme.Accent2, sh), 0f)) g.FillPath(b, p); } else using (var b = new SolidBrush(bg)) g.FillPath(b, p); if (Focused && !down) { var fr = r; fr.Inflate(-1.5f, -1.5f); using (var p2 = Theme.Round(fr, Theme.D(3))) using (var pen = new Pen(Color.FromArgb(150, 255, 255, 255), 1.4f)) g.DrawPath(pen, p2); } } var baseFc = Fore ?? (Primary && !Danger && Back == null ? Theme.OnAccent : Color.White); var fc = Enabled ? baseFc : Color.FromArgb(160, baseFc); var tr = Rectangle.Round(r); if (Glyph != null) { var tsz = TextRenderer.MeasureText(Text, Font, Size.Empty, TextFormatFlags.NoPadding); int gw = Theme.D(18), gap = Theme.D(8), total = gw + (Text.Length > 0 ? gap + tsz.Width : 0); int gx = tr.X + (tr.Width - total) / 2; Theme.Icon(g, Glyph, "", new Rectangle(gx, tr.Y, gw, tr.Height), fc, 14); if (Text.Length > 0) TextRenderer.DrawText(g, Text, Font, new Rectangle(gx + gw + gap, tr.Y, tsz.Width + 2, tr.Height), fc, bg, TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding); return; } TextRenderer.DrawText(g, Text, Font, tr, fc, bg, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis); } } class InputBox : Control { public readonly TextBox Box = new TextBox(); readonly Fx focus, hov; public InputBox(bool password) { SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); Box.BorderStyle = BorderStyle.None; Box.BackColor = Theme.Field; Box.ForeColor = Theme.Text; Box.Font = Theme.Body(15); Box.UseSystemPasswordChar = password; focus = new Fx(this); hov = new Fx(this); Box.GotFocus += (s, e) => focus.To(1); Box.LostFocus += (s, e) => focus.To(0); Box.MouseEnter += (s, e) => hov.To(1); Box.MouseLeave += (s, e) => { if (!ClientRectangle.Contains(PointToClient(MousePosition))) hov.To(0); }; Controls.Add(Box); Cursor = Cursors.IBeam; } public string Value { get { return Box.Text; } } protected override void OnClick(EventArgs e) { Box.Focus(); base.OnClick(e); } protected override void OnMouseEnter(EventArgs e) { hov.To(1); base.OnMouseEnter(e); } protected override void OnMouseLeave(EventArgs e) { if (!ClientRectangle.Contains(PointToClient(MousePosition))) hov.To(0); base.OnMouseLeave(e); } protected override void OnResize(EventArgs e) { base.OnResize(e); int pad = Theme.ClassicLayout ? Theme.D(12) : Theme.D(16); Box.SetBounds(pad, (Height - Box.PreferredHeight) / 2, Math.Max(10, Width - pad * 2), Box.PreferredHeight); } protected override void OnPaint(PaintEventArgs e) { if (Theme.ClassicLayout) { PaintClassic(e); return; } // zaobljeno polje; pod misem obrub posvijetli, u fokusu zasvijetli u prijelazu boje var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; var pbg = Parent != null ? Parent.BackColor : Theme.Bg; g.Clear(pbg); var r = new RectangleF(0.75f, 0.75f, Width - 2f, Height - 2f); using (var p = Theme.Round(r, Math.Min(Theme.D(12), r.Height / 2f))) { using (var b = new SolidBrush(Theme.Field)) g.FillPath(b, p); var line = Fx.Mix(Theme.Line, Theme.Muted, 0.5f * hov.V); using (var pen = new Pen(Color.FromArgb((int)(255 * (1 - focus.V)), line), 1f)) g.DrawPath(pen, p); Fx.GlowBorder(g, p, r, focus.V, 1.6f); } } // stari izgled (Xfire raspored) void PaintClassic(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(Parent != null ? Parent.BackColor : Theme.Bg); using (var p = Theme.Round(new RectangleF(0.75f, 0.75f, Width - 2f, Height - 2f), Theme.D(4))) { using (var b = new SolidBrush(Theme.Field)) g.FillPath(b, p); if (Box.Focused) using (var pen = new Pen(Theme.Link, 1.4f)) g.DrawPath(pen, p); } } } class Toggle : Control { bool on; readonly Fx pos; public event EventHandler CheckedChanged; public bool Checked { get { return on; } set { if (on != value) { on = value; pos.To(on ? 1 : 0); Invalidate(); } } } public Toggle() { SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.Selectable, true); Size = new Size(Theme.D(42), Theme.D(24)); Cursor = Cursors.Hand; TabStop = true; pos = new Fx(this, 0.35f); } protected override void OnClick(EventArgs e) { on = !on; pos.To(on ? 1 : 0); Invalidate(); if (CheckedChanged != null) CheckedChanged(this, EventArgs.Empty); base.OnClick(e); } protected override void OnKeyDown(KeyEventArgs e) { if (e.KeyCode == Keys.Space) OnClick(EventArgs.Empty); base.OnKeyDown(e); } protected override void OnGotFocus(EventArgs e) { Invalidate(); base.OnGotFocus(e); } protected override void OnLostFocus(EventArgs e) { Invalidate(); base.OnLostFocus(e); } protected override void OnPaint(PaintEventArgs e) { if (Theme.ClassicLayout) { PaintClassic(e); return; } // prekidac: staza se ispuni prijelazom boje, gumbic klizi i ima mekanu sjenu var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; var pbg = Parent != null ? Parent.BackColor : Theme.Bg; g.Clear(pbg); float k = pos.V; var r = new RectangleF(1, 1, Width - 3, Height - 3); using (var p = Theme.Round(r, r.Height / 2)) { using (var b = new SolidBrush(Fx.Mix(pbg, Theme.Light ? Color.Black : Color.White, Theme.Light ? 0.14f : 0.12f))) g.FillPath(b, p); if (k > 0.01f) using (var b = new LinearGradientBrush(Rectangle.Round(r), Color.FromArgb((int)(255 * k), Theme.Accent), Color.FromArgb((int)(255 * k), Theme.Accent2), 0f)) g.FillPath(b, p); if (Focused) using (var pen = new Pen(Color.FromArgb(140, 255, 255, 255), 1.2f)) g.DrawPath(pen, p); } float d = r.Height - Theme.D(6); float x = r.X + Theme.D(3) + (r.Width - d - Theme.D(6)) * k; using (var b = new SolidBrush(Color.FromArgb(60, 0, 0, 0))) g.FillEllipse(b, x, r.Y + Theme.D(4), d, d); using (var b = new SolidBrush(Color.White)) g.FillEllipse(b, x, r.Y + Theme.D(3), d, d); } // stari izgled (Xfire raspored) void PaintClassic(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(Parent != null ? Parent.BackColor : Theme.Bg); var r = new RectangleF(1, 1, Width - 3, Height - 3); using (var p = Theme.Round(r, r.Height / 2)) { if (on && !Theme.ClassicLayout) using (var b = new LinearGradientBrush(Rectangle.Round(r), Theme.Accent, Theme.Accent2, 0f)) g.FillPath(b, p); else using (var b = new SolidBrush(on ? Theme.On : Theme.Off)) g.FillPath(b, p); if (Focused) using (var pen = new Pen(Theme.Link, 1.5f)) g.DrawPath(pen, p); } float k = r.Height - Theme.D(6); float x = on ? r.Right - k - Theme.D(3) : r.X + Theme.D(3); using (var b = new SolidBrush(Color.White)) g.FillEllipse(b, x, r.Y + Theme.D(3), k, k); } } class LogoMark : Control { public float TextPx = 22; public LogoMark() { SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.SupportsTransparentBackColor, true); BackColor = Color.Transparent; } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; Theme.DrawFlame(g, new RectangleF(0, 0, Height, Height)); using (var f = Theme.Display(TextPx)) TextRenderer.DrawText(g, "Fragline", f, new Rectangle(Height + Theme.D(8), 0, Width, Height), Theme.Text, TextFormatFlags.VerticalCenter | TextFormatFlags.Left | TextFormatFlags.NoPadding); } } class DarkForm : Form { protected int TitleH; ClassicBar bar; bool resizable; // prozori koji sami slazu svoje kontrole (npr. glavni Xfire prozor) sami uzimaju u obzir TitleH protected virtual bool ManualLayout { get { return false; } } protected override CreateParams CreateParams { get { var cp = base.CreateParams; if (Theme.ClassicLayout) cp.ClassStyle |= 0x20000; return cp; } } public DarkForm() { AutoScaleMode = AutoScaleMode.None; BackColor = Theme.Bg; ForeColor = Theme.Text; Font = Theme.Body(14); Icon = Program.AppIcon; StartPosition = FormStartPosition.CenterScreen; FormBorderStyle = FormBorderStyle.FixedSingle; MaximizeBox = false; } protected override void OnHandleCreated(EventArgs e) { base.OnHandleCreated(e); Theme.DarkTitle(this); } protected override void OnLoad(EventArgs e) { if (Theme.ClassicLayout && FormBorderStyle != FormBorderStyle.None) UseClassicChrome(); base.OnLoad(e); } void UseClassicChrome() { resizable = FormBorderStyle == FormBorderStyle.Sizable; var cs = ClientSize; FormBorderStyle = FormBorderStyle.None; TitleH = Theme.D(30); int edge = resizable ? Theme.D(3) : 0; MaximizedBounds = Screen.FromHandle(Handle).WorkingArea; if (!ManualLayout) { ClientSize = new Size(cs.Width + edge * 2, cs.Height + TitleH + edge); // kontrole se pomaknu ispod trake; one vezane za dno je vec pomaknulo povecanje prozora foreach (Control c in Controls) { if (c.Dock != DockStyle.None) continue; bool top = (c.Anchor & AnchorStyles.Top) != 0, bottom = (c.Anchor & AnchorStyles.Bottom) != 0; if (top && bottom) { c.Top += TitleH; c.Height -= TitleH; } else if (top || !bottom) c.Top += TitleH; c.Left += edge; } Padding = new Padding(Padding.Left + edge, Padding.Top + TitleH, Padding.Right + edge, Padding.Bottom + edge); Top = Math.Max(Screen.FromHandle(Handle).WorkingArea.Top, Top - TitleH / 2); } else ClientSize = cs; bar = new ClassicBar(this) { Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right }; bar.SetBounds(0, 0, ClientSize.Width, TitleH); Controls.Add(bar); bar.BringToFront(); Invalidate(); } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); if (bar == null) { // moderni izgled: tanka svjetleca crta u prijelazu boje uz gornji rub prozora if (!Theme.ClassicLayout && ClientSize.Width > 0) { var r = new Rectangle(0, 0, ClientSize.Width, Theme.D(3)); using (var b = new System.Drawing.Drawing2D.LinearGradientBrush(new Rectangle(-1, 0, r.Width + 2, r.Height), Theme.Accent, Theme.Accent2, 0f)) e.Graphics.FillRectangle(b, r); using (var b = new System.Drawing.Drawing2D.LinearGradientBrush(new Rectangle(0, r.Bottom - 1, r.Width, Theme.D(28)), Color.FromArgb(28, Theme.Accent2), Color.FromArgb(0, Theme.Accent2), 90f)) e.Graphics.FillRectangle(b, 0, r.Bottom, r.Width, Theme.D(26)); } return; } // tanki plavi okvir oko prozora bez Windowsovog ruba using (var pen = new Pen(Xf.Border)) e.Graphics.DrawRectangle(pen, 0, 0, ClientSize.Width - 1, ClientSize.Height - 1); } protected override void OnResize(EventArgs e) { base.OnResize(e); if (bar != null) Invalidate(); } // rubovi za mijenjanje velicine prozora bez Windowsovog okvira protected override void WndProc(ref Message m) { base.WndProc(ref m); if (m.Msg != 0x84 || bar == null || !resizable || WindowState != FormWindowState.Normal) return; int lp = m.LParam.ToInt32(); var p = PointToClient(new Point((short)(lp & 0xFFFF), (short)((lp >> 16) & 0xFFFF))); int g = Theme.D(6); bool l = p.X < g, r = p.X >= ClientSize.Width - g, t = p.Y < Theme.D(4), b = p.Y >= ClientSize.Height - g; int ht = t && l ? 13 : t && r ? 14 : b && l ? 16 : b && r ? 17 : l ? 10 : r ? 11 : t ? 12 : b ? 15 : 0; if (ht != 0) m.Result = (IntPtr)ht; } protected Label L(string text, Font f, Color c, int x, int y, int w = 0) { var l = new Label { Text = text, Font = f, ForeColor = c, BackColor = Color.Transparent, Location = new Point(x, y), AutoSize = w == 0 }; if (w > 0) l.Size = new Size(w, f.Height * 2 + Theme.D(4)); Controls.Add(l); return l; } } // Sjajna plava naslovna traka za prozore u Xfire rasporedu: plamen, naslov i gumbi -, kvadrat, x class ClassicBar : Control { readonly Form f; int hot = -1; public ClassicBar(Form f) { this.f = f; SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); f.TextChanged += (s, e) => Invalidate(); f.Resize += (s, e) => Invalidate(); } // 0 = zatvori, 1 = povecaj, 2 = smanji (zdesna nalijevo) List Buttons() { var l = new List { 0 }; if (f.MaximizeBox) l.Add(1); if (f.MinimizeBox) l.Add(2); return l; } Rectangle Btn(int idx) { int w = Theme.D(42); return new Rectangle(Width - w * (idx + 1) - Theme.D(1), 0, w, Height); } int Hit(Point p) { var b = Buttons(); for (int i = 0; i < b.Count; i++) if (Btn(i).Contains(p)) return b[i]; return -1; } protected override void OnMouseMove(MouseEventArgs e) { int h = Hit(e.Location); if (h != hot) { hot = h; Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseLeave(EventArgs e) { hot = -1; Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseDown(MouseEventArgs e) { if (e.Button == MouseButtons.Left && Hit(e.Location) < 0) { if (e.Clicks >= 2 && f.MaximizeBox) { Toggle(); return; } // povlacenje prozora za traku Native.ReleaseCapture(); Native.SendMessage(f.Handle, 0xA1, (IntPtr)2, IntPtr.Zero); return; } base.OnMouseDown(e); } protected override void OnMouseClick(MouseEventArgs e) { int h = Hit(e.Location); if (h == 0) f.Close(); else if (h == 1) Toggle(); else if (h == 2) f.WindowState = FormWindowState.Minimized; base.OnMouseClick(e); } void Toggle() { f.WindowState = f.WindowState == FormWindowState.Maximized ? FormWindowState.Normal : FormWindowState.Maximized; } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; Xf.Gloss(g, ClientRectangle, Xf.Head1, Xf.Head2); Theme.DrawFlame(g, new RectangleF(Theme.D(8), (Height - Theme.D(18)) / 2f, Theme.D(16), Theme.D(18))); var bs = Buttons(); using (var fn = Xf.Font(12.5f, FontStyle.Bold)) Xf.Draw(g, f.Text, fn, new Rectangle(Theme.D(30), 0, Math.Max(10, Width - Theme.D(42) * bs.Count - Theme.D(40)), Height), Color.White); for (int i = 0; i < bs.Count; i++) { var r = Btn(i); int k = bs[i]; if (hot == k) using (var b = new SolidBrush(k == 0 ? Color.FromArgb(232, 17, 35) : Color.FromArgb(45, 255, 255, 255))) g.FillRectangle(b, r); string glyph = k == 0 ? "\uE8BB" : k == 1 ? (f.WindowState == FormWindowState.Maximized ? "\uE923" : "\uE922") : "\uE921"; Theme.Icon(g, glyph, k == 0 ? "x" : k == 1 ? "o" : "_", r, Color.White, 10); } } } } // ===================== Controls2.cs ===================== using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; namespace Fragline { // Klizac u stilu Discorda: tanka traka, uspravna rucica i vrijednost u oblacicu dok se vuce. // Za mikrofon prikazuje i razinu uzivo: zeleno = salje se, zuto = ispod praga. class RangeSlider : Control { public float Min, Max = 100; float val; public float Level = float.NaN, Marker = float.NaN; public bool Locked; public float[] Ticks; public float Snap; public Func Format; public event EventHandler ValueChanged; bool dragging, overKnob; static readonly ToolTip bubble = new ToolTip { OwnerDraw = true, UseAnimation = false, UseFading = false }; static readonly Font BubbleFont = Theme.Body(13, FontStyle.Bold); static string bubbleText = ""; static RangeSlider() { bubble.Popup += (s, e) => { var sz = TextRenderer.MeasureText(bubbleText, BubbleFont); e.ToolTipSize = new Size(sz.Width + Theme.D(16), sz.Height + Theme.D(10)); }; bubble.Draw += (s, e) => { e.Graphics.Clear(Theme.Menu); TextRenderer.DrawText(e.Graphics, e.ToolTipText, BubbleFont, e.Bounds, Theme.Text, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPrefix); }; } public RangeSlider() { SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.Selectable, true); Cursor = Cursors.Hand; TabStop = true; } public float Value { get { return val; } set { float v = Math.Max(Min, Math.Min(Max, value)); if (Math.Abs(v - val) > 0.0001f) { val = v; Invalidate(); } } } public bool Dragging { get { return dragging; } } float Pos(float v) { return Math.Max(0f, Math.Min(1f, (v - Min) / Math.Max(0.0001f, Max - Min))); } RectangleF Track() { float pad = Theme.D(12), h = Theme.D(8); float cy = Height / 2f - (Ticks != null ? Theme.D(4) : 0); return new RectangleF(pad, cy - h / 2, Math.Max(1f, Width - pad * 2), h); } RectangleF Knob() { var t = Track(); float x = t.X + t.Width * Pos(val), w = Theme.D(10), h = Theme.D(24); return new RectangleF(x - w / 2, t.Y + t.Height / 2 - h / 2, w, h); } string Label(float v) { return Format != null ? Format(v) : ((int)Math.Round(v)).ToString(); } void ShowBubble() { if (!IsHandleCreated) return; var k = Knob(); bubbleText = Label(val); var sz = TextRenderer.MeasureText(bubbleText, BubbleFont); bubble.Show(bubbleText, this, (int)(k.X + k.Width / 2 - (sz.Width + Theme.D(16)) / 2f), (int)k.Y - sz.Height - Theme.D(18)); } void HideBubble() { try { bubble.Hide(this); } catch { } } void SetValue(float v) { if (Snap > 0 && Ticks != null) foreach (var tk in Ticks) if (Math.Abs(v - tk) <= Snap) { v = tk; break; } Value = v; if (ValueChanged != null) ValueChanged(this, EventArgs.Empty); } void SetFromX(int x) { var t = Track(); SetValue(Min + (Max - Min) * Math.Max(0f, Math.Min(1f, (x - t.X) / t.Width))); } protected override void OnMouseDown(MouseEventArgs e) { if (!Locked && Enabled && e.Button == MouseButtons.Left) { dragging = true; Capture = true; Focus(); SetFromX(e.X); ShowBubble(); } base.OnMouseDown(e); } protected override void OnMouseMove(MouseEventArgs e) { if (dragging) { SetFromX(e.X); ShowBubble(); } else { var k = Knob(); k.Inflate(Theme.D(6), Theme.D(4)); bool o = !Locked && k.Contains(e.Location); if (o != overKnob) { overKnob = o; if (o) ShowBubble(); else HideBubble(); Invalidate(); } } base.OnMouseMove(e); } protected override void OnMouseUp(MouseEventArgs e) { dragging = false; Capture = false; if (!overKnob) HideBubble(); Invalidate(); base.OnMouseUp(e); } protected override void OnMouseLeave(EventArgs e) { overKnob = false; if (!dragging) HideBubble(); Invalidate(); base.OnMouseLeave(e); } // Kotacic misa pomice prozor, a ne mijenja vrijednost (inace se glasnoca slucajno mijenja pri skrolanju) protected override void OnMouseWheel(MouseEventArgs e) { var sc = Parent as ScrollableControl; if (sc != null && sc.AutoScroll) { var p = sc.AutoScrollPosition; sc.AutoScrollPosition = new Point(-p.X, Math.Max(0, -p.Y - e.Delta / 2)); } var h = e as HandledMouseEventArgs; if (h != null) h.Handled = true; } protected override bool IsInputKey(Keys k) { return k == Keys.Left || k == Keys.Right || k == Keys.Up || k == Keys.Down || base.IsInputKey(k); } protected override void OnKeyDown(KeyEventArgs e) { if (!Locked && Enabled) { float step = (Max - Min) / 50f; if (e.KeyCode == Keys.Left || e.KeyCode == Keys.Down) SetValue(val - step); else if (e.KeyCode == Keys.Right || e.KeyCode == Keys.Up) SetValue(val + step); else if (e.KeyCode == Keys.PageDown) SetValue(val - step * 5); else if (e.KeyCode == Keys.PageUp) SetValue(val + step * 5); else if (e.KeyCode == Keys.Home) SetValue(Min); else if (e.KeyCode == Keys.End) SetValue(Max); } base.OnKeyDown(e); } protected override void OnGotFocus(EventArgs e) { Invalidate(); base.OnGotFocus(e); } protected override void OnLostFocus(EventArgs e) { HideBubble(); Invalidate(); base.OnLostFocus(e); } protected override void OnEnabledChanged(EventArgs e) { Invalidate(); base.OnEnabledChanged(e); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(Parent != null ? Parent.BackColor : Theme.Bg); var t = Track(); float r = t.Height / 2; float gate = Locked && !float.IsNaN(Marker) ? Marker : val; using (var p = Theme.Round(t, r)) using (var b = new SolidBrush(Theme.Track)) g.FillPath(b, p); if (!float.IsNaN(Level)) { // podloga: lijevo od praga prigušeno žuto, desno prigušeno zeleno float gx = t.X + t.Width * Pos(gate); using (var p = Theme.Round(t, r)) { var st = g.Save(); g.SetClip(p); using (var b = new SolidBrush(Color.FromArgb(70, Theme.Idle))) g.FillRectangle(b, t.X, t.Y, gx - t.X, t.Height); using (var b = new SolidBrush(Color.FromArgb(70, Theme.On))) g.FillRectangle(b, gx, t.Y, t.Right - gx, t.Height); float w = t.Width * Pos(Level); if (w > 1) using (var b = new SolidBrush(Level >= gate ? Theme.On : Theme.Idle)) g.FillRectangle(b, t.X, t.Y, w, t.Height); g.Restore(st); } } else if (!Locked) { float w = t.Width * Pos(val); if (w > 1) using (var p = Theme.Round(new RectangleF(t.X, t.Y, Math.Max(w, t.Height), t.Height), r)) using (var b = new LinearGradientBrush(new RectangleF(t.X, t.Y, Math.Max(w, t.Height) + 1, t.Height), Enabled ? Theme.Accent : Theme.Muted, Enabled ? Theme.Accent2 : Theme.Muted, 0f)) g.FillPath(b, p); } if (Ticks != null) using (var b = new SolidBrush(Theme.Muted)) foreach (var tk in Ticks) { float x = t.X + t.Width * Pos(tk); using (var p = Theme.Round(new RectangleF(x - Theme.D(1), t.Bottom + Theme.D(5), Theme.D(2), Theme.D(6)), Theme.D(1))) g.FillPath(b, p); } if (Locked) { // automatski prag: samo tanka oznaka, rucice nema if (!float.IsNaN(Marker)) { float x = t.X + t.Width * Pos(Marker); using (var p = Theme.Round(new RectangleF(x - Theme.D(1.5f), t.Y - Theme.D(6), Theme.D(3), t.Height + Theme.D(12)), Theme.D(1.5f))) using (var b = new SolidBrush(Theme.Text)) g.FillPath(b, p); } return; } var k = Knob(); using (var p = Theme.Round(new RectangleF(k.X, k.Y + Theme.D(1), k.Width, k.Height), Theme.D(3))) using (var b = new SolidBrush(Color.FromArgb(70, 0, 0, 0))) g.FillPath(b, p); using (var p = Theme.Round(k, Theme.D(3))) { using (var b = new SolidBrush(Enabled ? Color.White : Theme.Muted)) g.FillPath(b, p); using (var pen = new Pen(Color.FromArgb(60, 0, 0, 0), 1f)) g.DrawPath(pen, p); } if (Focused || dragging || overKnob) { var fr = k; fr.Inflate(Theme.D(2.5f), Theme.D(2.5f)); using (var p = Theme.Round(fr, Theme.D(5))) using (var pen = new Pen(Color.FromArgb(Focused ? 255 : 120, Theme.Accent), Theme.D(2))) g.DrawPath(pen, p); } } } // Padajuci izbornik u stilu Discorda (zamjena za sistemski ComboBox) class DropDown : Control { public readonly List Items = new List(); public int MaxVisible = 8; public event EventHandler SelectedIndexChanged; int sel = -1; bool hover, open; readonly Fx hv, act; ToolStripDropDown popup; DateTime closedAt; public DropDown() { SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.Selectable, true); Cursor = Cursors.Hand; TabStop = true; Font = Theme.Body(14); hv = new Fx(this); act = new Fx(this); } public int SelectedIndex { get { return sel; } set { int v = value < 0 ? -1 : Math.Min(value, Items.Count - 1); if (v == sel) return; sel = v; Invalidate(); if (SelectedIndexChanged != null) SelectedIndexChanged(this, EventArgs.Empty); } } public object SelectedItem { get { return sel >= 0 && sel < Items.Count ? Items[sel] : null; } } public void OpenList() { if (!Enabled || Items.Count == 0 || open) return; // klik na polje dok je popis otvoren ga zatvara, pa ga ne otvaraj odmah ponovno if ((DateTime.UtcNow - closedAt).TotalMilliseconds < 200) return; var list = new PopupList(this); int rows = Math.Min(MaxVisible, Items.Count); list.Size = new Size(Width, rows * list.RowH + list.Pad * 2); var host = new ToolStripControlHost(list) { Margin = Padding.Empty, Padding = Padding.Empty, AutoSize = false, Size = list.Size }; popup = new ToolStripDropDown { Padding = Padding.Empty, Margin = Padding.Empty, AutoSize = false, Size = list.Size, DropShadowEnabled = true, Renderer = new DarkRenderer() }; popup.Items.Add(host); popup.Closed += (s, e) => { open = false; act.To(Focused ? 1 : 0); closedAt = DateTime.UtcNow; Invalidate(); }; if (!Theme.ClassicLayout) { // zaobljen plutajuci popis var rr = new Rectangle(0, 0, list.Width, list.Height); popup.Region = new Region(Theme.Round(new RectangleF(rr.X, rr.Y, rr.Width + 0.5f, rr.Height + 0.5f), Theme.D(12))); } open = true; act.To(1); Invalidate(); popup.Show(this, new Point(0, Height + Theme.D(Theme.ClassicLayout ? 4 : 6))); list.Focus(); } public void Pick(int i) { SelectedIndex = i; if (popup != null) popup.Close(); Focus(); } protected override void OnMouseEnter(EventArgs e) { hover = true; hv.To(1); Invalidate(); base.OnMouseEnter(e); } protected override void OnMouseLeave(EventArgs e) { hover = false; hv.To(0); Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseDown(MouseEventArgs e) { if (e.Button == MouseButtons.Left) OpenList(); base.OnMouseDown(e); } protected override void OnEnabledChanged(EventArgs e) { Cursor = Enabled ? Cursors.Hand : Cursors.Default; Invalidate(); base.OnEnabledChanged(e); } protected override void OnGotFocus(EventArgs e) { act.To(1); Invalidate(); base.OnGotFocus(e); } protected override void OnLostFocus(EventArgs e) { if (!open) act.To(0); Invalidate(); base.OnLostFocus(e); } protected override bool IsInputKey(Keys k) { return k == Keys.Up || k == Keys.Down || base.IsInputKey(k); } protected override void OnKeyDown(KeyEventArgs e) { if (e.KeyCode == Keys.Space || e.KeyCode == Keys.Enter || e.KeyCode == Keys.F4 || (e.Alt && e.KeyCode == Keys.Down)) { OpenList(); e.Handled = true; } else if (e.KeyCode == Keys.Down && sel < Items.Count - 1) SelectedIndex = sel + 1; else if (e.KeyCode == Keys.Up && sel > 0) SelectedIndex = sel - 1; base.OnKeyDown(e); } // kotacic ne mijenja odabir (kao kod sistemskog izbornika), nego pomice prozor protected override void OnMouseWheel(MouseEventArgs e) { var sc = Parent as ScrollableControl; if (sc != null && sc.AutoScroll) { var p = sc.AutoScrollPosition; sc.AutoScrollPosition = new Point(-p.X, Math.Max(0, -p.Y - e.Delta / 2)); } var h = e as HandledMouseEventArgs; if (h != null) h.Handled = true; } protected override void OnPaint(PaintEventArgs e) { if (Theme.ClassicLayout) { PaintClassic(e); return; } // isto polje kao za unos; strelica je vektorska u kruzicu i okrene se kad je popis otvoren var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; var pbg = Parent != null ? Parent.BackColor : Theme.Bg; g.Clear(pbg); var r = new RectangleF(0.75f, 0.75f, Width - 2f, Height - 2f); using (var p = Theme.Round(r, Math.Min(Theme.D(12), r.Height / 2f))) { using (var b = new SolidBrush(Fx.Mix(Theme.Field, Theme.Light ? Color.Black : Color.White, 0.03f * hv.V))) g.FillPath(b, p); var line = Fx.Mix(Theme.Line, Theme.Muted, 0.5f * hv.V); using (var pen = new Pen(Color.FromArgb((int)(255 * (1 - act.V)), line), 1f)) g.DrawPath(pen, p); Fx.GlowBorder(g, p, r, act.V, 1.6f); } int cs = Math.Min(Theme.D(24), Height - Theme.D(8)); var cr = new RectangleF(Width - cs - Theme.D(8), (Height - cs) / 2f, cs, cs); var tr = new Rectangle(Theme.D(14), 0, Math.Max(10, (int)cr.X - Theme.D(18)), Height); TextRenderer.DrawText(g, sel >= 0 ? Convert.ToString(Items[sel]) : "", Font, tr, Enabled ? Theme.Text : Theme.Muted, TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding); if (open || hv.V > 0.01f) using (var b = new SolidBrush(Color.FromArgb((int)(open ? 255 : 255 * hv.V), Fx.Mix(Theme.Field, Theme.Light ? Color.Black : Color.White, 0.1f)))) g.FillEllipse(b, cr); var cc = open ? Theme.Accent2 : Enabled ? Theme.Text2 : Theme.Muted; float cx = cr.X + cr.Width / 2f, cy = cr.Y + cr.Height / 2f, s = cr.Width * 0.2f; using (var pen = new Pen(cc, Math.Max(1.5f, Theme.D(1.7f))) { StartCap = LineCap.Round, EndCap = LineCap.Round, LineJoin = LineJoin.Round }) g.DrawLines(pen, open ? new[] { new PointF(cx - s, cy + s / 2), new PointF(cx, cy - s / 2), new PointF(cx + s, cy + s / 2) } : new[] { new PointF(cx - s, cy - s / 2), new PointF(cx, cy + s / 2), new PointF(cx + s, cy - s / 2) }); } // stari izgled (Xfire raspored) void PaintClassic(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(Parent != null ? Parent.BackColor : Theme.Bg); var r = new RectangleF(0.5f, 0.5f, Width - 1.5f, Height - 1.5f); var bg = Theme.Field; if (hover && Enabled && !open) bg = Theme.Light ? Theme.Shade(bg, -0.04f) : Theme.Shade(bg, 0.04f); using (var p = Theme.Round(r, Theme.D(4))) { using (var b = new SolidBrush(bg)) g.FillPath(b, p); if (open || Focused) using (var pen = new Pen(open ? Theme.Accent : Theme.Link, 1.4f)) g.DrawPath(pen, p); } int chev = Theme.D(28); var tr = new Rectangle(Theme.D(12), 0, Math.Max(10, Width - Theme.D(12) - chev), Height); TextRenderer.DrawText(g, sel >= 0 ? Convert.ToString(Items[sel]) : "", Font, tr, Enabled ? Theme.Text : Theme.Muted, TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding); var cr = new Rectangle(Width - chev - Theme.D(4), 0, chev, Height); Theme.Icon(g, open ? "\uE70E" : "\uE70D", open ? "▴" : "▾", cr, Enabled ? Theme.Text2 : Theme.Muted, 12); } } // Tanka klizna traka preko sistemske: lezi iznad njenog mjesta (u istom roditelju), // pokazuje zaobljeni klizac koji se moze vuci i pomice sadrzaj preko AutoScrollPosition class SlimBar : Control { readonly ScrollableControl sc; readonly Timer sync = new Timer { Interval = 120 }; readonly Fx hv; int lastVal = -1, lastMax = -1, dragOff = -1; public static void Attach(ScrollableControl sc) { if (Theme.ClassicLayout || sc == null || sc.Parent == null) return; var b = new SlimBar(sc); sc.Parent.Controls.Add(b); b.Place(); b.BringToFront(); } SlimBar(ScrollableControl sc) { this.sc = sc; SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); hv = new Fx(this); sc.Resize += (s, e) => Place(); sc.LocationChanged += (s, e) => Place(); sc.VisibleChanged += (s, e) => Place(); sc.Layout += (s, e) => Place(); sync.Tick += (s, e) => { if (!sc.Visible) return; var vs = sc.VerticalScroll; if (vs.Value != lastVal || vs.Maximum != lastMax || Visible != vs.Visible) { lastVal = vs.Value; lastMax = vs.Maximum; Place(); Invalidate(); } }; sync.Start(); Disposed += (s, e) => sync.Dispose(); } void Place() { if (sc.IsDisposed) return; int w = SystemInformation.VerticalScrollBarWidth; SetBounds(sc.Right - w, sc.Top, w, sc.Height); Visible = sc.Visible && sc.VerticalScroll.Visible; if (Visible) BringToFront(); } RectangleF Thumb() { var vs = sc.VerticalScroll; float range = Math.Max(1, vs.Maximum - vs.LargeChange + 1), track = Height - Theme.D(8); float th = Math.Max(Theme.D(32), track * vs.LargeChange / Math.Max(1f, vs.Maximum + 1)); float y = Theme.D(4) + (track - th) * Math.Min(1f, vs.Value / range); float w = Theme.D(4) + Theme.D(3) * hv.V; return new RectangleF(Width - w - Theme.D(3), y, w, th); } void ScrollTo(float thumbY) { var vs = sc.VerticalScroll; float track = Height - Theme.D(8), th = Thumb().Height, range = Math.Max(1, vs.Maximum - vs.LargeChange + 1); int v = (int)Math.Round(Math.Max(0, Math.Min(1, (thumbY - Theme.D(4)) / Math.Max(1, track - th))) * range); sc.AutoScrollPosition = new Point(-sc.AutoScrollPosition.X, v); lastVal = -1; Invalidate(); } protected override void OnMouseEnter(EventArgs e) { hv.To(1); base.OnMouseEnter(e); } protected override void OnMouseLeave(EventArgs e) { if (dragOff < 0) hv.To(0); base.OnMouseLeave(e); } protected override void OnMouseDown(MouseEventArgs e) { var t = Thumb(); dragOff = t.Contains(t.X, e.Y) ? (int)(e.Y - t.Y) : (int)(t.Height / 2); if (!t.Contains(t.X, e.Y)) ScrollTo(e.Y - dragOff); Capture = true; base.OnMouseDown(e); } protected override void OnMouseMove(MouseEventArgs e) { if (dragOff >= 0) ScrollTo(e.Y - dragOff); base.OnMouseMove(e); } protected override void OnMouseUp(MouseEventArgs e) { dragOff = -1; Capture = false; if (!ClientRectangle.Contains(e.Location)) hv.To(0); base.OnMouseUp(e); } protected override void OnMouseWheel(MouseEventArgs e) { var p = sc.AutoScrollPosition; sc.AutoScrollPosition = new Point(-p.X, Math.Max(0, -p.Y - e.Delta / 2)); lastVal = -1; Invalidate(); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(sc.BackColor); var t = Thumb(); using (var p = Theme.Round(t, t.Width / 2f)) { if (hv.V > 0.5f || dragOff >= 0) using (var b = new LinearGradientBrush(new RectangleF(t.X, t.Y - 1, t.Width, t.Height + 2), Theme.Accent, Theme.Accent2, 90f)) g.FillPath(b, p); else using (var b = new SolidBrush(Fx.Mix(sc.BackColor, Theme.Light ? Color.Black : Color.White, 0.18f + 0.1f * hv.V))) g.FillPath(b, p); } } } // Otvoreni popis padajuceg izbornika class PopupList : Control { readonly DropDown owner; public readonly int RowH = Theme.D(Theme.ClassicLayout ? 36 : 38); public readonly int Pad = Theme.D(Theme.ClassicLayout ? 4 : 6); int top, hover = -1; public PopupList(DropDown owner) { this.owner = owner; SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.Selectable, true); Font = owner.Font; Cursor = Cursors.Hand; hover = owner.SelectedIndex; top = Math.Max(0, Math.Min(owner.SelectedIndex - owner.MaxVisible / 2, owner.Items.Count - owner.MaxVisible)); } int Visible2 { get { return Math.Max(1, (Height - Pad * 2) / RowH); } } int MaxTop { get { return Math.Max(0, owner.Items.Count - Visible2); } } int Hit(Point p) { int i = top + (p.Y - Pad) / RowH; return p.Y >= Pad && i < owner.Items.Count ? i : -1; } void Ensure(int i) { if (i < top) top = i; else if (i >= top + Visible2) top = i - Visible2 + 1; top = Math.Max(0, Math.Min(top, MaxTop)); } protected override void OnMouseMove(MouseEventArgs e) { int h = Hit(e.Location); if (h != hover && h >= 0) { hover = h; Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseClick(MouseEventArgs e) { int h = Hit(e.Location); if (h >= 0) owner.Pick(h); base.OnMouseClick(e); } protected override void OnMouseWheel(MouseEventArgs e) { top = Math.Max(0, Math.Min(MaxTop, top - Math.Sign(e.Delta) * 2)); Invalidate(); } protected override bool IsInputKey(Keys k) { return k == Keys.Up || k == Keys.Down || k == Keys.Enter || base.IsInputKey(k); } protected override void OnKeyDown(KeyEventArgs e) { if (e.KeyCode == Keys.Down && hover < owner.Items.Count - 1) { hover++; Ensure(hover); Invalidate(); } else if (e.KeyCode == Keys.Up && hover > 0) { hover--; Ensure(hover); Invalidate(); } else if ((e.KeyCode == Keys.Enter || e.KeyCode == Keys.Space) && hover >= 0) owner.Pick(hover); base.OnKeyDown(e); } protected override void OnPaint(PaintEventArgs e) { if (Theme.ClassicLayout) { PaintClassic(e); return; } // plutajuca kartica: zaobljene stavke, odabrana ima traku u prijelazu boje i nacrtanu kvacicu var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; var bg = Theme.Menu; g.Clear(bg); using (var p = Theme.Round(new RectangleF(0.5f, 0.5f, Width - 1.5f, Height - 1.5f), Theme.D(12))) using (var pen = new Pen(Fx.Mix(Theme.Line, Theme.Light ? Color.Black : Color.White, 0.08f))) g.DrawPath(pen, p); int n = owner.Items.Count; bool scroll = n > Visible2; int w = Width - Pad * 2 - (scroll ? Theme.D(8) : 0); for (int i = top; i < Math.Min(n, top + Visible2); i++) { var r = new Rectangle(Pad, Pad + (i - top) * RowH, w, RowH); bool sel = i == owner.SelectedIndex, hot = i == hover; var ir = new Rectangle(r.X, r.Y + 1, r.Width, r.Height - 2); if (sel || hot) using (var p = Theme.Round(ir, Theme.D(8))) using (var b = new SolidBrush(sel ? Fx.Mix(bg, Theme.Accent, hot ? 0.24f : 0.17f) : Fx.Mix(bg, Theme.Light ? Color.Black : Color.White, 0.07f))) g.FillPath(b, p); if (sel) { var bar = new RectangleF(ir.X + Theme.D(5), ir.Y + ir.Height * 0.28f, Theme.D(3), ir.Height * 0.44f); using (var p = Theme.Round(bar, Theme.D(1.5f))) using (var b = new LinearGradientBrush(new RectangleF(bar.X, bar.Y - 1, bar.Width, bar.Height + 2), Theme.Accent, Theme.Accent2, 90f)) g.FillPath(b, p); } int right = r.Right - Theme.D(10); if (sel) { float cx = right - Theme.D(8), cy = r.Y + r.Height / 2f, s = Theme.D(5); using (var pen = new Pen(Theme.Accent2, Theme.D(2)) { StartCap = LineCap.Round, EndCap = LineCap.Round, LineJoin = LineJoin.Round }) g.DrawLines(pen, new[] { new PointF(cx - s, cy), new PointF(cx - s * 0.3f, cy + s * 0.7f), new PointF(cx + s, cy - s * 0.7f) }); right -= Theme.D(24); } TextRenderer.DrawText(g, Convert.ToString(owner.Items[i]), Font, new Rectangle(r.X + Theme.D(16), r.Y, Math.Max(10, right - r.X - Theme.D(16)), r.Height), sel ? Theme.Text : hot ? Theme.Text : Theme.Text2, TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding); } if (scroll) { float trackH = Height - Pad * 2 - Theme.D(4), thumb = Math.Max(Theme.D(24), trackH * Visible2 / n), ty = Pad + Theme.D(2) + (trackH - thumb) * top / Math.Max(1, MaxTop); using (var p = Theme.Round(new RectangleF(Width - Theme.D(10), ty, Theme.D(4), thumb), Theme.D(2))) using (var b = new SolidBrush(Fx.Mix(bg, Theme.Light ? Color.Black : Color.White, 0.2f))) g.FillPath(b, p); } } // stari izgled (Xfire raspored) void PaintClassic(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(Theme.Panel); using (var pen = new Pen(Theme.Rail)) g.DrawRectangle(pen, 0, 0, Width - 1, Height - 1); int n = owner.Items.Count; bool scroll = n > Visible2; int w = Width - Theme.D(8) - (scroll ? Theme.D(8) : 0); for (int i = top; i < Math.Min(n, top + Visible2); i++) { var r = new Rectangle(Theme.D(4), Theme.D(4) + (i - top) * RowH, w, RowH); bool sel = i == owner.SelectedIndex, hot = i == hover; if (sel || hot) using (var p = Theme.Round(new Rectangle(r.X, r.Y + 1, r.Width, r.Height - 2), Theme.D(4))) using (var b = new SolidBrush(sel ? Theme.Panel3 : Theme.Panel2)) g.FillPath(b, p); int right = r.Right - Theme.D(8); if (sel) { var cr = new Rectangle(right - Theme.D(18), r.Y + (r.Height - Theme.D(18)) / 2, Theme.D(18), Theme.D(18)); using (var b = new SolidBrush(Theme.Accent)) g.FillEllipse(b, cr); Theme.Icon(g, "\uE73E", "✓", cr, Color.White, 10); right = cr.X - Theme.D(6); } TextRenderer.DrawText(g, Convert.ToString(owner.Items[i]), Font, new Rectangle(r.X + Theme.D(10), r.Y, Math.Max(10, right - r.X - Theme.D(10)), r.Height), sel || hot ? Theme.Text : Theme.Text2, TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding); } if (scroll) { float trackH = Height - Theme.D(12), thumb = Math.Max(Theme.D(24), trackH * Visible2 / n), ty = Theme.D(6) + (trackH - thumb) * top / Math.Max(1, MaxTop); using (var p = Theme.Round(new RectangleF(Width - Theme.D(9), ty, Theme.D(5), thumb), Theme.D(2.5f))) using (var b = new SolidBrush(Theme.Rail)) g.FillPath(b, p); } } } // Popis bez treperenja: sam crta svoje stavke u medjuspremnik umjesto da Windows prvo obrise pozadinu class SmoothList : ListBox { public SmoothList() { SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.ResizeRedraw, true); } protected override void OnPaint(PaintEventArgs e) { using (var b = new SolidBrush(BackColor)) e.Graphics.FillRectangle(b, e.ClipRectangle); if (Items.Count == 0) return; for (int i = Math.Max(0, TopIndex); i < Items.Count; i++) { var r = GetItemRectangle(i); if (r.Top >= ClientSize.Height) break; if (!r.IntersectsWith(e.ClipRectangle)) continue; OnDrawItem(new DrawItemEventArgs(e.Graphics, Font, r, i, DrawItemState.Default, ForeColor, BackColor)); } } } } // ===================== Views.cs ===================== using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; using System.Net; using System.Threading.Tasks; using System.Windows.Forms; namespace Fragline { class LoginForm : DarkForm { public string Token, Username, UserColor; readonly InputBox user = new InputBox(false); readonly InputBox pass = new InputBox(true); readonly FlatButton ok = new FlatButton("Prijavi se", true); readonly Label err; public LoginForm() { Text = "Fragline - prijava"; MinimizeBox = false; TopMost = true; int W = Theme.D(440), pad = Theme.D(32), inner = W - pad * 2; ClientSize = new Size(W, Theme.D(470)); var logo = new LogoMark { Size = new Size(Theme.D(150), Theme.D(34)), TextPx = 24 }; logo.Location = new Point((W - logo.Width) / 2, Theme.D(26)); Controls.Add(logo); var title = new Label { Text = "Dobrodošao natrag!", Font = Theme.Body(24, FontStyle.Bold), ForeColor = Theme.Text, BackColor = Color.Transparent, TextAlign = ContentAlignment.MiddleCenter }; title.SetBounds(pad, Theme.D(72), inner, Theme.D(36)); var sub = new Label { Text = "Drago nam je što te opet vidimo!", Font = Theme.Body(15), ForeColor = Theme.Muted, BackColor = Color.Transparent, TextAlign = ContentAlignment.MiddleCenter }; sub.SetBounds(pad, Theme.D(108), inner, Theme.D(24)); Controls.AddRange(new Control[] { title, sub }); Func cap = (t, y) => { var l = L(t, Theme.Body(12, FontStyle.Bold), Theme.Text2, pad, y); var star = L("*", Theme.Body(12, FontStyle.Bold), Theme.Red, pad + l.PreferredWidth, y); return l; }; cap("KORISNIČKO IME", Theme.D(152)); user.SetBounds(pad, Theme.D(174), inner, Theme.D(42)); cap("LOZINKA", Theme.D(232)); pass.SetBounds(pad, Theme.D(254), inner, Theme.D(42)); err = L("", Theme.Body(13), Theme.Red, pad, Theme.D(302), inner); ok.SetBounds(pad, Theme.D(346), inner, Theme.D(44)); ok.Font = Theme.Body(15, FontStyle.Bold); var reg = new LinkLabel { Text = "Trebaš račun? Registriraj se", Font = Theme.Body(13.5f), LinkArea = new LinkArea(14, 14), ForeColor = Theme.Muted, LinkColor = Theme.Link, ActiveLinkColor = Theme.Link, VisitedLinkColor = Theme.Link, LinkBehavior = LinkBehavior.HoverUnderline, BackColor = Color.Transparent, AutoSize = true }; reg.LinkClicked += (s, e) => { try { Process.Start(Api.Base + "/login"); } catch { } }; Controls.AddRange(new Control[] { user, pass, ok, reg }); reg.Location = new Point(pad, Theme.D(402)); ok.Click += async (s, e) => { ok.Enabled = false; ok.Text = "Prijavljujem..."; err.Text = ""; var u = user.Value.Trim(); var p = pass.Value; try { var res = await Task.Run(() => Api.Login(u, p)); Token = Api.Field(res, "token"); Username = Api.Field(res, "username"); UserColor = Api.Field(res, "color"); if (Token == null) throw new Exception("Neočekivan odgovor servera."); DialogResult = DialogResult.OK; Close(); } catch (WebException ex) { err.Text = Api.ErrorMessage(ex); } catch (Exception ex) { err.Text = ex.Message; } finally { ok.Enabled = true; ok.Text = "Prijavi se"; } }; AcceptButton = ok; Shown += (s, e) => user.Box.Focus(); } } } // ===================== Views2.cs ===================== using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Drawing.Drawing2D; using System.Globalization; using System.Linq; using System.Threading.Tasks; using System.Windows.Forms; namespace Fragline { class ClanItem { public long Id; public string Name, Tag, Slug, Color; public bool Owner; public override string ToString() { return Name; } } class ActItem { public string Type, User, Color, Avatar, Game, Icon, At, Url; public long Seconds = -1; public bool Mine; } class VRow { public VRoom Room; public VMember Member; public bool Here; public string Header; } static class Ui { public static string Ago(string iso) { DateTimeOffset d; if (string.IsNullOrEmpty(iso) || !DateTimeOffset.TryParse(iso, CultureInfo.InvariantCulture, DateTimeStyles.None, out d)) return ""; int m = (int)(DateTimeOffset.UtcNow - d).TotalMinutes; if (m < 1) return "upravo"; if (m < 60) return "prije " + m + " min"; int h = m / 60; if (h < 24) return "prije " + h + " h"; int dd = h / 24; return dd == 1 ? "jučer" : "prije " + dd + " dana"; } public static string Dur(long s) { long m = Math.Max(1, (s + 30) / 60); return m < 60 ? m + " min" : (m / 60) + " h" + (m % 60 > 0 ? " " + (m % 60) + " min" : ""); } public static ContextMenuStrip Menu() { return new ContextMenuStrip { Renderer = new DarkRenderer(), Font = Theme.Body(13.5f), ShowImageMargin = true }; } public static void Combo(DropDown c) { c.Font = Theme.Body(13.5f); } public static Label Lbl(string t, float px, Color c, int x, int y, bool bold = false) { return new Label { Text = t, Font = Theme.Body(px, bold ? FontStyle.Bold : FontStyle.Regular), ForeColor = c, AutoSize = true, Location = new Point(x, y), BackColor = Color.Transparent }; } public static Label Section(string t, int x, int y) { return Lbl(t.ToUpperInvariant(), 12, Theme.Muted, x, y, true); } public static string KeyName(int vk) { foreach (var k in VoiceView.PttKeys) if (k.Vk == vk) return k.Name; return "tipku"; } } class VoiceView : UserControl { public static readonly (string Name, int Vk)[] PttKeys = { ("Mouse 4", 0x05), ("Mouse 5", 0x06), ("Caps Lock", 0x14), ("Lijevi Alt", 0xA4), ("V", 0x56), ("B", 0x42), ("T", 0x54), ("Q", 0x51) }; readonly TrayApp app; readonly SideHead head = new SideHead("Voice sobe", Theme.IcoAdd, "Nova soba"); readonly Label status = new Label(); readonly ListBox list = new SmoothList(); readonly Timer tick = new Timer { Interval = 120 }; List lastRooms = new List(); List rows = new List(); string lastSig = "", lastLook = "", info; bool loaded; int hover = -1; VoiceClient vc; public List Rooms { get { return lastRooms; } } public string StatusText = ""; public Color StatusColor = Theme.Muted; public bool Loaded { get { return loaded; } } public VoiceView(TrayApp app) { this.app = app; BackColor = Theme.Panel; head.Dock = DockStyle.Top; head.Height = Theme.D(48); head.ActionClicked += () => app.ShowCreateRoom(); status.Dock = DockStyle.Top; status.Height = Theme.D(30); status.Font = Theme.Body(12.5f); status.ForeColor = Theme.Muted; status.BackColor = Theme.Panel; status.Padding = new Padding(Theme.D(12), 0, Theme.D(8), 0); status.TextAlign = ContentAlignment.MiddleLeft; status.AutoEllipsis = true; list.DrawMode = DrawMode.OwnerDrawVariable; list.BorderStyle = BorderStyle.None; list.BackColor = Theme.Panel; list.Dock = DockStyle.Fill; list.IntegralHeight = false; list.SelectionMode = SelectionMode.None; list.MeasureItem += (s, e) => { var r = e.Index >= 0 && e.Index < rows.Count ? rows[e.Index] : null; e.ItemHeight = r == null ? Theme.D(30) : r.Header != null ? Theme.D(36) : r.Room != null ? Theme.D(36) : Theme.D(30); }; list.DrawItem += DrawRow; list.MouseMove += (s, e) => { int i = list.IndexFromPoint(e.Location); if (i != hover) { hover = i; list.Invalidate(); } }; list.MouseLeave += (s, e) => { hover = -1; list.Invalidate(); }; list.MouseUp += (s, e) => { int i = list.IndexFromPoint(e.Location); if (i < 0 || i >= rows.Count) return; var r = rows[i]; if (e.Button == MouseButtons.Left && r.Room != null) { if (r.Here) app.ShowVoicePage(); else app.JoinVoice(r.Room); } else if (e.Button == MouseButtons.Right && r.Room != null) RoomMenu(r, e.Location); else if (e.Button == MouseButtons.Right && r.Member != null) ShowMemberMenu(app, r.Member, list, e.Location); }; Theme.DarkScroll(list); Controls.Add(list); Controls.Add(status); Controls.Add(head); list.BringToFront(); tick.Tick += (s, e) => Tick(); tick.Start(); UpdateStatusLine(); } void Tick() { if (!Visible) return; if (app.Voice != vc) Attach(app.Voice); if (vc != null) { Rebuild(); UpdateStatusLine(); } } public void Attach(VoiceClient v) { vc = v; if (v != null) info = null; Rebuild(); UpdateStatusLine(); } public void SetStatus(string msg) { info = msg; UpdateStatusLine(); } public void SetRooms(List rooms) { loaded = true; lastRooms = rooms ?? new List(); Rebuild(); UpdateStatusLine(); } void UpdateStatusLine() { string t; Color c = Theme.Muted; if (vc != null) t = "Desni klik na osobu za glasnoću"; else if (info != null) { t = info; c = Theme.Link; } else if (!loaded) t = "Učitavam sobe..."; else if (!app.VoiceServer) { t = "Voice server trenutno ne radi."; c = Theme.Red; } else if (!app.VoiceEnabled) { t = "Voice je trenutno isključen."; c = Theme.Red; } else t = "Klikni sobu da uđeš."; if (status.Text != t) status.Text = t; status.ForeColor = c; StatusText = t; StatusColor = c; } static string RoomInfo(VRoom rm) { int n = rm.Members.Count; string pv = !rm.Custom ? null : rm.Privacy == "friends" ? "prijatelji" : rm.Privacy == "clan" ? "klan" : "javna"; string cnt = rm.Limit > 0 ? n + "/" + rm.Limit : null; return pv != null && cnt != null ? pv + " · " + cnt : (pv ?? cnt ?? ""); } void Rebuild() { var rooms = new List(lastRooms); if (vc != null && !rooms.Any(x => x.Key == vc.Room)) rooms.Insert(0, new VRoom { Key = vc.Room, Label = vc.Label }); var known = new Dictionary(); foreach (var rm in lastRooms) foreach (var m in rm.Members) if (m.Name != null) known[m.Name] = m; var nr = new List(); Action addRoom = rm => { bool here = vc != null && vc.Room == rm.Key; nr.Add(new VRow { Room = rm, Here = here }); if (here) { nr.Add(new VRow { Member = new VMember { Name = (app.Username ?? "Ti") + " (ti)", Color = app.ColorHex, Avatar = app.AvatarUrl, Speaking = vc.Talking } }); foreach (var m in vc.LiveMembers()) { VMember k; if (known.TryGetValue(m.Name ?? "", out k)) { m.Color = k.Color; m.Avatar = k.Avatar; } nr.Add(new VRow { Member = m }); } } else foreach (var m in rm.Members) nr.Add(new VRow { Member = m }); }; nr.Add(new VRow { Header = "GLAVNE SOBE" }); foreach (var rm in rooms.Where(x => !x.Custom)) addRoom(rm); var custom = rooms.Where(x => x.Custom).ToList(); if (custom.Count > 0) { nr.Add(new VRow { Header = "SOBE ZAJEDNICE" }); foreach (var rm in custom) addRoom(rm); } var sig = string.Join("|", nr.Select(x => x.Header != null ? "H" + x.Header : x.Room != null ? "R" + x.Room.Key + (x.Here ? "1" : "0") + x.Room.Members.Count : "M" + x.Member.Name).ToArray()); // tko govori i boje/slike mijenjaju samo izgled, ne i redove var look = string.Join("|", nr.Select(x => x.Member == null ? "" : (x.Member.Speaking ? "1" : "0") + x.Member.Color + x.Member.Avatar + (AudioCfg.UserMuted(x.Member.Name ?? "") ? "m" : "")).ToArray()); rows = nr; if (sig == lastSig && look == lastLook) return; lastLook = look; if (sig != lastSig) { lastSig = sig; int topI = list.Items.Count > 0 ? list.TopIndex : 0; list.BeginUpdate(); list.Items.Clear(); foreach (var x in nr) list.Items.Add(x); list.EndUpdate(); if (topI < list.Items.Count) list.TopIndex = topI; } else list.Invalidate(); } void DrawRow(object s, DrawItemEventArgs e) { if (e.Index < 0 || e.Index >= rows.Count) return; var r = rows[e.Index]; var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; var b = e.Bounds; using (var fill = new SolidBrush(Theme.Panel)) g.FillRectangle(fill, b); if (r.Header != null) { using (var f = Theme.Body(12, FontStyle.Bold)) TextRenderer.DrawText(g, r.Header, f, new Rectangle(b.X + Theme.D(12), b.Y + Theme.D(12), b.Width - Theme.D(24), b.Height - Theme.D(12)), Theme.Muted, TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding); return; } bool hot = e.Index == hover; var inner = new Rectangle(b.X + Theme.D(8), b.Y + Theme.D(1), b.Width - Theme.D(16), b.Height - Theme.D(2)); if (r.Room != null) { if (r.Here || hot) using (var p = Theme.Round(inner, Theme.D(4))) using (var fill = new SolidBrush(r.Here ? Theme.Panel3 : Theme.Panel2)) g.FillPath(fill, p); var fc = r.Here || hot ? Theme.Text : Theme.Muted; Theme.Icon(g, Theme.IcoSpeaker, "»", new Rectangle(inner.X + Theme.D(6), inner.Y, Theme.D(22), inner.Height), fc, 16); string info = RoomInfo(r.Room); int right = inner.Right - Theme.D(8); using (var fs = Theme.Body(11.5f)) { if (info.Length > 0) { var isz = TextRenderer.MeasureText(info, fs); TextRenderer.DrawText(g, info, fs, new Rectangle(right - isz.Width, inner.Y, isz.Width + 2, inner.Height), Theme.Muted, TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding); right -= isz.Width + Theme.D(6); } } int x = inner.X + Theme.D(34); using (var f = Theme.Body(14.5f, FontStyle.Bold)) TextRenderer.DrawText(g, r.Room.Label ?? r.Room.Key, f, new Rectangle(x, inner.Y, Math.Max(10, right - x), inner.Height), fc, TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPadding); } else { var m = r.Member; if (hot) using (var p = Theme.Round(inner, Theme.D(4))) using (var fill = new SolidBrush(Theme.Panel2)) g.FillPath(fill, p); var bg = hot ? Theme.Panel2 : Theme.Panel; int x = inner.X + Theme.D(30); float sz = Theme.D(22); var ar = new RectangleF(x, b.Y + (b.Height - sz) / 2f, sz, sz); Theme.Avatar(g, ar, m.Color, m.Name, m.Avatar, list, null, bg); if (m.Speaking) { var rr = ar; rr.Inflate(Theme.D(1.5f), Theme.D(1.5f)); using (var pen = new Pen(Theme.On, Theme.D(2))) g.DrawEllipse(pen, rr); } bool localMuted = m.Name != null && !m.Name.EndsWith(" (ti)") && AudioCfg.UserMuted(m.Name); int right = inner.Right - Theme.D(6); if (localMuted) { var ir = new Rectangle(right - Theme.D(18), b.Y + (b.Height - Theme.D(18)) / 2, Theme.D(18), Theme.D(18)); Theme.Icon(g, Theme.IcoSpeaker, "x", ir, Theme.Muted, 13); Theme.Slash(g, ir, bg); right -= Theme.D(22); } int tx = x + (int)sz + Theme.D(8); using (var f = Theme.Body(13.5f)) TextRenderer.DrawText(g, m.Name ?? "?", f, new Rectangle(tx, b.Y, Math.Max(10, right - tx), b.Height), m.Speaking || hot ? Theme.Text : Theme.Muted, TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPadding); } } void RoomMenu(VRow r, Point at) { var menu = Ui.Menu(); if (r.Room.Custom && r.Room.Owner != null) menu.Items.Add(new ToolStripMenuItem("Soba korisnika " + r.Room.Owner) { Enabled = false }); if (!r.Here) menu.Items.Add(new ToolStripMenuItem("Uđi u sobu", null, (s, e) => app.JoinVoice(r.Room))); else menu.Items.Add(new ToolStripMenuItem("Izađi iz sobe", null, (s, e) => app.LeaveVoice())); if (r.Room.Custom && r.Room.Mine) { menu.Items.Add(new ToolStripSeparator()); menu.Items.Add(new ToolStripMenuItem("Obriši sobu", null, (s, e) => { if (Dialog.Confirm(list, "Obriši sobu", "Obrisati sobu \"" + r.Room.Label + "\"? Svi koji su u njoj bit će izbačeni.", "Obriši sobu", true)) app.DeleteRoom(r.Room); })); } menu.Show(list, at); } public static void ShowMemberMenu(TrayApp app, VMember m, Control host, Point at) { string name = m.Name; if (name == null || name.EndsWith(" (ti)")) return; var menu = Ui.Menu(); menu.Items.Add(new ToolStripMenuItem(name) { Enabled = false }); menu.Items.Add(new ToolStripSeparator()); float cur = AudioCfg.UserVol(name); foreach (var pct in new[] { 25, 50, 75, 100, 150, 200 }) { int p = pct; menu.Items.Add(new ToolStripMenuItem("Glasnoća " + p + "%", null, (s, e) => { AudioCfg.SetUserVol(name, p / 100f); app.UserVolumeChanged(); }) { Checked = Math.Abs(cur - p / 100f) < 0.01f }); } menu.Items.Add(new ToolStripSeparator()); bool um = AudioCfg.UserMuted(name); menu.Items.Add(new ToolStripMenuItem(um ? "Ukloni utišavanje" : "Utišaj samo za mene", null, (s, e) => { AudioCfg.SetUserMuted(name, !um); app.UserVolumeChanged(); host.Invalidate(); })); if (app.IsFriend(name)) menu.Items.Add(new ToolStripMenuItem("Pošalji poruku", null, (s, e) => app.OpenChatByName(name))); menu.Items.Add(new ToolStripMenuItem("Otvori profil", null, (s, e) => app.Open("/profile/" + Uri.EscapeDataString(name)))); menu.Show(host, at); } } class ActivityView : UserControl { readonly TrayApp app; readonly ListBox list = new SmoothList(); readonly Label empty = new Label(); readonly Timer tick = new Timer { Interval = 1000 }; List data = new List(); int hover = -1; bool busy; DateTime last = DateTime.MinValue; public ActivityView(TrayApp app) { this.app = app; BackColor = Theme.Bg; empty.Dock = DockStyle.Fill; empty.Font = Theme.Body(14); empty.ForeColor = Theme.Muted; empty.BackColor = Theme.Bg; empty.TextAlign = ContentAlignment.MiddleCenter; empty.Text = "Učitavam aktivnost..."; list.Dock = DockStyle.Fill; list.DrawMode = DrawMode.OwnerDrawFixed; list.ItemHeight = Theme.D(56); list.BorderStyle = BorderStyle.None; list.BackColor = Theme.Bg; list.IntegralHeight = false; list.SelectionMode = SelectionMode.None; list.Visible = false; list.Cursor = Cursors.Hand; list.DrawItem += DrawItem; list.MouseMove += (s, e) => { int i = list.IndexFromPoint(e.Location); if (i != hover) { hover = i; list.Invalidate(); } }; list.MouseLeave += (s, e) => { hover = -1; list.Invalidate(); }; list.MouseClick += (s, e) => { int i = list.IndexFromPoint(e.Location); if (i < 0 || i >= data.Count) return; var it = data[i]; if (it.Url != null) { try { Process.Start(it.Url); } catch { } } else app.Open("/profile/" + Uri.EscapeDataString(it.User ?? "")); }; Theme.DarkScroll(list); Controls.Add(empty); Controls.Add(list); list.BringToFront(); tick.Tick += (s, e) => { if (Visible && (DateTime.UtcNow - last).TotalSeconds > 30) Fetch(); }; tick.Start(); } protected override void OnVisibleChanged(EventArgs e) { if (Visible) last = DateTime.MinValue; base.OnVisibleChanged(e); } async void Fetch() { if (busy) return; busy = true; last = DateTime.UtcNow; try { var res = await Task.Run(() => app.ApiGet("/client/activity")); var items = new List(); foreach (var x in J.A(J.Parse(res), "items")) { var sec = J.S(x, "seconds"); items.Add(new ActItem { Type = J.S(x, "type"), User = J.S(x, "user"), Color = J.S(x, "color"), Avatar = J.S(x, "avatar"), Game = J.S(x, "game"), Icon = J.S(x, "icon"), At = J.S(x, "at"), Url = J.S(x, "url"), Mine = J.B(x, "mine"), Seconds = sec == null ? -1 : J.L(x, "seconds") }); } data = items; list.BeginUpdate(); list.Items.Clear(); foreach (var it in items) list.Items.Add(it); list.EndUpdate(); list.Visible = items.Count > 0; empty.Visible = items.Count == 0; empty.Text = "Još nema aktivnosti.\nKad ti ili prijatelji zaigrate, pojavit će se ovdje."; } catch { empty.Text = "Aktivnost se trenutno ne može učitati."; } finally { busy = false; } } static string Sentence(ActItem it) { string who = it.Mine ? "Ti" : it.User; if (it.Type == "shot") return who + (it.Mine ? " si dodao screenshot" : " je dodao screenshot") + (it.Game != null ? " iz " + it.Game : ""); if (it.Seconds < 0) return who + (it.Mine ? " igraš " : " igra ") + it.Game; return who + (it.Mine ? " si igrao " : " je igrao ") + it.Game + ", " + Ui.Dur(it.Seconds); } void DrawItem(object s, DrawItemEventArgs e) { if (e.Index < 0 || e.Index >= data.Count) return; var it = data[e.Index]; var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; var r = e.Bounds; using (var b = new SolidBrush(Theme.Bg)) g.FillRectangle(b, r); var bg = e.Index == hover ? Theme.MsgHover : Theme.Bg; var inner = new Rectangle(r.X + Theme.D(16), r.Y + Theme.D(2), r.Width - Theme.D(32), r.Height - Theme.D(4)); if (e.Index == hover) using (var p = Theme.Round(inner, Theme.D(8))) using (var b = new SolidBrush(bg)) g.FillPath(b, p); Theme.Avatar(g, new RectangleF(inner.X + Theme.D(10), r.Y + Theme.D(10), Theme.D(36), Theme.D(36)), it.Color, it.User, it.Avatar, list, null, bg); int x = inner.X + Theme.D(58), right = inner.Right - Theme.D(12); var img = ImageCache.Get(it.Icon, list); if (img != null) { var ir = new Rectangle(right - Theme.D(26), r.Y + (r.Height - Theme.D(26)) / 2, Theme.D(26), Theme.D(26)); g.DrawImage(Theme.Masked(img, ir.Width, ir.Height, Theme.D(6)), ir.X, ir.Y, ir.Width, ir.Height); right -= Theme.D(34); } int tw = Math.Max(10, right - x - Theme.D(4)); using (var f = Theme.Body(13.5f, FontStyle.Bold)) TextRenderer.DrawText(g, Sentence(it), f, new Rectangle(x, r.Y + Theme.D(9), tw, Theme.D(20)), it.Type == "shot" ? Theme.Text : (it.Seconds < 0 ? Theme.On : Theme.Text), bg, TextFormatFlags.EndEllipsis | TextFormatFlags.NoPadding); using (var f = Theme.Body(12)) TextRenderer.DrawText(g, Ui.Ago(it.At) + (it.Url != null ? " · klikni za screenshot" : ""), f, new Rectangle(x, r.Y + Theme.D(30), tw, Theme.D(16)), Theme.Muted, bg, TextFormatFlags.EndEllipsis | TextFormatFlags.NoPadding); } } class SRow { public Label Title, Desc; public Control Right, Body; public Func BodyHeight; public bool Visible = true; public int Top, Height; } // Kartica u postavkama: zaobljena ploha s redovima (naslov, opis i kontrola desno ili ispod) class SCard : Panel { public readonly List Rows = new List(); public string Category; public SCard() { DoubleBuffered = true; BackColor = Theme.Panel; } public SRow Add(string title, string desc, Control right) { var row = new SRow { Title = new Label { Text = title, Font = Theme.Display(15), ForeColor = Theme.Text, BackColor = Theme.Panel, AutoEllipsis = true }, Desc = desc == null ? null : new Label { Text = desc, Font = Theme.Body(13), ForeColor = Theme.Muted, BackColor = Theme.Panel, AutoEllipsis = true }, Right = right, }; Controls.Add(row.Title); if (row.Desc != null) Controls.Add(row.Desc); if (right != null) Controls.Add(right); Rows.Add(row); return row; } // red s naslovom gore i sadrzajem preko cijele sirine ispod (npr. slicice tema) public SRow AddBlock(string title, string desc, Control body, Func height) { var row = Add(title, desc, null); row.Body = body; row.BodyHeight = height; Controls.Add(body); return row; } // vraca visinu kartice public int LayoutRows(int width) { int y = 0, pad = Theme.D(16); foreach (var r in Rows) { r.Title.Visible = r.Visible; if (r.Desc != null) r.Desc.Visible = r.Visible; if (r.Right != null) r.Right.Visible = r.Visible; if (r.Body != null) r.Body.Visible = r.Visible; if (!r.Visible) continue; int rw = r.Right != null ? r.Right.Width : 0; int tw = Math.Max(Theme.D(60), width - pad * 2 - rw - (rw > 0 ? Theme.D(16) : 0)); int h; if (r.Body != null) { r.Title.SetBounds(pad, y + Theme.D(14), tw, Theme.D(22)); int by = y + Theme.D(42); if (r.Desc != null) { r.Desc.SetBounds(pad, y + Theme.D(36), tw, Theme.D(20)); by = y + Theme.D(64); } int bw = width - pad * 2, bh = r.BodyHeight(bw); r.Body.SetBounds(pad, by, bw, bh); h = by - y + bh + Theme.D(16); } else { h = r.Desc != null ? Theme.D(66) : Theme.D(52); if (r.Desc != null) { r.Title.SetBounds(pad, y + Theme.D(12), tw, Theme.D(22)); r.Desc.SetBounds(pad, y + Theme.D(34), tw, Theme.D(20)); } else r.Title.SetBounds(pad, y + (h - Theme.D(22)) / 2, tw, Theme.D(22)); if (r.Right != null) r.Right.Location = new Point(width - pad - rw, y + (h - r.Right.Height) / 2); } r.Top = y; r.Height = h; y += h; } return y; } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(Parent != null ? Parent.BackColor : Theme.Bg); using (var p = Theme.Round(new RectangleF(0, 0, Width - 1, Height - 1), Theme.D(8))) using (var b = new SolidBrush(Theme.Panel)) g.FillPath(b, p); bool first = true; using (var pen = new Pen(Theme.Line)) foreach (var r in Rows) { if (!r.Visible) continue; if (!first) g.DrawLine(pen, Theme.D(16), r.Top, Width - Theme.D(16), r.Top); first = false; } } } // Kartica racuna: traka u boji naglaska, veliki avatar, ime i status class AccountCard : Control { readonly TrayApp app; public readonly FlatButton Change = new FlatButton("Promijeni sliku", true), Remove = new FlatButton("Ukloni", false); public string Category = "account"; public AccountCard(TrayApp app) { this.app = app; SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); BackColor = Theme.Panel; Change.Font = Theme.Display(14); Remove.Font = Theme.Display(14); Change.Click += (s, e) => app.ChangeAvatar(); Remove.Click += (s, e) => app.RemoveAvatar(); Controls.AddRange(new Control[] { Change, Remove }); } protected override void OnResize(EventArgs e) { base.OnResize(e); int bh = Theme.D(34), y = Theme.D(84); int cw = TextRenderer.MeasureText(Change.Text, Change.Font).Width + Theme.D(32), rw = TextRenderer.MeasureText(Remove.Text, Remove.Font).Width + Theme.D(32); Change.SetBounds(Width - Theme.D(16) - cw, y, cw, bh); Remove.SetBounds(Change.Left - Theme.D(8) - rw, y, rw, bh); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(Parent != null ? Parent.BackColor : Theme.Bg); var card = new RectangleF(0, 0, Width - 1, Height - 1); using (var p = Theme.Round(card, Theme.D(8))) using (var b = new SolidBrush(Theme.Panel)) g.FillPath(b, p); int bh = Theme.D(72); using (var p = new GraphicsPath()) { float d = Theme.D(16); p.AddArc(0, 0, d, d, 180, 90); p.AddArc(Width - 1 - d, 0, d, d, 270, 90); p.AddLine(Width - 1, bh, 0, bh); p.CloseFigure(); var c = Theme.Hex(app.ColorHex, Theme.Accent); using (var b = new LinearGradientBrush(new Rectangle(0, 0, Width, bh), Theme.Shade(Theme.Accent, -0.1f), Theme.Shade(c, -0.35f), 0f)) g.FillPath(b, p); } float av = Theme.D(84), ax = Theme.D(16), ay = Theme.D(30); using (var b = new SolidBrush(Theme.Panel)) g.FillEllipse(b, ax - Theme.D(6), ay - Theme.D(6), av + Theme.D(12), av + Theme.D(12)); var kind = app.Presence == "invisible" ? "off" : app.Presence == "dnd" ? "dnd" : app.Presence == "idle" || app.AutoIdle ? "idle" : "online"; Theme.Avatar(g, new RectangleF(ax, ay, av, av), app.ColorHex, app.Username, app.AvatarUrl, this, kind, Theme.Panel); int x = (int)(ax + av + Theme.D(16)), w = Math.Max(Theme.D(40), Remove.Left - x - Theme.D(8)); using (var f = Theme.Body(21, FontStyle.Bold)) Txt.Draw(g, app.Username ?? "", f, new Rectangle(x, Theme.D(80), w, Theme.D(28)), Theme.Text); using (var f = Theme.Body(13)) Txt.Draw(g, Theme.KindText(app.Presence == "invisible" ? "invisible" : kind), f, new Rectangle(x, Theme.D(108), w, Theme.D(20)), Theme.Muted); } } // Slicice za odabir (raspored ili tema): mali prikaz prozora, ime ispod, kvacica na odabranoj class TilePicker : Control { public string[] Names = new string[0]; public Action Preview; public int TileW = Theme.D(150), TileH = Theme.D(92); int sel, hover = -1; public event Action Picked; readonly List tiles = new List(); public TilePicker() { SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); Cursor = Cursors.Hand; } public int Selected { get { return sel; } set { sel = value; Invalidate(); } } int Cols(int w) { int gap = Theme.D(14); return Math.Max(1, Math.Min(Names.Length, (w + gap) / (TileW + gap))); } public int HeightFor(int w) { int rows = (Names.Length + Cols(w) - 1) / Cols(w); return rows * (TileH + Theme.D(30)) + (rows - 1) * Theme.D(10); } void Relayout() { tiles.Clear(); int cols = Cols(Width), gap = Theme.D(14); int tw = Math.Min(Theme.D(260), (Width - gap * (cols - 1)) / cols); for (int i = 0; i < Names.Length; i++) { int c = i % cols, r = i / cols; tiles.Add(new Rectangle(c * (tw + gap), r * (TileH + Theme.D(40)), tw, TileH)); } } protected override void OnResize(EventArgs e) { Relayout(); base.OnResize(e); } int Hit(Point p) { for (int i = 0; i < tiles.Count; i++) { var t = tiles[i]; t.Height += Theme.D(28); if (t.Contains(p)) return i; } return -1; } protected override void OnMouseMove(MouseEventArgs e) { int h = Hit(e.Location); if (h != hover) { hover = h; Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseLeave(EventArgs e) { hover = -1; Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseClick(MouseEventArgs e) { int h = Hit(e.Location); if (h >= 0 && h != sel) { sel = h; Invalidate(); if (Picked != null) Picked(h); } base.OnMouseClick(e); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(Parent != null ? Parent.BackColor : Theme.Panel); if (tiles.Count != Names.Length) Relayout(); for (int i = 0; i < tiles.Count; i++) { var t = tiles[i]; var st = g.Save(); using (var p = Theme.Round(t, Theme.D(8))) { g.SetClip(p); if (Preview != null) Preview(g, t, i); } g.Restore(st); g.SmoothingMode = SmoothingMode.AntiAlias; bool on = i == sel, hot = i == hover; var bt = t; bt.Inflate(1, 1); using (var p = Theme.Round(bt, Theme.D(9))) using (var pen = new Pen(on ? Theme.Accent : hot ? Theme.Muted : Theme.Line, on ? Theme.D(2.5f) : 1f)) g.DrawPath(pen, p); if (on) { var cr = new Rectangle(t.Right - Theme.D(26), t.Y + Theme.D(6), Theme.D(20), Theme.D(20)); using (var b = new SolidBrush(Theme.Accent)) g.FillEllipse(b, cr); Theme.Check(g, cr, Color.White, 11); } using (var f = Theme.Display(13.5f)) TextRenderer.DrawText(g, Names[i], f, new Rectangle(t.X, t.Bottom + Theme.D(6), t.Width, Theme.D(20)), on ? Theme.Text : Theme.Text2, TextFormatFlags.HorizontalCenter | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix); } } } // Krugovi s bojama naglaska class AccentPicker : Control { public static readonly string[] Hex = { null, "#8b6cf6", "#1db954", "#7c5cff", "#5865f2", "#ff7a2f", "#8e5cf7", "#eb459e" }; public Color AutoColor = Theme.Accent; readonly ToolTip tt = new ToolTip(); int sel, hover = -1; public event Action Picked; public AccentPicker() { SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); Cursor = Cursors.Hand; Height = Theme.D(48); } public int Selected { get { return sel; } set { sel = value; Invalidate(); } } Rectangle Cell(int i) { int s = Theme.D(40); return new Rectangle(i * (s + Theme.D(12)) + Theme.D(4), (Height - s) / 2, s, s); } int Hit(Point p) { for (int i = 0; i < Hex.Length; i++) if (Cell(i).Contains(p)) return i; return -1; } protected override void OnMouseMove(MouseEventArgs e) { int h = Hit(e.Location); if (h != hover) { hover = h; tt.SetToolTip(this, h >= 0 ? Theme.AccentNames[h] : null); Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseLeave(EventArgs e) { hover = -1; Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseClick(MouseEventArgs e) { int h = Hit(e.Location); if (h >= 0 && h != sel) { sel = h; Invalidate(); if (Picked != null) Picked(h); } base.OnMouseClick(e); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(Parent != null ? Parent.BackColor : Theme.Panel); for (int i = 0; i < Hex.Length; i++) { var r = Cell(i); var c = Hex[i] == null ? AutoColor : ColorTranslator.FromHtml(Hex[i]); if (i == sel || i == hover) { var rr = r; rr.Inflate(Theme.D(3), Theme.D(3)); using (var pen = new Pen(i == sel ? Theme.Text : Theme.Muted, Theme.D(2))) g.DrawEllipse(pen, rr); } using (var b = new SolidBrush(c)) g.FillEllipse(b, r); if (Hex[i] == null) using (var f = Theme.Display(12)) TextRenderer.DrawText(g, "Auto", f, r, Color.White, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding); else if (i == sel) Theme.Check(g, r, Color.White, 14); } } } // Izbornik kategorija lijevo (kao Discordove korisnicke postavke) class SettingsNav : Control { public static readonly string[] Keys = { "account", "look", "general", "notif", "keys" }; public static readonly string[] Names = { "Moj račun", "Izgled", "Općenito", "Obavijesti", "Prečaci i zvuk" }; static readonly string[] Glyphs = { Theme.IcoContact, "", Theme.IcoGear, "", "" }; int sel, hover = -1; public event Action Changed; public SettingsNav() { SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); Cursor = Cursors.Hand; } public int Selected { get { return sel; } set { if (sel != value) { sel = value; Invalidate(); if (Changed != null) Changed(sel); } } } Rectangle Item(int i) { return new Rectangle(Theme.D(12), Theme.D(52) + i * Theme.D(40), Width - Theme.D(24), Theme.D(36)); } int Hit(Point p) { for (int i = 0; i < Keys.Length; i++) if (Item(i).Contains(p)) return i; return -1; } protected override void OnMouseMove(MouseEventArgs e) { int h = Hit(e.Location); if (h != hover) { hover = h; Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseLeave(EventArgs e) { hover = -1; Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseClick(MouseEventArgs e) { int h = Hit(e.Location); if (h >= 0) Selected = h; base.OnMouseClick(e); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(Theme.Panel); using (var f = Theme.Body(12, FontStyle.Bold)) Txt.Draw(g, "KORISNIČKE POSTAVKE", f, new Rectangle(Theme.D(22), Theme.D(22), Width - Theme.D(30), Theme.D(22)), Theme.Muted); for (int i = 0; i < Keys.Length; i++) { var r = Item(i); bool on = i == sel, hot = i == hover; if (Theme.ClassicLayout) { if (on || hot) using (var p = Theme.Round(r, Theme.D(4))) using (var b = new SolidBrush(on ? Theme.Panel3 : Theme.Panel2)) g.FillPath(b, p); } else if (on || hot) { // odabrano: blago obojana pilula s trakom u prijelazu boje using (var p = Theme.Round(r, Theme.D(10))) using (var b = new SolidBrush(on ? Fx.Mix(Theme.Panel, Theme.Accent, 0.2f) : Fx.Mix(Theme.Panel, Color.White, 0.05f))) g.FillPath(b, p); if (on) { var bar = new RectangleF(r.X + Theme.D(4), r.Y + r.Height * 0.26f, Theme.D(3), r.Height * 0.48f); using (var p = Theme.Round(bar, Theme.D(1.5f))) using (var b = new LinearGradientBrush(new RectangleF(bar.X, bar.Y - 1, bar.Width, bar.Height + 2), Theme.Accent, Theme.Accent2, 90f)) g.FillPath(b, p); } } var fc = on ? Theme.Text : hot ? Theme.Text2 : Theme.Muted; Theme.Icon(g, Glyphs[i], "", new Rectangle(r.X + Theme.D(10), r.Y, Theme.D(20), r.Height), fc, 15); using (var f = Theme.Display(14.5f)) Txt.Draw(g, Names[i], f, new Rectangle(r.X + Theme.D(40), r.Y, r.Width - Theme.D(44), r.Height), fc); } using (var f = Theme.Body(12)) Txt.Draw(g, "Fragline v" + TrayApp.Current.ToString(2), f, new Rectangle(Theme.D(22), Height - Theme.D(36), Width - Theme.D(30), Theme.D(20)), Theme.Muted); using (var pen = new Pen(Theme.Shadow)) g.DrawLine(pen, Width - 1, 0, Width - 1, Height); } } class SettingsView : UserControl { // boje za sicice tema: traka, bocna traka, sadrzaj, isticanje, tekst static readonly Dictionary Pal = new Dictionary { { "dash", new[] { "#2b3048", "#363c5a", "#2b3048", "#4a5276", "#ffffff", "#8b6cf6" } }, { "nova", new[] { "#0b0d14", "#141826", "#0f121b", "#232940", "#eef0f6", "#ff8a3d" } }, { "dark", new[] { "#000000", "#121212", "#121212", "#2a2a2a", "#ffffff", "#1db954" } }, { "midnight", new[] { "#000000", "#0a0a0c", "#121214", "#26262b", "#f2f3f5", "#5865f2" } }, { "fragline", new[] { "#07090f", "#0f121b", "#141826", "#232940", "#eef0f6", "#ff7a2f" } }, { "xfire", new[] { "#0a0f17", "#111a28", "#0d131c", "#24344d", "#e8eef8", "#ff7a2f" } }, { "aurora", new[] { "#05060a", "#0b0d14", "#10121a", "#242838", "#f2f3f5", "#8e5cf7" } }, { "light", new[] { "#e9ecf3", "#f6f7fb", "#ffffff", "#e3e7f1", "#12151c", "#7c5cff" } }, }; readonly TrayApp app; readonly Panel scroll = new Panel(); readonly SettingsNav nav = new SettingsNav(); readonly Label pageTitle = new Label(); readonly Toggle auto = new Toggle(), ng = new Toggle(), nm = new Toggle(), snd = new Toggle(), idle = new Toggle(), sfx = new Toggle(), hkv = new Toggle(), hks = new Toggle(), share = new Toggle(); readonly TilePicker layoutPick = new TilePicker { TileW = Theme.D(190), TileH = Theme.D(104) }, themePick = new TilePicker(); readonly AccentPicker accentPick = new AccentPicker(); readonly AccountCard account; readonly List blocks = new List(); readonly Dictionary cat = new Dictionary(); readonly SRow applyRow, shotRow; readonly FlatButton shotOpen = new FlatButton("Otvori", false); readonly Timer refresh = new Timer { Interval = 2000 }; string shotUrl; bool laying; static FlatButton Btn(string text, bool primary, Action click) { var b = new FlatButton(text, primary) { Font = Theme.Display(14) }; b.Size = new Size(TextRenderer.MeasureText(text, b.Font).Width + Theme.D(32), Theme.D(34)); b.Click += (s, e) => click(); return b; } static Control Pair(params Control[] items) { var p = new Panel { BackColor = Theme.Panel }; int x = 0, h = items.Max(c => c.Height); foreach (var c in items) { c.Location = new Point(x, (h - c.Height) / 2); x += c.Width + Theme.D(8); p.Controls.Add(c); } p.Size = new Size(Math.Max(1, x - Theme.D(8)), h); return p; } void Block(Control c, string category) { blocks.Add(c); cat[c] = category; scroll.Controls.Add(c); } SCard Card(string section, string category) { Block(new Label { Text = section.ToUpperInvariant(), Font = Theme.Body(12, FontStyle.Bold), ForeColor = Theme.Muted, BackColor = Theme.Bg, AutoSize = true }, category); var c = new SCard { Category = category }; Block(c, category); return c; } static Color C(string h) { return ColorTranslator.FromHtml(h); } // mali prikaz prozora u bojama teme (novi raspored: gornja traka, sadrzaj, prijatelji desno) void DrawThemePreview(Graphics g, Rectangle r, int i) { var p = Pal[Theme.ThemeKeys[i]]; int ai = accentPick.Selected; var accent = ai > 0 ? C(AccentPicker.Hex[ai]) : C(p[5]); var accent2 = ai <= 1 && Theme.ThemeKeys[i] == "dash" ? C("#ef5ea8") : Theme.Shade(accent, 0.3f); g.SmoothingMode = SmoothingMode.AntiAlias; using (var b = new SolidBrush(C(p[0]))) g.FillRectangle(b, r); var nav = new Rectangle(r.X, r.Y, r.Width, Theme.D(16)); using (var b = new SolidBrush(C(p[1]))) g.FillRectangle(b, nav); if (Theme.ThemeKeys[i] == "aurora") using (var b = new LinearGradientBrush(new Rectangle(nav.X, nav.Bottom - Theme.D(2), nav.Width, Theme.D(2)), C("#8e5cf7"), C("#19c3ff"), 0f)) g.FillRectangle(b, nav.X, nav.Bottom - Theme.D(2), nav.Width, Theme.D(2)); using (var b = new SolidBrush(accent)) g.FillEllipse(b, nav.X + Theme.D(5), nav.Y + Theme.D(4), Theme.D(8), Theme.D(8)); using (var b = new SolidBrush(Color.FromArgb(110, C(p[4])))) for (int k = 0; k < 3; k++) g.FillRectangle(b, nav.X + Theme.D(22) + k * Theme.D(20), nav.Y + Theme.D(6), Theme.D(14), Theme.D(4)); int gp = Theme.D(4); var social = new Rectangle(r.Right - r.Width / 4 - gp, nav.Bottom + gp, r.Width / 4, r.Bottom - nav.Bottom - gp * 2); var main = new Rectangle(r.X + gp, social.Y, social.X - r.X - gp * 2, social.Height); using (var pth = Theme.Round(main, Theme.D(4))) using (var b = new SolidBrush(C(p[2]))) g.FillPath(b, pth); using (var pth = Theme.Round(social, Theme.D(4))) using (var b = new SolidBrush(C(p[1]))) g.FillPath(b, pth); var hero = new Rectangle(main.X + Theme.D(5), main.Y + Theme.D(5), main.Width - Theme.D(10), Theme.D(22)); using (var pth = Theme.Round(hero, Theme.D(4))) using (var b = new LinearGradientBrush(hero, accent, accent2, 20f)) g.FillPath(b, pth); using (var b = new SolidBrush(C(p[3]))) for (int k = 0; k < 2; k++) using (var pth = Theme.Round(new Rectangle(main.X + Theme.D(5) + k * (main.Width / 2 - Theme.D(2)), hero.Bottom + Theme.D(6), main.Width / 2 - Theme.D(8), Theme.D(18)), Theme.D(3))) g.FillPath(b, pth); for (int k = 0; k < 4; k++) { int yy = social.Y + Theme.D(6) + k * Theme.D(11); if (yy > social.Bottom - Theme.D(8)) break; using (var b = new SolidBrush(k < 2 ? C("#23a55a") : Color.FromArgb(120, C(p[4])))) g.FillEllipse(b, social.X + Theme.D(4), yy, Theme.D(6), Theme.D(6)); using (var b = new SolidBrush(Color.FromArgb(100, C(p[4])))) g.FillRectangle(b, social.X + Theme.D(13), yy + Theme.D(2), social.Width - Theme.D(18), Theme.D(3)); } } // mali prikaz rasporeda: moderni (Discord) ili klasicni uski (Xfire) void DrawLayoutPreview(Graphics g, Rectangle r, int i) { if (Theme.LayoutKeys[i] == "dock") { // dock: dva plutajuca panela na tamnoj pozadini sa sjajem i pilula s ikonama na dnu g.SmoothingMode = SmoothingMode.AntiAlias; var a1 = accentPick.Selected > 0 ? C(AccentPicker.Hex[accentPick.Selected]) : Theme.Accent; var a2 = accentPick.Selected > 0 ? Theme.Shade(a1, 0.3f) : Theme.Accent2; using (var bb = new LinearGradientBrush(r, C("#141728"), C("#0b0d18"), 70f)) g.FillRectangle(bb, r); using (var p = new GraphicsPath()) { p.AddEllipse(r.X - r.Width / 3, r.Y - r.Height / 2, r.Width, r.Height); using (var pb = new PathGradientBrush(p) { CenterColor = Color.FromArgb(90, a1), SurroundColors = new[] { Color.FromArgb(0, a1) } }) g.FillPath(pb, p); } int gp = Theme.D(6); var mainR = new Rectangle(r.X + gp, r.Y + Theme.D(14), r.Width * 2 / 3 - gp, r.Height - Theme.D(40)); var sideR = new Rectangle(mainR.Right + gp, mainR.Y, r.Right - mainR.Right - gp * 2, mainR.Height); using (var p = Theme.Round(mainR, Theme.D(7))) using (var bb = new SolidBrush(C("#262b42"))) g.FillPath(bb, p); using (var p = Theme.Round(sideR, Theme.D(7))) using (var bb = new SolidBrush(C("#262b42"))) g.FillPath(bb, p); var hero = new Rectangle(mainR.X + Theme.D(5), mainR.Y + Theme.D(5), mainR.Width - Theme.D(10), Theme.D(22)); using (var p = Theme.Round(hero, Theme.D(5))) using (var lb = new LinearGradientBrush(hero, a1, a2, 20f)) g.FillPath(lb, p); for (int k = 0; k < 4; k++) using (var bb = new SolidBrush(k < 2 ? Theme.On : C("#5a6080"))) g.FillEllipse(bb, sideR.X + Theme.D(5) + (k % 2) * Theme.D(16), sideR.Y + Theme.D(6) + (k / 2) * Theme.D(16), Theme.D(11), Theme.D(11)); var dk = new Rectangle(r.X + r.Width / 2 - Theme.D(48), r.Bottom - Theme.D(20), Theme.D(96), Theme.D(15)); using (var p = Theme.Round(dk, dk.Height / 2f)) using (var bb = new SolidBrush(C("#2f3550"))) g.FillPath(bb, p); for (int k = 0; k < 6; k++) { var dot = new Rectangle(dk.X + Theme.D(8) + k * Theme.D(14), dk.Y + Theme.D(4), Theme.D(7), Theme.D(7)); if (k == 0) using (var lb = new LinearGradientBrush(dot, a1, a2, 45f)) g.FillEllipse(lb, dot); else using (var bb = new SolidBrush(Color.FromArgb(140, Color.White))) g.FillEllipse(bb, dot); } return; } if (Theme.LayoutKeys[i] == "discord") i = 0; else i = 1; using (var b = new SolidBrush(i == 0 ? C("#313338") : C("#202a3a"))) g.FillRectangle(b, r); g.SmoothingMode = SmoothingMode.AntiAlias; if (i == 0) { // moderni: gornja navigacija, pozdravna kartica i kartice igara, prijatelji u ploci desno using (var bb = new SolidBrush(C("#07080b"))) g.FillRectangle(bb, r); var acc = accentPick.Selected > 0 ? C(AccentPicker.Hex[accentPick.Selected]) : C("#7c5cff"); var acc2 = accentPick.Selected <= 1 ? C("#19c3ff") : Theme.Shade(acc, 0.3f); int gp = Theme.D(4); var navR = new Rectangle(r.X, r.Y, r.Width, Theme.D(16)); using (var bb = new SolidBrush(C("#12151c"))) g.FillRectangle(bb, navR); using (var bb = new SolidBrush(acc)) g.FillEllipse(bb, navR.X + Theme.D(5), navR.Y + Theme.D(4), Theme.D(8), Theme.D(8)); using (var bb = new SolidBrush(Color.FromArgb(110, Color.White))) for (int k = 0; k < 4; k++) g.FillRectangle(bb, navR.X + Theme.D(26) + k * Theme.D(22), navR.Y + Theme.D(6), Theme.D(16), Theme.D(4)); using (var lb = new LinearGradientBrush(new Rectangle(navR.X + Theme.D(26), navR.Bottom - Theme.D(2), Theme.D(16), Theme.D(2)), acc, acc2, 0f)) g.FillRectangle(lb, navR.X + Theme.D(26), navR.Bottom - Theme.D(2), Theme.D(16), Theme.D(2)); var socialR = new Rectangle(r.Right - r.Width / 4 - gp, navR.Bottom + gp, r.Width / 4, r.Bottom - navR.Bottom - gp * 2); var mainR = new Rectangle(r.X + gp, navR.Bottom + gp, socialR.X - r.X - gp * 2, socialR.Height); using (var p = Theme.Round(mainR, Theme.D(5))) using (var bb = new SolidBrush(C("#161a22"))) g.FillPath(bb, p); using (var p = Theme.Round(socialR, Theme.D(5))) using (var bb = new SolidBrush(C("#12151c"))) g.FillPath(bb, p); var hero = new Rectangle(mainR.X + Theme.D(5), mainR.Y + Theme.D(5), mainR.Width - Theme.D(10), Theme.D(24)); using (var p = Theme.Round(hero, Theme.D(5))) using (var lb = new LinearGradientBrush(hero, acc, acc2, 25f)) g.FillPath(lb, p); for (int k = 0; k < 2; k++) { var card = new Rectangle(mainR.X + Theme.D(5) + k * (mainR.Width / 2 - Theme.D(2)), hero.Bottom + Theme.D(6), mainR.Width / 2 - Theme.D(8), Theme.D(20)); using (var p = Theme.Round(card, Theme.D(4))) using (var bb = new SolidBrush(C("#1e232d"))) g.FillPath(bb, p); using (var p = Theme.Round(new Rectangle(card.X + Theme.D(3), card.Y + Theme.D(3), Theme.D(14), Theme.D(14)), Theme.D(3))) using (var bb = new SolidBrush(acc2)) g.FillPath(bb, p); } for (int k = 0; k < 5; k++) { int yy = socialR.Y + Theme.D(6) + k * Theme.D(11); if (yy > socialR.Bottom - Theme.D(10)) break; using (var bb = new SolidBrush(k < 2 ? Theme.On : C("#8a93a6"))) g.FillEllipse(bb, socialR.X + Theme.D(4), yy, Theme.D(7), Theme.D(7)); using (var bb = new SolidBrush(Color.FromArgb(110, Color.White))) g.FillRectangle(bb, socialR.X + Theme.D(14), yy + Theme.D(2), socialR.Width - Theme.D(20), Theme.D(3)); } return; } var col = new Rectangle(r.X + r.Width / 2 - r.Width / 6, r.Y + Theme.D(6), r.Width / 3, r.Height - Theme.D(12)); using (var b = new SolidBrush(C("#0d131c"))) g.FillRectangle(b, col); using (var b = new LinearGradientBrush(new Rectangle(col.X, col.Y, col.Width, Theme.D(20)), C("#3a5478"), C("#16223a"), 90f)) g.FillRectangle(b, col.X, col.Y, col.Width, Theme.D(20)); using (var b = new SolidBrush(C("#ff6a1a"))) g.FillRectangle(b, col.X, col.Y + Theme.D(20), col.Width, Theme.D(2)); using (var b = new SolidBrush(C("#4a6a95"))) g.FillRectangle(b, col.X + Theme.D(3), col.Y + Theme.D(4), Theme.D(12), Theme.D(12)); using (var b = new LinearGradientBrush(new Rectangle(col.X, col.Y + Theme.D(26), col.Width, Theme.D(8)), C("#2b3f5e"), C("#172338"), 90f)) g.FillRectangle(b, col.X, col.Y + Theme.D(26), col.Width, Theme.D(8)); for (int k = 0; k < 5; k++) { int y = col.Y + Theme.D(38) + k * Theme.D(10); if (y + Theme.D(8) > col.Bottom - Theme.D(10)) break; using (var b = new SolidBrush(C("#4a6a95"))) g.FillRectangle(b, col.X + Theme.D(3), y, Theme.D(7), Theme.D(7)); using (var b = new SolidBrush(k < 2 ? C("#ffa040") : C("#7f8ea6"))) g.FillRectangle(b, col.X + Theme.D(13), y + Theme.D(2), col.Width - Theme.D(18), Theme.D(3)); } using (var b = new LinearGradientBrush(new Rectangle(col.X, col.Bottom - Theme.D(8), col.Width, Theme.D(8)), C("#2b3f5e"), C("#16223a"), 90f)) g.FillRectangle(b, col.X, col.Bottom - Theme.D(8), col.Width, Theme.D(8)); // mali chat prozor sa strane, kao u starim messengerima var chat = new Rectangle(col.Right + Theme.D(10), col.Y + Theme.D(16), r.Right - col.Right - Theme.D(18), col.Height - Theme.D(30)); if (chat.Width > Theme.D(20)) { using (var b = new SolidBrush(C("#313338"))) g.FillRectangle(b, chat); using (var b = new SolidBrush(C("#16223a"))) g.FillRectangle(b, chat.X, chat.Y, chat.Width, Theme.D(8)); using (var b = new SolidBrush(Color.FromArgb(110, Color.White))) for (int k = 0; k < 3; k++) g.FillRectangle(b, chat.X + Theme.D(4), chat.Y + Theme.D(14) + k * Theme.D(9), chat.Width - Theme.D(12) - k * Theme.D(6), Theme.D(3)); } } public SettingsView(TrayApp app) { this.app = app; BackColor = Theme.Bg; scroll.BackColor = Theme.Bg; scroll.AutoScroll = true; pageTitle.Font = Theme.Body(21, FontStyle.Bold); pageTitle.ForeColor = Theme.Text; pageTitle.BackColor = Theme.Bg; pageTitle.AutoSize = true; Controls.Add(nav); Controls.Add(scroll); SlimBar.Attach(scroll); scroll.Controls.Add(pageTitle); nav.Changed += i => { scroll.AutoScrollPosition = Point.Empty; Relayout(); }; // --- moj racun --- account = new AccountCard(app); Block(new Label { Text = "PROFIL", Font = Theme.Body(12, FontStyle.Bold), ForeColor = Theme.Muted, BackColor = Theme.Bg, AutoSize = true }, "account"); Block(account, "account"); var web = Card("Na webu", "account"); web.Add("Web messenger", "Fragline u pregledniku, sa svim porukama.", Btn("Otvori", false, () => app.Open("/app"))); web.Add("Moj profil", "Tvoja javna stranica sa screenshotovima i igrama.", Btn("Otvori", false, () => app.OpenProfile())); var danger = Card("Odjava", "account"); var logout = Btn("Odjavi se", false, () => app.LogoutAndAsk()); logout.Danger = true; danger.Add("Odjavi se s ovog računala", "Morat ćeš se ponovno prijaviti korisničkim imenom i lozinkom.", logout); // --- izgled: slicice umjesto padajucih izbornika --- layoutPick.Names = Theme.LayoutNames; layoutPick.Preview = DrawLayoutPreview; layoutPick.Selected = Math.Max(0, Array.IndexOf(Theme.LayoutKeys, Prefs.Str("layout", "dock"))); themePick.Names = Theme.ThemeNames.Select(n => n.Replace(" (animirana)", " (animirana)").Replace(" (crna)", "").Replace(" (plava noć)", "").Replace(" (Xfire)", "")).ToArray(); themePick.Preview = DrawThemePreview; themePick.Selected = Math.Max(0, Array.IndexOf(Theme.ThemeKeys, Prefs.Str("theme", "dash"))); accentPick.Selected = Math.Max(0, Array.IndexOf(Theme.AccentKeys, Prefs.Str("accent", "auto"))); accentPick.AutoColor = C(Pal[Theme.ThemeKeys[themePick.Selected]][5]); var look = Card("Izgled", "look"); look.AddBlock("Raspored", "Dock je novi izgled s plutajućim panelima. Promjena vrijedi nakon ponovnog pokretanja.", layoutPick, layoutPick.HeightFor); look.AddBlock("Tema", "Boje cijele aplikacije. Aurora se polako prelijeva bojama.", themePick, themePick.HeightFor); look.AddBlock("Boja naglaska", "Gumbi, odabrane stavke i oznake.", accentPick, w => Theme.D(48)); applyRow = look.Add("Promjene su spremljene", "Vrijede nakon ponovnog pokretanja aplikacije.", Btn("Ponovno pokreni", true, () => app.Restart())); applyRow.Visible = false; layoutPick.Picked += i => { var lk = Theme.LayoutKeys[i]; Prefs.Set("layout", lk); // uz klasicni raspored ide i klasicna plava tema (moze se promijeniti) if (lk == "xfire" || Prefs.Str("theme", "dash") == "xfire") PickTheme(Array.IndexOf(Theme.ThemeKeys, lk == "xfire" ? "xfire" : "dash")); ShowApply(); }; themePick.Picked += i => PickTheme(i); accentPick.Picked += i => { Prefs.Set("accent", Theme.AccentKeys[i]); themePick.Invalidate(); layoutPick.Invalidate(); ShowApply(); }; // --- opcenito --- var gen = Card("Općenito", "general"); gen.Add("Pokreni s Windowsima", "Fragline se pokrene skriven kraj sata.", auto); gen.Add("Automatski \"Odsutan\"", "Nakon 10 minuta bez miša i tipkovnice.", idle); gen.Add("Dijeli server s prijateljima", "Prijatelji vide na kojem serveru igraš (gdje je to moguće otkriti).", share); // --- obavijesti --- var notif = Card("Obavijesti", "notif"); notif.Add("Kad prijatelj zaigra", null, ng); notif.Add("Nove poruke", "Obavijest kraj sata kad ti netko piše.", nm); notif.Add("Zvuk za nove poruke", null, snd); notif.Add("Zvukovi u voiceu", "Kad netko uđe ili izađe iz sobe.", sfx); // --- precaci i zvuk --- var keys = Card("Prečaci", "keys"); keys.Add("Mikrofon i zvuk", "Ctrl+Shift+M isključuje mikrofon, Ctrl+Shift+D zvuk.", hkv); keys.Add("Screenshot", "Scroll Lock u igri šalje sliku na tvoj profil.", hks); var media = Card("Zvuk i screenshot", "keys"); media.Add("Zvuk i mikrofon", "Uređaji, glasnoća, osjetljivost i utišavanje buke.", Btn("Postavke zvuka", false, () => app.ShowAudioSettings())); shotOpen.Font = Theme.Display(14); shotOpen.Size = new Size(TextRenderer.MeasureText("Otvori", shotOpen.Font).Width + Theme.D(32), Theme.D(34)); shotOpen.Visible = false; shotOpen.Click += (s, e) => { if (shotUrl != null) { try { Process.Start(shotUrl); } catch { } } }; shotRow = media.Add("Zadnji screenshot", "Još ništa. Stisni Scroll Lock u igri.", Pair(shotOpen, Btn("Snimi sada", false, () => app.Screenshot()))); auto.CheckedChanged += (s, e) => TrayApp.SetAuto(auto.Checked); ng.CheckedChanged += (s, e) => Prefs.Set("notify_games", ng.Checked); nm.CheckedChanged += (s, e) => Prefs.Set("notify_msgs", nm.Checked); snd.CheckedChanged += (s, e) => Prefs.Set("msg_sound", snd.Checked); idle.CheckedChanged += (s, e) => Prefs.Set("auto_idle", idle.Checked); share.CheckedChanged += (s, e) => Prefs.Set("share_server", share.Checked); sfx.CheckedChanged += (s, e) => AudioCfg.Sounds = sfx.Checked; hkv.CheckedChanged += (s, e) => { Prefs.Set("hk_voice", hkv.Checked); app.ApplyHotkeys(); }; hks.CheckedChanged += (s, e) => { Prefs.Set("hk_shot", hks.Checked); app.ApplyHotkeys(); }; refresh.Tick += (s, e) => { if (Visible && account.Visible) { account.Remove.Visible = app.AvatarUrl != null; account.Invalidate(); } }; refresh.Start(); Theme.DarkScroll(scroll); Relayout(); } void PickTheme(int i) { if (i < 0) return; themePick.Selected = i; Prefs.Set("theme", Theme.ThemeKeys[i]); accentPick.AutoColor = C(Pal[Theme.ThemeKeys[i]][5]); accentPick.Invalidate(); ShowApply(); } void ShowApply() { if (applyRow.Visible) return; applyRow.Visible = true; Relayout(); } protected override void OnResize(EventArgs e) { base.OnResize(e); Relayout(); } bool Wide { get { return Width >= Theme.D(760); } } // siroko: izbornik lijevo i samo odabrana kategorija; usko: sve u jednom stupcu void Relayout() { if (laying || account == null || refresh == null) return; laying = true; int before = scroll.ClientSize.Width; try { int navW = Wide ? Theme.D(232) : 0; nav.Visible = Wide; nav.SetBounds(0, 0, navW, Height); scroll.SetBounds(navW, 0, Math.Max(10, Width - navW), Height); int cw = scroll.ClientSize.Width; int colW = Math.Min(Math.Max(Theme.D(300), cw - Theme.D(56)), Theme.D(720)); int x0 = Wide ? Theme.D(40) : Math.Max(Theme.D(16), (cw - colW) / 2); if (Wide) colW = Math.Min(colW, cw - Theme.D(80)); int y = Theme.D(28) + scroll.AutoScrollPosition.Y; string only = Wide ? SettingsNav.Keys[nav.Selected] : null; pageTitle.Visible = Wide; if (Wide) { pageTitle.Text = SettingsNav.Names[nav.Selected]; pageTitle.Location = new Point(x0, y); y += Theme.D(52); } foreach (var b in blocks) { bool show = only == null || cat[b] == only; b.Visible = show; if (!show) continue; var card = b as SCard; if (b == account) { account.SetBounds(x0, y, colW, Theme.D(140)); y += Theme.D(140) + Theme.D(28); } else if (card != null) { int h = card.LayoutRows(colW); card.SetBounds(x0, y, colW, h); card.Invalidate(); y += h + Theme.D(28); } else { b.Location = new Point(x0 + Theme.D(4), y); y += Theme.D(24); } } scroll.AutoScrollMinSize = new Size(0, y - scroll.AutoScrollPosition.Y + Theme.D(16)); } finally { laying = false; } // pojava trake za pomicanje suzi prostor, pa slozi jos jednom if (scroll.ClientSize.Width != before) Relayout(); } protected override void OnVisibleChanged(EventArgs e) { if (Visible) { auto.Checked = TrayApp.IsAuto(); ng.Checked = Prefs.Bool("notify_games", true); nm.Checked = Prefs.Bool("notify_msgs", true); snd.Checked = Prefs.Bool("msg_sound", true); idle.Checked = Prefs.Bool("auto_idle", true); share.Checked = Prefs.Bool("share_server", true); sfx.Checked = AudioCfg.Sounds; hkv.Checked = Prefs.Bool("hk_voice", true); hks.Checked = Prefs.Bool("hk_shot", true); account.Remove.Visible = app.AvatarUrl != null; account.Invalidate(); } base.OnVisibleChanged(e); } public void SetShot(string game, string url) { shotUrl = url; shotRow.Desc.Text = "Danas u " + DateTime.Now.ToString("HH:mm") + (game != null ? ", " + game : "") + "."; shotOpen.Visible = url != null; } } } // ===================== Shell.cs ===================== using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Globalization; using System.Linq; using System.Net; using System.Threading.Tasks; using System.Windows.Forms; namespace Fragline { static class Txt { static readonly CultureInfo Hr = CreateHr(); static CultureInfo CreateHr() { try { return new CultureInfo("hr-HR"); } catch { return CultureInfo.InvariantCulture; } } // Sto prijatelj trenutno radi (igra ili voice), ili null public static string Activity(Friend f) { if (f == null) return null; if (f.Status == "play" && f.Game != null) { var s = Theme.Since(f.Since); return "Igra " + f.Game + (s != null ? " · " + s : ""); } if (f.Voice != null) return "U voiceu: " + f.Voice; return null; } public static string Day(DateTime local) { var today = DateTime.Now.Date; if (local.Date == today) return "Danas"; if (local.Date == today.AddDays(-1)) return "Jučer"; return local.ToString("d. MMMM yyyy.", Hr); } public static string When(DateTime local) { var today = DateTime.Now.Date; if (local.Date == today) return "Danas u " + local.ToString("HH:mm"); if (local.Date == today.AddDays(-1)) return "Jučer u " + local.ToString("HH:mm"); return local.ToString("dd.MM.yyyy. HH:mm"); } public static int Rank(Friend f) { return f.Status == "play" ? 0 : f.Status == "on" ? 1 : 2; } public static void Draw(Graphics g, string text, Font f, Rectangle r, Color c, TextFormatFlags extra = 0) { TextRenderer.DrawText(g, text ?? "", f, r, c, TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPadding | TextFormatFlags.NoPrefix | extra); } } // Zaglavlje bocne trake: naslov i jedna ikona-gumb desno class SideHead : Control { readonly string glyph; readonly ToolTip tt = new ToolTip(); bool hot; public event Action ActionClicked; public SideHead(string title, string glyph, string tip) { Text = title; this.glyph = glyph; SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); BackColor = Theme.Panel; if (tip != null) tt.SetToolTip(this, tip); } Rectangle Btn() { int s = Theme.D(26); return new Rectangle(Width - Theme.D(12) - s, (Height - s) / 2, s, s); } protected override void OnMouseMove(MouseEventArgs e) { bool h = glyph != null && Btn().Contains(e.Location); if (h != hot) { hot = h; Cursor = h ? Cursors.Hand : Cursors.Default; Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseLeave(EventArgs e) { hot = false; Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseClick(MouseEventArgs e) { if (glyph != null && Btn().Contains(e.Location) && ActionClicked != null) ActionClicked(); base.OnMouseClick(e); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.Clear(Theme.Panel); using (var f = Theme.Body(15, FontStyle.Bold)) Txt.Draw(g, Text, f, new Rectangle(Theme.D(16), 0, Width - Theme.D(56), Height), Theme.Text); if (glyph != null) Theme.Icon(g, glyph, "+", Btn(), hot ? Theme.Text : Theme.Muted, 15); using (var pen = new Pen(Theme.Shadow, Theme.D(1))) g.DrawLine(pen, 0, Height - 1, Width, Height - 1); } } class SideRow { public string Kind, Key, Text, Glyph; public Friend F; } // Bocna traka pocetne: Prijatelji, Aktivnost i direktne poruke class HomeList : UserControl { readonly TrayApp app; readonly Panel head = new Panel(); readonly TextBox search = new TextBox(); readonly ListBox list = new SmoothList(); List rows = new List(); List friends = new List(); int requests, hover = -1; string sel = "nav:friends", lastSig = ""; readonly bool dmOnly; public event Action Picked; public HomeList(TrayApp app, bool dmOnly = false) { this.dmOnly = dmOnly; this.app = app; BackColor = Theme.Panel; head.Dock = DockStyle.Top; head.Height = Theme.D(48); head.BackColor = Theme.Panel; head.Paint += (s, e) => { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; using (var p = Theme.Round(new Rectangle(Theme.D(10), Theme.D(10), head.Width - Theme.D(20), Theme.D(28)), Theme.D(4))) using (var b = new SolidBrush(Theme.Field)) g.FillPath(b, p); using (var pen = new Pen(Theme.Shadow, Theme.D(1))) g.DrawLine(pen, 0, head.Height - 1, head.Width, head.Height - 1); }; search.BorderStyle = BorderStyle.None; search.BackColor = Theme.Field; search.ForeColor = Theme.Text; search.Font = Theme.Body(13.5f); Theme.Cue(search, "Pronađi razgovor"); head.Controls.Add(search); head.Resize += (s, e) => search.SetBounds(Theme.D(18), Theme.D(10) + (Theme.D(28) - search.PreferredHeight) / 2, Math.Max(10, head.Width - Theme.D(36)), search.PreferredHeight); search.TextChanged += (s, e) => Rebuild(); list.Dock = DockStyle.Fill; list.DrawMode = DrawMode.OwnerDrawVariable; list.BorderStyle = BorderStyle.None; list.BackColor = Theme.Panel; list.IntegralHeight = false; list.SelectionMode = SelectionMode.None; list.MeasureItem += (s, e) => { var r = e.Index >= 0 && e.Index < rows.Count ? rows[e.Index] : null; e.ItemHeight = r == null ? Theme.D(40) : r.Kind == "nav" ? Theme.D(44) : r.Kind == "head" ? Theme.D(40) : r.Kind == "empty" ? Theme.D(60) : Theme.D(44); }; list.DrawItem += DrawRow; list.MouseMove += (s, e) => { int i = list.IndexFromPoint(e.Location); if (i != hover) { hover = i; list.Cursor = i >= 0 && i < rows.Count && rows[i].Kind != "empty" ? Cursors.Hand : Cursors.Default; list.Invalidate(); } }; list.MouseLeave += (s, e) => { hover = -1; list.Invalidate(); }; list.MouseUp += (s, e) => { int i = list.IndexFromPoint(e.Location); if (i < 0 || i >= rows.Count) return; var r = rows[i]; if (e.Button == MouseButtons.Right && r.F != null) { var name = r.F.Username; var menu = Ui.Menu(); menu.Items.Add(new ToolStripMenuItem("Pošalji poruku", null, (a, b) => app.OpenChatByName(name))); menu.Items.Add(new ToolStripMenuItem("Pozovi", null, (a, b) => app.CallByName(name))); menu.Items.Add(new ToolStripMenuItem("Otvori profil", null, (a, b) => app.Open("/profile/" + Uri.EscapeDataString(name)))); menu.Items.Add(new ToolStripSeparator()); menu.Items.Add(new ToolStripMenuItem("Ukloni prijatelja", null, (a, b) => app.RemoveFriend(name)) { ForeColor = Theme.Red }); menu.Show(list, e.Location); return; } if (e.Button != MouseButtons.Left) return; if (r.Kind == "head") app.ShowAddFriend(); else if (r.Key != null) { Select(r.Key); if (Picked != null) Picked(r.Key); } }; Theme.DarkScroll(list); Controls.Add(list); Controls.Add(head); list.BringToFront(); Rebuild(); } public void Select(string key) { if (sel != key) { sel = key; list.Invalidate(); } } public void SetData(List f, int req) { friends = f; requests = req; Rebuild(); } void Rebuild() { var q = search.Text.Trim(); var nr = new List(); if (!dmOnly) { nr.Add(new SideRow { Kind = "nav", Key = "nav:friends", Text = "Prijatelji", Glyph = Theme.IcoPeople }); nr.Add(new SideRow { Kind = "nav", Key = "nav:activity", Text = "Aktivnost", Glyph = Theme.IcoHistory }); nr.Add(new SideRow { Kind = "nav", Key = "nav:servers", Text = "Serveri", Glyph = Theme.IcoGame }); } nr.Add(new SideRow { Kind = "head", Text = dmOnly ? "RAZGOVORI" : "DIREKTNE PORUKE" }); foreach (var f in friends.Where(x => q.Length == 0 || (x.Username ?? "").IndexOf(q, StringComparison.OrdinalIgnoreCase) >= 0) .OrderByDescending(x => x.Unread > 0).ThenBy(Txt.Rank).ThenBy(x => x.Username, StringComparer.OrdinalIgnoreCase)) nr.Add(new SideRow { Kind = "dm", Key = "dm:" + f.Username, Text = f.Username, F = f }); if (!nr.Any(x => x.Kind == "dm")) nr.Add(new SideRow { Kind = "empty", Text = q.Length > 0 ? "Nitko ne odgovara traženju." : "Još nemaš prijatelja. Dodaj ih klikom na + iznad." }); var sig = string.Join("|", nr.Select(x => x.Kind + x.Key + x.Text).ToArray()); rows = nr; if (sig != lastSig) { lastSig = sig; int top = list.Items.Count > 0 ? list.TopIndex : 0; list.BeginUpdate(); list.Items.Clear(); foreach (var x in nr) list.Items.Add(x); list.EndUpdate(); if (top < list.Items.Count) list.TopIndex = top; } else list.Invalidate(); } void DrawRow(object s, DrawItemEventArgs e) { if (e.Index < 0 || e.Index >= rows.Count) return; var r = rows[e.Index]; var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; var b = e.Bounds; using (var fill = new SolidBrush(Theme.Panel)) g.FillRectangle(fill, b); bool hot = e.Index == hover; if (r.Kind == "head") { using (var f = Theme.Body(12, FontStyle.Bold)) Txt.Draw(g, r.Text, f, new Rectangle(b.X + Theme.D(18), b.Y + Theme.D(14), b.Width - Theme.D(50), b.Height - Theme.D(14)), hot ? Theme.Text2 : Theme.Muted); Theme.Icon(g, Theme.IcoAdd, "+", new Rectangle(b.Right - Theme.D(34), b.Y + Theme.D(14), Theme.D(20), b.Height - Theme.D(14)), hot ? Theme.Text : Theme.Muted, 12); return; } if (r.Kind == "empty") { using (var f = Theme.Body(13)) TextRenderer.DrawText(g, r.Text, f, new Rectangle(b.X + Theme.D(18), b.Y + Theme.D(6), b.Width - Theme.D(36), b.Height - Theme.D(6)), Theme.Muted, TextFormatFlags.WordBreak | TextFormatFlags.NoPrefix); return; } bool selected = r.Key == sel; var inner = new Rectangle(b.X + Theme.D(8), b.Y + Theme.D(1), b.Width - Theme.D(16), b.Height - Theme.D(2)); var bg = Theme.ClassicLayout ? (selected ? Theme.Panel3 : hot ? Theme.Panel2 : Theme.Panel) : selected ? Fx.Mix(Theme.Panel, Theme.Accent, 0.2f) : hot ? Fx.Mix(Theme.Panel, Color.White, 0.05f) : Theme.Panel; if (selected || hot) using (var p = Theme.Round(inner, Theme.ClassicLayout ? Theme.D(4) : Theme.D(12))) using (var fill = new SolidBrush(bg)) g.FillPath(fill, p); if (selected && !Theme.ClassicLayout) using (var bar = new LinearGradientBrush(new Rectangle(inner.X, inner.Y, Theme.D(3), inner.Height), Theme.Accent, Theme.Accent2, 90f)) using (var p = Theme.Round(new Rectangle(inner.X + Theme.D(2), inner.Y + Theme.D(9), Theme.D(3), inner.Height - Theme.D(18)), Theme.D(1.5f))) g.FillPath(bar, p); var fc = selected || hot ? Theme.Text : Theme.Muted; if (r.Kind == "nav") { Theme.Icon(g, r.Glyph, r.Text.Substring(0, 1), new Rectangle(inner.X + Theme.D(8), inner.Y, Theme.D(26), inner.Height), fc, 18); int right = inner.Right - Theme.D(8); if (r.Key == "nav:friends" && requests > 0) Theme.Pill(g, requests.ToString(), new Rectangle(inner.X, inner.Y, right - inner.X, inner.Height), true, Color.Empty); using (var f = Theme.Body(15, FontStyle.Bold)) Txt.Draw(g, r.Text, f, new Rectangle(inner.X + Theme.D(44), inner.Y, right - inner.X - Theme.D(70), inner.Height), fc); return; } var fr = r.F; string kind = fr.Kind; float av = Theme.D(32); Theme.Avatar(g, new RectangleF(inner.X + Theme.D(8), inner.Y + (inner.Height - av) / 2f, av, av), fr.Color, fr.Username, fr.Avatar, list, kind, bg); int x = inner.X + Theme.D(50), rgt = inner.Right - Theme.D(8); if (fr.Unread > 0) { var txt = fr.Unread > 99 ? "99+" : fr.Unread.ToString(); Theme.Pill(g, txt, new Rectangle(x, inner.Y, rgt - x, inner.Height), true, Color.Empty); rgt -= Theme.D(30); } var nameColor = selected || hot || fr.Unread > 0 ? Theme.Text : kind == "off" ? Theme.Shade(Theme.Muted, -0.25f) : Theme.Muted; var sub = Txt.Activity(fr) ?? (kind == "dnd" || kind == "idle" ? Theme.KindText(kind) : null); using (var f = Theme.Body(15, fr.Unread > 0 ? FontStyle.Bold : FontStyle.Regular)) { if (sub == null) Txt.Draw(g, fr.Username, f, new Rectangle(x, inner.Y, Math.Max(10, rgt - x), inner.Height), nameColor); else { Txt.Draw(g, fr.Username, f, new Rectangle(x, inner.Y + Theme.D(3), Math.Max(10, rgt - x), Theme.D(20)), nameColor); using (var fs = Theme.Body(12)) Txt.Draw(g, sub, fs, new Rectangle(x, inner.Y + Theme.D(22), Math.Max(10, rgt - x), Theme.D(16)), Theme.Muted); } } } } // Donji lijevi panel: avatar, ime, status i gumbi za mikrofon, zvuk i postavke class UserPanel : Control { public string User = "", ColorHex, Avatar, Game, Presence = "online"; public DateTime? SinceUtc; public bool Offline, Idle, Muted, Deaf; readonly ToolTip tt = new ToolTip(); int hot = -1; public event Action ProfileClicked; public event Action MicClicked, DeafClicked, SettingsClicked; public UserPanel() { SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); BackColor = Theme.UserBar; Anim.Hook(this); } public string Kind { get { if (Offline || Presence == "invisible") return "off"; if (Presence == "dnd") return "dnd"; if (Presence == "idle" || Idle) return "idle"; return "online"; } } Rectangle Btn(int i) { int s = Theme.D(32); return new Rectangle(Width - Theme.D(8) - s * (3 - i), (Height - s) / 2, s, s); } Rectangle Prof() { return new Rectangle(Theme.D(6), Theme.D(6), Math.Max(10, Btn(0).X - Theme.D(10)), Height - Theme.D(12)); } int Hit(Point p) { if (Prof().Contains(p)) return 0; for (int i = 0; i < 3; i++) if (Btn(i).Contains(p)) return i + 1; return -1; } protected override void OnMouseMove(MouseEventArgs e) { int h = Hit(e.Location); if (h != hot) { hot = h; Cursor = h >= 0 ? Cursors.Hand : Cursors.Default; tt.SetToolTip(this, h == 0 ? "Promijeni status" : h == 1 ? (Muted ? "Uključi mikrofon" : "Isključi mikrofon") : h == 2 ? (Deaf ? "Uključi zvuk" : "Isključi zvuk") : h == 3 ? "Postavke" : null); Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseLeave(EventArgs e) { hot = -1; Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseClick(MouseEventArgs e) { Fire(Hit(e.Location), new Point(Prof().X, 0)); Invalidate(); base.OnMouseClick(e); } protected void Fire(int h, Point menuAt) { if (h == 0 && ProfileClicked != null) ProfileClicked(menuAt); else if (h == 1 && MicClicked != null) MicClicked(); else if (h == 2 && DeafClicked != null) DeafClicked(); else if (h == 3 && SettingsClicked != null) SettingsClicked(); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.Clear(Parent != null ? Parent.BackColor : Theme.UserBar); g.SmoothingMode = SmoothingMode.AntiAlias; using (var p = Theme.Round(new RectangleF(0, 0, Width, Height), Theme.D(12))) { using (var b = new SolidBrush(Theme.UserBar)) g.FillPath(b, p); if (Theme.Animated) { var st = g.Save(); g.SetClip(p); Anim.Fill(g, ClientRectangle, 0.7f, 0.16f, 0f); g.Restore(st); g.SmoothingMode = SmoothingMode.AntiAlias; } } var pr = Prof(); var pbg = hot == 0 ? Theme.Panel3 : Theme.UserBar; if (hot == 0) using (var p = Theme.Round(pr, Theme.D(4))) using (var b = new SolidBrush(pbg)) g.FillPath(b, p); float av = Theme.D(32); Theme.Avatar(g, new RectangleF(pr.X + Theme.D(2), (Height - av) / 2f, av, av), ColorHex, User, Avatar, this, Kind, pbg); int x = pr.X + Theme.D(42), w = Math.Max(10, pr.Right - x - Theme.D(2)); using (var f = Theme.Body(14, FontStyle.Bold)) Txt.Draw(g, User, f, new Rectangle(x, Height / 2 - Theme.D(17), w, Theme.D(18)), Theme.Text); string sub; Color sc = Theme.Muted; if (Offline) { sub = "Nema veze sa serverom"; sc = Theme.Red; } else if (Game != null) sub = "Igra " + Game + (SinceUtc.HasValue ? " · " + Theme.Since(new DateTimeOffset(SinceUtc.Value, TimeSpan.Zero).ToString("o")) : ""); else sub = Theme.KindText(Presence == "invisible" ? "invisible" : Kind); using (var f = Theme.Body(12)) Txt.Draw(g, sub, f, new Rectangle(x, Height / 2 + Theme.D(1), w, Theme.D(16)), sc); for (int i = 0; i < 3; i++) { var r = Btn(i); var bbg = hot == i + 1 ? Theme.Panel3 : Theme.UserBar; if (hot == i + 1) using (var p = Theme.Round(r, Theme.D(4))) using (var b = new SolidBrush(bbg)) g.FillPath(b, p); bool off = (i == 0 && Muted) || (i == 1 && Deaf); var col = off ? Theme.Red : Theme.Text2; Theme.Icon(g, i == 0 ? Theme.IcoMic : i == 1 ? Theme.IcoHead : Theme.IcoGear, i == 0 ? "M" : i == 1 ? "Z" : "P", r, col, 18); if (off) Theme.Slash(g, r, bbg); } } } // Zaglavlje sadrzaja: ikona, naslov, kartice i gumb (kao "Prijatelji | Online Svi Na cekanju | Dodaj prijatelja") class PageHeader : Control { public string Glyph, TextIcon, Title, Sub, Kind, ActionText, RightGlyph, RightTip, Right2Glyph, Right2Tip; // moderni izgled: avatar osobe u zaglavlju razgovora (umjesto znaka @) public string AvatarName, AvatarColor, AvatarUrl; public bool ActionActive; public Color SubColor = Theme.Muted; public string[] Tabs = new string[0]; public int[] TabBadges; int tab; readonly List tabRects = new List(); Rectangle actionRect, rightRect, right2Rect; int hot = -1; readonly ToolTip tt = new ToolTip(); public event Action TabChanged; public event Action ActionClicked, RightClicked, Right2Clicked; public PageHeader() { SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); BackColor = Theme.Bg; Anim.Hook(this, () => new Rectangle(0, Height - Theme.D(3), Width, Theme.D(3))); } public int Tab { get { return tab; } set { if (tab != value) { tab = value; Invalidate(); if (TabChanged != null) TabChanged(tab); } } } int Hit(Point p) { for (int i = 0; i < tabRects.Count; i++) if (tabRects[i].Contains(p)) return i; if (ActionText != null && actionRect.Contains(p)) return 100; if (RightGlyph != null && rightRect.Contains(p)) return 101; if (Right2Glyph != null && right2Rect.Contains(p)) return 102; return -1; } protected override void OnMouseMove(MouseEventArgs e) { int h = Hit(e.Location); if (h != hot) { hot = h; Cursor = h >= 0 ? Cursors.Hand : Cursors.Default; tt.SetToolTip(this, h == 101 ? RightTip : h == 102 ? Right2Tip : null); Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseLeave(EventArgs e) { hot = -1; Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseClick(MouseEventArgs e) { int h = Hit(e.Location); if (h >= 0 && h < 100) { tab = h; Invalidate(); if (TabChanged != null) TabChanged(h); } else if (h == 100 && ActionClicked != null) ActionClicked(); else if (h == 101 && RightClicked != null) RightClicked(); else if (h == 102 && Right2Clicked != null) Right2Clicked(); base.OnMouseClick(e); } void RoundGlass(Graphics g, Rectangle r, string icon, bool hot, bool call) { using (var b = new SolidBrush(hot && call ? Theme.On : Fx.Mix(Theme.Bg, Color.White, hot ? 0.14f : 0.07f))) g.FillEllipse(b, r); if (!hot) using (var pen = new Pen(Fx.Mix(Theme.Bg, Color.White, 0.12f))) g.DrawEllipse(pen, r); float s = r.Width * 0.46f; Vec.Draw(g, icon, new RectangleF(r.X + (r.Width - s) / 2, r.Y + (r.Height - s) / 2, s, s), hot ? Color.White : Theme.Text2, Math.Max(1.4f, Theme.D(1.6f))); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.Clear(Theme.Bg); g.SmoothingMode = SmoothingMode.AntiAlias; if (Theme.ClassicLayout) using (var pen = new Pen(Theme.Shadow, Theme.D(1))) g.DrawLine(pen, 0, Height - 1, Width, Height - 1); else using (var pen = new Pen(Theme.Line, Theme.D(1))) g.DrawLine(pen, Theme.D(16), Height - 1, Width - Theme.D(16), Height - 1); if (Theme.Animated) Anim.Fill(g, new Rectangle(0, Height - Theme.D(2), Width, Theme.D(2)), 0.8f, 1f, 0f); int x = Theme.D(16), right = Width - Theme.D(16); if (!Theme.ClassicLayout && (RightGlyph == Theme.IcoContact || RightGlyph == null) && (Right2Glyph == Theme.IcoPhone || Right2Glyph == null) && (RightGlyph != null || Right2Glyph != null)) { // stakleni okrugli gumbi s nacrtanim ikonama (profil, poziv) int bs = Theme.D(36); if (RightGlyph != null) { rightRect = new Rectangle(right - bs, (Height - bs) / 2, bs, bs); RoundGlass(g, rightRect, "user", hot == 101, false); right = rightRect.X - Theme.D(8); } if (Right2Glyph != null) { right2Rect = new Rectangle(right - bs, (Height - bs) / 2, bs, bs); RoundGlass(g, right2Rect, "phone", hot == 102, true); right = right2Rect.X - Theme.D(10); } } else { if (RightGlyph != null) { rightRect = new Rectangle(right - Theme.D(28), (Height - Theme.D(28)) / 2, Theme.D(28), Theme.D(28)); Theme.Icon(g, RightGlyph, "i", rightRect, hot == 101 ? Theme.Text : Theme.Muted, 18); right = rightRect.X - Theme.D(10); } if (Right2Glyph != null) { right2Rect = new Rectangle(right - Theme.D(28), (Height - Theme.D(28)) / 2, Theme.D(28), Theme.D(28)); Theme.Icon(g, Right2Glyph, "T", right2Rect, hot == 102 ? Theme.On : Theme.Muted, 18); right = right2Rect.X - Theme.D(10); } } if (AvatarName != null && !Theme.ClassicLayout) { float av = Theme.D(34); var ar = new RectangleF(x, (Height - av) / 2f, av, av); Theme.Avatar(g, ar, AvatarColor, AvatarName, AvatarUrl, this, Kind, Theme.Bg); x += Theme.D(46); } else if (Glyph != null) { Theme.Icon(g, Glyph, "", new Rectangle(x, 0, Theme.D(24), Height), Theme.Muted, 20); x += Theme.D(34); } else if (TextIcon != null) { using (var f = Theme.Body(21)) { var sz = TextRenderer.MeasureText(TextIcon, f); Txt.Draw(g, TextIcon, f, new Rectangle(x, 0, sz.Width, Height), Theme.Muted); x += sz.Width; } } using (var f = Theme.ClassicLayout ? Theme.Body(16, FontStyle.Bold) : Theme.Display(18)) { var sz = TextRenderer.MeasureText(Title ?? "", f, Size.Empty, TextFormatFlags.NoPadding); int tw = Math.Min(sz.Width + Theme.D(2), Math.Max(10, right - x)); Txt.Draw(g, Title, f, new Rectangle(x, 0, tw, Height), Theme.Text); x += tw + Theme.D(8); } if (Kind != null && (AvatarName == null || Theme.ClassicLayout)) { float ds = Theme.D(10); Theme.StatusDot(g, new RectangleF(x, (Height - ds) / 2f, ds, ds), Kind, Theme.Bg); x += Theme.D(20); } if (Sub != null && x < right) { using (var pen = new Pen(Theme.Panel3, Theme.D(1))) g.DrawLine(pen, x, Height / 2 - Theme.D(11), x, Height / 2 + Theme.D(11)); x += Theme.D(10); using (var f = Theme.Body(13.5f)) Txt.Draw(g, Sub, f, new Rectangle(x, 0, Math.Max(10, right - x), Height), SubColor); } tabRects.Clear(); if (Tabs.Length > 0) { x += Theme.D(6); using (var f = Theme.Display(14)) { // sve kartice u jednoj zaobljenoj "piluli", odabrana u prijelazu boje naglaska var widths = new List(); for (int i = 0; i < Tabs.Length; i++) { int badge = TabBadges != null && i < TabBadges.Length ? TabBadges[i] : 0; widths.Add(TextRenderer.MeasureText(Tabs[i], f, Size.Empty, TextFormatFlags.NoPadding).Width + Theme.D(28) + (badge > 0 ? Theme.D(24) : 0)); } int ph = Theme.D(34), py = (Height - ph) / 2; var pill = new Rectangle(x, py, widths.Sum() + Theme.D(8), ph); using (var p = Theme.Round(pill, ph / 2f)) using (var b = new SolidBrush(Theme.Field)) g.FillPath(b, p); int tx = pill.X + Theme.D(4); for (int i = 0; i < Tabs.Length; i++) { int badge = TabBadges != null && i < TabBadges.Length ? TabBadges[i] : 0; var r = new Rectangle(tx, py + Theme.D(4), widths[i], ph - Theme.D(8)); tabRects.Add(r); bool sel = i == tab && !ActionActive, h = i == hot; if (sel) using (var p = Theme.Round(r, r.Height / 2f)) using (var b = new LinearGradientBrush(r, Theme.Accent, Theme.Accent2, 0f)) g.FillPath(b, p); else if (h) using (var p = Theme.Round(r, r.Height / 2f)) using (var b = new SolidBrush(Theme.Panel2)) g.FillPath(b, p); var tr = new Rectangle(r.X + Theme.D(14), r.Y, r.Width - Theme.D(28) - (badge > 0 ? Theme.D(24) : 0), r.Height); TextRenderer.DrawText(g, Tabs[i], f, tr, sel ? Color.White : h ? Theme.Text : Theme.Muted, TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding); if (badge > 0) Theme.Pill(g, badge.ToString(), new Rectangle(r.X, r.Y, r.Width - Theme.D(10), r.Height), true, Color.Empty); tx = r.Right; } x = pill.Right + Theme.D(12); if (ActionText != null) { var sz = TextRenderer.MeasureText(ActionText, f, Size.Empty, TextFormatFlags.NoPadding); actionRect = new Rectangle(x, py, sz.Width + Theme.D(32), ph); using (var p = Theme.Round(actionRect, ph / 2f)) { if (ActionActive || hot == 100) using (var b = new LinearGradientBrush(actionRect, Theme.Shade(Theme.On, ActionActive ? 0 : 0.1f), Theme.Shade(Theme.On, -0.25f), 0f)) g.FillPath(b, p); using (var pen = new Pen(Theme.On, Theme.D(1.5f))) g.DrawPath(pen, p); } TextRenderer.DrawText(g, ActionText, f, actionRect, ActionActive || hot == 100 ? Color.White : Theme.On, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding); } } } } } class HeaderPage : UserControl { readonly PageHeader head; readonly Control body; public HeaderPage(PageHeader h, Control b) { head = h; body = b; BackColor = Theme.Bg; Controls.Add(body); Controls.Add(head); } protected override void OnResize(EventArgs e) { base.OnResize(e); if (head == null || body == null) return; head.SetBounds(0, 0, Width, Theme.D(48)); body.SetBounds(0, Theme.D(48), Width, Math.Max(0, Height - Theme.D(48))); } } // Desni stupac "Trenutno aktivni" na stranici prijatelja class ActiveNow : Control { readonly TrayApp app; List items = new List(); readonly List cards = new List(); int hover = -1; public ActiveNow(TrayApp app) { this.app = app; SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); BackColor = Theme.Bg; } public void SetData(List all) { items = all.Where(f => f.Status == "play" || (f.Voice != null && f.Kind != "off")).OrderBy(Txt.Rank).ThenBy(f => f.Username, StringComparer.OrdinalIgnoreCase).Take(12).ToList(); Invalidate(); } int Hit(Point p) { for (int i = 0; i < cards.Count; i++) if (cards[i].Contains(p)) return i; return -1; } protected override void OnMouseMove(MouseEventArgs e) { int h = Hit(e.Location); if (h != hover) { hover = h; Cursor = h >= 0 ? Cursors.Hand : Cursors.Default; Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseLeave(EventArgs e) { hover = -1; Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseClick(MouseEventArgs e) { int h = Hit(e.Location); if (h >= 0 && h < items.Count) app.OpenChat(items[h]); base.OnMouseClick(e); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.Clear(Theme.Bg); g.SmoothingMode = SmoothingMode.AntiAlias; using (var pen = new Pen(Theme.Line, Theme.D(1))) g.DrawLine(pen, 0, 0, 0, Height); int pad = Theme.D(16); using (var f = Theme.Body(20, FontStyle.Bold)) Txt.Draw(g, "Trenutno aktivni", f, new Rectangle(pad, Theme.D(16), Width - pad * 2, Theme.D(30)), Theme.Text); cards.Clear(); if (items.Count == 0) { using (var f = Theme.Body(15, FontStyle.Bold)) TextRenderer.DrawText(g, "Zasad je tiho...", f, new Rectangle(pad, Theme.D(70), Width - pad * 2, Theme.D(24)), Theme.Text, TextFormatFlags.HorizontalCenter | TextFormatFlags.NoPrefix); using (var f = Theme.Body(13)) TextRenderer.DrawText(g, "Kad prijatelj zaigra ili uđe u voice, vidjet ćeš to ovdje.", f, new Rectangle(pad + Theme.D(10), Theme.D(98), Width - pad * 2 - Theme.D(20), Theme.D(60)), Theme.Muted, TextFormatFlags.HorizontalCenter | TextFormatFlags.WordBreak | TextFormatFlags.NoPrefix); return; } int y = Theme.D(60); for (int i = 0; i < items.Count; i++) { var f = items[i]; var r = new Rectangle(pad, y, Width - pad * 2, Theme.D(68)); if (r.Bottom > Height) break; cards.Add(r); var bg = i == hover ? Theme.Panel2 : Theme.Panel; using (var p = Theme.Round(r, Theme.D(8))) using (var b = new SolidBrush(bg)) g.FillPath(b, p); float av = Theme.D(32); Theme.Avatar(g, new RectangleF(r.X + Theme.D(14), r.Y + (r.Height - av) / 2f, av, av), f.Color, f.Username, f.Avatar, this, f.Kind, bg); int x = r.X + Theme.D(58), rgt = r.Right - Theme.D(12); var img = f.GameIcon != null ? ImageCache.Get(f.GameIcon, this) : null; if (img != null) { var ir = new Rectangle(rgt - Theme.D(36), r.Y + (r.Height - Theme.D(36)) / 2, Theme.D(36), Theme.D(36)); g.DrawImage(Theme.Masked(img, ir.Width, ir.Height, Theme.D(8)), ir.X, ir.Y, ir.Width, ir.Height); rgt = ir.X - Theme.D(8); } using (var fn = Theme.Body(14.5f, FontStyle.Bold)) Txt.Draw(g, f.Username, fn, new Rectangle(x, r.Y + Theme.D(14), Math.Max(10, rgt - x), Theme.D(20)), Theme.Text); using (var fs = Theme.Body(12.5f)) Txt.Draw(g, Txt.Activity(f), fs, new Rectangle(x, r.Y + Theme.D(35), Math.Max(10, rgt - x), Theme.D(18)), Theme.Muted); y += Theme.D(76); } } } // Stranica "Prijatelji": Online / Svi / Na cekanju / Dodaj prijatelja class FriendsPage : UserControl { readonly TrayApp app; readonly PageHeader head = new PageHeader { Glyph = Theme.IcoPeople, Title = "Prijatelji", Tabs = new[] { "Online", "Svi", "Na čekanju" }, ActionText = "Dodaj prijatelja" }; readonly ListBox list = new SmoothList(); readonly Label empty = new Label(); readonly ActiveNow active; readonly Panel addPanel = new Panel(), addField = new Panel(); readonly TextBox addBox = new TextBox(); readonly FlatButton addBtn = new FlatButton("Pošalji zahtjev", true); readonly Label addTitle, addSub, addResult; List all = new List(); List reqs = new List(); List rows = new List(); int requests, hover = -1, hotBtn; bool adding; Point mouse; string lastSig = ""; public FriendsPage(TrayApp app) { this.app = app; BackColor = Theme.Bg; active = new ActiveNow(app); head.TabChanged += t => { adding = false; head.ActionActive = false; Rebuild(); }; head.ActionClicked += () => ShowAdd(); list.DrawMode = DrawMode.OwnerDrawVariable; list.BorderStyle = BorderStyle.None; list.BackColor = Theme.Bg; list.IntegralHeight = false; list.SelectionMode = SelectionMode.None; list.MeasureItem += (s, e) => { e.ItemHeight = e.Index >= 0 && e.Index < rows.Count && rows[e.Index] is string ? Theme.D(44) : Theme.D(62); }; list.DrawItem += DrawRow; list.MouseMove += (s, e) => { mouse = e.Location; int i = list.IndexFromPoint(e.Location); int hb = 0; if (i >= 0 && i < rows.Count && !(rows[i] is string)) { var b = list.GetItemRectangle(i); hb = LeftBtn(b).Contains(e.Location) ? 1 : RightBtn(b).Contains(e.Location) ? 2 : 0; } if (i != hover || hb != hotBtn) { hover = i; hotBtn = hb; list.Cursor = i >= 0 && i < rows.Count && !(rows[i] is string) ? Cursors.Hand : Cursors.Default; list.Invalidate(); } }; list.MouseLeave += (s, e) => { hover = -1; list.Invalidate(); }; list.MouseUp += (s, e) => { int i = list.IndexFromPoint(e.Location); if (i < 0 || i >= rows.Count) return; var b = list.GetItemRectangle(i); var f = rows[i] as Friend; var rq = rows[i] as FriendReq; if (f != null && e.Button == MouseButtons.Right) { var name = f.Username; var menu = Ui.Menu(); menu.Items.Add(new ToolStripMenuItem("Pošalji poruku", null, (a, c) => app.OpenChat(f))); menu.Items.Add(new ToolStripMenuItem("Pozovi", null, (a, c) => app.Call(f))); menu.Items.Add(new ToolStripMenuItem("Otvori profil", null, (a, c) => app.Open("/profile/" + Uri.EscapeDataString(name)))); menu.Items.Add(new ToolStripSeparator()); menu.Items.Add(new ToolStripMenuItem("Ukloni prijatelja", null, (a, c) => app.RemoveFriend(name)) { ForeColor = Theme.Red }); menu.Show(list, e.Location); return; } if (e.Button != MouseButtons.Left) return; if (f != null) { if (LeftBtn(b).Contains(e.Location)) app.Call(f); else app.OpenChat(f); } else if (rq != null) { if (rq.Incoming && LeftBtn(b).Contains(e.Location)) app.AcceptRequest(rq); else if (RightBtn(b).Contains(e.Location)) app.DeleteRequest(rq); else app.Open("/profile/" + Uri.EscapeDataString(rq.Username)); } }; Theme.DarkScroll(list); empty.Font = Theme.Body(15); empty.ForeColor = Theme.Muted; empty.BackColor = Theme.Bg; empty.TextAlign = ContentAlignment.MiddleCenter; // obrazac za dodavanje prijatelja addPanel.BackColor = Theme.Bg; addPanel.Visible = false; addTitle = Ui.Lbl("DODAJ PRIJATELJA", 16, Theme.Text, 0, 0, true); addSub = Ui.Lbl("Prijatelje možeš dodati pomoću njihovog korisničkog imena.", 14, Theme.Muted, 0, 0); addResult = Ui.Lbl("", 13.5f, Theme.On, 0, 0); addField.BackColor = Theme.Bg; addField.Paint += (s, e) => { e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; var rr = new Rectangle(0, 0, addField.Width - 1, addField.Height - 1); using (var p = Theme.Round(rr, Theme.D(8))) { using (var b = new SolidBrush(Theme.Field)) e.Graphics.FillPath(b, p); var bc = addResult.Text.Length == 0 ? (addBox.Focused ? Theme.Link : Theme.Field) : addResult.ForeColor; using (var pen = new Pen(bc, 1.4f)) e.Graphics.DrawPath(pen, p); } }; addBox.BorderStyle = BorderStyle.None; addBox.BackColor = Theme.Field; addBox.ForeColor = Theme.Text; addBox.Font = Theme.Body(15); addBox.MaxLength = 32; Theme.Cue(addBox, "Upiši korisničko ime"); addBox.TextChanged += (s, e) => { addBtn.Enabled = addBox.Text.Trim().Length > 0; if (addResult.Text.Length > 0) { addResult.Text = ""; addField.Invalidate(); } }; addBox.GotFocus += (s, e) => addField.Invalidate(); addBox.LostFocus += (s, e) => addField.Invalidate(); addBox.KeyDown += (s, e) => { if (e.KeyCode == Keys.Enter) { e.SuppressKeyPress = true; SendRequest(); } }; addBtn.Enabled = false; addBtn.Font = Theme.Body(13.5f, FontStyle.Bold); addBtn.Click += (s, e) => SendRequest(); addField.Controls.AddRange(new Control[] { addBox, addBtn }); addPanel.Controls.AddRange(new Control[] { addTitle, addSub, addField, addResult }); Controls.AddRange(new Control[] { head, list, empty, active, addPanel }); Rebuild(); } Rectangle RightBtn(Rectangle b) { int s = Theme.D(36); return new Rectangle(b.Right - Theme.D(10) - s, b.Y + (b.Height - s) / 2, s, s); } Rectangle LeftBtn(Rectangle b) { var m = RightBtn(b); return new Rectangle(m.X - m.Width - Theme.D(10), m.Y, m.Width, m.Height); } protected override void OnResize(EventArgs e) { base.OnResize(e); if (active == null || addResult == null) return; int top = Theme.D(48); head.SetBounds(0, 0, Width, top); bool side = Width >= Theme.D(880); int aw = side ? Theme.D(340) : 0; active.Visible = side; active.SetBounds(Width - aw, top, aw, Math.Max(0, Height - top)); var lr = new Rectangle(Theme.D(20), top + Theme.D(8), Math.Max(10, Width - aw - Theme.D(40)), Math.Max(10, Height - top - Theme.D(8))); list.Bounds = lr; empty.Bounds = lr; addPanel.Bounds = new Rectangle(lr.X, top + Theme.D(12), lr.Width, Theme.D(160)); int w = addPanel.Width; addTitle.Location = new Point(0, Theme.D(8)); addSub.Location = new Point(0, Theme.D(36)); addField.SetBounds(0, Theme.D(68), w, Theme.D(52)); int bw = Theme.D(150); addBtn.SetBounds(w - bw - Theme.D(10), Theme.D(10), bw, Theme.D(32)); addBox.SetBounds(Theme.D(14), (Theme.D(52) - addBox.PreferredHeight) / 2, Math.Max(10, w - bw - Theme.D(40)), addBox.PreferredHeight); addResult.Location = new Point(Theme.D(2), Theme.D(128)); } public void ShowAdd() { adding = true; head.ActionActive = true; head.Invalidate(); Rebuild(); addBox.Focus(); } async void SendRequest() { var name = addBox.Text.Trim(); if (name.Length == 0 || !addBtn.Enabled) return; addBtn.Enabled = false; var res = await app.AddFriend(name); addResult.ForeColor = res.Item1 ? Theme.On : Theme.Red; if (res.Item1) addBox.Text = ""; addResult.Text = res.Item2; addBtn.Enabled = addBox.Text.Trim().Length > 0; addField.Invalidate(); addBox.Focus(); } public void SetData(List f, int req) { all = f; requests = req; Rebuild(); } public void SetRequests(List r) { reqs = r; Rebuild(); } public void Refresh2() { list.Invalidate(); active.Invalidate(); } void Rebuild() { int tab = head.Tab; int incoming = reqs.Count(x => x.Incoming); head.TabBadges = new[] { 0, 0, Math.Max(incoming, requests) }; head.Invalidate(); active.SetData(all); var nr = new List(); if (tab == 2) { if (reqs.Count == 0) empty.Text = "Nema zahtjeva na čekanju.\nKlikni \"Dodaj prijatelja\" gore da nekoga dodaš."; else { nr.Add("NA ČEKANJU — " + reqs.Count); nr.AddRange(reqs.OrderByDescending(x => x.Incoming).ThenBy(x => x.Username, StringComparer.OrdinalIgnoreCase)); } } else { IEnumerable src = tab == 0 ? all.Where(f => f.Kind != "off") : (IEnumerable)all; var lst = src.OrderBy(Txt.Rank).ThenBy(f => f.Username, StringComparer.OrdinalIgnoreCase).ToList(); if (lst.Count == 0) empty.Text = tab == 0 ? "Nitko od prijatelja trenutno nije online." : "Još nemaš prijatelja.\nKlikni \"Dodaj prijatelja\" gore."; else { nr.Add((tab == 0 ? "ONLINE" : "SVI PRIJATELJI") + " — " + lst.Count); nr.AddRange(lst); } } addPanel.Visible = adding; list.Visible = !adding && nr.Count > 0; empty.Visible = !adding && nr.Count == 0; var sig = tab + ":" + string.Join("|", nr.Select(o => o is Friend ? ((Friend)o).Username : o is FriendReq ? "r" + ((FriendReq)o).Id : (string)o).ToArray()); rows = nr; if (sig != lastSig) { lastSig = sig; list.BeginUpdate(); list.Items.Clear(); foreach (var o in nr) list.Items.Add(o); list.EndUpdate(); } else list.Invalidate(); } void RoundBtn(Graphics g, Rectangle btn, bool rowHot, string glyph, string fb, Color hoverColor) { bool over = rowHot && btn.Contains(mouse); using (var fill = new SolidBrush(rowHot ? Theme.Rail : Theme.Panel)) g.FillEllipse(fill, btn); Theme.Icon(g, glyph, fb, btn, over ? hoverColor : Theme.Muted, 15); } void DrawRow(object s, DrawItemEventArgs e) { if (e.Index < 0 || e.Index >= rows.Count) return; var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; var b = e.Bounds; using (var fill = new SolidBrush(Theme.Bg)) g.FillRectangle(fill, b); var label = rows[e.Index] as string; if (label != null) { using (var f = Theme.Body(12, FontStyle.Bold)) Txt.Draw(g, label, f, new Rectangle(b.X + Theme.D(10), b.Y + Theme.D(16), b.Width - Theme.D(20), b.Height - Theme.D(16)), Theme.Muted); return; } bool hot = e.Index == hover; var bg = hot ? Theme.Panel2 : Theme.Bg; if (hot) using (var p = Theme.Round(b, Theme.D(8))) using (var fill = new SolidBrush(bg)) g.FillPath(fill, p); else if (e.Index > 0 && !(rows[e.Index - 1] is string) && e.Index - 1 != hover) using (var pen = new Pen(Theme.Line)) g.DrawLine(pen, b.X + Theme.D(10), b.Y, b.Right - Theme.D(10), b.Y); var fr = rows[e.Index] as Friend; var rq = rows[e.Index] as FriendReq; string name = fr != null ? fr.Username : rq.Username; string sub = fr != null ? (Txt.Activity(fr) ?? Theme.KindText(fr.Kind)) : rq.Incoming ? "Dolazni zahtjev za prijateljstvo" : "Odlazni zahtjev za prijateljstvo"; float av = Theme.D(32); Theme.Avatar(g, new RectangleF(b.X + Theme.D(10), b.Y + (b.Height - av) / 2f, av, av), fr != null ? fr.Color : rq.Color, name, fr != null ? fr.Avatar : rq.Avatar, list, fr != null ? fr.Kind : null, bg); var lb = LeftBtn(b); int x = b.X + Theme.D(54), w = Math.Max(10, lb.X - x - Theme.D(8)); using (var f = Theme.Body(15.5f, FontStyle.Bold)) Txt.Draw(g, name, f, new Rectangle(x, b.Y + Theme.D(10), w, Theme.D(22)), Theme.Text); using (var f = Theme.Body(13)) Txt.Draw(g, sub, f, new Rectangle(x, b.Y + Theme.D(32), w, Theme.D(18)), Theme.Muted); if (fr != null) { RoundBtn(g, lb, hot, Theme.IcoPhone, "T", Theme.On); RoundBtn(g, RightBtn(b), hot, Theme.IcoMsg, "P", Theme.Text); } else { if (rq.Incoming) RoundBtn(g, lb, hot, Theme.IcoCheck, "✓", Theme.On); RoundBtn(g, RightBtn(b), hot, Theme.IcoClose, "✕", Theme.Red); } } } class ChatRow { public ChatMsg M; public bool Head, Intro; public string Date; } // Direktne poruke u glavnom prozoru, u stilu Discorda class ChatView : UserControl { const TextFormatFlags Wrap = TextFormatFlags.WordBreak | TextFormatFlags.TextBoxControl | TextFormatFlags.NoPrefix; readonly TrayApp app; readonly PageHeader head = new PageHeader { TextIcon = "@", RightGlyph = Theme.IcoContact, RightTip = "Otvori profil", Right2Glyph = Theme.IcoPhone, Right2Tip = "Pozovi" }; readonly ListBox list = new SmoothList(); readonly Panel composer = new Panel(); readonly TextBox input = new TextBox(); readonly List msgs = new List(); readonly List rows = new List(); readonly Timer poll = new Timer { Interval = 2500 }; Friend friend; long lastId; bool busy, loaded, intro, overDel; int hover = -1, lastW, gen; public string FriendName { get { return friend != null ? friend.Username : null; } } bool sendHot; Rectangle PillRect() { return new Rectangle(Theme.D(16), Theme.D(6), Math.Max(Theme.D(80), composer.Width - Theme.D(32)), Theme.D(50)); } Rectangle SendRect() { var pr = PillRect(); int s = pr.Height - Theme.D(12); return new Rectangle(pr.Right - s - Theme.D(6), pr.Y + Theme.D(6), s, s); } public ChatView(TrayApp app) { this.app = app; BackColor = Theme.Bg; head.RightClicked += () => { if (friend != null) app.Open("/profile/" + Uri.EscapeDataString(friend.Username)); }; head.Right2Clicked += () => { if (friend != null) app.Call(friend); }; list.DrawMode = DrawMode.OwnerDrawVariable; list.BorderStyle = BorderStyle.None; list.BackColor = Theme.Bg; list.IntegralHeight = false; list.SelectionMode = SelectionMode.None; list.MeasureItem += Measure; list.DrawItem += DrawRow; list.MouseMove += (s, e) => { int i = list.IndexFromPoint(e.Location); bool od = i >= 0 && i < rows.Count && rows[i].M != null && rows[i].M.Mine && DelBtn(list.GetItemRectangle(i), rows[i]).Contains(e.Location); if (i == hover && od == overDel) return; hover = i; overDel = od; list.Cursor = od ? Cursors.Hand : Cursors.Default; list.Invalidate(); }; list.MouseLeave += (s, e) => { hover = -1; overDel = false; list.Invalidate(); }; list.MouseUp += (s, e) => { int i = list.IndexFromPoint(e.Location); if (i < 0 || i >= rows.Count || rows[i].M == null) return; var m = rows[i].M; if (e.Button == MouseButtons.Left) { if (m.Mine && DelBtn(list.GetItemRectangle(i), rows[i]).Contains(e.Location)) Delete(m, (ModifierKeys & Keys.Shift) != 0); return; } if (e.Button != MouseButtons.Right) return; var text = m.Body; var menu = Ui.Menu(); menu.Items.Add(new ToolStripMenuItem("Kopiraj tekst", null, (a, b) => { try { Clipboard.SetText(text); } catch { } })); if (m.Mine) { menu.Items.Add(new ToolStripSeparator()); menu.Items.Add(new ToolStripMenuItem("Obriši poruku", null, (a, b) => Delete(m, false)) { ForeColor = Theme.Red }); } menu.Show(list, e.Location); }; list.Resize += (s, e) => { if (list.ClientSize.Width != lastW) { lastW = list.ClientSize.Width; Refill(); } }; Theme.DarkScroll(list); composer.BackColor = Theme.Bg; composer.Paint += (s, e) => { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; if (Theme.ClassicLayout) { using (var p = Theme.Round(new Rectangle(Theme.D(16), Theme.D(2), composer.Width - Theme.D(32), Theme.D(44)), Theme.D(8))) using (var b = new SolidBrush(Theme.Input)) g.FillPath(b, p); return; } // pilula s obrubom koji zasvijetli dok pises i okrugli gumb za slanje u prijelazu boje var pr = PillRect(); using (var p = Theme.Round(pr, pr.Height / 2f)) { using (var b = new SolidBrush(Theme.Input)) g.FillPath(b, p); if (input.Focused) Fx.GlowBorder(g, p, pr, 1f, 1.6f); else using (var pen = new Pen(Theme.Line)) g.DrawPath(pen, p); } var sr = SendRect(); bool can = input.TextLength > 0; if (can) using (var b = new LinearGradientBrush(new RectangleF(sr.X - 1, sr.Y - 1, sr.Width + 2, sr.Height + 2), Theme.Shade(Theme.Accent, sendHot ? 0.1f : 0f), Theme.Shade(Theme.Accent2, sendHot ? 0.1f : 0f), 45f)) g.FillEllipse(b, sr); else using (var b = new SolidBrush(Fx.Mix(Theme.Input, Color.White, 0.08f))) g.FillEllipse(b, sr); float isz = sr.Width * 0.44f; Vec.Draw(g, "send", new RectangleF(sr.X + (sr.Width - isz) / 2 + Theme.D(1), sr.Y + (sr.Height - isz) / 2, isz, isz), can ? Color.White : Theme.Muted, Theme.D(1.6f)); }; composer.MouseMove += (s, e) => { bool h = SendRect().Contains(e.Location); if (h != sendHot) { sendHot = h; composer.Cursor = h ? Cursors.Hand : Cursors.Default; composer.Invalidate(); } }; composer.MouseLeave += (s, e) => { if (sendHot) { sendHot = false; composer.Invalidate(); } }; composer.MouseClick += (s, e) => { if (!Theme.ClassicLayout && SendRect().Contains(e.Location)) SendMsg(); else input.Focus(); }; input.GotFocus += (s, e) => composer.Invalidate(); input.LostFocus += (s, e) => composer.Invalidate(); input.TextChanged += (s, e) => composer.Invalidate(SendRect()); input.BorderStyle = BorderStyle.None; input.BackColor = Theme.Input; input.ForeColor = Theme.Text2; input.Font = Theme.Body(15); input.MaxLength = 1000; input.KeyDown += (s, e) => { if (e.KeyCode == Keys.Enter) { e.SuppressKeyPress = true; e.Handled = true; SendMsg(); } }; composer.Controls.Add(input); composer.Resize += (s, e) => { if (Theme.ClassicLayout) { input.SetBounds(Theme.D(30), Theme.D(2) + (Theme.D(44) - input.PreferredHeight) / 2, Math.Max(10, composer.Width - Theme.D(60)), input.PreferredHeight); return; } var pr = PillRect(); input.SetBounds(pr.X + Theme.D(20), pr.Y + (pr.Height - input.PreferredHeight) / 2, Math.Max(10, SendRect().X - pr.X - Theme.D(28)), input.PreferredHeight); composer.Invalidate(); }; Controls.AddRange(new Control[] { head, list, composer }); poll.Tick += (s, e) => { if (Visible && friend != null) Fetch(); }; } protected override void OnResize(EventArgs e) { base.OnResize(e); int top = Theme.ClassicLayout ? Theme.D(48) : Theme.D(60), bottom = Theme.ClassicLayout ? Theme.D(70) : Theme.D(72); head.SetBounds(0, 0, Width, top); composer.SetBounds(0, Math.Max(top, Height - bottom), Width, bottom); list.SetBounds(0, top, Width, Math.Max(10, Height - top - bottom)); } public void Open(Friend f) { bool same = friend != null && f != null && friend.Username == f.Username; friend = f; SetHeader(null); if (!same) { gen++; msgs.Clear(); rows.Clear(); list.Items.Clear(); lastId = 0; loaded = false; busy = false; input.Text = ""; Theme.Cue(input, "Poruka @" + f.Username); Fetch(); } poll.Start(); } public void FocusInput() { try { input.Focus(); } catch { } } public void Reset() { friend = null; gen++; poll.Stop(); msgs.Clear(); rows.Clear(); list.Items.Clear(); } public void UpdateFriend(Friend f) { if (friend == null || f.Username != friend.Username) return; friend = f; SetHeader(null); list.Invalidate(); } void SetHeader(string err) { head.Title = friend != null ? friend.Username : ""; head.Kind = friend != null ? friend.Kind : null; if (!Theme.ClassicLayout && friend != null) { head.TextIcon = null; head.AvatarName = friend.Username; head.AvatarColor = friend.Color; head.AvatarUrl = friend.Avatar; } head.Sub = err ?? Txt.Activity(friend); head.SubColor = err != null ? Theme.Red : Theme.Muted; head.Invalidate(); } static ChatMsg Parse(object o) { DateTimeOffset at; var m = new ChatMsg { Id = J.L(o, "id"), Mine = J.B(o, "mine"), Body = J.S(o, "body") ?? "" }; m.At = DateTimeOffset.TryParse(J.S(o, "at"), CultureInfo.InvariantCulture, DateTimeStyles.None, out at) ? at.UtcDateTime : DateTime.UtcNow; return m; } void AddMsg(ChatMsg m, bool live) { if (m.Id <= 0 || msgs.Any(x => x.Id == m.Id)) return; var prev = msgs.Count > 0 ? msgs[msgs.Count - 1] : null; msgs.Add(m); if (m.Id > lastId) lastId = m.Id; var lt = m.At.ToLocalTime(); bool newDay = prev == null || prev.At.ToLocalTime().Date != lt.Date; if (newDay) Push(new ChatRow { Date = Txt.Day(lt) }, live); bool headRow = newDay || prev.Mine != m.Mine || (m.At - prev.At).TotalMinutes > 7; Push(new ChatRow { M = m, Head = headRow }, live); } void Push(ChatRow r, bool live) { rows.Add(r); if (live) list.Items.Add(r); } void Refill() { list.BeginUpdate(); list.Items.Clear(); foreach (var r in rows) list.Items.Add(r); list.EndUpdate(); ScrollEnd(); } void ScrollEnd() { if (list.Items.Count > 0) list.TopIndex = list.Items.Count - 1; } // moderni izgled: poruke u oblacicima (svoje desno u boji naglaska, tudje lijevo); Xfire ostaje popis static bool Bubbles { get { return !Theme.ClassicLayout; } } Rectangle Bubble(Rectangle b, ChatRow r) { bool other = !r.M.Mine; int top = b.Y + (r.Head ? Theme.D(10) : Theme.D(2)) + (r.Head && other ? Theme.D(20) : 0); int maxW = Math.Max(Theme.D(120), Math.Min(b.Width - Theme.D(140), Theme.D(560))); using (var f = Theme.Body(14.5f)) { var sz = TextRenderer.MeasureText(r.M.Body, f, new Size(maxW - Theme.D(28), int.MaxValue), Wrap); int w = Math.Max(sz.Width, Theme.D(44)) + Theme.D(28), h = sz.Height + Theme.D(30); int x = other ? b.X + Theme.D(60) : b.Right - Theme.D(18) - w; return new Rectangle(x, top, w, h); } } Rectangle DelBtn(Rectangle b, ChatRow r) { int s = Theme.D(30); if (Bubbles) { var bb = Bubble(b, r); return new Rectangle(bb.X - s - Theme.D(8), bb.Y + (bb.Height - s) / 2, s, s); } return new Rectangle(b.Right - Theme.D(14) - s, b.Y + Theme.D(2), s, Math.Min(s, b.Height - Theme.D(4))); } // makni poruke i ponovno slozi grupe i datume, a polozaj popisa ostaje isti void RemoveIds(HashSet ids) { if (msgs.RemoveAll(m => ids.Contains(m.Id)) == 0) return; var keep = msgs.ToList(); msgs.Clear(); rows.Clear(); if (intro) rows.Add(new ChatRow { Intro = true }); foreach (var m in keep) AddMsg(m, false); int top = list.Items.Count > 0 ? list.TopIndex : 0; hover = -1; list.BeginUpdate(); list.Items.Clear(); foreach (var r in rows) list.Items.Add(r); list.EndUpdate(); if (list.Items.Count > 0) list.TopIndex = Math.Min(top, list.Items.Count - 1); } async void Delete(ChatMsg m, bool noAsk) { if (!noAsk && !Dialog.Confirm(this, "Obriši poruku", "Jesi li siguran da želiš obrisati ovu poruku? Nestat će i kod " + (friend != null ? friend.Username : "prijatelja") + ".", "Obriši", true, m.Body.Length > 300 ? m.Body.Substring(0, 300) + "..." : m.Body, "Savjet: Shift + klik na kantu briše bez pitanja.")) return; try { await Task.Run(() => app.ApiDel("/client/chat/msg/" + m.Id)); RemoveIds(new HashSet { m.Id }); } catch (WebException ex) { if (Api.Status(ex) == 404) RemoveIds(new HashSet { m.Id }); else SetHeader(Api.ErrorMessage(ex)); } catch (Exception ex) { SetHeader(ex.Message); } } async void Fetch() { if (busy || friend == null) return; busy = true; int my = gen; var name = friend.Username; long after = lastId; try { var res = await Task.Run(() => app.ApiGet("/client/chat/" + Uri.EscapeDataString(name) + "?after=" + after)); if (my != gen) return; var o = J.Parse(res); var got = J.A(o, "messages").Select(Parse).ToList(); if (!loaded) { loaded = true; rows.Clear(); msgs.Clear(); intro = got.Count < 80; if (intro) rows.Add(new ChatRow { Intro = true }); foreach (var m in got) AddMsg(m, false); Refill(); } else { int before = rows.Count; foreach (var m in got) AddMsg(m, true); if (rows.Count != before) ScrollEnd(); } // poruke koje je netko obrisao nestaju i ovdje var gone = new HashSet(J.A(o, "deleted").Select(x => Convert.ToInt64(x))); if (gone.Count > 0) RemoveIds(gone); } catch (WebException ex) { if (my == gen && Api.Status(ex) == 403) { poll.Stop(); SetHeader(Api.ErrorMessage(ex)); } } catch { } finally { if (my == gen) busy = false; } } async void SendMsg() { var v = input.Text.Trim(); if (v.Length == 0 || friend == null) return; var name = friend.Username; int my = gen; input.Text = ""; try { var res = await Task.Run(() => app.ApiPost("/client/chat/" + Uri.EscapeDataString(name), new Dictionary { { "body", v } })); if (my != gen) return; AddMsg(Parse(J.Parse(res)), true); ScrollEnd(); } catch (WebException ex) { if (my == gen) { input.Text = v; SetHeader(Api.ErrorMessage(ex)); } } catch (Exception ex) { if (my == gen) { input.Text = v; SetHeader(ex.Message); } } finally { FocusInput(); } } int TextW() { return Math.Max(60, list.ClientSize.Width - Theme.D(72) - Theme.D(24)); } void Measure(object s, MeasureItemEventArgs e) { if (e.Index < 0 || e.Index >= rows.Count) return; var r = rows[e.Index]; if (r.Intro) { e.ItemHeight = Theme.D(160); return; } if (r.Date != null) { e.ItemHeight = Theme.D(40); return; } if (Bubbles) { var bb = Bubble(new Rectangle(0, 0, list.ClientSize.Width, 1000), r); e.ItemHeight = Math.Min(255, bb.Bottom + Theme.D(4)); return; } using (var f = Theme.Body(15)) { var sz = TextRenderer.MeasureText(r.M.Body, f, new Size(TextW(), int.MaxValue), Wrap); int h = r.Head ? sz.Height + Theme.D(42) : sz.Height + Theme.D(4); e.ItemHeight = Math.Min(255, Math.Max(h, r.Head ? Theme.D(56) : Theme.D(24))); } } void DrawRow(object s, DrawItemEventArgs e) { if (e.Index < 0 || e.Index >= rows.Count) return; var r = rows[e.Index]; var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; var b = e.Bounds; bool hot = e.Index == hover && r.M != null; var bg = hot && !Bubbles ? Theme.MsgHover : Theme.Bg; using (var fill = new SolidBrush(bg)) g.FillRectangle(fill, b); if (friend == null) return; if (r.Intro) { float av = Theme.D(72); Theme.Avatar(g, new RectangleF(b.X + Theme.D(16), b.Y + Theme.D(16), av, av), friend.Color, friend.Username, friend.Avatar, list, null, bg); using (var f = Theme.Body(26, FontStyle.Bold)) Txt.Draw(g, friend.Username, f, new Rectangle(b.X + Theme.D(16), b.Y + Theme.D(94), b.Width - Theme.D(32), Theme.D(36)), Theme.Text); using (var f = Theme.Body(14)) Txt.Draw(g, "Ovo je početak tvoje povijesti izravnih poruka s @" + friend.Username + ".", f, new Rectangle(b.X + Theme.D(16), b.Y + Theme.D(130), b.Width - Theme.D(32), Theme.D(22)), Theme.Muted); return; } if (r.Date != null) { int cy = b.Y + b.Height / 2; using (var pen = new Pen(Theme.Line)) g.DrawLine(pen, b.X + Theme.D(16), cy, b.Right - Theme.D(16), cy); using (var f = Theme.Body(12, FontStyle.Bold)) { var sz = TextRenderer.MeasureText(r.Date, f); var tr = new Rectangle(b.X + (b.Width - sz.Width) / 2 - Theme.D(4), b.Y, sz.Width + Theme.D(8), b.Height); using (var fill = new SolidBrush(Theme.Bg)) g.FillRectangle(fill, tr.X, cy - Theme.D(8), tr.Width, Theme.D(16)); TextRenderer.DrawText(g, r.Date, f, tr, Theme.Muted, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPrefix); } return; } var m = r.M; int tx = b.X + Theme.D(72), tw = TextW(); var local = m.At.ToLocalTime(); if (Bubbles) { using (var fill = new SolidBrush(Theme.Bg)) g.FillRectangle(fill, b); var bb = Bubble(b, r); if (!m.Mine && r.Head) { float av = Theme.D(34); Theme.Avatar(g, new RectangleF(b.X + Theme.D(16), bb.Y, av, av), friend.Color, friend.Username, friend.Avatar, list, null, Theme.Bg); using (var f = Theme.Display(13)) Txt.Draw(g, friend.Username + " " + Txt.When(local), f, new Rectangle(bb.X + Theme.D(4), b.Y + Theme.D(8), b.Width - bb.X - Theme.D(20), Theme.D(18)), Theme.Muted); } using (var p = Theme.Round(bb, Math.Min(Theme.D(20), bb.Height / 2f))) { if (m.Mine) { using (var fill = new LinearGradientBrush(new Rectangle(bb.X - 1, bb.Y - 1, bb.Width + 2, bb.Height + 2), Theme.Accent, Theme.Accent2, 20f)) g.FillPath(fill, p); Fx.Gloss(g, p, bb, 26); } else { using (var fill = new SolidBrush(Fx.Mix(Theme.Bg, Color.White, hot ? 0.11f : 0.07f))) g.FillPath(fill, p); using (var pen = new Pen(Fx.Mix(Theme.Bg, Color.White, 0.1f))) g.DrawPath(pen, p); } } var tc = m.Mine ? Color.White : Theme.Text; using (var f = Theme.Body(14.5f)) TextRenderer.DrawText(g, m.Body, f, new Rectangle(bb.X + Theme.D(14), bb.Y + Theme.D(8), bb.Width - Theme.D(28), bb.Height - Theme.D(26)), tc, Wrap | TextFormatFlags.EndEllipsis); using (var f = Theme.Body(11)) TextRenderer.DrawText(g, local.ToString("HH:mm"), f, new Rectangle(bb.X + Theme.D(10), bb.Bottom - Theme.D(18), bb.Width - Theme.D(22), Theme.D(14)), m.Mine ? Color.FromArgb(200, 255, 255, 255) : Theme.Muted, TextFormatFlags.Right | TextFormatFlags.NoPadding); if (hot && m.Mine) { var dbb = DelBtn(b, r); using (var fill = new SolidBrush(overDel ? Theme.Red : Theme.Panel2)) g.FillEllipse(fill, dbb); float isz = dbb.Width * 0.5f; Vec.Draw(g, "trash", new RectangleF(dbb.X + (dbb.Width - isz) / 2, dbb.Y + (dbb.Height - isz) / 2, isz, isz), overDel ? Color.White : Theme.Text2, Math.Max(1.3f, Theme.D(1.4f))); } return; } if (r.Head) { string who = m.Mine ? (app.Username ?? "Ti") : friend.Username; string col = m.Mine ? app.ColorHex : friend.Color; string url = m.Mine ? app.AvatarUrl : friend.Avatar; float av = Theme.D(40); Theme.Avatar(g, new RectangleF(b.X + Theme.D(16), b.Y + Theme.D(10), av, av), col, who, url, list, null, bg); using (var f = Theme.Body(15.5f, FontStyle.Bold)) { var nsz = TextRenderer.MeasureText(who, f, Size.Empty, TextFormatFlags.NoPadding); Txt.Draw(g, who, f, new Rectangle(tx, b.Y + Theme.D(9), Math.Min(nsz.Width + Theme.D(2), tw), Theme.D(22)), Theme.Text); using (var ft = Theme.Body(12)) Txt.Draw(g, Txt.When(local), ft, new Rectangle(tx + nsz.Width + Theme.D(8), b.Y + Theme.D(11), Math.Max(10, tw - nsz.Width - Theme.D(8)), Theme.D(20)), Theme.Muted); } using (var f = Theme.Body(15)) TextRenderer.DrawText(g, m.Body, f, new Rectangle(tx, b.Y + Theme.D(34), tw, Math.Max(10, b.Height - Theme.D(36))), Theme.Text2, Wrap | TextFormatFlags.EndEllipsis); } else { if (hot) using (var f = Theme.Body(11)) TextRenderer.DrawText(g, local.ToString("HH:mm"), f, new Rectangle(b.X + Theme.D(8), b.Y + Theme.D(3), Theme.D(56), Theme.D(18)), Theme.Muted, TextFormatFlags.HorizontalCenter | TextFormatFlags.NoPadding); using (var f = Theme.Body(15)) TextRenderer.DrawText(g, m.Body, f, new Rectangle(tx, b.Y + Theme.D(2), tw, Math.Max(10, b.Height - Theme.D(2))), Theme.Text2, Wrap | TextFormatFlags.EndEllipsis); } // kanta za brisanje na vlastitim porukama (kao Discordova traka s radnjama) if (hot && m.Mine) { var db = DelBtn(b, r); using (var p = Theme.Round(db, Theme.D(6))) { using (var fill = new SolidBrush(overDel ? Theme.Red : Theme.Panel)) g.FillPath(fill, p); using (var pen = new Pen(overDel ? Theme.Red : Theme.Line)) g.DrawPath(pen, p); } Theme.Icon(g, "\uE74D", "x", db, overDel ? Color.White : Theme.Muted, 14); } } } // Okrugli gumb s ikonom (mikrofon, zvuk, izlaz) na voice stranici class RoundButton : Control { public string Glyph, Fallback; public bool Off, Danger, Good; bool hot; public RoundButton(string glyph, string fallback, string tip) { Glyph = glyph; Fallback = fallback; SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.SupportsTransparentBackColor, true); BackColor = Theme.Bg; Cursor = Cursors.Hand; new ToolTip().SetToolTip(this, tip); } protected override void OnMouseEnter(EventArgs e) { hot = true; Invalidate(); base.OnMouseEnter(e); } protected override void OnMouseLeave(EventArgs e) { hot = false; Invalidate(); base.OnMouseLeave(e); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.Clear(Parent != null ? Parent.BackColor : Theme.Bg); g.SmoothingMode = SmoothingMode.AntiAlias; var r = new Rectangle(0, 0, Width - 1, Height - 1); Color bg = Danger ? Theme.Red : Good ? Theme.On : Off ? Theme.Text : Theme.Secondary; if (hot) bg = Danger || Good || Off ? Theme.Shade(bg, -0.12f) : Theme.Shade(bg, 0.1f); using (var b = new SolidBrush(bg)) g.FillEllipse(b, r); var fc = Off && !Danger ? Theme.Bg : Color.White; Theme.Icon(g, Glyph, Fallback, r, fc, 20); if (Off && !Danger) Theme.Slash(g, new Rectangle(r.X + r.Width / 4, r.Y + r.Height / 4, r.Width / 2, r.Height / 2), bg); } } // Plocice sudionika u voice sobi class VoiceStage : Control { readonly TrayApp app; public List Members = new List(); readonly List tiles = new List(); public VoiceStage(TrayApp app) { this.app = app; SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); BackColor = Theme.Stage; } protected override void OnMouseUp(MouseEventArgs e) { if (e.Button == MouseButtons.Right) for (int i = 0; i < tiles.Count && i < Members.Count; i++) if (tiles[i].Contains(e.Location)) { VoiceView.ShowMemberMenu(app, Members[i], this, e.Location); break; } base.OnMouseUp(e); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.Clear(Theme.Stage); if (Theme.Animated) Anim.Fill(g, ClientRectangle, 0.8f, 0.2f, 35f); g.SmoothingMode = SmoothingMode.AntiAlias; tiles.Clear(); int n = Members.Count; if (n == 0) { using (var f = Theme.Body(22, FontStyle.Bold)) TextRenderer.DrawText(g, "Nisi u voice sobi", f, new Rectangle(0, Height / 2 - Theme.D(40), Width, Theme.D(34)), Theme.Text, TextFormatFlags.HorizontalCenter | TextFormatFlags.NoPrefix); using (var f = Theme.Body(14)) TextRenderer.DrawText(g, "Odaberi sobu s lijeve strane da uđeš.", f, new Rectangle(0, Height / 2, Width, Theme.D(24)), Theme.Muted, TextFormatFlags.HorizontalCenter | TextFormatFlags.NoPrefix); return; } int cols = n == 1 ? 1 : n <= 4 ? 2 : n <= 9 ? 3 : 4; int rowsN = (n + cols - 1) / cols; int pad = Theme.D(16), gap = Theme.D(8); float tw = (Width - pad * 2 - gap * (cols - 1)) / (float)cols; float th = (Height - pad * 2 - gap * (rowsN - 1)) / (float)rowsN; th = Math.Min(th, tw * 9f / 16f); tw = Math.Min(tw, th * 16f / 9f); if (tw < 20 || th < 20) return; float gw = tw * cols + gap * (cols - 1), gh = th * rowsN + gap * (rowsN - 1); float ox = (Width - gw) / 2f, oy = (Height - gh) / 2f; for (int i = 0; i < n; i++) { var m = Members[i]; int c = i % cols, rI = i / cols; int inRow = Math.Min(cols, n - rI * cols); float rowOx = ox + (cols - inRow) * (tw + gap) / 2f; var r = new RectangleF(rowOx + c * (tw + gap), oy + rI * (th + gap), tw, th); tiles.Add(Rectangle.Round(r)); var bg = Theme.Shade(Theme.Hex(m.Color, Theme.Panel3), -0.55f); using (var p = Theme.Round(r, Theme.D(8))) using (var b = new SolidBrush(bg)) g.FillPath(b, p); float av = Math.Min(th * 0.42f, Theme.D(96)); var ar = new RectangleF(r.X + (r.Width - av) / 2f, r.Y + (r.Height - av) / 2f, av, av); Theme.Avatar(g, ar, m.Color, (m.Name ?? "?").Replace(" (ti)", ""), m.Avatar, this, null, bg); if (m.Speaking) { var rr = ar; rr.Inflate(Theme.D(3), Theme.D(3)); using (var pen = new Pen(Theme.On, Theme.D(3))) g.DrawEllipse(pen, rr); using (var p = Theme.Round(new RectangleF(r.X + 1, r.Y + 1, r.Width - 2, r.Height - 2), Theme.D(8))) using (var pen = new Pen(Theme.On, Theme.D(2))) g.DrawPath(pen, p); } bool localMuted = m.Name != null && !m.Name.EndsWith(" (ti)") && AudioCfg.UserMuted(m.Name); using (var f = Theme.Body(13, FontStyle.Bold)) { var name = m.Name ?? "?"; var sz = TextRenderer.MeasureText(name, f); int extra = localMuted ? Theme.D(22) : 0; var nr = new RectangleF(r.X + Theme.D(8), r.Bottom - Theme.D(32), Math.Min(sz.Width + Theme.D(12) + extra, r.Width - Theme.D(16)), Theme.D(24)); using (var p = Theme.Round(nr, Theme.D(4))) using (var b = new SolidBrush(Color.FromArgb(160, 0, 0, 0))) g.FillPath(b, p); if (localMuted) { var ir = new Rectangle((int)nr.X + Theme.D(4), (int)nr.Y + Theme.D(3), Theme.D(18), Theme.D(18)); Theme.Icon(g, Theme.IcoSpeaker, "x", ir, Theme.Text2, 12); Theme.Slash(g, ir, Color.Black); } Txt.Draw(g, name, f, new Rectangle((int)nr.X + Theme.D(6) + extra, (int)nr.Y, (int)nr.Width - Theme.D(10) - extra, (int)nr.Height), Color.White); } } } } // Voice stranica: sudionici i kontrole (mikrofon, zvuk, izlaz, pritisni-za-pricanje, osjetljivost) class VoicePage : UserControl { readonly TrayApp app; readonly VoiceView rooms; readonly PageHeader head = new PageHeader { Glyph = Theme.IcoSpeaker, Title = "Voice" }; readonly VoiceStage stage; readonly Panel bottom = new Panel(); readonly RoundButton mic = new RoundButton(Theme.IcoMic, "M", "Mikrofon"), deaf = new RoundButton(Theme.IcoHead, "Z", "Zvuk"), leave = new RoundButton(Theme.IcoHangup, "X", "Izađi iz sobe") { Danger = true }; readonly Toggle ptt = new Toggle(); readonly DropDown key = new DropDown(); readonly RangeSlider meter = new RangeSlider { Min = -80, Max = 0, Format = v => (int)Math.Round(v) + " dB" }; readonly Label pttLbl, meterLbl; readonly FlatButton adv = new FlatButton("Napredno", false); readonly FlatButton newRoom = new FlatButton("+ Napravi novu sobu", true); readonly Timer tick = new Timer { Interval = 80 }; bool syncing; string lastHead = ""; TrayApp App2 { get { return app; } } public VoicePage(TrayApp app, VoiceView rooms) { this.app = app; this.rooms = rooms; BackColor = Theme.Bg; stage = new VoiceStage(app); bottom.BackColor = Theme.Bg; mic.Click += (s, e) => app.ToggleMute(); deaf.Click += (s, e) => app.ToggleDeaf(); leave.Click += (s, e) => app.LeaveVoice(); pttLbl = Ui.Lbl("Pritisni za pričanje", 13.5f, Theme.Text2, 0, 0); meterLbl = Ui.Lbl("", 12, Theme.Muted, 0, 0); Ui.Combo(key); foreach (var k in VoiceView.PttKeys) key.Items.Add(k.Name); adv.Font = Theme.Body(13, FontStyle.Bold); adv.Click += (s, e) => app.ShowAudioSettings(); bottom.Controls.AddRange(new Control[] { mic, deaf, leave, pttLbl, ptt, key, meter, adv, meterLbl }); syncing = true; ptt.Checked = AudioCfg.Ptt; int idx = Array.FindIndex(VoiceView.PttKeys, k => k.Vk == AudioCfg.PttKey); key.SelectedIndex = idx >= 0 ? idx : 0; key.Enabled = AudioCfg.Ptt; meter.Value = AudioCfg.SensDb; syncing = false; ptt.CheckedChanged += (s, e) => { AudioCfg.Ptt = ptt.Checked; key.Enabled = ptt.Checked; }; key.SelectedIndexChanged += (s, e) => { if (!syncing && key.SelectedIndex >= 0) AudioCfg.PttKey = VoiceView.PttKeys[key.SelectedIndex].Vk; }; meter.ValueChanged += (s, e) => { if (!syncing) AudioCfg.SensDb = meter.Value; }; newRoom.Click += (s, e) => app.ShowCreateRoom(); stage.Controls.Add(newRoom); Controls.AddRange(new Control[] { head, stage, bottom }); tick.Tick += (s, e) => Tick(); tick.Start(); } protected override void OnResize(EventArgs e) { base.OnResize(e); if (stage == null || pttLbl == null || meterLbl == null) return; bool inRoom = App2 != null && App2.Voice != null; int top = Theme.D(48), bh = inRoom ? Theme.D(150) : Theme.D(84); head.SetBounds(0, 0, Width, top); stage.SetBounds(0, top, Width, Math.Max(10, Height - top - bh)); newRoom.SetBounds((stage.Width - Theme.D(220)) / 2, stage.Height / 2 + Theme.D(40), Theme.D(220), Theme.D(40)); bottom.SetBounds(0, Math.Max(top, Height - bh), Width, bh); int bs = Theme.D(52), gap = Theme.D(16), bx = (Width - bs * 3 - gap * 2) / 2; mic.SetBounds(bx, Theme.D(14), bs, bs); deaf.SetBounds(bx + bs + gap, Theme.D(14), bs, bs); leave.SetBounds(bx + (bs + gap) * 2, Theme.D(14), bs, bs); int y = inRoom ? Theme.D(84) : Theme.D(18), pad = Theme.D(20), x = pad; pttLbl.Location = new Point(x, y + Theme.D(4)); x += pttLbl.PreferredWidth + Theme.D(10); ptt.Location = new Point(x, y + Theme.D(2)); x += ptt.Width + Theme.D(12); key.SetBounds(x, y, Theme.D(110), Theme.D(28)); x += Theme.D(122); int aw = Theme.D(100); adv.SetBounds(Width - pad - aw, y - Theme.D(2), aw, Theme.D(32)); meter.SetBounds(x, y - Theme.D(2), Math.Max(Theme.D(60), adv.Left - x - Theme.D(12)), Theme.D(32)); meterLbl.Location = new Point(x + Theme.D(8), y + Theme.D(34)); } protected override void OnVisibleChanged(EventArgs e) { if (Visible) { syncing = true; ptt.Checked = AudioCfg.Ptt; key.Enabled = AudioCfg.Ptt; int idx = Array.FindIndex(VoiceView.PttKeys, k => k.Vk == AudioCfg.PttKey); if (idx >= 0) key.SelectedIndex = idx; syncing = false; } base.OnVisibleChanged(e); } List Members(VoiceClient vc) { var known = new Dictionary(); foreach (var rm in rooms.Rooms) foreach (var m in rm.Members) if (m.Name != null) known[m.Name] = m; var res = new List { new VMember { Name = (app.Username ?? "Ti") + " (ti)", Color = app.ColorHex, Avatar = app.AvatarUrl, Speaking = vc.Talking } }; foreach (var m in vc.LiveMembers()) { VMember k; if (known.TryGetValue(m.Name ?? "", out k)) { m.Color = k.Color; m.Avatar = k.Avatar; } res.Add(m); } return res; } void Tick() { if (!Visible) return; var vc = app.Voice; string title = vc != null ? vc.Label : "Voice"; string sub = vc == null ? null : vc.Deafened ? "zvuk isključen" : vc.Muted ? "mikrofon isključen" : vc.Talking ? "pričaš" : "spojen"; if (title + "|" + sub != lastHead) { lastHead = title + "|" + sub; head.Title = title; head.Sub = sub; head.SubColor = vc != null && !vc.Muted && !vc.Deafened ? Theme.On : Theme.Muted; head.Invalidate(); } stage.Members = vc != null ? Members(vc) : new List(); stage.Invalidate(); bool on = vc != null; if (mic.Visible != on) { mic.Visible = on; deaf.Visible = on; leave.Visible = on; newRoom.Visible = !on; OnResize(EventArgs.Empty); } if (on && (mic.Off != vc.Muted || deaf.Off != vc.Deafened)) { mic.Off = vc.Muted; deaf.Off = vc.Deafened; mic.Invalidate(); deaf.Invalidate(); } var eng = vc != null ? vc.Audio : null; meter.Locked = AudioCfg.AutoSens || AudioCfg.Ptt; meter.Level = eng != null ? eng.LevelDb : float.NaN; meter.Marker = eng != null ? eng.ThresholdDb : float.NaN; if (!meter.Dragging) meter.Value = AudioCfg.SensDb; meter.Invalidate(); string t; if (AudioCfg.Ptt) t = "Drži " + Ui.KeyName(AudioCfg.PttKey) + " dok pričaš"; else if (AudioCfg.AutoSens) t = "Osjetljivost: automatski" + (eng != null ? " (" + (int)eng.ThresholdDb + " dB)" : ""); else t = "Osjetljivost: " + (int)AudioCfg.SensDb + " dB (povuci kružić)"; if (eng == null) t += " · razina se vidi u sobi"; if (meterLbl.Text != t) meterLbl.Text = t; } } // Zajednicka osnova za oba rasporeda (Discord i Xfire); TrayApp radi samo s ovim abstract class ShellForm : DarkForm { protected readonly TrayApp App; public bool AllowClose; protected ShellForm(TrayApp app) { App = app; } public abstract UserPanel User { get; } public abstract VoiceView Voice { get; } public abstract void SetFriends(List list, int requests); public abstract void SetRequests(List r); public abstract void SetClans(List clans); public abstract void SetShot(string game, string url); public abstract void ShowUpdate(string text); public abstract void SetUpdateText(string text, bool busy); public abstract void OpenChat(Friend f); public abstract bool ChatActive(string u); public abstract void CloseChat(string user); public abstract void ShowVoice(); public abstract void ShowAddFriend(); public abstract void ShowSettings(); // Obavijesti (komentari/lajkovi/spomeni). Moderni izgled prikazuje karticu; // klasicni (Xfire) ih pokriva toastovima i tray badgeom, pa su ovdje prazne. public virtual void SetNotifs(int unread, List items) { } public virtual void ShowNotifs() { } public virtual void SetLfg(List items) { } public virtual void ShowLfg() { } public virtual void ShowServers() { } protected void StatusMenu(Control anchor, Point at) { ProfilePopout.Show(App, anchor, at, anchor.Top > anchor.Parent.Height / 2); } } // Traka na vrhu prozora kad postoji nova verzija aplikacije (kao Discordova obavijest) class UpdateBar : Control { public string Message = ""; public bool Busy; int hot; public event Action UpdateClicked, Closed; public UpdateBar() { SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); } bool Narrow { get { return Height > Theme.D(44); } } Rectangle Btn() { using (var f = Theme.Body(13.5f, FontStyle.Bold)) { var sz = TextRenderer.MeasureText("Ažuriraj sada", f); int w = sz.Width + Theme.D(24), msgW; if (Narrow) return new Rectangle((Width - w) / 2, Height - Theme.D(28), w, Theme.D(24)); using (var fm = Theme.Body(14)) msgW = TextRenderer.MeasureText(Message, fm).Width; int x = Math.Min((Width + msgW) / 2 + Theme.D(14), Width - Theme.D(48) - w); return new Rectangle(x, (Height - Theme.D(26)) / 2, w, Theme.D(26)); } } Rectangle CloseBtn() { int s = Theme.D(24); return new Rectangle(Width - Theme.D(8) - s, Narrow ? Theme.D(4) : (Height - s) / 2, s, s); } protected override void OnMouseMove(MouseEventArgs e) { int h = !Busy && Btn().Contains(e.Location) ? 1 : CloseBtn().Contains(e.Location) ? 2 : 0; if (h != hot) { hot = h; Cursor = h > 0 ? Cursors.Hand : Cursors.Default; Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseLeave(EventArgs e) { hot = 0; Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseClick(MouseEventArgs e) { if (!Busy && Btn().Contains(e.Location) && UpdateClicked != null) UpdateClicked(); else if (CloseBtn().Contains(e.Location) && Closed != null) Closed(); base.OnMouseClick(e); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(Theme.Accent); var btn = Btn(); int msgRight = Busy ? Width - Theme.D(48) : btn.X - Theme.D(12); using (var f = Theme.Body(Narrow ? 12.5f : 14)) { if (Narrow) TextRenderer.DrawText(g, Message, f, new Rectangle(Theme.D(10), Theme.D(4), Width - Theme.D(44), Busy ? Height - Theme.D(8) : Theme.D(20)), Color.White, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix); else { int msgW = TextRenderer.MeasureText(Message, f).Width; int x = Busy ? (Width - msgW) / 2 : Math.Max(Theme.D(16), (Width - msgW) / 2 - (btn.Width + Theme.D(14)) / 2); Txt.Draw(g, Message, f, new Rectangle(x, 0, Math.Max(10, msgRight - x), Height), Color.White); } } if (!Busy) { using (var p = Theme.Round(btn, Theme.D(4))) { if (hot == 1) using (var b = new SolidBrush(Color.FromArgb(40, 255, 255, 255))) g.FillPath(b, p); using (var pen = new Pen(Color.White, Theme.D(1.5f))) g.DrawPath(pen, p); } using (var f = Theme.Body(13.5f, FontStyle.Bold)) TextRenderer.DrawText(g, "Ažuriraj sada", f, btn, Color.White, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding); } Theme.Icon(g, Theme.IcoClose, "x", CloseBtn(), hot == 2 ? Color.White : Color.FromArgb(200, 255, 255, 255), 12); } } // Iskocni profil (klik na svoje ime): veliki avatar, statusi i radnje, kao na Discordu class ProfilePopout : Control { class Item { public string Kind, Key, Title, Desc, Glyph; public bool Danger; public Action Run; public Rectangle R; } readonly TrayApp app; readonly List items = new List(); readonly List cards = new List(); int hover = -1; public ToolStripDropDown Host; public ProfilePopout(TrayApp app) { this.app = app; SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); Cursor = Cursors.Hand; Status("online", "Online", null); Status("idle", "Odsutan", "Izgledaš kao da nisi za računalom."); Status("dnd", "Ne smetaj", "Bez obavijesti i zvukova za poruke."); Status("invisible", "Nevidljiv", "Prijateljima izgledaš offline, a sve i dalje radi."); items.Add(new Item { Kind = "sep" }); Action("Promijeni profilnu sliku", "", () => app.ChangeAvatar(), false); if (app.AvatarUrl != null) Action("Ukloni profilnu sliku", "", () => app.RemoveAvatar(), false); Action("Moj profil na webu", Theme.IcoGlobe, () => app.OpenProfile(), false); items.Add(new Item { Kind = "sep" }); Action("Odjavi se", "", () => app.LogoutAndAsk(), true); Size = new Size(Theme.D(300), Measure()); } void Status(string key, string title, string desc) { items.Add(new Item { Kind = "status", Key = key, Title = title, Desc = desc, Run = () => app.SetPresence(key) }); } void Action(string title, string glyph, Action run, bool danger) { items.Add(new Item { Kind = "action", Title = title, Glyph = glyph, Run = run, Danger = danger }); } // rasporedi redove u kartice (odvojene sa "sep") i vrati ukupnu visinu int Measure() { int W = Theme.D(300), pad = Theme.D(12), y = Theme.D(156); cards.Clear(); int cardTop = y + Theme.D(6); y = cardTop + Theme.D(6); foreach (var it in items) { if (it.Kind == "sep") { cards.Add(new Rectangle(pad, cardTop, W - pad * 2, y + Theme.D(6) - cardTop)); cardTop = y + Theme.D(14); y = cardTop + Theme.D(6); continue; } int h = it.Kind == "status" ? (it.Desc != null ? Theme.D(50) : Theme.D(38)) : Theme.D(38); it.R = new Rectangle(pad + Theme.D(6), y, W - pad * 2 - Theme.D(12), h); y += h; } cards.Add(new Rectangle(pad, cardTop, W - pad * 2, y + Theme.D(6) - cardTop)); return y + Theme.D(6) + pad; } int Hit(Point p) { for (int i = 0; i < items.Count; i++) if (items[i].Kind != "sep" && items[i].R.Contains(p)) return i; return -1; } protected override void OnMouseMove(MouseEventArgs e) { int h = Hit(e.Location); if (h != hover) { hover = h; Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseLeave(EventArgs e) { hover = -1; Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseClick(MouseEventArgs e) { int h = Hit(e.Location); if (h < 0) return; if (Host != null) Host.Close(); items[h].Run(); } string Kind() { var p = app.Presence; return p == "invisible" ? "off" : p == "dnd" ? "dnd" : p == "idle" || app.AutoIdle ? "idle" : "online"; } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(Theme.Menu); int W = Width; float av = Theme.D(80), ax = Theme.D(16), ay = Theme.D(20); var kind = Kind(); if (Theme.ClassicLayout) { // klasicni (Xfire) izgled: sjajna plava traka i kvadratni avatar Xf.Gloss(g, new Rectangle(0, 0, W, Theme.D(62)), Xf.Head1, Xf.Head2); var ar = new Rectangle((int)ax, (int)ay, (int)av, (int)av); using (var b = new SolidBrush(Theme.Menu)) g.FillRectangle(b, ar.X - Theme.D(4), ar.Y - Theme.D(4), ar.Width + Theme.D(8), ar.Height + Theme.D(8)); Xf.Avatar(g, ar, app.ColorHex, app.Username, app.AvatarUrl, this, kind, Theme.Menu); } else { // traka u boji, gornji kutovi prate zaobljenje prozora using (var b = new LinearGradientBrush(new Rectangle(0, 0, W, Theme.D(62)), Theme.Shade(Theme.Accent, -0.05f), Theme.Shade(Theme.Hex(app.ColorHex, Theme.Accent), -0.3f), 0f)) g.FillRectangle(b, 0, 0, W, Theme.D(62)); using (var b = new SolidBrush(Theme.Menu)) g.FillEllipse(b, ax - Theme.D(6), ay - Theme.D(6), av + Theme.D(12), av + Theme.D(12)); Theme.Avatar(g, new RectangleF(ax, ay, av, av), app.ColorHex, app.Username, app.AvatarUrl, this, kind, Theme.Menu); } using (var f = Theme.ClassicLayout ? Xf.Font(19, FontStyle.Bold) : Theme.Body(20, FontStyle.Bold)) Txt.Draw(g, app.Username ?? "", f, new Rectangle(Theme.D(16), Theme.D(108), W - Theme.D(32), Theme.D(28)), Theme.Text); using (var f = Theme.Body(13)) Txt.Draw(g, Theme.KindText(app.Presence == "invisible" ? "invisible" : kind), f, new Rectangle(Theme.D(16), Theme.D(134), W - Theme.D(32), Theme.D(18)), Theme.Muted); foreach (var c in cards) using (var p = Theme.Round(c, Theme.D(8))) using (var b = new SolidBrush(Theme.Panel)) g.FillPath(b, p); for (int i = 0; i < items.Count; i++) { var it = items[i]; if (it.Kind == "sep") continue; bool hot = i == hover; var r = it.R; if (hot) using (var p = Theme.Round(r, Theme.D(4))) using (var b = new SolidBrush(it.Danger ? Theme.Red : Theme.Accent)) g.FillPath(b, p); var tc = hot ? Color.White : it.Danger ? Theme.Red : Theme.Text2; if (it.Kind == "status") { float ds = Theme.D(12); var dr = new RectangleF(r.X + Theme.D(10), r.Y + (it.Desc != null ? Theme.D(11) : (r.Height - ds) / 2f), ds, ds); Theme.StatusDot(g, dr, it.Key == "invisible" ? "off" : it.Key, hot ? Theme.Accent : Theme.Panel); int x = r.X + Theme.D(32), w = r.Width - Theme.D(64); using (var f = Theme.Display(14)) Txt.Draw(g, it.Title, f, new Rectangle(x, r.Y + (it.Desc != null ? Theme.D(5) : 0), w, it.Desc != null ? Theme.D(20) : r.Height), tc); if (it.Desc != null) using (var f = Theme.Body(12)) Txt.Draw(g, it.Desc, f, new Rectangle(x, r.Y + Theme.D(25), w, Theme.D(18)), hot ? Color.FromArgb(220, 255, 255, 255) : Theme.Muted); if (app.Presence == it.Key) { var cr = new Rectangle(r.Right - Theme.D(28), r.Y + (r.Height - Theme.D(18)) / 2, Theme.D(18), Theme.D(18)); using (var b = new SolidBrush(hot ? Color.White : Theme.Accent)) g.FillEllipse(b, cr); Theme.Check(g, cr, hot ? Theme.Accent : Color.White, 10); } } else { Theme.Icon(g, it.Glyph, "", new Rectangle(r.X + Theme.D(8), r.Y, Theme.D(18), r.Height), hot ? Color.White : it.Danger ? Theme.Red : Theme.Muted, 15); using (var f = Theme.Display(14)) Txt.Draw(g, it.Title, f, new Rectangle(r.X + Theme.D(36), r.Y, r.Width - Theme.D(44), r.Height), tc); } } using (var p = Theme.Round(new RectangleF(0.5f, 0.5f, Width - 1.5f, Height - 1.5f), Theme.D(Theme.ClassicLayout ? 4 : 10))) using (var pen = new Pen(Theme.ClassicLayout ? Xf.Border : Theme.Line)) g.DrawPath(pen, p); } // prikaz kao iskocni prozor sa zaobljenim rubovima public static void Show(TrayApp app, Control anchor, Point at, bool above) { var pop = new ProfilePopout(app); var host = new ToolStripControlHost(pop) { Margin = Padding.Empty, Padding = Padding.Empty, AutoSize = false, Size = pop.Size }; var dd = new ToolStripDropDown { Padding = Padding.Empty, Margin = Padding.Empty, AutoSize = false, Size = pop.Size, DropShadowEnabled = false, Renderer = new DarkRenderer() }; dd.Items.Add(host); using (var path = Theme.Round(new RectangleF(0, 0, pop.Width, pop.Height), Theme.D(Theme.ClassicLayout ? 4 : 10))) dd.Region = new Region(path); pop.Host = dd; dd.Show(anchor, at, above ? ToolStripDropDownDirection.AboveRight : ToolStripDropDownDirection.BelowRight); } } } // ===================== Neo.cs ===================== using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; using System.Windows.Forms; namespace Fragline { // Moderni raspored "Fragline Neo": gornja navigacija kao gaming launcher, prijatelji u desnoj ploci, // pocetna nadzorna ploca, voice "dock" na dnu. Nema bocnog popisa kao chat aplikacije. class TopNav : Control { public static readonly string[] Keys = { "home", "messages", "friends", "voice", "servers", "clans", "lfg" }; static readonly string[] Names = { "Početna", "Poruke", "Prijatelji", "Voice", "Serveri", "Klanovi", "LFG" }; static readonly string[] Glyphs = { "", Theme.IcoMsg, Theme.IcoPeople, Theme.IcoSpeaker, Theme.IcoGame, "", "\uE721" }; public int Selected = 0; public readonly int[] Badges = new int[7]; public bool VoiceOn, SocialOn = true; public int RightReserve; public int NotifUnread; public UserPanel Profile; public int OnlineCount; public event Action ProfileClicked; readonly List tabs = new List(); Rectangle socialBtn, gearBtn, bellBtn, profileR; int hot = -1; readonly ToolTip tt = new ToolTip(); public event Action TabClicked; public event Action SocialClicked, SettingsClicked, NotifsClicked; public TopNav() { SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); Cursor = Cursors.Hand; Anim.Hook(this, () => new Rectangle(0, Height - Theme.D(3), Width, Theme.D(3))); } int Hit(Point p) { for (int i = 0; i < tabs.Count; i++) if (tabs[i].Contains(p)) return i; if (socialBtn.Contains(p)) return 100; if (gearBtn.Contains(p)) return 101; if (bellBtn.Contains(p)) return 102; if (Theme.Dash && profileR.Contains(p)) return 103; return -1; } protected override void OnMouseMove(MouseEventArgs e) { int h = Hit(e.Location); if (h != hot) { hot = h; tt.SetToolTip(this, h == 100 ? (SocialOn ? "Sakrij prijatelje" : "Prikaži prijatelje") : h == 101 ? "Postavke" : h == 102 ? "Obavijesti" : null); Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseLeave(EventArgs e) { hot = -1; Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseClick(MouseEventArgs e) { int h = Hit(e.Location); if (h >= 0 && h < 100 && TabClicked != null) TabClicked(h); else if (h == 100 && SocialClicked != null) SocialClicked(); else if (h == 101 && SettingsClicked != null) SettingsClicked(); else if (h == 102 && NotifsClicked != null) NotifsClicked(); else if (h == 103 && ProfileClicked != null) ProfileClicked(new Point(profileR.X, Height + Theme.D(4))); base.OnMouseClick(e); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; bool dash = Theme.Dash; int x = Theme.D(196); if (dash) { // "Ploca": traka u ljubicasto-ruzicastom prijelazu, lijevo avatar, ime i broj prijatelja online using (var b = new LinearGradientBrush(ClientRectangle, Theme.Shade(Theme.Accent, -0.05f), Theme.Shade(Theme.Accent2, -0.05f), 0f)) g.FillRectangle(b, ClientRectangle); using (var b = new SolidBrush(Color.FromArgb(25, 255, 255, 255))) g.FillRectangle(b, 0, 0, Width, Height / 2); float av = Theme.D(42); var ar = new RectangleF(Theme.D(18), (Height - av) / 2f, av, av); using (var b = new SolidBrush(Color.White)) g.FillEllipse(b, ar.X - Theme.D(2), ar.Y - Theme.D(2), av + Theme.D(4), av + Theme.D(4)); if (Profile != null) Theme.Avatar(g, ar, Profile.ColorHex, Profile.User, Profile.Avatar, this, Profile.Kind, Theme.Accent); int nx = (int)ar.Right + Theme.D(12); string nm = Profile != null ? Profile.User : ""; using (var f = Theme.Display(16)) { int nw; using (var f2 = Theme.Body(11.5f, FontStyle.Bold)) nw = Math.Max(TextRenderer.MeasureText(nm, f).Width, TextRenderer.MeasureText(OnlineCount + " PRIJATELJA ONLINE", f2).Width + Theme.D(8)); profileR = new Rectangle((int)ar.X - Theme.D(4), Theme.D(8), nx - (int)ar.X + nw + Theme.D(30), Height - Theme.D(16)); if (hot == 103) using (var p = Theme.Round(profileR, Theme.D(12))) using (var b = new SolidBrush(Color.FromArgb(35, 255, 255, 255))) g.FillPath(b, p); Txt.Draw(g, nm, f, new Rectangle(nx, Theme.D(12), nw + Theme.D(4), Theme.D(22)), Color.White); } using (var f = Theme.Body(11.5f, FontStyle.Bold)) Txt.Draw(g, (OnlineCount + " PRIJATELJA ONLINE").ToUpperInvariant(), f, new Rectangle(nx, Theme.D(35), profileR.Right - nx - Theme.D(18), Theme.D(16)), Color.FromArgb(215, 255, 255, 255)); Theme.Chevron(g, new RectangleF(profileR.Right - Theme.D(20), Theme.D(32), Theme.D(12), Theme.D(20)), true, Color.FromArgb(215, 255, 255, 255)); x = Math.Max(Theme.D(196), profileR.Right + Theme.D(24)); } else { g.Clear(Theme.Panel); using (var pen = new Pen(Theme.Line)) g.DrawLine(pen, 0, Height - 1, Width, Height - 1); if (Theme.Animated) Anim.Fill(g, new Rectangle(0, Height - Theme.D(2), Width, Theme.D(2)), 0.8f, 1f, 0f); // znak Theme.DrawFlame(g, new RectangleF(Theme.D(18), (Height - Theme.D(28)) / 2f, Theme.D(25), Theme.D(28))); Theme.Wordmark(g, Theme.D(52), Height / 2f, 21, Theme.Text); } // kartice tabs.Clear(); using (var f = Theme.Display(14)) for (int i = 0; i < Names.Length; i++) { var tw = TextRenderer.MeasureText(Names[i], f, Size.Empty, TextFormatFlags.NoPadding).Width; var r = new Rectangle(x, 0, tw + Theme.D(52), Height); if (r.Right > Width - RightReserve - Theme.D(100)) break; tabs.Add(r); bool sel = i == Selected, h = i == hot; var pill = new Rectangle(r.X + Theme.D(4), Theme.D(12), r.Width - Theme.D(8), Height - Theme.D(24)); if (dash && (sel || h)) using (var p = Theme.Round(pill, Theme.D(12))) using (var b = new SolidBrush(Color.FromArgb(sel ? 55 : 28, 255, 255, 255))) g.FillPath(b, p); else if (h && !sel) using (var p = Theme.Round(pill, Theme.D(10))) using (var b = new SolidBrush(Theme.Panel2)) g.FillPath(b, p); var fc = dash ? Color.FromArgb(sel ? 255 : h ? 235 : 195, 255, 255, 255) : sel ? Theme.Text : h ? Theme.Text2 : Theme.Muted; var gc = dash ? fc : sel ? Theme.Accent2 : fc; if (i == 3 && VoiceOn) gc = Theme.On; Theme.Icon(g, Glyphs[i], "", new Rectangle(r.X + Theme.D(14), 0, Theme.D(18), Height), gc, 15); Txt.Draw(g, Names[i], f, new Rectangle(r.X + Theme.D(38), 0, tw + 2, Height), fc); if (Badges[i] > 0) Theme.Pill(g, Badges[i] > 99 ? "99+" : Badges[i].ToString(), new Rectangle(r.Right - Theme.D(22), Theme.D(10), Theme.D(22), Theme.D(16)), true, Theme.Panel); if (sel && !dash) { // odabrana kartica: svjetleca crta u prijelazu boje var ur = new Rectangle(r.X + Theme.D(14), Height - Theme.D(4), r.Width - Theme.D(28), Theme.D(3)); using (var b = new SolidBrush(Color.FromArgb(60, Theme.Accent2))) g.FillRectangle(b, ur.X, ur.Y - Theme.D(5), ur.Width, Theme.D(5)); using (var p = Theme.Round(ur, Theme.D(1.5f))) using (var b = new LinearGradientBrush(ur, Theme.Accent, Theme.Accent2, 0f)) g.FillPath(b, p); } x = r.Right; } // desno: prijatelji i postavke (lijevo od kartice profila) int s = Theme.D(36), bx = Width - RightReserve - Theme.D(8); gearBtn = new Rectangle(bx - s, (Height - s) / 2, s, s); socialBtn = new Rectangle(gearBtn.X - s - Theme.D(6), gearBtn.Y, s, s); bellBtn = new Rectangle(socialBtn.X - s - Theme.D(6), gearBtn.Y, s, s); foreach (var t in new[] { Tuple.Create(bellBtn, 102, ""), Tuple.Create(socialBtn, 100, Theme.IcoPeople), Tuple.Create(gearBtn, 101, Theme.IcoGear) }) { bool on = t.Item2 == 100 && SocialOn; if (dash) { if (hot == t.Item2 || on) using (var b = new SolidBrush(Color.FromArgb(hot == t.Item2 ? 45 : 25, 255, 255, 255))) g.FillEllipse(b, t.Item1); Theme.Icon(g, t.Item3, t.Item2 == 102 ? "!" : "", t.Item1, Color.White, 15); continue; } if (hot == t.Item2) using (var b = new SolidBrush(Theme.Panel2)) g.FillEllipse(b, t.Item1); if (on) using (var pen = new Pen(Color.FromArgb(120, Theme.Accent2), Theme.D(1.5f))) g.DrawEllipse(pen, t.Item1.X + 1, t.Item1.Y + 1, t.Item1.Width - 2, t.Item1.Height - 2); Theme.Icon(g, t.Item3, t.Item2 == 102 ? "!" : "", t.Item1, on ? Theme.Accent2 : hot == t.Item2 ? Theme.Text : Theme.Muted, 15); } if (NotifUnread > 0) Theme.Pill(g, NotifUnread > 99 ? "99+" : NotifUnread.ToString(), new Rectangle(bellBtn.Right - Theme.D(18), bellBtn.Y + Theme.D(2), Theme.D(18), Theme.D(14)), true, Theme.Panel); } } // Kartica profila gore desno: avatar, ime i status; klik otvara iskocni profil class ProfileChip : UserPanel { bool hot; public ProfileChip() { BackColor = Theme.Panel; Cursor = Cursors.Hand; } protected override void OnMouseMove(MouseEventArgs e) { if (!hot) { hot = true; Invalidate(); } } protected override void OnMouseLeave(EventArgs e) { hot = false; Invalidate(); } protected override void OnMouseClick(MouseEventArgs e) { Fire(0, new Point(Width - Theme.D(300), Height + Theme.D(6))); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(Theme.Panel); var r = new RectangleF(0.5f, 0.5f, Width - 1.5f, Height - 1.5f); var bg = hot ? Theme.Panel2 : Theme.Field; using (var p = Theme.Round(r, Height / 2f)) { using (var b = new SolidBrush(bg)) g.FillPath(b, p); using (var pen = new Pen(Color.FromArgb(hot ? 160 : 80, Theme.Accent))) g.DrawPath(pen, p); } float av = Height - Theme.D(10); Theme.Avatar(g, new RectangleF(Theme.D(5), Theme.D(5), av, av), ColorHex, User, Avatar, this, Kind, bg); int x = (int)av + Theme.D(14), w = Width - x - Theme.D(14); using (var f = Theme.Display(13.5f)) Txt.Draw(g, User, f, new Rectangle(x, Theme.D(4), w, Height / 2 - Theme.D(2)), Theme.Text); string sub = Offline ? "Nema veze" : Game != null ? "Igra " + Game : Theme.KindText(Presence == "invisible" ? "invisible" : Kind); using (var f = Theme.Body(11.5f)) Txt.Draw(g, sub, f, new Rectangle(x, Height / 2, w, Height / 2 - Theme.D(4)), Offline ? Theme.Red : Game != null ? Theme.On : Theme.Muted); } } // Desna ploca s prijateljima (kao u gaming launcherima) class SocialPanel : UserControl { readonly TrayApp app; public readonly XBuddyList List; int online, total; readonly Panel head = new Panel(); bool addHot; public SocialPanel(TrayApp app) { this.app = app; BackColor = Theme.Panel; List = new XBuddyList(app, true) { Dock = DockStyle.Fill }; head.Dock = DockStyle.Top; head.Height = Theme.D(58); head.BackColor = Theme.Panel; head.Cursor = Cursors.Default; head.Paint += (s, e) => { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; using (var f = Theme.Display(16)) Txt.Draw(g, "Prijatelji", f, new Rectangle(Theme.D(18), Theme.D(8), head.Width - Theme.D(70), Theme.D(24)), Theme.Text); using (var f = Theme.Body(12)) Txt.Draw(g, online + " online od " + total, f, new Rectangle(Theme.D(18), Theme.D(32), head.Width - Theme.D(70), Theme.D(18)), Theme.Muted); var ab = AddBtn(); using (var p = Theme.Round(ab, ab.Height / 2f)) { if (addHot) using (var b = new LinearGradientBrush(ab, Theme.Accent, Theme.Accent2, 0f)) g.FillPath(b, p); else using (var b = new SolidBrush(Theme.Panel2)) g.FillPath(b, p); } Theme.Icon(g, "", "+", ab, addHot ? Color.White : Theme.Text2, 15); }; head.MouseMove += (s, e) => { bool h = AddBtn().Contains(e.Location); if (h != addHot) { addHot = h; head.Cursor = h ? Cursors.Hand : Cursors.Default; head.Invalidate(); } }; head.MouseLeave += (s, e) => { addHot = false; head.Invalidate(); }; head.MouseClick += (s, e) => { if (AddBtn().Contains(e.Location)) app.ShowAddFriend(); }; new ToolTip().SetToolTip(head, "Dodaj prijatelja"); Controls.Add(List); Controls.Add(head); List.BringToFront(); } Rectangle AddBtn() { int s = Theme.D(36); return new Rectangle(head.Width - s - Theme.D(14), (head.Height - s) / 2, s, s); } public void SetData(List f) { online = f.Count(x => x.Kind != "off"); total = f.Count; List.SetData(f); head.Invalidate(); } } // Pozdravna kartica na pocetnoj: avatar, pozdrav, "staklene" oznake i gumbi-pilule class HeroCard : Control { readonly TrayApp app; public int Online, Playing; int hot = -1; Rectangle findR, addR; public event Action FindClicked, AddClicked; public HeroCard(TrayApp app) { this.app = app; SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); } int Hit(Point p) { return findR.Contains(p) ? 0 : addR.Contains(p) ? 1 : -1; } protected override void OnMouseMove(MouseEventArgs e) { int h = Hit(e.Location); if (h != hot) { hot = h; Cursor = h >= 0 ? Cursors.Hand : Cursors.Default; Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseLeave(EventArgs e) { hot = -1; Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseClick(MouseEventArgs e) { int h = Hit(e.Location); if (h == 0 && FindClicked != null) FindClicked(); else if (h == 1 && AddClicked != null) AddClicked(); base.OnMouseClick(e); } void Chip(Graphics g, ref int x, int y, Color dot, string text) { using (var f = Theme.Display(12.5f)) { int w = TextRenderer.MeasureText(text, f, Size.Empty, TextFormatFlags.NoPadding).Width + Theme.D(34); var r = new Rectangle(x, y, w, Theme.D(26)); using (var p = Theme.Round(r, r.Height / 2f)) using (var b = new SolidBrush(Color.FromArgb(46, 255, 255, 255))) g.FillPath(b, p); using (var b = new SolidBrush(dot)) g.FillEllipse(b, r.X + Theme.D(11), r.Y + (r.Height - Theme.D(8)) / 2, Theme.D(8), Theme.D(8)); TextRenderer.DrawText(g, text, f, new Rectangle(r.X + Theme.D(24), r.Y, w - Theme.D(28), r.Height), Color.White, TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding); x = r.Right + Theme.D(8); } } Rectangle Button(Graphics g, int x, int y, string text, bool solid, bool hover) { using (var f = Theme.Display(14)) { int w = TextRenderer.MeasureText(text, f, Size.Empty, TextFormatFlags.NoPadding).Width + Theme.D(40); var r = new Rectangle(x, y, w, Theme.D(40)); using (var p = Theme.Round(r, r.Height / 2f)) { if (solid) using (var b = new SolidBrush(hover ? Color.FromArgb(235, 240, 255) : Color.White)) g.FillPath(b, p); else { using (var b = new SolidBrush(Color.FromArgb(hover ? 70 : 40, 255, 255, 255))) g.FillPath(b, p); using (var pen = new Pen(Color.FromArgb(140, 255, 255, 255), Theme.D(1.2f))) g.DrawPath(pen, p); } } TextRenderer.DrawText(g, text, f, r, solid ? Theme.Shade(Theme.Accent, -0.25f) : Color.White, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding); return r; } } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(Parent != null ? Parent.BackColor : Theme.Bg); var r = new Rectangle(0, 0, Width - 1, Height - 1); using (var p = Theme.Round(r, Theme.D(20))) { using (var b = new LinearGradientBrush(r, Theme.Accent, Theme.Accent2, 20f)) g.FillPath(b, p); var st = g.Save(); g.SetClip(p); // meki ukrasni krugovi i tamniji donji rub za dubinu using (var b = new SolidBrush(Color.FromArgb(30, 255, 255, 255))) { g.FillEllipse(b, Width - Theme.D(300), -Theme.D(160), Theme.D(380), Theme.D(380)); g.FillEllipse(b, Width - Theme.D(170), Height - Theme.D(110), Theme.D(240), Theme.D(240)); } using (var b = new LinearGradientBrush(new Rectangle(0, Height / 2, Width, Height / 2 + 1), Color.FromArgb(0, 0, 0, 0), Color.FromArgb(50, 0, 0, 0), 90f)) g.FillRectangle(b, 0, Height / 2, Width, Height / 2 + 1); g.Restore(st); } g.SmoothingMode = SmoothingMode.AntiAlias; Theme.DrawFlame(g, new RectangleF(Width - Theme.D(150), (Height - Theme.D(120)) / 2f, Theme.D(104), Theme.D(120))); // avatar s bijelim prstenom float av = Theme.D(72), ax = Theme.D(28), ay = Theme.D(28); using (var b = new SolidBrush(Color.FromArgb(220, 255, 255, 255))) g.FillEllipse(b, ax - Theme.D(4), ay - Theme.D(4), av + Theme.D(8), av + Theme.D(8)); Theme.Avatar(g, new RectangleF(ax, ay, av, av), app.ColorHex, app.Username, app.AvatarUrl, this, null, Color.White); int tx = (int)(ax + av + Theme.D(22)), tw = Math.Max(10, Width - tx - Theme.D(190)); using (var f = Theme.Display(27)) Txt.Draw(g, "Bok, " + (app.Username ?? "igraču") + "!", f, new Rectangle(tx, Theme.D(24), tw, Theme.D(40)), Color.White); string sub; var game = app.CurrentGame; if (game != null) sub = "Upravo igraš " + game + ". Scroll Lock šalje screenshot na tvoj profil."; else if (Playing > 0) sub = "Prijatelji su u igri. Pridruži im se ili pronađi novi server."; else if (Online > 0) sub = "Prijatelji su online. Spreman za igru?"; else sub = "Nitko od prijatelja nije online. Dodaj nekoga ili pronađi server."; using (var f = Theme.Body(14.5f)) Txt.Draw(g, sub, f, new Rectangle(tx, Theme.D(64), tw, Theme.D(22)), Color.FromArgb(235, 255, 255, 255)); int cx = tx; Chip(g, ref cx, Theme.D(94), Theme.On, Online + " online"); Chip(g, ref cx, Theme.D(94), Theme.Idle, Playing + " u igri"); int by = Height - Theme.D(40) - Theme.D(24); findR = Button(g, Theme.D(28), by, "Pronađi server", true, hot == 0); addR = Button(g, findR.Right + Theme.D(10), by, "Dodaj prijatelja", false, hot == 1); } } // Velike kartice prijatelja koji igraju ili su u voiceu class GameCards : Control { readonly TrayApp app; List items = new List(); readonly List cards = new List(); int hover = -1; public GameCards(TrayApp app) { this.app = app; SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); } public void SetData(List all) { items = all.Where(f => f.Status == "play" || (f.Voice != null && f.Kind != "off")).OrderBy(Txt.Rank).ThenBy(f => f.Username, StringComparer.OrdinalIgnoreCase).Take(8).ToList(); Invalidate(); } int Cols(int w) { return Math.Max(1, (w + Theme.D(14)) / (Theme.D(250) + Theme.D(14))); } public int HeightFor(int w) { if (items.Count == 0) return Theme.D(64); int rows = Math.Min(2, (items.Count + Cols(w) - 1) / Cols(w)); return rows * Theme.D(96) + (rows - 1) * Theme.D(14); } int Hit(Point p) { for (int i = 0; i < cards.Count; i++) if (cards[i].Contains(p)) return i; return -1; } protected override void OnMouseMove(MouseEventArgs e) { int h = Hit(e.Location); if (h != hover) { hover = h; Cursor = h >= 0 ? Cursors.Hand : Cursors.Default; Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseLeave(EventArgs e) { hover = -1; Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseClick(MouseEventArgs e) { int h = Hit(e.Location); if (h >= 0 && h < items.Count) app.OpenChat(items[h]); base.OnMouseClick(e); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(Parent != null ? Parent.BackColor : Theme.Bg); cards.Clear(); if (items.Count == 0) { var er = new Rectangle(0, 0, Width - 1, Height - 1); using (var p = Theme.Round(er, Theme.D(14))) using (var pen = new Pen(Theme.Line) { DashStyle = DashStyle.Dash }) g.DrawPath(pen, p); using (var f = Theme.Body(13.5f)) TextRenderer.DrawText(g, "Nitko od prijatelja trenutno ne igra. Kad netko pokrene igru, ovdje će se pojaviti njegova kartica.", f, er, Theme.Muted, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.WordBreak | TextFormatFlags.NoPrefix); return; } int cols = Cols(Width), gap = Theme.D(14), cw = (Width - gap * (cols - 1)) / cols, ch = Theme.D(96); for (int i = 0; i < items.Count && i < cols * 2; i++) { var f = items[i]; var r = new Rectangle((i % cols) * (cw + gap), (i / cols) * (ch + gap), cw, ch); cards.Add(r); bool hot = i == hover; using (var p = Theme.Round(r, Theme.D(14))) { using (var b = new LinearGradientBrush(r, hot ? Theme.Panel3 : Theme.Panel2, Theme.Panel, 90f)) g.FillPath(b, p); using (var pen = new Pen(hot ? Color.FromArgb(160, Theme.Accent2) : Theme.Line)) g.DrawPath(pen, p); } // velika ikona igre s malim avatarom preko kuta var ir = new Rectangle(r.X + Theme.D(14), r.Y + Theme.D(14), Theme.D(68), Theme.D(68)); var img = f.GameIcon != null ? ImageCache.Get(f.GameIcon, this) : null; if (img != null) g.DrawImage(Theme.Masked(img, ir.Width, ir.Height, Theme.D(12)), ir.X, ir.Y, ir.Width, ir.Height); else { using (var p = Theme.Round(ir, Theme.D(12))) using (var b = new LinearGradientBrush(ir, Theme.Accent, Theme.Accent2, 45f)) g.FillPath(b, p); Theme.Icon(g, f.Status == "play" ? Theme.IcoGame : Theme.IcoSpeaker, "", ir, Color.White, 26); } float av = Theme.D(30); using (var b = new SolidBrush(Theme.Panel)) g.FillEllipse(b, ir.Right - av + Theme.D(2), ir.Bottom - av + Theme.D(2), av + Theme.D(4), av + Theme.D(4)); Theme.Avatar(g, new RectangleF(ir.Right - av + Theme.D(4), ir.Bottom - av + Theme.D(4), av, av), f.Color, f.Username, f.Avatar, this, null, Theme.Panel); int x = ir.Right + Theme.D(16), w = Math.Max(10, r.Right - x - Theme.D(12)); using (var fn = Theme.Display(15)) Txt.Draw(g, f.Username, fn, new Rectangle(x, r.Y + Theme.D(18), w, Theme.D(22)), Theme.Text); bool play = f.Status == "play"; using (var fn = Theme.Body(13)) Txt.Draw(g, play ? f.Game : "U voiceu: " + f.Voice, fn, new Rectangle(x, r.Y + Theme.D(42), w, Theme.D(20)), play ? Theme.On : Theme.Accent2); var since = play ? Theme.Since(f.Since) : null; using (var fn = Theme.Body(11.5f)) Txt.Draw(g, since != null ? "već " + since + " · klikni za poruku" : "klikni za poruku", fn, new Rectangle(x, r.Y + Theme.D(64), w, Theme.D(18)), Theme.Muted); } } } // Pocetna: pozdrav, prijatelji u igri i nedavna aktivnost class HomePage : UserControl { public readonly HeroCard Hero; public readonly GameCards Cards; readonly ActivityView activity; readonly Label l1 = new Label(), l2 = new Label(); public HomePage(TrayApp app) { BackColor = Theme.Bg; Hero = new HeroCard(app); Cards = new GameCards(app); activity = new ActivityView(app); foreach (var l in new[] { l1, l2 }) { l.Font = Theme.Body(12, FontStyle.Bold); l.ForeColor = Theme.Muted; l.BackColor = Theme.Bg; l.AutoSize = true; } l1.Text = "PRIJATELJI U IGRI"; l2.Text = "NEDAVNA AKTIVNOST"; Hero.FindClicked += () => app.ShowServers(); Hero.AddClicked += () => app.ShowAddFriend(); Controls.AddRange(new Control[] { Hero, l1, Cards, l2, activity }); } protected override void OnResize(EventArgs e) { base.OnResize(e); if (activity == null) return; int pad = Theme.D(24), w = Math.Max(10, Width - pad * 2), y = pad; Hero.SetBounds(pad, y, w, Theme.D(196)); y += Theme.D(196) + Theme.D(26); l1.Location = new Point(pad + Theme.D(2), y); y += Theme.D(24); int ch = Cards.HeightFor(w); Cards.SetBounds(pad, y, w, ch); y += ch + Theme.D(26); l2.Location = new Point(pad + Theme.D(2), y); y += Theme.D(20); activity.SetBounds(pad - Theme.D(16), y, w + Theme.D(32), Math.Max(10, Height - y - Theme.D(8))); } public void SetFriends(List f) { Hero.Online = f.Count(x => x.Kind != "off"); Hero.Playing = f.Count(x => x.Status == "play"); Hero.Invalidate(); int before = Cards.HeightFor(Math.Max(10, Width - Theme.D(48))); Cards.SetData(f); if (Cards.HeightFor(Math.Max(10, Width - Theme.D(48))) != before) OnResize(EventArgs.Empty); } } // Klanovi kao mreza velikih kartica class ClanCards : Control { readonly TrayApp app; List clans = new List(); readonly List cards = new List(); int hover = -1; public ClanCards(TrayApp app) { this.app = app; SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); BackColor = Theme.Bg; } public void SetData(List c) { clans = c; Invalidate(); } int Hit(Point p) { for (int i = 0; i < cards.Count; i++) if (cards[i].Contains(p)) return i; return -1; } protected override void OnMouseMove(MouseEventArgs e) { int h = Hit(e.Location); if (h != hover) { hover = h; Cursor = h >= 0 ? Cursors.Hand : Cursors.Default; Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseLeave(EventArgs e) { hover = -1; Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseClick(MouseEventArgs e) { int h = Hit(e.Location); if (h < 0) return; if (h >= clans.Count) { if (e.Button == MouseButtons.Left) app.ShowClanForm(false); return; } var c = clans[h]; if (e.Button == MouseButtons.Right) { var menu = Ui.Menu(); if (c.Slug != null) menu.Items.Add(new ToolStripMenuItem("Otvori stranicu klana", null, (a, b) => app.Open("/clan/" + Uri.EscapeDataString(c.Slug)))); menu.Items.Add(new ToolStripMenuItem("Kopiraj link za pozivanje", null, (a, b) => { try { Clipboard.SetText(Api.Base + "/clan/" + c.Slug); } catch { } })); menu.Items.Add(new ToolStripSeparator()); menu.Items.Add(new ToolStripMenuItem(c.Owner ? "Obriši klan" : "Napusti klan", null, (a, b) => app.LeaveClan(c)) { ForeColor = Theme.Red }); menu.Show(this, e.Location); } else if (c.Slug != null) app.Open("/clan/" + Uri.EscapeDataString(c.Slug)); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(Theme.Bg); cards.Clear(); int gap = Theme.D(16), cols = Math.Max(1, (Width + gap) / (Theme.D(210) + gap)), cw = (Width - gap * (cols - 1)) / cols, ch = Theme.D(150); for (int i = 0; i <= clans.Count; i++) { var r = new Rectangle((i % cols) * (cw + gap), (i / cols) * (ch + gap), cw, ch); if (r.Y > Height) break; cards.Add(r); bool hot = i == hover; if (i == clans.Count) { // zadnja kartica: novi klan using (var p = Theme.Round(r, Theme.D(16))) { if (hot) using (var b = new SolidBrush(Theme.Panel)) g.FillPath(b, p); using (var pen = new Pen(hot ? Theme.Accent2 : Theme.Line, Theme.D(1.5f)) { DashStyle = DashStyle.Dash }) g.DrawPath(pen, p); } Theme.Icon(g, Theme.IcoAdd, "+", new Rectangle(r.X, r.Y + Theme.D(30), r.Width, Theme.D(40)), hot ? Theme.Accent2 : Theme.Muted, 26); using (var f = Theme.Display(14)) TextRenderer.DrawText(g, "Napravi ili pronađi klan", f, new Rectangle(r.X, r.Y + Theme.D(84), r.Width, Theme.D(22)), hot ? Theme.Text : Theme.Muted, TextFormatFlags.HorizontalCenter | TextFormatFlags.NoPrefix); continue; } var c = clans[i]; var col = Theme.Hex(c.Color, Theme.Accent); using (var p = Theme.Round(r, Theme.D(16))) { using (var b = new SolidBrush(Theme.Panel)) g.FillPath(b, p); using (var pen = new Pen(hot ? col : Theme.Line, hot ? Theme.D(1.5f) : 1f)) g.DrawPath(pen, p); } // gornji dio kartice u boji klana var top = new Rectangle(r.X, r.Y, r.Width, Theme.D(70)); using (var p = new GraphicsPath()) { float d = Theme.D(32); p.AddArc(top.X, top.Y, d, d, 180, 90); p.AddArc(top.Right - d, top.Y, d, d, 270, 90); p.AddLine(top.Right, top.Bottom, top.X, top.Bottom); p.CloseFigure(); using (var b = new LinearGradientBrush(top, Theme.Shade(col, 0.1f), Theme.Shade(col, -0.35f), 30f)) g.FillPath(b, p); } var tag = c.Tag ?? "?"; var tr = new Rectangle(r.X + Theme.D(16), r.Y + Theme.D(40), Theme.D(56), Theme.D(56)); using (var p = Theme.Round(tr, Theme.D(14))) { using (var b = new SolidBrush(Theme.Panel)) g.FillPath(b, p); var inner = tr; inner.Inflate(-Theme.D(4), -Theme.D(4)); using (var p2 = Theme.Round(inner, Theme.D(11))) using (var b = new LinearGradientBrush(inner, col, Theme.Shade(col, -0.3f), 45f)) g.FillPath(b, p2); using (var f = Theme.Body(tag.Length > 3 ? 11 : 14, FontStyle.Bold)) TextRenderer.DrawText(g, tag, f, inner, Color.White, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding | TextFormatFlags.NoPrefix); } var name = c.Name ?? ""; if (!string.IsNullOrEmpty(c.Tag) && name.StartsWith("[" + c.Tag + "] ")) name = name.Substring(c.Tag.Length + 3); using (var f = Theme.Display(15)) Txt.Draw(g, name, f, new Rectangle(r.X + Theme.D(16), r.Y + Theme.D(100), r.Width - Theme.D(32), Theme.D(22)), Theme.Text); using (var f = Theme.Body(12)) Txt.Draw(g, c.Owner ? "Ti si vođa" : "Član", f, new Rectangle(r.X + Theme.D(16), r.Y + Theme.D(122), r.Width - Theme.D(32), Theme.D(18)), c.Owner ? Theme.Idle : Theme.Muted); } } } class ClansPage : UserControl { readonly PageHeader head = new PageHeader { Glyph = "", Title = "Klanovi", Sub = "Tvoje ekipe. Desni klik za više opcija." }; public readonly ClanCards Cards; public ClansPage(TrayApp app) { BackColor = Theme.Bg; Cards = new ClanCards(app); Controls.AddRange(new Control[] { head, Cards }); } protected override void OnResize(EventArgs e) { base.OnResize(e); if (Cards == null) return; head.SetBounds(0, 0, Width, Theme.D(56)); Cards.SetBounds(Theme.D(24), Theme.D(76), Math.Max(10, Width - Theme.D(48)), Math.Max(10, Height - Theme.D(96))); } } // Poruke: popis razgovora lijevo, razgovor u oblacicima desno class MessagesPage : UserControl { public readonly HomeList List; public readonly ChatView Chat; readonly EmptyState empty = new EmptyState(Theme.IcoMsg, "Tvoji razgovori", "Odaberi razgovor s lijeve strane ili klikni prijatelja u ploči desno."); public MessagesPage(TrayApp app) { BackColor = Theme.Bg; List = new HomeList(app, true); Chat = new ChatView(app) { Visible = false }; List.Picked += key => { if (key.StartsWith("dm:")) app.OpenChatByName(key.Substring(3)); }; Controls.AddRange(new Control[] { List, Chat, empty }); } protected override void OnResize(EventArgs e) { base.OnResize(e); if (empty == null) return; int lw = Theme.D(290); List.SetBounds(0, 0, lw, Height); Chat.SetBounds(lw, 0, Math.Max(10, Width - lw), Height); empty.SetBounds(lw, 0, Math.Max(10, Width - lw), Height); } public void ShowChat(bool on) { Chat.Visible = on; empty.Visible = !on; } } // Prazno stanje: velika ikona u krugu s prijelazom boje, naslov i opis class EmptyState : Control { readonly string glyph, title, text; public EmptyState(string glyph, string title, string text) { this.glyph = glyph; this.title = title; this.text = text; SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(Theme.Bg); int cy = Height / 2 - Theme.D(60), s = Theme.D(88); var c = new Rectangle((Width - s) / 2, cy - s / 2, s, s); using (var b = new SolidBrush(Color.FromArgb(40, Theme.Accent2))) g.FillEllipse(b, c.X - Theme.D(14), c.Y - Theme.D(14), s + Theme.D(28), s + Theme.D(28)); using (var b = new LinearGradientBrush(c, Theme.Accent, Theme.Accent2, 45f)) g.FillEllipse(b, c); Theme.Icon(g, glyph, "", c, Color.White, 34); using (var f = Theme.Display(20)) TextRenderer.DrawText(g, title, f, new Rectangle(0, c.Bottom + Theme.D(26), Width, Theme.D(30)), Theme.Text, TextFormatFlags.HorizontalCenter | TextFormatFlags.NoPrefix); using (var f = Theme.Body(14)) TextRenderer.DrawText(g, text, f, new Rectangle(Theme.D(40), c.Bottom + Theme.D(60), Width - Theme.D(80), Theme.D(60)), Theme.Muted, TextFormatFlags.HorizontalCenter | TextFormatFlags.WordBreak | TextFormatFlags.NoPrefix); } } // Voice: popis soba lijevo i soba (plocice sudionika i kontrole) desno class VoiceHub : UserControl { public readonly VoiceView Rooms; public readonly VoicePage Stage; public VoiceHub(TrayApp app) { BackColor = Theme.Bg; Rooms = new VoiceView(app); Stage = new VoicePage(app, Rooms); Controls.AddRange(new Control[] { Rooms, Stage }); } protected override void OnResize(EventArgs e) { base.OnResize(e); if (Stage == null) return; int lw = Theme.D(300); Rooms.SetBounds(0, 0, lw, Height); Stage.SetBounds(lw, 0, Math.Max(10, Width - lw), Height); } } // Traka na dnu dok si u voiceu (kao traka media playera) class VoiceDock : Control { readonly TrayApp app; public readonly RoundButton Mic = new RoundButton(Theme.IcoMic, "M", "Mikrofon"), Deaf = new RoundButton(Theme.IcoHead, "Z", "Zvuk"), Hang = new RoundButton(Theme.IcoHangup, "X", "Izađi") { Danger = true }; public string Room; public bool Talking; public int Members; public event Action Open; public VoiceDock(TrayApp app) { this.app = app; SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); Cursor = Cursors.Hand; foreach (var b in new[] { Mic, Deaf, Hang }) b.BackColor = Theme.UserBar; Mic.Click += (s, e) => app.ToggleMute(); Deaf.Click += (s, e) => app.ToggleDeaf(); Hang.Click += (s, e) => app.LeaveVoice(); Controls.AddRange(new Control[] { Mic, Deaf, Hang }); new ToolTip().SetToolTip(this, "Otvori voice sobu"); } protected override void OnResize(EventArgs e) { base.OnResize(e); int s = Theme.D(40), y = (Height - s) / 2, x = Width - Theme.D(14) - s; Hang.SetBounds(x, y, s, s); x -= s + Theme.D(8); Deaf.SetBounds(x, y, s, s); x -= s + Theme.D(8); Mic.SetBounds(x, y, s, s); Theme.RoundRegion(this, Height / 2); } protected override void OnMouseClick(MouseEventArgs e) { if (Open != null) Open(); base.OnMouseClick(e); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; var r = new RectangleF(0.5f, 0.5f, Width - 1.5f, Height - 1.5f); using (var p = Theme.Round(r, Height / 2f)) { using (var b = new SolidBrush(Theme.UserBar)) g.FillPath(b, p); using (var pen = new Pen(Color.FromArgb(150, Theme.On), Theme.D(1.5f))) g.DrawPath(pen, p); } // zvucni valovi koji se dizu dok pricas int bx = Theme.D(22), cy = Height / 2; using (var b = new SolidBrush(Theme.On)) for (int i = 0; i < 4; i++) { int h = Talking ? Theme.D(8 + (i % 2 == 0 ? 10 : 4) + i * 2) : Theme.D(6 + i * 3); using (var p = Theme.Round(new Rectangle(bx + i * Theme.D(6), cy - h / 2, Theme.D(3), h), Theme.D(1.5f))) g.FillPath(b, p); } int x = bx + Theme.D(36), w = Mic.Left - x - Theme.D(10); using (var f = Theme.Display(14)) Txt.Draw(g, Talking ? "Pričaš..." : "Glas spojen", f, new Rectangle(x, Theme.D(10), w, Theme.D(20)), Theme.On); using (var f = Theme.Body(12.5f)) Txt.Draw(g, (Room ?? "") + (Members > 0 ? " · " + (Members + 1) + " u sobi" : " · samo ti"), f, new Rectangle(x, Theme.D(31), w, Theme.D(18)), Theme.Text2); } } class NeoForm : ShellForm { readonly UpdateBar updateBar = new UpdateBar { Visible = false }; readonly TopNav nav = new TopNav(); readonly ProfileChip chip = new ProfileChip(); readonly SocialPanel social; readonly DashHome home; readonly MessagesPage messages; readonly FriendsPage friendsPg; readonly VoiceHub voiceHub; readonly HeaderPage serversPg, settingsPg, notifsPg, lfgPg; readonly NotifList notifList; readonly LfgList lfgList; readonly ClansPage clans; readonly SettingsView settings; readonly VoiceDock dock; readonly Control[] pages; readonly Timer tick = new Timer { Interval = 250 }; int page, ticks; bool socialOn; public override UserPanel User { get { return chip; } } public override VoiceView Voice { get { return voiceHub.Rooms; } } public NeoForm(TrayApp app) : base(app) { Text = "Fragline"; FormBorderStyle = FormBorderStyle.Sizable; MaximizeBox = true; BackColor = Theme.Canvas; MinimumSize = new Size(Theme.D(1000), Theme.D(620)); var wa = Screen.PrimaryScreen.WorkingArea; ClientSize = new Size(Math.Min(wa.Width - Theme.D(40), Prefs.Int("neo_w", Theme.D(1320))), Math.Min(wa.Height - Theme.D(60), Prefs.Int("neo_h", Theme.D(800)))); socialOn = Prefs.Bool("neo_social", true); social = new SocialPanel(app) { Visible = socialOn }; nav.SocialOn = socialOn; home = new DashHome(app); home.Navigate += k => { if (k == "settings") { ShowSettings(); return; } int ti = Array.IndexOf(TopNav.Keys, k); if (ti >= 0) ShowTab(ti); }; messages = new MessagesPage(app) { Visible = false }; friendsPg = new FriendsPage(app) { Visible = false }; voiceHub = new VoiceHub(app) { Visible = false }; serversPg = new HeaderPage(new PageHeader { Glyph = Theme.IcoGame, Title = "Serveri", Sub = "Game serveri, ping i igrači" }, new ServersView(app)) { Visible = false }; clans = new ClansPage(app) { Visible = false }; settings = new SettingsView(app); settingsPg = new HeaderPage(new PageHeader { Glyph = Theme.IcoGear, Title = "Postavke" }, settings) { Visible = false }; notifList = new NotifList(); notifsPg = new HeaderPage(new PageHeader { Glyph = "", Title = "Obavijesti", Sub = "Komentari, lajkovi i spomeni" }, notifList) { Visible = false }; lfgList = new LfgList(app); lfgPg = new HeaderPage(new PageHeader { Glyph = "", Title = "LFG", Sub = "Traženje ekipe za igru" }, lfgList) { Visible = false }; dock = new VoiceDock(app) { Visible = false }; pages = new Control[] { home, messages, friendsPg, voiceHub, serversPg, clans, lfgPg, settingsPg, notifsPg }; Controls.AddRange(new Control[] { updateBar, nav, chip, social }); Controls.AddRange(pages); Controls.Add(dock); dock.BringToFront(); chip.BringToFront(); nav.TabClicked += i => ShowTab(i); if (Theme.Dash) { // u "Ploci" profil je lijevo u gornjoj traci, pa kartica profila desno nije potrebna chip.Visible = false; nav.Profile = chip; nav.ProfileClicked += pt => StatusMenu(nav, pt); } nav.SocialClicked += () => { socialOn = !socialOn; social.Visible = socialOn; nav.SocialOn = socialOn; Prefs.Set("neo_social", socialOn); nav.Invalidate(); DoLayout(); }; nav.SettingsClicked += () => ShowSettings(); nav.NotifsClicked += () => ShowNotifs(); chip.ProfileClicked += pt => StatusMenu(chip, pt); dock.Open += () => ShowVoice(); updateBar.UpdateClicked += () => app.DoUpdate(); updateBar.Closed += () => { updateBar.Visible = false; DoLayout(); }; tick.Tick += (s, e) => Tick(); tick.Start(); DoLayout(); } protected override void OnResize(EventArgs e) { base.OnResize(e); DoLayout(); } void DoLayout() { if (pages == null || dock == null) return; int W = ClientSize.Width, H = ClientSize.Height, g = Theme.D(10), rad = Theme.D(16); if (W <= 0 || H <= 0) return; int top = updateBar.Visible ? Theme.D(36) : 0; updateBar.SetBounds(0, 0, W, top); int nh = Theme.D(64), cw = Theme.D(210); nav.RightReserve = Theme.Dash ? Theme.D(8) : cw + Theme.D(16); nav.SetBounds(0, top, W, nh); chip.SetBounds(W - cw - Theme.D(16), top + (nh - Theme.D(44)) / 2, cw, Theme.D(44)); int y = top + nh + g, bh = Math.Max(10, H - y - g); int sw = socialOn ? Theme.D(310) : 0; if (socialOn) { social.SetBounds(W - sw - g, y, sw, bh); Theme.RoundRegion(social, rad); } var cr = new Rectangle(g, y, Math.Max(10, W - g * 2 - (sw > 0 ? sw + g : 0)), bh); foreach (var p in pages) { p.Bounds = cr; Theme.RoundRegion(p, rad); } int dw = Math.Min(Theme.D(480), cr.Width - Theme.D(40)), dh = Theme.D(62); dock.SetBounds(cr.X + (cr.Width - dw) / 2, cr.Bottom - dh - Theme.D(16), dw, dh); nav.Invalidate(); } void ShowTab(int i) { page = i; for (int k = 0; k < pages.Length; k++) pages[k].Visible = k == i; nav.Selected = i < TopNav.Keys.Length ? i : -1; nav.Invalidate(); UpdateDock(); } void UpdateDock() { var vc = App.Voice; bool show = vc != null && page != 3; if (dock.Visible != show) { dock.Visible = show; if (show) dock.BringToFront(); } } void Tick() { ticks++; var vc = App.Voice; bool on = vc != null; UpdateDock(); if (nav.VoiceOn != on) { nav.VoiceOn = on; nav.Invalidate(); } if (on) { int n = vc.LiveMembers().Count; if (dock.Room != vc.Label || dock.Talking != vc.Talking || dock.Members != n || dock.Mic.Off != vc.Muted || dock.Deaf.Off != vc.Deafened) { dock.Room = vc.Label; dock.Talking = vc.Talking; dock.Members = n; dock.Mic.Off = vc.Muted; dock.Deaf.Off = vc.Deafened; dock.Invalidate(); dock.Mic.Invalidate(); dock.Deaf.Invalidate(); } } bool muted = on ? vc.Muted : Prefs.Bool("muted", false), deaf = on && vc.Deafened, idle = App.AutoIdle; if (chip.Muted != muted || chip.Deaf != deaf || chip.Idle != idle) { chip.Muted = muted; chip.Deaf = deaf; chip.Idle = idle; chip.Invalidate(); } if (ticks % 20 == 0 && Visible) { social.List.Refresh2(); friendsPg.Refresh2(); home.Invalidate(); } if (Theme.Dash && ticks % 4 == 0) nav.Invalidate(); } public override void SetFriends(List list, int requests) { social.SetData(list); messages.List.SetData(list, requests); friendsPg.SetData(list, requests); home.SetFriends(list); nav.OnlineCount = list.Count(f => f.Kind != "off"); foreach (var f in list) messages.Chat.UpdateFriend(f); int unread = list.Sum(f => f.Unread); if (nav.Badges[1] != unread || nav.Badges[2] != requests) { nav.Badges[1] = unread; nav.Badges[2] = requests; nav.Invalidate(); } } public override void SetRequests(List r) { social.List.SetRequests(r); friendsPg.SetRequests(r); } public override void SetClans(List c) { clans.Cards.SetData(c); home.SetClans(c.Count); } public override void SetShot(string game, string url) { settings.SetShot(game, url); } public override void ShowUpdate(string text) { updateBar.Message = text; updateBar.Busy = false; updateBar.Visible = true; updateBar.Invalidate(); DoLayout(); } public override void SetUpdateText(string text, bool busy) { updateBar.Message = text; updateBar.Busy = busy; if (!updateBar.Visible) { updateBar.Visible = true; DoLayout(); } updateBar.Invalidate(); } public override void OpenChat(Friend f) { ShowTab(1); messages.Chat.Open(f); messages.ShowChat(true); messages.List.Select("dm:" + f.Username); messages.Chat.FocusInput(); } public override bool ChatActive(string u) { return Visible && WindowState != FormWindowState.Minimized && ContainsFocus && page == 1 && messages.Chat.FriendName == u; } public override void CloseChat(string user) { if (user != null && messages.Chat.FriendName != user) return; messages.Chat.Reset(); messages.ShowChat(false); } public override void ShowVoice() { ShowTab(3); } public override void ShowAddFriend() { ShowTab(2); friendsPg.ShowAdd(); } public override void ShowSettings() { ShowTab(7); } public override void ShowServers() { ShowTab(4); } public override void ShowNotifs() { ShowTab(8); App.MarkNotifsSeen(); } public override void ShowLfg() { ShowTab(6); } public override void SetNotifs(int unread, List items) { notifList.SetData(items); if (nav.NotifUnread != unread) { nav.NotifUnread = unread; nav.Invalidate(); } } public override void SetLfg(List items) { lfgList.SetData(items); home.SetLfg(items); } protected override void OnFormClosing(FormClosingEventArgs e) { if (WindowState == FormWindowState.Normal) { Prefs.Set("neo_w", ClientSize.Width); Prefs.Set("neo_h", ClientSize.Height); } if (!AllowClose && e.CloseReason == CloseReason.UserClosing) { e.Cancel = true; Hide(); App.HiddenHint(); } base.OnFormClosing(e); } } } // ===================== Orbit.cs ===================== using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; using System.Windows.Forms; namespace Fragline { // Vektorske ikone (ne ovise o fontu sa simbolima, pa izgledaju isto na svakom Windowsu) static class Vec { public static void Draw(Graphics g, string key, RectangleF r, Color c, float stroke) { var st = g.Save(); g.SmoothingMode = SmoothingMode.AntiAlias; float s = Math.Min(r.Width, r.Height), x = r.X + (r.Width - s) / 2, y = r.Y + (r.Height - s) / 2; Func P = (a, b) => new PointF(x + a * s, y + b * s); using (var pen = new Pen(c, stroke) { StartCap = LineCap.Round, EndCap = LineCap.Round, LineJoin = LineJoin.Round }) using (var br = new SolidBrush(c)) { switch (key) { case "home": g.DrawLines(pen, new[] { P(.12f, .5f), P(.5f, .16f), P(.88f, .5f) }); g.DrawLines(pen, new[] { P(.24f, .4f), P(.24f, .86f), P(.76f, .86f), P(.76f, .4f) }); g.DrawLines(pen, new[] { P(.42f, .86f), P(.42f, .64f), P(.58f, .64f), P(.58f, .86f) }); break; case "chat": using (var p = Theme.Round(new RectangleF(x + .12f * s, y + .18f * s, .76f * s, .54f * s), .16f * s)) g.DrawPath(pen, p); g.DrawLines(pen, new[] { P(.32f, .72f), P(.28f, .88f), P(.48f, .72f) }); for (int i = 0; i < 3; i++) g.FillEllipse(br, x + (.34f + i * .16f) * s - stroke * .6f, y + .45f * s - stroke * .6f, stroke * 1.2f, stroke * 1.2f); break; case "people": // dvije osobe: glava i ramena, druga manja iza g.DrawEllipse(pen, x + .62f * s, y + .2f * s, .2f * s, .2f * s); g.DrawArc(pen, x + .56f * s, y + .5f * s, .36f * s, .36f * s, 200, 140); g.DrawEllipse(pen, x + .2f * s, y + .16f * s, .28f * s, .28f * s); g.DrawArc(pen, x + .08f * s, y + .52f * s, .52f * s, .5f * s, 180, 180); break; case "mic": using (var p = Theme.Round(new RectangleF(x + .36f * s, y + .1f * s, .28f * s, .48f * s), .14f * s)) g.DrawPath(pen, p); g.DrawArc(pen, x + .22f * s, y + .26f * s, .56f * s, .48f * s, 0, 180); g.DrawLine(pen, P(.5f, .74f), P(.5f, .9f)); g.DrawLine(pen, P(.36f, .9f), P(.64f, .9f)); break; case "server": for (int i = 0; i < 3; i++) { var rr = new RectangleF(x + .14f * s, y + (.12f + i * .26f) * s, .72f * s, .2f * s); using (var p = Theme.Round(rr, .06f * s)) g.DrawPath(pen, p); g.FillEllipse(br, x + .26f * s - stroke * .7f, rr.Y + rr.Height / 2 - stroke * .7f, stroke * 1.4f, stroke * 1.4f); } break; case "shield": using (var p = new GraphicsPath()) { p.AddLines(new[] { P(.5f, .1f), P(.84f, .22f), P(.82f, .5f) }); p.AddBezier(P(.82f, .5f), P(.8f, .72f), P(.64f, .84f), P(.5f, .92f)); p.AddBezier(P(.5f, .92f), P(.36f, .84f), P(.2f, .72f), P(.18f, .5f)); p.AddLines(new[] { P(.18f, .5f), P(.16f, .22f) }); p.CloseFigure(); g.DrawPath(pen, p); } g.DrawLines(pen, new[] { P(.36f, .5f), P(.47f, .61f), P(.66f, .4f) }); break; case "target": g.DrawEllipse(pen, x + .14f * s, y + .14f * s, .72f * s, .72f * s); g.DrawEllipse(pen, x + .34f * s, y + .34f * s, .32f * s, .32f * s); g.DrawLine(pen, P(.5f, .04f), P(.5f, .22f)); g.DrawLine(pen, P(.5f, .78f), P(.5f, .96f)); g.DrawLine(pen, P(.04f, .5f), P(.22f, .5f)); g.DrawLine(pen, P(.78f, .5f), P(.96f, .5f)); break; case "bell": using (var p = new GraphicsPath()) { p.AddBezier(P(.22f, .7f), P(.3f, .6f), P(.26f, .5f), P(.28f, .4f)); p.AddBezier(P(.28f, .4f), P(.32f, .16f), P(.68f, .16f), P(.72f, .4f)); p.AddBezier(P(.72f, .4f), P(.74f, .5f), P(.7f, .6f), P(.78f, .7f)); p.CloseFigure(); g.DrawPath(pen, p); } g.DrawArc(pen, x + .42f * s, y + .72f * s, .16f * s, .14f * s, 0, 180); break; case "gear": float cx = x + .5f * s, cy = y + .5f * s; for (int i = 0; i < 8; i++) { double a = i * Math.PI / 4; g.DrawLine(pen, cx + (float)Math.Cos(a) * .3f * s, cy + (float)Math.Sin(a) * .3f * s, cx + (float)Math.Cos(a) * .42f * s, cy + (float)Math.Sin(a) * .42f * s); } g.DrawEllipse(pen, cx - .28f * s, cy - .28f * s, .56f * s, .56f * s); g.DrawEllipse(pen, cx - .11f * s, cy - .11f * s, .22f * s, .22f * s); break; case "phone": using (var p = new GraphicsPath()) { p.AddBezier(P(.24f, .14f), P(.12f, .2f), P(.1f, .36f), P(.22f, .54f)); p.AddBezier(P(.22f, .54f), P(.34f, .72f), P(.5f, .86f), P(.66f, .9f)); p.AddBezier(P(.66f, .9f), P(.8f, .92f), P(.88f, .82f), P(.86f, .74f)); p.AddLines(new[] { P(.86f, .74f), P(.7f, .62f), P(.6f, .7f) }); p.AddBezier(P(.6f, .7f), P(.48f, .64f), P(.38f, .52f), P(.32f, .42f)); p.AddLines(new[] { P(.32f, .42f), P(.4f, .32f), P(.3f, .14f) }); p.CloseFigure(); g.DrawPath(pen, p); } break; case "hangup": using (var p = new GraphicsPath()) { p.AddBezier(P(.08f, .56f), P(.3f, .3f), P(.7f, .3f), P(.92f, .56f)); p.AddLines(new[] { P(.92f, .56f), P(.84f, .7f), P(.68f, .62f), P(.66f, .5f) }); p.AddBezier(P(.66f, .5f), P(.56f, .46f), P(.44f, .46f), P(.34f, .5f)); p.AddLines(new[] { P(.34f, .5f), P(.32f, .62f), P(.16f, .7f) }); p.CloseFigure(); g.FillPath(br, p); } break; case "user": g.DrawEllipse(pen, x + .32f * s, y + .12f * s, .36f * s, .36f * s); g.DrawArc(pen, x + .14f * s, y + .58f * s, .72f * s, .6f * s, 180, 180); break; case "send": using (var p = new GraphicsPath()) { p.AddLines(new[] { P(.14f, .2f), P(.88f, .5f), P(.14f, .8f), P(.26f, .5f) }); p.CloseFigure(); g.FillPath(br, p); } break; case "trash": g.DrawLine(pen, P(.18f, .26f), P(.82f, .26f)); g.DrawLines(pen, new[] { P(.38f, .26f), P(.4f, .14f), P(.6f, .14f), P(.62f, .26f) }); g.DrawLines(pen, new[] { P(.26f, .26f), P(.3f, .86f), P(.7f, .86f), P(.74f, .26f) }); g.DrawLine(pen, P(.43f, .4f), P(.44f, .72f)); g.DrawLine(pen, P(.57f, .4f), P(.56f, .72f)); break; case "headset": g.DrawArc(pen, x + .14f * s, y + .14f * s, .72f * s, .72f * s, 180, 180); using (var p = Theme.Round(new RectangleF(x + .12f * s, y + .5f * s, .16f * s, .3f * s), .06f * s)) g.FillPath(br, p); using (var p = Theme.Round(new RectangleF(x + .72f * s, y + .5f * s, .16f * s, .3f * s), .06f * s)) g.FillPath(br, p); g.DrawLine(pen, P(.14f, .5f), P(.14f, .62f)); g.DrawLine(pen, P(.86f, .5f), P(.86f, .62f)); break; case "slash": g.DrawLine(pen, P(.14f, .14f), P(.86f, .86f)); break; case "plus": g.DrawLine(pen, P(.5f, .22f), P(.5f, .78f)); g.DrawLine(pen, P(.22f, .5f), P(.78f, .5f)); break; } } g.Restore(st); } } // Plutajuci dock na dnu: ikone u mjehuricima koji narastu pod misem, odabrana je u prijelazu boje class DockBar : Control { public static readonly string[] Keys = { "home", "messages", "friends", "voice", "servers", "clans", "lfg", "|", "notifs", "settings" }; static readonly string[] Icons = { "home", "chat", "people", "mic", "server", "shield", "target", "", "bell", "gear" }; static readonly string[] Names = { "Početna", "Poruke", "Prijatelji", "Voice", "Serveri", "Klanovi", "Traže ekipu", "", "Obavijesti", "Postavke" }; public readonly int[] Badges = new int[Keys.Length]; public string Selected = "home"; public bool VoiceOn; public event Action Picked; readonly float[] grow = new float[Keys.Length]; readonly Timer anim = new Timer { Interval = 15 }; readonly ToolTip tt = new ToolTip(); int hot = -1, tipFor = -2; public int Cell { get { return Theme.D(56); } } public int Sep { get { return Theme.D(18); } } public int PreferredWidth { get { return (Keys.Length - 1) * Cell + Sep + Theme.D(20); } } public DockBar() { SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); Cursor = Cursors.Hand; anim.Tick += (s, e) => { bool moving = false; for (int i = 0; i < grow.Length; i++) { float t = i == hot ? 1f : 0f; if (Math.Abs(grow[i] - t) > 0.01f) { grow[i] += (t - grow[i]) * 0.3f; moving = true; } else grow[i] = t; } Invalidate(); if (!moving) anim.Stop(); }; Disposed += (s, e) => anim.Dispose(); } Rectangle CellRect(int i) { int x = Theme.D(10); for (int k = 0; k < i; k++) x += Keys[k] == "|" ? Sep : Cell; return new Rectangle(x, 0, Keys[i] == "|" ? Sep : Cell, Height); } int Hit(Point p) { for (int i = 0; i < Keys.Length; i++) if (Keys[i] != "|" && CellRect(i).Contains(p)) return i; return -1; } protected override void OnMouseMove(MouseEventArgs e) { int h = Hit(e.Location); if (h != hot) { hot = h; anim.Start(); } if (h != tipFor) { tipFor = h; tt.SetToolTip(this, h >= 0 ? Names[h] : null); } base.OnMouseMove(e); } protected override void OnMouseLeave(EventArgs e) { hot = -1; anim.Start(); base.OnMouseLeave(e); } protected override void OnMouseClick(MouseEventArgs e) { int h = Hit(e.Location); if (h >= 0 && Picked != null) Picked(Keys[h]); base.OnMouseClick(e); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; var bg = Fx.Mix(Theme.Panel, Color.White, Theme.Light ? 0 : 0.03f); g.Clear(bg); var all = new RectangleF(0, 0, Width - 1, Height - 1); using (var p = Theme.Round(all, Height / 2f)) { Fx.Gloss(g, p, all, 18); using (var br = new LinearGradientBrush(new RectangleF(-1, -1, Width + 2, Height + 2), Color.FromArgb(110, Theme.Accent), Color.FromArgb(110, Theme.Accent2), 0f)) using (var pen = new Pen(br, 1.2f)) g.DrawPath(pen, p); } for (int i = 0; i < Keys.Length; i++) { var c = CellRect(i); if (Keys[i] == "|") { using (var pen = new Pen(Fx.Mix(bg, Theme.Text, 0.15f))) g.DrawLine(pen, c.X + c.Width / 2, Height * 0.28f, c.X + c.Width / 2, Height * 0.72f); continue; } bool sel = Keys[i] == Selected; float d = Theme.D(42) + Theme.D(8) * grow[i]; var b = new RectangleF(c.X + (c.Width - d) / 2f, (Height - d) / 2f - Theme.D(3) * grow[i], d, d); if (sel) { using (var sh = new SolidBrush(Color.FromArgb(70, Theme.Accent2))) g.FillEllipse(sh, b.X + 1, b.Y + Theme.D(3), b.Width - 2, b.Height - 2); using (var br = new LinearGradientBrush(new RectangleF(b.X - 1, b.Y - 1, b.Width + 2, b.Height + 2), Theme.Accent, Theme.Accent2, 45f)) g.FillEllipse(br, b); using (var p = new GraphicsPath()) { p.AddEllipse(b); Fx.Gloss(g, p, b, 40); } } else if (grow[i] > 0.01f) using (var br = new SolidBrush(Color.FromArgb((int)(255 * grow[i]), Fx.Mix(bg, Theme.Light ? Color.Black : Color.White, 0.09f)))) g.FillEllipse(br, b); var ic = sel ? Color.White : Fx.Mix(Theme.Muted, Theme.Text, grow[i]); if (Keys[i] == "voice" && VoiceOn && !sel) ic = Theme.On; float isz = d * 0.54f; Vec.Draw(g, Icons[i], new RectangleF(b.X + (d - isz) / 2, b.Y + (d - isz) / 2, isz, isz), ic, Math.Max(1.5f, Theme.D(1.8f))); if (Keys[i] == "voice" && VoiceOn) using (var pen = new Pen(Theme.On, Theme.D(2))) g.DrawEllipse(pen, b.X - Theme.D(2), b.Y - Theme.D(2), b.Width + Theme.D(4), b.Height + Theme.D(4)); if (Badges[i] > 0) { string t = Badges[i] > 99 ? "99+" : Badges[i].ToString(); var pr = new Rectangle((int)(b.Right - Theme.D(14)), (int)b.Y - Theme.D(2), Theme.D(t.Length > 1 ? 24 : 18), Theme.D(18)); using (var p = Theme.Round(pr, pr.Height / 2f)) { using (var br = new SolidBrush(Theme.Red)) g.FillPath(br, p); using (var pen = new Pen(bg, Theme.D(2))) g.DrawPath(pen, p); } using (var f = Theme.Body(10.5f, FontStyle.Bold)) TextRenderer.DrawText(g, t, f, pr, Color.White, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding); } if (sel) using (var br = new SolidBrush(Theme.Accent2)) g.FillEllipse(br, c.X + c.Width / 2f - Theme.D(2), Height - Theme.D(7), Theme.D(4), Theme.D(4)); } } } // Prijatelji kao mjehurici avatara s prstenom statusa (u igri = prsten u prijelazu boje) class FriendOrbit : Control { readonly TrayApp app; List friends = new List(); int requests, scroll, hot = -1; bool hotAdd, hotReq; readonly List> cells = new List>(); Rectangle addR, reqR; int contentH; readonly ToolTip tt = new ToolTip(); Friend tipFor; public FriendOrbit(TrayApp app) { this.app = app; SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); BackColor = Theme.Panel; } public void SetData(List f, int req) { friends = f ?? new List(); requests = req; Invalidate(); } int HitCell(Point p) { for (int i = 0; i < cells.Count; i++) if (cells[i].Item1.Contains(p)) return i; return -1; } protected override void OnMouseMove(MouseEventArgs e) { int h = HitCell(e.Location); bool a = addR.Contains(e.Location), q = reqR.Contains(e.Location); if (h != hot || a != hotAdd || q != hotReq) { hot = h; hotAdd = a; hotReq = q; Cursor = h >= 0 || a || q ? Cursors.Hand : Cursors.Default; Invalidate(); } var f = h >= 0 ? cells[h].Item2 : null; if (f != tipFor) { tipFor = f; tt.SetToolTip(this, f == null ? null : f.Username + " · " + (f.Status == "play" && !string.IsNullOrEmpty(f.Game) ? "igra " + f.Game : f.Kind == "off" ? "offline" : f.Kind == "dnd" ? "ne smetaj" : f.Kind == "idle" ? "odsutan" : "online")); } base.OnMouseMove(e); } protected override void OnMouseLeave(EventArgs e) { hot = -1; hotAdd = hotReq = false; Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseWheel(MouseEventArgs e) { scroll = Math.Max(0, Math.Min(Math.Max(0, contentH - Height), scroll - e.Delta / 2)); Invalidate(); base.OnMouseWheel(e); } protected override void OnMouseUp(MouseEventArgs e) { if (addR.Contains(e.Location) || reqR.Contains(e.Location)) { app.ShowAddFriend(); return; } int h = HitCell(e.Location); if (h < 0) { base.OnMouseUp(e); return; } var name = cells[h].Item2.Username; if (e.Button == MouseButtons.Right) { var menu = Ui.Menu(); menu.Items.Add(new ToolStripMenuItem("Pošalji poruku", null, (a, b) => app.OpenChatByName(name))); menu.Items.Add(new ToolStripMenuItem("Pozovi", null, (a, b) => app.CallByName(name))); menu.Items.Add(new ToolStripMenuItem("Otvori profil", null, (a, b) => app.Open("/profile/" + Uri.EscapeDataString(name)))); menu.Items.Add(new ToolStripSeparator()); menu.Items.Add(new ToolStripMenuItem("Ukloni prijatelja", null, (a, b) => app.RemoveFriend(name)) { ForeColor = Theme.Red }); menu.Show(this, e.Location); } else if (e.Button == MouseButtons.Left) app.OpenChatByName(name); base.OnMouseUp(e); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(Theme.Panel); cells.Clear(); int pad = Theme.D(20); int online = friends.Count(f => f.Kind != "off"); // zaglavlje using (var f = Theme.Display(17)) TextRenderer.DrawText(g, "Prijatelji", f, new Point(pad, Theme.D(18)), Theme.Text, TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding); using (var f = Theme.Body(12)) TextRenderer.DrawText(g, online + " online od " + friends.Count, f, new Point(pad, Theme.D(44)), Theme.Muted, TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding); addR = new Rectangle(Width - pad - Theme.D(36), Theme.D(18), Theme.D(36), Theme.D(36)); using (var br = new LinearGradientBrush(addR, hotAdd ? Theme.Shade(Theme.Accent, 0.1f) : Theme.Accent, hotAdd ? Theme.Shade(Theme.Accent2, 0.1f) : Theme.Accent2, 45f)) g.FillEllipse(br, addR); Vec.Draw(g, "plus", new RectangleF(addR.X + Theme.D(9), addR.Y + Theme.D(9), Theme.D(18), Theme.D(18)), Color.White, Theme.D(2)); int y = Theme.D(76); reqR = Rectangle.Empty; if (requests > 0) { reqR = new Rectangle(pad, y, Width - pad * 2, Theme.D(34)); using (var p = Theme.Round(reqR, reqR.Height / 2f)) using (var br = new SolidBrush(Fx.Mix(Theme.Panel, Theme.Accent, hotReq ? 0.3f : 0.18f))) g.FillPath(br, p); using (var f = Theme.Body(12.5f, FontStyle.Bold)) TextRenderer.DrawText(g, requests == 1 ? "1 novi zahtjev za prijateljstvo" : requests + " nova zahtjeva za prijateljstvo", f, reqR, Theme.Text, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPrefix); y += Theme.D(46); } int top = y; var clip = new Rectangle(0, top, Width, Height - top); g.SetClip(clip); y -= scroll; var groups = new[] { Tuple.Create("U IGRI", friends.Where(f => f.Status == "play").ToList()), Tuple.Create("ONLINE", friends.Where(f => f.Kind != "off" && f.Status != "play").ToList()), Tuple.Create("OFFLINE", friends.Where(f => f.Kind == "off").ToList()), }; int cw = Theme.D(78), ch = Theme.D(92), cols = Math.Max(1, (Width - pad * 2 + Theme.D(4)) / cw); int gapX = cols > 1 ? (Width - pad * 2 - cols * cw) / (cols - 1) : 0; foreach (var grp in groups) { if (grp.Item2.Count == 0) continue; using (var f = Theme.Body(11.5f, FontStyle.Bold)) TextRenderer.DrawText(g, grp.Item1 + " " + grp.Item2.Count, f, new Point(pad, y), Theme.Muted, TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding); y += Theme.D(24); for (int i = 0; i < grp.Item2.Count; i++) { var fr = grp.Item2[i]; var cell = new Rectangle(pad + (i % cols) * (cw + gapX), y + (i / cols) * ch, cw, ch - Theme.D(6)); cells.Add(Tuple.Create(cell, fr)); bool h = cells.Count - 1 == hot; if (h) using (var p = Theme.Round(cell, Theme.D(14))) using (var br = new SolidBrush(Fx.Mix(Theme.Panel, Theme.Light ? Color.Black : Color.White, 0.06f))) g.FillPath(br, p); float av = Theme.D(50); var ar = new RectangleF(cell.X + (cell.Width - av) / 2f, cell.Y + Theme.D(8), av, av); var ring = ar; ring.Inflate(Theme.D(4), Theme.D(4)); bool off = fr.Kind == "off"; if (fr.Status == "play") using (var br = new LinearGradientBrush(ring, Theme.Accent, Theme.Accent2, 45f)) using (var pen = new Pen(br, Theme.D(2.5f))) g.DrawEllipse(pen, ring); else if (!off) using (var pen = new Pen(fr.Kind == "dnd" ? Theme.Dnd : fr.Kind == "idle" ? Theme.Idle : Theme.On, Theme.D(2.5f))) g.DrawEllipse(pen, ring); Theme.Avatar(g, ar, fr.Color, fr.Username, fr.Avatar, this, null, Theme.Panel); if (off) using (var br = new SolidBrush(Color.FromArgb(120, Theme.Panel))) g.FillEllipse(br, ar); if (fr.Unread > 0) { var pr = new Rectangle((int)ar.Right - Theme.D(12), (int)ar.Y - Theme.D(4), Theme.D(18), Theme.D(18)); using (var br = new SolidBrush(Theme.Red)) g.FillEllipse(br, pr); using (var f = Theme.Body(10, FontStyle.Bold)) TextRenderer.DrawText(g, fr.Unread > 9 ? "9+" : fr.Unread.ToString(), f, pr, Color.White, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding); } using (var f = Theme.Body(11.5f)) TextRenderer.DrawText(g, fr.Username, f, new Rectangle(cell.X, (int)ring.Bottom + Theme.D(4), cell.Width, Theme.D(16)), off ? Theme.Muted : Theme.Text, TextFormatFlags.HorizontalCenter | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding); if (fr.Status == "play" && !string.IsNullOrEmpty(fr.Game)) using (var f = Theme.Body(10)) TextRenderer.DrawText(g, fr.Game, f, new Rectangle(cell.X, (int)ring.Bottom + Theme.D(19), cell.Width, Theme.D(14)), Theme.Accent2, TextFormatFlags.HorizontalCenter | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding); } y += ((grp.Item2.Count + cols - 1) / cols) * ch + Theme.D(10); } if (friends.Count == 0) using (var f = Theme.Body(12.5f)) TextRenderer.DrawText(g, "Još nemaš prijatelja.\nKlikni + i dodaj nekoga.", f, new Rectangle(pad, y + Theme.D(20), Width - pad * 2, Theme.D(50)), Theme.Muted, TextFormatFlags.HorizontalCenter | TextFormatFlags.WordBreak | TextFormatFlags.NoPrefix); contentH = y + scroll - top + Theme.D(20) + top; g.ResetClip(); } } // Raspored "Dock": nema gornje trake ni bocnog popisa. Sadrzaj i prijatelji plutaju kao // zaobljeni paneli na zivoj pozadini, a navigacija je plutajuci dock na dnu. class OrbitForm : ShellForm { readonly UpdateBar updateBar = new UpdateBar { Visible = false }; readonly DockBar dockBar = new DockBar(); readonly ProfileChip chip = new ProfileChip(); readonly FriendOrbit orbit; readonly DashHome home; readonly MessagesPage messages; readonly FriendsPage friendsPg; readonly VoiceView voiceData; readonly VoiceSpace voiceSpace; readonly HeaderPage serversPg, settingsPg, notifsPg, lfgPg; readonly NotifList notifList; readonly LfgList lfgList; readonly ClansPage clans; readonly SettingsView settings; readonly VoiceDock voiceDock; readonly Dictionary pages = new Dictionary(); readonly Timer tick = new Timer { Interval = 250 }; Bitmap backdrop; string page = "home"; int ticks; public override UserPanel User { get { return chip; } } public override VoiceView Voice { get { return voiceData; } } public OrbitForm(TrayApp app) : base(app) { Text = "Fragline"; FormBorderStyle = FormBorderStyle.Sizable; MaximizeBox = true; DoubleBuffered = true; BackColor = Theme.Canvas; MinimumSize = new Size(Theme.D(1000), Theme.D(640)); var wa = Screen.PrimaryScreen.WorkingArea; ClientSize = new Size(Math.Min(wa.Width - Theme.D(40), Prefs.Int("neo_w", Theme.D(1320))), Math.Min(wa.Height - Theme.D(60), Prefs.Int("neo_h", Theme.D(820)))); orbit = new FriendOrbit(app); home = new DashHome(app); home.Navigate += k => Show2(k); messages = new MessagesPage(app); friendsPg = new FriendsPage(app); // popis soba (VoiceView) prima podatke od aplikacije, a prikazuje ih novi VoiceSpace voiceData = new VoiceView(app); voiceSpace = new VoiceSpace(app, voiceData); serversPg = new HeaderPage(new PageHeader { Glyph = Theme.IcoGame, Title = "Serveri", Sub = "Game serveri, ping i igrači" }, new ServersView(app)); clans = new ClansPage(app); settings = new SettingsView(app); settingsPg = new HeaderPage(new PageHeader { Glyph = Theme.IcoGear, Title = "Postavke" }, settings); notifList = new NotifList(); notifsPg = new HeaderPage(new PageHeader { Glyph = "", Title = "Obavijesti", Sub = "Komentari, lajkovi i spomeni" }, notifList); lfgList = new LfgList(app); lfgPg = new HeaderPage(new PageHeader { Glyph = "", Title = "Traže ekipu", Sub = "Oglasi se vide i na webu" }, lfgList); voiceDock = new VoiceDock(app) { Visible = false }; pages["home"] = home; pages["messages"] = messages; pages["friends"] = friendsPg; pages["voice"] = voiceSpace; pages["servers"] = serversPg; pages["clans"] = clans; pages["lfg"] = lfgPg; pages["settings"] = settingsPg; pages["notifs"] = notifsPg; foreach (var p in pages.Values) p.Visible = p == home; Controls.AddRange(new Control[] { updateBar, chip, orbit, dockBar, voiceDock }); Controls.AddRange(pages.Values.ToArray()); dockBar.BringToFront(); voiceDock.BringToFront(); dockBar.Picked += k => Show2(k); chip.ProfileClicked += pt => StatusMenu(chip, pt); voiceDock.Open += () => ShowVoice(); updateBar.UpdateClicked += () => app.DoUpdate(); updateBar.Closed += () => { updateBar.Visible = false; DoLayout(); }; tick.Tick += (s, e) => Tick(); tick.Start(); DoLayout(); } // ziva pozadina: tamni prijelaz i dva meka obojana sjaja, nacrtana jednom po velicini prozora void BuildBackdrop() { int W = ClientSize.Width, H = ClientSize.Height; if (W <= 0 || H <= 0) return; var old = backdrop; backdrop = new Bitmap(W, H); using (var g = Graphics.FromImage(backdrop)) { g.SmoothingMode = SmoothingMode.AntiAlias; using (var b = new LinearGradientBrush(new Rectangle(0, 0, W, H), Theme.Shade(Theme.Bg, -0.25f), Theme.Shade(Theme.Bg, -0.5f), 70f)) g.FillRectangle(b, 0, 0, W, H); Glow(g, new PointF(W * 0.18f, H * 0.12f), Math.Max(W, H) * 0.55f, Theme.Accent, 70); Glow(g, new PointF(W * 0.92f, H * 0.95f), Math.Max(W, H) * 0.5f, Theme.Accent2, 60); Glow(g, new PointF(W * 0.62f, H * 0.4f), Math.Max(W, H) * 0.3f, Theme.Accent, 20); } if (old != null) old.Dispose(); } static void Backing(Graphics g, Control c, int rad, Color col) { if (!c.Visible || c.Width <= 0) return; var r = new RectangleF(c.Left - 0.5f, c.Top - 0.5f, c.Width + 1f, c.Height + 1f); using (var p = Theme.Round(r, rad + 0.5f)) using (var b = new SolidBrush(col)) g.FillPath(b, p); } static void Glow(Graphics g, PointF c, float r, Color col, int alpha) { using (var p = new GraphicsPath()) { p.AddEllipse(c.X - r, c.Y - r, r * 2, r * 2); using (var b = new PathGradientBrush(p) { CenterColor = Color.FromArgb(alpha, col), SurroundColors = new[] { Color.FromArgb(0, col) } }) g.FillPath(b, p); } } protected override void OnPaintBackground(PaintEventArgs e) { if (backdrop == null || backdrop.Width != ClientSize.Width || backdrop.Height != ClientSize.Height) BuildBackdrop(); if (backdrop != null) e.Graphics.DrawImageUnscaled(backdrop, 0, 0); else base.OnPaintBackground(e); var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; // glatki rubovi: Region panela je uvijek "stepenast", pa ispod svakog panela nacrtaj // zagladjeni zaobljeni oblik iste boje; djelomicno pokriveni rubni pikseli se tako stope s pozadinom int rad = Theme.D(24); Backing(g, chip, Theme.D(22), Theme.Panel); Backing(g, dockBar, dockBar.Height / 2, Fx.Mix(Theme.Panel, Color.White, Theme.Light ? 0 : 0.03f)); if (orbit.Visible) Backing(g, orbit, rad, Theme.Panel); Control cur; if (pages.TryGetValue(page, out cur)) Backing(g, cur, rad, page == "messages" ? Theme.Panel : cur.BackColor); // znak i natpis gore lijevo, izravno na pozadini int top = updateBar.Visible ? Theme.D(36) : 0; Theme.DrawFlame(g, new RectangleF(Theme.D(26), top + Theme.D(18), Theme.D(22), Theme.D(25))); Theme.Wordmark(g, Theme.D(56), top + Theme.D(31), 20, Theme.Text); string t = PageTitle(); using (var f = Theme.Body(12.5f, FontStyle.Bold)) { int x = Theme.D(56) + Theme.D(118); using (var b = new SolidBrush(Fx.Mix(Theme.Bg, Theme.Text, 0.25f))) g.FillEllipse(b, x - Theme.D(14), top + Theme.D(29), Theme.D(4), Theme.D(4)); TextRenderer.DrawText(g, t.ToUpperInvariant(), f, new Point(x, top + Theme.D(23)), Theme.Muted, TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding); } } string PageTitle() { int i = Array.IndexOf(DockBar.Keys, page); string[] n = { "Početna", "Poruke", "Prijatelji", "Voice", "Serveri", "Klanovi", "Traže ekipu", "", "Obavijesti", "Postavke" }; return i >= 0 ? n[i] : ""; } protected override void OnResize(EventArgs e) { base.OnResize(e); DoLayout(); Invalidate(); } void DoLayout() { if (pages.Count == 0 || voiceDock == null) return; int W = ClientSize.Width, H = ClientSize.Height, g = Theme.D(18), rad = Theme.D(24); if (W <= 0 || H <= 0) return; int top = updateBar.Visible ? Theme.D(36) : 0; updateBar.SetBounds(0, 0, W, top); int cw = Theme.D(210); chip.SetBounds(W - cw - g, top + Theme.D(12), cw, Theme.D(44)); Theme.RoundRegion(chip, Theme.D(22)); int y = top + Theme.D(68); int dh = Theme.D(70), dw = Math.Min(dockBar.PreferredWidth, W - g * 2); int dy = H - dh - Theme.D(16); dockBar.SetBounds((W - dw) / 2, dy, dw, dh); Theme.RoundRegion(dockBar, dh / 2); int bottom = dy - Theme.D(16); bool showOrbit = W >= Theme.D(1150); int ow = showOrbit ? Theme.D(300) : 0; orbit.Visible = showOrbit; if (showOrbit) { orbit.SetBounds(W - ow - g, y, ow, bottom - y); Theme.RoundRegion(orbit, rad); } var cr = new Rectangle(g, y, Math.Max(10, W - g * 2 - (showOrbit ? ow + g : 0)), Math.Max(10, bottom - y)); foreach (var p in pages.Values) { p.Bounds = cr; Theme.RoundRegion(p, rad); } int vw = Math.Min(Theme.D(420), cr.Width - Theme.D(40)), vh = Theme.D(62); voiceDock.SetBounds(cr.X + (cr.Width - vw) / 2, cr.Bottom - vh - Theme.D(16), vw, vh); Invalidate(); } void Show2(string k) { if (k == "notifs") { ShowNotifs(); return; } if (!pages.ContainsKey(k)) return; page = k; foreach (var kv in pages) kv.Value.Visible = kv.Key == k; dockBar.Selected = k; dockBar.Invalidate(); UpdateVoiceDock(); Invalidate(); } void UpdateVoiceDock() { var vc = App.Voice; bool show = vc != null && page != "voice"; if (voiceDock.Visible != show) { voiceDock.Visible = show; if (show) voiceDock.BringToFront(); } } void Tick() { ticks++; var vc = App.Voice; bool on = vc != null; UpdateVoiceDock(); if (dockBar.VoiceOn != on) { dockBar.VoiceOn = on; dockBar.Invalidate(); } if (on) { int n = vc.LiveMembers().Count; if (voiceDock.Room != vc.Label || voiceDock.Talking != vc.Talking || voiceDock.Members != n || voiceDock.Mic.Off != vc.Muted || voiceDock.Deaf.Off != vc.Deafened) { voiceDock.Room = vc.Label; voiceDock.Talking = vc.Talking; voiceDock.Members = n; voiceDock.Mic.Off = vc.Muted; voiceDock.Deaf.Off = vc.Deafened; voiceDock.Invalidate(); voiceDock.Mic.Invalidate(); voiceDock.Deaf.Invalidate(); } } bool muted = on ? vc.Muted : Prefs.Bool("muted", false), deaf = on && vc.Deafened, idle = App.AutoIdle; if (chip.Muted != muted || chip.Deaf != deaf || chip.Idle != idle) { chip.Muted = muted; chip.Deaf = deaf; chip.Idle = idle; chip.Invalidate(); } if (ticks % 20 == 0 && Visible) { orbit.Invalidate(); friendsPg.Refresh2(); home.Invalidate(); } } void Badge(string key, int n) { int i = Array.IndexOf(DockBar.Keys, key); if (i >= 0 && dockBar.Badges[i] != n) { dockBar.Badges[i] = n; dockBar.Invalidate(); } } public override void SetFriends(List list, int requests) { orbit.SetData(list, requests); messages.List.SetData(list, requests); friendsPg.SetData(list, requests); home.SetFriends(list); foreach (var f in list) messages.Chat.UpdateFriend(f); Badge("messages", list.Sum(f => f.Unread)); Badge("friends", requests); } public override void SetRequests(List r) { friendsPg.SetRequests(r); } public override void SetClans(List c) { clans.Cards.SetData(c); home.SetClans(c.Count); } public override void SetShot(string game, string url) { settings.SetShot(game, url); } public override void ShowUpdate(string text) { updateBar.Message = text; updateBar.Busy = false; updateBar.Visible = true; updateBar.Invalidate(); DoLayout(); Invalidate(); } public override void SetUpdateText(string text, bool busy) { updateBar.Message = text; updateBar.Busy = busy; if (!updateBar.Visible) { updateBar.Visible = true; DoLayout(); Invalidate(); } updateBar.Invalidate(); } public override void OpenChat(Friend f) { Show2("messages"); messages.Chat.Open(f); messages.ShowChat(true); messages.List.Select("dm:" + f.Username); messages.Chat.FocusInput(); } public override bool ChatActive(string u) { return Visible && WindowState != FormWindowState.Minimized && ContainsFocus && page == "messages" && messages.Chat.FriendName == u; } public override void CloseChat(string user) { if (user != null && messages.Chat.FriendName != user) return; messages.Chat.Reset(); messages.ShowChat(false); } public override void ShowVoice() { Show2("voice"); } public override void ShowAddFriend() { Show2("friends"); friendsPg.ShowAdd(); } public override void ShowSettings() { Show2("settings"); } public override void ShowServers() { Show2("servers"); } public override void ShowLfg() { Show2("lfg"); } public override void ShowNotifs() { page = "notifs"; foreach (var kv in pages) kv.Value.Visible = kv.Key == "notifs"; dockBar.Selected = "notifs"; dockBar.Invalidate(); UpdateVoiceDock(); Invalidate(); App.MarkNotifsSeen(); } public override void SetNotifs(int unread, List items) { notifList.SetData(items); Badge("notifs", unread); } public override void SetLfg(List items) { lfgList.SetData(items); home.SetLfg(items); } protected override void OnFormClosing(FormClosingEventArgs e) { if (WindowState == FormWindowState.Normal) { Prefs.Set("neo_w", ClientSize.Width); Prefs.Set("neo_h", ClientSize.Height); } if (!AllowClose && e.CloseReason == CloseReason.UserClosing) { e.Cancel = true; Hide(); App.HiddenHint(); } base.OnFormClosing(e); } } } namespace Fragline { // Voice prostor (Dock raspored). Izvan sobe: sobe kao kartice s avatarima ljudi unutra. // U sobi: sudionici kao veliki krugovi koji svijetle dok pricaju i plutajuca traka s kontrolama. class VoiceSpace : Control { readonly TrayApp app; readonly VoiceView data; readonly Panel tools = new Panel(); readonly Toggle ptt = new Toggle(); readonly DropDown key = new DropDown(); readonly RangeSlider meter = new RangeSlider { Min = -80, Max = 0, Format = v => (int)Math.Round(v) + " dB" }; readonly Label pttLbl; readonly FlatButton adv = new FlatButton("Napredno", false), newRoom = new FlatButton("Nova soba", true); readonly Timer tick = new Timer { Interval = 80 }; readonly Dictionary hits = new Dictionary(); List members = new List(); readonly List> tiles = new List>(); string hot; float phase; bool syncing, inRoom; public VoiceSpace(TrayApp app, VoiceView data) { this.app = app; this.data = data; SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); BackColor = Theme.Bg; tools.BackColor = Theme.Panel; pttLbl = Ui.Lbl("Pritisni za pričanje", 13, Theme.Text2, 0, 0); pttLbl.BackColor = Theme.Panel; Ui.Combo(key); foreach (var k in VoiceView.PttKeys) key.Items.Add(k.Name); adv.Font = Theme.Body(13, FontStyle.Bold); adv.Click += (s, e) => app.ShowAudioSettings(); newRoom.Glyph = null; newRoom.Click += (s, e) => app.ShowCreateRoom(); tools.Controls.AddRange(new Control[] { pttLbl, ptt, key, meter, adv }); Controls.AddRange(new Control[] { tools, newRoom }); syncing = true; ptt.Checked = AudioCfg.Ptt; int idx = Array.FindIndex(VoiceView.PttKeys, k => k.Vk == AudioCfg.PttKey); key.SelectedIndex = idx >= 0 ? idx : 0; key.Enabled = AudioCfg.Ptt; meter.Value = AudioCfg.SensDb; syncing = false; ptt.CheckedChanged += (s, e) => { AudioCfg.Ptt = ptt.Checked; key.Enabled = ptt.Checked; }; key.SelectedIndexChanged += (s, e) => { if (!syncing && key.SelectedIndex >= 0) AudioCfg.PttKey = VoiceView.PttKeys[key.SelectedIndex].Vk; }; meter.ValueChanged += (s, e) => { if (!syncing) AudioCfg.SensDb = meter.Value; }; tick.Tick += (s, e) => Tick(); tick.Start(); Disposed += (s, e) => tick.Dispose(); } // donja traka s postavkama (u sobi je dio plutajuce trake s gumbima) Rectangle BarRect() { int w = Math.Min(Width - Theme.D(40), Theme.D(inRoom ? 900 : 760)), h = Theme.D(68); return new Rectangle((Width - w) / 2, Height - h - Theme.D(20), w, h); } protected override void OnResize(EventArgs e) { base.OnResize(e); Place(); } void Place() { if (tools == null || pttLbl == null) return; var br = BarRect(); int lead = inRoom ? Theme.D(3 * 56 + 30) : Theme.D(10); // unutarnji pravokutnik bez zaobljenih krajeva, iste boje kao traka tools.SetBounds(br.X + br.Height / 2 + lead - Theme.D(10), br.Y + Theme.D(6), Math.Max(10, br.Width - br.Height - lead + Theme.D(10)), br.Height - Theme.D(12)); int y = (tools.Height - Theme.D(38)) / 2, x = 0; pttLbl.Location = new Point(x, (tools.Height - pttLbl.PreferredHeight) / 2); x += pttLbl.PreferredWidth + Theme.D(8); ptt.Location = new Point(x, (tools.Height - ptt.Height) / 2); x += ptt.Width + Theme.D(10); key.SetBounds(x, y, Theme.D(118), Theme.D(38)); x += Theme.D(128); int aw = Theme.D(104); adv.SetBounds(tools.Width - aw, y, aw, Theme.D(38)); meter.SetBounds(x, y + Theme.D(2), Math.Max(Theme.D(60), adv.Left - x - Theme.D(10)), Theme.D(34)); newRoom.SetBounds(Width - Theme.D(28) - Theme.D(140), Theme.D(26), Theme.D(140), Theme.D(40)); newRoom.Visible = !inRoom; Invalidate(); } List Members(VoiceClient vc) { var known = new Dictionary(); foreach (var rm in data.Rooms) foreach (var m in rm.Members) if (m.Name != null) known[m.Name] = m; var res = new List { new VMember { Name = (app.Username ?? "Ti") + " (ti)", Color = app.ColorHex, Avatar = app.AvatarUrl, Speaking = vc.Talking } }; foreach (var m in vc.LiveMembers()) { VMember k; if (known.TryGetValue(m.Name ?? "", out k)) { m.Color = k.Color; m.Avatar = k.Avatar; } res.Add(m); } return res; } void Tick() { if (!Visible) return; var vc = app.Voice; bool on = vc != null; if (on != inRoom) { inRoom = on; Place(); } members = on ? Members(vc) : new List(); phase += 0.12f; var eng = vc != null ? vc.Audio : null; meter.Locked = AudioCfg.AutoSens || AudioCfg.Ptt; meter.Level = eng != null ? eng.LevelDb : float.NaN; meter.Marker = eng != null ? eng.ThresholdDb : float.NaN; if (!meter.Dragging) meter.Value = AudioCfg.SensDb; meter.Invalidate(); Invalidate(); } protected override void OnVisibleChanged(EventArgs e) { if (Visible) { syncing = true; ptt.Checked = AudioCfg.Ptt; key.Enabled = AudioCfg.Ptt; int idx = Array.FindIndex(VoiceView.PttKeys, k => k.Vk == AudioCfg.PttKey); if (idx >= 0) key.SelectedIndex = idx; syncing = false; } base.OnVisibleChanged(e); } string Hit(Point p) { foreach (var kv in hits) if (kv.Value.Contains(p)) return kv.Key; return null; } protected override void OnMouseMove(MouseEventArgs e) { var h = Hit(e.Location); if (h != hot) { hot = h; Cursor = h != null ? Cursors.Hand : Cursors.Default; Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseLeave(EventArgs e) { hot = null; Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseUp(MouseEventArgs e) { if (e.Button == MouseButtons.Right) { foreach (var t in tiles) if (t.Item1.Contains(e.Location)) { VoiceView.ShowMemberMenu(app, t.Item2, this, e.Location); return; } var hr = Hit(e.Location); var rm = hr != null && hr.StartsWith("room:") ? data.Rooms.FirstOrDefault(x => "room:" + x.Key == hr) : null; if (rm != null && rm.Mine && rm.Custom) { var menu = Ui.Menu(); menu.Items.Add(new ToolStripMenuItem("Obriši sobu", null, (a, b) => app.DeleteRoom(rm)) { ForeColor = Theme.Red }); menu.Show(this, e.Location); } return; } if (e.Button != MouseButtons.Left) return; var h = Hit(e.Location); if (h == null) return; if (h == "mic") app.ToggleMute(); else if (h == "deaf") app.ToggleDeaf(); else if (h == "leave") app.LeaveVoice(); else if (h.StartsWith("room:")) { var rm = data.Rooms.FirstOrDefault(x => "room:" + x.Key == h); if (rm != null && (app.Voice == null || app.Voice.Room != rm.Key)) app.JoinVoice(rm); } base.OnMouseUp(e); } static string RoomKind(VRoom rm) { if (!rm.Custom) return rm.Key != null && rm.Key.StartsWith("clan") ? "Klan" : "Glavna soba"; return rm.Privacy == "friends" ? "Samo prijatelji" : rm.Privacy == "clan" ? "Klan" : "Javna soba"; } void RoundCtl(Graphics g, string key, Rectangle r, string icon, bool off, bool danger) { hits[key] = r; bool h = hot == key; Color bg = danger ? Theme.Red : off ? Color.White : Fx.Mix(Theme.Panel, Color.White, h ? 0.16f : 0.09f); if (h && (danger || off)) bg = Theme.Shade(bg, -0.1f); using (var b = new SolidBrush(bg)) g.FillEllipse(b, r); var fc = off && !danger ? Theme.Red : Color.White; float s = r.Width * 0.46f; var ir = new RectangleF(r.X + (r.Width - s) / 2, r.Y + (r.Height - s) / 2, s, s); Vec.Draw(g, icon, ir, fc, Theme.D(1.8f)); if (off && !danger) Vec.Draw(g, "slash", ir, fc, Theme.D(2)); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(Theme.Bg); hits.Clear(); tiles.Clear(); int pad = Theme.D(28); var vc = app.Voice; var bar = BarRect(); if (vc == null) { using (var f = Theme.Display(22)) TextRenderer.DrawText(g, "Voice sobe", f, new Point(pad, Theme.D(24)), Theme.Text, TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding); var st = string.IsNullOrEmpty(data.StatusText) ? "Klikni sobu da uđeš." : data.StatusText; using (var f = Theme.Body(13)) TextRenderer.DrawText(g, st, f, new Point(pad, Theme.D(56)), data.StatusColor == Theme.Muted ? Theme.Muted : data.StatusColor, TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding); var rooms = data.Rooms; int top = Theme.D(98), gap = Theme.D(16), W = Width - pad * 2; int cols = Math.Max(1, (W + gap) / (Theme.D(250) + gap)); int cw = (W - gap * (cols - 1)) / cols, ch = Theme.D(150); if (rooms.Count == 0) using (var f = Theme.Body(13.5f)) TextRenderer.DrawText(g, data.Loaded ? "Nema soba." : "Učitavam sobe...", f, new Rectangle(pad, top + Theme.D(20), W, Theme.D(24)), Theme.Muted, TextFormatFlags.HorizontalCenter | TextFormatFlags.NoPrefix); for (int i = 0; i < rooms.Count; i++) { var rm = rooms[i]; var r = new Rectangle(pad + (i % cols) * (cw + gap), top + (i / cols) * (ch + gap), cw, ch); if (r.Bottom > bar.Y - Theme.D(10)) break; string k = "room:" + rm.Key; hits[k] = r; bool h = hot == k; Theme.Card(g, r, Theme.D(18), h ? Fx.Mix(Theme.Panel, Color.White, 0.05f) : Theme.Panel); if (h) using (var p = Theme.Round(r, Theme.D(18))) Fx.GlowBorder(g, p, r, 1f, 1.5f); var ic = new Rectangle(r.X + Theme.D(16), r.Y + Theme.D(16), Theme.D(40), Theme.D(40)); using (var b = new LinearGradientBrush(ic, Theme.Accent, Theme.Accent2, 45f)) g.FillEllipse(b, ic); Vec.Draw(g, rm.Custom ? "headset" : "mic", new RectangleF(ic.X + Theme.D(10), ic.Y + Theme.D(10), Theme.D(20), Theme.D(20)), Color.White, Theme.D(1.7f)); using (var f = Theme.Display(15)) TextRenderer.DrawText(g, rm.Label ?? rm.Key, f, new Rectangle(ic.Right + Theme.D(12), r.Y + Theme.D(16), r.Right - ic.Right - Theme.D(28), Theme.D(22)), Theme.Text, TextFormatFlags.NoPrefix | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPadding); using (var f = Theme.Body(12)) TextRenderer.DrawText(g, RoomKind(rm) + (rm.Limit > 0 ? " · do " + rm.Limit : ""), f, new Rectangle(ic.Right + Theme.D(12), r.Y + Theme.D(40), r.Right - ic.Right - Theme.D(28), Theme.D(18)), Theme.Muted, TextFormatFlags.NoPrefix | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPadding); // tko je unutra: avatari koji se preklapaju int ay = r.Bottom - Theme.D(52), ax = r.X + Theme.D(16), asz = Theme.D(30); if (rm.Members.Count == 0) using (var f = Theme.Body(12.5f)) TextRenderer.DrawText(g, "Prazno, budi prvi", f, new Rectangle(ax, ay, cw - Theme.D(130), asz), Theme.Muted, TextFormatFlags.VerticalCenter | TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding); for (int m = 0; m < rm.Members.Count && m < 5; m++) { var mm = rm.Members[m]; var ar = new RectangleF(ax + m * (asz - Theme.D(8)), ay, asz, asz); using (var b = new SolidBrush(h ? Fx.Mix(Theme.Panel, Color.White, 0.05f) : Theme.Panel)) g.FillEllipse(b, ar.X - Theme.D(2), ar.Y - Theme.D(2), asz + Theme.D(4), asz + Theme.D(4)); Theme.Avatar(g, ar, mm.Color, mm.Name, mm.Avatar, this, null, Theme.Panel); } if (rm.Members.Count > 5) using (var f = Theme.Body(12, FontStyle.Bold)) TextRenderer.DrawText(g, "+" + (rm.Members.Count - 5), f, new Point(ax + 5 * (asz - Theme.D(8)) + Theme.D(8), ay + Theme.D(7)), Theme.Text2, TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding); var jr = new Rectangle(r.Right - Theme.D(16) - Theme.D(84), r.Bottom - Theme.D(50), Theme.D(84), Theme.D(32)); using (var p = Theme.Round(jr, jr.Height / 2f)) { if (h) using (var b = new LinearGradientBrush(jr, Theme.Accent, Theme.Accent2, 0f)) g.FillPath(b, p); else using (var pen = new Pen(Fx.Mix(Theme.Panel, Color.White, 0.2f))) g.DrawPath(pen, p); } using (var f = Theme.Display(13)) TextRenderer.DrawText(g, "Uđi", f, jr, h ? Color.White : Theme.Text2, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPrefix); } } else { // naslov sobe using (var f = Theme.Display(22)) TextRenderer.DrawText(g, vc.Label ?? "Voice", f, new Point(pad, Theme.D(24)), Theme.Text, TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding); string sub = vc.Deafened ? "zvuk isključen" : vc.Muted ? "mikrofon isključen" : vc.Talking ? "pričaš" : "spojen"; var sc = vc.Muted || vc.Deafened ? Theme.Red : Theme.On; using (var b = new SolidBrush(sc)) g.FillEllipse(b, pad, Theme.D(62), Theme.D(8), Theme.D(8)); using (var f = Theme.Body(13)) TextRenderer.DrawText(g, sub + " · " + members.Count + (members.Count == 1 ? " osoba" : " osobe") + " u sobi", f, new Point(pad + Theme.D(16), Theme.D(56)), Theme.Muted, TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding); // sudionici: krugovi, svijetle dok pricaju int n = members.Count, top = Theme.D(100), bottom = bar.Y - Theme.D(24); int areaW = Width - pad * 2, areaH = Math.Max(Theme.D(80), bottom - top); int cols = n <= 1 ? 1 : n <= 4 ? n : n <= 6 ? 3 : n <= 12 ? 4 : 6; int rowsN = Math.Max(1, (n + cols - 1) / cols); float d = Math.Min(Theme.D(140), Math.Min(areaW / (float)cols - Theme.D(40), areaH / (float)rowsN - Theme.D(56))); d = Math.Max(Theme.D(48), d); float cellW = d + Theme.D(40), cellH = d + Theme.D(56); float oy = top + (areaH - rowsN * cellH) / 2f; for (int i = 0; i < n; i++) { var m = members[i]; int rI = i / cols, inRow = Math.Min(cols, n - rI * cols); float ox = (Width - inRow * cellW) / 2f; var cell = new RectangleF(ox + (i % cols) * cellW, oy + rI * cellH, cellW, cellH); var ar = new RectangleF(cell.X + (cellW - d) / 2, cell.Y, d, d); tiles.Add(Tuple.Create(Rectangle.Round(cell), m)); if (m.Speaking) { float pulse = (float)(Math.Sin(phase * 2) * 0.5 + 0.5); for (int k = 3; k >= 1; k--) { var gr = ar; gr.Inflate(Theme.D(4 + k * 4) * (0.6f + 0.4f * pulse), Theme.D(4 + k * 4) * (0.6f + 0.4f * pulse)); using (var b = new SolidBrush(Color.FromArgb(18 + 10 * (3 - k), Theme.Accent2))) g.FillEllipse(b, gr); } var rr = ar; rr.Inflate(Theme.D(4), Theme.D(4)); using (var b = new LinearGradientBrush(rr, Theme.Accent, Theme.Accent2, 45f)) using (var pen = new Pen(b, Theme.D(3))) g.DrawEllipse(pen, rr); } else { var rr = ar; rr.Inflate(Theme.D(4), Theme.D(4)); using (var pen = new Pen(Fx.Mix(Theme.Bg, Color.White, 0.1f), Theme.D(2))) g.DrawEllipse(pen, rr); } Theme.Avatar(g, ar, m.Color, (m.Name ?? "?").Replace(" (ti)", ""), m.Avatar, this, null, Theme.Bg); bool localMuted = m.Name != null && !m.Name.EndsWith(" (ti)") && AudioCfg.UserMuted(m.Name); using (var f = Theme.Display(14)) TextRenderer.DrawText(g, m.Name ?? "?", f, new Rectangle((int)cell.X, (int)(ar.Bottom + Theme.D(12)), (int)cellW, Theme.D(22)), m.Speaking ? Theme.Text : Theme.Text2, TextFormatFlags.HorizontalCenter | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix); if (localMuted) { var mr = new RectangleF(ar.Right - Theme.D(26), ar.Bottom - Theme.D(26), Theme.D(26), Theme.D(26)); using (var b = new SolidBrush(Theme.Red)) g.FillEllipse(b, mr); var ir = new RectangleF(mr.X + Theme.D(6), mr.Y + Theme.D(6), Theme.D(14), Theme.D(14)); Vec.Draw(g, "headset", ir, Color.White, Theme.D(1.4f)); Vec.Draw(g, "slash", ir, Color.White, Theme.D(1.6f)); } } } // plutajuca traka: u sobi gumbi za mikrofon, zvuk i izlaz, pa postavke pricanja using (var p = Theme.Round(new Rectangle(bar.X, bar.Y + Theme.D(4), bar.Width, bar.Height), bar.Height / 2f)) using (var b = new SolidBrush(Color.FromArgb(70, 0, 0, 0))) g.FillPath(b, p); using (var p = Theme.Round(bar, bar.Height / 2f)) { using (var b = new SolidBrush(Theme.Panel)) g.FillPath(b, p); using (var pen = new Pen(Fx.Mix(Theme.Panel, Color.White, 0.08f))) g.DrawPath(pen, p); } if (vc != null) { int bs = Theme.D(48), by = bar.Y + (bar.Height - bs) / 2, bx = bar.X + Theme.D(12); RoundCtl(g, "mic", new Rectangle(bx, by, bs, bs), "mic", vc.Muted, false); RoundCtl(g, "deaf", new Rectangle(bx + bs + Theme.D(8), by, bs, bs), "headset", vc.Deafened, false); RoundCtl(g, "leave", new Rectangle(bx + (bs + Theme.D(8)) * 2, by, bs, bs), "hangup", false, true); using (var pen = new Pen(Fx.Mix(Theme.Panel, Color.White, 0.12f))) g.DrawLine(pen, bx + (bs + Theme.D(8)) * 3 + Theme.D(4), bar.Y + Theme.D(16), bx + (bs + Theme.D(8)) * 3 + Theme.D(4), bar.Bottom - Theme.D(16)); } } } } // ===================== Dash.cs ===================== using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; using System.Windows.Forms; namespace Fragline { // Pocetna slozena kao naslovnica weba: veliki pozdravni blok (tekst, gumbi, statistika) // s karticama "Upravo igraju" desno, a ispod oglasi "Traze ekipu". Nista vise. class DashHome : UserControl { readonly TrayApp app; List friends = new List(); List lfg = new List(); int clanCount; string hot; readonly Dictionary hits = new Dictionary(); public event Action Navigate; const TextFormatFlags Mid = TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding; const TextFormatFlags Left = TextFormatFlags.VerticalCenter | TextFormatFlags.NoPrefix | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPadding; public DashHome(TrayApp app) { this.app = app; BackColor = Theme.Bg; SetStyle(ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.ResizeRedraw, true); } public void SetFriends(List f) { friends = f ?? new List(); Invalidate(); } public void SetClans(int n) { clanCount = n; Invalidate(); } public void SetLfg(List l) { lfg = l ?? new List(); Invalidate(); } string Hit(Point p) { foreach (var kv in hits) if (kv.Value.Contains(p)) return kv.Key; return null; } protected override void OnMouseMove(MouseEventArgs e) { var h = Hit(e.Location); if (h != hot) { hot = h; Cursor = h != null ? Cursors.Hand : Cursors.Default; Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseLeave(EventArgs e) { hot = null; Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseClick(MouseEventArgs e) { var h = Hit(e.Location); if (h == null) { base.OnMouseClick(e); return; } if (h == "new") app.NewLfg(); else if (h == "servers" || h == "lfg") { if (Navigate != null) Navigate(h); } else if (h.StartsWith("p:")) app.OpenChatByName(h.Substring(2)); else if (h.StartsWith("j:")) { long id = long.Parse(h.Substring(2)); var it = lfg.FirstOrDefault(x => x.Id == id); if (it != null) { if (it.Mine) app.LfgAction(id, true); else if (it.JoinedByMe) app.LfgAction(id, false); else app.JoinLfg(id); } } base.OnMouseClick(e); } void Pill(Graphics g, string key, Rectangle r, string text, bool solid, Color fg) { hits[key] = r; bool h = hot == key; using (var p = Theme.Round(r, r.Height / 2f)) { if (solid) using (var b = new SolidBrush(h ? Color.FromArgb(230, 255, 255, 255) : Color.White)) g.FillPath(b, p); else { if (h) using (var b = new SolidBrush(Color.FromArgb(40, 255, 255, 255))) g.FillPath(b, p); using (var pen = new Pen(Color.FromArgb(h ? 255 : 170, 255, 255, 255), Theme.D(1.5f))) g.DrawPath(pen, p); } } using (var f = Theme.Display(13.5f)) TextRenderer.DrawText(g, text, f, r, fg, Mid); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(Theme.Bg); hits.Clear(); int pad = Theme.D(24), W = Width - pad * 2; if (W < Theme.D(300)) return; float rad = Theme.D(16); var playing = friends.Where(f => f.Status == "play").ToList(); int online = friends.Count(f => f.Kind != "off"); // ---------- pozdravni blok (kao "hero" na webu) ---------- var hero = new Rectangle(pad, pad, W, Theme.D(296)); using (var p = Theme.Round(new Rectangle(hero.X, hero.Y + Theme.D(6), hero.Width, hero.Height), rad)) using (var b = new SolidBrush(Color.FromArgb(60, Theme.Accent2))) g.FillPath(b, p); using (var p = Theme.Round(hero, rad)) { using (var b = new LinearGradientBrush(hero, Theme.Accent, Theme.Accent2, 20f)) g.FillPath(b, p); var st = g.Save(); g.SetClip(p); using (var b = new SolidBrush(Color.FromArgb(22, 255, 255, 255))) g.FillEllipse(b, hero.Right - hero.Height * 1.4f, hero.Y - hero.Height * 0.6f, hero.Height * 1.6f, hero.Height * 1.6f); g.Restore(st); } bool showLive = W > Theme.D(720); int liveW = Math.Min(Theme.D(330), (int)(W * 0.4f)); int tx = hero.X + Theme.D(30), tw = (showLive ? hero.Width - liveW - Theme.D(40) : hero.Width) - Theme.D(60); int y = hero.Y + Theme.D(26); string live = playing.Count == 0 ? "Nitko od prijatelja ne igra" : playing.Count == 1 ? "1 prijatelj je u igri" : playing.Count + " prijatelja je u igri"; using (var f = Theme.Body(12, FontStyle.Bold)) { int lw = TextRenderer.MeasureText(live, f).Width + Theme.D(34); var lr = new Rectangle(tx, y, lw, Theme.D(26)); using (var p = Theme.Round(lr, lr.Height / 2f)) using (var b = new SolidBrush(Color.FromArgb(45, 0, 0, 0))) g.FillPath(b, p); using (var b = new SolidBrush(playing.Count > 0 ? Color.FromArgb(120, 255, 140) : Color.FromArgb(200, 255, 255, 255))) g.FillEllipse(b, lr.X + Theme.D(12), lr.Y + Theme.D(9), Theme.D(8), Theme.D(8)); TextRenderer.DrawText(g, live, f, new Rectangle(lr.X + Theme.D(26), lr.Y, lw - Theme.D(30), lr.Height), Color.White, Left); } y += Theme.D(40); using (var f = Theme.Display(28)) TextRenderer.DrawText(g, "Bok, " + (app.Username ?? "igraču") + "!", f, new Rectangle(tx, y, tw, Theme.D(40)), Color.White, Left); y += Theme.D(42); string lead = app.CurrentGame != null ? "Upravo igraš " + app.CurrentGame + ". Treba ti ekipa? Objavi oglas, vidjet će ga svi u aplikaciji i na webu." : "Traži ekipu, pronađi server ili se javi prijateljima koji su online."; using (var f = Theme.Body(13.5f)) TextRenderer.DrawText(g, lead, f, new Rectangle(tx, y, tw, Theme.D(40)), Color.FromArgb(235, 255, 255, 255), TextFormatFlags.WordBreak | TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding); y += Theme.D(48); using (var f = Theme.Display(13.5f)) { int b1 = TextRenderer.MeasureText("Objavi oglas", f).Width + Theme.D(40), b2 = TextRenderer.MeasureText("Pronađi server", f).Width + Theme.D(40); Pill(g, "new", new Rectangle(tx, y, b1, Theme.D(40)), "Objavi oglas", true, Theme.Shade(Theme.Accent, -0.25f)); Pill(g, "servers", new Rectangle(tx + b1 + Theme.D(10), y, b2, Theme.D(40)), "Pronađi server", false, Color.White); } int sy = y + Theme.D(62), sx = tx; var stats = new[] { Tuple.Create(online.ToString(), "online"), Tuple.Create(playing.Count.ToString(), "u igri"), Tuple.Create(lfg.Count.ToString(), lfg.Count == 1 ? "oglas" : "oglasa"), Tuple.Create(clanCount.ToString(), clanCount == 1 ? "klan" : "klanova") }; foreach (var s in stats) { if (sx > tx + tw - Theme.D(60)) break; using (var f = Theme.Display(20)) TextRenderer.DrawText(g, s.Item1, f, new Point(sx, sy), Color.White, TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding); using (var f = Theme.Body(11.5f)) TextRenderer.DrawText(g, s.Item2, f, new Point(sx, sy + Theme.D(28)), Color.FromArgb(210, 255, 255, 255), TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding); sx += Theme.D(86); } // desno u bloku: "Upravo igraju" (ili tko je online kad nitko ne igra) if (showLive) { var lr = new Rectangle(hero.Right - liveW - Theme.D(22), hero.Y + Theme.D(22), liveW, hero.Height - Theme.D(44)); int ch = Theme.D(46), cg = Theme.D(6); var list = playing.Count > 0 ? playing : friends.Where(f => f.Kind != "off").ToList(); int max = Math.Max(1, (lr.Height + cg) / (ch + cg)); if (list.Count == 0) { var r = new Rectangle(lr.X, lr.Y, lr.Width, ch + Theme.D(10)); using (var p = Theme.Round(r, Theme.D(12))) using (var b = new SolidBrush(Color.FromArgb(50, 0, 0, 0))) g.FillPath(b, p); using (var f = Theme.Display(13.5f)) TextRenderer.DrawText(g, "Nitko nije online", f, new Rectangle(r.X + Theme.D(16), r.Y + Theme.D(6), r.Width - Theme.D(32), Theme.D(22)), Color.White, Left); using (var f = Theme.Body(12)) TextRenderer.DrawText(g, "Dodaj prijatelje da ih vidiš ovdje.", f, new Rectangle(r.X + Theme.D(16), r.Y + Theme.D(28), r.Width - Theme.D(32), Theme.D(18)), Color.FromArgb(210, 255, 255, 255), Left); } for (int i = 0; i < list.Count && i < max; i++) { var fr = list[i]; var r = new Rectangle(lr.X, lr.Y + i * (ch + cg), lr.Width, ch); string key = "p:" + fr.Username; hits[key] = r; using (var p = Theme.Round(r, Theme.D(12))) using (var b = new SolidBrush(Color.FromArgb(hot == key ? 80 : 50, 0, 0, 0))) g.FillPath(b, p); float av = Theme.D(32); Theme.Avatar(g, new RectangleF(r.X + Theme.D(8), r.Y + (ch - av) / 2f, av, av), fr.Color, fr.Username, fr.Avatar, this, fr.Kind, Theme.Shade(Theme.Accent, -0.2f)); int nx = r.X + Theme.D(50); using (var f = Theme.Display(13.5f)) TextRenderer.DrawText(g, fr.Username, f, new Rectangle(nx, r.Y + Theme.D(5), r.Width - Theme.D(60), Theme.D(20)), Color.White, Left); string sub = fr.Status == "play" && !string.IsNullOrEmpty(fr.Game) ? "igra " + fr.Game : fr.Kind == "dnd" ? "ne smetaj" : fr.Kind == "idle" ? "odsutan" : "online"; using (var f = Theme.Body(11.5f)) TextRenderer.DrawText(g, sub, f, new Rectangle(nx, r.Y + Theme.D(24), r.Width - Theme.D(60), Theme.D(17)), Color.FromArgb(215, 255, 255, 255), Left); } } // ---------- traze ekipu ---------- y = hero.Bottom + Theme.D(30); using (var f = Theme.Body(12, FontStyle.Bold)) TextRenderer.DrawText(g, "TRAŽE EKIPU", f, new Point(pad, y), Theme.Muted, TextFormatFlags.NoPrefix); using (var f = Theme.Body(12.5f, FontStyle.Bold)) { string all = "Svi oglasi ›"; int aw = TextRenderer.MeasureText(all, f).Width + Theme.D(8); var ar = new Rectangle(pad + W - aw, y - Theme.D(3), aw, Theme.D(22)); hits["lfg"] = ar; TextRenderer.DrawText(g, all, f, ar, hot == "lfg" ? Theme.Text : Theme.Link, Mid); } y += Theme.D(28); if (lfg.Count == 0) { var r = new Rectangle(pad, y, W, Theme.D(96)); Theme.Card(g, r, rad, Theme.Panel); using (var f = Theme.Display(15)) TextRenderer.DrawText(g, "Trenutno nitko ne traži ekipu", f, new Rectangle(r.X, r.Y + Theme.D(20), r.Width, Theme.D(24)), Theme.Text, Mid); using (var f = Theme.Body(12.5f)) TextRenderer.DrawText(g, "Budi prvi. Oglas se odmah vidi i na webu.", f, new Rectangle(r.X, r.Y + Theme.D(50), r.Width, Theme.D(20)), Theme.Muted, Mid); return; } int cols = W > Theme.D(900) ? 3 : W > Theme.D(560) ? 2 : 1, gap = Theme.D(16); int cw = (W - gap * (cols - 1)) / cols, chh = Theme.D(150); for (int i = 0; i < lfg.Count && i < cols; i++) { var it = lfg[i]; var r = new Rectangle(pad + i * (cw + gap), y, cw, chh); Theme.Card(g, r, rad, Theme.Panel); int x = r.X + Theme.D(18), iw = r.Width - Theme.D(36); using (var f = Theme.Display(15)) TextRenderer.DrawText(g, string.IsNullOrEmpty(it.GameName) ? it.Game : it.GameName, f, new Rectangle(x, r.Y + Theme.D(14), iw, Theme.D(22)), Theme.Text, Left); using (var f = Theme.Body(12)) TextRenderer.DrawText(g, (it.Mine ? "tvoj oglas" : it.Author) + " · još " + it.ExpiresMin + " min", f, new Rectangle(x, r.Y + Theme.D(38), iw, Theme.D(18)), Theme.Muted, Left); using (var f = Theme.Body(13)) TextRenderer.DrawText(g, string.IsNullOrEmpty(it.Note) ? "Bez poruke" : it.Note, f, new Rectangle(x, r.Y + Theme.D(62), iw, Theme.D(36)), string.IsNullOrEmpty(it.Note) ? Theme.Muted : Theme.Text2, TextFormatFlags.WordBreak | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding); // popunjenost: kruzici za mjesta int taken = it.Joined.Count, slots = Math.Max(1, it.Slots), dy = r.Bottom - Theme.D(30); for (int s = 0; s < slots && s < 9; s++) { var dr = new Rectangle(x + s * Theme.D(15), dy + Theme.D(5), Theme.D(10), Theme.D(10)); if (s < taken) using (var b = new LinearGradientBrush(dr, Theme.Accent, Theme.Accent2, 0f)) g.FillEllipse(b, dr); else using (var pen = new Pen(Theme.Muted, Theme.D(1.2f))) g.DrawEllipse(pen, dr); } var br = new Rectangle(r.Right - Theme.D(122), r.Bottom - Theme.D(44), Theme.D(104), Theme.D(32)); string key = "j:" + it.Id; hits[key] = br; bool h = hot == key; using (var p = Theme.Round(br, br.Height / 2f)) { if (it.Mine || it.JoinedByMe) { var c = it.Mine ? Theme.Red : Theme.Muted; if (h) using (var b = new SolidBrush(Color.FromArgb(40, c))) g.FillPath(b, p); using (var pen = new Pen(c, Theme.D(1.5f))) g.DrawPath(pen, p); } else using (var b = new LinearGradientBrush(br, h ? Theme.Shade(Theme.Accent, 0.1f) : Theme.Accent, h ? Theme.Shade(Theme.Accent2, 0.1f) : Theme.Accent2, 0f)) g.FillPath(b, p); } using (var f = Theme.Display(13)) TextRenderer.DrawText(g, it.Mine ? "Zatvori" : it.JoinedByMe ? "Odjavi se" : "Javi se", f, br, it.Mine ? Theme.Red : it.JoinedByMe ? Theme.Text2 : Color.White, Mid); } } } } // ===================== WebForm.cs ===================== using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; using System.Windows.Forms; namespace Fragline { // Raspored identičan webu (resources/views/app.blade.php + fragline.css): // gornja traka (logo | status + ja) i tri stupca — Prijatelji | Chat | Profil. // Dodatne značajke desktopa (voice, serveri, klanovi, LFG, obavijesti, postavke) // otvaraju se u središnjem stupcu, pa aplikacija zadrži web izgled. class WebForm : ShellForm { readonly UpdateBar updateBar = new UpdateBar { Visible = false }; readonly WebTop top; readonly ProfileChip chip = new ProfileChip(); readonly SocialPanel social; readonly WebProfile profile; readonly Panel center = new Panel(); // središnji sadržaji readonly Panel chatHost = new Panel(); readonly ChatView chat; readonly Label chatEmpty = new Label(); readonly VoiceHub voiceHub; readonly ServersView serversView; readonly ClansPage clansPage; readonly SettingsView settings; readonly NotifList notifList; readonly LfgList lfgList; readonly Dictionary wrap = new Dictionary(); readonly Timer tick = new Timer { Interval = 250 }; int ticks; public override UserPanel User { get { return chip; } } public override VoiceView Voice { get { return voiceHub.Rooms; } } public WebForm(TrayApp app) : base(app) { Text = "Fragline"; FormBorderStyle = FormBorderStyle.Sizable; MaximizeBox = true; BackColor = Theme.Bg; MinimumSize = new Size(Theme.D(900), Theme.D(600)); var wa = Screen.PrimaryScreen.WorkingArea; ClientSize = new Size(Math.Min(wa.Width - Theme.D(40), Prefs.Int("web_w", Theme.D(1300))), Math.Min(wa.Height - Theme.D(60), Prefs.Int("web_h", Theme.D(820)))); social = new SocialPanel(app); chat = new ChatView(app); profile = new WebProfile(app, key => ShowCenter(key)); chatEmpty.Dock = DockStyle.Fill; chatEmpty.TextAlign = ContentAlignment.MiddleCenter; chatEmpty.Font = Theme.Body(15); chatEmpty.ForeColor = Theme.Muted; chatEmpty.BackColor = Theme.Bg2; chatEmpty.Text = "Odaberi prijatelja\nDodirni nekoga s liste da otvoriš chat."; chatHost.BackColor = Theme.Bg2; chat.Dock = DockStyle.Fill; chat.Visible = false; chatHost.Controls.Add(chat); chatHost.Controls.Add(chatEmpty); voiceHub = new VoiceHub(app); serversView = new ServersView(app); clansPage = new ClansPage(app); settings = new SettingsView(app); notifList = new NotifList(); lfgList = new LfgList(app); wrap["chat"] = chatHost; wrap["voice"] = Wrapped(voiceHub); wrap["servers"] = Wrapped(serversView); wrap["clans"] = Wrapped(clansPage); wrap["settings"] = Wrapped(settings); wrap["notifs"] = Wrapped(notifList); wrap["lfg"] = Wrapped(lfgList); center.BackColor = Theme.Bg; foreach (var c in wrap.Values) { c.Visible = false; center.Controls.Add(c); } top = new WebTop(app, chip); top.BellClicked += () => ShowNotifs(); top.GearClicked += () => ShowSettings(); Controls.Add(center); Controls.Add(social); Controls.Add(profile); Controls.Add(chip); Controls.Add(top); Controls.Add(updateBar); chip.BringToFront(); chip.ProfileClicked += pt => StatusMenu(chip, pt); updateBar.UpdateClicked += () => app.DoUpdate(); updateBar.Closed += () => { updateBar.Visible = false; DoLayout(); }; tick.Tick += (s, e) => Tick(); tick.Start(); ShowCenter("chat"); DoLayout(); } Control Wrapped(Control inner) { inner.Dock = DockStyle.Fill; var p = new Panel { BackColor = Theme.Panel }; p.Controls.Add(inner); return p; } string curCenter = "chat"; void ShowCenter(string key) { if (!wrap.ContainsKey(key)) return; curCenter = key; foreach (var kv in wrap) kv.Value.Visible = kv.Key == key; if (key == "notifs") App.MarkNotifsSeen(); var card = wrap[key]; card.BringToFront(); Theme.RoundRegion(card, Theme.D(20)); } protected override void OnResize(EventArgs e) { base.OnResize(e); DoLayout(); } void DoLayout() { int W = ClientSize.Width, H = ClientSize.Height, g = Theme.D(12); if (W <= 0 || H <= 0) return; int ub = updateBar.Visible ? Theme.D(36) : 0; updateBar.SetBounds(0, 0, W, ub); int th = Theme.D(60); top.SetBounds(0, ub, W, th); // me chip gore desno unutar trake int cw = Theme.D(210); chip.SetBounds(W - cw - Theme.D(16), ub + (th - Theme.D(42)) / 2, cw, Theme.D(42)); chip.BringToFront(); int y = ub + th + g, bh = Math.Max(10, H - y - g); int lw = Theme.D(320), rw = Theme.D(340); // na užem prozoru sakrij desni stupac bool wide = W >= Theme.D(1060); if (!wide) rw = 0; social.SetBounds(g, y, lw, bh); Theme.RoundRegion(social, Theme.D(20)); int cx = g + lw + g; int cwid = Math.Max(10, W - cx - g - (rw > 0 ? rw + g : 0)); center.SetBounds(cx, y, cwid, bh); foreach (var c in wrap.Values) { c.Bounds = new Rectangle(0, 0, center.Width, center.Height); Theme.RoundRegion(c, Theme.D(20)); } profile.Visible = rw > 0; if (rw > 0) { profile.SetBounds(W - rw - g, y, rw, bh); Theme.RoundRegion(profile, Theme.D(20)); } } void Tick() { ticks++; bool muted = App.Voice != null ? App.Voice.Muted : Prefs.Bool("muted", false); bool deaf = App.Voice != null && App.Voice.Deafened, idle = App.AutoIdle; if (chip.Muted != muted || chip.Deaf != deaf || chip.Idle != idle) { chip.Muted = muted; chip.Deaf = deaf; chip.Idle = idle; chip.Invalidate(); } if (ticks % 20 == 0 && Visible) { social.List.Refresh2(); profile.Invalidate(); } } // ---------- ShellForm ugovor ---------- public override void SetFriends(List list, int requests) { social.SetData(list); foreach (var f in list) chat.UpdateFriend(f); top.Requests = requests; top.Invalidate(); } public override void SetRequests(List r) { social.List.SetRequests(r); } public override void SetClans(List c) { clansPage.Cards.SetData(c); } public override void SetShot(string game, string url) { settings.SetShot(game, url); } public override void ShowUpdate(string text) { updateBar.Message = text; updateBar.Busy = false; updateBar.Visible = true; updateBar.Invalidate(); DoLayout(); } public override void SetUpdateText(string text, bool busy) { updateBar.Message = text; updateBar.Busy = busy; if (!updateBar.Visible) { updateBar.Visible = true; DoLayout(); } updateBar.Invalidate(); } public override void OpenChat(Friend f) { ShowCenter("chat"); chat.Visible = true; chatEmpty.Visible = false; chat.Open(f); chat.FocusInput(); } public override bool ChatActive(string u) { return Visible && WindowState != FormWindowState.Minimized && ContainsFocus && curCenter == "chat" && chat.Visible && chat.FriendName == u; } public override void CloseChat(string user) { if (user != null && chat.FriendName != user) return; chat.Reset(); chat.Visible = false; chatEmpty.Visible = true; } public override void ShowVoice() { ShowCenter("voice"); } public override void ShowAddFriend() { App.ShowAddFriend(); } public override void ShowSettings() { ShowCenter("settings"); } public override void ShowNotifs() { ShowCenter("notifs"); } public override void ShowLfg() { ShowCenter("lfg"); } public void ShowServers() { ShowCenter("servers"); } public void ShowClans() { ShowCenter("clans"); } public override void SetNotifs(int unread, List items) { notifList.SetData(items); top.NotifUnread = unread; top.Invalidate(); } public override void SetLfg(List items) { lfgList.SetData(items); } protected override void OnFormClosing(FormClosingEventArgs e) { if (WindowState == FormWindowState.Normal) { Prefs.Set("web_w", ClientSize.Width); Prefs.Set("web_h", ClientSize.Height); } if (!AllowClose && e.CloseReason == CloseReason.UserClosing) { e.Cancel = true; Hide(); App.HiddenHint(); } base.OnFormClosing(e); } } // Gornja traka: logo lijevo, zvonce + postavke desno (me chip dodaje forma) class WebTop : Control { readonly TrayApp app; readonly Control chip; Rectangle bell, gear; int hot = -1; readonly ToolTip tt = new ToolTip(); public int NotifUnread, Requests; public event Action BellClicked, GearClicked; public WebTop(TrayApp app, Control chip) { this.app = app; this.chip = chip; SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); Cursor = Cursors.Hand; } int Hit(Point p) { if (bell.Contains(p)) return 1; if (gear.Contains(p)) return 2; return -1; } protected override void OnMouseMove(MouseEventArgs e) { int h = Hit(e.Location); if (h != hot) { hot = h; tt.SetToolTip(this, h == 1 ? "Obavijesti" : h == 2 ? "Postavke" : null); Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseLeave(EventArgs e) { hot = -1; Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseClick(MouseEventArgs e) { int h = Hit(e.Location); if (h == 1 && BellClicked != null) BellClicked(); else if (h == 2 && GearClicked != null) GearClicked(); base.OnMouseClick(e); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(Theme.Panel); using (var pen = new Pen(Theme.Line)) g.DrawLine(pen, 0, Height - 1, Width, Height - 1); Theme.DrawFlame(g, new RectangleF(Theme.D(18), (Height - Theme.D(26)) / 2f, Theme.D(23), Theme.D(26))); Theme.Wordmark(g, Theme.D(50), Height / 2f, 21, Theme.Text); int s = Theme.D(38); // desno od chipa (chip je širine ~210 + 16 margine) int rx = Width - Theme.D(210) - Theme.D(16) - Theme.D(10); gear = new Rectangle(rx - s, (Height - s) / 2, s, s); bell = new Rectangle(gear.X - s - Theme.D(8), gear.Y, s, s); foreach (var t in new[] { Tuple.Create(bell, 1, ""), Tuple.Create(gear, 2, Theme.IcoGear) }) { if (hot == t.Item2) using (var b = new SolidBrush(Theme.Panel2)) g.FillEllipse(b, t.Item1); Theme.Icon(g, t.Item3, t.Item2 == 1 ? "!" : "", t.Item1, hot == t.Item2 ? Theme.Text : Theme.Muted, 16); } if (NotifUnread > 0) Theme.Pill(g, NotifUnread > 99 ? "99+" : NotifUnread.ToString(), new Rectangle(bell.Right - Theme.D(18), bell.Y + Theme.D(2), Theme.D(18), Theme.D(14)), true, Theme.Panel); } } // Desni stupac "Profil": ja + brze poveznice na ostale značajke class WebProfile : Control { readonly TrayApp app; readonly Action go; readonly List> items = new List>(); int hover = -1; public WebProfile(TrayApp app, Action go) { this.app = app; this.go = go; SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); BackColor = Theme.Panel; } int Hit(Point p) { for (int i = 0; i < items.Count; i++) if (items[i].Item1.Contains(p)) return i; return -1; } protected override void OnMouseMove(MouseEventArgs e) { int h = Hit(e.Location); if (h != hover) { hover = h; Cursor = h >= 0 ? Cursors.Hand : Cursors.Default; Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseLeave(EventArgs e) { hover = -1; Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseClick(MouseEventArgs e) { int h = Hit(e.Location); if (h >= 0) go(items[h].Item3); base.OnMouseClick(e); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(Theme.Panel); int pad = Theme.D(16), W = Width; // zaglavlje: avatar + ime + status float av = Theme.D(64); Theme.Avatar(g, new RectangleF(pad, Theme.D(18), av, av), app.ColorHex, app.Username, app.AvatarUrl, this, "online", Theme.Panel); using (var f = Theme.Display(19)) Txt.Draw(g, app.Username ?? "", f, new Rectangle(pad + (int)av + Theme.D(14), Theme.D(24), W - pad * 2 - (int)av - Theme.D(14), Theme.D(26)), Theme.Text); using (var f = Theme.Body(13)) Txt.Draw(g, app.CurrentGame != null ? "Igra " + app.CurrentGame : "Online", f, new Rectangle(pad + (int)av + Theme.D(14), Theme.D(50), W - pad * 2 - (int)av - Theme.D(14), Theme.D(20)), app.CurrentGame != null ? Theme.Play : Theme.Muted); using (var pen = new Pen(Theme.Line)) g.DrawLine(pen, pad, Theme.D(98), W - pad, Theme.D(98)); using (var f = Theme.Body(12.5f, FontStyle.Bold)) Txt.Draw(g, "BRZE POVEZNICE", f, new Rectangle(pad, Theme.D(110), W - pad * 2, Theme.D(18)), Theme.Muted); items.Clear(); var links = new[] { Tuple.Create(Theme.IcoSpeaker, "Voice", "voice"), Tuple.Create(Theme.IcoGame, "Serveri", "servers"), Tuple.Create("", "Klanovi", "clans"), Tuple.Create("", "LFG", "lfg"), Tuple.Create("", "Obavijesti", "notifs"), Tuple.Create(Theme.IcoGear, "Postavke", "settings"), }; int y = Theme.D(136), rh = Theme.D(44); for (int i = 0; i < links.Length; i++) { var r = new Rectangle(pad, y, W - pad * 2, rh); items.Add(Tuple.Create(r, links[i].Item2, links[i].Item3)); if (i == hover) using (var p = Theme.Round(r, Theme.D(12))) using (var b = new SolidBrush(Theme.Panel2)) g.FillPath(b, p); Theme.Icon(g, links[i].Item1, "•", new Rectangle(r.X + Theme.D(10), r.Y, Theme.D(22), rh), i == hover ? Theme.Accent : Theme.Muted, 16); using (var f = Theme.Display(14)) Txt.Draw(g, links[i].Item2, f, new Rectangle(r.X + Theme.D(42), r.Y, r.Width - Theme.D(50), rh), i == hover ? Theme.Text : Theme.Text2); y += rh + Theme.D(4); } } } } // ===================== Xfire.cs ===================== using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; using System.Windows.Forms; namespace Fragline { // Klasicni raspored u stilu Xfirea: uski prozor s popisom prijatelja, sjajne plave trake, chat u zasebnim prozorima static class Xf { public static readonly Color Head1 = ColorTranslator.FromHtml("#3a5478"), Head2 = ColorTranslator.FromHtml("#16223a"), Bar1 = ColorTranslator.FromHtml("#2b3f5e"), Bar2 = ColorTranslator.FromHtml("#172338"), Game = ColorTranslator.FromHtml("#ffa040"), Voice = ColorTranslator.FromHtml("#6fb4ff"), Border = ColorTranslator.FromHtml("#4a6a95"), Orange = ColorTranslator.FromHtml("#ff6a1a"); static string face = "Tahoma"; public static void Init() { if (!FontFamily.Families.Any(f => f.Name == "Tahoma")) face = "Segoe UI"; } public static Font Font(float px, FontStyle st = FontStyle.Regular) { return new Font(face, px * Theme.S, st, GraphicsUnit.Pixel); } // Sjajna traka: gradijent odozgo prema dolje i svijetli odsjaj na gornjoj polovici public static void Gloss(Graphics g, Rectangle r, Color top, Color bottom) { if (r.Width <= 0 || r.Height <= 0) return; using (var b = new LinearGradientBrush(new Rectangle(r.X, r.Y - 1, r.Width, r.Height + 2), top, bottom, 90f)) g.FillRectangle(b, r); using (var b = new SolidBrush(Color.FromArgb(22, 255, 255, 255))) g.FillRectangle(b, r.X, r.Y, r.Width, r.Height / 2); using (var pen = new Pen(Color.FromArgb(50, 255, 255, 255))) g.DrawLine(pen, r.X, r.Y, r.Right, r.Y); using (var pen = new Pen(Color.FromArgb(160, 0, 0, 0))) g.DrawLine(pen, r.X, r.Bottom - 1, r.Right, r.Bottom - 1); } // Kvadratni avatar s obrubom (Xfire nije imao okrugle slike) public static void Avatar(Graphics g, Rectangle r, string colorHex, string name, string url, Control notify, string kind, Color ringBg) { var st = g.Save(); g.SmoothingMode = SmoothingMode.AntiAlias; var img = ImageCache.Get(url, notify); using (var p = Theme.Round(r, Theme.D(3))) { if (img != null) g.DrawImage(Theme.Masked(img, r.Width, r.Height, Theme.D(3)), r.X, r.Y, r.Width, r.Height); else { var c = Theme.Hex(colorHex, Theme.Off); using (var b = new LinearGradientBrush(r, Theme.Shade(c, 0.15f), Theme.Shade(c, -0.25f), 90f)) g.FillPath(b, p); var init = string.IsNullOrEmpty(name) ? "?" : name.Substring(0, 1).ToUpperInvariant(); using (var f = Font(Math.Max(8f, r.Height * 0.45f / Theme.S), FontStyle.Bold)) TextRenderer.DrawText(g, init, f, r, Color.White, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding); } using (var pen = new Pen(Border)) g.DrawPath(pen, p); } g.Restore(st); if (kind == null) return; float ds = Math.Max(Theme.D(8), Math.Min(r.Width * 0.32f, Theme.D(14))); Theme.StatusDot(g, new RectangleF(r.Right - ds * 0.75f, r.Bottom - ds * 0.75f, ds, ds), kind, ringBg); } public static void Draw(Graphics g, string text, Font f, Rectangle r, Color c, TextFormatFlags extra = 0) { TextRenderer.DrawText(g, text ?? "", f, r, c, TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPadding | TextFormatFlags.NoPrefix | extra); } public static string Line(Friend f, out Color c) { if (f.Status == "play" && f.Game != null) { c = Game; var s = Theme.Since(f.Since); return "Igra " + f.Game + (s != null ? " · " + s : ""); } if (f.Voice != null && f.Kind != "off") { c = Voice; return "U voiceu: " + f.Voice; } var k = f.Kind; c = k == "dnd" ? Theme.Dnd : k == "idle" ? Theme.Idle : Theme.Muted; return Theme.KindText(k); } } // Traka s logom na vrhu class XLogo : Control { public XLogo() { SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; Xf.Gloss(g, ClientRectangle, Theme.Shade(Xf.Head2, 0.05f), Theme.Shade(Xf.Head2, -0.45f)); Theme.DrawFlame(g, new RectangleF(Theme.D(10), (Height - Theme.D(24)) / 2f, Theme.D(22), Theme.D(24))); using (var f = Xf.Font(17, FontStyle.Bold | FontStyle.Italic)) { var w = TextRenderer.MeasureText(g, "Frag", f, Size.Empty, TextFormatFlags.NoPadding).Width; Xf.Draw(g, "Frag", f, new Rectangle(Theme.D(36), 0, w + 2, Height), Color.White); Xf.Draw(g, "line", f, new Rectangle(Theme.D(36) + w, 0, Width, Height), Xf.Orange); } using (var f = Xf.Font(10.5f)) Xf.Draw(g, "v" + TrayApp.Current.ToString(2), f, new Rectangle(0, 0, Width - Theme.D(10), Height), Theme.Muted, TextFormatFlags.Right); } } // Profil na vrhu: kvadratni avatar, ime, status (klik = izbornik), igra i gumbi za mikrofon, zvuk i postavke class XProfile : UserPanel { int hot = -1; readonly ToolTip tt = new ToolTip(); public XProfile() { BackColor = Xf.Head2; } Rectangle Av() { int s = Theme.D(56); return new Rectangle(Theme.D(10), (Height - s) / 2, s, s); } Rectangle Btn(int i) { int s = Theme.D(24); return new Rectangle(Width - Theme.D(8) - (s + Theme.D(2)) * (3 - i), Theme.D(8), s, s); } Rectangle StatusRect() { using (var f = Xf.Font(12)) { var w = TextRenderer.MeasureText(StatusText(), f).Width + Theme.D(14); return new Rectangle(Theme.D(76), Theme.D(32), w + Theme.D(16), Theme.D(18)); } } string StatusText() { return Offline ? "Nema veze sa serverom" : Theme.KindText(Presence == "invisible" ? "invisible" : Kind); } int Hit(Point p) { for (int i = 0; i < 3; i++) if (Btn(i).Contains(p)) return i + 1; if (StatusRect().Contains(p) || Av().Contains(p)) return 0; return -1; } protected override void OnMouseMove(MouseEventArgs e) { int h = Hit(e.Location); if (h != hot) { hot = h; Cursor = h >= 0 ? Cursors.Hand : Cursors.Default; tt.SetToolTip(this, h == 0 ? "Promijeni status ili profilnu sliku" : h == 1 ? (Muted ? "Uključi mikrofon" : "Isključi mikrofon") : h == 2 ? (Deaf ? "Uključi zvuk" : "Isključi zvuk") : h == 3 ? "Postavke" : null); Invalidate(); } } protected override void OnMouseLeave(EventArgs e) { hot = -1; Invalidate(); } protected override void OnMouseClick(MouseEventArgs e) { int h = Hit(e.Location); if (h >= 0) Fire(h, new Point(StatusRect().X, StatusRect().Bottom + Theme.D(2))); Invalidate(); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; Xf.Gloss(g, ClientRectangle, Xf.Head1, Xf.Head2); Xf.Avatar(g, Av(), ColorHex, User, Avatar, this, Kind, Xf.Head2); int x = Theme.D(76), right = Btn(0).X - Theme.D(6); using (var f = Xf.Font(15, FontStyle.Bold)) Xf.Draw(g, User, f, new Rectangle(x, Theme.D(10), Math.Max(10, right - x), Theme.D(20)), Color.White); var sr = StatusRect(); if (hot == 0) using (var p = Theme.Round(sr, Theme.D(3))) using (var b = new SolidBrush(Color.FromArgb(40, 255, 255, 255))) g.FillPath(b, p); float ds = Theme.D(9); Theme.StatusDot(g, new RectangleF(sr.X + Theme.D(3), sr.Y + (sr.Height - ds) / 2f, ds, ds), Kind, Xf.Head2); using (var f = Xf.Font(12)) Xf.Draw(g, StatusText(), f, new Rectangle(sr.X + Theme.D(16), sr.Y, sr.Width - Theme.D(30), sr.Height), Offline ? Theme.Red : Theme.Text2); Theme.Chevron(g, new RectangleF(sr.Right - Theme.D(16), sr.Y, Theme.D(12), sr.Height), true, Theme.Text2); string game = Game != null ? "Igraš: " + Game + (SinceUtc.HasValue ? " · " + Theme.Since(new DateTimeOffset(SinceUtc.Value, TimeSpan.Zero).ToString("o")) : "") : "Nijedna igra nije pokrenuta"; using (var f = Xf.Font(11.5f, Game != null ? FontStyle.Bold : FontStyle.Regular)) Xf.Draw(g, game, f, new Rectangle(x, Theme.D(54), Math.Max(10, Width - x - Theme.D(8)), Theme.D(18)), Game != null ? Xf.Game : Theme.Muted); for (int i = 0; i < 3; i++) { var r = Btn(i); if (hot == i + 1) using (var p = Theme.Round(r, Theme.D(3))) using (var b = new SolidBrush(Color.FromArgb(50, 255, 255, 255))) g.FillPath(b, p); bool off = (i == 0 && Muted) || (i == 1 && Deaf); Theme.Icon(g, i == 0 ? Theme.IcoMic : i == 1 ? Theme.IcoHead : Theme.IcoGear, i == 0 ? "M" : i == 1 ? "Z" : "P", r, off ? Theme.Red : Theme.Text2, 14); if (off) Theme.Slash(g, r, Xf.Head1); } } } // Kartice ispod profila class XTabs : Control { readonly string[] names = { "Prijatelji", "Klanovi", "Serveri", "Voice", "Aktivnost" }; readonly string[] glyphs = { Theme.IcoPeople, "\uE716", Theme.IcoGame, Theme.IcoSpeaker, Theme.IcoHistory }; public int[] Badges = new int[5]; public bool VoiceOn; int sel, hover = -1; public event Action Changed; public XTabs() { SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); Cursor = Cursors.Hand; } public int Selected { get { return sel; } set { if (sel != value) { sel = value; Invalidate(); if (Changed != null) Changed(sel); } } } Rectangle Tab(int i) { int w = Width / names.Length; return new Rectangle(i * w, 0, i == names.Length - 1 ? Width - i * w : w, Height); } int Hit(Point p) { for (int i = 0; i < names.Length; i++) if (Tab(i).Contains(p)) return i; return -1; } protected override void OnMouseMove(MouseEventArgs e) { int h = Hit(e.Location); if (h != hover) { hover = h; Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseLeave(EventArgs e) { hover = -1; Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseClick(MouseEventArgs e) { int h = Hit(e.Location); if (h >= 0) Selected = h; base.OnMouseClick(e); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; Xf.Gloss(g, ClientRectangle, Xf.Bar2, Theme.Shade(Xf.Bar2, -0.4f)); for (int i = 0; i < names.Length; i++) { var r = Tab(i); if (i == sel) Xf.Gloss(g, r, Theme.Shade(Xf.Head1, 0.1f), Xf.Bar1); else if (i == hover) using (var b = new SolidBrush(Color.FromArgb(25, 255, 255, 255))) g.FillRectangle(b, r); if (i == sel) using (var b = new SolidBrush(Xf.Orange)) g.FillRectangle(b, r.X, r.Bottom - Theme.D(2), r.Width, Theme.D(2)); if (i > 0) using (var pen = new Pen(Color.FromArgb(90, 0, 0, 0))) g.DrawLine(pen, r.X, r.Y + Theme.D(4), r.X, r.Bottom - Theme.D(4)); var fc = i == sel ? Color.White : i == hover ? Theme.Text2 : Theme.Muted; Theme.Icon(g, glyphs[i], names[i].Substring(0, 1), new Rectangle(r.X, r.Y + Theme.D(3), r.Width, Theme.D(18)), i == 3 && VoiceOn ? Theme.On : fc, 13); using (var f = Xf.Font(10.5f, FontStyle.Bold)) TextRenderer.DrawText(g, names[i], f, new Rectangle(r.X, r.Y + Theme.D(21), r.Width, Theme.D(14)), fc, TextFormatFlags.HorizontalCenter | TextFormatFlags.NoPadding | TextFormatFlags.EndEllipsis); if (Badges[i] > 0) Theme.Pill(g, Badges[i] > 99 ? "99+" : Badges[i].ToString(), new Rectangle(r.X, r.Y + Theme.D(3), r.Width / 2 + Theme.D(22), Theme.D(14)), true, Color.Empty); } } } class XRow { public string Kind, Group, Text; public int Count; public Friend F; public FriendReq R; } // Popis prijatelja sa sklopivim grupama class XBuddyList : UserControl { readonly TrayApp app; readonly Panel searchBar = new Panel(); readonly TextBox search = new TextBox(); readonly ListBox list = new SmoothList(); readonly HashSet collapsed = new HashSet(); List friends = new List(); List reqs = new List(); List rows = new List(); int hover = -1; Point mouse; string sel, lastSig = ""; readonly bool modern; public XBuddyList(TrayApp app, bool modern = false) { this.app = app; this.modern = modern; BackColor = modern ? Theme.Panel : Theme.Bg; list.BackColor = modern ? Theme.Panel : Theme.Bg; foreach (var g in Prefs.Str(modern ? "neo_collapsed" : "xf_collapsed", "out").Split(',')) if (g.Length > 0) collapsed.Add(g); searchBar.Dock = DockStyle.Top; searchBar.Height = Theme.D(32); searchBar.BackColor = Theme.Panel; searchBar.Paint += (s, e) => { e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; using (var p = Theme.Round(new Rectangle(Theme.D(6), Theme.D(5), searchBar.Width - Theme.D(12), Theme.D(22)), Theme.D(modern ? 11 : 3))) { using (var b = new SolidBrush(Theme.Field)) e.Graphics.FillPath(b, p); if (!modern) using (var pen = new Pen(Theme.Line)) e.Graphics.DrawPath(pen, p); } }; search.BorderStyle = BorderStyle.None; search.BackColor = Theme.Field; search.ForeColor = Theme.Text; search.Font = modern ? Theme.Body(13.5f) : Xf.Font(12); Theme.Cue(search, "Traži prijatelja..."); searchBar.Controls.Add(search); searchBar.Resize += (s, e) => search.SetBounds(Theme.D(12), Theme.D(5) + (Theme.D(22) - search.PreferredHeight) / 2, Math.Max(10, searchBar.Width - Theme.D(24)), search.PreferredHeight); search.TextChanged += (s, e) => Rebuild(); list.Dock = DockStyle.Fill; list.DrawMode = DrawMode.OwnerDrawVariable; list.BorderStyle = BorderStyle.None; list.BackColor = modern ? Theme.Panel : Theme.Bg; list.IntegralHeight = false; list.SelectionMode = SelectionMode.None; list.MeasureItem += (s, e) => { var r = e.Index >= 0 && e.Index < rows.Count ? rows[e.Index] : null; e.ItemHeight = r == null ? Theme.D(44) : r.Kind == "head" ? Theme.D(modern ? 34 : 22) : r.Kind == "empty" ? Theme.D(70) : Theme.D(modern ? 54 : 44); }; list.DrawItem += DrawRow; list.MouseMove += (s, e) => { mouse = e.Location; int i = list.IndexFromPoint(e.Location); if (i != hover || (i >= 0 && i < rows.Count && rows[i].R != null)) { hover = i; list.Invalidate(); } }; list.MouseLeave += (s, e) => { hover = -1; list.Invalidate(); }; list.MouseDown += (s, e) => { int i = list.IndexFromPoint(e.Location); if (i < 0 || i >= rows.Count) return; var r = rows[i]; var b = list.GetItemRectangle(i); if (e.Button == MouseButtons.Left && r.Kind == "head") { Toggle(r.Group); return; } if (r.R != null && e.Button == MouseButtons.Left) { if (r.R.Incoming && AcceptBtn(b).Contains(e.Location)) app.AcceptRequest(r.R); else if (DeclineBtn(b).Contains(e.Location)) app.DeleteRequest(r.R); return; } if (r.F == null) return; sel = r.F.Username; list.Invalidate(); if (e.Button == MouseButtons.Right) FriendMenu(r.F, e.Location); else if (e.Clicks >= 2 || modern) app.OpenChat(r.F); }; list.KeyDown += (s, e) => { if (e.KeyCode == Keys.Enter && sel != null) app.OpenChatByName(sel); }; Theme.DarkScroll(list); Controls.Add(list); Controls.Add(searchBar); list.BringToFront(); Rebuild(); } void Toggle(string g) { if (!collapsed.Remove(g)) collapsed.Add(g); Prefs.Set(modern ? "neo_collapsed" : "xf_collapsed", string.Join(",", collapsed.ToArray())); Rebuild(); } void FriendMenu(Friend f, Point at) { var name = f.Username; var menu = Ui.Menu(); menu.Items.Add(new ToolStripMenuItem("Pošalji poruku", null, (a, b) => app.OpenChat(f)) { Font = Theme.Body(13.5f, FontStyle.Bold) }); menu.Items.Add(new ToolStripMenuItem("Pozovi", null, (a, b) => app.Call(f))); menu.Items.Add(new ToolStripMenuItem("Otvori profil", null, (a, b) => app.Open("/profile/" + Uri.EscapeDataString(name)))); menu.Items.Add(new ToolStripSeparator()); menu.Items.Add(new ToolStripMenuItem("Ukloni prijatelja", null, (a, b) => app.RemoveFriend(name)) { ForeColor = Theme.Red }); menu.Show(list, at); } public void SetData(List f) { friends = f; Rebuild(); } public void SetRequests(List r) { reqs = r; Rebuild(); } public void Refresh2() { list.Invalidate(); } void Rebuild() { var q = search.Text.Trim(); Func match = n => q.Length == 0 || (n ?? "").IndexOf(q, StringComparison.OrdinalIgnoreCase) >= 0; var nr = new List(); Action> group = (key, title, items) => { var l = items.ToList(); if (l.Count == 0) return; nr.Add(new XRow { Kind = "head", Group = key, Text = title, Count = l.Count }); if (!collapsed.Contains(key)) nr.AddRange(l); }; group("in", "Zahtjevi za prijateljstvo", reqs.Where(r => r.Incoming && match(r.Username)).Select(r => new XRow { Kind = "req", R = r })); var fs = friends.Where(f => match(f.Username)).OrderBy(f => f.Username, StringComparer.OrdinalIgnoreCase).ToList(); group("play", "U igri", fs.Where(f => f.Status == "play").Select(f => new XRow { Kind = "friend", F = f })); group("on", "Online", fs.Where(f => f.Status != "play" && f.Kind != "off").Select(f => new XRow { Kind = "friend", F = f })); group("off", "Offline", fs.Where(f => f.Kind == "off").Select(f => new XRow { Kind = "friend", F = f })); group("out", "Poslani zahtjevi", reqs.Where(r => !r.Incoming && match(r.Username)).Select(r => new XRow { Kind = "req", R = r })); if (nr.Count == 0) nr.Add(new XRow { Kind = "empty", Text = q.Length > 0 ? "Nitko ne odgovara traženju." : "Još nemaš prijatelja.\nKlikni zeleni + dolje da nekoga dodaš." }); var sig = string.Join("|", nr.Select(x => x.Kind + (x.Group ?? "") + x.Count + (x.F != null ? x.F.Username : x.R != null ? "r" + x.R.Id : x.Text)).ToArray()); rows = nr; if (sig != lastSig) { lastSig = sig; int top = list.Items.Count > 0 ? list.TopIndex : 0; list.BeginUpdate(); list.Items.Clear(); foreach (var x in nr) list.Items.Add(x); list.EndUpdate(); if (top < list.Items.Count) list.TopIndex = top; } else list.Invalidate(); } Rectangle DeclineBtn(Rectangle b) { int s = Theme.D(24); return new Rectangle(b.Right - Theme.D(8) - s, b.Y + (b.Height - s) / 2, s, s); } Rectangle AcceptBtn(Rectangle b) { var d = DeclineBtn(b); return new Rectangle(d.X - d.Width - Theme.D(6), d.Y, d.Width, d.Height); } // moderni izgled: okrugli avatari, tanki naslovi grupa, igra u boji naglaska void DrawModern(DrawItemEventArgs e, XRow r) { var g = e.Graphics; var b = e.Bounds; using (var fill = new SolidBrush(Theme.Panel)) g.FillRectangle(fill, b); if (r.Kind == "head") { var hc = r.Group == "in" ? Theme.Accent2 : Theme.Muted; Theme.Chevron(g, new RectangleF(b.X + Theme.D(12), b.Y + Theme.D(8), Theme.D(14), b.Height - Theme.D(8)), !collapsed.Contains(r.Group), hc); using (var f = Theme.Body(11.5f, FontStyle.Bold)) Txt.Draw(g, r.Text.ToUpperInvariant() + " — " + r.Count, f, new Rectangle(b.X + Theme.D(30), b.Y + Theme.D(8), b.Width - Theme.D(46), b.Height - Theme.D(8)), hc); return; } if (r.Kind == "empty") { using (var f = Theme.Body(13)) TextRenderer.DrawText(g, r.Text, f, new Rectangle(b.X + Theme.D(16), b.Y, b.Width - Theme.D(32), b.Height), Theme.Muted, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.WordBreak | TextFormatFlags.NoPrefix); return; } bool hot = e.Index == hover; var inner = new Rectangle(b.X + Theme.D(8), b.Y + Theme.D(2), b.Width - Theme.D(16), b.Height - Theme.D(4)); var bg = hot ? Theme.Panel2 : Theme.Panel; if (hot) using (var p = Theme.Round(inner, Theme.D(10))) using (var fill = new SolidBrush(bg)) g.FillPath(fill, p); float av = Theme.D(36); var ar = new RectangleF(inner.X + Theme.D(8), inner.Y + (inner.Height - av) / 2f, av, av); int x = (int)ar.Right + Theme.D(12), right = inner.Right - Theme.D(8); if (r.R != null) { Theme.Avatar(g, ar, r.R.Color, r.R.Username, r.R.Avatar, list, null, bg); var db = DeclineBtn(b); if (r.R.Incoming) { var ab = AcceptBtn(b); using (var fill = new SolidBrush(ab.Contains(mouse) && hot ? Theme.Shade(Theme.On, 0.15f) : Theme.On)) g.FillEllipse(fill, ab); Theme.Check(g, ab, Color.White, 11); right = ab.X - Theme.D(6); } else right = db.X - Theme.D(6); using (var fill = new SolidBrush(db.Contains(mouse) && hot ? Theme.Red : Theme.Panel3)) g.FillEllipse(fill, db); Theme.Icon(g, Theme.IcoClose, "✕", db, Color.White, 10); using (var f = Theme.Display(14)) Txt.Draw(g, r.R.Username, f, new Rectangle(x, inner.Y + Theme.D(7), Math.Max(10, right - x), Theme.D(20)), Theme.Text); using (var f = Theme.Body(12)) Txt.Draw(g, r.R.Incoming ? "Želi te dodati za prijatelja" : "Čeka da prihvati", f, new Rectangle(x, inner.Y + Theme.D(27), Math.Max(10, right - x), Theme.D(17)), Theme.Muted); return; } var fr = r.F; Theme.Avatar(g, ar, fr.Color, fr.Username, fr.Avatar, list, fr.Kind, bg); var img = fr.GameIcon != null && fr.Status == "play" ? ImageCache.Get(fr.GameIcon, list) : null; if (img != null) { var ir = new Rectangle(right - Theme.D(28), inner.Y + (inner.Height - Theme.D(28)) / 2, Theme.D(28), Theme.D(28)); g.DrawImage(Theme.Masked(img, ir.Width, ir.Height, Theme.D(7)), ir.X, ir.Y, ir.Width, ir.Height); right = ir.X - Theme.D(8); } if (fr.Unread > 0) { Theme.Pill(g, fr.Unread > 99 ? "99+" : fr.Unread.ToString(), new Rectangle(x, inner.Y, right - x, inner.Height), true, Color.Empty); right -= Theme.D(28); } Color lc; var line = Xf.Line(fr, out lc); if (fr.Status == "play") lc = Theme.On; else if (fr.Voice != null && fr.Kind != "off") lc = Theme.Accent2; using (var f = Theme.Display(14)) Txt.Draw(g, fr.Username, f, new Rectangle(x, inner.Y + Theme.D(7), Math.Max(10, right - x), Theme.D(20)), fr.Kind == "off" ? Theme.Muted : Theme.Text); using (var f = Theme.Body(12)) Txt.Draw(g, line, f, new Rectangle(x, inner.Y + Theme.D(27), Math.Max(10, right - x), Theme.D(17)), lc); } void DrawRow(object s, DrawItemEventArgs e) { if (e.Index < 0 || e.Index >= rows.Count) return; var r = rows[e.Index]; var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; var b = e.Bounds; if (modern) { DrawModern(e, r); return; } if (r.Kind == "head") { Xf.Gloss(g, b, Xf.Bar1, Xf.Bar2); var hc = r.Group == "in" ? Xf.Game : Theme.Text2; Theme.Chevron(g, new RectangleF(b.X + Theme.D(4), b.Y, Theme.D(14), b.Height), !collapsed.Contains(r.Group), hc); using (var f = Xf.Font(11.5f, FontStyle.Bold)) Xf.Draw(g, r.Text + " (" + r.Count + ")", f, new Rectangle(b.X + Theme.D(20), b.Y, b.Width - Theme.D(28), b.Height), hc); return; } using (var fill = new SolidBrush(Theme.Bg)) g.FillRectangle(fill, b); if (r.Kind == "empty") { using (var f = Xf.Font(12)) TextRenderer.DrawText(g, r.Text, f, new Rectangle(b.X + Theme.D(12), b.Y, b.Width - Theme.D(24), b.Height), Theme.Muted, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.WordBreak | TextFormatFlags.NoPrefix); return; } bool isSel = r.F != null && r.F.Username == sel, hot = e.Index == hover; var bg = isSel ? Theme.Panel3 : hot ? Theme.Panel2 : Theme.Bg; if (isSel || hot) { using (var fill = new LinearGradientBrush(b, Theme.Shade(bg, 0.08f), bg, 90f)) g.FillRectangle(fill, b); if (isSel) using (var fill = new SolidBrush(Xf.Orange)) g.FillRectangle(fill, b.X, b.Y, Theme.D(3), b.Height); } using (var pen = new Pen(Color.FromArgb(40, 255, 255, 255))) g.DrawLine(pen, b.X + Theme.D(48), b.Bottom - 1, b.Right - Theme.D(8), b.Bottom - 1); var ar = new Rectangle(b.X + Theme.D(8), b.Y + (b.Height - Theme.D(32)) / 2, Theme.D(32), Theme.D(32)); int x = ar.Right + Theme.D(8), right = b.Right - Theme.D(8); if (r.R != null) { Xf.Avatar(g, ar, r.R.Color, r.R.Username, r.R.Avatar, list, null, bg); var db = DeclineBtn(b); if (r.R.Incoming) { var ab = AcceptBtn(b); using (var fill = new SolidBrush(ab.Contains(mouse) && hot ? Theme.Shade(Theme.On, 0.15f) : Theme.On)) g.FillEllipse(fill, ab); Theme.Check(g, ab, Color.White, 11); right = ab.X - Theme.D(6); } else right = db.X - Theme.D(6); using (var fill = new SolidBrush(db.Contains(mouse) && hot ? Theme.Shade(Theme.Red, 0.15f) : Theme.Shade(Theme.Red, -0.15f))) g.FillEllipse(fill, db); Theme.Icon(g, Theme.IcoClose, "✕", db, Color.White, 10); using (var f = Xf.Font(12.5f, FontStyle.Bold)) Xf.Draw(g, r.R.Username, f, new Rectangle(x, b.Y + Theme.D(5), Math.Max(10, right - x), Theme.D(18)), Theme.Text); using (var f = Xf.Font(11)) Xf.Draw(g, r.R.Incoming ? "Želi te dodati za prijatelja" : "Čeka da prihvati", f, new Rectangle(x, b.Y + Theme.D(23), Math.Max(10, right - x), Theme.D(16)), Theme.Muted); return; } var fr = r.F; Xf.Avatar(g, ar, fr.Color, fr.Username, fr.Avatar, list, fr.Kind, bg); var img = fr.GameIcon != null && fr.Status == "play" ? ImageCache.Get(fr.GameIcon, list) : null; if (img != null) { var ir = new Rectangle(right - Theme.D(24), b.Y + (b.Height - Theme.D(24)) / 2, Theme.D(24), Theme.D(24)); g.DrawImage(Theme.Masked(img, ir.Width, ir.Height, Theme.D(3)), ir.X, ir.Y, ir.Width, ir.Height); right = ir.X - Theme.D(6); } if (fr.Unread > 0) { Theme.Pill(g, fr.Unread > 99 ? "99+" : fr.Unread.ToString(), new Rectangle(x, b.Y, right - x, b.Height), true, Color.Empty); right -= Theme.D(28); } Color lc; var line = Xf.Line(fr, out lc); using (var f = Xf.Font(12.5f, FontStyle.Bold)) Xf.Draw(g, fr.Username, f, new Rectangle(x, b.Y + Theme.D(5), Math.Max(10, right - x), Theme.D(18)), fr.Kind == "off" ? Theme.Muted : Theme.Text); using (var f = Xf.Font(11)) Xf.Draw(g, line, f, new Rectangle(x, b.Y + Theme.D(23), Math.Max(10, right - x), Theme.D(16)), lc); } } // Moji klanovi class XClans : UserControl { readonly TrayApp app; readonly ListBox list = new SmoothList(); readonly Label empty = new Label(); readonly Panel bottom = new Panel(); List clans = new List(); int hover = -1; public XClans(TrayApp app) { this.app = app; BackColor = Theme.Bg; list.Dock = DockStyle.Fill; list.DrawMode = DrawMode.OwnerDrawFixed; list.ItemHeight = Theme.D(46); list.BorderStyle = BorderStyle.None; list.BackColor = Theme.Bg; list.IntegralHeight = false; list.SelectionMode = SelectionMode.None; list.DrawItem += DrawRow; list.MouseMove += (s, e) => { int i = list.IndexFromPoint(e.Location); if (i != hover) { hover = i; list.Invalidate(); } }; list.MouseLeave += (s, e) => { hover = -1; list.Invalidate(); }; list.MouseDown += (s, e) => { int i = list.IndexFromPoint(e.Location); if (i < 0 || i >= clans.Count) return; var c = clans[i]; if (e.Button == MouseButtons.Right) { var menu = Ui.Menu(); if (c.Slug != null) menu.Items.Add(new ToolStripMenuItem("Otvori stranicu klana", null, (a, b) => app.Open("/clan/" + Uri.EscapeDataString(c.Slug)))); menu.Items.Add(new ToolStripMenuItem("Kopiraj link za pozivanje", null, (a, b) => { try { Clipboard.SetText(Api.Base + "/clan/" + c.Slug); } catch { } })); menu.Items.Add(new ToolStripSeparator()); menu.Items.Add(new ToolStripMenuItem(c.Owner ? "Obriši klan" : "Napusti klan", null, (a, b) => app.LeaveClan(c)) { ForeColor = Theme.Red }); menu.Show(list, e.Location); } else if (e.Clicks >= 2 && c.Slug != null) app.Open("/clan/" + Uri.EscapeDataString(c.Slug)); }; Theme.DarkScroll(list); empty.Dock = DockStyle.Fill; empty.Font = Xf.Font(12.5f); empty.ForeColor = Theme.Muted; empty.BackColor = Theme.Bg; empty.TextAlign = ContentAlignment.MiddleCenter; empty.Text = "Nisi ni u jednom klanu.\nNapravi svoj ili se pridruži postojećem."; bottom.Dock = DockStyle.Bottom; bottom.Height = Theme.D(48); bottom.BackColor = Theme.Panel; var create = new FlatButton("Napravi klan", true) { Font = Theme.Body(13, FontStyle.Bold) }; var join = new FlatButton("Pridruži se", false) { Font = Theme.Body(13, FontStyle.Bold) }; create.Click += (s, e) => app.ShowClanForm(false); join.Click += (s, e) => app.ShowClanForm(true); bottom.Controls.AddRange(new Control[] { create, join }); bottom.Resize += (s, e) => { int pad = Theme.D(8), half = (bottom.Width - pad * 3) / 2; create.SetBounds(pad, Theme.D(8), half, Theme.D(32)); join.SetBounds(pad * 2 + half, Theme.D(8), half, Theme.D(32)); }; Controls.Add(list); Controls.Add(empty); Controls.Add(bottom); list.BringToFront(); SetData(clans); } public void SetData(List c) { clans = c; list.BeginUpdate(); list.Items.Clear(); foreach (var x in c) list.Items.Add(x); list.EndUpdate(); list.Visible = c.Count > 0; empty.Visible = c.Count == 0; } void DrawRow(object s, DrawItemEventArgs e) { if (e.Index < 0 || e.Index >= clans.Count) return; var c = clans[e.Index]; var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; var b = e.Bounds; var bg = e.Index == hover ? Theme.Panel2 : Theme.Bg; using (var fill = new SolidBrush(bg)) g.FillRectangle(fill, b); using (var pen = new Pen(Color.FromArgb(40, 255, 255, 255))) g.DrawLine(pen, b.X + Theme.D(50), b.Bottom - 1, b.Right - Theme.D(8), b.Bottom - 1); var ic = new Rectangle(b.X + Theme.D(8), b.Y + (b.Height - Theme.D(34)) / 2, Theme.D(34), Theme.D(34)); var col = Theme.Hex(c.Color, Theme.Accent); using (var p = Theme.Round(ic, Theme.D(4))) { using (var fill = new LinearGradientBrush(ic, Theme.Shade(col, 0.2f), Theme.Shade(col, -0.25f), 90f)) g.FillPath(fill, p); using (var pen = new Pen(Xf.Border)) g.DrawPath(pen, p); } var tag = c.Tag ?? "?"; using (var f = Xf.Font(tag.Length > 3 ? 9.5f : 11.5f, FontStyle.Bold)) TextRenderer.DrawText(g, tag, f, ic, Color.White, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding | TextFormatFlags.NoPrefix); int x = ic.Right + Theme.D(10), w = Math.Max(10, b.Right - x - Theme.D(8)); using (var f = Xf.Font(12.5f, FontStyle.Bold)) Xf.Draw(g, c.Name, f, new Rectangle(x, b.Y + Theme.D(6), w, Theme.D(18)), Theme.Text); using (var f = Xf.Font(11)) Xf.Draw(g, c.Owner ? "Ti si vođa · dvoklik otvara stranicu" : "Član · dvoklik otvara stranicu", f, new Rectangle(x, b.Y + Theme.D(24), w, Theme.D(16)), c.Owner ? Xf.Game : Theme.Muted); } } // Donja traka s ikonama class XToolbar : Control { readonly string[] glyphs = { Theme.IcoAdd, Theme.IcoMsg, "", Theme.IcoGlobe, Theme.IcoHead, Theme.IcoGear }; readonly string[] tips = { "Dodaj prijatelja", "Web messenger", "Screenshot (Scroll Lock)", "Otvori web stranicu", "Postavke zvuka i mikrofona", "Postavke" }; readonly ToolTip tt = new ToolTip(); int hover = -1; public event Action Clicked; public XToolbar() { SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); Cursor = Cursors.Hand; } Rectangle Btn(int i) { int s = Theme.D(28), gap = Theme.D(4); int x = Theme.D(6) + i * (s + gap); return new Rectangle(x, (Height - s) / 2, s, s); } int Hit(Point p) { for (int i = 0; i < glyphs.Length; i++) if (Btn(i).Contains(p)) return i; return -1; } protected override void OnMouseMove(MouseEventArgs e) { int h = Hit(e.Location); if (h != hover) { hover = h; tt.SetToolTip(this, h >= 0 ? tips[h] : null); Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseLeave(EventArgs e) { hover = -1; Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseClick(MouseEventArgs e) { int h = Hit(e.Location); if (h >= 0 && Clicked != null) Clicked(h); base.OnMouseClick(e); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; Xf.Gloss(g, ClientRectangle, Xf.Bar1, Xf.Head2); for (int i = 0; i < glyphs.Length; i++) { var r = Btn(i); if (i == hover) using (var p = Theme.Round(r, Theme.D(3))) { using (var b = new SolidBrush(Color.FromArgb(45, 255, 255, 255))) g.FillPath(b, p); using (var pen = new Pen(Color.FromArgb(70, 255, 255, 255))) g.DrawPath(pen, p); } Theme.Icon(g, glyphs[i], tips[i].Substring(0, 1), r, i == 0 ? Theme.On : i == hover ? Color.White : Theme.Text2, 15); } } } // Mali prozor za dodavanje prijatelja class XAddFriendForm : DarkForm { public XAddFriendForm(TrayApp app) { Text = "Fragline - dodaj prijatelja"; MinimizeBox = false; int W = Theme.D(400), pad = Theme.D(22), inner = W - pad * 2; ClientSize = new Size(W, Theme.D(220)); Controls.Add(Ui.Lbl("Dodaj prijatelja", 19, Theme.Text, pad, Theme.D(16), true)); Controls.Add(Ui.Lbl("Upiši korisničko ime osobe s Fragline weba.", 13, Theme.Muted, pad, Theme.D(50))); var name = new InputBox(false); name.SetBounds(pad, Theme.D(80), inner, Theme.D(40)); name.Box.MaxLength = 32; var res = Ui.Lbl("", 13, Theme.On, pad, Theme.D(128)); res.AutoSize = false; res.Size = new Size(inner, Theme.D(22)); var close = new FlatButton("Zatvori", false); var send = new FlatButton("Pošalji zahtjev", true); int half = (inner - Theme.D(10)) / 2; close.SetBounds(pad, Theme.D(162), half, Theme.D(40)); send.SetBounds(pad + half + Theme.D(10), Theme.D(162), half, Theme.D(40)); close.Click += (s, e) => Close(); send.Click += async (s, e) => { var n = name.Value.Trim(); if (n.Length == 0) { res.ForeColor = Theme.Red; res.Text = "Upiši korisničko ime."; return; } send.Enabled = false; var r = await app.AddFriend(n); send.Enabled = true; res.ForeColor = r.Item1 ? Theme.On : Theme.Red; res.Text = r.Item2; if (r.Item1) name.Box.Text = ""; name.Box.Focus(); }; Controls.AddRange(new Control[] { name, res, close, send }); AcceptButton = send; Shown += (s, e) => name.Box.Focus(); } } // Chat s jednim prijateljem u zasebnom prozoru (kao na Xfireu) class XChatForm : DarkForm { public readonly ChatView View; public XChatForm(TrayApp app, Friend f) { Text = f.Username + " - Fragline"; FormBorderStyle = FormBorderStyle.Sizable; MaximizeBox = true; MinimizeBox = true; MinimumSize = new Size(Theme.D(360), Theme.D(380)); ClientSize = new Size(Theme.D(480), Theme.D(560)); View = new ChatView(app) { Dock = DockStyle.Fill }; Controls.Add(View); View.Open(f); Shown += (s, e) => View.FocusInput(); FormClosed += (s, e) => View.Reset(); } } // Postavke u zasebnom prozoru class XSettingsForm : DarkForm { readonly SettingsView settings; public XSettingsForm(SettingsView settings) { this.settings = settings; Text = "Fragline - postavke"; FormBorderStyle = FormBorderStyle.Sizable; MaximizeBox = true; ClientSize = new Size(Theme.D(600), Theme.D(640)); settings.Dock = DockStyle.Fill; Controls.Add(settings); } // postavke zive dulje od prozora, pa ih ne smijemo unistiti zajedno s njim protected override void OnFormClosing(FormClosingEventArgs e) { Controls.Remove(settings); base.OnFormClosing(e); } } class XfireForm : ShellForm { readonly XLogo logo = new XLogo(); readonly XProfile profile = new XProfile(); readonly XTabs tabs = new XTabs(); readonly XBuddyList buddies; readonly XClans clanList; readonly VoiceView voice; readonly Panel voicePage = new Panel(), voiceBar = new Panel(); readonly RoundButton mic = new RoundButton(Theme.IcoMic, "M", "Mikrofon"), deaf = new RoundButton(Theme.IcoHead, "Z", "Zvuk"), leave = new RoundButton(Theme.IcoHangup, "X", "Izađi iz sobe") { Danger = true }; readonly ActivityView activity; readonly ServersView serverList; readonly XToolbar toolbar = new XToolbar(); readonly UpdateBar updateBar = new UpdateBar { Visible = false }; readonly SettingsView settings; readonly Dictionary chats = new Dictionary(); readonly Timer tick = new Timer { Interval = 250 }; Control[] pages; XSettingsForm settingsForm; int ticks; public override UserPanel User { get { return profile; } } public override VoiceView Voice { get { return voice; } } protected override bool ManualLayout { get { return true; } } public XfireForm(TrayApp app) : base(app) { Xf.Init(); Text = "Fragline"; FormBorderStyle = FormBorderStyle.Sizable; MaximizeBox = false; BackColor = Theme.Bg; MinimumSize = new Size(Theme.D(300), Theme.D(520)); var wa = Screen.PrimaryScreen.WorkingArea; ClientSize = new Size(Prefs.Int("xf_w", Theme.D(340)), Math.Min(wa.Height - Theme.D(60), Prefs.Int("xf_h", Theme.D(700)))); StartPosition = FormStartPosition.Manual; Location = new Point(Math.Max(wa.Left, wa.Right - Width - Theme.D(24)), wa.Top + Theme.D(30)); buddies = new XBuddyList(app); clanList = new XClans(app) { Visible = false }; voice = new VoiceView(app); voicePage.Visible = false; voicePage.BackColor = Theme.Panel; voiceBar.Dock = DockStyle.Bottom; voiceBar.Height = Theme.D(56); voiceBar.BackColor = Theme.Panel; var audio = new FlatButton("Zvuk", false) { Font = Theme.Body(13, FontStyle.Bold) }; audio.Click += (s, e) => app.ShowAudioSettings(); mic.Click += (s, e) => app.ToggleMute(); deaf.Click += (s, e) => app.ToggleDeaf(); leave.Click += (s, e) => app.LeaveVoice(); voiceBar.Controls.AddRange(new Control[] { mic, deaf, leave, audio }); voiceBar.Resize += (s, e) => { int bs = Theme.D(40), gap = Theme.D(10), y = (voiceBar.Height - bs) / 2; mic.SetBounds(Theme.D(10), y, bs, bs); deaf.SetBounds(Theme.D(10) + bs + gap, y, bs, bs); leave.SetBounds(Theme.D(10) + (bs + gap) * 2, y, bs, bs); audio.SetBounds(voiceBar.Width - Theme.D(10) - Theme.D(90), y + Theme.D(4), Theme.D(90), bs - Theme.D(8)); }; voice.Dock = DockStyle.Fill; voicePage.Controls.Add(voice); voicePage.Controls.Add(voiceBar); voice.BringToFront(); activity = new ActivityView(app) { Visible = false }; settings = new SettingsView(app); serverList = new ServersView(app) { Visible = false }; pages = new Control[] { buddies, clanList, serverList, voicePage, activity }; Controls.AddRange(new Control[] { updateBar, logo, profile, tabs, buddies, clanList, serverList, voicePage, activity, toolbar }); tabs.Changed += i => { for (int k = 0; k < pages.Length; k++) pages[k].Visible = k == i; }; profile.ProfileClicked += pt => StatusMenu(profile, pt); profile.MicClicked += () => app.ToggleMute(); profile.DeafClicked += () => app.ToggleDeaf(); profile.SettingsClicked += () => ShowSettings(); toolbar.Clicked += i => { if (i == 0) ShowAddFriend(); else if (i == 1) app.Open("/app"); else if (i == 2) app.Screenshot(); else if (i == 3) app.Open("/"); else if (i == 4) app.ShowAudioSettings(); else ShowSettings(); }; updateBar.UpdateClicked += () => app.DoUpdate(); updateBar.Closed += () => { updateBar.Visible = false; DoLayout(); }; tick.Tick += (s, e) => Tick(); tick.Start(); DoLayout(); } protected override void OnResize(EventArgs e) { base.OnResize(e); DoLayout(); } protected override void OnLoad(EventArgs e) { base.OnLoad(e); DoLayout(); } void DoLayout() { if (buddies == null || activity == null || pages == null) return; int e = TitleH > 0 ? Theme.D(3) : 0, x = e, y = TitleH; int W = ClientSize.Width - e * 2; if (W <= 0) return; int ub = updateBar.Visible ? Theme.D(56) : 0; updateBar.SetBounds(x, y, W, ub); y += ub; // s vlastitom naslovnom trakom logo je vec gore, pa traka s logom nije potrebna logo.Visible = TitleH == 0; if (logo.Visible) { logo.SetBounds(x, y, W, Theme.D(34)); y += Theme.D(34); } profile.SetBounds(x, y, W, Theme.D(80)); y += Theme.D(80); tabs.SetBounds(x, y, W, Theme.D(38)); y += Theme.D(38); int tb = Theme.D(38); toolbar.SetBounds(x, ClientSize.Height - tb - e, W, tb); var cr = new Rectangle(x, y, W, Math.Max(10, ClientSize.Height - y - tb - e)); foreach (var p in pages) p.Bounds = cr; } void Tick() { ticks++; var vc = App.Voice; bool on = vc != null; bool muted = on ? vc.Muted : Prefs.Bool("muted", false), deafd = on && vc.Deafened, idle = App.AutoIdle; if (profile.Muted != muted || profile.Deaf != deafd || profile.Idle != idle) { profile.Muted = muted; profile.Deaf = deafd; profile.Idle = idle; profile.Invalidate(); } if (tabs.VoiceOn != on) { tabs.VoiceOn = on; tabs.Invalidate(); } if (mic.Visible != on) { mic.Visible = on; deaf.Visible = on; leave.Visible = on; } if (on && (mic.Off != vc.Muted || deaf.Off != vc.Deafened)) { mic.Off = vc.Muted; deaf.Off = vc.Deafened; mic.Invalidate(); deaf.Invalidate(); } if (ticks % 20 == 0 && Visible) { if (profile.Game != null) profile.Invalidate(); buddies.Refresh2(); } } public override void SetFriends(List list, int req) { buddies.SetData(list); foreach (var f in list) { XChatForm cf; if (chats.TryGetValue(f.Username, out cf) && !cf.IsDisposed) cf.View.UpdateFriend(f); } tabs.Badges[0] = list.Sum(f => f.Unread) + req; tabs.Invalidate(); } public override void SetRequests(List r) { buddies.SetRequests(r); } public override void SetClans(List clans) { clanList.SetData(clans); } public override void SetShot(string game, string url) { settings.SetShot(game, url); } public override void ShowUpdate(string text) { updateBar.Message = text; updateBar.Busy = false; updateBar.Visible = true; updateBar.Invalidate(); DoLayout(); } public override void SetUpdateText(string text, bool busy) { updateBar.Message = text; updateBar.Busy = busy; if (!updateBar.Visible) { updateBar.Visible = true; DoLayout(); } updateBar.Invalidate(); } public override void OpenChat(Friend f) { XChatForm cf; if (!chats.TryGetValue(f.Username, out cf) || cf.IsDisposed) { cf = new XChatForm(App, f); var name = f.Username; cf.FormClosed += (s, e) => chats.Remove(name); chats[name] = cf; // svaki novi chat malo pomaknut, kao kod starih messengera var wa = Screen.FromControl(this).WorkingArea; cf.StartPosition = FormStartPosition.Manual; int n = chats.Count - 1; cf.Location = new Point(Math.Max(wa.Left, Left - cf.Width - Theme.D(12) - n * Theme.D(24)), Math.Max(wa.Top, Math.Min(wa.Bottom - cf.Height, Top + n * Theme.D(24)))); cf.Show(); } else { if (cf.WindowState == FormWindowState.Minimized) cf.WindowState = FormWindowState.Normal; cf.Activate(); } } public override bool ChatActive(string u) { XChatForm cf; return chats.TryGetValue(u, out cf) && !cf.IsDisposed && cf.ContainsFocus; } public override void CloseChat(string user) { foreach (var kv in chats.ToList()) if (user == null || kv.Key == user) { try { kv.Value.Close(); } catch { } } } public override void ShowVoice() { tabs.Selected = 3; } public override void ShowAddFriend() { using (var f = new XAddFriendForm(App)) f.ShowDialog(Visible ? this : null); } public override void ShowSettings() { if (settingsForm != null && !settingsForm.IsDisposed) { settingsForm.Activate(); return; } settingsForm = new XSettingsForm(settings); settingsForm.Show(); } protected override void OnFormClosing(FormClosingEventArgs e) { if (WindowState == FormWindowState.Normal) { Prefs.Set("xf_w", ClientSize.Width); Prefs.Set("xf_h", ClientSize.Height); } if (!AllowClose && e.CloseReason == CloseReason.UserClosing) { e.Cancel = true; Hide(); App.HiddenHint(); } else CloseChat(null); base.OnFormClosing(e); } } } // ===================== Servers.cs ===================== using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace Fragline { class ServerInfo { public long Id; public string Name, Host, Game, GameName, Icon, Title, Map, Folder, GameDesc; public int Port, Players, Max, Bots, Ping = -1; public bool Official, Mine, Online, Queried, Busy; public string Address { get { return Host + ":" + Port; } } } class PlayerInfo { public string Name; public int Score, Ping = -1; public float Seconds = -1; } // Valveov upit za servere (A2S): radi za CS 1.6, CS:S, CS2, TF2, Rust i druge igre koje ga podrzavaju. // Upit ide izravno s ovog racunala, pa je ping stvaran. static class A2S { static readonly Dictionary dns = new Dictionary(); static IPAddress Resolve(string host) { IPAddress ip; if (IPAddress.TryParse(host, out ip)) return ip; lock (dns) if (dns.TryGetValue(host, out ip)) return ip; ip = Dns.GetHostAddresses(host).FirstOrDefault(a => a.AddressFamily == AddressFamily.InterNetwork); if (ip != null) lock (dns) dns[host] = ip; return ip; } static byte[] Packet(byte type, string payload, byte[] challenge) { var l = new List { 0xFF, 0xFF, 0xFF, 0xFF, type }; if (payload != null) { l.AddRange(Encoding.ASCII.GetBytes(payload)); l.Add(0); } if (challenge != null) l.AddRange(challenge); return l.ToArray(); } static byte[] Exchange(UdpClient udp, IPEndPoint ep, byte[] req) { udp.Send(req, req.Length, ep); var from = new IPEndPoint(IPAddress.Any, 0); var data = udp.Receive(ref from); if (data.Length < 5) throw new InvalidOperationException("kratak odgovor"); if (data[0] == 0xFE) throw new NotSupportedException("razlomljen odgovor"); return data; } class Reader { readonly byte[] d; public int Pos; public Reader(byte[] d, int pos) { this.d = d; Pos = pos; } public byte Byte() { return Pos < d.Length ? d[Pos++] : (byte)0; } public int Int() { int v = Pos + 4 <= d.Length ? BitConverter.ToInt32(d, Pos) : 0; Pos += 4; return v; } public float Float() { float v = Pos + 4 <= d.Length ? BitConverter.ToSingle(d, Pos) : 0; Pos += 4; return v; } public string Str() { int s = Pos; while (Pos < d.Length && d[Pos] != 0) Pos++; var r = Encoding.UTF8.GetString(d, s, Pos - s); Pos++; return r; } } public static void Query(ServerInfo s, int timeout = 1500) { try { var ip = Resolve(s.Host); if (ip == null) throw new InvalidOperationException("nema adrese"); using (var udp = new UdpClient(ip.AddressFamily)) { udp.Client.ReceiveTimeout = timeout; var ep = new IPEndPoint(ip, s.Port); var sw = Stopwatch.StartNew(); var data = Exchange(udp, ep, Packet(0x54, "Source Engine Query", null)); if (data[4] == 0x41 && data.Length >= 9) { // server trazi potvrdu (challenge) pa ponovi upit s njom sw.Restart(); data = Exchange(udp, ep, Packet(0x54, "Source Engine Query", data.Skip(5).Take(4).ToArray())); } s.Ping = (int)sw.ElapsedMilliseconds; var r = new Reader(data, 5); if (data[4] == 0x49) { r.Byte(); s.Title = r.Str(); s.Map = r.Str(); s.Folder = r.Str(); s.GameDesc = r.Str(); r.Byte(); r.Byte(); s.Players = r.Byte(); s.Max = r.Byte(); s.Bots = r.Byte(); } else if (data[4] == 0x6D) { r.Str(); s.Title = r.Str(); s.Map = r.Str(); s.Folder = r.Str(); s.GameDesc = r.Str(); s.Players = r.Byte(); s.Max = r.Byte(); s.Bots = 0; } else throw new InvalidOperationException("nepoznat odgovor"); s.Online = true; } } catch { // stariji Call of Duty i Quake serveri ne znaju A2S, pa probaj njihov "getstatus" List ignored; if (!Q3(s, timeout, out ignored)) { s.Online = false; s.Ping = -1; } } finally { s.Queried = true; } } static string StripColors(string t) { return System.Text.RegularExpressions.Regex.Replace(t ?? "", @"\^[0-9a-zA-Z]", ""); } // Quake 3 protokol: "statusResponse\n\kljuc\vrijednost...\n" pa redovi igraca: bodovi ping "ime" static bool Q3(ServerInfo s, int timeout, out List players) { players = new List(); try { var ip = Resolve(s.Host); if (ip == null) return false; using (var udp = new UdpClient(ip.AddressFamily)) { udp.Client.ReceiveTimeout = timeout; var ep = new IPEndPoint(ip, s.Port); var req = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }.Concat(Encoding.ASCII.GetBytes("getstatus")).ToArray(); var sw = Stopwatch.StartNew(); udp.Send(req, req.Length, ep); var from = new IPEndPoint(IPAddress.Any, 0); var data = udp.Receive(ref from); var text = Encoding.UTF8.GetString(data, 4, data.Length - 4); if (!text.StartsWith("statusResponse")) return false; var lines = text.Split('\n'); if (lines.Length < 2) return false; var kv = new Dictionary(StringComparer.OrdinalIgnoreCase); var parts = lines[1].Split('\\'); for (int i = 1; i + 1 < parts.Length; i += 2) kv[parts[i]] = parts[i + 1]; foreach (var l in lines.Skip(2)) { var m = System.Text.RegularExpressions.Regex.Match(l, "^(-?\\d+) (\\d+) \"(.*)\""); if (m.Success) players.Add(new PlayerInfo { Score = int.Parse(m.Groups[1].Value), Ping = int.Parse(m.Groups[2].Value), Name = StripColors(m.Groups[3].Value) }); } string v; s.Ping = (int)sw.ElapsedMilliseconds; s.Title = StripColors(kv.TryGetValue("sv_hostname", out v) ? v : s.Name); s.Map = kv.TryGetValue("mapname", out v) ? v : "?"; s.Folder = kv.TryGetValue("fs_game", out v) && v.Length > 0 ? v : kv.TryGetValue("gamename", out v) ? v : null; s.GameDesc = kv.TryGetValue("gamename", out v) ? v : null; int max; s.Max = kv.TryGetValue("sv_maxclients", out v) && int.TryParse(v, out max) ? max : 0; s.Players = players.Count; s.Bots = 0; s.Online = true; return true; } } catch { return false; } } public static List Players(ServerInfo s, int timeout = 2000) { List viaA2S = null; try { viaA2S = PlayersA2S(s, timeout); } catch { } if (viaA2S != null && viaA2S.Count > 0) return viaA2S; List q3; if (Q3(s, timeout, out q3) && q3.Count > 0) return q3.OrderByDescending(p => p.Score).ToList(); return viaA2S ?? new List(); } static List PlayersA2S(ServerInfo s, int timeout) { var list = new List(); var ip = Resolve(s.Host); if (ip == null) return list; using (var udp = new UdpClient(ip.AddressFamily)) { udp.Client.ReceiveTimeout = timeout; var ep = new IPEndPoint(ip, s.Port); var data = Exchange(udp, ep, Packet(0x55, null, new byte[] { 0xFF, 0xFF, 0xFF, 0xFF })); if (data[4] == 0x41 && data.Length >= 9) data = Exchange(udp, ep, Packet(0x55, null, data.Skip(5).Take(4).ToArray())); if (data[4] != 0x44) return list; var r = new Reader(data, 5); int n = r.Byte(); for (int i = 0; i < n && r.Pos < data.Length; i++) { r.Byte(); var p = new PlayerInfo { Name = r.Str(), Score = r.Int(), Seconds = r.Float() }; if (p.Name.Length > 0) list.Add(p); } } return list.OrderByDescending(p => p.Score).ToList(); } // Koja igra je na serveru: odabrana pri dodavanju, a ako nije, iz onoga sto server javi static string Kind(ServerInfo s) { var g = (s.Game ?? "").ToLowerInvariant(); var d = ((s.GameDesc ?? "") + " " + (s.Folder ?? "")).ToLowerInvariant(); if (g == "cod4" || d.Contains("call of duty 4") || d.Contains("cod4")) return "cod4"; if (g == "samp" || d.Contains("san andreas multiplayer") || d.Contains("sa-mp")) return "samp"; if (g == "fivem" || d.Contains("fivem")) return "fivem"; return "steam"; } public static string JoinUrl(ServerInfo s) { switch (Kind(s)) { case "cod4": return "cod4://" + s.Address; case "samp": return "samp://" + s.Address; case "fivem": return "fivem://connect/" + s.Address; default: return "steam://connect/" + s.Address; } } public static void Join(TrayApp app, ServerInfo s) { var url = JoinUrl(s); try { Process.Start(url); } catch { try { Clipboard.SetText(s.Address); } catch { } var what = url.Substring(0, url.IndexOf(':')); app.Tip("Igra se ne može pokrenuti preko " + what + "://. Adresa " + s.Address + " je kopirana, u konzoli igre upiši: connect " + s.Address, ToolTipIcon.Warning); } } public static Color PingColor(int ms) { return ms < 0 ? Theme.Muted : ms < 70 ? Theme.On : ms < 140 ? Theme.Idle : Theme.Red; } } // Lista game servera (u oba rasporeda) class ServersView : UserControl { readonly TrayApp app; readonly Panel bar = new Panel(); readonly TextBox search = new TextBox(); readonly Panel searchBox = new Panel(); readonly DropDown gameFilter = new DropDown(); readonly FlatButton add = new FlatButton("Dodaj server", true) { Glyph = Theme.IcoAdd }, refreshBtn = new FlatButton("Osvježi", false) { Glyph = "" }; readonly ListBox list = new SmoothList(); readonly Label empty = new Label(); readonly System.Windows.Forms.Timer tick = new System.Windows.Forms.Timer { Interval = 30000 }; readonly SemaphoreSlim gate = new SemaphoreSlim(12); List servers = new List(); List rows = new List(); List> games = new List>(); bool admin, loading, loaded; int hover = -1; bool overJoin; Point mouse; string lastSig = ""; public bool IsAdmin { get { return admin; } } public List> Games { get { return games; } } public ServersView(TrayApp app) { this.app = app; BackColor = Theme.Bg; bar.BackColor = Theme.Bg; searchBox.BackColor = Theme.Bg; searchBox.Paint += (s, e) => { e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; using (var p = Theme.Round(new Rectangle(0, 0, searchBox.Width - 1, searchBox.Height - 1), Theme.D(4))) using (var b = new SolidBrush(Theme.Field)) e.Graphics.FillPath(b, p); }; search.BorderStyle = BorderStyle.None; search.BackColor = Theme.Field; search.ForeColor = Theme.Text; search.Font = Theme.Body(13.5f); Theme.Cue(search, "Traži server ili mapu"); search.TextChanged += (s, e) => Rebuild(); searchBox.Controls.Add(search); Ui.Combo(gameFilter); gameFilter.Items.Add("Sve igre"); gameFilter.SelectedIndex = 0; gameFilter.SelectedIndexChanged += (s, e) => Rebuild(); foreach (var b in new[] { add, refreshBtn }) b.Font = Theme.Display(13.5f); add.Click += (s, e) => ShowAdd(); refreshBtn.Click += (s, e) => { if (!loaded) LoadServers(); else QueryAll(); }; bar.Controls.AddRange(new Control[] { searchBox, gameFilter, add, refreshBtn }); list.DrawMode = DrawMode.OwnerDrawVariable; list.BorderStyle = BorderStyle.None; list.BackColor = Theme.Bg; list.IntegralHeight = false; list.SelectionMode = SelectionMode.None; list.MeasureItem += (s, e) => { e.ItemHeight = e.Index >= 0 && e.Index < rows.Count && rows[e.Index] is string ? Theme.D(36) : Theme.D(58); }; list.DrawItem += DrawRow; list.MouseMove += (s, e) => { mouse = e.Location; int i = list.IndexFromPoint(e.Location); bool oj = i >= 0 && i < rows.Count && rows[i] is ServerInfo && JoinBtn(list.GetItemRectangle(i)).Contains(e.Location); if (i == hover && oj == overJoin) return; hover = i; overJoin = oj; list.Cursor = i >= 0 && i < rows.Count && rows[i] is ServerInfo ? Cursors.Hand : Cursors.Default; list.Invalidate(); }; list.MouseLeave += (s, e) => { hover = -1; list.Invalidate(); }; list.MouseDown += (s, e) => { int i = list.IndexFromPoint(e.Location); if (i < 0 || i >= rows.Count) return; var sv = rows[i] as ServerInfo; if (sv == null) return; if (e.Button == MouseButtons.Right) { Menu(sv, e.Location); return; } if (JoinBtn(list.GetItemRectangle(i)).Contains(e.Location)) A2S.Join(app, sv); else if (e.Clicks >= 2) ShowInfo(sv); }; Theme.DarkScroll(list); empty.Font = Theme.Body(14); empty.ForeColor = Theme.Muted; empty.BackColor = Theme.Bg; empty.TextAlign = ContentAlignment.MiddleCenter; empty.Text = "Učitavam servere..."; Controls.AddRange(new Control[] { bar, list, empty }); tick.Tick += (s, e) => { if (Visible && loaded) QueryAll(); }; tick.Start(); } bool Narrow { get { return Width < Theme.D(560); } } protected override void OnResize(EventArgs e) { base.OnResize(e); if (empty == null) return; int pad = Theme.D(Narrow ? 8 : 20), h = Theme.D(34); int bh = Narrow ? Theme.D(92) : Theme.D(58); bar.SetBounds(0, 0, Width, bh); int aw = Narrow ? h : TextRenderer.MeasureText(add.Text, add.Font).Width + Theme.D(52); int rw = Narrow ? h : TextRenderer.MeasureText(refreshBtn.Text, refreshBtn.Font).Width + Theme.D(52); add.Text = Narrow ? "" : "Dodaj server"; refreshBtn.Text = Narrow ? "" : "Osvježi"; if (Narrow) { searchBox.SetBounds(pad, Theme.D(10), Width - pad * 2, h); gameFilter.SetBounds(pad, Theme.D(50), Width - pad * 3 - aw - rw - Theme.D(8), h); refreshBtn.SetBounds(Width - pad - rw, Theme.D(50), rw, h); add.SetBounds(refreshBtn.Left - Theme.D(8) - aw, Theme.D(50), aw, h); } else { add.SetBounds(Width - pad - aw, Theme.D(12), aw, h); refreshBtn.SetBounds(add.Left - Theme.D(8) - rw, Theme.D(12), rw, h); gameFilter.SetBounds(refreshBtn.Left - Theme.D(8) - Theme.D(190), Theme.D(12), Theme.D(190), h); searchBox.SetBounds(pad, Theme.D(12), Math.Max(Theme.D(80), gameFilter.Left - Theme.D(8) - pad), h); } search.SetBounds(Theme.D(10), (h - search.PreferredHeight) / 2, Math.Max(10, searchBox.Width - Theme.D(20)), search.PreferredHeight); var lr = new Rectangle(pad, bh, Math.Max(10, Width - pad * 2), Math.Max(10, Height - bh)); list.Bounds = lr; empty.Bounds = lr; } protected override void OnVisibleChanged(EventArgs e) { base.OnVisibleChanged(e); if (Visible) { if (!loaded) LoadServers(); else QueryAll(); } } public async void LoadServers() { if (loading) return; loading = true; try { var res = await Task.Run(() => app.ApiGet("/client/servers")); var o = J.Parse(res); admin = J.B(o, "admin"); games = J.A(o, "games").Select(g => Tuple.Create(J.S(g, "slug"), J.S(g, "name"))).ToList(); var old = servers.ToDictionary(x => x.Id); var nl = new List(); foreach (var x in J.A(o, "servers")) { long id = J.L(x, "id"); ServerInfo s; if (!old.TryGetValue(id, out s)) s = new ServerInfo { Id = id }; s.Name = J.S(x, "name"); s.Host = J.S(x, "host"); s.Port = (int)J.L(x, "port"); s.Game = J.S(x, "game"); s.GameName = J.S(x, "game_name"); s.Icon = J.S(x, "icon"); s.Official = J.B(x, "official"); s.Mine = J.B(x, "mine"); nl.Add(s); } servers = nl; loaded = true; var gsel = gameFilter.SelectedIndex > 0 ? Convert.ToString(gameFilter.Items[gameFilter.SelectedIndex]) : null; gameFilter.Items.Clear(); gameFilter.Items.Add("Sve igre"); foreach (var gname in servers.Select(x => x.GameName).Where(x => x != null).Distinct().OrderBy(x => x)) gameFilter.Items.Add(gname); int gi = gsel != null ? gameFilter.Items.IndexOf(gsel) : 0; gameFilter.SelectedIndex = Math.Max(0, gi); Rebuild(); QueryAll(); } catch (WebException ex) { empty.Text = Api.ErrorMessage(ex); empty.Visible = true; list.Visible = false; } catch (Exception ex) { empty.Text = "Servere trenutno ne mogu učitati: " + ex.Message; } finally { loading = false; } } // svi serveri istovremeno (najvise 12 odjednom), lista se osvjezava kako koji odgovori public void QueryAll() { foreach (var s in servers.ToList()) { if (s.Busy) continue; s.Busy = true; var sv = s; Task.Run(async () => { await gate.WaitAsync(); try { A2S.Query(sv); } finally { gate.Release(); sv.Busy = false; try { if (IsHandleCreated) BeginInvoke((Action)Rebuild); } catch { } } }); } } void Rebuild() { var q = search.Text.Trim(); string g = gameFilter.SelectedIndex > 0 ? Convert.ToString(gameFilter.Items[gameFilter.SelectedIndex]) : null; Func ok = s => (g == null || s.GameName == g) && (q.Length == 0 || (s.Name ?? "").IndexOf(q, StringComparison.OrdinalIgnoreCase) >= 0 || (s.Title ?? "").IndexOf(q, StringComparison.OrdinalIgnoreCase) >= 0 || (s.Map ?? "").IndexOf(q, StringComparison.OrdinalIgnoreCase) >= 0 || s.Address.IndexOf(q, StringComparison.OrdinalIgnoreCase) >= 0); // online prvi, pa po broju igraca, pa po pingu Func, List> sort = l => l.Where(ok).OrderByDescending(s => s.Online).ThenByDescending(s => s.Players).ThenBy(s => s.Ping < 0 ? 9999 : s.Ping).ThenBy(s => s.Name).ToList(); var off = sort(servers.Where(s => s.Official)); var mine = sort(servers.Where(s => !s.Official)); var nr = new List(); if (off.Count > 0) { nr.Add("SLUŽBENI SERVERI — " + off.Count); nr.AddRange(off); } if (mine.Count > 0) { nr.Add("MOJI SERVERI — " + mine.Count); nr.AddRange(mine); } rows = nr; list.Visible = nr.Count > 0; empty.Visible = nr.Count == 0; if (nr.Count == 0) empty.Text = !loaded ? "Učitavam servere..." : servers.Count == 0 ? "Još nema servera na listi.\nKlikni \"Dodaj server\" i upiši adresu, npr. 95.211.43.191:27015." : "Nijedan server ne odgovara traženju."; var sig = string.Join("|", nr.Select(x => x is ServerInfo ? "s" + ((ServerInfo)x).Id : (string)x).ToArray()); if (sig != lastSig) { lastSig = sig; int top = list.Items.Count > 0 ? list.TopIndex : 0; list.BeginUpdate(); list.Items.Clear(); foreach (var x in nr) list.Items.Add(x); list.EndUpdate(); if (top < list.Items.Count) list.TopIndex = top; } else list.Invalidate(); } Rectangle JoinBtn(Rectangle b) { int w = Theme.D(64), h = Theme.D(30); return new Rectangle(b.Right - Theme.D(10) - w, b.Y + (b.Height - h) / 2, w, h); } void Menu(ServerInfo s, Point at) { var menu = Ui.Menu(); menu.Items.Add(new ToolStripMenuItem("Uđi na server", null, (a, b) => A2S.Join(app, s)) { Font = Theme.Body(13.5f, FontStyle.Bold) }); menu.Items.Add(new ToolStripMenuItem("Igrači i detalji...", null, (a, b) => ShowInfo(s))); menu.Items.Add(new ToolStripMenuItem("Kopiraj adresu (" + s.Address + ")", null, (a, b) => { try { Clipboard.SetText(s.Address); } catch { } })); menu.Items.Add(new ToolStripMenuItem("Osvježi", null, (a, b) => Task.Run(() => { A2S.Query(s); try { BeginInvoke((Action)Rebuild); } catch { } }))); if (s.Mine || (s.Official && admin)) { menu.Items.Add(new ToolStripSeparator()); menu.Items.Add(new ToolStripMenuItem("Makni s liste", null, (a, b) => Remove(s)) { ForeColor = Theme.Red }); } menu.Show(list, at); } async void Remove(ServerInfo s) { if (!Dialog.Confirm(this, "Makni server", "Maknuti server \"" + s.Name + "\" s liste?", "Makni s liste", true, null, s.Official ? "To je službeni server, nestat će svima." : null)) return; try { await Task.Run(() => app.ApiDel("/client/servers/" + s.Id)); servers.Remove(s); Rebuild(); } catch (WebException e) { app.Tip(Api.ErrorMessage(e), ToolTipIcon.Warning); } catch { } } public void ShowAdd() { using (var f = new ServerAddForm(app, this)) if (f.ShowDialog(FindForm()) == DialogResult.OK) LoadServers(); } void ShowInfo(ServerInfo s) { new ServerInfoForm(app, s).Show(FindForm()); } void DrawRow(object sender, DrawItemEventArgs e) { if (e.Index < 0 || e.Index >= rows.Count) return; var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; var b = e.Bounds; using (var fill = new SolidBrush(Theme.Bg)) g.FillRectangle(fill, b); var label = rows[e.Index] as string; if (label != null) { using (var f = Theme.Body(12, FontStyle.Bold)) Txt.Draw(g, label, f, new Rectangle(b.X + Theme.D(6), b.Y + Theme.D(10), b.Width, b.Height - Theme.D(10)), Theme.Muted); return; } var s = (ServerInfo)rows[e.Index]; bool hot = e.Index == hover; var bg = hot ? Theme.Panel2 : Theme.Bg; if (hot) using (var p = Theme.Round(b, Theme.D(8))) using (var fill = new SolidBrush(bg)) g.FillPath(fill, p); else using (var pen = new Pen(Theme.Line)) g.DrawLine(pen, b.X + Theme.D(54), b.Bottom - 1, b.Right - Theme.D(6), b.Bottom - 1); var ir = new Rectangle(b.X + Theme.D(8), b.Y + (b.Height - Theme.D(38)) / 2, Theme.D(38), Theme.D(38)); var img = ImageCache.Get(s.Icon, list); if (img != null) g.DrawImage(Theme.Masked(img, ir.Width, ir.Height, Theme.D(8)), ir.X, ir.Y, ir.Width, ir.Height); else { using (var p = Theme.Round(ir, Theme.D(8))) using (var fill = new SolidBrush(Theme.Panel3)) g.FillPath(fill, p); Theme.Icon(g, Theme.IcoGame, "S", ir, Theme.Muted, 18); } // status servera: zelena tocka = odgovara float ds = Theme.D(12); Theme.StatusDot(g, new RectangleF(ir.Right - ds * 0.7f, ir.Bottom - ds * 0.7f, ds, ds), !s.Queried ? "idle" : s.Online ? "online" : "off", bg); int x = ir.Right + Theme.D(14), right = b.Right - Theme.D(10); var jb = JoinBtn(b); if (hot && s.Online) { bool over = jb.Contains(mouse); using (var p = Theme.Round(jb, Theme.D(4))) using (var fill = new SolidBrush(over ? Theme.Shade(Theme.On, -0.15f) : Theme.Shade(Theme.On, -0.05f))) g.FillPath(fill, p); using (var f = Theme.Display(13.5f)) TextRenderer.DrawText(g, "Uđi", f, jb, Color.White, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding); right = jb.X - Theme.D(10); } // ping kao obojena oznaka string ping = !s.Queried || s.Busy && s.Ping < 0 ? "..." : s.Online ? s.Ping + " ms" : "offline"; using (var f = Theme.Display(12.5f)) { var psz = TextRenderer.MeasureText(ping, f); var pr = new Rectangle(right - psz.Width - Theme.D(14), b.Y + (b.Height - Theme.D(22)) / 2, psz.Width + Theme.D(14), Theme.D(22)); var pc = s.Online ? A2S.PingColor(s.Ping) : Theme.Muted; using (var p = Theme.Round(pr, Theme.D(11))) using (var fill = new SolidBrush(Color.FromArgb(40, pc))) g.FillPath(fill, p); TextRenderer.DrawText(g, ping, f, pr, pc, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding); right = pr.X - Theme.D(10); } int w = Math.Max(10, right - x); using (var f = Theme.Display(15)) Txt.Draw(g, s.Online && !string.IsNullOrEmpty(s.Title) && s.Name == s.Address ? s.Title : s.Name, f, new Rectangle(x, b.Y + Theme.D(9), w, Theme.D(22)), s.Online || !s.Queried ? Theme.Text : Theme.Muted); string line; if (!s.Queried) line = "Provjeravam server..."; else if (!s.Online) line = s.Address + " · ne odgovara"; else line = (s.Map ?? "?") + " · " + s.Players + "/" + s.Max + " igrača" + (s.Bots > 0 ? " (" + s.Bots + " bot" + (s.Bots == 1 ? "" : "a") + ")" : "") + (s.GameName != null && !Narrow ? " · " + s.GameName : ""); using (var f = Theme.Body(12.5f)) Txt.Draw(g, line, f, new Rectangle(x, b.Y + Theme.D(31), w, Theme.D(18)), Theme.Muted); } } // Dodavanje servera na listu class ServerAddForm : DarkForm { public ServerAddForm(TrayApp app, ServersView view) { Text = "Fragline - dodaj server"; MinimizeBox = false; int W = Theme.D(420), pad = Theme.D(24), inner = W - pad * 2; bool admin = view.IsAdmin; ClientSize = new Size(W, Theme.D(admin ? 430 : 390)); Controls.Add(Ui.Lbl("Dodaj server", 20, Theme.Text, pad, Theme.D(18), true)); Controls.Add(Ui.Lbl("Adresa servera s portom, kao u konzoli igre.", 13, Theme.Muted, pad, Theme.D(52))); Controls.Add(Ui.Lbl("ADRESA", 12, Theme.Text2, pad, Theme.D(84), true)); var addr = new InputBox(false); addr.SetBounds(pad, Theme.D(106), inner, Theme.D(40)); Theme.Cue(addr.Box, "npr. 95.211.43.191:27015"); Controls.Add(Ui.Lbl("IME (NEOBAVEZNO)", 12, Theme.Text2, pad, Theme.D(156), true)); var name = new InputBox(false); name.SetBounds(pad, Theme.D(178), inner, Theme.D(40)); name.Box.MaxLength = 80; Theme.Cue(name.Box, "Ako ostane prazno, piše ime sa servera"); Controls.Add(Ui.Lbl("IGRA", 12, Theme.Text2, pad, Theme.D(228), true)); var game = new DropDown(); Ui.Combo(game); game.SetBounds(pad, Theme.D(250), inner, Theme.D(36)); game.Items.Add("Ostalo / ne znam"); foreach (var gm in view.Games) game.Items.Add(gm.Item2); game.SelectedIndex = 0; var official = new Toggle(); int y = Theme.D(298); if (admin) { Controls.Add(Ui.Lbl("Službeni server (vide ga svi)", 13.5f, Theme.Text, pad, y + Theme.D(2))); official.Location = new Point(W - pad - official.Width, y); Controls.Add(official); y += Theme.D(40); } var err = Ui.Lbl("", 13, Theme.Red, pad, y); err.AutoSize = false; err.Size = new Size(inner, Theme.D(22)); var cancel = new FlatButton("Odustani", false); var ok = new FlatButton("Dodaj server", true); int half = (inner - Theme.D(10)) / 2; cancel.SetBounds(pad, ClientSize.Height - Theme.D(60), half, Theme.D(40)); ok.SetBounds(pad + half + Theme.D(10), ClientSize.Height - Theme.D(60), half, Theme.D(40)); cancel.Click += (s, e) => Close(); ok.Click += async (s, e) => { var a = addr.Value.Trim().Replace("connect ", "").Trim(); int port = 27015; string host = a; int ci = a.LastIndexOf(':'); if (ci > 0 && !int.TryParse(a.Substring(ci + 1), out port)) { err.Text = "Port iza dvotočke mora biti broj."; return; } if (ci > 0) host = a.Substring(0, ci); if (host.Length == 0) { err.Text = "Upiši adresu servera."; return; } var body = new Dictionary { { "host", host }, { "port", port }, { "name", name.Value.Trim() } }; if (game.SelectedIndex > 0) body["game"] = view.Games[game.SelectedIndex - 1].Item1; if (admin && official.Checked) body["official"] = true; ok.Enabled = false; err.Text = ""; try { var res = await Task.Run(() => app.ApiPost("/client/servers", body)); app.Tip(Api.Field(res, "message") ?? "Server je dodan."); DialogResult = DialogResult.OK; Close(); } catch (WebException ex) { err.Text = Api.ErrorMessage(ex); } catch (Exception ex) { err.Text = ex.Message; } finally { if (!IsDisposed) ok.Enabled = true; } }; Controls.AddRange(new Control[] { addr, name, game, err, cancel, ok }); AcceptButton = ok; Shown += (s, e) => addr.Box.Focus(); } } // Detalji servera i popis igraca class ServerInfoForm : DarkForm { readonly TrayApp app; readonly ServerInfo s; readonly ListBox players = new SmoothList(); readonly Label info, state; List list = new List(); public ServerInfoForm(TrayApp app, ServerInfo s) { this.app = app; this.s = s; Text = s.Name + " - Fragline"; FormBorderStyle = FormBorderStyle.Sizable; MinimizeBox = true; int W = Theme.D(440), pad = Theme.D(20); ClientSize = new Size(W, Theme.D(520)); MinimumSize = new Size(Theme.D(360), Theme.D(400)); var title = new Label { Text = string.IsNullOrEmpty(s.Title) ? s.Name : s.Title, Font = Theme.Body(18, FontStyle.Bold), ForeColor = Theme.Text, AutoEllipsis = true, BackColor = Theme.Bg }; title.SetBounds(pad, Theme.D(16), W - pad * 2, Theme.D(30)); info = new Label { Font = Theme.Body(13), ForeColor = Theme.Muted, BackColor = Theme.Bg, AutoEllipsis = true }; info.SetBounds(pad, Theme.D(48), W - pad * 2, Theme.D(40)); state = new Label { Text = "IGRAČI", Font = Theme.Body(12, FontStyle.Bold), ForeColor = Theme.Muted, BackColor = Theme.Bg }; state.SetBounds(pad, Theme.D(96), W - pad * 2, Theme.D(20)); players.SetBounds(pad, Theme.D(120), W - pad * 2, ClientSize.Height - Theme.D(120) - Theme.D(76)); players.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom; title.Anchor = info.Anchor = state.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; players.DrawMode = DrawMode.OwnerDrawFixed; players.ItemHeight = Theme.D(34); players.BorderStyle = BorderStyle.None; players.BackColor = Theme.Panel; players.IntegralHeight = false; players.SelectionMode = SelectionMode.None; players.DrawItem += DrawPlayer; Theme.DarkScroll(players); var copy = new FlatButton("Kopiraj adresu", false); var join = new FlatButton("Uđi na server", true) { Back = Theme.Shade(Theme.On, -0.1f) }; int half = (W - pad * 2 - Theme.D(10)) / 2; copy.SetBounds(pad, ClientSize.Height - Theme.D(58), half, Theme.D(40)); join.SetBounds(pad + half + Theme.D(10), ClientSize.Height - Theme.D(58), half, Theme.D(40)); copy.Anchor = AnchorStyles.Bottom | AnchorStyles.Left; join.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; copy.Click += (a, b) => { try { Clipboard.SetText(s.Address); app.Tip("Adresa " + s.Address + " je kopirana."); } catch { } }; join.Click += (a, b) => A2S.Join(app, s); Controls.AddRange(new Control[] { title, info, state, players, copy, join }); UpdateInfo(); Shown += (a, b) => Reload(); } void UpdateInfo() { info.Text = s.Address + (s.GameName != null ? " · " + s.GameName : "") + "\n" + (s.Online ? "Mapa " + s.Map + " · " + s.Players + "/" + s.Max + " igrača · ping " + s.Ping + " ms" : s.Queried ? "Server trenutno ne odgovara." : "Provjeravam..."); } async void Reload() { state.Text = "IGRAČI · učitavam..."; try { await Task.Run(() => A2S.Query(s)); UpdateInfo(); list = await Task.Run(() => A2S.Players(s)); players.BeginUpdate(); players.Items.Clear(); foreach (var p in list) players.Items.Add(p); players.EndUpdate(); state.Text = "IGRAČI — " + list.Count + (list.Count == 0 && s.Online ? " (prazno ili server skriva igrače)" : ""); } catch { state.Text = s.Online ? "IGRAČI · server ne daje popis igrača" : "IGRAČI · server ne odgovara"; } } void DrawPlayer(object sender, DrawItemEventArgs e) { if (e.Index < 0 || e.Index >= list.Count) return; var p = list[e.Index]; var g = e.Graphics; var b = e.Bounds; using (var fill = new SolidBrush(e.Index % 2 == 0 ? Theme.Panel : Theme.Shade(Theme.Panel, Theme.Light ? -0.03f : 0.03f))) g.FillRectangle(fill, b); var t = TimeSpan.FromSeconds(Math.Max(0, p.Seconds)); string time = p.Seconds < 0 ? (p.Ping >= 0 ? p.Ping + " ms" : "") : t.TotalHours >= 1 ? (int)t.TotalHours + " h " + t.Minutes + " min" : t.Minutes + " min"; using (var f = Theme.Body(13)) { Txt.Draw(g, (e.Index + 1) + ".", f, new Rectangle(b.X + Theme.D(10), b.Y, Theme.D(30), b.Height), Theme.Muted); Txt.Draw(g, time, f, new Rectangle(b.Right - Theme.D(90), b.Y, Theme.D(80), b.Height), Theme.Muted, TextFormatFlags.Right); Txt.Draw(g, p.Score.ToString(), f, new Rectangle(b.Right - Theme.D(150), b.Y, Theme.D(50), b.Height), Theme.Text2, TextFormatFlags.Right); } using (var f = Theme.Display(13.5f)) Txt.Draw(g, p.Name, f, new Rectangle(b.X + Theme.D(42), b.Y, Math.Max(10, b.Width - Theme.D(200)), b.Height), Theme.Text); } } } // ===================== ServerDetect.cs ===================== using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Runtime.InteropServices; namespace Fragline { // Prepoznavanje game servera na kojem korisnik igra, iz mrežnih veza procesa igre. // TCP igre (Minecraft, Metin2, FiveM): GetExtendedTcpTable daje udaljenu adresu. // UDP igre (CS 1.6/CS2, Rust, SA-MP): udaljena adresa se NE vidi bez ETW-a // (Microsoft-Windows-Kernel-Network + admin prava). Bez toga tiho preskačemo, // kako nalaže specifikacija. static class ServerScan { [DllImport("user32.dll")] static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid); [DllImport("iphlpapi.dll", SetLastError = true)] static extern uint GetExtendedTcpTable(IntPtr pTcpTable, ref int dwSize, bool sort, int ipVersion, int tblClass, int reserved); [StructLayout(LayoutKind.Sequential)] struct TcpRow { public uint state, local, localPort, remote, remotePort, pid; } const int AF_INET = 2, TCP_TABLE_OWNER_PID_ALL = 5, MIB_ESTABLISHED = 5; // PID procesa u prvom planu (igra u fullscreenu/fokusu). public static int ForegroundPid() { try { uint p; GetWindowThreadProcessId(GetForegroundWindow(), out p); return (int)p; } catch { return 0; } } // Najčešća javna udaljena TCP adresa procesa, kao "ip:port", ili null. public static string Best(int pid) { if (pid <= 0) return null; return Remotes(pid).GroupBy(x => x).OrderByDescending(g => g.Count()).Select(g => g.Key).FirstOrDefault(); } static List Remotes(int pid) { var outp = new List(); int size = 0; GetExtendedTcpTable(IntPtr.Zero, ref size, false, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0); if (size <= 0) return outp; IntPtr buf = Marshal.AllocHGlobal(size); try { if (GetExtendedTcpTable(buf, ref size, false, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0) != 0) return outp; int n = Marshal.ReadInt32(buf); int rowSz = Marshal.SizeOf(typeof(TcpRow)); IntPtr p = new IntPtr(buf.ToInt64() + 4); for (int i = 0; i < n; i++) { var row = (TcpRow)Marshal.PtrToStructure(p, typeof(TcpRow)); p = new IntPtr(p.ToInt64() + rowSz); if ((int)row.pid != pid || row.state != MIB_ESTABLISHED) continue; var ip = new IPAddress((long)row.remote).ToString(); int port = (int)(((row.remotePort & 0xFF) << 8) | ((row.remotePort >> 8) & 0xFF)); if (port <= 0 || !IsPublic(ip)) continue; outp.Add(ip + ":" + port); } } catch { } finally { Marshal.FreeHGlobal(buf); } return outp; } // Preskoči privatne, loopback, link-local i Valve/Steam relay rasponove. static bool IsPublic(string ip) { var p = ip.Split('.'); if (p.Length != 4) return false; int a, b; if (!int.TryParse(p[0], out a) || !int.TryParse(p[1], out b)) return false; if (a == 0 || a == 10 || a == 127) return false; if (a == 172 && b >= 16 && b <= 31) return false; if (a == 192 && b == 168) return false; if (a == 169 && b == 254) return false; if (a == 155 && b == 133) return false; // Steam relay if (a == 162 && b == 254) return false; // Steam relay if (a == 185 && b == 25) return false; // Steam relay return true; } } } // ===================== Lfg.cs ===================== using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; namespace Fragline { // "Looking for group": popis oglasa igrača koji traže ekipu. // Kartice s gumbom "Javi se" / "Odjavi se" / "Zatvori"; "Objavi oglas" otvara obrazac. Osvježava se svakih 60 s. class LfgList : Control { readonly TrayApp app; List items = new List(); readonly List joinRects = new List(); Rectangle objaviRect; int hover = -1, scroll; bool objaviHot; int CardH { get { return Theme.D(104); } } int TopH { get { return Theme.D(52); } } public LfgList(TrayApp app) { this.app = app; SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); BackColor = Theme.Bg; } public void SetData(List list) { items = list ?? new List(); ClampScroll(); Invalidate(); } void ClampScroll() { int max = Math.Max(0, items.Count * CardH - (Height - TopH) + Theme.D(12)); if (scroll > max) scroll = max; if (scroll < 0) scroll = 0; } protected override void OnResize(EventArgs e) { base.OnResize(e); ClampScroll(); Invalidate(); } protected override void OnMouseWheel(MouseEventArgs e) { scroll -= e.Delta; ClampScroll(); Invalidate(); base.OnMouseWheel(e); } int HitCard(Point p) { for (int i = 0; i < joinRects.Count; i++) if (joinRects[i] != Rectangle.Empty && joinRects[i].Contains(p)) return i; return -1; } protected override void OnMouseMove(MouseEventArgs e) { int h = HitCard(e.Location); bool oh = objaviRect.Contains(e.Location); if (h != hover || oh != objaviHot) { hover = h; objaviHot = oh; Cursor = (h >= 0 || oh) ? Cursors.Hand : Cursors.Default; Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseLeave(EventArgs e) { hover = -1; objaviHot = false; Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseClick(MouseEventArgs e) { if (objaviRect.Contains(e.Location)) { app.NewLfg(); return; } int h = HitCard(e.Location); if (h >= 0 && h < items.Count) { var it = items[h]; if (it.Mine) app.LfgAction(it.Id, true); else if (it.JoinedByMe) app.LfgAction(it.Id, false); else app.JoinLfg(it.Id); } base.OnMouseClick(e); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.Clear(Theme.Bg); g.SmoothingMode = SmoothingMode.AntiAlias; joinRects.Clear(); const TextFormatFlags mid = TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding; int pad = Theme.D(16); // "Objavi oglas" gore desno using (var f = Theme.Display(13.5f)) { int w = TextRenderer.MeasureText("Objavi oglas", f).Width + Theme.D(28), h = Theme.D(34); objaviRect = new Rectangle(Width - pad - w, Theme.D(9), w, h); using (var p = Theme.Round(objaviRect, h / 2f)) using (var b = new SolidBrush(objaviHot ? Theme.Shade(Theme.Button, -0.12f) : Theme.Button)) g.FillPath(b, p); TextRenderer.DrawText(g, "Objavi oglas", f, objaviRect, Theme.OnAccent, mid); } using (var f = Theme.Body(13)) TextRenderer.DrawText(g, "Igrači koji traže ekipu", f, new Rectangle(pad, Theme.D(16), Width - pad * 3, Theme.D(22)), Theme.Muted, TextFormatFlags.VerticalCenter | TextFormatFlags.NoPrefix); if (items.Count == 0) { using (var f = Theme.Body(14)) TextRenderer.DrawText(g, "Trenutno nema oglasa. Budi prvi i klikni \"Objavi oglas\".", f, new Rectangle(pad, TopH + Theme.D(30), Width - pad * 2, Theme.D(40)), Theme.Muted, mid); return; } var clip = new Rectangle(0, TopH, Width, Height - TopH); g.SetClip(clip); for (int i = 0; i < items.Count; i++) { joinRects.Add(Rectangle.Empty); var it = items[i]; var r = new Rectangle(pad, TopH + i * CardH - scroll + Theme.D(5), Width - pad * 2, CardH - Theme.D(10)); if (r.Bottom < TopH || r.Top > Height) continue; bool hot = i == hover; using (var p = Theme.Round(r, Theme.D(14))) { using (var b = new SolidBrush(Theme.Panel)) g.FillPath(b, p); using (var pen = new Pen(hot ? Color.FromArgb(120, Theme.Accent) : Theme.Line)) g.DrawPath(pen, p); } int x = r.X + Theme.D(16), top = r.Y + Theme.D(12); using (var f = Theme.Display(15)) Txt.Draw(g, string.IsNullOrEmpty(it.GameName) ? (it.Game ?? "Igra") : it.GameName, f, new Rectangle(x, top, r.Width - Theme.D(150), Theme.D(22)), Theme.Accent2); using (var f = Theme.Body(12)) Txt.Draw(g, "oglas: " + (it.Author ?? "?"), f, new Rectangle(x, top + Theme.D(23), r.Width - Theme.D(150), Theme.D(18)), Theme.Muted); if (!string.IsNullOrEmpty(it.Note)) using (var f = Theme.Body(13.5f)) TextRenderer.DrawText(g, it.Note, f, new Rectangle(x, top + Theme.D(44), r.Width - Theme.D(150), Theme.D(22)), Theme.Text2, TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix); int inTeam = (it.Joined != null ? it.Joined.Count : 0) + 1; var meta = inTeam + "/" + (it.Slots + 1) + " u ekipi · istječe za " + it.ExpiresMin + " min"; using (var f = Theme.Body(12)) Txt.Draw(g, meta, f, new Rectangle(x, r.Bottom - Theme.D(22), r.Width - Theme.D(150), Theme.D(18)), Theme.Muted); // desno: gumb "Javi se" ili status int bw = Theme.D(110), bh = Theme.D(34); var br = new Rectangle(r.Right - bw - Theme.D(16), r.Y + (r.Height - bh) / 2, bw, bh); if (it.Mine || it.JoinedByMe) { // svoj oglas se moze zatvoriti, a s tudeg se moze odjaviti joinRects[i] = br; var c = it.Mine ? Theme.Red : Theme.Muted; using (var p = Theme.Round(br, bh / 2f)) { if (hot) using (var b = new SolidBrush(Color.FromArgb(40, c))) g.FillPath(b, p); using (var pen = new Pen(c, Theme.D(1.5f))) g.DrawPath(pen, p); } using (var f = Theme.Display(13)) TextRenderer.DrawText(g, it.Mine ? "Zatvori" : "Odjavi se", f, br, it.Mine ? Theme.Red : Theme.Text2, mid); using (var f = Theme.Body(11.5f)) TextRenderer.DrawText(g, it.Mine ? "tvoj oglas" : "prijavljen ✓", f, new Rectangle(br.X, br.Bottom + Theme.D(2), br.Width, Theme.D(16)), it.Mine ? Theme.Muted : Theme.On, TextFormatFlags.HorizontalCenter | TextFormatFlags.NoPrefix); } else { joinRects[i] = br; using (var p = Theme.Round(br, bh / 2f)) using (var b = new LinearGradientBrush(br, Theme.Accent, Theme.Accent2, 0f)) g.FillPath(b, p); using (var f = Theme.Display(13.5f)) TextRenderer.DrawText(g, "Javi se", f, br, Color.White, mid); } } g.ResetClip(); } } } namespace Fragline { class GameItem { public string Slug, Name; public override string ToString() { return Name; } } // Novi oglas "tražim ekipu": sprema se na server i odmah se vidi i na webu (/lfg) class LfgCreateForm : DarkForm { readonly TrayApp app; readonly DropDown game = new DropDown(), slots = new DropDown(), dur = new DropDown(); readonly InputBox note = new InputBox(false); readonly Label err, count; readonly FlatButton ok = new FlatButton("Objavi oglas", true), cancel = new FlatButton("Odustani", false); static readonly int[] Minutes = { 30, 60, 120, 240 }; public LfgCreateForm(TrayApp app, List games) { this.app = app; Text = "Fragline - novi oglas"; MinimizeBox = false; int W = Theme.D(420), pad = Theme.D(24), inner = W - pad * 2; ClientSize = new Size(W, Theme.D(470)); Controls.Add(Ui.Lbl("Tražim ekipu", 20, Theme.Text, pad, Theme.D(20), true)); Controls.Add(Ui.Lbl("Oglas se objavljuje i na webu, u rubrici LFG.", 12.5f, Theme.Muted, pad, Theme.D(52))); Controls.Add(Ui.Lbl("Igra", 13, Theme.Muted, pad, Theme.D(86))); Ui.Combo(game); game.SetBounds(pad, Theme.D(108), inner, Theme.D(38)); foreach (var g in games) game.Items.Add(g); var cur = app.CurrentGame; int gi = cur == null ? -1 : games.FindIndex(g => g.Name == cur); if (game.Items.Count > 0) game.SelectedIndex = Math.Max(0, gi); int half = (inner - Theme.D(12)) / 2; Controls.Add(Ui.Lbl("Koliko igrača tražiš", 13, Theme.Muted, pad, Theme.D(154))); Ui.Combo(slots); slots.SetBounds(pad, Theme.D(176), half, Theme.D(38)); for (int i = 1; i <= 9; i++) slots.Items.Add(i == 1 ? "1 igrača" : i + " igrača"); slots.SelectedIndex = 2; Controls.Add(Ui.Lbl("Oglas traje", 13, Theme.Muted, pad + half + Theme.D(12), Theme.D(154))); Ui.Combo(dur); dur.SetBounds(pad + half + Theme.D(12), Theme.D(176), half, Theme.D(38)); dur.Items.AddRange(new object[] { "30 minuta", "1 sat", "2 sata", "4 sata" }); dur.SelectedIndex = 1; Controls.Add(Ui.Lbl("Poruka (nije obavezno)", 13, Theme.Muted, pad, Theme.D(226))); note.SetBounds(pad, Theme.D(248), inner, Theme.D(42)); note.Box.MaxLength = 140; count = Ui.Lbl("0/140", 11.5f, Theme.Muted, pad, Theme.D(294)); count.AutoSize = false; count.Size = new Size(inner, Theme.D(18)); count.TextAlign = ContentAlignment.TopRight; Controls.Add(Ui.Lbl("npr. \"Treba nam još dvoje za 5v5, mic obavezan\"", 11.5f, Theme.Muted, pad, Theme.D(294))); err = Ui.Lbl(games.Count == 0 ? "Popis igara nije dostupan. Pokušaj ponovno kasnije." : "", 13, Theme.Red, pad, Theme.D(326)); err.AutoSize = false; err.Size = new Size(inner, Theme.D(56)); cancel.SetBounds(pad, Theme.D(400), half, Theme.D(42)); ok.SetBounds(pad + half + Theme.D(12), Theme.D(400), half, Theme.D(42)); ok.Enabled = games.Count > 0; Controls.AddRange(new Control[] { game, slots, dur, note, count, err, cancel, ok }); note.Box.TextChanged += (s, e) => count.Text = note.Value.Length + "/140"; cancel.Click += (s, e) => Close(); ok.Click += async (s, e) => { var g = game.SelectedItem as GameItem; if (g == null) { err.Text = "Odaberi igru."; return; } ok.Enabled = false; ok.Text = "Objavljujem..."; var error = await app.CreateLfg(g.Slug, slots.SelectedIndex + 1, Minutes[Math.Max(0, dur.SelectedIndex)], note.Value.Trim()); ok.Enabled = true; ok.Text = "Objavi oglas"; if (error != null) { err.Text = error; return; } DialogResult = DialogResult.OK; Close(); }; AcceptButton = ok; } } } // ===================== Notifs.cs ===================== using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; namespace Fragline { // Popis obavijesti (komentari, lajkovi, spomeni) za karticu "Obavijesti". // Custom-crtani redovi s vlastitim kotacicem; klik otvara url u pregledniku. class NotifList : Control { List items = new List(); readonly List rows = new List(); int hover = -1, scroll; int RowH { get { return Theme.D(66); } } public NotifList() { SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); BackColor = Theme.Bg; } public void SetData(List list) { items = list ?? new List(); ClampScroll(); Invalidate(); } void ClampScroll() { int max = Math.Max(0, items.Count * RowH - Height + Theme.D(12)); if (scroll > max) scroll = max; if (scroll < 0) scroll = 0; } protected override void OnResize(EventArgs e) { base.OnResize(e); ClampScroll(); Invalidate(); } protected override void OnMouseWheel(MouseEventArgs e) { scroll -= e.Delta; ClampScroll(); Invalidate(); base.OnMouseWheel(e); } int Hit(Point p) { for (int i = 0; i < rows.Count; i++) if (rows[i].Contains(p)) return i; return -1; } protected override void OnMouseMove(MouseEventArgs e) { int h = Hit(e.Location); if (h != hover) { hover = h; Cursor = h >= 0 && h < items.Count && items[h].Url != null ? Cursors.Hand : Cursors.Default; Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseLeave(EventArgs e) { hover = -1; Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseClick(MouseEventArgs e) { int h = Hit(e.Location); if (h >= 0 && h < items.Count && items[h].Url != null) { try { System.Diagnostics.Process.Start(items[h].Url); } catch { } } base.OnMouseClick(e); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.Clear(Theme.Bg); g.SmoothingMode = SmoothingMode.AntiAlias; rows.Clear(); const TextFormatFlags mid = TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding; if (items.Count == 0) { using (var f = Theme.Body(14)) TextRenderer.DrawText(g, "Još nemaš obavijesti. Komentari, lajkovi i spomeni stizat će ovdje.", f, new Rectangle(Theme.D(20), Theme.D(40), Width - Theme.D(40), Theme.D(40)), Theme.Muted, mid); return; } int pad = Theme.D(16); for (int i = 0; i < items.Count; i++) { rows.Add(new Rectangle(0, i * RowH - scroll, Width, RowH)); var r = new Rectangle(pad, i * RowH - scroll + Theme.D(5), Width - pad * 2, RowH - Theme.D(10)); if (r.Bottom < 0 || r.Top > Height) continue; var it = items[i]; bool hot = i == hover; var cc = Theme.Hex(it.WhoColor, Theme.Accent); using (var p = Theme.Round(r, Theme.D(12))) using (var b = new SolidBrush(hot ? Theme.Panel2 : Theme.Panel)) g.FillPath(b, p); var ic = new Rectangle(r.X + Theme.D(12), r.Y + (r.Height - Theme.D(38)) / 2, Theme.D(38), Theme.D(38)); using (var b = new SolidBrush(Theme.Shade(cc, -0.55f))) g.FillEllipse(b, ic); using (var f = Theme.Body(17)) TextRenderer.DrawText(g, string.IsNullOrEmpty(it.Icon) ? "•" : it.Icon, f, ic, cc, mid); int tx = ic.Right + Theme.D(12), tw = r.Right - tx - Theme.D(74); using (var f = Theme.Display(13.5f)) Txt.Draw(g, string.IsNullOrEmpty(it.Who) ? "Fragline" : it.Who, f, new Rectangle(tx, r.Y + Theme.D(9), tw, Theme.D(20)), cc); var body = (it.Text ?? "") + (string.IsNullOrEmpty(it.Extra) ? "" : " — " + it.Extra); using (var f = Theme.Body(13)) TextRenderer.DrawText(g, body, f, new Rectangle(tx, r.Y + Theme.D(30), tw, Theme.D(22)), Theme.Text2, TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix); using (var f = Theme.Body(11.5f)) Txt.Draw(g, it.Ago ?? "", f, new Rectangle(r.Right - Theme.D(70), r.Y + Theme.D(10), Theme.D(62), Theme.D(16)), Theme.Muted); if (it.New) using (var b = new SolidBrush(Theme.Accent2)) g.FillEllipse(b, r.Right - Theme.D(18), r.Y + r.Height / 2 - Theme.D(3), Theme.D(7), Theme.D(7)); } } } } // ===================== Popups.cs ===================== using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; using System.Windows.Forms; namespace Fragline { // Prozor za potvrdu u stilu aplikacije (umjesto sistemskog MessageBoxa) static class Dialog { public static bool Confirm(Control owner, string title, string text, string ok, bool danger, string quote = null, string hint = null) { using (var f = new DialogForm(title, text, quote, hint, ok, "Odustani", danger)) return f.ShowDialog(OwnerOf(owner)) == DialogResult.OK; } public static void Info(Control owner, string title, string text) { using (var f = new DialogForm(title, text, null, null, "U redu", null, false)) f.ShowDialog(OwnerOf(owner)); } static IWin32Window OwnerOf(Control c) { var f = c != null ? c.FindForm() : Form.ActiveForm; return f != null && f.Visible ? f : null; } } class DialogForm : Form { readonly string title, text, quote, hint; readonly bool classic; readonly FlatButton okBtn, cancelBtn; Rectangle textRect, quoteRect, hintRect; int headH, footH; protected override CreateParams CreateParams { get { var cp = base.CreateParams; cp.ClassStyle |= 0x20000; return cp; } } public DialogForm(string title, string text, string quote, string hint, string ok, string cancel, bool danger) { this.title = title; this.text = text; this.quote = quote; this.hint = hint; classic = Theme.ClassicLayout; FormBorderStyle = FormBorderStyle.None; StartPosition = FormStartPosition.CenterParent; ShowInTaskbar = false; KeyPreview = true; DoubleBuffered = true; BackColor = Theme.Bg; Icon = Program.AppIcon; Text = title; AutoScaleMode = AutoScaleMode.None; int W = Theme.D(classic ? 400 : 440), pad = Theme.D(classic ? 16 : 20), inner = W - pad * 2; headH = classic ? Theme.D(30) : Theme.D(62); footH = Theme.D(classic ? 54 : 70); int y = headH + (classic ? Theme.D(16) : 0); using (var f = BodyFont()) { int th = TextRenderer.MeasureText(text ?? "", f, new Size(inner, int.MaxValue), TextFormatFlags.WordBreak | TextFormatFlags.NoPrefix).Height; textRect = new Rectangle(pad, y, inner, th); y += th + Theme.D(14); } if (quote != null) { using (var f = Theme.Body(14)) { int qh = Math.Min(Theme.D(110), TextRenderer.MeasureText(quote, f, new Size(inner - Theme.D(28), int.MaxValue), TextFormatFlags.WordBreak | TextFormatFlags.NoPrefix).Height); quoteRect = new Rectangle(pad, y, inner, qh + Theme.D(24)); y = quoteRect.Bottom + Theme.D(14); } } if (hint != null) { hintRect = new Rectangle(pad, y, inner, Theme.D(18)); y += Theme.D(26); } y += Theme.D(6); ClientSize = new Size(W, y + footH); okBtn = new FlatButton(ok, !danger) { Danger = danger, Font = Theme.Display(14) }; int bh = Theme.D(classic ? 32 : 38), ow = TextRenderer.MeasureText(ok, okBtn.Font).Width + Theme.D(36); int by = ClientSize.Height - footH + (footH - bh) / 2; okBtn.SetBounds(W - pad - ow, by, ow, bh); okBtn.Click += (s, e) => { DialogResult = DialogResult.OK; Close(); }; Controls.Add(okBtn); AcceptButton = okBtn; if (cancel != null) { // "Odustani" kao tekstualni gumb na boji podnozja, kao na Discordu cancelBtn = new FlatButton(cancel, false) { Font = Theme.Display(14), Back = FootColor(), Fore = Theme.Text2 }; int cw = TextRenderer.MeasureText(cancel, cancelBtn.Font).Width + Theme.D(32); cancelBtn.SetBounds(okBtn.Left - Theme.D(8) - cw, by, cw, bh); cancelBtn.Click += (s, e) => { DialogResult = DialogResult.Cancel; Close(); }; Controls.Add(cancelBtn); CancelButton = cancelBtn; } using (var p = Theme.Round(new RectangleF(0, 0, ClientSize.Width, ClientSize.Height), Theme.D(classic ? 4 : 10))) Region = new Region(p); } Font BodyFont() { return classic ? Xf.Font(13) : Theme.Body(15); } Color FootColor() { return classic ? Xf.Bar2 : Theme.Panel; } protected override void OnKeyDown(KeyEventArgs e) { if (e.KeyCode == Keys.Escape && cancelBtn == null) { DialogResult = DialogResult.OK; Close(); } base.OnKeyDown(e); } // prozor bez okvira se moze povuci za zaglavlje protected override void OnMouseDown(MouseEventArgs e) { if (e.Button == MouseButtons.Left && e.Y < headH) { Capture = false; var m = Message.Create(Handle, 0xA1, (IntPtr)2, IntPtr.Zero); WndProc(ref m); } base.OnMouseDown(e); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(Theme.Bg); int W = ClientSize.Width, H = ClientSize.Height; if (classic) { Xf.Gloss(g, new Rectangle(0, 0, W, headH), Xf.Head1, Xf.Head2); Theme.DrawFlame(g, new RectangleF(Theme.D(8), (headH - Theme.D(18)) / 2f, Theme.D(16), Theme.D(18))); using (var f = Xf.Font(13, FontStyle.Bold)) Xf.Draw(g, title, f, new Rectangle(Theme.D(30), 0, W - Theme.D(40), headH), Color.White); Xf.Gloss(g, new Rectangle(0, H - footH, W, footH), Xf.Bar1, Xf.Bar2); } else { using (var f = Theme.Body(20, FontStyle.Bold)) Txt.Draw(g, title, f, new Rectangle(Theme.D(20), Theme.D(18), W - Theme.D(40), Theme.D(32)), Theme.Text); using (var b = new SolidBrush(Theme.Panel)) g.FillRectangle(b, 0, H - footH, W, footH); } using (var f = BodyFont()) TextRenderer.DrawText(g, text ?? "", f, textRect, Theme.Text2, TextFormatFlags.WordBreak | TextFormatFlags.NoPrefix); if (quote != null) { // pregled poruke koja se brise using (var p = Theme.Round(quoteRect, Theme.D(6))) { using (var b = new SolidBrush(Theme.Field)) g.FillPath(b, p); using (var pen = new Pen(Theme.Line)) g.DrawPath(pen, p); } using (var b = new SolidBrush(Theme.Accent)) g.FillRectangle(b, quoteRect.X + Theme.D(10), quoteRect.Y + Theme.D(10), Theme.D(3), quoteRect.Height - Theme.D(20)); using (var f = Theme.Body(14)) TextRenderer.DrawText(g, quote, f, new Rectangle(quoteRect.X + Theme.D(22), quoteRect.Y + Theme.D(12), quoteRect.Width - Theme.D(32), quoteRect.Height - Theme.D(24)), Theme.Text, TextFormatFlags.WordBreak | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix); } if (hint != null) using (var f = Theme.Body(12.5f)) Txt.Draw(g, hint, f, hintRect, Theme.Muted); using (var p = Theme.Round(new RectangleF(0.5f, 0.5f, W - 1.5f, H - 1.5f), Theme.D(classic ? 4 : 10))) using (var pen = new Pen(classic ? Xf.Border : Theme.Line)) g.DrawPath(pen, p); } } // Obavijest dolje desno kraj sata (umjesto Windowsovih balona) static class Toast { static readonly List open = new List(); // kind: info, warn, msg, call, game public static void Show(string title, string text, string avatar, string color, string kind, Action click) { try { while (open.Count >= 4) open[0].Dismiss(); var t = new ToastForm(title, text, avatar, color, kind, click); open.Add(t); t.FormClosed += (s, e) => { open.Remove(t); Restack(); }; Restack(); t.Show(); } catch { } } static void Restack() { var wa = Screen.PrimaryScreen.WorkingArea; int y = wa.Bottom - Theme.D(12); for (int i = open.Count - 1; i >= 0; i--) { var t = open[i]; y -= t.Height; t.Location = new Point(wa.Right - t.Width - Theme.D(12), y); y -= Theme.D(8); } } } class ToastForm : Form { readonly string title, text, avatar, color, kind; readonly Action click; readonly Timer timer = new Timer { Interval = 15 }; readonly bool classic; int life, headH; bool hover, closing, overClose; protected override bool ShowWithoutActivation { get { return true; } } protected override CreateParams CreateParams { get { var cp = base.CreateParams; cp.ExStyle |= 0x08000000 | 0x80 | 0x8; cp.ClassStyle |= 0x20000; return cp; } } public ToastForm(string title, string text, string avatar, string color, string kind, Action click) { this.title = title; this.text = text; this.avatar = avatar; this.color = color; this.kind = kind; this.click = click; classic = Theme.ClassicLayout; FormBorderStyle = FormBorderStyle.None; ShowInTaskbar = false; TopMost = true; StartPosition = FormStartPosition.Manual; DoubleBuffered = true; BackColor = Theme.Panel; AutoScaleMode = AutoScaleMode.None; Cursor = Cursors.Hand; headH = classic ? Theme.D(24) : 0; Size = new Size(Theme.D(340), headH + Theme.D(80)); Opacity = 0; using (var p = Theme.Round(new RectangleF(0, 0, Width, Height), Theme.D(classic ? 3 : 10))) Region = new Region(p); timer.Tick += (s, e) => Step(); timer.Start(); } Color Stripe() { switch (kind) { case "warn": return Theme.Red; case "call": return Theme.On; case "game": return Theme.Idle; case "msg": return Theme.Accent; default: return classic ? Xf.Orange : Theme.Accent; } } Rectangle CloseRect() { int s = Theme.D(20); return new Rectangle(Width - s - Theme.D(8), headH + Theme.D(8), s, s); } // pojavi se, stoji ~5 s (dok je mis iznad, ne nestaje), pa polako nestane void Step() { if (closing) { Opacity = Math.Max(0, Opacity - 0.12); if (Opacity <= 0) { timer.Stop(); Close(); } return; } if (Opacity < 1) { Opacity = Math.Min(1, Opacity + 0.12); return; } if (!hover) life += timer.Interval; if (life > 5500) closing = true; } public void Dismiss() { closing = true; } protected override void OnMouseEnter(EventArgs e) { hover = true; base.OnMouseEnter(e); } protected override void OnMouseLeave(EventArgs e) { hover = false; overClose = false; Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseMove(MouseEventArgs e) { bool oc = CloseRect().Contains(e.Location); if (oc != overClose) { overClose = oc; Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseClick(MouseEventArgs e) { closing = true; if (!CloseRect().Contains(e.Location) && click != null) { try { click(); } catch { } } base.OnMouseClick(e); } protected override void OnFormClosed(FormClosedEventArgs e) { timer.Stop(); base.OnFormClosed(e); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; int W = Width, H = Height; g.Clear(Theme.Panel); if (classic) { Xf.Gloss(g, new Rectangle(0, 0, W, headH), Xf.Head1, Xf.Head2); Theme.DrawFlame(g, new RectangleF(Theme.D(6), (headH - Theme.D(16)) / 2f, Theme.D(14), Theme.D(16))); using (var f = Xf.Font(11.5f, FontStyle.Bold)) Xf.Draw(g, "Fragline", f, new Rectangle(Theme.D(26), 0, W - Theme.D(36), headH), Color.White); using (var b = new LinearGradientBrush(new Rectangle(0, headH, W, H - headH), Theme.Shade(Theme.Panel, 0.06f), Theme.Bg, 90f)) g.FillRectangle(b, 0, headH, W, H - headH); } using (var b = new SolidBrush(Stripe())) g.FillRectangle(b, 0, headH, Theme.D(4), H - headH); var ar = new Rectangle(Theme.D(16), headH + (H - headH - Theme.D(46)) / 2, Theme.D(46), Theme.D(46)); if (avatar != null || color != null) { if (classic) Xf.Avatar(g, ar, color, title, avatar, this, null, Theme.Panel); else Theme.Avatar(g, ar, color, title, avatar, this, null, Theme.Panel); } else { using (var b = new SolidBrush(Theme.Bg)) if (classic) using (var p = Theme.Round(ar, Theme.D(4))) g.FillPath(b, p); else g.FillEllipse(b, ar); Theme.DrawFlame(g, new RectangleF(ar.X + Theme.D(11), ar.Y + Theme.D(9), Theme.D(24), Theme.D(28))); } int x = ar.Right + Theme.D(14), w = W - x - Theme.D(34); using (var f = classic ? Xf.Font(13, FontStyle.Bold) : Theme.Display(15)) Txt.Draw(g, title, f, new Rectangle(x, headH + Theme.D(12), w, Theme.D(22)), Theme.Text); using (var f = classic ? Xf.Font(12) : Theme.Body(13.5f)) TextRenderer.DrawText(g, text ?? "", f, new Rectangle(x, headH + Theme.D(35), W - x - Theme.D(14), Theme.D(38)), Theme.Text2, TextFormatFlags.WordBreak | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix); var cr = CloseRect(); if (overClose) using (var b = new SolidBrush(Theme.Panel3)) g.FillEllipse(b, cr); Theme.Icon(g, Theme.IcoClose, "x", cr, overClose ? Theme.Text : Theme.Muted, 10); using (var p = Theme.Round(new RectangleF(0.5f, 0.5f, W - 1.5f, H - 1.5f), Theme.D(classic ? 3 : 10))) using (var pen = new Pen(classic ? Xf.Border : Theme.Line)) g.DrawPath(pen, p); } } } // ===================== Windows.cs ===================== using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Globalization; using System.Linq; using System.Net; using System.Threading.Tasks; using System.Windows.Forms; namespace Fragline { } // ===================== Windows2.cs ===================== using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Net; using System.Linq; using System.Threading.Tasks; using System.Windows.Forms; namespace Fragline { class AudioSettingsForm : DarkForm { readonly TrayApp app; readonly Panel body = new Panel(), foot = new Panel(); readonly DropDown inDev = new DropDown(), outDev = new DropDown(), key = new DropDown(), quality = new DropDown(); readonly RangeSlider inVol = new RangeSlider { Min = 0, Max = 200, Ticks = new[] { 100f }, Snap = 4, Format = v => (int)Math.Round(v) + "%" }, outVol = new RangeSlider { Min = 0, Max = 200, Ticks = new[] { 100f }, Snap = 4, Format = v => (int)Math.Round(v) + "%" }, sens = new RangeSlider { Min = -80, Max = 0, Format = v => (int)Math.Round(v) + " dB" }; readonly Label inVolLbl, outVolLbl, sensLbl, testHint; readonly Toggle ptt = new Toggle(), auto = new Toggle(), ns = new Toggle(), ks = new Toggle(), agc = new Toggle(), hp = new Toggle(); readonly FlatButton testBtn = new FlatButton("Pokreni test mikrofona", false), done = new FlatButton("Gotovo", true); readonly Timer tick = new Timer { Interval = 50 }; static readonly int[] Rates = { 32000, 64000, 96000, 192000, 320000 }; MicTest test; bool init = true; readonly int W, pad, inner; public AudioSettingsForm(TrayApp app) { this.app = app; Text = "Fragline - zvuk i mikrofon"; MinimizeBox = false; W = Theme.D(440); pad = Theme.D(20); inner = W - pad * 2 - Theme.D(18); ClientSize = new Size(W, Theme.D(640)); body.Dock = DockStyle.Fill; body.AutoScroll = true; body.BackColor = Theme.Bg; foot.Dock = DockStyle.Bottom; foot.Height = Theme.D(64); foot.BackColor = Theme.Panel; if (Theme.ClassicLayout) foot.Paint += (s, e) => Xf.Gloss(e.Graphics, foot.ClientRectangle, Xf.Bar1, Xf.Head2); done.SetBounds(W - pad - Theme.D(130), Theme.D(12), Theme.D(130), Theme.D(40)); done.Click += (s, e) => Close(); foot.Controls.Add(done); Controls.Add(body); Controls.Add(foot); body.BringToFront(); Theme.DarkScroll(body); SlimBar.Attach(body); int y = Theme.D(16); Sec("Uređaji", ref y); Add(Ui.Lbl("Mikrofon", 13, Theme.Muted, pad, y)); y += Theme.D(22); Ui.Combo(inDev); inDev.SetBounds(pad, y, inner, Theme.D(38)); Add(inDev); y += Theme.D(48); Add(Ui.Lbl("Zvučnici ili slušalice", 13, Theme.Muted, pad, y)); y += Theme.D(22); Ui.Combo(outDev); outDev.SetBounds(pad, y, inner, Theme.D(38)); Add(outDev); y += Theme.D(52); inDev.Items.Add("Zadani uređaj u Windowsu"); foreach (var n in AudioCfg.Inputs()) inDev.Items.Add(n); outDev.Items.Add("Zadani uređaj u Windowsu"); foreach (var n in AudioCfg.Outputs()) outDev.Items.Add(n); inDev.SelectedIndex = Math.Max(0, Math.Min(inDev.Items.Count - 1, AudioCfg.InDevice + 1)); outDev.SelectedIndex = Math.Max(0, Math.Min(outDev.Items.Count - 1, AudioCfg.OutDevice + 1)); Sec("Glasnoća", ref y); Add(Ui.Lbl("Glasnoća mikrofona", 13.5f, Theme.Text, pad, y)); inVolLbl = Ui.Lbl("", 13, Theme.Muted, pad + inner - Theme.D(50), y); Add(inVolLbl); y += Theme.D(22); inVol.SetBounds(pad - Theme.D(10), y, inner + Theme.D(20), Theme.D(32)); Add(inVol); y += Theme.D(40); Add(Ui.Lbl("Glasnoća zvuka", 13.5f, Theme.Text, pad, y)); outVolLbl = Ui.Lbl("", 13, Theme.Muted, pad + inner - Theme.D(50), y); Add(outVolLbl); y += Theme.D(22); outVol.SetBounds(pad - Theme.D(10), y, inner + Theme.D(20), Theme.D(32)); Add(outVol); y += Theme.D(44); inVol.Value = AudioCfg.InGain * 100; outVol.Value = AudioCfg.OutGain * 100; Sec("Način pričanja", ref y); Add(Ui.Lbl("Pritisni za pričanje", 13.5f, Theme.Text, pad, y + Theme.D(2))); ptt.Location = new Point(pad + Theme.D(160), y); Add(ptt); Ui.Combo(key); key.SetBounds(pad + inner - Theme.D(120), y - Theme.D(3), Theme.D(120), Theme.D(28)); Add(key); foreach (var k in VoiceView.PttKeys) key.Items.Add(k.Name); int ki = Array.FindIndex(VoiceView.PttKeys, k => k.Vk == AudioCfg.PttKey); key.SelectedIndex = ki >= 0 ? ki : 0; ptt.Checked = AudioCfg.Ptt; key.Enabled = AudioCfg.Ptt; y += Theme.D(44); Sec("Osjetljivost mikrofona", ref y); Add(Ui.Lbl("Automatski", 13.5f, Theme.Text, pad, y + Theme.D(2))); auto.Location = new Point(pad + inner - auto.Width, y); Add(auto); auto.Checked = AudioCfg.AutoSens; y += Theme.D(34); sens.SetBounds(pad - Theme.D(10), y, inner + Theme.D(20), Theme.D(34)); Add(sens); y += Theme.D(36); sens.Value = AudioCfg.SensDb; sensLbl = Ui.Lbl("", 12.5f, Theme.Muted, pad, y); Add(sensLbl); y += Theme.D(34); Sec("Obrada zvuka", ref y); Opt("Utišavanje pozadinske buke", "Ventilator, klima, šum u sobi", ns, AudioCfg.NoiseSuppress, ref y); Opt("Utišavanje tipkovnice i klikova", "Tipke i miš ne otvaraju mikrofon", ks, AudioCfg.KeySuppress, ref y); Opt("Automatska glasnoća mikrofona", "Drži tvoj glas jednako glasnim", agc, AudioCfg.Agc, ref y); Opt("Uklanjanje brujanja", "Duboki bruj i udarci u stol", hp, AudioCfg.HighPass, ref y); Sec("Kvaliteta zvuka", ref y); Ui.Combo(quality); quality.SetBounds(pad, y, inner, Theme.D(38)); Add(quality); y += Theme.D(52); quality.Items.AddRange(new object[] { "Normalna (32 kbps, manje interneta)", "Visoka (64 kbps)", "Vrlo visoka (96 kbps)", "Studijska (192 kbps)", "Maksimalna (320 kbps, puno interneta)" }); int qi = Array.IndexOf(Rates, AudioCfg.Bitrate); quality.SelectedIndex = qi >= 0 ? qi : 1; Sec("Test mikrofona", ref y); testBtn.SetBounds(pad, y, inner, Theme.D(40)); Add(testBtn); y += Theme.D(48); testHint = Ui.Lbl("Čuješ sam sebe s istom obradom kao u voiceu. Koristi slušalice.", 12.5f, Theme.Muted, pad, y); testHint.AutoSize = false; testHint.Size = new Size(inner, Theme.D(36)); Add(testHint); y += Theme.D(44); Add(new Panel { Bounds = new Rectangle(0, y, 1, Theme.D(8)), BackColor = Theme.Bg }); inDev.SelectedIndexChanged += (s, e) => { if (init) return; AudioCfg.InDevice = inDev.SelectedIndex - 1; DevicesChanged(); }; outDev.SelectedIndexChanged += (s, e) => { if (init) return; AudioCfg.OutDevice = outDev.SelectedIndex - 1; DevicesChanged(); }; inVol.ValueChanged += (s, e) => { AudioCfg.InGain = inVol.Value / 100f; Labels(); }; outVol.ValueChanged += (s, e) => { AudioCfg.OutGain = outVol.Value / 100f; app.AudioChanged(false); if (test != null) test.Audio.ApplyVolume(); Labels(); }; ptt.CheckedChanged += (s, e) => { AudioCfg.Ptt = ptt.Checked; key.Enabled = ptt.Checked; }; key.SelectedIndexChanged += (s, e) => { if (!init && key.SelectedIndex >= 0) AudioCfg.PttKey = VoiceView.PttKeys[key.SelectedIndex].Vk; }; auto.CheckedChanged += (s, e) => AudioCfg.AutoSens = auto.Checked; sens.ValueChanged += (s, e) => AudioCfg.SensDb = sens.Value; ns.CheckedChanged += (s, e) => AudioCfg.NoiseSuppress = ns.Checked; ks.CheckedChanged += (s, e) => AudioCfg.KeySuppress = ks.Checked; agc.CheckedChanged += (s, e) => AudioCfg.Agc = agc.Checked; hp.CheckedChanged += (s, e) => AudioCfg.HighPass = hp.Checked; quality.SelectedIndexChanged += (s, e) => { if (init || quality.SelectedIndex < 0) return; AudioCfg.Bitrate = Rates[quality.SelectedIndex]; app.AudioBitrateChanged(); }; testBtn.Click += (s, e) => ToggleTest(); tick.Tick += (s, e) => Tick(); FormClosed += (s, e) => { tick.Stop(); StopTest(); }; Labels(); init = false; tick.Start(); } void Add(Control c) { body.Controls.Add(c); } void Sec(string t, ref int y) { if (Theme.ClassicLayout) { // Xfire: sjajna plava traka za svaki odjeljak var bar = new Panel { Bounds = new Rectangle(pad - Theme.D(8), y, inner + Theme.D(16), Theme.D(24)), Tag = t }; bar.Paint += (s, e) => { Xf.Gloss(e.Graphics, bar.ClientRectangle, Xf.Bar1, Xf.Bar2); using (var f = Xf.Font(12, FontStyle.Bold)) Xf.Draw(e.Graphics, (string)bar.Tag, f, new Rectangle(Theme.D(8), 0, bar.Width - Theme.D(16), bar.Height), Theme.Text); }; Add(bar); y += Theme.D(32); return; } Add(Ui.Section(t, pad, y)); y += Theme.D(26); } void Opt(string title, string hint, Toggle t, bool val, ref int y) { Add(Ui.Lbl(title, 13.5f, Theme.Text, pad, y)); Add(Ui.Lbl(hint, 12, Theme.Muted, pad, y + Theme.D(20))); t.Location = new Point(pad + inner - t.Width, y + Theme.D(6)); t.Checked = val; Add(t); y += Theme.D(48); } void Labels() { inVolLbl.Text = (int)Math.Round(inVol.Value) + "%"; outVolLbl.Text = (int)Math.Round(outVol.Value) + "%"; } void DevicesChanged() { app.AudioChanged(true); if (test != null) { StopTest(); ToggleTest(); } } void ToggleTest() { if (test != null) { StopTest(); return; } if (app.Voice != null) { testHint.Text = "U voiceu si, pa razinu mikrofona vidiš gore na klizaču. Test nije potreban."; return; } try { test = new MicTest(); testBtn.Text = "Zaustavi test"; testHint.Text = test.Audio.HasMic ? "Pričaj i trebao bi se čuti. Zeleno na klizaču znači da se zvuk šalje." : "Mikrofon nije pronađen."; } catch (Exception ex) { test = null; testHint.Text = "Test nije uspio: " + ex.Message; } } void StopTest() { if (test == null) return; try { test.Dispose(); } catch { } test = null; testBtn.Text = "Pokreni test mikrofona"; } void Tick() { var eng = app.Voice != null ? app.Voice.Audio : (test != null ? test.Audio : null); sens.Locked = AudioCfg.AutoSens; sens.Level = eng != null ? eng.LevelDb : float.NaN; sens.Marker = eng != null ? eng.ThresholdDb : float.NaN; if (!sens.Dragging) sens.Value = AudioCfg.SensDb; sens.Invalidate(); string t; if (eng == null) t = "Pokreni test ispod ili uđi u voice da vidiš razinu mikrofona."; else if (AudioCfg.AutoSens) t = "Automatski prag: " + (int)eng.ThresholdDb + " dB. Zeleno znači da se zvuk šalje."; else t = "Prag: " + (int)AudioCfg.SensDb + " dB. Povuci kružić tako da je zeleno samo kad pričaš."; if (sensLbl.Text != t) sensLbl.Text = t; } } class CreateRoomForm : DarkForm { readonly TrayApp app; readonly InputBox name = new InputBox(false); readonly DropDown privacy = new DropDown(), clan = new DropDown(), limit = new DropDown(); readonly Label err, clanLbl; readonly FlatButton ok = new FlatButton("Napravi sobu", true), cancel = new FlatButton("Odustani", false); static readonly int[] Limits = { 0, 2, 3, 4, 5, 6, 8, 10, 15, 20, 30 }; public CreateRoomForm(TrayApp app) { this.app = app; Text = "Fragline - nova soba"; MinimizeBox = false; int W = Theme.D(380), pad = Theme.D(24), inner = W - pad * 2; ClientSize = new Size(W, Theme.D(440)); Controls.Add(Ui.Lbl("Nova voice soba", 20, Theme.Text, pad, Theme.D(20), true)); Controls.Add(Ui.Lbl("Ime sobe", 13, Theme.Muted, pad, Theme.D(66))); name.SetBounds(pad, Theme.D(88), inner, Theme.D(42)); name.Box.MaxLength = 40; Controls.Add(Ui.Lbl("Tko je vidi", 13, Theme.Muted, pad, Theme.D(142))); Ui.Combo(privacy); privacy.SetBounds(pad, Theme.D(164), inner, Theme.D(38)); privacy.Items.AddRange(new object[] { "Javna, vide je svi", "Samo moji prijatelji", "Samo moj klan" }); privacy.SelectedIndex = 0; clanLbl = Ui.Lbl("Klan", 13, Theme.Muted, pad, Theme.D(206)); Ui.Combo(clan); clan.SetBounds(pad, Theme.D(228), inner, Theme.D(38)); foreach (var c in app.Clans) clan.Items.Add(c); if (clan.Items.Count > 0) clan.SelectedIndex = 0; clanLbl.Visible = false; clan.Visible = false; Controls.Add(Ui.Lbl("Najviše ljudi", 13, Theme.Muted, pad, Theme.D(270))); Ui.Combo(limit); limit.SetBounds(pad, Theme.D(292), inner, Theme.D(38)); foreach (var l in Limits) limit.Items.Add(l == 0 ? "Bez ograničenja" : l + " ljudi"); limit.SelectedIndex = 0; err = Ui.Lbl("", 13, Theme.Red, pad, Theme.D(336)); err.AutoSize = false; err.Size = new Size(inner, Theme.D(36)); int half = (inner - Theme.D(10)) / 2; cancel.SetBounds(pad, Theme.D(376), half, Theme.D(42)); ok.SetBounds(pad + half + Theme.D(10), Theme.D(376), half, Theme.D(42)); Controls.AddRange(new Control[] { name, privacy, clanLbl, clan, limit, err, cancel, ok }); privacy.SelectedIndexChanged += (s, e) => { bool c = privacy.SelectedIndex == 2; clanLbl.Visible = c; clan.Visible = c; if (c && clan.Items.Count == 0) err.Text = "Nisi član nijednog klana."; else err.Text = ""; }; cancel.Click += (s, e) => Close(); ok.Click += async (s, e) => { var n = name.Value.Trim(); if (n.Length < 2) { err.Text = "Upiši ime sobe (barem 2 znaka)."; return; } string pv = privacy.SelectedIndex == 1 ? "friends" : privacy.SelectedIndex == 2 ? "clan" : "public"; long clanId = 0; if (pv == "clan") { var ci = clan.SelectedItem as ClanItem; if (ci == null) { err.Text = "Odaberi klan."; return; } clanId = ci.Id; } ok.Enabled = false; ok.Text = "Radim..."; var error = await app.CreateRoom(n, pv, clanId, Limits[Math.Max(0, limit.SelectedIndex)]); ok.Enabled = true; ok.Text = "Napravi sobu"; if (error != null) { err.Text = error; return; } DialogResult = DialogResult.OK; Close(); }; AcceptButton = ok; Shown += (s, e) => name.Box.Focus(); } } class ClanRow { public long Id; public string Name, Tag, Slug, Color, Description; public int Members; public bool Member, Owner; } // Krugovi s bojama za odabir boje klana class Swatches : Control { public static readonly string[] Colors = { "#5865f2", "#8a4fd6", "#eb459e", "#d6457a", "#f23f43", "#ff7a2f", "#f0b232", "#23a55a", "#1f8f8a", "#00a8fc" }; int sel, hover = -1; public event EventHandler Changed; public string Value { get { return Colors[sel]; } } public Swatches() { SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); Cursor = Cursors.Hand; sel = new Random().Next(Colors.Length); } Rectangle Cell(int i) { int s = Theme.D(28), gap = (Width - s * Colors.Length) / Math.Max(1, Colors.Length - 1); return new Rectangle(i * (s + gap), (Height - s) / 2, s, s); } int Hit(Point p) { for (int i = 0; i < Colors.Length; i++) if (Cell(i).Contains(p)) return i; return -1; } protected override void OnMouseMove(MouseEventArgs e) { int h = Hit(e.Location); if (h != hover) { hover = h; Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseLeave(EventArgs e) { hover = -1; Invalidate(); base.OnMouseLeave(e); } protected override void OnMouseClick(MouseEventArgs e) { int h = Hit(e.Location); if (h >= 0) { sel = h; Invalidate(); if (Changed != null) Changed(this, EventArgs.Empty); } base.OnMouseClick(e); } protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.Clear(Parent != null ? Parent.BackColor : Theme.Bg); g.SmoothingMode = SmoothingMode.AntiAlias; for (int i = 0; i < Colors.Length; i++) { var r = Cell(i); if (i == sel || i == hover) { var rr = r; rr.Inflate(-1, -1); using (var pen = new Pen(i == sel ? Theme.Text : Theme.Muted, Theme.D(2))) g.DrawEllipse(pen, rr); r.Inflate(-Theme.D(4), -Theme.D(4)); } using (var b = new SolidBrush(ColorTranslator.FromHtml(Colors[i]))) g.FillEllipse(b, r); } } } // Osnivanje klana i pridruživanje postojećem class ClanForm : DarkForm { readonly TrayApp app; readonly Panel create = new Panel(), browse = new Panel(); readonly InputBox name = new InputBox(false), tag = new InputBox(false), desc = new InputBox(false); readonly Swatches colors = new Swatches(); readonly Control preview; readonly Label err, browseInfo; readonly FlatButton ok = new FlatButton("Napravi klan", true), toBrowse = new FlatButton("Pridruži se postojećem", false), back = new FlatButton("Natrag", false); readonly ListBox list = new SmoothList(); List rows = new List(); int hover = -1; bool busy; public ClanForm(TrayApp app, bool startBrowsing) { this.app = app; Text = "Fragline - klanovi"; MinimizeBox = false; int W = Theme.D(460), H = Theme.D(600), pad = Theme.D(28), inner = W - pad * 2; ClientSize = new Size(W, H); create.SetBounds(0, 0, W, H); browse.SetBounds(0, 0, W, H); create.BackColor = Theme.Bg; browse.BackColor = Theme.Bg; browse.Visible = false; // --- osnivanje --- var title = new Label { Text = "Napravi svoj klan", Font = Theme.Body(22, FontStyle.Bold), ForeColor = Theme.Text, TextAlign = ContentAlignment.MiddleCenter }; title.SetBounds(pad, Theme.D(16), inner, Theme.D(34)); var sub = new Label { Text = "Klan je mjesto za tebe i prijatelje: dobiva ikonu na lijevoj traci, svoj chat i voice sobe.", Font = Theme.Body(13.5f), ForeColor = Theme.Muted, TextAlign = ContentAlignment.TopCenter }; sub.SetBounds(pad, Theme.D(52), inner, Theme.D(40)); preview = new Panel { Bounds = new Rectangle((W - Theme.D(72)) / 2, Theme.D(98), Theme.D(72), Theme.D(72)), BackColor = Theme.Bg }; preview.Paint += (s, e) => { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; var r = new Rectangle(0, 0, preview.Width - 1, preview.Height - 1); using (var p = Theme.Round(r, Theme.D(20))) using (var b = new SolidBrush(ColorTranslator.FromHtml(colors.Value))) g.FillPath(b, p); var t = tag.Value.Trim(); if (t.Length == 0) t = name.Value.Trim().Length > 0 ? name.Value.Trim().Substring(0, Math.Min(2, name.Value.Trim().Length)).ToUpperInvariant() : "?"; using (var f = Theme.Body(t.Length > 3 ? 15 : 20, FontStyle.Bold)) TextRenderer.DrawText(g, t, f, r, Color.White, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPrefix); }; create.Controls.AddRange(new Control[] { title, sub, preview }); int y = Theme.D(186); Func cap = t => { var l = Ui.Lbl(t, 12, Theme.Text2, pad, y, true); create.Controls.Add(l); y += Theme.D(22); return l; }; cap("IME KLANA"); name.SetBounds(pad, y, inner, Theme.D(40)); name.Box.MaxLength = 40; create.Controls.Add(name); y += Theme.D(52); cap("TAG (DO 6 ZNAKOVA, NPR. FRG)"); tag.SetBounds(pad, y, Theme.D(160), Theme.D(40)); tag.Box.MaxLength = 6; tag.Box.CharacterCasing = CharacterCasing.Upper; create.Controls.Add(tag); y += Theme.D(52); cap("BOJA"); colors.SetBounds(pad, y, inner, Theme.D(32)); create.Controls.Add(colors); y += Theme.D(44); cap("OPIS (NEOBAVEZNO)"); desc.SetBounds(pad, y, inner, Theme.D(40)); desc.Box.MaxLength = 500; create.Controls.Add(desc); y += Theme.D(48); err = Ui.Lbl("", 13, Theme.Red, pad, y); err.AutoSize = false; err.Size = new Size(inner, Theme.D(36)); create.Controls.Add(err); int half = (inner - Theme.D(10)) / 2; toBrowse.SetBounds(pad, H - Theme.D(66), half, Theme.D(42)); ok.SetBounds(pad + half + Theme.D(10), H - Theme.D(66), half, Theme.D(42)); create.Controls.AddRange(new Control[] { toBrowse, ok }); name.Box.TextChanged += (s, e) => preview.Invalidate(); tag.Box.TextChanged += (s, e) => preview.Invalidate(); colors.Changed += (s, e) => preview.Invalidate(); ok.Click += (s, e) => Create(); toBrowse.Click += (s, e) => ShowBrowse(); // --- pridruživanje --- var bt = new Label { Text = "Pridruži se klanu", Font = Theme.Body(22, FontStyle.Bold), ForeColor = Theme.Text, TextAlign = ContentAlignment.MiddleCenter }; bt.SetBounds(pad, Theme.D(16), inner, Theme.D(34)); browseInfo = new Label { Text = "Učitavam klanove...", Font = Theme.Body(13.5f), ForeColor = Theme.Muted, TextAlign = ContentAlignment.TopCenter }; browseInfo.SetBounds(pad, Theme.D(52), inner, Theme.D(24)); list.SetBounds(Theme.D(16), Theme.D(84), W - Theme.D(32), H - Theme.D(84) - Theme.D(80)); list.DrawMode = DrawMode.OwnerDrawFixed; list.ItemHeight = Theme.D(64); list.BorderStyle = BorderStyle.None; list.BackColor = Theme.Bg; list.IntegralHeight = false; list.SelectionMode = SelectionMode.None; list.DrawItem += DrawClan; list.MouseMove += (s, e) => { int i = list.IndexFromPoint(e.Location); if (i != hover) { hover = i; list.Cursor = i >= 0 && i < rows.Count && !rows[i].Member ? Cursors.Hand : Cursors.Default; list.Invalidate(); } }; list.MouseLeave += (s, e) => { hover = -1; list.Invalidate(); }; list.MouseClick += (s, e) => { int i = list.IndexFromPoint(e.Location); if (i >= 0 && i < rows.Count && !rows[i].Member) Join(rows[i]); }; Theme.DarkScroll(list); back.SetBounds(pad, H - Theme.D(66), half, Theme.D(42)); var close = new FlatButton("Zatvori", true); close.SetBounds(pad + half + Theme.D(10), H - Theme.D(66), half, Theme.D(42)); close.Click += (s, e) => Close(); back.Click += (s, e) => { browse.Visible = false; create.Visible = true; name.Box.Focus(); }; browse.Controls.AddRange(new Control[] { bt, browseInfo, list, back, close }); Controls.AddRange(new Control[] { create, browse }); AcceptButton = ok; Shown += (s, e) => { if (startBrowsing) ShowBrowse(); else name.Box.Focus(); }; } async void Create() { if (busy) return; var n = name.Value.Trim(); var t = tag.Value.Trim(); if (n.Length < 3) { err.Text = "Ime klana mora imati barem 3 znaka."; return; } if (t.Length == 0) { err.Text = "Upiši tag klana (kratica od 1 do 6 znakova)."; return; } busy = true; ok.Enabled = false; ok.Text = "Radim..."; err.Text = ""; var res = await app.CreateClan(n, t, colors.Value, desc.Value.Trim()); busy = false; ok.Enabled = true; ok.Text = "Napravi klan"; if (res != null) { err.Text = res; return; } DialogResult = DialogResult.OK; Close(); } async void ShowBrowse() { create.Visible = false; browse.Visible = true; AcceptButton = null; browseInfo.Text = "Učitavam klanove..."; try { var res = await Task.Run(() => app.ApiGet("/client/clans")); var nr = new List(); foreach (var x in J.A(J.Parse(res), "clans")) nr.Add(new ClanRow { Id = J.L(x, "id"), Name = J.S(x, "name"), Tag = J.S(x, "tag"), Slug = J.S(x, "slug"), Color = J.S(x, "color"), Description = J.S(x, "description"), Members = (int)J.L(x, "members"), Member = J.B(x, "member"), Owner = J.B(x, "owner") }); rows = nr; list.BeginUpdate(); list.Items.Clear(); foreach (var r in nr) list.Items.Add(r); list.EndUpdate(); browseInfo.Text = nr.Count == 0 ? "Još nema nijednog klana. Napravi prvi!" : "Klikni klan da mu se pridružiš."; } catch (WebException e) { browseInfo.Text = Api.ErrorMessage(e); } catch (Exception e) { browseInfo.Text = e.Message; } } async void Join(ClanRow c) { if (busy) return; busy = true; var res = await app.JoinClan(c.Id, c.Name); busy = false; if (res != null) { browseInfo.Text = res; return; } c.Member = true; browseInfo.Text = "Sad si član klana " + c.Name + "."; list.Invalidate(); } void DrawClan(object s, DrawItemEventArgs e) { if (e.Index < 0 || e.Index >= rows.Count) return; var c = rows[e.Index]; var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; var b = e.Bounds; using (var fill = new SolidBrush(Theme.Bg)) g.FillRectangle(fill, b); bool hot = e.Index == hover && !c.Member; var inner = new Rectangle(b.X, b.Y + Theme.D(2), b.Width, b.Height - Theme.D(4)); using (var p = Theme.Round(inner, Theme.D(8))) using (var fill = new SolidBrush(hot ? Theme.Panel2 : Theme.Panel)) g.FillPath(fill, p); var ic = new Rectangle(inner.X + Theme.D(12), inner.Y + (inner.Height - Theme.D(40)) / 2, Theme.D(40), Theme.D(40)); using (var p = Theme.Round(ic, Theme.D(14))) using (var fill = new SolidBrush(Theme.Hex(c.Color, Theme.Accent))) g.FillPath(fill, p); using (var f = Theme.Body((c.Tag ?? "").Length > 3 ? 11 : 13, FontStyle.Bold)) TextRenderer.DrawText(g, c.Tag ?? "?", f, ic, Color.White, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPrefix); string right = c.Owner ? "Tvoj klan" : c.Member ? "Član si" : "Pridruži se"; int rx = inner.Right - Theme.D(12); using (var f = Theme.Body(13.5f, FontStyle.Bold)) { var sz = TextRenderer.MeasureText(right, f); if (!c.Member) { var br = new Rectangle(rx - sz.Width - Theme.D(20), inner.Y + (inner.Height - Theme.D(30)) / 2, sz.Width + Theme.D(20), Theme.D(30)); using (var p = Theme.Round(br, Theme.D(4))) using (var fill = new SolidBrush(hot ? Theme.Shade(Theme.On, -0.2f) : Theme.Shade(Theme.On, -0.1f))) g.FillPath(fill, p); TextRenderer.DrawText(g, right, f, br, Color.White, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPrefix); rx = br.X - Theme.D(10); } else { TextRenderer.DrawText(g, right, f, new Rectangle(rx - sz.Width, inner.Y, sz.Width + 2, inner.Height), Theme.On, TextFormatFlags.VerticalCenter | TextFormatFlags.NoPrefix); rx -= sz.Width + Theme.D(10); } } int x = ic.Right + Theme.D(12), w = Math.Max(10, rx - x); using (var f = Theme.Body(15, FontStyle.Bold)) Txt.Draw(g, c.Name, f, new Rectangle(x, inner.Y + Theme.D(8), w, Theme.D(22)), Theme.Text); var info = c.Members + (c.Members == 1 ? " član" : " članova") + (string.IsNullOrEmpty(c.Description) ? "" : " · " + c.Description); using (var f = Theme.Body(12.5f)) Txt.Draw(g, info, f, new Rectangle(x, inner.Y + Theme.D(31), w, Theme.D(18)), Theme.Muted); } } // Dolazni poziv: iskoci dolje desno kraj sata, avatar pulsira dok zvoni class IncomingCallForm : DarkForm { readonly TrayApp app; readonly string from, color, avatar; readonly Panel pic = new Panel(); readonly Timer pulse = new Timer { Interval = 40 }; float phase; public bool Answered; public IncomingCallForm(TrayApp app, string from, string color, string avatar) { this.app = app; this.from = from; this.color = color; this.avatar = avatar; Text = "Poziv - Fragline"; TopMost = true; MinimizeBox = false; ShowInTaskbar = true; int W = Theme.D(300), H = Theme.D(330); ClientSize = new Size(W, H); StartPosition = FormStartPosition.Manual; var wa = Screen.PrimaryScreen.WorkingArea; Location = new Point(wa.Right - Width - Theme.D(16), wa.Bottom - Height - Theme.D(16)); pic.SetBounds((W - Theme.D(140)) / 2, Theme.D(16), Theme.D(140), Theme.D(140)); pic.BackColor = Theme.Bg; typeof(Panel).GetProperty("DoubleBuffered", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).SetValue(pic, true, null); pic.Paint += (s, e) => { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; float c = pic.Width / 2f, av = Theme.D(96); // valovi koji se sire oko avatara for (int k = 0; k < 2; k++) { float p = (phase + k * 0.5f) % 1f, rad = av / 2 + p * Theme.D(22); using (var pen = new Pen(Color.FromArgb((int)(170 * (1 - p)), Theme.On), Theme.D(3))) g.DrawEllipse(pen, c - rad, c - rad, rad * 2, rad * 2); } Theme.Avatar(g, new RectangleF(c - av / 2, c - av / 2, av, av), color, from, avatar, pic, null, Theme.Bg); }; pulse.Tick += (s, e) => { phase = (phase + 0.025f) % 1f; pic.Invalidate(); }; pulse.Start(); var name = new Label { Text = from, Font = Theme.Body(20, FontStyle.Bold), ForeColor = Theme.Text, TextAlign = ContentAlignment.MiddleCenter, AutoEllipsis = true }; name.SetBounds(Theme.D(12), Theme.D(162), W - Theme.D(24), Theme.D(32)); var sub = new Label { Text = "te zove...", Font = Theme.Body(14), ForeColor = Theme.Muted, TextAlign = ContentAlignment.MiddleCenter }; sub.SetBounds(Theme.D(12), Theme.D(194), W - Theme.D(24), Theme.D(22)); int bs = Theme.D(58); var decline = new RoundButton(Theme.IcoHangup, "X", "Odbij") { Danger = true }; var accept = new RoundButton(Theme.IcoPhone, "T", "Javi se") { Good = true }; decline.SetBounds(W / 2 - bs - Theme.D(24), Theme.D(232), bs, bs); accept.SetBounds(W / 2 + Theme.D(24), Theme.D(232), bs, bs); var dl = new Label { Text = "Odbij", Font = Theme.Body(12), ForeColor = Theme.Muted, TextAlign = ContentAlignment.MiddleCenter }; dl.SetBounds(decline.Left - Theme.D(10), decline.Bottom + Theme.D(2), bs + Theme.D(20), Theme.D(18)); var al = new Label { Text = "Javi se", Font = Theme.Body(12), ForeColor = Theme.Muted, TextAlign = ContentAlignment.MiddleCenter }; al.SetBounds(accept.Left - Theme.D(10), accept.Bottom + Theme.D(2), bs + Theme.D(20), Theme.D(18)); decline.Click += (s, e) => { Answered = true; app.AnswerCall(false); }; accept.Click += (s, e) => { Answered = true; app.AnswerCall(true); }; Controls.AddRange(new Control[] { pic, name, sub, decline, accept, dl, al }); FormClosed += (s, e) => pulse.Stop(); } // zatvaranje prozora krizicem = odbijanje protected override void OnFormClosing(FormClosingEventArgs e) { if (!Answered && e.CloseReason == CloseReason.UserClosing) { Answered = true; app.AnswerCall(false); } base.OnFormClosing(e); } } } // ===================== Audio.cs ===================== using System; using System.Collections.Generic; using NAudio.Wave; using NAudio.Wave.SampleProviders; namespace Fragline { // Sve postavke zvuka, spremaju se u %APPDATA%\Fragline\prefs.txt static class AudioCfg { public static int InDevice { get { return Prefs.Int("a_in", -1); } set { Prefs.Set("a_in", value); } } public static int OutDevice { get { return Prefs.Int("a_out", -1); } set { Prefs.Set("a_out", value); } } public static float InGain { get { return Prefs.Float("a_ingain", 1f); } set { Prefs.Set("a_ingain", value); } } public static float OutGain { get { return Prefs.Float("a_outgain", 1f); } set { Prefs.Set("a_outgain", value); } } public static bool AutoSens { get { return Prefs.Bool("a_auto", true); } set { Prefs.Set("a_auto", value); } } public static float SensDb { get { return Prefs.Float("a_sens", -45f); } set { Prefs.Set("a_sens", value); } } public static bool Ptt { get { return Prefs.Bool("ptt", false); } set { Prefs.Set("ptt", value); } } public static int PttKey { get { return Prefs.Int("pttkey", 0x05); } set { Prefs.Set("pttkey", value); } } public static bool NoiseSuppress { get { return Prefs.Bool("a_ns", true); } set { Prefs.Set("a_ns", value); } } public static bool KeySuppress { get { return Prefs.Bool("a_ks", true); } set { Prefs.Set("a_ks", value); } } public static bool Agc { get { return Prefs.Bool("a_agc", false); } set { Prefs.Set("a_agc", value); } } public static bool HighPass { get { return Prefs.Bool("a_hp", true); } set { Prefs.Set("a_hp", value); } } public static int Bitrate { get { return Prefs.Int("a_br", 64000); } set { Prefs.Set("a_br", value); } } public static bool Sounds { get { return Prefs.Bool("a_sfx", true); } set { Prefs.Set("a_sfx", value); } } public static float UserVol(string name) { return Prefs.Float("uv_" + (name ?? ""), 1f); } public static void SetUserVol(string name, float v) { Prefs.Set("uv_" + (name ?? ""), v); } public static bool UserMuted(string name) { return Prefs.Bool("um_" + (name ?? ""), false); } public static void SetUserMuted(string name, bool m) { Prefs.Set("um_" + (name ?? ""), m); } [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Unicode)] struct WaveInCaps { public ushort wMid, wPid; public uint vDriverVersion; [System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.ByValTStr, SizeConst = 32)] public string szPname; public uint dwFormats; public ushort wChannels, wReserved1; } [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Unicode)] struct WaveOutCaps { public ushort wMid, wPid; public uint vDriverVersion; [System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.ByValTStr, SizeConst = 32)] public string szPname; public uint dwFormats; public ushort wChannels, wReserved1; public uint dwSupport; } [System.Runtime.InteropServices.DllImport("winmm.dll")] static extern int waveInGetNumDevs(); [System.Runtime.InteropServices.DllImport("winmm.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode)] static extern int waveInGetDevCaps(IntPtr id, ref WaveInCaps caps, int size); [System.Runtime.InteropServices.DllImport("winmm.dll")] static extern int waveOutGetNumDevs(); [System.Runtime.InteropServices.DllImport("winmm.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode)] static extern int waveOutGetDevCaps(IntPtr id, ref WaveOutCaps caps, int size); public static List Inputs() { var l = new List(); try { int n = waveInGetNumDevs(); for (int i = 0; i < n; i++) { var c = new WaveInCaps(); l.Add(waveInGetDevCaps((IntPtr)i, ref c, System.Runtime.InteropServices.Marshal.SizeOf(typeof(WaveInCaps))) == 0 ? c.szPname : "Mikrofon " + (i + 1)); } } catch { } return l; } public static List Outputs() { var l = new List(); try { int n = waveOutGetNumDevs(); for (int i = 0; i < n; i++) { var c = new WaveOutCaps(); l.Add(waveOutGetDevCaps((IntPtr)i, ref c, System.Runtime.InteropServices.Marshal.SizeOf(typeof(WaveOutCaps))) == 0 ? c.szPname : "Zvučnici " + (i + 1)); } } catch { } return l; } } static class Fft { public static void Transform(float[] re, float[] im, bool inverse) { int n = re.Length; for (int i = 1, j = 0; i < n; i++) { int bit = n >> 1; for (; (j & bit) != 0; bit >>= 1) j ^= bit; j ^= bit; if (i < j) { float t = re[i]; re[i] = re[j]; re[j] = t; t = im[i]; im[i] = im[j]; im[j] = t; } } for (int len = 2; len <= n; len <<= 1) { double ang = 2 * Math.PI / len * (inverse ? 1 : -1); double wr = Math.Cos(ang), wi = Math.Sin(ang); int half = len / 2; for (int i = 0; i < n; i += len) { double cr = 1, ci = 0; for (int k = 0; k < half; k++) { int a = i + k, b = a + half; float xr = (float)(re[b] * cr - im[b] * ci); float xi = (float)(re[b] * ci + im[b] * cr); re[b] = re[a] - xr; im[b] = im[a] - xi; re[a] += xr; im[a] += xi; double ncr = cr * wr - ci * wi; ci = cr * wi + ci * wr; cr = ncr; } } } if (inverse) for (int i = 0; i < n; i++) { re[i] /= n; im[i] /= n; } } } class Biquad { float b0, b1, b2, a1, a2, x1, x2, y1, y2; public static Biquad HighPass(float fs, float fc) { double w = 2 * Math.PI * fc / fs, alpha = Math.Sin(w) / (2 * 0.7071), cs = Math.Cos(w), a0 = 1 + alpha; var b = new Biquad(); b.b0 = (float)((1 + cs) / 2 / a0); b.b1 = (float)(-(1 + cs) / a0); b.b2 = b.b0; b.a1 = (float)(-2 * cs / a0); b.a2 = (float)((1 - alpha) / a0); return b; } public void Reset() { x1 = x2 = y1 = y2 = 0; } public void Process(float[] s) { for (int i = 0; i < s.Length; i++) { float x = s[i]; float y = b0 * x + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2; x2 = x1; x1 = x; y2 = y1; y1 = y; s[i] = y; } } } // Utisavanje pozadinske buke: spektralno oduzimanje sa sqrt-Hann prozorom, 50% preklapanja class Denoiser { const int N = 1024, Hop = 480, Win = 960, Bins = N / 2 + 1; readonly float[] window = new float[Win]; readonly float[] inBuf = new float[Win]; readonly float[] olap = new float[Hop]; readonly float[] re = new float[N], im = new float[N]; readonly float[] noise = new float[Bins], smooth = new float[Bins], gain = new float[Bins]; readonly float[] outp = new float[960]; int frames; public Denoiser() { for (int i = 0; i < Win; i++) window[i] = (float)Math.Sqrt(0.5 - 0.5 * Math.Cos(2 * Math.PI * i / Win)); Reset(); } public void Reset() { Array.Clear(inBuf, 0, Win); Array.Clear(olap, 0, Hop); Array.Clear(noise, 0, Bins); Array.Clear(smooth, 0, Bins); for (int i = 0; i < Bins; i++) gain[i] = 1f; frames = 0; } public void Process(float[] buf) { for (int h = 0; h < 2; h++) { Array.Copy(inBuf, Hop, inBuf, 0, Hop); Array.Copy(buf, h * Hop, inBuf, Hop, Hop); for (int i = 0; i < N; i++) { re[i] = i < Win ? inBuf[i] * window[i] : 0f; im[i] = 0f; } Fft.Transform(re, im, false); frames++; for (int k = 0; k < Bins; k++) { float p = re[k] * re[k] + im[k] * im[k]; smooth[k] = frames == 1 ? p : smooth[k] * 0.7f + p * 0.3f; if (frames <= 15) noise[k] = (noise[k] * (frames - 1) + smooth[k]) / frames; else if (smooth[k] < noise[k]) noise[k] = noise[k] * 0.9f + smooth[k] * 0.1f; else noise[k] *= 1.0025f; float g = 1f - 1.6f * noise[k] / (p + 1e-12f); if (g < 0.1f) g = 0.1f; gain[k] = g > gain[k] ? gain[k] * 0.3f + g * 0.7f : gain[k] * 0.6f + g * 0.4f; re[k] *= gain[k]; im[k] *= gain[k]; if (k > 0 && k < N / 2) { re[N - k] *= gain[k]; im[N - k] *= gain[k]; } } Fft.Transform(re, im, true); for (int i = 0; i < Hop; i++) { outp[h * Hop + i] = olap[i] + re[i] * window[i]; olap[i] = re[Hop + i] * window[Hop + i]; } } Array.Copy(outp, buf, 960); } } // Cijeli lanac obrade mikrofona za jedan okvir od 20 ms (960 uzoraka na 48 kHz) class MicProcessor { readonly Biquad hp = Biquad.HighPass(48000, 90); readonly Denoiser dn = new Denoiser(); readonly float[] buf = new float[960]; readonly short[] cur = new short[960]; readonly short[] delayed = new short[960]; float agcGain = 1f, noiseFloorDb = -60f; bool haveDelayed, delayedVoice, prevVoice; DateTime holdUntil = DateTime.MinValue; public float LevelDb = -100f, ThresholdDb = -50f; public void Reset() { dn.Reset(); hp.Reset(); agcGain = 1f; noiseFloorDb = -60f; haveDelayed = false; prevVoice = false; LevelDb = -100f; } static bool IsClick(float[] b, float rms) { if (rms < 1e-4f) return false; float max = 0, sum = 0; for (int k = 0; k < 8; k++) { float e = 0; for (int i = k * 120; i < k * 120 + 120; i++) e += b[i] * b[i]; sum += e; if (e > max) max = e; } float crest = max / Math.Max(sum / 8f, 1e-9f); int zc = 0; for (int i = 1; i < 960; i++) if ((b[i] >= 0) != (b[i - 1] >= 0)) zc++; return crest > 4.5f && zc / 960f > 0.12f; } // vraca true ako se okvir u "output" treba poslati public bool Process(short[] input, short[] output) { float g = AudioCfg.InGain; for (int i = 0; i < 960; i++) buf[i] = input[i] / 32768f * g; if (AudioCfg.HighPass) hp.Process(buf); if (AudioCfg.NoiseSuppress) dn.Process(buf); double sum = 0; for (int i = 0; i < 960; i++) sum += buf[i] * buf[i]; float rms = (float)Math.Sqrt(sum / 960); float db = rms > 1e-6f ? 20f * (float)Math.Log10(rms) : -100f; if (db < noiseFloorDb) noiseFloorDb = noiseFloorDb * 0.7f + db * 0.3f; else noiseFloorDb += 0.02f; noiseFloorDb = Math.Max(-90f, Math.Min(-20f, noiseFloorDb)); float thr = AudioCfg.AutoSens ? Math.Max(-62f, Math.Min(-25f, noiseFloorDb + 14f)) : AudioCfg.SensDb; ThresholdDb = thr; LevelDb = Math.Max(db, LevelDb - 1.5f); bool click = AudioCfg.KeySuppress && IsClick(buf, rms); bool voice = db >= thr && !click; if (AudioCfg.Agc && voice) { float want = Math.Max(0.5f, Math.Min(4f, 0.1f / Math.Max(rms, 1e-4f))); agcGain += (want - agcGain) * 0.05f; } float ag = AudioCfg.Agc ? agcGain : 1f; for (int i = 0; i < 960; i++) { float v = buf[i] * ag; if (v > 1f) v = 1f; else if (v < -1f) v = -1f; cur[i] = (short)(v * 32767f); } if (AudioCfg.Ptt) { haveDelayed = false; Array.Copy(cur, output, 960); return (Native.GetAsyncKeyState(AudioCfg.PttKey) & 0x8000) != 0; } if (AudioCfg.KeySuppress) { // jedan okvir kasnjenja: odluka za prethodni okvir gleda i sljedeci, pa kratki klik ne otvara mikrofon if (!haveDelayed) { Array.Copy(cur, delayed, 960); delayedVoice = voice; prevVoice = false; haveDelayed = true; Array.Clear(output, 0, 960); return false; } bool open = delayedVoice && (voice || prevVoice); if (open) holdUntil = DateTime.UtcNow.AddMilliseconds(400); Array.Copy(delayed, output, 960); prevVoice = delayedVoice; Array.Copy(cur, delayed, 960); delayedVoice = voice; return open || DateTime.UtcNow < holdUntil; } haveDelayed = false; if (voice) holdUntil = DateTime.UtcNow.AddMilliseconds(350); Array.Copy(cur, output, 960); return DateTime.UtcNow < holdUntil; } } // Kratki zvukovi za ulazak, izlazak i mikrofon class SfxProvider : ISampleProvider { readonly Queue q = new Queue(); readonly object lk = new object(); public WaveFormat WaveFormat { get; } = WaveFormat.CreateIeeeFloatWaveFormat(48000, 1); public int Read(float[] buffer, int offset, int count) { lock (lk) { for (int i = 0; i < count; i++) buffer[offset + i] = q.Count > 0 ? q.Dequeue() : 0f; } return count; } void Play(params float[] notes) { if (!AudioCfg.Sounds) return; lock (lk) { if (q.Count > 48000) return; foreach (var f in notes) { const int n = 4320; for (int i = 0; i < n; i++) { float env = Math.Min(1f, i / 200f) * Math.Min(1f, (n - i) / 900f); q.Enqueue((float)(Math.Sin(2 * Math.PI * f * i / 48000.0) * 0.16 * env)); } } } } public void Join() { Play(660f, 990f); } public void Leave() { Play(990f, 660f); } public void Mute() { Play(440f); } public void Unmute() { Play(880f); } public void Connect() { Play(523f, 659f, 784f); } } // Mikrofon i zvucnici s obradom; koristi ga voice i test mikrofona class AudioEngine : IDisposable { public event Action Frame; public volatile float LevelDb = -100f, ThresholdDb = -50f; public volatile bool GateOpen; public bool HasMic, HasSpeakers; public readonly MixingSampleProvider Mixer; public readonly SfxProvider Sfx = new SfxProvider(); readonly VolumeSampleProvider master; readonly MicProcessor proc = new MicProcessor(); readonly short[] frame = new short[960]; readonly short[] outPcm = new short[960]; int fpos; WaveInEvent wi; WaveOutEvent wo; public AudioEngine() { Mixer = new MixingSampleProvider(WaveFormat.CreateIeeeFloatWaveFormat(48000, 1)) { ReadFully = true }; Mixer.AddMixerInput(Sfx); master = new VolumeSampleProvider(Mixer) { Volume = AudioCfg.OutGain }; } public void Start() { StartOut(); StartIn(); } void StartOut() { HasSpeakers = false; foreach (int dev in new[] { AudioCfg.OutDevice, -1 }) { try { wo = new WaveOutEvent { DeviceNumber = dev, DesiredLatency = 100, NumberOfBuffers = 3 }; wo.Init(new SampleToWaveProvider16(master)); wo.Play(); HasSpeakers = true; return; } catch { try { if (wo != null) wo.Dispose(); } catch { } wo = null; } } } void StartIn() { HasMic = false; foreach (int dev in new[] { AudioCfg.InDevice, -1 }) { try { wi = new WaveInEvent { DeviceNumber = dev, WaveFormat = new WaveFormat(48000, 16, 1), BufferMilliseconds = 20, NumberOfBuffers = 4 }; wi.DataAvailable += OnData; wi.StartRecording(); HasMic = true; return; } catch { try { if (wi != null) { wi.DataAvailable -= OnData; wi.Dispose(); } } catch { } wi = null; } } } void StopIn() { if (wi == null) return; try { wi.DataAvailable -= OnData; wi.StopRecording(); wi.Dispose(); } catch { } wi = null; } void StopOut() { if (wo == null) return; try { wo.Stop(); wo.Dispose(); } catch { } wo = null; } public void Restart() { StopIn(); StopOut(); proc.Reset(); fpos = 0; Start(); } public void ApplyVolume() { master.Volume = AudioCfg.OutGain; } void OnData(object s, WaveInEventArgs e) { for (int i = 0; i + 1 < e.BytesRecorded; i += 2) { frame[fpos++] = (short)(e.Buffer[i] | (e.Buffer[i + 1] << 8)); if (fpos == 960) { fpos = 0; bool open = proc.Process(frame, outPcm); LevelDb = proc.LevelDb; ThresholdDb = proc.ThresholdDb; GateOpen = open; var h = Frame; if (h != null) h(outPcm, open); } } } public void Dispose() { StopIn(); StopOut(); } } // Test mikrofona: cujes sam sebe, s istom obradom kao u voiceu class MicTest : IDisposable { public readonly AudioEngine Audio = new AudioEngine(); readonly BufferedWaveProvider loop = new BufferedWaveProvider(new WaveFormat(48000, 16, 1)) { BufferDuration = TimeSpan.FromSeconds(1), DiscardOnBufferOverflow = true }; public MicTest() { Audio.Mixer.AddMixerInput(new Pcm16BitToSampleProvider(loop)); Audio.Frame += (pcm, open) => { if (!open) return; var b = new byte[1920]; Buffer.BlockCopy(pcm, 0, b, 0, 1920); loop.AddSamples(b, 0, 1920); }; Audio.Start(); } public void Dispose() { Audio.Dispose(); } } } // ===================== Voice.cs ===================== using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using NAudio.Wave; using NAudio.Wave.SampleProviders; using Concentus.Structs; using Concentus.Enums; namespace Fragline { class VPeer { public uint Sid; public string Name = "?"; public OpusDecoder Dec; public BufferedWaveProvider Buf; public VolumeSampleProvider Vol; public uint LastCtr; public DateTime LastAudio = DateTime.MinValue; } class VoiceClient : IDisposable { public event Action State; public volatile bool Muted, Deafened, Talking; public string Room, Label, Warning; public AudioEngine Audio; readonly object lk = new object(); readonly object encLock = new object(); readonly Dictionary peers = new Dictionary(); readonly System.Threading.ManualResetEvent welcome = new System.Threading.ManualResetEvent(false); static readonly WaveFormat Pcm = new WaveFormat(48000, 16, 1); UdpClient udp; IPEndPoint srv; VoiceCrypto cr; volatile bool running; volatile string kick; volatile uint sid; int ctr; long lastRx; bool rosterSeen; System.Threading.Thread rx; System.Threading.Timer pingTimer; OpusEncoder enc; readonly byte[] obuf = new byte[1275]; public bool Connect(string host, int port, string ticket, string keyB64, out string error) { error = null; try { IPAddress addr; if (!IPAddress.TryParse(host, out addr)) addr = Dns.GetHostAddresses(host).FirstOrDefault(a => a.AddressFamily == AddressFamily.InterNetwork); if (addr == null) { error = "Ne mogu pronaći voice server."; return false; } srv = new IPEndPoint(addr, port); cr = new VoiceCrypto(Convert.FromBase64String(keyB64)); udp = new UdpClient(0, AddressFamily.InterNetwork); try { udp.Client.IOControl(-1744830452, new byte[] { 0, 0, 0, 0 }, null); } catch { } running = true; lastRx = DateTime.UtcNow.Ticks; rx = new System.Threading.Thread(RxLoop) { IsBackground = true, Name = "fragline-voice" }; rx.Start(); var hello = new byte[1 + ticket.Length]; hello[0] = 1; Encoding.ASCII.GetBytes(ticket, 0, ticket.Length, hello, 1); for (int i = 0; i < 6 && !welcome.WaitOne(0) && kick == null; i++) { udp.Send(hello, hello.Length, srv); welcome.WaitOne(700); } if (!welcome.WaitOne(0)) { error = kick ?? "Voice server se ne javlja. Provjeri je li UDP port otvoren."; Dispose(); return false; } StartAudio(); pingTimer = new System.Threading.Timer(_ => Ping(), null, 3000, 5000); return true; } catch (Exception e) { error = "Spajanje nije uspjelo: " + e.Message; Dispose(); return false; } } void RxLoop() { var ep = new IPEndPoint(IPAddress.Any, 0); while (running) { byte[] d; try { d = udp.Receive(ref ep); } catch { if (!running) return; continue; } if (d == null || d.Length < 17 || !ep.Address.Equals(srv.Address)) continue; var p = cr.Open(d, d.Length, 1); if (p == null) continue; System.Threading.Interlocked.Exchange(ref lastRx, DateTime.UtcNow.Ticks); switch (d[0]) { case 2: sid = VoiceCrypto.R32(d, 1); welcome.Set(); break; case 5: if (p.Length > 8) OnAudio(VoiceCrypto.R32(p, 0), VoiceCrypto.R32(p, 4), p, 8, p.Length - 8); break; case 8: OnRoster(Encoding.UTF8.GetString(p)); break; case 10: kick = Encoding.UTF8.GetString(p); if (welcome.WaitOne(0)) { running = false; var h = State; if (h != null) h(kick, false); } return; } } } static float VolFor(string name) { return AudioCfg.UserMuted(name) ? 0f : AudioCfg.UserVol(name); } VPeer NewPeer(uint s) { var pr = new VPeer { Sid = s, Dec = OpusDecoder.Create(48000, 1), Buf = new BufferedWaveProvider(Pcm) { BufferDuration = TimeSpan.FromSeconds(1), DiscardOnBufferOverflow = true } }; pr.Vol = new VolumeSampleProvider(new Pcm16BitToSampleProvider(pr.Buf)) { Volume = 1f }; peers[s] = pr; if (Audio != null) Audio.Mixer.AddMixerInput(pr.Vol); return pr; } public void RefreshVolumes() { lock (lk) foreach (var p in peers.Values) p.Vol.Volume = VolFor(p.Name); } static void Push(BufferedWaveProvider b, short[] pcm, int n) { var bytes = new byte[n * 2]; Buffer.BlockCopy(pcm, 0, bytes, 0, n * 2); b.AddSamples(bytes, 0, bytes.Length); } void OnAudio(uint spk, uint sctr, byte[] p, int off, int len) { if (Deafened) return; VPeer peer; lock (lk) { if (!peers.TryGetValue(spk, out peer)) peer = NewPeer(spk); } var pcm = new short[960]; lock (peer) { if (sctr <= peer.LastCtr) return; uint gap = peer.LastCtr == 0 ? 0 : sctr - peer.LastCtr - 1; peer.LastCtr = sctr; double buffered = peer.Buf.BufferedDuration.TotalMilliseconds; if (buffered > 400) peer.Buf.ClearBuffer(); else if (buffered < 20) peer.Buf.AddSamples(new byte[5760], 0, 5760); for (uint i = 0; i < Math.Min(gap, 3u); i++) { try { int k = peer.Dec.Decode(null, 0, 0, pcm, 0, 960, false); if (k > 0) Push(peer.Buf, pcm, k); } catch { } } var data = new byte[len]; Buffer.BlockCopy(p, off, data, 0, len); try { int n = peer.Dec.Decode(data, 0, len, pcm, 0, 960, false); if (n > 0) Push(peer.Buf, pcm, n); } catch { return; } peer.LastAudio = DateTime.UtcNow; } } void OnRoster(string json) { var arr = J.Parse(json) as object[]; if (arr == null) return; var seen = new HashSet(); int added = 0, removed = 0; lock (lk) { foreach (var o in arr) { uint s = (uint)J.L(o, "s"); if (s == 0 || s == sid) continue; seen.Add(s); VPeer p; if (!peers.TryGetValue(s, out p)) { p = NewPeer(s); added++; } p.Name = J.S(o, "n") ?? "?"; p.Vol.Volume = VolFor(p.Name); } foreach (var k in peers.Keys.Where(k => !seen.Contains(k)).ToList()) { if (Audio != null) Audio.Mixer.RemoveMixerInput(peers[k].Vol); peers.Remove(k); removed++; } } if (rosterSeen && Audio != null) { if (added > 0) Audio.Sfx.Join(); else if (removed > 0) Audio.Sfx.Leave(); } rosterSeen = true; } void StartAudio() { enc = OpusEncoder.Create(48000, 1, OpusApplication.OPUS_APPLICATION_VOIP); enc.Bitrate = AudioCfg.Bitrate; Audio = new AudioEngine(); lock (lk) foreach (var p in peers.Values) Audio.Mixer.AddMixerInput(p.Vol); Audio.Frame += OnFrame; Audio.Start(); if (!Audio.HasMic) Warning = "Mikrofon nije pronađen. Možeš samo slušati."; else if (!Audio.HasSpeakers) Warning = "Zvučnici nisu pronađeni."; Audio.Sfx.Connect(); } void OnFrame(short[] pcm, bool open) { bool s = open && !Muted && !Deafened && running && sid != 0; Talking = s; if (!s) return; int n; lock (encLock) { try { n = enc.Encode(pcm, 0, 960, obuf, 0, obuf.Length); } catch { return; } } if (n > 0) Send(3, obuf, n); } public void ApplyBitrate() { lock (encLock) { if (enc != null) enc.Bitrate = AudioCfg.Bitrate; } } public void RestartAudio() { if (Audio != null) Audio.Restart(); } public void SetMuted(bool m) { Muted = m; if (Audio != null) { if (m) Audio.Sfx.Mute(); else Audio.Sfx.Unmute(); } } public void SetDeafened(bool d) { Deafened = d; if (d) lock (lk) foreach (var p in peers.Values) p.Buf.ClearBuffer(); if (Audio != null) { if (d) Audio.Sfx.Mute(); else Audio.Sfx.Unmute(); } } void Send(byte type, byte[] payload, int len) { if (sid == 0 || udp == null || cr == null) return; var pkt = cr.Seal(type, sid, (uint)System.Threading.Interlocked.Increment(ref ctr), 0, payload, 0, len); try { udp.Send(pkt, pkt.Length, srv); } catch { } } void Ping() { if (!running) return; long last = System.Threading.Interlocked.Read(ref lastRx); if (DateTime.UtcNow.Ticks - last > TimeSpan.FromSeconds(16).Ticks) { running = false; var h = State; if (h != null) h("Veza s voice serverom je prekinuta.", false); return; } var b = BitConverter.GetBytes(DateTime.UtcNow.Ticks); Send(6, b, b.Length); } public void Leave() { if (!running) return; Send(9, new byte[0], 0); Send(9, new byte[0], 0); } public List LiveMembers() { lock (lk) return peers.Values.OrderBy(p => p.Name) .Select(p => new VMember { Name = p.Name, Speaking = (DateTime.UtcNow - p.LastAudio).TotalMilliseconds < 350 }) .ToList(); } public void Dispose() { running = false; try { if (pingTimer != null) pingTimer.Dispose(); } catch { } try { if (Audio != null) { Audio.Frame -= OnFrame; Audio.Dispose(); } } catch { } try { if (udp != null) udp.Close(); } catch { } } } } // ===================== App.cs ===================== using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using Microsoft.Win32; namespace Fragline { class DarkColors : ProfessionalColorTable { public override Color ToolStripDropDownBackground { get { return Theme.Menu; } } public override Color ImageMarginGradientBegin { get { return Theme.Menu; } } public override Color ImageMarginGradientMiddle { get { return Theme.Menu; } } public override Color ImageMarginGradientEnd { get { return Theme.Menu; } } public override Color MenuBorder { get { return Theme.Menu; } } public override Color MenuItemBorder { get { return Theme.Accent; } } public override Color MenuItemSelected { get { return Theme.Accent; } } public override Color MenuItemSelectedGradientBegin { get { return Theme.Accent; } } public override Color MenuItemSelectedGradientEnd { get { return Theme.Accent; } } public override Color MenuItemPressedGradientBegin { get { return Theme.Accent; } } public override Color MenuItemPressedGradientEnd { get { return Theme.Accent; } } public override Color SeparatorDark { get { return Theme.Panel3; } } public override Color SeparatorLight { get { return Theme.Menu; } } public override Color CheckBackground { get { return Theme.Accent; } } public override Color CheckSelectedBackground { get { return Theme.Accent; } } public override Color CheckPressedBackground { get { return Theme.Accent; } } } class DarkRenderer : ToolStripProfessionalRenderer { public DarkRenderer() : base(new DarkColors()) { RoundedEdges = false; } protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e) { e.TextColor = !e.Item.Enabled ? Theme.Muted : e.Item.Selected ? Color.White : e.Item.ForeColor == Theme.Red ? Theme.Red : Theme.Text2; base.OnRenderItemText(e); } protected override void OnRenderItemCheck(ToolStripItemImageRenderEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; var r = new RectangleF(e.ImageRectangle.X, e.ImageRectangle.Y, e.ImageRectangle.Width, e.ImageRectangle.Height); using (var p = Theme.Round(r, 4)) using (var b = new SolidBrush(Theme.Accent)) g.FillPath(b, p); using (var pen = new Pen(Color.White, 2f)) g.DrawLines(pen, new[] { new PointF(r.X + r.Width * 0.25f, r.Y + r.Height * 0.52f), new PointF(r.X + r.Width * 0.43f, r.Y + r.Height * 0.70f), new PointF(r.X + r.Width * 0.76f, r.Y + r.Height * 0.32f) }); } } // Globalni precaci: 1 = Scroll Lock, 2 = Ctrl+Shift+M, 3 = Ctrl+Shift+D class HotkeyWindow : NativeWindow, IDisposable { readonly Action action; public HotkeyWindow(Action a) { action = a; CreateHandle(new CreateParams()); } public void Set(bool shot, bool voice) { for (int i = 1; i <= 3; i++) Native.UnregisterHotKey(Handle, i); const uint norepeat = 0x4000, ctrlShift = 0x2 | 0x4; if (shot) Native.RegisterHotKey(Handle, 1, norepeat, 0x91); if (voice) { Native.RegisterHotKey(Handle, 2, norepeat | ctrlShift, 0x4D); Native.RegisterHotKey(Handle, 3, norepeat | ctrlShift, 0x44); } } protected override void WndProc(ref Message m) { if (m.Msg == 0x0312) action(m.WParam.ToInt32()); base.WndProc(ref m); } public void Dispose() { for (int i = 1; i <= 3; i++) Native.UnregisterHotKey(Handle, i); DestroyHandle(); } } class TrayApp : ApplicationContext { static readonly string Dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Fragline"); static readonly string LoginFile = Path.Combine(Dir, "login.dat"); const string RunKey = @"Software\Microsoft\Windows\CurrentVersion\Run"; readonly NotifyIcon icon; readonly Timer timer = new Timer { Interval = 15000 }; readonly Timer stateTimer = new Timer { Interval = 6000 }; readonly HotkeyWindow hotkey; readonly ShellForm main; readonly ToolStripMenuItem statusItem, loginItem, autoItem, profileItem, voiceLeaveItem, muteItem; string token, username, color, avatar, presence, lastGame, notifyTarget; bool busy, shooting, hintShown, polling, joining, voiceEnabled, voiceServer, isAdmin; List friends = new List(); List clans = new List(); Dictionary prev; VoiceClient voice; VRoom lastCreated; AudioSettingsForm audioForm; public string Username { get { return username; } } public string ColorHex { get { return color; } } public string AvatarUrl { get { return avatar; } } public static readonly Version Current = Norm(System.Reflection.Assembly.GetExecutingAssembly().GetName().Version); string updateVer, updateUrl, updateSha, updatePage; readonly Timer callTimer = new Timer { Interval = 2500 }; // obavijesti (komentari, lajkovi, spomeni...) s /client/notifs readonly Timer notifTimer = new Timer { Interval = 30000 }; HashSet notifSeen; List lastNotifs = new List(); int unreadNotifs; bool notifPolling; Icon badgeIcon; string statusText = ""; // auto-prepoznavanje game servera (korak 5) string lastServerSent, lastSlug; // LFG oglasi (korak 6) readonly Timer lfgTimer = new Timer { Interval = 60000 }; bool lfgPolling; IncomingCallForm incomingForm; string callOutTo, incomingFrom, incomingColor, incomingAvatar; bool callOutAccepted, callPolling, callHadPeer; bool updateShown, updating; public string Presence { get { return presence; } } // "Odsutan" sam od sebe kad nitko ne dira mis i tipkovnicu 10 minuta public bool AutoIdle { get { return presence == "online" && Prefs.Bool("auto_idle", true) && Native.IdleSeconds() > 600; } } string SentPresence { get { return AutoIdle ? "idle" : presence; } } bool Dnd { get { return presence == "dnd"; } } public VoiceClient Voice { get { return voice; } } public bool VoiceEnabled { get { return voiceEnabled; } } public bool VoiceServer { get { return voiceServer; } } public bool IsAdmin { get { return isAdmin; } } public List Clans { get { return clans; } } public TrayApp(bool startHidden) { Directory.CreateDirectory(Dir); Prefs.Load(); presence = Prefs.Str("presence", "online"); statusItem = new ToolStripMenuItem("Nisi prijavljen") { Enabled = false }; profileItem = new ToolStripMenuItem("Moj profil", null, (s, e) => OpenProfile()); loginItem = new ToolStripMenuItem("Prijavi se", null, (s, e) => { if (token == null) ShowLogin(); else LogoutAndAsk(); }); autoItem = new ToolStripMenuItem("Pokreni s Windowsima", null, (s, e) => SetAuto(!IsAuto())); muteItem = new ToolStripMenuItem("Isključi mikrofon", null, (s, e) => ToggleMute()) { Visible = false }; voiceLeaveItem = new ToolStripMenuItem("Izađi iz voicea", null, (s, e) => LeaveVoice()) { Visible = false }; var openItem = new ToolStripMenuItem("Otvori Fragline", null, (s, e) => ShowMain()) { Font = Theme.Body(14, FontStyle.Bold) }; var menu = new ContextMenuStrip { Renderer = new DarkRenderer(), Font = Theme.Body(14), ShowImageMargin = true }; menu.Items.Add(openItem); menu.Items.Add(statusItem); menu.Items.Add(muteItem); menu.Items.Add(voiceLeaveItem); menu.Items.Add(new ToolStripSeparator()); menu.Items.Add(new ToolStripMenuItem("Postavke zvuka i mikrofona", null, (s, e) => ShowAudioSettings())); menu.Items.Add(new ToolStripMenuItem("Otvori web messenger", null, (s, e) => Open("/app"))); menu.Items.Add(profileItem); menu.Items.Add(new ToolStripMenuItem("Screenshot (Scroll Lock)", null, (s, e) => Screenshot())); menu.Items.Add(new ToolStripSeparator()); menu.Items.Add(autoItem); menu.Items.Add(loginItem); menu.Items.Add(new ToolStripMenuItem("Izlaz", null, (s, e) => Quit())); menu.Opening += (s, e) => { autoItem.Checked = IsAuto(); profileItem.Enabled = token != null; voiceLeaveItem.Visible = voice != null; muteItem.Visible = voice != null; if (voice != null) muteItem.Text = voice.Muted ? "Uključi mikrofon" : "Isključi mikrofon"; }; icon = new NotifyIcon { Icon = Program.AppIcon, Text = "Fragline", Visible = true, ContextMenuStrip = menu }; icon.DoubleClick += (s, e) => ShowMain(); icon.BalloonTipClicked += (s, e) => { var u = notifyTarget; var f = u == null ? null : friends.FirstOrDefault(x => x.Username == u); if (f != null) OpenChat(f); else ShowMain(); }; hotkey = new HotkeyWindow(id => { if (id == 1) Screenshot(); else if (id == 2) ToggleMute(); else if (id == 3) ToggleDeaf(); }); ApplyHotkeys(); timer.Tick += (s, e) => Beat(); stateTimer.Tick += (s, e) => PollState(); callTimer.Tick += (s, e) => PollCall(); notifTimer.Tick += (s, e) => PollNotifs(); lfgTimer.Tick += (s, e) => PollLfg(); // 5.6: novi raspored "Dock" postaje zadani i za postojece korisnike (osim onih na klasicnom Xfire izgledu) if (!Prefs.Bool("dock_v1", false)) { if (Prefs.Str("layout", "dock") != "xfire") Prefs.Set("layout", "dock"); Prefs.Set("dock_v1", true); } var layout = Prefs.Str("layout", "dock"); main = layout == "xfire" ? (ShellForm)new XfireForm(this) : layout == "discord" ? (ShellForm)new NeoForm(this) : new OrbitForm(this); var handle = main.Handle; #if PREVIEW // probna verzija: automatska prijava testnim racunom var pu = Environment.GetEnvironmentVariable("FRAGLINE_USER"); if (pu != null) { try { var res = Api.Login(pu, Environment.GetEnvironmentVariable("FRAGLINE_PASS") ?? ""); token = Api.Field(res, "token"); username = Api.Field(res, "username"); color = Api.Field(res, "color"); SaveLogin(); } catch { } } #endif LoadLogin(); if (token == null) ShowLogin(); else { ApplyUser(); Start(); if (!startHidden) ShowMain(); } } public void ApplyHotkeys() { hotkey.Set(Prefs.Bool("hk_shot", true), Prefs.Bool("hk_voice", true)); } public void Open(string path) { try { Process.Start(Api.Base + path); } catch { } } public void OpenProfile() { if (username != null) Open("/profile/" + Uri.EscapeDataString(username)); } public void Tip(string text, ToolTipIcon ic = ToolTipIcon.Info) { notifyTarget = null; Toast.Show("Fragline", text, null, null, ic == ToolTipIcon.Warning || ic == ToolTipIcon.Error ? "warn" : "info", null); } public string ApiGet(string path) { var t = token; if (t == null) throw new InvalidOperationException("Nisi prijavljen."); return Api.Get(path, t); } public string ApiDel(string path) { var t = token; if (t == null) throw new InvalidOperationException("Nisi prijavljen."); return ApiDelete(path, t); } public string ApiPost(string path, object body) { var t = token; if (t == null) throw new InvalidOperationException("Nisi prijavljen."); return Api.PostJ(path, body, t); } static string ApiDelete(string path, string t) { using (var wc = new WebClient { Encoding = Encoding.UTF8 }) { wc.Headers[HttpRequestHeader.Accept] = "application/json"; wc.Headers[HttpRequestHeader.Authorization] = "Bearer " + t; return wc.UploadString(Api.Base + path, "DELETE", ""); } } public void ShowMain() { if (token == null) { ShowLogin(); return; } main.Show(); if (main.WindowState == FormWindowState.Minimized) main.WindowState = FormWindowState.Normal; main.Activate(); } public void HiddenHint() { if (hintShown) return; hintShown = true; Tip("Fragline i dalje radi kraj sata. Dvoklik na plamen ga otvara."); } void ApplyUser() { main.User.User = username ?? ""; main.User.ColorHex = color; main.User.Avatar = avatar; main.User.Presence = presence; main.User.Invalidate(); } public void SetPresence(string p) { presence = p; Prefs.Set("presence", p); ApplyUser(); Beat(); } // Nova tema se primjenjuje pri pokretanju, pa se aplikacija ponovno pokrene public void Restart() { try { Process.Start(Application.ExecutablePath, "--restarted"); } catch { return; } Quit(); } public void ShowVoicePage() { ShowMain(); main.ShowVoice(); } void SetStatus(string s) { statusText = s; statusItem.Text = s; RefreshTrayText(); } void RefreshTrayText() { var t = "Fragline - " + statusText; if (unreadNotifs > 0) t += " • " + unreadNotifs + (unreadNotifs == 1 ? " nova obavijest" : " nepročitanih"); icon.Text = t.Length > 63 ? t.Substring(0, 63) : t; } void Start() { loginItem.Text = "Odjavi se (" + username + ")"; SetStatus("Spajam se..."); timer.Start(); stateTimer.Start(); callTimer.Start(); notifTimer.Start(); lfgTimer.Start(); Beat(); PollState(); PollNotifs(); PollLfg(); } void CloseChats() { main.CloseChat(null); } void Logout() { timer.Stop(); stateTimer.Stop(); callTimer.Stop(); notifTimer.Stop(); lfgTimer.Stop(); notifSeen = null; SetUnread(0); lastServerSent = null; lastSlug = null; CloseIncoming(); LeaveVoice(); CloseChats(); token = null; username = null; avatar = null; lastGame = null; prev = null; friends = new List(); clans = new List(); try { File.Delete(LoginFile); } catch { } loginItem.Text = "Prijavi se"; SetStatus("Nisi prijavljen"); main.Hide(); } public void LogoutAndAsk() { Logout(); ShowLogin(); } void Quit() { callTimer.Stop(); CloseIncoming(); LeaveVoice(); timer.Stop(); stateTimer.Stop(); notifTimer.Stop(); lfgTimer.Stop(); CloseChats(); if (audioForm != null && !audioForm.IsDisposed) audioForm.Close(); hotkey.Dispose(); main.AllowClose = true; main.Close(); icon.Visible = false; icon.Dispose(); ExitThread(); } void ShowLogin() { using (var f = new LoginForm()) { if (f.ShowDialog() != DialogResult.OK) { if (token == null) SetStatus("Nisi prijavljen"); return; } bool first = !File.Exists(LoginFile); token = f.Token; username = f.Username; color = f.UserColor; SaveLogin(); if (first) SetAuto(true); ApplyUser(); Start(); ShowMain(); } } void LoadLogin() { try { var data = ProtectedData.Unprotect(File.ReadAllBytes(LoginFile), null, DataProtectionScope.CurrentUser); var parts = Encoding.UTF8.GetString(data).Split('\n'); if (parts.Length >= 2) { username = parts[0]; token = parts[1]; } if (parts.Length >= 3) color = parts[2]; } catch { token = null; } } void SaveLogin() { try { File.WriteAllBytes(LoginFile, ProtectedData.Protect(Encoding.UTF8.GetBytes(username + "\n" + token + "\n" + (color ?? "")), null, DataProtectionScope.CurrentUser)); } catch { } } public static bool IsAuto() { using (var k = Registry.CurrentUser.OpenSubKey(RunKey)) return k != null && k.GetValue("Fragline") != null; } public static void SetAuto(bool on) { using (var k = Registry.CurrentUser.CreateSubKey(RunKey)) { if (on) k.SetValue("Fragline", "\"" + Application.ExecutablePath + "\" --tray"); else k.DeleteValue("Fragline", false); } } // ---------- igra ---------- async void Beat() { if (busy || token == null) return; busy = true; try { var procs = await Task.Run(() => Process.GetProcesses() .Select(p => { try { return p.ProcessName; } catch { return null; } }) .Where(n => n != null).Distinct().ToArray()); var t = token; var sp = SentPresence; var res = await Task.Run(() => Api.Heartbeat(t, procs, sp)); var game = Api.Field(res, "game"); var since = Api.Field(res, "since"); var c = Api.Field(res, "color"); if (c != null && c != color) { color = c; SaveLogin(); ApplyUser(); } main.User.Offline = false; main.User.Game = game; DateTimeOffset dto; main.User.SinceUtc = since != null && DateTimeOffset.TryParse(since, CultureInfo.InvariantCulture, DateTimeStyles.None, out dto) ? (DateTime?)dto.UtcDateTime : null; main.User.Invalidate(); SetStatus(game == null ? "Online, nijedna igra" : "Igraš: " + game); if (game != null && game != lastGame && !Dnd) Tip("Prepoznato: " + game + ". Scroll Lock = screenshot."); lastGame = game; lastSlug = Api.Field(res, "slug"); DetectServer(); } catch (WebException e) { if (Api.Status(e) == 401 || Api.Status(e) == 403) { var msg = Api.ErrorMessage(e); Logout(); Tip(msg, ToolTipIcon.Warning); } else { SetStatus("Nema veze sa serverom"); main.User.Offline = true; main.User.Invalidate(); } } catch { SetStatus("Greška"); } finally { busy = false; } } public async void Screenshot() { if (token == null) { ShowLogin(); return; } if (shooting) return; shooting = true; string file = Path.Combine(Path.GetTempPath(), "fragline_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".jpg"); try { var bounds = Screen.FromHandle(Native.GetForegroundWindow()).Bounds; using (var bmp = new Bitmap(bounds.Width, bounds.Height)) { using (var g = Graphics.FromImage(bmp)) g.CopyFromScreen(bounds.Location, Point.Empty, bounds.Size); var enc = ImageCodecInfo.GetImageEncoders().First(x => x.FormatID == ImageFormat.Jpeg.Guid); var ps = new EncoderParameters(1); ps.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 85L); bmp.Save(file, enc, ps); } System.Media.SystemSounds.Asterisk.Play(); var t = token; var res = await Task.Run(() => Api.Upload(t, file)); main.SetShot(lastGame, Api.Field(res, "url")); Tip("Screenshot je spremljen na tvoj profil."); } catch (WebException e) { Tip("Screenshot nije poslan: " + Api.ErrorMessage(e), ToolTipIcon.Warning); } catch (Exception e) { Tip("Screenshot nije uspio: " + e.Message, ToolTipIcon.Warning); } finally { try { File.Delete(file); } catch { } shooting = false; } } // ---------- prijatelji, sobe, obavijesti ---------- async void PollState() { if (polling || token == null) return; polling = true; try { var t = token; var res = await Task.Run(() => Api.Get("/client/state", t)); var o = J.Parse(res); if (o == null) return; var list = new List(); foreach (var x in J.A(o, "friends")) { list.Add(new Friend { Username = J.S(x, "username"), Color = J.S(x, "color"), Avatar = J.S(x, "avatar"), Presence = J.S(x, "presence"), Status = J.S(x, "status"), Game = J.S(x, "game"), GameIcon = J.S(x, "game_icon"), Since = J.S(x, "since"), Voice = J.S(x, "voice"), Unread = (int)J.L(x, "unread") }); } var cl0 = J.D(o, "client"); CheckUpdate(J.S(cl0, "version"), J.S(cl0, "url"), J.S(cl0, "sha256"), J.S(cl0, "page")); var me = J.D(o, "me"); isAdmin = J.B(me, "admin"); var av = J.S(me, "avatar"); if (av != avatar) { avatar = av; ApplyUser(); } var cl = new List(); foreach (var x in J.A(o, "clans")) cl.Add(new ClanItem { Id = J.L(x, "id"), Name = J.S(x, "name"), Tag = J.S(x, "tag"), Slug = J.S(x, "slug"), Color = J.S(x, "color"), Owner = J.B(x, "owner") }); clans = cl; main.SetClans(cl); var v = J.D(o, "voice"); voiceEnabled = J.B(v, "enabled"); voiceServer = J.B(v, "server"); var rooms = new List(); foreach (var x in J.A(v, "rooms")) { var rm = new VRoom { Key = J.S(x, "key"), Label = J.S(x, "label"), Custom = J.B(x, "custom"), Mine = J.B(x, "mine"), Owner = J.S(x, "owner"), Privacy = J.S(x, "privacy"), Limit = (int)J.L(x, "limit") }; foreach (var m in J.A(x, "members")) rm.Members.Add(new VMember { Name = J.S(m, "name"), Color = J.S(m, "color"), Avatar = J.S(m, "avatar"), Speaking = J.B(m, "speaking") }); rooms.Add(rm); } int requests = (int)J.L(o, "requests"); var reqs = new List(); foreach (var x in J.A(o, "incoming")) reqs.Add(new FriendReq { Id = J.L(x, "id"), Username = J.S(x, "username"), Color = J.S(x, "color"), Avatar = J.S(x, "avatar"), Incoming = true }); foreach (var x in J.A(o, "outgoing")) reqs.Add(new FriendReq { Id = J.L(x, "id"), Username = J.S(x, "username"), Color = J.S(x, "color"), Avatar = J.S(x, "avatar") }); main.SetRequests(reqs); Notify(list); friends = list; main.SetFriends(list, requests); main.Voice.SetRooms(rooms); } catch { } finally { polling = false; } } void Notify(List list) { if (prev != null) { foreach (var f in list) { Friend p; if (f.Username == null || !prev.TryGetValue(f.Username, out p)) continue; if (Dnd) continue; if (Prefs.Bool("notify_games", true) && f.Game != null && f.Game != p.Game) { var fg = f; Toast.Show(f.Username, "Počeo je igrati " + f.Game, f.Avatar, f.Color, "game", () => OpenChat(fg)); } if (f.Unread > p.Unread && !ChatActive(f.Username)) { if (Prefs.Bool("notify_msgs", true)) { var fm = f; int n = f.Unread; Toast.Show(f.Username, n > 1 ? "Ima " + n + " nepročitanih poruka za tebe. Klikni da odgovoriš." : "Poslao ti je novu poruku. Klikni da odgovoriš.", f.Avatar, f.Color, "msg", () => OpenChat(fm)); } if (Prefs.Bool("msg_sound", true)) Ding.Play(); if (main.Visible && !main.ContainsFocus) Native.Flash(main); } } } prev = new Dictionary(); foreach (var f in list) if (f.Username != null) prev[f.Username] = f; } // ---------- auto-prepoznavanje game servera ---------- // Kad igraš, nađi javnu adresu servera iz TCP veza procesa igre i javi je serveru // (samo kad se promijeni). Kad igra stane ili je dijeljenje isključeno: očisti. async void DetectServer() { if (token == null) return; bool share = Prefs.Bool("share_server", true); string addr = ""; if (share && lastGame != null) { int pid = ServerScan.ForegroundPid(); try { addr = await Task.Run(() => ServerScan.Best(pid)) ?? ""; } catch { addr = ""; } } if (addr == (lastServerSent ?? "")) return; // samo na promjenu lastServerSent = addr; var body = new Dictionary { { "address", addr } }; if (addr.Length > 0 && lastSlug != null) body["game"] = lastSlug; var t = token; try { await Task.Run(() => Api.PostJ("/client/server", body, t)); } catch { } } // ---------- obavijesti (komentari, lajkovi, spomeni) ---------- // Svakih 30 s: /client/notifs -> badge na tray ikoni + toast za svaku novu stavku. async void PollNotifs() { if (notifPolling || token == null) return; notifPolling = true; try { var t = token; var o = J.Parse(await Task.Run(() => Api.Get("/client/notifs", t))); if (o == null) return; int unread = (int)J.L(o, "unread"); bool first = notifSeen == null; if (first) notifSeen = new HashSet(); bool busy = Native.FullScreenBusy(); int shown = 0; var list = new List(); foreach (var x in J.A(o, "items")) { var n = new NotifItem { Id = J.S(x, "id"), Who = J.S(x, "who"), WhoColor = J.S(x, "who_color"), Text = J.S(x, "text"), Extra = J.S(x, "extra"), Icon = J.S(x, "icon"), Url = J.S(x, "url"), Ago = J.S(x, "ago"), New = J.B(x, "new") }; list.Add(n); if (n.Id == null || notifSeen.Contains(n.Id)) continue; notifSeen.Add(n.Id); // na prvom pollu samo zapamti postojece (bez bujice toastova), badge/listu svejedno postavi if (first || !n.New || Dnd || busy || shown >= 3) continue; var body = (n.Text ?? "") + (string.IsNullOrEmpty(n.Extra) ? "" : " — " + n.Extra); var title = (string.IsNullOrEmpty(n.Icon) ? "" : n.Icon + " ") + (n.Who ?? "Fragline"); var url = n.Url; Toast.Show(title, body, null, n.WhoColor, "info", url == null ? (Action)null : () => { try { Process.Start(url); } catch { } }); shown++; } lastNotifs = list; SetUnread(unread); main.SetNotifs(unread, list); } catch { } finally { notifPolling = false; } } public async void MarkNotifsSeen() { if (token == null || unreadNotifs == 0) return; SetUnread(0); main.SetNotifs(0, lastNotifs); try { var t = token; await Task.Run(() => Api.PostJ("/client/notifs/seen", new Dictionary(), t)); } catch { } } void SetUnread(int n) { unreadNotifs = Math.Max(0, n); try { icon.Icon = unreadNotifs > 0 ? BadgeIcon(unreadNotifs) : Program.AppIcon; } catch { } RefreshTrayText(); } // Tray ikona s crvenim brojcanikom nepročitanih obavijesti Icon BadgeIcon(int n) { try { using (var bmp = new Bitmap(32, 32)) { using (var g = Graphics.FromImage(bmp)) { g.Clear(Color.Transparent); g.DrawIcon(Program.AppIcon, new Rectangle(0, 0, 32, 32)); g.SmoothingMode = SmoothingMode.AntiAlias; var s = n > 9 ? "9+" : n.ToString(CultureInfo.InvariantCulture); var r = new Rectangle(15, 1, 16, 16); using (var b = new SolidBrush(Color.FromArgb(242, 63, 67))) g.FillEllipse(b, r); using (var pen = new Pen(Color.White, 1.5f)) g.DrawEllipse(pen, r); using (var f = new Font("Segoe UI", 9f, FontStyle.Bold, GraphicsUnit.Pixel)) TextRenderer.DrawText(g, s, f, r, Color.White, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding); } var old = badgeIcon; badgeIcon = Icon.FromHandle(bmp.GetHicon()); if (old != null) { try { Native.DestroyIcon(old.Handle); } catch { } old.Dispose(); } return badgeIcon; } } catch { return Program.AppIcon; } } // ---------- LFG (traženje ekipe) ---------- async void PollLfg() { if (lfgPolling || token == null) return; lfgPolling = true; try { var t = token; var o = J.Parse(await Task.Run(() => Api.Get("/client/lfg", t))); if (o == null) return; var list = new List(); foreach (var x in J.A(o, "items")) { var li = new LfgItem { Id = J.L(x, "id"), Game = J.S(x, "game"), GameName = J.S(x, "game_name"), Author = J.S(x, "author"), Note = J.S(x, "note"), Slots = (int)J.L(x, "slots"), ExpiresMin = (int)J.L(x, "expires_in_min"), Mine = J.B(x, "mine"), JoinedByMe = J.B(x, "joined_by_me") }; foreach (var j in J.A(x, "joined")) { var s = Convert.ToString(j); if (s != null) li.Joined.Add(s); } list.Add(li); } main.SetLfg(list); } catch { } finally { lfgPolling = false; } } List games; // "Objavi oglas": popis igara dohvaca se jednom, oglas ide na server pa se vidi i na webu public async void NewLfg() { if (token == null) return; if (games == null || games.Count == 0) { try { var t = token; var o = J.Parse(await Task.Run(() => Api.Get("/client/games", t))); var list = new List(); foreach (var x in J.A(o, "items")) list.Add(new GameItem { Slug = J.S(x, "slug"), Name = J.S(x, "name") }); games = list; } catch (WebException e) { Tip(Api.ErrorMessage(e), ToolTipIcon.Warning); return; } catch { games = new List(); } } DialogResult r; using (var f = new LfgCreateForm(this, games)) r = f.ShowDialog(main.Visible ? main : null); if (r == DialogResult.OK) { Toast.Show("Oglas je objavljen", "Vidi se u aplikaciji i na webu u rubrici LFG.", null, null, "info", null); PollLfg(); main.ShowLfg(); } } public async Task CreateLfg(string game, int slots, int minutes, string note) { try { var body = new Dictionary { { "game", game }, { "slots", slots }, { "minutes", minutes }, { "note", note } }; var t = token; await Task.Run(() => Api.PostJ("/client/lfg", body, t)); return null; } catch (WebException e) { return Api.ErrorMessage(e); } catch (Exception e) { return e.Message; } } // odjava s tudeg oglasa ili zatvaranje svog public async void LfgAction(long id, bool close) { if (close && !Dialog.Confirm(main, "Zatvoriti oglas?", "Oglas će nestati iz aplikacije i s weba.", "Zatvori oglas", true)) return; try { var t = token; await Task.Run(() => Api.PostJ("/client/lfg/" + id + (close ? "/close" : "/leave"), new Dictionary(), t)); Tip(close ? "Oglas je zatvoren." : "Odjavio si se s oglasa."); PollLfg(); } catch (WebException e) { Tip(Api.ErrorMessage(e), ToolTipIcon.Warning); } catch { } } public async void JoinLfg(long id) { try { var t = token; var res = await Task.Run(() => Api.PostJ("/client/lfg/" + id + "/join", new Dictionary(), t)); Tip(Api.Field(res, "message") ?? "Javio si se na oglas."); PollLfg(); } catch (WebException e) { Tip(Api.ErrorMessage(e), ToolTipIcon.Warning); } catch { } } bool ChatActive(string u) { return main.ChatActive(u); } public bool IsFriend(string name) { return friends.Any(f => f.Username == name); } public void OpenChatByName(string name) { var f = friends.FirstOrDefault(x => x.Username == name); if (f != null) OpenChat(f); } public void OpenChat(Friend f) { if (f == null) return; ShowMain(); main.OpenChat(f); } // ---------- dodavanje prijatelja ---------- public async Task> AddFriend(string name) { try { var t = token; var res = await Task.Run(() => Api.PostJ("/client/friends", new Dictionary { { "username", name } }, t)); PollState(); return Tuple.Create(true, Api.Field(res, "message") ?? "Zahtjev je poslan."); } catch (WebException e) { return Tuple.Create(false, Api.ErrorMessage(e)); } catch (Exception e) { return Tuple.Create(false, e.Message); } } public async void AcceptRequest(FriendReq r) { try { var t = token; await Task.Run(() => Api.PostJ("/client/friends/" + r.Id + "/accept", new Dictionary(), t)); Tip(r.Username + " je sada tvoj prijatelj."); } catch (WebException e) { Tip(Api.ErrorMessage(e), ToolTipIcon.Warning); } catch { } PollState(); } public async void DeleteRequest(FriendReq r) { try { var t = token; await Task.Run(() => ApiDelete("/client/requests/" + r.Id, t)); } catch (WebException e) { Tip(Api.ErrorMessage(e), ToolTipIcon.Warning); } catch { } PollState(); } public async void RemoveFriend(string name) { if (!Dialog.Confirm(main, "Ukloni prijatelja", "Jesi li siguran da želiš ukloniti " + name + " iz prijatelja? Morat ćete se ponovno dodati da biste se dopisivali.", "Ukloni prijatelja", true)) return; try { var t = token; await Task.Run(() => ApiDelete("/client/friends/" + Uri.EscapeDataString(name), t)); main.CloseChat(name); } catch (WebException e) { Tip(Api.ErrorMessage(e), ToolTipIcon.Warning); } catch { } PollState(); } public void ShowAddFriend() { if (token != null) main.ShowAddFriend(); } public void ShowServers() { main.ShowServers(); } public string CurrentGame { get { return lastGame; } } // ---------- izravni pozivi ---------- public void CallByName(string name) { var f = friends.FirstOrDefault(x => x.Username == name); if (f != null) Call(f); } public async void Call(Friend f) { if (token == null || f == null) return; if (!voiceEnabled) { Tip(voiceServer ? "Voice je trenutno isključen." : "Voice server trenutno ne radi.", ToolTipIcon.Warning); return; } if (callOutTo != null) { Tip("Već nekoga zoveš.", ToolTipIcon.Warning); return; } try { var t = token; var res = await Task.Run(() => Api.PostJ("/client/call", new Dictionary { { "username", f.Username } }, t)); var room = new VRoom { Key = Api.Field(res, "room"), Label = Api.Field(res, "label") }; await JoinVoiceTask(room); if (voice == null || voice.Room != room.Key) { EndCallOnServer(); return; } callOutTo = f.Username; callOutAccepted = false; Ding.Ring(false); main.ShowVoice(); Tip("Zovem " + f.Username + "..."); } catch (WebException e) { Tip(Api.ErrorMessage(e), ToolTipIcon.Warning); } catch (Exception e) { Tip(e.Message, ToolTipIcon.Warning); } } void EndCallOnServer() { var t = token; if (t == null) return; Task.Run(() => { try { ApiDelete("/client/call", t); } catch { } }); } void CloseIncoming() { Ding.StopRing(); var f = incomingForm; incomingForm = null; if (f != null && !f.IsDisposed) { f.Answered = true; f.Close(); } } public async void AnswerCall(bool accept) { CloseIncoming(); try { var t = token; var res = await Task.Run(() => Api.PostJ("/client/call/" + (accept ? "accept" : "decline"), new Dictionary(), t)); if (!accept) return; ShowMain(); main.ShowVoice(); await JoinVoiceTask(new VRoom { Key = Api.Field(res, "room"), Label = Api.Field(res, "label") }); } catch (WebException e) { if (accept) Tip(Api.ErrorMessage(e), ToolTipIcon.Warning); } catch { } } // Svakih 2,5 s: ima li dolaznog poziva i sto je s mojim odlaznim pozivom async void PollCall() { if (callPolling || token == null) return; callPolling = true; try { var t = token; var o = J.Parse(await Task.Run(() => Api.Get("/client/call", t))); if (o == null) return; var inc = J.D(o, "incoming"); if (inc != null) { bool alreadyIn = voice != null && voice.Room == J.S(inc, "room"); if (!alreadyIn && (incomingForm == null || incomingForm.IsDisposed)) { incomingFrom = J.S(inc, "from"); incomingColor = J.S(inc, "color"); incomingAvatar = J.S(inc, "avatar"); incomingForm = new IncomingCallForm(this, incomingFrom, J.S(inc, "color"), J.S(inc, "avatar")); incomingForm.Show(); Native.Flash(incomingForm); if (!Dnd) Ding.Ring(true); } } else if (incomingForm != null && !incomingForm.IsDisposed && !incomingForm.Answered) { CloseIncoming(); var who = incomingFrom; Toast.Show(who, "Propušten poziv. Klikni da mu napišeš poruku.", incomingAvatar, incomingColor, "call", () => OpenChatByName(who)); } if (callOutTo != null) { var outc = J.D(o, "outgoing"); var st = outc == null ? "gone" : J.S(outc, "state"); if (st == "accepted" && !callOutAccepted) { callOutAccepted = true; Ding.StopRing(); Tip(callOutTo + " se javio."); } else if (!callOutAccepted && st != "ringing" && st != "accepted") { var who = callOutTo; callOutTo = null; EndCallOnServer(); LeaveVoice(); Tip(st == "declined" ? who + " je odbio poziv." : who + " se ne javlja."); } } // poziv zavrsava kad druga osoba izade if (voice != null && voice.Room != null && voice.Room.StartsWith("call:")) { int n = voice.LiveMembers().Count; if (n > 0) callHadPeer = true; else if (callHadPeer) { if (callOutTo != null) { callOutTo = null; EndCallOnServer(); } LeaveVoice(); Tip("Poziv je završio."); } } } catch { } finally { callPolling = false; } } // ---------- profilna slika ---------- public async void ChangeAvatar() { if (token == null) return; string file; using (var d = new OpenFileDialog { Title = "Odaberi profilnu sliku", Filter = "Slike|*.png;*.jpg;*.jpeg;*.gif;*.webp;*.bmp;*.avif;*.heic|Sve datoteke|*.*" }) { if (d.ShowDialog(main.Visible ? main : null) != DialogResult.OK) return; file = d.FileName; } try { if (new FileInfo(file).Length > 15L * 1024 * 1024) { Tip("Slika je veća od 15 MB.", ToolTipIcon.Warning); return; } var t = token; var res = await Task.Run(() => Api.UploadTo("/client/avatar", t, file)); avatar = Api.Field(res, "avatar"); ApplyUser(); Tip("Profilna slika je promijenjena. Ista je i na webu."); PollState(); } catch (WebException e) { Tip(Api.ErrorMessage(e), ToolTipIcon.Warning); } catch (Exception e) { Tip("Slika nije poslana: " + e.Message, ToolTipIcon.Warning); } } public async void RemoveAvatar() { if (token == null || avatar == null) return; try { var t = token; await Task.Run(() => ApiDelete("/client/avatar", t)); avatar = null; ApplyUser(); Tip("Profilna slika je maknuta."); } catch (WebException e) { Tip(Api.ErrorMessage(e), ToolTipIcon.Warning); } catch { } } // ---------- nova verzija aplikacije ---------- static Version Norm(Version v) { return new Version(v.Major, Math.Max(0, v.Minor), Math.Max(0, v.Build)); } void CheckUpdate(string ver, string url, string sha, string page) { Version v; if (ver == null || !Version.TryParse(ver.Contains(".") ? ver : ver + ".0", out v) || Norm(v) <= Current) return; updateVer = ver; updateUrl = url; updateSha = sha; updatePage = page; if (updateShown) return; updateShown = true; main.ShowUpdate("Dostupna je nova verzija Fragline " + ver + " (imaš " + Current.ToString(2) + ")."); Tip("Dostupna je nova verzija Fragline " + ver + ". Klikni \"Ažuriraj\" na vrhu prozora."); } // Preuzme novu verziju, provjeri je i zamijeni ovu datoteku, pa se ponovno pokrene public async void DoUpdate() { if (updating || updateUrl == null) { if (updatePage != null) Open("/preuzmi"); return; } updating = true; string tmp = Path.Combine(Path.GetTempPath(), "Fragline-" + updateVer + ".exe"); try { main.SetUpdateText("Preuzimam novu verziju... 0%", true); using (var wc = new WebClient()) { wc.DownloadProgressChanged += (s, e) => { try { main.BeginInvoke((Action)(() => main.SetUpdateText("Preuzimam novu verziju... " + e.ProgressPercentage + "%", true))); } catch { } }; await wc.DownloadFileTaskAsync(new Uri(updateUrl), tmp); } if (updateSha != null) { string got; using (var sha = SHA256.Create()) using (var fs = File.OpenRead(tmp)) got = BitConverter.ToString(sha.ComputeHash(fs)).Replace("-", "").ToLowerInvariant(); if (got != updateSha.ToLowerInvariant()) throw new IOException("Preuzeta datoteka je oštećena. Pokušaj ponovno."); } var exe = Application.ExecutablePath; var old = exe + ".old"; try { if (File.Exists(old)) File.Delete(old); File.Move(exe, old); File.Copy(tmp, exe); } catch (Exception) { try { if (!File.Exists(exe) && File.Exists(old)) File.Move(old, exe); } catch { } main.SetUpdateText("Ne mogu sam zamijeniti aplikaciju (nema dozvole). Preuzmi novu verziju ručno.", false); Open("/preuzmi"); return; } try { File.Delete(tmp); } catch { } Process.Start(exe, "--restarted"); Quit(); } catch (Exception e) { main.SetUpdateText("Ažuriranje nije uspjelo: " + (e is WebException ? Api.ErrorMessage((WebException)e) : e.Message), false); } finally { updating = false; } } // ---------- klanovi ---------- public void ShowClanForm(bool browse) { if (token == null) return; using (var f = new ClanForm(this, browse)) f.ShowDialog(main.Visible ? main : null); PollState(); } public async Task CreateClan(string name, string tag, string color, string desc) { try { var t = token; var body = new Dictionary { { "name", name }, { "tag", tag }, { "color", color }, { "description", desc } }; var res = await Task.Run(() => Api.PostJ("/client/clans", body, t)); Tip(Api.Field(res, "message") ?? "Klan je osnovan."); return null; } catch (WebException e) { return Api.ErrorMessage(e); } catch (Exception e) { return e.Message; } } public async Task JoinClan(long id, string name) { try { var t = token; await Task.Run(() => Api.PostJ("/client/clans/" + id + "/join", new Dictionary(), t)); PollState(); return null; } catch (WebException e) { return Api.ErrorMessage(e); } catch (Exception e) { return e.Message; } } public async void LeaveClan(ClanItem c) { bool del = c.Owner; var ok = del ? Dialog.Confirm(main, "Obriši klan", "Obrisati klan " + c.Name + "? Ovo se ne može poništiti, svi članovi ga gube.", "Obriši klan", true) : Dialog.Confirm(main, "Napusti klan", "Napustiti klan " + c.Name + "? Uvijek mu se možeš ponovno pridružiti.", "Napusti", true); if (!ok) return; try { var t = token; var res = await Task.Run(() => del ? ApiDelete("/client/clans/" + c.Id, t) : Api.PostJ("/client/clans/" + c.Id + "/leave", new Dictionary(), t)); Tip(Api.Field(res, "message") ?? "Gotovo."); } catch (WebException e) { Tip(Api.ErrorMessage(e), ToolTipIcon.Warning); } catch { } PollState(); } public void ShowCreateRoom() { if (token == null) return; if (!voiceEnabled) { Tip(voiceServer ? "Voice je trenutno isključen." : "Voice server trenutno ne radi.", ToolTipIcon.Warning); return; } lastCreated = null; DialogResult r; using (var f = new CreateRoomForm(this)) r = f.ShowDialog(main.Visible ? main : null); if (r == DialogResult.OK && lastCreated != null) { PollState(); JoinVoice(lastCreated); } } public async Task CreateRoom(string name, string privacy, long clanId, int limit) { try { var body = new Dictionary { { "name", name }, { "privacy", privacy }, { "limit", limit } }; if (clanId > 0) body["clan_id"] = clanId; var t = token; var res = await Task.Run(() => Api.PostJ("/client/voice/rooms", body, t)); var o = J.Parse(res); lastCreated = new VRoom { Key = J.S(o, "key"), Label = J.S(o, "label"), Custom = true, Mine = true }; return null; } catch (WebException e) { return Api.ErrorMessage(e); } catch (Exception e) { return e.Message; } } public async void DeleteRoom(VRoom room) { if (room == null || room.Key == null || !room.Key.StartsWith("r:")) return; try { var t = token; await Task.Run(() => ApiDelete("/client/voice/rooms/" + room.Key.Substring(2), t)); Tip("Soba " + room.Label + " je obrisana."); PollState(); } catch (WebException e) { Tip(Api.ErrorMessage(e), ToolTipIcon.Warning); } catch (Exception e) { Tip(e.Message, ToolTipIcon.Warning); } } // ---------- voice ---------- public async void JoinVoice(VRoom room) { await JoinVoiceTask(room); } async Task JoinVoiceTask(VRoom room) { if (token == null || joining || room == null) return; if (!voiceEnabled) { main.Voice.SetStatus(voiceServer ? "Voice je trenutno isključen." : "Voice server trenutno ne radi."); return; } joining = true; LeaveVoice(); main.Voice.SetStatus("Spajam se u " + room.Label + "..."); try { var t = token; var res = await Task.Run(() => Api.PostJ("/client/voice/join", new Dictionary { { "room", room.Key } }, t)); var o = J.Parse(res); var vc = new VoiceClient { Muted = Prefs.Bool("muted", false), Room = J.S(o, "room"), Label = J.S(o, "label") }; string host = J.S(o, "host"), ticket = J.S(o, "ticket"), key = J.S(o, "key"); int port = (int)J.L(o, "port"); string err = null; bool ok = await Task.Run(() => vc.Connect(host, port, ticket, key, out err)); if (!ok) { vc.Dispose(); main.Voice.SetStatus(err ?? "Spajanje nije uspjelo."); return; } voice = vc; vc.State += (txt, connected) => { try { main.BeginInvoke((Action)(() => OnVoiceState(vc, txt, connected))); } catch { } }; main.Voice.Attach(vc); if (vc.Warning != null) Tip(vc.Warning, ToolTipIcon.Warning); else Tip("Spojen si u " + vc.Label + "." + (vc.Muted ? " Mikrofon ti je isključen." : "")); } catch (WebException e) { main.Voice.SetStatus(Api.ErrorMessage(e)); } catch (Exception e) { main.Voice.SetStatus("Greška: " + e.Message); } finally { joining = false; } } void OnVoiceState(VoiceClient vc, string txt, bool connected) { if (vc != voice || connected) return; LeaveVoice(); main.Voice.SetStatus(txt); Tip(txt, ToolTipIcon.Warning); } public void LeaveVoice() { // izlazak dok jos zvoni = otkazani poziv if (callOutTo != null) { callOutTo = null; EndCallOnServer(); } Ding.StopRing(); callHadPeer = false; var vc = voice; voice = null; if (vc != null) { try { vc.Leave(); } catch { } vc.Dispose(); } main.Voice.Attach(null); } public void ToggleMute() { // izvan sobe se samo zapamti, pa ulaziš u sobu s isključenim mikrofonom if (voice == null) { Prefs.Set("muted", !Prefs.Bool("muted", false)); return; } voice.SetMuted(!voice.Muted); Prefs.Set("muted", voice.Muted); } public void ToggleDeaf() { if (voice == null) { Tip("Nisi u voice sobi."); return; } voice.SetDeafened(!voice.Deafened); } public void ShowAudioSettings() { if (audioForm != null && !audioForm.IsDisposed) { audioForm.Activate(); return; } audioForm = new AudioSettingsForm(this); audioForm.Show(); } public void AudioChanged(bool devices) { if (voice == null) return; if (devices) voice.RestartAudio(); else if (voice.Audio != null) voice.Audio.ApplyVolume(); } public void AudioBitrateChanged() { if (voice != null) voice.ApplyBitrate(); } public void UserVolumeChanged() { if (voice != null) voice.RefreshVolumes(); } } } // ===================== VoiceCrypto.cs ===================== using System; using System.Security.Cryptography; namespace Fragline { // AES-128-CTR + HMAC-SHA256 (8 bajtova taga). Paket: [tip:1][sid:4][brojac:4][sifrirano][tag:8] public sealed class VoiceCrypto { readonly ICryptoTransform ecb; readonly HMACSHA256 hmac; readonly object lk = new object(); public VoiceCrypto(byte[] key32) { var ek = new byte[16]; var mk = new byte[16]; Buffer.BlockCopy(key32, 0, ek, 0, 16); Buffer.BlockCopy(key32, 16, mk, 0, 16); var aes = Aes.Create(); aes.Mode = CipherMode.ECB; aes.Padding = PaddingMode.None; aes.Key = ek; ecb = aes.CreateEncryptor(); hmac = new HMACSHA256(mk); } public static void W32(byte[] b, int o, uint v) { b[o] = (byte)v; b[o + 1] = (byte)(v >> 8); b[o + 2] = (byte)(v >> 16); b[o + 3] = (byte)(v >> 24); } public static uint R32(byte[] b, int o) { return (uint)(b[o] | (b[o + 1] << 8) | (b[o + 2] << 16) | (b[o + 3] << 24)); } void Xor(uint sid, uint ctr, byte dir, byte[] src, int so, int len, byte[] dst, int dof) { var blk = new byte[16]; var ks = new byte[16]; W32(blk, 0, sid); W32(blk, 4, ctr); blk[8] = dir; int n = 0; for (int i = 0; i < len; i += 16, n++) { W32(blk, 12, (uint)n); ecb.TransformBlock(blk, 0, 16, ks, 0); int m = Math.Min(16, len - i); for (int j = 0; j < m; j++) dst[dof + i + j] = (byte)(src[so + i + j] ^ ks[j]); } } public byte[] Seal(byte type, uint sid, uint ctr, byte dir, byte[] p, int off, int len) { lock (lk) { var pk = new byte[9 + len + 8]; pk[0] = type; W32(pk, 1, sid); W32(pk, 5, ctr); Xor(sid, ctr, dir, p, off, len, pk, 9); var tag = hmac.ComputeHash(pk, 0, 9 + len); Buffer.BlockCopy(tag, 0, pk, 9 + len, 8); return pk; } } public byte[] Open(byte[] pk, int len, byte dir) { if (len < 17) return null; lock (lk) { var tag = hmac.ComputeHash(pk, 0, len - 8); int diff = 0; for (int i = 0; i < 8; i++) diff |= tag[i] ^ pk[len - 8 + i]; if (diff != 0) return null; uint sid = R32(pk, 1), ctr = R32(pk, 5); var outp = new byte[len - 17]; Xor(sid, ctr, dir, pk, 9, len - 17, outp, 0); return outp; } } } }