@ -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. |