|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Data;
|
|
using System.Data.SQLite;
|
|
using System.Diagnostics;
|
|
using System.Drawing;
|
|
using System.Drawing.Imaging;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using static AppTime.Recorder;
|
|
|
|
namespace AppTime
|
|
{
|
|
class Controller
|
|
{
|
|
public WebServer Server =>Program.server;
|
|
public Recorder Recorder=>Program.recorder;
|
|
|
|
|
|
#region basic
|
|
|
|
int getColor(string str)
|
|
{
|
|
return str.GetHashCode();
|
|
}
|
|
|
|
static Dictionary<long, int> appColors = new Dictionary<long, int>();
|
|
|
|
int getAppColor(long appId)
|
|
{
|
|
if(!appColors.TryGetValue(appId, out var color))
|
|
{
|
|
var text = db.ExecuteValue<string>($"select text from app where id={appId}");
|
|
lock (appColors)
|
|
{
|
|
color = appColors[appId] = getColor(text);
|
|
}
|
|
}
|
|
return color;
|
|
}
|
|
|
|
static Dictionary<long, int> tagColors = new Dictionary<long, int>();
|
|
int getTagColor(long tagId)
|
|
{
|
|
if (!tagColors.TryGetValue(tagId, out var color))
|
|
{
|
|
if (tagId == -1)
|
|
{
|
|
color = tagColors[tagId] = BitConverter.ToInt32(new byte[] { 0xFF, 0x99, 0x99, 0x99 }, 0);
|
|
}
|
|
else
|
|
{
|
|
var text = db.ExecuteValue<string>($"select text from tag where id={tagId}");
|
|
color = tagColors[tagId] = getColor(text);
|
|
}
|
|
}
|
|
return color;
|
|
}
|
|
|
|
|
|
public unsafe byte[] getPeriodBar(DateTime timefrom, DateTime timeto, string view, int width)
|
|
{
|
|
if (width <= 0 || width > 8000)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
|
|
var totalsecs = (timeto - timefrom).TotalSeconds;
|
|
IEnumerable<dynamic> data;
|
|
if (view == "app")
|
|
{
|
|
data = db.ExecuteDynamic(@"
|
|
select
|
|
app.id appId,
|
|
p.timeStart,
|
|
p.timeEnd
|
|
from app
|
|
join win on win.appid = app.id
|
|
join period p on p.winid = win.id
|
|
where
|
|
timeStart between @v0 and @v1
|
|
or timeEnd between @v0 and @v1
|
|
or @v0 between timeStart and timeEnd
|
|
order by p.timeStart
|
|
",
|
|
timefrom, timeto
|
|
);
|
|
}
|
|
else
|
|
{
|
|
data = db.ExecuteDynamic(@"
|
|
select
|
|
ifnull(tag.id,-1) tagId,
|
|
p.timeStart,
|
|
p.timeEnd
|
|
from app
|
|
join win on win.appid = app.id
|
|
join period p on p.winid = win.id
|
|
left join tag on tag.id = win.tagId or (win.tagId = 0 and tag.id = app.tagId)
|
|
where
|
|
timeStart between @v0 and @v1
|
|
or timeEnd between @v0 and @v1
|
|
or @v0 between timeStart and timeEnd
|
|
order by p.timeStart
|
|
",
|
|
timefrom, timeto
|
|
);
|
|
}
|
|
|
|
//绘制PeriodBar,直接写内存比gdi+快
|
|
var imgdata = new int[width];
|
|
foreach (var period in data)
|
|
{
|
|
var from = Math.Max(0, (int)Math.Round((period.timeStart - timefrom).TotalSeconds / totalsecs * width));
|
|
var to = Math.Min(width - 1, (int)Math.Round((period.timeEnd - timefrom).TotalSeconds / totalsecs * width));
|
|
|
|
for (var x = from; x <= Math.Min(width - 1, to); x++)
|
|
{
|
|
imgdata[x] = view == "app" ? (int)getAppColor(period.appId) : (int)getTagColor(period.tagId);
|
|
}
|
|
}
|
|
|
|
fixed (int* p = &imgdata[0])
|
|
{
|
|
var ptr = new IntPtr(p);
|
|
using var bmp = new Bitmap(width, 1, width * 4, PixelFormat.Format32bppArgb, ptr);
|
|
using var mem = new MemoryStream();
|
|
bmp.Save(mem, ImageFormat.Png);
|
|
return mem.ToArray();
|
|
}
|
|
|
|
}
|
|
|
|
public object getTree(DateTime timefrom, DateTime timeto, string view, long parentKey)
|
|
{
|
|
var result = new List<object>();
|
|
var totalSeconds = (timeto - timefrom).TotalSeconds;
|
|
IEnumerable<dynamic> data;
|
|
|
|
if (view == "app")
|
|
{
|
|
if (parentKey == 0)
|
|
{
|
|
data = db.ExecuteDynamic(@"
|
|
select
|
|
app.id appId,
|
|
app.text appText,
|
|
tag.text tagText,
|
|
sum(julianday(case when timeEnd > @v1 then @v1 else timeEnd end) -
|
|
julianday(case when timeStart < @v0 then @v0 else timeStart end)) days
|
|
from app
|
|
join win on win.appid = app.id
|
|
join period p on p.winid = win.id
|
|
left join tag on tag.id = app.tagId
|
|
where
|
|
timeStart between @v0 and @v1
|
|
or timeEnd between @v0 and @v1
|
|
or @v0 between timeStart and timeEnd
|
|
group by app.id
|
|
order by days desc
|
|
",
|
|
timefrom, timeto
|
|
);
|
|
|
|
foreach (var i in data)
|
|
{
|
|
var time = new TimeSpan((long)(i.days * TimeSpan.TicksPerDay));
|
|
result.Add(
|
|
new
|
|
{
|
|
i.appId,
|
|
i.tagText,
|
|
text = i.appText,
|
|
time = time.ToString(@"hh\:mm\:ss"),
|
|
percent = Math.Round(time.TotalSeconds * 100 / totalSeconds, 2) + "%",
|
|
children = new object[0]
|
|
}
|
|
);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
data = db.ExecuteDynamic(@"
|
|
select
|
|
win.id winId,
|
|
win.text winText,
|
|
tag.text tagText,
|
|
sum(julianday(case when timeEnd > @v1 then @v1 else timeEnd end) -
|
|
julianday(case when timeStart < @v0 then @v0 else timeStart end)) days
|
|
from win
|
|
join period p on p.winid = win.id
|
|
left join tag on tag.id = win.tagId
|
|
where
|
|
win.appId = @v2
|
|
and (
|
|
timeStart between @v0 and @v1
|
|
or timeEnd between @v0 and @v1
|
|
or @v0 between timeStart and timeEnd
|
|
)
|
|
group by win.id
|
|
order by days desc
|
|
",
|
|
timefrom, timeto, parentKey
|
|
);
|
|
|
|
foreach (var i in data)
|
|
{
|
|
var time = new TimeSpan((long)(i.days * TimeSpan.TicksPerDay));
|
|
result.Add(
|
|
new
|
|
{
|
|
i.winId,
|
|
i.tagText,
|
|
text = string.IsNullOrWhiteSpace(i.winText) ? "(无标题)" : i.winText,
|
|
time = time.ToString(@"hh\:mm\:ss"),
|
|
percent = Math.Round(time.TotalSeconds * 100 / totalSeconds, 2) + "%"
|
|
}
|
|
);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
if (parentKey == 0)
|
|
{
|
|
|
|
data = db.ExecuteDynamic(@"
|
|
select
|
|
ifnull(tag.id,-1) tagId,
|
|
ifnull(tag.text, '(无标签)') tagText,
|
|
sum(julianday(case when timeEnd > @v1 then @v1 else timeEnd end) -
|
|
julianday(case when timeStart < @v0 then @v0 else timeStart end)) days
|
|
from win
|
|
join app on app.id = win.appid
|
|
join period p on p.winid = win.id
|
|
left join tag on tag.id = win.tagId or (win.tagId = 0 and tag.id = app.tagId)
|
|
where
|
|
timeStart between @v0 and @v1
|
|
or timeEnd between @v0 and @v1
|
|
or @v0 between timeStart and timeEnd
|
|
group by tag.id
|
|
order by days desc
|
|
",
|
|
timefrom, timeto
|
|
);
|
|
|
|
foreach (var i in data)
|
|
{
|
|
var time = new TimeSpan((long)(i.days * TimeSpan.TicksPerDay));
|
|
result.Add(
|
|
new
|
|
{
|
|
i.tagId,
|
|
i.tagText,
|
|
time = time.ToString(@"hh\:mm\:ss"),
|
|
percent = Math.Round(time.TotalSeconds * 100 / totalSeconds, 2) + "%"
|
|
}
|
|
);
|
|
}
|
|
return result;
|
|
|
|
}
|
|
|
|
data = db.ExecuteDynamic(@"
|
|
select
|
|
win.id winId,
|
|
win.text winText,
|
|
sum(julianday(case when timeEnd > @v1 then @v1 else timeEnd end) -
|
|
julianday(case when timeStart < @v0 then @v0 else timeStart end)) days
|
|
from win
|
|
join period p on p.winid = win.id
|
|
join app on app.id = win.appid
|
|
left join tag on tag.id = win.tagId or (win.tagId = 0 and tag.id = app.tagId)
|
|
where
|
|
ifnull(tag.id, -1) = @v2
|
|
and (
|
|
timeStart between @v0 and @v1
|
|
or timeEnd between @v0 and @v1
|
|
or @v0 between timeStart and timeEnd
|
|
)
|
|
group by win.id
|
|
order by days desc
|
|
",
|
|
timefrom, timeto, parentKey
|
|
);
|
|
|
|
foreach (var i in data)
|
|
{
|
|
var time = new TimeSpan((long)(i.days * TimeSpan.TicksPerDay));
|
|
result.Add(
|
|
new
|
|
{
|
|
i.winId,
|
|
text = string.IsNullOrWhiteSpace(i.winText) ? "(无标题)" : i.winText,
|
|
time = time.ToString(@"hh\:mm\:ss"),
|
|
percent = Math.Round(time.TotalSeconds * 100 / totalSeconds, 2) + "%"
|
|
}
|
|
);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 时间
|
|
/// </summary>
|
|
public class TimeInfo
|
|
{
|
|
/// <summary>
|
|
/// 原始时间
|
|
/// </summary>
|
|
public DateTime timeSrc;
|
|
/// <summary>
|
|
/// 切换到应用的时间
|
|
/// </summary>
|
|
public DateTime timeStart;
|
|
/// <summary>
|
|
/// 应用名
|
|
/// </summary>
|
|
public string app;
|
|
/// <summary>
|
|
/// 应用id
|
|
/// </summary>
|
|
public long appId;
|
|
/// <summary>
|
|
/// 窗口标题
|
|
/// </summary>
|
|
public string title;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取指定时间的记录信息
|
|
/// </summary>
|
|
/// <param name="time"></param>
|
|
/// <returns></returns>
|
|
public TimeInfo getTimeInfo(DateTime time)
|
|
{
|
|
|
|
var data = db.ExecuteDynamic(@"
|
|
SELECT timeStart, app.text appText, win.text winText, app.id appId
|
|
from period
|
|
join win on win.id = period.winid
|
|
join app on app.id = win.appid
|
|
where @v0 between timeStart and timeEnd
|
|
limit 1",
|
|
time
|
|
).FirstOrDefault();
|
|
|
|
if (data == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return new TimeInfo
|
|
{
|
|
timeSrc = time,
|
|
timeStart = data.timeStart,
|
|
app = data.appText,
|
|
title = data.winText,
|
|
appId = data.appId
|
|
};
|
|
}
|
|
|
|
static byte[] defaultIcon;
|
|
|
|
public byte[] getIcon(int appId, bool large)
|
|
{
|
|
var path = Recorder.GetIconPath(appId, large);
|
|
if (File.Exists(path))
|
|
{
|
|
return File.ReadAllBytes(path);
|
|
}
|
|
if (defaultIcon == null)
|
|
{
|
|
defaultIcon = File.ReadAllBytes("./webui/img/icon.png");
|
|
}
|
|
return defaultIcon;
|
|
}
|
|
|
|
static byte[] imageNone = null;
|
|
|
|
TimeSpan getTime(string file)
|
|
{
|
|
return TimeSpan.ParseExact(Path.GetFileNameWithoutExtension(file), "hhmmss", CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// 查找不满足条件的最后一个元素
|
|
/// </summary>
|
|
/// <typeparam name="T"></typeparam>
|
|
/// <param name="items"></param>
|
|
/// <param name="largerThenTarget"></param>
|
|
/// <returns></returns>
|
|
T find<T>(IList<T> items, Func<T, bool> largerThenTarget)
|
|
{
|
|
if (items.Count == 0)
|
|
{
|
|
return default;
|
|
}
|
|
|
|
var match = items[0];
|
|
if (largerThenTarget(match))
|
|
{
|
|
return default;
|
|
}
|
|
|
|
for (var i = 1; i < items.Count; i++)
|
|
{
|
|
var item = items[i];
|
|
if (largerThenTarget(item))
|
|
{
|
|
break;
|
|
}
|
|
match = item;
|
|
}
|
|
return match;
|
|
}
|
|
|
|
|
|
static Thread lastThread;
|
|
static readonly object threadLock = new object();
|
|
/// <summary>
|
|
/// 获取指定时间的截图
|
|
/// </summary>
|
|
/// <param name="info"></param>
|
|
/// <returns></returns>
|
|
public byte[] getImage(TimeInfo info)
|
|
{
|
|
|
|
if (imageNone == null)
|
|
{
|
|
imageNone = File.ReadAllBytes(Path.Combine(Server.WebRootPath, "img", "none.png"));
|
|
}
|
|
|
|
//只响应最后一个请求,避免运行多个ffmpeg占用资源。
|
|
lock (threadLock)
|
|
{
|
|
Ffmpeg.KillLastFfmpeg();
|
|
if (lastThread != null && lastThread.IsAlive)
|
|
{
|
|
lastThread.Abort();
|
|
}
|
|
lastThread = Thread.CurrentThread;
|
|
}
|
|
|
|
try
|
|
{
|
|
//先从buffer中找
|
|
{
|
|
var buffers = new List<MemoryBuffer>(Recorder.flushing);
|
|
if (Recorder.buffer != null)
|
|
{
|
|
buffers.Add(Recorder.buffer);
|
|
}
|
|
|
|
var match = find(buffers, i => i.StartTime > info.timeSrc);
|
|
if (match != null)
|
|
{
|
|
var time = info.timeSrc - match.StartTime;
|
|
var frame = find(match.Frames, f => (match.StartTime + f.Time) > info.timeSrc);
|
|
if (frame != null)
|
|
{
|
|
return frame.Data;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//从文件系统找
|
|
{
|
|
var path = Recorder.getFileName(info.timeSrc);
|
|
var needtime = info.timeSrc.TimeOfDay;
|
|
var needtimetext = needtime.ToString("hhmmss");
|
|
var match = (from f in Directory.GetFiles(Path.GetDirectoryName(path), "????????." + Recorder.ExName)
|
|
where Path.GetFileNameWithoutExtension(f).CompareTo(needtimetext) < 0
|
|
orderby f
|
|
select f).LastOrDefault();
|
|
if (match != null)
|
|
{
|
|
var time = needtime - getTime(match);
|
|
var data = Ffmpeg.Snapshot(match, time);
|
|
if (data != null && data.Length > 0)
|
|
{
|
|
return data;
|
|
}
|
|
}
|
|
|
|
return imageNone;
|
|
}
|
|
}
|
|
catch (ThreadAbortException)
|
|
{
|
|
return imageNone;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region tag
|
|
|
|
private long nextTagId = 0;
|
|
private long NextTagId()
|
|
{
|
|
if (nextTagId == 0)
|
|
{
|
|
nextTagId = db.ExecuteValue<long>("select ifnull(max(id), 0) + 1 from tag");
|
|
}
|
|
return nextTagId++;
|
|
}
|
|
|
|
public bool existsTag(string text)
|
|
{
|
|
return db.ExecuteValue< bool>(
|
|
"select exists(select * from tag where text = @text)",
|
|
new SQLiteParameter("text", text)
|
|
);
|
|
}
|
|
|
|
public bool addTag(string text)
|
|
{
|
|
if(existsTag(text))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
db.Execute(
|
|
"insert into tag (id, text) values(@id, @text)",
|
|
new SQLiteParameter("id", NextTagId()),
|
|
new SQLiteParameter("text", text)
|
|
);
|
|
return true;
|
|
}
|
|
|
|
public void removeTag(int tagId)
|
|
{
|
|
|
|
db.Execute(
|
|
"delete from tag where id = @id",
|
|
new SQLiteParameter("id", tagId)
|
|
);
|
|
|
|
db.Execute($"update app set tagId=0 where tagId={tagId}");
|
|
db.Execute($"update win set tagId=0 where tagId={tagId}");
|
|
|
|
}
|
|
|
|
public void clearAppTag(long appId)
|
|
{
|
|
db.Execute("update app set tagid = 0 where id = @v0", appId);
|
|
}
|
|
|
|
public void clearWinTag(long winId)
|
|
{
|
|
db.Execute("update win set tagid = 0 where id = @v0", winId);
|
|
}
|
|
|
|
public DataTable getTags()
|
|
{
|
|
return db.ExecuteTable("select id, text from tag order by id");
|
|
}
|
|
|
|
|
|
public bool isTagUsed(int tagId)
|
|
{
|
|
return db.ExecuteValue<bool>(
|
|
@"select exists(
|
|
select * from app where tagId = @tagId
|
|
union all
|
|
select * from win where tagId = @tagId
|
|
)",
|
|
new SQLiteParameter("tagId", tagId)
|
|
);
|
|
}
|
|
|
|
DB db = DB.Instance;
|
|
|
|
public bool renameTag(long tagId, string newName)
|
|
{
|
|
if(existsTag(newName))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
db.Execute("update tag set text=@newName where id=@tagId",
|
|
new SQLiteParameter("newName", newName),
|
|
new SQLiteParameter("tagId", tagId)
|
|
);
|
|
|
|
|
|
return true;
|
|
}
|
|
|
|
public void tagApp(long appId, long tagId)
|
|
{
|
|
db.Execute(
|
|
"update app set tagid = @tagId where id=@appId",
|
|
new SQLiteParameter("appId", appId),
|
|
new SQLiteParameter("tagId", tagId)
|
|
);
|
|
}
|
|
|
|
public void tagWin(long winId, long tagId)
|
|
{
|
|
db.Execute(
|
|
"update win set tagid = @tagId where id=@winId",
|
|
new SQLiteParameter("winId", winId),
|
|
new SQLiteParameter("tagId", tagId)
|
|
);
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|