mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2024-09-18 19:58:45 -04:00
Migrate to Avalonia (#1220)
This commit is contained in:
parent
74f99b4e59
commit
b9c1c47474
89 changed files with 2467 additions and 2810 deletions
1
.github/workflows/main.yml
vendored
1
.github/workflows/main.yml
vendored
|
@ -106,6 +106,7 @@ jobs:
|
|||
-p:CSharpier_Bypass=true
|
||||
--output ${{ matrix.app }}/bin/publish/
|
||||
--configuration Release
|
||||
--use-current-runtime
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
|
||||
|
|
|
@ -11,19 +11,18 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AngleSharp" Version="1.0.7" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0" PrivateAssets="all" />
|
||||
<PackageReference Include="CSharpier.MsBuild" Version="0.26.7" PrivateAssets="all" />
|
||||
<PackageReference Include="AngleSharp" Version="1.1.2" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2" PrivateAssets="all" />
|
||||
<PackageReference Include="CSharpier.MsBuild" Version="0.28.0" PrivateAssets="all" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" PrivateAssets="all" />
|
||||
<PackageReference Include="JsonExtensions" Version="1.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="ReflectionMagic" Version="5.0.0" />
|
||||
<PackageReference Include="xunit" Version="2.6.4" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageReference Include="xunit" Version="2.7.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.8" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -93,13 +93,12 @@ public static class ExportWrapper
|
|||
Snowflake messageId
|
||||
)
|
||||
{
|
||||
var message = (await GetMessagesAsHtmlAsync(channelId)).SingleOrDefault(
|
||||
e =>
|
||||
string.Equals(
|
||||
e.GetAttribute("data-message-id"),
|
||||
messageId.ToString(),
|
||||
StringComparison.OrdinalIgnoreCase
|
||||
)
|
||||
var message = (await GetMessagesAsHtmlAsync(channelId)).SingleOrDefault(e =>
|
||||
string.Equals(
|
||||
e.GetAttribute("data-message-id"),
|
||||
messageId.ToString(),
|
||||
StringComparison.OrdinalIgnoreCase
|
||||
)
|
||||
);
|
||||
|
||||
if (message is null)
|
||||
|
@ -117,13 +116,12 @@ public static class ExportWrapper
|
|||
Snowflake messageId
|
||||
)
|
||||
{
|
||||
var message = (await GetMessagesAsJsonAsync(channelId)).SingleOrDefault(
|
||||
j =>
|
||||
string.Equals(
|
||||
j.GetProperty("id").GetString(),
|
||||
messageId.ToString(),
|
||||
StringComparison.OrdinalIgnoreCase
|
||||
)
|
||||
var message = (await GetMessagesAsJsonAsync(channelId)).SingleOrDefault(j =>
|
||||
string.Equals(
|
||||
j.GetProperty("id").GetString(),
|
||||
messageId.ToString(),
|
||||
StringComparison.OrdinalIgnoreCase
|
||||
)
|
||||
);
|
||||
|
||||
if (message.ValueKind == JsonValueKind.Undefined)
|
||||
|
|
|
@ -53,10 +53,8 @@ public class DateRangeSpecs
|
|||
new DateTimeOffset(2021, 09, 08, 14, 26, 35, TimeSpan.Zero)
|
||||
],
|
||||
o =>
|
||||
o.Using<DateTimeOffset>(
|
||||
ctx =>
|
||||
ctx.Subject.Should()
|
||||
.BeCloseTo(ctx.Expectation, TimeSpan.FromSeconds(1))
|
||||
o.Using<DateTimeOffset>(ctx =>
|
||||
ctx.Subject.Should().BeCloseTo(ctx.Expectation, TimeSpan.FromSeconds(1))
|
||||
)
|
||||
.WhenTypeIs<DateTimeOffset>()
|
||||
);
|
||||
|
@ -97,10 +95,8 @@ public class DateRangeSpecs
|
|||
new DateTimeOffset(2021, 07, 19, 17, 23, 58, TimeSpan.Zero)
|
||||
],
|
||||
o =>
|
||||
o.Using<DateTimeOffset>(
|
||||
ctx =>
|
||||
ctx.Subject.Should()
|
||||
.BeCloseTo(ctx.Expectation, TimeSpan.FromSeconds(1))
|
||||
o.Using<DateTimeOffset>(ctx =>
|
||||
ctx.Subject.Should().BeCloseTo(ctx.Expectation, TimeSpan.FromSeconds(1))
|
||||
)
|
||||
.WhenTypeIs<DateTimeOffset>()
|
||||
);
|
||||
|
@ -144,10 +140,8 @@ public class DateRangeSpecs
|
|||
new DateTimeOffset(2021, 07, 24, 14, 52, 40, TimeSpan.Zero)
|
||||
],
|
||||
o =>
|
||||
o.Using<DateTimeOffset>(
|
||||
ctx =>
|
||||
ctx.Subject.Should()
|
||||
.BeCloseTo(ctx.Expectation, TimeSpan.FromSeconds(1))
|
||||
o.Using<DateTimeOffset>(ctx =>
|
||||
ctx.Subject.Should().BeCloseTo(ctx.Expectation, TimeSpan.FromSeconds(1))
|
||||
)
|
||||
.WhenTypeIs<DateTimeOffset>()
|
||||
);
|
||||
|
|
|
@ -90,12 +90,11 @@ public class HtmlEmbedSpecs
|
|||
.QuerySelectorAll("source")
|
||||
.Select(e => e.GetAttribute("src"))
|
||||
.WhereNotNull()
|
||||
.Where(
|
||||
s =>
|
||||
s.Contains(
|
||||
"i_am_currently_feeling_slight_displeasure_of_what_you_have_just_sent_lqrem.mp4",
|
||||
StringComparison.Ordinal
|
||||
)
|
||||
.Where(s =>
|
||||
s.Contains(
|
||||
"i_am_currently_feeling_slight_displeasure_of_what_you_have_just_sent_lqrem.mp4",
|
||||
StringComparison.Ordinal
|
||||
)
|
||||
)
|
||||
.Should()
|
||||
.ContainSingle();
|
||||
|
@ -205,42 +204,38 @@ public class HtmlEmbedSpecs
|
|||
|
||||
imageUrls
|
||||
.Should()
|
||||
.Contain(
|
||||
u =>
|
||||
u.EndsWith(
|
||||
"https/pbs.twimg.com/media/FVYIzYPWAAAMBqZ.png",
|
||||
StringComparison.Ordinal
|
||||
)
|
||||
.Contain(u =>
|
||||
u.EndsWith(
|
||||
"https/pbs.twimg.com/media/FVYIzYPWAAAMBqZ.png",
|
||||
StringComparison.Ordinal
|
||||
)
|
||||
);
|
||||
|
||||
imageUrls
|
||||
.Should()
|
||||
.Contain(
|
||||
u =>
|
||||
u.EndsWith(
|
||||
"https/pbs.twimg.com/media/FVYJBWJWAAMNAx2.png",
|
||||
StringComparison.Ordinal
|
||||
)
|
||||
.Contain(u =>
|
||||
u.EndsWith(
|
||||
"https/pbs.twimg.com/media/FVYJBWJWAAMNAx2.png",
|
||||
StringComparison.Ordinal
|
||||
)
|
||||
);
|
||||
|
||||
imageUrls
|
||||
.Should()
|
||||
.Contain(
|
||||
u =>
|
||||
u.EndsWith(
|
||||
"https/pbs.twimg.com/media/FVYJHiRX0AANZcz.png",
|
||||
StringComparison.Ordinal
|
||||
)
|
||||
.Contain(u =>
|
||||
u.EndsWith(
|
||||
"https/pbs.twimg.com/media/FVYJHiRX0AANZcz.png",
|
||||
StringComparison.Ordinal
|
||||
)
|
||||
);
|
||||
|
||||
imageUrls
|
||||
.Should()
|
||||
.Contain(
|
||||
u =>
|
||||
u.EndsWith(
|
||||
"https/pbs.twimg.com/media/FVYJNZNXwAAPnVG.png",
|
||||
StringComparison.Ordinal
|
||||
)
|
||||
.Contain(u =>
|
||||
u.EndsWith(
|
||||
"https/pbs.twimg.com/media/FVYJNZNXwAAPnVG.png",
|
||||
StringComparison.Ordinal
|
||||
)
|
||||
);
|
||||
|
||||
message.QuerySelectorAll(".chatlog__embed").Should().ContainSingle();
|
||||
|
|
|
@ -11,9 +11,9 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CliFx" Version="2.3.5" />
|
||||
<PackageReference Include="CSharpier.MsBuild" Version="0.26.7" PrivateAssets="all" />
|
||||
<PackageReference Include="CSharpier.MsBuild" Version="0.28.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Deorcify" Version="1.0.2" PrivateAssets="all" />
|
||||
<PackageReference Include="DotnetRuntimeBootstrapper" Version="2.5.2" PrivateAssets="all" />
|
||||
<PackageReference Include="DotnetRuntimeBootstrapper" Version="2.5.3" PrivateAssets="all" />
|
||||
<PackageReference Include="Gress" Version="2.1.1" />
|
||||
<PackageReference Include="Spectre.Console" Version="0.48.0" />
|
||||
</ItemGroup>
|
||||
|
|
21
DiscordChatExporter.Core/Discord/Data/ChannelNode.cs
Normal file
21
DiscordChatExporter.Core/Discord/Data/ChannelNode.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace DiscordChatExporter.Core.Discord.Data;
|
||||
|
||||
public record ChannelNode(Channel Channel, IReadOnlyList<ChannelNode> Children)
|
||||
{
|
||||
public static IReadOnlyList<ChannelNode> BuildTree(IReadOnlyList<Channel> channels)
|
||||
{
|
||||
IReadOnlyList<ChannelNode> GetChildren(Channel parent) =>
|
||||
channels
|
||||
.Where(c => c.Parent?.Id == parent.Id)
|
||||
.Select(c => new ChannelNode(c, GetChildren(c)))
|
||||
.ToArray();
|
||||
|
||||
return channels
|
||||
.Where(c => c.Parent is null)
|
||||
.Select(c => new ChannelNode(c, GetChildren(c)))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
|
@ -83,16 +83,15 @@ public partial record Message
|
|||
// Find embeds with the same URL that only contain a single image and nothing else
|
||||
var trailingEmbeds = embeds
|
||||
.Skip(i + 1)
|
||||
.TakeWhile(
|
||||
e =>
|
||||
e.Url == embed.Url
|
||||
&& e.Timestamp is null
|
||||
&& e.Author is null
|
||||
&& e.Color is null
|
||||
&& string.IsNullOrWhiteSpace(e.Description)
|
||||
&& !e.Fields.Any()
|
||||
&& e.Images.Count == 1
|
||||
&& e.Footer is null
|
||||
.TakeWhile(e =>
|
||||
e.Url == embed.Url
|
||||
&& e.Timestamp is null
|
||||
&& e.Author is null
|
||||
&& e.Color is null
|
||||
&& string.IsNullOrWhiteSpace(e.Description)
|
||||
&& !e.Fields.Any()
|
||||
&& e.Images.Count == 1
|
||||
&& e.Footer is null
|
||||
)
|
||||
.ToArray();
|
||||
|
||||
|
|
|
@ -66,12 +66,12 @@ public class DiscordClient(string token)
|
|||
if (remainingRequestCount <= 0 && resetAfterDelay is not null)
|
||||
{
|
||||
var delay =
|
||||
// Adding a small buffer to the reset time reduces the chance of getting
|
||||
// rate limited again, because it allows for more requests to be released.
|
||||
(resetAfterDelay.Value + TimeSpan.FromSeconds(1))
|
||||
// Sometimes Discord returns an absurdly high value for the reset time, which
|
||||
// is not actually enforced by the server. So we cap it at a reasonable value.
|
||||
.Clamp(TimeSpan.Zero, TimeSpan.FromSeconds(60));
|
||||
// Adding a small buffer to the reset time reduces the chance of getting
|
||||
// rate limited again, because it allows for more requests to be released.
|
||||
(resetAfterDelay.Value + TimeSpan.FromSeconds(1))
|
||||
// Sometimes Discord returns an absurdly high value for the reset time, which
|
||||
// is not actually enforced by the server. So we cap it at a reasonable value.
|
||||
.Clamp(TimeSpan.Zero, TimeSpan.FromSeconds(60));
|
||||
|
||||
await Task.Delay(delay, innerCancellationToken);
|
||||
}
|
||||
|
@ -152,8 +152,13 @@ public class DiscordClient(string token)
|
|||
_
|
||||
=> throw new DiscordChatExporterException(
|
||||
$"""
|
||||
Request to '{url}' failed: {response.StatusCode.ToString().ToSpaceSeparatedWords().ToLowerInvariant()}.
|
||||
Response content: {await response.Content.ReadAsStringAsync(cancellationToken)}
|
||||
Request to '{url}' failed: {response
|
||||
.StatusCode.ToString()
|
||||
.ToSpaceSeparatedWords()
|
||||
.ToLowerInvariant()}.
|
||||
Response content: {await response.Content.ReadAsStringAsync(
|
||||
cancellationToken
|
||||
)}
|
||||
""",
|
||||
true
|
||||
)
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AsyncKeyedLock" Version="6.3.4" />
|
||||
<PackageReference Include="CSharpier.MsBuild" Version="0.26.7" PrivateAssets="all" />
|
||||
<PackageReference Include="CSharpier.MsBuild" Version="0.28.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Gress" Version="2.1.1" />
|
||||
<PackageReference Include="JsonExtensions" Version="1.2.0" />
|
||||
<PackageReference Include="Polly" Version="8.2.0" />
|
||||
<PackageReference Include="RazorBlade" Version="0.5.0" />
|
||||
<PackageReference Include="Polly" Version="8.3.1" />
|
||||
<PackageReference Include="RazorBlade" Version="0.6.0" />
|
||||
<PackageReference Include="Superpower" Version="3.0.0" />
|
||||
<PackageReference Include="WebMarkupMin.Core" Version="2.14.0" />
|
||||
<PackageReference Include="YoutubeExplode" Version="6.3.10" />
|
||||
<PackageReference Include="WebMarkupMin.Core" Version="2.16.0" />
|
||||
<PackageReference Include="YoutubeExplode" Version="6.3.13" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
</Project>
|
|
@ -58,16 +58,15 @@ internal partial class ExportAssetDownloader(string workingDirPath, bool reuse)
|
|||
{
|
||||
var lastModified = response
|
||||
.Content.Headers.TryGetValue("Last-Modified")
|
||||
?.Pipe(
|
||||
s =>
|
||||
DateTimeOffset.TryParse(
|
||||
s,
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.None,
|
||||
out var instant
|
||||
)
|
||||
? instant
|
||||
: (DateTimeOffset?)null
|
||||
?.Pipe(s =>
|
||||
DateTimeOffset.TryParse(
|
||||
s,
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.None,
|
||||
out var instant
|
||||
)
|
||||
? instant
|
||||
: (DateTimeOffset?)null
|
||||
);
|
||||
|
||||
if (lastModified is not null)
|
||||
|
|
|
@ -93,8 +93,7 @@ internal class ExportContext(DiscordClient discord, ExportRequest request)
|
|||
|
||||
public IReadOnlyList<Role> GetUserRoles(Snowflake id) =>
|
||||
TryGetMember(id)
|
||||
?.RoleIds
|
||||
.Select(TryGetRole)
|
||||
?.RoleIds.Select(TryGetRole)
|
||||
.WhereNotNull()
|
||||
.OrderByDescending(r => r.Position)
|
||||
.ToArray() ?? [];
|
||||
|
|
|
@ -22,12 +22,11 @@ internal class ContainsMessageFilter(string text) : MessageFilter
|
|||
|
||||
public override bool IsMatch(Message message) =>
|
||||
IsMatch(message.Content)
|
||||
|| message.Embeds.Any(
|
||||
e =>
|
||||
IsMatch(e.Title)
|
||||
|| IsMatch(e.Author?.Name)
|
||||
|| IsMatch(e.Description)
|
||||
|| IsMatch(e.Footer?.Text)
|
||||
|| e.Fields.Any(f => IsMatch(f.Name) || IsMatch(f.Value))
|
||||
|| message.Embeds.Any(e =>
|
||||
IsMatch(e.Title)
|
||||
|| IsMatch(e.Author?.Name)
|
||||
|| IsMatch(e.Description)
|
||||
|| IsMatch(e.Footer?.Text)
|
||||
|| e.Fields.Any(f => IsMatch(f.Name) || IsMatch(f.Value))
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,11 +7,10 @@ namespace DiscordChatExporter.Core.Exporting.Filtering;
|
|||
internal class MentionsMessageFilter(string value) : MessageFilter
|
||||
{
|
||||
public override bool IsMatch(Message message) =>
|
||||
message.MentionedUsers.Any(
|
||||
user =>
|
||||
string.Equals(value, user.Name, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(value, user.DisplayName, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(value, user.FullName, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(value, user.Id.ToString(), StringComparison.OrdinalIgnoreCase)
|
||||
message.MentionedUsers.Any(user =>
|
||||
string.Equals(value, user.Name, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(value, user.DisplayName, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(value, user.FullName, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(value, user.Id.ToString(), StringComparison.OrdinalIgnoreCase)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -30,8 +30,8 @@ internal static class FilterGrammar
|
|||
.OneOf(QuotedString, UnquotedString)
|
||||
.Named("text string");
|
||||
|
||||
private static readonly TextParser<MessageFilter> ContainsFilter = String.Select(
|
||||
v => (MessageFilter)new ContainsMessageFilter(v)
|
||||
private static readonly TextParser<MessageFilter> ContainsFilter = String.Select(v =>
|
||||
(MessageFilter)new ContainsMessageFilter(v)
|
||||
);
|
||||
|
||||
private static readonly TextParser<MessageFilter> FromFilter = Span.EqualToIgnoreCase("from:")
|
||||
|
|
|
@ -7,10 +7,9 @@ namespace DiscordChatExporter.Core.Exporting.Filtering;
|
|||
internal class ReactionMessageFilter(string value) : MessageFilter
|
||||
{
|
||||
public override bool IsMatch(Message message) =>
|
||||
message.Reactions.Any(
|
||||
r =>
|
||||
string.Equals(value, r.Emoji.Id?.ToString(), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(value, r.Emoji.Name, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(value, r.Emoji.Code, StringComparison.OrdinalIgnoreCase)
|
||||
message.Reactions.Any(r =>
|
||||
string.Equals(value, r.Emoji.Id?.ToString(), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(value, r.Emoji.Name, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(value, r.Emoji.Code, StringComparison.OrdinalIgnoreCase)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -155,7 +155,9 @@ internal partial class HtmlMarkdownVisitor(
|
|||
buffer.Append(
|
||||
// lang=html
|
||||
$"""
|
||||
<code class="chatlog__markdown-pre chatlog__markdown-pre--inline">{HtmlEncode(inlineCodeBlock.Code)}</code>
|
||||
<code class="chatlog__markdown-pre chatlog__markdown-pre--inline">{HtmlEncode(
|
||||
inlineCodeBlock.Code
|
||||
)}</code>
|
||||
"""
|
||||
);
|
||||
|
||||
|
@ -174,7 +176,9 @@ internal partial class HtmlMarkdownVisitor(
|
|||
buffer.Append(
|
||||
// lang=html
|
||||
$"""
|
||||
<code class="chatlog__markdown-pre chatlog__markdown-pre--multiline {highlightClass}">{HtmlEncode(multiLineCodeBlock.Code)}</code>
|
||||
<code class="chatlog__markdown-pre chatlog__markdown-pre--multiline {highlightClass}">{HtmlEncode(
|
||||
multiLineCodeBlock.Code
|
||||
)}</code>
|
||||
"""
|
||||
);
|
||||
|
||||
|
@ -267,7 +271,9 @@ internal partial class HtmlMarkdownVisitor(
|
|||
buffer.Append(
|
||||
// lang=html
|
||||
$"""
|
||||
<span class="chatlog__markdown-mention" title="{HtmlEncode(fullName)}">@{HtmlEncode(displayName)}</span>
|
||||
<span class="chatlog__markdown-mention" title="{HtmlEncode(fullName)}">@{HtmlEncode(
|
||||
displayName
|
||||
)}</span>
|
||||
"""
|
||||
);
|
||||
}
|
||||
|
@ -292,8 +298,12 @@ internal partial class HtmlMarkdownVisitor(
|
|||
|
||||
var style = color is not null
|
||||
? $"""
|
||||
color: rgb({color.Value.R}, {color.Value.G}, {color.Value.B}); background-color: rgba({color.Value.R}, {color.Value.G}, {color.Value.B}, 0.1);
|
||||
"""
|
||||
color: rgb({color.Value.R}, {color.Value.G}, {color
|
||||
.Value
|
||||
.B}); background-color: rgba({color.Value.R}, {color.Value.G}, {color
|
||||
.Value
|
||||
.B}, 0.1);
|
||||
"""
|
||||
: null;
|
||||
|
||||
buffer.Append(
|
||||
|
@ -321,7 +331,9 @@ internal partial class HtmlMarkdownVisitor(
|
|||
buffer.Append(
|
||||
// lang=html
|
||||
$"""
|
||||
<span class="chatlog__markdown-timestamp" title="{HtmlEncode(formattedLong)}">{HtmlEncode(formatted)}</span>
|
||||
<span class="chatlog__markdown-timestamp" title="{HtmlEncode(
|
||||
formattedLong
|
||||
)}">{HtmlEncode(formatted)}</span>
|
||||
"""
|
||||
);
|
||||
|
||||
|
@ -344,10 +356,8 @@ internal partial class HtmlMarkdownVisitor
|
|||
|
||||
var isJumbo =
|
||||
isJumboAllowed
|
||||
&& nodes.All(
|
||||
n =>
|
||||
n is EmojiNode
|
||||
|| n is TextNode textNode && string.IsNullOrWhiteSpace(textNode.Text)
|
||||
&& nodes.All(n =>
|
||||
n is EmojiNode || n is TextNode textNode && string.IsNullOrWhiteSpace(textNode.Text)
|
||||
);
|
||||
|
||||
var buffer = new StringBuilder();
|
||||
|
|
|
@ -25,11 +25,10 @@ public static class Http
|
|||
private static bool IsRetryableException(Exception exception) =>
|
||||
exception
|
||||
.GetSelfAndChildren()
|
||||
.Any(
|
||||
ex =>
|
||||
ex is TimeoutException or SocketException or AuthenticationException
|
||||
|| ex is HttpRequestException hrex
|
||||
&& IsRetryableStatusCode(hrex.StatusCode ?? HttpStatusCode.OK)
|
||||
.Any(ex =>
|
||||
ex is TimeoutException or SocketException or AuthenticationException
|
||||
|| ex is HttpRequestException hrex
|
||||
&& IsRetryableStatusCode(hrex.StatusCode ?? HttpStatusCode.OK)
|
||||
);
|
||||
|
||||
public static ResiliencePipeline ResiliencePipeline { get; } =
|
||||
|
|
135
DiscordChatExporter.Gui/App.axaml
Normal file
135
DiscordChatExporter.Gui/App.axaml
Normal file
|
@ -0,0 +1,135 @@
|
|||
<Application
|
||||
x:Class="DiscordChatExporter.Gui.App"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:dialogHostAvalonia="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
|
||||
xmlns:framework="clr-namespace:DiscordChatExporter.Gui.Framework"
|
||||
xmlns:materialAssists="clr-namespace:Material.Styles.Assists;assembly=Material.Styles"
|
||||
xmlns:materialControls="clr-namespace:Material.Styles.Controls;assembly=Material.Styles"
|
||||
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
xmlns:materialStyles="clr-namespace:Material.Styles.Themes;assembly=Material.Styles">
|
||||
<Application.DataTemplates>
|
||||
<framework:ViewManager />
|
||||
</Application.DataTemplates>
|
||||
|
||||
<Application.Styles>
|
||||
<materialStyles:MaterialTheme />
|
||||
<materialIcons:MaterialIconStyles />
|
||||
<dialogHostAvalonia:DialogHostStyles />
|
||||
|
||||
<!-- Combo box -->
|
||||
<Style Selector="ComboBox">
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
|
||||
<Style Selector="^ /template/ Panel#PART_RootPanel">
|
||||
<Setter Property="Height" Value="22" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^ /template/ ToggleButton">
|
||||
<Style Selector="^:checked, ^:unchecked">
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="CornerRadius" Value="0" />
|
||||
|
||||
<Style Selector="^ ContentPresenter#contentPresenter">
|
||||
<Setter Property="Margin" Value="12,8" />
|
||||
</Style>
|
||||
</Style>
|
||||
</Style>
|
||||
</Style>
|
||||
|
||||
<!-- Dialog host -->
|
||||
<Style Selector="dialogHostAvalonia|DialogHost">
|
||||
<Setter Property="DialogMargin" Value="0" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="dialogHostAvalonia|DialogOverlayPopupHost">
|
||||
<Setter Property="Margin" Value="48" />
|
||||
</Style>
|
||||
|
||||
<!-- Snack bar host -->
|
||||
<Style Selector="materialControls|SnackbarHost">
|
||||
<Setter Property="SnackbarHorizontalAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
|
||||
<Style Selector="^ /template/ ItemsControl#PART_SnackbarHostItemsContainer materialControls|Card">
|
||||
<Setter Property="Background" Value="{DynamicResource MaterialDarkBackgroundBrush}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource MaterialDarkForegroundBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^ /template/ ItemsControl#PART_SnackbarHostItemsContainer Button">
|
||||
<Setter Property="Foreground" Value="{DynamicResource MaterialSecondaryMidBrush}" />
|
||||
</Style>
|
||||
</Style>
|
||||
|
||||
<!-- Progress bar -->
|
||||
<Style Selector="ProgressBar">
|
||||
<Setter Property="Minimum" Value="0" />
|
||||
<Setter Property="Maximum" Value="1" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource MaterialSecondaryMidBrush}" />
|
||||
<Setter Property="materialAssists:TransitionAssist.DisableTransitions" Value="True" />
|
||||
|
||||
<Style Selector="^:horizontal">
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
</Style>
|
||||
</Style>
|
||||
|
||||
<!-- Slider -->
|
||||
<Style Selector="Slider">
|
||||
<Style Selector="^ /template/ ProgressBar#PART_ProgressLayer">
|
||||
<Style Selector="^:horizontal">
|
||||
<Style Selector="^ Panel#PART_InnerPanel">
|
||||
<Setter Property="Height" Value="2" />
|
||||
|
||||
<Style Selector="^ Border#PART_InactiveState">
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="Height" Value="2" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^ Border#PART_Indicator">
|
||||
<Setter Property="Margin" Value="0" />
|
||||
</Style>
|
||||
</Style>
|
||||
</Style>
|
||||
</Style>
|
||||
|
||||
<Style Selector="^ /template/ Track#PART_Track">
|
||||
<Style Selector="^:horizontal">
|
||||
<Setter Property="Margin" Value="4,0" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^ Border#PART_HoverEffect">
|
||||
<Setter Property="Width" Value="24" />
|
||||
<Setter Property="Height" Value="24" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^ Border#PART_ThumbGrip">
|
||||
<Setter Property="Width" Value="12" />
|
||||
<Setter Property="Height" Value="12" />
|
||||
</Style>
|
||||
</Style>
|
||||
</Style>
|
||||
|
||||
<!-- Text box -->
|
||||
<Style Selector="TextBox">
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
</Style>
|
||||
|
||||
<!-- Toggle button -->
|
||||
<Style Selector="ToggleButton">
|
||||
<Setter Property="TextElement.FontWeight" Value="Medium" />
|
||||
</Style>
|
||||
|
||||
<!-- Toggle switch -->
|
||||
<Style Selector="ToggleSwitch">
|
||||
<Setter Property="materialAssists:ToggleSwitchAssist.SwitchThumbOffBackground" Value="{DynamicResource MaterialBackgroundBrush}" />
|
||||
</Style>
|
||||
|
||||
<!-- Tooltip -->
|
||||
<Style Selector="ToolTip">
|
||||
<Setter Property="TextElement.FontSize" Value="14" />
|
||||
<Setter Property="TextElement.FontWeight" Value="Normal" />
|
||||
<Setter Property="TextElement.FontStyle" Value="Normal" />
|
||||
<Setter Property="TextElement.FontStretch" Value="Normal" />
|
||||
</Style>
|
||||
</Application.Styles>
|
||||
</Application>
|
110
DiscordChatExporter.Gui/App.axaml.cs
Normal file
110
DiscordChatExporter.Gui/App.axaml.cs
Normal file
|
@ -0,0 +1,110 @@
|
|||
using System;
|
||||
using System.Net;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Platform;
|
||||
using DiscordChatExporter.Gui.Framework;
|
||||
using DiscordChatExporter.Gui.Services;
|
||||
using DiscordChatExporter.Gui.ViewModels;
|
||||
using DiscordChatExporter.Gui.ViewModels.Components;
|
||||
using DiscordChatExporter.Gui.ViewModels.Dialogs;
|
||||
using DiscordChatExporter.Gui.Views;
|
||||
using Material.Styles.Themes;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace DiscordChatExporter.Gui;
|
||||
|
||||
public partial class App : Application, IDisposable
|
||||
{
|
||||
private readonly ServiceProvider _services;
|
||||
private readonly MainViewModel _mainViewModel;
|
||||
|
||||
public App()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// Framework
|
||||
services.AddSingleton<DialogManager>();
|
||||
services.AddSingleton<SnackbarManager>();
|
||||
services.AddSingleton<ViewManager>();
|
||||
services.AddSingleton<ViewModelManager>();
|
||||
|
||||
// Services
|
||||
services.AddSingleton<SettingsService>();
|
||||
services.AddSingleton<UpdateService>();
|
||||
|
||||
// View models
|
||||
services.AddTransient<MainViewModel>();
|
||||
services.AddTransient<DashboardViewModel>();
|
||||
services.AddTransient<ExportSetupViewModel>();
|
||||
services.AddTransient<MessageBoxViewModel>();
|
||||
services.AddTransient<SettingsViewModel>();
|
||||
|
||||
_services = services.BuildServiceProvider(true);
|
||||
_mainViewModel = _services.GetRequiredService<ViewModelManager>().CreateMainViewModel();
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
// Increase maximum concurrent connections
|
||||
ServicePointManager.DefaultConnectionLimit = 20;
|
||||
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
desktop.MainWindow = new MainView { DataContext = _mainViewModel };
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
|
||||
// Set custom theme colors
|
||||
SetDefaultTheme();
|
||||
}
|
||||
|
||||
public void Dispose() => _services.Dispose();
|
||||
}
|
||||
|
||||
public partial class App
|
||||
{
|
||||
public static void SetLightTheme()
|
||||
{
|
||||
if (Current is null)
|
||||
return;
|
||||
|
||||
Current.LocateMaterialTheme<MaterialThemeBase>().CurrentTheme = Theme.Create(
|
||||
Theme.Light,
|
||||
Color.Parse("#343838"),
|
||||
Color.Parse("#F9A825")
|
||||
);
|
||||
}
|
||||
|
||||
public static void SetDarkTheme()
|
||||
{
|
||||
if (Current is null)
|
||||
return;
|
||||
|
||||
Current.LocateMaterialTheme<MaterialThemeBase>().CurrentTheme = Theme.Create(
|
||||
Theme.Dark,
|
||||
Color.Parse("#E8E8E8"),
|
||||
Color.Parse("#F9A825")
|
||||
);
|
||||
}
|
||||
|
||||
public static void SetDefaultTheme()
|
||||
{
|
||||
if (Current is null)
|
||||
return;
|
||||
|
||||
var isDarkModeEnabledByDefault =
|
||||
Current.PlatformSettings?.GetColorValues().ThemeVariant == PlatformThemeVariant.Dark;
|
||||
|
||||
if (isDarkModeEnabledByDefault)
|
||||
SetDarkTheme();
|
||||
else
|
||||
SetLightTheme();
|
||||
}
|
||||
}
|
|
@ -1,543 +0,0 @@
|
|||
<Application
|
||||
x:Class="DiscordChatExporter.Gui.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:DiscordChatExporter.Gui"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:s="https://github.com/canton7/Stylet">
|
||||
<Application.Resources>
|
||||
<s:ApplicationLoader>
|
||||
<!-- Bootstrapper -->
|
||||
<s:ApplicationLoader.Bootstrapper>
|
||||
<local:Bootstrapper />
|
||||
</s:ApplicationLoader.Bootstrapper>
|
||||
|
||||
<!-- Merged dictionaries -->
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<!-- Colors are irrelevant because they are overriden in runtime -->
|
||||
<materialDesign:BundledTheme
|
||||
BaseTheme="Light"
|
||||
PrimaryColor="Blue"
|
||||
SecondaryColor="Blue" />
|
||||
|
||||
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Defaults.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<!-- Styles -->
|
||||
<Style x:Key="MaterialDesignRoot" TargetType="{x:Type Control}">
|
||||
<Setter Property="FontFamily" Value="{DynamicResource MaterialDesignFont}" />
|
||||
<Setter Property="RenderOptions.BitmapScalingMode" Value="HighQuality" />
|
||||
<Setter Property="SnapsToDevicePixels" Value="True" />
|
||||
<Setter Property="TextElement.FontSize" Value="13" />
|
||||
<Setter Property="TextElement.FontWeight" Value="Regular" />
|
||||
<Setter Property="TextElement.Foreground" Value="{DynamicResource MaterialDesignBody}" />
|
||||
<Setter Property="TextOptions.TextFormattingMode" Value="Ideal" />
|
||||
<Setter Property="TextOptions.TextRenderingMode" Value="Auto" />
|
||||
<Setter Property="UseLayoutRounding" Value="True" />
|
||||
</Style>
|
||||
|
||||
<Style BasedOn="{StaticResource MaterialDesignScrollBarMinimal}" TargetType="{x:Type ScrollBar}" />
|
||||
|
||||
<Style BasedOn="{StaticResource MaterialDesignLinearProgressBar}" TargetType="{x:Type ProgressBar}">
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource SecondaryHueMidBrush}" />
|
||||
<Setter Property="Height" Value="2" />
|
||||
<Setter Property="Maximum" Value="1" />
|
||||
<Setter Property="Minimum" Value="0" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="{x:Type Hyperlink}">
|
||||
<Setter Property="TextDecorations" Value="Underline" />
|
||||
<Setter Property="FontWeight" Value="Regular" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource MaterialDesignBody}" />
|
||||
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Foreground" Value="{DynamicResource MaterialDesignCheckBoxDisabled}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="True">
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
</Trigger>
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="IsEnabled" Value="True" />
|
||||
<Condition Property="IsMouseOver" Value="True" />
|
||||
</MultiTrigger.Conditions>
|
||||
|
||||
<Setter Property="Foreground" Value="{DynamicResource SecondaryHueMidBrush}" />
|
||||
</MultiTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<Style BasedOn="{StaticResource MaterialDesignTextBox}" TargetType="{x:Type TextBox}" />
|
||||
|
||||
<Style BasedOn="{StaticResource MaterialDesignComboBox}" TargetType="{x:Type ComboBox}" />
|
||||
|
||||
<Style BasedOn="{StaticResource MaterialDesignDatePicker}" TargetType="{x:Type DatePicker}" />
|
||||
|
||||
<Style BasedOn="{StaticResource MaterialDesignTimePicker}" TargetType="{x:Type materialDesign:TimePicker}" />
|
||||
|
||||
<Style TargetType="{x:Type materialDesign:Snackbar}">
|
||||
<Setter Property="Background" Value="{DynamicResource MaterialDesignDarkBackground}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource MaterialDesignDarkForeground}" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="ActionButtonStyle">
|
||||
<Setter.Value>
|
||||
<Style BasedOn="{StaticResource MaterialDesignFlatButton}" TargetType="{x:Type Button}">
|
||||
<Setter Property="VerticalAlignment" Value="Stretch" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Height" Value="36" />
|
||||
<Setter Property="Margin" Value="8,-10,-8,-10" />
|
||||
<Setter Property="Padding" Value="8" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource SecondaryHueMidBrush}" />
|
||||
<Setter Property="materialDesign:RippleAssist.Feedback" Value="{DynamicResource MaterialDesignSnackbarRipple}" />
|
||||
</Style>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="MaterialDesignFlatActionToggleButton"
|
||||
BasedOn="{StaticResource MaterialDesignActionToggleButton}"
|
||||
TargetType="{x:Type ToggleButton}">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource PrimaryHueMidBrush}" />
|
||||
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource MaterialDesignFlatButtonClick}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource MaterialDesignFlatButtonClick}" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<Style BasedOn="{StaticResource MaterialDesignContextMenu}" TargetType="{x:Type ContextMenu}">
|
||||
<Setter Property="Background" Value="{DynamicResource MaterialDesignPaper}" />
|
||||
</Style>
|
||||
|
||||
<!-- Default MD Expander is incredibly slow (https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit/issues/1307) -->
|
||||
<Style BasedOn="{StaticResource MaterialDesignExpander}" TargetType="{x:Type Expander}">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Expander">
|
||||
<Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
|
||||
<DockPanel Background="{TemplateBinding Background}">
|
||||
<ToggleButton
|
||||
Name="HeaderSite"
|
||||
BorderThickness="0"
|
||||
Content="{TemplateBinding Header}"
|
||||
ContentStringFormat="{TemplateBinding HeaderStringFormat}"
|
||||
ContentTemplate="{TemplateBinding HeaderTemplate}"
|
||||
ContentTemplateSelector="{TemplateBinding HeaderTemplateSelector}"
|
||||
Cursor="Hand"
|
||||
DockPanel.Dock="Top"
|
||||
Focusable="False"
|
||||
Foreground="{TemplateBinding Foreground}"
|
||||
IsChecked="{Binding Path=IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
IsTabStop="False"
|
||||
Opacity=".87"
|
||||
Style="{StaticResource MaterialDesignExpanderToggleButton}"
|
||||
TextElement.FontSize="15" />
|
||||
<Border
|
||||
Name="ContentSite"
|
||||
DockPanel.Dock="Bottom"
|
||||
Visibility="Collapsed">
|
||||
<StackPanel
|
||||
Name="ContentPanel"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
|
||||
<ContentPresenter
|
||||
Name="PART_Content"
|
||||
ContentStringFormat="{TemplateBinding ContentStringFormat}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||
ContentTemplateSelector="{TemplateBinding ContentTemplateSelector}"
|
||||
Focusable="False" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsExpanded" Value="true">
|
||||
<Setter TargetName="ContentSite" Property="Visibility" Value="Visible" />
|
||||
</Trigger>
|
||||
<Trigger Property="ExpandDirection" Value="Right">
|
||||
<Setter TargetName="HeaderSite" Property="DockPanel.Dock" Value="Left" />
|
||||
<Setter TargetName="ContentSite" Property="DockPanel.Dock" Value="Right" />
|
||||
<Setter TargetName="ContentPanel" Property="Orientation" Value="Horizontal" />
|
||||
<Setter TargetName="ContentPanel" Property="Height" Value="Auto" />
|
||||
<Setter TargetName="HeaderSite" Property="Style" Value="{StaticResource MaterialDesignVerticalHeaderStyle}" />
|
||||
</Trigger>
|
||||
|
||||
<Trigger Property="ExpandDirection" Value="Left">
|
||||
<Setter TargetName="HeaderSite" Property="DockPanel.Dock" Value="Right" />
|
||||
<Setter TargetName="ContentSite" Property="DockPanel.Dock" Value="Left" />
|
||||
<Setter TargetName="ContentPanel" Property="Orientation" Value="Horizontal" />
|
||||
<Setter TargetName="ContentPanel" Property="Height" Value="Auto" />
|
||||
<Setter TargetName="HeaderSite" Property="Style" Value="{StaticResource MaterialDesignVerticalHeaderStyle}" />
|
||||
</Trigger>
|
||||
|
||||
<Trigger Property="ExpandDirection" Value="Up">
|
||||
<Setter TargetName="HeaderSite" Property="DockPanel.Dock" Value="Bottom" />
|
||||
<Setter TargetName="ContentSite" Property="DockPanel.Dock" Value="Top" />
|
||||
<Setter TargetName="ContentPanel" Property="Orientation" Value="Vertical" />
|
||||
<Setter TargetName="ContentPanel" Property="Width" Value="Auto" />
|
||||
<Setter TargetName="HeaderSite" Property="Style" Value="{StaticResource MaterialDesignHorizontalHeaderStyle}" />
|
||||
</Trigger>
|
||||
|
||||
<Trigger Property="ExpandDirection" Value="Down">
|
||||
<Setter TargetName="HeaderSite" Property="DockPanel.Dock" Value="Top" />
|
||||
<Setter TargetName="ContentSite" Property="DockPanel.Dock" Value="Bottom" />
|
||||
<Setter TargetName="ContentPanel" Property="Orientation" Value="Vertical" />
|
||||
<Setter TargetName="ContentPanel" Property="Width" Value="Auto" />
|
||||
<Setter TargetName="HeaderSite" Property="Style" Value="{StaticResource MaterialDesignHorizontalHeaderStyle}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Use old MD style for slider, because the new one is too huge -->
|
||||
<Style x:Key="MaterialDesignHorizontalTrackRepeatButton" TargetType="{x:Type RepeatButton}">
|
||||
<Setter Property="OverridesDefaultStyle" Value="true" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Focusable" Value="false" />
|
||||
<Setter Property="IsTabStop" Value="false" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type RepeatButton}">
|
||||
<Canvas
|
||||
Width="{TemplateBinding Width}"
|
||||
Height="{TemplateBinding Height}"
|
||||
Background="Transparent">
|
||||
<Rectangle
|
||||
x:Name="PART_SelectionRange"
|
||||
Canvas.Top="8"
|
||||
Width="{TemplateBinding ActualWidth}"
|
||||
Height="2.0"
|
||||
Fill="{TemplateBinding Background}" />
|
||||
</Canvas>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="MaterialDesignVerticalTrackRepeatButton" TargetType="{x:Type RepeatButton}">
|
||||
<Setter Property="OverridesDefaultStyle" Value="true" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Focusable" Value="false" />
|
||||
<Setter Property="IsTabStop" Value="false" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type RepeatButton}">
|
||||
<Canvas
|
||||
Width="{TemplateBinding Width}"
|
||||
Height="{TemplateBinding Height}"
|
||||
Background="Transparent">
|
||||
<Rectangle
|
||||
x:Name="PART_SelectionRange"
|
||||
Canvas.Left="8"
|
||||
Width="2.0"
|
||||
Height="{TemplateBinding ActualHeight}"
|
||||
Fill="{TemplateBinding Background}" />
|
||||
</Canvas>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<ControlTemplate x:Key="MaterialDesignSliderThumb" TargetType="{x:Type Thumb}">
|
||||
<Grid
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
UseLayoutRounding="True">
|
||||
<Ellipse
|
||||
x:Name="shadow"
|
||||
Width="24"
|
||||
Height="24"
|
||||
Margin="-12"
|
||||
Fill="{TemplateBinding Foreground}"
|
||||
Opacity=".0"
|
||||
UseLayoutRounding="True" />
|
||||
<Ellipse
|
||||
x:Name="grip"
|
||||
Width="12"
|
||||
Height="12"
|
||||
Fill="{TemplateBinding Foreground}"
|
||||
RenderTransformOrigin=".5,.5"
|
||||
UseLayoutRounding="True">
|
||||
<Ellipse.RenderTransform>
|
||||
<ScaleTransform ScaleX="1" ScaleY="1" />
|
||||
</Ellipse.RenderTransform>
|
||||
</Ellipse>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="true">
|
||||
<Trigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="shadow"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
To=".26"
|
||||
Duration="0:0:0.2" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</Trigger.EnterActions>
|
||||
<Trigger.ExitActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="shadow"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
To=".0"
|
||||
Duration="0:0:0.2" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</Trigger.ExitActions>
|
||||
</Trigger>
|
||||
<Trigger Property="IsDragging" Value="true">
|
||||
<Trigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="grip" Storyboard.TargetProperty="RenderTransform.ScaleX">
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0.1" Value="1.5">
|
||||
<EasingDoubleKeyFrame.EasingFunction>
|
||||
<SineEase EasingMode="EaseInOut" />
|
||||
</EasingDoubleKeyFrame.EasingFunction>
|
||||
</EasingDoubleKeyFrame>
|
||||
</DoubleAnimationUsingKeyFrames>
|
||||
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="grip" Storyboard.TargetProperty="RenderTransform.ScaleY">
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0.1" Value="1.5">
|
||||
<EasingDoubleKeyFrame.EasingFunction>
|
||||
<SineEase EasingMode="EaseInOut" />
|
||||
</EasingDoubleKeyFrame.EasingFunction>
|
||||
</EasingDoubleKeyFrame>
|
||||
</DoubleAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</Trigger.EnterActions>
|
||||
<Trigger.ExitActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="grip" Storyboard.TargetProperty="RenderTransform.ScaleX">
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1.5" />
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0.1" Value="1">
|
||||
<EasingDoubleKeyFrame.EasingFunction>
|
||||
<SineEase EasingMode="EaseInOut" />
|
||||
</EasingDoubleKeyFrame.EasingFunction>
|
||||
</EasingDoubleKeyFrame>
|
||||
</DoubleAnimationUsingKeyFrames>
|
||||
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="grip" Storyboard.TargetProperty="RenderTransform.ScaleY">
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1.5" />
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0.1" Value="1">
|
||||
<EasingDoubleKeyFrame.EasingFunction>
|
||||
<SineEase EasingMode="EaseInOut" />
|
||||
</EasingDoubleKeyFrame.EasingFunction>
|
||||
</EasingDoubleKeyFrame>
|
||||
</DoubleAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</Trigger.ExitActions>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="false">
|
||||
<Setter TargetName="grip" Property="Fill" Value="{DynamicResource MaterialDesignCheckBoxDisabled}" />
|
||||
<Setter TargetName="grip" Property="Stroke" Value="{DynamicResource MaterialDesignCheckBoxDisabled}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
|
||||
<ControlTemplate x:Key="MaterialDesignSliderHorizontal" TargetType="{x:Type Slider}">
|
||||
<Border
|
||||
x:Name="border"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
SnapsToDevicePixels="True">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" MinHeight="{TemplateBinding MinHeight}" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TickBar
|
||||
x:Name="TopTick"
|
||||
Grid.Row="0"
|
||||
Height="4"
|
||||
Margin="0,0,0,2"
|
||||
Fill="{TemplateBinding Foreground}"
|
||||
Placement="Top"
|
||||
Visibility="Collapsed" />
|
||||
<TickBar
|
||||
x:Name="BottomTick"
|
||||
Grid.Row="2"
|
||||
Height="4"
|
||||
Margin="0,2,0,0"
|
||||
Fill="{TemplateBinding Foreground}"
|
||||
Placement="Bottom"
|
||||
Visibility="Collapsed" />
|
||||
<Rectangle
|
||||
x:Name="PART_SelectionRange"
|
||||
Grid.Row="1"
|
||||
Height="4.0"
|
||||
Fill="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"
|
||||
Visibility="Hidden" />
|
||||
<Track
|
||||
x:Name="PART_Track"
|
||||
Grid.Row="1"
|
||||
OpacityMask="{x:Null}">
|
||||
<Track.DecreaseRepeatButton>
|
||||
<RepeatButton
|
||||
Background="{TemplateBinding Foreground}"
|
||||
Command="{x:Static Slider.DecreaseLarge}"
|
||||
Style="{StaticResource MaterialDesignHorizontalTrackRepeatButton}" />
|
||||
</Track.DecreaseRepeatButton>
|
||||
<Track.IncreaseRepeatButton>
|
||||
<RepeatButton
|
||||
x:Name="IncreaseRepeatButton"
|
||||
Background="{DynamicResource MaterialDesignCheckBoxOff}"
|
||||
Command="{x:Static Slider.IncreaseLarge}"
|
||||
Style="{StaticResource MaterialDesignHorizontalTrackRepeatButton}" />
|
||||
</Track.IncreaseRepeatButton>
|
||||
<Track.Thumb>
|
||||
<Thumb
|
||||
x:Name="Thumb"
|
||||
Width="12"
|
||||
Height="18"
|
||||
VerticalAlignment="Center"
|
||||
Focusable="False"
|
||||
OverridesDefaultStyle="True"
|
||||
Template="{StaticResource MaterialDesignSliderThumb}" />
|
||||
</Track.Thumb>
|
||||
</Track>
|
||||
</Grid>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="TickPlacement" Value="TopLeft">
|
||||
<Setter TargetName="TopTick" Property="Visibility" Value="Visible" />
|
||||
</Trigger>
|
||||
<Trigger Property="TickPlacement" Value="BottomRight">
|
||||
<Setter TargetName="BottomTick" Property="Visibility" Value="Visible" />
|
||||
</Trigger>
|
||||
<Trigger Property="TickPlacement" Value="Both">
|
||||
<Setter TargetName="TopTick" Property="Visibility" Value="Visible" />
|
||||
<Setter TargetName="BottomTick" Property="Visibility" Value="Visible" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelectionRangeEnabled" Value="true">
|
||||
<Setter TargetName="PART_SelectionRange" Property="Visibility" Value="Visible" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="IncreaseRepeatButton" Property="Background" Value="{DynamicResource MaterialDesignCheckBoxDisabled}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
|
||||
<ControlTemplate x:Key="MaterialDesignSliderVertical" TargetType="{x:Type Slider}">
|
||||
<Border
|
||||
x:Name="border"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
SnapsToDevicePixels="True">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" MinWidth="{TemplateBinding MinWidth}" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TickBar
|
||||
x:Name="TopTick"
|
||||
Grid.Column="0"
|
||||
Width="4"
|
||||
Margin="0,0,2,0"
|
||||
Fill="{TemplateBinding Foreground}"
|
||||
Placement="Left"
|
||||
Visibility="Collapsed" />
|
||||
<TickBar
|
||||
x:Name="BottomTick"
|
||||
Grid.Column="2"
|
||||
Width="4"
|
||||
Margin="2,0,0,0"
|
||||
Fill="{TemplateBinding Foreground}"
|
||||
Placement="Right"
|
||||
Visibility="Collapsed" />
|
||||
<Rectangle
|
||||
x:Name="PART_SelectionRange"
|
||||
Grid.Column="1"
|
||||
Height="4.0"
|
||||
Fill="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"
|
||||
Visibility="Hidden" />
|
||||
<Track x:Name="PART_Track" Grid.Column="1">
|
||||
<Track.DecreaseRepeatButton>
|
||||
<RepeatButton
|
||||
Background="{TemplateBinding Foreground}"
|
||||
Command="{x:Static Slider.DecreaseLarge}"
|
||||
Style="{StaticResource MaterialDesignVerticalTrackRepeatButton}" />
|
||||
</Track.DecreaseRepeatButton>
|
||||
<Track.IncreaseRepeatButton>
|
||||
<RepeatButton
|
||||
x:Name="IncreaseRepeatButton"
|
||||
Background="{DynamicResource MaterialDesignCheckBoxOff}"
|
||||
Command="{x:Static Slider.IncreaseLarge}"
|
||||
Style="{StaticResource MaterialDesignVerticalTrackRepeatButton}" />
|
||||
</Track.IncreaseRepeatButton>
|
||||
<Track.Thumb>
|
||||
<Thumb
|
||||
x:Name="Thumb"
|
||||
Width="18"
|
||||
Height="12"
|
||||
VerticalAlignment="Top"
|
||||
Focusable="False"
|
||||
OverridesDefaultStyle="True"
|
||||
Template="{StaticResource MaterialDesignSliderThumb}" />
|
||||
</Track.Thumb>
|
||||
</Track>
|
||||
</Grid>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="TickPlacement" Value="TopLeft">
|
||||
<Setter TargetName="TopTick" Property="Visibility" Value="Visible" />
|
||||
</Trigger>
|
||||
<Trigger Property="TickPlacement" Value="BottomRight">
|
||||
<Setter TargetName="BottomTick" Property="Visibility" Value="Visible" />
|
||||
</Trigger>
|
||||
<Trigger Property="TickPlacement" Value="Both">
|
||||
<Setter TargetName="TopTick" Property="Visibility" Value="Visible" />
|
||||
<Setter TargetName="BottomTick" Property="Visibility" Value="Visible" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelectionRangeEnabled" Value="true">
|
||||
<Setter TargetName="PART_SelectionRange" Property="Visibility" Value="Visible" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="IncreaseRepeatButton" Property="Background" Value="{DynamicResource MaterialDesignCheckBoxDisabled}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
|
||||
<Style x:Key="MaterialDesignThinSlider" TargetType="{x:Type Slider}">
|
||||
<Setter Property="Stylus.IsPressAndHoldEnabled" Value="false" />
|
||||
<Setter Property="Background" Value="{x:Null}" />
|
||||
<Setter Property="BorderBrush" Value="Transparent" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource PrimaryHueMidBrush}" />
|
||||
<Setter Property="Template" Value="{StaticResource MaterialDesignSliderHorizontal}" />
|
||||
<Style.Triggers>
|
||||
<Trigger Property="Orientation" Value="Vertical">
|
||||
<Setter Property="Template" Value="{StaticResource MaterialDesignSliderVertical}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Foreground" Value="{DynamicResource MaterialDesignCheckBoxDisabled}" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</s:ApplicationLoader>
|
||||
</Application.Resources>
|
||||
</Application>
|
|
@ -1,52 +0,0 @@
|
|||
using System;
|
||||
using System.Reflection;
|
||||
using DiscordChatExporter.Gui.Utils;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
|
||||
namespace DiscordChatExporter.Gui;
|
||||
|
||||
public partial class App
|
||||
{
|
||||
private static Assembly Assembly { get; } = typeof(App).Assembly;
|
||||
|
||||
public static string Name { get; } = Assembly.GetName().Name!;
|
||||
|
||||
public static Version Version { get; } = Assembly.GetName().Version!;
|
||||
|
||||
public static string VersionString { get; } = Version.ToString(3);
|
||||
|
||||
public static string ProjectUrl { get; } = "https://github.com/Tyrrrz/DiscordChatExporter";
|
||||
|
||||
public static string LatestReleaseUrl { get; } = ProjectUrl + "/releases/latest";
|
||||
|
||||
public static string DocumentationUrl { get; } = ProjectUrl + "/tree/master/.docs";
|
||||
}
|
||||
|
||||
public partial class App
|
||||
{
|
||||
private static Theme LightTheme { get; } =
|
||||
Theme.Create(
|
||||
new MaterialDesignLightTheme(),
|
||||
MediaColor.FromHex("#343838"),
|
||||
MediaColor.FromHex("#F9A825")
|
||||
);
|
||||
|
||||
private static Theme DarkTheme { get; } =
|
||||
Theme.Create(
|
||||
new MaterialDesignDarkTheme(),
|
||||
MediaColor.FromHex("#E8E8E8"),
|
||||
MediaColor.FromHex("#F9A825")
|
||||
);
|
||||
|
||||
public static void SetLightTheme()
|
||||
{
|
||||
var paletteHelper = new PaletteHelper();
|
||||
paletteHelper.SetTheme(LightTheme);
|
||||
}
|
||||
|
||||
public static void SetDarkTheme()
|
||||
{
|
||||
var paletteHelper = new PaletteHelper();
|
||||
paletteHelper.SetTheme(DarkTheme);
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
using DiscordChatExporter.Core.Discord.Data;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Behaviors;
|
||||
|
||||
public class ChannelMultiSelectionListBoxBehavior : MultiSelectionListBoxBehavior<Channel>;
|
|
@ -1,104 +0,0 @@
|
|||
using System.Collections;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using Microsoft.Xaml.Behaviors;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Behaviors;
|
||||
|
||||
public class MultiSelectionListBoxBehavior<T> : Behavior<ListBox>
|
||||
{
|
||||
public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(
|
||||
nameof(SelectedItems),
|
||||
typeof(IList),
|
||||
typeof(MultiSelectionListBoxBehavior<T>),
|
||||
new FrameworkPropertyMetadata(
|
||||
null,
|
||||
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
|
||||
OnSelectedItemsChanged
|
||||
)
|
||||
);
|
||||
|
||||
private static void OnSelectedItemsChanged(
|
||||
DependencyObject sender,
|
||||
DependencyPropertyChangedEventArgs args
|
||||
)
|
||||
{
|
||||
var behavior = (MultiSelectionListBoxBehavior<T>)sender;
|
||||
if (behavior._modelHandled)
|
||||
return;
|
||||
|
||||
if (behavior.AssociatedObject is null)
|
||||
return;
|
||||
|
||||
behavior._modelHandled = true;
|
||||
behavior.SelectItems();
|
||||
behavior._modelHandled = false;
|
||||
}
|
||||
|
||||
private bool _viewHandled;
|
||||
private bool _modelHandled;
|
||||
|
||||
public IList? SelectedItems
|
||||
{
|
||||
get => (IList?)GetValue(SelectedItemsProperty);
|
||||
set => SetValue(SelectedItemsProperty, value);
|
||||
}
|
||||
|
||||
// Propagate selected items from the model to the view
|
||||
private void SelectItems()
|
||||
{
|
||||
_viewHandled = true;
|
||||
|
||||
AssociatedObject.SelectedItems.Clear();
|
||||
if (SelectedItems is not null)
|
||||
{
|
||||
foreach (var item in SelectedItems)
|
||||
AssociatedObject.SelectedItems.Add(item);
|
||||
}
|
||||
|
||||
_viewHandled = false;
|
||||
}
|
||||
|
||||
// Propagate selected items from the view to the model
|
||||
private void OnListBoxSelectionChanged(object? sender, SelectionChangedEventArgs args)
|
||||
{
|
||||
if (_viewHandled)
|
||||
return;
|
||||
if (AssociatedObject.Items.SourceCollection is null)
|
||||
return;
|
||||
|
||||
SelectedItems = AssociatedObject.SelectedItems.Cast<T>().ToArray();
|
||||
}
|
||||
|
||||
private void OnListBoxItemsChanged(object? sender, NotifyCollectionChangedEventArgs args)
|
||||
{
|
||||
if (_viewHandled)
|
||||
return;
|
||||
if (AssociatedObject.Items.SourceCollection is null)
|
||||
return;
|
||||
SelectItems();
|
||||
}
|
||||
|
||||
protected override void OnAttached()
|
||||
{
|
||||
base.OnAttached();
|
||||
|
||||
AssociatedObject.SelectionChanged += OnListBoxSelectionChanged;
|
||||
((INotifyCollectionChanged)AssociatedObject.Items).CollectionChanged +=
|
||||
OnListBoxItemsChanged;
|
||||
}
|
||||
|
||||
protected override void OnDetaching()
|
||||
{
|
||||
base.OnDetaching();
|
||||
|
||||
if (AssociatedObject is not null)
|
||||
{
|
||||
AssociatedObject.SelectionChanged -= OnListBoxSelectionChanged;
|
||||
((INotifyCollectionChanged)AssociatedObject.Items).CollectionChanged -=
|
||||
OnListBoxItemsChanged;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
using DiscordChatExporter.Gui.Services;
|
||||
using DiscordChatExporter.Gui.ViewModels;
|
||||
using DiscordChatExporter.Gui.ViewModels.Framework;
|
||||
using Stylet;
|
||||
using StyletIoC;
|
||||
#if !DEBUG
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
#endif
|
||||
|
||||
namespace DiscordChatExporter.Gui;
|
||||
|
||||
public class Bootstrapper : Bootstrapper<RootViewModel>
|
||||
{
|
||||
protected override void OnStart()
|
||||
{
|
||||
base.OnStart();
|
||||
|
||||
// Set the default theme.
|
||||
// Preferred theme will be set later, once the settings are loaded.
|
||||
App.SetLightTheme();
|
||||
}
|
||||
|
||||
protected override void ConfigureIoC(IStyletIoCBuilder builder)
|
||||
{
|
||||
base.ConfigureIoC(builder);
|
||||
|
||||
builder.Bind<SettingsService>().ToSelf().InSingletonScope();
|
||||
builder.Bind<IViewModelFactory>().ToAbstractFactory();
|
||||
}
|
||||
|
||||
#if !DEBUG
|
||||
protected override void OnUnhandledException(DispatcherUnhandledExceptionEventArgs args)
|
||||
{
|
||||
base.OnUnhandledException(args);
|
||||
|
||||
MessageBox.Show(
|
||||
args.Exception.ToString(),
|
||||
"Error occured",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Error
|
||||
);
|
||||
}
|
||||
#endif
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using DiscordChatExporter.Core.Discord.Data;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Converters;
|
||||
|
||||
[ValueConversion(typeof(Channel), typeof(string))]
|
||||
public class ChannelToGroupKeyConverter : IValueConverter
|
||||
{
|
||||
public static ChannelToGroupKeyConverter Instance { get; } = new();
|
||||
|
||||
public object? Convert(
|
||||
object? value,
|
||||
Type targetType,
|
||||
object? parameter,
|
||||
CultureInfo culture
|
||||
) =>
|
||||
value switch
|
||||
{
|
||||
Channel { IsThread: true, Parent: not null } channel
|
||||
=> $"Threads in #{channel.Parent.Name}",
|
||||
|
||||
Channel channel => channel.Parent?.Name ?? "???",
|
||||
|
||||
_ => null
|
||||
};
|
||||
|
||||
public object ConvertBack(
|
||||
object? value,
|
||||
Type targetType,
|
||||
object? parameter,
|
||||
CultureInfo culture
|
||||
) => throw new NotSupportedException();
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using Avalonia.Data.Converters;
|
||||
using DiscordChatExporter.Core.Discord.Data;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Converters;
|
||||
|
||||
public class ChannelToHierarchicalNameStringConverter : IValueConverter
|
||||
{
|
||||
public static ChannelToHierarchicalNameStringConverter Instance { get; } = new();
|
||||
|
||||
public object? Convert(
|
||||
object? value,
|
||||
Type targetType,
|
||||
object? parameter,
|
||||
CultureInfo culture
|
||||
) => value is Channel channel ? channel.GetHierarchicalName() : null;
|
||||
|
||||
public object ConvertBack(
|
||||
object? value,
|
||||
Type targetType,
|
||||
object? parameter,
|
||||
CultureInfo culture
|
||||
) => throw new NotSupportedException();
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Converters;
|
||||
|
||||
[ValueConversion(typeof(DateTimeOffset?), typeof(DateTime?))]
|
||||
public class DateTimeOffsetToDateTimeConverter : IValueConverter
|
||||
{
|
||||
public static DateTimeOffsetToDateTimeConverter Instance { get; } = new();
|
||||
|
||||
public object? Convert(
|
||||
object? value,
|
||||
Type targetType,
|
||||
object? parameter,
|
||||
CultureInfo culture
|
||||
) =>
|
||||
value is DateTimeOffset dateTimeOffsetValue
|
||||
? dateTimeOffsetValue.DateTime
|
||||
: default(DateTime?);
|
||||
|
||||
public object? ConvertBack(
|
||||
object? value,
|
||||
Type targetType,
|
||||
object? parameter,
|
||||
CultureInfo culture
|
||||
) =>
|
||||
value is DateTime dateTimeValue
|
||||
? new DateTimeOffset(dateTimeValue)
|
||||
: default(DateTimeOffset?);
|
||||
}
|
|
@ -1,11 +1,10 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using Avalonia.Data.Converters;
|
||||
using DiscordChatExporter.Core.Exporting;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Converters;
|
||||
|
||||
[ValueConversion(typeof(ExportFormat), typeof(string))]
|
||||
public class ExportFormatToStringConverter : IValueConverter
|
||||
{
|
||||
public static ExportFormatToStringConverter Instance { get; } = new();
|
||||
|
@ -15,7 +14,7 @@ public class ExportFormatToStringConverter : IValueConverter
|
|||
Type targetType,
|
||||
object? parameter,
|
||||
CultureInfo culture
|
||||
) => value is ExportFormat exportFormatValue ? exportFormatValue.GetDisplayName() : default;
|
||||
) => value is ExportFormat format ? format.GetDisplayName() : default;
|
||||
|
||||
public object ConvertBack(
|
||||
object? value,
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Converters;
|
||||
|
||||
[ValueConversion(typeof(bool), typeof(bool))]
|
||||
public class InverseBoolConverter : IValueConverter
|
||||
{
|
||||
public static InverseBoolConverter Instance { get; } = new();
|
||||
|
||||
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) =>
|
||||
value is false;
|
||||
|
||||
public object ConvertBack(
|
||||
object? value,
|
||||
Type targetType,
|
||||
object? parameter,
|
||||
CultureInfo culture
|
||||
) => value is false;
|
||||
}
|
|
@ -1,13 +1,12 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using Avalonia.Data.Converters;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Converters;
|
||||
|
||||
[ValueConversion(typeof(string), typeof(string))]
|
||||
public class LocaleToDisplayNameConverter : IValueConverter
|
||||
public class LocaleToDisplayNameStringConverter : IValueConverter
|
||||
{
|
||||
public static LocaleToDisplayNameConverter Instance { get; } = new();
|
||||
public static LocaleToDisplayNameStringConverter Instance { get; } = new();
|
||||
|
||||
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) =>
|
||||
value is string locale && !string.IsNullOrWhiteSpace(locale)
|
|
@ -1,21 +1,20 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using Avalonia.Data.Converters;
|
||||
using DiscordChatExporter.Core.Discord;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Converters;
|
||||
|
||||
[ValueConversion(typeof(Snowflake?), typeof(DateTimeOffset?))]
|
||||
public class SnowflakeToDateTimeOffsetConverter : IValueConverter
|
||||
public class SnowflakeToTimestampStringConverter : IValueConverter
|
||||
{
|
||||
public static SnowflakeToDateTimeOffsetConverter Instance { get; } = new();
|
||||
public static SnowflakeToTimestampStringConverter Instance { get; } = new();
|
||||
|
||||
public object? Convert(
|
||||
object? value,
|
||||
Type targetType,
|
||||
object? parameter,
|
||||
CultureInfo culture
|
||||
) => value is Snowflake snowflake ? snowflake.ToDate() : null;
|
||||
) => value is Snowflake snowflake ? snowflake.ToDate().ToString("g", culture) : null;
|
||||
|
||||
public object ConvertBack(
|
||||
object? value,
|
|
@ -1,25 +0,0 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Converters;
|
||||
|
||||
[ValueConversion(typeof(TimeSpan?), typeof(DateTime?))]
|
||||
public class TimeSpanToDateTimeConverter : IValueConverter
|
||||
{
|
||||
public static TimeSpanToDateTimeConverter Instance { get; } = new();
|
||||
|
||||
public object? Convert(
|
||||
object? value,
|
||||
Type targetType,
|
||||
object? parameter,
|
||||
CultureInfo culture
|
||||
) => value is TimeSpan timeSpanValue ? DateTime.Today.Add(timeSpanValue) : default(DateTime?);
|
||||
|
||||
public object? ConvertBack(
|
||||
object? value,
|
||||
Type targetType,
|
||||
object? parameter,
|
||||
CultureInfo culture
|
||||
) => value is DateTime dateTimeValue ? dateTimeValue.TimeOfDay : default(TimeSpan?);
|
||||
}
|
|
@ -2,28 +2,30 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>$(TargetFramework)-windows</TargetFramework>
|
||||
<AssemblyName>DiscordChatExporter</AssemblyName>
|
||||
<UseWPF>true</UseWPF>
|
||||
<ApplicationIcon>../favicon.ico</ApplicationIcon>
|
||||
<ApplicationIcon>..\favicon.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Resource Include="../favicon.ico" />
|
||||
<AvaloniaResource Include="..\favicon.ico" Link="favicon.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AsyncImageLoader.Avalonia" Version="3.2.1" />
|
||||
<PackageReference Include="Avalonia" Version="11.0.10" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.10" />
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.10" Condition="'$(Configuration)' == 'Debug'" />
|
||||
<PackageReference Include="Cogwheel" Version="2.0.4" />
|
||||
<PackageReference Include="CSharpier.MsBuild" Version="0.26.7" PrivateAssets="all" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
|
||||
<PackageReference Include="CSharpier.MsBuild" Version="0.28.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Deorcify" Version="1.0.2" PrivateAssets="all" />
|
||||
<PackageReference Include="DotnetRuntimeBootstrapper" Version="2.5.2" PrivateAssets="all" />
|
||||
<PackageReference Include="DialogHost.Avalonia" Version="0.7.7" />
|
||||
<PackageReference Include="DotnetRuntimeBootstrapper" Version="2.5.3" PrivateAssets="all" Condition="$([MSBuild]::IsOsPlatform('Windows'))" />
|
||||
<PackageReference Include="Gress" Version="2.1.1" />
|
||||
<PackageReference Include="MaterialDesignColors" Version="2.1.4" />
|
||||
<PackageReference Include="MaterialDesignThemes" Version="4.9.0" />
|
||||
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.77" />
|
||||
<PackageReference Include="Onova" Version="2.6.10" />
|
||||
<PackageReference Include="PropertyChanged.Fody" Version="4.1.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Stylet" Version="1.3.6" />
|
||||
<PackageReference Include="Material.Avalonia" Version="3.5.0" />
|
||||
<PackageReference Include="Material.Icons.Avalonia" Version="2.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Onova" Version="2.6.11" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<PropertyChanged />
|
||||
</Weavers>
|
|
@ -1,74 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
|
||||
<xs:element name="Weavers">
|
||||
<xs:complexType>
|
||||
<xs:all>
|
||||
<xs:element name="PropertyChanged" minOccurs="0" maxOccurs="1">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="InjectOnPropertyNameChanged" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Used to control if the On_PropertyName_Changed feature is enabled.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="TriggerDependentProperties" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Used to control if the Dependent properties feature is enabled.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="EnableIsChangedProperty" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Used to control if the IsChanged property feature is enabled.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="EventInvokerNames" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Used to change the name of the method that fires the notify event. This is a string that accepts multiple values in a comma separated form.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="CheckForEquality" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Used to control if equality checks should be inserted. If false, equality checking will be disabled for the project.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="CheckForEqualityUsingBaseEquals" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Used to control if equality checks should use the Equals method resolved from the base class.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="UseStaticEqualsFromBase" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Used to control if equality checks should use the static Equals method resolved from the base class.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="SuppressWarnings" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Used to turn off build warnings from this weaver.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="SuppressOnPropertyNameChangedWarning" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Used to turn off build warnings about mismatched On_PropertyName_Changed methods.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
<xs:attribute name="VerifyAssembly" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="GenerateXsd" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
88
DiscordChatExporter.Gui/Framework/DialogManager.cs
Normal file
88
DiscordChatExporter.Gui/Framework/DialogManager.cs
Normal file
|
@ -0,0 +1,88 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AsyncKeyedLock;
|
||||
using Avalonia;
|
||||
using Avalonia.Platform.Storage;
|
||||
using DialogHostAvalonia;
|
||||
using DiscordChatExporter.Gui.Utils.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Framework;
|
||||
|
||||
public class DialogManager : IDisposable
|
||||
{
|
||||
private readonly AsyncNonKeyedLocker _dialogLock = new();
|
||||
|
||||
public async Task<T?> ShowDialogAsync<T>(DialogViewModelBase<T> dialog)
|
||||
{
|
||||
using (await _dialogLock.LockAsync())
|
||||
{
|
||||
await DialogHost.Show(
|
||||
dialog,
|
||||
// It's fine to await in a void method here because it's an event handler
|
||||
// ReSharper disable once AsyncVoidLambda
|
||||
async (object _, DialogOpenedEventArgs args) =>
|
||||
{
|
||||
await dialog.WaitForCloseAsync();
|
||||
|
||||
try
|
||||
{
|
||||
args.Session.Close();
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Dialog host is already processing a close operation
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return dialog.DialogResult;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string?> PromptSaveFilePathAsync(
|
||||
IReadOnlyList<FilePickerFileType>? fileTypes = null,
|
||||
string defaultFilePath = ""
|
||||
)
|
||||
{
|
||||
var topLevel =
|
||||
Application.Current?.ApplicationLifetime?.TryGetTopLevel()
|
||||
?? throw new ApplicationException("Could not find the top-level visual element.");
|
||||
|
||||
var file = await topLevel.StorageProvider.SaveFilePickerAsync(
|
||||
new FilePickerSaveOptions
|
||||
{
|
||||
FileTypeChoices = fileTypes,
|
||||
SuggestedFileName = defaultFilePath,
|
||||
DefaultExtension = Path.GetExtension(defaultFilePath).TrimStart('.')
|
||||
}
|
||||
);
|
||||
|
||||
return file?.Path.LocalPath;
|
||||
}
|
||||
|
||||
public async Task<string?> PromptDirectoryPathAsync(string defaultDirPath = "")
|
||||
{
|
||||
var topLevel =
|
||||
Application.Current?.ApplicationLifetime?.TryGetTopLevel()
|
||||
?? throw new ApplicationException("Could not find the top-level visual element.");
|
||||
|
||||
var startLocation = await topLevel.StorageProvider.TryGetFolderFromPathAsync(
|
||||
defaultDirPath
|
||||
);
|
||||
|
||||
var folderPickResult = await topLevel.StorageProvider.OpenFolderPickerAsync(
|
||||
new FolderPickerOpenOptions
|
||||
{
|
||||
AllowMultiple = false,
|
||||
SuggestedStartLocation = startLocation
|
||||
}
|
||||
);
|
||||
|
||||
return folderPickResult.FirstOrDefault()?.Path.LocalPath;
|
||||
}
|
||||
|
||||
public void Dispose() => _dialogLock.Dispose();
|
||||
}
|
25
DiscordChatExporter.Gui/Framework/DialogVIewModelBase.cs
Normal file
25
DiscordChatExporter.Gui/Framework/DialogVIewModelBase.cs
Normal file
|
@ -0,0 +1,25 @@
|
|||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Framework;
|
||||
|
||||
public abstract partial class DialogViewModelBase<T> : ViewModelBase
|
||||
{
|
||||
private readonly TaskCompletionSource<T> _closeTcs =
|
||||
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
[ObservableProperty]
|
||||
private T? _dialogResult;
|
||||
|
||||
[RelayCommand]
|
||||
protected void Close(T dialogResult)
|
||||
{
|
||||
DialogResult = dialogResult;
|
||||
_closeTcs.TrySetResult(dialogResult);
|
||||
}
|
||||
|
||||
public async Task<T> WaitForCloseAsync() => await _closeTcs.Task;
|
||||
}
|
||||
|
||||
public abstract class DialogViewModelBase : DialogViewModelBase<bool?>;
|
34
DiscordChatExporter.Gui/Framework/SnackbarManager.cs
Normal file
34
DiscordChatExporter.Gui/Framework/SnackbarManager.cs
Normal file
|
@ -0,0 +1,34 @@
|
|||
using System;
|
||||
using Avalonia.Threading;
|
||||
using Material.Styles.Controls;
|
||||
using Material.Styles.Models;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Framework;
|
||||
|
||||
public class SnackbarManager
|
||||
{
|
||||
private readonly TimeSpan _defaultDuration = TimeSpan.FromSeconds(5);
|
||||
|
||||
public void Notify(string message, TimeSpan? duration = null) =>
|
||||
SnackbarHost.Post(
|
||||
new SnackbarModel(message, duration ?? _defaultDuration),
|
||||
null,
|
||||
DispatcherPriority.Normal
|
||||
);
|
||||
|
||||
public void Notify(
|
||||
string message,
|
||||
string actionText,
|
||||
Action actionHandler,
|
||||
TimeSpan? duration = null
|
||||
) =>
|
||||
SnackbarHost.Post(
|
||||
new SnackbarModel(
|
||||
message,
|
||||
duration ?? _defaultDuration,
|
||||
new SnackbarButtonModel { Text = actionText, Action = actionHandler }
|
||||
),
|
||||
null,
|
||||
DispatcherPriority.Normal
|
||||
);
|
||||
}
|
18
DiscordChatExporter.Gui/Framework/UserControl.cs
Normal file
18
DiscordChatExporter.Gui/Framework/UserControl.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
using System;
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Framework;
|
||||
|
||||
public class UserControl<TDataContext> : UserControl
|
||||
{
|
||||
public new TDataContext DataContext
|
||||
{
|
||||
get =>
|
||||
base.DataContext is TDataContext dataContext
|
||||
? dataContext
|
||||
: throw new InvalidCastException(
|
||||
$"DataContext is null or not of the expected type '{typeof(TDataContext).FullName}'."
|
||||
);
|
||||
set => base.DataContext = value;
|
||||
}
|
||||
}
|
37
DiscordChatExporter.Gui/Framework/ViewManager.cs
Normal file
37
DiscordChatExporter.Gui/Framework/ViewManager.cs
Normal file
|
@ -0,0 +1,37 @@
|
|||
using System;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Templates;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Framework;
|
||||
|
||||
public partial class ViewManager
|
||||
{
|
||||
public Control? TryBindView(ViewModelBase viewModel)
|
||||
{
|
||||
var name = viewModel
|
||||
.GetType()
|
||||
.FullName?.Replace("ViewModel", "View", StringComparison.Ordinal);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return null;
|
||||
|
||||
var type = Type.GetType(name);
|
||||
if (type is null)
|
||||
return null;
|
||||
|
||||
if (Activator.CreateInstance(type) is not Control view)
|
||||
return null;
|
||||
|
||||
view.DataContext ??= viewModel;
|
||||
|
||||
return view;
|
||||
}
|
||||
}
|
||||
|
||||
public partial class ViewManager : IDataTemplate
|
||||
{
|
||||
bool IDataTemplate.Match(object? data) => data is ViewModelBase;
|
||||
|
||||
Control? ITemplate<object?, Control?>.Build(object? data) =>
|
||||
data is ViewModelBase viewModel ? TryBindView(viewModel) : null;
|
||||
}
|
19
DiscordChatExporter.Gui/Framework/ViewModelBase.cs
Normal file
19
DiscordChatExporter.Gui/Framework/ViewModelBase.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
using System;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Framework;
|
||||
|
||||
public abstract class ViewModelBase : ObservableObject, IDisposable
|
||||
{
|
||||
~ViewModelBase() => Dispose(false);
|
||||
|
||||
protected void OnAllPropertiesChanged() => OnPropertyChanged(string.Empty);
|
||||
|
||||
protected virtual void Dispose(bool disposing) { }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
53
DiscordChatExporter.Gui/Framework/ViewModelManager.cs
Normal file
53
DiscordChatExporter.Gui/Framework/ViewModelManager.cs
Normal file
|
@ -0,0 +1,53 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DiscordChatExporter.Core.Discord.Data;
|
||||
using DiscordChatExporter.Gui.ViewModels;
|
||||
using DiscordChatExporter.Gui.ViewModels.Components;
|
||||
using DiscordChatExporter.Gui.ViewModels.Dialogs;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Framework;
|
||||
|
||||
public class ViewModelManager(IServiceProvider services)
|
||||
{
|
||||
public MainViewModel CreateMainViewModel() => services.GetRequiredService<MainViewModel>();
|
||||
|
||||
public DashboardViewModel CreateDashboardViewModel() =>
|
||||
services.GetRequiredService<DashboardViewModel>();
|
||||
|
||||
public ExportSetupViewModel CreateExportSetupViewModel(
|
||||
Guild guild,
|
||||
IReadOnlyList<Channel> channels
|
||||
)
|
||||
{
|
||||
var viewModel = services.GetRequiredService<ExportSetupViewModel>();
|
||||
|
||||
viewModel.Guild = guild;
|
||||
viewModel.Channels = channels;
|
||||
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
public MessageBoxViewModel CreateMessageBoxViewModel(
|
||||
string title,
|
||||
string message,
|
||||
string? okButtonText,
|
||||
string? cancelButtonText
|
||||
)
|
||||
{
|
||||
var viewModel = services.GetRequiredService<MessageBoxViewModel>();
|
||||
|
||||
viewModel.Title = title;
|
||||
viewModel.Message = message;
|
||||
viewModel.DefaultButtonText = okButtonText;
|
||||
viewModel.CancelButtonText = cancelButtonText;
|
||||
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
public MessageBoxViewModel CreateMessageBoxViewModel(string title, string message) =>
|
||||
CreateMessageBoxViewModel(title, message, "CLOSE", null);
|
||||
|
||||
public SettingsViewModel CreateSettingsViewModel() =>
|
||||
services.GetRequiredService<SettingsViewModel>();
|
||||
}
|
18
DiscordChatExporter.Gui/Framework/Window.cs
Normal file
18
DiscordChatExporter.Gui/Framework/Window.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
using System;
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Framework;
|
||||
|
||||
public class Window<TDataContext> : Window
|
||||
{
|
||||
public new TDataContext DataContext
|
||||
{
|
||||
get =>
|
||||
base.DataContext is TDataContext dataContext
|
||||
? dataContext
|
||||
: throw new InvalidCastException(
|
||||
$"DataContext is null or not of the expected type '{typeof(TDataContext).FullName}'."
|
||||
);
|
||||
set => base.DataContext = value;
|
||||
}
|
||||
}
|
51
DiscordChatExporter.Gui/Program.cs
Normal file
51
DiscordChatExporter.Gui/Program.cs
Normal file
|
@ -0,0 +1,51 @@
|
|||
using System;
|
||||
using System.Reflection;
|
||||
using Avalonia;
|
||||
using DiscordChatExporter.Gui.Utils;
|
||||
|
||||
namespace DiscordChatExporter.Gui;
|
||||
|
||||
public static class Program
|
||||
{
|
||||
private static Assembly Assembly { get; } = typeof(App).Assembly;
|
||||
|
||||
public static string Name { get; } = Assembly.GetName().Name!;
|
||||
|
||||
public static Version Version { get; } = Assembly.GetName().Version!;
|
||||
|
||||
public static string VersionString { get; } = Version.ToString(3);
|
||||
|
||||
public static string ProjectUrl { get; } = "https://github.com/Tyrrrz/DiscordChatExporter";
|
||||
|
||||
public static string LatestReleaseUrl { get; } = ProjectUrl + "/releases/latest";
|
||||
|
||||
public static string DocumentationUrl { get; } = ProjectUrl + "/tree/master/.docs";
|
||||
|
||||
public static AppBuilder BuildAvaloniaApp() =>
|
||||
AppBuilder.Configure<App>().UsePlatformDetect().LogToTrace();
|
||||
|
||||
[STAThread]
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
// Build and run the app
|
||||
var builder = BuildAvaloniaApp();
|
||||
|
||||
try
|
||||
{
|
||||
return builder.StartWithClassicDesktopLifetime(args);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
_ = NativeMethods.Windows.MessageBox(0, ex.ToString(), "Fatal Error", 0x10);
|
||||
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Clean up after application shutdown
|
||||
if (builder.Instance is IDisposable disposableApp)
|
||||
disposableApp.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,48 +1,80 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using Avalonia;
|
||||
using Avalonia.Platform;
|
||||
using Cogwheel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using DiscordChatExporter.Core.Exporting;
|
||||
using DiscordChatExporter.Gui.Models;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Services;
|
||||
|
||||
[INotifyPropertyChanged]
|
||||
public partial class SettingsService()
|
||||
: SettingsBase(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Settings.dat"))
|
||||
{
|
||||
public bool IsUkraineSupportMessageEnabled { get; set; } = true;
|
||||
[ObservableProperty]
|
||||
private bool _isUkraineSupportMessageEnabled = true;
|
||||
|
||||
public bool IsAutoUpdateEnabled { get; set; } = true;
|
||||
[ObservableProperty]
|
||||
private bool _isAutoUpdateEnabled = true;
|
||||
|
||||
public bool IsDarkModeEnabled { get; set; } = IsDarkModeEnabledByDefault();
|
||||
[ObservableProperty]
|
||||
private bool _isDarkModeEnabled;
|
||||
|
||||
public bool IsTokenPersisted { get; set; } = true;
|
||||
[ObservableProperty]
|
||||
private bool _isTokenPersisted = true;
|
||||
|
||||
public ThreadInclusionMode ThreadInclusionMode { get; set; } = ThreadInclusionMode.None;
|
||||
[ObservableProperty]
|
||||
private ThreadInclusionMode _threadInclusionMode;
|
||||
|
||||
public string? Locale { get; set; }
|
||||
[ObservableProperty]
|
||||
private string? _locale;
|
||||
|
||||
public bool IsUtcNormalizationEnabled { get; set; }
|
||||
[ObservableProperty]
|
||||
private bool _isUtcNormalizationEnabled;
|
||||
|
||||
public int ParallelLimit { get; set; } = 1;
|
||||
[ObservableProperty]
|
||||
private int _parallelLimit = 1;
|
||||
|
||||
public Version? LastAppVersion { get; set; }
|
||||
[ObservableProperty]
|
||||
private Version? _lastAppVersion;
|
||||
|
||||
public string? LastToken { get; set; }
|
||||
[ObservableProperty]
|
||||
private string? _lastToken;
|
||||
|
||||
public ExportFormat LastExportFormat { get; set; } = ExportFormat.HtmlDark;
|
||||
[ObservableProperty]
|
||||
private ExportFormat _lastExportFormat = ExportFormat.HtmlDark;
|
||||
|
||||
public string? LastPartitionLimitValue { get; set; }
|
||||
[ObservableProperty]
|
||||
private string? _lastPartitionLimitValue;
|
||||
|
||||
public string? LastMessageFilterValue { get; set; }
|
||||
[ObservableProperty]
|
||||
private string? _lastMessageFilterValue;
|
||||
|
||||
public bool LastShouldFormatMarkdown { get; set; } = true;
|
||||
[ObservableProperty]
|
||||
private bool _lastShouldFormatMarkdown = true;
|
||||
|
||||
public bool LastShouldDownloadAssets { get; set; }
|
||||
[ObservableProperty]
|
||||
private bool _lastShouldDownloadAssets;
|
||||
|
||||
public bool LastShouldReuseAssets { get; set; }
|
||||
[ObservableProperty]
|
||||
private bool _lastShouldReuseAssets;
|
||||
|
||||
public string? LastAssetsDirPath { get; set; }
|
||||
[ObservableProperty]
|
||||
private string? _lastAssetsDirPath;
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
|
||||
// Reset the dark mode setting separately because its default value is evaluated dynamically
|
||||
// and cannot be set by the field initializer.
|
||||
IsDarkModeEnabled =
|
||||
Application.Current?.PlatformSettings?.GetColorValues().ThemeVariant
|
||||
== PlatformThemeVariant.Dark;
|
||||
}
|
||||
|
||||
public override void Save()
|
||||
{
|
||||
|
@ -56,24 +88,3 @@ public partial class SettingsService()
|
|||
LastToken = lastToken;
|
||||
}
|
||||
}
|
||||
|
||||
public partial class SettingsService
|
||||
{
|
||||
private static bool IsDarkModeEnabledByDefault()
|
||||
{
|
||||
try
|
||||
{
|
||||
return Registry
|
||||
.CurrentUser.OpenSubKey(
|
||||
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
false
|
||||
)
|
||||
?.GetValue("AppsUseLightTheme")
|
||||
is 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,6 +51,10 @@ public class UpdateService(SettingsService settingsService) : IDisposable
|
|||
if (!settingsService.IsAutoUpdateEnabled)
|
||||
return;
|
||||
|
||||
// Onova only works on Windows currently
|
||||
if (!OperatingSystem.IsWindows())
|
||||
return;
|
||||
|
||||
if (_updateVersion is null || !_updatePrepared || _updaterLaunched)
|
||||
return;
|
||||
|
||||
|
|
10
DiscordChatExporter.Gui/Utils/Disposable.cs
Normal file
10
DiscordChatExporter.Gui/Utils/Disposable.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
using System;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Utils;
|
||||
|
||||
internal class Disposable(Action dispose) : IDisposable
|
||||
{
|
||||
public static IDisposable Create(Action dispose) => new Disposable(dispose);
|
||||
|
||||
public void Dispose() => dispose();
|
||||
}
|
28
DiscordChatExporter.Gui/Utils/DisposableCollector.cs
Normal file
28
DiscordChatExporter.Gui/Utils/DisposableCollector.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DiscordChatExporter.Gui.Utils.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Utils;
|
||||
|
||||
internal class DisposableCollector : IDisposable
|
||||
{
|
||||
private readonly object _lock = new();
|
||||
private readonly List<IDisposable> _items = [];
|
||||
|
||||
public void Add(IDisposable item)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_items.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_items.DisposeAll();
|
||||
_items.Clear();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
using System;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.VisualTree;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Utils.Extensions;
|
||||
|
||||
internal static class AvaloniaExtensions
|
||||
{
|
||||
public static Window? TryGetMainWindow(this IApplicationLifetime lifetime) =>
|
||||
lifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime
|
||||
? desktopLifetime.MainWindow
|
||||
: null;
|
||||
|
||||
public static TopLevel? TryGetTopLevel(this IApplicationLifetime lifetime) =>
|
||||
lifetime.TryGetMainWindow()
|
||||
?? (lifetime as ISingleViewApplicationLifetime)?.MainView?.GetVisualRoot() as TopLevel;
|
||||
|
||||
public static bool TryShutdown(this IApplicationLifetime lifetime, int exitCode = 0)
|
||||
{
|
||||
if (lifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
|
||||
{
|
||||
return desktopLifetime.TryShutdown(exitCode);
|
||||
}
|
||||
|
||||
if (lifetime is IControlledApplicationLifetime controlledLifetime)
|
||||
{
|
||||
controlledLifetime.Shutdown(exitCode);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Utils.Extensions;
|
||||
|
||||
internal static class DisposableExtensions
|
||||
{
|
||||
public static void DisposeAll(this IEnumerable<IDisposable> disposables)
|
||||
{
|
||||
var exceptions = default(List<Exception>);
|
||||
|
||||
foreach (var disposable in disposables)
|
||||
{
|
||||
try
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
(exceptions ??= []).Add(ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (exceptions?.Any() == true)
|
||||
throw new AggregateException(exceptions);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Utils.Extensions;
|
||||
|
||||
internal static class NotifyPropertyChangedExtensions
|
||||
{
|
||||
public static IDisposable WatchProperty<TOwner, TProperty>(
|
||||
this TOwner owner,
|
||||
Expression<Func<TOwner, TProperty>> propertyExpression,
|
||||
Action callback,
|
||||
bool watchInitialValue = true
|
||||
)
|
||||
where TOwner : INotifyPropertyChanged
|
||||
{
|
||||
var memberExpression =
|
||||
propertyExpression.Body as MemberExpression
|
||||
// Property value might be boxed inside a conversion expression, if the types don't match
|
||||
?? (propertyExpression.Body as UnaryExpression)?.Operand as MemberExpression;
|
||||
|
||||
if (memberExpression?.Member is not PropertyInfo property)
|
||||
throw new ArgumentException("Provided expression must reference a property.");
|
||||
|
||||
void OnPropertyChanged(object? sender, PropertyChangedEventArgs args)
|
||||
{
|
||||
if (
|
||||
string.IsNullOrWhiteSpace(args.PropertyName)
|
||||
|| string.Equals(args.PropertyName, property.Name, StringComparison.Ordinal)
|
||||
)
|
||||
{
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
owner.PropertyChanged += OnPropertyChanged;
|
||||
|
||||
if (watchInitialValue)
|
||||
callback();
|
||||
|
||||
return Disposable.Create(() => owner.PropertyChanged -= OnPropertyChanged);
|
||||
}
|
||||
|
||||
public static IDisposable WatchAllProperties<TOwner>(
|
||||
this TOwner owner,
|
||||
Action callback,
|
||||
bool watchInitialValues = true
|
||||
)
|
||||
where TOwner : INotifyPropertyChanged
|
||||
{
|
||||
void OnPropertyChanged(object? sender, PropertyChangedEventArgs args) => callback();
|
||||
owner.PropertyChanged += OnPropertyChanged;
|
||||
|
||||
if (watchInitialValues)
|
||||
callback();
|
||||
|
||||
return Disposable.Create(() => owner.PropertyChanged -= OnPropertyChanged);
|
||||
}
|
||||
}
|
|
@ -7,4 +7,6 @@ internal static class Internationalization
|
|||
public static bool Is24Hours =>
|
||||
string.IsNullOrWhiteSpace(CultureInfo.CurrentCulture.DateTimeFormat.AMDesignator)
|
||||
&& string.IsNullOrWhiteSpace(CultureInfo.CurrentCulture.DateTimeFormat.PMDesignator);
|
||||
|
||||
public static string AvaloniaClockIdentifier => Is24Hours ? "24HourClock" : "12HourClock";
|
||||
}
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
using System.Windows.Media;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Utils;
|
||||
|
||||
internal static class MediaColor
|
||||
{
|
||||
public static Color FromHex(string hex) => (Color)ColorConverter.ConvertFromString(hex);
|
||||
}
|
12
DiscordChatExporter.Gui/Utils/NativeMethods.cs
Normal file
12
DiscordChatExporter.Gui/Utils/NativeMethods.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Utils;
|
||||
|
||||
internal static class NativeMethods
|
||||
{
|
||||
public static class Windows
|
||||
{
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern int MessageBox(nint hWnd, string text, string caption, uint type);
|
||||
}
|
||||
}
|
|
@ -6,10 +6,8 @@ internal static class ProcessEx
|
|||
{
|
||||
public static void StartShellExecute(string path)
|
||||
{
|
||||
using var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo { FileName = path, UseShellExecute = true }
|
||||
};
|
||||
using var process = new Process();
|
||||
process.StartInfo = new ProcessStartInfo { FileName = path, UseShellExecute = true };
|
||||
|
||||
process.Start();
|
||||
}
|
||||
|
|
|
@ -1,104 +1,113 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using DiscordChatExporter.Core.Discord;
|
||||
using DiscordChatExporter.Core.Discord.Data;
|
||||
using DiscordChatExporter.Core.Exceptions;
|
||||
using DiscordChatExporter.Core.Exporting;
|
||||
using DiscordChatExporter.Core.Utils.Extensions;
|
||||
using DiscordChatExporter.Gui.Framework;
|
||||
using DiscordChatExporter.Gui.Models;
|
||||
using DiscordChatExporter.Gui.Services;
|
||||
using DiscordChatExporter.Gui.Utils;
|
||||
using DiscordChatExporter.Gui.ViewModels.Dialogs;
|
||||
using DiscordChatExporter.Gui.ViewModels.Framework;
|
||||
using DiscordChatExporter.Gui.ViewModels.Messages;
|
||||
using DiscordChatExporter.Gui.Utils.Extensions;
|
||||
using Gress;
|
||||
using Gress.Completable;
|
||||
using Stylet;
|
||||
|
||||
namespace DiscordChatExporter.Gui.ViewModels.Components;
|
||||
|
||||
public class DashboardViewModel : PropertyChangedBase
|
||||
public partial class DashboardViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IViewModelFactory _viewModelFactory;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly ViewModelManager _viewModelManager;
|
||||
private readonly SnackbarManager _snackbarManager;
|
||||
private readonly DialogManager _dialogManager;
|
||||
private readonly SettingsService _settingsService;
|
||||
|
||||
private readonly DisposableCollector _eventRoot = new();
|
||||
private readonly AutoResetProgressMuxer _progressMuxer;
|
||||
|
||||
private DiscordClient? _discord;
|
||||
|
||||
public bool IsBusy { get; private set; }
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsProgressIndeterminate))]
|
||||
[NotifyCanExecuteChangedFor(nameof(PullGuildsCommand))]
|
||||
[NotifyCanExecuteChangedFor(nameof(PullChannelsCommand))]
|
||||
[NotifyCanExecuteChangedFor(nameof(ExportCommand))]
|
||||
private bool _isBusy;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(PullGuildsCommand))]
|
||||
private string? _token;
|
||||
|
||||
[ObservableProperty]
|
||||
private IReadOnlyList<Guild>? _availableGuilds;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(PullChannelsCommand))]
|
||||
[NotifyCanExecuteChangedFor(nameof(ExportCommand))]
|
||||
private Guild? _selectedGuild;
|
||||
|
||||
[ObservableProperty]
|
||||
private IReadOnlyList<ChannelNode>? _availableChannels;
|
||||
|
||||
public DashboardViewModel(
|
||||
ViewModelManager viewModelManager,
|
||||
DialogManager dialogManager,
|
||||
SnackbarManager snackbarManager,
|
||||
SettingsService settingsService
|
||||
)
|
||||
{
|
||||
_viewModelManager = viewModelManager;
|
||||
_dialogManager = dialogManager;
|
||||
_snackbarManager = snackbarManager;
|
||||
_settingsService = settingsService;
|
||||
|
||||
_progressMuxer = Progress.CreateMuxer().WithAutoReset();
|
||||
|
||||
_eventRoot.Add(
|
||||
Progress.WatchProperty(
|
||||
o => o.Current,
|
||||
() => OnPropertyChanged(nameof(IsProgressIndeterminate))
|
||||
)
|
||||
);
|
||||
|
||||
_eventRoot.Add(
|
||||
SelectedChannels.WatchProperty(
|
||||
o => o.Count,
|
||||
() => ExportCommand.NotifyCanExecuteChanged()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public ProgressContainer<Percentage> Progress { get; } = new();
|
||||
|
||||
public bool IsProgressIndeterminate => IsBusy && Progress.Current.Fraction is <= 0 or >= 1;
|
||||
|
||||
public string? Token { get; set; }
|
||||
public ObservableCollection<ChannelNode> SelectedChannels { get; } = [];
|
||||
|
||||
public IReadOnlyList<Guild>? AvailableGuilds { get; private set; }
|
||||
|
||||
public Guild? SelectedGuild { get; set; }
|
||||
|
||||
public IReadOnlyList<Channel>? AvailableChannels { get; private set; }
|
||||
|
||||
public IReadOnlyList<Channel>? SelectedChannels { get; set; }
|
||||
|
||||
public DashboardViewModel(
|
||||
IViewModelFactory viewModelFactory,
|
||||
IEventAggregator eventAggregator,
|
||||
DialogManager dialogManager,
|
||||
SettingsService settingsService
|
||||
)
|
||||
{
|
||||
_viewModelFactory = viewModelFactory;
|
||||
_eventAggregator = eventAggregator;
|
||||
_dialogManager = dialogManager;
|
||||
_settingsService = settingsService;
|
||||
|
||||
_progressMuxer = Progress.CreateMuxer().WithAutoReset();
|
||||
|
||||
this.Bind(o => o.IsBusy, (_, _) => NotifyOfPropertyChange(() => IsProgressIndeterminate));
|
||||
|
||||
Progress.Bind(
|
||||
o => o.Current,
|
||||
(_, _) => NotifyOfPropertyChange(() => IsProgressIndeterminate)
|
||||
);
|
||||
|
||||
this.Bind(
|
||||
o => o.SelectedGuild,
|
||||
(_, _) =>
|
||||
{
|
||||
// Reset channels when the selected guild changes, to avoid jitter
|
||||
// due to the channels being asynchronously loaded.
|
||||
AvailableChannels = null;
|
||||
SelectedChannels = null;
|
||||
|
||||
// Pull channels for the selected guild
|
||||
// (ideally this should be called inside `PullGuilds()`,
|
||||
// but Stylet doesn't support async commands)
|
||||
PullChannels();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public void OnViewLoaded()
|
||||
[RelayCommand]
|
||||
private void Initialize()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_settingsService.LastToken))
|
||||
Token = _settingsService.LastToken;
|
||||
}
|
||||
|
||||
public async void ShowSettings() =>
|
||||
await _dialogManager.ShowDialogAsync(_viewModelFactory.CreateSettingsViewModel());
|
||||
[RelayCommand]
|
||||
private async Task ShowSettingsAsync() =>
|
||||
await _dialogManager.ShowDialogAsync(_viewModelManager.CreateSettingsViewModel());
|
||||
|
||||
public void ShowHelp() => ProcessEx.StartShellExecute(App.DocumentationUrl);
|
||||
[RelayCommand]
|
||||
private void ShowHelp() => ProcessEx.StartShellExecute(Program.DocumentationUrl);
|
||||
|
||||
public bool CanPullGuilds => !IsBusy && !string.IsNullOrWhiteSpace(Token);
|
||||
private bool CanPullGuilds() => !IsBusy && !string.IsNullOrWhiteSpace(Token);
|
||||
|
||||
public async void PullGuilds()
|
||||
[RelayCommand(CanExecute = nameof(CanPullGuilds))]
|
||||
private async Task PullGuildsAsync()
|
||||
{
|
||||
IsBusy = true;
|
||||
var progress = _progressMuxer.CreateInput();
|
||||
|
@ -112,7 +121,7 @@ public class DashboardViewModel : PropertyChangedBase
|
|||
AvailableGuilds = null;
|
||||
SelectedGuild = null;
|
||||
AvailableChannels = null;
|
||||
SelectedChannels = null;
|
||||
SelectedChannels.Clear();
|
||||
|
||||
_discord = new DiscordClient(token);
|
||||
_settingsService.LastToken = token;
|
||||
|
@ -121,14 +130,16 @@ public class DashboardViewModel : PropertyChangedBase
|
|||
|
||||
AvailableGuilds = guilds;
|
||||
SelectedGuild = guilds.FirstOrDefault();
|
||||
|
||||
await PullChannelsAsync();
|
||||
}
|
||||
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
|
||||
{
|
||||
_eventAggregator.Publish(new NotificationMessage(ex.Message.TrimEnd('.')));
|
||||
_snackbarManager.Notify(ex.Message.TrimEnd('.'));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var dialog = _viewModelFactory.CreateMessageBoxViewModel(
|
||||
var dialog = _viewModelManager.CreateMessageBoxViewModel(
|
||||
"Error pulling guilds",
|
||||
ex.ToString()
|
||||
);
|
||||
|
@ -142,9 +153,10 @@ public class DashboardViewModel : PropertyChangedBase
|
|||
}
|
||||
}
|
||||
|
||||
public bool CanPullChannels => !IsBusy && _discord is not null && SelectedGuild is not null;
|
||||
private bool CanPullChannels() => !IsBusy && _discord is not null && SelectedGuild is not null;
|
||||
|
||||
public async void PullChannels()
|
||||
[RelayCommand(CanExecute = nameof(CanPullChannels))]
|
||||
private async Task PullChannelsAsync()
|
||||
{
|
||||
IsBusy = true;
|
||||
var progress = _progressMuxer.CreateInput();
|
||||
|
@ -155,18 +167,13 @@ public class DashboardViewModel : PropertyChangedBase
|
|||
return;
|
||||
|
||||
AvailableChannels = null;
|
||||
SelectedChannels = null;
|
||||
SelectedChannels.Clear();
|
||||
|
||||
var channels = new List<Channel>();
|
||||
|
||||
// Regular channels
|
||||
await foreach (var channel in _discord.GetGuildChannelsAsync(SelectedGuild.Id))
|
||||
{
|
||||
if (channel.IsCategory)
|
||||
continue;
|
||||
|
||||
channels.Add(channel);
|
||||
}
|
||||
|
||||
// Threads
|
||||
if (_settingsService.ThreadInclusionMode != ThreadInclusionMode.None)
|
||||
|
@ -182,16 +189,24 @@ public class DashboardViewModel : PropertyChangedBase
|
|||
}
|
||||
}
|
||||
|
||||
AvailableChannels = channels;
|
||||
SelectedChannels = null;
|
||||
// Build a hierarchy of channels
|
||||
var channelTree = ChannelNode.BuildTree(
|
||||
channels
|
||||
.OrderByDescending(c => c.IsDirect ? c.LastMessageId : null)
|
||||
.ThenBy(c => c.Position)
|
||||
.ToArray()
|
||||
);
|
||||
|
||||
AvailableChannels = channelTree;
|
||||
SelectedChannels.Clear();
|
||||
}
|
||||
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
|
||||
{
|
||||
_eventAggregator.Publish(new NotificationMessage(ex.Message.TrimEnd('.')));
|
||||
_snackbarManager.Notify(ex.Message.TrimEnd('.'));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var dialog = _viewModelFactory.CreateMessageBoxViewModel(
|
||||
var dialog = _viewModelManager.CreateMessageBoxViewModel(
|
||||
"Error pulling channels",
|
||||
ex.ToString()
|
||||
);
|
||||
|
@ -205,30 +220,24 @@ public class DashboardViewModel : PropertyChangedBase
|
|||
}
|
||||
}
|
||||
|
||||
public bool CanExport =>
|
||||
!IsBusy
|
||||
&& _discord is not null
|
||||
&& SelectedGuild is not null
|
||||
&& SelectedChannels?.Any() is true;
|
||||
private bool CanExport() =>
|
||||
!IsBusy && _discord is not null && SelectedGuild is not null && SelectedChannels.Any();
|
||||
|
||||
public async void Export()
|
||||
[RelayCommand(CanExecute = nameof(CanExport))]
|
||||
private async Task ExportAsync()
|
||||
{
|
||||
IsBusy = true;
|
||||
|
||||
try
|
||||
{
|
||||
if (
|
||||
_discord is null
|
||||
|| SelectedGuild is null
|
||||
|| SelectedChannels is null
|
||||
|| !SelectedChannels.Any()
|
||||
)
|
||||
if (_discord is null || SelectedGuild is null || !SelectedChannels.Any())
|
||||
return;
|
||||
|
||||
var dialog = _viewModelFactory.CreateExportSetupViewModel(
|
||||
var dialog = _viewModelManager.CreateExportSetupViewModel(
|
||||
SelectedGuild,
|
||||
SelectedChannels
|
||||
SelectedChannels.Select(c => c.Channel).ToArray()
|
||||
);
|
||||
|
||||
if (await _dialogManager.ShowDialogAsync(dialog) != true)
|
||||
return;
|
||||
|
||||
|
@ -276,7 +285,7 @@ public class DashboardViewModel : PropertyChangedBase
|
|||
}
|
||||
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
|
||||
{
|
||||
_eventAggregator.Publish(new NotificationMessage(ex.Message.TrimEnd('.')));
|
||||
_snackbarManager.Notify(ex.Message.TrimEnd('.'));
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
@ -288,16 +297,14 @@ public class DashboardViewModel : PropertyChangedBase
|
|||
// Notify of the overall completion
|
||||
if (successfulExportCount > 0)
|
||||
{
|
||||
_eventAggregator.Publish(
|
||||
new NotificationMessage(
|
||||
$"Successfully exported {successfulExportCount} channel(s)"
|
||||
)
|
||||
_snackbarManager.Notify(
|
||||
$"Successfully exported {successfulExportCount} channel(s)"
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var dialog = _viewModelFactory.CreateMessageBoxViewModel(
|
||||
var dialog = _viewModelManager.CreateMessageBoxViewModel(
|
||||
"Error exporting channel(s)",
|
||||
ex.ToString()
|
||||
);
|
||||
|
@ -310,8 +317,20 @@ public class DashboardViewModel : PropertyChangedBase
|
|||
}
|
||||
}
|
||||
|
||||
public void OpenDiscord() => ProcessEx.StartShellExecute("https://discord.com/app");
|
||||
[RelayCommand]
|
||||
private void OpenDiscord() => ProcessEx.StartShellExecute("https://discord.com/app");
|
||||
|
||||
public void OpenDiscordDeveloperPortal() =>
|
||||
[RelayCommand]
|
||||
private void OpenDiscordDeveloperPortal() =>
|
||||
ProcessEx.StartShellExecute("https://discord.com/developers/applications");
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_eventRoot.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,89 +1,111 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Platform.Storage;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using DiscordChatExporter.Core.Discord;
|
||||
using DiscordChatExporter.Core.Discord.Data;
|
||||
using DiscordChatExporter.Core.Exporting;
|
||||
using DiscordChatExporter.Core.Exporting.Filtering;
|
||||
using DiscordChatExporter.Core.Exporting.Partitioning;
|
||||
using DiscordChatExporter.Core.Utils.Extensions;
|
||||
using DiscordChatExporter.Gui.Framework;
|
||||
using DiscordChatExporter.Gui.Services;
|
||||
using DiscordChatExporter.Gui.ViewModels.Framework;
|
||||
|
||||
namespace DiscordChatExporter.Gui.ViewModels.Dialogs;
|
||||
|
||||
public class ExportSetupViewModel : DialogScreen
|
||||
public partial class ExportSetupViewModel(
|
||||
DialogManager dialogManager,
|
||||
SettingsService settingsService
|
||||
) : DialogViewModelBase
|
||||
{
|
||||
private readonly DialogManager _dialogManager;
|
||||
private readonly SettingsService _settingsService;
|
||||
[ObservableProperty]
|
||||
private Guild? _guild;
|
||||
|
||||
public Guild? Guild { get; set; }
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsSingleChannel))]
|
||||
private IReadOnlyList<Channel>? _channels;
|
||||
|
||||
public IReadOnlyList<Channel>? Channels { get; set; }
|
||||
[ObservableProperty]
|
||||
private string? _outputPath;
|
||||
|
||||
[ObservableProperty]
|
||||
private ExportFormat _selectedFormat;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsAfterDateSet))]
|
||||
[NotifyPropertyChangedFor(nameof(After))]
|
||||
private DateTimeOffset? _afterDate;
|
||||
|
||||
[ObservableProperty]
|
||||
private TimeSpan? _afterTime;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsBeforeDateSet))]
|
||||
[NotifyPropertyChangedFor(nameof(Before))]
|
||||
private DateTimeOffset? _beforeDate;
|
||||
|
||||
[ObservableProperty]
|
||||
private TimeSpan? _beforeTime;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(PartitionLimit))]
|
||||
private string? _partitionLimitValue;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(MessageFilter))]
|
||||
private string? _messageFilterValue;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _shouldFormatMarkdown;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _shouldDownloadAssets;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _shouldReuseAssets;
|
||||
|
||||
[ObservableProperty]
|
||||
private string? _assetsDirPath;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isAdvancedSectionDisplayed;
|
||||
|
||||
public bool IsSingleChannel => Channels?.Count == 1;
|
||||
|
||||
public string? OutputPath { get; set; }
|
||||
|
||||
public IReadOnlyList<ExportFormat> AvailableFormats { get; } = Enum.GetValues<ExportFormat>();
|
||||
|
||||
public ExportFormat SelectedFormat { get; set; }
|
||||
|
||||
// This date/time abomination is required because we use separate controls to set these
|
||||
|
||||
public DateTimeOffset? AfterDate { get; set; }
|
||||
|
||||
public bool IsAfterDateSet => AfterDate is not null;
|
||||
|
||||
public TimeSpan? AfterTime { get; set; }
|
||||
|
||||
public DateTimeOffset? After => AfterDate?.Add(AfterTime ?? TimeSpan.Zero);
|
||||
|
||||
public DateTimeOffset? BeforeDate { get; set; }
|
||||
|
||||
public bool IsBeforeDateSet => BeforeDate is not null;
|
||||
|
||||
public TimeSpan? BeforeTime { get; set; }
|
||||
|
||||
public DateTimeOffset? Before => BeforeDate?.Add(BeforeTime ?? TimeSpan.Zero);
|
||||
|
||||
public string? PartitionLimitValue { get; set; }
|
||||
|
||||
public PartitionLimit PartitionLimit =>
|
||||
!string.IsNullOrWhiteSpace(PartitionLimitValue)
|
||||
? PartitionLimit.Parse(PartitionLimitValue)
|
||||
: PartitionLimit.Null;
|
||||
|
||||
public string? MessageFilterValue { get; set; }
|
||||
|
||||
public MessageFilter MessageFilter =>
|
||||
!string.IsNullOrWhiteSpace(MessageFilterValue)
|
||||
? MessageFilter.Parse(MessageFilterValue)
|
||||
: MessageFilter.Null;
|
||||
|
||||
public bool ShouldFormatMarkdown { get; set; }
|
||||
|
||||
public bool ShouldDownloadAssets { get; set; }
|
||||
|
||||
public bool ShouldReuseAssets { get; set; }
|
||||
|
||||
public string? AssetsDirPath { get; set; }
|
||||
|
||||
public bool IsAdvancedSectionDisplayed { get; set; }
|
||||
|
||||
public ExportSetupViewModel(DialogManager dialogManager, SettingsService settingsService)
|
||||
[RelayCommand]
|
||||
private void Initialize()
|
||||
{
|
||||
_dialogManager = dialogManager;
|
||||
_settingsService = settingsService;
|
||||
|
||||
// Persist preferences
|
||||
SelectedFormat = _settingsService.LastExportFormat;
|
||||
PartitionLimitValue = _settingsService.LastPartitionLimitValue;
|
||||
MessageFilterValue = _settingsService.LastMessageFilterValue;
|
||||
ShouldFormatMarkdown = _settingsService.LastShouldFormatMarkdown;
|
||||
ShouldDownloadAssets = _settingsService.LastShouldDownloadAssets;
|
||||
ShouldReuseAssets = _settingsService.LastShouldReuseAssets;
|
||||
AssetsDirPath = _settingsService.LastAssetsDirPath;
|
||||
SelectedFormat = settingsService.LastExportFormat;
|
||||
PartitionLimitValue = settingsService.LastPartitionLimitValue;
|
||||
MessageFilterValue = settingsService.LastMessageFilterValue;
|
||||
ShouldFormatMarkdown = settingsService.LastShouldFormatMarkdown;
|
||||
ShouldDownloadAssets = settingsService.LastShouldDownloadAssets;
|
||||
ShouldReuseAssets = settingsService.LastShouldReuseAssets;
|
||||
AssetsDirPath = settingsService.LastAssetsDirPath;
|
||||
|
||||
// Show the "advanced options" section by default if any
|
||||
// of the advanced options are set to non-default values.
|
||||
|
@ -97,9 +119,8 @@ public class ExportSetupViewModel : DialogScreen
|
|||
|| !string.IsNullOrWhiteSpace(AssetsDirPath);
|
||||
}
|
||||
|
||||
public void ToggleAdvancedSection() => IsAdvancedSectionDisplayed = !IsAdvancedSectionDisplayed;
|
||||
|
||||
public void ShowOutputPathPrompt()
|
||||
[RelayCommand]
|
||||
private async Task ShowOutputPathPromptAsync()
|
||||
{
|
||||
if (IsSingleChannel)
|
||||
{
|
||||
|
@ -112,33 +133,43 @@ public class ExportSetupViewModel : DialogScreen
|
|||
);
|
||||
|
||||
var extension = SelectedFormat.GetFileExtension();
|
||||
var filter = $"{extension.ToUpperInvariant()} files|*.{extension}";
|
||||
|
||||
var path = _dialogManager.PromptSaveFilePath(filter, defaultFileName);
|
||||
var path = await dialogManager.PromptSaveFilePathAsync(
|
||||
[
|
||||
new FilePickerFileType($"{extension.ToUpperInvariant()} file")
|
||||
{
|
||||
Patterns = [$"*.{extension}"]
|
||||
}
|
||||
],
|
||||
defaultFileName
|
||||
);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(path))
|
||||
OutputPath = path;
|
||||
}
|
||||
else
|
||||
{
|
||||
var path = _dialogManager.PromptDirectoryPath();
|
||||
var path = await dialogManager.PromptDirectoryPathAsync();
|
||||
if (!string.IsNullOrWhiteSpace(path))
|
||||
OutputPath = path;
|
||||
}
|
||||
}
|
||||
|
||||
public void ShowAssetsDirPathPrompt()
|
||||
[RelayCommand]
|
||||
private async Task ShowAssetsDirPathPromptAsync()
|
||||
{
|
||||
var path = _dialogManager.PromptDirectoryPath();
|
||||
var path = await dialogManager.PromptDirectoryPathAsync();
|
||||
if (!string.IsNullOrWhiteSpace(path))
|
||||
AssetsDirPath = path;
|
||||
}
|
||||
|
||||
public void Confirm()
|
||||
[RelayCommand]
|
||||
private async Task ConfirmAsync()
|
||||
{
|
||||
// Prompt the output path if it's not set yet
|
||||
// Prompt the output path if it hasn't been set yet
|
||||
if (string.IsNullOrWhiteSpace(OutputPath))
|
||||
{
|
||||
ShowOutputPathPrompt();
|
||||
await ShowOutputPathPromptAsync();
|
||||
|
||||
// If the output path is still not set, cancel the export
|
||||
if (string.IsNullOrWhiteSpace(OutputPath))
|
||||
|
@ -146,31 +177,14 @@ public class ExportSetupViewModel : DialogScreen
|
|||
}
|
||||
|
||||
// Persist preferences
|
||||
_settingsService.LastExportFormat = SelectedFormat;
|
||||
_settingsService.LastPartitionLimitValue = PartitionLimitValue;
|
||||
_settingsService.LastMessageFilterValue = MessageFilterValue;
|
||||
_settingsService.LastShouldFormatMarkdown = ShouldFormatMarkdown;
|
||||
_settingsService.LastShouldDownloadAssets = ShouldDownloadAssets;
|
||||
_settingsService.LastShouldReuseAssets = ShouldReuseAssets;
|
||||
_settingsService.LastAssetsDirPath = AssetsDirPath;
|
||||
settingsService.LastExportFormat = SelectedFormat;
|
||||
settingsService.LastPartitionLimitValue = PartitionLimitValue;
|
||||
settingsService.LastMessageFilterValue = MessageFilterValue;
|
||||
settingsService.LastShouldFormatMarkdown = ShouldFormatMarkdown;
|
||||
settingsService.LastShouldDownloadAssets = ShouldDownloadAssets;
|
||||
settingsService.LastShouldReuseAssets = ShouldReuseAssets;
|
||||
settingsService.LastAssetsDirPath = AssetsDirPath;
|
||||
|
||||
Close(true);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ExportSetupViewModelExtensions
|
||||
{
|
||||
public static ExportSetupViewModel CreateExportSetupViewModel(
|
||||
this IViewModelFactory factory,
|
||||
Guild guild,
|
||||
IReadOnlyList<Channel> channels
|
||||
)
|
||||
{
|
||||
var viewModel = factory.CreateExportSetupViewModel();
|
||||
|
||||
viewModel.Guild = guild;
|
||||
viewModel.Channels = channels;
|
||||
|
||||
return viewModel;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,49 +1,29 @@
|
|||
using DiscordChatExporter.Gui.ViewModels.Framework;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using DiscordChatExporter.Gui.Framework;
|
||||
|
||||
namespace DiscordChatExporter.Gui.ViewModels.Dialogs;
|
||||
|
||||
public class MessageBoxViewModel : DialogScreen
|
||||
public partial class MessageBoxViewModel : DialogViewModelBase
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
[ObservableProperty]
|
||||
private string? _title = "Title";
|
||||
|
||||
public string? Message { get; set; }
|
||||
[ObservableProperty]
|
||||
private string? _message = "Message";
|
||||
|
||||
public bool IsOkButtonVisible { get; set; } = true;
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsDefaultButtonVisible))]
|
||||
[NotifyPropertyChangedFor(nameof(ButtonsCount))]
|
||||
private string? _defaultButtonText = "OK";
|
||||
|
||||
public string? OkButtonText { get; set; }
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsCancelButtonVisible))]
|
||||
[NotifyPropertyChangedFor(nameof(ButtonsCount))]
|
||||
private string? _cancelButtonText = "Cancel";
|
||||
|
||||
public bool IsCancelButtonVisible { get; set; }
|
||||
public bool IsDefaultButtonVisible => !string.IsNullOrWhiteSpace(DefaultButtonText);
|
||||
|
||||
public string? CancelButtonText { get; set; }
|
||||
public bool IsCancelButtonVisible => !string.IsNullOrWhiteSpace(CancelButtonText);
|
||||
|
||||
public int ButtonsCount => (IsOkButtonVisible ? 1 : 0) + (IsCancelButtonVisible ? 1 : 0);
|
||||
}
|
||||
|
||||
public static class MessageBoxViewModelExtensions
|
||||
{
|
||||
public static MessageBoxViewModel CreateMessageBoxViewModel(
|
||||
this IViewModelFactory factory,
|
||||
string title,
|
||||
string message,
|
||||
string? okButtonText,
|
||||
string? cancelButtonText
|
||||
)
|
||||
{
|
||||
var viewModel = factory.CreateMessageBoxViewModel();
|
||||
|
||||
viewModel.Title = title;
|
||||
viewModel.Message = message;
|
||||
viewModel.IsOkButtonVisible = !string.IsNullOrWhiteSpace(okButtonText);
|
||||
viewModel.OkButtonText = okButtonText;
|
||||
viewModel.IsCancelButtonVisible = !string.IsNullOrWhiteSpace(cancelButtonText);
|
||||
viewModel.CancelButtonText = cancelButtonText;
|
||||
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
public static MessageBoxViewModel CreateMessageBoxViewModel(
|
||||
this IViewModelFactory factory,
|
||||
string title,
|
||||
string message
|
||||
) => factory.CreateMessageBoxViewModel(title, message, "CLOSE", null);
|
||||
public int ButtonsCount => (IsDefaultButtonVisible ? 1 : 0) + (IsCancelButtonVisible ? 1 : 0);
|
||||
}
|
||||
|
|
|
@ -2,30 +2,43 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using DiscordChatExporter.Core.Utils.Extensions;
|
||||
using DiscordChatExporter.Gui.Framework;
|
||||
using DiscordChatExporter.Gui.Models;
|
||||
using DiscordChatExporter.Gui.Services;
|
||||
using DiscordChatExporter.Gui.ViewModels.Framework;
|
||||
using DiscordChatExporter.Gui.Utils;
|
||||
using DiscordChatExporter.Gui.Utils.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Gui.ViewModels.Dialogs;
|
||||
|
||||
public class SettingsViewModel(SettingsService settingsService) : DialogScreen
|
||||
public class SettingsViewModel : DialogViewModelBase
|
||||
{
|
||||
private readonly SettingsService _settingsService;
|
||||
|
||||
private readonly DisposableCollector _eventRoot = new();
|
||||
|
||||
public SettingsViewModel(SettingsService settingsService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
|
||||
_eventRoot.Add(_settingsService.WatchAllProperties(OnAllPropertiesChanged));
|
||||
}
|
||||
|
||||
public bool IsAutoUpdateEnabled
|
||||
{
|
||||
get => settingsService.IsAutoUpdateEnabled;
|
||||
set => settingsService.IsAutoUpdateEnabled = value;
|
||||
get => _settingsService.IsAutoUpdateEnabled;
|
||||
set => _settingsService.IsAutoUpdateEnabled = value;
|
||||
}
|
||||
|
||||
public bool IsDarkModeEnabled
|
||||
{
|
||||
get => settingsService.IsDarkModeEnabled;
|
||||
set => settingsService.IsDarkModeEnabled = value;
|
||||
get => _settingsService.IsDarkModeEnabled;
|
||||
set => _settingsService.IsDarkModeEnabled = value;
|
||||
}
|
||||
|
||||
public bool IsTokenPersisted
|
||||
{
|
||||
get => settingsService.IsTokenPersisted;
|
||||
set => settingsService.IsTokenPersisted = value;
|
||||
get => _settingsService.IsTokenPersisted;
|
||||
set => _settingsService.IsTokenPersisted = value;
|
||||
}
|
||||
|
||||
public IReadOnlyList<ThreadInclusionMode> AvailableThreadInclusions { get; } =
|
||||
|
@ -33,13 +46,13 @@ public class SettingsViewModel(SettingsService settingsService) : DialogScreen
|
|||
|
||||
public ThreadInclusionMode ThreadInclusionMode
|
||||
{
|
||||
get => settingsService.ThreadInclusionMode;
|
||||
set => settingsService.ThreadInclusionMode = value;
|
||||
get => _settingsService.ThreadInclusionMode;
|
||||
set => _settingsService.ThreadInclusionMode = value;
|
||||
}
|
||||
|
||||
// These items have to be non-nullable because WPF ComboBox doesn't allow a null value to be selected
|
||||
public IReadOnlyList<string> AvailableLocales { get; } = new[]
|
||||
{
|
||||
// These items have to be non-nullable because Avalonia ComboBox doesn't allow a null value to be selected
|
||||
public IReadOnlyList<string> AvailableLocales { get; } =
|
||||
[
|
||||
// Current locale (maps to null downstream)
|
||||
"",
|
||||
// Locales supported by the Discord app
|
||||
|
@ -72,25 +85,35 @@ public class SettingsViewModel(SettingsService settingsService) : DialogScreen
|
|||
"ja-JP",
|
||||
"zh-TW",
|
||||
"ko-KR"
|
||||
}.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
];
|
||||
|
||||
// This has to be non-nullable because WPF ComboBox doesn't allow a null value to be selected
|
||||
// This has to be non-nullable because Avalonia ComboBox doesn't allow a null value to be selected
|
||||
public string Locale
|
||||
{
|
||||
get => settingsService.Locale ?? "";
|
||||
get => _settingsService.Locale ?? "";
|
||||
// Important to reduce empty strings to nulls, because empty strings don't correspond to valid cultures
|
||||
set => settingsService.Locale = value.NullIfWhiteSpace();
|
||||
set => _settingsService.Locale = value.NullIfWhiteSpace();
|
||||
}
|
||||
|
||||
public bool IsUtcNormalizationEnabled
|
||||
{
|
||||
get => settingsService.IsUtcNormalizationEnabled;
|
||||
set => settingsService.IsUtcNormalizationEnabled = value;
|
||||
get => _settingsService.IsUtcNormalizationEnabled;
|
||||
set => _settingsService.IsUtcNormalizationEnabled = value;
|
||||
}
|
||||
|
||||
public int ParallelLimit
|
||||
{
|
||||
get => settingsService.ParallelLimit;
|
||||
set => settingsService.ParallelLimit = Math.Clamp(value, 1, 10);
|
||||
get => _settingsService.ParallelLimit;
|
||||
set => _settingsService.ParallelLimit = Math.Clamp(value, 1, 10);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_eventRoot.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using AsyncKeyedLock;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using Microsoft.Win32;
|
||||
using Stylet;
|
||||
|
||||
namespace DiscordChatExporter.Gui.ViewModels.Framework;
|
||||
|
||||
public class DialogManager(IViewManager viewManager) : IDisposable
|
||||
{
|
||||
private readonly AsyncNonKeyedLocker _dialogLock = new();
|
||||
|
||||
public async ValueTask<T?> ShowDialogAsync<T>(DialogScreen<T> dialogScreen)
|
||||
{
|
||||
var view = viewManager.CreateAndBindViewForModelIfNecessary(dialogScreen);
|
||||
|
||||
void OnDialogOpened(object? openSender, DialogOpenedEventArgs openArgs)
|
||||
{
|
||||
void OnScreenClosed(object? closeSender, EventArgs closeArgs)
|
||||
{
|
||||
try
|
||||
{
|
||||
openArgs.Session.Close();
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Race condition: dialog is already being closed
|
||||
}
|
||||
|
||||
dialogScreen.Closed -= OnScreenClosed;
|
||||
}
|
||||
dialogScreen.Closed += OnScreenClosed;
|
||||
}
|
||||
|
||||
using (await _dialogLock.LockAsync())
|
||||
{
|
||||
await DialogHost.Show(view, OnDialogOpened);
|
||||
return dialogScreen.DialogResult;
|
||||
}
|
||||
}
|
||||
|
||||
public string? PromptSaveFilePath(string filter = "All files|*.*", string defaultFilePath = "")
|
||||
{
|
||||
var dialog = new SaveFileDialog
|
||||
{
|
||||
Filter = filter,
|
||||
AddExtension = true,
|
||||
FileName = defaultFilePath,
|
||||
DefaultExt = Path.GetExtension(defaultFilePath)
|
||||
};
|
||||
|
||||
return dialog.ShowDialog() == true ? dialog.FileName : null;
|
||||
}
|
||||
|
||||
public string? PromptDirectoryPath(string defaultDirPath = "")
|
||||
{
|
||||
var dialog = new OpenFolderDialog { InitialDirectory = defaultDirPath };
|
||||
return dialog.ShowDialog() == true ? dialog.FolderName : null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_dialogLock.Dispose();
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
using System;
|
||||
using Stylet;
|
||||
|
||||
namespace DiscordChatExporter.Gui.ViewModels.Framework;
|
||||
|
||||
public abstract class DialogScreen<T> : PropertyChangedBase
|
||||
{
|
||||
public T? DialogResult { get; private set; }
|
||||
|
||||
public event EventHandler? Closed;
|
||||
|
||||
public void Close(T dialogResult)
|
||||
{
|
||||
DialogResult = dialogResult;
|
||||
Closed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class DialogScreen : DialogScreen<bool?>;
|
|
@ -1,16 +0,0 @@
|
|||
using DiscordChatExporter.Gui.ViewModels.Components;
|
||||
using DiscordChatExporter.Gui.ViewModels.Dialogs;
|
||||
|
||||
namespace DiscordChatExporter.Gui.ViewModels.Framework;
|
||||
|
||||
// Used to instantiate new view models while making use of dependency injection
|
||||
public interface IViewModelFactory
|
||||
{
|
||||
DashboardViewModel CreateDashboardViewModel();
|
||||
|
||||
ExportSetupViewModel CreateExportSetupViewModel();
|
||||
|
||||
MessageBoxViewModel CreateMessageBoxViewModel();
|
||||
|
||||
SettingsViewModel CreateSettingsViewModel();
|
||||
}
|
127
DiscordChatExporter.Gui/ViewModels/MainViewModel.cs
Normal file
127
DiscordChatExporter.Gui/ViewModels/MainViewModel.cs
Normal file
|
@ -0,0 +1,127 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using DiscordChatExporter.Gui.Framework;
|
||||
using DiscordChatExporter.Gui.Services;
|
||||
using DiscordChatExporter.Gui.Utils;
|
||||
using DiscordChatExporter.Gui.Utils.Extensions;
|
||||
using DiscordChatExporter.Gui.ViewModels.Components;
|
||||
|
||||
namespace DiscordChatExporter.Gui.ViewModels;
|
||||
|
||||
public partial class MainViewModel(
|
||||
ViewModelManager viewModelManager,
|
||||
DialogManager dialogManager,
|
||||
SnackbarManager snackbarManager,
|
||||
SettingsService settingsService,
|
||||
UpdateService updateService
|
||||
) : ViewModelBase
|
||||
{
|
||||
public string Title { get; } = $"{Program.Name} v{Program.VersionString}";
|
||||
|
||||
public DashboardViewModel Dashboard { get; } = viewModelManager.CreateDashboardViewModel();
|
||||
|
||||
private async Task ShowUkraineSupportMessageAsync()
|
||||
{
|
||||
if (!settingsService.IsUkraineSupportMessageEnabled)
|
||||
return;
|
||||
|
||||
var dialog = viewModelManager.CreateMessageBoxViewModel(
|
||||
"Thank you for supporting Ukraine!",
|
||||
"""
|
||||
As Russia wages a genocidal war against my country, I'm grateful to everyone who continues to stand with Ukraine in our fight for freedom.
|
||||
|
||||
Click LEARN MORE to find ways that you can help.
|
||||
""",
|
||||
"LEARN MORE",
|
||||
"CLOSE"
|
||||
);
|
||||
|
||||
// Disable this message in the future
|
||||
settingsService.IsUkraineSupportMessageEnabled = false;
|
||||
settingsService.Save();
|
||||
|
||||
if (await dialogManager.ShowDialogAsync(dialog) == true)
|
||||
ProcessEx.StartShellExecute("https://tyrrrz.me/ukraine?source=discordchatexporter");
|
||||
}
|
||||
|
||||
private async Task CheckForUpdatesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var updateVersion = await updateService.CheckForUpdatesAsync();
|
||||
if (updateVersion is null)
|
||||
return;
|
||||
|
||||
snackbarManager.Notify($"Downloading update to {Program.Name} v{updateVersion}...");
|
||||
await updateService.PrepareUpdateAsync(updateVersion);
|
||||
|
||||
snackbarManager.Notify(
|
||||
"Update has been downloaded and will be installed when you exit",
|
||||
"INSTALL NOW",
|
||||
() =>
|
||||
{
|
||||
updateService.FinalizeUpdate(true);
|
||||
|
||||
if (Application.Current?.ApplicationLifetime?.TryShutdown(2) != true)
|
||||
Environment.Exit(2);
|
||||
}
|
||||
);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Failure to update shouldn't crash the application
|
||||
snackbarManager.Notify("Failed to perform application update");
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task InitializeAsync()
|
||||
{
|
||||
// Reset settings (needed to resolve the default dark mode setting)
|
||||
settingsService.Reset();
|
||||
|
||||
// Load settings
|
||||
settingsService.Load();
|
||||
|
||||
// Set the correct theme
|
||||
if (settingsService.IsDarkModeEnabled)
|
||||
App.SetDarkTheme();
|
||||
else
|
||||
App.SetLightTheme();
|
||||
|
||||
await ShowUkraineSupportMessageAsync();
|
||||
await CheckForUpdatesAsync();
|
||||
|
||||
// App has just been updated, display the changelog
|
||||
if (
|
||||
settingsService.LastAppVersion is not null
|
||||
&& settingsService.LastAppVersion != Program.Version
|
||||
)
|
||||
{
|
||||
snackbarManager.Notify(
|
||||
$"Successfully updated to {Program.Name} v{Program.VersionString}",
|
||||
"WHAT'S NEW",
|
||||
() => ProcessEx.StartShellExecute(Program.LatestReleaseUrl)
|
||||
);
|
||||
|
||||
settingsService.LastAppVersion = Program.Version;
|
||||
settingsService.Save();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
// Save settings
|
||||
settingsService.Save();
|
||||
|
||||
// Finalize pending updates
|
||||
updateService.FinalizeUpdate(false);
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
namespace DiscordChatExporter.Gui.ViewModels.Messages;
|
||||
|
||||
public record NotificationMessage(string Text);
|
|
@ -1,147 +0,0 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using DiscordChatExporter.Gui.Services;
|
||||
using DiscordChatExporter.Gui.Utils;
|
||||
using DiscordChatExporter.Gui.ViewModels.Components;
|
||||
using DiscordChatExporter.Gui.ViewModels.Dialogs;
|
||||
using DiscordChatExporter.Gui.ViewModels.Framework;
|
||||
using DiscordChatExporter.Gui.ViewModels.Messages;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using Stylet;
|
||||
|
||||
namespace DiscordChatExporter.Gui.ViewModels;
|
||||
|
||||
public class RootViewModel : Screen, IHandle<NotificationMessage>, IDisposable
|
||||
{
|
||||
private readonly IViewModelFactory _viewModelFactory;
|
||||
private readonly DialogManager _dialogManager;
|
||||
private readonly SettingsService _settingsService;
|
||||
private readonly UpdateService _updateService;
|
||||
|
||||
public SnackbarMessageQueue Notifications { get; } = new(TimeSpan.FromSeconds(5));
|
||||
|
||||
public DashboardViewModel Dashboard { get; }
|
||||
|
||||
public RootViewModel(
|
||||
IViewModelFactory viewModelFactory,
|
||||
IEventAggregator eventAggregator,
|
||||
DialogManager dialogManager,
|
||||
SettingsService settingsService,
|
||||
UpdateService updateService
|
||||
)
|
||||
{
|
||||
_viewModelFactory = viewModelFactory;
|
||||
_dialogManager = dialogManager;
|
||||
_settingsService = settingsService;
|
||||
_updateService = updateService;
|
||||
|
||||
eventAggregator.Subscribe(this);
|
||||
|
||||
Dashboard = _viewModelFactory.CreateDashboardViewModel();
|
||||
|
||||
DisplayName = $"{App.Name} v{App.VersionString}";
|
||||
}
|
||||
|
||||
private async Task ShowUkraineSupportMessageAsync()
|
||||
{
|
||||
if (!_settingsService.IsUkraineSupportMessageEnabled)
|
||||
return;
|
||||
|
||||
var dialog = _viewModelFactory.CreateMessageBoxViewModel(
|
||||
"Thank you for supporting Ukraine!",
|
||||
"""
|
||||
As Russia wages a genocidal war against my country, I'm grateful to everyone who continues to stand with Ukraine in our fight for freedom.
|
||||
|
||||
Click LEARN MORE to find ways that you can help.
|
||||
""",
|
||||
"LEARN MORE",
|
||||
"CLOSE"
|
||||
);
|
||||
|
||||
// Disable this message in the future
|
||||
_settingsService.IsUkraineSupportMessageEnabled = false;
|
||||
_settingsService.Save();
|
||||
|
||||
if (await _dialogManager.ShowDialogAsync(dialog) == true)
|
||||
ProcessEx.StartShellExecute("https://tyrrrz.me/ukraine?source=discordchatexporter");
|
||||
}
|
||||
|
||||
private async ValueTask CheckForUpdatesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var updateVersion = await _updateService.CheckForUpdatesAsync();
|
||||
if (updateVersion is null)
|
||||
return;
|
||||
|
||||
Notifications.Enqueue($"Downloading update to {App.Name} v{updateVersion}...");
|
||||
await _updateService.PrepareUpdateAsync(updateVersion);
|
||||
|
||||
Notifications.Enqueue(
|
||||
"Update has been downloaded and will be installed when you exit",
|
||||
"INSTALL NOW",
|
||||
() =>
|
||||
{
|
||||
_updateService.FinalizeUpdate(true);
|
||||
RequestClose();
|
||||
}
|
||||
);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Failure to update shouldn't crash the application
|
||||
Notifications.Enqueue("Failed to perform application update");
|
||||
}
|
||||
}
|
||||
|
||||
public async void OnViewFullyLoaded()
|
||||
{
|
||||
await ShowUkraineSupportMessageAsync();
|
||||
await CheckForUpdatesAsync();
|
||||
}
|
||||
|
||||
protected override void OnViewLoaded()
|
||||
{
|
||||
base.OnViewLoaded();
|
||||
|
||||
_settingsService.Load();
|
||||
|
||||
// Sync the theme with settings
|
||||
if (_settingsService.IsDarkModeEnabled)
|
||||
{
|
||||
App.SetDarkTheme();
|
||||
}
|
||||
else
|
||||
{
|
||||
App.SetLightTheme();
|
||||
}
|
||||
|
||||
// App has just been updated, display the changelog
|
||||
if (
|
||||
_settingsService.LastAppVersion is not null
|
||||
&& _settingsService.LastAppVersion != App.Version
|
||||
)
|
||||
{
|
||||
Notifications.Enqueue(
|
||||
$"Successfully updated to {App.Name} v{App.VersionString}",
|
||||
"WHAT'S NEW",
|
||||
() => ProcessEx.StartShellExecute(App.LatestReleaseUrl)
|
||||
);
|
||||
|
||||
_settingsService.LastAppVersion = App.Version;
|
||||
_settingsService.Save();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnClose()
|
||||
{
|
||||
base.OnClose();
|
||||
|
||||
_settingsService.Save();
|
||||
_updateService.FinalizeUpdate(false);
|
||||
}
|
||||
|
||||
public void Handle(NotificationMessage message) => Notifications.Enqueue(message.Text);
|
||||
|
||||
public void Dispose() => Notifications.Dispose();
|
||||
}
|
363
DiscordChatExporter.Gui/Views/Components/DashboardView.axaml
Normal file
363
DiscordChatExporter.Gui/Views/Components/DashboardView.axaml
Normal file
|
@ -0,0 +1,363 @@
|
|||
<UserControl
|
||||
x:Class="DiscordChatExporter.Gui.Views.Components.DashboardView"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
|
||||
xmlns:components="clr-namespace:DiscordChatExporter.Gui.ViewModels.Components"
|
||||
xmlns:controls="clr-namespace:DiscordChatExporter.Gui.Views.Controls"
|
||||
xmlns:converters="clr-namespace:DiscordChatExporter.Gui.Converters"
|
||||
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
xmlns:materialStyles="clr-namespace:Material.Styles.Controls;assembly=Material.Styles"
|
||||
x:Name="UserControl"
|
||||
Loaded="UserControl_OnLoaded">
|
||||
<Design.DataContext>
|
||||
<components:DashboardViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<DockPanel>
|
||||
<!-- Header -->
|
||||
<StackPanel
|
||||
Background="{DynamicResource MaterialDarkBackgroundBrush}"
|
||||
DockPanel.Dock="Top"
|
||||
Orientation="Vertical">
|
||||
<Grid Margin="12,12,8,12" ColumnDefinitions="*,Auto">
|
||||
<materialStyles:Card Grid.Column="0">
|
||||
<!-- Token -->
|
||||
<TextBox
|
||||
x:Name="TokenValueTextBox"
|
||||
FontSize="16"
|
||||
PasswordChar="*"
|
||||
RevealPassword="{Binding $self.IsFocused}"
|
||||
Text="{Binding Token}"
|
||||
Theme="{DynamicResource SoloTextBox}"
|
||||
Watermark="Token">
|
||||
<TextBox.InnerLeftContent>
|
||||
<materialIcons:MaterialIcon
|
||||
Grid.Column="0"
|
||||
Width="24"
|
||||
Height="24"
|
||||
Margin="4,0,8,0"
|
||||
Foreground="{DynamicResource PrimaryHueMidBrush}"
|
||||
Kind="Key" />
|
||||
</TextBox.InnerLeftContent>
|
||||
<TextBox.InnerRightContent>
|
||||
<Button
|
||||
Grid.Column="2"
|
||||
Margin="8,0,0,0"
|
||||
Padding="4"
|
||||
Command="{Binding PullGuildsCommand}"
|
||||
IsDefault="True"
|
||||
Theme="{DynamicResource MaterialFlatButton}"
|
||||
ToolTip.Tip="Pull available servers and channels (Enter)">
|
||||
<materialIcons:MaterialIcon
|
||||
Width="24"
|
||||
Height="24"
|
||||
Kind="ArrowRight" />
|
||||
</Button>
|
||||
</TextBox.InnerRightContent>
|
||||
</TextBox>
|
||||
</materialStyles:Card>
|
||||
|
||||
<!-- Settings button -->
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
Margin="8,0,0,0"
|
||||
Padding="8"
|
||||
VerticalAlignment="Center"
|
||||
Command="{Binding ShowSettingsCommand}"
|
||||
Foreground="{DynamicResource MaterialDarkForegroundBrush}"
|
||||
Theme="{DynamicResource MaterialFlatButton}"
|
||||
ToolTip.Tip="Settings">
|
||||
<materialIcons:MaterialIcon
|
||||
Width="24"
|
||||
Height="24"
|
||||
Kind="Settings" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- Progress -->
|
||||
<ProgressBar
|
||||
Height="2"
|
||||
Background="Transparent"
|
||||
IsIndeterminate="{Binding IsProgressIndeterminate}"
|
||||
Value="{Binding Progress.Current.Fraction, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Body -->
|
||||
<Panel
|
||||
Background="{DynamicResource MaterialCardBackgroundBrush}"
|
||||
DockPanel.Dock="Bottom"
|
||||
IsEnabled="{Binding !IsBusy}">
|
||||
<Panel.Styles>
|
||||
<Style Selector="Panel">
|
||||
<Style Selector="^:disabled">
|
||||
<Setter Property="Opacity" Value="0.5" />
|
||||
</Style>
|
||||
</Style>
|
||||
</Panel.Styles>
|
||||
<!-- Placeholder / usage instructions -->
|
||||
<Panel IsVisible="{Binding !AvailableGuilds.Count}">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||
<TextBlock
|
||||
Margin="32,16"
|
||||
FontSize="14"
|
||||
FontWeight="Light"
|
||||
LineHeight="23">
|
||||
<!-- User token -->
|
||||
<InlineUIContainer>
|
||||
<materialIcons:MaterialIcon
|
||||
Width="18"
|
||||
Height="18"
|
||||
Margin="0,-2,0,0"
|
||||
Foreground="{DynamicResource PrimaryHueMidBrush}"
|
||||
Kind="Account" />
|
||||
</InlineUIContainer>
|
||||
<Run Text="" />
|
||||
<Run
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Text="To get the token for your personal account:" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="* Automating user accounts is technically against TOS —" />
|
||||
<Run FontWeight="SemiBold" Text="use at your own risk" /><Run Text="!" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="1. Open Discord in your" />
|
||||
<controls:HyperLink Command="{Binding OpenDiscordCommand}" Text="web browser" />
|
||||
<Run Text="and login" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="2. Open any server or direct message channel" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="3. Press" />
|
||||
<Run FontWeight="SemiBold" Text="Ctrl+Shift+I" />
|
||||
<Run Text="to show developer tools" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="4. Navigate to the" />
|
||||
<Run FontWeight="SemiBold" Text="Network" />
|
||||
<Run Text="tab" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="5. Press" />
|
||||
<Run FontWeight="SemiBold" Text="Ctrl+R" />
|
||||
<Run Text="to reload" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="6. Switch between random channels to trigger network requests" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="7. Search for a request that starts with" />
|
||||
<Run FontWeight="SemiBold" Text="messages" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="8. Select the" />
|
||||
<Run FontWeight="SemiBold" Text="Headers" />
|
||||
<Run Text="tab on the right" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="9. Scroll down to the" />
|
||||
<Run FontWeight="SemiBold" Text="Request Headers" />
|
||||
<Run Text="section" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="10. Copy the value of the" />
|
||||
<Run FontWeight="SemiBold" Text="authorization" />
|
||||
<Run Text="header" />
|
||||
<LineBreak />
|
||||
<LineBreak />
|
||||
|
||||
<!-- Bot token -->
|
||||
<InlineUIContainer>
|
||||
<materialIcons:MaterialIcon
|
||||
Width="18"
|
||||
Height="18"
|
||||
Margin="0,-2,0,0"
|
||||
Foreground="{DynamicResource PrimaryHueMidBrush}"
|
||||
Kind="Robot" />
|
||||
</InlineUIContainer>
|
||||
<Run Text="" />
|
||||
<Run
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Text="To get the token for your bot:" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="1. Open Discord" />
|
||||
<controls:HyperLink Command="{Binding OpenDiscordDeveloperPortalCommand}" Text="developer portal" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="2. Open your application's settings" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="3. Navigate to the" />
|
||||
<Run FontWeight="SemiBold" Text="Bot" />
|
||||
<Run Text="section on the left" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="4. Under" />
|
||||
<Run FontWeight="SemiBold" Text="Token" />
|
||||
<Run Text="click" />
|
||||
<Run FontWeight="SemiBold" Text="Copy" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="* Your bot needs to have the" />
|
||||
<Run FontWeight="SemiBold" Text="Message Content Intent" />
|
||||
<Run Text="enabled to read messages" />
|
||||
<LineBreak />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="If you have questions or issues, please refer to the" />
|
||||
<controls:HyperLink Command="{Binding ShowHelpCommand}" Text="documentation" />
|
||||
</TextBlock>
|
||||
</ScrollViewer>
|
||||
</Panel>
|
||||
|
||||
<!-- Guilds and channels -->
|
||||
<Grid ColumnDefinitions="Auto,*" IsVisible="{Binding !!AvailableGuilds.Count}">
|
||||
<!-- Guilds -->
|
||||
<Border
|
||||
Grid.Column="0"
|
||||
BorderBrush="{DynamicResource MaterialDividerBrush}"
|
||||
BorderThickness="0,0,1,0">
|
||||
<ListBox
|
||||
x:Name="AvailableGuildsListBox"
|
||||
ItemsSource="{Binding AvailableGuilds}"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Hidden"
|
||||
SelectedItem="{Binding SelectedGuild}"
|
||||
SelectionChanged="AvailableGuildsListBox_OnSelectionChanged"
|
||||
SelectionMode="Single">
|
||||
<ListBox.Styles>
|
||||
<Style Selector="ListBox">
|
||||
<Style Selector="^ ListBoxItem">
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
</Style>
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Panel Background="Transparent" ToolTip.Tip="{Binding Name}">
|
||||
<!-- Guild icon placeholder -->
|
||||
<Ellipse
|
||||
Width="48"
|
||||
Height="48"
|
||||
Margin="12"
|
||||
Fill="{DynamicResource MaterialDividerBrush}" />
|
||||
|
||||
<!-- Guild icon -->
|
||||
<Ellipse
|
||||
Width="48"
|
||||
Height="48"
|
||||
Margin="12">
|
||||
<Ellipse.Fill>
|
||||
<ImageBrush asyncImageLoader:ImageBrushLoader.Source="{Binding IconUrl}" />
|
||||
</Ellipse.Fill>
|
||||
</Ellipse>
|
||||
</Panel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Border>
|
||||
|
||||
<!-- Channels -->
|
||||
<Border Grid.Column="1">
|
||||
<TreeView
|
||||
x:Name="AvailableChannelsTreeView"
|
||||
AutoScrollToSelectedItem="False"
|
||||
ItemsSource="{Binding AvailableChannels}"
|
||||
SelectedItems="{Binding SelectedChannels}"
|
||||
SelectionChanged="AvailableChannelsTreeView_OnSelectionChanged"
|
||||
SelectionMode="Multiple"
|
||||
TextSearch.Text="Name">
|
||||
<TreeView.Styles>
|
||||
<Style Selector="TreeView">
|
||||
<Style Selector="^ TreeViewItem">
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
</Style>
|
||||
</Style>
|
||||
</TreeView.Styles>
|
||||
<TreeView.ItemTemplate>
|
||||
<TreeDataTemplate ItemsSource="{Binding Children}">
|
||||
<Grid
|
||||
Background="Transparent"
|
||||
Classes.category="{Binding Channel.IsCategory}"
|
||||
ColumnDefinitions="Auto,*,Auto">
|
||||
<Grid.Styles>
|
||||
<Style Selector="Grid">
|
||||
<Style Selector="^:not(.category)">
|
||||
<Setter Property="ToolTip.Tip">
|
||||
<Template>
|
||||
<TextBlock>
|
||||
<Run Text="Last message sent:" />
|
||||
<Run FontWeight="SemiBold" Text="{Binding Channel.LastMessageId, Converter={x:Static converters:SnowflakeToTimestampStringConverter.Instance}, TargetNullValue=never, Mode=OneWay}" />
|
||||
</TextBlock>
|
||||
</Template>
|
||||
</Setter>
|
||||
</Style>
|
||||
</Style>
|
||||
</Grid.Styles>
|
||||
|
||||
<!-- Channel icon -->
|
||||
<materialIcons:MaterialIcon
|
||||
Grid.Column="0"
|
||||
Margin="0,0,4,0"
|
||||
Classes.voice="{Binding Channel.IsVoice}"
|
||||
IsVisible="{Binding !Channel.IsCategory}">
|
||||
<materialIcons:MaterialIcon.Styles>
|
||||
<Style Selector="materialIcons|MaterialIcon">
|
||||
<Setter Property="Kind" Value="Pound" />
|
||||
|
||||
<Style Selector="^.voice">
|
||||
<Setter Property="Kind" Value="VolumeHigh" />
|
||||
</Style>
|
||||
</Style>
|
||||
</materialIcons:MaterialIcon.Styles>
|
||||
</materialIcons:MaterialIcon>
|
||||
|
||||
<!-- Channel name -->
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Margin="0,12"
|
||||
FontSize="14"
|
||||
Text="{Binding Channel.Name, Mode=OneWay}" />
|
||||
|
||||
<!-- Checkmark -->
|
||||
<materialIcons:MaterialIcon
|
||||
Grid.Column="2"
|
||||
Width="24"
|
||||
Height="24"
|
||||
Margin="16,0"
|
||||
IsVisible="{Binding $parent[TreeViewItem].IsSelected}"
|
||||
Kind="Check" />
|
||||
</Grid>
|
||||
</TreeDataTemplate>
|
||||
</TreeView.ItemTemplate>
|
||||
</TreeView>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Export button -->
|
||||
<Button
|
||||
Width="56"
|
||||
Height="56"
|
||||
Margin="32,24"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Background="{DynamicResource MaterialSecondaryMidBrush}"
|
||||
Command="{Binding ExportCommand}"
|
||||
Foreground="{DynamicResource MaterialSecondaryMidForegroundBrush}"
|
||||
IsVisible="{Binding $self.IsEffectivelyEnabled}"
|
||||
Theme="{DynamicResource MaterialIconButton}">
|
||||
<materialIcons:MaterialIcon
|
||||
Width="32"
|
||||
Height="32"
|
||||
Kind="Download" />
|
||||
</Button>
|
||||
</Panel>
|
||||
</DockPanel>
|
||||
</UserControl>
|
|
@ -0,0 +1,37 @@
|
|||
using System.Linq;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using DiscordChatExporter.Core.Discord.Data;
|
||||
using DiscordChatExporter.Gui.Framework;
|
||||
using DiscordChatExporter.Gui.ViewModels.Components;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Views.Components;
|
||||
|
||||
public partial class DashboardView : UserControl<DashboardViewModel>
|
||||
{
|
||||
public DashboardView() => InitializeComponent();
|
||||
|
||||
private void UserControl_OnLoaded(object? sender, RoutedEventArgs args)
|
||||
{
|
||||
DataContext.InitializeCommand.Execute(null);
|
||||
TokenValueTextBox.Focus();
|
||||
}
|
||||
|
||||
private void AvailableGuildsListBox_OnSelectionChanged(
|
||||
object? sender,
|
||||
SelectionChangedEventArgs args
|
||||
) => DataContext.PullChannelsCommand.Execute(null);
|
||||
|
||||
private void AvailableChannelsTreeView_OnSelectionChanged(
|
||||
object? sender,
|
||||
SelectionChangedEventArgs args
|
||||
)
|
||||
{
|
||||
// Hack: unselect categories because they cannot be exported
|
||||
foreach (var item in args.AddedItems.OfType<ChannelNode>().Where(x => x.Channel.IsCategory))
|
||||
{
|
||||
if (AvailableChannelsTreeView.TreeContainerFromItem(item) is TreeViewItem container)
|
||||
container.IsSelected = false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,440 +0,0 @@
|
|||
<UserControl
|
||||
x:Class="DiscordChatExporter.Gui.Views.Components.DashboardView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
|
||||
xmlns:behaviors="clr-namespace:DiscordChatExporter.Gui.Behaviors"
|
||||
xmlns:componentModel="clr-namespace:System.ComponentModel;assembly=WindowsBase"
|
||||
xmlns:components="clr-namespace:DiscordChatExporter.Gui.ViewModels.Components"
|
||||
xmlns:controls="clr-namespace:DiscordChatExporter.Gui.Views.Controls"
|
||||
xmlns:converters="clr-namespace:DiscordChatExporter.Gui.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:data="clr-namespace:DiscordChatExporter.Core.Discord.Data;assembly=DiscordChatExporter.Core"
|
||||
xmlns:globalization="clr-namespace:System.Globalization;assembly=System.Runtime"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
d:DataContext="{d:DesignInstance Type=components:DashboardViewModel}"
|
||||
FocusManager.FocusedElement="{Binding ElementName=TokenValueTextBox}"
|
||||
Loaded="{s:Action OnViewLoaded}"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources>
|
||||
<!-- Collection view for DM channels -->
|
||||
<CollectionViewSource x:Key="AvailableDirectChannelsViewSource" Source="{Binding AvailableChannels, Mode=OneWay}">
|
||||
<CollectionViewSource.SortDescriptions>
|
||||
<componentModel:SortDescription Direction="Descending" PropertyName="LastMessageId" />
|
||||
<componentModel:SortDescription Direction="Ascending" PropertyName="Name" />
|
||||
</CollectionViewSource.SortDescriptions>
|
||||
</CollectionViewSource>
|
||||
<!-- Collection view for guild channels -->
|
||||
<CollectionViewSource x:Key="AvailableChannelsViewSource" Source="{Binding AvailableChannels, Mode=OneWay}">
|
||||
<CollectionViewSource.GroupDescriptions>
|
||||
<PropertyGroupDescription Converter="{x:Static converters:ChannelToGroupKeyConverter.Instance}" />
|
||||
</CollectionViewSource.GroupDescriptions>
|
||||
<CollectionViewSource.SortDescriptions>
|
||||
<componentModel:SortDescription Direction="Ascending" PropertyName="IsThread" />
|
||||
<componentModel:SortDescription Direction="Ascending" PropertyName="Position" />
|
||||
<componentModel:SortDescription Direction="Ascending" PropertyName="Name" />
|
||||
</CollectionViewSource.SortDescriptions>
|
||||
</CollectionViewSource>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<Grid Grid.Row="0" Background="{DynamicResource MaterialDesignDarkBackground}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<materialDesign:Card
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Margin="12,12,0,12">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Token icon -->
|
||||
<materialDesign:PackIcon
|
||||
Grid.Column="0"
|
||||
Width="24"
|
||||
Height="24"
|
||||
Margin="8"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryHueMidBrush}"
|
||||
Kind="Key" />
|
||||
|
||||
<!-- Token -->
|
||||
<controls:RevealablePasswordBox
|
||||
x:Name="TokenValueTextBox"
|
||||
Grid.Column="1"
|
||||
Margin="0,6,6,8"
|
||||
VerticalAlignment="Bottom"
|
||||
materialDesign:HintAssist.Hint="Token"
|
||||
BorderThickness="0"
|
||||
FontFamily="Consolas"
|
||||
FontSize="16"
|
||||
Password="{Binding Token, UpdateSourceTrigger=PropertyChanged}">
|
||||
<controls:RevealablePasswordBox.Style>
|
||||
<Style TargetType="{x:Type controls:RevealablePasswordBox}">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsMouseOver, RelativeSource={RelativeSource AncestorType=materialDesign:Card}}" Value="True">
|
||||
<Setter Property="IsRevealed" Value="True" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding IsKeyboardFocusWithin, RelativeSource={RelativeSource AncestorType=materialDesign:Card}}" Value="True">
|
||||
<Setter Property="IsRevealed" Value="True" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</controls:RevealablePasswordBox.Style>
|
||||
</controls:RevealablePasswordBox>
|
||||
|
||||
<!-- Pull guilds button -->
|
||||
<Button
|
||||
Grid.Column="2"
|
||||
Margin="0,6,6,6"
|
||||
Padding="4"
|
||||
Command="{s:Action PullGuilds}"
|
||||
IsDefault="True"
|
||||
Style="{DynamicResource MaterialDesignFlatButton}"
|
||||
ToolTip="Pull available guilds and channels (Enter)">
|
||||
<materialDesign:PackIcon
|
||||
Width="24"
|
||||
Height="24"
|
||||
Kind="ArrowRight" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</materialDesign:Card>
|
||||
|
||||
<!-- Settings button -->
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
Margin="6"
|
||||
Padding="4"
|
||||
Command="{s:Action ShowSettings}"
|
||||
Foreground="{DynamicResource MaterialDesignDarkForeground}"
|
||||
Style="{DynamicResource MaterialDesignFlatButton}"
|
||||
ToolTip="Settings">
|
||||
<Button.Resources>
|
||||
<SolidColorBrush x:Key="MaterialDesignFlatButtonClick" Color="#4C4C4C" />
|
||||
</Button.Resources>
|
||||
<materialDesign:PackIcon
|
||||
Width="24"
|
||||
Height="24"
|
||||
Kind="Settings" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- Progress bar -->
|
||||
<ProgressBar
|
||||
Grid.Row="1"
|
||||
Background="{DynamicResource MaterialDesignDarkBackground}"
|
||||
IsIndeterminate="{Binding IsProgressIndeterminate}"
|
||||
Value="{Binding Progress.Current.Fraction, Mode=OneWay}" />
|
||||
|
||||
<!-- Content -->
|
||||
<Grid
|
||||
Grid.Row="2"
|
||||
Background="{DynamicResource MaterialDesignCardBackground}"
|
||||
IsEnabled="{Binding IsBusy, Converter={x:Static converters:InverseBoolConverter.Instance}}">
|
||||
<!-- Placeholder / usage instructions -->
|
||||
<Grid Visibility="{Binding AvailableGuilds, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}}">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||
<TextBlock
|
||||
Margin="32,16"
|
||||
FontSize="14"
|
||||
FontWeight="Light"
|
||||
LineHeight="20">
|
||||
<!-- User token -->
|
||||
<InlineUIContainer>
|
||||
<materialDesign:PackIcon
|
||||
Margin="0,0,2,-2"
|
||||
Foreground="{DynamicResource PrimaryHueMidBrush}"
|
||||
Kind="Account" />
|
||||
</InlineUIContainer>
|
||||
<Run
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Text="To get the token for your personal account:" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="* Automating user accounts is technically against TOS —" />
|
||||
<Run FontWeight="SemiBold" Text="use at your own risk" /><Run Text="!" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="1. Open Discord in your" />
|
||||
<Hyperlink Command="{s:Action OpenDiscord}">
|
||||
<Run Text="web browser" />
|
||||
</Hyperlink>
|
||||
<Run Text="and login" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="2. Open any server or direct message channel" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="3. Press" />
|
||||
<Run FontWeight="SemiBold" Text="Ctrl+Shift+I" />
|
||||
<Run Text="to show developer tools" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="4. Navigate to the" />
|
||||
<Run FontWeight="SemiBold" Text="Network" />
|
||||
<Run Text="tab" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="5. Press" />
|
||||
<Run FontWeight="SemiBold" Text="Ctrl+R" />
|
||||
<Run Text="to reload" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="6. Switch between random channels to trigger network requests" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="7. Search for a request that starts with" />
|
||||
<Run FontWeight="SemiBold" Text="messages" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="8. Select the" />
|
||||
<Run FontWeight="SemiBold" Text="Headers" />
|
||||
<Run Text="tab on the right" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="9. Scroll down to the" />
|
||||
<Run FontWeight="SemiBold" Text="Request Headers" />
|
||||
<Run Text="section" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="10. Copy the value of the" />
|
||||
<Run FontWeight="SemiBold" Text="authorization" />
|
||||
<Run Text="header" />
|
||||
<LineBreak />
|
||||
<LineBreak />
|
||||
|
||||
<!-- Bot token -->
|
||||
<InlineUIContainer>
|
||||
<materialDesign:PackIcon
|
||||
Margin="0,0,2,-2"
|
||||
Foreground="{DynamicResource PrimaryHueMidBrush}"
|
||||
Kind="Robot" />
|
||||
</InlineUIContainer>
|
||||
<Run
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Text="To get the token for your bot:" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="1. Open Discord" />
|
||||
<Hyperlink Command="{s:Action OpenDiscordDeveloperPortal}">
|
||||
<Run Text="developer portal" />
|
||||
</Hyperlink>
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="2. Open your application's settings" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="3. Navigate to the" />
|
||||
<Run FontWeight="SemiBold" Text="Bot" />
|
||||
<Run Text="section on the left" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="4. Under" />
|
||||
<Run FontWeight="SemiBold" Text="Token" />
|
||||
<Run Text="click" />
|
||||
<Run FontWeight="SemiBold" Text="Copy" />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="* Your bot needs to have the" />
|
||||
<Run FontWeight="SemiBold" Text="Message Content Intent" />
|
||||
<Run Text="enabled to read messages" />
|
||||
<LineBreak />
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="If you have questions or issues, please refer to the" />
|
||||
<Hyperlink Command="{s:Action ShowHelp}">documentation</Hyperlink>
|
||||
</TextBlock>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
<!-- Guilds and channels -->
|
||||
<Grid Background="{DynamicResource MaterialDesignCardBackground}" Visibility="{Binding AvailableGuilds, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Guilds -->
|
||||
<Border
|
||||
Grid.Column="0"
|
||||
BorderBrush="{DynamicResource MaterialDesignDivider}"
|
||||
BorderThickness="0,0,1,0">
|
||||
<ListBox
|
||||
ItemsSource="{Binding AvailableGuilds}"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Hidden"
|
||||
SelectedItem="{Binding SelectedGuild}"
|
||||
SelectionMode="Single">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid
|
||||
Margin="-8"
|
||||
Background="Transparent"
|
||||
Cursor="Hand"
|
||||
MouseLeftButtonUp="{s:Action PullChannels}"
|
||||
ToolTip="{Binding Name}">
|
||||
<!-- Guild icon placeholder -->
|
||||
<Ellipse
|
||||
Width="48"
|
||||
Height="48"
|
||||
Margin="12,4,12,4"
|
||||
Fill="{DynamicResource MaterialDesignDivider}" />
|
||||
|
||||
<!-- Guild icon -->
|
||||
<Ellipse
|
||||
Width="48"
|
||||
Height="48"
|
||||
Margin="12,4,12,4"
|
||||
Stroke="{DynamicResource MaterialDesignDivider}"
|
||||
StrokeThickness="1">
|
||||
<Ellipse.Fill>
|
||||
<ImageBrush ImageSource="{Binding IconUrl}" />
|
||||
</Ellipse.Fill>
|
||||
</Ellipse>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Border>
|
||||
|
||||
<!-- Channels -->
|
||||
<Border Grid.Column="1">
|
||||
<ListBox
|
||||
HorizontalContentAlignment="Stretch"
|
||||
SelectionMode="Extended"
|
||||
TextSearch.TextPath="Name"
|
||||
VirtualizingPanel.IsVirtualizingWhenGrouping="True">
|
||||
<b:Interaction.Behaviors>
|
||||
<behaviors:ChannelMultiSelectionListBoxBehavior SelectedItems="{Binding SelectedChannels}" />
|
||||
</b:Interaction.Behaviors>
|
||||
<ListBox.Style>
|
||||
<Style BasedOn="{StaticResource {x:Type ListBox}}" TargetType="{x:Type ListBox}">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding SelectedGuild.IsDirect}" Value="True">
|
||||
<Setter Property="ItemsSource" Value="{Binding Source={StaticResource AvailableDirectChannelsViewSource}}" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding SelectedGuild.IsDirect}" Value="False">
|
||||
<Setter Property="ItemsSource" Value="{Binding Source={StaticResource AvailableChannelsViewSource}}" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</ListBox.Style>
|
||||
<ListBox.GroupStyle>
|
||||
<GroupStyle>
|
||||
<GroupStyle.ContainerStyle>
|
||||
<Style TargetType="{x:Type GroupItem}">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate d:DataContext="{x:Type CollectionViewGroup}">
|
||||
<Expander
|
||||
Margin="0"
|
||||
Padding="0"
|
||||
Background="Transparent"
|
||||
BorderBrush="{DynamicResource MaterialDesignDivider}"
|
||||
BorderThickness="0,0,0,1"
|
||||
Header="{Binding Name}"
|
||||
IsExpanded="False">
|
||||
<ItemsPresenter />
|
||||
</Expander>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</GroupStyle.ContainerStyle>
|
||||
</GroupStyle>
|
||||
</ListBox.GroupStyle>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type data:Channel}">
|
||||
<Grid Margin="-8" Background="Transparent">
|
||||
<Grid.InputBindings>
|
||||
<MouseBinding Command="{s:Action Export}" MouseAction="LeftDoubleClick" />
|
||||
</Grid.InputBindings>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.ToolTip>
|
||||
<TextBlock>
|
||||
<Run Text="Last message:" />
|
||||
<Run FontWeight="SemiBold" Text="{Binding LastMessageId, Converter={x:Static converters:SnowflakeToDateTimeOffsetConverter.Instance}, ConverterCulture={x:Static globalization:CultureInfo.CurrentCulture}, TargetNullValue=never}" />
|
||||
</TextBlock>
|
||||
</Grid.ToolTip>
|
||||
|
||||
<!-- Channel icon -->
|
||||
<materialDesign:PackIcon
|
||||
Grid.Column="0"
|
||||
Margin="16,7,0,6"
|
||||
VerticalAlignment="Center">
|
||||
<materialDesign:PackIcon.Style>
|
||||
<Style TargetType="{x:Type materialDesign:PackIcon}">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsVoice}" Value="True">
|
||||
<Setter Property="Kind" Value="VolumeHigh" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding IsVoice}" Value="False">
|
||||
<Setter Property="Kind" Value="Pound" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</materialDesign:PackIcon.Style>
|
||||
</materialDesign:PackIcon>
|
||||
|
||||
<!-- Channel name -->
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Margin="3,8,8,8"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
Text="{Binding Name, Mode=OneWay}" />
|
||||
|
||||
<!-- Is selected checkmark -->
|
||||
<materialDesign:PackIcon
|
||||
Grid.Column="2"
|
||||
Width="24"
|
||||
Height="24"
|
||||
Margin="8,0"
|
||||
VerticalAlignment="Center"
|
||||
Kind="Check"
|
||||
Visibility="{Binding IsSelected, RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Export button -->
|
||||
<Button
|
||||
Margin="32,24"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Command="{s:Action Export}"
|
||||
Style="{DynamicResource MaterialDesignFloatingActionAccentButton}"
|
||||
Visibility="{Binding CanExport, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
|
||||
<materialDesign:PackIcon
|
||||
Width="32"
|
||||
Height="32"
|
||||
Kind="Download" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
|
@ -1,9 +0,0 @@
|
|||
namespace DiscordChatExporter.Gui.Views.Components;
|
||||
|
||||
public partial class DashboardView
|
||||
{
|
||||
public DashboardView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
19
DiscordChatExporter.Gui/Views/Controls/HyperLink.axaml
Normal file
19
DiscordChatExporter.Gui/Views/Controls/HyperLink.axaml
Normal file
|
@ -0,0 +1,19 @@
|
|||
<UserControl
|
||||
x:Class="DiscordChatExporter.Gui.Views.Controls.HyperLink"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<TextBlock
|
||||
x:Name="TextBlock"
|
||||
Cursor="Hand"
|
||||
Foreground="{DynamicResource MaterialSecondaryDarkBrush}"
|
||||
PointerReleased="TextBlock_OnPointerReleased"
|
||||
Text="{Binding $parent[UserControl].Text, Mode=OneWay}">
|
||||
<TextBlock.Styles>
|
||||
<Style Selector="TextBlock">
|
||||
<Style Selector="^:pointerover">
|
||||
<Setter Property="TextDecorations" Value="Underline" />
|
||||
</Style>
|
||||
</Style>
|
||||
</TextBlock.Styles>
|
||||
</TextBlock>
|
||||
</UserControl>
|
49
DiscordChatExporter.Gui/Views/Controls/HyperLink.axaml.cs
Normal file
49
DiscordChatExporter.Gui/Views/Controls/HyperLink.axaml.cs
Normal file
|
@ -0,0 +1,49 @@
|
|||
using System.Windows.Input;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Views.Controls;
|
||||
|
||||
public partial class HyperLink : UserControl
|
||||
{
|
||||
public static readonly StyledProperty<string?> TextProperty =
|
||||
TextBlock.TextProperty.AddOwner<HyperLink>();
|
||||
|
||||
public static readonly StyledProperty<ICommand?> CommandProperty =
|
||||
Button.CommandProperty.AddOwner<HyperLink>();
|
||||
|
||||
public static readonly StyledProperty<object?> CommandParameterProperty =
|
||||
Button.CommandParameterProperty.AddOwner<HyperLink>();
|
||||
|
||||
public HyperLink() => InitializeComponent();
|
||||
|
||||
public string? Text
|
||||
{
|
||||
get => GetValue(TextProperty);
|
||||
set => SetValue(TextProperty, value);
|
||||
}
|
||||
|
||||
public ICommand? Command
|
||||
{
|
||||
get => GetValue(CommandProperty);
|
||||
set => SetValue(CommandProperty, value);
|
||||
}
|
||||
|
||||
public object? CommandParameter
|
||||
{
|
||||
get => GetValue(CommandParameterProperty);
|
||||
set => SetValue(CommandParameterProperty, value);
|
||||
}
|
||||
|
||||
private void TextBlock_OnPointerReleased(object? sender, PointerReleasedEventArgs args)
|
||||
{
|
||||
if (Command is null)
|
||||
return;
|
||||
|
||||
if (!Command.CanExecute(CommandParameter))
|
||||
return;
|
||||
|
||||
Command.Execute(CommandParameter);
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
<UserControl
|
||||
x:Class="DiscordChatExporter.Gui.Views.Controls.RevealablePasswordBox"
|
||||
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:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
x:Name="Root"
|
||||
mc:Ignorable="d">
|
||||
<Grid>
|
||||
<TextBox
|
||||
materialDesign:TextFieldAssist.DecorationVisibility="Hidden"
|
||||
BorderThickness="{Binding BorderThickness, ElementName=Root}"
|
||||
Text="{Binding Password, ElementName=Root, UpdateSourceTrigger=PropertyChanged}"
|
||||
Visibility="{Binding IsRevealed, ElementName=Root, Converter={x:Static s:BoolToVisibilityConverter.Instance}}" />
|
||||
<PasswordBox
|
||||
materialDesign:PasswordBoxAssist.Password="{Binding Password, ElementName=Root, UpdateSourceTrigger=PropertyChanged}"
|
||||
materialDesign:TextFieldAssist.DecorationVisibility="Hidden"
|
||||
BorderThickness="{Binding BorderThickness, ElementName=Root}"
|
||||
IsEnabled="False"
|
||||
Visibility="{Binding IsRevealed, ElementName=Root, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}}" />
|
||||
</Grid>
|
||||
</UserControl>
|
|
@ -1,40 +0,0 @@
|
|||
using System.Windows;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Views.Controls;
|
||||
|
||||
public partial class RevealablePasswordBox
|
||||
{
|
||||
public static readonly DependencyProperty PasswordProperty = DependencyProperty.Register(
|
||||
nameof(Password),
|
||||
typeof(string),
|
||||
typeof(RevealablePasswordBox),
|
||||
new FrameworkPropertyMetadata(
|
||||
string.Empty,
|
||||
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault
|
||||
)
|
||||
);
|
||||
|
||||
public static readonly DependencyProperty IsRevealedProperty = DependencyProperty.Register(
|
||||
nameof(IsRevealed),
|
||||
typeof(bool),
|
||||
typeof(RevealablePasswordBox),
|
||||
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.None)
|
||||
);
|
||||
|
||||
public string Password
|
||||
{
|
||||
get => (string)GetValue(PasswordProperty);
|
||||
set => SetValue(PasswordProperty, value);
|
||||
}
|
||||
|
||||
public bool IsRevealed
|
||||
{
|
||||
get => (bool)GetValue(IsRevealedProperty);
|
||||
set => SetValue(IsRevealedProperty, value);
|
||||
}
|
||||
|
||||
public RevealablePasswordBox()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
343
DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.axaml
Normal file
343
DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.axaml
Normal file
|
@ -0,0 +1,343 @@
|
|||
<UserControl
|
||||
x:Class="DiscordChatExporter.Gui.Views.Dialogs.ExportSetupView"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
|
||||
xmlns:converters="clr-namespace:DiscordChatExporter.Gui.Converters"
|
||||
xmlns:dialogs="clr-namespace:DiscordChatExporter.Gui.ViewModels.Dialogs"
|
||||
xmlns:materialAssists="clr-namespace:Material.Styles.Assists;assembly=Material.Styles"
|
||||
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
xmlns:utils="clr-namespace:DiscordChatExporter.Gui.Utils"
|
||||
x:Name="UserControl"
|
||||
Width="380"
|
||||
Loaded="UserControl_OnLoaded">
|
||||
<Design.DataContext>
|
||||
<dialogs:ExportSetupViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
<!-- Guild/channel info -->
|
||||
<Grid
|
||||
Grid.Row="0"
|
||||
Margin="16"
|
||||
ColumnDefinitions="Auto,*">
|
||||
<!-- Guild icon -->
|
||||
<Ellipse
|
||||
Grid.Column="0"
|
||||
Width="32"
|
||||
Height="32">
|
||||
<Ellipse.Fill>
|
||||
<ImageBrush asyncImageLoader:ImageBrushLoader.Source="{Binding Guild.IconUrl}" />
|
||||
</Ellipse.Fill>
|
||||
</Ellipse>
|
||||
|
||||
<!-- Channel count (for multiple channels) -->
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="19"
|
||||
FontWeight="Light"
|
||||
IsVisible="{Binding !IsSingleChannel}"
|
||||
TextTrimming="CharacterEllipsis">
|
||||
<Run Text="{Binding Channels.Count, Mode=OneWay}" />
|
||||
<Run Text="channels selected" />
|
||||
</TextBlock>
|
||||
|
||||
<!-- Category and channel name (for single channel) -->
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="19"
|
||||
FontWeight="Light"
|
||||
IsVisible="{Binding IsSingleChannel}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
ToolTip.Tip="{Binding Channels[0], Converter={x:Static converters:ChannelToHierarchicalNameStringConverter.Instance}}">
|
||||
<TextBlock IsVisible="{Binding !!Channels[0].Parent}">
|
||||
<Run Text="{Binding Channels[0].Parent.Name, Mode=OneWay}" />
|
||||
<Run Text="/" />
|
||||
</TextBlock>
|
||||
<Run FontWeight="SemiBold" Text="{Binding Channels[0].Name, Mode=OneWay}" />
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
Padding="0,8"
|
||||
BorderBrush="{DynamicResource MaterialDividerBrush}"
|
||||
BorderThickness="0,1">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<!-- Output path -->
|
||||
<TextBox
|
||||
Margin="16,8"
|
||||
materialAssists:TextFieldAssist.Label="Output path"
|
||||
Text="{Binding OutputPath}"
|
||||
Theme="{DynamicResource FilledTextBox}">
|
||||
<ToolTip.Tip>
|
||||
<TextBlock>
|
||||
<Run Text="Output file or directory path." />
|
||||
<LineBreak />
|
||||
<Run Text="If a directory is specified, file names will be generated automatically based on the channel names and export parameters." />
|
||||
<LineBreak />
|
||||
<Run Text="Directory paths must end with a slash to avoid ambiguity." />
|
||||
<LineBreak />
|
||||
<LineBreak />
|
||||
<Run Text="Available template tokens:" />
|
||||
<LineBreak />
|
||||
<Run Text=" " />
|
||||
<Run FontWeight="SemiBold" Text="%g" />
|
||||
<Run Text="— server ID" />
|
||||
<LineBreak />
|
||||
<Run Text=" " />
|
||||
<Run FontWeight="SemiBold" Text="%G" />
|
||||
<Run Text="— server name" />
|
||||
<LineBreak />
|
||||
<Run Text=" " />
|
||||
<Run FontWeight="SemiBold" Text="%t" />
|
||||
<Run Text="— category ID" />
|
||||
<LineBreak />
|
||||
<Run Text=" " />
|
||||
<Run FontWeight="SemiBold" Text="%T" />
|
||||
<Run Text="— category name" />
|
||||
<LineBreak />
|
||||
<Run Text=" " />
|
||||
<Run FontWeight="SemiBold" Text="%c" />
|
||||
<Run Text="— channel ID" />
|
||||
<LineBreak />
|
||||
<Run Text=" " />
|
||||
<Run FontWeight="SemiBold" Text="%C" />
|
||||
<Run Text="— channel name" />
|
||||
<LineBreak />
|
||||
<Run Text=" " />
|
||||
<Run FontWeight="SemiBold" Text="%p" />
|
||||
<Run Text="— channel position" />
|
||||
<LineBreak />
|
||||
<Run Text=" " />
|
||||
<Run FontWeight="SemiBold" Text="%P" />
|
||||
<Run Text="— category position" />
|
||||
<LineBreak />
|
||||
<Run Text=" " />
|
||||
<Run FontWeight="SemiBold" Text="%a" />
|
||||
<Run Text="— after date" />
|
||||
<LineBreak />
|
||||
<Run Text=" " />
|
||||
<Run FontWeight="SemiBold" Text="%b" />
|
||||
<Run Text="— before date" />
|
||||
<LineBreak />
|
||||
<Run Text=" " />
|
||||
<Run FontWeight="SemiBold" Text="%d" />
|
||||
<Run Text="— current date" />
|
||||
</TextBlock>
|
||||
</ToolTip.Tip>
|
||||
<TextBox.InnerRightContent>
|
||||
<Button
|
||||
Margin="8,8,8,6"
|
||||
Padding="8"
|
||||
VerticalAlignment="Center"
|
||||
Command="{Binding ShowOutputPathPromptCommand}"
|
||||
Theme="{DynamicResource MaterialFlatButton}">
|
||||
<materialIcons:MaterialIcon
|
||||
Width="20"
|
||||
Height="20"
|
||||
Kind="FolderOpen" />
|
||||
</Button>
|
||||
</TextBox.InnerRightContent>
|
||||
</TextBox>
|
||||
|
||||
<!-- Format -->
|
||||
<ComboBox
|
||||
Margin="16,8"
|
||||
materialAssists:ComboBoxAssist.Label="Format"
|
||||
ItemsSource="{Binding AvailableFormats}"
|
||||
SelectedItem="{Binding SelectedFormat}"
|
||||
Theme="{DynamicResource MaterialFilledComboBox}"
|
||||
ToolTip.Tip="Export format">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Converter={x:Static converters:ExportFormatToStringConverter.Instance}}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
|
||||
<!-- Advanced section -->
|
||||
<StackPanel IsVisible="{Binding IsAdvancedSectionDisplayed}" Orientation="Vertical">
|
||||
<!-- Date limits -->
|
||||
<Grid ColumnDefinitions="*,*" RowDefinitions="*,*">
|
||||
<DatePicker
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Margin="16,8,8,8"
|
||||
materialAssists:TextFieldAssist.Label="After (date)"
|
||||
SelectedDate="{Binding AfterDate}"
|
||||
ToolTip.Tip="Only include messages sent after this date">
|
||||
<DatePicker.Styles>
|
||||
<Style Selector="DatePicker">
|
||||
<Style Selector="^ /template/ TextBox#DisplayTextBox">
|
||||
<Setter Property="Theme" Value="{DynamicResource FilledTextBox}" />
|
||||
</Style>
|
||||
</Style>
|
||||
</DatePicker.Styles>
|
||||
</DatePicker>
|
||||
<DatePicker
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Margin="8,8,16,8"
|
||||
materialAssists:TextFieldAssist.Label="Before (date)"
|
||||
SelectedDate="{Binding BeforeDate}"
|
||||
ToolTip.Tip="Only include messages sent before this date">
|
||||
<DatePicker.Styles>
|
||||
<Style Selector="DatePicker">
|
||||
<Style Selector="^ /template/ TextBox#DisplayTextBox">
|
||||
<Setter Property="Theme" Value="{DynamicResource FilledTextBox}" />
|
||||
</Style>
|
||||
</Style>
|
||||
</DatePicker.Styles>
|
||||
</DatePicker>
|
||||
|
||||
<!-- Time limits -->
|
||||
<TimePicker
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Margin="16,8,8,8"
|
||||
materialAssists:TextFieldAssist.Label="After (time)"
|
||||
ClockIdentifier="{x:Static utils:Internationalization.AvaloniaClockIdentifier}"
|
||||
IsEnabled="{Binding IsAfterDateSet}"
|
||||
SelectedTime="{Binding AfterTime}"
|
||||
ToolTip.Tip="Only include messages sent after this time">
|
||||
<TimePicker.Styles>
|
||||
<Style Selector="TimePicker">
|
||||
<Style Selector="^ /template/ TextBox#PART_DisplayTextBox">
|
||||
<Setter Property="Theme" Value="{DynamicResource FilledTextBox}" />
|
||||
</Style>
|
||||
</Style>
|
||||
</TimePicker.Styles>
|
||||
</TimePicker>
|
||||
<TimePicker
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Margin="8,8,16,8"
|
||||
materialAssists:TextFieldAssist.Label="Before (time)"
|
||||
ClockIdentifier="{x:Static utils:Internationalization.AvaloniaClockIdentifier}"
|
||||
IsEnabled="{Binding IsBeforeDateSet}"
|
||||
SelectedTime="{Binding BeforeTime}"
|
||||
ToolTip.Tip="Only include messages sent before this time">
|
||||
<TimePicker.Styles>
|
||||
<Style Selector="TimePicker">
|
||||
<Style Selector="^ /template/ TextBox#PART_DisplayTextBox">
|
||||
<Setter Property="Theme" Value="{DynamicResource FilledTextBox}" />
|
||||
</Style>
|
||||
</Style>
|
||||
</TimePicker.Styles>
|
||||
</TimePicker>
|
||||
</Grid>
|
||||
|
||||
<!-- Partitioning -->
|
||||
<TextBox
|
||||
Margin="16,8"
|
||||
materialAssists:TextFieldAssist.Label="Partition limit"
|
||||
Text="{Binding PartitionLimitValue}"
|
||||
Theme="{DynamicResource FilledTextBox}"
|
||||
ToolTip.Tip="Split the output into partitions, each limited to the specified number of messages (e.g. '100') or file size (e.g. '10mb')" />
|
||||
|
||||
<!-- Filtering -->
|
||||
<TextBox
|
||||
Margin="16,8"
|
||||
materialAssists:TextFieldAssist.Label="Message filter"
|
||||
Text="{Binding MessageFilterValue}"
|
||||
Theme="{DynamicResource FilledTextBox}"
|
||||
ToolTip.Tip="Only include messages that satisfy this filter (e.g. 'from:foo#1234' or 'has:image'). See the documentation for more info." />
|
||||
|
||||
<!-- Markdown formatting -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
LastChildFill="False"
|
||||
ToolTip.Tip="Process markdown, mentions, and other special tokens">
|
||||
<TextBlock DockPanel.Dock="Left" Text="Format markdown" />
|
||||
<ToggleSwitch DockPanel.Dock="Right" IsChecked="{Binding ShouldFormatMarkdown}" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- Download assets -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
LastChildFill="False"
|
||||
ToolTip.Tip="Download assets referenced by the export (user avatars, attached files, embedded images, etc.)">
|
||||
<TextBlock DockPanel.Dock="Left" Text="Download assets" />
|
||||
<ToggleSwitch DockPanel.Dock="Right" IsChecked="{Binding ShouldDownloadAssets}" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- Reuse assets -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
IsEnabled="{Binding ShouldDownloadAssets}"
|
||||
LastChildFill="False"
|
||||
ToolTip.Tip="Reuse previously downloaded assets to avoid redundant requests">
|
||||
<TextBlock DockPanel.Dock="Left" Text="Reuse assets" />
|
||||
<ToggleSwitch DockPanel.Dock="Right" IsChecked="{Binding ShouldReuseAssets}" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- Assets path -->
|
||||
<TextBox
|
||||
Margin="16,8"
|
||||
materialAssists:TextFieldAssist.Label="Assets directory path"
|
||||
IsEnabled="{Binding ShouldDownloadAssets}"
|
||||
Text="{Binding AssetsDirPath}"
|
||||
Theme="{DynamicResource FilledTextBox}"
|
||||
ToolTip.Tip="Download assets to this directory. If not specified, the asset directory path will be derived from the output path.">
|
||||
<TextBox.InnerRightContent>
|
||||
<Button
|
||||
Margin="8,8,8,6"
|
||||
Padding="8"
|
||||
VerticalAlignment="Center"
|
||||
Command="{Binding ShowAssetsDirPathPromptCommand}"
|
||||
Theme="{DynamicResource MaterialFlatButton}">
|
||||
<materialIcons:MaterialIcon
|
||||
Width="20"
|
||||
Height="20"
|
||||
Kind="FolderOpen" />
|
||||
</Button>
|
||||
</TextBox.InnerRightContent>
|
||||
</TextBox>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<!-- Buttons -->
|
||||
<Grid
|
||||
Grid.Row="2"
|
||||
Margin="16"
|
||||
ColumnDefinitions="Auto,*,Auto,Auto">
|
||||
<ToggleButton
|
||||
Grid.Column="0"
|
||||
IsChecked="{Binding IsAdvancedSectionDisplayed}"
|
||||
Theme="{DynamicResource MaterialOutlineButton}"
|
||||
ToolTip.Tip="Toggle advanced options">
|
||||
<Button.Styles>
|
||||
<Style Selector="ToggleButton">
|
||||
<Setter Property="Content" Value="MORE" />
|
||||
|
||||
<Style Selector="^:checked">
|
||||
<Setter Property="Content" Value="LESS" />
|
||||
</Style>
|
||||
</Style>
|
||||
</Button.Styles>
|
||||
</ToggleButton>
|
||||
|
||||
<Button
|
||||
Grid.Column="2"
|
||||
Command="{Binding ConfirmCommand}"
|
||||
Content="EXPORT"
|
||||
IsDefault="True"
|
||||
Theme="{DynamicResource MaterialOutlineButton}" />
|
||||
<Button
|
||||
Grid.Column="3"
|
||||
Margin="16,0,0,0"
|
||||
Command="{Binding CloseCommand}"
|
||||
Content="CANCEL"
|
||||
IsCancel="True"
|
||||
Theme="{DynamicResource MaterialOutlineButton}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
|
@ -0,0 +1,13 @@
|
|||
using Avalonia.Interactivity;
|
||||
using DiscordChatExporter.Gui.Framework;
|
||||
using DiscordChatExporter.Gui.ViewModels.Dialogs;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Views.Dialogs;
|
||||
|
||||
public partial class ExportSetupView : UserControl<ExportSetupViewModel>
|
||||
{
|
||||
public ExportSetupView() => InitializeComponent();
|
||||
|
||||
private void UserControl_OnLoaded(object? sender, RoutedEventArgs args) =>
|
||||
DataContext.InitializeCommand.Execute(null);
|
||||
}
|
|
@ -1,365 +0,0 @@
|
|||
<UserControl
|
||||
x:Class="DiscordChatExporter.Gui.Views.Dialogs.ExportSetupView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="clr-namespace:DiscordChatExporter.Gui.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:dialogs="clr-namespace:DiscordChatExporter.Gui.ViewModels.Dialogs"
|
||||
xmlns:globalization="clr-namespace:System.Globalization;assembly=System.Runtime"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
xmlns:utils="clr-namespace:DiscordChatExporter.Gui.Utils"
|
||||
Width="380"
|
||||
d:DataContext="{d:DesignInstance Type=dialogs:ExportSetupViewModel}"
|
||||
Style="{DynamicResource MaterialDesignRoot}"
|
||||
mc:Ignorable="d">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Guild/channel info -->
|
||||
<Grid Grid.Row="0" Margin="16">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Guild icon -->
|
||||
<Ellipse
|
||||
Grid.Column="0"
|
||||
Width="32"
|
||||
Height="32">
|
||||
<Ellipse.Fill>
|
||||
<ImageBrush ImageSource="{Binding Guild.IconUrl}" />
|
||||
</Ellipse.Fill>
|
||||
</Ellipse>
|
||||
|
||||
<!-- Channel count (for multiple channels) -->
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="19"
|
||||
FontWeight="Light"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
Visibility="{Binding IsSingleChannel, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}}">
|
||||
<Run Text="{Binding Channels.Count, Mode=OneWay}" />
|
||||
<Run Text="channels selected" />
|
||||
</TextBlock>
|
||||
|
||||
<!-- Category and channel name (for single channel) -->
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="19"
|
||||
FontWeight="Light"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
Visibility="{Binding IsSingleChannel, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
|
||||
<TextBlock Visibility="{Binding Channels[0].Parent, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
|
||||
<Run Text="{Binding Channels[0].Parent.Name, Mode=OneWay}" ToolTip="{Binding Channels[0].Parent.Name, Mode=OneWay}" />
|
||||
<Run Text="/" />
|
||||
</TextBlock>
|
||||
<Run
|
||||
FontWeight="SemiBold"
|
||||
Text="{Binding Channels[0].Name, Mode=OneWay}"
|
||||
ToolTip="{Binding Channels[0].Name, Mode=OneWay}" />
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
Padding="0,8"
|
||||
BorderBrush="{DynamicResource MaterialDesignDivider}"
|
||||
BorderThickness="0,1">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel>
|
||||
<!-- Output path -->
|
||||
<Grid Margin="16,8">
|
||||
<TextBox
|
||||
Padding="16,16,42,16"
|
||||
materialDesign:HintAssist.Hint="Output path"
|
||||
materialDesign:HintAssist.IsFloating="True"
|
||||
Style="{DynamicResource MaterialDesignOutlinedTextBox}"
|
||||
Text="{Binding OutputPath}">
|
||||
<TextBox.ToolTip>
|
||||
<TextBlock>
|
||||
<Run Text="Output file or directory path." />
|
||||
<Run Text="If a directory is specified, file names will be generated automatically based on the channel names and export parameters." />
|
||||
<Run Text="Directory paths must end with a slash to avoid ambiguity." />
|
||||
<Run Text="Supports template tokens, see the documentation for more info." />
|
||||
<LineBreak />
|
||||
<LineBreak />
|
||||
<Run Text="Available template tokens:" />
|
||||
<LineBreak />
|
||||
<Run FontWeight="SemiBold" Text="%g" />
|
||||
<Run Text="— server ID" />
|
||||
<LineBreak />
|
||||
<Run FontWeight="SemiBold" Text="%G" />
|
||||
<Run Text="— server name" />
|
||||
<LineBreak />
|
||||
<Run FontWeight="SemiBold" Text="%t" />
|
||||
<Run Text="— category ID" />
|
||||
<LineBreak />
|
||||
<Run FontWeight="SemiBold" Text="%T" />
|
||||
<Run Text="— category name" />
|
||||
<LineBreak />
|
||||
<Run FontWeight="SemiBold" Text="%c" />
|
||||
<Run Text="— channel ID" />
|
||||
<LineBreak />
|
||||
<Run FontWeight="SemiBold" Text="%C" />
|
||||
<Run Text="— channel name" />
|
||||
<LineBreak />
|
||||
<Run FontWeight="SemiBold" Text="%p" />
|
||||
<Run Text="— channel position" />
|
||||
<LineBreak />
|
||||
<Run FontWeight="SemiBold" Text="%P" />
|
||||
<Run Text="— category position" />
|
||||
<LineBreak />
|
||||
<Run FontWeight="SemiBold" Text="%a" />
|
||||
<Run Text="— after date" />
|
||||
<LineBreak />
|
||||
<Run FontWeight="SemiBold" Text="%b" />
|
||||
<Run Text="— before date" />
|
||||
<LineBreak />
|
||||
<Run FontWeight="SemiBold" Text="%d" />
|
||||
<Run Text="— current date" />
|
||||
</TextBlock>
|
||||
</TextBox.ToolTip>
|
||||
</TextBox>
|
||||
<Button
|
||||
Width="24"
|
||||
Height="24"
|
||||
Margin="0,0,12,0"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Right"
|
||||
Command="{s:Action ShowOutputPathPrompt}"
|
||||
Style="{DynamicResource MaterialDesignToolForegroundButton}">
|
||||
<materialDesign:PackIcon Kind="FolderOpen" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- Format -->
|
||||
<ComboBox
|
||||
Margin="16,8"
|
||||
materialDesign:HintAssist.Hint="Format"
|
||||
materialDesign:HintAssist.IsFloating="True"
|
||||
IsReadOnly="True"
|
||||
ItemsSource="{Binding AvailableFormats}"
|
||||
SelectedItem="{Binding SelectedFormat}"
|
||||
Style="{DynamicResource MaterialDesignOutlinedComboBox}"
|
||||
ToolTip="Export format">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Converter={x:Static converters:ExportFormatToStringConverter.Instance}, ConverterCulture={x:Static globalization:CultureInfo.CurrentCulture}}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
|
||||
<!-- Advanced section -->
|
||||
<StackPanel Visibility="{Binding IsAdvancedSectionDisplayed, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
|
||||
|
||||
<!-- Date limits -->
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<DatePicker
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Margin="16,8,16,4"
|
||||
materialDesign:HintAssist.Hint="After (date)"
|
||||
materialDesign:HintAssist.IsFloating="True"
|
||||
DisplayDateEnd="{Binding BeforeDate, Converter={x:Static converters:DateTimeOffsetToDateTimeConverter.Instance}, ConverterCulture={x:Static globalization:CultureInfo.CurrentCulture}}"
|
||||
SelectedDate="{Binding AfterDate, Converter={x:Static converters:DateTimeOffsetToDateTimeConverter.Instance}, ConverterCulture={x:Static globalization:CultureInfo.CurrentCulture}}"
|
||||
Style="{DynamicResource MaterialDesignOutlinedDatePicker}"
|
||||
ToolTip="Only include messages sent after this date" />
|
||||
<DatePicker
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Margin="16,8,16,4"
|
||||
materialDesign:HintAssist.Hint="Before (date)"
|
||||
materialDesign:HintAssist.IsFloating="True"
|
||||
DisplayDateStart="{Binding AfterDate, Converter={x:Static converters:DateTimeOffsetToDateTimeConverter.Instance}, ConverterCulture={x:Static globalization:CultureInfo.CurrentCulture}}"
|
||||
SelectedDate="{Binding BeforeDate, Converter={x:Static converters:DateTimeOffsetToDateTimeConverter.Instance}, ConverterCulture={x:Static globalization:CultureInfo.CurrentCulture}}"
|
||||
Style="{DynamicResource MaterialDesignOutlinedDatePicker}"
|
||||
ToolTip="Only include messages sent before this date" />
|
||||
<materialDesign:TimePicker
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Margin="16,4,16,8"
|
||||
materialDesign:HintAssist.Hint="After (time)"
|
||||
materialDesign:HintAssist.IsFloating="True"
|
||||
Is24Hours="{x:Static utils:Internationalization.Is24Hours}"
|
||||
IsEnabled="{Binding IsAfterDateSet}"
|
||||
SelectedTime="{Binding AfterTime, Converter={x:Static converters:TimeSpanToDateTimeConverter.Instance}, ConverterCulture={x:Static globalization:CultureInfo.CurrentCulture}}"
|
||||
Style="{DynamicResource MaterialDesignOutlinedTimePicker}"
|
||||
ToolTip="Only include messages sent after this time" />
|
||||
<materialDesign:TimePicker
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Margin="16,4,16,8"
|
||||
materialDesign:HintAssist.Hint="Before (time)"
|
||||
materialDesign:HintAssist.IsFloating="True"
|
||||
Is24Hours="{x:Static utils:Internationalization.Is24Hours}"
|
||||
IsEnabled="{Binding IsBeforeDateSet}"
|
||||
SelectedTime="{Binding BeforeTime, Converter={x:Static converters:TimeSpanToDateTimeConverter.Instance}, ConverterCulture={x:Static globalization:CultureInfo.CurrentCulture}}"
|
||||
Style="{DynamicResource MaterialDesignOutlinedTimePicker}"
|
||||
ToolTip="Only include messages sent before this time" />
|
||||
</Grid>
|
||||
|
||||
<!-- Partitioning -->
|
||||
<TextBox
|
||||
Margin="16,8"
|
||||
materialDesign:HintAssist.Hint="Partition limit"
|
||||
materialDesign:HintAssist.IsFloating="True"
|
||||
Style="{DynamicResource MaterialDesignOutlinedTextBox}"
|
||||
Text="{Binding PartitionLimitValue}"
|
||||
ToolTip="Split the output into partitions, each limited to the specified number of messages (e.g. '100') or file size (e.g. '10mb')" />
|
||||
|
||||
<!-- Filtering -->
|
||||
<TextBox
|
||||
Margin="16,8"
|
||||
materialDesign:HintAssist.Hint="Message filter"
|
||||
materialDesign:HintAssist.IsFloating="True"
|
||||
Style="{DynamicResource MaterialDesignOutlinedTextBox}"
|
||||
Text="{Binding MessageFilterValue}"
|
||||
ToolTip="Only include messages that satisfy this filter (e.g. 'from:foo#1234' or 'has:image'). See the documentation for more info." />
|
||||
|
||||
<!-- Markdown formatting -->
|
||||
<Grid Margin="16,8" ToolTip="Process markdown, mentions, and other special tokens">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Text="Format markdown" />
|
||||
<ToggleButton
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
IsChecked="{Binding ShouldFormatMarkdown}" />
|
||||
</Grid>
|
||||
|
||||
<!-- Download assets -->
|
||||
<Grid Margin="16,8" ToolTip="Download assets referenced by the export (user avatars, attached files, embedded images, etc.)">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Text="Download assets" />
|
||||
<ToggleButton
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
IsChecked="{Binding ShouldDownloadAssets}" />
|
||||
</Grid>
|
||||
|
||||
<!-- Reuse assets -->
|
||||
<Grid
|
||||
Margin="16,8"
|
||||
IsEnabled="{Binding ShouldDownloadAssets}"
|
||||
ToolTip="Reuse previously downloaded assets to avoid redundant requests">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Text="Reuse assets" />
|
||||
<ToggleButton
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
IsChecked="{Binding ShouldReuseAssets}" />
|
||||
</Grid>
|
||||
|
||||
<!-- Assets path -->
|
||||
<Grid
|
||||
Margin="16,8"
|
||||
IsEnabled="{Binding ShouldDownloadAssets}"
|
||||
ToolTip="Download assets to this directory. If not specified, the asset directory path will be derived from the output path.">
|
||||
<TextBox
|
||||
Padding="16,16,42,16"
|
||||
materialDesign:HintAssist.Hint="Assets directory path"
|
||||
materialDesign:HintAssist.IsFloating="True"
|
||||
Style="{DynamicResource MaterialDesignOutlinedTextBox}"
|
||||
Text="{Binding AssetsDirPath}" />
|
||||
<Button
|
||||
Width="24"
|
||||
Height="24"
|
||||
Margin="0,0,12,0"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Right"
|
||||
Command="{s:Action ShowAssetsDirPathPrompt}"
|
||||
Style="{DynamicResource MaterialDesignToolForegroundButton}">
|
||||
<materialDesign:PackIcon Kind="FolderOpen" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<!-- Buttons -->
|
||||
<Grid Grid.Row="2" Margin="16">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Button
|
||||
Grid.Column="0"
|
||||
Command="{s:Action ToggleAdvancedSection}"
|
||||
IsDefault="True"
|
||||
ToolTip="Toggle advanced options">
|
||||
<Button.Style>
|
||||
<Style BasedOn="{StaticResource MaterialDesignOutlinedButton}" TargetType="{x:Type Button}">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsAdvancedSectionDisplayed}" Value="False">
|
||||
<Setter Property="Content" Value="MORE" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding IsAdvancedSectionDisplayed}" Value="True">
|
||||
<Setter Property="Content" Value="LESS" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
Grid.Column="2"
|
||||
Command="{s:Action Confirm}"
|
||||
Content="EXPORT"
|
||||
IsDefault="True"
|
||||
Style="{DynamicResource MaterialDesignOutlinedButton}" />
|
||||
<Button
|
||||
Grid.Column="3"
|
||||
Margin="8,0,0,0"
|
||||
Command="{s:Action Close}"
|
||||
Content="CANCEL"
|
||||
IsCancel="True"
|
||||
Style="{DynamicResource MaterialDesignOutlinedButton}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
|
@ -1,9 +0,0 @@
|
|||
namespace DiscordChatExporter.Gui.Views.Dialogs;
|
||||
|
||||
public partial class ExportSetupView
|
||||
{
|
||||
public ExportSetupView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
|
@ -1,23 +1,15 @@
|
|||
<UserControl
|
||||
x:Class="DiscordChatExporter.Gui.Views.Dialogs.MessageBoxView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:dialogs="clr-namespace:DiscordChatExporter.Gui.ViewModels.Dialogs"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
xmlns:system="clr-namespace:System;assembly=System.Runtime"
|
||||
Width="500"
|
||||
d:DataContext="{d:DesignInstance Type=dialogs:MessageBoxViewModel}"
|
||||
Style="{DynamicResource MaterialDesignRoot}"
|
||||
mc:Ignorable="d">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
Width="500">
|
||||
<Design.DataContext>
|
||||
<dialogs:MessageBoxViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
<!-- Title -->
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
|
@ -26,13 +18,13 @@
|
|||
FontWeight="Light"
|
||||
Text="{Binding Title}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
ToolTip="{Binding Title}" />
|
||||
ToolTip.Tip="{Binding Title}" />
|
||||
|
||||
<!-- Message -->
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
Padding="0,8"
|
||||
BorderBrush="{DynamicResource MaterialDesignDivider}"
|
||||
BorderBrush="{DynamicResource MaterialDividerBrush}"
|
||||
BorderThickness="0,1">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||
<TextBlock
|
||||
|
@ -49,11 +41,11 @@
|
|||
Columns="{Binding ButtonsCount}">
|
||||
<!-- OK -->
|
||||
<Button
|
||||
Command="{s:Action Close}"
|
||||
Content="{Binding OkButtonText}"
|
||||
Command="{Binding CloseCommand}"
|
||||
Content="{Binding DefaultButtonText}"
|
||||
IsDefault="True"
|
||||
Style="{DynamicResource MaterialDesignOutlinedButton}"
|
||||
Visibility="{Binding IsOkButtonVisible, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
|
||||
IsVisible="{Binding IsDefaultButtonVisible}"
|
||||
Theme="{DynamicResource MaterialOutlineButton}">
|
||||
<Button.CommandParameter>
|
||||
<system:Boolean>True</system:Boolean>
|
||||
</Button.CommandParameter>
|
||||
|
@ -61,13 +53,13 @@
|
|||
|
||||
<!-- Cancel -->
|
||||
<Button
|
||||
Margin="8,0,0,0"
|
||||
Margin="16,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{s:Action Close}"
|
||||
Command="{Binding CloseCommand}"
|
||||
Content="{Binding CancelButtonText}"
|
||||
IsCancel="True"
|
||||
Style="{DynamicResource MaterialDesignOutlinedButton}"
|
||||
Visibility="{Binding IsCancelButtonVisible, Converter={x:Static s:BoolToVisibilityConverter.Instance}}" />
|
||||
IsVisible="{Binding IsCancelButtonVisible}"
|
||||
Theme="{DynamicResource MaterialOutlineButton}" />
|
||||
</UniformGrid>
|
||||
</Grid>
|
||||
</UserControl>
|
|
@ -0,0 +1,9 @@
|
|||
using DiscordChatExporter.Gui.Framework;
|
||||
using DiscordChatExporter.Gui.ViewModels.Dialogs;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Views.Dialogs;
|
||||
|
||||
public partial class MessageBoxView : UserControl<MessageBoxViewModel>
|
||||
{
|
||||
public MessageBoxView() => InitializeComponent();
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
namespace DiscordChatExporter.Gui.Views.Dialogs;
|
||||
|
||||
public partial class MessageBoxView
|
||||
{
|
||||
public MessageBoxView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
133
DiscordChatExporter.Gui/Views/Dialogs/SettingsView.axaml
Normal file
133
DiscordChatExporter.Gui/Views/Dialogs/SettingsView.axaml
Normal file
|
@ -0,0 +1,133 @@
|
|||
<UserControl
|
||||
x:Class="DiscordChatExporter.Gui.Views.Dialogs.SettingsView"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="clr-namespace:DiscordChatExporter.Gui.Converters"
|
||||
xmlns:dialogs="clr-namespace:DiscordChatExporter.Gui.ViewModels.Dialogs"
|
||||
Width="380">
|
||||
<Design.DataContext>
|
||||
<dialogs:SettingsViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Margin="16"
|
||||
FontSize="19"
|
||||
FontWeight="Light"
|
||||
Text="Settings" />
|
||||
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
Padding="0,8"
|
||||
BorderBrush="{DynamicResource MaterialDividerBrush}"
|
||||
BorderThickness="0,1">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<!-- Auto-updates -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
IsVisible="{OnPlatform False,
|
||||
Windows=True}"
|
||||
LastChildFill="False"
|
||||
ToolTip.Tip="Perform automatic updates on every launch">
|
||||
<TextBlock DockPanel.Dock="Left" Text="Auto-update" />
|
||||
<ToggleSwitch DockPanel.Dock="Right" IsChecked="{Binding IsAutoUpdateEnabled}" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- Dark mode -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
LastChildFill="False"
|
||||
ToolTip.Tip="Use darker colors in the UI">
|
||||
<TextBlock DockPanel.Dock="Left" Text="Dark mode" />
|
||||
<ToggleSwitch
|
||||
x:Name="DarkModeToggleSwitch"
|
||||
DockPanel.Dock="Right"
|
||||
IsChecked="{Binding IsDarkModeEnabled}"
|
||||
IsCheckedChanged="DarkModeToggleSwitch_OnIsCheckedChanged" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- Persist token -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
LastChildFill="False"
|
||||
ToolTip.Tip="Save the last used token to a file so that it can be persisted between sessions">
|
||||
<TextBlock DockPanel.Dock="Left" Text="Persist token" />
|
||||
<ToggleSwitch DockPanel.Dock="Right" IsChecked="{Binding IsTokenPersisted}" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- Thread inclusion mode -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
LastChildFill="False"
|
||||
ToolTip.Tip="Which types of threads to show in the channel list">
|
||||
<TextBlock DockPanel.Dock="Left" Text="Show threads" />
|
||||
<ComboBox
|
||||
Width="150"
|
||||
DockPanel.Dock="Right"
|
||||
ItemsSource="{Binding AvailableThreadInclusions}"
|
||||
SelectedItem="{Binding ThreadInclusionMode}" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- Locale -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
LastChildFill="False"
|
||||
ToolTip.Tip="Locale to use when formatting dates and numbers">
|
||||
<TextBlock DockPanel.Dock="Left" Text="Locale" />
|
||||
<ComboBox
|
||||
Width="150"
|
||||
DockPanel.Dock="Right"
|
||||
ItemsSource="{Binding AvailableLocales}"
|
||||
SelectedItem="{Binding Locale}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Converter={x:Static converters:LocaleToDisplayNameStringConverter.Instance}}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</DockPanel>
|
||||
|
||||
<!-- UTC normalization -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
LastChildFill="False"
|
||||
ToolTip.Tip="Normalize all timestamps to UTC+0">
|
||||
<TextBlock DockPanel.Dock="Left" Text="Normalize to UTC" />
|
||||
<ToggleSwitch DockPanel.Dock="Right" IsChecked="{Binding IsUtcNormalizationEnabled}" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- Parallel limit -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
LastChildFill="False"
|
||||
ToolTip.Tip="How many channels can be exported at the same time">
|
||||
<TextBlock DockPanel.Dock="Left" Text="Parallel limit" />
|
||||
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
|
||||
<TextBlock Margin="10,0" Text="{Binding ParallelLimit}" />
|
||||
<Slider
|
||||
Width="150"
|
||||
IsSnapToTickEnabled="True"
|
||||
Maximum="10"
|
||||
Minimum="1"
|
||||
TickFrequency="1"
|
||||
Value="{Binding ParallelLimit}" />
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<!-- Close button -->
|
||||
<Button
|
||||
Grid.Row="2"
|
||||
Margin="16"
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding CloseCommand}"
|
||||
Content="CLOSE"
|
||||
IsCancel="True"
|
||||
IsDefault="True"
|
||||
Theme="{DynamicResource MaterialOutlineButton}" />
|
||||
</Grid>
|
||||
</UserControl>
|
27
DiscordChatExporter.Gui/Views/Dialogs/SettingsView.axaml.cs
Normal file
27
DiscordChatExporter.Gui/Views/Dialogs/SettingsView.axaml.cs
Normal file
|
@ -0,0 +1,27 @@
|
|||
using System.Windows;
|
||||
using Avalonia.Interactivity;
|
||||
using DiscordChatExporter.Gui.Framework;
|
||||
using DiscordChatExporter.Gui.ViewModels.Dialogs;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Views.Dialogs;
|
||||
|
||||
public partial class SettingsView : UserControl<SettingsViewModel>
|
||||
{
|
||||
public SettingsView() => InitializeComponent();
|
||||
|
||||
private void DarkModeToggleSwitch_OnIsCheckedChanged(object? sender, RoutedEventArgs args)
|
||||
{
|
||||
if (DarkModeToggleSwitch.IsChecked is true)
|
||||
{
|
||||
App.SetDarkTheme();
|
||||
}
|
||||
else if (DarkModeToggleSwitch.IsChecked is false)
|
||||
{
|
||||
App.SetLightTheme();
|
||||
}
|
||||
else
|
||||
{
|
||||
App.SetDefaultTheme();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,192 +0,0 @@
|
|||
<UserControl
|
||||
x:Class="DiscordChatExporter.Gui.Views.Dialogs.SettingsView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="clr-namespace:DiscordChatExporter.Gui.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:dialogs="clr-namespace:DiscordChatExporter.Gui.ViewModels.Dialogs"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
Width="380"
|
||||
d:DataContext="{d:DesignInstance Type=dialogs:SettingsViewModel}"
|
||||
Style="{DynamicResource MaterialDesignRoot}"
|
||||
mc:Ignorable="d">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Margin="16"
|
||||
FontSize="19"
|
||||
FontWeight="Light"
|
||||
Text="Settings" />
|
||||
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
Padding="0,8"
|
||||
BorderBrush="{DynamicResource MaterialDesignDivider}"
|
||||
BorderThickness="0,1">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel>
|
||||
<!-- Auto-updates -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
Background="Transparent"
|
||||
LastChildFill="False"
|
||||
ToolTip="Perform automatic updates on every launch">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Left"
|
||||
Text="Auto-update" />
|
||||
<ToggleButton
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Right"
|
||||
IsChecked="{Binding IsAutoUpdateEnabled}" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- Dark mode -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
Background="Transparent"
|
||||
LastChildFill="False"
|
||||
ToolTip="Use darker colors in the UI">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Left"
|
||||
Text="Dark mode" />
|
||||
<ToggleButton
|
||||
x:Name="DarkModeToggleButton"
|
||||
VerticalAlignment="Center"
|
||||
Checked="DarkModeToggleButton_OnChecked"
|
||||
DockPanel.Dock="Right"
|
||||
IsChecked="{Binding IsDarkModeEnabled}"
|
||||
Unchecked="DarkModeToggleButton_OnUnchecked" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- Persist token -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
Background="Transparent"
|
||||
LastChildFill="False"
|
||||
ToolTip="Save the last used token to a file so that it can be persisted between sessions">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Left"
|
||||
Text="Persist token" />
|
||||
<ToggleButton
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Right"
|
||||
IsChecked="{Binding IsTokenPersisted}" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- Thread inclusion mode -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
Background="Transparent"
|
||||
LastChildFill="False"
|
||||
ToolTip="Which types of threads to show in the channel list">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Left"
|
||||
Text="Show threads" />
|
||||
<ComboBox
|
||||
Width="150"
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Right"
|
||||
ItemsSource="{Binding AvailableThreadInclusions}"
|
||||
SelectedItem="{Binding ThreadInclusionMode}" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- Locale -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
Background="Transparent"
|
||||
LastChildFill="False"
|
||||
ToolTip="Locale to use when formatting dates and numbers">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Left"
|
||||
Text="Locale" />
|
||||
<ComboBox
|
||||
Width="150"
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Right"
|
||||
ItemsSource="{Binding AvailableLocales}"
|
||||
SelectedItem="{Binding Locale}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Converter={x:Static converters:LocaleToDisplayNameConverter.Instance}}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</DockPanel>
|
||||
|
||||
<!-- UTC normalization -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
Background="Transparent"
|
||||
LastChildFill="False"
|
||||
ToolTip="Normalize all timestamps to UTC+0">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Left"
|
||||
Text="Normalize to UTC" />
|
||||
<ToggleButton
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Right"
|
||||
IsChecked="{Binding IsUtcNormalizationEnabled}" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- Parallel limit -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
Background="Transparent"
|
||||
LastChildFill="False"
|
||||
ToolTip="How many channels can be exported at the same time">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Left"
|
||||
Text="Parallel limit"
|
||||
TextAlignment="Right" />
|
||||
<StackPanel
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Right"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="10,0"
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="SemiBold"
|
||||
Text="{Binding ParallelLimit}" />
|
||||
<Slider
|
||||
Width="150"
|
||||
VerticalAlignment="Center"
|
||||
IsSnapToTickEnabled="True"
|
||||
LargeChange="1"
|
||||
Maximum="10"
|
||||
Minimum="1"
|
||||
SmallChange="1"
|
||||
Style="{DynamicResource MaterialDesignThinSlider}"
|
||||
TickFrequency="1"
|
||||
Value="{Binding ParallelLimit}" />
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<!-- Close button -->
|
||||
<Button
|
||||
Grid.Row="2"
|
||||
Margin="16"
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{s:Action Close}"
|
||||
Content="CLOSE"
|
||||
IsCancel="True"
|
||||
IsDefault="True"
|
||||
Style="{DynamicResource MaterialDesignOutlinedButton}" />
|
||||
</Grid>
|
||||
</UserControl>
|
|
@ -1,17 +0,0 @@
|
|||
using System.Windows;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Views.Dialogs;
|
||||
|
||||
public partial class SettingsView
|
||||
{
|
||||
public SettingsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void DarkModeToggleButton_OnChecked(object sender, RoutedEventArgs args) =>
|
||||
App.SetDarkTheme();
|
||||
|
||||
private void DarkModeToggleButton_OnUnchecked(object sender, RoutedEventArgs args) =>
|
||||
App.SetLightTheme();
|
||||
}
|
28
DiscordChatExporter.Gui/Views/MainView.axaml
Normal file
28
DiscordChatExporter.Gui/Views/MainView.axaml
Normal file
|
@ -0,0 +1,28 @@
|
|||
<Window
|
||||
x:Class="DiscordChatExporter.Gui.Views.MainView"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:dialogHostAvalonia="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
|
||||
xmlns:materialStyles="clr-namespace:Material.Styles.Controls;assembly=Material.Styles"
|
||||
xmlns:viewModels="clr-namespace:DiscordChatExporter.Gui.ViewModels"
|
||||
Title="{Binding Title}"
|
||||
Width="625"
|
||||
Height="625"
|
||||
MinWidth="600"
|
||||
MinHeight="400"
|
||||
Icon="/favicon.ico"
|
||||
RenderOptions.BitmapInterpolationMode="HighQuality"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
<Design.DataContext>
|
||||
<viewModels:MainViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<dialogHostAvalonia:DialogHost
|
||||
x:Name="DialogHost"
|
||||
CloseOnClickAway="False"
|
||||
Loaded="DialogHost_OnLoaded">
|
||||
<materialStyles:SnackbarHost HostName="Root">
|
||||
<ContentControl Content="{Binding Dashboard}" />
|
||||
</materialStyles:SnackbarHost>
|
||||
</dialogHostAvalonia:DialogHost>
|
||||
</Window>
|
13
DiscordChatExporter.Gui/Views/MainView.axaml.cs
Normal file
13
DiscordChatExporter.Gui/Views/MainView.axaml.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
using Avalonia.Interactivity;
|
||||
using DiscordChatExporter.Gui.Framework;
|
||||
using DiscordChatExporter.Gui.ViewModels;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Views;
|
||||
|
||||
public partial class MainView : Window<MainViewModel>
|
||||
{
|
||||
public MainView() => InitializeComponent();
|
||||
|
||||
private void DialogHost_OnLoaded(object? sender, RoutedEventArgs args) =>
|
||||
DataContext.InitializeCommand.Execute(null);
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
<Window
|
||||
x:Class="DiscordChatExporter.Gui.Views.RootView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="clr-namespace:DiscordChatExporter.Gui.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
xmlns:viewModels="clr-namespace:DiscordChatExporter.Gui.ViewModels"
|
||||
Width="625"
|
||||
Height="575"
|
||||
MinWidth="325"
|
||||
d:DataContext="{d:DesignInstance Type=viewModels:RootViewModel}"
|
||||
Background="{DynamicResource MaterialDesignPaper}"
|
||||
Icon="/DiscordChatExporter;component/favicon.ico"
|
||||
Style="{DynamicResource MaterialDesignRoot}"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
mc:Ignorable="d">
|
||||
<Window.TaskbarItemInfo>
|
||||
<TaskbarItemInfo ProgressState="Normal" ProgressValue="{Binding Dashboard.Progress.Current.Fraction}" />
|
||||
</Window.TaskbarItemInfo>
|
||||
|
||||
<materialDesign:DialogHost
|
||||
x:Name="DialogHost"
|
||||
Loaded="{s:Action OnViewFullyLoaded}"
|
||||
SnackbarMessageQueue="{Binding Notifications}"
|
||||
Style="{DynamicResource MaterialDesignEmbeddedDialogHost}">
|
||||
<Grid IsEnabled="{Binding IsOpen, ElementName=DialogHost, Converter={x:Static converters:InverseBoolConverter.Instance}}">
|
||||
<ContentControl s:View.Model="{Binding Dashboard}" />
|
||||
<materialDesign:Snackbar MessageQueue="{Binding Notifications}" />
|
||||
</Grid>
|
||||
</materialDesign:DialogHost>
|
||||
</Window>
|
|
@ -1,9 +0,0 @@
|
|||
namespace DiscordChatExporter.Gui.Views;
|
||||
|
||||
public partial class RootView
|
||||
{
|
||||
public RootView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue