mirror of
https://github.com/BililiveRecorder/BililiveRecorder.git
synced 2024-11-16 11:42:22 +08:00
add new ui and others
This commit is contained in:
parent
f6ad6b8593
commit
a09e729178
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,12 +91,13 @@ 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);
|
||||
if (config.RecordDanmakuRaw)
|
||||
xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["info"]?.ToString(Newtonsoft.Json.Formatting.None));
|
||||
xmlWriter.WriteValue(danmakuModel.CommentText);
|
||||
xmlWriter.WriteEndElement();
|
||||
|
@ -108,9 +107,12 @@ 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());
|
||||
if (config.RecordDanmakuRaw)
|
||||
xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None));
|
||||
xmlWriter.WriteValue(danmakuModel.CommentText);
|
||||
xmlWriter.WriteEndElement();
|
||||
|
@ -120,9 +122,12 @@ 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());
|
||||
if (config.RecordDanmakuRaw)
|
||||
xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None));
|
||||
xmlWriter.WriteEndElement();
|
||||
}
|
||||
|
@ -131,9 +136,12 @@ 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());
|
||||
if (config.RecordDanmakuRaw)
|
||||
xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None));
|
||||
xmlWriter.WriteEndElement();
|
||||
}
|
||||
|
|
|
@ -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,37 +111,9 @@ namespace BililiveRecorder.Core
|
|||
/// <returns>FLV播放地址</returns>
|
||||
/// <exception cref="WebException"/>
|
||||
/// <exception cref="Exception"/>
|
||||
public static async Task<string> GetPlayUrlAsync(int roomid)
|
||||
{
|
||||
string url = $@"{Config.LiveApiHost}/room/v1/Room/playUrl?cid={roomid}&quality=4&platform=web";
|
||||
if (Config.AvoidTxy)
|
||||
{
|
||||
// 尽量避开腾讯云
|
||||
int attempt_left = 2;
|
||||
while (true)
|
||||
{
|
||||
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("没有直播播放地址");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
public async Task<string> GetPlayUrlAsync(int roomid)
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
@ -152,7 +126,6 @@ namespace BililiveRecorder.Core
|
|||
}
|
||||
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,56 +44,7 @@ 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)
|
||||
{
|
||||
|
@ -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,7 +179,7 @@ namespace BililiveRecorder.Core
|
|||
|
||||
public async Task<RoomInfo> FetchRoomInfoAsync()
|
||||
{
|
||||
RoomInfo roomInfo = await BililiveAPI.GetRoomInfoAsync(Roomid).ConfigureAwait(false);
|
||||
RoomInfo roomInfo = await bililiveAPI.GetRoomInfoAsync(Roomid).ConfigureAwait(false);
|
||||
RoomInfoUpdated?.Invoke(this, new RoomInfoUpdatedArgs { RoomInfo = roomInfo });
|
||||
return roomInfo;
|
||||
}
|
||||
|
@ -208,7 +209,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");
|
||||
|
||||
|
|
|
@ -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,19 @@
|
|||
<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 -->
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
|
|
|
@ -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,95 @@
|
|||
<SubType>Designer</SubType>
|
||||
</ApplicationDefinition>
|
||||
<Compile Include="..\TempBuildInfo\BuildInfo.WPF.cs" />
|
||||
<Compile Include="ClickSelectTextBox.xaml.cs">
|
||||
<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\WorkDirectorySelectorDialog.xaml.cs">
|
||||
<DependentUpon>WorkDirectorySelectorDialog.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Converters\ClipEnabledToBooleanConverter.cs" />
|
||||
<Compile Include="Converters\EnumToBooleanConverter.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="Legacy\ClickSelectTextBox.xaml.cs">
|
||||
<DependentUpon>ClickSelectTextBox.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="CommandLineOption.cs" />
|
||||
<Compile Include="EnumerableExtensions.cs" />
|
||||
<Compile Include="TimedMessageBox.xaml.cs">
|
||||
<Compile Include="Legacy\TimedMessageBox.xaml.cs">
|
||||
<DependentUpon>TimedMessageBox.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="ValueConverters.cs" />
|
||||
<Compile Include="SettingsWindow.xaml.cs">
|
||||
<Compile Include="Legacy\ValueConverters.cs" />
|
||||
<Compile Include="Legacy\SettingsWindow.xaml.cs">
|
||||
<DependentUpon>SettingsWindow.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="UpdateBarUserControl.xaml.cs">
|
||||
<Compile Include="Legacy\UpdateBarUserControl.xaml.cs">
|
||||
<DependentUpon>UpdateBarUserControl.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="WorkDirectoryWindow.xaml.cs">
|
||||
<Compile Include="Legacy\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\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="Legacy\ClickSelectTextBox.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="DesignTimeResources.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
<Page Include="Legacy\MainWindow.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
|
@ -162,28 +178,65 @@
|
|||
<DependentUpon>App.xaml</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="MainWindow.xaml.cs">
|
||||
<Compile Include="Legacy\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="Legacy\SettingsWindow.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="UpdateBarUserControl.xaml">
|
||||
<Page Include="Legacy\TimedMessageBox.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="WorkDirectoryWindow.xaml">
|
||||
<Page Include="Legacy\UpdateBarUserControl.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Legacy\WorkDirectoryWindow.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Pages\LogPage.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Pages\RoomListPage.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<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 +256,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 +270,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)
|
||||
|
|
27
BililiveRecorder.WPF/Controls/AddRoomCard.xaml
Normal file
27
BililiveRecorder.WPF/Controls/AddRoomCard.xaml
Normal file
|
@ -0,0 +1,27 @@
|
|||
<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">
|
||||
<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>
|
||||
</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();
|
||||
}
|
||||
}
|
||||
}
|
125
BililiveRecorder.WPF/Controls/RoomCard.xaml
Normal file
125
BililiveRecorder.WPF/Controls/RoomCard.xaml
Normal file
|
@ -0,0 +1,125 @@
|
|||
<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>
|
||||
<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*"/>
|
||||
<ColumnDefinition Width="0.7*"/>
|
||||
</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="Black" 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" 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>
|
||||
</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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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="确定"
|
||||
IsPrimaryButtonEnabled="True"
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
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,4 +1,4 @@
|
|||
<Window x:Class="BililiveRecorder.WPF.MainWindow"
|
||||
<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"
|
||||
|
@ -118,7 +118,9 @@
|
|||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
<Grid Grid.Column="1" DataContext="{Binding ElementName=RoomList,Path=SelectedItem}" Grid.RowSpan="2">
|
||||
<Grid Grid.Column="1" Grid.RowSpan="2"
|
||||
DataContext="{Binding ElementName=RoomList,Path=SelectedItem}"
|
||||
d:DataContext="{d:DesignInstance Type=core:RecordedRoom}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="10"/>
|
||||
<RowDefinition Height="Auto"/>
|
|
@ -1,4 +1,4 @@
|
|||
using Autofac;
|
||||
using Autofac;
|
||||
using BililiveRecorder.Core;
|
||||
using BililiveRecorder.FlvProcessor;
|
||||
using CommandLine;
|
||||
|
@ -140,7 +140,8 @@ namespace BililiveRecorder.WPF
|
|||
}.ShowDialog() == true)
|
||||
{
|
||||
_AddLog = null;
|
||||
Recorder.Shutdown();
|
||||
// Recorder.Shutdown();
|
||||
Recorder.Dispose();
|
||||
try
|
||||
{
|
||||
File.WriteAllText(LAST_WORK_DIR_FILE, Recorder.Config.WorkDirectory);
|
|
@ -1,4 +1,4 @@
|
|||
<Window x:Class="BililiveRecorder.WPF.SettingsWindow"
|
||||
<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"
|
||||
|
@ -10,7 +10,7 @@
|
|||
d:DataContext="{d:DesignInstance Type=config:ConfigV1}"
|
||||
mc:Ignorable="d"
|
||||
ShowInTaskbar="False" ResizeMode="NoResize"
|
||||
Title="设置 - 录播姬" Height="630" Width="300">
|
||||
Title="设置 - 录播姬" Height="930" Width="300">
|
||||
<Window.Resources>
|
||||
<local:ValueConverterGroup x:Key="EnumToInvertBooleanConverter">
|
||||
<local:EnumToBooleanConverter/>
|
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);
|
||||
}
|
||||
}
|
||||
}
|
50
BililiveRecorder.WPF/Models/RootModel.cs
Normal file
50
BililiveRecorder.WPF/Models/RootModel.cs
Normal file
|
@ -0,0 +1,50 @@
|
|||
using System;
|
||||
using BililiveRecorder.Core;
|
||||
|
||||
namespace BililiveRecorder.WPF.Models
|
||||
{
|
||||
internal class RootModel : IDisposable
|
||||
{
|
||||
private bool disposedValue;
|
||||
|
||||
public LogModel Logs { get; } = new LogModel();
|
||||
|
||||
public IRecorder Recorder { get; internal set; }
|
||||
|
||||
public RootModel()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
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
20
BililiveRecorder.WPF/NewMainWindow.xaml
Normal file
20
BililiveRecorder.WPF/NewMainWindow.xaml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<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"
|
||||
Title="录播姬">
|
||||
<pages:RootPage x:Name="RootPage" CloseWindowRequested="RootPage_CloseWindowRequested"/>
|
||||
</Window>
|
86
BililiveRecorder.WPF/NewMainWindow.xaml.cs
Normal file
86
BililiveRecorder.WPF/NewMainWindow.xaml.cs
Normal file
|
@ -0,0 +1,86 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
using BililiveRecorder.WPF.Controls;
|
||||
using ModernWpf.Controls;
|
||||
|
||||
namespace BililiveRecorder.WPF
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for NewMainWindow.xaml
|
||||
/// </summary>
|
||||
public partial class NewMainWindow : Window
|
||||
{
|
||||
public NewMainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
Title = "录播姬 " + BuildInfo.Version + " " + BuildInfo.HeadShaShort;
|
||||
|
||||
BeforeWindowClose += NewMainWindow_BeforeWindowClose;
|
||||
SingleInstance.NotificationReceived += SingleInstance_NotificationReceived;
|
||||
}
|
||||
|
||||
private void SingleInstance_NotificationReceived(object sender, EventArgs e)
|
||||
{
|
||||
WindowState = WindowState.Normal;
|
||||
Topmost = true;
|
||||
Activate();
|
||||
Topmost = false;
|
||||
Focus();
|
||||
}
|
||||
|
||||
private bool CloseConfirmed = false;
|
||||
|
||||
private readonly SemaphoreSlim CloseWindowSemaphoreSlim = new SemaphoreSlim(1, 1);
|
||||
|
||||
public event EventHandler BeforeWindowClose;
|
||||
|
||||
public bool PromptCloseConfirm { get; set; } = true;
|
||||
|
||||
private void NewMainWindow_BeforeWindowClose(object sender, EventArgs e)
|
||||
{
|
||||
RootPage?.Shutdown();
|
||||
SingleInstance.Cleanup();
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
BeforeWindowClose?.Invoke(this, EventArgs.Empty);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void CloseWithoutConfirm()
|
||||
{
|
||||
CloseConfirmed = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void RootPage_CloseWindowRequested(object sender, EventArgs e)
|
||||
{
|
||||
CloseWithoutConfirm();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
81
BililiveRecorder.WPF/Pages/LogPage.xaml
Normal file
81
BililiveRecorder.WPF/Pages/LogPage.xaml
Normal file
|
@ -0,0 +1,81 @@
|
|||
<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="White" 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="White" 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();
|
||||
}
|
||||
}
|
||||
}
|
54
BililiveRecorder.WPF/Pages/RoomListPage.xaml
Normal file
54
BililiveRecorder.WPF/Pages/RoomListPage.xaml
Normal file
|
@ -0,0 +1,54 @@
|
|||
<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">
|
||||
<Border Background="White" CornerRadius="5">
|
||||
<controls:RoomCard DeleteRequested="RoomCard_DeleteRequested" />
|
||||
</Border>
|
||||
</ui:ThemeShadowChrome>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="AddRoomCardTemplate">
|
||||
<ui:ThemeShadowChrome IsShadowEnabled="True" Depth="10">
|
||||
<Border Background="White" CornerRadius="5">
|
||||
<controls:AddRoomCard AddRoomRequested="AddRoomCard_AddRoomRequested"/>
|
||||
</Border>
|
||||
</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);
|
||||
}
|
||||
}
|
||||
}
|
76
BililiveRecorder.WPF/Pages/RootPage.xaml
Normal file
76
BililiveRecorder.WPF/Pages/RootPage.xaml
Normal file
|
@ -0,0 +1,76 @@
|
|||
<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:mock="clr-namespace:BililiveRecorder.WPF.MockData"
|
||||
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}">
|
||||
<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>
|
||||
<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>
|
||||
</UserControl>
|
188
BililiveRecorder.WPF/Pages/RootPage.xaml.cs
Normal file
188
BililiveRecorder.WPF/Pages/RootPage.xaml.cs
Normal file
|
@ -0,0 +1,188 @@
|
|||
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 event EventHandler CloseWindowRequested;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
Model.Dispose();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
CloseWindowRequested?.Invoke(this, EventArgs.Empty);
|
||||
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
|
||||
{
|
||||
CloseWindowRequested?.Invoke(this, EventArgs.Empty);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
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();
|
||||
}
|
||||
}
|
||||
}
|
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,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
|
||||
|
|
Loading…
Reference in New Issue
Block a user