@ -0,0 +1 @@ | |||
webui |
@ -0,0 +1,25 @@ | |||
| |||
Microsoft Visual Studio Solution File, Format Version 12.00 | |||
# Visual Studio Version 16 | |||
VisualStudioVersion = 16.0.30907.101 | |||
MinimumVisualStudioVersion = 10.0.40219.1 | |||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppTime", "AppTime\AppTime.csproj", "{4892E56B-5942-444B-B41C-1E4F22A3752A}" | |||
EndProject | |||
Global | |||
GlobalSection(SolutionConfigurationPlatforms) = preSolution | |||
Debug|Any CPU = Debug|Any CPU | |||
Release|Any CPU = Release|Any CPU | |||
EndGlobalSection | |||
GlobalSection(ProjectConfigurationPlatforms) = postSolution | |||
{4892E56B-5942-444B-B41C-1E4F22A3752A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | |||
{4892E56B-5942-444B-B41C-1E4F22A3752A}.Debug|Any CPU.Build.0 = Debug|Any CPU | |||
{4892E56B-5942-444B-B41C-1E4F22A3752A}.Release|Any CPU.ActiveCfg = Release|Any CPU | |||
{4892E56B-5942-444B-B41C-1E4F22A3752A}.Release|Any CPU.Build.0 = Release|Any CPU | |||
EndGlobalSection | |||
GlobalSection(SolutionProperties) = preSolution | |||
HideSolutionNode = FALSE | |||
EndGlobalSection | |||
GlobalSection(ExtensibilityGlobals) = postSolution | |||
SolutionGuid = {2525871C-F224-48A9-9189-E784A51F63A8} | |||
EndGlobalSection | |||
EndGlobal |
@ -0,0 +1,2 @@ | |||
# Auto detect text files and perform LF normalization | |||
* text=auto |
@ -0,0 +1,623 @@ | |||
| |||
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 | |||
} | |||
} |
@ -0,0 +1,136 @@ | |||
using System; | |||
using System.Collections; | |||
using System.Collections.Generic; | |||
using System.Data; | |||
using System.Data.Common; | |||
using System.Data.OleDb; | |||
using System.Data.SQLite; | |||
using System.Dynamic; | |||
using System.IO; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Windows.Forms; | |||
namespace AppTime | |||
{ | |||
class DB | |||
{ | |||
public readonly static DB Instance = new DB(); | |||
DbConnection conn; | |||
DbProviderFactory factory = SQLiteFactory.Instance; | |||
public DB() | |||
{ | |||
var connectionString = new SQLiteConnectionStringBuilder() | |||
{ | |||
DataSource= Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), "data.db") | |||
}.ToString(); | |||
conn = new SQLiteConnection | |||
{ | |||
ConnectionString = connectionString | |||
}; | |||
} | |||
T execute<T>(Func<DbCommand, T> handler, string sql, params object[] args) | |||
{ | |||
lock (this) | |||
{ | |||
conn.Open(); | |||
using var cmd = conn.CreateCommand(); | |||
cmd.CommandText = sql; | |||
for (var i = 0; i < args.Length; i++) | |||
{ | |||
var arg = args[i]; | |||
if (!(arg is DbParameter param)) | |||
{ | |||
param = factory.CreateParameter(); | |||
param.ParameterName = $"@v{i}"; | |||
param.Value = arg; | |||
} | |||
cmd.Parameters.Add(param); | |||
} | |||
var result = handler(cmd); | |||
conn.Close(); | |||
return result; | |||
} | |||
} | |||
public int Execute(string sql, params object[] args) | |||
{ | |||
return execute(cmd => cmd.ExecuteNonQuery(), sql, args) ; | |||
} | |||
public List<object[]> ExecuteData(string sql, params object[] args) | |||
{ | |||
return execute(cmd => | |||
{ | |||
var result = new List<object[]>(); | |||
using var reader = cmd.ExecuteReader(); | |||
while (reader.Read()) | |||
{ | |||
var row = new object[reader.FieldCount]; | |||
reader.GetValues(row); | |||
result.Add(row); | |||
} | |||
return result; | |||
}, sql, args); | |||
} | |||
public T ExecuteValue<T>(string sql, params object[] args) | |||
{ | |||
return (T)Convert.ChangeType(ExecuteData(sql, args)[0][0], typeof(T)); | |||
} | |||
public T[] ExecuteColumn<T>(string sql, params object[] args) | |||
{ | |||
var data = ExecuteData(sql, args); | |||
var result = new T[data.Count]; | |||
for(var i = 0;i<data.Count;i++) | |||
{ | |||
result[i] = (T)Convert.ChangeType(data[i][0], typeof(T)); | |||
} | |||
return result; | |||
} | |||
public DataTable ExecuteTable(string sql, params object[] args) | |||
{ | |||
return execute(cmd => | |||
{ | |||
using var adapter = factory.CreateDataAdapter(); | |||
adapter.SelectCommand = cmd; | |||
var result = new DataTable(); | |||
adapter.Fill(result); | |||
return result; | |||
}, sql, args); | |||
} | |||
public IEnumerable<dynamic> ExecuteDynamic(string sql, params object[] args) | |||
{ | |||
return from r in ExecuteTable(sql, args).AsEnumerable() select new DynamicDataRow(r); | |||
} | |||
} | |||
class DynamicDataRow : DynamicObject | |||
{ | |||
DataRow row; | |||
public DynamicDataRow(DataRow row) | |||
{ | |||
this.row = row; | |||
} | |||
public override bool TryGetMember(GetMemberBinder binder, out object result) | |||
{ | |||
result = row[binder.Name]; | |||
return true; | |||
} | |||
} | |||
} |
@ -0,0 +1,114 @@ | |||
using AppTime.Properties; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Diagnostics; | |||
using System.Drawing; | |||
using System.Drawing.Imaging; | |||
using System.IO; | |||
using System.IO.MemoryMappedFiles; | |||
using System.IO.Pipes; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
using System.Windows.Forms; | |||
using static AppTime.Recorder; | |||
namespace AppTime | |||
{ | |||
class Ffmpeg | |||
{ | |||
static Process lastFfmpeg; | |||
public static void KillLastFfmpeg() | |||
{ | |||
if (lastFfmpeg != null && !lastFfmpeg.HasExited) | |||
{ | |||
Utils.Try(() => lastFfmpeg.Kill()); | |||
lastFfmpeg = null; | |||
} | |||
} | |||
public static byte[] Snapshot(string file, TimeSpan time) | |||
{ | |||
var args = $@"-loglevel quiet -ss {time} -i ""{file}"" -y -frames 1 -q:v 2 -f image2 -"; | |||
var info = new ProcessStartInfo(@"ffmpeg\ffmpeg.exe", args) | |||
{ | |||
RedirectStandardOutput = true, | |||
RedirectStandardError = true, | |||
RedirectStandardInput = true, | |||
UseShellExecute = false, | |||
CreateNoWindow = true, | |||
WorkingDirectory = Path.GetDirectoryName(Application.ExecutablePath) | |||
}; | |||
var p = lastFfmpeg = Process.Start(info); | |||
var output = p.StandardOutput.BaseStream; | |||
var data = new List<byte>(); | |||
var b = output.ReadByte(); | |||
while (b != -1) | |||
{ | |||
data.Add((byte)b); | |||
b = output.ReadByte(); | |||
} | |||
return data.ToArray(); | |||
} | |||
public static void Save(string file, params Frame[] images) | |||
{ | |||
if (images.Length == 0) | |||
{ | |||
return; | |||
} | |||
var rate = images.Length / ((images.Last().Time - images.First().Time).TotalSeconds + 1); | |||
var crf = Settings.Default.ImageQuality;//0-质量最高 63-质量最低 实测40质量也不错且体积较小 | |||
var tempfile = Path.Combine(Path.GetDirectoryName(file), Path.GetFileNameWithoutExtension(file) + ".tmp"); | |||
var args = $@"-loglevel quiet -f image2pipe -r {rate} -i - -vcodec libx264 -crf {crf} -f matroska -y ""{tempfile}"""; | |||
var info = new ProcessStartInfo(@"ffmpeg\ffmpeg.exe", args) | |||
{ | |||
RedirectStandardOutput = true, | |||
RedirectStandardError = true, | |||
RedirectStandardInput = true, | |||
UseShellExecute = false, | |||
CreateNoWindow = true, | |||
WorkingDirectory = Path.GetDirectoryName(Application.ExecutablePath) | |||
}; | |||
var p = Process.Start(info); | |||
p.PriorityClass = ProcessPriorityClass.BelowNormal; | |||
foreach (var i in images) | |||
{ | |||
p.StandardInput.BaseStream.Write(i.Data, 0, i.Data.Length); | |||
} | |||
p.StandardInput.Close(); | |||
p.WaitForExit(); | |||
if (File.Exists(file)) | |||
{ | |||
File.Delete(file); | |||
} | |||
File.Move(tempfile, file); | |||
} | |||
} | |||
public class Frame | |||
{ | |||
public TimeSpan Time; | |||
public byte[] Data; | |||
public Frame(TimeSpan time, byte[] data) | |||
{ | |||
this.Time = time; | |||
this.Data = data; | |||
} | |||
} | |||
} |
@ -0,0 +1,267 @@ | |||
namespace AppTime | |||
{ | |||
partial class FrmMain | |||
{ | |||
/// <summary> | |||
/// 必需的设计器变量。 | |||
/// </summary> | |||
private System.ComponentModel.IContainer components = null; | |||
/// <summary> | |||
/// 清理所有正在使用的资源。 | |||
/// </summary> | |||
/// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param> | |||
protected override void Dispose(bool disposing) | |||
{ | |||
if (disposing && (components != null)) | |||
{ | |||
components.Dispose(); | |||
} | |||
base.Dispose(disposing); | |||
} | |||
#region Windows 窗体设计器生成的代码 | |||
/// <summary> | |||
/// 设计器支持所需的方法 - 不要修改 | |||
/// 使用代码编辑器修改此方法的内容。 | |||
/// </summary> | |||
private void InitializeComponent() | |||
{ | |||
this.components = new System.ComponentModel.Container(); | |||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(FrmMain)); | |||
this.txtDataPath = new System.Windows.Forms.TextBox(); | |||
this.label1 = new System.Windows.Forms.Label(); | |||
this.btnDataPath = new System.Windows.Forms.Button(); | |||
this.cboRecordScreen = new System.Windows.Forms.ComboBox(); | |||
this.btnOK = new System.Windows.Forms.Button(); | |||
this.btnCancel = new System.Windows.Forms.Button(); | |||
this.notifyIcon = new System.Windows.Forms.NotifyIcon(this.components); | |||
this.contextMenuStrip = new System.Windows.Forms.ContextMenuStrip(this.components); | |||
this.btnOpen = new System.Windows.Forms.ToolStripMenuItem(); | |||
this.btnSetting = new System.Windows.Forms.ToolStripMenuItem(); | |||
this.btnAbout = new System.Windows.Forms.ToolStripMenuItem(); | |||
this.btnExit = new System.Windows.Forms.ToolStripMenuItem(); | |||
this.groupBox1 = new System.Windows.Forms.GroupBox(); | |||
this.label4 = new System.Windows.Forms.Label(); | |||
this.label2 = new System.Windows.Forms.Label(); | |||
this.chkAutoRun = new System.Windows.Forms.CheckBox(); | |||
this.cboImageQuality = new System.Windows.Forms.ComboBox(); | |||
this.contextMenuStrip.SuspendLayout(); | |||
this.groupBox1.SuspendLayout(); | |||
this.SuspendLayout(); | |||
// | |||
// txtDataPath | |||
// | |||
this.txtDataPath.Location = new System.Drawing.Point(132, 34); | |||
this.txtDataPath.Name = "txtDataPath"; | |||
this.txtDataPath.Size = new System.Drawing.Size(494, 21); | |||
this.txtDataPath.TabIndex = 0; | |||
// | |||
// label1 | |||
// | |||
this.label1.AutoSize = true; | |||
this.label1.Location = new System.Drawing.Point(40, 37); | |||
this.label1.Name = "label1"; | |||
this.label1.Size = new System.Drawing.Size(77, 12); | |||
this.label1.TabIndex = 1; | |||
this.label1.Text = "数据存储位置"; | |||
// | |||
// btnDataPath | |||
// | |||
this.btnDataPath.Location = new System.Drawing.Point(641, 32); | |||
this.btnDataPath.Name = "btnDataPath"; | |||
this.btnDataPath.Size = new System.Drawing.Size(75, 23); | |||
this.btnDataPath.TabIndex = 2; | |||
this.btnDataPath.Text = "浏览(&B)..."; | |||
this.btnDataPath.UseVisualStyleBackColor = true; | |||
this.btnDataPath.Click += new System.EventHandler(this.btnDataPath_Click); | |||
// | |||
// cboRecordScreen | |||
// | |||
this.cboRecordScreen.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; | |||
this.cboRecordScreen.FormattingEnabled = true; | |||
this.cboRecordScreen.Items.AddRange(new object[] { | |||
"记录最近30天", | |||
"记录最近14天", | |||
"无限制记录", | |||
"不记录"}); | |||
this.cboRecordScreen.Location = new System.Drawing.Point(98, 33); | |||
this.cboRecordScreen.Name = "cboRecordScreen"; | |||
this.cboRecordScreen.Size = new System.Drawing.Size(121, 20); | |||
this.cboRecordScreen.TabIndex = 4; | |||
// | |||
// btnOK | |||
// | |||
this.btnOK.Location = new System.Drawing.Point(366, 245); | |||
this.btnOK.Name = "btnOK"; | |||
this.btnOK.Size = new System.Drawing.Size(158, 45); | |||
this.btnOK.TabIndex = 6; | |||
this.btnOK.Text = "确定(&O)"; | |||
this.btnOK.UseVisualStyleBackColor = true; | |||
this.btnOK.Click += new System.EventHandler(this.btnOK_Click); | |||
// | |||
// btnCancel | |||
// | |||
this.btnCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel; | |||
this.btnCancel.Location = new System.Drawing.Point(556, 245); | |||
this.btnCancel.Name = "btnCancel"; | |||
this.btnCancel.Size = new System.Drawing.Size(158, 45); | |||
this.btnCancel.TabIndex = 7; | |||
this.btnCancel.Text = "取消(&C)"; | |||
this.btnCancel.UseVisualStyleBackColor = true; | |||
this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click); | |||
// | |||
// notifyIcon | |||
// | |||
this.notifyIcon.ContextMenuStrip = this.contextMenuStrip; | |||
this.notifyIcon.Icon = ((System.Drawing.Icon)(resources.GetObject("notifyIcon.Icon"))); | |||
this.notifyIcon.Visible = true; | |||
this.notifyIcon.DoubleClick += new System.EventHandler(this.notifyIcon_DoubleClick); | |||
// | |||
// contextMenuStrip | |||
// | |||
this.contextMenuStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { | |||
this.btnOpen, | |||
this.btnSetting, | |||
this.btnAbout, | |||
this.btnExit}); | |||
this.contextMenuStrip.Name = "contextMenuStrip"; | |||
this.contextMenuStrip.Size = new System.Drawing.Size(121, 92); | |||
// | |||
// btnOpen | |||
// | |||
this.btnOpen.Font = new System.Drawing.Font("Microsoft YaHei UI", 9F, System.Drawing.FontStyle.Bold); | |||
this.btnOpen.Name = "btnOpen"; | |||
this.btnOpen.Size = new System.Drawing.Size(120, 22); | |||
this.btnOpen.Text = "打开(&O)"; | |||
this.btnOpen.Click += new System.EventHandler(this.btnOpen_Click); | |||
// | |||
// btnSetting | |||
// | |||
this.btnSetting.Name = "btnSetting"; | |||
this.btnSetting.Size = new System.Drawing.Size(120, 22); | |||
this.btnSetting.Text = "设置(&S)"; | |||
this.btnSetting.Click += new System.EventHandler(this.btnSetting_Click); | |||
// | |||
// btnAbout | |||
// | |||
this.btnAbout.Name = "btnAbout"; | |||
this.btnAbout.Size = new System.Drawing.Size(120, 22); | |||
this.btnAbout.Text = "关于(&A)"; | |||
this.btnAbout.Click += new System.EventHandler(this.btnAbout_Click); | |||
// | |||
// btnExit | |||
// | |||
this.btnExit.Name = "btnExit"; | |||
this.btnExit.Size = new System.Drawing.Size(120, 22); | |||
this.btnExit.Text = "退出(&E)"; | |||
this.btnExit.Click += new System.EventHandler(this.btnExit_Click); | |||
// | |||
// groupBox1 | |||
// | |||
this.groupBox1.Controls.Add(this.cboImageQuality); | |||
this.groupBox1.Controls.Add(this.label4); | |||
this.groupBox1.Controls.Add(this.label2); | |||
this.groupBox1.Controls.Add(this.cboRecordScreen); | |||
this.groupBox1.Location = new System.Drawing.Point(34, 85); | |||
this.groupBox1.Name = "groupBox1"; | |||
this.groupBox1.Size = new System.Drawing.Size(682, 132); | |||
this.groupBox1.TabIndex = 10; | |||
this.groupBox1.TabStop = false; | |||
this.groupBox1.Text = "记录屏幕"; | |||
// | |||
// label4 | |||
// | |||
this.label4.AutoSize = true; | |||
this.label4.Location = new System.Drawing.Point(14, 79); | |||
this.label4.Name = "label4"; | |||
this.label4.Size = new System.Drawing.Size(53, 12); | |||
this.label4.TabIndex = 17; | |||
this.label4.Text = "图片质量"; | |||
// | |||
// label2 | |||
// | |||
this.label2.AutoSize = true; | |||
this.label2.Location = new System.Drawing.Point(15, 36); | |||
this.label2.Name = "label2"; | |||
this.label2.Size = new System.Drawing.Size(53, 12); | |||
this.label2.TabIndex = 11; | |||
this.label2.Text = "留存时间"; | |||
// | |||
// chkAutoRun | |||
// | |||
this.chkAutoRun.AutoSize = true; | |||
this.chkAutoRun.Location = new System.Drawing.Point(40, 255); | |||
this.chkAutoRun.Name = "chkAutoRun"; | |||
this.chkAutoRun.Size = new System.Drawing.Size(114, 16); | |||
this.chkAutoRun.TabIndex = 11; | |||
this.chkAutoRun.Text = "开机自动启动(&A)"; | |||
this.chkAutoRun.UseVisualStyleBackColor = true; | |||
// | |||
// cboImageQuality | |||
// | |||
this.cboImageQuality.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; | |||
this.cboImageQuality.FormattingEnabled = true; | |||
this.cboImageQuality.Items.AddRange(new object[] { | |||
"记录最近30天", | |||
"记录最近14天", | |||
"无限制记录", | |||
"不记录"}); | |||
this.cboImageQuality.Location = new System.Drawing.Point(98, 76); | |||
this.cboImageQuality.Name = "cboImageQuality"; | |||
this.cboImageQuality.Size = new System.Drawing.Size(121, 20); | |||
this.cboImageQuality.TabIndex = 20; | |||
// | |||
// FrmMain | |||
// | |||
this.AcceptButton = this.btnOK; | |||
this.CancelButton = this.btnCancel; | |||
this.ClientSize = new System.Drawing.Size(751, 320); | |||
this.Controls.Add(this.chkAutoRun); | |||
this.Controls.Add(this.groupBox1); | |||
this.Controls.Add(this.btnCancel); | |||
this.Controls.Add(this.btnOK); | |||
this.Controls.Add(this.btnDataPath); | |||
this.Controls.Add(this.label1); | |||
this.Controls.Add(this.txtDataPath); | |||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; | |||
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); | |||
this.MaximizeBox = false; | |||
this.MinimizeBox = false; | |||
this.Name = "FrmMain"; | |||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; | |||
this.Text = "设置"; | |||
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.FrmMain_FormClosing); | |||
this.Load += new System.EventHandler(this.FrmMain_Load); | |||
this.Shown += new System.EventHandler(this.FrmMain_Shown); | |||
this.contextMenuStrip.ResumeLayout(false); | |||
this.groupBox1.ResumeLayout(false); | |||
this.groupBox1.PerformLayout(); | |||
this.ResumeLayout(false); | |||
this.PerformLayout(); | |||
} | |||
#endregion | |||
private System.Windows.Forms.TextBox txtDataPath; | |||
private System.Windows.Forms.Label label1; | |||
private System.Windows.Forms.Button btnDataPath; | |||
private System.Windows.Forms.ComboBox cboRecordScreen; | |||
private System.Windows.Forms.Button btnOK; | |||
private System.Windows.Forms.Button btnCancel; | |||
private System.Windows.Forms.NotifyIcon notifyIcon; | |||
private System.Windows.Forms.ContextMenuStrip contextMenuStrip; | |||
private System.Windows.Forms.ToolStripMenuItem btnOpen; | |||
private System.Windows.Forms.ToolStripMenuItem btnSetting; | |||
private System.Windows.Forms.ToolStripMenuItem btnAbout; | |||
private System.Windows.Forms.ToolStripMenuItem btnExit; | |||
private System.Windows.Forms.GroupBox groupBox1; | |||
private System.Windows.Forms.Label label2; | |||
private System.Windows.Forms.Label label4; | |||
private System.Windows.Forms.CheckBox chkAutoRun; | |||
private System.Windows.Forms.ComboBox cboImageQuality; | |||
} | |||
} | |||
@ -0,0 +1,165 @@ | |||
| |||
using AppTime.Properties; | |||
using Microsoft.Win32; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.ComponentModel; | |||
using System.Data; | |||
using System.Data.OleDb; | |||
using System.Diagnostics; | |||
using System.Drawing; | |||
using System.Drawing.Imaging; | |||
using System.IO; | |||
using System.Linq; | |||
using System.Runtime.InteropServices; | |||
using System.Security.AccessControl; | |||
using System.Text; | |||
using System.Web.ModelBinding; | |||
using System.Web.UI.WebControls; | |||
using System.Windows.Forms; | |||
namespace AppTime | |||
{ | |||
public partial class FrmMain : Form | |||
{ | |||
const string appname = "AppTime"; | |||
const string regkey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run"; | |||
public FrmMain() | |||
{ | |||
InitializeComponent(); | |||
} | |||
private void FrmMain_Load(object sender, EventArgs e) | |||
{ | |||
cboRecordScreen.DataSource = new[] { | |||
new {Text="最近30天", Value=30}, | |||
new {Text="最近15天", Value=15}, | |||
new {Text="无限制", Value=int.MaxValue}, | |||
new {Text="不留存", Value=0}, | |||
}; | |||
cboRecordScreen.DisplayMember = "Text"; | |||
cboRecordScreen.ValueMember = "Value"; | |||
cboImageQuality.DataSource = new[] { | |||
new {Text="最省磁盘", Value=63}, | |||
new {Text="均衡", Value=50}, | |||
new {Text="高质量", Value=40}, | |||
}; | |||
cboImageQuality.DisplayMember = "Text"; | |||
cboImageQuality.ValueMember = "Value"; | |||
} | |||
private void btnOpen_Click(object sender, EventArgs e) | |||
{ | |||
Process.Start($@"http://localhost:{Program.Port}/"); | |||
} | |||
private void notifyIcon_DoubleClick(object sender, EventArgs e) | |||
{ | |||
btnOpen_Click(null, null); | |||
} | |||
private void btnCancel_Click(object sender, EventArgs e) | |||
{ | |||
this.Hide(); | |||
} | |||
bool cancelClose = true; | |||
private void btnExit_Click(object sender, EventArgs e) | |||
{ | |||
this.Hide(); | |||
cancelClose = false; | |||
Program.recorder.FlushScreenBuffer(); | |||
this.Close(); | |||
} | |||
private void FrmMain_FormClosing(object sender, FormClosingEventArgs e) | |||
{ | |||
this.Hide(); | |||
e.Cancel = cancelClose; | |||
} | |||
private void btnOK_Click(object sender, EventArgs e) | |||
{ | |||
try | |||
{ | |||
Directory.CreateDirectory(txtDataPath.Text); | |||
Settings.Default.DataPath = txtDataPath.Text == Application.StartupPath ? "" : txtDataPath.Text; | |||
Program.recorder.BuildDataPath(); | |||
} | |||
catch | |||
{ | |||
MessageBox.Show("数据存储位置无效,请重新选择。"); | |||
} | |||
Settings.Default.ImageQuality = (int) cboImageQuality.SelectedValue; | |||
Settings.Default.RecordScreenDays = (int)cboRecordScreen.SelectedValue; | |||
Settings.Default.Save(); | |||
using var reg = Registry.CurrentUser.CreateSubKey(regkey); | |||
try | |||
{ | |||
if (chkAutoRun.Checked) | |||
{ | |||
reg.SetValue(appname, Application.ExecutablePath); | |||
} | |||
else | |||
{ | |||
reg.DeleteValue(appname); | |||
} | |||
} | |||
catch (UnauthorizedAccessException) | |||
{ | |||
MessageBox.Show("设置启动失败,请检查:\r\n\r\n1、关闭杀毒软件(如360等);\r\n2、以管理员身份运行本程序。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); | |||
} | |||
this.Hide(); | |||
} | |||
private void FrmMain_Shown(object sender, EventArgs e) | |||
{ | |||
this.Hide(); | |||
} | |||
private void btnSetting_Click(object sender, EventArgs e) | |||
{ | |||
if (!this.Visible) | |||
{ | |||
if (string.IsNullOrEmpty(Settings.Default.DataPath)) | |||
{ | |||
txtDataPath.Text = Application.StartupPath; | |||
} | |||
else | |||
{ | |||
txtDataPath.Text = Settings.Default.DataPath; | |||
} | |||
cboRecordScreen.SelectedValue = Settings.Default.RecordScreenDays; | |||
cboImageQuality.SelectedValue = Settings.Default.ImageQuality; | |||
using var reg = Registry.CurrentUser.CreateSubKey(regkey); | |||
chkAutoRun.Checked = (reg.GetValue(appname) as string) == Application.ExecutablePath; | |||
this.Show(); | |||
} | |||
} | |||
private void btnAbout_Click(object sender, EventArgs e) | |||
{ | |||
MessageBox.Show($"AppTime桌面时间管理\r\nV{Application.ProductVersion}\r\n\r\n联系作者:newdraw@hotmail.com", "关于", MessageBoxButtons.OK, MessageBoxIcon.Information); | |||
} | |||
private void btnDataPath_Click(object sender, EventArgs e) | |||
{ | |||
using var dlg = new FolderBrowserDialog(); | |||
if(dlg.ShowDialog()== DialogResult.OK) | |||
{ | |||
txtDataPath.Text = dlg.SelectedPath; | |||
} | |||
} | |||
} | |||
} |
@ -0,0 +1,63 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace AppTime | |||
{ | |||
/** | |||
* 初始化数据库 | |||
*/ | |||
class InitDB | |||
{ | |||
private DB db; | |||
public InitDB() | |||
{ | |||
db = DB.Instance; | |||
} | |||
public void Start() | |||
{ | |||
// 创建数据表 | |||
db.Execute("CREATE TABLE IF NOT EXISTS \"app\" (\"id\" INTEGER NOT NULL, \"process\" text NOT NULL," + | |||
"\"text\" TEXT NOT NULL DEFAULT process, " + | |||
"\"tagId\" INTEGER NOT NULL DEFAULT(0), " + | |||
"PRIMARY KEY(\"id\") ) WITHOUT ROWID"); | |||
db.Execute("CREATE TABLE IF NOT EXISTS \"period\" ( \"timeStart\" DATETIME NOT NULL, \"timeEnd\" DATETIME NOT NULL, " + | |||
"\"winId\" INTEGER NOT NULL," + | |||
"PRIMARY KEY(\"timeStart\")," + | |||
"UNIQUE(\"timeStart\" ASC) )WITHOUT ROWID"); | |||
db.Execute("CREATE TABLE IF NOT EXISTS \"tag\" ( \"id\" INTEGER NOT NULL, \"text\" TEXT NOT NULL," + | |||
"PRIMARY KEY(\"id\"), UNIQUE(\"id\" ASC), UNIQUE(\"text\" ASC) ) WITHOUT ROWID"); | |||
db.Execute("CREATE TABLE IF NOT EXISTS \"win\" (\"id\" INTEGER NOT NULL," + | |||
"\"appId\" INTEGER NOT NULL, \"text\" TEXT NOT NULL, " + | |||
"\"tagId\" INTEGER NOT NULL DEFAULT(0), " + | |||
"PRIMARY KEY(\"id\") ) WITHOUT ROWID"); | |||
// 创建索引 | |||
long existIndex = (long)db.ExecuteData("SELECT count(*) FROM sqlite_master WHERE type=\"table\" AND name =\'is_index\'")[0][0]; | |||
// 判断是否已经存在索引 | |||
if (existIndex == 0) | |||
{ | |||
db.Execute("CREATE UNIQUE INDEX \"ix_app\" ON \"app\"( \"process\" ASC )"); | |||
db.Execute("CREATE INDEX \"ix_app_tagId\" ON \"app\" ( \"tagId\" ASC )"); | |||
db.Execute("CREATE UNIQUE INDEX \"ix_period\" ON \"period\" ( \"timeStart\" ASC, \"timeEnd\" ASC )"); | |||
db.Execute("CREATE UNIQUE INDEX \"ix_win\" ON \"win\" ( \"appId\" ASC, \"text\" ASC )"); | |||
db.Execute("CREATE INDEX \"ix_win_tagId\" ON \"win\" (\"tagId\" ASC )"); | |||
// 此表仅用于标识索引已经创建完成 | |||
db.Execute("CREATE TABLE IF NOT EXISTS \"is_index\"( \"id\" INTEGER NOT NULL, PRIMARY KEY(\"id\"))"); | |||
} | |||
} | |||
} | |||
} |
@ -0,0 +1,82 @@ | |||
| |||
using System; | |||
using System.Collections.Generic; | |||
using System.Data.OleDb; | |||
using System.Diagnostics; | |||
using System.Drawing; | |||
using System.Drawing.Imaging; | |||
using System.IO; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading; | |||
using System.Windows.Forms; | |||
namespace AppTime | |||
{ | |||
static class Program | |||
{ | |||
public const int Port = 15720; | |||
public static InitDB init; | |||
public static Recorder recorder; | |||
public static WebServer server; | |||
public static Controller controller; | |||
public static FrmMain frmMain; | |||
/// <summary> | |||
/// 应用程序的主入口点。 | |||
/// </summary> | |||
[STAThread] | |||
static void Main() | |||
{ | |||
#if DEBUG | |||
CopyWebUI(); | |||
#endif | |||
init = new InitDB(); | |||
init.Start(); | |||
recorder = new Recorder(); | |||
recorder.Start(); | |||
controller = new Controller(); | |||
server = new WebServer(); | |||
server.Start(Port, controller, "./webui"); | |||
Application.EnableVisualStyles(); | |||
Application.SetCompatibleTextRenderingDefault(false); | |||
Application.Run(frmMain = new FrmMain()); | |||
} | |||
static void CopyWebUI() | |||
{ | |||
CopyDirectory("../../webui", "./webui"); | |||
} | |||
static void CopyDirectory(string src, string dest) | |||
{ | |||
if (!Directory.Exists(dest)) | |||
{ | |||
Directory.CreateDirectory(dest); | |||
} | |||
foreach (var srcfile in Directory.GetFiles(src)) | |||
{ | |||
var destfile = Path.Combine(dest, Path.GetFileName(srcfile)); | |||
//只复制更新的文件 | |||
if (File.Exists(destfile) && File.GetLastWriteTime(srcfile) == File.GetLastWriteTime(destfile)) | |||
{ | |||
continue; | |||
} | |||
File.Copy(srcfile, destfile, true); | |||
} | |||
foreach (var srcdir in Directory.GetDirectories(src)) | |||
{ | |||
var destdir = Path.Combine(dest, Path.GetFileName(srcdir)); | |||
CopyDirectory(srcdir, destdir); | |||
} | |||
} | |||
} | |||
} |
@ -0,0 +1,36 @@ | |||
using System.Reflection; | |||
using System.Runtime.CompilerServices; | |||
using System.Runtime.InteropServices; | |||
// 有关程序集的一般信息由以下 | |||
// 控制。更改这些特性值可修改 | |||
// 与程序集关联的信息。 | |||
[assembly: AssemblyTitle("AppTime")] | |||
[assembly: AssemblyDescription("")] | |||
[assembly: AssemblyConfiguration("")] | |||
[assembly: AssemblyCompany("")] | |||
[assembly: AssemblyProduct("AppTime")] | |||
[assembly: AssemblyCopyright("Copyright © 2017")] | |||
[assembly: AssemblyTrademark("")] | |||
[assembly: AssemblyCulture("")] | |||
// 将 ComVisible 设置为 false 会使此程序集中的类型 | |||
//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型 | |||
//请将此类型的 ComVisible 特性设置为 true。 | |||
[assembly: ComVisible(false)] | |||
// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID | |||
[assembly: Guid("4892e56b-5942-444b-b41c-1e4f22a3752a")] | |||
// 程序集的版本信息由下列四个值组成: | |||
// | |||
// 主版本 | |||
// 次版本 | |||
// 生成号 | |||
// 修订号 | |||
// | |||
// 可以指定所有值,也可以使用以下所示的 "*" 预置版本号和修订号 | |||
// 方法是按如下所示使用“*”: : | |||
// [assembly: AssemblyVersion("1.0.*")] | |||
[assembly: AssemblyVersion("0.0.0.0")] | |||
[assembly: AssemblyFileVersion("0.12.0.0")] |
@ -0,0 +1,63 @@ | |||
//------------------------------------------------------------------------------ | |||
// <auto-generated> | |||
// 此代码由工具生成。 | |||
// 运行时版本:4.0.30319.42000 | |||
// | |||
// 对此文件的更改可能会导致不正确的行为,并且如果 | |||
// 重新生成代码,这些更改将会丢失。 | |||
// </auto-generated> | |||
//------------------------------------------------------------------------------ | |||
namespace AppTime.Properties { | |||
using System; | |||
/// <summary> | |||
/// 一个强类型的资源类,用于查找本地化的字符串等。 | |||
/// </summary> | |||
// 此类是由 StronglyTypedResourceBuilder | |||
// 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。 | |||
// 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen | |||
// (以 /str 作为命令选项),或重新生成 VS 项目。 | |||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] | |||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] | |||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] | |||
internal class Resources { | |||
private static global::System.Resources.ResourceManager resourceMan; | |||
private static global::System.Globalization.CultureInfo resourceCulture; | |||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] | |||
internal Resources() { | |||
} | |||
/// <summary> | |||
/// 返回此类使用的缓存的 ResourceManager 实例。 | |||
/// </summary> | |||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] | |||
internal static global::System.Resources.ResourceManager ResourceManager { | |||
get { | |||
if (object.ReferenceEquals(resourceMan, null)) { | |||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AppTime.Properties.Resources", typeof(Resources).Assembly); | |||
resourceMan = temp; | |||
} | |||
return resourceMan; | |||
} | |||
} | |||
/// <summary> | |||
/// 重写当前线程的 CurrentUICulture 属性 | |||
/// 重写当前线程的 CurrentUICulture 属性。 | |||
/// </summary> | |||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] | |||
internal static global::System.Globalization.CultureInfo Culture { | |||
get { | |||
return resourceCulture; | |||
} | |||
set { | |||
resourceCulture = value; | |||
} | |||
} | |||
} | |||
} |
@ -0,0 +1,117 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<root> | |||
<!-- | |||
Microsoft ResX Schema | |||
Version 2.0 | |||
The primary goals of this format is to allow a simple XML format | |||
that is mostly human readable. The generation and parsing of the | |||
various data types are done through the TypeConverter classes | |||
associated with the data types. | |||
Example: | |||
... ado.net/XML headers & schema ... | |||
<resheader name="resmimetype">text/microsoft-resx</resheader> | |||
<resheader name="version">2.0</resheader> | |||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> | |||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> | |||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> | |||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> | |||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> | |||
<value>[base64 mime encoded serialized .NET Framework object]</value> | |||
</data> | |||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> | |||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> | |||
<comment>This is a comment</comment> | |||
</data> | |||
There are any number of "resheader" rows that contain simple | |||
name/value pairs. | |||
Each data row contains a name, and value. The row also contains a | |||
type or mimetype. Type corresponds to a .NET class that support | |||
text/value conversion through the TypeConverter architecture. | |||
Classes that don't support this are serialized and stored with the | |||
mimetype set. | |||
The mimetype is used for serialized objects, and tells the | |||
ResXResourceReader how to depersist the object. This is currently not | |||
extensible. For a given mimetype the value must be set accordingly: | |||
Note - application/x-microsoft.net.object.binary.base64 is the format | |||
that the ResXResourceWriter will generate, however the reader can | |||
read any of the formats listed below. | |||
mimetype: application/x-microsoft.net.object.binary.base64 | |||
value : The object must be serialized with | |||
: System.Serialization.Formatters.Binary.BinaryFormatter | |||
: and then encoded with base64 encoding. | |||
mimetype: application/x-microsoft.net.object.soap.base64 | |||
value : The object must be serialized with | |||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter | |||
: and then encoded with base64 encoding. | |||
mimetype: application/x-microsoft.net.object.bytearray.base64 | |||
value : The object must be serialized into a byte array | |||
: using a System.ComponentModel.TypeConverter | |||
: and then encoded with base64 encoding. | |||
--> | |||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> | |||
<xsd:element name="root" msdata:IsDataSet="true"> | |||
<xsd:complexType> | |||
<xsd:choice maxOccurs="unbounded"> | |||
<xsd:element name="metadata"> | |||
<xsd:complexType> | |||
<xsd:sequence> | |||
<xsd:element name="value" type="xsd:string" minOccurs="0" /> | |||
</xsd:sequence> | |||
<xsd:attribute name="name" type="xsd:string" /> | |||
<xsd:attribute name="type" type="xsd:string" /> | |||
<xsd:attribute name="mimetype" type="xsd:string" /> | |||
</xsd:complexType> | |||
</xsd:element> | |||
<xsd:element name="assembly"> | |||
<xsd:complexType> | |||
<xsd:attribute name="alias" type="xsd:string" /> | |||
<xsd:attribute name="name" type="xsd:string" /> | |||
</xsd:complexType> | |||
</xsd:element> | |||
<xsd:element name="data"> | |||
<xsd:complexType> | |||
<xsd:sequence> | |||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> | |||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> | |||
</xsd:sequence> | |||
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" /> | |||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> | |||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> | |||
</xsd:complexType> | |||
</xsd:element> | |||
<xsd:element name="resheader"> | |||
<xsd:complexType> | |||
<xsd:sequence> | |||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> | |||
</xsd:sequence> | |||
<xsd:attribute name="name" type="xsd:string" use="required" /> | |||
</xsd:complexType> | |||
</xsd:element> | |||
</xsd:choice> | |||
</xsd:complexType> | |||
</xsd:element> | |||
</xsd:schema> | |||
<resheader name="resmimetype"> | |||
<value>text/microsoft-resx</value> | |||
</resheader> | |||
<resheader name="version"> | |||
<value>2.0</value> | |||
</resheader> | |||
<resheader name="reader"> | |||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> | |||
</resheader> | |||
<resheader name="writer"> | |||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> | |||
</resheader> | |||
</root> |
@ -0,0 +1,62 @@ | |||
//------------------------------------------------------------------------------ | |||
// <auto-generated> | |||
// 此代码由工具生成。 | |||
// 运行时版本:4.0.30319.42000 | |||
// | |||
// 对此文件的更改可能会导致不正确的行为,并且如果 | |||
// 重新生成代码,这些更改将会丢失。 | |||
// </auto-generated> | |||
//------------------------------------------------------------------------------ | |||
namespace AppTime.Properties { | |||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] | |||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.8.1.0")] | |||
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { | |||
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); | |||
public static Settings Default { | |||
get { | |||
return defaultInstance; | |||
} | |||
} | |||
[global::System.Configuration.UserScopedSettingAttribute()] | |||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] | |||
[global::System.Configuration.DefaultSettingValueAttribute("")] | |||
public string DataPath { | |||
get { | |||
return ((string)(this["DataPath"])); | |||
} | |||
set { | |||
this["DataPath"] = value; | |||
} | |||
} | |||
[global::System.Configuration.UserScopedSettingAttribute()] | |||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] | |||
[global::System.Configuration.DefaultSettingValueAttribute("30")] | |||
public int RecordScreenDays { | |||
get { | |||
return ((int)(this["RecordScreenDays"])); | |||
} | |||
set { | |||
this["RecordScreenDays"] = value; | |||
} | |||
} | |||
[global::System.Configuration.UserScopedSettingAttribute()] | |||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] | |||
[global::System.Configuration.DefaultSettingValueAttribute("50")] | |||
public int ImageQuality { | |||
get { | |||
return ((int)(this["ImageQuality"])); | |||
} | |||
set { | |||
this["ImageQuality"] = value; | |||
} | |||
} | |||
} | |||
} |
@ -0,0 +1,15 @@ | |||
<?xml version='1.0' encoding='utf-8'?> | |||
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)" GeneratedClassNamespace="AppTime.Properties" GeneratedClassName="Settings"> | |||
<Profiles /> | |||
<Settings> | |||
<Setting Name="DataPath" Type="System.String" Scope="User"> | |||
<Value Profile="(Default)" /> | |||
</Setting> | |||
<Setting Name="RecordScreenDays" Type="System.Int32" Scope="User"> | |||
<Value Profile="(Default)">30</Value> | |||
</Setting> | |||
<Setting Name="ImageQuality" Type="System.Int32" Scope="User"> | |||
<Value Profile="(Default)">50</Value> | |||
</Setting> | |||
</Settings> | |||
</SettingsFile> |
@ -0,0 +1,69 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1"> | |||
<assemblyIdentity version="1.0.0.0" name="MyApplication.app" /> | |||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2"> | |||
<security> | |||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3"> | |||
<!-- UAC 清单选项 | |||
如果想要更改 Windows 用户帐户控制级别,请使用 | |||
以下节点之一替换 requestedExecutionLevel 节点。n | |||
<requestedExecutionLevel level="asInvoker" uiAccess="false" /> | |||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" /> | |||
<requestedExecutionLevel level="highestAvailable" uiAccess="false" /> | |||
指定 requestedExecutionLevel 元素将禁用文件和注册表虚拟化。 | |||
如果你的应用程序需要此虚拟化来实现向后兼容性,则删除此 | |||
元素。 | |||
--> | |||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" /> | |||
</requestedPrivileges> | |||
<applicationRequestMinimum> | |||
<defaultAssemblyRequest permissionSetReference="Custom" /> | |||
<PermissionSet class="System.Security.PermissionSet" version="1" ID="Custom" SameSite="site" Unrestricted="true" /> | |||
</applicationRequestMinimum> | |||
</security> | |||
</trustInfo> | |||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> | |||
<application> | |||
<!-- 设计此应用程序与其一起工作且已针对此应用程序进行测试的 | |||
Windows 版本的列表。取消评论适当的元素, | |||
Windows 将自动选择最兼容的环境。 --> | |||
<!-- Windows Vista --> | |||
<!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />--> | |||
<!-- Windows 7 --> | |||
<!--<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />--> | |||
<!-- Windows 8 --> | |||
<!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />--> | |||
<!-- Windows 8.1 --> | |||
<!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />--> | |||
<!-- Windows 10 --> | |||
<!--<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />--> | |||
</application> | |||
</compatibility> | |||
<!-- 指示该应用程序可以感知 DPI 且 Windows 在 DPI 较高时将不会对其进行 | |||
自动缩放。Windows Presentation Foundation (WPF)应用程序自动感知 DPI,无需 | |||
选择加入。选择加入此设置的 Windows 窗体应用程序(目标设定为 .NET Framework 4.6 )还应 | |||
在其 app.config 中将 "EnableWindowsFormsHighDpiAutoResizing" 设置设置为 "true"。--> | |||
<!-- | |||
<application xmlns="urn:schemas-microsoft-com:asm.v3"> | |||
<windowsSettings> | |||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware> | |||
</windowsSettings> | |||
</application> | |||
--> | |||
<!-- 启用 Windows 公共控件和对话框的主题(Windows XP 和更高版本) --> | |||
<!-- | |||
<dependency> | |||
<dependentAssembly> | |||
<assemblyIdentity | |||
type="win32" | |||
name="Microsoft.Windows.Common-Controls" | |||
version="6.0.0.0" | |||
processorArchitecture="*" | |||
publicKeyToken="6595b64144ccf1df" | |||
language="*" | |||
/> | |||
</dependentAssembly> | |||
</dependency> | |||
--> | |||
</assembly> |
@ -0,0 +1,418 @@ | |||
| |||
using AppTime.Properties; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.ComponentModel; | |||
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 System.Windows.Forms; | |||
namespace AppTime | |||
{ | |||
class Recorder | |||
{ | |||
public const string ExName = "mkv"; | |||
public Recorder() | |||
{ | |||
BuildDataPath(); | |||
} | |||
/// <summary> | |||
/// 统计周期。 | |||
/// </summary> | |||
public int IntervalMs = 1000; | |||
public void Start() | |||
{ | |||
new Thread(RecorderThreadProc) { IsBackground = true }.Start(); | |||
} | |||
class App | |||
{ | |||
public string WinText; | |||
public string AppProcess; | |||
public DateTime TimeStart; | |||
public long WinId; | |||
} | |||
public void BuildDataPath() | |||
{ | |||
Directory.CreateDirectory(IconPath); | |||
Directory.CreateDirectory(ScreenPath); | |||
} | |||
Dictionary<int, Process> processes; | |||
public Process GetProcess(int processID) | |||
{ | |||
if (processes != null && processes.TryGetValue(processID, out var p)) | |||
{ | |||
return p; | |||
} | |||
processes = Process.GetProcesses().ToDictionary(p => p.Id); | |||
return processes[processID]; | |||
} | |||
class app | |||
{ | |||
public long id; | |||
public string process; | |||
public Dictionary<string, win> wins = new Dictionary<string, win>(); | |||
} | |||
class win | |||
{ | |||
public long id; | |||
public string text; | |||
} | |||
long nextAppId = 0; | |||
Icon GetIcon(string fileName, bool largeIcon) | |||
{ | |||
var shfi = new SHFILEINFO(); | |||
WinApi.SHGetFileInfo(fileName, 0, ref shfi, | |||
(uint)Marshal.SizeOf(shfi), | |||
(uint)FileInfoFlags.SHGFI_ICON | (uint)FileInfoFlags.SHGFI_USEFILEATTRIBUTES | |||
| (uint)(largeIcon ? FileInfoFlags.SHGFI_LARGEICON : FileInfoFlags.SHGFI_SMALLICON) | |||
); | |||
return Icon.FromHandle(shfi.hIcon); | |||
} | |||
void SaveIcon(Icon icon, string filename) | |||
{ | |||
using var img = icon.ToBitmap(); | |||
img.Save(filename); | |||
} | |||
public string GetIconPath(long appId, bool large) | |||
{ | |||
return Path.Combine(IconPath, $"{appId}{(large ? "l" : "s")}.png"); | |||
} | |||
Dictionary<string, app> apps = new Dictionary<string, app>(); | |||
app GetApp(Process process) | |||
{ | |||
var name = process.ProcessName; | |||
if (apps.TryGetValue(name, out var app)) | |||
{ | |||
return app; | |||
} | |||
var data = db.ExecuteDynamic( | |||
@"select id from app where process = @process", | |||
new SQLiteParameter("process", name) | |||
).FirstOrDefault(); | |||
if (data == null) | |||
{ | |||
if (nextAppId == 0) | |||
{ | |||
nextAppId = (int)(long)db.ExecuteData("select ifnull(max(id),0) + 1 from app")[0][0]; | |||
} | |||
var text = ""; | |||
try | |||
{ | |||
text = process.MainModule.FileVersionInfo.FileDescription; | |||
using var iconl = GetIcon(process.MainModule.FileName, true); | |||
SaveIcon(iconl, GetIconPath(nextAppId, true)); | |||
using var icons = GetIcon(process.MainModule.FileName, false); | |||
SaveIcon(icons, GetIconPath(nextAppId, false)); | |||
} | |||
catch (Win32Exception) | |||
{ | |||
//ignore | |||
} | |||
catch (FileNotFoundException) | |||
{ | |||
//ignore | |||
} | |||
if (string.IsNullOrWhiteSpace(text)) | |||
{ | |||
text = process.ProcessName; | |||
} | |||
db.Execute( | |||
"insert into app (id, process, text, tagId) values(@id, @process, @text, 0)", | |||
new SQLiteParameter("id", nextAppId), | |||
new SQLiteParameter("process", name), | |||
new SQLiteParameter("text", text) | |||
); | |||
app = new app { id = nextAppId, process = name }; | |||
nextAppId++; | |||
} | |||
else | |||
{ | |||
app = new app | |||
{ | |||
id = data.id, | |||
process = name | |||
}; | |||
} | |||
apps.Add(name, app); | |||
//fix icons | |||
var largeIconPath = GetIconPath(app.id, true); | |||
if (!File.Exists(largeIconPath)) | |||
{ | |||
try | |||
{ | |||
using var iconl = GetIcon(process.MainModule.FileName, true); | |||
SaveIcon(iconl, largeIconPath); | |||
using var icons = GetIcon(process.MainModule.FileName, false); | |||
SaveIcon(icons, GetIconPath(app.id, false)); | |||
} | |||
catch(Win32Exception) | |||
{ | |||
} | |||
} | |||
return app; | |||
} | |||
long nextWinId = 0; | |||
win GetWin(Process process, string winText) | |||
{ | |||
var app = GetApp(process); | |||
if (app.wins.TryGetValue(winText, out var win)) | |||
{ | |||
return win; | |||
} | |||
var data = db.ExecuteDynamic( | |||
"select id from win where appid=@appid and text=@winText", | |||
new SQLiteParameter("appid", app.id), | |||
new SQLiteParameter("winText", winText) | |||
).FirstOrDefault(); | |||
if (data == null) | |||
{ | |||
if (nextWinId == 0) | |||
{ | |||
nextWinId = (int)(long)db.ExecuteData("select ifnull(max(id),0) + 1 from win")[0][0]; | |||
} | |||
db.Execute( | |||
"insert into win (id, appId, text) values(@id, @appId, @text)", | |||
new SQLiteParameter("id", nextWinId), | |||
new SQLiteParameter("appId", app.id), | |||
new SQLiteParameter("text", winText) | |||
); | |||
win = new win { id = nextWinId, text = winText }; | |||
nextWinId++; | |||
} | |||
else | |||
{ | |||
win = new win | |||
{ | |||
id = data.id, | |||
text = winText | |||
}; | |||
} | |||
app.wins.Add(winText, win); | |||
return win; | |||
} | |||
DB db = DB.Instance; | |||
public void RecorderThreadProc() | |||
{ | |||
App lastApp = null; | |||
while (true) | |||
{ | |||
var now = DateTime.Now; | |||
var hwnd = WinApi.GetForegroundWindow(); | |||
var text = new StringBuilder(255); | |||
WinApi.GetWindowText(hwnd, text, 255); | |||
var winText = text.ToString(); | |||
WinApi.GetWindowThreadProcessId(hwnd, out var processid); | |||
var process = GetProcess(processid); | |||
var appname = process.ProcessName; | |||
if (lastApp != null) | |||
{ | |||
db.Execute( | |||
"update period set timeend = @v1 where timestart = @v0", | |||
lastApp.TimeStart, | |||
now.AddMilliseconds(-1)//必须减小,否则可能与下个周期开始时间重叠 | |||
); | |||
} | |||
if (lastApp == null || lastApp.AppProcess != appname || lastApp.WinText != winText) | |||
{ | |||
var win = GetWin(process, winText); | |||
lastApp = new App { WinId = win.id, AppProcess = appname, TimeStart = now, WinText = winText }; | |||
db.Execute( | |||
"insert into [period](winid, timeStart, timeEnd) values(@v0, @v1, @v1)", | |||
win.id, now | |||
); | |||
} | |||
Screenshot(now); | |||
//等到下一个周期 | |||
var nextTime = now.AddMilliseconds(IntervalMs); | |||
now = DateTime.Now; | |||
if(nextTime > now) | |||
{ | |||
Thread.Sleep(nextTime - now); | |||
} | |||
} | |||
} | |||
string DataPath => string.IsNullOrWhiteSpace(Settings.Default.DataPath) ? Application.StartupPath : Settings.Default.DataPath; | |||
public string ScreenPath => Path.Combine(DataPath, "images"); | |||
public string IconPath => Path.Combine(DataPath, "icons"); | |||
ImageCodecInfo jpgcodec = ImageCodecInfo.GetImageDecoders().First(codec => codec.MimeType == "image/jpeg"); | |||
///// <summary> | |||
///// 获取图片文件路径 | |||
///// </summary> | |||
///// <param name="timeStart"></param> | |||
///// <param name="timeImage"></param> | |||
///// <returns></returns> | |||
//public string getImageFile(DateTime timeStart, DateTime timeImage) | |||
//{ | |||
// var folder = Path.Combine(ScreenPath, timeImage.ToString("yyyyMMdd")); | |||
// var filename = $"{timeStart:HHmmss}+{Math.Round((timeImage - timeStart).TotalSeconds)}"; | |||
// return Path.Combine(folder, $"{filename}.jpg"); | |||
//} | |||
public string getFileName(DateTime time) | |||
{ | |||
return Path.Combine(ScreenPath, $"{time:yyyyMMdd}", $"{time:HHmmss}." + Recorder.ExName); | |||
} | |||
DateTime lastCheck = DateTime.MinValue.Date; | |||
/// <summary> | |||
/// 截图 | |||
/// </summary> | |||
/// <param name="now"></param> | |||
/// <param name="lastApp"></param> | |||
void Screenshot(DateTime now) | |||
{ | |||
//检查记录天数限制 | |||
if (lastCheck != now.Date) | |||
{ | |||
var firstDate = now.Date.AddDays(-Settings.Default.RecordScreenDays); | |||
var dirs = Directory.EnumerateDirectories(ScreenPath, "????????"); | |||
foreach (var i in dirs) | |||
{ | |||
if (DateTime.TryParseExact(Path.GetFileName(i), "yyyyMMdd", CultureInfo.CurrentCulture, DateTimeStyles.None, out var date)) | |||
{ | |||
if (date < firstDate) | |||
{ | |||
Directory.Delete(i); | |||
} | |||
} | |||
} | |||
lastCheck = now.Date; | |||
} | |||
if (Settings.Default.RecordScreenDays == 0) | |||
{ | |||
return; | |||
} | |||
if (buffer == null) | |||
{ | |||
buffer = new MemoryBuffer(now); | |||
} | |||
using var img = GetScreen(); | |||
using var mem = new MemoryStream(); | |||
img.Save(mem, ImageFormat.Jpeg); | |||
buffer.Frames.Add(new Frame(now - buffer.StartTime, mem.ToArray())); | |||
if ((now - buffer.StartTime).TotalSeconds >= 10 * 60 || now.Date != buffer.StartTime.Date) //固定为10mins,防止保存时间长,减少出问题时影响的时长。 | |||
{ | |||
FlushScreenBuffer(); | |||
} | |||
} | |||
public void FlushScreenBuffer() | |||
{ | |||
//切换到新buffer | |||
var newBuffer = new MemoryBuffer(DateTime.Now); | |||
var b = buffer; | |||
buffer = newBuffer; | |||
//加入flushing | |||
lock (flushing) | |||
{ | |||
flushing.Add(b); | |||
} | |||
var path = getFileName(b.StartTime); | |||
var folder = Path.GetDirectoryName(path); | |||
if (!Directory.Exists(folder)) | |||
{ | |||
Directory.CreateDirectory(folder); | |||
} | |||
new Thread(() => | |||
{ | |||
Ffmpeg.Save(getFileName(b.StartTime), b.Frames.ToArray()); | |||
lock (flushing) | |||
{ | |||
flushing.Remove(b); | |||
} | |||
}) | |||
{ | |||
Priority = ThreadPriority.BelowNormal, | |||
IsBackground = false | |||
}.Start(); | |||
} | |||
public class MemoryBuffer | |||
{ | |||
public readonly DateTime StartTime; | |||
public readonly List<Frame> Frames = new List<Frame>(); | |||
public MemoryBuffer(DateTime startTime) | |||
{ | |||
StartTime = startTime; | |||
} | |||
} | |||
public MemoryBuffer buffer; | |||
public List<MemoryBuffer> flushing = new List<MemoryBuffer>(); | |||
Bitmap GetScreen() | |||
{ | |||
var result = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height); | |||
using var g = Graphics.FromImage(result); | |||
retry: | |||
try | |||
{ | |||
g.CopyFromScreen(0, 0, 0, 0, Screen.PrimaryScreen.Bounds.Size); | |||
} | |||
catch | |||
{ | |||
goto retry; | |||
} | |||
return result; | |||
} | |||
} | |||
} |
@ -0,0 +1,64 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
namespace AppTime | |||
{ | |||
static class Utils | |||
{ | |||
public static T CheckTimeout<T>(Func<T> act, Func<Thread, T> whenTimeout, int timeoutMs, bool abortThread = true) | |||
{ | |||
var isTimeout = true; | |||
Exception ex = null; | |||
var t = new Thread(() => | |||
{ | |||
try | |||
{ | |||
act(); | |||
isTimeout = false; | |||
} | |||
catch (Exception e) | |||
{ | |||
ex = e; | |||
} | |||
}); | |||
t.Start(); | |||
t.Join(timeoutMs); | |||
if (isTimeout) | |||
{ | |||
if (abortThread) | |||
{ | |||
t.Abort(); | |||
} | |||
return whenTimeout(t); | |||
} | |||
if (ex != null) | |||
{ | |||
throw ex; | |||
} | |||
return default(T); | |||
} | |||
public static Exception Try(Action act) | |||
{ | |||
try | |||
{ | |||
act(); | |||
return null; | |||
} | |||
catch (Exception ex) | |||
{ | |||
return ex; | |||
} | |||
} | |||
} | |||
} |
@ -0,0 +1,178 @@ | |||
using Newtonsoft.Json; | |||
using Newtonsoft.Json.Linq; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Diagnostics; | |||
using System.IO; | |||
using System.Linq; | |||
using System.Net; | |||
using System.Text; | |||
using System.Threading; | |||
using System.Web; | |||
using System.Windows.Forms; | |||
namespace AppTime | |||
{ | |||
class WebServer | |||
{ | |||
HttpListener listener; | |||
Thread thread; | |||
//public HttpListenerRequest Request; | |||
//public HttpListenerResponse Response; | |||
public string WebRootPath; | |||
HashSet<string> defaultPage = new HashSet<string>(StringComparer.OrdinalIgnoreCase){ | |||
"index.html", | |||
"index.htm", | |||
"default.html", | |||
"default.htm" | |||
}; | |||
public void Start(int port, object controller, string webfolder = "web") | |||
{ | |||
thread = new Thread(() => | |||
{ | |||
WebRootPath = webfolder = Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), webfolder); | |||
var folders = Directory.GetDirectories(webfolder, "*", SearchOption.AllDirectories); | |||
var webroot = $@"http://localhost:{port}/"; | |||
listener = new HttpListener(); | |||
foreach (var f in folders) | |||
{ | |||
listener.Prefixes.Add(webroot + f.Substring(webfolder.Length + 1).Replace("\\", "/") + "/"); | |||
} | |||
listener.Prefixes.Add(webroot); | |||
listener.Start(); | |||
while (true) | |||
{ | |||
var ctx = listener.GetContext(); | |||
//Debug.WriteLine($"Web Request:{ctx.Request.Url}"); | |||
//ThreadPool.QueueUserWorkItem(_ => processRequest(ctx, webfolder, controller)); | |||
new Thread(() => processRequest( | |||
ctx, | |||
webfolder, | |||
controller) | |||
) | |||
{ IsBackground = true, Name = "WebServer Process Request" }.Start(); | |||
} | |||
}) | |||
{ | |||
Name = "WebServer", | |||
IsBackground = true | |||
}; | |||
thread.Start(); | |||
} | |||
void processRequest(HttpListenerContext context, string webfolder, object controller) | |||
{ | |||
var request = context.Request; | |||
var file = webfolder + request.RawUrl.Replace("/", "\\"); | |||
string query = ""; | |||
var p = file.IndexOf("?"); | |||
if (p > 0) | |||
{ | |||
query = file.Substring(p + 1); | |||
file = file.Substring(0, p); | |||
} | |||
var response = context.Response; | |||
byte[] data = null; | |||
if (Path.GetFileName(Path.GetDirectoryName(file)).Equals("data", StringComparison.OrdinalIgnoreCase)) | |||
{ | |||
try | |||
{ | |||
if (string.IsNullOrWhiteSpace(query)) | |||
{ | |||
using var reader = new StreamReader(request.InputStream); | |||
query = reader.ReadToEnd(); | |||
} | |||
var info = JObject.Parse(HttpUtility.UrlDecode(query)); | |||
var method = controller.GetType().GetMethod(Path.GetFileName(file)); | |||
var args = info.Value<JArray>("args").Cast<object>().ToArray(); | |||
var @params = method.GetParameters(); | |||
for (var i = 0; i < args.Length; i++) | |||
{ | |||
if (args[i] is JObject jo) | |||
{ | |||
args[i] = jo.ToObject(@params[i].ParameterType); | |||
} | |||
else | |||
{ | |||
args[i] = Convert.ChangeType(args[i], @params[i].ParameterType); | |||
} | |||
} | |||
var result = method.Invoke(controller, args); | |||
if (result is byte[] bytes) | |||
{ | |||
data = bytes; | |||
} | |||
else | |||
{ | |||
data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(result)); | |||
} | |||
} | |||
catch (Exception ex) | |||
{ | |||
data = Encoding.UTF8.GetBytes($"bad request: method:{Path.GetFileName(file)} query:{query}"); | |||
} | |||
} | |||
else | |||
{ | |||
if (Directory.Exists(file)) | |||
{ | |||
var def = Directory.GetFiles(file).FirstOrDefault(f => defaultPage.Contains(Path.GetFileName(f), StringComparer.OrdinalIgnoreCase)); | |||
if (def != null) | |||
{ | |||
file = def; | |||
} | |||
} | |||
if (File.Exists(file)) | |||
{ | |||
var mime = MimeMapping.GetMimeMapping(file); | |||
response.Headers.Add("Content-type", mime); | |||
if (mime.StartsWith("text/")) | |||
{ | |||
response.ContentEncoding = Encoding.GetEncoding("utf-8"); | |||
data = Encoding.UTF8.GetBytes(File.ReadAllText(file, Encoding.Default)); | |||
} | |||
else | |||
{ | |||
data = File.ReadAllBytes(file); | |||
} | |||
} | |||
} | |||
if (data == null) | |||
{ | |||
data = Encoding.UTF8.GetBytes($"file not found: {file}"); | |||
} | |||
response.ContentLength64 = data.Length; | |||
var output = response.OutputStream; | |||
try | |||
{ | |||
output.Write(data, 0, data.Length); | |||
} | |||
catch (HttpListenerException ex) | |||
{ | |||
} | |||
output.Close(); | |||
} | |||
public void Stop() | |||
{ | |||
thread.Abort(); | |||
listener.Stop(); | |||
listener.Close(); | |||
} | |||
} | |||
} |
@ -0,0 +1,54 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<configuration> | |||
<configSections> | |||
<sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | |||
<section name="AppTime.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" /> | |||
</sectionGroup> | |||
</configSections> | |||
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" /></startup> | |||
<runtime> | |||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> | |||
<dependentAssembly> | |||
<assemblyIdentity name="System.Numerics.Vectors" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" /> | |||
<bindingRedirect oldVersion="0.0.0.0-4.1.4.0" newVersion="4.1.4.0" /> | |||
</dependentAssembly> | |||
<dependentAssembly> | |||
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" /> | |||
<bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="5.0.0.0" /> | |||
</dependentAssembly> | |||
<dependentAssembly> | |||
<assemblyIdentity name="System.Buffers" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" /> | |||
<bindingRedirect oldVersion="0.0.0.0-4.0.3.0" newVersion="4.0.3.0" /> | |||
</dependentAssembly> | |||
<dependentAssembly> | |||
<assemblyIdentity name="System.ValueTuple" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" /> | |||
<bindingRedirect oldVersion="0.0.0.0-4.0.3.0" newVersion="4.0.3.0" /> | |||
</dependentAssembly> | |||
<dependentAssembly> | |||
<assemblyIdentity name="SQLitePCLRaw.core" publicKeyToken="1488e028ca7ab535" culture="neutral" /> | |||
<bindingRedirect oldVersion="0.0.0.0-2.0.3.851" newVersion="2.0.3.851" /> | |||
</dependentAssembly> | |||
<dependentAssembly> | |||
<assemblyIdentity name="Accord" publicKeyToken="fa1a88e29555ccf7" culture="neutral" /> | |||
<bindingRedirect oldVersion="0.0.0.0-3.8.2.0" newVersion="3.8.2.0" /> | |||
</dependentAssembly> | |||
<dependentAssembly> | |||
<assemblyIdentity name="Accord.Video" publicKeyToken="fa1a88e29555ccf7" culture="neutral" /> | |||
<bindingRedirect oldVersion="0.0.0.0-3.8.2.0" newVersion="3.8.2.0" /> | |||
</dependentAssembly> | |||
</assemblyBinding> | |||
</runtime> | |||
<userSettings> | |||
<AppTime.Properties.Settings> | |||
<setting name="DataPath" serializeAs="String"> | |||
<value /> | |||
</setting> | |||
<setting name="RecordScreenDays" serializeAs="String"> | |||
<value>30</value> | |||
</setting> | |||
<setting name="ImageQuality" serializeAs="String"> | |||
<value>50</value> | |||
</setting> | |||
</AppTime.Properties.Settings> | |||
</userSettings> | |||
</configuration> |
@ -0,0 +1,11 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1"> | |||
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/> | |||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2"> | |||
<security> | |||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3"> | |||
<requestedExecutionLevel level="asInvoker" uiAccess="false" /> | |||
</requestedPrivileges> | |||
</security> | |||
</trustInfo> | |||
</assembly> |
@ -0,0 +1,4 @@ | |||
// <autogenerated /> | |||
using System; | |||
using System.Reflection; | |||
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] |
@ -0,0 +1,7 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<packages> | |||
<package id="Newtonsoft.Json" version="12.0.3" targetFramework="net48" /> | |||
<package id="SQLitePCLRaw.core" version="1.1.14" targetFramework="net48" /> | |||
<package id="SQLitePCLRaw.provider.e_sqlite3.net45" version="1.1.14" targetFramework="net48" /> | |||
<package id="System.Data.SQLite.Core" version="1.0.113.1" targetFramework="net48" /> | |||
</packages> |
@ -0,0 +1,20 @@ | |||
The MIT License (MIT) | |||
Copyright (c) 2007 James Newton-King | |||
Permission is hereby granted, free of charge, to any person obtaining a copy of | |||
this software and associated documentation files (the "Software"), to deal in | |||
the Software without restriction, including without limitation the rights to | |||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of | |||
the Software, and to permit persons to whom the Software is furnished to do so, | |||
subject to the following conditions: | |||
The above copyright notice and this permission notice shall be included in all | |||
copies or substantial portions of the Software. | |||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS | |||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR | |||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER | |||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN | |||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |