mirror of
https://github.com/BililiveRecorder/BililiveRecorder.git
synced 2024-11-16 11:42:22 +08:00
commit
69319b5d30
147
.editorconfig
147
.editorconfig
|
@ -7,3 +7,150 @@ root = true
|
|||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
charset = utf-8
|
||||
insert_final_newline = true
|
||||
|
||||
[*.cs]
|
||||
|
||||
#Core editorconfig formatting - indentation
|
||||
|
||||
#use soft tabs (spaces) for indentation
|
||||
indent_style = space
|
||||
|
||||
#Formatting - indentation options
|
||||
|
||||
#indent switch case contents.
|
||||
csharp_indent_case_contents = true
|
||||
#csharp_indent_case_contents_when_block
|
||||
csharp_indent_case_contents_when_block = true
|
||||
#indent switch labels
|
||||
csharp_indent_switch_labels = true
|
||||
|
||||
#Formatting - new line options
|
||||
|
||||
#place catch statements on a new line
|
||||
csharp_new_line_before_catch = true
|
||||
#place else statements on a new line
|
||||
csharp_new_line_before_else = true
|
||||
#require finally statements to be on a new line after the closing brace
|
||||
csharp_new_line_before_finally = true
|
||||
#require members of anonymous types to be on separate lines
|
||||
csharp_new_line_before_members_in_anonymous_types = true
|
||||
#require members of object intializers to be on separate lines
|
||||
csharp_new_line_before_members_in_object_initializers = true
|
||||
#require braces to be on a new line for anonymous_types, lambdas, properties, accessors, methods, object_collection_array_initializers, control_blocks, and types (also known as "Allman" style)
|
||||
csharp_new_line_before_open_brace = anonymous_types, lambdas, properties, accessors, methods, object_collection_array_initializers, control_blocks, types
|
||||
|
||||
#Formatting - organize using options
|
||||
|
||||
#place System.* using directives before other using directives
|
||||
dotnet_sort_system_directives_first = true
|
||||
|
||||
#Formatting - spacing options
|
||||
|
||||
#require NO space between a cast and the value
|
||||
csharp_space_after_cast = false
|
||||
#require a space before the colon for bases or interfaces in a type declaration
|
||||
csharp_space_after_colon_in_inheritance_clause = true
|
||||
#require a space after a keyword in a control flow statement such as a for loop
|
||||
csharp_space_after_keywords_in_control_flow_statements = true
|
||||
#require a space before the colon for bases or interfaces in a type declaration
|
||||
csharp_space_before_colon_in_inheritance_clause = true
|
||||
#remove space within empty argument list parentheses
|
||||
csharp_space_between_method_call_empty_parameter_list_parentheses = false
|
||||
#remove space between method call name and opening parenthesis
|
||||
csharp_space_between_method_call_name_and_opening_parenthesis = false
|
||||
#do not place space characters after the opening parenthesis and before the closing parenthesis of a method call
|
||||
csharp_space_between_method_call_parameter_list_parentheses = false
|
||||
#remove space within empty parameter list parentheses for a method declaration
|
||||
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
|
||||
#place a space character after the opening parenthesis and before the closing parenthesis of a method declaration parameter list.
|
||||
csharp_space_between_method_declaration_parameter_list_parentheses = false
|
||||
|
||||
#Formatting - wrapping options
|
||||
|
||||
#leave code block on single line
|
||||
csharp_preserve_single_line_blocks = true
|
||||
#leave statements and member declarations on the same line
|
||||
csharp_preserve_single_line_statements = true
|
||||
|
||||
#Style - Code block preferences
|
||||
|
||||
#prefer curly braces even for one line of code
|
||||
csharp_prefer_braces = true:suggestion
|
||||
|
||||
#Style - expression bodied member options
|
||||
|
||||
#prefer expression-bodied members for accessors
|
||||
csharp_style_expression_bodied_accessors = true:suggestion
|
||||
#prefer block bodies for constructors
|
||||
csharp_style_expression_bodied_constructors = false:suggestion
|
||||
#prefer expression-bodied members for indexers
|
||||
csharp_style_expression_bodied_indexers = true:suggestion
|
||||
#prefer block bodies for methods
|
||||
csharp_style_expression_bodied_methods = false:suggestion
|
||||
#prefer expression-bodied members for properties
|
||||
csharp_style_expression_bodied_properties = true:suggestion
|
||||
|
||||
#Style - expression level options
|
||||
|
||||
#prefer out variables to be declared inline in the argument list of a method call when possible
|
||||
csharp_style_inlined_variable_declaration = true:suggestion
|
||||
#prefer tuple names to ItemX properties
|
||||
dotnet_style_explicit_tuple_names = true:suggestion
|
||||
#prefer the language keyword for member access expressions, instead of the type name, for types that have a keyword to represent them
|
||||
dotnet_style_predefined_type_for_member_access = true:suggestion
|
||||
|
||||
#Style - Expression-level preferences
|
||||
|
||||
#prefer objects to be initialized using object initializers when possible
|
||||
dotnet_style_object_initializer = true:suggestion
|
||||
#prefer inferred anonymous type member names
|
||||
dotnet_style_prefer_inferred_anonymous_type_member_names = false:suggestion
|
||||
#prefer inferred tuple element names
|
||||
dotnet_style_prefer_inferred_tuple_names = true:suggestion
|
||||
|
||||
#Style - implicit and explicit types
|
||||
|
||||
#prefer var over explicit type in all cases, unless overridden by another code style rule
|
||||
csharp_style_var_elsewhere = true:suggestion
|
||||
#prefer var is used to declare variables with built-in system types such as int
|
||||
csharp_style_var_for_built_in_types = true:suggestion
|
||||
#prefer var when the type is already mentioned on the right-hand side of a declaration expression
|
||||
csharp_style_var_when_type_is_apparent = true:suggestion
|
||||
|
||||
#Style - language keyword and framework type options
|
||||
|
||||
#prefer the language keyword for local variables, method parameters, and class members, instead of the type name, for types that have a keyword to represent them
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
|
||||
|
||||
#Style - Miscellaneous preferences
|
||||
|
||||
#prefer local functions over anonymous functions
|
||||
csharp_style_pattern_local_over_anonymous_function = true:suggestion
|
||||
|
||||
#Style - modifier options
|
||||
|
||||
#prefer accessibility modifiers to be declared except for public interface members. This will currently not differ from always and will act as future proofing for if C# adds default interface methods.
|
||||
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
|
||||
|
||||
#Style - Modifier preferences
|
||||
|
||||
#when this rule is set to a list of modifiers, prefer the specified ordering.
|
||||
csharp_preferred_modifier_order = public,private,internal,protected,static,readonly,async,virtual,unsafe,override:suggestion
|
||||
|
||||
#Style - Pattern matching
|
||||
|
||||
#prefer pattern matching instead of is expression with type casts
|
||||
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
|
||||
|
||||
#Style - qualification options
|
||||
|
||||
#prefer events not to be prefaced with this. or Me. in Visual Basic
|
||||
dotnet_style_qualification_for_event = false:suggestion
|
||||
#prefer fields not to be prefaced with this. or Me. in Visual Basic
|
||||
dotnet_style_qualification_for_field = false:suggestion
|
||||
#prefer methods not to be prefaced with this. or Me. in Visual Basic
|
||||
dotnet_style_qualification_for_method = false:suggestion
|
||||
#prefer properties not to be prefaced with this. or Me. in Visual Basic
|
||||
dotnet_style_qualification_for_property = false:suggestion
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using BililiveRecorder.Core.Config;
|
||||
using BililiveRecorder.Core.Config;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
@ -84,8 +84,6 @@ namespace BililiveRecorder.Core
|
|||
{
|
||||
if (xmlWriter != null)
|
||||
{
|
||||
// TimeSpan diff = DateTimeOffset.UtcNow - offset;
|
||||
|
||||
switch (danmakuModel.MsgType)
|
||||
{
|
||||
case MsgTypeEnum.Comment:
|
||||
|
@ -93,13 +91,14 @@ namespace BililiveRecorder.Core
|
|||
var type = danmakuModel.RawObj?["info"]?[0]?[1]?.ToObject<int>() ?? 1;
|
||||
var size = danmakuModel.RawObj?["info"]?[0]?[2]?.ToObject<int>() ?? 25;
|
||||
var color = danmakuModel.RawObj?["info"]?[0]?[3]?.ToObject<int>() ?? 0XFFFFFF;
|
||||
long st = danmakuModel.RawObj?["info"]?[0]?[4]?.ToObject<long>() ?? 0L;
|
||||
var st = danmakuModel.RawObj?["info"]?[0]?[4]?.ToObject<long>() ?? 0L;
|
||||
var ts = Math.Max((DateTimeOffset.FromUnixTimeMilliseconds(st) - offset).TotalSeconds, 0d);
|
||||
|
||||
xmlWriter.WriteStartElement("d");
|
||||
xmlWriter.WriteAttributeString("p", $"{ts},{type},{size},{color},{st},0,{danmakuModel.UserID},0");
|
||||
xmlWriter.WriteAttributeString("user", danmakuModel.UserName);
|
||||
xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["info"]?.ToString(Newtonsoft.Json.Formatting.None));
|
||||
if (config.RecordDanmakuRaw)
|
||||
xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["info"]?.ToString(Newtonsoft.Json.Formatting.None));
|
||||
xmlWriter.WriteValue(danmakuModel.CommentText);
|
||||
xmlWriter.WriteEndElement();
|
||||
}
|
||||
|
@ -108,10 +107,13 @@ namespace BililiveRecorder.Core
|
|||
if (config.RecordDanmakuSuperChat)
|
||||
{
|
||||
xmlWriter.WriteStartElement("sc");
|
||||
var ts = Math.Max((DateTimeOffset.UtcNow - offset).TotalSeconds, 0d);
|
||||
xmlWriter.WriteAttributeString("ts", ts.ToString());
|
||||
xmlWriter.WriteAttributeString("user", danmakuModel.UserName);
|
||||
xmlWriter.WriteAttributeString("price", danmakuModel.Price.ToString());
|
||||
xmlWriter.WriteAttributeString("time", danmakuModel.SCKeepTime.ToString());
|
||||
xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None));
|
||||
if (config.RecordDanmakuRaw)
|
||||
xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None));
|
||||
xmlWriter.WriteValue(danmakuModel.CommentText);
|
||||
xmlWriter.WriteEndElement();
|
||||
}
|
||||
|
@ -120,10 +122,13 @@ namespace BililiveRecorder.Core
|
|||
if (config.RecordDanmakuGift)
|
||||
{
|
||||
xmlWriter.WriteStartElement("gift");
|
||||
var ts = Math.Max((DateTimeOffset.UtcNow - offset).TotalSeconds, 0d);
|
||||
xmlWriter.WriteAttributeString("ts", ts.ToString());
|
||||
xmlWriter.WriteAttributeString("user", danmakuModel.UserName);
|
||||
xmlWriter.WriteAttributeString("giftname", danmakuModel.GiftName);
|
||||
xmlWriter.WriteAttributeString("giftcount", danmakuModel.GiftCount.ToString());
|
||||
xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None));
|
||||
if (config.RecordDanmakuRaw)
|
||||
xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None));
|
||||
xmlWriter.WriteEndElement();
|
||||
}
|
||||
break;
|
||||
|
@ -131,10 +136,13 @@ namespace BililiveRecorder.Core
|
|||
if (config.RecordDanmakuGuard)
|
||||
{
|
||||
xmlWriter.WriteStartElement("guard");
|
||||
var ts = Math.Max((DateTimeOffset.UtcNow - offset).TotalSeconds, 0d);
|
||||
xmlWriter.WriteAttributeString("ts", ts.ToString());
|
||||
xmlWriter.WriteAttributeString("user", danmakuModel.UserName);
|
||||
xmlWriter.WriteAttributeString("level", danmakuModel.UserGuardLevel.ToString()); ;
|
||||
xmlWriter.WriteAttributeString("count", danmakuModel.GiftCount.ToString());
|
||||
xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None));
|
||||
if (config.RecordDanmakuRaw)
|
||||
xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None));
|
||||
xmlWriter.WriteEndElement();
|
||||
}
|
||||
break;
|
||||
|
|
|
@ -1,30 +1,40 @@
|
|||
using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BililiveRecorder.Core.Config;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
|
||||
namespace BililiveRecorder.Core
|
||||
{
|
||||
internal static class BililiveAPI
|
||||
public class BililiveAPI
|
||||
{
|
||||
private const string HTTP_HEADER_ACCEPT = "application/json, text/javascript, */*; q=0.01";
|
||||
private const string HTTP_HEADER_REFERER = "https://live.bilibili.com/";
|
||||
private const string DEFAULT_SERVER_HOST = "broadcastlv.chat.bilibili.com";
|
||||
private const int DEFAULT_SERVER_PORT = 2243;
|
||||
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
private static readonly Random random = new Random();
|
||||
private static readonly SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1);
|
||||
private static HttpClient httpclient;
|
||||
private static HttpClient danmakuhttpclient;
|
||||
internal static Config.ConfigV1 Config = null; // TODO: 以后有空把整个 class 改成非 static 的然后用 DI 获取 config
|
||||
|
||||
static BililiveAPI()
|
||||
private readonly ConfigV1 Config;
|
||||
private readonly HttpClient danmakuhttpclient;
|
||||
private HttpClient httpclient;
|
||||
|
||||
public BililiveAPI(ConfigV1 config)
|
||||
{
|
||||
Config = config;
|
||||
Config.PropertyChanged += (sender, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(Config.Cookie))
|
||||
{
|
||||
ApplyCookieSettings(Config.Cookie);
|
||||
}
|
||||
};
|
||||
|
||||
httpclient = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
|
||||
httpclient.DefaultRequestHeaders.Add("Accept", HTTP_HEADER_ACCEPT);
|
||||
httpclient.DefaultRequestHeaders.Add("Referer", HTTP_HEADER_REFERER);
|
||||
|
@ -36,11 +46,11 @@ namespace BililiveRecorder.Core
|
|||
danmakuhttpclient.DefaultRequestHeaders.Add("User-Agent", Utils.UserAgent);
|
||||
}
|
||||
|
||||
public static async Task ApplyCookieSettings(string cookie_string)
|
||||
public void ApplyCookieSettings(string cookie_string)
|
||||
{
|
||||
await semaphoreSlim.WaitAsync();
|
||||
try
|
||||
{
|
||||
logger.Trace("设置 Cookie 信息...");
|
||||
if (!string.IsNullOrWhiteSpace(cookie_string))
|
||||
{
|
||||
var pclient = new HttpClient(handler: new HttpClientHandler
|
||||
|
@ -65,15 +75,12 @@ namespace BililiveRecorder.Core
|
|||
cleanclient.DefaultRequestHeaders.Add("User-Agent", Utils.UserAgent);
|
||||
httpclient = cleanclient;
|
||||
}
|
||||
logger.Debug("设置 Cookie 成功");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "设置 Cookie 时发生错误");
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphoreSlim.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -83,9 +90,8 @@ namespace BililiveRecorder.Core
|
|||
/// <returns>数据</returns>
|
||||
/// <exception cref="ArgumentNullException"/>
|
||||
/// <exception cref="WebException"/>
|
||||
private static async Task<JObject> HttpGetJsonAsync(HttpClient client, string url)
|
||||
private async Task<JObject> HttpGetJsonAsync(HttpClient client, string url)
|
||||
{
|
||||
await semaphoreSlim.WaitAsync();
|
||||
try
|
||||
{
|
||||
var s = await client.GetStringAsync(url);
|
||||
|
@ -96,10 +102,6 @@ namespace BililiveRecorder.Core
|
|||
{
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphoreSlim.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -109,49 +111,20 @@ namespace BililiveRecorder.Core
|
|||
/// <returns>FLV播放地址</returns>
|
||||
/// <exception cref="WebException"/>
|
||||
/// <exception cref="Exception"/>
|
||||
public static async Task<string> GetPlayUrlAsync(int roomid)
|
||||
public async Task<string> GetPlayUrlAsync(int roomid)
|
||||
{
|
||||
string url = $@"{Config.LiveApiHost}/room/v1/Room/playUrl?cid={roomid}&quality=4&platform=web";
|
||||
if (Config.AvoidTxy)
|
||||
var url = $@"{Config.LiveApiHost}/room/v1/Room/playUrl?cid={roomid}&quality=4&platform=web";
|
||||
// 随机选择一个 url
|
||||
if ((await HttpGetJsonAsync(httpclient, url))?["data"]?["durl"] is JArray array)
|
||||
{
|
||||
// 尽量避开腾讯云
|
||||
int attempt_left = 2;
|
||||
while (true)
|
||||
var urls = array.Select(t => t?["url"]?.ToObject<string>());
|
||||
var distinct = urls.Distinct().ToArray();
|
||||
if (distinct.Length > 0)
|
||||
{
|
||||
attempt_left--;
|
||||
if ((await HttpGetJsonAsync(httpclient, url))?["data"]?["durl"] is JArray all_jtoken && all_jtoken.Count > 0)
|
||||
{
|
||||
var all = all_jtoken.Select(x => x["url"].ToObject<string>()).ToArray();
|
||||
var withoutTxy = all.Where(x => !x.Contains("txy.")).ToArray();
|
||||
if (withoutTxy.Length > 0)
|
||||
{
|
||||
return withoutTxy[random.Next(withoutTxy.Length)];
|
||||
}
|
||||
else if (attempt_left <= 0)
|
||||
{
|
||||
return all[random.Next(all.Length)];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("没有直播播放地址");
|
||||
}
|
||||
return distinct[random.Next(distinct.Length)];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 随机选择一个 url
|
||||
if ((await HttpGetJsonAsync(httpclient, url))?["data"]?["durl"] is JArray array)
|
||||
{
|
||||
var urls = array.Select(t => t?["url"]?.ToObject<string>());
|
||||
var distinct = urls.Distinct().ToArray();
|
||||
if (distinct.Length > 0)
|
||||
{
|
||||
return distinct[random.Next(distinct.Length)];
|
||||
}
|
||||
}
|
||||
throw new Exception("没有直播播放地址");
|
||||
}
|
||||
throw new Exception("没有直播播放地址");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -161,7 +134,7 @@ namespace BililiveRecorder.Core
|
|||
/// <returns>直播间信息</returns>
|
||||
/// <exception cref="WebException"/>
|
||||
/// <exception cref="Exception"/>
|
||||
public static async Task<RoomInfo> GetRoomInfoAsync(int roomid)
|
||||
public async Task<RoomInfo> GetRoomInfoAsync(int roomid)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -201,7 +174,7 @@ namespace BililiveRecorder.Core
|
|||
/// </summary>
|
||||
/// <param name="roomid"></param>
|
||||
/// <returns></returns>
|
||||
public static async Task<(string token, string host, int port)> GetDanmuConf(int roomid)
|
||||
public async Task<(string token, string host, int port)> GetDanmuConf(int roomid)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
<Compile Include="..\TempBuildInfo\BuildInfo.Core.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Autofac" Version="4.8.1" />
|
||||
<PackageReference Include="Autofac" Version="4.9.4" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
|
||||
<PackageReference Include="NLog" Version="4.5.10" />
|
||||
</ItemGroup>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
using BililiveRecorder.FlvProcessor;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using BililiveRecorder.FlvProcessor;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
|
||||
namespace BililiveRecorder.Core.Config
|
||||
{
|
||||
|
@ -98,6 +98,12 @@ namespace BililiveRecorder.Core.Config
|
|||
[JsonProperty("record_danmaku")]
|
||||
public bool RecordDanmaku { get => _recordDanmaku; set => SetField(ref _recordDanmaku, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 是否记录弹幕原始数据
|
||||
/// </summary>
|
||||
[JsonProperty("record_danmaku_raw")]
|
||||
public bool RecordDanmakuRaw { get => _recordDanmakuRaw; set => SetField(ref _recordDanmakuRaw, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 是否同时录制 SuperChat
|
||||
/// </summary>
|
||||
|
@ -116,12 +122,6 @@ namespace BililiveRecorder.Core.Config
|
|||
[JsonProperty("record_danmaku_guard")]
|
||||
public bool RecordDanmakuGuard { get => _recordDanmakuGuard; set => SetField(ref _recordDanmakuGuard, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 尽量避开腾讯云服务器,可有效提升录制文件能正常播放的概率。(垃圾腾讯云直播服务)
|
||||
/// </summary>
|
||||
[JsonProperty("avoidtxy")]
|
||||
public bool AvoidTxy { get => _avoidTxy; set => SetField(ref _avoidTxy, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 替换api.live.bilibili.com服务器为其他反代,可以支持在云服务器上录制
|
||||
/// </summary>
|
||||
|
@ -153,12 +153,13 @@ namespace BililiveRecorder.Core.Config
|
|||
}
|
||||
#endregion
|
||||
|
||||
private string _workDirectory;
|
||||
|
||||
private uint _clipLengthPast = 20;
|
||||
private uint _clipLengthFuture = 10;
|
||||
private uint _cuttingNumber = 10;
|
||||
private EnabledFeature _enabledFeature = EnabledFeature.RecordOnly;
|
||||
private AutoCuttingMode _cuttingMode = AutoCuttingMode.Disabled;
|
||||
private string _workDirectory;
|
||||
|
||||
private uint _timingWatchdogTimeout = 10 * 1000;
|
||||
private uint _timingStreamRetry = 6 * 1000;
|
||||
|
@ -172,12 +173,11 @@ namespace BililiveRecorder.Core.Config
|
|||
private string _clip_filename_format = @"{roomid}-{name}/剪辑片段-{roomid}-{date}-{time}-{title}.flv";
|
||||
|
||||
private bool _recordDanmaku = false;
|
||||
private bool _recordDanmakuRaw = false;
|
||||
private bool _recordDanmakuSuperChat = false;
|
||||
private bool _recordDanmakuGift = false;
|
||||
private bool _recordDanmakuGuard = false;
|
||||
|
||||
private bool _avoidTxy = false;
|
||||
|
||||
private string _liveApiHost = "https://api.live.bilibili.com";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
using Autofac;
|
||||
using BililiveRecorder.Core.Config;
|
||||
using System.Net.Sockets;
|
||||
using Autofac;
|
||||
using BililiveRecorder.Core.Config;
|
||||
|
||||
namespace BililiveRecorder.Core
|
||||
{
|
||||
|
@ -14,6 +14,7 @@ namespace BililiveRecorder.Core
|
|||
protected override void Load(ContainerBuilder builder)
|
||||
{
|
||||
builder.RegisterType<ConfigV1>().AsSelf().InstancePerMatchingLifetimeScope("recorder_root");
|
||||
builder.RegisterType<BililiveAPI>().AsSelf().InstancePerMatchingLifetimeScope("recorder_root");
|
||||
builder.RegisterType<TcpClient>().AsSelf().ExternallyOwned();
|
||||
builder.RegisterType<StreamMonitor>().As<IStreamMonitor>().ExternallyOwned();
|
||||
builder.RegisterType<RecordedRoom>().As<IRecordedRoom>().ExternallyOwned();
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
using BililiveRecorder.Core.Config;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using BililiveRecorder.Core.Config;
|
||||
|
||||
namespace BililiveRecorder.Core
|
||||
{
|
||||
public interface IRecorder : INotifyPropertyChanged, INotifyCollectionChanged, IEnumerable<IRecordedRoom>, ICollection<IRecordedRoom>
|
||||
public interface IRecorder : INotifyPropertyChanged, INotifyCollectionChanged, IEnumerable<IRecordedRoom>, ICollection<IRecordedRoom>, IDisposable
|
||||
{
|
||||
ConfigV1 Config { get; }
|
||||
|
||||
|
@ -19,6 +20,6 @@ namespace BililiveRecorder.Core
|
|||
|
||||
void SaveConfigToFile();
|
||||
|
||||
void Shutdown();
|
||||
// void Shutdown();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using BililiveRecorder.Core.Config;
|
||||
using BililiveRecorder.Core.Config;
|
||||
using BililiveRecorder.FlvProcessor;
|
||||
using NLog;
|
||||
using System;
|
||||
|
@ -85,6 +85,7 @@ namespace BililiveRecorder.Core
|
|||
}
|
||||
|
||||
private ConfigV1 _config { get; }
|
||||
private BililiveAPI BililiveAPI { get; }
|
||||
public IStreamMonitor StreamMonitor { get; }
|
||||
|
||||
private bool _retry = true;
|
||||
|
@ -115,11 +116,13 @@ namespace BililiveRecorder.Core
|
|||
IBasicDanmakuWriter basicDanmakuWriter,
|
||||
Func<int, IStreamMonitor> newIStreamMonitor,
|
||||
Func<IFlvStreamProcessor> newIFlvStreamProcessor,
|
||||
BililiveAPI bililiveAPI,
|
||||
int roomid)
|
||||
{
|
||||
this.newIFlvStreamProcessor = newIFlvStreamProcessor;
|
||||
|
||||
_config = config;
|
||||
BililiveAPI = bililiveAPI;
|
||||
|
||||
this.basicDanmakuWriter = basicDanmakuWriter;
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
using BililiveRecorder.Core.Config;
|
||||
using NLog;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
@ -9,6 +7,8 @@ using System.ComponentModel;
|
|||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BililiveRecorder.Core.Config;
|
||||
using NLog;
|
||||
|
||||
namespace BililiveRecorder.Core
|
||||
{
|
||||
|
@ -16,24 +16,18 @@ namespace BililiveRecorder.Core
|
|||
{
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
private ObservableCollection<IRecordedRoom> Rooms { get; } = new ObservableCollection<IRecordedRoom>();
|
||||
public ConfigV1 Config { get; }
|
||||
|
||||
ConfigV1 IRecorder.Config => Config;
|
||||
|
||||
public int Count => Rooms.Count;
|
||||
|
||||
public bool IsReadOnly => true;
|
||||
|
||||
int ICollection<IRecordedRoom>.Count => Rooms.Count;
|
||||
|
||||
bool ICollection<IRecordedRoom>.IsReadOnly => true;
|
||||
|
||||
private readonly Func<int, IRecordedRoom> newIRecordedRoom;
|
||||
private CancellationTokenSource tokenSource;
|
||||
private readonly CancellationTokenSource tokenSource;
|
||||
|
||||
private bool _valid = false;
|
||||
private bool disposedValue;
|
||||
|
||||
private ObservableCollection<IRecordedRoom> Rooms { get; } = new ObservableCollection<IRecordedRoom>();
|
||||
|
||||
public ConfigV1 Config { get; }
|
||||
|
||||
public int Count => Rooms.Count;
|
||||
public bool IsReadOnly => true;
|
||||
public IRecordedRoom this[int index] => Rooms[index];
|
||||
|
||||
public Recorder(ConfigV1 config, Func<int, IRecordedRoom> iRecordedRoom)
|
||||
|
@ -41,8 +35,6 @@ namespace BililiveRecorder.Core
|
|||
newIRecordedRoom = iRecordedRoom;
|
||||
Config = config;
|
||||
|
||||
BililiveAPI.Config = config;
|
||||
|
||||
tokenSource = new CancellationTokenSource();
|
||||
Repeat.Interval(TimeSpan.FromSeconds(3), DownloadWatchdog, tokenSource.Token);
|
||||
|
||||
|
@ -52,57 +44,8 @@ namespace BililiveRecorder.Core
|
|||
$"O:{e.OldItems?.Cast<IRecordedRoom>()?.Select(rr => rr.RoomId.ToString())?.Aggregate((current, next) => current + "," + next)};" +
|
||||
$"N:{e.NewItems?.Cast<IRecordedRoom>()?.Select(rr => rr.RoomId.ToString())?.Aggregate((current, next) => current + "," + next)}");
|
||||
};
|
||||
|
||||
var debouncing = new SemaphoreSlim(1, 1);
|
||||
Config.PropertyChanged += async (sender, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(Config.Cookie))
|
||||
{
|
||||
if (await debouncing.WaitAsync(0))
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Trace("设置 Cookie 等待...");
|
||||
await Task.Delay(100);
|
||||
logger.Trace("设置 Cookie 信息...");
|
||||
await BililiveAPI.ApplyCookieSettings(Config.Cookie);
|
||||
logger.Debug("设置 Cookie 成功");
|
||||
}
|
||||
finally
|
||||
{
|
||||
debouncing.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged
|
||||
{
|
||||
add => (Rooms as INotifyPropertyChanged).PropertyChanged += value;
|
||||
remove => (Rooms as INotifyPropertyChanged).PropertyChanged -= value;
|
||||
}
|
||||
|
||||
public event NotifyCollectionChangedEventHandler CollectionChanged
|
||||
{
|
||||
add => (Rooms as INotifyCollectionChanged).CollectionChanged += value;
|
||||
remove => (Rooms as INotifyCollectionChanged).CollectionChanged -= value;
|
||||
}
|
||||
|
||||
event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
|
||||
{
|
||||
add => (Rooms as INotifyPropertyChanged).PropertyChanged += value;
|
||||
remove => (Rooms as INotifyPropertyChanged).PropertyChanged -= value;
|
||||
}
|
||||
|
||||
event NotifyCollectionChangedEventHandler INotifyCollectionChanged.CollectionChanged
|
||||
{
|
||||
add => (Rooms as INotifyCollectionChanged).CollectionChanged += value;
|
||||
remove => (Rooms as INotifyCollectionChanged).CollectionChanged -= value;
|
||||
}
|
||||
|
||||
bool IRecorder.Initialize(string workdir) => Initialize(workdir);
|
||||
|
||||
public bool Initialize(string workdir)
|
||||
{
|
||||
logger.Debug("Initialize: " + workdir);
|
||||
|
@ -167,15 +110,16 @@ namespace BililiveRecorder.Core
|
|||
/// <param name="rr">直播间</param>
|
||||
public void RemoveRoom(IRecordedRoom rr)
|
||||
{
|
||||
if (rr is null) return;
|
||||
if (!_valid) { throw new InvalidOperationException("Not Initialized"); }
|
||||
rr.Shutdown();
|
||||
logger.Debug("RemoveRoom 移除了直播间 {roomid}", rr.RoomId);
|
||||
Rooms.Remove(rr);
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
private void Shutdown()
|
||||
{
|
||||
if (!_valid) { throw new InvalidOperationException("Not Initialized"); }
|
||||
if (!_valid) { return; }
|
||||
logger.Debug("Shutdown called.");
|
||||
tokenSource.Cancel();
|
||||
|
||||
|
@ -185,6 +129,8 @@ namespace BililiveRecorder.Core
|
|||
{
|
||||
rr.Shutdown();
|
||||
});
|
||||
|
||||
Rooms.Clear();
|
||||
}
|
||||
|
||||
public void SaveConfigToFile()
|
||||
|
@ -227,19 +173,54 @@ namespace BililiveRecorder.Core
|
|||
}
|
||||
|
||||
void ICollection<IRecordedRoom>.Add(IRecordedRoom item) => throw new NotSupportedException("Collection is readonly");
|
||||
|
||||
void ICollection<IRecordedRoom>.Clear() => throw new NotSupportedException("Collection is readonly");
|
||||
|
||||
bool ICollection<IRecordedRoom>.Remove(IRecordedRoom item) => throw new NotSupportedException("Collection is readonly");
|
||||
|
||||
bool ICollection<IRecordedRoom>.Contains(IRecordedRoom item) => Rooms.Contains(item);
|
||||
|
||||
void ICollection<IRecordedRoom>.CopyTo(IRecordedRoom[] array, int arrayIndex) => Rooms.CopyTo(array, arrayIndex);
|
||||
|
||||
public IEnumerator<IRecordedRoom> GetEnumerator() => Rooms.GetEnumerator();
|
||||
IEnumerator<IRecordedRoom> IEnumerable<IRecordedRoom>.GetEnumerator() => Rooms.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => Rooms.GetEnumerator();
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged
|
||||
{
|
||||
add => (Rooms as INotifyPropertyChanged).PropertyChanged += value;
|
||||
remove => (Rooms as INotifyPropertyChanged).PropertyChanged -= value;
|
||||
}
|
||||
|
||||
public event NotifyCollectionChangedEventHandler CollectionChanged
|
||||
{
|
||||
add => (Rooms as INotifyCollectionChanged).CollectionChanged += value;
|
||||
remove => (Rooms as INotifyCollectionChanged).CollectionChanged -= value;
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
// dispose managed state (managed objects)
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
// free unmanaged resources (unmanaged objects) and override finalizer
|
||||
// set large fields to null
|
||||
disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
// override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
|
||||
// ~Recorder()
|
||||
// {
|
||||
// // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
// Dispose(disposing: false);
|
||||
// }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
using BililiveRecorder.Core.Config;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BililiveRecorder.Core.Config;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
using Timer = System.Timers.Timer;
|
||||
|
||||
namespace BililiveRecorder.Core
|
||||
|
@ -32,6 +31,7 @@ namespace BililiveRecorder.Core
|
|||
|
||||
private readonly Func<TcpClient> funcTcpClient;
|
||||
private readonly ConfigV1 config;
|
||||
private readonly BililiveAPI bililiveAPI;
|
||||
|
||||
#pragma warning disable IDE1006 // 命名样式
|
||||
private bool dmTcpConnected => dmClient?.Connected ?? false;
|
||||
|
@ -40,7 +40,7 @@ namespace BililiveRecorder.Core
|
|||
private TcpClient dmClient;
|
||||
private NetworkStream dmNetStream;
|
||||
private Thread dmReceiveMessageLoopThread;
|
||||
private CancellationTokenSource dmTokenSource = null;
|
||||
private readonly CancellationTokenSource dmTokenSource = null;
|
||||
private bool dmConnectionTriggered = false;
|
||||
private readonly Timer httpTimer;
|
||||
|
||||
|
@ -50,10 +50,11 @@ namespace BililiveRecorder.Core
|
|||
public event StreamStartedEvent StreamStarted;
|
||||
public event ReceivedDanmakuEvt ReceivedDanmaku;
|
||||
|
||||
public StreamMonitor(int roomid, Func<TcpClient> funcTcpClient, ConfigV1 config)
|
||||
public StreamMonitor(int roomid, Func<TcpClient> funcTcpClient, ConfigV1 config, BililiveAPI bililiveAPI)
|
||||
{
|
||||
this.funcTcpClient = funcTcpClient;
|
||||
this.config = config;
|
||||
this.bililiveAPI = bililiveAPI;
|
||||
|
||||
Roomid = roomid;
|
||||
|
||||
|
@ -178,8 +179,9 @@ namespace BililiveRecorder.Core
|
|||
|
||||
public async Task<RoomInfo> FetchRoomInfoAsync()
|
||||
{
|
||||
RoomInfo roomInfo = await BililiveAPI.GetRoomInfoAsync(Roomid).ConfigureAwait(false);
|
||||
RoomInfoUpdated?.Invoke(this, new RoomInfoUpdatedArgs { RoomInfo = roomInfo });
|
||||
RoomInfo roomInfo = await bililiveAPI.GetRoomInfoAsync(Roomid).ConfigureAwait(false);
|
||||
if (roomInfo != null)
|
||||
RoomInfoUpdated?.Invoke(this, new RoomInfoUpdatedArgs { RoomInfo = roomInfo });
|
||||
return roomInfo;
|
||||
}
|
||||
|
||||
|
@ -208,7 +210,7 @@ namespace BililiveRecorder.Core
|
|||
|
||||
try
|
||||
{
|
||||
var (token, host, port) = await BililiveAPI.GetDanmuConf(Roomid);
|
||||
var (token, host, port) = await bililiveAPI.GetDanmuConf(Roomid);
|
||||
|
||||
logger.Log(Roomid, LogLevel.Debug, $"连接弹幕服务器 {host}:{port} {(string.IsNullOrWhiteSpace(token) ? "无" : "有")} token");
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
using NLog;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
using NLog;
|
||||
|
||||
namespace BililiveRecorder.Core
|
||||
{
|
||||
|
@ -51,20 +51,20 @@ namespace BililiveRecorder.Core
|
|||
return name;
|
||||
}
|
||||
|
||||
public static bool CopyPropertiesTo<T>(this T val1, T val2) where T : class
|
||||
public static bool CopyPropertiesTo<T>(this T source, T target) where T : class
|
||||
{
|
||||
if (val1 == null || val2 == null || val1 == val2) { return false; }
|
||||
foreach (var p in val1.GetType().GetProperties())
|
||||
if (source == null || target == null || source == target) { return false; }
|
||||
foreach (var p in source.GetType().GetProperties())
|
||||
{
|
||||
if (Attribute.IsDefined(p, typeof(DoNotCopyProperty)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var val = p.GetValue(val1);
|
||||
if (!val.Equals(p.GetValue(val2)))
|
||||
var val = p.GetValue(source);
|
||||
if (val == null || !val.Equals(p.GetValue(target)))
|
||||
{
|
||||
p.SetValue(val2, val);
|
||||
p.SetValue(target, val);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
<Compile Include="..\TempBuildInfo\BuildInfo.FlvProcessor.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Autofac" Version="4.8.1" />
|
||||
<PackageReference Include="Autofac" Version="4.9.4" />
|
||||
<PackageReference Include="NLog" Version="4.5.10" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
|
|
|
@ -1,6 +1,20 @@
|
|||
<Application x:Class="BililiveRecorder.WPF.App"
|
||||
<Application
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:BililiveRecorder.WPF"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
x:Class="BililiveRecorder.WPF.App"
|
||||
Startup="CheckUpdate"
|
||||
StartupUri="MainWindow.xaml"/>
|
||||
StartupUri="NewMainWindow.xaml">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ui:ThemeResources />
|
||||
<ui:XamlControlsResources />
|
||||
<!-- Other merged dictionaries here -->
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<!-- Other app resources here -->
|
||||
<FontFamily x:Key="ContentControlThemeFontFamily">Microsoft YaHei</FontFamily>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
using NLog;
|
||||
using Squirrel;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using NLog;
|
||||
using Squirrel;
|
||||
|
||||
namespace BililiveRecorder.WPF
|
||||
{
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<NuGetPackageImportStamp>
|
||||
</NuGetPackageImportStamp>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
|
@ -23,6 +25,7 @@
|
|||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<AllowedReferenceRelatedFileExtensions>-</AllowedReferenceRelatedFileExtensions>
|
||||
|
@ -50,65 +53,12 @@
|
|||
<ApplicationManifest>Properties\app.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Autofac, Version=4.8.1.0, Culture=neutral, PublicKeyToken=17863af14b0044da, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Autofac.4.8.1\lib\net45\Autofac.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="CommandLine, Version=2.4.3.0, Culture=neutral, PublicKeyToken=de6f01bd326f8c32, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\CommandLineParser.2.4.3\lib\netstandard2.0\CommandLine.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="DeltaCompressionDotNet, Version=1.1.0.0, Culture=neutral, PublicKeyToken=1d14d6e5194e7f4a, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DeltaCompressionDotNet.1.1.0\lib\net20\DeltaCompressionDotNet.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="DeltaCompressionDotNet.MsDelta, Version=1.1.0.0, Culture=neutral, PublicKeyToken=46b2138a390abf55, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DeltaCompressionDotNet.1.1.0\lib\net20\DeltaCompressionDotNet.MsDelta.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="DeltaCompressionDotNet.PatchApi, Version=1.1.0.0, Culture=neutral, PublicKeyToken=3e8888ee913ed789, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DeltaCompressionDotNet.1.1.0\lib\net20\DeltaCompressionDotNet.PatchApi.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Hardcodet.Wpf.TaskbarNotification, Version=1.0.5.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Hardcodet.NotifyIcon.Wpf.1.0.8\lib\net451\Hardcodet.Wpf.TaskbarNotification.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.WindowsAPICodePack, Version=1.1.2.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\WindowsAPICodePack-Core.1.1.2\lib\Microsoft.WindowsAPICodePack.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.WindowsAPICodePack.Shell, Version=1.1.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\WindowsAPICodePack-Shell.1.1.1\lib\Microsoft.WindowsAPICodePack.Shell.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Mono.Cecil, Version=0.9.6.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Mono.Cecil.0.9.6.1\lib\net45\Mono.Cecil.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Mono.Cecil.Mdb, Version=0.9.6.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Mono.Cecil.0.9.6.1\lib\net45\Mono.Cecil.Mdb.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Mono.Cecil.Pdb, Version=0.9.6.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Mono.Cecil.0.9.6.1\lib\net45\Mono.Cecil.Pdb.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Mono.Cecil.Rocks, Version=0.9.6.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Mono.Cecil.0.9.6.1\lib\net45\Mono.Cecil.Rocks.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json, Version=11.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\NLog.4.5.10\lib\net45\NLog.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="NuGet.Squirrel, Version=3.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\squirrel.windows.1.9.0\lib\Net45\NuGet.Squirrel.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="SharpCompress, Version=0.17.1.0, Culture=neutral, PublicKeyToken=afb0a02973931d96, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\SharpCompress.0.17.1\lib\net45\SharpCompress.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Splat, Version=1.6.2.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Splat.1.6.2\lib\Net45\Splat.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Squirrel, Version=1.9.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\squirrel.windows.1.9.0\lib\Net45\Squirrel.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Configuration" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Deployment" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Runtime.Remoting" />
|
||||
<Reference Include="System.Runtime.Serialization" />
|
||||
<Reference Include="System.ServiceModel" />
|
||||
<Reference Include="System.Transactions" />
|
||||
|
@ -132,29 +82,86 @@
|
|||
<SubType>Designer</SubType>
|
||||
</ApplicationDefinition>
|
||||
<Compile Include="..\TempBuildInfo\BuildInfo.WPF.cs" />
|
||||
<Compile Include="ClickSelectTextBox.xaml.cs">
|
||||
<DependentUpon>ClickSelectTextBox.xaml</DependentUpon>
|
||||
<Compile Include="Controls\AddRoomCard.xaml.cs">
|
||||
<DependentUpon>AddRoomCard.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Controls\AddRoomFailedDialog.xaml.cs">
|
||||
<DependentUpon>AddRoomFailedDialog.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Controls\CloseWindowConfirmDialog.xaml.cs">
|
||||
<DependentUpon>CloseWindowConfirmDialog.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Controls\DeleteRoomConfirmDialog.xaml.cs">
|
||||
<DependentUpon>DeleteRoomConfirmDialog.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Controls\RoomCard.xaml.cs">
|
||||
<DependentUpon>RoomCard.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Controls\TaskbarIconControl.xaml.cs">
|
||||
<DependentUpon>TaskbarIconControl.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Controls\TaskbarIconTrayPopup.xaml.cs">
|
||||
<DependentUpon>TaskbarIconTrayPopup.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Controls\WorkDirectorySelectorDialog.xaml.cs">
|
||||
<DependentUpon>WorkDirectorySelectorDialog.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Converters\ClipEnabledToBooleanConverter.cs" />
|
||||
<Compile Include="Converters\EnumToBooleanConverter.cs" />
|
||||
<Compile Include="Converters\IsNullToVisibilityConverter.cs" />
|
||||
<Compile Include="Converters\ShortRoomIdToVisibilityConverter.cs" />
|
||||
<Compile Include="Converters\ValueConverterGroup.cs" />
|
||||
<Compile Include="Models\LogModel.cs" />
|
||||
<Compile Include="Models\RootModel.cs" />
|
||||
<Compile Include="Pages\RootPage.xaml.cs">
|
||||
<DependentUpon>RootPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Converters\BooleanAndConverter.cs" />
|
||||
<Compile Include="Converters\BooleanInverterConverter.cs" />
|
||||
<Compile Include="Converters\BoolToValueConverter.cs" />
|
||||
<Compile Include="Converters\MultiBoolToValueConverter.cs" />
|
||||
<Compile Include="Converters\NullValueTemplateSelector.cs" />
|
||||
<Compile Include="Converters\PercentageToColorBrushConverter.cs" />
|
||||
<Compile Include="Converters\RoomListInterceptConverter.cs" />
|
||||
<Compile Include="CommandLineOption.cs" />
|
||||
<Compile Include="EnumerableExtensions.cs" />
|
||||
<Compile Include="TimedMessageBox.xaml.cs">
|
||||
<DependentUpon>TimedMessageBox.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="ValueConverters.cs" />
|
||||
<Compile Include="SettingsWindow.xaml.cs">
|
||||
<DependentUpon>SettingsWindow.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="UpdateBarUserControl.xaml.cs">
|
||||
<DependentUpon>UpdateBarUserControl.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="WorkDirectoryWindow.xaml.cs">
|
||||
<DependentUpon>WorkDirectoryWindow.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Page Include="ClickSelectTextBox.xaml">
|
||||
<Compile Include="SingleInstance.cs" />
|
||||
<Page Include="Controls\AddRoomCard.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="MainWindow.xaml">
|
||||
<Page Include="Controls\AddRoomFailedDialog.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Controls\CloseWindowConfirmDialog.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Controls\DeleteRoomConfirmDialog.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Controls\RoomCard.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Controls\TaskbarIconControl.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Controls\TaskbarIconTrayPopup.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Controls\WorkDirectorySelectorDialog.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Pages\RootPage.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="DesignTimeResources.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
|
@ -162,28 +169,45 @@
|
|||
<DependentUpon>App.xaml</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="MainWindow.xaml.cs">
|
||||
<DependentUpon>MainWindow.xaml</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Page Include="SettingsWindow.xaml">
|
||||
<Page Include="NewMainWindow.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="TimedMessageBox.xaml">
|
||||
<Page Include="Pages\LogPage.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="UpdateBarUserControl.xaml">
|
||||
<Page Include="Pages\RoomListPage.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="WorkDirectoryWindow.xaml">
|
||||
<Page Include="Pages\AdvancedSettingsPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
<Page Include="Pages\SettingsPage.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="MockData\MockRecordedRoom.cs" />
|
||||
<Compile Include="MockData\MockRecorder.cs" />
|
||||
<Compile Include="NewMainWindow.xaml.cs">
|
||||
<DependentUpon>NewMainWindow.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Pages\LogPage.xaml.cs">
|
||||
<DependentUpon>LogPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Pages\RoomListPage.xaml.cs">
|
||||
<DependentUpon>RoomListPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Pages\AdvancedSettingsPage.xaml.cs">
|
||||
<DependentUpon>AdvancedSettingsPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Pages\SettingsPage.xaml.cs">
|
||||
<DependentUpon>SettingsPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Properties\AssemblyInfo.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
|
@ -203,10 +227,6 @@
|
|||
</Content>
|
||||
<None Include="Nlog.Release.config" />
|
||||
<None Include="NLog.Debug.config" />
|
||||
<None Include="NLog.xsd">
|
||||
<SubType>Designer</SubType>
|
||||
</None>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="App.config" />
|
||||
|
@ -221,6 +241,41 @@
|
|||
<Name>BililiveRecorder.FlvProcessor</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Autofac">
|
||||
<Version>4.9.4</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="CommandLineParser">
|
||||
<Version>2.4.3</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Hardcodet.NotifyIcon.Wpf">
|
||||
<Version>1.0.8</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="ModernWpfUI">
|
||||
<Version>0.9.2</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Newtonsoft.Json">
|
||||
<Version>11.0.2</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NLog.Config">
|
||||
<Version>4.5.10</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NuGet.CommandLine">
|
||||
<Version>4.7.1</Version>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="squirrel.windows">
|
||||
<Version>2.0.1</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="WindowsAPICodePack-Core">
|
||||
<Version>1.1.2</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="WindowsAPICodePack-Shell">
|
||||
<Version>1.1.1</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<PropertyGroup>
|
||||
<PreBuildEvent>cd $(SolutionDir)
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
<TextBox x:Class="BililiveRecorder.WPF.ClickSelectTextBox"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:BililiveRecorder.WPF"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
|
||||
</TextBox>
|
|
@ -1,51 +0,0 @@
|
|||
/**
|
||||
* Author: Donnelle
|
||||
* From: https://stackoverflow.com/a/661224/8877830
|
||||
* */
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace BililiveRecorder.WPF
|
||||
{
|
||||
public partial class ClickSelectTextBox : TextBox
|
||||
{
|
||||
public ClickSelectTextBox()
|
||||
{
|
||||
AddHandler(PreviewMouseLeftButtonDownEvent,
|
||||
new MouseButtonEventHandler(SelectivelyIgnoreMouseButton), true);
|
||||
AddHandler(GotKeyboardFocusEvent,
|
||||
new RoutedEventHandler(SelectAllText), true);
|
||||
AddHandler(MouseDoubleClickEvent,
|
||||
new RoutedEventHandler(SelectAllText), true);
|
||||
}
|
||||
|
||||
private static void SelectivelyIgnoreMouseButton(object sender,
|
||||
MouseButtonEventArgs e)
|
||||
{
|
||||
// Find the TextBox
|
||||
DependencyObject parent = e.OriginalSource as UIElement;
|
||||
while (parent != null && !(parent is TextBox))
|
||||
parent = VisualTreeHelper.GetParent(parent);
|
||||
|
||||
if (parent != null)
|
||||
{
|
||||
var textBox = (TextBox)parent;
|
||||
if (!textBox.IsKeyboardFocusWithin)
|
||||
{
|
||||
// If the text box is not yet focussed, give it the focus and
|
||||
// stop further processing of this click event.
|
||||
textBox.Focus();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void SelectAllText(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (e.OriginalSource is TextBox textBox)
|
||||
textBox.SelectAll();
|
||||
}
|
||||
}
|
||||
}
|
32
BililiveRecorder.WPF/Controls/AddRoomCard.xaml
Normal file
32
BililiveRecorder.WPF/Controls/AddRoomCard.xaml
Normal file
|
@ -0,0 +1,32 @@
|
|||
<UserControl
|
||||
x:Class="BililiveRecorder.WPF.Controls.AddRoomCard"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
xmlns:local="clr-namespace:BililiveRecorder.WPF.Controls"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="110" d:DesignWidth="210">
|
||||
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||
BorderBrush="{DynamicResource SystemControlBackgroundAccentBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5">
|
||||
<Grid Margin="0,15">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="2*"/>
|
||||
<RowDefinition Height="3.5*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<ui:PathIcon Margin="5" Data="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z"/>
|
||||
<TextBlock FontSize="20" VerticalAlignment="Center" Text="添加房间"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="10,0"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||
<TextBox Width="140" x:Name="InputTextBox" KeyDown="InputTextBox_KeyDown"
|
||||
ui:ControlHelper.PlaceholderText="房间号或房间链接"/>
|
||||
<Button Content="确定" Click="Button_Click" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
40
BililiveRecorder.WPF/Controls/AddRoomCard.xaml.cs
Normal file
40
BililiveRecorder.WPF/Controls/AddRoomCard.xaml.cs
Normal file
|
@ -0,0 +1,40 @@
|
|||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace BililiveRecorder.WPF.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for AddRoomCard.xaml
|
||||
/// </summary>
|
||||
public partial class AddRoomCard : UserControl
|
||||
{
|
||||
public event EventHandler<string> AddRoomRequested;
|
||||
|
||||
public AddRoomCard()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void AddRoom()
|
||||
{
|
||||
AddRoomRequested?.Invoke(this, InputTextBox.Text);
|
||||
InputTextBox.Text = string.Empty;
|
||||
InputTextBox.Focus();
|
||||
}
|
||||
|
||||
private void Button_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
AddRoom();
|
||||
}
|
||||
|
||||
private void InputTextBox_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Enter)
|
||||
{
|
||||
AddRoom();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
20
BililiveRecorder.WPF/Controls/AddRoomFailedDialog.xaml
Normal file
20
BililiveRecorder.WPF/Controls/AddRoomFailedDialog.xaml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<ui:ContentDialog
|
||||
x:Class="BililiveRecorder.WPF.Controls.AddRoomFailedDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
xmlns:local="clr-namespace:BililiveRecorder.WPF.Controls"
|
||||
CloseButtonText="知道了"
|
||||
DefaultButton="Close"
|
||||
mc:Ignorable="d">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Text="添加房间失败" TextAlignment="Center" FontSize="20"/>
|
||||
<TextBlock Grid.Row="1" Text="{Binding}" VerticalAlignment="Center" TextAlignment="Center" FontSize="16"/>
|
||||
</Grid>
|
||||
</ui:ContentDialog>
|
13
BililiveRecorder.WPF/Controls/AddRoomFailedDialog.xaml.cs
Normal file
13
BililiveRecorder.WPF/Controls/AddRoomFailedDialog.xaml.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
namespace BililiveRecorder.WPF.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for AddRoomFailedDialog.xaml
|
||||
/// </summary>
|
||||
public partial class AddRoomFailedDialog
|
||||
{
|
||||
public AddRoomFailedDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
14
BililiveRecorder.WPF/Controls/CloseWindowConfirmDialog.xaml
Normal file
14
BililiveRecorder.WPF/Controls/CloseWindowConfirmDialog.xaml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<ui:ContentDialog
|
||||
x:Class="BililiveRecorder.WPF.Controls.CloseWindowConfirmDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
xmlns:local="clr-namespace:BililiveRecorder.WPF.Controls"
|
||||
PrimaryButtonText="退出"
|
||||
CloseButtonText="取消"
|
||||
DefaultButton="Close"
|
||||
mc:Ignorable="d">
|
||||
<TextBlock Text="确定要退出吗?" TextAlignment="Center" VerticalAlignment="Center" FontSize="26"/>
|
||||
</ui:ContentDialog>
|
|
@ -0,0 +1,13 @@
|
|||
namespace BililiveRecorder.WPF.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for CloseWindowConfirmDialog.xaml
|
||||
/// </summary>
|
||||
public partial class CloseWindowConfirmDialog
|
||||
{
|
||||
public CloseWindowConfirmDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
29
BililiveRecorder.WPF/Controls/DeleteRoomConfirmDialog.xaml
Normal file
29
BililiveRecorder.WPF/Controls/DeleteRoomConfirmDialog.xaml
Normal file
|
@ -0,0 +1,29 @@
|
|||
<ui:ContentDialog
|
||||
x:Class="BililiveRecorder.WPF.Controls.DeleteRoomConfirmDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
xmlns:local="clr-namespace:BililiveRecorder.WPF.Controls"
|
||||
xmlns:mock="clr-namespace:BililiveRecorder.WPF.MockData"
|
||||
xmlns:conv="clr-namespace:BililiveRecorder.WPF.Converters"
|
||||
DefaultButton="Close"
|
||||
PrimaryButtonText="删除"
|
||||
CloseButtonText="取消"
|
||||
d:DataContext="{d:DesignInstance Type=mock:MockRecordedRoom,IsDesignTimeCreatable=True}"
|
||||
mc:Ignorable="d" >
|
||||
<StackPanel>
|
||||
<StackPanel.Resources>
|
||||
<conv:ShortRoomIdToVisibilityConverter x:Key="ShortRoomIdToVisibilityConverter"/>
|
||||
</StackPanel.Resources>
|
||||
<TextBlock TextAlignment="Center" FontSize="18" Text="确定要删除这个直播间吗?" Margin="0,0,0,5"/>
|
||||
<TextBlock VerticalAlignment="Center" Style="{DynamicResource SubtitleTextBlockStyle}" TextWrapping="NoWrap" HorizontalAlignment="Center"
|
||||
TextAlignment="Center" TextTrimming="CharacterEllipsis" Text="{Binding StreamerName,Mode=OneWay}" ToolTip="{Binding StreamerName,Mode=OneWay}"/>
|
||||
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center" Orientation="Horizontal" Grid.Row="1" Grid.ColumnSpan="2" Margin="5,0,0,0">
|
||||
<ui:PathIcon Height="10" Data="M6,18V8H8V18H6M6,4.5H8V6.5H6V4.5M17,4H19V18H17V17.75C17,17.75 15.67,18 15,18A5,5 0 0,1 10,13A5,5 0 0,1 15,8C15.67,8 17,8.25 17,8.25V4M17,10.25C17,10.25 15.67,10 15,10A3,3 0 0,0 12,13A3,3 0 0,0 15,16C15.67,16 17,15.75 17,15.75V10.25Z" />
|
||||
<TextBlock Text="{Binding RoomId, StringFormat=\{0\},Mode=OneWay}" Margin="5,0"/>
|
||||
<TextBlock Text="{Binding ShortRoomId, StringFormat=(\{0\}),Mode=OneWay}" Visibility="{Binding ShortRoomId,Converter={StaticResource ShortRoomIdToVisibilityConverter}}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ui:ContentDialog>
|
|
@ -0,0 +1,13 @@
|
|||
namespace BililiveRecorder.WPF.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for DeleteRoomConfirmDialog.xaml
|
||||
/// </summary>
|
||||
public partial class DeleteRoomConfirmDialog
|
||||
{
|
||||
public DeleteRoomConfirmDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
130
BililiveRecorder.WPF/Controls/RoomCard.xaml
Normal file
130
BililiveRecorder.WPF/Controls/RoomCard.xaml
Normal file
|
@ -0,0 +1,130 @@
|
|||
<UserControl
|
||||
x:Class="BililiveRecorder.WPF.Controls.RoomCard"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
xmlns:local="clr-namespace:BililiveRecorder.WPF.Controls"
|
||||
xmlns:mock="clr-namespace:BililiveRecorder.WPF.MockData"
|
||||
xmlns:converter="clr-namespace:BililiveRecorder.WPF.Converters"
|
||||
xmlns:pages="clr-namespace:BililiveRecorder.WPF.Pages"
|
||||
xmlns:core="clr-namespace:BililiveRecorder.Core;assembly=BililiveRecorder.Core"
|
||||
d:DesignWidth="230" d:DesignHeight="120"
|
||||
d:DataContext="{d:DesignInstance Type=mock:MockRecordedRoom,IsDesignTimeCreatable=True}"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<BooleanToVisibilityConverter x:Key="b2vConverter" />
|
||||
<converter:BooleanInverterConverter x:Key="biConverter"/>
|
||||
<converter:MultiBoolToValueConverter x:Key="mbtvConverter" TrueValue="{x:Static Visibility.Visible}" FalseValue="{x:Static Visibility.Collapsed}"/>
|
||||
<converter:PercentageToColorBrushConverter x:Key="PercentageToColorBrushConverter"/>
|
||||
<converter:ClipEnabledToBooleanConverter x:Key="ClipEnabledToBooleanConverter"/>
|
||||
<converter:ShortRoomIdToVisibilityConverter x:Key="ShortRoomIdToVisibilityConverter"/>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||
BorderBrush="{DynamicResource SystemControlBackgroundAccentBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5">
|
||||
<Grid Margin="5">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="2*"/>
|
||||
<RowDefinition Height="2*"/>
|
||||
<RowDefinition Height="2*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="0.8*"/>
|
||||
<ColumnDefinition Width="1.1*"/>
|
||||
<ColumnDefinition Width="0.65*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.ColumnSpan="3" VerticalAlignment="Center" Margin="5,0,0,0"
|
||||
Style="{DynamicResource SubtitleTextBlockStyle}"
|
||||
TextWrapping="NoWrap" TextTrimming="CharacterEllipsis"
|
||||
Text="{Binding StreamerName,Mode=OneWay}"
|
||||
ToolTip="{Binding StreamerName,Mode=OneWay}"/>
|
||||
<Menu Grid.Row="1" Grid.Column="3" HorizontalAlignment="Right">
|
||||
<MenuItem ToolTip="操作">
|
||||
<MenuItem.Header>
|
||||
<ui:PathIcon Height="20" Width="20" Data="M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z"/>
|
||||
</MenuItem.Header>
|
||||
<MenuItem Header="开始录制" Click="MenuItem_StartRecording_Click">
|
||||
<MenuItem.Icon>
|
||||
<ui:PathIcon Data="M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M10,16.5L16,12L10,7.5V16.5Z"/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="停止录制" Click="MenuItem_StopRecording_Click">
|
||||
<MenuItem.Icon>
|
||||
<ui:PathIcon Data="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4M9,9V15H15V9"/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="刷新直播间信息" Click="MenuItem_RefreshInfo_Click">
|
||||
<MenuItem.Icon>
|
||||
<ui:PathIcon Data="M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z"/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator/>
|
||||
<ui:RadioMenuItem Header="自动录制" GroupName="自动录制" IsChecked="{Binding IsMonitoring,Mode=OneWay}" Click="MenuItem_StartMonitor_Click">
|
||||
<ui:RadioMenuItem.Icon>
|
||||
<ui:PathIcon Foreground="Orange" Data="M18.15,4.94C17.77,4.91 17.37,5 17,5.2L8.35,10.2C7.39,10.76 7.07,12 7.62,12.94L9.12,15.53C9.67,16.5 10.89,16.82 11.85,16.27L13.65,15.23C13.92,15.69 14.32,16.06 14.81,16.27V18.04C14.81,19.13 15.7,20 16.81,20H22V18.04H16.81V16.27C17.72,15.87 18.31,14.97 18.31,14C18.31,13.54 18.19,13.11 17.97,12.73L20.5,11.27C21.47,10.71 21.8,9.5 21.24,8.53L19.74,5.94C19.4,5.34 18.79,5 18.15,4.94M6.22,13.17L2,13.87L2.75,15.17L4.75,18.63L5.5,19.93L8.22,16.63L6.22,13.17Z"/>
|
||||
</ui:RadioMenuItem.Icon>
|
||||
</ui:RadioMenuItem>
|
||||
<ui:RadioMenuItem Header="不自动录制" GroupName="自动录制" IsChecked="{Binding IsMonitoring,Mode=OneWay,Converter={StaticResource biConverter}}" Click="MenuItem_StopMonitor_Click"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="删除房间" Click="MenuItem_DeleteRoom_Click">
|
||||
<MenuItem.Icon>
|
||||
<ui:PathIcon Foreground="DarkRed" Data="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z"/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
<StackPanel VerticalAlignment="Center" Orientation="Horizontal" Grid.Row="1" Grid.ColumnSpan="2" Margin="5,0,0,0">
|
||||
<ui:PathIcon Height="10" Data="M6,18V8H8V18H6M6,4.5H8V6.5H6V4.5M17,4H19V18H17V17.75C17,17.75 15.67,18 15,18A5,5 0 0,1 10,13A5,5 0 0,1 15,8C15.67,8 17,8.25 17,8.25V4M17,10.25C17,10.25 15.67,10 15,10A3,3 0 0,0 12,13A3,3 0 0,0 15,16C15.67,16 17,15.75 17,15.75V10.25Z" />
|
||||
<TextBlock Text="{Binding RoomId, StringFormat=\{0\},Mode=OneWay}" Margin="5,0"/>
|
||||
<TextBlock Text="{Binding ShortRoomId, StringFormat=(\{0\}),Mode=OneWay}" Visibility="{Binding ShortRoomId,Converter={StaticResource ShortRoomIdToVisibilityConverter}}"/>
|
||||
</StackPanel>
|
||||
<Border Grid.Row="2" Grid.Column="1" VerticalAlignment="Center" Visibility="{Binding Visibility, ElementName=RecordingIcon}"
|
||||
Background="{Binding DownloadSpeedPersentage, Converter={StaticResource PercentageToColorBrushConverter},Mode=OneWay}"
|
||||
BorderThickness="1" BorderBrush="{DynamicResource SystemControlBackgroundChromeBlackMediumBrush}" Margin="2,0" CornerRadius="5">
|
||||
<Border.ToolTip>
|
||||
<StackPanel>
|
||||
<TextBlock Text="录制的速度与质量"/>
|
||||
<TextBlock Text="绿色为良好,红色为差"/>
|
||||
<TextBlock Margin="0,5,0,0" Text="{Binding DownloadSpeedPersentage,StringFormat=当前速度比: 0.## %,Mode=OneWay}"/>
|
||||
</StackPanel>
|
||||
</Border.ToolTip>
|
||||
<TextBlock HorizontalAlignment="Center" Foreground="Black" Text="{Binding DownloadSpeedMegaBitps, StringFormat=0.## Mbps,Mode=OneWay}"/>
|
||||
</Border>
|
||||
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center" Orientation="Horizontal" Grid.Row="2" Grid.Column="0">
|
||||
<StackPanel.Visibility>
|
||||
<MultiBinding Converter="{StaticResource mbtvConverter}" Mode="OneWay">
|
||||
<Binding Path="IsRecording" Converter="{StaticResource biConverter}" Mode="OneWay"/>
|
||||
<Binding Path="IsMonitoring" Mode="OneWay"/>
|
||||
</MultiBinding>
|
||||
</StackPanel.Visibility>
|
||||
<ui:PathIcon Data="M18.15,4.94C17.77,4.91 17.37,5 17,5.2L8.35,10.2C7.39,10.76 7.07,12 7.62,12.94L9.12,15.53C9.67,16.5 10.89,16.82 11.85,16.27L13.65,15.23C13.92,15.69 14.32,16.06 14.81,16.27V18.04C14.81,19.13 15.7,20 16.81,20H22V18.04H16.81V16.27C17.72,15.87 18.31,14.97 18.31,14C18.31,13.54 18.19,13.11 17.97,12.73L20.5,11.27C21.47,10.71 21.8,9.5 21.24,8.53L19.74,5.94C19.4,5.34 18.79,5 18.15,4.94M6.22,13.17L2,13.87L2.75,15.17L4.75,18.63L5.5,19.93L8.22,16.63L6.22,13.17Z" Foreground="DarkOrange" Width="15"/>
|
||||
<TextBlock Foreground="DarkOrange" Margin="3,0,0,0" Text="监控中"/>
|
||||
</StackPanel>
|
||||
<StackPanel x:Name="RecordingIcon" Visibility="{Binding IsRecording, Converter={StaticResource b2vConverter},Mode=OneWay}"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Center" Orientation="Horizontal" Grid.Row="2" Grid.Column="0">
|
||||
<ui:PathIcon Data="M12.5,5A7.5,7.5 0 0,0 5,12.5A7.5,7.5 0 0,0 12.5,20A7.5,7.5 0 0,0 20,12.5A7.5,7.5 0 0,0 12.5,5M7,10H9A1,1 0 0,1 10,11V12C10,12.5 9.62,12.9 9.14,12.97L10.31,15H9.15L8,13V15H7M12,10H14V11H12V12H14V13H12V14H14V15H12A1,1 0 0,1 11,14V11A1,1 0 0,1 12,10M16,10H18V11H16V14H18V15H16A1,1 0 0,1 15,14V11A1,1 0 0,1 16,10M8,11V12H9V11" Foreground="Red" Width="15"/>
|
||||
<TextBlock Foreground="Red" Margin="3,0,0,0" Text="录制中"/>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="2" Grid.Row="2" Padding="3,1" HorizontalAlignment="Right" Margin="5,0" ToolTip="回放剪辑(正在处理中的数量)" Click="Button_Clip_Click">
|
||||
<Button.Visibility>
|
||||
<MultiBinding Converter="{StaticResource mbtvConverter}" Mode="OneWay">
|
||||
<Binding Path="IsRecording" Mode="OneWay"/>
|
||||
<Binding RelativeSource="{RelativeSource Mode=FindAncestor,AncestorType=pages:RootPage}"
|
||||
Path="DataContext.Recorder.Config.EnabledFeature" Mode="OneWay"
|
||||
Converter="{StaticResource ClipEnabledToBooleanConverter}"/>
|
||||
</MultiBinding>
|
||||
</Button.Visibility>
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<ui:PathIcon Width="10" Data="M19,3L13,9L15,11L22,4V3M12,12.5A0.5,0.5 0 0,1 11.5,12A0.5,0.5 0 0,1 12,11.5A0.5,0.5 0 0,1 12.5,12A0.5,0.5 0 0,1 12,12.5M6,20A2,2 0 0,1 4,18C4,16.89 4.9,16 6,16A2,2 0 0,1 8,18C8,19.11 7.1,20 6,20M6,8A2,2 0 0,1 4,6C4,4.89 4.9,4 6,4A2,2 0 0,1 8,6C8,7.11 7.1,8 6,8M9.64,7.64C9.87,7.14 10,6.59 10,6A4,4 0 0,0 6,2A4,4 0 0,0 2,6A4,4 0 0,0 6,10C6.59,10 7.14,9.87 7.64,9.64L10,12L7.64,14.36C7.14,14.13 6.59,14 6,14A4,4 0 0,0 2,18A4,4 0 0,0 6,22A4,4 0 0,0 10,18C10,17.41 9.87,16.86 9.64,16.36L12,14L19,21H22V20L9.64,7.64Z"/>
|
||||
<TextBlock VerticalAlignment="Center" Margin="2,0,0,2" Text="{Binding Processor.Clips.Count,FallbackValue=(-),StringFormat=({0}),Mode=OneWay}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
55
BililiveRecorder.WPF/Controls/RoomCard.xaml.cs
Normal file
55
BililiveRecorder.WPF/Controls/RoomCard.xaml.cs
Normal file
|
@ -0,0 +1,55 @@
|
|||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using BililiveRecorder.Core;
|
||||
|
||||
namespace BililiveRecorder.WPF.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for RoomCard.xaml
|
||||
/// </summary>
|
||||
public partial class RoomCard : UserControl
|
||||
{
|
||||
public RoomCard()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public event EventHandler DeleteRequested;
|
||||
|
||||
private void MenuItem_StartRecording_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
(DataContext as IRecordedRoom)?.StartRecord();
|
||||
}
|
||||
|
||||
private void MenuItem_StopRecording_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
(DataContext as IRecordedRoom)?.StopRecord();
|
||||
}
|
||||
|
||||
private void MenuItem_RefreshInfo_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
(DataContext as IRecordedRoom)?.RefreshRoomInfo();
|
||||
}
|
||||
|
||||
private void MenuItem_StartMonitor_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
(DataContext as IRecordedRoom)?.Start();
|
||||
}
|
||||
|
||||
private void MenuItem_StopMonitor_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
(DataContext as IRecordedRoom)?.Stop();
|
||||
}
|
||||
|
||||
private void MenuItem_DeleteRoom_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DeleteRequested?.Invoke(DataContext, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void Button_Clip_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
(DataContext as IRecordedRoom)?.Clip();
|
||||
}
|
||||
}
|
||||
}
|
37
BililiveRecorder.WPF/Controls/TaskbarIconControl.xaml
Normal file
37
BililiveRecorder.WPF/Controls/TaskbarIconControl.xaml
Normal file
|
@ -0,0 +1,37 @@
|
|||
<UserControl
|
||||
x:Class="BililiveRecorder.WPF.Controls.TaskbarIconControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
xmlns:tb="http://www.hardcodet.net/taskbar"
|
||||
xmlns:local="clr-namespace:BililiveRecorder.WPF.Controls"
|
||||
xmlns:wpf="clr-namespace:BililiveRecorder.WPF"
|
||||
xmlns:page="clr-namespace:BililiveRecorder.WPF.Pages"
|
||||
xmlns:conv="clr-namespace:BililiveRecorder.WPF.Converters"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources>
|
||||
<conv:IsNullToVisibilityConverter x:Key="IsNullToVisibilityConverter"/>
|
||||
</UserControl.Resources>
|
||||
<tb:TaskbarIcon x:Name="TaskbarIcon" IconSource="/ico.ico" TrayLeftMouseUp="TaskbarIcon_TrayLeftMouseUp"
|
||||
Visibility="{Binding Recorder, Converter={StaticResource IsNullToVisibilityConverter}}"
|
||||
ToolTipText="B站录播姬" PopupActivation="RightClick"
|
||||
>
|
||||
<tb:TaskbarIcon.TrayToolTip>
|
||||
<Border
|
||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||
BorderBrush="{DynamicResource SystemControlBackgroundAccentBrush}"
|
||||
BorderThickness="2"
|
||||
CornerRadius="5">
|
||||
<StackPanel Margin="10">
|
||||
<TextBlock Style="{DynamicResource SubtitleTextBlockStyle}" Text="B站录播姬"/>
|
||||
<TextBlock Text="{Binding Recorder.Config.WorkDirectory,Mode=OneWay}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</tb:TaskbarIcon.TrayToolTip>
|
||||
<tb:TaskbarIcon.TrayPopup>
|
||||
<local:TaskbarIconTrayPopup/>
|
||||
</tb:TaskbarIcon.TrayPopup>
|
||||
</tb:TaskbarIcon>
|
||||
</UserControl>
|
40
BililiveRecorder.WPF/Controls/TaskbarIconControl.xaml.cs
Normal file
40
BililiveRecorder.WPF/Controls/TaskbarIconControl.xaml.cs
Normal file
|
@ -0,0 +1,40 @@
|
|||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using Hardcodet.Wpf.TaskbarNotification;
|
||||
|
||||
namespace BililiveRecorder.WPF.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for TaskbarIconControl.xaml
|
||||
/// </summary>
|
||||
public partial class TaskbarIconControl : UserControl
|
||||
{
|
||||
public TaskbarIconControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
// AddHandler(NewMainWindow.ShowBalloonTipEvent, (RoutedEventHandler)UserControl_ShowBalloonTip);
|
||||
if (Application.Current.MainWindow is NewMainWindow nmw)
|
||||
{
|
||||
nmw.ShowBalloonTipCallback = (title, msg, sym) =>
|
||||
{
|
||||
TaskbarIcon.ShowBalloonTip(title, msg, sym);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private void TaskbarIcon_TrayLeftMouseUp(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// RaiseEvent(new RoutedEventArgs(NewMainWindow.SuperActivateEvent));
|
||||
(Application.Current.MainWindow as NewMainWindow)?.SuperActivateAction();
|
||||
}
|
||||
|
||||
/*
|
||||
private void UserControl_ShowBalloonTip(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var f = e as NewMainWindow.ShowBalloonTipRoutedEventArgs;
|
||||
TaskbarIcon.ShowBalloonTip(f.Title, f.Message, f.Symbol);
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
69
BililiveRecorder.WPF/Controls/TaskbarIconTrayPopup.xaml
Normal file
69
BililiveRecorder.WPF/Controls/TaskbarIconTrayPopup.xaml
Normal file
|
@ -0,0 +1,69 @@
|
|||
<UserControl
|
||||
x:Class="BililiveRecorder.WPF.Controls.TaskbarIconTrayPopup"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
xmlns:local="clr-namespace:BililiveRecorder.WPF.Controls"
|
||||
xmlns:models="clr-namespace:BililiveRecorder.WPF.Models"
|
||||
xmlns:converter="clr-namespace:BililiveRecorder.WPF.Converters"
|
||||
Foreground="{DynamicResource SystemControlPageTextBaseHighBrush}"
|
||||
mc:Ignorable="d" d:DataContext="{d:DesignInstance Type=models:RootModel}">
|
||||
<UserControl.Resources>
|
||||
<converter:BoolToValueConverter x:Key="BooleanToVisibilityConverter"
|
||||
TrueValue="{x:Static Visibility.Visible}"
|
||||
FalseValue="{x:Static Visibility.Hidden}" />
|
||||
</UserControl.Resources>
|
||||
<Border Height="250" Width="300"
|
||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||
BorderBrush="{DynamicResource SystemControlBackgroundAccentBrush}"
|
||||
BorderThickness="2"
|
||||
CornerRadius="5">
|
||||
<Grid Margin="15">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel>
|
||||
<TextBlock Style="{DynamicResource TitleTextBlockStyle}" Text="B站录播姬"/>
|
||||
<TextBlock Text="{Binding Recorder.Config.WorkDirectory,Mode=OneWay}" TextWrapping="NoWrap" TextTrimming="CharacterEllipsis"/>
|
||||
</StackPanel>
|
||||
<Separator Grid.Row="1" Grid.ColumnSpan="2"/>
|
||||
<ItemsControl Grid.Row="2" Grid.ColumnSpan="2" ItemsSource="{Binding Recorder, Mode=OneWay}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="0,5,0,0">
|
||||
<ui:PathIcon Data="M18.15,4.94C17.77,4.91 17.37,5 17,5.2L8.35,10.2C7.39,10.76 7.07,12 7.62,12.94L9.12,15.53C9.67,16.5 10.89,16.82 11.85,16.27L13.65,15.23C13.92,15.69 14.32,16.06 14.81,16.27V18.04C14.81,19.13 15.7,20 16.81,20H22V18.04H16.81V16.27C17.72,15.87 18.31,14.97 18.31,14C18.31,13.54 18.19,13.11 17.97,12.73L20.5,11.27C21.47,10.71 21.8,9.5 21.24,8.53L19.74,5.94C19.4,5.34 18.79,5 18.15,4.94M6.22,13.17L2,13.87L2.75,15.17L4.75,18.63L5.5,19.93L8.22,16.63L6.22,13.17Z"
|
||||
Visibility="{Binding IsMonitoring,Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||
Foreground="DarkOrange" Width="15" ToolTip="监控中"/>
|
||||
<ui:PathIcon Data="M12.5,5A7.5,7.5 0 0,0 5,12.5A7.5,7.5 0 0,0 12.5,20A7.5,7.5 0 0,0 20,12.5A7.5,7.5 0 0,0 12.5,5M7,10H9A1,1 0 0,1 10,11V12C10,12.5 9.62,12.9 9.14,12.97L10.31,15H9.15L8,13V15H7M12,10H14V11H12V12H14V13H12V14H14V15H12A1,1 0 0,1 11,14V11A1,1 0 0,1 12,10M16,10H18V11H16V14H18V15H16A1,1 0 0,1 15,14V11A1,1 0 0,1 16,10M8,11V12H9V11"
|
||||
Visibility="{Binding IsRecording,Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||
Foreground="Red" Width="15" ToolTip="录制中" Margin="3,0"/>
|
||||
<TextBlock Text="{Binding StreamerName, Mode=OneWay}"
|
||||
ToolTip="{Binding StreamerName, Mode=OneWay}"
|
||||
TextWrapping="NoWrap" TextTrimming="CharacterEllipsis"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
<ItemsControl.Template>
|
||||
<ControlTemplate>
|
||||
<ScrollViewer>
|
||||
<ItemsPresenter/>
|
||||
</ScrollViewer>
|
||||
</ControlTemplate>
|
||||
</ItemsControl.Template>
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<VirtualizingStackPanel IsItemsHost="True"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
15
BililiveRecorder.WPF/Controls/TaskbarIconTrayPopup.xaml.cs
Normal file
15
BililiveRecorder.WPF/Controls/TaskbarIconTrayPopup.xaml.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
using System.Windows.Controls;
|
||||
|
||||
namespace BililiveRecorder.WPF.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for TaskbarIconTrayPopup.xaml
|
||||
/// </summary>
|
||||
public partial class TaskbarIconTrayPopup : UserControl
|
||||
{
|
||||
public TaskbarIconTrayPopup()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<ui:ContentDialog
|
||||
x:Class="BililiveRecorder.WPF.Controls.WorkDirectorySelectorDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
xmlns:local="clr-namespace:BililiveRecorder.WPF.Controls"
|
||||
mc:Ignorable="d"
|
||||
d:DataContext="{d:DesignInstance Type=local:WorkDirectorySelectorDialog}"
|
||||
PrimaryButtonText="确定"
|
||||
DefaultButton="Primary"
|
||||
CloseButtonText="退出">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Text="选择工作目录" TextAlignment="Center" FontSize="26"/>
|
||||
<TextBlock Grid.Row="1" Text="{Binding Error}" Margin="0,5" TextAlignment="Center" FontSize="16" Foreground="#FFFF2828"/>
|
||||
<Grid Grid.Row="2">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox VerticalAlignment="Center" FontSize="14" Text="{Binding Path}"/>
|
||||
<Button Grid.Column="1" Click="Button_Click">浏览...</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ui:ContentDialog>
|
|
@ -0,0 +1,52 @@
|
|||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.WindowsAPICodePack.Dialogs;
|
||||
|
||||
namespace BililiveRecorder.WPF.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for WorkDirectorySelectorDialog.xaml
|
||||
/// </summary>
|
||||
public partial class WorkDirectorySelectorDialog : INotifyPropertyChanged
|
||||
{
|
||||
private string error = string.Empty;
|
||||
private string path = string.Empty;
|
||||
|
||||
public string Error { get => error; set => SetField(ref error, value); }
|
||||
|
||||
public string Path { get => path; set => SetField(ref path, value); }
|
||||
|
||||
public WorkDirectorySelectorDialog()
|
||||
{
|
||||
DataContext = this;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
protected virtual void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value)) { return false; }
|
||||
field = value; OnPropertyChanged(propertyName); return true;
|
||||
}
|
||||
|
||||
private void Button_Click(object sender, System.Windows.RoutedEventArgs e)
|
||||
{
|
||||
var fileDialog = new CommonOpenFileDialog()
|
||||
{
|
||||
IsFolderPicker = true,
|
||||
Multiselect = false,
|
||||
Title = "选择录播姬工作目录路径",
|
||||
AddToMostRecentlyUsedList = false,
|
||||
EnsurePathExists = true,
|
||||
NavigateToShortcut = true,
|
||||
InitialDirectory = Path,
|
||||
};
|
||||
if (fileDialog.ShowDialog() == CommonFileDialogResult.Ok)
|
||||
{
|
||||
Path = fileDialog.FileName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
30
BililiveRecorder.WPF/Converters/BoolToValueConverter.cs
Normal file
30
BililiveRecorder.WPF/Converters/BoolToValueConverter.cs
Normal file
|
@ -0,0 +1,30 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace BililiveRecorder.WPF.Converters
|
||||
{
|
||||
internal class BoolToValueConverter : IValueConverter
|
||||
{
|
||||
public object FalseValue { get; set; }
|
||||
public object TrueValue { get; set; }
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return FalseValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
return (bool)value ? TrueValue : FalseValue;
|
||||
}
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value != null ? value.Equals(TrueValue) : false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
26
BililiveRecorder.WPF/Converters/BooleanAndConverter.cs
Normal file
26
BililiveRecorder.WPF/Converters/BooleanAndConverter.cs
Normal file
|
@ -0,0 +1,26 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace BililiveRecorder.WPF.Converters
|
||||
{
|
||||
internal class BooleanAndConverter : IMultiValueConverter
|
||||
{
|
||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
foreach (var value in values)
|
||||
{
|
||||
if ((value is bool boolean) && boolean == false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
20
BililiveRecorder.WPF/Converters/BooleanInverterConverter.cs
Normal file
20
BililiveRecorder.WPF/Converters/BooleanInverterConverter.cs
Normal file
|
@ -0,0 +1,20 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace BililiveRecorder.WPF.Converters
|
||||
{
|
||||
internal class BooleanInverterConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return !(bool)value;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return !(bool)value;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using BililiveRecorder.FlvProcessor;
|
||||
|
||||
namespace BililiveRecorder.WPF.Converters
|
||||
{
|
||||
internal class ClipEnabledToBooleanConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return !(value is EnabledFeature v) || (EnabledFeature.RecordOnly != v);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
19
BililiveRecorder.WPF/Converters/EnumToBooleanConverter.cs
Normal file
19
BililiveRecorder.WPF/Converters/EnumToBooleanConverter.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace BililiveRecorder.WPF.Converters
|
||||
{
|
||||
internal class EnumToBooleanConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value.Equals(parameter);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value.Equals(true) ? parameter : Binding.DoNothing;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace BililiveRecorder.WPF.Converters
|
||||
{
|
||||
internal class IsNullToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value is null ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
29
BililiveRecorder.WPF/Converters/MultiBoolToValueConverter.cs
Normal file
29
BililiveRecorder.WPF/Converters/MultiBoolToValueConverter.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace BililiveRecorder.WPF.Converters
|
||||
{
|
||||
public class MultiBoolToValueConverter : IMultiValueConverter
|
||||
{
|
||||
public object FalseValue { get; set; }
|
||||
public object TrueValue { get; set; }
|
||||
|
||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
foreach (var value in values)
|
||||
{
|
||||
if ((value is bool boolean) && boolean == false)
|
||||
{
|
||||
return FalseValue;
|
||||
}
|
||||
}
|
||||
return TrueValue;
|
||||
}
|
||||
|
||||
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
13
BililiveRecorder.WPF/Converters/NullValueTemplateSelector.cs
Normal file
13
BililiveRecorder.WPF/Converters/NullValueTemplateSelector.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace BililiveRecorder.WPF.Converters
|
||||
{
|
||||
internal class NullValueTemplateSelector : DataTemplateSelector
|
||||
{
|
||||
public DataTemplate Normal { get; set; }
|
||||
public DataTemplate Null { get; set; }
|
||||
|
||||
public override DataTemplate SelectTemplate(object item, DependencyObject container) => item is null ? Null : Normal;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace BililiveRecorder.WPF.Converters
|
||||
{
|
||||
internal class PercentageToColorBrushConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
const double a = 1d;
|
||||
const double b = 6d;
|
||||
const double c = 100d;
|
||||
const double d = 2.2d;
|
||||
|
||||
var x = (double)value * 100d;
|
||||
var y = x < (c - a) ? c - Math.Pow(Math.Abs(x - c + a), d) / b : x > (c + a) ? c - Math.Pow(x - c - a, d) / b : c;
|
||||
return new SolidColorBrush(GradientPick(Math.Max(y, 0d) / 100d, Colors.Red, Colors.Yellow, Colors.Lime));
|
||||
Color GradientPick(double percentage, Color c1, Color c2, Color c3) => percentage < 0.5 ? ColorInterp(c1, c2, percentage / 0.5) : percentage == 0.5 ? c2 : ColorInterp(c2, c3, (percentage - 0.5) / 0.5);
|
||||
Color ColorInterp(Color start, Color end, double percentage) => Color.FromRgb(LinearInterp(start.R, end.R, percentage), LinearInterp(start.G, end.G, percentage), LinearInterp(start.B, end.B, percentage));
|
||||
byte LinearInterp(byte start, byte end, double percentage) => (byte)(start + Math.Round(percentage * (end - start)));
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using BililiveRecorder.Core;
|
||||
using BililiveRecorder.Core.Config;
|
||||
|
||||
namespace BililiveRecorder.WPF.Converters
|
||||
{
|
||||
internal class RoomListInterceptConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value is RecorderWrapper ? value : value is IRecorder recorder ? new RecorderWrapper(recorder) : value;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
private class RecorderWrapper : ObservableCollection<IRecordedRoom>
|
||||
{
|
||||
private readonly IRecorder recorder;
|
||||
|
||||
public RecorderWrapper(IRecorder recorder) : base(recorder)
|
||||
{
|
||||
this.recorder = recorder;
|
||||
Add(null);
|
||||
|
||||
recorder.CollectionChanged += (sender, e) =>
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
if (e.NewItems.Count != 1) throw new NotImplementedException("Wrapper Add Item Count != 1");
|
||||
InsertItem(e.NewStartingIndex, e.NewItems[0] as IRecordedRoom);
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
if (e.OldItems.Count != 1) throw new NotImplementedException("Wrapper Remove Item Count != 1");
|
||||
if (!Remove(e.OldItems[0] as IRecordedRoom)) throw new NotImplementedException("Wrapper Remove Item Sync Fail");
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
throw new NotImplementedException("Wrapper Replace Item");
|
||||
case NotifyCollectionChangedAction.Move:
|
||||
throw new NotImplementedException("Wrapper Move Item");
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
ClearItems();
|
||||
Add(null);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace BililiveRecorder.WPF.Converters
|
||||
{
|
||||
internal class ShortRoomIdToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value is int i ? i == 0 ? Visibility.Collapsed : Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
21
BililiveRecorder.WPF/Converters/ValueConverterGroup.cs
Normal file
21
BililiveRecorder.WPF/Converters/ValueConverterGroup.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace BililiveRecorder.WPF.Converters
|
||||
{
|
||||
internal class ValueConverterGroup : List<IValueConverter>, IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return this.Aggregate(value, (current, converter) => converter.Convert(current, targetType, parameter, culture));
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
8
BililiveRecorder.WPF/DesignTimeResources.xaml
Normal file
8
BililiveRecorder.WPF/DesignTimeResources.xaml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019">
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ui:IntellisenseResources Source="/ModernWpf;component/DesignTime/DesignTimeResources.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
|
@ -1,71 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BililiveRecorder.WPF
|
||||
{
|
||||
internal static class EnumerableExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the elements with the maximum key value by using the default comparer to compare key values.
|
||||
/// </summary>
|
||||
/// <typeparam name="TSource">Source sequence element type.</typeparam>
|
||||
/// <typeparam name="TKey">Key type.</typeparam>
|
||||
/// <param name="source">Source sequence.</param>
|
||||
/// <param name="keySelector">Key selector used to extract the key for each element in the sequence.</param>
|
||||
/// <returns>List with the elements that share the same maximum key value.</returns>
|
||||
public static IList<TSource> MaxBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
|
||||
{
|
||||
if (source == null) { throw new ArgumentNullException("source"); }
|
||||
if (keySelector == null) { throw new ArgumentNullException("keySelector"); }
|
||||
|
||||
return MaxBy(source, keySelector, Comparer<TKey>.Default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the elements with the minimum key value by using the specified comparer to compare key values.
|
||||
/// </summary>
|
||||
/// <typeparam name="TSource">Source sequence element type.</typeparam>
|
||||
/// <typeparam name="TKey">Key type.</typeparam>
|
||||
/// <param name="source">Source sequence.</param>
|
||||
/// <param name="keySelector">Key selector used to extract the key for each element in the sequence.</param>
|
||||
/// <param name="comparer">Comparer used to determine the maximum key value.</param>
|
||||
/// <returns>List with the elements that share the same maximum key value.</returns>
|
||||
public static IList<TSource> MaxBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IComparer<TKey> comparer)
|
||||
{
|
||||
if (source == null) { throw new ArgumentNullException("source"); }
|
||||
if (keySelector == null) { throw new ArgumentNullException("keySelector"); }
|
||||
if (comparer == null) { throw new ArgumentNullException("comparer"); }
|
||||
|
||||
return ExtremaBy(source, keySelector, (key, minValue) => comparer.Compare(key, minValue));
|
||||
}
|
||||
|
||||
private static IList<TSource> ExtremaBy<TSource, TKey>(IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TKey, TKey, int> compare)
|
||||
{
|
||||
var result = new List<TSource>();
|
||||
|
||||
using (var e = source.GetEnumerator())
|
||||
{
|
||||
if (!e.MoveNext()) { throw new InvalidOperationException("Source sequence doesn't contain any elements."); }
|
||||
|
||||
var current = e.Current;
|
||||
var resKey = keySelector(current);
|
||||
result.Add(current);
|
||||
|
||||
while (e.MoveNext())
|
||||
{
|
||||
var cur = e.Current;
|
||||
var key = keySelector(cur);
|
||||
|
||||
var cmp = compare(key, resKey);
|
||||
if (cmp == 0) { result.Add(cur); }
|
||||
else if (cmp > 0)
|
||||
{
|
||||
result = new List<TSource> { cur };
|
||||
resKey = key;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,221 +0,0 @@
|
|||
<Window x:Class="BililiveRecorder.WPF.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:tb="http://www.hardcodet.net/taskbar"
|
||||
xmlns:core="clr-namespace:BililiveRecorder.Core;assembly=BililiveRecorder.Core"
|
||||
xmlns:local="clr-namespace:BililiveRecorder.WPF"
|
||||
d:DataContext="{d:DesignInstance Type=core:Recorder}"
|
||||
mc:Ignorable="d"
|
||||
MinHeight="400" MinWidth="650"
|
||||
Title="录播姬" Height="450" Width="850"
|
||||
Loaded="Window_Loaded" Closing="Window_Closing" StateChanged="Window_StateChanged">
|
||||
<Grid>
|
||||
<Grid.Resources>
|
||||
<ResourceDictionary>
|
||||
<DataTemplate x:Key="LogTemplate" >
|
||||
<TextBlock Text="{Binding}" TextWrapping="Wrap" MouseRightButtonUp="TextBlock_MouseRightButtonUp"/>
|
||||
</DataTemplate>
|
||||
<local:RecordStatusConverter x:Key="RSC"/>
|
||||
<local:ClipButtonVisibilityConverter x:Key="ClipButtonVisibilityConverter"/>
|
||||
<local:BoolToStringConverter x:Key="RecordStatusConverter" TrueValue="录制中" FalseValue="闲置"/>
|
||||
<local:BoolToStringConverter x:Key="MonitorStatusConverter" TrueValue="自动录制" FalseValue="非自动"/>
|
||||
</ResourceDictionary>
|
||||
</Grid.Resources>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<!--<local:UpdateBarUserControl Grid.Row="0" x:Name="UpdateBar"/>-->
|
||||
<tb:TaskbarIcon x:Name="NotifyIcon" Visibility="Collapsed" IconSource="/ico.ico" TrayLeftMouseUp="Taskbar_Click">
|
||||
<tb:TaskbarIcon.TrayToolTip>
|
||||
<Border BorderBrush="Black" BorderThickness="1" Background="White" CornerRadius="1">
|
||||
<StackPanel Margin="5">
|
||||
<TextBlock FontSize="14" Text="{Binding Title}"/>
|
||||
<TextBlock FontSize="13" Text="{Binding Recorder.Config.WorkDirectory,Mode=OneWay}" Margin="0,0,0,7"/>
|
||||
<ItemsControl ItemsSource="{Binding Recorder}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock>
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource RSC}" Mode="OneWay">
|
||||
<Binding Path="IsMonitoring" Mode="OneWay"/>
|
||||
<Binding Path="IsRecording" Mode="OneWay"/>
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
<TextBlock Text="{Binding DownloadSpeedPersentage,StringFormat=0.##%,Mode=OneWay}" Margin="5,0,0,0"/>
|
||||
<TextBlock Margin="5,0" Text="{Binding RoomId,Mode=OneWay}"/>
|
||||
<TextBlock Text="{Binding StreamerName,Mode=OneWay}"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</tb:TaskbarIcon.TrayToolTip>
|
||||
<tb:TaskbarIcon.ContextMenu>
|
||||
<ContextMenu DataContext="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource Self}}">
|
||||
<!--<MenuItem IsCheckable="True">不弹出提醒</MenuItem>-->
|
||||
<MenuItem Header="退出" Click="Taskbar_Quit_Click"/>
|
||||
</ContextMenu>
|
||||
</tb:TaskbarIcon.ContextMenu>
|
||||
</tb:TaskbarIcon>
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="8*"/>
|
||||
<RowDefinition Height="2*"/>
|
||||
<RowDefinition Height="4*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="2*"/>
|
||||
<ColumnDefinition Width="1.1*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<DataGrid x:Name="RoomList" ItemsSource="{Binding Recorder}"
|
||||
SelectionMode="Extended" SelectionUnit="FullRow"
|
||||
CanUserAddRows="False" CanUserDeleteRows="False"
|
||||
AutoGenerateColumns="False" IsReadOnly="True">
|
||||
<DataGrid.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="刷新信息" Click="RefreshRoomInfo"/>
|
||||
<MenuItem Header="删除" Click="RemoveRecRoom"/>
|
||||
</ContextMenu>
|
||||
</DataGrid.ContextMenu>
|
||||
<DataGrid.Resources>
|
||||
<local:BindingProxy x:Key="proxy" Data="{Binding Recorder}" />
|
||||
</DataGrid.Resources>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTemplateColumn Visibility="{Binding Data.Config.EnabledFeature,Converter={StaticResource ClipButtonVisibilityConverter},Source={StaticResource proxy}}" Header="回放剪辑">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Button Click="Clip_Click" Content="{Binding Processor.Clips.Count,Mode=OneWay,FallbackValue=0}" ContentStringFormat="剪辑 ({0})"/>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTextColumn Binding="{Binding ShortRoomId,Mode=OneWay}" Header="短号"/>
|
||||
<DataGridTextColumn Binding="{Binding RoomId,Mode=OneWay}" Header="房间号"/>
|
||||
<DataGridTextColumn Binding="{Binding StreamerName,Mode=OneWay}" Header="主播名字"/>
|
||||
<DataGridTextColumn Binding="{Binding IsRecording,Converter={StaticResource RecordStatusConverter},Mode=OneWay}" Header="录制状态"/>
|
||||
<DataGridTextColumn Binding="{Binding Title,Mode=OneWay}" Header="直播间标题"/>
|
||||
<DataGridTextColumn Binding="{Binding IsMonitoring,Converter={StaticResource MonitorStatusConverter},Mode=OneWay}" Header="是否自动录制"/>
|
||||
<DataGridTextColumn Binding="{Binding DownloadSpeedMegaBitps,StringFormat=0.## Mbps,Mode=OneWay}" Header="实时下载速度"/>
|
||||
<DataGridTextColumn Binding="{Binding DownloadSpeedPersentage,StringFormat=0.## %,Mode=OneWay}" Header="录制速度比"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
<ItemsControl Grid.Row="1" Grid.RowSpan="2" x:Name="Log" ItemsSource="{Binding Logs}" ItemTemplate="{StaticResource LogTemplate}" ToolTip="右键点击可以复制单行日志">
|
||||
<ItemsControl.Template>
|
||||
<ControlTemplate>
|
||||
<ScrollViewer x:Name="LogScroll" CanContentScroll="True">
|
||||
<ItemsPresenter/>
|
||||
</ScrollViewer>
|
||||
</ControlTemplate>
|
||||
</ItemsControl.Template>
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<VirtualizingStackPanel IsItemsHost="True"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
<Grid Grid.Column="1" DataContext="{Binding ElementName=RoomList,Path=SelectedItem}" Grid.RowSpan="2">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="10"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="25"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
<ColumnDefinition Width="2*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right">短号:</TextBlock>
|
||||
<TextBlock Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left" Text="{Binding ShortRoomId,Mode=OneWay}"/>
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right">房间号:</TextBlock>
|
||||
<TextBlock Grid.Row="2" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left" Text="{Binding RoomId,Mode=OneWay}"/>
|
||||
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right">主播名字:</TextBlock>
|
||||
<TextBlock Grid.Row="3" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left" Text="{Binding StreamerName,Mode=OneWay}"/>
|
||||
|
||||
<TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right">录制状态:</TextBlock>
|
||||
<TextBlock Grid.Row="4" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left" >
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource RSC}" Mode="OneWay">
|
||||
<Binding Path="IsMonitoring" Mode="OneWay"/>
|
||||
<Binding Path="IsRecording" Mode="OneWay"/>
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
|
||||
<TextBlock Grid.Row="5" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right">处理中剪辑数量:</TextBlock>
|
||||
<TextBlock Grid.Row="5" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left" Text="{Binding Processor.Clips.Count,Mode=OneWay}"/>
|
||||
|
||||
<TextBlock Grid.Row="6" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right">实时下载速度:</TextBlock>
|
||||
<TextBlock Grid.Row="6" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left" Text="{Binding DownloadSpeedMegaBitps,StringFormat=0.## Mbps,Mode=OneWay}"/>
|
||||
|
||||
<TextBlock Grid.Row="7" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right">录制速度比:</TextBlock>
|
||||
<TextBlock Grid.Row="7" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left" Text="{Binding DownloadSpeedPersentage,StringFormat=0.## %,Mode=OneWay}"/>
|
||||
|
||||
|
||||
<Grid Grid.Row="9" Grid.ColumnSpan="2">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button Grid.Column="0" Margin="5,0" Click="EnableAutoRec">启用自动录制</Button>
|
||||
<Button Grid.Column="1" Margin="5,0" Click="DisableAutoRec">禁用自动录制</Button>
|
||||
<Button Grid.Row="1" Grid.Column="0" Margin="5" Click="TriggerRec">手动触发开始录制</Button>
|
||||
<Button Grid.Row="1" Grid.Column="1" Margin="5" Click="CutRec">切断当前录制</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid Grid.Column="1" Grid.Row="2">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Grid.Row="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock VerticalAlignment="Center" Margin="5,2" Grid.Column="0">添加直播间:</TextBlock>
|
||||
<TextBox VerticalAlignment="Center" Margin="0,2" Grid.Column="1" x:Name="AddRoomidTextBox" KeyUp="AddRoomidTextBox_KeyUp"/>
|
||||
<Button VerticalAlignment="Center" Margin="5,2" Padding="10,0" Grid.Column="2" x:Name="AddRoomidButton" Click="AddRoomidButton_Click">添加</Button>
|
||||
</Grid>
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button Grid.Column="0" Margin="5,2" Click="EnableAllAutoRec">启用所有直播间</Button>
|
||||
<Button Grid.Column="1" Margin="5,2" Click="DisableAllAutoRec">禁用所有直播间</Button>
|
||||
</Grid>
|
||||
<Grid Grid.Row="3">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button x:Name="SettingsButton" Click="SettingsButton_Click" Width="50" Margin="5" Content="设置" Grid.Column="1"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
|
@ -1,414 +0,0 @@
|
|||
using Autofac;
|
||||
using BililiveRecorder.Core;
|
||||
using BililiveRecorder.FlvProcessor;
|
||||
using CommandLine;
|
||||
using Hardcodet.Wpf.TaskbarNotification;
|
||||
using NLog;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace BililiveRecorder.WPF
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for MainWindow.xaml
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
private static readonly Regex UrlToRoomidRegex = new Regex(@"^https?:\/\/live\.bilibili\.com\/(?<roomid>\d+)(?:[#\?].*)?$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline);
|
||||
|
||||
private const int MAX_LOG_ROW = 30;
|
||||
private const string LAST_WORK_DIR_FILE = "lastworkdir";
|
||||
|
||||
private IContainer Container { get; set; }
|
||||
private ILifetimeScope RootScope { get; set; }
|
||||
|
||||
public IRecorder Recorder { get; set; }
|
||||
public ObservableCollection<string> Logs { get; set; } =
|
||||
new ObservableCollection<string>()
|
||||
{
|
||||
"当前版本:" + BuildInfo.Version,
|
||||
"网站: https://rec.danmuji.org",
|
||||
"更新日志: https://rec.danmuji.org/allposts",
|
||||
"问题反馈邮箱: rec@danmuji.org",
|
||||
"QQ群: 689636812",
|
||||
"",
|
||||
"删除直播间按钮在列表右键菜单里",
|
||||
"新增了弹幕录制功能,默认关闭,设置里可开启",
|
||||
"",
|
||||
"录制速度比 在 100% 左右说明跟上了主播直播的速度",
|
||||
"小于 100% 说明录播电脑的下载带宽不够,跟不上录制直播"
|
||||
};
|
||||
|
||||
public static void AddLog(string message) => _AddLog?.Invoke(message);
|
||||
private static Action<string> _AddLog;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
|
||||
_AddLog = (message) =>
|
||||
Log.Dispatcher.BeginInvoke(
|
||||
DispatcherPriority.DataBind,
|
||||
new Action(() => { Logs.Add(message); while (Logs.Count > MAX_LOG_ROW) { Logs.RemoveAt(0); } })
|
||||
);
|
||||
|
||||
var builder = new ContainerBuilder();
|
||||
builder.RegisterModule<FlvProcessorModule>();
|
||||
builder.RegisterModule<CoreModule>();
|
||||
Container = builder.Build();
|
||||
RootScope = Container.BeginLifetimeScope("recorder_root");
|
||||
|
||||
Recorder = RootScope.Resolve<IRecorder>();
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
DataContext = this;
|
||||
}
|
||||
|
||||
private void Window_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Title += " " + BuildInfo.Version + " " + BuildInfo.HeadShaShort;
|
||||
|
||||
bool skip_ui = false;
|
||||
string workdir = string.Empty;
|
||||
|
||||
CommandLineOption commandLineOption = null;
|
||||
Parser.Default
|
||||
.ParseArguments<CommandLineOption>(Environment.GetCommandLineArgs())
|
||||
.WithParsed(x => commandLineOption = x);
|
||||
|
||||
if (commandLineOption?.WorkDirectory != null)
|
||||
{
|
||||
skip_ui = true;
|
||||
workdir = commandLineOption.WorkDirectory;
|
||||
}
|
||||
|
||||
if (!skip_ui)
|
||||
{
|
||||
try
|
||||
{
|
||||
workdir = File.ReadAllText(LAST_WORK_DIR_FILE);
|
||||
}
|
||||
catch (Exception) { }
|
||||
var wdw = new WorkDirectoryWindow()
|
||||
{
|
||||
Owner = this,
|
||||
WorkPath = workdir,
|
||||
};
|
||||
|
||||
if (wdw.ShowDialog() == true)
|
||||
{
|
||||
workdir = wdw.WorkPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
Environment.Exit(-1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Recorder.Initialize(workdir))
|
||||
{
|
||||
if (!skip_ui)
|
||||
{
|
||||
MessageBox.Show("初始化错误", "录播姬", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
Environment.Exit(-2);
|
||||
return;
|
||||
}
|
||||
|
||||
NotifyIcon.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
if (new TimedMessageBox
|
||||
{
|
||||
Title = "关闭录播姬?",
|
||||
Message = "确定要关闭录播姬吗?",
|
||||
CountDown = 10,
|
||||
Left = Left,
|
||||
Top = Top
|
||||
}.ShowDialog() == true)
|
||||
{
|
||||
_AddLog = null;
|
||||
Recorder.Shutdown();
|
||||
try
|
||||
{
|
||||
File.WriteAllText(LAST_WORK_DIR_FILE, Recorder.Config.WorkDirectory);
|
||||
}
|
||||
catch (Exception) { }
|
||||
}
|
||||
else
|
||||
{
|
||||
e.Cancel = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void TextBlock_MouseRightButtonUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (sender is TextBlock textBlock)
|
||||
{
|
||||
Clipboard.SetText(textBlock.Text);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 触发回放剪辑
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void Clip_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var rr = _GetSenderAsRecordedRoom(sender);
|
||||
if (rr == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Task.Run(() => rr.Clip());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启用自动录制
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void EnableAutoRec(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var rr = _GetSenderAsRecordedRoom(sender);
|
||||
if (rr == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
rr.Start();
|
||||
Recorder.SaveConfigToFile();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 禁用自动录制
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void DisableAutoRec(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var rr = _GetSenderAsRecordedRoom(sender);
|
||||
if (rr == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
rr.Stop();
|
||||
Recorder.SaveConfigToFile();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动触发尝试录制
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void TriggerRec(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var rr = _GetSenderAsRecordedRoom(sender);
|
||||
if (rr == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Task.Run(() => rr.StartRecord());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切断当前录制
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void CutRec(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var rr = _GetSenderAsRecordedRoom(sender);
|
||||
if (rr == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Task.Run(() => rr.StopRecord());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除当前房间
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void RemoveRecRoom(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var rr = (IRecordedRoom)((DataGrid)((ContextMenu)((MenuItem)sender)?.Parent)?.PlacementTarget)?.SelectedItem;
|
||||
if (rr == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Recorder.RemoveRoom(rr);
|
||||
Recorder.SaveConfigToFile();
|
||||
}
|
||||
|
||||
private void RefreshRoomInfo(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var rr = (IRecordedRoom)((DataGrid)((ContextMenu)((MenuItem)sender)?.Parent)?.PlacementTarget)?.SelectedItem;
|
||||
if (rr == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
rr.RefreshRoomInfo();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 全部直播间启用自动录制
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private async void EnableAllAutoRec(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await Task.WhenAll(Recorder.ToList().Select(rr => Task.Run(() => rr.Start())));
|
||||
Recorder.SaveConfigToFile();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 全部直播间禁用自动录制
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private async void DisableAllAutoRec(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await Task.WhenAll(Recorder.ToList().Select(rr => Task.Run(() => rr.Stop())));
|
||||
Recorder.SaveConfigToFile();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 输入框按下回车键
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void AddRoomidTextBox_KeyUp(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Enter)
|
||||
AddRoom();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加直播间按钮
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void AddRoomidButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
AddRoom();
|
||||
}
|
||||
|
||||
private void AddRoom()
|
||||
{
|
||||
var match = UrlToRoomidRegex.Match(AddRoomidTextBox.Text);
|
||||
if (match.Success)
|
||||
{
|
||||
if (int.TryParse(match.Groups["roomid"].Value, out int roomid))
|
||||
{
|
||||
Add(roomid);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Warn("添加房间时发生了不应该出现的错误");
|
||||
}
|
||||
}
|
||||
else if (int.TryParse(AddRoomidTextBox.Text, out int roomid))
|
||||
{
|
||||
Add(roomid);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Info("房间号是数字!");
|
||||
}
|
||||
AddRoomidTextBox.Text = string.Empty;
|
||||
|
||||
void Add(int roomid)
|
||||
{
|
||||
if (roomid > 0)
|
||||
{
|
||||
if (Recorder.Any(x => x.RoomId == roomid || x.ShortRoomId == roomid))
|
||||
{
|
||||
logger.Info("该直播间已经添加过了");
|
||||
}
|
||||
else
|
||||
{
|
||||
Recorder.AddRoom(roomid);
|
||||
Recorder.SaveConfigToFile();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Info("房间号是大于0的数字!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SettingsButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ShowSettingsWindow();
|
||||
}
|
||||
|
||||
private void ShowSettingsWindow()
|
||||
{
|
||||
var sw = new SettingsWindow(this, Recorder.Config);
|
||||
if (sw.ShowDialog() == true)
|
||||
{
|
||||
sw.Config.CopyPropertiesTo(Recorder.Config);
|
||||
}
|
||||
Recorder.SaveConfigToFile();
|
||||
}
|
||||
|
||||
private IRecordedRoom _GetSenderAsRecordedRoom(object sender) => (sender as Button)?.DataContext as IRecordedRoom;
|
||||
|
||||
private void Taskbar_Quit_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
private void Window_StateChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (WindowState == WindowState.Minimized)
|
||||
{
|
||||
Hide();
|
||||
NotifyIcon.ShowBalloonTip("B站录播姬", "录播姬已最小化到托盘,左键单击图标恢复界面。", BalloonIcon.Info);
|
||||
}
|
||||
}
|
||||
|
||||
private void Taskbar_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Show();
|
||||
WindowState = WindowState.Normal;
|
||||
Topmost ^= true;
|
||||
Topmost ^= true;
|
||||
Focus();
|
||||
}
|
||||
}
|
||||
}
|
113
BililiveRecorder.WPF/MockData/MockRecordedRoom.cs
Normal file
113
BililiveRecorder.WPF/MockData/MockRecordedRoom.cs
Normal file
|
@ -0,0 +1,113 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using BililiveRecorder.Core;
|
||||
using BililiveRecorder.FlvProcessor;
|
||||
|
||||
namespace BililiveRecorder.WPF.MockData
|
||||
{
|
||||
#if DEBUG
|
||||
internal class MockRecordedRoom : IRecordedRoom
|
||||
{
|
||||
private bool disposedValue;
|
||||
|
||||
public MockRecordedRoom()
|
||||
{
|
||||
RoomId = 123456789;
|
||||
ShortRoomId = 1234;
|
||||
StreamerName = "Mock主播名Mock主播名Mock主播名Mock主播名";
|
||||
IsMonitoring = false;
|
||||
IsRecording = true;
|
||||
DownloadSpeedPersentage = 100d;
|
||||
DownloadSpeedMegaBitps = 2.45d;
|
||||
}
|
||||
|
||||
public int ShortRoomId { get; set; }
|
||||
|
||||
public int RoomId { get; set; }
|
||||
|
||||
public string StreamerName { get; set; }
|
||||
|
||||
public IStreamMonitor StreamMonitor { get; set; }
|
||||
|
||||
public IFlvStreamProcessor Processor { get; set; }
|
||||
|
||||
public bool IsMonitoring { get; set; }
|
||||
|
||||
public bool IsRecording { get; set; }
|
||||
|
||||
public double DownloadSpeedPersentage { get; set; }
|
||||
|
||||
public double DownloadSpeedMegaBitps { get; set; }
|
||||
|
||||
public DateTime LastUpdateDateTime { get; set; }
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
public void Clip()
|
||||
{
|
||||
}
|
||||
|
||||
public void RefreshRoomInfo()
|
||||
{
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
}
|
||||
|
||||
public bool Start()
|
||||
{
|
||||
IsMonitoring = true;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsMonitoring)));
|
||||
return true;
|
||||
}
|
||||
|
||||
public void StartRecord()
|
||||
{
|
||||
IsRecording = true;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsRecording)));
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
IsMonitoring = false;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsMonitoring)));
|
||||
}
|
||||
|
||||
public void StopRecord()
|
||||
{
|
||||
IsRecording = false;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsRecording)));
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
// TODO: dispose managed state (managed objects)
|
||||
}
|
||||
|
||||
// TODO: free unmanaged resources (unmanaged objects) and override finalizer
|
||||
// TODO: set large fields to null
|
||||
disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
// // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
|
||||
// ~MockRecordedRoom()
|
||||
// {
|
||||
// // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
// Dispose(disposing: false);
|
||||
// }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
165
BililiveRecorder.WPF/MockData/MockRecorder.cs
Normal file
165
BililiveRecorder.WPF/MockData/MockRecorder.cs
Normal file
|
@ -0,0 +1,165 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using BililiveRecorder.Core;
|
||||
using BililiveRecorder.Core.Config;
|
||||
|
||||
namespace BililiveRecorder.WPF.MockData
|
||||
{
|
||||
#if DEBUG
|
||||
internal class MockRecorder : IRecorder
|
||||
{
|
||||
private bool disposedValue;
|
||||
|
||||
public MockRecorder()
|
||||
{
|
||||
Rooms.Add(new MockRecordedRoom
|
||||
{
|
||||
IsMonitoring = false,
|
||||
IsRecording = false
|
||||
});
|
||||
Rooms.Add(new MockRecordedRoom
|
||||
{
|
||||
IsMonitoring = true,
|
||||
IsRecording = false
|
||||
});
|
||||
Rooms.Add(new MockRecordedRoom
|
||||
{
|
||||
DownloadSpeedPersentage = 100,
|
||||
DownloadSpeedMegaBitps = 12.45
|
||||
});
|
||||
Rooms.Add(new MockRecordedRoom
|
||||
{
|
||||
DownloadSpeedPersentage = 95,
|
||||
DownloadSpeedMegaBitps = 789.45
|
||||
});
|
||||
Rooms.Add(new MockRecordedRoom
|
||||
{
|
||||
DownloadSpeedPersentage = 90
|
||||
});
|
||||
Rooms.Add(new MockRecordedRoom
|
||||
{
|
||||
DownloadSpeedPersentage = 85
|
||||
});
|
||||
Rooms.Add(new MockRecordedRoom
|
||||
{
|
||||
DownloadSpeedPersentage = 80
|
||||
});
|
||||
Rooms.Add(new MockRecordedRoom
|
||||
{
|
||||
DownloadSpeedPersentage = 75
|
||||
});
|
||||
Rooms.Add(new MockRecordedRoom
|
||||
{
|
||||
DownloadSpeedPersentage = 70
|
||||
});
|
||||
Rooms.Add(new MockRecordedRoom
|
||||
{
|
||||
DownloadSpeedPersentage = 109
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private ObservableCollection<IRecordedRoom> Rooms { get; } = new ObservableCollection<IRecordedRoom>();
|
||||
|
||||
public ConfigV1 Config { get; } = new ConfigV1();
|
||||
|
||||
public int Count => Rooms.Count;
|
||||
|
||||
public bool IsReadOnly => true;
|
||||
|
||||
int ICollection<IRecordedRoom>.Count => Rooms.Count;
|
||||
|
||||
bool ICollection<IRecordedRoom>.IsReadOnly => true;
|
||||
|
||||
public IRecordedRoom this[int index] => Rooms[index];
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged
|
||||
{
|
||||
add => (Rooms as INotifyPropertyChanged).PropertyChanged += value;
|
||||
remove => (Rooms as INotifyPropertyChanged).PropertyChanged -= value;
|
||||
}
|
||||
|
||||
public event NotifyCollectionChangedEventHandler CollectionChanged
|
||||
{
|
||||
add => (Rooms as INotifyCollectionChanged).CollectionChanged += value;
|
||||
remove => (Rooms as INotifyCollectionChanged).CollectionChanged -= value;
|
||||
}
|
||||
|
||||
void ICollection<IRecordedRoom>.Add(IRecordedRoom item) => throw new NotSupportedException("Collection is readonly");
|
||||
|
||||
void ICollection<IRecordedRoom>.Clear() => throw new NotSupportedException("Collection is readonly");
|
||||
|
||||
bool ICollection<IRecordedRoom>.Remove(IRecordedRoom item) => throw new NotSupportedException("Collection is readonly");
|
||||
|
||||
bool ICollection<IRecordedRoom>.Contains(IRecordedRoom item) => Rooms.Contains(item);
|
||||
|
||||
void ICollection<IRecordedRoom>.CopyTo(IRecordedRoom[] array, int arrayIndex) => Rooms.CopyTo(array, arrayIndex);
|
||||
|
||||
public IEnumerator<IRecordedRoom> GetEnumerator() => Rooms.GetEnumerator();
|
||||
|
||||
IEnumerator<IRecordedRoom> IEnumerable<IRecordedRoom>.GetEnumerator() => Rooms.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => Rooms.GetEnumerator();
|
||||
|
||||
public bool Initialize(string workdir)
|
||||
{
|
||||
Config.WorkDirectory = workdir;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void AddRoom(int roomid)
|
||||
{
|
||||
AddRoom(roomid, false);
|
||||
}
|
||||
|
||||
public void AddRoom(int roomid, bool enabled)
|
||||
{
|
||||
Rooms.Add(new MockRecordedRoom { RoomId = roomid, IsMonitoring = enabled });
|
||||
}
|
||||
|
||||
public void RemoveRoom(IRecordedRoom rr)
|
||||
{
|
||||
Rooms.Remove(rr);
|
||||
}
|
||||
|
||||
public void SaveConfigToFile()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
// dispose managed state (managed objects)
|
||||
Rooms.Clear();
|
||||
}
|
||||
|
||||
// free unmanaged resources (unmanaged objects) and override finalizer
|
||||
// set large fields to null
|
||||
disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
// override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
|
||||
// ~MockRecorder()
|
||||
// {
|
||||
// // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
// Dispose(disposing: false);
|
||||
// }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
67
BililiveRecorder.WPF/Models/LogModel.cs
Normal file
67
BililiveRecorder.WPF/Models/LogModel.cs
Normal file
|
@ -0,0 +1,67 @@
|
|||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace BililiveRecorder.WPF.Models
|
||||
{
|
||||
internal class LogModel : ObservableCollection<string>, IDisposable
|
||||
{
|
||||
private const int MAX_LINE = 50;
|
||||
|
||||
private bool disposedValue;
|
||||
|
||||
public static void AddLog(string log) => LogReceived?.Invoke(null, log);
|
||||
public static event EventHandler<string> LogReceived;
|
||||
|
||||
public LogModel() : base(new[] { "" })
|
||||
{
|
||||
LogReceived += LogModel_LogReceived;
|
||||
}
|
||||
|
||||
private void LogModel_LogReceived(object sender, string e)
|
||||
{
|
||||
_ = Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.DataBind, (Action<string>)AddLogToCollection, e);
|
||||
}
|
||||
|
||||
private void AddLogToCollection(string e)
|
||||
{
|
||||
Add(e);
|
||||
while (Count > MAX_LINE)
|
||||
{
|
||||
RemoveItem(0);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
// dispose managed state (managed objects)
|
||||
LogReceived -= LogModel_LogReceived;
|
||||
ClearItems();
|
||||
}
|
||||
|
||||
// free unmanaged resources (unmanaged objects) and override finalizer
|
||||
// set large fields to null
|
||||
disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
// override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
|
||||
// ~LogModel()
|
||||
// {
|
||||
// // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
// Dispose(disposing: false);
|
||||
// }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
63
BililiveRecorder.WPF/Models/RootModel.cs
Normal file
63
BililiveRecorder.WPF/Models/RootModel.cs
Normal file
|
@ -0,0 +1,63 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using BililiveRecorder.Core;
|
||||
|
||||
namespace BililiveRecorder.WPF.Models
|
||||
{
|
||||
internal class RootModel : INotifyPropertyChanged, IDisposable
|
||||
{
|
||||
private bool disposedValue;
|
||||
private IRecorder recorder;
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
public LogModel Logs { get; } = new LogModel();
|
||||
|
||||
public IRecorder Recorder { get => recorder; internal set => SetField(ref recorder, value); }
|
||||
|
||||
public RootModel()
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
|
||||
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value)) { return false; }
|
||||
field = value; OnPropertyChanged(propertyName); return true;
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
// dispose managed state (managed objects)
|
||||
Recorder?.Dispose();
|
||||
Logs.Dispose();
|
||||
}
|
||||
|
||||
// free unmanaged resources (unmanaged objects) and override finalizer
|
||||
// set large fields to null
|
||||
disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
// override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
|
||||
// ~RootModel()
|
||||
// {
|
||||
// // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
// Dispose(disposing: false);
|
||||
// }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@
|
|||
for information on customizing logging rules and outputs.
|
||||
-->
|
||||
<targets>
|
||||
<target name="WPFLogger" xsi:type="MethodCall" className="BililiveRecorder.WPF.MainWindow, BililiveRecorder.WPF" methodName="AddLog">
|
||||
<target name="WPFLogger" xsi:type="MethodCall" className="BililiveRecorder.WPF.Models.LogModel, BililiveRecorder.WPF" methodName="AddLog">
|
||||
<parameter layout="[${date:format=HH\:mm\:ss}] ${uppercase:${level}} ${event-properties:item=roomid} ${message} ${exception:format=Message}" />
|
||||
</target>
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
21
BililiveRecorder.WPF/NewMainWindow.xaml
Normal file
21
BililiveRecorder.WPF/NewMainWindow.xaml
Normal file
|
@ -0,0 +1,21 @@
|
|||
<Window x:Class="BililiveRecorder.WPF.NewMainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
xmlns:local="clr-namespace:BililiveRecorder.WPF"
|
||||
xmlns:pages="clr-namespace:BililiveRecorder.WPF.Pages"
|
||||
mc:Ignorable="d"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
Background="{DynamicResource SystemControlPageBackgroundAltHighBrush}"
|
||||
Foreground="{DynamicResource SystemControlPageTextBaseHighBrush}"
|
||||
ui:ThemeManager.IsThemeAware="True"
|
||||
ui:WindowHelper.UseModernWindowStyle="True"
|
||||
Width="1000" Height="650"
|
||||
MinHeight="400" MinWidth="340"
|
||||
Closing="Window_Closing"
|
||||
StateChanged="Window_StateChanged"
|
||||
Title="录播姬">
|
||||
<pages:RootPage x:Name="RootPage"/>
|
||||
</Window>
|
162
BililiveRecorder.WPF/NewMainWindow.xaml.cs
Normal file
162
BililiveRecorder.WPF/NewMainWindow.xaml.cs
Normal file
|
@ -0,0 +1,162 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
using BililiveRecorder.WPF.Controls;
|
||||
using Hardcodet.Wpf.TaskbarNotification;
|
||||
using ModernWpf.Controls;
|
||||
|
||||
namespace BililiveRecorder.WPF
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for NewMainWindow.xaml
|
||||
/// </summary>
|
||||
public partial class NewMainWindow : Window
|
||||
{
|
||||
public NewMainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
Title = "B站录播姬 " + BuildInfo.Version + " " + BuildInfo.HeadShaShort;
|
||||
|
||||
SuperActivate += NewMainWindow_SuperActivate;
|
||||
CloseWithoutConfirm += NewMainWindow_CloseWithoutConfirm;
|
||||
SingleInstance.NotificationReceived += (sender, e) => SuperActivateAction();
|
||||
}
|
||||
|
||||
internal Action<string, string, BalloonIcon> ShowBalloonTipCallback { get; set; }
|
||||
|
||||
private void NewMainWindow_CloseWithoutConfirm(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CloseWithoutConfirmAction();
|
||||
}
|
||||
|
||||
internal void CloseWithoutConfirmAction()
|
||||
{
|
||||
CloseConfirmed = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void NewMainWindow_SuperActivate(object sender, RoutedEventArgs e)
|
||||
{
|
||||
SuperActivateAction();
|
||||
}
|
||||
|
||||
internal void SuperActivateAction()
|
||||
{
|
||||
Show();
|
||||
WindowState = WindowState.Normal;
|
||||
Topmost = true;
|
||||
Activate();
|
||||
Topmost = false;
|
||||
Focus();
|
||||
}
|
||||
|
||||
private void Window_StateChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (WindowState == WindowState.Minimized)
|
||||
{
|
||||
Hide();
|
||||
ShowBalloonTipCallback?.Invoke("B站录播姬", "录播姬已最小化到托盘,左键单击图标恢复界面", BalloonIcon.Info);
|
||||
// RaiseEvent(new RoutedEventArgs(ShowBalloonTipEvent));
|
||||
// RaiseEvent(new ShowBalloonTipRoutedEventArgs(ShowBalloonTipEvent)
|
||||
// {
|
||||
// Title = "B站录播姬",
|
||||
// Message = "录播姬已最小化到托盘,左键单击图标恢复界面。",
|
||||
// Symbol = BalloonIcon.Info
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
#region Routed Events
|
||||
|
||||
public static readonly RoutedEvent BeforeWindowCloseEvent
|
||||
= EventManager.RegisterRoutedEvent("BeforeWindowClose", RoutingStrategy.Tunnel, typeof(RoutedEventHandler), typeof(NewMainWindow));
|
||||
|
||||
public event RoutedEventHandler BeforeWindowClose
|
||||
{
|
||||
add { AddHandler(BeforeWindowCloseEvent, value); }
|
||||
remove { RemoveHandler(BeforeWindowCloseEvent, value); }
|
||||
}
|
||||
|
||||
public static readonly RoutedEvent CloseWithoutConfirmEvent
|
||||
= EventManager.RegisterRoutedEvent("CloseWithoutConfirm", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(NewMainWindow));
|
||||
|
||||
public event RoutedEventHandler CloseWithoutConfirm
|
||||
{
|
||||
add { AddHandler(CloseWithoutConfirmEvent, value); }
|
||||
remove { RemoveHandler(CloseWithoutConfirmEvent, value); }
|
||||
}
|
||||
|
||||
public static readonly RoutedEvent SuperActivateEvent
|
||||
= EventManager.RegisterRoutedEvent("SuperActivate", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(NewMainWindow));
|
||||
|
||||
public event RoutedEventHandler SuperActivate
|
||||
{
|
||||
add { AddHandler(SuperActivateEvent, value); }
|
||||
remove { RemoveHandler(SuperActivateEvent, value); }
|
||||
}
|
||||
|
||||
public delegate void ShowBalloonTipRoutedEventHandler(object sender, ShowBalloonTipRoutedEventArgs e);
|
||||
|
||||
public static readonly RoutedEvent ShowBalloonTipEvent
|
||||
= EventManager.RegisterRoutedEvent("ShowBalloonTip", RoutingStrategy.Tunnel, typeof(RoutedEventHandler), typeof(NewMainWindow));
|
||||
|
||||
public event RoutedEventHandler ShowBalloonTip
|
||||
{
|
||||
add { AddHandler(ShowBalloonTipEvent, value); }
|
||||
remove { RemoveHandler(ShowBalloonTipEvent, value); }
|
||||
}
|
||||
|
||||
public class ShowBalloonTipRoutedEventArgs : RoutedEventArgs
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string Message { get; set; }
|
||||
public BalloonIcon Symbol { get; set; }
|
||||
|
||||
public ShowBalloonTipRoutedEventArgs() { }
|
||||
public ShowBalloonTipRoutedEventArgs(RoutedEvent routedEvent) : base(routedEvent) { }
|
||||
public ShowBalloonTipRoutedEventArgs(RoutedEvent routedEvent, object source) : base(routedEvent, source) { }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Confirm Close Window
|
||||
|
||||
private bool CloseConfirmed = false;
|
||||
|
||||
private readonly SemaphoreSlim CloseWindowSemaphoreSlim = new SemaphoreSlim(1, 1);
|
||||
|
||||
public bool PromptCloseConfirm { get; set; } = true;
|
||||
|
||||
private async void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
if (PromptCloseConfirm && !CloseConfirmed)
|
||||
{
|
||||
e.Cancel = true;
|
||||
if (CloseWindowSemaphoreSlim.Wait(0))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (await new CloseWindowConfirmDialog().ShowAsync() == ContentDialogResult.Primary)
|
||||
{
|
||||
CloseConfirmed = true;
|
||||
CloseWindowSemaphoreSlim.Release();
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception) { }
|
||||
CloseWindowSemaphoreSlim.Release();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
RaiseEvent(new RoutedEventArgs(BeforeWindowCloseEvent));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
throwExceptions="false"
|
||||
internalLogLevel="Off" internalLogFile="c:\temp\nlog-internal.log">
|
||||
<targets>
|
||||
<target name="WPFLogger" xsi:type="MethodCall" className="BililiveRecorder.WPF.MainWindow, BililiveRecorder.WPF" methodName="AddLog">
|
||||
<target name="WPFLogger" xsi:type="MethodCall" className="BililiveRecorder.WPF.Models.LogModel, BililiveRecorder.WPF" methodName="AddLog">
|
||||
<parameter layout="[${date:format=HH\:mm\:ss}] ${uppercase:${level}} ${event-properties:item=roomid} ${message} ${exception:format=Message}" />
|
||||
</target>
|
||||
<target name="file" xsi:type="File"
|
||||
|
|
86
BililiveRecorder.WPF/Pages/AdvancedSettingsPage.xaml
Normal file
86
BililiveRecorder.WPF/Pages/AdvancedSettingsPage.xaml
Normal file
|
@ -0,0 +1,86 @@
|
|||
<ui:Page
|
||||
x:Class="BililiveRecorder.WPF.Pages.AdvancedSettingsPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
xmlns:local="clr-namespace:BililiveRecorder.WPF.Pages"
|
||||
xmlns:conv="clr-namespace:BililiveRecorder.WPF.Converters"
|
||||
xmlns:config="clr-namespace:BililiveRecorder.Core.Config;assembly=BililiveRecorder.Core"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="1500" d:DesignWidth="500"
|
||||
DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=local:RootPage},Path=DataContext.Recorder.Config}"
|
||||
Title="SettingsPage">
|
||||
<ui:Page.Resources>
|
||||
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
|
||||
<conv:ValueConverterGroup x:Key="InvertBooleanToVisibilityConverter">
|
||||
<conv:BooleanInverterConverter/>
|
||||
<BooleanToVisibilityConverter/>
|
||||
</conv:ValueConverterGroup>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
</Style>
|
||||
<Style TargetType="ui:NumberBox">
|
||||
<Setter Property="Width" Value="250"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Left"/>
|
||||
<Setter Property="ValidationMode" Value="InvalidInputOverwritten"/>
|
||||
<Setter Property="SpinButtonPlacementMode" Value="Inline"/>
|
||||
</Style>
|
||||
</ui:Page.Resources>
|
||||
<ScrollViewer d:DataContext="{d:DesignInstance Type=config:ConfigV1}">
|
||||
<ui:SimpleStackPanel Orientation="Vertical" Spacing="5" Margin="20">
|
||||
<TextBlock Text="高级设置" Style="{StaticResource TitleTextBlockStyle}" Margin="0,10"/>
|
||||
<GroupBox Header="Cookie">
|
||||
<StackPanel>
|
||||
<TextBlock Text="请求API时使用此 Cookie"/>
|
||||
<TextBox Text="{Binding Cookie,Delay=500}" Width="250" HorizontalAlignment="Left"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="Timing">
|
||||
<ui:SimpleStackPanel Spacing="10">
|
||||
<ui:NumberBox Minimum="1000" Header="录制重试间隔" Description="单位: 毫秒" SmallChange="100" Text="{Binding TimingStreamRetry,Delay=500}">
|
||||
<ui:NumberBox.ToolTip>
|
||||
<TextBlock>录制断开后等待多长时间再尝试开始录制</TextBlock>
|
||||
</ui:NumberBox.ToolTip>
|
||||
</ui:NumberBox>
|
||||
<ui:NumberBox Minimum="1000" Header="录制连接超时" Description="单位: 毫秒" SmallChange="100" Text="{Binding TimingStreamConnect,Delay=500}">
|
||||
<ui:NumberBox.ToolTip>
|
||||
<TextBlock>
|
||||
发出连接直播服务器的请求后等待多长时间<LineBreak/>
|
||||
防止直播服务器长时间不返回数据导致卡住
|
||||
</TextBlock>
|
||||
</ui:NumberBox.ToolTip>
|
||||
</ui:NumberBox>
|
||||
<ui:NumberBox Minimum="1000" Header="弹幕重连间隔" Description="单位: 毫秒" SmallChange="100" Text="{Binding TimingDanmakuRetry,Delay=500}">
|
||||
<ui:NumberBox.ToolTip>
|
||||
<TextBlock>
|
||||
弹幕服务器被断开后等待多长时间再尝试连接<LineBreak/>
|
||||
监控开播的主要途径是通过弹幕服务器发送的信息
|
||||
</TextBlock>
|
||||
</ui:NumberBox.ToolTip>
|
||||
</ui:NumberBox>
|
||||
<ui:NumberBox Minimum="1000" Header="接收数据超时" Description="单位: 毫秒" SmallChange="100" Text="{Binding TimingWatchdogTimeout,Delay=500}">
|
||||
<ui:NumberBox.ToolTip>
|
||||
<TextBlock>
|
||||
在一定时间没有收到直播服务器发送的数据后断开重连<LineBreak/>
|
||||
用于防止因为玄学网络问题导致卡住
|
||||
</TextBlock>
|
||||
</ui:NumberBox.ToolTip>
|
||||
</ui:NumberBox>
|
||||
<ui:NumberBox Minimum="60" Header="开播检查间隔" Description="单位: 秒" SmallChange="10" Text="{Binding TimingCheckInterval,Delay=500}">
|
||||
<ui:NumberBox.ToolTip>
|
||||
<TextBlock>
|
||||
此项影响的时间间隔是定时请求HTTP接口的间隔,
|
||||
主要目的是防止没有从弹幕服务器收到开播消息,
|
||||
所以此项不需要设置太短。<LineBreak/>
|
||||
时间间隔设置太短会被B站服务器屏蔽,导致无法录制。
|
||||
</TextBlock>
|
||||
</ui:NumberBox.ToolTip>
|
||||
</ui:NumberBox>
|
||||
</ui:SimpleStackPanel>
|
||||
</GroupBox>
|
||||
</ui:SimpleStackPanel>
|
||||
</ScrollViewer>
|
||||
</ui:Page>
|
28
BililiveRecorder.WPF/Pages/AdvancedSettingsPage.xaml.cs
Normal file
28
BililiveRecorder.WPF/Pages/AdvancedSettingsPage.xaml.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace BililiveRecorder.WPF.Pages
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for AdvancedSettingsPage.xaml
|
||||
/// </summary>
|
||||
public partial class AdvancedSettingsPage
|
||||
{
|
||||
public AdvancedSettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
85
BililiveRecorder.WPF/Pages/LogPage.xaml
Normal file
85
BililiveRecorder.WPF/Pages/LogPage.xaml
Normal file
|
@ -0,0 +1,85 @@
|
|||
<ui:Page
|
||||
x:Class="BililiveRecorder.WPF.Pages.LogPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
xmlns:wpf="clr-namespace:BililiveRecorder.WPF"
|
||||
xmlns:local="clr-namespace:BililiveRecorder.WPF.Pages"
|
||||
xmlns:models="clr-namespace:BililiveRecorder.WPF.Models"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance Type=models:LogModel}"
|
||||
DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=local:RootPage},Path=DataContext.Logs,Mode=OneWay}"
|
||||
Title="LogPage">
|
||||
<Grid Margin="5">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<ui:ThemeShadowChrome Margin="5">
|
||||
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||
BorderBrush="{DynamicResource SystemControlBackgroundAccentBrush}"
|
||||
BorderThickness="1" CornerRadius="5">
|
||||
<StackPanel Margin="10" Orientation="Vertical">
|
||||
<StackPanel.Resources>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="15"/>
|
||||
<Setter Property="TextAlignment" Value="Center"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
</Style>
|
||||
<Style TargetType="ui:HyperlinkButton">
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
</Style>
|
||||
<Style TargetType="StackPanel">
|
||||
<Setter Property="HorizontalAlignment" Value="Center"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="Orientation" Value="Horizontal"/>
|
||||
</Style>
|
||||
</StackPanel.Resources>
|
||||
<StackPanel>
|
||||
<TextBlock Text="B站录播姬 "/>
|
||||
<TextBlock x:Name="VersionTextBlock"/>
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Text="官网: "/>
|
||||
<ui:HyperlinkButton Content="https://rec.danmuji.org" NavigateUri="https://rec.danmuji.org"/>
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Text="联系方式/问题反馈: "/>
|
||||
<ui:HyperlinkButton Content="rec@danmuji.org" NavigateUri="mailto:rec@danmuji.org"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="QQ群: 689636812"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ui:ThemeShadowChrome>
|
||||
<ui:ThemeShadowChrome Grid.Row="1" IsShadowEnabled="True" Depth="10" Margin="5">
|
||||
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||
BorderBrush="{DynamicResource SystemControlBackgroundAccentBrush}"
|
||||
BorderThickness="1" CornerRadius="5">
|
||||
<ItemsControl x:Name="Log" ItemsSource="{Binding Mode=OneWay}" Margin="5" ToolTip="右键点击可以复制单行日志">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border BorderThickness="0,0,0,1" BorderBrush="#FFCCCCCC">
|
||||
<TextBlock Text="{Binding Mode=OneWay}" TextWrapping="Wrap" MouseRightButtonUp="TextBlock_MouseRightButtonUp"/>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
<ItemsControl.Template>
|
||||
<ControlTemplate>
|
||||
<ScrollViewer Loaded="ScrollViewer_Loaded" CanContentScroll="True">
|
||||
<ItemsPresenter/>
|
||||
</ScrollViewer>
|
||||
</ControlTemplate>
|
||||
</ItemsControl.Template>
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<VirtualizingStackPanel IsItemsHost="True"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
</Border>
|
||||
</ui:ThemeShadowChrome>
|
||||
</Grid>
|
||||
</ui:Page>
|
38
BililiveRecorder.WPF/Pages/LogPage.xaml.cs
Normal file
38
BililiveRecorder.WPF/Pages/LogPage.xaml.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace BililiveRecorder.WPF.Pages
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for LogPage.xaml
|
||||
/// </summary>
|
||||
public partial class LogPage
|
||||
{
|
||||
public LogPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
VersionTextBlock.Text = BuildInfo.Version + " " + BuildInfo.HeadShaShort;
|
||||
}
|
||||
|
||||
private void TextBlock_MouseRightButtonUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (sender is TextBlock textBlock)
|
||||
{
|
||||
Clipboard.SetText(textBlock.Text);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private void ScrollViewer_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
(sender as ScrollViewer)?.ScrollToEnd();
|
||||
}
|
||||
}
|
||||
}
|
50
BililiveRecorder.WPF/Pages/RoomListPage.xaml
Normal file
50
BililiveRecorder.WPF/Pages/RoomListPage.xaml
Normal file
|
@ -0,0 +1,50 @@
|
|||
<ui:Page
|
||||
x:Class="BililiveRecorder.WPF.Pages.RoomListPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
xmlns:local="clr-namespace:BililiveRecorder.WPF.Pages"
|
||||
xmlns:mock="clr-namespace:BililiveRecorder.WPF.MockData"
|
||||
xmlns:controls="clr-namespace:BililiveRecorder.WPF.Controls"
|
||||
xmlns:converters="clr-namespace:BililiveRecorder.WPF.Converters"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="1000" d:DesignWidth="960"
|
||||
d:DataContext="{d:DesignInstance mock:MockRecorder,IsDesignTimeCreatable=True}"
|
||||
DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=local:RootPage},Path=DataContext.Recorder,Mode=OneWay}"
|
||||
Title="房间列表">
|
||||
<ui:Page.Resources>
|
||||
<DataTemplate x:Key="NormalRoomCardTemplate">
|
||||
<ui:ThemeShadowChrome IsShadowEnabled="True" Depth="10">
|
||||
<controls:RoomCard DeleteRequested="RoomCard_DeleteRequested" />
|
||||
</ui:ThemeShadowChrome>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="AddRoomCardTemplate">
|
||||
<ui:ThemeShadowChrome IsShadowEnabled="True" Depth="10">
|
||||
<controls:AddRoomCard AddRoomRequested="AddRoomCard_AddRoomRequested"/>
|
||||
</ui:ThemeShadowChrome>
|
||||
</DataTemplate>
|
||||
<converters:NullValueTemplateSelector
|
||||
x:Key="SelectorTemplate"
|
||||
Normal="{StaticResource NormalRoomCardTemplate}"
|
||||
Null="{StaticResource AddRoomCardTemplate}"/>
|
||||
<ui:UniformGridLayout
|
||||
x:Key="UniformGridLayout"
|
||||
MinItemWidth="230"
|
||||
MinItemHeight="120"
|
||||
MinRowSpacing="7"
|
||||
MinColumnSpacing="5" />
|
||||
<converters:RoomListInterceptConverter x:Key="RoomListInterceptConverter"/>
|
||||
</ui:Page.Resources>
|
||||
<ui:ItemsRepeaterScrollHost>
|
||||
<ScrollViewer>
|
||||
<ui:ItemsRepeater
|
||||
HorizontalAlignment="Stretch" Margin="8"
|
||||
Layout="{StaticResource UniformGridLayout}"
|
||||
ItemsSource="{Binding Converter={StaticResource RoomListInterceptConverter},Mode=OneWay}"
|
||||
ItemTemplate="{StaticResource SelectorTemplate}" />
|
||||
</ScrollViewer>
|
||||
</ui:ItemsRepeaterScrollHost>
|
||||
</ui:Page>
|
80
BililiveRecorder.WPF/Pages/RoomListPage.xaml.cs
Normal file
80
BililiveRecorder.WPF/Pages/RoomListPage.xaml.cs
Normal file
|
@ -0,0 +1,80 @@
|
|||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Linq;
|
||||
using BililiveRecorder.Core;
|
||||
using BililiveRecorder.WPF.Controls;
|
||||
|
||||
namespace BililiveRecorder.WPF.Pages
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for RoomList.xaml
|
||||
/// </summary>
|
||||
public partial class RoomListPage
|
||||
{
|
||||
private static readonly Regex RoomIdRegex
|
||||
= new Regex(@"^(?:https?:\/\/)?live\.bilibili\.com\/(?:blanc\/|h5\/)?(\d*)(?:\?.*)?$",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);
|
||||
|
||||
public RoomListPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private async void RoomCard_DeleteRequested(object sender, EventArgs e)
|
||||
{
|
||||
if (DataContext is IRecorder rec && sender is IRecordedRoom room)
|
||||
{
|
||||
var dialog = new DeleteRoomConfirmDialog
|
||||
{
|
||||
DataContext = room
|
||||
};
|
||||
|
||||
var result = await dialog.ShowAsync();
|
||||
|
||||
if (result == ModernWpf.Controls.ContentDialogResult.Primary)
|
||||
{
|
||||
rec.RemoveRoom(room);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void AddRoomCard_AddRoomRequested(object sender, string e)
|
||||
{
|
||||
var input = e.Trim();
|
||||
if (string.IsNullOrWhiteSpace(input) || !(DataContext is IRecorder rec)) return;
|
||||
|
||||
if (!int.TryParse(input, out var roomid))
|
||||
{
|
||||
var m = RoomIdRegex.Match(input);
|
||||
if (m.Success && m.Groups.Count > 1 && int.TryParse(m.Groups[1].Value, out var result2))
|
||||
{
|
||||
roomid = result2;
|
||||
}
|
||||
else
|
||||
{
|
||||
await new AddRoomFailedDialog { DataContext = "请输入B站直播房间号或直播间链接" }.ShowAsync();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (roomid < 0)
|
||||
{
|
||||
await new AddRoomFailedDialog { DataContext = "房间号不能是负数" }.ShowAsync();
|
||||
return;
|
||||
}
|
||||
else if (roomid == 0)
|
||||
{
|
||||
await new AddRoomFailedDialog { DataContext = "房间号不能是 0" }.ShowAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
if (rec.Any(x => x.RoomId == roomid || x.ShortRoomId == roomid))
|
||||
{
|
||||
await new AddRoomFailedDialog { DataContext = "这个直播间已经被添加过了" }.ShowAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
rec.AddRoom(roomid);
|
||||
}
|
||||
}
|
||||
}
|
82
BililiveRecorder.WPF/Pages/RootPage.xaml
Normal file
82
BililiveRecorder.WPF/Pages/RootPage.xaml
Normal file
|
@ -0,0 +1,82 @@
|
|||
<UserControl
|
||||
x:Class="BililiveRecorder.WPF.Pages.RootPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
xmlns:local="clr-namespace:BililiveRecorder.WPF.Pages"
|
||||
xmlns:wpf="clr-namespace:BililiveRecorder.WPF"
|
||||
xmlns:controls="clr-namespace:BililiveRecorder.WPF.Controls"
|
||||
xmlns:models="clr-namespace:BililiveRecorder.WPF.Models"
|
||||
mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="600"
|
||||
d:DataContext="{d:DesignInstance Type=models:RootModel,IsDesignTimeCreatable=True}"
|
||||
Background="{DynamicResource SystemControlPageBackgroundAltHighBrush}"
|
||||
wpf:NewMainWindow.BeforeWindowClose="UserControl_BeforeWindowClose"
|
||||
>
|
||||
<UserControl.Resources>
|
||||
<Style x:Key="CascadeDataContextFrame" TargetType="{x:Type ui:Frame}">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type ui:Frame}">
|
||||
<Border
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Background="{TemplateBinding Background}"
|
||||
ClipToBounds="True"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<Grid>
|
||||
<ContentPresenter
|
||||
x:Name="FirstContentPresenter"
|
||||
Content="{x:Null}"
|
||||
DataContext="{TemplateBinding DataContext}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
|
||||
<ContentPresenter
|
||||
x:Name="SecondContentPresenter"
|
||||
Content="{x:Null}"
|
||||
DataContext="{TemplateBinding DataContext}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
<Grid>
|
||||
<controls:TaskbarIconControl DataContext="{Binding}"/>
|
||||
<ui:NavigationView IsBackEnabled="False" IsBackButtonVisible="Collapsed"
|
||||
IsPaneOpen="False" OpenPaneLength="150"
|
||||
IsSettingsVisible="False"
|
||||
SelectionChanged="NavigationView_SelectionChanged"
|
||||
>
|
||||
<ui:NavigationView.MenuItems>
|
||||
<ui:NavigationViewItem Content="房间列表" Tag="RoomListPage" x:Name="RoomListPageNavigationViewItem">
|
||||
<ui:NavigationViewItem.Icon>
|
||||
<ui:PathIcon Data="M12 5.69L17 10.19V18H15V12H9V18H7V10.19L12 5.69M12 3L2 12H5V20H11V14H13V20H19V12H22L12 3Z"/>
|
||||
</ui:NavigationViewItem.Icon>
|
||||
</ui:NavigationViewItem>
|
||||
</ui:NavigationView.MenuItems>
|
||||
<ui:NavigationView.FooterMenuItems>
|
||||
<ui:NavigationViewItem Content="高级设置" Tag="AdvancedSettingsPage" x:Name="AdvancedSettingsPageItem">
|
||||
<ui:NavigationViewItem.Icon>
|
||||
<ui:PathIcon Data="M15.9,18.45C17.25,18.45 18.35,17.35 18.35,16C18.35,14.65 17.25,13.55 15.9,13.55C14.54,13.55 13.45,14.65 13.45,16C13.45,17.35 14.54,18.45 15.9,18.45M21.1,16.68L22.58,17.84C22.71,17.95 22.75,18.13 22.66,18.29L21.26,20.71C21.17,20.86 21,20.92 20.83,20.86L19.09,20.16C18.73,20.44 18.33,20.67 17.91,20.85L17.64,22.7C17.62,22.87 17.47,23 17.3,23H14.5C14.32,23 14.18,22.87 14.15,22.7L13.89,20.85C13.46,20.67 13.07,20.44 12.71,20.16L10.96,20.86C10.81,20.92 10.62,20.86 10.54,20.71L9.14,18.29C9.05,18.13 9.09,17.95 9.22,17.84L10.7,16.68L10.65,16L10.7,15.31L9.22,14.16C9.09,14.05 9.05,13.86 9.14,13.71L10.54,11.29C10.62,11.13 10.81,11.07 10.96,11.13L12.71,11.84C13.07,11.56 13.46,11.32 13.89,11.15L14.15,9.29C14.18,9.13 14.32,9 14.5,9H17.3C17.47,9 17.62,9.13 17.64,9.29L17.91,11.15C18.33,11.32 18.73,11.56 19.09,11.84L20.83,11.13C21,11.07 21.17,11.13 21.26,11.29L22.66,13.71C22.75,13.86 22.71,14.05 22.58,14.16L21.1,15.31L21.15,16L21.1,16.68M6.69,8.07C7.56,8.07 8.26,7.37 8.26,6.5C8.26,5.63 7.56,4.92 6.69,4.92A1.58,1.58 0 0,0 5.11,6.5C5.11,7.37 5.82,8.07 6.69,8.07M10.03,6.94L11,7.68C11.07,7.75 11.09,7.87 11.03,7.97L10.13,9.53C10.08,9.63 9.96,9.67 9.86,9.63L8.74,9.18L8,9.62L7.81,10.81C7.79,10.92 7.7,11 7.59,11H5.79C5.67,11 5.58,10.92 5.56,10.81L5.4,9.62L4.64,9.18L3.5,9.63C3.41,9.67 3.3,9.63 3.24,9.53L2.34,7.97C2.28,7.87 2.31,7.75 2.39,7.68L3.34,6.94L3.31,6.5L3.34,6.06L2.39,5.32C2.31,5.25 2.28,5.13 2.34,5.03L3.24,3.47C3.3,3.37 3.41,3.33 3.5,3.37L4.63,3.82L5.4,3.38L5.56,2.19C5.58,2.08 5.67,2 5.79,2H7.59C7.7,2 7.79,2.08 7.81,2.19L8,3.38L8.74,3.82L9.86,3.37C9.96,3.33 10.08,3.37 10.13,3.47L11.03,5.03C11.09,5.13 11.07,5.25 11,5.32L10.03,6.06L10.06,6.5L10.03,6.94Z"/>
|
||||
</ui:NavigationViewItem.Icon>
|
||||
</ui:NavigationViewItem>
|
||||
<ui:NavigationViewItem Content="日志" Tag="LogPage">
|
||||
<ui:NavigationViewItem.Icon>
|
||||
<ui:PathIcon Data="M9 3C5.69 3 3.14 5.69 3 9V21H12V13.46C13.1 14.45 14.5 15 16 15C19.31 15 22 12.31 22 9C22 5.69 19.31 3 16 3H9M9 5H11.54C10.55 6.1 10 7.5 10 9V12H9V13H10V19H5V13H6V12H5V9C5 6.79 6.79 5 9 5M16 5C18.21 5 20 6.79 20 9C20 11.21 18.21 13 16 13C13.79 13 12 11.21 12 9C12 6.79 13.79 5 16 5M16 7.25C15.03 7.25 14.25 8.03 14.25 9C14.25 9.97 15.03 10.75 16 10.75C16.97 10.75 17.75 9.97 17.75 9C17.75 8.03 16.97 7.25 16 7.25M7 12V13H8V12H7Z"/>
|
||||
</ui:NavigationViewItem.Icon>
|
||||
</ui:NavigationViewItem>
|
||||
<ui:NavigationViewItem Content="设置" Tag="SettingsPage" MouseRightButtonUp="NavigationViewItem_MouseRightButtonUp">
|
||||
<ui:NavigationViewItem.Icon>
|
||||
<ui:PathIcon Data="M12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10M10,22C9.75,22 9.54,21.82 9.5,21.58L9.13,18.93C8.5,18.68 7.96,18.34 7.44,17.94L4.95,18.95C4.73,19.03 4.46,18.95 4.34,18.73L2.34,15.27C2.21,15.05 2.27,14.78 2.46,14.63L4.57,12.97L4.5,12L4.57,11L2.46,9.37C2.27,9.22 2.21,8.95 2.34,8.73L4.34,5.27C4.46,5.05 4.73,4.96 4.95,5.05L7.44,6.05C7.96,5.66 8.5,5.32 9.13,5.07L9.5,2.42C9.54,2.18 9.75,2 10,2H14C14.25,2 14.46,2.18 14.5,2.42L14.87,5.07C15.5,5.32 16.04,5.66 16.56,6.05L19.05,5.05C19.27,4.96 19.54,5.05 19.66,5.27L21.66,8.73C21.79,8.95 21.73,9.22 21.54,9.37L19.43,11L19.5,12L19.43,13L21.54,14.63C21.73,14.78 21.79,15.05 21.66,15.27L19.66,18.73C19.54,18.95 19.27,19.04 19.05,18.95L16.56,17.95C16.04,18.34 15.5,18.68 14.87,18.93L14.5,21.58C14.46,21.82 14.25,22 14,22H10M11.25,4L10.88,6.61C9.68,6.86 8.62,7.5 7.85,8.39L5.44,7.35L4.69,8.65L6.8,10.2C6.4,11.37 6.4,12.64 6.8,13.8L4.68,15.36L5.43,16.66L7.86,15.62C8.63,16.5 9.68,17.14 10.87,17.38L11.24,20H12.76L13.13,17.39C14.32,17.14 15.37,16.5 16.14,15.62L18.57,16.66L19.32,15.36L17.2,13.81C17.6,12.64 17.6,11.37 17.2,10.2L19.31,8.65L18.56,7.35L16.15,8.39C15.38,7.5 14.32,6.86 13.12,6.62L12.75,4H11.25Z"/>
|
||||
</ui:NavigationViewItem.Icon>
|
||||
</ui:NavigationViewItem>
|
||||
</ui:NavigationView.FooterMenuItems>
|
||||
<ui:Frame x:Name="MainFrame" Style="{DynamicResource CascadeDataContextFrame}" />
|
||||
</ui:NavigationView>
|
||||
</Grid>
|
||||
</UserControl>
|
187
BililiveRecorder.WPF/Pages/RootPage.xaml.cs
Normal file
187
BililiveRecorder.WPF/Pages/RootPage.xaml.cs
Normal file
|
@ -0,0 +1,187 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using Autofac;
|
||||
using BililiveRecorder.Core;
|
||||
using BililiveRecorder.FlvProcessor;
|
||||
using BililiveRecorder.WPF.Controls;
|
||||
using BililiveRecorder.WPF.Models;
|
||||
using CommandLine;
|
||||
using ModernWpf.Controls;
|
||||
using Path = System.IO.Path;
|
||||
|
||||
namespace BililiveRecorder.WPF.Pages
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for RootPage.xaml
|
||||
/// </summary>
|
||||
public partial class RootPage : UserControl
|
||||
{
|
||||
private readonly Dictionary<string, Type> PageMap = new Dictionary<string, Type>();
|
||||
private readonly string lastdir_path = Path.Combine(Path.GetDirectoryName(typeof(RootPage).Assembly.Location), "lastdir.txt");
|
||||
|
||||
private IContainer Container { get; set; }
|
||||
private ILifetimeScope RootScope { get; set; }
|
||||
|
||||
private int SettingsClickCount = 0;
|
||||
|
||||
internal RootModel Model { get; private set; }
|
||||
|
||||
public RootPage()
|
||||
{
|
||||
void AddType(Type t) => PageMap.Add(t.Name, t);
|
||||
AddType(typeof(RoomListPage));
|
||||
AddType(typeof(LogPage));
|
||||
AddType(typeof(SettingsPage));
|
||||
AddType(typeof(AdvancedSettingsPage));
|
||||
|
||||
Model = new RootModel();
|
||||
DataContext = Model;
|
||||
|
||||
var builder = new ContainerBuilder();
|
||||
builder.RegisterModule<FlvProcessorModule>();
|
||||
builder.RegisterModule<CoreModule>();
|
||||
Container = builder.Build();
|
||||
RootScope = Container.BeginLifetimeScope("recorder_root");
|
||||
|
||||
InitializeComponent();
|
||||
AdvancedSettingsPageItem.Visibility = Visibility.Hidden;
|
||||
|
||||
Loaded += RootPage_Loaded;
|
||||
}
|
||||
|
||||
private async void RootPage_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
bool first_time = true;
|
||||
|
||||
var recorder = RootScope.Resolve<IRecorder>();
|
||||
var error = string.Empty;
|
||||
string path;
|
||||
while (true)
|
||||
{
|
||||
CommandLineOption commandLineOption = null;
|
||||
if (first_time)
|
||||
{
|
||||
first_time = false;
|
||||
Parser.Default
|
||||
.ParseArguments<CommandLineOption>(Environment.GetCommandLineArgs())
|
||||
.WithParsed(x => commandLineOption = x);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(commandLineOption.WorkDirectory))
|
||||
{
|
||||
path = Path.GetFullPath(commandLineOption.WorkDirectory);
|
||||
goto check_path;
|
||||
}
|
||||
}
|
||||
|
||||
string lastdir = string.Empty;
|
||||
try
|
||||
{
|
||||
if (File.Exists(lastdir_path))
|
||||
{
|
||||
lastdir = File.ReadAllText(lastdir_path).Replace("\r", "").Replace("\n", "").Trim();
|
||||
}
|
||||
}
|
||||
catch (Exception) { }
|
||||
var w = new WorkDirectorySelectorDialog
|
||||
{
|
||||
Error = error,
|
||||
Path = lastdir
|
||||
};
|
||||
var result = await w.ShowAsync();
|
||||
if (result != ContentDialogResult.Primary)
|
||||
{
|
||||
RaiseEvent(new RoutedEventArgs(NewMainWindow.CloseWithoutConfirmEvent));
|
||||
return;
|
||||
}
|
||||
|
||||
path = Path.GetFullPath(w.Path);
|
||||
check_path:
|
||||
var config = Path.Combine(path, "config.json");
|
||||
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
error = "目录不存在";
|
||||
continue;
|
||||
}
|
||||
else if (!Directory.EnumerateFiles(path).Any())
|
||||
{
|
||||
// 可用的空文件夹
|
||||
}
|
||||
else if (!File.Exists(config))
|
||||
{
|
||||
error = "目录已有其他文件";
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(commandLineOption?.WorkDirectory))
|
||||
{
|
||||
File.WriteAllText(lastdir_path, path);
|
||||
}
|
||||
}
|
||||
catch (Exception) { }
|
||||
|
||||
// 检查已经在同目录运行的其他进程
|
||||
if (SingleInstance.CheckMutex(path))
|
||||
{
|
||||
if (recorder.Initialize(path))
|
||||
{
|
||||
Model.Recorder = recorder;
|
||||
RoomListPageNavigationViewItem.IsSelected = true;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
error = "配置文件加载失败";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
RaiseEvent(new RoutedEventArgs(NewMainWindow.CloseWithoutConfirmEvent));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void NavigationView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
|
||||
{
|
||||
SettingsClickCount = 0;
|
||||
if (args.IsSettingsSelected)
|
||||
{
|
||||
MainFrame.Navigate(typeof(SettingsPage));
|
||||
}
|
||||
else
|
||||
{
|
||||
var selectedItem = (NavigationViewItem)args.SelectedItem;
|
||||
var selectedItemTag = (string)selectedItem.Tag;
|
||||
if (PageMap.ContainsKey(selectedItemTag))
|
||||
{
|
||||
var pageType = PageMap[selectedItemTag];
|
||||
MainFrame.Navigate(pageType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void NavigationViewItem_MouseRightButtonUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (++SettingsClickCount > 3)
|
||||
{
|
||||
SettingsClickCount = 0;
|
||||
AdvancedSettingsPageItem.Visibility = AdvancedSettingsPageItem.Visibility != Visibility.Visible ? Visibility.Visible : Visibility.Hidden;
|
||||
}
|
||||
}
|
||||
|
||||
private void UserControl_BeforeWindowClose(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Model.Dispose();
|
||||
SingleInstance.Cleanup();
|
||||
}
|
||||
}
|
||||
}
|
125
BililiveRecorder.WPF/Pages/SettingsPage.xaml
Normal file
125
BililiveRecorder.WPF/Pages/SettingsPage.xaml
Normal file
|
@ -0,0 +1,125 @@
|
|||
<ui:Page
|
||||
x:Class="BililiveRecorder.WPF.Pages.SettingsPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
xmlns:local="clr-namespace:BililiveRecorder.WPF.Pages"
|
||||
xmlns:conv="clr-namespace:BililiveRecorder.WPF.Converters"
|
||||
xmlns:config="clr-namespace:BililiveRecorder.Core.Config;assembly=BililiveRecorder.Core"
|
||||
xmlns:flv="clr-namespace:BililiveRecorder.FlvProcessor;assembly=BililiveRecorder.FlvProcessor"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="1500" d:DesignWidth="500"
|
||||
DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=local:RootPage},Path=DataContext.Recorder.Config}"
|
||||
Title="SettingsPage">
|
||||
<ui:Page.Resources>
|
||||
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
|
||||
<conv:EnumToBooleanConverter x:Key="EnumToBooleanConverter"/>
|
||||
<conv:ValueConverterGroup x:Key="InvertBooleanToVisibilityConverter">
|
||||
<conv:BooleanInverterConverter/>
|
||||
<BooleanToVisibilityConverter/>
|
||||
</conv:ValueConverterGroup>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
</Style>
|
||||
</ui:Page.Resources>
|
||||
<ScrollViewer d:DataContext="{d:DesignInstance Type=config:ConfigV1}">
|
||||
<ui:SimpleStackPanel Orientation="Vertical" Spacing="5" Margin="20">
|
||||
<TextBlock Text="设置" Style="{StaticResource TitleTextBlockStyle}" Margin="0,10"/>
|
||||
<GroupBox Header="弹幕录制">
|
||||
<StackPanel>
|
||||
<ui:ToggleSwitch IsOn="{Binding RecordDanmaku}" Name="RecordDanmakuCheckbox" OnContent="录播时保存弹幕" OffContent="录播时不保存弹幕"/>
|
||||
<StackPanel Margin="0,7,0,0" Visibility="{Binding ElementName=RecordDanmakuCheckbox,Path=IsOn,Converter={StaticResource BooleanToVisibilityConverter}}">
|
||||
<ui:ToggleSwitch IsEnabled="{Binding RecordDanmaku}" IsOn="{Binding RecordDanmakuRaw}"
|
||||
OnContent="同时保存 弹幕原始数据" OffContent="不保存 弹幕原始数据"/>
|
||||
<ui:ToggleSwitch IsEnabled="{Binding RecordDanmaku}" IsOn="{Binding RecordDanmakuSuperChat}"
|
||||
OnContent="同时保存 SuperChat" OffContent="不保存 SuperChat"/>
|
||||
<ui:ToggleSwitch IsEnabled="{Binding RecordDanmaku}" IsOn="{Binding RecordDanmakuGift}"
|
||||
OnContent="同时保存 送礼信息" OffContent="不保存 送礼信息"/>
|
||||
<ui:ToggleSwitch IsEnabled="{Binding RecordDanmaku}" IsOn="{Binding RecordDanmakuGuard}"
|
||||
OnContent="同时保存 舰长购买" OffContent="不保存 舰长购买"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="自动分段">
|
||||
<StackPanel>
|
||||
<RadioButton GroupName="自动分段" Name="CutDisabledRadioButton" Content="不自动分段"
|
||||
IsChecked="{Binding Path=CuttingMode, Converter={StaticResource EnumToBooleanConverter},
|
||||
ConverterParameter={x:Static flv:AutoCuttingMode.Disabled}}" />
|
||||
<RadioButton GroupName="自动分段" Name="CutBySizeRadioButton" Content="根据文件大小自动分段"
|
||||
IsChecked="{Binding Path=CuttingMode, Converter={StaticResource EnumToBooleanConverter},
|
||||
ConverterParameter={x:Static flv:AutoCuttingMode.BySize}}" />
|
||||
<RadioButton GroupName="自动分段" Name="CutByTimeRadioButton" Content="根据视频时间自动分段"
|
||||
IsChecked="{Binding Path=CuttingMode, Converter={StaticResource EnumToBooleanConverter},
|
||||
ConverterParameter={x:Static flv:AutoCuttingMode.ByTime}}" />
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0,5,0,0"
|
||||
Visibility="{Binding ElementName=CutDisabledRadioButton,Path=IsChecked,Converter={StaticResource InvertBooleanToVisibilityConverter}}">
|
||||
<TextBlock Text="每"/>
|
||||
<TextBox Margin="5,0" Width="100" Text="{Binding CuttingNumber,Delay=500}" ui:TextBoxHelper.IsDeleteButtonVisible="False"/>
|
||||
<TextBlock Text="MiB 保存为一个文件"
|
||||
Visibility="{Binding ElementName=CutBySizeRadioButton,Path=IsChecked,Converter={StaticResource BooleanToVisibilityConverter}}"/>
|
||||
<TextBlock Text="分 保存为一个文件"
|
||||
Visibility="{Binding ElementName=CutByTimeRadioButton,Path=IsChecked,Converter={StaticResource BooleanToVisibilityConverter}}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="录制功能">
|
||||
<StackPanel>
|
||||
<RadioButton GroupName="录制功能" Name="EnabledFeatureRecordOnlyRadioButton" Content="只启用录制功能" ToolTip="(默认)占内存更少,但不能使用即时剪辑"
|
||||
IsChecked="{Binding Path=EnabledFeature, Converter={StaticResource EnumToBooleanConverter},
|
||||
ConverterParameter={x:Static flv:EnabledFeature.RecordOnly}}" />
|
||||
<RadioButton GroupName="录制功能" Content="只启用即时剪辑功能" ToolTip="不保存所有直播数据"
|
||||
IsChecked="{Binding Path=EnabledFeature, Converter={StaticResource EnumToBooleanConverter},
|
||||
ConverterParameter={x:Static flv:EnabledFeature.ClipOnly}}" />
|
||||
<RadioButton GroupName="录制功能" Content="同时启用两个功能"
|
||||
IsChecked="{Binding Path=EnabledFeature, Converter={StaticResource EnumToBooleanConverter},
|
||||
ConverterParameter={x:Static flv:EnabledFeature.Both}}" />
|
||||
|
||||
<StackPanel Margin="0,5,0,0" Visibility="{Binding ElementName=EnabledFeatureRecordOnlyRadioButton,Path=IsChecked,Converter={StaticResource InvertBooleanToVisibilityConverter}}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="剪辑过去时长"/>
|
||||
<TextBox Margin="5,0" Width="80" Text="{Binding ClipLengthPast,Delay=500}" ui:TextBoxHelper.IsDeleteButtonVisible="False"/>
|
||||
<TextBlock Text="秒"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="剪辑将来时长"/>
|
||||
<TextBox Margin="5,0" Width="80" Text="{Binding ClipLengthFuture,Delay=500}" ui:TextBoxHelper.IsDeleteButtonVisible="False"/>
|
||||
<TextBlock Text="秒"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="文件名">
|
||||
<StackPanel MaxWidth="400" HorizontalAlignment="Left">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
|
||||
<StackPanel.ToolTip>
|
||||
<TextBlock FontSize="13">
|
||||
文件名变量说明<LineBreak/>
|
||||
<LineBreak/>
|
||||
日期: {date} <LineBreak/>
|
||||
时间: {time} <LineBreak/>
|
||||
房间号: {roomid} <LineBreak/>
|
||||
标题: {title} <LineBreak/>
|
||||
主播名: {name} <LineBreak/>
|
||||
随机数字: {random} <LineBreak/>
|
||||
<LineBreak/>
|
||||
所有 { } 大括号均为英文半角括号 <LineBreak/>
|
||||
只支持 flv 格式
|
||||
</TextBlock>
|
||||
</StackPanel.ToolTip>
|
||||
<TextBlock Text="说明"/>
|
||||
<ui:PathIcon Margin="2,0" VerticalAlignment="Center" Height="15" Data="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="录制文件名格式"/>
|
||||
<TextBox Text="{Binding RecordFilenameFormat,Delay=500}" ui:TextBoxHelper.IsDeleteButtonVisible="False"/>
|
||||
<TextBlock Text="剪辑文件名格式" Margin="0,5,0,0" Visibility="{Binding ElementName=EnabledFeatureRecordOnlyRadioButton,Path=IsChecked,Converter={StaticResource InvertBooleanToVisibilityConverter}}"/>
|
||||
<TextBox Text="{Binding ClipFilenameFormat,Delay=500}" ui:TextBoxHelper.IsDeleteButtonVisible="False"
|
||||
Visibility="{Binding ElementName=EnabledFeatureRecordOnlyRadioButton,Path=IsChecked,Converter={StaticResource InvertBooleanToVisibilityConverter}}"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</ui:SimpleStackPanel>
|
||||
</ScrollViewer>
|
||||
</ui:Page>
|
28
BililiveRecorder.WPF/Pages/SettingsPage.xaml.cs
Normal file
28
BililiveRecorder.WPF/Pages/SettingsPage.xaml.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace BililiveRecorder.WPF.Pages
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for SettingsPage.xaml
|
||||
/// </summary>
|
||||
public partial class SettingsPage
|
||||
{
|
||||
public SettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,363 +0,0 @@
|
|||
<Window x:Class="BililiveRecorder.WPF.SettingsWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:BililiveRecorder.WPF"
|
||||
xmlns:core="clr-namespace:BililiveRecorder.Core;assembly=BililiveRecorder.Core"
|
||||
xmlns:config="clr-namespace:BililiveRecorder.Core.Config;assembly=BililiveRecorder.Core"
|
||||
xmlns:flv="clr-namespace:BililiveRecorder.FlvProcessor;assembly=BililiveRecorder.FlvProcessor"
|
||||
d:DataContext="{d:DesignInstance Type=config:ConfigV1}"
|
||||
mc:Ignorable="d"
|
||||
ShowInTaskbar="False" ResizeMode="NoResize"
|
||||
Title="设置 - 录播姬" Height="630" Width="300">
|
||||
<Window.Resources>
|
||||
<local:ValueConverterGroup x:Key="EnumToInvertBooleanConverter">
|
||||
<local:EnumToBooleanConverter/>
|
||||
<local:BooleanInverterConverter/>
|
||||
</local:ValueConverterGroup>
|
||||
<local:EnumToBooleanConverter x:Key="EnumToBooleanConverter"/>
|
||||
</Window.Resources>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Margin="5">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="1*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.Resources>
|
||||
<ResourceDictionary>
|
||||
<Style TargetType="TextBlock">
|
||||
<Style.Setters>
|
||||
<Setter Property="HorizontalAlignment" Value="Right"></Setter>
|
||||
<Setter Property="VerticalAlignment" Value="Center"></Setter>
|
||||
</Style.Setters>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</Grid.Resources>
|
||||
<StackPanel Grid.Row="0">
|
||||
<StackPanel.Resources>
|
||||
<Style TargetType="CheckBox">
|
||||
<Setter Property="Margin" Value="10,1"/>
|
||||
</Style>
|
||||
</StackPanel.Resources>
|
||||
<CheckBox IsChecked="{Binding RecordDanmaku}">
|
||||
录制弹幕
|
||||
</CheckBox>
|
||||
<CheckBox IsEnabled="{Binding RecordDanmaku}" IsChecked="{Binding RecordDanmakuSuperChat}">
|
||||
同时保存 SuperChat
|
||||
</CheckBox>
|
||||
<CheckBox IsEnabled="{Binding RecordDanmaku}" IsChecked="{Binding RecordDanmakuGift}">
|
||||
同时保存 送礼信息
|
||||
</CheckBox>
|
||||
<CheckBox IsEnabled="{Binding RecordDanmaku}" IsChecked="{Binding RecordDanmakuGuard}">
|
||||
同时保存 舰长购买
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
<Separator Grid.Row="1"/>
|
||||
<Grid Grid.Row="2">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="2*"/>
|
||||
<ColumnDefinition Width="3*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Row="0" Grid.ColumnSpan="2" Margin="10,0">
|
||||
<RadioButton GroupName="EnabledFeature" IsChecked="{Binding Path=EnabledFeature, Converter={StaticResource EnumToBooleanConverter},
|
||||
ConverterParameter={x:Static flv:EnabledFeature.RecordOnly}}" ToolTipService.InitialShowDelay="0"
|
||||
ToolTip="占内存更少,但不能剪辑回放">只使用录制功能</RadioButton>
|
||||
<RadioButton GroupName="EnabledFeature" IsChecked="{Binding Path=EnabledFeature, Converter={StaticResource EnumToBooleanConverter},
|
||||
ConverterParameter={x:Static flv:EnabledFeature.ClipOnly}}" ToolTipService.InitialShowDelay="0"
|
||||
ToolTip="不保存所有直播数据到硬盘">只使用即时回放剪辑功能</RadioButton>
|
||||
<RadioButton GroupName="EnabledFeature" IsChecked="{Binding Path=EnabledFeature, Converter={StaticResource EnumToBooleanConverter},
|
||||
ConverterParameter={x:Static flv:EnabledFeature.Both}}">同时启用两个功能</RadioButton>
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Row="1" Grid.Column="0">剪辑过去时长:</TextBlock>
|
||||
<Grid Grid.Row="1" Grid.Column="1" VerticalAlignment="Center">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<local:ClickSelectTextBox Grid.Column="0" Text="{Binding ClipLengthPast,Delay=500,UpdateSourceTrigger=PropertyChanged}"
|
||||
IsEnabled="{Binding Path=EnabledFeature, Converter={StaticResource EnumToInvertBooleanConverter},
|
||||
ConverterParameter={x:Static flv:EnabledFeature.RecordOnly}}"/>
|
||||
<TextBlock Grid.Column="1" Margin="5,0,10,0">秒</TextBlock>
|
||||
</Grid>
|
||||
<TextBlock Grid.Row="2" Grid.Column="0">剪辑将来时长:</TextBlock>
|
||||
<Grid Grid.Row="2" Grid.Column="1" VerticalAlignment="Center">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<local:ClickSelectTextBox Grid.Column="0" Text="{Binding ClipLengthFuture,Delay=500,UpdateSourceTrigger=PropertyChanged}"
|
||||
IsEnabled="{Binding Path=EnabledFeature, Converter={StaticResource EnumToInvertBooleanConverter},
|
||||
ConverterParameter={x:Static flv:EnabledFeature.RecordOnly}}"/>
|
||||
<TextBlock Grid.Column="1" Margin="5,0,10,0">秒</TextBlock>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Separator Grid.Row="3"/>
|
||||
<Grid Grid.Row="4">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="2*"/>
|
||||
<ColumnDefinition Width="3*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Row="0" Grid.Column="0">自动切割单位:</TextBlock>
|
||||
<Grid Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<local:ClickSelectTextBox Grid.Column="0" Text="{Binding CuttingNumber,Delay=500,UpdateSourceTrigger=PropertyChanged}"
|
||||
ToolTipService.InitialShowDelay="0" ToolTipService.ShowDuration="20000"
|
||||
IsEnabled="{Binding Path=CuttingMode, Converter={StaticResource EnumToInvertBooleanConverter},
|
||||
ConverterParameter={x:Static flv:AutoCuttingMode.Disabled}}">
|
||||
<local:ClickSelectTextBox.ToolTip>
|
||||
<ToolTip>
|
||||
<StackPanel>
|
||||
<StackPanel.Resources>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="TextAlignment" Value="Left"/>
|
||||
</Style>
|
||||
</StackPanel.Resources>
|
||||
<TextBlock FontWeight="Bold">注:</TextBlock>
|
||||
<TextBlock>实际切割出来的视频文件会比设定的要大一点</TextBlock>
|
||||
<TextBlock>根据实际情况不同,可能会 大不到1MiB 或 长几秒钟</TextBlock>
|
||||
<TextBlock>请根据实际需求调整</TextBlock>
|
||||
</StackPanel>
|
||||
</ToolTip>
|
||||
</local:ClickSelectTextBox.ToolTip>
|
||||
</local:ClickSelectTextBox>
|
||||
<TextBlock Grid.Column="1" Margin="5,0,10,0">分 或 MiB</TextBlock>
|
||||
</Grid>
|
||||
<StackPanel Grid.Row="1" Grid.ColumnSpan="2" Margin="10,0">
|
||||
<RadioButton GroupName="CuttingMode" IsChecked="{Binding Path=CuttingMode, Converter={StaticResource EnumToBooleanConverter},
|
||||
ConverterParameter={x:Static flv:AutoCuttingMode.Disabled}}">不切割录制的视频文件</RadioButton>
|
||||
<RadioButton GroupName="CuttingMode" IsChecked="{Binding Path=CuttingMode, Converter={StaticResource EnumToBooleanConverter},
|
||||
ConverterParameter={x:Static flv:AutoCuttingMode.ByTime}}">根据视频时间(分)切割</RadioButton>
|
||||
<RadioButton GroupName="CuttingMode" IsChecked="{Binding Path=CuttingMode, Converter={StaticResource EnumToBooleanConverter},
|
||||
ConverterParameter={x:Static flv:AutoCuttingMode.BySize}}">根据文件大小(MiB)切割</RadioButton>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<Separator Grid.Row="5"/>
|
||||
<Grid Grid.Row="6" Margin="10,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock HorizontalAlignment="Left">录制保存文件名格式: (只支持FLV)</TextBlock>
|
||||
<local:ClickSelectTextBox Grid.Row="1" Text="{Binding RecordFilenameFormat,Delay=500,UpdateSourceTrigger=PropertyChanged}"/>
|
||||
</Grid>
|
||||
<Grid Grid.Row="7" Margin="10,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock HorizontalAlignment="Left">剪辑保存文件名格式: (只支持FLV)</TextBlock>
|
||||
<local:ClickSelectTextBox Grid.Row="1" Text="{Binding ClipFilenameFormat,Delay=500,UpdateSourceTrigger=PropertyChanged}"/>
|
||||
</Grid>
|
||||
<Grid Grid.Row="8" Margin="10,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock HorizontalAlignment="Left">文件名变量说明:</TextBlock>
|
||||
<TextBlock Grid.Row="1" HorizontalAlignment="Left">日期: {date} 时间: {time} 房间号: {roomid}</TextBlock>
|
||||
<TextBlock Grid.Row="2" HorizontalAlignment="Left">标题: {title} 主播名: {name} 随机数: {random}</TextBlock>
|
||||
</Grid>
|
||||
<Separator Grid.Row="9"/>
|
||||
<TextBlock Grid.Row="10" FontSize="16" FontWeight="Bold" TextAlignment="Center" HorizontalAlignment="Center">
|
||||
以下设置项谨慎修改,建议保留默认
|
||||
</TextBlock>
|
||||
<Separator Grid.Row="11"/>
|
||||
<Grid Grid.Row="12" Margin="10,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock HorizontalAlignment="Left">请求 API 时使用 Cookie: (可选)</TextBlock>
|
||||
<local:ClickSelectTextBox Grid.Row="1" Text="{Binding Cookie,Delay=500,UpdateSourceTrigger=PropertyChanged}"/>
|
||||
</Grid>
|
||||
<Separator Grid.Row="13"/>
|
||||
<Grid Grid.Row="14">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="2*"/>
|
||||
<ColumnDefinition Width="3*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Row="0" Grid.Column="0">录制重试间隔:</TextBlock>
|
||||
<Grid Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<local:ClickSelectTextBox Grid.Column="0" Text="{Binding TimingStreamRetry,Delay=500,UpdateSourceTrigger=PropertyChanged}">
|
||||
<local:ClickSelectTextBox.ToolTip>
|
||||
<ToolTip>
|
||||
<StackPanel>
|
||||
<StackPanel.Resources>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="TextAlignment" Value="Left"/>
|
||||
</Style>
|
||||
</StackPanel.Resources>
|
||||
<TextBlock FontWeight="Bold">注:</TextBlock>
|
||||
<TextBlock>录制断开后等待多长时间再尝试开始录制</TextBlock>
|
||||
</StackPanel>
|
||||
</ToolTip>
|
||||
</local:ClickSelectTextBox.ToolTip>
|
||||
</local:ClickSelectTextBox>
|
||||
<TextBlock Grid.Column="1" Margin="5,0,10,0">毫秒</TextBlock>
|
||||
</Grid>
|
||||
<TextBlock Grid.Row="1" Grid.Column="0">录制连接超时:</TextBlock>
|
||||
<Grid Grid.Row="1" Grid.Column="1" VerticalAlignment="Center">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<local:ClickSelectTextBox Grid.Column="0" Text="{Binding TimingStreamConnect,Delay=500,UpdateSourceTrigger=PropertyChanged}">
|
||||
<local:ClickSelectTextBox.ToolTip>
|
||||
<ToolTip>
|
||||
<StackPanel>
|
||||
<StackPanel.Resources>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="TextAlignment" Value="Left"/>
|
||||
</Style>
|
||||
</StackPanel.Resources>
|
||||
<TextBlock FontWeight="Bold">注:</TextBlock>
|
||||
<TextBlock>发出连接直播服务器的请求后等待多长时间</TextBlock>
|
||||
<TextBlock>防止直播服务器长时间不返回数据导致卡住</TextBlock>
|
||||
</StackPanel>
|
||||
</ToolTip>
|
||||
</local:ClickSelectTextBox.ToolTip>
|
||||
</local:ClickSelectTextBox>
|
||||
<TextBlock Grid.Column="1" Margin="5,0,10,0">毫秒</TextBlock>
|
||||
</Grid>
|
||||
<TextBlock Grid.Row="2" Grid.Column="0">弹幕重连间隔:</TextBlock>
|
||||
<Grid Grid.Row="2" Grid.Column="1" VerticalAlignment="Center">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<local:ClickSelectTextBox Grid.Column="0" Text="{Binding TimingDanmakuRetry,Delay=500,UpdateSourceTrigger=PropertyChanged}">
|
||||
<local:ClickSelectTextBox.ToolTip>
|
||||
<ToolTip>
|
||||
<StackPanel>
|
||||
<StackPanel.Resources>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="TextAlignment" Value="Left"/>
|
||||
</Style>
|
||||
</StackPanel.Resources>
|
||||
<TextBlock FontWeight="Bold">注:</TextBlock>
|
||||
<TextBlock>弹幕服务器被断开后等待多长时间再尝试连接</TextBlock>
|
||||
<TextBlock>监控开播的主要途径是通过弹幕服务器发送的信息</TextBlock>
|
||||
</StackPanel>
|
||||
</ToolTip>
|
||||
</local:ClickSelectTextBox.ToolTip>
|
||||
</local:ClickSelectTextBox>
|
||||
<TextBlock Grid.Column="1" Margin="5,0,10,0">毫秒</TextBlock>
|
||||
</Grid>
|
||||
<TextBlock Grid.Row="3" Grid.Column="0">接收数据超时:</TextBlock>
|
||||
<Grid Grid.Row="3" Grid.Column="1" VerticalAlignment="Center">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<local:ClickSelectTextBox Grid.Column="0" Text="{Binding TimingWatchdogTimeout,Delay=500,UpdateSourceTrigger=PropertyChanged}">
|
||||
<local:ClickSelectTextBox.ToolTip>
|
||||
<ToolTip>
|
||||
<StackPanel>
|
||||
<StackPanel.Resources>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="TextAlignment" Value="Left"/>
|
||||
</Style>
|
||||
</StackPanel.Resources>
|
||||
<TextBlock FontWeight="Bold">注:</TextBlock>
|
||||
<TextBlock>在一定时间没有收到直播服务器发送的数据后断开重连。</TextBlock>
|
||||
<TextBlock>用于防止因为玄学网络问题导致卡住</TextBlock>
|
||||
</StackPanel>
|
||||
</ToolTip>
|
||||
</local:ClickSelectTextBox.ToolTip>
|
||||
</local:ClickSelectTextBox>
|
||||
<TextBlock Grid.Column="1" Margin="5,0,10,0">毫秒</TextBlock>
|
||||
</Grid>
|
||||
<TextBlock Grid.Row="5" Grid.Column="0">开播检查间隔:</TextBlock>
|
||||
<Grid Grid.Row="5" Grid.Column="1" VerticalAlignment="Center">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<local:ClickSelectTextBox Grid.Column="0" Text="{Binding TimingCheckInterval,Delay=500,UpdateSourceTrigger=PropertyChanged}">
|
||||
<local:ClickSelectTextBox.ToolTip>
|
||||
<ToolTip>
|
||||
<StackPanel>
|
||||
<StackPanel.Resources>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="TextAlignment" Value="Left"/>
|
||||
</Style>
|
||||
</StackPanel.Resources>
|
||||
<TextBlock FontWeight="Bold">注:</TextBlock>
|
||||
<TextBlock>此项影响的时间间隔是定时请求HTTP接口的间隔,</TextBlock>
|
||||
<TextBlock>主要目的是防止没有从弹幕服务器收到开播消息。</TextBlock>
|
||||
<TextBlock>通常主播开播后会通过弹幕服务器通知录播姬开始录制,</TextBlock>
|
||||
<TextBlock>所以此项不需要设置太短。</TextBlock>
|
||||
<TextBlock>时间间隔设置太短会导致被B站服务器屏蔽,无法录制。</TextBlock>
|
||||
</StackPanel>
|
||||
</ToolTip>
|
||||
</local:ClickSelectTextBox.ToolTip>
|
||||
</local:ClickSelectTextBox>
|
||||
<TextBlock Grid.Column="1" Margin="5,0,10,0">秒</TextBlock>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<!--
|
||||
<TextBlock Grid.Row="4" Grid.Column="0"></TextBlock>
|
||||
<StackPanel Grid.Row="4" Grid.Column="1">
|
||||
|
||||
</StackPanel>
|
||||
-->
|
||||
</Grid>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Grid.Row="1"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<Button Click="Save" Padding="20,5" Margin="5" IsDefault="True">保存</Button>
|
||||
<Button Click="Cancel" Padding="20,5" Margin="5" IsCancel="True">取消</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
|
@ -1,71 +0,0 @@
|
|||
using BililiveRecorder.Core;
|
||||
using BililiveRecorder.Core.Config;
|
||||
using System.Windows;
|
||||
|
||||
namespace BililiveRecorder.WPF
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for SettingsWindow.xaml
|
||||
/// </summary>
|
||||
public partial class SettingsWindow : Window
|
||||
{
|
||||
public ConfigV1 Config { get; set; } = new ConfigV1();
|
||||
|
||||
public SettingsWindow(MainWindow mainWindow, ConfigV1 config)
|
||||
{
|
||||
Owner = mainWindow;
|
||||
config.CopyPropertiesTo(Config);
|
||||
DataContext = Config;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void Save(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// if (!_CheckSavePath())
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
|
||||
DialogResult = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
// private bool _CheckSavePath()
|
||||
// {
|
||||
// if (string.IsNullOrWhiteSpace(Config.SavePath))
|
||||
// {
|
||||
// MessageBox.Show("请设置一个录像保存路径", "保存路径不能为空", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
// return false;
|
||||
// }
|
||||
// return true;
|
||||
// }
|
||||
|
||||
private void Cancel(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// if (!_CheckSavePath())
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
private void Button_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// var fileDialog = new CommonOpenFileDialog()
|
||||
// {
|
||||
// IsFolderPicker = true,
|
||||
// Multiselect = false,
|
||||
// Title = "选择录制路径",
|
||||
// AddToMostRecentlyUsedList = false,
|
||||
// EnsurePathExists = true,
|
||||
// NavigateToShortcut = true,
|
||||
// InitialDirectory = Config.SavePath,
|
||||
// };
|
||||
// if (fileDialog.ShowDialog(this) == CommonFileDialogResult.Ok)
|
||||
// {
|
||||
// Config.SavePath = fileDialog.FileName;
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
74
BililiveRecorder.WPF/SingleInstance.cs
Normal file
74
BililiveRecorder.WPF/SingleInstance.cs
Normal file
|
@ -0,0 +1,74 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Remoting;
|
||||
using System.Runtime.Remoting.Channels;
|
||||
using System.Runtime.Remoting.Channels.Ipc;
|
||||
using System.Runtime.Serialization.Formatters;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace BililiveRecorder.WPF
|
||||
{
|
||||
public static class SingleInstance
|
||||
{
|
||||
private static Mutex singleInstanceMutex;
|
||||
private static IpcServerChannel channel;
|
||||
|
||||
public static event EventHandler NotificationReceived;
|
||||
|
||||
public static bool CheckMutex(string path)
|
||||
{
|
||||
const string RemoteServiceName = "SingleInstanceApplicationService";
|
||||
|
||||
var b64path = Convert.ToBase64String(Encoding.UTF8.GetBytes(path)).Replace('+', '_').Replace('/', '-');
|
||||
var identifier = "BililiveRecorder:SingeInstance:" + b64path;
|
||||
|
||||
singleInstanceMutex = new Mutex(true, identifier, out var createdNew);
|
||||
if (createdNew)
|
||||
{
|
||||
channel = new IpcServerChannel(new Dictionary<string, string>
|
||||
{
|
||||
["name"] = identifier,
|
||||
["portName"] = identifier,
|
||||
["exclusiveAddressUse"] = "false"
|
||||
}, new BinaryServerFormatterSinkProvider
|
||||
{
|
||||
TypeFilterLevel = TypeFilterLevel.Full
|
||||
});
|
||||
|
||||
ChannelServices.RegisterChannel(channel, true);
|
||||
RemotingServices.Marshal(new IPCRemoteService(), RemoteServiceName);
|
||||
}
|
||||
else
|
||||
{
|
||||
ChannelServices.RegisterChannel(new IpcClientChannel(), true);
|
||||
var remote = (IPCRemoteService)RemotingServices.Connect(typeof(IPCRemoteService), $"ipc://{identifier}/{RemoteServiceName}");
|
||||
remote?.Notify();
|
||||
}
|
||||
|
||||
return createdNew;
|
||||
}
|
||||
|
||||
public static void Cleanup()
|
||||
{
|
||||
singleInstanceMutex?.Close();
|
||||
singleInstanceMutex = null;
|
||||
|
||||
if (channel != null)
|
||||
{
|
||||
ChannelServices.UnregisterChannel(channel);
|
||||
channel = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ActivateFirstInstanceCallback() => NotificationReceived?.Invoke(null, EventArgs.Empty);
|
||||
|
||||
private class IPCRemoteService : MarshalByRefObject
|
||||
{
|
||||
public void Notify() => Application.Current?.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (Action)ActivateFirstInstanceCallback);
|
||||
public override object InitializeLifetimeService() => null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
<Window x:Class="BililiveRecorder.WPF.TimedMessageBox"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:BililiveRecorder.WPF"
|
||||
mc:Ignorable="d" ResizeMode="NoResize"
|
||||
Title="TimedMessageBox" Height="200" Width="320"
|
||||
Loaded="Window_Loaded">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="5*"/>
|
||||
<RowDefinition Height="3*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.ColumnSpan="2" VerticalAlignment="Center" TextAlignment="Center" TextWrapping="Wrap" FontSize="16" Text="{Binding Message}"/>
|
||||
<Button Grid.Row="1" Grid.Column="0" Margin="10" FontSize="20" Click="ConfirmClick">
|
||||
确定
|
||||
</Button>
|
||||
<Button Grid.Row="1" Grid.Column="1" Margin="10" Click="CancelClick">
|
||||
<TextBlock FontSize="20">取消 (<TextBlock Text="{Binding CountDown}"/>)</TextBlock>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Window>
|
|
@ -1,73 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace BililiveRecorder.WPF
|
||||
{
|
||||
/// <summary>
|
||||
/// TimedMessageBox.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class TimedMessageBox : Window, INotifyPropertyChanged
|
||||
{
|
||||
public string Message { get => _message; set => SetField(ref _message, value); }
|
||||
private string _message = string.Empty;
|
||||
|
||||
public int CountDown { get => _countdown; set => SetField(ref _countdown, value); }
|
||||
private int _countdown = 10;
|
||||
|
||||
private DispatcherTimer timer;
|
||||
|
||||
public TimedMessageBox()
|
||||
{
|
||||
timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1), IsEnabled = false };
|
||||
timer.Tick += (sender, e) =>
|
||||
{
|
||||
CountDown -= 1;
|
||||
if (CountDown <= 0)
|
||||
{
|
||||
Cancel();
|
||||
timer.Stop();
|
||||
timer = null;
|
||||
}
|
||||
};
|
||||
|
||||
DataContext = this;
|
||||
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void ConfirmClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DialogResult = true;
|
||||
timer.Stop();
|
||||
timer = null;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void CancelClick(object sender, RoutedEventArgs e) => Cancel();
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
DialogResult = false;
|
||||
timer.Stop();
|
||||
timer = null;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void Window_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
timer?.Start();
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
protected virtual void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value)) { return false; }
|
||||
field = value; OnPropertyChanged(propertyName); return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
<UserControl x:Class="BililiveRecorder.WPF.UpdateBarUserControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:BililiveRecorder.WPF"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="30" d:DesignWidth="800">
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<BooleanToVisibilityConverter x:Key="BoolToVis"/>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
<Grid Height="30"
|
||||
Visibility="{Binding Display,Converter={StaticResource BoolToVis}}"
|
||||
Background="Orange">
|
||||
<Grid>
|
||||
<StackPanel Orientation="Horizontal" Height="20" VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||
<TextBlock VerticalAlignment="Center" Text="{Binding MainText}"/>
|
||||
<Button Margin="5,0" Click="Button_Click">
|
||||
<TextBlock Margin="5,0" Text="{Binding ButtonText}"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<Viewbox HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Height="20" Width="20"
|
||||
Margin="0,0,10,0">
|
||||
<Border Background="Transparent" MouseUp="CloseUpdateBar">
|
||||
<Canvas Width="52" Height="52">
|
||||
<Path Fill="Black" Data="M26,0C11.664,0,0,11.663,0,26s11.664,26,26,26s26-11.663,26-26S40.336,0,26,0z M26,50C12.767,50,2,39.233,2,26 S12.767,2,26,2s24,10.767,24,24S39.233,50,26,50z"/>
|
||||
<Path Fill="Black" Data="M35.707,16.293c-0.391-0.391-1.023-0.391-1.414,0L26,24.586l-8.293-8.293c-0.391-0.391-1.023-0.391-1.414,0 s-0.391,1.023,0,1.414L24.586,26l-8.293,8.293c-0.391,0.391-0.391,1.023,0,1.414C16.488,35.902,16.744,36,17,36 s0.512-0.098,0.707-0.293L26,27.414l8.293,8.293C34.488,35.902,34.744,36,35,36s0.512-0.098,0.707-0.293 c0.391-0.391,0.391-1.023,0-1.414L27.414,26l8.293-8.293C36.098,17.316,36.098,16.684,35.707,16.293z"/>
|
||||
</Canvas>
|
||||
</Border>
|
||||
</Viewbox>
|
||||
</Grid>
|
||||
<Grid Visibility="{Binding ShowProgressBar,Converter={StaticResource BoolToVis}}">
|
||||
<ProgressBar Margin="10,5" Value="{Binding Progress}"/>
|
||||
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="{Binding ProgressText}"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
|
@ -1,91 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace BililiveRecorder.WPF
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for Updater.xaml
|
||||
/// </summary>
|
||||
public partial class UpdateBarUserControl : UserControl
|
||||
{
|
||||
public UpdateBarUserControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = this;
|
||||
}
|
||||
|
||||
public bool Display
|
||||
{
|
||||
get { return (bool)GetValue(DisplayProperty); }
|
||||
set { SetValue(DisplayProperty, value); }
|
||||
}
|
||||
public static readonly DependencyProperty DisplayProperty =
|
||||
DependencyProperty.Register("Display", typeof(bool), typeof(UpdateBarUserControl), new UIPropertyMetadata(false));
|
||||
|
||||
public bool ShowProgressBar
|
||||
{
|
||||
get { return (bool)GetValue(ShowProgressBarProperty); }
|
||||
set { SetValue(ShowProgressBarProperty, value); }
|
||||
}
|
||||
public static readonly DependencyProperty ShowProgressBarProperty =
|
||||
DependencyProperty.Register("ShowProgressBar", typeof(bool), typeof(UpdateBarUserControl), new UIPropertyMetadata(false));
|
||||
|
||||
public string MainText
|
||||
{
|
||||
get { return (string)GetValue(MainTextProperty); }
|
||||
set { SetValue(MainTextProperty, value); }
|
||||
}
|
||||
public static readonly DependencyProperty MainTextProperty =
|
||||
DependencyProperty.Register("MainText", typeof(string), typeof(UpdateBarUserControl), new UIPropertyMetadata("有新更新可用!"));
|
||||
|
||||
public string ButtonText
|
||||
{
|
||||
get { return (string)GetValue(ButtonTextProperty); }
|
||||
set { SetValue(ButtonTextProperty, value); }
|
||||
}
|
||||
public static readonly DependencyProperty ButtonTextProperty =
|
||||
DependencyProperty.Register("ButtonText", typeof(string), typeof(UpdateBarUserControl), new UIPropertyMetadata("下载更新"));
|
||||
|
||||
public string ProgressText
|
||||
{
|
||||
get { return (string)GetValue(ProgressTextProperty); }
|
||||
set { SetValue(ProgressTextProperty, value); }
|
||||
}
|
||||
public static readonly DependencyProperty ProgressTextProperty =
|
||||
DependencyProperty.Register("ProgressText", typeof(string), typeof(UpdateBarUserControl), new UIPropertyMetadata("0KiB / 0KiB - 0%"));
|
||||
|
||||
public double Progress
|
||||
{
|
||||
get { return (double)GetValue(ProgressProperty); }
|
||||
set { SetValue(ProgressProperty, value); }
|
||||
}
|
||||
public static readonly DependencyProperty ProgressProperty =
|
||||
DependencyProperty.Register("Progress", typeof(double), typeof(UpdateBarUserControl), new UIPropertyMetadata(0d));
|
||||
|
||||
|
||||
public event RoutedEventHandler MainButtonClick;
|
||||
|
||||
|
||||
private void CloseUpdateBar(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
Display = false;
|
||||
}
|
||||
|
||||
private void Button_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
MainButtonClick?.Invoke(sender, e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,138 +0,0 @@
|
|||
using BililiveRecorder.FlvProcessor;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace BililiveRecorder.WPF
|
||||
{
|
||||
internal class ClipButtonVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is null) return Visibility.Visible;
|
||||
return EnabledFeature.RecordOnly == ((EnabledFeature)value) ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
internal class BindingProxy : Freezable
|
||||
{
|
||||
protected override Freezable CreateInstanceCore()
|
||||
{
|
||||
return new BindingProxy();
|
||||
}
|
||||
|
||||
public object Data
|
||||
{
|
||||
get { return (object)GetValue(DataProperty); }
|
||||
set { SetValue(DataProperty, value); }
|
||||
}
|
||||
|
||||
// Using a DependencyProperty as the backing store for Data. This enables animation, styling, binding, etc...
|
||||
public static readonly DependencyProperty DataProperty = DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
|
||||
}
|
||||
|
||||
internal class ValueConverterGroup : List<IValueConverter>, IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return this.Aggregate(value, (current, converter) => converter.Convert(current, targetType, parameter, culture));
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
internal class EnumToBooleanConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value.Equals(parameter);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value.Equals(true) ? parameter : Binding.DoNothing;
|
||||
}
|
||||
}
|
||||
|
||||
internal class BooleanInverterConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return !(bool)value;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return !(bool)value;
|
||||
}
|
||||
}
|
||||
|
||||
internal class RecordStatusConverter : IMultiValueConverter
|
||||
{
|
||||
public object Convert(object[] value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value[0] is bool IsMonitoring && value[1] is bool IsRecording)
|
||||
{
|
||||
int i = (IsMonitoring ? 1 : 0) + (IsRecording ? 2 : 0);
|
||||
switch (i)
|
||||
{
|
||||
case 0:
|
||||
return "闲置中";
|
||||
case 1:
|
||||
return "监控中";
|
||||
case 2:
|
||||
case 3:
|
||||
return "录制中";
|
||||
default:
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
internal class BoolToValueConverter<T> : IValueConverter
|
||||
{
|
||||
public T FalseValue { get; set; }
|
||||
public T TrueValue { get; set; }
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return FalseValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
return (bool)value ? TrueValue : FalseValue;
|
||||
}
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value != null ? value.Equals(TrueValue) : false;
|
||||
}
|
||||
}
|
||||
|
||||
internal class BoolToStringConverter : BoolToValueConverter<string> { }
|
||||
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
<Window x:Class="BililiveRecorder.WPF.WorkDirectoryWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:BililiveRecorder.WPF"
|
||||
mc:Ignorable="d" ResizeMode="NoResize"
|
||||
Title="录播姬 - 选择工作目录" Height="300" Width="400">
|
||||
<Grid Margin="15">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="3*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="5*"/>
|
||||
<RowDefinition Height="4*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock FontSize="26" HorizontalAlignment="Center" TextAlignment="Center" VerticalAlignment="Center">选择工作目录</TextBlock>
|
||||
<StackPanel Grid.Row="1">
|
||||
<StackPanel.Resources>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="TextAlignment" Value="Center"/>
|
||||
</Style>
|
||||
</StackPanel.Resources>
|
||||
<TextBlock>所有数据都会保存在这个目录中</TextBlock>
|
||||
<TextBlock>包括:软件配置、录制的视频文件</TextBlock>
|
||||
</StackPanel>
|
||||
<Grid Grid.Row="2" Margin="7">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="7*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBox VerticalAlignment="Center" FontSize="14" Text="{Binding WorkPath,Delay=800,UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Grid.Column="1" Margin="5,0,0,0" Click="Button_Click">浏览...</Button>
|
||||
<TextBlock Grid.Row="1" Grid.ColumnSpan="2" FontSize="18" Text="{Binding StatusText}" Foreground="{Binding StatusColor}"
|
||||
VerticalAlignment="Center" TextAlignment="Center" HorizontalAlignment="Center" FontWeight="Bold">
|
||||
<TextBlock.Effect>
|
||||
<DropShadowEffect Opacity="0.3" ShadowDepth="1"/>
|
||||
</TextBlock.Effect>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
<Grid Grid.Row="3">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.Resources>
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Center"/>
|
||||
<Setter Property="Padding" Value="20,10"/>
|
||||
<Setter Property="FontSize" Value="16"/>
|
||||
</Style>
|
||||
</Grid.Resources>
|
||||
<Button IsEnabled="{Binding Status}" IsDefault="True" Click="Button_Click_1">
|
||||
确认
|
||||
</Button>
|
||||
<Button Grid.Column="1" IsCancel="True" Click="Button_Click_2">
|
||||
退出
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
|
@ -1,187 +0,0 @@
|
|||
using Microsoft.WindowsAPICodePack.Dialogs;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace BililiveRecorder.WPF
|
||||
{
|
||||
/// <summary>
|
||||
/// WorkDirectoryWindow.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class WorkDirectoryWindow : Window, INotifyPropertyChanged
|
||||
{
|
||||
public static readonly SolidColorBrush Red = new SolidColorBrush(Color.FromArgb(0xFF, 0xF7, 0x1B, 0x1B));
|
||||
public static readonly SolidColorBrush Green = new SolidColorBrush(Color.FromArgb(0xFF, 0x0B, 0xB4, 0x22));
|
||||
|
||||
private Mutex mutex;
|
||||
|
||||
public WorkDirectoryWindow()
|
||||
{
|
||||
DataContext = this;
|
||||
InitializeComponent();
|
||||
PropertyChanged += WorkDirectoryWindow_PropertyChanged;
|
||||
//DialogResult = false;
|
||||
}
|
||||
|
||||
private void WorkDirectoryWindow_PropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(Status))
|
||||
{
|
||||
if (Status)
|
||||
{
|
||||
StatusColor = Green;
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusColor = Red;
|
||||
}
|
||||
}
|
||||
else if (e.PropertyName == nameof(WorkPath))
|
||||
{
|
||||
CheckPath();
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckPath()
|
||||
{
|
||||
string c = WorkPath;
|
||||
string config = Path.Combine(c, "config.json");
|
||||
bool result = false;
|
||||
|
||||
if (!Directory.Exists(c))
|
||||
{
|
||||
StatusText = "目录不存在";
|
||||
result = false;
|
||||
}
|
||||
else if (!Directory.EnumerateFiles(c).Any())
|
||||
{
|
||||
StatusText = "可用的空文件夹";
|
||||
result = true;
|
||||
}
|
||||
else if (!File.Exists(config))
|
||||
{
|
||||
StatusText = "此文件夹已有其他文件";
|
||||
result = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
JObject j = JObject.Parse(File.ReadAllText(config));
|
||||
if (j["version"] == null || j["data"] == null)
|
||||
{
|
||||
StatusText = "配置文件损坏";
|
||||
result = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusText = "录播姬曾经使用过的目录";
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
StatusText = "配置文件不可读";
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result)
|
||||
{
|
||||
Status = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (mutex != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
mutex.ReleaseMutex();
|
||||
}
|
||||
catch (Exception)
|
||||
{ }
|
||||
finally
|
||||
{
|
||||
mutex.Dispose();
|
||||
mutex = null;
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
mutex = new Mutex(true, @"Global\BililiveRecorder.WPF.." + c.GetHashCode(), out bool createdNew);
|
||||
if (createdNew)
|
||||
{
|
||||
Status = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Status = false;
|
||||
StatusText = "已有录播姬在此文件夹运行";
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Status = false;
|
||||
StatusText = "检查录播姬运行状态时出错";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string _workPath;
|
||||
public string WorkPath { get => _workPath; set => SetField(ref _workPath, value.TrimEnd('/', '\\')); }
|
||||
|
||||
private string _statusText = "请选择目录";
|
||||
public string StatusText { get => _statusText; set => SetField(ref _statusText, value); }
|
||||
|
||||
private SolidColorBrush _statusColor = Red;
|
||||
public SolidColorBrush StatusColor { get => _statusColor; set => SetField(ref _statusColor, value); }
|
||||
|
||||
private bool _status;
|
||||
public bool Status { get => _status; set => SetField(ref _status, value); }
|
||||
|
||||
private void Button_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var fileDialog = new CommonOpenFileDialog()
|
||||
{
|
||||
IsFolderPicker = true,
|
||||
Multiselect = false,
|
||||
Title = "选择录播姬工作目录路径",
|
||||
AddToMostRecentlyUsedList = false,
|
||||
EnsurePathExists = true,
|
||||
NavigateToShortcut = true,
|
||||
InitialDirectory = WorkPath,
|
||||
};
|
||||
if (fileDialog.ShowDialog(this) == CommonFileDialogResult.Ok)
|
||||
{
|
||||
WorkPath = fileDialog.FileName;
|
||||
}
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
protected virtual void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value)) { return false; }
|
||||
field = value; OnPropertyChanged(propertyName); return true;
|
||||
}
|
||||
|
||||
private void Button_Click_1(object sender, RoutedEventArgs e)
|
||||
{
|
||||
GC.KeepAlive(mutex);
|
||||
DialogResult = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void Button_Click_2(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Autofac" version="4.8.1" targetFramework="net462" />
|
||||
<package id="CommandLineParser" version="2.4.3" targetFramework="net462" />
|
||||
<package id="DeltaCompressionDotNet" version="1.1.0" targetFramework="net462" />
|
||||
<package id="Hardcodet.NotifyIcon.Wpf" version="1.0.8" targetFramework="net462" />
|
||||
<package id="Mono.Cecil" version="0.9.6.1" targetFramework="net462" />
|
||||
<package id="Newtonsoft.Json" version="11.0.2" targetFramework="net462" />
|
||||
<package id="NLog" version="4.5.10" targetFramework="net462" />
|
||||
<package id="NLog.Config" version="4.5.10" targetFramework="net462" />
|
||||
<package id="NLog.Schema" version="4.5.10" targetFramework="net462" />
|
||||
<package id="NuGet.CommandLine" version="4.7.1" targetFramework="net462" developmentDependency="true" />
|
||||
<package id="SharpCompress" version="0.17.1" targetFramework="net462" />
|
||||
<package id="Splat" version="1.6.2" targetFramework="net462" />
|
||||
<package id="squirrel.windows" version="1.9.0" targetFramework="net462" />
|
||||
<package id="WindowsAPICodePack-Core" version="1.1.2" targetFramework="net462" />
|
||||
<package id="WindowsAPICodePack-Shell" version="1.1.1" targetFramework="net462" />
|
||||
</packages>
|
|
@ -14,6 +14,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.FlvProcess
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.Cli", "BililiveRecorder.Cli\BililiveRecorder.Cli.csproj", "{1B626335-283F-4313-9045-B5B96FAAB2DF}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FD13D9F6-94CD-4CBA-AEA8-EF71002EAC6B}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
7. 修改 `rec.danmuji.org` 上的更新日志
|
||||
8. Fast-Forward merge back to dev
|
||||
|
||||
|
||||
## 发布新版本流程
|
||||
|
||||
- commit 所有代码和文件修改 ...
|
||||
|
|
Loading…
Reference in New Issue
Block a user