懒人记时 代码仓库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

623 lines
18 KiB

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
}
}