|
using System; |
|
using System.Collections.Generic; |
|
using System.Drawing; |
|
using System.Globalization; |
|
using System.Linq; |
|
using System.Net; |
|
using System.Text; |
|
using System.Text.RegularExpressions; |
|
using System.Threading; |
|
using System.Windows.Forms; |
|
|
|
namespace CosmicTracker |
|
{ |
|
internal sealed class ReportItem |
|
{ |
|
public int Rank { get; set; } |
|
public string Source { get; set; } |
|
public string Group { get; set; } |
|
public string World { get; set; } |
|
public int Stage { get; set; } |
|
public double Progress { get; set; } |
|
public int ProgressRank { get; set; } |
|
public string ProgressText { get; set; } |
|
public string Status { get; set; } |
|
public string Updated { get; set; } |
|
} |
|
|
|
internal sealed class Site |
|
{ |
|
public string Name { get; set; } |
|
public string Url { get; set; } |
|
public string Parser { get; set; } |
|
} |
|
|
|
internal sealed class GradeInfo |
|
{ |
|
public int Stage { get; set; } |
|
public string Status { get; set; } |
|
} |
|
|
|
internal sealed class ReportResult |
|
{ |
|
public List<ReportItem> Items { get; set; } |
|
public List<string> Errors { get; set; } |
|
public DateTime RetrievedAt { get; set; } |
|
} |
|
|
|
internal static class Program |
|
{ |
|
private static readonly Site[] Sites = |
|
{ |
|
new Site { Name = "한국", Url = "https://guide.ff14.co.kr/cosmic_exploration/report", Parser = "Korean" }, |
|
new Site { Name = "글로벌", Url = "https://na.finalfantasyxiv.com/lodestone/cosmic_exploration/report/", Parser = "Lodestone" }, |
|
new Site { Name = "중국", Url = "https://ff14act.web.sdo.com/api/cosmicData/getCosmicData", Parser = "China" } |
|
}; |
|
|
|
private static readonly Dictionary<string, double> GaugeProgress = new Dictionary<string, double> |
|
{ |
|
{ "0", 0.0 }, |
|
{ "1", 12.5 }, |
|
{ "2", 25.0 }, |
|
{ "3", 37.5 }, |
|
{ "4", 50.0 }, |
|
{ "5", 62.5 }, |
|
{ "6", 75.0 }, |
|
{ "7", 87.5 }, |
|
{ "max", 100.0 } |
|
}; |
|
|
|
private static readonly Dictionary<int, GradeInfo> ChinaAuxesiaDevelopmentGrades = new Dictionary<int, GradeInfo> |
|
{ |
|
{ 192, new GradeInfo { Stage = 1, Status = "居住舱安装工程支援行动 物资准备中" } }, |
|
{ 193, new GradeInfo { Stage = 1, Status = "居住舱安装工程支援行动 联合工程准备中" } }, |
|
{ 194, new GradeInfo { Stage = 1, Status = "居住舱安装工程支援行动 联合工程进行中" } }, |
|
{ 195, new GradeInfo { Stage = 2, Status = "居住舱 竣工" } }, |
|
{ 196, new GradeInfo { Stage = 3, Status = "第一次宇宙飞线铺设工程 物资准备中" } }, |
|
{ 197, new GradeInfo { Stage = 3, Status = "第一次宇宙飞线铺设工程 施工中" } }, |
|
{ 198, new GradeInfo { Stage = 4, Status = "宇宙飞线都心段 竣工" } }, |
|
{ 199, new GradeInfo { Stage = 5, Status = "能源设施建设工程支援行动 物资准备中" } }, |
|
{ 200, new GradeInfo { Stage = 5, Status = "能源设施建设工程支援行动 联合工程(共2期第1期)准备中" } }, |
|
{ 201, new GradeInfo { Stage = 5, Status = "能源设施建设工程支援行动 联合工程(共2期第1期)进行中" } }, |
|
{ 202, new GradeInfo { Stage = 6, Status = "供水设施建设工程支援行动 联合工程(共2期第2期)准备中" } }, |
|
{ 203, new GradeInfo { Stage = 6, Status = "供水设施建设工程支援行动 联合工程(共2期第2期)进行中" } }, |
|
{ 204, new GradeInfo { Stage = 7, Status = "发电站和供水站 竣工" } }, |
|
{ 205, new GradeInfo { Stage = 8, Status = "子叶塔修复计划 物资准备中" } }, |
|
{ 206, new GradeInfo { Stage = 8, Status = "子叶塔修复计划 联合工程准备中" } }, |
|
{ 207, new GradeInfo { Stage = 8, Status = "子叶塔修复计划 联合工程进行中" } }, |
|
{ 208, new GradeInfo { Stage = 9, Status = "子叶塔 修复完成" } }, |
|
{ 209, new GradeInfo { Stage = 10, Status = "原木屋基地扩建工程 物资准备中" } }, |
|
{ 210, new GradeInfo { Stage = 10, Status = "原木屋基地扩建工程 联合工程准备中" } }, |
|
{ 211, new GradeInfo { Stage = 10, Status = "原木屋基地扩建工程 联合工程进行中" } }, |
|
{ 212, new GradeInfo { Stage = 11, Status = "原木屋基地扩建工程 竣工" } }, |
|
{ 213, new GradeInfo { Stage = 12, Status = "常叶塔开路支援行动 物资准备中" } }, |
|
{ 214, new GradeInfo { Stage = 12, Status = "常叶塔开路支援行动 联合工程准备中" } }, |
|
{ 215, new GradeInfo { Stage = 12, Status = "常叶塔开路支援行动 联合工程进行中" } }, |
|
{ 216, new GradeInfo { Stage = 13, Status = "常叶塔 开路竣工" } }, |
|
{ 217, new GradeInfo { Stage = 14, Status = "常叶塔修复计划 物资准备中" } }, |
|
{ 218, new GradeInfo { Stage = 14, Status = "常叶塔修复计划 联合工程准备中" } }, |
|
{ 219, new GradeInfo { Stage = 14, Status = "常叶塔修复计划 联合工程进行中" } }, |
|
{ 220, new GradeInfo { Stage = 15, Status = "常叶塔 修复完成" } }, |
|
{ 221, new GradeInfo { Stage = 15, Status = "第二次宇宙飞线铺设工程 物资收集中" } }, |
|
{ 222, new GradeInfo { Stage = 15, Status = "第二次宇宙飞线铺设工程 施工中" } }, |
|
{ 223, new GradeInfo { Stage = 16, Status = "树晶堆栈线 竣工" } }, |
|
{ 224, new GradeInfo { Stage = 17, Status = "宇宙港建设工程支援行动 物资准备中" } }, |
|
{ 225, new GradeInfo { Stage = 17, Status = "宇宙港建设工程支援行动 联合工程准备中" } }, |
|
{ 226, new GradeInfo { Stage = 17, Status = "宇宙港建设工程支援行动 联合工程进行中" } }, |
|
{ 227, new GradeInfo { Stage = 18, Status = "宇宙港 竣工" } }, |
|
{ 228, new GradeInfo { Stage = 19, Status = "末叶塔开路支援行动 物资准备中" } }, |
|
{ 229, new GradeInfo { Stage = 19, Status = "末叶塔开路支援行动 联合工程准备中" } }, |
|
{ 230, new GradeInfo { Stage = 19, Status = "末叶塔开路支援行动 联合工程进行中" } }, |
|
{ 231, new GradeInfo { Stage = 20, Status = "末叶塔 开路竣工" } }, |
|
{ 232, new GradeInfo { Stage = 21, Status = "末叶塔修复计划 物资准备中" } }, |
|
{ 233, new GradeInfo { Stage = 21, Status = "末叶塔修复计划 联合工程准备中" } }, |
|
{ 234, new GradeInfo { Stage = 21, Status = "末叶塔修复计划 联合工程进行中" } }, |
|
{ 235, new GradeInfo { Stage = 22, Status = "末叶塔 修复完成" } }, |
|
{ 236, new GradeInfo { Stage = 22, Status = "第三次宇宙飞线铺设工程 物资收集中" } }, |
|
{ 237, new GradeInfo { Stage = 22, Status = "第三次宇宙飞线铺设工程 施工中" } }, |
|
{ 238, new GradeInfo { Stage = 23, Status = "苍石林班线 竣工" } }, |
|
{ 239, new GradeInfo { Stage = 24, Status = "机甲库扩建工程 物资准备中" } }, |
|
{ 240, new GradeInfo { Stage = 24, Status = "机甲库扩建工程 联合工程准备中" } }, |
|
{ 241, new GradeInfo { Stage = 24, Status = "机甲库扩建工程 联合工程进行中" } }, |
|
{ 242, new GradeInfo { Stage = 25, Status = "机甲库 扩建完成" } }, |
|
{ 243, new GradeInfo { Stage = 26, Status = "核心林区环境整备支援行动 物资收集中" } }, |
|
{ 244, new GradeInfo { Stage = 26, Status = "核心林区环境整备支援行动 联合工程准备中" } }, |
|
{ 245, new GradeInfo { Stage = 27, Status = "核心林区环境整备支援行动 联合工程进行中" } }, |
|
{ 246, new GradeInfo { Stage = 28, Status = "核心林区上层 环境整备完成" } }, |
|
{ 247, new GradeInfo { Stage = 29, Status = "太母神桂重启作战 物资收集中" } }, |
|
{ 248, new GradeInfo { Stage = 29, Status = "太母神桂重启作战 联合工程准备中" } }, |
|
{ 249, new GradeInfo { Stage = 29, Status = "太母神桂重启作战 联合工程进行中" } }, |
|
{ 250, new GradeInfo { Stage = 30, Status = "太母神桂 重启完成" } }, |
|
{ 251, new GradeInfo { Stage = 30, Status = "奥克塞西亚行星探索计划 圆满完成" } } |
|
}; |
|
|
|
[STAThread] |
|
private static void Main(string[] args) |
|
{ |
|
if (args.Any(arg => string.Equals(arg, "--test", StringComparison.OrdinalIgnoreCase))) |
|
{ |
|
ReportResult result = GetAuxesiaReport(); |
|
var lines = new List<string> |
|
{ |
|
"Items=" + result.Items.Count.ToString(CultureInfo.InvariantCulture), |
|
"Errors=" + result.Errors.Count.ToString(CultureInfo.InvariantCulture) |
|
}; |
|
|
|
lines.AddRange(result.Items.Take(10).Select(item => string.Format( |
|
CultureInfo.InvariantCulture, |
|
"{0}\t{1}\t{2}\t{3}\t{4}", |
|
item.Rank, |
|
item.Group, |
|
item.World, |
|
item.Stage, |
|
item.ProgressText))); |
|
lines.AddRange(result.Errors.Select(error => "Error=" + error)); |
|
System.IO.File.WriteAllLines("CosmicTracker.test.txt", lines.ToArray(), Encoding.UTF8); |
|
return; |
|
} |
|
|
|
Application.EnableVisualStyles(); |
|
Application.SetCompatibleTextRenderingDefault(false); |
|
Application.Run(new TrackerForm()); |
|
} |
|
|
|
internal static ReportResult GetAuxesiaReport() |
|
{ |
|
var items = new List<ReportItem>(); |
|
var errors = new List<string>(); |
|
|
|
foreach (var site in Sites) |
|
{ |
|
try |
|
{ |
|
string content = GetUrlText(site.Url); |
|
IEnumerable<ReportItem> parsed; |
|
|
|
if (site.Parser == "Korean") |
|
{ |
|
parsed = ParseKoreanReport(content); |
|
} |
|
else if (site.Parser == "China") |
|
{ |
|
parsed = ParseChinaReport(content); |
|
} |
|
else |
|
{ |
|
parsed = ParseLodestoneReport(content); |
|
} |
|
|
|
items.AddRange(parsed); |
|
} |
|
catch (Exception ex) |
|
{ |
|
errors.Add(site.Name + ": " + ex.Message); |
|
} |
|
} |
|
|
|
return new ReportResult |
|
{ |
|
Items = SetReportRanks(items), |
|
Errors = errors, |
|
RetrievedAt = DateTime.Now |
|
}; |
|
} |
|
|
|
private static string GetUrlText(string url) |
|
{ |
|
ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072; |
|
|
|
using (var client = new WebClient()) |
|
{ |
|
client.Encoding = Encoding.UTF8; |
|
client.Headers[HttpRequestHeader.UserAgent] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) CosmicTracker/1.0"; |
|
return client.DownloadString(url); |
|
} |
|
} |
|
|
|
private static List<ReportItem> ParseKoreanReport(string html) |
|
{ |
|
var items = new List<ReportItem>(); |
|
string updated = ""; |
|
Match updatedMatch = Regex.Match(html, "<span\\s+class=\"txt_update\">\\s*(?<updated>.*?)\\s*</span>", RegexOptions.Singleline | RegexOptions.IgnoreCase); |
|
|
|
if (updatedMatch.Success) |
|
{ |
|
updated = CleanHtml(updatedMatch.Groups["updated"].Value); |
|
} |
|
|
|
foreach (Match cardMatch in Regex.Matches(html, "<li\\b[^>]*>\\s*<dl>(?<card>.*?)</dl>\\s*</li>", RegexOptions.Singleline | RegexOptions.IgnoreCase)) |
|
{ |
|
string card = cardMatch.Groups["card"].Value; |
|
Match worldMatch = Regex.Match(card, "<dt\\s+class=\"title\">\\s*(?<world>.*?)\\s*</dt>", RegexOptions.Singleline | RegexOptions.IgnoreCase); |
|
Match stageMatch = Regex.Match(card, "<div\\s+class=\"level\">\\s*<strong>\\s*(?<stage>\\d+)\\s*</strong>", RegexOptions.Singleline | RegexOptions.IgnoreCase); |
|
|
|
if (!worldMatch.Success || !stageMatch.Success) |
|
{ |
|
continue; |
|
} |
|
|
|
double progress = 0.0; |
|
Match progressMatch = Regex.Match(card, "<div\\s+class=\"gauge\"\\s+style=\"width:\\s*(?<progress>[\\d.]+)%", RegexOptions.Singleline | RegexOptions.IgnoreCase); |
|
if (progressMatch.Success) |
|
{ |
|
progress = ParseDouble(progressMatch.Groups["progress"].Value); |
|
} |
|
|
|
string status = ""; |
|
Match statusMatch = Regex.Match(card, "<dd>.*?<p>\\s*(?<status>.*?)\\s*</p>", RegexOptions.Singleline | RegexOptions.IgnoreCase); |
|
if (statusMatch.Success) |
|
{ |
|
status = CleanHtml(statusMatch.Groups["status"].Value); |
|
} |
|
|
|
items.Add(NewReportItem("한국", "한국", CleanHtml(worldMatch.Groups["world"].Value), ParseInt(stageMatch.Groups["stage"].Value), progress, status, updated)); |
|
} |
|
|
|
return items; |
|
} |
|
|
|
private static List<ReportItem> ParseLodestoneReport(string html) |
|
{ |
|
var items = new List<ReportItem>(); |
|
string updated = ""; |
|
Match updatedMatch = Regex.Match(html, "ldst_strftime\\((?<seconds>\\d+),\\s*'YMDHM'\\)"); |
|
|
|
if (updatedMatch.Success) |
|
{ |
|
updated = UnixSecondsToLocalText(long.Parse(updatedMatch.Groups["seconds"].Value, CultureInfo.InvariantCulture)); |
|
} |
|
|
|
MatchCollection markers = Regex.Matches(html, "<h3\\s+class=\"cosmic__report__dc__title\">\\s*(?<dc>.*?)\\s*</h3>|<div\\s+class=\"cosmic__report__card\\b[^\"]*\"", RegexOptions.Singleline | RegexOptions.IgnoreCase); |
|
string currentDc = "글로벌"; |
|
|
|
for (int i = 0; i < markers.Count; i++) |
|
{ |
|
Match marker = markers[i]; |
|
if (marker.Groups["dc"].Success) |
|
{ |
|
currentDc = CleanHtml(marker.Groups["dc"].Value); |
|
continue; |
|
} |
|
|
|
int start = marker.Index; |
|
int end = i + 1 < markers.Count ? markers[i + 1].Index : html.Length; |
|
string segment = html.Substring(start, end - start); |
|
|
|
Match worldMatch = Regex.Match(segment, "cosmic__report__card__name.*?<p[^>]*>\\s*(?<world>.*?)\\s*</p>", RegexOptions.Singleline | RegexOptions.IgnoreCase); |
|
Match stageMatch = Regex.Match(segment, "cosmic__report__grade__level.*?<p>\\s*(?<stage>\\d+)\\s*</p>", RegexOptions.Singleline | RegexOptions.IgnoreCase); |
|
|
|
if (!worldMatch.Success || !stageMatch.Success) |
|
{ |
|
continue; |
|
} |
|
|
|
double progress = 0.0; |
|
Match gaugeMatch = Regex.Match(segment, "gauge-(?<gauge>max|\\d+)", RegexOptions.Singleline | RegexOptions.IgnoreCase); |
|
if (gaugeMatch.Success) |
|
{ |
|
progress = GaugeToPercent(gaugeMatch.Groups["gauge"].Value); |
|
} |
|
|
|
string status = ""; |
|
Match statusMatch = Regex.Match(segment, "<p\\s+class=\"cosmic__report__status__text\">\\s*(?<status>.*?)\\s*</p>", RegexOptions.Singleline | RegexOptions.IgnoreCase); |
|
if (!statusMatch.Success) |
|
{ |
|
statusMatch = Regex.Match(segment, "<div\\s+class=\"cosmic__report__status__completed\">\\s*(?<status>.*?)\\s*</div>", RegexOptions.Singleline | RegexOptions.IgnoreCase); |
|
} |
|
|
|
if (statusMatch.Success) |
|
{ |
|
status = CleanHtml(statusMatch.Groups["status"].Value); |
|
} |
|
|
|
items.Add(NewReportItem("글로벌", currentDc, CleanHtml(worldMatch.Groups["world"].Value), ParseInt(stageMatch.Groups["stage"].Value), progress, status, updated)); |
|
} |
|
|
|
return items; |
|
} |
|
|
|
private static List<ReportItem> ParseChinaReport(string json) |
|
{ |
|
var items = new List<ReportItem>(); |
|
|
|
if (!json.Contains("\"code\":10000")) |
|
{ |
|
throw new InvalidOperationException("중국 서버 API 응답이 예상 형식과 다릅니다."); |
|
} |
|
|
|
foreach (Match objectMatch in Regex.Matches(json, "\\{[^{}]*\"PlanetSerialId\":3[^{}]*\\}", RegexOptions.Singleline)) |
|
{ |
|
string entry = objectMatch.Value; |
|
int gradeKey = ExtractInt(entry, "DevelopmentGrade"); |
|
GradeInfo gradeInfo; |
|
|
|
if (!ChinaAuxesiaDevelopmentGrades.TryGetValue(gradeKey, out gradeInfo)) |
|
{ |
|
continue; |
|
} |
|
|
|
double progress = ExtractInt(entry, "ProgressRate") / 10.0; |
|
string areaName = ExtractString(entry, "area_name"); |
|
string groupName = ExtractString(entry, "group_name"); |
|
string updated = ExtractString(entry, "data_time"); |
|
|
|
items.Add(NewReportItem("중국", "중국/" + areaName, groupName, gradeInfo.Stage, progress, gradeInfo.Status, updated)); |
|
} |
|
|
|
return items; |
|
} |
|
|
|
private static ReportItem NewReportItem(string source, string group, string world, int stage, double progress, string status, string updated) |
|
{ |
|
int progressRank = (int)Math.Floor(progress); |
|
|
|
return new ReportItem |
|
{ |
|
Rank = 0, |
|
Source = source, |
|
Group = group, |
|
World = world, |
|
Stage = stage, |
|
Progress = progress, |
|
ProgressRank = progressRank, |
|
ProgressText = progressRank.ToString(CultureInfo.InvariantCulture) + "%", |
|
Status = status, |
|
Updated = updated |
|
}; |
|
} |
|
|
|
private static List<ReportItem> SetReportRanks(IEnumerable<ReportItem> sourceItems) |
|
{ |
|
var sorted = sourceItems |
|
.OrderByDescending(item => item.Stage) |
|
.ThenByDescending(item => item.ProgressRank) |
|
.ThenBy(item => item.Group, StringComparer.Ordinal) |
|
.ThenBy(item => item.World, StringComparer.Ordinal) |
|
.ToList(); |
|
|
|
int rank = 0; |
|
int position = 0; |
|
int? lastStage = null; |
|
int? lastProgressRank = null; |
|
|
|
foreach (ReportItem item in sorted) |
|
{ |
|
position++; |
|
|
|
if (!lastStage.HasValue || item.Stage != lastStage.Value || item.ProgressRank != lastProgressRank.Value) |
|
{ |
|
rank = position; |
|
lastStage = item.Stage; |
|
lastProgressRank = item.ProgressRank; |
|
} |
|
|
|
item.Rank = rank; |
|
} |
|
|
|
return sorted; |
|
} |
|
|
|
private static string CleanHtml(string value) |
|
{ |
|
if (string.IsNullOrWhiteSpace(value)) |
|
{ |
|
return ""; |
|
} |
|
|
|
string text = Regex.Replace(value, "<[^>]+>", " "); |
|
text = WebUtility.HtmlDecode(text); |
|
text = Regex.Replace(text, "\\s+", " "); |
|
return text.Trim(); |
|
} |
|
|
|
private static double GaugeToPercent(string gauge) |
|
{ |
|
double value; |
|
return GaugeProgress.TryGetValue(gauge, out value) ? value : 0.0; |
|
} |
|
|
|
private static int ExtractInt(string jsonObject, string name) |
|
{ |
|
Match match = Regex.Match(jsonObject, "\"" + Regex.Escape(name) + "\"\\s*:\\s*(?<value>-?\\d+)"); |
|
return match.Success ? ParseInt(match.Groups["value"].Value) : 0; |
|
} |
|
|
|
private static string ExtractString(string jsonObject, string name) |
|
{ |
|
Match match = Regex.Match(jsonObject, "\"" + Regex.Escape(name) + "\"\\s*:\\s*\"(?<value>(?:\\\\.|[^\"])*)\""); |
|
return match.Success ? Regex.Unescape(match.Groups["value"].Value) : ""; |
|
} |
|
|
|
private static int ParseInt(string value) |
|
{ |
|
int parsed; |
|
return int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out parsed) ? parsed : 0; |
|
} |
|
|
|
private static double ParseDouble(string value) |
|
{ |
|
double parsed; |
|
return double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out parsed) ? parsed : 0.0; |
|
} |
|
|
|
private static string UnixSecondsToLocalText(long seconds) |
|
{ |
|
DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); |
|
return epoch.AddSeconds(seconds).ToLocalTime().ToString("yyyy/MM/dd HH:mm", CultureInfo.InvariantCulture); |
|
} |
|
} |
|
|
|
internal sealed class TrackerForm : Form |
|
{ |
|
private readonly Label statusLabel; |
|
private readonly Label footerLabel; |
|
private readonly Button refreshButton; |
|
private readonly DataGridView grid; |
|
private readonly System.Windows.Forms.Timer refreshTimer; |
|
|
|
public TrackerForm() |
|
{ |
|
Text = "CosmicTracker"; |
|
TopMost = true; |
|
Opacity = 0.88; |
|
BackColor = Color.FromArgb(17, 24, 39); |
|
ForeColor = Color.FromArgb(248, 250, 252); |
|
Font = new Font("Malgun Gothic", 9F); |
|
Size = new Size(590, 760); |
|
MinimumSize = new Size(440, 360); |
|
StartPosition = FormStartPosition.CenterScreen; |
|
|
|
var root = new TableLayoutPanel |
|
{ |
|
Dock = DockStyle.Fill, |
|
ColumnCount = 1, |
|
RowCount = 4, |
|
Padding = new Padding(12), |
|
BackColor = Color.FromArgb(17, 24, 39) |
|
}; |
|
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 42)); |
|
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 28)); |
|
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); |
|
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 26)); |
|
Controls.Add(root); |
|
|
|
var header = new Panel { Dock = DockStyle.Fill }; |
|
var title = new Label |
|
{ |
|
Text = "아욱세시아 순위표", |
|
AutoSize = true, |
|
ForeColor = Color.FromArgb(248, 250, 252), |
|
Font = new Font("Malgun Gothic", 11F, FontStyle.Bold), |
|
Location = new Point(0, 0) |
|
}; |
|
var subtitle = new Label |
|
{ |
|
Text = "단계 높은 순, 진행도 높은 순", |
|
AutoSize = true, |
|
ForeColor = Color.FromArgb(156, 207, 232), |
|
Font = new Font("Malgun Gothic", 8F), |
|
Location = new Point(1, 24) |
|
}; |
|
refreshButton = new Button |
|
{ |
|
Text = "새로고침", |
|
Width = 82, |
|
Height = 28, |
|
Anchor = AnchorStyles.Top | AnchorStyles.Right, |
|
Location = new Point(Width - 205, 3) |
|
}; |
|
var closeButton = new Button |
|
{ |
|
Text = "닫기", |
|
Width = 54, |
|
Height = 28, |
|
Anchor = AnchorStyles.Top | AnchorStyles.Right, |
|
Location = new Point(Width - 115, 3) |
|
}; |
|
header.Resize += delegate |
|
{ |
|
refreshButton.Left = header.Width - 148; |
|
closeButton.Left = header.Width - 58; |
|
}; |
|
refreshButton.Click += delegate { RefreshData(); }; |
|
closeButton.Click += delegate { Close(); }; |
|
header.Controls.Add(title); |
|
header.Controls.Add(subtitle); |
|
header.Controls.Add(refreshButton); |
|
header.Controls.Add(closeButton); |
|
root.Controls.Add(header, 0, 0); |
|
|
|
statusLabel = new Label |
|
{ |
|
Text = "불러오는 중...", |
|
Dock = DockStyle.Fill, |
|
ForeColor = Color.FromArgb(214, 246, 255), |
|
TextAlign = ContentAlignment.MiddleLeft |
|
}; |
|
root.Controls.Add(statusLabel, 0, 1); |
|
|
|
grid = new DataGridView |
|
{ |
|
Dock = DockStyle.Fill, |
|
AllowUserToAddRows = false, |
|
AllowUserToDeleteRows = false, |
|
AllowUserToResizeRows = false, |
|
AutoGenerateColumns = false, |
|
BackgroundColor = Color.FromArgb(17, 24, 39), |
|
BorderStyle = BorderStyle.None, |
|
CellBorderStyle = DataGridViewCellBorderStyle.None, |
|
ColumnHeadersBorderStyle = DataGridViewHeaderBorderStyle.Single, |
|
EnableHeadersVisualStyles = false, |
|
ReadOnly = true, |
|
RowHeadersVisible = false, |
|
SelectionMode = DataGridViewSelectionMode.FullRowSelect |
|
}; |
|
grid.ColumnHeadersDefaultCellStyle.BackColor = Color.FromArgb(38, 61, 77); |
|
grid.ColumnHeadersDefaultCellStyle.ForeColor = Color.FromArgb(214, 246, 255); |
|
grid.ColumnHeadersDefaultCellStyle.Font = new Font("Malgun Gothic", 9F, FontStyle.Bold); |
|
grid.DefaultCellStyle.BackColor = Color.FromArgb(29, 42, 58); |
|
grid.DefaultCellStyle.ForeColor = Color.FromArgb(248, 250, 252); |
|
grid.DefaultCellStyle.SelectionBackColor = Color.FromArgb(50, 92, 120); |
|
grid.DefaultCellStyle.SelectionForeColor = Color.White; |
|
grid.AlternatingRowsDefaultCellStyle.BackColor = Color.FromArgb(24, 35, 50); |
|
grid.RowTemplate.Height = 28; |
|
grid.Columns.Add(TextColumn("순위", "Rank", 46)); |
|
grid.Columns.Add(TextColumn("구분", "Group", 104)); |
|
grid.Columns.Add(TextColumn("서버", "World", 150)); |
|
grid.Columns.Add(TextColumn("단계", "Stage", 50)); |
|
grid.Columns.Add(TextColumn("진행", "ProgressText", 60)); |
|
root.Controls.Add(grid, 0, 2); |
|
|
|
footerLabel = new Label |
|
{ |
|
Text = "", |
|
Dock = DockStyle.Fill, |
|
ForeColor = Color.FromArgb(156, 207, 232), |
|
TextAlign = ContentAlignment.MiddleLeft, |
|
AutoEllipsis = true |
|
}; |
|
root.Controls.Add(footerLabel, 0, 3); |
|
|
|
refreshTimer = new System.Windows.Forms.Timer { Interval = 5 * 60 * 1000 }; |
|
refreshTimer.Tick += delegate { RefreshData(); }; |
|
Shown += delegate |
|
{ |
|
RefreshData(); |
|
refreshTimer.Start(); |
|
}; |
|
} |
|
|
|
private static DataGridViewTextBoxColumn TextColumn(string header, string property, int width) |
|
{ |
|
return new DataGridViewTextBoxColumn |
|
{ |
|
HeaderText = header, |
|
DataPropertyName = property, |
|
Width = width, |
|
SortMode = DataGridViewColumnSortMode.NotSortable |
|
}; |
|
} |
|
|
|
private void RefreshData() |
|
{ |
|
refreshButton.Enabled = false; |
|
statusLabel.Text = "갱신 중..."; |
|
|
|
ThreadPool.QueueUserWorkItem(delegate |
|
{ |
|
ReportResult result = Program.GetAuxesiaReport(); |
|
BeginInvoke(new Action(delegate |
|
{ |
|
grid.DataSource = result.Items; |
|
statusLabel.Text = string.Format("마지막 갱신 {0} / {1}개 서버", result.RetrievedAt.ToString("HH:mm:ss"), result.Items.Count); |
|
|
|
if (result.Errors.Count > 0) |
|
{ |
|
footerLabel.Text = "일부 갱신 실패: " + string.Join(" / ", result.Errors.ToArray()); |
|
} |
|
else |
|
{ |
|
var updates = result.Items |
|
.Where(item => !string.IsNullOrWhiteSpace(item.Updated)) |
|
.GroupBy(item => item.Source) |
|
.Select(group => group.Key + ": " + group.First().Updated); |
|
footerLabel.Text = string.Join(" / ", updates.ToArray()); |
|
} |
|
|
|
refreshButton.Enabled = true; |
|
})); |
|
}); |
|
} |
|
} |
|
} |